wip
This commit is contained in:
@ -1,5 +1,5 @@
|
|||||||
[build]
|
[build]
|
||||||
target = "thumbv6m-none-eabi"
|
#target = "thumbv6m-none-eabi"
|
||||||
|
|
||||||
[target.thumbv6m-none-eabi]
|
[target.thumbv6m-none-eabi]
|
||||||
rustflags = [
|
rustflags = [
|
||||||
|
|||||||
3588
Cargo.lock
generated
3588
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,11 +1,9 @@
|
|||||||
[patch.'https://git.nubo.sh/hulthe/tgnt.git'.tgnt]
|
|
||||||
path = "../tgnt"
|
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"lib",
|
"lib",
|
||||||
"left",
|
"left",
|
||||||
"right",
|
"right",
|
||||||
|
"editor",
|
||||||
]
|
]
|
||||||
|
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|||||||
2
editor/.gitignore
vendored
Normal file
2
editor/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/target
|
||||||
|
/Cargo.lock
|
||||||
22
editor/Cargo.toml
Normal file
22
editor/Cargo.toml
Normal 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
3
editor/README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# tgnt
|
||||||
|
|
||||||
|
Keyboard configuration library and editor.
|
||||||
25
editor/src/default_layer.ron
Normal file
25
editor/src/default_layer.ron
Normal 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,
|
||||||
|
],
|
||||||
|
)
|
||||||
25
editor/src/default_layout.ron
Normal file
25
editor/src/default_layout.ron
Normal 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
395
editor/src/main.rs
Normal 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
98
editor/src/mat.rs
Normal 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
5
editor/src/serial.rs
Normal 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
144
editor/src/serial/port.rs
Normal 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
141
editor/src/serial/scan.rs
Normal 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)
|
||||||
|
}
|
||||||
2
left/.cargo/config.toml
Normal file
2
left/.cargo/config.toml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[build]
|
||||||
|
target = "thumbv6m-none-eabi"
|
||||||
@ -10,7 +10,6 @@ path = "../lib"
|
|||||||
package = "tangentbord1-lib"
|
package = "tangentbord1-lib"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tgnt = { git = "https://git.nubo.sh/hulthe/tgnt.git", default-features = false }
|
|
||||||
cortex-m-rt = "0.7"
|
cortex-m-rt = "0.7"
|
||||||
embassy-rp = { version = "0.1.0", features = ["time-driver", "critical-section-impl"] }
|
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-executor = { version = "0.5.0", features = ["nightly", "executor-thread", "integrated-timers", "arch-cortex-m"] }
|
||||||
@ -21,6 +20,9 @@ log = "0.4.17"
|
|||||||
postcard = { version = "1.0.4", features = ["alloc"] }
|
postcard = { version = "1.0.4", features = ["alloc"] }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tgnt = { git = "https://git.nubo.sh/hulthe/tgnt.git", default-features = false }
|
|
||||||
ron = "0.8.0"
|
ron = "0.8.0"
|
||||||
postcard = { version = "1", features = ["use-std"] }
|
postcard = { version = "1", features = ["use-std"] }
|
||||||
|
|
||||||
|
[build-dependencies.tangentbord1]
|
||||||
|
path = "../lib"
|
||||||
|
package = "tangentbord1-lib"
|
||||||
|
|||||||
@ -13,7 +13,7 @@ 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();
|
||||||
|
|||||||
@ -18,29 +18,19 @@ use tangentbord1::{
|
|||||||
event::Half,
|
event::Half,
|
||||||
interrupts::Irqs,
|
interrupts::Irqs,
|
||||||
keyboard::KeyboardConfig,
|
keyboard::KeyboardConfig,
|
||||||
logger::Logger,
|
layer::Layer,
|
||||||
|
logger::LogMultiplexer,
|
||||||
rgb::Rgb,
|
rgb::Rgb,
|
||||||
util::stall,
|
util::stall,
|
||||||
ws2812::Ws2812,
|
ws2812::Ws2812,
|
||||||
{allocator, rtt, uart, usb},
|
{allocator, rtt, uart, usb},
|
||||||
};
|
};
|
||||||
use tgnt::layer::Layer;
|
|
||||||
|
|
||||||
#[embassy_executor::main]
|
#[embassy_executor::main]
|
||||||
async fn main(_spawner: Spawner) {
|
async fn main(_spawner: Spawner) {
|
||||||
let half = Half::Left;
|
let half = Half::Left;
|
||||||
|
|
||||||
let rtt_write = rtt::init_rtt_logger();
|
let rtt_write = rtt::init_rtt_logger();
|
||||||
let logger = Logger {
|
|
||||||
outputs: [rtt_write],
|
|
||||||
};
|
|
||||||
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");
|
|
||||||
|
|
||||||
allocator::init();
|
allocator::init();
|
||||||
|
|
||||||
@ -102,7 +92,18 @@ async fn main(_spawner: Spawner) {
|
|||||||
|
|
||||||
neopixel.write(&[Rgb::new(0x00, 0x99, 0x99)]).await;
|
neopixel.write(&[Rgb::new(0x00, 0x99, 0x99)]).await;
|
||||||
|
|
||||||
usb::setup_logger_and_keyboard(board.USB, events1).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;
|
neopixel.write(&[Rgb::new(0x00, 0x00, 0xFF)]).await;
|
||||||
Timer::after_secs(5).await;
|
Timer::after_secs(5).await;
|
||||||
@ -112,7 +113,5 @@ async fn main(_spawner: Spawner) {
|
|||||||
Timer::after_millis(10).await;
|
Timer::after_millis(10).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
loop {
|
stall().await
|
||||||
Timer::after_secs(5).await;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
2
lib/.cargo/config.toml
Normal file
2
lib/.cargo/config.toml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[build]
|
||||||
|
target = "thumbv6m-none-eabi"
|
||||||
@ -6,12 +6,10 @@ description = "Keyboard firmware"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tgnt = { git = "https://git.nubo.sh/hulthe/tgnt.git", default-features = false }
|
#tgnt = { git = "https://git.nubo.sh/hulthe/tgnt.git", default-features = false }
|
||||||
cortex-m = "0.7.6"
|
cortex-m = "0.7.6"
|
||||||
cortex-m-rt = "0.7"
|
cortex-m-rt = "0.7"
|
||||||
embedded-hal = "0.2.5"
|
embedded-hal = "0.2.5"
|
||||||
usb-device = "0.2.9"
|
|
||||||
usbd-hid = "0.6.1"
|
|
||||||
static_cell = "1.0.0"
|
static_cell = "1.0.0"
|
||||||
embedded-io-async = "*"
|
embedded-io-async = "*"
|
||||||
futures = { version = "0.3", default-features = false, features = ["async-await"] }
|
futures = { version = "0.3", default-features = false, features = ["async-await"] }
|
||||||
@ -19,9 +17,6 @@ embassy-executor = { version = "0.5.0", features = ["nightly", "nightly", "execu
|
|||||||
embassy-sync = "0.5.0"
|
embassy-sync = "0.5.0"
|
||||||
embassy-time = "0.3.0"
|
embassy-time = "0.3.0"
|
||||||
embassy-futures = "0.1.1"
|
embassy-futures = "0.1.1"
|
||||||
embassy-usb = { version = "0.1.0", features = ["usbd-hid"] }
|
|
||||||
embassy-usb-logger = "0.1.0"
|
|
||||||
embassy-usb-driver = "0.1.0"
|
|
||||||
log = "0.4.17"
|
log = "0.4.17"
|
||||||
pio = "0.2.1"
|
pio = "0.2.1"
|
||||||
pio-proc = "0.2.1"
|
pio-proc = "0.2.1"
|
||||||
@ -32,13 +27,15 @@ fixed = "1.23.1"
|
|||||||
rtt-target = "0.4.0"
|
rtt-target = "0.4.0"
|
||||||
heapless = "0.7.16"
|
heapless = "0.7.16"
|
||||||
once_cell = { version = "1.17.1", default-features = false }
|
once_cell = { version = "1.17.1", default-features = false }
|
||||||
atomic-polyfill = "1.0.2"
|
|
||||||
critical-section = "1.1.1"
|
critical-section = "1.1.1"
|
||||||
crc-any = "2.4.3"
|
crc-any = "2.4.3"
|
||||||
serde = { version = "1.0.163", default-features = false, features = ["derive"] }
|
serde = { version = "1.0.163", default-features = false, features = ["derive"] }
|
||||||
bytemuck = { version = "1.13.1", features = ["derive"] }
|
bytemuck = { version = "1.13.1", features = ["derive"] }
|
||||||
libm = "0.2.8"
|
libm = "0.2.8"
|
||||||
glam = { version = "0.25.0", default-features = false, features = ["libm"] }
|
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]
|
[target.'cfg(target_arch = "x86_64")'.dependencies]
|
||||||
embassy-executor = { version = "0.5.0", features = ["arch-std"] }
|
embassy-executor = { version = "0.5.0", features = ["arch-std"] }
|
||||||
@ -48,12 +45,18 @@ simple_logger = "4"
|
|||||||
[target.thumbv6m-none-eabi.dependencies]
|
[target.thumbv6m-none-eabi.dependencies]
|
||||||
embassy-rp = { version = "0.1.0", features = ["time-driver", "critical-section-impl"] }
|
embassy-rp = { version = "0.1.0", features = ["time-driver", "critical-section-impl"] }
|
||||||
embassy-executor = { version = "0.5.0", features = ["arch-cortex-m"] }
|
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]
|
[build-dependencies]
|
||||||
tgnt = { git = "https://git.nubo.sh/hulthe/tgnt.git", default-features = false }
|
#tgnt = { git = "https://git.nubo.sh/hulthe/tgnt.git", default-features = false }
|
||||||
ron = "0.8.0"
|
ron = "0.8.0"
|
||||||
postcard = { version = "1", features = ["use-std"] }
|
postcard = { version = "1", features = ["use-std"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
std = []
|
||||||
default = ["n-key-rollover"]
|
default = ["n-key-rollover"]
|
||||||
n-key-rollover=[]
|
n-key-rollover=[]
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
use atomic_polyfill::{AtomicU32, Ordering};
|
use portable_atomic::{AtomicU32, Ordering};
|
||||||
|
|
||||||
pub struct AtomicCoord {
|
pub struct AtomicCoord {
|
||||||
inner: AtomicU32,
|
inner: AtomicU32,
|
||||||
|
|||||||
107
lib/src/button.rs
Normal file
107
lib/src/button.rs
Normal 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.
|
||||||
|
#[default]
|
||||||
|
Lower,
|
||||||
|
|
||||||
|
/// Shift the key.
|
||||||
|
Upper,
|
||||||
|
|
||||||
|
/// Shift the key if shift is being held down.
|
||||||
|
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}")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,7 +1,6 @@
|
|||||||
|
use crate::{button::Button, keys::Key};
|
||||||
use core::time::Duration;
|
use core::time::Duration;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tgnt::{button::Button, keys::Key};
|
|
||||||
|
|
||||||
pub mod switch {
|
pub mod switch {
|
||||||
use super::*;
|
use super::*;
|
||||||
@ -35,9 +34,8 @@ pub mod switch {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub mod button {
|
pub mod button {
|
||||||
use tgnt::button::Modifier;
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::button::Modifier;
|
||||||
|
|
||||||
/// A usb keyboard button was pressed or released.
|
/// A usb keyboard button was pressed or released.
|
||||||
///
|
///
|
||||||
|
|||||||
@ -2,7 +2,7 @@ mod lights;
|
|||||||
|
|
||||||
use core::sync::atomic::Ordering;
|
use core::sync::atomic::Ordering;
|
||||||
|
|
||||||
use alloc::{boxed::Box, vec::Vec};
|
use alloc::vec::Vec;
|
||||||
use embassy_executor::Spawner;
|
use embassy_executor::Spawner;
|
||||||
use embassy_rp::{
|
use embassy_rp::{
|
||||||
gpio::{AnyPin, Input, Pin, Pull},
|
gpio::{AnyPin, Input, Pin, Pull},
|
||||||
@ -12,19 +12,22 @@ use embassy_sync::pubsub::{ImmediatePublisher, PubSubChannel, Subscriber};
|
|||||||
use embassy_time::{Duration, Instant, Timer};
|
use embassy_time::{Duration, Instant, Timer};
|
||||||
use log::{debug, error, info, warn};
|
use log::{debug, error, info, warn};
|
||||||
use static_cell::StaticCell;
|
use static_cell::StaticCell;
|
||||||
use tgnt::{
|
|
||||||
button::{Button, LayerDir, LayerShift},
|
|
||||||
layer::Layer,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
atomics::AtomicCoord,
|
atomics::AtomicCoord,
|
||||||
|
button::{Button, LayerDir, LayerShift},
|
||||||
event::{
|
event::{
|
||||||
switch::{Event, EventKind},
|
switch::{Event, EventKind},
|
||||||
Half,
|
Half,
|
||||||
},
|
},
|
||||||
|
layer::{Layer, Layers},
|
||||||
lights::Lights,
|
lights::Lights,
|
||||||
util::CS,
|
serial_proto::{
|
||||||
|
borrowed::DeviceMsg,
|
||||||
|
owned::{ChangeLayer, SwitchPress, SwitchRelease},
|
||||||
|
},
|
||||||
|
usb::serial::serial_send,
|
||||||
|
util::{SwapCell, SwapCellRead, CS},
|
||||||
ws2812::Ws2812,
|
ws2812::Ws2812,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -45,7 +48,8 @@ struct State {
|
|||||||
half: Half,
|
half: Half,
|
||||||
current_layer: AtomicCoord,
|
current_layer: AtomicCoord,
|
||||||
layer_cols: usize,
|
layer_cols: usize,
|
||||||
layers: &'static [Vec<Layer>],
|
layer_rows: usize,
|
||||||
|
layers: SwapCellRead<Layers>,
|
||||||
/// Array of LED indices of each switch
|
/// Array of LED indices of each switch
|
||||||
led_map: [usize; SWITCH_COUNT],
|
led_map: [usize; SWITCH_COUNT],
|
||||||
lights: Lights<PIO1, SWITCH_COUNT>,
|
lights: Lights<PIO1, SWITCH_COUNT>,
|
||||||
@ -89,17 +93,7 @@ impl KeyboardConfig {
|
|||||||
self.layers.len()
|
self.layers.len()
|
||||||
);
|
);
|
||||||
|
|
||||||
static STATE: StaticCell<State> = StaticCell::new();
|
for (y, row) in self.layers.iter().enumerate() {
|
||||||
let state = STATE.init_with(|| State {
|
|
||||||
half: self.half,
|
|
||||||
current_layer: AtomicCoord::new(),
|
|
||||||
layer_cols: self.layers.iter().map(|row| row.len()).max().unwrap_or(0),
|
|
||||||
layers: Box::leak(self.layers.into_boxed_slice()),
|
|
||||||
lights: Lights::new(self.led_driver),
|
|
||||||
led_map: self.led_map,
|
|
||||||
});
|
|
||||||
|
|
||||||
for (y, row) in state.layers.iter().enumerate() {
|
|
||||||
for (x, layer) in row.iter().enumerate() {
|
for (x, layer) in row.iter().enumerate() {
|
||||||
if layer.buttons.len() != SWITCH_COUNT {
|
if layer.buttons.len() != SWITCH_COUNT {
|
||||||
warn!(
|
warn!(
|
||||||
@ -110,6 +104,24 @@ impl KeyboardConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
for (i, pin) in self.pins.into_iter().enumerate() {
|
||||||
if spawner.spawn(switch_task(i, pin, state)).is_err() {
|
if spawner.spawn(switch_task(i, pin, state)).is_err() {
|
||||||
error!("failed to spawn switch task, pool_size mismatch?");
|
error!("failed to spawn switch task, pool_size mismatch?");
|
||||||
@ -200,8 +212,8 @@ async fn switch_task(switch_num: usize, pin: AnyPin, state: &'static State) -> !
|
|||||||
// get current layer
|
// get current layer
|
||||||
let (x, y) = state.current_layer.load(Ordering::Relaxed);
|
let (x, y) = state.current_layer.load(Ordering::Relaxed);
|
||||||
|
|
||||||
let Some(Layer { buttons }) = state
|
let layers = state.layers.read();
|
||||||
.layers
|
let Some(Layer { buttons }) = layers
|
||||||
.get(usize::from(y))
|
.get(usize::from(y))
|
||||||
.and_then(|row| row.get(usize::from(x)))
|
.and_then(|row| row.get(usize::from(x)))
|
||||||
else {
|
else {
|
||||||
@ -210,14 +222,18 @@ async fn switch_task(switch_num: usize, pin: AnyPin, state: &'static State) -> !
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let button = buttons.get(switch_num).cloned();
|
||||||
|
drop(layers);
|
||||||
|
|
||||||
// and current button
|
// and current button
|
||||||
let Some(button) = buttons.get(switch_num) else {
|
let Some(button) = button else {
|
||||||
warn!("no button defined for switch {switch_num}");
|
warn!("no button defined for switch {switch_num}");
|
||||||
pin.wait_for_high().await;
|
pin.wait_for_high().await;
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
debug!("switch {switch_num} button {button:?} pressed");
|
debug!("switch {switch_num} button {button:?} pressed");
|
||||||
|
serial_send(&DeviceMsg::SwitchPress(SwitchPress(switch_num as u16)));
|
||||||
|
|
||||||
let ev = |kind| Event {
|
let ev = |kind| Event {
|
||||||
source: state.half,
|
source: state.half,
|
||||||
@ -234,6 +250,7 @@ async fn switch_task(switch_num: usize, pin: AnyPin, state: &'static State) -> !
|
|||||||
let released_after = pressed_at.elapsed();
|
let released_after = pressed_at.elapsed();
|
||||||
|
|
||||||
debug!("switch {switch_num} button {button:?} released");
|
debug!("switch {switch_num} button {button:?} released");
|
||||||
|
serial_send(&DeviceMsg::SwitchRelease(SwitchRelease(switch_num as u16)));
|
||||||
|
|
||||||
events.publish_immediate(ev(EventKind::Release {
|
events.publish_immediate(ev(EventKind::Release {
|
||||||
button: button.clone(),
|
button: button.clone(),
|
||||||
@ -247,7 +264,7 @@ async fn switch_task(switch_num: usize, pin: AnyPin, state: &'static State) -> !
|
|||||||
#[embassy_executor::task]
|
#[embassy_executor::task]
|
||||||
async fn layer_switch_task(mut events: KbEvents, state: &'static State) {
|
async fn layer_switch_task(mut events: KbEvents, state: &'static State) {
|
||||||
let col_count = state.layer_cols as u16;
|
let col_count = state.layer_cols as u16;
|
||||||
let row_count = state.layers.len() as u16;
|
let row_count = state.layer_rows as u16;
|
||||||
let Some(last_row) = row_count.checked_sub(1) else {
|
let Some(last_row) = row_count.checked_sub(1) else {
|
||||||
error!("no layers specified");
|
error!("no layers specified");
|
||||||
return;
|
return;
|
||||||
@ -282,6 +299,7 @@ async fn layer_switch_task(mut events: KbEvents, state: &'static State) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
state.current_layer.store(nx, ny, Ordering::Relaxed);
|
state.current_layer.store(nx, ny, Ordering::Relaxed);
|
||||||
|
serial_send(&DeviceMsg::ChangeLayer(ChangeLayer { x: nx, y: ny }));
|
||||||
debug!("switched to layer ({nx}, {ny})");
|
debug!("switched to layer ({nx}, {ny})");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -289,7 +307,7 @@ async fn layer_switch_task(mut events: KbEvents, state: &'static State) {
|
|||||||
/// Random functions for testing
|
/// Random functions for testing
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub mod test {
|
pub mod test {
|
||||||
use tgnt::{button::Button, keys::Key};
|
use crate::{button::Button, keys::Key};
|
||||||
|
|
||||||
pub fn letter_to_key(c: char) -> Button {
|
pub fn letter_to_key(c: char) -> Button {
|
||||||
if !c.is_ascii() {
|
if !c.is_ascii() {
|
||||||
|
|||||||
@ -2,9 +2,9 @@ use core::future::pending;
|
|||||||
|
|
||||||
use embassy_time::{Duration, Instant, Timer};
|
use embassy_time::{Duration, Instant, Timer};
|
||||||
use futures::{select_biased, FutureExt};
|
use futures::{select_biased, FutureExt};
|
||||||
use tgnt::button::Button;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
button::Button,
|
||||||
lights::shaders::{PowerOffAnim, PowerOnAnim, Shader, Shaders},
|
lights::shaders::{PowerOffAnim, PowerOnAnim, Shader, Shaders},
|
||||||
rgb::Rgb,
|
rgb::Rgb,
|
||||||
usb::{UsbEvent, USB_EVENTS},
|
usb::{UsbEvent, USB_EVENTS},
|
||||||
|
|||||||
@ -1,13 +1,15 @@
|
|||||||
use core::future::pending;
|
use core::future::pending;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
button::Button,
|
||||||
|
event::{button, switch, Half},
|
||||||
|
keys::Key,
|
||||||
|
};
|
||||||
use embassy_sync::pubsub::{publisher::Pub, subscriber::Sub, PubSubBehavior, WaitResult};
|
use embassy_sync::pubsub::{publisher::Pub, subscriber::Sub, PubSubBehavior, WaitResult};
|
||||||
use embassy_time::{Duration, Instant, Timer};
|
use embassy_time::{Duration, Instant, Timer};
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
use heapless::Deque;
|
use heapless::Deque;
|
||||||
use log::{debug, error};
|
use log::{debug, error};
|
||||||
use tgnt::{button::Button, keys::Key};
|
|
||||||
|
|
||||||
use crate::event::{button, switch, Half};
|
|
||||||
|
|
||||||
/// The time a ModTap button takes to resolve as a Mod while being held down.
|
/// The time a ModTap button takes to resolve as a Mod while being held down.
|
||||||
const MOD_TAP_TIME: Duration = Duration::from_millis(150);
|
const MOD_TAP_TIME: Duration = Duration::from_millis(150);
|
||||||
|
|||||||
269
lib/src/keys.rs
Normal file
269
lib/src/keys.rs
Normal 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,
|
||||||
|
/// KeyboardEnd7
|
||||||
|
End = 0x4D,
|
||||||
|
/// Keyboard PageDown7
|
||||||
|
PageDown7 = 0x4E,
|
||||||
|
/// Keyboard RightArrow7
|
||||||
|
RightArrow7 = 0x4F,
|
||||||
|
/// Keyboard LeftArrow7
|
||||||
|
LeftArrow7 = 0x50,
|
||||||
|
/// Keyboard DownArrow7
|
||||||
|
DownArrow7 = 0x51,
|
||||||
|
/// Keyboard UpArrow7
|
||||||
|
UpArrow7 = 0x52,
|
||||||
|
/// Keypad Num Lock and Clear6
|
||||||
|
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
12
lib/src/layer.rs
Normal 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
36
lib/src/layout.rs
Normal 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
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
#![no_std]
|
#![cfg_attr(not(feature = "std"), no_std)]
|
||||||
#![feature(type_alias_impl_trait)]
|
#![feature(type_alias_impl_trait)]
|
||||||
#![feature(split_array)]
|
#![feature(split_array)]
|
||||||
|
|
||||||
@ -24,10 +24,15 @@ pub mod usb;
|
|||||||
pub mod ws2812;
|
pub mod ws2812;
|
||||||
|
|
||||||
pub mod atomics;
|
pub mod atomics;
|
||||||
|
pub mod button;
|
||||||
pub mod event;
|
pub mod event;
|
||||||
pub mod keypress_handler;
|
pub mod keypress_handler;
|
||||||
|
pub mod keys;
|
||||||
|
pub mod layer;
|
||||||
|
pub mod layout;
|
||||||
pub mod logger;
|
pub mod logger;
|
||||||
pub mod neopixel;
|
pub mod neopixel;
|
||||||
pub mod rgb;
|
pub mod rgb;
|
||||||
pub mod rtt;
|
pub mod rtt;
|
||||||
|
pub mod serial_proto;
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
|||||||
@ -1,17 +1,25 @@
|
|||||||
use core::{fmt::Write, sync::atomic::Ordering};
|
use core::{fmt::Arguments, sync::atomic::Ordering};
|
||||||
|
|
||||||
use atomic_polyfill::AtomicBool;
|
|
||||||
use embassy_time::Instant;
|
use embassy_time::Instant;
|
||||||
use log::{Metadata, Record};
|
use log::{Level, Metadata, Record};
|
||||||
|
use portable_atomic::AtomicBool;
|
||||||
use static_cell::StaticCell;
|
use static_cell::StaticCell;
|
||||||
|
|
||||||
pub const LOGGER_OUTPUTS: usize = 1;
|
pub const LOGGER_OUTPUTS: usize = 2;
|
||||||
|
|
||||||
pub struct Logger {
|
/// A logger which timestamps logs and forwards them to mutiple other loggers.
|
||||||
pub outputs: [fn(&[u8]); LOGGER_OUTPUTS],
|
pub struct LogMultiplexer {
|
||||||
|
pub outputs: [&'static dyn LogOutput; LOGGER_OUTPUTS],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Logger {
|
pub struct TimestampedRecord<'a> {
|
||||||
|
pub record: Record<'a>,
|
||||||
|
|
||||||
|
/// Timestamp
|
||||||
|
pub timestamp: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LogMultiplexer {
|
||||||
/// Set this as the global logger.
|
/// Set this as the global logger.
|
||||||
///
|
///
|
||||||
/// Calling this function more than once does nothing.
|
/// Calling this function more than once does nothing.
|
||||||
@ -22,7 +30,7 @@ impl Logger {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
static LOGGER: StaticCell<Logger> = StaticCell::new();
|
static LOGGER: StaticCell<LogMultiplexer> = StaticCell::new();
|
||||||
let logger = LOGGER.init(self);
|
let logger = LOGGER.init(self);
|
||||||
unsafe {
|
unsafe {
|
||||||
log::set_logger_racy(logger).unwrap();
|
log::set_logger_racy(logger).unwrap();
|
||||||
@ -31,32 +39,43 @@ impl Logger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl log::Log for Logger {
|
impl log::Log for LogMultiplexer {
|
||||||
fn enabled(&self, _metadata: &Metadata) -> bool {
|
fn enabled(&self, _metadata: &Metadata) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
fn log(&self, record: &Record) {
|
fn log(&self, record: &Record) {
|
||||||
if self.enabled(record.metadata()) {
|
if self.enabled(record.metadata()) {
|
||||||
let now = Instant::now();
|
let timestamp = Instant::now();
|
||||||
let s = now.as_secs();
|
|
||||||
let ms = now.as_millis() % 1000;
|
let record = TimestampedRecord {
|
||||||
let level = record.metadata().level();
|
timestamp,
|
||||||
let mut w = &mut Writer(self);
|
record: record.clone(),
|
||||||
let _ = writeln!(&mut w, "[{s}.{ms:04}] ({level}) {}", record.args());
|
};
|
||||||
|
|
||||||
|
for output in &self.outputs {
|
||||||
|
output.log(&record);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn flush(&self) {}
|
fn flush(&self) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Writer<'a>(&'a Logger);
|
pub trait LogOutput: Send + Sync {
|
||||||
|
fn log(&self, record: &TimestampedRecord);
|
||||||
|
}
|
||||||
|
|
||||||
impl Write for Writer<'_> {
|
impl TimestampedRecord<'_> {
|
||||||
fn write_str(&mut self, s: &str) -> Result<(), core::fmt::Error> {
|
pub fn args(&self) -> &Arguments<'_> {
|
||||||
for output in &self.0.outputs {
|
self.record.args()
|
||||||
output(s.as_bytes());
|
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
|
pub fn level(&self) -> Level {
|
||||||
|
self.metadata().level()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn metadata(&self) -> &Metadata<'_> {
|
||||||
|
self.record.metadata()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,7 +23,7 @@ struct Writer;
|
|||||||
|
|
||||||
impl core::fmt::Write for Writer {
|
impl core::fmt::Write for Writer {
|
||||||
fn write_str(&mut self, s: &str) -> Result<(), core::fmt::Error> {
|
fn write_str(&mut self, s: &str) -> Result<(), core::fmt::Error> {
|
||||||
rtt_write(s.as_bytes());
|
rtt_write(s);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,23 +1,25 @@
|
|||||||
use core::cell::RefCell;
|
use core::{cell::RefCell, fmt::Write};
|
||||||
|
|
||||||
use critical_section::Mutex;
|
use critical_section::Mutex;
|
||||||
use rtt_target::{rtt_init, UpChannel};
|
use rtt_target::{rtt_init, UpChannel};
|
||||||
|
|
||||||
pub type RttWriteFn = fn(&[u8]);
|
use crate::logger::{LogOutput, TimestampedRecord};
|
||||||
|
|
||||||
|
pub type RttWriteFn = fn(&str);
|
||||||
|
|
||||||
static CHANNEL: Mutex<RefCell<Option<UpChannel>>> = Mutex::new(RefCell::new(None));
|
static CHANNEL: Mutex<RefCell<Option<UpChannel>>> = Mutex::new(RefCell::new(None));
|
||||||
|
|
||||||
/// Write directly to the rtt output. Must call [init_rtt_logger] first.
|
/// Write directly to the rtt output. Must call [init_rtt_logger] first.
|
||||||
pub fn rtt_write(bytes: &[u8]) {
|
pub fn rtt_write(s: &str) {
|
||||||
critical_section::with(|cs| {
|
critical_section::with(|cs| {
|
||||||
let mut slot = CHANNEL.borrow_ref_mut(cs);
|
let mut slot = CHANNEL.borrow_ref_mut(cs);
|
||||||
if let Some(channel) = slot.as_mut() {
|
if let Some(channel) = slot.as_mut() {
|
||||||
channel.write(bytes);
|
channel.write(s.as_bytes());
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init_rtt_logger() -> RttWriteFn {
|
pub fn init_rtt_logger() -> &'static RttLogger {
|
||||||
let channels = rtt_init! {
|
let channels = rtt_init! {
|
||||||
up: {
|
up: {
|
||||||
0: {
|
0: {
|
||||||
@ -31,12 +33,32 @@ pub fn init_rtt_logger() -> RttWriteFn {
|
|||||||
critical_section::with(|cs| {
|
critical_section::with(|cs| {
|
||||||
let mut slot = CHANNEL.borrow_ref_mut(cs);
|
let mut slot = CHANNEL.borrow_ref_mut(cs);
|
||||||
|
|
||||||
if slot.is_some() {
|
if slot.is_none() {
|
||||||
return rtt_write;
|
*slot = Some(channels.up.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
*slot = Some(channels.up.0);
|
static RTT_LOGGER: RttLogger = RttLogger;
|
||||||
|
&RTT_LOGGER
|
||||||
rtt_write
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
71
lib/src/serial_proto.rs
Normal 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),
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,8 +7,10 @@ use static_cell::StaticCell;
|
|||||||
|
|
||||||
use crate::{interrupts::Irqs, keyboard::KbEvents, uart::UART_USB_EVENTS_OUT, util::CS};
|
use crate::{interrupts::Irqs, keyboard::KbEvents, uart::UART_USB_EVENTS_OUT, util::CS};
|
||||||
|
|
||||||
|
use self::serial::UsbSerial;
|
||||||
|
|
||||||
pub mod keyboard;
|
pub mod keyboard;
|
||||||
pub mod logger;
|
pub mod serial;
|
||||||
|
|
||||||
pub const MAX_PACKET_SIZE: u8 = 64;
|
pub const MAX_PACKET_SIZE: u8 = 64;
|
||||||
|
|
||||||
@ -35,17 +37,18 @@ struct State {
|
|||||||
|
|
||||||
static STATE: StaticCell<State> = StaticCell::new();
|
static STATE: StaticCell<State> = StaticCell::new();
|
||||||
|
|
||||||
pub async fn setup_logger_and_keyboard(usb: USB, events: KbEvents) {
|
pub async fn setup_logger_and_keyboard(usb: USB, events: KbEvents) -> &'static UsbSerial {
|
||||||
let mut builder = builder(usb);
|
let mut builder = builder(usb);
|
||||||
|
|
||||||
//logger::setup(&mut builder).await;
|
let usb_serial = serial::setup(&mut builder).await;
|
||||||
|
|
||||||
keyboard::setup(&mut builder, events).await;
|
keyboard::setup(&mut builder, events).await;
|
||||||
|
|
||||||
log::info!("building usb device");
|
log::info!("building usb device");
|
||||||
let usb = builder.build();
|
let usb = builder.build();
|
||||||
log::info!("spawning usb task");
|
log::info!("spawning usb task");
|
||||||
Spawner::for_current_executor().await.must_spawn(run(usb));
|
Spawner::for_current_executor().await.must_spawn(run(usb));
|
||||||
|
|
||||||
|
usb_serial
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn builder(usb: USB) -> Builder<'static, Driver<'static, USB>> {
|
pub fn builder(usb: USB) -> Builder<'static, Driver<'static, USB>> {
|
||||||
|
|||||||
@ -218,6 +218,7 @@ async fn write_reports(mut stream: HidStream, ctx: &'static Context) -> Result<(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
#[allow(dead_code)]
|
||||||
enum Error {
|
enum Error {
|
||||||
Read(ReadError),
|
Read(ReadError),
|
||||||
Endpoint(EndpointError),
|
Endpoint(EndpointError),
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
#![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.
|
||||||
@ -15,10 +19,6 @@ pub struct KeyboardReport {
|
|||||||
pub keycodes: [u8; 27],
|
pub keycodes: [u8; 27],
|
||||||
}
|
}
|
||||||
|
|
||||||
use bytemuck::{cast_ref, Pod, Zeroable};
|
|
||||||
use core::mem::size_of;
|
|
||||||
use tgnt::{button::Modifier, keys::Key};
|
|
||||||
|
|
||||||
#[cfg(not(feature = "n-key-rollover"))]
|
#[cfg(not(feature = "n-key-rollover"))]
|
||||||
pub use ::usbd_hid::descriptor::KeyboardReport;
|
pub use ::usbd_hid::descriptor::KeyboardReport;
|
||||||
|
|
||||||
|
|||||||
@ -1,55 +0,0 @@
|
|||||||
use crate::util::CS;
|
|
||||||
|
|
||||||
use super::MAX_PACKET_SIZE;
|
|
||||||
use core::fmt::Write as WriteFmt;
|
|
||||||
use embassy_rp::{peripherals::USB, usb::Driver};
|
|
||||||
use embassy_sync::pipe::Pipe;
|
|
||||||
use embassy_time::Instant;
|
|
||||||
use embassy_usb::class::cdc_acm::CdcAcmClass;
|
|
||||||
use log::{Metadata, Record};
|
|
||||||
|
|
||||||
pub const BUFFER_SIZE: usize = 16 * 1024;
|
|
||||||
static BUFFER: Pipe<CS, BUFFER_SIZE> = Pipe::new();
|
|
||||||
|
|
||||||
struct UsbLogger;
|
|
||||||
|
|
||||||
#[embassy_executor::task]
|
|
||||||
async fn log_task(mut class: CdcAcmClass<'static, Driver<'static, USB>>) {
|
|
||||||
let mut buf = [0u8; MAX_PACKET_SIZE as usize];
|
|
||||||
|
|
||||||
class.wait_connection().await;
|
|
||||||
loop {
|
|
||||||
let n = BUFFER.read(&mut buf).await;
|
|
||||||
|
|
||||||
// not much we can do if this fails, just ignore the error
|
|
||||||
let _ = class.write_packet(&buf[..n]).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl log::Log for UsbLogger {
|
|
||||||
fn enabled(&self, _metadata: &Metadata) -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn log(&self, record: &Record) {
|
|
||||||
if self.enabled(record.metadata()) {
|
|
||||||
let mut w = Writer;
|
|
||||||
let now = Instant::now();
|
|
||||||
let s = now.as_secs();
|
|
||||||
let ms = now.as_millis() % 1000;
|
|
||||||
let level = record.metadata().level();
|
|
||||||
let _ = writeln!(w, "[{s}.{ms:04}] ({level}) {}", record.args());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn flush(&self) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Writer;
|
|
||||||
|
|
||||||
impl core::fmt::Write for Writer {
|
|
||||||
fn write_str(&mut self, s: &str) -> Result<(), core::fmt::Error> {
|
|
||||||
let _ = BUFFER.try_write(s.as_bytes());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
219
lib/src/usb/serial.rs
Normal file
219
lib/src/usb/serial.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
187
lib/src/util.rs
187
lib/src/util.rs
@ -1,10 +1,130 @@
|
|||||||
|
use core::{
|
||||||
|
cell::UnsafeCell,
|
||||||
|
fmt::{self, Debug, Display, Write},
|
||||||
|
iter,
|
||||||
|
mem::MaybeUninit,
|
||||||
|
ops::Deref,
|
||||||
|
};
|
||||||
|
|
||||||
|
use embassy_futures::yield_now;
|
||||||
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
||||||
use embassy_time::{Duration, Timer};
|
use embassy_time::{Duration, Timer};
|
||||||
|
use msgpck::{Marker, MsgPack, PackErr, Piece};
|
||||||
|
use portable_atomic::{AtomicU32, Ordering};
|
||||||
|
|
||||||
use crate::rgb::Rgb;
|
use crate::rgb::Rgb;
|
||||||
|
|
||||||
pub type CS = CriticalSectionRawMutex;
|
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() -> ! {
|
pub async fn stall() -> ! {
|
||||||
loop {
|
loop {
|
||||||
Timer::after(Duration::from_secs(1)).await;
|
Timer::after(Duration::from_secs(1)).await;
|
||||||
@ -25,3 +145,70 @@ pub fn wheel(mut wheel_pos: u8) -> Rgb {
|
|||||||
Rgb::new(wheel_pos * 3, 255 - wheel_pos * 3, 0)
|
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<'_>> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
2
right/.cargo/config.toml
Normal file
2
right/.cargo/config.toml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[build]
|
||||||
|
target = "thumbv6m-none-eabi"
|
||||||
@ -10,7 +10,6 @@ path = "../lib"
|
|||||||
package = "tangentbord1-lib"
|
package = "tangentbord1-lib"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tgnt = { git = "https://git.nubo.sh/hulthe/tgnt.git", default-features = false }
|
|
||||||
cortex-m-rt = "0.7"
|
cortex-m-rt = "0.7"
|
||||||
embassy-rp = { version = "0.1.0", features = ["time-driver", "critical-section-impl"] }
|
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-executor = { version = "0.5.0", features = ["nightly", "executor-thread", "integrated-timers", "arch-cortex-m"] }
|
||||||
@ -21,6 +20,9 @@ log = "0.4.17"
|
|||||||
postcard = { version = "1.0.4", features = ["alloc"] }
|
postcard = { version = "1.0.4", features = ["alloc"] }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tgnt = { git = "https://git.nubo.sh/hulthe/tgnt.git", default-features = false }
|
|
||||||
ron = "0.8.0"
|
ron = "0.8.0"
|
||||||
postcard = { version = "1", features = ["use-std"] }
|
postcard = { version = "1", features = ["use-std"] }
|
||||||
|
|
||||||
|
[build-dependencies.tangentbord1]
|
||||||
|
path = "../lib"
|
||||||
|
package = "tangentbord1-lib"
|
||||||
|
|||||||
@ -13,7 +13,7 @@ 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();
|
||||||
|
|||||||
@ -93,25 +93,25 @@
|
|||||||
Layer(
|
Layer(
|
||||||
buttons: [
|
buttons: [
|
||||||
// Row 1
|
// Row 1
|
||||||
None,
|
Key(Y),
|
||||||
None,
|
Key(U),
|
||||||
None,
|
Key(I),
|
||||||
None,
|
Key(O),
|
||||||
None,
|
Key(P),
|
||||||
|
|
||||||
// Row 2
|
// Row 2
|
||||||
None,
|
Key(H),
|
||||||
None,
|
Key(J),
|
||||||
None,
|
Key(K),
|
||||||
None,
|
Key(L),
|
||||||
None,
|
Key(G),
|
||||||
|
|
||||||
// Row 3
|
// Row 3
|
||||||
None,
|
Key(B),
|
||||||
None,
|
Key(N),
|
||||||
None,
|
Key(M),
|
||||||
None,
|
Key(Comma),
|
||||||
None,
|
Key(Period),
|
||||||
|
|
||||||
// Thumbpad
|
// Thumbpad
|
||||||
Layer(Peek, Right, 1),
|
Layer(Peek, Right, 1),
|
||||||
@ -124,8 +124,8 @@
|
|||||||
// Row 1
|
// Row 1
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
Key(CapsLock),
|
||||||
None,
|
Key(Tab),
|
||||||
None,
|
None,
|
||||||
|
|
||||||
// Row 2
|
// Row 2
|
||||||
|
|||||||
@ -17,29 +17,19 @@ use tangentbord1::{
|
|||||||
event::Half,
|
event::Half,
|
||||||
interrupts::Irqs,
|
interrupts::Irqs,
|
||||||
keyboard::KeyboardConfig,
|
keyboard::KeyboardConfig,
|
||||||
logger::Logger,
|
layer::Layer,
|
||||||
|
logger::LogMultiplexer,
|
||||||
rgb::Rgb,
|
rgb::Rgb,
|
||||||
util::stall,
|
util::stall,
|
||||||
ws2812::Ws2812,
|
ws2812::Ws2812,
|
||||||
{allocator, rtt, uart, usb},
|
{allocator, rtt, uart, usb},
|
||||||
};
|
};
|
||||||
use tgnt::layer::Layer;
|
|
||||||
|
|
||||||
#[embassy_executor::main]
|
#[embassy_executor::main]
|
||||||
async fn main(_spawner: Spawner) {
|
async fn main(_spawner: Spawner) {
|
||||||
let half = Half::Right;
|
let half = Half::Right;
|
||||||
|
|
||||||
let rtt_write = rtt::init_rtt_logger();
|
let rtt_logger = rtt::init_rtt_logger();
|
||||||
let logger = Logger {
|
|
||||||
outputs: [rtt_write],
|
|
||||||
};
|
|
||||||
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");
|
|
||||||
|
|
||||||
allocator::init();
|
allocator::init();
|
||||||
|
|
||||||
@ -100,7 +90,19 @@ async fn main(_spawner: Spawner) {
|
|||||||
|
|
||||||
uart::start(board.tx, board.rx, board.UART0, half, events2).await;
|
uart::start(board.tx, board.rx, board.UART0, half, events2).await;
|
||||||
|
|
||||||
usb::setup_logger_and_keyboard(board.USB, events1).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;
|
neopixel.write(&[Rgb::new(0x00, 0x00, 0xFF)]).await;
|
||||||
Timer::after_secs(5).await;
|
Timer::after_secs(5).await;
|
||||||
@ -110,7 +112,5 @@ async fn main(_spawner: Spawner) {
|
|||||||
Timer::after_millis(10).await;
|
Timer::after_millis(10).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
loop {
|
stall().await
|
||||||
Timer::after_secs(5).await;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user