40 KiB
CLAUDE.md - MeshCore SAR Technical Reference
AI assistant guide for the MeshCore SAR Flutter application.
⚠️ CRITICAL: Flutter Development Rules
NEVER run or kill Flutter processes:
- DO NOT execute
flutter runcommand - DO NOT kill Flutter processes (
pkill flutter,killall flutter) - User manages Flutter development server - only make code changes
- Hot reload happens automatically when files are saved
Quick Reference
Project Type: Flutter Mobile App (iOS 13+, Android API 21+) Architecture: Provider-based state management + BLE communication Protocol: MeshCore BLE Companion Radio (Little Endian byte order) Repository: https://github.com/meshcore-dev/meshcore.js
BLE Service UUIDs:
- Service:
6E400001-B5A3-F393-E0A9-E50E24DCCA9E - RX (write):
6E400002-B5A3-F393-E0A9-E50E24DCCA9E - TX (notify):
6E400003-B5A3-F393-E0A9-E50E24DCCA9E
Key Dependencies:
- flutter_blue_plus ^2.0.0 (BLE)
- flutter_map ^8.2.2 (mapping)
- provider ^6.1.0 (state)
- geolocator ^14.0.2 (GPS)
Note: No crypto dependencies required - echo detection uses a simple DJB2-style hash function
Project Structure
lib/
├── l10n/ # Internationalization (i18n)
│ ├── app_localizations.dart # Generated localization class
│ ├── app_en.arb # English (default)
│ ├── app_hr.arb # Croatian (Hrvatski)
│ └── app_sl.arb # Slovenian (Slovenščina)
├── models/ # Data models
│ ├── contact.dart, message.dart, sar_marker.dart
│ ├── map_drawing.dart # MapDrawing, LineDrawing, RectangleDrawing
│ ├── device_info.dart, room_login_state.dart, map_layer.dart
├── services/ # Business logic
│ ├── meshcore_ble_service.dart # BLE coordinator (399 lines)
│ ├── protocol/ # Frame parsing & building (628 lines)
│ ├── ble/ # Connection, commands, responses (963 lines)
│ ├── location_tracking_service.dart # GPS + mesh broadcast (501 lines)
│ ├── map_marker_service.dart # Marker generation + geodesic (518 lines)
│ └── validation_service.dart # Form validation (511 lines)
├── providers/ # State management
│ ├── connection_provider.dart # BLE connection state
│ ├── contacts_provider.dart # Contact list
│ ├── messages_provider.dart # Messages + SAR markers
│ ├── map_provider.dart # Map navigation
│ ├── drawing_provider.dart # Map drawing state
│ └── app_provider.dart # Coordinator (uses all above)
├── screens/ # UI screens (home, messages, contacts, map, settings, device_config, map_management, packet_log)
├── widgets/ # Reusable components
│ ├── map_markers.dart # Map marker rendering
│ ├── map/ # Map-specific widgets
│ │ ├── drawing_layer.dart # Drawing rendering on map
│ │ ├── drawing_toolbar.dart # Drawing UI controls
│ │ └── compass/ # Compass dialog components (fully localized)
│ ├── messages/, contacts/ # Feature-specific widgets (fully localized)
└── utils/ # Utilities
├── sar_message_parser.dart # SAR marker parsing
└── drawing_message_parser.dart # Drawing message parsing
MeshCore Protocol
Frame Delimiters
- BLE: Single characteristic value (link layer handles integrity)
- USB:
>(0x3E) outbound,<(0x3C) inbound, 2-byte length (LE), then frame data - All uint32 values use Little Endian byte order
Command Codes (App → Radio)
| Code | Name | Description |
|---|---|---|
| 1 | CMD_APP_START | First command after connection → RESP_CODE_SELF_INFO(5) |
| 2 | CMD_SEND_TXT_MSG | Send text message to contact (DM) |
| 3 | CMD_SEND_CHANNEL_TXT_MSG | Send flood-mode text to channel |
| 4 | CMD_GET_CONTACTS | Sync contacts (optional 'since' param) |
| 5 | CMD_GET_DEVICE_TIME | Get device clock (epoch secs, UTC) |
| 6 | CMD_SET_DEVICE_TIME | Set device clock |
| 7 | CMD_SEND_SELF_ADVERT | Send Advertisement packet |
| 8 | CMD_SET_ADVERT_NAME | Update node name in adverts |
| 9 | CMD_ADD_UPDATE_CONTACT | Add/modify contact |
| 10 | CMD_SYNC_NEXT_MESSAGE | Get next text message from queue |
| 11 | CMD_SET_RADIO_PARAMS | Save radio parameters |
| 12 | CMD_SET_RADIO_TX_POWER | Set radio TX power |
| 13 | CMD_RESET_PATH | Reset out_path for contact |
| 14 | CMD_SET_ADVERT_LATLON | Update lat/lon in adverts |
| 15 | CMD_REMOVE_CONTACT | Remove contact |
| 16 | CMD_SHARE_CONTACT | Share contact via zero-hop advert |
| 17 | CMD_EXPORT_CONTACT | Export contact as business card |
| 18 | CMD_IMPORT_CONTACT | Import contact from business card |
| 19 | CMD_REBOOT | Reboot companion device |
| 20 | CMD_GET_BATT_AND_STORAGE | Get battery mV and storage stats |
| 21 | CMD_SET_TUNING_PARAMS | Set tuning parameters |
| 22 | CMD_DEVICE_QUERY | First command to send → RESP_CODE_DEVICE_INFO(13) |
| 25 | CMD_SEND_RAW_DATA | Transmit PAYLOAD_TYPE_RAW_CUSTOM |
| 26 | CMD_SEND_LOGIN | Send login to repeater/room |
| 27 | CMD_SEND_STATUS_REQ | Send status request |
| 36 | CMD_SEND_TRACE_PATH | Initiate TRACE with SNR collection |
| 37 | CMD_SET_DEVICE_PIN | Set BLE PIN code |
| 38 | CMD_SET_OTHER_PARAMS | Set various parameters |
| 39 | CMD_SEND_TELEMETRY_REQ | Request telemetry (deprecated) |
| 40 | CMD_GET_CUSTOM_VARS | Retrieve custom variables |
| 41 | CMD_SET_CUSTOM_VAR | Set single custom variable |
| 42 | CMD_GET_ADVERT_PATH | Query last advert path |
| 43 | CMD_GET_TUNING_PARAMS | Get airtime-factor & rx-delay |
| 50 | CMD_SEND_BINARY_REQ | Binary request (preferred over 39) |
| 51 | CMD_FACTORY_RESET | Erase flash file system |
Response Codes (Radio → App)
| Code | Name | Description |
|---|---|---|
| 0 | RESP_CODE_OK | Success |
| 1 | RESP_CODE_ERR | Error (includes err_code) |
| 2 | RESP_CODE_CONTACTS_START | Start contacts sync |
| 3 | RESP_CODE_CONTACT | Single contact info |
| 4 | RESP_CODE_END_OF_CONTACTS | End contacts sync |
| 5 | RESP_CODE_SELF_INFO | Node's own information |
| 6 | RESP_CODE_SENT | Message sent with ACK/TAG |
| 7 | RESP_CODE_CONTACT_MSG_RECV | Contact message received |
| 8 | RESP_CODE_CHANNEL_MSG_RECV | Channel message received |
| 9 | RESP_CODE_CURR_TIME | Current device time |
| 10 | RESP_CODE_NO_MORE_MESSAGES | Message queue empty |
| 11 | RESP_CODE_EXPORT_CONTACT | Contact export data |
| 12 | RESP_CODE_BATT_AND_STORAGE | Battery and storage info |
| 13 | RESP_CODE_DEVICE_INFO | Device firmware/hardware info |
| 21 | RESP_CODE_CUSTOM_VARS | Custom variables state |
| 22 | RESP_CODE_ADVERT_PATH | Last advert path |
Push Notifications (Radio → App, Async)
| Code | Name | Description |
|---|---|---|
| 0x80 | PUSH_CODE_ADVERT | New advertisement received |
| 0x81 | PUSH_CODE_PATH_UPDATED | Contact received new path |
| 0x82 | PUSH_CODE_SEND_CONFIRMED | Message ACK received |
| 0x83 | PUSH_CODE_MSG_WAITING | New text message received |
| 0x84 | PUSH_CODE_RAW_DATA | PAYLOAD_TYPE_RAW_CUSTOM received |
| 0x85 | PUSH_CODE_LOGIN_SUCCESS | Login successful |
| 0x86 | PUSH_CODE_LOGIN_FAIL | Login failed |
| 0x87 | PUSH_CODE_STATUS_RESPONSE | Status response received |
| 0x88 | PUSH_CODE_LOG_RX_DATA | Debug: raw OTA packet (diagnostic) |
| 0x89 | PUSH_CODE_TRACE_DATA | TRACE packet end of path |
| 0x8A | PUSH_CODE_NEW_ADVERT | New contact advert |
| 0x8B | PUSH_CODE_TELEMETRY_RESPONSE | Telemetry response |
| 0x8C | PUSH_CODE_BINARY_RESPONSE | Binary response |
CRITICAL: PUSH_CODE_LOG_RX_DATA (0x88) - Diagnostic Packet Capture
⚠️ IMPORTANT: This is an always-on diagnostic feature when app is connected
Purpose: Real-time packet capture of ALL radio traffic for debugging and network analysis
Trigger: Automatically sent for EVERY packet received by the radio, before validation
- Triggered in
Dispatcher::checkRecv()→logRxRaw()virtual hook - No filtering, throttling, or configuration options
- Even malformed/incomplete packets are captured
Packet Format (3 + raw_packet_length bytes):
| Byte | Field | Description |
|---|---|---|
| 0 | Code | PUSH_CODE_LOG_RX_DATA (0x88) |
| 1 | SNR | Signal-to-Noise Ratio: (int8_t)(snr_db * 4) - decode by dividing by 4.0 |
| 2 | RSSI | Received Signal Strength: (int8_t)(rssi_dbm) - signed byte |
| 3...N | raw_data | Complete raw packet as received from radio (up to 255 bytes) |
Example Decoding:
final snrRaw = data[0];
final snrDb = (snrRaw.toSigned(8)) / 4.0; // e.g., 0x14 → 5.0 dB
final rssiDbm = data[1].toSigned(8); // e.g., 0xC8 → -56 dBm
final rawPacket = data.sublist(2); // Complete LoRa packet
Behavior:
- Always Active: Automatically enabled when BLE/USB/WiFi client connects
- NOT User-Configurable: No runtime enable/disable command exists
- Only Way to Disable: Disconnect the app from companion radio
- Bandwidth Impact: Can generate significant traffic in busy mesh networks
- Frame Size Limit: Only sent if
packet_length + 3 <= 172(BLE MTU constraint)
Use Cases:
- Packet Sniffer: Capture all mesh network traffic in range
- Signal Analysis: Monitor SNR/RSSI for link quality assessment
- Network Diagnostics: Identify interference, collisions, malformed packets
- Protocol Development: Analyze packet structures and timing
- Coverage Testing: Map signal strength across geographic areas
Current Implementation (lib/services/ble/ble_response_handler.dart:409):
- Parses SNR and RSSI from diagnostic packets
- Calculates entropy to detect encrypted vs. plaintext packets
- Stores in packet log (
_packetLogs) for viewing in Packet Log screen - Accessible via
screens/packet_log_screen.dart
Security Consideration: Raw packet capture means ALL traffic is visible (encrypted payloads are still captured at radio level)
Reference Files:
- Hook Definition:
/Users/dz0ny/meshcore-sar/MeshCore/src/Dispatcher.h(line 149) - Call Site:
/Users/dz0ny/meshcore-sar/MeshCore/src/Dispatcher.cpp(line 119) - Companion Implementation:
/Users/dz0ny/meshcore-sar/MeshCore/examples/companion_radio/MyMesh.cpp(lines 237-248)
CRITICAL: Public Message Echo Detection Using PUSH_CODE_LOG_RX_DATA
⚠️ IMPORTANT: You CAN detect when your broadcast messages are received and rebroadcast by other nodes
The Problem: Public channel messages don't have explicit ACKs (fire-and-forget). How do we know if anyone received them?
The Solution: Echo detection using PUSH_CODE_LOG_RX_DATA raw packet matching!
How It Works:
-
Deterministic Encryption: Public messages use AES128-ECB encryption
- Same plaintext + same channel key = identical encrypted output
- When node B receives your message and rebroadcasts it, the packet is byte-for-byte identical
- You can detect this by comparing raw packet data!
-
Public Message Packet Structure:
Plaintext Payload (before encryption):
[4 bytes] = Timestamp (uint32_t, little-endian) [1 byte] = TXT_TYPE (0x00 = plain, 0x01 = CLI, 0x02 = signed) [variable] = "sender_name: message_text" [0-15 bytes]= Zero padding to 16-byte boundaryEncrypted Wire Format (in PUSH_CODE_LOG_RX_DATA):
[1 byte] = Channel hash (identifies which channel) [2 bytes] = MAC (HMAC-SHA256 truncated to 2 bytes) [16+ bytes] = AES128-ECB encrypted payload -
Echo Detection Algorithm:
When sending public message: 1. Store encrypted payload (channel_hash + MAC + ciphertext) 2. Calculate SHA256 hash for fast lookup (8 bytes sufficient) 3. Set expiry (e.g., 5 minutes - messages won't echo after that) When receiving PUSH_CODE_LOG_RX_DATA: 1. Extract raw packet data (skip SNR/RSSI bytes) 2. Calculate hash of raw packet 3. Check if hash matches any recently sent message 4. If match found → ECHO DETECTED! Someone rebroadcast your message 5. Increment ACK/echo counter for that message -
What Echoes Mean:
- Echo detected: At least one node received your broadcast AND rebroadcast it
- Multiple echoes: Multiple nodes received and rebroadcast (indicates good mesh coverage)
- No echoes: Either no nodes in range, or message not rebroadcast (not necessarily failure)
- Echo count ≠ exact receiver count: One node can produce multiple echoes via different paths
-
Implementation Strategy:
Data Structure:
class SentMessageTracker { final String messageId; final String packetHashHex; // Simple hash of packet for O(1) lookup final DateTime sentTime; final DateTime expiryTime; int echoCount = 0; Set<String> uniqueEchoPaths = {}; // Track different signal paths }Storage:
- Keep last 50-100 sent messages in memory
- Use hash map for O(1) lookup:
Map<String, SentMessageTracker> - Auto-cleanup expired entries (5-10 minute TTL)
Matching:
/// Simple hash function for packet identification (no crypto dependency) String _simplePacketHash(Uint8List packet) { // Use DJB2-style hash with length and bytes from start/middle/end // Sufficient for short-lived echo detection (5 min TTL) int hash = packet.length; // Mix in bytes from strategic positions for (int i = 0; i < packet.length && i < 8; i++) { hash = ((hash << 5) - hash) + packet[i]; hash = hash & 0xFFFFFFFF; // Keep 32-bit } // ... sample from middle and end return hash.toRadixString(16).padLeft(8, '0'); } void _handleLogRxData(BufferReader reader) { final snrRaw = data[0]; final rssiDbm = data[1]; final rawPacket = data.sublist(2); // Calculate simple hash (no crypto package needed!) final packetHashHex = _simplePacketHash(rawPacket); // Check for echo final tracker = _sentMessageTrackers[packetHashHex]; if (tracker != null && !tracker.isExpired) { tracker.echoCount++; tracker.uniqueEchoPaths.add('${snrRaw}_${rssiDbm}'); onMessageEcho?.call(tracker.messageId, tracker.echoCount); } } -
UI Implications:
- Show echo count instead of "Broadcast" for channel messages
- Display: "Rebroadcast by 3 nodes" or "No echoes yet"
- Color coding: Green (echoes detected), Yellow (waiting), Gray (expired)
- Tap to show echo details: SNR/RSSI of each echo, timing, etc.
-
Limitations & Considerations:
- Not a guaranteed delivery count: Echoes indicate rebroadcast, not unique receivers
- Network topology dependent: Dense networks → more echoes
- Time window: Only detects echoes while app is connected and listening
- False negatives possible: Messages may be received but not rebroadcast if:
- Receiver's hop limit reached
- Receiver already saw packet via another path
- Network congestion/collision
- Timestamp uniqueness:
getCurrentTimeUnique()auto-increments to prevent collisions
-
Advanced Features:
- Signal quality heatmap: Map echo SNR/RSSI to visualize coverage
- Mesh health monitoring: Track echo rates over time
- Reliability score: Calculate delivery probability based on historical echoes
- Path diversity: Count unique echo paths (different SNR/RSSI signatures)
Reference Files:
- Send group message:
/Users/dz0ny/meshcore-sar/MeshCore/src/helpers/BaseChatMesh.cpp(lines 379-398) - Encryption:
/Users/dz0ny/meshcore-sar/MeshCore/src/Mesh.cpp(lines 509-527) - Packet hashing:
/Users/dz0ny/meshcore-sar/MeshCore/src/Packet.cpp(lines 17-26) - AES implementation:
/Users/dz0ny/meshcore-sar/MeshCore/src/Utils.cpp(lines 63-72)
Echo Detection Implementation Status
✅ FULLY IMPLEMENTED AND PRODUCTION-READY
The echo detection feature is 100% complete with intelligent packet identification using the sender's node hash from the packet structure. No firmware changes required!
Brilliant Discovery - Sender Identification in Packet Structure:
The raw packet structure contains the sender's identity in an unencrypted field:
Packet Structure for PAYLOAD_TYPE_GRP_TXT (0x05):
[Byte 0] = Header (route type + payload type + version)
[Byte 1] = Path length
[Byte 2] = Path[0] = SENDER'S NODE HASH (first byte of sender's public key) ✅
[Byte 3+] = Rest of path + encrypted payload
How Echo Detection Works:
-
Initialization (on connection):
- Receive
RESP_CODE_SELF_INFOwith our public key - Extract our node hash (first byte of public key)
- Store for packet identification
- Receive
-
Sending a Message:
- User sends channel message →
trackSentMessage(messageId)called - Tracker created with status "pending" (waiting for packet capture)
- User sends channel message →
-
Packet Capture (via
PUSH_CODE_LOG_RX_DATA):- Radio sends raw packet data (typically within 50-200ms)
- Extract header byte:
payloadType = (header >> 2) & 0x0F - Check if GRP_TXT packet:
payloadType == 0x05 - Extract sender hash:
senderNodeHash = packet[2] - If sender hash matches our node hash → This is OUR packet!
- Calculate simple hash of entire packet (DJB2-style, no crypto dependency)
- Store tracker by packet hash for echo detection
-
Echo Detection:
- Future
PUSH_CODE_LOG_RX_DATApackets arrive - Calculate packet hash using simple hash function
- Match against stored trackers (O(1) lookup)
- If match found → Echo detected! Another node rebroadcast our message
- Increment echo count, track SNR/RSSI signature
- Notify UI → Shows "Rebroadcast by X nodes"
- Future
Implementation Details:
-
Data Models (
lib/models/sent_message_tracker.dart,lib/models/message.dart)SentMessageTracker: Tracks sent messages with simple packet hashes (no crypto dependency)Message.echoCountandMessage.firstEchoAt: Track echo statisticsMessage.echoStatusText: Returns "Rebroadcast by X nodes" or "Broadcast (no echoes)"
-
Echo Detection Engine (
lib/services/ble/ble_response_handler.dart)_simplePacketHash(): DJB2-style hash function (replaces SHA256, no crypto package needed)setOurNodeHash(): Stores our node hash for packet identification_associatePacketWithSentMessage(): Smart packet matching using node hash_checkForEcho(): Matches received packets against sent message hashes (O(1) lookup)trackSentMessage(): Stores message ID when sending- Automatic cleanup: 5-minute TTL, max 100 tracked messages
- Tracks unique echo paths via SNR/RSSI signatures
-
Complete Callback Chain:
BleResponseHandler.onMessageEchoDetected (packet matching) ↓ MeshCoreBleService.onMessageEchoDetected (service layer) ↓ ConnectionProvider.onMessageEchoDetected (provider layer) ↓ AppProvider (wires to MessagesProvider) ↓ MessagesProvider.handleMessageEcho() (updates message state) ↓ UI auto-updates via notifyListeners() -
UI Integration:
- Message widgets automatically show echo count via
deliveryStatusText - "Broadcast (no echoes)" → No rebroadcasts detected yet
- "Rebroadcast by 1 node" → One node rebroadcast the message
- "Rebroadcast by X nodes" → Multiple nodes rebroadcast
- Message widgets automatically show echo count via
Example Log Output:
🔑 [Echo] Our node hash set to: 0xb8
📤 [Echo] Tracking message 1760818280435_channel_sent, will capture next packet within 500ms
📦 [Echo] Captured OUR packet (node hash match!)
Message ID: 1760818280435_channel_sent
Sender hash: 0xb8
Time delta: 147ms
Packet hash: a1b2c3d4e5f6...
Now tracking for echoes...
🔊 [Echo] Detected echo for message 1760818280435_channel_sent: count=1
Why This Solution Is Excellent:
✅ No firmware changes required - Uses existing packet structure ✅ Reliable identification - Explicit sender hash in packet (byte 2) ✅ No timing assumptions - Works even with delayed packets ✅ Handles rapid sends - Each packet uniquely identified ✅ Production-ready - Tested and functional ✅ Efficient - O(1) hash lookup for echo matching ✅ Automatic cleanup - 5-minute TTL prevents memory leaks
Files Modified for Echo Detection:
lib/models/sent_message_tracker.dart- NEW model for tracking sent messageslib/models/message.dart- AddedechoCountandfirstEchoAtfieldslib/services/ble/ble_response_handler.dart- Core detection logic with node hash matchinglib/services/meshcore_ble_service.dart- Callback wiring + node hash extractionlib/providers/connection_provider.dart- Provider callback declarationlib/providers/app_provider.dart- Wire echo callback to MessagesProviderlib/providers/messages_provider.dart-handleMessageEcho()methodpubspec.yaml- Addedcrypto: ^3.0.3dependency
Testing Instructions:
Setup:
- Ensure you have 2+ MeshCore devices in range
- Connect Device A (your device) to the app
- Wait for
RESP_CODE_SELF_INFO→ Look for log:🔑 [Echo] Our node hash set to: 0xXX
Test Echo Detection:
- Send a public channel message from Device A: "test message"
- Watch logs for packet capture:
📤 [Echo] Tracking message ... will capture next packet within 500ms 📦 [Echo] Captured OUR packet (node hash match!) Sender hash: 0xXX Packet hash: abc123... - Device B receives and rebroadcasts the message
- Device A detects echo:
🔊 [Echo] Detected echo for message ...: count=1 - UI automatically updates to show: "Rebroadcast by 1 node"
- Multiple devices → "Rebroadcast by X nodes"
Verification:
- Check message delivery status shows echo count
- Each unique rebroadcast increments the counter
- SNR/RSSI tracked for each echo path
- Echoes expire after 5 minutes
Performance Characteristics:
- Packet identification: O(1) - byte comparison at offset 2
- Hash calculation: O(n) where n = packet length (~38-200 bytes)
- Echo lookup: O(1) via HashMap with SHA256 hash key
- Memory: ~150 bytes per tracked message, max 100 messages = ~15KB
- Cleanup: Automatic on every check + when tracker limit exceeded
- Window: 1-second correlation window for initial packet capture
- TTL: 5-minute expiry for echo tracking
Constants
ADV_TYPE (Contact Type):
- 0: ADV_TYPE_NONE (unknown/invalid)
- 1: ADV_TYPE_CHAT (team member, shown on map)
- 2: ADV_TYPE_REPEATER (network repeater)
- 3: ADV_TYPE_ROOM (communication room/server)
TXT_TYPE (Message Type):
- 0: TXT_TYPE_PLAIN (plain text)
- 1: TXT_TYPE_CLI_DATA (CLI command)
- 2: TXT_TYPE_SIGNED_PLAIN (plain text + extra 4 bytes of sender's public key for verification)
ERR_CODE:
- 1: ERR_CODE_UNSUPPORTED_CMD
- 2: ERR_CODE_NOT_FOUND
- 3: ERR_CODE_TABLE_FULL
- 4: ERR_CODE_BAD_STATE
- 5: ERR_CODE_FILE_IO_ERROR
- 6: ERR_CODE_ILLEGAL_ARG
CRITICAL: Channels vs. Rooms
Channels (numeric identifiers):
- Channel 0 = "Public Channel" (default flood-mode broadcast)
- Channel 1+ = Reserved for future
- Ephemeral - messages NOT persisted
- Use
CMD_SEND_CHANNEL_TXT_MSG(3) - ⚠️ MUST be configured before use with
CMD_SET_CHANNEL(32)- Format:
[cmd(1)][channel_idx(1)][name(32)][secret(16)] - Default public channel secret (128-bit):
- Hex:
8b3387e9c5cdea6ac9e5edbaa115cd72 - Base64:
izOH6cXN6mrJ5e26oRXNcg== - Source: MeshCore FAQ
- Hex:
- Configuration: Most firmware versions have channel 0 pre-configured
- App attempts to configure via
CMD_SET_CHANNELduring init - If command times out, channel is likely pre-configured (this is normal)
- If not pre-configured, radio returns
ERR_CODE_NOT_FOUND(2) on send attempts
- App attempts to configure via
- Format:
Rooms (ADV_TYPE_ROOM contacts):
- Named contacts with public keys
- Persistent and immutable storage
- Use
CMD_SEND_TXT_MSGwith room's public key (direct message) - Optional: Login with
CMD_SEND_LOGINto read stored messages
SAR Message Routing:
- SAR markers MUST be sent to rooms, NOT public channel
- Rooms provide reliable delivery and storage for critical SAR data
CRITICAL: ACK Behavior - Channels vs. Direct Messages
⚠️ IMPORTANT: Channel Messages DO NOT Generate ACKs
Channel Messages (Public Channel):
CMD_SEND_CHANNEL_TXT_MSGuses fire-and-forget flood routing- NO individual ACKs from receivers
- Messages broadcast to all nearby nodes using shared channel encryption
- All subscribers in range receive and decrypt, but do NOT acknowledge
- Rationale: Multiple receivers would cause ACK explosion on mesh network
- Reliability: Best-effort delivery only
Direct Messages (Contact/Room DMs):
CMD_SEND_TXT_MSGto specific contact's public key- Recipient automatically generates ACK packet when message received
- ACK format: 4-byte checksum =
SHA256(timestamp + text + sender_pubkey)→ first 4 bytes - ACK routed back via same/reciprocal path using
PAYLOAD_TYPE_ACK (0x03) - Companion radio sends
PUSH_CODE_SEND_CONFIRMED (0x82)when ACK received - Multi-hop retry: Optional extra ACK transmissions at 300ms intervals for reliability
Room Server Messages (Special Case):
- Messages to room server (ADV_TYPE_ROOM) are sent as DMs
- Room server ACKs when message is stored successfully
- When room server pushes stored messages to clients, each client ACKs back
- Room tracks pending ACKs per client with 12s timeout (flood) or 4+s (direct)
ACK Checksum Calculation:
SHA256_first_4_bytes(
timestamp (4 bytes) +
flags (1 byte) +
message_text (N bytes) +
sender_public_key (32 bytes)
)
UI Implications:
- Channel messages: Show "Broadcast" status (no ACK count)
- Direct messages: Show ACK status when
PUSH_CODE_SEND_CONFIRMEDreceived - Room messages: Show ACK when room server confirms storage
Reference Files:
- Protocol:
/Users/dz0ny/meshcore-sar/MeshCore/docs/payloads.md(lines 58-65) - Implementation:
/Users/dz0ny/meshcore-sar/MeshCore/src/Mesh.cpp(lines 348-374, 529-556) - Client:
/Users/dz0ny/meshcore-sar/MeshCore/src/helpers/BaseChatMesh.cpp(lines 312-331) - Room Server:
/Users/dz0ny/meshcore-sar/MeshCore/examples/simple_room_server/MyMesh.cpp(lines 53-113)
Room Login Protocol Flow (CRITICAL)
- Client sends
CMD_SEND_LOGIN(26): Radio internally generates sender_timestamp and sync_since - Room server processes login: Validates password, stores sync_since, delays first push 2000ms
- Client receives response:
PUSH_CODE_LOGIN_SUCCESS(0x85) orPUSH_CODE_LOGIN_FAIL(0x86) - Room server auto-pushes messages: Round-robin every 1200ms, sends messages where post_timestamp > sync_since
- Client receives pushed messages:
PUSH_CODE_MSG_WAITING(0x83) → callCMD_SYNC_NEXT_MESSAGE(10)
Implementation Rules:
- ❌ DO NOT call
syncAllMessages()afterPUSH_CODE_LOGIN_SUCCESS - ✅ DO wait for
PUSH_CODE_MSG_WAITINGpush notifications - ✅ DO call
syncNextMessage()whenonMessageWaitingfires
Cayenne LPP Format
Format: [Channel] [Type] [Data...]
Supported Types:
- 136 (0x88): GPS Location (lat/lon/alt: int32/10000, int32/10000, int32/100)
- 103 (0x67): Temperature (int16/10 for °C)
- 2 (0x02): Analog Input (uint16/100 for volts, used for battery)
SAR Message Format
Format: S:<emoji>:<latitude>,<longitude>:<optional_message>
Recognized Emojis:
- 🧑 or 👤: Found Person
- 🔥: Fire Location
- 🏕️ or ⛺: Staging Area
Rules:
- Must start with
S: - Single emoji after first colon
- Comma-separated lat/lon coordinates
- Optional message after third colon (displayed in message bubble)
- No spaces in coordinates section
Examples:
S:🧑:37.7749,-122.4194- Basic SAR markerS:🔥:40.7128,-74.0060:Large wildfire spreading rapidly- With messageS:🏕️:34.0522,-118.2437:Base camp established, supplies available- With detailed note
Message Display:
- SAR markers shown with highlighted colored bubble
- Emoji, type name, and coordinates always displayed
- Optional message shown in secondary container below coordinates
- Tap to navigate to location on map
Map Drawing Message Format
Format: D:<json>
Ultra-Compact JSON Format:
- Prefix:
D:identifies drawing messages - Sender: Extracted from packet metadata (not in JSON)
- Type field (
t): Shape type as integer0: Line drawing1: Rectangle drawing
- Color field (
c): Color index (0-7)0: Red,1: Blue,2: Green,3: Yellow4: Orange,5: Purple,6: Pink,7: Cyan
- Points field (
p): Flat array of coordinates[lat1,lon1,lat2,lon2,...] - Bounds field (
b): Rectangle bounds[topLat,topLon,botLat,botLon]
Example Line Drawing (red, 2 points):
D:{"t":0,"c":0,"p":[45.123,-122.456,45.234,-122.567]}
Example Rectangle Drawing (blue):
D:{"t":1,"c":1,"b":[45.1,-122.5,45.2,-122.4]}
Implementation Details:
- Models:
lib/models/map_drawing.dart(MapDrawing, LineDrawing, RectangleDrawing) - Parser:
lib/utils/drawing_message_parser.dart(DrawingMessageParser) - Provider:
lib/providers/drawing_provider.dart(DrawingProvider) - Colors: 8 predefined colors mapped to indices for bandwidth efficiency
- Local persistence uses full JSON format with timestamps and IDs
- Network transmission uses ultra-compact format (~37% size reduction)
State Management Architecture
Provider Hierarchy
MultiProvider
├── ConnectionProvider # BLE connection state
├── ContactsProvider # Contact list
├── MessagesProvider # Messages + SAR markers
├── MapProvider # Map navigation
├── DrawingProvider # Map drawing state
└── AppProvider # Coordinator (uses all above)
Event Flow
BLE Device → MeshCoreBleService → ConnectionProvider → AppProvider
↓
ContactsProvider
MessagesProvider
DrawingProvider
↓
UI
Drawing Message Flow:
User draws → DrawingProvider → DrawingToolbar (share) → ConnectionProvider (BLE)
↓
Remote User ← UI ← DrawingProvider ← AppProvider ← ConnectionProvider ← BLE Device
Contact Types:
- none(0): Unknown/invalid
- chat(1): Team member (shown on map)
- repeater(2): Network repeater node
- room(3): Communication channel/room
Contact Path Status (outPathLen):
- -1 (0xFF): Path not learned yet → Flood mode (broadcasts to all neighbors)
- 0: Direct connection, zero hops → Direct mode (best quality)
- 1+: Multi-hop path with N hops → Direct mode (uses learned routing)
CRITICAL: outPathLen >= 0 means contact has a learned path and will use direct routing.
Only outPathLen == -1 will use flood mode. The hasPath getter in Contact model
correctly checks outPathLen >= 0 && outPathLen <= 64.
Map Display: Only ContactType.chat with valid GPS shown on map
Service Layer
LocationTrackingService (Singleton)
Purpose: GPS tracking + intelligent mesh network location broadcasting
Key Features:
- Callback-based architecture (onPositionUpdate, onError, onBroadcastSent, onTrackingStateChanged)
- Configurable thresholds (minDistanceMeters: 5.0m, maxDistanceMeters: 100.0m, minTimeIntervalSeconds: 30s)
- Smart broadcasting: First = immediate, ≥100m = immediate, ≥5m + ≥30s = broadcast
- Haversine distance calculation for GPS accuracy
Files: lib/services/location_tracking_service.dart (501 lines)
MapMarkerService (Singleton)
Purpose: Map marker generation + geodesic calculations
Key Features:
- Pure functions (testability + performance)
- Contact markers (battery badge, distance from user)
- SAR markers (color-coded: green=person, red=fire, orange=staging)
- Calculate distance, bearing/azimuth, format distance display
- Automatic "time ago" labels
Files: lib/services/map_marker_service.dart (518 lines)
ValidationService (Singleton)
Purpose: Form validation + input parsing with structured error handling
Key Features:
- Structured result types (
ValidationResult,ParseResult<T>) - Coordinate validation (lat: -90 to +90, lon: -180 to +180)
- Radio parameters (freq: 137-1020 MHz, bw: 7.8-500 kHz, sf: 5-12, cr: 5-8, tx: -9 to +22 dBm)
- Text/name validation, zoom level (0-19)
Files: lib/services/validation_service.dart (511 lines)
Map Implementation
Tile Layers
- OpenStreetMap (default): Max zoom 19, street-level navigation
- OpenTopoMap: Max zoom 17, topographic features
- ESRI World Imagery: Max zoom 19, satellite imagery
Offline Tile Caching
- Backend:
flutter_map_tile_cachingwith ObjectBox - Behavior:
CacheBehavior.cacheFirst, 30-day validity - Region downloads:
RectangleRegion(bounds)→store.download.startForeground()
Map Markers
Team Member (Blue): CircleAvatar, battery badge, name label, tap for details SAR Event (Color-coded): Green (person), Red (fire), Orange (staging), time ago label, type label, tap for details
Map Navigation
Message tab → tap SAR marker → MapProvider.navigateToLocation() → switch to Map tab → MapTab._handleMapNavigation() → MapProvider.clearNavigation()
User Location Tracking
- Permission:
NSLocationWhenInUseUsageDescription,NSLocationTemporaryPreciseUsageDescription - Accuracy:
LocationAccuracy.best, distance filter: 10m - Marker: Blue pulsing circle, navigation icon, tap to center/track
Map Legend
Collapsible legend (top-right), shows counts of team members and SAR markers
Detailed Compass Dialog
Ultra-compact location display, tap to toggle DD/DMS formats, no close button (tap outside to close)
Common Development Tasks
Adding a New BLE Command
- Add command code to
lib/services/meshcore_constants.dart - Add frame builder in
lib/services/protocol/frame_builder.dart - Add public API method in
lib/services/meshcore_ble_service.dart - Add response parser in
lib/services/protocol/frame_parser.dart - Handle response in
lib/services/ble/ble_response_handler.dart - Add callback in
lib/services/meshcore_ble_service.dart
Adding a New SAR Marker Type
- Update enum in
lib/models/sar_marker.dart - Add to parser in
lib/utils/sar_message_parser.dart - Add color in
lib/widgets/map_markers.dart - Update providers in
lib/providers/messages_provider.dart
Adding a New Map Layer
- Add to model in
lib/models/map_layer.dart - Add to
allLayerslist - Layer appears automatically in layer selector UI
Working with Map Drawings
Drawing Flow:
- User selects drawing mode (line/rectangle) →
DrawingProvider.setDrawingMode() - User taps map → touch events captured by
DrawingLayer - Preview rendered during drawing →
DrawingProvider.getPreviewDrawing() - User completes drawing → saved to
DrawingProvider._drawingslist - User shares drawing →
DrawingToolbar._shareDrawingsToChannel()or_shareDrawingsToRoom() - Message sent via BLE →
ConnectionProvider.sendChannelMessage()orsendTextMessage() - Receiver parses message →
DrawingMessageParser.parseDrawingMessage()with sender from packet - Drawing added to map →
DrawingProvider.addReceivedDrawing()
Color Management:
- UI uses
DrawingColors.palette(8 Flutter Color objects) - Network uses color indices (0-7) via
DrawingColors.colorToIndex()/indexToColor() - Persistence uses full ARGB32 color values
Key Files:
- Models:
lib/models/map_drawing.dart(278 lines) - Parser:
lib/utils/drawing_message_parser.dart(45 lines) - Provider:
lib/providers/drawing_provider.dart(280 lines) - UI:
lib/widgets/map/drawing_toolbar.dart,lib/widgets/map/drawing_layer.dart
Internationalization (i18n)
Supported Languages
- English (en): Default language
- Croatian (hr): Hrvatski - Full localization
- Slovenian (sl): Slovenščina - Full localization
Localization Files
- ARB files:
lib/l10n/app_{locale}.arb(Application Resource Bundle) - Generated class:
lib/l10n/app_localizations.dart(auto-generated, do not edit) - Configuration:
l10n.yamlin project root
Adding Localized Strings
- Add to English ARB (
lib/l10n/app_en.arb):
{
"myNewString": "My new text",
"@myNewString": {
"description": "Description of what this string is for"
},
"stringWithParam": "Hello {name}",
"@stringWithParam": {
"description": "Greeting with name parameter",
"placeholders": {
"name": {"type": "String"}
}
}
}
-
Add translations to
app_hr.arbandapp_sl.arb -
Generate localization files:
flutter gen-l10n
- Use in code:
import '../l10n/app_localizations.dart';
// In build method:
Text(AppLocalizations.of(context)!.myNewString)
Text(AppLocalizations.of(context)!.stringWithParam('John'))
Important Notes
- Import path: Always use
import '../l10n/app_localizations.dart'(relative path from widget location) - Do NOT use:
import 'package:flutter_gen/gen_l10n/app_localizations.dart'(incorrect) - Generate after changes: Run
flutter gen-l10nafter modifying ARB files - Null safety: Use
AppLocalizations.of(context)!(with null assertion operator)
Localized Components
All major UI components are fully localized:
- Home screen and status indicators
- Settings screen and preferences
- Messages tab and SAR markers
- Contacts tab and contact details
- Map screen and compass dialog
- Drawing tools and filters
- All dialogs and confirmation messages
Build Commands
# Dependencies
flutter pub get
# Localization
flutter gen-l10n # Generate localization files after ARB changes
# Development
flutter run # Debug mode
flutter run -d <device> # Specific device
# Hot reload: press 'r' | Hot restart: press 'R'
# Code Quality
flutter analyze # Static analysis
flutter test # Run tests
dart format lib/ # Format code
flutter clean # Clean build
# iOS
flutter build ios --release
flutter build ipa
# Android
flutter build apk --debug
flutter build apk --release
flutter build appbundle --release
Troubleshooting
BLE Issues
- "Bluetooth adapter not available": Check Bluetooth on, verify permissions, check Info.plist/AndroidManifest.xml
- "Connection failed": BLE support required, check service UUID, verify range (<10m), try scanning again
Runtime Issues
MissingPluginException: Native plugin not installed (common after adding dependencies)
cd ios && pod install && cd .. && flutter clean && flutter pub get && flutter run
Build Issues
iOS Pod Install Fails:
cd ios && rm Podfile.lock && rm -rf Pods/ && pod install --repo-update && cd ..
CocoaPods ObjectBox Version Conflict:
cd ios && rm Podfile.lock && rm -rf Pods/ && pod repo update && pod install && cd ..
flutter clean && flutter pub get && flutter run
Android Gradle Timeout: Add to android/gradle.properties:
org.gradle.daemon=true
org.gradle.parallel=true
org.gradle.jvmargs=-Xmx4096m
Flutter Version Conflicts:
flutter channel stable && flutter upgrade && flutter pub upgrade
Performance Optimization
BLE Communication
- Buffer incoming data for partial packets
- Throttle telemetry requests (max 1/sec per contact)
- Use
notifyListeners()sparingly
Map Performance
- Limit visible markers (cluster if >100)
- Use
repaint boundaryfor marker widgets - Implement marker virtualization for large datasets
Memory Management
- Dispose controllers in
dispose()methods - Clear message history after 1000 messages
- Implement tile cache size limits
Security Considerations
- BLE: No authentication in current protocol - add encryption for production
- Permissions: Request minimum required
- Data: No sensitive data logging
- Network: HTTPS for all tile sources