diff options
Diffstat (limited to 'tools/EventClients/lib/python')
-rw-r--r-- | tools/EventClients/lib/python/__init__.py | 2 | ||||
-rw-r--r-- | tools/EventClients/lib/python/bt/__init__.py | 2 | ||||
-rw-r--r-- | tools/EventClients/lib/python/bt/bt.py | 82 | ||||
-rw-r--r-- | tools/EventClients/lib/python/bt/hid.py | 81 | ||||
-rw-r--r-- | tools/EventClients/lib/python/ps3/__init__.py | 2 | ||||
-rw-r--r-- | tools/EventClients/lib/python/ps3/keymaps.py | 81 | ||||
-rw-r--r-- | tools/EventClients/lib/python/ps3/sixaxis.py | 372 | ||||
-rwxr-xr-x | tools/EventClients/lib/python/ps3/sixpair.py | 114 | ||||
-rwxr-xr-x | tools/EventClients/lib/python/ps3/sixwatch.py | 31 | ||||
-rw-r--r-- | tools/EventClients/lib/python/xbmcclient.py | 639 | ||||
-rw-r--r-- | tools/EventClients/lib/python/zeroconf.py | 160 |
11 files changed, 1566 insertions, 0 deletions
diff --git a/tools/EventClients/lib/python/__init__.py b/tools/EventClients/lib/python/__init__.py new file mode 100644 index 0000000..7deecc4 --- /dev/null +++ b/tools/EventClients/lib/python/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +# File intentionally left blank diff --git a/tools/EventClients/lib/python/bt/__init__.py b/tools/EventClients/lib/python/bt/__init__.py new file mode 100644 index 0000000..7deecc4 --- /dev/null +++ b/tools/EventClients/lib/python/bt/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +# File intentionally left blank diff --git a/tools/EventClients/lib/python/bt/bt.py b/tools/EventClients/lib/python/bt/bt.py new file mode 100644 index 0000000..979ebf2 --- /dev/null +++ b/tools/EventClients/lib/python/bt/bt.py @@ -0,0 +1,82 @@ +# -*- 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. + +BLUEZ=0 + +try: + import bluetooth + BLUEZ=1 +except: + try: + import lightblue + except: + print("ERROR: You need to have either LightBlue or PyBluez installed\n"\ + " in order to use this program.") + print("- PyBluez (Linux / Windows XP) http://org.csail.mit.edu/pybluez/") + print("- LightBlue (Mac OS X / Linux) http://lightblue.sourceforge.net/") + exit() + +def bt_create_socket(): + if BLUEZ: + sock = bluetooth.BluetoothSocket(bluetooth.L2CAP) + else: + sock = lightblue.socket(lightblue.L2CAP) + return sock + +def bt_create_rfcomm_socket(): + if BLUEZ: + sock = bluetooth.BluetoothSocket( bluetooth.RFCOMM ) + sock.bind(("",bluetooth.PORT_ANY)) + else: + sock = lightblue.socket(lightblue.RFCOMM) + sock.bind(("",0)) + return sock + +def bt_discover_devices(): + if BLUEZ: + nearby = bluetooth.discover_devices() + else: + nearby = lightblue.finddevices() + return nearby + +def bt_lookup_name(bdaddr): + if BLUEZ: + bname = bluetooth.lookup_name( bdaddr ) + else: + bname = bdaddr[1] + return bname + +def bt_lookup_addr(bdaddr): + if BLUEZ: + return bdaddr + else: + return bdaddr[0] + +def bt_advertise(name, uuid, socket): + if BLUEZ: + bluetooth.advertise_service( socket, name, + service_id = uuid, + service_classes = [ uuid, bluetooth.SERIAL_PORT_CLASS ], + profiles = [ bluetooth.SERIAL_PORT_PROFILE ] ) + else: + lightblue.advertise(name, socket, lightblue.RFCOMM) + +def bt_stop_advertising(socket): + if BLUEZ: + stop_advertising(socket) + else: + lightblue.stopadvertise(socket) diff --git a/tools/EventClients/lib/python/bt/hid.py b/tools/EventClients/lib/python/bt/hid.py new file mode 100644 index 0000000..c065054 --- /dev/null +++ b/tools/EventClients/lib/python/bt/hid.py @@ -0,0 +1,81 @@ +# -*- 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. + +from bluetooth import * +import fcntl +import bluetooth._bluetooth as _bt +import array + +class HID: + def __init__(self, bdaddress=None): + self.cport = 0x11 # HID's control PSM + self.iport = 0x13 # HID' interrupt PSM + self.backlog = 1 + + self.address = "" + if bdaddress: + self.address = bdaddress + + # create the HID control socket + self.csock = BluetoothSocket( L2CAP ) + self.csock.bind((self.address, self.cport)) + set_l2cap_mtu(self.csock, 64) + self.csock.settimeout(2) + self.csock.listen(self.backlog) + + # create the HID interrupt socket + self.isock = BluetoothSocket( L2CAP ) + self.isock.bind((self.address, self.iport)) + set_l2cap_mtu(self.isock, 64) + self.isock.settimeout(2) + self.isock.listen(self.backlog) + + self.connected = False + + + def listen(self): + try: + (self.client_csock, self.caddress) = self.csock.accept() + print("Accepted Control connection from %s" % self.caddress[0]) + (self.client_isock, self.iaddress) = self.isock.accept() + print("Accepted Interrupt connection from %s" % self.iaddress[0]) + self.connected = True + return True + except Exception as e: + self.connected = False + return False + + def get_local_address(self): + hci = BluetoothSocket( HCI ) + fd = hci.fileno() + buf = array.array('B', [0] * 96) + fcntl.ioctl(fd, _bt.HCIGETDEVINFO, buf, 1) + data = struct.unpack_from("H8s6B", buf.tostring()) + return data[2:8][::-1] + + def get_control_socket(self): + if self.connected: + return (self.client_csock, self.caddress) + else: + return None + + + def get_interrupt_socket(self): + if self.connected: + return (self.client_isock, self.iaddress) + else: + return None diff --git a/tools/EventClients/lib/python/ps3/__init__.py b/tools/EventClients/lib/python/ps3/__init__.py new file mode 100644 index 0000000..7deecc4 --- /dev/null +++ b/tools/EventClients/lib/python/ps3/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +# File intentionally left blank diff --git a/tools/EventClients/lib/python/ps3/keymaps.py b/tools/EventClients/lib/python/ps3/keymaps.py new file mode 100644 index 0000000..99c6e04 --- /dev/null +++ b/tools/EventClients/lib/python/ps3/keymaps.py @@ -0,0 +1,81 @@ +# -*- 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. + +# PS3 Remote and Controller Keymaps + +keymap_remote = { + "16": 'power' ,#EJECT + "64": None ,#AUDIO + "65": None ,#ANGLE + "63": 'subtitle' ,#SUBTITLE + "0f": None ,#CLEAR + "28": None ,#TIME + + "00": 'one' ,#1 + "01": 'two' ,#2 + "02": 'three' ,#3 + "03": 'four' ,#4 + "04": 'five' ,#5 + "05": 'six' ,#6 + "06": 'seven' ,#7 + "07": 'eight' ,#8 + "08": 'nine' ,#9 + "09": 'zero' ,#0 + + "81": 'mytv' ,#RED + "82": 'mymusic' ,#GREEN + "80": 'mypictures' ,#BLUE + "83": 'myvideo' ,#YELLOW + + "70": 'display' ,#DISPLAY + "1a": None ,#TOP MENU + "40": 'menu' ,#POP UP/MENU + "0e": None ,#RETURN + + "5c": 'menu' ,#OPTIONS/TRIANGLE + "5d": 'back' ,#BACK/CIRCLE + "5e": 'info' ,#X + "5f": 'title' ,#VIEW/SQUARE + + "54": 'up' ,#UP + "55": 'right' ,#RIGHT + "56": 'down' ,#DOWN + "57": 'left' ,#LEFT + "0b": 'select' ,#ENTER + + "5a": 'volumeplus' ,#L1 + "58": 'volumeminus' ,#L2 + "51": 'Mute' ,#L3 + "5b": 'pageplus' ,#R1 + "59": 'pageminus' ,#R2 + "52": None ,#R3 + + "43": None ,#PLAYSTATION + "50": None ,#SELECT + "53": None ,#START + + "33": 'reverse' ,#<-SCAN + "34": 'forward' ,# SCAN-> + "30": 'skipminus' ,#PREV + "31": 'skipplus' ,#NEXT + "60": None ,#<-SLOW/STEP + "61": None ,# SLOW/STEP-> + "32": 'play' ,#PLAY + "38": 'stop' ,#STOP + "39": 'pause' ,#PAUSE + } + diff --git a/tools/EventClients/lib/python/ps3/sixaxis.py b/tools/EventClients/lib/python/ps3/sixaxis.py new file mode 100644 index 0000000..b4899f6 --- /dev/null +++ b/tools/EventClients/lib/python/ps3/sixaxis.py @@ -0,0 +1,372 @@ +#!/usr/bin/env python3 +# -*- 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 time +import sys +import struct +import math +import binascii +from bluetooth import set_l2cap_mtu + +SX_SELECT = 1 << 0 +SX_L3 = 1 << 1 +SX_R3 = 1 << 2 +SX_START = 1 << 3 +SX_DUP = 1 << 4 +SX_DRIGHT = 1 << 5 +SX_DDOWN = 1 << 6 +SX_DLEFT = 1 << 7 +SX_L2 = 1 << 8 +SX_R2 = 1 << 9 +SX_L1 = 1 << 10 +SX_R1 = 1 << 11 +SX_TRIANGLE = 1 << 12 +SX_CIRCLE = 1 << 13 +SX_X = 1 << 14 +SX_SQUARE = 1 << 15 +SX_POWER = 1 << 16 + +SX_LSTICK_X = 0 +SX_LSTICK_Y = 1 +SX_RSTICK_X = 2 +SX_RSTICK_Y = 3 + +# (map, key, amount index, axis) +keymap_sixaxis = { + SX_X : ('XG', 'A', 0, 0), + SX_CIRCLE : ('XG', 'B', 0, 0), + SX_SQUARE : ('XG', 'X', 0, 0), + SX_TRIANGLE : ('XG', 'Y', 0, 0), + + SX_DUP : ('XG', 'dpadup', 0, 0), + SX_DDOWN : ('XG', 'dpaddown', 0, 0), + SX_DLEFT : ('XG', 'dpadleft', 0, 0), + SX_DRIGHT : ('XG', 'dpadright', 0, 0), + + SX_START : ('XG', 'start', 0, 0), + SX_SELECT : ('XG', 'back', 0, 0), + + SX_R1 : ('XG', 'white', 0, 0), + SX_R2 : ('XG', 'rightanalogtrigger', 6, 1), + SX_L2 : ('XG', 'leftanalogtrigger', 5, 1), + SX_L1 : ('XG', 'black', 0, 0), + + SX_L3 : ('XG', 'leftthumbbutton', 0, 0), + SX_R3 : ('XG', 'rightthumbbutton', 0, 0), +} + +# (data index, left map, left action, right map, right action) +axismap_sixaxis = { + SX_LSTICK_X : ('XG', 'leftthumbstickleft' , 'leftthumbstickright'), + SX_LSTICK_Y : ('XG', 'leftthumbstickup' , 'leftthumbstickdown'), + SX_RSTICK_X : ('XG', 'rightthumbstickleft', 'rightthumbstickright'), + SX_RSTICK_Y : ('XG', 'rightthumbstickup' , 'rightthumbstickdown'), +} + +# to make sure all combination keys are checked first +# we sort the keymap's button codes in reverse order +# this guarantees that any bit combined button code +# will be processed first +keymap_sixaxis_keys = keymap_sixaxis.keys() +keymap_sixaxis_keys.sort() +keymap_sixaxis_keys.reverse() + +def getkeys(bflags): + keys = []; + for k in keymap_sixaxis_keys: + if (k & bflags) == k: + keys.append(k) + bflags = bflags & ~k + return keys; + + +def normalize(val): + upperlimit = 65281 + lowerlimit = 2 + val_range = upperlimit - lowerlimit + offset = 10000 + + val = (val + val_range / 2) % val_range + upperlimit -= offset + lowerlimit += offset + + if val < lowerlimit: + val = lowerlimit + if val > upperlimit: + val = upperlimit + + val = ((float(val) - offset) / (float(upperlimit) - + lowerlimit)) * 65535.0 + if val <= 0: + val = 1 + return val + +def normalize_axis(val, deadzone): + + val = float(val) - 127.5 + val = val / 127.5 + + if abs(val) < deadzone: + return 0.0 + + if val > 0.0: + val = (val - deadzone) / (1.0 - deadzone) + else: + val = (val + deadzone) / (1.0 - deadzone) + + return 65536.0 * val + +def normalize_angle(val, valrange): + valrange *= 2 + + val = val / valrange + if val > 1.0: + val = 1.0 + if val < -1.0: + val = -1.0 + return (val + 0.5) * 65535.0 + +def average(array): + val = 0 + for i in array: + val += i + return val / len(array) + +def smooth(arr, val): + cnt = len(arr) + arr.insert(0, val) + arr.pop(cnt) + return average(arr) + +def set_l2cap_mtu2(sock, mtu): + SOL_L2CAP = 6 + L2CAP_OPTIONS = 1 + + s = sock.getsockopt (SOL_L2CAP, L2CAP_OPTIONS, 12) + o = list( struct.unpack ("HHHBBBH", s) ) + o[0] = o[1] = mtu + s = struct.pack ("HHHBBBH", *o) + try: + sock.setsockopt (SOL_L2CAP, L2CAP_OPTIONS, s) + except: + print("Warning: Unable to set mtu") + +class sixaxis(): + + def __init__(self, xbmc, control_sock, interrupt_sock): + + self.xbmc = xbmc + self.num_samples = 16 + self.sumx = [0] * self.num_samples + self.sumy = [0] * self.num_samples + self.sumr = [0] * self.num_samples + self.axis_amount = [0, 0, 0, 0] + + self.released = set() + self.pressed = set() + self.pending = set() + self.held = set() + self.psflags = 0 + self.psdown = 0 + self.mouse_enabled = 0 + + set_l2cap_mtu2(control_sock, 64) + set_l2cap_mtu2(interrupt_sock, 64) + time.sleep(0.25) # If we ask to quickly here, it sometimes doesn't start + + # sixaxis needs this to enable it + # 0x53 => HIDP_TRANS_SET_REPORT | HIDP_DATA_RTYPE_FEATURE + control_sock.send("\x53\xf4\x42\x03\x00\x00") + data = control_sock.recv(1) + # This command will turn on the gyro and set the leds + # I wonder if turning on the gyro makes it draw more current?? + # it's probably a flag somewhere in the following command + + # HID Command: HIDP_TRANS_SET_REPORT | HIDP_DATA_RTYPE_OUTPUT + # HID Report:1 + bytes = [0x52, 0x1] + bytes.extend([0x00, 0x00, 0x00]) + bytes.extend([0xFF, 0x72]) + bytes.extend([0x00, 0x00, 0x00, 0x00]) + bytes.extend([0x02]) # 0x02 LED1, 0x04 LED2 ... 0x10 LED4 + # The following sections should set the blink frequency of + # the leds on the controller, but i've not figured out how. + # These values where suggested in a mailing list, but no explanation + # for how they should be combined to the 5 bytes per led + #0xFF = 0.5Hz + #0x80 = 1Hz + #0x40 = 2Hz + bytes.extend([0xFF, 0x00, 0x01, 0x00, 0x01]) #LED4 [0xff, 0xff, 0x10, 0x10, 0x10] + bytes.extend([0xFF, 0x00, 0x01, 0x00, 0x01]) #LED3 [0xff, 0x40, 0x08, 0x10, 0x10] + bytes.extend([0xFF, 0x00, 0x01, 0x00, 0x01]) #LED2 [0xff, 0x00, 0x10, 0x30, 0x30] + bytes.extend([0xFF, 0x00, 0x01, 0x00, 0x01]) #LED1 [0xff, 0x00, 0x10, 0x40, 0x10] + bytes.extend([0x00, 0x00, 0x00, 0x00, 0x00]) + bytes.extend([0x00, 0x00, 0x00, 0x00, 0x00]) + + control_sock.send(struct.pack("42B", *bytes)) + data = control_sock.recv(1) + + def __del__(self): + self.close() + + def close(self): + + for key in (self.held | self.pressed): + (mapname, action, amount, axis) = keymap_sixaxis[key] + self.xbmc.send_button_state(map=mapname, button=action, amount=0, down=0, axis=axis) + self.held = set() + self.pressed = set() + + + def process_socket(self, isock): + data = isock.recv(50) + if data == None: + return False + return self.process_data(data) + + + def process_data(self, data): + if len(data) < 3: + return False + + # make sure this is the correct report + if struct.unpack("BBB", data[0:3]) != (0xa1, 0x01, 0x00): + return False + + if len(data) >= 48: + v1 = struct.unpack("h", data[42:44]) + v2 = struct.unpack("h", data[44:46]) + v3 = struct.unpack("h", data[46:48]) + else: + v1 = [0,0] + v2 = [0,0] + v3 = [0,0] + + if len(data) >= 50: + v4 = struct.unpack("h", data[48:50]) + else: + v4 = [0,0] + + ax = float(v1[0]) + ay = float(v2[0]) + az = float(v3[0]) + rz = float(v4[0]) + at = math.sqrt(ax*ax + ay*ay + az*az) + + bflags = struct.unpack("<I", data[3:7])[0] + if len(data) > 27: + pressure = struct.unpack("BBBBBBBBBBBB", data[15:27]) + else: + pressure = [0,0,0,0,0,0,0,0,0,0,0,0,0] + + roll = -math.atan2(ax, math.sqrt(ay*ay + az*az)) + pitch = math.atan2(ay, math.sqrt(ax*ax + az*az)) + + pitch -= math.radians(20); + + xpos = normalize_angle(roll, math.radians(30)) + ypos = normalize_angle(pitch, math.radians(30)) + + + axis = struct.unpack("BBBB", data[7:11]) + return self.process_input(bflags, pressure, axis, xpos, ypos) + + def process_input(self, bflags, pressure, axis, xpos, ypos): + + xval = smooth(self.sumx, xpos) + yval = smooth(self.sumy, ypos) + + analog = False + for i in range(4): + config = axismap_sixaxis[i] + self.axis_amount[i] = self.send_singleaxis(axis[i], self.axis_amount[i], config[0], config[1], config[2]) + if self.axis_amount[i] != 0: + analog = True + + # send the mouse position to xbmc + if self.mouse_enabled == 1: + self.xbmc.send_mouse_position(xval, yval) + + if (bflags & SX_POWER) == SX_POWER: + if self.psdown: + if (time.time() - self.psdown) > 5: + + for key in (self.held | self.pressed): + (mapname, action, amount, axis) = keymap_sixaxis[key] + self.xbmc.send_button_state(map=mapname, button=action, amount=0, down=0, axis=axis) + + raise Exception("PS3 Sixaxis powering off, user request") + else: + self.psdown = time.time() + else: + if self.psdown: + self.mouse_enabled = 1 - self.mouse_enabled + self.psdown = 0 + + keys = set(getkeys(bflags)) + self.released = (self.pressed | self.held) - keys + self.held = (self.pressed | self.held) - self.released + self.pressed = (keys - self.held) & self.pending + self.pending = (keys - self.held) + + for key in self.released: + (mapname, action, amount, axis) = keymap_sixaxis[key] + self.xbmc.send_button_state(map=mapname, button=action, amount=0, down=0, axis=axis) + + for key in self.held: + (mapname, action, amount, axis) = keymap_sixaxis[key] + if amount > 0: + amount = pressure[amount-1] * 256 + self.xbmc.send_button_state(map=mapname, button=action, amount=amount, down=1, axis=axis) + + for key in self.pressed: + (mapname, action, amount, axis) = keymap_sixaxis[key] + if amount > 0: + amount = pressure[amount-1] * 256 + self.xbmc.send_button_state(map=mapname, button=action, amount=amount, down=1, axis=axis) + + if analog or keys or self.mouse_enabled: + return True + else: + return False + + + def send_singleaxis(self, axis, last_amount, mapname, action_min, action_pos): + amount = normalize_axis(axis, 0.30) + if last_amount < 0: + last_action = action_min + elif last_amount > 0: + last_action = action_pos + else: + last_action = None + + if amount < 0: + new_action = action_min + elif amount > 0: + new_action = action_pos + else: + new_action = None + + if last_action and new_action != last_action: + self.xbmc.send_button_state(map=mapname, button=last_action, amount=0, axis=1) + + if new_action and amount != last_amount: + self.xbmc.send_button_state(map=mapname, button=new_action, amount=abs(amount), axis=1) + + return amount diff --git a/tools/EventClients/lib/python/ps3/sixpair.py b/tools/EventClients/lib/python/ps3/sixpair.py new file mode 100755 index 0000000..01f11c8 --- /dev/null +++ b/tools/EventClients/lib/python/ps3/sixpair.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import sys +import usb + +vendor = 0x054c +product = 0x0268 +timeout = 5000 +passed_value = 0x03f5 + +def find_sixaxes(): + res = [] + for bus in usb.busses(): + for dev in bus.devices: + if dev.idVendor == vendor and dev.idProduct == product: + res.append(dev) + return res + +def find_interface(dev): + for cfg in dev.configurations: + for itf in cfg.interfaces: + for alt in itf: + if alt.interfaceClass == 3: + return alt + raise Exception("Unable to find interface") + +def mac_to_string(mac): + return "%02x:%02x:%02x:%02x:%02x:%02x" % (mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]) + +def set_pair_filename(dirname, filename, mac): + for bus in usb.busses(): + if int(bus.dirname) == int(dirname): + for dev in bus.devices: + if int(dev.filename) == int(filename): + if dev.idVendor == vendor and dev.idProduct == product: + update_pair(dev, mac) + return + else: + raise Exception("Device is not a sixaxis") + raise Exception("Device not found") + + +def set_pair(dev, mac): + itf = find_interface(dev) + handle = dev.open() + + msg = (0x01, 0x00) + mac; + + try: + handle.detachKernelDriver(itf.interfaceNumber) + except usb.USBError: + pass + + handle.claimInterface(itf.interfaceNumber) + try: + handle.controlMsg(usb.ENDPOINT_OUT | usb.TYPE_CLASS | usb.RECIP_INTERFACE + , usb.REQ_SET_CONFIGURATION, msg, passed_value, itf.interfaceNumber, timeout) + finally: + handle.releaseInterface() + + +def get_pair(dev): + itf = find_interface(dev) + handle = dev.open() + + try: + handle.detachKernelDriver(itf.interfaceNumber) + except usb.USBError: + pass + + handle.claimInterface(itf.interfaceNumber) + try: + msg = handle.controlMsg(usb.ENDPOINT_IN | usb.TYPE_CLASS | usb.RECIP_INTERFACE + , usb.REQ_CLEAR_FEATURE, 8, passed_value, itf.interfaceNumber, timeout) + finally: + handle.releaseInterface() + return msg[2:8] + +def set_pair_all(mac): + devs = find_sixaxes() + for dev in devs: + update_pair(dev, mac) + +def update_pair(dev, mac): + old = get_pair(dev) + if old != mac: + print("Re-pairing sixaxis from:" + mac_to_string(old) + " to:" + mac_to_string(mac)) + set_pair(dev, mac) + +if __name__=="__main__": + devs = find_sixaxes() + + mac = None + if len(sys.argv) > 1: + try: + mac = sys.argv[1].split(':') + mac = tuple([int(x, 16) for x in mac]) + if len(mac) != 6: + print("Invalid length of HCI address, should be 6 parts") + mac = None + except: + print("Failed to parse HCI address") + mac = None + + for dev in devs: + if mac: + update_pair(dev, mac) + else: + print("Found sixaxis paired to: " + mac_to_string(get_pair(dev))) + + + + diff --git a/tools/EventClients/lib/python/ps3/sixwatch.py b/tools/EventClients/lib/python/ps3/sixwatch.py new file mode 100755 index 0000000..553829b --- /dev/null +++ b/tools/EventClients/lib/python/ps3/sixwatch.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import pyudev +import sixpair +import threading + +vendor = 0x054c +product = 0x0268 + + +def main(mac): + context = pyudev.Context() + monitor = pyudev.Monitor.from_netlink(context) + monitor.filter_by(subsystem="usb") + for action, device in monitor: + if 'ID_VENDOR_ID' in device and 'ID_MODEL_ID' in device: + if device['ID_VENDOR_ID'] == '054c' and device['ID_MODEL_ID'] == '0268': + if action == 'add': + print("Detected sixaxis connected by usb") + try: + sixpair.set_pair_filename(device.attributes['busnum'], device.attributes['devnum'], mac) + except Exception as e: + print("Failed to check pairing of sixaxis: " + str(e)) + pass + + + +if __name__=="__main__": + main((0,0,0,0,0,0)) + diff --git a/tools/EventClients/lib/python/xbmcclient.py b/tools/EventClients/lib/python/xbmcclient.py new file mode 100644 index 0000000..548c443 --- /dev/null +++ b/tools/EventClients/lib/python/xbmcclient.py @@ -0,0 +1,639 @@ +#!/usr/bin/env python3 +# -*- 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. + +""" +Implementation of XBMC's UDP based input system. + +A set of classes that abstract the various packets that the event server +currently supports. In addition, there's also a class, XBMCClient, that +provides functions that sends the various packets. Use XBMCClient if you +don't need complete control over packet structure. + +The basic workflow involves: + +1. Send a HELO packet +2. Send x number of valid packets +3. Send a BYE packet + +IMPORTANT NOTE ABOUT TIMEOUTS: +A client is considered to be timed out if XBMC doesn't received a packet +at least once every 60 seconds. To "ping" XBMC with an empty packet use +PacketPING or XBMCClient.ping(). See the documentation for details. +""" + +from __future__ import unicode_literals, print_function, absolute_import, division + +__author__ = "d4rk@xbmc.org" +__version__ = "0.1.0" + +import sys +if sys.version_info.major == 2: + str = unicode +from struct import pack +from socket import socket, AF_INET, SOCK_DGRAM, SOL_SOCKET, SO_BROADCAST +import time + +MAX_PACKET_SIZE = 1024 +HEADER_SIZE = 32 +MAX_PAYLOAD_SIZE = MAX_PACKET_SIZE - HEADER_SIZE +UNIQUE_IDENTIFICATION = (int)(time.time()) + +PT_HELO = 0x01 +PT_BYE = 0x02 +PT_BUTTON = 0x03 +PT_MOUSE = 0x04 +PT_PING = 0x05 +PT_BROADCAST = 0x06 +PT_NOTIFICATION = 0x07 +PT_BLOB = 0x08 +PT_LOG = 0x09 +PT_ACTION = 0x0A +PT_DEBUG = 0xFF + +ICON_NONE = 0x00 +ICON_JPEG = 0x01 +ICON_PNG = 0x02 +ICON_GIF = 0x03 + +BT_USE_NAME = 0x01 +BT_DOWN = 0x02 +BT_UP = 0x04 +BT_USE_AMOUNT = 0x08 +BT_QUEUE = 0x10 +BT_NO_REPEAT = 0x20 +BT_VKEY = 0x40 +BT_AXIS = 0x80 +BT_AXISSINGLE = 0x100 + +MS_ABSOLUTE = 0x01 + +LOGDEBUG = 0x00 +LOGINFO = 0x01 +LOGWARNING = 0x02 +LOGERROR = 0x03 +LOGFATAL = 0x04 +LOGNONE = 0x05 + +ACTION_EXECBUILTIN = 0x01 +ACTION_BUTTON = 0x02 + +###################################################################### +# Helper Functions +###################################################################### + +def format_string(msg): + """ """ + return msg.encode('utf-8') + b"\0" + +def format_uint32(num): + """ """ + return pack ("!I", num) + +def format_uint16(num): + """ """ + if num<0: + num = 0 + elif num>65535: + num = 65535 + return pack ("!H", num) + + +###################################################################### +# Packet Classes +###################################################################### + +class Packet: + """Base class that implements a single event packet. + + - Generic packet structure (maximum 1024 bytes per packet) + - Header is 32 bytes long, so 992 bytes available for payload + - large payloads can be split into multiple packets using H4 and H5 + H5 should contain total no. of packets in such a case + - H6 contains length of P1, which is limited to 992 bytes + - if H5 is 0 or 1, then H4 will be ignored (single packet msg) + - H7 must be set to zeros for now + + ----------------------------- + | -H1 Signature ("XBMC") | - 4 x CHAR 4B + | -H2 Version (eg. 2.0) | - 2 x UNSIGNED CHAR 2B + | -H3 PacketType | - 1 x UNSIGNED SHORT 2B + | -H4 Sequence number | - 1 x UNSIGNED LONG 4B + | -H5 No. of packets in msg | - 1 x UNSIGNED LONG 4B + | -H7 Client's unique token | - 1 x UNSIGNED LONG 4B + | -H8 Reserved | - 10 x UNSIGNED CHAR 10B + |---------------------------| + | -P1 payload | - + ----------------------------- + """ + def __init__(self): + self.sig = b"XBMC" + self.minver = 0 + self.majver = 2 + self.seq = 1 + self.maxseq = 1 + self.payloadsize = 0 + self.uid = UNIQUE_IDENTIFICATION + self.reserved = b"\0" * 10 + self.payload = b"" + + def append_payload(self, blob): + """Append to existing payload + + Arguments: + blob -- binary data to append to the current payload + """ + if isinstance(blob, str): + blob = blob.encode() + self.set_payload(self.payload + blob) + + + def set_payload(self, payload): + """Set the payload for this packet + + Arguments: + payload -- binary data that contains the payload + """ + if isinstance(payload, str): + payload = payload.encode() + self.payload = payload + self.payloadsize = len(self.payload) + self.maxseq = int((self.payloadsize + (MAX_PAYLOAD_SIZE - 1)) / MAX_PAYLOAD_SIZE) + + + def num_packets(self): + """ Return the number of packets required for payload """ + return self.maxseq + + def get_header(self, packettype=-1, seq=1, maxseq=1, payload_size=0): + """Construct a header and return as string + + Keyword arguments: + packettype -- valid packet types are PT_HELO, PT_BYE, PT_BUTTON, + PT_MOUSE, PT_PING, PT_BORADCAST, PT_NOTIFICATION, + PT_BLOB, PT_DEBUG + seq -- the sequence of this packet for a multi packet message + (default 1) + maxseq -- the total number of packets for a multi packet message + (default 1) + payload_size -- the size of the payload of this packet (default 0) + """ + if packettype < 0: + packettype = self.packettype + header = self.sig + header += chr(self.majver).encode() + header += chr(self.minver).encode() + header += format_uint16(packettype) + header += format_uint32(seq) + header += format_uint32(maxseq) + header += format_uint16(payload_size) + header += format_uint32(self.uid) + header += self.reserved + return header + + def get_payload_size(self, seq): + """Returns the calculated payload size for the particular packet + + Arguments: + seq -- the sequence number + """ + if self.maxseq == 1: + return self.payloadsize + + if seq < self.maxseq: + return MAX_PAYLOAD_SIZE + + return self.payloadsize % MAX_PAYLOAD_SIZE + + + def get_udp_message(self, packetnum=1): + """Construct the UDP message for the specified packetnum and return + as string + + Keyword arguments: + packetnum -- the packet no. for which to construct the message + (default 1) + """ + if packetnum > self.num_packets() or packetnum < 1: + return b"" + header = b"" + if packetnum==1: + header = self.get_header(self.packettype, packetnum, self.maxseq, + self.get_payload_size(packetnum)) + else: + header = self.get_header(PT_BLOB, packetnum, self.maxseq, + self.get_payload_size(packetnum)) + + payload = self.payload[ (packetnum-1) * MAX_PAYLOAD_SIZE : + (packetnum-1) * MAX_PAYLOAD_SIZE+ + self.get_payload_size(packetnum) ] + return header + payload + + def send(self, sock, addr, uid=UNIQUE_IDENTIFICATION): + """Send the entire message to the specified socket and address. + + Arguments: + sock -- datagram socket object (socket.socket) + addr -- address, port pair (eg: ("127.0.0.1", 9777) ) + uid -- unique identification + """ + self.uid = uid + for a in range ( 0, self.num_packets() ): + sock.sendto(self.get_udp_message(a+1), addr) + + +class PacketHELO (Packet): + """A HELO packet + + A HELO packet establishes a valid connection to XBMC. It is the + first packet that should be sent. + """ + def __init__(self, devicename=None, icon_type=ICON_NONE, icon_file=None): + """ + Keyword arguments: + devicename -- the string that identifies the client + icon_type -- one of ICON_NONE, ICON_JPEG, ICON_PNG, ICON_GIF + icon_file -- location of icon file with respect to current working + directory if icon_type is not ICON_NONE + """ + Packet.__init__(self) + self.packettype = PT_HELO + self.icontype = icon_type + self.set_payload ( format_string(devicename)[0:128] ) + self.append_payload( chr (icon_type) ) + self.append_payload( format_uint16 (0) ) # port no + self.append_payload( format_uint32 (0) ) # reserved1 + self.append_payload( format_uint32 (0) ) # reserved2 + if icon_type != ICON_NONE and icon_file: + with open(icon_file, 'rb') as f: + self.append_payload(f.read()) + + +class PacketNOTIFICATION (Packet): + """A NOTIFICATION packet + + This packet displays a notification window in XBMC. It can contain + a caption, a message and an icon. + """ + def __init__(self, title, message, icon_type=ICON_NONE, icon_file=None): + """ + Keyword arguments: + title -- the notification caption / title + message -- the main text of the notification + icon_type -- one of ICON_NONE, ICON_JPEG, ICON_PNG, ICON_GIF + icon_file -- location of icon file with respect to current working + directory if icon_type is not ICON_NONE + """ + Packet.__init__(self) + self.packettype = PT_NOTIFICATION + self.title = title + self.message = message + self.set_payload ( format_string(title) ) + self.append_payload( format_string(message) ) + self.append_payload( chr (icon_type) ) + self.append_payload( format_uint32 (0) ) # reserved + if icon_type != ICON_NONE and icon_file: + with open(icon_file, 'rb') as f: + self.append_payload(f.read()) + +class PacketBUTTON (Packet): + """A BUTTON packet + + A button packet send a key press or release event to XBMC + """ + def __init__(self, code=0, repeat=1, down=1, queue=0, + map_name="", button_name="", amount=0, axis=0): + """ + Keyword arguments: + code -- raw button code (default: 0) + repeat -- this key press should repeat until released (default: 1) + Note that queued pressed cannot repeat. + down -- if this is 1, it implies a press event, 0 implies a release + event. (default: 1) + queue -- a queued key press means that the button event is + executed just once after which the next key press is + processed. It can be used for macros. Currently there + is no support for time delays between queued presses. + (default: 0) + map_name -- a combination of map_name and button_name refers to a + mapping in the user's Keymap.xml or Lircmap.xml. + map_name can be one of the following: + "KB" => standard keyboard map ( <keyboard> section ) + "XG" => xbox gamepad map ( <gamepad> section ) + "R1" => xbox remote map ( <remote> section ) + "R2" => xbox universal remote map ( <universalremote> + section ) + "LI:devicename" => LIRC remote map where 'devicename' is the + actual device's name + button_name -- a button name defined in the map specified in map_name. + For example, if map_name is "KB" referring to the + <keyboard> section in Keymap.xml then, valid + button_names include "printscreen", "minus", "x", etc. + amount -- unimplemented for now; in the future it will be used for + specifying magnitude of analog key press events + """ + Packet.__init__(self) + self.flags = 0 + self.packettype = PT_BUTTON + if type (code ) == str: + code = ord(code) + + # assign code only if we don't have a map and button name + if not (map_name and button_name): + self.code = code + else: + self.flags |= BT_USE_NAME + self.code = 0 + if (amount != None): + self.flags |= BT_USE_AMOUNT + self.amount = int(amount) + else: + self.amount = 0 + + if down: + self.flags |= BT_DOWN + else: + self.flags |= BT_UP + if not repeat: + self.flags |= BT_NO_REPEAT + if queue: + self.flags |= BT_QUEUE + if axis == 1: + self.flags |= BT_AXISSINGLE + elif axis == 2: + self.flags |= BT_AXIS + + self.set_payload ( format_uint16(self.code) ) + self.append_payload( format_uint16(self.flags) ) + self.append_payload( format_uint16(self.amount) ) + self.append_payload( format_string (map_name) ) + self.append_payload( format_string (button_name) ) + +class PacketMOUSE (Packet): + """A MOUSE packet + + A MOUSE packets sets the mouse position in XBMC + """ + def __init__(self, x, y): + """ + Arguments: + x -- horizontal position ranging from 0 to 65535 + y -- vertical position ranging from 0 to 65535 + + The range will be mapped to the screen width and height in XBMC + """ + Packet.__init__(self) + self.packettype = PT_MOUSE + self.flags = MS_ABSOLUTE + self.append_payload( chr (self.flags) ) + self.append_payload( format_uint16(x) ) + self.append_payload( format_uint16(y) ) + +class PacketBYE (Packet): + """A BYE packet + + A BYE packet terminates the connection to XBMC. + """ + def __init__(self): + Packet.__init__(self) + self.packettype = PT_BYE + + +class PacketPING (Packet): + """A PING packet + + A PING packet tells XBMC that the client is still alive. All valid + packets act as ping (not just this one). A client needs to ping + XBMC at least once in 60 seconds or it will time out. + """ + def __init__(self): + Packet.__init__(self) + self.packettype = PT_PING + +class PacketLOG (Packet): + """A LOG packet + + A LOG packet tells XBMC to log the message to xbmc.log with the loglevel as specified. + """ + def __init__(self, loglevel=0, logmessage="", autoprint=True): + """ + Keyword arguments: + loglevel -- the loglevel, follows XBMC standard. + logmessage -- the message to log + autoprint -- if the logmessage should automatically be printed to stdout + """ + Packet.__init__(self) + self.packettype = PT_LOG + self.append_payload( chr (loglevel) ) + self.append_payload( format_string(logmessage) ) + if (autoprint): + print(logmessage) + +class PacketACTION (Packet): + """An ACTION packet + + An ACTION packet tells XBMC to do the action specified, based on the type it knows were it needs to be sent. + The idea is that this will be as in scripting/skining and keymapping, just triggered from afar. + """ + def __init__(self, actionmessage="", actiontype=ACTION_EXECBUILTIN): + """ + Keyword arguments: + loglevel -- the loglevel, follows XBMC standard. + logmessage -- the message to log + autoprint -- if the logmessage should automatically be printed to stdout + """ + Packet.__init__(self) + self.packettype = PT_ACTION + self.append_payload( chr (actiontype) ) + self.append_payload( format_string(actionmessage) ) + +###################################################################### +# XBMC Client Class +###################################################################### + +class XBMCClient: + """An XBMC event client""" + + def __init__(self, name ="", icon_file=None, broadcast=False, uid=UNIQUE_IDENTIFICATION, + ip="127.0.0.1"): + """ + Keyword arguments: + name -- Name of the client + icon_file -- location of an icon file, if any (png, jpg or gif) + uid -- unique identification + """ + self.name = str(name) + self.icon_file = icon_file + self.icon_type = self._get_icon_type(icon_file) + self.ip = ip + self.port = 9777 + self.sock = socket(AF_INET,SOCK_DGRAM) + if broadcast: + self.sock.setsockopt(SOL_SOCKET, SO_BROADCAST, 1) + self.uid = uid + + + def connect(self, ip=None, port=None): + """Initialize connection to XBMC + ip -- IP Address of XBMC + port -- port that the event server on XBMC is listening on + """ + if ip: + self.ip = ip + if port: + self.port = int(port) + self.addr = (self.ip, self.port) + packet = PacketHELO(self.name, self.icon_type, self.icon_file) + packet.send(self.sock, self.addr, self.uid) + + + def close(self): + """Close the current connection""" + packet = PacketBYE() + packet.send(self.sock, self.addr, self.uid) + + + def ping(self): + """Send a PING packet""" + packet = PacketPING() + packet.send(self.sock, self.addr, self.uid) + + + def send_notification(self, title="", message="", icon_file=None): + """Send a notification to the connected XBMC + Keyword Arguments: + title -- The title/heading for the notification + message -- The message to be displayed + icon_file -- location of an icon file, if any (png, jpg, gif) + """ + self.connect() + packet = PacketNOTIFICATION(title, message, + self._get_icon_type(icon_file), + icon_file) + packet.send(self.sock, self.addr, self.uid) + + + def send_keyboard_button(self, button=None): + """Send a keyboard event to XBMC + Keyword Arguments: + button -- name of the keyboard button to send (same as in Keymap.xml) + """ + if not button: + return + self.send_button(map="KB", button=button) + + + def send_remote_button(self, button=None): + """Send a remote control event to XBMC + Keyword Arguments: + button -- name of the remote control button to send (same as in Keymap.xml) + """ + if not button: + return + self.send_button(map="R1", button=button) + + + def release_button(self): + """Release all buttons""" + packet = PacketBUTTON(code=0x01, down=0) + packet.send(self.sock, self.addr, self.uid) + + + def send_button(self, map="", button="", amount=0): + """Send a button event to XBMC + Keyword arguments: + map -- a combination of map_name and button_name refers to a + mapping in the user's Keymap.xml or Lircmap.xml. + map_name can be one of the following: + "KB" => standard keyboard map ( <keyboard> section ) + "XG" => xbox gamepad map ( <gamepad> section ) + "R1" => xbox remote map ( <remote> section ) + "R2" => xbox universal remote map ( <universalremote> + section ) + "LI:devicename" => LIRC remote map where 'devicename' is the + actual device's name + button -- a button name defined in the map specified in map, above. + For example, if map is "KB" referring to the <keyboard> + section in Keymap.xml then, valid buttons include + "printscreen", "minus", "x", etc. + """ + packet = PacketBUTTON(map_name=str(map), button_name=str(button), amount=amount) + packet.send(self.sock, self.addr, self.uid) + + def send_button_state(self, map="", button="", amount=0, down=0, axis=0): + """Send a button event to XBMC + Keyword arguments: + map -- a combination of map_name and button_name refers to a + mapping in the user's Keymap.xml or Lircmap.xml. + map_name can be one of the following: + "KB" => standard keyboard map ( <keyboard> section ) + "XG" => xbox gamepad map ( <gamepad> section ) + "R1" => xbox remote map ( <remote> section ) + "R2" => xbox universal remote map ( <universalremote> + section ) + "LI:devicename" => LIRC remote map where 'devicename' is the + actual device's name + button -- a button name defined in the map specified in map, above. + For example, if map is "KB" referring to the <keyboard> + section in Keymap.xml then, valid buttons include + "printscreen", "minus", "x", etc. + """ + if axis: + down = int(amount != 0) + + packet = PacketBUTTON(map_name=str(map), button_name=str(button), amount=amount, down=down, queue=1, axis=axis) + packet.send(self.sock, self.addr, self.uid) + + def send_mouse_position(self, x=0, y=0): + """Send a mouse event to XBMC + Keywords Arguments: + x -- absolute x position of mouse ranging from 0 to 65535 + which maps to the entire screen width + y -- same a 'x' but relates to the screen height + """ + packet = PacketMOUSE(int(x), int(y)) + packet.send(self.sock, self.addr, self.uid) + + def send_log(self, loglevel=0, logmessage="", autoprint=True): + """ + Keyword arguments: + loglevel -- the loglevel, follows XBMC standard. + logmessage -- the message to log + autoprint -- if the logmessage should automatically be printed to stdout + """ + packet = PacketLOG(loglevel, logmessage) + packet.send(self.sock, self.addr, self.uid) + + def send_action(self, actionmessage="", actiontype=ACTION_EXECBUILTIN): + """ + Keyword arguments: + actionmessage -- the ActionString + actiontype -- The ActionType the ActionString should be sent to. + """ + packet = PacketACTION(actionmessage, actiontype) + packet.send(self.sock, self.addr, self.uid) + + def _get_icon_type(self, icon_file): + if icon_file: + if icon_file.lower()[-3:] == "png": + return ICON_PNG + elif icon_file.lower()[-3:] == "gif": + return ICON_GIF + elif icon_file.lower()[-3:] == "jpg": + return ICON_JPEG + return ICON_NONE diff --git a/tools/EventClients/lib/python/zeroconf.py b/tools/EventClients/lib/python/zeroconf.py new file mode 100644 index 0000000..ee2af14 --- /dev/null +++ b/tools/EventClients/lib/python/zeroconf.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +# -*- 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. + +""" +Simple wrapper around Avahi +""" + +__author__ = "d4rk@xbmc.org" +__version__ = "0.1" + +try: + import time + import dbus, avahi + from dbus import DBusException + from dbus.mainloop.glib import DBusGMainLoop + from gi.repository import GLib +except Exception as e: + print("Zeroconf support disabled. To enable, install the following Python modules:") + print(" dbus, gi, avahi") + pass + +SERVICE_FOUND = 1 +SERVICE_LOST = 2 + +class Browser: + """ Simple Zeroconf Browser """ + + def __init__( self, service_types = {} ): + """ + service_types - dictionary of services => handlers + """ + self._stop = False + self.loop = DBusGMainLoop() + self.bus = dbus.SystemBus( mainloop=self.loop ) + self.server = dbus.Interface( self.bus.get_object( avahi.DBUS_NAME, '/' ), + 'org.freedesktop.Avahi.Server') + self.handlers = {} + + for type in service_types.keys(): + self.add_service( type, service_types[ type ] ) + + + def add_service( self, type, handler = None ): + """ + Add a service that the browser should watch for + """ + self.sbrowser = dbus.Interface( + self.bus.get_object( + avahi.DBUS_NAME, + self.server.ServiceBrowserNew( + avahi.IF_UNSPEC, + avahi.PROTO_UNSPEC, + type, + 'local', + dbus.UInt32(0) + ) + ), + avahi.DBUS_INTERFACE_SERVICE_BROWSER) + self.handlers[ type ] = handler + self.sbrowser.connect_to_signal("ItemNew", self._new_item_handler) + self.sbrowser.connect_to_signal("ItemRemove", self._remove_item_handler) + + + def run(self): + """ + Run the GLib event loop + """ + # Don't use loop.run() because Python's GIL will block all threads + loop = GLib.MainLoop() + context = loop.get_context() + while not self._stop: + if context.pending(): + context.iteration( True ) + else: + time.sleep(1) + + def stop(self): + """ + Stop the GLib event loop + """ + self._stop = True + + + def _new_item_handler(self, interface, protocol, name, stype, domain, flags): + if flags & avahi.LOOKUP_RESULT_LOCAL: + # local service, skip + pass + + self.server.ResolveService( + interface, + protocol, + name, + stype, + domain, + avahi.PROTO_UNSPEC, + dbus.UInt32(0), + reply_handler = self._service_resolved_handler, + error_handler = self._error_handler + ) + return + + + def _remove_item_handler(self, interface, protocol, name, stype, domain, flags): + if self.handlers[ stype ]: + # FIXME: more details needed here + try: + self.handlers[ stype ]( SERVICE_LOST, { 'type' : stype, 'name' : name } ) + except: + pass + + + def _service_resolved_handler( self, *args ): + service = {} + service['type'] = str( args[3] ) + service['name'] = str( args[2] ) + service['address'] = str( args[7] ) + service['hostname'] = str( args[5] ) + service['port'] = int( args[8] ) + + # if the service type has a handler call it + try: + if self.handlers[ args[3] ]: + self.handlers[ args[3] ]( SERVICE_FOUND, service ) + except: + pass + + + def _error_handler( self, *args ): + print('ERROR: %s ' % str( args[0] )) + + +if __name__ == "__main__": + def service_handler( found, service ): + print("---------------------") + print(['Found Service', 'Lost Service'][found-1]) + for key in service.keys(): + print(key+" : "+str( service[key] )) + + browser = Browser( { + '_xbmc-events._udp' : service_handler, + '_xbmc-web._tcp' : service_handler + } ) + browser.run() + |