From eac1e91638c83ba1ec757c22ce4769ce51f43075 Mon Sep 17 00:00:00 2001 From: Peter Hutterer Date: Thu, 3 Mar 2016 10:10:38 +1000 Subject: [PATCH] Add a python module to talk to ratbagd Signed-off-by: Peter Hutterer --- .gitignore | 2 +- Makefile.am | 5 + configure.ac | 1 + python/ratbagd/__init__.py | 334 +++++++++++++++++++++++++++++++++++++ 4 files changed, 341 insertions(+), 1 deletion(-) create mode 100644 python/ratbagd/__init__.py diff --git a/.gitignore b/.gitignore index 5a957412..17d95834 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,6 @@ libtool *.m4 !m4/attributes.m4 ratbagctl -ratbagd +./ratbagd stamp-* ratbagd.service diff --git a/Makefile.am b/Makefile.am index faaa1d2b..af747773 100644 --- a/Makefile.am +++ b/Makefile.am @@ -105,3 +105,8 @@ $(unit_DATA): $(unit_DATA).in EXTRA_DIST += ratbagd.service.in BUILT_SOURCES += ratbagd.service + +# ------------------------------------------------------------------------------ +# python module +dist_pkgpython_DATA = \ + python/ratbagd/__init__.py diff --git a/configure.ac b/configure.ac index 005cd7af..25efbfad 100644 --- a/configure.ac +++ b/configure.ac @@ -34,6 +34,7 @@ AC_PROG_CC_C99 AC_PATH_PROG([M4], [m4]) AC_PATH_PROG([XSLTPROC], [xsltproc]) +AM_PATH_PYTHON AS_IF([! ln --relative --help > /dev/null 2>&1], [AC_MSG_ERROR([*** ln doesn't support --relative ***])]) diff --git a/python/ratbagd/__init__.py b/python/ratbagd/__init__.py new file mode 100644 index 00000000..23dd72e4 --- /dev/null +++ b/python/ratbagd/__init__.py @@ -0,0 +1,334 @@ +# vim: set expandtab shiftwidth=4 tabstop=4: +# +# This file is part of ratbagd. +# +# Copyright 2016 Red Hat, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice (including the next +# paragraph) shall be included in all copies or substantial portions of the +# Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +from gi.repository import GLib +from gi.repository import Gio + +class RatbagdDBusUnavailable(BaseException): + """ + Signals DBus is unavailable or the ratbagd daemon is not available. + """ + pass + +class _RatbagdDBus(object): + def __init__(self, interface, object_path): + self._dbus = Gio.bus_get_sync(Gio.BusType.SYSTEM, None) + self._proxy = Gio.DBusProxy.new_sync(self._dbus, + Gio.DBusProxyFlags.NONE, + None, + 'org.freedesktop.ratbag1', + object_path, + 'org.freedesktop.ratbag1.{}'.format(interface), + None) + if self._proxy.get_name_owner() == None: + raise RatbagdDBusUnavailable() + + def dbus_property(self, property): + p = self._proxy.get_cached_property(property) + if p != None: + return p.unpack() + return p + + def dbus_call(self, method, type, *value): + val = GLib.Variant("({})".format(type), value ) + self._proxy.call_sync(method, val, Gio.DBusCallFlags.NO_AUTO_START, 500, None) + +class Ratbagd(_RatbagdDBus): + """ + The ratbagd top-level object. Provides a list of devices available + through ratbagd, actual interaction with the devices is via the + RatbagdDevice, RatbagdProfile and RatbagdResolution objects. + """ + def __init__(self): + _RatbagdDBus.__init__(self, "Manager", '/org/freedesktop/ratbag1') + self._devices = [] + result = self.dbus_property("Devices") + if result != None: + self._devices = [RatbagdDevice(objpath) for objpath in result] + + @property + def devices(self): + """ + A list of RatbagdDevice objects supported by ratbagd. + """ + return self._devices + +class RatbagdDevice(_RatbagdDBus): + """ + Represents a ratbagd device. + """ + def __init__(self, object_path): + _RatbagdDBus.__init__(self, "Device", object_path) + self._devnode = self.dbus_property("Id") + self._description = self.dbus_property("Description") + self._svg = self.dbus_property("Svg") + + self._profiles = [] + self._active_profile = -1 + result = self.dbus_property("Profiles") + if result != None: + self._profiles = [RatbagdProfile(objpath) for objpath in result] + self._active_profile = self.dbus_property("ActiveProfile") + + caps = [ "switchable-resolution", + "switchable-profile", + "button-keys", + "button-macros", + "default-profile"] + self._caps = [] + for c in caps: + cname = "Cap" + "".join([n.capitalize() for n in c.split("-")]) + if self.dbus_property(cname): + self._caps.append(c) + + @property + def profiles(self): + """ + A list of RatbagdProfile objects provided by this device. + """ + return self._profiles + + @property + def description(self): + """ + The device name, usually provided by the kernel. + """ + return self._description + + @property + def svg(self): + """ + The SVG file name. This function returns the file name only, not the + absolute path to the file. + """ + return self._svg + + @property + def id(self): + """ + A unique identifier for this device. + """ + return self._devnode + + @property + def active_profile(self): + """ + The currently active profile. This function returns a RatbagdProfile + or None if no active profile was found. + """ + if self._active_profile == -1: + return None + return self._profiles[self._active_profile] + + @property + def capabilities(self): + """ + Return the capabilities of this device as a list of identifier strings. + Capabilities not present on the device are not in the list. Thus use + if "capability" is in deivce.caps: + do something + + Capabilities supported: "switchable-resolution", + "switchable-profile", "button-keys", "button-macros", + "default-profile" + """ + return self._caps + + def __eq__(self, other): + return other and self._objpath == other._objpath + +class RatbagdProfile(_RatbagdDBus): + """ + Represents a ratbagd profile + """ + def __init__(self, object_path): + _RatbagdDBus.__init__(self, "Profile", object_path) + self._objpath = object_path + self._index = self.dbus_property("Index") + + self._resolutions = [] + self._active_resolution_idx = -1 + self._default_resolution_idx = -1 + self._buttons = [] + + result = self.dbus_property("Resolutions") + if result != None: + self._resolutions = [RatbagdResolution(objpath) for objpath in result] + self._active_resolution_idx = self.dbus_property("ActiveResolution") + self._default_resolution_idx = self.dbus_property("DefaultResolution") + + result = self.dbus_property("Buttons") + if result != None: + self._buttons = [RatbagdButton(objpath) for objpath in result] + + @property + def index(self): + return self._index + + @property + def resolutions(self): + """ + A list of RatbagdResolution objects with this profile's resolutions. + """ + return self._resolutions + + @property + def active_resolution(self): + """ + The currently active resolution. This function returns a + RatbagdResolution object or None. + """ + if self._active_resolution_idx == -1: + return None + return self._resolutions[self._active_resolution_idx] + + @property + def default_resolution(self): + """ + The default resolution. This function returns a RatbagdResolution + object or None. + """ + if self._default_resolution_idx == -1: + return None + return self._resolutions[self._default_resolution_idx] + + @property + def buttons(self): + """ + A list of RatbagdButton objects with this profile's button + mappings. Note that the list of buttons differs between profiles but + the number of buttons is identical across profiles. + """ + return self._buttons + + def __eq__(self, other): + return self._objpath == other._objpath + +class RatbagdResolution(_RatbagdDBus): + """ + Represents a ratbagd resolution. + """ + def __init__(self, object_path): + _RatbagdDBus.__init__(self, "Resolution", object_path) + self._index = self.dbus_property("Index") + self._xres = self.dbus_property("XResolution") + self._yres = self.dbus_property("YResolution") + self._rate = self.dbus_property("ReportRate") + self._objpath = object_path + + caps = [ "individual-report-rate", + "separate-xy-resolution" ] + self._caps = [] + for c in caps: + cname = "Cap" + "".join([n.capitalize() for n in c.split("-")]) + if self.dbus_property(cname): + self._caps.append(c) + + @property + def resolution(self): + """Returns the tuple (xres, yres) with each resolution in DPI""" + return (self._xres, self._yres) + + @resolution.setter + def resolution(self, res): + return self.dbus_call("SetResolution", "uu", *res) + + @property + def report_rate(self): + """ + Returns the report rate in Hz. + """ + return self._rate + + @report_rate.setter + def report_rate(self, rate): + return self.dbus_call("SetReportRate", "u", rate) + + @property + def capabilities(self): + """ + Return the capabilities of this device as a list of identifier strings. + Capabilities not present on the device are not in the list. Thus use + if "capability" is in resolution.caps: + do something + + Capabilities supported: "individual-report-rate", + "separate-xy-resolution" + """ + return self._caps + + def __eq__(self, other): + return self._objpath == other._objpath + +class RatbagdButton(_RatbagdDBus): + """ + Represents a ratbagd button. + """ + def __init__(self, object_path): + _RatbagdDBus.__init__(self, "Button", object_path) + self._index = self.dbus_property("Index") + self._button = self.dbus_property("ButtonMapping") + + @property + def index(self): + return self._index + + @property + def button_type(self): + return self.dbus_property("Type") + + @property + def action_type(self): + return self.dbus_property("ActionType") + + @property + def special(self): + self._special = self.dbus_property("SpecialMapping") + return self._special + + @special.setter + def special(self, special): + return self.dbus_call("SetSpecialMapping", "s", special) + + @property + def key(self): + self._key = self.dbus_property("KeyMapping") + return self._key + + @key.setter + def key(self, key, modifiers): + return self.dbus_call("SetKeyMapping", "au", [key].append(modifiers)) + + @property + def button(self): + self._button = self.dbus_property("ButtonMapping") + return self._button + + @button.setter + def button(self, button): + return self.dbus_call("SetButtonMapping", "u", button) + + def disable(self): + return self.dbus_call("Disable", "")