diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 5393a8b..e97d383 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -23,7 +23,8 @@ "Read(//Users/dz0ny/.pub-cache/hosted/pub.dev/**)", "Bash(find:*)", "Read(//Users/dz0ny/meshcore-sar/**)", - "Bash(git grep:*)" + "Bash(git grep:*)", + "Bash(dart analyze:*)" ], "deny": [], "ask": [] diff --git a/CLAUDE.md b/CLAUDE.md index 0445f11..ecce248 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,748 +1,212 @@ # CLAUDE.md - MeshCore SAR Technical Reference -This document provides technical details for AI assistants (like Claude) working with this codebase. +AI assistant guide for the MeshCore SAR Flutter application. -## ⚠️ IMPORTANT: Flutter Development Rules +## ⚠️ CRITICAL: Flutter Development Rules **NEVER run or kill Flutter processes:** -- **DO NOT** execute `flutter run` command -- **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 +- DO NOT execute `flutter run` command +- 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 -**Hot reload happens automatically when you save files** - the user has their own Flutter process running and will see changes instantly. +## Quick Reference -## Project Overview +**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 -**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+) +**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) ## 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::lat,lon format -└── main.dart # App entry point +├── models/ # Data models (contact, message, sar_marker, device_info, room_login_state, map_layer) +├── 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 (ConnectionProvider, ContactsProvider, MessagesProvider, MapProvider, AppProvider) +├── screens/ # UI screens (home, messages, contacts, map, settings, device_config, map_management, packet_log) +├── widgets/ # Reusable components (map_markers, messages/, contacts/, map/) +└── utils/ # Utilities (sar_message_parser) ``` -## Key Technologies +## MeshCore Protocol -### 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 +### 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** -### 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 - -**NOTE**: All uint32 values use Little Endian byte order! - -#### Command Codes (App → Radio) +### Command Codes (App → Radio) | Code | Name | Description | |------|------|-------------| -| 1 | CMD_APP_START | First command after connection, returns RESP_CODE_SELF_INFO(5) | +| 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 message to channel | -| 4 | CMD_GET_CONTACTS | Sync contacts list (optional 'since' param) | +| 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 (optional flood-mode) | -| 8 | CMD_SET_ADVERT_NAME | Update node name in advertisements | -| 9 | CMD_ADD_UPDATE_CONTACT | Add or modify a contact | +| 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 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 | +| 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' | +| 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 | +| 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 other parameters | -| 39 | CMD_SEND_TELEMETRY_REQ | Send telemetry request to node | -| 40 | CMD_GET_CUSTOM_VARS | Retrieve all custom variables | +| 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 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) | +| 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) +### 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 | +| 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 expected ACK/TAG | +| 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 and hardware 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 for contact | +| 22 | RESP_CODE_ADVERT_PATH | Last advert path | -#### Push Notifications (Radio → App, Async) +### Push Notifications (Radio → App, Async) | Code | Name | Description | |------|------|-------------| -| 0x80 | PUSH_CODE_ADVERT | New advertisement packet received | +| 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 response successful | -| 0x86 | PUSH_CODE_LOGIN_FAIL | Login response failed | +| 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 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_MSG` for 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_MSG` with the room's public key - - Optional: Login to rooms using `CMD_SEND_LOGIN` with password to read stored messages - -**Room Login Protocol Flow (CRITICAL - Follow Exactly)**: - -1. **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) - ``` - -2. **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_RESPONSE` containing login result - - Sets `next_push = futureMillis(PUSH_NOTIFY_DELAY_MILLIS)` to delay first push by 2000ms (line 346) - -3. **Client receives login response**: - - Success: `PUSH_CODE_LOGIN_SUCCESS` (0x85) with permissions, admin flag, tag - - Failure: `PUSH_CODE_LOGIN_FAIL` (0x86) if password incorrect - -4. **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 sends `PAYLOAD_TYPE_TXT_MSG` directly to client - - Waits for ACK, then advances `client->extra.room.sync_since` to next post - - Continues until all messages where `post_timestamp > sync_since` are pushed - -5. **Client receives pushed messages as they arrive**: - - Each push triggers `PUSH_CODE_MSG_WAITING` (0x83) - - App's `onMessageWaiting` callback 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 - -**CRITICAL IMPLEMENTATION RULES**: -- ❌ **DO NOT** call `syncAllMessages()` immediately after `PUSH_CODE_LOGIN_SUCCESS` -- ✅ **DO** wait for `PUSH_CODE_MSG_WAITING` push notifications -- ✅ **DO** call `syncNextMessage()` when `onMessageWaiting` callback 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_MSG` with 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_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 +| 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 | + +### 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` + +**Rooms** (ADV_TYPE_ROOM contacts): +- Named contacts with public keys +- **Persistent and immutable storage** +- Use `CMD_SEND_TXT_MSG` with room's public key (direct message) +- Optional: Login with `CMD_SEND_LOGIN` to 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 + +### Room Login Protocol Flow (CRITICAL) + +1. **Client sends `CMD_SEND_LOGIN` (26)**: Radio internally generates sender_timestamp and sync_since +2. **Room server processes login**: Validates password, stores sync_since, delays first push 2000ms +3. **Client receives response**: `PUSH_CODE_LOGIN_SUCCESS` (0x85) or `PUSH_CODE_LOGIN_FAIL` (0x86) +4. **Room server auto-pushes messages**: Round-robin every 1200ms, sends messages where post_timestamp > sync_since +5. **Client receives pushed messages**: `PUSH_CODE_MSG_WAITING` (0x83) → call `CMD_SYNC_NEXT_MESSAGE` (10) + +**Implementation Rules:** +- ❌ DO NOT call `syncAllMessages()` after `PUSH_CODE_LOGIN_SUCCESS` +- ✅ DO wait for `PUSH_CODE_MSG_WAITING` push notifications +- ✅ DO call `syncNextMessage()` when `onMessageWaiting` fires ### Cayenne LPP Format -Telemetry data uses Cayenne Low Power Payload format: +Format: `[Channel] [Type] [Data...]` -``` -[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) +**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 -Special tactical markers embedded in messages: +Format: `S::,` -``` -S::, -``` +**Recognized Emojis:** +- 🧑 or 👤: Found Person +- 🔥: Fire Location +- 🏕️ or ⛺: Staging Area -**Recognized Emojis**: -- `🧑` or `👤`: Found Person -- `🔥`: Fire Location -- `🏕️` or `⛺`: Staging Area - -**Examples**: -- `S:🧑:46.0569,14.5058` - Person found at coordinates -- `S:🔥:46.0570,14.5060` - Fire detected -- `S:🏕️: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 +**Rules:** Must start with `S:`, single emoji after first colon, comma-separated lat/lon, no spaces ## State Management Architecture ### Provider Hierarchy - ``` MultiProvider ├── ConnectionProvider # BLE connection state @@ -753,1004 +217,163 @@ MultiProvider ``` ### Event Flow - ``` BLE Device → MeshCoreBleService → ConnectionProvider → AppProvider ↓ - ContactsProvider - MessagesProvider + ContactsProvider + MessagesProvider ↓ UI ``` -**Example: Receiving a Message** +**Contact Types:** +- none(0): Unknown/invalid +- chat(1): Team member (shown on map) +- repeater(2): Network repeater node +- room(3): Communication channel/room -1. BLE device sends message via TX characteristic -2. `MeshCoreBleService._onDataReceived()` parses binary data -3. Calls `onMessageReceived` callback -4. `ConnectionProvider` receives message -5. `AppProvider` enhances message (check for SAR format) -6. `MessagesProvider.addMessage()` stores message -7. UI rebuilds via `Consumer` +**Map Display:** Only `ContactType.chat` with valid GPS shown on map -### Contact Types +## Service Layer -```dart -enum ContactType { - none(0), // Unknown/invalid - chat(1), // Team member (shown on map) - repeater(2), // Network repeater node - room(3), // Communication channel/room -} -``` +### LocationTrackingService (Singleton) +**Purpose:** GPS tracking + intelligent mesh network location broadcasting -**Map Display Rules**: -- Only `ContactType.chat` contacts with valid GPS are shown on map -- Repeaters and rooms are listed in Contacts tab but not mapped +**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 -## Service Layer Architecture +**Files:** lib/services/location_tracking_service.dart (501 lines) -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. +### MapMarkerService (Singleton) +**Purpose:** Map marker generation + geodesic calculations -### LocationTrackingService +**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 -**Purpose**: Singleton service for GPS tracking and intelligent mesh network location broadcasting. +**Files:** lib/services/map_marker_service.dart (518 lines) -**Pattern**: Callback-based architecture with configurable thresholds. +### ValidationService (Singleton) +**Purpose:** Form validation + input parsing with structured error handling -**Initialization**: -```dart -final locationService = LocationTrackingService(); -await locationService.initialize(bleService); +**Key Features:** +- Structured result types (`ValidationResult`, `ParseResult`) +- 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) -// 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**: -```dart -// 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: - -1. **First broadcast**: Always sends immediately (no previous position to compare) -2. **Maximum distance trigger**: If user moves ≥100m (configurable), broadcasts immediately regardless of time -3. **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**: -```dart -// 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: -```dart -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**: -```dart -final markerService = MapMarkerService(); - -final contactMarkers = markerService.generateContactMarkers( - contacts: contactsWithLocation, - onTap: (contact) => showContactDetails(contact), - userLat: currentUserLatitude, // Optional: for distance calculations - userLon: currentUserLongitude, -); -``` - -**Generate SAR Markers**: -```dart -final sarMarkers = markerService.generateSarMarkers( - sarMarkers: allSarMarkers, - onTap: (marker) => showSarMarkerDetails(marker), -); -``` - -**Calculate Distance Between Points**: -```dart -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**: -```dart -final bearing = markerService.calculateBearing( - lat1: userLat, lon1: userLon, - lat2: targetLat, lon2: targetLon, -); -print('Bearing: ${bearing.toStringAsFixed(1)}°'); -``` - -**Format Distance for Display**: -```dart -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`) for type-safe error handling. - -**Coordinate Validation**: -```dart -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**: -```dart -// 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**: -```dart -// 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**: -```dart -// 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**: -```dart -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**: -```dart -// 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 - for parsing + validation -class ParseResult { - 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**: -```dart -// 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): -```dart -class _SettingsScreenState extends State { - final LocationTrackingService _locationService = LocationTrackingService(); - - @override - void initState() { - super.initState(); - _initLocationService(); - } - - Future _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): -```dart -class _MapTabState extends State { - 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): -```dart -Future _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!, - ); -} -``` +**Files:** lib/services/validation_service.dart (511 lines) ## Map Implementation ### Tile Layers - -Three tile sources are supported via `MapLayer` enum: - -1. **OpenStreetMap** (default) - - URL: `https://tile.openstreetmap.org/{z}/{x}/{y}.png` - - Max zoom: 19 - - Best for street-level navigation - -2. **OpenTopoMap** - - URL: `https://a.tile.opentopomap.org/{z}/{x}/{y}.png` - - Max zoom: 17 - - Shows topographic features, elevation contours - -3. **ESRI World Imagery** - - URL: `https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}` - - Max zoom: 19 - - Satellite imagery +1. **OpenStreetMap** (default): Max zoom 19, street-level navigation +2. **OpenTopoMap**: Max zoom 17, topographic features +3. **ESRI World Imagery**: Max zoom 19, satellite imagery ### Offline Tile Caching - -Uses `flutter_map_tile_caching` with ObjectBox backend: - -```dart -// 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 +- Backend: `flutter_map_tile_caching` with ObjectBox +- Behavior: `CacheBehavior.cacheFirst`, 30-day validity +- Region downloads: `RectangleRegion(bounds)` → `store.download.startForeground()` ### 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 +**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 - -**Navigation from Messages Tab**: -1. User taps SAR marker message -2. `MapProvider.navigateToLocation()` called -3. Target location and zoom stored in provider -4. Tab switches to Map -5. `MapTab._handleMapNavigation()` moves map -6. `MapProvider.clearNavigation()` resets state - -**Zoom State Preservation**: -- Current zoom stored in `MapProvider` -- Maintained across tab switches -- Updated on user zoom gestures +Message tab → tap SAR marker → `MapProvider.navigateToLocation()` → switch to Map tab → `MapTab._handleMapNavigation()` → `MapProvider.clearNavigation()` ### User Location Tracking - -The app tracks the user's precise GPS location in real-time: - -**Permission Setup** (iOS Info.plist): -```xml -NSLocationWhenInUseUsageDescription -MeshCore SAR needs location access to display team members and SAR markers on the map -NSLocationTemporaryPreciseUsageDescription -MeshCore SAR needs precise location for accurate positioning in SAR operations -NSLocationDefaultAccuracyReduced - -``` - -**Implementation** (lib/screens/map_tab.dart): -```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) +- Permission: `NSLocationWhenInUseUsageDescription`, `NSLocationTemporaryPreciseUsageDescription` +- Accuracy: `LocationAccuracy.best`, distance filter: 10m +- Marker: Blue pulsing circle, navigation icon, tap to center/track ### 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 - -```dart -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), - ]), - ), -) -``` +Collapsible legend (top-right), shows counts of team members and SAR markers ### 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 - -```dart -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 - -```bash -# Install dependencies -flutter pub get - -# Run in debug mode -flutter run - -# Run with specific device -flutter run -d - -# 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 - -```bash -# 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 configuration -- `ios/Podfile`: CocoaPods dependencies -- `ios/Runner.xcodeproj`: Xcode project - -### Android Build - -```bash -# 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 configuration -- `android/app/build.gradle`: App-level build configuration -- `android/build.gradle`: Project-level build configuration +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 - -The BLE service is now split into modular components. Follow these steps: - -1. **Add command code** to `lib/services/meshcore_constants.dart`: - ```dart - static const int cmdYourCommand = 42; - ``` - -2. **Add frame builder** in `lib/services/protocol/frame_builder.dart`: - ```dart - /// Build YOUR_COMMAND frame - static Uint8List buildYourCommand({required String param}) { - final writer = BufferWriter(); - writer.writeByte(MeshCoreConstants.cmdYourCommand); - writer.writeString(param); - return writer.toBytes(); - } - ``` - -3. **Add public API method** in `lib/services/meshcore_ble_service.dart`: - ```dart - Future yourCommand({required String param}) async { - final frame = FrameBuilder.buildYourCommand(param: param); - await _commandSender.sendCommand(frame); - } - ``` - -4. **Add response parser** in `lib/services/protocol/frame_parser.dart`: - ```dart - static Map parseYourResponse(Uint8List data) { - final reader = BufferReader(data); - reader.readByte(); // Skip response code - return { - 'yourField': reader.readString(), - // ... parse other fields - }; - } - ``` - -5. **Handle response** in `lib/services/ble/ble_response_handler.dart`: - ```dart - case MeshCoreConstants.respYourResponse: - final parsed = FrameParser.parseYourResponse(data); - _bleService.onYourCallback?.call(parsed); - break; - ``` - -6. **Add callback** in `lib/services/meshcore_ble_service.dart`: - ```dart - Function(Map)? onYourCallback; - ``` +1. Add command code to `lib/services/meshcore_constants.dart` +2. Add frame builder in `lib/services/protocol/frame_builder.dart` +3. Add public API method in `lib/services/meshcore_ble_service.dart` +4. Add response parser in `lib/services/protocol/frame_parser.dart` +5. Handle response in `lib/services/ble/ble_response_handler.dart` +6. Add callback in `lib/services/meshcore_ble_service.dart` ### Adding a New SAR Marker Type - -1. **Update enum** in `lib/models/sar_marker.dart`: - ```dart - enum SarMarkerType { - // existing types... - yourType('🆕', 'Your Type'); - } - ``` - -2. **Add to parser** in `lib/utils/sar_message_parser.dart`: - ```dart - case '🆕': - return SarMarkerType.yourType; - ``` - -3. **Add color** in `lib/widgets/map_markers.dart`: - ```dart - case SarMarkerType.yourType: - return Colors.purple; - ``` - -4. **Update providers** in `lib/providers/messages_provider.dart`: - ```dart - List get yourTypeMarkers => - sarMarkers.where((m) => m.type == SarMarkerType.yourType).toList(); - ``` +1. Update enum in `lib/models/sar_marker.dart` +2. Add to parser in `lib/utils/sar_message_parser.dart` +3. Add color in `lib/widgets/map_markers.dart` +4. Update providers in `lib/providers/messages_provider.dart` ### Adding a New Map Layer +1. Add to model in `lib/models/map_layer.dart` +2. Add to `allLayers` list +3. Layer appears automatically in layer selector UI -1. **Add to model** in `lib/models/map_layer.dart`: - ```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, - ); - ``` - -2. **Add to list**: - ```dart - static const List allLayers = [ - openStreetMap, - openTopoMap, - esriWorldImagery, - yourLayer, // Add here - ]; - ``` - -3. Layer automatically appears in layer selector UI - -## Testing - -### Unit Tests +## Build Commands ```bash -# Run all tests -flutter test +# Dependencies +flutter pub get -# Run specific test file -flutter test test/widget_test.dart +# Development +flutter run # Debug mode +flutter run -d # Specific device +# Hot reload: press 'r' | Hot restart: press 'R' -# Run with coverage -flutter test --coverage +# 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 ``` -### Integration Tests - -```bash -# 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 +- **"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 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**: +**MissingPluginException**: Native plugin not installed (common after adding dependencies) ```bash -# For iOS -cd ios -pod install -cd .. - -# Clean and rebuild -flutter clean -flutter pub get -flutter run -``` - -**If still failing on iOS**: -```bash -cd ios -rm Podfile.lock -rm -rf Pods/ -pod install -cd .. -flutter clean -flutter pub get +cd ios && pod install && cd .. && flutter clean && flutter pub get && flutter run ``` ### Build Issues - **iOS Pod Install Fails**: ```bash -cd ios -rm Podfile.lock -rm -rf Pods/ -pod install --repo-update -cd .. +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**: ```bash -# 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 +cd ios && rm Podfile.lock && rm -rf Pods/ && pod repo update && pod install && cd .. +flutter clean && flutter pub get && flutter run ``` -**Alternative solution** (if the above doesn't work): -```bash -cd ios -rm Podfile.lock -rm -rf Pods/ -pod deintegrate -pod cache clean --all -pod setup -pod install -cd .. -flutter clean -flutter pub get +**Android Gradle Timeout**: Add to `android/gradle.properties`: ``` - -**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**: -```gradle -// android/gradle.properties org.gradle.daemon=true org.gradle.parallel=true org.gradle.jvmargs=-Xmx4096m @@ -1758,20 +381,18 @@ org.gradle.jvmargs=-Xmx4096m **Flutter Version Conflicts**: ```bash -flutter channel stable -flutter upgrade -flutter pub upgrade +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 +- 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 markers) +- Limit visible markers (cluster if >100) - Use `repaint boundary` for marker widgets - Implement marker virtualization for large datasets @@ -1781,33 +402,15 @@ flutter pub upgrade - 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: - -1. **Message Sending**: UI to compose and send messages -2. **Route Recording**: Track team member paths over time -3. **Geofencing**: Alerts when team members enter/exit areas -4. **Voice Notes**: Attach audio to SAR markers -5. **Team Chat**: Real-time chat between team members -6. **Mission Plans**: Pre-loaded search patterns -7. **Statistics**: Coverage analysis, search time tracking +- **Permissions**: Request minimum required +- **Data**: No sensitive data logging +- **Network**: HTTPS for all tile sources ## References - - [Flutter Documentation](https://docs.flutter.dev/) - [flutter_blue_plus API](https://pub.dev/documentation/flutter_blue_plus/) - [flutter_map Documentation](https://docs.fleaflet.dev/) - [MeshCore Protocol](https://github.com/meshcore-dev/meshcore.js) - [Cayenne LPP Specification](https://developers.mydevices.com/cayenne/docs/lora/#lora-cayenne-low-power-payload) - [Provider Package](https://pub.dev/packages/provider) - -## Contact - -For questions or contributions, please refer to the project repository or contact the development team. diff --git a/ios/Gemfile b/ios/Gemfile new file mode 100644 index 0000000..7a118b4 --- /dev/null +++ b/ios/Gemfile @@ -0,0 +1,3 @@ +source "https://rubygems.org" + +gem "fastlane" diff --git a/ios/Gemfile.lock b/ios/Gemfile.lock new file mode 100644 index 0000000..86709de --- /dev/null +++ b/ios/Gemfile.lock @@ -0,0 +1,229 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.7) + base64 + nkf + rexml + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + artifactory (3.0.17) + atomos (0.1.3) + aws-eventstream (1.4.0) + aws-partitions (1.1172.0) + aws-sdk-core (3.233.0) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) + base64 + bigdecimal + jmespath (~> 1, >= 1.6.1) + logger + aws-sdk-kms (1.113.0) + aws-sdk-core (~> 3, >= 3.231.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.199.1) + aws-sdk-core (~> 3, >= 3.231.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.12.1) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.4) + base64 (0.3.0) + bigdecimal (3.3.1) + claide (1.1.0) + colored (1.2) + colored2 (3.1.2) + commander (4.6.0) + highline (~> 2.0.0) + declarative (0.0.20) + digest-crc (0.7.0) + rake (>= 12.0.0, < 14.0.0) + domain_name (0.6.20240107) + dotenv (2.8.1) + emoji_regex (3.2.3) + excon (0.112.0) + faraday (1.10.4) + faraday-em_http (~> 1.0) + faraday-em_synchrony (~> 1.0) + faraday-excon (~> 1.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.0) + faraday-patron (~> 1.0) + faraday-rack (~> 1.0) + faraday-retry (~> 1.0) + ruby2_keywords (>= 0.0.4) + faraday-cookie_jar (0.0.7) + faraday (>= 0.8.0) + http-cookie (~> 1.0.0) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.1) + faraday-excon (1.1.0) + faraday-httpclient (1.0.1) + faraday-multipart (1.1.1) + multipart-post (~> 2.0) + faraday-net_http (1.0.2) + faraday-net_http_persistent (1.2.0) + faraday-patron (1.0.0) + faraday-rack (1.0.0) + faraday-retry (1.0.3) + faraday_middleware (1.2.1) + faraday (~> 1.0) + fastimage (2.4.0) + fastlane (2.228.0) + CFPropertyList (>= 2.3, < 4.0.0) + addressable (>= 2.8, < 3.0.0) + artifactory (~> 3.0) + aws-sdk-s3 (~> 1.0) + babosa (>= 1.0.3, < 2.0.0) + bundler (>= 1.12.0, < 3.0.0) + colored (~> 1.2) + commander (~> 4.6) + dotenv (>= 2.1.1, < 3.0.0) + emoji_regex (>= 0.1, < 4.0) + excon (>= 0.71.0, < 1.0.0) + faraday (~> 1.0) + faraday-cookie_jar (~> 0.0.6) + faraday_middleware (~> 1.0) + fastimage (>= 2.1.0, < 3.0.0) + fastlane-sirp (>= 1.0.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-apis-androidpublisher_v3 (~> 0.3) + google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-env (>= 1.6.0, < 2.0.0) + google-cloud-storage (~> 1.31) + highline (~> 2.0) + http-cookie (~> 1.0.5) + json (< 3.0.0) + jwt (>= 2.1.0, < 3) + mini_magick (>= 4.9.4, < 5.0.0) + multipart-post (>= 2.0.0, < 3.0.0) + naturally (~> 2.2) + optparse (>= 0.1.1, < 1.0.0) + plist (>= 3.1.0, < 4.0.0) + rubyzip (>= 2.0.0, < 3.0.0) + security (= 0.1.5) + simctl (~> 1.6.3) + terminal-notifier (>= 2.0.0, < 3.0.0) + terminal-table (~> 3) + tty-screen (>= 0.6.3, < 1.0.0) + tty-spinner (>= 0.8.0, < 1.0.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) + xcpretty (~> 0.4.1) + xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) + fastlane-sirp (1.0.0) + sysrandom (~> 1.0) + gh_inspector (1.1.3) + google-apis-androidpublisher_v3 (0.54.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-core (0.11.3) + addressable (~> 2.5, >= 2.5.1) + googleauth (>= 0.16.2, < 2.a) + httpclient (>= 2.8.1, < 3.a) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.a) + rexml + google-apis-iamcredentials_v1 (0.17.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-playcustomapp_v1 (0.13.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-storage_v1 (0.31.0) + google-apis-core (>= 0.11.0, < 2.a) + google-cloud-core (1.8.0) + google-cloud-env (>= 1.0, < 3.a) + google-cloud-errors (~> 1.0) + google-cloud-env (1.6.0) + faraday (>= 0.17.3, < 3.0) + google-cloud-errors (1.5.0) + google-cloud-storage (1.47.0) + addressable (~> 2.8) + digest-crc (~> 0.4) + google-apis-iamcredentials_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.31.0) + google-cloud-core (~> 1.6) + googleauth (>= 0.16.2, < 2.a) + mini_mime (~> 1.0) + googleauth (1.8.1) + faraday (>= 0.17.3, < 3.a) + jwt (>= 1.4, < 3.0) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (>= 0.16, < 2.a) + highline (2.0.3) + http-cookie (1.0.8) + domain_name (~> 0.5) + httpclient (2.9.0) + mutex_m + jmespath (1.6.2) + json (2.15.1) + jwt (2.10.2) + base64 + logger (1.7.0) + mini_magick (4.13.2) + mini_mime (1.1.5) + multi_json (1.17.0) + multipart-post (2.4.1) + mutex_m (0.3.0) + nanaimo (0.4.0) + naturally (2.3.0) + nkf (0.2.0) + optparse (0.6.0) + os (1.1.4) + plist (3.7.2) + public_suffix (6.0.2) + rake (13.3.0) + representable (3.2.0) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + retriable (3.1.2) + rexml (3.4.4) + rouge (3.28.0) + ruby2_keywords (0.0.5) + rubyzip (2.4.1) + security (0.1.5) + signet (0.21.0) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) + jwt (>= 1.5, < 4.0) + multi_json (~> 1.10) + simctl (1.6.10) + CFPropertyList + naturally + sysrandom (1.0.5) + terminal-notifier (2.0.0) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + trailblazer-option (0.1.2) + tty-cursor (0.7.1) + tty-screen (0.8.2) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) + uber (0.1.0) + unicode-display_width (2.6.0) + word_wrap (1.0.0) + xcodeproj (1.27.0) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.4.0) + rexml (>= 3.3.6, < 4.0) + xcpretty (0.4.1) + rouge (~> 3.28.0) + xcpretty-travis-formatter (1.0.1) + xcpretty (~> 0.2, >= 0.0.7) + +PLATFORMS + arm64-darwin-25 + ruby + +DEPENDENCIES + fastlane + +BUNDLED WITH + 2.7.2 diff --git a/ios/Runner.ipa b/ios/Runner.ipa new file mode 100644 index 0000000..d39957c Binary files /dev/null and b/ios/Runner.ipa differ diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 9446723..7076992 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -489,7 +489,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = JND55328G8; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -676,7 +676,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = JND55328G8; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -699,7 +699,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = JND55328G8; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 28972d0..7024c61 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -21,7 +21,7 @@ CFBundleSignature ???? CFBundleVersion - $(FLUTTER_BUILD_NUMBER) + 1 LSRequiresIPhoneOS UILaunchStoryboardName diff --git a/ios/fastlane/Appfile b/ios/fastlane/Appfile new file mode 100644 index 0000000..0de215a --- /dev/null +++ b/ios/fastlane/Appfile @@ -0,0 +1,8 @@ +app_identifier("com.meshcore.sar.meshcoreSarApp") # The bundle identifier of your app +apple_id("hey@dz0ny.dev") # Your Apple Developer Portal username + +itc_team_id("123290368") # App Store Connect Team ID +team_id("JND55328G8") # Developer Portal Team ID + +# For more information about the Appfile, see: +# https://docs.fastlane.tools/advanced/#appfile diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile new file mode 100644 index 0000000..73e1657 --- /dev/null +++ b/ios/fastlane/Fastfile @@ -0,0 +1,25 @@ +# This file contains the fastlane.tools configuration +# You can find the documentation at https://docs.fastlane.tools +# +# For a list of all available actions, check out +# +# https://docs.fastlane.tools/actions +# +# For a list of all available plugins, check out +# +# https://docs.fastlane.tools/plugins/available-plugins +# + +# Uncomment the line if you want fastlane to automatically update itself +# update_fastlane + +default_platform(:ios) + +platform :ios do + desc "Push a new beta build to TestFlight" + lane :beta do + increment_build_number(xcodeproj: "Runner.xcodeproj") + build_app(workspace: "Runner.xcworkspace", scheme: "Runner") + upload_to_testflight + end +end diff --git a/ios/fastlane/README.md b/ios/fastlane/README.md new file mode 100644 index 0000000..891747d --- /dev/null +++ b/ios/fastlane/README.md @@ -0,0 +1,32 @@ +fastlane documentation +---- + +# Installation + +Make sure you have the latest version of the Xcode command line tools installed: + +```sh +xcode-select --install +``` + +For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) + +# Available Actions + +## iOS + +### ios beta + +```sh +[bundle exec] fastlane ios beta +``` + +Push a new beta build to TestFlight + +---- + +This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. + +More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). + +The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). diff --git a/ios/fastlane/report.xml b/ios/fastlane/report.xml new file mode 100644 index 0000000..f9dd048 --- /dev/null +++ b/ios/fastlane/report.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/screens/messages_tab.dart b/lib/screens/messages_tab.dart index 21e28dd..1cb01f4 100644 --- a/lib/screens/messages_tab.dart +++ b/lib/screens/messages_tab.dart @@ -50,6 +50,7 @@ class _MessagesTabState extends State { if (text.isEmpty) return; final connectionProvider = context.read(); + final messagesProvider = context.read(); if (!connectionProvider.deviceInfo.isConnected) { if (!mounted) return; @@ -63,10 +64,36 @@ class _MessagesTabState extends State { } try { - // Always send to public channel (channel 0) + // Create message ID + final messageId = '${DateTime.now().millisecondsSinceEpoch}_channel_sent'; + final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000; + + // Get current device's public key (first 6 bytes) + final devicePublicKey = connectionProvider.deviceInfo.publicKey; + final senderPublicKeyPrefix = devicePublicKey?.sublist(0, 6); + + // Create sent message object + final sentMessage = Message( + id: messageId, + messageType: MessageType.channel, + senderPublicKeyPrefix: senderPublicKeyPrefix, + pathLen: 0, + textType: MessageTextType.plain, + senderTimestamp: timestamp, + text: text, + receivedAt: DateTime.now(), + deliveryStatus: MessageDeliveryStatus.sending, + channelIdx: 0, + ); + + // Add to messages list with "sending" status + messagesProvider.addSentMessage(sentMessage); + + // Send to public channel (channel 0) await connectionProvider.sendChannelMessage( channelIdx: 0, text: text, + messageId: messageId, ); _textController.clear();