From 852fa32c93199c91d27259516f2a8d6da7b8c96d Mon Sep 17 00:00:00 2001 From: Daniel Bulant Date: Tue, 12 May 2026 21:35:54 +0200 Subject: [PATCH] simulator progress --- .envrc | 9 +++ Cargo.lock | 1 + simulator/Cargo.toml | 2 +- simulator/src/main.rs | 136 +++++++++++++++++++++++++++++++++++------- 4 files changed, 126 insertions(+), 22 deletions(-) create mode 100644 .envrc diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..b27be4d --- /dev/null +++ b/.envrc @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# the shebang is ignored, but nice for editors + +if type -P lorri &>/dev/null; then + eval "$(lorri direnv)" +else + echo 'while direnv evaluated .envrc, could not find the command "lorri" [https://github.com/nix-community/lorri]' + use flake +fi diff --git a/Cargo.lock b/Cargo.lock index 27467c3..54aabed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2956,6 +2956,7 @@ dependencies = [ "libc", "mio", "pin-project-lite", + "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.61.2", diff --git a/simulator/Cargo.toml b/simulator/Cargo.toml index 24d99a5..6a4bc43 100644 --- a/simulator/Cargo.toml +++ b/simulator/Cargo.toml @@ -9,4 +9,4 @@ default-target = "x86_64-unknown-linux-gnu" clap = { version = "4.5.45", features = ["derive"] } crossterm = "0.28.1" device-state = { path = "../device-state" } -tokio = { version = "1.47.0", features = ["macros", "rt-multi-thread", "net", "sync", "time", "io-util"] } +tokio = { version = "1.47.0", features = ["macros", "rt-multi-thread", "net", "sync", "time", "io-util", "signal"] } diff --git a/simulator/src/main.rs b/simulator/src/main.rs index bb012dc..2d9e199 100644 --- a/simulator/src/main.rs +++ b/simulator/src/main.rs @@ -4,8 +4,10 @@ use std::sync::{Arc, Mutex}; use std::thread; use clap::Parser; -use crossterm::event::{Event, KeyCode, KeyEventKind}; +use crossterm::event::{Event, KeyCode, KeyEventKind, KeyModifiers}; use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; +use crossterm::cursor::Show; +use crossterm::queue; use crossterm::event; use device_state::{DeviceState, WriteType, apply_wheel_delta}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; @@ -34,11 +36,30 @@ enum InputEvent { Quit, } +struct TerminalGuard; + +impl TerminalGuard { + fn new() -> Self { + let _ = enable_raw_mode(); + Self + } +} + +impl Drop for TerminalGuard { + fn drop(&mut self) { + let _ = disable_raw_mode(); + let mut stdout = io::stdout(); + let _ = queue!(stdout, Show); + let _ = stdout.flush(); + } +} + #[tokio::main] async fn main() -> io::Result<()> { let args = Args::parse(); let addr: SocketAddr = format!("{}:{}", args.host, args.port).parse().unwrap(); + let _terminal = TerminalGuard::new(); let state = Arc::new(Mutex::new(DeviceState::new())); let (input_tx, mut input_rx) = mpsc::channel::(64); @@ -46,30 +67,64 @@ async fn main() -> io::Result<()> { tokio::spawn(ui_task(state.clone())); let mut buf = [0u8; 1024]; + let sigint = tokio::signal::ctrl_c(); + tokio::pin!(sigint); loop { - let stream = match TcpStream::connect(addr).await { - Ok(stream) => stream, - Err(err) => { - eprintln!("connect error: {err}"); - tokio::time::sleep(Duration::from_secs(1)).await; - continue; + let stream = loop { + tokio::select! { + _ = &mut sigint => { + log_error("received SIGINT"); + return Ok(()); + } + maybe_event = input_rx.recv() => { + if matches!(maybe_event, Some(InputEvent::Quit) | None) { + log_error("quitting"); + return Ok(()); + } + } + connect = TcpStream::connect(addr) => { + match connect { + Ok(stream) => break stream, + Err(err) => { + log_error(&format!("connect error: {err}")); + tokio::select! { + _ = &mut sigint => { + log_error("received SIGINT"); + return Ok(()); + } + maybe_event = input_rx.recv() => { + if matches!(maybe_event, Some(InputEvent::Quit) | None) { + log_error("quitting"); + return Ok(()); + } + } + _ = tokio::time::sleep(Duration::from_secs(1)) => {} + } + } + } + } } }; let (mut read, mut write) = stream.into_split(); let device_id = device_state::serialize_write(&WriteType::DeviceId(DEVICE_ID)).unwrap(); if write.write_all(device_id.as_bytes()).await.is_err() { + log_error("failed to send device id"); tokio::time::sleep(Duration::from_secs(1)).await; continue; } loop { tokio::select! { + _ = &mut sigint => { + log_error("received SIGINT"); + return Ok(()); + } maybe_event = input_rx.recv() => { match maybe_event { Some(InputEvent::Quit) | None => { - let _ = disable_raw_mode(); + log_error("quitting"); return Ok(()); } Some(InputEvent::Button(id)) => { @@ -77,6 +132,7 @@ async fn main() -> io::Result<()> { let data = WriteType::QuizResponse(value); let buffer = device_state::serialize_write(&data).unwrap(); if write.write_all(buffer.as_bytes()).await.is_err() { + log_error("write error while sending response"); break; } } @@ -103,9 +159,15 @@ async fn main() -> io::Result<()> { } read_res = read.read(&mut buf) => { let len = match read_res { - Ok(0) => break, + Ok(0) => { + log_error("server closed connection"); + break; + } Ok(len) => len, - Err(_) => break, + Err(err) => { + log_error(&format!("read error: {err}")); + break; + } }; let Ok(text) = core::str::from_utf8(&buf[..len]) else { continue; @@ -121,7 +183,20 @@ async fn main() -> io::Result<()> { } } - tokio::time::sleep(Duration::from_millis(500)).await; + log_error("reconnecting in 500ms"); + tokio::select! { + _ = &mut sigint => { + log_error("received SIGINT"); + return Ok(()); + } + maybe_event = input_rx.recv() => { + if matches!(maybe_event, Some(InputEvent::Quit) | None) { + log_error("quitting"); + return Ok(()); + } + } + _ = tokio::time::sleep(Duration::from_millis(500)) => {} + } } } @@ -132,16 +207,15 @@ fn spawn_input_thread(tx: mpsc::Sender) { } fn input_thread(tx: mpsc::Sender) { - let _ = enable_raw_mode(); - let mut stdout = io::stdout(); - let _ = crossterm::execute!(stdout, crossterm::cursor::MoveTo(0, 0)); - loop { if let Ok(Event::Key(key)) = event::read() { if key.kind == KeyEventKind::Release { continue; } let event = match key.code { + KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => { + Some(InputEvent::Quit) + } KeyCode::Char('1') => Some(InputEvent::Button(1)), KeyCode::Char('2') => Some(InputEvent::Button(2)), KeyCode::Char('3') => Some(InputEvent::Button(3)), @@ -165,11 +239,10 @@ fn input_thread(tx: mpsc::Sender) { } } } - - let _ = disable_raw_mode(); } async fn ui_task(state: Arc>) { + let mut last = (String::new(), String::new()); let mut ticker = interval(Duration::from_millis(50)); loop { ticker.tick().await; @@ -182,11 +255,32 @@ async fn ui_task(state: Arc>) { } }; + let rendered = (pad_16(line1.as_str()), pad_16(line2.as_str())); + if rendered == last { + continue; + } + last = rendered.clone(); + let mut stdout = io::stdout(); - let _ = crossterm::execute!(stdout, crossterm::cursor::MoveTo(0, 0)); - let _ = writeln!(stdout, "{:<16}", line1.as_str()); - let _ = writeln!(stdout, "{:<16}", line2.as_str()); - let _ = writeln!(stdout, "[1-4]=buttons <-/->=wheel a/d q=quit"); + let _ = write!(stdout, "[lcd] {}\r\n", rendered.0); + let _ = write!(stdout, "[lcd] {}\r\n", rendered.1); let _ = stdout.flush(); } } + +fn pad_16(s: &str) -> String { + let mut out = String::with_capacity(16); + for ch in s.chars().take(16) { + out.push(ch); + } + while out.len() < 16 { + out.push(' '); + } + out +} + +fn log_error(msg: &str) { + let mut stderr = io::stderr(); + let _ = write!(stderr, "[simulator] {msg}\r\n"); + let _ = stderr.flush(); +}