riden 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.
riden-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vyacheslav Mayorov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
riden-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,168 @@
1
+ Metadata-Version: 2.4
2
+ Name: riden
3
+ Version: 0.1.0
4
+ Summary: Synchronous Python client for Riden RD60xx power supplies (USB serial and Bluetooth)
5
+ Keywords: riden,rd6006,rd6012,rd6018,rk6006,rk6012,rk6018,power-supply,modbus,bluetooth,serial
6
+ Author: Vyacheslav Mayorov
7
+ Author-email: Vyacheslav Mayorov <vyacheslav.mayorov@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Programming Language :: Python :: 3.15
16
+ Classifier: Topic :: System :: Hardware
17
+ Classifier: Typing :: Typed
18
+ Requires-Dist: minimalmodbus>=2.1.1
19
+ Requires-Dist: pyserial>=3.5
20
+ Requires-Python: >=3.14
21
+ Project-URL: Homepage, https://github.com/acc15/riden
22
+ Project-URL: Repository, https://github.com/acc15/riden
23
+ Project-URL: Issues, https://github.com/acc15/riden/issues
24
+ Description-Content-Type: text/markdown
25
+
26
+ # riden
27
+
28
+ A simple synchronous, single-threaded client for Riden laboratory power
29
+ supplies (RD6006, RD6006P, RD6012, RD6012P, RD6018, RD6024, RK6006).
30
+
31
+ It speaks Modbus RTU over a regular serial port or Bluetooth SPP (RFCOMM) on
32
+ Linux.
33
+
34
+ ## Features
35
+
36
+ - synchronous API with no background event loop:
37
+ `with Riden.connect(...) as rd: ...`;
38
+ - Bluetooth on Linux via `bt://` URLs (`AF_BLUETOOTH` + RFCOMM);
39
+ - batched I/O: fields are read and written in one Modbus transaction per
40
+ contiguous register block, not one transaction per field;
41
+ - automatic model and voltage/current scale detection on connect;
42
+ - ready-made transport stack: transaction pacing, keepalive, reconnect, and
43
+ logging.
44
+
45
+ ## Requirements
46
+
47
+ - Python 3.14 or newer;
48
+ - Linux with BlueZ for Bluetooth SPP;
49
+ - any OS supported by pyserial for USB serial.
50
+
51
+ ## Install
52
+
53
+ ```sh
54
+ uv add riden
55
+ ```
56
+
57
+ or with pip:
58
+
59
+ ```sh
60
+ pip install riden
61
+ ```
62
+
63
+ From source:
64
+
65
+ ```sh
66
+ git clone https://github.com/acc15/riden
67
+ cd riden
68
+ uv sync
69
+ ```
70
+
71
+ ## Quick start
72
+
73
+ ```python
74
+ from riden import Riden
75
+
76
+ with Riden.connect("bt://E2:F9:C6:24:71:C8?channel=1&timeout=0.5") as rd:
77
+ print(rd.status())
78
+
79
+ # Several fields — a single device transaction.
80
+ v, i, p = rd.read("v", "i", "p").values()
81
+ print(v, i, p)
82
+
83
+ # Several fields — a single write transaction.
84
+ rd.write(v_set=5.0, i_set=0.1, en=True)
85
+ ```
86
+
87
+ For USB serial, pass the device path:
88
+
89
+ ```python
90
+ with Riden.connect("/dev/ttyUSB0") as rd:
91
+ ...
92
+ ```
93
+
94
+ Field names are case-insensitive, so `"V"` and `"v"` refer to the same field.
95
+
96
+ ## Example: voltage sweep + status
97
+
98
+ ```python
99
+ import time
100
+
101
+ from riden import Riden
102
+
103
+ with Riden.connect("bt://E2:F9:C6:24:71:C8?channel=1&timeout=0.5") as rd:
104
+ try:
105
+ rd.write(v_set=0.0, i_set=0.1, en=True)
106
+
107
+ for voltage in range(0, 60, 5):
108
+ rd.write(v_set=float(voltage))
109
+ time.sleep(2)
110
+ print(rd.status())
111
+ finally:
112
+ rd.write(v_set=0.0, en=False)
113
+ ```
114
+
115
+ ## Bluetooth on Linux
116
+
117
+ Pair and trust the device first (`bluetoothctl pair/trust`); the RFCOMM
118
+ channel is usually `1`. URL format:
119
+
120
+ ```text
121
+ bt://AA:BB:CC:DD:EE:FF?channel=1&timeout=0.5
122
+ ```
123
+
124
+ `timeout` and `connect_timeout` are specified in seconds as query parameters.
125
+ `BtSerial` is registered as a pyserial URL handler, so `bt://` works anywhere a
126
+ regular serial port is accepted.
127
+
128
+ ## Performance
129
+
130
+ `read()` selects the requested fields, computes their overall address range,
131
+ and issues **one** `read_registers` call for the whole range, then decodes the
132
+ raw registers into fields. `write()` encodes and validates all values first,
133
+ merges adjacent fields into contiguous blocks, and sends one transaction per
134
+ block.
135
+
136
+ Therefore:
137
+
138
+ ```python
139
+ # 1 transaction instead of 3
140
+ v, i, p = rd.read("v", "i", "p").values()
141
+
142
+ # 1 transaction instead of 3
143
+ rd.write(v_set=5.0, i_set=0.1, en=True)
144
+ ```
145
+
146
+ Sparse fields are still read in one transaction spanning the whole range
147
+ between them, so grouping nearby fields is preferable.
148
+
149
+ ## Development
150
+
151
+ ```sh
152
+ uv sync
153
+ uv build # sdist + wheel in dist/
154
+ just check # ty + ruff + pytest
155
+ ```
156
+
157
+ Scripts in `scripts/` are run through `uv run`:
158
+
159
+ ```sh
160
+ uv run scripts/v_sweep.py
161
+ uv run scripts/link_test.py "bt://E2:F9:C6:24:71:C8?channel=1&timeout=0.5"
162
+ ```
163
+
164
+ `just --list` shows the remaining tasks.
165
+
166
+ ## License
167
+
168
+ MIT — see [LICENSE](https://github.com/acc15/riden/blob/master/LICENSE).
riden-0.1.0/README.md ADDED
@@ -0,0 +1,143 @@
1
+ # riden
2
+
3
+ A simple synchronous, single-threaded client for Riden laboratory power
4
+ supplies (RD6006, RD6006P, RD6012, RD6012P, RD6018, RD6024, RK6006).
5
+
6
+ It speaks Modbus RTU over a regular serial port or Bluetooth SPP (RFCOMM) on
7
+ Linux.
8
+
9
+ ## Features
10
+
11
+ - synchronous API with no background event loop:
12
+ `with Riden.connect(...) as rd: ...`;
13
+ - Bluetooth on Linux via `bt://` URLs (`AF_BLUETOOTH` + RFCOMM);
14
+ - batched I/O: fields are read and written in one Modbus transaction per
15
+ contiguous register block, not one transaction per field;
16
+ - automatic model and voltage/current scale detection on connect;
17
+ - ready-made transport stack: transaction pacing, keepalive, reconnect, and
18
+ logging.
19
+
20
+ ## Requirements
21
+
22
+ - Python 3.14 or newer;
23
+ - Linux with BlueZ for Bluetooth SPP;
24
+ - any OS supported by pyserial for USB serial.
25
+
26
+ ## Install
27
+
28
+ ```sh
29
+ uv add riden
30
+ ```
31
+
32
+ or with pip:
33
+
34
+ ```sh
35
+ pip install riden
36
+ ```
37
+
38
+ From source:
39
+
40
+ ```sh
41
+ git clone https://github.com/acc15/riden
42
+ cd riden
43
+ uv sync
44
+ ```
45
+
46
+ ## Quick start
47
+
48
+ ```python
49
+ from riden import Riden
50
+
51
+ with Riden.connect("bt://E2:F9:C6:24:71:C8?channel=1&timeout=0.5") as rd:
52
+ print(rd.status())
53
+
54
+ # Several fields — a single device transaction.
55
+ v, i, p = rd.read("v", "i", "p").values()
56
+ print(v, i, p)
57
+
58
+ # Several fields — a single write transaction.
59
+ rd.write(v_set=5.0, i_set=0.1, en=True)
60
+ ```
61
+
62
+ For USB serial, pass the device path:
63
+
64
+ ```python
65
+ with Riden.connect("/dev/ttyUSB0") as rd:
66
+ ...
67
+ ```
68
+
69
+ Field names are case-insensitive, so `"V"` and `"v"` refer to the same field.
70
+
71
+ ## Example: voltage sweep + status
72
+
73
+ ```python
74
+ import time
75
+
76
+ from riden import Riden
77
+
78
+ with Riden.connect("bt://E2:F9:C6:24:71:C8?channel=1&timeout=0.5") as rd:
79
+ try:
80
+ rd.write(v_set=0.0, i_set=0.1, en=True)
81
+
82
+ for voltage in range(0, 60, 5):
83
+ rd.write(v_set=float(voltage))
84
+ time.sleep(2)
85
+ print(rd.status())
86
+ finally:
87
+ rd.write(v_set=0.0, en=False)
88
+ ```
89
+
90
+ ## Bluetooth on Linux
91
+
92
+ Pair and trust the device first (`bluetoothctl pair/trust`); the RFCOMM
93
+ channel is usually `1`. URL format:
94
+
95
+ ```text
96
+ bt://AA:BB:CC:DD:EE:FF?channel=1&timeout=0.5
97
+ ```
98
+
99
+ `timeout` and `connect_timeout` are specified in seconds as query parameters.
100
+ `BtSerial` is registered as a pyserial URL handler, so `bt://` works anywhere a
101
+ regular serial port is accepted.
102
+
103
+ ## Performance
104
+
105
+ `read()` selects the requested fields, computes their overall address range,
106
+ and issues **one** `read_registers` call for the whole range, then decodes the
107
+ raw registers into fields. `write()` encodes and validates all values first,
108
+ merges adjacent fields into contiguous blocks, and sends one transaction per
109
+ block.
110
+
111
+ Therefore:
112
+
113
+ ```python
114
+ # 1 transaction instead of 3
115
+ v, i, p = rd.read("v", "i", "p").values()
116
+
117
+ # 1 transaction instead of 3
118
+ rd.write(v_set=5.0, i_set=0.1, en=True)
119
+ ```
120
+
121
+ Sparse fields are still read in one transaction spanning the whole range
122
+ between them, so grouping nearby fields is preferable.
123
+
124
+ ## Development
125
+
126
+ ```sh
127
+ uv sync
128
+ uv build # sdist + wheel in dist/
129
+ just check # ty + ruff + pytest
130
+ ```
131
+
132
+ Scripts in `scripts/` are run through `uv run`:
133
+
134
+ ```sh
135
+ uv run scripts/v_sweep.py
136
+ uv run scripts/link_test.py "bt://E2:F9:C6:24:71:C8?channel=1&timeout=0.5"
137
+ ```
138
+
139
+ `just --list` shows the remaining tasks.
140
+
141
+ ## License
142
+
143
+ MIT — see [LICENSE](https://github.com/acc15/riden/blob/master/LICENSE).
@@ -0,0 +1,65 @@
1
+ [project]
2
+ name = "riden"
3
+ version = "0.1.0"
4
+ description = "Synchronous Python client for Riden RD60xx power supplies (USB serial and Bluetooth)"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.14"
9
+ keywords = [
10
+ "riden",
11
+ "rd6006",
12
+ "rd6012",
13
+ "rd6018",
14
+ "rk6006",
15
+ "rk6012",
16
+ "rk6018",
17
+ "power-supply",
18
+ "modbus",
19
+ "bluetooth",
20
+ "serial",
21
+ ]
22
+ classifiers = [
23
+ "Development Status :: 4 - Beta",
24
+ "Intended Audience :: Developers",
25
+ "Operating System :: OS Independent",
26
+ "Programming Language :: Python :: 3",
27
+ "Programming Language :: Python :: 3.14",
28
+ "Programming Language :: Python :: 3.15",
29
+ "Topic :: System :: Hardware",
30
+ "Typing :: Typed",
31
+ ]
32
+ dependencies = [
33
+ "minimalmodbus>=2.1.1",
34
+ "pyserial>=3.5",
35
+ ]
36
+
37
+ [[project.authors]]
38
+ name = "Vyacheslav Mayorov"
39
+ email = "vyacheslav.mayorov@gmail.com"
40
+
41
+ [project.urls]
42
+ Homepage = "https://github.com/acc15/riden"
43
+ Repository = "https://github.com/acc15/riden"
44
+ Issues = "https://github.com/acc15/riden/issues"
45
+
46
+ [tool.ruff]
47
+ src = ["src"]
48
+ target-version = "py314"
49
+
50
+ [tool.ty.rules]
51
+ all = "error"
52
+
53
+ [tool.pytest.ini_options]
54
+ testpaths = ["tests"]
55
+
56
+ [build-system]
57
+ requires = ["uv_build>=0.13.7,<0.14.0"]
58
+ build-backend = "uv_build"
59
+
60
+ [dependency-groups]
61
+ dev = [
62
+ "pytest>=9.2.0",
63
+ "ruff>=0.17.4",
64
+ "ty>=0.0.100",
65
+ ]
@@ -0,0 +1,64 @@
1
+ [project]
2
+ name = "riden"
3
+ version = "0.1.0"
4
+ description = "Synchronous Python client for Riden RD60xx power supplies (USB serial and Bluetooth)"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Vyacheslav Mayorov", email = "vyacheslav.mayorov@gmail.com" }
8
+ ]
9
+ license = "MIT"
10
+ license-files = ["LICENSE"]
11
+ requires-python = ">=3.14"
12
+ keywords = [
13
+ "riden",
14
+ "rd6006",
15
+ "rd6012",
16
+ "rd6018",
17
+ "rk6006",
18
+ "rk6012",
19
+ "rk6018",
20
+ "power-supply",
21
+ "modbus",
22
+ "bluetooth",
23
+ "serial",
24
+ ]
25
+ classifiers = [
26
+ "Development Status :: 4 - Beta",
27
+ "Intended Audience :: Developers",
28
+ "Operating System :: OS Independent",
29
+ "Programming Language :: Python :: 3",
30
+ "Programming Language :: Python :: 3.14",
31
+ "Programming Language :: Python :: 3.15",
32
+ "Topic :: System :: Hardware",
33
+ "Typing :: Typed",
34
+ ]
35
+ dependencies = [
36
+ "minimalmodbus>=2.1.1",
37
+ "pyserial>=3.5",
38
+ ]
39
+
40
+ [project.urls]
41
+ Homepage = "https://github.com/acc15/riden"
42
+ Repository = "https://github.com/acc15/riden"
43
+ Issues = "https://github.com/acc15/riden/issues"
44
+
45
+ [tool.ruff]
46
+ src = ["src"]
47
+ target-version = "py314"
48
+
49
+ [tool.ty.rules]
50
+ all = "error"
51
+
52
+ [tool.pytest.ini_options]
53
+ testpaths = ["tests"]
54
+
55
+ [build-system]
56
+ requires = ["uv_build>=0.13.7,<0.14.0"]
57
+ build-backend = "uv_build"
58
+
59
+ [dependency-groups]
60
+ dev = [
61
+ "pytest>=9.2.0",
62
+ "ruff>=0.17.4",
63
+ "ty>=0.0.100",
64
+ ]
@@ -0,0 +1,26 @@
1
+ import serial
2
+
3
+ from .bt import BtSerial
4
+ from .enums import Mode, Protection
5
+ from .field import Field
6
+ from .riden import Riden
7
+ from .transport import Delayed, KeepAlive, Logging, Modbus, Reconnectable, Transport
8
+
9
+ HANDLER_PACKAGE = "riden.bt"
10
+
11
+ if HANDLER_PACKAGE not in serial.protocol_handler_packages:
12
+ serial.protocol_handler_packages.append(HANDLER_PACKAGE)
13
+
14
+ __all__ = [
15
+ "BtSerial",
16
+ "Delayed",
17
+ "Field",
18
+ "KeepAlive",
19
+ "Logging",
20
+ "Modbus",
21
+ "Mode",
22
+ "Protection",
23
+ "Reconnectable",
24
+ "Riden",
25
+ "Transport",
26
+ ]
@@ -0,0 +1,5 @@
1
+ from .bt_serial import BtSerial
2
+
3
+ __all__ = [
4
+ "BtSerial"
5
+ ]
@@ -0,0 +1,204 @@
1
+ """Bluetooth SPP transport shaped like a pyserial port.
2
+
3
+ BtSerial.port is always a Bluetooth MAC, just as serial.Serial.port is a
4
+ device path. URL parsing lives only in serial_urlhandler/protocol_bt.py,
5
+ which pyserial uses via serial_for_url():
6
+
7
+ serial.serial_for_url("bt://E2:F9:C6:24:71:C8?channel=1&timeout=0.5")
8
+
9
+ Direct construction uses the MAC:
10
+
11
+ BtSerial("E2:F9:C6:24:71:C8", channel=1, timeout=0.5)
12
+
13
+ The connect timeout is an internal constant (CONNECT_TIMEOUT), not part of the
14
+ serial interface. For a configured but closed object use
15
+ serial_for_url(..., do_not_open=True), or BtSerial(None) and set .port before
16
+ open().
17
+ """
18
+ import contextlib
19
+ import re
20
+ import select
21
+ import socket
22
+ import time
23
+ from typing import override
24
+
25
+ import serial
26
+
27
+ # Fixed for minimalmodbus; RFCOMM has no real baudrate.
28
+ BAUDRATE = 115200
29
+
30
+ _MAC_RE = re.compile(r"^(?:[0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}$")
31
+
32
+ def parse_mac(value: str) -> str:
33
+ value = value.strip()
34
+ if not _MAC_RE.match(value):
35
+ raise ValueError(f"invalid Bluetooth MAC: {value!r}")
36
+ return value.replace("-", ":").upper()
37
+
38
+
39
+ class BtSerial(contextlib.AbstractContextManager["BtSerial"]):
40
+
41
+ def __init__(self,
42
+ port: str | None = None,
43
+ channel: int = 1,
44
+ timeout: float | None = 0.5,
45
+ connect_timeout: float | None = 10
46
+ ):
47
+ self.port = port
48
+ self.channel = channel
49
+ self.sock: socket.socket | None = None
50
+ self._buf = bytearray()
51
+ self.timeout = timeout
52
+ self.connect_timeout = connect_timeout
53
+ if port is not None:
54
+ self.open()
55
+
56
+ @property
57
+ def timeout(self) -> float | None:
58
+ return self._timeout
59
+
60
+ @timeout.setter
61
+ def timeout(self, value: float | None) -> None:
62
+ if value is not None:
63
+ try:
64
+ value + 1 # numeric check, like pyserial
65
+ except TypeError:
66
+ raise ValueError(f"Not a valid timeout: {value!r}") from None
67
+ if value < 0:
68
+ raise ValueError(f"Not a valid timeout: {value!r}")
69
+ self._timeout = value
70
+ if self.sock is not None:
71
+ self.sock.settimeout(value)
72
+
73
+ @property
74
+ def baudrate(self) -> int:
75
+ # minimalmodbus reads this to calculate the 3.5-character silent
76
+ # period; RFCOMM itself has no baudrate setting.
77
+ return BAUDRATE
78
+
79
+ @property
80
+ def is_open(self) -> bool:
81
+ return self.sock is not None
82
+
83
+ # -- pyserial-compatible lifecycle ------------------------------------
84
+ def ensure_opened(self) -> socket.socket:
85
+ sock = self.sock
86
+ if sock is None:
87
+ raise serial.SerialException("Bluetooth socket is closed")
88
+ return sock
89
+
90
+ def open(self) -> None:
91
+ if self.is_open:
92
+ raise serial.SerialException("Port is already open.")
93
+ if self.port is None:
94
+ raise serial.SerialException("Port must be configured before it can be used.")
95
+
96
+ try:
97
+ mac = parse_mac(str(self.port))
98
+ except ValueError as exc:
99
+ raise serial.SerialException(str(exc)) from exc
100
+ channel = int(self.channel)
101
+ if not 1 <= channel <= 30:
102
+ raise ValueError(f"Bluetooth channel must be 1..30, got {channel}")
103
+
104
+ sock = socket.socket(
105
+ socket.AF_BLUETOOTH, socket.SOCK_STREAM, socket.BTPROTO_RFCOMM
106
+ )
107
+ try:
108
+ sock.settimeout(self.connect_timeout)
109
+ sock.connect((mac, channel))
110
+ sock.settimeout(self.timeout)
111
+ except Exception:
112
+ sock.close()
113
+ raise
114
+
115
+ self.sock = sock
116
+ self.mac = mac
117
+ self.channel = channel
118
+ self._buf.clear()
119
+
120
+ def close(self) -> None:
121
+ sock = self.sock
122
+ self.sock = None
123
+ if sock is not None:
124
+ sock.close()
125
+
126
+ def __del__(self):
127
+ with contextlib.suppress(Exception):
128
+ self.close()
129
+
130
+ @override
131
+ def __exit__(self, *_):
132
+ self.close()
133
+
134
+ # -- pyserial-like I/O ------------------------------------------------
135
+ def write(self, data: bytes) -> int:
136
+ sock = self.ensure_opened()
137
+ sock.sendall(bytes(data))
138
+ return len(data)
139
+
140
+ def read(self, size: int = 1) -> bytes:
141
+ sock = self.ensure_opened()
142
+ if size <= 0:
143
+ return b""
144
+
145
+ if self.timeout is None:
146
+ # pyserial semantics: None means blocking until size bytes or EOF.
147
+ while len(self._buf) < size:
148
+ chunk = sock.recv(size - len(self._buf))
149
+ if not chunk:
150
+ raise serial.SerialException("Bluetooth peer closed the link")
151
+ self._buf.extend(chunk)
152
+ out = bytes(self._buf[:size])
153
+ del self._buf[:size]
154
+ return out
155
+
156
+ if self.timeout == 0:
157
+ # pyserial semantics: return immediately with what is available.
158
+ try:
159
+ chunk = sock.recv(size - len(self._buf), socket.MSG_DONTWAIT)
160
+ if chunk:
161
+ self._buf.extend(chunk)
162
+ except BlockingIOError:
163
+ pass
164
+ except OSError as exc:
165
+ raise serial.SerialException(str(exc)) from exc
166
+ out = bytes(self._buf[:size])
167
+ del self._buf[:size]
168
+ return out
169
+
170
+ # timeout > 0: the timeout is for the whole call (pyserial semantics).
171
+ # select() waits for readability with the time left, so the socket
172
+ # timeout is never touched and recv() cannot block past the deadline.
173
+ deadline = time.monotonic() + self.timeout
174
+ while len(self._buf) < size:
175
+ remaining = deadline - time.monotonic()
176
+ if remaining <= 0:
177
+ break
178
+ if not select.select([sock], [], [], remaining)[0]:
179
+ break
180
+ chunk = sock.recv(size - len(self._buf))
181
+ if not chunk:
182
+ raise serial.SerialException("Bluetooth peer closed the link")
183
+ self._buf.extend(chunk)
184
+
185
+ out = bytes(self._buf[:size])
186
+ del self._buf[:size]
187
+ return out
188
+
189
+ def reset_input_buffer(self) -> None:
190
+ sock = self.ensure_opened()
191
+ self._buf.clear()
192
+ # Zero-timeout poll: recv() after a readable select() returns at once,
193
+ # so the socket timeout is never touched.
194
+ while select.select([sock], [], [], 0)[0]:
195
+ if not sock.recv(4096):
196
+ break
197
+
198
+ def reset_output_buffer(self) -> None:
199
+ pass
200
+
201
+ def flush(self) -> None:
202
+ pass
203
+
204
+