Skip to content
Open
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
30 changes: 19 additions & 11 deletions serial/serialwin32.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,31 @@

# pylint: disable=invalid-name,too-few-public-methods
import ctypes
import re
import time
from serial import win32

import serial
from serial.serialutil import SerialBase, SerialException, to_bytes, PortNotOpenError, SerialTimeoutException


LEGACY_DOS_DEVICE_NAMES = frozenset('COM{}'.format(number) for number in range(1, 10))
COM_PORT_NAME = re.compile('COM[0-9]+', re.IGNORECASE)


def device_path(port):
r"""Return the name that Windows resolves to the given serial port.

Windows resolves only the bare names COM1 through COM9 as MS-DOS device
aliases. No other serial port has an alias, so COM0 as well as COM10 and
above must be named in the "\\.\COMx" device namespace format; a bare name
would be resolved as an ordinary relative file system path instead.
"""
if COM_PORT_NAME.fullmatch(port) and port.upper() not in LEGACY_DOS_DEVICE_NAMES:
return '\\\\.\\' + port
return port


class Serial(SerialBase):
"""Serial port implementation for Win32 based on ctypes."""

Expand All @@ -41,18 +59,8 @@ def open(self):
raise SerialException("Port must be configured before it can be used.")
if self.is_open:
raise SerialException("Port is already open.")
# the "\\.\COMx" format is required for devices other than COM1-COM8
# not all versions of windows seem to support this properly
# so that the first few ports are used with the DOS device name
port = self.name
try:
if port.upper().startswith('COM') and int(port[3:]) > 8:
port = '\\\\.\\' + port
except ValueError:
# for like COMnotanumber
pass
self._port_handle = win32.CreateFile(
port,
device_path(self.name),
win32.GENERIC_READ | win32.GENERIC_WRITE,
0, # exclusive access
None, # no security
Expand Down
66 changes: 66 additions & 0 deletions test/test_serialwin32.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# This file is part of pySerial - Cross platform serial port support for Python
#
# SPDX-License-Identifier: BSD-3-Clause

"""
Test the Win32 serial port naming.
"""

import os

import pytest

if os.name == "nt":
from serial import serialwin32, win32

pytestmark = pytest.mark.skipif(os.name != "nt", reason="Windows only")


@pytest.mark.parametrize(
"port, expected",
(
("COM1", "COM1"),
("COM8", "COM8"),
("COM9", "COM9"),
("com3", "com3"),
("COM0", r"\\.\COM0"),
("com0", r"\\.\com0"),
("COM00", r"\\.\COM00"),
("COM01", r"\\.\COM01"),
("COM10", r"\\.\COM10"),
("COM255", r"\\.\COM255"),
(r"\\.\COM1", r"\\.\COM1"),
(r"\\.\COM0", r"\\.\COM0"),
("COM", "COM"),
("COMnotanumber", "COMnotanumber"),
("/dev/ttyS0", "/dev/ttyS0"),
),
)
def test_device_path(port, expected):
"""Verify that every port without an MS-DOS device alias is prefixed."""

assert serialwin32.device_path(port) == expected


@pytest.mark.parametrize(
"port, expected",
(
pytest.param("COM0", r"\\.\COM0", id="device-namespace"),
pytest.param("COM1", "COM1", id="ms-dos-alias"),
),
)
def test_open_names_the_device(monkeypatch, port, expected):
"""Verify that `open()` hands the resolvable name to CreateFile."""

names = []

def create_file(name, *args):
names.append(name)
return win32.INVALID_HANDLE_VALUE

monkeypatch.setattr(win32, "CreateFile", create_file)

with pytest.raises(serialwin32.SerialException):
serialwin32.Serial(port)

assert names == [expected]
Loading