Skip to content
9 changes: 4 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,14 @@

## Architecture & Code Organization
- **Library-first design:** Core logic in `src/lib.rs` and CLI entry point in `src/main.rs`.
- **Shared code:** Parser modules (`src/parser/`) are used by both library and CLI (`parse_frames`, `parse_headers_from_text`).
- **Duplicated code:** Export implementations exist separately in `src/export.rs` (library) and `src/main.rs` (CLI). CLI does NOT use library export functions.
- **Current state:** **Partial unification** — parsing shared, export duplicated (~1800 lines in `src/main.rs`).
- **Shared code:** Parser modules (`src/parser/`) and export functions (`src/export.rs`) are shared by both library and CLI.
- **CLI as thin wrapper:** The CLI (`src/main.rs`) uses library export functions (`export_to_csv`, `export_to_gpx`, `export_to_event`) with CLI-specific status messages.
- **Current state:** **Full unification complete** — parsing and export layers unified, CLI reduced from ~1800 to ~1400 lines.
- **Decision criteria:** "Is this needed by crate consumers?" determines placement — shared logic in library, CLI-only logic in `src/main.rs`.
- **Feature flags:** `csv`, `cli`, `json`, `serde` control optional dependencies; default: `csv` + `cli`.
- **CRATE_USAGE.md reference:** See `CRATE_USAGE.md` for library API examples with feature flags.
- **Code quality goals:** Reduce duplication by migrating CLI export logic to use library `export_to_csv()`, `export_to_gpx()`, `export_to_event()` functions.
- **Testing:** Comprehensive tests distributed across `src/main.rs`, `src/conversion.rs`, `src/parser/stream.rs`, and `src/parser/helpers.rs`.
- **Public API:** `parse_bbl_file()`, `parse_bbl_bytes()`, `BBLLog`, `ExportOptions`, conversion utilities, parser helpers.
- **Public API:** `parse_bbl_file()`, `parse_bbl_bytes()`, `BBLLog`, `ExportOptions`, `export_to_csv()`, `export_to_gpx()`, `export_to_event()`, conversion utilities, parser helpers.

