Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ jobs:
- {ref: "2026.7.4", api: "1.14"}
- {ref: "2026.8.2", api: "1.15"}
- {ref: "2026.9.0", api: "1.16"}
- {ref: "13c27429a4d54d1b2ff9fd611f9baafdc3e5f37a", api: "1.17"}
- {ref: "dcb943ded39dc1a7ec470dbfa7f461fc5041ab65", api: "1.19"}
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
Expand Down Expand Up @@ -410,7 +410,7 @@ jobs:
- {ref: "2026.7.4", api: "1.14"}
- {ref: "2026.8.2", api: "1.15"}
- {ref: "2026.9.0", api: "1.16"}
- {ref: "13c27429a4d54d1b2ff9fd611f9baafdc3e5f37a", api: "1.17"}
- {ref: "dcb943ded39dc1a7ec470dbfa7f461fc5041ab65", api: "1.19"}
name: Run ESPHome tests (${{ matrix.esphome.ref }}, API ${{ matrix.esphome.api }})
steps:
- name: Check out code from GitHub
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ repository = "https://github.com/puddly/serialx"
documentation = "https://puddly.github.io/serialx/"

[project.optional-dependencies]
esphome = ["aioesphomeapi>=46.5.0; python_version >= '3.11'"]
esphome = ["aioesphomeapi>=46.12.0; python_version >= '3.11'"]

[dependency-groups]
dev = [
Expand All @@ -49,7 +49,7 @@ dev = [
"types-pywin32>=311.0.0.20260508",
"types-setuptools>=82.0.0.20260508",
"setuptools-rust>=1.7.0",
"aioesphomeapi>=46.5.0 ; python_version >= '3.11'",
"aioesphomeapi>=46.12.0 ; python_version >= '3.11'",
]
docs = [
"sphinx>=7,<8.2.3; python_version < '3.11'",
Expand Down
3 changes: 3 additions & 0 deletions serialx/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -1545,6 +1545,9 @@ class SerialPortInfo:
bcd_device: int | None
interface_description: str | None
interface_num: int | None
# Index among the ports a vendor driver exposes on `interface_num`, which udev puts
# in the port's name. `None` under a class driver, which binds the interface itself.
port_number: int | None = None

def __getitem__(self, key: int | slice) -> str | None:
"""Compatibility shim for `serial.tools.list_ports_common.ListPortInfo`."""
Expand Down
9 changes: 9 additions & 0 deletions serialx/platforms/serial_esphome.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@
# Set-mode arrived in API 1.17. Earlier devices never answer a set-mode request.
MIN_VERSION_SERIAL_PROXY_SET_MODE = APIVersion(1, 17)

USB_CLASS_VENDOR_SPEC = 0xFF

# Identity arrived in API 1.18. Earlier devices never answer an identity request.
MIN_VERSION_SERIAL_PROXY_IDENTITY = APIVersion(1, 18)

Expand Down Expand Up @@ -158,6 +160,13 @@ def identity_port_info(
bcd_device=identity.usb.bcd_device if has_usb else None,
interface_description=interface_description,
interface_num=identity.usb.interface_number if has_usb else None,
# A vendor-specific interface is bound through a chip driver, which numbers its
# ports; a class interface (CDC ACM) is bound by the class driver itself
port_number=(
identity.usb.port_number
if has_usb and identity.usb.interface_class == USB_CLASS_VENDOR_SPEC
else None
),
)


Expand Down
4 changes: 4 additions & 0 deletions serialx/platforms/serial_linux.py
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,7 @@ def linux_list_serial_ports() -> list[SerialPortInfo]:
interface_num = int(
(usb_interface / "bInterfaceNumber").read_text(), 16
)
port_number = int((resolved / "port_number").read_text())
except OSError:
LOGGER.debug(
"Serial device %r disappeared during iteration", usb_device
Expand All @@ -408,6 +409,7 @@ def linux_list_serial_ports() -> list[SerialPortInfo]:
bcd_device=bcd_device,
interface_description=_read_optional_sysfs(usb_interface / "interface"),
interface_num=interface_num,
port_number=port_number,
)
elif subsystem == "usb":
# CDC ACM devices
Expand Down Expand Up @@ -436,6 +438,7 @@ def linux_list_serial_ports() -> list[SerialPortInfo]:
bcd_device=bcd_device,
interface_description=_read_optional_sysfs(usb_interface / "interface"),
interface_num=interface_num,
port_number=None,
)
elif subsystem in ("serial-base", "platform", "pnp", "amba"):
# `serial-base` is the per-port subsystem introduced in Linux 6.10.
Expand Down Expand Up @@ -465,6 +468,7 @@ def linux_list_serial_ports() -> list[SerialPortInfo]:
bcd_device=None,
interface_description=None,
interface_num=None,
port_number=None,
)
else:
LOGGER.warning(
Expand Down
14 changes: 10 additions & 4 deletions serialx/udev.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,11 @@ def _udev_replace_chars(value: bytes) -> bytes:


def udev_serial_by_id_stem(port: SerialPortInfo) -> str | None:
"""Compute the `/dev/serial/by-id/` link name udev gives a USB port, without `-portN`.
"""Compute the `/dev/serial/by-id/` link name udev gives a USB port.

udev appends `-port{N}` only when a `usb-serial` driver binds the port, not for CDC
ACM. Which one binds cannot be known from the port, so it is left out.
udev appends `-port{N}` only when a `usb-serial` driver binds the port, N being the
port's index within that driver. A class driver such as CDC ACM binds the interface
itself and gets no suffix.
"""

if port.interface_num is None:
Expand Down Expand Up @@ -118,4 +119,9 @@ def udev_serial_by_id_stem(port: SerialPortInfo) -> str | None:
id_serial += b"_" + serial

# `ID_SERIAL` is built in a 256 byte buffer
return f"usb-{id_serial[:255].decode('utf-8')}-if{port.interface_num:02x}"
stem = f"usb-{id_serial[:255].decode('utf-8')}-if{port.interface_num:02x}"

if port.port_number is not None:
stem += f"-port{port.port_number}"

return stem
12 changes: 12 additions & 0 deletions tests/data/linux/debian-13-7.0.meta.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Linux proxmox 7.0.2-4-pve #1 SMP PREEMPT_DYNAMIC PMX 7.0.2-4 (2026-05-15T07:32Z) x86_64 GNU/Linux

PRETTY_NAME="Debian GNU/Linux 13 (trixie)"
NAME="Debian GNU/Linux"
VERSION_ID="13"
VERSION="13 (trixie)"
VERSION_CODENAME=trixie
DEBIAN_VERSION_FULL=13.5
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"
Loading
Loading