Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.12.0] - xx-xx-xx

- Tighten Parquet output types for field conversion
- Fix decoding minimally sized last TDF
- Zero length index arrays are now rejected by the decoder
- Relative timestamps without a preceding absolute timestamp are now rejected

## [1.11.0] - 2026-06-30

- Output file list is now scrollable
Expand Down
3 changes: 0 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,5 @@ egui_extras = "0.34.1"
directories = "6.0.0"
image = "0.25.9"

[build-dependencies]
chrono = "0.4.42"

[target.'cfg(windows)'.dependencies]
winapi = { version = "*", features = ["winbase"] }
8 changes: 0 additions & 8 deletions build.rs

This file was deleted.

20 changes: 20 additions & 0 deletions scripts/tdf.json
Original file line number Diff line number Diff line change
Expand Up @@ -1959,6 +1959,26 @@
"description": "New data value, empty for delete, '*' for write-only"
}
]
},
"62": {
"name": "AMBIENT_PRESSURE",
"description": "Ambient pressure",
"fields": [
{
"name": "pressure",
"type": "uint32_t",
"description": "Atmospheric pressure (pascals)",
"display": {
"fmt": "float",
"digits": 3,
"postfix": "kPA"
},
"conversion": {
"m": 0.001,
"c": 0
}
}
]
}
}
}
120 changes: 116 additions & 4 deletions scripts/tdf_decoder_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import os
import pathlib
import re
import subprocess
from numpy import format_float_positional as float_format

from jinja2 import Environment, FileSystemLoader, select_autoescape
Expand Down Expand Up @@ -43,6 +44,39 @@
"float64_t": "DataType::Float64",
}

integer_range = {
"int8_t": (-(2**7), 2**7 - 1),
"uint8_t": (0, 2**8 - 1),
"int16_t": (-(2**15), 2**15 - 1),
"uint16_t": (0, 2**16 - 1),
"int32_t": (-(2**31), 2**31 - 1),
"uint32_t": (0, 2**32 - 1),
"int64_t": (-(2**63), 2**63 - 1),
"uint64_t": (0, 2**64 - 1),
}

rust_int_range = {
"i8": (-(2**7), 2**7 - 1),
"u8": (0, 2**8 - 1),
"i16": (-(2**15), 2**15 - 1),
"u16": (0, 2**16 - 1),
"i32": (-(2**31), 2**31 - 1),
"u32": (0, 2**32 - 1),
"i64": (-(2**63), 2**63 - 1),
"u64": (0, 2**64 - 1),
}

arrow_rust_int_type = {
"i8": "DataType::Int8",
"u8": "DataType::UInt8",
"i16": "DataType::Int16",
"u16": "DataType::UInt16",
"i32": "DataType::Int32",
"u32": "DataType::UInt32",
"i64": "DataType::Int64",
"u64": "DataType::UInt64",
}


def rust_str(value):
return json.dumps(value)
Expand All @@ -60,6 +94,57 @@ def indent_block(value, indent):
return "\n".join(f"{pad}{line}" for line in value.splitlines())


def integer_type_for_range(min_val, max_val):
if min_val < 0:
candidates = ("i8", "i16", "i32", "i64")
else:
candidates = ("u8", "u16", "u32", "u64")

for type_name in candidates:
type_min, type_max = rust_int_range[type_name]
if type_min <= min_val and max_val <= type_max:
return type_name

return None


def decimal_value(value):
if isinstance(value, decimal.Decimal):
return value
return decimal.Decimal(value)


def integral_decimal(value):
value = decimal_value(value)
if value == value.to_integral_value():
return int(value)
return None


def raw_integer_range(field):
conv = field.get("conversion", {})
if "int" in conv:
byte_len = field["num"]
return (0, 2 ** (byte_len * 8) - 1)
return integer_range.get(field["type"])


def integer_type_after_conversion(field):
value_range = raw_integer_range(field)
if value_range is None:
return None

conv = field.get("conversion", {})
m = integral_decimal(conv.get("m", 1))
c = integral_decimal(conv.get("c", 0))
if m is None or c is None:
return None

min_val, max_val = value_range
converted = (min_val * m + c, max_val * m + c)
return integer_type_for_range(min(converted), max(converted))


def arrow_scalar_type(field):
c_type = field["type"]
conv = field.get("conversion", {})
Expand All @@ -68,6 +153,9 @@ def arrow_scalar_type(field):
return "DataType::Utf8"

if "m" in conv or "c" in conv:
int_type = integer_type_after_conversion(field)
if int_type is not None:
return arrow_rust_int_type[int_type]
return "DataType::Float64"

if "int" in conv:
Expand Down Expand Up @@ -115,7 +203,7 @@ def arrow_data_type_expr(field, structs, indent):

num = field["num"]
if num == 0:
if c_type == "uint8_t":
if c_type == "uint8_t" and not conv:
return "DataType::Binary"
return (
"DataType::List(Arc::new(Field::new_list_field(\n"
Expand All @@ -124,7 +212,7 @@ def arrow_data_type_expr(field, structs, indent):
f"{' ' * indent})))"
)

