QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
JoystickSDL.cc
Go to the documentation of this file.
1#include "JoystickSDL.h"
2#include "JoystickManager.h"
3#include "SDLJoystick.h"
5
6#include <QtCore/QMetaObject>
7#include <QtCore/QThread>
8
9#include <array>
10
11#include <SDL3/SDL.h>
12
13// This is used for testing manual control extensions which require additional axes but all you have is a gamepad/joystick with limited axes.
14// Only available on debug builds.
15#ifdef QT_DEBUG
16//#define TEST_WITH_VIRTUAL_AXES
17#endif
18
19QGC_LOGGING_CATEGORY(JoystickSDLLog, "Joystick.JoystickSDL")
20
21
22static QMap<QString, Joystick*> s_discoveryCache;
23
25static bool sdlEventWatcher(void *userdata, SDL_Event *event)
26{
27 Q_UNUSED(userdata);
28
29 JoystickManager *manager = JoystickManager::instance();
30 if (!manager) {
31 return true;
32 }
33
34 switch (event->type) {
35 case SDL_EVENT_JOYSTICK_ADDED:
36 qCInfo(JoystickSDLLog) << "SDL event: Joystick added, instance ID:" << event->jdevice.which;
37 QMetaObject::invokeMethod(manager, "_checkForAddedOrRemovedJoysticks", Qt::QueuedConnection);
38 break;
39 case SDL_EVENT_JOYSTICK_REMOVED:
40 qCInfo(JoystickSDLLog) << "SDL event: Joystick removed, instance ID:" << event->jdevice.which;
41 QMetaObject::invokeMethod(manager, "_checkForAddedOrRemovedJoysticks", Qt::QueuedConnection);
42 break;
43 // Gamepad events ignored - SDL fires both joystick and gamepad events for gamepads
44 case SDL_EVENT_GAMEPAD_ADDED:
45 qCDebug(JoystickSDLLog) << "SDL event: Gamepad added (ignored, handled via joystick event), instance ID:" << event->gdevice.which;
46 break;
47 case SDL_EVENT_GAMEPAD_REMOVED:
48 qCDebug(JoystickSDLLog) << "SDL event: Gamepad removed (ignored, handled via joystick event), instance ID:" << event->gdevice.which;
49 break;
50
51 case SDL_EVENT_JOYSTICK_BATTERY_UPDATED:
52 qCDebug(JoystickSDLLog) << "Battery updated for joystick" << event->jbattery.which
53 << "state:" << event->jbattery.state
54 << "percent:" << event->jbattery.percent;
55 QMetaObject::invokeMethod(manager, "_handleBatteryUpdated",
56 Qt::QueuedConnection,
57 Q_ARG(int, static_cast<int>(event->jbattery.which)));
58 break;
59
60 case SDL_EVENT_GAMEPAD_REMAPPED:
61 qCDebug(JoystickSDLLog) << "Gamepad remapped:" << event->gdevice.which;
62 QMetaObject::invokeMethod(manager, "_handleGamepadRemapped",
63 Qt::QueuedConnection,
64 Q_ARG(int, static_cast<int>(event->gdevice.which)));
65 break;
66
67 case SDL_EVENT_GAMEPAD_TOUCHPAD_DOWN:
68 case SDL_EVENT_GAMEPAD_TOUCHPAD_MOTION:
69 case SDL_EVENT_GAMEPAD_TOUCHPAD_UP:
70 QMetaObject::invokeMethod(manager, "_handleTouchpadEvent",
71 Qt::QueuedConnection,
72 Q_ARG(int, static_cast<int>(event->gtouchpad.which)),
73 Q_ARG(int, event->gtouchpad.touchpad),
74 Q_ARG(int, event->gtouchpad.finger),
75 Q_ARG(bool, event->type != SDL_EVENT_GAMEPAD_TOUCHPAD_UP),
76 Q_ARG(float, event->gtouchpad.x),
77 Q_ARG(float, event->gtouchpad.y),
78 Q_ARG(float, event->gtouchpad.pressure));
79 break;
80
81 case SDL_EVENT_GAMEPAD_SENSOR_UPDATE:
82 QMetaObject::invokeMethod(manager, "_handleSensorUpdate",
83 Qt::QueuedConnection,
84 Q_ARG(int, static_cast<int>(event->gsensor.which)),
85 Q_ARG(int, event->gsensor.sensor),
86 Q_ARG(float, event->gsensor.data[0]),
87 Q_ARG(float, event->gsensor.data[1]),
88 Q_ARG(float, event->gsensor.data[2]));
89 break;
90
91 case SDL_EVENT_JOYSTICK_UPDATE_COMPLETE:
92 case SDL_EVENT_GAMEPAD_UPDATE_COMPLETE:
93 QMetaObject::invokeMethod(manager, "_handleUpdateComplete",
94 Qt::QueuedConnection,
95 Q_ARG(int, static_cast<int>(event->common.type == SDL_EVENT_GAMEPAD_UPDATE_COMPLETE
96 ? event->gdevice.which : event->jdevice.which)));
97 break;
98
99 default:
100 break;
101 }
102
103 return true;
104}
105
106JoystickSDL::JoystickSDL(const QString &name, const QList<int> &gamepadAxes, const QList<int> &nonGamepadAxes, int buttonCount, int hatCount, int instanceId, QObject *parent)
107 : Joystick(name, gamepadAxes.length() + nonGamepadAxes.length()
108#ifdef TEST_WITH_VIRTUAL_AXES
109 + 2 // Add 2 virtual axes for testing
110#endif
111 , buttonCount, hatCount, parent)
112 , _gamepadAxes(gamepadAxes)
113 , _nonGamepadAxes(nonGamepadAxes)
114 , _instanceId(instanceId)
115{
116 qCDebug(JoystickSDLLog) << this;
117}
118
120{
121 qCDebug(JoystickSDLLog) << this;
122}
123
124quint64 JoystickSDL::_getProperties() const
125{
126 if (_sdlGamepad) {
127 return SDL_GetGamepadProperties(_sdlGamepad);
128 }
129 if (_sdlJoystick) {
130 return SDL_GetJoystickProperties(_sdlJoystick);
131 }
132 return 0;
133}
134
135bool JoystickSDL::_checkVirtualJoystick(const char *methodName) const
136{
137 if (!_sdlJoystick) {
138 qCWarning(JoystickSDLLog) << methodName << "called with null joystick";
139 return false;
140 }
141 if (!isVirtual()) {
142 qCWarning(JoystickSDLLog) << methodName << "called on non-virtual joystick:" << _name;
143 return false;
144 }
145 return true;
146}
147
148bool JoystickSDL::_hasGamepadCapability(const char *propertyName) const
149{
150 if (_sdlGamepad) {
151 SDL_PropertiesID props = SDL_GetGamepadProperties(_sdlGamepad);
152 return SDL_GetBooleanProperty(props, propertyName, false);
153 }
154 return false;
155}
156
158{
159 if (!SDLJoystick::init()) {
160 return false;
161 }
162
163 SDL_AddEventWatch(sdlEventWatcher, nullptr);
164 return true;
165}
166
167void JoystickSDL::shutdown(bool deleteDiscoveryCache)
168{
169 SDL_RemoveEventWatch(sdlEventWatcher, nullptr);
171
172 if (deleteDiscoveryCache) {
173 qDeleteAll(s_discoveryCache);
174 }
175 s_discoveryCache.clear();
176}
177
178QMap<QString, Joystick*> JoystickSDL::discover()
179{
180 Q_ASSERT(QThread::isMainThread());
181
182 QMap<QString, Joystick*> current;
183
184 qCDebug(JoystickSDLLog) << "Discovering joysticks";
185
186 // Required on Android before SDL_GetJoysticks() returns devices
188
190
191 int count = 0;
192 SDL_JoystickID *ids = SDL_GetJoysticks(&count);
193 if (!ids) {
194 qCWarning(JoystickSDLLog) << "SDL_GetJoysticks failed:" << SDL_GetError();
195 return current;
196 }
197
198 qCDebug(JoystickSDLLog) << "SDL_GetJoysticks returned" << count << "joysticks";
199 for (int n = 0; n < count; ++n) {
200 const QString joystickName = SDLJoystick::getNameForInstanceId(ids[n]);
201 qCDebug(JoystickSDLLog) << " [" << n << "] ID:" << ids[n]
202 << "Name:" << joystickName
203 << "IsGamepad:" << SDL_IsGamepad(ids[n]);
204 }
205
206 for (int n = 0; n < count; ++n) {
207 const SDL_JoystickID jid = ids[n];
208 QString baseName = SDLJoystick::getNameForInstanceId(jid);
209 if (baseName.isEmpty()) {
210 baseName = QStringLiteral("Joystick %1").arg(jid);
211 }
212 QString name = baseName;
213
214 // Check cache by instance ID (reconnection of same device)
215 bool foundInCache = false;
216 for (auto it = s_discoveryCache.begin(); it != s_discoveryCache.end(); ++it) {
217 auto *cachedJs = static_cast<JoystickSDL*>(it.value());
218 if (static_cast<SDL_JoystickID>(cachedJs->instanceId()) == jid) {
219 if (cachedJs->name() == baseName) {
220 name = it.key();
221 current[name] = cachedJs;
222 s_discoveryCache.erase(it);
223 foundInCache = true;
224 } else {
225 // SDL instance ids can be recycled. If the id matches but the reported
226 // name changed, treat it as a new device and drop the stale cached object.
227 cachedJs->deleteLater();
228 s_discoveryCache.erase(it);
229 }
230 break;
231 }
232 }
233 if (foundInCache) {
234 continue;
235 }
236
237 // Check cache by name (for joysticks that were disconnected and reconnected)
238 if (s_discoveryCache.contains(name) && !current.contains(name)) {
239 current[name] = s_discoveryCache[name];
240 auto *js = static_cast<JoystickSDL*>(current[name]);
241 js->setInstanceId(jid);
242 (void) s_discoveryCache.remove(name);
243 continue;
244 }
245
246 // Handle duplicate names by appending a number
247 int duplicateIndex = 2;
248 while (current.contains(name) || s_discoveryCache.contains(name)) {
249 name = QStringLiteral("%1 #%2").arg(baseName).arg(duplicateIndex++);
250 }
251
252 QList<int> gamepadAxes;
253 QSet<int> joyAxesMappedToGamepad;
254
255 if (SDL_IsGamepad(jid)) {
256 auto tmpGamepad = SDL_OpenGamepad(jid);
257 if (!tmpGamepad) {
258 qCWarning(JoystickSDLLog) << "Failed to open gamepad" << jid << SDL_GetError();
259 continue;
260 }
261
262 // Determine if this gamepad axis is one we should show to the user
263 for (int i = 0; i < SDL_GAMEPAD_AXIS_COUNT; i++) {
264 if (SDL_GamepadHasAxis(tmpGamepad, static_cast<SDL_GamepadAxis>(i))) {
265 gamepadAxes.append(i);
266 }
267 }
268
269 // If a sdlJoystick axis is mapped to a sdlGamepad axis, then the axis is represented
270 // by both the sdlJoystick interface and the sdlGamepad interface. If this is the case,
271 // We'll only show the sdlGamepad interface version of the axis to the user.
272 int bindingCount = 0;
273 SDL_GamepadBinding **bindings = SDL_GetGamepadBindings(tmpGamepad, &bindingCount);
274 if (bindings) {
275 for (int i = 0; i < bindingCount; ++i) {
276 SDL_GamepadBinding *binding = bindings[i];
277 if (binding && binding->input_type == SDL_GAMEPAD_BINDTYPE_AXIS && binding->output_type == SDL_GAMEPAD_BINDTYPE_AXIS) {
278 joyAxesMappedToGamepad.insert(binding->input.axis.axis);
279 }
280 }
281 SDL_free(bindings);
282 } else {
283 qCWarning(JoystickSDLLog) << "Failed to get bindings for" << name << "error:" << SDL_GetError();
284 }
285
286 SDL_CloseGamepad(tmpGamepad);
287 }
288
289 SDL_Joystick *tmpJoy = SDL_OpenJoystick(jid);
290 if (!tmpJoy) {
291 qCWarning(JoystickSDLLog) << "Failed to open joystick" << jid << SDL_GetError();
292 continue;
293 }
294
295 QList<int> nonGamepadAxes;
296 const int axisCount = SDL_GetNumJoystickAxes(tmpJoy);
297 for (int i = 0; i < axisCount; i++) {
298 if (!joyAxesMappedToGamepad.contains(i)) {
299 nonGamepadAxes.append(i);
300 }
301 }
302
303 const int buttonCount = SDL_GetNumJoystickButtons(tmpJoy);
304 const int hatCount = SDL_GetNumJoystickHats(tmpJoy);
305 SDL_CloseJoystick(tmpJoy);
306
307 qCDebug(JoystickSDLLog) << "Creating JoystickSDL for" << name << "jid:" << jid;
308
309 current[name] = new JoystickSDL(name,
310 gamepadAxes,
311 nonGamepadAxes,
312 qMax(0, buttonCount),
313 qMax(0, hatCount),
314 jid);
315 }
316
317 SDL_free(ids);
318
319 qCDebug(JoystickSDLLog) << "Discovered" << current.size() << "joysticks:";
320 for (auto it = current.begin(); it != current.end(); ++it) {
321 auto *js = static_cast<JoystickSDL*>(it.value());
322 qCDebug(JoystickSDLLog) << " " << it.key() << "instanceId:" << js->instanceId();
323 }
324
325 for (auto *joystick : std::as_const(s_discoveryCache)) {
326 joystick->deleteLater();
327 }
328
329 s_discoveryCache = current;
330 return current;
331}
332
333//-----------------------------------------------------------------------------
334// Joystick Interface Overrides
335//-----------------------------------------------------------------------------
336
337bool JoystickSDL::_open()
338{
339 if (isGamepad()) {
340 _sdlGamepad = SDL_OpenGamepad(_instanceId);
341 if (!_sdlGamepad) {
342 qCWarning(JoystickSDLLog) << "SDL_OpenGamepad failed:" << SDL_GetError();
343 return false;
344 }
345 _sdlJoystick = SDL_GetGamepadJoystick(_sdlGamepad);
346 } else {
347 _sdlJoystick = SDL_OpenJoystick(_instanceId);
348 }
349
350 if (!_sdlJoystick) {
351 qCWarning(JoystickSDLLog) << "SDL_JoystickOpen failed:" << SDL_GetError();
352 return false;
353 }
354
355 qCDebug(JoystickSDLLog) << "Opened" << SDL_GetJoystickName(_sdlJoystick) << "joystick at" << _sdlJoystick;
356
357 return true;
358}
359
360void JoystickSDL::_close()
361{
362 if (!_sdlJoystick) {
363 qCWarning(JoystickSDLLog) << "Attempt to close null joystick!";
364 return;
365 }
366
367 qCDebug(JoystickSDLLog) << "Closing joystick" << _name << "at" << _sdlJoystick;
368
369 if (_sdlHaptic) {
370 SDL_CloseHaptic(_sdlHaptic);
371 _sdlHaptic = nullptr;
372 }
373
374 if (_sdlGamepad) {
375 SDL_CloseGamepad(_sdlGamepad);
376 } else {
377 SDL_CloseJoystick(_sdlJoystick);
378 }
379
380 _sdlJoystick = nullptr;
381 _sdlGamepad = nullptr;
382}
383
384bool JoystickSDL::_update()
385{
386 if (!_sdlJoystick || !SDL_JoystickConnected(_sdlJoystick)) {
387 qCWarning(JoystickSDLLog) << "Joystick disconnected during update:" << _name;
388 return false;
389 }
390
391 if (_sdlGamepad) {
392 SDL_UpdateGamepads();
393 } else {
394 SDL_UpdateJoysticks();
395 }
396
397 return true;
398}
399
400//-----------------------------------------------------------------------------
401// Input State Accessors
402//-----------------------------------------------------------------------------
403
404bool JoystickSDL::_getButton(int idx) const
405{
406 // First try the standardized gamepad set if idx is inside that set
407 if (_sdlGamepad && (idx >= 0) && (idx < SDL_GAMEPAD_BUTTON_COUNT)) {
408 return SDL_GetGamepadButton(_sdlGamepad, static_cast<SDL_GamepadButton>(idx));
409 }
410
411 // Fall back to raw joystick buttons (covers unmapped/extras)
412 if (_sdlJoystick && (idx >= 0) && (idx < SDL_GetNumJoystickButtons(_sdlJoystick))) {
413 return SDL_GetJoystickButton(_sdlJoystick, idx);
414 }
415
416 return false;
417}
418
419int JoystickSDL::_getAxisValue(int idx) const
420{
421 if (idx < 0) {
422 return 0;
423 }
424
425#ifdef TEST_WITH_VIRTUAL_AXES
426 // Handle virtual axes (last 2 axes)
427 const int totalPhysicalAxes = _gamepadAxes.length() + _nonGamepadAxes.length();
428 if (idx >= totalPhysicalAxes && idx < totalPhysicalAxes + 2) {
429 // Check if button 0 is pressed
430 bool button0Down = false;
431 if (_sdlGamepad) {
432 button0Down = SDL_GetGamepadButton(_sdlGamepad, static_cast<SDL_GamepadButton>(0));
433 } else if (_sdlJoystick) {
434 button0Down = SDL_GetJoystickButton(_sdlJoystick, 0);
435 }
436
437 if (button0Down) {
438 // When button 0 is down, return values from axis 0 and 1
439 const int virtualAxisIdx = idx - totalPhysicalAxes;
440 if (virtualAxisIdx == 0) {
441 // First virtual axis maps to axis 0
442 if (_sdlGamepad && _gamepadAxes.length() > 0) {
443 return SDL_GetGamepadAxis(_sdlGamepad, static_cast<SDL_GamepadAxis>(_gamepadAxes[0]));
444 } else if (_sdlJoystick) {
445 return SDL_GetJoystickAxis(_sdlJoystick, 0);
446 }
447 } else {
448 // Second virtual axis maps to axis 1
449 if (_sdlGamepad && _gamepadAxes.length() > 1) {
450 return SDL_GetGamepadAxis(_sdlGamepad, static_cast<SDL_GamepadAxis>(_gamepadAxes[1]));
451 } else if (_sdlJoystick) {
452 return SDL_GetJoystickAxis(_sdlJoystick, 1);
453 }
454 }
455 } else {
456 // When button 0 is up, return centered values
457 return 0;
458 }
459 }
460#endif
461
462 if (_sdlGamepad) {
463 if (idx < _gamepadAxes.length()) {
464 return SDL_GetGamepadAxis(_sdlGamepad, static_cast<SDL_GamepadAxis>(_gamepadAxes[idx]));
465 }
466 const int nonGamepadIdx = idx - static_cast<int>(_gamepadAxes.length());
467 if (nonGamepadIdx < _nonGamepadAxes.length()) {
468 return SDL_GetJoystickAxis(_sdlJoystick, _nonGamepadAxes[nonGamepadIdx]);
469 }
470 return 0;
471 }
472
473 return SDL_GetJoystickAxis(_sdlJoystick, idx);
474}
475
476bool JoystickSDL::_getHat(int hat, int idx) const
477{
478 static constexpr std::array<uint8_t, 4> hatButtons = {SDL_HAT_UP, SDL_HAT_DOWN, SDL_HAT_LEFT, SDL_HAT_RIGHT};
479
480 if (idx < 0 || static_cast<size_t>(idx) >= hatButtons.size()) {
481 return false;
482 }
483
484 return ((SDL_GetJoystickHat(_sdlJoystick, hat) & hatButtons[idx]) != 0);
485}
486
487//-----------------------------------------------------------------------------
488// Haptic and LED
489//-----------------------------------------------------------------------------
490
492{
493 const quint64 props = _getProperties();
494 return props != 0 && SDL_GetBooleanProperty(props, SDL_PROP_JOYSTICK_CAP_RUMBLE_BOOLEAN, false);
495}
496
498{
499 const quint64 props = _getProperties();
500 return props != 0 && SDL_GetBooleanProperty(props, SDL_PROP_JOYSTICK_CAP_TRIGGER_RUMBLE_BOOLEAN, false);
501}
502
504{
505 const quint64 props = _getProperties();
506 if (props == 0) {
507 return false;
508 }
509 return SDL_GetBooleanProperty(props, SDL_PROP_JOYSTICK_CAP_RGB_LED_BOOLEAN, false) ||
510 SDL_GetBooleanProperty(props, SDL_PROP_JOYSTICK_CAP_MONO_LED_BOOLEAN, false);
511}
512
513void JoystickSDL::rumble(quint16 lowFreq, quint16 highFreq, quint32 durationMs)
514{
515 if (_sdlGamepad) {
516 if (!SDL_RumbleGamepad(_sdlGamepad, lowFreq, highFreq, durationMs)) {
517 qCDebug(JoystickSDLLog) << "Rumble failed:" << SDL_GetError();
518 }
519 } else if (_sdlJoystick) {
520 if (!SDL_RumbleJoystick(_sdlJoystick, lowFreq, highFreq, durationMs)) {
521 qCDebug(JoystickSDLLog) << "Rumble failed:" << SDL_GetError();
522 }
523 }
524}
525
526void JoystickSDL::rumbleTriggers(quint16 left, quint16 right, quint32 durationMs)
527{
528 if (_sdlGamepad) {
529 if (!SDL_RumbleGamepadTriggers(_sdlGamepad, left, right, durationMs)) {
530 qCDebug(JoystickSDLLog) << "Trigger rumble failed:" << SDL_GetError();
531 }
532 }
533}
534
535void JoystickSDL::setLED(quint8 red, quint8 green, quint8 blue)
536{
537 if (_sdlGamepad) {
538 if (!SDL_SetGamepadLED(_sdlGamepad, red, green, blue)) {
539 qCDebug(JoystickSDLLog) << "Set LED failed:" << SDL_GetError();
540 }
541 } else if (_sdlJoystick) {
542 if (!SDL_SetJoystickLED(_sdlJoystick, red, green, blue)) {
543 qCDebug(JoystickSDLLog) << "Set LED failed:" << SDL_GetError();
544 }
545 }
546}
547
548bool JoystickSDL::sendEffect(const QByteArray &data)
549{
550 if (_sdlGamepad && !data.isEmpty()) {
551 if (!SDL_SendGamepadEffect(_sdlGamepad, data.constData(), static_cast<int>(data.size()))) {
552 qCDebug(JoystickSDLLog) << "sendEffect failed:" << SDL_GetError();
553 return false;
554 }
555 return true;
556 }
557 return false;
558}
559
560//-----------------------------------------------------------------------------
561// Device Identity
562//-----------------------------------------------------------------------------
563
564QString JoystickSDL::guid() const
565{
566 if (_sdlJoystick) {
567 SDL_GUID sdlGuid = SDL_GetJoystickGUID(_sdlJoystick);
568 char guidStr[33];
569 SDL_GUIDToString(sdlGuid, guidStr, sizeof(guidStr));
570 return QString::fromLatin1(guidStr);
571 }
572 return QString();
573}
574
576{
577 if (_sdlJoystick) {
578 return SDL_GetJoystickVendor(_sdlJoystick);
579 }
580 return 0;
581}
582
584{
585 if (_sdlJoystick) {
586 return SDL_GetJoystickProduct(_sdlJoystick);
587 }
588 return 0;
589}
590
591QString JoystickSDL::serial() const
592{
593 if (_sdlJoystick) {
594 const char *serialStr = SDL_GetJoystickSerial(_sdlJoystick);
595 if (serialStr) {
596 return QString::fromUtf8(serialStr);
597 }
598 }
599 return QString();
600}
601
603{
604 if (_sdlJoystick) {
605 SDL_JoystickType type = SDL_GetJoystickType(_sdlJoystick);
606 switch (type) {
607 case SDL_JOYSTICK_TYPE_GAMEPAD:
608 return QStringLiteral("Gamepad");
609 case SDL_JOYSTICK_TYPE_WHEEL:
610 return QStringLiteral("Wheel");
611 case SDL_JOYSTICK_TYPE_ARCADE_STICK:
612 return QStringLiteral("Arcade Stick");
613 case SDL_JOYSTICK_TYPE_FLIGHT_STICK:
614 return QStringLiteral("Flight Stick");
615 case SDL_JOYSTICK_TYPE_DANCE_PAD:
616 return QStringLiteral("Dance Pad");
617 case SDL_JOYSTICK_TYPE_GUITAR:
618 return QStringLiteral("Guitar");
619 case SDL_JOYSTICK_TYPE_DRUM_KIT:
620 return QStringLiteral("Drum Kit");
621 case SDL_JOYSTICK_TYPE_ARCADE_PAD:
622 return QStringLiteral("Arcade Pad");
623 case SDL_JOYSTICK_TYPE_THROTTLE:
624 return QStringLiteral("Throttle");
625 default:
626 return QStringLiteral("Unknown");
627 }
628 }
629 return QString();
630}
631
632QString JoystickSDL::path() const
633{
634 if (_sdlJoystick) {
635 const char *devicePath = SDL_GetJoystickPath(_sdlJoystick);
636 if (devicePath) {
637 return QString::fromUtf8(devicePath);
638 }
639 }
640 return QString();
641}
642
644{
645 return SDL_IsJoystickVirtual(_instanceId);
646}
647
649{
650 if (_sdlJoystick) {
651 return SDL_GetJoystickFirmwareVersion(_sdlJoystick);
652 }
653 return 0;
654}
655
657{
658 if (_sdlGamepad) {
659 return SDLJoystick::connectionStateToString(SDL_GetGamepadConnectionState(_sdlGamepad));
660 }
661 if (_sdlJoystick) {
662 return SDLJoystick::connectionStateToString(SDL_GetJoystickConnectionState(_sdlJoystick));
663 }
664 return QString();
665}
666
667//-----------------------------------------------------------------------------
668// Player Info
669//-----------------------------------------------------------------------------
670
672{
673 if (_sdlGamepad) {
674 return SDL_GetGamepadPlayerIndex(_sdlGamepad);
675 }
676 if (_sdlJoystick) {
677 return SDL_GetJoystickPlayerIndex(_sdlJoystick);
678 }
679 return -1;
680}
681
683{
684 bool success = false;
685 if (_sdlGamepad) {
686 success = SDL_SetGamepadPlayerIndex(_sdlGamepad, index);
687 } else if (_sdlJoystick) {
688 success = SDL_SetJoystickPlayerIndex(_sdlJoystick, index);
689 }
690
691 if (success) {
692 emit playerIndexChanged();
693 } else {
694 qCDebug(JoystickSDLLog) << "Failed to set player index:" << SDL_GetError();
695 }
696}
697
698//-----------------------------------------------------------------------------
699// Power & Battery Status
700//-----------------------------------------------------------------------------
701
703{
704 if (_sdlJoystick) {
705 int percent = -1;
706 SDL_GetJoystickPowerInfo(_sdlJoystick, &percent);
707 return percent;
708 }
709 return -1;
710}
711
713{
714 if (!_sdlJoystick) {
715 return QString();
716 }
717
718 switch (SDL_GetJoystickPowerInfo(_sdlJoystick, nullptr)) {
719 case SDL_POWERSTATE_ERROR:
720 return QStringLiteral("Error");
721 case SDL_POWERSTATE_UNKNOWN:
722 return QStringLiteral("Unknown");
723 case SDL_POWERSTATE_ON_BATTERY:
724 return QStringLiteral("On Battery");
725 case SDL_POWERSTATE_NO_BATTERY:
726 return QStringLiteral("No Battery");
727 case SDL_POWERSTATE_CHARGING:
728 return QStringLiteral("Charging");
729 case SDL_POWERSTATE_CHARGED:
730 return QStringLiteral("Charged");
731 default:
732 return QString();
733 }
734}
735
736//-----------------------------------------------------------------------------
737// Gamepad Type Detection
738//-----------------------------------------------------------------------------
739
741{
742 return SDL_IsGamepad(_instanceId);
743}
744
746{
747 if (_sdlGamepad) {
748 return SDLJoystick::gamepadTypeDisplayName(SDL_GetGamepadType(_sdlGamepad));
749 }
750 return QString();
751}
752
753//-----------------------------------------------------------------------------
754// Control Labels
755//-----------------------------------------------------------------------------
756
757QString JoystickSDL::axisLabel(int axis) const
758{
759 if (axis < 0) {
760 return tr("Axis %1").arg(axis);
761 }
762
763 // Check gamepad axes first
764 if (_sdlGamepad && axis < _gamepadAxes.length()) {
765 const char *label = SDL_GetGamepadStringForAxis(static_cast<SDL_GamepadAxis>(_gamepadAxes[axis]));
766 if (label) {
767 return QString::fromUtf8(label);
768 }
769 }
770
771 // Check non-gamepad axes
772 const int nonGamepadIdx = axis - _gamepadAxes.length();
773 if (nonGamepadIdx >= 0 && nonGamepadIdx < _nonGamepadAxes.length()) {
774 // Non-gamepad axes don't have standard labels, use raw joystick axis number
775 return tr("Axis %1").arg(_nonGamepadAxes[nonGamepadIdx]);
776 }
777
778 return tr("Axis %1").arg(axis);
779}
780
781QString JoystickSDL::buttonLabel(int button) const
782{
783 if (_sdlGamepad && button >= 0 && button < SDL_GAMEPAD_BUTTON_COUNT) {
784 const char *label = SDL_GetGamepadStringForButton(static_cast<SDL_GamepadButton>(button));
785 if (label) {
786 return QString::fromUtf8(label);
787 }
788 }
789 return tr("Button %1").arg(button);
790}
791
792//-----------------------------------------------------------------------------
793// Mapping Management
794//-----------------------------------------------------------------------------
795
797{
798 if (_sdlGamepad) {
799 char *mapping = SDL_GetGamepadMapping(_sdlGamepad);
800 if (mapping) {
801 QString result = QString::fromUtf8(mapping);
802 SDL_free(mapping);
803 return result;
804 }
805 }
806 return QString();
807}
808
809bool JoystickSDL::addMapping(const QString &mapping)
810{
811 if (mapping.isEmpty()) {
812 return false;
813 }
814
815 int result = SDL_AddGamepadMapping(qPrintable(mapping));
816 if (result == -1) {
817 qCWarning(JoystickSDLLog) << "Failed to add gamepad mapping:" << SDL_GetError();
818 return false;
819 }
820
821 qCDebug(JoystickSDLLog) << "Added gamepad mapping, result:" << result;
822 return true;
823}
824
825QVariantMap JoystickSDL::getAxisBinding(int axis) const
826{
827 QVariantMap result;
828 result[QStringLiteral("valid")] = false;
829
830 if (axis < 0) {
831 return result;
832 }
833
834 // Gamepad axes have bindings
835 if (_sdlGamepad && axis < _gamepadAxes.length()) {
836 const SDL_GamepadAxis gamepadAxis = static_cast<SDL_GamepadAxis>(_gamepadAxes[axis]);
837 return SDLJoystick::findBinding(_sdlGamepad, [gamepadAxis](const SDL_GamepadBinding *binding) {
838 return binding->output_type == SDL_GAMEPAD_BINDTYPE_AXIS &&
839 binding->output.axis.axis == gamepadAxis;
840 });
841 }
842
843 // Non-gamepad axes map directly to joystick axes (no binding indirection)
844 const int nonGamepadIdx = axis - _gamepadAxes.length();
845 if (nonGamepadIdx >= 0 && nonGamepadIdx < _nonGamepadAxes.length()) {
846 result[QStringLiteral("valid")] = true;
847 result[QStringLiteral("inputType")] = static_cast<int>(SDL_GAMEPAD_BINDTYPE_AXIS);
848 result[QStringLiteral("inputAxis")] = _nonGamepadAxes[nonGamepadIdx];
849 result[QStringLiteral("inputAxisMin")] = -32768;
850 result[QStringLiteral("inputAxisMax")] = 32767;
851 result[QStringLiteral("direct")] = true; // Indicates no gamepad mapping, direct joystick access
852 return result;
853 }
854
855 return result;
856}
857
858QVariantMap JoystickSDL::getButtonBinding(int button) const
859{
860 if (!_sdlGamepad || button < 0 || button >= SDL_GAMEPAD_BUTTON_COUNT) {
861 QVariantMap result;
862 result[QStringLiteral("valid")] = false;
863 return result;
864 }
865
866 const SDL_GamepadButton gamepadButton = static_cast<SDL_GamepadButton>(button);
867 return SDLJoystick::findBinding(_sdlGamepad, [gamepadButton](const SDL_GamepadBinding *binding) {
868 return binding->output_type == SDL_GAMEPAD_BINDTYPE_BUTTON &&
869 binding->output.button == gamepadButton;
870 });
871}
872
873//-----------------------------------------------------------------------------
874// Sensor Support
875//-----------------------------------------------------------------------------
876
878{
879 if (_sdlGamepad) {
880 return SDL_GamepadHasSensor(_sdlGamepad, SDL_SENSOR_GYRO);
881 }
882 return false;
883}
884
886{
887 if (_sdlGamepad) {
888 return SDL_GamepadHasSensor(_sdlGamepad, SDL_SENSOR_ACCEL);
889 }
890 return false;
891}
892
894{
895 if (_sdlGamepad && hasGyroscope()) {
896 if (!SDL_SetGamepadSensorEnabled(_sdlGamepad, SDL_SENSOR_GYRO, enabled)) {
897 qCWarning(JoystickSDLLog) << "Failed to" << (enabled ? "enable" : "disable") << "gyroscope:" << SDL_GetError();
898 return false;
899 }
900 return true;
901 }
902 return false;
903}
904
906{
907 if (_sdlGamepad && hasAccelerometer()) {
908 if (!SDL_SetGamepadSensorEnabled(_sdlGamepad, SDL_SENSOR_ACCEL, enabled)) {
909 qCWarning(JoystickSDLLog) << "Failed to" << (enabled ? "enable" : "disable") << "accelerometer:" << SDL_GetError();
910 return false;
911 }
912 return true;
913 }
914 return false;
915}
916
918{
919 // Use cached data from events if available (more efficient than polling)
920 if (_gyroDataCached) {
921 return _cachedGyroData;
922 }
923
924 if (!_sdlGamepad) {
925 return QVector3D();
926 }
927 std::array<float, 3> data = {0.0f, 0.0f, 0.0f};
928 if (!SDL_GetGamepadSensorData(_sdlGamepad, SDL_SENSOR_GYRO, data.data(), 3)) {
929 // Only log if sensor is supposed to be enabled - otherwise this is expected
930 if (SDL_GamepadSensorEnabled(_sdlGamepad, SDL_SENSOR_GYRO)) {
931 qCDebug(JoystickSDLLog) << "Failed to get gyroscope data:" << SDL_GetError();
932 }
933 return QVector3D();
934 }
935 return QVector3D(data[0], data[1], data[2]);
936}
937
939{
940 // Use cached data from events if available (more efficient than polling)
941 if (_accelDataCached) {
942 return _cachedAccelData;
943 }
944
945 if (!_sdlGamepad) {
946 return QVector3D();
947 }
948 std::array<float, 3> data = {0.0f, 0.0f, 0.0f};
949 if (!SDL_GetGamepadSensorData(_sdlGamepad, SDL_SENSOR_ACCEL, data.data(), 3)) {
950 // Only log if sensor is supposed to be enabled - otherwise this is expected
951 if (SDL_GamepadSensorEnabled(_sdlGamepad, SDL_SENSOR_ACCEL)) {
952 qCDebug(JoystickSDLLog) << "Failed to get accelerometer data:" << SDL_GetError();
953 }
954 return QVector3D();
955 }
956 return QVector3D(data[0], data[1], data[2]);
957}
958
960{
961 if (_sdlGamepad) {
962 return SDL_GetGamepadSensorDataRate(_sdlGamepad, SDL_SENSOR_GYRO);
963 }
964 return 0.0f;
965}
966
968{
969 if (_sdlGamepad) {
970 return SDL_GetGamepadSensorDataRate(_sdlGamepad, SDL_SENSOR_ACCEL);
971 }
972 return 0.0f;
973}
974
975//-----------------------------------------------------------------------------
976// Touchpad Support
977//-----------------------------------------------------------------------------
978
980{
981 if (_sdlGamepad) {
982 return SDL_GetNumGamepadTouchpads(_sdlGamepad);
983 }
984 return 0;
985}
986
987int JoystickSDL::touchpadFingerCount(int touchpad) const
988{
989 if (_sdlGamepad) {
990 return SDL_GetNumGamepadTouchpadFingers(_sdlGamepad, touchpad);
991 }
992 return 0;
993}
994
995QVariantMap JoystickSDL::getTouchpadFinger(int touchpad, int finger) const
996{
997 QVariantMap result;
998 if (_sdlGamepad) {
999 bool down = false;
1000 float x = 0.0f, y = 0.0f, pressure = 0.0f;
1001 if (SDL_GetGamepadTouchpadFinger(_sdlGamepad, touchpad, finger, &down, &x, &y, &pressure)) {
1002 result[QStringLiteral("valid")] = true;
1003 result[QStringLiteral("down")] = down;
1004 result[QStringLiteral("x")] = x;
1005 result[QStringLiteral("y")] = y;
1006 result[QStringLiteral("pressure")] = pressure;
1007 return result;
1008 }
1009 }
1010 result[QStringLiteral("valid")] = false;
1011 return result;
1012}
1013
1014//-----------------------------------------------------------------------------
1015// Trackball Support
1016//-----------------------------------------------------------------------------
1017
1019{
1020 if (_sdlJoystick) {
1021 return SDL_GetNumJoystickBalls(_sdlJoystick);
1022 }
1023 return 0;
1024}
1025
1026QVariantMap JoystickSDL::getBall(int ball) const
1027{
1028 QVariantMap result;
1029 if (_sdlJoystick) {
1030 int dx = 0, dy = 0;
1031 if (SDL_GetJoystickBall(_sdlJoystick, ball, &dx, &dy)) {
1032 result[QStringLiteral("valid")] = true;
1033 result[QStringLiteral("dx")] = dx;
1034 result[QStringLiteral("dy")] = dy;
1035 return result;
1036 }
1037 }
1038 result[QStringLiteral("valid")] = false;
1039 return result;
1040}
1041
1042//-----------------------------------------------------------------------------
1043// Capability Queries
1044//-----------------------------------------------------------------------------
1045
1046bool JoystickSDL::hasButton(int button) const
1047{
1048 if (_sdlGamepad && button >= 0 && button < SDL_GAMEPAD_BUTTON_COUNT) {
1049 return SDL_GamepadHasButton(_sdlGamepad, static_cast<SDL_GamepadButton>(button));
1050 }
1051 if (_sdlJoystick && button >= 0 && button < SDL_GetNumJoystickButtons(_sdlJoystick)) {
1052 return true;
1053 }
1054 return false;
1055}
1056
1057bool JoystickSDL::hasAxis(int axis) const
1058{
1059 if (_sdlGamepad && axis >= 0 && axis < _gamepadAxes.length()) {
1060 return SDL_GamepadHasAxis(_sdlGamepad, static_cast<SDL_GamepadAxis>(_gamepadAxes[axis]));
1061 }
1062 if (_sdlJoystick) {
1063 const int totalAxes = _gamepadAxes.length() + _nonGamepadAxes.length()
1064#ifdef TEST_WITH_VIRTUAL_AXES
1065 + 2 // Add 2 virtual axes for testing
1066#endif
1067 ;
1068 return axis >= 0 && axis < totalAxes;
1069 }
1070 return false;
1071}
1072
1074{
1075 if (_sdlGamepad) {
1076 return SDLJoystick::gamepadTypeDisplayName(SDL_GetRealGamepadType(_sdlGamepad));
1077 }
1078 return QString();
1079}
1080
1081//-----------------------------------------------------------------------------
1082// Type-Specific Labels
1083//-----------------------------------------------------------------------------
1084
1085QString JoystickSDL::buttonLabelForType(int button) const
1086{
1087 if (_sdlGamepad && button >= 0 && button < SDL_GAMEPAD_BUTTON_COUNT) {
1088 SDL_GamepadType type = SDL_GetGamepadType(_sdlGamepad);
1089 SDL_GamepadButtonLabel label = SDL_GetGamepadButtonLabelForType(type, static_cast<SDL_GamepadButton>(button));
1090 switch (label) {
1091 case SDL_GAMEPAD_BUTTON_LABEL_A:
1092 return QStringLiteral("A");
1093 case SDL_GAMEPAD_BUTTON_LABEL_B:
1094 return QStringLiteral("B");
1095 case SDL_GAMEPAD_BUTTON_LABEL_X:
1096 return QStringLiteral("X");
1097 case SDL_GAMEPAD_BUTTON_LABEL_Y:
1098 return QStringLiteral("Y");
1099 case SDL_GAMEPAD_BUTTON_LABEL_CROSS:
1100 return QStringLiteral("Cross");
1101 case SDL_GAMEPAD_BUTTON_LABEL_CIRCLE:
1102 return QStringLiteral("Circle");
1103 case SDL_GAMEPAD_BUTTON_LABEL_SQUARE:
1104 return QStringLiteral("Square");
1105 case SDL_GAMEPAD_BUTTON_LABEL_TRIANGLE:
1106 return QStringLiteral("Triangle");
1107 default:
1108 break;
1109 }
1110 }
1111 return buttonLabel(button);
1112}
1113
1114//-----------------------------------------------------------------------------
1115// Haptic/Force Feedback Support
1116//-----------------------------------------------------------------------------
1117
1119{
1120 if (_sdlJoystick) {
1121 return SDL_IsJoystickHaptic(_sdlJoystick);
1122 }
1123 return false;
1124}
1125
1127{
1128 if (_sdlHaptic) {
1129 return SDL_GetMaxHapticEffects(_sdlHaptic);
1130 }
1131 return 0;
1132}
1133
1135{
1136 if (_sdlHaptic) {
1137 const Uint32 features = SDL_GetHapticFeatures(_sdlHaptic);
1138 return (features & SDL_HAPTIC_LEFTRIGHT) || (features & SDL_HAPTIC_SINE);
1139 }
1140 return false;
1141}
1142
1144{
1145 if (_sdlHaptic) {
1146 return true;
1147 }
1148
1149 if (!_sdlJoystick || !SDL_IsJoystickHaptic(_sdlJoystick)) {
1150 return false;
1151 }
1152
1153 _sdlHaptic = SDL_OpenHapticFromJoystick(_sdlJoystick);
1154 if (!_sdlHaptic) {
1155 qCWarning(JoystickSDLLog) << "Failed to open haptic device:" << SDL_GetError();
1156 return false;
1157 }
1158
1159 if (!SDL_InitHapticRumble(_sdlHaptic)) {
1160 qCWarning(JoystickSDLLog) << "Failed to init haptic rumble:" << SDL_GetError();
1161 SDL_CloseHaptic(_sdlHaptic);
1162 _sdlHaptic = nullptr;
1163 return false;
1164 }
1165
1166 qCDebug(JoystickSDLLog) << "Haptic rumble initialized";
1167 return true;
1168}
1169
1170bool JoystickSDL::hapticRumblePlay(float strength, quint32 durationMs)
1171{
1172 if (!_sdlHaptic) {
1173 if (!hapticRumbleInit()) {
1174 return false;
1175 }
1176 }
1177
1178 if (!SDL_PlayHapticRumble(_sdlHaptic, strength, durationMs)) {
1179 qCWarning(JoystickSDLLog) << "Failed to play haptic rumble:" << SDL_GetError();
1180 return false;
1181 }
1182 return true;
1183}
1184
1186{
1187 if (_sdlHaptic) {
1188 SDL_StopHapticRumble(_sdlHaptic);
1189 }
1190}
1191
1192//-----------------------------------------------------------------------------
1193// Mapping Utilities
1194//-----------------------------------------------------------------------------
1195
1196QString JoystickSDL::getMappingForGUID(const QString &guid) const
1197{
1198 if (guid.isEmpty()) {
1199 return QString();
1200 }
1201
1202 SDL_GUID sdlGuid = SDL_StringToGUID(qPrintable(guid));
1203 char *mapping = SDL_GetGamepadMappingForGUID(sdlGuid);
1204 if (mapping) {
1205 QString result = QString::fromUtf8(mapping);
1206 SDL_free(mapping);
1207 return result;
1208 }
1209 return QString();
1210}
1211
1212//-----------------------------------------------------------------------------
1213// Virtual Joystick Support
1214//-----------------------------------------------------------------------------
1215
1216bool JoystickSDL::setVirtualAxis(int axis, int value)
1217{
1218 if (!_checkVirtualJoystick("setVirtualAxis")) {
1219 return false;
1220 }
1221
1222 if (!SDL_SetJoystickVirtualAxis(_sdlJoystick, axis, static_cast<Sint16>(value))) {
1223 qCDebug(JoystickSDLLog) << "Failed to set virtual axis" << axis << ":" << SDL_GetError();
1224 return false;
1225 }
1226 return true;
1227}
1228
1229bool JoystickSDL::setVirtualButton(int button, bool down)
1230{
1231 if (!_checkVirtualJoystick("setVirtualButton")) {
1232 return false;
1233 }
1234
1235 if (!SDL_SetJoystickVirtualButton(_sdlJoystick, button, down)) {
1236 qCDebug(JoystickSDLLog) << "Failed to set virtual button" << button << ":" << SDL_GetError();
1237 return false;
1238 }
1239 return true;
1240}
1241
1242bool JoystickSDL::setVirtualHat(int hat, quint8 value)
1243{
1244 if (!_checkVirtualJoystick("setVirtualHat")) {
1245 return false;
1246 }
1247
1248 if (!SDL_SetJoystickVirtualHat(_sdlJoystick, hat, value)) {
1249 qCDebug(JoystickSDLLog) << "Failed to set virtual hat" << hat << ":" << SDL_GetError();
1250 return false;
1251 }
1252 return true;
1253}
1254
1255//-----------------------------------------------------------------------------
1256// Properties/Capability Detection
1257//-----------------------------------------------------------------------------
1258
1260{
1261 return _hasGamepadCapability(SDL_PROP_GAMEPAD_CAP_MONO_LED_BOOLEAN);
1262}
1263
1265{
1266 return _hasGamepadCapability(SDL_PROP_GAMEPAD_CAP_RGB_LED_BOOLEAN);
1267}
1268
1270{
1271 return _hasGamepadCapability(SDL_PROP_GAMEPAD_CAP_PLAYER_LED_BOOLEAN);
1272}
1273
1274//-----------------------------------------------------------------------------
1275// Connection State
1276//-----------------------------------------------------------------------------
1277
1279{
1280 return connectionType();
1281}
1282
1283//-----------------------------------------------------------------------------
1284// Initial Axis State (for drift detection)
1285//-----------------------------------------------------------------------------
1286
1287QVariantMap JoystickSDL::getAxisInitialState(int axis) const
1288{
1289 QVariantMap result;
1290 result[QStringLiteral("valid")] = false;
1291 result[QStringLiteral("value")] = 0;
1292
1293 if (!_sdlJoystick || axis < 0) {
1294 return result;
1295 }
1296
1297 Sint16 initialValue = 0;
1298 if (SDL_GetJoystickAxisInitialState(_sdlJoystick, axis, &initialValue)) {
1299 result[QStringLiteral("valid")] = true;
1300 result[QStringLiteral("value")] = static_cast<int>(initialValue);
1301 }
1302 return result;
1303}
1304
1305//-----------------------------------------------------------------------------
1306// Per-Device Custom Mapping
1307//-----------------------------------------------------------------------------
1308
1309bool JoystickSDL::setMapping(const QString &mapping)
1310{
1311 if (!_sdlGamepad || mapping.isEmpty()) {
1312 return false;
1313 }
1314
1315 if (!SDL_SetGamepadMapping(SDL_GetJoystickID(_sdlJoystick), qPrintable(mapping))) {
1316 qCDebug(JoystickSDLLog) << "Failed to set gamepad mapping:" << SDL_GetError();
1317 return false;
1318 }
1319 return true;
1320}
1321
1322//-----------------------------------------------------------------------------
1323// Virtual Joystick Enhancements
1324//-----------------------------------------------------------------------------
1325
1326bool JoystickSDL::setVirtualBall(int ball, int dx, int dy)
1327{
1328 if (!_checkVirtualJoystick("setVirtualBall")) {
1329 return false;
1330 }
1331
1332 if (!SDL_SetJoystickVirtualBall(_sdlJoystick, ball, static_cast<Sint16>(dx), static_cast<Sint16>(dy))) {
1333 qCDebug(JoystickSDLLog) << "Failed to set virtual ball" << ball << ":" << SDL_GetError();
1334 return false;
1335 }
1336 return true;
1337}
1338
1339bool JoystickSDL::setVirtualTouchpad(int touchpad, int finger, bool down, float x, float y, float pressure)
1340{
1341 if (!_checkVirtualJoystick("setVirtualTouchpad")) {
1342 return false;
1343 }
1344
1345 // Validate and clamp touchpad coordinates to valid range [0.0, 1.0]
1346 const float clampedX = qBound(0.0f, x, 1.0f);
1347 const float clampedY = qBound(0.0f, y, 1.0f);
1348 const float clampedPressure = qBound(0.0f, pressure, 1.0f);
1349
1350 if (x != clampedX || y != clampedY || pressure != clampedPressure) {
1351 qCDebug(JoystickSDLLog) << "Virtual touchpad coordinates clamped: x" << x << "->" << clampedX
1352 << "y" << y << "->" << clampedY
1353 << "pressure" << pressure << "->" << clampedPressure;
1354 }
1355
1356 if (!SDL_SetJoystickVirtualTouchpad(_sdlJoystick, touchpad, finger, down, clampedX, clampedY, clampedPressure)) {
1357 qCDebug(JoystickSDLLog) << "Failed to set virtual touchpad" << touchpad << "finger" << finger << ":" << SDL_GetError();
1358 return false;
1359 }
1360 return true;
1361}
1362
1363bool JoystickSDL::sendVirtualSensorData(int sensorType, float x, float y, float z)
1364{
1365 if (!_checkVirtualJoystick("sendVirtualSensorData")) {
1366 return false;
1367 }
1368
1369 const float data[3] = {x, y, z};
1370 if (!SDL_SendJoystickVirtualSensorData(_sdlJoystick, static_cast<SDL_SensorType>(sensorType), SDL_GetTicksNS(), data, 3)) {
1371 qCDebug(JoystickSDLLog) << "Failed to send virtual sensor data:" << SDL_GetError();
1372 return false;
1373 }
1374 return true;
1375}
1376
1377//-----------------------------------------------------------------------------
1378// Cached Sensor Data (Event-Driven Updates)
1379//-----------------------------------------------------------------------------
1380
1381void JoystickSDL::updateCachedGyroData(const QVector3D &data)
1382{
1383 _cachedGyroData = data;
1384 _gyroDataCached = true;
1385 emit gyroscopeDataUpdated(data);
1386}
1387
1388void JoystickSDL::updateCachedAccelData(const QVector3D &data)
1389{
1390 _cachedAccelData = data;
1391 _accelDataCached = true;
1392 emit accelerometerDataUpdated(data);
1393}
1394
1396{
1397 const QString currentState = connectionState();
1398 if (currentState != _lastConnectionState && !currentState.isEmpty()) {
1399 qCDebug(JoystickSDLLog) << "Connection state changed:" << _lastConnectionState << "->" << currentState;
1400 _lastConnectionState = currentState;
1401 emit connectionStateChanged(currentState);
1402 }
1403}
1404
1405QVariantList JoystickSDL::detectAxisDrift(int threshold) const
1406{
1407 QVariantList driftingAxes;
1408
1409 const int totalAxes = _gamepadAxes.length() + _nonGamepadAxes.length();
1410 for (int i = 0; i < totalAxes; ++i) {
1411 const int currentValue = _getAxisValue(i);
1412
1413 // Check if axis is significantly off-center (not near 0)
1414 // Threshold default of 8000 is ~25% of full range
1415 if (qAbs(currentValue) > threshold) {
1416 QVariantMap axisInfo;
1417 axisInfo[QStringLiteral("axis")] = i;
1418 axisInfo[QStringLiteral("label")] = axisLabel(i);
1419 axisInfo[QStringLiteral("value")] = currentValue;
1420 axisInfo[QStringLiteral("percentage")] = qRound(qAbs(currentValue) * 100.0 / 32767.0);
1421 driftingAxes.append(axisInfo);
1422 }
1423 }
1424
1425 return driftingAxes;
1426}
static QMap< QString, Joystick * > s_discoveryCache
Discovery cache - main thread only, cleared in shutdown()
static bool sdlEventWatcher(void *userdata, SDL_Event *event)
SDL event watcher - uses Qt::QueuedConnection for thread safety.
struct SDL_Joystick SDL_Joystick
Definition JoystickSDL.h:11
#define QGC_LOGGING_CATEGORY(name, categoryStr)
QString getMapping() const override
QString powerState() const override
bool isVirtual() const override
int batteryPercent() const override
quint16 vendorId() const override
QVariantMap getTouchpadFinger(int touchpad, int finger) const override
bool addMapping(const QString &mapping) override
QVariantMap getButtonBinding(int button) const override
void rumbleTriggers(quint16 left, quint16 right, quint32 durationMs) override
bool setVirtualButton(int button, bool down) override
QString serial() const override
bool setMapping(const QString &mapping) override
void updateCachedGyroData(const QVector3D &data)
bool setVirtualTouchpad(int touchpad, int finger, bool down, float x, float y, float pressure) override
QVector3D gyroscopeData() const override
void checkConnectionStateChanged()
bool hasAxis(int axis) const override
bool hasPlayerLED() const override
JoystickSDL(const QString &name, const QList< int > &gamepadAxes, const QList< int > &nonGamepadAxes, int buttonCount, int hatCount, int instanceId, QObject *parent=nullptr)
int playerIndex() const override
bool setVirtualHat(int hat, quint8 value) override
static void shutdown(bool deleteDiscoveryCache=true)
QString deviceType() const override
QString realGamepadType() const override
QVariantMap getBall(int ball) const override
void setPlayerIndex(int index) override
QVariantMap getAxisInitialState(int axis) const override
bool hasGyroscope() const override
bool hasMonoLED() const override
~JoystickSDL() override
quint16 productId() const override
bool setAccelerometerEnabled(bool enabled) override
QString gamepadType() const override
void rumble(quint16 lowFreq, quint16 highFreq, quint32 durationMs) override
bool setVirtualBall(int ball, int dx, int dy) override
QString axisLabel(int axis) const override
QString getMappingForGUID(const QString &guid) const override
bool setVirtualAxis(int axis, int value) override
static bool init()
static QMap< QString, Joystick * > discover()
int ballCount() const override
void hapticRumbleStop() override
QVariantList detectAxisDrift(int threshold=8000) const
bool hasAccelerometer() const override
bool hasRumble() const override
void updateCachedAccelData(const QVector3D &data)
bool hapticRumblePlay(float strength, quint32 durationMs) override
QString buttonLabel(int button) const override
QVector3D accelerometerData() const override
QString connectionState() const override
QString buttonLabelForType(int button) const override
QVariantMap getAxisBinding(int axis) const override
QString connectionType() const override
bool hasButton(int button) const override
QString path() const override
int instanceId() const
Definition JoystickSDL.h:35
void setInstanceId(int instanceId)
Definition JoystickSDL.h:36
QString guid() const override
bool isGamepad() const override
bool hasRumbleTriggers() const override
bool sendVirtualSensorData(int sensorType, float x, float y, float z) override
void setLED(quint8 red, quint8 green, quint8 blue) override
float gyroscopeDataRate() const override
float accelerometerDataRate() const override
bool hasRGBLED() const override
bool hasLED() const override
bool hasHaptic() const override
bool hapticRumbleSupported() const override
bool sendEffect(const QByteArray &data) override
quint16 firmwareVersion() const override
int hapticEffectsCount() const override
int touchpadCount() const override
int touchpadFingerCount(int touchpad) const override
bool hapticRumbleInit() override
bool setGyroscopeEnabled(bool enabled) override
QString name() const
Definition Joystick.h:198
QString _name
Definition Joystick.h:376
void playerIndexChanged()
QString name READ name int int int hatCount
Definition Joystick.h:101
void connectionStateChanged(const QString &newState)
QString name READ name int axisCount
Definition Joystick.h:101
QString name READ name int int buttonCount
Definition Joystick.h:101
void accelerometerDataUpdated(const QVector3D &data)
void gyroscopeDataUpdated(const QVector3D &data)
RAII lock guard for joysticks.
Definition SDLJoystick.h:55
QString connectionStateToString(int state)
Connection state to string.
void shutdown()
Shutdown SDL joystick/gamepad subsystems.
QString gamepadTypeDisplayName(int type)
bool init()
Initialize SDL joystick/gamepad subsystems with QGC-specific hints.
QString getNameForInstanceId(int instanceId)
Get device name for instance ID.
void pumpEvents()
Pump SDL events (call periodically)
QVariantMap findBinding(SDL_Gamepad *gamepad, MatchFunc matchFunc)