meshcore 2.3.0's ConnectionManager has a bug: when auto-reconnect creates
a new TCP connection, the old connection's connection_lost callback fires,
triggering another reconnect cycle. Since each success resets the attempt
counter, this loops forever (~1 TCP connection/second).
Disabled library auto_reconnect and added reconnection logic to
_on_disconnected() with 3 attempts and increasing backoff (5/10/15s).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PATH_ROTATION now has 3 phases:
1. Exhaust retries on primary path first (initial send + retries_per_path-1)
2. Rotate through remaining non-primary paths
3. Optional FLOOD fallback (if no_auto_flood=False)
Previously, retry iterated all paths in sort_order giving the primary
path only the initial send attempt before switching to the first path
on the list, which was often an older/worse path.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
_change_path_async manually set out_path and out_path_len on the contact
dict then called update_contact(contact) with path=None. This path reads
out_path_hash_mode from the contact dict, which is -1 when the contact
is in flood mode (after reset_path or device read with plen=255).
The encoding then produced: hop_count | (-1 << 6) = negative number,
causing "can't convert negative int to unsigned" in to_bytes().
Fix: use mc.commands.change_contact_path() which properly computes all
fields including out_path_hash_mode, avoiding the negative value issue.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Enables detailed tracking of each DM retry step: send attempt,
ACK wait timeout, and ACK timeout results. device_manager logger
set to DEBUG level so these messages appear in System Log.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Endpoint now returns error if device reset fails instead of always
returning success:true. Added logging to both endpoint and
device_manager.reset_path to diagnose reset failures.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- New `contact_paths` table for storing multiple user-configured paths per contact
- New `no_auto_flood` column on contacts to prevent automatic DIRECT→FLOOD reset
- Path rotation during DM retry: cycles through configured paths before optional flood fallback
- REST API for path CRUD, reorder, reset-to-flood, repeater listing
- Path management UI in Contact Info modal: add/delete/reorder paths, repeater picker with uniqueness warnings, hash size selector (1B/2B/3B)
- "No Flood" per-contact toggle in modal footer
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
req_status, req_acl, req_neighbours, req_mma use send_binary_req
which calculates timeout from suggested_timeout/800. After firmware
updates this can be too short, causing instant timeouts. Adding
min_timeout=15 ensures we wait at least 15 seconds for a response.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace hardcoded DM retry logic with user-configurable settings stored
in app_settings DB. Settings modal opens from menu with tab-based UI
(ready for future settings tabs). Defaults: 3 direct + 1 flood retries
(was 8+2), 30s/60s intervals, 60s grace period.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
All settings (protected_contacts, cleanup_settings, retention_settings,
manual_add_contacts) moved from .webui_settings.json file to SQLite database.
Startup migration auto-imports existing file and renames it to .json.bak.
Added safeguard in _on_new_contact: if firmware fires NEW_CONTACT for a
contact already on the device, skip pending and log a warning. Also added
diagnostic logging showing previous DB state (source, protected) when
contacts reappear as pending.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- trace: accepts comma-separated hex path (e.g. "trace 5e,d1,e7"),
waits for TRACE_DATA response with proper timeout from device
- stats: fix field names (uptime_secs, queue_len, battery_mv, etc.),
show all radio/packet stats with detail breakdown
- self_telemetry: format LPP sensor data nicely instead of raw dict
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- get help / set help: detailed parameter descriptions with
explanations, matching meshcore-cli style
- get path_hash_mode: library returns int not Event, fixed check
- set help: now reachable (was behind len(args)>=3 guard)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Was using self_info (which has no firmware data). Now uses
send_device_query() like meshcore-cli, showing model, version,
build date and repeat mode.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- req_clock: parse timestamp from binary hex data (little-endian)
and display as human-readable datetime, matching meshcore-cli
- req_neighbours: new command that fetches neighbour list from
repeater with formatted output (name resolution from device
contacts and DB cache, time ago, SNR)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
meshcore _sync methods return dict (data) or None (error/timeout),
not Event objects. hasattr(dict, 'payload') is always False, causing
instant "timeout" errors. Changed to check `result is not None`.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Clock command now shows datetime like meshcore-cli: "Current time: 2026-03-19 11:39:07 (1773916747)"
- Repeater req_* commands: pass timeout=0 to meshcore library so it uses
device's suggested_timeout instead of hardcoded 30s (matching meshcore-cli behavior)
- Execute timeout raised to 120s to accommodate slow repeater responses
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add device management: get/set params, clock/clock sync, time,
reboot, ver, scope, self_telemetry, node_discover.
Add channel management: get_channel, set_channel, add_channel,
remove_channel. Update help text with all command categories.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add 9 new console commands for repeater management:
login, logout, cmd, req_status, req_regions, req_owner,
req_acl, req_clock, req_mma. Add resolve_contact helper
and _parse_time_arg utility. Update help text with categories.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1. Delete button now sends public_key instead of name to avoid matching
wrong contacts when multiple share similar names.
2. _on_advertisement adds cache-only contacts to mc.pending_contacts when
manual approval is enabled, so they appear in the pending list after
advertising (even if meshcore fires ADVERTISEMENT instead of NEW_CONTACT).
3. Added Delete button for cache-only contacts with dedicated
/api/contacts/cached/delete endpoint and hard_delete_contact DB method.
4. approve_contact/reject_contact now handle DB-only pending contacts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a DM is retried, the retry send generates a NEW ack code. The
backend correctly maps retry ack → dm_id via _pending_acks, but
the WebSocket emit was sending the retry ack code. The frontend DOM
still has data-ack="<original_ack>" from the first send, so it could
never match retry ACKs → delivery checkmark never appeared.
Now both _on_ack() and _confirm_delivery() look up the original
expected_ack from the database before emitting to the frontend.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- DIRECT (known path): 10 attempts (8 DIRECT + 2 FLOOD), 30s wait between
- FLOOD (no path): 3 attempts only, 60s wait between
- Prevents flooding the mesh with rapid retries when no path is known
- Longer DIRECT wait gives ACKs more time to return through multi-hop paths
- Log retry mode at task start for easier debugging
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Cancel retry task immediately when _on_ack or PATH handler confirms delivery
- Keep pending_acks for 60s after retry exhaustion so late ACKs are matched
- Prevents orphaned ACKs (no dm_id) when ACK arrives shortly after exhaustion
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The SocketIO new_message emit for channel messages was missing snr,
path_len, pkt_payload and analyzer_url fields, causing messages received
via WebSocket to render without metadata until a full page refresh.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Keep direct path retries longer before falling back to flood mode,
giving more time for ACK delivery on known routes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The /chat namespace had no server-side connect handler registered. With
python-socketio 5.x (always_connect=False), client connections to
unregistered namespaces are silently rejected. This caused all SocketIO
events (new_message, ack, echo) to never reach the frontend — messages
only appeared via the 60s polling fallback.
Fixes:
- Add @socketio.on('connect', namespace='/chat') handler in main.py
- Add optimistic message append: sent messages appear instantly before
API round-trip (eliminates 3-4s serial command delay)
- Skip own-message SocketIO events to prevent duplicates
- Add connect_error handler for frontend debugging
- Bump SW cache to v6
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- stats: device uptime, TX/RX air time, packet counts, errors
- telemetry <name>: request sensor data from remote node
- neighbors <name>: list neighbors of a remote node
- trace [tag]: send trace packet for mesh topology discovery
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Instead of reloading all messages 3 times (1s, 6s, 15s) after sending,
the sent message now appears instantly via SocketIO new_message event.
Only one deferred reload remains at 15s to pick up echo data.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- _on_new_contact() in manual mode: upsert to DB as cache (source='advert')
so contacts appear in @mentions and Cache filter before approval
- _on_advertisement(): check mc.pending_contacts for name/metadata fallback
- get_pending_contacts(): include last_advert in response
- /api/contacts/cached: return numeric last_advert timestamp
- contacts.js: fix adv_lat/adv_lon field names (was c.lat/c.lon),
use last_advert timestamp instead of last_seen datetime string
- upsert_contact: source priority — never downgrade 'device' to 'advert'
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ensure_contacts(follow=True) may not return the just-added contact.
Add manual fallback to mc.contacts so /api/contacts/detailed and
send_dm can find it immediately.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- get_contacts_with_last_seen() reads from mc.contacts (device firmware)
instead of DB, so /api/contacts/detailed returns only device contacts
- _sync_contacts_to_db() now bidirectional: downgrades stale 'device'
contacts to 'advert' (cache-only) when not on device anymore
- delete_contact() sets source='advert' (cache) instead of 'deleted',
keeping contacts visible in @mentions and cache filter
- get_contacts() returns all contacts (no 'deleted' filter needed)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- delete_contact() now sets source='deleted' instead of SQL DELETE
- get_contacts() filters out deleted contacts (hidden from UI)
- upsert_contact() on re-add overwrites source, auto-undeleting
- DM FK references stay intact, no more orphaned messages
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Stop deleting contacts from DB on device removal (preserves DM history)
- Filter NULL contact_pubkey from DM conversations list
- Match outgoing DMs by contact name in raw_json during relinking
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three fixes for DM sending after contact delete/re-add:
1. approve_contact() now calls ensure_contacts() to refresh mc.contacts
so send_dm can find newly added contacts immediately
2. cli.send_dm() falls back to DB name lookup when mc.contacts misses,
preventing the contact name from being passed as a pubkey string
3. approve_contact() re-links orphaned DMs (NULL contact_pubkey from
ON DELETE SET NULL) back to the re-added contact
New DB methods: get_contact_by_name(), relink_orphaned_dms()
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Device firmware requires contacts in its table to send DMs. Passing
a raw pubkey string results in error_code=2. Show actionable error
message instead of generic "Device error".
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When the contact is not found in the in-memory mc.contacts dict,
fall back to passing the pubkey hex string directly to send_msg()
which handles it natively. Fixes "Contact not found" error.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a DM arrives with only pubkey_prefix (short hex) and the sender
is not in mc.contacts, fall back to DB prefix lookup to get the full
64-char public key. Prevents ghost contact entries and "Unknown" DM
conversations.
Also adds get_contact_by_prefix() database helper.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix ACK handler bug: read 'code' field instead of 'expected_ack'
- Add DM retry (up to 3 attempts) with same timestamp for receiver dedup
- Add receiver-side dedup in _on_dm_received() (sender_timestamp or time-window)
- Add PATH_UPDATE as backup delivery signal for flood DMs
- Track pending acks with dm_id for proper ACK→DM linkage
- Return dm_id and expected_ack from POST /dm/messages API
- Add find_dm_duplicate() and get_dm_by_id() database helpers
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
meshcore lib 2.2.21 bug: reader.py line 434 does
self.channels = self.channels.extend([...])
which sets self.channels = None (extend returns None).
This corrupts ALL subsequent channel message processing.
Fix: after getting max_channels from device_info, pre-allocate
mc._reader.channels to max_channels slots so the extend path
is never triggered.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
meshcore lib 2.2.21 has a bug where list.extend() return value (None)
is assigned to self.channels, corrupting state for indices >= 20.
Stop iterating after 3 consecutive empty/failed channels to avoid
hitting this bug and reduce error log spam.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Firmware reports MAX_GROUP_CHANNELS (typically 40 for companion builds)
in the DEVICE_INFO response. Fetch it at startup and use it in all
channel iteration loops. Previously hardcoded range(8) prevented
channels 8+ from appearing and blocked adding new channels.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pending echo correlation was assigning ANY first echo to the sent message,
even if it came from a different channel. This caused cross-channel mismatches
(e.g., Public channel echo assigned to #krakow message).
Fix: check that pkt_payload's first byte (channel_hash = sha256(secret)[0])
matches the channel we sent on before accepting correlation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use array-based metaParts.join(' | ') instead of string concatenation
to avoid ugly leading "| Hops: 0" when meshcore lib doesn't provide SNR.
Also revert temporary INFO-level debug logging back to DEBUG.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Subscribe to RX_LOG_DATA events to capture repeated radio packets
- Parse GRP_TXT (0x05) payload to extract pkt_payload and path
- Classify echoes as sent (pending echo correlation) or incoming
- Register pending echo when sending channel messages for pkt_payload capture
- Add update_message_pkt_payload() DB method for sent message correlation
- Return echo_paths/echo_snrs for ALL messages (not just own) in GET /messages
- Frontend: build paths from echo_paths for incoming message route display
- Emit SocketIO 'echo' event for real-time badge updates
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When an ADVERTISEMENT arrives for a pubkey not in mc.contacts, the
firmware has auto-added a new contact. Trigger ensure_contacts() to
refresh the contact list and get the name. Also: remove contacts from
mc.contacts cache on delete, add reject/clear pending API endpoints,
promote advert logging to INFO level.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
approve_contact now removes the contact from mc.pending_contacts after
successful approval. Added reject_contact (remove without adding) and
clear_pending_contacts methods with API endpoints.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
auto_update_contacts=True triggers ensure_contacts() on every
ADVERTISEMENT event, fetching 324+ contacts over serial (several seconds).
This blocks the serial port and delays MESSAGES_WAITING processing,
causing 10-30s message reception delays. Contacts are synced at startup
and updated individually via NEW_CONTACT events.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When manual approval is enabled in settings, _on_new_contact now skips
DB upsert and emits SocketIO event for pending contacts. When auto mode
is on, contacts are added to DB immediately as before. Also enriched
approve_contact and get_pending_contacts with full contact data.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>