VOXL OpenVINS Server 1.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 Zauberflote
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>
13using namespace voxl;
14
15#define STR_MATCH(s, lit) (strncmp((s), (lit), strlen(lit)) == 0)
16
17// ============================================================================
18// PUBLISHER CLASS IMPLEMENTATION
19// ============================================================================
20
21/**
22 * @brief Constructor for the Publisher class
23 *
24 * Initializes the publisher by zeroing out the VIO data packet structure.
25 * This ensures that all fields start with known values.
26 */
27Publisher::Publisher()
28{
29 // Initialize the publisher
30 memset(&vio_packet, 0, sizeof(vio_data_t));
31}
32
33/**
34 * @brief Destructor for the Publisher class
35 *
36 * Performs cleanup by calling the stop method to ensure proper
37 * resource deallocation.
38 */
39Publisher::~Publisher()
40{
41 // Stop the publisher
42 stop();
43}
44
45/**
46 * @brief Start the publisher
47 *
48 * Initializes the publisher and prepares it for data transmission.
49 * Sets the first_packet flag to true to handle the initial angular
50 * velocity calculation.
51 */
53{
54 // Initialize publisher if needed
55 first_packet = true;
56
57 // Set VIO state to initializing
58 vio_state = VIO_STATE_INITIALIZING;
59
60 // Start the health check system
62}
63
64/**
65 * @brief Stop the publisher
66 *
67 * Stops the publisher and cleans up resources. Currently a placeholder
68 * for future cleanup operations.
69 */
71{
72 // Stop the health check system
74
75 // Clean up resources if needed
76}
77
78/**
79 * @brief Control-pipe callback for VIO commands
80 *
81 * This function is invoked every time a message
82 * is received on the VIO **control pipe**.
83 *
84 * @param ch Channel id supplied by the pipe framework.
85 * @param string Pointer to the received buffer.
86 * @param bytes Number of valid bytes in @p string.
87 * @param context User context pointer supplied during registration.
88 *
89 * @note Matching is performed with the `STR_MATCH()` macro, which compares the
90 * prefix of @p string against the command literal.
91 */
92void Publisher::ov_vio_control_pipe_cb(__attribute__((unused)) int ch,
93 char *string,
94 int bytes,
95 __attribute__((unused)) void *context)
96{
97 if (STR_MATCH(string, RESET_VIO_HARD))
98 {
99 if (reset_requested.load() || is_resetting.load())
100 {
101 fprintf(stderr, "[WARNING] Already resetting VIO, ignoring hard reset command.\n");
102 return;
103 }
104
105 reset_requested.store(true);
106 fprintf(stderr, "[ERROR] Client requested hard reset\n");
107 return;
108 }
109 else if (STR_MATCH(string, RESET_VIO_SOFT))
110 {
111 }
112 else
113 {
114 // Unrecognized command, log an error or handle appropriately
115 printf("Unrecognized control command: %.*s\n", bytes, string);
116 }
117
118 return;
119}
120
121/**
122 * @brief Publish VIO data to external systems
123 *
124 * This method formats and publishes the current VIO state and tracking
125 * information to external systems through the configured pipe interfaces.
126 *
127 * The function performs the following operations:
128 * - Formats VIO data packet with current state information
129 * - Performs coordinate frame transformations (OpenVINS to FRD)
130 * - Calculates angular velocity from quaternion differences
131 * - Extracts and formats covariance matrices
132 * - Handles camera-to-IMU extrinsic parameters
133 * - Publishes both simple and extended VIO data packets
134 *
135 * The coordinate frame transformation involves:
136 * - Converting from OpenVINS coordinate frame to Front-Right-Down (FRD)
137 * - Handling initialization state with NED rotation zeroing
138 * - Applying proper quaternion and rotation matrix transformations
139 *
140 * @param state Current VIO state containing pose, velocity, and covariance
141 */
142void Publisher::publish(std::shared_ptr<ov_msckf::State> state,
143 std::map<double, std::vector<std::shared_ptr<ov_core::Feature>>> used_features_map)
144{
145 // Handle FAILED state - publish failure packet and ensure reset is triggered
146 static bool wait_for_steady_init = true;
147 vio_packet.quality = -1;
148
149 if (vio_state.load() == VIO_STATE_FAILED && !is_resetting.load())
150 {
151 wait_for_steady_init = true;
152
153 // CRITICAL: Ensure reset is requested when in FAILED state
154 if (!reset_requested.load(std::memory_order_acquire))
155 {
156 reset_requested.store(true, std::memory_order_release);
157 fprintf(stderr, "[PUBLISH] VIO in FAILED state - REQUESTING RESET\n");
158 }
159
160 // Publish FAILED packet so external systems know status
161 memset(&vio_packet, 0, sizeof(vio_data_t));
162 vio_packet.magic_number = VIO_MAGIC_NUMBER;
163 vio_packet.timestamp_ns = state->_timestamp * 1e9;
164 vio_packet.quality = -1;
165 vio_packet.state = VIO_STATE_FAILED;
166 vio_packet.error_code = vio_error_codes.load();
167
168 pipe_server_write(SIMPLE_CH, (char *)&vio_packet, sizeof(vio_data_t));
169
170 if (en_debug)
171 {
172 static int64_t last_failed_msg = 0;
173 int64_t now = _apps_time_monotonic_ns();
174 if (now - last_failed_msg > 1000000000) // Print once per second
175 {
176 printf("[PUBLISH] Published FAILED packet, reset_requested=%d\n",
177 reset_requested.load());
178 last_failed_msg = now;
179 }
180 }
181 return;
182 }
183
184 vio_packet.magic_number = VIO_MAGIC_NUMBER;
185 vio_packet.timestamp_ns = state->_timestamp * 1e9;
186
187 // NOW LET'S DEAL WITH THE ACTUAL STATE
188 // RECALL: WE WANT TO EXPRESS THE GLOBAL FRAME IN THE IMU FRAME NOT THE IMU FRAME IN THE GLOBAL FRAME
189 // QUICK CLARIFICATION: IF WE WANTED PURELY IMU FRAME ESTIMATES, THEN P = 0, HENCE, WE REALLY WANT THE ABOVE-MENTIONED!
190
191 // LET'S GRAB THE QUATERNIONS FROM THE STATE: IN LATEX: {I}q_{G}
192 // USING FEJ -- LESS NOISY BUT "DELAYED" --> NOT A PROBLEM DUE TO IMU RATE
193 Eigen::Matrix<double, 4, 1> q_I_G = state->_imu->quat_fej();
194 // TODO: Validate quat_fej() vs. quat() usage
195 // Eigen::Matrix<double, 4, 1> q_I_G = state->_imu->quat();
196
197 // NOW DEAL WITH VELOCITY AND POSITION FROM THE STATE: IN LATEX: {G}p_{I} AND {G}v_{I}
198 // GLOBAL VELOCITY IN IMU FRAME FOLLOWS: v_I = {I}q_{G} \otimes v_G \otimes {G}q_{I}
199 Eigen::Matrix3d R_I_G = ov_core::quat_2_Rot(q_I_G);
200 auto RPY = ov_core::rot2rpy(R_I_G);
201 // printf("gravity axis: %d, gravity direction: %d\n", static_cast<int>(frame_transform.gravity_axis), static_cast<int>(frame_transform.gravity_direction));
202 // AT THIS POINT, BETTER TO ROTATE IT USING THE CORRECTION MATRIX...
203 // EXECUTE THE FORBIDDEN TECHNIQUE:
204 // CHECK IF THE IMU IS MOUNTED IN THE CORRECT WAY, I.E.,
205 // GRAVITY IS POINTING DOWN Z
206 // IF NOT, EXECUTE THE FORBIDDEN TECHNIQUE:
207 float grav_vec[3];
208 if (frame_transform.is_initialized)
209 {
210 if (frame_transform.gravity_axis == FrameTransform::Axis::Z && frame_transform.gravity_direction == FrameTransform::Direction::POSITIVE)
211 {
212 // FORBIDDEN TECHNIQUE (STINGER CASE)
213 RPY(0) = -RPY(0);
214 RPY(1) = M_PI - RPY(1);
215 RPY(2) = -M_PI + RPY(2);
216 R_I_G = ov_core::rot_x(RPY(0)) * ov_core::rot_y(RPY(1)) * ov_core::rot_z(RPY(2));
217 // GRAVITY VECTOR
218 grav_vec[0] = 0;
219 grav_vec[1] = 0;
220 grav_vec[2] = static_cast<float>(-9.81); // CHECK THIS VALUE OR CALCULATE IT BY MEASURING THE GRAVITY VECTOR
221 }
222 else
223 {
224 // CLASSIC CASE: STARLING2, STARLING MAX, D8V4, D8V5
225 RPY(0) = -RPY(0);
226 RPY(1) = -M_PI + RPY(1);
227 RPY(2) = M_PI - RPY(2);
228 R_I_G = ov_core::rot_x(RPY(0)) * ov_core::rot_y(RPY(1)) * ov_core::rot_z(RPY(2));
229 // GRAVITY VECTOR
230 grav_vec[0] = 0;
231 grav_vec[1] = 0;
232 grav_vec[2] = static_cast<float>(9.81); // CHECK THIS VALUE OR CALCULATE IT BY MEASURING THE GRAVITY VECTOR
233 }
234 }
235 else
236 {
237 // Handle the case where frame_transform is not initialized
238 printf("Frame transform is not initialized. Not publishing packet.\n");
239 return;
240 }
241
242 memcpy(vio_packet.gravity_vector, grav_vec, sizeof(float) * 3);
243
244 // NOW CONVERT IT TO FRD FRAME
245 auto ov2frd = R_OV_FRD(); // TODO: PASS AN ARG FOR HINTING THE RIGHT BOARD ORIENTATION ETC
246 // GLOBAL VELOCITY IN IMU AXIS:
247 Eigen::Matrix<double, 3, 1> v_I_G = ov2frd * state->_imu->vel();
248 // GLOBAL POSITION IN IMU AXIS:
249 Eigen::Matrix<double, 3, 1> p_I_G = ov2frd * (state->_imu->pos());
250 if (vio_manager->initialized())
251 {
252 static Eigen::Matrix<double, 3, 3> ned_rot_zero = R_I_G;
253 R_I_G = R_I_G * ned_rot_zero.transpose();
254 v_I_G = ned_rot_zero * v_I_G;
255 p_I_G = ned_rot_zero * p_I_G;
256
257 // Set VIO state to OK when system is initialized
258 if (vio_state.load() == VIO_STATE_INITIALIZING)
259 {
260 vio_state = VIO_STATE_OK;
261 }
262 }
263 // FILL IN THE VIO PACKET - Fix casting issues
264 for (int i = 0; i < 3; i++)
265 {
266 vio_packet.T_imu_wrt_vio[i] = static_cast<float>(p_I_G(i));
267 vio_packet.vel_imu_wrt_vio[i] = static_cast<float>(v_I_G(i));
268 }
269 alt_z.store(static_cast<float>(p_I_G(2)), std::memory_order_release);
270 for (int i = 0; i < 3; i++)
271 {
272 for (int j = 0; j < 3; j++)
273 {
274 vio_packet.R_imu_to_vio[i][j] = static_cast<float>(R_I_G(i, j));
275 }
276 }
277 q_I_G = ov_core::rot_2_quat(R_I_G);
278
279 // NOW LET'S HANDLE THE ANGULAR VELOCITY
280 if (first_packet)
281 {
282
283 for (int i = 0; i < 3; i++)
284 {
285 vio_packet.imu_angular_vel[i] = 0.0f;
286 }
287 past_q_I_G = q_I_G;
288 }
289 else
290 {
291 Eigen::Matrix<double, 3, 1> ang_vel_imu = dirtyOmega(q_I_G, past_q_I_G, state->_timestamp - past_state->_timestamp);
292 for (int i = 0; i < 3; i++)
293 {
294 vio_packet.imu_angular_vel[i] = static_cast<float>(ang_vel_imu(i));
295 }
296 past_q_I_G = q_I_G;
297 }
298
299 // NOW HANDLE THE COVARIANCE, HAS TO BE DONE THIS WAY FOR MAVLINK
300 std::vector<std::shared_ptr<ov_type::Type>> statevars;
301 statevars.push_back(state->_imu->p());
302 statevars.push_back(state->_imu->q());
303 statevars.push_back(state->_imu->v());
304 Eigen::Matrix<double, 9, 9> covariance_posori =
305 ov_msckf::StateHelper::get_marginal_covariance(state,
306 statevars);
307
308 // Fill covariances (upper triangular format)
309 vio_packet.pose_covariance[0] = static_cast<float>(covariance_posori(0, 0));
310 vio_packet.pose_covariance[6] = static_cast<float>(covariance_posori(1, 1));
311 vio_packet.pose_covariance[11] = static_cast<float>(covariance_posori(2, 2));
312 vio_packet.pose_covariance[15] = static_cast<float>(covariance_posori(3, 3));
313 vio_packet.pose_covariance[18] = static_cast<float>(covariance_posori(4, 4));
314 vio_packet.pose_covariance[20] = static_cast<float>(covariance_posori(5, 5));
315 vio_packet.velocity_covariance[0] = static_cast<float>(covariance_posori(6, 6));
316 vio_packet.velocity_covariance[6] = static_cast<float>(covariance_posori(7, 7));
317 vio_packet.velocity_covariance[11] = static_cast<float>(covariance_posori(8, 8));
318
319 // NOW LET'S HANDLE THE EXTRINSICS CAMERA TO IMU
320 Eigen::Matrix3d cam_out = ov_core::quat_2_Rot(state->_calib_IMUtoCAM[0]->quat()).transpose();
321 for (int i = 0; i < 3; i++)
322 {
323 for (int j = 0; j < 3; j++)
324 {
325 vio_packet.R_cam_to_imu[i][j] = static_cast<float>(cam_out(i, j));
326 }
327 }
328
329 Eigen::Vector3d t_cam_wrt_imu = -(ov_core::quat_2_Rot(state->_calib_IMUtoCAM[0]->quat()).transpose() * state->_calib_IMUtoCAM[0]->pos());
330 for (int i = 0; i < 3; i++)
331 {
332 vio_packet.T_cam_wrt_imu[i] = static_cast<float>(t_cam_wrt_imu(i));
333 }
334
335 // GYRO AND ACCELEROMETER BIAS --> NOT USING FEJ HERE...
336 // Note: These fields don't exist in the standard vio_data_t struct
337 // for(int i = 0; i < 3; i++) {
338 // vio_packet.gyro_bias[i] = static_cast<float>(state->_imu->bias_g()(i));
339 // vio_packet.accl_bias[i] = static_cast<float>(state->_imu->bias_a()(i));
340 // }
341 // ERROR CODE - Update atomic variable and copy to packet
342 // Check for covariance issues (negative diagonal elements)
343 if (covariance_posori(3, 3) < 0.0 || covariance_posori(4, 4) < 0.0 || covariance_posori(5, 5) < 0.0)
344 {
345 fprintf(stderr, "ERROR: covariance diagonal went negative\n");
346 vio_error_codes |= ERROR_CODE_COVARIANCE;
347 }
348
349 // Check for timestamp issues (packets from the past)
350 static int64_t last_sent_timestamp_ns = 0;
351 if (vio_packet.timestamp_ns < last_sent_timestamp_ns)
352 {
353 if (first_packet)
354 {
355 // During first packet, just update the timestamp without error
356 first_packet = false;
357 }
358 else
359 {
360 // Only flag error if timestamp is significantly in the past (more than 1ms)
361 int64_t time_diff = last_sent_timestamp_ns - vio_packet.timestamp_ns;
362 if (time_diff > 1000000)
363 { // 1ms in nanoseconds
364 fprintf(stderr, "WARNING: skipping pose data from the past %ld %ld (diff: %ld ns)\n",
365 vio_packet.timestamp_ns, last_sent_timestamp_ns, time_diff);
366 printf("[DEBUG-HK] Setting ERROR_CODE_BAD_TIMESTAMP in VIO packet\n");
367 vio_error_codes |= ERROR_CODE_BAD_TIMESTAMP;
368 }
369 }
370 }
371 last_sent_timestamp_ns = vio_packet.timestamp_ns;
372
373 // Check for velocity uncertainty issues
374 double V_uncertainty = 0.0;
375 V_uncertainty += covariance_posori(6, 6) * covariance_posori(6, 6);
376 V_uncertainty += covariance_posori(7, 7) * covariance_posori(7, 7);
377 V_uncertainty += covariance_posori(8, 8) * covariance_posori(8, 8);
378 V_uncertainty = sqrt(V_uncertainty);
379
380 if (is_armed && V_uncertainty > auto_reset_max_v_cov_instant)
381 {
382 fprintf(stderr, "ERROR: exceeded velocity uncertainty threshold %f vs %f\n",
383 V_uncertainty, auto_reset_max_v_cov_instant);
384 vio_error_codes |= ERROR_CODE_VEL_INST_CERT;
385 }
386
387 // Check for excessive velocity
388 double current_velocity = state->_imu->vel().norm();
389 if (current_velocity > auto_reset_max_velocity)
390 {
391 fprintf(stderr, "ERROR: exceeded maximum velocity %f vs %f\n",
392 current_velocity, auto_reset_max_velocity);
393 vio_error_codes |= ERROR_CODE_VEL_WINDOW_CERT;
394 }
395
396 std::unordered_map<size_t, std::shared_ptr<ov_type::Landmark>> SLAM_FEATS = state->_features_SLAM;
397
398 // NUMBER OF FEATURE POINTS
399 // FOR NOW, WE ONLY CONSIDER SLAM FEATURES, AS THESE ARE THE ONLY ONES IN THE STATE
400 vio_packet.n_feature_points = static_cast<uint16_t>(SLAM_FEATS.size());
401
402 // Check for insufficient features
403 static int64_t last_good_feat_ts = 0;
404 static int64_t last_good_qual_ts = 0;
405 static bool wait_for_features = true;
406 static uint32_t last_reset_count = reset_num_counter.load();
407 static int64_t last_good_state_ns = 0;
408
409 // C++17: Use const for reset counter check
410 const uint32_t current_reset_count = reset_num_counter.load(std::memory_order_acquire);
411 if (last_reset_count != current_reset_count)
412 {
413 // FIX: Reset tracking variables but DON'T return early
414 // Continuing allows first packet after reset to be published
415 last_good_state_ns = 0;
416 wait_for_steady_init = true;
417 last_reset_count = current_reset_count;
418 wait_for_features = true;
419 last_good_feat_ts = vio_packet.timestamp_ns;
420 last_good_qual_ts = vio_packet.timestamp_ns;
421
422 if (en_debug)
423 {
424 printf("[QUALITY] VIO reset detected (reset_count=%u), feature/quality tracking reset\n",
425 current_reset_count);
426 }
427 // CRITICAL FIX: Removed early return - allow publishing to continue
428 }
429
430 if (wait_for_features)
431 {
432 vio_error_codes.fetch_and(~ERROR_CODE_NO_FEATURES, std::memory_order_relaxed);
433 if (vio_packet.n_feature_points > auto_reset_min_features)
434 {
435 last_good_feat_ts = vio_packet.timestamp_ns;
436 wait_for_features = false;
437 }
438 }
439 else
440 {
441 if (vio_packet.n_feature_points > auto_reset_min_features)
442 {
443 last_good_feat_ts = vio_packet.timestamp_ns;
444 }
445
446 double ts = (vio_packet.timestamp_ns - last_good_feat_ts) * 1e-9;
448 {
449 fprintf(stderr, "ERROR: insufficient features for too long! cur: %d, min_req: %d\n",
450 vio_packet.n_feature_points, auto_reset_min_features);
451 vio_error_codes |= ERROR_CODE_NO_FEATURES;
452 wait_for_features = true;
453 }
454 }
455
456 // Check for fast yaw changes (spinning in place)
457 static int64_t start_spin_time = 0;
458 static bool spinning_detected = false;
459
460 double yawrate = vio_packet.imu_angular_vel[2];
461 bool spinning_in_place = (fabs(yawrate) > fast_yaw_thresh &&
462 fabs(vio_packet.vel_imu_wrt_vio[0]) <= 1.0 &&
463 fabs(vio_packet.vel_imu_wrt_vio[1]) <= 1.0);
464
465 if (!spinning_in_place)
466 {
467 start_spin_time = vio_packet.timestamp_ns;
468 spinning_detected = false;
469 }
470 else if (!spinning_detected)
471 {
472 double spin_duration = (vio_packet.timestamp_ns - start_spin_time) * 1e-9;
473 if (spin_duration > fast_yaw_timeout_s)
474 {
475 fprintf(stderr, "ERROR: exceeded spin rate over time threshold %f!\n", fast_yaw_timeout_s);
476 vio_error_codes |= ERROR_CODE_IMU_OOB;
477 spinning_detected = true;
478 }
479 }
480
481 // Copy error codes from atomic variable to packet
482 vio_packet.error_code = vio_error_codes.load();
483
484 // QUALITY CALCULATION
485 // Get SLAM features from state
486 // std::unordered_map<size_t, std::shared_ptr<ov_type::Landmark>> SLAM_FEATS = state->_features_SLAM;
487 // Calculate uncertainty metrics
488 double T_uncertainty = 0.0;
489 T_uncertainty += covariance_posori(0, 0) * covariance_posori(0, 0);
490 T_uncertainty += covariance_posori(1, 1) * covariance_posori(1, 1);
491 T_uncertainty += covariance_posori(2, 2) * covariance_posori(2, 2);
492 T_uncertainty = sqrt(T_uncertainty);
493
494 double R_uncertainty = 0.0;
495 R_uncertainty += covariance_posori(3, 3) * covariance_posori(3, 3);
496 R_uncertainty += covariance_posori(4, 4) * covariance_posori(4, 4);
497 R_uncertainty += covariance_posori(5, 5) * covariance_posori(5, 5);
498 R_uncertainty = sqrt(R_uncertainty);
499
500 // Helper lambda for CEP-based quality calculation (DRY principle)
501 constexpr double max_allowable_cep = 0.15; // 15cm CEP threshold
502 auto calculate_cep_quality = [max_allowable_cep](double t_uncertainty) -> double
503 {
504 return std::max(0.0, 100.0 * (1.0 - t_uncertainty / max_allowable_cep));
505 };
506
507 // Get used features map from VioManager if not provided and NOT RESETTING!!
508 double calculated_quality;
509 bool is_during_initialization = ((!vio_manager || !vio_manager->initialized()) && (!is_resetting.load(std::memory_order_acquire)));
510 const bool throttling_active =
511 (vio_manager && vio_manager->initialized() &&
512 vio_state.load(std::memory_order_acquire) == VIO_STATE_OK &&
513 (!non_static.load(std::memory_order_acquire) || !has_acc_jerk.load(std::memory_order_acquire)));
514
515 if (!is_during_initialization && !throttling_active)
516 {
517 // Calculate quality using the new feature-based method
518 calculated_quality = calcQuality(used_features_map, SLAM_FEATS, state);
519 }
520 else
521 {
522 // WE NEED THIS FOR ARMING IN POSITION
523 // FIX: During initialization or when throttling frames, use CEP-based quality.
524 // Throttling reduces feature updates; CEP avoids falsely penalizing quality.
525 // Use already-calculated uncertainties to avoid redundant operations
526
527 calculated_quality = calculate_cep_quality(T_uncertainty);
528
529 if (en_debug && is_during_initialization)
530 {
531 printf("[QUALITY_INIT] VIO initializing - CEP quality: %.1f (features=%d, T_unc=%.4f m)\n",
532 calculated_quality, vio_packet.n_feature_points, T_uncertainty);
533 }
534 else if (en_debug && throttling_active)
535 {
536 printf("[QUALITY_THROTTLE] Using CEP quality during throttling: %.1f (T_unc=%.4f m, static=%d, acc_no_jerk=%d)\n",
537 calculated_quality, T_uncertainty,
538 !non_static.load(std::memory_order_relaxed),
539 !has_acc_jerk.load(std::memory_order_relaxed));
540 }
541 }
542
543 // Ensure calculated quality is within bounds
544 calculated_quality = std::max(0.0, std::min(100.0, calculated_quality));
545
546 // Check for quality issues using ACTUAL calculated quality (not hysteresis-filtered)
547 // Only perform this check if VIO is initialized and past grace period
548 // This check monitors if quality has been consistently bad for too long
549 if (vio_manager && vio_manager->initialized())
550 {
551 double ts_threshold = auto_reset_max_v_cov_timeout_s;
552 if (calculated_quality >= 1)
553 {
554 last_good_qual_ts = vio_packet.timestamp_ns;
555 }
556
557 double qual_ts = (vio_packet.timestamp_ns - last_good_qual_ts) * 1e-9;
558 if (qual_ts > ts_threshold)
559 {
560 fprintf(stderr, "ERROR: actual quality was bad for too long!\n");
561 vio_error_codes |= ERROR_CODE_NOT_STATIONARY;
562 }
563 }
564
565 // ========================================================================
566 // QUALITY HYSTERESIS SYSTEM - State Machine Approach
567 // ========================================================================
568 // States:
569 // 1. INITIAL: Report quality as-is (no filtering) on first run after reset
570 // 2. BAD: Quality degraded - apply strict hysteresis to recover
571 // 3. GOOD: Quality stable - apply hysteresis to prevent false alarms
572 //
573 // Transitions:
574 // - INITIAL → BAD: When quality drops below 20 for 10 consecutive samples
575 // - BAD → GOOD: When quality above 40 for 60 consecutive samples
576 // - GOOD → BAD: When quality below 20 for 45 consecutive samples
577 // - Any state → INITIAL: On VIO reset or FAILED state
578 // ========================================================================
579
580 enum QualityState
581 {
582 INITIAL,
583 BAD,
584 GOOD
585 };
586 static QualityState quality_state = INITIAL;
587 static int consecutive_above_40 = 0;
588 static int consecutive_below_20 = 0;
589 static uint32_t last_quality_reset_count = reset_num_counter.load();
590
591 // Reset to INITIAL state on VIO reset OR when entering FAILED state
592 bool should_reset_quality_state = (last_quality_reset_count != current_reset_count) ||
593 (vio_state.load() == VIO_STATE_FAILED);
594
595 if (should_reset_quality_state)
596 {
597 quality_state = INITIAL;
598 consecutive_above_40 = 0;
599 consecutive_below_20 = 0;
600 last_quality_reset_count = current_reset_count;
601
602 if (en_debug)
603 {
604 printf("[QUALITY] RESET to INITIAL state - will report quality as-is (reset_count=%u)\n",
605 current_reset_count);
606 }
607 }
608
609 // State machine transitions based on current quality
610 if (!should_reset_quality_state)
611 {
612 switch (quality_state)
613 {
614 case INITIAL:
615 // Report quality as-is, but monitor for degradation
616 if (calculated_quality <= quality_low_thresh_initial)
617 {
618 consecutive_below_20++;
619 if (consecutive_below_20 >= quality_initial_to_bad_count)
620 {
621 quality_state = BAD;
622 consecutive_below_20 = 0;
623 consecutive_above_40 = 0;
624 if (en_debug)
625 printf("[QUALITY] Transition: INITIAL → BAD (quality degraded)\n");
626 }
627 }
628 else
629 {
630 consecutive_below_20 = 0;
631 }
632 if (calculated_quality > quality_high_thresh)
633 {
634 consecutive_above_40++;
635 if (consecutive_above_40 >= quality_initial_to_good_count)
636 {
637 quality_state = GOOD;
638 consecutive_above_40 = 0;
639 consecutive_below_20 = 0;
640 std::cout << "[QUALITY] Transition: INITIAL → GOOD (" << quality_initial_to_good_count << " samples > " << quality_high_thresh << ")" << std::endl;
641 }
642 }
643 else
644 {
645 consecutive_above_40 = 0;
646 }
647 break;
648
649 case BAD:
650 // Quality is bad - require strong evidence to recover
651 if (calculated_quality > quality_high_thresh)
652 {
653 consecutive_above_40++;
654 if (consecutive_above_40 >= quality_bad_to_good_count)
655 {
656 quality_state = GOOD;
657 consecutive_above_40 = 0;
658 consecutive_below_20 = 0;
659 std::cout << "[QUALITY] Transition: BAD → GOOD (" << quality_bad_to_good_count << " samples > " << quality_high_thresh << ")" << std::endl;
660 }
661 }
662 else
663 {
664 consecutive_above_40 = 0;
665 }
666 break;
667
668 case GOOD:
669 // Quality is good - require strong evidence of degradation
670 if (calculated_quality <= quality_low_thresh_good)
671 {
672 consecutive_below_20++;
673 if (consecutive_below_20 >= quality_good_to_bad_count)
674 {
675 quality_state = BAD;
676 consecutive_below_20 = 0;
677 consecutive_above_40 = 0;
678 std::cout << "[QUALITY] Transition: GOOD → BAD (" << quality_good_to_bad_count << " samples ≤ " << quality_low_thresh_good << ")" << std::endl;
679 }
680 }
681 else
682 {
683 consecutive_below_20 = 0;
684 }
685 break;
686 }
687 }
688
689 // Determine reported quality based on state
690 int reported_quality;
691 switch (quality_state)
692 {
693 case INITIAL:
694 // Report CEP quality in INITIAL state ONLY if ZUPT update occurred
695 // Otherwise report 0 until system proves itself
696 {
697 if (vio_manager && vio_manager->get_did_zupt_update())
698 {
699 const double cep_quality = calculate_cep_quality(T_uncertainty);
700 reported_quality = static_cast<int>(cep_quality);
701
702 if (en_debug)
703 {
704 printf("[QUALITY_INIT] INITIAL state - CEP quality: %.1f (T_unc=%.4f m) [ZUPT done]\n",
705 cep_quality, T_uncertainty);
706 }
707 }
708 else
709 {
710 reported_quality = 0;
711
712 if (en_debug)
713 {
714 printf("[QUALITY_INIT] INITIAL state - Quality: 0 [waiting for ZUPT]\n");
715 }
716 }
717 }
718 break;
719 case BAD:
720 // Report 0 until quality recovers
721 reported_quality = 0;
722 break;
723 case GOOD:
724 // Report calculated quality (feature-based or CEP-based depending on VIO state)
725 reported_quality = static_cast<int>(calculated_quality);
726 break;
727 }
728
729 // Ensure reported quality is within bounds
730 reported_quality = std::max(0, std::min(100, reported_quality));
731
732 // Debug logging
733 if (en_debug && (quality_state != GOOD || reported_quality < 10))
734 {
735 const char *state_str = (quality_state == INITIAL) ? "INITIAL" : (quality_state == BAD) ? "BAD"
736 : "GOOD";
737 printf("[QUALITY] State: %s, Reported: %d, Calculated: %.1f, Consec(>40): %d, Consec(≤20): %d\n",
738 state_str, reported_quality, calculated_quality,
739 consecutive_above_40, consecutive_below_20);
740 }
741
742 // For error detection and auto-reset, use the ACTUAL calculated quality, not the hysteresis-filtered one
743 // This prevents false auto-resets during the initial warm-up period
744 int quality_for_error_detection = static_cast<int32_t>(calculated_quality);
745 if (quality_for_error_detection > 100)
746 quality_for_error_detection = 100;
747 if (quality_for_error_detection < 0)
748 quality_for_error_detection = 0;
749
750 // Determine if we're in the grace period after initialization/reset
751 bool in_grace_period = false;
752 if (vio_manager && vio_manager->initialized())
753 {
754 const int64_t now_ns = _apps_time_monotonic_ns();
755
756 // If we just entered OK (or after a reset), start the grace window.
757 if ((last_good_state_ns == 0 || vio_packet.n_feature_points < auto_reset_min_features) && wait_for_steady_init)
758 {
759 last_good_state_ns = now_ns;
760 }
761
762 // Duration since OK started
763 const int64_t dt_ok_ns = now_ns - last_good_state_ns;
764 const int64_t grace_ns = (int64_t)(ok_state_grace_timeout_s * 1e9);
765
766 in_grace_period = (dt_ok_ns < grace_ns && wait_for_steady_init);
767 }
768
769 // ========================================================================
770 // STATE DETERMINATION
771 // ========================================================================
772 // Priority order:
773 // 1. INITIALIZING - if VIO manager not initialized yet or resetting
774 // 2. FAILED - if auto-reset conditions met (only when fully initialized)
775 // 3. OK - normal operation
776 // ========================================================================
777
778 // FIRST: Check if we're still initializing (vio_manager not ready OR currently resetting)
779 // Note: Once vio_manager is initialized, we report OK state externally, even during grace period
780 if (!vio_manager || !vio_manager->initialized())
781 {
782 // System is INITIALIZING - VIO manager not ready yet
783
784 vio_state.store(VIO_STATE_INITIALIZING, std::memory_order_release);
785 if (vio_manager->get_did_zupt_update())
786 {
787 vio_packet.state = VIO_STATE_OK;
788 }
789 else
790 {
791 vio_packet.state = VIO_STATE_INITIALIZING;
792 }
793 // During initialization, use actual CEP quality for arming checks
794 // This allows position-based arming to work correctly
795 vio_packet.quality = reported_quality;
796
797 if (en_debug)
798 {
799 printf("[STATE] VIO_STATE_INITIALIZING - vio_manager: %s, initialized: %s, quality: %d\n",
800 vio_manager ? "exists" : "null",
801 (vio_manager && vio_manager->initialized()) ? "yes" : "no",
802 vio_packet.quality);
803 }
804 }
805 // Handle resetting state separately - report as INITIALIZING but with different internal handling
806 else if (is_resetting.load(std::memory_order_acquire))
807 {
808 // System is resetting - report as INITIALIZING to external systems
809 vio_packet.state = VIO_STATE_INITIALIZING;
810 vio_state.store(VIO_STATE_INITIALIZING, std::memory_order_release);
811 vio_packet.quality = 0; // Quality is invalid during reset
812
813 if (en_debug)
814 {
815 printf("[STATE] VIO_STATE_INITIALIZING (resetting) - quality: %d\n", vio_packet.quality);
816 }
817 }
818 // SECOND: Check for auto-reset conditions (only if initialized and not in grace period)
819 else if (!in_grace_period &&
820 should_auto_reset(state, quality_for_error_detection, vio_packet.n_feature_points,
821 V_uncertainty, yawrate, current_velocity,
822 vio_packet.vel_imu_wrt_vio[0], vio_packet.vel_imu_wrt_vio[1]))
823 {
824 // Auto-reset conditions met - system has FAILED
825 fprintf(stderr, "WARNING: Auto-reset conditions detected! Quality: %d, Features: %d, V_uncertainty: %f\n",
826 quality_for_error_detection, vio_packet.n_feature_points, V_uncertainty);
827
828 vio_packet.quality = -1;
829 vio_packet.state = VIO_STATE_FAILED;
830 vio_state.store(VIO_STATE_FAILED, std::memory_order_release);
831
832 // CRITICAL: Request the actual reset
833 if (!reset_requested.load(std::memory_order_acquire))
834 {
835 reset_requested.store(true, std::memory_order_release);
836 fprintf(stderr, "[AUTO_RESET] REQUESTING RESET due to auto-reset conditions\n");
837 }
838 }
839 // THIRD: System is OK - normal operation
840 else
841 {
842 vio_packet.state = VIO_STATE_OK;
843 vio_state.store(VIO_STATE_OK, std::memory_order_release);
844
845 if (in_grace_period)
846 {
847 // During grace period, respect hysteresis while being conservative
848 if (reported_quality == 0)
849 {
850 vio_packet.quality = 0; // Respect hysteresis: system needs to prove itself
851 }
852 else
853 {
854 // Quality has passed hysteresis, but still in grace period - cap conservatively
855 vio_packet.quality = std::min(reported_quality, 15);
856 }
857
858 if (en_debug)
859 {
860 printf("[GRACE_PERIOD] In grace period: reported_quality=%d, vio_packet.quality=%d\n",
861 reported_quality, vio_packet.quality);
862 }
863 }
864 else
865 {
866 wait_for_steady_init = false;
867 // Set the reported quality with hysteresis applied
868 vio_packet.quality = reported_quality;
869 }
870 }
871
872 // FRAME
873 vio_packet.frame = 0; // Set appropriate frame value
874 past_state = state;
875
876 // publish the packet
877 // if (pipe_server_get_num_clients(SIMPLE_CH) > 0)
878 pipe_server_write(SIMPLE_CH, (char *)&vio_packet, sizeof(vio_data_t));
879
880 if (pipe_server_get_num_clients(EXTENDED_CH) > 0)
881 {
882 ext_vio_data_t ext_vio_packet;
883 ext_vio_packet.v = vio_packet;
884 ext_vio_packet.n_total_features = 0;
885
886 double current_timestamp = state->_timestamp;
887 auto timestamp_iter = used_features_map.find(current_timestamp);
888
889 if (timestamp_iter != used_features_map.end())
890 {
891 const auto &features = timestamp_iter->second;
892
893 // Group features by camera ID
894 std::map<int, std::vector<std::shared_ptr<ov_core::Feature>>> features_by_camera;
895 for (const auto &feature : features)
896 {
897 // Get camera ID from feature measurements
898 for (const auto &cam_meas : feature->timestamps)
899 {
900 int cam_id = static_cast<int>(cam_meas.first);
901 features_by_camera[cam_id].push_back(feature);
902 }
903 }
904
905 ov_type::LandmarkRepresentation::Representation msckf_ref = state->_options.feat_rep_msckf;
906
907 // Process each camera's features
908 for (const auto &camera_pair : features_by_camera)
909 {
910 int cam_id = camera_pair.first;
911 const auto &cam_features = camera_pair.second;
912
913 Eigen::Matrix3d R_I_C = ov_core::quat_2_Rot(state->_calib_IMUtoCAM[cam_id]->quat()).transpose();
914 Eigen::Vector3d p_I_C = state->_calib_IMUtoCAM[cam_id]->pos();
915
916 for (const auto &feature : cam_features)
917 {
918 // Check if this feature has measurements for this camera at current timestamp
919 if (feature->timestamps.find(cam_id) == feature->timestamps.end() ||
920 feature->uvs.find(cam_id) == feature->uvs.end())
921 {
922 continue;
923 }
924
925 // Find the measurement index for the current timestamp
926 const auto &timestamps = feature->timestamps.at(cam_id);
927 auto time_iter = std::find(timestamps.begin(), timestamps.end(), current_timestamp);
928 if (time_iter == timestamps.end())
929 {
930 continue;
931 }
932
933 size_t meas_idx = std::distance(timestamps.begin(), time_iter);
934 const auto &uv_meas = feature->uvs.at(cam_id)[meas_idx];
935
936 // check if feature id found in SLAM set
937 if (SLAM_FEATS.find(feature->featid) != SLAM_FEATS.end())
938 {
939 if (ext_vio_packet.n_total_features >= VIO_MAX_REPORTED_FEATURES)
940 break;
941
942 int i_global = ext_vio_packet.n_total_features++;
943
944 ext_vio_packet.features[i_global].id = feature->featid;
945 ext_vio_packet.features[i_global].cam_id = cam_id;
946
947 // Extract pixel location from UV measurement
948 ext_vio_packet.features[i_global].pix_loc[0] = uv_meas(0);
949 ext_vio_packet.features[i_global].pix_loc[1] = uv_meas(1);
950
951 Eigen::Vector3d p_FinG;
952 if (SLAM_FEATS[feature->featid]->_feat_representation == ov_type::LandmarkRepresentation::Representation::GLOBAL_3D)
953 {
954 p_FinG = SLAM_FEATS[feature->featid]->get_xyz(true); // GRAB FEJ VALUE --> AVOID SNAP BACK EFFECT IN VIZ
955 p_FinG = ov2frd * p_FinG;
956 }
957 else if (SLAM_FEATS[feature->featid]->_feat_representation == ov_type::LandmarkRepresentation::Representation::ANCHORED_MSCKF_INVERSE_DEPTH)
958 {
959 // FIX THIS --> THIS IS INCORRECT, NEED TO GRAB ROTATIONS AT ANCHOR FRAME
960 Eigen::Vector3d p_FinA = SLAM_FEATS[feature->featid]->get_xyz(false);
961 Eigen::Vector3d T_I_C(0, 0, 0); // translation from imu to camera position
962 p_FinG = R_I_G * (R_I_C * p_FinA + p_I_C) + p_I_G;
963 }
964 else
965 {
966 printf("[WARNING] SLAM feat representation: %d not recognized, 3D point locations are likely invalid\n", SLAM_FEATS[feature->featid]->_feat_representation);
967 }
968
969 ext_vio_packet.features[i_global].tsf[0] = p_FinG[0];
970 ext_vio_packet.features[i_global].tsf[1] = p_FinG[1];
971 ext_vio_packet.features[i_global].tsf[2] = p_FinG[2];
972
973 ext_vio_packet.features[i_global].point_quality = HIGH;
974 }
975 // if not found in SLAM, then it is an MSCKF feature
976 else
977 {
978 if (ext_vio_packet.n_total_features >= VIO_MAX_REPORTED_FEATURES)
979 break;
980
981 int i_global = ext_vio_packet.n_total_features++;
982
983 ext_vio_packet.features[i_global].id = feature->featid;
984 ext_vio_packet.features[i_global].cam_id = cam_id;
985
986 // Extract pixel location from UV measurement
987 ext_vio_packet.features[i_global].pix_loc[0] = uv_meas(0);
988 ext_vio_packet.features[i_global].pix_loc[1] = uv_meas(1);
989
990 Eigen::Vector3d p_FinA = feature->p_FinA;
991 Eigen::Vector3d p_FinG = feature->p_FinG;
992
993 if (msckf_ref == ov_type::LandmarkRepresentation::Representation::GLOBAL_3D)
994 {
995 p_FinG = ov2frd * p_FinG;
996 }
997 else if (msckf_ref == ov_type::LandmarkRepresentation::Representation::ANCHORED_MSCKF_INVERSE_DEPTH)
998 {
999 // FIX THIS --> THIS IS INCORRECT, NEED TO GRAB ROTATIONS AT ANCHOR FRAME
1000 p_FinG = R_I_G * (R_I_C * p_FinA + p_I_C) + p_I_G;
1001 }
1002 else
1003 {
1004 printf("[WARNING] MSCKF feat representation: %s not recognized, 3D point locations are likely invalid\n", ov_type::LandmarkRepresentation::as_string(msckf_ref).c_str());
1005 }
1006 ext_vio_packet.features[i_global].tsf[0] = p_FinG[0];
1007 ext_vio_packet.features[i_global].tsf[1] = p_FinG[1];
1008 ext_vio_packet.features[i_global].tsf[2] = p_FinG[2];
1009
1010 ext_vio_packet.features[i_global].point_quality = MEDIUM;
1011 }
1012 }
1013 }
1014 }
1015
1016 pipe_server_write(EXTENDED_CH, (char *)&ext_vio_packet, sizeof(ext_vio_data_t));
1017 }
1018}
1019
1020/**
1021 * @brief Check if auto-reset should be triggered
1022 *
1023 * This function evaluates the current VIO state and various error conditions
1024 * to determine if an automatic reset should be triggered. It implements the
1025 * same logic as the legacy code but in a more modular way.
1026 *
1027 * @param state Current VIO state
1028 * @param quality Current quality value
1029 * @param n_features Number of tracked features
1030 * @param V_uncertainty Velocity uncertainty
1031 * @return true if auto-reset should be triggered, false otherwise
1032 */
1033bool Publisher::should_auto_reset(std::shared_ptr<ov_msckf::State> state,
1034 int quality,
1035 int n_features,
1036 double V_uncertainty,
1037 double yawrate,
1038 double current_velocity,
1039 double vel_x,
1040 double vel_y)
1041{
1042 // Only check for auto-reset if enabled and system is stable
1043 if (!en_auto_reset)
1044 {
1045 return false;
1046 }
1047
1048 // FIX: Reset static variables when reset_num_counter changes
1049 // This prevents stale state from previous runs affecting current run
1050 static int64_t last_good_qual_ts = 0;
1051 static int64_t last_good_feat_ts = 0;
1052 static bool wait_for_features = true;
1053 static uint32_t last_reset_check_count = 0;
1054
1055 const uint32_t current_reset_count = reset_num_counter.load(std::memory_order_acquire);
1056 if (last_reset_check_count != current_reset_count)
1057 {
1058 // Reset occurred - clear all static tracking variables
1059 const int64_t current_ts_ns = state->_timestamp * 1e9;
1060 last_good_qual_ts = current_ts_ns;
1061 last_good_feat_ts = current_ts_ns;
1062 wait_for_features = true;
1063 last_reset_check_count = current_reset_count;
1064
1065 if (en_debug)
1066 {
1067 printf("[AUTO_RESET] Static variables reset for new VIO cycle (reset_count=%u)\n",
1068 current_reset_count);
1069 }
1070 }
1071
1072 // Check if VIO manager is in a bad state
1073 constexpr bool vio_manager_bad = false; // FUTURE IMPLEMENTATION WITH SfM
1074
1075 // CRITICAL FIX: Remove instant quality check - conflicts with hysteresis
1076 // Quality hysteresis reports 0 during transition, but actual quality may be good
1077 // This was causing resets when quality improves!
1078 // Only check for SUSTAINED bad quality, not instant quality
1079 bool stable_quality_bad = false;
1080
1081 // Check for stable quality issues (quality bad for extended period)
1082 if (quality >= 1)
1083 {
1084 last_good_qual_ts = state->_timestamp * 1e9;
1085 }
1086 const double qual_elapsed_s = (state->_timestamp * 1e9 - last_good_qual_ts) * 1e-9;
1087 if (qual_elapsed_s > auto_reset_max_v_cov_timeout_s)
1088 {
1089 stable_quality_bad = true;
1090 }
1091
1092 // Check feature conditions
1093 bool stable_features_bad = false;
1094
1095 if (wait_for_features)
1096 {
1097 if (n_features > auto_reset_min_features)
1098 {
1099 last_good_feat_ts = state->_timestamp * 1e9;
1100 wait_for_features = false;
1101 }
1102 }
1103 else
1104 {
1105 if (n_features > auto_reset_min_features)
1106 {
1107 last_good_feat_ts = state->_timestamp * 1e9;
1108 }
1109
1110 double ts = (state->_timestamp * 1e9 - last_good_feat_ts) * 1e-9;
1112 {
1113 stable_features_bad = true;
1114 wait_for_features = true;
1115 }
1116 }
1117
1118 // Check velocity conditions using passed values
1119 bool too_fast = current_velocity > auto_reset_max_velocity;
1120 bool too_uncertain = is_armed && V_uncertainty > auto_reset_max_v_cov_instant;
1121
1122 // Check for excessive spinning using passed yawrate
1123 // FIX: Reset spin tracking on VIO reset
1124 static int64_t start_spin_time = 0;
1125 static bool spinning_detected = false;
1126 static uint32_t last_spin_reset_count = 0;
1127
1128 if (last_spin_reset_count != current_reset_count)
1129 {
1130 start_spin_time = state->_timestamp * 1e9;
1131 spinning_detected = false;
1132 last_spin_reset_count = current_reset_count;
1133 }
1134
1135 bool too_much_spinning = false;
1136
1137 // Use the passed yawrate value and actual velocity components
1138 const bool spinning_in_place = (fabs(yawrate) > fast_yaw_thresh &&
1139 fabs(vel_x) <= 1.0 &&
1140 fabs(vel_y) <= 1.0);
1141
1142 if (!spinning_in_place)
1143 {
1144 start_spin_time = state->_timestamp * 1e9;
1145 spinning_detected = false;
1146 }
1147 else if (!spinning_detected)
1148 {
1149 const double spin_duration = (state->_timestamp * 1e9 - start_spin_time) * 1e-9;
1150 if (spin_duration > fast_yaw_timeout_s)
1151 {
1152 too_much_spinning = true;
1153 spinning_detected = true;
1154 }
1155 }
1156
1157 // Return true if any condition is met
1158 // CRITICAL FIX: Removed quality_bad - was causing resets during hysteresis transitions
1159 return vio_manager_bad || stable_quality_bad ||
1160 stable_features_bad || too_fast || too_uncertain || too_much_spinning;
1161}
1162
1163/**
1164 * @brief Calculate Quality of the VIO state
1165 *
1166 * This function computes the quality score based on the features used at
1167 * the current timestamp. The quality calculation considers:
1168 * 1. Grid distribution: 5x5 grid per camera with 50 features target (2 per grid)
1169 * 2. Active SLAM features: weighted by covariance largest eigenvalue and quality field
1170 * 3. MSCKF features: weighted by quality field and number of measurements
1171 *
1172 * @param used_features_map Map of used features at the current timestamp
1173 * @param slam_features Map of SLAM features from the state
1174 * @param state Current VIO state for covariance access
1175 * @return Quality score (0-100, higher is better)
1176 */
1177double Publisher::calcQuality(const std::map<double, std::vector<std::shared_ptr<ov_core::Feature>>> &used_features_map,
1178 std::unordered_map<size_t, std::shared_ptr<ov_type::Landmark>> &slam_features,
1179 std::shared_ptr<ov_msckf::State> state)
1180{
1181 // Constants for quality calculation
1182 const int GRID_SIZE = 5; // 5x5 grid
1183 const int TARGET_FEATURES_PER_CAM = 50; // 50 features per camera
1184 const int TARGET_FEATURES_PER_GRID = TARGET_FEATURES_PER_CAM / (GRID_SIZE * GRID_SIZE); // 2 features per grid
1185 const int CLONES = 11; // Number of clones for MSCKF weighting
1186 const double MAX_GRID_SCORE = 100.0; // Maximum score when 50% of grids are filled
1187 const double GRID_FILL_THRESHOLD = 0.5; // 50% of grids should be filled for max score
1188 const int en_qual_debug = 0;
1189
1190 double total_quality = 0.0;
1191 int total_grids_filled = 0;
1192 int total_grids = 0;
1193
1194 // Debug: Print total features available
1195 if (en_qual_debug)
1196 {
1197 printf("[QUALITY_DEBUG] Total features in used_features_map: %zu\n", used_features_map.size());
1198 printf("[QUALITY_DEBUG] Total SLAM features in state: %zu\n", slam_features.size());
1199 }
1200
1201 // Get the current timestamp to find the most recent features
1202 double current_timestamp = state->_timestamp;
1203 if (en_qual_debug)
1204 printf("[QUALITY_DEBUG] Current timestamp: %.6f\n", current_timestamp);
1205
1206 // Find the features for the current timestamp
1207 auto timestamp_iter = used_features_map.find(current_timestamp);
1208 if (timestamp_iter == used_features_map.end())
1209 {
1210 // No features for current timestamp, return low quality
1211 if (en_qual_debug)
1212 printf("[QUALITY_DEBUG] No features found for current timestamp, returning low quality\n");
1213 return 10.0;
1214 }
1215
1216 const auto &features = timestamp_iter->second;
1217 if (en_qual_debug)
1218 printf("[QUALITY_DEBUG] Features found for current timestamp: %zu\n", features.size());
1219
1220 // Group features by camera ID
1221 std::map<int, std::vector<std::shared_ptr<ov_core::Feature>>> features_by_camera;
1222 for (const auto &feature : features)
1223 {
1224 // Get camera ID from feature measurements
1225 for (const auto &cam_meas : feature->timestamps)
1226 {
1227 int cam_id = static_cast<int>(cam_meas.first);
1228 features_by_camera[cam_id].push_back(feature);
1229 }
1230 }
1231
1232 if (en_qual_debug)
1233 {
1234 printf("[QUALITY_DEBUG] Features grouped by camera:\n");
1235 for (const auto &cam_pair : features_by_camera)
1236 {
1237 printf("[QUALITY_DEBUG] Camera %d: %zu features\n", cam_pair.first, cam_pair.second.size());
1238 }
1239 }
1240
1241 // Count active SLAM and MSCKF features
1242 int total_active_slam = 0;
1243 int total_active_msckf = 0;
1244
1245 // Process each camera's features
1246 for (const auto &camera_pair : features_by_camera)
1247 {
1248 int cam_id = camera_pair.first;
1249 const auto &cam_features = camera_pair.second;
1250
1251 if (en_qual_debug)
1252 printf("[QUALITY_DEBUG] Processing camera %d with %zu features\n", cam_id, cam_features.size());
1253
1254 // Initialize 5x5 grid for this camera
1255 std::vector<std::vector<int>> grid(GRID_SIZE, std::vector<int>(GRID_SIZE, 0));
1256 std::vector<std::vector<double>> grid_quality(GRID_SIZE, std::vector<double>(GRID_SIZE, 0.0));
1257
1258 int cam_slam_count = 0;
1259 int cam_msckf_count = 0;
1260
1261 // Process features for this camera
1262 for (const auto &feature : cam_features)
1263 {
1264 // Get feature position in image for this camera
1265 if (feature->timestamps.find(cam_id) != feature->timestamps.end() &&
1266 !feature->uvs_norm.empty() &&
1267 feature->uvs_norm.find(cam_id) != feature->uvs_norm.end() &&
1268 !feature->uvs_norm.at(cam_id).empty())
1269 {
1270
1271 const auto &uv_norm = feature->uvs_norm.at(cam_id).back();
1272 if (en_qual_debug)
1273 printf("[QUALITY_DEBUG] Feature %zu - Camera %d - UV Norm: (%.3f, %.3f)\n",
1274 feature->featid, cam_id, uv_norm(0), uv_norm(1));
1275
1276 // Convert normalized coordinates to grid coordinates
1277 // Assuming normalized coordinates are in [-1, 1] range
1278 int grid_x = static_cast<int>((uv_norm(0) + 1.0) * 0.5 * GRID_SIZE);
1279 int grid_y = static_cast<int>((uv_norm(1) + 1.0) * 0.5 * GRID_SIZE);
1280
1281 // Clamp to valid grid range
1282 grid_x = std::max(0, std::min(GRID_SIZE - 1, grid_x));
1283 grid_y = std::max(0, std::min(GRID_SIZE - 1, grid_y));
1284
1285 // Calculate feature quality based on whether it's SLAM or MSCKF
1286 double feature_quality = 0.0;
1287
1288 // Check if this is a SLAM feature
1289 if (slam_features.find(feature->featid) != slam_features.end())
1290 {
1291 // This is an active SLAM feature
1292 cam_slam_count++;
1293 auto slam_feat = slam_features[feature->featid];
1294
1295 if (en_qual_debug)
1296 printf("[QUALITY_DEBUG] Feature %zu (SLAM) - Grid[%d][%d] - Quality field: %.3f, uniq_cam: %d, anchor_cam: %d\n",
1297 feature->featid, grid_x, grid_y, slam_feat->_quality, slam_feat->_unique_camera_id, slam_feat->_anchor_cam_id);
1298 // ATTENTION: COULD SOLVE THIS IN A BATCH MANNER, BUT FOR NOW DO THE FOR LOOP DIRECTLY
1299 // Get covariance for this SLAM feature
1300 Eigen::MatrixXd slam_cov;
1301 if (slam_feat->id() >= 0)
1302 {
1303 // Get the covariance block for this feature
1304 std::vector<std::shared_ptr<ov_type::Type>> slam_feat_vec = {slam_feat};
1305 slam_cov = ov_msckf::StateHelper::get_marginal_covariance(state, slam_feat_vec);
1306 }
1307 else
1308 {
1309 // Feature not in state yet, use default covariance
1310 slam_cov = Eigen::MatrixXd::Identity(3, 3) * 1.4 * 1.4; // Default multiply by slam sigma pixel^2 --> UNITS TBD
1311 }
1312
1313 // Calculate largest eigenvalue of covariance
1314 Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> eigensolver(slam_cov);
1315 double max_eigenvalue = eigensolver.eigenvalues().maxCoeff();
1316
1317 // Quality based on covariance (smaller is better)
1318 // double cov_quality = std::max(0.0, 1.0 - max_eigenvalue);
1319 const double sigma_ref = 1.4; // Reference sigma value for normalization
1320 double cov_quality = 1.0 / (1.0 + max_eigenvalue / sigma_ref);
1321
1322 // Combine with feature quality field (if available)
1323 double feat_quality_field = (slam_feat->_quality >= 0) ? slam_feat->_quality : 0.0;
1324
1325 // Weight SLAM features more heavily
1326 feature_quality = 0.5 * cov_quality + 0.5 * feat_quality_field;
1327 // feature->quality = feature_quality; // Store in feature
1328
1329 if (en_qual_debug)
1330 printf("[QUALITY_DEBUG] SLAM Feature %zu - Max eigenvalue: %.6f, Cov quality: %.3f, Final quality: %.3f\n",
1331 feature->featid, max_eigenvalue, cov_quality, feature_quality);
1332 }
1333 else
1334 {
1335 // This is an MSCKF feature
1336 cam_msckf_count++;
1337
1338 // Count number of measurements for this feature
1339 int num_measurements = 0;
1340 for (const auto &cam_meas : feature->timestamps)
1341 {
1342 num_measurements += static_cast<int>(cam_meas.second.size());
1343 }
1344
1345 // Weight down if measurements/clones < 1
1346 double measurement_weight = 1.0;
1347 if (num_measurements < CLONES)
1348 {
1349 measurement_weight = static_cast<double>(num_measurements) / CLONES;
1350 }
1351
1352 // Use feature quality field (if available)
1353 double feat_quality_field = (feature->quality >= 0) ? feature->quality : 0.0;
1354
1355 // MSCKF features weighted less than SLAM features
1356 feature_quality = 0.4 * feat_quality_field * measurement_weight;
1357
1358 if (en_qual_debug)
1359 printf("[QUALITY_DEBUG] Feature %zu (MSCKF) - Grid[%d][%d] - Quality field: %.3f, Measurements: %d, Weight: %.3f, Final quality: %.3f\n",
1360 feature->featid, grid_x, grid_y, feat_quality_field, num_measurements, measurement_weight, feature_quality);
1361 }
1362
1363 // Add feature to grid
1364 grid[grid_y][grid_x]++;
1365 grid_quality[grid_y][grid_x] += feature_quality;
1366 }
1367 }
1368
1369 total_active_slam += cam_slam_count;
1370 total_active_msckf += cam_msckf_count;
1371
1372 if (en_qual_debug)
1373 printf("[QUALITY_DEBUG] Camera %d summary - SLAM: %d, MSCKF: %d\n", cam_id, cam_slam_count, cam_msckf_count);
1374
1375 // Calculate grid distribution score for this camera
1376 int grids_with_features = 0;
1377 double camera_grid_score = 0.0;
1378
1379 if (en_qual_debug)
1380 printf("[QUALITY_DEBUG] Camera %d grid analysis:\n", cam_id);
1381 for (int i = 0; i < GRID_SIZE; i++)
1382 {
1383 for (int j = 0; j < GRID_SIZE; j++)
1384 {
1385 if (grid[i][j] > 0)
1386 {
1387 grids_with_features++;
1388
1389 // Calculate quality for this grid cell
1390 double grid_cell_quality = 0.0;
1391 if (grid[i][j] >= TARGET_FEATURES_PER_GRID)
1392 {
1393 // Grid has enough features, use average quality
1394 grid_cell_quality = grid_quality[i][j] / grid[i][j];
1395 }
1396 else
1397 {
1398 // Grid has insufficient features, penalize
1399 double fill_ratio = std::min(1.0, static_cast<double>(grid[i][j]) / TARGET_FEATURES_PER_GRID);
1400 grid_cell_quality = (grid_quality[i][j] / std::max(1, grid[i][j])) * std::sqrt(fill_ratio);
1401 }
1402 camera_grid_score += grid_cell_quality;
1403
1404 if (en_qual_debug)
1405 printf("[QUALITY_DEBUG] Grid[%d][%d]: %d features, avg quality: %.3f\n",
1406 i, j, grid[i][j], grid_quality[i][j] / grid[i][j]);
1407 }
1408 }
1409 }
1410
1411 // Calculate grid distribution score
1412 double grid_fill_ratio = static_cast<double>(grids_with_features) / (GRID_SIZE * GRID_SIZE);
1413 double grid_distribution_score = 0.0;
1414
1415 if (grid_fill_ratio >= GRID_FILL_THRESHOLD)
1416 {
1417 // 50% or more grids filled, give maximum score
1418 grid_distribution_score = MAX_GRID_SCORE;
1419 }
1420 else
1421 {
1422 // Linear interpolation for partial fill
1423 grid_distribution_score = (grid_fill_ratio / GRID_FILL_THRESHOLD) * MAX_GRID_SCORE;
1424 }
1425
1426 // Combine grid distribution with feature quality
1427 double camera_score = 0.5 * grid_distribution_score + 0.5 * camera_grid_score;
1428
1429 if (en_qual_debug)
1430 printf("[QUALITY_DEBUG] Camera %d scores - Grid fill ratio: %.3f, Grid distribution: %.3f, Grid quality: %.3f, Final camera score: %.3f\n",
1431 cam_id, grid_fill_ratio, grid_distribution_score, camera_grid_score, camera_score);
1432
1433 total_quality += camera_score;
1434 total_grids_filled += grids_with_features;
1435 total_grids += GRID_SIZE * GRID_SIZE;
1436 }
1437
1438 if (en_qual_debug)
1439 printf("[QUALITY_DEBUG] Overall feature counts - Active SLAM: %d, Active MSCKF: %d\n", total_active_slam, total_active_msckf);
1440
1441 // Calculate final quality score
1442 double final_quality = 0.0;
1443 if (!features_by_camera.empty())
1444 {
1445 // Average across all cameras
1446 final_quality = total_quality / features_by_camera.size();
1447 }
1448
1449 // Ensure quality is within bounds
1450 // do not use the averaged one, use the total quality
1451 final_quality = std::max(0.0, std::min(100.0, total_quality));
1452
1453 if (en_qual_debug)
1454 printf("[QUALITY_DEBUG] Final quality calculation - Total quality: %.3f, Num cameras: %zu, Final quality: %.3f\n",
1455 total_quality, features_by_camera.size(), final_quality);
1456
1457 return final_quality;
1458}
1459
1460void Publisher::publishBlank()
1461{
1462 // Zero out the VIO packet
1463 memset(&vio_packet, 0, sizeof(vio_data_t));
1464 vio_packet.magic_number = VIO_MAGIC_NUMBER;
1465 vio_packet.timestamp_ns = _apps_time_monotonic_ns();
1466
1467 // Set error codes for missing sensors
1468 uint32_t packet_error = 0;
1469 if (!is_imu_connected.load())
1470 packet_error |= ERROR_CODE_IMU_MISSING;
1471 if (!is_cam_connected.load())
1472 packet_error |= ERROR_CODE_CAM_MISSING;
1473 vio_packet.error_code = packet_error;
1474
1475 // Indicate failed state
1476 vio_packet.quality = -1;
1477 vio_packet.state = VIO_STATE_FAILED;
1478
1479 // Publish blank packet on simple channel
1480 pipe_server_write(SIMPLE_CH, (char *)&vio_packet, sizeof(vio_data_t));
1481}
Housekeeping and data publishing for VOXL OpenVINS.
int quality_initial_to_bad_count
Consecutive samples for INITIAL→BAD transition.
Definition VoxlVars.cpp:130
int quality_low_thresh_initial
Quality low threshold for INITIAL state.
Definition VoxlVars.cpp:121
int quality_bad_to_good_count
Consecutive samples for BAD→GOOD transition.
Definition VoxlVars.cpp:136
float fast_yaw_thresh
Fast yaw threshold.
Definition VoxlVars.cpp:111
int en_auto_reset
Enable automatic reset functionality.
Definition VoxlVars.cpp:78
float auto_reset_max_velocity
Maximum velocity threshold for auto reset.
Definition VoxlVars.cpp:81
voxl::FrameTransform frame_transform
Global frame transform instance.
Definition VoxlVars.cpp:183
int quality_good_to_bad_count
Consecutive samples for GOOD→BAD transition.
Definition VoxlVars.cpp:139
float auto_reset_max_v_cov_timeout_s
Timeout duration for velocity covariance reset (seconds)
Definition VoxlVars.cpp:90
float ok_state_grace_timeout_s
Minimum amount of time after initialization that quality is held low (CEP)
Definition VoxlVars.cpp:99
float auto_reset_min_feature_timeout_s
Minimum feature timeout for auto reset (seconds)
Definition VoxlVars.cpp:96
std::atomic< uint32_t > reset_num_counter
Counter which increments on resets.
Definition VoxlVars.cpp:53
float fast_yaw_timeout_s
Fast yaw timeout (seconds)
Definition VoxlVars.cpp:114
int auto_reset_min_features
Minimum number of features for auto reset.
Definition VoxlVars.cpp:93
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:127
int en_debug
Enable debug output.
Definition VoxlVars.cpp:148
float auto_reset_max_v_cov_instant
Maximum velocity covariance for instant reset.
Definition VoxlVars.cpp:84
int quality_low_thresh_good
Quality low threshold for GOOD state.
Definition VoxlVars.cpp:124
int quality_initial_to_good_count
Consecutive samples for INITIAL→GOOD transition.
Definition VoxlVars.cpp:133
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< bool > is_armed
System armed 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 > 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:293
void publish(std::shared_ptr< ov_msckf::State > state, std::map< double, std::vector< std::shared_ptr< ov_core::Feature > > > used_features_map={})
Publish VIO data.
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 V_uncertainty, double yawrate, double current_velocity, double vel_x, double vel_y)
Check if auto-reset should be triggered.
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.
Main namespace for VOXL OpenVINS server components.
const Eigen::Matrix3d & R_OV_FRD()
Get OpenVINS to FRD coordinate frame transformation matrix.
Definition VoxlHK.h:110
Eigen::Matrix< double, 3, 1 > dirtyOmega(const Eigen::Matrix< double, 4, 1 > &q_k, const Eigen::Matrix< double, 4, 1 > &q_km1, double dt)
Calculate dirty angular velocity from quaternions.
Definition VoxlHK.h:81