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.
@@ -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))