itpdp/simulator/src/main.rs
2026-05-12 21:22:36 +02:00

192 lines
6.8 KiB
Rust

use std::io::{self, Write as _};
use std::net::SocketAddr;
use std::sync::{Arc, Mutex};
use std::thread;
use clap::Parser;
use crossterm::event::{Event, KeyCode, KeyEventKind};
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use crossterm::event;
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,
}
#[tokio::main]
async fn main() -> io::Result<()> {
let args = Args::parse();
let addr: SocketAddr = format!("{}:{}", args.host, args.port).parse().unwrap();
let state = Arc::new(Mutex::new(DeviceState::new()));
let (input_tx, mut input_rx) = mpsc::channel::<InputEvent>(64);
spawn_input_thread(input_tx.clone());
tokio::spawn(ui_task(state.clone()));
let mut buf = [0u8; 1024];
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 (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() {
tokio::time::sleep(Duration::from_secs(1)).await;
continue;
}
loop {
tokio::select! {
maybe_event = input_rx.recv() => {
match maybe_event {
Some(InputEvent::Quit) | None => {
let _ = disable_raw_mode();
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() {
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) => break,
Ok(len) => len,
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);
}
}
}
}
tokio::time::sleep(Duration::from_millis(500)).await;
}
}
fn spawn_input_thread(tx: mpsc::Sender<InputEvent>) {
let _ = thread::Builder::new()
.name("sim-input".to_string())
.spawn(move || input_thread(tx));
}
fn input_thread(tx: mpsc::Sender<InputEvent>) {
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('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;
}
}
}
}
let _ = disable_raw_mode();
}
async fn ui_task(state: Arc<Mutex<DeviceState>>) {
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 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 _ = stdout.flush();
}
}