Compare commits

40 Commits

Author SHA1 Message Date
8a5fe69b5c Add 3d-files for case 2024-07-07 22:01:03 +02:00
e730fb9ec7 Remap layer 2,0 slightly.
Add arrow keys and move F-keys
2024-07-03 14:09:44 +02:00
a066b3c5dc Get Compose keys working with proper shifting 2024-06-23 14:43:19 +02:00
5a38ef7f78 Remove deprecated neopixel files 2024-05-29 23:16:28 +02:00
ae599f587a Remove old todos 2024-05-29 23:14:35 +02:00
58b4bc4004 Swap super and alt keys 2024-05-29 23:08:29 +02:00
4a528eb4b7 wip 2024-03-24 16:29:24 +01:00
84f8222b30 Refactor idle animation 2024-02-16 19:11:08 +01:00
9892ae1d70 Refactor keyboarg lights 2024-02-10 14:48:18 +01:00
4f8ed8c10f Rename split_array 2024-02-08 16:29:22 +01:00
800874c653 Flash LEDs on USB events 2024-02-04 17:29:58 +01:00
e185e5524b Update embassy; use crates.io 2024-02-04 15:51:38 +01:00
abb4df15f0 Add naive Compose button impl 2024-02-04 15:28:25 +01:00
7f6971b329 Some cleanup 2023-12-20 17:06:50 +01:00
00b3b51b36 Tweak brightness 2023-12-19 19:31:08 +01:00
7d9cfe9557 Fix the bug that has been bugging me for months
Finally got the little shit :D
2023-12-16 19:56:44 +01:00
fb57b0bdc6 Refactor keyboard timing tests 2023-12-16 19:56:44 +01:00
607e5b02ef Update deps 2023-12-16 19:42:58 +01:00
8066837cc9 Decrease idle animation brightness 2023-08-28 13:55:09 +02:00
f265898dda Increase debounce threshold 2023-08-28 13:55:02 +02:00
e68d25afa8 Remove patch 2023-08-28 13:54:50 +02:00
dcf950cc66 Switch to layer matrix instead of layer list 2023-07-21 16:18:47 +02:00
2c239b6248 Add basic swich debouncing. 2023-07-15 00:45:36 +02:00
9854e0c0a8 stuff 2023-06-12 18:18:12 +02:00
59f91e2f6b Add uart Header struct 2023-05-29 17:14:30 +02:00
fa3b1502d7 Add fado out effect to button presses 2023-05-21 17:38:43 +02:00
5c68b483af Use postcard to serialize uart messages 2023-05-21 11:11:28 +02:00
481066a343 Add RTT logging 2023-05-21 10:19:32 +02:00
8a14c13bf1 Update dependencies 2023-05-21 00:57:08 +02:00
c5c27c487e Update layout 2023-05-15 22:07:25 +02:00
78db14f88d Add idle animation 2023-05-15 22:07:15 +02:00
251fe2d79b Refactor light handling to use event messages 2023-05-14 16:01:08 +02:00
2714d549b0 Add light to layer switch key 2023-05-09 18:13:51 +02:00
c39893a067 Add communication between keyboard halves 2023-05-09 18:10:43 +02:00
2b66c022f7 Fix right.rs 2023-05-08 19:43:18 +02:00
4f59a59693 Make keys light up when pressed 2023-04-26 18:39:47 +02:00
2fc050eb4f Update layers-left.ron 2023-04-26 18:39:20 +02:00
05951e1b77 Add serial-logs script 2023-04-26 18:38:54 +02:00
e544e86cfa Swap RX/TX on pcb_right 2023-04-16 15:42:20 +02:00
dd613adf49 Add export files to kicad gitignore 2023-04-16 12:42:29 +02:00
88 changed files with 9229 additions and 1623 deletions

View File

@ -1,35 +0,0 @@
#
# Cargo Configuration for the https://github.com/rp-rs/rp-hal.git repository.
#
# Copyright (c) The RP-RS Developers, 2021
#
# You might want to make a similar file in your own repository if you are
# writing programs for Raspberry Silicon microcontrollers.
#
# This file is MIT or Apache-2.0 as per the repository README.md file
#
[build]
# Set the default target to match the Cortex-M0+ in the RP2040
target = "thumbv6m-none-eabi"
# Target specific options
[target.thumbv6m-none-eabi]
# Pass some extra options to rustc, some of which get passed on to the linker.
#
# * inline-threshold=5 makes the compiler more aggressive and inlining functions
# * no-vectorize-loops turns off the loop vectorizer (seeing as the M0+ doesn't
# have SIMD)
rustflags = [
"-C", "linker=flip-link",
"-C", "inline-threshold=5",
"-C", "no-vectorize-loops",
]
# This runner will make a UF2 file and then copy it to a mounted RP2040 in USB
# Bootloader mode:
runner = "elf2uf2-rs -d"
# This runner will find a supported SWD debug probe and flash your RP2040 over
# SWD:
# runner = "probe-run --chip RP2040"

12
.cargo/config.toml Normal file
View File

@ -0,0 +1,12 @@
[build]
#target = "thumbv6m-none-eabi"
[target.thumbv6m-none-eabi]
rustflags = [
"-C", "linker=flip-link",
"-C", "inline-threshold=5", # inline functions more aggressively
"-C", "no-vectorize-loops",
]
#runner = "elf2uf2-rs -d"
runner = "probe-rs run --chip RP2040"

3
.gitignore vendored
View File

@ -1 +1,2 @@
/target target
*.pc

4377
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,41 +1,10 @@
[package] [workspace]
name = "tangentbord1" members = [
version = "0.1.0" "lib",
authors = ["Joakim Hulthe <joakim@hulthe.net>"] "left",
description = "Keyboard firmware" "right",
edition = "2021" "editor",
]
[dependencies] resolver = "2"
tgnt = { git = "https://git.nubo.sh/hulthe/tgnt.git", default-features = false }
cortex-m = "0.7.6"
cortex-m-rt = "0.7"
embedded-hal = "0.2.5"
#panic-halt = "0.2.0"
usb-device = "0.2.9"
usbd-hid = "0.6.1"
static_cell = "1.0.0"
embedded-io = { version = "*", features = ["async"] }
futures = { version = "0.3", default-features = false }
embassy-executor = { git = "https://github.com/embassy-rs/embassy.git", features = ["log", "nightly", "integrated-timers" ] }
embassy-sync = { git = "https://github.com/embassy-rs/embassy.git", features = ["log", "nightly"] }
embassy-time = { git = "https://github.com/embassy-rs/embassy.git", features = ["log"] }
embassy-futures = { git = "https://github.com/embassy-rs/embassy.git", features = ["log"] }
embassy-usb = { git = "https://github.com/embassy-rs/embassy.git", features = ["usbd-hid"] }
embassy-usb-logger = { git = "https://github.com/embassy-rs/embassy.git", features = [] }
embassy-usb-driver = { git = "https://github.com/embassy-rs/embassy.git", features = [] }
embassy-rp = { git = "https://github.com/embassy-rs/embassy.git", features = ["log", "nightly", "unstable-traits", "unstable-pac", "time-driver", "pio", "critical-section-impl"] }
log = "0.4.17"
pio = "0.2.1"
pio-proc = "0.2.1"
smart-leds = "0.3.0"
embedded-alloc = "0.5.0"
postcard = { version = "1.0.4", features = ["alloc"] }
[build-dependencies]
tgnt = { git = "https://git.nubo.sh/hulthe/tgnt.git", default-features = false }
ron = "0.8.0"
postcard = { version = "1", features = ["use-std"] }
[features]
default = ["n-key-rollover"]
n-key-rollover=[]

BIN
case/backplate.stl Normal file

Binary file not shown.

BIN
case/frontplate.stl Normal file

Binary file not shown.

BIN
case/keycap-filled.stl Normal file

Binary file not shown.

BIN
case/keycap.stl Normal file

Binary file not shown.

BIN
case/leg-long.stl Normal file

Binary file not shown.

BIN
case/leg.stl Normal file

Binary file not shown.

BIN
case/thumb-backplate.stl Normal file

Binary file not shown.

BIN
case/thumb-frontplate.stl Normal file

Binary file not shown.

2
editor/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
/Cargo.lock

22
editor/Cargo.toml Normal file
View File

@ -0,0 +1,22 @@
[package]
name = "editor"
version = "0.1.0"
edition = "2021"
[dependencies.tangentbord1]
path = "../lib"
package = "tangentbord1-lib"
features = ["std"]
[dependencies]
clap = { version = "4.5.1", features = ["derive"] }
eframe = { version = "0.26.2", features = ["persistence", "ron", "serde"] }
egui = { version = "0.26.2", features = ["persistence", "ron", "serde"] }
egui_extras = { version = "0.26.2" }
msgpck = { version = "0.2.8", features = ["alloc"] }
eyre = "0.6.12"
log = "0.4.20"
pretty_env_logger = "0.5.0"
ron = { version = "0.8.0" }
serde = { version = "1.0.156", features = ["derive"] }
tokio = { version = "1.36.0", features = ["full"] }

3
editor/README.md Normal file
View File

@ -0,0 +1,3 @@
# tgnt
Keyboard configuration library and editor.

View File

@ -0,0 +1,25 @@
Layer(
buttons: [
Keycode(0x04),
Keycode(0x05),
Keycode(0x06),
Keycode(0x07),
Keycode(0x08),
Keycode(0x09),
Keycode(0x0A),
Keycode(0x0B),
Keycode(0x0C),
Keycode(0x0D),
Keycode(0x0E),
Keycode(0x0F),
Keycode(0x10),
Keycode(0x11),
ModTap( keycode: 0x12, modifier: LMod),
Modifier(LShift),
Modifier(LCtrl),
NextLayer,
],
)

View File

@ -0,0 +1,25 @@
Layout(
buttons: [
(x: 0.0, y: 0.4),
(x: 1.0, y: 0.2),
(x: 2.0, y: 0.0),
(x: 3.0, y: 0.2),
(x: 4.0, y: 0.4),
(x: 0.0, y: 1.4),
(x: 1.0, y: 1.2),
(x: 2.0, y: 1.0),
(x: 3.0, y: 1.2),
(x: 4.0, y: 1.4),
(x: 0.0, y: 2.4),
(x: 1.0, y: 2.2),
(x: 2.0, y: 2.0),
(x: 3.0, y: 2.2),
(x: 4.0, y: 2.4),
(x: 3.0, y: 3.2),
(x: 4.0, y: 3.4),
(x: 5.0, y: 3.6),
]
)

395
editor/src/main.rs Normal file
View File

@ -0,0 +1,395 @@
mod mat;
mod serial;
use clap::Parser;
use egui::{Button, Color32, Frame, Rect, ScrollArea, Slider, TextEdit, Vec2};
use eyre::eyre;
use mat::Mat;
use serde::{Deserialize, Serialize};
use serial::{connect_to_serial, scan_for_serial};
use std::{collections::VecDeque, path::PathBuf};
use tangentbord1::{
layer::Layer,
layout::Layout,
serial_proto::owned::{ChangeLayer, DeviceMsg, LogRecord},
};
use tokio::{
runtime::Runtime,
sync::{
mpsc::{self, Receiver},
oneshot::{self, error::TryRecvError},
},
task::spawn_blocking,
};
#[derive(Parser)]
struct Opt {
/// Path to device serial port
device: Option<PathBuf>,
}
fn main() -> eyre::Result<()> {
let _opt = Opt::parse();
pretty_env_logger::init();
let rt = Runtime::new().unwrap();
let _enter_rt = rt.enter();
log::info!("starting application");
let native_options = eframe::NativeOptions::default();
eframe::run_native(
"tgnt keyboard editor",
native_options,
Box::new(|cc| Box::new(App::new(cc))),
)
.map_err(|e| eyre!("Failed to run program: {e:?}"))?;
rt.shutdown_background();
Ok(())
}
/// We derive Deserialize/Serialize so we can persist app state on shutdown.
#[derive(Deserialize, Serialize)]
#[serde(default)] // if we add new fields, give them default values when deserializing old state
pub struct App {
layout: RonEdit<Layout>,
layers: Mat<RonEdit<Layer>>,
u1: f32,
margin: f32,
#[serde(skip)]
serial: SerialState,
}
struct SerialState {
scan_task: Option<oneshot::Receiver<Result<Option<PathBuf>, String>>>,
dev: Result<Option<PathBuf>, String>,
reader: Option<Receiver<DeviceMsg>>,
logs: VecDeque<LogRecord>,
active_layer: Option<(u16, u16)>,
}
#[derive(Deserialize, Serialize, Clone)]
struct RonEdit<T> {
pub t: T,
pub ron: String,
pub error: String,
}
impl<T: Serialize> RonEdit<T> {
pub fn new(t: T) -> Self {
RonEdit {
ron: ron::ser::to_string_pretty(&t, Default::default()).unwrap(),
error: String::new(),
t,
}
}
}
impl<T: Default + Serialize> Default for RonEdit<T> {
fn default() -> Self {
RonEdit::new(Default::default())
}
}
impl Default for App {
fn default() -> Self {
Self {
layers: Default::default(),
layout: Default::default(),
u1: 75.0,
margin: 0.05,
serial: Default::default(),
}
}
}
impl Default for SerialState {
fn default() -> Self {
Self {
scan_task: Default::default(),
dev: Ok(None),
reader: Default::default(),
logs: Default::default(),
active_layer: None,
}
}
}
impl App {
/// Called once before the first frame.
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
// This is also where you can customize the look and feel of egui using
// `cc.egui_ctx.set_visuals` and `cc.egui_ctx.set_fonts`.
// Load previous app state (if any).
// Note that you must enable the `persistence` feature for this to work.
if let Some(storage) = cc.storage {
return eframe::get_value(storage, eframe::APP_KEY).unwrap_or_default();
}
let layer_str = include_str!("default_layer.ron");
let layer: Layer = ron::from_str(&layer_str).expect("Failed to deserialize default layer");
let layout_str = include_str!("default_layout.ron");
let layout: Layout =
ron::from_str(&layout_str).expect("Failed to deserialize default layout");
let mut layers = Mat::default();
layers.push_row(RonEdit::new(layer));
Self {
layers,
layout: RonEdit::new(layout),
..Self::default()
}
}
}
impl eframe::App for App {
/// Called by the frame work to save state before shutdown.
fn save(&mut self, storage: &mut dyn eframe::Storage) {
eframe::set_value(storage, eframe::APP_KEY, self);
}
/// Called each time the UI needs repainting, which may be many times per second.
/// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`.
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
let Self {
margin,
u1,
layout,
layers,
serial:
SerialState {
scan_task: scan_serial_task,
dev: serial_devs,
reader: serial_reader,
logs: serial_logs,
active_layer,
},
} = self;
while let Some(rx) = scan_serial_task {
match rx.try_recv() {
Ok(r) => {
*serial_devs = r;
*scan_serial_task = None;
}
Err(TryRecvError::Empty) => break,
Err(TryRecvError::Closed) => {
*scan_serial_task = None;
break;
}
}
}
while let Some(rx) = serial_reader {
match rx.try_recv() {
Ok(DeviceMsg::Log(record)) => {
serial_logs.push_back(record);
if serial_logs.len() > 10 {
serial_logs.pop_front();
}
}
Ok(DeviceMsg::ChangeLayer(ChangeLayer { x, y })) => {
*active_layer = Some((x, y));
}
Ok(_) => {}
Err(mpsc::error::TryRecvError::Empty) => break,
Err(mpsc::error::TryRecvError::Disconnected) => {
*serial_reader = None;
break;
}
}
}
#[cfg(not(target_arch = "wasm32"))] // no File->Quit on web pages!
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
// The top panel is often a good place for a menu bar:
egui::menu::bar(ui, |ui| {
ui.menu_button("File", |ui| {
if ui.button("Quit").clicked() {
todo!("implement quit")
}
});
});
});
egui::SidePanel::left("side_panel")
.resizable(true)
.width_range(250.0..=500.0)
.show(ctx, |ui| {
ScrollArea::vertical().show(ui, |ui| {
ui.heading("Side Panel");
ui.collapsing("Serial", |ui| {
match serial_devs {
Ok(Some(dev)) => {
if serial_reader.is_none()
&& ui.button(format!("Connect to {dev:?}")).clicked()
{
*serial_reader =
Some(connect_to_serial(dev.clone(), ctx.clone()));
};
}
Ok(None) => {
ui.label("No devices found.");
}
Err(e) => {
ui.code_editor(e);
}
}
if ui.button("scan for serial device").clicked()
&& scan_serial_task.is_none()
{
let (tx, rx) = oneshot::channel();
let ctx = ctx.clone();
spawn_blocking(move || {
let r = scan_for_serial().map_err(|e| e.to_string());
let _ = tx.send(r);
ctx.request_repaint();
});
*scan_serial_task = Some(rx);
}
if scan_serial_task.is_some() {
ui.label("Scanning...");
}
ScrollArea::both().show(ui, |ui| {
for log in &*serial_logs {
ui.group(|ui| {
ui.horizontal(|ui| {
ui.label(&log.level);
ui.label(&log.message);
});
});
}
})
});
ui.label("u1");
ui.add(Slider::new(u1, 20.0..=150.0));
ui.label("margin");
ui.add(Slider::new(margin, 0.0..=0.4));
ui.collapsing("Layout", |ui| {
if ui
.add(TextEdit::multiline(&mut layout.ron).code_editor())
.changed()
{
layout.error.clear();
match ron::from_str(&layout.ron) {
Ok(new) => layout.t = new,
Err(e) => layout.error = e.to_string(),
}
}
if !layout.error.is_empty() {
ui.add(
TextEdit::multiline(&mut layout.error)
.interactive(false)
.text_color(Color32::RED),
);
}
});
for x in 0..layers.width() {
for y in 0..layers.height() {
let layer = layers.get_mut(x, y).unwrap();
ui.collapsing(format!("Layer {x},{y}"), |ui| {
if ui
.add(TextEdit::multiline(&mut layer.ron).code_editor())
.changed()
{
layer.error.clear();
match ron::from_str(&layer.ron) {
Ok(new) => layer.t = new,
Err(e) => layer.error = e.to_string(),
}
}
if !layer.error.is_empty() {
ui.add(
TextEdit::multiline(&mut layer.error)
.interactive(false)
.text_color(Color32::RED),
);
}
});
}
}
if ui.button("Add layer row").clicked() {
layers.push_row(RonEdit::default());
}
ui.menu_button("Delete layer row", |ui| {
for r in 0..layers.height() {
if ui.button(format!("row {r}")).clicked() {
layers.remove_row(r);
}
}
});
if ui.button("Add layer column").clicked() {
layers.push_col(RonEdit::default());
}
ui.menu_button("Delete layer column", |ui| {
for c in 0..layers.width() {
if ui.button(format!("column {c}")).clicked() {
layers.remove_col(c);
}
}
});
});
});
egui::CentralPanel::default().show(ctx, |ui| {
ScrollArea::both().show(ui, |ui| {
for (i, layer) in layers.iter_cf().enumerate() {
let x = (i / layers.height()) as u16;
let y = (i % layers.height()) as u16;
if Some((x, y)) == *active_layer {
ui.visuals_mut().widgets.inactive.bg_stroke =
(1.0, Color32::DARK_GREEN).into();
}
Frame::none().show(ui, |ui| {
for (i, geometry) in layout.t.buttons.iter().enumerate() {
let margin = *margin * *u1;
let size = Vec2::new(
*u1 * geometry.w - margin * 2.0,
*u1 * geometry.h - margin * 2.0,
);
let offset =
Vec2::new(*u1 * geometry.x + margin, *u1 * geometry.y + margin);
let rect = Rect::from_min_size(ui.min_rect().min + offset, size);
let button = if let Some(button) = layer.t.buttons.get(i) {
Button::new(button.to_string())
} else {
Button::new("")
};
ui.put(rect, button);
}
});
ui.visuals_mut().widgets = Default::default();
ui.separator();
}
})
});
}
}

98
editor/src/mat.rs Normal file
View File

@ -0,0 +1,98 @@
use serde::{Deserialize, Serialize};
/// 2-dimensional dynamically sized matrix
#[derive(Default, Deserialize, Serialize)]
pub struct Mat<T> {
rows: Vec<Vec<T>>,
}
#[allow(dead_code)]
impl<T> Mat<T> {
/// Iterater over entries, rows first.
pub fn iter_rf(&self) -> impl Iterator<Item = &T> {
todo!();
#[allow(unreachable_code)]
[].into_iter()
}
/// Iterater over entries, columns first.
pub fn iter_cf(&self) -> impl Iterator<Item = &T> {
let height = self.height();
(0..self.width())
.flat_map(move |x| (0..height).map(move |y| (x, y)))
.map(|(x, y)| &self.rows[y][x])
}
pub fn get_mut(&mut self, x: usize, y: usize) -> Option<&mut T> {
if x >= self.width() || y >= self.height() {
return None;
}
Some(&mut self.rows[y][x])
}
/// Get the width & height of the matrix
pub fn height(&self) -> usize {
self.rows.len()
}
/// Get the width & height of the matrix
pub fn width(&self) -> usize {
self.rows.get(0).map(|row| row.len()).unwrap_or(0)
}
/// Get the width & height of the matrix
pub fn size(&self) -> (usize, usize) {
(self.width(), self.height())
}
pub fn push_row(&mut self, t: T)
where
T: Clone,
{
let width = self.width();
if width == 0 {
self.rows.push(vec![t]);
} else {
self.rows.push(vec![t; width])
}
}
pub fn push_col(&mut self, t: T)
where
T: Clone,
{
let height = self.height();
if height == 0 {
self.rows.push(vec![t]);
} else {
self.rows.iter_mut().for_each(|row| row.push(t.clone()));
}
}
pub fn remove_row(&mut self, y: usize) {
if y >= self.height() {
panic!("row {y} out of matrix bounds");
}
if self.height() == 1 {
self.rows.clear();
} else {
self.rows.remove(y);
}
}
pub fn remove_col(&mut self, x: usize) {
if x >= self.width() {
panic!("col {x} out of matrix bounds");
}
if self.width() == 1 {
self.rows.clear();
} else {
for row in self.rows.iter_mut() {
row.remove(x);
}
}
}
}

