aboutsummaryrefslogtreecommitdiff
path: root/tools/EventClients/Clients/PS3SixaxisController/ps3d.py
blob: f961cf8d33385fb2083a7620e5557fb9c2def008 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
#!/usr/bin/python
# -*- coding: utf-8 -*-

#   Copyright (C) 2008-2013 Team XBMC
#
#   This program is free software; you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation; either version 2 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License along
#   with this program; if not, write to the Free Software Foundation, Inc.,
#   51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

import sys
import traceback
import time
import struct
import threading
import os

if os.path.exists("../../lib/python"):
    sys.path.append("../PS3BDRemote")
    sys.path.append("../../lib/python")
    from bt.hid import HID
    from bt.bt import bt_lookup_name
    from xbmcclient import XBMCClient
    from ps3 import sixaxis
    from ps3_remote import process_keys as process_remote
    try:
        from ps3 import sixwatch
    except Exception as e:
        print("Failed to import sixwatch now disabled: " + str(e))
        sixwatch = None

    try:
        import zeroconf
    except:
        zeroconf = None
    ICON_PATH = "../../icons/"
else:
    # fallback to system wide modules
    from kodi.bt.hid import HID
    from kodi.bt.bt import bt_lookup_name
    from kodi.xbmcclient import XBMCClient
    from kodi.ps3 import sixaxis
    from kodi.ps3_remote import process_keys as process_remote
    from kodi.defs import *
    try:
        from kodi.ps3 import sixwatch
    except Exception as e:
        print("Failed to import sixwatch now disabled: " + str(e))
        sixwatch = None
    try:
        import kodi.zeroconf as zeroconf
    except:
        zeroconf = None


event_threads = []

def printerr():
	trace = ""
	exception = ""
	exc_list = traceback.format_exception_only (sys.exc_type, sys.exc_value)
	for entry in exc_list:
		exception += entry
	tb_list = traceback.format_tb(sys.exc_info()[2])
	for entry in tb_list:
		trace += entry
	print("%s\n%s" % (exception, trace), "Script Error")


class StoppableThread ( threading.Thread ):
    def __init__(self):
        threading.Thread.__init__(self)
        self._stop = False
        self.set_timeout(0)

    def stop_thread(self):
        self._stop = True

    def stop(self):
        return self._stop

    def close_sockets(self):
        if self.isock:
            try:
                self.isock.close()
            except:
                pass
        self.isock = None
        if self.csock:
            try:
                self.csock.close()
            except:
                pass
        self.csock = None
        self.last_action = 0

    def set_timeout(self, seconds):
        self.timeout = seconds

    def reset_timeout(self):
        self.last_action = time.time()

    def idle_time(self):
        return time.time() - self.last_action

    def timed_out(self):
        if (time.time() - self.last_action) > self.timeout:
            return True
        else:
            return False


class PS3SixaxisThread ( StoppableThread ):
    def __init__(self, csock, isock, ipaddr="127.0.0.1"):
        StoppableThread.__init__(self)
        self.csock = csock
        self.isock = isock
        self.xbmc = XBMCClient(name="PS3 Sixaxis", icon_file=ICON_PATH + "/bluetooth.png", ip=ipaddr)
        self.set_timeout(600)

    def run(self):
        six = sixaxis.sixaxis(self.xbmc, self.csock, self.isock)
        self.xbmc.connect()
        self.reset_timeout()
        try:
            while not self.stop():

                if self.timed_out():
                    raise Exception("PS3 Sixaxis powering off, timed out")
                if self.idle_time() > 50:
                    self.xbmc.connect()
                try:
                    if six.process_socket(self.isock):
                        self.reset_timeout()
                except Exception as e:
                    print(e)
                    break

        except Exception as e:
            printerr()
        six.close()
        self.close_sockets()


class PS3RemoteThread ( StoppableThread ):
    def __init__(self, csock, isock, ipaddr="127.0.0.1"):
        StoppableThread.__init__(self)
        self.csock = csock
        self.isock = isock
        self.xbmc = XBMCClient(name="PS3 Blu-Ray Remote", icon_file=ICON_PATH + "/bluetooth.png", ip=ipaddr)
        self.set_timeout(600)
        self.services = []
        self.current_xbmc = 0

    def run(self):
        self.xbmc.connect()
        try:
            # start the zeroconf thread if possible
            try:
                self.zeroconf_thread = ZeroconfThread()
                self.zeroconf_thread.add_service('_xbmc-events._udp',
                                             self.zeroconf_service_handler)
                self.zeroconf_thread.start()
            except Exception as e:
                print(str(e))

            # main thread loop
            while not self.stop():
                status = process_remote(self.isock, self.xbmc)

                if status == 2:   # 2 = socket read timeout
                    if self.timed_out():
                        raise Exception("PS3 Blu-Ray Remote powering off, "\
                                            "timed out")
                elif status == 3: # 3 = ps and skip +
                    self.next_xbmc()

                elif status == 4: # 4 = ps and skip -
                    self.previous_xbmc()

                elif not status:  # 0 = keys are normally processed
                    self.reset_timeout()

        # process_remote() will raise an exception on read errors
        except Exception as e:
            print(str(e))

        self.zeroconf_thread.stop()
        self.close_sockets()

    def next_xbmc(self):
        """
        Connect to the next XBMC instance
        """
        self.current_xbmc = (self.current_xbmc + 1) % len( self.services )
        self.reconnect()
        return

    def previous_xbmc(self):
        """
        Connect to the previous XBMC instance
        """
        self.current_xbmc -= 1
        if self.current_xbmc < 0 :
            self.current_xbmc = len( self.services ) - 1
        self.reconnect()
        return

    def reconnect(self):
        """
        Reconnect to an XBMC instance based on self.current_xbmc
        """
        try:
            service = self.services[ self.current_xbmc ]
            print("Connecting to %s" % service['name'])
            self.xbmc.connect( service['address'], service['port'] )
            self.xbmc.send_notification("PS3 Blu-Ray Remote", "New Connection", None)
        except Exception as e:
            print(str(e))

    def zeroconf_service_handler(self, event, service):
        """
        Zeroconf event handler
        """
        if event == zeroconf.SERVICE_FOUND:  # new xbmc service detected
            self.services.append( service )

        elif event == zeroconf.SERVICE_LOST: # xbmc service lost
            try:
                # search for the service by name, since IP+port isn't available
                for s in self.services:
                    # nuke it, if found
                    if service['name'] == s['name']:
                        self.services.remove(s)
                        break
            except:
                pass
        return