if c_type == "uint8_t":
if c_type == "uint8_t" and not conv:
return f"DataType::FixedSizeList(Arc::new(Field::new_list_field(DataType::UInt8, false)), {num})"
return (
"DataType::FixedSizeList(Arc::new(Field::new_list_field(\n"
Expand Down Expand Up @@ -405,6 +493,9 @@ def csv_field_byte_size(field, repeated_item=False):
def rust_type_after_conversion(field):
conv = field.get("conversion", {})
if "m" in conv or "c" in conv:
int_type = integer_type_after_conversion(field)
if int_type is not None:
return int_type
return "f64"
if field["type"] == "char":
return "String"
Expand Down Expand Up @@ -445,9 +536,29 @@ def primitive_read_expr(field):
func = f"cursor.read_{t_name}::<{e}>()?"

if "m" in c or "c" in c:
int_type = integer_type_after_conversion(field)
if int_type is not None:
scale = integral_decimal(c.get("m", 1))
offset = integral_decimal(c.get("c", 0))

if scale == 0:
func = f"({offset} as {int_type})"
else:
func = f"({func} as {int_type})"
if scale != 1:
func += f" * {scale}"
if offset > 0:
func += f" + {offset}"
elif offset < 0:
func += f" - {-offset}"
return func

func += " as f64"
if "m" in c and c["m"] != 0:
if "m" in c and c["m"] != 1:
val = c["m"]
if val == 0:
func += " * 0.0"
return func
inverse_ratio = (1 / val).as_integer_ratio()
if inverse_ratio[1] == 1:
func += f" / {inverse_ratio[0]}.0"
Expand Down Expand Up @@ -519,7 +630,7 @@ def field_model(field, path):
"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:
if num == 0 and c_type == "uint8_t" and not conv:
return {
"kind": "binary",
"path": path,
Expand Down Expand Up @@ -768,6 +879,7 @@ def write_rendered(path, template):
)
f.write(rendered)
f.write(os.linesep)
subprocess.run(["rustfmt", path], check=True)

write_rendered(common_output, common_template)
write_rendered(csv_output, csv_template)
Expand Down
10 changes: 5 additions & 5 deletions src/main_gui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use std::sync::{Arc, Mutex};
use std::thread;
use std::{collections::HashMap, path::PathBuf};

use chrono::{Datelike, Utc};
use eframe::egui::{self, IconData};
use egui_extras::{Column, TableBuilder};
use image::GenericImageView;
Expand Down Expand Up @@ -275,7 +276,7 @@ fn draw_doc_marker(painter: &egui::Painter, marker: &DocMarker) {
painter.circle_stroke(
position,
radius,
egui::Stroke::new(1.5, egui::Color32::WHITE),
egui::Stroke::new(1.5_f32, egui::Color32::WHITE),
);
painter.text(
position,
Expand Down Expand Up @@ -692,11 +693,10 @@ fn copyright_bar(ui: &mut egui::Ui) {
.num_columns(2)
.show(ui, |ui| {
ui.with_layout(egui::Layout::right_to_left(egui::Align::LEFT), |ui| {
ui.label(concat!(
"v",
ui.label(format!(
"v{} © Embeint Inc 2024-{}",
env!("CARGO_PKG_VERSION"),
" © Embeint Inc 2024-",
env!("INFUSE_DECODER_BUILD_YEAR")
Utc::now().year()
));
});

Expand Down
1 change: 1 addition & 0 deletions tdf/src/decoders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ pub fn tdf_name(tdf_id: &u16) -> String {
59 => String::from("PCM_16BIT_CHAN_RIGHT"),
60 => String::from("PCM_16BIT_CHAN_DUAL"),
61 => String::from("KVS_VALUE_CHANGED"),
62 => String::from("AMBIENT_PRESSURE"),
_ => format!("{}", tdf_id),
}
}
Expand Down
6 changes: 6 additions & 0 deletions tdf/src/decoders_csv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ pub fn tdf_fields(tdf_id: &u16) -> Vec<&'static str> {
59 => vec!["val"],
60 => vec!["left", "right"],
61 => vec!["key", "value"],
62 => vec!["pressure"],
_ => vec!["unknown"],
}
}
Expand Down Expand Up @@ -778,6 +779,11 @@ pub fn tdf_read_into_str(tdf_id: &u16, size: u8, cursor: &mut Cursor<&[u8]>) ->
cursor.read_u16::<LittleEndian>()?,
tdf_field_read_vla_to_str(cursor, cursor_start, size)?,
)),
62 =>
Ok(format!(
"{}",
cursor.read_u32::<LittleEndian>()? as f64 / 1000.0,
)),
_ => {
let mut buf = vec![0; size as usize];
cursor.read_exact(&mut buf)?;
Expand Down
Loading
Loading