Skip to content

Commit ec84fba

Browse files
Dzarda7radimkarnis
authored andcommitted
feat: Improve ports sorting when autodetection is used
Closes #1124
1 parent aa1b04a commit ec84fba

File tree

4 files changed

+226
-3
lines changed

4 files changed

+226
-3
lines changed

.gitlab-ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ host_tests:
9494
- coverage run --parallel-mode -m pytest ${CI_PROJECT_DIR}/test/test_image_info.py
9595
- coverage run --parallel-mode -m pytest ${CI_PROJECT_DIR}/test/test_modules.py
9696
- coverage run --parallel-mode -m pytest ${CI_PROJECT_DIR}/test/test_logger.py
97+
- coverage run --parallel-mode -m pytest ${CI_PROJECT_DIR}/test/test_port_sorting.py
9798
# some .coverage files in sub-directories are not collected on some runners, move them firs
9899
- find . -mindepth 2 -type f -name ".coverage*" -print -exec mv --backup=numbered {} . \;
99100

@@ -135,6 +136,7 @@ host_tests_latest_python:
135136
- pytest ${CI_PROJECT_DIR}/test/test_image_info.py
136137
- pytest ${CI_PROJECT_DIR}/test/test_modules.py
137138
- pytest ${CI_PROJECT_DIR}/test/test_logger.py
139+
- pytest ${CI_PROJECT_DIR}/test/test_port_sorting.py
138140
- pytest ${CI_PROJECT_DIR}/test/test_espefuse.py --chip esp32
139141

140142
# A new job "host_test_hsm" is created for the test "test_espsecure_hsm.py" which runs an ubuntu image,
@@ -487,6 +489,7 @@ host_tests_windows:
487489
- python -m coverage run --parallel-mode -m pytest ${CI_PROJECT_DIR}/test/test_modules.py
488490
- python -m coverage run --parallel-mode -m pytest ${CI_PROJECT_DIR}/test/test_merge_bin.py
489491
- python -m coverage run --parallel-mode -m pytest ${CI_PROJECT_DIR}/test/test_logger.py
492+
- python -m coverage run --parallel-mode -m pytest ${CI_PROJECT_DIR}/test/test_port_sorting.py
490493

491494
target_tests_windows:
492495
extends: .windows_test

CONTRIBUTING.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ The following tests run automatically by GitHub Actions for each Pull Request. Y
131131
* ``test_imagegen.py`` tests the ``elf2image`` command
132132
* ``test_image_info.py`` tests the ``image-info`` command
133133
* ``test_mergebin.py`` tests the ``merge-bin`` command
134+
* ``test_port_sorting.py`` tests the port sorting algorithm of ``esptool``
134135
* ``test_modules.py`` tests the modules used by ``esptool`` for regressions
135136
* ``test_espsecure.py`` tests ``espsecure`` functionality
136137
* ``test_espsecure_hsm.py`` tests support of external HSM signing in ``espsecure``. These tests require additional prerequisites, see ``SoftHSM2 setup`` in the `tests workflow definition <https://github.com/espressif/esptool/blob/master/.github/workflows/test_esptool.yml>`_ for more information.

