use std::io::{self, Write as _}; use std::net::SocketAddr; use std::sync::{Arc, Mutex}; use std::thread; use clap::Parser; use crossterm::cursor::Show; use crossterm::event; use crossterm::event::{Event, KeyCode, KeyEventKind, KeyModifiers}; use crossterm::queue; use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; use device_state::{DeviceState, WriteType, apply_wheel_delta}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::TcpStream; use tokio::sync::mpsc; use tokio::time::{Duration, interval}; const DEVICE_ID: &str = "esp32-1"; const DEFAULT_HOST: &str = "84.238.32.253"; const DEFAULT_PORT: u16 = 7070; const WHEEL_PRECISION: i32 = 32; const WHEEL_STEP: i32 = 1; #[derive(Parser, Debug)] #[command(author, version, about)] struct Args { #[arg(long, default_value = DEFAULT_HOST)] host: String, #[arg(long, default_value_t = DEFAULT_PORT)] port: u16, } enum InputEvent { Button(u8), WheelDelta(i32), 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); spawn_input_thread(input_tx.clone()); tokio::spawn(ui_task(state.clone())); let mut buf = [0u8; 1024]; let sigint = tokio::signal::ctrl_c(); tokio::pin!(sigint); loop { 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}")); state.lock().unwrap().reconnecting(); 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(); state.lock().unwrap().reset(); 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"); state.lock().unwrap().reconnecting(); 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 => { log_error("quitting"); return Ok(()); } Some(InputEvent::Button(id)) => { let value = state.lock().unwrap().response_value(id); 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; } } Some(InputEvent::WheelDelta(diff)) => { let mut state = state.lock().unwrap(); let wheel = state.wheel(); if wheel.max != wheel.min { let mut value = wheel.value; let mut accumulated = wheel.accumulated; apply_wheel_delta( &mut value, wheel.min, wheel.max, &mut accumulated, diff, WHEEL_PRECISION, ); let wheel = state.wheel_mut(); wheel.value = value; wheel.accumulated = accumulated; } } } } read_res = read.read(&mut buf) => { let len = match read_res { Ok(0) => { log_error("server closed connection"); break; } Ok(len) => len, Err(err) => { log_error(&format!("read error: {err}")); break; } }; let Ok(text) = core::str::from_utf8(&buf[..len]) else { continue; }; if let Some(last) = text.lines().last() { let Ok(data) = device_state::parse_proxy_output(last) else { continue; }; let mut state = state.lock().unwrap(); state.apply_proxy_output(data); } } } } log_error("reconnecting in 500ms"); state.lock().unwrap().reconnecting(); 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)) => {} } } } fn spawn_input_thread(tx: mpsc::Sender) { let _ = thread::Builder::new() .name("sim-input".to_string()) .spawn(move || input_thread(tx)); } fn input_thread(tx: mpsc::Sender) { 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)), KeyCode::Char('4') => Some(InputEvent::Button(4)), KeyCode::Left => Some(InputEvent::WheelDelta(-WHEEL_STEP)), KeyCode::Right => Some(InputEvent::WheelDelta(WHEEL_STEP)), KeyCode::Char('a') => Some(InputEvent::WheelDelta(-WHEEL_STEP)), KeyCode::Char('d') => Some(InputEvent::WheelDelta(WHEEL_STEP)), KeyCode::Char('q') => Some(InputEvent::Quit), _ => None, }; if let Some(event) = event { let quit = matches!(event, InputEvent::Quit); if tx.blocking_send(event).is_err() { break; } if quit { break; } } } } } 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; let (line1, line2) = { let mut state = state.lock().unwrap(); state.tick(); match state.render_lines() { Some((line1, line2)) => (line1, line2), None => continue, } }; 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 _ = 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(); }