pub mod actisense_ascii;
pub mod airmar;
pub mod candump;
pub mod chetco;
pub mod common;
pub mod ebl;
pub mod garmin_csv;
pub mod ikonvert;
pub mod maretron_ipg;
pub mod ngt1;
pub mod plain;
pub mod timestamp;
pub mod ydwg02;
pub use common::{iso11783_compose, iso11783_decompose};
pub use ngt1::{encode_n2k_send_frame, encode_startup_ping};
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)]
pub enum InputFormat {
Plain,
PlainMixFast,
ActisenseAscii,
Ydwg02,
Ikonvert,
Airmar,
Chetco,
GarminCsv,
GarminCsv2,
Candump,
}
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),
"CHETCO" => Some(InputFormat::Chetco),
"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"
)
}
pub fn detect(line: &str) -> Option<InputFormat> {
let t = line.trim_start();
if t.is_empty() || t.starts_with('#') {
return None;
}
if t.starts_with("!PDGY,") || t.starts_with("$PDGY,") {
return Some(InputFormat::Ikonvert);
}
if t.starts_with("$PCDIN") {
return Some(InputFormat::Chetco);
}
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) {
return Some(InputFormat::Candump);
}
if t.contains(',') {
return Some(InputFormat::Plain);
}
None
}
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_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(line).map(Some),
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::Chetco => chetco::parse_line(line).map(Some),
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 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_chetco() {
assert_eq!(detect("$PCDIN,01F801,..."), Some(InputFormat::Chetco));
}
#[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);
}
}