VOXL OpenVINS Server 0.7.0
Visual Inertial Odometry Server for VOXL Platform
Loading...
Searching...
No Matches
VoxlPublisher.cpp
Go to the documentation of this file.
1/**
2 * @file VoxlPublisher.cpp
3 * @brief Publisher implementation for VOXL OpenVINS
4 * @author Joao Leonardo Silva Cotta (@zauberflote1)
5 * @date 2025
6 * @version 1.0
7 *
8 * This file implements the publisher for the VOXL OpenVINS server.
9 */
10
11#include "VoxlHK.h"
12#include <atomic>
13#include <iterator>
14using namespace voxl;
15
16#define STR_MATCH(s, lit) (strncmp((s), (lit), strlen(lit)) == 0)
17
18// ============================================================================
19// PUBLISHER CLASS IMPLEMENTATION
20// ============================================================================
21
22/**
23 * @brief Constructor for the Publisher class
24 *
25 * Initializes the publisher by zeroing out the VIO data packet structure.
26 * This ensures that all fields start with known values.
27 */
28Publisher::Publisher()
29{
30 // Initialize the publisher
31 memset(&vio_packet, 0, sizeof(vio_data_t));
32}
33
34/**
35 * @brief Destructor for the Publisher class
36 *
37 * Performs cleanup by calling the stop method to ensure proper
38 * resource deallocation.
39 */
40Publisher::~Publisher()
41{
42 // Stop the publisher
43 stop();
44}
45
46/**
47 * @brief Start the publisher
48 *
49 * Initializes the publisher and prepares it for data transmission.
50 * Sets the first_packet flag to true to handle the initial angular
51 * velocity calculation.
52 */
54{
55 // Initialize publisher if needed
56 first_packet = true;
57
58 // Set VIO state to initializing
59 vio_state = VIO_STATE_INITIALIZING;
60
61 // Start the health check system
63}
64
65/**
66 * @brief Stop the publisher
67 *
68 * Stops the publisher and cleans up resources. Currently a placeholder
69 * for future cleanup operations.
70 */
72{
73 // Stop the health check system
75
76 // Clean up resources if needed
77}
78
79/**
80 * @brief Control-pipe callback for VIO commands
81 *
82 * This function is invoked every time a message
83 * is received on the VIO **control pipe**.
84 *
85 * @param ch Channel id supplied by the pipe framework.
86 * @param string Pointer to the received buffer.
87 * @param bytes Number of valid bytes in @p string.
88 * @param context User context pointer supplied during registration.
89 *
90 * @note Matching is performed with the `STR_MATCH()` macro, which compares the
91 * prefix of @p string against the command literal.
92 */
93void Publisher::ov_vio_control_pipe_cb(__attribute__((unused)) int ch,
94 char *string,
95 int bytes,
96 __attribute__((unused)) void *context)
97{
98 if (STR_MATCH(string, RESET_VIO_HARD))
99 {
100 if (reset_requested.load() || is_resetting.load())
101 {
102 fprintf(stderr, "[WARNING] Already resetting VIO, ignoring hard reset command.\n");
103 return;
104 }
105
106 reset_requested.store(true);
107 fprintf(stderr, "[ERROR] Client requested hard reset\n");
108 return;
109 }
110 else if (STR_MATCH(string, RESET_VIO_SOFT))
111 {
112 if (reset_requested.load() || soft_reset_requested.load() || is_resetting.load())
113 {
114 fprintf(stderr, "[WARNING] Already resetting VIO, ignoring soft reset command.\n");
115 return;
116 }
117 soft_reset_requested.store(true);
118 fprintf(stderr, "[INFO] Client requested soft reset (front-end preserving)\n");
119 return;
120 }
121 else
122 {
123 // Unrecognized command, log an error or handle appropriately
124 printf("Unrecognized control command: %.*s\n", bytes, string);
125 }
126
127 return;
128}
129
130/**
131 * @brief Publish VIO data to external systems
132 *
133 * This method formats and publishes the current VIO state and tracking
134 * information to external systems through the configured pipe interfaces.
135 *
136 * The function performs the following operations:
137 * - Formats VIO data packet with current state information
138 * - Performs coordinate frame transformations (OpenVINS to FRD)
139 * - Calculates angular velocity from quaternion differences
140 * - Extracts and formats covariance matrices
141 * - Handles camera-to-IMU extrinsic parameters
142 * - Publishes both simple and extended VIO data packets
143 *
144 * The coordinate frame transformation involves:
145 * - Converting from OpenVINS coordinate frame to Front-Right-Down (FRD)
146 * - Handling initialization state with NED rotation zeroing
147 * - Applying proper quaternion and rotation matrix transformations
148 *
149 * @param state Current VIO state containing pose, velocity, and covariance
150 */
151void Publisher::publish(std::shared_ptr<ov_msckf::State> state,
152 const std::map<double, std::vector<std::shared_ptr<ov_core::Feature>>> &used_features_map)
153{
154 // Handle FAILED state - publish failure packet and ensure reset is triggered
155 static bool wait_for_steady_init = true;
156 vio_packet.quality = -1;
157
158 if (vio_state.load() == VIO_STATE_FAILED && !is_resetting.load())
159 {
160 wait_for_steady_init = true;
161
162 // CRITICAL: Ensure reset is requested when in FAILED state
163 if (!reset_requested.load(std::memory_order_acquire))
164 {
165 reset_requested.store(true, std::memory_order_release);
166 fprintf(stderr, "[PUBLISH] VIO in FAILED state - REQUESTING RESET\n");
167 }
168
169 // Publish FAILED packet so external systems know status
170 memset(&vio_packet, 0, sizeof(vio_data_t));
171 vio_packet.magic_number = VIO_MAGIC_NUMBER;
172 vio_packet.timestamp_ns = state->_timestamp * 1e9;
173 vio_packet.quality = -1;
174 vio_packet.state = VIO_STATE_FAILED;
175 vio_packet.error_code = vio_error_codes.load();
176
177 pipe_server_write(SIMPLE_CH, (char *)&vio_packet, sizeof(vio_data_t));
178
179 if (en_debug)
180 {
181 static int64_t last_failed_msg = 0;
182 int64_t now = _apps_time_monotonic_ns();
183 if (now - last_failed_msg > 1000000000) // Print once per second
184 {
185 printf("[PUBLISH] Published FAILED packet, reset_requested=%d\n",
186 reset_requested.load());
187 last_failed_msg = now;
188 }
189 }
190 return;
191 }
192
193 vio_packet.magic_number = VIO_MAGIC_NUMBER;
194 vio_packet.timestamp_ns = state->_timestamp * 1e9;
195
196 // NOW LET'S DEAL WITH THE ACTUAL STATE
197 // RECALL: WE WANT TO EXPRESS THE GLOBAL FRAME IN THE IMU FRAME NOT THE IMU FRAME IN THE GLOBAL FRAME
198 // QUICK CLARIFICATION: IF WE WANTED PURELY IMU FRAME ESTIMATES, THEN P = 0, HENCE, WE REALLY WANT THE ABOVE-MENTIONED!
199
200 // LET'S GRAB THE QUATERNIONS FROM THE STATE: IN LATEX: {I}q_{G}
201 // USING FEJ -- LESS NOISY BUT "DELAYED" --> NOT A PROBLEM DUE TO IMU RATE
202 Eigen::Matrix<double, 4, 1> q_I_G = state->_imu->quat_fej();
203
204 // NOW DEAL WITH VELOCITY AND POSITION FROM THE STATE: IN LATEX: {G}p_{I} AND {G}v_{I}
205 // GLOBAL VELOCITY IN IMU FRAME FOLLOWS: v_I = {I}q_{G} \otimes v_G \otimes {G}q_{I}
206 Eigen::Matrix3d R_I_G = ov_core::quat_2_Rot(q_I_G);
207 auto RPY = ov_core::rot2rpy(R_I_G);
208 // AT THIS POINT, BETTER TO ROTATE IT USING THE CORRECTION MATRIX...
209 // EXECUTE THE FORBIDDEN TECHNIQUE:
210 // CHECK IF THE IMU IS MOUNTED IN THE CORRECT WAY, I.E.,
211 // GRAVITY IS POINTING DOWN Z
212 // IF NOT, EXECUTE THE FORBIDDEN TECHNIQUE:
213 float grav_vec[3];
214 if (frame_transform.is_initialized)
215 {
216 if (frame_transform.gravity_axis == FrameTransform::Axis::Z && frame_transform.gravity_direction == FrameTransform::Direction::POSITIVE)
217 {
218 // FORBIDDEN TECHNIQUE (STINGER CASE)
219 RPY(0) = -RPY(0);
220 RPY(1) = M_PI - RPY(1);
221 RPY(2) = -M_PI + RPY(2);
222 R_I_G = ov_core::rot_x(RPY(0)) * ov_core::rot_y(RPY(1)) * ov_core::rot_z(RPY(2));
223 // GRAVITY VECTOR
224 grav_vec[0] = 0;
225 grav_vec[1] = 0;
226 grav_vec[2] = static_cast<float>(-9.81); // CHECK THIS VALUE OR CALCULATE IT BY MEASURING THE GRAVITY VECTOR
227 }
228 else
229 {
230 // CLASSIC CASE: STARLING2, STARLING MAX, D8V4, D8V5
231 RPY(0) = -RPY(0);
232 RPY(1) = -M_PI + RPY(1);
233 RPY(2) = M_PI - RPY(2);
234 R_I_G = ov_core::rot_x(RPY(0)) * ov_core::rot_y(RPY(1)) * ov_core::rot_z(RPY(2));
235 // GRAVITY VECTOR
236 grav_vec[0] = 0;
237 grav_vec[1] = 0;
238 grav_vec[2] = static_cast<float>(9.81); // CHECK THIS VALUE OR CALCULATE IT BY MEASURING THE GRAVITY VECTOR
239 }
240 }
241 else
242 {
243 // Handle the case where frame_transform is not initialized
244 printf("Frame transform is not initialized. Not publishing packet.\n");
245 return;
246 }
247
248 memcpy(vio_packet.gravity_vector, grav_vec, sizeof(float) * 3);
249
250 // NOW CONVERT IT TO FRD FRAME
251 auto ov2frd = R_OV_FRD(); // TODO: PASS AN ARG FOR HINTING THE RIGHT BOARD ORIENTATION ETC
252 // GLOBAL VELOCITY IN IMU AXIS:
253 Eigen::Matrix<double, 3, 1> v_I_G = ov2frd * state->_imu->vel();
254 // GLOBAL POSITION IN IMU AXIS:
255 Eigen::Matrix<double, 3, 1> p_I_G = ov2frd * (state->_imu->pos());
256 // Re-origin the published trajectory at each init. Only YAW and position are
257 // unobservable, so only those are zeroed; gravity-anchored roll/pitch are
258 // preserved. ned_rot_zero is re-captured on the rising edge of
259 // vio_manager->initialized() so it stays valid across in-flight resets
260 // (doHardReset rebuilds vio_manager but keeps this Publisher alive; soft
261 // reset re-inits in place). Incorporates the origin-heading fix from
262 // origin/tilt_init (a6f8aa6).
263 bool now_init = vio_manager && vio_manager->initialized();
264 if (now_init)
265 {
266 if (!prev_initialized) // fresh init OR first frame after any reset
267 {
268 double yaw = ov_core::rot2rpy(R_I_G)(2); // body heading (psi) in the world frame at init
269
270 // ned_rot_zero is the world_uncorrected->world_corrected rotation
271 // (Wu->Wc): rot_z(-psi) cancels the init heading. In ZYX,
272 // rot_z(-psi) * R = rot_y(pitch) * rot_x(roll) has EXACTLY zero yaw at
273 // any tilt, so the published heading is 0 at init for any attitude.
274 ned_rot_zero = ov_core::rot_z(-yaw);
275 }
276
277 // Apply Wu->Wc as ONE consistent left-multiply to orientation AND the world
278 // vectors (velocity, position) -- a world-frame gauge (yaw) rotation:
279 // orientation: R_{Wu->Wc} * R_{B->Wu} = R_{B->Wc}
280 // vectors: R_{Wu->Wc} * v_{Wu} = v_{Wc}
281 // Right-multiplying orientation (R_{B->Wu} * R_{Wc->Wu}) does NOT chain
282 // (B != Wu): it re-references the BODY, desyncing attitude from velocity
283 // -> lateral drift. That was the pre-fix bug (plus it zeroed full R, i.e.
284 // roll/pitch too -- catastrophic at a tilted init).
285 R_I_G = ned_rot_zero * R_I_G;
286 v_I_G = ned_rot_zero * v_I_G;
287 p_I_G = ned_rot_zero * p_I_G;
288
289 // Set VIO state to OK when system is initialized
290 if (vio_state.load() == VIO_STATE_INITIALIZING)
291 {
292 vio_state = VIO_STATE_OK;
293 }
294 }
295 prev_initialized = now_init;
296 // FILL IN THE VIO PACKET - Fix casting issues
297 for (int i = 0; i < 3; i++)
298 {
299 vio_packet.T_imu_wrt_vio[i] = static_cast<float>(p_I_G(i));
300 vio_packet.vel_imu_wrt_vio[i] = static_cast<float>(v_I_G(i));
301 }
302 alt_z.store(static_cast<float>(p_I_G(2)), std::memory_order_release);
303 for (int i = 0; i < 3; i++)
304 {
305 for (int j = 0; j < 3; j++)
306 {
307 vio_packet.R_imu_to_vio[i][j] = static_cast<float>(R_I_G(i, j));
308 }
309 }
310 q_I_G = ov_core::rot_2_quat(R_I_G);
311
312 static int64_t prev_timestamp_ns = 0;
313
314 // NOW LET'S HANDLE THE ANGULAR VELOCITY
315 if (first_packet)
316 {
317
318 for (int i = 0; i < 3; i++)
319 {
320 vio_packet.imu_angular_vel[i] = 0.0f;
321 }
322 past_q_I_G = q_I_G;
323 prev_timestamp_ns = vio_packet.timestamp_ns;
324 }
325 else
326 {
327 double dt = (vio_packet.timestamp_ns - prev_timestamp_ns); // in nanoseconds
328
329 Eigen::Matrix<double, 3, 1> ang_vel_imu = dirtyOmega(past_q_I_G, q_I_G, dt);
330
331 for (int i = 0; i < 3; i++)
332 {
333 vio_packet.imu_angular_vel[i] = static_cast<float>(ang_vel_imu(i));
334 }
335
336 past_q_I_G = q_I_G;
337 prev_timestamp_ns = vio_packet.timestamp_ns;
338 }
339
340 // NOW HANDLE THE COVARIANCE, HAS TO BE DONE THIS WAY FOR MAVLINK
341 std::vector<std::shared_ptr<ov_type::Type>> statevars;
342 statevars.push_back(state->_imu->p());
343 statevars.push_back(state->_imu->q());
344 statevars.push_back(state->_imu->v());
345 Eigen::Matrix<double, 9, 9> covariance_posori =
346 ov_msckf::StateHelper::get_marginal_covariance(state,
347 statevars);
348
349 // Fill covariances (upper triangular format)
350 vio_packet.pose_covariance[0] = static_cast<float>(covariance_posori(0, 0));
351 vio_packet.pose_covariance[6] = static_cast<float>(covariance_posori(1, 1));
352 vio_packet.pose_covariance[11] = static_cast<float>(covariance_posori(2, 2));
353 vio_packet.pose_covariance[15] = static_cast<float>(covariance_posori(3, 3));
354 vio_packet.pose_covariance[18] = static_cast<float>(covariance_posori(4, 4));
355 vio_packet.pose_covariance[20] = static_cast<float>(covariance_posori(5, 5));
356 vio_packet.velocity_covariance[0] = static_cast<float>(covariance_posori(6, 6));
357 vio_packet.velocity_covariance[6] = static_cast<float>(covariance_posori(7, 7));
358 vio_packet.velocity_covariance[11] = static_cast<float>(covariance_posori(8, 8));
359
360 // NOW LET'S HANDLE THE EXTRINSICS CAMERA TO IMU
361 Eigen::Matrix3d cam_out = ov_core::quat_2_Rot(state->_calib_IMUtoCAM[0]->quat()).transpose();
362 for (int i = 0; i < 3; i++)
363 {
364 for (int j = 0; j < 3; j++)
365 {
366 vio_packet.R_cam_to_imu[i][j] = static_cast<float>(cam_out(i, j));
367 }
368 }
369
370 Eigen::Vector3d t_cam_wrt_imu = -(ov_core::quat_2_Rot(state->_calib_IMUtoCAM[0]->quat()).transpose() * state->_calib_IMUtoCAM[0]->pos());
371 for (int i = 0; i < 3; i++)
372 {
373 vio_packet.T_cam_wrt_imu[i] = static_cast<float>(t_cam_wrt_imu(i));
374 }
375
376 // ERROR CODE - Update atomic variable and copy to packet
377 // Check for covariance issues (negative diagonal elements)
378 if (covariance_posori(3, 3) < 0.0 || covariance_posori(4, 4) < 0.0 || covariance_posori(5, 5) < 0.0)
379 {
380 fprintf(stderr, "ERROR: covariance diagonal went negative\n");
381 vio_error_codes |= ERROR_CODE_COVARIANCE;
382 }
383
384 // Check for timestamp issues (packets from the past)
385 static int64_t last_sent_timestamp_ns = 0;
386 if (vio_packet.timestamp_ns < last_sent_timestamp_ns)
387 {
388 if (first_packet)
389 {
390 // During first packet, just update the timestamp without error
391 first_packet = false;
392 }
393 else
394 {
395 // Only flag error if timestamp is significantly in the past (more than 1ms)
396 int64_t time_diff = last_sent_timestamp_ns - vio_packet.timestamp_ns;
397 if (time_diff > 1000000)
398 { // 1ms in nanoseconds
399 fprintf(stderr, "WARNING: skipping pose data from the past %ld %ld (diff: %ld ns)\n",
400 vio_packet.timestamp_ns, last_sent_timestamp_ns, time_diff);
401 vio_error_codes |= ERROR_CODE_BAD_TIMESTAMP;
402 }
403 }
404 }
405 last_sent_timestamp_ns = vio_packet.timestamp_ns;
406
407 // Check for velocity uncertainty issues
408 double V_uncertainty = 0.0;
409 V_uncertainty += covariance_posori(6, 6) * covariance_posori(6, 6);
410 V_uncertainty += covariance_posori(7, 7) * covariance_posori(7, 7);
411 V_uncertainty += covariance_posori(8, 8) * covariance_posori(8, 8);
412 V_uncertainty = sqrt(V_uncertainty);
413
414
415 // Check for excessive velocity
416 double current_velocity = state->_imu->vel().norm();
417 if (current_velocity > auto_reset_max_velocity)
418 {
419 fprintf(stderr, "ERROR: exceeded maximum velocity %f vs %f\n",
420 current_velocity, auto_reset_max_velocity);
421 vio_error_codes |= ERROR_CODE_VEL_WINDOW_CERT;
422 }
423
424 std::unordered_map<size_t, std::shared_ptr<ov_type::Landmark>> SLAM_FEATS = state->_features_SLAM;
425
426 // NUMBER OF FEATURE POINTS
427 // FOR NOW, WE ONLY CONSIDER SLAM FEATURES, AS THESE ARE THE ONLY ONES IN THE STATE
428 vio_packet.n_feature_points = static_cast<uint16_t>(SLAM_FEATS.size());
429
430 // Check for insufficient features
431 static int64_t last_good_feat_ts = 0;
432 static int64_t last_good_qual_ts = 0;
433 static bool wait_for_features = true;
434 static uint32_t last_reset_count = reset_num_counter.load();
435 static int64_t last_good_state_ns = 0;
436
437 // C++17: Use const for reset counter check
438 const uint32_t current_reset_count = reset_num_counter.load(std::memory_order_acquire);
439 if (last_reset_count != current_reset_count)
440 {
441 // FIX: Reset tracking variables but DON'T return early
442 // Continuing allows first packet after reset to be published
443 last_good_state_ns = 0;
444 wait_for_steady_init = true;
445 last_reset_count = current_reset_count;
446 wait_for_features = true;
447 last_good_feat_ts = vio_packet.timestamp_ns;
448 last_good_qual_ts = vio_packet.timestamp_ns;
449
450 if (en_debug)
451 {
452 printf("[QUALITY] VIO reset detected (reset_count=%u), feature/quality tracking reset\n",
453 current_reset_count);
454 }
455 // CRITICAL FIX: Removed early return - allow publishing to continue
456 }
457
458 if (wait_for_features)
459 {
460 vio_error_codes.fetch_and(~ERROR_CODE_NO_FEATURES, std::memory_order_relaxed);
461 if (vio_packet.n_feature_points > auto_reset_min_features)
462 {
463 last_good_feat_ts = vio_packet.timestamp_ns;
464 wait_for_features = false;
465 }
466 }
467 else
468 {
469 if (vio_packet.n_feature_points > auto_reset_min_features)
470 {
471 last_good_feat_ts = vio_packet.timestamp_ns;
472 }
473
474 double ts = (vio_packet.timestamp_ns - last_good_feat_ts) * 1e-9;
476 {
477 fprintf(stderr, "ERROR: insufficient features for too long! cur: %d, min_req: %d\n",
478 vio_packet.n_feature_points, auto_reset_min_features);
479 vio_error_codes |= ERROR_CODE_NO_FEATURES;
480 wait_for_features = true;
481 }
482 }
483
484 // Check for fast yaw changes (spinning in place)
485 static int64_t start_spin_time = 0;
486 static bool spinning_detected = false;
487
488 double yawrate = vio_packet.imu_angular_vel[2];
489 bool spinning_in_place = (fabs(yawrate) > fast_yaw_thresh &&
490 fabs(vio_packet.vel_imu_wrt_vio[0]) <= 1.0 &&
491 fabs(vio_packet.vel_imu_wrt_vio[1]) <= 1.0);
492
493 if (!spinning_in_place)
494 {
495 start_spin_time = vio_packet.timestamp_ns;
496 spinning_detected = false;
497 }
498 else if (!spinning_detected)
499 {
500 double spin_duration = (vio_packet.timestamp_ns - start_spin_time) * 1e-9;
501 if (spin_duration > fast_yaw_timeout_s)
502 {
503 fprintf(stderr, "ERROR: exceeded spin rate over time threshold %f!\n", fast_yaw_timeout_s);
504 vio_error_codes |= ERROR_CODE_IMU_OOB;
505 spinning_detected = true;
506 }
507 }
508
509 // Copy error codes from atomic variable to packet
510 vio_packet.error_code = vio_error_codes.load();
511
512 // QUALITY CALCULATION
513 // Calculate uncertainty metrics
514 double T_uncertainty = 0.0;
515 T_uncertainty += covariance_posori(0, 0) * covariance_posori(0, 0);
516 T_uncertainty += covariance_posori(1, 1) * covariance_posori(1, 1);
517 T_uncertainty += covariance_posori(2, 2) * covariance_posori(2, 2);
518 T_uncertainty = sqrt(T_uncertainty);
519
520 double R_uncertainty = 0.0;
521 R_uncertainty += covariance_posori(3, 3) * covariance_posori(3, 3);
522 R_uncertainty += covariance_posori(4, 4) * covariance_posori(4, 4);
523 R_uncertainty += covariance_posori(5, 5) * covariance_posori(5, 5);
524 R_uncertainty = sqrt(R_uncertainty);
525
526 // Helper lambda for CEP-based quality calculation (DRY principle)
527 constexpr double max_allowable_cep = 0.15; // 15cm CEP threshold
528 auto calculate_cep_quality = [max_allowable_cep](double t_uncertainty) -> double
529 {
530 return std::max(0.0, 100.0 * (1.0 - t_uncertainty / max_allowable_cep));
531 };
532
533 // Get used features map from VioManager if not provided and NOT RESETTING!!
534 double calculated_quality;
535 bool is_during_initialization = ((!vio_manager || !vio_manager->initialized()) && (!is_resetting.load(std::memory_order_acquire)));
536 const bool throttling_active =
537 (vio_manager && vio_manager->initialized() &&
538 vio_state.load(std::memory_order_acquire) == VIO_STATE_OK &&
539 (!non_static.load(std::memory_order_acquire) || !has_acc_jerk.load(std::memory_order_acquire)));
540
541 if (!is_during_initialization && !throttling_active)
542 {
543 // Calculate quality using the new feature-based method
544 calculated_quality = calcQuality(used_features_map, SLAM_FEATS, state);
545 }
546 else
547 {
548 // WE NEED THIS FOR ARMING IN POSITION
549 // FIX: During initialization or when throttling frames, use CEP-based quality.
550 // Throttling reduces feature updates; CEP avoids falsely penalizing quality.
551 // Use already-calculated uncertainties to avoid redundant operations
552
553 calculated_quality = calculate_cep_quality(T_uncertainty);
554
555 if (en_debug && is_during_initialization)
556 {
557 printf("[QUALITY_INIT] VIO initializing - CEP quality: %.1f (features=%d, T_unc=%.4f m)\n",
558 calculated_quality, vio_packet.n_feature_points, T_uncertainty);
559 }
560 else if (en_debug && throttling_active)
561 {
562 printf("[QUALITY_THROTTLE] Using CEP quality during throttling: %.1f (T_unc=%.4f m, static=%d, acc_no_jerk=%d)\n",
563 calculated_quality, T_uncertainty,
564 !non_static.load(std::memory_order_relaxed),
565 !has_acc_jerk.load(std::memory_order_relaxed));
566 }
567 }
568
569 // Ensure calculated quality is within bounds
570 calculated_quality = std::max(0.0, std::min(100.0, calculated_quality));
571
572 // Check for quality issues using ACTUAL calculated quality (not hysteresis-filtered)
573 // This check monitors if quality has been consistently bad for too long. It only runs while
574 // initialized, so the timer must be armed at the INITIALIZED RISING EDGE: last_good_qual_ts
575 // was last refreshed before/at the reset, and measuring the whole multi-second re-init window
576 // against the sub-second threshold fired the error on the first initialized packet -- a
577 // freshly re-initialized filter never got a chance (auto-reset loop).
578 static bool qual_check_armed = false;
579 if (vio_manager && vio_manager->initialized())
580 {
581 if (!qual_check_armed)
582 {
583 last_good_qual_ts = vio_packet.timestamp_ns;
584 qual_check_armed = true;
585 }
586 double ts_threshold = auto_reset_max_v_cov_timeout_s;
587 if (calculated_quality >= 1)
588 {
589 last_good_qual_ts = vio_packet.timestamp_ns;
590 }
591
592 double qual_ts = (vio_packet.timestamp_ns - last_good_qual_ts) * 1e-9;
593 if (qual_ts > ts_threshold)
594 {
595 fprintf(stderr, "ERROR: actual quality was bad for too long!\n");
596 vio_error_codes |= ERROR_CODE_NOT_STATIONARY;
597 }
598 }
599 else
600 {
601 qual_check_armed = false;
602 }
603
604 // ========================================================================
605 // QUALITY HYSTERESIS SYSTEM - State Machine Approach
606 // ========================================================================
607 // States:
608 // 1. INITIAL: Report quality as-is (no filtering) on first run after reset
609 // 2. BAD: Quality degraded - apply strict hysteresis to recover
610 // 3. GOOD: Quality stable - apply hysteresis to prevent false alarms
611 //
612 // Transitions:
613 // - INITIAL → BAD: When quality drops below 20 for 10 consecutive samples
614 // - BAD → GOOD: When quality above 40 for 60 consecutive samples
615 // - GOOD → BAD: When quality below 20 for 45 consecutive samples
616 // - Any state → INITIAL: On VIO reset or FAILED state
617 // ========================================================================
618
619 enum QualityState
620 {
621 INITIAL,
622 BAD,
623 GOOD
624 };
625 static QualityState quality_state = INITIAL;
626 static int consecutive_above_40 = 0;
627 static int consecutive_below_20 = 0;
628 static uint32_t last_quality_reset_count = reset_num_counter.load();
629
630 // Reset to INITIAL state on VIO reset OR when entering FAILED state
631 bool should_reset_quality_state = (last_quality_reset_count != current_reset_count) ||
632 (vio_state.load() == VIO_STATE_FAILED);
633
634 if (should_reset_quality_state)
635 {
636 quality_state = INITIAL;
637 consecutive_above_40 = 0;
638 consecutive_below_20 = 0;
639 last_quality_reset_count = current_reset_count;
640 // Quality-metric lifecycle matches the hysteresis machine: reset_num_counter bumps for
641 // BOTH hard and soft resets, so smoothed values from the previous episode can never
642 // pre-charge this one (sensor time is continuous across resets)
643 qual_ema = -1.0;
644 qual_ema_t = 0.0;
645 qual_src_key = 0.0;
646 qual_src_size = 0;
647 qual_src_map_size = 0;
648
649 if (en_debug)
650 {
651 printf("[QUALITY] RESET to INITIAL state - will report quality as-is (reset_count=%u)\n",
652 current_reset_count);
653 }
654 }
655
656 // State machine transitions based on current quality.
657 // Gated on initialized(): during (re)initialization calculated_quality is CEP of a freshly
658 // seeded covariance (~1e-3 sigma => CEP ~100), which would latch GOOD before the filter has
659 // earned it -- the counters must only evolve on real feature-backed samples.
660 if (!should_reset_quality_state && vio_manager && vio_manager->initialized())
661 {
662 switch (quality_state)
663 {
664 case INITIAL:
665 // Report quality as-is, but monitor for degradation
666 if (calculated_quality <= quality_low_thresh_initial)
667 {
668 consecutive_below_20++;
669 if (consecutive_below_20 >= quality_initial_to_bad_count)
670 {
671 quality_state = BAD;
672 consecutive_below_20 = 0;
673 consecutive_above_40 = 0;
674 if (en_debug)
675 printf("[QUALITY] Transition: INITIAL → BAD (quality degraded)\n");
676 }
677 }
678 else
679 {
680 consecutive_below_20 = 0;
681 }
682 if (calculated_quality > quality_high_thresh)
683 {
684 consecutive_above_40++;
685 if (consecutive_above_40 >= quality_initial_to_good_count)
686 {
687 quality_state = GOOD;
688 consecutive_above_40 = 0;
689 consecutive_below_20 = 0;
690 std::cout << "[QUALITY] Transition: INITIAL → GOOD (" << quality_initial_to_good_count << " samples > " << quality_high_thresh << ")" << std::endl;
691 }
692 }
693 else
694 {
695 consecutive_above_40 = 0;
696 }
697 break;
698
699 case BAD:
700 // Quality is bad - require strong evidence to recover
701 if (calculated_quality > quality_high_thresh)
702 {
703 consecutive_above_40++;
704 if (consecutive_above_40 >= quality_bad_to_good_count)
705 {
706 quality_state = GOOD;
707 consecutive_above_40 = 0;
708 consecutive_below_20 = 0;
709 std::cout << "[QUALITY] Transition: BAD → GOOD (" << quality_bad_to_good_count << " samples > " << quality_high_thresh << ")" << std::endl;
710 }
711 }
712 else
713 {
714 consecutive_above_40 = 0;
715 }
716 break;
717
718 case GOOD:
719 // Quality is good - require strong evidence of degradation
720 if (calculated_quality <= quality_low_thresh_good)
721 {
722 consecutive_below_20++;
723 if (consecutive_below_20 >= quality_good_to_bad_count)
724 {
725 quality_state = BAD;
726 consecutive_below_20 = 0;
727 consecutive_above_40 = 0;
728 std::cout << "[QUALITY] Transition: GOOD → BAD (" << quality_good_to_bad_count << " samples ≤ " << quality_low_thresh_good << ")" << std::endl;
729 }
730 }
731 else
732 {
733 consecutive_below_20 = 0;
734 }
735 break;
736 }
737 }
738
739 // Determine reported quality based on state
740 int reported_quality;
741 switch (quality_state)
742 {
743 case INITIAL:
744 // Report CEP quality in INITIAL state ONLY if ZUPT update occurred
745 // Otherwise report 0 until system proves itself
746 {
747 if (vio_manager && vio_manager->get_did_zupt_update())
748 {
749 const double cep_quality = calculate_cep_quality(T_uncertainty);
750 reported_quality = static_cast<int>(cep_quality);
751
752 if (en_debug)
753 {
754 printf("[QUALITY_INIT] INITIAL state - CEP quality: %.1f (T_unc=%.4f m) [ZUPT done]\n",
755 cep_quality, T_uncertainty);
756 }
757 }
758 else
759 {
760 reported_quality = 0;
761
762 if (en_debug)
763 {
764 printf("[QUALITY_INIT] INITIAL state - Quality: 0 [waiting for ZUPT]\n");
765 }
766 }
767 }
768 break;
769 case BAD:
770 // Report 0 until quality recovers
771 reported_quality = 0;
772 break;
773 case GOOD:
774 // Report calculated quality (feature-based or CEP-based depending on VIO state)
775 reported_quality = static_cast<int>(calculated_quality);
776 break;
777 }
778
779 // Ensure reported quality is within bounds
780 reported_quality = std::max(0, std::min(100, reported_quality));
781
782 // Debug logging
783 if (en_debug && (quality_state != GOOD || reported_quality < 10))
784 {
785 const char *state_str = (quality_state == INITIAL) ? "INITIAL" : (quality_state == BAD) ? "BAD"
786 : "GOOD";
787 printf("[QUALITY] State: %s, Reported: %d, Calculated: %.1f, Consec(>40): %d, Consec(≤20): %d\n",
788 state_str, reported_quality, calculated_quality,
789 consecutive_above_40, consecutive_below_20);
790 }
791
792 // For error detection and auto-reset, use the ACTUAL calculated quality, not the hysteresis-filtered one
793 // This prevents false auto-resets during the initial warm-up period
794 int quality_for_error_detection = static_cast<int32_t>(calculated_quality);
795 if (quality_for_error_detection > 100)
796 quality_for_error_detection = 100;
797 if (quality_for_error_detection < 0)
798 quality_for_error_detection = 0;
799
800 // Determine if we're in the grace period after initialization/reset
801 bool in_grace_period = false;
802 if (vio_manager && vio_manager->initialized())
803 {
804 const int64_t now_ns = _apps_time_monotonic_ns();
805
806 // If we just entered OK (or after a reset), start the grace window.
807 if ((last_good_state_ns == 0 || vio_packet.n_feature_points < auto_reset_min_features) && wait_for_steady_init)
808 {
809 last_good_state_ns = now_ns;
810 }
811
812 // Duration since OK started
813 const int64_t dt_ok_ns = now_ns - last_good_state_ns;
814 const int64_t grace_ns = (int64_t)(ok_state_grace_timeout_s * 1e9);
815
816 in_grace_period = (dt_ok_ns < grace_ns && wait_for_steady_init);
817 }
818
819 // ========================================================================
820 // STATE DETERMINATION
821 // ========================================================================
822 // Priority order:
823 // 1. INITIALIZING - if VIO manager not initialized yet or resetting
824 // 2. FAILED - if auto-reset conditions met (only when fully initialized)
825 // 3. OK - normal operation
826 // ========================================================================
827
828 // FIRST: Check if we're still initializing (vio_manager not ready OR currently resetting)
829 // Note: Once vio_manager is initialized, we report OK state externally, even during grace period
830 if (!vio_manager || !vio_manager->initialized())
831 {
832 // System is INITIALIZING - VIO manager not ready yet
833
834 vio_state.store(VIO_STATE_INITIALIZING, std::memory_order_release);
835 if (vio_manager->get_did_zupt_update())
836 {
837 vio_packet.state = VIO_STATE_OK;
838 }
839 else
840 {
841 vio_packet.state = VIO_STATE_INITIALIZING;
842 }
843 // During initialization, use actual CEP quality for arming checks
844 // This allows position-based arming to work correctly -- but never claim more than the
845 // grace-period cap: a freshly (re)built covariance makes CEP read ~100 before the filter
846 // has earned it (a stale-ZUPT soft reset would otherwise publish near-perfect quality
847 // throughout re-init)
848 vio_packet.quality = std::min(reported_quality, 15);
849
850 if (en_debug)
851 {
852 printf("[STATE] VIO_STATE_INITIALIZING - vio_manager: %s, initialized: %s, quality: %d\n",
853 vio_manager ? "exists" : "null",
854 (vio_manager && vio_manager->initialized()) ? "yes" : "no",
855 vio_packet.quality);
856 }
857 }
858 // Handle resetting state separately - report as INITIALIZING but with different internal handling
859 else if (is_resetting.load(std::memory_order_acquire))
860 {
861 // System is resetting - report as INITIALIZING to external systems
862 vio_packet.state = VIO_STATE_INITIALIZING;
863 vio_state.store(VIO_STATE_INITIALIZING, std::memory_order_release);
864 vio_packet.quality = 0; // Quality is invalid during reset
865
866 if (en_debug)
867 {
868 printf("[STATE] VIO_STATE_INITIALIZING (resetting) - quality: %d\n", vio_packet.quality);
869 }
870 }
871 // SECOND: Check for auto-reset conditions (only if initialized and not in grace period)
872 else if (!in_grace_period &&
873 should_auto_reset(state, quality_for_error_detection, vio_packet.n_feature_points,
874 yawrate, current_velocity,
875 vio_packet.vel_imu_wrt_vio[0], vio_packet.vel_imu_wrt_vio[1]))
876 {
877 // Auto-reset conditions met - system has FAILED
878 fprintf(stderr, "WARNING: Auto-reset conditions detected! Quality: %d, Features: %d, V_uncertainty: %f\n",
879 quality_for_error_detection, vio_packet.n_feature_points, V_uncertainty);
880
881 vio_packet.quality = -1;
882 vio_packet.state = VIO_STATE_FAILED;
883 vio_state.store(VIO_STATE_FAILED, std::memory_order_release);
884
885 // CRITICAL: Request the actual reset
886 if (!reset_requested.load(std::memory_order_acquire))
887 {
888 reset_requested.store(true, std::memory_order_release);
889 fprintf(stderr, "[AUTO_RESET] REQUESTING RESET due to auto-reset conditions\n");
890 }
891 }
892 // THIRD: System is OK - normal operation
893 else
894 {
895 vio_packet.state = VIO_STATE_OK;
896 vio_state.store(VIO_STATE_OK, std::memory_order_release);
897
898 if (in_grace_period)
899 {
900 // During grace period, respect hysteresis while being conservative
901 if (reported_quality == 0)
902 {
903 vio_packet.quality = 0; // Respect hysteresis: system needs to prove itself
904 }
905 else
906 {
907 // Quality has passed hysteresis, but still in grace period - cap conservatively
908 vio_packet.quality = std::min(reported_quality, 15);
909 }
910
911 if (en_debug)
912 {
913 printf("[GRACE_PERIOD] In grace period: reported_quality=%d, vio_packet.quality=%d\n",
914 reported_quality, vio_packet.quality);
915 }
916 }
917 else
918 {
919 wait_for_steady_init = false;
920 // Set the reported quality with hysteresis applied
921 vio_packet.quality = reported_quality;
922 }
923 }
924
925 // FRAME
926 vio_packet.frame = 0; // Set appropriate frame value
927
928 // A reset can begin while this (pre-reset) publish is still in flight -- do not let one last
929 // stale-good packet race the teardown
930 if (is_resetting.load(std::memory_order_acquire))
931 {
932 vio_packet.quality = 0;
933 vio_packet.state = VIO_STATE_INITIALIZING;
934 }
935
936 // publish the packet
937 pipe_server_write(SIMPLE_CH, (char *)&vio_packet, sizeof(vio_data_t));
938
939 if (pipe_server_get_num_clients(EXTENDED_CH) > 0)
940 {
941 ext_vio_data_t ext_vio_packet;
942 ext_vio_packet.v = vio_packet;
943 ext_vio_packet.n_total_features = 0;
944
945 double current_timestamp = state->_timestamp;
946 auto timestamp_iter = used_features_map.find(current_timestamp);
947 // Async/ZUPT ticks may have no entry at the exact state time (the state advanced without
948 // a feature update) -- fall back to the newest entry so the extended feature list does
949 // not flicker empty. The per-measurement search below must then use the ENTRY's key:
950 // epoch-snapped measurements are stamped with the entry time, not the state time.
951 if (timestamp_iter == used_features_map.end() && !used_features_map.empty())
952 {
953 timestamp_iter = std::prev(used_features_map.end());
954 current_timestamp = timestamp_iter->first;
955 }
956
957 if (timestamp_iter != used_features_map.end())
958 {
959 const auto &features = timestamp_iter->second;
960
961 // Group features by camera ID
962 std::map<int, std::vector<std::shared_ptr<ov_core::Feature>>> features_by_camera;
963 for (const auto &feature : features)
964 {
965 // Get camera ID from feature measurements
966 for (const auto &cam_meas : feature->timestamps)
967 {
968 int cam_id = static_cast<int>(cam_meas.first);
969 features_by_camera[cam_id].push_back(feature);
970 }
971 }
972
973 ov_type::LandmarkRepresentation::Representation msckf_ref = state->_options.feat_rep_msckf;
974
975 // Process each camera's features
976 for (const auto &camera_pair : features_by_camera)
977 {
978 int cam_id = camera_pair.first;
979 const auto &cam_features = camera_pair.second;
980
981 for (const auto &feature : cam_features)
982 {
983 // Check if this feature has measurements for this camera at current timestamp
984 if (feature->timestamps.find(cam_id) == feature->timestamps.end() ||
985 feature->uvs.find(cam_id) == feature->uvs.end())
986 {
987 continue;
988 }
989
990 // Find the measurement index for the current timestamp
991 const auto &timestamps = feature->timestamps.at(cam_id);
992 auto time_iter = std::find(timestamps.begin(), timestamps.end(), current_timestamp);
993 if (time_iter == timestamps.end())
994 {
995 continue;
996 }
997
998 size_t meas_idx = std::distance(timestamps.begin(), time_iter);
999 const auto &uv_meas = feature->uvs.at(cam_id)[meas_idx];
1000
1001 // check if feature id found in SLAM set
1002 if (SLAM_FEATS.find(feature->featid) != SLAM_FEATS.end())
1003 {
1004 if (ext_vio_packet.n_total_features >= VIO_MAX_REPORTED_FEATURES)
1005 break;
1006
1007 int i_global = ext_vio_packet.n_total_features++;
1008
1009 ext_vio_packet.features[i_global].id = feature->featid;
1010 ext_vio_packet.features[i_global].cam_id = cam_id;
1011
1012 // Extract pixel location from UV measurement
1013 ext_vio_packet.features[i_global].pix_loc[0] = uv_meas(0);
1014 ext_vio_packet.features[i_global].pix_loc[1] = uv_meas(1);
1015
1016 Eigen::Vector3d p_FinG;
1017 if (SLAM_FEATS[feature->featid]->_feat_representation == ov_type::LandmarkRepresentation::Representation::GLOBAL_3D)
1018 {
1019 p_FinG = SLAM_FEATS[feature->featid]->get_xyz(true); // GRAB FEJ VALUE --> AVOID SNAP BACK EFFECT IN VIZ
1020 p_FinG = ov2frd * p_FinG;
1021 }
1022 else if (SLAM_FEATS[feature->featid]->_feat_representation == ov_type::LandmarkRepresentation::Representation::ANCHORED_MSCKF_INVERSE_DEPTH)
1023 {
1024 try
1025 {
1026 auto &slam_feat = SLAM_FEATS[feature->featid];
1027
1028 Eigen::Vector3d p_FinA = slam_feat->get_xyz(false);
1029
1030 // Anchor IMU clone
1031 auto imu_clone = state->_clones_IMU.at(slam_feat->_anchor_clone_timestamp);
1032 auto calib = state->_calib_IMUtoCAM.at(slam_feat->_anchor_cam_id);
1033
1034 Eigen::Matrix3d R_GtoI = imu_clone->Rot();
1035 Eigen::Vector3d p_IinG = imu_clone->pos();
1036
1037 Eigen::Matrix3d R_ItoC = calib->Rot();
1038 Eigen::Vector3d p_IinC = calib->pos();
1039
1040 // Camera pose at anchor
1041 Eigen::Matrix3d R_GtoC = R_ItoC * R_GtoI;
1042 Eigen::Vector3d p_CinG = p_IinG - R_GtoC.transpose() * p_IinC;
1043
1044 // Feature in OpenVINS global
1045 Eigen::Vector3d p_FinG_ov = R_GtoC.transpose() * p_FinA + p_CinG;
1046
1047 // Convert to FRD
1048 p_FinG = ov2frd * p_FinG_ov;
1049 }
1050 catch (const std::exception &e)
1051 {
1052 std::cerr << e.what() << '\n';
1053 }
1054 }
1055 else
1056 {
1057 printf("[WARNING] SLAM feat representation: %d not recognized, 3D point locations are likely invalid\n", SLAM_FEATS[feature->featid]->_feat_representation);
1058 }
1059
1060 ext_vio_packet.features[i_global].tsf[0] = p_FinG[0];
1061 ext_vio_packet.features[i_global].tsf[1] = p_FinG[1];
1062 ext_vio_packet.features[i_global].tsf[2] = p_FinG[2];
1063
1064 ext_vio_packet.features[i_global].point_quality = HIGH;
1065 }
1066 // if not found in SLAM, then it is an MSCKF feature
1067 else
1068 {
1069 if (ext_vio_packet.n_total_features >= VIO_MAX_REPORTED_FEATURES)
1070 break;
1071
1072 int i_global = ext_vio_packet.n_total_features++;
1073
1074 ext_vio_packet.features[i_global].id = feature->featid;
1075 ext_vio_packet.features[i_global].cam_id = cam_id;
1076
1077 // Extract pixel location from UV measurement
1078 ext_vio_packet.features[i_global].pix_loc[0] = uv_meas(0);
1079 ext_vio_packet.features[i_global].pix_loc[1] = uv_meas(1);
1080
1081 Eigen::Vector3d p_FinA = feature->p_FinA;
1082 Eigen::Vector3d p_FinG = feature->p_FinG;
1083
1084 if (msckf_ref == ov_type::LandmarkRepresentation::Representation::GLOBAL_3D)
1085 {
1086 p_FinG = ov2frd * p_FinG;
1087 }
1088 else if (msckf_ref == ov_type::LandmarkRepresentation::Representation::ANCHORED_MSCKF_INVERSE_DEPTH)
1089 {
1090 try
1091 {
1092 // Anchor IMU clone
1093 auto imu_clone = state->_clones_IMU.at(feature->anchor_clone_timestamp);
1094 auto calib = state->_calib_IMUtoCAM.at(feature->anchor_cam_id);
1095
1096 Eigen::Matrix3d R_GtoI = imu_clone->Rot();
1097 Eigen::Vector3d p_IinG = imu_clone->pos();
1098
1099 Eigen::Matrix3d R_ItoC = calib->Rot();
1100 Eigen::Vector3d p_IinC = calib->pos();
1101
1102 // Camera pose at anchor
1103 Eigen::Matrix3d R_GtoC = R_ItoC * R_GtoI;
1104 Eigen::Vector3d p_CinG = p_IinG - R_GtoC.transpose() * p_IinC;
1105
1106 // Feature in OpenVINS global
1107 Eigen::Vector3d p_FinG_ov = R_GtoC.transpose() * p_FinA + p_CinG;
1108
1109 // Convert to FRD
1110 p_FinG = ov2frd * p_FinG_ov;
1111 }
1112 catch (const std::exception &e)
1113 {
1114 std::cerr << e.what() << '\n';
1115 }
1116 }
1117 else
1118 {
1119 printf("[WARNING] MSCKF feat representation: %s not recognized, 3D point locations are likely invalid\n", ov_type::LandmarkRepresentation::as_string(msckf_ref).c_str());
1120 }
1121 ext_vio_packet.features[i_global].tsf[0] = p_FinG[0];
1122 ext_vio_packet.features[i_global].tsf[1] = p_FinG[1];
1123 ext_vio_packet.features[i_global].tsf[2] = p_FinG[2];
1124
1125 ext_vio_packet.features[i_global].point_quality = MEDIUM;
1126 }
1127 }
1128 }
1129 }
1130
1131 pipe_server_write(EXTENDED_CH, (char *)&ext_vio_packet, sizeof(ext_vio_data_t));
1132 }
1133}
1134
1135/**
1136 * @brief Check if auto-reset should be triggered
1137 *
1138 * This function evaluates the current VIO state and various error conditions
1139 * to determine if an automatic reset should be triggered. It implements the
1140 * same logic as the legacy code but in a more modular way.
1141 *
1142 * @param state Current VIO state
1143 * @param quality Current quality value
1144 * @param n_features Number of tracked features
1145 * @return true if auto-reset should be triggered, false otherwise
1146 */
1147bool Publisher::should_auto_reset(std::shared_ptr<ov_msckf::State> state,
1148 int quality,
1149 int n_features,
1150 double yawrate,
1151 double current_velocity,
1152 double vel_x,
1153 double vel_y)
1154{
1155 // Only check for auto-reset if enabled and system is stable
1156 if (!en_auto_reset)
1157 {
1158 return false;
1159 }
1160
1161 // FIX: Reset static variables when reset_num_counter changes
1162 // This prevents stale state from previous runs affecting current run
1163 static int64_t last_good_qual_ts = 0;
1164 static int64_t last_good_feat_ts = 0;
1165 static bool wait_for_features = true;
1166 static uint32_t last_reset_check_count = 0;
1167
1168 const uint32_t current_reset_count = reset_num_counter.load(std::memory_order_acquire);
1169 if (last_reset_check_count != current_reset_count)
1170 {
1171 // Reset occurred - clear all static tracking variables
1172 const int64_t current_ts_ns = state->_timestamp * 1e9;
1173 last_good_qual_ts = current_ts_ns;
1174 last_good_feat_ts = current_ts_ns;
1175 wait_for_features = true;
1176 last_reset_check_count = current_reset_count;
1177
1178 if (en_debug)
1179 {
1180 printf("[AUTO_RESET] Static variables reset for new VIO cycle (reset_count=%u)\n",
1181 current_reset_count);
1182 }
1183 }
1184
1185 // CRITICAL FIX: Remove instant quality check - conflicts with hysteresis
1186 // Quality hysteresis reports 0 during transition, but actual quality may be good
1187 // This was causing resets when quality improves!
1188 // Only check for SUSTAINED bad quality, not instant quality
1189 bool stable_quality_bad = false;
1190
1191 // Check for stable quality issues (quality bad for extended period)
1192 if (quality >= 1)
1193 {
1194 last_good_qual_ts = state->_timestamp * 1e9;
1195 }
1196 const double qual_elapsed_s = (state->_timestamp * 1e9 - last_good_qual_ts) * 1e-9;
1197 if (qual_elapsed_s > auto_reset_max_v_cov_timeout_s)
1198 {
1199 stable_quality_bad = true;
1200 }
1201
1202 // Check feature conditions
1203 bool stable_features_bad = false;
1204
1205 if (wait_for_features)
1206 {
1207 if (n_features > auto_reset_min_features)
1208 {
1209 last_good_feat_ts = state->_timestamp * 1e9;
1210 wait_for_features = false;
1211 }
1212 }
1213 else
1214 {
1215 if (n_features > auto_reset_min_features)
1216 {
1217 last_good_feat_ts = state->_timestamp * 1e9;
1218 }
1219
1220 double ts = (state->_timestamp * 1e9 - last_good_feat_ts) * 1e-9;
1222 {
1223 stable_features_bad = true;
1224 wait_for_features = true;
1225 }
1226 }
1227
1228 // Check velocity conditions using passed values
1229 bool too_fast = current_velocity > auto_reset_max_velocity;
1230
1231 // Check for excessive spinning using passed yawrate
1232 // FIX: Reset spin tracking on VIO reset
1233 static int64_t start_spin_time = 0;
1234 static bool spinning_detected = false;
1235 static uint32_t last_spin_reset_count = 0;
1236
1237 if (last_spin_reset_count != current_reset_count)
1238 {
1239 start_spin_time = state->_timestamp * 1e9;
1240 spinning_detected = false;
1241 last_spin_reset_count = current_reset_count;
1242 }
1243
1244 bool too_much_spinning = false;
1245
1246 // Use the passed yawrate value and actual velocity components
1247 const bool spinning_in_place = (fabs(yawrate) > fast_yaw_thresh &&
1248 fabs(vel_x) <= 1.0 &&
1249 fabs(vel_y) <= 1.0);
1250
1251 if (!spinning_in_place)
1252 {
1253 start_spin_time = state->_timestamp * 1e9;
1254 spinning_detected = false;
1255 }
1256 else if (!spinning_detected)
1257 {
1258 const double spin_duration = (state->_timestamp * 1e9 - start_spin_time) * 1e-9;
1259 if (spin_duration > fast_yaw_timeout_s)
1260 {
1261 too_much_spinning = true;
1262 spinning_detected = true;
1263 }
1264 }
1265
1266 // Return true if any condition is met
1267 // CRITICAL FIX: Removed quality_bad - was causing resets during hysteresis transitions
1268 return stable_quality_bad || stable_features_bad || too_fast || too_much_spinning;
1269}
1270
1271// ---------------------------------------------------------------------------------------------
1272// Quality metric internals (async-aware, per-camera freshness ledger)
1273//
1274// Async dual-cam updates are per-camera: the reference camera opens a new epoch entry in
1275// used_features_map and the other camera's features append to the SAME entry up to one bind
1276// horizon later (or land in their own entry on an epoch fallback). Scoring only the entry at
1277// exactly state->_timestamp therefore sees HALF the rig on ref-camera ticks, and nothing at all
1278// on ZUPT/featureless ticks (flat 10) -- which starved the downstream hysteresis (needs >40 for
1279// 50 CONSECUTIVE samples) and pinned published async quality at 0.
1280//
1281// Fix: pool every entry inside a short union window (dedup by featid) and score each camera over
1282// the POOLED set with the unchanged synced-path scorer, sum-then-clamp as sync always fused. One
1283// update's used-feature list is a sparse, noisy sample of tracking health (MSCKF features only
1284// appear when their tracks end), so scoring single entries makes the value jumpy; the union
1285// reproduces the full per-camera track set the synced batches effectively scored, and a
1286// time-constant EMA removes what noise remains. Everything here runs exclusively on the VIO
1287// thread (the ingest camera-processed callback) -- single writer, no locks by construction.
1288// ---------------------------------------------------------------------------------------------
1289
1290static constexpr int QUAL_DEDUP_CAP = 256; // max unique features pooled per window
1291static constexpr double QUAL_UNION_WINDOW_S = 0.120; // pool entries this far back (~3-4 epochs
1292 // at 30 Hz -- also the horizon over which a
1293 // dead camera's evidence ages out)
1294static constexpr double QUAL_EMA_TAU_S = 0.25; // published-value smoothing time constant:
1295 // one weak update moves the value ~2 pts; a
1296 // real collapse reaches the BAD band in ~0.5 s
1297
1298/// Append an entry's features into a fixed dedup array (by featid), returning the new count --
1299/// epoch entries accumulate per-camera update lists and the union spans several entries, so the
1300/// same feature object shows up repeatedly. O(n^2) pointer walk, n <= QUAL_DEDUP_CAP; zero
1301/// allocation.
1302static int collect_unique_features(const std::vector<std::shared_ptr<ov_core::Feature>> &entry_feats,
1303 const ov_core::Feature **uniq, int n, int cap)
1304{
1305 for (const auto &f : entry_feats)
1306 {
1307 if (f == nullptr)
1308 continue;
1309 bool dup = false;
1310 for (int i = 0; i < n; i++)
1311 {
1312 if (uniq[i]->featid == f->featid)
1313 {
1314 dup = true;
1315 break;
1316 }
1317 }
1318 if (!dup)
1319 {
1320 if (n >= cap)
1321 break;
1322 uniq[n++] = f.get();
1323 }
1324 }
1325 return n;
1326}
1327
1328/**
1329 * @brief Score one camera's used-feature entry on a 5x5 coverage grid
1330 *
1331 * Scoring is UNCHANGED from the original synced-path metric: SLAM features weighted by
1332 * marginal-covariance largest eigenvalue and tracker quality field, MSCKF features by quality
1333 * field and track length, combined 50/50 with grid coverage. Features without measurements on
1334 * cam_id are skipped, so the whole (multi-camera, already deduplicated) union can be passed
1335 * directly.
1336 *
1337 * Called only from calcQuality() on the VIO thread.
1338 */
1339static float score_camera_features(int cam_id,
1340 const ov_core::Feature **feats,
1341 int n_feats,
1342 std::unordered_map<size_t, std::shared_ptr<ov_type::Landmark>> &slam_features,
1343 const std::shared_ptr<ov_msckf::State> &state,
1344 int &slam_count,
1345 int &msckf_count)
1346{
1347 // Constants for quality calculation
1348 constexpr int GRID_SIZE = 5; // 5x5 grid
1349 constexpr int TARGET_FEATURES_PER_CAM = 50; // 50 features per camera
1350 constexpr int TARGET_FEATURES_PER_GRID = TARGET_FEATURES_PER_CAM / (GRID_SIZE * GRID_SIZE); // 2 features per grid
1351 constexpr int CLONES = 11; // Number of clones for MSCKF weighting
1352 constexpr double MAX_GRID_SCORE = 100.0; // Maximum score when 50% of grids are filled
1353 constexpr double GRID_FILL_THRESHOLD = 0.5; // 50% of grids should be filled for max score
1354 const int en_qual_debug = 0;
1355
1356 // Fixed-size grids on the stack -- no per-call allocation
1357 int grid[GRID_SIZE][GRID_SIZE] = {};
1358 double grid_quality[GRID_SIZE][GRID_SIZE] = {};
1359
1360 {
1361 int cam_slam_count = 0;
1362 int cam_msckf_count = 0;
1363
1364 // Process features for this camera (already deduplicated by the caller)
1365 for (int fi = 0; fi < n_feats; fi++)
1366 {
1367 const ov_core::Feature *feature = feats[fi];
1368 // Get feature position in image for this camera
1369 if (feature->timestamps.find(cam_id) != feature->timestamps.end() &&
1370 !feature->uvs_norm.empty() &&
1371 feature->uvs_norm.find(cam_id) != feature->uvs_norm.end() &&
1372 !feature->uvs_norm.at(cam_id).empty())
1373 {
1374
1375 const auto &uv_norm = feature->uvs_norm.at(cam_id).back();
1376 if (en_qual_debug)
1377 printf("[QUALITY_DEBUG] Feature %zu - Camera %d - UV Norm: (%.3f, %.3f)\n",
1378 feature->featid, cam_id, uv_norm(0), uv_norm(1));
1379
1380 // Convert normalized coordinates to grid coordinates
1381 // Assuming normalized coordinates are in [-1, 1] range
1382 int grid_x = static_cast<int>((uv_norm(0) + 1.0) * 0.5 * GRID_SIZE);
1383 int grid_y = static_cast<int>((uv_norm(1) + 1.0) * 0.5 * GRID_SIZE);
1384
1385 // Clamp to valid grid range
1386 grid_x = std::max(0, std::min(GRID_SIZE - 1, grid_x));
1387 grid_y = std::max(0, std::min(GRID_SIZE - 1, grid_y));
1388
1389 // Calculate feature quality based on whether it's SLAM or MSCKF
1390 double feature_quality = 0.0;
1391
1392 // Check if this is a SLAM feature
1393 if (slam_features.find(feature->featid) != slam_features.end())
1394 {
1395 // This is an active SLAM feature
1396 cam_slam_count++;
1397 auto slam_feat = slam_features[feature->featid];
1398
1399 if (en_qual_debug)
1400 printf("[QUALITY_DEBUG] Feature %zu (SLAM) - Grid[%d][%d] - Quality field: %.3f, uniq_cam: %d, anchor_cam: %d\n",
1401 feature->featid, grid_x, grid_y, slam_feat->_quality, slam_feat->_unique_camera_id, slam_feat->_anchor_cam_id);
1402 // ATTENTION: COULD SOLVE THIS IN A BATCH MANNER, BUT FOR NOW DO THE FOR LOOP DIRECTLY
1403 // Get covariance for this SLAM feature
1404 Eigen::MatrixXd slam_cov;
1405 if (slam_feat->id() >= 0)
1406 {
1407 // Get the covariance block for this feature
1408 std::vector<std::shared_ptr<ov_type::Type>> slam_feat_vec = {slam_feat};
1409 slam_cov = ov_msckf::StateHelper::get_marginal_covariance(state, slam_feat_vec);
1410 }
1411 else
1412 {
1413 // Feature not in state yet, use default covariance
1414 slam_cov = Eigen::MatrixXd::Identity(3, 3) * 1.4 * 1.4; // Default multiply by slam sigma pixel^2 --> UNITS TBD
1415 }
1416
1417 // Calculate largest eigenvalue of covariance
1418 Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> eigensolver(slam_cov);
1419 double max_eigenvalue = eigensolver.eigenvalues().maxCoeff();
1420
1421 // Quality based on covariance (smaller is better)
1422 // double cov_quality = std::max(0.0, 1.0 - max_eigenvalue);
1423 const double sigma_ref = 1.4; // Reference sigma value for normalization
1424 double cov_quality = 1.0 / (1.0 + max_eigenvalue / sigma_ref);
1425
1426 // Combine with feature quality field (if available)
1427 double feat_quality_field = (slam_feat->_quality >= 0) ? slam_feat->_quality : 0.0;
1428
1429 // Weight SLAM features more heavily
1430 feature_quality = 0.5 * cov_quality + 0.5 * feat_quality_field;
1431 // feature->quality = feature_quality; // Store in feature
1432
1433 if (en_qual_debug)
1434 printf("[QUALITY_DEBUG] SLAM Feature %zu - Max eigenvalue: %.6f, Cov quality: %.3f, Final quality: %.3f\n",
1435 feature->featid, max_eigenvalue, cov_quality, feature_quality);
1436 }
1437 else
1438 {
1439 // This is an MSCKF feature
1440 cam_msckf_count++;
1441
1442 // Count number of measurements for this feature
1443 int num_measurements = 0;
1444 for (const auto &cam_meas : feature->timestamps)
1445 {
1446 num_measurements += static_cast<int>(cam_meas.second.size());
1447 }
1448
1449 // Weight down if measurements/clones < 1
1450 double measurement_weight = 1.0;
1451 if (num_measurements < CLONES)
1452 {
1453 measurement_weight = static_cast<double>(num_measurements) / CLONES;
1454 }
1455
1456 // Use feature quality field (if available)
1457 double feat_quality_field = (feature->quality >= 0) ? feature->quality : 0.0;
1458
1459 // MSCKF features weighted less than SLAM features
1460 feature_quality = 0.4 * feat_quality_field * measurement_weight;
1461
1462 if (en_qual_debug)
1463 printf("[QUALITY_DEBUG] Feature %zu (MSCKF) - Grid[%d][%d] - Quality field: %.3f, Measurements: %d, Weight: %.3f, Final quality: %.3f\n",
1464 feature->featid, grid_x, grid_y, feat_quality_field, num_measurements, measurement_weight, feature_quality);
1465 }
1466
1467 // Add feature to grid
1468 grid[grid_y][grid_x]++;
1469 grid_quality[grid_y][grid_x] += feature_quality;
1470 }
1471 }
1472
1473 slam_count = cam_slam_count;
1474 msckf_count = cam_msckf_count;
1475
1476 if (en_qual_debug)
1477 printf("[QUALITY_DEBUG] Camera %d summary - SLAM: %d, MSCKF: %d\n", cam_id, cam_slam_count, cam_msckf_count);
1478
1479 // Calculate grid distribution score for this camera
1480 int grids_with_features = 0;
1481 double camera_grid_score = 0.0;
1482
1483 if (en_qual_debug)
1484 printf("[QUALITY_DEBUG] Camera %d grid analysis:\n", cam_id);
1485 for (int i = 0; i < GRID_SIZE; i++)
1486 {
1487 for (int j = 0; j < GRID_SIZE; j++)
1488 {
1489 if (grid[i][j] > 0)
1490 {
1491 grids_with_features++;
1492
1493 // Calculate quality for this grid cell
1494 double grid_cell_quality = 0.0;
1495 if (grid[i][j] >= TARGET_FEATURES_PER_GRID)
1496 {
1497 // Grid has enough features, use average quality
1498 grid_cell_quality = grid_quality[i][j] / grid[i][j];
1499 }
1500 else
1501 {
1502 // Grid has insufficient features, penalize
1503 double fill_ratio = std::min(1.0, static_cast<double>(grid[i][j]) / TARGET_FEATURES_PER_GRID);
1504 grid_cell_quality = (grid_quality[i][j] / std::max(1, grid[i][j])) * std::sqrt(fill_ratio);
1505 }
1506 camera_grid_score += grid_cell_quality;
1507
1508 if (en_qual_debug)
1509 printf("[QUALITY_DEBUG] Grid[%d][%d]: %d features, avg quality: %.3f\n",
1510 i, j, grid[i][j], grid_quality[i][j] / grid[i][j]);
1511 }
1512 }
1513 }
1514
1515 // Calculate grid distribution score
1516 double grid_fill_ratio = static_cast<double>(grids_with_features) / (GRID_SIZE * GRID_SIZE);
1517 double grid_distribution_score = 0.0;
1518
1519 if (grid_fill_ratio >= GRID_FILL_THRESHOLD)
1520 {
1521 // 50% or more grids filled, give maximum score
1522 grid_distribution_score = MAX_GRID_SCORE;
1523 }
1524 else
1525 {
1526 // Linear interpolation for partial fill
1527 grid_distribution_score = (grid_fill_ratio / GRID_FILL_THRESHOLD) * MAX_GRID_SCORE;
1528 }
1529
1530 // Combine grid distribution with feature quality
1531 double camera_score = 0.5 * grid_distribution_score + 0.5 * camera_grid_score;
1532
1533 if (en_qual_debug)
1534 printf("[QUALITY_DEBUG] Camera %d scores - Grid fill ratio: %.3f, Grid distribution: %.3f, Grid quality: %.3f, Final camera score: %.3f\n",
1535 cam_id, grid_fill_ratio, grid_distribution_score, camera_grid_score, camera_score);
1536
1537 return static_cast<float>(camera_score);
1538 }
1539}
1540
1541double Publisher::calcQuality(const std::map<double, std::vector<std::shared_ptr<ov_core::Feature>>> &used_features_map,
1542 std::unordered_map<size_t, std::shared_ptr<ov_type::Landmark>> &slam_features,
1543 std::shared_ptr<ov_msckf::State> state)
1544{
1545 // Metric-level floor: quality alone must never drive the sustained quality<1 auto-reset
1546 // timers (should_auto_reset / ERROR_CODE_NOT_STATIONARY refresh on quality >= 1). Collapse
1547 // is reported through the hysteresis machine instead (10 <= BAD threshold -> reported 0),
1548 // and the reset/FAILED paths force 0/-1 through their own branches in publish().
1549 constexpr double QUAL_FLOOR = 10.0;
1550 const int en_qual_debug = 0;
1551
1552 if (used_features_map.empty())
1553 return QUAL_FLOOR;
1554
1555 // ZUPT ticks advance state->_timestamp without inserting a map entry: anchor the evaluation
1556 // clock at the newest feature-update time on those ticks so a parked vehicle holds its last
1557 // smoothed value instead of decaying it (and then drifting the hysteresis machine).
1558 double t_now = state->_timestamp;
1559 const double newest_key = used_features_map.rbegin()->first;
1560 bool anchored = false;
1561 if (t_now > newest_key && vio_manager && vio_manager->get_did_zupt_update())
1562 {
1563 t_now = newest_key;
1564 anchored = true;
1565 }
1566
1567 // Rescore only when the union can have changed: entries are append-only, so the newest key,
1568 // its vector size, and the map size are a sufficient fingerprint. Time passing WITHOUT new
1569 // entries (mid-flight starvation) must also re-evaluate so the union thins honestly -- but
1570 // not while ZUPT-anchored.
1571 const size_t newest_size = used_features_map.rbegin()->second.size();
1572 const bool dirty = (newest_key != qual_src_key) || (newest_size != qual_src_size) ||
1573 (used_features_map.size() != qual_src_map_size) ||
1574 (!anchored && t_now > qual_ema_t);
1575
1576 if (dirty)
1577 {
1578 qual_src_key = newest_key;
1579 qual_src_size = newest_size;
1580 qual_src_map_size = used_features_map.size();
1581
1582 // Pool every entry inside the union window, deduplicated by featid. One update's
1583 // used-feature list is a sparse, noisy sample of the rig's tracking health (MSCKF
1584 // features only enter it when their tracks END), and scoring single entries is what
1585 // made the published value jumpy. The pooled set is each camera's full recent track
1586 // set -- the same thing the synced batches effectively scored -- so healthy flight
1587 // saturates the sum at 100 exactly like the synced path did.
1588 const ov_core::Feature *uniq[QUAL_DEDUP_CAP];
1589 int n_uniq = 0;
1590 const double t_min = t_now - QUAL_UNION_WINDOW_S;
1591 for (auto it = used_features_map.rbegin(); it != used_features_map.rend() && it->first >= t_min; ++it)
1592 {
1593 n_uniq = collect_unique_features(it->second, uniq, n_uniq, QUAL_DEDUP_CAP);
1594 }
1595
1596 // Which cameras have recent evidence?
1597 bool cam_present[QUAL_MAX_CAMS] = {};
1598 for (int fi = 0; fi < n_uniq; fi++)
1599 {
1600 for (const auto &cam_meas : uniq[fi]->timestamps)
1601 {
1602 const int cam_id = static_cast<int>(cam_meas.first);
1603 if (cam_id < 0 || cam_id >= QUAL_MAX_CAMS)
1604 continue;
1605 auto uv_it = uniq[fi]->uvs_norm.find(cam_meas.first);
1606 if (uv_it != uniq[fi]->uvs_norm.end() && !uv_it->second.empty())
1607 cam_present[cam_id] = true;
1608 }
1609 }
1610
1611 // Per-camera scores over the union, sum-then-clamp -- exactly the synced fusion
1612 double fused = 0.0;
1613 for (int c = 0; c < QUAL_MAX_CAMS; c++)
1614 {
1615 if (!cam_present[c])
1616 continue;
1617 int n_slam = 0, n_msckf = 0;
1618 const double cam_score = score_camera_features(c, uniq, n_uniq, slam_features, state, n_slam, n_msckf);
1619 fused += cam_score;
1620
1621 if (en_qual_debug)
1622 printf("[QUALITY_DEBUG] cam %d: %.1f (SLAM %d, MSCKF %d, union %d)\n",
1623 c, cam_score, n_slam, n_msckf, n_uniq);
1624 }
1625 fused = std::max(0.0, std::min(100.0, fused));
1626
1627 // Time-constant smoothing of the published value: a single weak update moves it by only
1628 // a couple of points (the synced metric hid this noise inside its 2x clamp headroom); a
1629 // real collapse still reaches the hysteresis BAD band in ~2*tau, well under the
1630 // 45-consecutive-sample requirement the machine itself imposes on top.
1631 if (qual_ema < 0.0)
1632 {
1633 qual_ema = fused;
1634 }
1635 else
1636 {
1637 const double dt = std::max(0.0, t_now - qual_ema_t);
1638 const double alpha = std::min(1.0, dt / QUAL_EMA_TAU_S);
1639 qual_ema += alpha * (fused - qual_ema);
1640 }
1641 qual_ema_t = t_now;
1642
1643 if (en_qual_debug)
1644 printf("[QUALITY_DEBUG] fused %.1f -> ema %.1f (union %d feats, window %.0f ms)\n",
1645 fused, qual_ema, n_uniq, QUAL_UNION_WINDOW_S * 1e3);
1646 }
1647
1648 if (qual_ema < 0.0)
1649 return QUAL_FLOOR;
1650
1651 return std::max(QUAL_FLOOR, std::min(100.0, qual_ema));
1652}
1653
1654void Publisher::publishBlank()
1655{
1656 // Zero out the VIO packet
1657 memset(&vio_packet, 0, sizeof(vio_data_t));
1658 vio_packet.magic_number = VIO_MAGIC_NUMBER;
1659 vio_packet.timestamp_ns = _apps_time_monotonic_ns();
1660
1661 // Set error codes for missing sensors
1662 uint32_t packet_error = 0;
1663 if (!is_imu_connected.load())
1664 packet_error |= ERROR_CODE_IMU_MISSING;
1665 if (!is_cam_connected.load())
1666 packet_error |= ERROR_CODE_CAM_MISSING;
1667 vio_packet.error_code = packet_error;
1668
1669 // Indicate failed state
1670 vio_packet.quality = -1;
1671 vio_packet.state = VIO_STATE_FAILED;
1672
1673 // Publish blank packet on simple channel
1674 pipe_server_write(SIMPLE_CH, (char *)&vio_packet, sizeof(vio_data_t));
1675}
Housekeeping and data publishing for VOXL OpenVINS.
int quality_initial_to_bad_count
Consecutive samples for INITIAL→BAD transition.
Definition VoxlVars.cpp:131
int quality_low_thresh_initial
Quality low threshold for INITIAL state.
Definition VoxlVars.cpp:122
int quality_bad_to_good_count
Consecutive samples for BAD→GOOD transition.
Definition VoxlVars.cpp:137
float fast_yaw_thresh
Auto fallback timeout (seconds)
Definition VoxlVars.cpp:112
int en_auto_reset
Enable automatic reset functionality.
Definition VoxlVars.cpp:84
float auto_reset_max_velocity
Maximum velocity threshold for auto reset.
Definition VoxlVars.cpp:87
voxl::FrameTransform frame_transform
Global frame transform instance.
Definition VoxlVars.cpp:181
int quality_good_to_bad_count
Consecutive samples for GOOD→BAD transition.
Definition VoxlVars.cpp:140
float auto_reset_max_v_cov_timeout_s
Maximum velocity covariance for instant reset.
Definition VoxlVars.cpp:94
float ok_state_grace_timeout_s
Minimum amount of time after initialization that quality is held low (CEP)
Definition VoxlVars.cpp:103
float auto_reset_min_feature_timeout_s
Minimum feature timeout for auto reset (seconds)
Definition VoxlVars.cpp:100
std::atomic< uint32_t > reset_num_counter
Counter which increments on resets (hard + soft)
Definition VoxlVars.cpp:56
float fast_yaw_timeout_s
Fast yaw timeout (seconds)
Definition VoxlVars.cpp:115
int auto_reset_min_features
Minimum number of features for auto reset.
Definition VoxlVars.cpp:97
std::unique_ptr< ov_msckf::VioManager > vio_manager
Main VIO manager instance.
Definition VoxlVars.cpp:31
int quality_high_thresh
Quality high threshold for recovery.
Definition VoxlVars.cpp:128
int en_debug
Enable debug output.
Definition VoxlVars.cpp:149
int quality_low_thresh_good
Quality low threshold for GOOD state.
Definition VoxlVars.cpp:125
int quality_initial_to_good_count
Consecutive samples for INITIAL→GOOD transition.
Definition VoxlVars.cpp:134
std::atomic< bool > non_static
Non-static flag for jerk detection.
#define SIMPLE_CH
MPA server channel for simple VIO data output.
Definition VoxlVars.h:154
std::atomic< bool > is_resetting
VIO reset state flag.
std::atomic< uint8_t > vio_state
Current VIO system state.
std::atomic< float > alt_z
Altitude z.
std::atomic< bool > has_acc_jerk
Flag indicating if accelerometer jerk is detected.
#define EXTENDED_CH
MPA server channel for extended VIO data output.
Definition VoxlVars.h:146
std::atomic< uint32_t > vio_error_codes
VIO error codes.
std::atomic< bool > is_imu_connected
IMU connection state.
std::atomic< bool > reset_requested
Should reset floag.
std::atomic< bool > soft_reset_requested
Flag indicating that a front-end-preserving SOFT reset was requested.
std::atomic< bool > is_cam_connected
Camera connection state.
void start()
Start the health check system.
void stop()
Stop the health check system.
static HealthCheck & getInstance()
Get singleton instance.
Definition VoxlHK.h:312
static void ov_vio_control_pipe_cb(int ch, char *string, int bytes, void *context)
Control pipe callback function.
void stop()
Stop the publisher.
void start()
Start the publisher.
bool should_auto_reset(std::shared_ptr< ov_msckf::State > state, int quality, int n_features, double yawrate, double current_velocity, double vel_x, double vel_y)
Check if auto-reset should be triggered.
void publish(std::shared_ptr< ov_msckf::State > state, const std::map< double, std::vector< std::shared_ptr< ov_core::Feature > > > &used_features_map={})
Publish VIO data.
double calcQuality(const std::map< double, std::vector< std::shared_ptr< ov_core::Feature > > > &used_features_map, std::unordered_map< size_t, std::shared_ptr< ov_type::Landmark > > &slam_features, std::shared_ptr< ov_msckf::State > state)
Calculate Quality of the VIO state (async-aware per-camera freshness ledger)
Main namespace for VOXL OpenVINS server components.
Eigen::Matrix< double, 3, 1 > dirtyOmega(const Eigen::Matrix< double, 4, 1 > &q0, const Eigen::Matrix< double, 4, 1 > &q1, double dt)
Calculate angular velocity from consecutive quaternions.
Definition VoxlHK.h:63
const Eigen::Matrix3d & R_OV_FRD()
Get OpenVINS to FRD coordinate frame transformation matrix.
Definition VoxlHK.h:105