ruvnet/RuView · error · ValueError
fps_hz must be positive, got {fps_hz}
Error message
fps_hz must be positive, got {fps_hz} What it means
SyncPacket.mesh_aligned_us_for_sequence (ADR-110 section A0.12 timestamp recovery) divides microseconds by fps_hz, so a zero or negative rate is rejected with a plain ValueError before any arithmetic. fps_hz is the assumed/measured CSI frame rate used to convert a frame-sequence delta into microseconds; it is caller-supplied, not derived from the packet.
Source
Thrown at archive/v1/src/hardware/csi_extractor.py:324
Identical contract to Rust's `SyncPacket::apply_to_local`.
Identity at `local_at_frame_us == self.local_us` returns `epoch_us`.
"""
offset = self.epoch_us - self.local_us
return local_at_frame_us + offset
def mesh_aligned_us_for_sequence(self, frame_seq: int, fps_hz: float) -> int:
"""ADR-110 §A0.12 — recover the mesh-aligned timestamp for an
in-flight CSI frame by its sequence number.
Pairs the frame's sequence number against this sync packet's
sequence high-water + an assumed/measured CSI rate. Matches the
Rust implementation byte-for-byte at the integer level (Python
rounds via `int()` truncation; for the canonical bench values
this is exact).
"""
if fps_hz <= 0:
raise ValueError(f"fps_hz must be positive, got {fps_hz}")
# Wrap to handle u32 sequence overflow the same way Rust does.
dframes = (frame_seq - self.sequence) & 0xFFFFFFFF
if dframes >= 0x80000000:
dframes -= 0x1_0000_0000
dus = int(dframes * 1_000_000 / fps_hz)
local_at = self.local_us + dus
return self.apply_to_local(local_at)
class SyncPacketParser:
"""Parser for ADR-110 §A0.12 32-byte sync packets.
Distinguished from CSI frames by the leading magic. Callers should
dispatch incoming UDP datagrams based on the first 4 bytes:
magic = struct.unpack_from('<I', data, 0)[0]
if magic == ESP32BinaryParser.MAGIC: # 0xC5110001 — CSI frame
...View on GitHub (pinned to 4685618388)
Solutions
- Pass a positive measured rate (e.g. the observed frames-per-second of the CSI stream, ~100 Hz for the canonical bench config)
- Derive it from two sync packets: fps = 1e6 * (seq2 - seq1) / (local_us2 - local_us1)
- Guard at the call site: skip mesh-aligned timestamping until a positive fps estimate exists
Example fix
# before
ts = sync.mesh_aligned_us_for_sequence(frame_seq, fps_hz=0) # ValueError
# after
fps = 1e6 * (sync2.sequence - sync1.sequence) / (sync2.local_us - sync1.local_us)
if fps <= 0:
raise RuntimeError('could not estimate CSI rate from sync packets')
ts = sync.mesh_aligned_us_for_sequence(frame_seq, fps_hz=fps) Defensive patterns
Strategy: validation
Validate before calling
def estimate_fps(sync_a, sync_b) -> float:
fps = 1e6 * (sync_b.sequence - sync_a.sequence) / (sync_b.local_us - sync_a.local_us)
if fps <= 0:
raise RuntimeError(f'invalid fps estimate {fps} from sync packets')
return fps
# usage
if fps_hz is None or fps_hz <= 0:
fps_hz = estimate_fps(first_sync, latest_sync)
ts = sync.mesh_aligned_us_for_sequence(frame_seq, fps_hz) Try / catch
try:
ts = sync.mesh_aligned_us_for_sequence(frame_seq, fps_hz)
except ValueError as e:
logger.warning('skipping timestamp recovery, fps not measured yet: %s', e) Prevention
- Derive fps_hz from two observed sync packets instead of hard-coding or defaulting it to 0
- Skip timestamp recovery until a positive rate estimate is available; do not pass 'unset' sentinels
- Pass rate (frames per second), not inter-frame period, to avoid unit-inversion mistakes
When it happens
Trigger: Passing fps_hz=0.0 because the rate has not been measured yet; a config default of 0 flowing in from an 'unset' sentinel; a negative value from a sign error or inverted period/rate computation.
Common situations: Bootstrapping: calling timestamp recovery before enough sync packets have been observed to estimate the rate; config schemas where 0 means 'auto' but the API has no auto mode; passing an inter-frame period (e.g. 0.01 s) where fps (100.0) is expected.
Related errors
- Sync packet too short: {len(raw_data)} bytes, need {cls.SIZE
- Sync magic mismatch: got 0x{magic:08x}
- brain corpus exceeds 1000 records
- command must be a non-empty string
- args must be an array of strings
AI-assisted analysis of ruvnet/RuView@4685618388 (2026-08-16).
Data as JSON: /api/errors/deb0a255cc704529.
Report an issue: GitHub.