Instrument Neutral Distributed Interface INDI 2.2.5
indiimu.cpp
Go to the documentation of this file.
1/*
2 IMU Driver Base Class
3 Copyright (C) 2025 Jasem Mutlaq ([email protected])
4
5 This library is free software; you can redistribute it and/or
6 modify it under the terms of the GNU Lesser General Public
7 License as published by the Free Software Foundation; either
8 version 2.1 of the License, or (at your option) any later version.
9
10 This library is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 Lesser General Public License for more details.
14
15 You should have received a copy of the GNU Lesser General Public
16 License along with this library; if not, write to the Free Software
17 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18
19*/
20
21#include "indiimu.h"
24#include "indicom.h"
27#include <cmath>
28#include <memory>
29
30namespace INDI
31{
32
34{
35 // Constructor only initializes the interface base class.
36 // Properties will be initialized in initProperties().
37 serialConnection = nullptr;
38 i2cConnection = nullptr;
39}
40
42{
43 delete serialConnection;
44 delete i2cConnection;
45}
46
47void IMU::setSupportedConnections(const uint8_t &value)
48{
50
51 if (value == 0 || (mask & value) == 0)
52 {
53 DEBUGF(Logger::DBG_ERROR, "Invalid connection mode %d", value);
54 return;
55 }
56
57 imuConnection = value;
58}
59
61{
64
65 // Initialize driver-specific properties
66
67 AstroCoordinatesNP[AXIS1].fill("AXIS1", "Axis 1 (deg)", "%.2f", 0, 360, 0, 0);
68 AstroCoordinatesNP[AXIS2].fill("AXIS2", "Axis 2 (deg)", "%.2f", 0, 360, 0, 0);
69 AstroCoordinatesNP.fill(getDeviceName(), "COORDINATES", "Coordinates", COORDINATES_TAB.c_str(), IP_RO,
70 0, IPS_IDLE);
71
72 AstroCoordsTypeSP[COORD_EQUATORIAL].fill("EQUATORIAL", "Equatorial (HA/DEC)", ISS_ON);
73 AstroCoordsTypeSP[COORD_ALTAZ].fill("ALTAZ", "Alt-Az (AZ/ALT)", ISS_OFF);
74 AstroCoordsTypeSP.fill(getDeviceName(), "COORDS_TYPE", "Coordinate Type", COORDINATES_TAB.c_str(), IP_RW,
77
78 IMUFrameSP[ENU].fill("ENU", "East-North-Up", ISS_ON);
79 IMUFrameSP[NWU].fill("NWU", "North-West-Up", ISS_OFF);
80 IMUFrameSP[SWU].fill("SWU", "South-West-Up", ISS_OFF);
81 IMUFrameSP.fill(getDeviceName(), "IMU_FRAME", "IMU Frame", IMU_TAB.c_str(), IP_RW, ISR_1OFMANY, 0, IPS_IDLE);
83
84 OrientationAdjustmentsNP[ROLL_MULTIPLIER].fill("ROLL_M", "Roll Multiplier", "%.2f", -1.0, 1.0, 0.1, 1.0);
85 OrientationAdjustmentsNP[PITCH_MULTIPLIER].fill("PITCH_M", "Pitch Multiplier", "%.2f", -1.0, 1.0, 0.1, 1.0);
86 OrientationAdjustmentsNP[YAW_MULTIPLIER].fill("YAW_M", "Yaw Multiplier", "%.2f", -1.0, 1.0, 0.1, 1.0);
87 OrientationAdjustmentsNP[ROLL_OFFSET].fill("ROLL_O", "Roll Offset (deg)", "%.2f", -360.0, 360.0, 10, 0.0);
88 OrientationAdjustmentsNP[PITCH_OFFSET].fill("PITCH_O", "Pitch Offset (deg)", "%.2f", -360.0, 360.0, 10, 0.0);
89 OrientationAdjustmentsNP[YAW_OFFSET].fill("YAW_O", "Yaw Offset (deg)", "%.2f", -360.0, 360.0, 10, 0.0);
90 OrientationAdjustmentsNP.fill(getDeviceName(), "ORIENTATION_ADJUSTMENTS", "Orientation Adjustments", IMU_TAB.c_str(), IP_RW,
91 0, IPS_IDLE);
93
94 SyncAxisNP[AXIS1].fill("SYNC_AXIS1", "Sync Axis 1 (deg)", "%.2f", -360, 360, 10, 0);
95 SyncAxisNP[AXIS2].fill("SYNC_AXIS2", "Sync Axis 2 (deg)", "%.2f", -360, 360, 10, 0);
96 SyncAxisNP.fill(getDeviceName(), "SYNC_AXIS", "Sync Axis", COORDINATES_TAB.c_str(), IP_RW, 0, IPS_IDLE);
97
98 TelescopeVectorNP[TELESCOPE_VECTOR_X].fill("TELESCOPE_VECTOR_X", "Telescope Vector X", "%.2f", -1.0, 1.0, 0.1, 1.0);
99 TelescopeVectorNP[TELESCOPE_VECTOR_Y].fill("TELESCOPE_VECTOR_Y", "Telescope Vector Y", "%.2f", -1.0, 1.0, 0.1, 0.0);
100 TelescopeVectorNP[TELESCOPE_VECTOR_Z].fill("TELESCOPE_VECTOR_Z", "Telescope Vector Z", "%.2f", -1.0, 1.0, 0.1, 0.0);
101 TelescopeVectorNP.fill(getDeviceName(), "TELESCOPE_VECTOR", "Telescope Vector", IMU_TAB.c_str(), IP_RW, 0, IPS_IDLE);
103
104 GeographicCoordNP[LOCATION_LATITUDE].fill("LAT", "Lat (dd:mm:ss.s)", "%012.8m", -90, 90, 0, 0.0);
105 GeographicCoordNP[LOCATION_LONGITUDE].fill("LONG", "Lon (dd:mm:ss.s)", "%012.8m", 0, 360, 0, 0.0);
106 GeographicCoordNP[LOCATION_ELEVATION].fill("ELEV", "Elevation (m)", "%g", -200, 10000, 0, 0);
107 GeographicCoordNP.fill(getDeviceName(), "GEOGRAPHIC_COORD", "Location", MAIN_CONTROL_TAB, IP_RW, 0, IPS_IDLE);
109
110 MagneticDeclinationNP[0].fill("MAGNETIC_DECLINATION", "Magnetic Declination", "%.4f", -180, 180, 0, 0);
111 MagneticDeclinationNP.fill(getDeviceName(), "MAGNETIC_DECLINATION", "Magnetic Declination", MAIN_CONTROL_TAB, IP_RW, 0,
112 IPS_IDLE);
114
115 // ── Software Sensor Fusion ────────────────────────────────────────────────
116 // Drivers that provide only raw accel/gyro/mag (e.g. ICM-20948) do not set
117 // IMU_HAS_ORIENTATION in their capability. In that case we enable software
118 // Kalman/AHRS fusion here so that orientation output is produced centrally in
119 // this base class without any driver changes.
120 // Drivers that perform on-chip fusion (e.g. BNO08x) set IMU_HAS_ORIENTATION
121 // and call SetOrientationData() directly – they are completely unaffected.
122 if (!HasOrientation())
123 {
124 // Mark that software fusion is available/active.
126
127 // IMUInterface::initProperties() skipped OrientationNP because HasOrientation()
128 // was false at call time. Initialise it here so fusion output can be displayed.
129 OrientationNP[ORIENTATION_ROLL].fill("ROLL", "Roll (deg)", "%.4f", -180, 180, 0, 0);
130 OrientationNP[ORIENTATION_PITCH].fill("PITCH", "Pitch (deg)", "%.4f", -90, 90, 0, 0);
131 OrientationNP[ORIENTATION_YAW].fill("YAW", "Yaw (deg)", "%.4f", 0, 360, 0, 0);
132 OrientationNP[ORIENTATION_QUATERNION_W].fill("QUATERNION_W", "Quaternion W", "%.4f", -1, 1, 0, 0);
133 OrientationNP.fill(getDeviceName(), "ORIENTATION", "Orientation", IMU_TAB.c_str(), IP_RO, 0, IPS_IDLE);
134
135 // Enable / Disable switch (enabled by default for raw-data drivers)
136 SensorFusionSP[FUSION_ENABLE].fill("FUSION_ENABLE", "Enable", ISS_ON);
137 SensorFusionSP[FUSION_DISABLE].fill("FUSION_DISABLE", "Disable", ISS_OFF);
138 SensorFusionSP.fill(getDeviceName(), "SENSOR_FUSION", "Sensor Fusion", IMU_TAB.c_str(), IP_RW, ISR_1OFMANY, 0,
139 IPS_IDLE);
141
142 // Algorithm selection: Madgwick (default) or Mahony
143 FusionTypeSP[FUSION_MADGWICK].fill("MADGWICK", "Madgwick", ISS_ON);
144 FusionTypeSP[FUSION_MAHONY].fill("MAHONY", "Mahony", ISS_OFF);
145 FusionTypeSP.fill(getDeviceName(), "FUSION_TYPE", "Fusion Algorithm", IMU_TAB.c_str(), IP_RW, ISR_1OFMANY, 0,
146 IPS_IDLE);
148
149 // Tuning parameters:
150 // FUSION_PARAM_1 = β for Madgwick / Kp for Mahony
151 // FUSION_PARAM_2 = unused (Madgwick) / Ki for Mahony
152 FusionParamsNP[FUSION_PARAM_1].fill("FUSION_PARAM_1", "β (Madgwick) / Kp (Mahony)", "%.4f", 0.001, 10.0, 0.01,
153 0.1);
154 FusionParamsNP[FUSION_PARAM_2].fill("FUSION_PARAM_2", "Ki – Mahony integral gain", "%.4f", 0.0, 1.0, 0.001,
155 0.005);
156 FusionParamsNP.fill(getDeviceName(), "FUSION_PARAMS", "Fusion Parameters", IMU_TAB.c_str(), IP_RW, 0, IPS_IDLE);
158
159 // Create the initial filter instance (Madgwick with default β = 0.1)
160 recreateFusionFilter();
161 }
162
163 if (imuConnection & CONNECTION_SERIAL)
164 {
167 {
168 return callHandshake();
169 });
171 }
172
173 if (imuConnection & CONNECTION_I2C)
174 {
175 i2cConnection = new Connection::I2C(this);
176 i2cConnection->setDefaultBusPath("/dev/i2c-1");
178 {
179 return callHandshake();
180 });
182 }
183
184 return true;
185}
186
188{
191
192 if (isConnected())
193 {
194 // Define driver-specific properties when connected
203
204 // For raw-data drivers using software fusion: define orientation output
205 // and fusion control properties. Hardware-fusion drivers (e.g. BNO08x)
206 // have IMU_HAS_ORIENTATION set and are handled by IMUInterface above.
208 {
213
214 // Reset the fusion filter on every new connection so we start fresh.
215 recreateFusionFilter();
216 }
217 }
218 else
219 {
220 // Delete driver-specific properties when disconnected
229
231 {
236 }
237 }
238 return true;
239}
240
241bool IMU::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n)
242{
243 if (IMUInterface::processNumber(dev, name, values, names, n))
244 return true;
245
247 {
248 updateProperty(OrientationAdjustmentsNP, values, names, n, [this]()
249 {
250 // When orientation adjustments change, recalculate astro coordinates
251 // and also update the IMU's internal orientation properties.
252 // This will trigger SetOrientationData to apply the new adjustments.
253 SetOrientationData(last_q_i, last_q_j, last_q_k, last_q_w);
254 return true;
255 }, true);
256 return true;
257 }
258
260 {
261 updateProperty(TelescopeVectorNP, values, names, n, [this]()
262 {
264 return true;
265 }, true);
266 return true;
267 }
268
270 {
271 updateProperty(GeographicCoordNP, values, names, n, [this]()
272 {
274 return true;
275 }, true);
276 return true;
277 }
278
280 {
281 updateProperty(MagneticDeclinationNP, values, names, n, [&]()
282 {
284 return true;
285 }, true);
286 return true;
287 }
288
289 if (SyncAxisNP.isNameMatch(name))
290 {
291 updateProperty(SyncAxisNP, values, names, n, [this, values]()
292 {
294
295 // Step 1: Convert sync coordinates to a TelescopeDirectionVector in the appropriate sky frame.
298 {
300 syncEq.rightascension = values[AXIS1] / 15.0; // Convert degrees to hours
301 syncEq.declination = values[AXIS2];
302 skyVector = tdvFunctions.TelescopeDirectionVectorFromLocalHourAngleDeclination(syncEq);
303 }
304 else
305 {
307 syncHor.azimuth = values[AXIS1];
308 syncHor.altitude = values[AXIS2];
309 // This function returns a vector in the NWU frame
310 skyVector = tdvFunctions.TelescopeDirectionVectorFromAltitudeAzimuth(syncHor);
311 }
312
313 // Step 2: Inverse transform the sky vector back to the local horizon (ENU) frame.
316 {
317 // Inverse of the transformation from ENU to Equatorial
318 double latitude = GeographicCoordNP[LOCATION_LATITUDE].getValue();
319 double lat_rad = DEG_TO_RAD(latitude);
320 double sin_lat = sin(lat_rad);
321 double cos_lat = cos(lat_rad);
322
323 // Original transform: x_eq = z_enu*cos-y_enu*sin, y_eq = -x_enu, z_eq = z_enu*sin+y_enu*cos
324 // Inverse transform:
325 enuVector.x = -skyVector.y; // East
326 enuVector.y = (skyVector.z - (skyVector.x * sin_lat / cos_lat)) / (cos_lat + (sin_lat * sin_lat / cos_lat)); // North
327 enuVector.z = (skyVector.x + enuVector.y * sin_lat) / cos_lat; // Up
328 }
329 else
330 {
331 // The skyVector is in the NWU frame, convert it to ENU
332 // NWU to ENU: X_enu = -Y_nwu, Y_enu = X_nwu, Z_enu = Z_nwu
333 enuVector.x = -skyVector.y;
334 enuVector.y = skyVector.x;
335 enuVector.z = skyVector.z;
336 }
337
338 // Step 3: Inverse transform from the local horizon (ENU) frame to the IMU's sensor frame.
339 // This gives us the "target" vector - what the telescope vector *should* be in the sensor's frame.
342 switch (frame)
343 {
344 case ENU:
345 // No conversion needed
346 targetIMUVector = enuVector;
347 break;
348 case NWU:
349 // ENU to NWU: X_nwu = Y_enu, Y_nwu = -X_enu, Z_nwu = Z_enu
350 targetIMUVector.x = enuVector.y;
351 targetIMUVector.y = -enuVector.x;
352 targetIMUVector.z = enuVector.z;
353 break;
354 case SWU:
355 // ENU to SWU: X_swu = -Y_enu, Y_swu = -X_enu, Z_swu = Z_enu
356 targetIMUVector.x = -enuVector.y;
357 targetIMUVector.y = -enuVector.x;
358 targetIMUVector.z = enuVector.z;
359 break;
360 }
361
362 // Step 4: Calculate the telescope's *current* pointing vector in the sensor frame.
363 // This is done by rotating the configured TelescopeVector by the raw sensor quaternion.
364 double vx = TelescopeVectorNP[TELESCOPE_VECTOR_X].getValue();
365 double vy = TelescopeVectorNP[TELESCOPE_VECTOR_Y].getValue();
366 double vz = TelescopeVectorNP[TELESCOPE_VECTOR_Z].getValue();
367 double qw = last_raw_q_w, qx = last_raw_q_i, qy = last_raw_q_j, qz = last_raw_q_k;
368
369 double current_x = vx * (1 - 2 * qy * qy - 2 * qz * qz) + vy * (2 * qx * qy - 2 * qz * qw) + vz *
370 (2 * qx * qz + 2 * qy * qw);
371 double current_y = vx * (2 * qx * qy + 2 * qz * qw) + vy * (1 - 2 * qx * qx - 2 * qz * qz) + vz *
372 (2 * qy * qz - 2 * qx * qw);
373 double current_z = vx * (2 * qx * qz - 2 * qy * qw) + vy * (2 * qy * qz + 2 * qx * qw) + vz *
374 (1 - 2 * qx * qx - 2 * qy * qy);
375 INDI::AlignmentSubsystem::TelescopeDirectionVector currentIMUVector(current_x, current_y, current_z);
376
377 // Step 5: Calculate the rotation required to get from the current sensor vector to the target sensor vector.
378 INDI::AlignmentSubsystem::TelescopeDirectionVector rotationAxis = currentIMUVector * targetIMUVector;
379 double dotProduct = std::max(-1.0, std::min(1.0, currentIMUVector ^ targetIMUVector));
380 double rotationAngle = acos(dotProduct);
381
382 if (rotationAngle < 1e-6) // Already aligned
383 {
384 DEBUG(Logger::DBG_DEBUG, "IMU Sync: Already aligned, no adjustment needed.");
385 return true;
386 }
387 rotationAxis.Normalise();
388
389 // Step 5.1: Determine the full rotation that is currently being applied to the raw data
390 double rollMultiplier = OrientationAdjustmentsNP[ROLL_MULTIPLIER].getValue();
391 double pitchMultiplier = OrientationAdjustmentsNP[PITCH_MULTIPLIER].getValue();
392 double yawMultiplier = OrientationAdjustmentsNP[YAW_MULTIPLIER].getValue();
393 double magneticDeclinationRad = DEG_TO_RAD(MagneticDeclinationNP[0].getValue());
394
395 double rawRollRad, rawPitchRad, rawYawRad;
396 QuaternionToEuler(qx, qy, qz, qw, rawRollRad, rawPitchRad, rawYawRad);
397
398 rawRollRad *= rollMultiplier;
399 rawPitchRad *= pitchMultiplier;
400 rawYawRad *= yawMultiplier;
401
402 double multiplied_i, multiplied_j, multiplied_k, multiplied_w;
403 EulerToQuaternion(rawRollRad, rawPitchRad, rawYawRad, multiplied_i, multiplied_j, multiplied_k, multiplied_w);
404
405 double mag_i, mag_j, mag_k, mag_w;
406 EulerToQuaternion(0, 0, magneticDeclinationRad, mag_i, mag_j, mag_k, mag_w);
407
408 // Step 5.2: The rotation from the telescope vector to the target IMU vector is the final, absolute orientation
409 // that the IMU should have. Let's call it target_orientation_q.
411 v.Normalise();
412 targetIMUVector.Normalise();
413 double dot = v ^ targetIMUVector;
415 axis.Normalise();
416 double angle = acos(dot);
417
418 double s = sin(angle / 2.0);
419 double target_orientation_w = cos(angle / 2.0);
420 double target_orientation_i = axis.x * s;
421 double target_orientation_j = axis.y * s;
422 double target_orientation_k = axis.z * s;
423
424 // Step 5.3: We have target_orientation_q = mag_q * offset_q * multiplied_q
425 // We need to solve for offset_q.
426 // offset_q = conjugate(mag_q) * target_orientation_q * conjugate(multiplied_q)
427
428 // conjugate(mag_q)
429 double conj_mag_i = -mag_i, conj_mag_j = -mag_j, conj_mag_k = -mag_k, conj_mag_w = mag_w;
430 // conjugate(multiplied_q)
431 double conj_mult_i = -multiplied_i, conj_mult_j = -multiplied_j, conj_mult_k = -multiplied_k, conj_mult_w = multiplied_w;
432
433 // temp_q = conjugate(mag_q) * target_orientation_q
434 double temp_w = conj_mag_w * target_orientation_w - conj_mag_i * target_orientation_i - conj_mag_j * target_orientation_j -
435 conj_mag_k * target_orientation_k;
436 double temp_i = conj_mag_w * target_orientation_i + conj_mag_i * target_orientation_w + conj_mag_j * target_orientation_k -
437 conj_mag_k * target_orientation_j;
438 double temp_j = conj_mag_w * target_orientation_j - conj_mag_i * target_orientation_k + conj_mag_j * target_orientation_w +
439 conj_mag_k * target_orientation_i;
440 double temp_k = conj_mag_w * target_orientation_k + conj_mag_i * target_orientation_j - conj_mag_j * target_orientation_i +
441 conj_mag_k * target_orientation_w;
442
443 // offset_q = temp_q * conjugate(multiplied_q)
444 double offset_w = temp_w * conj_mult_w - temp_i * conj_mult_i - temp_j * conj_mult_j - temp_k * conj_mult_k;
445 double offset_i = temp_w * conj_mult_i + temp_i * conj_mult_w + temp_j * conj_mult_k - temp_k * conj_mult_j;
446 double offset_j = temp_w * conj_mult_j - temp_i * conj_mult_k + temp_j * conj_mult_w + temp_k * conj_mult_i;
447 double offset_k = temp_w * conj_mult_k + temp_i * conj_mult_j - temp_j * conj_mult_i + temp_k * conj_mult_w;
448
449 // Step 6: Convert the new offset quaternion to Euler angles and set the properties.
450 double roll, pitch, yaw;
451 QuaternionToEuler(offset_i, offset_j, offset_k, offset_w, roll, pitch, yaw);
452
457
458 // Step 7: Add extensive logging for debugging.
459 DEBUGF(Logger::DBG_DEBUG, "IMU Sync: Syncing to Axis1=%.2f, Axis2=%.2f", values[AXIS1], values[AXIS2]);
460 DEBUGF(Logger::DBG_DEBUG, "IMU Sync: Selected IMU Frame: %s", IMUFrameSP.findOnSwitch()->getLabel());
461 DEBUGF(Logger::DBG_DEBUG, "IMU Sync: Telescope Vector: X=%.4f, Y=%.4f, Z=%.4f", vx, vy, vz);
462 DEBUGF(Logger::DBG_DEBUG, "IMU Sync: Raw Quaternion: i=%.4f, j=%.4f, k=%.4f, w=%.4f", qx, qy, qz, qw);
463 DEBUGF(Logger::DBG_DEBUG, "IMU Sync: Target IMU Vector: X=%.4f, Y=%.4f, Z=%.4f", targetIMUVector.x,
464 targetIMUVector.y, targetIMUVector.z);
465 DEBUGF(Logger::DBG_DEBUG, "IMU Sync: Current IMU Vector: X=%.4f, Y=%.4f, Z=%.4f", currentIMUVector.x,
466 currentIMUVector.y, currentIMUVector.z);
467 DEBUGF(Logger::DBG_DEBUG, "IMU Sync: Rotation Angle: %.4f deg", RAD_TO_DEG(rotationAngle));
468 DEBUGF(Logger::DBG_DEBUG, "IMU Sync: New Offset Quaternion: i=%.4f, j=%.4f, k=%.4f, w=%.4f", offset_i, offset_j,
469 offset_k, offset_w);
470 DEBUGF(Logger::DBG_DEBUG, "IMU Sync: New Offsets (deg): Roll=%.2f, Pitch=%.2f, Yaw=%.2f", RAD_TO_DEG(roll),
471 RAD_TO_DEG(pitch), RAD_TO_DEG(yaw));
472
473 // Step 8: Trigger a full recalculation with the new offsets using the last raw sensor data.
474 SetOrientationData(last_raw_q_i, last_raw_q_j, last_raw_q_k, last_raw_q_w);
475 return true;
476 });
477 return true;
478 }
479
480 // ── Sensor Fusion parameters ──────────────────────────────────────────────
482 {
483 FusionParamsNP.update(values, names, n);
487
488 // Apply new parameters to the running filter without a full reset
489 double p1 = FusionParamsNP[FUSION_PARAM_1].getValue();
490 double p2 = FusionParamsNP[FUSION_PARAM_2].getValue();
491
492 if (m_SensorFusion)
493 {
494 if (FusionTypeSP[FUSION_MAHONY].getState() == ISS_ON)
495 {
496 auto *f = dynamic_cast<MahonyFilter *>(m_SensorFusion.get());
497 if (f)
498 {
499 f->setKp(p1);
500 f->setKi(p2);
501 }
502 }
503 else
504 {
505 auto *f = dynamic_cast<MadgwickFilter *>(m_SensorFusion.get());
506 if (f)
507 f->setBeta(p1);
508 }
509 }
510
511 LOGF_INFO("IMU: Fusion parameters updated (param1=%.4f, param2=%.4f).", p1, p2);
512 return true;
513 }
514
515 return DefaultDevice::ISNewNumber(dev, name, values, names, n);
516}
517
518bool IMU::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n)
519{
520 if (IMUInterface::processSwitch(dev, name, states, names, n))
521 return true;
522
524 {
525 updateProperty(AstroCoordsTypeSP, states, names, n, [this]()
526 {
528 return true;
529 }, true);
530 return true;
531 }
532
533 if (IMUFrameSP.isNameMatch(name))
534 {
535 updateProperty(IMUFrameSP, states, names, n, [this]()
536 {
538 return true;
539 }, true);
540 return true;
541 }
542
543 // ── Sensor Fusion switches ────────────────────────────────────────────────
545 {
546 SensorFusionSP.update(states, names, n);
550
551 if (SensorFusionSP[FUSION_ENABLE].getState() == ISS_ON)
552 {
553 recreateFusionFilter();
554 LOG_INFO("IMU: Software sensor fusion enabled.");
555 }
556 else
557 {
558 LOG_INFO("IMU: Software sensor fusion disabled.");
559 }
560 return true;
561 }
562
564 {
565 FusionTypeSP.update(states, names, n);
569
570 recreateFusionFilter();
571
572 LOGF_INFO("IMU: Fusion algorithm changed to %s.", FusionTypeSP.findOnSwitch()->getLabel());
573 return true;
574 }
575
576 return DefaultDevice::ISNewSwitch(dev, name, states, names, n);
577}
578
579bool IMU::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n)
580{
581 if (IMUInterface::processText(dev, name, texts, names, n))
582 return true;
583
584 return DefaultDevice::ISNewText(dev, name, texts, names, n);
585}
586
588{
591
592 // Save driver-specific properties
594 IMUFrameSP.save(fp);
598
599 // Save fusion properties (only meaningful for raw-data drivers)
601 {
603 FusionTypeSP.save(fp);
605 }
606
607 return true;
608}
609
611{
612 // Default implementation does nothing and returns false
613 // Concrete drivers should override this method
614 return false;
615}
616
617bool IMU::callHandshake()
618{
619 if (imuConnection > 0)
620 {
625 }
626
627 return Handshake();
628}
629
630// Helper function to convert quaternion to Euler angles (roll, pitch, yaw)
631// Angles are in radians
632void IMU::QuaternionToEuler(double i, double j, double k, double w, double &roll, double &pitch, double &yaw)
633{
634 // Roll (x-axis rotation)
635 double sinr_cosp = 2 * (w * i + j * k);
636 double cosr_cosp = 1 - 2 * (i * i + j * j);
637 roll = std::atan2(sinr_cosp, cosr_cosp);
638
639 // Pitch (y-axis rotation)
640 double sinp = 2 * (w * j - k * i);
641 if (std::abs(sinp) >= 1)
642 pitch = std::copysign(M_PI / 2, sinp); // Use 90 degrees if out of range
643 else
644 pitch = std::asin(sinp);
645
646 // Yaw (z-axis rotation)
647 double siny_cosp = 2 * (w * k + i * j);
648 double cosy_cosp = 1 - 2 * (j * j + k * k);
649 yaw = std::atan2(siny_cosp, cosy_cosp);
650}
651
652// Helper function to convert Euler angles (roll, pitch, yaw) in radians to a quaternion
653void IMU::EulerToQuaternion(double roll, double pitch, double yaw, double &i, double &j, double &k, double &w)
654{
655 double cy = cos(yaw * 0.5);
656 double sy = sin(yaw * 0.5);
657 double cp = cos(pitch * 0.5);
658 double sp = sin(pitch * 0.5);
659 double cr = cos(roll * 0.5);
660 double sr = sin(roll * 0.5);
661
662 w = cr * cp * cy + sr * sp * sy;
663 i = sr * cp * cy - cr * sp * sy;
664 j = cr * sp * cy + sr * cp * sy;
665 k = cr * cp * sy - sr * sp * cy;
666}
667
669{
670 /*
671 * COORDINATE TRANSFORMATION SYSTEM OVERVIEW
672 * ========================================
673 *
674 * This function implements a multi-stage coordinate transformation chain to convert
675 * IMU sensor orientation data into astronomical coordinates (Hour Angle/Declination
676 * or Azimuth/Altitude). The transformation addresses the fundamental challenge of
677 * German Equatorial Mount (GEM) axis coupling.
678 *
679 * GERMAN EQUATORIAL MOUNT AXIS COUPLING PROBLEM:
680 * ----------------------------------------------
681 * In a German Equatorial Mount, the telescope's orientation is determined by two
682 * coupled axes:
683 * 1. Right Ascension (RA) axis - aligned with Earth's rotation axis
684 * 2. Declination (DEC) axis - perpendicular to RA axis
685 *
686 * The coupling issue arises because:
687 * - A single IMU sensor measures the telescope's absolute orientation in space
688 * - But the mount's axes are mechanically coupled - rotating one affects the other
689 * - The same celestial target can be reached by multiple combinations of RA/DEC positions
690 * - This creates ambiguity in determining individual axis positions from IMU data alone
691 *
692 * CURRENT APPROACH - DIRECT COORDINATE TRANSFORMATION:
693 * ---------------------------------------------------
694 * Instead of trying to decouple individual axis positions, this implementation:
695 * 1. Uses the IMU to determine the telescope's absolute pointing direction
696 * 2. Transforms this direction through multiple coordinate frames
697 * 3. Converts the final direction to astronomical coordinates
698 *
699 * This approach provides accurate pointing information but doesn't solve the
700 * fundamental axis decoupling problem for mount control.
701 *
702 * COORDINATE TRANSFORMATION CHAIN:
703 * -------------------------------
704 * IMU Sensor Frame → Local Horizon Frame → Equatorial Frame → Astronomical Coordinates
705 *
706 * Each stage is documented in detail below.
707 */
708
709 // Log raw quaternion values for debugging
710 DEBUGF(Logger::DBG_DEBUG, "IMU: Recalculating Astro Coordinates from stored Quaternion: i=%.4f, j=%.4f, k=%.4f, w=%.4f",
711 last_q_i, last_q_j, last_q_k, last_q_w);
712
713 double rollRad, pitchRad, yawRad;
714 QuaternionToEuler(last_q_i, last_q_j, last_q_k, last_q_w, rollRad, pitchRad, yawRad);
715
716 /*
717 * STAGE 1: IMU SENSOR FRAME TO TELESCOPE POINTING VECTOR
718 * =====================================================
719 *
720 * The quaternion (last_q_w, last_q_i, last_q_j, last_q_k) represents the IMU's
721 * orientation after applying user-defined multipliers, offsets, and magnetic declination.
722 * This quaternion describes the rotation from the IMU's reference frame to the
723 * local horizon frame.
724 */
725 double qw = last_q_w;
726 double qx = last_q_i;
727 double qy = last_q_j;
728 double qz = last_q_k;
729
730 /*
731 * Define the telescope's pointing vector in the IMU's own reference frame.
732 * This vector represents the direction the telescope is pointing relative to
733 * the IMU sensor's coordinate system.
734 *
735 * Default: (1, 0, 0) assumes telescope points along IMU's X-axis
736 * This vector is configurable via the TELESCOPE_VECTOR property to accommodate
737 * different physical mounting orientations of the IMU on the telescope.
738 */
739 double vx = TelescopeVectorNP[TELESCOPE_VECTOR_X].getValue();
740 double vy = TelescopeVectorNP[TELESCOPE_VECTOR_Y].getValue();
741 double vz = TelescopeVectorNP[TELESCOPE_VECTOR_Z].getValue();
742
743 // Log telescope vector configuration
744 DEBUGF(Logger::DBG_DEBUG, "IMU: Telescope Vector: X=%.4f, Y=%.4f, Z=%.4f", vx, vy, vz);
745
746 // Log IMU frame setting
747 IMUFrame currentFrame = static_cast<IMUFrame>(IMUFrameSP.findOnSwitchIndex());
748 DEBUGF(Logger::DBG_DEBUG, "IMU: IMU Frame: %s (%d)", IMUFrameSP.findOnSwitch()->getLabel(), currentFrame);
749
750 // Log geographic location
751 double latitude = GeographicCoordNP[LOCATION_LATITUDE].getValue();
752 DEBUGF(Logger::DBG_DEBUG, "IMU: Geographic Latitude=%.6f°", latitude);
753
754 // Log orientation adjustments
755 double rollMultiplier = OrientationAdjustmentsNP[ROLL_MULTIPLIER].getValue();
756 double pitchMultiplier = OrientationAdjustmentsNP[PITCH_MULTIPLIER].getValue();
757 double yawMultiplier = OrientationAdjustmentsNP[YAW_MULTIPLIER].getValue();
758 double rollOffset = OrientationAdjustmentsNP[ROLL_OFFSET].getValue();
759 double pitchOffset = OrientationAdjustmentsNP[PITCH_OFFSET].getValue();
760 double yawOffset = OrientationAdjustmentsNP[YAW_OFFSET].getValue();
761 double magneticDeclination = MagneticDeclinationNP[0].getValue();
762
763 DEBUGF(Logger::DBG_DEBUG, "IMU: Orientation Multipliers: Roll=%.2f, Pitch=%.2f, Yaw=%.2f",
764 rollMultiplier, pitchMultiplier, yawMultiplier);
765 DEBUGF(Logger::DBG_DEBUG, "IMU: Orientation Offsets (deg): Roll=%.2f, Pitch=%.2f, Yaw=%.2f",
766 rollOffset, pitchOffset, yawOffset);
767 DEBUGF(Logger::DBG_DEBUG, "IMU: Magnetic Declination=%.4f°", magneticDeclination);
768
769 /*
770 * STAGE 2: QUATERNION-VECTOR ROTATION
771 * ===================================
772 *
773 * Rotate the telescope pointing vector by the IMU's adjusted quaternion to get the
774 * pointing direction in the local horizon frame. This is a standard quaternion-vector
775 * rotation that transforms the telescope vector from the IMU's reference frame to
776 * the local horizon frame.
777 *
778 * Mathematical Formula: V' = q * V * q_conjugate
779 * Where V is treated as a pure quaternion (0, vx, vy, vz)
780 *
781 * The expanded formula for rotated vector components (x', y', z') is:
782 * x' = vx*(1-2*qy²-2*qz²) + vy*(2*qx*qy-2*qz*qw) + vz*(2*qx*qz+2*qy*qw)
783 * y' = vx*(2*qx*qy+2*qz*qw) + vy*(1-2*qx²-2*qz²) + vz*(2*qy*qz-2*qx*qw)
784 * z' = vx*(2*qx*qz-2*qy*qw) + vy*(2*qy*qz+2*qx*qw) + vz*(1-2*qx²-2*qy²)
785 */
786 double x_hor = vx * (1 - 2 * qy * qy - 2 * qz * qz) + vy * (2 * qx * qy - 2 * qz * qw) + vz * (2 * qx * qz + 2 * qy * qw);
787 double y_hor = vx * (2 * qx * qy + 2 * qz * qw) + vy * (1 - 2 * qx * qx - 2 * qz * qz) + vz * (2 * qy * qz - 2 * qx * qw);
788 double z_hor = vx * (2 * qx * qz - 2 * qy * qw) + vy * (2 * qy * qz + 2 * qx * qw) + vz * (1 - 2 * qx * qx - 2 * qy * qy);
789
790 INDI::AlignmentSubsystem::TelescopeDirectionVector imu_vector(x_hor, y_hor, z_hor);
791
792 // Log IMU vector before frame conversion
793 DEBUGF(Logger::DBG_DEBUG, "IMU: IMU Vector (before frame conversion): X=%.4f, Y=%.4f, Z=%.4f", x_hor, y_hor, z_hor);
794
795 /*
796 * STAGE 3: IMU COORDINATE FRAME CONVERSION
797 * =======================================
798 *
799 * Convert between different IMU coordinate frame conventions. Different IMU sensors
800 * and mounting orientations may use different coordinate frame definitions:
801 *
802 * - ENU (East-North-Up): X=East, Y=North, Z=Up
803 * - NWU (North-West-Up): X=North, Y=West, Z=Up
804 * - SWU (South-West-Up): X=South, Y=West, Z=Up
805 *
806 * The imu_vector is initially in the IMU's native frame. We need to convert it to
807 * a standardized local horizon frame for subsequent astronomical calculations.
808 *
809 * COORDINATE FRAME TRANSFORMATION MATRICES:
810 * ----------------------------------------
811 * ENU → NWU: [X_nwu, Y_nwu, Z_nwu] = [X_enu, -Y_enu, Z_enu]
812 * ENU → SWU: [X_swu, Y_swu, Z_swu] = [-Y_enu, -X_enu, Z_enu]
813 */
815
816 DEBUGF(Logger::DBG_DEBUG, "IMU: Converting from ENU to %s telescope frame", IMUFrameSP.findOnSwitch()->getLabel());
818 {
819 case ENU:
820 // Telescope uses ENU - no conversion needed
821 horizontal_vector = imu_vector;
822 break;
823 case NWU:
824 // Convert East-North-Up to North-West-Up telescope frame
825 // X_nwu = Y_enu (North = ENU_Y)
826 // Y_nwu = -X_enu (West = -ENU_X)
827 // Z_nwu = Z_enu (Up = ENU_Z)
828 horizontal_vector.x = imu_vector.y;
829 horizontal_vector.y = -imu_vector.x;
830 horizontal_vector.z = imu_vector.z;
831 break;
832 case SWU:
833 // Convert East-North-Up to South-West-Up telescope frame
834 // X_swu = -Y_enu (South = -ENU_Y)
835 // Y_swu = -X_enu (West = -ENU_X)
836 // Z_swu = Z_enu (Up = ENU_Z)
837 horizontal_vector.x = -imu_vector.y;
838 horizontal_vector.y = -imu_vector.x;
839 horizontal_vector.z = imu_vector.z;
840 break;
841 }
842
843 // Log the horizontal vector
844 DEBUGF(Logger::DBG_DEBUG, "IMU: Horizontal Vector: X=%.4f, Y=%.4f, Z=%.4f", horizontal_vector.x, horizontal_vector.y,
845 horizontal_vector.z);
846
848 {
849 /*
850 * STAGE 4: LOCAL HORIZON TO EQUATORIAL COORDINATE TRANSFORMATION
851 * =============================================================
852 *
853 * Transform the horizontal vector to the equatorial frame based on the observer's
854 * geographic latitude. This is a fundamental astronomical coordinate transformation
855 * that accounts for the Earth's rotation and the observer's position.
856 *
857 * COORDINATE FRAME DEFINITIONS:
858 * ----------------------------
859 * Horizontal Frame (NWU):
860 * - X = North component (v_n)
861 * - Y = West component (v_w)
862 * - Z = Up component (v_u)
863 *
864 * Equatorial Frame:
865 * - X_eq = Points toward the meridian (local hour angle = 0)
866 * - Y_eq = Points West (same as horizontal frame)
867 * - Z_eq = Points toward the North Celestial Pole
868 *
869 * TRANSFORMATION MATHEMATICS:
870 * --------------------------
871 * The transformation rotates the horizontal frame around the East-West axis
872 * by the complement of the latitude angle (90° - latitude).
873 *
874 * For NWU input coordinates:
875 * x_eq = v_u * cos(lat) + v_n * sin(lat) [Meridian component]
876 * y_eq = v_w [West component unchanged]
877 * z_eq = v_u * sin(lat) + v_n * cos(lat) [Celestial pole component]
878 *
879 * PHYSICAL INTERPRETATION:
880 * -----------------------
881 * - At latitude 0° (equator): horizontal up = equatorial meridian
882 * - At latitude 90° (pole): horizontal north = equatorial meridian
883 * - The West component remains unchanged in both frames
884 */
885 double lat_rad = DEG_TO_RAD(latitude);
886 double sin_lat = sin(lat_rad);
887 double cos_lat = cos(lat_rad);
888
889 // Log transformation parameters
890 DEBUGF(Logger::DBG_DEBUG, "IMU: Horizontal→Equatorial transformation: lat_rad=%.6f, sin_lat=%.6f, cos_lat=%.6f",
891 lat_rad, sin_lat, cos_lat);
892
893 // Apply the latitude-based rotation transformation
894 double x_eq = (horizontal_vector.z * cos_lat) + (horizontal_vector.x * sin_lat);
895 double y_eq = horizontal_vector.y;
896 double z_eq = (horizontal_vector.z * sin_lat) + (horizontal_vector.x * cos_lat);
897
898 INDI::AlignmentSubsystem::TelescopeDirectionVector equatorial_vector(x_eq, y_eq, z_eq);
899
900 // Log the equatorial vector
901 DEBUGF(Logger::DBG_DEBUG, "IMU: Equatorial Vector: X=%.4f, Y=%.4f, Z=%.4f", x_eq, y_eq, z_eq);
902
903 /*
904 * STAGE 5: EQUATORIAL VECTOR TO ASTRONOMICAL COORDINATES
905 * =====================================================
906 *
907 * Convert the equatorial direction vector to Hour Angle and Declination.
908 * This uses the INDI Alignment Subsystem's support functions which implement
909 * the standard astronomical coordinate conversion algorithms.
910 *
911 * The LocalHourAngleDeclinationFromTelescopeDirectionVector function:
912 * - Expects a unit vector in the equatorial frame
913 * - Returns Hour Angle in hours (not degrees)
914 * - Returns Declination in degrees
915 *
916 * COORDINATE INTERPRETATION:
917 * -------------------------
918 * - Hour Angle = 0: telescope pointing at meridian (due south)
919 * - Hour Angle = 6h: telescope pointing west
920 * - Hour Angle = 12h: telescope pointing north
921 * - Hour Angle = 18h: telescope pointing east
922 * - Declination = +90°: pointing at North Celestial Pole
923 * - Declination = -90°: pointing at South Celestial Pole
924 */
927 tdv_functions.LocalHourAngleDeclinationFromTelescopeDirectionVector(equatorial_vector, eq_coords);
928
929 // Log intermediate calculation details
931 "IMU: LocalHourAngleDeclinationFromTelescopeDirectionVector returned: HA=%.6f hours, Dec=%.6f deg",
932 eq_coords.rightascension, eq_coords.declination);
933
934 // Update INDI properties with final astronomical coordinates
935 // Convert Hour Angle from hours to degrees for the property display
936 AstroCoordinatesNP[AXIS1].setValue(eq_coords.rightascension * 15.0); // HA in degrees
937 AstroCoordinatesNP[AXIS2].setValue(eq_coords.declination);
938 DEBUGF(Logger::DBG_DEBUG, "IMU: Calculated HA=%.2f deg, Dec=%.2f deg", eq_coords.rightascension * 15.0,
939 eq_coords.declination);
940 }
941 else // Alt-Az calculation
942 {
943 /*
944 * ALTERNATIVE PATH: ALTITUDE-AZIMUTH COORDINATES
945 * =============================================
946 *
947 * For Alt-Az coordinate output, we use the horizontal frame vector directly.
948 * The AltitudeAzimuthFromTelescopeDirectionVector function expects a vector
949 * in the NWU (North-West-Up) frame, which matches our horizontal_vector after
950 * frame conversion.
951 *
952 * ALTITUDE-AZIMUTH COORDINATE SYSTEM:
953 * ----------------------------------
954 * - Azimuth = 0°: North, 90°: East, 180°: South, 270°: West
955 * - Altitude = 0°: Horizon, +90°: Zenith, -90°: Nadir
956 *
957 * This coordinate system is simpler than equatorial coordinates as it doesn't
958 * require the latitude-based transformation. However, Alt-Az coordinates are
959 * time-dependent due to Earth's rotation, while equatorial coordinates are
960 * more stable for tracking celestial objects.
961 */
962 INDI::IHorizontalCoordinates horiz_coords;
964 tdv_functions.AltitudeAzimuthFromTelescopeDirectionVector(horizontal_vector, horiz_coords);
965
966 AstroCoordinatesNP[AXIS1].setValue(horiz_coords.azimuth);
967 AstroCoordinatesNP[AXIS2].setValue(horiz_coords.altitude);
968 DEBUGF(Logger::DBG_DEBUG, "IMU: Horizontal Vector for Alt/Az calc: X=%.4f, Y=%.4f, Z=%.4f",
969 horizontal_vector.x, horizontal_vector.y, horizontal_vector.z);
970 DEBUGF(Logger::DBG_DEBUG, "IMU: Calculated Az=%.2f deg, Alt=%.2f deg", horiz_coords.azimuth, horiz_coords.altitude);
971 }
972
973 /*
974 * FINAL STEP: UPDATE INDI PROPERTIES
975 * ==================================
976 */
977
978 // Common code to send the update
981}
982
983/*
984 * ORIENTATION DATA PROCESSING AND ADJUSTMENT SYSTEM
985 * =================================================
986 *
987 * This function processes raw IMU orientation data and applies a series of adjustments
988 * to compensate for mounting orientation, sensor calibration issues, and magnetic
989 * declination. The processed orientation is then used for astronomical coordinate
990 * calculations.
991 *
992 * ADJUSTMENT PIPELINE OVERVIEW:
993 * ----------------------------
994 * Raw Sensor Data → Multiplier Adjustments → Offset Adjustments → Magnetic Declination → Final Orientation
995 *
996 * PURPOSE OF EACH ADJUSTMENT STAGE:
997 * --------------------------------
998 * 1. MULTIPLIER ADJUSTMENTS: Correct for sensor axis inversions or scaling issues
999 * - Roll/Pitch/Yaw multipliers can be set to -1 to flip axes
1000 * - Useful when IMU is mounted in non-standard orientations
1001 *
1002 * 2. OFFSET ADJUSTMENTS: Apply fixed angular corrections
1003 * - Compensate for mounting alignment errors
1004 * - Can be manually set or calculated via sync operations
1005 *
1006 * 3. MAGNETIC DECLINATION: Correct for difference between magnetic and true north
1007 * - Essential for accurate azimuth/bearing calculations
1008 * - Varies by geographic location and changes over time
1009 *
1010 * QUATERNION PROCESSING RATIONALE:
1011 * -------------------------------
1012 * While adjustments are conceptually easier to understand in Euler angles,
1013 * the implementation uses quaternions for the final calculations to avoid
1014 * gimbal lock issues and maintain numerical stability. The process:
1015 * 1. Convert raw quaternion to Euler angles for multiplier application
1016 * 2. Convert back to quaternion for offset and magnetic declination application
1017 * 3. Use quaternion multiplication for final composition
1018 *
1019 * RELATIONSHIP TO COORDINATE TRANSFORMATION:
1020 * -----------------------------------------
1021 * The final adjusted quaternion (last_q_*) is used by RecalculateAstroCoordinates()
1022 * to transform the telescope pointing vector from the IMU frame to astronomical
1023 * coordinates. This creates a complete chain from raw sensor data to sky coordinates.
1024 */
1025bool IMU::SetOrientationData(double i, double j, double k, double w)
1026{
1027 /*
1028 * STAGE 1: STORE RAW SENSOR DATA
1029 * =============================
1030 *
1031 * Preserve the original, unmodified quaternion from the IMU sensor.
1032 * This raw data is essential for:
1033 * - Sync operations that need to calculate correction offsets
1034 * - Debugging and diagnostics
1035 * - Potential future recalibration without losing original data
1036 */
1037 last_raw_q_i = i;
1038 last_raw_q_j = j;
1039 last_raw_q_k = k;
1040 last_raw_q_w = w;
1041
1042 DEBUGF(Logger::DBG_DEBUG, "IMU: Raw Quaternion: i=%.4f, j=%.4f, k=%.4f, w=%.4f", i, j, k, w);
1043
1044 /*
1045 * STAGE 2: RETRIEVE USER-CONFIGURED ADJUSTMENTS
1046 * =============================================
1047 *
1048 * Load all adjustment parameters from INDI properties. These values can be:
1049 * - Set manually by the user through the client interface
1050 * - Calculated automatically by sync operations
1051 * - Loaded from saved configuration files
1052 */
1053 double rollMultiplier = OrientationAdjustmentsNP[ROLL_MULTIPLIER].getValue();
1054 double pitchMultiplier = OrientationAdjustmentsNP[PITCH_MULTIPLIER].getValue();
1055 double yawMultiplier = OrientationAdjustmentsNP[YAW_MULTIPLIER].getValue();
1056 double rollOffset = DEG_TO_RAD(OrientationAdjustmentsNP[ROLL_OFFSET].getValue());
1057 double pitchOffset = DEG_TO_RAD(OrientationAdjustmentsNP[PITCH_OFFSET].getValue());
1058 double yawOffset = DEG_TO_RAD(OrientationAdjustmentsNP[YAW_OFFSET].getValue());
1059 double magneticDeclinationRad = DEG_TO_RAD(MagneticDeclinationNP[0].getValue());
1060
1061 /*
1062 * STAGE 3: APPLY MULTIPLIER ADJUSTMENTS
1063 * =====================================
1064 *
1065 * Multipliers are applied in Euler angle space to handle axis inversions
1066 * and scaling corrections. This is necessary because:
1067 * - IMU mounting orientation may not match telescope coordinate system
1068 * - Some sensors may have inverted axes that need correction
1069 * - Multipliers of -1 effectively flip axis directions
1070 *
1071 * MATHEMATICAL PROCESS:
1072 * 1. Convert quaternion to Euler angles (roll, pitch, yaw)
1073 * 2. Apply multipliers: angle_adjusted = angle_raw * multiplier
1074 * 3. Convert back to quaternion for subsequent processing
1075 */
1076 double rawRollRad, rawPitchRad, rawYawRad;
1077 QuaternionToEuler(i, j, k, w, rawRollRad, rawPitchRad, rawYawRad);
1078
1079 // Apply axis multipliers to handle mounting orientation corrections
1080 rawRollRad *= rollMultiplier;
1081 rawPitchRad *= pitchMultiplier;
1082 rawYawRad *= yawMultiplier;
1083
1084 // Convert multiplier-adjusted Euler angles back to quaternion
1085 double temp_i, temp_j, temp_k, temp_w;
1086 EulerToQuaternion(rawRollRad, rawPitchRad, rawYawRad, temp_i, temp_j, temp_k, temp_w);
1087
1088 /*
1089 * STAGE 4: PREPARE OFFSET AND MAGNETIC DECLINATION QUATERNIONS
1090 * ============================================================
1091 *
1092 * Convert the offset angles and magnetic declination to quaternion form
1093 * for proper quaternion multiplication. This approach:
1094 * - Avoids gimbal lock issues that can occur with Euler angle arithmetic
1095 * - Maintains numerical precision for small angle corrections
1096 * - Allows proper composition of multiple rotations
1097 */
1098
1099 // Convert user-defined offset angles to a quaternion
1100 double offset_i, offset_j, offset_k, offset_w;
1101 EulerToQuaternion(rollOffset, pitchOffset, yawOffset, offset_i, offset_j, offset_k, offset_w);
1102
1103 // Convert magnetic declination (yaw-only rotation) to a quaternion
1104 double mag_i, mag_j, mag_k, mag_w;
1105 EulerToQuaternion(0, 0, magneticDeclinationRad, mag_i, mag_j, mag_k, mag_w);
1106
1107 /*
1108 * STAGE 5: QUATERNION COMPOSITION
1109 * ===============================
1110 *
1111 * Apply offset and magnetic declination corrections using quaternion multiplication.
1112 * The order of operations is critical:
1113 *
1114 * COMPOSITION ORDER: final_q = mag_q * offset_q * temp_q
1115 * Where:
1116 * - temp_q = multiplier-adjusted raw quaternion
1117 * - offset_q = user-defined offset corrections
1118 * - mag_q = magnetic declination correction
1119 *
1120 * MATHEMATICAL JUSTIFICATION:
1121 * Quaternion multiplication represents rotation composition. The rightmost
1122 * quaternion is applied first, so this order means:
1123 * 1. First apply multiplier adjustments (temp_q)
1124 * 2. Then apply offset corrections (offset_q)
1125 * 3. Finally apply magnetic declination (mag_q)
1126 *
1127 * QUATERNION MULTIPLICATION FORMULA:
1128 * For q1 * q2 = (w1*w2 - x1*x2 - y1*y2 - z1*z2,
1129 * w1*x2 + x1*w2 + y1*z2 - z1*y2,
1130 * w1*y2 - x1*z2 + y1*w2 + z1*x2,
1131 * w1*z2 + x1*y2 - y1*x2 + z1*w2)
1132 */
1133
1134 // First composition: adjusted_q = offset_q * temp_q
1135 double adjusted_w = offset_w * temp_w - offset_i * temp_i - offset_j * temp_j - offset_k * temp_k;
1136 double adjusted_i = offset_w * temp_i + offset_i * temp_w + offset_j * temp_k - offset_k * temp_j;
1137 double adjusted_j = offset_w * temp_j - offset_i * temp_k + offset_j * temp_w + offset_k * temp_i;
1138 double adjusted_k = offset_w * temp_k + offset_i * temp_j - offset_j * temp_i + offset_k * temp_w;
1139
1140 // Final composition: final_q = mag_q * adjusted_q
1141 last_q_w = mag_w * adjusted_w - mag_i * adjusted_i - mag_j * adjusted_j - mag_k * adjusted_k;
1142 last_q_i = mag_w * adjusted_i + mag_i * adjusted_w + mag_j * adjusted_k - mag_k * adjusted_j;
1143 last_q_j = mag_w * adjusted_j - mag_i * adjusted_k + mag_j * adjusted_w + mag_k * adjusted_i;
1144 last_q_k = mag_w * adjusted_k + mag_i * adjusted_j - mag_j * adjusted_i + mag_k * adjusted_w;
1145
1146 /*
1147 * STAGE 6: LOGGING AND DIAGNOSTICS
1148 * ================================
1149 *
1150 * Convert the final adjusted quaternion back to Euler angles for human-readable
1151 * logging. This helps with:
1152 * - Debugging orientation issues
1153 * - Verifying that adjustments are being applied correctly
1154 * - Monitoring the telescope's orientation in familiar angle units
1155 */
1156 double adjustedRollRad, adjustedPitchRad, adjustedYawRad;
1157 QuaternionToEuler(last_q_i, last_q_j, last_q_k, last_q_w, adjustedRollRad, adjustedPitchRad, adjustedYawRad);
1158 DEBUGF(Logger::DBG_DEBUG, "IMU: Adjusted Euler Angles (deg): Roll=%.2f, Pitch=%.2f, Yaw=%.2f",
1159 RAD_TO_DEG(adjustedRollRad), RAD_TO_DEG(adjustedPitchRad), RAD_TO_DEG(adjustedYawRad));
1160
1161 /*
1162 * STAGE 7: UPDATE INDI PROPERTIES
1163 * ===============================
1164 *
1165 * Update the standard INDI orientation properties with the RAW (unadjusted) data.
1166 * This design choice ensures that:
1167 * - Users can see the actual sensor readings
1168 * - Diagnostic tools can access unmodified sensor data
1169 * - The adjustment pipeline remains transparent
1170 *
1171 * The adjusted data is used internally for coordinate calculations but not
1172 * directly exposed through these standard properties.
1173 */
1174 QuaternionToEuler(i, j, k, w, rawRollRad, rawPitchRad, rawYawRad);
1175 OrientationNP[ORIENTATION_ROLL].setValue(RAD_TO_DEG(rawRollRad));
1176 OrientationNP[ORIENTATION_PITCH].setValue(RAD_TO_DEG(rawPitchRad));
1177 OrientationNP[ORIENTATION_YAW].setValue(RAD_TO_DEG(rawYawRad));
1181
1182 /*
1183 * STAGE 8: TRIGGER COORDINATE RECALCULATION
1184 * =========================================
1185 *
1186 * Call RecalculateAstroCoordinates() to transform the newly adjusted orientation
1187 * into astronomical coordinates (Hour Angle/Declination or Azimuth/Altitude).
1188 *
1189 * This completes the full pipeline from raw IMU sensor data to usable
1190 * astronomical pointing information, addressing the German Equatorial Mount
1191 * axis coupling challenge through direct coordinate transformation rather
1192 * than attempting to decouple individual axis positions.
1193 *
1194 * FUTURE ENHANCEMENT PATHWAY:
1195 * --------------------------
1196 * For full axis decoupling, this system could be extended with:
1197 * 1. Integration with INDI Alignment Subsystem for pointing model corrections
1198 * 2. Machine learning algorithms to learn mount-specific coupling patterns
1199 * 3. Multi-point calibration systems for improved accuracy
1200 * 4. Real-time tracking error feedback for dynamic corrections
1201 */
1203
1204 return true;
1205}
1206
1207bool IMU::SetAccelerationData(double x, double y, double z)
1208{
1209 // Buffer for software fusion (used as the accelerometer correction step)
1210 m_RawAccel = {x, y, z, true};
1211
1212 AccelerationNP[ACCELERATION_X].setValue(x);
1213 AccelerationNP[ACCELERATION_Y].setValue(y);
1214 AccelerationNP[ACCELERATION_Z].setValue(z);
1217 return true;
1218}
1219
1220bool IMU::SetGyroscopeData(double x, double y, double z)
1221{
1222 // Buffer for software fusion
1223 m_RawGyro = {x, y, z, true};
1224
1225 // ── Run software sensor fusion ───────────────────────────────────────────
1226 // The gyroscope is the highest-rate sensor and drives the integration step.
1227 // We trigger a fusion update each time new gyro data arrives, using the most
1228 // recently buffered accelerometer and magnetometer readings.
1229 if (isFusionEnabled() && m_SensorFusion && m_RawAccel.valid)
1230 {
1231 // Compute dt (time since last fusion update)
1232 auto now = std::chrono::steady_clock::now();
1233 double dt;
1234 if (!m_FusionTimerInitialized)
1235 {
1236 dt = 0.01; // Assume 100 Hz for the very first sample
1237 m_FusionTimerInitialized = true;
1238 }
1239 else
1240 {
1241 dt = std::chrono::duration<double>(now - m_LastFusionTime).count();
1242 }
1243 m_LastFusionTime = now;
1244
1245 // Convert gyro to rad/s if the driver reports in deg/s.
1246 // AngularUnitsSP defaults to DEGREES (matching the ICM-20948 which uses
1247 // getGyroX_dps()). Drivers that supply rad/s should set the property to
1248 // RADIANS so no conversion is applied.
1249 bool gyroInDegrees = (AngularUnitsSP[ANGULAR_UNITS_DEGREES].getState() == ISS_ON);
1250 double gx = gyroInDegrees ? DEG_TO_RAD(x) : x;
1251 double gy = gyroInDegrees ? DEG_TO_RAD(y) : y;
1252 double gz = gyroInDegrees ? DEG_TO_RAD(z) : z;
1253
1254 // Use magnetometer data only when available
1255 bool useMag = m_RawMag.valid;
1256 m_SensorFusion->update(
1257 gx, gy, gz,
1258 m_RawAccel.x, m_RawAccel.y, m_RawAccel.z,
1259 m_RawMag.x, m_RawMag.y, m_RawMag.z,
1260 dt, useMag);
1261
1262 // Feed fused quaternion into the existing orientation pipeline
1263 // (multipliers → offsets → mag declination → AstroCoordinates)
1264 double qw, qi, qj, qk;
1265 m_SensorFusion->getQuaternion(qw, qi, qj, qk);
1266
1268 "IMU Fusion: dt=%.4fs q=(w=%.4f, i=%.4f, j=%.4f, k=%.4f) useMag=%d",
1269 dt, qw, qi, qj, qk, useMag);
1270
1271 SetOrientationData(qi, qj, qk, qw);
1272 }
1273
1274 GyroscopeNP[GYROSCOPE_X].setValue(x);
1275 GyroscopeNP[GYROSCOPE_Y].setValue(y);
1276 GyroscopeNP[GYROSCOPE_Z].setValue(z);
1279 return true;
1280}
1281
1282bool IMU::SetMagnetometerData(double x, double y, double z)
1283{
1284 // Buffer for software fusion (used as the magnetometer correction step)
1285 m_RawMag = {x, y, z, true};
1286
1287 MagnetometerNP[MAGNETOMETER_X].setValue(x);
1288 MagnetometerNP[MAGNETOMETER_Y].setValue(y);
1289 MagnetometerNP[MAGNETOMETER_Z].setValue(z);
1292 return true;
1293}
1294
1295// ─────────────────────────────────────────────────────────────────────────────
1296// Sensor fusion helpers
1297// ─────────────────────────────────────────────────────────────────────────────
1298
1299bool IMU::isFusionEnabled() const
1300{
1301 // Fusion is only available for raw-data drivers (no hardware orientation)
1303 return false;
1304
1306}
1307
1308void IMU::recreateFusionFilter()
1309{
1310 double p1 = FusionParamsNP[FUSION_PARAM_1].getValue(); // β or Kp
1311 double p2 = FusionParamsNP[FUSION_PARAM_2].getValue(); // unused or Ki
1312
1313 if (FusionTypeSP[FUSION_MAHONY].getState() == ISS_ON)
1314 m_SensorFusion = std::make_unique<MahonyFilter>(p1, p2);
1315 else
1316 m_SensorFusion = std::make_unique<MadgwickFilter>(p1);
1317
1318 // Invalidate buffered sensor data so the filter starts from rest
1319 m_RawAccel.valid = false;
1320 m_RawGyro.valid = false;
1321 m_RawMag.valid = false;
1322 m_FusionTimerInitialized = false;
1323
1324 DEBUGF(Logger::DBG_DEBUG, "IMU: Fusion filter created: %s (p1=%.4f, p2=%.4f)",
1325 FusionTypeSP[FUSION_MADGWICK].getState() == ISS_ON ? "Madgwick" : "Mahony",
1326 p1, p2);
1327}
1329{
1330 return false;
1331}
1333{
1334 return false;
1335}
1337{
1338 return false;
1339}
1341{
1342 return false;
1343}
1344bool IMU::SetPowerMode(const std::string &mode)
1345{
1346 INDI_UNUSED(mode);
1347 return false;
1348}
1349bool IMU::SetOperationMode(const std::string &mode)
1350{
1351 INDI_UNUSED(mode);
1352 return false;
1353}
1354bool IMU::SetDistanceUnits(bool metric)
1355{
1356 INDI_UNUSED(metric);
1357 return false;
1358}
1359bool IMU::SetAngularUnits(bool degrees)
1360{
1361 INDI_UNUSED(degrees);
1362 return false;
1363}
1364bool IMU::SetUpdateRate(double rate)
1365{
1366 INDI_UNUSED(rate);
1367 return false;
1368}
1369
1370}
The I2C class manages connection with I2C devices on Linux systems. I2C communication is commonly use...
int getPortFD() const
void setDefaultBusPath(const char *path)
setDefaultBusPath Set default I2C bus path. Call this function in initProperties() of your driver if ...
void registerHandshake(std::function< bool()> callback)
registerHandshake Register a handshake function to be called once the initial connection to the devic...
The Serial class manages connection with serial devices including Bluetooth. Serial communication is ...
These functions are used to convert different coordinate systems to and from the telescope direction ...
const TelescopeDirectionVector TelescopeDirectionVectorFromAltitudeAzimuth(INDI::IHorizontalCoordinates HorizontalCoordinates)
Calculates a normalised direction vector from the supplied altitude and azimuth.
const TelescopeDirectionVector TelescopeDirectionVectorFromLocalHourAngleDeclination(INDI::IEquatorialCoordinates EquatorialCoordinates)
Calculates a telescope direction vector from the supplied local hour angle and declination.
void LocalHourAngleDeclinationFromTelescopeDirectionVector(const TelescopeDirectionVector TelescopeDirectionVector, INDI::IEquatorialCoordinates &EquatorialCoordinates)
Calculates a local hour angle and declination from the supplied telescope direction vector and declin...
void AltitudeAzimuthFromTelescopeDirectionVector(const TelescopeDirectionVector TelescopeDirectionVector, INDI::IHorizontalCoordinates &HorizontalCoordinates)
Calculates an altitude and azimuth from the supplied normalised direction vector and declination.
bool isConnected() const
const char * getDeviceName() const
bool updateProperty(PropertyType &property, ValueType *values, char *names[], int n, std::function< bool()> updater, bool saveConfig=false)
Generic helper function to update an INDI property based on an external operation.
virtual bool updateProperties()
updateProperties is called whenever there is a change in the CONNECTION status of the driver....
virtual bool saveConfig(bool silent=false, const char *property=nullptr)
Save the current properties in a configuration file.
virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n)
Process the client newSwitch command.
void registerConnection(Connection::Interface *newConnection)
registerConnection Add new connection plugin to the existing connection pool. The connection type sha...
virtual bool deleteProperty(const char *propertyName)
Delete a property and unregister it. It will also be deleted from all clients.
void defineProperty(INumberVectorProperty *property)
virtual bool saveConfigItems(FILE *fp)
saveConfigItems Save specific properties in the provide config file handler. Child class usually over...
virtual bool initProperties()
Initialize properties initial state and value. The child class must implement this function.
virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n)
Process the client newNumber command.
Connection::Interface * getActiveConnection()
virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n)
Process the client newSwitch command.
bool processSwitch(const std::string &dev, const std::string &name, ISState *states, char *names[], int n)
Process IMU switch properties.
const std::string IMU_TAB
bool saveConfigItems(FILE *fp)
saveConfigItems Save IMU properties in config file
bool processText(const std::string &dev, const std::string &name, char *texts[], char *names[], int n)
Process IMU text properties.
INDI::PropertyNumber OrientationNP
INDI::PropertySwitch AngularUnitsSP
uint32_t GetCapability() const
GetIMUCapability returns the capability of the IMU device.
INDI::PropertyNumber AccelerationNP
INDI::PropertyNumber GyroscopeNP
const std::string COORDINATES_TAB
bool updateProperties()
updateProperties Define or Delete IMU properties based on the connection status of the base device
bool processNumber(const std::string &dev, const std::string &name, double values[], char *names[], int n)
Process IMU number properties.
INDI::PropertyNumber MagnetometerNP
void SetCapability(uint32_t cap)
SetIMUCapability sets the IMU capabilities. All capabilities must be initialized.
void initProperties(const std::string &groupName)
Initialize IMU properties. It is recommended to call this function within initProperties() of your pr...
virtual bool SetOperationMode(const std::string &mode) override
SetOperationMode Set the operation mode of the IMU sensor.
Definition indiimu.cpp:1349
INDI::PropertySwitch IMUFrameSP
Definition indiimu.h:154
virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override
Process the client newSwitch command.
Definition indiimu.cpp:518
INDI::PropertySwitch AstroCoordsTypeSP
Definition indiimu.h:153
bool SetAccelerationData(double x, double y, double z) override
SetAccelerationData Set the linear acceleration data.
Definition indiimu.cpp:1207
INDI::PropertyNumber MagneticDeclinationNP
Definition indiimu.h:183
INDI::PropertyNumber FusionParamsNP
Algorithm tuning parameters (β for Madgwick; Kp & Ki for Mahony)
Definition indiimu.h:215
virtual bool SetUpdateRate(double rate) override
SetUpdateRate Set the sensor polling frequency.
Definition indiimu.cpp:1364
void RecalculateAstroCoordinates()
Definition indiimu.cpp:668
Connection::Serial * serialConnection
Definition indiimu.h:217
INDI::PropertySwitch FusionTypeSP
Select fusion algorithm: Madgwick or Mahony.
Definition indiimu.h:213
@ PITCH_MULTIPLIER
Definition indiimu.h:160
@ YAW_MULTIPLIER
Definition indiimu.h:161
@ YAW_OFFSET
Definition indiimu.h:164
@ ROLL_OFFSET
Definition indiimu.h:162
@ PITCH_OFFSET
Definition indiimu.h:163
@ ROLL_MULTIPLIER
Definition indiimu.h:159
int PortFD
Definition indiimu.h:220
@ CONNECTION_I2C
Definition indiimu.h:52
@ CONNECTION_SERIAL
Definition indiimu.h:51
@ CONNECTION_NONE
Definition indiimu.h:50
@ LOCATION_LATITUDE
Definition indiimu.h:147
@ LOCATION_ELEVATION
Definition indiimu.h:149
@ LOCATION_LONGITUDE
Definition indiimu.h:148
virtual ~IMU()
Definition indiimu.cpp:41
bool SetGyroscopeData(double x, double y, double z) override
SetGyroscopeData Set the angular velocity data.
Definition indiimu.cpp:1220
virtual bool saveConfigItems(FILE *fp) override
saveConfigItems Save specific properties in the provide config file handler. Child class usually over...
Definition indiimu.cpp:587
@ FUSION_DISABLE
Definition indiimu.h:195
@ FUSION_ENABLE
Definition indiimu.h:194
void EulerToQuaternion(double roll, double pitch, double yaw, double &i, double &j, double &k, double &w)
Definition indiimu.cpp:653
virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override
Process the client newNumber command.
Definition indiimu.cpp:241
Connection::I2C * i2cConnection
Definition indiimu.h:218
virtual bool LoadCalibrationData() override
LoadCalibrationData Load previously saved calibration data.
Definition indiimu.cpp:1336
virtual bool SaveCalibrationData() override
SaveCalibrationData Save the current calibration data.
Definition indiimu.cpp:1332
virtual bool StartCalibration() override
StartCalibration Initiate the calibration sequence.
Definition indiimu.cpp:1328
bool SetMagnetometerData(double x, double y, double z) override
SetMagnetometerData Set the magnetic field strength data.
Definition indiimu.cpp:1282
@ FUSION_MADGWICK
Definition indiimu.h:200
@ FUSION_MAHONY
Definition indiimu.h:201
void QuaternionToEuler(double i, double j, double k, double w, double &roll, double &pitch, double &yaw)
Definition indiimu.cpp:632
INDI::PropertyNumber GeographicCoordNP
Definition indiimu.h:181
virtual bool Handshake()
Performs a device-specific handshake after a connection is established. This method should be overrid...
Definition indiimu.cpp:610
@ TELESCOPE_VECTOR_X
Definition indiimu.h:174
@ TELESCOPE_VECTOR_Z
Definition indiimu.h:176
@ TELESCOPE_VECTOR_Y
Definition indiimu.h:175
virtual bool updateProperties() override
updateProperties is called whenever there is a change in the CONNECTION status of the driver....
Definition indiimu.cpp:187
@ COORD_ALTAZ
Definition indiimu.h:135
@ COORD_EQUATORIAL
Definition indiimu.h:134
virtual bool SetDistanceUnits(bool metric) override
SetUnits Set the units for sensor data.
Definition indiimu.cpp:1354
INDI::PropertyNumber TelescopeVectorNP
Definition indiimu.h:178
virtual bool SetPowerMode(const std::string &mode) override
SetPowerMode Set the power mode of the IMU sensor.
Definition indiimu.cpp:1344
INDI::PropertyNumber AstroCoordinatesNP
Definition indiimu.h:152
void setSupportedConnections(const uint8_t &value)
Definition indiimu.cpp:47
INDI::PropertyNumber SyncAxisNP
Definition indiimu.h:169
virtual bool initProperties() override
Initialize properties initial state and value. The child class must implement this function.
Definition indiimu.cpp:60
virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) override
Process the client newSwitch command.
Definition indiimu.cpp:579
INDI::PropertySwitch SensorFusionSP
Enable / Disable software sensor fusion.
Definition indiimu.h:211
INDI::PropertyNumber OrientationAdjustmentsNP
Definition indiimu.h:166
virtual bool ResetCalibration() override
ResetCalibration Reset calibration data.
Definition indiimu.cpp:1340
virtual bool SetAngularUnits(bool degrees) override
SetAngularUnits Set the angular units for sensor data.
Definition indiimu.cpp:1359
bool SetOrientationData(double roll, double pitch, double yaw, double w=0.0) override
SetOrientationData Set the orientation data (Roll, Pitch, Yaw, Quaternion W)
Definition indiimu.cpp:1025
@ FUSION_PARAM_1
β for Madgwick; Kp for Mahony
Definition indiimu.h:206
@ FUSION_PARAM_2
unused for Madgwick; Ki for Mahony
Definition indiimu.h:207
Madgwick AHRS filter (9-DOF and 6-DOF).
void setBeta(double beta)
Set the filter gain β.
Mahony AHRS filter (9-DOF and 6-DOF).
void setKp(double kp)
Set proportional gain Kp.
void setState(IPState state)
void apply(const char *format,...) const ATTRIBUTE_FORMAT_PRINTF(2
bool load()
load Attempt to load property values from configuration file.
void save(FILE *f) const
save Save property to configuration file.
bool isNameMatch(const char *otherName) const
bool update(const double values[], const char *const names[], int n)
void fill(const char *device, const char *name, const char *label, const char *group, IPerm permission, double timeout, IPState state)
bool update(const ISState states[], const char *const names[], int n)
INDI::WidgetViewSwitch * findOnSwitch() const
void fill(const char *device, const char *name, const char *label, const char *group, IPerm permission, ISRule rule, double timeout, IPState state)
const char * MAIN_CONTROL_TAB
MAIN_CONTROL_TAB Where all the primary controls for the device are located.
#define M_PI
Definition dsp.h:52
ISState
Switch state.
Definition indiapi.h:148
@ ISS_OFF
Definition indiapi.h:149
@ ISS_ON
Definition indiapi.h:150
@ IP_RW
Definition indiapi.h:184
@ IP_RO
Definition indiapi.h:182
@ IPS_IDLE
Definition indiapi.h:159
@ IPS_OK
Definition indiapi.h:160
@ ISR_1OFMANY
Definition indiapi.h:171
Implementations for common driver routines.
#define INDI_UNUSED(x)
Definition indidevapi.h:132
#define LOGF_INFO(fmt,...)
Definition indilogger.h:82
#define DEBUG(priority, msg)
Macro to print log messages. Example of usage of the Logger: DEBUG(DBG_DEBUG, "hello " << "world");.
Definition indilogger.h:56
#define LOG_INFO(txt)
Definition indilogger.h:74
#define DEBUGF(priority, msg,...)
Definition indilogger.h:57
#define RAD_TO_DEG(r)
Definition indimacros.h:131
#define DEG_TO_RAD(d)
Definition indimacros.h:128
Namespace to encapsulate INDI client, drivers, and mediator classes.
Holds a normalised direction vector (direction cosines)
Definition Common.h:69
const char * getLabel() const