use std::io::{self, BufRead, BufWriter, Read, Write};
use std::net::TcpStream;
use std::sync::Arc;
use std::sync::atomic::AtomicU8;
use std::thread;
use anyhow::{Context, Result};
use crate::engine::RawFrame;
use crate::io::device::{self, DeviceHandle};
use crate::io::{FrameWriter, PlainWriter, copy, open_serial_rw};
const FORMAT_FAST_HEADER: &[u8] = b"# format=FAST\n";
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
enum Kind {
Ngt1,
Ikonvert,
Maretron,
Socketcan,
Ydwg,
#[value(name = "w2k-ascii")]
W2kAscii,
}
impl Kind {
fn producer(self) -> &'static str {
match self {
Kind::Ngt1 => "actisense-serial",
Kind::Ikonvert => "ikonvert-serial",
Kind::Maretron => "maretron-ipg",
Kind::Socketcan => "socketcan-serial",
Kind::Ydwg => "ydwg-gateway",
Kind::W2kAscii => "w2k-gateway",
}
}
fn default_baud(self) -> u32 {
match self {
Kind::Ngt1 => 115_200,
Kind::Ikonvert => 230_400,
Kind::Maretron | Kind::Socketcan | Kind::Ydwg | Kind::W2kAscii => 0,
}
}
}
#[derive(Debug, clap::Args)]
pub struct Args {
#[arg(long, value_enum)]
kind: Kind,
#[arg(value_name = "DEVICE")]
device: String,
#[arg(short = 'b', long, short_alias = 's')]
baud: Option<u32>,
#[arg(short = 'r', long = "read-only", conflicts_with = "write_only")]
read_only: bool,
#[arg(short = 'w', long = "write-only")]
write_only: bool,
#[arg(long, value_name = "PGN,...")]
rx: Option<String>,
#[arg(long, value_name = "PGN,...")]
tx: Option<String>,
#[arg(long)]
rate_limit_off: bool,
#[arg(long, value_name = "PASSWORD")]
password: Option<String>,
#[arg(short = 'a', long, value_name = "ADDR", default_value_t = 0)]
address: u8,
#[arg(short = 'n', long)]
no_claim: bool,
#[arg(short = 'u', long, value_name = "N", default_value_t = 0)]
unique: u32,
#[arg(short = 'm', long, value_name = "N", default_value_t = 999)]
manufacturer: u16,
#[arg(long, alias = "hb", value_name = "MS", default_value_t = 60_000)]
heartbeat: u64,
#[arg(long, alias = "si", value_name = "N", default_value_t = 15)]
system_instance: u8,
#[arg(short = 't', long, value_name = "SECONDS", default_value_t = 0)]
timeout: u64,
}
pub fn run(args: Args) -> Result<()> {
let mut handle = open_device(&args)?;
let stdout = io::stdout();
let mut out = BufWriter::new(stdout.lock());
write_prologue(&mut out, &args).context("writing prologue")?;
match (args.read_only, args.write_only) {
(true, _) => {
let mut writer = PlainWriter::new(&mut out);
copy(&mut handle.frames_rx, &mut writer).context("reading from device")?;
}
(false, true) => {
let mut sender = handle.frame_sender();
let mut rx = handle.frames_rx;
let drain = thread::Builder::new()
.name("frame-drain".into())
.spawn(move || {
let _ = copy(&mut rx, &mut DiscardWriter);
})
.expect("spawn frame drain");
pump_stdin(&mut sender).context("sending stdin frames")?;
drop(drain);
}
(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) {
log::warn!("stdin pump stopped: {e}");
}
})
.expect("spawn stdin pump");
let mut writer = PlainWriter::new(&mut out);
copy(&mut handle.frames_rx, &mut writer).context("reading from device")?;
drop(pump); }
}
Ok(())
}
fn pump_stdin(sender: &mut device::FrameSender) -> io::Result<()> {
let db = crate::engine::PgnDatabase::embedded(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"),
args.kind.producer(),
&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> {
match args.kind {
Kind::Ngt1 => {
let (r, w) = open_stream(args)?;
Ok(device::ngt1::run(r, w))
}
Kind::Ikonvert => {
let (r, w) = open_stream(args)?;
let config = device::ikonvert::Config {
rx_list: args.rx.clone(),
tx_list: args.tx.clone(),
rate_limit_off: args.rate_limit_off,
skip_init: false,
};
Ok(device::ikonvert::run(r, w, config))
}
Kind::Maretron => {
let endpoint = args.device.strip_prefix("tcp://").unwrap_or(&args.device);
let endpoint = if endpoint.contains(':') {
endpoint.to_string()
} else {
format!("{endpoint}:6543")
};
let stream = TcpStream::connect(&endpoint)
.with_context(|| format!("connecting to {endpoint}"))?;
let reader: Box<dyn Read + Send> =
Box::new(stream.try_clone().context("cloning TCP stream")?);
let writer: Box<dyn Write + Send> = Box::new(stream);
let config = device::maretron::Config {
password: args.password.clone().unwrap_or_default(),
fixtime: None,
};
Ok(device::maretron::run(reader, writer, config))
}
Kind::Ydwg => {
let (reader, writer) = open_line_endpoint(&args.device, 1457)?;
Ok(device::line_gateway::run(
reader,
writer,
device::line_gateway::Protocol::YdwgRaw,
))
}
Kind::W2kAscii => {
let (reader, writer) = open_line_endpoint(&args.device, 60002)?;
Ok(device::line_gateway::run(
reader,
writer,
device::line_gateway::Protocol::N2kAscii,
))
}
Kind::Socketcan => {
let config = device::socketcan::Config {
address: args.address,
unique: args.unique,
manufacturer: args.manufacturer,
system_instance: args.system_instance,
heartbeat_ms: args.heartbeat,
no_claim: args.no_claim,
timeout_secs: args.timeout,
..device::socketcan::Config::default()
};
let claim = Arc::new(AtomicU8::new(config.address));
device::socketcan::run(&args.device, config, claim)
.with_context(|| format!("opening SocketCAN interface {}", args.device))
}
}
}
fn open_stream(args: &Args) -> Result<(Box<dyn Read + Send>, Box<dyn Write + Send>)> {
if args.device.starts_with("tcp://") {
return open_tcp(&args.device, 6001);
}
let baud = args.baud.unwrap_or_else(|| args.kind.default_baud());
open_serial_rw(&args.device, baud)
.with_context(|| format!("opening serial port {}", args.device))
}
fn open_tcp(
device: &str,
default_port: u16,
) -> Result<(Box<dyn Read + Send>, Box<dyn Write + Send>)> {
let endpoint = device.strip_prefix("tcp://").unwrap_or(device);
let endpoint = if endpoint.contains(':') {
endpoint.to_string()
} else {
format!("{endpoint}:{default_port}")
};
let stream =
TcpStream::connect(&endpoint).with_context(|| format!("connecting to {endpoint}"))?;
let reader: Box<dyn Read + Send> = Box::new(stream.try_clone().context("cloning TCP stream")?);
let writer: Box<dyn Write + Send> = Box::new(stream);
Ok((reader, writer))
}
fn open_line_endpoint(
device: &str,
default_tcp_port: u16,
) -> Result<(Box<dyn Read + Send>, Box<dyn Write + Send>)> {
if let Some(rest) = device.strip_prefix("udp://") {
let bind = if rest.contains(':') {
rest.to_string()
} else {
format!("0.0.0.0:{rest}")
};
let sock =
std::net::UdpSocket::bind(&bind).with_context(|| format!("binding UDP {bind}"))?;
log::warn!("UDP gateway mode is receive-only; transmitted frames are dropped");
let reader: Box<dyn Read + Send> = Box::new(UdpRead(sock));
let writer: Box<dyn Write + Send> = Box::new(std::io::sink());
return Ok((reader, writer));
}
open_tcp(device, default_tcp_port)
}
struct UdpRead(std::net::UdpSocket);
impl Read for UdpRead {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let (n, _peer) = self.0.recv_from(buf)?;
Ok(n)
}
}