Skip to content
Merged
Changes from 2 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
180 changes: 19 additions & 161 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
use anyhow::{Context, Result};
use clap::{Arg, Command};
use glob::glob;
use semver::Version;
use std::collections::{HashMap, HashSet};
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};

// Import conversion functions from crate library to avoid code duplication
use bbl_parser::conversion::{
convert_gps_altitude, convert_gps_coordinate, convert_gps_course, convert_gps_speed,
format_failsafe_phase, format_flight_mode_flags, format_state_flags, generate_gpx_timestamp,
convert_amperage_to_amps, convert_gps_altitude, convert_gps_coordinate, convert_gps_course,
convert_gps_speed, convert_vbat_to_volts, format_failsafe_phase, format_flight_mode_flags,
format_state_flags, generate_gpx_timestamp,
};

// Import parser types from crate library
Expand All @@ -19,7 +19,10 @@ use bbl_parser::parser::{
};

// Import types from crate library
use bbl_parser::types::{EventFrame, FrameDefinition};
use bbl_parser::types::{
BBLHeader, BBLLog, DecodedFrame, EventFrame, FrameDefinition, FrameHistory, FrameStats,
GpsCoordinate, GpsHomeCoordinate,
};

/// Maximum recursion depth to prevent stack overflow
const MAX_RECURSION_DEPTH: usize = 100;
Expand Down Expand Up @@ -230,93 +233,6 @@ fn find_bbl_files_in_dir_with_depth(
Ok(bbl_files)
}

// FieldDefinition and FrameDefinition now imported from bbl_parser::types

#[derive(Debug)]
struct BBLHeader {
firmware_revision: String,
board_info: String,
craft_name: String,
data_version: u8,
looptime: u32,
/// Log start datetime from header (ISO 8601 format)
log_start_datetime: Option<String>,
i_frame_def: FrameDefinition,
p_frame_def: FrameDefinition,
s_frame_def: FrameDefinition,
g_frame_def: FrameDefinition,
h_frame_def: FrameDefinition,
sysconfig: HashMap<String, i32>,
all_headers: Vec<String>,
}

#[derive(Debug, Default)]
struct FrameStats {
i_frames: u32,
p_frames: u32,
h_frames: u32,
g_frames: u32,
e_frames: u32,
s_frames: u32,
total_frames: u32,
total_bytes: u64,
start_time_us: u64,
end_time_us: u64,
failed_frames: u32,
missing_iterations: u64,
}

#[derive(Debug, Clone)]
struct DecodedFrame {
frame_type: char,
timestamp_us: u64,
#[allow(dead_code)]
loop_iteration: u32,
data: HashMap<String, i32>,
}

// GPS related structures for GPX export
#[derive(Debug, Clone)]
struct GpsCoordinate {
latitude: f64,
longitude: f64,
altitude: f64,
timestamp_us: u64,
num_sats: Option<i32>,
#[allow(dead_code)]
speed: Option<f64>,
#[allow(dead_code)]
ground_course: Option<f64>,
}

#[derive(Debug, Clone)]
struct GpsHomeCoordinate {
#[allow(dead_code)]
home_latitude: f64,
#[allow(dead_code)]
home_longitude: f64,
#[allow(dead_code)]
timestamp_us: u64,
}

#[derive(Debug)]
struct BBLLog {
log_number: usize,
total_logs: usize,
header: BBLHeader,
stats: FrameStats,
sample_frames: Vec<DecodedFrame>, // Only store a few sample frames, not all
debug_frames: Option<HashMap<char, Vec<DecodedFrame>>>, // Frame data by type for debug output
}

// Frame history for prediction during parsing
struct FrameHistory {
current_frame: Vec<i32>,
previous_frame: Vec<i32>,
previous2_frame: Vec<i32>,
valid: bool,
}

