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")