use std::fmt;
use std::str::FromStr;
use crate::engine::BusProtocol;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Kind {
Actisense(ActisenseFormat),
Aos,
Ikonvert,
Maretron,
YachtDevices,
Miniplex,
SeaSmart,
Candump,
Plain,
SocketCan,
Canalyst,
UsbCanA,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ActisenseFormat {
NgtActisense,
N2kAscii,
N2kActisense,
RawAscii,
RawActisense,
Mxpgn,
}
impl ActisenseFormat {
pub const ALL: &'static [ActisenseFormat] = &[
ActisenseFormat::NgtActisense,
ActisenseFormat::N2kAscii,
ActisenseFormat::N2kActisense,
ActisenseFormat::RawAscii,
ActisenseFormat::RawActisense,
ActisenseFormat::Mxpgn,
];
pub fn describe(self) -> &'static str {
match self {
ActisenseFormat::NgtActisense => "NGT Actisense: the NGT-1's binary format (BST-93/94)",
ActisenseFormat::N2kAscii => "N2K ASCII: one reassembled NMEA 2000 message per line",
ActisenseFormat::N2kActisense => {
"N2K Actisense (BST-D0): reassembled NMEA 2000 messages, binary"
}
ActisenseFormat::RawAscii => {
"RAW ASCII: one CAN frame per line, as Yacht Devices sends it"
}
ActisenseFormat::RawActisense => "RAW Actisense (BST-95): CAN frames, binary",
ActisenseFormat::Mxpgn => {
"NMEA 0183 with $MXPGN: CAN frames, the data in bus order, as a W2K-2's \
CANDIRECT data server sends them"
}
}
}
pub fn name(self) -> &'static str {
match self {
ActisenseFormat::NgtActisense => "ngt-actisense",
ActisenseFormat::N2kAscii => "n2k-ascii",
ActisenseFormat::N2kActisense => "n2k-actisense",
ActisenseFormat::RawAscii => "raw-ascii",
ActisenseFormat::RawActisense => "raw-actisense",
ActisenseFormat::Mxpgn => "mxpgn",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Transport {
Serial { baud: u32 },
Usb,
Tcp { port: u16 },
Udp { port: Option<u16> },
Interface,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Setting {
Baud,
Bitrate,
ConfigureLink,
Channel,
Password,
Mode,
NoTxRateLimit,
IgnoreChecksum,
RxFilter,
RunNode,
Node,
NoClaim,
}
impl Setting {
pub const ALL: &'static [Setting] = &[
Setting::Baud,
Setting::Bitrate,
Setting::ConfigureLink,
Setting::Channel,
Setting::Password,
Setting::Mode,
Setting::NoTxRateLimit,
Setting::IgnoreChecksum,
Setting::RxFilter,
Setting::RunNode,
Setting::Node,
Setting::NoClaim,
];
pub fn name(self) -> &'static str {
match self {
Setting::Baud => "baud",
Setting::Bitrate => "bitrate",
Setting::ConfigureLink => "configure_link",
Setting::Channel => "channel",
Setting::Password => "password",
Setting::Mode => "mode",
Setting::NoTxRateLimit => "no_tx_rate_limit",
Setting::IgnoreChecksum => "ignore_checksum",
Setting::RxFilter => "rx_filter",
Setting::RunNode => "run_node",
Setting::Node => "node",
Setting::NoClaim => "no_claim",
}
}
pub fn kinds(self) -> impl Iterator<Item = Kind> {
Kind::ALL.iter().copied().filter(move |k| k.takes(self))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ListUse {
Ignored,
Advertised,
Stored,
StoredExclusive,
TransmitEnable,
}
impl ListUse {
pub fn describe(self) -> &'static str {
match self {
ListUse::Ignored => "ignored",
ListUse::Advertised => "advertised by canboat's node",
ListUse::Stored => "stored in the gateway, which advertises it",
ListUse::StoredExclusive => {
"stored in the gateway, which advertises it and sends only these"
}
ListUse::TransmitEnable => {
"the gateway's Transmit PGN Enable list: it sends only these"
}
}
}
pub fn exclusive(self) -> bool {
matches!(self, ListUse::StoredExclusive | ListUse::TransmitEnable)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct PgnListUse {
pub rx: ListUse,
pub tx: ListUse,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Bitrates {
Fixed,
Any,
Only(&'static [u32]),
}
impl Bitrates {
pub fn allows(self, bitrate: u32) -> bool {
match self {
Bitrates::Fixed => false,
Bitrates::Any => bitrate > 0,
Bitrates::Only(rates) => rates.contains(&bitrate),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct Capabilities {
pub raw: bool,
pub raw_sends: bool,
pub sends: bool,
pub rewrites_source: bool,
pub standard_frames: bool,
pub n2k_tx_only: bool,
pub iso_tp_sends: bool,
pub node: bool,
pub echoes: bool,
pub bitrates: Bitrates,
pub protocols: &'static [BusProtocol],
}
const N2K: &[BusProtocol] = &[BusProtocol::Nmea2000];
const EXTENDED: &[BusProtocol] = &[BusProtocol::Nmea2000, BusProtocol::J1939];
const EVERY: &[BusProtocol] = &[
BusProtocol::Nmea2000,
BusProtocol::J1939,
BusProtocol::Quick,
];
const N2K_GATEWAY: Capabilities = Capabilities {
raw: false,
raw_sends: false,
sends: true,
rewrites_source: false,
standard_frames: false,
n2k_tx_only: true,
iso_tp_sends: false,
node: false,
echoes: false,
bitrates: Bitrates::Fixed,
protocols: N2K,
};
const FRAME_GATEWAY: Capabilities = Capabilities {
raw: true,
raw_sends: false,
sends: true,
rewrites_source: true,
standard_frames: false,
n2k_tx_only: false,
iso_tp_sends: false,
node: false,
echoes: false,
bitrates: Bitrates::Fixed,
protocols: EXTENDED,
};
const CONTROLLER: Capabilities = Capabilities {
raw: true,
raw_sends: true,
sends: true,
rewrites_source: false,
standard_frames: true,
n2k_tx_only: false,
iso_tp_sends: true,
node: true,
echoes: false,
bitrates: Bitrates::Any,
protocols: EVERY,
};
impl Kind {
pub const NGT1: Kind = Kind::Actisense(ActisenseFormat::NgtActisense);
pub const ALL: &'static [Kind] = &[
Kind::NGT1,
Kind::Actisense(ActisenseFormat::N2kAscii),
Kind::Actisense(ActisenseFormat::N2kActisense),
Kind::Actisense(ActisenseFormat::RawAscii),
Kind::Actisense(ActisenseFormat::RawActisense),
Kind::Actisense(ActisenseFormat::Mxpgn),
Kind::YachtDevices,
Kind::Miniplex,
Kind::SeaSmart,
Kind::Candump,
Kind::Plain,
Kind::Ikonvert,
Kind::Maretron,
Kind::Aos,
Kind::SocketCan,
Kind::Canalyst,
Kind::UsbCanA,
];
pub const OLD_NAMES: &'static [(&'static str, Kind)] = &[
("ngt1", Kind::NGT1),
("w2k-ascii", Kind::Actisense(ActisenseFormat::N2kAscii)),
("ydwg", Kind::YachtDevices),
];
pub fn name(self) -> &'static str {
match self {
Kind::Actisense(_) => "actisense",
Kind::Aos => "aos",
Kind::Ikonvert => "ikonvert",
Kind::Maretron => "maretron",
Kind::YachtDevices => "yachtd",
Kind::Miniplex => "miniplex",
Kind::SeaSmart => "seasmart",
Kind::Candump => "candump",
Kind::Plain => "plain",
Kind::SocketCan => "socketcan",
Kind::Canalyst => "canalyst",
Kind::UsbCanA => "usb-can-a",
}
}
pub fn data_format(self) -> Option<ActisenseFormat> {
match self {
Kind::Actisense(format) => Some(format),
_ => None,
}
}
pub fn with_format(name: &str, format: Option<ActisenseFormat>) -> Result<Kind, UnknownKind> {
let kind = Kind::by_name(name).ok_or_else(|| UnknownKind(name.to_string()))?;
Ok(match (kind, format) {
(_, None) => kind,
(Kind::Actisense(_), Some(format)) => Kind::Actisense(format),
(Kind::YachtDevices, Some(ActisenseFormat::RawAscii)) => kind,
(_, Some(format)) => {
return Err(UnknownKind(format!("{name}:{}", format.name())));
}
})
}
fn by_name(name: &str) -> Option<Kind> {
Kind::ALL
.iter()
.find(|k| {
k.name() == name
&& k.data_format()
.is_none_or(|f| f == ActisenseFormat::NgtActisense)
})
.copied()
.or_else(|| {
Kind::OLD_NAMES
.iter()
.find(|(old, _)| *old == name)
.map(|&(_, kind)| kind)
})
}
pub fn describe(self) -> &'static str {
match self {
Kind::NGT1 => "Actisense NGT-1",
Kind::Actisense(ActisenseFormat::N2kAscii)
| Kind::Actisense(ActisenseFormat::N2kActisense)
| Kind::Actisense(ActisenseFormat::RawAscii) => "Actisense W2K-1, PRO-NDC-1E2K",
Kind::Actisense(ActisenseFormat::RawActisense) => "Actisense W2K-1, PRO-NDC-1E2K, NGX",
Kind::Actisense(ActisenseFormat::Mxpgn) => "Actisense W2K-2 (CANDIRECT, NMEA 0183)",
Kind::YachtDevices => "Yacht Devices YDWG-02, YDEN-02, YDNR-02, YDNU-02",
Kind::Miniplex => "ShipModul MiniPlex-3 with NMEA 2000",
Kind::SeaSmart => {
"Chetco SeaSmart, Quark-elec QK-A032/A034/A036, Yacht Devices in NMEA 0183 mode"
}
Kind::Candump => "SensESP N2K gateway, other N2K IP gateways in candump",
Kind::Plain => "N2K IP gateways in canboat PLAIN/FAST, a canboat server's raw port",
Kind::Ikonvert => "Digital Yacht iKonvert, NavLink2",
Kind::Maretron => "Maretron IPG100/200",
Kind::Aos => "AOS NMEA 2000/0183 Simulator",
Kind::SocketCan => "Linux SocketCAN interface",
Kind::Canalyst => "CANalyst-II, Waveshare USB-CAN-B",
Kind::UsbCanA => "Waveshare USB-CAN-A",
}
}
pub fn transports(self) -> &'static [Transport] {
use Transport::*;
match self {
Kind::NGT1 | Kind::Aos => &[Serial { baud: 115_200 }, Usb, Tcp { port: 6001 }],
Kind::Actisense(ActisenseFormat::N2kAscii)
| Kind::Actisense(ActisenseFormat::RawAscii) => {
&[Tcp { port: 60002 }, Udp { port: None }]
}
Kind::Actisense(ActisenseFormat::N2kActisense) => &[Tcp { port: 60002 }],
Kind::Actisense(ActisenseFormat::RawActisense) => {
&[Tcp { port: 60002 }, Serial { baud: 115_200 }]
}
Kind::Actisense(ActisenseFormat::Mxpgn) => &[Tcp { port: 20000 }, Udp { port: None }],
Kind::YachtDevices => &[
Tcp { port: 1457 },
Udp { port: None },
Serial { baud: 38_400 },
],
Kind::Miniplex => &[
Tcp { port: 10110 },
Udp { port: None },
Serial { baud: 460_800 },
],
Kind::SeaSmart => &[
Tcp { port: 10001 },
Udp { port: None },
Serial { baud: 38_400 },
],
Kind::Ikonvert => &[
Serial { baud: 230_400 },
Tcp { port: 2000 },
Udp { port: Some(2000) },
],
Kind::Maretron => &[Tcp { port: 6543 }],
Kind::Candump | Kind::Plain => &[Tcp { port: 2599 }],
Kind::SocketCan => &[Interface],
Kind::Canalyst => &[Usb],
Kind::UsbCanA => &[Serial {
baud: crate::engine::codec::usb_can_a::BAUD,
}],
}
}
pub fn default_baud(self) -> Option<u32> {
self.transports().iter().find_map(|t| match *t {
Transport::Serial { baud } => Some(baud),
_ => None,
})
}
pub fn settings(self) -> &'static [Setting] {
use Setting::*;
match self {
Kind::NGT1 => &[Baud, Mode],
Kind::Aos => &[Baud, Node, NoClaim],
Kind::Ikonvert => &[Baud, NoTxRateLimit, RxFilter],
Kind::Maretron => &[Password],
Kind::YachtDevices | Kind::Actisense(ActisenseFormat::RawActisense) => &[Baud],
Kind::Miniplex | Kind::SeaSmart => &[Baud, IgnoreChecksum],
Kind::Actisense(_) | Kind::Plain => &[],
Kind::Candump => &[RunNode, Node, NoClaim],
Kind::SocketCan => &[Bitrate, ConfigureLink, Node, NoClaim],
Kind::Canalyst => &[Bitrate, Channel, Node, NoClaim],
Kind::UsbCanA => &[Baud, Bitrate, Node, NoClaim],
}
}
pub fn takes(self, setting: Setting) -> bool {
self.settings().contains(&setting)
}
pub fn pgn_lists(self) -> PgnListUse {
let (rx, tx) = match self {
Kind::Ikonvert => (ListUse::Stored, ListUse::StoredExclusive),
Kind::NGT1 => (ListUse::Ignored, ListUse::TransmitEnable),
Kind::Aos | Kind::SocketCan | Kind::Canalyst | Kind::UsbCanA | Kind::Candump => {
(ListUse::Advertised, ListUse::Advertised)
}
_ => (ListUse::Ignored, ListUse::Ignored),
};
PgnListUse { rx, tx }
}
pub fn capabilities(self) -> Capabilities {
match self {
Kind::NGT1 => Capabilities {
rewrites_source: true,
..N2K_GATEWAY
},
Kind::Ikonvert | Kind::Maretron => Capabilities {
rewrites_source: true,
..N2K_GATEWAY
},
Kind::SeaSmart => Capabilities {
rewrites_source: true,
..N2K_GATEWAY
},
Kind::Aos => Capabilities {
node: true,
..N2K_GATEWAY
},
Kind::Actisense(ActisenseFormat::N2kAscii)
| Kind::Actisense(ActisenseFormat::N2kActisense)
| Kind::Plain => N2K_GATEWAY,
Kind::Candump => Capabilities {
raw_sends: true,
rewrites_source: false,
standard_frames: true,
node: true,
iso_tp_sends: true,
protocols: EVERY,
..FRAME_GATEWAY
},
Kind::YachtDevices => Capabilities {
echoes: true,
iso_tp_sends: true,
..FRAME_GATEWAY
},
Kind::Miniplex | Kind::Actisense(ActisenseFormat::Mxpgn) => FRAME_GATEWAY,
Kind::Actisense(ActisenseFormat::RawAscii) => Capabilities {
rewrites_source: false,
iso_tp_sends: true,
..FRAME_GATEWAY
},
Kind::Actisense(ActisenseFormat::RawActisense) => Capabilities {
raw_sends: true,
rewrites_source: false,
iso_tp_sends: true,
echoes: true,
..FRAME_GATEWAY
},
Kind::SocketCan => CONTROLLER,
Kind::Canalyst => Capabilities {
bitrates: Bitrates::Only(&crate::engine::codec::canalyst::BITRATES),
..CONTROLLER
},
Kind::UsbCanA => Capabilities {
bitrates: Bitrates::Only(&crate::engine::codec::usb_can_a::RATES),
..CONTROLLER
},
}
}
}
impl fmt::Display for Kind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.data_format() {
Some(format) => write!(f, "{} ({})", self.name(), format.name()),
None => f.write_str(self.name()),
}
}
}
impl FromStr for Kind {
type Err = UnknownKind;
fn from_str(s: &str) -> Result<Kind, UnknownKind> {
let Some((name, format)) = s.split_once(':') else {
return Kind::with_format(s, None);
};
let format = ActisenseFormat::ALL
.iter()
.find(|f| f.name() == format)
.copied()
.ok_or_else(|| UnknownKind(s.to_string()))?;
Kind::with_format(name, Some(format)).map_err(|_| UnknownKind(s.to_string()))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnknownKind(String);
impl fmt::Display for UnknownKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "no such kind of gateway: {}", self.0)
}
}
impl std::error::Error for UnknownKind {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_kind_parses_back_from_its_name() {
for &kind in Kind::ALL {
let spelled = match kind.data_format() {
Some(format) => format!("{}:{}", kind.name(), format.name()),
None => kind.name().to_string(),
};
assert_eq!(spelled.parse::<Kind>(), Ok(kind), "{spelled}");
}
assert_eq!("actisense".parse::<Kind>(), Ok(Kind::NGT1));
for &(old, kind) in Kind::OLD_NAMES {
assert_eq!(old.parse::<Kind>(), Ok(kind), "{old}");
}
assert!("yachtd:raw-ascii".parse::<Kind>().is_ok());
assert!("yachtd:n2k-ascii".parse::<Kind>().is_err());
assert!("miniplex:mxpgn".parse::<Kind>().is_err());
assert!("ngt2".parse::<Kind>().is_err());
}
#[test]
fn a_kind_with_a_serial_port_takes_a_baud_rate() {
for &kind in Kind::ALL {
assert_eq!(
kind.takes(Setting::Baud),
kind.default_baud().is_some(),
"{kind}"
);
}
}
#[test]
fn the_capabilities_hang_together() {
for &kind in Kind::ALL {
let caps = kind.capabilities();
assert!(caps.protocols.contains(&BusProtocol::Nmea2000), "{kind}");
assert_eq!(caps.raw, caps.protocols.len() > 1, "{kind}");
assert_eq!(
caps.standard_frames,
caps.protocols.contains(&BusProtocol::Quick),
"{kind}"
);
assert!(!caps.raw_sends || caps.raw, "{kind}");
assert!(!(caps.raw_sends && caps.rewrites_source), "{kind}");
assert!(!(caps.n2k_tx_only && caps.raw), "{kind}");
assert!(!caps.iso_tp_sends || caps.raw, "{kind}");
assert!(!(caps.node && caps.raw) || caps.iso_tp_sends, "{kind}");
assert_eq!(caps.node, kind.takes(Setting::Node), "{kind}");
assert_eq!(
kind.takes(Setting::Bitrate),
caps.bitrates != Bitrates::Fixed,
"{kind}"
);
assert_eq!(
kind.pgn_lists().rx == ListUse::Advertised,
caps.node,
"{kind}"
);
}
}
#[test]
fn the_bit_rates_are_the_codecs() {
let rates = |kind: Kind| match kind.capabilities().bitrates {
Bitrates::Only(rates) => rates.to_vec(),
other => panic!("{kind}: {other:?}"),
};
assert_eq!(
rates(Kind::UsbCanA),
crate::engine::codec::usb_can_a::bitrates().collect::<Vec<_>>()
);
assert!(rates(Kind::Canalyst).contains(&250_000));
assert!(Kind::SocketCan.capabilities().bitrates.allows(500_000));
assert!(!Kind::YachtDevices.capabilities().bitrates.allows(250_000));
}
}