#[allow(dead_code)]
fn should_have_frame(frame_index: u32, sysconfig: &HashMap<String, i32>) -> bool {
let frame_interval_i = sysconfig.get("frameIntervalI").copied().unwrap_or(32);
Expand Down Expand Up @@ -669,7 +585,7 @@ fn parse_bbl_file(
let log_data = &file_data[start_pos..end_pos];

// Parse this individual log
let (log, _gps_coords, _home_coords, _events) = parse_single_log(
let log = parse_single_log(
log_data,
log_index + 1,
log_positions.len(),
Expand All @@ -682,21 +598,13 @@ fn parse_bbl_file(
Ok(logs)
}

// Type alias to reduce complexity
type ParseSingleLogResult = (
BBLLog,
Vec<GpsCoordinate>,
Vec<GpsHomeCoordinate>,
Vec<EventFrame>,
);

fn parse_single_log(
log_data: &[u8],
log_number: usize,
total_logs: usize,
debug: bool,
export_options: &ExportOptions,
) -> Result<ParseSingleLogResult> {
) -> Result<BBLLog> {
// Find where headers end and binary data begins
let mut header_end = 0;
for i in 1..log_data.len() {
Expand Down Expand Up @@ -759,9 +667,12 @@ fn parse_single_log(
stats,
sample_frames: frames,
debug_frames,
gps_coordinates: gps_coords,
home_coordinates: home_coords,
event_frames: events,
};

Ok((log, gps_coords, home_coords, events))
Ok(log)
}

fn parse_headers_from_text(header_text: &str, debug: bool) -> Result<BBLHeader> {
Expand Down Expand Up @@ -2349,59 +2260,6 @@ fn parse_numeric_data(numeric_data: &str) -> Vec<u8> {
.collect()
}

/// Converts raw vbatLatest value to volts using firmware-aware scaling.
///
/// Betaflight < 4.3.0: tenths (0.1V units)
/// Betaflight >= 4.3.0: hundredths (0.01V units)
/// EmuFlight: always tenths (0.1V units)
/// iNav: always hundredths (0.01V units)
fn convert_vbat_to_volts(raw_value: i32, firmware_revision: &str) -> f32 {
// Determine scaling factor based on firmware
let scale_factor = if firmware_revision.contains("EmuFlight") {
// EmuFlight always uses tenths
0.1
} else if firmware_revision.contains("iNav") {
// iNav always uses hundredths
0.01
} else if firmware_revision.contains("Betaflight") {
// Betaflight version-dependent scaling
if let Some(version) = extract_firmware_version(firmware_revision) {
if version >= Version::new(4, 3, 0) {
0.01 // hundredths for >= 4.3.0
} else {
0.1 // tenths for < 4.3.0
}
} else {
// Default to modern Betaflight scaling if version can't be parsed
0.01
}
} else {
// Unknown firmware, default to hundredths
0.01
};

raw_value as f32 * scale_factor
}

/// Extract version from firmware revision string
fn extract_firmware_version(firmware_revision: &str) -> Option<Version> {
// Parse version from strings like "Betaflight 4.5.1 (77d01ba3b) AT32F435M"
let words: Vec<&str> = firmware_revision.split_whitespace().collect();
for (i, word) in words.iter().enumerate() {
if word.to_lowercase().contains("betaflight") && i + 1 < words.len() {
if let Ok(version) = Version::parse(words[i + 1]) {
return Some(version);
}
}
}
None
}

/// Converts raw amperageLatest value to amps (0.01A units)
fn convert_amperage_to_amps(raw_value: i32) -> f32 {
raw_value as f32 / 100.0
}

fn parse_bbl_file_streaming(
file_path: &Path,
debug: bool,
Expand Down Expand Up @@ -2460,7 +2318,7 @@ fn parse_bbl_file_streaming(
let log_data = &file_data[start_pos..end_pos];

// Parse this individual log
let (log, gps_coords, home_coords, events) = parse_single_log(
let log = parse_single_log(
log_data,
log_index + 1,
log_positions.len(),
Expand Down Expand Up @@ -2499,13 +2357,13 @@ fn parse_bbl_file_streaming(
}

// Export GPS data to GPX if requested
if export_options.gpx && !gps_coords.is_empty() {
if export_options.gpx && !log.gps_coordinates.is_empty() {
if let Err(e) = export_gpx_file(
file_path,
log_index,
log_positions.len(),
&gps_coords,
&home_coords,
&log.gps_coordinates,
&log.home_coordinates,
export_options,
log.header.log_start_datetime.as_deref(),
) {
Expand All @@ -2521,12 +2379,12 @@ fn parse_bbl_file_streaming(
}

// Export event data to JSON if requested
if export_options.event && !events.is_empty() {
if export_options.event && !log.event_frames.is_empty() {
if let Err(e) = export_event_file(
file_path,
log_index,
log_positions.len(),
&events,
&log.event_frames,
export_options,
) {
let filename = file_path
Expand Down