Instrument Neutral Distributed Interface INDI 2.2.5
joystick.cpp
Go to the documentation of this file.
1/*******************************************************************************
2 Copyright(c) 2013-2026 Jasem Mutlaq. All rights reserved.
3
4 This program is free software; you can redistribute it and/or modify it
5 under the terms of the GNU General Public License as published by the Free
6 Software Foundation; either version 2 of the License, or (at your option)
7 any later version.
8
9 This program is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
12 more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to
16 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17 Boston, MA 02110-1301, USA.
18
19 The full GNU General Public License is included in this distribution in the
20 file called LICENSE.
21*******************************************************************************/
22
23#include "joystick.h"
24
25#include "joystickdriver.h"
27
28// LilXML for calibration file I/O (same approach as ParkData in inditelescope.cpp)
29#include "lilxml.h"
30
31#include <memory>
32#include <cstring>
33#include <cmath>
34#include <climits>
35#include <cerrno>
36#include <cstdlib>
37#include <pwd.h>
38#include <unistd.h>
39#include <wordexp.h>
40
41#include "indimacros.h"
42
43// We declare an auto pointer to joystick.
44std::unique_ptr<JoyStick> joystick(new JoyStick());
45
46// ─────────────────────────────────────────────────────────────────────────────
47// Constructor
48// ─────────────────────────────────────────────────────────────────────────────
49
51{
52 driver.reset(new JoyStickDriver());
53}
54
55// ─────────────────────────────────────────────────────────────────────────────
56// INDI DefaultDevice overrides
57// ─────────────────────────────────────────────────────────────────────────────
58
60{
61 return "Joystick";
62}
63
65{
66 bool rc = driver->Connect();
67
68 if (rc)
69 {
70 LOG_INFO("Joystick is online.");
72
73 // Load saved calibration for this hardware name (silent if not found).
74 if (loadCalibrationData())
75 LOGF_INFO("Calibration loaded for \"%s\".", driver->getName());
76 else
77 LOG_DEBUG("No saved calibration found for this joystick.");
78 }
79 else
80 LOG_INFO("Error: cannot find Joystick device.");
81
82 return rc;
83}
84
86{
87 LOG_INFO("Joystick is offline.");
88 return driver->Disconnect();
89}
90
91// ─────────────────────────────────────────────────────────────────────────────
92// setupParams — called right after a successful Connect()
93// ─────────────────────────────────────────────────────────────────────────────
94
96{
97 char propName[MAXINDINAME], propLabel[MAXINDILABEL];
98
99 if (driver == nullptr)
100 return;
101
102 int nAxis = driver->getNumOfAxes();
103 int nJoysticks = driver->getNumOfJoysticks();
104 int nButtons = driver->getNumrOfButtons();
105
106 // One PropertyNumber per joystick, each with 2 elements (Magnitude + Angle).
107 // Use the human-readable pair name from the driver (e.g. "Left Stick", "D-pad (Hat 0)").
108 JoyStickNP.clear();
109 JoyStickNP.reserve(nJoysticks);
110 for (int i = 0; i < nJoysticks; i++)
111 {
112 snprintf(propName, MAXINDINAME, "JOYSTICK_%d", i + 1);
113
114 const JoyAxisPair *pair = driver->getJoystickPair(i);
115 if (pair)
116 snprintf(propLabel, MAXINDILABEL, "%s", pair->name);
117 else
118 snprintf(propLabel, MAXINDILABEL, "Joystick %d", i + 1);
119
120 JoyStickNP.emplace_back(2);
121 auto &jp = JoyStickNP.back();
122 jp[0].fill("JOYSTICK_MAGNITUDE", "Magnitude", "%g", 0.0, 1.0, 0, 0);
123 jp[1].fill("JOYSTICK_ANGLE", "Angle", "%g", 0.0, 360.0, 0, 0);
124 jp.fill(getDeviceName(), propName, propLabel, "Monitor", IP_RO, 0, IPS_IDLE);
125 }
126
127 // Axis raw values — label each axis with its hardware type (Left-Stick X, L2 Trigger, etc.)
128 AxisNP.resize(nAxis);
129 for (int i = 0; i < nAxis; i++)
130 {
131 snprintf(propName, MAXINDINAME, "AXIS_%d", i + 1);
132 // Use axis label from driver; fall back to "Axis N"
133 const char *axLabel = driver->getAxisLabel(i);
134 if (axLabel && axLabel[0] != '\0' && strcmp(axLabel, "Unknown") != 0)
135 snprintf(propLabel, MAXINDILABEL, "%s", axLabel);
136 else
137 snprintf(propLabel, MAXINDILABEL, "Axis %d", i + 1);
138 AxisNP[i].fill(propName, propLabel, "%.f", -32767.0, 32767.0, 0, 0);
139 }
140 AxisNP.fill(getDeviceName(), "JOYSTICK_AXES", "Axes", "Monitor", IP_RO, 0, IPS_IDLE);
141
142 // Dead zone thresholds — same labels as AxisNP
143 DeadZoneNP.resize(nAxis);
144 for (int i = 0; i < nAxis; i++)
145 {
146 snprintf(propName, MAXINDINAME, "AXIS_%d", i + 1);
147 const char *axLabel = driver->getAxisLabel(i);
148 if (axLabel && axLabel[0] != '\0' && strcmp(axLabel, "Unknown") != 0)
149 snprintf(propLabel, MAXINDILABEL, "%s", axLabel);
150 else
151 snprintf(propLabel, MAXINDILABEL, "Axis %d", i + 1);
152 DeadZoneNP[i].fill(propName, propLabel, "%.f", 0, 5000, 500, 5);
153 }
154 DeadZoneNP.fill(getDeviceName(), "JOYSTICK_DEAD_ZONE", "Dead Zones", "Monitor", IP_RW, 0, IPS_IDLE);
155
156 // Initialize EMA state vectors to zero
157 m_axisEMA.assign(nAxis, 0.0);
158 m_joystickAngleCosEMA.assign(nJoysticks, 0.0);
159 m_joystickAngleSinEMA.assign(nJoysticks, 0.0);
160
161 // Button states
162 ButtonSP.resize(nButtons);
163 for (int i = 0; i < nButtons; i++)
164 {
165 snprintf(propName, MAXINDINAME, "BUTTON_%d", i + 1);
166 snprintf(propLabel, MAXINDILABEL, "Button %d", i + 1);
167 ButtonSP[i].fill(propName, propLabel, ISS_OFF);
168 }
169 ButtonSP.fill(getDeviceName(), "JOYSTICK_BUTTONS", "Buttons", "Monitor", IP_RO, ISR_NOFMANY, 0, IPS_IDLE);
170
171 // ── Per-axis calibration property ────────────────────────────────────────
172 // Three elements per axis: center, min, max.
173 AxisCalibrationNP.resize(nAxis * 3);
174 for (int i = 0; i < nAxis; i++)
175 {
176 snprintf(propName, MAXINDINAME, "AXIS_%d_CENTER", i + 1);
177 snprintf(propLabel, MAXINDILABEL, "Axis %d Center", i + 1);
178 AxisCalibrationNP[i * 3 + 0].fill(propName, propLabel, "%.f", -32767.0, 32767.0, 1, 0);
179
180 snprintf(propName, MAXINDINAME, "AXIS_%d_MIN", i + 1);
181 snprintf(propLabel, MAXINDILABEL, "Axis %d Min", i + 1);
182 AxisCalibrationNP[i * 3 + 1].fill(propName, propLabel, "%.f", -32767.0, 32767.0, 1, -32767);
183
184 snprintf(propName, MAXINDINAME, "AXIS_%d_MAX", i + 1);
185 snprintf(propLabel, MAXINDILABEL, "Axis %d Max", i + 1);
186 AxisCalibrationNP[i * 3 + 2].fill(propName, propLabel, "%.f", -32767.0, 32767.0, 1, 32767);
187 }
188 AxisCalibrationNP.fill(getDeviceName(), "JOYSTICK_AXIS_CALIBRATION", "Axis Calibration",
189 "Calibration", IP_RW, 0, IPS_IDLE);
190
191 // Initialise runtime calibration tracking vectors
192 m_axisCalCenter.assign(nAxis, 0);
193 m_axisCalMin.assign(nAxis, -32767);
194 m_axisCalMax.assign(nAxis, 32767);
195
196 // ── Per-axis reversal property ────────────────────────────────────────────
197 // One switch per axis, all default OFF. ISR_NOFMANY → each is independent.
198 AxisReverseSP.resize(nAxis);
199 for (int i = 0; i < nAxis; i++)
200 {
201 const char *axLabel = driver->getAxisLabel(i);
202 if (axLabel && axLabel[0] != '\0' && strcmp(axLabel, "Unknown") != 0)
203 snprintf(propLabel, MAXINDILABEL, "%s", axLabel);
204 else
205 snprintf(propLabel, MAXINDILABEL, "Axis %d", i + 1);
206
207 snprintf(propName, MAXINDINAME, "AXIS_%d_REVERSE", i + 1);
208 AxisReverseSP[i].fill(propName, propLabel, ISS_OFF);
209 }
210 AxisReverseSP.fill(getDeviceName(), "JOYSTICK_AXIS_REVERSE", "Reverse Axes",
211 "Calibration", IP_RW, ISR_NOFMANY, 0, IPS_IDLE);
212}
213
214// ─────────────────────────────────────────────────────────────────────────────
215// initProperties
216// ─────────────────────────────────────────────────────────────────────────────
217
219{
221
222 PortTP[0].fill("PORT", "Port", "/dev/input/js0");
224
225 JoystickInfoTP[0].fill("JOYSTICK_NAME", "Name", "");
226 JoystickInfoTP[1].fill("JOYSTICK_VERSION", "Version", "");
227 JoystickInfoTP[2].fill("JOYSTICK_NJOYSTICKS", "# Joysticks", "");
228 JoystickInfoTP[3].fill("JOYSTICK_NAXES", "# Axes", "");
229 JoystickInfoTP[4].fill("JOYSTICK_NBUTTONS", "# Buttons", "");
230 JoystickInfoTP.fill(getDeviceName(), "JOYSTICK_INFO", "Joystick Info", MAIN_CONTROL_TAB, IP_RO, 60, IPS_IDLE);
231
232 // EMA smoothing factor: 1.0 = pass-through (no filter), lower = more smoothing
233 FilterNP[0].fill("JOYSTICK_FILTER_FACTOR", "Smoothing (α)", "%.2f", 0.01, 1.0, 0.05, 0.5);
234 FilterNP.fill(getDeviceName(), "JOYSTICK_FILTER", "EMA Filter", "Options", IP_RW, 0, IPS_IDLE);
235
236 // Calibration control (defined here; AxisCalibrationNP is defined in setupParams)
237 CalibrationSP[0].fill("CALIBRATION_START", "Start Calibration", ISS_OFF);
238 CalibrationSP[1].fill("CALIBRATION_END", "End Calibration", ISS_OFF);
239 CalibrationSP.fill(getDeviceName(), "JOYSTICK_CALIBRATION", "Calibration",
240 "Calibration", IP_RW, ISR_ATMOST1, 0, IPS_IDLE);
241
242 // Build calibration file path: ~/.indi/JoystickCalibrationData.xml
243 {
244 const char *home = getenv("HOME");
245 if (!home)
246 {
247 struct passwd *pw = getpwuid(getuid());
248 if (pw) home = pw->pw_dir;
249 }
250 if (home)
251 m_calibrationFile = std::string(home) + "/.indi/JoystickCalibrationData.xml";
252 else
253 m_calibrationFile = "/tmp/JoystickCalibrationData.xml";
254 }
255
257
258 return true;
259}
260
261// ─────────────────────────────────────────────────────────────────────────────
262// updateProperties
263// ─────────────────────────────────────────────────────────────────────────────
264
266{
268
269 if (isConnected())
270 {
271 char buf[8];
272
273 // Populate joystick info text fields
274 JoystickInfoTP[0].setText(driver->getName());
275 snprintf(buf, sizeof(buf), "%d", driver->getVersion());
276 JoystickInfoTP[1].setText(buf);
277 snprintf(buf, sizeof(buf), "%d", driver->getNumOfJoysticks());
278 JoystickInfoTP[2].setText(buf);
279 snprintf(buf, sizeof(buf), "%d", driver->getNumOfAxes());
280 JoystickInfoTP[3].setText(buf);
281 snprintf(buf, sizeof(buf), "%d", driver->getNumrOfButtons());
282 JoystickInfoTP[4].setText(buf);
283
285
286 for (auto &jp : JoyStickNP)
287 defineProperty(jp);
288
291
292 // Dead zones and EMA filter
295
296 // Calibration
299
300 // Per-axis reversal — restore saved state immediately after defining
303 applyReverseToDriver();
304
305 // N.B. Only set callbacks AFTER we define our properties above
306 // because these callbacks otherwise can be called asynchronously
307 // and they mess up INDI XML output
308 driver->setJoystickCallback(joystickHelper);
309 driver->setAxisCallback(axisHelper);
310 driver->setButtonCallback(buttonHelper);
311 }
312 else
313 {
315
316 for (auto &jp : JoyStickNP)
317 deleteProperty(jp);
318 JoyStickNP.clear();
319
327
328 AxisNP.resize(0);
332 ButtonSP.resize(0);
333 m_axisEMA.clear();
334 m_joystickAngleCosEMA.clear();
335 m_joystickAngleSinEMA.clear();
336 m_axisCalCenter.clear();
337 m_axisCalMin.clear();
338 m_axisCalMax.clear();
339 m_calibrating = false;
340 }
341
342 return true;
343}
344
345// ─────────────────────────────────────────────────────────────────────────────
346// ISGetProperties / ISSnoopDevice
347// ─────────────────────────────────────────────────────────────────────────────
348
356
361
362// ─────────────────────────────────────────────────────────────────────────────
363// ISNewText
364// ─────────────────────────────────────────────────────────────────────────────
365
366bool JoyStick::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n)
367{
368 if (dev != nullptr && strcmp(dev, getDeviceName()) == 0)
369 {
370 if (PortTP.isNameMatch(name))
371 {
372 PortTP.update(texts, names, n);
374 PortTP.apply();
375 driver->setPort(PortTP[0].getText());
376 return true;
377 }
378 }
379
380 return DefaultDevice::ISNewText(dev, name, texts, names, n);
381}
382
383// ─────────────────────────────────────────────────────────────────────────────
384// ISNewNumber
385// ─────────────────────────────────────────────────────────────────────────────
386
387bool JoyStick::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n)
388{
389 if (dev != nullptr && strcmp(dev, getDeviceName()) == 0)
390 {
391 if (DeadZoneNP.isNameMatch(name))
392 {
393 DeadZoneNP.update(values, names, n);
396 return true;
397 }
398
399 if (FilterNP.isNameMatch(name))
400 {
401 FilterNP.update(values, names, n);
403 FilterNP.apply();
405 // Reset EMA state so the new alpha takes effect cleanly
406 std::fill(m_axisEMA.begin(), m_axisEMA.end(), 0.0);
407 std::fill(m_joystickAngleCosEMA.begin(), m_joystickAngleCosEMA.end(), 0.0);
408 std::fill(m_joystickAngleSinEMA.begin(), m_joystickAngleSinEMA.end(), 0.0);
409 return true;
410 }
411
412 // Manual edit of calibration numbers → push to driver immediately
414 {
415 AxisCalibrationNP.update(values, names, n);
418 applyCalibrationToDriver();
419 return true;
420 }
421 }
422
423 return INDI::DefaultDevice::ISNewNumber(dev, name, values, names, n);
424}
425
426// ─────────────────────────────────────────────────────────────────────────────
427// ISNewSwitch
428// ─────────────────────────────────────────────────────────────────────────────
429
430bool JoyStick::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n)
431{
432 if (dev != nullptr && strcmp(dev, getDeviceName()) == 0)
433 {
434 if (CalibrationSP.isNameMatch(name))
435 {
436 CalibrationSP.update(states, names, n);
437
438 if (CalibrationSP[0].getState() == ISS_ON) // Start Calibration
439 {
440 if (!isConnected())
441 {
442 LOG_WARN("Cannot start calibration: joystick is not connected.");
446 return true;
447 }
448
449 const int nAxis = static_cast<int>(AxisNP.size());
450
451 // Snapshot current raw axis values as center (user should be at rest)
452 for (int i = 0; i < nAxis; i++)
453 m_axisCalCenter[i] = static_cast<int>(AxisNP[i].getValue());
454
455 // Reset min/max tracking to extreme sentinels
456 m_axisCalMin.assign(nAxis, INT_MAX);
457 m_axisCalMax.assign(nAxis, INT_MIN);
458
459 m_calibrating = true;
461 LOG_INFO("Calibration started. Move every axis to its full extent in all directions, then click End Calibration.");
462 }
463 else if (CalibrationSP[1].getState() == ISS_ON) // End Calibration
464 {
465 m_calibrating = false;
466
467 const int nAxis = static_cast<int>(AxisNP.size());
468 bool anyUpdated = false;
469
470 for (int i = 0; i < nAxis; i++)
471 {
472 // If an axis was never moved its sentinel values remain → keep defaults
473 if (m_axisCalMin[i] == INT_MAX) m_axisCalMin[i] = -32767;
474 if (m_axisCalMax[i] == INT_MIN) m_axisCalMax[i] = 32767;
475
476 // Sanity: min must be < center < max
477 if (m_axisCalMin[i] >= m_axisCalCenter[i])
478 m_axisCalMin[i] = m_axisCalCenter[i] - 1;
479 if (m_axisCalMax[i] <= m_axisCalCenter[i])
480 m_axisCalMax[i] = m_axisCalCenter[i] + 1;
481
482 AxisCalibrationNP[i * 3 + 0].setValue(m_axisCalCenter[i]);
483 AxisCalibrationNP[i * 3 + 1].setValue(m_axisCalMin[i]);
484 AxisCalibrationNP[i * 3 + 2].setValue(m_axisCalMax[i]);
485 anyUpdated = true;
486 }
487
488 if (anyUpdated)
489 {
492 applyCalibrationToDriver();
493
494 if (saveCalibrationData())
495 LOGF_INFO("Calibration complete and saved for \"%s\".", driver->getName());
496 else
497 LOG_WARN("Calibration applied but could not be saved to file.");
498 }
499
502 }
503
505 return true;
506 }
507
508 // Per-axis reversal toggle — update driver and persist immediately
509 if (AxisReverseSP.isNameMatch(name))
510 {
511 AxisReverseSP.update(states, names, n);
514 applyReverseToDriver();
516 return true;
517 }
518 }
519
520 return INDI::DefaultDevice::ISNewSwitch(dev, name, states, names, n);
521}
522
523// ─────────────────────────────────────────────────────────────────────────────
524// Static callback trampolines
525// ─────────────────────────────────────────────────────────────────────────────
526
527void JoyStick::joystickHelper(int joystick_n, double mag, double angle)
528{
529 joystick->joystickEvent(joystick_n, mag, angle);
530}
531
532void JoyStick::buttonHelper(int button_n, int value)
533{
534 joystick->buttonEvent(button_n, value);
535}
536
537void JoyStick::axisHelper(int axis_n, int value)
538{
539 joystick->axisEvent(axis_n, value);
540}
541
542// ─────────────────────────────────────────────────────────────────────────────
543// Event handlers
544// ─────────────────────────────────────────────────────────────────────────────
545
546void JoyStick::joystickEvent(int joystick_n, double mag, double angle)
547{
548 if (!isConnected())
549 return;
550
551 LOGF_DEBUG("joystickEvent[%d]: %g @ %g", joystick_n, mag, angle);
552
553 if (joystick_n < 0 || joystick_n >= static_cast<int>(JoyStickNP.size()))
554 return;
555
556 // Snap magnitude to zero when within the normalized dead-zone of the
557 // corresponding axis pair so that a resting stick shows IDLE.
558 const int axisIdx = joystick_n * 2;
559 const double dzNorm = (axisIdx < static_cast<int>(DeadZoneNP.size()))
560 ? DeadZoneNP[axisIdx].getValue() / 32767.0
561 : 0.0;
562 if (mag <= dzNorm)
563 {
564 mag = 0.0;
565 angle = 0.0;
566 if (joystick_n < static_cast<int>(m_joystickAngleCosEMA.size()))
567 {
568 m_joystickAngleCosEMA[joystick_n] = 0.0;
569 m_joystickAngleSinEMA[joystick_n] = 0.0;
570 }
571 }
572 else
573 {
574 // Apply circular EMA to angle using unit-vector decomposition.
575 if (joystick_n < static_cast<int>(m_joystickAngleCosEMA.size()))
576 {
577 const double alpha = FilterNP[0].getValue();
578 const double rad = DEG_TO_RAD(angle);
579 m_joystickAngleCosEMA[joystick_n] = alpha * std::cos(rad) + (1.0 - alpha) * m_joystickAngleCosEMA[joystick_n];
580 m_joystickAngleSinEMA[joystick_n] = alpha * std::sin(rad) + (1.0 - alpha) * m_joystickAngleSinEMA[joystick_n];
581 double filtAngle = RAD_TO_DEG(std::atan2(m_joystickAngleSinEMA[joystick_n],
582 m_joystickAngleCosEMA[joystick_n]));
583 if (filtAngle < 0.0)
584 filtAngle += 360.0;
585 angle = filtAngle;
586 }
587 }
588
589 JoyStickNP[joystick_n].setState(mag == 0 ? IPS_IDLE : IPS_BUSY);
590 JoyStickNP[joystick_n][0].setValue(mag);
591 JoyStickNP[joystick_n][1].setValue(angle);
592 JoyStickNP[joystick_n].apply();
593}
594
595void JoyStick::axisEvent(int axis_n, int value)
596{
597 if (!isConnected())
598 return;
599
600 LOGF_DEBUG("axisEvent[%d]: %d", axis_n, value);
601
602 if (axis_n < 0 || axis_n >= static_cast<int>(AxisNP.size()))
603 return;
604
605 // ── Calibration tracking (raw value, before any filtering) ───────────────
606 if (m_calibrating && axis_n < static_cast<int>(m_axisCalMin.size()))
607 {
608 if (value < m_axisCalMin[axis_n]) m_axisCalMin[axis_n] = value;
609 if (value > m_axisCalMax[axis_n]) m_axisCalMax[axis_n] = value;
610
611 // Mirror live min/max into the calibration property so the user can
612 // watch the values update in real time.
613 AxisCalibrationNP[axis_n * 3 + 0].setValue(m_axisCalCenter[axis_n]);
614 AxisCalibrationNP[axis_n * 3 + 1].setValue(m_axisCalMin[axis_n]);
615 AxisCalibrationNP[axis_n * 3 + 2].setValue(m_axisCalMax[axis_n]);
618 }
619
620 // Apply EMA low-pass filter to smooth out rapid jitter.
621 // Smooth on deflection (moving away from center) to reduce noise,
622 // but track instantly on release (moving toward center) so that
623 // both analog sticks and digital D-pads reach zero without delay.
624 const double alpha = FilterNP[0].getValue();
625 if (axis_n < static_cast<int>(m_axisEMA.size()))
626 {
627 if (std::abs(value) < std::abs(static_cast<int>(m_axisEMA[axis_n])))
628 m_axisEMA[axis_n] = static_cast<double>(value);
629 else
630 m_axisEMA[axis_n] = alpha * static_cast<double>(value) + (1.0 - alpha) * m_axisEMA[axis_n];
631 value = static_cast<int>(std::round(m_axisEMA[axis_n]));
632 }
633
634 // All values within dead-zone are snapped to zero.
635 if (std::abs(value) <= static_cast<int>(DeadZoneNP[axis_n].getValue()))
636 value = 0;
637
638 AxisNP.setState(value == 0 ? IPS_IDLE : IPS_BUSY);
639 AxisNP[axis_n].setValue(value);
640 AxisNP.apply();
641}
642
643void JoyStick::buttonEvent(int button_n, int value)
644{
645 if (!isConnected())
646 return;
647
648 LOGF_DEBUG("buttonEvent[%d]: %s", button_n, value > 0 ? "On" : "Off");
649
650 if (button_n < 0 || button_n >= static_cast<int>(ButtonSP.size()))
651 return;
652
654 ButtonSP[button_n].setState(value == 0 ? ISS_OFF : ISS_ON);
655 ButtonSP.apply();
656}
657
658// ─────────────────────────────────────────────────────────────────────────────
659// saveConfigItems
660// ─────────────────────────────────────────────────────────────────────────────
661
663{
665
666 PortTP.save(fp);
667 DeadZoneNP.save(fp);
668 FilterNP.save(fp);
669 if (AxisReverseSP.size() > 0)
671
672 return true;
673}
674
675// ─────────────────────────────────────────────────────────────────────────────
676// applyCalibrationToDriver
677// ─────────────────────────────────────────────────────────────────────────────
678
679void JoyStick::applyCalibrationToDriver()
680{
681 const int nAxis = static_cast<int>(AxisNP.size());
682 for (int i = 0; i < nAxis; i++)
683 {
684 const int center = static_cast<int>(AxisCalibrationNP[i * 3 + 0].getValue());
685 const int minVal = static_cast<int>(AxisCalibrationNP[i * 3 + 1].getValue());
686 const int maxVal = static_cast<int>(AxisCalibrationNP[i * 3 + 2].getValue());
687 driver->setAxisCalibration(i, center, minVal, maxVal);
688 }
689}
690
691// ─────────────────────────────────────────────────────────────────────────────
692// applyReverseToDriver
693// ─────────────────────────────────────────────────────────────────────────────
694
695void JoyStick::applyReverseToDriver()
696{
697 const int nAxis = static_cast<int>(AxisReverseSP.size());
698 for (int i = 0; i < nAxis; i++)
699 driver->setAxisReversed(i, AxisReverseSP[i].getState() == ISS_ON);
700}
701
702// ─────────────────────────────────────────────────────────────────────────────
703// loadCalibrationData
704// ─────────────────────────────────────────────────────────────────────────────
705
706bool JoyStick::loadCalibrationData()
707{
708 if (!isConnected() || driver == nullptr)
709 return false;
710
711 const char *hwName = driver->getName();
712 if (!hwName || hwName[0] == '\0')
713 return false;
714
715 wordexp_t wexp;
716 if (wordexp(m_calibrationFile.c_str(), &wexp, 0) != 0)
717 {
718 wordfree(&wexp);
719 return false;
720 }
721
722 FILE *fp = fopen(wexp.we_wordv[0], "r");
723 wordfree(&wexp);
724
725 if (!fp)
726 return false; // file does not exist yet — normal on first use
727
728 LilXML *lp = newLilXML();
729 static char errmsg[XML_ERROR_SIZE];
730 XMLEle *root = readXMLFile(fp, lp, errmsg);
731 fclose(fp);
732 delLilXML(lp);
733
734 if (!root)
735 {
736 LOGF_DEBUG("loadCalibrationData: parse error: %s", errmsg);
737 return false;
738 }
739
740 // Iterate <device name="…"> children looking for our hardware name
741 bool found = false;
742 for (XMLEle *devXml = nextXMLEle(root, 1); devXml; devXml = nextXMLEle(root, 0))
743 {
744 if (strcmp(tagXMLEle(devXml), "device") != 0)
745 continue;
746
747 XMLAtt *nameAtt = findXMLAtt(devXml, "name");
748 if (!nameAtt || strcmp(valuXMLAtt(nameAtt), hwName) != 0)
749 continue;
750
751 // Found a matching device — read per-axis entries
752 found = true;
753 for (XMLEle *axXml = nextXMLEle(devXml, 1); axXml; axXml = nextXMLEle(devXml, 0))
754 {
755 if (strcmp(tagXMLEle(axXml), "axis") != 0)
756 continue;
757
758 XMLAtt *idxAtt = findXMLAtt(axXml, "index");
759 if (!idxAtt) continue;
760
761 const int idx = atoi(valuXMLAtt(idxAtt));
762 const int nAxis = static_cast<int>(AxisNP.size());
763 if (idx < 0 || idx >= nAxis)
764 continue;
765
766 XMLEle *centerXml = findXMLEle(axXml, "center");
767 XMLEle *minXml = findXMLEle(axXml, "min");
768 XMLEle *maxXml = findXMLEle(axXml, "max");
769
770 if (!centerXml || !minXml || !maxXml)
771 continue;
772
773 const int center = atoi(pcdataXMLEle(centerXml));
774 const int minVal = atoi(pcdataXMLEle(minXml));
775 const int maxVal = atoi(pcdataXMLEle(maxXml));
776
777 AxisCalibrationNP[idx * 3 + 0].setValue(center);
778 AxisCalibrationNP[idx * 3 + 1].setValue(minVal);
779 AxisCalibrationNP[idx * 3 + 2].setValue(maxVal);
780
781 m_axisCalCenter[idx] = center;
782 m_axisCalMin[idx] = minVal;
783 m_axisCalMax[idx] = maxVal;
784 }
785 break;
786 }
787
788 delXMLEle(root);
789
790 if (found)
791 {
794 applyCalibrationToDriver();
795 }
796
797 return found;
798}
799
800// ─────────────────────────────────────────────────────────────────────────────
801// saveCalibrationData
802// ─────────────────────────────────────────────────────────────────────────────
803
804bool JoyStick::saveCalibrationData()
805{
806 if (!isConnected() || driver == nullptr)
807 return false;
808
809 const char *hwName = driver->getName();
810 if (!hwName || hwName[0] == '\0')
811 return false;
812
813 // ── Step 1: Load existing file so we preserve other devices' entries ────
814 XMLEle *root = nullptr;
815 XMLEle *existingDevXml = nullptr; // entry for THIS device, if already present
816
817 wordexp_t wexp;
818 if (wordexp(m_calibrationFile.c_str(), &wexp, 0) == 0)
819 {
820 FILE *fp = fopen(wexp.we_wordv[0], "r");
821 if (fp)
822 {
823 LilXML *lp = newLilXML();
824 static char errmsg[XML_ERROR_SIZE];
825 root = readXMLFile(fp, lp, errmsg);
826 fclose(fp);
827 delLilXML(lp);
828 }
829 wordfree(&wexp);
830 }
831
832 // If the file was empty / missing / corrupt, create a fresh root element
833 if (!root)
834 root = addXMLEle(nullptr, "joystickcalibration");
835
836 // ── Step 2: Find (or create) the <device name="…"> child for us ─────────
837 for (XMLEle *devXml = nextXMLEle(root, 1); devXml; devXml = nextXMLEle(root, 0))
838 {
839 if (strcmp(tagXMLEle(devXml), "device") != 0)
840 continue;
841 XMLAtt *nameAtt = findXMLAtt(devXml, "name");
842 if (nameAtt && strcmp(valuXMLAtt(nameAtt), hwName) == 0)
843 {
844 existingDevXml = devXml;
845 break;
846 }
847 }
848
849 // Remove the old entry so we can replace it cleanly
850 if (existingDevXml)
851 delXMLEle(existingDevXml);
852
853 XMLEle *devXml = addXMLEle(root, "device");
854 addXMLAtt(devXml, "name", hwName);
855
856 // ── Step 3: Write per-axis elements ─────────────────────────────────────
857 const int nAxis = static_cast<int>(AxisNP.size());
858 char buf[32];
859
860 for (int i = 0; i < nAxis; i++)
861 {
862 snprintf(buf, sizeof(buf), "%d", i);
863 XMLEle *axXml = addXMLEle(devXml, "axis");
864 addXMLAtt(axXml, "index", buf);
865
866 XMLEle *centerXml = addXMLEle(axXml, "center");
867 XMLEle *minXml = addXMLEle(axXml, "min");
868 XMLEle *maxXml = addXMLEle(axXml, "max");
869
870 snprintf(buf, sizeof(buf), "%d", static_cast<int>(AxisCalibrationNP[i * 3 + 0].getValue()));
871 editXMLEle(centerXml, buf);
872
873 snprintf(buf, sizeof(buf), "%d", static_cast<int>(AxisCalibrationNP[i * 3 + 1].getValue()));
874 editXMLEle(minXml, buf);
875
876 snprintf(buf, sizeof(buf), "%d", static_cast<int>(AxisCalibrationNP[i * 3 + 2].getValue()));
877 editXMLEle(maxXml, buf);
878 }
879
880 // ── Step 4: Write the XML file ───────────────────────────────────────────
881 if (wordexp(m_calibrationFile.c_str(), &wexp, 0) != 0)
882 {
883 wordfree(&wexp);
884 delXMLEle(root);
885 LOGF_ERROR("saveCalibrationData: bad filename: %s", m_calibrationFile.c_str());
886 return false;
887 }
888
889 FILE *fp = fopen(wexp.we_wordv[0], "w");
890 wordfree(&wexp);
891
892 if (!fp)
893 {
894 delXMLEle(root);
895 LOGF_ERROR("saveCalibrationData: cannot open %s: %s",
896 m_calibrationFile.c_str(), strerror(errno));
897 return false;
898 }
899
900 prXMLEle(fp, root, 0);
901 fclose(fp);
902 delXMLEle(root);
903
904 return true;
905}
bool isConnected() const
const char * getDeviceName() const
INDI::PropertyText getText(const char *name) const
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.
virtual void ISGetProperties(const char *dev)
define the driver's properties to the client. Usually, only a minimum set of properties are defined t...
virtual bool ISSnoopDevice(XMLEle *root)
Process a snoop event from INDI server. This function is called when a snooped property is updated in...
virtual bool loadConfig(bool silent=false, const char *property=nullptr)
Load the last saved configuration file.
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.
void addDebugControl()
Add Debug control to the driver.
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.
void resize(size_t size)
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)
void fill(const char *device, const char *name, const char *label, const char *group, IPerm permission, ISRule rule, double timeout, IPState state)
bool update(const char *const texts[], 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)
The JoyStickDriver class provides basic functionality to read events from supported game pads under L...
The JoyStick class provides an INDI driver that displays event data from game pads....
Definition joystick.h:48
virtual bool initProperties() override
Initialize properties initial state and value. The child class must implement this function.
Definition joystick.cpp:218
virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override
Process the client newSwitch command.
Definition joystick.cpp:430
INDI::PropertyNumber FilterNP
Definition joystick.h:86
static void buttonHelper(int button_n, int value)
Definition joystick.cpp:532
virtual bool Disconnect() override
Disconnect from device.
Definition joystick.cpp:85
std::vector< double > m_axisEMA
Definition joystick.h:140
std::unique_ptr< JoyStickDriver > driver
Definition joystick.h:137
INDI::PropertyNumber AxisCalibrationNP
AxisCalibrationNP Per-axis calibration values (IP_RW).
Definition joystick.h:120
static void joystickHelper(int joystick_n, double mag, double angle)
Definition joystick.cpp:527
bool saveConfigItems(FILE *fp) override
saveConfigItems Save specific properties in the provide config file handler. Child class usually over...
Definition joystick.cpp:662
virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override
Process the client newNumber command.
Definition joystick.cpp:387
virtual const char * getDefaultName() override
Definition joystick.cpp:59
INDI::PropertyText JoystickInfoTP
Definition joystick.h:95
INDI::PropertySwitch ButtonSP
Definition joystick.h:89
virtual void ISGetProperties(const char *dev) override
define the driver's properties to the client. Usually, only a minimum set of properties are defined t...
Definition joystick.cpp:349
static void axisHelper(int axis_n, int value)
Definition joystick.cpp:537
std::vector< INDI::PropertyNumber > JoyStickNP
Definition joystick.h:79
virtual bool updateProperties() override
updateProperties is called whenever there is a change in the CONNECTION status of the driver....
Definition joystick.cpp:265
virtual bool ISSnoopDevice(XMLEle *root) override
Process a snoop event from INDI server. This function is called when a snooped property is updated in...
Definition joystick.cpp:357
INDI::PropertyText PortTP
Definition joystick.h:92
void joystickEvent(int joystick_n, double mag, double angle)
Definition joystick.cpp:546
INDI::PropertySwitch CalibrationSP
CalibrationSP Start / End the calibration sweep.
Definition joystick.h:107
virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) override
Process the client newSwitch command.
Definition joystick.cpp:366
INDI::PropertySwitch AxisReverseSP
AxisReverseSP Per-axis polarity reversal toggles (IP_RW).
Definition joystick.h:135
virtual bool Connect() override
Connect to the device. INDI::DefaultDevice implementation connects to appropriate connection interfac...
Definition joystick.cpp:64
void axisEvent(int axis_n, int value)
Definition joystick.cpp:595
INDI::PropertyNumber DeadZoneNP
Definition joystick.h:83
INDI::PropertyNumber AxisNP
Definition joystick.h:82
void setupParams()
Definition joystick.cpp:95
std::vector< double > m_joystickAngleSinEMA
Definition joystick.h:144
std::vector< double > m_joystickAngleCosEMA
Definition joystick.h:143
void buttonEvent(int button_n, int value)
Definition joystick.cpp:643
const char * MAIN_CONTROL_TAB
MAIN_CONTROL_TAB Where all the primary controls for the device are located.
const char * OPTIONS_TAB
OPTIONS_TAB Where all the driver's options are located. Those may include auxiliary controls,...
int errno
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_BUSY
Definition indiapi.h:161
@ IPS_ALERT
Definition indiapi.h:162
@ IPS_IDLE
Definition indiapi.h:159
@ IPS_OK
Definition indiapi.h:160
#define MAXINDILABEL
Definition indiapi.h:190
@ ISR_NOFMANY
Definition indiapi.h:173
@ ISR_ATMOST1
Definition indiapi.h:172
#define MAXINDINAME
Definition indiapi.h:189
#define LOGF_INFO(fmt,...)
Definition indilogger.h:82
#define LOG_DEBUG(txt)
Definition indilogger.h:75
#define LOG_WARN(txt)
Definition indilogger.h:73
#define LOGF_DEBUG(fmt,...)
Definition indilogger.h:83
#define LOGF_ERROR(fmt,...)
Definition indilogger.h:80
#define LOG_INFO(txt)
Definition indilogger.h:74
#define RAD_TO_DEG(r)
Definition indimacros.h:131
#define DEG_TO_RAD(d)
Definition indimacros.h:128
std::unique_ptr< JoyStick > joystick(new JoyStick())
XMLEle * nextXMLEle(XMLEle *ep, int init)
Iterate an XML element for a list of nesetd XML elements.
Definition lilxml.cpp:555
XMLAtt * findXMLAtt(XMLEle *ep, const char *name)
Find an XML attribute within an XML element.
Definition lilxml.cpp:524
LilXML * newLilXML()
Create a new lilxml parser.
Definition lilxml.cpp:150
char * tagXMLEle(XMLEle *ep)
Return the tag of an XML element.
Definition lilxml.cpp:600
XMLEle * addXMLEle(XMLEle *parent, const char *tag)
add an element with the given tag to the given element. parent can be NULL to make a new root.
Definition lilxml.cpp:670
XMLEle * findXMLEle(XMLEle *ep, const char *tag)
Find an XML element within an XML element.
Definition lilxml.cpp:537
char * valuXMLAtt(XMLAtt *ap)
Return the value of an XML attribute.
Definition lilxml.cpp:624
void editXMLEle(XMLEle *ep, const char *pcdata)
set the pcdata of the given element
Definition lilxml.cpp:698
void prXMLEle(FILE *fp, XMLEle *ep, int level)
Print an XML element.
Definition lilxml.cpp:844
void delXMLEle(XMLEle *ep)
delXMLEle Delete XML element.
Definition lilxml.cpp:167
char * pcdataXMLEle(XMLEle *ep)
Return the pcdata of an XML element.
Definition lilxml.cpp:606
void delLilXML(LilXML *lp)
Delete a lilxml parser.
Definition lilxml.cpp:159
XMLAtt * addXMLAtt(XMLEle *ep, const char *name, const char *valu)
Add an XML attribute to an existing XML element.
Definition lilxml.cpp:706
XMLEle * readXMLFile(FILE *fp, LilXML *lp, char ynot[])
Handy wrapper to read one xml file.
Definition lilxml.cpp:653
A little DOM-style library to handle parsing and processing an XML file.
#define XML_ERROR_SIZE
Definition lilxml.h:73
const char * DEVICE_PORT
Device serial (or bluetooth) connection port. The default value on Linux is /dev/ttyUSB0 while on Mac...
A matched pair of axis indices that together form a joystick.
const char * name
Human-readable name (e.g. "Left Stick")