fixed: bug where a release event from a PS3 BD remote after a long press wasn't detected.
[xbmc:xbmc-antiquated.git] / XBMC / tools / EventClients / Clients / PS3 Sixaxis Controller / ps3d.py
1 #!/usr/bin/python
2
3 import sys
4 import traceback
5
6 sys.path.append("../PS3 BD Remote")
7 try:
8     from xbmc.bt.hid import HID
9     from xbmc.bt.bt import bt_lookup_name
10     from xbmc.xbmcclient import XBMCClient
11     from xbmc.ps3 import sixaxis
12     from xbmc.ps3.keymaps import keymap_sixaxis
13     from xbmc.ps3_remote import process_keys as process_remote
14     from xbmc.xbmc.defs import *
15 except:
16     sys.path.append("../../lib/python")
17     from bt.hid import HID
18     from bt.bt import bt_lookup_name
19     from xbmcclient import XBMCClient
20     from ps3 import sixaxis
21     from ps3.keymaps import keymap_sixaxis
22     from ps3_remote import process_keys as process_remote
23     ICON_PATH = "../../icons/"
24
25 import time
26 import struct
27 import threading
28
29 event_threads = []
30
31 def printerr():
32         trace = ""
33         exception = ""
34         exc_list = traceback.format_exception_only (sys.exc_type, sys.exc_value)
35         for entry in exc_list:
36                 exception += entry
37         tb_list = traceback.format_tb(sys.exc_info()[2])
38         for entry in tb_list:
39                 trace += entry  
40         print("%s\n%s" % (exception, trace), "Script Error")
41
42
43
44 class StoppableThread ( threading.Thread ):
45     def __init__(self):
46         threading.Thread.__init__(self)
47         self._stop = False
48         self.set_timeout(0)
49
50     def stop_thread(self):
51         self._stop = True
52
53     def stop(self):
54         return self._stop
55
56     def close_sockets(self):
57         if self.isock:
58             try:
59                 self.isock.close()
60             except:
61                 pass
62         self.isock = None
63         if self.csock:
64             try:
65                 self.csock.close()
66             except:
67                 pass
68         self.csock = None
69         self.last_action = 0
70
71     def set_timeout(self, seconds):
72         self.timeout = seconds
73
74     def reset_timeout(self):
75         self.last_action = time.time()
76
77     def idle_time(self):
78         return time.time() - self.last_action
79
80     def timed_out(self):
81         if (time.time() - self.last_action) > self.timeout:
82             return True
83         else:
84             return False
85
86 # to make sure all combination keys are checked first
87 # we sort the keymap's button codes in reverse order
88 # this guranties that any bit combined button code
89 # will be processed first
90 keymap_sixaxis_keys = keymap_sixaxis.keys()
91 keymap_sixaxis_keys.sort()
92 keymap_sixaxis_keys.reverse()
93
94 def getkeys(bflags):
95     keys = [];
96     for k in keymap_sixaxis_keys:
97         if (k & bflags) == k:
98             keys.append(k)
99             bflags = bflags & ~k
100     return keys;
101
102 class PS3SixaxisThread ( StoppableThread ):
103     def __init__(self, csock, isock, ipaddr="127.0.0.1"):
104         StoppableThread.__init__(self)
105         self.csock = csock
106         self.isock = isock
107         self.xbmc = XBMCClient(name="PS3 Sixaxis", icon_file=ICON_PATH + "/bluetooth.png", ip=ipaddr)
108         self.set_timeout(600)
109
110     def run(self):
111         sixaxis.initialize(self.csock, self.isock)
112         self.xbmc.connect()
113         bflags = 0
114         released = set()
115         pressed  = set()
116         pending  = set()
117         held     = set()
118         psflags = 0
119         psdown = 0
120         toggle_mouse = 0
121         self.reset_timeout()
122         try:
123             while not self.stop():
124                 if self.timed_out():
125
126                     for key in (held | pressed):
127                         (mapname, action, amount, axis) = keymap_sixaxis[key]
128                         self.xbmc.send_button_state(map=mapname, button=action, amount=0, down=0, axis=axis)
129
130                     raise Exception("PS3 Sixaxis powering off, timed out")
131                 if self.idle_time() > 50:
132                     self.xbmc.connect()
133                 try:
134                     data = sixaxis.read_input(self.isock)
135                 except Exception, e:
136                     print str(e)
137                     break
138                 if not data:
139                     continue
140
141                 (bflags, psflags, pressure) = sixaxis.process_input(data, self.xbmc, toggle_mouse)
142
143                 if psflags:
144                     self.reset_timeout()
145                     if psdown:
146                         if (time.time() - psdown) > 5:
147
148                             for key in (held | pressed):
149                                 (mapname, action, amount, axis) = keymap_sixaxis[key]
150                                 self.xbmc.send_button_state(map=mapname, button=action, amount=0, down=0, axis=axis)
151   
152                             raise Exception("PS3 Sixaxis powering off, user request")
153                     else:
154                         psdown = time.time()
155                 else:
156                     if psdown:
157                         toggle_mouse = 1 - toggle_mouse
158                     psdown = 0
159
160                 keys = set(getkeys(bflags))
161                 released = (pressed | held) - keys
162                 held     = (pressed | held) - released
163                 pressed  = (keys - held) & pending
164                 pending  = (keys - held)
165
166                 for key in released:
167                     (mapname, action, amount, axis) = keymap_sixaxis[key]
168                     self.xbmc.send_button_state(map=mapname, button=action, amount=0, down=0, axis=axis)
169
170                 for key in held:
171                     (mapname, action, amount, axis) = keymap_sixaxis[key]
172                     if amount > 0:
173                         amount = pressure[amount-1] * 256
174                         self.xbmc.send_button_state(map=mapname, button=action, amount=amount, down=1, axis=axis)
175
176                 for key in pressed:
177                     (mapname, action, amount, axis) = keymap_sixaxis[key]
178                     if amount > 0:
179                         amount = pressure[amount-1] * 256
180                     self.xbmc.send_button_state(map=mapname, button=action, amount=amount, down=1, axis=axis)
181
182                 if keys:
183                     self.reset_timeout()
184
185
186         except Exception, e:
187             printerr()
188         self.close_sockets()
189
190
191 class PS3RemoteThread ( StoppableThread ):
192     def __init__(self, csock, isock, ipaddr="127.0.0.1"):
193         StoppableThread.__init__(self)
194         self.csock = csock
195         self.isock = isock
196         self.xbmc = XBMCClient(name="PS3 Blu-Ray Remote", icon_file=ICON_PATH + "/bluetooth.png", ip=ipaddr)
197         self.set_timeout(300)
198
199     def run(self):
200         sixaxis.initialize(self.csock, self.isock)
201         self.xbmc.connect()
202         try:
203             while not self.stop():
204                 status = process_remote(self.isock, self.xbmc)
205
206                 if status == 2:   # 2 = socket read timeout
207                     if self.timed_out():
208                         raise Exception("PS3 Blu-Ray Remote powering off, "\
209                                             "timed out")
210
211                 elif not status:  # 0 = keys are normally processed
212                     self.reset_timeout()
213
214         # process_remote() will raise an exception on read errors
215         except Exception, e:
216             print str(e)
217         self.close_sockets()
218
219
220 def usage():
221     print """
222 PS3 Sixaxis / Blu-Ray Remote HID Server v0.1
223
224 Usage: ps3.py [bdaddress] [XBMC host]
225
226   bdaddress  => address of local bluetooth device to use (default: auto)
227                 (e.g. aa:bb:cc:dd:ee:ff)
228   ip address => IP address or hostname of the XBMC instance (default: localhost)
229                 (e.g. 192.168.1.110)
230 """
231
232 def start_hidd(bdaddr=None, ipaddr="127.0.0.1"):
233     devices = [ 'PLAYSTATION(R)3 Controller',
234                 'BD Remote Control' ]
235     hid = HID(bdaddr)
236     while True:
237         if hid.listen():
238             (csock, addr) = hid.get_control_socket()
239             device_name = bt_lookup_name(addr[0])
240             if device_name == devices[0]:
241                 # handle PS3 controller
242                 handle_ps3_controller(hid, ipaddr)
243             elif device_name == devices[1]:
244                 # handle the PS3 remote
245                 handle_ps3_remote(hid, ipaddr)
246             else:
247                 print "Unknown Device: %s" % (device_name)
248
249 def handle_ps3_controller(hid, ipaddr):
250     print "Received connection from a Sixaxis PS3 Controller"
251     csock = hid.get_control_socket()[0]
252     isock = hid.get_interrupt_socket()[0]
253     sixaxis = PS3SixaxisThread(csock, isock, ipaddr)
254     add_thread(sixaxis)
255     sixaxis.start()
256     return
257
258 def handle_ps3_remote(hid, ipaddr):
259     print "Received connection from a PS3 Blu-Ray Remote"
260     csock = hid.get_control_socket()[0]
261     isock = hid.get_interrupt_socket()[0]
262     isock.settimeout(1)
263     remote = PS3RemoteThread(csock, isock, ipaddr)
264     add_thread(remote)
265     remote.start()
266     return
267
268 def add_thread(thread):
269     global event_threads
270     event_threads.append(thread)
271
272 def main():
273     if len(sys.argv)>3:
274         return usage()
275     bdaddr = ""
276     ipaddr = "127.0.0.1"
277     try:
278         for addr in sys.argv[1:]:
279             try:
280                 # ensure that the addr is of the format 'aa:bb:cc:dd:ee:ff'
281                 if "".join([ str(len(a)) for a in addr.split(":") ]) != "222222":
282                     raise Exception("Invalid format")
283                 bdaddr = addr
284                 print "Connecting to Bluetooth device: %s" % bdaddr
285             except Exception, e:
286                 try:
287                     ipaddr = addr
288                     print "Connecting to : %s" % ipaddr
289                 except:
290                     print str(e)
291                     return usage()
292     except:
293         pass
294
295     print "Starting HID daemon"
296     start_hidd(bdaddr, ipaddr)
297
298 if __name__=="__main__":
299     try:
300         main()
301     finally:
302         for t in event_threads:
303             try:
304                 print "Waiting for thread "+str(t)+" to terminate"
305                 t.stop_thread()
306                 if t.isAlive():
307                     t.join()
308                 print "Thread "+str(t)+" terminated"
309
310             except Exception, e:
311                 print str(e)
312         pass