Compare commits
5 Commits
437e0c1f81
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 9d5989a5e3 | |||
| baf457901e | |||
| 8796d8c6bb | |||
| 1e403dcd4a | |||
| 592142cef2 |
+142
-78
@@ -41,25 +41,10 @@ static int mode = G29_MODE_MEDIA;
|
|||||||
module_param(mode, int, 0644);
|
module_param(mode, int, 0644);
|
||||||
MODULE_PARM_DESC(mode, "Initial mode (0=MEDIA, 1=WASD, 2=MOUSE)");
|
MODULE_PARM_DESC(mode, "Initial mode (0=MEDIA, 1=WASD, 2=MOUSE)");
|
||||||
|
|
||||||
/* Steering curve exponent (100 = linear, 200 = squared, 150 = ^1.5)
|
|
||||||
* Higher values reduce sensitivity at low steering angles.
|
|
||||||
*/
|
|
||||||
static int steer_curve = 100;
|
|
||||||
module_param(steer_curve, int, 0644);
|
|
||||||
MODULE_PARM_DESC(steer_curve, "Steering sensitivity curve (100=linear, 200=squared, default=200)");
|
|
||||||
|
|
||||||
static int steer_deadzone = 10;
|
static int steer_deadzone = 10;
|
||||||
module_param(steer_deadzone, int, 0644);
|
module_param(steer_deadzone, int, 0644);
|
||||||
MODULE_PARM_DESC(steer_deadzone, "Steering deadzone radius from center (default=10)");
|
MODULE_PARM_DESC(steer_deadzone, "Steering deadzone radius from center (default=10)");
|
||||||
|
|
||||||
static int gas_curve = 100;
|
|
||||||
module_param(gas_curve, int, 0644);
|
|
||||||
MODULE_PARM_DESC(gas_curve, "Gas pedal sensitivity curve (100=linear, 200=squared, default=200)");
|
|
||||||
|
|
||||||
static int clutch_curve = 100;
|
|
||||||
module_param(clutch_curve, int, 0644);
|
|
||||||
MODULE_PARM_DESC(clutch_curve, "Clutch pedal sensitivity curve (100=linear, 200=squared, default=200)");
|
|
||||||
|
|
||||||
#define NORMALIZATION_PRECISION 1000
|
#define NORMALIZATION_PRECISION 1000
|
||||||
|
|
||||||
struct g29_keymap {
|
struct g29_keymap {
|
||||||
@@ -81,10 +66,9 @@ static const struct g29_keymap media_mode_keymap[] = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static const struct g29_keymap mouse_mode_keymap[] = {
|
static const struct g29_keymap mouse_mode_keymap[] = {
|
||||||
{G29_BTN_X, BTN_LEFT},
|
{G29_BTN_L1, BTN_LEFT},
|
||||||
{G29_BTN_CIRCLE, BTN_RIGHT},
|
{G29_BTN_R1, BTN_RIGHT},
|
||||||
{G29_BTN_TRIANGLE, BTN_MIDDLE},
|
{G29_BTN_RETURN, BTN_MIDDLE},
|
||||||
{G29_BTN_SQUARE, BTN_SIDE},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct g29_dev {
|
struct g29_dev {
|
||||||
@@ -92,6 +76,7 @@ struct g29_dev {
|
|||||||
char phys[64];
|
char phys[64];
|
||||||
|
|
||||||
struct usb_device *udev;
|
struct usb_device *udev;
|
||||||
|
struct usb_interface *intf;
|
||||||
struct input_dev *input;
|
struct input_dev *input;
|
||||||
|
|
||||||
struct urb *urb;
|
struct urb *urb;
|
||||||
@@ -109,11 +94,111 @@ struct g29_dev {
|
|||||||
u32 gas_phase_accumulator;
|
u32 gas_phase_accumulator;
|
||||||
u32 clutch_phase_accumulator;
|
u32 clutch_phase_accumulator;
|
||||||
|
|
||||||
|
struct work_struct autocenter_work;
|
||||||
|
int out_ep_addr;
|
||||||
|
int out_ep_maxp;
|
||||||
|
int out_ep_interval;
|
||||||
|
|
||||||
enum g29_mode current_mode;
|
enum g29_mode current_mode;
|
||||||
struct g29_state last;
|
struct g29_state last;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
static void g29_find_int_out_ep(struct g29_dev *g29) {
|
||||||
|
const struct usb_host_interface *alts = g29->intf->cur_altsetting;
|
||||||
|
|
||||||
|
g29->out_ep_addr = 0;
|
||||||
|
g29->out_ep_maxp = 0;
|
||||||
|
g29->out_ep_interval = 0;
|
||||||
|
for (int i = 0; i < alts->desc.bNumEndpoints; i++) {
|
||||||
|
const struct usb_endpoint_descriptor *d = &alts->endpoint[i].desc;
|
||||||
|
if (!usb_endpoint_is_int_out(d))
|
||||||
|
continue;
|
||||||
|
g29->out_ep_addr = d->bEndpointAddress;
|
||||||
|
g29->out_ep_maxp = usb_endpoint_maxp(d);
|
||||||
|
g29->out_ep_interval = d->bInterval;
|
||||||
|
dev_info(&g29->intf->dev, "Found interrupt OUT ep=%02x maxp=%d interval=%d\n", g29->out_ep_addr, g29->out_ep_maxp, g29->out_ep_interval);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dev_info(&g29->intf->dev, "No interrupt OUT endpoint found on this interface (will use SET_REPORT)\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
static int g29_send_interrupt_out(struct g29_dev *g29, const u8 *buf, int len) {
|
||||||
|
int ret, actual = 0;
|
||||||
|
|
||||||
|
if (!g29->out_ep_addr)
|
||||||
|
return -ENODEV;
|
||||||
|
|
||||||
|
ret = usb_interrupt_msg(g29->udev,
|
||||||
|
usb_sndintpipe(g29->udev, g29->out_ep_addr),
|
||||||
|
(void *) buf, len, &actual, 1000);
|
||||||
|
if (ret < 0)
|
||||||
|
return ret;
|
||||||
|
if (actual != len)
|
||||||
|
return -EIO;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int g29_send_set_report(const struct g29_dev *g29, const int ifnum, const int report_id, const u8 *buf, const int len) {
|
||||||
|
const u16 wValue = ((u16) 0x02 << 8) | (report_id & 0xff);
|
||||||
|
const int ret = usb_control_msg(g29->udev, usb_sndctrlpipe(g29->udev, 0), 0x09,
|
||||||
|
USB_TYPE_CLASS | USB_RECIP_INTERFACE | USB_DIR_OUT,
|
||||||
|
wValue, ifnum, (void *) buf, len, USB_CTRL_SET_TIMEOUT);
|
||||||
|
if (ret < 0)
|
||||||
|
return ret;
|
||||||
|
if (ret != len)
|
||||||
|
return -EIO;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int g29_send_cmd7(struct g29_dev *g29, const u8 cmd0, const u8 cmd1, const u8 cmd2, const u8 cmd3, const u8 cmd4, const u8 cmd5, const u8 cmd6) {
|
||||||
|
const u8 cmd[] = {cmd0, cmd1, cmd2, cmd3, cmd4, cmd5, cmd6};
|
||||||
|
int ret;
|
||||||
|
if ((ret = g29_send_interrupt_out(g29, cmd, 7)) == 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
const int ifnum = 0, report_id = 0;
|
||||||
|
if ((ret = g29_send_set_report(g29, ifnum, report_id, cmd, 7)) < 0) {
|
||||||
|
dev_warn(&g29->intf->dev,
|
||||||
|
"autocenter cmd %02x %02x %02x %02x %02x %02x %02x failed: %d (ifnum=%d report_id=%d out_ep=%02x)\n",
|
||||||
|
cmd[0], cmd[1], cmd[2], cmd[3], cmd[4], cmd[5], cmd[6],
|
||||||
|
ret, ifnum, report_id, g29->out_ep_addr);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void g29_set_autocenter_default(struct g29_dev *g29, const u16 magnitude) {
|
||||||
|
if (magnitude == 0) {
|
||||||
|
g29_send_cmd7(g29, 0xf5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 expand_a, expand_b;
|
||||||
|
if (magnitude <= 0xaaaa) {
|
||||||
|
expand_a = 0x0c * magnitude;
|
||||||
|
expand_b = 0x80 * magnitude;
|
||||||
|
} else {
|
||||||
|
expand_a = (0x0c * 0xaaaa) + 0x06 * (magnitude - 0xaaaa);
|
||||||
|
expand_b = (0x80 * 0xaaaa) + 0xff * (magnitude - 0xaaaa);
|
||||||
|
}
|
||||||
|
expand_a >>= 1;
|
||||||
|
|
||||||
|
if (g29_send_cmd7(g29, 0xfe, 0x0d, expand_a / 0xaaaa, expand_a / 0xaaaa, expand_b / 0xaaaa, 0x00, 0x00) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
g29_send_cmd7(g29, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void g29_autocenter_work_fn(struct work_struct *work) {
|
||||||
|
struct g29_dev *g29 = container_of(work, struct g29_dev, autocenter_work);
|
||||||
|
const u16 mag = 0xFFFF;
|
||||||
|
if (mag == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
g29_set_autocenter_default(g29, mag);
|
||||||
|
}
|
||||||
|
|
||||||
static void g29_switch_mode(struct g29_dev *g29, enum g29_mode new_mode) {
|
static void g29_switch_mode(struct g29_dev *g29, enum g29_mode new_mode) {
|
||||||
if (g29->current_mode == new_mode)
|
if (g29->current_mode == new_mode)
|
||||||
return;
|
return;
|
||||||
@@ -148,63 +233,20 @@ static void mouse_mode_timer_fn(struct timer_list *t) {
|
|||||||
struct g29_dev *g29 = timer_container_of(g29, t, mouse_timer);
|
struct g29_dev *g29 = timer_container_of(g29, t, mouse_timer);
|
||||||
|
|
||||||
const int rot = le16_to_cpu(g29->last.rot_le);
|
const int rot = le16_to_cpu(g29->last.rot_le);
|
||||||
int gas_pressure = G29_PEDAL_RELEASED - g29->last.gas;
|
const int up = G29_PEDAL_RELEASED - g29->last.gas;
|
||||||
int clutch_pressure = G29_PEDAL_RELEASED - g29->last.clt;
|
const int down = G29_PEDAL_RELEASED - g29->last.clt;
|
||||||
|
const int left = rot < 0x8000 ? WHEEL_CENTER - rot : 0;
|
||||||
|
const int right = rot > 0x8000 ? rot - WHEEL_CENTER : 0;
|
||||||
|
|
||||||
/* Calculate speed: positive for forward (gas), negative for backward (clutch) */
|
const int dx = (right - left) / 0x400;
|
||||||
int speed = gas_pressure - clutch_pressure;
|
const int dy = (down - up) / 0x20;
|
||||||
|
|
||||||
/* Apply deadzone to steering */
|
if (dx != 0 || dy != 0) {
|
||||||
int effective_rot = rot;
|
input_report_rel(g29->input, REL_X, dx);
|
||||||
if (abs(rot - WHEEL_CENTER) <= steer_deadzone) {
|
input_report_rel(g29->input, REL_Y, dy);
|
||||||
effective_rot = WHEEL_CENTER;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Calculate angle from wheel rotation
|
|
||||||
* Map wheel rotation to angle:
|
|
||||||
* - Center (32768) = 0° (straight forward)
|
|
||||||
* - Full left (0) = -180° (reverse)
|
|
||||||
* - Full right (65535) = +180° (reverse)
|
|
||||||
* We normalize to -1000 to +1000 representing -π to +π radians
|
|
||||||
*/
|
|
||||||
int angle_normalized = ((effective_rot - WHEEL_CENTER) * 1000) / WHEEL_CENTER;
|
|
||||||
|
|
||||||
/* Clamp angle to prevent overflow */
|
|
||||||
if (angle_normalized > 1000) angle_normalized = 1000;
|
|
||||||
if (angle_normalized < -1000) angle_normalized = -1000;
|
|
||||||
|
|
||||||
/* Calculate movement components using better trigonometric approximations:
|
|
||||||
* dx = sin(angle) * speed
|
|
||||||
* dy = cos(angle) * speed
|
|
||||||
*
|
|
||||||
* sin(x) ≈ x - x³/6 (Taylor series)
|
|
||||||
* cos(x) ≈ 1 - x²/2 + x⁴/24 (Taylor series)
|
|
||||||
*
|
|
||||||
* For angle_normalized in [-1000, 1000] representing [-π, +π]:
|
|
||||||
* This gives us full reverse when fully steered
|
|
||||||
*/
|
|
||||||
long angle_cubed = ((long) angle_normalized * angle_normalized * angle_normalized) / 1000000;
|
|
||||||
int sin_approx = (angle_normalized * 1000 - angle_cubed / 6) / 1000;
|
|
||||||
|
|
||||||
long angle_squared = ((long) angle_normalized * angle_normalized) / 1000;
|
|
||||||
long angle_fourth = (angle_squared * angle_squared) / 1000;
|
|
||||||
int cos_approx = 1000 - angle_squared / 2 + angle_fourth / 24;
|
|
||||||
|
|
||||||
int dx = (sin_approx * speed) / 1000;
|
|
||||||
int dy = -(cos_approx * speed) / 1000; /* Negative because forward is -Y */
|
|
||||||
|
|
||||||
/* Scale down the movement for reasonable mouse speed */
|
|
||||||
int scaled_dx = dx / 50;
|
|
||||||
int scaled_dy = dy / 50;
|
|
||||||
|
|
||||||
/* Report mouse movement if there's any */
|
|
||||||
if (scaled_dx != 0 || scaled_dy != 0) {
|
|
||||||
input_report_rel(g29->input, REL_X, scaled_dx);
|
|
||||||
input_report_rel(g29->input, REL_Y, scaled_dy);
|
|
||||||
input_sync(g29->input);
|
input_sync(g29->input);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Reschedule timer if still in mouse mode */
|
|
||||||
if (g29->current_mode == G29_MODE_MOUSE)
|
if (g29->current_mode == G29_MODE_MOUSE)
|
||||||
mod_timer(&g29->mouse_timer, jiffies + msecs_to_jiffies(10));
|
mod_timer(&g29->mouse_timer, jiffies + msecs_to_jiffies(10));
|
||||||
}
|
}
|
||||||
@@ -214,24 +256,24 @@ static void wasd_mode_timer_fn(struct timer_list *t) {
|
|||||||
|
|
||||||
const int period = 50;
|
const int period = 50;
|
||||||
const int rot = le16_to_cpu(g29->last.rot_le);
|
const int rot = le16_to_cpu(g29->last.rot_le);
|
||||||
const int gas = 0xFF - g29->last.gas;
|
const int gas = G29_PEDAL_RELEASED - g29->last.gas;
|
||||||
const int brk = 0xFF - g29->last.brk;
|
const int brk = G29_PEDAL_RELEASED - g29->last.brk;
|
||||||
|
|
||||||
const int gas_duty = gas * period / 0x40;
|
const int gas_duty = gas * period / 0x40;
|
||||||
input_report_key(g29->input, KEY_W, g29->gas_phase_accumulator < gas_duty);
|
input_report_key(g29->input, KEY_W, g29->gas_phase_accumulator < gas_duty);
|
||||||
g29->gas_phase_accumulator++;
|
g29->gas_phase_accumulator++;
|
||||||
g29->gas_phase_accumulator %= period;
|
g29->gas_phase_accumulator %= period;
|
||||||
|
|
||||||
input_report_key(g29->input, KEY_S, brk >= 0x80);
|
input_report_key(g29->input, KEY_S, brk >= G29_PEDAL_THRESHOLD);
|
||||||
|
|
||||||
if (rot >= 0x7ff8 && rot <= 0x8008){
|
if (rot >= 0x7ff8 && rot <= 0x8008) {
|
||||||
input_report_key(g29->input, KEY_A, 0);
|
input_report_key(g29->input, KEY_A, 0);
|
||||||
input_report_key(g29->input, KEY_D, 0);
|
input_report_key(g29->input, KEY_D, 0);
|
||||||
} else {
|
} else {
|
||||||
const int mag = rot < 0x8000 ? 0x8000 - rot : rot - 0x8000;
|
const int mag = rot < WHEEL_CENTER ? WHEEL_CENTER - rot : rot - WHEEL_CENTER;
|
||||||
const int duty = mag * period / 0x3000;
|
const int duty = mag * period / 0x3000;
|
||||||
const int k1 = rot > 0x8000 ? KEY_D : KEY_A;
|
const int k1 = rot > WHEEL_CENTER ? KEY_D : KEY_A;
|
||||||
const int k2 = rot > 0x8000 ? KEY_A : KEY_D;
|
const int k2 = rot > WHEEL_CENTER ? KEY_A : KEY_D;
|
||||||
input_report_key(g29->input, k2, 0);
|
input_report_key(g29->input, k2, 0);
|
||||||
input_report_key(g29->input, k1, g29->phase_accumulator < duty);
|
input_report_key(g29->input, k1, g29->phase_accumulator < duty);
|
||||||
}
|
}
|
||||||
@@ -257,8 +299,20 @@ static void process_media_mode(const struct g29_dev *g29, const struct g29_state
|
|||||||
static void process_wasd_mode(const struct g29_dev *g29, const struct g29_state *cur, const struct g29_state *prev) {
|
static void process_wasd_mode(const struct g29_dev *g29, const struct g29_state *cur, const struct g29_state *prev) {
|
||||||
// WASD mode is handled by the timer function (g29_steer_timer_fn)
|
// WASD mode is handled by the timer function (g29_steer_timer_fn)
|
||||||
const u32 buttons = le32_to_cpu(cur->buttons_le);
|
const u32 buttons = le32_to_cpu(cur->buttons_le);
|
||||||
|
|
||||||
input_report_key(g29->input, KEY_C, !!(buttons & G29_BTN_L1));
|
input_report_key(g29->input, KEY_C, !!(buttons & G29_BTN_L1));
|
||||||
input_report_key(g29->input, KEY_SPACE, !!(buttons & G29_BTN_R1));
|
input_report_key(g29->input, KEY_SPACE, !!(buttons & G29_BTN_R1));
|
||||||
|
input_report_key(g29->input, KEY_ENTER, !!(buttons & G29_BTN_RETURN));
|
||||||
|
input_report_key(g29->input, KEY_M, !!(buttons & G29_BTN_CIRCLE));
|
||||||
|
|
||||||
|
if (buttons & G29_BTN_RED_CW) {
|
||||||
|
input_report_rel(g29->input, REL_WHEEL, 1);
|
||||||
|
} else if (buttons & G29_BTN_RED_CCW) {
|
||||||
|
input_report_rel(g29->input, REL_WHEEL, -1);
|
||||||
|
} else {
|
||||||
|
input_report_rel(g29->input, REL_WHEEL, 0);
|
||||||
|
}
|
||||||
|
|
||||||
input_sync(g29->input);
|
input_sync(g29->input);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,6 +393,7 @@ static int g29_input_open(struct input_dev *input) {
|
|||||||
return -EIO;
|
return -EIO;
|
||||||
|
|
||||||
g29_switch_mode(g29, mode);
|
g29_switch_mode(g29, mode);
|
||||||
|
schedule_work(&g29->autocenter_work);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -351,6 +406,7 @@ static void g29_input_close(struct input_dev *input) {
|
|||||||
if (g29->current_mode == G29_MODE_MOUSE)
|
if (g29->current_mode == G29_MODE_MOUSE)
|
||||||
timer_delete_sync(&g29->mouse_timer);
|
timer_delete_sync(&g29->mouse_timer);
|
||||||
|
|
||||||
|
cancel_work_sync(&g29->autocenter_work);
|
||||||
usb_kill_urb(g29->urb);
|
usb_kill_urb(g29->urb);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,6 +440,7 @@ static int g29_probe(struct usb_interface *intf, const struct usb_device_id *id)
|
|||||||
}
|
}
|
||||||
|
|
||||||
g29->udev = udev;
|
g29->udev = udev;
|
||||||
|
g29->intf = intf;
|
||||||
g29->input = input;
|
g29->input = input;
|
||||||
|
|
||||||
g29->endpoint = usb_endpoint_num(ep);
|
g29->endpoint = usb_endpoint_num(ep);
|
||||||
@@ -426,6 +483,9 @@ static int g29_probe(struct usb_interface *intf, const struct usb_device_id *id)
|
|||||||
usb_to_input_id(udev, &input->id);
|
usb_to_input_id(udev, &input->id);
|
||||||
input->dev.parent = &intf->dev;
|
input->dev.parent = &intf->dev;
|
||||||
|
|
||||||
|
g29_find_int_out_ep(g29);
|
||||||
|
INIT_WORK(&g29->autocenter_work, g29_autocenter_work_fn);
|
||||||
|
|
||||||
__set_bit(EV_KEY, input->evbit);
|
__set_bit(EV_KEY, input->evbit);
|
||||||
__set_bit(EV_REL, input->evbit);
|
__set_bit(EV_REL, input->evbit);
|
||||||
|
|
||||||
@@ -443,6 +503,8 @@ static int g29_probe(struct usb_interface *intf, const struct usb_device_id *id)
|
|||||||
input_set_capability(input, EV_KEY, KEY_D);
|
input_set_capability(input, EV_KEY, KEY_D);
|
||||||
input_set_capability(input, EV_KEY, KEY_C);
|
input_set_capability(input, EV_KEY, KEY_C);
|
||||||
input_set_capability(input, EV_KEY, KEY_SPACE);
|
input_set_capability(input, EV_KEY, KEY_SPACE);
|
||||||
|
input_set_capability(input, EV_KEY, KEY_ENTER);
|
||||||
|
input_set_capability(input, EV_KEY, KEY_M);
|
||||||
|
|
||||||
// Mouse mode capabilities
|
// Mouse mode capabilities
|
||||||
input_set_capability(input, EV_KEY, BTN_LEFT);
|
input_set_capability(input, EV_KEY, BTN_LEFT);
|
||||||
@@ -489,6 +551,8 @@ static void g29_disconnect(struct usb_interface *intf) {
|
|||||||
struct g29_dev *g29 = usb_get_intfdata(intf);
|
struct g29_dev *g29 = usb_get_intfdata(intf);
|
||||||
usb_set_intfdata(intf, NULL);
|
usb_set_intfdata(intf, NULL);
|
||||||
if (!g29) return;
|
if (!g29) return;
|
||||||
|
timer_delete_sync(&g29->steer_timer);
|
||||||
|
cancel_work_sync(&g29->autocenter_work);
|
||||||
usb_kill_urb(g29->urb);
|
usb_kill_urb(g29->urb);
|
||||||
input_unregister_device(g29->input);
|
input_unregister_device(g29->input);
|
||||||
usb_free_urb(g29->urb);
|
usb_free_urb(g29->urb);
|
||||||
|
|||||||
@@ -60,7 +60,6 @@
|
|||||||
#define G29_GEARSHIFT_Z_PRESSED 0xDC /* Pressed down */
|
#define G29_GEARSHIFT_Z_PRESSED 0xDC /* Pressed down */
|
||||||
|
|
||||||
#define WHEEL_CENTER 0x8000
|
#define WHEEL_CENTER 0x8000
|
||||||
#define WHEEL_MAX_DIST 0x8000
|
|
||||||
|
|
||||||
struct g29_state {
|
struct g29_state {
|
||||||
u32 buttons_le; /* Button bitmask (little-endian) */
|
u32 buttons_le; /* Button bitmask (little-endian) */
|
||||||
|
|||||||
Reference in New Issue
Block a user