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
2 changes: 2 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[alias]
doc-screenshot = "run --bin infuse_decoder -- --docs-screenshot"
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.

This project adheres to [Semantic Versioning](https://semver.org).

## [1.11.0] - 2026-06-xx

- Output file list is now scrollable
- Folder search improvements
- In addition to the SD card file naming convention, folders with one file per device are now supported
- Each file must have the 16 character hex ID somewhere in the filename
- Fix decoding crash when no files are found

## [1.10.0] - 2026-06-25

- Nested TDF definitions inherit the parent field name in the CSV header column
Expand Down
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,8 @@ 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"] }
27 changes: 18 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,21 @@ 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). The output folder can be opened
in the default system viewer by clicking `Open` (1b).
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 the `Folder` button.
The output folder can be opened in the default system viewer by clicking the `Open` button.

### 2) Input folder/file

This field specifies the source of the input data. This can be either a SD card filesystem (2a) or a previously output binary file (2b).
This field specifies the source of the input data.
This can be either a single binary file by clicking the `File` button, or a folder containing many files with the `Folder` button.
If selecting a folder, there are two expected forms:

When an SD card is inserted into the computer, it will appear as a removable disk named INFUSE. This is the folder to select from button 2a.
a) An SD card filesystem, with files of the form `infuse_{device_id}_{subfile_num}.bin`

b) A folder containing one binary file per device, where each file has the `{device_id}` somewhere in the name.

When an SD card is inserted into the computer, it will appear as a removable disk named INFUSE. This is the folder to select from the `Folder` button.

### 3) Device ID

Expand All @@ -38,31 +45,33 @@ This field controls the prefix of the output filenames, created in the output fo
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
### 6) Output Format Options

#### 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
#### 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
### 7) 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.

### 9) Input Block Size
### 8) 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
### 9) 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
Expand Down
3 changes: 3 additions & 0 deletions assets/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Configuration Options

The configuration options screenshot is autogenerated with `cargo doc-screenshot`.
Binary file modified assets/configuration_options.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
use chrono::{Datelike, Utc};

fn main() {
println!(
"cargo:rustc-env=INFUSE_DECODER_BUILD_YEAR={}",
Utc::now().year()
);
}
100 changes: 100 additions & 0 deletions src/fs_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,105 @@ pub fn find_infuse_iot_files(dir: &PathBuf) -> io::Result<HashMap<u64, Vec<PathB
}
}

if !matching_files.is_empty() {
return Ok(matching_files);
}

// Fallback for files that include a standalone 16-character hex ID and end in ".bin".
let fallback_pattern =
Regex::new(r"(?:^|[^0-9a-fA-F])([0-9a-fA-F]{16})(?:[^0-9a-fA-F].*)?\.bin$").unwrap();

for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_file() {
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
if let Some(captures) = fallback_pattern.captures(file_name) {
let device_id = u64::from_str_radix(&captures[1], 16).unwrap();

if let Some(existing_paths) = matching_files.get(&device_id) {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"Multiple fallback files found for Infuse-IoT device ID {device_id:016x}: {:?} and {:?}",
existing_paths[0], path
),
));
}

matching_files.insert(device_id, vec![path]);
}
}
}
}

Ok(matching_files)
}

#[cfg(test)]
mod tests {
use super::*;
use std::{
fs::File,
time::{SystemTime, UNIX_EPOCH},
};

fn temp_dir(name: &str) -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let dir = std::env::temp_dir().join(format!("infuse_decoder_{name}_{nanos}"));
fs::create_dir(&dir).unwrap();
dir
}

fn touch(dir: &PathBuf, file_name: &str) {
File::create(dir.join(file_name)).unwrap();
}

#[test]
fn finds_current_infuse_iot_pattern() {
let dir = temp_dir("current_pattern");
touch(&dir, "infuse_0123456789abcdef_0.bin");
touch(&dir, "infuse_0123456789abcdef_1.bin");
touch(&dir, "capture_fedcba9876543210.bin");

let files = find_infuse_iot_files(&dir).unwrap();

assert_eq!(files.len(), 1);
assert_eq!(files.get(&0x0123_4567_89ab_cdef).unwrap().len(), 2);

fs::remove_dir_all(dir).unwrap();
}

#[test]
fn falls_back_to_standalone_hex_id_bin_files() {
let dir = temp_dir("fallback");
touch(&dir, "capture_0123456789abcdef.bin");
touch(&dir, "fedcba9876543210.bin");
touch(&dir, "ignored_00123456789abcdef.bin");

let files = find_infuse_iot_files(&dir).unwrap();

assert_eq!(files.len(), 2);
assert!(files.contains_key(&0x0123_4567_89ab_cdef));
assert!(files.contains_key(&0xfedc_ba98_7654_3210));

fs::remove_dir_all(dir).unwrap();
}

#[test]
fn fallback_errors_on_duplicate_device_ids() {
let dir = temp_dir("fallback_duplicate");
touch(&dir, "capture_0123456789abcdef.bin");
touch(&dir, "backup_0123456789abcdef.bin");

let err = find_infuse_iot_files(&dir).unwrap_err();

assert_eq!(err.kind(), io::ErrorKind::InvalidData);
assert!(err.to_string().contains("0123456789abcdef"));

fs::remove_dir_all(dir).unwrap();
}
}
Loading
Loading