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
39enum
40{
41 SDL_GAMEPAD_BUTTON_LUNA_MICROPHONE = 11,
42 SDL_GAMEPAD_NUM_LUNA_BUTTONS,
43};
44
45typedef struct
46{
47 Uint8 last_state[USB_PACKET_LENGTH];
48} SDL_DriverLuna_Context;
49
50static void HIDAPI_DriverLuna_RegisterHints(SDL_HintCallback callback, void *userdata)
51{
52 SDL_AddHintCallback(SDL_HINT_JOYSTICK_HIDAPI_LUNA, callback, userdata);
53}
54
55static void HIDAPI_DriverLuna_UnregisterHints(SDL_HintCallback callback, void *userdata)
56{
57 SDL_RemoveHintCallback(SDL_HINT_JOYSTICK_HIDAPI_LUNA, callback, userdata);
58}
59
60static 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
65static 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
70static 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
85static int HIDAPI_DriverLuna_GetDevicePlayerIndex(SDL_HIDAPI_Device *device, SDL_JoystickID instance_id)
86{
87 return -1;
88}
89
90static void HIDAPI_DriverLuna_SetDevicePlayerIndex(SDL_HIDAPI_Device *device, SDL_JoystickID instance_id, int player_index)
91{
92}
93
94static 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
110static 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
133static bool HIDAPI_DriverLuna_RumbleJoystickTriggers(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, Uint16 left_rumble, Uint16 right_rumble)
134{
135 return SDL_Unsupported();
136}
137
138static 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
151static bool HIDAPI_DriverLuna_SetJoystickLED(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, Uint8 red, Uint8 green, Uint8 blue)
152{
153 return SDL_Unsupported();
154}
155
156static bool HIDAPI_DriverLuna_SendJoystickEffect(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, const void *data, int size)
157{
158 return SDL_Unsupported();
159}
160
161static bool HIDAPI_DriverLuna_SetJoystickSensorsEnabled(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, bool enabled)
162{
163 return SDL_Unsupported();
164}
165
166static 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
249static 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
351static 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
389static void HIDAPI_DriverLuna_CloseJoystick(SDL_HIDAPI_Device *device, SDL_Joystick *joystick)
390{
391}
392
393static void HIDAPI_DriverLuna_FreeDevice(SDL_HIDAPI_Device *device)
394{
395}
396
397SDL_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