mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-08-26 17:31:34 +00:00
3 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
a7b7f5a6a4 |
feat(transport): per-radio serialized+paced outbound send queue (#93)
* feat(transport): per-radio serialized+paced outbound send queue Prevents simultaneous LoRa transmissions when N events arrive at once. ## Mechanism Two `RadioSendQueue` instances (one MT, one MC), each a FIFO asyncio.Queue with a long-running drain task. The MT queue drains on the main asyncio loop; the MC queue drains on MeshCore's dedicated event-loop thread. - MT sends: `run_in_executor` offloads the blocking `sendText` call; queue started in `set_message_callback`, cancelled in `disconnect`. - MC sends: drain loop runs pure-async MC lib coroutines directly on the MC loop (no `_run_coro` deadlock); cross-loop callers bridge via `concurrent.futures.Future` + `asyncio.wrap_future`. - Pacing: `await asyncio.sleep(pacing_seconds)` between items; read live from config per iteration; floor clamped to 0.25 s. - Config knobs: `meshtastic_send_pacing_seconds` (default 2.0) and `meshcore_send_pacing_seconds` (default 2.0) on `ConnectionConfig`. ## Send sites rerouted All callers now `await connector.send_message_async(...)`: - `notifications/channels.py` — MeshBroadcast/MeshCoreBroadcast/MeshDM/ MeshCoreDM deliver(), test_connection(), deliver_test() - `responder.py` — DM replies in send_response() - `transport/meshcore_transport.py` — periodic_advert_loop, telemetry poll loop, send_advert() → send_advert_async(), req_telemetry() → req_telemetry_async() (all queue-routed from main loop) - `dashboard/api/mesh_send_routes.py` — test-send, advert, telemetry poll ## Audit accuracy `deliver()` now returns the actual bool from the radio send (not optimistic True), so `mesh_broadcasts_out` reflects the real result. ## Tests 17 new tests in tests/test_send_queue.py covering FIFO ordering, no drops, pacing gap, pacing floor enforcement, event-loop non-blocking, serialization, lifecycle, MT fallback, config round-trip. Existing test stubs updated to wire `send_message_async = AsyncMock(side_effect=send_message)` so prior call_count / call_args assertions remain valid without changes. Full suite: 2135 passed, 17 pre-existing failures (unchanged), 0 new regressions. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(send-queue): resolve MC telemetry self-deadlock + resolve pending futures on teardown/reconnect; composite MC-channel kwarg; audit no-op false BLOCKER 1 — req_telemetry_async self-deadlock (meshcore_transport.py): _req_telemetry_async was calling _enqueue_mc_loop_send inside itself; when _telem_job_outer ran inside the drain it nested another enqueue+await on the same single-threaded drain — permanent deadlock on first telemetry poll. Fix: _req_telemetry_async is now fully inline (no _enqueue_mc_loop_send). _telemetry_poll_loop wraps its call in _enqueue_mc_loop_send for serialization. req_telemetry_async's outer job calls _req_telemetry_async inline (safe). BLOCKER 2 — pending futures abandoned on teardown/reconnect: RadioSendQueue.stop() only cancelled the drain task; queue-sitting items had their concurrent.futures.Futures left unresolved, causing wrap_future() callers to hang indefinitely. Fix: stop() drains the remaining queue with get_nowait() and cancels every pending cfut. _cancel_mc_queue() schedules the same drain- and-cancel via call_soon_threadsafe. _start_mc_queue() cancels old drain task and drains old queue cfuts before arming the new queue (reconnect path). connector.disconnect() now .result(timeout=5) on stop() instead of fire-and-forget. SHOULD-FIX 3 — composite passes MC channel as wrong kwarg (composite_transport.py): _broadcast_async no-hint loop was calling send_message_async(channel=child_channel) for the meshcore child; should be meshcore_channel=child_channel. Silent drop fixed. NIT 5 — false success on zero-channel MC send (meshcore_transport.py): send_message_async returned True when meshcore_channel is None (nothing sent). Now returns False so audit does not record a success for a no-op. NIT 7 — config comment contradiction (config.py): meshtastic_send_pacing_seconds comment said "0 disables the floor" while simultaneously stating "still floored at 0.25". Removed the contradiction. Regression tests (tests/test_send_queue.py — 3 new, all in TestDeadlockRegression): - test_telemetry_queue_no_deadlock: drives req_telemetry_async through a real _mc_send_queue with fake MC commands; times out on pre-fix code (deadlock). - test_teardown_resolves_pending_futures: enqueues slow+fast jobs, stops mid-drain, asserts every task resolves promptly; hangs on pre-fix code. - test_reconnect_resolves_old_futures: calls _start_mc_queue twice, asserts old cfuts are cancelled; pre-fix leaves them unresolved. All 17 pre-existing send-queue tests still pass (20 total now). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
|||
|
de1e58aa71 |
feat(routing): MeshCore as first-class delivery types (meshcore_broadcast/dm) (#11)
* feat(routing): MeshCore as first-class delivery types (meshcore_broadcast/dm) Replace the composite auto-fan with explicit per-mesh delivery types so each family independently controls broadcast/DM per severity on Meshtastic AND MeshCore. mesh_broadcast->Meshtastic only, meshcore_broadcast->MeshCore (by channel name), mesh_dm/meshcore_dm likewise; routing via the existing transport hint. Adds meshcore_dm_contacts. Meshtastic-only configs unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(routing): deliver meshcore_broadcast via meshcore_channel through CompositeTransport The hinted _broadcast path passed the channel NAME on the `channel` kwarg, which MeshCoreTransport ignores (it reads meshcore_channel), so meshcore_broadcast silently no-op'd on transport=both configs. Route the meshcore child via meshcore_channel and the meshtastic child via channel. Fix the test that asserted the broken kwarg layout. Add the new delivery types to the remaining enumeration/validation sites (channel-test endpoint, scheduler digest chunking, danger-zone valid set). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(dashboard): split Notifications into Meshtastic and MeshCore sections Delineate per-mesh routing: each family configures Meshtastic delivery (mesh_broadcast/mesh_dm, channel index, node IDs) and MeshCore delivery (meshcore_broadcast/meshcore_dm, channel name, contacts) in separate sections; shared settings (enable/severity/regions/email/webhook) once. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(dashboard): first-class MeshCore nav section + dedicated pages Group the sidebar into Meshtastic and MeshCore sections. Promote MeshCore routing and connection to their own pages; move MeshCore routing out of Notifications (which stays Meshtastic + shared family settings). Add placeholder Contacts and Companion pages for the follow-on companion data API. No backend change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(dashboard): parallel MT/MC nav order + symmetric connection links Order both nav groups Connection/Routing/Mesh(Contacts)/Sources(Companion). Replace the prominent MeshCore block on the Meshtastic Connection page with a single subtle cross-link, mirrored on the MeshCore Connection page. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(dashboard): focused Meshtastic Connection/Sources pages for MT/MC parity Meshtastic Connection and Sources are now their own focused pages (mirroring MeshCore), instead of deep-linking into the full Config page. Global settings move to a restored top-level Config item. No duplicate editors; connection cross-links are mirror-image between the two pages. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
|||
|
|
2e1fb325f7 |
refactor: move source tree into work/, multi-stage Docker build, fix satpass
- Move all application source (meshai/, dashboard-frontend/, tests/, config/, docs/, Dockerfile, etc.) into work/ directory - Add Node.js multi-stage build to Dockerfile for frontend compilation; remove compiled static assets from git tracking - Fix satpass missing time windows: consolidation was splitting wire on newline and only putting line 1 in event.title, dropping the time window line that the composer uses for precomposed broadcasts - Fix satpass burst flooding: stagger consolidation timers (+60s per pending pass) so Central batch publishes don't blast the mesh - Update CI workflow build context to work/ - Anchor lib/ and data/ gitignore patterns to repo root to prevent false matches on nested directories - Add dashboard-frontend/node_modules/ to .dockerignore Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |