Skip to content

Commit 81f10c4

Browse files
committed
selkies_gstreamer support for evdev joystick interposer
1 parent fbc49d4 commit 81f10c4

File tree

4 files changed

+161
-58
lines changed

4 files changed

+161
-58
lines changed

addons/js-interposer/Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ deps:
77
@sudo apt-get install -y build-essential evtest strace joystick libsdl2-dev gcc-multilib libevdev-dev
88

99
build:
10-
gcc -shared -fPIC -o selkies_joystick_interposer.so joystick_interposer.c -ldl
10+
gcc -shared -fPIC -o selkies_joystick_interposer.so joystick_interposer.c -ldl -Wall
1111

1212
build32:
13-
gcc -m32 -shared -fPIC -o selkies_joystick_interposer_i386.so joystick_interposer.c -ldl
13+
gcc -m32 -shared -fPIC -o selkies_joystick_interposer_i386.so joystick_interposer.c -ldl -Wall
1414

1515
install: build
1616
mkdir -p $(PREFIX)/lib/$(gcc -print-multiarch | sed -e 's/i.*86/i386/')

addons/js-interposer/joystick_interposer.c

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/.
1414
The ioctl() SYSCALL is interposed to fake the behavior of a input event character device.
1515
These ioctl requests were mostly reverse engineered from the joystick.h source and using the jstest command to test.
1616
17-
Note that some applications list the /dev/input/* directory to discover JS devices, to solve for this, create empty files at the following paths:
17+
Note that some applications list the /dev/input/ directory to discover JS devices, to solve for this, create empty files at the following paths:
1818
sudo mkdir -pm1777 /dev/input
1919
sudo touch /dev/input/{js0,js1,js2,js3,event1000,event1001,event1002,event1003}
2020
sudo chmod 777 /dev/input/js* /dev/input/event*
@@ -487,8 +487,8 @@ int intercept_ev_ioctl(js_interposer_t *interposer, int fd, unsigned long reques
487487
va_end(args);
488488

489489
struct input_absinfo *absinfo;
490+
struct input_id *id;
490491
int fake_version = 0x010100;
491-
char name[256] = "JS Interposer";
492492
int len;
493493

494494
/* The EVIOCGABS(key) is a request to get the calibration values for the ABS type axes.
@@ -583,7 +583,7 @@ int intercept_ev_ioctl(js_interposer_t *interposer, int fd, unsigned long reques
583583
return 0;
584584

585585
case 0x02: /* Handle EVIOCGID: device ID request. */
586-
struct input_id *id = (struct input_id *)arg;
586+
id = (struct input_id *)arg;
587587
// Populate the fake input_id for a joystick device
588588
id->bustype = BUS_VIRTUAL; // Example bus type (Virtual)
589589
id->vendor = 0x045E; // Fake vendor ID (e.g., Microsoft)

src/selkies_gstreamer/gamepad.py

Lines changed: 146 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -116,42 +116,6 @@
116116
ABS_MIN = -32767
117117
ABS_MAX = 32767
118118

119-
# Joystick event struct
120-
# https://www.kernel.org/doc/Documentation/input/joystick-api.txt
121-
# struct js_event {
122-
# __u32 time; /* event timestamp in milliseconds */
123-
# __s16 value; /* value */
124-
# __u8 type; /* event type */
125-
# __u8 number; /* axis/button number */
126-
# };
127-
128-
def get_btn_event(btn_num, btn_val):
129-
ts = int((time.time() * 1000) % 1000000000)
130-
131-
# see js_event struct definition above.
132-
# https://docs.python.org/3/library/struct.html
133-
struct_format = 'IhBB'
134-
event = struct.pack(struct_format, ts, btn_val,
135-
JS_EVENT_BUTTON, btn_num)
136-
137-
logger.debug(struct.unpack(struct_format, event))
138-
139-
return event
140-
141-
142-
def get_axis_event(axis_num, axis_val):
143-
ts = int((time.time() * 1000) % 1000000000)
144-
145-
# see js_event struct definition above.
146-
# https://docs.python.org/3/library/struct.html
147-
struct_format = 'IhBB'
148-
event = struct.pack(struct_format, ts, axis_val,
149-
JS_EVENT_AXIS, axis_num)
150-
151-
logger.debug(struct.unpack(struct_format, event))
152-
153-
return event
154-
155119
def detect_gamepad_config(name):
156120
# TODO switch mapping based on name.
157121
return STANDARD_XPAD_CONFIG
@@ -173,8 +137,8 @@ def normalize_axis_val(val):
173137
def normalize_trigger_val(val):
174138
return round(val * (ABS_MAX - ABS_MIN)) + ABS_MIN
175139

