mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-11-03 16:12:19 +01:00
Monkeypatch the meshcore_py lib for frame-start handling
This commit is contained in:
2 files changed
+109
-1
No files matched your search
+34
@@ -45,6 +45,40 @@ setup_logging()
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _install_meshcore_serial_junk_prefix_patch(serial_connection_cls=None) -> None:
|
||||||
|
"""Make meshcore serial framing tolerant of leading junk bytes.
|
||||||
|
|
||||||
|
Some radios emit console/debug text on the same UART as the companion
|
||||||
|
protocol. The current meshcore serial parser searches for the frame start
|
||||||
|
marker but then incorrectly begins parsing at byte 0 of the chunk instead
|
||||||
|
of the located marker offset, which can drop valid response frames.
|
||||||
|
|
||||||
|
TODO: Remove this monkeypatch once meshcore_py includes the upstream fix:
|
||||||
|
https://github.com/meshcore-dev/meshcore_py/pull/67
|
||||||
|
"""
|
||||||
|
if serial_connection_cls is None:
|
||||||
|
from meshcore.serial_cx import SerialConnection as serial_connection_cls
|
||||||
|
|
||||||
|
original_handle_rx = serial_connection_cls.handle_rx
|
||||||
|
if getattr(original_handle_rx, "_rtmesh_junk_prefix_patch", False):
|
||||||
|
return
|
||||||
|
|
||||||
|
def patched_handle_rx(self, data: bytearray):
|
||||||
|
if len(self.header) == 0:
|
||||||
|
idx = data.find(b"\x3e")
|
||||||
|
if idx < 0:
|
||||||
|
return
|
||||||
|
if idx > 0:
|
||||||
|
data = data[idx:]
|
||||||
|
return original_handle_rx(self, data)
|
||||||
|
|
||||||
|
patched_handle_rx._rtmesh_junk_prefix_patch = True
|
||||||
|
serial_connection_cls.handle_rx = patched_handle_rx
|
||||||
|
|
||||||
|
|
||||||
|
_install_meshcore_serial_junk_prefix_patch()
|
||||||
|
|
||||||
|
|
||||||
async def _startup_radio_connect_and_setup() -> None:
|
async def _startup_radio_connect_and_setup() -> None:
|
||||||
"""Connect/setup the radio in the background so HTTP serving can start immediately."""
|
"""Connect/setup the radio in the background so HTTP serving can start immediately."""
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -5,10 +5,84 @@ from unittest.mock import AsyncMock, patch
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from app.main import app, lifespan
|
from app.main import _install_meshcore_serial_junk_prefix_patch, app, lifespan
|
||||||
|
|
||||||
|
|
||||||
class TestStartupLifespan:
|
class TestStartupLifespan:
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_meshcore_serial_patch_discards_leading_junk_before_frame(self):
|
||||||
|
class RecordingReader:
|
||||||
|
def __init__(self):
|
||||||
|
self.frames = []
|
||||||
|
|
||||||
|
async def handle_rx(self, data):
|
||||||
|
self.frames.append(bytes(data))
|
||||||
|
|
||||||
|
class FakeSerialConnection:
|
||||||
|
def __init__(self):
|
||||||
|
self.header = b""
|
||||||
|
self.reader = None
|
||||||
|
self.frame_expected_size = 0
|
||||||
|
self.inframe = b""
|
||||||
|
|
||||||
|
def set_reader(self, reader):
|
||||||
|
self.reader = reader
|
||||||
|
|
||||||
|
def handle_rx(self, data: bytearray):
|
||||||
|
if len(self.header) == 0:
|
||||||
|
idx = data.find(b"\x3e")
|
||||||
|
if idx < 0:
|
||||||
|
return
|
||||||
|
self.header = data[0:1]
|
||||||
|
data = data[1:]
|
||||||
|
|
||||||
|
if len(self.header) < 3:
|
||||||
|
while len(self.header) < 3 and len(data) > 0:
|
||||||
|
self.header = self.header + data[0:1]
|
||||||
|
data = data[1:]
|
||||||
|
if len(self.header) < 3:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.frame_expected_size = int.from_bytes(
|
||||||
|
self.header[1:], "little", signed=False
|
||||||
|
)
|
||||||
|
if self.frame_expected_size > 300:
|
||||||
|
self.header = b""
|
||||||
|
self.inframe = b""
|
||||||
|
self.frame_expected_size = 0
|
||||||
|
if len(data) > 0:
|
||||||
|
self.handle_rx(data)
|
||||||
|
return
|
||||||
|
|
||||||
|
upbound = self.frame_expected_size - len(self.inframe)
|
||||||
|
if len(data) < upbound:
|
||||||
|
self.inframe = self.inframe + data
|
||||||
|
return
|
||||||
|
|
||||||
|
self.inframe = self.inframe + data[0:upbound]
|
||||||
|
data = data[upbound:]
|
||||||
|
if self.reader is not None:
|
||||||
|
asyncio.create_task(self.reader.handle_rx(self.inframe))
|
||||||
|
self.inframe = b""
|
||||||
|
self.header = b""
|
||||||
|
self.frame_expected_size = 0
|
||||||
|
if len(data) > 0:
|
||||||
|
self.handle_rx(data)
|
||||||
|
|
||||||
|
_install_meshcore_serial_junk_prefix_patch(FakeSerialConnection)
|
||||||
|
|
||||||
|
conn = FakeSerialConnection()
|
||||||
|
reader = RecordingReader()
|
||||||
|
conn.set_reader(reader)
|
||||||
|
|
||||||
|
payload = b"\x00\x01\x02\x53"
|
||||||
|
frame = b"\x3e" + len(payload).to_bytes(2, "little") + payload
|
||||||
|
|
||||||
|
conn.handle_rx(b"junk bytes\r\n" + frame)
|
||||||
|
await asyncio.sleep(0)
|
||||||
|
|
||||||
|
assert reader.frames == [payload]
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_lifespan_does_not_wait_for_radio_setup(self):
|
async def test_lifespan_does_not_wait_for_radio_setup(self):
|
||||||
"""HTTP serving should start before post-connect setup finishes."""
|
"""HTTP serving should start before post-connect setup finishes."""
|
||||||
|
|||||||
Reference in new issue
Block a user