Merge pull request #26035 from garbear/donate-qr
[xbmc.git] / tools / EventClients / Clients / PS3SixaxisController / ps3d.py
blob32ed361b9edb69f941348ddb20bec18c3d96a7e6
1 #!/usr/bin/env python3
2 # -*- coding: utf-8 -*-
4 # Copyright (C) 2008-2013 Team XBMC
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.
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.
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.
20 import sys
21 import traceback
22 import time
23 import struct
24 import threading
25 import os
27 if os.path.exists("../../lib/python"):
28 sys.path.append("../PS3BDRemote")
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 as e:
38 print("Failed to import sixwatch now disabled: " + str(e))
39 sixwatch = None
41 try:
42 import zeroconf
43 except:
44 zeroconf = None
45 ICON_PATH = "../../icons/"
46 else:
47 # fallback to system wide modules
48 from kodi.bt.hid import HID
49 from kodi.bt.bt import bt_lookup_name
50 from kodi.xbmcclient import XBMCClient
51 from kodi.ps3 import sixaxis
52 from kodi.ps3_remote import process_keys as process_remote
53 from kodi.defs import *
54 try:
55 from kodi.ps3 import sixwatch
56 except Exception as e:
57 print("Failed to import sixwatch now disabled: " + str(e))
58 sixwatch = None
59 try:
60 import kodi.zeroconf as zeroconf
61 except:
62 zeroconf = None
65 event_threads = []
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")
79 class StoppableThread ( threading.Thread ):
80 def __init__(self):
81 threading.Thread.__init__(self)
82 self._stop = False
83 self.set_timeout(0)
85 def stop_thread(self):
86 self._stop = True
88 def stop(self):
89 return self._stop
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
106 def set_timeout(self, seconds):
107 self.timeout = seconds
109 def reset_timeout(self):
110 self.last_action = time.time()
112 def idle_time(self):
113 return time.time() - self.last_action
115 def timed_out(self):
116 if (time.time() - self.last_action) > self.timeout:
117 return True
118 else:
119 return False
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)
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():
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 as e:
145 print(e)
146 break
148 except Exception as e:
149 printerr()
150 six.close()
151 self.close_sockets()
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
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 as e:
174 print(str(e))
176 # main thread loop
177 while not self.stop():
178 status = process_remote(self.isock, self.xbmc)
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()
187 elif status == 4: # 4 = ps and skip -
188 self.previous_xbmc()
190 elif not status: # 0 = keys are normally processed
191 self.reset_timeout()
193 # process_remote() will raise an exception on read errors
194 except Exception as e:
195 print(str(e))
197 self.zeroconf_thread.stop()
198 self.close_sockets()
200 def next_xbmc(self):
202 Connect to the next XBMC instance
204 self.current_xbmc = (self.current_xbmc + 1) % len( self.services )
205 self.reconnect()
206 return
208 def previous_xbmc(self):
210 Connect to the previous XBMC instance
212 self.current_xbmc -= 1
213 if self.current_xbmc < 0 :
214 self.current_xbmc = len( self.services ) - 1
215 self.reconnect()
216 return
218 def reconnect(self):
220 Reconnect to an XBMC instance based on self.current_xbmc
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 as e:
228 print(str(e))
230 def zeroconf_service_handler(self, event, service):
232 Zeroconf event handler
234 if event == zeroconf.SERVICE_FOUND: # new xbmc service detected
235 self.services.append( service )
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
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 as e:
260 print("Exception caught in sixwatch, restarting: " + str(e))
262 class ZeroconfThread ( threading.Thread ):
266 def __init__(self):
267 threading.Thread.__init__(self)
268 self._zbrowser = None
269 self._services = []
271 def run(self):
272 if zeroconf:
273 # create zeroconf service browser
274 self._zbrowser = zeroconf.Browser()
276 # add the requested services
277 for service in self._services:
278 self._zbrowser.add_service( service[0], service[1] )
280 # run the event loop
281 self._zbrowser.run()
283 return
286 def stop(self):
288 Stop the zeroconf browser
290 try:
291 self._zbrowser.stop()
292 except:
293 pass
294 return
296 def add_service(self, type, handler):
298 Add a new service to search for.
299 NOTE: Services must be added before thread starts.
301 self._services.append( [ type, handler ] )
304 def usage():
305 print("""
306 PS3 Sixaxis / Blu-Ray Remote HID Server v0.1
308 Usage: ps3.py [bdaddress] [XBMC host]
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 """)
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 as e:
326 print("Failed to initialize sixwatch" + str(e))
327 pass
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))
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
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
361 def add_thread(thread):
362 global event_threads
363 event_threads.append(thread)
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 as e:
379 try:
380 ipaddr = addr
381 print("Connecting to : %s" % ipaddr)
382 except:
383 print(str(e))
384 return usage()
385 except Exception as e:
386 pass
388 print("Starting HID daemon")
389 start_hidd(bdaddr, ipaddr)
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")
403 except Exception as e:
404 print(str(e))
405 pass