Skip to content

Commit 21bb5ad

Browse files
committed
fix: support firmware that terminates streaming events with bare CR
Newer DwarfG2 v2 firmware (1.99.5+) terminates +CINV continuous-inventory events with a bare \r (no LF), while readuntil(b"\n") would block indefinitely waiting for an LF that never arrives. The watchdog would then trip after 5s and the reader would never produce inventory events in practice. Changes: - SerialConnection._work: replace fixed \n delimiter with byte-by-byte line splitter that accepts \r, \n, or \r\n. - _send_command: accumulate response lines as a list (each line now arrives as its own _recv call instead of one blob to split on \r). - _send_command (echo path): when a failed command is not echoed, drain the trailing ERROR terminator and surface the parsed +ERR:/+CMD: error frame instead of leaking it into the next command's buffer. - start_inventory: default timeout 4.0 -> 5.0. - _check_connection: poll every 100ms while inventory is active so the watchdog deadline is honoured precisely instead of padded by the general 1s connection-check interval. Reword reset messages to "continuous inventory timeout - self reset" / "connection test failed - self reset" so they no longer suggest a hardware fault.
1 parent 93b51b3 commit 21bb5ad

5 files changed

Lines changed: 67 additions & 21 deletions

File tree

metratec_rfid/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
# from .connection.serial_connection import SerialConnection
99
# from .connection.socket_connection import SocketConnection
1010

11-
__version__ = "1.4.4"
11+
__version__ = "1.4.5"
1212
__email__ = "neumann@metratec.com"
1313
__license__ = "MIT License"
1414
__author__ = "Matthias Neumann"

metratec_rfid/connection/serial_connection.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -160,14 +160,28 @@ async def _work(self) -> None:
160160
while self._is_started:
161161
if not self._writer:
162162
await self._connect()
163+
# Accumulate bytes and split on any of \r, \n, or \r\n. Some firmware
164+
# versions terminate continuous-inventory events with bare \r (no LF),
165+
# so a fixed \n delimiter would block forever waiting for an LF that
166+
# never arrives.
167+
line_buffer = bytearray()
163168
while self._writer:
164169
# disable Catching too general exception Exception - pylint: disable=W0703
165170
try:
166171
while self._reader:
167-
msg: bytes = await self._reader.readuntil(self._separator_encoded)
168-
# self._logger.debug("data received (config) %s",
169-
# msg.decode().replace("\r", "<CR>").replace("\n", "<LF>"))
170-
self.data_received(msg[:-1])
172+
chunk: bytes = await self._reader.read(4096)
173+
if not chunk:
174+
# peer closed; let outer loop reconnect
175+
break
176+
for byte in chunk:
177+
if byte in (0x0d, 0x0a): # \r or \n -> end of line
178+
if line_buffer:
179+
# Append \r to keep the legacy contract:
180+
# downstream handlers strip one trailing byte.
181+
self.data_received(bytes(line_buffer) + b'\r')
182+
line_buffer.clear()
183+
else:
184+
line_buffer.append(byte)
171185
except serial.SerialException as err:
172186
# print("exception consumed")
173187
try:

metratec_rfid/reader.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -456,11 +456,18 @@ def _set_last_inventory_time(self, last_inventory_time: float):
456456
async def _check_connection(self) -> None:
457457
"""Check the connection - reconnect the device if no messages have been received for a while
458458
"""
459+
# Tight polling while continuous inventory is active so the watchdog
460+
# fires at the configured deadline (not up to one full check_interval later).
461+
inventory_check_interval = 0.1
459462
while self.get_status()['status'] >= 1:
460-
await asyncio.sleep(self._connection_check_interval)
463+
sleep_time = (
464+
inventory_check_interval if self._continuous_inventory_check_time
465+
else self._connection_check_interval
466+
)
467+
await asyncio.sleep(sleep_time)
461468
if self._continuous_inventory_check_time:
462469
if self._last_inventory_time + self._continuous_inventory_check_time < time():
463-
msg = "continuous inventory problem - reset"
470+
msg = "continuous inventory timeout - self reset"
464471
break
465472
if self._last_message_time + self._connection_check_time >= time():
466473
continue
@@ -472,9 +479,9 @@ async def _check_connection(self) -> None:
472479
# pylint: disable=broad-exception-caught
473480
except Exception as err:
474481
self._logger.debug("connection check fails - %s", err)
475-
msg = 'connection lost'
482+
msg = 'connection test failed - self reset'
476483
break
477-
# connection lost or continuous inventory missing
484+
# connection test failed or continuous inventory missing
478485
self._update_status(self.ERROR, msg)
479486
try:
480487
# await self.disconnect()

