1#![allow(dead_code)]
10#![allow(clippy::too_many_arguments)]
11
12use std::collections::VecDeque;
13use std::io::{BufWriter, Write};
14
15#[derive(Debug, Clone, PartialEq)]
17pub struct ImuDataRecord {
18 pub timestamp: f64,
20 pub accel: [f64; 3],
22 pub gyro: [f64; 3],
24 pub mag: [f64; 3],
26}
27
28impl ImuDataRecord {
29 pub fn new(timestamp: f64, accel: [f64; 3], gyro: [f64; 3], mag: [f64; 3]) -> Self {
31 Self {
32 timestamp,
33 accel,
34 gyro,
35 mag,
36 }
37 }
38
39 pub fn to_bytes(&self) -> Vec<u8> {
41 let mut buf = Vec::with_capacity(80);
42 buf.extend_from_slice(&self.timestamp.to_le_bytes());
43 for &v in &self.accel {
44 buf.extend_from_slice(&v.to_le_bytes());
45 }
46 for &v in &self.gyro {
47 buf.extend_from_slice(&v.to_le_bytes());
48 }
49 for &v in &self.mag {
50 buf.extend_from_slice(&v.to_le_bytes());
51 }
52 buf
53 }
54
55 pub fn from_bytes(data: &[u8]) -> Option<Self> {
57 if data.len() < 80 {
58 return None;
59 }
60 let mut off = 0usize;
61 let read_f64 = |d: &[u8], o: &mut usize| -> f64 {
62 let v = f64::from_le_bytes(d[*o..*o + 8].try_into().unwrap_or([0u8; 8]));
63 *o += 8;
64 v
65 };
66 let timestamp = read_f64(data, &mut off);
67 let accel = [
68 read_f64(data, &mut off),
69 read_f64(data, &mut off),
70 read_f64(data, &mut off),
71 ];
72 let gyro = [
73 read_f64(data, &mut off),
74 read_f64(data, &mut off),
75 read_f64(data, &mut off),
76 ];
77 let mag = [
78 read_f64(data, &mut off),
79 read_f64(data, &mut off),
80 read_f64(data, &mut off),
81 ];
82 Some(Self {
83 timestamp,
84 accel,
85 gyro,
86 mag,
87 })
88 }
89}
90
91#[derive(Debug, Clone, PartialEq)]
93pub struct GpsDataRecord {
94 pub timestamp: f64,
96 pub lat: f64,
98 pub lon: f64,
100 pub alt: f64,
102 pub velocity: [f64; 3],
104 pub hdop: f64,
106}
107
108impl GpsDataRecord {
109 pub fn new(
111 timestamp: f64,
112 lat: f64,
113 lon: f64,
114 alt: f64,
115 velocity: [f64; 3],
116 hdop: f64,
117 ) -> Self {
118 Self {
119 timestamp,
120 lat,
121 lon,
122 alt,
123 velocity,
124 hdop,
125 }
126 }
127
128 pub fn to_bytes(&self) -> Vec<u8> {
130 let mut buf = Vec::with_capacity(64);
131 for v in [
132 self.timestamp,
133 self.lat,
134 self.lon,
135 self.alt,
136 self.velocity[0],
137 self.velocity[1],
138 self.velocity[2],
139 self.hdop,
140 ] {
141 buf.extend_from_slice(&v.to_le_bytes());
142 }
143 buf
144 }
145
146 pub fn ground_speed(&self) -> f64 {
148 (self.velocity[0] * self.velocity[0] + self.velocity[1] * self.velocity[1]).sqrt()
149 }
150}
151
152#[derive(Debug, Clone)]
154pub struct LidarFrame {
155 pub timestamp: f64,
157 pub n_points: usize,
159 pub positions: Vec<[f32; 3]>,
161 pub intensities: Vec<f32>,
163}
164
165impl LidarFrame {
166 pub fn new(timestamp: f64) -> Self {
168 Self {
169 timestamp,
170 n_points: 0,
171 positions: Vec::new(),
172 intensities: Vec::new(),
173 }
174 }
175
176 pub fn add_point(&mut self, pos: [f32; 3], intensity: f32) {
178 self.positions.push(pos);
179 self.intensities.push(intensity);
180 self.n_points += 1;
181 }
182
183 pub fn to_bytes(&self) -> Vec<u8> {
185 let mut buf = Vec::new();
186 buf.extend_from_slice(&self.timestamp.to_le_bytes());
187 buf.extend_from_slice(&(self.n_points as u32).to_le_bytes());
188 for p in &self.positions {
189 for &v in p {
190 buf.extend_from_slice(&v.to_le_bytes());
191 }
192 }
193 for &intensity in &self.intensities {
194 buf.extend_from_slice(&intensity.to_le_bytes());
195 }
196 buf
197 }
198
199 pub fn bounding_box(&self) -> [[f32; 2]; 3] {
201 let mut bb = [[f32::MAX, f32::MIN]; 3];
202 for p in &self.positions {
203 for axis in 0..3 {
204 bb[axis][0] = bb[axis][0].min(p[axis]);
205 bb[axis][1] = bb[axis][1].max(p[axis]);
206 }
207 }
208 bb
209 }
210}
211
212#[derive(Debug, Clone)]
214pub struct ForceGaugeRecord {
215 pub timestamp: f64,
217 pub force: [f64; 3],
219 pub torque: [f64; 3],
221 pub temperature: f64,
223}
224
225impl ForceGaugeRecord {
226 pub fn new(timestamp: f64, force: [f64; 3], torque: [f64; 3], temperature: f64) -> Self {
228 Self {
229 timestamp,
230 force,
231 torque,
232 temperature,
233 }
234 }
235
236 pub fn force_magnitude(&self) -> f64 {
238 let f = &self.force;
239 (f[0] * f[0] + f[1] * f[1] + f[2] * f[2]).sqrt()
240 }
241
242 pub fn torque_magnitude(&self) -> f64 {
244 let t = &self.torque;
245 (t[0] * t[0] + t[1] * t[1] + t[2] * t[2]).sqrt()
246 }
247
248 pub fn to_bytes(&self) -> Vec<u8> {
250 let mut buf = Vec::with_capacity(56);
251 for v in [
252 self.timestamp,
253 self.force[0],
254 self.force[1],
255 self.force[2],
256 self.torque[0],
257 self.torque[1],
258 self.torque[2],
259 self.temperature,
260 ] {
261 buf.extend_from_slice(&v.to_le_bytes());
262 }
263 buf
264 }
265}
266
267#[derive(Debug, Clone)]
271pub struct TelemetryProtocol {
272 pub magic: u8,
274 pub version: u8,
276 pub sys_id: u8,
278 pub comp_id: u8,
280}
281
282#[derive(Debug, Clone)]
284pub struct TelemetryMessage {
285 pub message_id: u16,
287 pub seq: u8,
289 pub payload: Vec<u8>,
291}
292
293impl TelemetryProtocol {
294 pub fn new(sys_id: u8, comp_id: u8) -> Self {
296 Self {
297 magic: 0xFE,
298 version: 1,
299 sys_id,
300 comp_id,
301 }
302 }
303
304 pub fn encode(&self, msg: &TelemetryMessage) -> Vec<u8> {
306 let payload_len = msg.payload.len() as u8;
307 let mut buf = vec![self.magic, payload_len, msg.seq, self.sys_id, self.comp_id];
308 buf.extend_from_slice(&msg.message_id.to_le_bytes());
309 buf.extend_from_slice(&msg.payload);
310 let crc = self.crc16(&buf[1..]);
311 buf.extend_from_slice(&crc.to_le_bytes());
312 buf
313 }
314
315 pub fn decode(&self, data: &[u8]) -> Option<TelemetryMessage> {
317 if data.len() < 8 {
318 return None;
319 }
320 if data[0] != self.magic {
321 return None;
322 }
323 let payload_len = data[1] as usize;
324 if data.len() < 7 + payload_len + 2 {
325 return None;
326 }
327 let seq = data[2];
328 let _sys_id = data[3];
329 let _comp_id = data[4];
330 let message_id = u16::from_le_bytes([data[5], data[6]]);
331 let payload = data[7..7 + payload_len].to_vec();
332 Some(TelemetryMessage {
333 message_id,
334 seq,
335 payload,
336 })
337 }
338
339 fn crc16(&self, data: &[u8]) -> u16 {
341 let mut crc = 0xFFFFu16;
342 for &byte in data {
343 let tmp = byte as u16 ^ (crc & 0xFF);
344 let tmp = tmp ^ (tmp << 4);
345 crc = (crc >> 8) ^ (tmp << 8) ^ (tmp << 3) ^ (tmp >> 4);
346 }
347 crc
348 }
349}
350
351#[derive(Debug, Clone)]
355pub struct SensorFusion {
356 pub quaternion: [f64; 4],
358 pub alpha: f64,
360 pub beta: f64,
362 pub euler: [f64; 3],
364}
365
366impl SensorFusion {
367 pub fn new(alpha: f64, beta: f64) -> Self {
369 Self {
370 quaternion: [1.0, 0.0, 0.0, 0.0],
371 alpha,
372 beta,
373 euler: [0.0; 3],
374 }
375 }
376
377 pub fn update_complementary(&mut self, accel: [f64; 3], gyro: [f64; 3], dt: f64) {
379 let [w, x, y, z] = self.quaternion;
381 let [gx, gy, gz] = gyro;
382 let dw = 0.5 * (-x * gx - y * gy - z * gz);
383 let dx = 0.5 * (w * gx + y * gz - z * gy);
384 let dy = 0.5 * (w * gy - x * gz + z * gx);
385 let dz = 0.5 * (w * gz + x * gy - y * gx);
386 let qw = w + dw * dt;
387 let qx = x + dx * dt;
388 let qy = y + dy * dt;
389 let qz = z + dz * dt;
390 let mag = (qw * qw + qx * qx + qy * qy + qz * qz).sqrt().max(1e-10);
391 let q_gyro = [qw / mag, qx / mag, qy / mag, qz / mag];
392
393 let ax = accel[0];
395 let ay = accel[1];
396 let az = accel[2];
397 let norm_a = (ax * ax + ay * ay + az * az).sqrt().max(1e-10);
398 let roll = (ay / norm_a).atan2((az / norm_a).max(1e-10));
399 let pitch = -(ax / norm_a)
400 .asin()
401 .clamp(-std::f64::consts::FRAC_PI_2, std::f64::consts::FRAC_PI_2);
402 let q_accel = euler_to_quat(roll, pitch, 0.0);
403
404 let a = self.alpha;
406 let q_blend = quat_slerp(q_accel, q_gyro, a);
407 self.quaternion = q_blend;
408 self.euler = quat_to_euler(self.quaternion);
409 }
410
411 pub fn update_madgwick(&mut self, accel: [f64; 3], gyro: [f64; 3], dt: f64) {
413 let [mut qw, mut qx, mut qy, mut qz] = self.quaternion;
414 let [gx, gy, gz] = gyro;
415 let [ax, ay, az] = accel;
416
417 let norm_a = (ax * ax + ay * ay + az * az).sqrt().max(1e-10);
418 let (ax, ay, az) = (ax / norm_a, ay / norm_a, az / norm_a);
419
420 let f1 = 2.0 * (qx * qz - qw * qy) - ax;
422 let f2 = 2.0 * (qw * qx + qy * qz) - ay;
423 let f3 = 1.0 - 2.0 * (qx * qx + qy * qy) - az;
424 let j11 = 2.0 * qy;
425 let j12 = 2.0 * qz;
426 let j13 = 2.0 * qw;
427 let j14 = 2.0 * qx;
428 let j21 = 2.0 * qx;
429 let j22 = 2.0 * qw;
430 let j23 = 2.0 * qz;
431 let j24 = 2.0 * qy;
432 let j31 = 0.0;
433 let j32 = 4.0 * qx;
434 let j33 = 4.0 * qy;
435 let j34 = 0.0;
436 let step_w = j11 * f1 + j21 * f2 + j31 * f3;
437 let step_x = j12 * f1 + j22 * f2 + j32 * f3;
438 let step_y = j13 * f1 + j23 * f2 + j33 * f3;
439 let step_z = j14 * f1 + j24 * f2 + j34 * f3;
440 let norm_s = (step_w * step_w + step_x * step_x + step_y * step_y + step_z * step_z)
441 .sqrt()
442 .max(1e-10);
443 let (sw, sx, sy, sz) = (
444 step_w / norm_s,
445 step_x / norm_s,
446 step_y / norm_s,
447 step_z / norm_s,
448 );
449
450 let qdotw = 0.5 * (-qx * gx - qy * gy - qz * gz) - self.beta * sw;
452 let qdotx = 0.5 * (qw * gx + qy * gz - qz * gy) - self.beta * sx;
453 let qdoty = 0.5 * (qw * gy - qx * gz + qz * gx) - self.beta * sy;
454 let qdotz = 0.5 * (qw * gz + qx * gy - qy * gx) - self.beta * sz;
455
456 qw += qdotw * dt;
457 qx += qdotx * dt;
458 qy += qdoty * dt;
459 qz += qdotz * dt;
460 let norm_q = (qw * qw + qx * qx + qy * qy + qz * qz).sqrt().max(1e-10);
461 self.quaternion = [qw / norm_q, qx / norm_q, qy / norm_q, qz / norm_q];
462 self.euler = quat_to_euler(self.quaternion);
463 }
464
465 pub fn roll(&self) -> f64 {
467 self.euler[0]
468 }
469 pub fn pitch(&self) -> f64 {
471 self.euler[1]
472 }
473 pub fn yaw(&self) -> f64 {
475 self.euler[2]
476 }
477}
478
479#[derive(Debug, Clone)]
483pub struct DataLogger<T: Clone> {
484 buffer: VecDeque<T>,
486 capacity: usize,
488 flushed: Vec<T>,
490}
491
492impl<T: Clone> DataLogger<T> {
493 pub fn new(capacity: usize) -> Self {
495 Self {
496 buffer: VecDeque::with_capacity(capacity),
497 capacity,
498 flushed: Vec::new(),
499 }
500 }
501
502 pub fn log(&mut self, record: T) {
504 if self.buffer.len() >= self.capacity {
505 self.flush();
506 }
507 self.buffer.push_back(record);
508 }
509
510 pub fn flush(&mut self) {
512 self.flushed.extend(self.buffer.drain(..));
513 }
514
515 pub fn total_count(&self) -> usize {
517 self.buffer.len() + self.flushed.len()
518 }
519
520 pub fn buffer_size(&self) -> usize {
522 self.buffer.len()
523 }
524
525 pub fn flushed_records(&self) -> &[T] {
527 &self.flushed
528 }
529
530 pub fn pop(&mut self) -> Option<T> {
532 self.buffer.pop_front()
533 }
534
535 pub fn replay(&self) -> Vec<T> {
537 let mut all = self.flushed.clone();
538 all.extend(self.buffer.iter().cloned());
539 all
540 }
541}
542
543#[derive(Debug)]
545pub struct SensorDataWriter {
546 pub buffer: Vec<u8>,
548 pub n_records: usize,
550 pub sensor_type: SensorType,
552}
553
554#[derive(Debug, Clone, Copy, PartialEq)]
556pub enum SensorType {
557 Imu,
559 Gps,
561 Lidar,
563 ForceGauge,
565}
566
567impl SensorDataWriter {
568 pub fn new(sensor_type: SensorType) -> Self {
570 Self {
571 buffer: Vec::new(),
572 n_records: 0,
573 sensor_type,
574 }
575 }
576
577 pub fn write_imu(&mut self, record: &ImuDataRecord) {
579 self.buffer.extend_from_slice(&record.to_bytes());
580 self.n_records += 1;
581 }
582
583 pub fn write_gps(&mut self, record: &GpsDataRecord) {
585 self.buffer.extend_from_slice(&record.to_bytes());
586 self.n_records += 1;
587 }
588
589 pub fn write_force(&mut self, record: &ForceGaugeRecord) {
591 self.buffer.extend_from_slice(&record.to_bytes());
592 self.n_records += 1;
593 }
594
595 pub fn write_lidar(&mut self, frame: &LidarFrame) {
597 self.buffer.extend_from_slice(&frame.to_bytes());
598 self.n_records += 1;
599 }
600
601 pub fn clear(&mut self) {
603 self.buffer.clear();
604 self.n_records = 0;
605 }
606
607 pub fn bytes_written(&self) -> usize {
609 self.buffer.len()
610 }
611}
612
613#[derive(Debug)]
615pub struct SensorDataReader {
616 pub data: Vec<u8>,
618 pub sensor_type: SensorType,
620 pub pos: usize,
622}
623
624impl SensorDataReader {
625 pub fn new(data: Vec<u8>, sensor_type: SensorType) -> Self {
627 Self {
628 data,
629 sensor_type,
630 pos: 0,
631 }
632 }
633
634 pub fn read_imu(&mut self) -> Option<ImuDataRecord> {
636 let record = ImuDataRecord::from_bytes(&self.data[self.pos..])?;
637 self.pos += 80;
638 Some(record)
639 }
640
641 pub fn has_more(&self) -> bool {
643 self.pos < self.data.len()
644 }
645
646 pub fn seek(&mut self, pos: usize) {
648 self.pos = pos.min(self.data.len());
649 }
650
651 pub fn remaining(&self) -> usize {
653 self.data.len().saturating_sub(self.pos)
654 }
655}
656
657#[derive(Debug, Clone)]
661pub struct CsvTelemetry {
662 pub headers: Vec<String>,
664 pub rows: Vec<Vec<String>>,
666}
667
668impl CsvTelemetry {
669 pub fn new(headers: Vec<String>) -> Self {
671 Self {
672 headers,
673 rows: Vec::new(),
674 }
675 }
676
677 pub fn append_row(&mut self, values: Vec<String>) {
679 self.rows.push(values);
680 }
681
682 pub fn append_imu(&mut self, r: &ImuDataRecord) {
684 self.rows.push(vec![
685 r.timestamp.to_string(),
686 r.accel[0].to_string(),
687 r.accel[1].to_string(),
688 r.accel[2].to_string(),
689 r.gyro[0].to_string(),
690 r.gyro[1].to_string(),
691 r.gyro[2].to_string(),
692 ]);
693 }
694
695 pub fn to_csv_string(&self) -> String {
697 let mut out = self.headers.join(",") + "\n";
698 for row in &self.rows {
699 out += &(row.join(",") + "\n");
700 }
701 out
702 }
703
704 pub fn n_rows(&self) -> usize {
706 self.rows.len()
707 }
708
709 pub fn write_to<W: Write>(&self, writer: &mut BufWriter<W>) -> std::io::Result<()> {
711 writer.write_all(self.to_csv_string().as_bytes())
712 }
713}
714
715fn euler_to_quat(roll: f64, pitch: f64, yaw: f64) -> [f64; 4] {
719 let (cr, sr) = ((roll * 0.5).cos(), (roll * 0.5).sin());
720 let (cp, sp) = ((pitch * 0.5).cos(), (pitch * 0.5).sin());
721 let (cy, sy) = ((yaw * 0.5).cos(), (yaw * 0.5).sin());
722 [
723 cr * cp * cy + sr * sp * sy,
724 sr * cp * cy - cr * sp * sy,
725 cr * sp * cy + sr * cp * sy,
726 cr * cp * sy - sr * sp * cy,
727 ]
728}
729
730fn quat_to_euler(q: [f64; 4]) -> [f64; 3] {
732 let [w, x, y, z] = q;
733 let roll = (2.0 * (w * x + y * z))
734 .atan2(1.0 - 2.0 * (x * x + y * y))
735 .to_degrees();
736 let sin_pitch = (2.0 * (w * y - z * x)).clamp(-1.0, 1.0);
737 let pitch = sin_pitch.asin().to_degrees();
738 let yaw = (2.0 * (w * z + x * y))
739 .atan2(1.0 - 2.0 * (y * y + z * z))
740 .to_degrees();
741 [roll, pitch, yaw]
742}
743
744fn quat_slerp(q1: [f64; 4], q2: [f64; 4], t: f64) -> [f64; 4] {
746 let dot = q1[0] * q2[0] + q1[1] * q2[1] + q1[2] * q2[2] + q1[3] * q2[3];
747 let dot = dot.clamp(-1.0, 1.0);
748 if dot.abs() > 0.9995 {
749 let s = 1.0 - t;
751 let q = [
752 s * q1[0] + t * q2[0],
753 s * q1[1] + t * q2[1],
754 s * q1[2] + t * q2[2],
755 s * q1[3] + t * q2[3],
756 ];
757 let norm = (q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3])
758 .sqrt()
759 .max(1e-10);
760 return [q[0] / norm, q[1] / norm, q[2] / norm, q[3] / norm];
761 }
762 let theta = dot.acos();
763 let sin_theta = theta.sin().max(1e-10);
764 let s1 = ((1.0 - t) * theta).sin() / sin_theta;
765 let s2 = (t * theta).sin() / sin_theta;
766 [
767 s1 * q1[0] + s2 * q2[0],
768 s1 * q1[1] + s2 * q2[1],
769 s1 * q1[2] + s2 * q2[2],
770 s1 * q1[3] + s2 * q2[3],
771 ]
772}
773
774#[cfg(test)]
778mod tests {
779 use super::*;
780
781 #[test]
782 fn test_imu_serialize_deserialize() {
783 let r = ImuDataRecord::new(
784 1.0,
785 [0.1, 0.2, 9.81],
786 [0.01, 0.02, 0.03],
787 [20.0, 30.0, 40.0],
788 );
789 let bytes = r.to_bytes();
790 assert_eq!(bytes.len(), 80);
791 let r2 = ImuDataRecord::from_bytes(&bytes).unwrap();
792 assert!((r2.timestamp - 1.0).abs() < 1e-15);
793 assert!((r2.accel[2] - 9.81).abs() < 1e-10);
794 }
795
796 #[test]
797 fn test_imu_from_bytes_too_short() {
798 let result = ImuDataRecord::from_bytes(&[0u8; 10]);
799 assert!(result.is_none());
800 }
801
802 #[test]
803 fn test_gps_serialize() {
804 let r = GpsDataRecord::new(0.5, 35.0, 139.0, 100.0, [1.0, 2.0, 0.0], 1.2);
805 let bytes = r.to_bytes();
806 assert_eq!(bytes.len(), 64);
807 }
808
809 #[test]
810 fn test_gps_ground_speed() {
811 let r = GpsDataRecord::new(0.0, 0.0, 0.0, 0.0, [3.0, 4.0, 0.0], 1.0);
812 assert!((r.ground_speed() - 5.0).abs() < 1e-10);
813 }
814
815 #[test]
816 fn test_lidar_frame_add_point() {
817 let mut f = LidarFrame::new(1.0);
818 f.add_point([1.0, 2.0, 3.0], 0.5);
819 assert_eq!(f.n_points, 1);
820 }
821
822 #[test]
823 fn test_lidar_frame_serialize() {
824 let mut f = LidarFrame::new(1.0);
825 f.add_point([1.0, 0.0, 0.0], 0.8);
826 let bytes = f.to_bytes();
827 assert!(!bytes.is_empty());
828 }
829
830 #[test]
831 fn test_lidar_bounding_box() {
832 let mut f = LidarFrame::new(0.0);
833 f.add_point([1.0, 2.0, 3.0], 1.0);
834 f.add_point([-1.0, -2.0, -3.0], 0.5);
835 let bb = f.bounding_box();
836 assert!((bb[0][0] + 1.0).abs() < 1e-6);
837 assert!((bb[0][1] - 1.0).abs() < 1e-6);
838 }
839
840 #[test]
841 fn test_force_gauge_magnitude() {
842 let r = ForceGaugeRecord::new(0.0, [3.0, 4.0, 0.0], [0.0; 3], 25.0);
843 assert!((r.force_magnitude() - 5.0).abs() < 1e-10);
844 }
845
846 #[test]
847 fn test_force_gauge_serialize() {
848 let r = ForceGaugeRecord::new(1.0, [1.0, 2.0, 3.0], [0.1, 0.2, 0.3], 22.5);
849 let bytes = r.to_bytes();
850 assert_eq!(bytes.len(), 64);
851 }
852
853 #[test]
854 fn test_telemetry_encode_decode() {
855 let proto = TelemetryProtocol::new(1, 1);
856 let msg = TelemetryMessage {
857 message_id: 100,
858 seq: 0,
859 payload: vec![1u8, 2, 3, 4],
860 };
861 let encoded = proto.encode(&msg);
862 let decoded = proto.decode(&encoded).unwrap();
863 assert_eq!(decoded.message_id, 100);
864 assert_eq!(decoded.payload, vec![1u8, 2, 3, 4]);
865 }
866
867 #[test]
868 fn test_telemetry_wrong_magic() {
869 let proto = TelemetryProtocol::new(1, 1);
870 let mut data = vec![0u8; 20];
871 data[0] = 0x00; assert!(proto.decode(&data).is_none());
873 }
874
875 #[test]
876 fn test_sensor_fusion_complementary() {
877 let mut sf = SensorFusion::new(0.98, 0.1);
878 let accel = [0.0, 0.0, 9.81];
879 let gyro = [0.0, 0.0, 0.0];
880 sf.update_complementary(accel, gyro, 0.01);
881 assert!(sf.roll().is_finite());
882 assert!(sf.pitch().is_finite());
883 }
884
885 #[test]
886 fn test_sensor_fusion_madgwick() {
887 let mut sf = SensorFusion::new(0.98, 0.1);
888 let accel = [0.0, 0.0, 9.81];
889 let gyro = [0.01, 0.02, 0.0];
890 sf.update_madgwick(accel, gyro, 0.01);
891 let norm = sf.quaternion.iter().map(|v| v * v).sum::<f64>().sqrt();
892 assert!((norm - 1.0).abs() < 0.01);
893 }
894
895 #[test]
896 fn test_data_logger_log_and_flush() {
897 let mut logger: DataLogger<i32> = DataLogger::new(3);
898 logger.log(1);
899 logger.log(2);
900 logger.log(3);
901 logger.log(4); assert_eq!(logger.total_count(), 4);
903 }
904
905 #[test]
906 fn test_data_logger_replay() {
907 let mut logger: DataLogger<i32> = DataLogger::new(10);
908 for i in 0..5 {
909 logger.log(i);
910 }
911 let all = logger.replay();
912 assert_eq!(all.len(), 5);
913 }
914
915 #[test]
916 fn test_sensor_data_writer_imu() {
917 let mut w = SensorDataWriter::new(SensorType::Imu);
918 let r = ImuDataRecord::new(0.0, [0.0; 3], [0.0; 3], [0.0; 3]);
919 w.write_imu(&r);
920 assert_eq!(w.n_records, 1);
921 assert_eq!(w.bytes_written(), 80);
922 }
923
924 #[test]
925 fn test_sensor_data_writer_clear() {
926 let mut w = SensorDataWriter::new(SensorType::Gps);
927 let r = GpsDataRecord::new(0.0, 0.0, 0.0, 0.0, [0.0; 3], 1.0);
928 w.write_gps(&r);
929 w.clear();
930 assert_eq!(w.n_records, 0);
931 assert_eq!(w.bytes_written(), 0);
932 }
933
934 #[test]
935 fn test_sensor_data_reader_imu() {
936 let r = ImuDataRecord::new(1.5, [1.0, 2.0, 3.0], [0.1, 0.2, 0.3], [10.0, 20.0, 30.0]);
937 let bytes = r.to_bytes();
938 let mut reader = SensorDataReader::new(bytes, SensorType::Imu);
939 let r2 = reader.read_imu().unwrap();
940 assert!((r2.timestamp - 1.5).abs() < 1e-10);
941 }
942
943 #[test]
944 fn test_csv_telemetry_to_string() {
945 let mut csv = CsvTelemetry::new(vec!["t".into(), "ax".into(), "ay".into()]);
946 csv.append_row(vec!["0.0".into(), "1.0".into(), "2.0".into()]);
947 let s = csv.to_csv_string();
948 assert!(s.contains("t,ax,ay"));
949 assert!(s.contains("0.0,1.0,2.0"));
950 }
951
952 #[test]
953 fn test_csv_telemetry_n_rows() {
954 let mut csv = CsvTelemetry::new(vec!["x".into()]);
955 csv.append_row(vec!["1".into()]);
956 csv.append_row(vec!["2".into()]);
957 assert_eq!(csv.n_rows(), 2);
958 }
959
960 #[test]
961 fn test_quat_to_euler_identity() {
962 let euler = quat_to_euler([1.0, 0.0, 0.0, 0.0]);
963 for &v in &euler {
964 assert!(v.abs() < 1e-8, "Expected near zero, got {v}");
965 }
966 }
967}
968
969#[derive(Debug, Clone, PartialEq)]
975pub struct SensorReading {
976 pub timestamp: f64,
978 pub sensor_id: usize,
980 pub value: f64,
982 pub uncertainty: f64,
984}
985
986impl SensorReading {
987 pub fn new(timestamp: f64, sensor_id: usize, value: f64, uncertainty: f64) -> Self {
989 Self {
990 timestamp,
991 sensor_id,
992 value,
993 uncertainty,
994 }
995 }
996}
997
998#[derive(Debug, Clone)]
1000pub struct SensorTimeSeries {
1001 pub id: usize,
1003 pub name: String,
1005 pub readings: Vec<SensorReading>,
1007 pub sample_rate_hz: f64,
1009}
1010
1011impl SensorTimeSeries {
1012 pub fn new(id: usize, name: impl Into<String>, sample_rate_hz: f64) -> Self {
1014 Self {
1015 id,
1016 name: name.into(),
1017 readings: Vec::new(),
1018 sample_rate_hz,
1019 }
1020 }
1021
1022 pub fn push(&mut self, r: SensorReading) {
1024 self.readings.push(r);
1025 }
1026
1027 pub fn len(&self) -> usize {
1029 self.readings.len()
1030 }
1031
1032 pub fn is_empty(&self) -> bool {
1034 self.readings.is_empty()
1035 }
1036}
1037
1038pub fn write_sensor_csv(series: &SensorTimeSeries, path: &str) -> Result<(), String> {
1042 use std::io::Write as IoWrite;
1043 let file = std::fs::File::create(path).map_err(|e| e.to_string())?;
1044 let mut writer = std::io::BufWriter::new(file);
1045 writeln!(writer, "timestamp,value,uncertainty").map_err(|e| e.to_string())?;
1046 for r in &series.readings {
1047 writeln!(writer, "{},{},{}", r.timestamp, r.value, r.uncertainty)
1048 .map_err(|e| e.to_string())?;
1049 }
1050 Ok(())
1051}
1052
1053pub fn read_sensor_csv(path: &str, sensor_id: usize) -> Result<SensorTimeSeries, String> {
1055 let content = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
1056 let mut lines = content.lines();
1057 let _header = lines.next().ok_or("missing header")?;
1058 let mut series = SensorTimeSeries::new(sensor_id, path, 0.0);
1059 for line in lines {
1060 let parts: Vec<&str> = line.split(',').collect();
1061 if parts.len() < 2 {
1062 continue;
1063 }
1064 let ts: f64 = parts[0]
1065 .trim()
1066 .parse()
1067 .map_err(|_| format!("bad timestamp: {}", parts[0]))?;
1068 let val: f64 = parts[1]
1069 .trim()
1070 .parse()
1071 .map_err(|_| format!("bad value: {}", parts[1]))?;
1072 let unc: f64 = if parts.len() >= 3 {
1073 parts[2].trim().parse().unwrap_or(0.0)
1074 } else {
1075 0.0
1076 };
1077 series.push(SensorReading::new(ts, sensor_id, val, unc));
1078 }
1079 Ok(series)
1080}
1081
1082pub fn sensor_moving_average(series: &SensorTimeSeries, window: usize) -> SensorTimeSeries {
1086 let w = window.max(1);
1087 let mut out = SensorTimeSeries::new(series.id, series.name.clone(), series.sample_rate_hz);
1088 for i in 0..series.readings.len() {
1089 let start = (i + 1).saturating_sub(w);
1090 let slice = &series.readings[start..=i];
1091 let avg = slice.iter().map(|r| r.value).sum::<f64>() / slice.len() as f64;
1092 let mut r = series.readings[i].clone();
1093 r.value = avg;
1094 out.push(r);
1095 }
1096 out
1097}
1098
1099pub fn sensor_downsample(series: &SensorTimeSeries, factor: usize) -> SensorTimeSeries {
1103 let f = factor.max(1);
1104 let mut out = SensorTimeSeries::new(
1105 series.id,
1106 series.name.clone(),
1107 series.sample_rate_hz / f as f64,
1108 );
1109 for (i, r) in series.readings.iter().enumerate() {
1110 if i % f == 0 {
1111 out.push(r.clone());
1112 }
1113 }
1114 out
1115}
1116
1117pub fn sensor_peak_detection(
1119 series: &SensorTimeSeries,
1120 threshold: f64,
1121 min_gap: usize,
1122) -> Vec<usize> {
1123 let vals: Vec<f64> = series.readings.iter().map(|r| r.value).collect();
1124 let n = vals.len();
1125 let mut peaks = Vec::new();
1126 let mut last_peak: Option<usize> = None;
1127 for i in 1..n.saturating_sub(1) {
1128 if vals[i] > threshold && vals[i] >= vals[i - 1] && vals[i] >= vals[i + 1] {
1129 if let Some(lp) = last_peak
1130 && i - lp < min_gap
1131 {
1132 continue;
1133 }
1134 peaks.push(i);
1135 last_peak = Some(i);
1136 }
1137 }
1138 peaks
1139}
1140
1141pub fn sensor_fft_spectrum(series: &SensorTimeSeries) -> Vec<(f64, f64)> {
1145 let n = series.readings.len();
1146 if n == 0 {
1147 return Vec::new();
1148 }
1149 let vals: Vec<f64> = series.readings.iter().map(|r| r.value).collect();
1150 let dt = if series.sample_rate_hz > 0.0 {
1151 1.0 / series.sample_rate_hz
1152 } else {
1153 1.0
1154 };
1155 let half = n / 2 + 1;
1156 let mut spectrum = Vec::with_capacity(half);
1157 for k in 0..half {
1158 let mut re = 0.0f64;
1159 let mut im = 0.0f64;
1160 for (t, &v) in vals.iter().enumerate() {
1161 let angle = -2.0 * std::f64::consts::PI * (k as f64) * (t as f64) / (n as f64);
1162 re += v * angle.cos();
1163 im += v * angle.sin();
1164 }
1165 let mag = (re * re + im * im).sqrt();
1166 let freq = (k as f64) / (n as f64 * dt);
1167 spectrum.push((freq, mag));
1168 }
1169 spectrum
1170}
1171
1172pub fn sensor_calibration_linear(raw: f64, gain: f64, offset: f64) -> f64 {
1174 gain * raw + offset
1175}
1176
1177pub fn sensor_synchronize(series: &[SensorTimeSeries], target_rate: f64) -> Vec<SensorTimeSeries> {
1181 if series.is_empty() || target_rate <= 0.0 {
1182 return Vec::new();
1183 }
1184 let t_start = series
1186 .iter()
1187 .filter_map(|s| s.readings.first().map(|r| r.timestamp))
1188 .fold(f64::INFINITY, f64::min);
1189 let t_end = series
1190 .iter()
1191 .filter_map(|s| s.readings.last().map(|r| r.timestamp))
1192 .fold(f64::NEG_INFINITY, f64::max);
1193 if t_start >= t_end {
1194 return series
1195 .iter()
1196 .map(|s| SensorTimeSeries::new(s.id, s.name.clone(), target_rate))
1197 .collect();
1198 }
1199 let n_samples = ((t_end - t_start) * target_rate).ceil() as usize + 1;
1200 let timestamps: Vec<f64> = (0..n_samples)
1201 .map(|i| t_start + i as f64 / target_rate)
1202 .collect();
1203
1204 series
1205 .iter()
1206 .map(|s| {
1207 let mut out = SensorTimeSeries::new(s.id, s.name.clone(), target_rate);
1208 for &ts in ×tamps {
1209 let val = if s.readings.is_empty() {
1211 0.0
1212 } else {
1213 s.readings
1214 .iter()
1215 .min_by(|a, b| {
1216 (a.timestamp - ts)
1217 .abs()
1218 .partial_cmp(&(b.timestamp - ts).abs())
1219 .expect("operation should succeed")
1220 })
1221 .map(|r| r.value)
1222 .unwrap_or(0.0)
1223 };
1224 out.push(SensorReading::new(ts, s.id, val, 0.0));
1225 }
1226 out
1227 })
1228 .collect()
1229}
1230
1231pub fn merge_sensor_channels(channels: &[SensorTimeSeries]) -> Vec<Vec<f64>> {
1235 if channels.is_empty() {
1236 return Vec::new();
1237 }
1238 let n_rows = channels.iter().map(|c| c.len()).max().unwrap_or(0);
1239 (0..n_rows)
1240 .map(|i| {
1241 channels
1242 .iter()
1243 .map(|c| c.readings.get(i).map(|r| r.value).unwrap_or(0.0))
1244 .collect()
1245 })
1246 .collect()
1247}
1248
1249#[cfg(test)]
1251mod sensor_ts_tests {
1252 use super::*;
1253
1254 fn make_series(n: usize, sample_rate_hz: f64) -> SensorTimeSeries {
1255 let mut s = SensorTimeSeries::new(1, "test", sample_rate_hz);
1256 for i in 0..n {
1257 s.push(SensorReading::new(
1258 i as f64 / sample_rate_hz,
1259 1,
1260 i as f64,
1261 0.1,
1262 ));
1263 }
1264 s
1265 }
1266
1267 #[test]
1268 fn test_sensor_reading_new() {
1269 let r = SensorReading::new(1.0, 2, 3.5, 0.1);
1270 assert!((r.timestamp - 1.0).abs() < 1e-15);
1271 assert_eq!(r.sensor_id, 2);
1272 assert!((r.value - 3.5).abs() < 1e-15);
1273 }
1274
1275 #[test]
1276 fn test_series_len() {
1277 let s = make_series(10, 100.0);
1278 assert_eq!(s.len(), 10);
1279 }
1280
1281 #[test]
1282 fn test_series_is_empty() {
1283 let s = SensorTimeSeries::new(0, "empty", 100.0);
1284 assert!(s.is_empty());
1285 }
1286
1287 #[test]
1288 fn test_write_read_csv_roundtrip() {
1289 let s = make_series(5, 10.0);
1290 let path = "/tmp/oxiphysics_sensor_test.csv";
1291 write_sensor_csv(&s, path).unwrap();
1292 let s2 = read_sensor_csv(path, 1).unwrap();
1293 assert_eq!(s2.len(), 5);
1294 for (a, b) in s.readings.iter().zip(s2.readings.iter()) {
1295 assert!((a.timestamp - b.timestamp).abs() < 1e-10);
1296 assert!((a.value - b.value).abs() < 1e-10);
1297 }
1298 }
1299
1300 #[test]
1301 fn test_write_csv_creates_file() {
1302 let s = make_series(3, 1.0);
1303 let path = "/tmp/oxiphysics_sensor_create_test.csv";
1304 assert!(write_sensor_csv(&s, path).is_ok());
1305 }
1306
1307 #[test]
1308 fn test_read_csv_nonexistent() {
1309 let res = read_sensor_csv("/tmp/nonexistent_oxiphysics_xyz.csv", 0);
1310 assert!(res.is_err());
1311 }
1312
1313 #[test]
1314 fn test_moving_average_same_length() {
1315 let s = make_series(10, 10.0);
1316 let avg = sensor_moving_average(&s, 3);
1317 assert_eq!(avg.len(), s.len());
1318 }
1319
1320 #[test]
1321 fn test_moving_average_smooths() {
1322 let mut s = SensorTimeSeries::new(0, "noisy", 1.0);
1324 for i in 0..10 {
1325 s.push(SensorReading::new(
1326 i as f64,
1327 0,
1328 if i % 2 == 0 { 0.0 } else { 10.0 },
1329 0.0,
1330 ));
1331 }
1332 let avg = sensor_moving_average(&s, 3);
1333 let max_avg = avg
1335 .readings
1336 .iter()
1337 .map(|r| r.value)
1338 .fold(f64::NEG_INFINITY, f64::max);
1339 assert!(max_avg < 10.0, "max_avg={max_avg}");
1340 }
1341
1342 #[test]
1343 fn test_moving_average_window_1_identity() {
1344 let s = make_series(5, 10.0);
1345 let avg = sensor_moving_average(&s, 1);
1346 for (a, b) in s.readings.iter().zip(avg.readings.iter()) {
1347 assert!((a.value - b.value).abs() < 1e-12);
1348 }
1349 }
1350
1351 #[test]
1352 fn test_downsample_reduces_count() {
1353 let s = make_series(10, 10.0);
1354 let ds = sensor_downsample(&s, 2);
1355 assert_eq!(ds.len(), 5);
1356 }
1357
1358 #[test]
1359 fn test_downsample_factor_1_same_count() {
1360 let s = make_series(8, 10.0);
1361 let ds = sensor_downsample(&s, 1);
1362 assert_eq!(ds.len(), s.len());
1363 }
1364
1365 #[test]
1366 fn test_downsample_sample_rate_reduced() {
1367 let s = make_series(10, 100.0);
1368 let ds = sensor_downsample(&s, 4);
1369 assert!((ds.sample_rate_hz - 25.0).abs() < 1e-10);
1370 }
1371
1372 #[test]
1373 fn test_peak_detection_finds_peaks() {
1374 let mut s = SensorTimeSeries::new(0, "peaks", 1.0);
1375 let vals = [0.0f64, 5.0, 0.0, 0.0, 7.0, 0.0, 0.0, 6.0, 0.0];
1377 for (i, &v) in vals.iter().enumerate() {
1378 s.push(SensorReading::new(i as f64, 0, v, 0.0));
1379 }
1380 let peaks = sensor_peak_detection(&s, 3.0, 1);
1381 assert!(!peaks.is_empty(), "should find at least one peak");
1382 for &p in &peaks {
1384 assert!(s.readings[p].value > 3.0);
1385 }
1386 }
1387
1388 #[test]
1389 fn test_peak_detection_no_peaks_below_threshold() {
1390 let mut s = SensorTimeSeries::new(0, "flat", 1.0);
1391 for i in 0..5 {
1392 s.push(SensorReading::new(i as f64, 0, 1.0, 0.0));
1393 }
1394 let peaks = sensor_peak_detection(&s, 5.0, 1);
1395 assert!(peaks.is_empty());
1396 }
1397
1398 #[test]
1399 fn test_peak_detection_min_gap_enforced() {
1400 let mut s = SensorTimeSeries::new(0, "close_peaks", 1.0);
1401 let vals = [0.0f64, 5.0, 0.0, 5.0, 0.0, 5.0, 0.0];
1402 for (i, &v) in vals.iter().enumerate() {
1403 s.push(SensorReading::new(i as f64, 0, v, 0.0));
1404 }
1405 let peaks_close = sensor_peak_detection(&s, 3.0, 1);
1406 let peaks_far = sensor_peak_detection(&s, 3.0, 3);
1407 assert!(peaks_far.len() <= peaks_close.len());
1408 }
1409
1410 #[test]
1411 fn test_fft_spectrum_positive_magnitude() {
1412 let s = make_series(16, 16.0);
1413 let spec = sensor_fft_spectrum(&s);
1414 assert!(!spec.is_empty());
1415 for (_, mag) in &spec {
1416 assert!(*mag >= 0.0);
1417 }
1418 }
1419
1420 #[test]
1421 fn test_fft_spectrum_empty_series() {
1422 let s = SensorTimeSeries::new(0, "empty", 100.0);
1423 let spec = sensor_fft_spectrum(&s);
1424 assert!(spec.is_empty());
1425 }
1426
1427 #[test]
1428 fn test_fft_spectrum_length() {
1429 let s = make_series(10, 10.0);
1430 let spec = sensor_fft_spectrum(&s);
1431 assert_eq!(spec.len(), 6); }
1433
1434 #[test]
1435 fn test_calibration_linear_gain_offset() {
1436 let cal = sensor_calibration_linear(5.0, 2.0, 1.0);
1437 assert!((cal - 11.0).abs() < 1e-12);
1438 }
1439
1440 #[test]
1441 fn test_calibration_linear_identity() {
1442 let val = sensor_calibration_linear(3.0, 1.0, 0.0);
1443 assert!((val - 3.0).abs() < 1e-12);
1444 }
1445
1446 #[test]
1447 fn test_calibration_linear_zero_gain() {
1448 let val = sensor_calibration_linear(100.0, 0.0, 5.0);
1449 assert!((val - 5.0).abs() < 1e-12);
1450 }
1451
1452 #[test]
1453 fn test_synchronize_equal_length() {
1454 let s1 = make_series(10, 10.0);
1455 let s2 = make_series(8, 8.0);
1456 let synced = sensor_synchronize(&[s1, s2], 10.0);
1457 assert_eq!(synced.len(), 2);
1458 assert_eq!(synced[0].len(), synced[1].len());
1459 }
1460
1461 #[test]
1462 fn test_synchronize_empty_input() {
1463 let synced = sensor_synchronize(&[], 10.0);
1464 assert!(synced.is_empty());
1465 }
1466
1467 #[test]
1468 fn test_synchronize_zero_rate() {
1469 let s = make_series(5, 10.0);
1470 let synced = sensor_synchronize(&[s], 0.0);
1471 assert!(synced.is_empty());
1472 }
1473
1474 #[test]
1475 fn test_merge_sensor_channels_shape() {
1476 let s1 = make_series(5, 10.0);
1477 let s2 = make_series(5, 10.0);
1478 let mat = merge_sensor_channels(&[s1, s2]);
1479 assert_eq!(mat.len(), 5);
1480 for row in &mat {
1481 assert_eq!(row.len(), 2);
1482 }
1483 }
1484
1485 #[test]
1486 fn test_merge_sensor_channels_empty() {
1487 let mat = merge_sensor_channels(&[]);
1488 assert!(mat.is_empty());
1489 }
1490
1491 #[test]
1492 fn test_merge_sensor_channels_values() {
1493 let s1 = make_series(3, 1.0);
1494 let mat = merge_sensor_channels(&[s1]);
1495 assert!((mat[0][0] - 0.0).abs() < 1e-10);
1496 assert!((mat[1][0] - 1.0).abs() < 1e-10);
1497 assert!((mat[2][0] - 2.0).abs() < 1e-10);
1498 }
1499
1500 #[test]
1501 fn test_csv_roundtrip_larger_series() {
1502 let s = make_series(100, 50.0);
1503 let path = "/tmp/oxiphysics_sensor_large.csv";
1504 write_sensor_csv(&s, path).unwrap();
1505 let s2 = read_sensor_csv(path, 1).unwrap();
1506 assert_eq!(s2.len(), 100);
1507 }
1508
1509 #[test]
1510 fn test_sensor_time_series_push() {
1511 let mut s = SensorTimeSeries::new(0, "x", 1.0);
1512 s.push(SensorReading::new(0.0, 0, 42.0, 0.0));
1513 assert_eq!(s.len(), 1);
1514 assert!(!s.is_empty());
1515 }
1516
1517 #[test]
1518 fn test_fft_dc_component_is_sum() {
1519 let mut s = SensorTimeSeries::new(0, "dc", 4.0);
1521 let vals = [2.0f64, 2.0, 2.0, 2.0];
1522 for (i, &v) in vals.iter().enumerate() {
1523 s.push(SensorReading::new(i as f64 / 4.0, 0, v, 0.0));
1524 }
1525 let spec = sensor_fft_spectrum(&s);
1526 assert!((spec[0].1 - 8.0).abs() < 1e-8);
1528 }
1529}