176-
class SelkiesGamepad:
177-
def __init__(self, socket_path, loop):
140+
class SelkiesGamepadBase:
141+
def __init__(self, socket_path, loop, gamepad_mapper_class):
178142
self.socket_path = socket_path
179143
self.loop = loop
180144

@@ -197,11 +161,17 @@ def __init__(self, socket_path, loop):
197161

198162
# flag indicating that loop is running.
199163
self.running = False
164+
165+
# class used for mapping gamepad events
166+
self.gamepad_mapper_class = gamepad_mapper_class
167+
168+
def get_gamepad_mapper(self, config, name, num_btns, num_axes):
169+
raise Exception("get_gamepad_mapper must be overriden")
200170

201171
def set_config(self, name, num_btns, num_axes):
202172
self.name = name
203173
self.config = detect_gamepad_config(name)
204-
self.mapper = GamepadMapper(self.config, name, num_btns, num_axes)
174+
self.mapper = self.gamepad_mapper_class(self.config, name, num_btns, num_axes)
205175

206176
def __make_config(self):
207177
'''
@@ -238,7 +208,7 @@ async def __send_events(self):
238208
await asyncio.sleep(0.001)
239209
continue
240210
while self.running and not self.events.empty():
241-
await self.send_event(self.events.get())
211+
await self.__send_event(self.events.get())
242212

243213
def send_btn(self, btn_num, btn_val):
244214
if not self.mapper:
@@ -256,7 +226,7 @@ def send_axis(self, axis_num, axis_val):
256226
if event is not None:
257227
self.events.put(event)
258228

259-
async def send_event(self, event):
229+
async def __send_event(self, event):
260230
if len(self.clients) < 1:
261231
return
262232

@@ -274,7 +244,7 @@ async def send_event(self, event):
274244
for fd in closed_clients:
275245
del self.clients[fd]
276246

277-
async def setup_client(self, client):
247+
async def __setup_client(self, client):
278248
logger.info("Sending config to client with fd: %d" % client.fileno())
279249
try:
280250
config_data = self.__make_config()
@@ -321,7 +291,7 @@ async def run_server(self):
321291
logger.info("Client connected with fd: %d" % fd)
322292

323293
# Send client the joystick configuration
324-
await self.setup_client(client)
294+
await self.__setup_client(client)
325295

326296
# Add client to dictionary to receive events.
327297
self.clients[fd] = client
@@ -342,13 +312,48 @@ def stop_server(self):
342312
except:
343313
pass
344314

345-
class GamepadMapper:
315+
class SelkiesGamepad:
316+
def __init__(self, js_socket_path, ev_socket_path, loop):
317+
self.js_socket_path = js_socket_path
318+
self.ev_socket_path = ev_socket_path
319+
self.loop = loop
320+
321+
self.js_gamepad = SelkiesJSGamepad(js_socket_path, loop)
322+
self.ev_gamepad = SelkiesEVGamepad(ev_socket_path, loop)
323+
324+
def set_config(self, name, num_btns, num_axes):
325+
self.js_gamepad.set_config(name, num_btns, num_axes)
326+
self.ev_gamepad.set_config(name, num_btns, num_axes)
327+
328+
def send_btn(self, btn_num, btn_val):
329+
self.js_gamepad.send_btn(btn_num, btn_val)
330+
self.ev_gamepad.send_btn(btn_num, btn_val)
331+
332+
def send_axis(self, axis_num, axis_val):
333+
self.js_gamepad.send_axis(axis_num, axis_val)
334+
self.ev_gamepad.send_axis(axis_num, axis_val)
335+
336+
def run_server(self):
337+
asyncio.ensure_future(self.js_gamepad.run_server(), loop=self.loop)
338+
asyncio.ensure_future(self.ev_gamepad.run_server(), loop=self.loop)
339+
340+
def stop_server(self):
341+
self.js_gamepad.run_server()
342+
self.ev_gamepad.run_server()
343+
344+
class GamepadMapperBase:
346345
def __init__(self, config, name, num_btns, num_axes):
347346
self.config = config
348347
self.input_name = name
349348
self.input_num_btns = num_btns
350349
self.input_num_axes = num_axes
351-
350+
351+
def get_btn_event(self, btn_num, btn_val):
352+
raise Exception("get_btn_event not implemented")
353+
354+
def get_axis_event(self, axis_num, axis_val):
355+
raise Exception("get_axis_event not implemented")
356+
352357
def get_mapped_btn(self, btn_num, btn_val):
353358
'''
354359
return either a button or axis event based on mapping.
@@ -373,7 +378,7 @@ def get_mapped_btn(self, btn_num, btn_val):
373378
# Normalize to full range for input between 0 and 1.
374379
axis_val = normalize_trigger_val(btn_val)
375380

