From 8d04fabd889ce9de35466ca2ca9c845ab5454742 Mon Sep 17 00:00:00 2001 From: Rob Berwick Date: Sun, 1 Dec 2024 08:17:02 +0000 Subject: [PATCH 01/10] refactor: move BlinkStickVariant to enums module --- src/blinkstick/__init__.py | 2 +- src/blinkstick/clients/blinkstick.py | 2 +- src/blinkstick/constants.py | 41 ---------------------------- src/blinkstick/enums.py | 40 +++++++++++++++++++++++++++ src/scripts/main.py | 8 ++++-- 5 files changed, 48 insertions(+), 45 deletions(-) create mode 100644 src/blinkstick/enums.py diff --git a/src/blinkstick/__init__.py b/src/blinkstick/__init__.py index 4b6c522..7053da9 100644 --- a/src/blinkstick/__init__.py +++ b/src/blinkstick/__init__.py @@ -3,7 +3,7 @@ from blinkstick.clients import BlinkStick, BlinkStickPro, BlinkStickProMatrix from .core import find_all, find_first, find_by_serial, get_blinkstick_package_version from .colors import Color, ColorFormat -from .constants import BlinkStickVariant +from .enums import BlinkStickVariant from .exceptions import BlinkStickException try: diff --git a/src/blinkstick/clients/blinkstick.py b/src/blinkstick/clients/blinkstick.py index 3efd004..697540a 100644 --- a/src/blinkstick/clients/blinkstick.py +++ b/src/blinkstick/clients/blinkstick.py @@ -12,7 +12,7 @@ remap_rgb_value_reverse, ColorFormat, ) -from blinkstick.constants import BlinkStickVariant +from blinkstick.enums import BlinkStickVariant from blinkstick.devices import BlinkStickDevice from blinkstick.utilities import string_to_info_block_data diff --git a/src/blinkstick/constants.py b/src/blinkstick/constants.py index 14348d3..4c6e431 100644 --- a/src/blinkstick/constants.py +++ b/src/blinkstick/constants.py @@ -1,43 +1,2 @@ -from __future__ import annotations - -from enum import Enum - VENDOR_ID = 0x20A0 PRODUCT_ID = 0x41E5 - - -class BlinkStickVariant(Enum): - UNKNOWN = (0, "Unknown") - BLINKSTICK = (1, "BlinkStick") - BLINKSTICK_PRO = (2, "BlinkStick Pro") - BLINKSTICK_STRIP = (3, "BlinkStick Strip") - BLINKSTICK_SQUARE = (4, "BlinkStick Square") - BLINKSTICK_NANO = (5, "BlinkStick Nano") - BLINKSTICK_FLEX = (6, "BlinkStick Flex") - - @property - def value(self) -> int: - return self._value_[0] - - @property - def description(self) -> str: - return self._value_[1] - - @staticmethod - def identify( - major_version: int, version_attribute: int | None - ) -> "BlinkStickVariant": - if major_version == 1: - return BlinkStickVariant.BLINKSTICK - elif major_version == 2: - return BlinkStickVariant.BLINKSTICK_PRO - elif major_version == 3: - if version_attribute == 0x200: - return BlinkStickVariant.BLINKSTICK_SQUARE - elif version_attribute == 0x201: - return BlinkStickVariant.BLINKSTICK_STRIP - elif version_attribute == 0x202: - return BlinkStickVariant.BLINKSTICK_NANO - elif version_attribute == 0x203: - return BlinkStickVariant.BLINKSTICK_FLEX - return BlinkStickVariant.UNKNOWN diff --git a/src/blinkstick/enums.py b/src/blinkstick/enums.py new file mode 100644 index 0000000..f56d719 --- /dev/null +++ b/src/blinkstick/enums.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +from enum import Enum + + +class BlinkStickVariant(Enum): + UNKNOWN = (0, "Unknown") + BLINKSTICK = (1, "BlinkStick") + BLINKSTICK_PRO = (2, "BlinkStick Pro") + BLINKSTICK_STRIP = (3, "BlinkStick Strip") + BLINKSTICK_SQUARE = (4, "BlinkStick Square") + BLINKSTICK_NANO = (5, "BlinkStick Nano") + BLINKSTICK_FLEX = (6, "BlinkStick Flex") + + @property + def value(self) -> int: + return self._value_[0] + + @property + def description(self) -> str: + return self._value_[1] + + @staticmethod + def identify( + major_version: int, version_attribute: int | None + ) -> "BlinkStickVariant": + if major_version == 1: + return BlinkStickVariant.BLINKSTICK + elif major_version == 2: + return BlinkStickVariant.BLINKSTICK_PRO + elif major_version == 3: + if version_attribute == 0x200: + return BlinkStickVariant.BLINKSTICK_SQUARE + elif version_attribute == 0x201: + return BlinkStickVariant.BLINKSTICK_STRIP + elif version_attribute == 0x202: + return BlinkStickVariant.BLINKSTICK_NANO + elif version_attribute == 0x203: + return BlinkStickVariant.BLINKSTICK_FLEX + return BlinkStickVariant.UNKNOWN diff --git a/src/scripts/main.py b/src/scripts/main.py index ed0d6bb..2240b57 100644 --- a/src/scripts/main.py +++ b/src/scripts/main.py @@ -5,8 +5,12 @@ import sys import logging -from blinkstick import find_all, find_by_serial, get_blinkstick_package_version -from blinkstick.constants import BlinkStickVariant +from blinkstick import ( + find_all, + find_by_serial, + get_blinkstick_package_version, + BlinkStickVariant, +) logging.basicConfig() From 3fa16bb0a99cabb59163b4902528703c1707ac59 Mon Sep 17 00:00:00 2001 From: Rob Berwick Date: Sun, 1 Dec 2024 08:50:29 +0000 Subject: [PATCH 02/10] refactor: get variant from backend --- src/blinkstick/backends/base.py | 3 +++ src/blinkstick/clients/blinkstick.py | 7 +------ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/blinkstick/backends/base.py b/src/blinkstick/backends/base.py index c907a7b..ac50d20 100644 --- a/src/blinkstick/backends/base.py +++ b/src/blinkstick/backends/base.py @@ -55,3 +55,6 @@ def get_version_attribute(self) -> int: def get_description(self): return self.blinkstick_device.description + + def get_variant(self): + return self.blinkstick_device.variant diff --git a/src/blinkstick/clients/blinkstick.py b/src/blinkstick/clients/blinkstick.py index 697540a..14fedf5 100644 --- a/src/blinkstick/clients/blinkstick.py +++ b/src/blinkstick/clients/blinkstick.py @@ -97,12 +97,7 @@ def get_variant(self) -> BlinkStickVariant: @return: BlinkStickVariant.UNKNOWN, BlinkStickVariant.BLINKSTICK, BlinkStickVariant.BLINKSTICK_PRO and etc """ - serial = self.get_serial() - major = serial[-3] - - version_attribute = self.backend.get_version_attribute() - - return BlinkStickVariant.identify(int(major), version_attribute) + return self.backend.get_variant() def get_variant_string(self) -> str: """ From 679441eda16d4f2d9d4e980f810ace5f002995ae Mon Sep 17 00:00:00 2001 From: Rob Berwick Date: Sun, 1 Dec 2024 09:09:05 +0000 Subject: [PATCH 03/10] test: fix up blinkstick variant tests --- tests/clients/test_blinkstick.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/clients/test_blinkstick.py b/tests/clients/test_blinkstick.py index c67477e..166e9fb 100644 --- a/tests/clients/test_blinkstick.py +++ b/tests/clients/test_blinkstick.py @@ -2,9 +2,9 @@ import pytest -from blinkstick import ColorFormat +from blinkstick.colors import ColorFormat +from blinkstick.enums import BlinkStickVariant from blinkstick.clients.blinkstick import BlinkStick -from blinkstick.constants import BlinkStickVariant from pytest_mock import MockFixture from tests.conftest import make_blinkstick @@ -54,8 +54,8 @@ def test_get_variant( make_blinkstick, serial, version_attribute, expected_variant, expected_variant_value ): bs = make_blinkstick() - bs.get_serial = MagicMock(return_value=serial) - bs.backend.get_version_attribute = MagicMock(return_value=version_attribute) + synthesised_variant = BlinkStickVariant.identify(int(serial[-3]), version_attribute) + bs.backend.get_variant = MagicMock(return_value=synthesised_variant) assert bs.get_variant() == expected_variant assert bs.get_variant().value == expected_variant_value From a75e3bbc15f212b9869a5d9dfcebb809a4eb9496 Mon Sep 17 00:00:00 2001 From: Rob Berwick Date: Sun, 1 Dec 2024 09:21:52 +0000 Subject: [PATCH 04/10] refactor: rename identify class method Rename BlinkStickVariant.identify() to .from_version_attrs(), for added clarity. --- src/blinkstick/enums.py | 2 +- tests/clients/test_blinkstick.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/blinkstick/enums.py b/src/blinkstick/enums.py index f56d719..fcc3e2e 100644 --- a/src/blinkstick/enums.py +++ b/src/blinkstick/enums.py @@ -21,7 +21,7 @@ def description(self) -> str: return self._value_[1] @staticmethod - def identify( + def from_version_attrs( major_version: int, version_attribute: int | None ) -> "BlinkStickVariant": if major_version == 1: diff --git a/tests/clients/test_blinkstick.py b/tests/clients/test_blinkstick.py index 166e9fb..eb7d007 100644 --- a/tests/clients/test_blinkstick.py +++ b/tests/clients/test_blinkstick.py @@ -54,7 +54,9 @@ def test_get_variant( make_blinkstick, serial, version_attribute, expected_variant, expected_variant_value ): bs = make_blinkstick() - synthesised_variant = BlinkStickVariant.identify(int(serial[-3]), version_attribute) + synthesised_variant = BlinkStickVariant.from_version_attrs( + int(serial[-3]), version_attribute + ) bs.backend.get_variant = MagicMock(return_value=synthesised_variant) assert bs.get_variant() == expected_variant assert bs.get_variant().value == expected_variant_value From dd51c29d87ad09a85215574833c07f1a56b7c84f Mon Sep 17 00:00:00 2001 From: Rob Berwick Date: Sun, 1 Dec 2024 08:31:51 +0000 Subject: [PATCH 05/10] chore: add serialnumber model Add a serial number model so that it's easier to decompose th estring into constituent parts --- src/blinkstick/models.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/blinkstick/models.py diff --git a/src/blinkstick/models.py b/src/blinkstick/models.py new file mode 100644 index 0000000..c858eb3 --- /dev/null +++ b/src/blinkstick/models.py @@ -0,0 +1,32 @@ +import re +from dataclasses import dataclass, field + + +@dataclass(frozen=True) +class SerialDetails: + """ + A BlinkStick serial number representation. + + BSnnnnnn-1.0 + || | | |- Software minor version + || | |--- Software major version + || |-------- Denotes sequential number + ||----------- Denotes BlinkStick backend + + + """ + + serial: str + major_version: int = field(init=False) + minor_version: int = field(init=False) + sequence_number: int = field(init=False) + + def __post_init__(self): + serial_number_regex = r"BS(\d+)-(\d+)\.(\d+)" + match = re.match(serial_number_regex, self.serial) + if not match: + raise ValueError(f"Invalid serial number: {self.serial}") + + object.__setattr__(self, "sequence_number", int(match.group(1))) + object.__setattr__(self, "major_version", int(match.group(2))) + object.__setattr__(self, "minor_version", int(match.group(3))) From 714fbe0e0e0f7a04c352c63a010bfd2c65e035b0 Mon Sep 17 00:00:00 2001 From: Rob Berwick Date: Sun, 1 Dec 2024 08:34:36 +0000 Subject: [PATCH 06/10] test: add tests for serial number model --- tests/models/__init__.py | 0 tests/models/serial_number.py | 17 +++++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 tests/models/__init__.py create mode 100644 tests/models/serial_number.py diff --git a/tests/models/__init__.py b/tests/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/models/serial_number.py b/tests/models/serial_number.py new file mode 100644 index 0000000..c5dbb3c --- /dev/null +++ b/tests/models/serial_number.py @@ -0,0 +1,17 @@ +import pytest +from blinkstick.models import SerialDetails + + +def test_serial_number_initialization(): + serial = "BS123456-1.0" + serial_number = SerialDetails(serial=serial) + + assert serial_number.serial == serial + assert serial_number.sequence_number == 123456 + assert serial_number.major_version == 1 + assert serial_number.minor_version == 0 + + +def test_serial_number_invalid_serial(): + with pytest.raises(ValueError, match="Invalid serial number: BS123456"): + SerialDetails(serial="BS123456") From ffce44511045df7a103b92c23d723676b9932dbe Mon Sep 17 00:00:00 2001 From: Rob Berwick Date: Sat, 30 Nov 2024 20:43:39 +0000 Subject: [PATCH 07/10] chore: expose major version and variant on BlinkStickDeviceClass --- src/blinkstick/devices/device.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/blinkstick/devices/device.py b/src/blinkstick/devices/device.py index 410f8c9..fa9c1f2 100644 --- a/src/blinkstick/devices/device.py +++ b/src/blinkstick/devices/device.py @@ -1,6 +1,8 @@ -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import Generic, TypeVar +from blinkstick.enums import BlinkStickVariant + T = TypeVar("T") @@ -13,3 +15,11 @@ class BlinkStickDevice(Generic[T]): manufacturer: str version_attribute: int description: str + major_version: int = field(init=False) + variant: BlinkStickVariant = field(init=False) + + def __post_init__(self): + self.major_version = int(self.serial[-3]) + self.variant = BlinkStickVariant.from_version_attrs( + major_version=self.major_version, version_attribute=self.version_attribute + ) From 2df7344d7a4751ed72d53599973c6ad10a0f4d16 Mon Sep 17 00:00:00 2001 From: Rob Berwick Date: Sun, 1 Dec 2024 15:42:24 +0000 Subject: [PATCH 08/10] refactor: access serial through blinkstick_device --- src/blinkstick/backends/base.py | 7 +++---- src/blinkstick/backends/unix_like.py | 21 +++++++++------------ src/blinkstick/backends/win32.py | 14 ++++++-------- src/blinkstick/devices/device.py | 7 ++++--- 4 files changed, 22 insertions(+), 27 deletions(-) diff --git a/src/blinkstick/backends/base.py b/src/blinkstick/backends/base.py index ac50d20..1f1e028 100644 --- a/src/blinkstick/backends/base.py +++ b/src/blinkstick/backends/base.py @@ -11,11 +11,10 @@ class BaseBackend(ABC, Generic[T]): - serial: str | None blinkstick_device: BlinkStickDevice[T] - def __init__(self): - self.serial = None + def __init__(self, device: BlinkStickDevice[T]): + self.blinkstick_device = device @abstractmethod def _refresh_attached_blinkstick_device(self): @@ -45,7 +44,7 @@ def control_transfer( raise NotImplementedError def get_serial(self) -> str: - return self.blinkstick_device.serial + return self.blinkstick_device.serial_details.serial def get_manufacturer(self) -> str: return self.blinkstick_device.manufacturer diff --git a/src/blinkstick/backends/unix_like.py b/src/blinkstick/backends/unix_like.py index d30f443..052d9f1 100644 --- a/src/blinkstick/backends/unix_like.py +++ b/src/blinkstick/backends/unix_like.py @@ -7,19 +7,14 @@ from blinkstick.backends.base import BaseBackend from blinkstick.devices import BlinkStickDevice from blinkstick.exceptions import BlinkStickException +from blinkstick.models import SerialDetails class UnixLikeBackend(BaseBackend[usb.core.Device]): - - serial: str - blinkstick_device: BlinkStickDevice[usb.core.Device] - - def __init__(self, device=None): - self.blinkstick_device = device - super().__init__() + def __init__(self, device: BlinkStickDevice[usb.core.Device]): + super().__init__(device=device) if device: self.open_device() - self.serial = self.get_serial() def open_device(self) -> None: if self.blinkstick_device is None: @@ -34,7 +29,7 @@ def open_device(self) -> None: def _refresh_attached_blinkstick_device(self): if not self.blinkstick_device: return False - if devices := self.find_by_serial(self.blinkstick_device.serial): + if devices := self.find_by_serial(self.blinkstick_device.serial_details.serial): self.blinkstick_device = devices[0] self.open_device() return True @@ -51,7 +46,9 @@ def get_attached_blinkstick_devices( # TODO: refactor this to DRY up the usb.util.get_string calls BlinkStickDevice( raw_device=device, - serial=str(usb.util.get_string(device, 3, 1033)), + serial_details=SerialDetails( + serial=str(usb.util.get_string(device, 3, 1033)) + ), manufacturer=str(usb.util.get_string(device, 1, 1033)), version_attribute=device.bcdDevice, description=str(usb.util.get_string(device, 2, 1033)), @@ -63,7 +60,7 @@ def get_attached_blinkstick_devices( def find_by_serial(serial: str) -> list[BlinkStickDevice[usb.core.Device]] | None: found_devices = UnixLikeBackend.get_attached_blinkstick_devices() for d in found_devices: - if d.serial == serial: + if d.serial_details.serial == serial: return [d] return None @@ -91,6 +88,6 @@ def control_transfer( else: raise BlinkStickException( "Could not communicate with BlinkStick {0} - it may have been removed".format( - self.serial + self.blinkstick_device.serial_details.serial ) ) diff --git a/src/blinkstick/backends/win32.py b/src/blinkstick/backends/win32.py index 9399951..c0e9c5c 100644 --- a/src/blinkstick/backends/win32.py +++ b/src/blinkstick/backends/win32.py @@ -9,16 +9,14 @@ from blinkstick.backends.base import BaseBackend from blinkstick.devices import BlinkStickDevice from blinkstick.exceptions import BlinkStickException +from blinkstick.models import SerialDetails class Win32Backend(BaseBackend[hid.HidDevice]): - serial: str - blinkstick_device: BlinkStickDevice[hid.HidDevice] reports: list[hid.core.HidReport] def __init__(self, device: BlinkStickDevice[hid.HidDevice]): - super().__init__() - self.blinkstick_device = device + super().__init__(device=device) if device: self.blinkstick_device.raw_device.open() self.reports = self.blinkstick_device.raw_device.find_feature_reports() @@ -28,16 +26,16 @@ def __init__(self, device: BlinkStickDevice[hid.HidDevice]): def find_by_serial(serial: str) -> list[BlinkStickDevice[hid.HidDevice]] | None: found_devices = Win32Backend.get_attached_blinkstick_devices() for d in found_devices: - if d.serial == serial: + if d.serial_details.serial == serial: return [d] return None def _refresh_attached_blinkstick_device(self): # TODO This is weird semantics. fix up return values to be more sensible - if not self.serial: + if not self.blinkstick_device: return False - if devices := self.find_by_serial(self.serial): + if devices := self.find_by_serial(self.blinkstick_device.serial_details.serial): self.blinkstick_device = devices[0] self.blinkstick_device.raw_device.open() self.reports = self.blinkstick_device.raw_device.find_feature_reports() @@ -54,7 +52,7 @@ def get_attached_blinkstick_devices( blinkstick_devices = [ BlinkStickDevice( raw_device=device, - serial=device.serial_number, + serial_details=SerialDetails(serial=device.serial_number), manufacturer=device.vendor_name, version_attribute=device.version_number, description=device.product_name, diff --git a/src/blinkstick/devices/device.py b/src/blinkstick/devices/device.py index fa9c1f2..dc13792 100644 --- a/src/blinkstick/devices/device.py +++ b/src/blinkstick/devices/device.py @@ -2,6 +2,7 @@ from typing import Generic, TypeVar from blinkstick.enums import BlinkStickVariant +from blinkstick.models import SerialDetails T = TypeVar("T") @@ -11,7 +12,7 @@ class BlinkStickDevice(Generic[T]): """A BlinkStick device representation""" raw_device: T - serial: str + serial_details: SerialDetails manufacturer: str version_attribute: int description: str @@ -19,7 +20,7 @@ class BlinkStickDevice(Generic[T]): variant: BlinkStickVariant = field(init=False) def __post_init__(self): - self.major_version = int(self.serial[-3]) self.variant = BlinkStickVariant.from_version_attrs( - major_version=self.major_version, version_attribute=self.version_attribute + major_version=self.serial_details.major_version, + version_attribute=self.version_attribute, ) From 1aa1b0f97dcc23c7ad2d2b804b6e172d2ec38485 Mon Sep 17 00:00:00 2001 From: Rob Berwick Date: Sun, 1 Dec 2024 16:04:24 +0000 Subject: [PATCH 09/10] test: add tests for BlinkStickDevice class --- tests/devices/__init__.py | 0 tests/devices/test_blinkstick_device.py | 60 +++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 tests/devices/__init__.py create mode 100644 tests/devices/test_blinkstick_device.py diff --git a/tests/devices/__init__.py b/tests/devices/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/devices/test_blinkstick_device.py b/tests/devices/test_blinkstick_device.py new file mode 100644 index 0000000..ad8ad3f --- /dev/null +++ b/tests/devices/test_blinkstick_device.py @@ -0,0 +1,60 @@ +import pytest +from blinkstick.enums import BlinkStickVariant +from blinkstick.models import SerialDetails +from blinkstick.devices.device import BlinkStickDevice + + +@pytest.fixture +def make_serial_details(): + def _serial_details(serial: str = "BS123456-1.0") -> SerialDetails: + return SerialDetails(serial=serial) + + return _serial_details + + +@pytest.fixture +def make_blinkstick_device(mocker, make_serial_details): + def _make_blinkstick_device( + manufacturer: str = "Test Manufacturer", + version_attribute: int = 1, + description: str = "Test Description", + serial_number: str = "BS123456-1.0", + ): + return BlinkStickDevice( + raw_device=mocker.MagicMock(), + serial_details=make_serial_details(serial=serial_number), + manufacturer=manufacturer, + version_attribute=version_attribute, + description=description, + ) + + return _make_blinkstick_device + + +def test_blinkstick_device_initialization(make_blinkstick_device): + blinkstick_device = make_blinkstick_device( + manufacturer="Test Manufacturer", + version_attribute=1, + description="Test Description", + ) + assert blinkstick_device.manufacturer == "Test Manufacturer" + assert blinkstick_device.version_attribute == 1 + assert blinkstick_device.description == "Test Description" + assert blinkstick_device.serial_details.serial == "BS123456-1.0" + + +def test_blinkstick_device_variant(make_blinkstick_device): + manufacturer = "Test Manufacturer" + version_attribute = 1 + description = "Test Description" + serial_number = "BS123456-1.0" + + blinkstick_device = make_blinkstick_device( + manufacturer=manufacturer, + version_attribute=version_attribute, + description=description, + ) + assert blinkstick_device.variant == BlinkStickVariant.from_version_attrs( + major_version=1, # major version is 1 from the serial number + version_attribute=version_attribute, + ) From 3fdef2189bcba1ed56bf5b1889d4faf191b8cf56 Mon Sep 17 00:00:00 2001 From: Rob Berwick Date: Sun, 1 Dec 2024 16:45:09 +0000 Subject: [PATCH 10/10] chore: update random color example --- examples/random_color.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/random_color.py b/examples/random_color.py index c9eb068..19f8d41 100644 --- a/examples/random_color.py +++ b/examples/random_color.py @@ -1,8 +1,7 @@ -import blinkstick.core -from blinkstick.clients import blinkstick +from blinkstick import find_first from blinkstick.exceptions import BlinkStickException -bs = blinkstick.core.find_first() +bs = find_first() if bs is None: print("Could not find any BlinkSticks")