use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, AtomicU8};
use std::sync::{Arc, Mutex, mpsc};
use std::thread;
use anyhow::{Result, anyhow};
use crate::engine::output::JsonOptions;
use crate::engine::pgn_list::{PgnListStatus, RxFilter};
use crate::engine::{DecodedPgn, PgnDatabase, RawFrame};
use crate::io::device::{DeviceHandle, FrameSender};
use crate::n2kd::request_engine::{self, RequestEngine};
use crate::n2kd::serving::Hub;
use crate::n2kd::serving::tcp as serving_tcp;
use crate::server::pipeline::{self, Hubs, Nmea0183Options};
use crate::server::snapshot::SnapshotStore;
use crate::server::{BridgeConfig, OpenedSource, mdns, quirks, tcp};
pub struct Bridge {
config: BridgeConfig,
db: &'static PgnDatabase,
json_opts: JsonOptions,
banner: &'static [u8],
frames_rx: Option<mpsc::Receiver<RawFrame>>,
supervisor: Option<DeviceHandle>,
failure: Option<crate::io::device::DeviceFailure>,
pre_coalesced: Arc<AtomicBool>,
device_sender: Option<FrameSender>,
claim_addr: Option<Arc<AtomicU8>>,
pgn_list_status: Option<PgnListStatus>,
raw_hub: Arc<Hub>,
nmea_hub: Arc<Hub>,
analyzer_hub: Arc<Hub>,
snapshot: Option<Arc<SnapshotStore>>,
engine: Arc<RequestEngine>,
nmea_filter: Option<Arc<Mutex<crate::n2kd::nmea_filter::NmeaFilter>>>,
overrides: Option<Arc<Mutex<crate::n2kd::overrides::OverrideEngine>>>,
mcu006_link: Option<quirks::mcu006::Link>,
#[cfg_attr(not(feature = "mdns"), allow(dead_code))]
node_name: Option<u64>,
txt: mdns::Txt,
decoded_tx: Option<mpsc::Sender<Arc<DecodedPgn>>>,
serve_stop: Arc<AtomicBool>,
tcp_joins: Vec<thread::JoinHandle<()>>,
pipeline_join: Option<thread::JoinHandle<()>>,
}
fn state_file_paths(
config_dir: Option<&Path>,
explicit_filter: Option<&Path>,
) -> (Option<PathBuf>, Option<PathBuf>) {
let filter = explicit_filter
.map(Path::to_path_buf)
.or_else(|| config_dir.map(|d| d.join("nmea0183-filter.json")));
let overrides = config_dir.map(|d| d.join("overrides.json"));
(filter, overrides)
}
impl Bridge {
pub fn new(config: BridgeConfig) -> Result<Self> {
if config.protocol != crate::engine::BusProtocol::Nmea2000
&& let Some(kind) = config.quirk.first()
{
anyhow::bail!(
"--quirk {} works on NMEA 2000 PGNs and cannot run with --protocol {}",
kind.name(),
config.protocol
);
}
for kind in [
quirks::QuirkKind::Scx20,
quirks::QuirkKind::Motion,
quirks::QuirkKind::Mcu006,
] {
if config.quirk.contains(&kind) && !config.is_node() {
let name = kind.name();
anyhow::bail!(
"--quirk {name} only works with --kind socketcan, canalyst or usb-can-a; it claims/impersonates \
a device, and other backends rewrite src on outbound writes so the \
frame cannot reach the bus with the claimed source address"
);
}
}
let units = config.units;
let db = config.protocol.database(units);
let json_opts = JsonOptions {
include_empty: false,
name_value: true,
debug: false,
camel_case: config.camel_case,
wrap: config.wrap,
protocol: config.protocol,
};
let banner: &'static [u8] = Box::leak(
format!(
"{}\n",
crate::build_info::version_banner(
units == crate::engine::Units::Si,
json_opts.name_value,
config.protocol,
)
)
.into_bytes()
.into_boxed_slice(),
);
let snapshot = if config.snapshot_port != 0 {
Some(Arc::new(SnapshotStore::new(json_opts.clone(), db)))
} else {
None
};
let engine = Arc::new(RequestEngine::new());
let OpenedSource {
frames_rx,
supervisor,
pre_coalesced,
claim_addr,
pgn_list_status,
node_name,
} = super::open_source(&config)?;
let device_sender = supervisor.as_ref().map(|s| s.frame_sender());
if config.quirk.contains(&quirks::QuirkKind::GpsRelay) {
if !quirks::QuirkKind::gps_relay_has_devices(&config.quirk) {
anyhow::bail!(
"--quirk gps-relay needs --quirk gps-rollover=<device>[,<device>...] naming \
the device(s) whose data to relay (not `all`)"
);
}
if device_sender.is_none() {
anyhow::bail!(
"--quirk gps-relay needs a writable device backend (e.g. --kind socketcan or an \
NGT-1/iKonvert gateway) to re-send frames; there is no bus to write to \
in stdin/log-only mode"
);
}
}
if config.quirk.contains(&quirks::QuirkKind::Wmm) && device_sender.is_none() {
anyhow::bail!(
"--quirk wmm needs a writable device backend (e.g. --kind socketcan or an \
NGT-1/iKonvert gateway) to emit PGN 127258; there is no bus to write \
to in stdin/log-only mode"
);
}
let frames_rx = match device_sender.clone() {
Some(sender) => super::install_stdin_loopback(
frames_rx,
sender,
pre_coalesced.clone(),
config.protocol,
),
None => frames_rx,
};
if !config.no_request_claims
&& let Some(sender) = device_sender.clone()
{
request_engine::spawn(Arc::clone(&engine), move |dst, pgn| {
let _ = sender.send_frame(request_engine::iso_request_frame(0, dst, pgn));
});
}
let (filter_path, overrides_path) = state_file_paths(
config.config_dir.as_deref(),
config.nmea0183_filter.as_deref(),
);
let nmea_filter = match filter_path {
Some(path) => match crate::n2kd::nmea_filter::NmeaFilter::load(&path) {
Ok(f) => {
log::info!("NMEA 0183 per-device filter loaded from {}", path.display());
Some(Arc::new(Mutex::new(f)))
}
Err(e) => {
log::warn!("NMEA 0183 filter disabled: {e:#}");
None
}
},
None => None,
};
let overrides = match overrides_path {
Some(path) => match crate::n2kd::overrides::OverrideEngine::load(&path) {
Ok(engine) => {
log::info!("PGN-rate overrides loaded from {}", path.display());
Some(Arc::new(Mutex::new(engine)))
}
Err(e) => {
log::warn!("PGN-rate overrides disabled: {e:#}");
None
}
},
None => None,
};
let mcu006_link = config
.quirk
.contains(&quirks::QuirkKind::Mcu006)
.then(quirks::mcu006::Link::default);
Ok(Self {
config,
db,
json_opts,
banner,
frames_rx: Some(frames_rx),
failure: supervisor.as_ref().map(DeviceHandle::failure),
supervisor,
pre_coalesced,
device_sender,
claim_addr,
pgn_list_status,
raw_hub: Arc::new(Hub::new()),
nmea_hub: Arc::new(Hub::new()),
analyzer_hub: Arc::new(Hub::new()),
snapshot,
engine,
nmea_filter,
overrides,
mcu006_link,
node_name,
txt: mdns::Txt::default(),
decoded_tx: None,
serve_stop: Arc::new(AtomicBool::new(false)),
tcp_joins: Vec::new(),
pipeline_join: None,
})
}
pub fn decoded(&mut self) -> mpsc::Receiver<Arc<DecodedPgn>> {
let (tx, rx) = mpsc::channel();
self.decoded_tx = Some(tx);
rx
}
pub fn frame_sender(&self) -> Option<FrameSender> {
self.device_sender.clone()
}
pub fn database(&self) -> &'static PgnDatabase {
self.db
}
pub fn transmit(&self, frame: RawFrame) -> Result<()> {
self.transmitter()
.ok_or_else(|| anyhow!("bridge has no writable device backend to transmit on"))?
.transmit(frame)
}
pub fn transmitter(&self) -> Option<Transmitter> {
self.device_sender.as_ref().map(|sender| Transmitter {
sender: sender.clone(),
claim_addr: self.claim_addr.clone(),
})
}
pub fn encode(
&self,
pgn: u32,
) -> Result<crate::engine::PgnBuilder, crate::engine::EncodeError> {
self.db.encode_by_pgn(pgn)
}
pub fn pgn_list_status(&self) -> Option<&PgnListStatus> {
self.pgn_list_status.as_ref()
}
pub fn claimed_address(&self) -> Option<u8> {
self.claim_addr
.as_ref()
.map(|a| a.load(std::sync::atomic::Ordering::Relaxed))
.filter(|&a| a != crate::engine::ADDR_GLOBAL && a != crate::engine::ADDR_NULL)
}
pub fn serve(&mut self) -> Result<()> {
let config = &self.config;
let stop = || Some(self.serve_stop.clone());
let mut served: Vec<(&'static str, u16)> = Vec::new();
if let Some(store) = self.snapshot.as_ref() {
self.tcp_joins.push(serving_tcp::spawn_snapshot(
config.bind,
config.snapshot_port,
store.core(),
Some(self.banner),
stop(),
)?);
served.push(("snapshot", config.snapshot_port));
}
if config.input_port != 0
&& let Some(sender) = self.device_sender.clone()
{
self.tcp_joins.push(tcp::spawn_input_server(
"input",
config.bind,
config.input_port,
Some(sender),
stop(),
)?);
served.push(("input", config.input_port));
}
if config.raw_port != 0 {
self.tcp_joins.push(serving_tcp::spawn_stream_server(
"raw",
config.bind,
config.raw_port,
self.raw_hub.clone(),
Some(serving_tcp::CANBOAT_FORMAT_FAST_HEADER),
stop(),
)?);
served.push(("raw", config.raw_port));
}
if config.nmea0183_port != 0 {
self.tcp_joins.push(serving_tcp::spawn_stream_server(
"nmea0183",
config.bind,
config.nmea0183_port,
self.nmea_hub.clone(),
None,
stop(),
)?);
served.push(("nmea0183", config.nmea0183_port));
}
if config.analyzer_port != 0 {
self.tcp_joins.push(serving_tcp::spawn_stream_server(
"analyzer",
config.bind,
config.analyzer_port,
self.analyzer_hub.clone(),
Some(self.banner),
stop(),
)?);
served.push(("analyzer", config.analyzer_port));
}
if config.ais_port != 0 {
if let Some(store) = self.snapshot.as_ref() {
self.tcp_joins.push(serving_tcp::spawn_ais_snapshot(
config.bind,
config.ais_port,
store.core(),
stop(),
)?);
served.push(("ais", config.ais_port));
} else {
log::warn!(
"--ais-port {} ignored: snapshot port is disabled, no AIS cache to dump",
config.ais_port,
);
}
}
if let Some(filter) = self.nmea_filter.as_ref()
&& config.nmea0183_filter_port != 0
{
self.tcp_joins.push(tcp::spawn_filter_control_server(
config.bind,
config.nmea0183_filter_port,
filter.clone(),
self.json_opts.clone(),
stop(),
)?);
served.push(("nmea0183-filter", config.nmea0183_filter_port));
}
if let Some(engine) = self.overrides.as_ref()
&& config.overrides_port != 0
{
self.tcp_joins.push(tcp::spawn_overrides_control_server(
config.bind,
config.overrides_port,
engine.clone(),
self.device_sender.clone(),
self.json_opts.clone(),
stop(),
)?);
served.push(("overrides", config.overrides_port));
}
if let (Some(link), Some(sender)) = (self.mcu006_link.as_ref(), self.device_sender.as_ref())
&& config.mcu006_port != 0
{
self.tcp_joins
.push(super::mcu006_control::spawn_mcu006_control_server(
config.bind,
config.mcu006_port,
link.clone(),
sender.clone(),
stop(),
)?);
served.push(("mcu006", config.mcu006_port));
}
if config.mdns {
self.advertise(&served);
}
Ok(())
}
fn advertise(&self, served: &[(&'static str, u16)]) {
#[cfg(feature = "mdns")]
{
let Some(port) = mdns::srv_port(served) else {
log::info!("mDNS: no port is open, nothing to advertise");
return;
};
let identity = self.node_name.map(|name| mdns::Identity {
name,
nmea2000: self.config.protocol == crate::engine::BusProtocol::Nmea2000,
});
let base = mdns::txt_record(identity, served, &self.config.quirk);
if let Err(e) = self.txt.start(self.config.bind, port, base) {
log::warn!("mDNS: not advertising: {e:#}");
}
}
#[cfg(not(feature = "mdns"))]
{
let _ = served;
log::info!("mDNS: not advertising, canboat was built without the `mdns` feature");
}
}
fn build_hubs(&self) -> Hubs {
Hubs {
raw: self.raw_hub.clone(),
nmea: self.nmea_hub.clone(),
analyzer: self.analyzer_hub.clone(),
snapshot: self.snapshot.clone(),
engine: Arc::clone(&self.engine),
quirks: {
let mut q = quirks::Quirks::new(
self.config.quirk.clone(),
self.config.config_dir.as_deref(),
);
if let Some(link) = &self.mcu006_link {
q.set_mcu006_link(link.clone());
}
q.set_txt(self.txt.clone());
q
},
device_sender: self.device_sender.clone(),
claim_addr: self.claim_addr.clone(),
overrides: self.overrides.clone(),
decoded_tx: self.decoded_tx.clone(),
}
}
pub fn run(mut self) -> Result<()> {
let frames_rx = self.take_source()?;
let hubs = self.build_hubs();
let (db, pre_coalesced, json_opts, nmea) = self.pipeline_inputs();
let rx_filter = RxFilter::new(&self.config.rx_filter);
pipeline::run(
db,
frames_rx,
hubs,
pre_coalesced,
json_opts,
nmea,
rx_filter,
);
self.shutdown();
match self.failure() {
Some(why) => Err(anyhow!("{why}")),
None => Ok(()),
}
}
pub fn failure(&self) -> Option<String> {
self.failure.as_ref()?.get().map(str::to_owned)
}
pub fn spawn(&mut self) -> Result<()> {
let frames_rx = self.take_source()?;
let hubs = self.build_hubs();
let (db, pre_coalesced, json_opts, nmea) = self.pipeline_inputs();
let rx_filter = RxFilter::new(&self.config.rx_filter);
let join = thread::Builder::new()
.name("bridge-pipeline".into())
.spawn(move || {
pipeline::run(
db,
frames_rx,
hubs,
pre_coalesced,
json_opts,
nmea,
rx_filter,
)
})
.map_err(|e| anyhow!("spawning the bridge pipeline thread: {e}"))?;
self.pipeline_join = Some(join);
Ok(())
}
pub fn is_running(&self) -> bool {
self.pipeline_join
.as_ref()
.is_some_and(|join| !join.is_finished())
}
pub fn wait(&mut self) {
if let Some(join) = self.pipeline_join.take() {
let _ = join.join();
}
self.shutdown();
}
pub fn shutdown(&mut self) {
self.txt.stop();
if let Some(s) = self.supervisor.take() {
let _ = s.close();
}
self.serve_stop
.store(true, std::sync::atomic::Ordering::Relaxed);
for join in self.tcp_joins.drain(..) {
let _ = join.join();
}
}
fn take_source(&mut self) -> Result<mpsc::Receiver<RawFrame>> {
self.frames_rx
.take()
.ok_or_else(|| anyhow!("bridge pipeline already started"))
}
fn pipeline_inputs(
&self,
) -> (
&'static PgnDatabase,
Arc<AtomicBool>,
JsonOptions,
Nmea0183Options,
) {
(
self.db,
self.pre_coalesced.clone(),
self.json_opts.clone(),
Nmea0183Options {
emit_stdout: self.config.nmea0183_stdout,
rate_limit: !self.config.no_nmea0183_rate_limit,
filter: self.nmea_filter.clone(),
},
)
}
}
#[derive(Clone)]
pub struct Transmitter {
sender: FrameSender,
claim_addr: Option<Arc<AtomicU8>>,
}
impl Transmitter {
pub fn transmit(&self, mut frame: RawFrame) -> Result<()> {
if (frame.src == 0 || frame.src == crate::engine::ADDR_GLOBAL)
&& let Some(addr) = self.claimed_address()
{
frame.src = addr;
}
self.sender
.send_frame(frame)
.map_err(|_| anyhow!("device writer is gone (backend disconnected)"))
}
pub fn claimed_address(&self) -> Option<u8> {
self.claim_addr
.as_ref()
.map(|a| a.load(std::sync::atomic::Ordering::Relaxed))
.filter(|&a| a != crate::engine::ADDR_GLOBAL && a != crate::engine::ADDR_NULL)
}
}
impl Drop for Bridge {
fn drop(&mut self) {
self.shutdown();
}
}
#[cfg(test)]
mod tests {
use super::state_file_paths;
use std::path::{Path, PathBuf};
#[test]
fn quirks_need_an_nmea2000_bus() {
let config = crate::server::BridgeConfig {
protocol: crate::engine::BusProtocol::J1939,
quirk: vec![crate::server::quirks::QuirkKind::Wmm],
..Default::default()
};
let err = super::Bridge::new(config).err().expect("refused");
assert!(err.to_string().contains("--protocol j1939"), "{err}");
}
#[test]
fn no_config_dir_disables_persistence() {
let (filter, overrides) = state_file_paths(None, None);
assert_eq!(filter, None);
assert_eq!(overrides, None);
}
#[test]
fn config_dir_places_both_files() {
let (filter, overrides) = state_file_paths(Some(Path::new("/etc/default/canboat")), None);
assert_eq!(
filter,
Some(PathBuf::from("/etc/default/canboat/nmea0183-filter.json"))
);
assert_eq!(
overrides,
Some(PathBuf::from("/etc/default/canboat/overrides.json"))
);
}
#[test]
fn explicit_filter_wins_and_stands_alone() {
let (filter, overrides) = state_file_paths(None, Some(Path::new("/custom/f.json")));
assert_eq!(filter, Some(PathBuf::from("/custom/f.json")));
assert_eq!(overrides, None);
let (filter, overrides) =
state_file_paths(Some(Path::new("/cfg")), Some(Path::new("/custom/f.json")));
assert_eq!(filter, Some(PathBuf::from("/custom/f.json")));
assert_eq!(overrides, Some(PathBuf::from("/cfg/overrides.json")));
}
}