5
editor/src/serial.rs Normal file
View File

@ -0,0 +1,5 @@
mod port;
mod scan;
pub use port::connect_to_serial;
pub use scan::scan_for_serial;

144
editor/src/serial/port.rs Normal file
View File

@ -0,0 +1,144 @@
use std::{future::pending, path::PathBuf, time::Duration};
use egui::Context;
use eyre::{bail, Context as EyreContext};
use msgpck::{MsgPack, MsgUnpack, UnpackErr};
use tangentbord1::serial_proto::owned::{DeviceMsg, HostMsg};
use tokio::{
fs::File,
io::{AsyncReadExt, AsyncWriteExt},
process::Command,
select,
sync::mpsc::{self, Receiver, Sender},
time::{sleep, Instant},
};
const MAX_MESSAGE_SIZE: usize = 16 * 1024;
const MESSAGE_TIMEOUT: Duration = Duration::from_millis(30);
pub fn connect_to_serial(dev: PathBuf, ctx: Context) -> Receiver<DeviceMsg> {
let (tx, rx) = mpsc::channel(12);
tokio::spawn(async {
if let Err(e) = read_serial(dev, tx, ctx).await {
log::error!("serial read task exited with error: {e:#?}");
}
});
rx
}
async fn read_serial(dev: PathBuf, tx: Sender<DeviceMsg>, ctx: Context) -> eyre::Result<()> {
log::debug!("configuring keyboard serial device");
let out = Command::new("stty")
.arg("-F")
.arg(&dev)
.args(["115200", "raw", "-clocal", "-echo"])
.output()
.await
.wrap_err("failed to configure serial device, couldn't execute stty")?;
if !out.status.success() {
bail!("failed to configure serial device");
}
log::debug!("opening keyboard serial device");
let mut file = File::options()
.create(false)
.read(true)
.append(true)
.open(dev)
.await?;
log::debug!("requesting keyboard layers");
for p in HostMsg::GetLayers.pack() {
log::debug!("packing {:x?}", p);
file.write_all(p.as_bytes()).await?;
}
let mut buf = Vec::with_capacity(MAX_MESSAGE_SIZE);
let mut last_read = Instant::now();
loop {
// if buffer is not empty, this future will sleep until the pending message times out
let timeout = async {
if buf.is_empty() {
pending().await
} else {
let timeout_at = last_read + MESSAGE_TIMEOUT;
sleep(Instant::now() - timeout_at).await;
}
};
// try to read some bytes from the file
let mut tmp = [0u8; 1024];
let n = select! {
n = file.read(&mut tmp) => n?,
// need to continuously poll read if nothing is happening
_ = sleep(Duration::from_millis(20)) => continue,
_ = timeout => {
log::warn!("message timeout, clearing buffer");
buf.clear();
continue;
}
};
// exit on eof
if n == 0 {
break;
}
last_read = Instant::now();
buf.extend_from_slice(&tmp[..n]);
// make sure we're not just reading garbage forever
if buf.len() > MAX_MESSAGE_SIZE {
log::warn!("max message size exceeded");
buf.clear();
continue;
}
// try to parse messages from the read bytes
loop {
let mut reader = &mut &buf[..];
let record = match DeviceMsg::unpack(&mut reader) {
Ok(r) => r,
// we probably have not gotten the entire message yet, go back to reading bytes.
// if the message is corrupted, we will eventually hit MESSAGE_TIMEOUT or
// MAX_MESSAGE_SIZE.
Err(UnpackErr::UnexpectedEof) => break,
// on any other error, the message is corrupt. clear the buffer.
Err(e) => {
log::warn!("corrupt message: {e:?}");
buf.clear();
break;
}
};
// remove the decoded bytes from buf
if reader.is_empty() {
buf.clear();
} else {
let bytes_read = buf.len() - reader.len();
buf.rotate_left(bytes_read);
buf.truncate(buf.len() - bytes_read);
}
if let Err(_) = tx.send(record).await {
log::info!("channel closed, closing serial thingy");
return Ok(());
}
// if there are no more bytes, stop trying to decode messages.
if buf.is_empty() {
break;
}
}
ctx.request_repaint();
}
Ok(())
}

141
editor/src/serial/scan.rs Normal file
View File

@ -0,0 +1,141 @@
use std::{
collections::HashMap,
fs::read_dir,
path::{Path, PathBuf},
process::Command,
str,
};
use eyre::Context;
/// Scan for the keyboard serial device
pub fn scan_for_serial() -> eyre::Result<Option<PathBuf>> {
log::info!("scanning for keyboard serial device");
let mut syspaths = vec![];
// Reqursively scan all "/sys/bus/usb/devices/usb*" folders for files called "dev"
// and get the paths to the parent folders of those "dev" files.
for f in read_dir("/sys/bus/usb/devices/")? {
let f = f?;
let file_name = f.file_name();
let Some(name) = file_name.to_str() else {
continue;
};
if !name.starts_with("usb") {
continue;
}
let mut paths = scan_usb_for_devs(&f.path())?;
syspaths.append(&mut paths);
}
log::debug!("checking these devices: {syspaths:#?}");
for syspath in syspaths {
let syspath = syspath.to_string_lossy();
let devname = Command::new("udevadm")
.args(["info", "-q", "name", "-p", &syspath])
.output()
.wrap_err("failed to run udevadmn to query dev name")?;
let devname = str::from_utf8(&devname.stdout)
.wrap_err("failed to parse udevadm output as utf-8")?
.trim();
let devname = Path::new(devname);
// Ignore USB hubs and such
if devname.starts_with("bus/") {
continue;
}
let devpath = Path::new("/dev").join(devname);
let properties = Command::new("udevadm")
.args(["info", "-q", "property", "--export", "-p", &syspath])
.output()
.wrap_err("failed to run udevadmn to query properities")?;
let properties = str::from_utf8(&properties.stdout)
.wrap_err("failed to parse udevadm output as utf-8")?;
let properties = parse_env(properties);
let Some(properties) = parse_properties(&properties) else {
continue;
};
log::debug!("{devpath:?}: {properties:#?}");
let is_serial_device = [
properties.model == "Tangentbord1",
properties.vendor == "Tux",
properties.vendor_id == "b00b",
properties.usb_type == "generic",
properties.usb_driver == "cdc_acm",
]
.into_iter()
.all(|b| b);
if is_serial_device {
return Ok(Some(devpath));
}
}
Ok(None)
}
#[allow(dead_code)]
#[derive(Debug)]
struct Properties<'a> {
model: &'a str,
serial: &'a str,
serial_short: &'a str,
vendor: &'a str,
vendor_id: &'a str,
usb_type: &'a str,
usb_driver: &'a str,
}
fn parse_properties<'a>(properties: &HashMap<&'a str, &'a str>) -> Option<Properties<'a>> {
Some(Properties {
model: *properties.get("ID_MODEL")?,
serial: *properties.get("ID_SERIAL")?,
serial_short: *properties.get("ID_SERIAL_SHORT")?,
vendor: *properties.get("ID_VENDOR")?,
vendor_id: *properties.get("ID_VENDOR_ID")?,
usb_type: *properties.get("ID_USB_TYPE")?,
usb_driver: *properties.get("ID_USB_DRIVER")?,
})
}
fn parse_env(s: &str) -> HashMap<&str, &str> {
s.lines()
.filter_map(|line| {
let (key, val) = line.split_once('=')?;
let val = val.trim_matches('\'');
Some((key, val))
})
.collect()
}
fn scan_usb_for_devs(p: &Path) -> eyre::Result<Vec<PathBuf>> {
let mut out = vec![];
for f in read_dir(p)? {
let f = f?;
let meta = f.metadata()?;
let path = f.path();
if meta.is_dir() {
let mut results = scan_usb_for_devs(&path)?;
out.append(&mut results);
} else if meta.is_file() && f.file_name() == "dev" {
if let Some(parent) = path.parent() {
out.push(parent.to_owned());
}
}
}
Ok(out)
}

View File

@ -1,31 +0,0 @@
[
Layer(
buttons: [
// Row 1
Key(Apostrophe),
Key(Comma),
Key(Period),
Key(P),
Key(Y),
// Row 2
Key(A),
Key(O),
Key(E),
Key(U),
Key(I),
// Row 3
Key(Colon),
Key(Q),
Key(J),
Key(K),
Key(X),
// Thumbpad
Mod(LShift),
Mod(LCtrl),
NextLayer,
],
)
]

View File

@ -1,31 +0,0 @@
[
Layer(
buttons: [
// Row 1
Key(F),
Key(G),
Key(C),
Key(R),
Key(L),
// Row 2
Key(D),
Key(H),
Key(T),
Key(N),
Key(S),
// Row 3
Key(B),
Key(M),
Key(W),
Key(V),
Key(Z),
// Thumbpad
PrevLayer,
Mod(RAlt),
Mod(RMod),
],
)
]

2
left/.cargo/config.toml Normal file
View File

@ -0,0 +1,2 @@
[build]
target = "thumbv6m-none-eabi"

28
left/Cargo.toml Normal file
View File

@ -0,0 +1,28 @@
[package]
name = "tangentbord1-left"
version = "0.1.0"
authors = ["Joakim Hulthe <joakim@hulthe.net>"]
description = "Keyboard firmware"
edition = "2021"
[dependencies.tangentbord1]
path = "../lib"
package = "tangentbord1-lib"
[dependencies]
cortex-m-rt = "0.7"
embassy-rp = { version = "0.1.0", features = ["time-driver", "critical-section-impl"] }
embassy-executor = { version = "0.5.0", features = ["nightly", "executor-thread", "integrated-timers", "arch-cortex-m"] }
embassy-sync = "0.5.0"
embassy-time = "0.3.0"
embassy-futures = "0.1.1"
log = "0.4.17"
postcard = { version = "1.0.4", features = ["alloc"] }
[build-dependencies]
ron = "0.8.0"
postcard = { version = "1", features = ["use-std"] }
[build-dependencies.tangentbord1]
path = "../lib"
package = "tangentbord1-lib"

View File

@ -13,12 +13,11 @@ use std::io::Write;
use std::path::PathBuf; use std::path::PathBuf;
use std::{env, fs}; use std::{env, fs};
use tgnt::layer::Layer; use tangentbord1::layer::Layer;
fn main() { fn main() {
memory(); memory();
serialize_layout("./layers-left.ron", "./src/bin/layers-left.pc"); serialize_layout("./layers.ron", "./src/layers.pc");
serialize_layout("./layers-right.ron", "./src/bin/layers-right.pc");
} }
fn memory() { fn memory() {
@ -27,7 +26,7 @@ fn memory() {
let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap()); let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap());
File::create(out.join("memory.x")) File::create(out.join("memory.x"))
.unwrap() .unwrap()
.write_all(include_bytes!("memory.x")) .write_all(include_bytes!("../memory.x"))
.unwrap(); .unwrap();
println!("cargo:rustc-link-search={}", out.display()); println!("cargo:rustc-link-search={}", out.display());
@ -35,7 +34,7 @@ fn memory() {
// any file in the project changes. By specifying `memory.x` // any file in the project changes. By specifying `memory.x`
// here, we ensure the build script is only re-run when // here, we ensure the build script is only re-run when
// `memory.x` is changed. // `memory.x` is changed.
println!("cargo:rerun-if-changed=memory.x"); println!("cargo:rerun-if-changed=../memory.x");
// --nmagic turns off page alignment of sections (which saves flash space) // --nmagic turns off page alignment of sections (which saves flash space)
println!("cargo:rustc-link-arg-bins=--nmagic"); println!("cargo:rustc-link-arg-bins=--nmagic");
@ -47,9 +46,10 @@ fn memory() {
fn serialize_layout(ron_path: &str, postcard_path: &str) { fn serialize_layout(ron_path: &str, postcard_path: &str) {
println!("cargo:rerun-if-changed={ron_path}"); println!("cargo:rerun-if-changed={ron_path}");
println!("cargo:rerun-if-changed={postcard_path}");
let layers = fs::read_to_string(ron_path).expect("Failed to read .ron"); let layers = fs::read_to_string(ron_path).expect("Failed to read .ron");
let layers: Vec<Layer> = ron::from_str(&layers).expect("Failed to deserialize .ron"); let layers: Vec<Vec<Layer>> = ron::from_str(&layers).expect("Failed to deserialize .ron");
let serialized = postcard::to_stdvec(&layers).expect("Failed to serialize layers"); let serialized = postcard::to_stdvec(&layers).expect("Failed to serialize layers");

180
left/layers.ron Normal file
View File

@ -0,0 +1,180 @@
[
[
Layer(
buttons: [
// Row 1
Key(Escape),
Key(Comma),
Key(Period),
Key(P),
Key(Y),
// Row 2
ModTap(A, LAlt),
ModTap(O, LMod),
ModTap(E, LShift),
ModTap(U, LCtrl),
Key(I),
// Row 3
Key(Colon),
Key(Q),
Key(J),
Key(K),
Key(X),
// Thumbpad
Key(Backspace),
Key(Space),
Layer(Peek, Right, 1),
],
),
Layer(
buttons: [
// Row 1
Key(Escape),
Key(Slash),
Key(Equal),
Key(Accent),
Key(Period),
// Row 2
ModTap(BackslashPipe, LAlt),
ModTap(Dash, LMod),
ModTap(LBracket, LShift),
ModTap(RBracket, LCtrl),
Key(Tab),
// Row 3
None,
None,
None,
Key(Apostrophe),
None,
// Thumbpad
Key(Backspace),
Key(Space),
Layer(Peek, Right, 1),
],
),
Layer(
buttons: [
// Row 1
Key(F1),
Key(F4),
Key(F7),
Key(F10),
None,
// Row 2
Key(F2),
Key(F5),
Key(F8),
Key(F11),
None,
// Row 3
Key(F3),
Key(F6),
Key(F9),
Key(F12),
None,
// Thumbpad
Key(Backspace),
Key(Space),
Layer(Peek, Right, 1),
],
),
],
[ // gaming row
Layer(
buttons: [
// Row 1
Key(Escape),
Key(Q),
Key(W),
Key(E),
Key(R),
// Row 2
Mod(LShift),
Key(A),
Key(S),
Key(D),
Key(F),
// Row 3
Mod(LCtrl),
Key(Z),
Key(X),
Key(C),
Key(V),
// Thumbpad
Key(Backspace),
Key(Space),
Layer(Peek, Right, 1),
],
),
Layer(
buttons: [
// Row 1
Key(D1),
Key(D2),
Key(D3),
Key(D4),
Key(D5),
// Row 2
Key(D6),
Key(D7),
Key(D8),
Key(D9),
Key(D0),
// Row 3
None,
None,
None,
None,
None,
// Thumbpad
Key(Backspace),
Key(Space),
Layer(Peek, Right, 1),
],
),
Layer(
buttons: [
// Row 1
None,
None,
None,
None,
None,
// Row 2
None,
None,
None,
None,
None,
// Row 3
None,
None,
None,
None,
None,
// Thumbpad
Key(Backspace),
Key(Space),
Layer(Peek, Right, 1),
],
),
],
]

117
left/src/main.rs Normal file
View File

@ -0,0 +1,117 @@
//! Firmware for Tangentbord1, left half.
#![no_std]
#![no_main]
#![feature(type_alias_impl_trait)]
#![cfg(target_arch = "arm")]
extern crate alloc;
extern crate cortex_m_rt;
use alloc::vec::Vec;
use embassy_executor::Spawner;
use embassy_rp::gpio::{Level, Output, Pin};
use embassy_time::Timer;
use log::error;
use tangentbord1::{
board::Board,
event::Half,
interrupts::Irqs,
keyboard::KeyboardConfig,
layer::Layer,
logger::LogMultiplexer,
rgb::Rgb,
util::stall,
ws2812::Ws2812,
{allocator, rtt, uart, usb},
};
#[embassy_executor::main]
async fn main(_spawner: Spawner) {
let half = Half::Left;
let rtt_write = rtt::init_rtt_logger();
allocator::init();
let p = embassy_rp::init(Default::default());
let board = Board::from(p);
let _neopixel_power = Output::new(board.neopixel_power, Level::High);
let mut neopixel = Ws2812::new(board.PIO0, Irqs, board.DMA_CH0, board.neopixel);
let neopixels_d5 = Ws2812::new(board.PIO1, Irqs, board.DMA_CH1, board.d5);
neopixel.write(&[Rgb::new(0xFF, 0x00, 0x00)]).await;
let layers = include_bytes!("layers.pc");
let Ok(layers): Result<Vec<Vec<Layer>>, _> = postcard::from_bytes(layers) else {
log::error!("Failed to deserialize layer config");
stall().await
};
let keyboard = KeyboardConfig {
half,
pins: [
// row 1
board.d24.degrade(),
board.a3.degrade(),
board.a2.degrade(),
board.a1.degrade(),
board.a0.degrade(),
// row 2
board.d25.degrade(),
board.sck.degrade(),
board.mosi.degrade(),
board.miso.degrade(),
board.d2.degrade(),
// row 3
board.d12.degrade(),
board.d11.degrade(),
board.d10.degrade(),
board.d9.degrade(),
board.d3.degrade(),
// thumbpad
board.d7.degrade(),
board.scl.degrade(),
board.sda.degrade(),
],
// the index of the LEDs is different than the switch index.
// each number is the index of the LED for the switch of that index.
led_map: [4, 3, 2, 1, 0, 5, 6, 7, 8, 9, 14, 13, 12, 11, 10, 15, 16, 17],
led_driver: neopixels_d5,
layers,
};
let Some([events1, events2]) = keyboard.create().await else {
error!("failed to create keyboard");
return;
};
uart::start(board.tx, board.rx, board.UART0, half, events2).await;
neopixel.write(&[Rgb::new(0x00, 0x99, 0x99)]).await;
let usb_logger = usb::setup_logger_and_keyboard(board.USB, events1).await;
let logger = LogMultiplexer {
outputs: [rtt_write, usb_logger],
};
logger.init();
log::error!("log_level: error");
log::warn!("log_level: warn");
log::info!("log_level: info");
log::debug!("log_level: debug");
log::trace!("log_level: trace");
neopixel.write(&[Rgb::new(0x00, 0x00, 0xFF)]).await;
Timer::after_secs(5).await;
for b in (0u8..0xff).rev() {
neopixel.write(&[Rgb::new(0, 0, b)]).await;
Timer::after_millis(10).await;
}
stall().await
}

2
lib/.cargo/config.toml Normal file
View File

@ -0,0 +1,2 @@
[build]
target = "thumbv6m-none-eabi"

62
lib/Cargo.toml Normal file
View File

@ -0,0 +1,62 @@
[package]
name = "tangentbord1-lib"
version = "0.1.0"
authors = ["Joakim Hulthe <joakim@hulthe.net>"]
description = "Keyboard firmware"
edition = "2021"
[dependencies]
#tgnt = { git = "https://git.nubo.sh/hulthe/tgnt.git", default-features = false }
cortex-m = "0.7.6"
cortex-m-rt = "0.7"
embedded-hal = "0.2.5"
static_cell = "1.0.0"
embedded-io-async = "*"
futures = { version = "0.3", default-features = false, features = ["async-await"] }
embassy-executor = { version = "0.5.0", features = ["nightly", "nightly", "executor-thread", "integrated-timers"] }
embassy-sync = "0.5.0"
embassy-time = "0.3.0"
embassy-futures = "0.1.1"
log = "0.4.17"
pio = "0.2.1"
pio-proc = "0.2.1"
smart-leds = "0.3.0"
embedded-alloc = "0.5.0"
postcard = { version = "1.0.4", features = ["alloc"] }
fixed = "1.23.1"
rtt-target = "0.4.0"
heapless = "0.7.16"
once_cell = { version = "1.17.1", default-features = false }
critical-section = "1.1.1"
crc-any = "2.4.3"
serde = { version = "1.0.163", default-features = false, features = ["derive"] }
bytemuck = { version = "1.13.1", features = ["derive"] }
libm = "0.2.8"
glam = { version = "0.25.0", default-features = false, features = ["libm"] }
msgpck = { version = "0.2.8", features = ["alloc"] }
#atomic-polyfill = "1.0.2"
portable-atomic = { version = "1.6.0", default-features = false }
[target.'cfg(target_arch = "x86_64")'.dependencies]
embassy-executor = { version = "0.5.0", features = ["arch-std"] }
embassy-time = { version = "0.3.0", features = ["std", "generic-queue"] }
simple_logger = "4"
[target.thumbv6m-none-eabi.dependencies]
embassy-rp = { version = "0.1.0", features = ["time-driver", "critical-section-impl"] }
embassy-executor = { version = "0.5.0", features = ["arch-cortex-m"] }
embassy-usb = { version = "0.1.0", features = ["usbd-hid"] }
embassy-usb-logger = "0.1.0"
embassy-usb-driver = "0.1.0"
usb-device = "0.2.9"
usbd-hid = "0.6.1"
[build-dependencies]
#tgnt = { git = "https://git.nubo.sh/hulthe/tgnt.git", default-features = false }
ron = "0.8.0"
postcard = { version = "1", features = ["use-std"] }
[features]
std = []
default = ["n-key-rollover"]
n-key-rollover=[]

15
lib/memory.x Normal file
View File

@ -0,0 +1,15 @@
MEMORY {
BOOT2 : ORIGIN = 0x10000000, LENGTH = 0x100
FLASH : ORIGIN = 0x10000100, LENGTH = 2048K - 0x100
RAM : ORIGIN = 0x20000000, LENGTH = 256K
}
EXTERN(BOOT2_FIRMWARE)
SECTIONS {
/* ### Boot loader */
.boot2 ORIGIN(BOOT2) :
{
KEEP(*(.boot2));
} > BOOT2
} INSERT BEFORE .text;

View File

@ -4,11 +4,12 @@ use core::mem::MaybeUninit;
use embedded_alloc::Heap; use embedded_alloc::Heap;
pub const HEAP_SIZE: usize = 4096;
#[global_allocator] #[global_allocator]
static HEAP: Heap = Heap::empty(); static HEAP: Heap = Heap::empty();
pub fn init() { pub fn init() {
const HEAP_SIZE: usize = 4096;
static mut HEAP_MEM: [MaybeUninit<u8>; HEAP_SIZE] = [MaybeUninit::uninit(); HEAP_SIZE]; static mut HEAP_MEM: [MaybeUninit<u8>; HEAP_SIZE] = [MaybeUninit::uninit(); HEAP_SIZE];
unsafe { HEAP.init(HEAP_MEM.as_ptr() as usize, HEAP_SIZE) } unsafe { HEAP.init(HEAP_MEM.as_ptr() as usize, HEAP_SIZE) }
} }

28
lib/src/atomics.rs Normal file
View File

@ -0,0 +1,28 @@
use portable_atomic::{AtomicU32, Ordering};
pub struct AtomicCoord {
inner: AtomicU32,
}
impl AtomicCoord {
pub const fn new() -> Self {
AtomicCoord {
inner: AtomicU32::new(0),
}
}
pub fn load(&self, ordering: Ordering) -> (u16, u16) {
let [a, b, c, d] = self.inner.load(ordering).to_ne_bytes();
let x = u16::from_ne_bytes([a, b]);
let y = u16::from_ne_bytes([c, d]);
(x, y)
}
pub fn store(&self, x: u16, y: u16, ordering: Ordering) {
let [a, b] = x.to_ne_bytes();
let [c, d] = y.to_ne_bytes();
let xy = u32::from_ne_bytes([a, b, c, d]);
self.inner.store(xy, ordering);
}
}

107
lib/src/button.rs Normal file
View File

@ -0,0 +1,107 @@
use core::fmt::{self, Debug, Display};
use msgpck::{MsgPack, MsgUnpack};
use serde::{Deserialize, Serialize};
use crate::keys::Key;
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, MsgPack, MsgUnpack)]
pub enum Modifier {
LCtrl = 0x01,
LShift = 0x02,
LAlt = 0x04,
LMod = 0x08,
RCtrl = 0x10,
RShift = 0x20,
RAlt = 0x40,
RMod = 0x80,
}
impl From<Modifier> for u8 {
fn from(modifier: Modifier) -> Self {
modifier as u8
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, MsgPack, MsgUnpack)]
#[non_exhaustive]
pub enum Button {
Mod(Modifier),
Key(Key),
ModTap(Key, Modifier),
Compose2(CompShift, Key, CompShift, Key),
Compose3(CompShift, Key, CompShift, Key, CompShift, Key),
Layer(LayerShift, LayerDir, u16),
None,
}
/// Whether a key should be shift modified as part of a compose chain.
#[derive(
Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, MsgPack, MsgUnpack,
)]
pub enum CompShift {
/// Do not shift the key. [Key::A] becomes 'a'.
#[default]
Lower,
/// Shift the key. [Key::A] becomes 'A'.
Upper,
/// Shift the key if shift is being held down. [Key::A] becomes 'a' or 'A' depending.
Variable,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, MsgPack, MsgUnpack)]
pub enum LayerDir {
Left,
Right,
Up,
Down,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, MsgPack, MsgUnpack)]
pub enum LayerShift {
Move,
Peek,
}
impl Display for Button {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let cs = |cs: &CompShift| match cs {
CompShift::Lower => "",
CompShift::Upper => "",
CompShift::Variable => "⇧?",
};
match self {
Button::Mod(modifier) => Debug::fmt(&modifier, f),
Button::Key(key) => write!(f, "{key:?}"),
Button::ModTap(key, modifier) => write!(f, "{key:?}/{modifier}"),
Button::Compose2(cs1, k1, cs2, k2) => {
write!(f, "⎄ {}{k1:?} {}{k2:?}", cs(cs1), cs(cs2))
}
Button::Compose3(cs1, k1, cs2, k2, cs3, k3) => {
write!(f, "⎄ {}{k1:?} {}{k2:?} {}{k3:?}", cs(cs1), cs(cs2), cs(cs3))
}
Button::Layer(..) => write!(f, "Lr"),
Button::None => write!(f, "Ø"),
}
}
}
impl Display for Modifier {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Modifier::LCtrl => "",
Modifier::LShift => "",
Modifier::LAlt => "",
Modifier::LMod => "",
Modifier::RCtrl => "",
Modifier::RShift => "",
Modifier::RAlt => "",
Modifier::RMod => "",
};
write!(f, "{s}")
}
}

