pub mod actisense_ascii;
pub mod airmar;
pub mod aos;
pub mod bst;
pub mod bst_d0;
pub mod candump;
pub mod common;
pub mod ebl;
pub mod garmin_csv;
pub mod ikonvert;
pub mod maretron_ipg;
pub mod mxpgn;
pub mod ngt1;
pub mod plain;
pub mod seasmart;
pub mod timestamp;
pub mod ydwg02;
pub use common::{iso11783_compose, iso11783_decompose};
pub use plain::{ParseError as PlainError, parse_line as parse_plain, write_line as write_plain};
pub use timestamp::{days_since_epoch, days_to_ymd, normalize_timestamp};
use crate::engine::frame::RawFrame;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum InputFormat {
Plain,
PlainMixFast,
ActisenseAscii,
Ydwg02,
Ikonvert,
Airmar,
SeaSmart,
Mxpgn,
ActisenseMxpgn,
GarminCsv,
GarminCsv2,
Candump,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Framing {
Single,
Coalesced,
Unknown,
}
impl InputFormat {
pub fn framing(self) -> Framing {
match self {
InputFormat::Ydwg02
| InputFormat::Mxpgn
| InputFormat::ActisenseMxpgn
| InputFormat::Candump => Framing::Single,
InputFormat::ActisenseAscii
| InputFormat::Ikonvert
| InputFormat::Airmar
| InputFormat::SeaSmart
| InputFormat::GarminCsv
| InputFormat::GarminCsv2 => Framing::Coalesced,
InputFormat::Plain | InputFormat::PlainMixFast => Framing::Unknown,
}
}
}
pub fn parse_format_header(line: &str) -> Option<InputFormat> {
let rest = line.strip_prefix("# format=")?;
let name = rest.trim().to_ascii_uppercase();
match name.as_str() {
"PLAIN" | "PLAIN_OR_FAST" => Some(InputFormat::Plain),
"PLAIN_MIX_FAST" => Some(InputFormat::PlainMixFast),
"FAST" => Some(InputFormat::Plain),
"ACTISENSE" | "ACTISENSE_ASCII" => Some(InputFormat::ActisenseAscii),
"YDWG02" => Some(InputFormat::Ydwg02),
"IKONVERT" => Some(InputFormat::Ikonvert),
"AIRMAR" => Some(InputFormat::Airmar),
"SEASMART" | "CHETCO" => Some(InputFormat::SeaSmart),
"MXPGN" => Some(InputFormat::Mxpgn),
"ACTISENSE_MXPGN" => Some(InputFormat::ActisenseMxpgn),
"GARMIN_CSV1" | "GARMIN" => Some(InputFormat::GarminCsv),
"GARMIN_CSV2" => Some(InputFormat::GarminCsv2),
"CANDUMP" => Some(InputFormat::Candump),
_ => None,
}
}
pub fn header_implies_coalesced(line: &str) -> bool {
let Some(rest) = line.strip_prefix("# format=") else {
return false;
};
let name = rest.trim().to_ascii_uppercase();
!matches!(
name.as_str(),
"PLAIN" | "PLAIN_OR_FAST" | "PLAIN_MIX_FAST" | "YDWG02" | "CANDUMP" | "MXPGN"
)
}
pub fn detect(line: &str) -> Option<InputFormat> {
let t = line.trim_start();
if t.is_empty() || t.starts_with('#') {
return None;
}
if mxpgn::is_mxpgn(t) || mxpgn::is_psmd(t) {
return Some(InputFormat::Mxpgn);
}
if t.starts_with("!PDGY,") || t.starts_with("$PDGY,") {
return Some(InputFormat::Ikonvert);
}
if seasmart::is_pcdin(t) {
return Some(InputFormat::SeaSmart);
}
if t.starts_with("Sequence #,Timestamp,PGN,") {
return Some(InputFormat::GarminCsv);
}
if t.starts_with("Sequence #,Month_Day_Year_Hours_Minutes_Seconds_msTicks,") {
return Some(InputFormat::GarminCsv2);
}
if t.starts_with('A') && t.as_bytes().get(1).is_some_and(u8::is_ascii_digit) {
return Some(InputFormat::ActisenseAscii);
}
if looks_like_ydwg02(t) {
return Some(InputFormat::Ydwg02);
}
if looks_like_airmar(t) {
return Some(InputFormat::Airmar);
}
if candump::looks_like_pretty(t)
|| candump::looks_like_log(t)
|| candump::looks_like_angstrom(t)
|| candump::is_banner(t)
{
return Some(InputFormat::Candump);
}
if mxpgn::is_nmea0183(t) {
return None;
}
if t.contains(',') {
return Some(InputFormat::Plain);
}
None
}
pub fn is_undecided(line: &str) -> bool {
detect(line).is_none() && mxpgn::is_nmea0183(line)
}
fn looks_like_airmar(line: &str) -> bool {
let bytes = line.as_bytes();
let Some(space) = bytes.iter().position(|&c| c == b' ') else {
return false;
};
matches!(bytes.get(space + 1), Some(&b'-')) || matches!(bytes.get(space + 2), Some(&b'-'))
}
fn looks_like_ydwg02(line: &str) -> bool {
let bytes = line.as_bytes();
if bytes.len() < 13 {
return false;
}
let stamp = bytes[0].is_ascii_digit()
&& bytes[1].is_ascii_digit()
&& bytes[2] == b':'
&& bytes[3].is_ascii_digit()
&& bytes[4].is_ascii_digit()
&& bytes[5] == b':'
&& bytes[6].is_ascii_digit()
&& bytes[7].is_ascii_digit()
&& bytes[8] == b'.';
if !stamp {
return false;
}
let mut i = 9;
while bytes.get(i).is_some_and(u8::is_ascii_digit) {
i += 1;
}
if bytes.get(i) != Some(&b' ') {
return false;
}
let dir = bytes.get(i + 1);
if dir != Some(&b'R') && dir != Some(&b'T') {
return false;
}
if bytes.get(i + 2) != Some(&b' ') {
return false;
}
bytes.get(i + 3).is_some_and(|c| c.is_ascii_hexdigit())
&& bytes.get(i + 4).is_some_and(u8::is_ascii_hexdigit)
}
pub fn parse_for(
format: InputFormat,
protocol: crate::engine::BusProtocol,
line: &str,
) -> Result<Option<RawFrame>, plain::ParseError> {
parse_checked(format, protocol, line, true)
}
pub(crate) fn parse_checked(
format: InputFormat,
protocol: crate::engine::BusProtocol,
line: &str,
checksum: bool,
) -> Result<Option<RawFrame>, plain::ParseError> {
let standard = protocol.standard_frames();
match format {
InputFormat::Candump => candump::parse_line_for(line, standard),
InputFormat::Plain | InputFormat::PlainMixFast => parse_with(format, line),
_ if standard => Ok(None),
InputFormat::SeaSmart => seasmart::parse_sentence(line, checksum),
InputFormat::Mxpgn => mxpgn::parse_sentence(line, checksum),
InputFormat::ActisenseMxpgn => mxpgn::parse_ordered(line, checksum, mxpgn::DataOrder::Bus),
_ => parse_with(format, line),
}
}
pub fn parse_with(format: InputFormat, line: &str) -> Result<Option<RawFrame>, plain::ParseError> {
match format {
InputFormat::Plain | InputFormat::PlainMixFast => plain::parse_line(line).map(Some),
InputFormat::ActisenseAscii => actisense_ascii::parse_line(line).map(Some),
InputFormat::Ydwg02 => ydwg02::parse_line(line).map(Some),
InputFormat::Candump => candump::parse_line_for(line, false),
InputFormat::Ikonvert => match ikonvert::parse_line(line)? {
ikonvert::IkonvertLine::Frame(f) => Ok(Some(f)),
_ => Ok(None),
},
InputFormat::Airmar => airmar::parse_line(line).map(Some),
InputFormat::SeaSmart => seasmart::parse_line(line),
InputFormat::Mxpgn => mxpgn::parse_line(line),
InputFormat::ActisenseMxpgn => mxpgn::parse_ordered(line, true, mxpgn::DataOrder::Bus),
InputFormat::GarminCsv | InputFormat::GarminCsv2 => {
let variant = if format == InputFormat::GarminCsv2 {
garmin_csv::Variant::Absolute
} else {
garmin_csv::Variant::Relative
};
match garmin_csv::parse_line(line, variant)? {
garmin_csv::GarminCsvLine::Frame(f) => Ok(Some(f)),
garmin_csv::GarminCsvLine::Header => Ok(None),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn framing_follows_the_analyzer() {
for f in [
InputFormat::Ydwg02,
InputFormat::Mxpgn,
InputFormat::ActisenseMxpgn,
InputFormat::Candump,
] {
assert_eq!(f.framing(), Framing::Single, "{f:?}");
}
for f in [
InputFormat::ActisenseAscii,
InputFormat::Ikonvert,
InputFormat::Airmar,
InputFormat::SeaSmart,
InputFormat::GarminCsv,
InputFormat::GarminCsv2,
] {
assert_eq!(f.framing(), Framing::Coalesced, "{f:?}");
}
for f in [InputFormat::Plain, InputFormat::PlainMixFast] {
assert_eq!(f.framing(), Framing::Unknown, "{f:?}");
}
}
#[test]
fn detect_plain() {
assert_eq!(
detect("2022-09-10T12:10:16.614Z,6,60928,5,255,8,fb,9b,70,22,00,9b,50,c0"),
Some(InputFormat::Plain)
);
}
#[test]
fn detect_actisense_ascii() {
assert_eq!(
detect("A173321.107 23FF7 1F119 01 02"),
Some(InputFormat::ActisenseAscii)
);
}
#[test]
fn detect_ydwg02() {
assert_eq!(
detect("00:17:55.475 R 0DF50B23 FF FF"),
Some(InputFormat::Ydwg02)
);
}
#[test]
fn detect_ikonvert() {
assert_eq!(
detect("!PDGY,127257,3,35,255,12.345,AQID"),
Some(InputFormat::Ikonvert)
);
}
#[test]
fn detect_mxpgn() {
assert_eq!(
detect("$MXPGN,01F801,2801,C1308AC40C5DE343*19"),
Some(InputFormat::Mxpgn)
);
assert_eq!(
parse_format_header("# format=MXPGN"),
Some(InputFormat::Mxpgn)
);
assert!(!header_implies_coalesced("# format=MXPGN"));
let rmc = "$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A";
assert_eq!(detect(rmc), None);
assert!(is_undecided(rmc));
assert!(!is_undecided("$PCDIN,01F801,000C7E1B,0F,FFFF*6E"));
}
#[test]
fn detect_seasmart() {
assert_eq!(detect("$PCDIN,01F801,..."), Some(InputFormat::SeaSmart));
}
#[test]
fn chetco_is_the_old_name_of_seasmart() {
for header in ["# format=SEASMART", "# format=CHETCO", "# format=chetco"] {
assert_eq!(parse_format_header(header), Some(InputFormat::SeaSmart));
}
}
#[test]
fn detect_ydwg02_needs_its_direction_token() {
assert_eq!(
detect("21:55:35.425 R 15FD0723 FF C0 D9 6F FF 7F FF FF"),
Some(InputFormat::Ydwg02)
);
assert_eq!(
detect("00:54:47.929 T 18EAFF44 14 F0 01"),
Some(InputFormat::Ydwg02)
);
assert_eq!(
detect("08:15:07.204,6,60928,64,255,8,4F,28,A9,59,00,89,32,C0"),
Some(InputFormat::Plain)
);
}
#[test]
fn detect_blank_and_comment_ignored() {
assert_eq!(detect(""), None);
assert_eq!(detect("# a comment"), None);
}
}