[cosmetics] update date in GPL header
[vuplus_xbmc] / tools / EventClients / Clients / PS3 Sixaxis Controller / ps3d.py
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
3
4 #   Copyright (C) 2008-2013 Team XBMC
5 #
6 #   This program is free software; you can redistribute it and/or modify
7 #   it under the terms of the GNU General Public License as published by
8 #   the Free Software Foundation; either version 2 of the License, or
9 #   (at your option) any later version.
10 #
11 #   This program is distributed in the hope that it will be useful,
12 #   but WITHOUT ANY WARRANTY; without even the implied warranty of
13 #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 #   GNU General Public License for more details.
15 #
16 #   You should have received a copy of the GNU General Public License along
17 #   with this program; if not, write to the Free Software Foundation, Inc.,
18 #   51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20 import sys
21 import traceback
22 import time
23 import struct
24 import threading
25 import os
26
27 if os.path.exists("../../lib/python"):
28     sys.path.append("../PS3 BD Remote")
29     sys.path.append("../../lib/python")
30     from bt.hid import HID
31     from bt.bt import bt_lookup_name
32     from xbmcclient import XBMCClient
33     from ps3 import sixaxis
34     from ps3_remote import process_keys as process_remote
35     try:
36         from ps3 import sixwatch
37     except Exception, e:
38         print "Failed to import sixwatch now disabled: " + str(e)
39         sixwatch = None
40
41     try:
42         import zeroconf
43     except:
44         zeroconf = None
45     ICON_PATH = "../../icons/"
46 else:
47     # fallback to system wide modules
48     from xbmc.bt.hid import HID
49     from xbmc.bt.bt import bt_lookup_name
50     from xbmc.xbmcclient import XBMCClient
51     from xbmc.ps3 import sixaxis
52     from xbmc.ps3_remote import process_keys as process_remote
53     from xbmc.defs import *
54     try:
55         from xbmc.ps3 import sixwatch
56     except Exception, e:
57         print "Failed to import sixwatch now disabled: " + str(e)
58         sixwatch = None
59     try:
60         import xbmc.zeroconf as zeroconf
61     except:
62         zeroconf = None
63
64
65 event_threads = []
66
67 def printerr():
68         trace = ""
69         exception = ""
70         exc_list = traceback.format_exception_only (sys.exc_type, sys.exc_value)
71         for entry in exc_list:
72                 exception += entry
73         tb_list = traceback.format_tb(sys.exc_info()[2])
74         for entry in tb_list:
75                 trace += entry
76         print("%s\n%s" % (exception, trace), "Script Error")
77
78
79 class StoppableThread ( threading.Thread ):
80     def __init__(self):
81         threading.Thread.__init__(self)
82         self._stop = False
83         self.set_timeout(0)
84
85     def stop_thread(self):
86         self._stop = True
87
88     def stop(self):
89         return self._stop
90
91     def close_sockets(self):
92         if self.isock:
93             try:
94                 self.isock.close()
95             except:
96                 pass
97         self.isock = None
98         if self.csock:
99             try:
100                 self.csock.close()
101             except:
102                 pass
103         self.csock = None
104         self.last_action = 0
105
106     def set_timeout(self, seconds):
107         self.timeout = seconds
108
109     def reset_timeout(self):
110         self.last_action = time.time()
111
112     def idle_time(self):
113         return time.time() - self.last_action
114
115     def timed_out(self):
116         if (time.time() - self.last_action) > self.timeout:
117             return True
118         else:
119             return False
120
121
122 class PS3SixaxisThread ( StoppableThread ):
123     def __init__(self, csock, isock, ipaddr="127.0.0.1"):
124         StoppableThread.__init__(self)
125         self.csock = csock
126         self.isock = isock
127         self.xbmc = XBMCClient(name="PS3 Sixaxis", icon_file=ICON_PATH + "/bluetooth.png", ip=ipaddr)
128         self.set_timeout(600)
129
130     def run(self):
131         six = sixaxis.sixaxis(self.xbmc, self.csock, self.isock)
132         self.xbmc.connect()
133         self.reset_timeout()
134         try:
135             while not self.stop():
136
137                 if self.timed_out():
138                     raise Exception("PS3 Sixaxis powering off, timed out")
139                 if self.idle_time() > 50:
140                     self.xbmc.connect()
141                 try:
142                     if six.process_socket(self.isock):
143                         self.reset_timeout()
144                 except Exception, e:
145                     print e
146                     break
147
148         except Exception, e:
149             printerr()
150         six.close()
151         self.close_sockets()
152
153
154 class PS3RemoteThread ( StoppableThread ):
155     def __init__(self, csock, isock, ipaddr="127.0.0.1"):
156         StoppableThread.__init__(self)
157         self.csock = csock
158         self.isock = isock
159         self.xbmc = XBMCClient(name="PS3 Blu-Ray Remote", icon_file=ICON_PATH + "/bluetooth.png", ip=ipaddr)
160         self.set_timeout(600)
161         self.services = []
162         self.current_xbmc = 0
163
164     def run(self):
165         self.xbmc.connect()
166         try:
167             # start the zeroconf thread if possible
168             try:
169                 self.zeroconf_thread = ZeroconfThread()
170                 self.zeroconf_thread.add_service('_xbmc-events._udp',
171                                              self.zeroconf_service_handler)
172                 self.zeroconf_thread.start()
173             except Exception, e:
174                 print str(e)
175
176             # main thread loop
177             while not self.stop():
178                 status = process_remote(self.isock, self.xbmc)
179
180                 if status == 2:   # 2 = socket read timeout
181                     if self.timed_out():
182                         raise Exception("PS3 Blu-Ray Remote powering off, "\
183                                             "timed out")
184                 elif status == 3: # 3 = ps and skip +
185                     self.next_xbmc()
186
187                 elif status == 4: # 4 = ps and skip -
188                     self.previous_xbmc()
189
190                 elif not status:  # 0 = keys are normally processed
191                     self.reset_timeout()
192
193         # process_remote() will raise an exception on read errors
194         except Exception, e:
195             print str(e)
196
197         self.zeroconf_thread.stop()
198         self.close_sockets()
199
200     def next_xbmc(self):
201         """
202         Connect to the next XBMC instance
203         """
204         self.current_xbmc = (self.current_xbmc + 1) % len( self.services )
205         self.reconnect()
206         return
207
208     def previous_xbmc(self):
209         """
210         Connect to the previous XBMC instance
211         """
212         self.current_xbmc -= 1
213         if self.current_xbmc < 0 :
214             self.current_xbmc = len( self.services ) - 1
215         self.reconnect()
216         return
217
218     def reconnect(self):
219         """
220         Reconnect to an XBMC instance based on self.current_xbmc
221         """
222         try:
223             service = self.services[ self.current_xbmc ]
224             print "Connecting to %s" % service['name']
225             self.xbmc.connect( service['address'], service['port'] )
226             self.xbmc.send_notification("PS3 Blu-Ray Remote", "New Connection", None)
227         except Exception, e:
228             print str(e)
229
230     def zeroconf_service_handler(self, event, service):
231         """
232         Zeroconf event handler
233         """
234         if event == zeroconf.SERVICE_FOUND:  # new xbmc service detected
235             self.services.append( service )
236
237         elif event == zeroconf.SERVICE_LOST: # xbmc service lost
238             try:
239                 # search for the service by name, since IP+port isn't available
240                 for s in self.services:
241                     # nuke it, if found
242                     if service['name'] == s['name']:
243                         self.services.remove(s)
244                         break
245             except:
246                 pass
247         return
248
249 class SixWatch(threading.Thread):
250     def __init__(self, mac):
251         threading.Thread.__init__(self)
252         self.mac = mac
253         self.daemon = True
254         self.start()
255     def run(self):
256       while True:
257         try:
258             sixwatch.main(self.mac)
259         except Exception, e:
260             print "Exception caught in sixwatch, restarting: " + str(e)
261
262 class ZeroconfThread ( threading.Thread ):
263     """
264     
265     """
266     def __init__(self):
267         threading.Thread.__init__(self)
268         self._zbrowser = None
269         self._services = []
270
271     def run(self):
272         if zeroconf:
273             # create zeroconf service browser
274             self._zbrowser = zeroconf.Browser()
275
276             # add the requested services
277             for service in self._services:
278                 self._zbrowser.add_service( service[0], service[1] )
279
280             # run the event loop
281             self._zbrowser.run()
282
283         return
284
285
286     def stop(self):
287         """
288         Stop the zeroconf browser
289         """
290         try:
291             self._zbrowser.stop()
292         except:
293             pass
294         return
295
296     def add_service(self, type, handler):
297         """
298         Add a new service to search for.
299         NOTE: Services must be added before thread starts.
300         """
301         self._services.append( [ type, handler ] )
302
303
304 def usage():
305     print """
306 PS3 Sixaxis / Blu-Ray Remote HID Server v0.1
307
308 Usage: ps3.py [bdaddress] [XBMC host]
309
310   bdaddress  => address of local bluetooth device to use (default: auto)
311                 (e.g. aa:bb:cc:dd:ee:ff)
312   ip address => IP address or hostname of the XBMC instance (default: localhost)
313                 (e.g. 192.168.1.110)
314 """
315
316 def start_hidd(bdaddr=None, ipaddr="127.0.0.1"):
317     devices = [ 'PLAYSTATION(R)3 Controller',
318                 'BD Remote Control' ]
319     hid = HID(bdaddr)
320     watch = None
321     if sixwatch:
322         try:
323             print "Starting USB sixwatch"
324             watch = SixWatch(hid.get_local_address())
325         except Exception, e:
326             print "Failed to initialize sixwatch" + str(e)
327             pass
328
329     while True:
330         if hid.listen():
331             (csock, addr) = hid.get_control_socket()
332             device_name = bt_lookup_name(addr[0])
333             if device_name == devices[0]:
334                 # handle PS3 controller
335                 handle_ps3_controller(hid, ipaddr)
336             elif device_name == devices[1]:
337                 # handle the PS3 remote
338                 handle_ps3_remote(hid, ipaddr)
339             else:
340                 print "Unknown Device: %s" % (device_name)
341
342 def handle_ps3_controller(hid, ipaddr):
343     print "Received connection from a Sixaxis PS3 Controller"
344     csock = hid.get_control_socket()[0]
345     isock = hid.get_interrupt_socket()[0]
346     sixaxis = PS3SixaxisThread(csock, isock, ipaddr)
347     add_thread(sixaxis)
348     sixaxis.start()
349     return
350
351 def handle_ps3_remote(hid, ipaddr):
352     print "Received connection from a PS3 Blu-Ray Remote"
353     csock = hid.get_control_socket()[0]
354     isock = hid.get_interrupt_socket()[0]
355     isock.settimeout(1)
356     remote = PS3RemoteThread(csock, isock, ipaddr)
357     add_thread(remote)
358     remote.start()
359     return
360
361 def add_thread(thread):
362     global event_threads
363     event_threads.append(thread)
364
365 def main():
366     if len(sys.argv)>3:
367         return usage()
368     bdaddr = ""
369     ipaddr = "127.0.0.1"
370     try:
371         for addr in sys.argv[1:]:
372             try:
373                 # ensure that the addr is of the format 'aa:bb:cc:dd:ee:ff'
374                 if "".join([ str(len(a)) for a in addr.split(":") ]) != "222222":
375                     raise Exception("Invalid format")
376                 bdaddr = addr
377                 print "Connecting to Bluetooth device: %s" % bdaddr
378             except Exception, e:
379                 try:
380                     ipaddr = addr
381                     print "Connecting to : %s" % ipaddr
382                 except:
383                     print str(e)
384                     return usage()
385     except Exception, e:
386         pass
387
388     print "Starting HID daemon"
389     start_hidd(bdaddr, ipaddr)
390
391 if __name__=="__main__":
392     try:
393         main()
394     finally:
395         for t in event_threads:
396             try:
397                 print "Waiting for thread "+str(t)+" to terminate"
398                 t.stop_thread()
399                 if t.isAlive():
400                     t.join()
401                 print "Thread "+str(t)+" terminated"
402
403             except Exception, e:
404                 print str(e)
405         pass
406