Solaar/lib/logitech_receiver/hidpp10.py

251 lines
9.2 KiB
Python

## 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.
import logging
from .common import Battery as _Battery
from .common import FirmwareInfo as _FirmwareInfo
from .common import bytes2int as _bytes2int
from .common import int2bytes as _int2bytes
from .common import strhex as _strhex
from .hidpp10_constants import REGISTERS
from .hidpp20_constants import FIRMWARE_KIND
logger = logging.getLogger(__name__)
#
# functions
#
def read_register(device, register_number, *params):
assert device is not None, f"tried to read register {register_number:02X} from invalid device {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 is not None, f"tried to write register {register_number:02X} to invalid device {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)
class Hidpp10:
def get_battery(self, device):
assert device is not None
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 get_firmware(self, device):
assert device is not None
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 = f"{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 = f"{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 = f"{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(self, device, battery_level=None, charging=None, warning=None):
assert device is not None
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.APPROX.critical:
# 1 orange, and force blink
v1, v2 = 0x22, 0x00
warning = True
elif battery_level < _Battery.APPROX.low:
# 1 orange
v1, v2 = 0x22, 0x00
elif battery_level < _Battery.APPROX.good:
# 1 green
v1, v2 = 0x20, 0x00
elif battery_level < _Battery.APPROX.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(self, device):
return self._get_register(device, REGISTERS.notifications)
def set_notification_flags(self, device, *flag_bits):
assert device is not None
# 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
def get_device_features(self, device):
self._get_register(device, REGISTERS.mouse_button_flags)
def _get_register(self, device, register):
assert device is not None
# 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, register)
if flags is not None:
assert len(flags) == 3
return _bytes2int(flags)
def get_configuration_pending_flags(self, receiver):
assert not receiver.isDevice
result = read_register(receiver, REGISTERS.devices_configuration)
if result is not None:
return ord(result[:1])
def set_configuration_pending_flags(self, receiver, devices):
assert not receiver.isDevice
result = write_register(receiver, REGISTERS.devices_configuration, devices)
return result is not None
def parse_battery_status(register, reply):
if register == REGISTERS.battery_charge:
charge = ord(reply[:1])
status_byte = ord(reply[2:3]) & 0xF0
status_text = (
_Battery.STATUS.discharging
if status_byte == 0x30
else _Battery.STATUS.recharging
if status_byte == 0x50
else _Battery.STATUS.full
if status_byte == 0x90
else None
)
return _Battery(charge, None, status_text, None)
if register == REGISTERS.battery_status:
status_byte = ord(reply[:1])
charge = (
_Battery.APPROX.full
if status_byte == 7 # full
else _Battery.APPROX.good
if status_byte == 5 # good
else _Battery.APPROX.low
if status_byte == 3 # low
else _Battery.APPROX.critical
if status_byte == 1 # critical
# pure 'charging' notifications may come without a status
else _Battery.APPROX.empty
)
charging_byte = ord(reply[1:2])
if charging_byte == 0x00:
status_text = _Battery.STATUS.discharging
elif charging_byte & 0x21 == 0x21:
status_text = _Battery.STATUS.recharging
elif charging_byte & 0x22 == 0x22:
status_text = _Battery.STATUS.full
else:
logger.warning("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 None for next charge level and voltage as these are not in HID++ 1.0 spec
return _Battery(charge, None, status_text, None)