use crate::engine::analyzer_json as json;
use crate::engine::db::PgnDatabase;
use crate::engine::decode::{DecodedField, DecodedPgn, FieldValue, build_index_by_order};
use crate::engine::output;
use crate::engine::types::{FieldInfo, FieldType};
pub fn json_to_decoded(line: &str, db: &PgnDatabase) -> Option<DecodedPgn> {
let camel_id = json::camel_wrapper_id(line);
let pgn = json::int(line, "pgn")? as u32;
let info = match camel_id {
Some(id) => db.pgn_by_id(id).or_else(|| db.first_pgn(pgn))?,
None => db.first_pgn(pgn)?,
};
let use_camel = camel_id.is_some() || json::uses_camel_keys(line, info.fields);
let src = json::int(line, "src").unwrap_or(0) as u8;
let dst = json::int(line, "dst").unwrap_or(255) as u8;
let prio = json::int(line, "prio").unwrap_or(0) as u8;
let timestamp = json::value(line, "timestamp").map(str::to_owned);
let mut fields = Vec::with_capacity(info.fields.len());
for fi in info.fields {
if let Some(value) = field_value_from_json(line, fi, db, use_camel) {
fields.push(DecodedField {
info: fi,
value,
bit_offset: None,
bit_length: None,
repeat_index: None,
repeat_set: 0,
overrides: None,
});
}
}
let index_by_order = build_index_by_order(&fields);
Some(DecodedPgn {
timestamp,
prio,
pgn,
src,
dst,
description: info.description,
id: info.id,
id_is_pinned: info.id_is_pinned,
data: Vec::new(),
fields,
has_repeating_set: [false, false],
index_by_order,
})
}
fn field_value_from_json(
line: &str,
fi: &FieldInfo,
db: &PgnDatabase,
use_camel: bool,
) -> Option<FieldValue> {
use FieldType::*;
let name = if use_camel { fi.id } else { fi.name };
match fi.field_type {
Some(Lookup | BitLookup | IndirectLookup | DynamicFieldKey | FieldIndex) => {
let value = match json::lookup_int(line, name) {
Some(v) => v as u64,
None => resolve_lookup_name(db, fi, json::value(line, name)?)?,
};
Some(FieldValue::Lookup { value, name: None })
}
Some(Date) => Some(FieldValue::Date(nested_or_bare_int(line, name)? as u16)),
Some(Time | Duration) => {
let v = json::value(line, name)?;
let seconds = if v.starts_with('{') {
json::value(v, "name")
.and_then(output::parse_time)
.or_else(|| json::number(v, "value"))?
} else {
output::parse_time(v)?
};
let res = fi.resolution.unwrap_or(1.0);
let raw = (seconds / res).round() as i64;
Some(FieldValue::Time { raw, seconds })
}
Some(Mmsi) => Some(FieldValue::Mmsi(mmsi_from_json(line, name)?)),
Some(Pgn) => Some(FieldValue::Pgn {
value: nested_or_bare_int(line, name)? as u32,
description: None,
}),
Some(StringFix | StringLz | StringLau | Variable) => {
Some(FieldValue::String(json::value(line, name)?.to_owned()))
}
Some(Float) => Some(FieldValue::Float(json::number(line, name)?)),
Some(Number | Decimal | Address) | None => {
if is_integer_field(fi) {
Some(FieldValue::Integer(json::int(line, name)?))
} else {
Some(FieldValue::Number(json::number(line, name)?))
}
}
_ => None,
}
}
fn resolve_lookup_name(db: &PgnDatabase, fi: &FieldInfo, name: &str) -> Option<u64> {
let table = fi
.lookup_enumeration
.or(fi.lookup_bit_enumeration)
.or(fi.lookup_indirect_enumeration)?;
db.lookup(table)?
.values
.iter()
.find(|v| v.name == name)
.map(|v| v.value)
}
fn is_integer_field(fi: &FieldInfo) -> bool {
fi.resolution.is_none_or(|r| r == 1.0) && fi.unit.is_none()
}
fn nested_or_bare_int(line: &str, field: &str) -> Option<i64> {
json::lookup_int(line, field).or_else(|| json::int(line, field))
}
fn mmsi_from_json(line: &str, field: &str) -> Option<u32> {
if let Some(v) = json::lookup_int(line, field) {
return u32::try_from(v).ok();
}
json::value(line, field)?.trim().parse().ok()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engine::Units;
fn field_f64(d: &DecodedPgn, id: &str) -> Option<f64> {
d.fields
.iter()
.find(|f| f.info.id == id)
.and_then(|f| f.value.as_f64())
}
#[test]
fn camel_and_bare_decode_the_same_record() {
let db = crate::engine::PgnDatabase::embedded(Units::Metric);
let bare = r#"{"prio":2,"src":7,"dst":255,"pgn":130306,"description":"Wind Data","fields":{"Wind Speed":5.0,"Wind Angle":1.2,"Reference":{"value":2,"name":"Apparent"}}}"#;
let camel = r#"{"windData":{"prio":2,"src":7,"dst":255,"pgn":130306,"description":"windData","fields":{"windSpeed":5.0,"windAngle":1.2,"reference":{"value":2,"name":"Apparent"}}}}"#;
let b = json_to_decoded(bare, db).expect("bare decodes");
let c = json_to_decoded(camel, db).expect("camel decodes");
assert_eq!(b.pgn, 130306);
assert_eq!(c.pgn, 130306);
assert_eq!(c.id, "windData");
assert_eq!(c.src, 7);
assert_eq!(field_f64(&b, "windSpeed"), Some(5.0));
assert_eq!(field_f64(&c, "windSpeed"), Some(5.0));
assert_eq!(field_f64(&b, "windAngle"), Some(1.2));
assert_eq!(field_f64(&c, "windAngle"), Some(1.2));
assert_eq!(field_f64(&b, "reference"), field_f64(&c, "reference"));
}
#[test]
fn time_reads_from_seconds_or_any_clock_form() {
let db = crate::engine::PgnDatabase::embedded(Units::Si);
for time in [
"33020.2240",
r#"{"value":33020.2240,"name":"09:10:20.2240"}"#,
r#""09:10:20.2240""#,
r#"{"value":330202240,"name":"09:10:20.2240"}"#,
] {
let line = format!(
r#"{{"prio":3,"src":1,"dst":255,"pgn":126992,"description":"System Time","fields":{{"Source":{{"value":0,"name":"GPS"}},"Date":{{"value":19934,"name":"2024.07.30"}},"Time":{time}}}}}"#
);
let d = json_to_decoded(&line, db).expect("decodes");
let t = d.fields.iter().find(|f| f.info.id == "time").expect("time");
match t.value {
FieldValue::Time { raw, seconds } => {
assert_eq!(raw, 330202240, "{time}");
assert!((seconds - 33020.224).abs() < 1e-6, "{time}");
}
ref other => panic!("{time}: {other:?}"),
}
}
}
#[test]
fn camel_wrapper_detection() {
assert_eq!(
json::camel_wrapper_id(r#"{"prio":2,"src":7,"pgn":130306,"fields":{}}"#),
None
);
assert_eq!(
json::camel_wrapper_id(r#"{"timestamp":"x","pgn":130306}"#),
None
);
assert_eq!(
json::camel_wrapper_id(r#"{"windData":{"pgn":130306}}"#),
Some("windData")
);
}
#[test]
fn flat_camel_record_decodes() {
let db = crate::engine::PgnDatabase::embedded(Units::Si);
let line = r#"{"prio":2,"src":7,"dst":255,"pgn":130306,"description":"Wind Data","fields":{"windSpeed":5.0,"windAngle":1.2,"reference":{"value":2,"name":"Apparent"}}}"#;
let d = json_to_decoded(line, db).expect("flat camel decodes");
assert_eq!(d.pgn, 130306);
assert_eq!(field_f64(&d, "windSpeed"), Some(5.0));
assert_eq!(field_f64(&d, "windAngle"), Some(1.2));
let reference = d
.fields
.iter()
.find(|f| f.info.id == "reference")
.expect("reference field");
assert!(
matches!(reference.value, FieldValue::Lookup { value: 2, .. }),
"got: {:?}",
reference.value
);
}
#[test]
fn key_style_sniffing() {
let info = crate::engine::PgnDatabase::embedded(Units::Si)
.first_pgn(130306)
.expect("PGN 130306 present");
let camel = r#"{"pgn":130306,"fields":{"windSpeed":5.0,"windAngle":1.2}}"#;
let bare = r#"{"pgn":130306,"fields":{"Wind Speed":5.0,"Wind Angle":1.2}}"#;
assert!(json::uses_camel_keys(camel, info.fields));
assert!(!json::uses_camel_keys(bare, info.fields));
assert!(!json::uses_camel_keys(r#"{"pgn":130306}"#, info.fields));
}
#[test]
fn unknown_camel_id_falls_back_to_pgn_number() {
let db = crate::engine::PgnDatabase::embedded(Units::Metric);
let line = r#"{"notARealId":{"prio":2,"src":7,"pgn":130306,"description":"notARealId","fields":{"windAngle":1.2}}}"#;
let d = json_to_decoded(line, db).expect("falls back to pgn 130306");
assert_eq!(d.pgn, 130306);
assert_eq!(d.id, "windData");
}
}