0
lib/src/defmt.rs Normal file
View File

59
lib/src/event.rs Normal file
View File

@ -0,0 +1,59 @@
use crate::{button::Button, keys::Key};
use core::time::Duration;
use serde::{Deserialize, Serialize};
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 super::*;
use crate::button::Modifier;
/// A usb keyboard button was pressed or released.
///
/// This is a lower-level event than a [switch::Event], 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),
Wait,
}
}
/// A keyboard half.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Half {
Left,
Right,
}

13
lib/src/interrupts.rs Normal file
View File

@ -0,0 +1,13 @@
use embassy_rp::{
bind_interrupts,
peripherals::{PIO0, PIO1, UART0, USB},
};
bind_interrupts! {
pub struct Irqs {
UART0_IRQ => embassy_rp::uart::BufferedInterruptHandler<UART0>;
USBCTRL_IRQ => embassy_rp::usb::InterruptHandler<USB>;
PIO0_IRQ_0 => embassy_rp::pio::InterruptHandler<PIO0>;
PIO1_IRQ_0 => embassy_rp::pio::InterruptHandler<PIO1>;
}
}

358
lib/src/keyboard.rs Normal file
View File

@ -0,0 +1,358 @@
mod lights;
use core::sync::atomic::Ordering;
use alloc::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, Timer};
use log::{debug, error, info, warn};
use static_cell::StaticCell;
use crate::{
atomics::AtomicCoord,
button::{Button, LayerDir, LayerShift},
event::{
switch::{Event, EventKind},
Half,
},
layer::{Layer, Layers},
lights::Lights,
serial_proto::{
borrowed::DeviceMsg,
owned::{ChangeLayer, SwitchPress, SwitchRelease},
},
usb::serial::serial_send,
util::{SwapCell, SwapCellRead, 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>,
/// Matrix of layers. Stored as rows of columns.
pub layers: Vec<Vec<Layer>>,
}
struct State {
/// Which board is this.
half: Half,
current_layer: AtomicCoord,
layer_cols: usize,
layer_rows: usize,
layers: SwapCellRead<Layers>,
/// Array of LED indices of each switch
led_map: [usize; SWITCH_COUNT],
lights: Lights<PIO1, SWITCH_COUNT>,
}
/// Number of [KbEvents] returned by [KeyboardConfig::create].
pub const KB_SUBSCRIBERS: usize = 2;
/// Max number of subscribers for [KB_EVENTS]. Equals [KB_SUBSCRIBERS] plus the two used by
/// [layer_switch_task] and [lights::task].
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()
);
for (y, row) in self.layers.iter().enumerate() {
for (x, layer) in row.iter().enumerate() {
if layer.buttons.len() != SWITCH_COUNT {
warn!(
"layer ({x}, {y}) defines {} buttons, but there are {SWITCH_COUNT} switches",
layer.buttons.len(),
)
}
}
}
let layer_cols = self.layers.iter().map(|row| row.len()).max().unwrap_or(0);
let layer_rows = self.layers.len();
static LAYERS: StaticCell<SwapCell<Layers>> = StaticCell::new();
let layers = LAYERS.init_with(|| SwapCell::new(self.layers));
let (layers_read, layers_write) = layers.split();
static STATE: StaticCell<State> = StaticCell::new();
let state = STATE.init_with(|| State {
half: self.half,
current_layer: AtomicCoord::new(),
layer_cols,
layer_rows,
layers: layers_read,
lights: Lights::new(self.led_driver),
led_map: self.led_map,
});
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()
.expect("Not enough KbEvents allocated"),
},
state,
));
spawner.must_spawn(lights::task(
KbEvents {
publisher: KB_EVENTS.immediate_publisher(),
subscriber: KB_EVENTS
.subscriber()
.expect("Not enough KbEvents allocated"),
},
state,
));
Some([(); KB_SUBSCRIBERS].map(|_| {
KbEvents {
publisher: KB_EVENTS.immediate_publisher(),
subscriber: KB_EVENTS
.subscriber()
.expect("Not enough KbEvents allocated"),
}
}))
}
}
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(200);
pub const SWITCH_COUNT: usize = 18;
pub const DEBOUNCE_THRESHOLD: Duration = Duration::from_millis(15);
/// 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 (x, y) = state.current_layer.load(Ordering::Relaxed);
let layers = state.layers.read();
let Some(Layer { buttons }) = layers
.get(usize::from(y))
.and_then(|row| row.get(usize::from(x)))
else {
// currently layer is null, do nothing
pin.wait_for_high().await;
continue;
};
let button = buttons.get(switch_num).cloned();
drop(layers);
// and current button
let Some(button) = button else {
warn!("no button defined for switch {switch_num}");
pin.wait_for_high().await;
continue;
};
debug!("switch {switch_num} button {button:?} pressed");
serial_send(&DeviceMsg::SwitchPress(SwitchPress(switch_num as u16)));
let ev = |kind| Event {
source: state.half,
source_button: switch_num,
kind,
};
events.publish_immediate(ev(EventKind::Press {
button: button.clone(),
}));
Timer::after(DEBOUNCE_THRESHOLD).await;
pin.wait_for_high().await;
let released_after = pressed_at.elapsed();
debug!("switch {switch_num} button {button:?} released");
serial_send(&DeviceMsg::SwitchRelease(SwitchRelease(switch_num as u16)));
events.publish_immediate(ev(EventKind::Release {
button: button.clone(),
after: released_after.into(),
}));
Timer::after(DEBOUNCE_THRESHOLD).await;
}
}
#[embassy_executor::task]
async fn layer_switch_task(mut events: KbEvents, state: &'static State) {
let col_count = state.layer_cols as u16;
let row_count = state.layer_rows as u16;
let Some(last_row) = row_count.checked_sub(1) else {
error!("no layers specified");
return;
};
let Some(last_col) = col_count.checked_sub(1) else {
error!("no layers specified");
return;
};
loop {
use LayerDir::*;
use LayerShift::*;
let event = events.recv().await;
let (x, y) = state.current_layer.load(Ordering::Relaxed);
let (nx, ny) = match event.kind {
EventKind::Press { button } => match button {
Button::Layer(_, Up, n) => (x, y.checked_sub(n).unwrap_or(last_row)),
Button::Layer(_, Down, n) => (x, y.wrapping_add(n) % row_count),
Button::Layer(_, Left, n) => (x.checked_sub(n).unwrap_or(last_col), y),
Button::Layer(_, Right, n) => (x.wrapping_add(n) % col_count, y),
_ => continue,
},
EventKind::Release { button, .. } => match button {
Button::Layer(Peek, Up, n) => (x, y.wrapping_add(n) % row_count),
Button::Layer(Peek, Down, n) => (x, y.checked_sub(n).unwrap_or(last_row)),
Button::Layer(Peek, Left, n) => (x.wrapping_add(n) % col_count, y),
Button::Layer(Peek, Right, n) => (x.checked_sub(n).unwrap_or(last_col), y),
_ => continue,
},
};
state.current_layer.store(nx, ny, Ordering::Relaxed);
serial_send(&DeviceMsg::ChangeLayer(ChangeLayer { x: nx, y: ny }));
debug!("switched to layer ({nx}, {ny})");
}
}
/// Random functions for testing
#[allow(dead_code)]
pub mod test {
use crate::{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)
}
}

247
lib/src/keyboard/lights.rs Normal file
View File

@ -0,0 +1,247 @@
use core::future::pending;
use embassy_time::{Duration, Instant, Timer};
use futures::{select_biased, FutureExt};
use crate::{
button::Button,
lights::shaders::{PowerOffAnim, PowerOnAnim, Shader, Shaders},
rgb::Rgb,
usb::{UsbEvent, USB_EVENTS},
};
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 between each animation frame.
const FRAMETIME: Duration = Duration::from_millis(16);
const IDLE_ANIM: Shaders = Shaders::OrthoRainbow;
#[derive(Default)]
enum LightsState {
Active {
keys: [KeyLedState; SWITCH_COUNT],
next_frame: Instant,
idle_at: Instant,
},
PoweringOff(PowerOffAnim),
PoweringOn(PowerOnAnim),
#[default]
PoweredOff,
Idle(Shaders),
}
impl LightsState {
pub fn active_default() -> Self {
let now = Instant::now();
LightsState::Active {
keys: Default::default(),
next_frame: now,
idle_at: now + UNTIL_IDLE,
}
}
}
#[derive(Clone, Copy, Default)]
enum KeyLedState {
#[default]
None,
Solid(Rgb),
FadeBy(f32),
}
#[embassy_executor::task]
pub(super) async fn task(mut events: KbEvents, state: &'static State) {
let mut lights = LightsState::default();
let mut usb_events = USB_EVENTS
.dyn_subscriber()
.expect("USB_EVENTS: out of subscribers");
loop {
match &mut lights {
LightsState::Active {
keys,
next_frame,
idle_at,
} => {
select_biased! {
event = events.recv().fuse() => {
*idle_at = Instant::now() + UNTIL_IDLE;
handle_event(event, state, keys).await;
}
ev = usb_events.next_message_pure().fuse() => {
*idle_at = Instant::now() + UNTIL_IDLE;
handle_usb_event(ev, &mut lights).await;
}
_ = Timer::at(*idle_at).fuse() => lights = LightsState::Idle(IDLE_ANIM),
_ = Timer::at(*next_frame).fuse() => {
keypress_tick(state, keys).await;
*next_frame = Instant::now() + FRAMETIME;
}
}
}
LightsState::PoweringOff(anim) => {
select_biased! {
ev = usb_events.next_message_pure().fuse() => handle_usb_event(ev, &mut lights).await,
_ = play_shader(state, anim).fuse() => lights = LightsState::PoweredOff,
}
}
LightsState::PoweringOn(anim) => {
select_biased! {
ev = usb_events.next_message_pure().fuse() => handle_usb_event(ev, &mut lights).await,
_ = play_shader(state, anim).fuse() => lights = LightsState::active_default(),
}
}
LightsState::PoweredOff => {
let ev = usb_events.next_message_pure().await;
handle_usb_event(ev, &mut lights).await;
}
LightsState::Idle(anim) => {
select_biased! {
ev = usb_events.next_message_pure().fuse() => handle_usb_event(ev, &mut lights).await,
event = events.recv().fuse() => {
let now = Instant::now();
let mut keys = Default::default();
handle_event(event, state, &mut keys).await;
lights = LightsState::Active { keys, next_frame: now, idle_at: now + UNTIL_IDLE};
}
_ = play_shader(state, anim).fuse() => {}
}
}
};
}
}
async fn play_shader(state: &'static State, shader: &impl Shader) {
const SWITCH_COORDS: [(u16, u16); SWITCH_COUNT] = [
(0, 1),
(1, 1),
(2, 1),
(3, 1),
(4, 1),
(4, 2),
(3, 2),
(2, 2),
(1, 2),
(0, 2),
(0, 3),
(1, 3),
(2, 3),
(3, 3),
(4, 3),
(2, 4),
(3, 4),
(4, 4),
];
const BRIGHTNESS: f32 = 1.00;
let switch_coords = SWITCH_COORDS.map(|(x, y)| (f32::from(x) / 4.0, f32::from(y) / 4.0));
let animate_shader = async {
loop {
let now = Instant::now();
state
.lights
.update(|rgbs| {
(switch_coords.into_iter().zip(rgbs.iter_mut()))
.for_each(|(uv, rgb)| *rgb = shader.sample(now, uv) * BRIGHTNESS)
})
.await;
Timer::after(FRAMETIME).await;
}
};
let end_animation = async {
match shader.end_time() {
Some(end_time) => Timer::at(end_time).await,
None => pending().await,
};
};
select_biased! {
_ = animate_shader.fuse() => {}
_ = end_animation.fuse() => {}
}
}
async fn keypress_tick(state: &'static State, lights: &mut [KeyLedState; SWITCH_COUNT]) {
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 {
KeyLedState::None => *rgb = Rgb::new(0, 0, 0),
KeyLedState::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 = KeyLedState::None;
}
}
&KeyLedState::Solid(color) => *rgb = color,
}
}
})
.await;
}
async fn handle_event(
event: Event,
state: &'static State,
lights: &mut [KeyLedState; SWITCH_COUNT],
) {
let rgb = match event.kind {
EventKind::Press { button } => match button {
Button::Key(..) => KeyLedState::Solid(Rgb::new(0, 150, 0)),
Button::Mod(..) => KeyLedState::Solid(Rgb::new(0, 0, 150)),
Button::ModTap(..) => KeyLedState::Solid(Rgb::new(0, 0, 150)),
Button::Compose2(..) | Button::Compose3(..) => {
KeyLedState::Solid(Rgb::new(0, 100, 100))
}
Button::Layer(..) => KeyLedState::Solid(Rgb::new(120, 0, 120)),
_ => KeyLedState::Solid(Rgb::new(150, 0, 0)),
},
EventKind::Release { .. } => KeyLedState::FadeBy(0.85),
};
if event.source != state.half {
return;
}
let Some(light) = lights.get_mut(event.source_button) else {
return;
};
*light = rgb;
}
async fn handle_usb_event(event: UsbEvent, state: &mut LightsState) {
let usb_enabled = match event {
UsbEvent::Suspended(false) | UsbEvent::Configured(true) => true,
UsbEvent::Configured(false) | UsbEvent::Suspended(true) | UsbEvent::Reset => false,
_ => return,
};
let start = Instant::now();
*state = match (&state, usb_enabled) {
(LightsState::PoweringOn(..), true) => return,
(LightsState::PoweringOff(..), false) => return,
(LightsState::PoweredOff, false) => return,
(_, true) => LightsState::PoweringOn(PowerOnAnim { start }),
(_, false) => LightsState::PoweringOff(PowerOffAnim { start }),
};
}

505
lib/src/keypress_handler.rs Normal file
View File

@ -0,0 +1,505 @@
use core::future::pending;
use crate::{
button::{Button, CompShift, Modifier},
event::{button, switch, Half},
keys::Key,
};
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};
/// The time a ModTap button takes to resolve as a Mod while being held down.
const MOD_TAP_TIME: Duration = Duration::from_millis(150);
/// The number of switches on this keyboard half.
const SWITCH_COUNT: usize = 18;
/// The usb keycode mapped to Compose.
const COMPOSE_KEY: Key = Key::Application;
/// 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,
}
#[derive(Default)]
struct ShiftHeld {
left: bool,
right: bool,
}
impl ShiftHeld {
pub fn either(&self) -> bool {
self.left || self.right
}
}
async fn slow_press(
output: &mut Pub<'_, impl PubSubBehavior<button::Event>, button::Event>,
shift_held: &mut ShiftHeld,
button: &Button,
) {
let do_compose = |keys: &[(CompShift, Key)]| {
for &(comp_shift, key) in keys {
match comp_shift {
// Release any pressed shift keys
CompShift::Lower => {
if shift_held.left {
output.publish_immediate(button::Event::ReleaseMod(Modifier::LShift));
}
if shift_held.right {
output.publish_immediate(button::Event::ReleaseMod(Modifier::RShift));
}
}
// Press shift if not already pressed
CompShift::Upper if !shift_held.either() => {
output.publish_immediate(button::Event::PressMod(Modifier::LShift));
}
_ => {}
}
output.publish_immediate(button::Event::PressKey(key));
output.publish_immediate(button::Event::ReleaseKey(key));
output.publish_immediate(button::Event::Wait);
match comp_shift {
// Re-press any shift keys we released
CompShift::Lower => {
if shift_held.left {
output.publish_immediate(button::Event::PressMod(Modifier::LShift));
}
if shift_held.right {
output.publish_immediate(button::Event::PressMod(Modifier::RShift));
}
}
// Release the shift keys we pressed
CompShift::Upper if !shift_held.either() => {
output.publish_immediate(button::Event::ReleaseMod(Modifier::LShift));
}
_ => {}
}
}
};
let event = match button {
&Button::Mod(m) | &Button::ModTap(_, m) => {
match m {
Modifier::LShift => shift_held.left = true,
Modifier::RShift => shift_held.right = true,
_ => {}
}
button::Event::PressMod(m)
}
&Button::Key(k) => button::Event::PressKey(k),
&Button::Compose2(csa, a, csb, b) => {
do_compose(&[(CompShift::Variable, COMPOSE_KEY), (csa, a), (csb, b)]);
return;
}
&Button::Compose3(csa, a, csb, b, csc, c) => {
do_compose(&[
(CompShift::Variable, COMPOSE_KEY),
(csa, a),
(csb, b),
(csc, c),
]);
return;
}
_ => return,
};
output.publish_immediate(event);
}
async fn slow_release(
output: &mut Pub<'_, impl PubSubBehavior<button::Event>, button::Event>,
shift_held: &mut ShiftHeld,
button: &Button,
) {
let event = match button {
&Button::Mod(m) | &Button::ModTap(_, m) => {
match m {
Modifier::LShift => shift_held.left = false,
Modifier::RShift => shift_held.right = false,
_ => {}
}
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;
// tracks whether a shift-key is being held down.
// required to properly play compose macros.
let mut shift_held = ShiftHeld::default();
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;
};
debug!("modtap resolved as mod: {:?}", event.button);
slow_press(output, &mut shift_held, &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, &mut shift_held, &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(..)
| Button::Key(..)
| Button::Compose2(..)
| Button::Compose3(..) => {
if queue.is_empty() {
debug!("sending key now");
// otherwise, send immediately
slow_press(output, &mut shift_held, &button).await;
} else {
debug!("adding key to queue");
// if events in queue, also add to queue
insert(queue, button);
}
}
_ => {}
}
}
switch::EventKind::Release { button, .. } => {
let position_in_queue = queue
.iter()
.enumerate()
.filter(|(_, queued)| queued.source_half == event.source)
.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: {k:?}");
for _ in 0..position_in_queue {
let prev_event = queue.pop_front().unwrap();
debug!("pressing earlier event {:?}", prev_event);
slow_press(output, &mut shift_held, &prev_event.button).await;
}
let _ = queue.pop_front();
debug!("pressing modtap as key");
slow_press(output, &mut shift_held, &Button::Key(k)).await;
slow_release(output, &mut shift_held, &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, &mut shift_held, &button).await;
};
}
Button::Mod(..)
| Button::Key(..)
| Button::Compose2(..)
| Button::Compose3(..) => {
// 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, &mut shift_held, &prev_event.button).await;
}
}
debug!("releasing key {button:?}");
slow_release(output, &mut shift_held, &button).await;
}
_ => {}
}
}
}
}
}
#[cfg(all(target_arch = "x86_64", 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 tgnt::{button::Modifier, keys::Key};
struct Test {
// button index, pressed, delay
input: Vec<(usize, bool, Duration)>,
expected: Vec<button::Event>,
}
macro_rules! timing_test {
($name:ident, $test:expr) => {
#[test]
fn $name() {
let test: Test = $test;
run_test(test);
}
};
}
const SHORT: Duration = Duration::from_millis(1);
const LONG: Duration = Duration::from_millis(160);
timing_test! {
modtap_mod,
Test {
input: vec![
(0, true, SHORT),
(1, true, SHORT),
(1, false, SHORT),
(0, false, SHORT),
],
expected: vec![
button::Event::PressMod(Modifier::LShift),
button::Event::PressKey(Key::B),
button::Event::ReleaseKey(Key::B),
button::Event::ReleaseMod(Modifier::LShift),
],
}
}
timing_test! {
modtap_tap,
Test {
input: vec![
(0, true, SHORT),
(1, true, SHORT),
(0, false, SHORT),
(1, false, SHORT),
],
expected: vec![
button::Event::PressKey(Key::A),
button::Event::ReleaseKey(Key::A),
button::Event::PressKey(Key::B),
button::Event::ReleaseKey(Key::B),
],
}
}
timing_test! { modtap_tap_2x, Test {
input: vec![
(0, true, SHORT),
(2, true, SHORT),
(2, false, SHORT),
(0, false, SHORT),
(0, true, SHORT),
(2, true, SHORT),
(2, false, SHORT),
(0, false, SHORT),
],
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),
],
}}
timing_test! { modtap_double_shift, Test {
input: vec![
(0, true, LONG),
(3, true, SHORT),
(0, false, LONG),
(3, false, SHORT),
],
expected: vec![
button::Event::PressMod(Modifier::LShift),
button::Event::ReleaseMod(Modifier::LShift),
button::Event::PressMod(Modifier::RShift),
button::Event::ReleaseMod(Modifier::RShift),
],
}}
fn run_test(test: Test) {
let _ = simple_logger::SimpleLogger::new().init();
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),
Button::ModTap(Key::D, Modifier::RShift),
];
// future that iterates over the test input and sends switch events.
let clicker = 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;
}
};
let output_listener = async {
let mut got = Vec::new();
for _expected in &test.expected {
let timeout = LONG + Duration::from_millis(50);
let event = match with_timeout(timeout, 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(())
};
embassy_futures::block_on(async {
let r = select(
join(clicker, output_listener),
keypress_handler(
&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.expected, got
),
Either::Second(never) => never,
}
});
panic!();
}
}

