diff --git a/app/main.py b/app/main.py index 67e8992..911635b 100644 --- a/app/main.py +++ b/app/main.py @@ -45,40 +45,6 @@ setup_logging() 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: """Connect/setup the radio in the background so HTTP serving can start immediately.""" try: diff --git a/pyproject.toml b/pyproject.toml index ef8e5e5..03d0331 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ dependencies = [ "httpx>=0.28.1", "pycryptodome>=3.20.0", "pynacl>=1.5.0", - "meshcore==2.3.1", + "meshcore==2.3.2", "aiomqtt>=2.0", "apprise>=1.9.7", "boto3>=1.38.0", diff --git a/tests/test_main_startup.py b/tests/test_main_startup.py index 00655fc..0bb5cb7 100644 --- a/tests/test_main_startup.py +++ b/tests/test_main_startup.py @@ -5,84 +5,10 @@ from unittest.mock import AsyncMock, patch import pytest -from app.main import _install_meshcore_serial_junk_prefix_patch, app, lifespan +from app.main import app, lifespan 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 async def test_lifespan_does_not_wait_for_radio_setup(self): """HTTP serving should start before post-connect setup finishes.""" diff --git a/uv.lock b/uv.lock index 55690bb..0433bb7 100644 --- a/uv.lock +++ b/uv.lock @@ -534,7 +534,7 @@ wheels = [ [[package]] name = "meshcore" -version = "2.3.1" +version = "2.3.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "bleak" }, @@ -542,9 +542,9 @@ dependencies = [ { name = "pycryptodome" }, { 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 = [ - { 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]] @@ -1142,7 +1142,7 @@ requires-dist = [ { name = "fastapi", specifier = ">=0.115.0" }, { name = "httpx", specifier = ">=0.28.1" }, { 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 = "pydantic-settings", specifier = ">=2.0.0" }, { name = "pynacl", specifier = ">=1.5.0" },