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