Skip to content

Commit 12f0019

Browse files
Merge pull request #73 from michealroberts/fix/tmc/USBTMCCommonInterface
fix: amend USBTMCCommonInterface in tmc in samps module
2 parents 67ea1c3 + e18d795 commit 12f0019

File tree

2 files changed

+25
-125
lines changed

2 files changed

+25
-125
lines changed

src/samps/tmc.py

Lines changed: 24 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,11 @@
88
import os
99
from errno import EAGAIN, EINTR, EINVAL, ENOTTY, EWOULDBLOCK
1010
from fcntl import ioctl
11-
from select import select
1211
from struct import pack
1312
from types import TracebackType
1413
from typing import Optional, Type, TypedDict
1514

1615
from .errors import SerialReadError, SerialWriteError
17-
from .handlers import ReadTimeoutHandler
1816
from .utilities import no_op
1917

2018
# **************************************************************************************
@@ -156,61 +154,19 @@ def read(self, size: int = 1) -> bytes:
156154
if size == 0:
157155
return b""
158156

159-
# Initialize a bytearray to accumulate incoming data:
160-
read: bytearray = bytearray()
161-
162-
# Convert timeout from seconds to milliseconds, as required by ReadTimeoutHandler:
163-
timer = ReadTimeoutHandler(timeout=self._timeout * 1000)
164-
165-
timer.start()
166-
167-
# Continue reading until we have collected the requested number of bytes
168-
# or until the overall timeout period has elapsed.
169-
while len(read) < size:
170-
# Check if the timeout has expired:
171-
if timer.has_expired():
172-
raise SerialReadError(
173-
f"Read timeout after {self._timeout}s, got {len(read)}/{size} bytes"
174-
)
175-
176-
# Wait for readability with the remaining time budget
177-
remaining = timer.remaining()
157+
try:
158+
# Blocking read call to read from the USBTMC device:
159+
data: bytes = os.read(self._fd, size)
160+
except OSError as e:
161+
raise SerialReadError(f"Reading from USBTMC device failed: {e}") from e
178162

