This commit is contained in:
2024-03-24 16:29:24 +01:00
parent 84f8222b30
commit 4a528eb4b7
44 changed files with 5438 additions and 328 deletions

View File

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

View File

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

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

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

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

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

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

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

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

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

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

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