1use std::fmt::{Display, Formatter};
2use std::io::Read;
3use std::path::{Path, PathBuf};
4
5use bincode::config::standard;
6use bincode::decode_from_std_read;
7use bincode::error::DecodeError;
8use clap::{Parser, Subcommand, ValueEnum};
9use cu29::prelude::*;
10
11#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
12pub enum ExportFormat {
13 Json,
14 Csv,
15}
16
17impl Display for ExportFormat {
18 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
19 match self {
20 ExportFormat::Json => write!(f, "json"),
21 ExportFormat::Csv => write!(f, "csv"),
22 }
23 }
24}
25
26#[derive(Parser)]
28#[command(author, version, about)]
29pub struct LogReaderCli {
30 pub unifiedlog_base: PathBuf,
33
34 #[command(subcommand)]
35 pub command: Command,
36}
37
38#[derive(Subcommand)]
39pub enum Command {
40 ExtractLog { log_index: PathBuf },
42 ExtractCopperlist {
44 #[arg(short, long, default_value_t = ExportFormat::Json)]
45 export_format: ExportFormat,
46 },
47}
48
49pub fn run_cli<P>() -> CuResult<()>
52where
53 P: CopperListTuple,
54{
55 let args = LogReaderCli::parse();
56 let unifiedlog_base = args.unifiedlog_base;
57
58 let UnifiedLogger::Read(dl) = UnifiedLoggerBuilder::new()
59 .file_base_name(&unifiedlog_base)
60 .build()
61 .expect("Failed to create logger")
62 else {
63 panic!("Failed to create logger");
64 };
65
66 match args.command {
67 Command::ExtractLog { log_index } => {
68 let reader = UnifiedLoggerIOReader::new(dl, UnifiedLogType::StructuredLogLine);
69 textlog_dump(reader, &log_index)?;
70 }
71 Command::ExtractCopperlist { export_format } => {
72 println!("Extracting copperlists with format: {export_format}");
73 let mut reader = UnifiedLoggerIOReader::new(dl, UnifiedLogType::CopperList);
74 let iter = copperlists_dump::<P>(&mut reader);
75 for entry in iter {
76 println!("{entry:#?}");
77 }
78 }
79 }
80
81 Ok(())
82}
83
84pub fn copperlists_dump<P: CopperListTuple>(
87 mut src: impl Read,
88) -> impl Iterator<Item = CopperList<P>> {
89 std::iter::from_fn(move || {
90 let entry = decode_from_std_read::<CopperList<P>, _, _>(&mut src, standard());
91 match entry {
92 Ok(entry) => Some(entry),
93 Err(e) => match e {
94 DecodeError::UnexpectedEnd { .. } => None,
95 DecodeError::Io { inner, additional } => {
96 if inner.kind() == std::io::ErrorKind::UnexpectedEof {
97 None
98 } else {
99 println!("Error {inner:?} additional:{additional}");
100 None
101 }
102 }
103 _ => {
104 println!("Error {e:?}");
105 None
106 }
107 },
108 }
109 })
110}
111
112pub fn textlog_dump(mut src: impl Read, index: &Path) -> CuResult<()> {
117 let all_strings = read_interned_strings(index)?;
118 loop {
119 let entry = decode_from_std_read::<CuLogEntry, _, _>(&mut src, standard());
120
121 match entry {
122 Err(DecodeError::UnexpectedEnd { .. }) => return Ok(()),
123 Err(DecodeError::Io { inner, additional }) => {
124 if inner.kind() == std::io::ErrorKind::UnexpectedEof {
125 return Ok(());
126 } else {
127 println!("Error {inner:?} additional:{additional}");
128 return Err(CuError::new_with_cause("Error reading log", inner));
129 }
130 }
131 Err(e) => {
132 println!("Error {e:?}");
133 return Err(CuError::new_with_cause("Error reading log", e));
134 }
135 Ok(entry) => {
136 if entry.msg_index == 0 {
137 break;
138 }
139
140 let result = rebuild_logline(&all_strings, &entry);
141 if result.is_err() {
142 println!("Failed to rebuild log line: {result:?}");
143 continue;
144 }
145 println!("{}: {}", entry.time, result.unwrap());
146 }
147 };
148 }
149 Ok(())
150}
151
152#[cfg(all(feature = "python", not(target_os = "macos")))]
154mod python {
155 use bincode::config::standard;
156 use bincode::decode_from_std_read;
157 use bincode::error::DecodeError;
158 use cu29::prelude::*;
159 use pyo3::exceptions::PyIOError;
160 use pyo3::prelude::*;
161 use pyo3::types::{PyDelta, PyDict, PyList};
162 use std::io::Read;
163 use std::path::Path;
164
165 #[pyclass]
166 pub struct PyLogIterator {
167 reader: Box<dyn Read + Send + Sync>,
168 }
169
170 #[pymethods]
171 impl PyLogIterator {
172 fn __iter__(slf: PyRefMut<Self>) -> PyRefMut<Self> {
173 slf
174 }
175
176 fn __next__(mut slf: PyRefMut<Self>) -> Option<PyResult<PyCuLogEntry>> {
177 match decode_from_std_read::<CuLogEntry, _, _>(&mut slf.reader, standard()) {
178 Ok(entry) => {
179 if entry.msg_index == 0 {
180 None
181 } else {
182 Some(Ok(PyCuLogEntry { inner: entry }))
183 }
184 }
185 Err(DecodeError::UnexpectedEnd { .. }) => None,
186 Err(DecodeError::Io { inner, .. })
187 if inner.kind() == std::io::ErrorKind::UnexpectedEof =>
188 {
189 None
190 }
191 Err(e) => Some(Err(PyIOError::new_err(e.to_string()))),
192 }
193 }
194 }
195
196 #[pyfunction]
200 pub fn struct_log_iterator_bare(
201 bare_struct_src_path: &str,
202 index_path: &str,
203 ) -> PyResult<(PyLogIterator, Vec<String>)> {
204 let file = std::fs::File::open(bare_struct_src_path)
205 .map_err(|e| PyIOError::new_err(e.to_string()))?;
206 let all_strings = read_interned_strings(Path::new(index_path))
207 .map_err(|e| PyIOError::new_err(e.to_string()))?;
208 Ok((
209 PyLogIterator {
210 reader: Box::new(file),
211 },
212 all_strings,
213 ))
214 }
215 #[pyfunction]
219 pub fn struct_log_iterator_unified(
220 unified_src_path: &str,
221 index_path: &str,
222 ) -> PyResult<(PyLogIterator, Vec<String>)> {
223 let all_strings = read_interned_strings(Path::new(index_path))
224 .map_err(|e| PyIOError::new_err(e.to_string()))?;
225
226 let UnifiedLogger::Read(dl) = UnifiedLoggerBuilder::new()
227 .file_base_name(Path::new(unified_src_path))
228 .build()
229 .expect("Failed to create logger")
230 else {
231 panic!("Failed to create logger");
232 };
233
234 let reader = UnifiedLoggerIOReader::new(dl, UnifiedLogType::StructuredLogLine);
235 Ok((
236 PyLogIterator {
237 reader: Box::new(reader),
238 },
239 all_strings,
240 ))
241 }
242
243 #[pyclass]
245 pub struct PyCuLogEntry {
246 pub inner: CuLogEntry,
247 }
248
249 #[pymethods]
250 impl PyCuLogEntry {
251 pub fn ts<'a>(&self, py: Python<'a>) -> Bound<'a, PyDelta> {
253 let nanoseconds: u64 = self.inner.time.into();
254
255 let days = (nanoseconds / 86_400_000_000_000) as i32;
257 let seconds = (nanoseconds / 1_000_000_000) as i32;
258 let microseconds = ((nanoseconds % 1_000_000_000) / 1_000) as i32;
259
260 PyDelta::new(py, days, seconds, microseconds, false).unwrap()
261 }
262
263 pub fn msg_index(&self) -> u32 {
265 self.inner.msg_index
266 }
267
268 pub fn paramname_indexes(&self) -> Vec<u32> {
270 self.inner.paramname_indexes.iter().copied().collect()
271 }
272
273 pub fn params(&self) -> Vec<PyObject> {
275 self.inner.params.iter().map(value_to_py).collect()
276 }
277 }
278
279 #[pymodule(name = "libcu29_export")]
281 fn cu29_export(m: &Bound<'_, PyModule>) -> PyResult<()> {
282 m.add_class::<PyCuLogEntry>()?;
283 m.add_class::<PyLogIterator>()?;
284 m.add_function(wrap_pyfunction!(struct_log_iterator_bare, m)?)?;
285 m.add_function(wrap_pyfunction!(struct_log_iterator_unified, m)?)?;
286 Ok(())
287 }
288
289 fn value_to_py(value: &cu29::prelude::Value) -> PyObject {
290 match value {
291 Value::String(s) => Python::with_gil(|py| s.into_pyobject(py).unwrap().into()),
292 Value::U64(u) => Python::with_gil(|py| u.into_pyobject(py).unwrap().into()),
293 Value::I64(i) => Python::with_gil(|py| i.into_pyobject(py).unwrap().into()),
294 Value::F64(f) => Python::with_gil(|py| f.into_pyobject(py).unwrap().into()),
295 Value::Bool(b) => Python::with_gil(|py| b.into_pyobject(py).unwrap().to_owned().into()),
296 Value::CuTime(t) => Python::with_gil(|py| t.0.into_pyobject(py).unwrap().into()),
297 Value::Bytes(b) => Python::with_gil(|py| b.into_pyobject(py).unwrap().into()),
298 Value::Char(c) => Python::with_gil(|py| c.into_pyobject(py).unwrap().into()),
299 Value::I8(i) => Python::with_gil(|py| i.into_pyobject(py).unwrap().into()),
300 Value::U8(u) => Python::with_gil(|py| u.into_pyobject(py).unwrap().into()),
301 Value::I16(i) => Python::with_gil(|py| i.into_pyobject(py).unwrap().into()),
302 Value::U16(u) => Python::with_gil(|py| u.into_pyobject(py).unwrap().into()),
303 Value::I32(i) => Python::with_gil(|py| i.into_pyobject(py).unwrap().into()),
304 Value::U32(u) => Python::with_gil(|py| u.into_pyobject(py).unwrap().into()),
305 Value::Map(m) => Python::with_gil(|py| {
306 let dict = PyDict::new(py);
307 for (k, v) in m.iter() {
308 dict.set_item(value_to_py(k), value_to_py(v)).unwrap();
309 }
310 dict.into_pyobject(py).unwrap().into()
311 }),
312 Value::F32(f) => Python::with_gil(|py| f.into_pyobject(py).unwrap().into()),
313 Value::Option(o) => Python::with_gil(|py| {
314 if o.is_none() {
315 py.None()
316 } else {
317 o.clone().map(|v| value_to_py(&v)).unwrap()
318 }
319 }),
320 Value::Unit => Python::with_gil(|py| py.None()),
321 Value::Newtype(v) => value_to_py(v),
322 Value::Seq(s) => Python::with_gil(|py| {
323 let list = PyList::new(py, s.iter().map(value_to_py)).unwrap();
324 list.into_pyobject(py).unwrap().into()
325 }),
326 }
327 }
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333 use bincode::encode_into_slice;
334 use fs_extra::dir::{copy, CopyOptions};
335 use std::io::Cursor;
336 use std::sync::{Arc, Mutex};
337 use tempfile::{tempdir, TempDir};
338
339 fn copy_stringindex_to_temp(tmpdir: &TempDir) -> PathBuf {
340 let temp_path = tmpdir.path();
342
343 let mut copy_options = CopyOptions::new();
344 copy_options.copy_inside = true;
345
346 copy("test/cu29_log_index", temp_path, ©_options).unwrap();
347 temp_path.join("cu29_log_index")
348 }
349
350 #[test]
351 fn test_extract_low_level_cu29_log() {
352 let temp_dir = TempDir::new().unwrap();
353 let temp_path = copy_stringindex_to_temp(&temp_dir);
354 let entry = CuLogEntry::new(3);
355 let bytes = bincode::encode_to_vec(&entry, standard()).unwrap();
356 let reader = Cursor::new(bytes.as_slice());
357 textlog_dump(reader, temp_path.as_path()).unwrap();
358 }
359
360 #[test]
361 fn end_to_end_datalogger_and_structlog_test() {
362 let dir = tempdir().expect("Failed to create temp dir");
363 let path = dir
364 .path()
365 .join("end_to_end_datalogger_and_structlog_test.copper");
366 {
367 let UnifiedLogger::Write(logger) = UnifiedLoggerBuilder::new()
369 .write(true)
370 .create(true)
371 .file_base_name(&path)
372 .preallocated_size(100000)
373 .build()
374 .expect("Failed to create logger")
375 else {
376 panic!("Failed to create logger")
377 };
378 let data_logger = Arc::new(Mutex::new(logger));
379 let stream = stream_write(data_logger.clone(), UnifiedLogType::StructuredLogLine, 1024);
380 let rt = LoggerRuntime::init(RobotClock::default(), stream, None::<NullLog>);
381
382 let mut entry = CuLogEntry::new(4); entry.add_param(0, Value::String("Parameter for the log line".into()));
384 log(&mut entry).expect("Failed to log");
385 let mut entry = CuLogEntry::new(2); entry.add_param(0, Value::String("Parameter for the log line".into()));
387 log(&mut entry).expect("Failed to log");
388
389 drop(rt);
391 }
392 let UnifiedLogger::Read(logger) = UnifiedLoggerBuilder::new()
394 .file_base_name(
395 &dir.path()
396 .join("end_to_end_datalogger_and_structlog_test.copper"),
397 )
398 .build()
399 .expect("Failed to create logger")
400 else {
401 panic!("Failed to create logger")
402 };
403 let reader = UnifiedLoggerIOReader::new(logger, UnifiedLogType::StructuredLogLine);
404 let temp_dir = TempDir::new().unwrap();
405 textlog_dump(
406 reader,
407 Path::new(copy_stringindex_to_temp(&temp_dir).as_path()),
408 )
409 .expect("Failed to dump log");
410 }
411
412 type MyCuPayload = (u8, i32, f32);
414
415 #[test]
417 fn test_copperlists_dump() {
418 let mut data = vec![0u8; 10000];
419 let mypls: [MyCuPayload; 4] = [(1, 2, 3.0), (2, 3, 4.0), (3, 4, 5.0), (4, 5, 6.0)];
420
421 let mut offset: usize = 0;
422 for pl in mypls.iter() {
423 let cl = CopperList::<MyCuPayload>::new(1, *pl);
424 offset +=
425 encode_into_slice(&cl, &mut data.as_mut_slice()[offset..], standard()).unwrap();
426 }
427
428 let reader = Cursor::new(data);
429
430 let mut iter = copperlists_dump::<MyCuPayload>(reader);
431 assert_eq!(iter.next().unwrap().msgs, (1, 2, 3.0));
432 assert_eq!(iter.next().unwrap().msgs, (2, 3, 4.0));
433 assert_eq!(iter.next().unwrap().msgs, (3, 4, 5.0));
434 assert_eq!(iter.next().unwrap().msgs, (4, 5, 6.0));
435 }
436}