class SixWatch(threading.Thread):
    def __init__(self, mac):
        threading.Thread.__init__(self)
        self.mac = mac
        self.daemon = True
        self.start()
    def run(self):
      while True:
        try:
            sixwatch.main(self.mac)
        except Exception as e:
            print("Exception caught in sixwatch, restarting: " + str(e))

class ZeroconfThread ( threading.Thread ):
    """

    """
    def __init__(self):
        threading.Thread.__init__(self)
        self._zbrowser = None
        self._services = []

    def run(self):
        if zeroconf:
            # create zeroconf service browser
            self._zbrowser = zeroconf.Browser()

            # add the requested services
            for service in self._services:
                self._zbrowser.add_service( service[0], service[1] )

            # run the event loop
            self._zbrowser.run()

        return


    def stop(self):
        """
        Stop the zeroconf browser
        """
        try:
            self._zbrowser.stop()
        except:
            pass
        return

    def add_service(self, type, handler):
        """
        Add a new service to search for.
        NOTE: Services must be added before thread starts.
        """
        self._services.append( [ type, handler ] )


def usage():
    print("""
PS3 Sixaxis / Blu-Ray Remote HID Server v0.1

Usage: ps3.py [bdaddress] [XBMC host]

  bdaddress  => address of local bluetooth device to use (default: auto)
                (e.g. aa:bb:cc:dd:ee:ff)
  ip address => IP address or hostname of the XBMC instance (default: localhost)
                (e.g. 192.168.1.110)
""")

def start_hidd(bdaddr=None, ipaddr="127.0.0.1"):
    devices = [ 'PLAYSTATION(R)3 Controller',
                'BD Remote Control' ]
    hid = HID(bdaddr)
    watch = None
    if sixwatch:
        try:
            print("Starting USB sixwatch")
            watch = SixWatch(hid.get_local_address())
        except Exception as e:
            print("Failed to initialize sixwatch" + str(e))
            pass

    while True:
        if hid.listen():
            (csock, addr) = hid.get_control_socket()
            device_name = bt_lookup_name(addr[0])
            if device_name == devices[0]:
                # handle PS3 controller
                handle_ps3_controller(hid, ipaddr)
            elif device_name == devices[1]:
                # handle the PS3 remote
                handle_ps3_remote(hid, ipaddr)
            else:
                print("Unknown Device: %s" % (device_name))

def handle_ps3_controller(hid, ipaddr):
    print("Received connection from a Sixaxis PS3 Controller")
    csock = hid.get_control_socket()[0]
    isock = hid.get_interrupt_socket()[0]
    sixaxis = PS3SixaxisThread(csock, isock, ipaddr)
    add_thread(sixaxis)
    sixaxis.start()
    return

def handle_ps3_remote(hid, ipaddr):
    print("Received connection from a PS3 Blu-Ray Remote")
    csock = hid.get_control_socket()[0]
    isock = hid.get_interrupt_socket()[0]
    isock.settimeout(1)
    remote = PS3RemoteThread(csock, isock, ipaddr)
    add_thread(remote)
    remote.start()
    return

def add_thread(thread):
    global event_threads
    event_threads.append(thread)

def main():
    if len(sys.argv)>3:
        return usage()
    bdaddr = ""
    ipaddr = "127.0.0.1"
    try:
        for addr in sys.argv[1:]:
            try:
                # ensure that the addr is of the format 'aa:bb:cc:dd:ee:ff'
                if "".join([ str(len(a)) for a in addr.split(":") ]) != "222222":
                    raise Exception("Invalid format")
                bdaddr = addr
                print("Connecting to Bluetooth device: %s" % bdaddr)
            except Exception as e:
                try:
                    ipaddr = addr
                    print("Connecting to : %s" % ipaddr)
                except:
                    print(str(e))
                    return usage()
    except Exception as e:
        pass

    print("Starting HID daemon")
    start_hidd(bdaddr, ipaddr)

if __name__=="__main__":
    try:
        main()
    finally:
        for t in event_threads:
            try:
                print("Waiting for thread "+str(t)+" to terminate")
                t.stop_thread()
                if t.isAlive():
                    t.join()
                print("Thread "+str(t)+" terminated")

            except Exception as e:
                print(str(e))
        pass