# 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()