use std::io::Write as _;
use crate::engine::format::{actisense_ascii, ebl, plain, ydwg02};
use crate::engine::{PgnBuilder, PgnDatabase, PgnInfo, Units};
use anyhow::bail;
#[derive(Debug, clap::Args)]
#[command(disable_help_flag = true)]
pub struct Args {
pgn: Option<String>,
#[arg(value_name = "FIELD=VALUE")]
assignments: Vec<String>,
#[arg(long)]
src: Option<u8>,
#[arg(long)]
dst: Option<u8>,
#[arg(long)]
prio: Option<u8>,
#[arg(long)]
si: bool,
#[arg(long, value_enum, default_value_t = OutFmt::Plain)]
to: OutFmt,
#[arg(long)]
list: bool,
#[arg(short = 'h', long)]
help: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
enum OutFmt {
#[value(name = "plain", alias = "fast")]
Plain,
#[value(name = "actisense", alias = "actisense-ascii")]
Actisense,
#[value(name = "ydwg02", alias = "yden")]
Ydwg02,
#[value(name = "actisense-ebl")]
ActisenseEbl,
}
pub fn run(args: Args) -> anyhow::Result<()> {
let db = PgnDatabase::embedded(if args.si { Units::Si } else { Units::Metric });
if args.list {
list_pgns(db);
return Ok(());
}
let Some(pgn) = args.pgn.as_deref() else {
if args.help {
print_usage();
return Ok(());
}
bail!("specify a PGN (id or number), or --list. See --help.");
};
let mut builder = resolve(db, pgn)?;
if args.help {
describe(builder.pgn_info(), args.si);
return Ok(());
}
if let Some(p) = args.prio {
builder = builder.priority(p);
}
if let Some(s) = args.src {
builder = builder.source(s);
}
if let Some(d) = args.dst {
builder = builder.destination(d);
}
for a in &args.assignments {
let Some((field, value)) = a.split_once('=') else {
bail!("bad assignment '{a}' (expected FIELD=VALUE)");
};
builder.push_arg(field.trim(), value.trim())?;
}
let frame = builder.build()?;
match args.to {
OutFmt::Plain => print_text(|s| plain::write_line(s, &frame)),
OutFmt::Actisense => print_text(|s| actisense_ascii::write_line(s, &frame)),
OutFmt::Ydwg02 => print_text(|s| ydwg02::write_line(s, &frame)),
OutFmt::ActisenseEbl => {
let mut bytes = Vec::new();
ebl::encode_frame(&frame, &mut bytes);
std::io::stdout().write_all(&bytes)?;
}
}
Ok(())
}
fn resolve(db: &'static PgnDatabase, pgn: &str) -> anyhow::Result<PgnBuilder> {
match pgn.parse::<u32>() {
Ok(num) => Ok(db.encode_by_pgn(num)?),
Err(_) => Ok(db.encode(pgn)?),
}
}
fn print_text(f: impl FnOnce(&mut String) -> std::fmt::Result) {
let mut s = String::new();
if f(&mut s).is_ok() {
println!("{}", s.trim_end_matches(['\r', '\n']));
}
}
fn list_pgns(db: &'static PgnDatabase) {
for p in db.pgns() {
println!("{:>6} {:34} {}", p.pgn, p.id, p.description);
}
}
fn describe(pgn: &'static PgnInfo, si: bool) {
println!("PGN {} — {} [{}]", pgn.pgn, pgn.description, pgn.id);
if let Some(e) = pgn.explanation {
println!(" {e}");
}
println!(
"\nFields (set as FIELD=VALUE, by name or id; units are {}):",
if si { "SI" } else { "practical/Metric" }
);
for f in pgn.fields {
let kind = match f.lookup_enumeration {
Some(name) => format!("lookup {name}"),
None => match f.field_type {
Some(ft) => format!("{ft:?}"),
None => "Number".to_string(),
},
};
let unit = f.unit.map(|u| format!(" [{u}]")).unwrap_or_default();
let note = if f.match_value.is_some() {
" (auto: variant selector)"
} else {
""
};
println!(
" {:>2}. {:26} id={:26} {}{}{}",
f.order, f.name, f.id, kind, unit, note
);
}
println!(
"\nA number is a value in the field's unit; raw:N (e.g. raw:0xffff) writes \
the wire bits.\nUnset fields default to \"not available\". Example:"
);
let example = pgn
.fields
.iter()
.find(|f| f.match_value.is_none())
.map(|f| format!("{}=<value>", f.id))
.unwrap_or_default();
println!(" canboat format-message {} {example}", pgn.id);
}
fn print_usage() {
println!(
"Usage: canboat format-message <PGN> [FIELD=VALUE ...] [--src N] [--dst N] \
[--prio N] [--si] [--to FORMAT]\n\
\n\
<PGN> is a schema id (isoRequest) or number (126996).\n\
A VALUE number is in the field's unit; raw:N writes the wire bits.\n\
\n\
canboat format-message --list list all PGNs\n\
canboat format-message <PGN> --help list a PGN's settable fields\n\
\n\
Example:\n\
\x20 canboat format-message isoRequest --dst 0 PGN=126996"
);
}