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