Skip to content

Commit

Permalink
Introduce DeviceProtocol
Browse files Browse the repository at this point in the history
Type device interface for hidpp10 and hidpp20
  • Loading branch information
MattHag committed Apr 13, 2024
1 parent 5730faa commit ccebd3b
Show file tree
Hide file tree
Showing 2 changed files with 133 additions and 74 deletions.
135 changes: 84 additions & 51 deletions lib/logitech_receiver/hidpp10.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,15 @@
## 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 annotations

import logging

from .common import Battery as _Battery
from typing_extensions import Protocol

from .common import Battery
from .common import BatteryChargeApproximation
from .common import BatteryStatus
from .common import FirmwareInfo as _FirmwareInfo
from .common import bytes2int as _bytes2int
from .common import int2bytes as _int2bytes
Expand All @@ -26,19 +31,36 @@

logger = logging.getLogger(__name__)

#
# functions
#

class DeviceProtocol(Protocol):
def request(self):
...

@property
def kind(self):
...

@property
def online(self):
...

@property
def protocol(self):
...

def read_register(device, register_number, *params):
@property
def registers(self):
...


def read_register(device: DeviceProtocol, 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):
def write_register(device: DeviceProtocol, 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)
Expand All @@ -59,7 +81,7 @@ def set_configuration_pending_flags(receiver, devices):


class Hidpp10:
def get_battery(self, device):
def get_battery(self, device: DeviceProtocol):
assert device is not None
assert device.kind is not None
if not device.online:
Expand Down Expand Up @@ -89,7 +111,7 @@ def get_battery(self, device):
device.registers.append(REGISTERS.battery_status)
return parse_battery_status(REGISTERS.battery_status, reply)

def get_firmware(self, device):
def get_firmware(self, device: DeviceProtocol):
assert device is not None

firmware = [None, None, None]
Expand Down Expand Up @@ -124,7 +146,7 @@ def get_firmware(self, device):
if any(firmware):
return tuple(f for f in firmware if f)

def set_3leds(self, device, battery_level=None, charging=None, warning=None):
def set_3leds(self, device: DeviceProtocol, battery_level=None, charging=None, warning=None):
assert device is not None
assert device.kind is not None
if not device.online:
Expand All @@ -134,17 +156,17 @@ def set_3leds(self, device, battery_level=None, charging=None, warning=None):
return

if battery_level is not None:
if battery_level < _Battery.APPROX.critical:
if battery_level < BatteryChargeApproximation.CRITICAL:
# 1 orange, and force blink
v1, v2 = 0x22, 0x00
warning = True
elif battery_level < _Battery.APPROX.low:
elif battery_level < BatteryChargeApproximation.LOW:
# 1 orange
v1, v2 = 0x22, 0x00
elif battery_level < _Battery.APPROX.good:
elif battery_level < BatteryChargeApproximation.GOOD:
# 1 green
v1, v2 = 0x20, 0x00
elif battery_level < _Battery.APPROX.full:
elif battery_level < BatteryChargeApproximation.FULL:
# 2 greens
v1, v2 = 0x20, 0x02
else:
Expand All @@ -166,10 +188,10 @@ def set_3leds(self, device, battery_level=None, charging=None, warning=None):

write_register(device, REGISTERS.three_leds, v1, v2)

def get_notification_flags(self, device):
def get_notification_flags(self, device: DeviceProtocol):
return self._get_register(device, REGISTERS.notifications)

def set_notification_flags(self, device, *flag_bits):
def set_notification_flags(self, device: DeviceProtocol, *flag_bits):
assert device is not None

# Avoid a call if the device is not online,
Expand All @@ -184,10 +206,10 @@ def set_notification_flags(self, device, *flag_bits):
result = write_register(device, REGISTERS.notifications, _int2bytes(flag_bits, 3))
return result is not None

def get_device_features(self, device):
def get_device_features(self, device: DeviceProtocol):
return self._get_register(device, REGISTERS.mouse_button_flags)

def _get_register(self, device, register):
def _get_register(self, device: DeviceProtocol, register):
assert device is not None

# Avoid a call if the device is not online,
Expand All @@ -203,50 +225,61 @@ def _get_register(self, device, register):
return _bytes2int(flags)


def parse_battery_status(register, reply):
def parse_battery_status(register, reply) -> Battery | None:
def status_byte_to_charge(status_byte_: int) -> BatteryChargeApproximation:
if status_byte_ == 7:
charge_ = BatteryChargeApproximation.FULL
elif status_byte_ == 5:
charge_ = BatteryChargeApproximation.GOOD
elif status_byte_ == 3:
charge_ = BatteryChargeApproximation.LOW
elif status_byte_ == 1:
charge_ = BatteryChargeApproximation.CRITICAL
else:
# pure 'charging' notifications may come without a status
charge_ = BatteryChargeApproximation.EMPTY
return charge_

def status_byte_to_battery_status(status_byte_: int) -> BatteryStatus:
if status_byte_ == 0x30:
status_text_ = BatteryStatus.DISCHARGING
elif status_byte_ == 0x50:
status_text_ = BatteryStatus.RECHARGING
elif status_byte_ == 0x90:
status_text_ = BatteryStatus.FULL
else:
status_text_ = None
return status_text_

def charging_byte_to_status_text(charging_byte_: int) -> BatteryStatus:
if charging_byte_ == 0x00:
status_text_ = BatteryStatus.DISCHARGING
elif charging_byte_ & 0x21 == 0x21:
status_text_ = BatteryStatus.RECHARGING
elif charging_byte_ & 0x22 == 0x22:
status_text_ = BatteryStatus.FULL
else:
logger.warning("could not parse 0x07 battery status: %02X (level %02X)", charging_byte_, status_byte)
status_text_ = None
return status_text_

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)

battery_status = status_byte_to_battery_status(status_byte)
return Battery(charge, None, battery_status, 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

status_text = charging_byte_to_status_text(charging_byte)
charge = status_byte_to_charge(status_byte)

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)
return Battery(charge, None, status_text, None)
Loading

0 comments on commit ccebd3b

Please sign in to comment.