use std::collections::VecDeque;
use std::io::{self, LineWriter, Write};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::Receiver;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use crate::engine::format::write_plain;
use crate::engine::output::{JsonOptions, write_json};
use crate::engine::{FramePacketType, PgnDatabase, RawFrame, Reassembled, Reassembler};
use crate::io::device::FrameSender;
use crate::n2kd::request_engine::RequestEngine;
use crate::n2kd::serving::Hub;
use crate::server::quirks::Quirks;
use crate::server::snapshot::SnapshotStore;
struct OutputBatcher {
hub: Arc<Hub>,
pending: String,
oldest: Instant,
}
const BATCH_MAX_AGE: Duration = Duration::from_millis(25);
const BATCH_MAX_BYTES: usize = 16 * 1024;
impl OutputBatcher {
fn new(hub: Arc<Hub>) -> Self {
Self {
hub,
pending: String::with_capacity(2048),
oldest: Instant::now(),
}
}
#[inline]
fn has_subscribers(&self) -> bool {
self.hub.has_subscribers()
}
fn push(&mut self, line: &str, now: Instant) {
if self.pending.is_empty() {
self.oldest = now;
}
self.pending.push_str(line);
if self.pending.len() >= BATCH_MAX_BYTES {
self.flush();
}
}
#[inline]
fn has_pending(&self) -> bool {
!self.pending.is_empty()
}
#[inline]
fn flush_due(&mut self, now: Instant) {
if !self.pending.is_empty() && now.duration_since(self.oldest) >= BATCH_MAX_AGE {
self.flush();
}
}
fn flush(&mut self) {
self.hub.broadcast(&self.pending);
self.pending.clear();
}
}
fn is_ais_pgn(pgn: u32) -> bool {
matches!(
pgn,
129038
| 129039
| 129040
| 129041
| 129793
| 129794
| 129798
| 129801
| 129802
| 129809
| 129810
)
}
pub struct Hubs {
pub raw: Arc<Hub>,
pub nmea: Arc<Hub>,
pub analyzer: Arc<Hub>,
pub snapshot: Option<Arc<SnapshotStore>>,
pub engine: Arc<RequestEngine>,
pub quirks: Quirks,
pub device_sender: Option<FrameSender>,
pub claim_addr: Option<Arc<std::sync::atomic::AtomicU8>>,
pub overrides: Option<Arc<Mutex<crate::n2kd::overrides::OverrideEngine>>>,
pub decoded_tx: Option<std::sync::mpsc::Sender<Arc<crate::engine::DecodedPgn>>>,
}
pub struct Nmea0183Options {
pub emit_stdout: bool,
pub rate_limit: bool,
pub filter: Option<Arc<Mutex<crate::n2kd::nmea_filter::NmeaFilter>>>,
}
pub fn run(
db: &'static PgnDatabase,
frames_rx: Receiver<RawFrame>,
mut hubs: Hubs,
pre_coalesced: Arc<AtomicBool>,
json_opts: JsonOptions,
nmea: Nmea0183Options,
) {
let Nmea0183Options {
emit_stdout: emit_nmea_stdout,
rate_limit: nmea0183_rate_limit,
filter: nmea_filter,
} = nmea;
let stdout = io::stdout();
let mut out = LineWriter::new(stdout.lock());
let mut reasm = Reassembler::new();
let mut nmea_buf = String::with_capacity(256);
let mut raw_line = String::with_capacity(256);
let mut json_line = String::with_capacity(1024);
let mut rl = crate::n2kd::nmea0183::RateLimiter::new(nmea0183_rate_limit);
let handles = crate::n2kd::decoded::Handles::new();
let mut pending_synth: VecDeque<RawFrame> = VecDeque::new();
let mut raw_batch = OutputBatcher::new(hubs.raw.clone());
let mut nmea_batch = OutputBatcher::new(hubs.nmea.clone());
let mut analyzer_batch = OutputBatcher::new(hubs.analyzer.clone());
loop {
let any_pending =
raw_batch.has_pending() || nmea_batch.has_pending() || analyzer_batch.has_pending();
let frame = if let Some(synth) = pending_synth.pop_front() {
synth
} else if any_pending {
match frames_rx.recv_timeout(BATCH_MAX_AGE) {
Ok(f) => f,
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
let now = Instant::now();
raw_batch.flush_due(now);
nmea_batch.flush_due(now);
analyzer_batch.flush_due(now);
continue;
}
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break,
}
} else {
match frames_rx.recv() {
Ok(f) => f,
Err(_) => break,
}
};
let now = Instant::now();
if !pre_coalesced.load(Ordering::Relaxed) && frame.data.len() > 8 {
log::debug!(
"frame with {} payload bytes seen; locking pipeline into coalesced mode",
frame.data.len()
);
pre_coalesced.store(true, Ordering::Relaxed);
}
let assembled = if pre_coalesced.load(Ordering::Relaxed) {
frame
} else {
let packet_type = db
.first_pgn(frame.pgn)
.or_else(|| db.fallback_pgn(frame.pgn))
.map(|p| match p.packet_type {
crate::engine::PacketType::Fast => FramePacketType::Fast,
crate::engine::PacketType::Single => FramePacketType::Single,
_ => FramePacketType::Other,
})
.unwrap_or(FramePacketType::Other);
match reasm.push(frame, packet_type) {
Reassembled::PassThrough(f) | Reassembled::Complete(f) => f,
_ => continue,
}
};
if raw_batch.has_subscribers() {
raw_line.clear();
if write_plain(&mut raw_line, &assembled).is_ok() {
raw_line.push('\n');
raw_batch.push(&raw_line, now);
}
}
let Ok(decoded) = db.decode(&assembled) else {
continue;
};
let decoded = Arc::new(decoded);
if let Some(tx) = hubs.decoded_tx.as_ref() {
let _ = tx.send(Arc::clone(&decoded));
}
if hubs.quirks.is_enabled() {
let own_addr = hubs
.claim_addr
.as_ref()
.map(|a| a.load(Ordering::Relaxed))
.filter(|&a| a != crate::engine::ADDR_GLOBAL && a != crate::engine::ADDR_NULL);
for mut synth in hubs.quirks.process_decoded(&decoded, own_addr) {
if (synth.src == 0 || synth.src == crate::engine::ADDR_GLOBAL)
&& let Some(addr) = own_addr
{
synth.src = addr;
}
if let Some(sender) = hubs.device_sender.as_ref() {
let _ = sender.send_frame(synth.clone());
}
pending_synth.push_back(synth);
}
}
hubs.engine.note_device_seen(decoded.pgn, decoded.src);
if let Some(f) = nmea_filter.as_ref()
&& let Some(name) = decoded.iso_name()
{
f.lock().unwrap().note_address_claim(decoded.src, name);
}
if let Some(ov) = hubs.overrides.as_ref() {
if let Some(name) = decoded.iso_name() {
let replay = ov.lock().unwrap().note_address_claim(decoded.src, name);
if let Some(sender) = hubs.device_sender.as_ref() {
for frame in replay {
let _ = sender.send_frame(frame);
}
}
} else if decoded.pgn == 126208 {
use crate::engine::field::nmea_acknowledge_group_function as ack;
let field_int = |f| decoded.field(f).and_then(|d| d.value.as_i64());
if field_int(ack::FUNCTION_CODE) == Some(2) {
let pgn_err = field_int(ack::PGN_ERROR_CODE).unwrap_or(0);
let iv_err =
field_int(ack::TRANSMISSION_INTERVAL_PRIORITY_ERROR_CODE).unwrap_or(0);
if (pgn_err != 0 || iv_err != 0)
&& let Some(acked) = field_int(ack::PGN)
{
ov.lock().unwrap().note_nak(decoded.src, acked as u32);
}
}
}
}
if let Some(snap) = hubs.snapshot.as_ref() {
snap.store(&decoded, now);
}
let want_json = analyzer_batch.has_subscribers();
let mut json_ok = false;
if want_json {
json_line.clear();
json_ok = write_json(&mut json_line, &decoded, &json_opts).is_ok();
}
nmea_buf.clear();
let pgn = decoded.pgn;
let ais_branch = is_ais_pgn(pgn);
let want_nmea = emit_nmea_stdout || nmea_batch.has_subscribers() || nmea_filter.is_some();
let converted = if !want_nmea {
false
} else {
crate::n2kd::decoded::convert_nmea0183(&mut nmea_buf, &decoded, &mut rl, &handles) > 0
};
if converted
&& !ais_branch
&& let Some(f) = nmea_filter.as_ref()
{
f.lock().unwrap().apply(decoded.src, &mut nmea_buf);
}
if !nmea_buf.is_empty() {
if emit_nmea_stdout {
let _ = out.write_all(nmea_buf.as_bytes());
}
if nmea_batch.has_subscribers() {
nmea_batch.push(&nmea_buf, now);
}
}
if json_ok && analyzer_batch.has_subscribers() {
json_line.push('\n');
analyzer_batch.push(&json_line, now);
}
raw_batch.flush_due(now);
nmea_batch.flush_due(now);
analyzer_batch.flush_due(now);
}
raw_batch.flush();
nmea_batch.flush();
analyzer_batch.flush();
out.flush().ok();
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::mpsc;
fn tapped_hubs(decoded_tx: mpsc::Sender<Arc<crate::engine::DecodedPgn>>) -> Hubs {
let quirks = {
let _guard = crate::server::quirks::tests::switch_guard();
crate::server::quirks::Quirks::new(Vec::new())
};
Hubs {
raw: Arc::new(Hub::new()),
nmea: Arc::new(Hub::new()),
analyzer: Arc::new(Hub::new()),
snapshot: None,
engine: Arc::new(RequestEngine::new()),
quirks,
device_sender: None,
claim_addr: None,
overrides: None,
decoded_tx: Some(decoded_tx),
}
}
#[test]
fn decoded_tap_forwards_each_record() {
let db = crate::engine::PgnDatabase::embedded(crate::engine::Units::Metric);
let (frames_tx, frames_rx) = mpsc::channel();
let (tap_tx, tap_rx) = mpsc::channel();
let claim =
crate::engine::RawFrame::new(None, 6, 60928, 0x21, 0xFF, [1, 2, 3, 4, 5, 6, 7, 8]);
frames_tx.send(claim).unwrap();
drop(frames_tx);
let pre_coalesced = Arc::new(AtomicBool::new(true));
let handle = std::thread::spawn(move || {
run(
db,
frames_rx,
tapped_hubs(tap_tx),
pre_coalesced,
JsonOptions {
include_empty: false,
name_value: true,
debug: false,
camel_case: crate::engine::output::CamelCase::Off,
wrap: false,
},
Nmea0183Options {
emit_stdout: false,
rate_limit: true,
filter: None,
},
)
});
let decoded = tap_rx
.recv()
.expect("the tap should receive the decoded record");
assert_eq!(decoded.pgn, 60928);
assert_eq!(decoded.src, 0x21);
assert!(tap_rx.recv().is_err(), "tap should see only the one record");
handle.join().unwrap();
}
}