PX4 Firmware
PX4 Autopilot Software http://px4.io
attitude_estimator_q_main.cpp
Go to the documentation of this file.
1 /****************************************************************************
2  *
3  * Copyright (c) 2015 PX4 Development Team. All rights reserved.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions
7  * are met:
8  *
9  * 1. Redistributions of source code must retain the above copyright
10  * notice, this list of conditions and the following disclaimer.
11  * 2. Redistributions in binary form must reproduce the above copyright
12  * notice, this list of conditions and the following disclaimer in
13  * the documentation and/or other materials provided with the
14  * distribution.
15  * 3. Neither the name PX4 nor the names of its contributors may be
16  * used to endorse or promote products derived from this software
17  * without specific prior written permission.
18  *
19  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
22  * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
23  * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
24  * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
25  * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
26  * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
27  * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
28  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
29  * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30  * POSSIBILITY OF SUCH DAMAGE.
31  *
32  ****************************************************************************/
33 
34 /*
35  * @file attitude_estimator_q_main.cpp
36  *
37  * Attitude estimator (quaternion based)
38  *
39  * @author Anton Babushkin <anton.babushkin@me.com>
40  */
41 
42 #include <float.h>
43 
44 #include <drivers/drv_hrt.h>
45 #include <lib/ecl/geo/geo.h>
47 #include <lib/mathlib/mathlib.h>
48 #include <lib/parameters/param.h>
49 #include <matrix/math.hpp>
50 #include <px4_platform_common/defines.h>
51 #include <px4_platform_common/module.h>
52 #include <px4_platform_common/module_params.h>
53 #include <px4_platform_common/posix.h>
54 #include <uORB/Publication.hpp>
55 #include <uORB/Subscription.hpp>
63 
64 using matrix::Dcmf;
65 using matrix::Eulerf;
66 using matrix::Quatf;
67 using matrix::Vector3f;
68 using matrix::wrap_pi;
69 
70 using namespace time_literals;
71 
72 class AttitudeEstimatorQ : public ModuleBase<AttitudeEstimatorQ>, public ModuleParams, public px4::WorkItem
73 {
74 public:
75 
77  ~AttitudeEstimatorQ() override = default;
78 
79  /** @see ModuleBase */
80  static int task_spawn(int argc, char *argv[]);
81 
82  /** @see ModuleBase */
83  static int custom_command(int argc, char *argv[]);
84 
85  /** @see ModuleBase */
86  static int print_usage(const char *reason = nullptr);
87 
88  void Run() override;
89 
90  bool init();
91 
92 private:
93 
94  void update_parameters(bool force = false);
95 
96  bool init_attq();
97 
98  bool update(float dt);
99 
100  // Update magnetic declination (in rads) immediately changing yaw rotation
101  void update_mag_declination(float new_declination);
102 
103 
104  const float _eo_max_std_dev = 100.0f; /**< Maximum permissible standard deviation for estimated orientation */
105  const float _dt_min = 0.00001f;
106  const float _dt_max = 0.02f;
107 
108  uORB::SubscriptionCallbackWorkItem _sensors_sub{this, ORB_ID(sensor_combined)};
109 
110  uORB::Subscription _parameter_update_sub{ORB_ID(parameter_update)};
111  uORB::Subscription _global_pos_sub{ORB_ID(vehicle_global_position)};
112  uORB::Subscription _vision_odom_sub{ORB_ID(vehicle_visual_odometry)};
113  uORB::Subscription _mocap_odom_sub{ORB_ID(vehicle_mocap_odometry)};
114  uORB::Subscription _magnetometer_sub{ORB_ID(vehicle_magnetometer)};
115 
116  uORB::Publication<vehicle_attitude_s> _att_pub{ORB_ID(vehicle_attitude)};
117 
118  float _mag_decl{0.0f};
119  float _bias_max{0.0f};
120 
124 
127 
131 
133  hrt_abstime _vel_prev_t{0};
134 
136 
137  hrt_abstime _last_time{0};
138 
139  bool _inited{false};
140  bool _data_good{false};
141  bool _ext_hdg_good{false};
142 
143  DEFINE_PARAMETERS(
144  (ParamFloat<px4::params::ATT_W_ACC>) _param_att_w_acc,
145  (ParamFloat<px4::params::ATT_W_MAG>) _param_att_w_mag,
146  (ParamFloat<px4::params::ATT_W_EXT_HDG>) _param_att_w_ext_hdg,
147  (ParamFloat<px4::params::ATT_W_GYRO_BIAS>) _param_att_w_gyro_bias,
148  (ParamFloat<px4::params::ATT_MAG_DECL>) _param_att_mag_decl,
149  (ParamInt<px4::params::ATT_MAG_DECL_A>) _param_att_mag_decl_a,
150  (ParamInt<px4::params::ATT_EXT_HDG_M>) _param_att_ext_hdg_m,
151  (ParamInt<px4::params::ATT_ACC_COMP>) _param_att_acc_comp,
152  (ParamFloat<px4::params::ATT_BIAS_MAX>) _param_att_bias_mas,
153  (ParamInt<px4::params::SYS_HAS_MAG>) _param_sys_has_mag
154  )
155 };
156 
158  ModuleParams(nullptr),
159  WorkItem(MODULE_NAME, px4::wq_configurations::att_pos_ctrl)
160 {
161  _vel_prev.zero();
162  _pos_acc.zero();
163 
164  _gyro.zero();
165  _accel.zero();
166  _mag.zero();
167 
168  _vision_hdg.zero();
169  _mocap_hdg.zero();
170 
171  _q.zero();
172  _rates.zero();
173  _gyro_bias.zero();
174 
175  update_parameters(true);
176 }
177 
178 bool
180 {
182  PX4_ERR("sensor combined callback registration failed!");
183  return false;
184  }
185 
186  return true;
187 }
188 
189 void
191 {
192  if (should_exit()) {
194  exit_and_cleanup();
195  return;
196  }
197 
199 
200  if (_sensors_sub.update(&sensors)) {
201 
203 
204  // Feed validator with recent sensor data
205  if (sensors.timestamp > 0) {
206  _gyro(0) = sensors.gyro_rad[0];
207  _gyro(1) = sensors.gyro_rad[1];
208  _gyro(2) = sensors.gyro_rad[2];
209  }
210 
211  if (sensors.accelerometer_timestamp_relative != sensor_combined_s::RELATIVE_TIMESTAMP_INVALID) {
212  _accel(0) = sensors.accelerometer_m_s2[0];
213  _accel(1) = sensors.accelerometer_m_s2[1];
214  _accel(2) = sensors.accelerometer_m_s2[2];
215 
216  if (_accel.length() < 0.01f) {
217  PX4_ERR("degenerate accel!");
218  return;
219  }
220  }
221 
222  // Update magnetometer
223  if (_magnetometer_sub.updated()) {
224  vehicle_magnetometer_s magnetometer;
225 
226  if (_magnetometer_sub.copy(&magnetometer)) {
227  _mag(0) = magnetometer.magnetometer_ga[0];
228  _mag(1) = magnetometer.magnetometer_ga[1];
229  _mag(2) = magnetometer.magnetometer_ga[2];
230 
231  if (_mag.length() < 0.01f) {
232  PX4_ERR("degenerate mag!");
233  return;
234  }
235  }
236 
237  }
238 
239  _data_good = true;
240 
241  // Update vision and motion capture heading
242  _ext_hdg_good = false;
243 
244  if (_vision_odom_sub.updated()) {
245  vehicle_odometry_s vision;
246 
247  if (_vision_odom_sub.copy(&vision)) {
248  // validation check for vision attitude data
249  bool vision_att_valid = PX4_ISFINITE(vision.q[0])
250  && (PX4_ISFINITE(vision.pose_covariance[vision.COVARIANCE_MATRIX_ROLL_VARIANCE]) ? sqrtf(fmaxf(
251  vision.pose_covariance[vision.COVARIANCE_MATRIX_ROLL_VARIANCE],
252  fmaxf(vision.pose_covariance[vision.COVARIANCE_MATRIX_PITCH_VARIANCE],
253  vision.pose_covariance[vision.COVARIANCE_MATRIX_YAW_VARIANCE]))) <= _eo_max_std_dev : true);
254 
255  if (vision_att_valid) {
256  Dcmf Rvis = Quatf(vision.q);
257  Vector3f v(1.0f, 0.0f, 0.4f);
258 
259  // Rvis is Rwr (robot respect to world) while v is respect to world.
260  // Hence Rvis must be transposed having (Rwr)' * Vw
261  // Rrw * Vw = vn. This way we have consistency
262  _vision_hdg = Rvis.transpose() * v;
263 
264  // vision external heading usage (ATT_EXT_HDG_M 1)
265  if (_param_att_ext_hdg_m.get() == 1) {
266  // Check for timeouts on data
267  _ext_hdg_good = vision.timestamp > 0 && (hrt_elapsed_time(&vision.timestamp) < 500000);
268  }
269  }
270  }
271  }
272 
273  if (_mocap_odom_sub.updated()) {
274  vehicle_odometry_s mocap;
275 
276  if (_mocap_odom_sub.copy(&mocap)) {
277  // validation check for mocap attitude data
278  bool mocap_att_valid = PX4_ISFINITE(mocap.q[0])
279  && (PX4_ISFINITE(mocap.pose_covariance[mocap.COVARIANCE_MATRIX_ROLL_VARIANCE]) ? sqrtf(fmaxf(
280  mocap.pose_covariance[mocap.COVARIANCE_MATRIX_ROLL_VARIANCE],
281  fmaxf(mocap.pose_covariance[mocap.COVARIANCE_MATRIX_PITCH_VARIANCE],
282  mocap.pose_covariance[mocap.COVARIANCE_MATRIX_YAW_VARIANCE]))) <= _eo_max_std_dev : true);
283 
284  if (mocap_att_valid) {
285  Dcmf Rmoc = Quatf(mocap.q);
286  Vector3f v(1.0f, 0.0f, 0.4f);
287 
288  // Rmoc is Rwr (robot respect to world) while v is respect to world.
289  // Hence Rmoc must be transposed having (Rwr)' * Vw
290  // Rrw * Vw = vn. This way we have consistency
291  _mocap_hdg = Rmoc.transpose() * v;
292 
293  // Motion Capture external heading usage (ATT_EXT_HDG_M 2)
294  if (_param_att_ext_hdg_m.get() == 2) {
295  // Check for timeouts on data
296  _ext_hdg_good = mocap.timestamp > 0 && (hrt_elapsed_time(&mocap.timestamp) < 500000);
297  }
298  }
299  }
300  }
301 
302  if (_global_pos_sub.updated()) {
304 
305  if (_global_pos_sub.copy(&gpos)) {
306  if (_param_att_mag_decl_a.get() && gpos.eph < 20.0f && hrt_elapsed_time(&gpos.timestamp) < 1_s) {
307  /* set magnetic declination automatically */
309  }
310 
311  if (_param_att_acc_comp.get() && gpos.timestamp != 0 && hrt_absolute_time() < gpos.timestamp + 20000 && gpos.eph < 5.0f
312  && _inited) {
313 
314  /* position data is actual */
315  Vector3f vel(gpos.vel_n, gpos.vel_e, gpos.vel_d);
316 
317  /* velocity updated */
318  if (_vel_prev_t != 0 && gpos.timestamp != _vel_prev_t) {
319  float vel_dt = (gpos.timestamp - _vel_prev_t) / 1e6f;
320  /* calculate acceleration in body frame */
321  _pos_acc = _q.conjugate_inversed((vel - _vel_prev) / vel_dt);
322  }
323 
324  _vel_prev_t = gpos.timestamp;
325  _vel_prev = vel;
326 
327  } else {
328  /* position data is outdated, reset acceleration */
329  _pos_acc.zero();
330  _vel_prev.zero();
331  _vel_prev_t = 0;
332  }
333  }
334  }
335 
336  /* time from previous iteration */
338  const float dt = math::constrain((now - _last_time) / 1e6f, _dt_min, _dt_max);
339  _last_time = now;
340 
341  if (update(dt)) {
342  vehicle_attitude_s att = {};
343  att.timestamp = sensors.timestamp;
344  _q.copyTo(att.q);
345 
346  /* the instance count is not used here */
347  _att_pub.publish(att);
348  }
349  }
350 }
351 
352 void
354 {
355  // check for parameter updates
356  if (_parameter_update_sub.updated() || force) {
357  // clear update
358  parameter_update_s pupdate;
359  _parameter_update_sub.copy(&pupdate);
360 
361  // update parameters from storage
362  updateParams();
363 
364  // disable mag fusion if the system does not have a mag
365  if (_param_sys_has_mag.get() == 0) {
366  _param_att_w_mag.set(0.0f);
367  }
368 
369  // if the weight is zero (=mag disabled), make sure the estimator initializes
370  if (_param_att_w_mag.get() < FLT_EPSILON) {
371  _mag(0) = 1.f;
372  _mag(1) = 0.f;
373  _mag(2) = 0.f;
374  }
375 
376  update_mag_declination(math::radians(_param_att_mag_decl.get()));
377  }
378 }
379 
380 bool
382 {
383  // Rotation matrix can be easily constructed from acceleration and mag field vectors
384  // 'k' is Earth Z axis (Down) unit vector in body frame
385  Vector3f k = -_accel;
386  k.normalize();
387 
388  // 'i' is Earth X axis (North) unit vector in body frame, orthogonal with 'k'
389  Vector3f i = (_mag - k * (_mag * k));
390  i.normalize();
391 
392  // 'j' is Earth Y axis (East) unit vector in body frame, orthogonal with 'k' and 'i'
393  Vector3f j = k % i;
394 
395  // Fill rotation matrix
396  Dcmf R;
397  R.row(0) = i;
398  R.row(1) = j;
399  R.row(2) = k;
400 
401  // Convert to quaternion
402  _q = R;
403 
404  // Compensate for magnetic declination
405  Quatf decl_rotation = Eulerf(0.0f, 0.0f, _mag_decl);
406  _q = _q * decl_rotation;
407 
408  _q.normalize();
409 
410  if (PX4_ISFINITE(_q(0)) && PX4_ISFINITE(_q(1)) &&
411  PX4_ISFINITE(_q(2)) && PX4_ISFINITE(_q(3)) &&
412  _q.length() > 0.95f && _q.length() < 1.05f) {
413  _inited = true;
414 
415  } else {
416  _inited = false;
417  }
418 
419  return _inited;
420 }
421 
422 bool
424 {
425  if (!_inited) {
426 
427  if (!_data_good) {
428  return false;
429  }
430 
431  return init_attq();
432  }
433 
434  Quatf q_last = _q;
435 
436  // Angular rate of correction
437  Vector3f corr;
438  float spinRate = _gyro.length();
439 
440  if (_param_att_ext_hdg_m.get() > 0 && _ext_hdg_good) {
441  if (_param_att_ext_hdg_m.get() == 1) {
442  // Vision heading correction
443  // Project heading to global frame and extract XY component
444  Vector3f vision_hdg_earth = _q.conjugate(_vision_hdg);
445  float vision_hdg_err = wrap_pi(atan2f(vision_hdg_earth(1), vision_hdg_earth(0)));
446  // Project correction to body frame
447  corr += _q.conjugate_inversed(Vector3f(0.0f, 0.0f, -vision_hdg_err)) * _param_att_w_ext_hdg.get();
448  }
449 
450  if (_param_att_ext_hdg_m.get() == 2) {
451  // Mocap heading correction
452  // Project heading to global frame and extract XY component
453  Vector3f mocap_hdg_earth = _q.conjugate(_mocap_hdg);
454  float mocap_hdg_err = wrap_pi(atan2f(mocap_hdg_earth(1), mocap_hdg_earth(0)));
455  // Project correction to body frame
456  corr += _q.conjugate_inversed(Vector3f(0.0f, 0.0f, -mocap_hdg_err)) * _param_att_w_ext_hdg.get();
457  }
458  }
459 
460  if (_param_att_ext_hdg_m.get() == 0 || !_ext_hdg_good) {
461  // Magnetometer correction
462  // Project mag field vector to global frame and extract XY component
463  Vector3f mag_earth = _q.conjugate(_mag);
464  float mag_err = wrap_pi(atan2f(mag_earth(1), mag_earth(0)) - _mag_decl);
465  float gainMult = 1.0f;
466  const float fifty_dps = 0.873f;
467 
468  if (spinRate > fifty_dps) {
469  gainMult = math::min(spinRate / fifty_dps, 10.0f);
470  }
471 
472  // Project magnetometer correction to body frame
473  corr += _q.conjugate_inversed(Vector3f(0.0f, 0.0f, -mag_err)) * _param_att_w_mag.get() * gainMult;
474  }
475 
476  _q.normalize();
477 
478 
479  // Accelerometer correction
480  // Project 'k' unit vector of earth frame to body frame
481  // Vector3f k = _q.conjugate_inversed(Vector3f(0.0f, 0.0f, 1.0f));
482  // Optimized version with dropped zeros
483  Vector3f k(
484  2.0f * (_q(1) * _q(3) - _q(0) * _q(2)),
485  2.0f * (_q(2) * _q(3) + _q(0) * _q(1)),
486  (_q(0) * _q(0) - _q(1) * _q(1) - _q(2) * _q(2) + _q(3) * _q(3))
487  );
488 
489  // If we are not using acceleration compensation based on GPS velocity,
490  // fuse accel data only if its norm is close to 1 g (reduces drift).
491  const float accel_norm_sq = _accel.norm_squared();
492  const float upper_accel_limit = CONSTANTS_ONE_G * 1.1f;
493  const float lower_accel_limit = CONSTANTS_ONE_G * 0.9f;
494 
495  if (_param_att_acc_comp.get() || ((accel_norm_sq > lower_accel_limit * lower_accel_limit) &&
496  (accel_norm_sq < upper_accel_limit * upper_accel_limit))) {
497 
498  corr += (k % (_accel - _pos_acc).normalized()) * _param_att_w_acc.get();
499  }
500 
501  // Gyro bias estimation
502  if (spinRate < 0.175f) {
503  _gyro_bias += corr * (_param_att_w_gyro_bias.get() * dt);
504 
505  for (int i = 0; i < 3; i++) {
507  }
508 
509  }
510 
511  _rates = _gyro + _gyro_bias;
512 
513  // Feed forward gyro
514  corr += _rates;
515 
516  // Apply correction to state
517  _q += _q.derivative1(corr) * dt;
518 
519  // Normalize quaternion
520  _q.normalize();
521 
522  if (!(PX4_ISFINITE(_q(0)) && PX4_ISFINITE(_q(1)) &&
523  PX4_ISFINITE(_q(2)) && PX4_ISFINITE(_q(3)))) {
524 
525  // Reset quaternion to last good state
526  _q = q_last;
527  _rates.zero();
528  _gyro_bias.zero();
529  return false;
530  }
531 
532  return true;
533 }
534 
535 void
537 {
538  // Apply initial declination or trivial rotations without changing estimation
539  if (!_inited || fabsf(new_declination - _mag_decl) < 0.0001f) {
540  _mag_decl = new_declination;
541 
542  } else {
543  // Immediately rotate current estimation to avoid gyro bias growth
544  Quatf decl_rotation = Eulerf(0.0f, 0.0f, new_declination - _mag_decl);
545  _q = _q * decl_rotation;
546  _mag_decl = new_declination;
547  }
548 }
549 
550 int
551 AttitudeEstimatorQ::custom_command(int argc, char *argv[])
552 {
553  return print_usage("unknown command");
554 }
555 
556 int
557 AttitudeEstimatorQ::task_spawn(int argc, char *argv[])
558 {
560 
561  if (instance) {
562  _object.store(instance);
563  _task_id = task_id_is_work_queue;
564 
565  if (instance->init()) {
566  return PX4_OK;
567  }
568 
569  } else {
570  PX4_ERR("alloc failed");
571  }
572 
573  delete instance;
574  _object.store(nullptr);
575  _task_id = -1;
576 
577  return PX4_ERROR;
578 }
579 
580 int
582 {
583  if (reason) {
584  PX4_WARN("%s\n", reason);
585  }
586 
587  PRINT_MODULE_DESCRIPTION(
588  R"DESCR_STR(
589 ### Description
590 Attitude estimator q.
591 
592 )DESCR_STR");
593 
594  PRINT_MODULE_USAGE_NAME("AttitudeEstimatorQ", "estimator");
595  PRINT_MODULE_USAGE_COMMAND("start");
596  PRINT_MODULE_USAGE_DEFAULT_COMMANDS();
597 
598  return 0;
599 }
600 
601 extern "C" __EXPORT int attitude_estimator_q_main(int argc, char *argv[])
602 {
603  return AttitudeEstimatorQ::main(argc, argv);
604 }
constexpr _Tp constrain(_Tp val, _Tp min_val, _Tp max_val)
Definition: Limits.hpp:66
void update_parameters(bool force=false)
const float _eo_max_std_dev
Maximum permissible standard deviation for estimated orientation.
static int task_spawn(int argc, char *argv[])
bool update(void *dst)
Copy the struct if updated.
Definition of geo / math functions to perform geodesic calculations.
Dcm< float > Dcmf
Definition: Dcm.hpp:185
uORB::Subscription _global_pos_sub
int main(int argc, char **argv)
Definition: main.cpp:3
Definition: I2C.hpp:51
float accelerometer_m_s2[3]
static void print_usage()
LidarLite * instance
Definition: ll40ls.cpp:65
#define FLT_EPSILON
High-resolution timer with callouts and timekeeping.
static constexpr float CONSTANTS_ONE_G
Definition: geo.h:51
Global flash based parameter store.
#define ORB_ID(_name)
Generates a pointer to the uORB metadata structure for a given topic.
Definition: uORB.h:87
uORB::Subscription _mocap_odom_sub
bool publish(const T &data)
Publish the struct.
Definition: Publication.hpp:68
void init()
Activates/configures the hardware registers.
constexpr T radians(T degrees)
Definition: Limits.hpp:85
void update_mag_declination(float new_declination)
Vector< float, 6 > f(float t, const Matrix< float, 6, 1 > &, const Matrix< float, 3, 1 > &)
Definition: integration.cpp:8
static hrt_abstime hrt_elapsed_time(const hrt_abstime *then)
Compute the delta between a timestamp taken in the past and now.
Definition: drv_hrt.h:102
uORB::SubscriptionCallbackWorkItem _sensors_sub
Euler< float > Eulerf
Definition: Euler.hpp:156
uORB::Publication< vehicle_attitude_s > _att_pub
bool updated()
Check if there is a new update.
__BEGIN_DECLS typedef uint64_t hrt_abstime
Absolute time, in microsecond units.
Definition: drv_hrt.h:58
constexpr _Tp min(_Tp a, _Tp b)
Definition: Limits.hpp:54
Type wrap_pi(Type x)
Wrap value in range [-π, π)
Definition: common.h:43
Calculation / lookup table for Earth&#39;s magnetic field declination, inclination and strength...
uORB::Subscription _magnetometer_sub
Vector3< float > Vector3f
Definition: Vector3.hpp:136
int update_parameters(const ParameterHandles &parameter_handles, Parameters &parameters)
Read out the parameters using the handles into the parameters struct.
Definition: parameters.cpp:60
Quaternion< float > Quatf
Definition: Quaternion.hpp:544
float dt
Definition: px4io.c:73
uORB::Subscription _parameter_update_sub
uORB::Subscription _vision_odom_sub
const Slice< Type, 1, N, M, N > row(size_t i) const
Definition: Matrix.hpp:385
Definition: bst.cpp:62
static int print_usage(const char *reason=nullptr)
__EXPORT int attitude_estimator_q_main(int argc, char *argv[])
int32_t accelerometer_timestamp_relative
float get_mag_declination(float lat, float lon)
static int custom_command(int argc, char *argv[])
bool copy(void *dst)
Copy the struct.
__EXPORT hrt_abstime hrt_absolute_time(void)
Get absolute time in [us] (does not wrap).