1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 #include "calibration/nano_calibration/nano_calibration.h"
18 
19 #include <cstdint>
20 #include <cstring>
21 
22 #include "common/techeng_log_util.h"
23 
24 namespace nano_calibration {
25 namespace {
26 
27 // Common log message sensor-specific identifiers.
28 constexpr char kAccelTag[] = {"[NanoSensorCal:ACCEL_MPS2]"};
29 constexpr char kGyroTag[] = {"[NanoSensorCal:GYRO_RPS]"};
30 constexpr char kMagTag[] = {"[NanoSensorCal:MAG_UT]"};
31 
32 // Defines a plan for limiting log messages so that upon initialization there
33 // begins a period set by 'duration_of_rapid_messages_min' where log messages
34 // appear at a rate set by 'rapid_message_interval_sec'. Afterwards, log
35 // messages will be produced at a rate determined by
36 // 'slow_message_interval_min'.
37 struct LogMessageRegimen {
38   uint8_t rapid_message_interval_sec;  // Assists device verification.
39   uint8_t slow_message_interval_min;   // Avoids long-term log spam.
40   uint8_t duration_of_rapid_messages_min;
41 };
42 
43 constexpr LogMessageRegimen kGyroscopeMessagePlan = {
44     /*rapid_message_interval_sec*/ 20,
45     /*slow_message_interval_min*/ 5,
46     /*duration_of_rapid_messages_min*/ 3};
47 
48 using ::online_calibration::CalibrationDataThreeAxis;
49 using ::online_calibration::CalibrationTypeFlags;
50 using ::online_calibration::SensorData;
51 using ::online_calibration::SensorIndex;
52 using ::online_calibration::SensorType;
53 
54 // NanoSensorCal logging macros.
55 #ifndef LOG_TAG
56 #define LOG_TAG "[ImuCal]"
57 #endif
58 
59 #ifdef NANO_SENSOR_CAL_DBG_ENABLED
60 #define NANO_CAL_LOGD(tag, format, ...) \
61   TECHENG_LOGD("%s " format, tag, ##__VA_ARGS__)
62 #define NANO_CAL_LOGW(tag, format, ...) \
63   TECHENG_LOGW("%s " format, tag, ##__VA_ARGS__)
64 #define NANO_CAL_LOGE(tag, format, ...) \
65   TECHENG_LOGE("%s " format, tag, ##__VA_ARGS__)
66 #else
67 #define NANO_CAL_LOGD(tag, format, ...) techeng_log_null(format, ##__VA_ARGS__)
68 #define NANO_CAL_LOGW(tag, format, ...) techeng_log_null(format, ##__VA_ARGS__)
69 #define NANO_CAL_LOGE(tag, format, ...) techeng_log_null(format, ##__VA_ARGS__)
70 #endif  // NANO_SENSOR_CAL_DBG_ENABLED
71 
72 // NOTE: LOGI is defined to ensure calibration updates are always logged for
73 // field diagnosis and verification.
74 #define NANO_CAL_LOGI(tag, format, ...) \
75   TECHENG_LOGI("%s " format, tag, ##__VA_ARGS__)
76 
77 }  // namespace
78 
Initialize(OnlineCalibrationThreeAxis * accel_cal,OnlineCalibrationThreeAxis * gyro_cal,OnlineCalibrationThreeAxis * mag_cal)79 void NanoSensorCal::Initialize(OnlineCalibrationThreeAxis *accel_cal,
80                                OnlineCalibrationThreeAxis *gyro_cal,
81                                OnlineCalibrationThreeAxis *mag_cal) {
82   // Loads stored calibration data and initializes the calibration algorithms.
83   accel_cal_ = accel_cal;
84   if (accel_cal_ != nullptr) {
85     if (accel_cal_->get_sensor_type() == SensorType::kAccelerometerMps2) {
86       LoadAshCalibration(CHRE_SENSOR_TYPE_ACCELEROMETER, accel_cal_,
87                          &accel_cal_update_flags_, kAccelTag);
88       NANO_CAL_LOGI(kAccelTag,
89                     "Accelerometer runtime calibration initialized.");
90     } else {
91       accel_cal_ = nullptr;
92       NANO_CAL_LOGE(kAccelTag, "Failed to initialize: wrong sensor type.");
93     }
94   }
95 
96   gyro_cal_ = gyro_cal;
97   if (gyro_cal_ != nullptr) {
98     if (gyro_cal_->get_sensor_type() == SensorType::kGyroscopeRps) {
99       LoadAshCalibration(CHRE_SENSOR_TYPE_GYROSCOPE, gyro_cal_,
100                          &gyro_cal_update_flags_, kGyroTag);
101       NANO_CAL_LOGI(kGyroTag, "Gyroscope runtime calibration initialized.");
102     } else {
103       gyro_cal_ = nullptr;
104       NANO_CAL_LOGE(kGyroTag, "Failed to initialize: wrong sensor type.");
105     }
106   }
107 
108   mag_cal_ = mag_cal;
109   if (mag_cal != nullptr) {
110     if (mag_cal->get_sensor_type() == SensorType::kMagnetometerUt) {
111       LoadAshCalibration(CHRE_SENSOR_TYPE_GEOMAGNETIC_FIELD, mag_cal_,
112                          &mag_cal_update_flags_, kMagTag);
113       NANO_CAL_LOGI(kMagTag, "Magnetometer runtime calibration initialized.");
114     } else {
115       mag_cal_ = nullptr;
116       NANO_CAL_LOGE(kMagTag, "Failed to initialize: wrong sensor type.");
117     }
118   }
119 
120   // Resets the initialization timestamp. Set below in HandleSensorSamples.
121   initialization_start_time_nanos_ = 0;
122 }
123 
HandleSensorSamples(uint16_t event_type,const chreSensorThreeAxisData * event_data)124 void NanoSensorCal::HandleSensorSamples(
125     uint16_t event_type, const chreSensorThreeAxisData *event_data) {
126   // Converts CHRE Event -> SensorData::SensorType.
127   SensorData sample;
128   switch (event_type) {
129     case CHRE_EVENT_SENSOR_UNCALIBRATED_ACCELEROMETER_DATA:
130       sample.type = SensorType::kAccelerometerMps2;
131       break;
132     case CHRE_EVENT_SENSOR_UNCALIBRATED_GYROSCOPE_DATA:
133       sample.type = SensorType::kGyroscopeRps;
134       break;
135     case CHRE_EVENT_SENSOR_UNCALIBRATED_GEOMAGNETIC_FIELD_DATA:
136       sample.type = SensorType::kMagnetometerUt;
137       break;
138     default:
139       // This sensor type is not used.
140       NANO_CAL_LOGW("[NanoSensorCal]",
141                     "Unexpected 3-axis sensor type received.");
142       return;
143   }
144 
145   // Sends the sensor payload to the calibration algorithms and checks for
146   // calibration updates.
147   const auto &header = event_data->header;
148   const auto *data = event_data->readings;
149   sample.timestamp_nanos = header.baseTimestamp;
150   for (size_t i = 0; i < header.readingCount; i++) {
151     sample.timestamp_nanos += data[i].timestampDelta;
152     memcpy(sample.data, data[i].v, sizeof(sample.data));
153     ProcessSample(sample);
154   }
155 
156   // Starts tracking the time after initialization to help rate limit gyro log
157   // messaging.
158   if (initialization_start_time_nanos_ == 0) {
159     initialization_start_time_nanos_ = header.baseTimestamp;
160     gyro_notification_time_nanos_ = 0;
161   }
162 }
163 
HandleTemperatureSamples(uint16_t event_type,const chreSensorFloatData * event_data)164 void NanoSensorCal::HandleTemperatureSamples(
165     uint16_t event_type, const chreSensorFloatData *event_data) {
166   // Computes the mean of the batched temperature samples and delivers it to the
167   // calibration algorithms. Note, the temperature sensor batch size determines
168   // its minimum update interval.
169   if (event_type == CHRE_EVENT_SENSOR_ACCELEROMETER_TEMPERATURE_DATA &&
170       event_data->header.readingCount > 0) {
171     const auto header = event_data->header;
172     const auto *data = event_data->readings;
173 
174     SensorData sample;
175     sample.type = SensorType::kTemperatureCelsius;
176     sample.timestamp_nanos = header.baseTimestamp;
177 
178     float accum_temperature_celsius = 0.0f;
179     for (size_t i = 0; i < header.readingCount; i++) {
180       sample.timestamp_nanos += data[i].timestampDelta;
181       accum_temperature_celsius += data[i].value;
182     }
183     sample.data[SensorIndex::kSingleAxis] =
184         accum_temperature_celsius / header.readingCount;
185     ProcessSample(sample);
186   } else {
187     NANO_CAL_LOGW("[NanoSensorCal]",
188                   "Unexpected single-axis sensor type received.");
189   }
190 }
191 
ProcessSample(const SensorData & sample)192 void NanoSensorCal::ProcessSample(const SensorData &sample) {
193   // Sends a new sensor sample to each active calibration algorithm and sends
194   // out notifications for new calibration updates.
195   if (accel_cal_ != nullptr) {
196     const CalibrationTypeFlags new_cal_flags =
197         accel_cal_->SetMeasurement(sample);
198     if (new_cal_flags != CalibrationTypeFlags::NONE) {
199       accel_cal_update_flags_ |= new_cal_flags;
200       NotifyAshCalibration(CHRE_SENSOR_TYPE_ACCELEROMETER,
201                            accel_cal_->GetSensorCalibration(),
202                            accel_cal_update_flags_, kAccelTag);
203       PrintCalibration(accel_cal_->GetSensorCalibration(),
204                        accel_cal_update_flags_, kAccelTag);
205 
206       if (result_callback_ != nullptr) {
207         result_callback_->SetCalibrationEvent(sample.timestamp_nanos,
208                                               SensorType::kAccelerometerMps2,
209                                               accel_cal_update_flags_);
210       }
211     }
212   }
213 
214   if (gyro_cal_ != nullptr) {
215     const CalibrationTypeFlags new_cal_flags =
216         gyro_cal_->SetMeasurement(sample);
217     if (new_cal_flags != CalibrationTypeFlags::NONE) {
218       gyro_cal_update_flags_ |= new_cal_flags;
219       if (NotifyAshCalibration(CHRE_SENSOR_TYPE_GYROSCOPE,
220                                gyro_cal_->GetSensorCalibration(),
221                                gyro_cal_update_flags_, kGyroTag)) {
222         const bool print_gyro_log =
223             HandleGyroLogMessage(sample.timestamp_nanos);
224 
225         if (result_callback_ != nullptr &&
226             (print_gyro_log ||
227              gyro_cal_update_flags_ != CalibrationTypeFlags::BIAS)) {
228           // Rate-limits OTC gyro telemetry updates since they can happen
229           // frequently with temperature change. However, all GyroCal stillness
230           // and OTC model parameter updates will be recorded.
231           result_callback_->SetCalibrationEvent(sample.timestamp_nanos,
232                                                 SensorType::kGyroscopeRps,
233                                                 gyro_cal_update_flags_);
234         }
235       }
236     }
237   }
238 
239   if (mag_cal_ != nullptr) {
240     const CalibrationTypeFlags new_cal_flags = mag_cal_->SetMeasurement(sample);
241     if (new_cal_flags != CalibrationTypeFlags::NONE) {
242       mag_cal_update_flags_ |= new_cal_flags;
243       NotifyAshCalibration(CHRE_SENSOR_TYPE_GEOMAGNETIC_FIELD,
244                            mag_cal_->GetSensorCalibration(),
245                            mag_cal_update_flags_, kMagTag);
246       PrintCalibration(mag_cal_->GetSensorCalibration(), mag_cal_update_flags_,
247                        kMagTag);
248 
249       if (result_callback_ != nullptr) {
250         result_callback_->SetCalibrationEvent(sample.timestamp_nanos,
251                                               SensorType::kMagnetometerUt,
252                                               mag_cal_update_flags_);
253       }
254     }
255   }
256 }
257 
NotifyAshCalibration(uint8_t chreSensorType,const CalibrationDataThreeAxis & cal_data,CalibrationTypeFlags flags,const char * sensor_tag)258 bool NanoSensorCal::NotifyAshCalibration(
259     uint8_t chreSensorType, const CalibrationDataThreeAxis &cal_data,
260     CalibrationTypeFlags flags, const char *sensor_tag) {
261   // Updates the sensor offset calibration using the ASH API.
262   ashCalInfo ash_cal_info;
263   memset(&ash_cal_info, 0, sizeof(ashCalInfo));
264   ash_cal_info.compMatrix[0] = 1.0f;  // Sets diagonal to unity (scale factor).
265   ash_cal_info.compMatrix[4] = 1.0f;
266   ash_cal_info.compMatrix[8] = 1.0f;
267   memcpy(ash_cal_info.bias, cal_data.offset, sizeof(ash_cal_info.bias));
268 
269   // Maps CalibrationQualityLevel to ASH calibration accuracy.
270   switch (cal_data.calibration_quality.level) {
271     case online_calibration::CalibrationQualityLevel::HIGH_QUALITY:
272       ash_cal_info.accuracy = ASH_CAL_ACCURACY_HIGH;
273       break;
274 
275     case online_calibration::CalibrationQualityLevel::MEDIUM_QUALITY:
276       ash_cal_info.accuracy = ASH_CAL_ACCURACY_MEDIUM;
277       break;
278 
279     case online_calibration::CalibrationQualityLevel::LOW_QUALITY:
280       ash_cal_info.accuracy = ASH_CAL_ACCURACY_LOW;
281       break;
282 
283     default:
284       ash_cal_info.accuracy = ASH_CAL_ACCURACY_UNRELIABLE;
285       break;
286   }
287 
288   if (!ashSetCalibration(chreSensorType, &ash_cal_info)) {
289     NANO_CAL_LOGE(sensor_tag, "ASH failed to apply calibration update.");
290     return false;
291   }
292 
293   // Uses the ASH API to store all calibration parameters relevant to a given
294   // algorithm as indicated by the input calibration type flags.
295   ashCalParams ash_cal_parameters;
296   memset(&ash_cal_parameters, 0, sizeof(ashCalParams));
297   if (flags & CalibrationTypeFlags::BIAS) {
298     ash_cal_parameters.offsetTempCelsius = cal_data.offset_temp_celsius;
299     memcpy(ash_cal_parameters.offset, cal_data.offset,
300            sizeof(ash_cal_parameters.offset));
301     ash_cal_parameters.offsetSource = ASH_CAL_PARAMS_SOURCE_RUNTIME;
302     ash_cal_parameters.offsetTempCelsiusSource = ASH_CAL_PARAMS_SOURCE_RUNTIME;
303   }
304 
305   if (flags & CalibrationTypeFlags::OVER_TEMP) {
306     memcpy(ash_cal_parameters.tempSensitivity, cal_data.temp_sensitivity,
307            sizeof(ash_cal_parameters.tempSensitivity));
308     memcpy(ash_cal_parameters.tempIntercept, cal_data.temp_intercept,
309            sizeof(ash_cal_parameters.tempIntercept));
310     ash_cal_parameters.tempSensitivitySource = ASH_CAL_PARAMS_SOURCE_RUNTIME;
311     ash_cal_parameters.tempInterceptSource = ASH_CAL_PARAMS_SOURCE_RUNTIME;
312   }
313 
314   if (!ashSaveCalibrationParams(chreSensorType, &ash_cal_parameters)) {
315     NANO_CAL_LOGE(sensor_tag, "ASH failed to write calibration update.");
316     return false;
317   }
318 
319   return true;
320 }
321 
LoadAshCalibration(uint8_t chreSensorType,OnlineCalibrationThreeAxis * online_cal,CalibrationTypeFlags * flags,const char * sensor_tag)322 bool NanoSensorCal::LoadAshCalibration(uint8_t chreSensorType,
323                                        OnlineCalibrationThreeAxis *online_cal,
324                                        CalibrationTypeFlags *flags,
325                                        const char *sensor_tag) {
326   ashCalParams recalled_ash_cal_parameters;
327   if (ashLoadCalibrationParams(chreSensorType, ASH_CAL_STORAGE_ASH,
328                                &recalled_ash_cal_parameters)) {
329     // Checks whether a valid set of runtime calibration parameters was received
330     // and can be used for initialization.
331     if (DetectRuntimeCalibration(chreSensorType, sensor_tag, flags,
332                                  &recalled_ash_cal_parameters)) {
333       CalibrationDataThreeAxis cal_data;
334       cal_data.type = online_cal->get_sensor_type();
335       cal_data.cal_update_time_nanos = chreGetTime();
336 
337       // Analyzes the calibration flags and sets only the runtime calibration
338       // values that were received.
339       if (*flags & CalibrationTypeFlags::BIAS) {
340         cal_data.offset_temp_celsius =
341             recalled_ash_cal_parameters.offsetTempCelsius;
342         memcpy(cal_data.offset, recalled_ash_cal_parameters.offset,
343                sizeof(cal_data.offset));
344       }
345 
346       if (*flags & CalibrationTypeFlags::OVER_TEMP) {
347         memcpy(cal_data.temp_sensitivity,
348                recalled_ash_cal_parameters.tempSensitivity,
349                sizeof(cal_data.temp_sensitivity));
350         memcpy(cal_data.temp_intercept,
351                recalled_ash_cal_parameters.tempIntercept,
352                sizeof(cal_data.temp_intercept));
353       }
354 
355       // Sets the algorithm's initial calibration data and notifies ASH to apply
356       // the recalled calibration data.
357       if (online_cal->SetInitialCalibration(cal_data)) {
358         return NotifyAshCalibration(chreSensorType,
359                                     online_cal->GetSensorCalibration(), *flags,
360                                     sensor_tag);
361       } else {
362         NANO_CAL_LOGE(sensor_tag,
363                       "Calibration data failed to initialize algorithm.");
364       }
365     }
366   } else {
367     // This is not necessarily an error since there may not be any previously
368     // stored runtime calibration data to load yet (e.g., first device boot).
369     NANO_CAL_LOGW(sensor_tag, "ASH did not recall calibration data.");
370   }
371 
372   return false;
373 }
374 
DetectRuntimeCalibration(uint8_t chreSensorType,const char * sensor_tag,CalibrationTypeFlags * flags,ashCalParams * ash_cal_parameters)375 bool NanoSensorCal::DetectRuntimeCalibration(uint8_t chreSensorType,
376                                              const char *sensor_tag,
377                                              CalibrationTypeFlags *flags,
378                                              ashCalParams *ash_cal_parameters) {
379   // Analyzes calibration source flags to determine whether runtime
380   // calibration values have been loaded and may be used for initialization. A
381   // valid runtime calibration source will include at least an offset.
382   *flags = CalibrationTypeFlags::NONE;  // Resets the calibration flags.
383 
384   // Uses the ASH calibration source flags to set the appropriate
385   // CalibrationTypeFlags. These will be used to determine which values to copy
386   // from 'ash_cal_parameters' and provide to the calibration algorithms for
387   // initialization.
388   bool runtime_cal_detected = false;
389   if (ash_cal_parameters->offsetSource == ASH_CAL_PARAMS_SOURCE_RUNTIME &&
390       ash_cal_parameters->offsetTempCelsiusSource ==
391           ASH_CAL_PARAMS_SOURCE_RUNTIME) {
392     runtime_cal_detected = true;
393     *flags = CalibrationTypeFlags::BIAS;
394   }
395 
396   if (ash_cal_parameters->tempSensitivitySource ==
397           ASH_CAL_PARAMS_SOURCE_RUNTIME &&
398       ash_cal_parameters->tempInterceptSource ==
399           ASH_CAL_PARAMS_SOURCE_RUNTIME) {
400     *flags |= CalibrationTypeFlags::OVER_TEMP;
401   }
402 
403   if (runtime_cal_detected) {
404     // Prints the retrieved runtime calibration data.
405     NANO_CAL_LOGI(sensor_tag, "Runtime calibration data detected.");
406     PrintAshCalParams(*ash_cal_parameters, sensor_tag);
407   } else {
408     // This is a warning (not an error) since the runtime algorithms will
409     // function correctly with no recalled calibration values. They will
410     // eventually trigger and update the system with valid calibration data.
411     NANO_CAL_LOGW(sensor_tag, "No runtime offset calibration data found.");
412   }
413 
414   return runtime_cal_detected;
415 }
416 
417 // Helper functions for logging calibration information.
PrintAshCalParams(const ashCalParams & cal_params,const char * sensor_tag)418 void NanoSensorCal::PrintAshCalParams(const ashCalParams &cal_params,
419                                       const char *sensor_tag) {
420   if (cal_params.offsetSource == ASH_CAL_PARAMS_SOURCE_RUNTIME) {
421     NANO_CAL_LOGI(sensor_tag,
422                   "Offset | Temperature [C]: %.6f, %.6f, %.6f | %.2f",
423                   cal_params.offset[0], cal_params.offset[1],
424                   cal_params.offset[2], cal_params.offsetTempCelsius);
425   }
426 
427   if (cal_params.tempSensitivitySource == ASH_CAL_PARAMS_SOURCE_RUNTIME) {
428     NANO_CAL_LOGI(sensor_tag, "Temp Sensitivity [units/C]: %.6f, %.6f, %.6f",
429                   cal_params.tempSensitivity[0], cal_params.tempSensitivity[1],
430                   cal_params.tempSensitivity[2]);
431   }
432 
433   if (cal_params.tempInterceptSource == ASH_CAL_PARAMS_SOURCE_RUNTIME) {
434     NANO_CAL_LOGI(sensor_tag, "Temp Intercept [units]: %.6f, %.6f, %.6f",
435                   cal_params.tempIntercept[0], cal_params.tempIntercept[1],
436                   cal_params.tempIntercept[2]);
437   }
438 
439   if (cal_params.scaleFactorSource == ASH_CAL_PARAMS_SOURCE_RUNTIME) {
440     NANO_CAL_LOGI(sensor_tag, "Scale Factor: %.6f, %.6f, %.6f",
441                   cal_params.scaleFactor[0], cal_params.scaleFactor[1],
442                   cal_params.scaleFactor[2]);
443   }
444 
445   if (cal_params.crossAxisSource == ASH_CAL_PARAMS_SOURCE_RUNTIME) {
446     NANO_CAL_LOGI(sensor_tag,
447                   "Cross-Axis in [yx, zx, zy] order: %.6f, %.6f, %.6f",
448                   cal_params.crossAxis[0], cal_params.crossAxis[1],
449                   cal_params.crossAxis[2]);
450   }
451 }
452 
PrintCalibration(const CalibrationDataThreeAxis & cal_data,CalibrationTypeFlags flags,const char * sensor_tag)453 void NanoSensorCal::PrintCalibration(const CalibrationDataThreeAxis &cal_data,
454                                      CalibrationTypeFlags flags,
455                                      const char *sensor_tag) {
456   if (flags & CalibrationTypeFlags::BIAS) {
457     NANO_CAL_LOGI(sensor_tag,
458                   "Offset | Temperature [C]: %.6f, %.6f, %.6f | %.2f",
459                   cal_data.offset[0], cal_data.offset[1], cal_data.offset[2],
460                   cal_data.offset_temp_celsius);
461   }
462 
463   if (flags & CalibrationTypeFlags::OVER_TEMP) {
464     NANO_CAL_LOGI(sensor_tag, "Temp Sensitivity: %.6f, %.6f, %.6f",
465                   cal_data.temp_sensitivity[0], cal_data.temp_sensitivity[1],
466                   cal_data.temp_sensitivity[2]);
467     NANO_CAL_LOGI(sensor_tag, "Temp Intercept: %.6f, %.6f, %.6f",
468                   cal_data.temp_intercept[0], cal_data.temp_intercept[1],
469                   cal_data.temp_intercept[2]);
470   }
471 }
472 
HandleGyroLogMessage(uint64_t timestamp_nanos)473 bool NanoSensorCal::HandleGyroLogMessage(uint64_t timestamp_nanos) {
474   // Limits the log messaging update rate for the gyro calibrations since
475   // these can occur frequently with rapid temperature changes.
476   const int64_t next_log_interval_nanos =
477       (NANO_TIMER_CHECK_T1_GEQUAL_T2_PLUS_DELTA(
478           timestamp_nanos, initialization_start_time_nanos_,
479           MIN_TO_NANOS(kGyroscopeMessagePlan.duration_of_rapid_messages_min)))
480           ? MIN_TO_NANOS(kGyroscopeMessagePlan.slow_message_interval_min)
481           : SEC_TO_NANOS(kGyroscopeMessagePlan.rapid_message_interval_sec);
482 
483   const bool print_gyro_log = NANO_TIMER_CHECK_T1_GEQUAL_T2_PLUS_DELTA(
484       timestamp_nanos, gyro_notification_time_nanos_, next_log_interval_nanos);
485 
486   if (print_gyro_log) {
487     gyro_notification_time_nanos_ = timestamp_nanos;
488     PrintCalibration(gyro_cal_->GetSensorCalibration(), gyro_cal_update_flags_,
489                      kGyroTag);
490   }
491 
492   return print_gyro_log;
493 }
494 
495 }  // namespace nano_calibration
496