@@ -32,6 +32,9 @@ STRUCT_CONFIG_SECTION(SurviveKalmanTracker)
3232 STRUCT_CONFIG_ITEM ("kalman-max-pose-angular-rate" ,
3333 "Maximum angular rate (rad/s) before suppressing pose output; -1 to disable" ,
3434 -1. , t -> max_pose_angular_rate )
35+ STRUCT_CONFIG_ITEM ("lc-angular-rate-max" ,
36+ "Maximum angular rate (rad/s) before rejecting lightcap batch input; -1 to disable" ,
37+ -1. , t -> lc_angular_rate_max )
3538 STRUCT_CONFIG_ITEM ("min-report-time" ,
3639 "Minimum kalman report time in s (-1 defaults to 1. / imu_hz)" , -1. , t -> min_report_time )
3740 STRUCT_CONFIG_ITEM ("report-covariance" , "Report covariance matrix every n poses" , -1 , t -> report_covariance_cnt );
@@ -412,6 +415,28 @@ void survive_kalman_tracker_integrate_saved_light(SurviveKalmanTracker *tracker,
412415 qsort (tracker -> savedLight , tracker -> savedLight_idx , sizeof (tracker -> savedLight [0 ]), sort_by_lh_axis_sensor );
413416 }
414417
418+ // Input-level angular rate gate: reject lightcap batches implying physically
419+ // impossible rotation speed before they can corrupt the Kalman state.
420+ // Unlike the output-level kalman-max-pose-angular-rate gate, this prevents
421+ // the filter from ever integrating reflection data — the state stays clean
422+ // and coasts on IMU-only until a valid batch arrives.
423+ if (tracker -> lc_angular_rate_max > 0 && tracker -> last_accepted_lc_time > 0 &&
424+ quatmagnitude (tracker -> last_accepted_lc_rot ) > 0.5 ) {
425+ SurvivePose predicted = {0 };
426+ survive_kalman_tracker_predict (tracker , time , & predicted );
427+ FLT dt = time - tracker -> last_accepted_lc_time ;
428+ if (dt > 0 ) {
429+ FLT ang_rate = quatdist (tracker -> last_accepted_lc_rot , predicted .Rot ) / dt ;
430+ if (ang_rate > tracker -> lc_angular_rate_max ) {
431+ SV_INFO ("lc-gate: dropping batch %.2f rad/s > lc-angular-rate-max %.2f for %s" ,
432+ ang_rate , tracker -> lc_angular_rate_max ,
433+ survive_colorize_codename (tracker -> so ));
434+ tracker -> stats .lightcap_model_dropped ++ ;
435+ return ;
436+ }
437+ }
438+ }
439+
415440 FLT rtn = 0 ;
416441 while (tracker -> savedLight_idx > 0 ) {
417442 int lh = tracker -> savedLight [tracker -> savedLight_idx - 1 ].lh ;
@@ -457,7 +482,11 @@ void survive_kalman_tracker_integrate_saved_light(SurviveKalmanTracker *tracker,
457482 // measurements too far from the current state estimate to be trusted
458483 // (typically a reflection or severe miscalibration) and is skipped for
459484 // this sync cycle.
460- if (tracker -> light_outlier_threshold > 0 && tracker -> light_residuals_all > 0 ) {
485+ // Require the EWMA to exceed a floor before gating: on cold start / after
486+ // a reset the mean is tiny and 5*tiny ≈ 0, which rejects everything and
487+ // prevents the EWMA from ever warming up (death spiral). 1e-4 is safely
488+ // below steady-state residuals (~0.0002) but above the warm-up bootstrap.
489+ if (tracker -> light_outlier_threshold > 0 && tracker -> light_residuals_all > 1e-4 ) {
461490 CN_CREATE_STACK_VEC (y_dry , cnt );
462491 bool dry_ok = map_light_data (& cbctx , & Z , & tracker -> model .state , & y_dry , NULL );
463492 if (dry_ok ) {
@@ -466,6 +495,10 @@ void survive_kalman_tracker_integrate_saved_light(SurviveKalmanTracker *tracker,
466495 for (int i = 0 ; i < cnt ; i ++ ) sq += yv [i ] * yv [i ];
467496 FLT rms = FLT_SQRT (sq / cnt );
468497 if (rms > tracker -> light_outlier_threshold * tracker -> light_residuals_all ) {
498+ SV_INFO ("lc-gate: dropping LH%d batch rms=%.4f > %.1f*mean=%.4f for %s" ,
499+ lh , rms , tracker -> light_outlier_threshold ,
500+ tracker -> light_residuals_all ,
501+ survive_colorize_codename (tracker -> so ));
469502 tracker -> stats .lightcap_model_dropped ++ ;
470503 continue ;
471504 }
@@ -533,6 +566,14 @@ void survive_kalman_tracker_integrate_saved_light(SurviveKalmanTracker *tracker,
533566 SV_DATA_LOG ("res_error_light_avg" , & tracker -> light_residuals_all , 1 );
534567 tracker -> stats .lightcap_count ++ ;
535568
569+ // Update the lightcap input gate reference now that this batch was accepted.
570+ if (tracker -> lc_angular_rate_max > 0 ) {
571+ SurvivePose post = {0 };
572+ survive_kalman_tracker_predict (tracker , time , & post );
573+ quatcopy (tracker -> last_accepted_lc_rot , post .Rot );
574+ tracker -> last_accepted_lc_time = time ;
575+ }
576+
536577 survive_kalman_tracker_report_state (pd , tracker );
537578 }
538579}
@@ -1614,6 +1655,7 @@ void survive_kalman_tracker_stats(SurviveKalmanTracker *tracker) {
16141655
16151656 memset (& tracker -> stats , 0 , sizeof (tracker -> stats ));
16161657 tracker -> first_report_time = tracker -> last_report_time = 0 ;
1658+ tracker -> last_accepted_lc_time = 0 ;
16171659
16181660 SV_VERBOSE (5 , " " );
16191661}
0 commit comments