use crate::sink::{EventColumns, TriggerColumns};
use crate::types::{CdEvent, SensorMetadata, TriggerEvent};
use std::fs::File;
use std::io::{BufWriter, Seek, SeekFrom, Write};
use std::path::Path;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum OutputError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Invalid format: {0}")]
InvalidFormat(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum FieldOrder {
#[default]
XYPT,
TXYP,
XYTP,
Custom([usize; 4]),
}
impl std::str::FromStr for FieldOrder {
type Err = OutputError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let parts: Vec<String> = s.split(',').map(|p| p.trim().to_lowercase()).collect();
if parts.len() != 4 {
return Err(OutputError::InvalidFormat(
"Format must have exactly 4 fields: x, y, p, t".to_string(),
));
}
let mut indices = [0usize; 4];
let mut used = [false; 4];
for (i, part) in parts.iter().enumerate() {
let field_idx = match part.as_str() {
"x" => 0,
"y" => 1,
"p" | "pol" | "polarity" => 2,
"t" | "time" | "timestamp" => 3,
_ => {
return Err(OutputError::InvalidFormat(format!(
"Unknown field: {}. Use x, y, p, t",
part
)))
}
};
if used[field_idx] {
return Err(OutputError::InvalidFormat(format!(
"Duplicate field: {}",
part
)));
}
indices[i] = field_idx;
used[field_idx] = true;
}
if indices == [0, 1, 2, 3] {
Ok(Self::XYPT)
} else if indices == [3, 0, 1, 2] {
Ok(Self::TXYP)
} else if indices == [0, 1, 3, 2] {
Ok(Self::XYTP)
} else {
Ok(Self::Custom(indices))
}
}
}
impl FieldOrder {
pub fn header(&self) -> &'static str {
match self {
Self::XYPT => "x,y,polarity,timestamp",
Self::TXYP => "timestamp,x,y,polarity",
Self::XYTP => "x,y,timestamp,polarity",
Self::Custom(_) => "x,y,polarity,timestamp", }
}
}
pub struct CsvWriter<W: Write> {
writer: BufWriter<W>,
field_order: FieldOrder,
}
impl<W: Write> CsvWriter<W> {
pub fn new(writer: W, field_order: FieldOrder) -> Self {
Self {
writer: BufWriter::with_capacity(1024 * 1024, writer),
field_order,
}
}
pub fn write_header(&mut self, metadata: Option<&SensorMetadata>) -> Result<(), OutputError> {
if let Some(meta) = metadata {
writeln!(self.writer, "%geometry:{},{}", meta.width, meta.height)?;
}
Ok(())
}
pub fn write_events(&mut self, events: &[CdEvent]) -> Result<(), OutputError> {
for event in events {
self.write_event(event)?;
}
Ok(())
}
pub fn write_columns(&mut self, events: &EventColumns) -> Result<(), OutputError> {
for index in 0..events.len() {
self.write_values(
events.x[index],
events.y[index],
events.polarity[index],
events.timestamp[index],
)?;
}
Ok(())
}
#[inline]
fn write_event(&mut self, event: &CdEvent) -> Result<(), OutputError> {
self.write_values(event.x, event.y, event.polarity, event.timestamp)
}
#[inline]
fn write_values(
&mut self,
x: u16,
y: u16,
polarity: u8,
timestamp: u64,
) -> Result<(), OutputError> {
match self.field_order {
FieldOrder::XYPT => {
writeln!(self.writer, "{},{},{},{}", x, y, polarity, timestamp)?;
}
FieldOrder::TXYP => {
writeln!(self.writer, "{},{},{},{}", timestamp, x, y, polarity)?;
}
FieldOrder::XYTP => {
writeln!(self.writer, "{},{},{},{}", x, y, timestamp, polarity)?;
}
FieldOrder::Custom(indices) => {
let values = [x as u64, y as u64, polarity as u64, timestamp];
writeln!(
self.writer,
"{},{},{},{}",
values[indices[0]], values[indices[1]], values[indices[2]], values[indices[3]]
)?;
}
}
Ok(())
}
pub fn flush(&mut self) -> Result<(), OutputError> {
self.writer.flush()?;
Ok(())
}
}
pub struct TriggerCsvWriter<W: Write> {
writer: BufWriter<W>,
}
impl<W: Write> TriggerCsvWriter<W> {
pub fn new(writer: W) -> Self {
Self {
writer: BufWriter::with_capacity(1024 * 1024, writer),
}
}
pub fn write_events(&mut self, events: &[TriggerEvent]) -> Result<(), OutputError> {
for event in events {
writeln!(
self.writer,
"{},{},{}",
event.value, event.id, event.timestamp
)?;
}
Ok(())
}
pub fn write_columns(&mut self, events: &TriggerColumns) -> Result<(), OutputError> {
for index in 0..events.len() {
writeln!(
self.writer,
"{},{},{}",
events.value[index], events.id[index], events.timestamp[index]
)?;
}
Ok(())
}
pub fn flush(&mut self) -> Result<(), OutputError> {
self.writer.flush()?;
Ok(())
}
}
pub struct BinaryWriter<W: Write> {
writer: BufWriter<W>,
packed: Vec<u8>,
}
impl<W: Write> BinaryWriter<W> {
pub fn new(writer: W) -> Self {
Self {
writer: BufWriter::with_capacity(1024 * 1024, writer),
packed: Vec::new(),
}
}
pub fn write_header(
&mut self,
metadata: &SensorMetadata,
event_count: u64,
) -> Result<(), OutputError> {
self.writer.write_all(b"EVT3BIN\0")?;
self.writer.write_all(&1u32.to_le_bytes())?;
self.writer.write_all(&metadata.width.to_le_bytes())?;
self.writer.write_all(&metadata.height.to_le_bytes())?;
self.writer.write_all(&event_count.to_le_bytes())?;
Ok(())
}
pub fn write_events(&mut self, events: &[CdEvent]) -> Result<(), OutputError> {
self.packed.clear();
self.packed.reserve(events.len() * 14);
for event in events {
append_binary_event(
&mut self.packed,
event.x,
event.y,
event.polarity,
event.timestamp,
);
}
self.writer.write_all(&self.packed)?;
Ok(())
}
pub fn write_columns(&mut self, events: &EventColumns) -> Result<(), OutputError> {
self.packed.clear();
self.packed.reserve(events.len() * 14);
for index in 0..events.len() {
append_binary_event(
&mut self.packed,
events.x[index],
events.y[index],
events.polarity[index],
events.timestamp[index],
);
}
self.writer.write_all(&self.packed)?;
Ok(())
}
pub fn flush(&mut self) -> Result<(), OutputError> {
self.writer.flush()?;
Ok(())
}
}
impl<W: Write + Seek> BinaryWriter<W> {
pub fn update_event_count(&mut self, event_count: u64) -> Result<(), OutputError> {
self.writer.flush()?;
self.writer.seek(SeekFrom::Start(20))?;
self.writer.write_all(&event_count.to_le_bytes())?;
self.writer.seek(SeekFrom::End(0))?;
Ok(())
}
}
#[inline]
fn append_binary_event(packed: &mut Vec<u8>, x: u16, y: u16, polarity: u8, timestamp: u64) {
packed.extend_from_slice(&x.to_le_bytes());
packed.extend_from_slice(&y.to_le_bytes());
packed.extend_from_slice(&[polarity, 0]);
packed.extend_from_slice(×tamp.to_le_bytes());
}
pub fn write_csv<P: AsRef<Path>>(
path: P,
events: &[CdEvent],
metadata: Option<&SensorMetadata>,
field_order: FieldOrder,
) -> Result<(), OutputError> {
let file = File::create(path)?;
let mut writer = CsvWriter::new(file, field_order);
writer.write_header(metadata)?;
writer.write_events(events)?;
writer.flush()?;
Ok(())
}
pub fn write_trigger_csv<P: AsRef<Path>>(
path: P,
events: &[TriggerEvent],
) -> Result<(), OutputError> {
let file = File::create(path)?;
let mut writer = TriggerCsvWriter::new(file);
writer.write_events(events)?;
writer.flush()?;
Ok(())
}
pub fn write_binary<P: AsRef<Path>>(
path: P,
events: &[CdEvent],
metadata: &SensorMetadata,
) -> Result<(), OutputError> {
let file = File::create(path)?;
let mut writer = BinaryWriter::new(file);
writer.write_header(metadata, events.len() as u64)?;
writer.write_events(events)?;
writer.flush()?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
use std::str::FromStr;
#[test]
fn test_field_order_parsing() {
assert_eq!(FieldOrder::from_str("x,y,p,t").unwrap(), FieldOrder::XYPT);
assert_eq!(FieldOrder::from_str("t,x,y,p").unwrap(), FieldOrder::TXYP);
assert_eq!(FieldOrder::from_str("x,y,t,p").unwrap(), FieldOrder::XYTP);
assert_eq!(
FieldOrder::from_str("X, Y, P, T").unwrap(),
FieldOrder::XYPT
);
}
#[test]
fn test_field_order_invalid() {
assert!(FieldOrder::from_str("x,y,z,t").is_err());
assert!(FieldOrder::from_str("x,y,p").is_err());
assert!(FieldOrder::from_str("x,x,y,t").is_err());
}
#[test]
fn test_csv_writer() {
let mut output = Vec::new();
{
let mut writer = CsvWriter::new(&mut output, FieldOrder::XYPT);
writer
.write_header(Some(&SensorMetadata {
width: 640,
height: 480,
}))
.unwrap();
writer
.write_events(&[
CdEvent::new(100, 200, 1, 12345),
CdEvent::new(101, 201, 0, 12346),
])
.unwrap();
writer.flush().unwrap();
}
let output_str = String::from_utf8(output).unwrap();
assert!(output_str.contains("%geometry:640,480"));
assert!(output_str.contains("100,200,1,12345"));
assert!(output_str.contains("101,201,0,12346"));
}
#[test]
fn test_csv_writer_txyp_order() {
let mut output = Vec::new();
{
let mut writer = CsvWriter::new(&mut output, FieldOrder::TXYP);
writer
.write_events(&[CdEvent::new(100, 200, 1, 12345)])
.unwrap();
writer.flush().unwrap();
}
let output_str = String::from_utf8(output).unwrap();
assert!(output_str.contains("12345,100,200,1"));
}
#[test]
fn columnar_writers_match_streaming_formats() {
let events = EventColumns {
x: vec![100, 101],
y: vec![200, 201],
polarity: vec![1, 0],
timestamp: vec![12_345, 12_346],
};
let metadata = SensorMetadata {
width: 640,
height: 480,
};
let mut csv = Vec::new();
{
let mut writer = CsvWriter::new(&mut csv, FieldOrder::XYPT);
writer.write_header(Some(&metadata)).unwrap();
writer.write_columns(&events).unwrap();
writer.flush().unwrap();
}
assert_eq!(
String::from_utf8(csv).unwrap(),
"%geometry:640,480\n100,200,1,12345\n101,201,0,12346\n"
);
let mut binary = Cursor::new(Vec::new());
{
let mut writer = BinaryWriter::new(&mut binary);
writer.write_header(&metadata, 0).unwrap();
writer.write_columns(&events).unwrap();
writer.update_event_count(events.len() as u64).unwrap();
writer.flush().unwrap();
}
let bytes = binary.into_inner();
assert_eq!(u64::from_le_bytes(bytes[20..28].try_into().unwrap()), 2);
assert_eq!(bytes.len(), 28 + 2 * 14);
}
}