Skip to main content

eventcv_core/io/
bag.rs

1use std::collections::HashMap;
2use std::fs::File;
3use std::io::{BufWriter, Write};
4use std::path::Path;
5use std::sync::Mutex;
6
7use rosbag::{ChunkRecord, IndexRecord, MessageRecord, RosBag};
8
9use super::{IoError, LoadOptions, SliceSource};
10use crate::{EventStream, EventStreamBuilder};
11
12const DEFAULT_TOPIC: &str = "/davis/left/events";
13const EVENT_ARRAY_TYPE: &str = "dvs_msgs/EventArray";
14/// `dvs_msgs/EventArray` md5sum (32 lowercase hex). The reader matches on topic + type,
15/// not this value, but the rosbag parser requires a well-formed md5sum on every connection.
16const EVENT_ARRAY_MD5: &str = "5e8beee5759d85e9f5a8c1f5ad1cd00b";
17/// Message definition stored on the connection (informational; any non-empty text parses).
18const EVENT_ARRAY_DEF: &str =
19    "std_msgs/Header header\nuint32 height\nuint32 width\ndvs_msgs/Event[] events\n";
20/// Events per `dvs_msgs/EventArray` message — batches keep individual message buffers bounded.
21const MESSAGE_EVENTS: usize = 1_000_000;
22/// ROS bag 2.0 magic line.
23const BAG_MAGIC: &[u8] = b"#ROSBAG V2.0\n";
24
25/// Reads a ROS1 bag, decoding `dvs_msgs/EventArray` messages on a single topic
26/// (`options.topic`, default `/davis/left/events`). Sensor size comes from the
27/// messages unless `options.sensor_size` overrides it; timestamps become microseconds.
28pub fn read_bag(path: impl AsRef<Path>, options: &LoadOptions) -> Result<EventStream, IoError> {
29    let topic = options.topic.as_deref().unwrap_or(DEFAULT_TOPIC);
30    let bag = RosBag::new(path).map_err(IoError::Io)?;
31
32    let mut wanted: HashMap<u32, bool> = HashMap::new();
33    let mut builder: Option<EventStreamBuilder> = None;
34
35    'outer: for record in bag.chunk_records() {
36        let ChunkRecord::Chunk(chunk) = record.map_err(map_bag_error)? else {
37            continue;
38        };
39        for message in chunk.messages() {
40            match message.map_err(map_bag_error)? {
41                MessageRecord::Connection(connection) => {
42                    let matches = connection.topic == topic && connection.tp == EVENT_ARRAY_TYPE;
43                    wanted.insert(connection.id, matches);
44                }
45                MessageRecord::MessageData(data) => {
46                    if !wanted.get(&data.conn_id).copied().unwrap_or(false) {
47                        continue;
48                    }
49                    let (width, height) = match options.sensor_size {
50                        Some(size) => size,
51                        None => read_event_array_header(data.data)?,
52                    };
53                    let builder = builder
54                        .get_or_insert_with(|| EventStreamBuilder::new(width, height, 0.001));
55                    let stopped = decode_event_array(data.data, &mut |x, y, t, p| {
56                        builder.push(x, y, t, p);
57                        options.max_events.is_some_and(|max| builder.len() >= max)
58                    })?;
59                    if stopped {
60                        break 'outer;
61                    }
62                }
63            }
64        }
65    }
66
67    builder.map(EventStreamBuilder::build).ok_or_else(|| {
68        IoError::Format(format!(
69            "no {EVENT_ARRAY_TYPE} messages found on topic {topic}"
70        ))
71    })
72}
73
74/// Reads the `(width, height)` from a `dvs_msgs/EventArray` header (the fields precede
75/// the events), without decoding the events.
76fn read_event_array_header(bytes: &[u8]) -> Result<(usize, usize), IoError> {
77    let mut reader = ByteReader::new(bytes);
78    reader.skip(4 + 8)?; // Header: seq (u32) + stamp (sec u32, nsec u32)
79    let frame_id_len = reader.u32()? as usize;
80    reader.skip(frame_id_len)?; // Header: frame_id
81    let height = reader.u32()? as usize;
82    let width = reader.u32()? as usize;
83    Ok((width, height))
84}
85
86/// Decodes one serialized `dvs_msgs/EventArray`, calling `on_event(x, y, t_us, polarity)`
87/// per event. Stops early (returning `Ok(true)`) once `on_event` returns `true`. The
88/// single decode path shared by `read_bag` (eager), time slicing, and the count pass.
89fn decode_event_array(
90    bytes: &[u8],
91    on_event: &mut dyn FnMut(u16, u16, i64, bool) -> bool,
92) -> Result<bool, IoError> {
93    let mut reader = ByteReader::new(bytes);
94    reader.skip(4 + 8)?;
95    let frame_id_len = reader.u32()? as usize;
96    reader.skip(frame_id_len)?;
97    reader.skip(4 + 4)?; // height + width (read by `read_event_array_header`)
98    let count = reader.u32()? as usize;
99    for _ in 0..count {
100        let x = reader.u16()?;
101        let y = reader.u16()?;
102        let seconds = i64::from(reader.u32()?);
103        let nanoseconds = i64::from(reader.u32()?);
104        let polarity = reader.u8()? != 0;
105        if on_event(x, y, seconds * 1_000_000 + nanoseconds / 1000, polarity) {
106            return Ok(true);
107        }
108    }
109    Ok(false)
110}
111
112/// Writes a stream as a ROS1 bag containing one connection (`topic`, default
113/// `/davis/left/events`) and a single chunk of `dvs_msgs/EventArray` messages, round-tripping
114/// through [`read_bag`] and [`open_bag_slice`]. Events are batched into messages and the
115/// timestamps split into ROS `sec`/`nsec` (so microseconds are preserved exactly). The whole
116/// chunk is buffered, so very large streams use proportional memory.
117pub fn write_bag(
118    path: impl AsRef<Path>,
119    stream: &EventStream,
120    topic: Option<&str>,
121) -> Result<(), IoError> {
122    let topic = topic.unwrap_or(DEFAULT_TOPIC);
123    let conn_id: u32 = 0;
124    let ts = stream.ts();
125
126    // Build the chunk payload: one connection record, then the batched message records.
127    // `index_entries` records each message's (start time, byte offset within the payload).
128    let mut payload: Vec<u8> =
129        make_record(&connection_fields(conn_id, topic), &connection_data(topic));
130    let mut index_entries: Vec<(i64, u32)> = Vec::new();
131    let mut start = 0;
132    while start < stream.len() {
133        let end = (start + MESSAGE_EVENTS).min(stream.len());
134        let offset = payload.len() as u32;
135        let message = serialize_event_array(stream, start, end);
136        write_record(&mut payload, &message_fields(conn_id, ts[start]), &message)
137            .expect("writing to a Vec never fails");
138        index_entries.push((ts[start], offset));
139        start = end;
140    }
141    if stream.is_empty() {
142        // Emit one empty message so `read_bag` finds a connection + data and rebuilds an
143        // empty stream (with the correct sensor size from the message header).
144        let offset = payload.len() as u32;
145        let message = serialize_event_array(stream, 0, 0);
146        write_record(&mut payload, &message_fields(conn_id, 0), &message)
147            .expect("writing to a Vec never fails");
148        index_entries.push((0, offset));
149    }
150    let message_count = index_entries.len() as u32;
151    let (start_us, end_us) = match (ts.iter().min(), ts.iter().max()) {
152        (Some(&lo), Some(&hi)) => (lo, hi),
153        _ => (0, 0),
154    };
155
156    // The bag header has a fixed length (its fields are fixed-size), so the chunk position is
157    // known before the index position is, and the header can be built once index_pos is set.
158    let bag_header_len = make_record(&bag_header_fields(0, 1, 1), &[]).len();
159    let chunk_pos = BAG_MAGIC.len() + bag_header_len;
160    let chunk_fields = chunk_fields(payload.len() as u32);
161    let mut index_data: Vec<u8> = Vec::with_capacity(index_entries.len() * 12);
162    for (time_us, offset) in &index_entries {
163        index_data.extend_from_slice(&ros_time(*time_us));
164        index_data.extend_from_slice(&offset.to_le_bytes());
165    }
166    let index_record = make_record(&index_data_fields(conn_id, message_count), &index_data);
167    let index_pos = chunk_pos + record_len(&chunk_fields, payload.len()) + index_record.len();
168
169    let mut writer = BufWriter::new(File::create(path).map_err(IoError::Io)?);
170    writer.write_all(BAG_MAGIC).map_err(IoError::Io)?;
171    write_record(&mut writer, &bag_header_fields(index_pos as u64, 1, 1), &[])
172        .map_err(IoError::Io)?;
173    write_record(&mut writer, &chunk_fields, &payload).map_err(IoError::Io)?;
174    writer.write_all(&index_record).map_err(IoError::Io)?;
175    // Footer (read by `index_records`): the connection, then the chunk info.
176    write_record(
177        &mut writer,
178        &connection_fields(conn_id, topic),
179        &connection_data(topic),
180    )
181    .map_err(IoError::Io)?;
182    let chunk_info_data = {
183        let mut data = Vec::with_capacity(8);
184        data.extend_from_slice(&conn_id.to_le_bytes());
185        data.extend_from_slice(&message_count.to_le_bytes());
186        data
187    };
188    write_record(
189        &mut writer,
190        &chunk_info_fields(chunk_pos as u64, start_us, end_us),
191        &chunk_info_data,
192    )
193    .map_err(IoError::Io)?;
194    writer.flush().map_err(IoError::Io)
195}
196
197/// Splits a microsecond timestamp into the ROS `sec`/`nsec` pair (8 bytes, little-endian) the
198/// reader recombines into nanoseconds — exact for microsecond data. Negative times clamp to 0.
199fn ros_time(t_us: i64) -> [u8; 8] {
200    let t_us = t_us.max(0);
201    let seconds = (t_us / 1_000_000) as u32;
202    let nanoseconds = ((t_us % 1_000_000) * 1000) as u32;
203    let mut bytes = [0u8; 8];
204    bytes[..4].copy_from_slice(&seconds.to_le_bytes());
205    bytes[4..].copy_from_slice(&nanoseconds.to_le_bytes());
206    bytes
207}
208
209/// Serializes events `[start, end)` as a `dvs_msgs/EventArray` body matching [`decode_event_array`].
210fn serialize_event_array(stream: &EventStream, start: usize, end: usize) -> Vec<u8> {
211    let (width, height) = stream.sensor_size();
212    let (xs, ys, ts, ps) = (stream.xs(), stream.ys(), stream.ts(), stream.ps());
213    let stamp = ts.get(start).copied().unwrap_or(0);
214    let mut message = Vec::with_capacity(24 + (end - start) * 13);
215    message.extend_from_slice(&0u32.to_le_bytes()); // header.seq
216    message.extend_from_slice(&ros_time(stamp)); // header.stamp
217    message.extend_from_slice(&0u32.to_le_bytes()); // header.frame_id (empty string)
218    message.extend_from_slice(&(height as u32).to_le_bytes());
219    message.extend_from_slice(&(width as u32).to_le_bytes());
220    message.extend_from_slice(&((end - start) as u32).to_le_bytes());
221    for index in start..end {
222        message.extend_from_slice(&xs[index].to_le_bytes());
223        message.extend_from_slice(&ys[index].to_le_bytes());
224        message.extend_from_slice(&ros_time(ts[index]));
225        message.push(u8::from(ps[index]));
226    }
227    message
228}
229
230/// One `<len:u32><name=value>` record-header field.
231fn field(name: &str, value: &[u8]) -> Vec<u8> {
232    let mut bytes = Vec::with_capacity(4 + name.len() + 1 + value.len());
233    let body_len = (name.len() + 1 + value.len()) as u32;
234    bytes.extend_from_slice(&body_len.to_le_bytes());
235    bytes.extend_from_slice(name.as_bytes());
236    bytes.push(b'=');
237    bytes.extend_from_slice(value);
238    bytes
239}
240
241/// Byte length of a record: `<header_len:u32><header><data_len:u32><data>`.
242fn record_len(fields: &[Vec<u8>], data_len: usize) -> usize {
243    4 + fields.iter().map(Vec::len).sum::<usize>() + 4 + data_len
244}
245
246/// Builds a complete record in memory (for the small records whose bytes/length are reused).
247fn make_record(fields: &[Vec<u8>], data: &[u8]) -> Vec<u8> {
248    let mut record = Vec::with_capacity(record_len(fields, data.len()));
249    write_record(&mut record, fields, data).expect("writing to a Vec never fails");
250    record
251}
252
253/// Streams one record to `writer` (used for the large chunk record to avoid copying the payload).
254fn write_record<W: Write>(writer: &mut W, fields: &[Vec<u8>], data: &[u8]) -> std::io::Result<()> {
255    let header_len: usize = fields.iter().map(Vec::len).sum();
256    writer.write_all(&(header_len as u32).to_le_bytes())?;
257    for field in fields {
258        writer.write_all(field)?;
259    }
260    writer.write_all(&(data.len() as u32).to_le_bytes())?;
261    writer.write_all(data)
262}
263
264fn bag_header_fields(index_pos: u64, conn_count: u32, chunk_count: u32) -> Vec<Vec<u8>> {
265    vec![
266        field("op", &[0x03]),
267        field("index_pos", &index_pos.to_le_bytes()),
268        field("conn_count", &conn_count.to_le_bytes()),
269        field("chunk_count", &chunk_count.to_le_bytes()),
270    ]
271}
272
273fn chunk_fields(uncompressed_size: u32) -> Vec<Vec<u8>> {
274    vec![
275        field("op", &[0x05]),
276        field("compression", b"none"),
277        field("size", &uncompressed_size.to_le_bytes()),
278    ]
279}
280
281fn connection_fields(conn_id: u32, topic: &str) -> Vec<Vec<u8>> {
282    vec![
283        field("op", &[0x07]),
284        field("conn", &conn_id.to_le_bytes()),
285        field("topic", topic.as_bytes()),
286    ]
287}
288
289fn connection_data(topic: &str) -> Vec<u8> {
290    let fields = [
291        field("topic", topic.as_bytes()),
292        field("type", EVENT_ARRAY_TYPE.as_bytes()),
293        field("md5sum", EVENT_ARRAY_MD5.as_bytes()),
294        field("message_definition", EVENT_ARRAY_DEF.as_bytes()),
295    ];
296    fields.concat()
297}
298
299fn message_fields(conn_id: u32, time_us: i64) -> Vec<Vec<u8>> {
300    vec![
301        field("op", &[0x02]),
302        field("conn", &conn_id.to_le_bytes()),
303        field("time", &ros_time(time_us)),
304    ]
305}
306
307fn index_data_fields(conn_id: u32, count: u32) -> Vec<Vec<u8>> {
308    vec![
309        field("op", &[0x04]),
310        field("ver", &1u32.to_le_bytes()),
311        field("conn", &conn_id.to_le_bytes()),
312        field("count", &count.to_le_bytes()),
313    ]
314}
315
316fn chunk_info_fields(chunk_pos: u64, start_us: i64, end_us: i64) -> Vec<Vec<u8>> {
317    vec![
318        field("op", &[0x06]),
319        field("ver", &1u32.to_le_bytes()),
320        field("chunk_pos", &chunk_pos.to_le_bytes()),
321        field("start_time", &ros_time(start_us)),
322        field("end_time", &ros_time(end_us)),
323        field("count", &1u32.to_le_bytes()),
324    ]
325}
326
327/// One chunk's byte offset and (microsecond) time range, copied out of the bag's index
328/// so the [`BagSliceSource`] holds no borrows of the mmapped `RosBag`.
329#[derive(Clone, Copy, Debug, PartialEq, Eq)]
330struct ChunkMeta {
331    pos: u64,
332    start_us: i64,
333    end_us: i64,
334}
335
336/// In-place [`SliceSource`] for rosbags. The bag's own chunk index (`ChunkInfo`) gives
337/// each chunk's byte offset and time range, so time slicing seeks straight to the
338/// overlapping chunks and decompresses only those — no full read, bounded memory. Event
339/// **counts** aren't in the index, so `n_events`/`slice_index` decode the target chunks
340/// once and cache the per-chunk cumulative counts; time slicing never triggers that.
341pub struct BagSliceSource {
342    bag: RosBag,
343    conn_ids: Vec<u32>,
344    chunks: Vec<ChunkMeta>,
345    sensor: (usize, usize),
346    span_us: (i64, i64),
347    counts: Mutex<Option<Vec<usize>>>,
348}
349
350/// Opens a bag for lazy slicing: reads the index for the target topic's connection ids,
351/// the chunks containing them (offset + time range), the span, and the sensor size
352/// (override, else the first message header). No chunk data is decompressed.
353pub fn open_bag_slice(
354    path: impl AsRef<Path>,
355    options: &LoadOptions,
356) -> Result<BagSliceSource, IoError> {
357    let topic = options.topic.as_deref().unwrap_or(DEFAULT_TOPIC);
358    let bag = RosBag::new(path).map_err(IoError::Io)?;
359
360    let mut conn_ids: Vec<u32> = Vec::new();
361    for record in bag.index_records() {
362        if let IndexRecord::Connection(connection) = record.map_err(map_bag_error)? {
363            if connection.topic == topic && connection.tp == EVENT_ARRAY_TYPE {
364                conn_ids.push(connection.id);
365            }
366        }
367    }
368    if conn_ids.is_empty() {
369        return Err(IoError::Format(format!(
370            "no {EVENT_ARRAY_TYPE} connection on topic {topic}"
371        )));
372    }
373
374    let mut chunks: Vec<ChunkMeta> = Vec::new();
375    for record in bag.index_records() {
376        if let IndexRecord::ChunkInfo(info) = record.map_err(map_bag_error)? {
377            if info
378                .entries()
379                .any(|entry| conn_ids.contains(&entry.conn_id))
380            {
381                chunks.push(ChunkMeta {
382                    pos: info.chunk_pos,
383                    start_us: (info.start_time / 1000) as i64,
384                    end_us: (info.end_time / 1000) as i64,
385                });
386            }
387        }
388    }
389    chunks.sort_by_key(|chunk| chunk.start_us);
390
391    let span_us = match (
392        chunks.iter().map(|c| c.start_us).min(),
393        chunks.iter().map(|c| c.end_us).max(),
394    ) {
395        (Some(lo), Some(hi)) => (lo, hi),
396        _ => (0, 0),
397    };
398    let sensor = match options.sensor_size {
399        Some(size) => size,
400        None => detect_sensor(&bag, &chunks, &conn_ids)?,
401    };
402
403    Ok(BagSliceSource {
404        bag,
405        conn_ids,
406        chunks,
407        sensor,
408        span_us,
409        counts: Mutex::new(None),
410    })
411}
412
413/// Reads the sensor size from the first event message of the first chunk.
414fn detect_sensor(
415    bag: &RosBag,
416    chunks: &[ChunkMeta],
417    conn_ids: &[u32],
418) -> Result<(usize, usize), IoError> {
419    let Some(first) = chunks.first() else {
420        return Ok((1, 1));
421    };
422    let mut iterator = bag.chunk_records();
423    iterator.seek(first.pos).map_err(map_bag_error)?;
424    if let Some(record) = iterator.next() {
425        if let ChunkRecord::Chunk(chunk) = record.map_err(map_bag_error)? {
426            for message in chunk.messages() {
427                if let MessageRecord::MessageData(data) = message.map_err(map_bag_error)? {
428                    if conn_ids.contains(&data.conn_id) {
429                        return read_event_array_header(data.data);
430                    }
431                }
432            }
433        }
434    }
435    Ok((1, 1))
436}
437
438/// Chunks whose time range overlaps the half-open window `[t0, t1)` (µs).
439fn select_chunks(chunks: &[ChunkMeta], t0: i64, t1: i64) -> impl Iterator<Item = &ChunkMeta> {
440    chunks
441        .iter()
442        .filter(move |chunk| chunk.start_us < t1 && chunk.end_us >= t0)
443}
444
445/// Index of the chunk containing global event `i`, given cumulative per-chunk counts
446/// (`counts[k]` = events before chunk `k`, `counts.len() == chunks + 1`).
447fn locate_chunk(counts: &[usize], i: usize) -> usize {
448    counts
449        .partition_point(|&count| count <= i)
450        .saturating_sub(1)
451}
452
453impl BagSliceSource {
454    /// Seeks to `chunk` and calls `on_event` for every event on a target connection,
455    /// stopping the chunk early once `on_event` returns `true`.
456    fn for_each_event(
457        &self,
458        chunk: &ChunkMeta,
459        on_event: &mut dyn FnMut(u16, u16, i64, bool) -> bool,
460    ) -> Result<(), IoError> {
461        let mut iterator = self.bag.chunk_records();
462        iterator.seek(chunk.pos).map_err(map_bag_error)?;
463        let Some(record) = iterator.next() else {
464            return Ok(());
465        };
466        let ChunkRecord::Chunk(decoded) = record.map_err(map_bag_error)? else {
467            return Ok(());
468        };
469        for message in decoded.messages() {
470            if let MessageRecord::MessageData(data) = message.map_err(map_bag_error)? {
471                if self.conn_ids.contains(&data.conn_id) && decode_event_array(data.data, on_event)?
472                {
473                    break;
474                }
475            }
476        }
477        Ok(())
478    }
479
480    /// Cumulative event count per chunk, decoded once and cached.
481    fn cumulative_counts(&self) -> Result<Vec<usize>, IoError> {
482        let mut guard = self.counts.lock().unwrap();
483        if let Some(counts) = guard.as_ref() {
484            return Ok(counts.clone());
485        }
486        let mut counts = Vec::with_capacity(self.chunks.len() + 1);
487        counts.push(0);
488        for chunk in &self.chunks {
489            let mut events = 0usize;
490            self.for_each_event(chunk, &mut |_, _, _, _| {
491                events += 1;
492                false
493            })?;
494            counts.push(counts.last().unwrap() + events);
495        }
496        *guard = Some(counts.clone());
497        Ok(counts)
498    }
499}
500
501impl SliceSource for BagSliceSource {
502    fn sensor_size(&self) -> (usize, usize) {
503        self.sensor
504    }
505
506    fn timestamp_scale_ms(&self) -> f64 {
507        0.001
508    }
509
510    fn n_events(&self) -> usize {
511        // The count requires decoding; treat a decode error as "unknown" (0).
512        self.cumulative_counts()
513            .ok()
514            .and_then(|counts| counts.last().copied())
515            .unwrap_or(0)
516    }
517
518    fn time_span(&self) -> (i64, i64) {
519        self.span_us
520    }
521
522    fn slice_time(&self, t0: i64, t1: i64) -> Result<EventStream, IoError> {
523        let mut builder = EventStreamBuilder::new(self.sensor.0, self.sensor.1, 0.001);
524        for chunk in select_chunks(&self.chunks, t0, t1) {
525            self.for_each_event(chunk, &mut |x, y, t, p| {
526                if t >= t1 {
527                    return true; // events are time-ordered within a chunk
528                }
529                if t >= t0 {
530                    builder.push(x, y, t, p);
531                }
532                false
533            })?;
534        }
535        Ok(builder.build())
536    }
537
538    fn slice_index(&self, i0: usize, i1: usize) -> Result<EventStream, IoError> {
539        let counts = self.cumulative_counts()?;
540        let total = counts.last().copied().unwrap_or(0);
541        let i0 = i0.min(total);
542        let i1 = i1.clamp(i0, total);
543        let mut builder = EventStreamBuilder::new(self.sensor.0, self.sensor.1, 0.001);
544        if i0 == i1 {
545            return Ok(builder.build());
546        }
547        for (offset, chunk) in self
548            .chunks
549            .iter()
550            .enumerate()
551            .skip(locate_chunk(&counts, i0))
552        {
553            if counts[offset] >= i1 {
554                break;
555            }
556            let mut index = counts[offset];
557            self.for_each_event(chunk, &mut |x, y, t, p| {
558                if (i0..i1).contains(&index) {
559                    builder.push(x, y, t, p);
560                }
561                index += 1;
562                index >= i1
563            })?;
564        }
565        Ok(builder.build())
566    }
567}
568
569fn map_bag_error(error: rosbag::Error) -> IoError {
570    IoError::Format(format!("rosbag: {error}"))
571}
572
573/// Little-endian cursor over a ROS message payload.
574struct ByteReader<'a> {
575    bytes: &'a [u8],
576    position: usize,
577}
578
579impl<'a> ByteReader<'a> {
580    fn new(bytes: &'a [u8]) -> Self {
581        Self { bytes, position: 0 }
582    }
583
584    fn take(&mut self, count: usize) -> Result<&'a [u8], IoError> {
585        let end = self
586            .position
587            .checked_add(count)
588            .filter(|&end| end <= self.bytes.len())
589            .ok_or_else(|| IoError::Format("truncated dvs_msgs/EventArray message".to_owned()))?;
590        let slice = &self.bytes[self.position..end];
591        self.position = end;
592        Ok(slice)
593    }
594
595    fn skip(&mut self, count: usize) -> Result<(), IoError> {
596        self.take(count).map(|_| ())
597    }
598
599    fn u8(&mut self) -> Result<u8, IoError> {
600        Ok(self.take(1)?[0])
601    }
602
603    fn u16(&mut self) -> Result<u16, IoError> {
604        Ok(u16::from_le_bytes(self.take(2)?.try_into().unwrap()))
605    }
606
607    fn u32(&mut self) -> Result<u32, IoError> {
608        Ok(u32::from_le_bytes(self.take(4)?.try_into().unwrap()))
609    }
610}
611
612#[cfg(test)]
613mod tests {
614    use super::{locate_chunk, open_bag_slice, read_bag, select_chunks, write_bag, ChunkMeta};
615    use crate::io::{LoadOptions, SliceSource};
616    use crate::{EventStream, EventStreamBuilder};
617
618    fn temp_path(tag: &str) -> std::path::PathBuf {
619        let nanos = std::time::SystemTime::now()
620            .duration_since(std::time::UNIX_EPOCH)
621            .unwrap()
622            .as_nanos();
623        std::env::temp_dir().join(format!("eventcv_{tag}_{nanos}.bag"))
624    }
625
626    fn sample_stream() -> EventStream {
627        let mut builder = EventStreamBuilder::new(16, 12, 0.001);
628        for &(x, y, t, p) in &[
629            (0u16, 0u16, 5i64, true),
630            (3, 4, 1_000_001, false),
631            (15, 11, 2_500_000, true),
632            (7, 8, 2_500_000, false),
633        ] {
634            builder.push(x, y, t, p);
635        }
636        builder.build()
637    }
638
639    #[test]
640    fn bag_round_trips_through_the_reader() {
641        let stream = sample_stream();
642        let path = temp_path("bag_rt");
643        write_bag(&path, &stream, None).unwrap();
644
645        let loaded = read_bag(&path, &LoadOptions::default()).unwrap();
646        assert_eq!(loaded.sensor_size(), (16, 12));
647        assert_eq!(loaded.xs(), stream.xs());
648        assert_eq!(loaded.ys(), stream.ys());
649        assert_eq!(loaded.ts(), stream.ts()); // microseconds preserved exactly
650        assert_eq!(loaded.ps(), stream.ps());
651
652        // The lazy slicer reads the same file in place.
653        let reader = open_bag_slice(&path, &LoadOptions::default()).unwrap();
654        assert_eq!(reader.sensor_size(), (16, 12));
655        assert_eq!(reader.n_events(), stream.len());
656        assert_eq!(reader.time_span(), (5, 2_500_000));
657        let window = reader.slice_time(1_000_000, 2_000_000).unwrap();
658        assert_eq!(window.ts(), &[1_000_001]);
659
660        std::fs::remove_file(&path).ok();
661    }
662
663    #[test]
664    fn bag_round_trips_an_empty_stream() {
665        let stream = EventStreamBuilder::new(8, 6, 0.001).build();
666        let path = temp_path("bag_empty");
667        write_bag(&path, &stream, Some("/cam/events")).unwrap();
668
669        let options = LoadOptions {
670            topic: Some("/cam/events".to_owned()),
671            ..LoadOptions::default()
672        };
673        let loaded = read_bag(&path, &options).unwrap();
674        assert!(loaded.is_empty());
675        assert_eq!(loaded.sensor_size(), (8, 6));
676
677        std::fs::remove_file(&path).ok();
678    }
679
680    fn meta(pos: u64, start_us: i64, end_us: i64) -> ChunkMeta {
681        ChunkMeta {
682            pos,
683            start_us,
684            end_us,
685        }
686    }
687
688    #[test]
689    fn select_chunks_picks_overlapping_windows() {
690        let chunks = [meta(0, 0, 100), meta(1, 100, 200), meta(2, 200, 300)];
691
692        let picked: Vec<u64> = select_chunks(&chunks, 150, 250).map(|c| c.pos).collect();
693        assert_eq!(picked, [1, 2]); // [150, 250) overlaps chunks 1 and 2
694
695        // Half-open: a window ending exactly at a chunk's start excludes it.
696        let picked: Vec<u64> = select_chunks(&chunks, 0, 100).map(|c| c.pos).collect();
697        assert_eq!(picked, [0]);
698
699        assert!(select_chunks(&chunks, 1000, 2000).next().is_none());
700    }
701
702    #[test]
703    fn locate_chunk_finds_the_containing_chunk() {
704        let counts = [0usize, 10, 25, 40]; // cumulative counts for 3 chunks
705
706        assert_eq!(locate_chunk(&counts, 0), 0);
707        assert_eq!(locate_chunk(&counts, 9), 0);
708        assert_eq!(locate_chunk(&counts, 10), 1);
709        assert_eq!(locate_chunk(&counts, 24), 1);
710        assert_eq!(locate_chunk(&counts, 39), 2);
711    }
712}