From b335bbda21e004de3ac14db6ccded14331494851 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 13:35:23 +0000 Subject: [PATCH 01/11] Update gs_usb driver to support WinUSB by not forcing libusb1 backend Replace GsUsb.scan() and GsUsb.find() calls with local helper functions that call usb.core.find() without specifying a backend, allowing pyusb to auto-detect the best available backend. This enables WinUSB support on Windows in addition to libusbK. Update documentation to reflect WinUSB support and add unit tests. Co-authored-by: BenGardiner <243321+BenGardiner@users.noreply.github.com> --- can/interfaces/gs_usb.py | 42 ++++++++++++++-- doc/interfaces/gs_usb.rst | 6 ++- test/test_interface_gs_usb.py | 90 +++++++++++++++++++++++++++++++++++ 3 files changed, 133 insertions(+), 5 deletions(-) create mode 100644 test/test_interface_gs_usb.py diff --git a/can/interfaces/gs_usb.py b/can/interfaces/gs_usb.py index 6297fc1f5..3b1dee606 100644 --- a/can/interfaces/gs_usb.py +++ b/can/interfaces/gs_usb.py @@ -12,6 +12,42 @@ logger = logging.getLogger(__name__) +def _scan_gs_usb_devices() -> list[GsUsb]: + """Scan for gs_usb devices using auto-detected backend. + + Unlike :meth:`GsUsb.scan`, this does not force the ``libusb1`` backend, + allowing ``pyusb`` to auto-detect the best available backend. This enables + support for WinUSB on Windows in addition to libusbK. + """ + return [ + GsUsb(dev) + for dev in ( + usb.core.find( + find_all=True, + custom_match=GsUsb.is_gs_usb_device, + ) + or [] + ) + ] + + +def _find_gs_usb_device(bus: int, address: int) -> GsUsb | None: + """Find a specific gs_usb device using auto-detected backend. + + Unlike :meth:`GsUsb.find`, this does not force the ``libusb1`` backend, + allowing ``pyusb`` to auto-detect the best available backend. This enables + support for WinUSB on Windows in addition to libusbK. + """ + dev = usb.core.find( + custom_match=GsUsb.is_gs_usb_device, + bus=bus, + address=address, + ) + if dev: + return GsUsb(dev) + return None + + class GsUsbBus(can.BusABC): def __init__( self, @@ -43,7 +79,7 @@ def __init__( self._index = None if index is not None: - devs = GsUsb.scan() + devs = _scan_gs_usb_devices() if len(devs) <= index: raise CanInitializationError( f"Cannot find device {index}. Devices found: {len(devs)}" @@ -51,7 +87,7 @@ def __init__( gs_usb = devs[index] self._index = index else: - gs_usb = GsUsb.find(bus=bus, address=address) + gs_usb = _find_gs_usb_device(bus=bus, address=address) if not gs_usb: raise CanInitializationError(f"Cannot find device {channel}") @@ -166,7 +202,7 @@ def shutdown(self): if self._index is not None: # Avoid errors on subsequent __init() by repeating the .scan() and .start() that would otherwise fail # the next time the device is opened in __init__() - devs = GsUsb.scan() + devs = _scan_gs_usb_devices() if self._index < len(devs): gs_usb = devs[self._index] try: diff --git a/doc/interfaces/gs_usb.rst b/doc/interfaces/gs_usb.rst index 8bab07c6f..580a994fc 100755 --- a/doc/interfaces/gs_usb.rst +++ b/doc/interfaces/gs_usb.rst @@ -52,8 +52,10 @@ Windows, Linux and Mac. The backend driver depends on `pyusb `_ so a ``pyusb`` backend driver library such as ``libusb`` must be installed. - On Windows a tool such as `Zadig `_ can be used to set the USB device driver to - ``libusbK``. + On Windows, WinUSB and libusbK are both supported. Devices with WCID (Windows Compatible ID) descriptors, + such as candleLight firmware, will automatically use WinUSB without any additional driver installation. + Alternatively, a tool such as `Zadig `_ can be used to set the USB device driver to + either ``WinUSB`` or ``libusbK``. Supplementary Info diff --git a/test/test_interface_gs_usb.py b/test/test_interface_gs_usb.py new file mode 100644 index 000000000..8aea1a28e --- /dev/null +++ b/test/test_interface_gs_usb.py @@ -0,0 +1,90 @@ +"""Tests for the gs_usb interface.""" + +from unittest.mock import MagicMock, patch + +import pytest + +from can.interfaces.gs_usb import ( + GsUsbBus, + _find_gs_usb_device, + _scan_gs_usb_devices, +) + + +@patch("can.interfaces.gs_usb.usb.core.find") +def test_scan_does_not_force_backend(mock_find): + """Verify that _scan_gs_usb_devices does not pass a backend argument, + allowing pyusb to auto-detect the best available backend (WinUSB, libusbK, etc.).""" + mock_find.return_value = [] + + _scan_gs_usb_devices() + + mock_find.assert_called_once() + call_kwargs = mock_find.call_args[1] + assert "backend" not in call_kwargs, ( + "backend should not be specified so pyusb can auto-detect" + ) + assert call_kwargs["find_all"] is True + + +@patch("can.interfaces.gs_usb.usb.core.find") +def test_find_does_not_force_backend(mock_find): + """Verify that _find_gs_usb_device does not pass a backend argument, + allowing pyusb to auto-detect the best available backend (WinUSB, libusbK, etc.).""" + mock_find.return_value = None + + _find_gs_usb_device(bus=1, address=2) + + mock_find.assert_called_once() + call_kwargs = mock_find.call_args[1] + assert "backend" not in call_kwargs, ( + "backend should not be specified so pyusb can auto-detect" + ) + assert call_kwargs["bus"] == 1 + assert call_kwargs["address"] == 2 + + +@patch("can.interfaces.gs_usb.usb.core.find") +def test_scan_returns_gs_usb_devices(mock_find): + """Verify that _scan_gs_usb_devices wraps found USB devices in GsUsb objects.""" + mock_dev1 = MagicMock() + mock_dev2 = MagicMock() + mock_find.return_value = [mock_dev1, mock_dev2] + + devices = _scan_gs_usb_devices() + + assert len(devices) == 2 + assert devices[0].gs_usb is mock_dev1 + assert devices[1].gs_usb is mock_dev2 + + +@patch("can.interfaces.gs_usb.usb.core.find") +def test_find_returns_gs_usb_device(mock_find): + """Verify that _find_gs_usb_device wraps the found USB device in a GsUsb object.""" + mock_dev = MagicMock() + mock_find.return_value = mock_dev + + device = _find_gs_usb_device(bus=1, address=2) + + assert device is not None + assert device.gs_usb is mock_dev + + +@patch("can.interfaces.gs_usb.usb.core.find") +def test_find_returns_none_when_no_device(mock_find): + """Verify that _find_gs_usb_device returns None when no device is found.""" + mock_find.return_value = None + + device = _find_gs_usb_device(bus=1, address=2) + + assert device is None + + +@patch("can.interfaces.gs_usb.usb.core.find") +def test_scan_returns_empty_list_when_no_devices(mock_find): + """Verify that _scan_gs_usb_devices returns an empty list when no devices are found.""" + mock_find.return_value = [] + + devices = _scan_gs_usb_devices() + + assert devices == [] From eecde923084b617e2bbd5615547f768bfa2205c7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 14:50:30 +0000 Subject: [PATCH 02/11] Add pyusb as explicit dependency in gs-usb optional group The gs_usb interface directly imports `usb` (pyusb) for USB device discovery, so pyusb must be an explicit dependency rather than relying on it being a transitive dependency of the gs-usb package. Co-authored-by: BenGardiner <243321+BenGardiner@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9eb4a41cd..8ed86e611 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,7 +64,7 @@ neovi = ["filelock", "python-ics>=2.12"] canalystii = ["canalystii>=0.1.0"] cantact = ["cantact>=0.0.7"] cvector = ["python-can-cvector"] -gs-usb = ["gs-usb>=0.2.1"] +gs-usb = ["gs-usb>=0.2.1", "pyusb>=1.0.2"] nixnet = ["nixnet>=0.3.2"] pcan = ["uptime~=3.0.1"] remote = ["python-can-remote"] From 2408840284fc53dcc0b93293fe56dd2942f126ac Mon Sep 17 00:00:00 2001 From: Ben Gardiner Date: Mon, 23 Feb 2026 09:59:05 -0500 Subject: [PATCH 03/11] gs_usb: treat timeout=None as forever pass '0' when timeout=None (as proposed by @zariiii9003 in https://github.com/hardbyte/python-can/pull/2026#issuecomment-3941747658) --- can/interfaces/gs_usb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/interfaces/gs_usb.py b/can/interfaces/gs_usb.py index 3b1dee606..31fd663f8 100644 --- a/can/interfaces/gs_usb.py +++ b/can/interfaces/gs_usb.py @@ -175,7 +175,7 @@ def _recv_internal(self, timeout: float | None) -> tuple[can.Message | None, boo frame = GsUsbFrame() # Do not set timeout as None or zero here to avoid blocking - timeout_ms = round(timeout * 1000) if timeout else 1 + timeout_ms = round(timeout * 1000) if timeout else 0 if not self.gs_usb.read(frame=frame, timeout_ms=timeout_ms): return None, False From d7e2e3a4acc624dffa89b1b1484d407ae1521c38 Mon Sep 17 00:00:00 2001 From: Ben Gardiner Date: Mon, 23 Feb 2026 10:18:28 -0500 Subject: [PATCH 04/11] add news fragment --- doc/changelog.d/2031.changed.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 doc/changelog.d/2031.changed.md diff --git a/doc/changelog.d/2031.changed.md b/doc/changelog.d/2031.changed.md new file mode 100644 index 000000000..7d33a6c80 --- /dev/null +++ b/doc/changelog.d/2031.changed.md @@ -0,0 +1,2 @@ +* make gs_usb use WinUSB instead of requiring libusbK. +* gs_usb: timeout=None means foreever From 1e68110dffc867f1976b05b677752fd1ae2357ae Mon Sep 17 00:00:00 2001 From: Ben Gardiner Date: Mon, 23 Feb 2026 10:28:44 -0500 Subject: [PATCH 05/11] formatting fixes --- test/test_interface_gs_usb.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/test_interface_gs_usb.py b/test/test_interface_gs_usb.py index 8aea1a28e..b4ec76e5a 100644 --- a/test/test_interface_gs_usb.py +++ b/test/test_interface_gs_usb.py @@ -21,9 +21,9 @@ def test_scan_does_not_force_backend(mock_find): mock_find.assert_called_once() call_kwargs = mock_find.call_args[1] - assert "backend" not in call_kwargs, ( - "backend should not be specified so pyusb can auto-detect" - ) + assert ( + "backend" not in call_kwargs + ), "backend should not be specified so pyusb can auto-detect" assert call_kwargs["find_all"] is True @@ -37,9 +37,9 @@ def test_find_does_not_force_backend(mock_find): mock_find.assert_called_once() call_kwargs = mock_find.call_args[1] - assert "backend" not in call_kwargs, ( - "backend should not be specified so pyusb can auto-detect" - ) + assert ( + "backend" not in call_kwargs + ), "backend should not be specified so pyusb can auto-detect" assert call_kwargs["bus"] == 1 assert call_kwargs["address"] == 2 From f0cbee0b90b659801a05f9b50091ba5126003df8 Mon Sep 17 00:00:00 2001 From: Ben Gardiner Date: Mon, 23 Feb 2026 10:32:44 -0500 Subject: [PATCH 06/11] gs_usb module needed in tests deps now --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 8ed86e611..5fc07dc74 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -103,6 +103,7 @@ test = [ "coverage==7.13.*", "hypothesis==6.*", "parameterized==0.9.*", + "gs-usb==0.3.*", ] dev = [ {include-group = "docs"}, From 4367d9071a8e042b6133996876e15634c3a2918c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 15:46:29 +0000 Subject: [PATCH 07/11] Fix gs_usb shutdown to always call parent BusABC.shutdown() BusABC has a class-level _is_shutdown = True attribute. When __init__ was not called (as in test mocks), GsUsbBus.shutdown() resolved this class attribute and returned early, never calling super().shutdown(). Restructure shutdown() to always call super().shutdown(), using the pre-call _is_shutdown state only to guard interface-specific cleanup. Co-authored-by: BenGardiner <243321+BenGardiner@users.noreply.github.com> --- can/interfaces/gs_usb.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/can/interfaces/gs_usb.py b/can/interfaces/gs_usb.py index 31fd663f8..35de63668 100644 --- a/can/interfaces/gs_usb.py +++ b/can/interfaces/gs_usb.py @@ -68,7 +68,6 @@ def __init__( :param can_filters: not supported :param bitrate: CAN network bandwidth (bits/s) """ - self._is_shutdown = False if (index is not None) and ((bus or address) is not None): raise CanInitializationError( "index and bus/address cannot be used simultaneously" @@ -194,10 +193,11 @@ def _recv_internal(self, timeout: float | None) -> tuple[can.Message | None, boo return msg, False def shutdown(self): - if self._is_shutdown: + already_shutdown = self._is_shutdown + super().shutdown() + if already_shutdown: return - super().shutdown() self.gs_usb.stop() if self._index is not None: # Avoid errors on subsequent __init() by repeating the .scan() and .start() that would otherwise fail @@ -211,4 +211,3 @@ def shutdown(self): gs_usb.stop() except usb.core.USBError: pass - self._is_shutdown = True From 961893479bfd5bb432e66a46bde1cb5727737140 Mon Sep 17 00:00:00 2001 From: Ben Gardiner Date: Tue, 3 Mar 2026 22:53:06 -0500 Subject: [PATCH 08/11] note pyusb not WinUSB in news and news frags are a sentence not a list (@zariiii9003) --- doc/changelog.d/2031.changed.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/changelog.d/2031.changed.md b/doc/changelog.d/2031.changed.md index 7d33a6c80..9d6bf79aa 100644 --- a/doc/changelog.d/2031.changed.md +++ b/doc/changelog.d/2031.changed.md @@ -1,2 +1 @@ -* make gs_usb use WinUSB instead of requiring libusbK. -* gs_usb: timeout=None means foreever +make gs_usb use pyusb (allows WinUSB instead of requiring libusbK on windows) also timeout=None means foreever From ea01de5870a594282543920252509a6b02c510a4 Mon Sep 17 00:00:00 2001 From: Ben Gardiner Date: Tue, 3 Mar 2026 22:57:01 -0500 Subject: [PATCH 09/11] put gs-usb dep into tox.ini (@zariiii9003) --- pyproject.toml | 1 - tox.ini | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5fc07dc74..8ed86e611 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -103,7 +103,6 @@ test = [ "coverage==7.13.*", "hypothesis==6.*", "parameterized==0.9.*", - "gs-usb==0.3.*", ] dev = [ {include-group = "docs"}, diff --git a/tox.ini b/tox.ini index e635ec000..2e695f9e4 100644 --- a/tox.ini +++ b/tox.ini @@ -14,6 +14,7 @@ dependency_groups = test extras = canalystii + gs-usb mf4 multicast pywin32 @@ -27,6 +28,8 @@ extras = canalystii mf4 multicast + gs-usb + serial pywin32 serial # still no windows-curses for py314 @@ -34,6 +37,7 @@ extras = [testenv:{py313t,py314t,pypy310,pypy311}] extras = canalystii + gs-usb serial [testenv:docs] From 75fe6190556a02107bb970f1fe5925b808e00247 Mon Sep 17 00:00:00 2001 From: Ben Gardiner Date: Tue, 3 Mar 2026 23:45:06 -0500 Subject: [PATCH 10/11] combine _scan_gs_usb_devices() and _find_gs_usb_device() (@zariiii9003) --- can/interfaces/gs_usb.py | 91 +++++++++++++++++++---------------- test/test_interface_gs_usb.py | 53 ++++++-------------- 2 files changed, 65 insertions(+), 79 deletions(-) diff --git a/can/interfaces/gs_usb.py b/can/interfaces/gs_usb.py index 35de63668..b9516dade 100644 --- a/can/interfaces/gs_usb.py +++ b/can/interfaces/gs_usb.py @@ -1,4 +1,5 @@ import logging +from typing import Any import usb from gs_usb.constants import CAN_EFF_FLAG, CAN_ERR_FLAG, CAN_MAX_DLC, CAN_RTR_FLAG @@ -12,53 +13,49 @@ logger = logging.getLogger(__name__) -def _scan_gs_usb_devices() -> list[GsUsb]: - """Scan for gs_usb devices using auto-detected backend. +def _find_gs_usb_devices( + bus: int | None = None, address: int | None = None +) -> list[GsUsb]: + """Find gs_usb devices using auto-detected backend. Unlike :meth:`GsUsb.scan`, this does not force the ``libusb1`` backend, allowing ``pyusb`` to auto-detect the best available backend. This enables support for WinUSB on Windows in addition to libusbK. + + :param bus: number of the bus that the device is connected to + :param address: address of the device on the bus it is connected to + :return: a list of found GsUsb devices """ + kwargs = {} + if bus is not None: + kwargs["bus"] = bus + if address is not None: + kwargs["address"] = address + return [ GsUsb(dev) for dev in ( usb.core.find( find_all=True, custom_match=GsUsb.is_gs_usb_device, + **kwargs, ) or [] ) ] -def _find_gs_usb_device(bus: int, address: int) -> GsUsb | None: - """Find a specific gs_usb device using auto-detected backend. - - Unlike :meth:`GsUsb.find`, this does not force the ``libusb1`` backend, - allowing ``pyusb`` to auto-detect the best available backend. This enables - support for WinUSB on Windows in addition to libusbK. - """ - dev = usb.core.find( - custom_match=GsUsb.is_gs_usb_device, - bus=bus, - address=address, - ) - if dev: - return GsUsb(dev) - return None - - class GsUsbBus(can.BusABC): def __init__( self, - channel, + channel: can.typechecking.Channel, bitrate: int = 500_000, - index=None, - bus=None, - address=None, - can_filters=None, - **kwargs, - ): + index: int | None = None, + bus: int | None = None, + address: int | None = None, + can_filters: can.typechecking.CanFilters | None = None, + **kwargs: Any, + ) -> None: """ :param channel: usb device name :param index: device number if using automatic scan, starting from 0. @@ -74,24 +71,35 @@ def __init__( ) if index is None and address is None and bus is None: - index = channel + _index: Any = channel + else: + _index = index - self._index = None - if index is not None: - devs = _scan_gs_usb_devices() - if len(devs) <= index: + self._index: int | None = None + if _index is not None: + if not isinstance(_index, int): + try: + _index = int(_index) + except (ValueError, TypeError): + raise CanInitializationError( + f"index must be an integer, but got {type(_index).__name__} ({_index})" + ) from None + + devs = _find_gs_usb_devices() + if len(devs) <= _index: raise CanInitializationError( - f"Cannot find device {index}. Devices found: {len(devs)}" + f"Cannot find device {_index}. Devices found: {len(devs)}" ) - gs_usb = devs[index] - self._index = index + gs_usb = devs[_index] + self._index = _index else: - gs_usb = _find_gs_usb_device(bus=bus, address=address) - if not gs_usb: + devs = _find_gs_usb_devices(bus=bus, address=address) + if not devs: raise CanInitializationError(f"Cannot find device {channel}") + gs_usb = devs[0] self.gs_usb = gs_usb - self.channel_info = channel + self.channel_info = str(channel) self._can_protocol = can.CanProtocol.CAN_20 bit_timing = can.BitTiming.from_sample_point( @@ -116,7 +124,7 @@ def __init__( **kwargs, ) - def send(self, msg: can.Message, timeout: float | None = None): + def send(self, msg: can.Message, timeout: float | None = None) -> None: """Transmit a message to the CAN bus. :param Message msg: A message object. @@ -200,9 +208,10 @@ def shutdown(self): self.gs_usb.stop() if self._index is not None: - # Avoid errors on subsequent __init() by repeating the .scan() and .start() that would otherwise fail - # the next time the device is opened in __init__() - devs = _scan_gs_usb_devices() + # Avoid errors on subsequent __init() by repeating the .scan() and + # .start() that would otherwise fail the next time the device is + # opened in __init__() + devs = _find_gs_usb_devices() if self._index < len(devs): gs_usb = devs[self._index] try: diff --git a/test/test_interface_gs_usb.py b/test/test_interface_gs_usb.py index b4ec76e5a..62801809f 100644 --- a/test/test_interface_gs_usb.py +++ b/test/test_interface_gs_usb.py @@ -6,18 +6,17 @@ from can.interfaces.gs_usb import ( GsUsbBus, - _find_gs_usb_device, - _scan_gs_usb_devices, + _find_gs_usb_devices, ) @patch("can.interfaces.gs_usb.usb.core.find") -def test_scan_does_not_force_backend(mock_find): - """Verify that _scan_gs_usb_devices does not pass a backend argument, +def test_find_devices_does_not_force_backend(mock_find): + """Verify that _find_gs_usb_devices does not pass a backend argument, allowing pyusb to auto-detect the best available backend (WinUSB, libusbK, etc.).""" mock_find.return_value = [] - _scan_gs_usb_devices() + _find_gs_usb_devices() mock_find.assert_called_once() call_kwargs = mock_find.call_args[1] @@ -28,12 +27,11 @@ def test_scan_does_not_force_backend(mock_find): @patch("can.interfaces.gs_usb.usb.core.find") -def test_find_does_not_force_backend(mock_find): - """Verify that _find_gs_usb_device does not pass a backend argument, - allowing pyusb to auto-detect the best available backend (WinUSB, libusbK, etc.).""" - mock_find.return_value = None +def test_find_devices_with_args_does_not_force_backend(mock_find): + """Verify that _find_gs_usb_devices with bus/address does not pass a backend argument.""" + mock_find.return_value = [] - _find_gs_usb_device(bus=1, address=2) + _find_gs_usb_devices(bus=1, address=2) mock_find.assert_called_once() call_kwargs = mock_find.call_args[1] @@ -42,16 +40,17 @@ def test_find_does_not_force_backend(mock_find): ), "backend should not be specified so pyusb can auto-detect" assert call_kwargs["bus"] == 1 assert call_kwargs["address"] == 2 + assert call_kwargs["find_all"] is True @patch("can.interfaces.gs_usb.usb.core.find") -def test_scan_returns_gs_usb_devices(mock_find): - """Verify that _scan_gs_usb_devices wraps found USB devices in GsUsb objects.""" +def test_find_devices_returns_gs_usb_devices(mock_find): + """Verify that _find_gs_usb_devices wraps found USB devices in GsUsb objects.""" mock_dev1 = MagicMock() mock_dev2 = MagicMock() mock_find.return_value = [mock_dev1, mock_dev2] - devices = _scan_gs_usb_devices() + devices = _find_gs_usb_devices() assert len(devices) == 2 assert devices[0].gs_usb is mock_dev1 @@ -59,32 +58,10 @@ def test_scan_returns_gs_usb_devices(mock_find): @patch("can.interfaces.gs_usb.usb.core.find") -def test_find_returns_gs_usb_device(mock_find): - """Verify that _find_gs_usb_device wraps the found USB device in a GsUsb object.""" - mock_dev = MagicMock() - mock_find.return_value = mock_dev - - device = _find_gs_usb_device(bus=1, address=2) - - assert device is not None - assert device.gs_usb is mock_dev - - -@patch("can.interfaces.gs_usb.usb.core.find") -def test_find_returns_none_when_no_device(mock_find): - """Verify that _find_gs_usb_device returns None when no device is found.""" - mock_find.return_value = None - - device = _find_gs_usb_device(bus=1, address=2) - - assert device is None - - -@patch("can.interfaces.gs_usb.usb.core.find") -def test_scan_returns_empty_list_when_no_devices(mock_find): - """Verify that _scan_gs_usb_devices returns an empty list when no devices are found.""" +def test_find_devices_returns_empty_list_when_no_devices(mock_find): + """Verify that _find_gs_usb_devices returns an empty list when no devices are found.""" mock_find.return_value = [] - devices = _scan_gs_usb_devices() + devices = _find_gs_usb_devices() assert devices == [] From 3e5b1c062ea6166f19680d64b0c5e68736aaef2d Mon Sep 17 00:00:00 2001 From: Ben Gardiner Date: Tue, 3 Mar 2026 23:59:41 -0500 Subject: [PATCH 11/11] don't instantiate a GsUsb device for every one detected (@zariiii9003) --- can/interfaces/gs_usb.py | 31 ++++++++++++++----------------- test/test_interface_gs_usb.py | 8 ++++---- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/can/interfaces/gs_usb.py b/can/interfaces/gs_usb.py index b9516dade..2169ded7a 100644 --- a/can/interfaces/gs_usb.py +++ b/can/interfaces/gs_usb.py @@ -15,8 +15,8 @@ def _find_gs_usb_devices( bus: int | None = None, address: int | None = None -) -> list[GsUsb]: - """Find gs_usb devices using auto-detected backend. +) -> list[usb.core.Device]: + """Find raw USB devices for gs_usb using auto-detected backend. Unlike :meth:`GsUsb.scan`, this does not force the ``libusb1`` backend, allowing ``pyusb`` to auto-detect the best available backend. This enables @@ -24,7 +24,7 @@ def _find_gs_usb_devices( :param bus: number of the bus that the device is connected to :param address: address of the device on the bus it is connected to - :return: a list of found GsUsb devices + :return: a list of found raw USB devices """ kwargs = {} if bus is not None: @@ -32,17 +32,14 @@ def _find_gs_usb_devices( if address is not None: kwargs["address"] = address - return [ - GsUsb(dev) - for dev in ( - usb.core.find( - find_all=True, - custom_match=GsUsb.is_gs_usb_device, - **kwargs, - ) - or [] + return list( + usb.core.find( + find_all=True, + custom_match=GsUsb.is_gs_usb_device, + **kwargs, ) - ] + or [] + ) class GsUsbBus(can.BusABC): @@ -90,15 +87,15 @@ def __init__( raise CanInitializationError( f"Cannot find device {_index}. Devices found: {len(devs)}" ) - gs_usb = devs[_index] + gs_usb_dev = devs[_index] self._index = _index else: devs = _find_gs_usb_devices(bus=bus, address=address) if not devs: raise CanInitializationError(f"Cannot find device {channel}") - gs_usb = devs[0] + gs_usb_dev = devs[0] - self.gs_usb = gs_usb + self.gs_usb = GsUsb(gs_usb_dev) self.channel_info = str(channel) self._can_protocol = can.CanProtocol.CAN_20 @@ -213,7 +210,7 @@ def shutdown(self): # opened in __init__() devs = _find_gs_usb_devices() if self._index < len(devs): - gs_usb = devs[self._index] + gs_usb = GsUsb(devs[self._index]) try: gs_usb.set_bitrate(self._bitrate) gs_usb.start() diff --git a/test/test_interface_gs_usb.py b/test/test_interface_gs_usb.py index 62801809f..9f0e35534 100644 --- a/test/test_interface_gs_usb.py +++ b/test/test_interface_gs_usb.py @@ -44,8 +44,8 @@ def test_find_devices_with_args_does_not_force_backend(mock_find): @patch("can.interfaces.gs_usb.usb.core.find") -def test_find_devices_returns_gs_usb_devices(mock_find): - """Verify that _find_gs_usb_devices wraps found USB devices in GsUsb objects.""" +def test_find_devices_returns_raw_usb_devices(mock_find): + """Verify that _find_gs_usb_devices returns the raw USB devices.""" mock_dev1 = MagicMock() mock_dev2 = MagicMock() mock_find.return_value = [mock_dev1, mock_dev2] @@ -53,8 +53,8 @@ def test_find_devices_returns_gs_usb_devices(mock_find): devices = _find_gs_usb_devices() assert len(devices) == 2 - assert devices[0].gs_usb is mock_dev1 - assert devices[1].gs_usb is mock_dev2 + assert devices[0] is mock_dev1 + assert devices[1] is mock_dev2 @patch("can.interfaces.gs_usb.usb.core.find")