From 12e296b7a2701869e3317f3b72a7733a8f28f132 Mon Sep 17 00:00:00 2001 From: Jordan Yates Date: Sat, 13 Jun 2026 15:43:39 +1000 Subject: [PATCH 1/5] lib: split CSV output into dedicated file Moev all CSV specific logic into a dedicated file. Signed-off-by: Jordan Yates --- src/lib.rs | 179 ++----------------------------------------- src/output_csv.rs | 190 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 195 insertions(+), 174 deletions(-) create mode 100644 src/output_csv.rs diff --git a/src/lib.rs b/src/lib.rs index 6bdeb07..9f08849 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,7 @@ -use chrono::SecondsFormat; -use itertools::Itertools; use memmap::Mmap; use std::collections::HashMap; -use std::collections::hash_map::Entry; use std::fs::File; -use std::io::{self, BufRead, BufReader, BufWriter, Cursor, Write}; +use std::io; use std::path::PathBuf; use std::sync::{Arc, Mutex}; use std::thread; @@ -13,6 +10,7 @@ use tdf::TdfOutput; pub mod args; pub mod fs_util; +mod output_csv; pub trait ProgressReporter { /// Called when progress starts. Could be used to initialize the state or display a start message. @@ -25,123 +23,6 @@ pub trait ProgressReporter { fn stop(&mut self); } -pub struct TdfCsvWriter { - decoder_idx: usize, - output_folder: std::path::PathBuf, - output_unix: bool, - pub outputs: - HashMap<(Option, u16), (std::path::PathBuf, std::io::BufWriter)>, - output_cnt: HashMap<(Option, u16), usize>, -} -impl TdfCsvWriter { - pub fn new( - decoder_idx: usize, - output_folder: std::path::PathBuf, - output_unix_time: bool, - ) -> Self { - Self { - decoder_idx: decoder_idx, - output_folder: output_folder, - output_unix: output_unix_time, - outputs: HashMap::new(), - output_cnt: HashMap::new(), - } - } - - pub fn output_path(self: &Self, remote_id: Option, tdf_id: u16) -> Option { - self.outputs - .get(&(remote_id, tdf_id)) - .map(|(pathbuf, _)| pathbuf.clone()) - } -} - -impl TdfOutput for TdfCsvWriter { - fn write( - &mut self, - remote_id: Option, - tdf_id: u16, - tdf_time: i64, - tdf_idx: Option, - size: u8, - cursor: &mut Cursor<&[u8]>, - ) -> std::io::Result<()> { - // Create writer if it doesn't exist - let (_, writer) = match self.outputs.entry((remote_id, tdf_id)) { - Entry::Occupied(o) => o.into_mut(), - Entry::Vacant(v) => { - let id_prefix = match remote_id { - Some(id) => format!("_{:016x}", id), - None => "".to_string(), - }; - let fname = format!( - "{}_{}_{:05}.csv", - id_prefix, - tdf::decoders::tdf_name(&tdf_id), - self.decoder_idx - ); - let path = self.output_folder.join(fname); - let mut writer = std::io::BufWriter::new(std::fs::File::create(path.clone())?); - - // Write header into file - let heading = tdf::decoders_csv::tdf_fields(&tdf_id).join(","); - writer.write_all(format!("time,{}\n", heading).as_bytes())?; - - // Touch the count variable in case the decoding fails - *self - .output_cnt - .entry((remote_id, tdf_id.to_owned())) - .or_default() += 0; - - // Insert into hashmap and return - v.insert((path, writer)) - } - }; - - // Construct CSV line - let reading = tdf::decoders_csv::tdf_read_into_str(&tdf_id, size, cursor)?; - let time = match tdf_idx { - Some(idx) => { - // Use the index directly if provided - format!("{idx}") - } - None => match self.output_unix { - // Otherwise, format the time to a string - true => { - let (unix_seconds, unix_nano) = tdf::time::tdf_time_to_unix(tdf_time); - format!("{}.{:06}", unix_seconds, unix_nano / 1000) - } - false => { - let datetime = tdf::time::tdf_time_to_datetime(tdf_time).expect("Invalid time"); - datetime.to_rfc3339_opts(SecondsFormat::Micros, true) - } - }, - }; - - let line: String = format!("{},{}\n", time, reading); - - // Write line to output - writer.write_all(line.as_bytes())?; - - // Increment output counter - *self - .output_cnt - .entry((remote_id, tdf_id.to_owned())) - .or_default() += 1; - Ok(()) - } - - fn iter_written(&self) -> impl Iterator, u16), &usize)> { - self.output_cnt.iter() - } - - fn written(&self, remote_id: Option, tdf_id: u16) -> usize { - match self.output_cnt.get(&(remote_id, tdf_id)) { - Some(val) => *val, - None => 0, - } - } -} - pub fn merge_input_files( output_prefix: &String, input_files: &Vec, @@ -188,7 +69,7 @@ pub struct DecodeWorkerArgsReporter { pub fn worker_run_decode(mut args: DecodeWorkerArgsReporter) { let mut block_counter: HashMap = HashMap::new(); - let mut csv_writer = TdfCsvWriter::new( + let mut csv_writer = output_csv::TdfCsvWriter::new( args.decode_args.decoder_idx, args.decode_args.output_folder, args.decode_args.output_unix_time, @@ -334,61 +215,11 @@ pub fn run( args.decode_reporter.stop(); // Merge output files - let results = stats_tdf.lock().unwrap(); - let num_files: usize = results.values().map(|inner| inner.len()).sum(); - - args.merge_reporter.start("Merging output files", num_files); - - for ((remote_id, tdf_id), worker_outputs) in results.iter() { - let id_prefix = match remote_id { - Some(id) => format!("_{:016x}", id), - None => "".to_string(), - }; - let output_path = args.output_folder.join(format!( - "{}{}_{}.csv", - args.output_prefix, - id_prefix, - tdf::decoders::tdf_name(tdf_id) - )); - output_files.push(output_path.clone()); - let err_path = output_path.clone(); - let output_file = File::create(output_path).map_err(|e| { - io::Error::new( - e.kind(), - format!( - "Failed to create output file '{}': {}", - err_path.display(), - e - ), - ) - })?; - let mut output = BufWriter::new(output_file); - let mut write_headings = true; - - for worker in worker_outputs.keys().sorted() { - let input_path = worker_outputs[worker].output.clone(); - let input = BufReader::new(File::open(&input_path)?); - - // Copy from input to output - for (idx, line) in input.lines().flatten().enumerate() { - if idx == 0 && !write_headings { - continue; - } - output.write_all(line.as_bytes())?; - output.write_all(b"\n")?; - } - // Remove input file - std::fs::remove_file(input_path)?; - write_headings = false; - - args.merge_reporter.increment(1); - } - output.flush()?; - } - args.merge_reporter.stop(); + output_csv::merge(args, &mut output_files, &stats_tdf)?; let block = stats_block.lock().unwrap().clone(); let mut tdf = HashMap::new(); + let results = stats_tdf.lock().unwrap(); for ((remote_id, tdf_id), worker_outputs) in results.iter() { let values = tdf.entry(*remote_id).or_insert_with(|| HashMap::new()); diff --git a/src/output_csv.rs b/src/output_csv.rs new file mode 100644 index 0000000..7a62692 --- /dev/null +++ b/src/output_csv.rs @@ -0,0 +1,190 @@ +use chrono::SecondsFormat; +use itertools::Itertools; +use std::collections::HashMap; +use std::collections::hash_map::Entry; +use std::fs::File; +use std::io::Cursor; +use std::io::{self, BufRead, BufReader, BufWriter, Write}; +use std::path::PathBuf; +use std::sync::{Arc, Mutex}; +use tdf::TdfOutput; + +use crate::{ProgressReporter, RunArgs, TdfDecoderOutputs}; + +pub struct TdfCsvWriter { + decoder_idx: usize, + output_folder: std::path::PathBuf, + output_unix: bool, + pub outputs: + HashMap<(Option, u16), (std::path::PathBuf, std::io::BufWriter)>, + output_cnt: HashMap<(Option, u16), usize>, +} +impl TdfCsvWriter { + pub fn new( + decoder_idx: usize, + output_folder: std::path::PathBuf, + output_unix_time: bool, + ) -> Self { + Self { + decoder_idx: decoder_idx, + output_folder: output_folder, + output_unix: output_unix_time, + outputs: HashMap::new(), + output_cnt: HashMap::new(), + } + } + + pub fn output_path(self: &Self, remote_id: Option, tdf_id: u16) -> Option { + self.outputs + .get(&(remote_id, tdf_id)) + .map(|(pathbuf, _)| pathbuf.clone()) + } +} + +impl TdfOutput for TdfCsvWriter { + fn write( + &mut self, + remote_id: Option, + tdf_id: u16, + tdf_time: i64, + tdf_idx: Option, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> std::io::Result<()> { + // Create writer if it doesn't exist + let (_, writer) = match self.outputs.entry((remote_id, tdf_id)) { + Entry::Occupied(o) => o.into_mut(), + Entry::Vacant(v) => { + let id_prefix = match remote_id { + Some(id) => format!("_{:016x}", id), + None => "".to_string(), + }; + let fname = format!( + "{}_{}_{:05}.csv", + id_prefix, + tdf::decoders::tdf_name(&tdf_id), + self.decoder_idx + ); + let path = self.output_folder.join(fname); + let mut writer = std::io::BufWriter::new(std::fs::File::create(path.clone())?); + + // Write header into file + let heading = tdf::decoders_csv::tdf_fields(&tdf_id).join(","); + writer.write_all(format!("time,{}\n", heading).as_bytes())?; + + // Touch the count variable in case the decoding fails + *self + .output_cnt + .entry((remote_id, tdf_id.to_owned())) + .or_default() += 0; + + // Insert into hashmap and return + v.insert((path, writer)) + } + }; + + // Construct CSV line + let reading = tdf::decoders_csv::tdf_read_into_str(&tdf_id, size, cursor)?; + let time = match tdf_idx { + Some(idx) => { + // Use the index directly if provided + format!("{idx}") + } + None => match self.output_unix { + // Otherwise, format the time to a string + true => { + let (unix_seconds, unix_nano) = tdf::time::tdf_time_to_unix(tdf_time); + format!("{}.{:06}", unix_seconds, unix_nano / 1000) + } + false => { + let datetime = tdf::time::tdf_time_to_datetime(tdf_time).expect("Invalid time"); + datetime.to_rfc3339_opts(SecondsFormat::Micros, true) + } + }, + }; + + let line: String = format!("{},{}\n", time, reading); + + // Write line to output + writer.write_all(line.as_bytes())?; + + // Increment output counter + *self + .output_cnt + .entry((remote_id, tdf_id.to_owned())) + .or_default() += 1; + Ok(()) + } + + fn iter_written(&self) -> impl Iterator, u16), &usize)> { + self.output_cnt.iter() + } + + fn written(&self, remote_id: Option, tdf_id: u16) -> usize { + match self.output_cnt.get(&(remote_id, tdf_id)) { + Some(val) => *val, + None => 0, + } + } +} + +pub fn merge( + args: &mut RunArgs, + output_files: &mut Vec, + stats_tdf: &Arc, u16), HashMap>>>, +) -> io::Result<()> { + let results = stats_tdf.lock().unwrap(); + let num_files: usize = results.values().map(|inner| inner.len()).sum(); + + args.merge_reporter.start("Merging output files", num_files); + + for ((remote_id, tdf_id), worker_outputs) in results.iter() { + let id_prefix = match remote_id { + Some(id) => format!("_{:016x}", id), + None => "".to_string(), + }; + let output_path = args.output_folder.join(format!( + "{}{}_{}.csv", + args.output_prefix, + id_prefix, + tdf::decoders::tdf_name(tdf_id) + )); + output_files.push(output_path.clone()); + let err_path = output_path.clone(); + let output_file = File::create(output_path).map_err(|e| { + io::Error::new( + e.kind(), + format!( + "Failed to create output file '{}': {}", + err_path.display(), + e + ), + ) + })?; + let mut output = BufWriter::new(output_file); + let mut write_headings = true; + + for worker in worker_outputs.keys().sorted() { + let input_path = worker_outputs[worker].output.clone(); + let input = BufReader::new(File::open(&input_path)?); + + // Copy from input to output + for (idx, line) in input.lines().flatten().enumerate() { + if idx == 0 && !write_headings { + continue; + } + output.write_all(line.as_bytes())?; + output.write_all(b"\n")?; + } + // Remove input file + std::fs::remove_file(input_path)?; + write_headings = false; + + args.merge_reporter.increment(1); + } + output.flush()?; + } + args.merge_reporter.stop(); + + Ok(()) +} From e43a3aa5148995bdcb5cf38e0bafe8715fbbfbfb Mon Sep 17 00:00:00 2001 From: Jordan Yates Date: Fri, 12 Jun 2026 14:29:04 +1000 Subject: [PATCH 2/5] scripts: tdf_decoder_build: generate TDF parquet schemas Generate Apache Parquet structure definitions alongside the CSV field names. Assisted-by: GPT-5.5 Signed-off-by: Jordan Yates --- scripts/tdf_decoder_build.py | 164 ++++++- scripts/tdf_decoder_parquet.rs.jinja | 34 ++ tdf/Cargo.toml | 1 + tdf/src/decoders_parquet.rs | 653 +++++++++++++++++++++++++++ tdf/src/lib.rs | 1 + 5 files changed, 841 insertions(+), 12 deletions(-) create mode 100644 scripts/tdf_decoder_parquet.rs.jinja create mode 100644 tdf/src/decoders_parquet.rs diff --git a/scripts/tdf_decoder_build.py b/scripts/tdf_decoder_build.py index b92230a..2414266 100644 --- a/scripts/tdf_decoder_build.py +++ b/scripts/tdf_decoder_build.py @@ -5,6 +5,7 @@ import json import os import pathlib +import re from numpy import format_float_positional as float_format from jinja2 import Environment, FileSystemLoader, select_autoescape @@ -25,6 +26,141 @@ "float64_t": ("f64", True), } +arrow_int_type = { + "int8_t": "DataType::Int8", + "uint8_t": "DataType::UInt8", + "int16_t": "DataType::Int16", + "uint16_t": "DataType::UInt16", + "int32_t": "DataType::Int32", + "uint32_t": "DataType::UInt32", + "int64_t": "DataType::Int64", + "uint64_t": "DataType::UInt64", +} + +arrow_float_type = { + "float": "DataType::Float32", + "float32_t": "DataType::Float32", + "float64_t": "DataType::Float64", +} + + +def rust_str(value): + return json.dumps(value) + + +def arrow_name(name): + name = re.sub(r"\W", "_", name) + if not name or name[0].isdigit(): + name = f"_{name}" + return name + + +def indent_block(value, indent): + pad = " " * indent + return "\n".join(f"{pad}{line}" for line in value.splitlines()) + + +def arrow_scalar_type(field): + c_type = field["type"] + conv = field.get("conversion", {}) + + if c_type == "char": + return "DataType::Utf8" + + if "m" in conv or "c" in conv: + return "DataType::Float64" + + if "int" in conv: + assert "num" in field + byte_len = field["num"] + if byte_len <= 1: + return "DataType::UInt8" + if byte_len <= 2: + return "DataType::UInt16" + if byte_len <= 4: + return "DataType::UInt32" + if byte_len <= 8: + return "DataType::UInt64" + return f"DataType::FixedSizeBinary({byte_len})" + + if c_type in arrow_int_type: + return arrow_int_type[c_type] + + if c_type in arrow_float_type: + return arrow_float_type[c_type] + + raise RuntimeError(f"Bad type '{c_type}'") + + +def arrow_data_type_expr(field, structs, indent): + c_type = field["type"] + conv = field.get("conversion", {}) + + if c_type.startswith("struct "): + struct_name = c_type.removeprefix("struct ") + if struct_name not in structs: + raise RuntimeError(f"Bad type '{c_type}'") + nested = ",\n".join( + arrow_field_expr(nested, structs, indent + 8) + for nested in structs[struct_name]["fields"] + ) + base = "DataType::Struct(Fields::from(vec![\n" + base += f"{indent_block(nested, indent + 4)}\n" + base += f"{' ' * indent}]))" + else: + base = arrow_scalar_type(field) + + if "num" not in field or field["type"] == "char" or "int" in conv: + return base + + num = field["num"] + if num == 0: + if c_type == "uint8_t": + return "DataType::Binary" + return ( + "DataType::List(Arc::new(Field::new_list_field(\n" + f"{indent_block(base, indent + 4)},\n" + f"{' ' * (indent + 4)}false,\n" + f"{' ' * indent})))" + ) + + if c_type == "uint8_t": + return f"DataType::FixedSizeList(Arc::new(Field::new_list_field(DataType::UInt8, false)), {num})" + return ( + "DataType::FixedSizeList(Arc::new(Field::new_list_field(\n" + f"{indent_block(base, indent + 4)},\n" + f"{' ' * (indent + 4)}false,\n" + f"{' ' * indent})), " + f"{num})" + ) + + +def arrow_field_expr(field, structs, indent): + field_name = rust_str(arrow_name(field["name"])) + data_type = arrow_data_type_expr(field, structs, indent + 4) + if "\n" not in data_type: + return f"Field::new({field_name}, {data_type}, false)" + return ( + "Field::new(\n" + f"{' ' * (indent + 4)}{field_name},\n" + f"{indent_block(data_type, indent + 4)},\n" + f"{' ' * (indent + 4)}false,\n" + f"{' ' * indent})" + ) + + +def arrow_schema_expr(info, structs): + fields = ",\n".join( + arrow_field_expr(field, structs, 12) for field in info["fields"] + ) + return ( + "Schema::new(vec![\n" + + indent_block("timestamp_field(),", 8) + + "\n" + + indent_block(fields, 8) + + "\n ])" + ) + def decoders_gen(tdf_defs, output): env = Environment( @@ -35,6 +171,10 @@ def decoders_gen(tdf_defs, output): ) common_template = env.get_template("tdf_decoder.rs.jinja") csv_template = env.get_template("tdf_decoder_csv.rs.jinja") + parquet_template = env.get_template("tdf_decoder_parquet.rs.jinja") + + for _tdf_id, info in tdf_defs["definitions"].items(): + info["arrow_schema"] = arrow_schema_expr(info, tdf_defs["structs"]) def field_conv_func(field, name_prefix=None): t = rust_type[field["type"]] @@ -138,19 +278,19 @@ def field_fmt(field): common_output = pathlib.Path(output) / "decoders.rs" csv_output = pathlib.Path(output) / "decoders_csv.rs" - with common_output.open("w") as f: - rendered = common_template.render( - structs=tdf_defs["structs"], definitions=tdf_defs["definitions"] - ) - f.write(rendered) - f.write(os.linesep) + parquet_output = pathlib.Path(output) / "decoders_parquet.rs" - with csv_output.open("w") as f: - rendered = csv_template.render( - structs=tdf_defs["structs"], definitions=tdf_defs["definitions"] - ) - f.write(rendered) - f.write(os.linesep) + def write_rendered(path, template): + with path.open("w", newline="\n") as f: + rendered = template.render( + structs=tdf_defs["structs"], definitions=tdf_defs["definitions"] + ) + f.write(rendered) + f.write(os.linesep) + + write_rendered(common_output, common_template) + write_rendered(csv_output, csv_template) + write_rendered(parquet_output, parquet_template) if __name__ == "__main__": diff --git a/scripts/tdf_decoder_parquet.rs.jinja b/scripts/tdf_decoder_parquet.rs.jinja new file mode 100644 index 0000000..596bcd3 --- /dev/null +++ b/scripts/tdf_decoder_parquet.rs.jinja @@ -0,0 +1,34 @@ +use std::sync::Arc; + +use arrow_schema::{DataType, Field, Fields, Schema, TimeUnit}; + +fn timestamp_field() -> Field { + Field::new( + "timestamp", + DataType::Timestamp(TimeUnit::Microsecond, Some("+00:00".into())), + false, + ) +} + +pub fn tdf_parquet_schemas() -> Vec<(u16, &'static str, Schema)> +{ + vec![ +{% for tdf_id, info in definitions.items() %} + ( + {{ tdf_id }}, + "{{ info['name'] }}", + tdf_parquet_schema(&{{ tdf_id }}).unwrap(), + ), +{% endfor %} + ] +} + +pub fn tdf_parquet_schema(tdf_id: &u16) -> Option +{ + match tdf_id { +{% for tdf_id, info in definitions.items() %} + {{ tdf_id }} => Some({{ info['arrow_schema'] }}), +{% endfor %} + _ => None, + } +} diff --git a/tdf/Cargo.toml b/tdf/Cargo.toml index 72b9d0a..c8c15a6 100644 --- a/tdf/Cargo.toml +++ b/tdf/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] +arrow-schema = "59.0.0" byteorder = "1.5.0" chrono = "0.4.38" hex = "0.4.3" diff --git a/tdf/src/decoders_parquet.rs b/tdf/src/decoders_parquet.rs new file mode 100644 index 0000000..a1ee44a --- /dev/null +++ b/tdf/src/decoders_parquet.rs @@ -0,0 +1,653 @@ +use std::sync::Arc; + +use arrow_schema::{DataType, Field, Fields, Schema, TimeUnit}; + +fn timestamp_field() -> Field { + Field::new( + "timestamp", + DataType::Timestamp(TimeUnit::Microsecond, Some("+00:00".into())), + false, + ) +} + +pub fn tdf_parquet_schemas() -> Vec<(u16, &'static str, Schema)> { + vec![ + (1, "ANNOUNCE", tdf_parquet_schema(&1).unwrap()), + (2, "BATTERY_STATE", tdf_parquet_schema(&2).unwrap()), + (3, "AMBIENT_TEMP_PRES_HUM", tdf_parquet_schema(&3).unwrap()), + (4, "AMBIENT_TEMPERATURE", tdf_parquet_schema(&4).unwrap()), + (5, "TIME_SYNC", tdf_parquet_schema(&5).unwrap()), + (6, "REBOOT_INFO", tdf_parquet_schema(&6).unwrap()), + (7, "ANNOUNCE_V2", tdf_parquet_schema(&7).unwrap()), + (8, "SOC_TEMPERATURE", tdf_parquet_schema(&8).unwrap()), + (10, "ACC_2G", tdf_parquet_schema(&10).unwrap()), + (11, "ACC_4G", tdf_parquet_schema(&11).unwrap()), + (12, "ACC_8G", tdf_parquet_schema(&12).unwrap()), + (13, "ACC_16G", tdf_parquet_schema(&13).unwrap()), + (14, "GYR_125DPS", tdf_parquet_schema(&14).unwrap()), + (15, "GYR_250DPS", tdf_parquet_schema(&15).unwrap()), + (16, "GYR_500DPS", tdf_parquet_schema(&16).unwrap()), + (17, "GYR_1000DPS", tdf_parquet_schema(&17).unwrap()), + (18, "GYR_2000DPS", tdf_parquet_schema(&18).unwrap()), + (19, "GCS_WGS84_LLHA", tdf_parquet_schema(&19).unwrap()), + (20, "UBX_NAV_PVT", tdf_parquet_schema(&20).unwrap()), + (21, "LTE_CONN_STATUS", tdf_parquet_schema(&21).unwrap()), + (22, "GLOBALSTAR_PKT", tdf_parquet_schema(&22).unwrap()), + ( + 23, + "ACC_MAGNITUDE_STD_DEV", + tdf_parquet_schema(&23).unwrap(), + ), + (24, "ACTIVITY_METRIC", tdf_parquet_schema(&24).unwrap()), + (25, "ALGORITHM_OUTPUT", tdf_parquet_schema(&25).unwrap()), + (26, "RUNTIME_ERROR", tdf_parquet_schema(&26).unwrap()), + (27, "CHARGER_EN_CONTROL", tdf_parquet_schema(&27).unwrap()), + (28, "GNSS_FIX_INFO", tdf_parquet_schema(&28).unwrap()), + (29, "BLUETOOTH_CONNECTION", tdf_parquet_schema(&29).unwrap()), + (30, "BLUETOOTH_RSSI", tdf_parquet_schema(&30).unwrap()), + ( + 31, + "BLUETOOTH_DATA_THROUGHPUT", + tdf_parquet_schema(&31).unwrap(), + ), + ( + 32, + "ALGORITHM_CLASS_HISTOGRAM", + tdf_parquet_schema(&32).unwrap(), + ), + ( + 33, + "ALGORITHM_CLASS_TIME_SERIES", + tdf_parquet_schema(&33).unwrap(), + ), + (34, "LTE_TAC_CELLS", tdf_parquet_schema(&34).unwrap()), + (35, "WIFI_AP_INFO", tdf_parquet_schema(&35).unwrap()), + (36, "DEVICE_TILT", tdf_parquet_schema(&36).unwrap()), + (37, "NRF9X_GNSS_PVT", tdf_parquet_schema(&37).unwrap()), + ( + 38, + "BATTERY_CHARGE_ACCUMULATED", + tdf_parquet_schema(&38).unwrap(), + ), + ( + 39, + "INFUSE_BLUETOOTH_RSSI", + tdf_parquet_schema(&39).unwrap(), + ), + (40, "ADC_RAW_8", tdf_parquet_schema(&40).unwrap()), + (41, "ADC_RAW_16", tdf_parquet_schema(&41).unwrap()), + (42, "ADC_RAW_32", tdf_parquet_schema(&42).unwrap()), + (43, "ANNOTATION", tdf_parquet_schema(&43).unwrap()), + (44, "LORA_RX", tdf_parquet_schema(&44).unwrap()), + (45, "LORA_TX", tdf_parquet_schema(&45).unwrap()), + (46, "IDX_ARRAY_FREQ", tdf_parquet_schema(&46).unwrap()), + (47, "IDX_ARRAY_PERIOD", tdf_parquet_schema(&47).unwrap()), + (48, "WIFI_CONNECTED", tdf_parquet_schema(&48).unwrap()), + ( + 49, + "WIFI_CONNECTION_FAILED", + tdf_parquet_schema(&49).unwrap(), + ), + (50, "WIFI_DISCONNECTED", tdf_parquet_schema(&50).unwrap()), + (51, "NETWORK_SCAN_COUNT", tdf_parquet_schema(&51).unwrap()), + ( + 52, + "EXCEPTION_STACK_FRAME", + tdf_parquet_schema(&52).unwrap(), + ), + (53, "BATTERY_VOLTAGE", tdf_parquet_schema(&53).unwrap()), + (54, "BATTERY_SOC", tdf_parquet_schema(&54).unwrap()), + (55, "STATE_EVENT_SET", tdf_parquet_schema(&55).unwrap()), + (56, "STATE_EVENT_CLEARED", tdf_parquet_schema(&56).unwrap()), + (57, "STATE_DURATION", tdf_parquet_schema(&57).unwrap()), + (58, "PCM_16BIT_CHAN_LEFT", tdf_parquet_schema(&58).unwrap()), + (59, "PCM_16BIT_CHAN_RIGHT", tdf_parquet_schema(&59).unwrap()), + (60, "PCM_16BIT_CHAN_DUAL", tdf_parquet_schema(&60).unwrap()), + (61, "KVS_VALUE_CHANGED", tdf_parquet_schema(&61).unwrap()), + ] +} + +pub fn tdf_parquet_schema(tdf_id: &u16) -> Option { + match tdf_id { + 1 => Some(Schema::new(vec![ + timestamp_field(), + Field::new("application", DataType::UInt32, false), + Field::new( + "version", + DataType::Struct(Fields::from(vec![ + Field::new("major", DataType::UInt8, false), + Field::new("minor", DataType::UInt8, false), + Field::new("revision", DataType::UInt16, false), + Field::new("build_num", DataType::UInt32, false), + ])), + false, + ), + Field::new("kv_crc", DataType::UInt32, false), + Field::new("blocks", DataType::UInt32, false), + Field::new("uptime", DataType::UInt32, false), + Field::new("reboots", DataType::UInt16, false), + Field::new("flags", DataType::UInt8, false), + ])), + 2 => Some(Schema::new(vec![ + timestamp_field(), + Field::new("voltage_mv", DataType::UInt32, false), + Field::new("current_ua", DataType::Int32, false), + Field::new("soc", DataType::UInt8, false), + ])), + 3 => Some(Schema::new(vec![ + timestamp_field(), + Field::new("temperature", DataType::Float64, false), + Field::new("pressure", DataType::Float64, false), + Field::new("humidity", DataType::Float64, false), + ])), + 4 => Some(Schema::new(vec![ + timestamp_field(), + Field::new("temperature", DataType::Float64, false), + ])), + 5 => Some(Schema::new(vec![ + timestamp_field(), + Field::new("source", DataType::UInt8, false), + Field::new("shift", DataType::Float64, false), + ])), + 6 => Some(Schema::new(vec![ + timestamp_field(), + Field::new("reason", DataType::UInt8, false), + Field::new("hardware_flags", DataType::UInt32, false), + Field::new("count", DataType::UInt32, false), + Field::new("uptime", DataType::UInt32, false), + Field::new("param_1", DataType::UInt32, false), + Field::new("param_2", DataType::UInt32, false), + Field::new("thread", DataType::Utf8, false), + ])), + 7 => Some(Schema::new(vec![ + timestamp_field(), + Field::new("application", DataType::UInt32, false), + Field::new( + "version", + DataType::Struct(Fields::from(vec![ + Field::new("major", DataType::UInt8, false), + Field::new("minor", DataType::UInt8, false), + Field::new("revision", DataType::UInt16, false), + Field::new("build_num", DataType::UInt32, false), + ])), + false, + ), + Field::new("board_crc", DataType::UInt16, false), + Field::new("kv_crc", DataType::UInt32, false), + Field::new("blocks", DataType::UInt32, false), + Field::new("uptime", DataType::UInt32, false), + Field::new("reboots", DataType::UInt16, false), + Field::new("flags", DataType::UInt8, false), + ])), + 8 => Some(Schema::new(vec![ + timestamp_field(), + Field::new("temperature", DataType::Float64, false), + ])), + 10 => Some(Schema::new(vec![ + timestamp_field(), + Field::new( + "sample", + DataType::Struct(Fields::from(vec![ + Field::new("x", DataType::Int16, false), + Field::new("y", DataType::Int16, false), + Field::new("z", DataType::Int16, false), + ])), + false, + ), + ])), + 11 => Some(Schema::new(vec![ + timestamp_field(), + Field::new( + "sample", + DataType::Struct(Fields::from(vec![ + Field::new("x", DataType::Int16, false), + Field::new("y", DataType::Int16, false), + Field::new("z", DataType::Int16, false), + ])), + false, + ), + ])), + 12 => Some(Schema::new(vec![ + timestamp_field(), + Field::new( + "sample", + DataType::Struct(Fields::from(vec![ + Field::new("x", DataType::Int16, false), + Field::new("y", DataType::Int16, false), + Field::new("z", DataType::Int16, false), + ])), + false, + ), + ])), + 13 => Some(Schema::new(vec![ + timestamp_field(), + Field::new( + "sample", + DataType::Struct(Fields::from(vec![ + Field::new("x", DataType::Int16, false), + Field::new("y", DataType::Int16, false), + Field::new("z", DataType::Int16, false), + ])), + false, + ), + ])), + 14 => Some(Schema::new(vec![ + timestamp_field(), + Field::new( + "sample", + DataType::Struct(Fields::from(vec![ + Field::new("x", DataType::Int16, false), + Field::new("y", DataType::Int16, false), + Field::new("z", DataType::Int16, false), + ])), + false, + ), + ])), + 15 => Some(Schema::new(vec![ + timestamp_field(), + Field::new( + "sample", + DataType::Struct(Fields::from(vec![ + Field::new("x", DataType::Int16, false), + Field::new("y", DataType::Int16, false), + Field::new("z", DataType::Int16, false), + ])), + false, + ), + ])), + 16 => Some(Schema::new(vec![ + timestamp_field(), + Field::new( + "sample", + DataType::Struct(Fields::from(vec![ + Field::new("x", DataType::Int16, false), + Field::new("y", DataType::Int16, false), + Field::new("z", DataType::Int16, false), + ])), + false, + ), + ])), + 17 => Some(Schema::new(vec![ + timestamp_field(), + Field::new( + "sample", + DataType::Struct(Fields::from(vec![ + Field::new("x", DataType::Int16, false), + Field::new("y", DataType::Int16, false), + Field::new("z", DataType::Int16, false), + ])), + false, + ), + ])), + 18 => Some(Schema::new(vec![ + timestamp_field(), + Field::new( + "sample", + DataType::Struct(Fields::from(vec![ + Field::new("x", DataType::Int16, false), + Field::new("y", DataType::Int16, false), + Field::new("z", DataType::Int16, false), + ])), + false, + ), + ])), + 19 => Some(Schema::new(vec![ + timestamp_field(), + Field::new( + "location", + DataType::Struct(Fields::from(vec![ + Field::new("latitude", DataType::Float64, false), + Field::new("longitude", DataType::Float64, false), + Field::new("height", DataType::Float64, false), + ])), + false, + ), + Field::new("h_acc", DataType::Float64, false), + Field::new("v_acc", DataType::Float64, false), + ])), + 20 => Some(Schema::new(vec![ + timestamp_field(), + Field::new("itow", DataType::UInt32, false), + Field::new("year", DataType::UInt16, false), + Field::new("month", DataType::UInt8, false), + Field::new("day", DataType::UInt8, false), + Field::new("hour", DataType::UInt8, false), + Field::new("min", DataType::UInt8, false), + Field::new("sec", DataType::UInt8, false), + Field::new("valid", DataType::UInt8, false), + Field::new("t_acc", DataType::UInt32, false), + Field::new("nano", DataType::Int32, false), + Field::new("fix_type", DataType::UInt8, false), + Field::new("flags", DataType::UInt8, false), + Field::new("flags2", DataType::UInt8, false), + Field::new("num_sv", DataType::UInt8, false), + Field::new("lon", DataType::Float64, false), + Field::new("lat", DataType::Float64, false), + Field::new("height", DataType::Float64, false), + Field::new("h_msl", DataType::Float64, false), + Field::new("h_acc", DataType::Float64, false), + Field::new("v_acc", DataType::Float64, false), + Field::new("vel_n", DataType::Float64, false), + Field::new("vel_e", DataType::Float64, false), + Field::new("vel_d", DataType::Float64, false), + Field::new("g_speed", DataType::Float64, false), + Field::new("head_mot", DataType::Float64, false), + Field::new("s_acc", DataType::Float64, false), + Field::new("head_acc", DataType::Float64, false), + Field::new("p_dop", DataType::Float64, false), + Field::new("flags3", DataType::UInt16, false), + Field::new( + "reserved0", + DataType::FixedSizeList(Arc::new(Field::new_list_field(DataType::UInt8, false)), 4), + false, + ), + Field::new("head_veh", DataType::Float64, false), + Field::new("mag_dec", DataType::Float64, false), + Field::new("mag_acc", DataType::Float64, false), + ])), + 21 => Some(Schema::new(vec![ + timestamp_field(), + Field::new( + "cell", + DataType::Struct(Fields::from(vec![ + Field::new("mcc", DataType::UInt16, false), + Field::new("mnc", DataType::UInt16, false), + Field::new("eci", DataType::UInt32, false), + Field::new("tac", DataType::UInt16, false), + ])), + false, + ), + Field::new("earfcn", DataType::UInt32, false), + Field::new("status", DataType::UInt8, false), + Field::new("tech", DataType::UInt8, false), + Field::new("rsrp", DataType::Float64, false), + Field::new("rsrq", DataType::Int8, false), + ])), + 22 => Some(Schema::new(vec![ + timestamp_field(), + Field::new( + "payload", + DataType::FixedSizeList(Arc::new(Field::new_list_field(DataType::UInt8, false)), 9), + false, + ), + ])), + 23 => Some(Schema::new(vec![ + timestamp_field(), + Field::new("count", DataType::UInt32, false), + Field::new("std_dev", DataType::UInt32, false), + ])), + 24 => Some(Schema::new(vec![ + timestamp_field(), + Field::new("value", DataType::UInt32, false), + ])), + 25 => Some(Schema::new(vec![ + timestamp_field(), + Field::new("algorithm_id", DataType::UInt32, false), + Field::new("algorithm_version", DataType::UInt16, false), + Field::new("output", DataType::Binary, false), + ])), + 26 => Some(Schema::new(vec![ + timestamp_field(), + Field::new("error_id", DataType::UInt32, false), + Field::new("error_ctx", DataType::UInt32, false), + ])), + 27 => Some(Schema::new(vec![ + timestamp_field(), + Field::new("enabled", DataType::UInt8, false), + ])), + 28 => Some(Schema::new(vec![ + timestamp_field(), + Field::new("time_fix", DataType::UInt16, false), + Field::new("location_fix", DataType::UInt16, false), + Field::new("num_sv", DataType::UInt8, false), + ])), + 29 => Some(Schema::new(vec![ + timestamp_field(), + Field::new( + "address", + DataType::Struct(Fields::from(vec![ + Field::new("type", DataType::UInt8, false), + Field::new("val", DataType::UInt64, false), + ])), + false, + ), + Field::new("connected", DataType::UInt8, false), + ])), + 30 => Some(Schema::new(vec![ + timestamp_field(), + Field::new( + "address", + DataType::Struct(Fields::from(vec![ + Field::new("type", DataType::UInt8, false), + Field::new("val", DataType::UInt64, false), + ])), + false, + ), + Field::new("rssi", DataType::Int8, false), + ])), + 31 => Some(Schema::new(vec![ + timestamp_field(), + Field::new( + "address", + DataType::Struct(Fields::from(vec![ + Field::new("type", DataType::UInt8, false), + Field::new("val", DataType::UInt64, false), + ])), + false, + ), + Field::new("throughput", DataType::Int32, false), + ])), + 32 => Some(Schema::new(vec![ + timestamp_field(), + Field::new("algorithm_id", DataType::UInt32, false), + Field::new("algorithm_version", DataType::UInt16, false), + Field::new("classes", DataType::Binary, false), + ])), + 33 => Some(Schema::new(vec![ + timestamp_field(), + Field::new("algorithm_id", DataType::UInt32, false), + Field::new("algorithm_version", DataType::UInt16, false), + Field::new("values", DataType::Binary, false), + ])), + 34 => Some(Schema::new(vec![ + timestamp_field(), + Field::new( + "cell", + DataType::Struct(Fields::from(vec![ + Field::new("mcc", DataType::UInt16, false), + Field::new("mnc", DataType::UInt16, false), + Field::new("eci", DataType::UInt32, false), + Field::new("tac", DataType::UInt16, false), + ])), + false, + ), + Field::new("earfcn", DataType::UInt32, false), + Field::new("rsrp", DataType::Float64, false), + Field::new("rsrq", DataType::Int8, false), + Field::new( + "neighbours", + DataType::List(Arc::new(Field::new_list_field( + DataType::Struct(Fields::from(vec![ + Field::new("earfcn", DataType::UInt32, false), + Field::new("pci", DataType::UInt16, false), + Field::new("time_diff", DataType::Float64, false), + Field::new("rsrp", DataType::Float64, false), + Field::new("rsrq", DataType::Int8, false), + ])), + false, + ))), + false, + ), + ])), + 35 => Some(Schema::new(vec![ + timestamp_field(), + Field::new( + "bssid", + DataType::Struct(Fields::from(vec![Field::new( + "val", + DataType::UInt64, + false, + )])), + false, + ), + Field::new("channel", DataType::UInt8, false), + Field::new("rsrp", DataType::Int8, false), + ])), + 36 => Some(Schema::new(vec![ + timestamp_field(), + Field::new("cosine", DataType::Float32, false), + ])), + 37 => Some(Schema::new(vec![ + timestamp_field(), + Field::new("lat", DataType::Float64, false), + Field::new("lon", DataType::Float64, false), + Field::new("height", DataType::Float64, false), + Field::new("h_acc", DataType::Float64, false), + Field::new("v_acc", DataType::Float64, false), + Field::new("h_speed", DataType::Float64, false), + Field::new("h_speed_acc", DataType::Float64, false), + Field::new("v_speed", DataType::Float64, false), + Field::new("v_speed_acc", DataType::Float64, false), + Field::new("head_mot", DataType::Float64, false), + Field::new("head_acc", DataType::Float64, false), + Field::new("year", DataType::UInt16, false), + Field::new("month", DataType::UInt8, false), + Field::new("day", DataType::UInt8, false), + Field::new("hour", DataType::UInt8, false), + Field::new("min", DataType::UInt8, false), + Field::new("sec", DataType::UInt8, false), + Field::new("ms", DataType::UInt16, false), + Field::new("p_dop", DataType::Float64, false), + Field::new("h_dop", DataType::Float64, false), + Field::new("v_dop", DataType::Float64, false), + Field::new("t_dop", DataType::Float64, false), + Field::new("flags", DataType::UInt8, false), + Field::new("num_sv", DataType::UInt8, false), + ])), + 38 => Some(Schema::new(vec![ + timestamp_field(), + Field::new("charge", DataType::Int32, false), + ])), + 39 => Some(Schema::new(vec![ + timestamp_field(), + Field::new("infuse_id", DataType::UInt64, false), + Field::new("rssi", DataType::Int8, false), + ])), + 40 => Some(Schema::new(vec![ + timestamp_field(), + Field::new("val", DataType::Int8, false), + ])), + 41 => Some(Schema::new(vec![ + timestamp_field(), + Field::new("val", DataType::Int16, false), + ])), + 42 => Some(Schema::new(vec![ + timestamp_field(), + Field::new("val", DataType::Int32, false), + ])), + 43 => Some(Schema::new(vec![ + timestamp_field(), + Field::new("timestamp", DataType::UInt32, false), + Field::new("event", DataType::Utf8, false), + ])), + 44 => Some(Schema::new(vec![ + timestamp_field(), + Field::new("snr", DataType::Int8, false), + Field::new("rssi", DataType::Int16, false), + Field::new("payload", DataType::Binary, false), + ])), + 45 => Some(Schema::new(vec![ + timestamp_field(), + Field::new("payload", DataType::Binary, false), + ])), + 46 => Some(Schema::new(vec![ + timestamp_field(), + Field::new("tdf_id", DataType::UInt16, false), + Field::new("frequency", DataType::UInt32, false), + ])), + 47 => Some(Schema::new(vec![ + timestamp_field(), + Field::new("tdf_id", DataType::UInt16, false), + Field::new("period", DataType::UInt32, false), + ])), + 48 => Some(Schema::new(vec![ + timestamp_field(), + Field::new( + "network", + DataType::Struct(Fields::from(vec![ + Field::new("bssid", DataType::UInt64, false), + Field::new("band", DataType::UInt8, false), + Field::new("channel", DataType::UInt8, false), + Field::new("iface_mode", DataType::UInt8, false), + Field::new("link_mode", DataType::UInt8, false), + Field::new("security", DataType::UInt8, false), + Field::new("rssi", DataType::Int8, false), + Field::new("beacon_interval", DataType::UInt16, false), + Field::new("twt_capable", DataType::UInt8, false), + ])), + false, + ), + ])), + 49 => Some(Schema::new(vec![ + timestamp_field(), + Field::new("reason", DataType::UInt8, false), + ])), + 50 => Some(Schema::new(vec![ + timestamp_field(), + Field::new("reason", DataType::UInt8, false), + ])), + 51 => Some(Schema::new(vec![ + timestamp_field(), + Field::new("num_wifi", DataType::UInt8, false), + Field::new("num_lte", DataType::UInt8, false), + ])), + 52 => Some(Schema::new(vec![ + timestamp_field(), + Field::new( + "frame", + DataType::List(Arc::new(Field::new_list_field(DataType::UInt32, false))), + false, + ), + ])), + 53 => Some(Schema::new(vec![ + timestamp_field(), + Field::new("voltage", DataType::UInt16, false), + ])), + 54 => Some(Schema::new(vec![ + timestamp_field(), + Field::new("soc", DataType::UInt8, false), + ])), + 55 => Some(Schema::new(vec![ + timestamp_field(), + Field::new("state", DataType::UInt8, false), + ])), + 56 => Some(Schema::new(vec![ + timestamp_field(), + Field::new("state", DataType::UInt8, false), + ])), + 57 => Some(Schema::new(vec![ + timestamp_field(), + Field::new("state", DataType::UInt8, false), + Field::new("duration", DataType::UInt32, false), + ])), + 58 => Some(Schema::new(vec![ + timestamp_field(), + Field::new("val", DataType::Int16, false), + ])), + 59 => Some(Schema::new(vec![ + timestamp_field(), + Field::new("val", DataType::Int16, false), + ])), + 60 => Some(Schema::new(vec![ + timestamp_field(), + Field::new("left", DataType::Int16, false), + Field::new("right", DataType::Int16, false), + ])), + 61 => Some(Schema::new(vec![ + timestamp_field(), + Field::new("key", DataType::UInt16, false), + Field::new("value", DataType::Binary, false), + ])), + _ => None, + } +} diff --git a/tdf/src/lib.rs b/tdf/src/lib.rs index 66775fe..ab1bbff 100644 --- a/tdf/src/lib.rs +++ b/tdf/src/lib.rs @@ -5,6 +5,7 @@ use std::io::{Cursor, ErrorKind, Read}; pub mod decoders; pub mod decoders_csv; +pub mod decoders_parquet; pub mod time; const TDF_TIME_MASK: u16 = 0xC000; From c6d5ff790e27a9089a5a3f6f7e9b75729bf2c6fa Mon Sep 17 00:00:00 2001 From: Jordan Yates Date: Mon, 15 Jun 2026 11:46:18 +1000 Subject: [PATCH 3/5] treewide: Apache Parquet output support Add support for Apache Parquet as an output format for decoded data. Decoding to Parquet files is faster than to CSV, the resulting files are smaller and the format is supported by an array of third-party tools. The downside is that it is a binary storage format, so the output files are not human-readable. Assisted-by: GPT-5.5 Signed-off-by: Jordan Yates --- .gitignore | 2 + Cargo.toml | 3 + scripts/tdf_decoder_build.py | 373 +- scripts/tdf_decoder_parquet.rs.jinja | 194 +- src/args.rs | 17 + src/lib.rs | 73 +- src/main_cli.rs | 12 + src/main_gui.rs | 62 +- src/output_common.rs | 95 + src/output_csv.rs | 293 +- src/output_parquet.rs | 415 ++ tdf/Cargo.toml | 2 + tdf/src/decoders.rs | 27 +- tdf/src/decoders_parquet.rs | 5332 +++++++++++++++++++++++++- tdf/src/lib.rs | 7 +- tdf/src/time.rs | 5 + 16 files changed, 6602 insertions(+), 310 deletions(-) create mode 100644 src/output_common.rs create mode 100644 src/output_parquet.rs diff --git a/.gitignore b/.gitignore index 3a24643..704bf14 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ /target +**/target/* *.bin *.csv +*.parquet Cargo.lock \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 498cffc..6a35194 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,9 @@ indicatif = "0.18.3" itertools = "0.14.0" memmap = "0.7.0" num_cpus = "1.17.0" +arrow-array = "59.0.0" +arrow-schema = "59.0.0" +parquet = "59.0.0" prettytable-rs = "0.10.0" regex = "1.12.2" rfd = "0.16.0" diff --git a/scripts/tdf_decoder_build.py b/scripts/tdf_decoder_build.py index 2414266..f047292 100644 --- a/scripts/tdf_decoder_build.py +++ b/scripts/tdf_decoder_build.py @@ -157,6 +157,8 @@ def arrow_schema_expr(info, structs): "Schema::new(vec![\n" + indent_block("timestamp_field(),", 8) + "\n" + + indent_block("sample_idx_field(),", 8) + + "\n" + indent_block(fields, 8) + "\n ])" ) @@ -182,8 +184,10 @@ def field_conv_func(field, name_prefix=None): if t[1]: func += "::" func += "()?" + int_endian = None if c := field.get("conversion"): - if endian := c.get("int", None): + int_endian = c.get("int", None) + if endian := int_endian: assert "num" in field assert t[0] == "u8" e = "LittleEndian" if endian == "little" else "BigEndian" @@ -195,7 +199,6 @@ def field_conv_func(field, name_prefix=None): raise RuntimeError("Unknown integer length") func = f"cursor.read_{t}::<{e}>()?" - del field["num"] if "m" in c or "c" in c: func += " as f64" @@ -216,7 +219,7 @@ def field_conv_func(field, name_prefix=None): if name_prefix is not None: n = f"{name_prefix}." + n - if "num" in field: + if "num" in field and not int_endian: if field["type"] == "char": return [ ( @@ -244,6 +247,8 @@ def field_fmt(field): single = ["0x{:x}"] else: single = ["{}"] + if field.get("conversion", {}).get("int", None): + return single if field.get("num", None) == 0: return ["{}"] return single * field.get("num", 1) @@ -276,6 +281,368 @@ def field_fmt(field): info["rust_head"] = ",".join([f'"{c[0]}"' for c in info["rust_convs"]]) info["rust_fmt"] = ",".join(fmt) + rust_array_type = { + "i8": "Int8Array", + "u8": "UInt8Array", + "i16": "Int16Array", + "u16": "UInt16Array", + "i32": "Int32Array", + "u32": "UInt32Array", + "i64": "Int64Array", + "u64": "UInt64Array", + "f32": "Float32Array", + "f64": "Float64Array", + } + + rust_vec_type = { + "char": "String", + "int8_t": "i8", + "uint8_t": "u8", + "int16_t": "i16", + "uint16_t": "u16", + "int32_t": "i32", + "uint32_t": "u32", + "int64_t": "i64", + "uint64_t": "u64", + "float": "f32", + "float32_t": "f32", + "float64_t": "f64", + } + + def rust_type_after_conversion(field): + conv = field.get("conversion", {}) + if "m" in conv or "c" in conv: + return "f64" + if field["type"] == "char": + return "String" + if "int" in conv: + byte_len = field["num"] + if byte_len <= 1: + return "u8" + if byte_len <= 2: + return "u16" + if byte_len <= 4: + return "u32" + if byte_len <= 8: + return "u64" + return rust_vec_type[field["type"]] + + def rust_pascal(value): + return "".join(part.capitalize() for part in re.split(r"\W|_", value) if part) + + def rust_field_ident(path): + return arrow_name("_".join(path)) + + def primitive_read_expr(field): + t = rust_type[field["type"]] + func = f"cursor.read_{t[0]}" + if t[1]: + func += "::" + func += "()?" + + if c := field.get("conversion"): + if endian := c.get("int", None): + e = "LittleEndian" if endian == "little" else "BigEndian" + if field["num"] == 3: + t_name = "u24" + elif field["num"] == 6: + t_name = "u48" + else: + raise RuntimeError("Unknown integer length") + func = f"cursor.read_{t_name}::<{e}>()?" + + if "m" in c or "c" in c: + func += " as f64" + if "m" in c and c["m"] != 0: + val = c["m"] + inverse_ratio = (1 / val).as_integer_ratio() + if inverse_ratio[1] == 1: + func += f" / {inverse_ratio[0]}.0" + else: + func += f" * {float_format(c['m'])}" + if "c" in c and c["c"] != 0: + func += f" + {float_format(c['c'])}" + return func + + def field_byte_size(field): + c_type = field["type"] + conv = field.get("conversion", {}) + if "int" in conv: + return field["num"] + if c_type.startswith("struct "): + struct_name = c_type.removeprefix("struct ") + return sum(field_byte_size(f) for f in tdf_defs["structs"][struct_name]["fields"]) + if c_type == "char": + return field.get("num", 0) + base = { + "int8_t": 1, + "uint8_t": 1, + "int16_t": 2, + "uint16_t": 2, + "int32_t": 4, + "uint32_t": 4, + "int64_t": 8, + "uint64_t": 8, + "float": 4, + "float32_t": 4, + "float64_t": 8, + }[c_type] + return base * field.get("num", 1) + + def field_model(field, path): + c_type = field["type"] + num = field.get("num", None) + conv = field.get("conversion", {}) + + if c_type.startswith("struct "): + struct_name = c_type.removeprefix("struct ") + child_fields = tdf_defs["structs"][struct_name]["fields"] + children = [field_model(child, path + [arrow_name(child["name"])]) for child in child_fields] + if num == 0: + return { + "kind": "list", + "path": path, + "child": {"kind": "struct", "path": path, "children": children}, + "item_size": field_byte_size(field), + "field_expr": arrow_field_expr(field, tdf_defs["structs"], 12), + } + return { + "kind": "struct", + "path": path, + "children": children, + "field_expr": arrow_field_expr(field, tdf_defs["structs"], 12), + } + + if c_type == "char": + return { + "kind": "string", + "path": path, + "field_expr": arrow_field_expr(field, tdf_defs["structs"], 12), + "read": f"tdf_field_read_string_to_string(cursor, cursor_start, {num or 0}, size)?", + } + + if num == 0 and c_type == "uint8_t" and "int" not in conv: + return { + "kind": "binary", + "path": path, + "field_expr": arrow_field_expr(field, tdf_defs["structs"], 12), + } + + if num == 0: + return { + "kind": "list", + "path": path, + "child": field_model({k: v for k, v in field.items() if k != "num"}, path), + "item_size": field_byte_size({k: v for k, v in field.items() if k != "num"}), + "field_expr": arrow_field_expr(field, tdf_defs["structs"], 12), + } + + if num is not None and "int" not in conv: + return { + "kind": "fixed_list", + "path": path, + "child": field_model({k: v for k, v in field.items() if k != "num"}, path), + "num": num, + "field_expr": arrow_field_expr(field, tdf_defs["structs"], 12), + } + + rust_type_name = rust_type_after_conversion(field) + return { + "kind": "primitive", + "path": path, + "rust_type": rust_type_name, + "array_type": rust_array_type[rust_type_name], + "field_expr": arrow_field_expr(field, tdf_defs["structs"], 12), + "read": primitive_read_expr(field), + } + + def model_storage_fields(model, out): + kind = model["kind"] + if kind == "primitive": + out.append((rust_field_ident(model["path"]), f"Vec<{model['rust_type']}>")) + elif kind == "string": + out.append((rust_field_ident(model["path"]), "Vec")) + elif kind == "binary": + out.append((rust_field_ident(model["path"]), "Vec>")) + elif kind == "fixed_list": + model_storage_fields(model["child"], out) + elif kind == "list": + out.append((rust_field_ident(model["path"]) + "_offsets", "Vec")) + model_storage_fields(model["child"], out) + elif kind == "struct": + for child in model["children"]: + model_storage_fields(child, out) + else: + raise RuntimeError(f"Bad model kind {kind}") + + def model_init_fields(model, out, capacity): + kind = model["kind"] + if kind == "primitive": + out.append(f"{rust_field_ident(model['path'])}: Vec::with_capacity({capacity})") + elif kind == "string": + out.append(f"{rust_field_ident(model['path'])}: Vec::with_capacity({capacity})") + elif kind == "binary": + out.append(f"{rust_field_ident(model['path'])}: Vec::with_capacity({capacity})") + elif kind == "fixed_list": + model_init_fields(model["child"], out, f"{capacity} * {model['num']}") + elif kind == "list": + out.append(f"{rust_field_ident(model['path'])}_offsets: vec![0]") + model_init_fields(model["child"], out, capacity) + elif kind == "struct": + for child in model["children"]: + model_init_fields(child, out, capacity) + else: + raise RuntimeError(f"Bad model kind {kind}") + + def model_append_lines(model, out, cursor_name="cursor"): + kind = model["kind"] + if kind == "primitive": + out.append(f"self.{rust_field_ident(model['path'])}.push({model['read']});") + elif kind == "string": + out.append(f"self.{rust_field_ident(model['path'])}.push({model['read']});") + elif kind == "binary": + out.append( + f"self.{rust_field_ident(model['path'])}.push(crate::decoders::tdf_field_read_vla({cursor_name}, cursor_start, size)?);" + ) + elif kind == "fixed_list": + for _ in range(model["num"]): + model_append_lines(model["child"], out, cursor_name) + elif kind == "list": + out.append("{") + out.append( + f" let bytes_remaining = crate::decoders::vla_bytes_remaining({cursor_name}, cursor_start, size)?;" + ) + out.append(f" if bytes_remaining % {model['item_size']} != 0 {{") + out.append( + ' return Err(Error::new(ErrorKind::InvalidData, "Variable-length array does not align to element size"));' + ) + out.append(" }") + out.append(f" let item_count = bytes_remaining / {model['item_size']};") + out.append(" for _ in 0..item_count {") + child_lines = [] + model_append_lines(model["child"], child_lines, cursor_name) + out.extend([f" {line}" for line in child_lines]) + out.append(" }") + out.append( + f" self.{rust_field_ident(model['path'])}_offsets.push(*self.{rust_field_ident(model['path'])}_offsets.last().unwrap() + item_count as i32);" + ) + out.append("}") + elif kind == "struct": + for child in model["children"]: + model_append_lines(child, out, cursor_name) + else: + raise RuntimeError(f"Bad model kind {kind}") + + def model_finish_expr(model): + kind = model["kind"] + if kind == "primitive": + return f"Arc::new({model['array_type']}::from(std::mem::take(&mut self.{rust_field_ident(model['path'])}))) as ArrayRef" + if kind == "string": + return f"Arc::new(StringArray::from_iter_values(std::mem::take(&mut self.{rust_field_ident(model['path'])}))) as ArrayRef" + if kind == "binary": + return f"Arc::new(BinaryArray::from_iter_values(std::mem::take(&mut self.{rust_field_ident(model['path'])}))) as ArrayRef" + if kind == "fixed_list": + child = model_finish_expr(model["child"]) + child_field = arrow_data_type_expr( + {k: v for k, v in model.get("raw_field", {}).items() if k != "num"}, + tdf_defs["structs"], + 16, + ) + return ( + "Arc::new(FixedSizeListArray::try_new(\n" + f" Arc::new(Field::new_list_field({child_field}, false)),\n" + f" {model['num']},\n" + f" {child},\n" + " None,\n" + " )?) as ArrayRef" + ) + if kind == "list": + child = model_finish_expr(model["child"]) + offsets = rust_field_ident(model["path"]) + "_offsets" + return ( + "{\n" + f" let offsets = std::mem::replace(&mut self.{offsets}, vec![0]);\n" + f" Arc::new(ListArray::try_new(\n" + f" self.list_value_field({model['field_index']}),\n" + " OffsetBuffer::new(ScalarBuffer::from(offsets)),\n" + f" {child},\n" + " None,\n" + " )?) as ArrayRef\n" + " }" + ) + if kind == "struct": + child_fields = ",\n ".join(child["field_expr"] for child in model["children"]) + child_arrays = ",\n ".join(model_finish_expr(child) for child in model["children"]) + return ( + "Arc::new(StructArray::try_new(\n" + f" Fields::from(vec![\n {child_fields}\n ]),\n" + f" vec![\n {child_arrays}\n ],\n" + " None,\n" + " )?) as ArrayRef" + ) + raise RuntimeError(f"Bad model kind {kind}") + + def model_set_raw_field(model, field): + model["raw_field"] = field + if model["kind"] in ("fixed_list", "list"): + child = {k: v for k, v in field.items() if k != "num"} + model_set_raw_field(model["child"], child) + + def model_has_list(model): + if model["kind"] == "list": + return True + if model["kind"] == "fixed_list": + return model_has_list(model["child"]) + if model["kind"] == "struct": + return any(model_has_list(child) for child in model["children"]) + return False + + for tdf_id, info in tdf_defs["definitions"].items(): + info["rust_builder_name"] = f"Tdf{tdf_id}{rust_pascal(info['name'])}Builder" + info["rust_variant_name"] = f"Tdf{tdf_id}{rust_pascal(info['name'])}" + info["parquet_fields"] = [] + models = [] + for idx, field in enumerate(info["fields"]): + model = field_model(field, [arrow_name(field["name"])]) + model["field_index"] = idx + 2 + model_set_raw_field(model, field) + models.append(model) + info["parquet_models"] = models + info["parquet_has_lists"] = any(model_has_list(model) for model in models) + + storage_fields = [ + ("row_timestamp", "Vec>"), + ("row_sample_idx", "Vec>"), + ] + for model in models: + model_storage_fields(model, storage_fields) + info["parquet_storage_fields"] = storage_fields + + init_fields = [ + "row_timestamp: Vec::with_capacity(capacity)", + "row_sample_idx: Vec::with_capacity(capacity)", + ] + for model in models: + model_init_fields(model, init_fields, "capacity") + info["parquet_init_fields"] = init_fields + + append_lines = [ + "self.row_timestamp.push(meta.time_unix_micros);", + "self.row_sample_idx.push(meta.sample_idx);", + ] + for model in models: + model_append_lines(model, append_lines) + info["parquet_append_lines"] = append_lines + + finish_arrays = [ + 'Arc::new(TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)).with_timezone("+00:00")) as ArrayRef', + "Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef", + ] + for idx, model in enumerate(models): + model["field_index"] = idx + 2 + finish_arrays.append(model_finish_expr(model)) + info["parquet_finish_arrays"] = finish_arrays + common_output = pathlib.Path(output) / "decoders.rs" csv_output = pathlib.Path(output) / "decoders_csv.rs" parquet_output = pathlib.Path(output) / "decoders_parquet.rs" diff --git a/scripts/tdf_decoder_parquet.rs.jinja b/scripts/tdf_decoder_parquet.rs.jinja index 596bcd3..18df8c6 100644 --- a/scripts/tdf_decoder_parquet.rs.jinja +++ b/scripts/tdf_decoder_parquet.rs.jinja @@ -1,34 +1,212 @@ +use std::io::{Cursor, Error, ErrorKind, Read, Result}; use std::sync::Arc; -use arrow_schema::{DataType, Field, Fields, Schema, TimeUnit}; +use arrow_array::{ + ArrayRef, BinaryArray, FixedSizeListArray, Float32Array, Float64Array, Int8Array, Int16Array, + Int32Array, ListArray, RecordBatch, StringArray, StructArray, + TimestampMicrosecondArray, UInt8Array, UInt16Array, UInt32Array, UInt64Array, +}; +use arrow_buffer::{OffsetBuffer, ScalarBuffer}; +use arrow_schema::{ArrowError, DataType, Field, Fields, Schema, SchemaRef, TimeUnit}; +use byteorder::{BigEndian, LittleEndian, ReadBytesExt}; fn timestamp_field() -> Field { Field::new( "timestamp", DataType::Timestamp(TimeUnit::Microsecond, Some("+00:00".into())), - false, + true, ) } -pub fn tdf_parquet_schemas() -> Vec<(u16, &'static str, Schema)> -{ +fn sample_idx_field() -> Field { + Field::new("sample_idx", DataType::UInt16, true) +} + +fn tdf_field_read_string_to_string( + cursor: &mut Cursor<&[u8]>, + cursor_start: u64, + num: u8, + size: u8, +) -> Result { + let buf = crate::decoders::tdf_field_read_string(cursor, cursor_start, num, size)?; + + match String::from_utf8(buf) { + Ok(val) => Ok(val.trim_matches(char::from(0)).to_string()), + Err(..) => Ok(String::new()), + } +} + +fn finish_tdf_read(cursor: &mut Cursor<&[u8]>, cursor_start: u64, size: u8) -> Result<()> { + let cursor_end = cursor.position(); + let cursor_read = cursor_end - cursor_start; + + if (size as u64) < cursor_read { + return Err(Error::new( + ErrorKind::InvalidData, + "Read overflow, corrupt data/metadata", + )); + } + + let underflow = size as u64 - cursor_read; + if underflow > 0 { + let mut buf = vec![0; underflow as usize]; + cursor.read_exact(&mut buf)?; + } + + Ok(()) +} + +pub fn tdf_parquet_schemas() -> Vec<(u16, &'static str, SchemaRef)> { vec![ {% for tdf_id, info in definitions.items() %} ( {{ tdf_id }}, "{{ info['name'] }}", - tdf_parquet_schema(&{{ tdf_id }}).unwrap(), + tdf_parquet_schema({{ tdf_id }}).unwrap(), ), {% endfor %} ] } -pub fn tdf_parquet_schema(tdf_id: &u16) -> Option -{ +pub fn tdf_parquet_has_schema(tdf_id: u16) -> bool { match tdf_id { {% for tdf_id, info in definitions.items() %} - {{ tdf_id }} => Some({{ info['arrow_schema'] }}), + {{ tdf_id }} => true, +{% endfor %} + _ => false, + } +} + +pub fn tdf_parquet_schema(tdf_id: u16) -> Option { + match tdf_id { +{% for tdf_id, info in definitions.items() %} + {{ tdf_id }} => Some(Arc::new({{ info['arrow_schema'] }})), {% endfor %} _ => None, } } + +pub fn tdf_parquet_builder(tdf_id: u16, capacity: usize) -> Option { + match tdf_id { +{% for tdf_id, info in definitions.items() %} + {{ tdf_id }} => Some(TdfParquetBatchBuilder::{{ info['rust_variant_name'] }}( + {{ info['rust_builder_name'] }}::new(capacity), + )), +{% endfor %} + _ => None, + } +} + +#[derive(Clone, Copy, Debug)] +pub struct TdfParquetRowMeta { + pub time_unix_micros: Option, + pub sample_idx: Option, +} + +pub enum TdfParquetBatchBuilder { +{% for _tdf_id, info in definitions.items() %} + {{ info['rust_variant_name'] }}({{ info['rust_builder_name'] }}), +{% endfor %} +} + +impl TdfParquetBatchBuilder { + pub fn schema(&self) -> SchemaRef { + match self { +{% for _tdf_id, info in definitions.items() %} + Self::{{ info['rust_variant_name'] }}(builder) => builder.schema(), +{% endfor %} + } + } + + pub fn rows(&self) -> usize { + match self { +{% for _tdf_id, info in definitions.items() %} + Self::{{ info['rust_variant_name'] }}(builder) => builder.rows(), +{% endfor %} + } + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + match self { +{% for _tdf_id, info in definitions.items() %} + Self::{{ info['rust_variant_name'] }}(builder) => builder.append(meta, size, cursor), +{% endfor %} + } + } + + pub fn finish_batch(&mut self) -> std::result::Result { + match self { +{% for _tdf_id, info in definitions.items() %} + Self::{{ info['rust_variant_name'] }}(builder) => builder.finish_batch(), +{% endfor %} + } + } +} + +{% for tdf_id, info in definitions.items() %} +pub struct {{ info['rust_builder_name'] }} { +{% for name, ty in info['parquet_storage_fields'] %} + {{ name }}: {{ ty }}, +{% endfor %} +} + +impl {{ info['rust_builder_name'] }} { + pub fn new(capacity: usize) -> Self { + Self { +{% for init in info['parquet_init_fields'] %} + {{ init }}, +{% endfor %} + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema({{ tdf_id }}).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + +{% if info['parquet_has_lists'] %} + fn list_value_field(&self, field_index: usize) -> Arc { + let schema = self.schema(); + match schema.field(field_index).data_type() { + DataType::List(field) => field.clone(), + _ => unreachable!("generated list field index is not a list"), + } + } +{% endif %} + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + +{% for line in info['parquet_append_lines'] %} + {{ line }} +{% endfor %} + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ +{% for array in info['parquet_finish_arrays'] %} + {{ array }}, +{% endfor %} + ]; + + RecordBatch::try_new(schema, columns) + } +} + +{% endfor %} diff --git a/src/args.rs b/src/args.rs index be779ae..e6d5166 100644 --- a/src/args.rs +++ b/src/args.rs @@ -18,3 +18,20 @@ impl fmt::Display for BlockSizeOptions { } } } + +#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)] +pub enum OutputFormat { + #[value(name = "csv")] + CSV, + #[value(name = "parquet")] + PARQUET, +} + +impl fmt::Display for OutputFormat { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + OutputFormat::CSV => write!(f, "csv"), + OutputFormat::PARQUET => write!(f, "Parquet"), + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 9f08849..cd26dd3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,7 +10,11 @@ use tdf::TdfOutput; pub mod args; pub mod fs_util; +mod output_common; mod output_csv; +mod output_parquet; + +pub const DEFAULT_MAX_READINGS_PER_OUTPUT_FILE: usize = 0; pub trait ProgressReporter { /// Called when progress starts. Could be used to initialize the state or display a start message. @@ -48,10 +52,12 @@ pub struct DecodeWorkerArgs { pub decoder_idx: usize, pub input_file: std::path::PathBuf, pub output_folder: std::path::PathBuf, + pub output_prefix: String, pub output_unix_time: bool, pub start_block: usize, pub num_blocks: usize, pub block_size: usize, + pub output_format: args::OutputFormat, } #[derive(Clone)] @@ -67,14 +73,11 @@ pub struct DecodeWorkerArgsReporter { pub reporter: T, } -pub fn worker_run_decode(mut args: DecodeWorkerArgsReporter) { +pub fn worker_run_decode( + mut args: DecodeWorkerArgsReporter, + mut writer: U, +) { let mut block_counter: HashMap = HashMap::new(); - let mut csv_writer = output_csv::TdfCsvWriter::new( - args.decode_args.decoder_idx, - args.decode_args.output_folder, - args.decode_args.output_unix_time, - ); - // Open file let file = File::open(args.decode_args.input_file.clone()).unwrap(); let mmap = unsafe { Mmap::map(&file).unwrap() }; @@ -90,7 +93,7 @@ pub fn worker_run_decode(mut args: DecodeWorkerArgsReporter .chunks_exact(args.decode_args.block_size) .enumerate() { - match blocks::decode_block(&mut csv_writer, block) { + match blocks::decode_block(&mut writer, block) { Ok(block_type) => *block_counter.entry(block_type).or_default() += 1, Err(_) => *block_counter.entry(blocks::BlockTypes::ERROR).or_default() += 1, } @@ -104,11 +107,11 @@ pub fn worker_run_decode(mut args: DecodeWorkerArgsReporter // Push TDF stats into the output hashmap let mut tdf_stats = args.tdf_stats.lock().unwrap(); - for ((remote_id, tdf_id), tdf_cnt) in csv_writer.iter_written() { + for ((remote_id, tdf_id), tdf_cnt) in writer.iter_written() { let res = tdf_stats .entry((*remote_id, *tdf_id)) .or_insert_with(|| HashMap::new()); - let path = csv_writer.output_path(*remote_id, *tdf_id).unwrap(); + let path = writer.output_path(*remote_id, *tdf_id).unwrap(); res.insert( args.decode_args.decoder_idx, @@ -134,6 +137,9 @@ pub struct RunArgs { pub output_folder: PathBuf, pub output_prefix: String, pub output_unix_time: bool, + pub output_format: args::OutputFormat, + pub merge_output_files: bool, + pub max_readings_per_output_file: usize, pub copy_reporter: T, pub decode_reporter: T, pub merge_reporter: T, @@ -189,10 +195,12 @@ pub fn run( decoder_idx: idx, input_file: merged_file.clone(), output_folder: args.output_folder.clone(), + output_prefix: args.output_prefix.clone(), output_unix_time: args.output_unix_time, start_block: idx * blocks_per_worker, num_blocks: num, block_size: args.block_size, + output_format: args.output_format, }, block_stats: stats_block.clone(), tdf_stats: stats_tdf.clone(), @@ -204,7 +212,25 @@ pub fn run( let mut workers = vec![]; for worker_arg in worker_args.into_iter() { workers.push(thread::spawn(move || { - worker_run_decode(worker_arg); + match worker_arg.decode_args.output_format { + args::OutputFormat::CSV => { + let writer = output_csv::TdfCsvWriter::new( + worker_arg.decode_args.decoder_idx, + worker_arg.decode_args.output_folder.clone(), + worker_arg.decode_args.output_prefix.clone(), + worker_arg.decode_args.output_unix_time, + ); + worker_run_decode(worker_arg, writer); + } + args::OutputFormat::PARQUET => { + let writer = output_parquet::TdfParquetWriter::new( + worker_arg.decode_args.decoder_idx, + worker_arg.decode_args.output_folder.clone(), + worker_arg.decode_args.output_prefix.clone(), + ); + worker_run_decode(worker_arg, writer); + } + }; })); } @@ -214,8 +240,29 @@ pub fn run( } args.decode_reporter.stop(); - // Merge output files - output_csv::merge(args, &mut output_files, &stats_tdf)?; + if args.merge_output_files { + match args.output_format { + args::OutputFormat::CSV => { + output_csv::merge(args, &mut output_files, &stats_tdf)?; + } + args::OutputFormat::PARQUET => { + output_parquet::merge_with_threshold( + args, + &mut output_files, + &stats_tdf, + args.max_readings_per_output_file, + )?; + } + } + } else { + let results = stats_tdf.lock().unwrap(); + let mut worker_output_files: Vec = results + .values() + .flat_map(|worker_outputs| worker_outputs.values().map(|output| output.output.clone())) + .collect(); + worker_output_files.sort(); + output_files.extend(worker_output_files); + } let block = stats_block.lock().unwrap().clone(); let mut tdf = HashMap::new(); diff --git a/src/main_cli.rs b/src/main_cli.rs index c9a7620..3a7ec30 100644 --- a/src/main_cli.rs +++ b/src/main_cli.rs @@ -1,5 +1,6 @@ use clap::Parser; use indicatif::{ProgressBar, ProgressStyle}; +use infuse_decoder::args; use std::collections::HashMap; use std::io; use std::path::PathBuf; @@ -56,11 +57,19 @@ struct Cli { /// Write Unix timestamps instead of UTC strings #[arg(short, long)] unix: bool, + #[arg(long, default_value_t = args::OutputFormat::CSV)] + format: args::OutputFormat, /// Verbose CLI output #[arg(short, long)] verbose: bool, #[arg(long, default_value_t = infuse_decoder::args::BlockSizeOptions::B512)] block_size: infuse_decoder::args::BlockSizeOptions, + /// Maximum readings per output file (0 is no limit) + #[arg(long, default_value_t = infuse_decoder::DEFAULT_MAX_READINGS_PER_OUTPUT_FILE)] + max_readings_per_output_file: usize, + /// Keep decoder worker output files instead of merging them into linearized outputs + #[arg(long = "no-linearize-output", alias = "no-merge-output-files")] + no_linearize_output: bool, } fn main() -> io::Result<()> { @@ -108,6 +117,9 @@ fn main() -> io::Result<()> { output_folder: args.output.clone(), output_prefix: output_prefix, output_unix_time: args.unix, + output_format: args.format, + merge_output_files: !args.no_linearize_output, + max_readings_per_output_file: args.max_readings_per_output_file, copy_reporter: IndicatifProgress::new(), decode_reporter: IndicatifProgress::new(), merge_reporter: IndicatifProgress::new(), diff --git a/src/main_gui.rs b/src/main_gui.rs index 13b2239..b842f8f 100644 --- a/src/main_gui.rs +++ b/src/main_gui.rs @@ -8,6 +8,7 @@ use std::{collections::HashMap, path::PathBuf}; use eframe::egui::{self, IconData}; use egui_extras::{Column, TableBuilder}; use image::GenericImageView; +use infuse_decoder::args::OutputFormat; use rfd::FileDialog; use infuse_decoder::args::BlockSizeOptions; @@ -78,8 +79,11 @@ impl infuse_decoder::ProgressReporter for SliderState { struct MyApp { time_mode: TimeOutput, + output_format: OutputFormat, + linearize_output_files: bool, device_id: u64, block_size: BlockSizeOptions, + max_readings_per_output_file: usize, error_msg: Option, input_path: Option, input_files: Option>>, @@ -124,8 +128,11 @@ impl Default for MyApp { Self { time_mode: TimeOutput::UTC, + output_format: OutputFormat::CSV, + linearize_output_files: true, device_id: 0, block_size: BlockSizeOptions::B512, + max_readings_per_output_file: infuse_decoder::DEFAULT_MAX_READINGS_PER_OUTPUT_FILE, error_msg: None, input_path: None, input_files: None, @@ -276,29 +283,57 @@ fn core_options(app: &mut MyApp, ui: &mut egui::Ui) { ui.label("Output Prefix"); ui.text_edit_singleline(&mut app.output_prefix); - ui.label(format!("(e.g. {}_BATTERY_STATE.csv)", app.output_prefix)); + let extension = match app.output_format { + OutputFormat::CSV => "csv", + OutputFormat::PARQUET => "parquet", + }; + ui.label(format!( + "(e.g. {}_BATTERY_STATE.{extension})", + app.output_prefix + )); ui.end_row(); }); } fn decode_options(app: &mut MyApp, ui: &mut egui::Ui) { ui.horizontal(|ui| { + ui.vertical(|ui| { + ui.label("Output Format"); + ui.radio_value(&mut app.output_format, OutputFormat::CSV, "CSV"); + ui.radio_value(&mut app.output_format, OutputFormat::PARQUET, "Parquet"); + }); + ui.separator(); + ui.vertical(|ui| { + ui.label("File Output Control"); + ui.checkbox(&mut app.linearize_output_files, "Linearize Output"); + ui.label("Max Readings Per File"); + ui.add_enabled_ui(app.linearize_output_files, |ui| { + ui.add( + egui::DragValue::new(&mut app.max_readings_per_output_file) + .range(0..=usize::MAX) + .speed(10_000), + ); + }); + }); + ui.separator(); ui.vertical(|ui| { ui.label("Time Output Format"); - ui.radio_value( - &mut app.time_mode, - TimeOutput::UTC, - "UTC (2020-01-01T00:00:00.000000Z)", - ); - ui.radio_value( - &mut app.time_mode, - TimeOutput::UNIX, - "UNIX (1577800800.000000)", - ); + ui.add_enabled_ui(app.output_format == OutputFormat::CSV, |ui| { + ui.radio_value( + &mut app.time_mode, + TimeOutput::UTC, + "UTC (2020-01-01T00:00:00.000000Z)", + ); + ui.radio_value( + &mut app.time_mode, + TimeOutput::UNIX, + "UNIX (1577800800.000000)", + ); + }); }); ui.separator(); ui.vertical(|ui| { - ui.label("Block Size"); + ui.label("Input Block Size"); egui::ComboBox::from_id_salt("Block Size") .selected_text(format!("{:}", app.block_size)) .show_ui(ui, |ui| { @@ -347,6 +382,9 @@ fn start_button(app: &mut MyApp, ui: &mut egui::Ui) { output_folder: app.output_folder.clone(), output_prefix: app.output_prefix.clone(), output_unix_time: app.time_mode == TimeOutput::UNIX, + output_format: app.output_format, + merge_output_files: app.linearize_output_files, + max_readings_per_output_file: app.max_readings_per_output_file, copy_reporter: app.progress_copy.clone(), decode_reporter: app.progress_decode.clone(), merge_reporter: app.progress_merge.clone(), diff --git a/src/output_common.rs b/src/output_common.rs new file mode 100644 index 0000000..726aaf6 --- /dev/null +++ b/src/output_common.rs @@ -0,0 +1,95 @@ +use std::collections::HashMap; +use std::fs; +use std::io; +use std::path::{Path, PathBuf}; + +pub(crate) type OutputKey = (Option, u16); + +pub(crate) fn worker_output_path( + output_folder: &Path, + output_prefix: &str, + remote_id: Option, + tdf_id: u16, + decoder_idx: usize, + extension: &str, +) -> PathBuf { + let mut fname_parts = Vec::new(); + if !output_prefix.is_empty() { + fname_parts.push(output_prefix.to_string()); + } + if let Some(id) = remote_id { + fname_parts.push(format!("{id:016x}")); + } + fname_parts.push(tdf::decoders::tdf_name(&tdf_id).to_string()); + + output_folder.join(format!( + "{}_{:05}.{}", + fname_parts.join("_"), + decoder_idx, + extension + )) +} + +pub(crate) fn merged_output_path( + output_folder: &Path, + output_prefix: &str, + remote_id: Option, + tdf_id: u16, + part_idx: Option, + extension: &str, +) -> PathBuf { + let id_prefix = match remote_id { + Some(id) => format!("_{id:016x}"), + None => String::new(), + }; + let name = tdf::decoders::tdf_name(&tdf_id); + + match part_idx { + Some(part_idx) => output_folder.join(format!( + "{}{}_{}_{:05}.{}", + output_prefix, id_prefix, name, part_idx, extension + )), + None => output_folder.join(format!( + "{}{}_{}.{}", + output_prefix, id_prefix, name, extension + )), + } +} + +pub(crate) fn rename_first_file_if_splitting( + part_idx: usize, + output_files: &mut [PathBuf], + plain_path: PathBuf, + numbered_path: PathBuf, +) -> io::Result<()> { + if part_idx != 1 || plain_path == numbered_path || !plain_path.exists() { + return Ok(()); + } + + match fs::rename(&plain_path, &numbered_path) { + Ok(()) => {} + Err(err) if err.kind() == io::ErrorKind::AlreadyExists => { + fs::remove_file(&numbered_path)?; + fs::rename(&plain_path, &numbered_path)?; + } + Err(err) => return Err(err), + } + + if let Some(path) = output_files.first_mut() { + *path = numbered_path; + } + + Ok(()) +} + +pub(crate) fn touch_output_count(output_cnt: &mut HashMap, key: OutputKey) { + output_cnt.entry(key).or_default(); +} + +pub(crate) fn increment_output_count(output_cnt: &mut HashMap, key: OutputKey) { + *output_cnt.entry(key).or_default() += 1; +} + +pub(crate) fn written(output_cnt: &HashMap, key: OutputKey) -> usize { + output_cnt.get(&key).copied().unwrap_or_default() +} diff --git a/src/output_csv.rs b/src/output_csv.rs index 7a62692..10924ec 100644 --- a/src/output_csv.rs +++ b/src/output_csv.rs @@ -2,46 +2,53 @@ use chrono::SecondsFormat; use itertools::Itertools; use std::collections::HashMap; use std::collections::hash_map::Entry; -use std::fs::File; +use std::fs::{self, File}; use std::io::Cursor; use std::io::{self, BufRead, BufReader, BufWriter, Write}; use std::path::PathBuf; use std::sync::{Arc, Mutex}; use tdf::TdfOutput; +use crate::output_common::{ + OutputKey, increment_output_count, merged_output_path, rename_first_file_if_splitting, + touch_output_count, worker_output_path, written, +}; use crate::{ProgressReporter, RunArgs, TdfDecoderOutputs}; pub struct TdfCsvWriter { decoder_idx: usize, output_folder: std::path::PathBuf, + output_prefix: String, output_unix: bool, pub outputs: HashMap<(Option, u16), (std::path::PathBuf, std::io::BufWriter)>, - output_cnt: HashMap<(Option, u16), usize>, + output_cnt: HashMap, } impl TdfCsvWriter { pub fn new( decoder_idx: usize, output_folder: std::path::PathBuf, + output_prefix: String, output_unix_time: bool, ) -> Self { Self { decoder_idx: decoder_idx, output_folder: output_folder, + output_prefix, output_unix: output_unix_time, outputs: HashMap::new(), output_cnt: HashMap::new(), } } +} - pub fn output_path(self: &Self, remote_id: Option, tdf_id: u16) -> Option { +impl TdfOutput for TdfCsvWriter { + fn output_path(self: &Self, remote_id: Option, tdf_id: u16) -> Option { self.outputs .get(&(remote_id, tdf_id)) .map(|(pathbuf, _)| pathbuf.clone()) } -} -impl TdfOutput for TdfCsvWriter { fn write( &mut self, remote_id: Option, @@ -55,17 +62,14 @@ impl TdfOutput for TdfCsvWriter { let (_, writer) = match self.outputs.entry((remote_id, tdf_id)) { Entry::Occupied(o) => o.into_mut(), Entry::Vacant(v) => { - let id_prefix = match remote_id { - Some(id) => format!("_{:016x}", id), - None => "".to_string(), - }; - let fname = format!( - "{}_{}_{:05}.csv", - id_prefix, - tdf::decoders::tdf_name(&tdf_id), - self.decoder_idx + let path = worker_output_path( + &self.output_folder, + &self.output_prefix, + remote_id, + tdf_id, + self.decoder_idx, + "csv", ); - let path = self.output_folder.join(fname); let mut writer = std::io::BufWriter::new(std::fs::File::create(path.clone())?); // Write header into file @@ -73,10 +77,7 @@ impl TdfOutput for TdfCsvWriter { writer.write_all(format!("time,{}\n", heading).as_bytes())?; // Touch the count variable in case the decoding fails - *self - .output_cnt - .entry((remote_id, tdf_id.to_owned())) - .or_default() += 0; + touch_output_count(&mut self.output_cnt, (remote_id, tdf_id)); // Insert into hashmap and return v.insert((path, writer)) @@ -109,10 +110,7 @@ impl TdfOutput for TdfCsvWriter { writer.write_all(line.as_bytes())?; // Increment output counter - *self - .output_cnt - .entry((remote_id, tdf_id.to_owned())) - .or_default() += 1; + increment_output_count(&mut self.output_cnt, (remote_id, tdf_id)); Ok(()) } @@ -121,10 +119,164 @@ impl TdfOutput for TdfCsvWriter { } fn written(&self, remote_id: Option, tdf_id: u16) -> usize { - match self.output_cnt.get(&(remote_id, tdf_id)) { - Some(val) => *val, - None => 0, + written(&self.output_cnt, (remote_id, tdf_id)) + } +} + +struct TdfCsvMergedOutput { + output_folder: PathBuf, + output_prefix: String, + remote_id: Option, + tdf_id: u16, + max_readings_per_file: Option, + output_files: Vec, + writer: Option>, + header: Option, + readings_in_file: usize, + part_idx: usize, +} + +impl TdfCsvMergedOutput { + fn new( + output_folder: PathBuf, + output_prefix: String, + remote_id: Option, + tdf_id: u16, + max_readings_per_file: usize, + ) -> Self { + Self { + output_folder, + output_prefix, + remote_id, + tdf_id, + max_readings_per_file: match max_readings_per_file { + 0 => None, + value => Some(value), + }, + output_files: Vec::new(), + writer: None, + header: None, + readings_in_file: 0, + part_idx: 0, + } + } + + fn set_header(&mut self, header: String) -> io::Result<()> { + if self.header.is_none() { + self.header = Some(header); + self.start_next_file()?; + } + Ok(()) + } + + fn append_line(&mut self, line: &str) -> io::Result<()> { + if self.writer.is_none() { + self.start_next_file()?; + } + + if self + .max_readings_per_file + .is_some_and(|max_readings| self.readings_in_file >= max_readings) + { + self.start_next_file()?; + } + + self.writer + .as_mut() + .expect("CSV writer should be open") + .write_all(line.as_bytes())?; + self.writer + .as_mut() + .expect("CSV writer should be open") + .write_all(b"\n")?; + self.readings_in_file += 1; + Ok(()) + } + + fn finish(&mut self) -> io::Result> { + self.finish_current_file()?; + Ok(std::mem::take(&mut self.output_files)) + } + + fn start_next_file(&mut self) -> io::Result<()> { + self.finish_current_file()?; + self.rename_first_file_if_splitting()?; + + let path = self.output_path(); + let err_path = path.clone(); + let file = File::create(path.clone()).map_err(|e| { + io::Error::new( + e.kind(), + format!( + "Failed to create output file '{}': {}", + err_path.display(), + e + ), + ) + })?; + let mut writer = BufWriter::new(file); + + if let Some(header) = &self.header { + writer.write_all(header.as_bytes())?; + writer.write_all(b"\n")?; + self.readings_in_file = 0; + } else { + self.readings_in_file = 0; + } + + self.output_files.push(path); + self.writer = Some(writer); + self.part_idx += 1; + Ok(()) + } + + fn rename_first_file_if_splitting(&mut self) -> io::Result<()> { + let plain_path = self.plain_output_path(); + let numbered_path = self.numbered_output_path(0); + + rename_first_file_if_splitting( + self.part_idx, + &mut self.output_files, + plain_path, + numbered_path, + ) + } + + fn finish_current_file(&mut self) -> io::Result<()> { + if let Some(mut writer) = self.writer.take() { + writer.flush()?; } + Ok(()) + } + + fn output_path(&self) -> PathBuf { + if self.part_idx == 0 { + self.plain_output_path() + } else { + self.numbered_output_path(self.part_idx) + } + } + + fn plain_output_path(&self) -> PathBuf { + merged_output_path( + &self.output_folder, + &self.output_prefix, + self.remote_id, + self.tdf_id, + None, + "csv", + ) + } + + fn numbered_output_path(&self, part_idx: usize) -> PathBuf { + merged_output_path( + &self.output_folder, + &self.output_prefix, + self.remote_id, + self.tdf_id, + Some(part_idx), + "csv", + ) } } @@ -139,52 +291,75 @@ pub fn merge( args.merge_reporter.start("Merging output files", num_files); for ((remote_id, tdf_id), worker_outputs) in results.iter() { - let id_prefix = match remote_id { - Some(id) => format!("_{:016x}", id), - None => "".to_string(), - }; - let output_path = args.output_folder.join(format!( - "{}{}_{}.csv", - args.output_prefix, - id_prefix, - tdf::decoders::tdf_name(tdf_id) - )); - output_files.push(output_path.clone()); - let err_path = output_path.clone(); - let output_file = File::create(output_path).map_err(|e| { - io::Error::new( - e.kind(), - format!( - "Failed to create output file '{}': {}", - err_path.display(), - e - ), - ) - })?; - let mut output = BufWriter::new(output_file); - let mut write_headings = true; + let mut output = TdfCsvMergedOutput::new( + args.output_folder.clone(), + args.output_prefix.clone(), + *remote_id, + *tdf_id, + args.max_readings_per_output_file, + ); for worker in worker_outputs.keys().sorted() { let input_path = worker_outputs[worker].output.clone(); let input = BufReader::new(File::open(&input_path)?); - // Copy from input to output - for (idx, line) in input.lines().flatten().enumerate() { - if idx == 0 && !write_headings { + for (idx, line) in input.lines().enumerate() { + let line = line?; + if idx == 0 { + output.set_header(line)?; continue; } - output.write_all(line.as_bytes())?; - output.write_all(b"\n")?; + output.append_line(&line)?; } - // Remove input file - std::fs::remove_file(input_path)?; - write_headings = false; + + fs::remove_file(input_path)?; args.merge_reporter.increment(1); } - output.flush()?; + output_files.extend(output.finish()?); } args.merge_reporter.stop(); Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Read; + + fn unique_temp_dir(name: &str) -> PathBuf { + let dir = + std::env::temp_dir().join(format!("infuse_decoder_{name}_{}", std::process::id())); + let _ = fs::remove_dir_all(&dir); + fs::create_dir_all(&dir).unwrap(); + dir + } + + #[test] + fn zero_max_readings_keeps_csv_output_in_one_file() { + let output_dir = unique_temp_dir("zero_max_readings_csv"); + let mut output = TdfCsvMergedOutput::new(output_dir.clone(), "out".to_string(), None, 1, 0); + + output.set_header("time,value".to_string()).unwrap(); + output.append_line("1,10").unwrap(); + output.append_line("2,20").unwrap(); + output.append_line("3,30").unwrap(); + + let files = output.finish().unwrap(); + assert_eq!(files.len(), 1); + assert_eq!( + files[0], + output_dir.join(format!("out_{}.csv", tdf::decoders::tdf_name(&1))) + ); + + let mut contents = String::new(); + File::open(&files[0]) + .unwrap() + .read_to_string(&mut contents) + .unwrap(); + assert_eq!(contents, "time,value\n1,10\n2,20\n3,30\n"); + + fs::remove_dir_all(output_dir).unwrap(); + } +} diff --git a/src/output_parquet.rs b/src/output_parquet.rs new file mode 100644 index 0000000..75d0660 --- /dev/null +++ b/src/output_parquet.rs @@ -0,0 +1,415 @@ +use std::collections::HashMap; +use std::collections::hash_map::Entry; +use std::fs::File; +use std::io::{self, Cursor, Read}; +use std::path::PathBuf; +use std::sync::{Arc, Mutex}; + +use arrow_array::RecordBatch; +use arrow_schema::SchemaRef; +use itertools::Itertools; +use parquet::arrow::ArrowWriter; +use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder; +use tdf::TdfOutput; +use tdf::decoders_parquet::{TdfParquetBatchBuilder, TdfParquetRowMeta}; + +use crate::output_common::{ + OutputKey, increment_output_count, merged_output_path, rename_first_file_if_splitting, + touch_output_count, worker_output_path, written, +}; +use crate::{ProgressReporter, RunArgs, TdfDecoderOutputs}; + +const DEFAULT_BATCH_ROWS: usize = 65536; + +struct TdfParquetOutputFile { + path: PathBuf, + tdf_id: u16, + builder: TdfParquetBatchBuilder, + writer: ArrowWriter, + finished: bool, +} + +impl TdfParquetOutputFile { + fn new(path: PathBuf, tdf_id: u16, batch_rows: usize) -> io::Result { + let builder = tdf::decoders_parquet::tdf_parquet_builder(tdf_id, batch_rows) + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "Unknown TDF ID"))?; + let file = File::create(path.clone())?; + let writer = ArrowWriter::try_new(file, builder.schema(), None).map_err(to_io_error)?; + + Ok(Self { + path, + tdf_id, + builder, + writer, + finished: false, + }) + } + + fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> io::Result<()> { + self.builder.append(meta, size, cursor)?; + Ok(()) + } + + fn flush_batch(&mut self, batch_rows: usize) -> io::Result<()> { + if self.builder.rows() == 0 { + return Ok(()); + } + + let batch = self.builder.finish_batch().map_err(to_io_error)?; + self.writer.write(&batch).map_err(to_io_error)?; + self.builder = tdf::decoders_parquet::tdf_parquet_builder(self.tdf_id, batch_rows) + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "Unknown TDF ID"))?; + Ok(()) + } + + fn finish(&mut self, batch_rows: usize) -> io::Result<()> { + if self.finished { + return Ok(()); + } + + self.flush_batch(batch_rows)?; + self.writer.finish().map_err(to_io_error)?; + self.finished = true; + Ok(()) + } +} + +impl Drop for TdfParquetOutputFile { + fn drop(&mut self) { + let _ = self.finish(DEFAULT_BATCH_ROWS); + } +} + +pub struct TdfParquetWriter { + decoder_idx: usize, + output_folder: PathBuf, + output_prefix: String, + batch_rows: usize, + outputs: HashMap<(Option, u16), TdfParquetOutputFile>, + output_cnt: HashMap, +} + +impl TdfParquetWriter { + pub fn new(decoder_idx: usize, output_folder: PathBuf, output_prefix: String) -> Self { + Self::new_with_batch_rows( + decoder_idx, + output_folder, + output_prefix, + DEFAULT_BATCH_ROWS, + ) + } + + pub fn new_with_batch_rows( + decoder_idx: usize, + output_folder: PathBuf, + output_prefix: String, + batch_rows: usize, + ) -> Self { + Self { + decoder_idx, + output_folder, + output_prefix, + batch_rows: batch_rows.max(1), + outputs: HashMap::new(), + output_cnt: HashMap::new(), + } + } + + pub fn finish(&mut self) -> io::Result<()> { + for output in self.outputs.values_mut() { + output.finish(self.batch_rows)?; + } + Ok(()) + } + + fn create_output( + decoder_idx: usize, + output_folder: &std::path::Path, + output_prefix: &str, + batch_rows: usize, + remote_id: Option, + tdf_id: u16, + ) -> io::Result { + let path = worker_output_path( + output_folder, + output_prefix, + remote_id, + tdf_id, + decoder_idx, + "parquet", + ); + + TdfParquetOutputFile::new(path, tdf_id, batch_rows) + } +} + +impl Drop for TdfParquetWriter { + fn drop(&mut self) { + let _ = self.finish(); + } +} + +impl TdfOutput for TdfParquetWriter { + fn output_path(&self, remote_id: Option, tdf_id: u16) -> Option { + self.outputs + .get(&(remote_id, tdf_id)) + .map(|output| output.path.clone()) + } + + fn write( + &mut self, + remote_id: Option, + tdf_id: u16, + tdf_time: i64, + tdf_idx: Option, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> io::Result<()> { + if !tdf::decoders_parquet::tdf_parquet_has_schema(tdf_id) { + let mut buf = vec![0; size as usize]; + cursor.read_exact(&mut buf)?; + return Ok(()); + } + + let output = match self.outputs.entry((remote_id, tdf_id)) { + Entry::Occupied(entry) => entry.into_mut(), + Entry::Vacant(entry) => { + let output = Self::create_output( + self.decoder_idx, + &self.output_folder, + &self.output_prefix, + self.batch_rows, + remote_id, + tdf_id, + )?; + + touch_output_count(&mut self.output_cnt, (remote_id, tdf_id)); + entry.insert(output) + } + }; + + let meta = match tdf_idx { + Some(idx) => TdfParquetRowMeta { + time_unix_micros: None, + sample_idx: Some(idx), + }, + None => TdfParquetRowMeta { + time_unix_micros: Some(tdf::time::tdf_time_to_unix_micros(tdf_time)), + sample_idx: None, + }, + }; + + output.append(meta, size, cursor)?; + + if output.builder.rows() >= self.batch_rows { + output.flush_batch(self.batch_rows)?; + } + + increment_output_count(&mut self.output_cnt, (remote_id, tdf_id)); + + Ok(()) + } + + fn iter_written(&self) -> impl Iterator, u16), &usize)> { + self.output_cnt.iter() + } + + fn written(&self, remote_id: Option, tdf_id: u16) -> usize { + written(&self.output_cnt, (remote_id, tdf_id)) + } +} + +struct TdfParquetMergedOutput { + output_folder: PathBuf, + output_prefix: String, + remote_id: Option, + tdf_id: u16, + threshold_rows: Option, + output_files: Vec, + writer: Option>, + rows_in_file: usize, + part_idx: usize, +} + +impl TdfParquetMergedOutput { + fn new( + output_folder: PathBuf, + output_prefix: String, + remote_id: Option, + tdf_id: u16, + threshold_rows: usize, + ) -> Self { + Self { + output_folder, + output_prefix, + remote_id, + tdf_id, + threshold_rows: match threshold_rows { + 0 => None, + value => Some(value), + }, + output_files: Vec::new(), + writer: None, + rows_in_file: 0, + part_idx: 0, + } + } + + fn append_batch(&mut self, batch: &RecordBatch) -> io::Result<()> { + let mut offset = 0; + + while offset < batch.num_rows() { + if self.writer.is_none() + || self + .threshold_rows + .is_some_and(|threshold| self.rows_in_file >= threshold) + { + self.start_next_file(batch.schema())?; + } + + let rows_to_write = match self.threshold_rows { + Some(threshold) => { + let rows_remaining = threshold - self.rows_in_file; + rows_remaining.min(batch.num_rows() - offset) + } + None => batch.num_rows() - offset, + }; + let batch = batch.slice(offset, rows_to_write); + + self.writer + .as_mut() + .expect("Parquet writer should be open") + .write(&batch) + .map_err(to_io_error)?; + + self.rows_in_file += rows_to_write; + offset += rows_to_write; + } + + Ok(()) + } + + fn finish(&mut self) -> io::Result> { + self.finish_current_file()?; + Ok(std::mem::take(&mut self.output_files)) + } + + fn start_next_file(&mut self, schema: SchemaRef) -> io::Result<()> { + self.finish_current_file()?; + self.rename_first_file_if_splitting()?; + + let path = self.output_path(); + let file = File::create(path.clone())?; + let writer = ArrowWriter::try_new(file, schema, None).map_err(to_io_error)?; + + self.output_files.push(path); + self.writer = Some(writer); + self.rows_in_file = 0; + self.part_idx += 1; + + Ok(()) + } + + fn rename_first_file_if_splitting(&mut self) -> io::Result<()> { + let plain_path = self.plain_output_path(); + let numbered_path = self.numbered_output_path(0); + + rename_first_file_if_splitting( + self.part_idx, + &mut self.output_files, + plain_path, + numbered_path, + ) + } + + fn finish_current_file(&mut self) -> io::Result<()> { + if let Some(mut writer) = self.writer.take() { + writer.finish().map_err(to_io_error)?; + } + Ok(()) + } + + fn output_path(&self) -> PathBuf { + if self.part_idx == 0 { + self.plain_output_path() + } else { + self.numbered_output_path(self.part_idx) + } + } + + fn plain_output_path(&self) -> PathBuf { + merged_output_path( + &self.output_folder, + &self.output_prefix, + self.remote_id, + self.tdf_id, + None, + "parquet", + ) + } + + fn numbered_output_path(&self, part_idx: usize) -> PathBuf { + merged_output_path( + &self.output_folder, + &self.output_prefix, + self.remote_id, + self.tdf_id, + Some(part_idx), + "parquet", + ) + } +} + +pub fn merge_with_threshold( + args: &mut RunArgs, + output_files: &mut Vec, + stats_tdf: &Arc, u16), HashMap>>>, + threshold_rows: usize, +) -> io::Result<()> { + let results = stats_tdf.lock().unwrap(); + let num_files: usize = results.values().map(|inner| inner.len()).sum(); + + args.merge_reporter.start("Merging output files", num_files); + + for ((remote_id, tdf_id), worker_outputs) in results.iter() { + let mut output = TdfParquetMergedOutput::new( + args.output_folder.clone(), + args.output_prefix.clone(), + *remote_id, + *tdf_id, + threshold_rows, + ); + + for worker in worker_outputs.keys().sorted() { + let input_path = worker_outputs[worker].output.clone(); + let file = File::open(&input_path)?; + let reader = ParquetRecordBatchReaderBuilder::try_new(file) + .map_err(to_io_error)? + .with_batch_size(DEFAULT_BATCH_ROWS) + .build() + .map_err(to_io_error)?; + + for batch in reader { + output.append_batch(&batch.map_err(to_io_error)?)?; + } + + std::fs::remove_file(input_path)?; + args.merge_reporter.increment(1); + } + + output_files.extend(output.finish()?); + } + + args.merge_reporter.stop(); + + Ok(()) +} + +fn to_io_error(err: E) -> io::Error { + io::Error::new(io::ErrorKind::Other, err) +} diff --git a/tdf/Cargo.toml b/tdf/Cargo.toml index c8c15a6..0b9ccc8 100644 --- a/tdf/Cargo.toml +++ b/tdf/Cargo.toml @@ -4,6 +4,8 @@ version = "0.1.0" edition = "2021" [dependencies] +arrow-array = "59.0.0" +arrow-buffer = "59.0.0" arrow-schema = "59.0.0" byteorder = "1.5.0" chrono = "0.4.38" diff --git a/tdf/src/decoders.rs b/tdf/src/decoders.rs index e6cad67..7c00032 100644 --- a/tdf/src/decoders.rs +++ b/tdf/src/decoders.rs @@ -1,6 +1,7 @@ -use std::io::{Cursor, Error, ErrorKind, Read, Result}; +use std::io::{Cursor, Read, Result, Error, ErrorKind}; -pub fn tdf_name(tdf_id: &u16) -> String { +pub fn tdf_name(tdf_id: &u16) -> String +{ match tdf_id { 1 => String::from("ANNOUNCE"), 2 => String::from("BATTERY_STATE"), @@ -66,11 +67,8 @@ pub fn tdf_name(tdf_id: &u16) -> String { } } -pub fn vla_bytes_remaining( - cursor: &mut Cursor<&[u8]>, - cursor_start: u64, - size: u8, -) -> Result { +pub fn vla_bytes_remaining(cursor: &mut Cursor<&[u8]>, cursor_start: u64, size: u8) -> Result +{ let cursor_current = cursor.position(); let cursor_read = cursor_current - cursor_start; if cursor_read >= size as u64 { @@ -84,12 +82,8 @@ pub fn vla_bytes_remaining( Ok(bytes_remaining as usize) } -pub fn tdf_field_read_string( - cursor: &mut Cursor<&[u8]>, - cursor_start: u64, - num: u8, - size: u8, -) -> Result> { +pub fn tdf_field_read_string(cursor: &mut Cursor<&[u8]>, cursor_start: u64, num: u8, size: u8) -> Result> +{ let string_length = match num { 0 => vla_bytes_remaining(cursor, cursor_start, size)?, _ => num as usize, @@ -101,11 +95,8 @@ pub fn tdf_field_read_string( Ok(buf) } -pub fn tdf_field_read_vla( - cursor: &mut Cursor<&[u8]>, - cursor_start: u64, - size: u8, -) -> Result> { +pub fn tdf_field_read_vla(cursor: &mut Cursor<&[u8]>, cursor_start: u64, size: u8) -> Result> +{ let bytes_remaining = vla_bytes_remaining(cursor, cursor_start, size)?; let mut buf = vec![0u8; bytes_remaining]; diff --git a/tdf/src/decoders_parquet.rs b/tdf/src/decoders_parquet.rs index a1ee44a..ecbf66f 100644 --- a/tdf/src/decoders_parquet.rs +++ b/tdf/src/decoders_parquet.rs @@ -1,116 +1,217 @@ +use std::io::{Cursor, Error, ErrorKind, Read, Result}; use std::sync::Arc; -use arrow_schema::{DataType, Field, Fields, Schema, TimeUnit}; +use arrow_array::{ + ArrayRef, BinaryArray, FixedSizeListArray, Float32Array, Float64Array, Int16Array, Int32Array, + Int8Array, ListArray, RecordBatch, StringArray, StructArray, TimestampMicrosecondArray, + UInt16Array, UInt32Array, UInt64Array, UInt8Array, +}; +use arrow_buffer::{OffsetBuffer, ScalarBuffer}; +use arrow_schema::{ArrowError, DataType, Field, Fields, Schema, SchemaRef, TimeUnit}; +use byteorder::{BigEndian, LittleEndian, ReadBytesExt}; fn timestamp_field() -> Field { Field::new( "timestamp", DataType::Timestamp(TimeUnit::Microsecond, Some("+00:00".into())), - false, + true, ) } -pub fn tdf_parquet_schemas() -> Vec<(u16, &'static str, Schema)> { +fn sample_idx_field() -> Field { + Field::new("sample_idx", DataType::UInt16, true) +} + +fn tdf_field_read_string_to_string( + cursor: &mut Cursor<&[u8]>, + cursor_start: u64, + num: u8, + size: u8, +) -> Result { + let buf = crate::decoders::tdf_field_read_string(cursor, cursor_start, num, size)?; + + match String::from_utf8(buf) { + Ok(val) => Ok(val.trim_matches(char::from(0)).to_string()), + Err(..) => Ok(String::new()), + } +} + +fn finish_tdf_read(cursor: &mut Cursor<&[u8]>, cursor_start: u64, size: u8) -> Result<()> { + let cursor_end = cursor.position(); + let cursor_read = cursor_end - cursor_start; + + if (size as u64) < cursor_read { + return Err(Error::new( + ErrorKind::InvalidData, + "Read overflow, corrupt data/metadata", + )); + } + + let underflow = size as u64 - cursor_read; + if underflow > 0 { + let mut buf = vec![0; underflow as usize]; + cursor.read_exact(&mut buf)?; + } + + Ok(()) +} + +pub fn tdf_parquet_schemas() -> Vec<(u16, &'static str, SchemaRef)> { vec![ - (1, "ANNOUNCE", tdf_parquet_schema(&1).unwrap()), - (2, "BATTERY_STATE", tdf_parquet_schema(&2).unwrap()), - (3, "AMBIENT_TEMP_PRES_HUM", tdf_parquet_schema(&3).unwrap()), - (4, "AMBIENT_TEMPERATURE", tdf_parquet_schema(&4).unwrap()), - (5, "TIME_SYNC", tdf_parquet_schema(&5).unwrap()), - (6, "REBOOT_INFO", tdf_parquet_schema(&6).unwrap()), - (7, "ANNOUNCE_V2", tdf_parquet_schema(&7).unwrap()), - (8, "SOC_TEMPERATURE", tdf_parquet_schema(&8).unwrap()), - (10, "ACC_2G", tdf_parquet_schema(&10).unwrap()), - (11, "ACC_4G", tdf_parquet_schema(&11).unwrap()), - (12, "ACC_8G", tdf_parquet_schema(&12).unwrap()), - (13, "ACC_16G", tdf_parquet_schema(&13).unwrap()), - (14, "GYR_125DPS", tdf_parquet_schema(&14).unwrap()), - (15, "GYR_250DPS", tdf_parquet_schema(&15).unwrap()), - (16, "GYR_500DPS", tdf_parquet_schema(&16).unwrap()), - (17, "GYR_1000DPS", tdf_parquet_schema(&17).unwrap()), - (18, "GYR_2000DPS", tdf_parquet_schema(&18).unwrap()), - (19, "GCS_WGS84_LLHA", tdf_parquet_schema(&19).unwrap()), - (20, "UBX_NAV_PVT", tdf_parquet_schema(&20).unwrap()), - (21, "LTE_CONN_STATUS", tdf_parquet_schema(&21).unwrap()), - (22, "GLOBALSTAR_PKT", tdf_parquet_schema(&22).unwrap()), - ( - 23, - "ACC_MAGNITUDE_STD_DEV", - tdf_parquet_schema(&23).unwrap(), - ), - (24, "ACTIVITY_METRIC", tdf_parquet_schema(&24).unwrap()), - (25, "ALGORITHM_OUTPUT", tdf_parquet_schema(&25).unwrap()), - (26, "RUNTIME_ERROR", tdf_parquet_schema(&26).unwrap()), - (27, "CHARGER_EN_CONTROL", tdf_parquet_schema(&27).unwrap()), - (28, "GNSS_FIX_INFO", tdf_parquet_schema(&28).unwrap()), - (29, "BLUETOOTH_CONNECTION", tdf_parquet_schema(&29).unwrap()), - (30, "BLUETOOTH_RSSI", tdf_parquet_schema(&30).unwrap()), + (1, "ANNOUNCE", tdf_parquet_schema(1).unwrap()), + (2, "BATTERY_STATE", tdf_parquet_schema(2).unwrap()), + (3, "AMBIENT_TEMP_PRES_HUM", tdf_parquet_schema(3).unwrap()), + (4, "AMBIENT_TEMPERATURE", tdf_parquet_schema(4).unwrap()), + (5, "TIME_SYNC", tdf_parquet_schema(5).unwrap()), + (6, "REBOOT_INFO", tdf_parquet_schema(6).unwrap()), + (7, "ANNOUNCE_V2", tdf_parquet_schema(7).unwrap()), + (8, "SOC_TEMPERATURE", tdf_parquet_schema(8).unwrap()), + (10, "ACC_2G", tdf_parquet_schema(10).unwrap()), + (11, "ACC_4G", tdf_parquet_schema(11).unwrap()), + (12, "ACC_8G", tdf_parquet_schema(12).unwrap()), + (13, "ACC_16G", tdf_parquet_schema(13).unwrap()), + (14, "GYR_125DPS", tdf_parquet_schema(14).unwrap()), + (15, "GYR_250DPS", tdf_parquet_schema(15).unwrap()), + (16, "GYR_500DPS", tdf_parquet_schema(16).unwrap()), + (17, "GYR_1000DPS", tdf_parquet_schema(17).unwrap()), + (18, "GYR_2000DPS", tdf_parquet_schema(18).unwrap()), + (19, "GCS_WGS84_LLHA", tdf_parquet_schema(19).unwrap()), + (20, "UBX_NAV_PVT", tdf_parquet_schema(20).unwrap()), + (21, "LTE_CONN_STATUS", tdf_parquet_schema(21).unwrap()), + (22, "GLOBALSTAR_PKT", tdf_parquet_schema(22).unwrap()), + (23, "ACC_MAGNITUDE_STD_DEV", tdf_parquet_schema(23).unwrap()), + (24, "ACTIVITY_METRIC", tdf_parquet_schema(24).unwrap()), + (25, "ALGORITHM_OUTPUT", tdf_parquet_schema(25).unwrap()), + (26, "RUNTIME_ERROR", tdf_parquet_schema(26).unwrap()), + (27, "CHARGER_EN_CONTROL", tdf_parquet_schema(27).unwrap()), + (28, "GNSS_FIX_INFO", tdf_parquet_schema(28).unwrap()), + (29, "BLUETOOTH_CONNECTION", tdf_parquet_schema(29).unwrap()), + (30, "BLUETOOTH_RSSI", tdf_parquet_schema(30).unwrap()), ( 31, "BLUETOOTH_DATA_THROUGHPUT", - tdf_parquet_schema(&31).unwrap(), + tdf_parquet_schema(31).unwrap(), ), ( 32, "ALGORITHM_CLASS_HISTOGRAM", - tdf_parquet_schema(&32).unwrap(), + tdf_parquet_schema(32).unwrap(), ), ( 33, "ALGORITHM_CLASS_TIME_SERIES", - tdf_parquet_schema(&33).unwrap(), + tdf_parquet_schema(33).unwrap(), ), - (34, "LTE_TAC_CELLS", tdf_parquet_schema(&34).unwrap()), - (35, "WIFI_AP_INFO", tdf_parquet_schema(&35).unwrap()), - (36, "DEVICE_TILT", tdf_parquet_schema(&36).unwrap()), - (37, "NRF9X_GNSS_PVT", tdf_parquet_schema(&37).unwrap()), + (34, "LTE_TAC_CELLS", tdf_parquet_schema(34).unwrap()), + (35, "WIFI_AP_INFO", tdf_parquet_schema(35).unwrap()), + (36, "DEVICE_TILT", tdf_parquet_schema(36).unwrap()), + (37, "NRF9X_GNSS_PVT", tdf_parquet_schema(37).unwrap()), ( 38, "BATTERY_CHARGE_ACCUMULATED", - tdf_parquet_schema(&38).unwrap(), - ), - ( - 39, - "INFUSE_BLUETOOTH_RSSI", - tdf_parquet_schema(&39).unwrap(), + tdf_parquet_schema(38).unwrap(), ), - (40, "ADC_RAW_8", tdf_parquet_schema(&40).unwrap()), - (41, "ADC_RAW_16", tdf_parquet_schema(&41).unwrap()), - (42, "ADC_RAW_32", tdf_parquet_schema(&42).unwrap()), - (43, "ANNOTATION", tdf_parquet_schema(&43).unwrap()), - (44, "LORA_RX", tdf_parquet_schema(&44).unwrap()), - (45, "LORA_TX", tdf_parquet_schema(&45).unwrap()), - (46, "IDX_ARRAY_FREQ", tdf_parquet_schema(&46).unwrap()), - (47, "IDX_ARRAY_PERIOD", tdf_parquet_schema(&47).unwrap()), - (48, "WIFI_CONNECTED", tdf_parquet_schema(&48).unwrap()), + (39, "INFUSE_BLUETOOTH_RSSI", tdf_parquet_schema(39).unwrap()), + (40, "ADC_RAW_8", tdf_parquet_schema(40).unwrap()), + (41, "ADC_RAW_16", tdf_parquet_schema(41).unwrap()), + (42, "ADC_RAW_32", tdf_parquet_schema(42).unwrap()), + (43, "ANNOTATION", tdf_parquet_schema(43).unwrap()), + (44, "LORA_RX", tdf_parquet_schema(44).unwrap()), + (45, "LORA_TX", tdf_parquet_schema(45).unwrap()), + (46, "IDX_ARRAY_FREQ", tdf_parquet_schema(46).unwrap()), + (47, "IDX_ARRAY_PERIOD", tdf_parquet_schema(47).unwrap()), + (48, "WIFI_CONNECTED", tdf_parquet_schema(48).unwrap()), ( 49, "WIFI_CONNECTION_FAILED", - tdf_parquet_schema(&49).unwrap(), - ), - (50, "WIFI_DISCONNECTED", tdf_parquet_schema(&50).unwrap()), - (51, "NETWORK_SCAN_COUNT", tdf_parquet_schema(&51).unwrap()), - ( - 52, - "EXCEPTION_STACK_FRAME", - tdf_parquet_schema(&52).unwrap(), + tdf_parquet_schema(49).unwrap(), ), - (53, "BATTERY_VOLTAGE", tdf_parquet_schema(&53).unwrap()), - (54, "BATTERY_SOC", tdf_parquet_schema(&54).unwrap()), - (55, "STATE_EVENT_SET", tdf_parquet_schema(&55).unwrap()), - (56, "STATE_EVENT_CLEARED", tdf_parquet_schema(&56).unwrap()), - (57, "STATE_DURATION", tdf_parquet_schema(&57).unwrap()), - (58, "PCM_16BIT_CHAN_LEFT", tdf_parquet_schema(&58).unwrap()), - (59, "PCM_16BIT_CHAN_RIGHT", tdf_parquet_schema(&59).unwrap()), - (60, "PCM_16BIT_CHAN_DUAL", tdf_parquet_schema(&60).unwrap()), - (61, "KVS_VALUE_CHANGED", tdf_parquet_schema(&61).unwrap()), + (50, "WIFI_DISCONNECTED", tdf_parquet_schema(50).unwrap()), + (51, "NETWORK_SCAN_COUNT", tdf_parquet_schema(51).unwrap()), + (52, "EXCEPTION_STACK_FRAME", tdf_parquet_schema(52).unwrap()), + (53, "BATTERY_VOLTAGE", tdf_parquet_schema(53).unwrap()), + (54, "BATTERY_SOC", tdf_parquet_schema(54).unwrap()), + (55, "STATE_EVENT_SET", tdf_parquet_schema(55).unwrap()), + (56, "STATE_EVENT_CLEARED", tdf_parquet_schema(56).unwrap()), + (57, "STATE_DURATION", tdf_parquet_schema(57).unwrap()), + (58, "PCM_16BIT_CHAN_LEFT", tdf_parquet_schema(58).unwrap()), + (59, "PCM_16BIT_CHAN_RIGHT", tdf_parquet_schema(59).unwrap()), + (60, "PCM_16BIT_CHAN_DUAL", tdf_parquet_schema(60).unwrap()), + (61, "KVS_VALUE_CHANGED", tdf_parquet_schema(61).unwrap()), ] } -pub fn tdf_parquet_schema(tdf_id: &u16) -> Option { +pub fn tdf_parquet_has_schema(tdf_id: u16) -> bool { + match tdf_id { + 1 => true, + 2 => true, + 3 => true, + 4 => true, + 5 => true, + 6 => true, + 7 => true, + 8 => true, + 10 => true, + 11 => true, + 12 => true, + 13 => true, + 14 => true, + 15 => true, + 16 => true, + 17 => true, + 18 => true, + 19 => true, + 20 => true, + 21 => true, + 22 => true, + 23 => true, + 24 => true, + 25 => true, + 26 => true, + 27 => true, + 28 => true, + 29 => true, + 30 => true, + 31 => true, + 32 => true, + 33 => true, + 34 => true, + 35 => true, + 36 => true, + 37 => true, + 38 => true, + 39 => true, + 40 => true, + 41 => true, + 42 => true, + 43 => true, + 44 => true, + 45 => true, + 46 => true, + 47 => true, + 48 => true, + 49 => true, + 50 => true, + 51 => true, + 52 => true, + 53 => true, + 54 => true, + 55 => true, + 56 => true, + 57 => true, + 58 => true, + 59 => true, + 60 => true, + 61 => true, + _ => false, + } +} + +pub fn tdf_parquet_schema(tdf_id: u16) -> Option { match tdf_id { - 1 => Some(Schema::new(vec![ + 1 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new("application", DataType::UInt32, false), Field::new( "version", @@ -127,30 +228,35 @@ pub fn tdf_parquet_schema(tdf_id: &u16) -> Option { Field::new("uptime", DataType::UInt32, false), Field::new("reboots", DataType::UInt16, false), Field::new("flags", DataType::UInt8, false), - ])), - 2 => Some(Schema::new(vec![ + ]))), + 2 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new("voltage_mv", DataType::UInt32, false), Field::new("current_ua", DataType::Int32, false), Field::new("soc", DataType::UInt8, false), - ])), - 3 => Some(Schema::new(vec![ + ]))), + 3 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new("temperature", DataType::Float64, false), Field::new("pressure", DataType::Float64, false), Field::new("humidity", DataType::Float64, false), - ])), - 4 => Some(Schema::new(vec![ + ]))), + 4 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new("temperature", DataType::Float64, false), - ])), - 5 => Some(Schema::new(vec![ + ]))), + 5 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new("source", DataType::UInt8, false), Field::new("shift", DataType::Float64, false), - ])), - 6 => Some(Schema::new(vec![ + ]))), + 6 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new("reason", DataType::UInt8, false), Field::new("hardware_flags", DataType::UInt32, false), Field::new("count", DataType::UInt32, false), @@ -158,9 +264,10 @@ pub fn tdf_parquet_schema(tdf_id: &u16) -> Option { Field::new("param_1", DataType::UInt32, false), Field::new("param_2", DataType::UInt32, false), Field::new("thread", DataType::Utf8, false), - ])), - 7 => Some(Schema::new(vec![ + ]))), + 7 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new("application", DataType::UInt32, false), Field::new( "version", @@ -178,13 +285,15 @@ pub fn tdf_parquet_schema(tdf_id: &u16) -> Option { Field::new("uptime", DataType::UInt32, false), Field::new("reboots", DataType::UInt16, false), Field::new("flags", DataType::UInt8, false), - ])), - 8 => Some(Schema::new(vec![ + ]))), + 8 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new("temperature", DataType::Float64, false), - ])), - 10 => Some(Schema::new(vec![ + ]))), + 10 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new( "sample", DataType::Struct(Fields::from(vec![ @@ -194,9 +303,10 @@ pub fn tdf_parquet_schema(tdf_id: &u16) -> Option { ])), false, ), - ])), - 11 => Some(Schema::new(vec![ + ]))), + 11 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new( "sample", DataType::Struct(Fields::from(vec![ @@ -206,9 +316,10 @@ pub fn tdf_parquet_schema(tdf_id: &u16) -> Option { ])), false, ), - ])), - 12 => Some(Schema::new(vec![ + ]))), + 12 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new( "sample", DataType::Struct(Fields::from(vec![ @@ -218,9 +329,10 @@ pub fn tdf_parquet_schema(tdf_id: &u16) -> Option { ])), false, ), - ])), - 13 => Some(Schema::new(vec![ + ]))), + 13 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new( "sample", DataType::Struct(Fields::from(vec![ @@ -230,9 +342,10 @@ pub fn tdf_parquet_schema(tdf_id: &u16) -> Option { ])), false, ), - ])), - 14 => Some(Schema::new(vec![ + ]))), + 14 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new( "sample", DataType::Struct(Fields::from(vec![ @@ -242,9 +355,10 @@ pub fn tdf_parquet_schema(tdf_id: &u16) -> Option { ])), false, ), - ])), - 15 => Some(Schema::new(vec![ + ]))), + 15 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new( "sample", DataType::Struct(Fields::from(vec![ @@ -254,9 +368,10 @@ pub fn tdf_parquet_schema(tdf_id: &u16) -> Option { ])), false, ), - ])), - 16 => Some(Schema::new(vec![ + ]))), + 16 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new( "sample", DataType::Struct(Fields::from(vec![ @@ -266,9 +381,10 @@ pub fn tdf_parquet_schema(tdf_id: &u16) -> Option { ])), false, ), - ])), - 17 => Some(Schema::new(vec![ + ]))), + 17 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new( "sample", DataType::Struct(Fields::from(vec![ @@ -278,9 +394,10 @@ pub fn tdf_parquet_schema(tdf_id: &u16) -> Option { ])), false, ), - ])), - 18 => Some(Schema::new(vec![ + ]))), + 18 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new( "sample", DataType::Struct(Fields::from(vec![ @@ -290,9 +407,10 @@ pub fn tdf_parquet_schema(tdf_id: &u16) -> Option { ])), false, ), - ])), - 19 => Some(Schema::new(vec![ + ]))), + 19 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new( "location", DataType::Struct(Fields::from(vec![ @@ -304,9 +422,10 @@ pub fn tdf_parquet_schema(tdf_id: &u16) -> Option { ), Field::new("h_acc", DataType::Float64, false), Field::new("v_acc", DataType::Float64, false), - ])), - 20 => Some(Schema::new(vec![ + ]))), + 20 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new("itow", DataType::UInt32, false), Field::new("year", DataType::UInt16, false), Field::new("month", DataType::UInt8, false), @@ -344,9 +463,10 @@ pub fn tdf_parquet_schema(tdf_id: &u16) -> Option { Field::new("head_veh", DataType::Float64, false), Field::new("mag_dec", DataType::Float64, false), Field::new("mag_acc", DataType::Float64, false), - ])), - 21 => Some(Schema::new(vec![ + ]))), + 21 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new( "cell", DataType::Struct(Fields::from(vec![ @@ -362,47 +482,55 @@ pub fn tdf_parquet_schema(tdf_id: &u16) -> Option { Field::new("tech", DataType::UInt8, false), Field::new("rsrp", DataType::Float64, false), Field::new("rsrq", DataType::Int8, false), - ])), - 22 => Some(Schema::new(vec![ + ]))), + 22 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new( "payload", DataType::FixedSizeList(Arc::new(Field::new_list_field(DataType::UInt8, false)), 9), false, ), - ])), - 23 => Some(Schema::new(vec![ + ]))), + 23 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new("count", DataType::UInt32, false), Field::new("std_dev", DataType::UInt32, false), - ])), - 24 => Some(Schema::new(vec![ + ]))), + 24 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new("value", DataType::UInt32, false), - ])), - 25 => Some(Schema::new(vec![ + ]))), + 25 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new("algorithm_id", DataType::UInt32, false), Field::new("algorithm_version", DataType::UInt16, false), Field::new("output", DataType::Binary, false), - ])), - 26 => Some(Schema::new(vec![ + ]))), + 26 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new("error_id", DataType::UInt32, false), Field::new("error_ctx", DataType::UInt32, false), - ])), - 27 => Some(Schema::new(vec![ + ]))), + 27 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new("enabled", DataType::UInt8, false), - ])), - 28 => Some(Schema::new(vec![ + ]))), + 28 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new("time_fix", DataType::UInt16, false), Field::new("location_fix", DataType::UInt16, false), Field::new("num_sv", DataType::UInt8, false), - ])), - 29 => Some(Schema::new(vec![ + ]))), + 29 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new( "address", DataType::Struct(Fields::from(vec![ @@ -412,9 +540,10 @@ pub fn tdf_parquet_schema(tdf_id: &u16) -> Option { false, ), Field::new("connected", DataType::UInt8, false), - ])), - 30 => Some(Schema::new(vec![ + ]))), + 30 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new( "address", DataType::Struct(Fields::from(vec![ @@ -424,9 +553,10 @@ pub fn tdf_parquet_schema(tdf_id: &u16) -> Option { false, ), Field::new("rssi", DataType::Int8, false), - ])), - 31 => Some(Schema::new(vec![ + ]))), + 31 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new( "address", DataType::Struct(Fields::from(vec![ @@ -436,21 +566,24 @@ pub fn tdf_parquet_schema(tdf_id: &u16) -> Option { false, ), Field::new("throughput", DataType::Int32, false), - ])), - 32 => Some(Schema::new(vec![ + ]))), + 32 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new("algorithm_id", DataType::UInt32, false), Field::new("algorithm_version", DataType::UInt16, false), Field::new("classes", DataType::Binary, false), - ])), - 33 => Some(Schema::new(vec![ + ]))), + 33 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new("algorithm_id", DataType::UInt32, false), Field::new("algorithm_version", DataType::UInt16, false), Field::new("values", DataType::Binary, false), - ])), - 34 => Some(Schema::new(vec![ + ]))), + 34 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new( "cell", DataType::Struct(Fields::from(vec![ @@ -478,9 +611,10 @@ pub fn tdf_parquet_schema(tdf_id: &u16) -> Option { ))), false, ), - ])), - 35 => Some(Schema::new(vec![ + ]))), + 35 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new( "bssid", DataType::Struct(Fields::from(vec![Field::new( @@ -492,13 +626,15 @@ pub fn tdf_parquet_schema(tdf_id: &u16) -> Option { ), Field::new("channel", DataType::UInt8, false), Field::new("rsrp", DataType::Int8, false), - ])), - 36 => Some(Schema::new(vec![ + ]))), + 36 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new("cosine", DataType::Float32, false), - ])), - 37 => Some(Schema::new(vec![ + ]))), + 37 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new("lat", DataType::Float64, false), Field::new("lon", DataType::Float64, false), Field::new("height", DataType::Float64, false), @@ -523,55 +659,66 @@ pub fn tdf_parquet_schema(tdf_id: &u16) -> Option { Field::new("t_dop", DataType::Float64, false), Field::new("flags", DataType::UInt8, false), Field::new("num_sv", DataType::UInt8, false), - ])), - 38 => Some(Schema::new(vec![ + ]))), + 38 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new("charge", DataType::Int32, false), - ])), - 39 => Some(Schema::new(vec![ + ]))), + 39 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new("infuse_id", DataType::UInt64, false), Field::new("rssi", DataType::Int8, false), - ])), - 40 => Some(Schema::new(vec![ + ]))), + 40 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new("val", DataType::Int8, false), - ])), - 41 => Some(Schema::new(vec![ + ]))), + 41 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new("val", DataType::Int16, false), - ])), - 42 => Some(Schema::new(vec![ + ]))), + 42 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new("val", DataType::Int32, false), - ])), - 43 => Some(Schema::new(vec![ + ]))), + 43 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new("timestamp", DataType::UInt32, false), Field::new("event", DataType::Utf8, false), - ])), - 44 => Some(Schema::new(vec![ + ]))), + 44 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new("snr", DataType::Int8, false), Field::new("rssi", DataType::Int16, false), Field::new("payload", DataType::Binary, false), - ])), - 45 => Some(Schema::new(vec![ + ]))), + 45 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new("payload", DataType::Binary, false), - ])), - 46 => Some(Schema::new(vec![ + ]))), + 46 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new("tdf_id", DataType::UInt16, false), Field::new("frequency", DataType::UInt32, false), - ])), - 47 => Some(Schema::new(vec![ + ]))), + 47 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new("tdf_id", DataType::UInt16, false), Field::new("period", DataType::UInt32, false), - ])), - 48 => Some(Schema::new(vec![ + ]))), + 48 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new( "network", DataType::Struct(Fields::from(vec![ @@ -587,67 +734,4860 @@ pub fn tdf_parquet_schema(tdf_id: &u16) -> Option { ])), false, ), - ])), - 49 => Some(Schema::new(vec![ + ]))), + 49 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new("reason", DataType::UInt8, false), - ])), - 50 => Some(Schema::new(vec![ + ]))), + 50 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new("reason", DataType::UInt8, false), - ])), - 51 => Some(Schema::new(vec![ + ]))), + 51 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new("num_wifi", DataType::UInt8, false), Field::new("num_lte", DataType::UInt8, false), - ])), - 52 => Some(Schema::new(vec![ + ]))), + 52 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new( "frame", DataType::List(Arc::new(Field::new_list_field(DataType::UInt32, false))), false, ), - ])), - 53 => Some(Schema::new(vec![ + ]))), + 53 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new("voltage", DataType::UInt16, false), - ])), - 54 => Some(Schema::new(vec![ + ]))), + 54 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new("soc", DataType::UInt8, false), - ])), - 55 => Some(Schema::new(vec![ + ]))), + 55 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new("state", DataType::UInt8, false), - ])), - 56 => Some(Schema::new(vec![ + ]))), + 56 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new("state", DataType::UInt8, false), - ])), - 57 => Some(Schema::new(vec![ + ]))), + 57 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new("state", DataType::UInt8, false), Field::new("duration", DataType::UInt32, false), - ])), - 58 => Some(Schema::new(vec![ + ]))), + 58 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new("val", DataType::Int16, false), - ])), - 59 => Some(Schema::new(vec![ + ]))), + 59 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new("val", DataType::Int16, false), - ])), - 60 => Some(Schema::new(vec![ + ]))), + 60 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new("left", DataType::Int16, false), Field::new("right", DataType::Int16, false), - ])), - 61 => Some(Schema::new(vec![ + ]))), + 61 => Some(Arc::new(Schema::new(vec![ timestamp_field(), + sample_idx_field(), Field::new("key", DataType::UInt16, false), Field::new("value", DataType::Binary, false), - ])), + ]))), + _ => None, + } +} + +pub fn tdf_parquet_builder(tdf_id: u16, capacity: usize) -> Option { + match tdf_id { + 1 => Some(TdfParquetBatchBuilder::Tdf1Announce( + Tdf1AnnounceBuilder::new(capacity), + )), + 2 => Some(TdfParquetBatchBuilder::Tdf2BatteryState( + Tdf2BatteryStateBuilder::new(capacity), + )), + 3 => Some(TdfParquetBatchBuilder::Tdf3AmbientTempPresHum( + Tdf3AmbientTempPresHumBuilder::new(capacity), + )), + 4 => Some(TdfParquetBatchBuilder::Tdf4AmbientTemperature( + Tdf4AmbientTemperatureBuilder::new(capacity), + )), + 5 => Some(TdfParquetBatchBuilder::Tdf5TimeSync( + Tdf5TimeSyncBuilder::new(capacity), + )), + 6 => Some(TdfParquetBatchBuilder::Tdf6RebootInfo( + Tdf6RebootInfoBuilder::new(capacity), + )), + 7 => Some(TdfParquetBatchBuilder::Tdf7AnnounceV2( + Tdf7AnnounceV2Builder::new(capacity), + )), + 8 => Some(TdfParquetBatchBuilder::Tdf8SocTemperature( + Tdf8SocTemperatureBuilder::new(capacity), + )), + 10 => Some(TdfParquetBatchBuilder::Tdf10Acc2g(Tdf10Acc2gBuilder::new( + capacity, + ))), + 11 => Some(TdfParquetBatchBuilder::Tdf11Acc4g(Tdf11Acc4gBuilder::new( + capacity, + ))), + 12 => Some(TdfParquetBatchBuilder::Tdf12Acc8g(Tdf12Acc8gBuilder::new( + capacity, + ))), + 13 => Some(TdfParquetBatchBuilder::Tdf13Acc16g( + Tdf13Acc16gBuilder::new(capacity), + )), + 14 => Some(TdfParquetBatchBuilder::Tdf14Gyr125dps( + Tdf14Gyr125dpsBuilder::new(capacity), + )), + 15 => Some(TdfParquetBatchBuilder::Tdf15Gyr250dps( + Tdf15Gyr250dpsBuilder::new(capacity), + )), + 16 => Some(TdfParquetBatchBuilder::Tdf16Gyr500dps( + Tdf16Gyr500dpsBuilder::new(capacity), + )), + 17 => Some(TdfParquetBatchBuilder::Tdf17Gyr1000dps( + Tdf17Gyr1000dpsBuilder::new(capacity), + )), + 18 => Some(TdfParquetBatchBuilder::Tdf18Gyr2000dps( + Tdf18Gyr2000dpsBuilder::new(capacity), + )), + 19 => Some(TdfParquetBatchBuilder::Tdf19GcsWgs84Llha( + Tdf19GcsWgs84LlhaBuilder::new(capacity), + )), + 20 => Some(TdfParquetBatchBuilder::Tdf20UbxNavPvt( + Tdf20UbxNavPvtBuilder::new(capacity), + )), + 21 => Some(TdfParquetBatchBuilder::Tdf21LteConnStatus( + Tdf21LteConnStatusBuilder::new(capacity), + )), + 22 => Some(TdfParquetBatchBuilder::Tdf22GlobalstarPkt( + Tdf22GlobalstarPktBuilder::new(capacity), + )), + 23 => Some(TdfParquetBatchBuilder::Tdf23AccMagnitudeStdDev( + Tdf23AccMagnitudeStdDevBuilder::new(capacity), + )), + 24 => Some(TdfParquetBatchBuilder::Tdf24ActivityMetric( + Tdf24ActivityMetricBuilder::new(capacity), + )), + 25 => Some(TdfParquetBatchBuilder::Tdf25AlgorithmOutput( + Tdf25AlgorithmOutputBuilder::new(capacity), + )), + 26 => Some(TdfParquetBatchBuilder::Tdf26RuntimeError( + Tdf26RuntimeErrorBuilder::new(capacity), + )), + 27 => Some(TdfParquetBatchBuilder::Tdf27ChargerEnControl( + Tdf27ChargerEnControlBuilder::new(capacity), + )), + 28 => Some(TdfParquetBatchBuilder::Tdf28GnssFixInfo( + Tdf28GnssFixInfoBuilder::new(capacity), + )), + 29 => Some(TdfParquetBatchBuilder::Tdf29BluetoothConnection( + Tdf29BluetoothConnectionBuilder::new(capacity), + )), + 30 => Some(TdfParquetBatchBuilder::Tdf30BluetoothRssi( + Tdf30BluetoothRssiBuilder::new(capacity), + )), + 31 => Some(TdfParquetBatchBuilder::Tdf31BluetoothDataThroughput( + Tdf31BluetoothDataThroughputBuilder::new(capacity), + )), + 32 => Some(TdfParquetBatchBuilder::Tdf32AlgorithmClassHistogram( + Tdf32AlgorithmClassHistogramBuilder::new(capacity), + )), + 33 => Some(TdfParquetBatchBuilder::Tdf33AlgorithmClassTimeSeries( + Tdf33AlgorithmClassTimeSeriesBuilder::new(capacity), + )), + 34 => Some(TdfParquetBatchBuilder::Tdf34LteTacCells( + Tdf34LteTacCellsBuilder::new(capacity), + )), + 35 => Some(TdfParquetBatchBuilder::Tdf35WifiApInfo( + Tdf35WifiApInfoBuilder::new(capacity), + )), + 36 => Some(TdfParquetBatchBuilder::Tdf36DeviceTilt( + Tdf36DeviceTiltBuilder::new(capacity), + )), + 37 => Some(TdfParquetBatchBuilder::Tdf37Nrf9xGnssPvt( + Tdf37Nrf9xGnssPvtBuilder::new(capacity), + )), + 38 => Some(TdfParquetBatchBuilder::Tdf38BatteryChargeAccumulated( + Tdf38BatteryChargeAccumulatedBuilder::new(capacity), + )), + 39 => Some(TdfParquetBatchBuilder::Tdf39InfuseBluetoothRssi( + Tdf39InfuseBluetoothRssiBuilder::new(capacity), + )), + 40 => Some(TdfParquetBatchBuilder::Tdf40AdcRaw8( + Tdf40AdcRaw8Builder::new(capacity), + )), + 41 => Some(TdfParquetBatchBuilder::Tdf41AdcRaw16( + Tdf41AdcRaw16Builder::new(capacity), + )), + 42 => Some(TdfParquetBatchBuilder::Tdf42AdcRaw32( + Tdf42AdcRaw32Builder::new(capacity), + )), + 43 => Some(TdfParquetBatchBuilder::Tdf43Annotation( + Tdf43AnnotationBuilder::new(capacity), + )), + 44 => Some(TdfParquetBatchBuilder::Tdf44LoraRx( + Tdf44LoraRxBuilder::new(capacity), + )), + 45 => Some(TdfParquetBatchBuilder::Tdf45LoraTx( + Tdf45LoraTxBuilder::new(capacity), + )), + 46 => Some(TdfParquetBatchBuilder::Tdf46IdxArrayFreq( + Tdf46IdxArrayFreqBuilder::new(capacity), + )), + 47 => Some(TdfParquetBatchBuilder::Tdf47IdxArrayPeriod( + Tdf47IdxArrayPeriodBuilder::new(capacity), + )), + 48 => Some(TdfParquetBatchBuilder::Tdf48WifiConnected( + Tdf48WifiConnectedBuilder::new(capacity), + )), + 49 => Some(TdfParquetBatchBuilder::Tdf49WifiConnectionFailed( + Tdf49WifiConnectionFailedBuilder::new(capacity), + )), + 50 => Some(TdfParquetBatchBuilder::Tdf50WifiDisconnected( + Tdf50WifiDisconnectedBuilder::new(capacity), + )), + 51 => Some(TdfParquetBatchBuilder::Tdf51NetworkScanCount( + Tdf51NetworkScanCountBuilder::new(capacity), + )), + 52 => Some(TdfParquetBatchBuilder::Tdf52ExceptionStackFrame( + Tdf52ExceptionStackFrameBuilder::new(capacity), + )), + 53 => Some(TdfParquetBatchBuilder::Tdf53BatteryVoltage( + Tdf53BatteryVoltageBuilder::new(capacity), + )), + 54 => Some(TdfParquetBatchBuilder::Tdf54BatterySoc( + Tdf54BatterySocBuilder::new(capacity), + )), + 55 => Some(TdfParquetBatchBuilder::Tdf55StateEventSet( + Tdf55StateEventSetBuilder::new(capacity), + )), + 56 => Some(TdfParquetBatchBuilder::Tdf56StateEventCleared( + Tdf56StateEventClearedBuilder::new(capacity), + )), + 57 => Some(TdfParquetBatchBuilder::Tdf57StateDuration( + Tdf57StateDurationBuilder::new(capacity), + )), + 58 => Some(TdfParquetBatchBuilder::Tdf58Pcm16bitChanLeft( + Tdf58Pcm16bitChanLeftBuilder::new(capacity), + )), + 59 => Some(TdfParquetBatchBuilder::Tdf59Pcm16bitChanRight( + Tdf59Pcm16bitChanRightBuilder::new(capacity), + )), + 60 => Some(TdfParquetBatchBuilder::Tdf60Pcm16bitChanDual( + Tdf60Pcm16bitChanDualBuilder::new(capacity), + )), + 61 => Some(TdfParquetBatchBuilder::Tdf61KvsValueChanged( + Tdf61KvsValueChangedBuilder::new(capacity), + )), _ => None, } } + +#[derive(Clone, Copy, Debug)] +pub struct TdfParquetRowMeta { + pub time_unix_micros: Option, + pub sample_idx: Option, +} + +pub enum TdfParquetBatchBuilder { + Tdf1Announce(Tdf1AnnounceBuilder), + Tdf2BatteryState(Tdf2BatteryStateBuilder), + Tdf3AmbientTempPresHum(Tdf3AmbientTempPresHumBuilder), + Tdf4AmbientTemperature(Tdf4AmbientTemperatureBuilder), + Tdf5TimeSync(Tdf5TimeSyncBuilder), + Tdf6RebootInfo(Tdf6RebootInfoBuilder), + Tdf7AnnounceV2(Tdf7AnnounceV2Builder), + Tdf8SocTemperature(Tdf8SocTemperatureBuilder), + Tdf10Acc2g(Tdf10Acc2gBuilder), + Tdf11Acc4g(Tdf11Acc4gBuilder), + Tdf12Acc8g(Tdf12Acc8gBuilder), + Tdf13Acc16g(Tdf13Acc16gBuilder), + Tdf14Gyr125dps(Tdf14Gyr125dpsBuilder), + Tdf15Gyr250dps(Tdf15Gyr250dpsBuilder), + Tdf16Gyr500dps(Tdf16Gyr500dpsBuilder), + Tdf17Gyr1000dps(Tdf17Gyr1000dpsBuilder), + Tdf18Gyr2000dps(Tdf18Gyr2000dpsBuilder), + Tdf19GcsWgs84Llha(Tdf19GcsWgs84LlhaBuilder), + Tdf20UbxNavPvt(Tdf20UbxNavPvtBuilder), + Tdf21LteConnStatus(Tdf21LteConnStatusBuilder), + Tdf22GlobalstarPkt(Tdf22GlobalstarPktBuilder), + Tdf23AccMagnitudeStdDev(Tdf23AccMagnitudeStdDevBuilder), + Tdf24ActivityMetric(Tdf24ActivityMetricBuilder), + Tdf25AlgorithmOutput(Tdf25AlgorithmOutputBuilder), + Tdf26RuntimeError(Tdf26RuntimeErrorBuilder), + Tdf27ChargerEnControl(Tdf27ChargerEnControlBuilder), + Tdf28GnssFixInfo(Tdf28GnssFixInfoBuilder), + Tdf29BluetoothConnection(Tdf29BluetoothConnectionBuilder), + Tdf30BluetoothRssi(Tdf30BluetoothRssiBuilder), + Tdf31BluetoothDataThroughput(Tdf31BluetoothDataThroughputBuilder), + Tdf32AlgorithmClassHistogram(Tdf32AlgorithmClassHistogramBuilder), + Tdf33AlgorithmClassTimeSeries(Tdf33AlgorithmClassTimeSeriesBuilder), + Tdf34LteTacCells(Tdf34LteTacCellsBuilder), + Tdf35WifiApInfo(Tdf35WifiApInfoBuilder), + Tdf36DeviceTilt(Tdf36DeviceTiltBuilder), + Tdf37Nrf9xGnssPvt(Tdf37Nrf9xGnssPvtBuilder), + Tdf38BatteryChargeAccumulated(Tdf38BatteryChargeAccumulatedBuilder), + Tdf39InfuseBluetoothRssi(Tdf39InfuseBluetoothRssiBuilder), + Tdf40AdcRaw8(Tdf40AdcRaw8Builder), + Tdf41AdcRaw16(Tdf41AdcRaw16Builder), + Tdf42AdcRaw32(Tdf42AdcRaw32Builder), + Tdf43Annotation(Tdf43AnnotationBuilder), + Tdf44LoraRx(Tdf44LoraRxBuilder), + Tdf45LoraTx(Tdf45LoraTxBuilder), + Tdf46IdxArrayFreq(Tdf46IdxArrayFreqBuilder), + Tdf47IdxArrayPeriod(Tdf47IdxArrayPeriodBuilder), + Tdf48WifiConnected(Tdf48WifiConnectedBuilder), + Tdf49WifiConnectionFailed(Tdf49WifiConnectionFailedBuilder), + Tdf50WifiDisconnected(Tdf50WifiDisconnectedBuilder), + Tdf51NetworkScanCount(Tdf51NetworkScanCountBuilder), + Tdf52ExceptionStackFrame(Tdf52ExceptionStackFrameBuilder), + Tdf53BatteryVoltage(Tdf53BatteryVoltageBuilder), + Tdf54BatterySoc(Tdf54BatterySocBuilder), + Tdf55StateEventSet(Tdf55StateEventSetBuilder), + Tdf56StateEventCleared(Tdf56StateEventClearedBuilder), + Tdf57StateDuration(Tdf57StateDurationBuilder), + Tdf58Pcm16bitChanLeft(Tdf58Pcm16bitChanLeftBuilder), + Tdf59Pcm16bitChanRight(Tdf59Pcm16bitChanRightBuilder), + Tdf60Pcm16bitChanDual(Tdf60Pcm16bitChanDualBuilder), + Tdf61KvsValueChanged(Tdf61KvsValueChangedBuilder), +} + +impl TdfParquetBatchBuilder { + pub fn schema(&self) -> SchemaRef { + match self { + Self::Tdf1Announce(builder) => builder.schema(), + Self::Tdf2BatteryState(builder) => builder.schema(), + Self::Tdf3AmbientTempPresHum(builder) => builder.schema(), + Self::Tdf4AmbientTemperature(builder) => builder.schema(), + Self::Tdf5TimeSync(builder) => builder.schema(), + Self::Tdf6RebootInfo(builder) => builder.schema(), + Self::Tdf7AnnounceV2(builder) => builder.schema(), + Self::Tdf8SocTemperature(builder) => builder.schema(), + Self::Tdf10Acc2g(builder) => builder.schema(), + Self::Tdf11Acc4g(builder) => builder.schema(), + Self::Tdf12Acc8g(builder) => builder.schema(), + Self::Tdf13Acc16g(builder) => builder.schema(), + Self::Tdf14Gyr125dps(builder) => builder.schema(), + Self::Tdf15Gyr250dps(builder) => builder.schema(), + Self::Tdf16Gyr500dps(builder) => builder.schema(), + Self::Tdf17Gyr1000dps(builder) => builder.schema(), + Self::Tdf18Gyr2000dps(builder) => builder.schema(), + Self::Tdf19GcsWgs84Llha(builder) => builder.schema(), + Self::Tdf20UbxNavPvt(builder) => builder.schema(), + Self::Tdf21LteConnStatus(builder) => builder.schema(), + Self::Tdf22GlobalstarPkt(builder) => builder.schema(), + Self::Tdf23AccMagnitudeStdDev(builder) => builder.schema(), + Self::Tdf24ActivityMetric(builder) => builder.schema(), + Self::Tdf25AlgorithmOutput(builder) => builder.schema(), + Self::Tdf26RuntimeError(builder) => builder.schema(), + Self::Tdf27ChargerEnControl(builder) => builder.schema(), + Self::Tdf28GnssFixInfo(builder) => builder.schema(), + Self::Tdf29BluetoothConnection(builder) => builder.schema(), + Self::Tdf30BluetoothRssi(builder) => builder.schema(), + Self::Tdf31BluetoothDataThroughput(builder) => builder.schema(), + Self::Tdf32AlgorithmClassHistogram(builder) => builder.schema(), + Self::Tdf33AlgorithmClassTimeSeries(builder) => builder.schema(), + Self::Tdf34LteTacCells(builder) => builder.schema(), + Self::Tdf35WifiApInfo(builder) => builder.schema(), + Self::Tdf36DeviceTilt(builder) => builder.schema(), + Self::Tdf37Nrf9xGnssPvt(builder) => builder.schema(), + Self::Tdf38BatteryChargeAccumulated(builder) => builder.schema(), + Self::Tdf39InfuseBluetoothRssi(builder) => builder.schema(), + Self::Tdf40AdcRaw8(builder) => builder.schema(), + Self::Tdf41AdcRaw16(builder) => builder.schema(), + Self::Tdf42AdcRaw32(builder) => builder.schema(), + Self::Tdf43Annotation(builder) => builder.schema(), + Self::Tdf44LoraRx(builder) => builder.schema(), + Self::Tdf45LoraTx(builder) => builder.schema(), + Self::Tdf46IdxArrayFreq(builder) => builder.schema(), + Self::Tdf47IdxArrayPeriod(builder) => builder.schema(), + Self::Tdf48WifiConnected(builder) => builder.schema(), + Self::Tdf49WifiConnectionFailed(builder) => builder.schema(), + Self::Tdf50WifiDisconnected(builder) => builder.schema(), + Self::Tdf51NetworkScanCount(builder) => builder.schema(), + Self::Tdf52ExceptionStackFrame(builder) => builder.schema(), + Self::Tdf53BatteryVoltage(builder) => builder.schema(), + Self::Tdf54BatterySoc(builder) => builder.schema(), + Self::Tdf55StateEventSet(builder) => builder.schema(), + Self::Tdf56StateEventCleared(builder) => builder.schema(), + Self::Tdf57StateDuration(builder) => builder.schema(), + Self::Tdf58Pcm16bitChanLeft(builder) => builder.schema(), + Self::Tdf59Pcm16bitChanRight(builder) => builder.schema(), + Self::Tdf60Pcm16bitChanDual(builder) => builder.schema(), + Self::Tdf61KvsValueChanged(builder) => builder.schema(), + } + } + + pub fn rows(&self) -> usize { + match self { + Self::Tdf1Announce(builder) => builder.rows(), + Self::Tdf2BatteryState(builder) => builder.rows(), + Self::Tdf3AmbientTempPresHum(builder) => builder.rows(), + Self::Tdf4AmbientTemperature(builder) => builder.rows(), + Self::Tdf5TimeSync(builder) => builder.rows(), + Self::Tdf6RebootInfo(builder) => builder.rows(), + Self::Tdf7AnnounceV2(builder) => builder.rows(), + Self::Tdf8SocTemperature(builder) => builder.rows(), + Self::Tdf10Acc2g(builder) => builder.rows(), + Self::Tdf11Acc4g(builder) => builder.rows(), + Self::Tdf12Acc8g(builder) => builder.rows(), + Self::Tdf13Acc16g(builder) => builder.rows(), + Self::Tdf14Gyr125dps(builder) => builder.rows(), + Self::Tdf15Gyr250dps(builder) => builder.rows(), + Self::Tdf16Gyr500dps(builder) => builder.rows(), + Self::Tdf17Gyr1000dps(builder) => builder.rows(), + Self::Tdf18Gyr2000dps(builder) => builder.rows(), + Self::Tdf19GcsWgs84Llha(builder) => builder.rows(), + Self::Tdf20UbxNavPvt(builder) => builder.rows(), + Self::Tdf21LteConnStatus(builder) => builder.rows(), + Self::Tdf22GlobalstarPkt(builder) => builder.rows(), + Self::Tdf23AccMagnitudeStdDev(builder) => builder.rows(), + Self::Tdf24ActivityMetric(builder) => builder.rows(), + Self::Tdf25AlgorithmOutput(builder) => builder.rows(), + Self::Tdf26RuntimeError(builder) => builder.rows(), + Self::Tdf27ChargerEnControl(builder) => builder.rows(), + Self::Tdf28GnssFixInfo(builder) => builder.rows(), + Self::Tdf29BluetoothConnection(builder) => builder.rows(), + Self::Tdf30BluetoothRssi(builder) => builder.rows(), + Self::Tdf31BluetoothDataThroughput(builder) => builder.rows(), + Self::Tdf32AlgorithmClassHistogram(builder) => builder.rows(), + Self::Tdf33AlgorithmClassTimeSeries(builder) => builder.rows(), + Self::Tdf34LteTacCells(builder) => builder.rows(), + Self::Tdf35WifiApInfo(builder) => builder.rows(), + Self::Tdf36DeviceTilt(builder) => builder.rows(), + Self::Tdf37Nrf9xGnssPvt(builder) => builder.rows(), + Self::Tdf38BatteryChargeAccumulated(builder) => builder.rows(), + Self::Tdf39InfuseBluetoothRssi(builder) => builder.rows(), + Self::Tdf40AdcRaw8(builder) => builder.rows(), + Self::Tdf41AdcRaw16(builder) => builder.rows(), + Self::Tdf42AdcRaw32(builder) => builder.rows(), + Self::Tdf43Annotation(builder) => builder.rows(), + Self::Tdf44LoraRx(builder) => builder.rows(), + Self::Tdf45LoraTx(builder) => builder.rows(), + Self::Tdf46IdxArrayFreq(builder) => builder.rows(), + Self::Tdf47IdxArrayPeriod(builder) => builder.rows(), + Self::Tdf48WifiConnected(builder) => builder.rows(), + Self::Tdf49WifiConnectionFailed(builder) => builder.rows(), + Self::Tdf50WifiDisconnected(builder) => builder.rows(), + Self::Tdf51NetworkScanCount(builder) => builder.rows(), + Self::Tdf52ExceptionStackFrame(builder) => builder.rows(), + Self::Tdf53BatteryVoltage(builder) => builder.rows(), + Self::Tdf54BatterySoc(builder) => builder.rows(), + Self::Tdf55StateEventSet(builder) => builder.rows(), + Self::Tdf56StateEventCleared(builder) => builder.rows(), + Self::Tdf57StateDuration(builder) => builder.rows(), + Self::Tdf58Pcm16bitChanLeft(builder) => builder.rows(), + Self::Tdf59Pcm16bitChanRight(builder) => builder.rows(), + Self::Tdf60Pcm16bitChanDual(builder) => builder.rows(), + Self::Tdf61KvsValueChanged(builder) => builder.rows(), + } + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + match self { + Self::Tdf1Announce(builder) => builder.append(meta, size, cursor), + Self::Tdf2BatteryState(builder) => builder.append(meta, size, cursor), + Self::Tdf3AmbientTempPresHum(builder) => builder.append(meta, size, cursor), + Self::Tdf4AmbientTemperature(builder) => builder.append(meta, size, cursor), + Self::Tdf5TimeSync(builder) => builder.append(meta, size, cursor), + Self::Tdf6RebootInfo(builder) => builder.append(meta, size, cursor), + Self::Tdf7AnnounceV2(builder) => builder.append(meta, size, cursor), + Self::Tdf8SocTemperature(builder) => builder.append(meta, size, cursor), + Self::Tdf10Acc2g(builder) => builder.append(meta, size, cursor), + Self::Tdf11Acc4g(builder) => builder.append(meta, size, cursor), + Self::Tdf12Acc8g(builder) => builder.append(meta, size, cursor), + Self::Tdf13Acc16g(builder) => builder.append(meta, size, cursor), + Self::Tdf14Gyr125dps(builder) => builder.append(meta, size, cursor), + Self::Tdf15Gyr250dps(builder) => builder.append(meta, size, cursor), + Self::Tdf16Gyr500dps(builder) => builder.append(meta, size, cursor), + Self::Tdf17Gyr1000dps(builder) => builder.append(meta, size, cursor), + Self::Tdf18Gyr2000dps(builder) => builder.append(meta, size, cursor), + Self::Tdf19GcsWgs84Llha(builder) => builder.append(meta, size, cursor), + Self::Tdf20UbxNavPvt(builder) => builder.append(meta, size, cursor), + Self::Tdf21LteConnStatus(builder) => builder.append(meta, size, cursor), + Self::Tdf22GlobalstarPkt(builder) => builder.append(meta, size, cursor), + Self::Tdf23AccMagnitudeStdDev(builder) => builder.append(meta, size, cursor), + Self::Tdf24ActivityMetric(builder) => builder.append(meta, size, cursor), + Self::Tdf25AlgorithmOutput(builder) => builder.append(meta, size, cursor), + Self::Tdf26RuntimeError(builder) => builder.append(meta, size, cursor), + Self::Tdf27ChargerEnControl(builder) => builder.append(meta, size, cursor), + Self::Tdf28GnssFixInfo(builder) => builder.append(meta, size, cursor), + Self::Tdf29BluetoothConnection(builder) => builder.append(meta, size, cursor), + Self::Tdf30BluetoothRssi(builder) => builder.append(meta, size, cursor), + Self::Tdf31BluetoothDataThroughput(builder) => builder.append(meta, size, cursor), + Self::Tdf32AlgorithmClassHistogram(builder) => builder.append(meta, size, cursor), + Self::Tdf33AlgorithmClassTimeSeries(builder) => builder.append(meta, size, cursor), + Self::Tdf34LteTacCells(builder) => builder.append(meta, size, cursor), + Self::Tdf35WifiApInfo(builder) => builder.append(meta, size, cursor), + Self::Tdf36DeviceTilt(builder) => builder.append(meta, size, cursor), + Self::Tdf37Nrf9xGnssPvt(builder) => builder.append(meta, size, cursor), + Self::Tdf38BatteryChargeAccumulated(builder) => builder.append(meta, size, cursor), + Self::Tdf39InfuseBluetoothRssi(builder) => builder.append(meta, size, cursor), + Self::Tdf40AdcRaw8(builder) => builder.append(meta, size, cursor), + Self::Tdf41AdcRaw16(builder) => builder.append(meta, size, cursor), + Self::Tdf42AdcRaw32(builder) => builder.append(meta, size, cursor), + Self::Tdf43Annotation(builder) => builder.append(meta, size, cursor), + Self::Tdf44LoraRx(builder) => builder.append(meta, size, cursor), + Self::Tdf45LoraTx(builder) => builder.append(meta, size, cursor), + Self::Tdf46IdxArrayFreq(builder) => builder.append(meta, size, cursor), + Self::Tdf47IdxArrayPeriod(builder) => builder.append(meta, size, cursor), + Self::Tdf48WifiConnected(builder) => builder.append(meta, size, cursor), + Self::Tdf49WifiConnectionFailed(builder) => builder.append(meta, size, cursor), + Self::Tdf50WifiDisconnected(builder) => builder.append(meta, size, cursor), + Self::Tdf51NetworkScanCount(builder) => builder.append(meta, size, cursor), + Self::Tdf52ExceptionStackFrame(builder) => builder.append(meta, size, cursor), + Self::Tdf53BatteryVoltage(builder) => builder.append(meta, size, cursor), + Self::Tdf54BatterySoc(builder) => builder.append(meta, size, cursor), + Self::Tdf55StateEventSet(builder) => builder.append(meta, size, cursor), + Self::Tdf56StateEventCleared(builder) => builder.append(meta, size, cursor), + Self::Tdf57StateDuration(builder) => builder.append(meta, size, cursor), + Self::Tdf58Pcm16bitChanLeft(builder) => builder.append(meta, size, cursor), + Self::Tdf59Pcm16bitChanRight(builder) => builder.append(meta, size, cursor), + Self::Tdf60Pcm16bitChanDual(builder) => builder.append(meta, size, cursor), + Self::Tdf61KvsValueChanged(builder) => builder.append(meta, size, cursor), + } + } + + pub fn finish_batch(&mut self) -> std::result::Result { + match self { + Self::Tdf1Announce(builder) => builder.finish_batch(), + Self::Tdf2BatteryState(builder) => builder.finish_batch(), + Self::Tdf3AmbientTempPresHum(builder) => builder.finish_batch(), + Self::Tdf4AmbientTemperature(builder) => builder.finish_batch(), + Self::Tdf5TimeSync(builder) => builder.finish_batch(), + Self::Tdf6RebootInfo(builder) => builder.finish_batch(), + Self::Tdf7AnnounceV2(builder) => builder.finish_batch(), + Self::Tdf8SocTemperature(builder) => builder.finish_batch(), + Self::Tdf10Acc2g(builder) => builder.finish_batch(), + Self::Tdf11Acc4g(builder) => builder.finish_batch(), + Self::Tdf12Acc8g(builder) => builder.finish_batch(), + Self::Tdf13Acc16g(builder) => builder.finish_batch(), + Self::Tdf14Gyr125dps(builder) => builder.finish_batch(), + Self::Tdf15Gyr250dps(builder) => builder.finish_batch(), + Self::Tdf16Gyr500dps(builder) => builder.finish_batch(), + Self::Tdf17Gyr1000dps(builder) => builder.finish_batch(), + Self::Tdf18Gyr2000dps(builder) => builder.finish_batch(), + Self::Tdf19GcsWgs84Llha(builder) => builder.finish_batch(), + Self::Tdf20UbxNavPvt(builder) => builder.finish_batch(), + Self::Tdf21LteConnStatus(builder) => builder.finish_batch(), + Self::Tdf22GlobalstarPkt(builder) => builder.finish_batch(), + Self::Tdf23AccMagnitudeStdDev(builder) => builder.finish_batch(), + Self::Tdf24ActivityMetric(builder) => builder.finish_batch(), + Self::Tdf25AlgorithmOutput(builder) => builder.finish_batch(), + Self::Tdf26RuntimeError(builder) => builder.finish_batch(), + Self::Tdf27ChargerEnControl(builder) => builder.finish_batch(), + Self::Tdf28GnssFixInfo(builder) => builder.finish_batch(), + Self::Tdf29BluetoothConnection(builder) => builder.finish_batch(), + Self::Tdf30BluetoothRssi(builder) => builder.finish_batch(), + Self::Tdf31BluetoothDataThroughput(builder) => builder.finish_batch(), + Self::Tdf32AlgorithmClassHistogram(builder) => builder.finish_batch(), + Self::Tdf33AlgorithmClassTimeSeries(builder) => builder.finish_batch(), + Self::Tdf34LteTacCells(builder) => builder.finish_batch(), + Self::Tdf35WifiApInfo(builder) => builder.finish_batch(), + Self::Tdf36DeviceTilt(builder) => builder.finish_batch(), + Self::Tdf37Nrf9xGnssPvt(builder) => builder.finish_batch(), + Self::Tdf38BatteryChargeAccumulated(builder) => builder.finish_batch(), + Self::Tdf39InfuseBluetoothRssi(builder) => builder.finish_batch(), + Self::Tdf40AdcRaw8(builder) => builder.finish_batch(), + Self::Tdf41AdcRaw16(builder) => builder.finish_batch(), + Self::Tdf42AdcRaw32(builder) => builder.finish_batch(), + Self::Tdf43Annotation(builder) => builder.finish_batch(), + Self::Tdf44LoraRx(builder) => builder.finish_batch(), + Self::Tdf45LoraTx(builder) => builder.finish_batch(), + Self::Tdf46IdxArrayFreq(builder) => builder.finish_batch(), + Self::Tdf47IdxArrayPeriod(builder) => builder.finish_batch(), + Self::Tdf48WifiConnected(builder) => builder.finish_batch(), + Self::Tdf49WifiConnectionFailed(builder) => builder.finish_batch(), + Self::Tdf50WifiDisconnected(builder) => builder.finish_batch(), + Self::Tdf51NetworkScanCount(builder) => builder.finish_batch(), + Self::Tdf52ExceptionStackFrame(builder) => builder.finish_batch(), + Self::Tdf53BatteryVoltage(builder) => builder.finish_batch(), + Self::Tdf54BatterySoc(builder) => builder.finish_batch(), + Self::Tdf55StateEventSet(builder) => builder.finish_batch(), + Self::Tdf56StateEventCleared(builder) => builder.finish_batch(), + Self::Tdf57StateDuration(builder) => builder.finish_batch(), + Self::Tdf58Pcm16bitChanLeft(builder) => builder.finish_batch(), + Self::Tdf59Pcm16bitChanRight(builder) => builder.finish_batch(), + Self::Tdf60Pcm16bitChanDual(builder) => builder.finish_batch(), + Self::Tdf61KvsValueChanged(builder) => builder.finish_batch(), + } + } +} + +pub struct Tdf1AnnounceBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + application: Vec, + version_major: Vec, + version_minor: Vec, + version_revision: Vec, + version_build_num: Vec, + kv_crc: Vec, + blocks: Vec, + uptime: Vec, + reboots: Vec, + flags: Vec, +} + +impl Tdf1AnnounceBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + application: Vec::with_capacity(capacity), + version_major: Vec::with_capacity(capacity), + version_minor: Vec::with_capacity(capacity), + version_revision: Vec::with_capacity(capacity), + version_build_num: Vec::with_capacity(capacity), + kv_crc: Vec::with_capacity(capacity), + blocks: Vec::with_capacity(capacity), + uptime: Vec::with_capacity(capacity), + reboots: Vec::with_capacity(capacity), + flags: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(1).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.application.push(cursor.read_u32::()?); + self.version_major.push(cursor.read_u8()?); + self.version_minor.push(cursor.read_u8()?); + self.version_revision + .push(cursor.read_u16::()?); + self.version_build_num + .push(cursor.read_u32::()?); + self.kv_crc.push(cursor.read_u32::()?); + self.blocks.push(cursor.read_u32::()?); + self.uptime.push(cursor.read_u32::()?); + self.reboots.push(cursor.read_u16::()?); + self.flags.push(cursor.read_u8()?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(UInt32Array::from(std::mem::take(&mut self.application))) as ArrayRef, + Arc::new(StructArray::try_new( + Fields::from(vec![ + Field::new("major", DataType::UInt8, false), + Field::new("minor", DataType::UInt8, false), + Field::new("revision", DataType::UInt16, false), + Field::new("build_num", DataType::UInt32, false), + ]), + vec![ + Arc::new(UInt8Array::from(std::mem::take(&mut self.version_major))) as ArrayRef, + Arc::new(UInt8Array::from(std::mem::take(&mut self.version_minor))) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take( + &mut self.version_revision, + ))) as ArrayRef, + Arc::new(UInt32Array::from(std::mem::take( + &mut self.version_build_num, + ))) as ArrayRef, + ], + None, + )?) as ArrayRef, + Arc::new(UInt32Array::from(std::mem::take(&mut self.kv_crc))) as ArrayRef, + Arc::new(UInt32Array::from(std::mem::take(&mut self.blocks))) as ArrayRef, + Arc::new(UInt32Array::from(std::mem::take(&mut self.uptime))) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.reboots))) as ArrayRef, + Arc::new(UInt8Array::from(std::mem::take(&mut self.flags))) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf2BatteryStateBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + voltage_mv: Vec, + current_ua: Vec, + soc: Vec, +} + +impl Tdf2BatteryStateBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + voltage_mv: Vec::with_capacity(capacity), + current_ua: Vec::with_capacity(capacity), + soc: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(2).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.voltage_mv.push(cursor.read_u32::()?); + self.current_ua.push(cursor.read_i32::()?); + self.soc.push(cursor.read_u8()?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(UInt32Array::from(std::mem::take(&mut self.voltage_mv))) as ArrayRef, + Arc::new(Int32Array::from(std::mem::take(&mut self.current_ua))) as ArrayRef, + Arc::new(UInt8Array::from(std::mem::take(&mut self.soc))) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf3AmbientTempPresHumBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + temperature: Vec, + pressure: Vec, + humidity: Vec, +} + +impl Tdf3AmbientTempPresHumBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + temperature: Vec::with_capacity(capacity), + pressure: Vec::with_capacity(capacity), + humidity: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(3).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.temperature + .push(cursor.read_i32::()? as f64 / 1000.0); + self.pressure + .push(cursor.read_u32::()? as f64 / 1000.0); + self.humidity + .push(cursor.read_u16::()? as f64 / 100.0); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(Float64Array::from(std::mem::take(&mut self.temperature))) as ArrayRef, + Arc::new(Float64Array::from(std::mem::take(&mut self.pressure))) as ArrayRef, + Arc::new(Float64Array::from(std::mem::take(&mut self.humidity))) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf4AmbientTemperatureBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + temperature: Vec, +} + +impl Tdf4AmbientTemperatureBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + temperature: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(4).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.temperature + .push(cursor.read_i32::()? as f64 / 1000.0); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(Float64Array::from(std::mem::take(&mut self.temperature))) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf5TimeSyncBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + source: Vec, + shift: Vec, +} + +impl Tdf5TimeSyncBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + source: Vec::with_capacity(capacity), + shift: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(5).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.source.push(cursor.read_u8()?); + self.shift + .push(cursor.read_i32::()? as f64 / 1000000.0); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(UInt8Array::from(std::mem::take(&mut self.source))) as ArrayRef, + Arc::new(Float64Array::from(std::mem::take(&mut self.shift))) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf6RebootInfoBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + reason: Vec, + hardware_flags: Vec, + count: Vec, + uptime: Vec, + param_1: Vec, + param_2: Vec, + thread: Vec, +} + +impl Tdf6RebootInfoBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + reason: Vec::with_capacity(capacity), + hardware_flags: Vec::with_capacity(capacity), + count: Vec::with_capacity(capacity), + uptime: Vec::with_capacity(capacity), + param_1: Vec::with_capacity(capacity), + param_2: Vec::with_capacity(capacity), + thread: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(6).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.reason.push(cursor.read_u8()?); + self.hardware_flags.push(cursor.read_u32::()?); + self.count.push(cursor.read_u32::()?); + self.uptime.push(cursor.read_u32::()?); + self.param_1.push(cursor.read_u32::()?); + self.param_2.push(cursor.read_u32::()?); + self.thread.push(tdf_field_read_string_to_string( + cursor, + cursor_start, + 8, + size, + )?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(UInt8Array::from(std::mem::take(&mut self.reason))) as ArrayRef, + Arc::new(UInt32Array::from(std::mem::take(&mut self.hardware_flags))) as ArrayRef, + Arc::new(UInt32Array::from(std::mem::take(&mut self.count))) as ArrayRef, + Arc::new(UInt32Array::from(std::mem::take(&mut self.uptime))) as ArrayRef, + Arc::new(UInt32Array::from(std::mem::take(&mut self.param_1))) as ArrayRef, + Arc::new(UInt32Array::from(std::mem::take(&mut self.param_2))) as ArrayRef, + Arc::new(StringArray::from_iter_values(std::mem::take( + &mut self.thread, + ))) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf7AnnounceV2Builder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + application: Vec, + version_major: Vec, + version_minor: Vec, + version_revision: Vec, + version_build_num: Vec, + board_crc: Vec, + kv_crc: Vec, + blocks: Vec, + uptime: Vec, + reboots: Vec, + flags: Vec, +} + +impl Tdf7AnnounceV2Builder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + application: Vec::with_capacity(capacity), + version_major: Vec::with_capacity(capacity), + version_minor: Vec::with_capacity(capacity), + version_revision: Vec::with_capacity(capacity), + version_build_num: Vec::with_capacity(capacity), + board_crc: Vec::with_capacity(capacity), + kv_crc: Vec::with_capacity(capacity), + blocks: Vec::with_capacity(capacity), + uptime: Vec::with_capacity(capacity), + reboots: Vec::with_capacity(capacity), + flags: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(7).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.application.push(cursor.read_u32::()?); + self.version_major.push(cursor.read_u8()?); + self.version_minor.push(cursor.read_u8()?); + self.version_revision + .push(cursor.read_u16::()?); + self.version_build_num + .push(cursor.read_u32::()?); + self.board_crc.push(cursor.read_u16::()?); + self.kv_crc.push(cursor.read_u32::()?); + self.blocks.push(cursor.read_u32::()?); + self.uptime.push(cursor.read_u32::()?); + self.reboots.push(cursor.read_u16::()?); + self.flags.push(cursor.read_u8()?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(UInt32Array::from(std::mem::take(&mut self.application))) as ArrayRef, + Arc::new(StructArray::try_new( + Fields::from(vec![ + Field::new("major", DataType::UInt8, false), + Field::new("minor", DataType::UInt8, false), + Field::new("revision", DataType::UInt16, false), + Field::new("build_num", DataType::UInt32, false), + ]), + vec![ + Arc::new(UInt8Array::from(std::mem::take(&mut self.version_major))) as ArrayRef, + Arc::new(UInt8Array::from(std::mem::take(&mut self.version_minor))) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take( + &mut self.version_revision, + ))) as ArrayRef, + Arc::new(UInt32Array::from(std::mem::take( + &mut self.version_build_num, + ))) as ArrayRef, + ], + None, + )?) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.board_crc))) as ArrayRef, + Arc::new(UInt32Array::from(std::mem::take(&mut self.kv_crc))) as ArrayRef, + Arc::new(UInt32Array::from(std::mem::take(&mut self.blocks))) as ArrayRef, + Arc::new(UInt32Array::from(std::mem::take(&mut self.uptime))) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.reboots))) as ArrayRef, + Arc::new(UInt8Array::from(std::mem::take(&mut self.flags))) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf8SocTemperatureBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + temperature: Vec, +} + +impl Tdf8SocTemperatureBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + temperature: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(8).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.temperature + .push(cursor.read_i16::()? as f64 / 100.0); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(Float64Array::from(std::mem::take(&mut self.temperature))) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf10Acc2gBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + sample_x: Vec, + sample_y: Vec, + sample_z: Vec, +} + +impl Tdf10Acc2gBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + sample_x: Vec::with_capacity(capacity), + sample_y: Vec::with_capacity(capacity), + sample_z: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(10).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.sample_x.push(cursor.read_i16::()?); + self.sample_y.push(cursor.read_i16::()?); + self.sample_z.push(cursor.read_i16::()?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(StructArray::try_new( + Fields::from(vec![ + Field::new("x", DataType::Int16, false), + Field::new("y", DataType::Int16, false), + Field::new("z", DataType::Int16, false), + ]), + vec![ + Arc::new(Int16Array::from(std::mem::take(&mut self.sample_x))) as ArrayRef, + Arc::new(Int16Array::from(std::mem::take(&mut self.sample_y))) as ArrayRef, + Arc::new(Int16Array::from(std::mem::take(&mut self.sample_z))) as ArrayRef, + ], + None, + )?) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf11Acc4gBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + sample_x: Vec, + sample_y: Vec, + sample_z: Vec, +} + +impl Tdf11Acc4gBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + sample_x: Vec::with_capacity(capacity), + sample_y: Vec::with_capacity(capacity), + sample_z: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(11).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.sample_x.push(cursor.read_i16::()?); + self.sample_y.push(cursor.read_i16::()?); + self.sample_z.push(cursor.read_i16::()?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(StructArray::try_new( + Fields::from(vec![ + Field::new("x", DataType::Int16, false), + Field::new("y", DataType::Int16, false), + Field::new("z", DataType::Int16, false), + ]), + vec![ + Arc::new(Int16Array::from(std::mem::take(&mut self.sample_x))) as ArrayRef, + Arc::new(Int16Array::from(std::mem::take(&mut self.sample_y))) as ArrayRef, + Arc::new(Int16Array::from(std::mem::take(&mut self.sample_z))) as ArrayRef, + ], + None, + )?) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf12Acc8gBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + sample_x: Vec, + sample_y: Vec, + sample_z: Vec, +} + +impl Tdf12Acc8gBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + sample_x: Vec::with_capacity(capacity), + sample_y: Vec::with_capacity(capacity), + sample_z: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(12).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.sample_x.push(cursor.read_i16::()?); + self.sample_y.push(cursor.read_i16::()?); + self.sample_z.push(cursor.read_i16::()?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(StructArray::try_new( + Fields::from(vec![ + Field::new("x", DataType::Int16, false), + Field::new("y", DataType::Int16, false), + Field::new("z", DataType::Int16, false), + ]), + vec![ + Arc::new(Int16Array::from(std::mem::take(&mut self.sample_x))) as ArrayRef, + Arc::new(Int16Array::from(std::mem::take(&mut self.sample_y))) as ArrayRef, + Arc::new(Int16Array::from(std::mem::take(&mut self.sample_z))) as ArrayRef, + ], + None, + )?) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf13Acc16gBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + sample_x: Vec, + sample_y: Vec, + sample_z: Vec, +} + +impl Tdf13Acc16gBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + sample_x: Vec::with_capacity(capacity), + sample_y: Vec::with_capacity(capacity), + sample_z: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(13).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.sample_x.push(cursor.read_i16::()?); + self.sample_y.push(cursor.read_i16::()?); + self.sample_z.push(cursor.read_i16::()?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(StructArray::try_new( + Fields::from(vec![ + Field::new("x", DataType::Int16, false), + Field::new("y", DataType::Int16, false), + Field::new("z", DataType::Int16, false), + ]), + vec![ + Arc::new(Int16Array::from(std::mem::take(&mut self.sample_x))) as ArrayRef, + Arc::new(Int16Array::from(std::mem::take(&mut self.sample_y))) as ArrayRef, + Arc::new(Int16Array::from(std::mem::take(&mut self.sample_z))) as ArrayRef, + ], + None, + )?) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf14Gyr125dpsBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + sample_x: Vec, + sample_y: Vec, + sample_z: Vec, +} + +impl Tdf14Gyr125dpsBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + sample_x: Vec::with_capacity(capacity), + sample_y: Vec::with_capacity(capacity), + sample_z: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(14).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.sample_x.push(cursor.read_i16::()?); + self.sample_y.push(cursor.read_i16::()?); + self.sample_z.push(cursor.read_i16::()?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(StructArray::try_new( + Fields::from(vec![ + Field::new("x", DataType::Int16, false), + Field::new("y", DataType::Int16, false), + Field::new("z", DataType::Int16, false), + ]), + vec![ + Arc::new(Int16Array::from(std::mem::take(&mut self.sample_x))) as ArrayRef, + Arc::new(Int16Array::from(std::mem::take(&mut self.sample_y))) as ArrayRef, + Arc::new(Int16Array::from(std::mem::take(&mut self.sample_z))) as ArrayRef, + ], + None, + )?) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf15Gyr250dpsBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + sample_x: Vec, + sample_y: Vec, + sample_z: Vec, +} + +impl Tdf15Gyr250dpsBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + sample_x: Vec::with_capacity(capacity), + sample_y: Vec::with_capacity(capacity), + sample_z: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(15).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.sample_x.push(cursor.read_i16::()?); + self.sample_y.push(cursor.read_i16::()?); + self.sample_z.push(cursor.read_i16::()?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(StructArray::try_new( + Fields::from(vec![ + Field::new("x", DataType::Int16, false), + Field::new("y", DataType::Int16, false), + Field::new("z", DataType::Int16, false), + ]), + vec![ + Arc::new(Int16Array::from(std::mem::take(&mut self.sample_x))) as ArrayRef, + Arc::new(Int16Array::from(std::mem::take(&mut self.sample_y))) as ArrayRef, + Arc::new(Int16Array::from(std::mem::take(&mut self.sample_z))) as ArrayRef, + ], + None, + )?) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf16Gyr500dpsBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + sample_x: Vec, + sample_y: Vec, + sample_z: Vec, +} + +impl Tdf16Gyr500dpsBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + sample_x: Vec::with_capacity(capacity), + sample_y: Vec::with_capacity(capacity), + sample_z: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(16).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.sample_x.push(cursor.read_i16::()?); + self.sample_y.push(cursor.read_i16::()?); + self.sample_z.push(cursor.read_i16::()?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(StructArray::try_new( + Fields::from(vec![ + Field::new("x", DataType::Int16, false), + Field::new("y", DataType::Int16, false), + Field::new("z", DataType::Int16, false), + ]), + vec![ + Arc::new(Int16Array::from(std::mem::take(&mut self.sample_x))) as ArrayRef, + Arc::new(Int16Array::from(std::mem::take(&mut self.sample_y))) as ArrayRef, + Arc::new(Int16Array::from(std::mem::take(&mut self.sample_z))) as ArrayRef, + ], + None, + )?) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf17Gyr1000dpsBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + sample_x: Vec, + sample_y: Vec, + sample_z: Vec, +} + +impl Tdf17Gyr1000dpsBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + sample_x: Vec::with_capacity(capacity), + sample_y: Vec::with_capacity(capacity), + sample_z: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(17).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.sample_x.push(cursor.read_i16::()?); + self.sample_y.push(cursor.read_i16::()?); + self.sample_z.push(cursor.read_i16::()?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(StructArray::try_new( + Fields::from(vec![ + Field::new("x", DataType::Int16, false), + Field::new("y", DataType::Int16, false), + Field::new("z", DataType::Int16, false), + ]), + vec![ + Arc::new(Int16Array::from(std::mem::take(&mut self.sample_x))) as ArrayRef, + Arc::new(Int16Array::from(std::mem::take(&mut self.sample_y))) as ArrayRef, + Arc::new(Int16Array::from(std::mem::take(&mut self.sample_z))) as ArrayRef, + ], + None, + )?) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf18Gyr2000dpsBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + sample_x: Vec, + sample_y: Vec, + sample_z: Vec, +} + +impl Tdf18Gyr2000dpsBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + sample_x: Vec::with_capacity(capacity), + sample_y: Vec::with_capacity(capacity), + sample_z: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(18).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.sample_x.push(cursor.read_i16::()?); + self.sample_y.push(cursor.read_i16::()?); + self.sample_z.push(cursor.read_i16::()?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(StructArray::try_new( + Fields::from(vec![ + Field::new("x", DataType::Int16, false), + Field::new("y", DataType::Int16, false), + Field::new("z", DataType::Int16, false), + ]), + vec![ + Arc::new(Int16Array::from(std::mem::take(&mut self.sample_x))) as ArrayRef, + Arc::new(Int16Array::from(std::mem::take(&mut self.sample_y))) as ArrayRef, + Arc::new(Int16Array::from(std::mem::take(&mut self.sample_z))) as ArrayRef, + ], + None, + )?) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf19GcsWgs84LlhaBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + location_latitude: Vec, + location_longitude: Vec, + location_height: Vec, + h_acc: Vec, + v_acc: Vec, +} + +impl Tdf19GcsWgs84LlhaBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + location_latitude: Vec::with_capacity(capacity), + location_longitude: Vec::with_capacity(capacity), + location_height: Vec::with_capacity(capacity), + h_acc: Vec::with_capacity(capacity), + v_acc: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(19).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.location_latitude + .push(cursor.read_i32::()? as f64 / 10000000.0); + self.location_longitude + .push(cursor.read_i32::()? as f64 / 10000000.0); + self.location_height + .push(cursor.read_i32::()? as f64 / 1000.0); + self.h_acc + .push(cursor.read_i32::()? as f64 / 1000.0); + self.v_acc + .push(cursor.read_i32::()? as f64 / 1000.0); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(StructArray::try_new( + Fields::from(vec![ + Field::new("latitude", DataType::Float64, false), + Field::new("longitude", DataType::Float64, false), + Field::new("height", DataType::Float64, false), + ]), + vec![ + Arc::new(Float64Array::from(std::mem::take( + &mut self.location_latitude, + ))) as ArrayRef, + Arc::new(Float64Array::from(std::mem::take( + &mut self.location_longitude, + ))) as ArrayRef, + Arc::new(Float64Array::from(std::mem::take( + &mut self.location_height, + ))) as ArrayRef, + ], + None, + )?) as ArrayRef, + Arc::new(Float64Array::from(std::mem::take(&mut self.h_acc))) as ArrayRef, + Arc::new(Float64Array::from(std::mem::take(&mut self.v_acc))) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf20UbxNavPvtBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + itow: Vec, + year: Vec, + month: Vec, + day: Vec, + hour: Vec, + min: Vec, + sec: Vec, + valid: Vec, + t_acc: Vec, + nano: Vec, + fix_type: Vec, + flags: Vec, + flags2: Vec, + num_sv: Vec, + lon: Vec, + lat: Vec, + height: Vec, + h_msl: Vec, + h_acc: Vec, + v_acc: Vec, + vel_n: Vec, + vel_e: Vec, + vel_d: Vec, + g_speed: Vec, + head_mot: Vec, + s_acc: Vec, + head_acc: Vec, + p_dop: Vec, + flags3: Vec, + reserved0: Vec, + head_veh: Vec, + mag_dec: Vec, + mag_acc: Vec, +} + +impl Tdf20UbxNavPvtBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + itow: Vec::with_capacity(capacity), + year: Vec::with_capacity(capacity), + month: Vec::with_capacity(capacity), + day: Vec::with_capacity(capacity), + hour: Vec::with_capacity(capacity), + min: Vec::with_capacity(capacity), + sec: Vec::with_capacity(capacity), + valid: Vec::with_capacity(capacity), + t_acc: Vec::with_capacity(capacity), + nano: Vec::with_capacity(capacity), + fix_type: Vec::with_capacity(capacity), + flags: Vec::with_capacity(capacity), + flags2: Vec::with_capacity(capacity), + num_sv: Vec::with_capacity(capacity), + lon: Vec::with_capacity(capacity), + lat: Vec::with_capacity(capacity), + height: Vec::with_capacity(capacity), + h_msl: Vec::with_capacity(capacity), + h_acc: Vec::with_capacity(capacity), + v_acc: Vec::with_capacity(capacity), + vel_n: Vec::with_capacity(capacity), + vel_e: Vec::with_capacity(capacity), + vel_d: Vec::with_capacity(capacity), + g_speed: Vec::with_capacity(capacity), + head_mot: Vec::with_capacity(capacity), + s_acc: Vec::with_capacity(capacity), + head_acc: Vec::with_capacity(capacity), + p_dop: Vec::with_capacity(capacity), + flags3: Vec::with_capacity(capacity), + reserved0: Vec::with_capacity(capacity * 4), + head_veh: Vec::with_capacity(capacity), + mag_dec: Vec::with_capacity(capacity), + mag_acc: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(20).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.itow.push(cursor.read_u32::()?); + self.year.push(cursor.read_u16::()?); + self.month.push(cursor.read_u8()?); + self.day.push(cursor.read_u8()?); + self.hour.push(cursor.read_u8()?); + self.min.push(cursor.read_u8()?); + self.sec.push(cursor.read_u8()?); + self.valid.push(cursor.read_u8()?); + self.t_acc.push(cursor.read_u32::()?); + self.nano.push(cursor.read_i32::()?); + self.fix_type.push(cursor.read_u8()?); + self.flags.push(cursor.read_u8()?); + self.flags2.push(cursor.read_u8()?); + self.num_sv.push(cursor.read_u8()?); + self.lon + .push(cursor.read_i32::()? as f64 / 10000000.0); + self.lat + .push(cursor.read_i32::()? as f64 / 10000000.0); + self.height + .push(cursor.read_i32::()? as f64 / 1000.0); + self.h_msl + .push(cursor.read_i32::()? as f64 / 1000.0); + self.h_acc + .push(cursor.read_u32::()? as f64 / 1000.0); + self.v_acc + .push(cursor.read_u32::()? as f64 / 1000.0); + self.vel_n + .push(cursor.read_i32::()? as f64 / 1000.0); + self.vel_e + .push(cursor.read_i32::()? as f64 / 1000.0); + self.vel_d + .push(cursor.read_i32::()? as f64 / 1000.0); + self.g_speed + .push(cursor.read_i32::()? as f64 / 1000.0); + self.head_mot + .push(cursor.read_i32::()? as f64 / 100000.0); + self.s_acc + .push(cursor.read_u32::()? as f64 / 1000.0); + self.head_acc + .push(cursor.read_u32::()? as f64 / 100000.0); + self.p_dop + .push(cursor.read_u16::()? as f64 / 100.0); + self.flags3.push(cursor.read_u16::()?); + self.reserved0.push(cursor.read_u8()?); + self.reserved0.push(cursor.read_u8()?); + self.reserved0.push(cursor.read_u8()?); + self.reserved0.push(cursor.read_u8()?); + self.head_veh + .push(cursor.read_i32::()? as f64 / 100000.0); + self.mag_dec + .push(cursor.read_i16::()? as f64 / 100.0); + self.mag_acc + .push(cursor.read_u16::()? as f64 / 100.0); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(UInt32Array::from(std::mem::take(&mut self.itow))) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.year))) as ArrayRef, + Arc::new(UInt8Array::from(std::mem::take(&mut self.month))) as ArrayRef, + Arc::new(UInt8Array::from(std::mem::take(&mut self.day))) as ArrayRef, + Arc::new(UInt8Array::from(std::mem::take(&mut self.hour))) as ArrayRef, + Arc::new(UInt8Array::from(std::mem::take(&mut self.min))) as ArrayRef, + Arc::new(UInt8Array::from(std::mem::take(&mut self.sec))) as ArrayRef, + Arc::new(UInt8Array::from(std::mem::take(&mut self.valid))) as ArrayRef, + Arc::new(UInt32Array::from(std::mem::take(&mut self.t_acc))) as ArrayRef, + Arc::new(Int32Array::from(std::mem::take(&mut self.nano))) as ArrayRef, + Arc::new(UInt8Array::from(std::mem::take(&mut self.fix_type))) as ArrayRef, + Arc::new(UInt8Array::from(std::mem::take(&mut self.flags))) as ArrayRef, + Arc::new(UInt8Array::from(std::mem::take(&mut self.flags2))) as ArrayRef, + Arc::new(UInt8Array::from(std::mem::take(&mut self.num_sv))) as ArrayRef, + Arc::new(Float64Array::from(std::mem::take(&mut self.lon))) as ArrayRef, + Arc::new(Float64Array::from(std::mem::take(&mut self.lat))) as ArrayRef, + Arc::new(Float64Array::from(std::mem::take(&mut self.height))) as ArrayRef, + Arc::new(Float64Array::from(std::mem::take(&mut self.h_msl))) as ArrayRef, + Arc::new(Float64Array::from(std::mem::take(&mut self.h_acc))) as ArrayRef, + Arc::new(Float64Array::from(std::mem::take(&mut self.v_acc))) as ArrayRef, + Arc::new(Float64Array::from(std::mem::take(&mut self.vel_n))) as ArrayRef, + Arc::new(Float64Array::from(std::mem::take(&mut self.vel_e))) as ArrayRef, + Arc::new(Float64Array::from(std::mem::take(&mut self.vel_d))) as ArrayRef, + Arc::new(Float64Array::from(std::mem::take(&mut self.g_speed))) as ArrayRef, + Arc::new(Float64Array::from(std::mem::take(&mut self.head_mot))) as ArrayRef, + Arc::new(Float64Array::from(std::mem::take(&mut self.s_acc))) as ArrayRef, + Arc::new(Float64Array::from(std::mem::take(&mut self.head_acc))) as ArrayRef, + Arc::new(Float64Array::from(std::mem::take(&mut self.p_dop))) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.flags3))) as ArrayRef, + Arc::new(FixedSizeListArray::try_new( + Arc::new(Field::new_list_field(DataType::UInt8, false)), + 4, + Arc::new(UInt8Array::from(std::mem::take(&mut self.reserved0))) as ArrayRef, + None, + )?) as ArrayRef, + Arc::new(Float64Array::from(std::mem::take(&mut self.head_veh))) as ArrayRef, + Arc::new(Float64Array::from(std::mem::take(&mut self.mag_dec))) as ArrayRef, + Arc::new(Float64Array::from(std::mem::take(&mut self.mag_acc))) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf21LteConnStatusBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + cell_mcc: Vec, + cell_mnc: Vec, + cell_eci: Vec, + cell_tac: Vec, + earfcn: Vec, + status: Vec, + tech: Vec, + rsrp: Vec, + rsrq: Vec, +} + +impl Tdf21LteConnStatusBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + cell_mcc: Vec::with_capacity(capacity), + cell_mnc: Vec::with_capacity(capacity), + cell_eci: Vec::with_capacity(capacity), + cell_tac: Vec::with_capacity(capacity), + earfcn: Vec::with_capacity(capacity), + status: Vec::with_capacity(capacity), + tech: Vec::with_capacity(capacity), + rsrp: Vec::with_capacity(capacity), + rsrq: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(21).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.cell_mcc.push(cursor.read_u16::()?); + self.cell_mnc.push(cursor.read_u16::()?); + self.cell_eci.push(cursor.read_u32::()?); + self.cell_tac.push(cursor.read_u16::()?); + self.earfcn.push(cursor.read_u32::()?); + self.status.push(cursor.read_u8()?); + self.tech.push(cursor.read_u8()?); + self.rsrp.push(cursor.read_u8()? as f64 / -1.0); + self.rsrq.push(cursor.read_i8()?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(StructArray::try_new( + Fields::from(vec![ + Field::new("mcc", DataType::UInt16, false), + Field::new("mnc", DataType::UInt16, false), + Field::new("eci", DataType::UInt32, false), + Field::new("tac", DataType::UInt16, false), + ]), + vec![ + Arc::new(UInt16Array::from(std::mem::take(&mut self.cell_mcc))) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.cell_mnc))) as ArrayRef, + Arc::new(UInt32Array::from(std::mem::take(&mut self.cell_eci))) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.cell_tac))) as ArrayRef, + ], + None, + )?) as ArrayRef, + Arc::new(UInt32Array::from(std::mem::take(&mut self.earfcn))) as ArrayRef, + Arc::new(UInt8Array::from(std::mem::take(&mut self.status))) as ArrayRef, + Arc::new(UInt8Array::from(std::mem::take(&mut self.tech))) as ArrayRef, + Arc::new(Float64Array::from(std::mem::take(&mut self.rsrp))) as ArrayRef, + Arc::new(Int8Array::from(std::mem::take(&mut self.rsrq))) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf22GlobalstarPktBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + payload: Vec, +} + +impl Tdf22GlobalstarPktBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + payload: Vec::with_capacity(capacity * 9), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(22).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.payload.push(cursor.read_u8()?); + self.payload.push(cursor.read_u8()?); + self.payload.push(cursor.read_u8()?); + self.payload.push(cursor.read_u8()?); + self.payload.push(cursor.read_u8()?); + self.payload.push(cursor.read_u8()?); + self.payload.push(cursor.read_u8()?); + self.payload.push(cursor.read_u8()?); + self.payload.push(cursor.read_u8()?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(FixedSizeListArray::try_new( + Arc::new(Field::new_list_field(DataType::UInt8, false)), + 9, + Arc::new(UInt8Array::from(std::mem::take(&mut self.payload))) as ArrayRef, + None, + )?) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf23AccMagnitudeStdDevBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + count: Vec, + std_dev: Vec, +} + +impl Tdf23AccMagnitudeStdDevBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + count: Vec::with_capacity(capacity), + std_dev: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(23).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.count.push(cursor.read_u32::()?); + self.std_dev.push(cursor.read_u32::()?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(UInt32Array::from(std::mem::take(&mut self.count))) as ArrayRef, + Arc::new(UInt32Array::from(std::mem::take(&mut self.std_dev))) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf24ActivityMetricBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + value: Vec, +} + +impl Tdf24ActivityMetricBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + value: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(24).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.value.push(cursor.read_u32::()?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(UInt32Array::from(std::mem::take(&mut self.value))) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf25AlgorithmOutputBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + algorithm_id: Vec, + algorithm_version: Vec, + output: Vec>, +} + +impl Tdf25AlgorithmOutputBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + algorithm_id: Vec::with_capacity(capacity), + algorithm_version: Vec::with_capacity(capacity), + output: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(25).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.algorithm_id.push(cursor.read_u32::()?); + self.algorithm_version + .push(cursor.read_u16::()?); + self.output.push(crate::decoders::tdf_field_read_vla( + cursor, + cursor_start, + size, + )?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(UInt32Array::from(std::mem::take(&mut self.algorithm_id))) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take( + &mut self.algorithm_version, + ))) as ArrayRef, + Arc::new(BinaryArray::from_iter_values(std::mem::take( + &mut self.output, + ))) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf26RuntimeErrorBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + error_id: Vec, + error_ctx: Vec, +} + +impl Tdf26RuntimeErrorBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + error_id: Vec::with_capacity(capacity), + error_ctx: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(26).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.error_id.push(cursor.read_u32::()?); + self.error_ctx.push(cursor.read_u32::()?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(UInt32Array::from(std::mem::take(&mut self.error_id))) as ArrayRef, + Arc::new(UInt32Array::from(std::mem::take(&mut self.error_ctx))) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf27ChargerEnControlBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + enabled: Vec, +} + +impl Tdf27ChargerEnControlBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + enabled: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(27).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.enabled.push(cursor.read_u8()?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(UInt8Array::from(std::mem::take(&mut self.enabled))) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf28GnssFixInfoBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + time_fix: Vec, + location_fix: Vec, + num_sv: Vec, +} + +impl Tdf28GnssFixInfoBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + time_fix: Vec::with_capacity(capacity), + location_fix: Vec::with_capacity(capacity), + num_sv: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(28).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.time_fix.push(cursor.read_u16::()?); + self.location_fix.push(cursor.read_u16::()?); + self.num_sv.push(cursor.read_u8()?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.time_fix))) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.location_fix))) as ArrayRef, + Arc::new(UInt8Array::from(std::mem::take(&mut self.num_sv))) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf29BluetoothConnectionBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + address_type: Vec, + address_val: Vec, + connected: Vec, +} + +impl Tdf29BluetoothConnectionBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + address_type: Vec::with_capacity(capacity), + address_val: Vec::with_capacity(capacity), + connected: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(29).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.address_type.push(cursor.read_u8()?); + self.address_val.push(cursor.read_u48::()?); + self.connected.push(cursor.read_u8()?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(StructArray::try_new( + Fields::from(vec![ + Field::new("type", DataType::UInt8, false), + Field::new("val", DataType::UInt64, false), + ]), + vec![ + Arc::new(UInt8Array::from(std::mem::take(&mut self.address_type))) as ArrayRef, + Arc::new(UInt64Array::from(std::mem::take(&mut self.address_val))) as ArrayRef, + ], + None, + )?) as ArrayRef, + Arc::new(UInt8Array::from(std::mem::take(&mut self.connected))) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf30BluetoothRssiBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + address_type: Vec, + address_val: Vec, + rssi: Vec, +} + +impl Tdf30BluetoothRssiBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + address_type: Vec::with_capacity(capacity), + address_val: Vec::with_capacity(capacity), + rssi: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(30).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.address_type.push(cursor.read_u8()?); + self.address_val.push(cursor.read_u48::()?); + self.rssi.push(cursor.read_i8()?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(StructArray::try_new( + Fields::from(vec![ + Field::new("type", DataType::UInt8, false), + Field::new("val", DataType::UInt64, false), + ]), + vec![ + Arc::new(UInt8Array::from(std::mem::take(&mut self.address_type))) as ArrayRef, + Arc::new(UInt64Array::from(std::mem::take(&mut self.address_val))) as ArrayRef, + ], + None, + )?) as ArrayRef, + Arc::new(Int8Array::from(std::mem::take(&mut self.rssi))) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf31BluetoothDataThroughputBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + address_type: Vec, + address_val: Vec, + throughput: Vec, +} + +impl Tdf31BluetoothDataThroughputBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + address_type: Vec::with_capacity(capacity), + address_val: Vec::with_capacity(capacity), + throughput: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(31).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.address_type.push(cursor.read_u8()?); + self.address_val.push(cursor.read_u48::()?); + self.throughput.push(cursor.read_i32::()?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(StructArray::try_new( + Fields::from(vec![ + Field::new("type", DataType::UInt8, false), + Field::new("val", DataType::UInt64, false), + ]), + vec![ + Arc::new(UInt8Array::from(std::mem::take(&mut self.address_type))) as ArrayRef, + Arc::new(UInt64Array::from(std::mem::take(&mut self.address_val))) as ArrayRef, + ], + None, + )?) as ArrayRef, + Arc::new(Int32Array::from(std::mem::take(&mut self.throughput))) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf32AlgorithmClassHistogramBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + algorithm_id: Vec, + algorithm_version: Vec, + classes: Vec>, +} + +impl Tdf32AlgorithmClassHistogramBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + algorithm_id: Vec::with_capacity(capacity), + algorithm_version: Vec::with_capacity(capacity), + classes: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(32).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.algorithm_id.push(cursor.read_u32::()?); + self.algorithm_version + .push(cursor.read_u16::()?); + self.classes.push(crate::decoders::tdf_field_read_vla( + cursor, + cursor_start, + size, + )?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(UInt32Array::from(std::mem::take(&mut self.algorithm_id))) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take( + &mut self.algorithm_version, + ))) as ArrayRef, + Arc::new(BinaryArray::from_iter_values(std::mem::take( + &mut self.classes, + ))) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf33AlgorithmClassTimeSeriesBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + algorithm_id: Vec, + algorithm_version: Vec, + values: Vec>, +} + +impl Tdf33AlgorithmClassTimeSeriesBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + algorithm_id: Vec::with_capacity(capacity), + algorithm_version: Vec::with_capacity(capacity), + values: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(33).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.algorithm_id.push(cursor.read_u32::()?); + self.algorithm_version + .push(cursor.read_u16::()?); + self.values.push(crate::decoders::tdf_field_read_vla( + cursor, + cursor_start, + size, + )?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(UInt32Array::from(std::mem::take(&mut self.algorithm_id))) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take( + &mut self.algorithm_version, + ))) as ArrayRef, + Arc::new(BinaryArray::from_iter_values(std::mem::take( + &mut self.values, + ))) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf34LteTacCellsBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + cell_mcc: Vec, + cell_mnc: Vec, + cell_eci: Vec, + cell_tac: Vec, + earfcn: Vec, + rsrp: Vec, + rsrq: Vec, + neighbours_offsets: Vec, + neighbours_earfcn: Vec, + neighbours_pci: Vec, + neighbours_time_diff: Vec, + neighbours_rsrp: Vec, + neighbours_rsrq: Vec, +} + +impl Tdf34LteTacCellsBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + cell_mcc: Vec::with_capacity(capacity), + cell_mnc: Vec::with_capacity(capacity), + cell_eci: Vec::with_capacity(capacity), + cell_tac: Vec::with_capacity(capacity), + earfcn: Vec::with_capacity(capacity), + rsrp: Vec::with_capacity(capacity), + rsrq: Vec::with_capacity(capacity), + neighbours_offsets: vec![0], + neighbours_earfcn: Vec::with_capacity(capacity), + neighbours_pci: Vec::with_capacity(capacity), + neighbours_time_diff: Vec::with_capacity(capacity), + neighbours_rsrp: Vec::with_capacity(capacity), + neighbours_rsrq: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(34).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + fn list_value_field(&self, field_index: usize) -> Arc { + let schema = self.schema(); + match schema.field(field_index).data_type() { + DataType::List(field) => field.clone(), + _ => unreachable!("generated list field index is not a list"), + } + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.cell_mcc.push(cursor.read_u16::()?); + self.cell_mnc.push(cursor.read_u16::()?); + self.cell_eci.push(cursor.read_u32::()?); + self.cell_tac.push(cursor.read_u16::()?); + self.earfcn.push(cursor.read_u32::()?); + self.rsrp.push(cursor.read_u8()? as f64 / -1.0); + self.rsrq.push(cursor.read_i8()?); + { + let bytes_remaining = crate::decoders::vla_bytes_remaining(cursor, cursor_start, size)?; + if bytes_remaining % 10 != 0 { + return Err(Error::new( + ErrorKind::InvalidData, + "Variable-length array does not align to element size", + )); + } + let item_count = bytes_remaining / 10; + for _ in 0..item_count { + self.neighbours_earfcn + .push(cursor.read_u32::()?); + self.neighbours_pci.push(cursor.read_u16::()?); + self.neighbours_time_diff + .push(cursor.read_u16::()? as f64 / 1000.0); + self.neighbours_rsrp.push(cursor.read_u8()? as f64 / -1.0); + self.neighbours_rsrq.push(cursor.read_i8()?); + } + self.neighbours_offsets + .push(*self.neighbours_offsets.last().unwrap() + item_count as i32); + } + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(StructArray::try_new( + Fields::from(vec![ + Field::new("mcc", DataType::UInt16, false), + Field::new("mnc", DataType::UInt16, false), + Field::new("eci", DataType::UInt32, false), + Field::new("tac", DataType::UInt16, false), + ]), + vec![ + Arc::new(UInt16Array::from(std::mem::take(&mut self.cell_mcc))) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.cell_mnc))) as ArrayRef, + Arc::new(UInt32Array::from(std::mem::take(&mut self.cell_eci))) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.cell_tac))) as ArrayRef, + ], + None, + )?) as ArrayRef, + Arc::new(UInt32Array::from(std::mem::take(&mut self.earfcn))) as ArrayRef, + Arc::new(Float64Array::from(std::mem::take(&mut self.rsrp))) as ArrayRef, + Arc::new(Int8Array::from(std::mem::take(&mut self.rsrq))) as ArrayRef, + { + let offsets = std::mem::replace(&mut self.neighbours_offsets, vec![0]); + Arc::new(ListArray::try_new( + self.list_value_field(6), + OffsetBuffer::new(ScalarBuffer::from(offsets)), + Arc::new(StructArray::try_new( + Fields::from(vec![ + Field::new("earfcn", DataType::UInt32, false), + Field::new("pci", DataType::UInt16, false), + Field::new("time_diff", DataType::Float64, false), + Field::new("rsrp", DataType::Float64, false), + Field::new("rsrq", DataType::Int8, false), + ]), + vec![ + Arc::new(UInt32Array::from(std::mem::take( + &mut self.neighbours_earfcn, + ))) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.neighbours_pci))) + as ArrayRef, + Arc::new(Float64Array::from(std::mem::take( + &mut self.neighbours_time_diff, + ))) as ArrayRef, + Arc::new(Float64Array::from(std::mem::take( + &mut self.neighbours_rsrp, + ))) as ArrayRef, + Arc::new(Int8Array::from(std::mem::take(&mut self.neighbours_rsrq))) + as ArrayRef, + ], + None, + )?) as ArrayRef, + None, + )?) as ArrayRef + }, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf35WifiApInfoBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + bssid_val: Vec, + channel: Vec, + rsrp: Vec, +} + +impl Tdf35WifiApInfoBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + bssid_val: Vec::with_capacity(capacity), + channel: Vec::with_capacity(capacity), + rsrp: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(35).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.bssid_val.push(cursor.read_u48::()?); + self.channel.push(cursor.read_u8()?); + self.rsrp.push(cursor.read_i8()?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(StructArray::try_new( + Fields::from(vec![Field::new("val", DataType::UInt64, false)]), + vec![Arc::new(UInt64Array::from(std::mem::take(&mut self.bssid_val))) as ArrayRef], + None, + )?) as ArrayRef, + Arc::new(UInt8Array::from(std::mem::take(&mut self.channel))) as ArrayRef, + Arc::new(Int8Array::from(std::mem::take(&mut self.rsrp))) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf36DeviceTiltBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + cosine: Vec, +} + +impl Tdf36DeviceTiltBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + cosine: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(36).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.cosine.push(cursor.read_f32::()?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(Float32Array::from(std::mem::take(&mut self.cosine))) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf37Nrf9xGnssPvtBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + lat: Vec, + lon: Vec, + height: Vec, + h_acc: Vec, + v_acc: Vec, + h_speed: Vec, + h_speed_acc: Vec, + v_speed: Vec, + v_speed_acc: Vec, + head_mot: Vec, + head_acc: Vec, + year: Vec, + month: Vec, + day: Vec, + hour: Vec, + min: Vec, + sec: Vec, + ms: Vec, + p_dop: Vec, + h_dop: Vec, + v_dop: Vec, + t_dop: Vec, + flags: Vec, + num_sv: Vec, +} + +impl Tdf37Nrf9xGnssPvtBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + lat: Vec::with_capacity(capacity), + lon: Vec::with_capacity(capacity), + height: Vec::with_capacity(capacity), + h_acc: Vec::with_capacity(capacity), + v_acc: Vec::with_capacity(capacity), + h_speed: Vec::with_capacity(capacity), + h_speed_acc: Vec::with_capacity(capacity), + v_speed: Vec::with_capacity(capacity), + v_speed_acc: Vec::with_capacity(capacity), + head_mot: Vec::with_capacity(capacity), + head_acc: Vec::with_capacity(capacity), + year: Vec::with_capacity(capacity), + month: Vec::with_capacity(capacity), + day: Vec::with_capacity(capacity), + hour: Vec::with_capacity(capacity), + min: Vec::with_capacity(capacity), + sec: Vec::with_capacity(capacity), + ms: Vec::with_capacity(capacity), + p_dop: Vec::with_capacity(capacity), + h_dop: Vec::with_capacity(capacity), + v_dop: Vec::with_capacity(capacity), + t_dop: Vec::with_capacity(capacity), + flags: Vec::with_capacity(capacity), + num_sv: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(37).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.lat + .push(cursor.read_i32::()? as f64 / 10000000.0); + self.lon + .push(cursor.read_i32::()? as f64 / 10000000.0); + self.height + .push(cursor.read_i32::()? as f64 / 1000.0); + self.h_acc + .push(cursor.read_u32::()? as f64 / 1000.0); + self.v_acc + .push(cursor.read_u32::()? as f64 / 1000.0); + self.h_speed + .push(cursor.read_i32::()? as f64 / 1000.0); + self.h_speed_acc + .push(cursor.read_u32::()? as f64 / 1000.0); + self.v_speed + .push(cursor.read_i32::()? as f64 / 1000.0); + self.v_speed_acc + .push(cursor.read_u32::()? as f64 / 1000.0); + self.head_mot + .push(cursor.read_i32::()? as f64 / 100000.0); + self.head_acc + .push(cursor.read_u32::()? as f64 / 100000.0); + self.year.push(cursor.read_u16::()?); + self.month.push(cursor.read_u8()?); + self.day.push(cursor.read_u8()?); + self.hour.push(cursor.read_u8()?); + self.min.push(cursor.read_u8()?); + self.sec.push(cursor.read_u8()?); + self.ms.push(cursor.read_u16::()?); + self.p_dop + .push(cursor.read_u16::()? as f64 / 100.0); + self.h_dop + .push(cursor.read_u16::()? as f64 / 100.0); + self.v_dop + .push(cursor.read_u16::()? as f64 / 100.0); + self.t_dop + .push(cursor.read_u16::()? as f64 / 100.0); + self.flags.push(cursor.read_u8()?); + self.num_sv.push(cursor.read_u8()?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(Float64Array::from(std::mem::take(&mut self.lat))) as ArrayRef, + Arc::new(Float64Array::from(std::mem::take(&mut self.lon))) as ArrayRef, + Arc::new(Float64Array::from(std::mem::take(&mut self.height))) as ArrayRef, + Arc::new(Float64Array::from(std::mem::take(&mut self.h_acc))) as ArrayRef, + Arc::new(Float64Array::from(std::mem::take(&mut self.v_acc))) as ArrayRef, + Arc::new(Float64Array::from(std::mem::take(&mut self.h_speed))) as ArrayRef, + Arc::new(Float64Array::from(std::mem::take(&mut self.h_speed_acc))) as ArrayRef, + Arc::new(Float64Array::from(std::mem::take(&mut self.v_speed))) as ArrayRef, + Arc::new(Float64Array::from(std::mem::take(&mut self.v_speed_acc))) as ArrayRef, + Arc::new(Float64Array::from(std::mem::take(&mut self.head_mot))) as ArrayRef, + Arc::new(Float64Array::from(std::mem::take(&mut self.head_acc))) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.year))) as ArrayRef, + Arc::new(UInt8Array::from(std::mem::take(&mut self.month))) as ArrayRef, + Arc::new(UInt8Array::from(std::mem::take(&mut self.day))) as ArrayRef, + Arc::new(UInt8Array::from(std::mem::take(&mut self.hour))) as ArrayRef, + Arc::new(UInt8Array::from(std::mem::take(&mut self.min))) as ArrayRef, + Arc::new(UInt8Array::from(std::mem::take(&mut self.sec))) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.ms))) as ArrayRef, + Arc::new(Float64Array::from(std::mem::take(&mut self.p_dop))) as ArrayRef, + Arc::new(Float64Array::from(std::mem::take(&mut self.h_dop))) as ArrayRef, + Arc::new(Float64Array::from(std::mem::take(&mut self.v_dop))) as ArrayRef, + Arc::new(Float64Array::from(std::mem::take(&mut self.t_dop))) as ArrayRef, + Arc::new(UInt8Array::from(std::mem::take(&mut self.flags))) as ArrayRef, + Arc::new(UInt8Array::from(std::mem::take(&mut self.num_sv))) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf38BatteryChargeAccumulatedBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + charge: Vec, +} + +impl Tdf38BatteryChargeAccumulatedBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + charge: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(38).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.charge.push(cursor.read_i32::()?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(Int32Array::from(std::mem::take(&mut self.charge))) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf39InfuseBluetoothRssiBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + infuse_id: Vec, + rssi: Vec, +} + +impl Tdf39InfuseBluetoothRssiBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + infuse_id: Vec::with_capacity(capacity), + rssi: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(39).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.infuse_id.push(cursor.read_u64::()?); + self.rssi.push(cursor.read_i8()?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(UInt64Array::from(std::mem::take(&mut self.infuse_id))) as ArrayRef, + Arc::new(Int8Array::from(std::mem::take(&mut self.rssi))) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf40AdcRaw8Builder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + val: Vec, +} + +impl Tdf40AdcRaw8Builder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + val: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(40).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.val.push(cursor.read_i8()?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(Int8Array::from(std::mem::take(&mut self.val))) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf41AdcRaw16Builder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + val: Vec, +} + +impl Tdf41AdcRaw16Builder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + val: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(41).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.val.push(cursor.read_i16::()?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(Int16Array::from(std::mem::take(&mut self.val))) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf42AdcRaw32Builder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + val: Vec, +} + +impl Tdf42AdcRaw32Builder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + val: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(42).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.val.push(cursor.read_i32::()?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(Int32Array::from(std::mem::take(&mut self.val))) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf43AnnotationBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + timestamp: Vec, + event: Vec, +} + +impl Tdf43AnnotationBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + timestamp: Vec::with_capacity(capacity), + event: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(43).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.timestamp.push(cursor.read_u32::()?); + self.event.push(tdf_field_read_string_to_string( + cursor, + cursor_start, + 0, + size, + )?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(UInt32Array::from(std::mem::take(&mut self.timestamp))) as ArrayRef, + Arc::new(StringArray::from_iter_values(std::mem::take( + &mut self.event, + ))) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf44LoraRxBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + snr: Vec, + rssi: Vec, + payload: Vec>, +} + +impl Tdf44LoraRxBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + snr: Vec::with_capacity(capacity), + rssi: Vec::with_capacity(capacity), + payload: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(44).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.snr.push(cursor.read_i8()?); + self.rssi.push(cursor.read_i16::()?); + self.payload.push(crate::decoders::tdf_field_read_vla( + cursor, + cursor_start, + size, + )?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(Int8Array::from(std::mem::take(&mut self.snr))) as ArrayRef, + Arc::new(Int16Array::from(std::mem::take(&mut self.rssi))) as ArrayRef, + Arc::new(BinaryArray::from_iter_values(std::mem::take( + &mut self.payload, + ))) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf45LoraTxBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + payload: Vec>, +} + +impl Tdf45LoraTxBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + payload: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(45).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.payload.push(crate::decoders::tdf_field_read_vla( + cursor, + cursor_start, + size, + )?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(BinaryArray::from_iter_values(std::mem::take( + &mut self.payload, + ))) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf46IdxArrayFreqBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + tdf_id: Vec, + frequency: Vec, +} + +impl Tdf46IdxArrayFreqBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + tdf_id: Vec::with_capacity(capacity), + frequency: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(46).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.tdf_id.push(cursor.read_u16::()?); + self.frequency.push(cursor.read_u32::()?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.tdf_id))) as ArrayRef, + Arc::new(UInt32Array::from(std::mem::take(&mut self.frequency))) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf47IdxArrayPeriodBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + tdf_id: Vec, + period: Vec, +} + +impl Tdf47IdxArrayPeriodBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + tdf_id: Vec::with_capacity(capacity), + period: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(47).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.tdf_id.push(cursor.read_u16::()?); + self.period.push(cursor.read_u32::()?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.tdf_id))) as ArrayRef, + Arc::new(UInt32Array::from(std::mem::take(&mut self.period))) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf48WifiConnectedBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + network_bssid: Vec, + network_band: Vec, + network_channel: Vec, + network_iface_mode: Vec, + network_link_mode: Vec, + network_security: Vec, + network_rssi: Vec, + network_beacon_interval: Vec, + network_twt_capable: Vec, +} + +impl Tdf48WifiConnectedBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + network_bssid: Vec::with_capacity(capacity), + network_band: Vec::with_capacity(capacity), + network_channel: Vec::with_capacity(capacity), + network_iface_mode: Vec::with_capacity(capacity), + network_link_mode: Vec::with_capacity(capacity), + network_security: Vec::with_capacity(capacity), + network_rssi: Vec::with_capacity(capacity), + network_beacon_interval: Vec::with_capacity(capacity), + network_twt_capable: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(48).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.network_bssid.push(cursor.read_u48::()?); + self.network_band.push(cursor.read_u8()?); + self.network_channel.push(cursor.read_u8()?); + self.network_iface_mode.push(cursor.read_u8()?); + self.network_link_mode.push(cursor.read_u8()?); + self.network_security.push(cursor.read_u8()?); + self.network_rssi.push(cursor.read_i8()?); + self.network_beacon_interval + .push(cursor.read_u16::()?); + self.network_twt_capable.push(cursor.read_u8()?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(StructArray::try_new( + Fields::from(vec![ + Field::new("bssid", DataType::UInt64, false), + Field::new("band", DataType::UInt8, false), + Field::new("channel", DataType::UInt8, false), + Field::new("iface_mode", DataType::UInt8, false), + Field::new("link_mode", DataType::UInt8, false), + Field::new("security", DataType::UInt8, false), + Field::new("rssi", DataType::Int8, false), + Field::new("beacon_interval", DataType::UInt16, false), + Field::new("twt_capable", DataType::UInt8, false), + ]), + vec![ + Arc::new(UInt64Array::from(std::mem::take(&mut self.network_bssid))) + as ArrayRef, + Arc::new(UInt8Array::from(std::mem::take(&mut self.network_band))) as ArrayRef, + Arc::new(UInt8Array::from(std::mem::take(&mut self.network_channel))) + as ArrayRef, + Arc::new(UInt8Array::from(std::mem::take( + &mut self.network_iface_mode, + ))) as ArrayRef, + Arc::new(UInt8Array::from(std::mem::take( + &mut self.network_link_mode, + ))) as ArrayRef, + Arc::new(UInt8Array::from(std::mem::take(&mut self.network_security))) + as ArrayRef, + Arc::new(Int8Array::from(std::mem::take(&mut self.network_rssi))) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take( + &mut self.network_beacon_interval, + ))) as ArrayRef, + Arc::new(UInt8Array::from(std::mem::take( + &mut self.network_twt_capable, + ))) as ArrayRef, + ], + None, + )?) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf49WifiConnectionFailedBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + reason: Vec, +} + +impl Tdf49WifiConnectionFailedBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + reason: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(49).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.reason.push(cursor.read_u8()?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(UInt8Array::from(std::mem::take(&mut self.reason))) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf50WifiDisconnectedBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + reason: Vec, +} + +impl Tdf50WifiDisconnectedBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + reason: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(50).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.reason.push(cursor.read_u8()?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(UInt8Array::from(std::mem::take(&mut self.reason))) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf51NetworkScanCountBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + num_wifi: Vec, + num_lte: Vec, +} + +impl Tdf51NetworkScanCountBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + num_wifi: Vec::with_capacity(capacity), + num_lte: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(51).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.num_wifi.push(cursor.read_u8()?); + self.num_lte.push(cursor.read_u8()?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(UInt8Array::from(std::mem::take(&mut self.num_wifi))) as ArrayRef, + Arc::new(UInt8Array::from(std::mem::take(&mut self.num_lte))) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf52ExceptionStackFrameBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + frame_offsets: Vec, + frame: Vec, +} + +impl Tdf52ExceptionStackFrameBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + frame_offsets: vec![0], + frame: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(52).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + fn list_value_field(&self, field_index: usize) -> Arc { + let schema = self.schema(); + match schema.field(field_index).data_type() { + DataType::List(field) => field.clone(), + _ => unreachable!("generated list field index is not a list"), + } + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + { + let bytes_remaining = crate::decoders::vla_bytes_remaining(cursor, cursor_start, size)?; + if bytes_remaining % 4 != 0 { + return Err(Error::new( + ErrorKind::InvalidData, + "Variable-length array does not align to element size", + )); + } + let item_count = bytes_remaining / 4; + for _ in 0..item_count { + self.frame.push(cursor.read_u32::()?); + } + self.frame_offsets + .push(*self.frame_offsets.last().unwrap() + item_count as i32); + } + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + { + let offsets = std::mem::replace(&mut self.frame_offsets, vec![0]); + Arc::new(ListArray::try_new( + self.list_value_field(2), + OffsetBuffer::new(ScalarBuffer::from(offsets)), + Arc::new(UInt32Array::from(std::mem::take(&mut self.frame))) as ArrayRef, + None, + )?) as ArrayRef + }, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf53BatteryVoltageBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + voltage: Vec, +} + +impl Tdf53BatteryVoltageBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + voltage: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(53).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.voltage.push(cursor.read_u16::()?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.voltage))) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf54BatterySocBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + soc: Vec, +} + +impl Tdf54BatterySocBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + soc: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(54).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.soc.push(cursor.read_u8()?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(UInt8Array::from(std::mem::take(&mut self.soc))) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf55StateEventSetBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + state: Vec, +} + +impl Tdf55StateEventSetBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + state: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(55).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.state.push(cursor.read_u8()?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(UInt8Array::from(std::mem::take(&mut self.state))) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf56StateEventClearedBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + state: Vec, +} + +impl Tdf56StateEventClearedBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + state: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(56).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.state.push(cursor.read_u8()?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(UInt8Array::from(std::mem::take(&mut self.state))) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf57StateDurationBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + state: Vec, + duration: Vec, +} + +impl Tdf57StateDurationBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + state: Vec::with_capacity(capacity), + duration: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(57).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.state.push(cursor.read_u8()?); + self.duration.push(cursor.read_u32::()?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(UInt8Array::from(std::mem::take(&mut self.state))) as ArrayRef, + Arc::new(UInt32Array::from(std::mem::take(&mut self.duration))) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf58Pcm16bitChanLeftBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + val: Vec, +} + +impl Tdf58Pcm16bitChanLeftBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + val: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(58).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.val.push(cursor.read_i16::()?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(Int16Array::from(std::mem::take(&mut self.val))) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf59Pcm16bitChanRightBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + val: Vec, +} + +impl Tdf59Pcm16bitChanRightBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + val: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(59).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.val.push(cursor.read_i16::()?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(Int16Array::from(std::mem::take(&mut self.val))) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf60Pcm16bitChanDualBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + left: Vec, + right: Vec, +} + +impl Tdf60Pcm16bitChanDualBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + left: Vec::with_capacity(capacity), + right: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(60).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.left.push(cursor.read_i16::()?); + self.right.push(cursor.read_i16::()?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(Int16Array::from(std::mem::take(&mut self.left))) as ArrayRef, + Arc::new(Int16Array::from(std::mem::take(&mut self.right))) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} + +pub struct Tdf61KvsValueChangedBuilder { + row_timestamp: Vec>, + row_sample_idx: Vec>, + key: Vec, + value: Vec>, +} + +impl Tdf61KvsValueChangedBuilder { + pub fn new(capacity: usize) -> Self { + Self { + row_timestamp: Vec::with_capacity(capacity), + row_sample_idx: Vec::with_capacity(capacity), + key: Vec::with_capacity(capacity), + value: Vec::with_capacity(capacity), + } + } + + pub fn schema(&self) -> SchemaRef { + tdf_parquet_schema(61).unwrap() + } + + pub fn rows(&self) -> usize { + self.row_timestamp.len() + } + + pub fn append( + &mut self, + meta: TdfParquetRowMeta, + size: u8, + cursor: &mut Cursor<&[u8]>, + ) -> Result<()> { + let cursor_start = cursor.position(); + + self.row_timestamp.push(meta.time_unix_micros); + self.row_sample_idx.push(meta.sample_idx); + self.key.push(cursor.read_u16::()?); + self.value.push(crate::decoders::tdf_field_read_vla( + cursor, + cursor_start, + size, + )?); + + finish_tdf_read(cursor, cursor_start, size) + } + + pub fn finish_batch(&mut self) -> std::result::Result { + let schema = self.schema(); + let columns = vec![ + Arc::new( + TimestampMicrosecondArray::from(std::mem::take(&mut self.row_timestamp)) + .with_timezone("+00:00"), + ) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.row_sample_idx))) as ArrayRef, + Arc::new(UInt16Array::from(std::mem::take(&mut self.key))) as ArrayRef, + Arc::new(BinaryArray::from_iter_values(std::mem::take( + &mut self.value, + ))) as ArrayRef, + ]; + + RecordBatch::try_new(schema, columns) + } +} diff --git a/tdf/src/lib.rs b/tdf/src/lib.rs index ab1bbff..f78f7bd 100644 --- a/tdf/src/lib.rs +++ b/tdf/src/lib.rs @@ -1,7 +1,10 @@ use bytemuck; use byteorder::{LittleEndian, ReadBytesExt}; use num::{cast::AsPrimitive, traits::WrappingAdd}; -use std::io::{Cursor, ErrorKind, Read}; +use std::{ + io::{Cursor, ErrorKind, Read}, + path::PathBuf, +}; pub mod decoders; pub mod decoders_csv; @@ -45,6 +48,8 @@ pub trait TdfOutput { fn iter_written(&self) -> impl Iterator, u16), &usize)>; /// Get the number of times a specific TDF was written fn written(&self, remote_id: Option, tdf_id: u16) -> usize; + /// Get the path for the file associated with a specific TDF + fn output_path(self: &Self, remote_id: Option, tdf_id: u16) -> Option; } fn diff_data_reconstruct< diff --git a/tdf/src/time.rs b/tdf/src/time.rs index 7fbfc48..cb4dde7 100644 --- a/tdf/src/time.rs +++ b/tdf/src/time.rs @@ -11,6 +11,11 @@ pub fn tdf_time_to_unix(tdf_time: i64) -> (i64, u32) { (unix_seconds, unix_nano as u32) } +pub fn tdf_time_to_unix_micros(tdf_time: i64) -> i64 { + let (unix_seconds, unix_nano) = tdf_time_to_unix(tdf_time); + (unix_seconds * 1_000_000) + (unix_nano as i64 / 1_000) +} + pub fn tdf_time_to_datetime(tdf_time: i64) -> Option> { let (unix_seconds, unix_nano) = tdf_time_to_unix(tdf_time); DateTime::from_timestamp(unix_seconds as i64, unix_nano as u32) From 21b40ebc59beb3cf9b8d16f88996411077ea9ddd Mon Sep 17 00:00:00 2001 From: Jordan Yates Date: Mon, 15 Jun 2026 12:12:55 +1000 Subject: [PATCH 4/5] README.md: Update docs for new options and GUI Document the new options in the GUI. Signed-off-by: Jordan Yates --- README.md | 34 ++++++++++++++++++++++++++----- assets/configuration_options.png | Bin 71538 -> 144632 bytes 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 83f2d60..8d5c45c 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,8 @@ The application provides options to control the input selection and output gener ### 1) Output Folder -This field controls where the output data will be placed after decoding. It defaults to $USER_HOME/infuse_iot. The output folder can be updated by clicking `Folder` (1a). +This field controls where the output data will be placed after decoding. It defaults to $USER_HOME/infuse_iot. The output folder can be updated by clicking `Folder` (1a). The output folder can be opened +in the default system viewer by clicking `Open` (1b). ### 2) Input folder/file @@ -32,13 +33,36 @@ If data from multiple Tauro collars exists on a single SD card, this option will This field controls the prefix of the output filenames, created in the output folder. This value will default to the Device ID. -### 5) Time Output Format +### 5) Output Format + +Save the output as either Comma Separated Value (CSV) or [Apache Parquet](https://parquet.apache.org/) files. +Decoding to Parquet is faster and the resulting files are smaller, but the results are not human readable. + +### 6) Linearize Output + +To optimize processing times, the decoding work is split across multiple CPU cores to intermediate files, with a +post-processing step pulling the intermediate results back into a single file. +If a single output file is not required, disabling this skips the merging step, saving decoding time. +If disabled, the final files have a numeric postfix (e.g. `test_BATTERY_STATE_00000.csv`) which indicates the order from the +original binary file. + +### 7) Maximum Readings per File + +If the Linearize Output step is enabled, the output data can be split into multiple files based on the number of rows in each file. +This can be useful to limit individual files sizes or optimize data loading. The default value of 0 means no limit. + +### 8) Time Output Format This option controls the output format of the timestamps written into the output CSV files. The two options are a [RFC3339](https://www.rfc-editor.org/rfc/rfc3339) formatted string (for example 2024-06-27T13:55:12.123456Z), or a Unix timestamp with subseconds (for example 1731457165.123456). The RFC3339 option is recommended if the CSV outputs will be looked at by users, as it is a more human-readable format. By comparison, the Unix timestamps are simpler for data processing scripts to parse, and are faster for the decoder tool to generate. -### 6) Decode +### 9) Input Block Size + +Specifies the data block size of the input binary data. The default value of `512` should be used unless instructions to the +contrary are provided. + +### 10) Decode Once an input file or folder has been selected, the decode button becomes available to select. Clicking this button begins the decode process with the currently selected options. The button is unavailable to select again until the previous decode has completed. ## Decoding Process @@ -49,11 +73,11 @@ The data on the SD card is copied onto the local filesystem to create the merged ### 2) Decoding files -The binary data format is decoded into human-readable CSV files. This process is run across all CPU cores to maximize performance. +The binary data format is decoded into human-readable CSV or Apache Parquet files. This process is run across all CPU cores to maximize performance. ### 3) Merging output -The individual CSV files created by each thread in the decoding step are merged back together into a single file per sensor stream. +The individual files created by each thread in the decoding step are merged back together into a single file per sensor stream, unless output linearization is disabled. ## Output Statistics diff --git a/assets/configuration_options.png b/assets/configuration_options.png index 086e4e74a491c7cbd5495519a298dd462d768b1d..0ae3789da1c39bd2e86bc7e0a12d3f99ebede67f 100644 GIT binary patch literal 144632 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

_A~r~H!WjBsQS)ZZlb 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

    N*mm21;{RvJzoZQMglQ5nXkT=p3YE z`dX%?ch5tXPz14%(Alk~RuR!WSkNylNPPE?+TZ?FZuql9SO7>TL~1GanDHO{0U}q6 zvzBd1w*u|XL`XGl?fGVbA*OPYs?1&;1bh=4+q5p{Me8gI=_*^dvX-`4(w8|kOSb*3Ere@MAFe8@G; zcwe0E>y|eu37B$(3`I3@2_%6D^~}>GpY`vxa)x9uv<&7J%1-b#YP5R024*?6m2F#p zlHwvQELUo9%FZz>BNT$A{aaeMac?uxRXjF-SI1BP#NlIXiep}w(2xX<44XD~*L1AQ z!PSj+b4JNBTIZ$WY%O!kOrw+T|J#CDj99|BrXQ_Mp~p=hw^iM-k$r8i){YSET-bhT z?FIT06^;=T-;|@qcns4CD%U+`oV{+Gty9gP``1lw>L2Mg&M7tG0{>EL|LAGZmR$Gm zS@XzP0x!csH==F z9J#7Ay(ysK5}&i@*247EH#M1(sDP(&fI6>hLV@pc(wi?Zr{>QUfuzrg3{ONEVrcN>$=jDtw)Gnm)7L#M zcc0{yl$~e!*NgnPFY2AFY(tzrewnge5^QeWIhe+nUWH2gP)(#{`_-d5XCKJ$sndq3 zCA^}p;PB7M$*FLwEX%izO3WoyxZQ`9#Xg4FZ@z9}>OPmRR$)B}|5#)sYX-bYdj776 zAi3>LUHz{+F;+&ZF6C{`$ftf7-FADk>qm|pX@20b!rnInWcwQEs#5?EYhjH!RH|kl(EQfYHo^EzRcg~eSKx%n}#LkwGv7* zkSt0i&ii-1n{A(;kWQ%XFXm_T;ELmPAHlrqvbcxE>Dm1qa!PM1+y39$NbCNcebJWY zy;rA0ww82?Ftbl{&Ern5|vj92XZfA)lPegFUf literal 71538 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 Wq#C Date: Mon, 15 Jun 2026 14:00:25 +1000 Subject: [PATCH 5/5] Cargo.toml: `v1.9.0` - New output format [Apache Parquet](https://parquet.apache.org/) - Option to limit output files to a certain number of readings * If the limit is hit, output files have a numeric postfix - Option to skip merge step to optimize decoding times Signed-off-by: Jordan Yates --- CHANGELOG.md | 7 +++++++ Cargo.toml | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e148b35..e50fc5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org). +## [1.9.0] - 2026-06-16 + + - New output format [Apache Parquet](https://parquet.apache.org/) + - Option to limit output files to a certain number of readings + * If the limit is hit, output files have a numeric postfix + - Option to skip merge step to optimize decoding times + ## [1.8.1] - 2026-06-05 - MacOS GUI distributed as notorized `.dmg` diff --git a/Cargo.toml b/Cargo.toml index 6a35194..b044fac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "infuse_decoder" -version = "1.8.1" +version = "1.9.0" edition = "2024" [[bin]]