1/*
2 Simple DirectMedia Layer
3 Copyright (C) 2025 Simon Wood <simon@mungewell.org>
4 Copyright (C) 2025 Michal Malý <madcatxster@devoid-pointer.net>
5 Copyright (C) 2025 Katharine Chui <katharine.chui@gmail.com>
6
7 This software is provided 'as-is', without any express or implied
8 warranty. In no event will the authors be held liable for any damages
9 arising from the use of this software.
10
11 Permission is granted to anyone to use this software for any purpose,
12 including commercial applications, and to alter it and redistribute it
13 freely, subject to the following restrictions:
14
15 1. The origin of this software must not be misrepresented; you must not
16 claim that you wrote the original software. If you use this software
17 in a product, an acknowledgment in the product documentation would be
18 appreciated but is not required.
19 2. Altered source versions must be plainly marked as such, and must not be
20 misrepresented as being the original software.
21 3. This notice may not be removed or altered from any source distribution.
22*/
23
24#include "SDL_internal.h"
25
26#ifdef SDL_JOYSTICK_HIDAPI
27
28#include "../SDL_sysjoystick.h"
29#include "SDL3/SDL_events.h"
30#include "SDL_hidapijoystick_c.h"
31
32#ifdef SDL_JOYSTICK_HIDAPI_LG4FF
33
34#define USB_VENDOR_ID_LOGITECH 0x046d
35#define USB_DEVICE_ID_LOGITECH_G29_WHEEL 0xc24f
36#define USB_DEVICE_ID_LOGITECH_G27_WHEEL 0xc29b
37#define USB_DEVICE_ID_LOGITECH_G25_WHEEL 0xc299
38#define USB_DEVICE_ID_LOGITECH_DFGT_WHEEL 0xc29a
39#define USB_DEVICE_ID_LOGITECH_DFP_WHEEL 0xc298
40#define USB_DEVICE_ID_LOGITECH_WHEEL 0xc294
41
42static Uint32 supported_device_ids[] = {
43 USB_DEVICE_ID_LOGITECH_G29_WHEEL,
44 USB_DEVICE_ID_LOGITECH_G27_WHEEL,
45 USB_DEVICE_ID_LOGITECH_G25_WHEEL,
46 USB_DEVICE_ID_LOGITECH_DFGT_WHEEL,
47 USB_DEVICE_ID_LOGITECH_DFP_WHEEL,
48 USB_DEVICE_ID_LOGITECH_WHEEL
49};
50
51// keep the same order as the supported_ids array
52static const char *supported_device_names[] = {
53 "Logitech G29",
54 "Logitech G27",
55 "Logitech G25",
56 "Logitech Driving Force GT",
57 "Logitech Driving Force Pro",
58 "Driving Force EX"
59};
60
61static const char *HIDAPI_DriverLg4ff_GetDeviceName(Uint32 device_id)
62{
63 for (int i = 0;i < (sizeof supported_device_ids) / sizeof(Uint32);i++) {
64 if (supported_device_ids[i] == device_id) {
65 return supported_device_names[i];
66 }
67 }
68 SDL_assert(0);
69 return "";
70}
71
72static int HIDAPI_DriverLg4ff_GetNumberOfButtons(Uint32 device_id)
73{
74 switch (device_id) {
75 case USB_DEVICE_ID_LOGITECH_G29_WHEEL:
76 return 25;
77 case USB_DEVICE_ID_LOGITECH_G27_WHEEL:
78 return 22;
79 case USB_DEVICE_ID_LOGITECH_G25_WHEEL:
80 return 19;
81 case USB_DEVICE_ID_LOGITECH_DFGT_WHEEL:
82 return 21;
83 case USB_DEVICE_ID_LOGITECH_DFP_WHEEL:
84 return 14;
85 case USB_DEVICE_ID_LOGITECH_WHEEL:
86 return 13;
87 default:
88 SDL_assert(0);
89 return 0;
90 }
91}
92
93typedef struct
94{
95 Uint8 last_report_buf[32];
96 bool initialized;
97 bool is_ffex;
98 Uint16 range;
99} SDL_DriverLg4ff_Context;
100
101static void HIDAPI_DriverLg4ff_RegisterHints(SDL_HintCallback callback, void *userdata)
102{
103 SDL_AddHintCallback(SDL_HINT_JOYSTICK_HIDAPI_LG4FF, callback, userdata);
104}
105
106static void HIDAPI_DriverLg4ff_UnregisterHints(SDL_HintCallback callback, void *userdata)
107{
108 SDL_RemoveHintCallback(SDL_HINT_JOYSTICK_HIDAPI_LG4FF, callback, userdata);
109}
110
111static bool HIDAPI_DriverLg4ff_IsEnabled(void)
112{
113 bool enabled = SDL_GetHintBoolean(SDL_HINT_JOYSTICK_HIDAPI_LG4FF,
114 SDL_GetHintBoolean(SDL_HINT_JOYSTICK_HIDAPI, SDL_HIDAPI_DEFAULT));
115
116 return enabled;
117}
118
119/*
120 Wheel id information by:
121 Michal Malý <madcatxster@devoid-pointer.net> <madcatxster@gmail.com>
122 Simon Wood <simon@mungewell.org>
123 `git blame v6.12 drivers/hid/hid-lg4ff.c`, https://github.com/torvalds/linux.git
124*/
125static Uint16 HIDAPI_DriverLg4ff_IdentifyWheel(Uint16 device_id, Uint16 release_number)
126{
127 #define is_device(ret, m, r) { \
128 if ((release_number & m) == r) { \
129 return ret; \
130 } \
131 }
132 #define is_dfp { \
133 is_device(USB_DEVICE_ID_LOGITECH_DFP_WHEEL, 0xf000, 0x1000); \
134 }
135 #define is_dfgt { \
136 is_device(USB_DEVICE_ID_LOGITECH_DFGT_WHEEL, 0xff00, 0x1300); \
137 }
138 #define is_g25 { \
139 is_device(USB_DEVICE_ID_LOGITECH_G25_WHEEL, 0xff00, 0x1200); \
140 }
141 #define is_g27 { \
142 is_device(USB_DEVICE_ID_LOGITECH_G27_WHEEL, 0xfff0, 0x1230); \
143 }
144 #define is_g29 { \
145 is_device(USB_DEVICE_ID_LOGITECH_G29_WHEEL, 0xfff8, 0x1350); \
146 is_device(USB_DEVICE_ID_LOGITECH_G29_WHEEL, 0xff00, 0x8900); \
147 }
148 switch(device_id){
149 case USB_DEVICE_ID_LOGITECH_DFP_WHEEL:
150 case USB_DEVICE_ID_LOGITECH_WHEEL:
151 is_g29;
152 is_g27;
153 is_g25;
154 is_dfgt;
155 is_dfp;
156 break;
157 case USB_DEVICE_ID_LOGITECH_DFGT_WHEEL:
158 is_g29;
159 is_dfgt;
160 break;
161 case USB_DEVICE_ID_LOGITECH_G25_WHEEL:
162 is_g29;
163 is_g27;
164 is_g25;
165 break;
166 case USB_DEVICE_ID_LOGITECH_G27_WHEEL:
167 is_g29;
168 is_g27;
169 break;
170 case USB_DEVICE_ID_LOGITECH_G29_WHEEL:
171 is_g29;
172 break;
173 }
174 return 0;
175 #undef is_device
176 #undef is_dfp
177 #undef is_dfgt
178 #undef is_g25
179 #undef is_g27
180 #undef is_g29
181}
182
183static int SDL_HIDAPI_DriverLg4ff_GetEnvInt(const char *env_name, int min, int max, int def)
184{
185 const char *env = SDL_getenv(env_name);
186 int value = 0;
187 if(env == NULL) {
188 return def;
189 }
190 value = SDL_atoi(env);
191 if (value < min) {
192 value = min;
193 }
194 if (value > max) {
195 value = max;
196 }
197 return value;
198}
199
200/*
201 Commands by:
202 Michal Malý <madcatxster@devoid-pointer.net> <madcatxster@gmail.com>
203 Simon Wood <simon@mungewell.org>
204 `git blame v6.12 drivers/hid/hid-lg4ff.c`, https://github.com/torvalds/linux.git
205*/
206static bool HIDAPI_DriverLg4ff_SwitchMode(SDL_HIDAPI_Device *device, Uint16 target_product_id){
207 int ret = 0;
208
209 switch(target_product_id){
210 case USB_DEVICE_ID_LOGITECH_G29_WHEEL:{
211 Uint8 cmd[] = {0xf8, 0x09, 0x05, 0x01, 0x01, 0x00, 0x00};
212 ret = SDL_hid_write(device->dev, cmd, sizeof(cmd));
213 break;
214 }
215 case USB_DEVICE_ID_LOGITECH_G27_WHEEL:{
216 Uint8 cmd[] = {0xf8, 0x09, 0x04, 0x01, 0x00, 0x00, 0x00};
217 ret = SDL_hid_write(device->dev, cmd, sizeof(cmd));
218 break;
219 }
220 case USB_DEVICE_ID_LOGITECH_G25_WHEEL:{
221 Uint8 cmd[] = {0xf8, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00};
222 ret = SDL_hid_write(device->dev, cmd, sizeof(cmd));
223 break;
224 }
225 case USB_DEVICE_ID_LOGITECH_DFGT_WHEEL:{
226 Uint8 cmd[] = {0xf8, 0x09, 0x03, 0x01, 0x00, 0x00, 0x00};
227 ret = SDL_hid_write(device->dev, cmd, sizeof(cmd));
228 break;
229 }
230 case USB_DEVICE_ID_LOGITECH_DFP_WHEEL:{
231 Uint8 cmd[] = {0xf8, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00};
232 ret = SDL_hid_write(device->dev, cmd, sizeof(cmd));
233 break;
234 }
235 case USB_DEVICE_ID_LOGITECH_WHEEL:{
236 Uint8 cmd[] = {0xf8, 0x09, 0x00, 0x01, 0x00, 0x00, 0x00};
237 ret = SDL_hid_write(device->dev, cmd, sizeof(cmd));
238 break;
239 }
240 default:{
241 SDL_assert(0);
242 }
243 }
244 if(ret == -1){
245 return false;
246 }
247 return true;
248}
249
250static bool HIDAPI_DriverLg4ff_IsSupportedDevice(
251 SDL_HIDAPI_Device *device,
252 const char *name,
253 SDL_GamepadType type,
254 Uint16 vendor_id,
255 Uint16 product_id,
256 Uint16 version,
257 int interface_number,
258 int interface_class,
259 int interface_subclass,
260 int interface_protocol)
261{
262 int i;
263 if (vendor_id != USB_VENDOR_ID_LOGITECH) {
264 return false;
265 }
266 for (i = 0;i < sizeof(supported_device_ids) / sizeof(Uint32);i++) {
267 if (supported_device_ids[i] == product_id) {
268 break;
269 }
270 }
271 if (i == sizeof(supported_device_ids) / sizeof(Uint32)) {
272 return false;
273 }
274 Uint16 real_id = HIDAPI_DriverLg4ff_IdentifyWheel(product_id, version);
275 if (real_id == product_id || real_id == 0) {
276 // either it is already in native mode, or we don't know what the native mode is
277 return true;
278 }
279 // a supported native mode is found, send mode change command, then still state that we support the device
280 if (device != NULL && SDL_HIDAPI_DriverLg4ff_GetEnvInt("SDL_HIDAPI_LG4FF_NO_MODE_SWITCH", 0, 1, 0) == 0) {
281 HIDAPI_DriverLg4ff_SwitchMode(device, real_id);
282 }
283 return true;
284}
285
286/*
287 *Ported*
288 Original functions by:
289 Michal Malý <madcatxster@devoid-pointer.net> <madcatxster@gmail.com>
290 lg4ff_set_range_g25 lg4ff_set_range_dfp
291 `git blame v6.12 drivers/hid/hid-lg4ff.c`, https://github.com/torvalds/linux.git
292*/
293static bool HIDAPI_DriverLg4ff_SetRange(SDL_HIDAPI_Device *device, int range)
294{
295 Uint8 cmd[7] = {0};
296 int ret = 0;
297 SDL_DriverLg4ff_Context *ctx = (SDL_DriverLg4ff_Context *)device->context;
298
299 if (range < 40) {
300 range = 40;
301 }
302 if (range > 900) {
303 range = 900;
304 }
305
306 ctx->range = (Uint16)range;
307 switch (device->product_id) {
308 case USB_DEVICE_ID_LOGITECH_G29_WHEEL:
309 case USB_DEVICE_ID_LOGITECH_G27_WHEEL:
310 case USB_DEVICE_ID_LOGITECH_G25_WHEEL:
311 case USB_DEVICE_ID_LOGITECH_DFGT_WHEEL:{
312 cmd[0] = 0xf8;
313 cmd[1] = 0x81;
314 cmd[2] = range & 0x00ff;
315 cmd[3] = (range & 0xff00) >> 8;
316 ret = SDL_hid_write(device->dev, cmd, sizeof(cmd));
317 if (ret == -1) {
318 return false;
319 }
320 break;
321 }
322 case USB_DEVICE_ID_LOGITECH_DFP_WHEEL:{
323 int start_left, start_right, full_range;
324
325 /* Prepare "coarse" limit command */
326 cmd[0] = 0xf8;
327 cmd[1] = 0x00; /* Set later */
328 cmd[2] = 0x00;
329 cmd[3] = 0x00;
330 cmd[4] = 0x00;
331 cmd[5] = 0x00;
332 cmd[6] = 0x00;
333
334 if (range > 200) {
335 cmd[1] = 0x03;
336 full_range = 900;
337 } else {
338 cmd[1] = 0x02;
339 full_range = 200;
340 }
341 ret = SDL_hid_write(device->dev, cmd, 7);
342 if(ret == -1){
343 return false;
344 }
345
346 /* Prepare "fine" limit command */
347 cmd[0] = 0x81;
348 cmd[1] = 0x0b;
349 cmd[2] = 0x00;
350 cmd[3] = 0x00;
351 cmd[4] = 0x00;
352 cmd[5] = 0x00;
353 cmd[6] = 0x00;
354
355 if (range != 200 && range != 900) {
356 /* Construct fine limit command */
357 start_left = (((full_range - range + 1) * 2047) / full_range);
358 start_right = 0xfff - start_left;
359
360 cmd[2] = (Uint8)(start_left >> 4);
361 cmd[3] = (Uint8)(start_right >> 4);
362 cmd[4] = 0xff;
363 cmd[5] = (start_right & 0xe) << 4 | (start_left & 0xe);
364 cmd[6] = 0xff;
365 }
366
367 ret = SDL_hid_write(device->dev, cmd, 7);
368 if (ret == -1) {
369 return false;
370 }
371 break;
372 }
373 case USB_DEVICE_ID_LOGITECH_WHEEL:
374 // no range setting for ffex/dfex
375 break;
376 default:
377 SDL_assert(0);
378 }
379
380 return true;
381}
382
383/*
384 *Ported*
385 Original functions by:
386 Simon Wood <simon@mungewell.org>
387 Michal Malý <madcatxster@devoid-pointer.net> <madcatxster@gmail.com>
388 lg4ff_set_autocenter_default lg4ff_set_autocenter_ffex
389 `git blame v6.12 drivers/hid/hid-lg4ff.c`, https://github.com/torvalds/linux.git
390*/
391static bool HIDAPI_DriverLg4ff_SetAutoCenter(SDL_HIDAPI_Device *device, int magnitude)
392{
393 SDL_DriverLg4ff_Context *ctx = (SDL_DriverLg4ff_Context *)device->context;
394 Uint8 cmd[7] = {0};
395 int ret;
396
397 if (magnitude < 0) {
398 magnitude = 0;
399 }
400 if (magnitude > 65535) {
401 magnitude = 65535;
402 }
403
404 if (ctx->is_ffex) {
405 magnitude = magnitude * 90 / 65535;
406
407 cmd[0] = 0xfe;
408 cmd[1] = 0x03;
409 cmd[2] = (Uint8)((Uint16)magnitude >> 14);
410 cmd[3] = (Uint8)((Uint16)magnitude >> 14);
411 cmd[4] = (Uint8)magnitude;
412
413 ret = SDL_hid_write(device->dev, cmd, sizeof(cmd));
414 if(ret == -1){
415 return false;
416 }
417 } else {
418 Uint32 expand_a;
419 Uint32 expand_b;
420 // first disable
421 cmd[0] = 0xf5;
422
423 ret = SDL_hid_write(device->dev, cmd, sizeof(cmd));
424 if (ret == -1) {
425 return false;
426 }
427
428 if (magnitude == 0) {
429 return true;
430 }
431
432 // set strength
433
434 if (magnitude <= 0xaaaa) {
435 expand_a = 0x0c * magnitude;
436 expand_b = 0x80 * magnitude;
437 } else {
438 expand_a = (0x0c * 0xaaaa) + 0x06 * (magnitude - 0xaaaa);
439 expand_b = (0x80 * 0xaaaa) + 0xff * (magnitude - 0xaaaa);
440 }
441 // TODO do not adjust for MOMO wheels, when support is added
442 expand_a = expand_a >> 1;
443
444 SDL_memset(cmd, 0x00, sizeof(cmd));
445 cmd[0] = 0xfe;
446 cmd[1] = 0x0d;
447 cmd[2] = (Uint8)(expand_a / 0xaaaa);
448 cmd[3] = (Uint8)(expand_a / 0xaaaa);
449 cmd[4] = (Uint8)(expand_b / 0xaaaa);
450
451 ret = SDL_hid_write(device->dev, cmd, sizeof(cmd));
452 if (ret == -1) {
453 return false;
454 }
455
456 // enable
457 SDL_memset(cmd, 0x00, sizeof(cmd));
458 cmd[0] = 0x14;
459
460 ret = SDL_hid_write(device->dev, cmd, sizeof(cmd));
461 if (ret == -1) {
462 return false;
463 }
464 }
465 return true;
466}
467
468/*
469 ffex identification method by:
470 Simon Wood <simon@mungewell.org>
471 Michal Malý <madcatxster@devoid-pointer.net> <madcatxster@gmail.com>
472 lg4ff_init
473 `git blame v6.12 drivers/hid/hid-lg4ff.c`, https://github.com/torvalds/linux.git
474*/
475static bool HIDAPI_DriverLg4ff_InitDevice(SDL_HIDAPI_Device *device)
476{
477 SDL_DriverLg4ff_Context *ctx;
478
479 ctx = (SDL_DriverLg4ff_Context *)SDL_malloc(sizeof(SDL_DriverLg4ff_Context));
480 if (ctx == NULL) {
481 SDL_OutOfMemory();
482 return false;
483 }
484 SDL_memset(ctx, 0, sizeof(SDL_DriverLg4ff_Context));
485
486 device->context = ctx;
487 device->joystick_type = SDL_JOYSTICK_TYPE_WHEEL;
488
489 HIDAPI_SetDeviceName(device, HIDAPI_DriverLg4ff_GetDeviceName(device->product_id));
490
491 if (SDL_hid_set_nonblocking(device->dev, 1) != 0) {
492 return false;
493 }
494
495 if (!HIDAPI_DriverLg4ff_SetAutoCenter(device, 0)) {
496 return false;
497 }
498
499 if (device->product_id == USB_DEVICE_ID_LOGITECH_WHEEL &&
500 (device->version >> 8) == 0x21 &&
501 (device->version & 0xff) == 0x00) {
502 ctx->is_ffex = true;
503 } else {
504 ctx->is_ffex = false;
505 }
506
507 ctx->range = 900;
508
509 return HIDAPI_JoystickConnected(device, NULL);
510}
511
512static int HIDAPI_DriverLg4ff_GetDevicePlayerIndex(SDL_HIDAPI_Device *device, SDL_JoystickID instance_id)
513{
514 return -1;
515}
516
517static void HIDAPI_DriverLg4ff_SetDevicePlayerIndex(SDL_HIDAPI_Device *device, SDL_JoystickID instance_id, int player_index)
518{
519}
520
521
522static bool HIDAPI_DriverLg4ff_GetBit(const Uint8 *buf, int bit_num, size_t buf_len)
523{
524 int byte_offset = bit_num / 8;
525 int local_bit = bit_num % 8;
526 Uint8 mask = 1 << local_bit;
527 if ((size_t)byte_offset >= buf_len) {
528 SDL_assert(0);
529 }
530 return (buf[byte_offset] & mask) ? true : false;
531}
532
533/*
534 *Ported*
535 Original functions by:
536 Michal Malý <madcatxster@devoid-pointer.net> <madcatxster@gmail.com>
537 lg4ff_adjust_dfp_x_axis
538 `git blame v6.12 drivers/hid/hid-lg4ff.c`, https://github.com/torvalds/linux.git
539*/
540static Uint16 lg4ff_adjust_dfp_x_axis(Uint16 value, Uint16 range)
541{
542 Uint16 max_range;
543 Sint32 new_value;
544
545 if (range == 900)
546 return value;
547 else if (range == 200)
548 return value;
549 else if (range < 200)
550 max_range = 200;
551 else
552 max_range = 900;
553
554 new_value = 8192 + ((value - 8192) * max_range / range);
555 if (new_value < 0)
556 return 0;
557 else if (new_value > 16383)
558 return 16383;
559 else
560 return (Uint16)new_value;
561}
562
563static bool HIDAPI_DriverLg4ff_HandleState(SDL_HIDAPI_Device *device,
564 SDL_Joystick *joystick,
565 Uint8 *report_buf,
566 size_t report_size)
567{
568 SDL_DriverLg4ff_Context *ctx = (SDL_DriverLg4ff_Context *)device->context;
569 Uint8 hat = 0;
570 Uint8 last_hat = 0;
571 int num_buttons = HIDAPI_DriverLg4ff_GetNumberOfButtons(device->product_id);
572 int bit_offset = 0;
573 Uint64 timestamp = SDL_GetTicksNS();
574
575 bool state_changed = false;
576
577 switch (device->product_id) {
578 case USB_DEVICE_ID_LOGITECH_G29_WHEEL:
579 case USB_DEVICE_ID_LOGITECH_G27_WHEEL:
580 case USB_DEVICE_ID_LOGITECH_G25_WHEEL:
581 case USB_DEVICE_ID_LOGITECH_DFGT_WHEEL:
582 hat = report_buf[0] & 0x0f;
583 last_hat = ctx->last_report_buf[0] & 0x0f;
584 break;
585 case USB_DEVICE_ID_LOGITECH_DFP_WHEEL:
586 hat = report_buf[3] >> 4;
587 last_hat = ctx->last_report_buf[3] >> 4;
588 break;
589 case USB_DEVICE_ID_LOGITECH_WHEEL:
590 hat = report_buf[2] & 0x0F;
591 last_hat = ctx->last_report_buf[2] & 0x0F;
592 break;
593 default:
594 SDL_assert(0);
595 }
596
597 if (hat != last_hat) {
598 Uint8 sdl_hat = 0;
599 state_changed = true;
600 switch (hat) {
601 case 0:
602 sdl_hat = SDL_HAT_UP;
603 break;
604 case 1:
605 sdl_hat = SDL_HAT_RIGHTUP;
606 break;
607 case 2:
608 sdl_hat = SDL_HAT_RIGHT;
609 break;
610 case 3:
611 sdl_hat = SDL_HAT_RIGHTDOWN;
612 break;
613 case 4:
614 sdl_hat = SDL_HAT_DOWN;
615 break;
616 case 5:
617 sdl_hat = SDL_HAT_LEFTDOWN;
618 break;
619 case 6:
620 sdl_hat = SDL_HAT_LEFT;
621 break;
622 case 7:
623 sdl_hat = SDL_HAT_LEFTUP;
624 break;
625 case 8:
626 sdl_hat = SDL_HAT_CENTERED;
627 break;
628 // do not assert out, in case hardware can report weird hat values
629 }
630 SDL_SendJoystickHat(timestamp, joystick, 0, sdl_hat);
631 }
632
633 switch (device->product_id) {
634 case USB_DEVICE_ID_LOGITECH_G29_WHEEL:
635 case USB_DEVICE_ID_LOGITECH_G27_WHEEL:
636 case USB_DEVICE_ID_LOGITECH_G25_WHEEL:
637 case USB_DEVICE_ID_LOGITECH_DFGT_WHEEL:
638 bit_offset = 4;
639 break;
640 case USB_DEVICE_ID_LOGITECH_DFP_WHEEL:
641 bit_offset = 14;
642 break;
643 case USB_DEVICE_ID_LOGITECH_WHEEL:
644 bit_offset = 0;
645 break;
646 default:
647 SDL_assert(0);
648 }
649
650 for (int i = 0;i < num_buttons;i++) {
651 int bit_num = bit_offset + i;
652 bool button_on = HIDAPI_DriverLg4ff_GetBit(report_buf, bit_num, report_size);
653 bool button_was_on = HIDAPI_DriverLg4ff_GetBit(ctx->last_report_buf, bit_num, report_size);
654 if(button_on != button_was_on){
655 state_changed = true;
656 SDL_SendJoystickButton(timestamp, joystick, (Uint8)(SDL_GAMEPAD_BUTTON_SOUTH + i), button_on);
657 }
658 }
659
660 switch (device->product_id) {
661 case USB_DEVICE_ID_LOGITECH_G29_WHEEL:{
662 Uint16 x = *(Uint16 *)&report_buf[4];
663 Uint16 last_x = *(Uint16 *)&ctx->last_report_buf[4];
664 if (x != last_x) {
665 state_changed = true;
666 SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFTX, x - 32768);
667 }
668 if (report_buf[6] != ctx->last_report_buf[6]) {
669 state_changed = true;
670 SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_RIGHTX, report_buf[6] * 257 - 32768);
671 }
672 if (report_buf[7] != ctx->last_report_buf[7]) {
673 state_changed = true;
674 SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_RIGHTY, report_buf[7] * 257 - 32768);
675 }
676 if (report_buf[8] != ctx->last_report_buf[8]) {
677 state_changed = true;
678 SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFTY, report_buf[8] * 257 - 32768);
679 }
680 break;
681 }
682 case USB_DEVICE_ID_LOGITECH_G27_WHEEL:
683 case USB_DEVICE_ID_LOGITECH_G25_WHEEL:{
684 Uint16 x = report_buf[4] << 6;
685 Uint16 last_x = ctx->last_report_buf[4] << 6;
686 x = x | report_buf[3] >> 2;
687 last_x = last_x | ctx->last_report_buf[3] >> 2;
688 if (x != last_x) {
689 state_changed = true;
690 SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFTX, x * 4 - 32768);
691 }
692 if (report_buf[5] != ctx->last_report_buf[5]) {
693 state_changed = true;
694 SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_RIGHTX, report_buf[5] * 257 - 32768);
695 }
696 if (report_buf[6] != ctx->last_report_buf[6]) {
697 state_changed = true;
698 SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_RIGHTY, report_buf[6] * 257 - 32768);
699 }
700 if (report_buf[7] != ctx->last_report_buf[7]) {
701 state_changed = true;
702 SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFTY, report_buf[7] * 257 - 32768);
703 }
704 break;
705 }
706 case USB_DEVICE_ID_LOGITECH_DFGT_WHEEL:{
707 Uint16 x = report_buf[4];
708 Uint16 last_x = ctx->last_report_buf[4];
709 x = x | (report_buf[5] & 0x3F) << 8;
710 last_x = last_x | (ctx->last_report_buf[5] & 0x3F) << 8;
711 if (x != last_x) {
712 state_changed = true;
713 SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFTX, x * 4 - 32768);
714 }
715 if (report_buf[6] != ctx->last_report_buf[6]) {
716 state_changed = true;
717 SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFTY, report_buf[6] * 257 - 32768);
718 }
719 if (report_buf[7] != ctx->last_report_buf[7]) {
720 state_changed = true;
721 SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_RIGHTX, report_buf[7] * 257 - 32768);
722 }
723 break;
724 }
725 case USB_DEVICE_ID_LOGITECH_DFP_WHEEL:{
726 Uint16 x = report_buf[0];
727 Uint16 last_x = ctx->last_report_buf[0];
728 x = x | (report_buf[1] & 0x3F) << 8;
729 last_x = last_x | (ctx->last_report_buf[1] & 0x3F) << 8;
730 if (x != last_x) {
731 state_changed = true;
732 SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFTX, lg4ff_adjust_dfp_x_axis(x, ctx->range) * 4 - 32768);
733 }
734 if (report_buf[5] != ctx->last_report_buf[5]) {
735 state_changed = true;
736 SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFTY, report_buf[5] * 257 - 32768);
737 }
738 if (report_buf[6] != ctx->last_report_buf[6]) {
739 state_changed = true;
740 SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_RIGHTX, report_buf[6] * 257 - 32768);
741 }
742 break;
743 }
744 case USB_DEVICE_ID_LOGITECH_WHEEL:{
745 if (report_buf[3] != ctx->last_report_buf[3]) {
746 state_changed = true;
747 SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFTX, report_buf[3] * 257 - 32768);
748 }
749 if (report_buf[4] != ctx->last_report_buf[4]) {
750 state_changed = true;
751 SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFTY, report_buf[4] * 257 - 32768);
752 }
753 if (report_buf[5] != ctx->last_report_buf[5]) {
754 state_changed = true;
755 SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_RIGHTX, report_buf[5] * 257 - 32768);
756 }
757 if (report_buf[6] != ctx->last_report_buf[6]) {
758 state_changed = true;
759 SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_RIGHTY, report_buf[7] * 257 - 32768);
760 }
761 break;
762 }
763 default:
764 SDL_assert(0);
765 }
766
767 SDL_memcpy(ctx->last_report_buf, report_buf, report_size);
768 return state_changed;
769}
770
771static bool HIDAPI_DriverLg4ff_UpdateDevice(SDL_HIDAPI_Device *device)
772{
773 SDL_Joystick *joystick = NULL;
774 int r;
775 Uint8 report_buf[32] = {0};
776 size_t report_size = 0;
777 SDL_DriverLg4ff_Context *ctx = (SDL_DriverLg4ff_Context *)device->context;
778
779 if (device->num_joysticks > 0) {
780 joystick = SDL_GetJoystickFromID(device->joysticks[0]);
781 if (joystick == NULL) {
782 return false;
783 }
784 } else {
785 return false;
786 }
787
788 switch (device->product_id) {
789 case USB_DEVICE_ID_LOGITECH_G29_WHEEL:
790 report_size = 12;
791 break;
792 case USB_DEVICE_ID_LOGITECH_G27_WHEEL:
793 case USB_DEVICE_ID_LOGITECH_G25_WHEEL:
794 report_size = 11;
795 break;
796 case USB_DEVICE_ID_LOGITECH_DFGT_WHEEL:
797 case USB_DEVICE_ID_LOGITECH_DFP_WHEEL:
798 report_size = 8;
799 break;
800 case USB_DEVICE_ID_LOGITECH_WHEEL:
801 report_size = 27;
802 break;
803 default:
804 SDL_assert(0);
805 }
806
807 do {
808 r = SDL_hid_read(device->dev, report_buf, report_size);
809 if (r < 0) {
810 /* Failed to read from controller */
811 HIDAPI_JoystickDisconnected(device, device->joysticks[0]);
812 return false;
813 } else if ((size_t)r == report_size) {
814 bool state_changed = HIDAPI_DriverLg4ff_HandleState(device, joystick, report_buf, report_size);
815 if(state_changed && !ctx->initialized) {
816 ctx->initialized = true;
817 HIDAPI_DriverLg4ff_SetRange(device, SDL_HIDAPI_DriverLg4ff_GetEnvInt("SDL_HIDAPI_LG4FF_RANGE", 40, 900, 900));
818 HIDAPI_DriverLg4ff_SetAutoCenter(device, 0);
819 }
820 }
821 } while (r > 0);
822
823 return true;
824}
825
826static bool HIDAPI_DriverLg4ff_OpenJoystick(SDL_HIDAPI_Device *device, SDL_Joystick *joystick)
827{
828 SDL_AssertJoysticksLocked();
829
830 // Initialize the joystick capabilities
831 joystick->nhats = 1;
832 joystick->nbuttons = HIDAPI_DriverLg4ff_GetNumberOfButtons(device->product_id);
833 switch(device->product_id){
834 case USB_DEVICE_ID_LOGITECH_G29_WHEEL:
835 case USB_DEVICE_ID_LOGITECH_G27_WHEEL:
836 case USB_DEVICE_ID_LOGITECH_G25_WHEEL:
837 case USB_DEVICE_ID_LOGITECH_WHEEL:
838 joystick->naxes = 4;
839 break;
840 case USB_DEVICE_ID_LOGITECH_DFGT_WHEEL:
841 joystick->naxes = 3;
842 break;
843 case USB_DEVICE_ID_LOGITECH_DFP_WHEEL:
844 joystick->naxes = 3;
845 break;
846 default:
847 SDL_assert(0);
848 }
849
850 return true;
851}
852
853static bool HIDAPI_DriverLg4ff_RumbleJoystick(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, Uint16 low_frequency_rumble, Uint16 high_frequency_rumble)
854{
855 return SDL_Unsupported();
856}
857
858static bool HIDAPI_DriverLg4ff_RumbleJoystickTriggers(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, Uint16 left_rumble, Uint16 right_rumble)
859{
860 return SDL_Unsupported();
861}
862
863static Uint32 HIDAPI_DriverLg4ff_GetJoystickCapabilities(SDL_HIDAPI_Device *device, SDL_Joystick *joystick)
864{
865 switch(device->product_id) {
866 case USB_DEVICE_ID_LOGITECH_G29_WHEEL:
867 case USB_DEVICE_ID_LOGITECH_G27_WHEEL:
868 return SDL_JOYSTICK_CAP_MONO_LED;
869 default:
870 return 0;
871 }
872}
873
874/*
875 Commands by:
876 Michal Malý <madcatxster@devoid-pointer.net> <madcatxster@gmail.com>
877 Simon Wood <simon@mungewell.org>
878 lg4ff_led_set_brightness lg4ff_set_leds
879 `git blame v6.12 drivers/hid/hid-lg4ff.c`, https://github.com/torvalds/linux.git
880*/
881static bool HIDAPI_DriverLg4ff_SendLedCommand(SDL_HIDAPI_Device *device, Uint8 state)
882{
883 Uint8 cmd[7];
884 Uint8 led_state = 0;
885
886 switch (state) {
887 case 0:
888 led_state = 0;
889 break;
890 case 1:
891 led_state = 1;
892 break;
893 case 2:
894 led_state = 3;
895 break;
896 case 3:
897 led_state = 7;
898 break;
899 case 4:
900 led_state = 15;
901 break;
902 case 5:
903 led_state = 31;
904 break;
905 default:
906 SDL_assert(0);
907 }
908
909 cmd[0] = 0xf8;
910 cmd[1] = 0x12;
911 cmd[2] = led_state;
912 cmd[3] = 0x00;
913 cmd[4] = 0x00;
914 cmd[5] = 0x00;
915 cmd[6] = 0x00;
916
917 return SDL_hid_write(device->dev, cmd, sizeof(cmd)) == sizeof(cmd);
918}
919
920static bool HIDAPI_DriverLg4ff_SetJoystickLED(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, Uint8 red, Uint8 green, Uint8 blue)
921{
922 int max_led = red;
923
924 // only g27/g29, and g923 when supported is added
925 if (device->product_id != USB_DEVICE_ID_LOGITECH_G29_WHEEL &&
926 device->product_id != USB_DEVICE_ID_LOGITECH_G27_WHEEL) {
927 return SDL_Unsupported();
928 }
929
930 if (green > max_led) {
931 max_led = green;
932 }
933 if (blue > max_led) {
934 max_led = blue;
935 }
936
937 return HIDAPI_DriverLg4ff_SendLedCommand(device, (Uint8)((5 * max_led) / 255));
938}
939
940static bool HIDAPI_DriverLg4ff_SendJoystickEffect(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, const void *data, int size)
941{
942 // allow programs to send raw commands
943 return SDL_hid_write(device->dev, data, size) == size;
944}
945
946static bool HIDAPI_DriverLg4ff_SetSensorsEnabled(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, bool enabled)
947{
948 // On steam deck, sensors are enabled by default. Nothing to do here.
949 return SDL_Unsupported();
950}
951
952static void HIDAPI_DriverLg4ff_CloseJoystick(SDL_HIDAPI_Device *device, SDL_Joystick *joystick)
953{
954 // remember to stop effects on haptics close, when implemented
955 HIDAPI_DriverLg4ff_SetJoystickLED(device, joystick, 0, 0, 0);
956}
957
958static void HIDAPI_DriverLg4ff_FreeDevice(SDL_HIDAPI_Device *device)
959{
960 // device context is freed in SDL_hidapijoystick.c
961}
962
963
964SDL_HIDAPI_DeviceDriver SDL_HIDAPI_DriverLg4ff = {
965 SDL_HINT_JOYSTICK_HIDAPI_LG4FF,
966 true,
967 HIDAPI_DriverLg4ff_RegisterHints,
968 HIDAPI_DriverLg4ff_UnregisterHints,
969 HIDAPI_DriverLg4ff_IsEnabled,
970 HIDAPI_DriverLg4ff_IsSupportedDevice,
971 HIDAPI_DriverLg4ff_InitDevice,
972 HIDAPI_DriverLg4ff_GetDevicePlayerIndex,
973 HIDAPI_DriverLg4ff_SetDevicePlayerIndex,
974 HIDAPI_DriverLg4ff_UpdateDevice,
975 HIDAPI_DriverLg4ff_OpenJoystick,
976 HIDAPI_DriverLg4ff_RumbleJoystick,
977 HIDAPI_DriverLg4ff_RumbleJoystickTriggers,
978 HIDAPI_DriverLg4ff_GetJoystickCapabilities,
979 HIDAPI_DriverLg4ff_SetJoystickLED,
980 HIDAPI_DriverLg4ff_SendJoystickEffect,
981 HIDAPI_DriverLg4ff_SetSensorsEnabled,
982 HIDAPI_DriverLg4ff_CloseJoystick,
983 HIDAPI_DriverLg4ff_FreeDevice,
984};
985
986
987#endif /* SDL_JOYSTICK_HIDAPI_LG4FF */
988
989#endif /* SDL_JOYSTICK_HIDAPI */
990