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

  1. Pass a positive measured rate (e.g. the observed frames-per-second of the CSI stream, ~100 Hz for the canonical bench config)
  2. Derive it from two sync packets: fps = 1e6 * (seq2 - seq1) / (local_us2 - local_us1)
  3. 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

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


AI-assisted analysis of ruvnet/RuView@4685618388 (2026-08-16). Data as JSON: /api/errors/deb0a255cc704529. Report an issue: GitHub.