summaryrefslogtreecommitdiffstats
path: root/tools/EventClients/lib/python
diff options
context:
space:
mode:
Diffstat (limited to 'tools/EventClients/lib/python')
-rw-r--r--tools/EventClients/lib/python/__init__.py2
-rw-r--r--tools/EventClients/lib/python/bt/__init__.py2
-rw-r--r--tools/EventClients/lib/python/bt/bt.py82
-rw-r--r--tools/EventClients/lib/python/bt/hid.py81
-rw-r--r--tools/EventClients/lib/python/ps3/__init__.py2
-rw-r--r--tools/EventClients/lib/python/ps3/keymaps.py81
-rw-r--r--tools/EventClients/lib/python/ps3/sixaxis.py372
-rwxr-xr-xtools/EventClients/lib/python/ps3/sixpair.py114
-rwxr-xr-xtools/EventClients/lib/python/ps3/sixwatch.py31
-rw-r--r--tools/EventClients/lib/python/xbmcclient.py639
-rw-r--r--tools/EventClients/lib/python/zeroconf.py160
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()
+