376-
return get_axis_event(axis_num, axis_val)
381+
return self.get_axis_event(axis_num, axis_val)
377382

378383
# Perform button mapping.
379384
mapped_btn = self.config["mapping"]["btns"].get(btn_num, btn_num)
@@ -382,7 +387,7 @@ def get_mapped_btn(self, btn_num, btn_val):
382387
mapped_btn, len(self.config["btn_map"]) - 1))
383388
return None
384389

385-
return get_btn_event(mapped_btn, int(btn_val))
390+
return self.get_btn_event(mapped_btn, int(btn_val))
386391

387392
def get_mapped_axis(self, axis_num, axis_val):
388393
mapped_axis = self.config["mapping"]["axes"].get(axis_num, axis_num)
@@ -392,4 +397,97 @@ def get_mapped_axis(self, axis_num, axis_val):
392397
return None
393398

394399
# Normalize axis value to be within range.
395-
return get_axis_event(mapped_axis, normalize_axis_val(axis_val))
400+
return self.get_axis_event(mapped_axis, normalize_axis_val(axis_val))
401+
402+
403+
class JSGamepadMapper(GamepadMapperBase):
404+
def __init__(self, config, name, num_btns, num_axes):
405+
super().__init__(config, name, num_btns, num_axes)
406+
407+
# https://www.kernel.org/doc/Documentation/input/joystick-api.txt
408+
# struct js_event {
409+
# __u32 time; /* event timestamp in milliseconds */
410+
# __s16 value; /* value */
411+
# __u8 type; /* event type */
412+
# __u8 number; /* axis/button number */
413+
# };
414+
self.struct_format = 'IhBB'
415+
416+
def get_btn_event(self, btn_num, btn_val):
417+
ts = int((time.time() * 1000) % 1000000000)
418+
419+
event = struct.pack(self.struct_format, ts, btn_val,
420+
JS_EVENT_BUTTON, btn_num)
421+
422+
logger.debug(struct.unpack(self.struct_format, event))
423+
424+
return event
425+
426+
def get_axis_event(self, axis_num, axis_val):
427+
ts = int((time.time() * 1000) % 1000000000)
428+
429+
event = struct.pack(self.struct_format, ts, axis_val,
430+
JS_EVENT_AXIS, axis_num)
431+
432+
logger.debug(struct.unpack(self.struct_format, event))
433+
434+
return event
435+
436+
class EVGamepadMapper(GamepadMapperBase):
437+
def __init__(self, config, name, num_btns, num_axes):
438+
super().__init__(config, name, num_btns, num_axes)
439+
440+
# https://www.kernel.org/doc/Documentation/input/joystick-api.txt
441+
# struct input_event {
442+
# struct timeval time;
443+
# unsigned short type;
444+
# unsigned short code;
445+
# unsigned int value;
446+
# };
447+
448+
# Double the input_event to include sycn, EV_SYN event.
449+
self.struct_format = 'llHHillHHi'
450+
451+
def get_btn_event(self, btn_num, btn_val):
452+
now = time.time()
453+
ts_sec = int(now)
454+
ts_usec = int((now *1e6) % 1e6)
455+
456+
# evdev expects ev key codes, not button numbers
457+
ev_btn = self.config["btn_map"][btn_num]
458+
459+
# timestamp_sec, timestamp_usec, type, code, value
460+
event = struct.pack(self.struct_format,
461+
ts_sec, ts_usec, EV_KEY, ev_btn, btn_val,
462+
ts_sec, ts_usec, EV_SYN, SYN_REPORT, 0
463+
)
464+
465+
logger.debug(struct.unpack(self.struct_format, event))
466+
467+
return event
468+
469+
def get_axis_event(self, axis_num, axis_val):
470+
now = time.time()
471+
ts_sec = int(now)
472+
ts_usec = int((now *1e6) % 1e6)
473+
474+
# evdev expects ev key codes, not axis numbers
475+
ev_axis = self.config["axes_map"][axis_num]
476+
477+
# timestamp_sec, timestamp_usec, type, code, value
478+
event = struct.pack(self.struct_format,
479+
ts_sec, ts_usec, EV_ABS, ev_axis, axis_val,
480+
ts_sec, ts_usec, EV_SYN, SYN_REPORT, 0
481+
)
482+
483+
logger.debug(struct.unpack(self.struct_format, event))
484+
485+
return event
486+
487+
class SelkiesJSGamepad(SelkiesGamepadBase):
488+
def __init__(self, socket_path, loop):
489+
super().__init__(socket_path, loop, JSGamepadMapper)
490+
491+
class SelkiesEVGamepad(SelkiesGamepadBase):
492+
def __init__(self, socket_path, loop):
493+
super().__init__(socket_path, loop, EVGamepadMapper)

