348int main(
int argc,
char *argv[])
350 const char *devPath =
"/dev/input/js0";
351 if (argc > 1) devPath = argv[1];
353 std::cout <<
"\n=== Joystick Analysis & Calibration Diagnostic ===\n";
354 std::cout <<
"Device: " << devPath <<
"\n\n";
356 int fd = open(devPath, O_RDONLY);
359 std::cerr <<
"ERROR: cannot open " << devPath <<
": " << strerror(
errno) <<
"\n";
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);
377 const int ABS_BUF_SIZE = 64;
378 uint8_t axmapBuf[ABS_BUF_SIZE] = {};
379 std::vector<uint8_t> axmap(numAxes, 0);
380 if (ioctl(
fd, JSIOCGAXMAP, axmapBuf) < 0)
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";
388 for (
int i = 0; i < numAxes; i++)
389 axmap[i] = axmapBuf[i];
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";
398 std::cout <<
"Axis map (from JSIOCGAXMAP):\n";
399 for (
int i = 0; i < numAxes; i++)
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";
410 std::vector<int> axisState = drainInitEvents(
fd, numAxes);
422 std::vector<StickPair> stickPairs;
425 const std::array<std::pair<uint8_t, uint8_t>, 6> knownPairs = {{
435 const std::array<const char *, 6> pairNames = {{
445 for (
size_t p = 0; p < knownPairs.size(); p++)
447 int idx0 = -1, idx1 = -1;
448 for (
int i = 0; i < numAxes; i++)
450 if (axmap[i] == knownPairs[p].first) idx0 = i;
451 if (axmap[i] == knownPairs[p].second) idx1 = i;
453 if (idx0 >= 0 && idx1 >= 0)
458 sp.absX = knownPairs[p].first;
459 sp.absY = knownPairs[p].second;
460 sp.name = pairNames[p];
461 stickPairs.push_back(sp);
472 std::vector<Trigger> triggers;
473 for (
int i = 0; i < numAxes; i++)
476 triggers.push_back({i, axmap[i], absCodeLabel(axmap[i])});
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";
494 double expectedAngle;
495 bool useMaxAxis0, useMaxAxis1;
496 bool testAxis0, testAxis1;
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},
514 std::vector<Sample> samples;
515 int calCenter0, calMin0, calMax0;
516 int calCenter1, calMin1, calMax1;
519 std::vector<JoyResult> joyResults;
521 for (
const auto &sp : stickPairs)
526 "══════════════════════════════════════════════\n"
527 <<
" " << sp.name <<
" (Axis " << sp.i0 <<
" = " << absCodeLabel(sp.absX)
528 <<
", Axis " << sp.i1 <<
" = " << absCodeLabel(sp.absY) <<
")\n"
529 <<
"══════════════════════════════════════════════\n\n";
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... ";
543 AxisWindow w = collectWindow(
fd, sp.i0, sp.i1, axisState, 4000);
544 std::cout <<
"done.\n\n";
546 res.calMin0 = w.
min0;
547 res.calMax0 = w.
max0;
549 res.calMin1 = w.
min1;
550 res.calMax1 = w.
max1;
562 res.samples.push_back(s);
565 for (
const auto &dt : dirTests)
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;
584 res.samples.push_back(s);
586 joyResults.push_back(res);
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... ";
595 AxisWindow cw = collectWindow(
fd, sp.i0, sp.i1, axisState, 1500);
598 std::cout <<
"done. Center: Axis" << sp.i0 <<
"=" << c0
599 <<
" Axis" << sp.i1 <<
"=" << c1 <<
"\n\n";
603 res.calMin0 = -32767;
605 res.calMin1 = -32767;
613 res.samples.push_back(s);
622 std::vector<DirCap> caps(dirTests.size());
624 int omin0 = INT_MAX, omax0 = INT_MIN;
625 int omin1 = INT_MAX, omax1 = INT_MIN;
627 for (
size_t di = 0;
di < dirTests.size();
di++)
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... ";
636 AxisWindow w = collectWindow(
fd, sp.i0, sp.i1, axisState, 1500);
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;
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);
646 std::cout <<
"done. Axis" << sp.i0 <<
": [" << w.
min0 <<
", " << w.
max0 <<
"]"
647 <<
" Axis" << sp.i1 <<
": [" << w.
min1 <<
", " << w.
max1 <<
"]\n\n";
656 for (
size_t di = 0;
di < dirTests.size();
di++)
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;
676 res.samples.push_back(s);
679 joyResults.push_back(res);
688 int restVal, pressVal;
690 std::vector<TriggerResult> trigResults;
692 if (!triggers.empty())
695 "══════════════════════════════════════════════\n"
697 <<
"══════════════════════════════════════════════\n\n";
699 for (
const auto &tr : triggers)
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... ";
707 std::cout <<
"done. rest=" << rw.
minVal <<
" to " << rw.
maxVal <<
"\n";
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... ";
714 std::cout <<
"done. pressed=" << pw.
minVal <<
" to " << pw.
maxVal <<
"\n\n";
719 tres.pressVal = pw.
maxVal;
720 trigResults.push_back(tres);
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";
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";
744 for (
const auto &res : joyResults)
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";
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";
757 report <<
"\n Results:\n";
758 for (
const auto &s : res.samples)
759 printSample(report, s);
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";
775 if (!trigResults.empty())
777 report <<
"──────────────────────────────────────────────────────\n";
778 report <<
"TRIGGERS\n\n";
779 for (
const auto &tr : trigResults)
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";
791 std::cout <<
"\n" << report.str();
793 std::string outPath = homeDir() +
"/joystick_analysis.txt";
794 std::ofstream ofs(outPath);
798 std::cout <<
"Report saved to: " << outPath <<
"\n";
801 std::cerr <<
"WARNING: could not write to " << outPath <<
"\n";