use std::io::{self, BufRead, Write};
use std::thread;
use std::time::Duration;
use anyhow::{Context, Result};
use crate::cli::device::{self as gateway_args, DeviceArgs, KindArg};
use crate::engine::pgn_list::RxFilter;
use crate::engine::{BusProtocol, RawFrame};
use crate::io::device::kind::{ActisenseFormat, Kind, Setting};
use crate::io::device::{self, DeviceHandle};
use crate::io::{FrameWriter, PlainWriter, copy};
const FORMAT_FAST_HEADER: &[u8] = b"# format=FAST\n";
fn producer(kind: Kind) -> &'static str {
match kind {
Kind::Actisense(ActisenseFormat::NgtActisense) => "actisense-serial",
Kind::Actisense(_) => "w2k-gateway",
Kind::Aos => "aos",
Kind::Ikonvert => "ikonvert-serial",
Kind::Maretron => "maretron-ipg",
Kind::SocketCan => "socketcan-serial",
Kind::YachtDevices => "ydwg-gateway",
Kind::Miniplex => "miniplex",
Kind::SeaSmart => "seasmart",
Kind::Candump => "candump-gateway",
Kind::Plain => "plain-gateway",
Kind::Canalyst => "canalyst",
Kind::UsbCanA => "usb-can-a",
}
}
#[derive(Debug, clap::Args)]
#[command(after_long_help = super::help::gateway_help())]
pub struct Args {
#[arg(long = "kind", value_enum, value_name = "KIND", help_heading = gateway_args::GATEWAY)]
kind_arg: KindArg,
#[arg(skip = Kind::NGT1)]
kind: Kind,
#[arg(value_name = "DEVICE")]
device: String,
#[arg(short = 'r', long = "read-only", conflicts_with = "write_only", help_heading = STREAM)]
read_only: bool,
#[arg(short = 'w', long = "write-only", help_heading = STREAM)]
write_only: bool,
#[arg(short = 't', long, value_name = "SECONDS", default_value_t = 0, hide_default_value = true, help_heading = STREAM)]
timeout: u64,
#[command(flatten)]
gateway: DeviceArgs,
#[arg(long = "si", hide = true, value_name = "N", value_parser = clap::value_parser!(u8).range(0..=15))]
si: Option<u8>,
}
const STREAM: &str = "stdin and stdout";
pub fn run(mut args: Args) -> Result<()> {
args.kind = args.kind_arg.resolve(args.gateway.data_format)?;
let _ = env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info"))
.try_init();
let handle = open_device(&args)?;
let closer = handle.closer();
{
let closer = closer.clone();
super::stop_signal::on_stop(move || {
let closed = closer.close(device::CLOSE_TIMEOUT);
if closed == device::Closed::Unconfirmed {
log::error!(
"the device did not confirm closing (a gateway may still be on the bus)"
);
}
closed != device::Closed::Unconfirmed
});
}
let result = stream(&args, handle);
let closed = closer.close(device::CLOSE_TIMEOUT);
result?;
if closed == device::Closed::Unconfirmed {
anyhow::bail!("the device did not confirm closing (a gateway may still be on the bus)");
}
Ok(())
}
fn stream(args: &Args, mut handle: DeviceHandle) -> Result<()> {
let protocol = args.gateway.protocol();
let failure = handle.failure();
let lost = handle.lost();
let filter = RxFilter::new(&args.gateway.rx_filter);
let mut out = super::stdout(true);
write_prologue(&mut out, args).context("writing prologue")?;
match (args.read_only, args.write_only) {
(true, _) => {
let mut writer = PlainWriter::new(&mut out);
let mut rx = Idle::new(&mut handle.frames_rx, args.timeout).filtered(filter.clone());
copy(&mut rx, &mut writer).context("reading from device")?;
}
(false, true) => {
let (done_tx, done_rx) = std::sync::mpsc::channel::<Result<()>>();
let mut sender = handle.frame_sender();
let pump_done = done_tx.clone();
thread::Builder::new()
.name("stdin-pump".into())
.spawn(move || {
let result = pump_stdin(&mut sender, protocol).context("sending stdin frames");
let _ = pump_done.send(result);
})
.expect("spawn stdin pump");
let mut rx = handle.frames_rx;
let timeout = args.timeout;
thread::Builder::new()
.name("frame-drain".into())
.spawn(move || {
let mut rx = Idle::new(&mut rx, timeout).filtered(filter);
let result = copy(&mut rx, &mut DiscardWriter)
.map(drop)
.context("reading from device");
let _ = done_tx.send(result);
})
.expect("spawn frame drain");
done_rx.recv().expect("a stream thread reports")?;
}
(false, false) => {
let mut sender = handle.frame_sender();
let pump = thread::Builder::new()
.name("stdin-pump".into())
.spawn(move || {
if let Err(e) = pump_stdin(&mut sender, protocol) {
log::warn!("stdin pump stopped: {e}");
}
})
.expect("spawn stdin pump");
let mut writer = PlainWriter::new(&mut out);
let mut rx = Idle::new(&mut handle.frames_rx, args.timeout).filtered(filter.clone());
copy(&mut rx, &mut writer).context("reading from device")?;
drop(pump); }
}
match failure.get().or(lost.get()) {
Some(why) => anyhow::bail!("{why}"),
None => Ok(()),
}
}
struct Idle<'a> {
rx: &'a mut std::sync::mpsc::Receiver<RawFrame>,
timeout: Option<std::time::Duration>,
last: std::time::Instant,
filter: Option<RxFilter>,
}
impl<'a> Idle<'a> {
fn new(rx: &'a mut std::sync::mpsc::Receiver<RawFrame>, timeout_secs: u64) -> Self {
Self {
rx,
timeout: (timeout_secs > 0).then(|| std::time::Duration::from_secs(timeout_secs)),
last: std::time::Instant::now(),
filter: None,
}
}
fn filtered(self, filter: Option<RxFilter>) -> Self {
Self { filter, ..self }
}
fn passes(&self, frame: &RawFrame) -> bool {
self.filter.as_ref().is_none_or(|f| f.passes(frame.pgn))
}
}
impl crate::io::FrameReader for Idle<'_> {
fn read_frame(&mut self) -> io::Result<Option<RawFrame>> {
use std::sync::mpsc::RecvTimeoutError;
let Some(timeout) = self.timeout else {
return Ok(self.rx.iter().find(|frame| self.passes(frame)));
};
loop {
let left = timeout.saturating_sub(self.last.elapsed());
match self.rx.recv_timeout(left) {
Ok(frame) => {
if frame.pgn < crate::engine::codec::SYNTHETIC_PGN_START {
self.last = std::time::Instant::now();
}
if self.passes(&frame) {
return Ok(Some(frame));
}
}
Err(RecvTimeoutError::Disconnected) => return Ok(None),
Err(RecvTimeoutError::Timeout) if self.last.elapsed() >= timeout => {
return Err(io::Error::new(
io::ErrorKind::TimedOut,
format!("no data received for {} seconds", timeout.as_secs()),
));
}
Err(RecvTimeoutError::Timeout) => {}
}
}
}
}
fn pump_stdin(sender: &mut device::FrameSender, protocol: BusProtocol) -> io::Result<()> {
let db = protocol.database(crate::engine::Units::Si);
let stdin = io::stdin();
for line in stdin.lock().lines() {
let line = line?;
let t = line.trim();
if t.is_empty() || t.starts_with('#') {
continue;
}
if t.starts_with('{') {
match crate::json_input::frame_from_json(db, t) {
Ok(Some(frame)) => sender.write_frame(&frame)?,
Ok(None) => {}
Err(e) => log::warn!("stdin json: {e:#}"),
}
} else {
match crate::engine::format::plain::parse_line(t) {
Ok(frame) => sender.write_frame(&frame)?,
Err(e) => log::warn!("stdin: {e}"),
}
}
}
Ok(())
}
struct DiscardWriter;
impl FrameWriter for DiscardWriter {
fn write_frame(&mut self, _frame: &RawFrame) -> io::Result<()> {
Ok(())
}
}
fn write_prologue<W: Write>(out: &mut W, args: &Args) -> io::Result<()> {
out.write_all(FORMAT_FAST_HEADER)?;
let rec =
crate::engine::startup_record(env!("CARGO_PKG_VERSION"), producer(args.kind), &args.device);
let mut line = String::with_capacity(160);
crate::engine::format::plain::write_line(&mut line, &rec).ok();
out.write_all(line.as_bytes())?;
out.write_all(b"\n")?;
out.flush()
}
fn open_device(args: &Args) -> Result<DeviceHandle> {
let (gateway, kind) = (&args.gateway, args.kind);
gateway.check(Some(kind))?;
if args.si.is_some() && !kind.takes(Setting::Node) {
anyhow::bail!("--si: --kind {kind} does not have it");
}
if args.si.is_some() && kind.takes(Setting::RunNode) && !gateway.node {
anyhow::bail!("--si: --kind {kind} runs canboat's node only with --node");
}
let mut dev = gateway.device(kind, &args.device);
if dev.capabilities().node {
if let Some(instance) = args.si {
dev = dev.system_instance(instance);
}
dev = dev.timeout(Duration::from_secs(args.timeout));
if gateway.unique.is_none() {
dev = dev.state_dir(super::app::resolve_config_dir());
}
}
if args.write_only && !dev.capabilities().sends {
anyhow::bail!(
"--write-only: --kind {kind} at {} only receives; use tcp:// to send",
args.device
);
}
dev.open(gateway.protocol()).map_err(|e| {
if e.kind() == io::ErrorKind::InvalidInput {
anyhow::Error::new(e)
} else {
anyhow::Error::new(e).context(format!("opening --kind {kind} {}", args.device))
}
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::io::FrameReader;
use std::sync::mpsc;
use std::time::{Duration, Instant};
fn frame(pgn: u32) -> RawFrame {
RawFrame::new(None, 6, pgn, 1, 255, [0u8; 8])
}
#[test]
fn a_silent_bus_times_out() {
let (tx, mut rx) = mpsc::channel();
let mut idle = Idle::new(&mut rx, 1);
tx.send(frame(127508)).unwrap();
assert_eq!(idle.read_frame().unwrap().unwrap().pgn, 127508);
let start = Instant::now();
let sender = thread::spawn(move || {
for _ in 0..3 {
thread::sleep(Duration::from_millis(200));
let _ = tx.send(frame(262400));
}
thread::sleep(Duration::from_secs(2));
drop(tx);
});
let mut synthetic = 0;
let err = loop {
match idle.read_frame() {
Ok(Some(_)) => synthetic += 1,
Ok(None) => panic!("ended without a timeout"),
Err(e) => break e,
}
};
assert_eq!(synthetic, 3);
assert_eq!(err.kind(), io::ErrorKind::TimedOut);
let waited = start.elapsed();
assert!(waited >= Duration::from_millis(900) && waited < Duration::from_millis(1500));
sender.join().unwrap();
}
#[test]
fn the_rx_filter_drops_what_was_not_asked_for() {
for timeout in [0, 5] {
let (tx, mut rx) = mpsc::channel();
for pgn in [130306, 127250, 60928, 129029, 262400] {
tx.send(frame(pgn)).unwrap();
}
drop(tx);
let mut idle = Idle::new(&mut rx, timeout).filtered(RxFilter::new(&[127250]));
let mut pgns = Vec::new();
while let Some(f) = idle.read_frame().unwrap() {
pgns.push(f.pgn);
}
assert_eq!(pgns, [127250, 60928, 262400]);
}
}
#[test]
fn a_closed_device_ends_the_stream() {
let (tx, mut rx) = mpsc::channel::<RawFrame>();
drop(tx);
assert!(Idle::new(&mut rx, 0).read_frame().unwrap().is_none());
assert!(Idle::new(&mut rx, 5).read_frame().unwrap().is_none());
}
}