@ -0,0 +1,3 @@ | |||||
*.pyc | |||||
*.swp | |||||
test.* |
@ -0,0 +1,20 @@ | |||||
<?xml version="1.0" encoding="utf-8"?> | |||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 1500 500" style="enable-background:new 0 0 1500 500;"> | |||||
<style type="text/css"> | |||||
.st0{fill:url(#SVGID_1_);} | |||||
.st1{fill:#FFD94A;} | |||||
.st2{fill:#3771A2;} | |||||
</style> | |||||
<g transform="translate(0, -500)"> | |||||
<path class="st1" d="M123.34,605.23h115.2c62.21,0,100.67,28.8,90.17,95.23c-10.8,68.35-52.4,98.31-118.06,98.31h-42.62l-15.17,96 H77.6L123.34,605.23z M177.62,738.1h12.67c27.26,0,54.14,0,59.73-35.33c5.76-36.48-19.14-36.86-48.32-36.86h-12.67L177.62,738.1z"/> | |||||
<path class="st1" d="M326.09,605.23h90.24l41.71,78.72l66.58-78.72h90.24L484.33,755.76l-21.96,139.01H387.1l21.96-139.01 L326.09,605.23z"/> | |||||
<path class="st2" d="M586.39,894.77l45.74-289.54h107.52c51.46,0,82.86,19.97,74.13,75.26c-4.13,26.11-15.91,47.23-41.21,59.14 l-0.12,0.77c40.62,5.38,53.86,33.41,47.49,73.73c-9.59,60.67-64.58,80.64-117.57,80.64H586.39z M670.76,837.17h9.6 c22.27,0,59.72,1.15,64.76-30.72c5.52-34.94-32.27-31.49-56.85-31.49h-7.68L670.76,837.17z M689.44,718.9h8.45 c19.97,0,41.25-3.46,45.13-28.03c4.19-26.5-19-28.03-39.35-28.03h-5.38L689.44,718.9z"/> | |||||
<path class="st2" d="M1156.54,744.62c-14.8,93.7-92.44,159.74-185.37,159.74s-149.69-66.05-134.89-159.74 c13.83-87.55,100.34-148.99,183.67-148.99C1103.28,595.63,1170.37,657.07,1156.54,744.62z M914.56,745.01 c-7.89,49.92,23.21,86.4,68.14,86.4c44.93,0,87.56-36.48,95.44-86.4c6.31-39.94-24.79-76.42-69.72-76.42 C963.5,668.59,920.87,705.07,914.56,745.01z"/> | |||||
<path class="st2" d="M1133.63,605.23h90.24l41.71,78.72l66.58-78.72h90.24l-130.54,150.53l-21.96,139.01h-75.26l21.96-139.01 L1133.63,605.23z"/> | |||||
</g> | |||||
</svg> |
@ -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 |
@ -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 |
@ -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() |
@ -0,0 +1,484 @@ | |||||
# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets | |||||
# Copyright (C) 2010 Brett Smith <tanktarta@blueyonder.co.uk> | |||||
# | |||||
# 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 <http://www.gnu.org/licenses/>. | |||||
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() |
@ -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 | |||||
@ -0,0 +1,449 @@ | |||||
# Gnome15 - Suite of tools for the Logitech G series keyboards and headsets | |||||
# Copyright (C) 2010 Brett Smith <tanktarta@blueyonder.co.uk> | |||||
# | |||||
# 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 <http://www.gnu.org/licenses/>. | |||||
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() |
@ -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 |
@ -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 |
@ -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() |
@ -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() |