1 | /* |
2 | Simple DirectMedia Layer |
3 | Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org> |
4 | |
5 | This software is provided 'as-is', without any express or implied |
6 | warranty. In no event will the authors be held liable for any damages |
7 | arising from the use of this software. |
8 | |
9 | Permission is granted to anyone to use this software for any purpose, |
10 | including commercial applications, and to alter it and redistribute it |
11 | freely, subject to the following restrictions: |
12 | |
13 | 1. The origin of this software must not be misrepresented; you must not |
14 | claim that you wrote the original software. If you use this software |
15 | in a product, an acknowledgment in the product documentation would be |
16 | appreciated but is not required. |
17 | 2. Altered source versions must be plainly marked as such, and must not be |
18 | misrepresented as being the original software. |
19 | 3. This notice may not be removed or altered from any source distribution. |
20 | */ |
21 | #include "SDL_internal.h" |
22 | |
23 | #ifdef SDL_JOYSTICK_HIDAPI |
24 | |
25 | #include "../SDL_sysjoystick.h" |
26 | #include "SDL_hidapijoystick_c.h" |
27 | #include "SDL_hidapi_rumble.h" |
28 | |
29 | #ifdef SDL_JOYSTICK_HIDAPI_LUNA |
30 | |
31 | // Define this if you want to log all packets from the controller |
32 | // #define DEBUG_LUNA_PROTOCOL |
33 | |
34 | // Sending rumble on macOS blocks for a long time and eventually fails |
35 | #ifndef SDL_PLATFORM_MACOS |
36 | #define ENABLE_LUNA_BLUETOOTH_RUMBLE |
37 | #endif |
38 | |
39 | enum |
40 | { |
41 | SDL_GAMEPAD_BUTTON_LUNA_MICROPHONE = 11, |
42 | SDL_GAMEPAD_NUM_LUNA_BUTTONS, |
43 | }; |
44 | |
45 | typedef struct |
46 | { |
47 | Uint8 last_state[USB_PACKET_LENGTH]; |
48 | } SDL_DriverLuna_Context; |
49 | |
50 | static void HIDAPI_DriverLuna_RegisterHints(SDL_HintCallback callback, void *userdata) |
51 | { |
52 | SDL_AddHintCallback(SDL_HINT_JOYSTICK_HIDAPI_LUNA, callback, userdata); |
53 | } |
54 | |
55 | static void HIDAPI_DriverLuna_UnregisterHints(SDL_HintCallback callback, void *userdata) |
56 | { |
57 | SDL_RemoveHintCallback(SDL_HINT_JOYSTICK_HIDAPI_LUNA, callback, userdata); |
58 | } |
59 | |
60 | static bool HIDAPI_DriverLuna_IsEnabled(void) |
61 | { |
62 | return SDL_GetHintBoolean(SDL_HINT_JOYSTICK_HIDAPI_LUNA, SDL_GetHintBoolean(SDL_HINT_JOYSTICK_HIDAPI, SDL_HIDAPI_DEFAULT)); |
63 | } |
64 | |
65 | static bool HIDAPI_DriverLuna_IsSupportedDevice(SDL_HIDAPI_Device *device, const char *name, SDL_GamepadType type, Uint16 vendor_id, Uint16 product_id, Uint16 version, int interface_number, int interface_class, int interface_subclass, int interface_protocol) |
66 | { |
67 | return SDL_IsJoystickAmazonLunaController(vendor_id, product_id); |
68 | } |
69 | |
70 | static bool HIDAPI_DriverLuna_InitDevice(SDL_HIDAPI_Device *device) |
71 | { |
72 | SDL_DriverLuna_Context *ctx; |
73 | |
74 | ctx = (SDL_DriverLuna_Context *)SDL_calloc(1, sizeof(*ctx)); |
75 | if (!ctx) { |
76 | return false; |
77 | } |
78 | device->context = ctx; |
79 | |
80 | HIDAPI_SetDeviceName(device, "Amazon Luna Controller" ); |
81 | |
82 | return HIDAPI_JoystickConnected(device, NULL); |
83 | } |
84 | |
85 | static int HIDAPI_DriverLuna_GetDevicePlayerIndex(SDL_HIDAPI_Device *device, SDL_JoystickID instance_id) |
86 | { |
87 | return -1; |
88 | } |
89 | |
90 | static void HIDAPI_DriverLuna_SetDevicePlayerIndex(SDL_HIDAPI_Device *device, SDL_JoystickID instance_id, int player_index) |
91 | { |
92 | } |
93 | |
94 | static bool HIDAPI_DriverLuna_OpenJoystick(SDL_HIDAPI_Device *device, SDL_Joystick *joystick) |
95 | { |
96 | SDL_DriverLuna_Context *ctx = (SDL_DriverLuna_Context *)device->context; |
97 | |
98 | SDL_AssertJoysticksLocked(); |
99 | |
100 | SDL_zeroa(ctx->last_state); |
101 | |
102 | // Initialize the joystick capabilities |
103 | joystick->nbuttons = SDL_GAMEPAD_NUM_LUNA_BUTTONS; |
104 | joystick->naxes = SDL_GAMEPAD_AXIS_COUNT; |
105 | joystick->nhats = 1; |
106 | |
107 | return true; |
108 | } |
109 | |
110 | static bool HIDAPI_DriverLuna_RumbleJoystick(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, Uint16 low_frequency_rumble, Uint16 high_frequency_rumble) |
111 | { |
112 | #ifdef ENABLE_LUNA_BLUETOOTH_RUMBLE |
113 | if (device->product_id == BLUETOOTH_PRODUCT_LUNA_CONTROLLER) { |
114 | // Same packet as on Xbox One controllers connected via Bluetooth |
115 | Uint8 rumble_packet[] = { 0x03, 0x0F, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xEB }; |
116 | |
117 | // Magnitude is 1..100 so scale the 16-bit input here |
118 | rumble_packet[4] = (Uint8)(low_frequency_rumble / 655); |
119 | rumble_packet[5] = (Uint8)(high_frequency_rumble / 655); |
120 | |
121 | if (SDL_HIDAPI_SendRumble(device, rumble_packet, sizeof(rumble_packet)) != sizeof(rumble_packet)) { |
122 | return SDL_SetError("Couldn't send rumble packet" ); |
123 | } |
124 | |
125 | return true; |
126 | } |
127 | #endif // ENABLE_LUNA_BLUETOOTH_RUMBLE |
128 | |
129 | // There is currently no rumble packet over USB |
130 | return SDL_Unsupported(); |
131 | } |
132 | |
133 | static bool HIDAPI_DriverLuna_RumbleJoystickTriggers(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, Uint16 left_rumble, Uint16 right_rumble) |
134 | { |
135 | return SDL_Unsupported(); |
136 | } |
137 | |
138 | static Uint32 HIDAPI_DriverLuna_GetJoystickCapabilities(SDL_HIDAPI_Device *device, SDL_Joystick *joystick) |
139 | { |
140 | Uint32 result = 0; |
141 | |
142 | #ifdef ENABLE_LUNA_BLUETOOTH_RUMBLE |
143 | if (device->product_id == BLUETOOTH_PRODUCT_LUNA_CONTROLLER) { |
144 | result |= SDL_JOYSTICK_CAP_RUMBLE; |
145 | } |
146 | #endif |
147 | |
148 | return result; |
149 | } |
150 | |
151 | static bool HIDAPI_DriverLuna_SetJoystickLED(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, Uint8 red, Uint8 green, Uint8 blue) |
152 | { |
153 | return SDL_Unsupported(); |
154 | } |
155 | |
156 | static bool HIDAPI_DriverLuna_SendJoystickEffect(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, const void *data, int size) |
157 | { |
158 | return SDL_Unsupported(); |
159 | } |
160 | |
161 | static bool HIDAPI_DriverLuna_SetJoystickSensorsEnabled(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, bool enabled) |
162 | { |
163 | return SDL_Unsupported(); |
164 | } |
165 | |
166 | static void HIDAPI_DriverLuna_HandleUSBStatePacket(SDL_Joystick *joystick, SDL_DriverLuna_Context *ctx, Uint8 *data, int size) |
167 | { |
168 | Uint64 timestamp = SDL_GetTicksNS(); |
169 | |
170 | if (ctx->last_state[1] != data[1]) { |
171 | SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SOUTH, ((data[1] & 0x01) != 0)); |
172 | SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_EAST, ((data[1] & 0x02) != 0)); |
173 | SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_WEST, ((data[1] & 0x04) != 0)); |
174 | SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_NORTH, ((data[1] & 0x08) != 0)); |
175 | SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, ((data[1] & 0x10) != 0)); |
176 | SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, ((data[1] & 0x20) != 0)); |
177 | SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_BACK, ((data[1] & 0x40) != 0)); |
178 | SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_START, ((data[1] & 0x80) != 0)); |
179 | } |
180 | if (ctx->last_state[2] != data[2]) { |
181 | SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_GUIDE, ((data[2] & 0x01) != 0)); |
182 | SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_LUNA_MICROPHONE, ((data[2] & 0x02) != 0)); |
183 | SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_LEFT_STICK, ((data[2] & 0x04) != 0)); |
184 | SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_RIGHT_STICK, ((data[2] & 0x08) != 0)); |
185 | } |
186 | |
187 | if (ctx->last_state[3] != data[3]) { |
188 | Uint8 hat; |
189 | |
190 | switch (data[3] & 0x0f) { |
191 | case 0: |
192 | hat = SDL_HAT_UP; |
193 | break; |
194 | case 1: |
195 | hat = SDL_HAT_RIGHTUP; |
196 | break; |
197 | case 2: |
198 | hat = SDL_HAT_RIGHT; |
199 | break; |
200 | case 3: |
201 | hat = SDL_HAT_RIGHTDOWN; |
202 | break; |
203 | case 4: |
204 | hat = SDL_HAT_DOWN; |
205 | break; |
206 | case 5: |
207 | hat = SDL_HAT_LEFTDOWN; |
208 | break; |
209 | case 6: |
210 | hat = SDL_HAT_LEFT; |
211 | break; |
212 | case 7: |
213 | hat = SDL_HAT_LEFTUP; |
214 | break; |
215 | default: |
216 | hat = SDL_HAT_CENTERED; |
217 | break; |
218 | } |
219 | SDL_SendJoystickHat(timestamp, joystick, 0, hat); |
220 | } |
221 | |
222 | #define READ_STICK_AXIS(offset) \ |
223 | (data[offset] == 0x7f ? 0 : (Sint16)HIDAPI_RemapVal((float)data[offset], 0x00, 0xff, SDL_MIN_SINT16, SDL_MAX_SINT16)) |
224 | { |
225 | Sint16 axis = READ_STICK_AXIS(4); |
226 | SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFTX, axis); |
227 | axis = READ_STICK_AXIS(5); |
228 | SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFTY, axis); |
229 | axis = READ_STICK_AXIS(6); |
230 | SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_RIGHTX, axis); |
231 | axis = READ_STICK_AXIS(7); |
232 | SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_RIGHTY, axis); |
233 | } |
234 | #undef READ_STICK_AXIS |
235 | |
236 | #define READ_TRIGGER_AXIS(offset) \ |
237 | (Sint16) HIDAPI_RemapVal((float)data[offset], 0x00, 0xff, SDL_MIN_SINT16, SDL_MAX_SINT16) |
238 | { |
239 | Sint16 axis = READ_TRIGGER_AXIS(8); |
240 | SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFT_TRIGGER, axis); |
241 | axis = READ_TRIGGER_AXIS(9); |
242 | SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, axis); |
243 | } |
244 | #undef READ_TRIGGER_AXIS |
245 | |
246 | SDL_memcpy(ctx->last_state, data, SDL_min(size, sizeof(ctx->last_state))); |
247 | } |
248 | |
249 | static void HIDAPI_DriverLuna_HandleBluetoothStatePacket(SDL_Joystick *joystick, SDL_DriverLuna_Context *ctx, Uint8 *data, int size) |
250 | { |
251 | Uint64 timestamp = SDL_GetTicksNS(); |
252 | |
253 | if (size >= 2 && data[0] == 0x02) { |
254 | // Home button has dedicated report |
255 | SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_GUIDE, ((data[1] & 0x1) != 0)); |
256 | return; |
257 | } |
258 | |
259 | if (size >= 2 && data[0] == 0x04) { |
260 | // Battery level report |
261 | int percent = (int)SDL_roundf((data[1] / 255.0f) * 100.0f); |
262 | SDL_SendJoystickPowerInfo(joystick, SDL_POWERSTATE_ON_BATTERY, percent); |
263 | return; |
264 | } |
265 | |
266 | if (size < 17 || data[0] != 0x01) { |
267 | // We don't know how to handle this report |
268 | return; |
269 | } |
270 | |
271 | if (ctx->last_state[13] != data[13]) { |
272 | Uint8 hat; |
273 | |
274 | switch (data[13] & 0x0f) { |
275 | case 1: |
276 | hat = SDL_HAT_UP; |
277 | break; |
278 | case 2: |
279 | hat = SDL_HAT_RIGHTUP; |
280 | break; |
281 | case 3: |
282 | hat = SDL_HAT_RIGHT; |
283 | break; |
284 | case 4: |
285 | hat = SDL_HAT_RIGHTDOWN; |
286 | break; |
287 | case 5: |
288 | hat = SDL_HAT_DOWN; |
289 | break; |
290 | case 6: |
291 | hat = SDL_HAT_LEFTDOWN; |
292 | break; |
293 | case 7: |
294 | hat = SDL_HAT_LEFT; |
295 | break; |
296 | case 8: |
297 | hat = SDL_HAT_LEFTUP; |
298 | break; |
299 | default: |
300 | hat = SDL_HAT_CENTERED; |
301 | break; |
302 | } |
303 | SDL_SendJoystickHat(timestamp, joystick, 0, hat); |
304 | } |
305 | |
306 | if (ctx->last_state[14] != data[14]) { |
307 | SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SOUTH, ((data[14] & 0x01) != 0)); |
308 | SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_EAST, ((data[14] & 0x02) != 0)); |
309 | SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_WEST, ((data[14] & 0x08) != 0)); |
310 | SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_NORTH, ((data[14] & 0x10) != 0)); |
311 | SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, ((data[14] & 0x40) != 0)); |
312 | SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, ((data[14] & 0x80) != 0)); |
313 | } |
314 | if (ctx->last_state[15] != data[15]) { |
315 | SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_START, ((data[15] & 0x08) != 0)); |
316 | SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_LEFT_STICK, ((data[15] & 0x20) != 0)); |
317 | SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_RIGHT_STICK, ((data[15] & 0x40) != 0)); |
318 | } |
319 | if (ctx->last_state[16] != data[16]) { |
320 | SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_BACK, ((data[16] & 0x01) != 0)); |
321 | SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_LUNA_MICROPHONE, ((data[16] & 0x02) != 0)); |
322 | } |
323 | |
324 | #define READ_STICK_AXIS(offset) \ |
325 | (data[offset] == 0x7f ? 0 : (Sint16)HIDAPI_RemapVal((float)data[offset], 0x00, 0xff, SDL_MIN_SINT16, SDL_MAX_SINT16)) |
326 | { |
327 | Sint16 axis = READ_STICK_AXIS(2); |
328 | SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFTX, axis); |
329 | axis = READ_STICK_AXIS(4); |
330 | SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFTY, axis); |
331 | axis = READ_STICK_AXIS(6); |
332 | SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_RIGHTX, axis); |
333 | axis = READ_STICK_AXIS(8); |
334 | SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_RIGHTY, axis); |
335 | } |
336 | #undef READ_STICK_AXIS |
337 | |
338 | #define READ_TRIGGER_AXIS(offset) \ |
339 | (Sint16) HIDAPI_RemapVal((float)((int)(((data[offset] | (data[offset + 1] << 8)) & 0x3ff) - 0x200)), 0x00 - 0x200, 0x3ff - 0x200, SDL_MIN_SINT16, SDL_MAX_SINT16) |
340 | { |
341 | Sint16 axis = READ_TRIGGER_AXIS(9); |
342 | SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFT_TRIGGER, axis); |
343 | axis = READ_TRIGGER_AXIS(11); |
344 | SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, axis); |
345 | } |
346 | #undef READ_TRIGGER_AXIS |
347 | |
348 | SDL_memcpy(ctx->last_state, data, SDL_min(size, sizeof(ctx->last_state))); |
349 | } |
350 | |
351 | static bool HIDAPI_DriverLuna_UpdateDevice(SDL_HIDAPI_Device *device) |
352 | { |
353 | SDL_DriverLuna_Context *ctx = (SDL_DriverLuna_Context *)device->context; |
354 | SDL_Joystick *joystick = NULL; |
355 | Uint8 data[USB_PACKET_LENGTH]; |
356 | int size = 0; |
357 | |
358 | if (device->num_joysticks > 0) { |
359 | joystick = SDL_GetJoystickFromID(device->joysticks[0]); |
360 | } else { |
361 | return false; |
362 | } |
363 | |
364 | while ((size = SDL_hid_read_timeout(device->dev, data, sizeof(data), 0)) > 0) { |
365 | #ifdef DEBUG_LUNA_PROTOCOL |
366 | HIDAPI_DumpPacket("Amazon Luna packet: size = %d" , data, size); |
367 | #endif |
368 | if (!joystick) { |
369 | continue; |
370 | } |
371 | |
372 | switch (size) { |
373 | case 10: |
374 | HIDAPI_DriverLuna_HandleUSBStatePacket(joystick, ctx, data, size); |
375 | break; |
376 | default: |
377 | HIDAPI_DriverLuna_HandleBluetoothStatePacket(joystick, ctx, data, size); |
378 | break; |
379 | } |
380 | } |
381 | |
382 | if (size < 0) { |
383 | // Read error, device is disconnected |
384 | HIDAPI_JoystickDisconnected(device, device->joysticks[0]); |
385 | } |
386 | return (size >= 0); |
387 | } |
388 | |
389 | static void HIDAPI_DriverLuna_CloseJoystick(SDL_HIDAPI_Device *device, SDL_Joystick *joystick) |
390 | { |
391 | } |
392 | |
393 | static void HIDAPI_DriverLuna_FreeDevice(SDL_HIDAPI_Device *device) |
394 | { |
395 | } |
396 | |
397 | SDL_HIDAPI_DeviceDriver SDL_HIDAPI_DriverLuna = { |
398 | SDL_HINT_JOYSTICK_HIDAPI_LUNA, |
399 | true, |
400 | HIDAPI_DriverLuna_RegisterHints, |
401 | HIDAPI_DriverLuna_UnregisterHints, |
402 | HIDAPI_DriverLuna_IsEnabled, |
403 | HIDAPI_DriverLuna_IsSupportedDevice, |
404 | HIDAPI_DriverLuna_InitDevice, |
405 | HIDAPI_DriverLuna_GetDevicePlayerIndex, |
406 | HIDAPI_DriverLuna_SetDevicePlayerIndex, |
407 | HIDAPI_DriverLuna_UpdateDevice, |
408 | HIDAPI_DriverLuna_OpenJoystick, |
409 | HIDAPI_DriverLuna_RumbleJoystick, |
410 | HIDAPI_DriverLuna_RumbleJoystickTriggers, |
411 | HIDAPI_DriverLuna_GetJoystickCapabilities, |
412 | HIDAPI_DriverLuna_SetJoystickLED, |
413 | HIDAPI_DriverLuna_SendJoystickEffect, |
414 | HIDAPI_DriverLuna_SetJoystickSensorsEnabled, |
415 | HIDAPI_DriverLuna_CloseJoystick, |
416 | HIDAPI_DriverLuna_FreeDevice, |
417 | }; |
418 | |
419 | #endif // SDL_JOYSTICK_HIDAPI_LUNA |
420 | |
421 | #endif // SDL_JOYSTICK_HIDAPI |
422 | |