{"record":{"id":"deb0a255cc704529","repo":"ruvnet/RuView","slug":"fps-hz-must-be-positive-got-fps-hz","errorCode":null,"errorMessage":"fps_hz must be positive, got {fps_hz}","messagePattern":"fps_hz must be positive, got (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"archive/v1/src/hardware/csi_extractor.py","lineNumber":324,"sourceCode":"\n        Identical contract to Rust's `SyncPacket::apply_to_local`.\n        Identity at `local_at_frame_us == self.local_us` returns `epoch_us`.\n        \"\"\"\n        offset = self.epoch_us - self.local_us\n        return local_at_frame_us + offset\n\n    def mesh_aligned_us_for_sequence(self, frame_seq: int, fps_hz: float) -> int:\n        \"\"\"ADR-110 §A0.12 — recover the mesh-aligned timestamp for an\n        in-flight CSI frame by its sequence number.\n\n        Pairs the frame's sequence number against this sync packet's\n        sequence high-water + an assumed/measured CSI rate. Matches the\n        Rust implementation byte-for-byte at the integer level (Python\n        rounds via `int()` truncation; for the canonical bench values\n        this is exact).\n        \"\"\"\n        if fps_hz <= 0:\n            raise ValueError(f\"fps_hz must be positive, got {fps_hz}\")\n        # Wrap to handle u32 sequence overflow the same way Rust does.\n        dframes = (frame_seq - self.sequence) & 0xFFFFFFFF\n        if dframes >= 0x80000000:\n            dframes -= 0x1_0000_0000\n        dus = int(dframes * 1_000_000 / fps_hz)\n        local_at = self.local_us + dus\n        return self.apply_to_local(local_at)\n\n\nclass SyncPacketParser:\n    \"\"\"Parser for ADR-110 §A0.12 32-byte sync packets.\n\n    Distinguished from CSI frames by the leading magic. Callers should\n    dispatch incoming UDP datagrams based on the first 4 bytes:\n\n        magic = struct.unpack_from('<I', data, 0)[0]\n        if magic == ESP32BinaryParser.MAGIC:    # 0xC5110001 — CSI frame\n            ...","sourceCodeStart":306,"sourceCodeEnd":342,"githubUrl":"https://github.com/ruvnet/RuView/blob/4685618388a5e49fad5b3005806f3bdd6a7c25c3/archive/v1/src/hardware/csi_extractor.py#L306-L342","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"# before\nts = sync.mesh_aligned_us_for_sequence(frame_seq, fps_hz=0)  # ValueError\n\n# after\nfps = 1e6 * (sync2.sequence - sync1.sequence) / (sync2.local_us - sync1.local_us)\nif fps <= 0:\n    raise RuntimeError('could not estimate CSI rate from sync packets')\nts = sync.mesh_aligned_us_for_sequence(frame_seq, fps_hz=fps)","handlingStrategy":"validation","validationCode":"def estimate_fps(sync_a, sync_b) -> float:\n    fps = 1e6 * (sync_b.sequence - sync_a.sequence) / (sync_b.local_us - sync_a.local_us)\n    if fps <= 0:\n        raise RuntimeError(f'invalid fps estimate {fps} from sync packets')\n    return fps\n\n# usage\nif fps_hz is None or fps_hz <= 0:\n    fps_hz = estimate_fps(first_sync, latest_sync)\nts = sync.mesh_aligned_us_for_sequence(frame_seq, fps_hz)","typeGuard":null,"tryCatchPattern":"try:\n    ts = sync.mesh_aligned_us_for_sequence(frame_seq, fps_hz)\nexcept ValueError as e:\n    logger.warning('skipping timestamp recovery, fps not measured yet: %s', e)","preventionTips":["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"],"tags":["sync","timestamp","validation","adr-110"],"backgroundTag":null,"analyzedSha":"4685618388a5e49fad5b3005806f3bdd6a7c25c3","analyzedAt":"2026-08-16T06:09:40.886Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}