- Store last status message on MeshcoreClient so error details survive
beyond the SSE event (which isn't active during connecting state)
- Status endpoint now returns message field so the frontend can show
the real reason (e.g. 'Connection failed after retries: ...')
- Extend JS polling from 30s to 90s to outlast the backend's 65s
retry sequence (5+15+45s delays) before declaring timeout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
asyncio.run() called from a gevent-patched Flask thread fails under
gunicorn+gevent. Run the one-shot scan in a ThreadPoolExecutor thread
with its own event loop, matching how AsyncWorker handles it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Revert route to scan_ble() — scan_ble_sync() lives on AsyncWorker,
not MeshcoreClient; the 500 was caused by our previous fix
- MeshcoreClient.scan_ble() now runs a one-shot asyncio scan when no
worker is active, so Scan works before Connect is pressed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Lock-protect `get_state` and `_set_state` to prevent data race
between Flask and asyncio daemon threads
- Atomically check-and-set CONNECTING guard in `connect()` to close
TOCTOU window between concurrent Flask threads
- Push status events outside the lock in both `_set_state` and
`connect()` to avoid potential deadlock
- Add TestMeshcoreContact, TestMeshcoreClientStateMachine tests
covering to_dict keys, queue push on state change, message append
and 500-item cap (9 -> 13 tests)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements utils/meshcore.py with all dataclasses (MeshcoreMessage,
MeshcoreNode, MeshcoreContact, MeshcoreTelemetry, MeshcoreTraceroute),
connection configs (SerialConfig, TCPConfig, BLEConfig), ConnectionState
enum, serial port discovery, and the MeshcoreClient singleton skeleton.
Adds tests/test_meshcore_client.py covering all dataclasses, availability
check, and state enum (8/8 tests passing).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>