Hello community, here is the log from the commit of package sc-controller for openSUSE:Factory checked in at 2019-07-12 11:59:48 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/sc-controller (Old) and /work/SRC/openSUSE:Factory/.sc-controller.new.4615 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "sc-controller" Fri Jul 12 11:59:48 2019 rev:4 rq:714459 version:0.4.7 Changes: -------- --- /work/SRC/openSUSE:Factory/sc-controller/sc-controller.changes 2018-12-31 09:47:38.466154937 +0100 +++ /work/SRC/openSUSE:Factory/.sc-controller.new.4615/sc-controller.changes 2019-07-12 11:59:49.580171928 +0200 @@ -1,0 +2,11 @@ +Tue Jul 9 11:34:41 UTC 2019 - Jannik Seiler <[email protected]> + +- Update to version 0.4.7 + * Added option to send gyroscope data to Citra and Cemu (Cemuhook support) + * Fixes in 0.4.7: + * Not minimizing to tray on start + * Issues when using stick simultaneously with lpad + * Crash in controller registration dialog if udev device has special symbols in name (thanks @Verequies) + * Trackball friction resets when set to zero + +------------------------------------------------------------------- Old: ---- sc-controller-0.4.6.1.tar.gz New: ---- sc-controller-0.4.7.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ sc-controller.spec ++++++ --- /var/tmp/diff_new_pack.Yw7CSb/_old 2019-07-12 11:59:51.408171983 +0200 +++ /var/tmp/diff_new_pack.Yw7CSb/_new 2019-07-12 11:59:51.408171983 +0200 @@ -17,7 +17,7 @@ Name: sc-controller -Version: 0.4.6.1 +Version: 0.4.7 Release: 0 Summary: User-mode driver and GTK3-based GUI for the Steam Controller License: GPL-2.0-only @@ -30,6 +30,7 @@ BuildRequires: hicolor-icon-theme BuildRequires: pkgconfig BuildRequires: shared-mime-info +BuildRequires: zlib-devel BuildRequires: pkgconfig(python2) BuildRequires: pkgconfig(udev) #Requires: libgtk-3-0 ++++++ sc-controller-0.4.6.1.tar.gz -> sc-controller-0.4.7.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sc-controller-0.4.6.1/appimage-build.sh new/sc-controller-0.4.7/appimage-build.sh --- old/sc-controller-0.4.6.1/appimage-build.sh 2018-12-21 00:20:53.000000000 +0100 +++ new/sc-controller-0.4.7/appimage-build.sh 2019-07-08 14:28:01.000000000 +0200 @@ -51,11 +51,11 @@ download_dep "python-gobject-3.26.1" "https://archive.archlinux.org/packages/p/python2-gobject/python2-gobject-3.26.1-1-x86_64.pkg.tar.xz" download_dep "libpng-1.6.34" "https://archive.archlinux.org/packages/l/libpng/libpng-1.6.34-2-x86_64.pkg.tar.xz" download_dep "gdk-pixbuf-2.36.9" "https://archive.archlinux.org/packages/g/gdk-pixbuf2/gdk-pixbuf2-2.36.9-1-x86_64.pkg.tar.xz" -download_dep "libcroco-0.6.12" "https://archive.archlinux.org/packages/l/libcroco/libcroco-0.6.12-1-x86_64.pkg.tar.xz" +download_dep "libcroco-0.6.13" "https://archive.archlinux.org/packages/l/libcroco/libcroco-0.6.13-1-x86_64.pkg.tar.xz" download_dep "libxml2-2.9.7" "https://archive.archlinux.org/packages/l/libxml2/libxml2-2.9.7%2B4%2Bg72182550-2-x86_64.pkg.tar.xz" download_dep "librsvg-2.42.2" "https://archive.archlinux.org/packages/l/librsvg/librsvg-2%3A2.42.2-1-x86_64.pkg.tar.xz" download_dep "icu-60.2" "https://archive.archlinux.org/packages/i/icu/icu-60.2-1-x86_64.pkg.tar.xz" -download_dep "zlib-1:1.2.9" "https://archive.archlinux.org/packages/z/zlib/zlib-1%3A1.2.9-1-x86_64.pkg.tar.xz" +download_dep "zlib-1:1.2.11" "https://archive.archlinux.org/packages/z/zlib/zlib-1%3A1.2.11-1-x86_64.pkg.tar.xz" # Prepare & build deps export PYTHONPATH=${BUILD_APPDIR}/usr/lib/python2.7/site-packages/ @@ -72,11 +72,11 @@ unpack_dep "libpng-1.6.34" unpack_dep "python-gobject-3.26.1" unpack_dep "gdk-pixbuf-2.36.9" -unpack_dep "libcroco-0.6.12" +unpack_dep "libcroco-0.6.13" unpack_dep "libxml2-2.9.7" unpack_dep "librsvg-2.42.2" unpack_dep "icu-60.2" -unpack_dep "zlib-1:1.2.9" +unpack_dep "zlib-1:1.2.11" # Remove uneeded files rm -f "${BUILD_APPDIR}/usr/${LIB}/gdk-pixbuf-2.0/2.10.0/loaders/libpixbufloader-ani.so" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sc-controller-0.4.6.1/default_profiles/Desktop.sccprofile new/sc-controller-0.4.7/default_profiles/Desktop.sccprofile --- old/sc-controller-0.4.6.1/default_profiles/Desktop.sccprofile 2018-12-21 00:20:53.000000000 +0100 +++ new/sc-controller-0.4.7/default_profiles/Desktop.sccprofile 2019-07-08 14:28:01.000000000 +0200 @@ -38,7 +38,9 @@ "cpad": { "action": "mouse()" }, - "gyro": {}, + "gyro": { + "action": "cemuhook" + }, "is_template": false, "menus": {}, "pad_left": { @@ -57,4 +59,4 @@ "action": "button(Keys.BTN_LEFT)" }, "version": 1.4 -} \ No newline at end of file +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' "old/sc-controller-0.4.6.1/default_profiles/XBox Controller.sccprofile" "new/sc-controller-0.4.7/default_profiles/XBox Controller.sccprofile" --- "old/sc-controller-0.4.6.1/default_profiles/XBox Controller.sccprofile" 2018-12-21 00:20:53.000000000 +0100 +++ "new/sc-controller-0.4.7/default_profiles/XBox Controller.sccprofile" 2019-07-08 14:28:01.000000000 +0200 @@ -42,7 +42,9 @@ } }, "cpad": {}, - "gyro": {}, + "gyro": { + "action": "cemuhook" + }, "is_template": false, "menus": {}, "pad_left": { @@ -61,4 +63,4 @@ "action": "axis(Axes.ABS_RZ)" }, "version": 1.4 -} \ No newline at end of file +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sc-controller-0.4.6.1/docs/actions.md new/sc-controller-0.4.7/docs/actions.md --- old/sc-controller-0.4.6.1/docs/actions.md 2018-12-21 00:20:53.000000000 +0100 +++ new/sc-controller-0.4.7/docs/actions.md 2019-07-08 14:28:01.000000000 +0200 @@ -153,6 +153,11 @@ Resets gyroscope offsets so current orientation is treated as neutral. +#### <a name="cemuhook"></a> cemuhook() +When set to gyro, outputs gyroscope data in way compatibile with Cemu, Citra and +other applications using CemuHookUDP motion provider protocol. + + #### <a name="tilt"></a> gyro(front_down, front_up, tilt_left, tilt_right) Maps tilting of gamepad into actions. When gamepad is tilt to one of for supported sides, assigned action is executed as if by button press and then "released" after diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sc-controller-0.4.6.1/glade/about.glade new/sc-controller-0.4.7/glade/about.glade --- old/sc-controller-0.4.6.1/glade/about.glade 2018-12-21 00:20:53.000000000 +0100 +++ new/sc-controller-0.4.7/glade/about.glade 2019-07-08 14:28:01.000000000 +0200 @@ -117,11 +117,11 @@ I'd like to express my biggest thanks for all my <b><a href="https://www.patreon.com/kozec">Patreon</a></b> supporters, with special mention of -<b>Deryn L.</b> -<b>Eric Duhamel</b> -<b>Enric Morales</b> +<b>guido haag</b> <b>Orivej Desh</b> <b>G. Wilson</b> +<b>Eric Duhamel</b> +<b>Enric Morales</b> <b>Joshua M</b> <b>Lutris</b> <b>Taowa</b> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sc-controller-0.4.6.1/glade/ae/gyro_action.glade new/sc-controller-0.4.7/glade/ae/gyro_action.glade --- old/sc-controller-0.4.6.1/glade/ae/gyro_action.glade 2018-12-21 00:20:53.000000000 +0100 +++ new/sc-controller-0.4.7/glade/ae/gyro_action.glade 2019-07-08 14:28:01.000000000 +0200 @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="UTF-8"?> -<!-- Generated with glade 3.22.1 --> +<!-- Generated with glade 3.20.4 --> <interface> <requires lib="gtk+" version="3.10"/> <object class="GtkAdjustment" id="adjSoftLevel"> @@ -67,6 +67,11 @@ <col id="1" translatable="yes">Left Stick (Absolute)</col> <col id="2">left_abs</col> </row> + <row> + <col id="0">cemuhook</col> + <col id="1" translatable="yes">CemuHook motion provider</col> + <col id="2">cemuhook</col> + </row> </data> </object> <object class="GtkListStore" id="lstYawRoll"> @@ -95,7 +100,7 @@ <property name="row_spacing">3</property> <property name="column_spacing">10</property> <child> - <object class="GtkLabel" id="label1"> + <object class="GtkLabel" id="lblYawRoll"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="margin_left">5</property> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sc-controller-0.4.6.1/run.sh new/sc-controller-0.4.7/run.sh --- old/sc-controller-0.4.6.1/run.sh 2018-12-21 00:20:53.000000000 +0100 +++ new/sc-controller-0.4.7/run.sh 2019-07-08 14:28:01.000000000 +0200 @@ -1,9 +1,10 @@ #!/bin/bash -C_MODULES=(uinput hiddrv sc_by_bt remotepad) +C_MODULES=(uinput hiddrv sc_by_bt remotepad cemuhook) C_VERSION_uinput=9 C_VERSION_hiddrv=5 C_VERSION_sc_by_bt=3 C_VERSION_remotepad=1 +C_VERSION_cemuhook=1 function rebuild_c_modules() { echo "lib$1.so is outdated or missing, building one" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sc-controller-0.4.6.1/scc/actions.py new/sc-controller-0.4.7/scc/actions.py --- old/sc-controller-0.4.6.1/scc/actions.py 2018-12-21 00:20:53.000000000 +0100 +++ new/sc-controller-0.4.7/scc/actions.py 2019-07-08 14:28:01.000000000 +0200 @@ -535,6 +535,7 @@ to actually do something. """ SA = "" + def execute_named(self, name, mapper, *a): sa = mapper.get_special_actions_handler() h_name = "on_sa_%s" % (name,) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sc-controller-0.4.6.1/scc/c_branch.h new/sc-controller-0.4.7/scc/c_branch.h --- old/sc-controller-0.4.6.1/scc/c_branch.h 1970-01-01 01:00:00.000000000 +0100 +++ new/sc-controller-0.4.7/scc/c_branch.h 2019-07-08 14:28:01.000000000 +0200 @@ -0,0 +1,22 @@ +/** + * Glue between code from future and current stuff in python + */ +#pragma once +#include "Python.h" +#include <stdbool.h> +#include <stdint.h> + +#define LERROR(fmt, ...) do { fprintf(stderr, "E " LOG_TAG " " fmt, ##__VA_ARGS__); fprintf(stderr, "\n"); fflush(stderr); } while(0) +#define WARN(fmt, ...) do { fprintf(stderr, "W " LOG_TAG " " fmt, ##__VA_ARGS__); fprintf(stderr, "\n"); fflush(stderr); } while(0) +#define DEBUG(fmt, ...) do { fprintf(stdout, "D " LOG_TAG " " fmt, ##__VA_ARGS__); fprintf(stdout, "\n"); fflush(stdout); } while(0) +#define LOG(fmt, ...) do { fprintf(stdout, "L " LOG_TAG " " fmt, ##__VA_ARGS__); fprintf(stdout, "\n"); fflush(stdout); } while(0) + +typedef uint64_t monotime_t; + +/** Returns current value of CLOCK_MONOTONIC converted to number of milliseconds */ +inline static uint64_t mono_time_ms() { + static struct timespec t; + clock_gettime(CLOCK_MONOTONIC, &t); + return t.tv_sec * 1000 + (t.tv_nsec / 10e5); +} + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sc-controller-0.4.6.1/scc/cemuhook_server.c new/sc-controller-0.4.7/scc/cemuhook_server.c --- old/sc-controller-0.4.6.1/scc/cemuhook_server.c 1970-01-01 01:00:00.000000000 +0100 +++ new/sc-controller-0.4.7/scc/cemuhook_server.c 2019-07-08 14:28:01.000000000 +0200 @@ -0,0 +1,395 @@ +/** + * SC-Controller - Daemon - CemuHookUDP motion provider + * + * Accepts all connections from clients and sends data captured + * by 'cemuhook' actions to them. + * + * This code is also used as library in Python code in master branch. + */ + +#define LOG_TAG "CemuHook" +#ifndef PYTHON + #include "scc/utils/logging.h" + #include "scc/utils/strbuilder.h" + #include "scc/utils/iterable.h" + #include "scc/utils/assert.h" + #include "scc/utils/math.h" + #include "scc/utils/list.h" + #include "scc/tools.h" + #include "daemon.h" +#else // PYTHON + #undef LOG_TAG + #define LOG_TAG "CemuHook " + #include "c_branch.h" +#endif // PYTHON +#ifdef _WIN32 + #error "Implement me!" +#else // _WIN32 + #include <netinet/in.h> + #include <sys/socket.h> + #include <arpa/inet.h> + #include <sys/types.h> + #define SOCKETERROR ": %s", strerror(errno) +#endif // _WIN32 +#include <stdbool.h> +#include <stdint.h> +#include <string.h> +#include <stdlib.h> +#include <unistd.h> +#include <stdio.h> +#include <errno.h> +#include <zlib.h> + +#define BUFFER_SIZE 1024 +#define MAX_PROTO_VERSION 1001 +#define CLIENT_TIMEOUT (5 * 1000) +#define CEMUHOOK_MODULE_VERSION 1 +typedef struct CEHClient { + struct sockaddr_in address; + monotime_t last_seen; + uint32_t next_packet_no; +} CEHClient; +static uint32_t next_id = 1; +#ifndef PYTHON +static LIST_TYPE(CEHClient) clients; +static int sock; +#else +#define CLIENT_LIMIT 10 +static CEHClient clients[CLIENT_LIMIT]; +#endif + + +typedef enum { + DSUC_VERSIONREQ = 0x100000, + DSUS_VERSIONRSP = 0x100000, + DSUC_LISTPORTS = 0x100001, + DSUS_PORTINFO = 0x100001, + DSUC_PADDATAREQ = 0x100002, + DSUS_PADDATARSP = 0x100002, +} MessageType; + +struct __attribute__((packed)) PortInfo { + uint8_t pad_id; + uint8_t state; + uint8_t model; + uint8_t connection_type; + uint8_t mac[6]; + uint8_t battery; + uint8_t active; +}; + +struct __attribute__((packed)) Message { + char header[4]; // 0B + uint16_t protocol_version; // 4B + uint16_t packet_size; // 6B + uint32_t crc; // 8B + uint32_t msg_id; // 12B + MessageType message_type; // 16B + union { // 20B + struct { + int32_t count; + uint8_t ids[4]; + } list_ports; + struct { + uint8_t flags; + uint8_t id; + uint8_t mac[6]; + } pad_data_req; + struct { + uint16_t max_version; + uint16_t min_version; + } version_info; + struct PortInfo port_info[4]; // size = 12B * 4B + struct { + struct PortInfo pad_info; + uint32_t packet_number; + uint8_t buttons1; + uint8_t buttons2; + uint8_t button_ps; + uint8_t button_touch; + uint8_t sticks[4]; // LX, LY, RX, RY + uint8_t analog_buttons[12]; + struct { + uint8_t active; + uint8_t id; + uint16_t x; + uint16_t y; + } touch_point[2]; + uint64_t motion_timestamp; + struct { + float x; + float y; + float z; + float pitch; + float yaw; + float roll; + } accel; + } pad_data; + }; +}; + +static void send_msg(int fd, struct sockaddr_in* target, struct Message* msg, MessageType type, uint16_t payload_size) { + size_t size = 20 + payload_size; + memcpy(msg->header, "DSUS", 4); + msg->protocol_version = MAX_PROTO_VERSION; + msg->packet_size = 4 + payload_size; + msg->message_type = type; + msg->msg_id = next_id ++; + msg->crc = 0; + + uLong crc = crc32(0, (const Bytef*)msg, size); + msg->crc = crc; + + ssize_t r = sendto(fd, (char*)msg, size, 0, (struct sockaddr*)target, sizeof(struct sockaddr_in)); + if (r < 0) LERROR("sendto failed: " SOCKETERROR); +} + +static void fill_port_info(struct PortInfo* pi, uint16_t id, uint8_t active) { + static uint8_t mac[6] = { 0x05, 0x0C, 0x0C, 0x00, 0x00, 0x01 }; + pi->pad_id = id; + pi->state = (id == 0) ? 0x02 : 0x00; // Connected : Disconnected + pi->connection_type = 0x01; // Usb + pi->model = 0x02; // DS4 + pi->battery = 0x04; // High + pi->active = active; + memcpy(pi->mac, mac, 5); + pi->mac[5] = 1 + id; +} + +static void send_gyro_data(int fd, CEHClient* target, uint16_t id, float data[6], uint64_t timestamp) { + struct Message out; + memset(&out, 0, sizeof(struct Message)); + fill_port_info(&out.pad_data.pad_info, id, 1); + memcpy(&out.pad_data.accel, data, sizeof(float) * 6); + out.pad_data.motion_timestamp = timestamp * 1000; + out.pad_data.packet_number = target->next_packet_no ++; + send_msg(fd, &target->address, &out, DSUS_PADDATARSP, 80); + // DEBUG("Sent data to (0x%x)", target->address.sin_port); +} + +static void parse_message(int fd, const char* buffer, size_t size, struct sockaddr_in* source) { + struct Message* msg = (struct Message*)&buffer[0]; + struct Message out; + int i, x; + if ((size < 20) || (buffer[0] != 'D') || (buffer[1]!='S') || (buffer[2] != 'U') || (buffer[3] != 'C')) { + WARN("Recieved invalid message: Invalid header"); + return; + } + if (msg->protocol_version > MAX_PROTO_VERSION) { + WARN("Recieved invalid message: Unsupported version"); + return; + } + if (size < msg->packet_size + 20 - 4) { + WARN("Recieved invalid message: Invalid size (expected %i, got %i)", msg->packet_size + 20 - 4, size); + return; + } + + switch (msg->message_type) { + case DSUC_VERSIONREQ: + out.version_info.min_version = 0; + out.version_info.max_version = MAX_PROTO_VERSION; + send_msg(fd, source, &out, DSUS_VERSIONRSP, 4); + break; + case DSUC_LISTPORTS: + if ((msg->list_ports.count > 0) && (msg->list_ports.count <= 4)) { + for (i=0; i<msg->list_ports.count; i++) + fill_port_info(&out.port_info[i], msg->list_ports.ids[i], 0); + send_msg(fd, source, &out, DSUS_PORTINFO, 12 * msg->list_ports.count); + } + break; + case DSUC_PADDATAREQ: { + if (!((msg->pad_data_req.flags == 0) + || (((msg->pad_data_req.flags & 0x01) != 0) && (msg->pad_data_req.id == 0)))) { + // Only querying by ID and querying for 1st controller is supported + WARN("Refusing request: flags=%x id=%x mac=%x:%x:%x:%x:%x:%x", + msg->pad_data_req.flags, msg->pad_data_req.id, + msg->pad_data_req.mac[0], + msg->pad_data_req.mac[1], + msg->pad_data_req.mac[2], + msg->pad_data_req.mac[3], + msg->pad_data_req.mac[4], + msg->pad_data_req.mac[5] + ); + break; + } + CEHClient* c = NULL; +#ifdef PYTHON + for (x=0; x<CLIENT_LIMIT; x++) { + if (clients[x].address.sin_port == 0) + continue; + CEHClient* i = &clients[x]; +#else + FOREACH_IN(CEHClient*, i, clients) { +#endif + if (i->address.sin_port == source->sin_port) { + // Server listens only on localhost, so it should be safe to assume IP matches + c = i; + break; + } + } + if (c == NULL) { +#ifdef PYTHON + for (x=0; x<CLIENT_LIMIT; x++) { + if (clients[x].address.sin_port == 0) { + c = &clients[x]; + } + } + if (c == NULL) { + WARN("Client limit reached"); + break; + } +#else + c = malloc(sizeof(CEHClient)); + if ((c == NULL) || (!list_allocate(clients, 1))) { + WARN("Out of memory"); + free(c); + break; + } + list_add(clients, c); +#endif + memcpy(&c->address, source, sizeof(struct sockaddr_in)); + c->next_packet_no = mono_time_ms() & 0xFFFFFFFF; + DEBUG("New client (0x%x) added", c->address.sin_port); + } + c->last_seen = mono_time_ms(); + break; + } + default: + // WARN("Recieved invalid message: Unknown message type"); + return; + } +} + +#ifdef PYTHON +bool cemuhook_feed(int fd, int index, float data[6]) { +#else +bool sccd_cemuhook_feed(int index, float data[6]) { + const int fd = sock; +#endif + monotime_t t = mono_time_ms(); +#ifdef PYTHON + int x; + for (x=0; x<CLIENT_LIMIT; x++) { + CEHClient* c = &clients[x]; + if (c->address.sin_port == 0) + continue; +#else + ListIterator it = iter_get(clients); + if (it == NULL) return false; // OOM + while (iter_has_next(it)) { + CEHClient* c = iter_next(it); +#endif + if ((t > c->last_seen + CLIENT_TIMEOUT) || (t < c->last_seen)) { + DEBUG("Dropping client (0x%x)", c->address.sin_port); + c->address.sin_port = 0; +#ifndef PYTHON + iter_remove(it); +#endif + } else { + send_gyro_data(fd, c, 0, data, (uint64_t)t); + } + } +#ifndef PYTHON + iter_free(it); +#endif + return true; +} + +#ifdef PYTHON + +const int cemuhook_module_version(void) { + return CEMUHOOK_MODULE_VERSION; +} + +void cemuhook_data_recieved(int fd, int port, const char* buffer, size_t size) { + struct sockaddr_in source; + source.sin_family = AF_INET; + source.sin_addr.s_addr = inet_addr("127.0.0.1"); + source.sin_port = htons(port); + + parse_message(fd, buffer, size, &source); +} + +bool cemuhook_socket_enable() { + int i; + for (i=0; i<CLIENT_LIMIT; i++) + clients[i].address.sin_port = 0; + // listening is done in python + return true; +} + +#else + +static void on_data_recieved(Daemon* d, int fd, void* userdata) { + char buffer[BUFFER_SIZE]; + struct sockaddr_in source; + socklen_t len = sizeof(struct sockaddr_in); + ssize_t n = recvfrom(fd, buffer, BUFFER_SIZE, 0, (struct sockaddr*)&source, &len); + + if (n < 0) { + LERROR("recvfrom: " SOCKETERROR); + return; + } + + parse_message(sock, buffer, n, &source); +} + + +bool sccd_cemuhook_socket_enable() { + clients = list_new(CEHClient, 4); + if (clients == NULL) + // This may be enabled at random time, so I can't just crash here + return false; + + struct sockaddr_in server_addr; + memset(&server_addr, 0, sizeof(struct sockaddr_in)); + server_addr.sin_family = AF_INET; + server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); + server_addr.sin_port = htons(26760); + +#ifdef _WIN32 + WSADATA wsaData; + int err = WSAStartup(MAKEWORD(2, 2), &wsaData); + if (err != 0) { + LERROR("Failed to initialize Winsock2: error %i", err); + return false; + } +#endif + sock = socket(AF_INET, SOCK_DGRAM, 0); + if (sock < 0) { + LERROR("Failed to open control socket" SOCKETERROR); + return false; + } + + /* +#ifndef _WIN32 + if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &(int){ 1 }, sizeof(int)) < 0) { +#else + if (setsockopt(sock, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, &(char){ 1 }, sizeof(char)) < 0) { +#endif + // stupid, but not fatal + WARN("setsockopt failed" SOCKETERROR); + } + */ + if (bind(sock, (const struct sockaddr *)&server_addr, sizeof(struct sockaddr_in)) < 0) { + LERROR("Bind failed" SOCKETERROR); + return false; + } + + if (!sccd_poller_add(sock, &on_data_recieved, NULL)) { + LERROR("sccd_poller_add failed to add listening socket"); + return false; + } + + LOG("Created CemuHookUDP Motion Provider"); + return true; +} + + +__attribute__((constructor)) void check_stuff() { + ASSERT(sizeof(MessageType) == sizeof(uint32_t)); + ASSERT(sizeof(float) == 4); +} + +#endif + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sc-controller-0.4.6.1/scc/cemuhook_server.py new/sc-controller-0.4.7/scc/cemuhook_server.py --- old/sc-controller-0.4.6.1/scc/cemuhook_server.py 1970-01-01 01:00:00.000000000 +0100 +++ new/sc-controller-0.4.7/scc/cemuhook_server.py 2019-07-08 14:28:01.000000000 +0200 @@ -0,0 +1,66 @@ +#!/usr/bin/env python2 +""" +SC-Controller - Daemon - CemuHookUDP motion provider + +Accepts all connections from clients and sends data captured +by 'cemuhook' actions to them. +""" +from __future__ import unicode_literals +from scc.tools import find_library +from scc.lib.enum import IntEnum +from ctypes import c_uint32, c_int, c_bool, c_char_p, c_size_t, c_float +from ctypes import create_string_buffer +import logging, socket +log = logging.getLogger("CemuHook") + +BUFFER_SIZE = 1024 +PORT = 26760 + + +class MessageType(IntEnum): + DSUC_VERSIONREQ = 0x100000 + DSUS_VERSIONRSP = 0x100000 + DSUC_LISTPORTS = 0x100001 + DSUS_PORTINFO = 0x100001 + DSUC_PADDATAREQ = 0x100002 + DSUS_PADDATARSP = 0x100002 + + +class CemuhookServer: + C_DATA_T = c_float * 6 + + def __init__(self, daemon): + self._lib = find_library('libcemuhook') + self._lib.cemuhook_data_recieved.argtypes = [ c_int, c_int, c_char_p, c_size_t ] + self._lib.cemuhook_data_recieved.restype = None + self._lib.cemuhook_feed.argtypes = [ c_int, c_int, CemuhookServer.C_DATA_T ] + self._lib.cemuhook_feed.restype = None + self._lib.cemuhook_socket_enable.argtypes = [] + self._lib.cemuhook_socket_enable.restype = c_bool + + if not self._lib.cemuhook_socket_enable(): + raise OSError("cemuhook_socket_enable failed") + + self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + + poller = daemon.get_poller() + daemon.poller.register(self.socket.fileno(), poller.POLLIN, self.on_data_recieved) + + self.socket.bind(('127.0.0.1', 26760)) + log.info("Created CemuHookUDP Motion Provider") + + + def on_data_recieved(self, fd, event_type): + if fd != self.socket.fileno(): return + message, (ip, port) = self.socket.recvfrom(BUFFER_SIZE) + buffer = create_string_buffer(BUFFER_SIZE) + self._lib.cemuhook_data_recieved(fd, port, message, len(message), buffer) + + + def feed(self, data): + c_data = CemuhookServer.C_DATA_T() + c_data[3:6] = data[0:3] + self._lib.cemuhook_feed(self.socket.fileno(), 0, c_data) + + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sc-controller-0.4.6.1/scc/constants.py new/sc-controller-0.4.7/scc/constants.py --- old/sc-controller-0.4.6.1/scc/constants.py 2018-12-21 00:20:53.000000000 +0100 +++ new/sc-controller-0.4.7/scc/constants.py 2019-07-08 14:28:01.000000000 +0200 @@ -28,7 +28,7 @@ If SC-Controller is updated while daemon is running, DAEMON_VERSION send by daemon will differ one one expected by UI and daemon will be forcefully restarted. """ -DAEMON_VERSION = "0.4.6" +DAEMON_VERSION = "0.4.7" HPERIOD = 0.02 LPERIOD = 0.5 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sc-controller-0.4.6.1/scc/gui/action_editor.py new/sc-controller-0.4.7/scc/gui/action_editor.py --- old/sc-controller-0.4.6.1/scc/gui/action_editor.py 2018-12-21 00:20:53.000000000 +0100 +++ new/sc-controller-0.4.7/scc/gui/action_editor.py 2019-07-08 14:28:01.000000000 +0200 @@ -1183,3 +1183,4 @@ rvMore.set_reveal_child(False) exMore.set_sensitive(enabled) self._modifiers_enabled = enabled + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sc-controller-0.4.6.1/scc/gui/ae/axis_action.py new/sc-controller-0.4.7/scc/gui/ae/axis_action.py --- old/sc-controller-0.4.6.1/scc/gui/ae/axis_action.py 2018-12-21 00:20:53.000000000 +0100 +++ new/sc-controller-0.4.7/scc/gui/ae/axis_action.py 2019-07-08 14:28:01.000000000 +0200 @@ -482,7 +482,7 @@ action = self.make_circular_action() elif key == 'mouse': stActionData.set_visible_child(self.builder.get_object("vbMose")) - if self.editor.friction == 0: + if not self._recursing and self.editor.friction == 0: # When switching to mouse, enable trackball by default self.editor.friction = 10 action = self.make_mouse_action() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sc-controller-0.4.6.1/scc/gui/ae/gyro_action.py new/sc-controller-0.4.7/scc/gui/ae/gyro_action.py --- old/sc-controller-0.4.6.1/scc/gui/ae/gyro_action.py 2018-12-21 00:20:53.000000000 +0100 +++ new/sc-controller-0.4.7/scc/gui/ae/gyro_action.py 2019-07-08 14:28:01.000000000 +0200 @@ -7,6 +7,7 @@ from scc.actions import Action, NoAction, MouseAction, MultiAction, RangeOP from scc.actions import GyroAction, GyroAbsAction, MouseAbsAction +from scc.special_actions import CemuHookAction from scc.modifiers import ModeModifier, SensitivityModifier from scc.uinput import Axes, Rels from scc.constants import SCButtons, STICK, YAW, ROLL @@ -118,6 +119,8 @@ self.select_gyro_output("right") elif ap[0] == Rels.REL_Y and ap[-1] == Rels.REL_X: self.select_gyro_output("mouse_stick") + elif isinstance(action, CemuHookAction): + self.select_gyro_output("cemuhook") self.modifier_updated() @@ -129,6 +132,8 @@ self._recursing = True cbInvertY.set_active(inverted) self._recursing = False + + self.update() def cbInvertY_toggled_cb(self, cb, *a): @@ -164,7 +169,7 @@ elif ap[0] == Rels.REL_Y and ap[-1] == Rels.REL_X: return True return False - if isinstance(action, (MouseAction, MouseAbsAction)): + if isinstance(action, (MouseAction, MouseAbsAction, CemuHookAction)): return True return False @@ -232,7 +237,12 @@ def update(self, *a): - pass + cbMode = self.builder.get_object("cbMode") + cbYawRoll = self.builder.get_object("cbYawRoll") + lblYawRoll = self.builder.get_object("lblYawRoll") + key = cbMode.get_model().get_value(cbMode.get_active_iter(), 2) + cbYawRoll.set_sensitive(key != "cemuhook") + lblYawRoll.set_sensitive(key != "cemuhook") def hidden(self): @@ -248,6 +258,7 @@ sclSoftLevel = self.builder.get_object("sclSoftLevel") cbGyroButton = self.builder.get_object("cbGyroButton") cbInvertGyro = self.builder.get_object("cbInvertGyro") + cbInvertGyro = self.builder.get_object("cbInvertGyro") action = cbMode.get_model().get_value(cbMode.get_active_iter(), 0) key = cbMode.get_model().get_value(cbMode.get_active_iter(), 2) yawroll = cbYawRoll.get_model().get_value(cbYawRoll.get_active_iter(), 0) @@ -279,6 +290,7 @@ else: self.editor.set_default_sensitivity(1, 1, 1) + self.update() self.editor.set_action(action) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sc-controller-0.4.6.1/scc/gui/app.py new/sc-controller-0.4.7/scc/gui/app.py --- old/sc-controller-0.4.6.1/scc/gui/app.py 2018-12-21 00:20:53.000000000 +0100 +++ new/sc-controller-0.4.7/scc/gui/app.py 2019-07-08 14:28:01.000000000 +0200 @@ -1040,7 +1040,7 @@ elif "LIBUSB_ERROR_ACCESS" in error: msg += "\n" + _("You don't have access to controller device.") msg += "\n\n" + ( _("Consult your distribution manual, try installing Steam package or <a href='%s'>install required udev rules manually</a>.") % - 'https://wiki.archlinux.org/index.php/Gamepad#Steam_Controller_Not_Pairing' ) + 'https://wiki.archlinux.org/index.php/Gamepad#Steam_Controller_not_pairing' ) # TODO: Write howto somewhere instead of linking to ArchWiki elif "LIBUSB_ERROR_BUSY" in error: msg += "\n" + _("Another application (most likely Steam) is using the controller.") @@ -1351,6 +1351,10 @@ def do_activate(self, *a): self.builder.get_object("window").show() + if self.config['gui']['minimize_on_start'] and self.statusicon.get_property("active"): + self.builder.get_object("window").hide() + else: + self.builder.get_object("window").show() def remove_dot_profile(self): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sc-controller-0.4.6.1/scc/gui/creg/dialog.py new/sc-controller-0.4.7/scc/gui/creg/dialog.py --- old/sc-controller-0.4.6.1/scc/gui/creg/dialog.py 2018-12-21 00:20:53.000000000 +0100 +++ new/sc-controller-0.4.7/scc/gui/creg/dialog.py 2019-07-08 14:28:01.000000000 +0200 @@ -25,7 +25,7 @@ from scc.config import Config import evdev -import os, logging, json +import os, logging, json, re log = logging.getLogger("CRegistration") @@ -84,14 +84,18 @@ Device is considered gamepad-like if has at least one button with keycode in gamepad range and at least two axes. """ + + # Strip special symbols + name = re.sub( r"[^a-zA-Z0-9.]+", ' ', dev.name.lower() ) + # ... but some cheating first - if "keyboard" in dev.name.lower(): + if "keyboard" in name: return False - if "touchpad" in dev.name.lower(): + if "touchpad" in name: return False - if "mouse" in dev.name.lower(): + if "mouse" in name: return False - if "gamepad" in dev.name.lower(): + if "gamepad" in name: return True caps = dev.capabilities(verbose=False) if evdev.ecodes.EV_ABS in caps: # Has axes diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sc-controller-0.4.6.1/scc/gui/dwsnc.py new/sc-controller-0.4.7/scc/gui/dwsnc.py --- old/sc-controller-0.4.6.1/scc/gui/dwsnc.py 2018-12-21 00:20:53.000000000 +0100 +++ new/sc-controller-0.4.7/scc/gui/dwsnc.py 2019-07-08 14:28:01.000000000 +0200 @@ -38,35 +38,41 @@ IS_UNITY = False IS_GNOME = False +IS_KDE = False -if "XDG_CURRENT_DESKTOP" in os.environ and "GNOME" in os.environ["XDG_CURRENT_DESKTOP"].split(":"): - IS_GNOME = True - -if "XDG_CURRENT_DESKTOP" in os.environ and "Unity" in os.environ["XDG_CURRENT_DESKTOP"].split(":"): - # User runs Unity - IS_UNITY = True +if "XDG_CURRENT_DESKTOP" in os.environ: + if "GNOME" in os.environ["XDG_CURRENT_DESKTOP"].split(":"): + IS_GNOME = True + + if "KDE" in os.environ["XDG_CURRENT_DESKTOP"].split(":"): + IS_KDE = True - def _headerbar(bar): - children = [] + bar.get_children() - pack_start = [] - pack_end = [] - for c in children: - if child_get_property(bar, c, 'pack-type') == Gtk.PackType.END: - bar.remove(c) - pack_start.append(c) - else: - bar.remove(c) + if "Unity" in os.environ["XDG_CURRENT_DESKTOP"].split(":"): + # User runs Unity + IS_UNITY = True + + def _headerbar(bar): + children = [] + bar.get_children() + pack_start = [] + pack_end = [] + for c in children: + if child_get_property(bar, c, 'pack-type') == Gtk.PackType.END: + bar.remove(c) + pack_start.append(c) + else: + bar.remove(c) + pack_end.append(c) + if len(pack_end) > 1: + c, pack_end = pack_end[0], pack_end[1:] pack_end.append(c) - if len(pack_end) > 1: - c, pack_end = pack_end[0], pack_end[1:] - pack_end.append(c) - if (Gtk.get_major_version(), Gtk.get_minor_version()) > (3, 10): - # Old ubuntu has this in order, new Ubuntu has it reversed - pack_end = reversed(pack_end) - for c in pack_start: bar.pack_start(c) - for c in pack_end: bar.pack_end(c) - headerbar = _headerbar + if (Gtk.get_major_version(), Gtk.get_minor_version()) > (3, 10): + # Old ubuntu has this in order, new Ubuntu has it reversed + pack_end = reversed(pack_end) + for c in pack_start: bar.pack_start(c) + for c in pack_end: bar.pack_end(c) + headerbar = _headerbar if not hasattr(Gtk.Label, "set_xalign"): - # GTK is old enought + # GTK is old enough fix_label_missing_set_XYalign_methods() + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sc-controller-0.4.6.1/scc/gui/statusicon.py new/sc-controller-0.4.7/scc/gui/statusicon.py --- old/sc-controller-0.4.6.1/scc/gui/statusicon.py 2018-12-21 00:20:53.000000000 +0100 +++ new/sc-controller-0.4.7/scc/gui/statusicon.py 2019-07-08 14:28:01.000000000 +0200 @@ -419,3 +419,4 @@ # Use proxy backend to determine the correct backend while the application is running return StatusIconProxy(*args, **kwargs) + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sc-controller-0.4.6.1/scc/mapper.py new/sc-controller-0.4.7/scc/mapper.py --- old/sc-controller-0.4.6.1/scc/mapper.py 2018-12-21 00:20:53.000000000 +0100 +++ new/sc-controller-0.4.7/scc/mapper.py 2019-07-08 14:28:01.000000000 +0200 @@ -418,12 +418,16 @@ if not self.lpad_touched: self.lpad_touched = True self.profile.pads[LEFT].whole(self, state.lpad_x, state.lpad_y, LEFT) + if self.old_state.buttons & STICKTILT and not self.buttons & STICKTILT: + # LPAD and stick share axes and so when they are used simultaneously (by someone with 3 hands or so :) + # this is how mapper can tell that stick was recentered + self.profile.stick.whole(self, 0, 0, STICK) elif not self.buttons & STICKTILT: # Pad is not being touched if self.lpad_touched: self.lpad_touched = False self.profile.pads[LEFT].whole(self, 0, 0, LEFT) - + # CPAD (touchpad on DS4 controller) if controller.flags & ControllerFlags.HAS_CPAD: if FE_PAD in fe or self.old_state.cpad_x != state.cpad_x or self.old_state.cpad_y != state.cpad_y: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sc-controller-0.4.6.1/scc/sccdaemon.py new/sc-controller-0.4.7/scc/sccdaemon.py --- old/sc-controller-0.4.6.1/scc/sccdaemon.py 2018-12-21 00:20:53.000000000 +0100 +++ new/sc-controller-0.4.7/scc/sccdaemon.py 2019-07-08 14:28:01.000000000 +0200 @@ -13,6 +13,7 @@ from scc.uinput import CannotCreateUInputException from scc.tools import set_logging_level, find_binary, clamp from scc.device_monitor import create_device_monitor +from scc.cemuhook_server import CemuhookServer from scc.custom import load_custom_module from scc.gestures import GestureDetector from scc.parser import TalkingActionParser @@ -63,6 +64,7 @@ self.on_exit_cbs = [] self.subprocs = [] self.lock = threading.Lock() + self.cemuhook = None self.default_mapper = None self.free_mappers = [ ] self.clients = set() @@ -276,6 +278,15 @@ log.debug("Gesture detection started on %s", what) gd.whole(mapper, x, y, what) + def on_sa_cemuhook(self, mapper, action, data): + """ Called by 'cemuhook' action """ + if self.cemuhook is None: + try: + self.cemuhook = CemuhookServer(self) + except Exception, e: + log.error("Failed to initialize CemuHookUDP Motion Provider: %s", e) + return + self.cemuhook.feed(data) def _osd(self, *data): """ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sc-controller-0.4.6.1/scc/special_actions.py new/sc-controller-0.4.7/scc/special_actions.py --- old/sc-controller-0.4.6.1/scc/special_actions.py 2018-12-21 00:20:53.000000000 +0100 +++ new/sc-controller-0.4.7/scc/special_actions.py 2019-07-08 14:28:01.000000000 +0200 @@ -755,5 +755,22 @@ self.execute(mapper, x, y, what) +class CemuHookAction(Action, SpecialAction): + SA = COMMAND = "cemuhook" + MAGIC_GYRO = (2000.0 / 32768.0) + + def gyro(self, mapper, *pyr): + sa_data = ( + pyr[0] * CemuHookAction.MAGIC_GYRO, + -pyr[1] * CemuHookAction.MAGIC_GYRO, + -pyr[2] * CemuHookAction.MAGIC_GYRO, + ) + self.execute(mapper, sa_data) + + def describe(self, context): + if self.name: return self.name + return _("CemuHook") + # Register actions from current module Action.register_all(sys.modules[__name__]) + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sc-controller-0.4.6.1/scripts/sc-controller.desktop new/sc-controller-0.4.7/scripts/sc-controller.desktop --- old/sc-controller-0.4.6.1/scripts/sc-controller.desktop 2018-12-21 00:20:53.000000000 +0100 +++ new/sc-controller-0.4.7/scripts/sc-controller.desktop 2019-07-08 14:28:01.000000000 +0200 @@ -7,4 +7,4 @@ Type=Application Icon=sc-controller Categories=GTK;Settings;HardwareSettings; -MimeType=application/x-steamcontrollerdb-profile;application/x-scc-profile;application/x-scc-profile-package +MimeType=application/x-steamcontrollerdb-profile;application/x-scc-profile;application/x-scc-profile-package; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sc-controller-0.4.6.1/setup.py new/sc-controller-0.4.7/setup.py --- old/sc-controller-0.4.6.1/setup.py 2018-12-21 00:20:53.000000000 +0100 +++ new/sc-controller-0.4.7/setup.py 2019-07-08 14:28:01.000000000 +0200 @@ -66,8 +66,11 @@ platforms = ['Linux'], ext_modules = [ Extension('libuinput', sources = ['scc/uinput.c']), + Extension('libcemuhook', define_macros = [('PYTHON', 1)], + sources = ['scc/cemuhook_server.c'], libraries = ["z"]), Extension('libhiddrv', sources = ['scc/drivers/hiddrv.c']), Extension('libsc_by_bt', sources = ['scc/drivers/sc_by_bt.c']), Extension('libremotepad', sources = ['scc/drivers/remotepad_controller.c']), ] ) + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sc-controller-0.4.6.1/tests/test_parser/test_actions.py new/sc-controller-0.4.7/tests/test_parser/test_actions.py --- old/sc-controller-0.4.6.1/tests/test_parser/test_actions.py 2018-12-21 00:20:53.000000000 +0100 +++ new/sc-controller-0.4.7/tests/test_parser/test_actions.py 2019-07-08 14:28:01.000000000 +0200 @@ -270,8 +270,8 @@ AxisAction(Axes.ABS_X), AxisAction(Axes.ABS_Y) )) - - + + def test_relXY(self): """ Tests if relXYAciton can be converted to string and parsed back to @@ -281,8 +281,8 @@ AxisAction(Axes.ABS_RX), AxisAction(Axes.ABS_RY) )) - - + + def test_trigger(self): """ Tests if TriggerAction can be converted to string and parsed back to @@ -292,3 +292,4 @@ 15, 234, ButtonAction(Keys.KEY_A) )) + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sc-controller-0.4.6.1/tests/test_parser/test_special_actions.py new/sc-controller-0.4.7/tests/test_parser/test_special_actions.py --- old/sc-controller-0.4.6.1/tests/test_parser/test_special_actions.py 2018-12-21 00:20:53.000000000 +0100 +++ new/sc-controller-0.4.7/tests/test_parser/test_special_actions.py 2019-07-08 14:28:01.000000000 +0200 @@ -143,3 +143,11 @@ 'LRLR', TurnOffAction() ) ) + + + def test_cemuhook(self): + """ + Nothing to test here + """ + pass + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sc-controller-0.4.6.1/tests/test_profile/test_special_actions.py new/sc-controller-0.4.7/tests/test_profile/test_special_actions.py --- old/sc-controller-0.4.6.1/tests/test_profile/test_special_actions.py 2018-12-21 00:20:53.000000000 +0100 +++ new/sc-controller-0.4.7/tests/test_profile/test_special_actions.py 2019-07-08 14:28:01.000000000 +0200 @@ -161,3 +161,11 @@ assert isinstance(a, OSDAction) assert isinstance(a.action, GesturesAction) assert isinstance(a.action.gestures['UD'], TurnOffAction) + + + def test_cemuhook(self): + """ + Nothing to test here + """ + pass +
