pyserial-pyusb 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyserial_pyusb-0.1.0/PKG-INFO +9 -0
- pyserial_pyusb-0.1.0/pyproject.toml +22 -0
- pyserial_pyusb-0.1.0/setup.cfg +4 -0
- pyserial_pyusb-0.1.0/src/pyserial_pyusb.egg-info/PKG-INFO +9 -0
- pyserial_pyusb-0.1.0/src/pyserial_pyusb.egg-info/SOURCES.txt +15 -0
- pyserial_pyusb-0.1.0/src/pyserial_pyusb.egg-info/dependency_links.txt +1 -0
- pyserial_pyusb-0.1.0/src/pyserial_pyusb.egg-info/requires.txt +5 -0
- pyserial_pyusb-0.1.0/src/pyserial_pyusb.egg-info/top_level.txt +1 -0
- pyserial_pyusb-0.1.0/src/serial_pyusb/__init__.py +15 -0
- pyserial_pyusb-0.1.0/src/serial_pyusb/drivers/__init__.py +7 -0
- pyserial_pyusb-0.1.0/src/serial_pyusb/drivers/base.py +85 -0
- pyserial_pyusb-0.1.0/src/serial_pyusb/drivers/cdc_acm.py +140 -0
- pyserial_pyusb-0.1.0/src/serial_pyusb/drivers/cp210x.py +104 -0
- pyserial_pyusb-0.1.0/src/serial_pyusb/serial.py +480 -0
- pyserial_pyusb-0.1.0/src/serial_pyusb/urlhandler/__init__.py +0 -0
- pyserial_pyusb-0.1.0/src/serial_pyusb/urlhandler/protocol_pyusb.py +10 -0
- pyserial_pyusb-0.1.0/src/serial_pyusb/urlhandler/protocol_usbip.py +10 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyserial-pyusb
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pyserial backend for USB serial devices via pyusb (CDC ACM and CP210x)
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Requires-Dist: pyserial>=3.5
|
|
7
|
+
Requires-Dist: pyusb>=1.3.1
|
|
8
|
+
Provides-Extra: usbip
|
|
9
|
+
Requires-Dist: pyusb-usbip-backend>=0.1.0; extra == "usbip"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pyserial-pyusb"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Pyserial backend for USB serial devices via pyusb (CDC ACM and CP210x)"
|
|
9
|
+
requires-python = ">=3.9"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"pyserial>=3.5",
|
|
12
|
+
"pyusb>=1.3.1",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.optional-dependencies]
|
|
16
|
+
usbip = ["pyusb-usbip-backend>=0.1.0"]
|
|
17
|
+
|
|
18
|
+
[tool.setuptools]
|
|
19
|
+
package-dir = {"" = "src"}
|
|
20
|
+
|
|
21
|
+
[tool.setuptools.packages.find]
|
|
22
|
+
where = ["src"]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyserial-pyusb
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pyserial backend for USB serial devices via pyusb (CDC ACM and CP210x)
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Requires-Dist: pyserial>=3.5
|
|
7
|
+
Requires-Dist: pyusb>=1.3.1
|
|
8
|
+
Provides-Extra: usbip
|
|
9
|
+
Requires-Dist: pyusb-usbip-backend>=0.1.0; extra == "usbip"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
pyproject.toml
|
|
2
|
+
src/pyserial_pyusb.egg-info/PKG-INFO
|
|
3
|
+
src/pyserial_pyusb.egg-info/SOURCES.txt
|
|
4
|
+
src/pyserial_pyusb.egg-info/dependency_links.txt
|
|
5
|
+
src/pyserial_pyusb.egg-info/requires.txt
|
|
6
|
+
src/pyserial_pyusb.egg-info/top_level.txt
|
|
7
|
+
src/serial_pyusb/__init__.py
|
|
8
|
+
src/serial_pyusb/serial.py
|
|
9
|
+
src/serial_pyusb/drivers/__init__.py
|
|
10
|
+
src/serial_pyusb/drivers/base.py
|
|
11
|
+
src/serial_pyusb/drivers/cdc_acm.py
|
|
12
|
+
src/serial_pyusb/drivers/cp210x.py
|
|
13
|
+
src/serial_pyusb/urlhandler/__init__.py
|
|
14
|
+
src/serial_pyusb/urlhandler/protocol_pyusb.py
|
|
15
|
+
src/serial_pyusb/urlhandler/protocol_usbip.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
serial_pyusb
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Pyserial backend for USB serial devices via pyusb.
|
|
2
|
+
|
|
3
|
+
Supports local USB devices (via libusb) and remote devices (via USB/IP).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from serial_pyusb.serial import Serial
|
|
7
|
+
|
|
8
|
+
# Auto-register the pyusb:// URL handler with pyserial on import.
|
|
9
|
+
import importlib as _importlib
|
|
10
|
+
_serial_pkg = _importlib.import_module("serial")
|
|
11
|
+
_HANDLER_PACKAGE = "serial_pyusb.urlhandler"
|
|
12
|
+
if _HANDLER_PACKAGE not in _serial_pkg.protocol_handler_packages:
|
|
13
|
+
_serial_pkg.protocol_handler_packages.append(_HANDLER_PACKAGE)
|
|
14
|
+
|
|
15
|
+
__all__ = ["Serial"]
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Base class for USB serial drivers."""
|
|
2
|
+
|
|
3
|
+
import usb.core
|
|
4
|
+
import usb.util
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class USBSerialDriver:
|
|
8
|
+
"""Abstract base for USB serial device drivers.
|
|
9
|
+
|
|
10
|
+
Subclasses implement the vendor-specific USB control transfers needed to
|
|
11
|
+
configure baud rate, line parameters, and modem control signals for a
|
|
12
|
+
particular USB-to-serial chip.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, dev: usb.core.Device):
|
|
16
|
+
self.dev = dev
|
|
17
|
+
self.logger = None
|
|
18
|
+
self._ep_in = None
|
|
19
|
+
self._ep_out = None
|
|
20
|
+
self._find_endpoints()
|
|
21
|
+
|
|
22
|
+
def _find_endpoints(self):
|
|
23
|
+
"""Locate bulk IN and OUT endpoints on the data interface."""
|
|
24
|
+
cfg = self.dev.get_active_configuration()
|
|
25
|
+
intf = self._get_data_interface(cfg)
|
|
26
|
+
for ep in intf:
|
|
27
|
+
direction = usb.util.endpoint_direction(ep.bEndpointAddress)
|
|
28
|
+
ep_type = usb.util.endpoint_type(ep.bmAttributes)
|
|
29
|
+
if ep_type == usb.util.ENDPOINT_TYPE_BULK:
|
|
30
|
+
if direction == usb.util.ENDPOINT_IN:
|
|
31
|
+
self._ep_in = ep.bEndpointAddress
|
|
32
|
+
elif direction == usb.util.ENDPOINT_OUT:
|
|
33
|
+
self._ep_out = ep.bEndpointAddress
|
|
34
|
+
if self._ep_in is None or self._ep_out is None:
|
|
35
|
+
raise usb.core.USBError("Could not find bulk IN/OUT endpoints")
|
|
36
|
+
|
|
37
|
+
def _get_data_interface(self, cfg):
|
|
38
|
+
"""Return the USB interface that carries serial data.
|
|
39
|
+
|
|
40
|
+
Override in subclasses if the data interface is not interface 0.
|
|
41
|
+
"""
|
|
42
|
+
return cfg[(0, 0)]
|
|
43
|
+
|
|
44
|
+
# -- Methods subclasses must implement --
|
|
45
|
+
|
|
46
|
+
def open(self):
|
|
47
|
+
"""Enable the serial interface on the device."""
|
|
48
|
+
raise NotImplementedError
|
|
49
|
+
|
|
50
|
+
def close(self):
|
|
51
|
+
"""Disable the serial interface on the device."""
|
|
52
|
+
raise NotImplementedError
|
|
53
|
+
|
|
54
|
+
def set_baudrate(self, baudrate: int):
|
|
55
|
+
raise NotImplementedError
|
|
56
|
+
|
|
57
|
+
def set_line_params(self, bytesize: int, parity: str, stopbits: float):
|
|
58
|
+
"""Configure data bits, parity, and stop bits."""
|
|
59
|
+
raise NotImplementedError
|
|
60
|
+
|
|
61
|
+
def set_dtr(self, state: bool):
|
|
62
|
+
raise NotImplementedError
|
|
63
|
+
|
|
64
|
+
def set_rts(self, state: bool):
|
|
65
|
+
raise NotImplementedError
|
|
66
|
+
|
|
67
|
+
def get_modem_status(self) -> dict:
|
|
68
|
+
"""Return dict with bool keys: cts, dsr, ri, cd."""
|
|
69
|
+
return {"cts": True, "dsr": True, "ri": False, "cd": True}
|
|
70
|
+
|
|
71
|
+
def set_break(self, state: bool):
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
# -- Data transfer --
|
|
75
|
+
|
|
76
|
+
def write(self, data: bytes, timeout: int = 0) -> int:
|
|
77
|
+
return self.dev.write(self._ep_out, data, timeout=timeout)
|
|
78
|
+
|
|
79
|
+
def read(self, size: int, timeout: int = 0) -> bytes:
|
|
80
|
+
return bytes(self.dev.read(self._ep_in, size, timeout=timeout))
|
|
81
|
+
|
|
82
|
+
@classmethod
|
|
83
|
+
def matches(cls, dev: usb.core.Device) -> bool:
|
|
84
|
+
"""Return True if this driver can handle the given USB device."""
|
|
85
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Standard USB CDC ACM serial driver."""
|
|
2
|
+
|
|
3
|
+
import struct
|
|
4
|
+
|
|
5
|
+
import usb.core
|
|
6
|
+
import usb.util
|
|
7
|
+
|
|
8
|
+
from serial_pyusb.drivers.base import USBSerialDriver
|
|
9
|
+
|
|
10
|
+
# CDC class requests
|
|
11
|
+
_SET_LINE_CODING = 0x20
|
|
12
|
+
_GET_LINE_CODING = 0x21
|
|
13
|
+
_SET_CONTROL_LINE_STATE = 0x22
|
|
14
|
+
_SEND_BREAK = 0x23
|
|
15
|
+
|
|
16
|
+
# Line coding: parity
|
|
17
|
+
_PARITY = {"N": 0, "O": 1, "E": 2, "M": 3, "S": 4}
|
|
18
|
+
|
|
19
|
+
# Line coding: stop bits
|
|
20
|
+
_STOP_BITS = {1: 0, 1.5: 1, 2: 2}
|
|
21
|
+
|
|
22
|
+
# USB CDC interface class/subclass
|
|
23
|
+
_CDC_CLASS = 0x02
|
|
24
|
+
_CDC_ACM_SUBCLASS = 0x02
|
|
25
|
+
_CDC_DATA_CLASS = 0x0A
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CDCACMDriver(USBSerialDriver):
|
|
29
|
+
"""Driver for standard USB CDC ACM serial devices."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, dev: usb.core.Device):
|
|
32
|
+
self._ctrl_intf = 0
|
|
33
|
+
self._data_intf = 1
|
|
34
|
+
self._dtr = False
|
|
35
|
+
self._rts = False
|
|
36
|
+
self._baudrate = 9600
|
|
37
|
+
self._stop_val = 0
|
|
38
|
+
self._parity_val = 0
|
|
39
|
+
self._data_bits = 8
|
|
40
|
+
super().__init__(dev)
|
|
41
|
+
self._detect_interfaces()
|
|
42
|
+
|
|
43
|
+
def _detect_interfaces(self):
|
|
44
|
+
"""Find the CDC control and data interfaces from descriptors."""
|
|
45
|
+
try:
|
|
46
|
+
cfg = self.dev.get_active_configuration()
|
|
47
|
+
except usb.core.USBError:
|
|
48
|
+
self.dev.set_configuration()
|
|
49
|
+
cfg = self.dev.get_active_configuration()
|
|
50
|
+
for intf in cfg:
|
|
51
|
+
if (intf.bInterfaceClass == _CDC_CLASS and
|
|
52
|
+
intf.bInterfaceSubClass == _CDC_ACM_SUBCLASS):
|
|
53
|
+
self._ctrl_intf = intf.bInterfaceNumber
|
|
54
|
+
elif intf.bInterfaceClass == _CDC_DATA_CLASS:
|
|
55
|
+
self._data_intf = intf.bInterfaceNumber
|
|
56
|
+
|
|
57
|
+
def _get_data_interface(self, cfg):
|
|
58
|
+
return cfg[(self._data_intf, 0)]
|
|
59
|
+
|
|
60
|
+
def _send_line_state(self):
|
|
61
|
+
"""Send SET_CONTROL_LINE_STATE with current DTR/RTS."""
|
|
62
|
+
value = (int(self._dtr)) | (int(self._rts) << 1)
|
|
63
|
+
if self.logger:
|
|
64
|
+
self.logger.debug("cdc_acm SET_CONTROL_LINE_STATE val=0x%04x (DTR=%s RTS=%s)", value, self._dtr, self._rts)
|
|
65
|
+
self.dev.ctrl_transfer(
|
|
66
|
+
0x21, _SET_CONTROL_LINE_STATE, value, self._ctrl_intf, timeout=1000
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def open(self):
|
|
70
|
+
self._dtr = True
|
|
71
|
+
self._rts = True
|
|
72
|
+
self._send_line_state()
|
|
73
|
+
|
|
74
|
+
def close(self):
|
|
75
|
+
self._dtr = False
|
|
76
|
+
self._rts = False
|
|
77
|
+
self._send_line_state()
|
|
78
|
+
|
|
79
|
+
def _send_line_coding(self):
|
|
80
|
+
"""Send SET_LINE_CODING with current cached values."""
|
|
81
|
+
payload = struct.pack(
|
|
82
|
+
"<IBBB",
|
|
83
|
+
self._baudrate,
|
|
84
|
+
self._stop_val,
|
|
85
|
+
self._parity_val,
|
|
86
|
+
self._data_bits,
|
|
87
|
+
)
|
|
88
|
+
if self.logger:
|
|
89
|
+
self.logger.debug(
|
|
90
|
+
"cdc_acm SET_LINE_CODING baud=%d stop=%d parity=%d bits=%d payload=%s",
|
|
91
|
+
self._baudrate, self._stop_val, self._parity_val, self._data_bits,
|
|
92
|
+
payload.hex(),
|
|
93
|
+
)
|
|
94
|
+
self.dev.ctrl_transfer(
|
|
95
|
+
0x21, _SET_LINE_CODING, 0, self._ctrl_intf, payload, timeout=1000
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def set_baudrate(self, baudrate: int):
|
|
99
|
+
self._baudrate = baudrate
|
|
100
|
+
self._send_line_coding()
|
|
101
|
+
|
|
102
|
+
def set_line_params(self, bytesize: int, parity: str, stopbits: float):
|
|
103
|
+
self._stop_val = _STOP_BITS.get(stopbits, 0)
|
|
104
|
+
self._parity_val = _PARITY.get(parity, 0)
|
|
105
|
+
self._data_bits = bytesize
|
|
106
|
+
self._send_line_coding()
|
|
107
|
+
|
|
108
|
+
def set_dtr(self, state: bool):
|
|
109
|
+
self._dtr = state
|
|
110
|
+
self._send_line_state()
|
|
111
|
+
|
|
112
|
+
def set_rts(self, state: bool):
|
|
113
|
+
self._rts = state
|
|
114
|
+
self._send_line_state()
|
|
115
|
+
|
|
116
|
+
def set_break(self, state: bool):
|
|
117
|
+
# SEND_BREAK: wValue = duration in ms, 0xFFFF = on until cleared, 0 = off
|
|
118
|
+
value = 0xFFFF if state else 0x0000
|
|
119
|
+
if self.logger:
|
|
120
|
+
self.logger.debug("cdc_acm SEND_BREAK val=0x%04x", value)
|
|
121
|
+
self.dev.ctrl_transfer(
|
|
122
|
+
0x21, _SEND_BREAK, value, self._ctrl_intf, timeout=1000
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
@classmethod
|
|
126
|
+
def matches(cls, dev: usb.core.Device) -> bool:
|
|
127
|
+
"""Match any device with a CDC ACM interface."""
|
|
128
|
+
try:
|
|
129
|
+
cfg = dev.get_active_configuration()
|
|
130
|
+
except usb.core.USBError:
|
|
131
|
+
try:
|
|
132
|
+
dev.set_configuration()
|
|
133
|
+
cfg = dev.get_active_configuration()
|
|
134
|
+
except usb.core.USBError:
|
|
135
|
+
return False
|
|
136
|
+
for intf in cfg:
|
|
137
|
+
if (intf.bInterfaceClass == _CDC_CLASS and
|
|
138
|
+
intf.bInterfaceSubClass == _CDC_ACM_SUBCLASS):
|
|
139
|
+
return True
|
|
140
|
+
return False
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Silicon Labs CP210x USB-to-UART driver."""
|
|
2
|
+
|
|
3
|
+
import struct
|
|
4
|
+
|
|
5
|
+
import usb.core
|
|
6
|
+
|
|
7
|
+
from serial_pyusb.drivers.base import USBSerialDriver
|
|
8
|
+
|
|
9
|
+
# CP210x vendor-specific request codes
|
|
10
|
+
_IFC_ENABLE = 0x00
|
|
11
|
+
_SET_LINE_CTL = 0x03
|
|
12
|
+
_SET_FLOW = 0x13
|
|
13
|
+
_SET_MHS = 0x07
|
|
14
|
+
_GET_MDMSTS = 0x08
|
|
15
|
+
_SET_BAUDRATE = 0x1E
|
|
16
|
+
|
|
17
|
+
# Line control encoding
|
|
18
|
+
_DATA_BITS = {5: 0x0500, 6: 0x0600, 7: 0x0700, 8: 0x0800}
|
|
19
|
+
_PARITY = {"N": 0x0000, "O": 0x0010, "E": 0x0020, "M": 0x0030, "S": 0x0040}
|
|
20
|
+
_STOP_BITS = {1: 0x0000, 1.5: 0x0100, 2: 0x0200}
|
|
21
|
+
|
|
22
|
+
# Modem handshake bits
|
|
23
|
+
_DTR_ON = 0x0101
|
|
24
|
+
_DTR_OFF = 0x0100
|
|
25
|
+
_RTS_ON = 0x0202
|
|
26
|
+
_RTS_OFF = 0x0200
|
|
27
|
+
|
|
28
|
+
# Modem status bits
|
|
29
|
+
_CTS = 0x10
|
|
30
|
+
_DSR = 0x20
|
|
31
|
+
_RI = 0x40
|
|
32
|
+
_DCD = 0x80
|
|
33
|
+
|
|
34
|
+
# Known CP210x VID/PIDs
|
|
35
|
+
_VID = 0x10C4
|
|
36
|
+
_PIDS = {0xEA60, 0xEA61, 0xEA70, 0xEA80}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class CP210xDriver(USBSerialDriver):
|
|
40
|
+
"""Driver for Silicon Labs CP210x USB-to-UART bridges."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, dev: usb.core.Device):
|
|
43
|
+
self._intf = 0
|
|
44
|
+
super().__init__(dev)
|
|
45
|
+
|
|
46
|
+
def _ctrl(self, request, value=0, data=None):
|
|
47
|
+
"""Send a vendor-specific control transfer to the CP210x."""
|
|
48
|
+
if self.logger:
|
|
49
|
+
self.logger.debug(
|
|
50
|
+
"cp210x ctrl_out req=0x%02x val=0x%04x data=%s",
|
|
51
|
+
request, value, data.hex() if data else None,
|
|
52
|
+
)
|
|
53
|
+
if data is not None:
|
|
54
|
+
self.dev.ctrl_transfer(0x41, request, value, self._intf, data, timeout=1000)
|
|
55
|
+
else:
|
|
56
|
+
self.dev.ctrl_transfer(0x41, request, value, self._intf, timeout=1000)
|
|
57
|
+
|
|
58
|
+
def _ctrl_in(self, request, length, value=0):
|
|
59
|
+
"""Read a vendor-specific control transfer from the CP210x."""
|
|
60
|
+
result = self.dev.ctrl_transfer(0xC1, request, value, self._intf, length, timeout=1000)
|
|
61
|
+
if self.logger:
|
|
62
|
+
self.logger.debug(
|
|
63
|
+
"cp210x ctrl_in req=0x%02x val=0x%04x result=%s",
|
|
64
|
+
request, value, bytes(result).hex(),
|
|
65
|
+
)
|
|
66
|
+
return result
|
|
67
|
+
|
|
68
|
+
def open(self):
|
|
69
|
+
self._ctrl(_IFC_ENABLE, 0x0001)
|
|
70
|
+
|
|
71
|
+
def close(self):
|
|
72
|
+
self._ctrl(_IFC_ENABLE, 0x0000)
|
|
73
|
+
|
|
74
|
+
def set_baudrate(self, baudrate: int):
|
|
75
|
+
payload = struct.pack("<I", baudrate)
|
|
76
|
+
self._ctrl(_SET_BAUDRATE, data=payload)
|
|
77
|
+
|
|
78
|
+
def set_line_params(self, bytesize: int, parity: str, stopbits: float):
|
|
79
|
+
value = _DATA_BITS.get(bytesize, 0x0800) | _PARITY.get(parity, 0) | _STOP_BITS.get(stopbits, 0)
|
|
80
|
+
self._ctrl(_SET_LINE_CTL, value)
|
|
81
|
+
|
|
82
|
+
def set_dtr(self, state: bool):
|
|
83
|
+
self._ctrl(_SET_MHS, _DTR_ON if state else _DTR_OFF)
|
|
84
|
+
|
|
85
|
+
def set_rts(self, state: bool):
|
|
86
|
+
self._ctrl(_SET_MHS, _RTS_ON if state else _RTS_OFF)
|
|
87
|
+
|
|
88
|
+
def get_modem_status(self) -> dict:
|
|
89
|
+
data = self._ctrl_in(_GET_MDMSTS, 1)
|
|
90
|
+
status = data[0]
|
|
91
|
+
return {
|
|
92
|
+
"cts": bool(status & _CTS),
|
|
93
|
+
"dsr": bool(status & _DSR),
|
|
94
|
+
"ri": bool(status & _RI),
|
|
95
|
+
"cd": bool(status & _DCD),
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
def set_break(self, state: bool):
|
|
99
|
+
# CP210x SET_BREAK: request 0x05, wValue 1=on 0=off
|
|
100
|
+
self._ctrl(0x05, 0x0001 if state else 0x0000)
|
|
101
|
+
|
|
102
|
+
@classmethod
|
|
103
|
+
def matches(cls, dev: usb.core.Device) -> bool:
|
|
104
|
+
return dev.idVendor == _VID and dev.idProduct in _PIDS
|
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
"""Pyserial Serial class backed by pyusb.
|
|
2
|
+
|
|
3
|
+
URL format:
|
|
4
|
+
pyusb://host[:port]/VID:PID[?serial=X&instance=N&logging=LEVEL]
|
|
5
|
+
pyusb:///direct/VID:PID[?serial=X&instance=N&logging=LEVEL]
|
|
6
|
+
|
|
7
|
+
Remote examples (via USB/IP):
|
|
8
|
+
pyusb://192.168.1.20/10c4:ea60
|
|
9
|
+
pyusb://192.168.1.20:3240/10c4:ea60?serial=0001
|
|
10
|
+
pyusb://192.168.1.20/10c4:ea60?logging=debug
|
|
11
|
+
|
|
12
|
+
Local examples (direct via libusb):
|
|
13
|
+
pyusb:///direct/10c4:ea60
|
|
14
|
+
pyusb:///direct/10c4:ea60?serial=0001&instance=1
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import errno
|
|
18
|
+
import logging
|
|
19
|
+
import time
|
|
20
|
+
import urllib.parse
|
|
21
|
+
|
|
22
|
+
import usb.core
|
|
23
|
+
import usb.util
|
|
24
|
+
|
|
25
|
+
from serial.serialutil import (
|
|
26
|
+
SerialBase,
|
|
27
|
+
SerialException,
|
|
28
|
+
PortNotOpenError,
|
|
29
|
+
to_bytes,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
from serial_pyusb.drivers.cp210x import CP210xDriver
|
|
33
|
+
from serial_pyusb.drivers.cdc_acm import CDCACMDriver
|
|
34
|
+
|
|
35
|
+
LOGGER_NAME = "pySerial.pyusb"
|
|
36
|
+
|
|
37
|
+
# Driver classes in priority order (most specific first)
|
|
38
|
+
_DRIVERS = [CP210xDriver, CDCACMDriver]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _select_driver(dev):
|
|
42
|
+
"""Auto-select a driver for the given USB device."""
|
|
43
|
+
for cls in _DRIVERS:
|
|
44
|
+
if cls.matches(dev):
|
|
45
|
+
return cls(dev)
|
|
46
|
+
raise SerialException(
|
|
47
|
+
f"No driver found for USB device "
|
|
48
|
+
f"{dev.idVendor:04x}:{dev.idProduct:04x}"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Serial(SerialBase):
|
|
53
|
+
"""Pyserial port backed by a USB serial device via pyusb."""
|
|
54
|
+
|
|
55
|
+
BAUDRATES = (
|
|
56
|
+
50, 75, 110, 134, 150, 200, 300, 600, 1200, 1800, 2400, 4800,
|
|
57
|
+
9600, 19200, 38400, 57600, 115200, 230400, 460800, 500000,
|
|
58
|
+
576000, 921600, 1000000, 1152000, 1500000, 2000000, 2500000,
|
|
59
|
+
3000000, 3500000, 4000000,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Minimum USB bulk read size — USB bulk transfers deliver whole
|
|
63
|
+
# packets, so requesting fewer bytes than the device sends causes
|
|
64
|
+
# EOVERFLOW. 64 bytes matches the full-speed bulk max packet size.
|
|
65
|
+
_USB_READ_SIZE = 64
|
|
66
|
+
|
|
67
|
+
def __init__(self, *args, **kwargs):
|
|
68
|
+
self._backend = None
|
|
69
|
+
self._usb_dev = None
|
|
70
|
+
self._driver = None
|
|
71
|
+
self._driver_cls = None
|
|
72
|
+
self.logger = None
|
|
73
|
+
self._last_baudrate = None
|
|
74
|
+
self._last_line_params = None
|
|
75
|
+
self._read_buf = bytearray()
|
|
76
|
+
super().__init__(*args, **kwargs)
|
|
77
|
+
|
|
78
|
+
def open(self):
|
|
79
|
+
if self.is_open:
|
|
80
|
+
raise SerialException("Port is already open.")
|
|
81
|
+
if self._port is None:
|
|
82
|
+
raise SerialException("Port must be configured before it can be used.")
|
|
83
|
+
|
|
84
|
+
params = self._parse_url(self.portstr)
|
|
85
|
+
|
|
86
|
+
if self._backend is not None:
|
|
87
|
+
# Reopen after close — use reconnect logic which reuses the
|
|
88
|
+
# existing connection instead of opening a new one
|
|
89
|
+
# (USB/IP servers are often single-connection).
|
|
90
|
+
self.is_open = True
|
|
91
|
+
self._reconnect_driver()
|
|
92
|
+
if not self._dsrdtr:
|
|
93
|
+
self._update_dtr_state()
|
|
94
|
+
if not self._rtscts:
|
|
95
|
+
self._update_rts_state()
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
if params["host"] is not None:
|
|
99
|
+
from usbip_backend import get_backend
|
|
100
|
+
self._backend = get_backend(params["host"], port=params["port"])
|
|
101
|
+
else:
|
|
102
|
+
self._backend = None # use default pyusb backend (libusb)
|
|
103
|
+
|
|
104
|
+
find_kwargs = {
|
|
105
|
+
"idVendor": params["vid"],
|
|
106
|
+
"idProduct": params["pid"],
|
|
107
|
+
}
|
|
108
|
+
if self._backend is not None:
|
|
109
|
+
find_kwargs["backend"] = self._backend
|
|
110
|
+
if params["serial"] is not None:
|
|
111
|
+
find_kwargs["serial_number"] = params["serial"]
|
|
112
|
+
|
|
113
|
+
# Find matching device(s)
|
|
114
|
+
instance = params["instance"]
|
|
115
|
+
if instance > 0:
|
|
116
|
+
devs = list(usb.core.find(find_all=True, **find_kwargs))
|
|
117
|
+
if instance >= len(devs):
|
|
118
|
+
raise SerialException(
|
|
119
|
+
f"USB device {params['vid']:04x}:{params['pid']:04x} "
|
|
120
|
+
f"instance {instance} not found "
|
|
121
|
+
f"({len(devs)} device(s) available)"
|
|
122
|
+
)
|
|
123
|
+
self._usb_dev = devs[instance]
|
|
124
|
+
else:
|
|
125
|
+
self._usb_dev = usb.core.find(**find_kwargs)
|
|
126
|
+
|
|
127
|
+
if self._usb_dev is None:
|
|
128
|
+
raise SerialException(
|
|
129
|
+
f"USB device {params['vid']:04x}:{params['pid']:04x} not found"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
self._driver = _select_driver(self._usb_dev)
|
|
133
|
+
self._driver_cls = type(self._driver)
|
|
134
|
+
self._driver.logger = self.logger
|
|
135
|
+
if self.logger:
|
|
136
|
+
self.logger.info(
|
|
137
|
+
"opened %04x:%04x using %s",
|
|
138
|
+
self._usb_dev.idVendor, self._usb_dev.idProduct,
|
|
139
|
+
type(self._driver).__name__,
|
|
140
|
+
)
|
|
141
|
+
self._driver.open()
|
|
142
|
+
self.is_open = True
|
|
143
|
+
self._reconfigure_port()
|
|
144
|
+
if not self._dsrdtr:
|
|
145
|
+
self._update_dtr_state()
|
|
146
|
+
if not self._rtscts:
|
|
147
|
+
self._update_rts_state()
|
|
148
|
+
|
|
149
|
+
def close(self):
|
|
150
|
+
if self.is_open:
|
|
151
|
+
try:
|
|
152
|
+
self._driver.close()
|
|
153
|
+
except Exception:
|
|
154
|
+
pass
|
|
155
|
+
self._driver = None
|
|
156
|
+
# Keep self._backend and self._usb_dev alive for reuse on reopen
|
|
157
|
+
self.is_open = False
|
|
158
|
+
super().close()
|
|
159
|
+
|
|
160
|
+
def _reconfigure_port(self):
|
|
161
|
+
if not self.is_open:
|
|
162
|
+
return
|
|
163
|
+
if self._baudrate != self._last_baudrate:
|
|
164
|
+
self._driver.set_baudrate(self._baudrate)
|
|
165
|
+
self._last_baudrate = self._baudrate
|
|
166
|
+
line_params = (self._bytesize, self._parity, self._stopbits)
|
|
167
|
+
if line_params != self._last_line_params:
|
|
168
|
+
self._driver.set_line_params(*line_params)
|
|
169
|
+
self._last_line_params = line_params
|
|
170
|
+
|
|
171
|
+
def _parse_url(self, url):
|
|
172
|
+
"""Parse a pyusb:// URL and return a dict of connection parameters.
|
|
173
|
+
|
|
174
|
+
Formats:
|
|
175
|
+
pyusb://host[:port]/VID:PID[?serial=X&instance=N&logging=LEVEL]
|
|
176
|
+
pyusb:///direct/VID:PID[?serial=X&instance=N&logging=LEVEL]
|
|
177
|
+
"""
|
|
178
|
+
parts = urllib.parse.urlsplit(url)
|
|
179
|
+
if parts.scheme == "usbip":
|
|
180
|
+
raise SerialException(
|
|
181
|
+
'The usbip:// scheme has been replaced by pyusb://. '
|
|
182
|
+
'Use pyusb://host[:port]/VID:PID instead of usbip://host/busid'
|
|
183
|
+
)
|
|
184
|
+
if parts.scheme != "pyusb":
|
|
185
|
+
raise SerialException(
|
|
186
|
+
f'Expected URL starting with "pyusb://", got {parts.scheme!r}'
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
query = urllib.parse.parse_qs(parts.query)
|
|
190
|
+
|
|
191
|
+
# Logging setup
|
|
192
|
+
if "logging" in query:
|
|
193
|
+
level_name = query["logging"][0].upper()
|
|
194
|
+
level = getattr(logging, level_name, None)
|
|
195
|
+
if level is not None:
|
|
196
|
+
self.logger = logging.getLogger(LOGGER_NAME)
|
|
197
|
+
self.logger.setLevel(level)
|
|
198
|
+
logging.basicConfig()
|
|
199
|
+
|
|
200
|
+
host = parts.hostname or None
|
|
201
|
+
port = parts.port or 3240
|
|
202
|
+
|
|
203
|
+
path = parts.path.lstrip("/")
|
|
204
|
+
|
|
205
|
+
# Local/direct mode: pyusb:///direct/VID:PID
|
|
206
|
+
if path.startswith("direct/"):
|
|
207
|
+
if host is not None:
|
|
208
|
+
raise SerialException(
|
|
209
|
+
f'"direct" mode cannot be used with a host: {url}'
|
|
210
|
+
)
|
|
211
|
+
host = None
|
|
212
|
+
vid_pid = path[len("direct/"):]
|
|
213
|
+
elif host is not None:
|
|
214
|
+
# Remote mode: pyusb://host/VID:PID
|
|
215
|
+
vid_pid = path
|
|
216
|
+
else:
|
|
217
|
+
raise SerialException(
|
|
218
|
+
f'Expected a host or "direct/" prefix. '
|
|
219
|
+
f'Format: pyusb://host/VID:PID or pyusb:///direct/VID:PID'
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Parse VID:PID
|
|
223
|
+
if ":" not in vid_pid:
|
|
224
|
+
raise SerialException(
|
|
225
|
+
f"Expected VID:PID in URL path, got {vid_pid!r}"
|
|
226
|
+
)
|
|
227
|
+
try:
|
|
228
|
+
vid_str, pid_str = vid_pid.split(":", 1)
|
|
229
|
+
vid = int(vid_str, 16)
|
|
230
|
+
pid = int(pid_str, 16)
|
|
231
|
+
except ValueError:
|
|
232
|
+
raise SerialException(
|
|
233
|
+
f"Invalid VID:PID {vid_pid!r} — expected hex values like 10c4:ea60"
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
serial = query.get("serial", [None])[0]
|
|
237
|
+
instance = int(query.get("instance", [0])[0])
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
"host": host,
|
|
241
|
+
"port": port,
|
|
242
|
+
"vid": vid,
|
|
243
|
+
"pid": pid,
|
|
244
|
+
"serial": serial,
|
|
245
|
+
"instance": instance,
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
# -- Reconnect handling --
|
|
249
|
+
|
|
250
|
+
# Maximum time (seconds) to wait for a device to re-enumerate after
|
|
251
|
+
# a disconnect (e.g. ESP32 resetting into bootloader mode).
|
|
252
|
+
_RECONNECT_TIMEOUT = 5.0
|
|
253
|
+
_RECONNECT_INTERVAL = 0.1
|
|
254
|
+
|
|
255
|
+
def _is_connection_error(self, exc):
|
|
256
|
+
"""Return True if the USBError indicates a broken connection."""
|
|
257
|
+
msg = str(exc).lower()
|
|
258
|
+
return any(k in msg for k in ("connection closed", "receive timeout",
|
|
259
|
+
"send failed", "receive failed"))
|
|
260
|
+
|
|
261
|
+
def _clear_backend_cache(self):
|
|
262
|
+
"""Clear cached descriptors so the driver reads fresh ones."""
|
|
263
|
+
if self._backend is not None and hasattr(self._backend, "_cache"):
|
|
264
|
+
self._backend._cache.clear()
|
|
265
|
+
|
|
266
|
+
def _reconnect_driver(self):
|
|
267
|
+
"""Rebuild the driver after a disconnect.
|
|
268
|
+
|
|
269
|
+
For USB/IP backends, the backend's _reopen() (triggered on the
|
|
270
|
+
next submit()) handles the TCP reconnect and re-import
|
|
271
|
+
automatically. We just need to rebuild the driver so it picks
|
|
272
|
+
up fresh endpoint info, then reconfigure baud/line params.
|
|
273
|
+
|
|
274
|
+
Does NOT call driver.open() or touch modem control lines -- the
|
|
275
|
+
caller (esptool reset logic) is in charge of DTR/RTS sequencing.
|
|
276
|
+
"""
|
|
277
|
+
driver_cls = self._driver_cls or type(self._driver)
|
|
278
|
+
self._clear_backend_cache()
|
|
279
|
+
deadline = time.monotonic() + self._RECONNECT_TIMEOUT
|
|
280
|
+
while True:
|
|
281
|
+
try:
|
|
282
|
+
self._driver = driver_cls(self._usb_dev)
|
|
283
|
+
self._driver.logger = self.logger
|
|
284
|
+
self._last_baudrate = None
|
|
285
|
+
self._last_line_params = None
|
|
286
|
+
self._read_buf.clear()
|
|
287
|
+
self._reconfigure_port()
|
|
288
|
+
if self.logger:
|
|
289
|
+
self.logger.info("reconnected")
|
|
290
|
+
return
|
|
291
|
+
except (usb.core.USBError, SerialException) as e:
|
|
292
|
+
if time.monotonic() >= deadline:
|
|
293
|
+
raise SerialException(
|
|
294
|
+
f"Device did not recover within "
|
|
295
|
+
f"{self._RECONNECT_TIMEOUT}s: {e}"
|
|
296
|
+
)
|
|
297
|
+
if self.logger:
|
|
298
|
+
self.logger.debug("reconnect attempt failed: %s, retrying...", e)
|
|
299
|
+
self._clear_backend_cache()
|
|
300
|
+
time.sleep(self._RECONNECT_INTERVAL)
|
|
301
|
+
|
|
302
|
+
# -- Read / Write --
|
|
303
|
+
|
|
304
|
+
def read(self, size=1):
|
|
305
|
+
if not self.is_open:
|
|
306
|
+
raise PortNotOpenError()
|
|
307
|
+
if size == 0:
|
|
308
|
+
return b""
|
|
309
|
+
|
|
310
|
+
# Convert pyserial timeout convention to USB timeout in ms.
|
|
311
|
+
# None = blocking (very long timeout), 0 = non-blocking, float = seconds
|
|
312
|
+
if self._timeout is None:
|
|
313
|
+
usb_timeout = 0 # pyusb 0 = unlimited
|
|
314
|
+
elif self._timeout == 0:
|
|
315
|
+
usb_timeout = 1 # minimal non-blocking
|
|
316
|
+
else:
|
|
317
|
+
usb_timeout = int(self._timeout * 1000)
|
|
318
|
+
|
|
319
|
+
data = bytearray()
|
|
320
|
+
# Return buffered data first
|
|
321
|
+
if self._read_buf:
|
|
322
|
+
n = min(size, len(self._read_buf))
|
|
323
|
+
data.extend(self._read_buf[:n])
|
|
324
|
+
del self._read_buf[:n]
|
|
325
|
+
|
|
326
|
+
while len(data) < size:
|
|
327
|
+
# Always read at least _USB_READ_SIZE to avoid EOVERFLOW
|
|
328
|
+
# on USB bulk endpoints.
|
|
329
|
+
read_size = max(size - len(data), self._USB_READ_SIZE)
|
|
330
|
+
try:
|
|
331
|
+
chunk = self._driver.read(read_size, timeout=usb_timeout)
|
|
332
|
+
needed = size - len(data)
|
|
333
|
+
data.extend(chunk[:needed])
|
|
334
|
+
if len(chunk) > needed:
|
|
335
|
+
self._read_buf.extend(chunk[needed:])
|
|
336
|
+
except usb.core.USBTimeoutError:
|
|
337
|
+
break
|
|
338
|
+
except usb.core.USBError as e:
|
|
339
|
+
if "timeout" in str(e).lower():
|
|
340
|
+
break
|
|
341
|
+
if self._is_connection_error(e):
|
|
342
|
+
if self.logger:
|
|
343
|
+
self.logger.info("read: connection lost, reconnecting...")
|
|
344
|
+
self._reconnect_driver()
|
|
345
|
+
continue
|
|
346
|
+
raise SerialException(f"USB read error: {e}")
|
|
347
|
+
# After first chunk, don't block waiting for more
|
|
348
|
+
if self._timeout is not None:
|
|
349
|
+
break
|
|
350
|
+
if self.logger:
|
|
351
|
+
self.logger.debug("read %d bytes: %s", len(data), data.hex())
|
|
352
|
+
return bytes(data)
|
|
353
|
+
|
|
354
|
+
def write(self, data):
|
|
355
|
+
if not self.is_open:
|
|
356
|
+
raise PortNotOpenError()
|
|
357
|
+
data = to_bytes(data)
|
|
358
|
+
if not data:
|
|
359
|
+
return 0
|
|
360
|
+
|
|
361
|
+
if self._write_timeout is None:
|
|
362
|
+
usb_timeout = 0
|
|
363
|
+
elif self._write_timeout == 0:
|
|
364
|
+
usb_timeout = 1
|
|
365
|
+
else:
|
|
366
|
+
usb_timeout = int(self._write_timeout * 1000)
|
|
367
|
+
|
|
368
|
+
if self.logger:
|
|
369
|
+
self.logger.debug("write %d bytes: %s", len(data), data.hex())
|
|
370
|
+
try:
|
|
371
|
+
return self._driver.write(data, timeout=usb_timeout)
|
|
372
|
+
except usb.core.USBError as e:
|
|
373
|
+
if self._is_connection_error(e):
|
|
374
|
+
if self.logger:
|
|
375
|
+
self.logger.info("write: connection lost, reconnecting...")
|
|
376
|
+
self._reconnect_driver()
|
|
377
|
+
return self._driver.write(data, timeout=usb_timeout)
|
|
378
|
+
raise SerialException(f"USB write error: {e}")
|
|
379
|
+
|
|
380
|
+
@property
|
|
381
|
+
def in_waiting(self):
|
|
382
|
+
if not self.is_open:
|
|
383
|
+
raise PortNotOpenError()
|
|
384
|
+
# USB bulk endpoints don't have a queryable input buffer size
|
|
385
|
+
return 0
|
|
386
|
+
|
|
387
|
+
def reset_input_buffer(self):
|
|
388
|
+
if not self.is_open:
|
|
389
|
+
raise PortNotOpenError()
|
|
390
|
+
# USB bulk endpoints don't buffer data on our side, so there's
|
|
391
|
+
# nothing to drain. Attempting a short-timeout USB read over
|
|
392
|
+
# USB/IP is expensive due to server-side timeout margins.
|
|
393
|
+
|
|
394
|
+
def reset_output_buffer(self):
|
|
395
|
+
if not self.is_open:
|
|
396
|
+
raise PortNotOpenError()
|
|
397
|
+
|
|
398
|
+
def fileno(self):
|
|
399
|
+
raise OSError(errno.ENOTTY, "pyusb serial port has no file descriptor")
|
|
400
|
+
|
|
401
|
+
def setDTR(self, state):
|
|
402
|
+
self.dtr = state
|
|
403
|
+
|
|
404
|
+
def setRTS(self, state):
|
|
405
|
+
self.rts = state
|
|
406
|
+
|
|
407
|
+
def flush(self):
|
|
408
|
+
if not self.is_open:
|
|
409
|
+
return
|
|
410
|
+
|
|
411
|
+
# -- Modem control lines --
|
|
412
|
+
|
|
413
|
+
def _update_rts_state(self):
|
|
414
|
+
if self.is_open:
|
|
415
|
+
try:
|
|
416
|
+
self._driver.set_rts(self._rts_state)
|
|
417
|
+
except usb.core.USBError as e:
|
|
418
|
+
if self._is_connection_error(e):
|
|
419
|
+
self._reconnect_driver()
|
|
420
|
+
self._driver.set_rts(self._rts_state)
|
|
421
|
+
else:
|
|
422
|
+
raise
|
|
423
|
+
|
|
424
|
+
def _update_dtr_state(self):
|
|
425
|
+
if self.is_open:
|
|
426
|
+
try:
|
|
427
|
+
self._driver.set_dtr(self._dtr_state)
|
|
428
|
+
except usb.core.USBError as e:
|
|
429
|
+
if self._is_connection_error(e):
|
|
430
|
+
self._reconnect_driver()
|
|
431
|
+
self._driver.set_dtr(self._dtr_state)
|
|
432
|
+
else:
|
|
433
|
+
raise
|
|
434
|
+
|
|
435
|
+
def _update_break_state(self):
|
|
436
|
+
if self.is_open:
|
|
437
|
+
try:
|
|
438
|
+
self._driver.set_break(self._break_state)
|
|
439
|
+
except usb.core.USBError as e:
|
|
440
|
+
if self._is_connection_error(e):
|
|
441
|
+
self._reconnect_driver()
|
|
442
|
+
self._driver.set_break(self._break_state)
|
|
443
|
+
else:
|
|
444
|
+
raise
|
|
445
|
+
|
|
446
|
+
@property
|
|
447
|
+
def pid(self):
|
|
448
|
+
if self._usb_dev is not None:
|
|
449
|
+
return self._usb_dev.idProduct
|
|
450
|
+
return None
|
|
451
|
+
|
|
452
|
+
@property
|
|
453
|
+
def vid(self):
|
|
454
|
+
if self._usb_dev is not None:
|
|
455
|
+
return self._usb_dev.idVendor
|
|
456
|
+
return None
|
|
457
|
+
|
|
458
|
+
@property
|
|
459
|
+
def cts(self):
|
|
460
|
+
if not self.is_open:
|
|
461
|
+
raise PortNotOpenError()
|
|
462
|
+
return self._driver.get_modem_status()["cts"]
|
|
463
|
+
|
|
464
|
+
@property
|
|
465
|
+
def dsr(self):
|
|
466
|
+
if not self.is_open:
|
|
467
|
+
raise PortNotOpenError()
|
|
468
|
+
return self._driver.get_modem_status()["dsr"]
|
|
469
|
+
|
|
470
|
+
@property
|
|
471
|
+
def ri(self):
|
|
472
|
+
if not self.is_open:
|
|
473
|
+
raise PortNotOpenError()
|
|
474
|
+
return self._driver.get_modem_status()["ri"]
|
|
475
|
+
|
|
476
|
+
@property
|
|
477
|
+
def cd(self):
|
|
478
|
+
if not self.is_open:
|
|
479
|
+
raise PortNotOpenError()
|
|
480
|
+
return self._driver.get_modem_status()["cd"]
|
|
File without changes
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Pyserial URL handler for pyusb:// scheme.
|
|
2
|
+
|
|
3
|
+
Register this handler by calling serial_pyusb.register() or by
|
|
4
|
+
appending 'serial_pyusb.urlhandler' to serial.protocol_handler_packages
|
|
5
|
+
before calling serial.serial_for_url().
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from serial_pyusb.serial import Serial
|
|
9
|
+
|
|
10
|
+
__all__ = ["Serial"]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Pyserial URL handler for usbip:// scheme.
|
|
2
|
+
|
|
3
|
+
Register this handler by calling serial_pyusb.register() or by
|
|
4
|
+
appending 'serial_pyusb.urlhandler' to serial.protocol_handler_packages
|
|
5
|
+
before calling serial.serial_for_url().
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from serial_pyusb.serial import Serial
|
|
9
|
+
|
|
10
|
+
__all__ = ["Serial"]
|