290 lines
10 KiB
Rust
290 lines
10 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::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::<InputEvent>(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<InputEvent>) {
|
|
let _ = thread::Builder::new()
|
|
.name("sim-input".to_string())
|
|
.spawn(move || input_thread(tx));
|
|
}
|
|
|
|
fn input_thread(tx: mpsc::Sender<InputEvent>) {
|
|
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<Mutex<DeviceState>>) {
|
|
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();
|
|
}
|