269
lib/src/keys.rs Normal file
View File

@ -0,0 +1,269 @@
use msgpck::{MsgPack, MsgUnpack};
use serde::{Deserialize, Serialize};
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, MsgPack, MsgUnpack)]
#[non_exhaustive]
// https://usb.org/sites/default/files/hut1_3_0.pdf
pub enum Key {
A = 0x04,
B = 0x05,
C = 0x06,
D = 0x07,
E = 0x08,
F = 0x09,
G = 0x0A,
H = 0x0B,
I = 0x0C,
J = 0x0D,
K = 0x0E,
L = 0x0F,
M = 0x10,
N = 0x11,
O = 0x12,
P = 0x13,
Q = 0x14,
R = 0x15,
S = 0x16,
T = 0x17,
U = 0x18,
V = 0x19,
W = 0x1A,
X = 0x1B,
Y = 0x1C,
Z = 0x1D,
/// Keyboard 1 and !
D1 = 0x1E,
/// Keyboard 2 and @
D2 = 0x1F,
/// Keyboard 3 and #
D3 = 0x20,
/// Keyboard 4 and $
D4 = 0x21,
/// Keyboard 5 and %
D5 = 0x22,
/// Keyboard 6 and ∧
D6 = 0x23,
/// Keyboard 7 and &
D7 = 0x24,
/// Keyboard 8 and *
D8 = 0x25,
/// Keyboard 9 and (
D9 = 0x26,
/// Keyboard 0 and )
D0 = 0x27,
Return = 0x28,
Escape = 0x29,
/// Keyboard DELETE (Backspace)
Backspace = 0x2A,
Tab = 0x2B,
/// Keyboard Spacebar
Space = 0x2C,
/// Keyboard - and (underscore)
Dash = 0x2D,
/// Keyboard = and +2
Equal = 0x2E,
/// Keyboard [ and {
LBracket = 0x2F,
/// Keyboard ] and }
RBracket = 0x30,
/// Keyboard \and |
BackslashPipe = 0x31,
/// Keyboard Non-US # and ̃
Pound = 0x32,
/// Keyboard ; and :
Colon = 0x33,
/// Keyboard and “
Apostrophe = 0x34,
/// Keyboard Grave Accent and Tilde
Accent = 0x35,
/// Keyboard , and <
Comma = 0x36,
/// Keyboard . and >
Period = 0x37,
/// Keyboard / and ?
Slash = 0x38,
CapsLock = 0x39,
F1 = 0x3A,
F2 = 0x3B,
F3 = 0x3C,
F4 = 0x3D,
F5 = 0x3E,
F6 = 0x3F,
F7 = 0x40,
F8 = 0x41,
F9 = 0x42,
F10 = 0x43,
F11 = 0x44,
F12 = 0x45,
/// Keyboard PrintScreen
PrintScreen = 0x46,
/// Keyboard Scroll Lock
ScrollLock = 0x47,
/// Keyboard Pause
Pause = 0x48,
/// Keyboard Insert
Insert = 0x49,
/// Keyboard Home
Home = 0x4A,
/// Keyboard PageUp
PageUp = 0x4B,
/// Keyboard Delete Forward
Delete = 0x4C,
/// Keyboard End
End = 0x4D,
/// Keyboard PageDown
PageDown = 0x4E,
/// Keyboard RightArrow
RightArrow = 0x4F,
/// Keyboard LeftArrow
LeftArrow = 0x50,
/// Keyboard DownArrow
DownArrow = 0x51,
/// Keyboard UpArrow
UpArrow = 0x52,
/// Keypad Num Lock and Clear
KeypadNumLock = 0x53,
/// Keypad /
KeypadSlash = 0x54,
/// Keypad *
KeypadAsterisk = 0x55,
/// Keypad -
KeypadMinus = 0x56,
/// Keypad +
KeypadPlus = 0x57,
/// KeypadEnter
KeypadEnter = 0x58,
/// Keypad 1 and End
Keypad1 = 0x59,
/// Keypad 2 and Down Arrow
Keypad2 = 0x5A,
/// Keypad 3 and PageDn
Keypad3 = 0x5B,
/// Keypad 4 and Left Arrow
Keypad4 = 0x5C,
/// Keypad 5
Keypad5 = 0x5D,
/// Keypad 6 and Right Arrow
Keypad6 = 0x5E,
/// Keypad 7 and Home
Keypad7 = 0x5F,
/// Keypad 8 and Up Arrow
Keypad8 = 0x60,
/// Keypad 9 and PageUp
Keypad9 = 0x61,
/// Keypad 0 and Insert
Keypad0 = 0x62,
/// Keypad . and Delete
KbPeriod = 0x63,
/// Keyboard Non-US \ and |
NonUsBackslashPipe = 0x64,
Application = 0x65,
Power = 0x66,
/// Keypad =
KpEqual = 0x67,
F13 = 0x68,
F14 = 0x69,
F15 = 0x6A,
F16 = 0x6B,
F17 = 0x6C,
F18 = 0x6D,
F19 = 0x6E,
F20 = 0x6F,
F21 = 0x70,
F22 = 0x71,
F23 = 0x72,
F24 = 0x73,
Execute = 0x74,
Help = 0x75,
Menu = 0x76,
Select = 0x77,
Stop = 0x78,
Again = 0x79,
Undo = 0x7A,
Cut = 0x7B,
Copy = 0x7C,
Paste = 0x7D,
Find = 0x7E,
Mute = 0x7F,
VolumeUp = 0x80,
VolumeDown = 0x81,
/// Keyboard Locking Caps Lock
LockingCapsLock = 0x82,
/// Keyboard Locking Num Lock
LockingNumLock = 0x83,
/// Keyboard Locking Scroll Lock
LockingScrollLock = 0x84,
/// Keypad ,
KpComma = 0x85,
/// Keypad Equal Sign
KpEqualSIgn = 0x86,
International1 = 0x87,
International2 = 0x88,
International3 = 0x89,
International4 = 0x8A,
International5 = 0x8B,
International6 = 0x8C,
International7 = 0x8D,
International8 = 0x8E,
International9 = 0x8F,
Lang1 = 0x90,
Lang2 = 0x91,
Lang3 = 0x92,
Lang4 = 0x93,
Lang5 = 0x94,
Lang6 = 0x95,
Lang7 = 0x96,
Lang8 = 0x97,
Lang9 = 0x98,
/// Keyboard Alternative Erase
AltErase = 0x99,
/// Keyboard SysReq/Attention
SysReq = 0x9A,
Cancel = 0x9B,
Clear = 0x9C,
Prior = 0x9D,
/// Keyboard Return
Return2 = 0x9E,
/// Keyboard Separator
Separator = 0x9F,
/// Keyboard Out
Out = 0xA0,
/// Keyboard Oper
Oper = 0xA1,
/// Keyboard Clear/Again
ClearAgain = 0xA2,
/// Keyboard CrSel/Props
CrSel = 0xA3,
/// Keyboard ExSel
ExSel = 0xA4,
//
// NOTE: the usb keyboard/keypad page allegedly har characters in the rage above A4,
// like the ones below, but they don't seem to work
///// Keypad {
//LCurly = 0xB8,
///// Keypad }
//RCurly = 0xB9,
//
// NOTE: Not sure if these are required
//LCtrl = 0xE0,
//LShift = 0xE1,
//LAlt = 0xE2,
//RCtrl = 0xE4,
//RShift = 0xE5,
//RAlt = 0xE6,
}
impl From<Key> for u8 {
fn from(key: Key) -> u8 {
key as u8
}
}

12
lib/src/layer.rs Normal file
View File

@ -0,0 +1,12 @@
use alloc::vec::Vec;
use msgpck::{MsgPack, MsgUnpack};
use serde::{Deserialize, Serialize};
use crate::button::Button;
#[derive(Default, Debug, Serialize, Deserialize, Clone, MsgPack, MsgUnpack)]
pub struct Layer {
pub buttons: Vec<Button>,
}
pub type Layers = Vec<Vec<Layer>>;

36
lib/src/layout.rs Normal file
View File

@ -0,0 +1,36 @@
use alloc::vec::Vec;
use serde::{Deserialize, Serialize};
#[derive(Default, Debug, Serialize, Deserialize)]
pub struct Layout {
pub buttons: Vec<Rect>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Rect {
pub x: f32,
pub y: f32,
#[serde(default = "default_width")]
pub w: f32,
#[serde(default = "default_height")]
pub h: f32,
}
impl Default for Rect {
fn default() -> Self {
Self {
x: 0.0,
y: 0.0,
w: default_width(),
h: default_height(),
}
}
}
fn default_width() -> f32 {
1.0
}
fn default_height() -> f32 {
1.0
}

37
lib/src/lib.rs Normal file
View File

@ -0,0 +1,37 @@
#![cfg_attr(not(feature = "std"), 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 atomics;
pub mod button;
pub mod event;
pub mod keypress_handler;
pub mod keys;
pub mod layer;
pub mod layout;
pub mod logger;
pub mod rgb;
pub mod rtt;
pub mod serial_proto;
pub mod util;

43
lib/src/lights.rs Normal file
View File

@ -0,0 +1,43 @@
pub mod shaders;
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;
}
}

117
lib/src/lights/shaders.rs Normal file
View File

@ -0,0 +1,117 @@
use core::f32::consts::PI;
use embassy_time::{Duration, Instant};
use glam::{vec2, vec3, Vec3};
use libm::cosf;
use crate::rgb::Rgb;
/// A fragment shader.
pub trait Shader {
/// Sample a normalized coordinate (0 to 1) using the shader function and return a color.
fn sample(&self, time: Instant, uv: (f32, f32)) -> Rgb;
fn end_time(&self) -> Option<Instant> {
None
}
}
pub enum Shaders {
OrthoRainbow,
LsdHyperspace,
}
impl Shader for Shaders {
fn sample(&self, time: Instant, uv: (f32, f32)) -> Rgb {
match self {
Shaders::OrthoRainbow => OrthoRainbow.sample(time, uv),
Shaders::LsdHyperspace => LsdHyperspace.sample(time, uv),
}
}
}
pub struct OrthoRainbow;
impl Shader for OrthoRainbow {
fn sample(&self, time: Instant, (x, y): (f32, f32)) -> Rgb {
let time = time.as_millis() as f32 / 1000.0;
let r = 0.5 + 0.5 * cosf(time + x + 0.0);
let g = 0.5 + 0.5 * cosf(time + y + 2.0);
let b = 0.5 + 0.5 * cosf(time + x + 4.0);
Rgb::from_f32s(r, g, b)
}
}
pub struct LsdHyperspace;
impl Shader for LsdHyperspace {
fn sample(&self, time: Instant, uv: (f32, f32)) -> Rgb {
let time = time.as_millis() as f32 / 1000.0 * 3.0;
let uv = vec2(uv.0, uv.1);
let center = vec2(0.5, 0.5);
let dist = (uv - center).length();
let fac = dist * PI + vec3(0.0, 1.0, 4.0) - time;
let col = cos3(fac) * cos3(fac * 0.5);
Rgb::from_f32s(col.x, col.y, col.z)
}
}
pub struct PowerOffAnim {
/// Animation starting time.
pub start: Instant,
}
impl PowerOffAnim {
const FADE_FACTOR: f32 = 0.5;
/// Animation duration, in seconds.
const DURATION_SEC: u16 = 5;
}
impl Shader for PowerOffAnim {
fn sample(&self, time: Instant, (_, y): (f32, f32)) -> Rgb {
let time = time.as_millis().saturating_sub(self.start.as_millis());
let time = time as f32 / 1000.0;
let duration: f32 = Self::DURATION_SEC.into();
let r = Self::FADE_FACTOR * (duration - time - 1.0 + y);
Rgb::from_f32s(r, 0.0, 0.0)
}
fn end_time(&self) -> Option<Instant> {
Some(self.start + Duration::from_secs(Self::DURATION_SEC.into()))
}
}
pub struct PowerOnAnim {
/// Animation starting time.
pub start: Instant,
}
impl PowerOnAnim {
const FADE_FACTOR: f32 = 1.0;
/// Animation duration, in seconds.
const DURATION_SEC: u16 = 1;
}
fn cos3(v: Vec3) -> Vec3 {
vec3(cosf(v.x), cosf(v.y), cosf(v.z))
}
impl Shader for PowerOnAnim {
fn sample(&self, time: Instant, (_, y): (f32, f32)) -> Rgb {
let time = time.as_millis().saturating_sub(self.start.as_millis());
let time = time as f32 / 1000.0;
let duration: f32 = Self::DURATION_SEC.into();
let g = Self::FADE_FACTOR * (duration - time - y);
Rgb::from_f32s(0.0, g, 0.0)
}
fn end_time(&self) -> Option<Instant> {
Some(self.start + Duration::from_secs(Self::DURATION_SEC.into()))
}
}

81
lib/src/logger.rs Normal file
View File

@ -0,0 +1,81 @@
use core::{fmt::Arguments, sync::atomic::Ordering};
use embassy_time::Instant;
use log::{Level, Metadata, Record};
use portable_atomic::AtomicBool;
use static_cell::StaticCell;
pub const LOGGER_OUTPUTS: usize = 2;
/// A logger which timestamps logs and forwards them to mutiple other loggers.
pub struct LogMultiplexer {
pub outputs: [&'static dyn LogOutput; LOGGER_OUTPUTS],
}
pub struct TimestampedRecord<'a> {
pub record: Record<'a>,
/// Timestamp
pub timestamp: Instant,
}
impl LogMultiplexer {
/// 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<LogMultiplexer> = 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 LogMultiplexer {
fn enabled(&self, _metadata: &Metadata) -> bool {
true
}
fn log(&self, record: &Record) {
if self.enabled(record.metadata()) {
let timestamp = Instant::now();
let record = TimestampedRecord {
timestamp,
record: record.clone(),
};
for output in &self.outputs {
output.log(&record);
}
}
}
fn flush(&self) {}
}
pub trait LogOutput: Send + Sync {
fn log(&self, record: &TimestampedRecord);
}
impl TimestampedRecord<'_> {
pub fn args(&self) -> &Arguments<'_> {
self.record.args()
}
pub fn level(&self) -> Level {
self.metadata().level()
}
pub fn metadata(&self) -> &Metadata<'_> {
self.record.metadata()
}
}

29
lib/src/panic_handler.rs Normal file
View 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);
Ok(())
}
}

85
lib/src/rgb.rs Normal file
View File

@ -0,0 +1,85 @@
use bytemuck::{cast_slice, Pod, Zeroable};
use core::{
fmt::{self, Debug},
ops::{Div, Mul},
};
/// 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]))
}
pub fn from_f64s(r: f64, g: f64, b: f64) -> Self {
let r = r.clamp(0.0, 1.0) * 255.0;
let g = g.clamp(0.0, 1.0) * 255.0;
let b = b.clamp(0.0, 1.0) * 255.0;
Self::new(r as u8, g as u8, b as u8)
}
pub fn from_f32s(r: f32, g: f32, b: f32) -> Self {
let r = r.clamp(0.0, 1.0) * 255.0;
let g = g.clamp(0.0, 1.0) * 255.0;
let b = b.clamp(0.0, 1.0) * 255.0;
Self::new(r as u8, g as u8, b as u8)
}
/// 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)
}
}
impl Mul<f32> for Rgb {
type Output = Rgb;
fn mul(self, factor: f32) -> Self::Output {
let [r, g, b] = self.components();
Rgb::new(
((r as f32) * factor) as u8,
((g as f32) * factor) as u8,
((b as f32) * factor) as u8,
)
}
}
#[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);
}
}

64
lib/src/rtt.rs Normal file
View File

@ -0,0 +1,64 @@
use core::{cell::RefCell, fmt::Write};
use critical_section::Mutex;
use rtt_target::{rtt_init, UpChannel};
use crate::logger::{LogOutput, TimestampedRecord};
pub type RttWriteFn = fn(&str);
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(s: &str) {
critical_section::with(|cs| {
let mut slot = CHANNEL.borrow_ref_mut(cs);
if let Some(channel) = slot.as_mut() {
channel.write(s.as_bytes());
};
});
}
pub fn init_rtt_logger() -> &'static RttLogger {
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_none() {
*slot = Some(channels.up.0);
}
static RTT_LOGGER: RttLogger = RttLogger;
&RTT_LOGGER
})
}
pub struct RttLogger;
impl LogOutput for RttLogger {
fn log(&self, record: &TimestampedRecord) {
let s = record.timestamp.as_secs();
let ms = record.timestamp.as_millis() % 1000;
let level = record.level();
let mut w = &mut Writer;
let _ = writeln!(&mut w, "[{s}.{ms:04}] ({level}) {}", record.args());
}
}
struct Writer;
impl Write for Writer {
fn write_str(&mut self, s: &str) -> Result<(), core::fmt::Error> {
rtt_write(s);
Ok(())
}
}

71
lib/src/serial_proto.rs Normal file
View File

@ -0,0 +1,71 @@
pub mod owned {
use alloc::string::String;
use msgpck::{MsgPack, MsgUnpack};
use crate::layer::Layers;
#[derive(Debug, MsgPack, MsgUnpack)]
pub struct LogRecord {
/// Milliseconds since boot
pub timestamp: u64,
pub level: String,
pub message: String,
}
/// Press the switch with the provided index.
#[derive(Debug, MsgPack, MsgUnpack)]
pub struct SwitchPress(pub u16);
/// Release the switch with the provided index.
#[derive(Debug, MsgPack, MsgUnpack)]
pub struct SwitchRelease(pub u16);
/// Change to the layer at the provided coordinates in the layer matrix.
#[derive(Debug, MsgPack, MsgUnpack)]
pub struct ChangeLayer {
pub x: u16,
pub y: u16,
}
#[derive(Debug, MsgPack, MsgUnpack)]
pub enum DeviceMsg {
Log(LogRecord),
SwitchPress(SwitchPress),
SwitchRelease(SwitchRelease),
ChangeLayer(ChangeLayer),
Layers(Layers),
}
#[derive(Debug, MsgPack, MsgUnpack)]
pub enum HostMsg {
GetLayers,
}
}
pub mod borrowed {
use msgpck::MsgPack;
use super::owned::*;
use crate::{layer::Layers, util::DisplayPack};
#[derive(Debug, MsgPack)]
pub struct LogRecord<'a> {
/// Milliseconds since boot
pub timestamp: u64,
pub level: DisplayPack<log::Level>,
pub message: DisplayPack<&'a core::fmt::Arguments<'a>>,
}
#[derive(Debug, MsgPack)]
pub enum DeviceMsg<'a> {
Log(LogRecord<'a>),
SwitchPress(SwitchPress),
SwitchRelease(SwitchRelease),
ChangeLayer(ChangeLayer),
Layers(&'a Layers),
}
}

