From 7cd720aa2038d6b5138f5f1cfc03f56649e32f86 Mon Sep 17 00:00:00 2001 From: Hanno Perrey Date: Thu, 30 May 2024 14:15:52 +0200 Subject: [PATCH 01/30] add IPv4Transport and IPv4TransportHandle classes --- src/seabreeze/pyseabreeze/transport.py | 192 +++++++++++++++++++++++++ 1 file changed, 192 insertions(+) diff --git a/src/seabreeze/pyseabreeze/transport.py b/src/seabreeze/pyseabreeze/transport.py index 43068a50..11e7cf8b 100644 --- a/src/seabreeze/pyseabreeze/transport.py +++ b/src/seabreeze/pyseabreeze/transport.py @@ -20,6 +20,8 @@ import usb.core import usb.util +import socket + from seabreeze.pyseabreeze.types import PySeaBreezeProtocol from seabreeze.pyseabreeze.types import PySeaBreezeTransport @@ -364,3 +366,193 @@ def get_name_from_pyusb_backend(backend: usb.backend.IBackend) -> str | None: if not module: return None return module.__name__.split(".")[-1] + +# ___ ____ _ _ +# |_ _| _ \__ _| || | +# | || |_) \ \ / / || |_ +# | || __/ \ V /|__ _| +# |___|_| \_/ |_| + + +# this can and should be opaque to pyseabreeze +class IPv4TransportHandle: + def __init__(self, socket: socket.socket, address: str = None, port: int = None) -> None: + """encapsulation for IPv4 socket classes + + Parameters + ---------- + + """ + self.socket: socket.socket = socket + # TODO check if socket is connected and get address via socket (socket.getpeername()) + self.identity: tuple[str, int] = (address, port) + + def close(self) -> None: + # TODO check for exceptions that close() might throw + self.socket.close() + + def __del__(self) -> None: + self.close() + self.socket = None + + +class IPv4Transport(PySeaBreezeTransport[IPv4TransportHandle]): + """implementation of the IPv4 socket transport interface for spectrometers""" + + _required_init_kwargs = ( + "ipv4_address", + "ipv4_port", + "ipv4_protocol", + ) + + # add logging + _log = logging.getLogger(__name__) + + def __init__( + self, + ipv4_address: str, + ipv4_port: int, + ipv4_protocol: type[PySeaBreezeProtocol], + ) -> None: + super().__init__() + self._address = ipv4_address + self._port = ipv4_port + self._protocol_cls = ipv4_protocol + # internal state + self._device: IPv4TransportHandle | None = None + self._opened: bool | None = None + self._protocol: PySeaBreezeProtocol | None = None + + def open_device(self, device: IPv4TransportHandle) -> None: + if not isinstance(device, IPv4TransportHandle): + raise TypeError("device needs to be an IPv4TransportHandle") + # TODO handle possible exceptions + self._device = device + self._opened = True + + # This will initialize the communication protocol + if self._opened: + self._protocol = self._protocol_cls(self) + + @property + def is_open(self) -> bool: + return self._opened or False + + def close_device(self) -> None: + if self._device is not None: + self._device.close() + self._device = None + self._opened = False + self._protocol = None + + def write(self, data: bytes, timeout_ms: int | None = None, **kwargs: Any) -> int: + if self._device is None: + raise RuntimeError("device not opened") + if kwargs: + warnings.warn(f"kwargs provided but ignored: {kwargs}") + if timeout_ms: + self._device.socket.settimeout(timeout_ms / 1000.0) + return self._device.socket.send(data) + + def read( + self, + size: int | None = None, + timeout_ms: int | None = None, + mode: str | None = None, + **kwargs: Any, + ) -> bytes: + if self._device is None: + raise RuntimeError("device not opened") + if size is None: + # use minimum packet size (no payload) + size = 64 + if kwargs: + warnings.warn(f"kwargs provided but ignored: {kwargs}") + if timeout_ms: + self._device.socket.settimeout(timeout_ms / 1000.0) + data = bytearray(size) + toread = size + view = memoryview(data) + while toread: + nbytes = self._device.socket.recv_into(view, toread) + view = view[nbytes:] + toread -= nbytes + return bytes(data) + + @property + def default_timeout_ms(self) -> int: + if not self._device: + raise RuntimeError("no protocol instance available") + timeout = self._device.socket.gettimeout() + if not timeout: + return 10000 + return int(timeout * 1000) # type: ignore + + @property + def protocol(self) -> PySeaBreezeProtocol: + if self._protocol is None: + raise RuntimeError("no protocol instance available") + return self._protocol + + @classmethod + def list_devices(cls, **kwargs: Any) -> Iterable[IPv4TransportHandle]: + """list IPv4 devices for all available spectrometers + + Note: this includes spectrometers that are currently opened in other + processes on the machine. + + Yields + ------ + devices : IPv4TransportHandle + unique socket devices for each available spectrometer + """ + # TODO use multicast to discover potential spectrometers + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + # FIXME this uses the default address only + sock.connect(('192.168.254.254', 57357)) + for dev in range(1): + yield IPv4TransportHandle(sock) + + @classmethod + def register_model(cls, model_name: str, **kwargs: Any) -> None: + # TODO implement e.g. IP to model mapping? + print(f"Trying to register {model_name} with {kwargs}") + pass + + @classmethod + def supported_model(cls, device: IPv4TransportHandle) -> str | None: + """return supported model + + Parameters + ---------- + device : IPv4TransportHandle + """ + # TODO implement this method + if not isinstance(device, IPv4TransportHandle): + return None + # noinspection PyUnresolvedReferences + # FIXME return something other then a hard-coded string + return "HDX" + + @classmethod + def specialize(cls, model_name: str, **kwargs: Any) -> type[IPv4Transport]: + # TODO check that this makes sense for the ipv4 transport + assert set(kwargs) == set(cls._required_init_kwargs) + # ipv4 transport register automatically on registration + cls.register_model(model_name, **kwargs) + specialized_class = type( + f"IPv4Transport{model_name}", + (cls,), + {"__init__": partialmethod(cls.__init__, **kwargs)}, + ) + return specialized_class + + @classmethod + def initialize(cls, **_kwargs: Any) -> None: + # TODO implement socket resent (close/open?) + pass + + @classmethod + def shutdown(cls, **_kwargs: Any) -> None: + # TODO implement + pass From 04c2a2bca75aaa21a78744b7b9064382acd72474 Mon Sep 17 00:00:00 2001 From: Hanno Perrey Date: Thu, 30 May 2024 14:16:53 +0200 Subject: [PATCH 02/30] pyseabreeze: add basic IPv4 support --- src/seabreeze/pyseabreeze/api.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/seabreeze/pyseabreeze/api.py b/src/seabreeze/pyseabreeze/api.py index 42622960..cefe0f12 100644 --- a/src/seabreeze/pyseabreeze/api.py +++ b/src/seabreeze/pyseabreeze/api.py @@ -21,6 +21,8 @@ from seabreeze.pyseabreeze.transport import USBTransportDeviceInUse from seabreeze.pyseabreeze.transport import USBTransportError from seabreeze.pyseabreeze.transport import USBTransportHandle +from seabreeze.pyseabreeze.transport import IPv4Transport +from seabreeze.pyseabreeze.transport import IPv4TransportHandle from seabreeze.types import SeaBreezeAPI as _SeaBreezeAPIProtocol if TYPE_CHECKING: @@ -35,20 +37,20 @@ ] = weakref.WeakValueDictionary() -def _seabreeze_device_factory(device: USBTransportHandle) -> SeaBreezeDevice: +def _seabreeze_device_factory(device: USBTransportHandle | IPv4TransportHandle) -> SeaBreezeDevice: """return existing instances instead of creating temporary ones Parameters ---------- - device : USBTransportHandle + device : USBTransportHandle | IPv4TransportHandle Returns ------- dev : SeaBreezeDevice """ global _seabreeze_device_instance_registry - if not isinstance(device, USBTransportHandle): - raise TypeError("needs to be instance of USBTransportHandle") + if not isinstance(device, USBTransportHandle) and not isinstance(device, IPv4TransportHandle): + raise TypeError(f"needs to be instance of USBTransportHandle or IPv4TransportHandle and not '{type(device)}'") ident = device.identity try: return _seabreeze_device_instance_registry[ident] @@ -127,6 +129,19 @@ def list_devices(self) -> list[_SeaBreezeDevice]: else: dev.close() devices.append(dev) # type: ignore + for ipv4_dev in IPv4Transport.list_devices(**self._kwargs): + # get the correct communication interface + dev = _seabreeze_device_factory(ipv4_dev) + if not dev.is_open: + # opening the device will populate its serial number + try: + dev.open() + except Exception: + # TODO specify expected exception + raise + #else: + # dev.close() + devices.append(dev) # type: ignore return devices # note: to be fully consistent with cseabreeze this shouldn't be a staticmethod From 136a805503d9142616288f21d7488c87887a0ce6 Mon Sep 17 00:00:00 2001 From: Hanno Perrey Date: Thu, 30 May 2024 14:17:37 +0200 Subject: [PATCH 03/30] devices: add support for IPv4 --- src/seabreeze/pyseabreeze/devices.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/seabreeze/pyseabreeze/devices.py b/src/seabreeze/pyseabreeze/devices.py index c56ff73c..8f9e7eec 100644 --- a/src/seabreeze/pyseabreeze/devices.py +++ b/src/seabreeze/pyseabreeze/devices.py @@ -21,6 +21,7 @@ from seabreeze.pyseabreeze.protocol import OBPProtocol from seabreeze.pyseabreeze.protocol import OOIProtocol from seabreeze.pyseabreeze.transport import USBTransport +from seabreeze.pyseabreeze.transport import IPv4Transport from seabreeze.pyseabreeze.types import PySeaBreezeTransport from seabreeze.types import SeaBreezeFeatureAccessor @@ -312,7 +313,7 @@ def __new__(cls: type[DT], raw_device: Any = None) -> SeaBreezeDevice: raise SeaBreezeError( "Don't instantiate SeaBreezeDevice directly. Use `SeabreezeAPI.list_devices()`." ) - for transport in {USBTransport}: + for transport in {USBTransport, IPv4Transport}: supported_model = transport.supported_model(raw_device) if supported_model is not None: break @@ -365,7 +366,7 @@ def __repr__(self) -> str: return f"" def open(self) -> None: - """open the spectrometer usb connection + """open the spectrometer connection Returns ------- From bb175dd10df8ba79e051f16b3e9ff5592fa009ac Mon Sep 17 00:00:00 2001 From: Hanno Perrey Date: Thu, 30 May 2024 14:17:53 +0200 Subject: [PATCH 04/30] HDX: move to IPv4Transport [WIP] --- src/seabreeze/pyseabreeze/devices.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/seabreeze/pyseabreeze/devices.py b/src/seabreeze/pyseabreeze/devices.py index 8f9e7eec..19a1cf92 100644 --- a/src/seabreeze/pyseabreeze/devices.py +++ b/src/seabreeze/pyseabreeze/devices.py @@ -1147,13 +1147,17 @@ class HDX(SeaBreezeDevice): model_name = "HDX" # communication config - transport = (USBTransport,) - usb_vendor_id = 0x2457 - usb_product_id = 0x2003 - usb_endpoint_map = EndPointMap( - ep_out=0x01, lowspeed_in=0x81, highspeed_in=0x82, highspeed_in2=0x86 - ) - usb_protocol = OBPProtocol + transport = (IPv4Transport, ) + #usb_vendor_id = 0x2457 + #usb_product_id = 0x2003 + #usb_endpoint_map = EndPointMap( + # ep_out=0x01, lowspeed_in=0x81, highspeed_in=0x82, highspeed_in2=0x86 + #) + #usb_protocol = OBPProtocol + + ipv4_address = '192.168.254.254' + ipv4_port = 57357 + ipv4_protocol = OBPProtocol # spectrometer config dark_pixel_indices = DarkPixelIndices.from_ranges() From aeb32c6bf8f188199cfc3f070b13aaaa231e294c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 30 May 2024 12:28:24 +0000 Subject: [PATCH 05/30] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/seabreeze/pyseabreeze/api.py | 18 ++++++++++++------ src/seabreeze/pyseabreeze/devices.py | 16 ++++++++-------- src/seabreeze/pyseabreeze/transport.py | 12 +++++++----- 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/src/seabreeze/pyseabreeze/api.py b/src/seabreeze/pyseabreeze/api.py index cefe0f12..a4ca1e58 100644 --- a/src/seabreeze/pyseabreeze/api.py +++ b/src/seabreeze/pyseabreeze/api.py @@ -17,12 +17,12 @@ from seabreeze.pyseabreeze.devices import SeaBreezeDevice from seabreeze.pyseabreeze.devices import _model_class_registry from seabreeze.pyseabreeze.transport import DeviceIdentity +from seabreeze.pyseabreeze.transport import IPv4Transport +from seabreeze.pyseabreeze.transport import IPv4TransportHandle from seabreeze.pyseabreeze.transport import USBTransport from seabreeze.pyseabreeze.transport import USBTransportDeviceInUse from seabreeze.pyseabreeze.transport import USBTransportError from seabreeze.pyseabreeze.transport import USBTransportHandle -from seabreeze.pyseabreeze.transport import IPv4Transport -from seabreeze.pyseabreeze.transport import IPv4TransportHandle from seabreeze.types import SeaBreezeAPI as _SeaBreezeAPIProtocol if TYPE_CHECKING: @@ -37,7 +37,9 @@ ] = weakref.WeakValueDictionary() -def _seabreeze_device_factory(device: USBTransportHandle | IPv4TransportHandle) -> SeaBreezeDevice: +def _seabreeze_device_factory( + device: USBTransportHandle | IPv4TransportHandle, +) -> SeaBreezeDevice: """return existing instances instead of creating temporary ones Parameters @@ -49,8 +51,12 @@ def _seabreeze_device_factory(device: USBTransportHandle | IPv4TransportHandle) dev : SeaBreezeDevice """ global _seabreeze_device_instance_registry - if not isinstance(device, USBTransportHandle) and not isinstance(device, IPv4TransportHandle): - raise TypeError(f"needs to be instance of USBTransportHandle or IPv4TransportHandle and not '{type(device)}'") + if not isinstance(device, USBTransportHandle) and not isinstance( + device, IPv4TransportHandle + ): + raise TypeError( + f"needs to be instance of USBTransportHandle or IPv4TransportHandle and not '{type(device)}'" + ) ident = device.identity try: return _seabreeze_device_instance_registry[ident] @@ -139,7 +145,7 @@ def list_devices(self) -> list[_SeaBreezeDevice]: except Exception: # TODO specify expected exception raise - #else: + # else: # dev.close() devices.append(dev) # type: ignore return devices diff --git a/src/seabreeze/pyseabreeze/devices.py b/src/seabreeze/pyseabreeze/devices.py index 19a1cf92..cd2599f8 100644 --- a/src/seabreeze/pyseabreeze/devices.py +++ b/src/seabreeze/pyseabreeze/devices.py @@ -20,8 +20,8 @@ from seabreeze.pyseabreeze.protocol import OBP2Protocol from seabreeze.pyseabreeze.protocol import OBPProtocol from seabreeze.pyseabreeze.protocol import OOIProtocol -from seabreeze.pyseabreeze.transport import USBTransport from seabreeze.pyseabreeze.transport import IPv4Transport +from seabreeze.pyseabreeze.transport import USBTransport from seabreeze.pyseabreeze.types import PySeaBreezeTransport from seabreeze.types import SeaBreezeFeatureAccessor @@ -1147,15 +1147,15 @@ class HDX(SeaBreezeDevice): model_name = "HDX" # communication config - transport = (IPv4Transport, ) - #usb_vendor_id = 0x2457 - #usb_product_id = 0x2003 - #usb_endpoint_map = EndPointMap( + transport = (IPv4Transport,) + # usb_vendor_id = 0x2457 + # usb_product_id = 0x2003 + # usb_endpoint_map = EndPointMap( # ep_out=0x01, lowspeed_in=0x81, highspeed_in=0x82, highspeed_in2=0x86 - #) - #usb_protocol = OBPProtocol + # ) + # usb_protocol = OBPProtocol - ipv4_address = '192.168.254.254' + ipv4_address = "192.168.254.254" ipv4_port = 57357 ipv4_protocol = OBPProtocol diff --git a/src/seabreeze/pyseabreeze/transport.py b/src/seabreeze/pyseabreeze/transport.py index 11e7cf8b..9b7f171b 100644 --- a/src/seabreeze/pyseabreeze/transport.py +++ b/src/seabreeze/pyseabreeze/transport.py @@ -9,6 +9,7 @@ import importlib import inspect import logging +import socket import warnings from functools import partialmethod from typing import TYPE_CHECKING @@ -20,8 +21,6 @@ import usb.core import usb.util -import socket - from seabreeze.pyseabreeze.types import PySeaBreezeProtocol from seabreeze.pyseabreeze.types import PySeaBreezeTransport @@ -367,6 +366,7 @@ def get_name_from_pyusb_backend(backend: usb.backend.IBackend) -> str | None: return None return module.__name__.split(".")[-1] + # ___ ____ _ _ # |_ _| _ \__ _| || | # | || |_) \ \ / / || |_ @@ -376,7 +376,9 @@ def get_name_from_pyusb_backend(backend: usb.backend.IBackend) -> str | None: # this can and should be opaque to pyseabreeze class IPv4TransportHandle: - def __init__(self, socket: socket.socket, address: str = None, port: int = None) -> None: + def __init__( + self, socket: socket.socket, address: str = None, port: int = None + ) -> None: """encapsulation for IPv4 socket classes Parameters @@ -486,7 +488,7 @@ def default_timeout_ms(self) -> int: timeout = self._device.socket.gettimeout() if not timeout: return 10000 - return int(timeout * 1000) # type: ignore + return int(timeout * 1000) # type: ignore @property def protocol(self) -> PySeaBreezeProtocol: @@ -509,7 +511,7 @@ def list_devices(cls, **kwargs: Any) -> Iterable[IPv4TransportHandle]: # TODO use multicast to discover potential spectrometers sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # FIXME this uses the default address only - sock.connect(('192.168.254.254', 57357)) + sock.connect(("192.168.254.254", 57357)) for dev in range(1): yield IPv4TransportHandle(sock) From 8e215a62cdb2433a1cabc953283d7b989e4af998 Mon Sep 17 00:00:00 2001 From: Hanno Perrey Date: Fri, 31 May 2024 11:05:38 +0200 Subject: [PATCH 06/30] IPv4: implement `register_model` removing hard-coded default values --- src/seabreeze/pyseabreeze/api.py | 3 +-- src/seabreeze/pyseabreeze/transport.py | 29 +++++++++++++++++--------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/seabreeze/pyseabreeze/api.py b/src/seabreeze/pyseabreeze/api.py index cefe0f12..162a4b9d 100644 --- a/src/seabreeze/pyseabreeze/api.py +++ b/src/seabreeze/pyseabreeze/api.py @@ -97,8 +97,7 @@ def add_ipv4_device_location( self, device_type: str, ip_address: str, port: int ) -> None: """add ipv4 device location""" - # IPV4Transport.register_device(device_type, ip_address, port) - raise NotImplementedError("ipv4 communication not implemented for pyseabreeze") + IPv4Transport.register_model(device_type, ipv4_address=ip_address, ipv4_port=port) def list_devices(self) -> list[_SeaBreezeDevice]: """returns available SeaBreezeDevices diff --git a/src/seabreeze/pyseabreeze/transport.py b/src/seabreeze/pyseabreeze/transport.py index 11e7cf8b..e1946649 100644 --- a/src/seabreeze/pyseabreeze/transport.py +++ b/src/seabreeze/pyseabreeze/transport.py @@ -21,6 +21,7 @@ import usb.util import socket +import ipaddress from seabreeze.pyseabreeze.types import PySeaBreezeProtocol from seabreeze.pyseabreeze.types import PySeaBreezeTransport @@ -400,23 +401,19 @@ class IPv4Transport(PySeaBreezeTransport[IPv4TransportHandle]): """implementation of the IPv4 socket transport interface for spectrometers""" _required_init_kwargs = ( - "ipv4_address", - "ipv4_port", "ipv4_protocol", ) + devices_ip_port: dict[tuple[str, int], str] = {} + # add logging _log = logging.getLogger(__name__) def __init__( self, - ipv4_address: str, - ipv4_port: int, ipv4_protocol: type[PySeaBreezeProtocol], ) -> None: super().__init__() - self._address = ipv4_address - self._port = ipv4_port self._protocol_cls = ipv4_protocol # internal state self._device: IPv4TransportHandle | None = None @@ -515,9 +512,21 @@ def list_devices(cls, **kwargs: Any) -> Iterable[IPv4TransportHandle]: @classmethod def register_model(cls, model_name: str, **kwargs: Any) -> None: - # TODO implement e.g. IP to model mapping? - print(f"Trying to register {model_name} with {kwargs}") - pass + ip = kwargs.get("ipv4_address") + if not isinstance(ip, str): + raise TypeError(f"ip address {ip} not a string") + try: + ipaddress.ip_address(ip) + except ValueError: + raise ValueError(f"ip address {ip} does not represent a valid IPv4 address") + port = kwargs.get("ipv4_port") + if not isinstance(port, int): + raise TypeError(f"port {port} not an integer") + if (ip, port) in cls.devices_ip_port: + raise ValueError( + f"ip address:port {ip}:{port} already in registry" + ) + cls.devices_ip_port[(ip, port)] = model_name @classmethod def supported_model(cls, device: IPv4TransportHandle) -> str | None: @@ -539,7 +548,7 @@ def specialize(cls, model_name: str, **kwargs: Any) -> type[IPv4Transport]: # TODO check that this makes sense for the ipv4 transport assert set(kwargs) == set(cls._required_init_kwargs) # ipv4 transport register automatically on registration - cls.register_model(model_name, **kwargs) + # cls.register_model(model_name, **kwargs) specialized_class = type( f"IPv4Transport{model_name}", (cls,), From 2aa2f17d2af3694e1840bc7840358bf4ac091f7b Mon Sep 17 00:00:00 2001 From: Hanno Perrey Date: Fri, 31 May 2024 11:07:04 +0200 Subject: [PATCH 07/30] IPv4: list devices based on known dev locations --- src/seabreeze/pyseabreeze/transport.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/seabreeze/pyseabreeze/transport.py b/src/seabreeze/pyseabreeze/transport.py index e1946649..8dd404c8 100644 --- a/src/seabreeze/pyseabreeze/transport.py +++ b/src/seabreeze/pyseabreeze/transport.py @@ -504,11 +504,17 @@ def list_devices(cls, **kwargs: Any) -> Iterable[IPv4TransportHandle]: unique socket devices for each available spectrometer """ # TODO use multicast to discover potential spectrometers - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - # FIXME this uses the default address only - sock.connect(('192.168.254.254', 57357)) - for dev in range(1): - yield IPv4TransportHandle(sock) + dev_sockets = [] + for address, model in cls.devices_ip_port.items(): + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + sock.connect(address) + except OSError as e: + raise RuntimeError(f"Could not connect to {address}: {e}") + else: + dev_sockets.append(sock) + for dev in dev_sockets: + yield IPv4TransportHandle(dev) @classmethod def register_model(cls, model_name: str, **kwargs: Any) -> None: From 467e66bc99f7feee911702dca143befe1b8ff584 Mon Sep 17 00:00:00 2001 From: Hanno Perrey Date: Fri, 31 May 2024 11:07:36 +0200 Subject: [PATCH 08/30] HDX: reinstate USB parameters --- src/seabreeze/pyseabreeze/devices.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/seabreeze/pyseabreeze/devices.py b/src/seabreeze/pyseabreeze/devices.py index 19a1cf92..fbd4b551 100644 --- a/src/seabreeze/pyseabreeze/devices.py +++ b/src/seabreeze/pyseabreeze/devices.py @@ -1147,16 +1147,14 @@ class HDX(SeaBreezeDevice): model_name = "HDX" # communication config - transport = (IPv4Transport, ) - #usb_vendor_id = 0x2457 - #usb_product_id = 0x2003 - #usb_endpoint_map = EndPointMap( - # ep_out=0x01, lowspeed_in=0x81, highspeed_in=0x82, highspeed_in2=0x86 - #) - #usb_protocol = OBPProtocol - - ipv4_address = '192.168.254.254' - ipv4_port = 57357 + transport = (IPv4Transport, USBTransport, ) + usb_vendor_id = 0x2457 + usb_product_id = 0x2003 + usb_endpoint_map = EndPointMap( + ep_out=0x01, lowspeed_in=0x81, highspeed_in=0x82, highspeed_in2=0x86 + ) + usb_protocol = OBPProtocol + ipv4_protocol = OBPProtocol # spectrometer config From e7b0be3b90078f6d6f2103a99dbf927ce402c9c3 Mon Sep 17 00:00:00 2001 From: Hanno Perrey Date: Fri, 31 May 2024 11:08:21 +0200 Subject: [PATCH 09/30] features: implement OBP multicast according to "Firmware and Advanced Communications: OCEAN HDX Firmware" --- src/seabreeze/pyseabreeze/devices.py | 1 + .../pyseabreeze/features/multicast.py | 41 ++++++++++++++++++- src/seabreeze/pyseabreeze/protocol.py | 4 ++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/seabreeze/pyseabreeze/devices.py b/src/seabreeze/pyseabreeze/devices.py index fbd4b551..f447d6da 100644 --- a/src/seabreeze/pyseabreeze/devices.py +++ b/src/seabreeze/pyseabreeze/devices.py @@ -1174,6 +1174,7 @@ class HDX(SeaBreezeDevice): sbf.spectrometer.SeaBreezeSpectrometerFeatureHDX, sbf.rawusb.SeaBreezeRawUSBBusAccessFeature, sbf.nonlinearity.NonlinearityCoefficientsFeatureOBP, + sbf.multicast.SeaBreezeMulticastFeatureOBP, ) diff --git a/src/seabreeze/pyseabreeze/features/multicast.py b/src/seabreeze/pyseabreeze/features/multicast.py index b10ef3bb..9f290fd0 100644 --- a/src/seabreeze/pyseabreeze/features/multicast.py +++ b/src/seabreeze/pyseabreeze/features/multicast.py @@ -1,5 +1,6 @@ +import struct from seabreeze.pyseabreeze.features._base import SeaBreezeFeature - +from seabreeze.pyseabreeze.protocol import OBPProtocol # Definition # ========== @@ -16,3 +17,41 @@ def set_multicast_enable_state( self, interface_index: int, enable_state: bool ) -> None: raise NotImplementedError("implement in derived class") + + def get_multicast_group_address(self, interface_index: int) -> tuple[int, int , int, int]: + raise NotImplementedError("implement in derived class") + + def get_multicast_group_port(self, interface_index: int) -> int: + raise NotImplementedError("implement in derived class") + + def get_multicast_ttl(self, interface_index: int) -> int: + raise NotImplementedError("implement in derived class") + + +# OBP implementation +# ================== +# +class SeaBreezeMulticastFeatureOBP(SeaBreezeMulticastFeature): + _required_protocol_cls = OBPProtocol + + def get_multicast_enable_state(self, interface_index: int) -> bool: + ret = self.protocol.query(0x00000A80, int(interface_index)) + return bool(struct.unpack(" None: + raise NotImplementedError("missing from HDX FW documentation") + + def get_multicast_group_address(self, interface_index: int) -> tuple[int, int, int, int]: + ret = self.protocol.query(0x00000A81, int(interface_index)) + ip = struct.unpack(" int: + ret = self.protocol.query(0x00000A82, int(interface_index)) + return struct.unpack(" int: + ret = self.protocol.query(0x00000A83, int(interface_index)) + return struct.unpack(" Date: Fri, 31 May 2024 09:11:26 +0000 Subject: [PATCH 10/30] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/seabreeze/pyseabreeze/api.py | 4 +++- src/seabreeze/pyseabreeze/devices.py | 5 ++++- src/seabreeze/pyseabreeze/features/multicast.py | 10 ++++++++-- src/seabreeze/pyseabreeze/protocol.py | 8 ++++---- src/seabreeze/pyseabreeze/transport.py | 12 +++--------- 5 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/seabreeze/pyseabreeze/api.py b/src/seabreeze/pyseabreeze/api.py index ca1a7dc7..51c7dcff 100644 --- a/src/seabreeze/pyseabreeze/api.py +++ b/src/seabreeze/pyseabreeze/api.py @@ -103,7 +103,9 @@ def add_ipv4_device_location( self, device_type: str, ip_address: str, port: int ) -> None: """add ipv4 device location""" - IPv4Transport.register_model(device_type, ipv4_address=ip_address, ipv4_port=port) + IPv4Transport.register_model( + device_type, ipv4_address=ip_address, ipv4_port=port + ) def list_devices(self) -> list[_SeaBreezeDevice]: """returns available SeaBreezeDevices diff --git a/src/seabreeze/pyseabreeze/devices.py b/src/seabreeze/pyseabreeze/devices.py index db92d7c2..04291ffa 100644 --- a/src/seabreeze/pyseabreeze/devices.py +++ b/src/seabreeze/pyseabreeze/devices.py @@ -1147,7 +1147,10 @@ class HDX(SeaBreezeDevice): model_name = "HDX" # communication config - transport = (IPv4Transport, USBTransport, ) + transport = ( + IPv4Transport, + USBTransport, + ) usb_vendor_id = 0x2457 usb_product_id = 0x2003 usb_endpoint_map = EndPointMap( diff --git a/src/seabreeze/pyseabreeze/features/multicast.py b/src/seabreeze/pyseabreeze/features/multicast.py index 9f290fd0..551605cd 100644 --- a/src/seabreeze/pyseabreeze/features/multicast.py +++ b/src/seabreeze/pyseabreeze/features/multicast.py @@ -1,7 +1,9 @@ import struct + from seabreeze.pyseabreeze.features._base import SeaBreezeFeature from seabreeze.pyseabreeze.protocol import OBPProtocol + # Definition # ========== # @@ -18,7 +20,9 @@ def set_multicast_enable_state( ) -> None: raise NotImplementedError("implement in derived class") - def get_multicast_group_address(self, interface_index: int) -> tuple[int, int , int, int]: + def get_multicast_group_address( + self, interface_index: int + ) -> tuple[int, int, int, int]: raise NotImplementedError("implement in derived class") def get_multicast_group_port(self, interface_index: int) -> int: @@ -43,7 +47,9 @@ def set_multicast_enable_state( ) -> None: raise NotImplementedError("missing from HDX FW documentation") - def get_multicast_group_address(self, interface_index: int) -> tuple[int, int, int, int]: + def get_multicast_group_address( + self, interface_index: int + ) -> tuple[int, int, int, int]: ret = self.protocol.query(0x00000A81, int(interface_index)) ip = struct.unpack(" None: class IPv4Transport(PySeaBreezeTransport[IPv4TransportHandle]): """implementation of the IPv4 socket transport interface for spectrometers""" - _required_init_kwargs = ( - "ipv4_protocol", - ) + _required_init_kwargs = ("ipv4_protocol",) devices_ip_port: dict[tuple[str, int], str] = {} @@ -533,9 +529,7 @@ def register_model(cls, model_name: str, **kwargs: Any) -> None: if not isinstance(port, int): raise TypeError(f"port {port} not an integer") if (ip, port) in cls.devices_ip_port: - raise ValueError( - f"ip address:port {ip}:{port} already in registry" - ) + raise ValueError(f"ip address:port {ip}:{port} already in registry") cls.devices_ip_port[(ip, port)] = model_name @classmethod From b754d41c3bd5ff8a3f6fbbda9bea779695911fa3 Mon Sep 17 00:00:00 2001 From: Hanno Perrey Date: Fri, 31 May 2024 13:16:55 +0200 Subject: [PATCH 11/30] ipv4transport: add multicast discovery listing dev --- src/seabreeze/pyseabreeze/transport.py | 51 ++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/src/seabreeze/pyseabreeze/transport.py b/src/seabreeze/pyseabreeze/transport.py index f6eed8b6..1397aee2 100644 --- a/src/seabreeze/pyseabreeze/transport.py +++ b/src/seabreeze/pyseabreeze/transport.py @@ -9,7 +9,6 @@ import importlib import inspect import logging -import socket import warnings from functools import partialmethod from typing import TYPE_CHECKING @@ -17,15 +16,16 @@ from typing import Iterable from typing import Tuple +import socket +import ipaddress + import usb.backend import usb.core import usb.util -import socket -import ipaddress - from seabreeze.pyseabreeze.types import PySeaBreezeProtocol from seabreeze.pyseabreeze.types import PySeaBreezeTransport +from seabreeze.pyseabreeze.protocol import OBPProtocol if TYPE_CHECKING: from seabreeze.pyseabreeze.devices import EndPointMap @@ -507,9 +507,44 @@ def list_devices(cls, **kwargs: Any) -> Iterable[IPv4TransportHandle]: devices : IPv4TransportHandle unique socket devices for each available spectrometer """ - # TODO use multicast to discover potential spectrometers dev_sockets = [] - for address, model in cls.devices_ip_port.items(): + + # Use multicast to discover potential spectrometers. Requires a network + # adapter to be specified. + network_adapter = kwargs.get("network_adapter", None) + if network_adapter: + multicast_group = ( + kwargs.get("multicast_group", '239.239.239.239'), + kwargs.get("multicast_port", 57357), + ) # default values for HDX devices + # Create the datagram (UDP) socket + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + # Set a timeout so the socket does not block + # indefinitely when trying to receive data. + sock.settimeout(kwargs.get("multicast_timeout", 2)) + sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF, socket.inet_aton(network_adapter)) + handle = IPv4TransportHandle(sock) + transport = IPv4Transport(handle) + protocol = OBPProtocol(transport) + msg_type = 0x00000100 # GET_SERIAL + data = protocol.msgs[msg_type](*()) + message = protocol._construct_outgoing_message( + msg_type, data, request_ack=True, + ) + sock.sendto(message, multicast_group) + print('Waiting to receive multicast response(s)') + while True: + try: + data = bytearray(90) + nbytes, server = sock.recvfrom_into(data) + except socket.timeout: + break + else: + serial = protocol._extract_message_data(data[:nbytes]) + cls.register_model(model_name=serial, ipv4_address=server[0], ipv4_port=server[1]) + + # connect to discovered and registered devices + for address in cls.devices_ip_port: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: sock.connect(address) @@ -517,8 +552,8 @@ def list_devices(cls, **kwargs: Any) -> Iterable[IPv4TransportHandle]: raise RuntimeError(f"Could not connect to {address}: {e}") else: dev_sockets.append(sock) - for dev in dev_sockets: - yield IPv4TransportHandle(dev) + for sock in dev_sockets: + yield IPv4TransportHandle(sock) @classmethod def register_model(cls, model_name: str, **kwargs: Any) -> None: From 3df7d493be4da8ba3fa31869c1ed3552bd65ddbc Mon Sep 17 00:00:00 2001 From: Hanno Perrey Date: Fri, 31 May 2024 13:52:50 +0200 Subject: [PATCH 12/30] IPv4TransportHandle: get port+address from socket --- src/seabreeze/pyseabreeze/transport.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/seabreeze/pyseabreeze/transport.py b/src/seabreeze/pyseabreeze/transport.py index 1397aee2..0700d71b 100644 --- a/src/seabreeze/pyseabreeze/transport.py +++ b/src/seabreeze/pyseabreeze/transport.py @@ -380,7 +380,7 @@ def get_name_from_pyusb_backend(backend: usb.backend.IBackend) -> str | None: # this can and should be opaque to pyseabreeze class IPv4TransportHandle: def __init__( - self, socket: socket.socket, address: str = None, port: int = None + self, sock: socket.socket ) -> None: """encapsulation for IPv4 socket classes @@ -388,8 +388,12 @@ def __init__( ---------- """ - self.socket: socket.socket = socket + self.socket: socket.socket = sock # TODO check if socket is connected and get address via socket (socket.getpeername()) + try: + address, port = sock.getpeername() + except OSError: + address, port = None, None self.identity: tuple[str, int] = (address, port) def close(self) -> None: From a5eed7c9e7f436fdf7d95d495b749c1023853f05 Mon Sep 17 00:00:00 2001 From: Hanno Perrey Date: Fri, 31 May 2024 13:53:06 +0200 Subject: [PATCH 13/30] IPv4Transport: fix type error --- src/seabreeze/pyseabreeze/transport.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/seabreeze/pyseabreeze/transport.py b/src/seabreeze/pyseabreeze/transport.py index 0700d71b..a60f5b15 100644 --- a/src/seabreeze/pyseabreeze/transport.py +++ b/src/seabreeze/pyseabreeze/transport.py @@ -527,8 +527,7 @@ def list_devices(cls, **kwargs: Any) -> Iterable[IPv4TransportHandle]: # indefinitely when trying to receive data. sock.settimeout(kwargs.get("multicast_timeout", 2)) sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF, socket.inet_aton(network_adapter)) - handle = IPv4TransportHandle(sock) - transport = IPv4Transport(handle) + transport = IPv4Transport(OBPProtocol) protocol = OBPProtocol(transport) msg_type = 0x00000100 # GET_SERIAL data = protocol.msgs[msg_type](*()) From abd378b0f719b667aaed50101e15ad9b812b75ba Mon Sep 17 00:00:00 2001 From: Hanno Perrey Date: Fri, 31 May 2024 14:27:37 +0200 Subject: [PATCH 14/30] isort, black, type fixes for mypy --- src/seabreeze/pyseabreeze/api.py | 4 +- src/seabreeze/pyseabreeze/devices.py | 5 ++- .../pyseabreeze/features/multicast.py | 13 +++--- src/seabreeze/pyseabreeze/protocol.py | 8 ++-- src/seabreeze/pyseabreeze/transport.py | 42 ++++++++++--------- 5 files changed, 41 insertions(+), 31 deletions(-) diff --git a/src/seabreeze/pyseabreeze/api.py b/src/seabreeze/pyseabreeze/api.py index ca1a7dc7..51c7dcff 100644 --- a/src/seabreeze/pyseabreeze/api.py +++ b/src/seabreeze/pyseabreeze/api.py @@ -103,7 +103,9 @@ def add_ipv4_device_location( self, device_type: str, ip_address: str, port: int ) -> None: """add ipv4 device location""" - IPv4Transport.register_model(device_type, ipv4_address=ip_address, ipv4_port=port) + IPv4Transport.register_model( + device_type, ipv4_address=ip_address, ipv4_port=port + ) def list_devices(self) -> list[_SeaBreezeDevice]: """returns available SeaBreezeDevices diff --git a/src/seabreeze/pyseabreeze/devices.py b/src/seabreeze/pyseabreeze/devices.py index db92d7c2..04291ffa 100644 --- a/src/seabreeze/pyseabreeze/devices.py +++ b/src/seabreeze/pyseabreeze/devices.py @@ -1147,7 +1147,10 @@ class HDX(SeaBreezeDevice): model_name = "HDX" # communication config - transport = (IPv4Transport, USBTransport, ) + transport = ( + IPv4Transport, + USBTransport, + ) usb_vendor_id = 0x2457 usb_product_id = 0x2003 usb_endpoint_map = EndPointMap( diff --git a/src/seabreeze/pyseabreeze/features/multicast.py b/src/seabreeze/pyseabreeze/features/multicast.py index 9f290fd0..19faa0b1 100644 --- a/src/seabreeze/pyseabreeze/features/multicast.py +++ b/src/seabreeze/pyseabreeze/features/multicast.py @@ -1,7 +1,10 @@ import struct +from typing import List + from seabreeze.pyseabreeze.features._base import SeaBreezeFeature from seabreeze.pyseabreeze.protocol import OBPProtocol + # Definition # ========== # @@ -18,7 +21,7 @@ def set_multicast_enable_state( ) -> None: raise NotImplementedError("implement in derived class") - def get_multicast_group_address(self, interface_index: int) -> tuple[int, int , int, int]: + def get_multicast_group_address(self, interface_index: int) -> List[int]: raise NotImplementedError("implement in derived class") def get_multicast_group_port(self, interface_index: int) -> int: @@ -43,15 +46,15 @@ def set_multicast_enable_state( ) -> None: raise NotImplementedError("missing from HDX FW documentation") - def get_multicast_group_address(self, interface_index: int) -> tuple[int, int, int, int]: + def get_multicast_group_address(self, interface_index: int) -> List[int]: ret = self.protocol.query(0x00000A81, int(interface_index)) ip = struct.unpack(" int: ret = self.protocol.query(0x00000A82, int(interface_index)) - return struct.unpack(" int: ret = self.protocol.query(0x00000A83, int(interface_index)) - return struct.unpack(" str | None: # this can and should be opaque to pyseabreeze class IPv4TransportHandle: - def __init__( - self, sock: socket.socket - ) -> None: + def __init__(self, sock: socket.socket) -> None: """encapsulation for IPv4 socket classes Parameters @@ -402,15 +399,12 @@ def close(self) -> None: def __del__(self) -> None: self.close() - self.socket = None class IPv4Transport(PySeaBreezeTransport[IPv4TransportHandle]): """implementation of the IPv4 socket transport interface for spectrometers""" - _required_init_kwargs = ( - "ipv4_protocol", - ) + _required_init_kwargs = ("ipv4_protocol",) devices_ip_port: dict[tuple[str, int], str] = {} @@ -491,7 +485,7 @@ def default_timeout_ms(self) -> int: timeout = self._device.socket.gettimeout() if not timeout: return 10000 - return int(timeout * 1000) # type: ignore + return int(timeout * 1000) @property def protocol(self) -> PySeaBreezeProtocol: @@ -518,7 +512,7 @@ def list_devices(cls, **kwargs: Any) -> Iterable[IPv4TransportHandle]: network_adapter = kwargs.get("network_adapter", None) if network_adapter: multicast_group = ( - kwargs.get("multicast_group", '239.239.239.239'), + kwargs.get("multicast_group", "239.239.239.239"), kwargs.get("multicast_port", 57357), ) # default values for HDX devices # Create the datagram (UDP) socket @@ -526,16 +520,22 @@ def list_devices(cls, **kwargs: Any) -> Iterable[IPv4TransportHandle]: # Set a timeout so the socket does not block # indefinitely when trying to receive data. sock.settimeout(kwargs.get("multicast_timeout", 2)) - sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF, socket.inet_aton(network_adapter)) + sock.setsockopt( + socket.IPPROTO_IP, + socket.IP_MULTICAST_IF, + socket.inet_aton(network_adapter), + ) transport = IPv4Transport(OBPProtocol) protocol = OBPProtocol(transport) msg_type = 0x00000100 # GET_SERIAL data = protocol.msgs[msg_type](*()) message = protocol._construct_outgoing_message( - msg_type, data, request_ack=True, + msg_type, + data, + request_ack=True, ) sock.sendto(message, multicast_group) - print('Waiting to receive multicast response(s)') + print("Waiting to receive multicast response(s)") while True: try: data = bytearray(90) @@ -544,7 +544,11 @@ def list_devices(cls, **kwargs: Any) -> Iterable[IPv4TransportHandle]: break else: serial = protocol._extract_message_data(data[:nbytes]) - cls.register_model(model_name=serial, ipv4_address=server[0], ipv4_port=server[1]) + cls.register_model( + model_name=serial.decode(), + ipv4_address=server[0], + ipv4_port=server[1], + ) # connect to discovered and registered devices for address in cls.devices_ip_port: @@ -571,9 +575,7 @@ def register_model(cls, model_name: str, **kwargs: Any) -> None: if not isinstance(port, int): raise TypeError(f"port {port} not an integer") if (ip, port) in cls.devices_ip_port: - raise ValueError( - f"ip address:port {ip}:{port} already in registry" - ) + raise ValueError(f"ip address:port {ip}:{port} already in registry") cls.devices_ip_port[(ip, port)] = model_name @classmethod From 561ee33dca872cf74df970e104b6f2c8877a76b1 Mon Sep 17 00:00:00 2001 From: Hanno Perrey Date: Fri, 31 May 2024 17:06:40 +0200 Subject: [PATCH 15/30] multicast: request product ID instead for serial allows to match to correct model via known USB parameters --- src/seabreeze/pyseabreeze/protocol.py | 3 +++ src/seabreeze/pyseabreeze/transport.py | 13 ++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/seabreeze/pyseabreeze/protocol.py b/src/seabreeze/pyseabreeze/protocol.py index 99961f3e..69a2a981 100644 --- a/src/seabreeze/pyseabreeze/protocol.py +++ b/src/seabreeze/pyseabreeze/protocol.py @@ -233,6 +233,9 @@ class OBPProtocol(PySeaBreezeProtocol): 0x00000A81: " Iterable[IPv4TransportHandle]: socket.IP_MULTICAST_IF, socket.inet_aton(network_adapter), ) + # prepare a message requesting all devices in the multicast group + # to send their (USB) product id transport = IPv4Transport(OBPProtocol) protocol = OBPProtocol(transport) - msg_type = 0x00000100 # GET_SERIAL + msg_type = 0xE01 # Product ID data = protocol.msgs[msg_type](*()) message = protocol._construct_outgoing_message( msg_type, @@ -543,9 +546,13 @@ def list_devices(cls, **kwargs: Any) -> Iterable[IPv4TransportHandle]: except socket.timeout: break else: - serial = protocol._extract_message_data(data[:nbytes]) + pid_raw = protocol._extract_message_data(data[:nbytes]) + pid = int(struct.unpack(" Date: Fri, 31 May 2024 17:07:27 +0200 Subject: [PATCH 16/30] ipv4transport: implement `supported_model` --- src/seabreeze/pyseabreeze/transport.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/seabreeze/pyseabreeze/transport.py b/src/seabreeze/pyseabreeze/transport.py index e67f2d54..c8a731e3 100644 --- a/src/seabreeze/pyseabreeze/transport.py +++ b/src/seabreeze/pyseabreeze/transport.py @@ -593,12 +593,9 @@ def supported_model(cls, device: IPv4TransportHandle) -> str | None: ---------- device : IPv4TransportHandle """ - # TODO implement this method if not isinstance(device, IPv4TransportHandle): return None - # noinspection PyUnresolvedReferences - # FIXME return something other then a hard-coded string - return "HDX" + return cls.devices_ip_port[device.identity] @classmethod def specialize(cls, model_name: str, **kwargs: Any) -> type[IPv4Transport]: @@ -615,7 +612,7 @@ def specialize(cls, model_name: str, **kwargs: Any) -> type[IPv4Transport]: @classmethod def initialize(cls, **_kwargs: Any) -> None: - # TODO implement socket resent (close/open?) + # TODO implement socket reset? (close/open?) pass @classmethod From 2d6732b06cd5e4ec2200cff623c977cd14964437 Mon Sep 17 00:00:00 2001 From: Hanno Perrey Date: Mon, 3 Jun 2024 10:15:30 +0200 Subject: [PATCH 17/30] ipv4transport: send multicast on INADDR_ANY if no `network_adapter` argument has been provided. This will send the multicast request on an "appropriate" interface, usually the one with the highest metric. --- src/seabreeze/pyseabreeze/transport.py | 106 ++++++++++++++----------- 1 file changed, 59 insertions(+), 47 deletions(-) diff --git a/src/seabreeze/pyseabreeze/transport.py b/src/seabreeze/pyseabreeze/transport.py index c8a731e3..ca88ae19 100644 --- a/src/seabreeze/pyseabreeze/transport.py +++ b/src/seabreeze/pyseabreeze/transport.py @@ -508,54 +508,66 @@ def list_devices(cls, **kwargs: Any) -> Iterable[IPv4TransportHandle]: """ dev_sockets = [] - # Use multicast to discover potential spectrometers. Requires a network - # adapter to be specified. + # Use multicast to discover potential spectrometers. If no network + # adapter was specified use INADDR_ANY: an appropriate interface is + # chosen by the system (see ip(7)). This is usually the interface with + # the highest metric. network_adapter = kwargs.get("network_adapter", None) - if network_adapter: - multicast_group = ( - kwargs.get("multicast_group", "239.239.239.239"), - kwargs.get("multicast_port", 57357), - ) # default values for HDX devices - # Create the datagram (UDP) socket - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - # Set a timeout so the socket does not block - # indefinitely when trying to receive data. - sock.settimeout(kwargs.get("multicast_timeout", 2)) - sock.setsockopt( - socket.IPPROTO_IP, - socket.IP_MULTICAST_IF, - socket.inet_aton(network_adapter), - ) - # prepare a message requesting all devices in the multicast group - # to send their (USB) product id - transport = IPv4Transport(OBPProtocol) - protocol = OBPProtocol(transport) - msg_type = 0xE01 # Product ID - data = protocol.msgs[msg_type](*()) - message = protocol._construct_outgoing_message( - msg_type, - data, - request_ack=True, - ) - sock.sendto(message, multicast_group) - print("Waiting to receive multicast response(s)") - while True: - try: - data = bytearray(90) - nbytes, server = sock.recvfrom_into(data) - except socket.timeout: - break - else: - pid_raw = protocol._extract_message_data(data[:nbytes]) - pid = int(struct.unpack(" Date: Mon, 3 Jun 2024 10:17:20 +0200 Subject: [PATCH 18/30] ipv4transport: move list initialization --- src/seabreeze/pyseabreeze/transport.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/seabreeze/pyseabreeze/transport.py b/src/seabreeze/pyseabreeze/transport.py index ca88ae19..e22d4411 100644 --- a/src/seabreeze/pyseabreeze/transport.py +++ b/src/seabreeze/pyseabreeze/transport.py @@ -506,8 +506,6 @@ def list_devices(cls, **kwargs: Any) -> Iterable[IPv4TransportHandle]: devices : IPv4TransportHandle unique socket devices for each available spectrometer """ - dev_sockets = [] - # Use multicast to discover potential spectrometers. If no network # adapter was specified use INADDR_ANY: an appropriate interface is # chosen by the system (see ip(7)). This is usually the interface with @@ -570,6 +568,7 @@ def list_devices(cls, **kwargs: Any) -> Iterable[IPv4TransportHandle]: ) # connect to discovered and registered devices + dev_sockets = [] for address in cls.devices_ip_port: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: From 1105993aae4c54acb484efd2ce8a15f182f2bdee Mon Sep 17 00:00:00 2001 From: Hanno Perrey Date: Mon, 3 Jun 2024 10:21:17 +0200 Subject: [PATCH 19/30] Update src/seabreeze/pyseabreeze/api.py improve style Co-authored-by: Andreas Poehlmann --- src/seabreeze/pyseabreeze/api.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/seabreeze/pyseabreeze/api.py b/src/seabreeze/pyseabreeze/api.py index 51c7dcff..942853b9 100644 --- a/src/seabreeze/pyseabreeze/api.py +++ b/src/seabreeze/pyseabreeze/api.py @@ -51,9 +51,7 @@ def _seabreeze_device_factory( dev : SeaBreezeDevice """ global _seabreeze_device_instance_registry - if not isinstance(device, USBTransportHandle) and not isinstance( - device, IPv4TransportHandle - ): + if not isinstance(device, (USBTransportHandle, IPv4TransportHandle)): raise TypeError( f"needs to be instance of USBTransportHandle or IPv4TransportHandle and not '{type(device)}'" ) From 7e6ef0f8a717d6efed229fdd3629d15c7542a42f Mon Sep 17 00:00:00 2001 From: Hanno Perrey Date: Mon, 3 Jun 2024 10:48:01 +0200 Subject: [PATCH 20/30] ipv4transport: remove print statement --- src/seabreeze/pyseabreeze/transport.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/seabreeze/pyseabreeze/transport.py b/src/seabreeze/pyseabreeze/transport.py index e22d4411..3d43f3e4 100644 --- a/src/seabreeze/pyseabreeze/transport.py +++ b/src/seabreeze/pyseabreeze/transport.py @@ -548,7 +548,6 @@ def list_devices(cls, **kwargs: Any) -> Iterable[IPv4TransportHandle]: request_ack=True, ) sock.sendto(message, (multicast_group, multicast_port)) - print("Waiting to receive multicast response(s)") while True: try: data = bytearray(90) From a21e3968f4907cf820e74d9f12c326e1de7fc93d Mon Sep 17 00:00:00 2001 From: Hanno Perrey Date: Mon, 3 Jun 2024 10:48:20 +0200 Subject: [PATCH 21/30] ipv4transport: remove unnecessary argument --- src/seabreeze/pyseabreeze/transport.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/seabreeze/pyseabreeze/transport.py b/src/seabreeze/pyseabreeze/transport.py index 3d43f3e4..97711437 100644 --- a/src/seabreeze/pyseabreeze/transport.py +++ b/src/seabreeze/pyseabreeze/transport.py @@ -541,7 +541,7 @@ def list_devices(cls, **kwargs: Any) -> Iterable[IPv4TransportHandle]: transport = IPv4Transport(OBPProtocol) protocol = OBPProtocol(transport) msg_type = 0xE01 # Product ID - data = protocol.msgs[msg_type](*()) + data = protocol.msgs[msg_type]() message = protocol._construct_outgoing_message( msg_type, data, From eabcc1c9d42381d8759c939bd107632bef636dfd Mon Sep 17 00:00:00 2001 From: Hanno Perrey Date: Mon, 3 Jun 2024 10:48:42 +0200 Subject: [PATCH 22/30] ipv4transport: remove TODOs --- src/seabreeze/pyseabreeze/transport.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/seabreeze/pyseabreeze/transport.py b/src/seabreeze/pyseabreeze/transport.py index 97711437..7f0a52c5 100644 --- a/src/seabreeze/pyseabreeze/transport.py +++ b/src/seabreeze/pyseabreeze/transport.py @@ -622,10 +622,8 @@ def specialize(cls, model_name: str, **kwargs: Any) -> type[IPv4Transport]: @classmethod def initialize(cls, **_kwargs: Any) -> None: - # TODO implement socket reset? (close/open?) pass @classmethod def shutdown(cls, **_kwargs: Any) -> None: - # TODO implement pass From 88afbe1744ba2549e7dfa3b9bc8542badf567aed Mon Sep 17 00:00:00 2001 From: Hanno Perrey Date: Mon, 3 Jun 2024 10:56:01 +0200 Subject: [PATCH 23/30] ipv4transporthandle: use weakref --- src/seabreeze/pyseabreeze/transport.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/seabreeze/pyseabreeze/transport.py b/src/seabreeze/pyseabreeze/transport.py index 7f0a52c5..9acc176b 100644 --- a/src/seabreeze/pyseabreeze/transport.py +++ b/src/seabreeze/pyseabreeze/transport.py @@ -13,6 +13,7 @@ import socket import struct import warnings +import weakref from functools import partialmethod from typing import TYPE_CHECKING from typing import Any @@ -393,13 +394,15 @@ def __init__(self, sock: socket.socket) -> None: except OSError: address, port = None, None self.identity: tuple[str, int] = (address, port) + # register callback to close socket on garbage collection + self._finalizer = weakref.finalize(self, self.socket.close) def close(self) -> None: - # TODO check for exceptions that close() might throw - self.socket.close() + self._finalizer() - def __del__(self) -> None: - self.close() + @property + def closed(self) -> bool: + return not self._finalizer.alive class IPv4Transport(PySeaBreezeTransport[IPv4TransportHandle]): From 4598793714768f350af371d5ae27b23006e7b9a8 Mon Sep 17 00:00:00 2001 From: Hanno Perrey Date: Mon, 3 Jun 2024 10:56:27 +0200 Subject: [PATCH 24/30] api: style improvement --- src/seabreeze/pyseabreeze/api.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/seabreeze/pyseabreeze/api.py b/src/seabreeze/pyseabreeze/api.py index 51c7dcff..942853b9 100644 --- a/src/seabreeze/pyseabreeze/api.py +++ b/src/seabreeze/pyseabreeze/api.py @@ -51,9 +51,7 @@ def _seabreeze_device_factory( dev : SeaBreezeDevice """ global _seabreeze_device_instance_registry - if not isinstance(device, USBTransportHandle) and not isinstance( - device, IPv4TransportHandle - ): + if not isinstance(device, (USBTransportHandle, IPv4TransportHandle)): raise TypeError( f"needs to be instance of USBTransportHandle or IPv4TransportHandle and not '{type(device)}'" ) From 6c362e1a79b2a3eb809114204d7880816b022bd9 Mon Sep 17 00:00:00 2001 From: Hanno Perrey Date: Mon, 3 Jun 2024 11:51:19 +0200 Subject: [PATCH 25/30] ipv4transport: reduce multicast timeout --- src/seabreeze/pyseabreeze/transport.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/seabreeze/pyseabreeze/transport.py b/src/seabreeze/pyseabreeze/transport.py index 9acc176b..8c7a3692 100644 --- a/src/seabreeze/pyseabreeze/transport.py +++ b/src/seabreeze/pyseabreeze/transport.py @@ -523,7 +523,7 @@ def list_devices(cls, **kwargs: Any) -> Iterable[IPv4TransportHandle]: sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # Set a timeout so the socket does not block # indefinitely when trying to receive data. - sock.settimeout(kwargs.get("multicast_timeout", 2)) + sock.settimeout(kwargs.get("multicast_timeout", 1)) sock.setsockopt( socket.IPPROTO_IP, socket.IP_MULTICAST_IF, From 8b2f6173746651c08e1db9b3ae0b769f6133f287 Mon Sep 17 00:00:00 2001 From: Hanno Perrey Date: Mon, 3 Jun 2024 13:44:29 +0200 Subject: [PATCH 26/30] ipv4transporthandle: use DeviceIdentity type + fixes `mypy` errors --- src/seabreeze/pyseabreeze/transport.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/seabreeze/pyseabreeze/transport.py b/src/seabreeze/pyseabreeze/transport.py index 8c7a3692..4ce7962a 100644 --- a/src/seabreeze/pyseabreeze/transport.py +++ b/src/seabreeze/pyseabreeze/transport.py @@ -393,7 +393,12 @@ def __init__(self, sock: socket.socket) -> None: address, port = sock.getpeername() except OSError: address, port = None, None - self.identity: tuple[str, int] = (address, port) + self.identity: DeviceIdentity = ( + int(ipaddress.IPv4Address(address)), + port, + 0, + 0, + ) # register callback to close socket on garbage collection self._finalizer = weakref.finalize(self, self.socket.close) @@ -608,7 +613,12 @@ def supported_model(cls, device: IPv4TransportHandle) -> str | None: """ if not isinstance(device, IPv4TransportHandle): return None - return cls.devices_ip_port[device.identity] + return cls.devices_ip_port[ + # IP address + (str(ipaddress.IPv4Address(device.identity[0]))), + # port + device.identity[1], + ] @classmethod def specialize(cls, model_name: str, **kwargs: Any) -> type[IPv4Transport]: From 001cea2e9c46b746d8296898803124a06a4a6e1f Mon Sep 17 00:00:00 2001 From: Hanno Perrey Date: Mon, 3 Jun 2024 16:02:58 +0200 Subject: [PATCH 27/30] devices: fix mypy error fixes `src/seabreeze/pyseabreeze/devices.py:317: error: "ABCMeta" has no attribute "supported_model" [attr-defined]` --- src/seabreeze/pyseabreeze/devices.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/seabreeze/pyseabreeze/devices.py b/src/seabreeze/pyseabreeze/devices.py index 04291ffa..e41deba5 100644 --- a/src/seabreeze/pyseabreeze/devices.py +++ b/src/seabreeze/pyseabreeze/devices.py @@ -313,7 +313,11 @@ def __new__(cls: type[DT], raw_device: Any = None) -> SeaBreezeDevice: raise SeaBreezeError( "Don't instantiate SeaBreezeDevice directly. Use `SeabreezeAPI.list_devices()`." ) - for transport in {USBTransport, IPv4Transport}: + transports: list[type[PySeaBreezeTransport[Any]]] = [ + IPv4Transport, + USBTransport, + ] + for transport in transports: supported_model = transport.supported_model(raw_device) if supported_model is not None: break From 84405aacd3c63837198bf97be17b12815ac29f92 Mon Sep 17 00:00:00 2001 From: Hanno Perrey Date: Mon, 3 Jun 2024 16:07:13 +0200 Subject: [PATCH 28/30] ipv4transport: ignore known devices protects against repeated runs of `list_devices` failing --- src/seabreeze/pyseabreeze/transport.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/seabreeze/pyseabreeze/transport.py b/src/seabreeze/pyseabreeze/transport.py index 4ce7962a..22cb18d0 100644 --- a/src/seabreeze/pyseabreeze/transport.py +++ b/src/seabreeze/pyseabreeze/transport.py @@ -568,11 +568,15 @@ def list_devices(cls, **kwargs: Any) -> Iterable[IPv4TransportHandle]: # use known product ids of the USB transport to look up the model name vid = 0x2457 # Ocean vendor ID model = USBTransport.vendor_product_ids[(vid, pid)] - cls.register_model( - model_name=model, - ipv4_address=server[0], - ipv4_port=server[1], - ) + try: + cls.register_model( + model_name=model, + ipv4_address=server[0], + ipv4_port=server[1], + ) + except ValueError: + # device already known + pass # connect to discovered and registered devices dev_sockets = [] From 81e12426fd7506bfbe61423b51dc5b5bdec07b9c Mon Sep 17 00:00:00 2001 From: Hanno Perrey Date: Tue, 4 Jun 2024 07:24:12 +0200 Subject: [PATCH 29/30] ipv4: handle allows reconnect - change argument to handle from socket to ip+port - manage opening socket in handle - close socket in `list_devices` --- src/seabreeze/pyseabreeze/api.py | 7 ++-- src/seabreeze/pyseabreeze/transport.py | 46 ++++++++++++-------------- 2 files changed, 25 insertions(+), 28 deletions(-) diff --git a/src/seabreeze/pyseabreeze/api.py b/src/seabreeze/pyseabreeze/api.py index 942853b9..dc5937e2 100644 --- a/src/seabreeze/pyseabreeze/api.py +++ b/src/seabreeze/pyseabreeze/api.py @@ -141,11 +141,10 @@ def list_devices(self) -> list[_SeaBreezeDevice]: # opening the device will populate its serial number try: dev.open() - except Exception: - # TODO specify expected exception + except RuntimeError: raise - # else: - # dev.close() + else: + dev.close() devices.append(dev) # type: ignore return devices diff --git a/src/seabreeze/pyseabreeze/transport.py b/src/seabreeze/pyseabreeze/transport.py index 22cb18d0..43928e40 100644 --- a/src/seabreeze/pyseabreeze/transport.py +++ b/src/seabreeze/pyseabreeze/transport.py @@ -380,19 +380,14 @@ def get_name_from_pyusb_backend(backend: usb.backend.IBackend) -> str | None: # this can and should be opaque to pyseabreeze class IPv4TransportHandle: - def __init__(self, sock: socket.socket) -> None: + def __init__(self, address: str, port: int) -> None: """encapsulation for IPv4 socket classes Parameters ---------- """ - self.socket: socket.socket = sock - # TODO check if socket is connected and get address via socket (socket.getpeername()) - try: - address, port = sock.getpeername() - except OSError: - address, port = None, None + self.socket: socket.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.identity: DeviceIdentity = ( int(ipaddress.IPv4Address(address)), port, @@ -402,6 +397,14 @@ def __init__(self, sock: socket.socket) -> None: # register callback to close socket on garbage collection self._finalizer = weakref.finalize(self, self.socket.close) + def open(self) -> None: + # create a new socket; if we closed it, it will have lost its file descriptor + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + self.socket.connect(self.get_address()) + except OSError as e: + raise RuntimeError(f"Could not connect to {self.get_address()}: {e}") + def close(self) -> None: self._finalizer() @@ -409,6 +412,15 @@ def close(self) -> None: def closed(self) -> bool: return not self._finalizer.alive + def get_address(self) -> tuple[str, int]: + """Return a touple consisting of the ip address and the port.""" + return ( + # IP address + (str(ipaddress.IPv4Address(self.identity[0]))), + # port + self.identity[1], + ) + class IPv4Transport(PySeaBreezeTransport[IPv4TransportHandle]): """implementation of the IPv4 socket transport interface for spectrometers""" @@ -434,8 +446,8 @@ def __init__( def open_device(self, device: IPv4TransportHandle) -> None: if not isinstance(device, IPv4TransportHandle): raise TypeError("device needs to be an IPv4TransportHandle") - # TODO handle possible exceptions self._device = device + self._device.open() self._opened = True # This will initialize the communication protocol @@ -579,17 +591,8 @@ def list_devices(cls, **kwargs: Any) -> Iterable[IPv4TransportHandle]: pass # connect to discovered and registered devices - dev_sockets = [] for address in cls.devices_ip_port: - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - try: - sock.connect(address) - except OSError as e: - raise RuntimeError(f"Could not connect to {address}: {e}") - else: - dev_sockets.append(sock) - for sock in dev_sockets: - yield IPv4TransportHandle(sock) + yield IPv4TransportHandle(*address) @classmethod def register_model(cls, model_name: str, **kwargs: Any) -> None: @@ -617,12 +620,7 @@ def supported_model(cls, device: IPv4TransportHandle) -> str | None: """ if not isinstance(device, IPv4TransportHandle): return None - return cls.devices_ip_port[ - # IP address - (str(ipaddress.IPv4Address(device.identity[0]))), - # port - device.identity[1], - ] + return cls.devices_ip_port[device.get_address()] @classmethod def specialize(cls, model_name: str, **kwargs: Any) -> type[IPv4Transport]: From 0e5a59ba0cbceabdb9074c7d279023eed515bce8 Mon Sep 17 00:00:00 2001 From: Hanno Perrey Date: Tue, 4 Jun 2024 12:37:08 +0200 Subject: [PATCH 30/30] pass network_adapter kwarg to backend --- src/seabreeze/backends.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/seabreeze/backends.py b/src/seabreeze/backends.py index a0ca6a45..c0e3d49b 100644 --- a/src/seabreeze/backends.py +++ b/src/seabreeze/backends.py @@ -61,6 +61,9 @@ def use( if "pyusb_backend" in kwargs: pyusb_backend = kwargs.pop("pyusb_backend") BackendConfig.api_kwargs["pyusb_backend"] = pyusb_backend + if "network_adapter" in kwargs: + network_adapter = kwargs.pop("network_adapter") + BackendConfig.api_kwargs["network_adapter"] = network_adapter if kwargs: raise TypeError( f"unknown keyword arguments {set(kwargs)!r} for backend {backend!r}"