179-
r, _, _ = select(
180-
[self._fd],
181-
[],
182-
[],
183-
max(0.0, min(self._timeout, (remaining or 0.0) / 1000.0)),
163+
# If the port was ready but returned no data, treat it as a disconnection.
164+
if not data:
165+
raise SerialReadError(
166+
"The device reported readiness to read but returned no data."
184167
)
185168

186-
if not r:
187-
raise SerialReadError(
188-
f"Read timeout after {self._timeout}s (no data ready)"
189-
)
190-
191-
try:
192-
chunk: bytes = os.read(self._fd, size - len(read))
193-
except OSError as e:
194-
# Retry on non-fatal errors and propagate others upwards:
195-
if e.errno in (
196-
EAGAIN,
197-
EWOULDBLOCK,
198-
EINTR,
199-
):
200-
continue
201-
raise SerialReadError(f"Reading from USBTMC device failed: {e}") from e
202-
203-
# If the port was ready but returned no data, treat it as a disconnection.
204-
if not chunk:
205-
raise SerialReadError(
206-
"The device reported readiness to read but returned no data."
207-
)
208-
209-
# If the chunk read was successful, append it to the data:
210-
read.extend(chunk)
211-
212-
# Finally, return the accumulated data:
213-
return bytes(read)
169+
return data
214170

215171
def readline(self, eol: bytes = b"\n", maximum_bytes: int = -1) -> bytes:
216172
"""
@@ -231,65 +187,36 @@ def readline(self, eol: bytes = b"\n", maximum_bytes: int = -1) -> bytes:
231187
# Initialize a bytearray to accumulate incoming data:
232188
read: bytearray = bytearray()
233189

234-
# Convert timeout from seconds to milliseconds, as required by ReadTimeoutHandler:
235-
timer = ReadTimeoutHandler(timeout=self._timeout * 1000)
236-
237-
timer.start()
238-
239190
# Determine how many bytes to read in this chunk:
240191
chunk_size = 1024
241192

242-
# Continue reading until we have collected the requested number of bytes
243-
# or until the overall timeout period has elapsed:
244193
while True:
245194
# Check if we have read enough bytes to satisfy max_bytes:
246195
if maximum_bytes > 0 and len(read) >= maximum_bytes:
247-
break
196+
# We have reached the maximum allowed bytes; return what we have:
197+
return bytes(read)
248198

249-
# Check if the timeout has expired:
250-
if timer.has_expired():
251-
raise SerialReadError(
252-
f"Read timeout after {self._timeout}s, got {len(read)} bytes"
253-
)
254-
255-
# Wait for readability with the remaining time budget:
256-
remaining = timer.remaining()
257-
258-
r, _, _ = select(
259-
[self._fd],
260-
[],
261-
[],
262-
max(0.0, min(self._timeout, (remaining or 0.0) / 1000.0)),
263-
)
199+
# If maximum_bytes is set, do not read past it:
200+
if maximum_bytes > 0:
201+
remaining = maximum_bytes - len(read)
264202

265-
# If no file descriptors are ready, return partial data if any is available:
266-
if not r:
267-
if len(read) > 0:
203+
# If the remaining bytes is less than or equal to zero, return what we have:
204+
if remaining <= 0:
268205
return bytes(read)
269206

270-
raise SerialReadError(
271-
f"Read timeout after {self._timeout}s (no data ready)"
272-
)
273-
274-
# Limit read size if maximum_bytes is used:
275-
if maximum_bytes > 0:
276-
chunk_size = min(chunk_size, maximum_bytes - len(read))
207+
# Adjust chunk size to not exceed maximum_bytes:
208+
chunk_size = min(1024, remaining)
277209

278210
try:
211+
# Blocking read call to read from the USBTMC device:
279212
chunk: bytes = os.read(self._fd, chunk_size)
280213
except OSError as e:
281-
# Retry on non-fatal errors and propagate others upwards:
282-
if e.errno in (
283-
EAGAIN,
284-
EWOULDBLOCK,
285-
EINTR,
286-
):
287-
continue
288-
raise SerialReadError(f"Reading from USBTMC device failed: {e}")
214+
raise SerialReadError(f"Reading from USBTMC device failed: {e}") from e
289215

290-
# If the port was ready but returned no data, return partial data if any:
216+
# If the port returned no data at all:
291217
if not chunk:
292218
if len(read) > 0:
219+
# Return whatever we have so far.
293220
return bytes(read)
294221
raise SerialReadError(
295222
"The device reported readiness to read but returned no data."
@@ -303,9 +230,6 @@ def readline(self, eol: bytes = b"\n", maximum_bytes: int = -1) -> bytes:
303230
index = read.index(eol) + len(eol)
304231
return bytes(read[:index])
305232

306-
# Finally, return the accumulated data:
307-
return bytes(read)
308-
309233
def write(self, data: bytes) -> int:
310234
"""
311235
Write all of `data` to the USBTMC device, retrying on transient errors.
@@ -338,14 +262,8 @@ def write(self, data: bytes) -> int:
338262
try:
339263
n = os.write(self._fd, data[written:])
340264
except OSError as e:
341-
# Retry on transient POSIX errors:
265+
# Retry on transient POSIX errors for a blocking descriptor:
342266
if e.errno in (EAGAIN, EWOULDBLOCK, EINTR):
343-
_, w, _ = select([], [self._fd], [], self._timeout)
344-
345-
if not w:
346-
raise SerialWriteError(
347-
f"Write timeout after {self._timeout}s (not writable)"
348-
)
349267
continue
350268
raise SerialWriteError(f"Writing to USBTMC device failed: {e}") from e
351269

test/test_tmc.py

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
# **************************************************************************************
77

88
import os
9-
import time
109
import unittest
1110
import unittest.mock
1211
from errno import EINVAL
@@ -116,7 +115,7 @@ def test_ask(self) -> None:
116115
Test the ask method for write followed by read:
117116
"""
118117
query = b"hello-world\n"
119-
reply = b"echo: hello-world\n"
118+
reply = b"hello-world\n"
120119

121120
# Write the reply from the master side so that readline() can consume it:
122121
os.write(self.master_file_descriptor, reply)
@@ -170,23 +169,6 @@ def fake_write(fd: int, buf: bytes) -> int:
170169
)
171170
self.assertEqual(received, data)
172171

173-
def test_read_timeout_raises(self) -> None:
174-
"""
175-
Test that read raises a SerialReadError after the timeout expires:
176-
"""
177-
short_serial = USBTMCCommonInterface(
178-
port=self.slave_device_name,
179-
params={
180-
"timeout": 0.1,
181-
},
182-
)
183-
short_serial.open()
184-
start_time = time.time()
185-
with self.assertRaises(SerialReadError):
186-
short_serial.read(1)
187-
self.assertGreaterEqual(time.time() - start_time, 0.1)
188-
short_serial.close()
189-
190172
def test_constructor_without_params_uses_defaults(self) -> None:
191173
"""
192174
Test that omitting params falls back to the default parameters:

0 commit comments

Comments
 (0)