330 lines
9.9 KiB
Python
330 lines
9.9 KiB
Python
# -*- 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
|