esptool/__init__.py

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1050,7 +1050,7 @@ def get_port_list(
10501050
ports = []
10511051
for port in list_ports.comports():
10521052
if sys.platform == "darwin" and port.device.endswith(
1053-
("Bluetooth-Incoming-Port", "wlan-debug")
1053+
("Bluetooth-Incoming-Port", "wlan-debug", "cu.debug-console")
10541054
):
10551055
continue
10561056
if vids and (port.vid is None or port.vid not in vids):
@@ -1066,8 +1066,52 @@ def get_port_list(
10661066
or all(serial not in port.serial_number for serial in serials)
10671067
):
10681068
continue
1069-
ports.append(port.device)
1070-
return sorted(ports)
1069+
ports.append((port.device, port.vid))
1070+
1071+
# Constants for sorting optimization
1072+
ESPRESSIF_VID = 0x303A
1073+
LINUX_DEVICE_PATTERNS = ("ttyUSB", "ttyACM")
1074+
MACOS_DEVICE_PATTERNS = ("usbserial", "usbmodem")
1075+
1076+
def _port_sort_key_linux(port_info):
1077+
device, vid = port_info
1078+
1079+
if vid == ESPRESSIF_VID:
1080+
return (3, device)
1081+
1082+
if any(pattern in device for pattern in LINUX_DEVICE_PATTERNS):
1083+
return (2, device)
1084+
1085+
return (1, device)
1086+
1087+
def _port_sort_key_macos(port_info):
1088+
device, vid = port_info
1089+
1090+
if vid == ESPRESSIF_VID:
1091+
return (3, device)
1092+
1093+
if any(pattern in device for pattern in MACOS_DEVICE_PATTERNS):
1094+
return (2, device)
1095+
1096+
return (1, device)
1097+
1098+
def _port_sort_key_windows(port_info):
1099+
device, vid = port_info
1100+
1101+
if vid == ESPRESSIF_VID:
1102+
return (2, device)
1103+
1104+
return (1, device)
1105+
1106+
if sys.platform == "win32":
1107+
key_func = _port_sort_key_windows
1108+
elif sys.platform == "darwin":
1109+
key_func = _port_sort_key_macos
1110+
else:
1111+
key_func = _port_sort_key_linux
1112+
1113+
sorted_port_info = sorted(ports, key=key_func)
1114+
return [device for device, _ in sorted_port_info]
10711115

10721116

10731117
def expand_file_arguments(argv: list[str]) -> list[str]:

test/test_port_sorting.py

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import pytest
2+
from unittest.mock import patch
3+
import esptool
4+
5+
# Espressif VID constant (same as in esptool/__init__.py)
6+
ESPRESSIF_VID = 0x303A
7+
8+
9+
class MockPort:
10+
"""Mock serial port object that mimics pyserial's ListPortInfo"""
11+
12+
def __init__(self, device, vid=None, pid=None, name=None, serial_number=None):
13+
self.device = device
14+
self.vid = vid
15+
self.pid = pid
16+
self.name = name
17+
self.serial_number = serial_number
18+
19+
20+
@pytest.mark.host_test
21+
class TestPortSorting:
22+
"""Test the port sorting algorithm in get_port_list function"""
23+
24+
def test_linux_port_sorting(self):
25+
"""Test port sorting on Linux platform"""
26+
mock_ports = [
27+
MockPort("/dev/ttyS0", vid=0x1234),
28+
MockPort("/dev/ttyS1", vid=0x1234),
29+
MockPort("/dev/ttyUSB0", vid=0x1234),
30+
MockPort("/dev/ttyUSB1", vid=0x1234),
31+
MockPort("/dev/ttyACM1", vid=ESPRESSIF_VID),
32+
MockPort("/dev/ttyACM0", vid=0x1234),
33+
]
34+
35+
with (
36+
patch("sys.platform", "linux"),
37+
patch("esptool.list_ports") as mock_list_ports,
38+
):
39+
mock_list_ports.comports.return_value = mock_ports
40+
41+
result = esptool.get_port_list()
42+
43+
# Expected sorting order (alphabetically within each group):
44+
# 1. Other devices (priority 1)
45+
# 2. ttyUSB*/ttyACM* devices (priority 2)
46+
# 3. Espressif VID devices (priority 3) - highest priority, appear LAST
47+
expected = [
48+
"/dev/ttyS0", # Other
49+
"/dev/ttyS1", # Other
50+
"/dev/ttyACM0", # USB/ACM
51+
"/dev/ttyUSB0", # USB/ACM
52+
"/dev/ttyUSB1", # USB/ACM
53+
"/dev/ttyACM1", # Espressif VID
54+
]
55+
56+
assert result == expected
57+
58+
def test_macos_port_sorting(self):
59+
"""Test port sorting on macOS platform"""
60+
mock_ports = [
61+
MockPort("/dev/cu.wlan-debug", vid=0x1234), # Excluded by macOS filter
62+
MockPort(
63+
"/dev/cu.Bluetooth-Incoming-Port", vid=0x1234
64+
), # Excluded by macOS filter
65+
MockPort("/dev/cu.debug-console", vid=0x1234), # Excluded by macOS filter
66+
MockPort("/dev/cu.usbserial2", vid=0x1234),
67+
MockPort("/dev/cu.usbmodem1", vid=0x1234),
68+
MockPort("/dev/cu.usbmodem2", vid=ESPRESSIF_VID),
69+
MockPort("/dev/cu.usbserial1", vid=0x1234),
70+
]
71+
72+
with (
73+
patch("sys.platform", "darwin"),
74+
patch("esptool.list_ports") as mock_list_ports,
75+
):
76+
mock_list_ports.comports.return_value = mock_ports
77+
78+
result = esptool.get_port_list()
79+
80+
# Expected sorting order (alphabetically within each group):
81+
# 1. Other devices (priority 1)
82+
# 2. usbserial*/usbmodem* devices (priority 2)
83+
# 3. Espressif VID devices (priority 3) - highest priority, appear LAST
84+
# Note: wlan-debug, Bluetooth-Incoming-Port, debug-console are excluded
85+
expected = [
86+
"/dev/cu.usbmodem1", # usbmodem
87+
"/dev/cu.usbserial1", # usbserial
88+
"/dev/cu.usbserial2", # usbserial
89+
"/dev/cu.usbmodem2", # Espressif VID
90+
]
91+
92+
assert result == expected
93+
94+
def test_windows_port_sorting(self):
95+
"""Test port sorting on Windows platform"""
96+
mock_ports = [
97+
MockPort("COM3", vid=0x1234),
98+
MockPort("COM1", vid=0x1234),
99+
MockPort("COM10", vid=0x1234),
100+
MockPort("COM5", vid=ESPRESSIF_VID),
101+
MockPort("COM2", vid=0x1234),
102+
]
103+
104+
with (
105+
patch("sys.platform", "win32"),
106+
patch("esptool.list_ports") as mock_list_ports,
107+
):
108+
mock_list_ports.comports.return_value = mock_ports
109+
110+
result = esptool.get_port_list()
111+
112+
# Expected sorting order (alphabetically within each group):
113+
# 1. All other COM ports (priority 1)
114+
# 2. Espressif VID devices (priority 2) - highest priority, appear LAST
115+
expected = [
116+
"COM1", # Other
117+
"COM10", # Other
118+
"COM2", # Other
119+
"COM3", # Other
120+
"COM5", # Espressif VID
121+
]
122+
123+
assert result == expected
124+
125+
def test_port_filtering_parameters(self):
126+
"""Test port filtering with various parameters while maintaining sorting"""
127+
mock_ports = [
128+
MockPort(
129+
"/dev/ttyUSB0",
130+
vid=0x1234,
131+
pid=0x5678,
132+
name="USB Serial",
133+
serial_number="ABC123",
134+
),
135+
MockPort(
136+
"/dev/ttyUSB1",
137+
vid=ESPRESSIF_VID,
138+
pid=0x1001,
139+
name="ESP32",
140+
serial_number="ESP001",
141+
),
142+
MockPort(
143+
"/dev/ttyUSB2",
144+
vid=ESPRESSIF_VID,
145+
pid=0x1002,
146+
name="ESP32-S3",
147+
serial_number="ESP002",
148+
),
149+
]
150+
151+
with (
152+
patch("sys.platform", "linux"),
153+
patch("esptool.list_ports") as mock_list_ports,
154+
):
155+
mock_list_ports.comports.return_value = mock_ports
156+
157+
# Test VID filtering - Espressif devices appear last
158+
result = esptool.get_port_list(vids=[ESPRESSIF_VID])
159+
expected = ["/dev/ttyUSB1", "/dev/ttyUSB2"]
160+
assert result == expected
161+
162+
# Test PID filtering
163+
result = esptool.get_port_list(pids=[0x1001])
164+
expected = ["/dev/ttyUSB1"]
165+
assert result == expected
166+
167+
# Test name filtering
168+
result = esptool.get_port_list(names=["ESP32"])
169+
expected = ["/dev/ttyUSB1", "/dev/ttyUSB2"]
170+
assert result == expected
171+
172+
# Test serial filtering
173+
result = esptool.get_port_list(serials=["ESP"])
174+
expected = ["/dev/ttyUSB1", "/dev/ttyUSB2"]
175+
assert result == expected

0 commit comments

Comments
 (0)