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.

483 lines
17 KiB

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. @staticmethod
  70. def rgb_to_uint16(r, g, b):
  71. '''Converts a RGB value to 16bit highcolor (5-6-5).
  72. @return 16bit highcolor value in little-endian.
  73. '''
  74. rBits = math.trunc(r * 2**5 / 255)
  75. gBits = math.trunc(g * 2**6 / 255)
  76. bBits = math.trunc(b * 2**5 / 255)
  77. rBits = rBits if rBits <= 0b00011111 else 0b00011111
  78. gBits = gBits if gBits <= 0b00111111 else 0b00111111
  79. bBits = bBits if bBits <= 0b00011111 else 0b00011111
  80. # print(rBits)
  81. # print(gBits)
  82. # print(bBits)
  83. # print("........")
  84. # print("{0:b}".format(rBits))
  85. # print("{0:b}".format(gBits))
  86. # print("{0:b}".format(bBits))
  87. # print(";;;;;;;;")
  88. # print("{0:b}".format(rBits << 3))
  89. # print("{0:b}".format(gBits >> 3))
  90. # print("{0:b}".format(gBits << 5))
  91. # print("{0:b}".format(bBits))
  92. # #print("{0:b}".format(bBits))
  93. valueH = (gBits << 5) | bBits
  94. valueL = (rBits << 3) | (gBits >> 3)
  95. value = (rBits << 11) | (gBits << 5) | bBits
  96. # print("=======")
  97. # print(valueL)
  98. # print(valueH)
  99. # print(value)
  100. # print("****")
  101. # print("{0:b}".format(valueL))
  102. # print("{0:b}".format(valueH))
  103. # print("{0:b}".format(r))
  104. # print("{0:b}".format(value))
  105. return value
  106. def add_input_processor(self, input_processor):
  107. self.__keyReceiver.add_input_processor(input_processor)
  108. def add_applet(self, applet):
  109. '''Starts an applet.'''
  110. self.add_input_processor(applet.get_input_processor())
  111. def fill_display_with_color(self, r, g, b):
  112. '''Fills display with given color.'''
  113. # 16bit highcolor format: 5 red, 6 gree, 5 blue
  114. # saved in little-endian, because USB is little-endian
  115. value = self.rgb_to_uint16(r, g, b)
  116. valueH = value & 0xff
  117. valueL = value >> 8
  118. frame = [valueH, valueL] * (320 * 240)
  119. self.send_frame(frame)
  120. def load_image(self, filename):
  121. '''Loads image from given file.
  122. Format will be auto-detected. If neccessary, the image will be resized
  123. to 320x240.
  124. '''
  125. self.send_frame(self.convert_image_to_frame(filename))
  126. def read_g_and_m_keys(self, maxLen=20):
  127. '''Reads interrupt data from G, M and light switch keys.
  128. @return maxLen Maximum number of bytes to read.
  129. @return Read data or empty list.
  130. '''
  131. self.__usbDeviceMutex.acquire()
  132. val = []
  133. try:
  134. val = list(self.__usbDevice.handleIf1.interruptRead(
  135. 0x83, maxLen, 10))
  136. except usb.USBError as e:
  137. if e.message != "Connection timed out":
  138. logger.debug("Error reading g and m keys", exc_info = e)
  139. pass
  140. finally:
  141. self.__usbDeviceMutex.release()
  142. return val
  143. def read_display_menu_keys(self):
  144. '''Reads interrupt data from display keys.
  145. @return Read data or empty list.
  146. '''
  147. self.__usbDeviceMutex.acquire()
  148. val = []
  149. try:
  150. val = list(self.__usbDevice.handleIf0.interruptRead(0x81, 2, 10))
  151. except usb.USBError as e:
  152. if e.message != "Connection timed out":
  153. logger.debug("Error reading display menu keys", exc_info = e)
  154. pass
  155. finally:
  156. self.__usbDeviceMutex.release()
  157. return val
  158. def read_multimedia_keys(self):
  159. '''Reads interrupt data from multimedia keys.
  160. @return Read data or empty list.
  161. '''
  162. if not self.enable_mm_keys:
  163. return False
  164. self.__usbDeviceMutex.acquire()
  165. val = []
  166. try:
  167. val = list(self.__usbDevice.handleIfMM.interruptRead(0x82, 2, 10))
  168. except usb.USBError as e:
  169. if e.message != "Connection timed out":
  170. logger.debug("Error reading multimedia keys", exc_info = e)
  171. pass
  172. finally:
  173. self.__usbDeviceMutex.release()
  174. return val
  175. def reset(self):
  176. '''Initiates a bus reset to USB device.'''
  177. self.__usbDeviceMutex.acquire()
  178. try:
  179. self.__usbDevice.reset()
  180. finally:
  181. self.__usbDeviceMutex.release()
  182. def save_default_bg_color(self, r, g, b):
  183. '''This stores given color permanently to keyboard.
  184. After a reset this will be color used by default.
  185. '''
  186. rtype = usb.TYPE_CLASS | usb.RECIP_INTERFACE
  187. colorData = [7, r, g, b]
  188. self.__usbDeviceMutex.acquire()
  189. try:
  190. self.__usbDevice.handleIf1.controlMsg(
  191. rtype, 0x09, colorData, 0x308, 0x01, self.__write_timeout)
  192. finally:
  193. self.__usbDeviceMutex.release()
  194. def set_display_colorful(self):
  195. '''This is an example how to create an image having a green to red
  196. transition from left to right and a black to blue from top to bottom.
  197. '''
  198. data = []
  199. for i in range(320 * 240 * 2):
  200. data.append(0)
  201. for x in range(320):
  202. for y in range(240):
  203. data[2*(x*240+y)] = self.rgb_to_uint16(
  204. 255 * x / 320, 255 * (320 - x) / 320, 255 * y / 240) >> 8
  205. data[2*(x*240+y)+1] = self.rgb_to_uint16(
  206. 255 * x / 320, 255 * (320 - x) / 320, 255 * y / 240) & 0xff
  207. self.send_frame(data)
  208. def send_frame(self, data):
  209. '''Sends a frame to display.
  210. @param data 320x240x2 bytes, containing the frame in little-endian
  211. 16bit highcolor (5-6-5) format.
  212. Image must be row-wise, starting at upper left corner and ending at
  213. lower right. This means (data[0], data[1]) is the first pixel and
  214. (data[239 * 2], data[239 * 2 + 1]) the lower left one.
  215. '''
  216. if len(data) != (320 * 240 * 2):
  217. raise ValueError("illegal frame size: " + str(len(data))
  218. + " should be 320x240x2=" + str(320 * 240 * 2))
  219. frame = list(self.__frame_content)
  220. frame += data
  221. self.__usbDeviceMutex.acquire()
  222. try:
  223. self.__usbDevice.handleIf0.bulkWrite(2, frame, self.__write_timeout)
  224. finally:
  225. self.__usbDeviceMutex.release()
  226. def set_bg_color(self, r, g, b):
  227. '''Sets backlight to given color.'''
  228. rtype = usb.TYPE_CLASS | usb.RECIP_INTERFACE
  229. colorData = [7, r, g, b]
  230. self.__usbDeviceMutex.acquire()
  231. try:
  232. self.__usbDevice.handleIf1.controlMsg(
  233. rtype, 0x09, colorData, 0x307, 0x01, 10000)
  234. finally:
  235. self.__usbDeviceMutex.release()
  236. def set_enabled_m_keys(self, keys):
  237. '''Sets currently lit keys as an OR-combination of LIGHT_KEY_M1..3,R.
  238. example:
  239. from logitech.g19_keys import Data
  240. lg19 = G19()
  241. lg19.set_enabled_m_keys(Data.LIGHT_KEY_M1 | Data.LIGHT_KEY_MR)
  242. '''
  243. rtype = usb.TYPE_CLASS | usb.RECIP_INTERFACE
  244. self.__usbDeviceMutex.acquire()
  245. try:
  246. self.__usbDevice.handleIf1.controlMsg(
  247. rtype, 0x09, [5, keys], 0x305, 0x01, self.__write_timeout)
  248. finally:
  249. self.__usbDeviceMutex.release()
  250. def set_display_brightness(self, val):
  251. '''Sets display brightness.
  252. @param val in [0,100] (off..maximum).
  253. '''
  254. data = [val, 0xe2, 0x12, 0x00, 0x8c, 0x11, 0x00, 0x10, 0x00]
  255. rtype = usb.TYPE_VENDOR | usb.RECIP_INTERFACE
  256. self.__usbDeviceMutex.acquire()
  257. try:
  258. self.__usbDevice.handleIf1.controlMsg(rtype, 0x0a, data, 0x0, 0x0, self.__write_timeout)
  259. finally:
  260. self.__usbDeviceMutex.release()
  261. def start_event_handling(self):
  262. '''Start event processing (aka keyboard driver).
  263. This method is NOT thread-safe.
  264. '''
  265. self.stop_event_handling()
  266. self.__threadDisplay = threading.Thread(
  267. target=self.__keyReceiver.run)
  268. self.__keyReceiver.start()
  269. self.__threadDisplay.name = "EventThread"
  270. self.__threadDisplay.setDaemon(True)
  271. self.__threadDisplay.start()
  272. def stop_event_handling(self):
  273. '''Stops event processing (aka keyboard driver).
  274. This method is NOT thread-safe.
  275. '''
  276. self.__keyReceiver.stop()
  277. if self.__threadDisplay:
  278. self.__threadDisplay.join()
  279. self.__threadDisplay = None
  280. def close(self):
  281. logger.info("Closing G19")
  282. self.stop_event_handling()
  283. self.__usbDevice.close()
  284. class G19UsbController(object):
  285. '''Controller for accessing the G19 USB device.
  286. The G19 consists of two composite USB devices:
  287. * 046d:c228
  288. The keyboard consisting of two interfaces:
  289. MI00: keyboard
  290. EP 0x81(in) - INT the keyboard itself
  291. MI01: (ifacMM)
  292. EP 0x82(in) - multimedia keys, incl. scroll and Winkey-switch
  293. * 046d:c229
  294. LCD display with two interfaces:
  295. MI00 (0x05): (iface0) via control data in: display keys
  296. EP 0x81(in) - INT
  297. EP 0x02(out) - BULK display itself
  298. MI01 (0x06): (iface1) backlight
  299. EP 0x83(in) - INT G-keys, M1..3/MR key, light key
  300. '''
  301. def __init__(self, resetOnStart=False, enable_mm_keys=False, resetWait = 0):
  302. self.enable_mm_keys = enable_mm_keys
  303. logger.info("Looking for LCD device")
  304. self.__lcd_device = self._find_device(0x046d, 0xc229)
  305. if not self.__lcd_device:
  306. raise usb.USBError("G19 LCD not found on USB bus")
  307. # Reset
  308. self.handleIf0 = self.__lcd_device.open()
  309. if resetOnStart:
  310. logger.info("Resetting LCD device")
  311. self.handleIf0.reset()
  312. time.sleep(float(resetWait) / 1000.0)
  313. logger.info("Re-opening LCD device")
  314. self.handleIf0 = self.__lcd_device.open()
  315. logger.info("Re-opened LCD device")
  316. self.handleIf1 = self.__lcd_device.open()
  317. config = self.__lcd_device.configurations[0]
  318. display_interface = config.interfaces[0][0]
  319. # This is to cope with a difference in pyusb 1.0 compatibility layer
  320. if len(config.interfaces) > 1:
  321. macro_and_backlight_interface = config.interfaces[1][0]
  322. else:
  323. macro_and_backlight_interface = config.interfaces[0][1]
  324. try:
  325. logger.debug("Detaching kernel driver for LCD device")
  326. # Use .interfaceNumber for pyusb 1.0 compatibility layer
  327. self.handleIf0.detachKernelDriver(display_interface.interfaceNumber)
  328. logger.debug("Detached kernel driver for LCD device")
  329. except usb.USBError as e:
  330. logger.debug("Detaching kernel driver for LCD device failed.", exc_info = e)
  331. try:
  332. logger.debug("Detaching kernel driver for macro / backlight device")
  333. # Use .interfaceNumber for pyusb 1.0 compatibility layer
  334. self.handleIf1.detachKernelDriver(macro_and_backlight_interface.interfaceNumber)
  335. logger.debug("Detached kernel driver for macro / backlight device")
  336. except usb.USBError as e:
  337. logger.debug("Detaching kernel driver for macro / backlight device failed.", exc_info = e)
  338. logger.debug("Setting configuration")
  339. #self.handleIf0.setConfiguration(1)
  340. #self.handleIf1.setConfiguration(1)
  341. logger.debug("Claiming LCD interface")
  342. self.handleIf0.claimInterface(display_interface.interfaceNumber)
  343. logger.info("Claimed LCD interface")
  344. logger.debug("Claiming macro interface")
  345. self.handleIf1.claimInterface(macro_and_backlight_interface.interfaceNumber)
  346. logger.info("Claimed macro interface")
  347. if self.enable_mm_keys:
  348. logger.debug("Looking for multimedia keys device")
  349. self.__kbd_device = self._find_device(0x046d, 0xc228)
  350. if not self.__kbd_device:
  351. raise usb.USBError("G19 keyboard not found on USB bus")
  352. self.handleIfMM = self.__kbd_device.open()
  353. if resetOnStart:
  354. logger.debug("Resetting multimedia keys device")
  355. self.handleIfMM.reset()
  356. logger.debug("Re-opening multimedia keys device")
  357. self.handleIfMM = self.__kbd_device.open()
  358. logger.debug("Re-opened multimedia keys device")
  359. config = self.__kbd_device.configurations[0]
  360. ifacMM = config.interfaces[1][0]
  361. try:
  362. self.handleIfMM.setConfiguration(1)
  363. except usb.USBError as e:
  364. logger.debug("Error when trying to set configuration", exc_info = e)
  365. pass
  366. try:
  367. logger.debug("Detaching kernel driver for multimedia keys device")
  368. self.handleIfMM.detachKernelDriver(ifacMM)
  369. logger.debug("Detached kernel driver for multimedia keys device")
  370. except usb.USBError as e:
  371. logger.debug("Detaching kernel driver for multimedia keys device failed.", exc_info = e)
  372. logger.debug("Claiming multimedia interface")
  373. self.handleIfMM.claimInterface(1)
  374. logger.info("Claimed multimedia keys interface")
  375. def close(self):
  376. if self.enable_mm_keys:
  377. self.handleIfMM.releaseInterface()
  378. self.handleIf1.releaseInterface()
  379. self.handleIf0.releaseInterface()
  380. @staticmethod
  381. def _find_device(idVendor, idProduct):
  382. for bus in usb.busses():
  383. for dev in bus.devices:
  384. if dev.idVendor == idVendor and \
  385. dev.idProduct == idProduct:
  386. return dev
  387. return None
  388. def reset(self):
  389. '''Resets the device on the USB.'''
  390. self.handleIf0.reset()
  391. self.handleIf1.reset()
  392. def main():
  393. lg19 = G19()
  394. lg19.start_event_handling()
  395. time.sleep(20)
  396. lg19.stop_event_handling()
  397. if __name__ == '__main__':
  398. main()