Instrument Neutral Distributed Interface INDI 2.2.5
joystickdriver.cpp
Go to the documentation of this file.
1/*******************************************************************************
2 Copyright(c) 2012-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 "joystickdriver.h"
24
25#include <cstring>
26#include <sys/ioctl.h>
27
28// ─────────────────────────────────────────────────────────────────────────────
29// Axis-pair table: each entry is (absX, absY, isHat, name).
30// We search for both codes in the axis map and, if both are found, create a
31// JoyAxisPair. Order matters — we report pairs in this sequence.
32// ─────────────────────────────────────────────────────────────────────────────
33namespace
34{
35struct PairDef
36{
37 uint8_t absX, absY;
38 bool isHat;
39 const char *name;
40};
41
42static const PairDef kKnownPairs[] =
43{
44 {ABS_X, ABS_Y, false, "Left Stick"},
45 {ABS_RX, ABS_RY, false, "Right Stick"},
46 {ABS_THROTTLE, ABS_RUDDER, false, "Throttle/Rudder"},
47 {ABS_WHEEL, ABS_RUDDER, false, "Wheel/Rudder"},
48 {ABS_HAT0X, ABS_HAT0Y, true, "D-pad (Hat 0)"},
49 {ABS_HAT1X, ABS_HAT1Y, true, "Hat 1"},
50 {ABS_HAT2X, ABS_HAT2Y, true, "Hat 2"},
51 {ABS_HAT3X, ABS_HAT3Y, true, "Hat 3"},
52};
53static const int kNumKnownPairs = sizeof(kKnownPairs) / sizeof(kKnownPairs[0]);
54} // namespace
55
56// ─────────────────────────────────────────────────────────────────────────────
57// JoyStickDriver — constructor / destructor
58// ─────────────────────────────────────────────────────────────────────────────
59
61{
62 active = false;
63 pollMS = 100;
64 joystick_fd = 0;
65 joystick_ev = new js_event();
66 joystick_st = new joystick_state();
67
68 strncpy(dev_path, JOYSTICK_DEV, 256);
69
73}
74
76{
77 Disconnect();
78 delete joystick_st;
79 delete joystick_ev;
80}
81
82// ─────────────────────────────────────────────────────────────────────────────
83// Connect / Disconnect
84// ─────────────────────────────────────────────────────────────────────────────
85
87{
88 joystick_fd = open(dev_path, O_RDONLY | O_NONBLOCK);
89 if (joystick_fd <= 0)
90 return false;
91
92 ioctl(joystick_fd, JSIOCGNAME(256), name);
93 ioctl(joystick_fd, JSIOCGVERSION, &version);
94 ioctl(joystick_fd, JSIOCGAXES, &axes);
95 ioctl(joystick_fd, JSIOCGBUTTONS, &buttons);
96
97 joystick_st->axis.resize(axes);
98 joystick_st->button.resize(buttons);
99
100 // ── Query axis-to-ABS map ─────────────────────────────────────────────
101 // IMPORTANT: JSIOCGAXMAP always writes ABS_MAX+1 = 64 bytes regardless
102 // of the device's actual axis count. Using a smaller destination buffer
103 // causes heap corruption. Use a fixed 64-byte scratch buffer and copy
104 // only the relevant entries into m_axmap.
105 {
106 const int ABS_BUF_SIZE = 64; // ABS_MAX+1
107 uint8_t axmapBuf[ABS_BUF_SIZE] = {};
108 m_axmap.resize(axes, 0);
109 if (ioctl(joystick_fd, JSIOCGAXMAP, axmapBuf) < 0)
110 {
111 // Not available on older kernels — fall back to sequential codes.
112 for (int i = 0; i < axes; i++)
113 m_axmap[i] = static_cast<uint8_t>(i);
114 }
115 else
116 {
117 for (int i = 0; i < axes; i++)
118 m_axmap[i] = axmapBuf[i];
119 }
120 }
121
122 // ── Build axis pairs ──────────────────────────────────────────────────
123 buildAxisPairs();
124
125 // ── Per-axis calibration defaults ─────────────────────────────────────
126 m_calCenter.assign(axes, 0);
127 m_calMin.assign(axes, -32767);
128 m_calMax.assign(axes, 32767);
129
130 // ── Per-axis reversal defaults (all normal polarity) ──────────────────
131 m_axisReversed.assign(axes, false);
132
133 active = true;
134 pthread_create(&thread, nullptr, &JoyStickDriver::loop, this);
135 return true;
136}
137
139{
140 if (joystick_fd > 0)
141 {
142 active = false;
143 pthread_join(thread, nullptr);
144 close(joystick_fd);
145 }
146 joystick_fd = 0;
147 return true;
148}
149
150// ─────────────────────────────────────────────────────────────────────────────
151// buildAxisPairs
152// ─────────────────────────────────────────────────────────────────────────────
153
154void JoyStickDriver::buildAxisPairs()
155{
156 m_stickPairs.clear();
157 m_axisToPairIndex.assign(axes, -1);
158
159 for (int p = 0; p < kNumKnownPairs; p++)
160 {
161 const PairDef &def = kKnownPairs[p];
162 int idx0 = -1, idx1 = -1;
163
164 for (int i = 0; i < axes; i++)
165 {
166 if (m_axmap[i] == def.absX) idx0 = i;
167 if (m_axmap[i] == def.absY) idx1 = i;
168 }
169
170 if (idx0 >= 0 && idx1 >= 0)
171 {
172 // Skip if either axis is already claimed by an earlier pair
173 if (m_axisToPairIndex[idx0] >= 0 || m_axisToPairIndex[idx1] >= 0)
174 continue;
175
176 JoyAxisPair pair;
177 pair.i0 = idx0;
178 pair.i1 = idx1;
179 pair.absX = def.absX;
180 pair.absY = def.absY;
181 pair.isHat = def.isHat;
182 pair.name = def.name;
183
184 int pairIdx = static_cast<int>(m_stickPairs.size());
185 m_stickPairs.push_back(pair);
186 m_axisToPairIndex[idx0] = pairIdx;
187 m_axisToPairIndex[idx1] = pairIdx;
188 }
189 }
190
191 // If JSIOCGAXMAP was not available (all codes are sequential 0,1,2,…) and
192 // no known pairs were found, fall back to the legacy even/odd pairing.
193 if (m_stickPairs.empty())
194 {
195 for (int i = 0; i + 1 < axes; i += 2)
196 {
197 JoyAxisPair pair;
198 pair.i0 = i;
199 pair.i1 = i + 1;
200 pair.absX = static_cast<uint8_t>(i);
201 pair.absY = static_cast<uint8_t>(i + 1);
202 pair.isHat = false;
203 pair.name = "Joystick";
204
205 int pairIdx = static_cast<int>(m_stickPairs.size());
206 m_stickPairs.push_back(pair);
207 m_axisToPairIndex[i] = pairIdx;
208 m_axisToPairIndex[i + 1] = pairIdx;
209 }
210 }
211}
212
213// ─────────────────────────────────────────────────────────────────────────────
214// classifyAbsCode / absCodeLabel
215// ─────────────────────────────────────────────────────────────────────────────
216
217JoyAxisType JoyStickDriver::classifyAbsCode(uint8_t code)
218{
219 switch (code)
220 {
221 case ABS_X:
222 case ABS_Y:
223 case ABS_RX:
224 case ABS_RY:
225 case ABS_THROTTLE:
226 case ABS_RUDDER:
227 case ABS_WHEEL:
228 return JoyAxisType::STICK;
229 case ABS_Z:
230 case ABS_RZ:
231 case ABS_GAS:
232 case ABS_BRAKE:
234 case ABS_HAT0X:
235 case ABS_HAT0Y:
236 case ABS_HAT1X:
237 case ABS_HAT1Y:
238 case ABS_HAT2X:
239 case ABS_HAT2Y:
240 case ABS_HAT3X:
241 case ABS_HAT3Y:
242 return JoyAxisType::HAT;
243 default:
245 }
246}
247
248const char *JoyStickDriver::absCodeLabel(uint8_t code)
249{
250 switch (code)
251 {
252 case ABS_X:
253 return "Left-Stick X";
254 case ABS_Y:
255 return "Left-Stick Y";
256 case ABS_Z:
257 return "L2 Trigger";
258 case ABS_RX:
259 return "Right-Stick X";
260 case ABS_RY:
261 return "Right-Stick Y";
262 case ABS_RZ:
263 return "R2 Trigger";
264 case ABS_THROTTLE:
265 return "Throttle";
266 case ABS_RUDDER:
267 return "Rudder";
268 case ABS_WHEEL:
269 return "Wheel";
270 case ABS_GAS:
271 return "Gas";
272 case ABS_BRAKE:
273 return "Brake";
274 case ABS_HAT0X:
275 return "D-pad X";
276 case ABS_HAT0Y:
277 return "D-pad Y";
278 case ABS_HAT1X:
279 return "Hat1 X";
280 case ABS_HAT1Y:
281 return "Hat1 Y";
282 case ABS_HAT2X:
283 return "Hat2 X";
284 case ABS_HAT2Y:
285 return "Hat2 Y";
286 case ABS_HAT3X:
287 return "Hat3 X";
288 case ABS_HAT3Y:
289 return "Hat3 Y";
290 default:
291 return "Unknown";
292 }
293}
294
295// ─────────────────────────────────────────────────────────────────────────────
296// Public accessors
297// ─────────────────────────────────────────────────────────────────────────────
298
311
312void JoyStickDriver::setPort(const char *port)
313{
314 snprintf(dev_path, 256, "%s", port);
315}
317{
318 pollMS = ms;
319}
320
322{
323 return name;
324}
326{
327 return version;
328}
330{
331 return axes;
332}
334{
335 return buttons;
336}
337
339{
340 if (!m_stickPairs.empty())
341 return static_cast<uint8_t>(m_stickPairs.size());
342
343 // Legacy fallback
344 int n = axes / 2;
345 if (axes % 2 != 0) n++;
346 return static_cast<uint8_t>(n);
347}
348
349uint8_t JoyStickDriver::getAxisAbsCode(int axis) const
350{
351 if (axis < 0 || axis >= static_cast<int>(m_axmap.size()))
352 return 0xFF;
353 return m_axmap[axis];
354}
355
357{
358 if (axis < 0 || axis >= static_cast<int>(m_axmap.size()))
360 return classifyAbsCode(m_axmap[axis]);
361}
362
363const char *JoyStickDriver::getAxisLabel(int axis) const
364{
365 if (axis < 0 || axis >= static_cast<int>(m_axmap.size()))
366 return "Unknown";
367 return absCodeLabel(m_axmap[axis]);
368}
369
371{
372 if (n < 0 || n >= static_cast<int>(m_stickPairs.size()))
373 return nullptr;
374 return &m_stickPairs[n];
375}
376
377void JoyStickDriver::setAxisCalibration(int axis, int center, int minVal, int maxVal)
378{
379 if (axis < 0 || axis >= static_cast<int>(m_calCenter.size()))
380 return;
381 m_calCenter[axis] = center;
382 m_calMin[axis] = minVal;
383 m_calMax[axis] = maxVal;
384}
385
386void JoyStickDriver::setAxisReversed(int axis, bool reversed)
387{
388 if (axis < 0 || axis >= static_cast<int>(m_axisReversed.size()))
389 return;
390 m_axisReversed[axis] = reversed;
391}
392
394{
395 return n > -1 && n < buttons ? joystick_st->button[n] : 0;
396}
397
398// ─────────────────────────────────────────────────────────────────────────────
399// Event loop
400// ─────────────────────────────────────────────────────────────────────────────
401
402void *JoyStickDriver::loop(void *obj)
403{
404 while (reinterpret_cast<JoyStickDriver *>(obj)->active)
405 reinterpret_cast<JoyStickDriver *>(obj)->readEv();
406 return obj;
407}
408
410{
411 int bytes = read(joystick_fd, joystick_ev, sizeof(*joystick_ev));
412 if (bytes > 0)
413 {
414 joystick_ev->type &= ~JS_EVENT_INIT;
415
416 if (joystick_ev->type & JS_EVENT_BUTTON)
417 {
418 const int n = joystick_ev->number;
419 if (n < static_cast<int>(joystick_st->button.size()))
420 {
421 joystick_st->button[n] = joystick_ev->value;
422 buttonCallbackFunc(n, joystick_ev->value);
423 }
424 }
425
426 if (joystick_ev->type & JS_EVENT_AXIS)
427 {
428 const int axIdx = joystick_ev->number;
429 if (axIdx >= static_cast<int>(joystick_st->axis.size()))
430 return;
431
432 joystick_st->axis[axIdx] = joystick_ev->value;
433
434 // Dispatch axis callback (raw value)
435 axisCallbackFunc(axIdx, joystick_ev->value);
436
437 // Dispatch joystick callback if this axis belongs to a pair
438 if (axIdx < static_cast<int>(m_axisToPairIndex.size()))
439 {
440 const int pairIdx = m_axisToPairIndex[axIdx];
441 if (pairIdx >= 0)
442 {
443 joystick_position pos = joystickPosition(pairIdx);
444
445 // Reject noise
446 if (pos.r < 0.001f)
447 {
448 pos.r = 0.0f;
449 pos.theta = 0.0f;
450 }
451
452 joystickCallbackFunc(pairIdx, pos.r, pos.theta);
453 }
454 }
455 }
456 }
457 else
458 usleep(pollMS * 1000);
459}
460
461// ─────────────────────────────────────────────────────────────────────────────
462// normalizeAxis — piecewise linear [min..center..max] → [-1..0..+1]
463// ─────────────────────────────────────────────────────────────────────────────
464
465float JoyStickDriver::normalizeAxis(int value, int idx) const
466{
467 if (idx < 0 || idx >= static_cast<int>(m_calCenter.size()))
468 return value / 32767.0f;
469
470 // Apply per-axis reversal before calibration so center/min/max remain valid.
471 if (idx < static_cast<int>(m_axisReversed.size()) && m_axisReversed[idx])
472 value = -value;
473
474 const int center = m_calCenter[idx];
475 if (value >= center)
476 {
477 const int span = m_calMax[idx] - center;
478 if (span <= 0) return 0.0f;
479 return std::min(1.0f, static_cast<float>(value - center) / static_cast<float>(span));
480 }
481 else
482 {
483 const int span = center - m_calMin[idx];
484 if (span <= 0) return 0.0f;
485 return std::max(-1.0f, static_cast<float>(value - center) / static_cast<float>(span));
486 }
487}
488
489// ─────────────────────────────────────────────────────────────────────────────
490// joystickPosition — uses m_stickPairs for correct axis pairing
491// ─────────────────────────────────────────────────────────────────────────────
492
494{
496 pos.theta = pos.r = pos.x = pos.y = 0.0f;
497
498 if (n < 0 || n >= static_cast<int>(m_stickPairs.size()))
499 return pos;
500
501 const JoyAxisPair &pair = m_stickPairs[n];
502 const int i0 = pair.i0;
503 const int i1 = pair.i1;
504
505 if (i0 >= static_cast<int>(joystick_st->axis.size()) ||
506 i1 >= static_cast<int>(joystick_st->axis.size()))
507 return pos;
508
509 // Apply per-axis piecewise-linear calibration → [-1, +1].
510 // Y axis is negated: hardware UP = negative raw value.
511 const float x0 = normalizeAxis(joystick_st->axis[i0], i0);
512 const float y0 = -normalizeAxis(joystick_st->axis[i1], i1);
513
514 // Squircle mapping to keep diagonal magnitude ≈ 1 when both axes are
515 // fully deflected.
516 const float x = x0 * std::sqrt(1.0f - 0.5f * y0 * y0);
517 const float y = y0 * std::sqrt(1.0f - 0.5f * x0 * x0);
518
519 pos.x = x0;
520 pos.y = y0;
521 pos.r = std::sqrt(x * x + y * y);
522
523 // Math angle convention: E=0°, N=90°, S=270°, W=180°.
524 // This matches processNSWE() in inditelescope.cpp.
525 if (pos.r > 0.001f)
526 {
527 pos.theta = std::atan2(y, x) * (180.0f / 3.141592653589793f);
528 if (pos.theta < 0.0f)
529 pos.theta += 360.0f;
530 }
531 else
532 pos.theta = 0.0f;
533
534 return pos;
535}
536
537// ─────────────────────────────────────────────────────────────────────────────
538// Default (no-op) static callbacks
539// ─────────────────────────────────────────────────────────────────────────────
540
541void JoyStickDriver::joystickEvent(int joystick_n, double mag, double angle)
542{
543 (void)joystick_n;
544 (void)mag;
545 (void)angle;
546}
547
548void JoyStickDriver::axisEvent(int axis_n, int value)
549{
550 (void)axis_n;
551 (void)value;
552}
553
554void JoyStickDriver::buttonEvent(int button_n, int button_value)
555{
556 (void)button_n;
557 (void)button_value;
558}
The JoyStickDriver class provides basic functionality to read events from supported game pads under L...
buttonFunc buttonCallbackFunc
static void buttonEvent(int button_n, int value)
uint8_t getNumOfJoysticks()
getNumOfJoysticks Returns the number of proper analog-stick / hat pairs found via JSIOCGAXMAP....
JoyAxisType getAxisType(int axis) const
getAxisType Returns the classified type of axis.
uint8_t getAxisAbsCode(int axis) const
getAxisAbsCode Returns the ABS_* event code for axis, or 0xFF if the axis map is not available.
const JoyAxisPair * getJoystickPair(int n) const
getJoystickPair Returns the axis-pair descriptor for joystick n, or nullptr if out of range.
uint32_t getVersion()
uint8_t getNumrOfButtons()
static void * loop(void *obj)
void setAxisCalibration(int axis, int center, int minVal, int maxVal)
setAxisCalibration Set piecewise-linear calibration for one axis.
static void axisEvent(int axis_n, int value)
void setButtonCallback(buttonFunc buttonCallback)
void setAxisReversed(int axis, bool reversed)
setAxisReversed Enable or disable reversal for a single axis.
axisFunc axisCallbackFunc
joystickFunc joystickCallbackFunc
std::function< void(int axis_n, double value)> axisFunc
std::function< void(int joystick_n, double mag, double angle)> joystickFunc
void setAxisCallback(axisFunc axisCallback)
const char * getName()
static void joystickEvent(int joystick_n, double mag, double angle)
void setPoll(int ms)
void setJoystickCallback(joystickFunc joystickCallback)
bool buttonPressed(int n)
void setPort(const char *port)
const char * getAxisLabel(int axis) const
getAxisLabel Returns a human-readable label for axis (e.g. "Left-Stick X", "L2 Trigger",...
std::function< void(int button_n, int value)> buttonFunc
joystick_position joystickPosition(int n)
JoyAxisType
Axis type classification inferred from JSIOCGAXMAP.
@ HAT
Digital hat / d-pad (ABS_HAT0X/Y … ABS_HAT3Y)
@ STICK
Analog stick (ABS_X/Y, ABS_RX/RY, throttle, rudder, wheel)
@ TRIGGER
Analog trigger — single-axis input (ABS_Z/RZ, gas, brake)
#define ABS_RZ
R2 / right trigger.
#define ABS_HAT1X
#define ABS_BRAKE
#define ABS_HAT2X
#define ABS_RY
#define ABS_HAT1Y
#define ABS_RUDDER
#define ABS_HAT3Y
#define ABS_Z
L2 / left trigger.
#define ABS_Y
#define ABS_THROTTLE
#define JOYSTICK_DEV
#define ABS_X
#define ABS_HAT2Y
#define ABS_HAT0X
#define ABS_WHEEL
#define ABS_HAT0Y
#define ABS_RX
#define ABS_GAS
#define ABS_HAT3X
A matched pair of axis indices that together form a joystick.
bool isHat
True for digital hat / d-pad pairs.
const char * name
Human-readable name (e.g. "Left Stick")
uint8_t absY
ABS_* code for the Y axis.
int i0
Index of the horizontal (X) axis.
uint8_t absX
ABS_* code for the X axis.
int i1
Index of the vertical (Y) axis.
std::vector< signed short > axis
std::vector< signed short > button