wip
This commit is contained in:
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)
|
||||
}
|
||||
Reference in New Issue
Block a user