1#!/usr/bin/env python3 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 20import sys 21import traceback 22import time 23import struct 24import threading 25import os 26 27if 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 40 41 try: 42 import zeroconf 43 except: 44 zeroconf = None 45 ICON_PATH = "../../icons/" 46else: 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 63 64 65event_threads = [] 66 67def 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 79class 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 122class 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 as e: 145 print(e) 146 break 147 148 except Exception as e: 149 printerr() 150 six.close() 151 self.close_sockets() 152 153 154class 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 as 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 as 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 as 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 249class 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)) 261 262class 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 304def usage(): 305 print(""" 306PS3 Sixaxis / Blu-Ray Remote HID Server v0.1 307 308Usage: 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 316def 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 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 342def 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 351def 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 361def add_thread(thread): 362 global event_threads 363 event_threads.append(thread) 364 365def 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 387 388 print("Starting HID daemon") 389 start_hidd(bdaddr, ipaddr) 390 391if __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 as e: 404 print(str(e)) 405 pass 406 407