"""Decode Wheeltec N300 FDILink IMU frames from a V2 .rscap capture.""" from __future__ import annotations import struct from dataclasses import dataclass import numpy as np from .capture_format_v2 import CaptureFile, RawChunk, iter_contiguous_segments @dataclass(frozen=True) class ImuSample: t_s: float gyro_rad_s: tuple[float, float, float] accel_m_s2: tuple[float, float, float] host_receive_utc_ticks: int device_timestamp_us: int def crc8_fdilink(data: bytes) -> int: crc = 0 for value in data: crc ^= value for _ in range(8): crc = ((crc << 1) ^ 0x07) & 0xFF if crc & 0x80 else (crc << 1) & 0xFF return crc def crc16_fdilink(data: bytes) -> int: crc = 0 for value in data: crc ^= value << 8 for _ in range(8): crc = ((crc << 1) ^ 0x1021) & 0xFFFF if crc & 0x8000 else (crc << 1) & 0xFFFF return crc def _host_ticks_for_span(chunks: list[RawChunk], start: int, end: int) -> int: stream_offset = 0 last = chunks[0] for chunk in chunks: next_offset = stream_offset + len(chunk.raw) if start < next_offset and end > stream_offset: last = chunk stream_offset = next_offset return last.receive_utc_ticks def iter_n300_imu_samples(capture: CaptureFile) -> list[ImuSample]: """Return CRC-valid MSG_IMU (0x40) samples sorted by device timestamp.""" samples: list[ImuSample] = [] expected_lengths = {0x40: 56, 0x41: 48} for _segment_id, chunks in iter_contiguous_segments(capture.chunks): stream = b"".join(chunk.raw for chunk in chunks) cursor = 0 while cursor < len(stream): start = stream.find(b"\xFC", cursor) if start < 0: break if start + 8 > len(stream): break payload_length = stream[start + 2] end = start + payload_length + 8 if end > len(stream): if stream.find(b"\xFC", start + 1) < 0: break cursor = start + 1 continue frame = stream[start:end] if frame[-1] != 0xFD: cursor = start + 1 continue packet_id = frame[1] payload = frame[7:-1] header_ok = crc8_fdilink(frame[:4]) == frame[4] payload_ok = crc16_fdilink(payload) == int.from_bytes(frame[5:7], "big") expected = expected_lengths.get(packet_id) length_ok = expected is None or len(payload) == expected if not (header_ok and payload_ok and length_ok): cursor = start + 1 continue if packet_id == 0x40: gyro = struct.unpack_from("<3f", payload, 0) accel = struct.unpack_from("<3f", payload, 12) device_us = struct.unpack_from(" tuple[np.ndarray, np.ndarray, np.ndarray]: if not samples: return ( np.zeros(0, dtype=np.float64), np.zeros((0, 3), dtype=np.float64), np.zeros((0, 3), dtype=np.float64), ) t = np.asarray([sample.t_s for sample in samples], dtype=np.float64) gyro = np.asarray([sample.gyro_rad_s for sample in samples], dtype=np.float64) accel = np.asarray([sample.accel_m_s2 for sample in samples], dtype=np.float64) return t, gyro, accel