本文介绍在 Linux 下 Logitech G29 套件的编程接入,包括摇杆数据读取、自动归中配置以及 LED 控制。

Linux 内核对于摇杆输入处理分几个部分,其中数据读取是 Kernel Joystick API, 设备是 /dev/input/jsX,对于 G29 而言,可以通过 /dev/input/by-id 来获取节点名,通常为 /dev/input/by-id/usb-Logitech_G29_Driving_Force_Racing_Wheel-joystick

而反馈,是 Kernel Force feeback API,设备是 /dev/input/eventX,也可以通过 /dev/input/by-id 来获取节点名。

这两部分参考内核文档即可读取传感器数据和设置反馈归中等效果。G29上还带了一个 LED 带,通常用于游戏中的视觉特效,比如发动机转速等。这个 LEDs 在内核中不属于通用设备,没有特定 API 支持,映射的设备文件为 /dev/hidrawX,可以通过 hidraw 与之通信,来控制 LEDs 状态,这部分可以参考 Logitech G29 Shifter LEDs. 需要注意的是,需要设定 udev 规则来配置该 hidrawX 的写入权限,因为默认该设备所属用户组均为 root

完整样例代码如下,请注意需要将 G29 配置成 PS3 模式(G29在不同配置模式下,生成的 USB ID不同)。需要内核支持 Logitech 驱动,一般桌面 Linux 发行版已经打开了对应编译开关。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
#include <stdio.h>
#include <linux/joystick.h>
#include <linux/input.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/select.h>
#include <stdlib.h>

#include <libudev.h>


static sig_atomic_t m_signal = 0;
static void signal_cb(int sig)
{
    m_signal = sig;
    signal(sig, signal_cb);
}

static unsigned char LED_ON[] =   { 0xf8, 0x12, 0x1f, 0x00, 0x00, 0x00, 0x01 };
static unsigned char LED_OFF[] =  { 0xf8, 0x12, 0x00, 0x00, 0x00, 0x00, 0x01 };


static int open_g29_led()
{
    struct udev* udev = udev_new();
    if (!udev) {
        printf("udev_new() failed\n");
        return -1;
    }
    int hid = -1;
    struct udev_enumerate *enumerate = udev_enumerate_new(udev);
    udev_enumerate_add_match_subsystem(enumerate, "hidraw");
    udev_enumerate_scan_devices(enumerate);

    struct udev_list_entry *devices = udev_enumerate_get_list_entry(enumerate);
    struct udev_list_entry *entry;
    udev_list_entry_foreach(entry, devices) {
        const char *path = udev_list_entry_get_name(entry);
        struct udev_device *dev = udev_device_new_from_syspath(udev, path);
        if (dev) {
            struct udev_device *hid_dev =
                udev_device_get_parent_with_subsystem_devtype(dev, "usb", "usb_device");
            if (hid_dev) {
                const char *node = udev_device_get_devnode(dev); // devnode should be hidraw, not usb
                const char *vendor = udev_device_get_sysattr_value(hid_dev, "idVendor");
                const char *product = udev_device_get_sysattr_value(hid_dev, "idProduct");
                if (node && vendor && product) {
                    printf("%s\t%s:%s\n", node, vendor, product);
                    if (strtol(vendor, NULL, 16) == 0x046d && strtol(product, NULL, 16) == 0xc24f) {
                        printf("g29 led hidraw path: %s\n", node);
                        hid = open(node, O_WRONLY);
                        if (hid == -1) {
                            perror("open() hidraw");
                        }
                        break;
                    }
                }
            }
            udev_device_unref(dev);
        }
    }

    udev_enumerate_unref(enumerate);
    udev_unref(udev);
    return hid;
}

int main(int argc, char **argv)
{
    signal(SIGTERM, signal_cb);
    signal(SIGINT, signal_cb);
    char *input = "/dev/input/by-id/usb-Logitech_G29_Driving_Force_Racing_Wheel-joystick";
    char *event = "/dev/input/by-id/usb-Logitech_G29_Driving_Force_Racing_Wheel-event-joystick";
    if (argc > 1) {
        input = argv[1];
    }
    if (argc > 2) {
        event = argv[2];
    }
    printf("device: %s\n", input);
    printf("event: %s\n", event);
    open_g29_led();
    int fd = open(input, O_RDONLY);
    if (fd == -1) {
        perror("open");
        return -1;
    }
    int ev = open(event, O_RDWR);
    if (ev == -1) {
        close(fd);
        perror("open");
        return -1;
    }

    int autocenter = 65;
    struct input_event ie;
    ie.type = EV_FF;
    ie.code = FF_AUTOCENTER;
    ie.value = 0xFFFFUL * autocenter / 100;
    if (write(ev, &ie, sizeof(ie)) == -1) {
        perror("set auto-center");
    }

    int led = open_g29_led();
    if (led == -1) {
        printf("open led failed, ignore LEDs\n");
    }
    if (led != -1) {
        write(led, LED_ON, sizeof(LED_ON));
    }
    puts("Hello, world!");
    sleep(1);

    while (m_signal == 0) {
        fd_set fds;
        FD_ZERO(&fds);
        FD_SET(fd, &fds);
        struct timeval tv;
        tv.tv_sec = 0;
        tv.tv_usec = 50000;
        int rv = select(fd + 1, &fds, NULL, NULL, &tv);
        if (rv == -1) {
            perror("select");
            break;
        }
        if (rv == 0) {
            continue;
        }

        struct js_event e;
        int r = read(fd, &e, sizeof(e));
        if (r == -1) {
            perror("read");
            break;
        } else if (r == 0) {
            printf("!!!\n");
        } else {
            int init = (e.type & JS_EVENT_INIT) != 0;
            e.type &= ~JS_EVENT_INIT;
            printf("%.3lf %d\t%d\t%d\t%d\n", e.time * 1E-3, init,  e.type, e.number, e.value);
        }
    }
    printf("signal : %s\n", strsignal(m_signal));

    if (led != -1) {
        write(led, LED_OFF, sizeof(LED_OFF));
        close(led);
    }

    ie.type = EV_FF;
    ie.code = FF_AUTOCENTER;
    ie.value = 0xFFFFUL * 0 / 100;
    if (write(ev, &ie, sizeof(ie)) == -1) {
        perror("set auto-center");
    }

    close(fd);
    close(ev);
    return 0;
}

备注,对于 LED 控制权限问题,可以通过 udev rule 来配置:

1
ACTION=="add",SUBSYSTEMS=="usb",ATTRS{idVendor}=="046d",ATTRS{idProduct}=="c24f",GROUP="users",MODE="0666"