Skip to content

Commit 29ca920

Browse files
authored
Add ZUIKI EVOTOP controller support with gyroscope and accelerometer sensor capabilities. (#15034)
1 parent 230814e commit 29ca920

File tree

2 files changed

+209
-3
lines changed

2 files changed

+209
-3
lines changed

src/joystick/hidapi/SDL_hidapi_zuiki.c

Lines changed: 206 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,52 @@
2929

3030
#ifdef SDL_JOYSTICK_HIDAPI_ZUIKI
3131

32+
#define GYRO_SCALE (1024.0f / 32768.0f * SDL_PI_F / 180.0f) // Calculate scaling factor based on gyroscope data range and radians
33+
#define ACCEL_SCALE (8.0f / 32768.0f * SDL_STANDARD_GRAVITY) // Calculate acceleration scaling factor based on gyroscope data range and standard gravity
34+
#define FILTER_SIZE 11 // Must be an odd number
35+
#define MAX_RETRY_COUNT 10 // zuiki device initialization retry count
36+
3237
// Define this if you want to log all packets from the controller
3338
#if 0
3439
#define DEBUG_ZUIKI_PROTOCOL
3540
#endif
3641

42+
typedef struct {
43+
float buffer[FILTER_SIZE];
44+
uint8_t index;
45+
uint8_t count;
46+
} MedianFilter_t;
47+
3748
typedef struct
3849
{
3950
Uint8 last_state[USB_PACKET_LENGTH];
51+
bool sensors_supported; // Sensor enabled status flag
52+
Uint64 sensor_timestamp_ns; // Sensor timestamp (nanoseconds, cumulative update)
53+
float sensor_rate;
54+
MedianFilter_t filter_gyro_x;
55+
MedianFilter_t filter_gyro_y;
56+
MedianFilter_t filter_gyro_z;
4057
} SDL_DriverZUIKI_Context;
4158

59+
static float median_filter_update(MedianFilter_t* mf, float input) {
60+
mf->buffer[mf->index] = input;
61+
mf->index = (mf->index + 1) % FILTER_SIZE;
62+
if (mf->count < FILTER_SIZE) mf->count++;
63+
float temp[FILTER_SIZE];
64+
SDL_memcpy(temp, mf->buffer, sizeof(temp));
65+
for (int i = 0; i < mf->count - 1; i++) {
66+
for (int j = i + 1; j < mf->count; j++) {
67+
if (temp[i] > temp[j]) {
68+
float t = temp[i];
69+
temp[i] = temp[j];
70+
temp[j] = t;
71+
}
72+
}
73+
}
74+
return temp[mf->count / 2];
75+
}
76+
77+
4278
static void HIDAPI_DriverZUIKI_RegisterHints(SDL_HintCallback callback, void *userdata)
4379
{
4480
SDL_AddHintCallback(SDL_HINT_JOYSTICK_HIDAPI_ZUIKI, callback, userdata);
@@ -59,6 +95,9 @@ static bool HIDAPI_DriverZUIKI_IsSupportedDevice(SDL_HIDAPI_Device *device, cons
5995
if (vendor_id == USB_VENDOR_ZUIKI) {
6096
switch (product_id) {
6197
case USB_PRODUCT_ZUIKI_MASCON_PRO:
98+
case USB_PRODUCT_ZUIKI_EVOTOP_UWB_DINPUT:
99+
case USB_PRODUCT_ZUIKI_EVOTOP_PC_DINPUT:
100+
case USB_PRODUCT_ZUIKI_EVOTOP_PC_BT:
62101
return true;
63102
default:
64103
break;
@@ -69,14 +108,49 @@ static bool HIDAPI_DriverZUIKI_IsSupportedDevice(SDL_HIDAPI_Device *device, cons
69108

70109
static bool HIDAPI_DriverZUIKI_InitDevice(SDL_HIDAPI_Device *device)
71110
{
111+
Uint8 data[USB_PACKET_LENGTH * 2];
72112
SDL_DriverZUIKI_Context *ctx = (SDL_DriverZUIKI_Context *)SDL_calloc(1, sizeof(*ctx));
73113
if (!ctx) {
74114
return false;
75115
}
76116
device->context = ctx;
117+
ctx->sensors_supported = false;
118+
119+
// Read report data once for device initialization
120+
int size = -1;
121+
Uint8 retry_count = 0;
122+
while (retry_count < MAX_RETRY_COUNT) {
123+
size = SDL_hid_read_timeout(device->dev, data, sizeof(data), 10);
124+
if (size > 0) {
125+
break;
126+
}
127+
retry_count++;
128+
}
129+
if (size <= 0) {
130+
return false;
131+
}
77132

78-
if (device->product_id == USB_PRODUCT_ZUIKI_MASCON_PRO) {
79-
HIDAPI_SetDeviceName(device, "ZUIKI MASCON PRO");
133+
switch (device->product_id) {
134+
case USB_PRODUCT_ZUIKI_MASCON_PRO:
135+
HIDAPI_SetDeviceName(device, "ZUIKI MASCON PRO");
136+
break;
137+
case USB_PRODUCT_ZUIKI_EVOTOP_PC_DINPUT:
138+
ctx->sensors_supported = true;
139+
ctx->sensor_rate = 200.0f;
140+
break;
141+
case USB_PRODUCT_ZUIKI_EVOTOP_UWB_DINPUT:
142+
ctx->sensors_supported = true;
143+
ctx->sensor_rate = 100.0f;
144+
break;
145+
case USB_PRODUCT_ZUIKI_EVOTOP_PC_BT:
146+
if (size > 0 && data[16] != 0) {
147+
ctx->sensors_supported = true;
148+
ctx->sensor_rate = 50.0f;
149+
}
150+
HIDAPI_SetDeviceName(device, "ZUIKI EVOTOP");
151+
break;
152+
default:
153+
break;
80154
}
81155

82156
return HIDAPI_JoystickConnected(device, NULL);
@@ -106,6 +180,10 @@ static bool HIDAPI_DriverZUIKI_OpenJoystick(SDL_HIDAPI_Device *device, SDL_Joyst
106180
joystick->nbuttons = 11;
107181
joystick->naxes = SDL_GAMEPAD_AXIS_COUNT;
108182
joystick->nhats = 1;
183+
if (ctx->sensors_supported) {
184+
SDL_PrivateJoystickAddSensor(joystick, SDL_SENSOR_GYRO, ctx->sensor_rate);
185+
SDL_PrivateJoystickAddSensor(joystick, SDL_SENSOR_ACCEL, ctx->sensor_rate);
186+
}
109187

110188
return true;
111189
}
@@ -148,6 +226,10 @@ static bool HIDAPI_DriverZUIKI_SendJoystickEffect(SDL_HIDAPI_Device *device, SDL
148226

149227
static bool HIDAPI_DriverZUIKI_SetJoystickSensorsEnabled(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, bool enabled)
150228
{
229+
SDL_DriverZUIKI_Context *ctx = (SDL_DriverZUIKI_Context *)device->context;
230+
if (ctx->sensors_supported) {
231+
return true;
232+
}
151233
return SDL_Unsupported();
152234
}
153235

@@ -226,6 +308,123 @@ static void HIDAPI_DriverZUIKI_HandleOldStatePacket(SDL_Joystick *joystick, SDL_
226308
}
227309
#undef READ_STICK_AXIS
228310

311+
if (ctx->sensors_supported) {
312+
Uint64 sensor_timestamp = timestamp;
313+
float gyro_values[3];
314+
gyro_values[0] = median_filter_update(&ctx->filter_gyro_x, LOAD16(data[8], data[9]) * GYRO_SCALE);
315+
gyro_values[1] = median_filter_update(&ctx->filter_gyro_y, LOAD16(data[12], data[13]) * GYRO_SCALE);
316+
gyro_values[2] = median_filter_update(&ctx->filter_gyro_z, -LOAD16(data[10], data[11]) * GYRO_SCALE);
317+
float accel_values[3];
318+
accel_values[0] = LOAD16(data[14], data[15]) * ACCEL_SCALE;
319+
accel_values[2] = -LOAD16(data[16], data[17]) * ACCEL_SCALE;
320+
accel_values[1] = LOAD16(data[18], data[19]) * ACCEL_SCALE;
321+
#ifdef DEBUG_ZUIKI_PROTOCOL
322+
SDL_Log("Gyro raw: %d, %d, %d -> scaled: %.2f, %.2f, %.2f rad/s",
323+
LOAD16(data[8], data[9]), LOAD16(data[10], data[11]), LOAD16(data[12], data[13]),
324+
gyro_values[0], gyro_values[1], gyro_values[2]);
325+
SDL_Log("Accel raw: %d, %d, %d -> scaled: %.2f, %.2f, %.2f m/s²",
326+
LOAD16(data[14], data[15]), LOAD16(data[16], data[17]), LOAD16(data[18], data[19]),
327+
accel_values[0], accel_values[1], accel_values[2]);
328+
#endif
329+
330+
SDL_SendJoystickSensor(timestamp, joystick, SDL_SENSOR_GYRO, sensor_timestamp, gyro_values, 3);
331+
SDL_SendJoystickSensor(timestamp, joystick, SDL_SENSOR_ACCEL, sensor_timestamp, accel_values, 3);
332+
}
333+
334+
SDL_memcpy(ctx->last_state, data, SDL_min(size, sizeof(ctx->last_state)));
335+
}
336+
337+
static void HIDAPI_DriverZUIKI_Handle_EVOTOP_PCBT_StatePacket(SDL_Joystick *joystick, SDL_DriverZUIKI_Context *ctx, Uint8 *data, int size)
338+
{
339+
Sint16 axis;
340+
Uint64 timestamp = SDL_GetTicksNS();
341+
342+
axis = (Sint16)HIDAPI_RemapVal((float)(data[2] << 8 | data[1]), 0x0000, 0xffff, SDL_MIN_SINT16, SDL_MAX_SINT16);
343+
SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFTX, axis);
344+
axis = (Sint16)HIDAPI_RemapVal((float)(data[4] << 8 | data[3]), 0x0000, 0xffff, SDL_MIN_SINT16, SDL_MAX_SINT16);
345+
SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFTY, axis);
346+
axis = (Sint16)HIDAPI_RemapVal((float)(data[6] << 8 | data[5]), 0x0000, 0xffff, SDL_MIN_SINT16, SDL_MAX_SINT16);
347+
SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_RIGHTX, axis);
348+
axis = (Sint16)HIDAPI_RemapVal((float)(data[8] << 8 | data[7]), 0x0000, 0xffff, SDL_MIN_SINT16, SDL_MAX_SINT16);
349+
SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_RIGHTY, axis);
350+
351+
axis = (Sint16)HIDAPI_RemapVal((float)(data[10] << 8 | data[9]), 0x0000, 0x03ff, SDL_MIN_SINT16, SDL_MAX_SINT16);
352+
SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFT_TRIGGER, axis);
353+
axis = (Sint16)HIDAPI_RemapVal((float)(data[12] << 8 | data[11]), 0x0000, 0x03ff, SDL_MIN_SINT16, SDL_MAX_SINT16);
354+
SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, axis);
355+
356+
if (ctx->last_state[13] != data[13]) {
357+
Uint8 hat;
358+
switch (data[13]) {
359+
case 1:
360+
hat = SDL_HAT_UP;
361+
break;
362+
case 2:
363+
hat = SDL_HAT_RIGHTUP;
364+
break;
365+
case 3:
366+
hat = SDL_HAT_RIGHT;
367+
break;
368+
case 4:
369+
hat = SDL_HAT_RIGHTDOWN;
370+
break;
371+
case 5:
372+
hat = SDL_HAT_DOWN;
373+
break;
374+
case 6:
375+
hat = SDL_HAT_LEFTDOWN;
376+
break;
377+
case 7:
378+
hat = SDL_HAT_LEFT;
379+
break;
380+
case 8:
381+
hat = SDL_HAT_LEFTUP;
382+
break;
383+
default:
384+
hat = SDL_HAT_CENTERED;
385+
break;
386+
}
387+
SDL_SendJoystickHat(timestamp, joystick, 0, hat);
388+
}
389+
if (ctx->last_state[14] != data[14]) {
390+
SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SOUTH, ((data[14] & 0x01) != 0));
391+
SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_EAST, ((data[14] & 0x02) != 0));
392+
SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_WEST, ((data[14] & 0x08) != 0));
393+
SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_NORTH, ((data[14] & 0x10) != 0));
394+
SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, ((data[14] & 0x40) != 0));
395+
SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, ((data[14] & 0x80) != 0));
396+
}
397+
398+
if (ctx->last_state[15] != data[15]) {
399+
SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_BACK, ((data[15] & 0x04) != 0));
400+
SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_START, ((data[15] & 0x08) != 0));
401+
SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_GUIDE, ((data[15] & 0x10) != 0));
402+
SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_LEFT_STICK, ((data[15] & 0x20) != 0));
403+
SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_RIGHT_STICK, ((data[15] & 0x40) != 0));
404+
}
405+
406+
if (ctx->sensors_supported) {
407+
Uint64 sensor_timestamp = timestamp;
408+
float gyro_values[3];
409+
gyro_values[0] = median_filter_update(&ctx->filter_gyro_x, LOAD16(data[17], data[18]) * GYRO_SCALE);
410+
gyro_values[1] = median_filter_update(&ctx->filter_gyro_y, LOAD16(data[21], data[22]) * GYRO_SCALE);
411+
gyro_values[2] = median_filter_update(&ctx->filter_gyro_z, -LOAD16(data[19], data[20]) * GYRO_SCALE);
412+
SDL_SendJoystickSensor(timestamp, joystick, SDL_SENSOR_GYRO, sensor_timestamp, gyro_values, 3);
413+
float accel_values[3];
414+
accel_values[0] = LOAD16(data[23], data[24]) * ACCEL_SCALE;
415+
accel_values[2] = -LOAD16(data[25], data[26]) * ACCEL_SCALE;
416+
accel_values[1] = LOAD16(data[27], data[28]) * ACCEL_SCALE;
417+
SDL_SendJoystickSensor(timestamp, joystick, SDL_SENSOR_ACCEL, sensor_timestamp, accel_values, 3);
418+
#ifdef DEBUG_ZUIKI_PROTOCOL
419+
SDL_Log("Gyro raw: %d, %d, %d -> scaled: %.2f, %.2f, %.2f rad/s",
420+
LOAD16(data[17], data[18]), LOAD16(data[19], data[20]), LOAD16(data[21], data[22]),
421+
gyro_values[0], gyro_values[1], gyro_values[2]);
422+
SDL_Log("Accel raw: %d, %d, %d -> scaled: %.2f, %.2f, %.2f m/s²",
423+
LOAD16(data[23], data[24]), LOAD16(data[25], data[26]), LOAD16(data[27], data[28]),
424+
accel_values[0], accel_values[1], accel_values[2]);
425+
#endif
426+
}
427+
229428
SDL_memcpy(ctx->last_state, data, SDL_min(size, sizeof(ctx->last_state)));
230429
}
231430