189
lib/src/uart.rs Normal file
View File

@ -0,0 +1,189 @@
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 embassy_sync::channel::Channel;
use embassy_sync::pubsub::PubSubBehavior;
use embedded_io_async::{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;
use crate::usb::{UsbEvent, USB_EVENTS};
use crate::util::CS;
/// Channel for [UsbEvent]s to be sent on the uart line.
pub static UART_USB_EVENTS_OUT: Channel<CS, UsbEvent, 8> = Channel::new();
#[derive(Clone, Debug, Serialize, Deserialize)]
enum Message {
KeyboardEvent(switch::Event),
UsbEvent(UsbEvent),
}
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();
/// The header of a UART packet.
#[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_first_chunk::<HEADER_LEN>().unwrap();
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); // drop the first byte and hope we find a good packet header
continue;
}
log::trace!(
"reading from uart, header={header:?}, bytes_received={}",
rest.len()
);
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()),
&Message::UsbEvent(event) => USB_EVENTS.publish_immediate(event),
}
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;
// forward messages to the other keyboard half
loop {
let message = select_biased! {
event = events_rx.recv().fuse() => {
if event.source != this_half {
continue; // do not forward messages from the other half back to it
}
Message::KeyboardEvent(event)
}
event = UART_USB_EVENTS_OUT.receive().fuse() => Message::UsbEvent(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"),
}
}

136
lib/src/usb.rs Normal file
View File

@ -0,0 +1,136 @@
use embassy_executor::Spawner;
use embassy_rp::{peripherals::USB, usb::Driver};
use embassy_sync::pubsub::{PubSubBehavior, PubSubChannel};
use embassy_usb::{Builder, Config, Handler, UsbDevice};
use serde::{Deserialize, Serialize};
use static_cell::StaticCell;
use crate::{interrupts::Irqs, keyboard::KbEvents, uart::UART_USB_EVENTS_OUT, util::CS};
use self::serial::UsbSerial;
pub mod keyboard;
pub mod serial;
pub const MAX_PACKET_SIZE: u8 = 64;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum UsbEvent {
Enabled(bool),
Suspended(bool),
Configured(bool),
Addressed(u8),
Reset,
}
pub type UsbEventChannel = PubSubChannel<CS, UsbEvent, 8, 24, 0>;
pub static USB_EVENTS: UsbEventChannel = UsbEventChannel::new();
struct State {
device_descriptor: [u8; 256],
config_descriptor: [u8; 256],
bos_descriptor: [u8; 256],
msos_descriptor: [u8; 256],
control_buf: [u8; 64],
handler: UsbHandler,
}
static STATE: StaticCell<State> = StaticCell::new();
pub async fn setup_logger_and_keyboard(usb: USB, events: KbEvents) -> &'static UsbSerial {
let mut builder = builder(usb);
let usb_serial = serial::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));
usb_serial
}
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],
msos_descriptor: [0; 256],
control_buf: [0; 64],
handler: UsbHandler,
});
// 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);
let mut builder = Builder::new(
driver,
config,
&mut state.device_descriptor,
&mut state.config_descriptor,
&mut state.bos_descriptor,
&mut state.msos_descriptor,
&mut state.control_buf,
);
builder.handler(&mut state.handler);
builder
}
#[embassy_executor::task]
pub async fn run(mut device: UsbDevice<'static, Driver<'static, USB>>) {
log::info!("running usb device");
device.run().await
}
struct UsbHandler;
impl Handler for UsbHandler {
fn enabled(&mut self, enabled: bool) {
USB_EVENTS.publish_immediate(UsbEvent::Enabled(enabled));
let _ = UART_USB_EVENTS_OUT.try_send(UsbEvent::Enabled(enabled));
log::debug!("usb enabled({enabled})");
}
fn reset(&mut self) {
USB_EVENTS.publish_immediate(UsbEvent::Reset);
let _ = UART_USB_EVENTS_OUT.try_send(UsbEvent::Reset);
log::debug!("usb reset()");
}
fn addressed(&mut self, addr: u8) {
USB_EVENTS.publish_immediate(UsbEvent::Addressed(addr));
let _ = UART_USB_EVENTS_OUT.try_send(UsbEvent::Addressed(addr));
log::debug!("usb addressed({addr})");
}
fn configured(&mut self, configured: bool) {
USB_EVENTS.publish_immediate(UsbEvent::Configured(configured));
let _ = UART_USB_EVENTS_OUT.try_send(UsbEvent::Configured(configured));
log::debug!("usb configured({configured})");
}
fn suspended(&mut self, suspended: bool) {
USB_EVENTS.publish_immediate(UsbEvent::Suspended(suspended));
let _ = UART_USB_EVENTS_OUT.try_send(UsbEvent::Suspended(suspended));
log::debug!("usb suspended({suspended})");
}
}

242
lib/src/usb/keyboard.rs Normal file
View File

@ -0,0 +1,242 @@
pub mod report;
use embassy_executor::Spawner;
use embassy_futures::select::{select, Either};
use embassy_rp::{peripherals::USB, usb::Driver};
use embassy_sync::{
blocking_mutex::raw::NoopRawMutex,
mutex::Mutex,
pubsub::{subscriber::Sub, PubSubBehavior, PubSubChannel, WaitResult},
signal::Signal,
};
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,
keypress_handler::keypress_handler,
usb::keyboard::report::{KeyboardReport, EMPTY_KEYBOARD_REPORT},
util::CS,
};
use super::MAX_PACKET_SIZE;
struct Handler;
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 {
reports: Mutex<CS, Reports>,
/// Signalled by [report_task] when a report is sent.
report_signal: Signal<CS, ()>,
handler: Handler,
}
/// Set up a USB HID keyboard. This function panics if called more than once.
pub async fn setup(builder: &mut Builder<'static, Driver<'static, USB>>, events: KbEvents) {
log::info!("setting up usb hid");
let context = {
static CONTEXT: StaticCell<Context> = StaticCell::new();
// this panics if the functon is called twice
CONTEXT.init(Context {
reports: Mutex::new(Reports {
actual: EMPTY_KEYBOARD_REPORT,
unsent: EMPTY_KEYBOARD_REPORT,
}),
report_signal: Signal::new(),
handler: Handler,
})
};
let hid_state = {
static HID_STATE: StaticCell<hid::State<'static>> = StaticCell::new();
// this panics if the functon is called twice
HID_STATE.init(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, hid_state, config);
let spawner = Spawner::for_current_executor().await;
spawner.must_spawn(report_task(stream, context));
spawner.must_spawn(event_listener_task(events, context));
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>;
/// Capacity of the [button::Event] buffer in [event_listener_task]. A too small capacity may cause
/// events to be dropped.
pub const BUTTON_EVENT_BUF_LEN: usize = 32;
#[embassy_executor::task]
async fn event_listener_task(mut events: KbEvents, ctx: &'static Context) -> ! {
let button_events =
PubSubChannel::<NoopRawMutex, button::Event, BUTTON_EVENT_BUF_LEN, 1, 1>::new();
let mut button_pub = button_events.publisher().unwrap();
let mut button_sub = button_events.subscriber().unwrap();
let r = select(
button_events_to_report(ctx, &mut *button_sub),
keypress_handler(&mut *events.subscriber, &mut *button_pub),
)
.await;
// match hack because rust isn't smart enough.
match r {
Either::First(never) | Either::Second(never) => match never {},
}
}
/// Listen for [button::Event]s and add them to the keyboard [Reports].
async fn button_events_to_report(
ctx: &'static Context,
button_sub: &mut Sub<'_, impl PubSubBehavior<button::Event>, button::Event>,
) -> ! {
loop {
let WaitResult::Message(event) = button_sub.next_message().await else {
error!("lagged");
continue;
};
loop {
let mut r = ctx.reports.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) => {
ctx.report_signal.reset();
drop(r);
ctx.report_signal.wait().await;
continue;
}
button::Event::ReleaseMod(m) if r.unsent.modifier_pressed(m) => {
ctx.report_signal.reset();
drop(r);
ctx.report_signal.wait().await;
continue;
}
button::Event::ReleaseKey(k) => r.actual.release_key(k),
button::Event::ReleaseMod(m) => r.actual.release_modifier(m),
button::Event::Wait => {
ctx.report_signal.reset();
drop(r);
ctx.report_signal.wait().await;
}
}
break;
}
}
}
#[embassy_executor::task]
async fn report_task(stream: HidStream, ctx: &'static Context) {
if let Err(e) = write_reports(stream, ctx).await {
log::error!("keyboard error: {e:?}");
}
}
/// Write keyboard reports.
async fn write_reports(mut stream: HidStream, ctx: &'static Context) -> Result<(), Error> {
stream.ready().await;
loop {
Timer::after(Duration::from_millis(2)).await;
let report = {
let mut reports = ctx.reports.lock().await;
ctx.report_signal.signal(());
reports.unsent = EMPTY_KEYBOARD_REPORT;
reports.actual
};
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)]
#[allow(dead_code)]
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)
}
}

View File

@ -1,11 +1,15 @@
#![allow(dead_code)] #![allow(dead_code)]
use crate::{button::Modifier, keys::Key};
use bytemuck::{cast_ref, Pod, Zeroable};
use core::mem::size_of;
/// KeyboardReport describes a report and its companion descriptor that can be /// 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 /// used to send keyboard button presses to a host and receive the status of the
/// keyboard LEDs. /// keyboard LEDs.
/// ///
/// Unlike usbd_hids KeyboardReport, this one supports N-key rollover. /// Unlike usbd_hids KeyboardReport, this one supports N-key rollover.
#[derive(Clone, PartialEq, Eq)] #[derive(Clone, Copy, PartialEq, Eq, Zeroable, Pod)]
#[cfg(feature = "n-key-rollover")] #[cfg(feature = "n-key-rollover")]
#[repr(C, packed)] #[repr(C, packed)]
pub struct KeyboardReport { pub struct KeyboardReport {
@ -15,9 +19,6 @@ pub struct KeyboardReport {
pub keycodes: [u8; 27], pub keycodes: [u8; 27],
} }
use core::mem::{size_of, transmute};
use tgnt::{button::Modifier, keys::Key};
#[cfg(not(feature = "n-key-rollover"))] #[cfg(not(feature = "n-key-rollover"))]
pub use ::usbd_hid::descriptor::KeyboardReport; pub use ::usbd_hid::descriptor::KeyboardReport;
@ -35,23 +36,30 @@ pub const EMPTY_KEYBOARD_REPORT: KeyboardReport = KeyboardReport {
keycodes: [0; 6], 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")] #[cfg(feature = "n-key-rollover")]
impl KeyboardReport { impl KeyboardReport {
#[inline(always)] #[inline(always)]
pub fn set_key(&mut self, key: Key, pressed: bool) { pub fn set_key(&mut self, key: Key, pressed: bool) {
let keycode = u8::from(key); let (byte_i, mask) = key_to_byte_mask(key);
let byte = keycode >> 3;
let bit = keycode & 0b111;
let mask = 1 << bit;
if let Some(k) = self.keycodes.get_mut(byte as usize) { if let Some(k) = self.keycodes.get_mut(byte_i) {
if pressed { if pressed {
*k |= mask; *k |= mask;
} else { } else {
*k &= !mask; *k &= !mask;
} }
} else { } else {
log::warn!("Tried to set out-of-range keycode: 0x{keycode:x}"); log::warn!("Tried to set out-of-range keycode: 0x{:x}", u8::from(key));
} }
} }
@ -65,6 +73,17 @@ impl KeyboardReport {
self.set_key(key, false) self.set_key(key, false)
} }
#[inline(always)]
pub fn key_pressed(&mut self, key: Key) -> bool {
let (byte_i, mask) = key_to_byte_mask(key);
if let Some(k) = self.keycodes.get_mut(byte_i) {
(*k & mask) != 0
} else {
log::warn!("Tried to get out-of-range keycode: 0x{:x}", u8::from(key));
false
}
}
#[inline(always)] #[inline(always)]
pub fn set_modifier(&mut self, modifier: Modifier, pressed: bool) { pub fn set_modifier(&mut self, modifier: Modifier, pressed: bool) {
if pressed { if pressed {
@ -84,10 +103,14 @@ impl KeyboardReport {
self.set_modifier(modifier, false) self.set_modifier(modifier, false)
} }
#[inline(always)]
pub fn modifier_pressed(&mut self, modifier: Modifier) -> bool {
(self.modifier & u8::from(modifier)) != 0
}
#[inline(always)] #[inline(always)]
pub fn as_bytes(&self) -> &[u8; size_of::<KeyboardReport>()] { pub fn as_bytes(&self) -> &[u8; size_of::<KeyboardReport>()] {
// SAFETY: KeyboardReport is repr(C, packed) and contains only u8s. cast_ref(self)
unsafe { transmute(self) }
} }
} }

219
lib/src/usb/serial.rs Normal file
View File

@ -0,0 +1,219 @@
use crate::{
logger::{LogOutput, TimestampedRecord},
serial_proto::borrowed::{DeviceMsg, LogRecord},
serial_proto::owned::HostMsg,
util::{DisplayPack, CS},
};
use super::MAX_PACKET_SIZE;
use core::future::pending;
use embassy_executor::Spawner;
use embassy_rp::{peripherals::USB, usb::Driver};
use embassy_sync::{channel::Channel, pipe::Pipe};
use embassy_time::{Duration, Timer};
use embassy_usb::{
class::cdc_acm::{self, CdcAcmClass},
Builder,
};
use embassy_usb_driver::EndpointError;
use futures::{select_biased, FutureExt};
use msgpck::{MsgPack, MsgUnpack, PackErr, UnpackErr};
use static_cell::StaticCell;
pub const BUFFER_SIZE: usize = 16 * 1024;
static OUT: Pipe<CS, BUFFER_SIZE> = Pipe::new();
pub static IN: Channel<CS, HostMsg, 16> = Channel::new(); // TODO: read from this guy
#[derive(Clone)]
pub struct UsbSerial;
pub async fn setup(usb_builder: &mut Builder<'static, Driver<'static, USB>>) -> &'static UsbSerial {
let spawner = Spawner::for_current_executor().await;
static STATE: StaticCell<cdc_acm::State<'static>> = StaticCell::new();
let state = STATE.init(cdc_acm::State::new());
let class = CdcAcmClass::new(usb_builder, state, MAX_PACKET_SIZE as u16);
spawner.must_spawn(serial_task(class));
static USB_SERIAL: UsbSerial = UsbSerial;
&USB_SERIAL
}
async fn wait_connection(class: &mut CdcAcmClass<'static, Driver<'static, USB>>) {
class.wait_connection().await;
OUT.clear();
}
#[embassy_executor::task]
async fn serial_task(mut class: CdcAcmClass<'static, Driver<'static, USB>>) {
let mut write_buf = [0u8; MAX_PACKET_SIZE as usize];
let mut read_buf = [0u8; 1024 as usize];
let mut message_parser = MessageParser::new(&mut read_buf);
class.wait_connection().await;
loop {
select_biased! {
n = OUT.read(&mut write_buf).fuse() => {
let write = async {
class.write_packet(&write_buf[..n]).await?;
// if we send a packet containing exactly MAX_PACKET_SIZE bytes, we need to send another
// packet to "flush" the buffer.
if OUT.len() == 0 && n == usize::from(MAX_PACKET_SIZE) {
let _ = class.write_packet(&[]).await?;
}
Ok(())
};
match write.await {
Ok(()) => {}
Err(EndpointError::Disabled) => wait_connection(&mut class).await,
Err(EndpointError::BufferOverflow) => {
// not much we can do if this happens, just ignore the error
}
}
}
//r = class.read_packet(&mut read_buf).fuse() => {
r = message_parser.read(&mut class).fuse() => {
match r {
Ok(Some(message)) => {
log::info!("Got message!!!: {:?}", message);
if IN.try_send(message).is_err() {
log::error!("USB serial in buffer is full");
}
}
Ok(None) |
Err(EndpointError::Disabled) => wait_connection(&mut class).await,
Err(EndpointError::BufferOverflow) => {
// wtf, this shouldn't happen?
panic!("usb serial buffer overflow on read");
}
}
}
}
}
}
/// Send a [DeviceMsg] over the serial connection.
///
/// If the OUT buffer is full, the message may be partially dropped.
///
/// If no serial connection is active, the message may not be ever be sent.
pub fn serial_send(message: &DeviceMsg<'_>) {
struct UsbWriter;
impl msgpck::Write for UsbWriter {
fn write_all(&mut self, bytes: &[u8]) -> Result<(), PackErr> {
OUT.try_write(bytes).map_err(|_| PackErr::BufferOverflow)?;
Ok(())
}
}
let _ = message.pack_with_writer(&mut UsbWriter);
}
impl LogOutput for UsbSerial {
fn log(&self, record: &TimestampedRecord) {
let ms = record.timestamp.as_millis();
let level = record.metadata().level();
let record = LogRecord {
timestamp: ms,
level: DisplayPack(level),
message: DisplayPack(record.args()),
};
serial_send(&DeviceMsg::Log(record));
}
}
pub struct MessageParser<'a> {
pub buf: &'a mut [u8],
pub len: usize,
}
impl<'buf> MessageParser<'buf> {
pub fn new(buf: &'buf mut [u8]) -> Self {
Self { buf, len: 0 }
}
pub async fn read(
&mut self,
class: &mut CdcAcmClass<'static, Driver<'static, USB>>,
) -> Result<Option<HostMsg>, EndpointError> {
loop {
// try to parse messages from the buffer
if self.len > 0 {
log::debug!("buf: {:x?}", &self.buf[..self.len]);
}
let mut reader = &mut &self.buf[..self.len];
match HostMsg::unpack(&mut reader) {
Ok(r) => {
// remove the decoded bytes from buf
if reader.is_empty() {
self.len = 0;
} else {
let bytes_read = self.len - reader.len();
self.buf.rotate_left(bytes_read);
self.len -= bytes_read;
}
log::debug!("received message: {r:?}");
return Ok(Some(r));
}
// we probably have not gotten the entire message yet, go back to reading bytes.
// if the message is corrupted, we will eventually hit MESSAGE_TIMEOUT or
// max buffer size.
Err(UnpackErr::UnexpectedEof) => {}
// on any other error, the message is corrupt. clear the buffer.
Err(_e) => {
log::error!("{:?}", _e);
self.len = 0;
}
};
// if buffer is not empty, this future will sleep until the pending message times out
let buf_is_empty = self.len == 0;
let timeout = async {
if buf_is_empty {
pending().await
} else {
const MESSAGE_TIMEOUT: Duration = Duration::from_millis(30);
Timer::after(MESSAGE_TIMEOUT).await
}
};
// try to read some bytes from the file
let n = select_biased! {
n = class.read_packet(&mut self.buf[self.len..]).fuse() => n?,
_ = timeout.fuse() => {
log::debug!("clearing buffer");
self.len = 0;
continue;
}
};
// make sure we're not just reading garbage forever
if self.len >= self.buf.len() {
log::debug!("max message size exceeded");
self.len = 0;
continue;
}
self.len += n;
log::info!("read {} bytes, buf: {:x?}", n, &self.buf[..self.len]);
// exit on eof
if n == 0 {
log::error!("read 0 bytes, fuck.");
return Ok(None);
}
}
}
}

214
lib/src/util.rs Normal file
View File

@ -0,0 +1,214 @@
use core::{
cell::UnsafeCell,
fmt::{self, Debug, Display, Write},
mem::MaybeUninit,
ops::Deref,
};
use embassy_futures::yield_now;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_time::{Duration, Timer};
use msgpck::{Marker, MsgPack, PackErr, Piece};
use portable_atomic::{AtomicU32, Ordering};
use crate::rgb::Rgb;
pub type CS = CriticalSectionRawMutex;
#[derive()]
pub struct SwapCell<T> {
// active_slot: 1 bit
// readers: 31 bits
entries: AtomicU32,
exits: AtomicU32,
slot0: UnsafeCell<MaybeUninit<T>>,
slot1: UnsafeCell<MaybeUninit<T>>,
}
pub struct SwapCellWrite<T: 'static> {
cell: &'static SwapCell<T>,
}
#[derive(Clone)]
pub struct SwapCellRead<T: 'static> {
cell: &'static SwapCell<T>,
}
pub struct SwapCellGuard<'a, T> {
cell: &'a SwapCell<T>,
slot: &'a T,
}
const SLOT_MASK: u32 = 0x80000000;
const ENTRIES_MASK: u32 = 0x7FFFFFFF;
impl<T> SwapCell<T> {
pub const fn new(initial: T) -> Self {
SwapCell {
entries: AtomicU32::new(0),
exits: AtomicU32::new(0),
slot0: UnsafeCell::new(MaybeUninit::new(initial)),
slot1: UnsafeCell::new(MaybeUninit::uninit()),
}
}
pub fn split(&'static mut self) -> (SwapCellRead<T>, SwapCellWrite<T>) {
(SwapCellRead { cell: self }, SwapCellWrite { cell: self })
}
}
impl<T> SwapCellWrite<T> {
pub async fn write(&mut self, t: T) {
let x = self.cell.entries.load(Ordering::SeqCst);
let active_slot = (x & SLOT_MASK).rotate_left(1) as usize;
let mut slots = [&self.cell.slot0, &self.cell.slot1];
slots.rotate_left(active_slot);
let [active_slot, inactive_slot] = slots;
// SAFETY: no one elase is allowed to touch the inactive slot.
unsafe { inactive_slot.get().write(MaybeUninit::new(t)) };
// swap active/inactive slots
let x = self.cell.entries.fetch_xor(SLOT_MASK, Ordering::SeqCst);
let (_active_slot, inactive_slot) = (inactive_slot, active_slot);
// wait until there are no more readers in the previously active slot
let entries = x & ENTRIES_MASK;
loop {
let exits = self.cell.exits.load(Ordering::SeqCst);
if exits >= entries {
break;
}
yield_now().await;
}
// drop the now inactive slot
// SAFETY: we waited until everyone else stopped using this slot.
unsafe { inactive_slot.get().drop_in_place() };
}
}
impl<T> SwapCellRead<T> {
pub fn read(&self) -> SwapCellGuard<'_, T> {
let x = self.cell.entries.fetch_add(1, Ordering::SeqCst);
let entries = x & ENTRIES_MASK;
debug_assert!(entries < ENTRIES_MASK, "SwapCell overflowed");
let slot = (x & SLOT_MASK).rotate_left(1) as usize;
let slot = [&self.cell.slot0, &self.cell.slot1][slot];
let slot = unsafe { &*slot.get() };
let slot = unsafe { slot.assume_init_ref() };
SwapCellGuard {
cell: self.cell,
slot,
}
}
}
impl<T> Drop for SwapCellGuard<'_, T> {
fn drop(&mut self) {
self.cell.exits.fetch_add(1, Ordering::SeqCst);
}
}
impl<T> Deref for SwapCellGuard<'_, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
self.slot
}
}
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)
}
}
/// Calculate the length of the formatted string of a type that impls Display.
pub fn display_len(t: &impl Display) -> usize {
// impl fmt::Write for a dummy struct that just increments a length
struct Write<'a>(&'a mut usize);
impl fmt::Write for Write<'_> {
fn write_str(&mut self, s: &str) -> fmt::Result {
*self.0 += s.len();
Ok(())
}
}
let mut n = 0;
let mut w = Write(&mut n);
write!(&mut w, "{}", t).expect("Write impl is infallible");
n
}
/// Wrapper type that impls [MsgPack] for `T: Display`, by serializing `T` as a msgpck string.
pub struct DisplayPack<T>(pub T);
impl<T: Display> MsgPack for DisplayPack<T> {
fn pack(&self) -> impl Iterator<Item = msgpck::Piece<'_>> {
"TODO".pack()
//let len = display_len(&self.0) as u32;
//[msgpck::Marker::Str32.into(), len.into()]
// .into_iter()
// .chain(iter::from_fn(move || None)) // TODO
}
fn pack_with_writer(&self, w: &mut dyn msgpck::Write) -> Result<usize, PackErr> {
let mut n = 0;
let str_len = display_len(&self.0);
for bytes in [
Piece::from(Marker::Str32).as_bytes(),
Piece::from(str_len as u32).as_bytes(),
] {
w.write_all(bytes)?;
n += bytes.len();
}
// this is advanced stupid.
struct Write<'a>(&'a mut dyn msgpck::Write);
impl fmt::Write for Write<'_> {
fn write_str(&mut self, s: &str) -> fmt::Result {
self.0
.write_all(s.as_bytes())
.map_err(|_| fmt::Error::default())?;
Ok(())
}
}
write!(&mut Write(w), "{}", self.0).map_err(|_| PackErr::BufferOverflow)?;
n += str_len;
Ok(n)
}
}
impl<T: Debug> Debug for DisplayPack<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Debug::fmt(&self.0, f)
}
}

