use std::io::{BufRead, BufReader, Write};
use std::net::{Ipv4Addr, Shutdown, SocketAddrV4, TcpListener, TcpStream};
use std::sync::atomic::AtomicBool;
use std::sync::{Arc, Mutex};
use std::thread::{self, JoinHandle};
use std::time::{SystemTime, UNIX_EPOCH};
use anyhow::{Context, Result};
use crate::engine::PgnDatabase;
use crate::engine::format::{PlainError, parse_plain};
use crate::engine::output::{JsonOptions, write_json};
use crate::io::device::FrameSender;
use crate::n2kd::nmea_filter::NmeaFilter;
use crate::n2kd::overrides::OverrideEngine;
use crate::n2kd::serving::tcp::accept_until;
pub fn spawn_input_server(
name: &'static str,
bind: Ipv4Addr,
port: u16,
device: Option<FrameSender>,
stop: Option<Arc<AtomicBool>>,
) -> Result<JoinHandle<()>> {
let listener = TcpListener::bind(SocketAddrV4::new(bind, port))
.with_context(|| format!("binding {name} TCP port {}:{}", bind, port))?;
let mode = if device.is_some() {
"write-only"
} else {
"write-only (writes dropped)"
};
log::info!("{name} server listening on {}:{} ({mode})", bind, port);
Ok(thread::Builder::new()
.name(format!("{name}-accept"))
.spawn(move || input_accept(name, listener, device, stop))
.expect("spawn input accept"))
}
fn input_accept(
name: &'static str,
listener: TcpListener,
device: Option<FrameSender>,
stop: Option<Arc<AtomicBool>>,
) {
accept_until(name, listener, stop, |stream, peer| {
log::info!("{name} client connected: {peer}");
let dev = device.clone();
thread::Builder::new()
.name(format!("{name}-client"))
.spawn(move || {
if let Err(e) = stream.shutdown(Shutdown::Write) {
log::debug!("{name}: shutdown(write) failed: {e}");
}
run_inbound_reader(name, stream, dev);
})
.ok();
});
}
fn run_inbound_reader(name: &'static str, stream: TcpStream, device: Option<FrameSender>) {
let reader = BufReader::new(stream);
for line in reader.lines() {
let line = match line {
Ok(l) => l,
Err(_) => return,
};
match &device {
Some(d) => {
if !forward_plain_line(&line, d) {
return;
}
}
None => {
let trimmed = line.trim_end_matches(['\r', '\n']);
if !trimmed.is_empty() {
log::debug!("{name}: no device wired up, dropping client line: {trimmed}");
}
}
}
}
}
fn forward_plain_line(line: &str, device: &FrameSender) -> bool {
let trimmed = line.trim_end_matches(['\r', '\n']);
if trimmed.is_empty() || trimmed.starts_with('#') {
return true;
}
if trimmed.starts_with('$') || trimmed.starts_with('!') {
log::debug!("skipping non-PLAIN/FAST line: {trimmed}");
return true;
}
match parse_plain(trimmed) {
Ok(frame) => {
if frame.pgn == crate::n2kd::nmea_filter::PGN_NMEA0183_FILTER
|| frame.pgn == crate::n2kd::overrides::PGN_PGN_OVERRIDE
{
log::debug!(
"dropping stray control PGN {} on input port; control channels have dedicated ports",
frame.pgn
);
return true;
}
device.send_frame(frame).is_ok()
}
Err(PlainError::Empty) => true,
Err(e) => {
log_bad_plain_line(trimmed, &e);
true
}
}
}
fn log_bad_plain_line(line: &str, err: &PlainError) {
match err.byte_offset() {
Some(offset) => {
let pointer_col = offset.min(line.len());
let pointer = format!("{:width$}^", "", width = pointer_col);
log::warn!(
"ignoring malformed PLAIN/FAST line: {err}\n line: {line}\n pointer: {pointer}"
);
}
None => {
log::warn!("ignoring malformed PLAIN/FAST line: {err}\n line: {line}");
}
}
}
pub fn spawn_filter_control_server(
bind: Ipv4Addr,
port: u16,
filter: Arc<Mutex<NmeaFilter>>,
json_opts: JsonOptions,
stop: Option<Arc<AtomicBool>>,
) -> Result<JoinHandle<()>> {
let listener = TcpListener::bind(SocketAddrV4::new(bind, port))
.with_context(|| format!("binding filter-control TCP port {}:{}", bind, port))?;
log::info!(
"nmea0183-filter control server listening on {}:{} (RW)",
bind,
port
);
Ok(thread::Builder::new()
.name("filter-control-accept".into())
.spawn(move || filter_control_accept(listener, filter, json_opts, stop))
.expect("spawn filter-control accept"))
}
fn filter_control_accept(
listener: TcpListener,
filter: Arc<Mutex<NmeaFilter>>,
json_opts: JsonOptions,
stop: Option<Arc<AtomicBool>>,
) {
accept_until("filter-control", listener, stop, |stream, peer| {
log::info!("filter-control client connected: {peer}");
let f = filter.clone();
let opts = json_opts.clone();
thread::Builder::new()
.name("filter-control-client".into())
.spawn(move || run_filter_control_client(stream, f, opts))
.ok();
});
}
fn run_filter_control_client(
stream: TcpStream,
filter: Arc<Mutex<NmeaFilter>>,
json_opts: JsonOptions,
) {
let db = PgnDatabase::embedded(crate::engine::Units::Metric);
let mut write_stream = match stream.try_clone() {
Ok(s) => s,
Err(e) => {
log::debug!("filter-control: try_clone failed: {e}");
return;
}
};
if !send_filter_report(&mut write_stream, &filter, db, &json_opts) {
return;
}
let reader = BufReader::new(stream);
for line in reader.lines() {
let line = match line {
Ok(l) => l,
Err(_) => return,
};
let trimmed = line.trim_end_matches(['\r', '\n']);
if trimmed.is_empty() {
continue;
}
let frame = match parse_plain(trimmed) {
Ok(f) => f,
Err(PlainError::Empty) => continue,
Err(e) => {
log::debug!("filter-control: ignoring non-PLAIN line: {e}");
continue;
}
};
if crate::n2kd::nmea_filter::is_set_frame(&frame) {
filter.lock().unwrap().apply_set_frame(&frame.data);
} else if !crate::n2kd::nmea_filter::is_request_frame(&frame) {
log::debug!("filter-control: ignoring frame pgn {}", frame.pgn);
continue;
}
if !send_filter_report(&mut write_stream, &filter, db, &json_opts) {
return;
}
}
}
fn send_filter_report(
stream: &mut TcpStream,
filter: &Mutex<NmeaFilter>,
db: &PgnDatabase,
json_opts: &JsonOptions,
) -> bool {
let out = filter_report_lines(filter, db, json_opts);
if out.is_empty() {
return true;
}
stream.write_all(out.as_bytes()).is_ok()
}
fn filter_report_lines(
filter: &Mutex<NmeaFilter>,
db: &PgnDatabase,
json_opts: &JsonOptions,
) -> String {
let frames = filter.lock().unwrap().report_frames(Some(now_iso()));
let mut out = String::with_capacity(frames.len() * 96);
for frame in &frames {
match db.decode(frame) {
Ok(decoded) => {
let start = out.len();
if write_json(&mut out, &decoded, json_opts).is_ok() {
out.push('\n');
} else {
out.truncate(start);
}
}
Err(e) => log::debug!("filter-control: decoding a 262657 report failed: {e}"),
}
}
out
}
fn now_iso() -> String {
let ms = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
crate::engine::format_iso_ms(ms)
}
pub fn spawn_overrides_control_server(
bind: Ipv4Addr,
port: u16,
engine: Arc<Mutex<OverrideEngine>>,
device_sender: Option<FrameSender>,
json_opts: JsonOptions,
stop: Option<Arc<AtomicBool>>,
) -> Result<JoinHandle<()>> {
let listener = TcpListener::bind(SocketAddrV4::new(bind, port))
.with_context(|| format!("binding overrides-control TCP port {}:{}", bind, port))?;
log::info!(
"overrides control server listening on {}:{} (RW)",
bind,
port
);
Ok(thread::Builder::new()
.name("overrides-control-accept".into())
.spawn(move || overrides_control_accept(listener, engine, device_sender, json_opts, stop))
.expect("spawn overrides-control accept"))
}
fn overrides_control_accept(
listener: TcpListener,
engine: Arc<Mutex<OverrideEngine>>,
device_sender: Option<FrameSender>,
json_opts: JsonOptions,
stop: Option<Arc<AtomicBool>>,
) {
accept_until("overrides-control", listener, stop, |stream, peer| {
log::info!("overrides-control client connected: {peer}");
let e = engine.clone();
let sender = device_sender.clone();
let opts = json_opts.clone();
thread::Builder::new()
.name("overrides-control-client".into())
.spawn(move || run_overrides_control_client(stream, e, sender, opts))
.ok();
});
}
fn run_overrides_control_client(
stream: TcpStream,
engine: Arc<Mutex<OverrideEngine>>,
device_sender: Option<FrameSender>,
json_opts: JsonOptions,
) {
let db = PgnDatabase::embedded(crate::engine::Units::Metric);
let mut write_stream = match stream.try_clone() {
Ok(s) => s,
Err(e) => {
log::debug!("overrides-control: try_clone failed: {e}");
return;
}
};
if !send_override_report(&mut write_stream, &engine, db, &json_opts) {
return;
}
let reader = BufReader::new(stream);
for line in reader.lines() {
let line = match line {
Ok(l) => l,
Err(_) => return,
};
let trimmed = line.trim_end_matches(['\r', '\n']);
if trimmed.is_empty() {
continue;
}
let frame = match parse_plain(trimmed) {
Ok(f) => f,
Err(PlainError::Empty) => continue,
Err(e) => {
log::debug!("overrides-control: ignoring non-PLAIN line: {e}");
continue;
}
};
if crate::n2kd::overrides::is_set_frame(&frame) {
let request = engine.lock().unwrap().apply_set_frame(&frame.data);
if let (Some(req), Some(sender)) = (request, device_sender.as_ref()) {
let _ = sender.send_frame(req);
}
} else if crate::n2kd::overrides::is_delete_frame(&frame) {
engine.lock().unwrap().apply_delete_frame(&frame.data);
} else if !crate::n2kd::overrides::is_request_frame(&frame) {
log::debug!("overrides-control: ignoring frame pgn {}", frame.pgn);
continue;
}
if !send_override_report(&mut write_stream, &engine, db, &json_opts) {
return;
}
}
}
fn send_override_report(
stream: &mut TcpStream,
engine: &Mutex<OverrideEngine>,
db: &PgnDatabase,
json_opts: &JsonOptions,
) -> bool {
let frames = engine.lock().unwrap().report_frames(Some(now_iso()));
let mut out = String::with_capacity(frames.len() * 96);
for frame in &frames {
match db.decode(frame) {
Ok(decoded) => {
let start = out.len();
if write_json(&mut out, &decoded, json_opts).is_ok() {
out.push('\n');
} else {
out.truncate(start);
}
}
Err(e) => log::debug!("overrides-control: decoding a 262658 report failed: {e}"),
}
}
if out.is_empty() {
return true;
}
stream.write_all(out.as_bytes()).is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engine::output::CamelCase;
#[test]
fn filter_report_lines_render_nv_json_with_timestamp() {
let path = std::env::temp_dir().join("canboat-filter-control-test.json");
let mut filter = NmeaFilter::load(&path).unwrap();
filter.note_address_claim(35, 0x0004_0000_0761_9208);
let mut buf = "$CDVHW,,T,,M,4.6,N,8.6,K*5E\r\n".to_string();
filter.apply(35, &mut buf);
assert!(filter.set(35, "VHW", true));
let db = PgnDatabase::embedded(crate::engine::Units::Metric);
let json_opts = JsonOptions {
include_empty: false,
name_value: true,
debug: false,
camel_case: CamelCase::Off,
wrap: false,
protocol: crate::engine::BusProtocol::Nmea2000,
};
let filter = Mutex::new(filter);
let out = filter_report_lines(&filter, db, &json_opts);
assert!(out.contains("262657"), "out: {out}");
assert!(
out.contains("\"timestamp\""),
"every report line must carry a timestamp: {out}"
);
assert!(
out.lines()
.any(|l| l.contains("\"Sentence\":\"VHW\"") && l.contains("\"Muted\":1")),
"expected a muted VHW row: {out}"
);
assert!(
out.contains("\"Sentence\":\"ALL\""),
"expected a whole-source ALL row: {out}"
);
let _ = std::fs::remove_file(&path);
}
}