use std::sync::LazyLock;
use anyhow::{Result, bail};
use crate::engine::BusProtocol;
use crate::engine::format::ngt1::OperatingMode;
use crate::io::device::kind::{ActisenseFormat, Kind, Setting};
use crate::io::device::open::Device;
use super::help;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct KindArg {
name: &'static str,
kind: Kind,
old: bool,
}
static KINDS: LazyLock<Vec<KindArg>> = LazyLock::new(|| {
let mut kinds: Vec<KindArg> = Vec::new();
for &kind in Kind::ALL {
if !kinds.iter().any(|k| k.name == kind.name()) {
kinds.push(KindArg {
name: kind.name(),
kind,
old: false,
});
}
}
kinds.extend(Kind::OLD_NAMES.iter().map(|&(name, kind)| KindArg {
name,
kind,
old: true,
}));
kinds
});
impl clap::ValueEnum for KindArg {
fn value_variants<'a>() -> &'a [Self] {
&KINDS
}
fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
let value = clap::builder::PossibleValue::new(self.name).hide(self.old);
Some(if self.old {
value
} else {
value.help(crate::cli::help::kind_help(self.kind))
})
}
}
impl KindArg {
fn implied_format(self) -> Option<ActisenseFormat> {
match self.kind {
Kind::Actisense(format) if self.old => Some(format),
Kind::YachtDevices if self.old => Some(ActisenseFormat::RawAscii),
_ => None,
}
}
pub fn resolve(self, data_format: Option<DataFormat>) -> Result<Kind> {
let given = data_format.map(|f| f.0);
if let (Some(implied), Some(given)) = (self.implied_format(), given)
&& implied != given
{
bail!(
"--kind {} is --data-format {}, not {}",
self.name,
implied.name(),
given.name()
);
}
let Some(format) = given else {
return Ok(self.kind);
};
match self.kind {
Kind::Actisense(_) => Ok(Kind::Actisense(format)),
Kind::YachtDevices if format == ActisenseFormat::RawAscii => Ok(self.kind),
Kind::YachtDevices => bail!(
"--data-format {}: canboat speaks only raw-ascii to Yacht Devices gateways",
format.name()
),
_ => bail!(
"--data-format {}: --kind {} has no data formats",
format.name(),
self.name
),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DataFormat(pub ActisenseFormat);
static DATA_FORMATS: LazyLock<Vec<DataFormat>> = LazyLock::new(|| {
ActisenseFormat::ALL
.iter()
.copied()
.map(DataFormat)
.collect()
});
impl DataFormat {
pub fn name(self) -> &'static str {
self.0.name()
}
}
impl clap::ValueEnum for DataFormat {
fn value_variants<'a>() -> &'a [Self] {
&DATA_FORMATS
}
fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
Some(clap::builder::PossibleValue::new(self.0.name()).help(self.0.describe()))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
pub enum Ngt1Mode {
RxAll,
Normal,
}
impl From<Ngt1Mode> for OperatingMode {
fn from(mode: Ngt1Mode) -> Self {
match mode {
Ngt1Mode::RxAll => OperatingMode::NgTransferRxAll,
Ngt1Mode::Normal => OperatingMode::NgTransferNormal,
}
}
}
pub const GATEWAY: &str = "Gateway";
pub const SPECIFIC: &str = "Gateway-specific";
pub const LISTS: &str = "PGN lists";
pub const BUS: &str = "Bus";
#[derive(Debug, Clone, clap::Args)]
pub struct DeviceArgs {
#[arg(
long,
value_enum,
value_name = "FORMAT",
help = help::data_format_help(),
help_heading = GATEWAY
)]
pub data_format: Option<DataFormat>,
#[arg(
short = 'b',
long,
short_alias = 's',
value_name = "BAUD",
help = help::baud_help(),
help_heading = GATEWAY
)]
pub baud: Option<u32>,
#[arg(
long,
value_name = "BIT/S",
help = help::bitrate_help(),
long_help = help::bitrate_long_help(),
help_heading = help::can_heading()
)]
pub bitrate: Option<u32>,
#[arg(
long,
value_name = "N",
help = help::only_help(Setting::Channel, "the CAN channel, 0 or 1 [default: 0]."),
help_heading = help::can_heading()
)]
pub channel: Option<u8>,
#[arg(
long,
alias = "maretron-password",
value_name = "PASSWORD",
help = help::only_help(Setting::Password, "the login password [default: empty]."),
help_heading = SPECIFIC
)]
pub password: Option<String>,
#[arg(
long,
value_enum,
help = help::only_help(Setting::Mode, "the operating mode [default: rx-all]."),
long_help = format!(
"{}\n\n`rx-all` (\"NGT Transfer Rx All Mode\") passes on every PGN received; \
`normal` (\"NGT Transfer Normal Mode\") only those on the Receive PGN Enable \
list the gateway holds.",
help::only_help(Setting::Mode, "the operating mode [default: rx-all].")
),
help_heading = SPECIFIC
)]
pub mode: Option<Ngt1Mode>,
#[arg(
long,
visible_alias = "rate-limit-off",
alias = "ikonvert-rate-limit-off",
help = help::only_help(
Setting::NoTxRateLimit,
"turn off the gateway's transmit rate limit, at your own risk."
),
help_heading = SPECIFIC
)]
pub no_tx_rate_limit: bool,
#[arg(
long,
help = help::only_help(
Setting::IgnoreChecksum,
"accept $PCDIN or $MXPGN sentences whose checksum is wrong, as some gateways \
may send them."
),
help_heading = SPECIFIC
)]
pub ignore_checksum: bool,
#[arg(long = "rx-pgn", value_name = "PGN", value_delimiter = ',', help_heading = LISTS)]
pub rx_pgn: Vec<u32>,
#[arg(
long = "rx-filter",
alias = "rx",
alias = "ikonvert-rx",
value_name = "PGN",
value_delimiter = ',',
long_help = help::rx_filter_long_help(),
help_heading = LISTS
)]
pub rx_filter: Vec<u32>,
#[arg(
long = "tx-pgn",
alias = "tx",
alias = "ikonvert-tx",
value_name = "PGN",
value_delimiter = ',',
help = help::tx_pgn_help(),
long_help = help::tx_pgn_long_help(),
help_heading = LISTS
)]
pub tx_pgn: Vec<u32>,
#[arg(short = 'a', long, alias = "socketcan-address", value_name = "ADDR", help_heading = help::node_heading())]
pub address: Option<u8>,
#[arg(
long,
help = help::only_help(
Setting::RunNode,
"run canboat's node on the gateway: claim an address and answer ISO \
Requests; without, frames go out as they are given."
),
help_heading = help::node_heading()
)]
pub node: bool,
#[arg(short = 'n', long, help_heading = help::node_heading())]
pub no_claim: bool,
#[arg(short = 'u', long, value_name = "N", help_heading = help::node_heading())]
pub unique: Option<u32>,
#[arg(short = 'm', long, value_name = "N", help_heading = help::node_heading())]
pub manufacturer: Option<u16>,
#[arg(
long,
value_name = "N",
value_parser = clap::value_parser!(u8).range(0..=15),
help_heading = help::node_heading()
)]
pub system_instance: Option<u8>,
#[arg(
long,
alias = "hb",
value_name = "MS",
value_parser = clap::value_parser!(u64).range(0..=65_532),
help_heading = help::node_heading()
)]
pub heartbeat: Option<u64>,
#[arg(long, help_heading = help::node_heading())]
pub no_learn_tx_pgns: bool,
#[command(flatten, next_help_heading = BUS)]
pub protocol: crate::cli::protocol::ProtocolArgs,
}
impl DeviceArgs {
fn settings_given(&self) -> Vec<(&'static str, Setting)> {
[
("--baud", Setting::Baud, self.baud.is_some()),
("--bitrate", Setting::Bitrate, self.bitrate.is_some()),
("--channel", Setting::Channel, self.channel.is_some()),
("--password", Setting::Password, self.password.is_some()),
("--mode", Setting::Mode, self.mode.is_some()),
(
"--no-tx-rate-limit",
Setting::NoTxRateLimit,
self.no_tx_rate_limit,
),
(
"--ignore-checksum",
Setting::IgnoreChecksum,
self.ignore_checksum,
),
("--node", Setting::RunNode, self.node),
("--address", Setting::Node, self.address.is_some()),
("--no-claim", Setting::NoClaim, self.no_claim),
("--unique", Setting::Node, self.unique.is_some()),
("--manufacturer", Setting::Node, self.manufacturer.is_some()),
(
"--system-instance",
Setting::Node,
self.system_instance.is_some(),
),
("--heartbeat", Setting::Node, self.heartbeat.is_some()),
("--no-learn-tx-pgns", Setting::Node, self.no_learn_tx_pgns),
]
.into_iter()
.filter(|&(_, _, given)| given)
.map(|(flag, setting, _)| (flag, setting))
.collect()
}
pub fn check(&self, kind: Option<Kind>) -> Result<()> {
let protocol = self.protocol.resolve()?;
let Some(kind) = kind else {
if let Some((flag, _)) = self.settings_given().first() {
bail!("{flag}: the source is not a gateway");
}
return Ok(());
};
if !kind.capabilities().protocols.contains(&protocol) {
let carriers = crate::cli::help::kinds_carrying(protocol);
if protocol.standard_frames() {
bail!(
"--protocol {protocol} needs {carriers}: the other gateways pass only 29-bit frames"
);
}
bail!(
"--protocol {protocol} needs a gateway that passes raw CAN frames ({carriers}); \
{kind} does NMEA 2000 framing itself"
);
}
for (flag, setting) in self.settings_given() {
if kind.takes(setting) {
continue;
}
if setting == Setting::Bitrate {
bail!(
"--bitrate sets the CAN bus rate of {}, which canboat drives as the CAN \
controller (--kind {kind} does not have it; for the serial speed to a \
gateway, use --baud)",
crate::cli::help::kinds_taking(Setting::Bitrate)
);
}
if kind == Kind::NGT1 && Kind::Aos.takes(setting) {
bail!("{flag}: --kind {kind} does not have it (an AOS simulator is --kind aos)");
}
bail!("{flag}: --kind {kind} does not have it");
}
if kind.takes(Setting::RunNode)
&& !self.node
&& let Some((flag, _)) = self
.settings_given()
.into_iter()
.find(|&(_, s)| matches!(s, Setting::Node | Setting::NoClaim))
{
bail!("{flag}: --kind {kind} runs canboat's node only with --node");
}
Ok(())
}
pub fn protocol(&self) -> BusProtocol {
self.protocol
.resolve()
.expect("DeviceArgs::check rejects a --protocol this cannot convert")
}
pub fn device(&self, kind: Kind, address: &str) -> Device {
let mut dev = Device::new(kind, address).pgn_lists(self.pgn_lists());
if let Some(baud) = self.baud {
dev = dev.baud(baud);
}
if let Some(bitrate) = self.bitrate {
dev = dev.bitrate(bitrate);
}
if let Some(channel) = self.channel {
dev = dev.channel(channel);
}
if let Some(password) = &self.password {
dev = dev.password(password.clone());
}
if let Some(mode) = self.mode {
dev = dev.mode(mode.into());
}
if self.no_tx_rate_limit {
dev = dev.no_tx_rate_limit();
}
if self.ignore_checksum {
dev = dev.ignore_checksum();
}
if !self.rx_filter.is_empty() && kind.takes(Setting::RxFilter) {
dev = dev.rx_filter(self.rx_filter.clone());
}
if let Some(address) = self.address {
dev = dev.source_address(address);
}
if let Some(unique) = self.unique {
dev = dev.unique(unique);
}
if let Some(manufacturer) = self.manufacturer {
dev = dev.manufacturer(manufacturer);
}
if let Some(instance) = self.system_instance {
dev = dev.system_instance(instance);
}
if let Some(ms) = self.heartbeat {
dev = dev.heartbeat_ms(ms);
}
if self.no_learn_tx_pgns {
dev = dev.learn_tx_pgns(false);
}
if self.no_claim {
dev = dev.no_claim();
}
if self.node {
dev = dev.run_node(true);
}
dev
}
pub fn pgn_lists(&self) -> crate::engine::pgn_list::PgnLists {
crate::engine::pgn_list::PgnLists {
tx: self.tx_pgn.clone(),
rx: self.rx_pgn.clone(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
#[derive(Parser)]
struct Cli {
#[command(flatten)]
device: DeviceArgs,
}
fn parse(args: &[&str]) -> DeviceArgs {
Cli::try_parse_from([&["x"], args].concat()).unwrap().device
}
const YACHTD: Kind = Kind::YachtDevices;
#[test]
fn the_old_spellings_still_parse() {
let d = parse(&[
"--maretron-password",
"pw",
"--ikonvert-rx",
"129025,129026",
"--tx",
"127508",
"--rate-limit-off",
"--socketcan-address",
"7",
]);
assert_eq!(d.password.as_deref(), Some("pw"));
assert_eq!(d.rx_filter, [129025, 129026]);
assert!(d.rx_pgn.is_empty());
assert_eq!(d.tx_pgn, [127508]);
assert!(d.no_tx_rate_limit);
assert_eq!(d.address, Some(7));
}
#[test]
fn an_option_the_gateway_lacks_is_refused() {
assert!(
parse(&["--bitrate", "500000"])
.check(Some(Kind::SocketCan))
.is_ok()
);
let err = parse(&["--bitrate", "500000"])
.check(Some(Kind::NGT1))
.unwrap_err();
assert!(err.to_string().contains("--baud"), "{err}");
assert!(
parse(&["--password", "x"])
.check(Some(Kind::Ikonvert))
.is_err()
);
assert!(
parse(&["--channel", "1"])
.check(Some(Kind::SocketCan))
.is_err()
);
for kind in [Kind::SeaSmart, Kind::Miniplex] {
assert!(parse(&["--ignore-checksum"]).check(Some(kind)).is_ok());
}
assert!(parse(&["--ignore-checksum"]).check(Some(YACHTD)).is_err());
assert!(parse(&["--mode", "normal"]).check(Some(Kind::NGT1)).is_ok());
assert!(parse(&["--mode", "normal"]).check(Some(YACHTD)).is_err());
let err = parse(&["-a", "9"]).check(Some(Kind::NGT1)).unwrap_err();
assert!(err.to_string().contains("--kind aos"), "{err}");
for kind in [Kind::SocketCan, Kind::Canalyst, Kind::UsbCanA, Kind::Aos] {
assert!(
parse(&["-a", "9", "-u", "1", "-n"])
.check(Some(kind))
.is_ok()
);
}
assert!(parse(&["-a", "9"]).check(Some(Kind::Ikonvert)).is_err());
assert!(parse(&["-a", "9"]).check(None).is_err());
for kind in [Kind::Ikonvert, Kind::SocketCan, Kind::NGT1] {
assert!(parse(&["--rx", "129025"]).check(Some(kind)).is_ok());
}
assert!(parse(&["--rx-filter", "129025"]).check(None).is_ok());
assert!(
parse(&["--tx-pgn", "127508"])
.check(Some(Kind::Maretron))
.is_ok()
);
assert!(
parse(&["--baud", "9600"])
.check(Some(Kind::SocketCan))
.is_err()
);
assert!(parse(&["-s", "115200"]).check(Some(Kind::NGT1)).is_ok());
}
#[test]
fn what_check_allows_the_device_takes() {
for &kind in Kind::ALL {
for args in [
&["--baud", "9600"][..],
&["--bitrate", "250000"],
&["--channel", "1"],
&["--password", "x"],
&["--mode", "normal"],
&["--no-tx-rate-limit"],
&["--ignore-checksum"],
&["-a", "9", "-u", "1", "--heartbeat", "0"],
&["-n"],
&["--rx-filter", "129025"],
] {
let d = parse(args);
if d.check(Some(kind)).is_ok() {
use crate::io::device::kind::Transport;
let address = match kind.transports()[0] {
Transport::Serial { .. } => "/dev/ttyUSB0",
Transport::Usb => "usb",
Transport::Interface => "can0",
_ => "tcp://192.0.2.1",
};
let dev = d.device(kind, address);
assert!(dev.check(None).is_ok(), "{kind} {args:?}");
}
}
}
}
#[test]
fn candump_takes_the_node_options_only_with_node() {
assert!(parse(&["-a", "9"]).check(Some(Kind::Candump)).is_err());
assert!(
parse(&["--node", "-a", "9"])
.check(Some(Kind::Candump))
.is_ok()
);
assert!(parse(&["--node"]).check(Some(Kind::SocketCan)).is_err());
let dev = parse(&["--node", "-a", "9"]).device(Kind::Candump, "tcp://sensesp");
assert!(dev.capabilities().node);
assert!(
!parse(&[])
.device(Kind::Candump, "tcp://x")
.capabilities()
.node
);
}
#[test]
fn kind_and_data_format_resolve() {
use clap::ValueEnum;
let kind = |name: &str| KindArg::from_str(name, false).unwrap();
let format = |f: ActisenseFormat| Some(DataFormat(f));
use ActisenseFormat::*;
for (name, given, want) in [
("actisense", None, Kind::NGT1),
("actisense", format(NgtActisense), Kind::NGT1),
("actisense", format(N2kAscii), Kind::Actisense(N2kAscii)),
(
"actisense",
format(N2kActisense),
Kind::Actisense(N2kActisense),
),
("actisense", format(RawAscii), Kind::Actisense(RawAscii)),
(
"actisense",
format(RawActisense),
Kind::Actisense(RawActisense),
),
("actisense", format(Mxpgn), Kind::Actisense(Mxpgn)),
("yachtd", None, YACHTD),
("yachtd", format(RawAscii), YACHTD),
("seasmart", None, Kind::SeaSmart),
("aos", None, Kind::Aos),
("ngt1", None, Kind::NGT1),
("w2k-ascii", format(N2kAscii), Kind::Actisense(N2kAscii)),
("ydwg", None, YACHTD),
] {
assert_eq!(kind(name).resolve(given).unwrap(), want, "{name} {given:?}");
}
assert!(kind("w2k-ascii").resolve(format(RawAscii)).is_err());
assert!(kind("yachtd").resolve(format(N2kAscii)).is_err());
assert!(kind("socketcan").resolve(format(RawAscii)).is_err());
let listed: Vec<_> = KindArg::value_variants()
.iter()
.filter_map(|k| k.to_possible_value())
.filter(|v| !v.is_hide_set())
.map(|v| v.get_name().to_owned())
.collect();
assert_eq!(
listed,
[
"actisense",
"yachtd",
"miniplex",
"seasmart",
"candump",
"plain",
"ikonvert",
"maretron",
"aos",
"socketcan",
"canalyst",
"usb-can-a",
]
);
}
#[test]
fn only_raw_frame_gateways_carry_j1939_and_quick() {
let j1939 = parse(&["--protocol", "j1939"]);
assert!(j1939.check(Some(YACHTD)).is_ok());
assert!(
j1939
.check(Some(Kind::Actisense(ActisenseFormat::Mxpgn)))
.is_ok()
);
let err = j1939.check(Some(Kind::NGT1)).unwrap_err().to_string();
assert!(err.contains("actisense (mxpgn)"), "{err}");
let quick = parse(&["--protocol", "quick"]);
assert!(quick.check(Some(Kind::Canalyst)).is_ok());
assert!(quick.check(Some(Kind::UsbCanA)).is_ok());
assert!(j1939.check(Some(Kind::UsbCanA)).is_ok());
assert!(
quick
.check(Some(Kind::Actisense(ActisenseFormat::RawActisense)))
.is_err()
);
}
}