You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

466 lines
16 KiB

4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
  1. # Gnome15 - Suite of tools for the Logitech G series keyboards and headsets
  2. # Copyright (C) 2010 Brett Smith <tanktarta@blueyonder.co.uk>
  3. #
  4. # This program is free software: you can redistribute it and/or modify
  5. # it under the terms of the GNU General Public License as published by
  6. # the Free Software Foundation, either version 3 of the License, or
  7. # (at your option) any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. from .g19_receivers import G19Receiver
  17. import sys
  18. import threading
  19. import time
  20. import usb
  21. from PIL import Image as Img
  22. import logging
  23. import array
  24. import math
  25. logger = logging.getLogger(__name__)
  26. class G19(object):
  27. '''Simple access to Logitech G19 features.
  28. All methods are thread-safe if not denoted otherwise.
  29. '''
  30. def __init__(self, resetOnStart=False, enable_mm_keys=False, write_timeout = 10000, reset_wait = 0):
  31. '''Initializes and opens the USB device.'''
  32. logger.info("Setting up G19 with write timeout of %d", write_timeout)
  33. self.enable_mm_keys = enable_mm_keys
  34. self.__write_timeout = write_timeout
  35. self.__usbDevice = G19UsbController(resetOnStart, enable_mm_keys, reset_wait)
  36. self.__usbDeviceMutex = threading.Lock()
  37. self.__keyReceiver = G19Receiver(self)
  38. self.__threadDisplay = None
  39. self.__frame_content = [0x10, 0x0F, 0x00, 0x58, 0x02, 0x00, 0x00, 0x00,
  40. 0x00, 0x00, 0x00, 0x3F, 0x01, 0xEF, 0x00, 0x0F]
  41. for i in range(16, 256):
  42. self.__frame_content.append(i)
  43. for i in range(256):
  44. self.__frame_content.append(i)
  45. @staticmethod
  46. def convert_image_to_frame(filename):
  47. '''Loads image from given file.
  48. Format will be auto-detected. If neccessary, the image will be resized
  49. to 320x240.
  50. @return Frame data to be used with send_frame().
  51. '''
  52. img = Img.open(filename)
  53. access = img.load()
  54. if img.size != (320, 240):
  55. img = img.resize((320, 240), Img.CUBIC)
  56. access = img.load()
  57. data = []
  58. for x in range(320):
  59. for y in range(240):
  60. ax = access[x, y]
  61. if len(ax) == 3:
  62. r, g, b = ax
  63. else:
  64. r, g, b, a = ax
  65. val = G19.rgb_to_uint16(r, g, b)
  66. data.append(val & 0xff)
  67. data.append(val >> 8)
  68. return data
  69. def convert_buffe_image_to_frame(buffe,dd):
  70. '''Loads image from given file.
  71. Format will be auto-detected. If neccessary, the image will be resized
  72. to 320x240.
  73. @return Frame data to be used with send_frame().
  74. '''
  75. access = Img.frombytes("RGB", [160,144], dd).resize((320, 240), Img.CUBIC).load()
  76. data = []
  77. for x in range(320):
  78. for y in range(240):
  79. ax = access[x, y]
  80. if len(ax) == 3:
  81. r, g, b = ax
  82. else:
  83. r, g, b, a = ax
  84. val = G19.rgb_to_uint16(r, g, b)
  85. data.append(val & 0xff)
  86. data.append(val >> 8)
  87. return data
  88. @staticmethod
  89. def rgb_to_uint16(r, g, b):
  90. '''
  91. Converts a RGB value to 16bit highcolor (5-6-5).
  92. @return 16bit highcolor value in little-endian.
  93. '''
  94. return (math.trunc(r * 31 / 255) << 11) | \
  95. (math.trunc(g * 63 / 255) << 5) | \
  96. math.trunc(b * 31 / 255)
  97. def add_input_processor(self, input_processor):
  98. self.__keyReceiver.add_input_processor(input_processor)
  99. def add_applet(self, applet):
  100. '''Starts an applet.'''
  101. self.add_input_processor(applet.get_input_processor())
  102. def fill_display_with_color(self, r, g, b):
  103. '''Fills display with given color.'''
  104. # 16bit highcolor format: 5 red, 6 gree, 5 blue
  105. # saved in little-endian, because USB is little-endian
  106. value = self.rgb_to_uint16(r, g, b)
  107. valueH = value & 0xff
  108. valueL = value >> 8
  109. frame = [valueH, valueL] * (320 * 240)
  110. self.send_frame(frame)
  111. def load_image(self, filename):
  112. '''Loads image from given file.
  113. Format will be auto-detected. If neccessary, the image will be resized
  114. to 320x240.
  115. '''
  116. self.send_frame(self.convert_image_to_frame(filename))
  117. def read_g_and_m_keys(self, maxLen=20):
  118. '''Reads interrupt data from G, M and light switch keys.
  119. @return maxLen Maximum number of bytes to read.
  120. @return Read data or empty list.
  121. '''
  122. self.__usbDeviceMutex.acquire()
  123. val = []
  124. try:
  125. val = list(self.__usbDevice.handleIf1.interruptRead(
  126. 0x83, maxLen, 10))
  127. except usb.USBError as e:
  128. if e.message != "Connection timed out":
  129. logger.debug("Error reading g and m keys", exc_info = e)
  130. pass
  131. finally:
  132. self.__usbDeviceMutex.release()
  133. return val
  134. def read_display_menu_keys(self):
  135. '''Reads interrupt data from display keys.
  136. @return Read data or empty list.
  137. '''
  138. self.__usbDeviceMutex.acquire()
  139. val = []
  140. try:
  141. val = list(self.__usbDevice.handleIf0.interruptRead(0x81, 2, 10))
  142. except usb.USBError as e:
  143. if e.message != "Connection timed out":
  144. logger.debug("Error reading display menu keys", exc_info = e)
  145. pass
  146. finally:
  147. self.__usbDeviceMutex.release()
  148. return val
  149. def read_multimedia_keys(self):
  150. '''Reads interrupt data from multimedia keys.
  151. @return Read data or empty list.
  152. '''
  153. if not self.enable_mm_keys:
  154. return False
  155. self.__usbDeviceMutex.acquire()
  156. val = []
  157. try:
  158. val = list(self.__usbDevice.handleIfMM.interruptRead(0x82, 2, 10))
  159. except usb.USBError as e:
  160. if e.message != "Connection timed out":
  161. logger.debug("Error reading multimedia keys", exc_info = e)
  162. pass
  163. finally:
  164. self.__usbDeviceMutex.release()
  165. return val
  166. def reset(self):
  167. '''Initiates a bus reset to USB device.'''
  168. self.__usbDeviceMutex.acquire()
  169. try:
  170. self.__usbDevice.reset()
  171. finally:
  172. self.__usbDeviceMutex.release()
  173. def save_default_bg_color(self, r, g, b):
  174. '''This stores given color permanently to keyboard.
  175. After a reset this will be color used by default.
  176. '''
  177. rtype = usb.TYPE_CLASS | usb.RECIP_INTERFACE
  178. colorData = [7, r, g, b]
  179. self.__usbDeviceMutex.acquire()
  180. try:
  181. self.__usbDevice.handleIf1.controlMsg(
  182. rtype, 0x09, colorData, 0x308, 0x01, self.__write_timeout)
  183. finally:
  184. self.__usbDeviceMutex.release()
  185. def set_display_colorful(self):
  186. '''This is an example how to create an image having a green to red
  187. transition from left to right and a black to blue from top to bottom.
  188. '''
  189. data = []
  190. for i in range(320 * 240 * 2):
  191. data.append(0)
  192. for x in range(320):
  193. for y in range(240):
  194. data[2*(x*240+y)] = self.rgb_to_uint16(
  195. 255 * x / 320, 255 * (320 - x) / 320, 255 * y / 240) >> 8
  196. data[2*(x*240+y)+1] = self.rgb_to_uint16(
  197. 255 * x / 320, 255 * (320 - x) / 320, 255 * y / 240) & 0xff
  198. self.send_frame(data)
  199. def send_frame(self, data):
  200. '''Sends a frame to display.
  201. @param data 320x240x2 bytes, containing the frame in little-endian
  202. 16bit highcolor (5-6-5) format.
  203. Image must be row-wise, starting at upper left corner and ending at
  204. lower right. This means (data[0], data[1]) is the first pixel and
  205. (data[239 * 2], data[239 * 2 + 1]) the lower left one.
  206. '''
  207. self.__usbDeviceMutex.acquire()
  208. try:
  209. self.__usbDevice.handleIf0.bulkWrite(2, list(self.__frame_content) + data, self.__write_timeout)
  210. finally:
  211. self.__usbDeviceMutex.release()
  212. def set_bg_color(self, r, g, b):
  213. '''Sets backlight to given color.'''
  214. rtype = usb.TYPE_CLASS | usb.RECIP_INTERFACE
  215. colorData = [7, r, g, b]
  216. self.__usbDeviceMutex.acquire()
  217. try:
  218. self.__usbDevice.handleIf1.controlMsg(
  219. rtype, 0x09, colorData, 0x307, 0x01, 10000)
  220. finally:
  221. self.__usbDeviceMutex.release()
  222. def set_enabled_m_keys(self, keys):
  223. '''Sets currently lit keys as an OR-combination of LIGHT_KEY_M1..3,R.
  224. example:
  225. from logitech.g19_keys import Data
  226. lg19 = G19()
  227. lg19.set_enabled_m_keys(Data.LIGHT_KEY_M1 | Data.LIGHT_KEY_MR)
  228. '''
  229. rtype = usb.TYPE_CLASS | usb.RECIP_INTERFACE
  230. self.__usbDeviceMutex.acquire()
  231. try:
  232. self.__usbDevice.handleIf1.controlMsg(
  233. rtype, 0x09, [5, keys], 0x305, 0x01, self.__write_timeout)
  234. finally:
  235. self.__usbDeviceMutex.release()
  236. def set_display_brightness(self, val):
  237. '''Sets display brightness.
  238. @param val in [0,100] (off..maximum).
  239. '''
  240. data = [val, 0xe2, 0x12, 0x00, 0x8c, 0x11, 0x00, 0x10, 0x00]
  241. rtype = usb.TYPE_VENDOR | usb.RECIP_INTERFACE
  242. self.__usbDeviceMutex.acquire()
  243. try:
  244. self.__usbDevice.handleIf1.controlMsg(rtype, 0x0a, data, 0x0, 0x0, self.__write_timeout)
  245. finally:
  246. self.__usbDeviceMutex.release()
  247. def start_event_handling(self):
  248. '''Start event processing (aka keyboard driver).
  249. This method is NOT thread-safe.
  250. '''
  251. self.stop_event_handling()
  252. self.__threadDisplay = threading.Thread(
  253. target=self.__keyReceiver.run)
  254. self.__keyReceiver.start()
  255. self.__threadDisplay.name = "EventThread"
  256. self.__threadDisplay.setDaemon(True)
  257. self.__threadDisplay.start()
  258. def stop_event_handling(self):
  259. '''Stops event processing (aka keyboard driver).
  260. This method is NOT thread-safe.
  261. '''
  262. self.__keyReceiver.stop()
  263. if self.__threadDisplay:
  264. self.__threadDisplay.join()
  265. self.__threadDisplay = None
  266. def close(self):
  267. logger.info("Closing G19")
  268. self.stop_event_handling()
  269. self.__usbDevice.close()
  270. class G19UsbController(object):
  271. '''Controller for accessing the G19 USB device.
  272. The G19 consists of two composite USB devices:
  273. * 046d:c228
  274. The keyboard consisting of two interfaces:
  275. MI00: keyboard
  276. EP 0x81(in) - INT the keyboard itself
  277. MI01: (ifacMM)
  278. EP 0x82(in) - multimedia keys, incl. scroll and Winkey-switch
  279. * 046d:c229
  280. LCD display with two interfaces:
  281. MI00 (0x05): (iface0) via control data in: display keys
  282. EP 0x81(in) - INT
  283. EP 0x02(out) - BULK display itself
  284. MI01 (0x06): (iface1) backlight
  285. EP 0x83(in) - INT G-keys, M1..3/MR key, light key
  286. '''
  287. def __init__(self, resetOnStart=False, enable_mm_keys=False, resetWait = 0):
  288. self.enable_mm_keys = enable_mm_keys
  289. logger.info("Looking for LCD device")
  290. self.__lcd_device = self._find_device(0x046d, 0xc229)
  291. if not self.__lcd_device:
  292. raise usb.USBError("G19 LCD not found on USB bus")
  293. # Reset
  294. self.handleIf0 = self.__lcd_device.open()
  295. if resetOnStart:
  296. logger.info("Resetting LCD device")
  297. self.handleIf0.reset()
  298. time.sleep(float(resetWait) / 1000.0)
  299. logger.info("Re-opening LCD device")
  300. self.handleIf0 = self.__lcd_device.open()
  301. logger.info("Re-opened LCD device")
  302. self.handleIf1 = self.__lcd_device.open()
  303. config = self.__lcd_device.configurations[0]
  304. display_interface = config.interfaces[0][0]
  305. # This is to cope with a difference in pyusb 1.0 compatibility layer
  306. if len(config.interfaces) > 1:
  307. macro_and_backlight_interface = config.interfaces[1][0]
  308. else:
  309. macro_and_backlight_interface = config.interfaces[0][1]
  310. try:
  311. logger.debug("Detaching kernel driver for LCD device")
  312. # Use .interfaceNumber for pyusb 1.0 compatibility layer
  313. self.handleIf0.detachKernelDriver(display_interface.interfaceNumber)
  314. logger.debug("Detached kernel driver for LCD device")
  315. except usb.USBError as e:
  316. logger.debug("Detaching kernel driver for LCD device failed.", exc_info = e)
  317. try:
  318. logger.debug("Detaching kernel driver for macro / backlight device")
  319. # Use .interfaceNumber for pyusb 1.0 compatibility layer
  320. self.handleIf1.detachKernelDriver(macro_and_backlight_interface.interfaceNumber)
  321. logger.debug("Detached kernel driver for macro / backlight device")
  322. except usb.USBError as e:
  323. logger.debug("Detaching kernel driver for macro / backlight device failed.", exc_info = e)
  324. logger.debug("Setting configuration")
  325. #self.handleIf0.setConfiguration(1)
  326. #self.handleIf1.setConfiguration(1)
  327. logger.debug("Claiming LCD interface")
  328. self.handleIf0.claimInterface(display_interface.interfaceNumber)
  329. logger.info("Claimed LCD interface")
  330. logger.debug("Claiming macro interface")
  331. self.handleIf1.claimInterface(macro_and_backlight_interface.interfaceNumber)
  332. logger.info("Claimed macro interface")
  333. if self.enable_mm_keys:
  334. logger.debug("Looking for multimedia keys device")
  335. self.__kbd_device = self._find_device(0x046d, 0xc228)
  336. if not self.__kbd_device:
  337. raise usb.USBError("G19 keyboard not found on USB bus")
  338. self.handleIfMM = self.__kbd_device.open()
  339. if resetOnStart:
  340. logger.debug("Resetting multimedia keys device")
  341. self.handleIfMM.reset()
  342. logger.debug("Re-opening multimedia keys device")
  343. self.handleIfMM = self.__kbd_device.open()
  344. logger.debug("Re-opened multimedia keys device")
  345. config = self.__kbd_device.configurations[0]
  346. ifacMM = config.interfaces[1][0]
  347. try:
  348. self.handleIfMM.setConfiguration(1)
  349. except usb.USBError as e:
  350. logger.debug("Error when trying to set configuration", exc_info = e)
  351. pass
  352. try:
  353. logger.debug("Detaching kernel driver for multimedia keys device")
  354. self.handleIfMM.detachKernelDriver(ifacMM)
  355. logger.debug("Detached kernel driver for multimedia keys device")
  356. except usb.USBError as e:
  357. logger.debug("Detaching kernel driver for multimedia keys device failed.", exc_info = e)
  358. logger.debug("Claiming multimedia interface")
  359. self.handleIfMM.claimInterface(1)
  360. logger.info("Claimed multimedia keys interface")
  361. def close(self):
  362. if self.enable_mm_keys:
  363. self.handleIfMM.releaseInterface()
  364. self.handleIf1.releaseInterface()
  365. self.handleIf0.releaseInterface()
  366. @staticmethod
  367. def _find_device(idVendor, idProduct):
  368. for bus in usb.busses():
  369. for dev in bus.devices:
  370. if dev.idVendor == idVendor and \
  371. dev.idProduct == idProduct:
  372. return dev
  373. return None
  374. def reset(self):
  375. '''Resets the device on the USB.'''
  376. self.handleIf0.reset()
  377. self.handleIf1.reset()
  378. def main():
  379. lg19 = G19()
  380. lg19.start_event_handling()
  381. time.sleep(20)
  382. lg19.stop_event_handling()
  383. if __name__ == '__main__':
  384. main()