92
lib/src/ws2812.rs Normal file
View File

@ -0,0 +1,92 @@
use embassy_rp::dma::{self, AnyChannel};
use embassy_rp::interrupt::typelevel::Binding;
use embassy_rp::pio::{self, FifoJoin, Instance, Pio, PioPin, ShiftConfig, ShiftDirection};
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,
irqs: impl Binding<P::Interrupt, pio::InterruptHandler<P>>,
dma: impl dma::Channel,
pin: impl PioPin,
) -> Self {
let mut pio = Pio::new(pio, irqs);
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(&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;
}
}

2
right/.cargo/config.toml Normal file
View File

@ -0,0 +1,2 @@
[build]
target = "thumbv6m-none-eabi"

28
right/Cargo.toml Normal file
View File

@ -0,0 +1,28 @@
[package]
name = "tangentbord1-right"
version = "0.1.0"
authors = ["Joakim Hulthe <joakim@hulthe.net>"]
description = "Keyboard firmware"
edition = "2021"
[dependencies.tangentbord1]
path = "../lib"
package = "tangentbord1-lib"
[dependencies]
cortex-m-rt = "0.7"
embassy-rp = { version = "0.1.0", features = ["time-driver", "critical-section-impl"] }
embassy-executor = { version = "0.5.0", features = ["nightly", "executor-thread", "integrated-timers", "arch-cortex-m"] }
embassy-sync = "0.5.0"
embassy-time = "0.3.0"
embassy-futures = "0.1.1"
log = "0.4.17"
postcard = { version = "1.0.4", features = ["alloc"] }
[build-dependencies]
ron = "0.8.0"
postcard = { version = "1", features = ["use-std"] }
[build-dependencies.tangentbord1]
path = "../lib"
package = "tangentbord1-lib"

59
right/build.rs Normal file
View File

@ -0,0 +1,59 @@
//! This build script copies the `memory.x` file from the crate root into
//! a directory where the linker can always find it at build time.
//! For many projects this is optional, as the linker always searches the
//! project root directory -- wherever `Cargo.toml` is. However, if you
//! are using a workspace or have a more complicated build setup, this
//! build script becomes required. Additionally, by requesting that
//! Cargo re-run the build script whenever `memory.x` is changed,
//! updating `memory.x` ensures a rebuild of the application with the
//! new memory settings.
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
use std::{env, fs};
use tangentbord1::layer::Layer;
fn main() {
memory();
serialize_layout("./layers.ron", "./src/layers.pc");
}
fn memory() {
// Put `memory.x` in our output directory and ensure it's
// on the linker search path.
let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap());
File::create(out.join("memory.x"))
.unwrap()
.write_all(include_bytes!("../memory.x"))
.unwrap();
println!("cargo:rustc-link-search={}", out.display());
// By default, Cargo will re-run a build script whenever
// any file in the project changes. By specifying `memory.x`
// here, we ensure the build script is only re-run when
// `memory.x` is changed.
println!("cargo:rerun-if-changed=../memory.x");
// --nmagic turns off page alignment of sections (which saves flash space)
println!("cargo:rustc-link-arg-bins=--nmagic");
println!("cargo:rustc-link-arg-bins=-Tlink.x");
println!("cargo:rustc-link-arg-bins=-Tlink-rp.x");
//println!("cargo:rustc-link-arg-bins=-Tdefmt.x");
}
fn serialize_layout(ron_path: &str, postcard_path: &str) {
println!("cargo:rerun-if-changed={ron_path}");
let layers = fs::read_to_string(ron_path).expect("Failed to read .ron");
let layers: Vec<Vec<Layer>> = ron::from_str(&layers).expect("Failed to deserialize .ron");
let serialized = postcard::to_stdvec(&layers).expect("Failed to serialize layers");
File::create(postcard_path)
.expect("Failed to create .pc")
.write_all(&serialized)
.expect("Failed to write .pc");
}

181
right/layers.ron Normal file
View File

@ -0,0 +1,181 @@
[
[
Layer(
buttons: [
// Row 1
Key(F),
Key(G),
Key(C),
Key(R),
Key(L),
// Row 2
Key(D),
ModTap(H, RCtrl),
ModTap(T, RShift),
ModTap(N, RMod),
ModTap(S, RAlt),
// Row 3
Key(B),
Key(M),
Key(W),
Key(V),
Key(Z),
// Thumbpad
Layer(Peek, Right, 1),
Key(Return),
Key(Delete),
],
),
Layer(
buttons: [
// Row 1
Compose2(Lower, O, Variable, A), // å
Key(D7),
Key(D8),
Key(D9),
Key(D0),
// Row 2
Compose2(Upper, Apostrophe, Variable, A), // ä
ModTap(D4, RCtrl),
ModTap(D5, RShift),
ModTap(D6, RMod),
//ModTap(, RAlt),
Mod(RAlt),
// Row 3
Compose2(Upper, Apostrophe, Variable, O), // ö
Key(D1),
Key(D2),
Key(D3),
None,
// Thumbpad
Layer(Peek, Right, 1),
Key(Return),
Key(Delete),
],
),
Layer(
buttons: [
// Row 1
None,
Key(Mute),
Key(VolumeDown),
Key(VolumeUp),
Layer(Move, Down, 1),
// Row 2
Key(LeftArrow),
Key(DownArrow),
Key(UpArrow),
Key(RightArrow),
None,
// Row 3
Key(Home),
Key(PageDown),
Key(PageUp),
Key(End),
None,
// Thumbpad
Layer(Peek, Right, 1),
Key(Return),
Key(Delete),
],
),
],
[ // gaming row
Layer(
buttons: [
// Row 1
Key(Y),
Key(U),
Key(I),
Key(O),
Key(P),
// Row 2
Key(H),
Key(J),
Key(K),
Key(L),
Key(G),
// Row 3
Key(B),
Key(N),
Key(M),
Key(Comma),
Key(Period),
// Thumbpad
Layer(Peek, Right, 1),
Key(Return),
Key(Delete),
],
),
Layer(
buttons: [
// Row 1
None,
None,
Key(CapsLock),
Key(Tab),
None,
// Row 2
None,
None,
None,
None,
None,
// Row 3
None,
None,
None,
None,
None,
// Thumbpad
Layer(Peek, Right, 1),
Key(Return),
Key(Delete),
],
),
Layer(
buttons: [
// Row 1
None,
None,
None,
None,
Layer(Move, Up, 1),
// Row 2
None,
None,
None,
None,
None,
// Row 3
None,
None,
None,
None,
None,
// Thumbpad
Layer(Peek, Right, 1),
Key(Return),
Key(Delete),
],
),
],
]

113
right/src/main.rs Normal file
View File

@ -0,0 +1,113 @@
//! Firmware for Tangentbord1, right half.
#![no_std]
#![no_main]
#![feature(type_alias_impl_trait)]
extern crate alloc;
extern crate cortex_m_rt;
use alloc::vec::Vec;
use embassy_executor::Spawner;
use embassy_rp::gpio::{Level, Output, Pin};
use embassy_time::Timer;
use log::error;
use tangentbord1::{
board::Board,
event::Half,
interrupts::Irqs,
keyboard::KeyboardConfig,
layer::Layer,
logger::LogMultiplexer,
rgb::Rgb,
util::stall,
ws2812::Ws2812,
{allocator, rtt, uart, usb},
};
#[embassy_executor::main]
async fn main(_spawner: Spawner) {
let half = Half::Right;
let rtt_logger = rtt::init_rtt_logger();
allocator::init();
let p = embassy_rp::init(Default::default());
let board = Board::from(p);
let _neopixel_power = Output::new(board.neopixel_power, Level::High);
let mut neopixel = Ws2812::new(board.PIO0, Irqs, board.DMA_CH0, board.neopixel);
let neopixels_d5 = Ws2812::new(board.PIO1, Irqs, board.DMA_CH1, board.d5);
neopixel.write(&[Rgb::new(0xFF, 0x00, 0x00)]).await;
let layers = include_bytes!("layers.pc");
let Ok(layers): Result<Vec<Vec<Layer>>, _> = postcard::from_bytes(layers) else {
log::error!("Failed to deserialize layer config");
stall().await
};
let keyboard = KeyboardConfig {
half,
pins: [
// row 1
board.d12.degrade(),
board.d11.degrade(),
board.d10.degrade(),
board.d9.degrade(),
board.d7.degrade(),
// row 2
board.a2.degrade(),
board.a1.degrade(),
board.a0.degrade(),
board.sda.degrade(),
board.scl.degrade(),
// row 3
board.mosi.degrade(),
board.sck.degrade(),
board.d25.degrade(),
board.d24.degrade(),
board.a3.degrade(),
// thumbpad
board.d3.degrade(),
board.d2.degrade(),
board.miso.degrade(),
],
led_map: [0, 1, 2, 3, 4, 9, 8, 7, 6, 5, 10, 11, 12, 13, 14, 15, 16, 17],
led_driver: neopixels_d5,
layers,
};
let Some([events1, events2]) = keyboard.create().await else {
error!("failed to create keyboard");
return;
};
uart::start(board.tx, board.rx, board.UART0, half, events2).await;
// TODO: delaying the logger until here is not ideal
let usb_logger = usb::setup_logger_and_keyboard(board.USB, events1).await;
let logger = LogMultiplexer {
outputs: [rtt_logger, usb_logger],
};
logger.init();
log::error!("log_level: error");
log::warn!("log_level: warn");
log::info!("log_level: info");
log::debug!("log_level: debug");
log::trace!("log_level: trace");
neopixel.write(&[Rgb::new(0x00, 0x00, 0xFF)]).await;
Timer::after_secs(5).await;
for b in (0u8..0xff).rev() {
neopixel.write(&[Rgb::new(0, 0, b)]).await;
Timer::after_millis(10).await;
}
stall().await
}

37
run
View File

@ -1,19 +1,26 @@
#!/bin/sh #!/usr/bin/env nu
set -e # Build, flash, and run the firmware.
#
# Will also attach to serial logs
def main [
bin: string # Which firmware to run, 'left' or 'right'.
--serial (-s): string # The serial device to get logs from.
--probe (-p): bool # Flash & run using probe-run instead of elf2uf2
] {
if $probe == true {
cargo build --bin $bin
probe-run --chip RP2040 --speed 4000 ("./target/thumbv6m-none-eabi/debug/" + $bin)
} else {
cargo run --bin $bin
}
cargo run --bin $1 let log_script = ($env.FILE_PWD | path join "serial-logs")
printf "waiting for serial log" if $serial == null {
nu $log_script
} else {
nu $log_script --serial $serial
}
}
SERIAL=/dev/ttyACM0
while [ ! -e $SERIAL ]
do
printf "."
sleep 1
done
echo
sleep 1
cat $SERIAL

View File

@ -1,4 +1,4 @@
[toolchain] [toolchain]
channel = "nightly-2023-02-01" channel = "nightly-2024-02-01"
components = ["rust-std", "rust-src", "rustfmt", "cargo", "clippy"] components = ["rust-std", "rust-src", "rustfmt", "cargo", "clippy"]
targets = ["thumbv6m-none-eabi"] targets = ["thumbv6m-none-eabi"]

View File

@ -17,6 +17,12 @@ _autosave-*
*-save.kicad_pcb *-save.kicad_pcb
fp-info-cache fp-info-cache
# Gerber- & Drillfiles
*.gbr
*.gbrjob
*.drl
*.zip
# Netlist files (exported from Eeschema) # Netlist files (exported from Eeschema)
*.net *.net

View File

