1use crate::error::{SbfError, SbfResult};
4use crate::header::SbfHeader;
5
6use super::block_ids;
7use super::dnu::{f32_or_none, f64_or_none, u8_or_none, F32_DNU};
8use super::SbfBlockParse;
9
10#[cfg(test)]
11use super::dnu::F64_DNU;
12
13#[derive(Debug, Clone)]
21pub struct AttEulerBlock {
22 tow_ms: u32,
23 wnc: u16,
24 nr_sv: u8,
25 error: u8,
26 mode: u16,
27 datum: u8,
28 heading_deg: f32,
29 pitch_deg: f32,
30 roll_deg: f32,
31 pitch_rate_dps: f32,
32 roll_rate_dps: f32,
33 heading_rate_dps: f32,
34}
35
36impl AttEulerBlock {
37 pub fn tow_seconds(&self) -> f64 {
38 self.tow_ms as f64 * 0.001
39 }
40 pub fn tow_ms(&self) -> u32 {
41 self.tow_ms
42 }
43 pub fn wnc(&self) -> u16 {
44 self.wnc
45 }
46
47 pub fn num_satellites(&self) -> u8 {
52 u8_or_none(self.nr_sv).unwrap_or(0)
53 }
54 pub fn num_satellites_opt(&self) -> Option<u8> {
56 u8_or_none(self.nr_sv)
57 }
58 pub fn num_satellites_raw(&self) -> u8 {
60 self.nr_sv
61 }
62 pub fn error_raw(&self) -> u8 {
63 self.error
64 }
65 pub fn mode_raw(&self) -> u16 {
66 self.mode
67 }
68 pub fn datum(&self) -> u8 {
69 self.datum
70 }
71
72 pub fn heading_deg(&self) -> Option<f32> {
73 f32_or_none(self.heading_deg)
74 }
75 pub fn pitch_deg(&self) -> Option<f32> {
76 f32_or_none(self.pitch_deg)
77 }
78 pub fn roll_deg(&self) -> Option<f32> {
79 f32_or_none(self.roll_deg)
80 }
81 pub fn pitch_rate_dps(&self) -> Option<f32> {
82 f32_or_none(self.pitch_rate_dps)
83 }
84 pub fn roll_rate_dps(&self) -> Option<f32> {
85 f32_or_none(self.roll_rate_dps)
86 }
87 pub fn heading_rate_dps(&self) -> Option<f32> {
88 f32_or_none(self.heading_rate_dps)
89 }
90}
91
92impl SbfBlockParse for AttEulerBlock {
93 const BLOCK_ID: u16 = block_ids::ATT_EULER;
94
95 fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
96 if data.len() < 42 {
97 return Err(SbfError::ParseError("AttEuler too short".into()));
98 }
99
100 let nr_sv = data[12];
114 let error = data[13];
115 let mode = u16::from_le_bytes([data[14], data[15]]);
116 let datum = data[16];
117
118 let heading_deg = f32::from_le_bytes(data[18..22].try_into().unwrap());
119 let pitch_deg = f32::from_le_bytes(data[22..26].try_into().unwrap());
120 let roll_deg = f32::from_le_bytes(data[26..30].try_into().unwrap());
121 let pitch_rate_dps = f32::from_le_bytes(data[30..34].try_into().unwrap());
122 let roll_rate_dps = f32::from_le_bytes(data[34..38].try_into().unwrap());
123 let heading_rate_dps = f32::from_le_bytes(data[38..42].try_into().unwrap());
124
125 Ok(Self {
126 tow_ms: header.tow_ms,
127 wnc: header.wnc,
128 nr_sv,
129 error,
130 mode,
131 datum,
132 heading_deg,
133 pitch_deg,
134 roll_deg,
135 pitch_rate_dps,
136 roll_rate_dps,
137 heading_rate_dps,
138 })
139 }
140}
141
142#[derive(Debug, Clone)]
150pub struct AttCovEulerBlock {
151 tow_ms: u32,
152 wnc: u16,
153 error: u8,
154 pub cov_head_head: f32,
156 pub cov_pitch_pitch: f32,
158 pub cov_roll_roll: f32,
160 pub cov_head_pitch: f32,
162 pub cov_head_roll: f32,
164 pub cov_pitch_roll: f32,
166}
167
168impl AttCovEulerBlock {
169 pub fn tow_seconds(&self) -> f64 {
170 self.tow_ms as f64 * 0.001
171 }
172 pub fn tow_ms(&self) -> u32 {
173 self.tow_ms
174 }
175 pub fn wnc(&self) -> u16 {
176 self.wnc
177 }
178
179 pub fn error_raw(&self) -> u8 {
180 self.error
181 }
182
183 pub fn heading_std_deg(&self) -> Option<f32> {
184 if self.cov_head_head == F32_DNU || self.cov_head_head < 0.0 {
185 None
186 } else {
187 Some(self.cov_head_head.sqrt())
188 }
189 }
190 pub fn pitch_std_deg(&self) -> Option<f32> {
191 if self.cov_pitch_pitch == F32_DNU || self.cov_pitch_pitch < 0.0 {
192 None
193 } else {
194 Some(self.cov_pitch_pitch.sqrt())
195 }
196 }
197 pub fn roll_std_deg(&self) -> Option<f32> {
198 if self.cov_roll_roll == F32_DNU || self.cov_roll_roll < 0.0 {
199 None
200 } else {
201 Some(self.cov_roll_roll.sqrt())
202 }
203 }
204}
205
206impl SbfBlockParse for AttCovEulerBlock {
207 const BLOCK_ID: u16 = block_ids::ATT_COV_EULER;
208
209 fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
210 if data.len() < 38 {
211 return Err(SbfError::ParseError("AttCovEuler too short".into()));
212 }
213
214 let error = data[13];
225 let cov_head_head = f32::from_le_bytes(data[14..18].try_into().unwrap());
226 let cov_pitch_pitch = f32::from_le_bytes(data[18..22].try_into().unwrap());
227 let cov_roll_roll = f32::from_le_bytes(data[22..26].try_into().unwrap());
228 let cov_head_pitch = f32::from_le_bytes(data[26..30].try_into().unwrap());
229 let cov_head_roll = f32::from_le_bytes(data[30..34].try_into().unwrap());
230 let cov_pitch_roll = f32::from_le_bytes(data[34..38].try_into().unwrap());
231
232 Ok(Self {
233 tow_ms: header.tow_ms,
234 wnc: header.wnc,
235 error,
236 cov_head_head,
237 cov_pitch_pitch,
238 cov_roll_roll,
239 cov_head_pitch,
240 cov_head_roll,
241 cov_pitch_roll,
242 })
243 }
244}
245
246#[derive(Debug, Clone)]
257pub struct AuxAntPosition {
258 pub nr_sv: u8,
259 pub error: u8,
260 pub ambiguity_type: u8,
261 pub aux_ant_id: u8,
262 d_east_m: f64,
263 d_north_m: f64,
264 d_up_m: f64,
265 east_vel_mps: f64,
266 north_vel_mps: f64,
267 up_vel_mps: f64,
268}
269
270impl AuxAntPosition {
271 pub fn d_east_m(&self) -> Option<f64> {
272 f64_or_none(self.d_east_m)
273 }
274 pub fn d_north_m(&self) -> Option<f64> {
275 f64_or_none(self.d_north_m)
276 }
277 pub fn d_up_m(&self) -> Option<f64> {
278 f64_or_none(self.d_up_m)
279 }
280 pub fn velocity_east_mps(&self) -> Option<f64> {
281 f64_or_none(self.east_vel_mps)
282 }
283 pub fn velocity_north_mps(&self) -> Option<f64> {
284 f64_or_none(self.north_vel_mps)
285 }
286 pub fn velocity_up_mps(&self) -> Option<f64> {
287 f64_or_none(self.up_vel_mps)
288 }
289}
290
291#[derive(Debug, Clone)]
293pub struct AuxAntPositionsBlock {
294 tow_ms: u32,
295 wnc: u16,
296 pub positions: Vec<AuxAntPosition>,
297}
298
299impl AuxAntPositionsBlock {
300 pub fn tow_seconds(&self) -> f64 {
301 self.tow_ms as f64 * 0.001
302 }
303 pub fn tow_ms(&self) -> u32 {
304 self.tow_ms
305 }
306 pub fn wnc(&self) -> u16 {
307 self.wnc
308 }
309
310 pub fn num_positions(&self) -> usize {
311 self.positions.len()
312 }
313}
314
315impl SbfBlockParse for AuxAntPositionsBlock {
316 const BLOCK_ID: u16 = block_ids::AUX_ANT_POSITIONS;
317
318 fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
319 if data.len() < 14 {
320 return Err(SbfError::ParseError("AuxAntPositions too short".into()));
321 }
322
323 let n = data[12] as usize;
324 let sb_length = data[13] as usize;
325
326 if sb_length < 52 {
327 return Err(SbfError::ParseError(
328 "AuxAntPositions SBLength too small".into(),
329 ));
330 }
331
332 let mut positions = Vec::new();
333 let mut offset = 14;
334
335 for _ in 0..n {
336 if offset + sb_length > data.len() {
337 break;
338 }
339
340 let nr_sv = data[offset];
341 let error = data[offset + 1];
342 let ambiguity_type = data[offset + 2];
343 let aux_ant_id = data[offset + 3];
344
345 let d_east_m = f64::from_le_bytes(data[offset + 4..offset + 12].try_into().unwrap());
346 let d_north_m = f64::from_le_bytes(data[offset + 12..offset + 20].try_into().unwrap());
347 let d_up_m = f64::from_le_bytes(data[offset + 20..offset + 28].try_into().unwrap());
348 let east_vel_mps =
349 f64::from_le_bytes(data[offset + 28..offset + 36].try_into().unwrap());
350 let north_vel_mps =
351 f64::from_le_bytes(data[offset + 36..offset + 44].try_into().unwrap());
352 let up_vel_mps = f64::from_le_bytes(data[offset + 44..offset + 52].try_into().unwrap());
353
354 positions.push(AuxAntPosition {
355 nr_sv,
356 error,
357 ambiguity_type,
358 aux_ant_id,
359 d_east_m,
360 d_north_m,
361 d_up_m,
362 east_vel_mps,
363 north_vel_mps,
364 up_vel_mps,
365 });
366
367 offset += sb_length;
368 }
369
370 Ok(Self {
371 tow_ms: header.tow_ms,
372 wnc: header.wnc,
373 positions,
374 })
375 }
376}
377
378#[derive(Debug, Clone)]
384pub struct EndOfAttBlock {
385 tow_ms: u32,
386 wnc: u16,
387}
388
389impl EndOfAttBlock {
390 pub fn tow_seconds(&self) -> f64 {
391 self.tow_ms as f64 * 0.001
392 }
393 pub fn tow_ms(&self) -> u32 {
394 self.tow_ms
395 }
396 pub fn wnc(&self) -> u16 {
397 self.wnc
398 }
399}
400
401impl SbfBlockParse for EndOfAttBlock {
402 const BLOCK_ID: u16 = block_ids::END_OF_ATT;
403
404 fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
405 if data.len() < 12 {
406 return Err(SbfError::ParseError("EndOfAtt too short".into()));
407 }
408
409 Ok(Self {
410 tow_ms: header.tow_ms,
411 wnc: header.wnc,
412 })
413 }
414}
415
416#[cfg(test)]
417mod tests {
418 use super::*;
419 use crate::header::SbfHeader;
420
421 fn header_for(block_id: u16, data_len: usize, tow_ms: u32, wnc: u16) -> SbfHeader {
422 SbfHeader {
423 crc: 0,
424 block_id,
425 block_rev: 0,
426 length: (data_len + 2) as u16,
427 tow_ms,
428 wnc,
429 }
430 }
431
432 #[test]
433 fn test_att_euler_scaled_accessors() {
434 let block = AttEulerBlock {
435 tow_ms: 1000,
436 wnc: 2000,
437 nr_sv: 12,
438 error: 0,
439 mode: 3,
440 datum: 1,
441 heading_deg: 45.0,
442 pitch_deg: -2.0,
443 roll_deg: 1.5,
444 pitch_rate_dps: 0.1,
445 roll_rate_dps: 0.2,
446 heading_rate_dps: -0.3,
447 };
448
449 assert!((block.tow_seconds() - 1.0).abs() < 1e-6);
450 assert_eq!(block.num_satellites(), 12);
451 assert_eq!(block.mode_raw(), 3);
452 assert!((block.heading_deg().unwrap() - 45.0).abs() < 1e-6);
453 assert!((block.roll_rate_dps().unwrap() - 0.2).abs() < 1e-6);
454 }
455
456 #[test]
457 fn test_att_euler_dnu_handling() {
458 let block = AttEulerBlock {
459 tow_ms: 0,
460 wnc: 0,
461 nr_sv: 255,
462 error: 0,
463 mode: 0,
464 datum: 0,
465 heading_deg: F32_DNU,
466 pitch_deg: F32_DNU,
467 roll_deg: 1.0,
468 pitch_rate_dps: F32_DNU,
469 roll_rate_dps: 0.0,
470 heading_rate_dps: F32_DNU,
471 };
472
473 assert_eq!(block.num_satellites_raw(), 255);
474 assert_eq!(block.num_satellites_opt(), None);
475 assert_eq!(block.num_satellites(), 0);
476 assert!(block.heading_deg().is_none());
477 assert!(block.pitch_deg().is_none());
478 assert!(block.roll_deg().is_some());
479 assert!(block.pitch_rate_dps().is_none());
480 assert!(block.heading_rate_dps().is_none());
481 }
482
483 #[test]
484 fn test_att_euler_parse() {
485 let mut data = vec![0u8; 42];
486 data[12] = 8; data[13] = 1; data[14..16].copy_from_slice(&500_u16.to_le_bytes());
489 data[16] = 2; data[18..22].copy_from_slice(&10.5_f32.to_le_bytes());
491 data[22..26].copy_from_slice(&(-1.25_f32).to_le_bytes());
492 data[26..30].copy_from_slice(&0.75_f32.to_le_bytes());
493 data[30..34].copy_from_slice(&0.1_f32.to_le_bytes());
494 data[34..38].copy_from_slice(&0.2_f32.to_le_bytes());
495 data[38..42].copy_from_slice(&0.3_f32.to_le_bytes());
496
497 let header = header_for(block_ids::ATT_EULER, data.len(), 123456, 2048);
498 let block = AttEulerBlock::parse(&header, &data).unwrap();
499
500 assert_eq!(block.num_satellites(), 8);
501 assert_eq!(block.num_satellites_opt(), Some(8));
502 assert_eq!(block.num_satellites_raw(), 8);
503 assert_eq!(block.error_raw(), 1);
504 assert_eq!(block.mode_raw(), 500);
505 assert!((block.heading_deg().unwrap() - 10.5).abs() < 1e-6);
506 assert!((block.pitch_rate_dps().unwrap() - 0.1).abs() < 1e-6);
507 }
508
509 #[test]
510 fn test_att_cov_euler_std_accessors() {
511 let block = AttCovEulerBlock {
512 tow_ms: 2000,
513 wnc: 100,
514 error: 0,
515 cov_head_head: 4.0,
516 cov_pitch_pitch: 9.0,
517 cov_roll_roll: 16.0,
518 cov_head_pitch: 0.0,
519 cov_head_roll: 0.0,
520 cov_pitch_roll: 0.0,
521 };
522
523 assert!((block.heading_std_deg().unwrap() - 2.0).abs() < 1e-6);
524 assert!((block.pitch_std_deg().unwrap() - 3.0).abs() < 1e-6);
525 assert!((block.roll_std_deg().unwrap() - 4.0).abs() < 1e-6);
526 assert!((block.tow_seconds() - 2.0).abs() < 1e-6);
527 }
528
529 #[test]
530 fn test_att_cov_euler_dnu_handling() {
531 let block = AttCovEulerBlock {
532 tow_ms: 0,
533 wnc: 0,
534 error: 0,
535 cov_head_head: F32_DNU,
536 cov_pitch_pitch: -1.0,
537 cov_roll_roll: 1.0,
538 cov_head_pitch: 0.0,
539 cov_head_roll: 0.0,
540 cov_pitch_roll: 0.0,
541 };
542
543 assert!(block.heading_std_deg().is_none());
544 assert!(block.pitch_std_deg().is_none());
545 assert!(block.roll_std_deg().is_some());
546 }
547
548 #[test]
549 fn test_att_cov_euler_parse() {
550 let mut data = vec![0u8; 38];
551 data[13] = 2; data[14..18].copy_from_slice(&1.0_f32.to_le_bytes());
553 data[18..22].copy_from_slice(&4.0_f32.to_le_bytes());
554 data[22..26].copy_from_slice(&9.0_f32.to_le_bytes());
555 data[26..30].copy_from_slice(&0.1_f32.to_le_bytes());
556 data[30..34].copy_from_slice(&0.2_f32.to_le_bytes());
557 data[34..38].copy_from_slice(&0.3_f32.to_le_bytes());
558
559 let header = header_for(block_ids::ATT_COV_EULER, data.len(), 654321, 1024);
560 let block = AttCovEulerBlock::parse(&header, &data).unwrap();
561
562 assert_eq!(block.error_raw(), 2);
563 assert!((block.heading_std_deg().unwrap() - 1.0).abs() < 1e-6);
564 }
565
566 #[test]
567 fn test_aux_ant_positions_accessors() {
568 let info = AuxAntPosition {
569 nr_sv: 7,
570 error: 0,
571 ambiguity_type: 1,
572 aux_ant_id: 2,
573 d_east_m: 1.5,
574 d_north_m: -2.5,
575 d_up_m: 0.5,
576 east_vel_mps: 0.1,
577 north_vel_mps: -0.2,
578 up_vel_mps: 0.0,
579 };
580
581 assert_eq!(info.nr_sv, 7);
582 assert!((info.d_east_m().unwrap() - 1.5).abs() < 1e-6);
583 assert!((info.velocity_north_mps().unwrap() + 0.2).abs() < 1e-6);
584 }
585
586 #[test]
587 fn test_aux_ant_positions_dnu_handling() {
588 let info = AuxAntPosition {
589 nr_sv: 0,
590 error: 0,
591 ambiguity_type: 0,
592 aux_ant_id: 0,
593 d_east_m: F64_DNU,
594 d_north_m: 1.0,
595 d_up_m: F64_DNU,
596 east_vel_mps: F64_DNU,
597 north_vel_mps: 0.0,
598 up_vel_mps: F64_DNU,
599 };
600
601 assert!(info.d_east_m().is_none());
602 assert!(info.d_up_m().is_none());
603 assert!(info.velocity_east_mps().is_none());
604 assert!(info.velocity_up_mps().is_none());
605 assert!(info.velocity_north_mps().is_some());
606 }
607
608 #[test]
609 fn test_aux_ant_positions_parse() {
610 let mut data = vec![0u8; 14 + 52];
611 data[12] = 1; data[13] = 52; let offset = 14;
615 data[offset] = 5; data[offset + 1] = 1; data[offset + 2] = 2; data[offset + 3] = 3; data[offset + 4..offset + 12].copy_from_slice(&1.0_f64.to_le_bytes());
620 data[offset + 12..offset + 20].copy_from_slice(&2.0_f64.to_le_bytes());
621 data[offset + 20..offset + 28].copy_from_slice(&3.0_f64.to_le_bytes());
622 data[offset + 28..offset + 36].copy_from_slice(&0.1_f64.to_le_bytes());
623 data[offset + 36..offset + 44].copy_from_slice(&0.2_f64.to_le_bytes());
624 data[offset + 44..offset + 52].copy_from_slice(&0.3_f64.to_le_bytes());
625
626 let header = header_for(block_ids::AUX_ANT_POSITIONS, data.len(), 9999, 55);
627 let block = AuxAntPositionsBlock::parse(&header, &data).unwrap();
628
629 assert_eq!(block.num_positions(), 1);
630 let entry = &block.positions[0];
631 assert_eq!(entry.aux_ant_id, 3);
632 assert!((entry.d_north_m().unwrap() - 2.0).abs() < 1e-6);
633 }
634
635 #[test]
636 fn test_end_of_att_parse() {
637 let data = vec![0u8; 14];
638 let header = header_for(block_ids::END_OF_ATT, data.len(), 2500, 42);
639 let block = EndOfAttBlock::parse(&header, &data).unwrap();
640
641 assert!((block.tow_seconds() - 2.5).abs() < 1e-6);
642 assert_eq!(block.wnc(), 42);
643 }
644}