fix: use shared (non-exclusive) COM port access on Windows

pyserial opens COM ports with dwShareMode=0 (exclusive) by default.
On Windows 10/11 this can cause PermissionError(13) even when the port
is not visibly occupied — the OS may have an internal handle.

Added _open_port() helper that tries exclusive=False (FILE_SHARE_READ|
FILE_SHARE_WRITE) first on Windows, falling back to exclusive if
pyserial < 3.3. Applied to all 7 serial.Serial() call sites across
serial_monitor, reboot_manager, widgets.
This commit is contained in:
2026-06-12 09:39:18 +08:00
parent 4ef1ce0c6b
commit b1d8588a23
3 changed files with 27 additions and 7 deletions
+2 -1
View File
@@ -1077,7 +1077,8 @@ class UartMonitorWindow(ctk.CTkToplevel):
def _bg_open() -> None: def _bg_open() -> None:
try: try:
import serial import serial
ser = serial.Serial(self._port, self._baudrate, timeout=1) from serial_monitor import _open_port
ser = _open_port(self._port, self._baudrate, 1)
ser.close() ser.close()
self.after(0, self._on_open_success) self.after(0, self._on_open_success)
except Exception as e: except Exception as e:
+3 -2
View File
@@ -125,11 +125,12 @@ def reboot_via_serial(
try: try:
import serial import serial
from serial_monitor import _open_port
with serial.Serial( with _open_port(
config.serial_port, config.serial_port,
config.serial_baudrate, config.serial_baudrate,
timeout=5, timeout=5.0,
) as ser: ) as ser:
ser.reset_input_buffer() ser.reset_input_buffer()
# Send Ctrl+C to break out of any running process # Send Ctrl+C to break out of any running process
+22 -4
View File
@@ -7,6 +7,7 @@ Provides UartMonitor for continuous background monitoring.
from __future__ import annotations from __future__ import annotations
import sys
import re import re
import threading import threading
import time import time
@@ -17,6 +18,23 @@ import serial
import serial.tools.list_ports import serial.tools.list_ports
def _open_port(port: str, baudrate: int, timeout: float = 1.0) -> serial.Serial:
"""Open a serial port with Windows-friendly settings.
On Windows, tries exclusive=False first (shared access) to avoid
PermissionError(13) when the port appears free but CreateFile
with dwShareMode=0 returns ERROR_ACCESS_DENIED.
Falls back to default (exclusive) if pyserial < 3.3.
"""
if sys.platform == "win32":
try:
return serial.Serial(port, baudrate, timeout=timeout, exclusive=False)
except TypeError:
pass # pyserial < 3.3, doesn't support exclusive=
return serial.Serial(port, baudrate, timeout=timeout)
@dataclass @dataclass
class BootInfo: class BootInfo:
"""Parsed information extracted from Zynq boot output.""" """Parsed information extracted from Zynq boot output."""
@@ -169,7 +187,7 @@ def check_uart_available(
# 3. Try to open and read # 3. Try to open and read
try: try:
with serial.Serial(port, baudrate, timeout=timeout) as ser: with _open_port(port, baudrate, timeout) as ser:
ser.reset_input_buffer() ser.reset_input_buffer()
lines: list[str] = [] lines: list[str] = []
while ser.in_waiting: while ser.in_waiting:
@@ -243,7 +261,7 @@ def test_serial_version(
import serial import serial
try: try:
with serial.Serial(port, baudrate, timeout=timeout) as ser: with _open_port(port, baudrate, timeout) as ser:
ser.reset_input_buffer() ser.reset_input_buffer()
ser.reset_output_buffer() ser.reset_output_buffer()
@@ -384,7 +402,7 @@ class UartMonitor:
# Quick port check before starting thread # Quick port check before starting thread
try: try:
test_ser = serial.Serial(self._port, self._baudrate, timeout=1) test_ser = _open_port(self._port, self._baudrate, 1)
test_ser.close() test_ser.close()
except serial.SerialException as e: except serial.SerialException as e:
if self.on_error: if self.on_error:
@@ -410,7 +428,7 @@ class UartMonitor:
def _read_loop(self) -> None: def _read_loop(self) -> None:
"""Main read loop running in background thread.""" """Main read loop running in background thread."""
try: try:
ser = serial.Serial( ser = _open_port(
self._port, self._port,
self._baudrate, self._baudrate,
timeout=self._timeout, timeout=self._timeout,