@ -1589,9 +1589,9 @@
(pad "" np_thru_hole circle (at -4.75 0) (size 1.6 1.6) (drill 1.6) (layers "F&B.Cu" "*.Mask") (tstamp 4ef9163a-607f-4c16-ba07-039ed3c9a178)) (pad "" np_thru_hole circle (at -4.75 0) (size 1.6 1.6) (drill 1.6) (layers "F&B.Cu" "*.Mask") (tstamp 4ef9163a-607f-4c16-ba07-039ed3c9a178))
(pad "" np_thru_hole circle (at 2.25 0) (size 1.6 1.6) (drill 1.6) (layers "F&B.Cu" "*.Mask") (tstamp 4d5d0edd-f7c9-4099-8b76-1fe6539cd315)) (pad "" np_thru_hole circle (at 2.25 0) (size 1.6 1.6) (drill 1.6) (layers "F&B.Cu" "*.Mask") (tstamp 4d5d0edd-f7c9-4099-8b76-1fe6539cd315))
(pad "R1" smd rect (at -0.15 -3.25) (size 1.2 2.5) (layers "F.Cu" "F.Paste" "F.Mask") (pad "R1" smd rect (at -0.15 -3.25) (size 1.2 2.5) (layers "F.Cu" "F.Paste" "F.Mask")
(net 28 "/RX") (pintype "passive") (tstamp c1f0e596-c9ea-4fc4-94cb-42bfcdbf078f)) (net 29 "/TX") (pintype "passive") (tstamp c1f0e596-c9ea-4fc4-94cb-42bfcdbf078f))
(pad "R2" smd rect (at -3.15 -3.25) (size 1.2 2.5) (layers "F.Cu" "F.Paste" "F.Mask") (pad "R2" smd rect (at -3.15 -3.25) (size 1.2 2.5) (layers "F.Cu" "F.Paste" "F.Mask")
(net 29 "/TX") (pintype "passive") (tstamp b5ea9e55-dd5f-4fd9-824b-933f1a2993cc)) (net 28 "/RX") (pintype "passive") (tstamp b5ea9e55-dd5f-4fd9-824b-933f1a2993cc))
(pad "S" smd rect (at 4.95 3.25) (size 1.2 2.5) (layers "F.Cu" "F.Paste" "F.Mask") (pad "S" smd rect (at 4.95 3.25) (size 1.2 2.5) (layers "F.Cu" "F.Paste" "F.Mask")
(net 1 "GND") (pintype "passive") (tstamp b272ec82-9723-4dde-bb98-36cdd8b785f6)) (net 1 "GND") (pintype "passive") (tstamp b272ec82-9723-4dde-bb98-36cdd8b785f6))
(pad "T" smd rect (at 3.85 -3.25) (size 1.2 2.5) (layers "F.Cu" "F.Paste" "F.Mask") (pad "T" smd rect (at 3.85 -3.25) (size 1.2 2.5) (layers "F.Cu" "F.Paste" "F.Mask")
@ -5847,30 +5847,31 @@
(segment (start 77.136396 45) (end 80.636396 41.5) (width 0.25) (layer "B.Cu") (net 27) (tstamp 96085405-d09c-4171-af0f-7c144dec03cf)) (segment (start 77.136396 45) (end 80.636396 41.5) (width 0.25) (layer "B.Cu") (net 27) (tstamp 96085405-d09c-4171-af0f-7c144dec03cf))
(segment (start 132 40.5) (end 139.142 47.642) (width 0.25) (layer "B.Cu") (net 27) (tstamp a094ca3f-357b-4f11-b63a-18a36964edb4)) (segment (start 132 40.5) (end 139.142 47.642) (width 0.25) (layer "B.Cu") (net 27) (tstamp a094ca3f-357b-4f11-b63a-18a36964edb4))
(segment (start 101 38) (end 117 38) (width 0.25) (layer "B.Cu") (net 27) (tstamp c657351f-21db-4a82-9d4e-a4b18388e41b)) (segment (start 101 38) (end 117 38) (width 0.25) (layer "B.Cu") (net 27) (tstamp c657351f-21db-4a82-9d4e-a4b18388e41b))
(segment (start 34.6 72.75) (end 35.25 72.75) (width 0.25) (layer "F.Cu") (net 28) (tstamp 2aecbfc7-ff0a-4003-b8bb-0af4755158e1)) (segment (start 32.75 71.6) (end 31.6 72.75) (width 0.25) (layer "F.Cu") (net 28) (tstamp 257d91be-ba3e-4c65-a2dc-d0b8c6ff460e))
(segment (start 35.25 72.75) (end 36 72) (width 0.25) (layer "F.Cu") (net 28) (tstamp 92911de8-3a23-4b4f-96b9-46b91c3bce7f)) (segment (start 32.75 71) (end 32.75 71.6) (width 0.25) (layer "F.Cu") (net 28) (tstamp c611b538-e729-44f3-8bca-31f31edb2fd3))
(segment (start 48.74 70.76) (end 48.5 71) (width 0.25) (layer "F.Cu") (net 28) (tstamp de259030-01a2-4071-94bc-39f7ef7d2749)) (segment (start 48.74 70.76) (end 48.5 71) (width 0.25) (layer "F.Cu") (net 28) (tstamp de259030-01a2-4071-94bc-39f7ef7d2749))
(segment (start 48.74 63.52) (end 48.74 70.76) (width 0.25) (layer "F.Cu") (net 28) (tstamp e48b1469-3af2-4118-9bc5-9e5c37c94163)) (segment (start 48.74 63.52) (end 48.74 70.76) (width 0.25) (layer "F.Cu") (net 28) (tstamp e48b1469-3af2-4118-9bc5-9e5c37c94163))
(via (at 36 72) (size 0.8) (drill 0.4) (layers "F.Cu" "B.Cu") (net 28) (tstamp 878bff88-e36a-4f24-a538-a4b52aa7d7e0)) (via (at 32.75 71) (size 0.8) (drill 0.4) (layers "F.Cu" "B.Cu") (net 28) (tstamp 0a82a331-2d2b-4f04-9dcf-9a403b395a28))
(via (at 48.5 71) (size 0.8) (drill 0.4) (layers "F.Cu" "B.Cu") (net 28) (tstamp ebcacad5-955c-4c4f-8978-1a135b18c83f)) (via (at 48.5 71) (size 0.8) (drill 0.4) (layers "F.Cu" "B.Cu") (net 28) (tstamp ebcacad5-955c-4c4f-8978-1a135b18c83f))
(segment (start 36 72) (end 37 71) (width 0.25) (layer "B.Cu") (net 28) (tstamp 32b66a24-bed2-40ca-8c77-fc574ad69eb9))
(segment (start 46.5 71) (end 37 71) (width 0.25) (layer "B.Cu") (net 28) (tstamp 4a5a63b9-3924-4d58-9b5d-a611ce3e3167)) (segment (start 46.5 71) (end 37 71) (width 0.25) (layer "B.Cu") (net 28) (tstamp 4a5a63b9-3924-4d58-9b5d-a611ce3e3167))
(segment (start 47.800305 71.725) (end 47.199695 71.725) (width 0.25) (layer "B.Cu") (net 28) (tstamp 4d4eef30-5c7a-426c-8b81-b3f88f8efe9c)) (segment (start 47.800305 71.725) (end 47.199695 71.725) (width 0.25) (layer "B.Cu") (net 28) (tstamp 4d4eef30-5c7a-426c-8b81-b3f88f8efe9c))
(segment (start 48.5 71) (end 48.5 71.025305) (width 0.25) (layer "B.Cu") (net 28) (tstamp 4f3a45aa-140a-4a61-a8cd-54aabe5401d5)) (segment (start 48.5 71) (end 48.5 71.025305) (width 0.25) (layer "B.Cu") (net 28) (tstamp 4f3a45aa-140a-4a61-a8cd-54aabe5401d5))
(segment (start 48.74 63.52) (end 51.28 63.52) (width 0.25) (layer "B.Cu") (net 28) (tstamp 5cf03d53-3dcf-4352-a586-d34a50742a03)) (segment (start 48.74 63.52) (end 51.28 63.52) (width 0.25) (layer "B.Cu") (net 28) (tstamp 5cf03d53-3dcf-4352-a586-d34a50742a03))
(segment (start 37 71) (end 32.75 71) (width 0.25) (layer "B.Cu") (net 28) (tstamp 5dabcd76-b7d6-48f1-8c8e-cca58a79a12e))
(segment (start 48.5 71.025305) (end 47.800305 71.725) (width 0.25) (layer "B.Cu") (net 28) (tstamp bc5b52e8-ce44-425c-8735-a816f74ea50e)) (segment (start 48.5 71.025305) (end 47.800305 71.725) (width 0.25) (layer "B.Cu") (net 28) (tstamp bc5b52e8-ce44-425c-8735-a816f74ea50e))
(segment (start 46.5 71.025305) (end 46.5 71) (width 0.25) (layer "B.Cu") (net 28) (tstamp f17f6ff6-e9b0-49eb-be28-46887e686fb7)) (segment (start 46.5 71.025305) (end 46.5 71) (width 0.25) (layer "B.Cu") (net 28) (tstamp f17f6ff6-e9b0-49eb-be28-46887e686fb7))
(segment (start 47.199695 71.725) (end 46.5 71.025305) (width 0.25) (layer "B.Cu") (net 28) (tstamp fe3d94be-7f6b-4326-a169-590e775626c3)) (segment (start 47.199695 71.725) (end 46.5 71.025305) (width 0.25) (layer "B.Cu") (net 28) (tstamp fe3d94be-7f6b-4326-a169-590e775626c3))
(segment (start 31.6 71.4) (end 32.5 70.5) (width 0.25) (layer "F.Cu") (net 29) (tstamp 439e4474-2835-4e39-8928-78eb9cf36d68))
(segment (start 47.475 62.245) (end 48.74 60.98) (width 0.25) (layer "F.Cu") (net 29) (tstamp 6cda5ff2-ccee-40cb-bfc0-3cf6a723164e)) (segment (start 47.475 62.245) (end 48.74 60.98) (width 0.25) (layer "F.Cu") (net 29) (tstamp 6cda5ff2-ccee-40cb-bfc0-3cf6a723164e))
(segment (start 47.475 62.245) (end 47.475 70.975) (width 0.25) (layer "F.Cu") (net 29) (tstamp a57d3b54-bc08-45e5-9b51-7f33dab65a8f)) (segment (start 47.475 62.245) (end 47.475 70.975) (width 0.25) (layer "F.Cu") (net 29) (tstamp a57d3b54-bc08-45e5-9b51-7f33dab65a8f))
(segment (start 34.6 70.4) (end 34.6 72.75) (width 0.25) (layer "F.Cu") (net 29) (tstamp bd02cff6-e2e4-4c46-ac64-4b693c23d326))
(segment (start 47.475 70.975) (end 47.5 71) (width 0.25) (layer "F.Cu") (net 29) (tstamp cd6b777f-10ca-467b-9eeb-3c4c6834ad9d)) (segment (start 47.475 70.975) (end 47.5 71) (width 0.25) (layer "F.Cu") (net 29) (tstamp cd6b777f-10ca-467b-9eeb-3c4c6834ad9d))
(segment (start 31.6 72.75) (end 31.6 71.4) (width 0.25) (layer "F.Cu") (net 29) (tstamp d8660258-e773-4a0e-90ee-4e11a0774814)) (segment (start 34.75 70.25) (end 34.6 70.4) (width 0.25) (layer "F.Cu") (net 29) (tstamp de022714-9ff9-4605-8b63-77ce8ad6b1da))
(via (at 47.5 71) (size 0.8) (drill 0.4) (layers "F.Cu" "B.Cu") (net 29) (tstamp 5dbd696e-afc2-4ea2-b4f6-d4be983e5e15)) (via (at 47.5 71) (size 0.8) (drill 0.4) (layers "F.Cu" "B.Cu") (net 29) (tstamp 5dbd696e-afc2-4ea2-b4f6-d4be983e5e15))
(via (at 32.5 70.5) (size 0.8) (drill 0.4) (layers "F.Cu" "B.Cu") (net 29) (tstamp bd2abb3c-6b02-45db-b7d7-7813beeae0c7)) (via (at 34.75 70.25) (size 0.8) (drill 0.4) (layers "F.Cu" "B.Cu") (net 29) (tstamp edd6d0ee-811e-47b1-a25e-a8dea186bd79))
(segment (start 51.28 60.98) (end 48.74 60.98) (width 0.25) (layer "B.Cu") (net 29) (tstamp 18785f4e-d358-42c8-a146-e286295d644f)) (segment (start 51.28 60.98) (end 48.74 60.98) (width 0.25) (layer "B.Cu") (net 29) (tstamp 18785f4e-d358-42c8-a146-e286295d644f))
(segment (start 47 70.5) (end 35 70.5) (width 0.25) (layer "B.Cu") (net 29) (tstamp 90296410-b0aa-4ba3-9747-539dca0c5653))
(segment (start 47.5 71) (end 47 70.5) (width 0.25) (layer "B.Cu") (net 29) (tstamp a4993e91-f6f3-4cee-89fb-438274989758)) (segment (start 47.5 71) (end 47 70.5) (width 0.25) (layer "B.Cu") (net 29) (tstamp a4993e91-f6f3-4cee-89fb-438274989758))
(segment (start 32.5 70.5) (end 47 70.5) (width 0.25) (layer "B.Cu") (net 29) (tstamp b2f38454-648b-4c69-a719-9f743dee5c67)) (segment (start 35 70.5) (end 34.75 70.25) (width 0.25) (layer "B.Cu") (net 29) (tstamp f07f7ef7-7b0f-45db-b1c6-0d97add181ed))
(segment (start 92.5 80) (end 93.5 79) (width 0.25) (layer "B.Cu") (net 30) (tstamp 028b37a0-0fa2-4168-9364-43f54ea11158)) (segment (start 92.5 80) (end 93.5 79) (width 0.25) (layer "B.Cu") (net 30) (tstamp 028b37a0-0fa2-4168-9364-43f54ea11158))
(segment (start 33.5 60.98) (end 30.96 60.98) (width 0.25) (layer "B.Cu") (net 30) (tstamp 040d2626-2565-4f78-b962-e33c60eadf75)) (segment (start 33.5 60.98) (end 30.96 60.98) (width 0.25) (layer "B.Cu") (net 30) (tstamp 040d2626-2565-4f78-b962-e33c60eadf75))
(segment (start 29.5 71.5) (end 31.5 73.5) (width 0.25) (layer "B.Cu") (net 30) (tstamp 252b4023-5bb1-48af-9179-bbbd954b5c0b)) (segment (start 29.5 71.5) (end 31.5 73.5) (width 0.25) (layer "B.Cu") (net 30) (tstamp 252b4023-5bb1-48af-9179-bbbd954b5c0b))
@ -6907,6 +6908,26 @@
(xy 32.650889 74.246204) (xy 32.650889 74.246204)
(xy 32.701989 74.109201) (xy 32.701989 74.109201)
(xy 32.7085 74.048638) (xy 32.7085 74.048638)
(xy 32.7085 72.589595)
(xy 32.718091 72.541377)
(xy 32.745404 72.5005)
(xy 32.842538 72.403366)
(xy 33.13866 72.107242)
(xy 33.154981 72.094168)
(xy 33.157015 72.092001)
(xy 33.157018 72.092)
(xy 33.203676 72.042312)
(xy 33.206367 72.039535)
(xy 33.226135 72.019769)
(xy 33.228611 72.016576)
(xy 33.236318 72.007551)
(xy 33.248622 71.994449)
(xy 33.273654 71.967796)
(xy 33.323569 71.935235)
(xy 33.382865 71.929255)
(xy 33.438276 71.951195)
(xy 33.477406 71.996147)
(xy 33.4915 72.054053)
(xy 33.4915 74.048638) (xy 33.4915 74.048638)
(xy 33.498011 74.109201) (xy 33.498011 74.109201)
(xy 33.508351 74.136922) (xy 33.508351 74.136922)
@ -6977,7 +6998,7 @@
(xy 39.636785 71.234955) (xy 39.636785 71.234955)
(xy 39.563261 71.136738) (xy 39.563261 71.136738)
(xy 39.446205 71.049111) (xy 39.446205 71.049111)
(xy 39.377702 71.023561) (xy 39.375777 71.022843)
(xy 39.309201 70.998011) (xy 39.309201 70.998011)
(xy 39.248638 70.9915) (xy 39.248638 70.9915)
(xy 37.951362 70.9915) (xy 37.951362 70.9915)
@ -6985,99 +7006,88 @@
(xy 37.753794 71.049111) (xy 37.753794 71.049111)
(xy 37.636738 71.136738) (xy 37.636738 71.136738)
(xy 37.549111 71.253794) (xy 37.549111 71.253794)
(xy 37.499717 71.386224) (xy 37.498888 71.388448)
(xy 37.498011 71.390799) (xy 37.498011 71.390799)
(xy 37.4915 71.451362) (xy 37.4915 71.451362)
(xy 37.4915 74.048638) (xy 37.4915 74.048638)
(xy 35.7085 74.048638) (xy 35.7085 74.048638)
(xy 35.7085 73.239594) (xy 35.7085 71.451362)
(xy 35.718091 73.191376) (xy 35.701989 71.390799)
(xy 35.745405 73.150498) (xy 35.650889 71.253796)
(xy 35.9505 72.945404) (xy 35.636785 71.234955)
(xy 35.991377 72.918091) (xy 35.563261 71.136738)
(xy 36.039595 72.9085) (xy 35.441212 71.045373)
(xy 36.095485 72.9085) (xy 35.399452 70.990591)
(xy 36.095487 72.9085) (xy 35.392741 70.922035)
(xy 36.282288 72.868794) (xy 35.423085 70.860194)
(xy 36.456752 72.791118) (xy 35.426977 70.855872)
(xy 36.611253 72.678866) (xy 35.48904 70.786944)
(xy 36.73904 72.536944) (xy 35.584527 70.621556)
(xy 36.834527 72.371556) (xy 35.643542 70.439928)
(xy 36.893542 72.189928) (xy 35.663504 70.25)
(xy 36.913504 72) (xy 35.643542 70.060072)
(xy 36.893542 71.810072) (xy 35.594846 69.910202)
(xy 36.848375 71.671063) (xy 35.584527 69.878443)
(xy 36.834527 71.628443) (xy 35.489041 69.713057)
(xy 36.739041 71.463057) (xy 35.361252 69.571133)
(xy 36.626274 71.337817) (xy 35.206753 69.458883)
(xy 36.611253 71.321134) (xy 35.206752 69.458882)
(xy 36.456752 71.208882) (xy 35.032288 69.381206)
(xy 36.282288 71.131206) (xy 34.845487 69.3415)
(xy 36.095487 71.0915) (xy 34.654513 69.3415)
(xy 35.904513 71.0915) (xy 34.529978 69.36797)
(xy 35.779978 71.11797) (xy 34.467711 69.381206)
(xy 35.717711 71.131206) (xy 34.293246 69.458883)
(xy 35.705284 71.136739) (xy 34.138747 69.571133)
(xy 35.683782 71.146312) (xy 34.010958 69.713057)
(xy 35.61847 71.156416) (xy 33.915472 69.878443)
(xy 35.557027 71.132072) (xy 33.856458 70.06007)
(xy 35.446205 71.049111) (xy 33.836496 70.25)
(xy 35.377702 71.023561) (xy 33.856458 70.439929)
(xy 35.309201 70.998011) (xy 33.915472 70.621555)
(xy 35.248638 70.9915) (xy 33.949619 70.680699)
(xy 33.951362 70.9915) (xy 33.9665 70.743699)
(xy 33.890799 70.998011) (xy 33.9665 70.882293)
(xy 33.753794 71.049111) (xy 33.956327 70.931893)
(xy 33.636738 71.136738) (xy 33.92745 70.973484)
(xy 33.549111 71.253794) (xy 33.884533 71.000347)
(xy 33.499717 71.386224) (xy 33.840767 71.016672)
(xy 33.498011 71.390799) (xy 33.824222 71.022843)
(xy 33.4915 71.451362) (xy 33.767764 71.030173)
(xy 33.4915 74.048638) (xy 33.713844 71.011905)
(xy 32.7085 74.048638) (xy 33.673468 70.971768)
(xy 32.7085 71.483471) (xy 33.654881 70.917961)
(xy 32.717675 71.43627) (xy 33.643542 70.810072)
(xy 32.743863 71.395944) (xy 33.584527 70.628444)
(xy 32.783248 71.368366) (xy 33.584527 70.628443)
(xy 32.956752 71.291118) (xy 33.489041 70.463057)
(xy 33.111253 71.178866) (xy 33.361252 70.321133)
(xy 33.23904 71.036944) (xy 33.206753 70.208883)
(xy 33.334527 70.871556) (xy 33.206752 70.208882)
(xy 33.393542 70.689928) (xy 33.032288 70.131206)
(xy 33.413504 70.5) (xy 32.845487 70.0915)
(xy 33.393542 70.310072) (xy 32.654513 70.0915)
(xy 33.359632 70.20571) (xy 32.529978 70.11797)
(xy 33.334527 70.128443) (xy 32.467711 70.131206)
(xy 33.239041 69.963057) (xy 32.356811 70.180582)
(xy 33.111252 69.821133) (xy 32.300373 70.20571)
(xy 32.998781 69.739418) (xy 32.293246 70.208883)
(xy 32.956752 69.708882) (xy 32.138747 70.321133)
(xy 32.782288 69.631206) (xy 32.010958 70.463057)
(xy 32.595487 69.5915) (xy 31.915472 70.628443)
(xy 32.404513 69.5915) (xy 31.856458 70.81007)
(xy 32.279978 69.61797) (xy 31.852823 70.844651)
(xy 32.217711 69.631206) (xy 31.850508 70.866688)
(xy 32.043246 69.708883) (xy 31.849248 70.878672)
(xy 31.888747 69.821133) (xy 31.828396 70.935959)
(xy 31.760958 69.963057) (xy 31.783091 70.976752)
(xy 31.665472 70.128443) (xy 31.723938 70.9915)
(xy 31.606458 70.31007)
(xy 31.589093 70.475291)
(xy 31.577509 70.516365)
(xy 31.552878 70.551215)
(xy 31.211336 70.892757)
(xy 31.195017 70.905832)
(xy 31.169146 70.933382)
(xy 31.152133 70.9515)
(xy 31.151895 70.951753)
(xy 31.110086 70.981137)
(xy 31.060045 70.9915)
(xy 30.951362 70.9915) (xy 30.951362 70.9915)
(xy 30.890799 70.998011) (xy 30.890799 70.998011)
(xy 30.753794 71.049111) (xy 30.753794 71.049111)
(xy 30.636738 71.136738) (xy 30.636738 71.136738)
(xy 30.549111 71.253794) (xy 30.549111 71.253794)
(xy 30.499717 71.386224) (xy 30.498888 71.388448)
(xy 30.498011 71.390799) (xy 30.498011 71.390799)
(xy 30.4915 71.451362) (xy 30.4915 71.451362)
(xy 30.4915 74.048638) (xy 30.4915 74.048638)
@ -7215,7 +7225,7 @@
(xy 47.119104 72.140743) (xy 47.119104 72.140743)
(xy 47.120096 72.134477) (xy 47.120096 72.134477)
(xy 47.122504 72.122853) (xy 47.122504 72.122853)
(xy 47.129385 72.096056) (xy 47.130426 72.092)
(xy 47.1335 72.08003) (xy 47.1335 72.08003)
(xy 47.1335 72.059776) (xy 47.1335 72.059776)
(xy 47.135051 72.040066) (xy 47.135051 72.040066)
@ -7378,7 +7388,7 @@
(xy 71.974006 73.026528) (xy 71.974006 73.026528)
(xy 71.910202 72.897145) (xy 71.910202 72.897145)
(xy 71.749908 72.657248) (xy 71.749908 72.657248)
(xy 71.661965 72.556969) (xy 71.648292 72.541377)
(xy 71.559673 72.440326) (xy 71.559673 72.440326)
(xy 71.342749 72.250089) (xy 71.342749 72.250089)
(xy 71.102855 72.089797) (xy 71.102855 72.089797)
@ -7389,16 +7399,16 @@
(xy 69.712097 71.813158) (xy 69.712097 71.813158)
(xy 69.429117 71.869447) (xy 69.429117 71.869447)
(xy 69.155914 71.962186) (xy 69.155914 71.962186)
(xy 68.978135 72.049858) (xy 68.996292 72.040904)
(xy 68.897145 72.089798) (xy 68.897145 72.089798)
(xy 68.861915 72.113338) (xy 68.871031 72.107247)
(xy 68.833774 72.132141) (xy 68.833774 72.132141)
(xy 68.778778 72.152478) (xy 68.778778 72.152478)
(xy 68.720533 72.145723) (xy 68.720533 72.145723)
(xy 68.671652 72.113338) (xy 68.671652 72.113338)
(xy 68.642721 72.062337) (xy 68.642721 72.062337)
(xy 68.640005 72.003764) (xy 68.640005 72.003764)
(xy 68.640723 72) (xy 68.641458 71.996147)
(xy 68.683641 71.77502) (xy 68.683641 71.77502)
(xy 68.70346 71.46) (xy 68.70346 71.46)
(xy 68.683641 71.14498) (xy 68.683641 71.14498)
@ -7424,7 +7434,7 @@
(xy 68.624495 70.834928) (xy 68.624495 70.834928)
(xy 68.526956 70.534734) (xy 68.526956 70.534734)
(xy 68.392562 70.249131) (xy 68.392562 70.249131)
(xy 68.31731 70.130553) (xy 68.333403 70.155911)
(xy 68.223434 69.982627) (xy 68.223434 69.982627)
(xy 68.022233 69.739417) (xy 68.022233 69.739417)
(xy 67.792139 69.523345) (xy 67.792139 69.523345)
@ -7493,14 +7503,14 @@
(xy 83.843221 70.582184) (xy 83.843221 70.582184)
(xy 84.119821 70.734247) (xy 84.119821 70.734247)
(xy 84.413298 70.850443) (xy 84.413298 70.850443)
(xy 84.573804 70.891653) (xy 84.537346 70.882293)
(xy 84.719024 70.92894) (xy 84.719024 70.92894)
(xy 85.032176 70.9685) (xy 85.032176 70.9685)
(xy 85.032179 70.9685) (xy 85.032179 70.9685)
(xy 85.347821 70.9685) (xy 85.347821 70.9685)
(xy 85.347824 70.9685) (xy 85.347824 70.9685)
(xy 85.660975 70.92894) (xy 85.660975 70.92894)
(xy 85.76359 70.902593) (xy 85.703751 70.917957)
(xy 85.966702 70.850443) (xy 85.966702 70.850443)
(xy 86.260179 70.734247) (xy 86.260179 70.734247)
(xy 86.536779 70.582184) (xy 86.536779 70.582184)
@ -7794,7 +7804,7 @@
(xy 109.967263 71.068166) (xy 109.967263 71.068166)
(xy 109.967262 71.068164) (xy 109.967262 71.068164)
(xy 109.890594 70.953433) (xy 109.890594 70.953433)
(xy 109.857156 70.919999) (xy 109.870541 70.933382)
(xy 109.79301 70.85586) (xy 109.79301 70.85586)
(xy 109.678272 70.779205) (xy 109.678272 70.779205)
(xy 109.678268 70.779202) (xy 109.678268 70.779202)
@ -7821,7 +7831,7 @@
(xy 91.205711 71) (xy 91.205711 71)
(xy 91.186841 70.712097) (xy 91.186841 70.712097)
(xy 91.130553 70.42912) (xy 91.130553 70.42912)
(xy 91.090141 70.31007) (xy 91.093897 70.321134)
(xy 91.037813 70.155914) (xy 91.037813 70.155914)
(xy 90.958417 69.994915) (xy 90.958417 69.994915)
(xy 90.910202 69.897145) (xy 90.910202 69.897145)
@ -8051,7 +8061,7 @@
(xy 108 70.205711) (xy 108 70.205711)
(xy 108.287903 70.186841) (xy 108.287903 70.186841)
(xy 108.57088 70.130553) (xy 108.57088 70.130553)
(xy 108.754721 70.068147) (xy 108.778516 70.06007)
(xy 108.844087 70.037812) (xy 108.844087 70.037812)
(xy 108.949089 69.986031) (xy 108.949089 69.986031)
(xy 109.102856 69.910202) (xy 109.102856 69.910202)
@ -8076,14 +8086,14 @@
(xy 121.843221 70.582184) (xy 121.843221 70.582184)
(xy 122.119821 70.734247) (xy 122.119821 70.734247)
(xy 122.413298 70.850443) (xy 122.413298 70.850443)
(xy 122.573804 70.891653) (xy 122.537346 70.882293)
(xy 122.719024 70.92894) (xy 122.719024 70.92894)
(xy 123.032176 70.9685) (xy 123.032176 70.9685)
(xy 123.032179 70.9685) (xy 123.032179 70.9685)
(xy 123.347821 70.9685) (xy 123.347821 70.9685)
(xy 123.347824 70.9685) (xy 123.347824 70.9685)
(xy 123.660975 70.92894) (xy 123.660975 70.92894)
(xy 123.76359 70.902593) (xy 123.703751 70.917957)
(xy 123.966702 70.850443) (xy 123.966702 70.850443)
(xy 124.260179 70.734247) (xy 124.260179 70.734247)
(xy 124.536779 70.582184) (xy 124.536779 70.582184)
@ -8165,7 +8175,7 @@
(xy 129.200468 70.92) (xy 129.200468 70.92)
(xy 129.186841 70.712097) (xy 129.186841 70.712097)
(xy 129.130553 70.42912) (xy 129.130553 70.42912)
(xy 129.090141 70.31007) (xy 129.093897 70.321134)
(xy 129.037813 70.155914) (xy 129.037813 70.155914)
(xy 128.958417 69.994915) (xy 128.958417 69.994915)
(xy 128.910202 69.897145) (xy 128.910202 69.897145)
@ -11098,7 +11108,7 @@
(xy 45.705416 71.643504) (xy 45.705416 71.643504)
(xy 45.705414 71.643504) (xy 45.705414 71.643504)
(xy 45.705413 71.643505) (xy 45.705413 71.643505)
(xy 45.625991 71.678866) (xy 45.558573 71.708882)
(xy 45.530947 71.721182) (xy 45.530947 71.721182)
(xy 45.376448 71.833432) (xy 45.376448 71.833432)
(xy 45.248659 71.975356) (xy 45.248659 71.975356)
@ -11117,8 +11127,8 @@
(xy 44.739041 71.187557) (xy 44.739041 71.187557)
(xy 44.660658 71.100504) (xy 44.660658 71.100504)
(xy 44.611253 71.045634) (xy 44.611253 71.045634)
(xy 44.599292 71.036944) (xy 44.548443 71)
(xy 44.521232 70.98023) (xy 44.511947 70.973484)
(xy 44.456752 70.933382) (xy 44.456752 70.933382)
(xy 44.368251 70.893979) (xy 44.368251 70.893979)
(xy 44.328863 70.866399) (xy 44.328863 70.866399)

View File

@ -862,7 +862,7 @@
(effects (font (size 1.27 1.27)) (justify left bottom)) (effects (font (size 1.27 1.27)) (justify left bottom))
(uuid 083bb491-0977-432e-acc8-a4698c0afad2) (uuid 083bb491-0977-432e-acc8-a4698c0afad2)
) )
(label "RX" (at 80.01 111.76 0) (fields_autoplaced) (label "RX" (at 80.01 109.22 0) (fields_autoplaced)
(effects (font (size 1.27 1.27)) (justify left bottom)) (effects (font (size 1.27 1.27)) (justify left bottom))
(uuid 0a1a64ee-6eb8-49a4-ad91-7a1ebc48e4ef) (uuid 0a1a64ee-6eb8-49a4-ad91-7a1ebc48e4ef)
) )
@ -890,7 +890,7 @@
(effects (font (size 1.27 1.27)) (justify left bottom)) (effects (font (size 1.27 1.27)) (justify left bottom))
(uuid 36b8996b-7d3e-4977-ba5a-db3904739724) (uuid 36b8996b-7d3e-4977-ba5a-db3904739724)
) )
(label "TX" (at 80.01 109.22 0) (fields_autoplaced) (label "TX" (at 80.01 111.76 0) (fields_autoplaced)
(effects (font (size 1.27 1.27)) (justify left bottom)) (effects (font (size 1.27 1.27)) (justify left bottom))
(uuid 3a56c460-dc5f-48bf-ab4e-c1d328b684a8) (uuid 3a56c460-dc5f-48bf-ab4e-c1d328b684a8)
) )