metratec_rfid/reader_at.py

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ async def get_antenna(self) -> int:
7575
f"Not expected response for command AT+ANT? - {response}") from exc
7676

7777
# @override
78-
async def start_inventory(self, timeout: float = 4.0) -> None:
78+
async def start_inventory(self, timeout: float = 5.0) -> None:
7979
"""Start a continuous inventory.
8080
8181
This will cause the reader to perform inventories continuously
@@ -305,26 +305,51 @@ async def _send_command(self, command: str, *parameters: Any, timeout: float = 2
305305
msg: str = "Reader not " + ("responding" if self.get_status()["status"] >= 1 else "connected")
306306
raise RfidReaderException(msg) from err
307307
if send_command not in resp:
308+
# Failed commands are not echoed by the firmware - the first
309+
# line is already the error frame ("+CMD: <Error (xx xx xx xx)>"
310+
# or, on newer firmware, "+ERR: <code: message>"). Drain the
311+
# trailing ERROR terminator so it doesn't leak into the buffer
312+
# and raise the actual reader error instead of a generic one.
313+
if resp.startswith('+'):
314+
try:
315+
terminator = await self._recv(timeout=0.5)
316+
except TimeoutError:
317+
terminator = None
318+
if terminator == 'ERROR':
319+
try:
320+
err_msg = resp[resp.rindex("<") + 1:resp.rindex(">")]
321+
except ValueError:
322+
err_msg = f"{command} ERROR"
323+
raise self._parse_error_response(err_msg)
308324
raise RfidReaderException(
309325
f"Not expected response for {send_command} - {resp}")
310326
max_time: float = time() + timeout
311-
response: str = ""
327+
# Accumulate response lines as a list. Earlier code relied on
328+
# multi-line responses arriving in a single read (because the serial
329+
# layer used \n as the only line delimiter and the firmware separated
330+
# internal lines with \r), then split on \r. With the byte-by-byte
331+
# line splitter in SerialConnection each line arrives separately, so
332+
# we collect them explicitly here.
333+
response: List[str] = []
312334
try:
313335
while True:
314336
resp = await self._recv(timeout)
315337
if resp is None:
316338
break
317339
if resp == 'OK':
318-
return response.split("\r") if response else []
340+
return response
319341
if resp == 'ERROR':
320-
try:
321-
msg = response[response.rindex(
322-
"<")+1:response.rindex(">")]
323-
raise self._parse_error_response(msg)
324-
except ValueError:
342+
msg: str = ""
343+
for line in reversed(response):
344+
try:
345+
msg = line[line.rindex("<") + 1:line.rindex(">")]
346+
break
347+
except ValueError:
348+
continue
349+
if not msg:
325350
msg = f"{command} ERROR"
326-
raise RfidReaderException(str(msg))
327-
response = resp
351+
raise self._parse_error_response(msg)
352+
response.append(resp)
328353
if max_time <= time():
329354
break
330355
except TimeoutError:
@@ -333,7 +358,7 @@ async def _send_command(self, command: str, *parameters: Any, timeout: float = 2
333358
raise RfidReaderException(
334359
f"No reader response for command {send_command}")
335360
raise RfidReaderException(
336-
f"Wrong response for command {send_command} - {str(response)}")
361+
f"Wrong response for command {send_command} - {response}")
337362
except AttributeError as err:
338363
self.get_logger().debug("send command error - %s", err)
339364
raise RfidReaderException("Reader not connected") from err

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
# This call to setup() does all the work
1717
setup(
1818
name="metratec_rfid",
19-
version="1.4.4",
19+
version="1.4.5",
2020
description="Metratec RFID SDK",
2121
long_description=long_description,
2222
long_description_content_type="text/markdown",

0 commit comments

Comments
 (0)