src/selkies_gstreamer/webrtc_input.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ def __init__(self, uinput_mouse_socket_path="", js_socket_path="", enable_clipbo
9292

9393
# Map of gamepad numbers to socket paths
9494
self.js_socket_path_map = {i: os.path.join(js_socket_path, "selkies_js%d.sock" % i) for i in range(4)}
95+
self.ev_socket_path_map = {i: os.path.join(js_socket_path, "selkies_event%d.sock" % (1000 + i)) for i in range(4)}
9596

9697
# Map of gamepad number to SelkiesGamepad objects
9798
self.js_map = {}
@@ -172,16 +173,20 @@ def __js_connect(self, js_num, name, num_btns, num_axes):
172173

173174
logger.info("creating selkies gamepad for js%d, name: '%s', buttons: %d, axes: %d" % (js_num, name, num_btns, num_axes))
174175

175-
socket_path = self.js_socket_path_map.get(js_num, None)
176-
if socket_path is None:
176+
js_socket_path = self.js_socket_path_map.get(js_num, None)
177+
if js_socket_path is None:
177178
logger.error("failed to connect js%d because socket_path was not found" % js_num)
178179
return
179180

181+
ev_socket_path = self.ev_socket_path_map.get(js_num, None)
182+
if ev_socket_path is None:
183+
logger.error("failed to connect EV joystick %d because socket_path was not found" % js_num)
184+
return
185+
180186
# Create the gamepad and button config.
181-
js = SelkiesGamepad(socket_path, self.loop)
187+
js = SelkiesGamepad(js_socket_path, ev_socket_path, self.loop)
182188
js.set_config(name, num_btns, num_axes)
183-
184-
asyncio.ensure_future(js.run_server(), loop=self.loop)
189+
js.run_server()
185190

186191
self.js_map[js_num] = js
187192

0 commit comments

Comments
 (0)