33from modules .base_module import BaseModule
44import yaml
55
6- try :
7- import inputs # pip install inputs
8- except ImportError :
9- raise ImportError ("Please install the 'inputs' package: pip install inputs" )
6+ import struct
107
118class Controller (BaseModule ):
129 def __init__ (self , ** kwargs ):
@@ -19,21 +16,121 @@ def __init__(self, **kwargs):
1916 self .pressed_buttons = set ()
2017 self .axis_last_value = {} # Track last value for each axis
2118 self .axis_last_time = {} # Track last event time (ms) for each axis
19+ self .device = kwargs .get ('device' , "/dev/input/js0" )
2220
2321 def setup_messaging (self ):
2422 self .running = True
2523 self .thread .start ()
2624
2725 def listen (self ):
2826 self .log ("Controller listening for input..." )
27+ JS_EVENT_FORMAT = "IhBB"
28+ JS_EVENT_SIZE = struct .calcsize (JS_EVENT_FORMAT )
2929 while self .running :
3030 try :
31- events = inputs .get_gamepad ()
32- for event in events :
33- self .handle_event (event )
34- except inputs .UnpluggedError :
35- self .log ("No gamepad found. Waiting for connection..." , level = 'warning' )
31+ with open (self .device , "rb" ) as jsdev :
32+ while self .running :
33+ evbuf = jsdev .read (JS_EVENT_SIZE )
34+ if not evbuf :
35+ continue
36+ time_ms , value , type_ , number = struct .unpack (JS_EVENT_FORMAT , evbuf )
37+ # print(f"time={time} value={value} type={type_} number={number}")
38+ self .handle_js_event (time_ms , value , type_ , number )
39+ except FileNotFoundError :
40+ self .log (f"No gamepad found at { self .device } . Waiting for connection..." , level = 'warning' )
3641 time .sleep (1 )
42+ except Exception as e :
43+ self .log (f"Error reading { self .device } : { e } " , level = 'error' )
44+ time .sleep (1 )
45+
46+ def handle_js_event (self , time_ms , value , type_ , number ):
47+ # type_: 0x01 = button, 0x02 = axis, 0x80 = init
48+ JS_EVENT_BUTTON = 1
49+ JS_EVENT_AXIS = 0x02
50+ JS_EVENT_INIT = 0x80
51+
52+ is_init = (type_ & JS_EVENT_INIT ) != 0
53+ event_type = type_ & ~ JS_EVENT_INIT
54+
55+ # Map button/axis numbers to names as needed for your controller
56+ button_names = {
57+ 0 : 'BTN_A' , 1 : 'BTN_B' , 2 : 'BTN_X' , 3 : 'BTN_Y' ,
58+ 4 : 'BTN_TL' , 5 : 'BTN_TR' , 6 : 'BTN_SELECT' , 7 : 'BTN_START' ,
59+ 8 : 'BTN_MODE' , 9 : 'BTN_THUMBL' , 10 : 'BTN_THUMBR'
60+ }
61+ axis_names = {
62+ 0 : 'ABS_X' , 1 : 'ABS_Y' , 2 : 'ABS_Z' , 3 : 'ABS_RZ' ,
63+ 4 : 'ABS_RX' , 5 : 'ABS_RY' , 6 : 'ABS_HAT0X' , 7 : 'ABS_HAT0Y'
64+ }
65+
66+ # self.log(f"JS Event: time={time_ms} value={value} type={event_type} number={number} (init={is_init})")
67+
68+ if event_type == JS_EVENT_BUTTON :
69+ # self.log(f"JS Button Event: number={number} value={value} (init={is_init})")
70+ button = button_names .get (number , f'BTN_{ number } ' )
71+ self .log (f"Button event: { button } value={ value } (jsdev)" )
72+ # value: 1=pressed, 0=released
73+ if value == 1 :
74+ self .pressed_buttons .add (button )
75+ elif value == 0 and button in self .pressed_buttons :
76+ self .pressed_buttons .remove (button )
77+
78+ active_mods = sorted ([b for b in self .pressed_buttons if b in self .modifier_buttons ])
79+ mapping_key = '+' .join (active_mods ) if active_mods else 'default'
80+ self .log (f"Active modifiers: { active_mods } (jsdev)" )
81+ button_map = self .button_action_map .get (mapping_key , self .button_action_map .get ('default' , {}))
82+
83+ self .log (button )
84+ if button in button_map and value == 1 :
85+ actions = button_map [button ]
86+ for mapping in actions :
87+ topic = mapping .get ('topic' )
88+ if not topic :
89+ self .log (f"Empty topic for button { button } mapping, skipping." , level = 'warning' )
90+ continue # Skip empty topic
91+ args = mapping .get ('args' , {})
92+ self .publish (topic , ** args )
93+ self .log (f"Published to topic { topic } with args { args } (jsdev)" )
94+
95+ elif event_type == JS_EVENT_AXIS :
96+ axis = axis_names .get (number , f'AXIS_{ number } ' )
97+ # self.log(f"Axis event: {axis} value={value} (jsdev)")
98+ # value: -32767..32767 for sticks, -1/0/1 for hats
99+ active_mods = sorted ([b for b in self .pressed_buttons if b in self .modifier_buttons ])
100+ mapping_key = '+' .join (active_mods ) if active_mods else 'default'
101+ button_map = self .button_action_map .get (mapping_key , self .button_action_map .get ('default' , {}))
102+ self .log (f"Active modifiers: { active_mods } (jsdev)" )
103+
104+ if axis in button_map :
105+ actions = button_map [axis ]
106+ for mapping in actions :
107+ deadzone = mapping .get ('deadzone' , self .global_deadzone )
108+ if deadzone > abs (value ):
109+ continue # Skip if within deadzone
110+
111+ debounce = mapping .get ('debounce' , self .global_debounce ) # debounce in ms
112+ now = int (time .time () * 1000 ) # current time in ms
113+ last_time = self .axis_last_time .get (axis , None )
114+
115+ # Only allow if enough time has passed
116+ if last_time is not None and (now - last_time ) < debounce :
117+ continue # Skip if within debounce interval
118+ self .axis_last_time [axis ] = now
119+ last_value = self .axis_last_value .get (axis , 0 )
120+ delta = value - last_value
121+ self .axis_last_value [axis ] = value
122+
123+ topic = mapping .get ('topic' )
124+ if not topic :
125+ continue # Skip empty topic
126+ args = dict (mapping .get ('args' , {})) # Copy to avoid mutation
127+ modifier = mapping .get ('modifier' , None )
128+ if modifier is not None :
129+ scale = modifier .get ('scale' , 1.0 )
130+ args ['delta' ] = delta * scale
131+ # self.log(f"Axis {axis} moved to {args.get('delta', value)} (jsdev)")
132+ self .publish (topic , ** args )
133+ self .log (f"Published to topic { topic } with args { args } (jsdev)" )
37134
38135 def handle_event (self , event ):
39136 # if event.code == 'ABS_Y':
0 commit comments