Instrument Neutral Distributed Interface INDI 2.2.5
joystick_analyze.cpp
Go to the documentation of this file.
1/*******************************************************************************
2 * joystick_analyze.cpp — Standalone gamepad axis analysis & calibration
3 * diagnostic tool. Uses JSIOCGAXMAP to correctly identify axis types
4 * (analog stick, trigger, d-pad/hat) and skips non-stick axes from the
5 * interactive calibration procedure.
6 *
7 * Compile:
8 * g++ -std=c++17 -O2 -o /tmp/joystick_analyze \
9 * /home/jasem/Projects/indi/tools/joystick_analyze.cpp -lm
10 *
11 * Usage:
12 * /tmp/joystick_analyze [device]
13 * e.g. /tmp/joystick_analyze /dev/input/js2
14 ******************************************************************************/
15
16#include <algorithm>
17#include <array>
18#include <cerrno>
19#include <chrono>
20#include <climits>
21#include <cmath>
22#include <cstdio>
23#include <cstdlib>
24#include <cstring>
25#include <fcntl.h>
26#include <fstream>
27#include <iomanip>
28#include <iostream>
29#include <linux/joystick.h>
30#include <pwd.h>
31#include <sstream>
32#include <string>
33#include <sys/ioctl.h>
34#include <thread>
35#include <unistd.h>
36#include <vector>
37
38// ─────────────────────────────────────────────────────────────────────────────
39// ABS event-code constants (from linux/input-event-codes.h)
40// Duplicated here so we don't need to pull in the full evdev header.
41// ─────────────────────────────────────────────────────────────────────────────
42#ifndef ABS_X
43#define ABS_X 0x00
44#define ABS_Y 0x01
45#define ABS_Z 0x02 // L2 / left trigger
46#define ABS_RX 0x03
47#define ABS_RY 0x04
48#define ABS_RZ 0x05 // R2 / right trigger
49#define ABS_THROTTLE 0x06
50#define ABS_RUDDER 0x07
51#define ABS_WHEEL 0x08
52#define ABS_GAS 0x09
53#define ABS_BRAKE 0x0a
54#define ABS_HAT0X 0x10
55#define ABS_HAT0Y 0x11
56#define ABS_HAT1X 0x12
57#define ABS_HAT1Y 0x13
58#define ABS_HAT2X 0x14
59#define ABS_HAT2Y 0x15
60#define ABS_HAT3X 0x16
61#define ABS_HAT3Y 0x17
62#endif
63
64// ─────────────────────────────────────────────────────────────────────────────
65// Axis classification
66// ─────────────────────────────────────────────────────────────────────────────
67enum class AxisType { STICK, TRIGGER, HAT, UNKNOWN };
68
69static AxisType classifyAxis(uint8_t absCode)
70{
71 switch (absCode)
72 {
73 case ABS_X:
74 case ABS_Y:
75 case ABS_RX:
76 case ABS_RY:
77 case ABS_THROTTLE:
78 case ABS_RUDDER:
79 case ABS_WHEEL:
80 return AxisType::STICK;
81 case ABS_Z:
82 case ABS_RZ:
83 case ABS_GAS:
84 case ABS_BRAKE:
85 return AxisType::TRIGGER;
86 case ABS_HAT0X:
87 case ABS_HAT0Y:
88 case ABS_HAT1X:
89 case ABS_HAT1Y:
90 case ABS_HAT2X:
91 case ABS_HAT2Y:
92 case ABS_HAT3X:
93 case ABS_HAT3Y:
94 return AxisType::HAT;
95 default:
96 return AxisType::UNKNOWN;
97 }
98}
99
100static const char *axisTypeName(AxisType t)
101{
102 switch (t)
103 {
104 case AxisType::STICK:
105 return "STICK";
107 return "TRIGGER";
108 case AxisType::HAT:
109 return "HAT/D-PAD";
110 default:
111 return "UNKNOWN";
112 }
113}
114
115// Human-readable label for an ABS code
116static const char *absCodeLabel(uint8_t code)
117{
118 switch (code)
119 {
120 case ABS_X:
121 return "Left-Stick X";
122 case ABS_Y:
123 return "Left-Stick Y";
124 case ABS_Z:
125 return "L2 Trigger";
126 case ABS_RX:
127 return "Right-Stick X";
128 case ABS_RY:
129 return "Right-Stick Y";
130 case ABS_RZ:
131 return "R2 Trigger";
132 case ABS_THROTTLE:
133 return "Throttle";
134 case ABS_RUDDER:
135 return "Rudder";
136 case ABS_WHEEL:
137 return "Wheel";
138 case ABS_GAS:
139 return "Gas";
140 case ABS_BRAKE:
141 return "Brake";
142 case ABS_HAT0X:
143 return "D-pad X";
144 case ABS_HAT0Y:
145 return "D-pad Y";
146 case ABS_HAT1X:
147 return "Hat1 X";
148 case ABS_HAT1Y:
149 return "Hat1 Y";
150 case ABS_HAT2X:
151 return "Hat2 X";
152 case ABS_HAT2Y:
153 return "Hat2 Y";
154 case ABS_HAT3X:
155 return "Hat3 X";
156 case ABS_HAT3Y:
157 return "Hat3 Y";
158 default:
159 return "Unknown";
160 }
161}
162
163// ─────────────────────────────────────────────────────────────────────────────
164// Helpers
165// ─────────────────────────────────────────────────────────────────────────────
166
167static std::string homeDir()
168{
169 const char *h = getenv("HOME");
170 if (!h)
171 {
172 struct passwd *pw = getpwuid(getuid());
173 if (pw) h = pw->pw_dir;
174 }
175 return h ? std::string(h) : "/tmp";
176}
177
178// Piecewise linear normalisation: [min..center..max] → [-1..0..+1]
179static double normalise(int value, int center, int minVal, int maxVal)
180{
181 if (value >= center)
182 {
183 int span = maxVal - center;
184 if (span <= 0) return 0.0;
185 return std::min(1.0, static_cast<double>(value - center) / span);
186 }
187 else
188 {
189 int span = center - minVal;
190 if (span <= 0) return 0.0;
191 return std::max(-1.0, static_cast<double>(value - center) / span);
192 }
193}
194
195// Compass angle (x=East, y=North): N=0°, E=90°, S=180°, W=270°
196static double compassAngle(double x, double y)
197{
198 double a = std::atan2(x, y) * (180.0 / M_PI);
199 if (a < 0.0) a += 360.0;
200 return a;
201}
202
203// ─────────────────────────────────────────────────────────────────────────────
204// Window: collect raw axis extremes over a time window
205// ─────────────────────────────────────────────────────────────────────────────
207{
208 int min0 {INT_MAX}, max0 {INT_MIN};
209 int min1 {INT_MAX}, max1 {INT_MIN};
210};
211
212static AxisWindow collectWindow(int fd, int i0, int i1,
213 const std::vector<int> &state,
214 int durationMs)
215{
216 AxisWindow w;
217 w.min0 = w.max0 = state[i0];
218 w.min1 = w.max1 = state[i1];
219
220 auto deadline = std::chrono::steady_clock::now() +
221 std::chrono::milliseconds(durationMs);
222
223 int flags = fcntl(fd, F_GETFL, 0);
224 fcntl(fd, F_SETFL, flags | O_NONBLOCK);
225
226 while (std::chrono::steady_clock::now() < deadline)
227 {
228 js_event ev;
229 int bytes = read(fd, &ev, sizeof(ev));
230 if (bytes == sizeof(ev))
231 {
232 ev.type &= ~JS_EVENT_INIT;
233 if (ev.type & JS_EVENT_AXIS)
234 {
235 if (ev.number == i0)
236 {
237 w.min0 = std::min(w.min0, (int)ev.value);
238 w.max0 = std::max(w.max0, (int)ev.value);
239 }
240 else if (ev.number == i1)
241 {
242 w.min1 = std::min(w.min1, (int)ev.value);
243 w.max1 = std::max(w.max1, (int)ev.value);
244 }
245 }
246 }
247 else
248 std::this_thread::sleep_for(std::chrono::milliseconds(5));
249 }
250
251 fcntl(fd, F_SETFL, flags);
252 return w;
253}
254
255// Single-axis window for triggers
257{
258 int minVal {INT_MAX}, maxVal {INT_MIN};
259};
260
261static SingleWindow collectSingle(int fd, int idx,
262 const std::vector<int> &state,
263 int durationMs)
264{
265 SingleWindow w;
266 w.minVal = w.maxVal = state[idx];
267
268 auto deadline = std::chrono::steady_clock::now() +
269 std::chrono::milliseconds(durationMs);
270 int flags = fcntl(fd, F_GETFL, 0);
271 fcntl(fd, F_SETFL, flags | O_NONBLOCK);
272
273 while (std::chrono::steady_clock::now() < deadline)
274 {
275 js_event ev;
276 int bytes = read(fd, &ev, sizeof(ev));
277 if (bytes == sizeof(ev))
278 {
279 ev.type &= ~JS_EVENT_INIT;
280 if ((ev.type & JS_EVENT_AXIS) && ev.number == idx)
281 {
282 w.minVal = std::min(w.minVal, (int)ev.value);
283 w.maxVal = std::max(w.maxVal, (int)ev.value);
284 }
285 }
286 else
287 std::this_thread::sleep_for(std::chrono::milliseconds(5));
288 }
289
290 fcntl(fd, F_SETFL, flags);
291 return w;
292}
293
294// ─────────────────────────────────────────────────────────────────────────────
295// Drain init events
296// ─────────────────────────────────────────────────────────────────────────────
297static std::vector<int> drainInitEvents(int fd, int numAxes)
298{
299 std::vector<int> s(numAxes, 0);
300 int flags = fcntl(fd, F_GETFL, 0);
301 fcntl(fd, F_SETFL, flags | O_NONBLOCK);
302 js_event ev;
303 while (read(fd, &ev, sizeof(ev)) == sizeof(ev))
304 if ((ev.type & JS_EVENT_AXIS) && ev.number < numAxes)
305 s[ev.number] = ev.value;
306 fcntl(fd, F_SETFL, flags);
307 return s;
308}
309
310// ─────────────────────────────────────────────────────────────────────────────
311// Sample for report
312// ─────────────────────────────────────────────────────────────────────────────
313struct Sample
314{
315 std::string label;
316 int raw0 {0}, raw1 {0};
317 double normX {0}, normY {0};
318 double magnitude {0};
319 double angle {0};
320 double expectedAngle {-1};
321};
322
323static void printSample(std::ostream &out, const Sample &s)
324{
325 out << " " << std::left << std::setw(9) << s.label << ": "
326 << "Axis0=" << std::right << std::setw(7) << s.raw0 << " "
327 << "Axis1=" << std::right << std::setw(7) << s.raw1 << " "
328 << "normX=" << std::fixed << std::setw(6) << std::setprecision(3) << s.normX << " "
329 << "normY=" << std::fixed << std::setw(6) << std::setprecision(3) << s.normY << " "
330 << "mag=" << std::fixed << std::setw(5) << std::setprecision(3) << s.magnitude << " "
331 << "angle=" << std::right << std::setw(6) << std::fixed << std::setprecision(1) << s.angle << "°";
332
333 if (s.expectedAngle >= 0)
334 {
335 double err = s.angle - s.expectedAngle;
336 while (err > 180.0) err -= 360.0;
337 while (err < -180.0) err += 360.0;
338 out << " (expected " << std::setw(5) << s.expectedAngle
339 << "° err=" << std::showpos << std::fixed << std::setprecision(1) << err << "°"
340 << std::noshowpos << ")";
341 }
342 out << "\n";
343}
344
345// ─────────────────────────────────────────────────────────────────────────────
346// main
347// ─────────────────────────────────────────────────────────────────────────────
348int main(int argc, char *argv[])
349{
350 const char *devPath = "/dev/input/js0";
351 if (argc > 1) devPath = argv[1];
352
353 std::cout << "\n=== Joystick Analysis & Calibration Diagnostic ===\n";
354 std::cout << "Device: " << devPath << "\n\n";
355
356 int fd = open(devPath, O_RDONLY);
357 if (fd < 0)
358 {
359 std::cerr << "ERROR: cannot open " << devPath << ": " << strerror(errno) << "\n";
360 return 1;
361 }
362
363 // Query device info
364 char name[256] = "Unknown";
365 uint32_t version = 0;
366 uint8_t numAxes = 0, numButtons = 0;
367 ioctl(fd, JSIOCGNAME(sizeof(name)), name);
368 ioctl(fd, JSIOCGVERSION, &version);
369 ioctl(fd, JSIOCGAXES, &numAxes);
370 ioctl(fd, JSIOCGBUTTONS, &numButtons);
371
372 // Query axis-to-ABS map.
373 // IMPORTANT: JSIOCGAXMAP always writes ABS_MAX+1 = 64 bytes into the buffer
374 // regardless of how many axes the device actually has. Passing a smaller
375 // buffer causes heap corruption. Always allocate the full 64-byte scratch
376 // buffer and copy only the relevant entries afterwards.
377 const int ABS_BUF_SIZE = 64; // ABS_MAX+1 from linux/input.h
378 uint8_t axmapBuf[ABS_BUF_SIZE] = {};
379 std::vector<uint8_t> axmap(numAxes, 0);
380 if (ioctl(fd, JSIOCGAXMAP, axmapBuf) < 0)
381 {
382 // Fall back to sequential ABS codes if ioctl fails
383 for (int i = 0; i < numAxes; i++) axmap[i] = static_cast<uint8_t>(i);
384 std::cerr << "WARNING: JSIOCGAXMAP not available; axis types may be wrong.\n";
385 }
386 else
387 {
388 for (int i = 0; i < numAxes; i++)
389 axmap[i] = axmapBuf[i];
390 }
391
392 std::cout << "Name : " << name << "\n";
393 std::cout << "Version : " << version << "\n";
394 std::cout << "Axes : " << (int)numAxes << "\n";
395 std::cout << "Buttons : " << (int)numButtons << "\n\n";
396
397 // Print axis map
398 std::cout << "Axis map (from JSIOCGAXMAP):\n";
399 for (int i = 0; i < numAxes; i++)
400 {
401 std::cout << " Axis " << i << " ABS=0x"
402 << std::right << std::hex << std::setw(2) << std::setfill('0')
403 << (int)axmap[i] << std::dec << std::setfill(' ')
404 << " Type=" << std::left << std::setw(10) << axisTypeName(classifyAxis(axmap[i]))
405 << std::right << " Label=" << absCodeLabel(axmap[i]) << "\n";
406 }
407 std::cout << "\n";
408
409 // Drain init events
410 std::vector<int> axisState = drainInitEvents(fd, numAxes);
411
412 // ── Find analog stick pairs ───────────────────────────────────────────
413 // A stick pair is (ABS_X + ABS_Y) or (ABS_RX + ABS_RY) etc.
414 // We look for matching X/Y pairs based on ABS code.
415 struct StickPair
416 {
417 int i0, i1; // axis indices
418 uint8_t absX, absY; // ABS codes
419 std::string name;
420 };
421
422 std::vector<StickPair> stickPairs;
423
424 // Known X+Y pairs in order of preference
425 const std::array<std::pair<uint8_t, uint8_t>, 6> knownPairs = {{
426 {ABS_X, ABS_Y},
427 {ABS_RX, ABS_RY},
432 }
433 };
434
435 const std::array<const char *, 6> pairNames = {{
436 "Left Stick",
437 "Right Stick",
438 "Throttle/Rudder",
439 "D-pad (Hat 0)",
440 "Hat 1",
441 "Hat 2",
442 }
443 };
444
445 for (size_t p = 0; p < knownPairs.size(); p++)
446 {
447 int idx0 = -1, idx1 = -1;
448 for (int i = 0; i < numAxes; i++)
449 {
450 if (axmap[i] == knownPairs[p].first) idx0 = i;
451 if (axmap[i] == knownPairs[p].second) idx1 = i;
452 }
453 if (idx0 >= 0 && idx1 >= 0)
454 {
455 StickPair sp;
456 sp.i0 = idx0;
457 sp.i1 = idx1;
458 sp.absX = knownPairs[p].first;
459 sp.absY = knownPairs[p].second;
460 sp.name = pairNames[p];
461 stickPairs.push_back(sp);
462 }
463 }
464
465 // ── Find triggers ─────────────────────────────────────────────────────
466 struct Trigger
467 {
468 int idx;
469 uint8_t absCode;
470 std::string label;
471 };
472 std::vector<Trigger> triggers;
473 for (int i = 0; i < numAxes; i++)
474 {
475 if (classifyAxis(axmap[i]) == AxisType::TRIGGER)
476 triggers.push_back({i, axmap[i], absCodeLabel(axmap[i])});
477 }
478
479 // ── Summary of what we found ──────────────────────────────────────────
480 std::cout << "Found " << stickPairs.size() << " joystick pair(s):\n";
481 for (const auto &sp : stickPairs)
482 std::cout << " " << sp.name << " → Axis " << sp.i0 << " + Axis " << sp.i1 << "\n";
483 std::cout << "Found " << triggers.size() << " trigger(s):\n";
484 for (const auto &tr : triggers)
485 std::cout << " " << tr.label << " → Axis " << tr.idx << "\n";
486 std::cout << "\n";
487
488 // ─────────────────────────────────────────────────────────────────────
489 // Per-stick interactive calibration test
490 // ─────────────────────────────────────────────────────────────────────
491 struct DirTest
492 {
493 const char *label;
494 double expectedAngle;
495 bool useMaxAxis0, useMaxAxis1;
496 bool testAxis0, testAxis1;
497 };
498
499 const std::array<DirTest, 8> dirTests = {{
500 {"UP", 0.0, false, false, false, true},
501 {"UP+RIGHT", 45.0, true, false, true, true},
502 {"RIGHT", 90.0, true, false, true, false},
503 {"DN+RIGHT", 135.0, true, true, true, true},
504 {"DOWN", 180.0, false, true, false, true},
505 {"DN+LEFT", 225.0, false, true, true, true},
506 {"LEFT", 270.0, false, false, true, false},
507 {"UP+LEFT", 315.0, false, false, true, true},
508 }
509 };
510
511 struct JoyResult
512 {
513 StickPair sp;
514 std::vector<Sample> samples;
515 int calCenter0, calMin0, calMax0;
516 int calCenter1, calMin1, calMax1;
517 bool isHat;
518 };
519 std::vector<JoyResult> joyResults;
520
521 for (const auto &sp : stickPairs)
522 {
523 bool isHat = (classifyAxis(sp.absX) == AxisType::HAT);
524
525 std::cout <<
526 "══════════════════════════════════════════════\n"
527 << " " << sp.name << " (Axis " << sp.i0 << " = " << absCodeLabel(sp.absX)
528 << ", Axis " << sp.i1 << " = " << absCodeLabel(sp.absY) << ")\n"
529 << "══════════════════════════════════════════════\n\n";
530
531 JoyResult res;
532 res.sp = sp;
533 res.isHat = isHat;
534
535 if (isHat)
536 {
537 // D-pad: digital, already perfect — just snapshot the current range
538 std::cout << " D-pad/hat detected. Snapping a quick sample (press all 4 directions).\n"
539 " Press Enter when ready, then move d-pad to all 4 directions...\n";
540 std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
541 std::cout << " Sampling for 4 seconds... ";
542 std::cout.flush();
543 AxisWindow w = collectWindow(fd, sp.i0, sp.i1, axisState, 4000);
544 std::cout << "done.\n\n";
545 res.calCenter0 = 0;
546 res.calMin0 = w.min0;
547 res.calMax0 = w.max0;
548 res.calCenter1 = 0;
549 res.calMin1 = w.min1;
550 res.calMax1 = w.max1;
551
552 // Compute sample for center
553 {
554 Sample s;
555 s.label = "CENTER";
556 s.raw0 = 0;
557 s.raw1 = 0;
558 s.normX = 0;
559 s.normY = 0;
560 s.magnitude = 0;
561 s.angle = 0;
562 res.samples.push_back(s);
563 }
564 // Compute expected values for each direction using what we saw
565 for (const auto &dt : dirTests)
566 {
567 int r0 = dt.testAxis0 ? (dt.useMaxAxis0 ? w.max0 : w.min0) : 0;
568 int r1 = dt.testAxis1 ? (dt.useMaxAxis1 ? w.max1 : w.min1) : 0;
569 double nx = normalise(r0, 0, w.min0, w.max0);
570 double ny = -normalise(r1, 0, w.min1, w.max1);
571 double sx = nx * std::sqrt(1.0 - 0.5 * ny * ny);
572 double sy = ny * std::sqrt(1.0 - 0.5 * nx * nx);
573 double mag = std::sqrt(sx * sx + sy * sy);
574 double ang = (mag > 0.001) ? compassAngle(sx, sy) : 0.0;
575 Sample s;
576 s.label = dt.label;
577 s.raw0 = r0;
578 s.raw1 = r1;
579 s.normX = nx;
580 s.normY = ny;
581 s.magnitude = mag;
582 s.angle = ang;
583 s.expectedAngle = dt.expectedAngle;
584 res.samples.push_back(s);
585 }
586 joyResults.push_back(res);
587 continue;
588 }
589
590 // ── Step 1: Center ────────────────────────────────────────────────
591 std::cout << " [1] Release stick to CENTER/REST, then press Enter and hold still...\n";
592 std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
593 std::cout << " Sampling 1.5s... ";
594 std::cout.flush();
595 AxisWindow cw = collectWindow(fd, sp.i0, sp.i1, axisState, 1500);
596 int c0 = (cw.min0 + cw.max0) / 2;
597 int c1 = (cw.min1 + cw.max1) / 2;
598 std::cout << "done. Center: Axis" << sp.i0 << "=" << c0
599 << " Axis" << sp.i1 << "=" << c1 << "\n\n";
600
601 res.calCenter0 = c0;
602 res.calCenter1 = c1;
603 res.calMin0 = -32767;
604 res.calMax0 = 32767;
605 res.calMin1 = -32767;
606 res.calMax1 = 32767;
607
608 {
609 Sample s;
610 s.label = "CENTER";
611 s.raw0 = c0;
612 s.raw1 = c1;
613 res.samples.push_back(s);
614 }
615
616 // ── Step 2: Directions ────────────────────────────────────────────
617 struct DirCap
618 {
619 AxisWindow win;
620 int raw0, raw1;
621 };
622 std::vector<DirCap> caps(dirTests.size());
623
624 int omin0 = INT_MAX, omax0 = INT_MIN;
625 int omin1 = INT_MAX, omax1 = INT_MIN;
626
627 for (size_t di = 0; di < dirTests.size(); di++)
628 {
629 const auto &dt = dirTests[di];
630 std::cout << " [" << di + 2 << "] Push " << sp.name << " fully " << dt.label
631 << ", then press Enter and hold 1.5s...\n";
632 std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
633 std::cout << " Sampling... ";
634 std::cout.flush();
635
636 AxisWindow w = collectWindow(fd, sp.i0, sp.i1, axisState, 1500);
637 caps[di].win = w;
638 caps[di].raw0 = dt.testAxis0 ? (dt.useMaxAxis0 ? w.max0 : w.min0) : c0;
639 caps[di].raw1 = dt.testAxis1 ? (dt.useMaxAxis1 ? w.max1 : w.min1) : c1;
640
641 omin0 = std::min(omin0, w.min0);
642 omax0 = std::max(omax0, w.max0);
643 omin1 = std::min(omin1, w.min1);
644 omax1 = std::max(omax1, w.max1);
645
646 std::cout << "done. Axis" << sp.i0 << ": [" << w.min0 << ", " << w.max0 << "]"
647 << " Axis" << sp.i1 << ": [" << w.min1 << ", " << w.max1 << "]\n\n";
648 }
649
650 res.calMin0 = omin0;
651 res.calMax0 = omax0;
652 res.calMin1 = omin1;
653 res.calMax1 = omax1;
654
655 // Compute angles with final calibration
656 for (size_t di = 0; di < dirTests.size(); di++)
657 {
658 const auto &dt = dirTests[di];
659 int r0 = caps[di].raw0, r1 = caps[di].raw1;
660 double nx = normalise(r0, c0, omin0, omax0);
661 double ny = -normalise(r1, c1, omin1, omax1);
662 double sx = nx * std::sqrt(1.0 - 0.5 * ny * ny);
663 double sy = ny * std::sqrt(1.0 - 0.5 * nx * nx);
664 double mag = std::sqrt(sx * sx + sy * sy);
665 double ang = (mag > 0.001) ? compassAngle(sx, sy) : 0.0;
666
667 Sample s;
668 s.label = dt.label;
669 s.raw0 = r0;
670 s.raw1 = r1;
671 s.normX = nx;
672 s.normY = ny;
673 s.magnitude = mag;
674 s.angle = ang;
675 s.expectedAngle = dt.expectedAngle;
676 res.samples.push_back(s);
677 }
678
679 joyResults.push_back(res);
680 }
681
682 // ─────────────────────────────────────────────────────────────────────
683 // Trigger section: just observe rest and full press
684 // ─────────────────────────────────────────────────────────────────────
685 struct TriggerResult
686 {
687 Trigger tr;
688 int restVal, pressVal;
689 };
690 std::vector<TriggerResult> trigResults;
691
692 if (!triggers.empty())
693 {
694 std::cout <<
695 "══════════════════════════════════════════════\n"
696 << " TRIGGERS\n"
697 << "══════════════════════════════════════════════\n\n";
698
699 for (const auto &tr : triggers)
700 {
701 std::cout << " [" << tr.label << " / Axis " << tr.idx << "]\n";
702 std::cout << " Release trigger fully, then press Enter...\n";
703 std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
704 std::cout << " Sampling 1s rest... ";
705 std::cout.flush();
706 SingleWindow rw = collectSingle(fd, tr.idx, axisState, 1000);
707 std::cout << "done. rest=" << rw.minVal << " to " << rw.maxVal << "\n";
708
709 std::cout << " Press trigger FULLY, then press Enter...\n";
710 std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
711 std::cout << " Sampling 1s full press... ";
712 std::cout.flush();
713 SingleWindow pw = collectSingle(fd, tr.idx, axisState, 1000);
714 std::cout << "done. pressed=" << pw.minVal << " to " << pw.maxVal << "\n\n";
715
716 TriggerResult tres;
717 tres.tr = tr;
718 tres.restVal = rw.minVal; // minimum = rest position
719 tres.pressVal = pw.maxVal; // maximum = fully pressed
720 trigResults.push_back(tres);
721 }
722 }
723
724 // ─────────────────────────────────────────────────────────────────────
725 // Build report
726 // ─────────────────────────────────────────────────────────────────────
727 std::ostringstream report;
728 report << "=== Joystick Analysis Report ===\n";
729 report << "Device : " << devPath << "\n";
730 report << "Name : " << name << "\n";
731 report << "Version : " << version << "\n";
732 report << "Axes : " << (int)numAxes << " | Buttons: " << (int)numButtons << "\n\n";
733
734 report << "Axis map:\n";
735 for (int i = 0; i < numAxes; i++)
736 report << " Axis " << i << " ABS=0x"
737 << std::right << std::hex << std::setw(2) << std::setfill('0')
738 << (int)axmap[i] << std::dec << std::setfill(' ')
739 << " Type=" << std::left << std::setw(10) << axisTypeName(classifyAxis(axmap[i]))
740 << std::right << " " << absCodeLabel(axmap[i]) << "\n";
741 report << "\n";
742
743 // Joystick results
744 for (const auto &res : joyResults)
745 {
746 report << "──────────────────────────────────────────────────────\n";
747 report << res.sp.name << " (Axis " << res.sp.i0 << " + Axis " << res.sp.i1 << ")"
748 << (res.isHat ? " [DIGITAL / D-PAD — no calibration needed]" : "") << "\n";
749 if (!res.isHat)
750 {
751 report << "Calibration:\n";
752 report << " Axis " << res.sp.i0 << ": center=" << res.calCenter0
753 << " min=" << res.calMin0 << " max=" << res.calMax0 << "\n";
754 report << " Axis " << res.sp.i1 << ": center=" << res.calCenter1
755 << " min=" << res.calMin1 << " max=" << res.calMax1 << "\n";
756 }
757 report << "\n Results:\n";
758 for (const auto &s : res.samples)
759 printSample(report, s);
760
761 if (!res.isHat)
762 {
763 report << "\n Suggested INDI AxisCalibrationNP values:\n";
764 report << " AXIS_" << res.sp.i0 + 1 << "_CENTER = " << res.calCenter0 << "\n";
765 report << " AXIS_" << res.sp.i0 + 1 << "_MIN = " << res.calMin0 << "\n";
766 report << " AXIS_" << res.sp.i0 + 1 << "_MAX = " << res.calMax0 << "\n";
767 report << " AXIS_" << res.sp.i1 + 1 << "_CENTER = " << res.calCenter1 << "\n";
768 report << " AXIS_" << res.sp.i1 + 1 << "_MIN = " << res.calMin1 << "\n";
769 report << " AXIS_" << res.sp.i1 + 1 << "_MAX = " << res.calMax1 << "\n";
770 }
771 report << "\n";
772 }
773
774 // Trigger results
775 if (!trigResults.empty())
776 {
777 report << "──────────────────────────────────────────────────────\n";
778 report << "TRIGGERS\n\n";
779 for (const auto &tr : trigResults)
780 {
781 report << " " << tr.tr.label << " (Axis " << tr.tr.idx << "):\n";
782 report << " rest=" << tr.restVal << " fully-pressed=" << tr.pressVal << "\n";
783 double norm = normalise(tr.pressVal, tr.restVal, tr.restVal, tr.pressVal);
784 report << " Normalised at full press = " << norm << " (should be 1.0)\n";
785 report << " AXIS_" << tr.tr.idx + 1 << "_CENTER = " << tr.restVal << "\n";
786 report << " AXIS_" << tr.tr.idx + 1 << "_MIN = " << tr.restVal << "\n";
787 report << " AXIS_" << tr.tr.idx + 1 << "_MAX = " << tr.pressVal << "\n\n";
788 }
789 }
790
791 std::cout << "\n" << report.str();
792
793 std::string outPath = homeDir() + "/joystick_analysis.txt";
794 std::ofstream ofs(outPath);
795 if (ofs)
796 {
797 ofs << report.str();
798 std::cout << "Report saved to: " << outPath << "\n";
799 }
800 else
801 std::cerr << "WARNING: could not write to " << outPath << "\n";
802
803 close(fd);
804 return 0;
805}
#define M_PI
Definition dsp.h:52
int main()
int errno
int fd
#define ABS_RZ
#define ABS_HAT1X
#define ABS_BRAKE
#define ABS_HAT2X
#define ABS_RY
#define ABS_HAT1Y
#define ABS_RUDDER
#define ABS_HAT3Y
#define ABS_Z
#define ABS_Y
#define ABS_THROTTLE
#define ABS_X
#define ABS_HAT2Y
#define ABS_HAT0X
#define ABS_WHEEL
#define ABS_HAT0Y
#define ABS_RX
#define ABS_GAS
#define ABS_HAT3X
double expectedAngle
std::string label
DISPLAY_INFO di
Definition stvdriver.c:95