Skip to content

Commit

Permalink
Merge pull request #116 from robberwick/device-major-version-variant
Browse files Browse the repository at this point in the history
Device major version variant
  • Loading branch information
robberwick authored Dec 1, 2024
2 parents e166311 + 3fdef21 commit e402922
Show file tree
Hide file tree
Showing 16 changed files with 200 additions and 84 deletions.
5 changes: 2 additions & 3 deletions examples/random_color.py
Original file line number Diff line number Diff line change
@@ -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")
Expand Down
2 changes: 1 addition & 1 deletion src/blinkstick/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
10 changes: 6 additions & 4 deletions src/blinkstick/backends/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand All @@ -55,3 +54,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
21 changes: 9 additions & 12 deletions src/blinkstick/backends/unix_like.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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)),
Expand All @@ -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
Expand Down Expand Up @@ -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
)
)
14 changes: 6 additions & 8 deletions src/blinkstick/backends/win32.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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()
Expand All @@ -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,
Expand Down
9 changes: 2 additions & 7 deletions src/blinkstick/clients/blinkstick.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
"""
Expand Down
41 changes: 0 additions & 41 deletions src/blinkstick/constants.py
Original file line number Diff line number Diff line change
@@ -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
15 changes: 13 additions & 2 deletions src/blinkstick/devices/device.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from dataclasses import dataclass
from dataclasses import dataclass, field
from typing import Generic, TypeVar

from blinkstick.enums import BlinkStickVariant
from blinkstick.models import SerialDetails

T = TypeVar("T")


Expand All @@ -9,7 +12,15 @@ class BlinkStickDevice(Generic[T]):
"""A BlinkStick device representation"""

raw_device: T
serial: str
serial_details: SerialDetails
manufacturer: str
version_attribute: int
description: str
major_version: int = field(init=False)
variant: BlinkStickVariant = field(init=False)

def __post_init__(self):
self.variant = BlinkStickVariant.from_version_attrs(
major_version=self.serial_details.major_version,
version_attribute=self.version_attribute,
)
40 changes: 40 additions & 0 deletions src/blinkstick/enums.py
Original file line number Diff line number Diff line change
@@ -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 from_version_attrs(
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
32 changes: 32 additions & 0 deletions src/blinkstick/models.py
Original file line number Diff line number Diff line change
@@ -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)))
8 changes: 6 additions & 2 deletions src/scripts/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
10 changes: 6 additions & 4 deletions tests/clients/test_blinkstick.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -54,8 +54,10 @@ 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.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

Expand Down
Empty file added tests/devices/__init__.py
Empty file.
Loading

0 comments on commit e402922

Please sign in to comment.