mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-03-28 17:43:05 +01:00
Update meshcore_py and remove monkeypatch for serial frame start detection.
This commit is contained in:
34
app/main.py
34
app/main.py
@@ -45,40 +45,6 @@ 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:
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ dependencies = [
|
|||||||
"httpx>=0.28.1",
|
"httpx>=0.28.1",
|
||||||
"pycryptodome>=3.20.0",
|
"pycryptodome>=3.20.0",
|
||||||
"pynacl>=1.5.0",
|
"pynacl>=1.5.0",
|
||||||
"meshcore==2.3.1",
|
"meshcore==2.3.2",
|
||||||
"aiomqtt>=2.0",
|
"aiomqtt>=2.0",
|
||||||
"apprise>=1.9.7",
|
"apprise>=1.9.7",
|
||||||
"boto3>=1.38.0",
|
"boto3>=1.38.0",
|
||||||
|
|||||||
@@ -5,84 +5,10 @@ from unittest.mock import AsyncMock, patch
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from app.main import _install_meshcore_serial_junk_prefix_patch, app, lifespan
|
from app.main import 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."""
|
||||||
|
|||||||
8
uv.lock
generated
8
uv.lock
generated
@@ -534,7 +534,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "meshcore"
|
name = "meshcore"
|
||||||
version = "2.3.1"
|
version = "2.3.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "bleak" },
|
{ name = "bleak" },
|
||||||
@@ -542,9 +542,9 @@ dependencies = [
|
|||||||
{ name = "pycryptodome" },
|
{ name = "pycryptodome" },
|
||||||
{ name = "pyserial-asyncio-fast" },
|
{ name = "pyserial-asyncio-fast" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/c2/a8/79f84f32cad056358b1e31dbb343d7f986f78fd93021dbbde306a9b4d36e/meshcore-2.3.1.tar.gz", hash = "sha256:07bd2267cb84a335b915ea6dab1601ae7ae13cad5923793e66b2356c3e351e24", size = 69503 }
|
sdist = { url = "https://files.pythonhosted.org/packages/4c/32/6e7a3e7dcc379888bc2bfcbbdf518af89e47b3697977cbfefd0b87fdf333/meshcore-2.3.2.tar.gz", hash = "sha256:98ceb8c28a8abe5b5b77f0941b30f99ba3d4fc2350f76de99b6c8a4e778dad6f", size = 69871 }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/14/df/66d615298b717c2c6471592e2b96117f391ae3c99f477d7f424449897bf0/meshcore-2.3.1-py3-none-any.whl", hash = "sha256:59bb8b66fd9e3261dbdb0e69fc038d4606bfd4ad1a260bbdd8659066e4bf12d2", size = 53084 },
|
{ url = "https://files.pythonhosted.org/packages/db/e4/9aafcd70315e48ca1bbae2f4ad1e00a13d5ef00019c486f964b31c34c488/meshcore-2.3.2-py3-none-any.whl", hash = "sha256:7b98e6d71f2c1e1ee146dd2fe96da40eb5bf33077e34ca840557ee53b192e322", size = 53325 },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1142,7 +1142,7 @@ requires-dist = [
|
|||||||
{ name = "fastapi", specifier = ">=0.115.0" },
|
{ name = "fastapi", specifier = ">=0.115.0" },
|
||||||
{ name = "httpx", specifier = ">=0.28.1" },
|
{ name = "httpx", specifier = ">=0.28.1" },
|
||||||
{ name = "httpx", marker = "extra == 'test'", specifier = ">=0.27.0" },
|
{ name = "httpx", marker = "extra == 'test'", specifier = ">=0.27.0" },
|
||||||
{ name = "meshcore", specifier = "==2.3.1" },
|
{ name = "meshcore", specifier = "==2.3.2" },
|
||||||
{ name = "pycryptodome", specifier = ">=3.20.0" },
|
{ name = "pycryptodome", specifier = ">=3.20.0" },
|
||||||
{ name = "pydantic-settings", specifier = ">=2.0.0" },
|
{ name = "pydantic-settings", specifier = ">=2.0.0" },
|
||||||
{ name = "pynacl", specifier = ">=1.5.0" },
|
{ name = "pynacl", specifier = ">=1.5.0" },
|
||||||
|
|||||||
Reference in New Issue
Block a user