## Algorithms
- **Method Selection:**
Expand Down
1 change: 1 addition & 0 deletions examples/export_demo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ fn main() -> Result<()> {
&log.gps_coordinates,
&log.home_coordinates,
&export_opts,
log.header.log_start_datetime.as_deref(),
)?;
println!("✓ GPX export complete");
} else {
Expand Down
1 change: 1 addition & 0 deletions examples/gpx_export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ fn main() -> anyhow::Result<()> {
&log.gps_coordinates,
&log.home_coordinates,
&export_opts,
log.header.log_start_datetime.as_deref(),
)?;
println!("✓ GPX export complete");
println!(" Exported {} GPS coordinates", log.gps_coordinates.len());
Expand Down
1 change: 1 addition & 0 deletions examples/multi_export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ fn main() -> anyhow::Result<()> {
&log.gps_coordinates,
&log.home_coordinates,
&export_opts,
log.header.log_start_datetime.as_deref(),
)?;
println!(
"✓ GPX export complete ({} coordinates)",
Expand Down
102 changes: 80 additions & 22 deletions src/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,63 @@ pub struct ExportOptions {
pub force_export: bool,
}

/// Extract the base filename from an input path with consistent fallback.
/// Used by all export functions and path computation helpers to ensure
/// consistent naming across CSV, GPX, and event exports.
///
/// Always returns "blackbox" as fallback for missing or non-UTF-8 filenames,
/// ensuring compute_export_paths() predictions match actual export filenames.
fn extract_base_name(input_path: &Path) -> &str {
input_path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("blackbox")
}

/// Helper to compute export file paths with consistent naming across all export types.
/// Ensures CLI status messages match actual filenames written by export functions.
///
/// # Arguments
/// * `input_path` - Path to the input BBL file (used to extract base filename)
/// * `export_options` - Export configuration with optional output directory
/// * `log_number` - 1-based log number (for .NN suffix when multiple logs)
/// * `total_logs` - Total number of logs in the file
///
/// # Returns
/// Tuple of (csv_path, headers_path, gpx_path, event_path) using consistent naming
pub fn compute_export_paths(
input_path: &Path,
export_options: &ExportOptions,
log_number: usize,
total_logs: usize,
) -> (
std::path::PathBuf,
std::path::PathBuf,
std::path::PathBuf,
std::path::PathBuf,
) {
let base_name = extract_base_name(input_path);

let output_dir = if let Some(ref dir) = export_options.output_dir {
std::path::Path::new(dir)
} else {
input_path.parent().unwrap_or(std::path::Path::new("."))
};

let log_suffix = if total_logs > 1 {
format!(".{:02}", log_number)
} else {
String::new()
};

let csv_path = output_dir.join(format!("{}{}.csv", base_name, log_suffix));
let headers_path = output_dir.join(format!("{}{}.headers.csv", base_name, log_suffix));
let gpx_path = output_dir.join(format!("{}{}.gps.gpx", base_name, log_suffix));
let event_path = output_dir.join(format!("{}{}.event", base_name, log_suffix));

(csv_path, headers_path, gpx_path, event_path)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/// Pre-computed CSV field mapping for performance
#[derive(Debug)]
struct CsvFieldMap {
Expand Down Expand Up @@ -87,10 +144,7 @@ pub fn export_to_csv(
input_path: &Path,
export_options: &ExportOptions,
) -> Result<()> {
let base_name = input_path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("blackbox");
let base_name = extract_base_name(input_path);

let output_dir = if let Some(ref dir) = export_options.output_dir {
Path::new(dir)
Expand Down Expand Up @@ -288,22 +342,34 @@ fn export_flight_data_to_csv(log: &BBLLog, output_path: &Path) -> Result<()> {
}

/// Export GPS data to GPX format
///
/// # Arguments
/// * `input_path` - Path to the input BBL file (used for output naming)
/// * `log_index` - Index of the current log (0-based)
/// * `total_logs` - Total number of logs in the file
/// * `gps_coordinates` - GPS coordinate data to export
/// * `_home_coordinates` - Home coordinates (reserved for future use)
/// * `export_options` - Export configuration options
/// * `log_start_datetime` - Optional log start datetime from header for accurate timestamps
///
/// # Performance Notes
/// For very large GPS traces, the `log_start_datetime` is parsed via `generate_gpx_timestamp()`
/// on each trackpoint. Future optimization: consider caching the parsed base epoch once per log
/// to avoid repeated parsing overhead when exporting thousands of GPS points.
pub fn export_to_gpx(
input_path: &Path,
log_index: usize,
total_logs: usize,
gps_coordinates: &[GpsCoordinate],
_home_coordinates: &[GpsHomeCoordinate],
export_options: &ExportOptions,
log_start_datetime: Option<&str>,
) -> Result<()> {
if gps_coordinates.is_empty() {
return Ok(());
}

let base_name = input_path
.file_stem()
.and_then(|n| n.to_str())
.unwrap_or("unknown");
let base_name = extract_base_name(input_path);

let output_dir = export_options
.output_dir
Expand Down Expand Up @@ -339,19 +405,14 @@ pub fn export_to_gpx(
}
}

// Convert timestamp to ISO format
let total_seconds = coord.timestamp_us / 1_000_000;
let microseconds = coord.timestamp_us % 1_000_000;

// Use March 26, 2025 as base date
let hours = 5 + (total_seconds / 3600) % 24;
let minutes = (total_seconds % 3600) / 60;
let seconds = total_seconds % 60;
// Generate GPX timestamp from log_start_datetime + frame timestamp
// Following blackbox_decode approach: dateTime + (gpsFrameTime / 1000000)
let timestamp_str = generate_gpx_timestamp(log_start_datetime, coord.timestamp_us);

writeln!(
gpx_file,
r#" <trkpt lat="{:.7}" lon="{:.7}"><ele>{:.2}</ele><time>2025-03-26T{:02}:{:02}:{:02}.{:06}Z</time></trkpt>"#,
coord.latitude, coord.longitude, coord.altitude, hours, minutes, seconds, microseconds
r#" <trkpt lat="{:.7}" lon="{:.7}"><ele>{:.2}</ele><time>{}</time></trkpt>"#,
coord.latitude, coord.longitude, coord.altitude, timestamp_str
)?;
}

Expand All @@ -373,10 +434,7 @@ pub fn export_to_event(
return Ok(());
}

let base_name = input_path
.file_stem()
.and_then(|n| n.to_str())
.unwrap_or("unknown");
let base_name = extract_base_name(input_path);

let output_dir = export_options
.output_dir
Expand Down
Loading