Skip to content

Commit 811e2eb

Browse files
authored
Update PythonCANMedia Class (#371)
This PR bundles three changes: - the python-can [virtual](https://python-can.readthedocs.io/en/stable/interfaces/virtual.html) interface can now be used with multiple distinct channels at once, instead of only the default channel - type hints in the different `_construct_*()` functions have been fixed - the stub function `PythonCANMedia.list_available_interface_names()` has been implemented, utilizing [`can.detect_available_configs()`](https://python-can.readthedocs.io/en/stable/utils.html#can.detect_available_configs)
1 parent d0f68e6 commit 811e2eb

File tree

3 files changed

+35
-22
lines changed

3 files changed

+35
-22
lines changed

pycyphal/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "1.24.4"
1+
__version__ = "1.24.5"

pycyphal/transport/can/media/pythoncan/_pythoncan.py

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,8 @@ def __init__(
105105
Example: ``pcan:PCAN_USBBUS1``
106106
107107
- Interface ``virtual`` is described in https://python-can.readthedocs.io/en/master/interfaces/virtual.html.
108-
The channel name should be empty.
109-
Example: ``virtual:``
108+
The channel name may be empty.
109+
Example: ``virtual:``, ``virtual:foo-can``
110110
111111
- Interface ``usb2can`` is described in https://python-can.readthedocs.io/en/stable/interfaces/usb2can.html.
112112
Example: ``usb2can:ED000100``
@@ -348,9 +348,17 @@ def close(self) -> None:
348348
@staticmethod
349349
def list_available_interface_names() -> typing.Iterable[str]:
350350
"""
351-
Returns an empty list. TODO: provide minimally functional implementation.
351+
Returns a list of available interfaces.
352352
"""
353-
return []
353+
available_configs: typing.List[can.typechecking.AutoDetectedConfig] = []
354+
for interface in _CONSTRUCTORS.keys():
355+
# try each interface on its own to catch errors if the interface library is not available
356+
try:
357+
available_configs.extend(can.detect_available_configs(interfaces=[interface]))
358+
except NotImplementedError:
359+
_logger.debug("%s: Interface not supported", interface)
360+
continue
361+
return [f"{config['interface']}:{config['channel']}" for config in available_configs]
354362

355363
def _invoke_rx_handler(self, frs: typing.List[typing.Tuple[Timestamp, Envelope]]) -> None:
356364
try:
@@ -425,7 +433,7 @@ class _FDInterfaceParameters(_InterfaceParameters):
425433
bitrate: typing.Tuple[int, int]
426434

427435

428-
def _construct_socketcan(parameters: _InterfaceParameters) -> can.ThreadSafeBus:
436+
def _construct_socketcan(parameters: _InterfaceParameters) -> typing.Tuple[PythonCANBusOptions, can.ThreadSafeBus]:
429437
if isinstance(parameters, _ClassicInterfaceParameters):
430438
return (
431439
PythonCANBusOptions(),
@@ -439,7 +447,7 @@ def _construct_socketcan(parameters: _InterfaceParameters) -> can.ThreadSafeBus:
439447
assert False, "Internal error"
440448

441449

442-
def _construct_kvaser(parameters: _InterfaceParameters) -> can.ThreadSafeBus:
450+
def _construct_kvaser(parameters: _InterfaceParameters) -> typing.Tuple[PythonCANBusOptions, can.ThreadSafeBus]:
443451
if isinstance(parameters, _ClassicInterfaceParameters):
444452
return (
445453
PythonCANBusOptions(),
@@ -464,7 +472,7 @@ def _construct_kvaser(parameters: _InterfaceParameters) -> can.ThreadSafeBus:
464472
assert False, "Internal error"
465473

466474

467-
def _construct_slcan(parameters: _InterfaceParameters) -> can.ThreadSafeBus:
475+
def _construct_slcan(parameters: _InterfaceParameters) -> typing.Tuple[PythonCANBusOptions, can.ThreadSafeBus]:
468476
if isinstance(parameters, _ClassicInterfaceParameters):
469477
return (
470478
PythonCANBusOptions(),
@@ -479,7 +487,7 @@ def _construct_slcan(parameters: _InterfaceParameters) -> can.ThreadSafeBus:
479487
assert False, "Internal error"
480488

481489

482-
def _construct_pcan(parameters: _InterfaceParameters) -> can.ThreadSafeBus:
490+
def _construct_pcan(parameters: _InterfaceParameters) -> typing.Tuple[PythonCANBusOptions, can.ThreadSafeBus]:
483491
if isinstance(parameters, _ClassicInterfaceParameters):
484492
return (
485493
PythonCANBusOptions(),
@@ -514,18 +522,14 @@ def _construct_pcan(parameters: _InterfaceParameters) -> can.ThreadSafeBus:
514522
assert False, "Internal error"
515523

516524

517-
def _construct_virtual(parameters: _InterfaceParameters) -> can.ThreadSafeBus:
518-
if isinstance(parameters, _ClassicInterfaceParameters):
519-
return (
520-
PythonCANBusOptions(),
521-
can.ThreadSafeBus(interface=parameters.interface_name, bitrate=parameters.bitrate),
522-
)
523-
if isinstance(parameters, _FDInterfaceParameters):
524-
return (PythonCANBusOptions(), can.ThreadSafeBus(interface=parameters.interface_name))
525-
assert False, "Internal error"
525+
def _construct_virtual(parameters: _InterfaceParameters) -> typing.Tuple[PythonCANBusOptions, can.ThreadSafeBus]:
526+
return (
527+
PythonCANBusOptions(),
528+
can.ThreadSafeBus(interface=parameters.interface_name, channel=parameters.channel_name),
529+
)
526530

527531

528-
def _construct_usb2can(parameters: _InterfaceParameters) -> can.ThreadSafeBus:
532+
def _construct_usb2can(parameters: _InterfaceParameters) -> typing.Tuple[PythonCANBusOptions, can.ThreadSafeBus]:
529533
if isinstance(parameters, _ClassicInterfaceParameters):
530534
return (
531535
PythonCANBusOptions(),
@@ -540,7 +544,7 @@ def _construct_usb2can(parameters: _InterfaceParameters) -> can.ThreadSafeBus:
540544
assert False, "Internal error"
541545

542546

543-
def _construct_canalystii(parameters: _InterfaceParameters) -> can.ThreadSafeBus:
547+
def _construct_canalystii(parameters: _InterfaceParameters) -> typing.Tuple[PythonCANBusOptions, can.ThreadSafeBus]:
544548
if isinstance(parameters, _ClassicInterfaceParameters):
545549
return (
546550
PythonCANBusOptions(),
@@ -553,7 +557,7 @@ def _construct_canalystii(parameters: _InterfaceParameters) -> can.ThreadSafeBus
553557
assert False, "Internal error"
554558

555559

556-
def _construct_seeedstudio(parameters: _InterfaceParameters) -> can.ThreadSafeBus:
560+
def _construct_seeedstudio(parameters: _InterfaceParameters) -> typing.Tuple[PythonCANBusOptions, can.ThreadSafeBus]:
557561
if isinstance(parameters, _ClassicInterfaceParameters):
558562
return (
559563
PythonCANBusOptions(),
@@ -568,7 +572,7 @@ def _construct_seeedstudio(parameters: _InterfaceParameters) -> can.ThreadSafeBu
568572
assert False, "Internal error"
569573

570574

571-
def _construct_gs_usb(parameters: _InterfaceParameters) -> can.ThreadSafeBus:
575+
def _construct_gs_usb(parameters: _InterfaceParameters) -> typing.Tuple[PythonCANBusOptions, can.ThreadSafeBus]:
572576
if isinstance(parameters, _ClassicInterfaceParameters):
573577
try:
574578
index = int(parameters.channel_name)

tests/transport/can/media/_pythoncan.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,15 @@ def _unittest_can_pythoncan_iface_name() -> None:
167167
media.close()
168168

169169

170+
def _unittest_can_pythoncan_list_iface_names() -> None:
171+
available_iface_names = list(PythonCANMedia.list_available_interface_names())
172+
assert len(available_iface_names) > 0
173+
# https://python-can.readthedocs.io/en/stable/interfaces/virtual.html#can.interfaces.virtual.VirtualBus._detect_available_configs
174+
assert any(
175+
name.startswith("virtual:") for name in available_iface_names
176+
), "At least one virtual interface should be available"
177+
178+
170179
def _unittest_can_pythoncan_errors() -> None:
171180
with pytest.raises(InvalidMediaConfigurationError, match=r".*interface:channel.*"):
172181
PythonCANMedia("malformed_name", 1_000_000)

0 commit comments

Comments
 (0)