37
serial-logs Executable file
View File

@ -0,0 +1,37 @@
#!/usr/bin/env nu
let level_colors = {
TRACE: (ansi m)
DEBUG: (ansi c)
INFO: (ansi g)
WARN: (ansi y)
ERROR: (ansi rb)
}
# Attach to serial logs of a running firmware
def main [
--serial (-s): string # The serial device to get logs from.
] {
let serial = if $serial == null {
"/dev/serial/by-id/usb-Tux_Tangentbord1_42069-if00"
} else {
$serial
}
print -n ("waiting for serial log " + $serial)
while not ($serial | path exists) {
print -n "."
sleep 500ms
}
echo
cat $serial |
parse -r '\[(?<timestamp>[0-9\.]*)\] \((?<level>\w*)\) (?<message>.*)' |
each { |it|
print -n "[" (ansi d) $it.timestamp (ansi reset) "] "
print -n "(" ($level_colors | get $it.level) $it.level (ansi reset) ") "
print $it.message
}
}

1
src/bin/.gitignore vendored
View File

@ -1 +0,0 @@
*.pc

View File

@ -1,88 +0,0 @@
//! Firmware for Tangentbord1, left half.
#![no_std]
#![no_main]
#![feature(type_alias_impl_trait)]
extern crate alloc;
extern crate cortex_m_rt;
use alloc::vec::Vec;
use embassy_executor::Spawner;
use embassy_rp::gpio::{Level, Output, Pin};
use embassy_time::{Duration, Timer};
use tangentbord1::board::Board;
use tangentbord1::keyboard::KeyboardConfig;
use tangentbord1::util::{stall, wheel};
use tangentbord1::ws2812::{Rgb, Ws2812};
use tangentbord1::{allocator, usb};
use tgnt::layer::Layer;
#[embassy_executor::main]
async fn main(_spawner: Spawner) {
allocator::init();
let p = embassy_rp::init(Default::default());
let board = Board::from(p);
let _led = Output::new(board.d13, Level::High);
let _neopixel_power = Output::new(board.neopixel_power, Level::High);
let mut neopixel = Ws2812::new(board.PIO0, board.DMA_CH0, board.neopixel.degrade());
let mut neopixels_d5 = Ws2812::new(board.PIO1, board.DMA_CH1, board.d5.degrade());
neopixel.write(&[Rgb::new(0xFF, 0x00, 0x00)]).await;
usb::setup_logger_and_keyboard(board.USB).await;
neopixel.write(&[Rgb::new(0x00, 0x00, 0xFF)]).await;
//Timer::after(Duration::from_millis(3000)).await;
let layers = include_bytes!("layers-left.pc");
let Ok(layers): Result<Vec<Layer>, _> = postcard::from_bytes(layers) else {
log::error!("Failed to deserialize layer config");
stall().await
};
let keyboard = KeyboardConfig {
layers,
pins: [
// row 1
board.d24.degrade(),
board.a3.degrade(),
board.a2.degrade(),
board.a1.degrade(),
board.a0.degrade(),
// row 2
board.d25.degrade(),
board.sck.degrade(),
board.mosi.degrade(),
board.miso.degrade(),
board.d2.degrade(),
// row 3
board.d12.degrade(),
board.d11.degrade(),
board.d10.degrade(),
board.d9.degrade(),
board.d3.degrade(),
// thumbpad
board.d7.degrade(),
board.scl.degrade(),
board.sda.degrade(),
],
};
keyboard.create().await;
for w in 0usize.. {
neopixel.write(&[wheel(w as u8)]).await;
neopixels_d5
.write(&[
wheel((w + 50) as u8),
wheel((w + 100) as u8),
wheel((w + 150) as u8),
wheel((w + 200) as u8),
])
.await;
Timer::after(Duration::from_millis(10)).await;
}
}

View File

@ -1,89 +0,0 @@
//! Firmware for Tangentbord1, right half.
#![no_std]
#![no_main]
#![feature(type_alias_impl_trait)]
extern crate alloc;
extern crate cortex_m_rt;
use alloc::vec::Vec;
use embassy_executor::Spawner;
use embassy_rp::gpio::{Level, Output, Pin};
use embassy_time::{Duration, Timer};
use tangentbord1::board::Board;
use tangentbord1::keyboard::KeyboardConfig;
use tangentbord1::util::{stall, wheel};
use tangentbord1::ws2812::{Rgb, Ws2812};
use tangentbord1::{allocator, usb};
use tgnt::layer::Layer;
#[embassy_executor::main]
async fn main(_spawner: Spawner) {
allocator::init();
let p = embassy_rp::init(Default::default());
let board = Board::from(p);
let _led = Output::new(board.d13, Level::High);
let _neopixel_power = Output::new(board.neopixel_power, Level::High);
let mut neopixel = Ws2812::new(board.PIO0, board.DMA_CH0, board.neopixel.degrade());
let mut neopixels_d5 = Ws2812::new(board.PIO1, board.DMA_CH1, board.d5.degrade());
neopixel.write(&[Rgb::new(0xFF, 0x00, 0x00)]).await;
usb::setup_logger_and_keyboard(board.USB).await;
neopixel.write(&[Rgb::new(0x00, 0x00, 0xFF)]).await;
//Timer::after(Duration::from_millis(3000)).await;
let layers = include_bytes!("layers-right.pc");
let Ok(layers): Result<Vec<Layer>, _> = postcard::from_bytes(layers) else {
log::error!("Failed to deserialize layer config");
stall().await
};
let keyboard = KeyboardConfig {
layers,
pins: [
// TODO: reconfigure these for right PCB
// row 1
board.d24.degrade(),
board.a3.degrade(),
board.a2.degrade(),
board.a1.degrade(),
board.a0.degrade(),
// row 2
board.d25.degrade(),
board.sck.degrade(),
board.mosi.degrade(),
board.miso.degrade(),
board.d2.degrade(),
// row 3
board.d12.degrade(),
board.d11.degrade(),
board.d10.degrade(),
board.d9.degrade(),
board.d3.degrade(),
// thumbpad
board.d7.degrade(),
board.scl.degrade(),
board.sda.degrade(),
],
};
keyboard.create().await;
for w in 0usize.. {
neopixel.write(&[wheel(w as u8)]).await;
neopixels_d5
.write(&[
wheel((w + 50) as u8),
wheel((w + 100) as u8),
wheel((w + 150) as u8),
wheel((w + 200) as u8),
])
.await;
Timer::after(Duration::from_millis(10)).await;
}
}

View File

@ -1,191 +0,0 @@
use core::sync::atomic::{AtomicU16, Ordering};
use alloc::{boxed::Box, vec::Vec};
use embassy_executor::Spawner;
use embassy_rp::gpio::{AnyPin, Input, Pin, Pull};
use embassy_time::{Duration, Timer};
use futures::{select_biased, FutureExt};
use log::{debug, error, info, warn};
use tgnt::{button::Button, layer::Layer};
use crate::usb::keyboard::KB_REPORT;
static CURRENT_LAYER: AtomicU16 = AtomicU16::new(0);
pub struct KeyboardConfig {
pub pins: [AnyPin; SWITCH_COUNT],
pub layers: Vec<Layer>,
}
impl KeyboardConfig {
pub async fn create(self) {
let spawner = Spawner::for_current_executor().await;
if self.layers.is_empty() {
error!("no layers defined");
return;
}
info!(
"setting up keyboard layout with {} layer(s)",
self.layers.len()
);
let layers = Box::leak(self.layers.into_boxed_slice());
for (i, layer) in 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, layers)).is_err() {
error!("failed to spawn switch task, pool_size mismatch?");
break;
}
}
}
}
const MOD_TAP_TIME: Duration = Duration::from_millis(100);
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, layers: &'static [Layer]) -> ! {
let _pin_nr = pin.pin();
let mut pin = Input::new(pin, Pull::Up);
loop {
// pins are pull-up, so when the switch is pressed they are brought low.
pin.wait_for_low().await;
// TODO: do we need debouncing?
// get current layer
let mut current_layer = CURRENT_LAYER.load(Ordering::Relaxed);
let layer_count = 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 }) = layers.get(usize::from(current_layer)) else {
error!("current layer was out of bounds for some reason ({current_layer})");
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 wait_for_release = async {
pin.wait_for_high().await;
debug!("switch {switch_num} button {button:?} released");
};
match button {
&Button::Key(key) => {
KB_REPORT.lock().await.press_key(key);
wait_for_release.await;
KB_REPORT.lock().await.release_key(key);
continue;
}
&Button::Mod(modifier) => {
KB_REPORT.lock().await.press_modifier(modifier);
wait_for_release.await;
KB_REPORT.lock().await.release_modifier(modifier);
continue;
}
&Button::ModTap { keycode, modifier } => {
select_biased! {
_ = Timer::after(MOD_TAP_TIME).fuse() => {
KB_REPORT.lock().await.press_modifier(modifier);
pin.wait_for_high().await;
KB_REPORT.lock().await.release_modifier(modifier);
debug!("switch {switch_num} button {button:?} released");
continue;
}
_ = wait_for_release.fuse() => {
KB_REPORT.lock().await.press_key(keycode);
Timer::after(Duration::from_millis(10)).await;
KB_REPORT.lock().await.release_key(keycode);
continue;
}
}
}
Button::NextLayer => {
let next_layer = (current_layer + 1) % layer_count;
CURRENT_LAYER.store(next_layer, Ordering::Relaxed);
debug!("switched to layer {next_layer}");
}
Button::PrevLayer => {
let prev_layer = current_layer.checked_sub(1).unwrap_or(layer_count - 1);
CURRENT_LAYER.store(prev_layer, Ordering::Relaxed);
debug!("switched to layer {prev_layer}");
}
Button::None => {}
}
wait_for_release.await;
}
}
/// 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)
}
}

View File

@ -1,13 +0,0 @@
#![no_std]
#![feature(type_alias_impl_trait)]
extern crate alloc;
pub mod allocator;
pub mod board;
pub mod keyboard;
pub mod neopixel;
pub mod panic_handler;
pub mod usb;
pub mod util;
pub mod ws2812;

View File

@ -1,11 +0,0 @@
.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

View File

@ -1,122 +0,0 @@
/*
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!");
}
}
*/

View File

@ -1,8 +0,0 @@
.program ws2812
.origin 0
.wrap_target
out x 1
set pins,1 [1]
mov pins,x [1]
set pins,0
.wrap

View File

@ -1,28 +0,0 @@
use core::panic::PanicInfo;
use embassy_executor::Executor;
use embassy_rp::gpio::{AnyPin, Level, Output, Pin};
use embassy_time::{Duration, Timer};
use static_cell::StaticCell;
#[panic_handler]
fn panic_blink(_info: &PanicInfo) -> ! {
static EXECUTOR: StaticCell<Executor> = StaticCell::new();
// SAFETY: we panicked, so no other code will be running.
let p = unsafe { embassy_rp::Peripherals::steal() };
EXECUTOR.init(Executor::new()).run(|spawner| {
spawner.spawn(blink(p.PIN_11.degrade())).ok();
});
}
#[embassy_executor::task]
async fn blink(led: AnyPin) {
let mut led = Output::new(led, Level::High);
loop {
Timer::after(Duration::from_secs(1)).await;
led.toggle();
}
}

View File

@ -1,73 +0,0 @@
use embassy_executor::Spawner;
use embassy_rp::{interrupt, peripherals::USB, usb::Driver};
use embassy_usb::{Builder, Config, UsbDevice};
use static_cell::StaticCell;
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) {
let mut builder = builder(usb);
logger::setup(&mut builder).await;
log::error!("log_level: error");
log::warn!("log_level: warn");
log::info!("log_level: info");
log::debug!("log_level: debug");
log::trace!("log_level: trace");
keyboard::setup(&mut builder).await;
let usb = builder.build();
Spawner::for_current_executor().await.must_spawn(run(usb));
}
pub fn builder(usb: USB) -> Builder<'static, Driver<'static, USB>> {
let state = STATE.init(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, interrupt::take!(USBCTRL_IRQ));
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>>) {
device.run().await
}

View File

@ -1,170 +0,0 @@
pub mod report;
use embassy_executor::Spawner;
use embassy_rp::{peripherals::USB, usb::Driver};
use embassy_sync::mutex::Mutex;
use embassy_time::{Duration, Timer};
use embassy_usb::{
class::hid::{self, HidReaderWriter, ReadError, ReportId, RequestHandler},
control::OutResponse,
Builder,
};
use embassy_usb_driver::EndpointError;
use static_cell::StaticCell;
use usbd_hid::descriptor::{MouseReport, SerializedDescriptor};
use crate::{
usb::keyboard::report::{KeyboardReport, EMPTY_KEYBOARD_REPORT},
util::CS,
};
use super::MAX_PACKET_SIZE;
struct Handler;
static CONTEXT: StaticCell<Context> = StaticCell::new();
pub static KB_REPORT: Mutex<CS, KeyboardReport> = Mutex::new(EMPTY_KEYBOARD_REPORT);
struct Context {
handler: Handler,
state: hid::State<'static>,
}
pub async fn setup(builder: &mut Builder<'static, Driver<'static, USB>>) {
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));
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 task(stream: HidStream, handler: &'static Handler) {
if let Err(e) = keyboard_test(stream, handler).await {
log::error!("keyboard error: {e:?}");
}
//if let Err(e) = mouse_wiggler(stream).await {
// log::error!("mouse wiggler: {e:?}");
//}
}
async fn keyboard_test(mut stream: HidStream, _handler: &'static Handler) -> Result<(), Error> {
stream.ready().await;
loop {
Timer::after(Duration::from_millis(2)).await;
let report = KB_REPORT.lock().await.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?;
}
}
#[allow(dead_code)]
async fn mouse_wiggler(mut stream: HidStream) -> Result<(), Error> {
stream.ready().await;
let (_r, mut w) = stream.split();
let write_fut = async move {
let mut x = 1;
loop {
for _ in 0..100 {
Timer::after(Duration::from_millis(10)).await;
log::info!("sending mouse report");
//w.ready().await;
w.write_serialize(&MouseReport {
x,
y: 0,
buttons: 0,
wheel: 0,
pan: 0,
})
.await?;
}
x = -x;
}
};
//let read_fut = async move {
// let mut buf = [0u8; MAX_PACKET_SIZE as usize];
// loop {
// Timer::after(Duration::from_millis(30)).await;
// let n = r.read(&mut buf).await?;
// log::info!("got packet: {:?}", &buf[..n]);
// }
//};
//let r: Result<((), ()), Error> = try_join(write_fut, read_fut).await;
let r: Result<(), Error> = write_fut.await;
r?;
Ok(())
}
#[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)
}
}

View File

@ -1,81 +0,0 @@
use crate::util::CS;
use super::MAX_PACKET_SIZE;
use core::fmt::Write as WriteFmt;
use embassy_executor::Spawner;
use embassy_rp::{peripherals::USB, usb::Driver};
use embassy_sync::pipe::Pipe;
use embassy_time::Instant;
use embassy_usb::{
class::cdc_acm::{self, CdcAcmClass},
Builder,
};
use log::{Metadata, Record};
use static_cell::StaticCell;
pub const BUFFER_SIZE: usize = 16 * 1024;
static BUFFER: Pipe<CS, BUFFER_SIZE> = Pipe::new();
static STATE: StaticCell<cdc_acm::State<'static>> = StaticCell::new();
struct UsbLogger;
pub async fn setup(usb_builder: &mut Builder<'static, Driver<'static, USB>>) {
unsafe {
static LOGGER: UsbLogger = UsbLogger;
log::set_logger_racy(&LOGGER).unwrap();
log::set_max_level(log::LevelFilter::Debug);
}
let spawner = Spawner::for_current_executor().await;
let state = STATE.init(cdc_acm::State::new());
let class = CdcAcmClass::new(usb_builder, state, MAX_PACKET_SIZE as u16);
spawner.must_spawn(log_task(class));
}
//pub async fn print(s: &str) {
// BUFFER.writer().write_all(s.as_bytes()).await.ok(/* infallible */);
//}
#[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(())
}
}

View File

@ -1,27 +0,0 @@
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_time::{Duration, Timer};
use crate::ws2812::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 {
return Rgb::new(255 - wheel_pos * 3, 0, wheel_pos * 3);
}
if wheel_pos < 170 {
wheel_pos -= 85;
return Rgb::new(0, wheel_pos * 3, 255 - wheel_pos * 3);
}
wheel_pos -= 170;
Rgb::new(wheel_pos * 3, 255 - wheel_pos * 3, 0)
}

View File

@ -1,130 +0,0 @@
use core::fmt::{self, Debug};
use core::mem::transmute;
use embassy_rp::dma::{self, AnyChannel};
use embassy_rp::pio::{
FifoJoin, PioInstance, PioPeripheral, PioStateMachine, PioStateMachineInstance, ShiftDirection,
SmInstanceBase,
};
use embassy_rp::pio_instr_util;
use embassy_rp::relocate::RelocatedProgram;
use embassy_rp::{gpio, PeripheralRef};
pub struct Ws2812<P: PioInstance> {
sm: PioStateMachineInstance<P, SmInstanceBase<0>>,
dma: PeripheralRef<'static, AnyChannel>,
}
/// An Rgb value that can be safely transmuted to u32 for use with Ws2812.
#[repr(transparent)]
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct Rgb(u32);
impl<P: PioInstance> Ws2812<P> {
pub fn new<PP: PioPeripheral<Pio = P>>(
pio: PP,
dma: impl dma::Channel,
pin: gpio::AnyPin,
) -> Self {
let (_, mut sm, ..) = pio.split();
// 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 = RelocatedProgram::new(&prg);
sm.write_instr(relocated.origin() as usize, relocated.code());
pio_instr_util::exec_jmp(&mut sm, relocated.origin());
// Pin config
let out_pin = sm.make_pio_pin(pin);
sm.set_set_pins(&[&out_pin]);
sm.set_sideset_base_pin(&out_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;
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);
// FIFO config
sm.set_autopull(true);
sm.set_fifo_join(FifoJoin::TxOnly);
sm.set_pull_threshold(24);
sm.set_out_shift_dir(ShiftDirection::Left);
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.dma_push(self.dma.reborrow(), colors).await;
}
}
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, u8, u8) {
let [g, r, b, _] = self.0.to_be_bytes();
(r, g, b)
}
#[inline(always)]
pub fn slice_as_u32s(rgbs: &[Rgb]) -> &[u32] {
// SAFETY: Rgb contains only a u32, and is #[repr(transparent)]
unsafe { transmute(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()
}
}