# -*- python-mode -*- # -*- coding: UTF-8 -*- ## Copyright (C) 2012-2013 Daniel Pavel ## ## 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 2 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, write to the Free Software Foundation, Inc., ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. from __future__ import absolute_import, division, print_function, unicode_literals from logging import getLogger # , DEBUG as _DEBUG _log = getLogger(__name__) del getLogger from .common import (strhex as _strhex, bytes2int as _bytes2int, int2bytes as _int2bytes, NamedInts as _NamedInts, FirmwareInfo as _FirmwareInfo) from .hidpp20 import FIRMWARE_KIND # # Constants - most of them as defined by the official Logitech HID++ 1.0 # documentation, some of them guessed. # DEVICE_KIND = _NamedInts( keyboard=0x01, mouse=0x02, numpad=0x03, presenter=0x04, trackball=0x08, touchpad=0x09) POWER_SWITCH_LOCATION = _NamedInts( base=0x01, top_case=0x02, edge_of_top_right_corner=0x03, top_left_corner=0x05, bottom_left_corner=0x06, top_right_corner=0x07, bottom_right_corner=0x08, top_edge=0x09, right_edge=0x0A, left_edge=0x0B, bottom_edge=0x0C) # Some flags are used both by devices and receivers. The Logitech documentation # mentions that the first and last (third) byte are used for devices while the # second is used for the receiver. In practise, the second byte is also used for # some device-specific notifications (keyboard illumination level). Do not # simply set all notification bits if the software does not support it. For # example, enabling keyboard_sleep_raw makes the Sleep key a no-operation unless # the software is updated to handle that event. # Observations: # - wireless and software present were seen on receivers, reserved_r1b4 as well # - the rest work only on devices as far as we can tell right now # In the future would be useful to have separate enums for receiver and device notification flags, # but right now we don't know enough. NOTIFICATION_FLAG = _NamedInts( battery_status= 0x100000, # send battery charge notifications (0x07 or 0x0D) keyboard_sleep_raw= 0x020000, # system control keys such as Sleep keyboard_multimedia_raw=0x010000, # consumer controls such as Mute and Calculator # reserved_r1b4= 0x001000, # unknown, seen on a unifying receiver software_present= 0x000800, # .. no idea keyboard_illumination= 0x000200, # illumination brightness level changes (by pressing keys) wireless= 0x000100, # notify when the device wireless goes on/off-line ) ERROR = _NamedInts( invalid_SubID__command=0x01, invalid_address=0x02, invalid_value=0x03, connection_request_failed=0x04, too_many_devices=0x05, already_exists=0x06, busy=0x07, unknown_device=0x08, resource_error=0x09, request_unavailable=0x0A, unsupported_parameter_value=0x0B, wrong_pin_code=0x0C) PAIRING_ERRORS = _NamedInts( device_timeout=0x01, device_not_supported=0x02, too_many_devices=0x03, sequence_timeout=0x06) BATTERY_APPOX = _NamedInts( empty = 0, critical = 5, low = 20, good = 50, full = 90) """Known registers. Devices usually have a (small) sub-set of these. Some registers are only applicable to certain device kinds (e.g. smooth_scroll only applies to mice.""" REGISTERS = _NamedInts( # only apply to receivers receiver_connection=0x02, receiver_pairing=0xB2, devices_activity=0x2B3, receiver_info=0x2B5, # only apply to devices mouse_smooth_scroll=0x01, keyboard_hand_detection=0x01, battery_status=0x07, keyboard_fn_swap=0x09, battery_charge=0x0D, keyboard_illumination=0x17, three_leds=0x51, mouse_dpi=0x63, # apply to both notifications=0x00, firmware=0xF1, ) # # functions # def read_register(device, register_number, *params): assert device # support long registers by adding a 2 in front of the register number request_id = 0x8100 | (int(register_number) & 0x2FF) return device.request(request_id, *params) def write_register(device, register_number, *value): assert device # support long registers by adding a 2 in front of the register number request_id = 0x8000 | (int(register_number) & 0x2FF) return device.request(request_id, *value) def get_battery(device): assert device assert device.kind is not None if not device.online: return """Reads a device's battery level, if provided by the HID++ 1.0 protocol.""" if device.protocol and device.protocol >= 2.0: # let's just assume HID++ 2.0 devices do not provide the battery info in a register return for r in (REGISTERS.battery_status, REGISTERS.battery_charge): if r in device.registers: reply = read_register(device, r) if reply: return parse_battery_status(r, reply) return # the descriptor does not tell us which register this device has, try them both reply = read_register(device, REGISTERS.battery_charge) if reply: # remember this for the next time device.registers.append(REGISTERS.battery_charge) return parse_battery_status(REGISTERS.battery_charge, reply) reply = read_register(device, REGISTERS.battery_status) if reply: # remember this for the next time device.registers.append(REGISTERS.battery_status) return parse_battery_status(REGISTERS.battery_status, reply) def parse_battery_status(register, reply): if register == REGISTERS.battery_charge: charge = ord(reply[:1]) status_byte = ord(reply[2:3]) & 0xF0 status_text = ('discharging' if status_byte == 0x30 else 'charging' if status_byte == 0x50 else 'fully charged' if status_byte == 0x90 else None) return charge, status_text if register == REGISTERS.battery_status: status_byte = ord(reply[:1]) charge = (BATTERY_APPOX.full if status_byte == 7 # full else BATTERY_APPOX.good if status_byte == 5 # good else BATTERY_APPOX.low if status_byte == 3 # low else BATTERY_APPOX.critical if status_byte == 1 # critical # pure 'charging' notifications may come without a status else BATTERY_APPOX.empty) charging_byte = ord(reply[1:2]) if charging_byte == 0x00: status_text = 'discharging' elif charging_byte & 0x21 == 0x21: status_text = 'charging' elif charging_byte & 0x22 == 0x22: status_text = 'fully charged' else: _log.warn("could not parse 0x07 battery status: %02X (level %02X)", charging_byte, status_byte) status_text = None if charging_byte & 0x03 and status_byte == 0: # some 'charging' notifications may come with no battery level information charge = None return charge, status_text def get_firmware(device): assert device firmware = [None, None, None] reply = read_register(device, REGISTERS.firmware, 0x01) if not reply: # won't be able to read any of it now... return fw_version = _strhex(reply[1:3]) fw_version = '%s.%s' % (fw_version[0:2], fw_version[2:4]) reply = read_register(device, REGISTERS.firmware, 0x02) if reply: fw_version += '.B' + _strhex(reply[1:3]) fw = _FirmwareInfo(FIRMWARE_KIND.Firmware, '', fw_version, None) firmware[0] = fw reply = read_register(device, REGISTERS.firmware, 0x04) if reply: bl_version = _strhex(reply[1:3]) bl_version = '%s.%s' % (bl_version[0:2], bl_version[2:4]) bl = _FirmwareInfo(FIRMWARE_KIND.Bootloader, '', bl_version, None) firmware[1] = bl reply = read_register(device, REGISTERS.firmware, 0x03) if reply: o_version = _strhex(reply[1:3]) o_version = '%s.%s' % (o_version[0:2], o_version[2:4]) o = _FirmwareInfo(FIRMWARE_KIND.Other, '', o_version, None) firmware[2] = o if any(firmware): return tuple(f for f in firmware if f) def set_3leds(device, battery_level=None, charging=None, warning=None): assert device assert device.kind is not None if not device.online: return if REGISTERS.three_leds not in device.registers: return if battery_level is not None: if battery_level < BATTERY_APPOX.critical: # 1 orange, and force blink v1, v2 = 0x22, 0x00 warning = True elif battery_level < BATTERY_APPOX.low: # 1 orange v1, v2 = 0x22, 0x00 elif battery_level < BATTERY_APPOX.good: # 1 green v1, v2 = 0x20, 0x00 elif battery_level < BATTERY_APPOX.full: # 2 greens v1, v2 = 0x20, 0x02 else: # all 3 green v1, v2 = 0x20, 0x22 if warning: # set the blinking flag for the leds already set v1 |= (v1 >> 1) v2 |= (v2 >> 1) elif charging: # blink all green v1, v2 = 0x30,0x33 elif warning: # 1 red v1, v2 = 0x02, 0x00 else: # turn off all leds v1, v2 = 0x11, 0x11 write_register(device, REGISTERS.three_leds, v1, v2) def get_notification_flags(device): assert device # Avoid a call if the device is not online, # or the device does not support registers. if device.kind is not None: # peripherals with protocol >= 2.0 don't support registers if device.protocol and device.protocol >= 2.0: return flags = read_register(device, REGISTERS.notifications) if flags is not None: assert len(flags) == 3 return _bytes2int(flags) def set_notification_flags(device, *flag_bits): assert device # Avoid a call if the device is not online, # or the device does not support registers. if device.kind is not None: # peripherals with protocol >= 2.0 don't support registers if device.protocol and device.protocol >= 2.0: return flag_bits = sum(int(b) for b in flag_bits) assert flag_bits & 0x00FFFFFF == flag_bits result = write_register(device, REGISTERS.notifications, _int2bytes(flag_bits, 3)) return result is not None