3333 * Includes
3434 *****************************************************************************/
3535#include " App.h"
36+ #include < cmath>
3637#include < Logging.h>
3738#include < LogSinkPrinter.h>
3839#include < Util.h>
@@ -106,8 +107,8 @@ static const uint32_t JSON_BIRTHMESSAGE_MAX_SIZE = 64U;
106107/* * Buffer size for JSON serialization of combined sensor snapshot. */
107108static const uint32_t JSON_SENSOR_SNAPSHOT_MAX_SIZE = 256U ;
108109
109- /* * Buffer size for JSON serialization of fusion pose. */
110- static const uint32_t JSON_FUSION_POSE_MAX_SIZE = 128U ;
110+ /* * Buffer size for JSON serialization of fusion pose including nested NIS fields . */
111+ static const uint32_t JSON_FUSION_POSE_MAX_SIZE = 320U ;
111112
112113/* * Status send interval in ms. */
113114const uint16_t STATUS_SEND_INTERVAL_MS = 1000U ;
@@ -116,11 +117,10 @@ const uint16_t STATUS_SEND_INTERVAL_MS = 1000U;
116117const uint16_t HOST_TIMESYNC_INTERVAL_MS = 10000U ;
117118
118119/* Convenience aliases for EKF types. */
119- using ImuMeasurementVector = ExtendedKalmanFilter5D::ImuMeasurementVector;
120- using OdoMeasurementVector = ExtendedKalmanFilter5D::OdoMeasurementVector;
121- using CamMeasurementVector = ExtendedKalmanFilter5D::CamMeasurementVector;
122- using StateVector = ExtendedKalmanFilter5D::StateVector;
123- using StateMatrix = ExtendedKalmanFilter5D::StateMatrix;
120+ using OdoMeasurementVector = ExtendedKalmanFilter4D::OdoMeasurementVector;
121+ using CamMeasurementVector = ExtendedKalmanFilter4D::CamMeasurementVector;
122+ using StateVector = ExtendedKalmanFilter4D::StateVector;
123+ using StateMatrix = ExtendedKalmanFilter4D::StateMatrix;
124124
125125/* *****************************************************************************
126126 * Public Methods
@@ -129,6 +129,7 @@ using StateMatrix = ExtendedKalmanFilter5D::StateMatrix;
129129App::App () :
130130 m_initialDataSent(false ),
131131 m_statusTimer(),
132+ m_hostTimeSyncTimer(),
132133 m_serMuxChannelProvider(Board::getInstance().getRobot().getStream()),
133134 m_timeSync(m_serMuxChannelProvider),
134135 m_lineSensors(m_serMuxChannelProvider),
@@ -141,9 +142,6 @@ App::App() :
141142 m_hasVehicleData (false ),
142143 m_lastSsrPose{},
143144 m_hasSsrPose (false ),
144- m_odoOriginInitialized(false ),
145- m_odoOriginX_mm(0 .0F ),
146- m_odoOriginY_mm(0 .0F ),
147145 m_ekfInitializedFromSSR(false )
148146{
149147 /* Inject dependencies into states. */
@@ -311,8 +309,8 @@ bool App::setupMqtt(const String& clientId, const String& brokerAddr, uint16_t b
311309 {
312310 LOG_FATAL (" MQTT configuration could not be set." );
313311 }
314- else if (false ==
315- m_mqttClient. subscribe (ssrTopic, false , [ this ]( const String& payload) { ssrTopicCallback (payload); }))
312+ else if (false == m_mqttClient. subscribe (ssrTopic, false , [ this ]( const String& payload)
313+ { ssrTopicCallback (payload); }))
316314 {
317315 LOG_FATAL (" Could not subscribe to MQTT topic: %s." , TOPIC_NAME_RADAR_POSE );
318316 }
@@ -358,51 +356,37 @@ void App::ssrTopicCallback(const String& payload)
358356 const int ang_mrad_i = angle_mrad.as <int >();
359357 const uint64_t hostEpochMs = timestamp_ms.as <uint64_t >();
360358
361- const bool hostSynced = m_timeSync.isHostSynced ();
362- const uint64_t ssrLocalTsMs =
363- hostSynced ? m_timeSync.hostToEspLocalMs (hostEpochMs)
364- : m_timeSync.localNowMs (); /* Fallback: use local time if host is not synced */
359+ const bool hostSynced = m_timeSync.isHostSynced ();
360+ const uint64_t ssrLocalTsMs = hostSynced ? m_timeSync.hostToEspLocalMs (hostEpochMs)
361+ : m_timeSync.localNowMs ();
365362
366363 LOG_INFO (" SSR pose: ts_host_ms=%llu (hostSync=%s)" , hostEpochMs, hostSynced ? " true" : " false" );
367364
368365 SpaceShipRadarPose ssrPose;
369366 ssrPose.x = static_cast <float >(x_mm_i);
370367 ssrPose.y = static_cast <float >(y_mm_i);
371368 ssrPose.theta = static_cast <float >(ang_mrad_i);
372- ssrPose.v_x = static_cast <float >(vx_mms_i);
373- ssrPose.v_y = static_cast <float >(vy_mms_i);
374369 ssrPose.timestamp = static_cast <uint32_t >(ssrLocalTsMs);
375370
376- if (false == m_odoOriginInitialized)
377- {
378- m_odoOriginX_mm = ssrPose.x ;
379- m_odoOriginY_mm = ssrPose.y ;
380- m_odoOriginInitialized = true ;
381-
382- LOG_INFO (" Odometry origin set from SSR: x=%dmm y=%dmm" , x_mm_i, y_mm_i);
383- }
384-
385371 if (false == m_ekfInitializedFromSSR)
386372 {
373+ const float vxMms = static_cast <float >(vx_mms_i);
374+ const float vyMms = static_cast <float >(vy_mms_i);
387375 StateVector x0;
388376 x0.setZero ();
389377
390378 x0 (0 ) = ssrPose.x ;
391379 x0 (1 ) = ssrPose.y ;
392380 x0 (2 ) = ssrPose.theta ;
393-
394- const float v_mms = std::sqrt (ssrPose.v_x * ssrPose.v_x + ssrPose.v_y * ssrPose.v_y );
395- x0 (3 ) = v_mms;
396- x0 (4 ) = 0 .0F ;
381+ x0 (3 ) = std::sqrt ((vxMms * vxMms) + (vyMms * vyMms));
397382
398383 StateMatrix P0 = StateMatrix::Identity ();
399384 P0 (0 , 0 ) = 50 .0F * 50 .0F ;
400385 P0 (1 , 1 ) = 50 .0F * 50 .0F ;
401386 P0 (2 , 2 ) = 200 .0F * 200 .0F ;
402387 P0 (3 , 3 ) = 200 .0F * 200 .0F ;
403- P0 (4 , 4 ) = 200 .0F * 200 .0F ;
404388
405- ( void ) m_ekf.init (x0, P0 );
389+ m_ekf.init (x0, P0 );
406390 m_lastEkfUpdateMs = ssrPose.timestamp ;
407391 m_ekfInitializedFromSSR = true ;
408392
@@ -466,116 +450,120 @@ void App::publishVehicleAndSensorSnapshot(const VehicleData& data)
466450
467451void App::filterLocationData (const VehicleData& vehicleData, const SpaceShipRadarPose& ssrPose)
468452{
469- /* Local variables. */
470453 uint32_t zumoTs32 = 0U ;
471454 uint32_t zumoLocalMs32 = 0U ;
472455 uint32_t ssrLocalMs32 = 0U ;
473456 uint32_t newestLocalTs = 0U ;
474457 uint32_t dtMs = 0U ;
475458 float dt = 0 .0F ;
476- float a_x = 0 .0F ;
477459 Source newestSource = Source::None;
478- bool ekfReady = false ;
479460 bool hasTimestamp = false ;
480- bool doProcessing = false ;
481461
482462 /* Do not run fusion until EKF has been initialized from SSR. */
483- ekfReady = m_ekfInitializedFromSSR;
484-
485- if (ekfReady)
463+ if (false == m_ekfInitializedFromSSR)
486464 {
487- doProcessing = true ;
465+ return ;
488466 }
489467
490- if (doProcessing)
468+ /* Timestamp conversion. */
469+ zumoTs32 = static_cast <uint32_t >(vehicleData.timestamp );
470+ zumoLocalMs32 = static_cast <uint32_t >(m_timeSync.mapZumoToLocalMs (zumoTs32));
471+ ssrLocalMs32 = static_cast <uint32_t >(ssrPose.timestamp );
472+
473+ LOG_INFO (" Filtering location data: Zumo=%u ms, SSR=%u ms" , zumoLocalMs32, ssrLocalMs32);
474+
475+ /* Initialize EKF time on first data. */
476+ hasTimestamp = initializeEkfTimestamp (zumoLocalMs32, ssrLocalMs32);
477+
478+ if (false == hasTimestamp)
491479 {
492- /* Timestamp conversion. */
493- zumoTs32 = static_cast <uint32_t >(vehicleData.timestamp );
494- zumoLocalMs32 = static_cast <uint32_t >(m_timeSync.mapZumoToLocalMs (zumoTs32));
495- ssrLocalMs32 = static_cast <uint32_t >(ssrPose.timestamp );
480+ return ;
481+ }
496482
497- LOG_INFO ( " Filtering location data: Zumo=%u ms, SSR=%u ms " , zumoLocalMs32, ssrLocalMs32) ;
483+ newestLocalTs = m_lastEkfUpdateMs ;
498484
499- /* Initialize EKF time on first data . */
500- hasTimestamp = initializeEkfTimestamp (zumoLocalMs32, ssrLocalMs32);
485+ /* Determine which sensor has the newest update . */
486+ newestSource = determineNewestSource (zumoLocalMs32, ssrLocalMs32, m_lastEkfUpdateMs, newestLocalTs );
501487
502- if (hasTimestamp)
503- {
504- newestLocalTs = m_lastEkfUpdateMs;
505-
506- /* Determine which sensor has the newest update. */
507- newestSource = determineNewestSource (zumoLocalMs32, ssrLocalMs32, m_lastEkfUpdateMs, newestLocalTs);
508-
509- if (newestSource != Source::None)
510- {
511- /* Time delta for prediction step. */
512- dtMs = newestLocalTs - m_lastEkfUpdateMs;
513- dt = static_cast <float >(dtMs) / 1000 .0F ;
514-
515- /* Longitudinal acceleration input. */
516- a_x = static_cast <float >(vehicleData.accelerationX );
517-
518- /* EKF prediction. */
519- m_ekf.predict (a_x, dt);
520-
521- /* EKF correction step based on sensor source. */
522- if (newestSource == Source::Vehicle)
523- {
524- updateFromVehicle (vehicleData);
525- }
526- else if (newestSource == Source::SSR )
527- {
528- updateFromSsr (ssrPose);
529- }
530- else if (newestSource == Source::VehicleAndSSR)
531- {
532- updateFromVehicle (vehicleData);
533- updateFromSsr (ssrPose);
534- }
535-
536- /* Update last EKF timestamp. */
537- m_lastEkfUpdateMs = newestLocalTs;
538-
539- /* Publish fused pose. */
540- publishFusionPose (newestLocalTs);
541- }
542- }
488+ if (newestSource == Source::None)
489+ {
490+ return ;
543491 }
544- }
545492
546- void App::transformOdometryToGlobal (const VehicleData& vehicleData, float & xGlob_mm, float & yGlob_mm,
547- float & thetaGlob_mrad) const
548- {
549- /* Y axis and heading sign differ between local odometry frame and SSR frame. */
550- constexpr float Y_SIGN = -1 .0F ;
551- constexpr float THETA_SIGN = 1 .0F ;
493+ /* Time delta for prediction step. */
494+ dtMs = newestLocalTs - m_lastEkfUpdateMs;
495+ dt = static_cast <float >(dtMs) / 1000 .0F ;
496+
497+ /* Use the latest available gyro yaw rate as control input for the process model. */
498+ const float omegaMradPerSec =
499+ ExtendedKalmanFilter4D::gyroDigitsToMradPerSec (static_cast <int16_t >(vehicleData.turnRateZ ));
552500
553- const float xLocal_mm = static_cast <float >(vehicleData.xPos );
554- const float yLocal_mm = static_cast <float >(vehicleData.yPos );
555- const float thetaLocal_mrad = static_cast <float >(vehicleData.orientation );
501+ /* EKF prediction. */
502+ m_ekf.predict (omegaMradPerSec, dt);
556503
557- const float originX = (true == m_odoOriginInitialized) ? m_odoOriginX_mm : 0 .0F ;
558- const float originY = (true == m_odoOriginInitialized) ? m_odoOriginY_mm : 0 .0F ;
504+ /* EKF correction step based on sensor source. */
505+ if (newestSource == Source::Vehicle)
506+ {
507+ updateFromVehicle (vehicleData);
508+ }
509+ else if (newestSource == Source::SSR )
510+ {
511+ updateFromSsr (ssrPose);
512+ }
513+ else if (newestSource == Source::VehicleAndSSR)
514+ {
515+ updateFromVehicle (vehicleData);
516+ updateFromSsr (ssrPose);
517+ }
518+
519+ /* Update last EKF timestamp. */
520+ m_lastEkfUpdateMs = newestLocalTs;
559521
560- xGlob_mm = originX + xLocal_mm;
561- yGlob_mm = originY + (Y_SIGN * yLocal_mm);
562- thetaGlob_mrad = THETA_SIGN * thetaLocal_mrad;
522+ /* Publish fused pose. */
523+ publishFusionPose (newestLocalTs);
563524}
564525
565526void App::publishFusionPose (uint32_t tsMs)
566527{
567- /* EKF state: [p_x, p_y, theta, v, omega]. */
568- const StateVector& state = m_ekf.getState ();
528+ const StateVector& state = m_ekf.getState ();
529+ const ExtendedKalmanFilter4D::NisData& odometryNis = m_ekf.getLastOdometryNis ();
530+ const ExtendedKalmanFilter4D::NisData& cameraNis = m_ekf.getLastCameraNis ();
531+ const float omegaMradPerSec =
532+ (true == m_hasVehicleData)
533+ ? ExtendedKalmanFilter4D::gyroDigitsToMradPerSec (static_cast <int16_t >(m_lastVehicleData.turnRateZ ))
534+ : 0 .0F ;
569535
570536 JsonDocument payloadJson;
571537 char payloadArray[JSON_FUSION_POSE_MAX_SIZE ];
572538
573- payloadJson[" ts_ms" ] = static_cast <int64_t >(tsMs);
574- payloadJson[" x_mm" ] = static_cast <int32_t >(state (0 ));
575- payloadJson[" y_mm" ] = static_cast <int32_t >(state (1 ));
576- payloadJson[" theta_mrad" ] = static_cast <int32_t >(state (2 ));
577- payloadJson[" v_mms" ] = static_cast <int32_t >(state (3 ));
578- payloadJson[" omega_mradps" ] = static_cast <int32_t >(state (4 ));
539+ payloadJson[" ts_ms" ] = static_cast <int64_t >(tsMs);
540+ payloadJson[" x_mm" ] = static_cast <int32_t >(state (0 ));
541+ payloadJson[" y_mm" ] = static_cast <int32_t >(state (1 ));
542+ payloadJson[" theta_mrad" ] = static_cast <int32_t >(state (2 ));
543+ payloadJson[" v_mms" ] = static_cast <int32_t >(state (3 ));
544+
545+ /* Keep the latest gyro control input in the payload for existing consumers. */
546+ payloadJson[" omega_mradps" ] = static_cast <int32_t >(omegaMradPerSec);
547+
548+ JsonObject nisObj = payloadJson[" nis" ].to <JsonObject>();
549+
550+ JsonObject camObj = nisObj[" cam" ].to <JsonObject>();
551+ camObj[" sensor" ] = " cam" ;
552+ camObj[" valid" ] = cameraNis.isValid ;
553+ if (true == cameraNis.isValid )
554+ {
555+ camObj[" value" ] = cameraNis.value ;
556+ camObj[" ts_ms" ] = static_cast <int64_t >(cameraNis.timestampMs );
557+ }
558+
559+ JsonObject odoObj = nisObj[" odo" ].to <JsonObject>();
560+ odoObj[" sensor" ] = " odo" ;
561+ odoObj[" valid" ] = odometryNis.isValid ;
562+ if (true == odometryNis.isValid )
563+ {
564+ odoObj[" value" ] = odometryNis.value ;
565+ odoObj[" ts_ms" ] = static_cast <int64_t >(odometryNis.timestampMs );
566+ }
579567
580568 (void )serializeJson (payloadJson, payloadArray);
581569 String payloadStr (payloadArray);
@@ -683,28 +671,16 @@ Source App::determineNewestSource(uint32_t zumoLocalMs32, uint32_t ssrLocalMs32,
683671
684672void App::updateFromVehicle (const VehicleData& vehicleData)
685673{
686- const int16_t rawGyroZ = static_cast <int16_t >(vehicleData.turnRateZ );
687- float xGlob_mm = 0 .0F ;
688- float yGlob_mm = 0 .0F ;
689- float thetaGlob_mrad = 0 .0F ;
690- OdoMeasurementVector z_odo;
674+ const uint32_t zumoTs32 = static_cast <uint32_t >(vehicleData.timestamp );
675+ const uint32_t zumoLocalMs32 = static_cast <uint32_t >(m_timeSync.mapZumoToLocalMs (zumoTs32));
676+ OdoMeasurementVector z_odo;
691677
692678 LOG_INFO (" EKF update from Vehicle." );
693679
694- /* IMU update using yaw-rate only. */
695- m_ekf.updateImuFromDigits (rawGyroZ);
696-
697- /* Convert odometry into global coordinates. */
698- transformOdometryToGlobal (vehicleData, xGlob_mm, yGlob_mm, thetaGlob_mrad);
699-
700- /* Update odometry measurement:
701- * z_odo(0) = v
702- * z_odo(1) = theta
703- */
680+ /* Odometry measurement: z_odo(0) = v */
704681 z_odo (0 ) = static_cast <float >(vehicleData.center );
705- z_odo (1 ) = thetaGlob_mrad;
706682
707- m_ekf.updateOdometry (z_odo);
683+ m_ekf.updateOdometry (z_odo, zumoLocalMs32 );
708684}
709685
710686void App::updateFromSsr (const SpaceShipRadarPose& ssrPose)
@@ -713,16 +689,12 @@ void App::updateFromSsr(const SpaceShipRadarPose& ssrPose)
713689
714690 LOG_INFO (" EKF update from SSR." );
715691
716- /* Camera measurement:
717- * z_cam(0..4) = [x, y, theta, v_x, v_y]
718- */
692+ /* Camera measurement: z_cam(0..2) = [x, y, theta] */
719693 z_cam (0 ) = ssrPose.x ;
720694 z_cam (1 ) = ssrPose.y ;
721695 z_cam (2 ) = ssrPose.theta ;
722- z_cam (3 ) = ssrPose.v_x ;
723- z_cam (4 ) = ssrPose.v_y ;
724696
725- m_ekf.updateCamera (z_cam);
697+ m_ekf.updateCamera (z_cam, ssrPose. timestamp );
726698}
727699
728700/* *****************************************************************************
0 commit comments