@@ -250,7 +449,11 @@ static bool HIDAPI_DriverZUIKI_UpdateDevice(SDL_HIDAPI_Device *device)
250449
continue;
251450
}
252451

253-
if (size == 8) {
452+
if (device->product_id == USB_PRODUCT_ZUIKI_EVOTOP_PC_BT) {
453+
HIDAPI_DriverZUIKI_Handle_EVOTOP_PCBT_StatePacket(joystick, ctx, data, size);
454+
} else if (device->product_id == USB_PRODUCT_ZUIKI_EVOTOP_PC_DINPUT
455+
|| device->product_id == USB_PRODUCT_ZUIKI_MASCON_PRO
456+
|| device->product_id == USB_PRODUCT_ZUIKI_EVOTOP_UWB_DINPUT) {
254457
HIDAPI_DriverZUIKI_HandleOldStatePacket(joystick, ctx, data, size);
255458
}
256459
}

src/joystick/usb_ids.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,9 @@
194194
#define USB_PRODUCT_HANDHELDLEGEND_GCULTIMATE 0x10dd
195195
#define USB_PRODUCT_BONZIRICHANNEL_FIREBIRD 0x10e0
196196
#define USB_PRODUCT_ZUIKI_MASCON_PRO 0x0006
197+
#define USB_PRODUCT_ZUIKI_EVOTOP_UWB_DINPUT 0X001c
198+
#define USB_PRODUCT_ZUIKI_EVOTOP_PC_DINPUT 0X001d
199+
#define USB_PRODUCT_ZUIKI_EVOTOP_PC_BT 0X0017
197200
#define USB_PRODUCT_VOIDGAMING_PS4FIREBIRD 0x10e5
198201

199202
// USB usage pages

0 commit comments

Comments
 (0)