1use std::io::BufRead;
40
41use crate::engine::source::FrameSource;
42use crate::engine::types::{FieldInfo, FieldType};
43use crate::engine::{BusProtocol, EncodeValue, PgnBuilder, PgnDatabase, RawFrame};
44use anyhow::{Context, Result, anyhow, bail};
45use serde_json::{Map, Value};
46
47const BARE_TOP_LEVEL: [&str; 7] = [
51 "timestamp",
52 "prio",
53 "src",
54 "dst",
55 "pgn",
56 "description",
57 "fields",
58];
59
60pub struct JsonFrameReader<R> {
80 src: R,
81 db: &'static PgnDatabase,
82 buf: String,
83 line_no: usize,
84}
85
86impl<R: BufRead> JsonFrameReader<R> {
87 pub fn new(src: R, db: &'static PgnDatabase) -> Self {
91 Self {
92 src,
93 db,
94 buf: String::with_capacity(512),
95 line_no: 0,
96 }
97 }
98}
99
100#[derive(Debug, PartialEq, Eq)]
102struct Banner {
103 units: Option<crate::engine::Units>,
106 protocol: std::result::Result<BusProtocol, String>,
109}
110
111fn banner(line: &str) -> Option<Banner> {
113 if !line.starts_with("{\"version\"") {
114 return None;
115 }
116 let root: Value = serde_json::from_str(line).ok()?;
117 let root = root.as_object()?;
118 let units = match root.get("units").and_then(Value::as_str) {
119 Some("si") => Some(crate::engine::Units::Si),
120 Some("std") => Some(crate::engine::Units::Metric),
121 _ => None,
122 };
123 let protocol = match root.get("protocol").and_then(Value::as_str) {
124 None => Ok(BusProtocol::Nmea2000),
125 Some(p) => p.parse::<BusProtocol>().map_err(|_| p.to_string()),
126 };
127 Some(Banner { units, protocol })
128}
129
130impl<R: BufRead> FrameSource for JsonFrameReader<R> {
131 fn read_frame(&mut self) -> std::io::Result<Option<RawFrame>> {
132 loop {
133 self.buf.clear();
134 if self.src.read_line(&mut self.buf)? == 0 {
135 return Ok(None);
136 }
137 self.line_no += 1;
138 let line = self.buf.trim();
139 if line.is_empty() {
140 continue;
141 }
142 if let Some(banner) = banner(line) {
146 let ours = self.db.protocol();
147 match banner.protocol {
148 Ok(theirs) if theirs == ours => {}
149 Ok(theirs) => {
150 return Err(std::io::Error::new(
151 std::io::ErrorKind::InvalidData,
152 format!(
153 "line {}: the input was decoded as {theirs}, but is read as \
154 {ours}; pass --protocol {theirs}",
155 self.line_no
156 ),
157 ));
158 }
159 Err(theirs) => {
160 return Err(std::io::Error::new(
161 std::io::ErrorKind::InvalidData,
162 format!(
163 "line {}: the input was decoded as protocol '{theirs}', \
164 which this version cannot read",
165 self.line_no
166 ),
167 ));
168 }
169 }
170 if let Some(units) = banner.units
171 && units != self.db.units()
172 {
173 log::info!(
174 "input declares {} units; reading values against that schema",
175 if units == crate::engine::Units::Si {
176 "SI (rad/K/Pa)"
177 } else {
178 "Metric (deg/°C/bar)"
179 }
180 );
181 self.db = ours.database(units);
182 }
183 continue;
184 }
185 match frame_from_json(self.db, line) {
186 Ok(Some(frame)) => return Ok(Some(frame)),
187 Ok(None) => continue,
188 Err(e) => {
189 log::warn!("line {}: {e:#}", self.line_no);
190 continue;
191 }
192 }
193 }
194 }
195}
196
197pub fn frame_from_json(db: &'static PgnDatabase, line: &str) -> Result<Option<RawFrame>> {
200 let root: Value = serde_json::from_str(line).context("parsing JSON")?;
201 let root = root
202 .as_object()
203 .ok_or_else(|| anyhow!("not a JSON object"))?;
204
205 let (variant_id, record) = match root.iter().next() {
207 Some((key, Value::Object(inner)))
208 if root.len() == 1 && !BARE_TOP_LEVEL.contains(&key.as_str()) =>
209 {
210 (Some(key.as_str()), inner)
211 }
212 _ => (None, root),
213 };
214
215 let Some(pgn) = record.get("pgn").and_then(Value::as_u64) else {
216 if record.contains_key("version") {
217 return Ok(None); }
219 bail!("record has no 'pgn'");
220 };
221 if pgn >= 0x40000 {
224 return Ok(None);
225 }
226
227 let mut builder = match variant_id {
228 Some(id) => db
229 .encode(id)
230 .or_else(|_| db.encode_by_pgn(pgn as u32))
231 .map_err(|e| anyhow!("{e}"))?,
232 None => {
233 let by_desc = record
238 .get("description")
239 .and_then(Value::as_str)
240 .and_then(|d| {
241 db.pgn_variants(pgn as u32)
242 .find(|p| p.description == d || p.id == d)
243 });
244 let by_match = || variant_by_match_fields(db, pgn as u32, record.get("fields"));
245 match by_desc.or_else(by_match) {
246 Some(info) => db.encode_for(info),
247 None => db.encode_by_pgn(pgn as u32).map_err(|e| {
248 anyhow!("{e}; use the -camel wrapped form to select the exact variant")
249 })?,
250 }
251 }
252 };
253
254 if let Some(p) = record.get("prio").and_then(Value::as_u64) {
255 builder = builder.priority(p as u8);
256 }
257 if let Some(s) = record.get("src").and_then(Value::as_u64) {
258 builder = builder.source(s as u8);
259 }
260 if let Some(d) = record.get("dst").and_then(Value::as_u64) {
261 builder = builder.destination(d as u8);
262 }
263 if let Some(ts) = record.get("timestamp").and_then(Value::as_str) {
264 builder = builder.timestamp(ts);
265 }
266
267 if let Some(Value::Object(fields)) = record.get("fields") {
268 stage_fields(db, &mut builder, fields)?;
269 }
270
271 builder.build().map(Some).map_err(|e| anyhow!("{e}"))
272}
273
274fn stage_fields(
278 db: &'static PgnDatabase,
279 builder: &mut PgnBuilder,
280 fields: &Map<String, Value>,
281) -> Result<()> {
282 for (key, value) in fields {
283 let set = match key.as_str() {
284 "list" => Some(1),
285 "list2" => Some(2),
286 _ => None,
287 };
288 if let Some(set) = set {
289 let Value::Array(instances) = value else {
290 bail!("'{key}' is not an array");
291 };
292 for inst in instances {
293 let Value::Object(inst_fields) = inst else {
294 bail!("'{key}' entry is not an object");
295 };
296 let idx = builder.add_set_instance(set).map_err(|e| anyhow!("{e}"))?;
297 for (fkey, fval) in inst_fields {
298 let Some(info) = find_field(builder, fkey) else {
299 log::warn!("{key}[{idx}]: unknown field '{fkey}' ignored");
300 continue;
301 };
302 if let Some(ev) = to_encode_value(db, info, fval)
303 .with_context(|| format!("{key}[{idx}].{fkey}"))?
304 {
305 builder
306 .push_in_set(set, idx, info.name, ev)
307 .map_err(|e| anyhow!("{key}[{idx}].{fkey}: {e}"))?;
308 }
309 }
310 }
311 continue;
312 }
313 let Some(info) = find_field(builder, key) else {
314 log::warn!("unknown field '{key}' ignored");
315 continue;
316 };
317 if let Some(ev) = to_encode_value(db, info, value).with_context(|| key.clone())? {
318 builder
321 .push_by_name(info.name, ev)
322 .map_err(|e| anyhow!("{key}: {e}"))?;
323 }
324 }
325 Ok(())
330}
331
332fn find_field(builder: &PgnBuilder, key: &str) -> Option<&'static FieldInfo> {
333 builder
334 .pgn_info()
335 .fields
336 .iter()
337 .find(|f| f.id == key || f.name == key)
338}
339
340const LOOKUP_TYPES: [FieldType; 4] = [
341 FieldType::Lookup,
342 FieldType::IndirectLookup,
343 FieldType::BitLookup,
344 FieldType::DynamicFieldKey,
345];
346
347fn is_lookup(f: &FieldInfo) -> bool {
348 f.field_type.is_some_and(|t| LOOKUP_TYPES.contains(&t))
349}
350
351fn to_encode_value(
355 db: &'static PgnDatabase,
356 f: &'static FieldInfo,
357 v: &Value,
358) -> Result<Option<EncodeValue>> {
359 Ok(match v {
360 Value::Null => None,
361 Value::Object(o)
369 if matches!(
370 f.field_type,
371 Some(FieldType::Time) | Some(FieldType::Duration)
372 ) && let Some(seconds) = o
373 .get("name")
374 .and_then(Value::as_str)
375 .and_then(crate::engine::output::parse_time) =>
376 {
377 Some(EncodeValue::Number(seconds))
378 }
379 Value::Object(o) => match o.get("value") {
380 Some(Value::Number(n)) => Some(raw_number(f, n)?),
381 Some(Value::String(s)) => Some(string_value(f, s)?),
382 _ => None,
383 },
384 Value::Number(n) => {
385 if is_lookup(f) {
386 Some(EncodeValue::Int(n.as_i64().ok_or_else(|| {
388 anyhow!("lookup value {n} is not an integer")
389 })?))
390 } else if matches!(f.field_type, Some(FieldType::Binary)) {
391 let raw = n
394 .as_u64()
395 .ok_or_else(|| anyhow!("binary value {n} is not an integer"))?;
396 let width = f.bit_length.map(|bl| bl.div_ceil(8) as usize).unwrap_or(8);
397 Some(EncodeValue::Bytes(
398 raw.to_le_bytes()[..width.min(8)].to_vec(),
399 ))
400 } else {
401 Some(EncodeValue::Number(
403 n.as_f64().ok_or_else(|| anyhow!("bad number {n}"))?,
404 ))
405 }
406 }
407 Value::String(s) => Some(string_value(f, s)?),
408 Value::Array(entries) => {
411 let mut raw: u64 = 0;
412 for e in entries {
413 match e {
414 Value::Object(o) => {
415 let mask = o
416 .get("value")
417 .and_then(Value::as_u64)
418 .ok_or_else(|| anyhow!("bit entry without numeric 'value'"))?;
419 raw |= mask;
420 }
421 Value::String(name) => {
422 raw |= 1u64 << bit_by_name(db, f, name)?;
423 }
424 Value::Number(n) => {
425 raw |= n.as_u64().ok_or_else(|| anyhow!("bad bit mask {n}"))?;
426 }
427 other => bail!("unsupported bit entry {other}"),
428 }
429 }
430 Some(EncodeValue::Int(raw as i64))
431 }
432 Value::Bool(_) => bail!("boolean values are not part of analyzer JSON"),
433 })
434}
435
436fn raw_number(f: &FieldInfo, n: &serde_json::Number) -> Result<EncodeValue> {
445 let physical = matches!(
450 f.field_type,
451 Some(FieldType::Float)
452 | Some(FieldType::Time)
453 | Some(FieldType::Duration)
454 | Some(FieldType::Variable)
455 );
456 let raw = n.as_u64().or_else(|| n.as_i64().map(|i| i as u64));
459 if let Some(raw) = raw.filter(|_| !physical) {
460 Ok(EncodeValue::Raw(raw))
461 } else if let Some(i) = n.as_i64() {
462 Ok(EncodeValue::Int(i)) } else {
464 Ok(EncodeValue::Number(n.as_f64().ok_or_else(|| {
465 anyhow!("field '{}': bad number {n}", f.name)
466 })?))
467 }
468}
469
470fn string_value(f: &'static FieldInfo, s: &str) -> Result<EncodeValue> {
471 match f.field_type {
472 Some(FieldType::StringFix) | Some(FieldType::StringLz) | Some(FieldType::StringLau) => {
473 Ok(EncodeValue::Text(s.to_string()))
474 }
475 Some(FieldType::Binary) | Some(FieldType::DynamicFieldValue) => {
476 Ok(EncodeValue::Bytes(parse_hex(s)?))
477 }
478 Some(FieldType::Variable) => Ok(EncodeValue::Text(s.to_string())),
482 Some(FieldType::Date) => parse_date_days(s)
487 .map(EncodeValue::Number)
488 .ok_or_else(|| anyhow!("field '{}': bad date '{s}'", f.name)),
489 Some(FieldType::Time) | Some(FieldType::Duration) => crate::engine::output::parse_time(s)
490 .map(EncodeValue::Number)
491 .ok_or_else(|| anyhow!("field '{}': bad time '{s}'", f.name)),
492 _ if is_lookup(f) => Ok(match s.parse::<i64>() {
496 Ok(raw) => EncodeValue::Int(raw),
497 Err(_) => EncodeValue::Lookup(s.to_string()),
498 }),
499 _ => s.parse::<f64>().map(EncodeValue::Number).map_err(|_| {
502 anyhow!(
503 "field '{}': cannot re-encode display string '{s}'; use -nv JSON input",
504 f.name
505 )
506 }),
507 }
508}
509
510fn parse_date_days(s: &str) -> Option<f64> {
513 let mut it = s.split('.');
514 let (y, m, d) = (
515 it.next()?.parse::<i32>().ok()?,
516 it.next()?.parse::<u32>().ok()?,
517 it.next()?.parse::<u32>().ok()?,
518 );
519 if it.next().is_some() {
520 return None;
521 }
522 let date = chrono::NaiveDate::from_ymd_opt(y, m, d)?;
523 let epoch = chrono::NaiveDate::from_ymd_opt(1970, 1, 1)?;
524 Some((date - epoch).num_days() as f64)
525}
526
527fn variant_by_match_fields(
535 db: &'static PgnDatabase,
536 pgn: u32,
537 fields: Option<&Value>,
538) -> Option<&'static crate::engine::types::PgnInfo> {
539 let fields = fields?.as_object()?;
540 let mut best: Option<(usize, &'static crate::engine::types::PgnInfo)> = None;
541 let mut tied = false;
542 'variants: for info in db.pgn_variants(pgn) {
543 let mut agreed = 0usize;
544 for f in info.fields {
545 let Some(mv) = f.match_value else { continue };
546 let Some(v) = fields.get(f.id).or_else(|| fields.get(f.name)) else {
547 continue;
548 };
549 match match_field_agrees(db, f, v, mv) {
550 Some(true) => agreed += 1,
551 Some(false) => continue 'variants,
552 None => continue,
553 }
554 }
555 if agreed == 0 {
556 continue;
557 }
558 match &best {
559 Some((n, _)) if *n == agreed => tied = true,
560 Some((n, _)) if *n > agreed => {}
561 _ => {
562 best = Some((agreed, info));
563 tied = false;
564 }
565 }
566 }
567 if tied { None } else { best.map(|(_, i)| i) }
568}
569
570fn match_field_agrees(
573 db: &'static PgnDatabase,
574 f: &'static FieldInfo,
575 v: &Value,
576 mv: i64,
577) -> Option<bool> {
578 match v {
579 Value::Number(n) => Some(n.as_i64()? == mv),
580 Value::String(s) => {
581 let table = f.lookup_enumeration.and_then(|t| db.lookup(t))?;
582 let val = table
583 .values
584 .iter()
585 .find(|lv| lv.name == *s || lv.id == Some(s.as_str()))?
586 .value;
587 Some(val as i64 == mv)
588 }
589 Value::Object(o) => o.get("value").and_then(Value::as_i64).map(|x| x == mv),
590 _ => None,
591 }
592}
593
594fn bit_by_name(db: &'static PgnDatabase, f: &'static FieldInfo, name: &str) -> Result<u8> {
597 let table = f
598 .lookup_bit_enumeration
599 .and_then(|t| db.bit_lookup(t))
600 .ok_or_else(|| anyhow!("field '{}' has no bit lookup table", f.name))?;
601 table
602 .values
603 .iter()
604 .find(|bv| bv.name == name || bv.id == Some(name))
605 .map(|bv| bv.bit)
606 .ok_or_else(|| anyhow!("field '{}': unknown bit '{name}'", f.name))
607}
608
609fn parse_hex(s: &str) -> Result<Vec<u8>> {
612 let compact: String = s.chars().filter(|c| !c.is_whitespace()).collect();
613 if !compact.len().is_multiple_of(2) || compact.is_empty() {
614 bail!("bad hex string '{s}'");
615 }
616 (0..compact.len())
617 .step_by(2)
618 .map(|i| u8::from_str_radix(&compact[i..i + 2], 16).map_err(|e| anyhow!("{e}")))
619 .collect()
620}
621
622#[cfg(test)]
623mod tests {
624 use super::*;
625 use crate::engine::Units;
626
627 fn db() -> &'static PgnDatabase {
628 PgnDatabase::embedded(Units::Metric)
629 }
630
631 #[test]
632 fn si_energy_and_charge_encode_from_joules_and_coulombs() {
633 let si = PgnDatabase::embedded(Units::Si);
636 for (si_line, metric_line) in [
637 (
638 r#"{"pgn":65005,"fields":{"totalEnergyExport":14806800000,"totalEnergyImport":1080000000}}"#,
639 r#"{"pgn":65005,"fields":{"totalEnergyExport":4113,"totalEnergyImport":300}}"#,
640 ),
641 (
642 r#"{"pgn":127506,"fields":{"instance":1,"remainingCapacity":360000}}"#,
643 r#"{"pgn":127506,"fields":{"instance":1,"remainingCapacity":100}}"#,
644 ),
645 ] {
646 let from_si = frame_from_json(si, si_line).unwrap().unwrap();
647 let from_metric = frame_from_json(db(), metric_line).unwrap().unwrap();
648 assert_eq!(from_si.data, from_metric.data, "{si_line}");
649 }
650 let f = frame_from_json(
651 si,
652 r#"{"pgn":65005,"fields":{"totalEnergyExport":14806800000}}"#,
653 )
654 .unwrap()
655 .unwrap();
656 assert_eq!(f.data[..4], [0x11, 0x10, 0x00, 0x00]);
657 }
658
659 #[test]
660 fn si_percentages_encode_from_ratios() {
661 let si = PgnDatabase::embedded(Units::Si);
664 for (si_line, metric_line) in [
665 (
666 r#"{"pgn":127505,"fields":{"instance":0,"type":"Fuel","level":0.97536}}"#,
667 r#"{"pgn":127505,"fields":{"instance":0,"type":"Fuel","level":97.536}}"#,
668 ),
669 (
670 r#"{"pgn":130576,"fields":{"portTrimTab":-0.5,"starboardTrimTab":0.25}}"#,
671 r#"{"pgn":130576,"fields":{"portTrimTab":-50,"starboardTrimTab":25}}"#,
672 ),
673 ] {
674 let from_si = frame_from_json(si, si_line).unwrap().unwrap();
675 let from_metric = frame_from_json(db(), metric_line).unwrap().unwrap();
676 assert_eq!(from_si.data, from_metric.data, "{si_line}");
677 }
678 let f = frame_from_json(
679 si,
680 r#"{"pgn":130576,"fields":{"portTrimTab":-0.5,"starboardTrimTab":0.25}}"#,
681 )
682 .unwrap()
683 .unwrap();
684 assert_eq!(f.data[..2], [0xce, 0x19]);
685 }
686
687 #[test]
688 fn time_encodes_from_seconds_or_any_clock_form() {
689 let want = [0x80u8, 0x7c, 0xae, 0x13];
695 for time in [
696 "33020.224",
697 r#"{"value":33020.2240,"name":"09:10:20.2240"}"#,
698 r#""09:10:20.2240""#,
699 r#""33020.224""#,
700 r#"{"value":330202240,"name":"09:10:20.2240"}"#,
701 ] {
702 let line = format!(
703 r#"{{"pgn":126992,"fields":{{"source":"GPS","date":"2024.07.30","time":{time}}}}}"#
704 );
705 let frame = frame_from_json(db(), &line).unwrap().unwrap();
706 assert_eq!(frame.data[frame.data.len() - 4..], want, "{time}");
707 }
708 }
709
710 #[test]
711 fn negative_duration_clock_encodes_negative() {
712 assert_eq!(
715 crate::engine::output::parse_time("-00:05:00.000"),
716 Some(-300.0)
717 );
718 assert_eq!(crate::engine::output::parse_time("-300"), Some(-300.0));
719 assert_eq!(crate::engine::output::parse_time("01:10:10"), Some(4210.0));
720 assert_eq!(crate::engine::output::parse_time("later"), None);
721 assert_eq!(crate::engine::output::parse_time("--00:05:00"), None);
722 assert_eq!(crate::engine::output::parse_time("00:-05:00"), None);
723 assert_eq!(crate::engine::output::parse_time("inf:00"), None);
724 assert_eq!(crate::engine::output::parse_time("1e308:00"), None);
725 }
726
727 #[test]
728 fn si_volumes_rotations_and_angles_encode_from_si_values() {
729 let si = PgnDatabase::embedded(Units::Si);
733 for (si_line, metric_line) in [
734 (
735 r#"{"pgn":127488,"fields":{"instance":0,"speed":30.0}}"#,
736 r#"{"pgn":127488,"fields":{"instance":0,"speed":1800.0}}"#,
737 ),
738 (
739 r#"{"pgn":127497,"fields":{"instance":0,"tripFuelUsed":0.045,"fuelRateAverage":0.0000029166666666666666}}"#,
740 r#"{"pgn":127497,"fields":{"instance":0,"tripFuelUsed":45.0,"fuelRateAverage":10.5}}"#,
741 ),
742 (
743 r#"{"pgn":130818,"fields":{"manufacturerCode":"Furuno","industryCode":"Marine Industry","headingOffset":0.21816615649929116,"pitchOffset":-0.04363323129985824}}"#,
744 r#"{"pgn":130818,"fields":{"manufacturerCode":"Furuno","industryCode":"Marine Industry","headingOffset":12.5,"pitchOffset":-2.5}}"#,
745 ),
746 ] {
747 let from_si = frame_from_json(si, si_line).unwrap().unwrap();
748 let from_metric = frame_from_json(db(), metric_line).unwrap().unwrap();
749 assert_eq!(from_si.data, from_metric.data, "{si_line}");
750 }
751 }
752
753 #[test]
754 fn debug_float_encodes_from_its_presented_value() {
755 let si = PgnDatabase::embedded(Units::Si);
758 for (db, value) in [(db(), "156.26844443585534"), (si, "2.7273988723754883")] {
759 let line = format!(
760 r#"{{"pgn":126720,"fields":{{"manufacturerCode":"Garmin","industryCode":"Marine Industry","subProtocolId":"Autopilot transport","field":11,"headingToSteer":{{"value":{value},"bytes":"B4 8D 2E 40"}}}}}}"#
761 );
762 let frame = frame_from_json(db, &line).unwrap().unwrap();
763 assert_eq!(
764 frame.data[frame.data.len() - 4..],
765 [0xb4, 0x8d, 0x2e, 0x40],
766 "{value}"
767 );
768 }
769 }
770
771 #[test]
772 fn debug_float_with_a_whole_value_is_still_scaled() {
773 let line = r#"{"pgn":126720,"fields":{"manufacturerCode":"Garmin","industryCode":"Marine Industry","subProtocolId":"Autopilot transport","field":11,"headingToSteer":{"value":180}}}"#;
775 let frame = frame_from_json(db(), line).unwrap().unwrap();
776 let wire = f32::from_le_bytes(frame.data[frame.data.len() - 4..].try_into().unwrap());
777 assert!((wire - std::f32::consts::PI).abs() < 1e-5, "{wire}");
778 }
779
780 #[test]
781 fn camel_envelope_encodes_byte_exact() {
782 let line = r#"{"isoAddressClaim":{"prio":6,"src":3,"dst":255,"pgn":60928,
785 "fields":{"uniqueNumber":1631699,"manufacturerCode":{"value":1580},
786 "deviceInstanceLower":0,"deviceInstanceUpper":0,
787 "deviceFunction":{"value":130,"name":"PC Gateway"},
788 "deviceClass":{"value":25,"name":"Internetwork device"},
789 "systemInstance":0,"industryGroup":{"value":4,"name":"Marine Industry"},
790 "arbitraryAddressCapable":{"value":1,"name":"Yes"}}}}"#
791 .replace('\n', " ");
792 let frame = frame_from_json(db(), &line).unwrap().unwrap();
793 assert_eq!(frame.pgn, 60928);
794 assert_eq!(frame.src, 3);
795 assert_eq!(
796 frame.data.as_slice(),
797 &[0xd3, 0xe5, 0x98, 0xc5, 0x00, 0x82, 0x32, 0xc0]
798 );
799 }
800
801 #[test]
802 fn repeating_list_encodes_instances() {
803 let line = r#"{"gnssSatsInView":{"pgn":129540,"src":3,"dst":255,"prio":6,
804 "fields":{"sid":9,"list":[
805 {"prn":7,"snr":30.0},
806 {"prn":11,"snr":42.5}]}}}"#
807 .replace('\n', " ");
808 let frame = frame_from_json(db(), &line).unwrap().unwrap();
809 let decoded = db().decode(&frame).unwrap();
810 assert!(decoded.has_repeating_set[0]);
811 let prns: Vec<i64> = decoded
812 .fields
813 .iter()
814 .filter(|f| f.info.id == "prn" && f.repeat_set == 1)
815 .map(|f| match &f.value {
816 crate::engine::FieldValue::Integer(n) => *n,
817 crate::engine::FieldValue::Number(x) => *x as i64,
818 other => panic!("prn: {other:?}"),
819 })
820 .collect();
821 assert_eq!(prns, vec![7, 11]);
822 }
823
824 #[test]
825 fn startup_header_and_synthetic_pgns_are_skipped() {
826 assert!(
827 frame_from_json(db(), r#"{"version":"8.0.0","units":"si"}"#)
828 .unwrap()
829 .is_none()
830 );
831 assert!(
832 frame_from_json(db(), r#"{"canboatStartup":{"pgn":262656,"fields":{}}}"#)
833 .unwrap()
834 .is_none()
835 );
836 }
837
838 #[test]
839 fn sub_byte_binary_hex_round_trips() {
840 let line = r#"{"positionReportClassB":{"pgn":129039,"src":43,"dst":255,"prio":2,
843 "fields":{"userId":{"value":"338654321"},"communicationState":"2A 4C 01"}}}"#
844 .replace('\n', " ");
845 let frame = frame_from_json(db(), &line).unwrap().unwrap();
846 let decoded = db().decode(&frame).unwrap();
847 match &decoded.field_by_name("Communication State").unwrap().value {
848 crate::engine::FieldValue::Binary(b) => {
849 assert_eq!(&b[..], &[0x2a, 0x4c, 0x01]);
850 }
851 other => panic!("communicationState: {other:?}"),
852 }
853 }
854
855 #[test]
856 fn bare_record_selects_variant_by_match_fields() {
857 let line = r#"{"pgn":126208,"prio":3,"dst":42,"fields":{
862 "Function Code":"Command","PGN":60928,"priority":8,
863 "numberOfParameters":2,
864 "list":[{"parameter":3,"value":5},{"parameter":4,"value":0}]}}"#
865 .replace('\n', " ");
866 let frame = frame_from_json(db(), &line).unwrap().unwrap();
867 assert_eq!(frame.pgn, 126208);
868 assert_eq!(frame.dst, 42);
869 assert_eq!(
870 frame.data.as_slice(),
871 &[0x01, 0x00, 0xee, 0x00, 0xf8, 0x02, 0x03, 0x05, 0x04, 0x00]
872 );
873 }
874
875 #[test]
876 fn request_names_a_126720_variant_behind_the_catch_all() {
877 let line = r#"{"pgn":126208,"prio":3,"dst":35,"fields":{
885 "functionCode":"Request","pgn":126720,"numberOfParameters":3,
886 "list":[{"parameter":1,"value":135},{"parameter":3,"value":4},
887 {"parameter":4,"value":35}]}}"#
888 .replace('\n', " ");
889 let frame = frame_from_json(db(), &line).unwrap().unwrap();
890 assert_eq!(
891 frame.data.as_slice(),
892 &[
893 0x00, 0x00, 0xef, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x03, 0x01, 0x87, 0x00,
894 0x03, 0x04, 0x04, 0x23
895 ]
896 );
897
898 let decoded = db().decode(&frame).unwrap();
900 let id = decoded
901 .fields
902 .iter()
903 .rfind(|f| f.repeat_set == 1)
904 .expect("parameter 4's value");
905 assert!(
906 matches!(
907 id.value,
908 crate::engine::FieldValue::Lookup { value: 35, .. }
909 ),
910 "got {:?}",
911 id.value
912 );
913 }
914
915 #[test]
916 fn variable_time_target_encodes_from_seconds_or_clock() {
917 let encode = |value: &str| {
921 let line = format!(
922 r#"{{"pgn":126208,"prio":3,"dst":42,"fields":{{"Function Code":"Command","PGN":126992,"priority":8,"numberOfParameters":1,"list":[{{"parameter":5,"value":{value}}}]}}}}"#
923 );
924 frame_from_json(db(), &line).unwrap().unwrap().data.to_vec()
925 };
926 let want = encode("33020.5");
927 for value in [
928 r#""09:10:20.5""#,
929 r#"{"value":33020.5,"name":"09:10:20.5000"}"#,
930 ] {
931 assert_eq!(encode(value), want, "{value}");
932 }
933 assert_eq!(encode("600"), encode("600.0"));
935 assert_eq!(
936 encode(r#"{"value":600,"name":"00:10:00"}"#),
937 encode("600.0")
938 );
939 }
940
941 #[test]
942 fn ambiguous_bare_pgn_is_a_clear_error() {
943 let err = frame_from_json(db(), r#"{"pgn":126208,"fields":{}}"#).unwrap_err();
944 assert!(err.to_string().contains("-camel"), "got: {err:#}");
945 }
946
947 #[test]
948 fn banner_reads_the_producers_declaration() {
949 let si = r#"{"version":"8.0.0","commit":"abc","units":"si","showLookupValues":true}"#;
950 let std = r#"{"version":"7.1.0","units":"std","showLookupValues":true}"#;
951 let j1939 =
952 r#"{"version":"8.4.0","units":"si","protocol":"j1939","showLookupValues":true}"#;
953 assert_eq!(banner(si).unwrap().units, Some(Units::Si));
954 assert_eq!(banner(std).unwrap().units, Some(Units::Metric));
955 assert_eq!(banner(si).unwrap().protocol, Ok(BusProtocol::Nmea2000));
957 assert_eq!(banner(j1939).unwrap().protocol, Ok(BusProtocol::J1939));
958 assert_eq!(
959 banner(r#"{"version":"8.4.0","protocol":"quick"}"#)
960 .unwrap()
961 .protocol,
962 Ok(BusProtocol::Quick)
963 );
964 assert_eq!(
965 banner(r#"{"version":"9.0.0","protocol":"canopen"}"#)
966 .unwrap()
967 .protocol,
968 Err("canopen".to_string())
969 );
970 assert_eq!(banner(r#"{"version":"7.1.0"}"#).unwrap().units, None);
973 assert_eq!(banner(r#"{"pgn":127250,"fields":{}}"#), None);
974 }
975
976 #[test]
979 fn banner_units_keep_the_protocol() {
980 let input = "{\"version\":\"8.4.0\",\"units\":\"std\",\"protocol\":\"j1939\",\"showLookupValues\":true}\n";
981 let mut reader =
982 JsonFrameReader::new(input.as_bytes(), BusProtocol::J1939.database(Units::Si));
983 assert!(reader.read_frame().unwrap().is_none());
984 assert!(reader.db.is_j1939());
985 assert_eq!(reader.db.units(), Units::Metric);
986 }
987
988 #[test]
991 fn a_protocol_mismatch_is_an_error() {
992 let input = "{\"version\":\"8.4.0\",\"units\":\"si\",\"protocol\":\"j1939\",\"showLookupValues\":true}\n";
993 let mut reader = JsonFrameReader::new(input.as_bytes(), db());
994 let err = reader.read_frame().unwrap_err();
995 assert!(err.to_string().contains("--protocol j1939"), "{err}");
996 }
997
998 #[test]
1001 fn banner_units_decide_how_bare_values_encode() {
1002 let heading = |units, value: f64| {
1003 frame_from_json(
1004 PgnDatabase::embedded(units),
1005 &format!(
1006 r#"{{"pgn":127250,"src":27,"description":"Vessel Heading","fields":{{"heading":{value}}}}}"#
1007 ),
1008 )
1009 };
1010 let deg = heading(Units::Metric, 3.5).unwrap().unwrap();
1011 let rad = heading(Units::Si, 3.5).unwrap().unwrap();
1012 assert_ne!(
1013 deg.data.as_slice(),
1014 rad.data.as_slice(),
1015 "3.5 deg and 3.5 rad must not encode alike"
1016 );
1017 assert_eq!(°.data[1..3], &[0x63, 0x02]);
1019 assert_eq!(
1022 &heading(Units::Metric, 210.9).unwrap().unwrap().data[1..3],
1023 &[0xc9, 0x8f]
1024 );
1025 assert!(heading(Units::Si, 210.9).is_err());
1026 }
1027}