1use crate::tag::{Tag, TagGroup, TagId};
8use crate::value::Value;
9
10const PROCESS_BY_META_FORMAT: &[&[u8; 4]] = &[b"meta", b"data", b"sbtl", b"ctbx"];
13
14const KNOTS_TO_KPH: f64 = 1.852;
16const MPS_TO_KPH: f64 = 3.6;
17const MPH_TO_KPH: f64 = 1.60934;
18
19#[derive(Debug, Clone, Default)]
23pub struct TrackInfo {
24 pub track_number: u32,
28 pub handler_type: [u8; 4],
30 pub has_jpeg_box: bool,
33 pub meta_format: Option<String>,
35 pub media_timescale: u32,
37 pub stco: Vec<u64>,
39 pub stsc: Vec<(u32, u32, u32)>,
41 pub stsz: Vec<u32>,
43 pub stts: Vec<(u32, u32)>,
45}
46
47#[derive(Debug, Clone, Default)]
49pub struct StreamState {
50 pub tracks: Vec<TrackInfo>,
51 pub current: TrackInfo,
53 pub in_stbl: bool,
55}
56
57pub fn extract_stream_tags(
65 data: &[u8],
66 tracks: &[TrackInfo],
67 _extract_embedded: u8,
68 main_tags: &[Tag],
69) -> Vec<Tag> {
70 let mut tags = Vec::new();
71 let mut doc_count: u32 = 0;
72
73 for track in tracks {
74 let handler = track.handler_type;
79 let meta_format = track.meta_format.as_deref().unwrap_or("");
80 let sample_type: [u8; 4] = if &handler == b"vide" {
81 if track.has_jpeg_box {
82 *b"JPEG"
83 } else {
84 continue;
85 }
86 } else if &handler == b"soun" {
87 continue;
88 } else {
89 handler
90 };
91
92 let samples = compute_samples(track);
94 if samples.is_empty() {
95 continue;
96 }
97
98 for s in &samples {
99 if s.offset as usize + s.size as usize > data.len() || s.size == 0 {
100 continue;
101 }
102 let sample_data = &data[s.offset as usize..(s.offset as usize + s.size as usize)];
103
104 let mut sample_tags = Vec::new();
105
106 let dispatched = if &sample_type == b"JPEG" {
107 process_jpg_from_raw(sample_data, &mut sample_tags)
110 } else if PROCESS_BY_META_FORMAT.contains(&&handler) && meta_format == "CTMD" {
111 super::quicktime::process_ctmd(sample_data, main_tags, &mut sample_tags)
114 } else {
115 dispatch_sample(
116 sample_data,
117 &handler,
118 meta_format,
119 s.time,
120 s.duration,
121 &mut sample_tags,
122 )
123 };
124
125 if dispatched && !sample_tags.is_empty() {
126 doc_count += 1;
127 if let Some(d) = s.duration {
130 sample_tags.insert(
131 0,
132 mk_stream(
133 "SampleDuration",
134 "Sample Duration",
135 Value::String(convert_duration(d)),
136 ),
137 );
138 }
139 if let Some(t) = s.time {
140 sample_tags.insert(
141 0,
142 mk_stream(
143 "SampleTime",
144 "Sample Time",
145 Value::String(convert_duration(t)),
146 ),
147 );
148 }
149 let doc = format!("Doc{}", doc_count);
150 let group1 = format!("Track{}", track.track_number);
151 for t in &mut sample_tags {
152 t.group.family3 = doc.clone();
153 if track.track_number > 0 {
154 t.group.family1 = group1.clone();
155 }
156 }
157 tags.extend(sample_tags);
158 }
159 }
160 }
161
162 if doc_count == 0 {
164 scan_mdat_for_freegps(data, &mut tags, &mut doc_count);
165 }
166
167 tags
168}
169
170fn process_jpg_from_raw(sample: &[u8], tags: &mut Vec<Tag>) -> bool {
174 if sample.is_empty() {
175 return false;
176 }
177 tags.push(Tag {
178 id: TagId::Text("JpgFromRaw".into()),
179 name: "JpgFromRaw".into(),
180 description: "Jpg From Raw".into(),
181 group: TagGroup {
182 family0: "QuickTime".into(),
183 family1: "QuickTime".into(),
184 family2: "Preview".into(),
185 family3: "Main".into(),
186 },
187 raw_value: Value::Binary(sample.to_vec()),
188 print_value: format!(
189 "(Binary data {} bytes, use -b option to extract)",
190 sample.len()
191 ),
192 priority: 0,
193 });
194 true
195}
196
197fn convert_duration(secs: f64) -> String {
200 if secs == 0.0 {
201 return "0 s".to_string();
202 }
203 let (sign, t) = if secs < 0.0 { ("-", -secs) } else { ("", secs) };
204 if t < 30.0 {
205 return format!("{}{:.2} s", sign, t);
206 }
207 let rounded = (t + 0.5) as u64;
208 let h = rounded / 3600;
209 let m = (rounded / 60) % 60;
210 let s = rounded % 60;
211 if h > 24 {
212 let d = h / 24;
213 return format!("{}{} days {}:{:02}:{:02}", sign, d, h - d * 24, m, s);
214 }
215 format!("{}{}:{:02}:{:02}", sign, h, m, s)
216}
217
218struct SampleInfo {
221 offset: u64,
222 size: u32,
223 time: Option<f64>,
224 duration: Option<f64>,
225}
226
227fn compute_samples(track: &TrackInfo) -> Vec<SampleInfo> {
228 let mut result = Vec::new();
229 if track.stsz.is_empty() || track.stco.is_empty() || track.stsc.is_empty() {
230 return result;
231 }
232
233 let ts = if track.media_timescale > 0 {
234 track.media_timescale as f64
235 } else {
236 1.0
237 };
238
239 let mut stts_flat: Vec<(u32, u32)> = Vec::new();
241 for &(count, delta) in &track.stts {
242 stts_flat.push((count, delta));
243 }
244 let mut stts_idx = 0;
245 let mut stts_remaining: u32 = if !stts_flat.is_empty() {
246 stts_flat[0].0
247 } else {
248 0
249 };
250 let mut stts_delta: u32 = if !stts_flat.is_empty() {
251 stts_flat[0].1
252 } else {
253 0
254 };
255 let mut time_acc: u64 = 0;
256 let has_time = !stts_flat.is_empty();
257
258 let mut stsc_idx = 0;
260 let mut samples_per_chunk = track.stsc[0].1;
261 let mut next_first_chunk: Option<u32> = if track.stsc.len() > 1 {
262 Some(track.stsc[1].0)
263 } else {
264 None
265 };
266
267 let mut sample_idx: usize = 0;
268
269 for (chunk_idx_0, &chunk_offset) in track.stco.iter().enumerate() {
270 let chunk_num = chunk_idx_0 as u32 + 1; if let Some(nfc) = next_first_chunk {
274 if chunk_num >= nfc {
275 stsc_idx += 1;
276 if stsc_idx < track.stsc.len() {
277 samples_per_chunk = track.stsc[stsc_idx].1;
278 next_first_chunk = if stsc_idx + 1 < track.stsc.len() {
279 Some(track.stsc[stsc_idx + 1].0)
280 } else {
281 None
282 };
283 }
284 }
285 }
286
287 let mut offset_in_chunk: u64 = 0;
288 for _ in 0..samples_per_chunk {
289 if sample_idx >= track.stsz.len() {
290 break;
291 }
292 let sz = track.stsz[sample_idx];
293 let sample_time = if has_time {
294 Some(time_acc as f64 / ts)
295 } else {
296 None
297 };
298 let sample_dur = if has_time {
299 Some(stts_delta as f64 / ts)
300 } else {
301 None
302 };
303
304 result.push(SampleInfo {
305 offset: chunk_offset + offset_in_chunk,
306 size: sz,
307 time: sample_time,
308 duration: sample_dur,
309 });
310
311 offset_in_chunk += sz as u64;
312 sample_idx += 1;
313
314 if has_time {
316 time_acc += stts_delta as u64;
317 stts_remaining = stts_remaining.saturating_sub(1);
318 if stts_remaining == 0 {
319 stts_idx += 1;
320 if stts_idx < stts_flat.len() {
321 stts_remaining = stts_flat[stts_idx].0;
322 stts_delta = stts_flat[stts_idx].1;
323 }
324 }
325 }
326 }
327 }
328
329 result
330}
331
332fn dispatch_sample(
335 sample: &[u8],
336 handler: &[u8; 4],
337 meta_format: &str,
338 _time: Option<f64>,
339 _dur: Option<f64>,
340 tags: &mut Vec<Tag>,
341) -> bool {
342 match meta_format {
344 "camm" => return process_camm(sample, tags),
345 "gpmd" => return process_gpmd(sample, tags),
346 "mebx" => return process_mebx(sample, tags),
347 "tx3g" => return process_tx3g(sample, tags),
348 _ => {}
349 }
350
351 match handler {
353 b"text" | b"sbtl" => {
354 if meta_format == "tx3g" {
356 return process_tx3g(sample, tags);
357 }
358 return process_text(sample, tags);
359 }
360 b"gps " => {
361 if sample.len() >= 12 && &sample[4..12] == b"freeGPS " {
363 return process_freegps(sample, tags);
364 }
365 return process_nmea(sample, tags);
367 }
368 b"meta" | b"data" => {
369 match meta_format {
371 "RVMI" => return process_rvmi(sample, tags),
372 _ => {
373 if sample.len() >= 12 && &sample[4..12] == b"freeGPS " {
374 return process_freegps(sample, tags);
375 }
376 }
377 }
378 }
379 _ => {
380 if sample.starts_with(b"VIDEO") && sample.windows(2).any(|w| w == b"\xfe\xfe") {
382 return process_kenwood(sample, tags);
383 }
384 }
385 }
386 false
387}
388
389fn process_freegps(data: &[u8], tags: &mut Vec<Tag>) -> bool {
392 if data.len() < 82 {
393 return false;
394 }
395
396 if data.len() > 26 && &data[18..26] == b"\xaa\xaa\xf2\xe1\xf0\xee\x54\x54" {
398 return process_freegps_type1_encrypted(data, tags);
399 }
400
401 if data.len() > 64 {
403 if let Some(dt) = try_ascii_digits(&data[52..], 14) {
404 if dt.len() == 14 {
405 return process_freegps_type2_nmea(data, tags);
406 }
407 }
408 }
409
410 if data.len() > 75 && data[72] == b'A' && is_ns(data[73]) && is_ew(data[74]) && data[75] == 0 {
412 return process_freegps_novatek(data, tags);
413 }
414
415 if data.len() > 44 && &data[37..41] == b"\0\0\0A" && is_ns(data[41]) && is_ew(data[42]) {
417 return process_freegps_viofo(data, 0, tags);
418 }
419 if data.len() > 92 && &data[85..89] == b"\0\0\0A" && is_ns(data[89]) && is_ew(data[90]) {
420 return process_freegps_viofo(data, 48, tags);
422 }
423
424 if data.len() > 96
426 && data[60] == b'A'
427 && data[61] == 0
428 && data[62] == 0
429 && data[63] == 0
430 && is_ns(data[68])
431 && is_ew(data[76])
432 {
433 return process_freegps_akaso(data, tags);
434 }
435
436 if data.len() > 100 && data[64] == b'A' && is_ns(data[65]) && is_ew(data[66]) && data[67] == 0 {
438 return process_freegps_vantrue_s1(data, tags);
439 }
440
441 if data.len() >= 0x88
443 && data[60] == b'A'
444 && data[61] == 0
445 && is_ns(data[72])
446 && data[73] == 0
447 && is_ew(data[88])
448 && data[89] == 0
449 {
450 return process_freegps_type12(data, tags);
451 }
452
453 if data.len() > 48 && data[16] == b'A' && is_ns(data[17]) && is_ew(data[18]) && data[19] == 0 {
455 return process_freegps_innovv(data, tags);
456 }
457
458 if data.len() > 80 && data[28] == b'A' && is_ns(data[40]) && is_ew(data[56]) {
460 return process_freegps_vantrue_n4(data, tags);
461 }
462
463 if data.len() > 0x50 {
465 return process_freegps_nextbase_binary(data, tags);
466 }
467
468 false
469}
470
471fn process_freegps_type1_encrypted(data: &[u8], tags: &mut Vec<Tag>) -> bool {
474 let n = (data.len() - 18).min(0x101);
475 let decrypted: Vec<u8> = data[18..18 + n].iter().map(|b| b ^ 0xaa).collect();
476
477 if decrypted.len() < 66 {
478 return false;
479 }
480
481 let dt_bytes = &decrypted[8..22];
483 let dt_str = match std::str::from_utf8(dt_bytes) {
484 Ok(s) if s.chars().all(|c| c.is_ascii_digit()) => s,
485 _ => return false,
486 };
487 if dt_str.len() < 14 {
488 return false;
489 }
490 let yr = &dt_str[0..4];
491 let mo = &dt_str[4..6];
492 let dy = &dt_str[6..8];
493 let hr = &dt_str[8..10];
494 let mi = &dt_str[10..12];
495 let se = &dt_str[12..14];
496
497 if decrypted.len() < 57 {
499 return false;
500 }
501 let lat_ref = decrypted[37];
502 if lat_ref != b'N' && lat_ref != b'S' {
503 return false;
504 }
505 let lon_ref = decrypted[46];
506 if lon_ref != b'E' && lon_ref != b'W' {
507 return false;
508 }
509 let lat_str = match std::str::from_utf8(&decrypted[38..46]) {
510 Ok(s) if s.chars().all(|c| c.is_ascii_digit()) => s,
511 _ => return false,
512 };
513 let lon_str = match std::str::from_utf8(&decrypted[47..56]) {
514 Ok(s) if s.chars().all(|c| c.is_ascii_digit()) => s,
515 _ => return false,
516 };
517 let lat: f64 = lat_str.parse::<f64>().unwrap_or(0.0) / 1e4;
518 let lon: f64 = lon_str.parse::<f64>().unwrap_or(0.0) / 1e4;
519 let (lat_dd, lon_dd) = convert_lat_lon(lat, lon);
520 let lat_final = lat_dd * if lat_ref == b'S' { -1.0 } else { 1.0 };
521 let lon_final = lon_dd * if lon_ref == b'W' { -1.0 } else { 1.0 };
522
523 tags.push(mk_gps_dt(&format!(
524 "{}:{}:{} {}:{}:{}Z",
525 yr, mo, dy, hr, mi, se
526 )));
527 tags.push(mk_gps_lat(lat_final));
528 tags.push(mk_gps_lon(lon_final));
529
530 if decrypted.len() >= 65 {
532 if let Ok(s) = std::str::from_utf8(&decrypted[56..64]) {
533 if let Ok(spd) = s.trim_start_matches('0').parse::<f64>() {
534 tags.push(mk_gps_spd(spd));
535 }
536 }
537 }
538
539 true
540}
541
542fn process_freegps_type2_nmea(data: &[u8], tags: &mut Vec<Tag>) -> bool {
545 if let Some(dt) = try_ascii_digits(&data[52..], 14) {
547 if dt.len() >= 14 {
548 let cam_dt = format!(
549 "{}:{}:{} {}:{}:{}",
550 &dt[0..4],
551 &dt[4..6],
552 &dt[6..8],
553 &dt[8..10],
554 &dt[10..12],
555 &dt[12..14]
556 );
557 tags.push(mk_stream(
558 "CameraDateTime",
559 "Camera Date/Time",
560 Value::String(cam_dt),
561 ));
562 }
563 }
564
565 let text = crate::encoding::decode_utf8_or_latin1(data);
567 if parse_nmea_rmc(&text, tags) {
568 return true;
569 }
570 if parse_nmea_gga(&text, tags) {
571 return true;
572 }
573 false
574}
575
576fn process_freegps_novatek(data: &[u8], tags: &mut Vec<Tag>) -> bool {
579 if data.len() < 0x5c {
584 return false;
585 }
586 let hr = get_u32_le(data, 0x30);
587 let min = get_u32_le(data, 0x34);
588 let sec = get_u32_le(data, 0x38);
589 let yr = get_u32_le(data, 0x3c);
590 let mon = get_u32_le(data, 0x40);
591 let day = get_u32_le(data, 0x44);
592 let lat_ref = data[0x49];
593 let lon_ref = data[0x4a];
594
595 if !(1..=12).contains(&mon) || !(1..=31).contains(&day) {
596 return false;
597 }
598
599 let full_yr = if yr < 2000 { yr + 2000 } else { yr };
600
601 let lat = get_f32_le(data, 0x4c) as f64;
602 let lon = get_f32_le(data, 0x50) as f64;
603 let spd = get_f32_le(data, 0x54) as f64 * KNOTS_TO_KPH;
604 let trk = get_f32_le(data, 0x58) as f64;
605
606 let (lat_dd, lon_dd) = convert_lat_lon(lat, lon);
607 let lat_final = lat_dd * if lat_ref == b'S' { -1.0 } else { 1.0 };
608 let lon_final = lon_dd * if lon_ref == b'W' { -1.0 } else { 1.0 };
609
610 tags.push(mk_gps_dt(&format!(
611 "{:04}:{:02}:{:02} {:02}:{:02}:{:02}Z",
612 full_yr, mon, day, hr, min, sec
613 )));
614 tags.push(mk_gps_lat(lat_final));
615 tags.push(mk_gps_lon(lon_final));
616 tags.push(mk_gps_spd(spd));
617 tags.push(mk_gps_trk(trk));
618
619 true
620}
621
622fn process_freegps_viofo(data: &[u8], extra_offset: usize, tags: &mut Vec<Tag>) -> bool {
625 let d = if extra_offset > 0 && data.len() > extra_offset {
626 &data[extra_offset..]
627 } else {
628 data
629 };
630 if d.len() < 0x3c {
631 return false;
632 }
633
634 let hr = get_u32_le(d, 0x10);
635 let min = get_u32_le(d, 0x14);
636 let sec = get_u32_le(d, 0x18);
637 let yr = get_u32_le(d, 0x1c);
638 let mon = get_u32_le(d, 0x20);
639 let day = get_u32_le(d, 0x24);
640
641 let lat_ref = d[0x29]; let lon_ref = d[0x2a]; if !(1..=12).contains(&mon) || !(1..=31).contains(&day) {
645 return false;
646 }
647
648 let full_yr = if yr < 2000 { yr + 2000 } else { yr };
649
650 let lat = get_f32_le(d, 0x2c) as f64;
651 let lon = get_f32_le(d, 0x30) as f64;
652 let spd = get_f32_le(d, 0x34) as f64 * KNOTS_TO_KPH;
653 let trk = get_f32_le(d, 0x38) as f64;
654
655 tags.push(mk_gps_dt(&format!(
656 "{:04}:{:02}:{:02} {:02}:{:02}:{:02}Z",
657 full_yr, mon, day, hr, min, sec
658 )));
659 tags.push(mk_gps_lat(lat * if lat_ref == b'S' { -1.0 } else { 1.0 }));
660 tags.push(mk_gps_lon(lon * if lon_ref == b'W' { -1.0 } else { 1.0 }));
661 tags.push(mk_gps_spd(spd));
662 tags.push(mk_gps_trk(trk));
663
664 true
665}
666
667fn process_freegps_akaso(data: &[u8], tags: &mut Vec<Tag>) -> bool {
670 if data.len() < 0x58 {
671 return false;
672 }
673 let lat_ref = data[68];
674 let lon_ref = data[76];
675 let hr = get_u32_le(data, 48);
676 let min = get_u32_le(data, 52);
677 let sec = get_u32_le(data, 56);
678 let yr = get_u32_le(data, 84);
679 let mon = get_u32_le(data, 88);
680 let day = get_u32_le(data, 92);
681
682 if !(1..=12).contains(&mon) {
683 return false;
684 }
685
686 let lat = get_f32_le(data, 0x40) as f64;
687 let lon = get_f32_le(data, 0x48) as f64;
688 let spd = get_f32_le(data, 0x50) as f64;
689 let trk = get_f32_le(data, 0x54) as f64;
690
691 tags.push(mk_gps_dt(&format!(
692 "{:04}:{:02}:{:02} {:02}:{:02}:{:02}Z",
693 yr, mon, day, hr, min, sec
694 )));
695 tags.push(mk_gps_lat(lat * if lat_ref == b'S' { -1.0 } else { 1.0 }));
696 tags.push(mk_gps_lon(lon * if lon_ref == b'W' { -1.0 } else { 1.0 }));
697 tags.push(mk_gps_spd(spd));
698 tags.push(mk_gps_trk(trk));
699
700 true
701}
702
703fn process_freegps_vantrue_s1(data: &[u8], tags: &mut Vec<Tag>) -> bool {
706 if data.len() < 0x70 {
707 return false;
708 }
709 let lat_ref = data[65];
710 let lon_ref = data[66];
711
712 let yr = get_u32_le(data, 68);
713 let mon = get_u32_le(data, 72);
714 let day = get_u32_le(data, 76);
715 let hr = get_u32_le(data, 80);
716 let min = get_u32_le(data, 84);
717 let sec = get_u32_le(data, 88);
718
719 if !(1..=12).contains(&mon) || !(1..=31).contains(&day) {
720 return false;
721 }
722
723 let lon = get_f32_le(data, 0x5c) as f64;
724 let lat = get_f32_le(data, 0x60) as f64;
725 let spd = get_f32_le(data, 0x64) as f64 * KNOTS_TO_KPH;
726 let trk = get_f32_le(data, 0x68) as f64;
727 let alt = get_f32_le(data, 0x6c) as f64;
728
729 tags.push(mk_gps_dt(&format!(
730 "{:04}:{:02}:{:02} {:02}:{:02}:{:02}Z",
731 yr, mon, day, hr, min, sec
732 )));
733 tags.push(mk_gps_lat(lat * if lat_ref == b'S' { -1.0 } else { 1.0 }));
734 tags.push(mk_gps_lon(lon * if lon_ref == b'W' { -1.0 } else { 1.0 }));
735 tags.push(mk_gps_spd(spd));
736 tags.push(mk_gps_trk(trk));
737 tags.push(mk_gps_alt(alt));
738
739 true
740}
741
742fn process_freegps_type12(data: &[u8], tags: &mut Vec<Tag>) -> bool {
745 if data.len() < 0x88 {
746 return false;
747 }
748 let lat_ref = data[72];
749 let lon_ref = data[88];
750
751 let hr = get_u32_le(data, 48);
752 let min = get_u32_le(data, 52);
753 let sec = get_u32_le(data, 56);
754 let yr = get_u32_le(data, 0x70);
755 let mon = get_u32_le(data, 0x74);
756 let day = get_u32_le(data, 0x78);
757
758 if !(1..=12).contains(&mon) {
759 return false;
760 }
761
762 let full_yr = if yr < 2000 { yr + 2000 } else { yr };
763
764 let lat = get_f64_le(data, 0x40);
765 let lon = get_f64_le(data, 0x50);
766 let spd = get_f64_le(data, 0x60) * KNOTS_TO_KPH;
767 let trk = get_f64_le(data, 0x68);
768
769 let (lat_dd, lon_dd) = convert_lat_lon(lat, lon);
770
771 tags.push(mk_gps_dt(&format!(
772 "{:04}:{:02}:{:02} {:02}:{:02}:{:02}Z",
773 full_yr, mon, day, hr, min, sec
774 )));
775 tags.push(mk_gps_lat(
776 lat_dd * if lat_ref == b'S' { -1.0 } else { 1.0 },
777 ));
778 tags.push(mk_gps_lon(
779 lon_dd * if lon_ref == b'W' { -1.0 } else { 1.0 },
780 ));
781 tags.push(mk_gps_spd(spd));
782 tags.push(mk_gps_trk(trk));
783
784 true
785}
786
787fn process_freegps_innovv(data: &[u8], tags: &mut Vec<Tag>) -> bool {
790 let mut pos = 16;
792 let mut found = false;
793 while pos + 32 <= data.len() {
794 if data[pos] != b'A' || !is_ns(data[pos + 1]) || !is_ew(data[pos + 2]) || data[pos + 3] != 0
795 {
796 break;
797 }
798 let lat_ref = data[pos + 1];
799 let lon_ref = data[pos + 2];
800 let lat = get_f32_le(data, pos + 4).abs() as f64;
801 let lon = get_f32_le(data, pos + 8).abs() as f64;
802 let spd = get_f32_le(data, pos + 12) as f64 * KNOTS_TO_KPH;
803 let trk = get_f32_le(data, pos + 16) as f64;
804
805 let (lat_dd, lon_dd) = convert_lat_lon(lat, lon);
806 tags.push(mk_gps_lat(
807 lat_dd * if lat_ref == b'S' { -1.0 } else { 1.0 },
808 ));
809 tags.push(mk_gps_lon(
810 lon_dd * if lon_ref == b'W' { -1.0 } else { 1.0 },
811 ));
812 tags.push(mk_gps_spd(spd));
813 tags.push(mk_gps_trk(trk));
814 found = true;
815 pos += 32;
816 }
817 found
818}
819
820fn process_freegps_vantrue_n4(data: &[u8], tags: &mut Vec<Tag>) -> bool {
823 if data.len() < 80 {
824 return false;
825 }
826 let lat_ref = data[40];
827 let lon_ref = data[56];
828
829 let hr = get_u32_le(data, 16);
830 let min = get_u32_le(data, 20);
831 let sec = get_u32_le(data, 24);
832
833 if data.len() < 92 {
835 return false;
836 }
837 let yr = get_u32_le(data, 80);
838 let mon = get_u32_le(data, 84);
839 let day = get_u32_le(data, 88);
840
841 if !(1..=12).contains(&mon) {
842 return false;
843 }
844
845 let lat = get_f64_le(data, 32).abs();
846 let lon = get_f64_le(data, 48).abs();
847 let spd = get_f64_le(data, 64) * KNOTS_TO_KPH;
848 let trk = get_f64_le(data, 72);
849
850 tags.push(mk_gps_dt(&format!(
851 "{:04}:{:02}:{:02} {:02}:{:02}:{:02}Z",
852 yr, mon, day, hr, min, sec
853 )));
854 tags.push(mk_gps_lat(lat * if lat_ref == b'S' { -1.0 } else { 1.0 }));
855 tags.push(mk_gps_lon(lon * if lon_ref == b'W' { -1.0 } else { 1.0 }));
856 tags.push(mk_gps_spd(spd));
857 tags.push(mk_gps_trk(trk));
858
859 true
860}
861
862fn process_freegps_nextbase_binary(data: &[u8], tags: &mut Vec<Tag>) -> bool {
865 let mut pos = 0x32usize;
868 let mut found = false;
869 while pos + 0x1e <= data.len() {
870 let spd_raw = get_u16_be(data, pos);
871 let trk_raw = get_u16_be(data, pos + 2) as i16;
872 let yr = get_u16_be(data, pos + 4);
873 let mon = data[pos + 6];
874 let day = data[pos + 7];
875 let hr = data[pos + 8];
876 let min = data[pos + 9];
877 let sec10 = get_u16_be(data, pos + 10);
878
879 if !(2000..=2200).contains(&yr)
880 || !(1..=12).contains(&mon)
881 || !(1..=31).contains(&day)
882 || hr > 59
883 || min > 59
884 || sec10 > 600
885 {
886 break;
887 }
888
889 let lat_raw = get_u32_be(data, pos + 13);
890 let lon_raw = get_u32_be(data, pos + 17);
891 let lat = signed_u32(lat_raw) as f64 / 1e7;
892 let lon = signed_u32(lon_raw) as f64 / 1e7;
893 let mut trk = trk_raw as f64 / 100.0;
894 if trk < 0.0 {
895 trk += 360.0;
896 }
897
898 let time = format!(
899 "{:04}:{:02}:{:02} {:02}:{:02}:{:04.1}Z",
900 yr,
901 mon,
902 day,
903 hr,
904 min,
905 sec10 as f64 / 10.0
906 );
907 tags.push(mk_gps_dt(&time));
908 tags.push(mk_gps_lat(lat));
909 tags.push(mk_gps_lon(lon));
910 tags.push(mk_gps_spd(spd_raw as f64 / 100.0 * MPS_TO_KPH));
911 tags.push(mk_gps_trk(trk));
912 found = true;
913
914 pos += 0x20;
915 }
916 found
917}
918
919fn process_camm(data: &[u8], tags: &mut Vec<Tag>) -> bool {
922 if data.len() < 4 {
923 return false;
924 }
925 let camm_type = get_u16_le(data, 2);
926
927 match camm_type {
928 0 => {
929 if data.len() >= 16 {
931 let x = get_f32_le(data, 4);
932 let y = get_f32_le(data, 8);
933 let z = get_f32_le(data, 12);
934 tags.push(mk_stream(
935 "AngleAxis",
936 "Angle Axis",
937 Value::String(format!("{} {} {}", x, y, z)),
938 ));
939 return true;
940 }
941 }
942 2 => {
943 if data.len() >= 16 {
945 let x = get_f32_le(data, 4);
946 let y = get_f32_le(data, 8);
947 let z = get_f32_le(data, 12);
948 tags.push(mk_stream(
949 "AngularVelocity",
950 "Angular Velocity",
951 Value::String(format!("{} {} {}", x, y, z)),
952 ));
953 return true;
954 }
955 }
956 3 => {
957 if data.len() >= 16 {
959 let x = get_f32_le(data, 4);
960 let y = get_f32_le(data, 8);
961 let z = get_f32_le(data, 12);
962 tags.push(mk_stream(
963 "Accelerometer",
964 "Accelerometer",
965 Value::String(format!("{} {} {}", x, y, z)),
966 ));
967 return true;
968 }
969 }
970 5 => {
971 if data.len() >= 28 {
973 let lat = get_f64_le(data, 4);
974 let lon = get_f64_le(data, 12);
975 let alt = get_f64_le(data, 20);
976 tags.push(mk_gps_lat(lat));
977 tags.push(mk_gps_lon(lon));
978 tags.push(mk_gps_alt(alt));
979 return true;
980 }
981 }
982 6 => {
983 if data.len() >= 60 {
985 let _timestamp = get_f64_le(data, 4);
986 let lat = get_f64_le(data, 0x10);
987 let lon = get_f64_le(data, 0x18);
988 let alt = get_f32_le(data, 0x20) as f64;
989
990 tags.push(mk_gps_lat(lat));
991 tags.push(mk_gps_lon(lon));
992 tags.push(mk_gps_alt(alt));
993
994 if data.len() >= 0x38 {
995 let vel_east = get_f32_le(data, 0x2c);
996 let vel_north = get_f32_le(data, 0x30);
997 let speed =
998 ((vel_east * vel_east + vel_north * vel_north) as f64).sqrt() * MPS_TO_KPH;
999 tags.push(mk_gps_spd(speed));
1000 }
1001 return true;
1002 }
1003 }
1004 7
1005 if data.len() >= 16 => {
1007 let x = get_f32_le(data, 4);
1008 let y = get_f32_le(data, 8);
1009 let z = get_f32_le(data, 12);
1010 tags.push(mk_stream(
1011 "MagneticField",
1012 "Magnetic Field",
1013 Value::String(format!("{} {} {}", x, y, z)),
1014 ));
1015 return true;
1016 }
1017 _ => {}
1018 }
1019 false
1020}
1021
1022fn process_gpmd(data: &[u8], tags: &mut Vec<Tag>) -> bool {
1025 process_gpmf_klv(data, 0, data.len(), tags)
1028}
1029
1030fn process_gpmf_klv(data: &[u8], start: usize, end: usize, tags: &mut Vec<Tag>) -> bool {
1031 let mut pos = start;
1032 let mut found = false;
1033
1034 while pos + 8 <= end {
1035 let fourcc = &data[pos..pos + 4];
1036 let type_byte = data[pos + 4];
1037 let size_byte = data[pos + 5];
1038 let repeat = get_u16_be(data, pos + 6) as usize;
1039
1040 let struct_size = size_byte as usize;
1041 let total_data = struct_size * repeat;
1042 let padded = (total_data + 3) & !3;
1044 let data_start = pos + 8;
1045
1046 if data_start + padded > end {
1047 break;
1048 }
1049
1050 if type_byte == 0 && struct_size == 4 {
1051 if process_gpmf_klv(data, data_start, data_start + total_data, tags) {
1053 found = true;
1054 }
1055 } else if fourcc == b"GPS5" && struct_size >= 20 && type_byte == b'l' {
1056 for i in 0..repeat {
1058 let off = data_start + i * struct_size;
1059 if off + 20 > end {
1060 break;
1061 }
1062 let lat = get_i32_be(data, off) as f64 / 1e7;
1063 let lon = get_i32_be(data, off + 4) as f64 / 1e7;
1064 let alt = get_i32_be(data, off + 8) as f64 / 100.0;
1065 let speed2d = get_i32_be(data, off + 12) as f64 / 100.0 * MPS_TO_KPH;
1066
1067 tags.push(mk_gps_lat(lat));
1068 tags.push(mk_gps_lon(lon));
1069 tags.push(mk_gps_alt(alt));
1070 tags.push(mk_gps_spd(speed2d));
1071 found = true;
1072 }
1073 } else if fourcc == b"GPSU" && type_byte == b'U' && total_data >= 16 {
1074 if let Ok(s) = std::str::from_utf8(&data[data_start..data_start + total_data.min(16)]) {
1076 let s = s.trim_end_matches('\0');
1077 if s.len() >= 12 {
1078 let dt = format!(
1079 "20{}:{}:{} {}:{}:{}Z",
1080 &s[0..2],
1081 &s[2..4],
1082 &s[4..6],
1083 &s[6..8],
1084 &s[8..10],
1085 &s[10..]
1086 );
1087 tags.push(mk_gps_dt(&dt));
1088 found = true;
1089 }
1090 }
1091 } else if fourcc == b"ACCL" && type_byte == b's' && struct_size >= 6 {
1092 for i in 0..repeat.min(1) {
1094 let off = data_start + i * struct_size;
1095 if off + 6 > end {
1096 break;
1097 }
1098 let x = get_i16_be(data, off) as f64 / 100.0;
1099 let y = get_i16_be(data, off + 2) as f64 / 100.0;
1100 let z = get_i16_be(data, off + 4) as f64 / 100.0;
1101 tags.push(mk_stream(
1102 "Accelerometer",
1103 "Accelerometer",
1104 Value::String(format!("{:.4} {:.4} {:.4}", x, y, z)),
1105 ));
1106 found = true;
1107 }
1108 } else if fourcc == b"GYRO" && type_byte == b's' && struct_size >= 6 {
1109 for i in 0..repeat.min(1) {
1111 let off = data_start + i * struct_size;
1112 if off + 6 > end {
1113 break;
1114 }
1115 let x = get_i16_be(data, off) as f64 / 100.0;
1116 let y = get_i16_be(data, off + 2) as f64 / 100.0;
1117 let z = get_i16_be(data, off + 4) as f64 / 100.0;
1118 tags.push(mk_stream(
1119 "AngularVelocity",
1120 "Angular Velocity",
1121 Value::String(format!("{:.4} {:.4} {:.4}", x, y, z)),
1122 ));
1123 found = true;
1124 }
1125 }
1126
1127 pos = data_start + padded;
1128 }
1129
1130 found
1131}
1132
1133fn process_mebx(data: &[u8], tags: &mut Vec<Tag>) -> bool {
1136 let mut pos = 0;
1138 let mut found = false;
1139 while pos + 8 < data.len() {
1140 let len = get_u32_be(data, pos) as usize;
1141 if len < 8 || pos + len > data.len() {
1142 break;
1143 }
1144 let key = &data[pos + 4..pos + 8];
1145 let val_data = &data[pos + 8..pos + len];
1146
1147 if let Ok(s) = std::str::from_utf8(val_data) {
1149 let key_str = crate::encoding::decode_utf8_or_latin1(key).to_string();
1150 let name = key_str.trim().to_string();
1151 if !name.is_empty() {
1152 tags.push(mk_stream(&name, &name, Value::String(s.trim().to_string())));
1153 found = true;
1154 }
1155 }
1156 pos += len;
1157 }
1158 found
1159}
1160
1161fn process_tx3g(data: &[u8], tags: &mut Vec<Tag>) -> bool {
1164 if data.len() < 2 {
1165 return false;
1166 }
1167 let text = crate::encoding::decode_utf8_or_latin1(&data[2..]); let text = text.trim();
1169 if text.is_empty() {
1170 return false;
1171 }
1172
1173 if text.starts_with("HOME(") {
1175 return process_tx3g_autel(text, tags);
1176 }
1177
1178 let mut found = false;
1180 for line in text.lines() {
1182 let line = line.trim();
1183 for cap in line.split_whitespace() {
1185 if let Some((k, v)) = cap.split_once(':') {
1186 match k {
1187 "Lat" => {
1188 if let Ok(val) = v.parse::<f64>() {
1189 tags.push(mk_gps_lat(val));
1190 found = true;
1191 }
1192 }
1193 "Lon" => {
1194 if let Ok(val) = v.parse::<f64>() {
1195 tags.push(mk_gps_lon(val));
1196 found = true;
1197 }
1198 }
1199 "Alt" => {
1200 if let Ok(val) = v.trim_end_matches('m').trim().parse::<f64>() {
1201 tags.push(mk_gps_alt(val));
1202 found = true;
1203 }
1204 }
1205 _ => {}
1206 }
1207 }
1208 }
1209 }
1210
1211 if !found {
1212 tags.push(mk_stream("Text", "Text", Value::String(text.to_string())));
1214 let _ = parse_nmea_rmc(text, tags) || parse_nmea_gga(text, tags);
1216 found = true;
1217 }
1218 found
1219}
1220
1221fn process_tx3g_autel(text: &str, tags: &mut Vec<Tag>) -> bool {
1222 let mut found = false;
1223 for line in text.lines() {
1224 let line = line.trim();
1225 if line.starts_with("HOME(") {
1227 if let Some(rest) = line.strip_prefix("HOME(") {
1229 if let Some(paren_end) = rest.find(')') {
1230 let coords = &rest[..paren_end];
1231 let after = rest[paren_end + 1..].trim();
1232 let parts: Vec<&str> = coords.split(',').collect();
1234 if parts.len() == 2 {
1235 for part in &parts {
1236 let part = part.trim();
1237 if let Some((dir, val_s)) = part.split_once(':') {
1238 let dir = dir.trim();
1239 let val_s = val_s.trim();
1240 if let Ok(val) = val_s.parse::<f64>() {
1241 match dir {
1242 "N" | "S" => {
1243 let v = if dir == "S" { -val } else { val };
1244 tags.push(mk_stream(
1245 "GPSHomeLatitude",
1246 "GPS Home Latitude",
1247 Value::String(format!("{:.6}", v)),
1248 ));
1249 found = true;
1250 }
1251 "E" | "W" => {
1252 let v = if dir == "W" { -val } else { val };
1253 tags.push(mk_stream(
1254 "GPSHomeLongitude",
1255 "GPS Home Longitude",
1256 Value::String(format!("{:.6}", v)),
1257 ));
1258 found = true;
1259 }
1260 _ => {}
1261 }
1262 }
1263 }
1264 }
1265 }
1266 if !after.is_empty() {
1268 let dt = after.replace('-', ":");
1269 tags.push(mk_gps_dt(&dt));
1270 found = true;
1271 }
1272 }
1273 }
1274 } else if line.starts_with("GPS(") {
1275 if let Some(rest) = line.strip_prefix("GPS(") {
1277 if let Some(paren_end) = rest.find(')') {
1278 let inner = &rest[..paren_end];
1279 let parts: Vec<&str> = inner.split(',').collect();
1280 for part in &parts {
1281 let part = part.trim();
1282 if let Some((dir, val_s)) = part.split_once(':') {
1283 let dir = dir.trim();
1284 let val_s = val_s.trim();
1285 if let Ok(val) = val_s.parse::<f64>() {
1286 match dir {
1287 "N" | "S" => {
1288 let v = if dir == "S" { -val } else { val };
1289 tags.push(mk_gps_lat(v));
1290 found = true;
1291 }
1292 "E" | "W" => {
1293 let v = if dir == "W" { -val } else { val };
1294 tags.push(mk_gps_lon(v));
1295 found = true;
1296 }
1297 _ => {}
1298 }
1299 }
1300 } else if part.ends_with('m') {
1301 if let Ok(alt) = part.trim_end_matches('m').trim().parse::<f64>() {
1302 tags.push(mk_gps_alt(alt));
1303 found = true;
1304 }
1305 }
1306 }
1307 }
1308 }
1309 }
1310 }
1311 found
1312}
1313
1314fn process_nmea(data: &[u8], tags: &mut Vec<Tag>) -> bool {
1317 let text = crate::encoding::decode_utf8_or_latin1(data);
1318 parse_nmea_rmc(&text, tags) || parse_nmea_gga(&text, tags)
1319}
1320
1321fn parse_nmea_rmc(text: &str, tags: &mut Vec<Tag>) -> bool {
1322 let rmc_patterns = ["$GPRMC,", "$GNRMC,", "$GBRMC,"];
1325 for pat in &rmc_patterns {
1326 if let Some(start) = text.find(pat) {
1327 let rest = &text[start + pat.len()..];
1328 return parse_rmc_fields(rest, tags);
1329 }
1330 }
1331 false
1332}
1333
1334fn parse_rmc_fields(rest: &str, tags: &mut Vec<Tag>) -> bool {
1335 let fields: Vec<&str> = rest.split(',').collect();
1336 if fields.len() < 12 {
1337 return false;
1338 }
1339
1340 let time_str = fields[0];
1343 let status = fields[1];
1344 if status != "A" && !status.is_empty() {
1345 }
1347 let lat_str = fields[2];
1348 let lat_ref = fields[3];
1349 let lon_str = fields[4];
1350 let lon_ref = fields[5];
1351 let spd_str = fields[6];
1352 let trk_str = fields[7];
1353 let date_str = fields[8];
1354
1355 let lat = match parse_nmea_coord(lat_str) {
1357 Some(v) => v * if lat_ref == "S" { -1.0 } else { 1.0 },
1358 None => return false,
1359 };
1360 let lon = match parse_nmea_coord(lon_str) {
1361 Some(v) => v * if lon_ref == "W" { -1.0 } else { 1.0 },
1362 None => return false,
1363 };
1364
1365 if date_str.len() >= 6 && time_str.len() >= 6 {
1367 let dd = &date_str[0..2];
1368 let mm = &date_str[2..4];
1369 let yy = &date_str[4..6];
1370 let yr: u32 = yy.parse().unwrap_or(0);
1371 let full_yr = if yr >= 70 { 1900 + yr } else { 2000 + yr };
1372 let time_part = if time_str.len() > 6 {
1373 &time_str[..6]
1374 } else {
1375 time_str
1376 };
1377 let dt = format!(
1378 "{:04}:{:02}:{:02} {}:{}:{}Z",
1379 full_yr,
1380 mm,
1381 dd,
1382 &time_part[0..2],
1383 &time_part[2..4],
1384 &time_part[4..6]
1385 );
1386 tags.push(mk_gps_dt(&dt));
1387 }
1388
1389 tags.push(mk_gps_lat(lat));
1390 tags.push(mk_gps_lon(lon));
1391
1392 if let Ok(spd) = spd_str.parse::<f64>() {
1393 tags.push(mk_gps_spd(spd * KNOTS_TO_KPH));
1394 }
1395 if let Ok(trk) = trk_str.parse::<f64>() {
1396 tags.push(mk_gps_trk(trk));
1397 }
1398
1399 true
1400}
1401
1402fn parse_nmea_gga(text: &str, tags: &mut Vec<Tag>) -> bool {
1403 let patterns = ["$GPGGA,", "$GNGGA,"];
1404 for pat in &patterns {
1405 if let Some(start) = text.find(pat) {
1406 let rest = &text[start + pat.len()..];
1407 let fields: Vec<&str> = rest.split(',').collect();
1408 if fields.len() < 10 {
1409 continue;
1410 }
1411
1412 let lat_str = fields[1];
1413 let lat_ref = fields[2];
1414 let lon_str = fields[3];
1415 let lon_ref = fields[4];
1416
1417 let lat = match parse_nmea_coord(lat_str) {
1418 Some(v) => v * if lat_ref == "S" { -1.0 } else { 1.0 },
1419 None => continue,
1420 };
1421 let lon = match parse_nmea_coord(lon_str) {
1422 Some(v) => v * if lon_ref == "W" { -1.0 } else { 1.0 },
1423 None => continue,
1424 };
1425
1426 tags.push(mk_gps_lat(lat));
1427 tags.push(mk_gps_lon(lon));
1428
1429 if fields.len() > 8 {
1431 if let Ok(alt) = fields[8].parse::<f64>() {
1432 tags.push(mk_gps_alt(alt));
1433 }
1434 }
1435 if let Ok(sats) = fields[6].parse::<u32>() {
1437 tags.push(mk_stream(
1438 "GPSSatellites",
1439 "GPS Satellites",
1440 Value::String(sats.to_string()),
1441 ));
1442 }
1443 return true;
1444 }
1445 }
1446 false
1447}
1448
1449fn parse_nmea_coord(s: &str) -> Option<f64> {
1450 if s.is_empty() {
1452 return None;
1453 }
1454 let val: f64 = s.parse().ok()?;
1455 let deg = (val / 100.0).floor();
1456 let min = val - deg * 100.0;
1457 Some(deg + min / 60.0)
1458}
1459
1460fn process_text(data: &[u8], tags: &mut Vec<Tag>) -> bool {
1463 let text = crate::encoding::decode_utf8_or_latin1(data);
1464 let text = text.trim();
1465 if text.is_empty() {
1466 return false;
1467 }
1468
1469 if parse_nmea_rmc(text, tags) || parse_nmea_gga(text, tags) {
1471 return true;
1472 }
1473
1474 if text.contains("GPS (") || text.contains("GPS(") {
1476 return process_dji_text(text, tags);
1477 }
1478
1479 if data.len() >= 20
1481 && (data.starts_with(b"PNDM") || (data.len() > 4 && &data[4..8.min(data.len())] == b"PNDM"))
1482 {
1483 return process_garmin_pndm(data, tags);
1484 }
1485
1486 false
1487}
1488
1489fn process_dji_text(text: &str, tags: &mut Vec<Tag>) -> bool {
1490 let gps_start = text.find("GPS (").or_else(|| text.find("GPS("));
1492 if let Some(idx) = gps_start {
1493 let rest = &text[idx..];
1494 if let Some(paren_start) = rest.find('(') {
1495 if let Some(paren_end) = rest.find(')') {
1496 let inner = &rest[paren_start + 1..paren_end];
1497 let parts: Vec<&str> = inner.split(',').collect();
1498 if parts.len() >= 2 {
1499 if let (Ok(lon), Ok(lat)) = (
1500 parts[0].trim().parse::<f64>(),
1501 parts[1].trim().parse::<f64>(),
1502 ) {
1503 tags.push(mk_gps_lat(lat));
1504 tags.push(mk_gps_lon(lon));
1505 if parts.len() >= 3 {
1506 if let Ok(alt) = parts[2].trim().parse::<f64>() {
1507 tags.push(mk_gps_alt(alt));
1508 }
1509 }
1510 }
1511 }
1512 }
1513 }
1514 }
1515
1516 if let Some(idx) = text.find("H.S ") {
1518 let rest = &text[idx + 4..];
1519 if let Some(end) = rest.find("m/s") {
1520 if let Ok(spd) = rest[..end].trim().parse::<f64>() {
1521 tags.push(mk_gps_spd(spd * MPS_TO_KPH));
1522 }
1523 }
1524 }
1525
1526 if let Some(idx) = text.find("ISO ") {
1528 let rest = &text[idx + 4..];
1529 let val: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect();
1530 if !val.is_empty() {
1531 tags.push(mk_stream("ISO", "ISO", Value::String(val)));
1532 }
1533 }
1534
1535 true
1536}
1537
1538fn process_garmin_pndm(data: &[u8], tags: &mut Vec<Tag>) -> bool {
1539 let offset = if data.starts_with(b"PNDM") { 0 } else { 4 };
1540 if data.len() < offset + 20 {
1541 return false;
1542 }
1543 let lat = get_i32_be(data, offset + 12) as f64 * 180.0 / 0x80000000u32 as f64;
1544 let lon = get_i32_be(data, offset + 16) as f64 * 180.0 / 0x80000000u32 as f64;
1545 let spd = get_u16_be(data, offset + 8) as f64 * MPH_TO_KPH;
1546
1547 tags.push(mk_gps_lat(lat));
1548 tags.push(mk_gps_lon(lon));
1549 tags.push(mk_gps_spd(spd));
1550 true
1551}
1552
1553fn process_rvmi(data: &[u8], tags: &mut Vec<Tag>) -> bool {
1556 if data.len() < 20 {
1557 return false;
1558 }
1559 if &data[0..4] == b"gReV" {
1560 let lat = get_i32_le(data, 4) as f64 / 1e6;
1562 let lon = get_i32_le(data, 8) as f64 / 1e6;
1563 let spd = get_i16_le(data, 16) as f64 / 10.0;
1564 let trk = get_u16_le(data, 18) as f64 * 2.0;
1565 tags.push(mk_gps_lat(lat));
1566 tags.push(mk_gps_lon(lon));
1567 tags.push(mk_gps_spd(spd));
1568 tags.push(mk_gps_trk(trk));
1569 return true;
1570 }
1571 if &data[0..4] == b"sReV" {
1572 if data.len() >= 10 {
1574 let x = get_i16_le(data, 4) as f64 / 1000.0;
1575 let y = get_i16_le(data, 6) as f64 / 1000.0;
1576 let z = get_i16_le(data, 8) as f64 / 1000.0;
1577 tags.push(mk_stream(
1578 "GSensor",
1579 "G Sensor",
1580 Value::String(format!("{} {} {}", x, y, z)),
1581 ));
1582 return true;
1583 }
1584 }
1585 false
1586}
1587
1588fn process_kenwood(data: &[u8], tags: &mut Vec<Tag>) -> bool {
1591 let mut found = false;
1593 let mut pos = 0;
1594 while pos + 2 < data.len() {
1595 if let Some(idx) = data[pos..].windows(2).position(|w| w == b"\xfe\xfe") {
1597 let start = pos + idx + 2;
1598 if start + 40 > data.len() {
1599 break;
1600 }
1601 let dat = &data[start..];
1602 if let Some(dt) = try_ascii_digits(dat, 14) {
1604 if dt.len() == 14 {
1605 let time = format!(
1606 "{}:{}:{} {}:{}:{}",
1607 &dt[0..4],
1608 &dt[4..6],
1609 &dt[6..8],
1610 &dt[8..10],
1611 &dt[10..12],
1612 &dt[12..14]
1613 );
1614
1615 let after = &dat[15..]; if after.len() < 20 {
1618 pos = start + 14;
1619 continue;
1620 }
1621 let after2 = if after.len() > 15 {
1623 &after[15..]
1624 } else {
1625 after
1626 };
1627
1628 if !after2.is_empty() && is_ns(after2[0]) {
1630 let lat_ref = after2[0];
1631 let mut ew_pos = 1;
1633 while ew_pos < after2.len() && !is_ew(after2[ew_pos]) {
1634 ew_pos += 1;
1635 }
1636 if ew_pos < after2.len() {
1637 let lon_ref = after2[ew_pos];
1638 let lat_digits = &after2[1..ew_pos];
1639 let lon_start = ew_pos + 1;
1641 let mut lon_end = lon_start;
1642 while lon_end < after2.len() && after2[lon_end].is_ascii_digit() {
1643 lon_end += 1;
1644 }
1645 let lon_digits = &after2[lon_start..lon_end];
1646
1647 if let (Ok(lat_s), Ok(lon_s)) = (
1648 std::str::from_utf8(lat_digits),
1649 std::str::from_utf8(lon_digits),
1650 ) {
1651 if let (Ok(lat_raw), Ok(lon_raw)) =
1652 (lat_s.parse::<f64>(), lon_s.parse::<f64>())
1653 {
1654 let lat = lat_raw / 1e4;
1655 let lon = lon_raw / 1e4;
1656 let (lat_dd, lon_dd) = convert_lat_lon(lat, lon);
1657
1658 tags.push(mk_gps_dt(&time));
1659 tags.push(mk_gps_lat(
1660 lat_dd * if lat_ref == b'S' { -1.0 } else { 1.0 },
1661 ));
1662 tags.push(mk_gps_lon(
1663 lon_dd * if lon_ref == b'W' { -1.0 } else { 1.0 },
1664 ));
1665 found = true;
1666
1667 if lon_end + 9 <= after2.len() {
1669 if let Ok(rest) =
1670 std::str::from_utf8(&after2[lon_end..lon_end + 9])
1671 {
1672 if rest.starts_with('+') || rest.starts_with('-') {
1674 if let Ok(alt) = rest[0..5].parse::<f64>() {
1675 tags.push(mk_gps_alt(alt));
1676 }
1677 if let Ok(spd) = rest[5..].parse::<f64>() {
1678 tags.push(mk_gps_spd(spd));
1679 }
1680 }
1681 }
1682 }
1683 }
1684 }
1685 }
1686 }
1687 }
1688 }
1689 pos = start + 40;
1690 } else {
1691 break;
1692 }
1693 }
1694 found
1695}
1696
1697fn scan_mdat_for_freegps(data: &[u8], tags: &mut Vec<Tag>, doc_count: &mut u32) {
1700 let pattern = b"freeGPS ";
1702 let mut pos = 0;
1703 let limit = data.len().min(20_000_000); while pos + 12 < limit {
1706 if let Some(idx) = data[pos..limit].windows(8).position(|w| w == pattern) {
1707 let abs_pos = pos + idx;
1708 if abs_pos >= 4 {
1710 let atom_start = abs_pos - 4;
1711 let atom_size = u32::from_be_bytes([
1712 data[atom_start],
1713 data[atom_start + 1],
1714 data[atom_start + 2],
1715 data[atom_start + 3],
1716 ]) as usize;
1717 let atom_size = if atom_size < 12 { 12 } else { atom_size };
1718 let end = (atom_start + atom_size).min(data.len());
1719 let block = &data[atom_start..end];
1720
1721 let mut sample_tags = Vec::new();
1722 if process_freegps(block, &mut sample_tags) && !sample_tags.is_empty() {
1723 *doc_count += 1;
1724 for t in &mut sample_tags {
1725 t.group.family3 = format!("Doc{}", doc_count);
1726 }
1727 tags.extend(sample_tags);
1728 }
1729 pos = end;
1730 } else {
1731 pos = abs_pos + 8;
1732 }
1733 } else {
1734 break;
1735 }
1736 }
1737}
1738
1739fn is_ns(b: u8) -> bool {
1742 b == b'N' || b == b'S'
1743}
1744fn is_ew(b: u8) -> bool {
1745 b == b'E' || b == b'W'
1746}
1747
1748fn convert_lat_lon(lat: f64, lon: f64) -> (f64, f64) {
1750 let lat_deg = (lat / 100.0).floor();
1751 let lat_dd = lat_deg + (lat - lat_deg * 100.0) / 60.0;
1752 let lon_deg = (lon / 100.0).floor();
1753 let lon_dd = lon_deg + (lon - lon_deg * 100.0) / 60.0;
1754 (lat_dd, lon_dd)
1755}
1756
1757fn signed_u32(v: u32) -> i32 {
1758 v as i32 }
1760
1761fn get_u16_be(data: &[u8], off: usize) -> u16 {
1762 u16::from_be_bytes([data[off], data[off + 1]])
1763}
1764
1765fn get_u16_le(data: &[u8], off: usize) -> u16 {
1766 u16::from_le_bytes([data[off], data[off + 1]])
1767}
1768
1769fn get_u32_be(data: &[u8], off: usize) -> u32 {
1770 u32::from_be_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]])
1771}
1772
1773fn get_u32_le(data: &[u8], off: usize) -> u32 {
1774 u32::from_le_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]])
1775}
1776
1777fn get_i32_be(data: &[u8], off: usize) -> i32 {
1778 i32::from_be_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]])
1779}
1780
1781fn get_i32_le(data: &[u8], off: usize) -> i32 {
1782 i32::from_le_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]])
1783}
1784
1785fn get_i16_be(data: &[u8], off: usize) -> i16 {
1786 i16::from_be_bytes([data[off], data[off + 1]])
1787}
1788
1789fn get_i16_le(data: &[u8], off: usize) -> i16 {
1790 i16::from_le_bytes([data[off], data[off + 1]])
1791}
1792
1793fn get_f32_le(data: &[u8], off: usize) -> f32 {
1794 f32::from_le_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]])
1795}
1796
1797fn get_f64_le(data: &[u8], off: usize) -> f64 {
1798 f64::from_le_bytes([
1799 data[off],
1800 data[off + 1],
1801 data[off + 2],
1802 data[off + 3],
1803 data[off + 4],
1804 data[off + 5],
1805 data[off + 6],
1806 data[off + 7],
1807 ])
1808}
1809
1810fn try_ascii_digits(data: &[u8], max_len: usize) -> Option<String> {
1811 let end = data.len().min(max_len);
1812 let slice = &data[..end];
1813 if slice.iter().all(|b| b.is_ascii_digit()) {
1814 Some(crate::encoding::decode_utf8_or_latin1(slice).to_string())
1815 } else {
1816 None
1817 }
1818}
1819
1820fn mk_stream(name: &str, description: &str, value: Value) -> Tag {
1823 let print_value = value.to_display_string();
1824 Tag {
1825 id: TagId::Text(name.to_string()),
1826 name: name.to_string(),
1827 description: description.to_string(),
1828 group: TagGroup {
1829 family0: "QuickTime".into(),
1830 family1: "QuickTime".into(),
1831 family2: "Location".into(),
1832 family3: "Main".into(),
1833 },
1834 raw_value: value,
1835 print_value,
1836 priority: 0,
1837 }
1838}
1839
1840fn mk_gps_dt(dt: &str) -> Tag {
1841 Tag {
1842 id: TagId::Text("GPSDateTime".into()),
1843 name: "GPSDateTime".into(),
1844 description: "GPS Date/Time".into(),
1845 group: TagGroup {
1846 family0: "QuickTime".into(),
1847 family1: "QuickTime".into(),
1848 family2: "Time".into(),
1849 family3: "Main".into(),
1850 },
1851 raw_value: Value::String(dt.to_string()),
1852 print_value: dt.to_string(),
1853 priority: 0,
1854 }
1855}
1856
1857fn mk_gps_lat(val: f64) -> Tag {
1858 let abs_val = val.abs();
1859 let d = abs_val.floor() as u32;
1860 let m_total = (abs_val - d as f64) * 60.0;
1861 let m = m_total.floor() as u32;
1862 let s = (m_total - m as f64) * 60.0;
1863 let ref_c = if val >= 0.0 { "N" } else { "S" };
1864 let print = format!("{} deg {}' {:.2}\" {}", d, m, s, ref_c);
1865 Tag {
1866 id: TagId::Text("GPSLatitude".into()),
1867 name: "GPSLatitude".into(),
1868 description: "GPS Latitude".into(),
1869 group: TagGroup {
1870 family0: "QuickTime".into(),
1871 family1: "QuickTime".into(),
1872 family2: "Location".into(),
1873 family3: "Main".into(),
1874 },
1875 raw_value: Value::F64(val),
1876 print_value: print,
1877 priority: 0,
1878 }
1879}
1880
1881fn mk_gps_lon(val: f64) -> Tag {
1882 let abs_val = val.abs();
1883 let d = abs_val.floor() as u32;
1884 let m_total = (abs_val - d as f64) * 60.0;
1885 let m = m_total.floor() as u32;
1886 let s = (m_total - m as f64) * 60.0;
1887 let ref_c = if val >= 0.0 { "E" } else { "W" };
1888 let print = format!("{} deg {}' {:.2}\" {}", d, m, s, ref_c);
1889 Tag {
1890 id: TagId::Text("GPSLongitude".into()),
1891 name: "GPSLongitude".into(),
1892 description: "GPS Longitude".into(),
1893 group: TagGroup {
1894 family0: "QuickTime".into(),
1895 family1: "QuickTime".into(),
1896 family2: "Location".into(),
1897 family3: "Main".into(),
1898 },
1899 raw_value: Value::F64(val),
1900 print_value: print,
1901 priority: 0,
1902 }
1903}
1904
1905fn mk_gps_alt(val: f64) -> Tag {
1906 Tag {
1907 id: TagId::Text("GPSAltitude".into()),
1908 name: "GPSAltitude".into(),
1909 description: "GPS Altitude".into(),
1910 group: TagGroup {
1911 family0: "QuickTime".into(),
1912 family1: "QuickTime".into(),
1913 family2: "Location".into(),
1914 family3: "Main".into(),
1915 },
1916 raw_value: Value::F64(val),
1917 print_value: format!("{:.4} m", val),
1918 priority: 0,
1919 }
1920}
1921
1922fn mk_gps_spd(val: f64) -> Tag {
1923 Tag {
1924 id: TagId::Text("GPSSpeed".into()),
1925 name: "GPSSpeed".into(),
1926 description: "GPS Speed".into(),
1927 group: TagGroup {
1928 family0: "QuickTime".into(),
1929 family1: "QuickTime".into(),
1930 family2: "Location".into(),
1931 family3: "Main".into(),
1932 },
1933 raw_value: Value::F64(val),
1934 print_value: format!("{:.4}", val),
1935 priority: 0,
1936 }
1937}
1938
1939fn mk_gps_trk(val: f64) -> Tag {
1940 Tag {
1941 id: TagId::Text("GPSTrack".into()),
1942 name: "GPSTrack".into(),
1943 description: "GPS Track".into(),
1944 group: TagGroup {
1945 family0: "QuickTime".into(),
1946 family1: "QuickTime".into(),
1947 family2: "Location".into(),
1948 family3: "Main".into(),
1949 },
1950 raw_value: Value::F64(val),
1951 print_value: format!("{:.4}", val),
1952 priority: 0,
1953 }
1954}