{"record":{"id":"db563d41cf4fa6a9","repo":"ruvnet/RuView","slug":"frame-too-short-for-i-q-data-need-expected-len","errorCode":null,"errorMessage":"Frame too short for I/Q data: need {expected_len} bytes, got {len(raw_data)}","messagePattern":"Frame too short for I/Q data: need (.+?) bytes, got (.+?)","errorType":"exception","errorClass":"CSIParseError","httpStatus":null,"severity":"error","filePath":"archive/v1/src/hardware/csi_extractor.py","lineNumber":210,"sourceCode":"\n        magic, node_id, n_antennas, n_subcarriers, freq_mhz, sequence, rssi_u8, noise_u8, \\\n            ppdu_byte, flags_byte = struct.unpack_from(self.HEADER_FMT, raw_data, 0)\n\n        if magic != self.MAGIC:\n            raise CSIParseError(\n                f\"Invalid magic: expected 0x{self.MAGIC:08X}, got 0x{magic:08X}\"\n            )\n\n        # Convert unsigned bytes to signed i8\n        rssi = rssi_u8 if rssi_u8 < 128 else rssi_u8 - 256\n        noise_floor = noise_u8 if noise_u8 < 128 else noise_u8 - 256\n\n        iq_count = n_antennas * n_subcarriers\n        iq_bytes = iq_count * 2\n        expected_len = self.HEADER_SIZE + iq_bytes\n\n        if len(raw_data) < expected_len:\n            raise CSIParseError(\n                f\"Frame too short for I/Q data: need {expected_len} bytes, got {len(raw_data)}\"\n            )\n\n        # Parse I/Q pairs as signed bytes\n        iq_raw = struct.unpack_from(f'<{iq_count * 2}b', raw_data, self.HEADER_SIZE)\n        i_vals = np.array(iq_raw[0::2], dtype=np.float64).reshape(n_antennas, n_subcarriers)\n        q_vals = np.array(iq_raw[1::2], dtype=np.float64).reshape(n_antennas, n_subcarriers)\n\n        amplitude = np.sqrt(i_vals ** 2 + q_vals ** 2)\n        phase = np.arctan2(q_vals, i_vals)\n\n        snr = float(rssi - noise_floor)\n        frequency = float(freq_mhz) * 1e6\n\n        # Bandwidth inference (issue #1005): HE-LTF uses a 4x denser tone\n        # grid than HT-LTF on the same channel width — an HE-SU frame with\n        # 256 bins (242 active HE20 tones) is a *20 MHz* capture, not 160.\n        if ppdu_byte in (1, 2, 3):  # HE-SU / HE-MU / HE-TB","sourceCodeStart":192,"sourceCodeEnd":228,"githubUrl":"https://github.com/ruvnet/RuView/blob/4685618388a5e49fad5b3005806f3bdd6a7c25c3/archive/v1/src/hardware/csi_extractor.py#L192-L228","documentation":"After a valid 20-byte header, the parser needs 2 bytes of signed i8 I/Q per antenna x subcarrier cell; if the buffer is shorter than 20 + 2*n_antennas*n_subcarriers it raises CSIParseError('Frame too short for I/Q data: need N bytes, got M'). The header's declared dimensions do not fit in the received bytes — truncated frame or corrupted count fields.","triggerScenarios":"A corrupted n_antennas/n_subcarriers field (a flipped byte makes expected_len enormous); a UDP datagram truncated by MTU; a collector slicing frames at a fixed size smaller than the real frame; firmware header claiming more subcarriers than it appends.","commonSituations":"Large CSI frames (e.g. 2 antennas x many subcarriers) exceeding a transport buffer assumption; serial buffering dropping tail bytes under load; dimension fields drifting between firmware versions.","solutions":["Sanity-check the unpacked header before trusting it: reject implausible dimensions (n_antennas > 4, n_subcarriers > 1024) and resync on magic","If frames exceed the transport MTU, fix the framing/fragmentation on the firmware side or raise the datagram size","Log expected_len vs received length per dropped frame to distinguish systematic truncation from random corruption"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"import struct\n\ndef frame_length_plausible(buf: bytes) -> bool:\n    if len(buf) < ESP32BinaryParser.HEADER_SIZE:\n        return False\n    _, _, n_ant, n_sub, *_ = struct.unpack_from(ESP32BinaryParser.HEADER_FMT, buf, 0)\n    if not (1 <= n_ant <= 4 and 1 <= n_sub <= 1024):\n        return False  # corrupted dimension fields\n    return len(buf) >= ESP32BinaryParser.HEADER_SIZE + 2 * n_ant * n_sub","typeGuard":null,"tryCatchPattern":"try:\n    data = parser.parse(frame)\nexcept CSIParseError as e:\n    logger.warning('short I/Q frame (truncation or corrupt dims): %s', e)\n    resync_buffer()  # do not retry the same bytes unchanged","preventionTips":["Validate header dimension fields against physical limits (few antennas, bounded subcarriers) before trusting the declared length","Ensure the transport MTU/buffer can carry the largest frame the firmware emits","After this error, resync on magic instead of re-parsing the same buffer — the stream position is no longer trustworthy"],"tags":["esp32","binary","framing","truncation","adr-018"],"backgroundTag":null,"analyzedSha":"4685618388a5e49fad5b3005806f3bdd6a7c25c3","analyzedAt":"2026-08-16T06:09:40.886Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}