Skip to content

Commit 0b7c74b

Browse files
committed
Add smart export filtering with gyro-based ground test detection
- Add --force-export flag to bypass filtering - Implement 3-tier filtering: <5s skip, 5-15s check data density, >15s check gyro activity - Skip logs with <1500fps data density in 5-15s range - Detect ground tests using gyro variance analysis (threshold 0.3) - Conservative approach prevents false-skips of legitimate flights - Update README with filtering documentation and examples
1 parent 9cc122e commit 0b7c74b

2 files changed

Lines changed: 196 additions & 0 deletions

File tree

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ A high-performance Rust library and command-line tool for parsing BBL (Blackbox
1919
- **CSV Export**: Flight data and header export with blackbox_decode compatibility
2020
- **GPS Export**: GPX file generation for GPS-enabled flight logs
2121
- **Event Export**: Flight event data extraction in JSONL format
22+
- **Smart Export Filtering**: Automatically skips short test flights while preserving high-quality short logs
2223
- **Command Line Interface**: Glob patterns, debug mode, configurable output directories
2324
- **Comprehensive Examples**: Practical demonstrations of crate usage with PID display and multi-firmware support
2425

@@ -290,8 +291,27 @@ cargo build --release
290291

291292
# Custom output directory
292293
./target/release/bbl_parser --csv --output-dir ./output logs/*.BBL
294+
295+
# Force export all logs (bypasses smart filtering)
296+
./target/release/bbl_parser --csv --force-export logs/*.BBL
293297
```
294298

299+
### Smart Export Filtering
300+
301+
By default, the tool uses smart filtering to skip short test flights and focus on meaningful flight data:
302+
303+
- **< 5 seconds**: Always skipped (brief arming, connectivity tests)
304+
- **5-15 seconds**: Kept only if data density > 1500 fps (sufficient for analysis)
305+
- **> 15 seconds**: Always kept (normal flight logs)
306+
307+
**Examples:**
308+
- 0.7s flight with 1500 frames → Skipped (too short)
309+
- 2.2s flight with 4407 frames (2003 fps) → Kept (good data density)
310+
- 10s flight with 8000 frames (800 fps) → Skipped (insufficient data density)
311+
- 20s flight → Always kept (normal duration)
312+
313+
Use `--force-export` to bypass filtering and export all logs regardless of duration or data quality.
314+
295315
## Output
296316

297317
### Console Statistics (Always Displayed)

src/main.rs

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ struct ExportOptions {
190190
gpx: bool,
191191
event: bool,
192192
output_dir: Option<String>,
193+
force_export: bool,
193194
}
194195

195196
// Pre-computed CSV field mapping for performance
@@ -304,12 +305,19 @@ fn main() -> Result<()> {
304305
.help("Export event data (E frames) to JSON files")
305306
.action(clap::ArgAction::SetTrue),
306307
)
308+
.arg(
309+
Arg::new("force-export")
310+
.long("force-export")
311+
.help("Force export of all logs, including short flights (bypasses smart filtering: <5s skip, 5-15s needs >1500fps, >15s keep)")
312+
.action(clap::ArgAction::SetTrue),
313+
)
307314
.get_matches();
308315

309316
let debug = matches.get_flag("debug");
310317
let export_csv = matches.get_flag("csv");
311318
let export_gpx = matches.get_flag("gpx") || matches.get_flag("gps");
312319
let export_event = matches.get_flag("event");
320+
let force_export = matches.get_flag("force-export");
313321
let output_dir = matches.get_one::<String>("output-dir").cloned();
314322
let file_patterns: Vec<&String> = matches.get_many::<String>("files").unwrap().collect();
315323

@@ -318,6 +326,7 @@ fn main() -> Result<()> {
318326
gpx: export_gpx,
319327
event: export_event,
320328
output_dir: output_dir.clone(),
329+
force_export,
321330
};
322331

323332
// Keep legacy csv_options for compatibility
@@ -1028,6 +1037,160 @@ fn display_log_info(log: &BBLLog) {
10281037
}
10291038
}
10301039

1040+
/// Determines if a log should be skipped for export based on duration and frame count
1041+
/// Uses smart filtering: <5s always skip, 5-15s keep if good data density (>1500fps), >15s always keep
1042+
fn should_skip_export(log: &BBLLog, force_export: bool) -> (bool, String) {
1043+
if force_export {
1044+
return (false, String::new()); // Never skip when forced
1045+
}
1046+
1047+
const VERY_SHORT_DURATION_MS: u64 = 5_000; // 5 seconds - always skip
1048+
const SHORT_DURATION_MS: u64 = 15_000; // 15 seconds - threshold for normal logs
1049+
const MIN_DATA_DENSITY_FPS: f64 = 1500.0; // Minimum fps for short logs
1050+
const FALLBACK_MIN_FRAMES: u32 = 7_500; // ~5 seconds at 1500 fps (fallback when no duration)
1051+
1052+
// Check if we have duration information
1053+
if log.stats.start_time_us > 0 && log.stats.end_time_us > log.stats.start_time_us {
1054+
let duration_us = log
1055+
.stats
1056+
.end_time_us
1057+
.saturating_sub(log.stats.start_time_us);
1058+
let duration_ms = duration_us / 1000;
1059+
let duration_s = duration_ms as f64 / 1000.0;
1060+
let fps = log.stats.total_frames as f64 / duration_s;
1061+
1062+
// Very short logs: < 5 seconds → Always skip
1063+
if duration_ms < VERY_SHORT_DURATION_MS {
1064+
return (true, format!("too short ({:.1}s < 5.0s)", duration_s));
1065+
}
1066+
1067+
// Short logs: 5-15 seconds → Keep if sufficient data density (>1500 fps)
1068+
if duration_ms < SHORT_DURATION_MS {
1069+
if fps < MIN_DATA_DENSITY_FPS {
1070+
return (
1071+
true,
1072+
format!(
1073+
"insufficient data density ({:.0}fps < {:.0}fps for {:.1}s log)",
1074+
fps, MIN_DATA_DENSITY_FPS, duration_s
1075+
),
1076+
);
1077+
}
1078+
// Good data density, keep it
1079+
return (false, String::new());
1080+
}
1081+
1082+
// Normal logs: > 15 seconds → Check for minimal gyro activity (ground tests)
1083+
if duration_ms >= SHORT_DURATION_MS {
1084+
let (is_minimal_movement, max_variance) = has_minimal_gyro_activity(log);
1085+
if is_minimal_movement {
1086+
return (
1087+
true,
1088+
format!(
1089+
"minimal gyro activity ({:.1} variance) - likely ground test",
1090+
max_variance
1091+
),
1092+
);
1093+
}
1094+
}
1095+
1096+
return (false, String::new());
1097+
}
1098+
1099+
// No duration information available, fall back to frame count
1100+
// Skip if very low frame count (equivalent to <5s at minimum viable fps)
1101+
if log.stats.total_frames < FALLBACK_MIN_FRAMES {
1102+
return (
1103+
true,
1104+
format!(
1105+
"too few frames ({} < {}) and no duration info",
1106+
log.stats.total_frames, FALLBACK_MIN_FRAMES
1107+
),
1108+
);
1109+
}
1110+
1111+
// Sufficient frames without duration info, keep it
1112+
(false, String::new())
1113+
}
1114+
1115+
/// Analyzes gyro variance to detect ground tests vs actual flight
1116+
/// Returns true if the log appears to be a static ground test (minimal movement)
1117+
fn has_minimal_gyro_activity(log: &BBLLog) -> (bool, f64) {
1118+
// Conservative thresholds to avoid false-skips
1119+
const MIN_SAMPLES_FOR_ANALYSIS: usize = 15; // Reduced for limited sample data
1120+
const VERY_LOW_GYRO_VARIANCE_THRESHOLD: f64 = 0.3; // More aggressive threshold for ground test detection
1121+
1122+
let mut gyro_x_values = Vec::new();
1123+
let mut gyro_y_values = Vec::new();
1124+
let mut gyro_z_values = Vec::new();
1125+
1126+
// First try to use debug_frames if available (contains more comprehensive data)
1127+
if let Some(debug_frames) = &log.debug_frames {
1128+
// Collect gyro data from I and P frames in debug_frames
1129+
for (frame_type, frames) in debug_frames {
1130+
if *frame_type == 'I' || *frame_type == 'P' {
1131+
for frame in frames {
1132+
if let Some(gyro_x) = frame.data.get("gyroADC[0]") {
1133+
if let Some(gyro_y) = frame.data.get("gyroADC[1]") {
1134+
if let Some(gyro_z) = frame.data.get("gyroADC[2]") {
1135+
gyro_x_values.push(*gyro_x as f64);
1136+
gyro_y_values.push(*gyro_y as f64);
1137+
gyro_z_values.push(*gyro_z as f64);
1138+
}
1139+
}
1140+
}
1141+
}
1142+
}
1143+
}
1144+
}
1145+
1146+
// Fallback to sample_frames if debug_frames not available or insufficient data
1147+
if gyro_x_values.len() < MIN_SAMPLES_FOR_ANALYSIS {
1148+
for frame in &log.sample_frames {
1149+
if let Some(gyro_x) = frame.data.get("gyroADC[0]") {
1150+
if let Some(gyro_y) = frame.data.get("gyroADC[1]") {
1151+
if let Some(gyro_z) = frame.data.get("gyroADC[2]") {
1152+
gyro_x_values.push(*gyro_x as f64);
1153+
gyro_y_values.push(*gyro_y as f64);
1154+
gyro_z_values.push(*gyro_z as f64);
1155+
}
1156+
}
1157+
}
1158+
}
1159+
}
1160+
1161+
// Need sufficient data points for reliable analysis
1162+
if gyro_x_values.len() < MIN_SAMPLES_FOR_ANALYSIS {
1163+
return (false, 0.0); // Not enough data, don't skip (conservative approach)
1164+
}
1165+
1166+
// Calculate variance for each axis
1167+
let variance_x = calculate_variance(&gyro_x_values);
1168+
let variance_y = calculate_variance(&gyro_y_values);
1169+
let variance_z = calculate_variance(&gyro_z_values);
1170+
1171+
// Use the maximum variance across all axes
1172+
let max_variance = variance_x.max(variance_y).max(variance_z);
1173+
1174+
// Very conservative: only skip if ALL axes show extremely low variance
1175+
let is_minimal = max_variance < VERY_LOW_GYRO_VARIANCE_THRESHOLD;
1176+
1177+
(is_minimal, max_variance)
1178+
}
1179+
1180+
/// Calculate variance of a dataset
1181+
fn calculate_variance(values: &[f64]) -> f64 {
1182+
if values.len() < 2 {
1183+
return 0.0;
1184+
}
1185+
1186+
let mean = values.iter().sum::<f64>() / values.len() as f64;
1187+
let variance = values.iter()
1188+
.map(|x| (x - mean).powi(2))
1189+
.sum::<f64>() / values.len() as f64;
1190+
1191+
variance
1192+
}
1193+
10311194
#[allow(dead_code)]
10321195
fn export_logs_to_csv(
10331196
logs: &[BBLLog],
@@ -2454,6 +2617,19 @@ fn parse_bbl_file_streaming(
24542617
// Display log info immediately
24552618
display_log_info(&log);
24562619

2620+
// Check if we should skip exports for this log
2621+
let (should_skip, reason) = should_skip_export(&log, export_options.force_export);
2622+
if should_skip {
2623+
println!("Skipping exports for this log: {}", reason);
2624+
processed_logs += 1;
2625+
2626+
// Add separator between logs for clarity
2627+
if log_index + 1 < log_positions.len() {
2628+
println!();
2629+
}
2630+
continue;
2631+
}
2632+
24572633
// Export CSV immediately while data is hot in cache
24582634
if export_options.csv {
24592635
if let Err(e) = export_single_log_to_csv(&log, file_path, csv_options, debug) {

0 commit comments

Comments
 (0)