@@ -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) ]
10321195fn 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