//! CLI wiring for the RS-485 bridge. All behavior lives in the lib (tested //! there); this file only parses arguments, connects sockets, and spawns the //! control listener. //! //! Typical use (matches qemu/run.sh --rs485): //! //! qemu/run.sh --kernel ... --rs485 /tmp/warden-rs485.sock & //! rs485-bridge --serial /tmp/warden-rs485.sock --control /tmp/warden-rs485-ctl.sock use std::io::{BufRead, BufReader, Write}; use std::os::unix::net::{UnixListener, UnixStream}; use std::process::exit; use std::time::{Duration, Instant}; use warden_rs485_bridge::{handle_control_line, pump_serial, Bus, DEFAULT_GAP}; fn usage() -> ! { eprintln!( "usage: rs485-bridge --serial [--control ] [--address N] \ [--regs N] [--bits N] [--gap-ms N]" ); exit(2); } fn main() { let mut serial: Option = None; let mut control: Option = None; let mut address: u8 = 1; let mut regs: usize = 128; let mut bits: usize = 64; let mut gap = DEFAULT_GAP; let mut args = std::env::args().skip(1); while let Some(a) = args.next() { let mut val = |name: &str| args.next().unwrap_or_else(|| { eprintln!("{name} needs a value"); usage() }); match a.as_str() { "--serial" => serial = Some(val("--serial")), "--control" => control = Some(val("--control")), "--address" => address = val("--address").parse().unwrap_or_else(|_| usage()), "--regs" => regs = val("--regs").parse().unwrap_or_else(|_| usage()), "--bits" => bits = val("--bits").parse().unwrap_or_else(|_| usage()), "--gap-ms" => { gap = Duration::from_millis(val("--gap-ms").parse().unwrap_or_else(|_| usage())) } _ => usage(), } } let serial = serial.unwrap_or_else(|| usage()); // The bus is shared between the serial pump and the control channel. // 'static so the control thread needs no scoped lifetime: the bridge runs // until killed. let bus: &'static Bus = Box::leak(Box::new(Bus::new(address, regs, bits))); if let Some(path) = control { let _ = std::fs::remove_file(&path); // stale socket from a previous run let listener = UnixListener::bind(&path).unwrap_or_else(|e| { eprintln!("FATAL: cannot bind control socket {path}: {e}"); exit(1); }); eprintln!("rs485: control socket at {path}"); std::thread::spawn(move || { for conn in listener.incoming().flatten() { let reader = BufReader::new(conn.try_clone().expect("clone control conn")); let mut writer = conn; for line in reader.lines() { let line = match line { Ok(l) => l, Err(_) => break, }; let reply = handle_control_line(&line, bus); if writeln!(writer, "{reply}").is_err() { break; } } } }); } // QEMU (chardev server=on) may come up after us: retry the connect briefly // instead of racing the VM launch. let deadline = Instant::now() + Duration::from_secs(15); let stream = loop { match UnixStream::connect(&serial) { Ok(s) => break s, Err(e) if Instant::now() < deadline => { eprintln!("rs485: waiting for {serial} ({e})"); std::thread::sleep(Duration::from_millis(500)); } Err(e) => { eprintln!("FATAL: cannot connect serial socket {serial}: {e}"); exit(1); } } }; eprintln!("rs485: connected to {serial}, slave address {address}, gap {gap:?}"); if let Err(e) = pump_serial(&stream, &bus.slave, gap) { eprintln!("FATAL: serial pump: {e}"); exit(1); } eprintln!("rs485: serial closed (VM gone), exiting"); }