steamcontroller-original 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- steamcontroller_original/__init__.py +406 -0
- steamcontroller_original/cheader.py +202 -0
- steamcontroller_original/daemon.py +238 -0
- steamcontroller_original/events.py +746 -0
- steamcontroller_original/gen_input_event_codes.py +75 -0
- steamcontroller_original/input_event_codes.py +720 -0
- steamcontroller_original/scripts/__init__.py +0 -0
- steamcontroller_original/scripts/desktop.py +104 -0
- steamcontroller_original/scripts/dump.py +50 -0
- steamcontroller_original/scripts/gamepad.py +110 -0
- steamcontroller_original/scripts/gyro_plot.py +166 -0
- steamcontroller_original/scripts/json2vdf.py +103 -0
- steamcontroller_original/scripts/mixed.py +143 -0
- steamcontroller_original/scripts/test_cmsg.py +56 -0
- steamcontroller_original/scripts/vdf2json.py +165 -0
- steamcontroller_original/scripts/xbox.py +145 -0
- steamcontroller_original/tools.py +43 -0
- steamcontroller_original/uinput.c +214 -0
- steamcontroller_original/uinput.py +716 -0
- steamcontroller_original/uinput_backends.py +344 -0
- steamcontroller_original-2.0.0.dist-info/METADATA +362 -0
- steamcontroller_original-2.0.0.dist-info/RECORD +27 -0
- steamcontroller_original-2.0.0.dist-info/WHEEL +5 -0
- steamcontroller_original-2.0.0.dist-info/entry_points.txt +10 -0
- steamcontroller_original-2.0.0.dist-info/licenses/LICENSE +661 -0
- steamcontroller_original-2.0.0.dist-info/licenses/NOTICE +37 -0
- steamcontroller_original-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
# Copyright (c) 2015 Stany MARCEL <stanypub@gmail.com>
|
|
2
|
+
#
|
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
# furnished to do so, subject to the following conditions:
|
|
9
|
+
#
|
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
|
11
|
+
# all copies or substantial portions of the Software.
|
|
12
|
+
#
|
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
19
|
+
# THE SOFTWARE.
|
|
20
|
+
|
|
21
|
+
from enum import IntEnum
|
|
22
|
+
from threading import Event, Lock, Timer
|
|
23
|
+
from time import time, sleep
|
|
24
|
+
from struct import pack, unpack
|
|
25
|
+
from collections import namedtuple
|
|
26
|
+
|
|
27
|
+
import usb1
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
VENDOR_ID = 0x28de
|
|
31
|
+
PRODUCT_ID = [0x1102, 0x1142, 0x1142, 0x1142, 0x1142]
|
|
32
|
+
ENDPOINT = [3, 2, 3, 4, 5]
|
|
33
|
+
CONTROLIDX = [2, 1, 2, 3, 4]
|
|
34
|
+
|
|
35
|
+
HPERIOD = 0.02
|
|
36
|
+
LPERIOD = 0.5
|
|
37
|
+
DURATION = 1.0
|
|
38
|
+
|
|
39
|
+
STEAM_CONTROLLER_FORMAT = [
|
|
40
|
+
('x', 'ukn_00'),
|
|
41
|
+
('x', 'ukn_01'),
|
|
42
|
+
('B', 'status'),
|
|
43
|
+
('x', 'ukn_02'),
|
|
44
|
+
('H', 'seq'),
|
|
45
|
+
('x', 'ukn_03'),
|
|
46
|
+
('I', 'buttons'),
|
|
47
|
+
('B', 'ltrig'),
|
|
48
|
+
('B', 'rtrig'),
|
|
49
|
+
('x', 'ukn_04'),
|
|
50
|
+
('x', 'ukn_05'),
|
|
51
|
+
('x', 'ukn_06'),
|
|
52
|
+
('h', 'lpad_x'),
|
|
53
|
+
('h', 'lpad_y'),
|
|
54
|
+
('h', 'rpad_x'),
|
|
55
|
+
('h', 'rpad_y'),
|
|
56
|
+
('10x', 'ukn_07'),
|
|
57
|
+
('h', 'gpitch'),
|
|
58
|
+
('h', 'groll'),
|
|
59
|
+
('h', 'gyaw'),
|
|
60
|
+
('h', 'q1'),
|
|
61
|
+
('h', 'q2'),
|
|
62
|
+
('h', 'q3'),
|
|
63
|
+
('h', 'q4'),
|
|
64
|
+
('16x', 'ukn_08'),
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
_FORMATS, _NAMES = zip(*STEAM_CONTROLLER_FORMAT)
|
|
68
|
+
|
|
69
|
+
EXITCMD = pack('>' + 'I' * 2,
|
|
70
|
+
0x9f046f66,
|
|
71
|
+
0x66210000)
|
|
72
|
+
|
|
73
|
+
SteamControllerInput = namedtuple('SteamControllerInput', ' '.join([x for x in _NAMES if not x.startswith('ukn_')]))
|
|
74
|
+
|
|
75
|
+
SCI_NULL = SteamControllerInput._make(unpack('<' + ''.join(_FORMATS), b'\x00' * 64))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class SCStatus(IntEnum):
|
|
79
|
+
INPUT = 0x01
|
|
80
|
+
HOTPLUG = 0x03
|
|
81
|
+
IDLE = 0x04
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class SCButtons(IntEnum):
|
|
85
|
+
RPADTOUCH = 0b00010000000000000000000000000000
|
|
86
|
+
LPADTOUCH = 0b00001000000000000000000000000000
|
|
87
|
+
RPAD = 0b00000100000000000000000000000000
|
|
88
|
+
LPAD = 0b00000010000000000000000000000000 # Same for stick but without LPadTouch
|
|
89
|
+
RGRIP = 0b00000001000000000000000000000000
|
|
90
|
+
LGRIP = 0b00000000100000000000000000000000
|
|
91
|
+
START = 0b00000000010000000000000000000000
|
|
92
|
+
STEAM = 0b00000000001000000000000000000000
|
|
93
|
+
BACK = 0b00000000000100000000000000000000
|
|
94
|
+
A = 0b00000000000000001000000000000000
|
|
95
|
+
X = 0b00000000000000000100000000000000
|
|
96
|
+
B = 0b00000000000000000010000000000000
|
|
97
|
+
Y = 0b00000000000000000001000000000000
|
|
98
|
+
LB = 0b00000000000000000000100000000000
|
|
99
|
+
RB = 0b00000000000000000000010000000000
|
|
100
|
+
LT = 0b00000000000000000000001000000000
|
|
101
|
+
RT = 0b00000000000000000000000100000000
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class HapticPos(IntEnum):
|
|
105
|
+
"""Specify which pad or trig is used"""
|
|
106
|
+
RIGHT = 0
|
|
107
|
+
LEFT = 1
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class SteamController(object):
|
|
111
|
+
|
|
112
|
+
def __init__(self, callback, callback_args=None, keep_alive=False):
|
|
113
|
+
"""
|
|
114
|
+
Constructor
|
|
115
|
+
|
|
116
|
+
callback: function called on usb message must take at lead a
|
|
117
|
+
SteamControllerInput as first argument
|
|
118
|
+
|
|
119
|
+
callback_args: Optional arguments passed to the callback afer the
|
|
120
|
+
SteamControllerInput argument
|
|
121
|
+
"""
|
|
122
|
+
self._handle = None
|
|
123
|
+
self._number = None
|
|
124
|
+
self._ccidx = None
|
|
125
|
+
self._timer = None
|
|
126
|
+
self._timer_stop = Event()
|
|
127
|
+
self._cb_lock = Lock()
|
|
128
|
+
self._cb = callback
|
|
129
|
+
self._cb_args = callback_args
|
|
130
|
+
self._cmsg = []
|
|
131
|
+
self._ctx = usb1.USBContext()
|
|
132
|
+
self._transfer_list = []
|
|
133
|
+
self.keep_alive = keep_alive
|
|
134
|
+
try:
|
|
135
|
+
self._open()
|
|
136
|
+
except (usb1.USBError, ValueError):
|
|
137
|
+
if not keep_alive:
|
|
138
|
+
raise
|
|
139
|
+
|
|
140
|
+
def _open(self):
|
|
141
|
+
handle = []
|
|
142
|
+
pid = []
|
|
143
|
+
endpoint = []
|
|
144
|
+
ccidx = []
|
|
145
|
+
for i in range(len(PRODUCT_ID)):
|
|
146
|
+
_pid = PRODUCT_ID[i]
|
|
147
|
+
_endpoint = ENDPOINT[i]
|
|
148
|
+
_ccidx = CONTROLIDX[i]
|
|
149
|
+
|
|
150
|
+
_handle = self._ctx.openByVendorIDAndProductID(
|
|
151
|
+
VENDOR_ID, _pid,
|
|
152
|
+
skip_on_error=True,
|
|
153
|
+
)
|
|
154
|
+
if _handle is not None:
|
|
155
|
+
handle.append(_handle)
|
|
156
|
+
pid.append(_pid)
|
|
157
|
+
endpoint.append(_endpoint)
|
|
158
|
+
ccidx.append(_ccidx)
|
|
159
|
+
|
|
160
|
+
if not handle:
|
|
161
|
+
raise ValueError('No SteamController Device found')
|
|
162
|
+
|
|
163
|
+
claimed = False
|
|
164
|
+
for i in range(len(handle)):
|
|
165
|
+
self._ccidx = ccidx[i]
|
|
166
|
+
self._handle = handle[i]
|
|
167
|
+
self._pid = pid[i]
|
|
168
|
+
self._endpoint = endpoint[i]
|
|
169
|
+
dev = handle[i].getDevice()
|
|
170
|
+
cfg = dev[0]
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
for inter in cfg:
|
|
174
|
+
for setting in inter:
|
|
175
|
+
number = setting.getNumber()
|
|
176
|
+
if self._handle.kernelDriverActive(number):
|
|
177
|
+
self._handle.detachKernelDriver(number)
|
|
178
|
+
if (setting.getClass() == 3 and
|
|
179
|
+
setting.getSubClass() == 0 and
|
|
180
|
+
setting.getProtocol() == 0 and
|
|
181
|
+
number == i+1):
|
|
182
|
+
self._handle.claimInterface(number)
|
|
183
|
+
self._number = number
|
|
184
|
+
claimed = True
|
|
185
|
+
except usb1.USBErrorBusy:
|
|
186
|
+
claimed = False
|
|
187
|
+
|
|
188
|
+
if claimed:
|
|
189
|
+
break
|
|
190
|
+
|
|
191
|
+
if not claimed:
|
|
192
|
+
raise ValueError('All SteamController are busy')
|
|
193
|
+
|
|
194
|
+
self._transfer_list = []
|
|
195
|
+
transfer = self._handle.getTransfer()
|
|
196
|
+
transfer.setInterrupt(
|
|
197
|
+
usb1.ENDPOINT_IN | self._endpoint,
|
|
198
|
+
64,
|
|
199
|
+
callback=self._processReceivedData,
|
|
200
|
+
)
|
|
201
|
+
transfer.submit()
|
|
202
|
+
self._transfer_list.append(transfer)
|
|
203
|
+
|
|
204
|
+
self._period = LPERIOD
|
|
205
|
+
|
|
206
|
+
if self._pid == 0x1102:
|
|
207
|
+
self._timer_stop.clear()
|
|
208
|
+
self._timer = Timer(LPERIOD, self._callbackTimer)
|
|
209
|
+
# Daemonised so a timer that somehow outlives us can never
|
|
210
|
+
# keep the interpreter from exiting.
|
|
211
|
+
self._timer.daemon = True
|
|
212
|
+
self._timer.start()
|
|
213
|
+
else:
|
|
214
|
+
self._timer = None
|
|
215
|
+
|
|
216
|
+
self._tup = None
|
|
217
|
+
self._lastusb = time()
|
|
218
|
+
|
|
219
|
+
# Disable Haptic auto feedback
|
|
220
|
+
|
|
221
|
+
self._ctx.handleEvents()
|
|
222
|
+
self._sendControl(pack('>' + 'I' * 1,
|
|
223
|
+
0x81000000))
|
|
224
|
+
self._ctx.handleEvents()
|
|
225
|
+
self._sendControl(pack('>' + 'I' * 6,
|
|
226
|
+
0x87153284,
|
|
227
|
+
0x03180000,
|
|
228
|
+
0x31020008,
|
|
229
|
+
0x07000707,
|
|
230
|
+
0x00300000,
|
|
231
|
+
0x2f010000))
|
|
232
|
+
self._ctx.handleEvents()
|
|
233
|
+
|
|
234
|
+
def _stopTimer(self):
|
|
235
|
+
"""Stop the re-arming callback timer, if this controller has one.
|
|
236
|
+
|
|
237
|
+
Reached from __del__, which can run on an object whose __init__
|
|
238
|
+
bailed out early, so nothing here may assume an attribute exists.
|
|
239
|
+
"""
|
|
240
|
+
stop = getattr(self, '_timer_stop', None)
|
|
241
|
+
if stop is not None:
|
|
242
|
+
stop.set()
|
|
243
|
+
|
|
244
|
+
timer = getattr(self, '_timer', None)
|
|
245
|
+
if timer is not None:
|
|
246
|
+
timer.cancel()
|
|
247
|
+
self._timer = None
|
|
248
|
+
|
|
249
|
+
def _close(self):
|
|
250
|
+
self._stopTimer()
|
|
251
|
+
if self._handle:
|
|
252
|
+
# _open() assigns _handle before it manages to claim an
|
|
253
|
+
# interface, so a controller that turns out to be busy leaves
|
|
254
|
+
# us holding a handle with no _number to go with it. Only
|
|
255
|
+
# talk to, and release, an interface we actually claimed;
|
|
256
|
+
# otherwise just let the handle go.
|
|
257
|
+
if self._number is not None:
|
|
258
|
+
self._sendControl(EXITCMD)
|
|
259
|
+
self._handle.releaseInterface(self._number)
|
|
260
|
+
self._number = None
|
|
261
|
+
self._handle.resetDevice()
|
|
262
|
+
self._handle.close()
|
|
263
|
+
self._handle = None
|
|
264
|
+
|
|
265
|
+
def __del__(self):
|
|
266
|
+
# A destructor must not raise, and this one runs during
|
|
267
|
+
# interpreter teardown and on half-constructed objects.
|
|
268
|
+
try:
|
|
269
|
+
self._close()
|
|
270
|
+
except Exception:
|
|
271
|
+
pass
|
|
272
|
+
|
|
273
|
+
def _sendControl(self, data, timeout=0):
|
|
274
|
+
# Control messages are a single 64-byte packet. Anything longer
|
|
275
|
+
# used to compute a negative pad length, quietly pad with
|
|
276
|
+
# nothing, and send an overlong message -- reachable from
|
|
277
|
+
# sc-test-cmsg.py, which sends whatever hex it is given.
|
|
278
|
+
if len(data) > 64:
|
|
279
|
+
raise ValueError(
|
|
280
|
+
'control message is {} bytes, maximum is 64'.format(len(data)))
|
|
281
|
+
|
|
282
|
+
zeros = b'\x00' * (64 - len(data))
|
|
283
|
+
|
|
284
|
+
self._handle.controlWrite(request_type=0x21,
|
|
285
|
+
request=0x09,
|
|
286
|
+
value=0x0300,
|
|
287
|
+
index=self._ccidx,
|
|
288
|
+
data=data + zeros,
|
|
289
|
+
timeout=timeout)
|
|
290
|
+
|
|
291
|
+
def addExit(self):
|
|
292
|
+
self._cmsg.insert(0, EXITCMD)
|
|
293
|
+
|
|
294
|
+
def addFeedback(self, position, amplitude=128, period=0, count=1):
|
|
295
|
+
"""
|
|
296
|
+
Add haptic feedback to be sent on next usb tick
|
|
297
|
+
|
|
298
|
+
@param int position haptic to use 1 for left 0 for right
|
|
299
|
+
@param int amplitude signal amplitude from 0 to 65535
|
|
300
|
+
@param int period signal period from 0 to 65535
|
|
301
|
+
@param int count number of period to play
|
|
302
|
+
"""
|
|
303
|
+
self._cmsg.insert(0, pack('<BBBHHH', 0x8f, 0x07, position, amplitude, period, count))
|
|
304
|
+
|
|
305
|
+
def _processReceivedData(self, transfer):
|
|
306
|
+
"""Private USB async Rx function"""
|
|
307
|
+
if (transfer.getStatus() != usb1.TRANSFER_COMPLETED or
|
|
308
|
+
transfer.getActualLength() != 64):
|
|
309
|
+
return
|
|
310
|
+
|
|
311
|
+
data = transfer.getBuffer()
|
|
312
|
+
tup = SteamControllerInput._make(unpack('<' + ''.join(_FORMATS), data))
|
|
313
|
+
if tup.status == SCStatus.INPUT:
|
|
314
|
+
self._tup = tup
|
|
315
|
+
|
|
316
|
+
self._callback()
|
|
317
|
+
|
|
318
|
+
# Re-arm for the next packet. If the controller vanished in the
|
|
319
|
+
# meantime this raises, and it would do so from inside libusb's
|
|
320
|
+
# event handling -- escaping run() entirely and defeating the
|
|
321
|
+
# keep_alive reconnect. Leaving the transfer unsubmitted is
|
|
322
|
+
# exactly what run() watches for, so let it drive the reconnect.
|
|
323
|
+
try:
|
|
324
|
+
transfer.submit()
|
|
325
|
+
except usb1.USBError:
|
|
326
|
+
pass
|
|
327
|
+
|
|
328
|
+
def _callback(self):
|
|
329
|
+
if self._tup is None:
|
|
330
|
+
return
|
|
331
|
+
|
|
332
|
+
self._lastusb = time()
|
|
333
|
+
|
|
334
|
+
# See _callbackTimer: the wired controller drives callbacks from a
|
|
335
|
+
# Timer thread as well as from here.
|
|
336
|
+
with self._cb_lock:
|
|
337
|
+
if isinstance(self._cb_args, (list, tuple)):
|
|
338
|
+
self._cb(self, self._tup, *self._cb_args)
|
|
339
|
+
else:
|
|
340
|
+
self._cb(self, self._tup)
|
|
341
|
+
|
|
342
|
+
self._period = HPERIOD
|
|
343
|
+
|
|
344
|
+
def _callbackTimer(self):
|
|
345
|
+
# This timer re-arms itself, so without this check a closed
|
|
346
|
+
# controller would keep one alive forever -- still calling back
|
|
347
|
+
# into a mapper whose devices are gone, and leaking another
|
|
348
|
+
# thread every time the daemon reconnected.
|
|
349
|
+
if self._timer_stop.is_set():
|
|
350
|
+
return
|
|
351
|
+
|
|
352
|
+
d = time() - self._lastusb
|
|
353
|
+
self._timer.cancel()
|
|
354
|
+
|
|
355
|
+
if d > DURATION:
|
|
356
|
+
self._period = LPERIOD
|
|
357
|
+
|
|
358
|
+
self._timer = Timer(self._period, self._callbackTimer)
|
|
359
|
+
self._timer.daemon = True
|
|
360
|
+
self._timer.start()
|
|
361
|
+
|
|
362
|
+
if self._tup is None:
|
|
363
|
+
return
|
|
364
|
+
|
|
365
|
+
if d < HPERIOD:
|
|
366
|
+
return
|
|
367
|
+
|
|
368
|
+
# Serialised against the USB thread: both paths call the same
|
|
369
|
+
# user callback, and EventMapper keeps mutable state (pressed
|
|
370
|
+
# keys, smoothing deques) that two threads must not interleave.
|
|
371
|
+
with self._cb_lock:
|
|
372
|
+
if isinstance(self._cb_args, (list, tuple)):
|
|
373
|
+
self._cb(self, self._tup, *self._cb_args)
|
|
374
|
+
else:
|
|
375
|
+
self._cb(self, self._tup)
|
|
376
|
+
|
|
377
|
+
def run(self):
|
|
378
|
+
"""Function to run in order to process USB events"""
|
|
379
|
+
if self._handle or self.keep_alive:
|
|
380
|
+
try:
|
|
381
|
+
while True:
|
|
382
|
+
while any(x.isSubmitted() for x in self._transfer_list):
|
|
383
|
+
self._ctx.handleEvents()
|
|
384
|
+
if self._cmsg:
|
|
385
|
+
cmsg = self._cmsg.pop()
|
|
386
|
+
if cmsg == EXITCMD and not self.keep_alive:
|
|
387
|
+
return
|
|
388
|
+
self._sendControl(cmsg)
|
|
389
|
+
try:
|
|
390
|
+
self._close()
|
|
391
|
+
except usb1.USBError:
|
|
392
|
+
pass
|
|
393
|
+
if not self.keep_alive:
|
|
394
|
+
return
|
|
395
|
+
sleep(2)
|
|
396
|
+
try:
|
|
397
|
+
self._open()
|
|
398
|
+
except (usb1.USBError, ValueError):
|
|
399
|
+
pass
|
|
400
|
+
except usb1.USBErrorInterrupted:
|
|
401
|
+
pass
|
|
402
|
+
|
|
403
|
+
def handleEvents(self):
|
|
404
|
+
"""Function to run in order to handle USB events"""
|
|
405
|
+
if self._handle and self._ctx:
|
|
406
|
+
self._ctx.handleEvents()
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
# The MIT License (MIT)
|
|
4
|
+
#
|
|
5
|
+
# Copyright (c) 2015 Stany MARCEL <stanypub@gmail.com>
|
|
6
|
+
#
|
|
7
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
# in the Software without restriction, including without limitation the rights
|
|
10
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
# furnished to do so, subject to the following conditions:
|
|
13
|
+
#
|
|
14
|
+
# The above copyright notice and this permission notice shall be included in
|
|
15
|
+
# all copies or substantial portions of the Software.
|
|
16
|
+
#
|
|
17
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
23
|
+
# THE SOFTWARE.
|
|
24
|
+
|
|
25
|
+
import os
|
|
26
|
+
import io
|
|
27
|
+
import ast
|
|
28
|
+
import sys
|
|
29
|
+
import shlex
|
|
30
|
+
import operator as op
|
|
31
|
+
from collections import OrderedDict
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
OPERATORS = {
|
|
35
|
+
ast.Add : op.add,
|
|
36
|
+
ast.Sub : op.sub,
|
|
37
|
+
ast.Mult : op.mul,
|
|
38
|
+
ast.Div : op.floordiv,
|
|
39
|
+
ast.Mod : op.mod,
|
|
40
|
+
ast.LShift : op.lshift,
|
|
41
|
+
ast.RShift : op.rshift,
|
|
42
|
+
ast.BitOr : op.or_,
|
|
43
|
+
ast.BitXor : op.xor,
|
|
44
|
+
ast.BitAnd : op.and_,
|
|
45
|
+
ast.Invert : op.invert,
|
|
46
|
+
ast.Not : op.not_,
|
|
47
|
+
ast.UAdd : op.pos,
|
|
48
|
+
ast.USub : op.neg,
|
|
49
|
+
ast.And : op.and_,
|
|
50
|
+
ast.Or : op.or_,
|
|
51
|
+
ast.Eq : op.eq,
|
|
52
|
+
ast.NotEq : op.ne,
|
|
53
|
+
ast.Lt : op.lt,
|
|
54
|
+
ast.LtE : op.le,
|
|
55
|
+
ast.Gt : op.gt,
|
|
56
|
+
ast.GtE : op.ge,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def eval_expr(expr):
|
|
61
|
+
"""Eval and expression inside a #define using a support of Python grammar"""
|
|
62
|
+
def _eval(node):
|
|
63
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, int):
|
|
64
|
+
return node.value
|
|
65
|
+
if isinstance(node, ast.BinOp):
|
|
66
|
+
return OPERATORS[type(node.op)](_eval(node.left), _eval(node.right))
|
|
67
|
+
if isinstance(node, ast.UnaryOp):
|
|
68
|
+
return OPERATORS[type(node.op)](_eval(node.operand))
|
|
69
|
+
if isinstance(node, ast.BoolOp):
|
|
70
|
+
values = [_eval(x) for x in node.values]
|
|
71
|
+
return OPERATORS[type(node.op)](**values)
|
|
72
|
+
raise TypeError(node)
|
|
73
|
+
|
|
74
|
+
return _eval(ast.parse(expr, mode='eval').body)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def read_source(filename):
|
|
78
|
+
"""Read a header file into an in-memory stream for shlex.
|
|
79
|
+
|
|
80
|
+
shlex holds on to whatever stream it is handed, so passing it a live
|
|
81
|
+
file object leaks the descriptor until the garbage collector gets
|
|
82
|
+
round to it. Read the text up front and close the file immediately.
|
|
83
|
+
|
|
84
|
+
Decoding is deliberately lenient: this runs at import time (uinput.py
|
|
85
|
+
parses the kernel headers to build its Keys/Axes/Rels enums), and a
|
|
86
|
+
stray non-UTF-8 byte in a comment somewhere under /usr/include must
|
|
87
|
+
not take the whole package down.
|
|
88
|
+
"""
|
|
89
|
+
with open(filename, encoding='utf-8', errors='replace') as stream:
|
|
90
|
+
return io.StringIO(stream.read())
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def defines(base, include):
|
|
94
|
+
"""Extract #define from base/include following #includes"""
|
|
95
|
+
parsed = set()
|
|
96
|
+
filename = os.path.normpath(os.path.abspath(os.path.join(base, include)))
|
|
97
|
+
parsed.add(filename)
|
|
98
|
+
|
|
99
|
+
lexer = shlex.shlex(read_source(filename), posix=True)
|
|
100
|
+
|
|
101
|
+
lexer.whitespace = ' \t\r'
|
|
102
|
+
lexer.commenters = ''
|
|
103
|
+
lexer.quotes = '"'
|
|
104
|
+
|
|
105
|
+
out = OrderedDict()
|
|
106
|
+
|
|
107
|
+
def parse_c_comments(lexer, tok, ntok):
|
|
108
|
+
if tok != '/' or ntok != '*':
|
|
109
|
+
return False
|
|
110
|
+
quotes = lexer.quotes
|
|
111
|
+
lexer.quotes = ''
|
|
112
|
+
while True:
|
|
113
|
+
tok = lexer.get_token()
|
|
114
|
+
ntok = lexer.get_token()
|
|
115
|
+
if tok == '*' and ntok == '/':
|
|
116
|
+
lexer.quotes = quotes
|
|
117
|
+
break
|
|
118
|
+
lexer.push_token(ntok)
|
|
119
|
+
return True
|
|
120
|
+
|
|
121
|
+
def parse_cpp_comments(lexer, tok, ntok):
|
|
122
|
+
if tok != '/' or ntok != '/':
|
|
123
|
+
return False
|
|
124
|
+
quotes = lexer.quotes
|
|
125
|
+
lexer.quotes = ''
|
|
126
|
+
while True:
|
|
127
|
+
tok = lexer.get_token()
|
|
128
|
+
if tok == '\n':
|
|
129
|
+
lexer.quotes = quotes
|
|
130
|
+
lexer.push_token(tok)
|
|
131
|
+
break
|
|
132
|
+
return True
|
|
133
|
+
|
|
134
|
+
while True:
|
|
135
|
+
tok = lexer.get_token()
|
|
136
|
+
if not tok:
|
|
137
|
+
break
|
|
138
|
+
ntok = lexer.get_token()
|
|
139
|
+
|
|
140
|
+
if parse_c_comments(lexer, tok, ntok):
|
|
141
|
+
continue
|
|
142
|
+
if parse_cpp_comments(lexer, tok, ntok):
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
if tok != '\n' or ntok != '#':
|
|
146
|
+
lexer.push_token(ntok)
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
tok = lexer.get_token()
|
|
150
|
+
if tok == 'define':
|
|
151
|
+
name = lexer.get_token()
|
|
152
|
+
expr = ''
|
|
153
|
+
while True:
|
|
154
|
+
tok = lexer.get_token()
|
|
155
|
+
ntok = lexer.get_token()
|
|
156
|
+
|
|
157
|
+
if parse_c_comments(lexer, tok, ntok):
|
|
158
|
+
continue
|
|
159
|
+
if parse_cpp_comments(lexer, tok, ntok):
|
|
160
|
+
continue
|
|
161
|
+
lexer.push_token(ntok)
|
|
162
|
+
|
|
163
|
+
if not tok:
|
|
164
|
+
break
|
|
165
|
+
if tok == '\n':
|
|
166
|
+
lexer.push_token(tok)
|
|
167
|
+
break
|
|
168
|
+
|
|
169
|
+
if tok in out:
|
|
170
|
+
tok = str(out[tok])
|
|
171
|
+
expr = expr + tok
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
val = eval_expr(expr)
|
|
175
|
+
out[name] = val
|
|
176
|
+
except (SyntaxError, TypeError):
|
|
177
|
+
pass
|
|
178
|
+
elif tok == 'include':
|
|
179
|
+
tok = lexer.get_token()
|
|
180
|
+
if tok == '<':
|
|
181
|
+
name = ''
|
|
182
|
+
while True:
|
|
183
|
+
tok = lexer.get_token()
|
|
184
|
+
if tok == '>':
|
|
185
|
+
break
|
|
186
|
+
name = name + tok
|
|
187
|
+
else:
|
|
188
|
+
name = tok
|
|
189
|
+
filename = os.path.normpath(os.path.abspath(os.path.join(base, name)))
|
|
190
|
+
if os.path.isfile(filename) and filename not in parsed:
|
|
191
|
+
parsed.add(filename)
|
|
192
|
+
lexer.push_source(read_source(filename))
|
|
193
|
+
else:
|
|
194
|
+
lexer.push_token(tok)
|
|
195
|
+
|
|
196
|
+
return out
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
if __name__ == '__main__':
|
|
200
|
+
definesDict = defines(sys.argv[1], sys.argv[2])
|
|
201
|
+
for k, v in definesDict.items():
|
|
202
|
+
print('{}:\t{}'.format(k, v))
|