- Updated ConnectionProvider to track pending messages before sending to avoid race conditions. - Integrated ValidationService in DeviceConfigScreen for validating latitude, longitude, frequency, and TX power inputs. - Added bounds and zoom level validation in MapManagementScreen using ValidationService. - Refactored MapTab to utilize LocationTrackingService for location updates and removed deprecated location permission handling. - Replaced BackgroundLocationService with LocationTrackingService in SettingsScreen, improving location update management and error handling. - Enhanced SAR Update Sheet to validate coordinates, notes length, and location accuracy before sending SAR markers.
54 KiB
CLAUDE.md - MeshCore SAR Technical Reference
This document provides technical details for AI assistants (like Claude) working with this codebase.
⚠️ IMPORTANT: Flutter Development Rules
NEVER run or kill Flutter processes:
- DO NOT execute
flutter runcommand - DO NOT kill Flutter processes (e.g.,
pkill flutter,killall flutter) - The user manages the Flutter development server themselves
- Only make code changes and let the user trigger hot reload manually
Hot reload happens automatically when you save files - the user has their own Flutter process running and will see changes instantly.
Project Overview
Type: Flutter Mobile Application Purpose: Search and Rescue (SAR) operations with MeshCore mesh network devices Architecture: Provider-based state management with BLE communication Target Platforms: iOS 13+, Android 5.0+ (API 21+)
Project Structure
lib/
├── models/ # Data models
│ ├── contact.dart # Contact with telemetry
│ ├── contact_telemetry.dart # GPS, battery, temperature
│ ├── message.dart # Messages and SAR markers
│ ├── sar_marker.dart # SAR tactical markers
│ ├── device_info.dart # BLE device connection state
│ ├── room_login_state.dart # Room login status tracking
│ └── map_layer.dart # Map tile layer definitions
├── services/ # Business logic services
│ ├── meshcore_ble_service.dart # BLE service coordinator (399 lines)
│ ├── meshcore_constants.dart # Protocol constants
│ ├── buffer_reader.dart # Binary protocol reader
│ ├── buffer_writer.dart # Binary protocol writer
│ ├── cayenne_lpp_parser.dart # Telemetry decoder
│ ├── tile_cache_service.dart # Offline map tiles
│ ├── background_location_service.dart # Background GPS tracking (legacy)
│ ├── protocol/ # Protocol layer (628 lines)
│ │ ├── frame_parser.dart # Parse incoming BLE frames
│ │ └── frame_builder.dart # Build outgoing BLE frames
│ ├── ble/ # BLE layer (963 lines)
│ │ ├── ble_connection_manager.dart # Connection lifecycle
│ │ ├── ble_command_sender.dart # Command transmission
│ │ └── ble_response_handler.dart # Response processing
│ ├── location_tracking_service.dart # GPS tracking & mesh broadcasting (501 lines)
│ ├── map_marker_service.dart # Marker generation & geodesic calculations (518 lines)
│ └── validation_service.dart # Form validation & input parsing (511 lines)
├── providers/ # State management
│ ├── connection_provider.dart # BLE connection state (957 lines)
│ ├── contacts_provider.dart # Contact list management
│ ├── messages_provider.dart # Message history + SAR markers
│ ├── map_provider.dart # Map navigation state
│ ├── app_provider.dart # Coordinator provider
│ └── helpers/ # Provider helpers (155 lines)
│ ├── room_login_manager.dart # Room login state management
│ └── message_delivery_tracker.dart # Message delivery tracking
├── screens/ # UI screens
│ ├── home_screen.dart # Main screen with tabs
│ ├── messages_tab.dart # Message list view (767 lines)
│ ├── contacts_tab.dart # Contact list view (208 lines)
│ ├── map_tab.dart # Interactive map view (1,217 lines)
│ ├── settings_screen.dart # App settings (875 lines)
│ ├── device_config_screen.dart # Device configuration (718 lines)
│ ├── map_management_screen.dart # Map tile management (728 lines)
│ └── packet_log_screen.dart # BLE packet diagnostics (565 lines)
├── widgets/ # Reusable UI components
│ ├── map_markers.dart # Custom map marker widgets
│ ├── messages/ # Message components (698 lines)
│ │ └── sar_update_sheet.dart # SAR marker creation modal
│ ├── contacts/ # Contact components (1,578 lines)
│ │ ├── contact_tile.dart # Contact list tile + details dialog
│ │ ├── direct_message_sheet.dart # Direct messaging modal
│ │ ├── room_login_sheet.dart # Room login modal
│ │ └── section_header.dart # Section header component
│ └── map/ # Map components (2,867 lines)
│ ├── detailed_compass_dialog.dart # Main compass dialog (570 lines)
│ ├── map_legend.dart # Map legend with counts
│ ├── compass_widget.dart # Small compass widget
│ └── compass/ # Compass subcomponents (1,150 lines)
│ ├── compass_header.dart # Compass rose + location display
│ ├── compass_filters.dart # Filter controls
│ ├── compass_sar_list.dart # SAR marker list
│ └── compass_contact_list.dart # Contact list
├── utils/ # Utilities
│ └── sar_message_parser.dart # Parse S:<emoji>:lat,lon format
└── main.dart # App entry point
Key Technologies
Core Dependencies
- flutter_blue_plus (^2.0.0): BLE communication
- flutter_map (^8.2.2): Interactive mapping with OpenStreetMap
- flutter_map_tile_caching (^10.1.1): Offline map tile storage
- provider (^6.1.0): State management
- latlong2 (^0.9.0): GPS coordinate handling
- geolocator (^14.0.2): Precise GPS location tracking
- permission_handler (^12.0.1): Runtime permissions
MeshCore Protocol
The app implements the MeshCore BLE Companion Radio protocol based on https://github.com/meshcore-dev/meshcore.js
BLE Service: 6E400001-B5A3-F393-E0A9-E50E24DCCA9E
RX Characteristic (write): 6E400002-B5A3-F393-E0A9-E50E24DCCA9E
TX Characteristic (notify): 6E400003-B5A3-F393-E0A9-E50E24DCCA9E
Protocol Overview
The companion radio acts as a 'server', responding to requests from the connected app (the 'client').
Frame Delimiters:
- BLE: A frame is a single characteristic value (BLE link layer handles integrity)
- USB:
- Outbound (radio → app): Starts with
>(0x3E), 2-byte length (LE), then frame data - Inbound (app → radio): Starts with
<(0x3C), 2-byte length (LE), then frame data
- Outbound (radio → app): Starts with
NOTE: All uint32 values use Little Endian byte order!
Command Codes (App → Radio)
| Code | Name | Description |
|---|---|---|
| 1 | CMD_APP_START | First command after connection, returns 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 message to channel |
| 4 | CMD_GET_CONTACTS | Sync contacts list (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 (optional flood-mode) |
| 8 | CMD_SET_ADVERT_NAME | Update node name in advertisements |
| 9 | CMD_ADD_UPDATE_CONTACT | Add or modify a contact |
| 10 | CMD_SYNC_NEXT_MESSAGE | Get next text message from queue |
| 11 | CMD_SET_RADIO_PARAMS | Save new radio parameters |
| 12 | CMD_SET_RADIO_TX_POWER | Set radio TX power level |
| 13 | CMD_RESET_PATH | Reset out_path for a contact |
| 14 | CMD_SET_ADVERT_LATLON | Update lat/lon in advertisements |
| 15 | CMD_REMOVE_CONTACT | Remove a 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, returns RESP_CODE_DEVICE_INFO(13) |
| 25 | CMD_SEND_RAW_DATA | Transmit PAYLOAD_TYPE_RAW_CUSTOM packet |
| 26 | CMD_SEND_LOGIN | Send login request to repeater/room |
| 27 | CMD_SEND_STATUS_REQ | Send status request to repeater/sensor |
| 36 | CMD_SEND_TRACE_PATH | Initiate TRACE packet with SNR collection |
| 37 | CMD_SET_DEVICE_PIN | Set BLE PIN code |
| 38 | CMD_SET_OTHER_PARAMS | Set various other parameters |
| 39 | CMD_SEND_TELEMETRY_REQ | Send telemetry request to node |
| 40 | CMD_GET_CUSTOM_VARS | Retrieve all custom variables |
| 41 | CMD_SET_CUSTOM_VAR | Set single custom variable |
| 42 | CMD_GET_ADVERT_PATH | Query last advert path for contact |
| 43 | CMD_GET_TUNING_PARAMS | Get airtime-factor and rx-delay settings |
| 50 | CMD_SEND_BINARY_REQ | Send binary request to node (preferred over CMD_SEND_TELEMETRY_REQ) |
| 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 of contacts sync sequence |
| 3 | RESP_CODE_CONTACT | Single contact information |
| 4 | RESP_CODE_END_OF_CONTACTS | End of contacts sync sequence |
| 5 | RESP_CODE_SELF_INFO | Node's own information |
| 6 | RESP_CODE_SENT | Message sent with expected 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 and hardware info |
| 21 | RESP_CODE_CUSTOM_VARS | Custom variables state |
| 22 | RESP_CODE_ADVERT_PATH | Last advert path for contact |
Push Notifications (Radio → App, Async)
| Code | Name | Description |
|---|---|---|
| 0x80 | PUSH_CODE_ADVERT | New advertisement packet 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 response successful |
| 0x86 | PUSH_CODE_LOGIN_FAIL | Login response failed |
| 0x87 | PUSH_CODE_STATUS_RESPONSE | Status response received |
| 0x88 | PUSH_CODE_LOG_RX_DATA | Debug: raw over-the-air packet received (diagnostic) |
| 0x89 | PUSH_CODE_TRACE_DATA | TRACE packet reached end of path |
| 0x8A | PUSH_CODE_NEW_ADVERT | New contact advert (manual_add_contacts=1) |
| 0x8B | PUSH_CODE_TELEMETRY_RESPONSE | Telemetry response received |
| 0x8C | PUSH_CODE_BINARY_RESPONSE | Binary response received |
Frame Formats
CMD_DEVICE_QUERY (22):
[0x16] - Command code (22)
[1 byte] - App target version (protocol version app understands)
RESP_CODE_DEVICE_INFO (13):
[0x0D] - Response code (13)
[1 byte] - Firmware version
[1 byte] - Max contacts ÷ 2 (ver 3+)
[1 byte] - Max channels (ver 3+)
[4 bytes] - BLE PIN (uint32, ver 3+)
[12 bytes] - Firmware build date (ASCII null-terminated, e.g., "19 Feb 2025")
[40 bytes] - Manufacturer model (ASCII null-terminated)
[20 bytes] - Semantic version (ASCII null-terminated)
CMD_APP_START (1):
[0x01] - Command code (1)
[1 byte] - App version
[6 bytes] - Reserved (zeros)
[N bytes] - App name (remainder of frame, varchar)
RESP_CODE_SELF_INFO (5):
[0x05] - Response code (5)
[1 byte] - Type (ADV_TYPE_*)
[1 byte] - TX power in dBm (current)
[1 byte] - Max TX power radio supports
[32 bytes] - Public key
[4 bytes] - Advert latitude * 1E6 (int32)
[4 bytes] - Advert longitude * 1E6 (int32)
[1 byte] - Multi ACKs (0=no extra, 1=send extra ACK)
[1 byte] - Advert location policy (0=don't share, 1=share)
[1 byte] - Telemetry modes (bits 0-1: Base mode, bits 2-3: Location mode)
Modes: 0=DENY, 1=apply contact.flags, 2=ALLOW ALL
[1 byte] - Manual add contacts (0 or 1)
[4 bytes] - Radio freq * 1000 (uint32)
[4 bytes] - Radio bandwidth (kHz) * 1000 (uint32)
[1 byte] - Spreading factor
[1 byte] - Coding rate
[N bytes] - Name (remainder of frame, varchar)
CMD_GET_CONTACTS (4):
[0x04] - Command code (4)
[4 bytes] - (Optional) Since timestamp (uint32, last contact.lastmod received)
RESP_CODE_CONTACTS_START (2):
[0x02] - Response code (2)
[4 bytes] - Total contact count (uint32)
RESP_CODE_CONTACT (3):
[0x03] - Response code (3)
[32 bytes] - Public key
[1 byte] - Type (ADV_TYPE_*)
[1 byte] - Flags
[1 byte] - Out path length (signed)
[64 bytes] - Out path
[32 bytes] - Advertised name (null-terminated)
[4 bytes] - Last advert timestamp (uint32)
[4 bytes] - Advert latitude * 1E6 (int32)
[4 bytes] - Advert longitude * 1E6 (int32)
[4 bytes] - Last modified timestamp (uint32)
RESP_CODE_END_OF_CONTACTS (4):
[0x04] - Response code (4)
[4 bytes] - Most recent lastmod (uint32, use for next 'since' param)
CMD_SET_DEVICE_TIME (6):
[0x06] - Command code (6)
[4 bytes] - Epoch seconds (uint32)
RESP_CODE_CURR_TIME (9):
[0x09] - Response code (9)
[4 bytes] - Epoch seconds (uint32)
CMD_SEND_SELF_ADVERT (7):
[0x07] - Command code (7)
[1 byte] - (Optional) Type: 1=flood, 0=zero-hop (default)
CMD_SET_ADVERT_NAME (8):
[0x08] - Command code (8)
[N bytes] - Name (remainder of frame, varchar)
CMD_SET_ADVERT_LATLON (14):
[0x0E] - Command code (14)
[4 bytes] - Latitude * 1E6 (int32)
[4 bytes] - Longitude * 1E6 (int32)
[4 bytes] - (Optional) Altitude (int32, future support)
CMD_ADD_UPDATE_CONTACT (9):
[0x09] - Command code (9)
[32 bytes] - Public key
[1 byte] - Type (ADV_TYPE_*)
[1 byte] - Flags
[1 byte] - Out path length (signed)
[64 bytes] - Out path
[32 bytes] - Advertised name (null-terminated)
[4 bytes] - Last advert timestamp (uint32)
[4 bytes] - (Optional) Advert latitude * 1E6 (int32)
[4 bytes] - (Optional) Advert longitude * 1E6 (int32)
CMD_REMOVE_CONTACT (15):
[0x0F] - Command code (15)
[32 bytes] - Public key
CMD_SHARE_CONTACT (16):
[0x10] - Command code (16)
[32 bytes] - Public key
CMD_EXPORT_CONTACT (17):
[0x11] - Command code (17)
[32 bytes] - (Optional) Public key (if omitted, export SELF)
RESP_CODE_EXPORT_CONTACT (11):
[0x0B] - Response code (11)
[N bytes] - Card data (remainder of frame)
Format: "meshcore://{hex(card_data)}"
CMD_IMPORT_CONTACT (18):
[0x12] - Command code (18)
[N bytes] - Card data (remainder of frame)
CMD_RESET_PATH (13):
[0x0D] - Command code (13)
[32 bytes] - Public key
CMD_SEND_TXT_MSG (2):
[0x02] - Command code (2)
[1 byte] - Text type (TXT_TYPE_*, 0=plain)
[1 byte] - Attempt (0-3, attempt number)
[4 bytes] - Sender timestamp (uint32)
[6 bytes] - Recipient public key prefix (first 6 bytes)
[N bytes] - Text (remainder of frame, varchar, max 160 bytes)
CMD_SEND_CHANNEL_TXT_MSG (3):
[0x03] - Command code (3)
[1 byte] - Text type (TXT_TYPE_*, 0=plain)
[1 byte] - Channel index (reserved, 0 for 'public')
[4 bytes] - Sender timestamp (uint32)
[N bytes] - Text (remainder of frame, max 160 - len(advert_name) - 2)
RESP_CODE_SENT (6):
[0x06] - Response code (6)
[1 byte] - Send type: 1=flood, 0=direct
[4 bytes] - Expected ACK code or TAG
[4 bytes] - Suggested timeout (uint32, milliseconds)
PUSH_CODE_SEND_CONFIRMED (0x82):
[0x82] - Push code
[4 bytes] - ACK code
[4 bytes] - Round trip time (uint32, milliseconds)
RESP_CODE_CONTACT_MSG_RECV (7):
[0x07] - Response code (7)
[6 bytes] - Sender public key prefix (first 6 bytes)
[1 byte] - Path length (0xFF if direct, else hop count for flood-mode)
[1 byte] - Text type (TXT_TYPE_*, 0=plain, 2=signed)
[4 bytes] - Sender timestamp (uint32)
[4 bytes] - (Only if text type = 2) Extra sender prefix bytes for verification
[N bytes] - Text (remainder of frame, varchar)
Note on TXT_TYPE_SIGNED_PLAIN (2): Despite the name "signed", this doesn't contain a cryptographic signature. It includes 4 extra bytes of the sender's public key prefix (bytes 6-9) for additional verification. The text follows immediately after these 4 bytes.
RESP_CODE_CHANNEL_MSG_RECV (8):
[0x08] - Response code (8)
[1 byte] - Channel index (reserved, 0 for 'public')
[1 byte] - Path length (0xFF if direct, else hop count for flood-mode)
[1 byte] - Text type (TXT_TYPE_*, 0=plain)
[4 bytes] - Sender timestamp (uint32)
[N bytes] - Text (remainder of frame, varchar)
CMD_SET_RADIO_PARAMS (11):
[0x0B] - Command code (11)
[4 bytes] - Radio freq * 1000 (uint32)
[4 bytes] - Radio bandwidth (kHz) * 1000 (uint32)
[1 byte] - Spreading factor
[1 byte] - Coding rate
CMD_SET_RADIO_TX_POWER (12):
[0x0C] - Command code (12)
[1 byte] - TX power in dBm
CMD_SET_TUNING_PARAMS (21) / RESP_CODE_TUNING_PARAMS:
[0x15] - Command/Response code (21)
[4 bytes] - RX delay base * 1000 (uint32)
[4 bytes] - Airtime factor * 1000 (uint32)
[8 bytes] - Reserved (set to zero)
CMD_SET_OTHER_PARAMS (38):
[0x26] - Command code (38)
[1 byte] - Manual add contacts (0 or 1)
[1 byte] - (Optional v5+) Telemetry modes
[1 byte] - (Optional v5+) Advert location policy
[1 byte] - (Optional v7+) Multi ACKs (0=no extra, 1=send extra)
RESP_CODE_BATT_AND_STORAGE (12):
[0x0C] - Response code (12)
[2 bytes] - Millivolts (uint16)
[4 bytes] - (Optional) Used KB (uint32)
[4 bytes] - (Optional) Total KB (uint32, zero if unknown)
CMD_SEND_RAW_DATA (25):
[0x19] - Command code (25)
[1 byte] - Path length
[N bytes] - Path (variable length)
[M bytes] - Payload (remainder of frame)
PUSH_CODE_RAW_DATA (0x84):
[0x84] - Push code
[1 byte] - SNR * 4 (signed)
[1 byte] - RSSI (signed)
[1 byte] - Reserved (0xFF)
[N bytes] - Payload (remainder of frame)
CMD_SEND_LOGIN (26):
[0x1A] - Command code (26)
[32 bytes] - Public key (repeater or room server)
[N bytes] - Password (remainder of frame, varchar, max 15 bytes, null-terminated)
NOTE: The companion radio's sendLogin() function internally generates the sender_timestamp and sync_since parameters when creating the over-the-air packet. The BLE protocol does NOT accept these parameters.
PUSH_CODE_LOGIN_SUCCESS (0x85):
[0x85] - Push code
[1 byte] - Permissions (lowest bit=is_admin)
[6 bytes] - Public key prefix (first 6 bytes)
[4 bytes] - Tag (int32)
[1 byte] - (V7+) New permissions
CMD_SEND_STATUS_REQ (27):
[0x1B] - Command code (27)
[32 bytes] - Public key (repeater or sensor)
PUSH_CODE_STATUS_RESPONSE (0x87):
[0x87] - Push code
[1 byte] - Reserved (zero)
[6 bytes] - Public key prefix (first 6 bytes)
[N bytes] - Status data (remainder of frame)
CMD_SEND_TELEMETRY_REQ (39):
[0x27] - Command code (39)
[3 bytes] - Reserved (zeros)
[32 bytes] - Public key (destination node)
PUSH_CODE_TELEMETRY_RESPONSE (0x8B):
[0x8B] - Push code
[1 byte] - Reserved (zero)
[6 bytes] - Public key prefix (first 6 bytes)
[N bytes] - LPP sensor data (Cayenne LPP format, remainder of frame)
CMD_SEND_BINARY_REQ (50) (Preferred over CMD_SEND_TELEMETRY_REQ):
[0x32] - Command code (50)
[32 bytes] - Public key (contact to send request to)
[N bytes] - Request code and params (remainder of frame)
PUSH_CODE_BINARY_RESPONSE (0x8C):
[0x8C] - Push code
[1 byte] - Reserved (zero)
[4 bytes] - Tag (uint32, matches RESP_CODE_SENT expected_ack_or_tag)
[N bytes] - Response data (remainder of frame)
CMD_SEND_TRACE_PATH (36):
[0x24] - Command code (36)
[4 bytes] - Tag (int32, random initiator tag)
[4 bytes] - Auth code (int32, optional authentication)
[1 byte] - Flags (zero for now)
[N bytes] - Path (remainder of frame, hashes for TRACE to follow)
PUSH_CODE_TRACE_DATA (0x89):
[0x89] - Push code
[1 byte] - Reserved (zero)
[1 byte] - Path length
[1 byte] - Flags (zero for now)
[4 bytes] - Tag (int32)
[4 bytes] - Auth code (int32)
[N bytes] - Path hashes (variable length)
[N+1 bytes] - Path SNRs (last byte = SNR for last hop, each byte = SNR * 4)
PUSH_CODE_LOG_RX_DATA (0x88) (Diagnostic/Debug Feature):
[0x88] - Push code
[1 byte] - SNR × 4 (signed int8, divide by 4 to get SNR in dB)
[1 byte] - RSSI (signed int8, in dBm)
[N bytes] - Raw over-the-air packet data (encrypted LoRa packet from mesh network)
Purpose: This is a diagnostic push notification that forwards ALL over-the-air packets received by the companion radio to the app, allowing network debugging and signal quality monitoring.
Implementation: Based on MyMesh::logRxRaw() in MeshCore C++ source (MyMesh.cpp lines 237-248).
Usage:
- Monitor mesh network activity in real-time
- Analyze signal quality (SNR/RSSI) for received packets
- Debug packet reception issues
- The raw packet data is typically encrypted (high entropy ~95%+)
- Not part of official protocol documentation (debug feature)
CMD_SET_DEVICE_PIN (37):
[0x25] - Command code (37)
[4 bytes] - BLE PIN (uint32)
CMD_GET_ADVERT_PATH (42):
[0x2A] - Command code (42)
[1 byte] - Reserved (zero)
[32 bytes] - Public key (contact being queried)
RESP_CODE_ADVERT_PATH (22):
[0x16] - Response code (22)
[4 bytes] - Receive timestamp (uint32, by local clock)
[1 byte] - Path length
[N bytes] - Path (variable length)
CMD_FACTORY_RESET (51):
[0x33] - Command code (51)
[5 bytes] - ASCII "reset" (confirmation)
RESP_CODE_ERR (1):
[0x01] - Response code (1)
[1 byte] - Error code (ERR_CODE_*)
Constants
ADV_TYPE (Advertisement/Contact Type):
0- ADV_TYPE_NONE (unknown/invalid)1- ADV_TYPE_CHAT (team member, shown on map)2- ADV_TYPE_REPEATER (network repeater node)3- ADV_TYPE_ROOM (communication room/server - NOT the same as channel index!)
IMPORTANT: Channels vs. Rooms:
- Channels (channel index): Numeric identifiers used with
CMD_SEND_CHANNEL_TXT_MSGfor flood-mode broadcasts- Channel 0 = "Public Channel" (default flood-mode broadcast to all nodes)
- Channel 1+ = Reserved for future use (not currently mapped to room contacts)
- Channels are ephemeral - messages broadcast over the air are NOT persisted
- Rooms (ADV_TYPE_ROOM): Actual named contacts with public keys that provide persistent message storage
- Rooms appear in the Contacts tab as ContactType.room
- Rooms provide persistent and immutable storage - messages are stored even when offline
- To communicate with a room, send direct messages using
CMD_SEND_TXT_MSGwith the room's public key - Optional: Login to rooms using
CMD_SEND_LOGINwith password to read stored messages
Room Login Protocol Flow (CRITICAL - Follow Exactly):
-
Client sends login request (
CMD_SEND_LOGIN, code 26):[0x1A] - Command code (26) [4 bytes] - Sender timestamp (uint32, current epoch seconds) [4 bytes] - sync_since timestamp (uint32, epoch seconds - 0 for all messages) [32 bytes] - Room public key [N bytes] - Password (max 15 bytes, null-terminated) -
Room server processes login (C++ code:
MyMesh::onAnonDataRecv()):- Validates password against
_prefs.password(admin) or_prefs.guest_password(read/write) - Stores
client->extra.room.sync_since = sender_sync_since(line 324 of MyMesh.cpp) - Responds with
PAYLOAD_TYPE_RESPONSEcontaining login result - Sets
next_push = futureMillis(PUSH_NOTIFY_DELAY_MILLIS)to delay first push by 2000ms (line 346)
- Validates password against
-
Client receives login response:
- Success:
PUSH_CODE_LOGIN_SUCCESS(0x85) with permissions, admin flag, tag - Failure:
PUSH_CODE_LOGIN_FAIL(0x86) if password incorrect
- Success:
-
Room server automatically pushes messages (C++ code:
MyMesh::loop()lines 498-542):- Server runs round-robin polling every
SYNC_PUSH_INTERVAL(1200ms) - For each logged-in client, checks if
post_timestamp > client->extra.room.sync_since - Calls
pushPostToClient()which sendsPAYLOAD_TYPE_TXT_MSGdirectly to client - Waits for ACK, then advances
client->extra.room.sync_sinceto next post - Continues until all messages where
post_timestamp > sync_sinceare pushed
- Server runs round-robin polling every
-
Client receives pushed messages as they arrive:
- Each push triggers
PUSH_CODE_MSG_WAITING(0x83) - App's
onMessageWaitingcallback fires automatically - App then calls
CMD_SYNC_NEXT_MESSAGE(10) to fetch each message from device queue - Repeats until
RESP_CODE_NO_MORE_MESSAGES(10) received
- Each push triggers
CRITICAL IMPLEMENTATION RULES:
- ❌ DO NOT call
syncAllMessages()immediately afterPUSH_CODE_LOGIN_SUCCESS - ✅ DO wait for
PUSH_CODE_MSG_WAITINGpush notifications - ✅ DO call
syncNextMessage()whenonMessageWaitingcallback fires - The room server pushes messages automatically - the app only needs to listen and fetch when notified
- Server delays first push by 2000ms to allow login response to arrive first
- Server uses round-robin with 1200ms intervals between push attempts
- Each pushed message requires ACK before server advances to next message
SAR Message Routing:
- SAR markers MUST be sent to rooms, NOT to public channel
- Use
CMD_SEND_TXT_MSGwith the room's public key (direct message to room) - This ensures SAR markers are persisted and immutable in the room's storage
- Public channel (
CMD_SEND_CHANNEL_TXT_MSG) is ephemeral over-the-air only - Rooms provide reliable message delivery and storage for critical SAR data
TXT_TYPE (Text Message Type):
0- TXT_TYPE_PLAIN (plain text message)1- TXT_TYPE_CLI_DATA (CLI command)2- TXT_TYPE_SIGNED_PLAIN (plain text, signed by sender)
ERR_CODE (Error Codes):
1- ERR_CODE_UNSUPPORTED_CMD2- ERR_CODE_NOT_FOUND3- ERR_CODE_TABLE_FULL4- ERR_CODE_BAD_STATE5- ERR_CODE_FILE_IO_ERROR6- ERR_CODE_ILLEGAL_ARG
Cayenne LPP Format
Telemetry data uses Cayenne Low Power Payload format:
[Channel] [Type] [Data...]
Supported Types:
136(0x88): GPS Location- 4 bytes: Latitude (int32, divide by 10000)
- 4 bytes: Longitude (int32, divide by 10000)
- 4 bytes: Altitude (int32, divide by 100)
103(0x67): Temperature Sensor- 2 bytes: Temperature (int16, divide by 10 for °C)
2(0x02): Analog Input (used for battery voltage)- 2 bytes: Value (uint16, divide by 100 for volts)
SAR Message Format
Special tactical markers embedded in messages:
S:<emoji>:<latitude>,<longitude>
Recognized Emojis:
🧑or👤: Found Person🔥: Fire Location🏕️or⛺: Staging Area
Examples:
S:🧑:46.0569,14.5058- Person found at coordinatesS:🔥:46.0570,14.5060- Fire detectedS:🏕️:46.0571,14.5062- Base camp location
Parsing Rules:
- Must start with
S: - Single emoji character after first colon
- Comma-separated lat,lon after second colon
- Coordinates can be negative (e.g.,
-12.3456) - No spaces allowed in format
State Management Architecture
Provider Hierarchy
MultiProvider
├── ConnectionProvider # BLE connection state
├── ContactsProvider # Contact list
├── MessagesProvider # Messages + SAR markers
├── MapProvider # Map navigation
└── AppProvider # Coordinator (uses all above)
Event Flow
BLE Device → MeshCoreBleService → ConnectionProvider → AppProvider
↓
ContactsProvider
MessagesProvider
↓
UI
Example: Receiving a Message
- BLE device sends message via TX characteristic
MeshCoreBleService._onDataReceived()parses binary data- Calls
onMessageReceivedcallback ConnectionProviderreceives messageAppProviderenhances message (check for SAR format)MessagesProvider.addMessage()stores message- UI rebuilds via
Consumer<MessagesProvider>
Contact Types
enum ContactType {
none(0), // Unknown/invalid
chat(1), // Team member (shown on map)
repeater(2), // Network repeater node
room(3), // Communication channel/room
}
Map Display Rules:
- Only
ContactType.chatcontacts with valid GPS are shown on map - Repeaters and rooms are listed in Contacts tab but not mapped
Service Layer Architecture
The app uses a service layer pattern to centralize business logic outside of UI components. Three main services handle location tracking, map operations, and validation.
LocationTrackingService
Purpose: Singleton service for GPS tracking and intelligent mesh network location broadcasting.
Pattern: Callback-based architecture with configurable thresholds.
Initialization:
final locationService = LocationTrackingService();
await locationService.initialize(bleService);
// Set up callbacks
locationService.onPositionUpdate = (position) {
// Handle GPS position updates
print('Position: ${position.latitude}, ${position.longitude}');
};
locationService.onError = (error) {
// Handle errors
showSnackBar(error);
};
locationService.onBroadcastSent = (position) {
// Called when location is broadcast to mesh network
print('Broadcast sent: ${position.latitude}, ${position.longitude}');
};
locationService.onTrackingStateChanged = (isTracking) {
// Called when tracking starts/stops
setState(() => _isTracking = isTracking);
};
Configuration Parameters:
// Minimum distance before considering broadcast (default: 5.0m)
locationService.minDistanceMeters = 5.0;
// Maximum distance that forces immediate broadcast (default: 100.0m)
locationService.maxDistanceMeters = 100.0;
// Minimum time between broadcasts (default: 30s)
locationService.minTimeIntervalSeconds = 30;
// GPS update distance threshold (default: 10.0m)
locationService.gpsUpdateDistance = 10.0;
Smart Broadcasting Logic: The service implements intelligent broadcasting that balances network traffic with position accuracy:
- First broadcast: Always sends immediately (no previous position to compare)
- Maximum distance trigger: If user moves ≥100m (configurable), broadcasts immediately regardless of time
- Combined trigger: If user moves ≥5m (configurable) AND ≥30s have passed since last broadcast, broadcasts
This prevents flooding the mesh network while ensuring position updates are sent when meaningful movement occurs.
Usage Example:
// Request permissions
final granted = await locationService.requestPermissions();
if (!granted) {
showError('Location permission denied');
return;
}
// Start tracking
await locationService.startTracking(distanceThreshold: 10);
// Manual broadcast (bypasses smart logic)
final success = await locationService.broadcastLocationNow();
// Stop tracking
await locationService.stopTracking();
// Check state
if (locationService.isTracking) {
print('Current: ${locationService.currentPosition?.latitude}');
}
Haversine Distance Calculation: The service uses the Haversine formula to calculate accurate distances between GPS coordinates, accounting for Earth's curvature:
double _calculateDistance(Position pos1, Position pos2) {
const earthRadius = 6371000.0; // meters
final dLat = _degreesToRadians(pos2.latitude - pos1.latitude);
final dLon = _degreesToRadians(pos2.longitude - pos1.longitude);
final a = sin(dLat / 2) * sin(dLat / 2) +
cos(_degreesToRadians(pos1.latitude)) * cos(_degreesToRadians(pos2.latitude)) *
sin(dLon / 2) * sin(dLon / 2);
final c = 2 * atan2(sqrt(a), sqrt(1 - a));
return earthRadius * c;
}
MapMarkerService
Purpose: Singleton service for generating map markers and performing geodesic calculations.
Pattern: Pure functions for testability and performance.
Generate Contact Markers:
final markerService = MapMarkerService();
final contactMarkers = markerService.generateContactMarkers(
contacts: contactsWithLocation,
onTap: (contact) => showContactDetails(contact),
userLat: currentUserLatitude, // Optional: for distance calculations
userLon: currentUserLongitude,
);
Generate SAR Markers:
final sarMarkers = markerService.generateSarMarkers(
sarMarkers: allSarMarkers,
onTap: (marker) => showSarMarkerDetails(marker),
);
Calculate Distance Between Points:
final distance = markerService.calculateDistance(
lat1: 46.0569, lon1: 14.5058, // Point A
lat2: 46.0570, lon2: 14.5060, // Point B
);
print('Distance: ${distance.toStringAsFixed(1)}m');
Calculate Bearing/Azimuth:
final bearing = markerService.calculateBearing(
lat1: userLat, lon1: userLon,
lat2: targetLat, lon2: targetLon,
);
print('Bearing: ${bearing.toStringAsFixed(1)}°');
Format Distance for Display:
final formatted = markerService.formatDistance(1234.56);
// Returns: "1.2 km" or "123 m" depending on distance
Marker Features:
- Contact markers show battery level badge and distance from user
- SAR markers are color-coded by type (green=person, red=fire, orange=staging)
- Automatic "time ago" labels (e.g., "5m ago", "2h ago")
- Tap handlers for showing detailed information
- Custom icons and colors per marker type
Implementation Notes:
- All functions are pure (no side effects)
- Uses Haversine formula for accurate geodesic calculations
- Marker widgets are lightweight for performance
- Distance calculations account for Earth's curvature
ValidationService
Purpose: Singleton service for form validation and input parsing with structured error handling.
Pattern: Structured result types (ValidationResult, ParseResult<T>) for type-safe error handling.
Coordinate Validation:
final validator = ValidationService();
// Validate latitude
final latResult = validator.validateLatitude(46.0569);
if (!latResult.isValid) {
showError(latResult.errorMessage!);
}
// Validate longitude
final lonResult = validator.validateLongitude(14.5058);
if (!lonResult.isValid) {
showError(lonResult.errorMessage!);
}
// Validate both coordinates at once
final coordResult = validator.validateCoordinates(
46.0569, // latitude
14.5058, // longitude
);
if (!coordResult.isValid) {
showError(coordResult.errorMessage!);
}
// Validate bounds (for map region downloads)
final boundsResult = validator.validateBounds(
north: 46.10, south: 46.00,
east: 14.60, west: 14.50,
);
Parse + Validate Text Input:
// Parse latitude from text field
final latResult = validator.parseLatitude(latController.text);
if (!latResult.isSuccess) {
showError(latResult.errorMessage!);
return;
}
final latitude = latResult.value!; // Safe to use
// Parse longitude from text field
final lonResult = validator.parseLongitude(lonController.text);
if (!lonResult.isSuccess) {
showError(lonResult.errorMessage!);
return;
}
final longitude = lonResult.value!;
// Parse radio frequency
final freqResult = validator.parseFrequency(freqController.text);
if (!freqResult.isSuccess) {
showError(freqResult.errorMessage!);
return;
}
final frequency = freqResult.value!;
Radio Parameter Validation:
// Frequency (137.0 - 1020.0 MHz)
final freqValidation = validator.validateFrequency(433.5);
// Bandwidth (7.8 - 500.0 kHz)
final bwValidation = validator.validateBandwidth(125.0);
// Spreading Factor (5 - 12)
final sfValidation = validator.validateSpreadingFactor(7);
// Coding Rate (5 - 8)
final crValidation = validator.validateCodingRate(5);
// TX Power (-9 to +22 dBm, device-dependent)
final txValidation = validator.validateTxPower(20, maxTxPower: 22);
Text and Name Validation:
// Validate name (max length)
final nameResult = validator.validateName(
nameController.text,
maxLength: 32,
);
// Validate with minimum length
final passwordResult = validator.validateName(
passwordController.text,
minLength: 4,
maxLength: 15,
);
Zoom Level Validation:
final zoomResult = validator.validateZoomLevel(15);
if (!zoomResult.isValid) {
showError('Zoom: ${zoomResult.errorMessage}');
}
Validation Ranges:
- Latitude: -90.0 to +90.0 (decimal degrees)
- Longitude: -180.0 to +180.0 (decimal degrees)
- Frequency: 137.0 to 1020.0 (MHz)
- Bandwidth: 7.8 to 500.0 (kHz)
- Spreading Factor: 5 to 12
- Coding Rate: 5 to 8
- TX Power: -9 to +22 dBm (max depends on device)
- Zoom Level: 0 to 19
Result Types:
// ValidationResult - for validation only
class ValidationResult {
final bool isValid;
final String? errorMessage;
const ValidationResult.valid() : isValid = true, errorMessage = null;
const ValidationResult.invalid(this.errorMessage) : isValid = false;
}
// ParseResult<T> - for parsing + validation
class ParseResult<T> {
final T? value;
final String? errorMessage;
const ParseResult.success(this.value) : errorMessage = null;
const ParseResult.error(this.errorMessage) : value = null;
bool get isSuccess => value != null;
}
Usage Pattern:
// Pattern 1: Validate existing value
final validation = validator.validateLatitude(existingValue);
if (validation.isValid) {
// Use existingValue
}
// Pattern 2: Parse + validate text input
final parseResult = validator.parseLatitude(textController.text);
if (parseResult.isSuccess) {
final latitude = parseResult.value!; // Type-safe
// Use latitude
} else {
showError(parseResult.errorMessage!);
}
Service Integration Examples
Settings Screen (settings_screen.dart):
class _SettingsScreenState extends State<SettingsScreen> {
final LocationTrackingService _locationService = LocationTrackingService();
@override
void initState() {
super.initState();
_initLocationService();
}
Future<void> _initLocationService() async {
await _locationService.initialize(bleService);
_locationService.onError = (error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(error), backgroundColor: Colors.orange),
);
};
_locationService.onBroadcastSent = (position) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Location broadcast: ${position.latitude.toStringAsFixed(5)}, ${position.longitude.toStringAsFixed(5)}'),
backgroundColor: Colors.green,
),
);
};
}
@override
void dispose() {
_locationService.stopTracking();
super.dispose();
}
}
Map Tab (map_tab.dart):
class _MapTabState extends State<MapTab> {
final LocationTrackingService _locationService = LocationTrackingService();
final MapMarkerService _markerService = MapMarkerService();
@override
Widget build(BuildContext context) {
// Generate markers using service
final contactMarkers = _markerService.generateContactMarkers(
contacts: contactsProvider.contactsWithLocation,
onTap: (contact) => _showContactDetails(contact),
userLat: _locationService.currentPosition?.latitude,
userLon: _locationService.currentPosition?.longitude,
);
final sarMarkers = _markerService.generateSarMarkers(
sarMarkers: messagesProvider.sarMarkers,
onTap: (marker) => _showSarMarkerDetails(marker),
);
return FlutterMap(
children: [
TileLayer(...),
MarkerLayer(markers: [...contactMarkers, ...sarMarkers]),
],
);
}
}
Device Config Screen (device_config_screen.dart):
Future<void> _saveRadioParams() async {
final validator = ValidationService();
// Parse and validate all inputs
final freqResult = validator.parseFrequency(_freqController.text);
if (!freqResult.isSuccess) {
_showError(freqResult.errorMessage!);
return;
}
final bwResult = validator.parseBandwidth(_bwController.text);
if (!bwResult.isSuccess) {
_showError(bwResult.errorMessage!);
return;
}
final sfResult = validator.parseSpreadingFactor(_sfController.text);
if (!sfResult.isSuccess) {
_showError(sfResult.errorMessage!);
return;
}
// All validation passed, save to device
await connectionProvider.setRadioParams(
frequency: freqResult.value!,
bandwidth: bwResult.value!,
spreadingFactor: sfResult.value!,
codingRate: crResult.value!,
);
}
Map Implementation
Tile Layers
Three tile sources are supported via MapLayer enum:
-
OpenStreetMap (default)
- URL:
https://tile.openstreetmap.org/{z}/{x}/{y}.png - Max zoom: 19
- Best for street-level navigation
- URL:
-
OpenTopoMap
- URL:
https://a.tile.opentopomap.org/{z}/{x}/{y}.png - Max zoom: 17
- Shows topographic features, elevation contours
- URL:
-
ESRI World Imagery
- URL:
https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x} - Max zoom: 19
- Satellite imagery
- URL:
Offline Tile Caching
Uses flutter_map_tile_caching with ObjectBox backend:
// Initialize cache
await FMTCObjectBoxBackend().initialise();
final store = FMTCStore('meshcore_sar_tiles');
await store.manage.create();
// Download region
final region = RectangleRegion(bounds);
await store.download.startForeground(region: region);
Cache Behavior:
CacheBehavior.cacheFirst: Use cached tiles if available- 30-day validity period
- Automatic background updates when online
Map Markers
Team Member Markers (Blue):
- CircleAvatar with person icon
- Battery percentage badge at top
- Name label at bottom
- Tap to show details dialog
SAR Event Markers (Color-coded):
- Found Person: Green with 🧑
- Fire: Red with 🔥
- Staging Area: Orange with 🏕️
- Time ago label at top
- Type label at bottom
- Tap to show details dialog
Map Navigation
Navigation from Messages Tab:
- User taps SAR marker message
MapProvider.navigateToLocation()called- Target location and zoom stored in provider
- Tab switches to Map
MapTab._handleMapNavigation()moves mapMapProvider.clearNavigation()resets state
Zoom State Preservation:
- Current zoom stored in
MapProvider - Maintained across tab switches
- Updated on user zoom gestures
User Location Tracking
The app tracks the user's precise GPS location in real-time:
Permission Setup (iOS Info.plist):
<key>NSLocationWhenInUseUsageDescription</key>
<string>MeshCore SAR needs location access to display team members and SAR markers on the map</string>
<key>NSLocationTemporaryPreciseUsageDescription</key>
<string>MeshCore SAR needs precise location for accurate positioning in SAR operations</string>
<key>NSLocationDefaultAccuracyReduced</key>
<false/>
Implementation (lib/screens/map_tab.dart):
Position? _currentPosition;
bool _trackingLocation = false;
// Request permission and start tracking
final position = await Geolocator.getCurrentPosition(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.best,
distanceFilter: 0,
),
);
// Listen to continuous position updates
Geolocator.getPositionStream(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.best,
distanceFilter: 10, // Update every 10 meters
),
).listen((Position position) {
setState(() => _currentPosition = position);
if (_trackingLocation) {
// Auto-center map on user location
_mapController.move(
LatLng(position.latitude, position.longitude),
_mapController.camera.zoom
);
}
});
User Location Marker:
- Blue pulsing circle showing current position
- Navigation icon indicating heading
- Tap location button to center map on user
- Tap again to enable tracking mode (map follows user movement)
Map Legend
Collapsible Legend (lib/screens/map_tab.dart):
- Shows counts of team members and SAR markers
- Click to collapse to compact view
- Click again to expand
- Positioned in top-right corner
bool _showLegend = true;
GestureDetector(
onTap: () => setState(() => _showLegend = !_showLegend),
child: _showLegend
? _MapLegend(/* full legend with all counts */)
: Card(
child: Column([
Text('Legend'),
Icon(Icons.expand_more),
]),
),
)
Detailed Compass Dialog
Location Display (lib/screens/map_tab.dart):
- Ultra-compact location format display with tap-to-toggle formats
- Tap location text to switch between DD and DMS formats
- No labels - just the coordinates for maximum space efficiency
- Two formats available:
- DD (Decimal Degrees): 5 decimal places (e.g., "46.05690, 14.50580")
- DMS (Degrees, Minutes, Seconds): Traditional format (e.g., "46°03'24.84"N, 14°30'20.88"E")
- Monospace font for coordinate values
Dialog Controls:
- Tap anywhere outside interactive elements to close dialog
- Tap location coordinates to toggle format
- No close button - cleaner, more compact interface
class _LocationFormatToggle extends StatefulWidget {
bool _showDMS = false;
@override
Widget build(BuildContext context) {
final displayText = _showDMS
? 'DMS format with newline'
: 'DD format single line';
return GestureDetector(
onTap: () => setState(() => _showDMS = !_showDMS),
behavior: HitTestBehavior.opaque,
child: Container(/* compact display */),
);
}
}
Building and Development
Development Commands
# Install dependencies
flutter pub get
# Run in debug mode
flutter run
# Run with specific device
flutter run -d <device-id>
# Hot reload (during debug)
# Press 'r' in terminal
# Hot restart (during debug)
# Press 'R' in terminal
# Analyze code
flutter analyze
# Run tests
flutter test
# Format code
dart format lib/
# Clean build
flutter clean
iOS Build
# Open Xcode workspace
open ios/Runner.xcworkspace
# Build from command line
flutter build ios --release
# Create IPA (requires signing)
flutter build ipa
Key iOS Files:
ios/Runner/Info.plist: Permissions and app configurationios/Podfile: CocoaPods dependenciesios/Runner.xcodeproj: Xcode project
Android Build
# Debug APK
flutter build apk --debug
# Release APK
flutter build apk --release
# App Bundle (for Play Store)
flutter build appbundle --release
# Split APKs by ABI
flutter build apk --split-per-abi
Key Android Files:
android/app/src/main/AndroidManifest.xml: Permissions and app configurationandroid/app/build.gradle: App-level build configurationandroid/build.gradle: Project-level build configuration
Common Development Tasks
Adding a New BLE Command
The BLE service is now split into modular components. Follow these steps:
-
Add command code to
lib/services/meshcore_constants.dart:static const int cmdYourCommand = 42; -
Add frame builder in
lib/services/protocol/frame_builder.dart:/// Build YOUR_COMMAND frame static Uint8List buildYourCommand({required String param}) { final writer = BufferWriter(); writer.writeByte(MeshCoreConstants.cmdYourCommand); writer.writeString(param); return writer.toBytes(); } -
Add public API method in
lib/services/meshcore_ble_service.dart:Future<void> yourCommand({required String param}) async { final frame = FrameBuilder.buildYourCommand(param: param); await _commandSender.sendCommand(frame); } -
Add response parser in
lib/services/protocol/frame_parser.dart:static Map<String, dynamic> parseYourResponse(Uint8List data) { final reader = BufferReader(data); reader.readByte(); // Skip response code return { 'yourField': reader.readString(), // ... parse other fields }; } -
Handle response in
lib/services/ble/ble_response_handler.dart:case MeshCoreConstants.respYourResponse: final parsed = FrameParser.parseYourResponse(data); _bleService.onYourCallback?.call(parsed); break; -
Add callback in
lib/services/meshcore_ble_service.dart:Function(Map<String, dynamic>)? onYourCallback;
Adding a New SAR Marker Type
-
Update enum in
lib/models/sar_marker.dart:enum SarMarkerType { // existing types... yourType('🆕', 'Your Type'); } -
Add to parser in
lib/utils/sar_message_parser.dart:case '🆕': return SarMarkerType.yourType; -
Add color in
lib/widgets/map_markers.dart:case SarMarkerType.yourType: return Colors.purple; -
Update providers in
lib/providers/messages_provider.dart:List<SarMarker> get yourTypeMarkers => sarMarkers.where((m) => m.type == SarMarkerType.yourType).toList();
Adding a New Map Layer
-
Add to model in
lib/models/map_layer.dart:static const yourLayer = MapLayer( type: MapLayerType.yourLayer, name: 'Your Layer', urlTemplate: 'https://your-tile-server/{z}/{x}/{y}.png', attribution: '© Your Attribution', maxZoom: 19, ); -
Add to list:
static const List<MapLayer> allLayers = [ openStreetMap, openTopoMap, esriWorldImagery, yourLayer, // Add here ]; -
Layer automatically appears in layer selector UI
Testing
Unit Tests
# Run all tests
flutter test
# Run specific test file
flutter test test/widget_test.dart
# Run with coverage
flutter test --coverage
Integration Tests
# Run integration tests
flutter drive --target=test_driver/app.dart
Manual Testing Checklist
BLE Connection:
- Scan discovers MeshCore devices
- Connection successful
- Device info displayed in status bar
- Disconnect works properly
Contacts:
- Contacts load after connection
- Contacts grouped by type
- Telemetry request works
- Battery/GPS displayed correctly
Messages:
- Messages received and displayed
- SAR markers highlighted
- Tap SAR marker navigates to map
- Message timestamps correct
Map:
- Map loads and displays tiles
- Team member markers appear
- SAR markers appear with correct colors
- Layer switching works
- Zoom/pan gestures work
- Marker tap shows details
- Offline tiles load
Troubleshooting
BLE Issues
"Bluetooth adapter not available":
- Check device Bluetooth is on
- Verify permissions granted
- iOS: Check Info.plist has usage descriptions
- Android: Check AndroidManifest.xml has permissions
"Connection failed":
- Device must support BLE
- Check service UUID matches
- Verify device is in range (<10m typically)
- Try scanning again
Runtime Issues
MissingPluginException for geolocator or other plugins:
Example error:
MissingPluginException(No implementation found for method isLocationServiceEnabled
on channel flutter.baseflow.com/geolocator_apple)
This occurs when native plugin implementations aren't properly installed. Common after adding new dependencies.
Solution:
# For iOS
cd ios
pod install
cd ..
# Clean and rebuild
flutter clean
flutter pub get
flutter run
If still failing on iOS:
cd ios
rm Podfile.lock
rm -rf Pods/
pod install
cd ..
flutter clean
flutter pub get
Build Issues
iOS Pod Install Fails:
cd ios
rm Podfile.lock
rm -rf Pods/
pod install --repo-update
cd ..
CocoaPods ObjectBox Version Conflict:
This error occurs when flutter_map_tile_caching updates its ObjectBox dependency but the cached Podfile.lock has an older version:
[!] CocoaPods could not find compatible versions for pod "ObjectBox":
In snapshot (Podfile.lock): ObjectBox (= 1.9.2)
In Podfile: objectbox_flutter_libs depends on ObjectBox (= 4.4.1)
Solution:
# Navigate to iOS directory
cd ios
# Remove cached dependency lock file
rm Podfile.lock
# Remove all installed pods
rm -rf Pods/
# Update CocoaPods repository (this may take a few minutes)
pod repo update
# Reinstall all pods with updated versions
pod install
# Return to project root
cd ..
# Clean Flutter build cache
flutter clean
# Reinstall Flutter dependencies
flutter pub get
# Run the app
flutter run
Alternative solution (if the above doesn't work):
cd ios
rm Podfile.lock
rm -rf Pods/
pod deintegrate
pod cache clean --all
pod setup
pod install
cd ..
flutter clean
flutter pub get
Note: The pod repo update command can take 5-10 minutes as it downloads the entire CocoaPods specifications repository. This is normal.
Android Gradle Timeout:
// 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 to handle partial packets
- Throttle telemetry requests (max 1 per second per contact)
- Use
notifyListeners()sparingly in providers
Map Performance
- Limit visible markers (cluster if >100 markers)
- 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 permissions
- Data: No sensitive data should be logged
- Network: Use HTTPS for all tile sources
Future Enhancements
Potential features to add:
- Message Sending: UI to compose and send messages
- Route Recording: Track team member paths over time
- Geofencing: Alerts when team members enter/exit areas
- Voice Notes: Attach audio to SAR markers
- Team Chat: Real-time chat between team members
- Mission Plans: Pre-loaded search patterns
- Statistics: Coverage analysis, search time tracking
References
- Flutter Documentation
- flutter_blue_plus API
- flutter_map Documentation
- MeshCore Protocol
- Cayenne LPP Specification
- Provider Package
Contact
For questions or contributions, please refer to the project repository or contact the development team.