Hi, okay, while I can’t post the datasheet. Here’s what Claude says on how to decode EVT2.1 and add it to the current viz tool:
Decoding the GenX320 raw stream when it’s in EVT2.1 mode
If you’re capturing with the genx320-event-streaming tool but you’ve switched the sensor to EVT2.1, the bytes on the wire are fine — you just have to decode them differently. Here’s the one gotcha and the decoder.
The 32-bit vs 64-bit gotcha
IOCTL_GENX320_READ_EVENTS_RAW packs the stream as 32-bit words, because it was written for EVT2.0 where one event = one 32-bit word. In EVT2.1 one event is a 64-bit word = two of those 32-bit slots. The cam still ships the same raw bytes, but:
- The 32-bit-oriented count is now twice the real event count — i.e. your event count halves going to EVT2.1. Same bytes, half as many events.
- Each EVT2.1 event lands as two consecutive little-endian 32-bit words: the low half (bits 31..0) first, then the high half (bits 63..32, which is where the type / timestamp / x / y actually live). So if you keep decoding the stream as EVT2.0 32-bit words, every other “event” is the all-zero low half and the rest is mis-aligned garbage.
The fix is a one-liner: take the exact same byte buffer the tool captures and read it as little-endian uint64 instead of uint32. Little-endian byte order pairs the two 32-bit halves back into the original 64-bit word for you.
# in the tool's PC-side decode, where it currently does:
# words = np.frombuffer(data, dtype='<u4') # EVT2.0
# do this instead:
words = np.frombuffer(data, dtype='<u8') # EVT2.1, 64-bit events
Everything downstream then works on words with the EVT2.1 bit layout below.
The decoder
import numpy as np
EVT_NEG = 0x0 # CD event, OFF (illumination decrease)
EVT_POS = 0x1 # CD event, ON (illumination increase)
EVT_TIME_HIGH = 0x8
EXT_TRIGGER = 0xA
def _bits(x, hi, lo): # bits [hi:lo] out of a uint64 array
return (x >> np.uint64(lo)) & np.uint64((1 << (hi - lo + 1)) - 1)
def decode_evt21(data):
"""data: raw little-endian byte stream from READ_EVENTS_RAW (EVT2.1).
Returns (cd_events, triggers):
cd_events: (M,4) -> [t_us, x, y, polarity] polarity 1=ON, 0=OFF
triggers: (K,3) -> [t_us, channel_id, edge] edge 1=rising, 0=falling
"""
w = np.frombuffer(data, dtype='<u8') # 64-bit EVT2.1 words
etype = _bits(w, 63, 60)
# TIME_HIGH carries ts[33:6]; carry the latest one forward to every event.
th = etype == EVT_TIME_HIGH
th_val = np.where(th, _bits(w, 59, 32), np.uint64(0))
src = np.maximum.accumulate(np.where(th, np.arange(w.size), 0))
time_high = th_val[src]
# --- CD events: each word is a vector of up to 32 pixels on one row ---
cd = np.flatnonzero((etype == EVT_NEG) | (etype == EVT_POS))
t = (time_high[cd] << np.uint64(6)) | _bits(w[cd], 59, 54) # full us
x_b = _bits(w[cd], 53, 43).astype(np.uint32) # x, mult. of 32
y = _bits(w[cd], 42, 32).astype(np.uint32)
vmask = _bits(w[cd], 31, 0).astype(np.uint32) # 32-bit valid
pol = (etype[cd] == EVT_POS).astype(np.uint8)
# expand the valid mask: bit n set -> an event at (x_b + n, y)
onbits = ((vmask[:, None] >> np.arange(32, dtype=np.uint32))
& np.uint32(1)).astype(bool)
r, n = np.nonzero(onbits)
cd_events = np.column_stack([t[r], x_b[r] + n.astype(np.uint32),
y[r], pol[r]])
# --- external triggers (same timestamp reconstruction) ---
tg = np.flatnonzero(etype == EXT_TRIGGER)
tgt = (time_high[tg] << np.uint64(6)) | _bits(w[tg], 59, 54)
tid = _bits(w[tg], 44, 40).astype(np.uint8) # 0=ext_trigger, 1=PXRSTN
edg = _bits(w[tg], 32, 32).astype(np.uint8) # 1=rising, 0=falling
triggers = np.column_stack([tgt, tid, edg])
return cd_events, triggers
Timestamps come out as the full 34-bit microsecond value: the 6 low bits ride on each event (ts[5:0]), the high 28 bits (ts[33:6]) come from the most recent TIME_HIGH, combined as (time_high << 6) | ts_low. Triggers use the exact same reconstruction, so a rising edge on the external trigger pin is a row in triggers with channel_id == 0 and edge == 1 at a real µs timestamp.
Optional: feed it back into the tool’s visualizer
The streaming tool’s events_to_image() expects the 6-column [type, sec, ms, us, x, y] layout. If you want the EVT2.1 events to flow straight into it, split the µs timestamp and map polarity to the ON/OFF type:
def to_six_col(cd_events):
t, x, y, p = cd_events.T
sec = (t // 1_000_000).astype(np.uint16)
ms = ((t // 1000) % 1000).astype(np.uint16)
us = (t % 1000).astype(np.uint16)
typ = p.astype(np.uint16) # 1=ON, 0=OFF (matches PIX_ON/OFF)
return np.column_stack([typ, sec, ms, us,
x.astype(np.uint16), y.astype(np.uint16)])
(sec as uint16 wraps at ~18 h, same as the tool’s native format.)
Watch out for
- Drop events before the first
TIME_HIGH. Until one arrives time_high is 0 and the timestamps are wrong — the forward-fill leaves them at the low 6 bits only, so filter them.
- Ship whole 64-bit words. Make sure each captured chunk is a multiple of 8 bytes before
frombuffer('<u8'), or stitch chunks together first; a packet boundary that splits a 64-bit word will misalign everything after it.
- 34-bit µs counter wraps ~every 4.77 h — handle
time_high going backwards if you record longer.
Bit layout is from the EVT2.1 spec; the only OpenMV-specific change vs the stock raw decode is reading the stream as <u8 instead of <u4 because the sensor is now emitting 64-bit events. Pin your firmware version when you post.