commit c50b5646e9f54b8924a463ba882a3f0b5cd88324 Author: Gustavo Adolfo Mesa Roldán Date: Fri Feb 14 20:10:36 2020 +0100 initial diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1874d06 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.pyc +*.swp +test.* diff --git a/img/1.jpg b/img/1.jpg new file mode 100644 index 0000000..574fdb7 Binary files /dev/null and b/img/1.jpg differ diff --git a/img/2.gif b/img/2.gif new file mode 100644 index 0000000..4b45ac1 Binary files /dev/null and b/img/2.gif differ diff --git a/img/2.jpg b/img/2.jpg new file mode 100644 index 0000000..c37f6c7 Binary files /dev/null and b/img/2.jpg differ diff --git a/img/3.jpg b/img/3.jpg new file mode 100644 index 0000000..9a1742c Binary files /dev/null and b/img/3.jpg differ diff --git a/img/4.jpg b/img/4.jpg new file mode 100644 index 0000000..a3e92ea Binary files /dev/null and b/img/4.jpg differ diff --git a/img/5.jpg b/img/5.jpg new file mode 100644 index 0000000..b9e2e82 Binary files /dev/null and b/img/5.jpg differ diff --git a/img/Captura de pantalla de 2019-07-04 06-46-11.png b/img/Captura de pantalla de 2019-07-04 06-46-11.png new file mode 100644 index 0000000..64a3c47 Binary files /dev/null and b/img/Captura de pantalla de 2019-07-04 06-46-11.png differ diff --git a/img/pyboy.svg b/img/pyboy.svg new file mode 100644 index 0000000..2c67678 --- /dev/null +++ b/img/pyboy.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + diff --git a/logitech/__init__.py b/logitech/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/logitech/applets/__init__.py b/logitech/applets/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/logitech/applets/simple_bg_light/__init__.py b/logitech/applets/simple_bg_light/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/logitech/applets/simple_bg_light/simple_bg_light.py b/logitech/applets/simple_bg_light/simple_bg_light.py new file mode 100644 index 0000000..6406e59 --- /dev/null +++ b/logitech/applets/simple_bg_light/simple_bg_light.py @@ -0,0 +1,84 @@ +from logitech.g19_keys import (Data, Key) +from logitech.g19_receivers import InputProcessor + +class SimpleBgLight(object): + '''Simple color changing. + + Enable M1..3 for red/green/blue and use the scroll to change the intensity + for the currently selected colors. + + ''' + + def __init__(self, lg19): + self.__lg19 = lg19 + self.__redEnabled = False + self.__greenEnabled = False + self.__blueEnabled = False + self.__curColor = [255, 255, 255] + + def _clamp_current_color(self): + '''Assures that all color components are in [0, 255].''' + for i in range(3): + val = self.__curColor[i] + self.__curColor[i] = val if val >= 0 else 0 + val = self.__curColor[i] + self.__curColor[i] = val if val <= 255 else 255 + + def _update_leds(self): + '''Updates M-leds according to enabled state.''' + val = 0 + if self.__redEnabled: + val |= Data.LIGHT_KEY_M1 + if self.__greenEnabled: + val |= Data.LIGHT_KEY_M2 + if self.__blueEnabled: + val |= Data.LIGHT_KEY_M3 + self.__lg19.set_enabled_m_keys(val) + + def get_input_processor(self): + return self + + def process_input(self, evt): + processed = False + if Key.M1 in evt.keysDown: + self.__redEnabled = not self.__redEnabled + self._update_leds() + processed = True + if Key.M2 in evt.keysDown: + self.__greenEnabled = not self.__greenEnabled + self._update_leds() + processed = True + if Key.M3 in evt.keysDown: + self.__blueEnabled = not self.__blueEnabled + self._update_leds() + processed = True + + oldColor = list(self.__curColor) + diffVal = 0 + scrollUsed = False + + if Key.SCROLL_UP in evt.keysDown: + diffVal = 10 + scrollUsed = True + if Key.SCROLL_DOWN in evt.keysDown: + diffVal = -10 + scrollUsed = True + + atLeastOneColorIsEnabled = False + + if self.__redEnabled: + self.__curColor[0] += diffVal + atLeastOneColorIsEnabled = True + if self.__greenEnabled: + self.__curColor[1] += diffVal + atLeastOneColorIsEnabled = True + if self.__blueEnabled: + self.__curColor[2] += diffVal + atLeastOneColorIsEnabled = True + + self._clamp_current_color() + processed = processed or atLeastOneColorIsEnabled and scrollUsed + + if oldColor != self.__curColor: + self.__lg19.set_bg_color(*self.__curColor) + return processed diff --git a/logitech/applets/simple_display_brightness/__init__.py b/logitech/applets/simple_display_brightness/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/logitech/applets/simple_display_brightness/simple_display_brightness.py b/logitech/applets/simple_display_brightness/simple_display_brightness.py new file mode 100644 index 0000000..a94c630 --- /dev/null +++ b/logitech/applets/simple_display_brightness/simple_display_brightness.py @@ -0,0 +1,42 @@ +from logitech.g19_keys import (Data, Key) +from logitech.g19_receivers import InputProcessor + +class SimpleDisplayBrightness(object): + '''Simple adjustment of display brightness. + + Uses scroll to adjust display brightness. + + ''' + + def __init__(self, lg19): + self.__lg19 = lg19 + self.__curBrightness = 100 + + @staticmethod + def _clamp_brightness(val): + '''Clamps given value to [0, 100].''' + val = val if val >= 0 else 0 + val = val if val <= 100 else 100 + return val + + def get_input_processor(self): + return self + + def process_input(self, evt): + usedInput = False + diffVal = 0 + + if Key.SCROLL_UP in evt.keysDown: + diffVal = 5 + usedInput = True + if Key.SCROLL_DOWN in evt.keysDown: + diffVal = -5 + usedInput = True + + oldVal = self.__curBrightness + newVal = self._clamp_brightness(oldVal + diffVal) + + if oldVal != newVal: + self.__lg19.set_display_brightness(newVal) + self.__curBrightness = newVal + return usedInput diff --git a/logitech/applets/xplanet/__init__.py b/logitech/applets/xplanet/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/logitech/applets/xplanet/xplanet.py b/logitech/applets/xplanet/xplanet.py new file mode 100644 index 0000000..72616a2 --- /dev/null +++ b/logitech/applets/xplanet/xplanet.py @@ -0,0 +1,205 @@ +from logitech.g19 import * +from logitech.g19_keys import Key +from logitech.g19_receivers import * +from logitech.runnable import Runnable + +import multiprocessing +import os +import tempfile +import threading +import time + + +class EarthImageCreator(Runnable): + '''Thread for calling xplanet for specific angles.''' + + def __init__(self, lg19, angleStart, angleStop, slot, dataStore): + '''Creates images for angles [angleStart, angleStop) and stores a the + list of frames via dataStore.store(slot, frames). + + After completion of each frame, dataStore.signal_frame_done() will be + called. + + ''' + Runnable.__init__(self) + self.__angleStart = angleStart + self.__angleStop = angleStop + self.__dataStore = dataStore + self.__lg19 = lg19 + self.__slot = slot + + def run(self): + frames = [] + try: + handle, filename = tempfile.mkstemp('.bmp') + os.close(handle) + + for i in range(self.__angleStart, self.__angleStop): + if self.is_about_to_stop(): + break + cmdline = "xplanet -geometry 320x240 -output " + cmdline += filename + cmdline += " -num_times 1 -latitude 40 -longitude " + cmdline += str(i) + os.system(cmdline) + frames.append( self.__lg19.convert_image_to_frame(filename) ); + self.__dataStore.signal_frame_done() + finally: + os.remove(filename) + self.__dataStore.store(self.__slot, frames) + + +class DataStore(object): + '''Maintains all xplanet generated frames.''' + + def __init__(self, lg19): + self.__allFrames = [] + self.__creators = [] + self.__numThreads = multiprocessing.cpu_count() + self.__lg19 = lg19 + self.__data = [[]] * self.__numThreads + self.__lock = threading.Lock() + self.__framesDone = 0 + + def abort_update(self): + self.__lock.acquire() + for creator in self.__creators: + creator.stop() + self.__lock.release() + + def get_data(self): + '''Returns all currently available data. + + @return List of all frames. If no frames are calculated, an empty list + will be returned. + + ''' + self.__lock.acquire() + frames = self.__allFrames + self.__lock.release() + return frames + + def signal_frame_done(self): + self.__lock.acquire() + self.__framesDone += 1 + print "frames done: {0}".format(self.__framesDone) + self.__lock.release() + + def update(self): + '''Regenerates all data.''' + self.__lock.acquire() + self.__framesDone = 0 + self.__data = [[]] * self.__numThreads + self.__lock.release() + threads = [] + + for i in range(self.__numThreads): + perCpu = 360 / self.__numThreads + angleStart = i * perCpu + if i == self.__numThreads - 1: + angleStop = 360 + else: + angleStop = angleStart + perCpu + c = EarthImageCreator(self.__lg19, angleStart, angleStop, i, self) + self.__lock.acquire() + self.__creators.append(c) + self.__lock.release() + c.start() + t = threading.Thread(target=c.run) + threads.append(t) + t.start() + + for t in threads: + t.join() + + self.__lock.acquire() + self.__creators = [] + self.__allFrames = [] + for frame in self.__data: + self.__allFrames += frame + self.__lock.release() + + def store(self, slot, frames): + self.__lock.acquire() + print "committing {0}".format(slot) + self.__data[slot] = frames + self.__lock.release() + + +class XplanetRenderer(Runnable): + '''Renderer which renderes current data from DataStore.''' + + def __init__(self, lg19, dataStore): + Runnable.__init__(self) + self.__dataStore = dataStore + self.__fps = 25 + self.__lastTime = time.clock() + self.__lg19 = lg19 + + def execute(self): + frames = reversed(self.__dataStore.get_data()) + if not frames: + time.sleep(1) + counter = 0 + for frame in frames: + counter += 1 + if counter > self.__fps: + counter = 0 + if self.is_about_to_stop(): + break + now = time.clock() + diff = self.__lastTime - now + (1.0 / self.__fps) + if diff > 0: + time.sleep(diff) + self.__lastTime = time.clock() + self.__lg19.send_frame(frame) + + +class XplanetInputProcessor(InputProcessor): + + def __init__(self, xplanet): + self.__xplanet = xplanet + + def process_input(self, inputEvent): + processed = False + if Key.PLAY in inputEvent.keysDown: + self.__xplanet.start() + processed = True + if Key.STOP in inputEvent.keysDown: + self.__xplanet.stop() + processed = True + return processed + + +class Xplanet(object): + + def __init__(self, lg19): + self.__dataStore = DataStore(lg19) + self.__lg19 = lg19 + self.__renderer = XplanetRenderer(lg19, self.__dataStore) + self.__inputProcessor = XplanetInputProcessor(self) + + def get_input_processor(self): + return self.__inputProcessor + + def start(self): + t = threading.Thread(target=self.__dataStore.update) + t.start() + t = threading.Thread(target=self.__renderer.run) + self.__renderer.start() + t.start() + + def stop(self): + self.__renderer.stop() + self.__dataStore.abort_update() + + +if __name__ == '__main__': + lg19 = G19() + xplanet = Xplanet(lg19) + xplanet.start() + try: + while True: + time.sleep(10) + finally: + xplanet.stop() diff --git a/logitech/g19.py b/logitech/g19.py new file mode 100644 index 0000000..b1fcdfa --- /dev/null +++ b/logitech/g19.py @@ -0,0 +1,484 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# 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 3 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, see . + +from .g19_receivers import G19Receiver + +import sys +import threading +import time +import usb +from PIL import Image as Img +import logging +import array +import math +logger = logging.getLogger(__name__) + +class G19(object): + '''Simple access to Logitech G19 features. + + All methods are thread-safe if not denoted otherwise. + + ''' + + def __init__(self, resetOnStart=False, enable_mm_keys=False, write_timeout = 10000, reset_wait = 0): + '''Initializes and opens the USB device.''' + + logger.info("Setting up G19 with write timeout of %d", write_timeout) + self.enable_mm_keys = enable_mm_keys + self.__write_timeout = write_timeout + self.__usbDevice = G19UsbController(resetOnStart, enable_mm_keys, reset_wait) + self.__usbDeviceMutex = threading.Lock() + self.__keyReceiver = G19Receiver(self) + self.__threadDisplay = None + + self.__frame_content = [0x10, 0x0F, 0x00, 0x58, 0x02, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x3F, 0x01, 0xEF, 0x00, 0x0F] + for i in range(16, 256): + self.__frame_content.append(i) + for i in range(256): + self.__frame_content.append(i) + + @staticmethod + def convert_image_to_frame(filename): + '''Loads image from given file. + + Format will be auto-detected. If neccessary, the image will be resized + to 320x240. + + @return Frame data to be used with send_frame(). + + ''' + img = Img.open(filename) + access = img.load() + if img.size != (320, 240): + img = img.resize((320, 240), Img.CUBIC) + access = img.load() + data = [] + for x in range(320): + for y in range(240): + ax = access[x, y] + if len(ax) == 3: + r, g, b = ax + else: + r, g, b, a = ax + val = G19.rgb_to_uint16(r, g, b) + data.append(val & 0xff) + data.append(val >> 8) + return data + + @staticmethod + def rgb_to_uint16(r, g, b): + '''Converts a RGB value to 16bit highcolor (5-6-5). + + @return 16bit highcolor value in little-endian. + + ''' + rBits = math.trunc(r * 2**5 / 255) + gBits = math.trunc(g * 2**6 / 255) + bBits = math.trunc(b * 2**5 / 255) + + rBits = rBits if rBits <= 0b00011111 else 0b00011111 + gBits = gBits if gBits <= 0b00111111 else 0b00111111 + bBits = bBits if bBits <= 0b00011111 else 0b00011111 + + # print(rBits) + # print(gBits) + # print(bBits) + # print("........") + # print("{0:b}".format(rBits)) + # print("{0:b}".format(gBits)) + # print("{0:b}".format(bBits)) + # print(";;;;;;;;") + # print("{0:b}".format(rBits << 3)) + # print("{0:b}".format(gBits >> 3)) + # print("{0:b}".format(gBits << 5)) + # print("{0:b}".format(bBits)) + # #print("{0:b}".format(bBits)) + valueH = (gBits << 5) | bBits + valueL = (rBits << 3) | (gBits >> 3) + value = (rBits << 11) | (gBits << 5) | bBits + + # print("=======") + # print(valueL) + # print(valueH) + # print(value) + # print("****") + # print("{0:b}".format(valueL)) + # print("{0:b}".format(valueH)) + # print("{0:b}".format(r)) + # print("{0:b}".format(value)) + return value + + def add_input_processor(self, input_processor): + self.__keyReceiver.add_input_processor(input_processor) + + def add_applet(self, applet): + '''Starts an applet.''' + self.add_input_processor(applet.get_input_processor()) + + def fill_display_with_color(self, r, g, b): + '''Fills display with given color.''' + # 16bit highcolor format: 5 red, 6 gree, 5 blue + # saved in little-endian, because USB is little-endian + value = self.rgb_to_uint16(r, g, b) + valueH = value & 0xff + valueL = value >> 8 + frame = [valueH, valueL] * (320 * 240) + self.send_frame(frame) + + def load_image(self, filename): + '''Loads image from given file. + + Format will be auto-detected. If neccessary, the image will be resized + to 320x240. + + ''' + self.send_frame(self.convert_image_to_frame(filename)) + + def read_g_and_m_keys(self, maxLen=20): + '''Reads interrupt data from G, M and light switch keys. + + @return maxLen Maximum number of bytes to read. + @return Read data or empty list. + + ''' + self.__usbDeviceMutex.acquire() + val = [] + try: + val = list(self.__usbDevice.handleIf1.interruptRead( + 0x83, maxLen, 10)) + except usb.USBError as e: + if e.message != "Connection timed out": + logger.debug("Error reading g and m keys", exc_info = e) + pass + finally: + self.__usbDeviceMutex.release() + return val + + def read_display_menu_keys(self): + '''Reads interrupt data from display keys. + + @return Read data or empty list. + + ''' + self.__usbDeviceMutex.acquire() + val = [] + try: + val = list(self.__usbDevice.handleIf0.interruptRead(0x81, 2, 10)) + except usb.USBError as e: + if e.message != "Connection timed out": + logger.debug("Error reading display menu keys", exc_info = e) + pass + finally: + self.__usbDeviceMutex.release() + return val + + def read_multimedia_keys(self): + '''Reads interrupt data from multimedia keys. + + @return Read data or empty list. + + ''' + if not self.enable_mm_keys: + return False + + self.__usbDeviceMutex.acquire() + val = [] + try: + val = list(self.__usbDevice.handleIfMM.interruptRead(0x82, 2, 10)) + except usb.USBError as e: + if e.message != "Connection timed out": + logger.debug("Error reading multimedia keys", exc_info = e) + pass + finally: + self.__usbDeviceMutex.release() + return val + + def reset(self): + '''Initiates a bus reset to USB device.''' + self.__usbDeviceMutex.acquire() + try: + self.__usbDevice.reset() + finally: + self.__usbDeviceMutex.release() + + def save_default_bg_color(self, r, g, b): + '''This stores given color permanently to keyboard. + + After a reset this will be color used by default. + + ''' + rtype = usb.TYPE_CLASS | usb.RECIP_INTERFACE + colorData = [7, r, g, b] + self.__usbDeviceMutex.acquire() + try: + self.__usbDevice.handleIf1.controlMsg( + rtype, 0x09, colorData, 0x308, 0x01, self.__write_timeout) + finally: + self.__usbDeviceMutex.release() + + def set_display_colorful(self): + '''This is an example how to create an image having a green to red + transition from left to right and a black to blue from top to bottom. + + ''' + data = [] + for i in range(320 * 240 * 2): + data.append(0) + for x in range(320): + for y in range(240): + data[2*(x*240+y)] = self.rgb_to_uint16( + 255 * x / 320, 255 * (320 - x) / 320, 255 * y / 240) >> 8 + data[2*(x*240+y)+1] = self.rgb_to_uint16( + 255 * x / 320, 255 * (320 - x) / 320, 255 * y / 240) & 0xff + self.send_frame(data) + + def send_frame(self, data): + '''Sends a frame to display. + + @param data 320x240x2 bytes, containing the frame in little-endian + 16bit highcolor (5-6-5) format. + Image must be row-wise, starting at upper left corner and ending at + lower right. This means (data[0], data[1]) is the first pixel and + (data[239 * 2], data[239 * 2 + 1]) the lower left one. + + ''' + if len(data) != (320 * 240 * 2): + raise ValueError("illegal frame size: " + str(len(data)) + + " should be 320x240x2=" + str(320 * 240 * 2)) + + frame = list(self.__frame_content) + frame += data + + self.__usbDeviceMutex.acquire() + try: + self.__usbDevice.handleIf0.bulkWrite(2, frame, self.__write_timeout) + finally: + self.__usbDeviceMutex.release() + + def set_bg_color(self, r, g, b): + '''Sets backlight to given color.''' + rtype = usb.TYPE_CLASS | usb.RECIP_INTERFACE + colorData = [7, r, g, b] + self.__usbDeviceMutex.acquire() + try: + self.__usbDevice.handleIf1.controlMsg( + rtype, 0x09, colorData, 0x307, 0x01, 10000) + finally: + self.__usbDeviceMutex.release() + + def set_enabled_m_keys(self, keys): + '''Sets currently lit keys as an OR-combination of LIGHT_KEY_M1..3,R. + + example: + from logitech.g19_keys import Data + lg19 = G19() + lg19.set_enabled_m_keys(Data.LIGHT_KEY_M1 | Data.LIGHT_KEY_MR) + + ''' + rtype = usb.TYPE_CLASS | usb.RECIP_INTERFACE + self.__usbDeviceMutex.acquire() + try: + self.__usbDevice.handleIf1.controlMsg( + rtype, 0x09, [5, keys], 0x305, 0x01, self.__write_timeout) + finally: + self.__usbDeviceMutex.release() + + def set_display_brightness(self, val): + '''Sets display brightness. + + @param val in [0,100] (off..maximum). + + ''' + data = [val, 0xe2, 0x12, 0x00, 0x8c, 0x11, 0x00, 0x10, 0x00] + rtype = usb.TYPE_VENDOR | usb.RECIP_INTERFACE + self.__usbDeviceMutex.acquire() + try: + self.__usbDevice.handleIf1.controlMsg(rtype, 0x0a, data, 0x0, 0x0, self.__write_timeout) + finally: + self.__usbDeviceMutex.release() + + def start_event_handling(self): + '''Start event processing (aka keyboard driver). + + This method is NOT thread-safe. + + ''' + self.stop_event_handling() + self.__threadDisplay = threading.Thread( + target=self.__keyReceiver.run) + self.__keyReceiver.start() + self.__threadDisplay.name = "EventThread" + self.__threadDisplay.setDaemon(True) + self.__threadDisplay.start() + + def stop_event_handling(self): + '''Stops event processing (aka keyboard driver). + + This method is NOT thread-safe. + + ''' + self.__keyReceiver.stop() + if self.__threadDisplay: + self.__threadDisplay.join() + self.__threadDisplay = None + + def close(self): + logger.info("Closing G19") + self.stop_event_handling() + self.__usbDevice.close() + + +class G19UsbController(object): + '''Controller for accessing the G19 USB device. + + The G19 consists of two composite USB devices: + * 046d:c228 + The keyboard consisting of two interfaces: + MI00: keyboard + EP 0x81(in) - INT the keyboard itself + MI01: (ifacMM) + EP 0x82(in) - multimedia keys, incl. scroll and Winkey-switch + + * 046d:c229 + LCD display with two interfaces: + MI00 (0x05): (iface0) via control data in: display keys + EP 0x81(in) - INT + EP 0x02(out) - BULK display itself + MI01 (0x06): (iface1) backlight + EP 0x83(in) - INT G-keys, M1..3/MR key, light key + + ''' + + def __init__(self, resetOnStart=False, enable_mm_keys=False, resetWait = 0): + self.enable_mm_keys = enable_mm_keys + logger.info("Looking for LCD device") + self.__lcd_device = self._find_device(0x046d, 0xc229) + if not self.__lcd_device: + raise usb.USBError("G19 LCD not found on USB bus") + + # Reset + self.handleIf0 = self.__lcd_device.open() + if resetOnStart: + logger.info("Resetting LCD device") + self.handleIf0.reset() + time.sleep(float(resetWait) / 1000.0) + logger.info("Re-opening LCD device") + self.handleIf0 = self.__lcd_device.open() + logger.info("Re-opened LCD device") + + self.handleIf1 = self.__lcd_device.open() + + config = self.__lcd_device.configurations[0] + display_interface = config.interfaces[0][0] + + # This is to cope with a difference in pyusb 1.0 compatibility layer + if len(config.interfaces) > 1: + macro_and_backlight_interface = config.interfaces[1][0] + else: + macro_and_backlight_interface = config.interfaces[0][1] + + try: + logger.debug("Detaching kernel driver for LCD device") + # Use .interfaceNumber for pyusb 1.0 compatibility layer + self.handleIf0.detachKernelDriver(display_interface.interfaceNumber) + logger.debug("Detached kernel driver for LCD device") + except usb.USBError as e: + logger.debug("Detaching kernel driver for LCD device failed.", exc_info = e) + + try: + logger.debug("Detaching kernel driver for macro / backlight device") + # Use .interfaceNumber for pyusb 1.0 compatibility layer + self.handleIf1.detachKernelDriver(macro_and_backlight_interface.interfaceNumber) + logger.debug("Detached kernel driver for macro / backlight device") + except usb.USBError as e: + logger.debug("Detaching kernel driver for macro / backlight device failed.", exc_info = e) + + logger.debug("Setting configuration") + + #self.handleIf0.setConfiguration(1) + #self.handleIf1.setConfiguration(1) + + logger.debug("Claiming LCD interface") + self.handleIf0.claimInterface(display_interface.interfaceNumber) + logger.info("Claimed LCD interface") + logger.debug("Claiming macro interface") + self.handleIf1.claimInterface(macro_and_backlight_interface.interfaceNumber) + logger.info("Claimed macro interface") + + if self.enable_mm_keys: + logger.debug("Looking for multimedia keys device") + self.__kbd_device = self._find_device(0x046d, 0xc228) + if not self.__kbd_device: + raise usb.USBError("G19 keyboard not found on USB bus") + self.handleIfMM = self.__kbd_device.open() + + if resetOnStart: + logger.debug("Resetting multimedia keys device") + self.handleIfMM.reset() + logger.debug("Re-opening multimedia keys device") + self.handleIfMM = self.__kbd_device.open() + logger.debug("Re-opened multimedia keys device") + + + config = self.__kbd_device.configurations[0] + ifacMM = config.interfaces[1][0] + + try: + self.handleIfMM.setConfiguration(1) + except usb.USBError as e: + logger.debug("Error when trying to set configuration", exc_info = e) + pass + try: + logger.debug("Detaching kernel driver for multimedia keys device") + self.handleIfMM.detachKernelDriver(ifacMM) + logger.debug("Detached kernel driver for multimedia keys device") + except usb.USBError as e: + logger.debug("Detaching kernel driver for multimedia keys device failed.", exc_info = e) + + logger.debug("Claiming multimedia interface") + self.handleIfMM.claimInterface(1) + logger.info("Claimed multimedia keys interface") + + + def close(self): + if self.enable_mm_keys: + self.handleIfMM.releaseInterface() + self.handleIf1.releaseInterface() + self.handleIf0.releaseInterface() + + @staticmethod + def _find_device(idVendor, idProduct): + for bus in usb.busses(): + for dev in bus.devices: + if dev.idVendor == idVendor and \ + dev.idProduct == idProduct: + return dev + return None + + def reset(self): + '''Resets the device on the USB.''' + self.handleIf0.reset() + self.handleIf1.reset() + +def main(): + lg19 = G19() + lg19.start_event_handling() + time.sleep(20) + lg19.stop_event_handling() + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/logitech/g19_keys.py b/logitech/g19_keys.py new file mode 100644 index 0000000..fb6e22b --- /dev/null +++ b/logitech/g19_keys.py @@ -0,0 +1,178 @@ +class Key(object): + '''Static container containing all keys.''' + + # G/M keys + # light switch + LIGHT, \ + M1, \ + M2, \ + M3, \ + MR, \ + G01, \ + G02, \ + G03, \ + G04, \ + G05, \ + G06, \ + G07, \ + G08, \ + G09, \ + G10, \ + G11, \ + G12 = range(17) + + # special keys at display + BACK, \ + DOWN, \ + LEFT, \ + MENU, \ + OK, \ + RIGHT, \ + SETTINGS, \ + UP = range(G12 + 1, G12 + 9) + + # multimedia keys + WINKEY_SWITCH, \ + NEXT, \ + PREV, \ + STOP, \ + PLAY, \ + MUTE, \ + SCROLL_UP, \ + SCROLL_DOWN = range(UP + 1, UP + 9) + + mmKeys = set([ + WINKEY_SWITCH, + NEXT, + PREV, + STOP, + PLAY, + MUTE, + SCROLL_UP, + SCROLL_DOWN]) + + gmKeys = set([ + G01, + G02, + G03, + G04, + G05, + G06, + G07, + G08, + G09, + G10, + G11, + G12, + LIGHT, + M1, + M2, + M3, + MR]) + + +class Data(object): + '''Static container with all data values for all keys.''' + + ## + ## display keys + ## + + # special keys at display + # The current state of pressed keys is an OR-combination of the following + # codes. + # Incoming data always has 0x80 appended, e.g. pressing and releasing the menu + # key results in two INTERRUPT transmissions: [0x04, 0x80] and [0x00, 0x80] + # Pressing (and holding) UP and OK at the same time results in [0x88, 0x80]. + displayKeys = {} + displayKeys[0x01] = Key.SETTINGS + displayKeys[0x02] = Key.BACK + displayKeys[0x04] = Key.MENU + displayKeys[0x08] = Key.OK + displayKeys[0x10] = Key.RIGHT + displayKeys[0x20] = Key.LEFT + displayKeys[0x40] = Key.DOWN + displayKeys[0x80] = Key.UP + + + ## + ## G- and M-Keys + ## + + # these are the bit fields for setting the currently illuminated keys + # (see set_enabled_m_keys()) + LIGHT_KEY_M1 = 0x80 + LIGHT_KEY_M2 = 0x40 + LIGHT_KEY_M3 = 0x20 + LIGHT_KEY_MR = 0x10 + + # specific codes sent by M- and G-keys + # received as [0x02, keyL, keyH, 0x40] + # example: G3: [0x02, 0x04, 0x00, 0x40] + # G1 + G2 + G11: [0x02, 0x03, 0x04, 0x40] + KEY_G01 = 0x000001 + KEY_G02 = 0x000002 + KEY_G03 = 0x000004 + KEY_G04 = 0x000008 + KEY_G05 = 0x000010 + KEY_G06 = 0x000020 + KEY_G07 = 0x000040 + KEY_G08 = 0x000080 + KEY_G09 = 0x000100 + KEY_G10 = 0x000200 + KEY_G11 = 0x000400 + KEY_G12 = 0x000800 + KEY_M1 = 0x001000 + KEY_M2 = 0x002000 + KEY_M3 = 0x004000 + KEY_MR = 0x008000 + + # light switch + # this on is similar to G-keys: + # down: [0x02, 0x00, 0x00, 0x48] + # up: [0x02, 0x00, 0x00, 0x40] + KEY_LIGHT = 0x080000 + + gmKeys = {} + gmKeys[KEY_G01] = Key.G01 + gmKeys[KEY_G02] = Key.G02 + gmKeys[KEY_G03] = Key.G03 + gmKeys[KEY_G04] = Key.G04 + gmKeys[KEY_G05] = Key.G05 + gmKeys[KEY_G06] = Key.G06 + gmKeys[KEY_G07] = Key.G07 + gmKeys[KEY_G08] = Key.G08 + gmKeys[KEY_G09] = Key.G09 + gmKeys[KEY_G10] = Key.G10 + gmKeys[KEY_G11] = Key.G11 + gmKeys[KEY_G12] = Key.G12 + gmKeys[KEY_G12] = Key.G12 + gmKeys[KEY_M1] = Key.M1 + gmKeys[KEY_M2] = Key.M2 + gmKeys[KEY_M3] = Key.M3 + gmKeys[KEY_MR] = Key.MR + gmKeys[KEY_LIGHT] = Key.LIGHT + + + ## + ## MM-keys + ## + + # multimedia keys + # received as [0x01, key] + # example: NEXT+SCROLL_UP: [0x01, 0x21] + # after scroll stopped: [0x01, 0x01] + # after release: [0x01, 0x00] + mmKeys = {} + mmKeys[0x01] = Key.NEXT + mmKeys[0x02] = Key.PREV + mmKeys[0x04] = Key.STOP + mmKeys[0x08] = Key.PLAY + mmKeys[0x10] = Key.MUTE + mmKeys[0x20] = Key.SCROLL_UP + mmKeys[0x40] = Key.SCROLL_DOWN + + # winkey switch to winkey off: [0x03, 0x01] + # winkey switch to winkey on: [0x03, 0x00] + KEY_WIN_SWITCH = 0x0103 + diff --git a/logitech/g19_old.py b/logitech/g19_old.py new file mode 100644 index 0000000..fef8e6d --- /dev/null +++ b/logitech/g19_old.py @@ -0,0 +1,449 @@ +# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets +# Copyright (C) 2010 Brett Smith +# +# 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 3 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, see . + +from .g19_receivers import G19Receiver + +import sys +import threading +import time +import usb +from PIL import Image as Img +import logging +import array +import math +logger = logging.getLogger(__name__) + +class G19(object): + '''Simple access to Logitech G19 features. + + All methods are thread-safe if not denoted otherwise. + + ''' + + def __init__(self, resetOnStart=False, enable_mm_keys=False, write_timeout = 10000, reset_wait = 0): + '''Initializes and opens the USB device.''' + + logger.info("Setting up G19 with write timeout of %d", write_timeout) + self.enable_mm_keys = enable_mm_keys + self.__write_timeout = write_timeout + self.__usbDevice = G19UsbController(resetOnStart, enable_mm_keys, reset_wait) + self.__usbDeviceMutex = threading.Lock() + self.__keyReceiver = G19Receiver(self) + self.__threadDisplay = None + + self.__frame_content = [0x10, 0x0F, 0x00, 0x58, 0x02, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x3F, 0x01, 0xEF, 0x00, 0x0F] + for i in range(16, 256): + self.__frame_content.append(i) + for i in range(256): + self.__frame_content.append(i) + + @staticmethod + def convert_image_to_frame(filename): + '''Loads image from given file. + + Format will be auto-detected. If neccessary, the image will be resized + to 320x240. + + @return Frame data to be used with send_frame(). + + ''' + img = Img.open(filename) + access = img.load() + if img.size != (320, 240): + img = img.resize((320, 240), Img.CUBIC) + access = img.load() + data = [] + for x in range(320): + for y in range(240): + ax = access[x, y] + if len(ax) == 3: + r, g, b = ax + else: + r, g, b, a = ax + val = G19.rgb_to_uint16(r, g, b) + data.append(val & 0xff) + data.append(val >> 8) + return data + + @staticmethod + def rgb_to_uint16(r, g, b): + '''Converts a RGB value to 16bit highcolor (5-6-5). + @return 16bit highcolor value in little-endian. + + ''' + return (math.trunc(r * 31 / 255) << 11) | (math.trunc(g * 63 / 255) << 5) | math.trunc(b * 31 / 255) + + def add_input_processor(self, input_processor): + self.__keyReceiver.add_input_processor(input_processor) + + def add_applet(self, applet): + '''Starts an applet.''' + self.add_input_processor(applet.get_input_processor()) + + def fill_display_with_color(self, r, g, b): + '''Fills display with given color.''' + # 16bit highcolor format: 5 red, 6 gree, 5 blue + # saved in little-endian, because USB is little-endian + value = self.rgb_to_uint16(r, g, b) + valueH = value & 0xff + valueL = value >> 8 + frame = [valueH, valueL] * (320 * 240) + self.send_frame(frame) + + def load_image(self, filename): + '''Loads image from given file. + + Format will be auto-detected. If neccessary, the image will be resized + to 320x240. + + ''' + self.send_frame(self.convert_image_to_frame(filename)) + + def read_g_and_m_keys(self, maxLen=20): + '''Reads interrupt data from G, M and light switch keys. + + @return maxLen Maximum number of bytes to read. + @return Read data or empty list. + + ''' + self.__usbDeviceMutex.acquire() + val = [] + try: + val = list(self.__usbDevice.handleIf1.interruptRead( + 0x83, maxLen, 10)) + except usb.USBError as e: + if e.message != "Connection timed out": + logger.debug("Error reading g and m keys", exc_info = e) + pass + finally: + self.__usbDeviceMutex.release() + return val + + def read_display_menu_keys(self): + '''Reads interrupt data from display keys. + + @return Read data or empty list. + + ''' + self.__usbDeviceMutex.acquire() + val = [] + try: + val = list(self.__usbDevice.handleIf0.interruptRead(0x81, 2, 10)) + except usb.USBError as e: + if e.message != "Connection timed out": + logger.debug("Error reading display menu keys", exc_info = e) + pass + finally: + self.__usbDeviceMutex.release() + return val + + def read_multimedia_keys(self): + '''Reads interrupt data from multimedia keys. + + @return Read data or empty list. + + ''' + if not self.enable_mm_keys: + return False + + self.__usbDeviceMutex.acquire() + val = [] + try: + val = list(self.__usbDevice.handleIfMM.interruptRead(0x82, 2, 10)) + except usb.USBError as e: + if e.message != "Connection timed out": + logger.debug("Error reading multimedia keys", exc_info = e) + pass + finally: + self.__usbDeviceMutex.release() + return val + + def reset(self): + '''Initiates a bus reset to USB device.''' + self.__usbDeviceMutex.acquire() + try: + self.__usbDevice.reset() + finally: + self.__usbDeviceMutex.release() + + def save_default_bg_color(self, r, g, b): + '''This stores given color permanently to keyboard. + + After a reset this will be color used by default. + + ''' + rtype = usb.TYPE_CLASS | usb.RECIP_INTERFACE + colorData = [7, r, g, b] + self.__usbDeviceMutex.acquire() + try: + self.__usbDevice.handleIf1.controlMsg( + rtype, 0x09, colorData, 0x308, 0x01, self.__write_timeout) + finally: + self.__usbDeviceMutex.release() + + def set_display_colorful(self): + '''This is an example how to create an image having a green to red + transition from left to right and a black to blue from top to bottom. + + ''' + data = [] + for i in range(320 * 240 * 2): + data.append(0) + for x in range(320): + for y in range(240): + data[2*(x*240+y)] = self.rgb_to_uint16( + 255 * x / 320, 255 * (320 - x) / 320, 255 * y / 240) >> 8 + data[2*(x*240+y)+1] = self.rgb_to_uint16( + 255 * x / 320, 255 * (320 - x) / 320, 255 * y / 240) & 0xff + self.send_frame(data) + + def send_frame(self, data): + '''Sends a frame to display. + + @param data 320x240x2 bytes, containing the frame in little-endian + 16bit highcolor (5-6-5) format. + Image must be row-wise, starting at upper left corner and ending at + lower right. This means (data[0], data[1]) is the first pixel and + (data[239 * 2], data[239 * 2 + 1]) the lower left one. + + ''' + if len(data) != (320 * 240 * 2): + raise ValueError("illegal frame size: " + str(len(data)) + + " should be 320x240x2=" + str(320 * 240 * 2)) + + frame = list(self.__frame_content) + frame += data + + self.__usbDeviceMutex.acquire() + try: + self.__usbDevice.handleIf0.bulkWrite(2, frame, self.__write_timeout) + finally: + self.__usbDeviceMutex.release() + + def set_bg_color(self, r, g, b): + '''Sets backlight to given color.''' + rtype = usb.TYPE_CLASS | usb.RECIP_INTERFACE + colorData = [7, r, g, b] + self.__usbDeviceMutex.acquire() + try: + self.__usbDevice.handleIf1.controlMsg( + rtype, 0x09, colorData, 0x307, 0x01, 10000) + finally: + self.__usbDeviceMutex.release() + + def set_enabled_m_keys(self, keys): + '''Sets currently lit keys as an OR-combination of LIGHT_KEY_M1..3,R. + + example: + from logitech.g19_keys import Data + lg19 = G19() + lg19.set_enabled_m_keys(Data.LIGHT_KEY_M1 | Data.LIGHT_KEY_MR) + + ''' + rtype = usb.TYPE_CLASS | usb.RECIP_INTERFACE + self.__usbDeviceMutex.acquire() + try: + self.__usbDevice.handleIf1.controlMsg( + rtype, 0x09, [5, keys], 0x305, 0x01, self.__write_timeout) + finally: + self.__usbDeviceMutex.release() + + def set_display_brightness(self, val): + '''Sets display brightness. + + @param val in [0,100] (off..maximum). + + ''' + data = [val, 0xe2, 0x12, 0x00, 0x8c, 0x11, 0x00, 0x10, 0x00] + rtype = usb.TYPE_VENDOR | usb.RECIP_INTERFACE + self.__usbDeviceMutex.acquire() + try: + self.__usbDevice.handleIf1.controlMsg(rtype, 0x0a, data, 0x0, 0x0, self.__write_timeout) + finally: + self.__usbDeviceMutex.release() + + def start_event_handling(self): + '''Start event processing (aka keyboard driver). + + This method is NOT thread-safe. + + ''' + self.stop_event_handling() + self.__threadDisplay = threading.Thread( + target=self.__keyReceiver.run) + self.__keyReceiver.start() + self.__threadDisplay.name = "EventThread" + self.__threadDisplay.setDaemon(True) + self.__threadDisplay.start() + + def stop_event_handling(self): + '''Stops event processing (aka keyboard driver). + + This method is NOT thread-safe. + + ''' + self.__keyReceiver.stop() + if self.__threadDisplay: + self.__threadDisplay.join() + self.__threadDisplay = None + + def close(self): + logger.info("Closing G19") + self.stop_event_handling() + self.__usbDevice.close() + + +class G19UsbController(object): + '''Controller for accessing the G19 USB device. + + The G19 consists of two composite USB devices: + * 046d:c228 + The keyboard consisting of two interfaces: + MI00: keyboard + EP 0x81(in) - INT the keyboard itself + MI01: (ifacMM) + EP 0x82(in) - multimedia keys, incl. scroll and Winkey-switch + + * 046d:c229 + LCD display with two interfaces: + MI00 (0x05): (iface0) via control data in: display keys + EP 0x81(in) - INT + EP 0x02(out) - BULK display itself + MI01 (0x06): (iface1) backlight + EP 0x83(in) - INT G-keys, M1..3/MR key, light key + + ''' + + def __init__(self, resetOnStart=False, enable_mm_keys=False, resetWait = 0): + self.enable_mm_keys = enable_mm_keys + logger.info("Looking for LCD device") + self.__lcd_device = self._find_device(0x046d, 0xc229) + if not self.__lcd_device: + raise usb.USBError("G19 LCD not found on USB bus") + + # Reset + self.handleIf0 = self.__lcd_device.open() + if resetOnStart: + logger.info("Resetting LCD device") + self.handleIf0.reset() + time.sleep(float(resetWait) / 1000.0) + logger.info("Re-opening LCD device") + self.handleIf0 = self.__lcd_device.open() + logger.info("Re-opened LCD device") + + self.handleIf1 = self.__lcd_device.open() + + config = self.__lcd_device.configurations[0] + display_interface = config.interfaces[0][0] + + # This is to cope with a difference in pyusb 1.0 compatibility layer + if len(config.interfaces) > 1: + macro_and_backlight_interface = config.interfaces[1][0] + else: + macro_and_backlight_interface = config.interfaces[0][1] + + try: + logger.debug("Detaching kernel driver for LCD device") + # Use .interfaceNumber for pyusb 1.0 compatibility layer + self.handleIf0.detachKernelDriver(display_interface.interfaceNumber) + logger.debug("Detached kernel driver for LCD device") + except usb.USBError as e: + logger.debug("Detaching kernel driver for LCD device failed.", exc_info = e) + + try: + logger.debug("Detaching kernel driver for macro / backlight device") + # Use .interfaceNumber for pyusb 1.0 compatibility layer + self.handleIf1.detachKernelDriver(macro_and_backlight_interface.interfaceNumber) + logger.debug("Detached kernel driver for macro / backlight device") + except usb.USBError as e: + logger.debug("Detaching kernel driver for macro / backlight device failed.", exc_info = e) + + logger.debug("Setting configuration") + + #self.handleIf0.setConfiguration(1) + #self.handleIf1.setConfiguration(1) + + logger.debug("Claiming LCD interface") + self.handleIf0.claimInterface(display_interface.interfaceNumber) + logger.info("Claimed LCD interface") + logger.debug("Claiming macro interface") + self.handleIf1.claimInterface(macro_and_backlight_interface.interfaceNumber) + logger.info("Claimed macro interface") + + if self.enable_mm_keys: + logger.debug("Looking for multimedia keys device") + self.__kbd_device = self._find_device(0x046d, 0xc228) + if not self.__kbd_device: + raise usb.USBError("G19 keyboard not found on USB bus") + self.handleIfMM = self.__kbd_device.open() + + if resetOnStart: + logger.debug("Resetting multimedia keys device") + self.handleIfMM.reset() + logger.debug("Re-opening multimedia keys device") + self.handleIfMM = self.__kbd_device.open() + logger.debug("Re-opened multimedia keys device") + + + config = self.__kbd_device.configurations[0] + ifacMM = config.interfaces[1][0] + + try: + self.handleIfMM.setConfiguration(1) + except usb.USBError as e: + logger.debug("Error when trying to set configuration", exc_info = e) + pass + try: + logger.debug("Detaching kernel driver for multimedia keys device") + self.handleIfMM.detachKernelDriver(ifacMM) + logger.debug("Detached kernel driver for multimedia keys device") + except usb.USBError as e: + logger.debug("Detaching kernel driver for multimedia keys device failed.", exc_info = e) + + logger.debug("Claiming multimedia interface") + self.handleIfMM.claimInterface(1) + logger.info("Claimed multimedia keys interface") + + + def close(self): + if self.enable_mm_keys: + self.handleIfMM.releaseInterface() + self.handleIf1.releaseInterface() + self.handleIf0.releaseInterface() + + @staticmethod + def _find_device(idVendor, idProduct): + for bus in usb.busses(): + for dev in bus.devices: + if dev.idVendor == idVendor and \ + dev.idProduct == idProduct: + return dev + return None + + def reset(self): + '''Resets the device on the USB.''' + self.handleIf0.reset() + self.handleIf1.reset() + +def main(): + lg19 = G19() + lg19.start_event_handling() + time.sleep(20) + lg19.stop_event_handling() + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/logitech/g19_qt.py b/logitech/g19_qt.py new file mode 100644 index 0000000..477e681 --- /dev/null +++ b/logitech/g19_qt.py @@ -0,0 +1,86 @@ +from PyQt4 import QtCore +from PyQt4 import QtGui + + +def convert_image(img): + data = [0] * (320 * 240 * 2) + for x in range(320): + for y in range(240): + val = img.pixel(x, y) + data[2*(x * 240 + y)] = val >> 8 + data[2*(x * 240 + y) + 1] = val & 0xff + return data + + +QtGui.QApplication.setGraphicsSystem("raster"); +app = QtGui.QApplication(["-graphicssystem", "raster"]) +img = QtGui.QImage(320, 240, QtGui.QImage.Format_RGB16) + +class Fuck(QtGui.QWidget): + + def __init__(self): + QtGui.QWidget.__init__(self) + self.setAttribute(QtCore.Qt.WA_PaintOnScreen) + + def paintEngine(self): + return img.paintEngine() + + #def paintEvent(self, evt): + # evt.accept() + # self.render(img) + +def logit(func): + def wrapped(*lols): + print "called" + return fund(*lols) + return wrapped + +#QtGui.QWidget.paintEngine = lambda a: img.paintEngine() +w = QtGui.QWidget() + +#w = Fuck() +w.resize(320,240) +l = QtGui.QVBoxLayout(w) +w.setLayout(l) + +text1 = QtGui.QLabel("text1", w) +l.addWidget(text1) + +text2 = QtGui.QLabel("text2", w) +l.addWidget(text2) + +button1 = QtGui.QPushButton("Push me now", w) +l.addWidget(button1) + +button2 = QtGui.QPushButton("Cancel this shit", w) +l.addWidget(button2) + +w.render(img) +data = convert_image(img) + +from logitech.g19 import G19 +lg19 = G19() +lg19.reset() + +lg19 = G19() +lg19.send_frame(data) + +evtDownPress = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, QtCore.Qt.Key_Down, QtCore.Qt.NoModifier) +evtDownRelease = QtGui.QKeyEvent(QtCore.QEvent.KeyRelease, QtCore.Qt.Key_Down, QtCore.Qt.NoModifier) + +QtCore.QCoreApplication.postEvent(w, evtDownPress) +QtCore.QCoreApplication.postEvent(w, evtDownRelease) + +w.render(img) +data = convert_image(img) + +evtDownPress = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, QtCore.Qt.Key_Tab, QtCore.Qt.NoModifier) +evtDownRelease = QtGui.QKeyEvent(QtCore.QEvent.KeyRelease, QtCore.Qt.Key_Tab, QtCore.Qt.NoModifier) + +QtCore.QCoreApplication.postEvent(w, evtDownPress) +QtCore.QCoreApplication.postEvent(w, evtDownRelease) + + +# Xvfb :1 -screen 0 320x240x16 -fbdir /tmp/lala +# xwud -in /tmp/lala/Xvfb_screen0 +# xwdtopnm diff --git a/logitech/g19_receivers.py b/logitech/g19_receivers.py new file mode 100644 index 0000000..255433f --- /dev/null +++ b/logitech/g19_receivers.py @@ -0,0 +1,238 @@ +from .g19_keys import (Data, Key) +from .runnable import Runnable + +import threading +import time + +class InputProcessor(object): + '''Object to process key presses.''' + + def process_input(self, inputEvent): + '''Processes given event. + + Should return as fast as possible. Any time-consuming processing + should be done in another thread. + + @param inputEvent Event to process. + @return True if event was consumed, or False if ignored. + + ''' + return False + + +class InputEvent(object): + '''Event created by a key press or release.''' + + def __init__(self, oldState, newState, keysDown, keysUp): + '''Creates an InputEvent. + + @param oldState State before event happened. + @param newState State after event happened. + @param keysDown Keys newly pressed. + @param keysUp Kys released by this event. + + ''' + self.oldState = oldState + self.newState = newState + self.keysDown = keysDown + self.keysUp = keysUp + + +class State(object): + '''Current state of keyboard.''' + + def __init__(self): + self.__keysDown = set() + + def _data_to_keys_g_and_m(self, data): + '''Converts a G/M keys data package to a set of keys defined as + pressed by it. + + ''' + if len(data) != 4 or data[0] != 2: + raise ValueError("not a multimedia key packet: " + str(data)) + empty = 0x400000 + curVal = data[3] << 16 | data[2] << 8 | data[1] + keys = [] + while curVal != empty: + foundAKey = False + for val in Data.gmKeys.keys(): + if val & curVal == val: + curVal ^= val + keys.append(Data.gmKeys[val]) + foundAKey = True + if not foundAKey: + raise ValueError("incorrect g/m key packet: " + + str(data)) + + return set(keys) + + def _data_to_keys_mm(self, data): + '''Converts a multimedia keys data package to a set of keys defined as + pressed by it. + + ''' + if len(data) != 2 or data[0] not in [1, 3]: + raise ValueError("not a multimedia key packet: " + str(data)) + if data[0] == 1: + curVal = data[1] + keys = [] + while curVal: + foundAKey = False + for val in Data.mmKeys.keys(): + if val & curVal == val: + curVal ^= val + keys.append(Data.mmKeys[val]) + foundAKey = True + if not foundAKey: + raise ValueError("incorrect multimedia key packet: " + + str(data)) + elif data == [3, 1]: + keys = [Key.WINKEY_SWITCH] + elif data == [3, 0]: + keys = [] + else: + raise ValueError("incorrect multimedia key packet: " + str(data)) + + return set(keys) + + def _update_keys_down(self, possibleKeys, keys): + '''Updates internal keysDown set. + + Updates the current state of all keys in 'possibleKeys' with state + given in 'keys'. + + Example: + Currently set as pressed in self.__keysDown: [A, B] + possibleKeys: [B, C, D] + keys: [C] + + This would set self.__keysDown to [A, C] and return ([C], [B]) + + @param possibleKeys Keys whose state could be given as 'pressed' at the + same time by 'keys'. + @param keys Current state of all keys in possibleKeys. + @return A pair of sets listing newly pressed and newly released keys. + + ''' + keysDown = set() + keysUp = set() + for key in possibleKeys: + if key in keys: + if key not in self.__keysDown: + self.__keysDown.add(key) + keysDown.add(key) + else: + if key in self.__keysDown: + self.__keysDown.remove(key) + keysUp.add(key) + return (keysDown, keysUp) + + def clone(self): + '''Returns an exact copy of this state.''' + state = State() + state.__keysDown = set(self.__keysDown) + return state + + def packet_received_g_and_m(self, data): + '''Mutates the state by given data packet from G- and M- keys. + + @param data Data packet received. + @return InputEvent for data packet, or None if data packet was ignored. + + ''' + oldState = self.clone() + evt = None + if len(data) == 4: + keys = self._data_to_keys_g_and_m(data) + keysDown, keysUp = self._update_keys_down(Key.gmKeys, keys) + newState = self.clone() + evt = InputEvent(oldState, newState, keysDown, keysUp) + return evt + + def packet_received_mm(self, data): + '''Mutates the state by given data packet from multimedia keys. + + @param data Data packet received. + @return InputEvent for data packet. + + ''' + oldState = self.clone() + if len(data) != 2: + raise ValueError("incorrect multimedia key packet: " + str(data)) + keys = self._data_to_keys_mm(data) + winKeySet = set([Key.WINKEY_SWITCH]) + if data[0] == 1: + # update state of all mm keys + possibleKeys = Key.mmKeys.difference(winKeySet) + keysDown, keysUp = self._update_keys_down(possibleKeys, keys) + else: + # update winkey state + keysDown, keysUp = self._update_keys_down(winKeySet, keys) + newState = self.clone() + return InputEvent(oldState, newState, keysDown, keysUp) + + +class G19Receiver(Runnable): + '''This receiver consumes all data sent by special keys.''' + + def __init__(self, g19): + Runnable.__init__(self) + self.__g19 = g19 + self.__ips = [] + self.__mutex = threading.Lock() + self.__state = State() + + def add_input_processor(self, processor): + '''Adds an input processor.''' + self.__mutex.acquire() + self.__ips.append(processor) + self.__mutex.release() + pass + + def execute(self): + gotData = False + processors = self.list_all_input_processors() + + data = self.__g19.read_multimedia_keys() + if data: + evt = self.__state.packet_received_mm(data) + if evt: + for proc in processors: + if proc.process_input(evt): + break + else: + print("mm ignored: ", data) + gotData = True + + data = self.__g19.read_g_and_m_keys() + if data: + evt = self.__state.packet_received_g_and_m(data) + if evt: + for proc in processors: + if proc.process_input(evt): + break + else: + print("m/g ignored: ", data) + gotData = True + + data = self.__g19.read_display_menu_keys() + if data: + print("dis: ", data) + gotData = True + + if not gotData: + time.sleep(0.03) + + def list_all_input_processors(self): + '''Returns a list of all input processors currently registered to this + receiver. + + @return All registered processors. This list is a copy of the internal + one. + + ''' + self.__mutex.acquire() + allProcessors = list(self.__ips) + self.__mutex.release() + return allProcessors diff --git a/logitech/runnable.py b/logitech/runnable.py new file mode 100644 index 0000000..cea6f68 --- /dev/null +++ b/logitech/runnable.py @@ -0,0 +1,69 @@ +import threading + +class Runnable(object): + '''Helper object to create thread content objects doing periodic tasks, or + tasks supporting premature termination. + + Override execute() in inherited class. This will be called until the + thread is stopped. A Runnable can be started multiple times opposed to + threading.Thread. + + To write a non-periodic task that should support premature termination, + simply override run() and call is_about_to_stop() at possible termination + points. + + ''' + + def __init__(self): + self.__keepRunning = True + self.__mutex = threading.Lock() + + def execute(self): + '''This method must be implemented and will be executed in an infinite + loop as long as stop() was not called. + + An implementation is free to check is_about_to_stop() at any time to + allow a clean termination of current processing before reaching the end + of execute(). + + ''' + pass + + def is_about_to_stop(self): + '''Returns whether this thread will terminate after completing the + current execution cycle. + + @return True if thread will terminate after current execution cycle. + + ''' + self.__mutex.acquire() + val = self.__keepRunning + self.__mutex.release() + return not val + + def run(self): + '''Implements the infinite loop. Do not override, but override + execute() instead. + + ''' + while not self.is_about_to_stop(): + self.execute() + + def start(self): + '''Starts the thread. If stop() was called, but start() was not, run() + will do nothing. + + ''' + self.__mutex.acquire() + self.__keepRunning = True + self.__mutex.release() + + def stop(self): + '''Flags this thread to be terminated after next completed execution + cycle. Calling this method will NOT stop the thread instantaniously, + but will complete the current operation and terminate in a clean way. + + ''' + self.__mutex.acquire() + self.__keepRunning = False + self.__mutex.release() diff --git a/main.py b/main.py new file mode 100644 index 0000000..ccc6eb6 --- /dev/null +++ b/main.py @@ -0,0 +1,93 @@ +from logitech.g19 import G19 + +import time +import array + +print("Start") +print("-----------------------") + +if __name__ == '__main__': + lg19 = G19(True) + lg19.start_event_handling() + try: + + data = [] + r = 255 + g = 0 + b = 255 + + # b1 = lg19.rgb_to_uint16(r,g,b) >> 8 + # b2 = lg19.rgb_to_uint16(r,g,b) & 0xff + + # print(lg19.rgb_to_uint16(r,g,b)) + # print("-----------------------") + # print(b1) + # print(b2) + + # for x in range(320): + # for y in range(240): + # data.append(0) + # data.append(b2) + + #print(str(data)) + #print(array.array('B', str(data))) + + #frame = array.array('B', str(data)) + + #lg19.fill_display_with_color(0, 0, 255) + #lg19.fill_display_with_color(255, 0, 0) + #lg19.set_display_colorful + c = 1 + while True: + lg19.load_image("img/" + str(c) + ".jpg") + if c == 5: + c = 1 + else: + c = c + 1 + time.sleep(1) + + # lg19.load_image("img/3.jpeg") + # data = [] + + # for x in range(320): + # for y in range(240): + # data.append(0) + # data.append(248) + + # print(c) + # lg19.send_frame(data) + + print("-----------------------") + finally: + print("Stop!!!!") + lg19.stop_event_handling() + +# # if you get an error: lg19 = G19(True) +# lg19 = G19() +# lg19.reset() +# print(lg19) +# # # setting backlight to red + + +# # fill your display with green +# lg19.fill_display_with_color(0, 255, 0) + +# # test your screen +# lg19.set_display_colorful() + +# # set backlight to blue after reset +# # this will be your backlight color after a bus reset (or switching the keyboard +# # off and no) +# lg19.save_default_bg_color(0, 0, 255) + +# # send an image to display +# # data = [...] # format described in g19.py +# # lg19.send_frame(data) + +# # load an arbitrary image from disk to display (will be resized non-uniform) +# lg19.load_image("img/pyboy.jpg") + +# # reset the keyboard via USB + +# # now you have to rebuild the connection: +# lg19 = G19() \ No newline at end of file