From 6bd7517180e2963bdbc6eabb3adc354441135845 Mon Sep 17 00:00:00 2001 From: Janez T Date: Sat, 28 Feb 2026 11:23:11 +0100 Subject: [PATCH] fix: init tabs advert quick add ref: --- .github/workflows/release-build.yml | 210 +++ AUTO_RECOVERY.md | 310 ---- BLOG_POST.md | 289 ---- CLAUDE.md | 897 ---------- IMPLEMENTATION_NOTES.md | 211 --- MESHCORE_BLE_PROTOCOL.md | 1224 -------------- MESHCORE_PACKET_RESEARCH.md | 578 ------- MESHCORE_PROTOCOL.md | 1163 ------------- MESHCORE_QUICK_REFERENCE.md | 216 --- MESSAGES.md | 1322 --------------- ROOM_LOGIN_FIX.md | 268 --- ROOM_LOGIN_REVIEW.md | 851 ---------- SCREENSHOTS.md | 367 ----- TEST_COVERAGE.md | 219 --- UNIMPLEMENTED_BLE_COMMANDS.md | 1725 -------------------- VECTOR_MAPS.md | 265 --- WMS_IMPLEMENTATION_PLAN.md | 463 ------ docs/ios_background_execution.md | 726 -------- docs/wms_layer_analysis.md | 304 ---- ios/Flutter/Debug.xcconfig | 1 + ios/Podfile.lock | 8 +- lib/providers/app_provider.dart | 370 +++-- lib/providers/connection_provider.dart | 12 + lib/providers/contacts_provider.dart | 79 +- lib/screens/contacts_tab.dart | 97 +- lib/screens/home_screen.dart | 31 +- lib/services/ble/ble_response_handler.dart | 61 + lib/services/meshcore_ble_service.dart | 8 + lib/services/meshcore_constants.dart | 40 +- lib/services/meshcore_opcode_names.dart | 56 + lib/services/protocol/frame_parser.dart | 17 + lib/widgets/messages/message_bubble.dart | 20 +- 32 files changed, 850 insertions(+), 11558 deletions(-) create mode 100644 .github/workflows/release-build.yml delete mode 100644 AUTO_RECOVERY.md delete mode 100644 BLOG_POST.md delete mode 100644 CLAUDE.md delete mode 100644 IMPLEMENTATION_NOTES.md delete mode 100644 MESHCORE_BLE_PROTOCOL.md delete mode 100644 MESHCORE_PACKET_RESEARCH.md delete mode 100644 MESHCORE_PROTOCOL.md delete mode 100644 MESHCORE_QUICK_REFERENCE.md delete mode 100644 MESSAGES.md delete mode 100644 ROOM_LOGIN_FIX.md delete mode 100644 ROOM_LOGIN_REVIEW.md delete mode 100644 SCREENSHOTS.md delete mode 100644 TEST_COVERAGE.md delete mode 100644 UNIMPLEMENTED_BLE_COMMANDS.md delete mode 100644 VECTOR_MAPS.md delete mode 100644 WMS_IMPLEMENTATION_PLAN.md delete mode 100644 docs/ios_background_execution.md delete mode 100644 docs/wms_layer_analysis.md diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml new file mode 100644 index 0000000..e671393 --- /dev/null +++ b/.github/workflows/release-build.yml @@ -0,0 +1,210 @@ +name: Build And Upload Release Assets + +on: + release: + types: [published] + workflow_dispatch: + +permissions: + contents: write + +env: + FLUTTER_VERSION: "3.35.6" + APP_NAME: meshcore-sar + +jobs: + build-android: + name: Build Android APK + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "17" + + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: ${{ env.FLUTTER_VERSION }} + channel: stable + cache: true + + - name: Install dependencies + run: flutter pub get + + - name: Build APK + run: flutter build apk --release + + - name: Prepare APK artifact + run: | + TAG="${{ github.event.release.tag_name || github.ref_name }}" + cp build/app/outputs/flutter-apk/app-release.apk "${APP_NAME}-${TAG}-android.apk" + + - name: Upload APK artifact + uses: actions/upload-artifact@v4 + with: + name: release-android + path: "${{ env.APP_NAME }}-*-android.apk" + if-no-files-found: error + + build-linux: + name: Build Linux App + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Linux build dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + clang \ + cmake \ + ninja-build \ + pkg-config \ + libgtk-3-dev \ + liblzma-dev + + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: ${{ env.FLUTTER_VERSION }} + channel: stable + cache: true + + - name: Enable Linux desktop + run: flutter config --enable-linux-desktop + + - name: Install dependencies + run: flutter pub get + + - name: Build Linux release + run: flutter build linux --release + + - name: Package Linux artifact + run: | + TAG="${{ github.event.release.tag_name || github.ref_name }}" + tar -C build/linux/x64/release -czf "${APP_NAME}-${TAG}-linux.tar.gz" bundle + + - name: Upload Linux artifact + uses: actions/upload-artifact@v4 + with: + name: release-linux + path: "${{ env.APP_NAME }}-*-linux.tar.gz" + if-no-files-found: error + + build-windows: + name: Build Windows App + runs-on: windows-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: ${{ env.FLUTTER_VERSION }} + channel: stable + cache: true + + - name: Enable Windows desktop + shell: pwsh + run: flutter config --enable-windows-desktop + + - name: Install dependencies + shell: pwsh + run: flutter pub get + + - name: Build Windows release + shell: pwsh + run: flutter build windows --release + + - name: Package Windows artifact + shell: pwsh + run: | + $Tag = "${{ github.event.release.tag_name || github.ref_name }}" + $Output = "${{ env.APP_NAME }}-$Tag-windows.zip" + Compress-Archive -Path "build/windows/x64/runner/Release/*" -DestinationPath $Output + + - name: Upload Windows artifact + uses: actions/upload-artifact@v4 + with: + name: release-windows + path: "${{ env.APP_NAME }}-*-windows.zip" + if-no-files-found: error + + build-macos: + name: Build macOS DMG + runs-on: macos-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: ${{ env.FLUTTER_VERSION }} + channel: stable + cache: true + + - name: Enable macOS desktop + run: flutter config --enable-macos-desktop + + - name: Install dependencies + run: flutter pub get + + - name: Build macOS release + run: flutter build macos --release + + - name: Create DMG + run: | + TAG="${{ github.event.release.tag_name || github.ref_name }}" + APP_PATH=$(find build/macos/Build/Products/Release -maxdepth 1 -name "*.app" -print -quit) + if [ -z "$APP_PATH" ]; then + echo "No .app bundle found in build output" + exit 1 + fi + hdiutil create -volname "MeshCore SAR" -srcfolder "$APP_PATH" -ov -format UDZO "${APP_NAME}-${TAG}-macos.dmg" + + - name: Upload macOS artifact + uses: actions/upload-artifact@v4 + with: + name: release-macos + path: "${{ env.APP_NAME }}-*-macos.dmg" + if-no-files-found: error + + upload-release-assets: + name: Upload Assets To Release + runs-on: ubuntu-latest + needs: + - build-android + - build-linux + - build-windows + - build-macos + + steps: + - name: Download all build artifacts + uses: actions/download-artifact@v4 + with: + pattern: release-* + path: dist + merge-multiple: true + + - name: Upload assets to published release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.event.release.tag_name || github.ref_name }} + files: | + dist/*.apk + dist/*.tar.gz + dist/*.zip + dist/*.dmg + fail_on_unmatched_files: true diff --git a/AUTO_RECOVERY.md b/AUTO_RECOVERY.md deleted file mode 100644 index 2716d99..0000000 --- a/AUTO_RECOVERY.md +++ /dev/null @@ -1,310 +0,0 @@ -# Auto-Recovery System for ERR_CODE_NOT_FOUND - -## Problem Statement - -When sending a message to a room contact that exists in the app's contact list but not in the companion radio's contact table, the radio responds with: -``` -ERROR (0x01) with error code 2 (ERR_CODE_NOT_FOUND) -``` - -This happens because: -1. Room contacts may have been deleted from the radio -2. New room contacts added to the app haven't been synced to the radio yet -3. The radio was factory reset but the app still has cached contacts - -## Solution: Automatic Contact Recovery - -The system now automatically detects and recovers from `ERR_CODE_NOT_FOUND` errors by: - -1. **Detecting the error** in `BleResponseHandler` (ble_response_handler.dart:584-587) -2. **Tracking the failing contact** via `setLastContactPublicKey()` -3. **Triggering auto-recovery** via `onContactNotFound` callback -4. **Adding the missing contact** to the radio using `CMD_ADD_UPDATE_CONTACT` -5. **Retrying the send operation** automatically after 300ms delay - -## Implementation Flow - -``` -┌─────────────────────────────────────────────────────────────┐ -│ User sends message to room contact │ -└─────────────────┬───────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ ConnectionProvider.sendTextMessage() │ -│ - Tracks pending operation: _PendingSendOperation │ -│ - Contains: contactPublicKey, text, messageId, contact │ -└─────────────────┬───────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ MeshCoreBleService.sendTextMessage() │ -│ - Calls: responseHandler.setLastContactPublicKey() │ -│ - Sends: CMD_SEND_TXT_MSG (0x02) │ -└─────────────────┬───────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ Radio processes command │ -│ ❌ Contact not found in radio's contact table │ -│ → Responds: RESP_CODE_ERR (0x01) with errCode=2 │ -└─────────────────┬───────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ BleResponseHandler._handleError() │ -│ - Detects: errorCode == 2 (ERR_CODE_NOT_FOUND) │ -│ - Triggers: onContactNotFound(lastContactPublicKey) │ -└─────────────────┬───────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ ConnectionProvider.onContactNotFound callback │ -│ 1. Looks up pending operation by public key │ -│ 2. Calls: bleService.addOrUpdateContact(contact) │ -│ → Sends: CMD_ADD_UPDATE_CONTACT (0x09) │ -│ 3. Waits 300ms for contact to be added │ -│ 4. Retries: bleService.sendTextMessage() │ -│ → Sends: CMD_SEND_TXT_MSG (0x02) again │ -│ 5. Clears pending operation on success │ -└─────────────────┬───────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ Radio processes retry │ -│ ✅ Contact now exists in radio's contact table │ -│ → Responds: RESP_CODE_SENT (0x06) with ACK tag │ -└─────────────────┬───────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ Message sent successfully │ -│ → UI shows "sent" status │ -│ → Normal delivery confirmation flow continues │ -└─────────────────────────────────────────────────────────────┘ -``` - -## Key Components - -### 1. BleResponseHandler (lib/services/ble/ble_response_handler.dart) - -**New Callbacks:** -```dart -typedef OnContactNotFoundCallback = void Function(Uint8List? contactPublicKey); -``` - -**Error Detection:** -```dart -void _handleError(BufferReader reader) { - final errorCode = FrameParser.parseError(reader); - if (errorCode == 2) { // ERR_CODE_NOT_FOUND - onContactNotFound?.call(_lastContactPublicKey); - } - onError?.call(errorMsg, errorCode: errorCode); -} -``` - -**Tracking:** -```dart -void setLastContactPublicKey(Uint8List? publicKey) { - _lastContactPublicKey = publicKey; -} -``` - -### 2. MeshCoreBleService (lib/services/meshcore_ble_service.dart) - -**Updated Typedef:** -```dart -typedef OnErrorCallback = void Function(String error, {int? errorCode}); -typedef OnContactNotFoundCallback = void Function(Uint8List? contactPublicKey); -``` - -**Callback Forwarding:** -```dart -_responseHandler.onError = (error, {int? errorCode}) { - onError?.call(error, errorCode: errorCode); -}; -_responseHandler.onContactNotFound = (contactPublicKey) { - onContactNotFound?.call(contactPublicKey); -}; -``` - -**Contact Tracking:** -```dart -Future sendTextMessage({...}) async { - // Track the last contact for auto-recovery if contact not found - _responseHandler.setLastContactPublicKey(contactPublicKey); - await _commandSender.writeData(...); -} -``` - -### 3. ConnectionProvider (lib/providers/connection_provider.dart) - -**Pending Operation Tracking:** -```dart -class _PendingSendOperation { - final Uint8List contactPublicKey; - final String text; - final String? messageId; - final Contact? contact; - final int retryAttempt; -} - -final Map _pendingSendOperations = {}; -``` - -**Auto-Recovery Logic:** -```dart -_bleService.onContactNotFound = (contactPublicKey) async { - // 1. Look up pending operation - final pendingOp = _pendingSendOperations[operationId]; - - // 2. Add contact to radio - await _bleService.addOrUpdateContact(pendingOp.contact!); - await Future.delayed(const Duration(milliseconds: 300)); - - // 3. Retry send - await _bleService.sendTextMessage( - contactPublicKey: pendingOp.contactPublicKey, - text: pendingOp.text, - attempt: pendingOp.retryAttempt, - ); - - // 4. Cleanup - _pendingSendOperations.remove(operationId); -}; -``` - -**Send Message Tracking:** -```dart -Future sendTextMessage({...}) async { - // Track operation before sending - if (contact != null) { - _pendingSendOperations[operationId] = _PendingSendOperation(...); - } - - await _bleService.sendTextMessage(...); - - // Clear after 500ms (if no error occurs) - Future.delayed(const Duration(milliseconds: 500), () { - _pendingSendOperations.remove(operationId); - }); -} -``` - -## Testing Scenarios - -### Scenario 1: Room contact deleted from radio -1. User has room "SAR-Command" in app's contact list -2. Room was deleted from companion radio -3. User sends SAR marker to room -4. **Expected:** System automatically adds room and sends message -5. **Verify:** Message appears as "sent" in UI - -### Scenario 2: New room added to app -1. User manually adds new room contact to app -2. Room not yet in companion radio's contact table -3. User sends message to new room -4. **Expected:** System automatically adds room and sends message -5. **Verify:** Message appears as "sent" in UI - -### Scenario 3: Factory reset radio -1. Radio was factory reset (all contacts cleared) -2. App still has cached contacts -3. User sends message to any room -4. **Expected:** System automatically re-adds contact and sends message -5. **Verify:** Message appears as "sent" in UI - -## Console Output Example - -**Before (failed send):** -``` -📤 [TX] Sending command: SEND_TXT_MSG (0x02) - Data size: 46 bytes -📥 [RX] Received: ERROR (0x01) - ❌ [Error] Not found -⚠️ [Provider] BLE error received: Not found -``` - -**After (auto-recovery):** -``` -📤 [TX] Sending command: SEND_TXT_MSG (0x02) - Data size: 46 bytes - 📝 Tracked pending operation for auto-recovery: 8f:a0:f2:68:d0:c1 -📥 [RX] Received: ERROR (0x01) - ❌ [Error] Not found - ⚠️ [Error] Contact not found in radio - attempting auto-recovery -🔧 [Provider] Contact not found error detected - initiating auto-recovery - 📋 Found pending operation for: SAR-Command - 📤 Step 1: Adding contact to radio... -📝 [BLE] Adding/updating contact on companion radio: - Name: SAR-Command - Public key prefix: 8f:a0:f2:68:d0:c1 - Type: room (3) -📤 [TX] Sending command: ADD_UPDATE_CONTACT (0x09) -📥 [RX] Received: OK (0x00) - ✅ Contact added successfully - 🔄 Step 2: Retrying message send... -📤 [TX] Sending command: SEND_TXT_MSG (0x02) -📥 [RX] Received: SENT (0x06) - ✅ Auto-recovery completed - message resent -``` - -## Error Handling - -### If contact is null -``` -⚠️ No pending operation found for recovery: 8f:a0:f2:68:d0:c1 -``` -This happens if the send was called without a Contact object. Cannot auto-recover. - -### If add contact fails -``` -❌ Auto-recovery failed: BLE write error -``` -Error is logged and operation is cleared. User must manually retry. - -### If retry send fails -Same as above - error logged, operation cleared. - -## Benefits - -1. **Zero user intervention** - automatic recovery -2. **Transparent operation** - user sees "sent" status as expected -3. **Robust handling** - works for all ERR_CODE_NOT_FOUND scenarios -4. **Minimal latency** - 300ms delay is imperceptible to user -5. **Clean architecture** - isolated to ConnectionProvider layer - -## Future Enhancements - -1. **Batch recovery** - if multiple contacts missing, add all at once -2. **Proactive sync** - periodically sync app contacts to radio -3. **Smart caching** - track which contacts have been successfully added -4. **UI feedback** - optional: show "Adding contact..." toast during recovery -5. **Telemetry** - track auto-recovery success rate for monitoring - -## Related Files - -- `lib/services/ble/ble_response_handler.dart` - Error detection -- `lib/services/meshcore_ble_service.dart` - Service layer coordination -- `lib/providers/connection_provider.dart` - Auto-recovery orchestration -- `lib/services/protocol/frame_parser.dart` - Error code parsing -- `lib/services/meshcore_constants.dart` - Error code constants - -## MeshCore Protocol Reference - -**Error Codes (ERR_CODE):** -- 1: ERR_CODE_UNSUPPORTED_CMD -- **2: ERR_CODE_NOT_FOUND** ← This is what we handle -- 3: ERR_CODE_TABLE_FULL -- 4: ERR_CODE_BAD_STATE -- 5: ERR_CODE_FILE_IO_ERROR -- 6: ERR_CODE_ILLEGAL_ARG - -**Recovery Commands:** -- CMD_ADD_UPDATE_CONTACT (9): Adds contact to radio's contact table -- CMD_SEND_TXT_MSG (2): Sends message to contact (retried after add) - -## Conclusion - -The auto-recovery system provides a seamless user experience by automatically handling missing contacts in the radio's contact table. Users can now send messages to room contacts without worrying about synchronization issues, and the system will intelligently recover from `ERR_CODE_NOT_FOUND` errors. diff --git a/BLOG_POST.md b/BLOG_POST.md deleted file mode 100644 index 3794905..0000000 --- a/BLOG_POST.md +++ /dev/null @@ -1,289 +0,0 @@ -# MeshCore SAR: Off-Grid Communication for Search & Rescue Operations - -When the grid goes down, lives are on the line. MeshCore SAR is a revolutionary mobile application that enables search and rescue teams to communicate, coordinate, and share critical information—even when cellular networks and internet connectivity are completely unavailable. - -Built on cutting-edge mesh networking technology, MeshCore SAR transforms ordinary smartphones into powerful off-grid communication devices using low-power radio hardware. Whether you're coordinating a wilderness rescue, managing a disaster response, or operating in remote areas, MeshCore SAR keeps your team connected when it matters most. - -## Why MeshCore SAR? - -Traditional communication systems fail when you need them most: -- **Cellular networks** collapse during disasters or don't exist in remote wilderness -- **Satellite phones** are expensive and have limited messaging capabilities -- **Radio systems** require licensing and lack modern features like GPS integration - -MeshCore SAR solves these problems by creating a resilient, self-healing mesh network that: -- ✅ **Works completely off-grid** - No cellular, WiFi, or internet required -- ✅ **Extends range through mesh routing** - Messages hop through nearby devices -- ✅ **Integrates GPS tracking** - Real-time location sharing on offline maps -- ✅ **Specializes in SAR operations** - Purpose-built features for emergency response -- ✅ **Uses affordable hardware** - Low-cost LoRa radios via Bluetooth - ---- - -## 🗨️ Messages: Reliable Communication When Networks Fail - -[*Image placeholder: Messages screen showing conversation with delivery status*] - -At the heart of MeshCore SAR is a robust messaging system designed for mission-critical communication. - -### Key Features: - -**🎯 Multiple Communication Modes** -- **Direct Messages**: Private one-to-one communication with team members -- **Public Channel**: Broadcast updates to all nearby devices -- **Rooms**: Persistent message storage for coordination centers - -**📡 Smart Message Delivery** -- **Intelligent routing**: Messages automatically find the best path through the mesh network -- **Delivery confirmation**: Know when your message reaches its destination with ACK tracking -- **Automatic retries**: Failed messages retry automatically with progressive timeouts -- **Flood fallback**: Critical messages use broadcast mode if routing fails - -**🚨 SAR Marker Messages** -Send location-tagged alerts with a simple message format: -- **🧑 Found Person**: `S:🧑:37.7749,-122.4194:Survivor located, needs medical attention` -- **🔥 Fire Location**: `S:🔥:40.7128,-74.0060:Wildfire spreading rapidly northeast` -- **🏕️ Staging Area**: `S:🏕️:34.0522,-118.2437:Base camp established with supplies` - -These special messages automatically appear as markers on the map, making critical information instantly visual for the entire team. - -**📊 Message Status Tracking** -Every message shows its delivery status: -- ⏳ **Sending** - Message is being transmitted -- ✅ **Sent** - Message queued with expected acknowledgment -- ✔️✔️ **Delivered** - Confirmation received with round-trip time -- 🔄 **Retrying** - Automatic retry in progress -- ❌ **Failed** - Delivery unsuccessful after all attempts - -**🌍 Multilingual Support** -Full localization in English, Croatian (Hrvatski), and Slovenian (Slovenščina) ensures teams can communicate in their native language. - ---- - -## 👥 Contacts: Know Your Team's Status and Location - -[*Image placeholder: Contacts list showing team members with GPS locations and battery levels*] - -MeshCore SAR's contact system goes beyond simple names and numbers—it provides real-time situational awareness for your entire team. - -### Contact Intelligence: - -**📍 Real-Time Location Tracking** -- GPS coordinates automatically broadcast at configurable intervals -- Location history tracking (last 100 positions per contact) -- Distance and bearing calculations from your position -- "Last seen" timestamps for situational awareness - -**🔋 Battery Monitoring** -- Battery percentage displayed for each team member -- Voltage telemetry via Cayenne LPP format -- Early warning when team members need to conserve power - -**🛤️ Mesh Network Routing** -The app shows routing information for each contact: -- **Direct (0 hops)**: Connected directly to your radio -- **Good path (1-2 hops)**: Reliable routing through 1-2 intermediate devices -- **Medium/Long path (3-5+ hops)**: Extended range through multiple hops -- **No path (flood mode)**: Messages broadcast to entire network - -**👔 Role-Based Identification** -- Add emoji prefixes to names (🧑🏻‍🚒 for firefighter, 👮 for police, 🏥 for medical) -- Instant visual identification on map and in contact lists -- Customizable display names - -**📡 Contact Types** -- **Chat** (Team Members): Standard team members shown on map -- **Repeater**: Network infrastructure nodes that extend range -- **Room**: Message servers with persistent storage and login capabilities - -**📞 Contact Sharing** -- Export contacts as business cards -- Share contacts directly over the mesh network -- Import contacts from other team members - ---- - -## 🗺️ Map: Offline Navigation and Tactical Awareness - -[*Image placeholder: Map screen showing team members, SAR markers, and offline terrain*] - -The map is where everything comes together—combining team locations, SAR events, and offline navigation into a single, comprehensive tactical display. - -### Map Features: - -**🗺️ Offline Vector Maps** -- **MBTiles format**: Lightweight vector maps that work completely offline -- **Multiple layers**: Street maps (OpenStreetMap), topographic (OpenTopoMap), satellite imagery (ESRI) -- **High zoom levels**: Street-level detail up to zoom level 19 -- **Smart caching**: Tiles cached locally for 30 days - -**📍 Team Member Tracking** -Each team member appears on the map with: -- Blue circle markers with role emoji -- Battery level badge (green/yellow/red indicators) -- Distance from your location -- Tap to view detailed information and message directly - -**🚨 SAR Event Markers** -Critical events appear as color-coded markers: -- 🟢 **Green** - Found Person -- 🔴 **Red** - Fire Location -- 🔵 **Blue** - Staging Area -- 🟣 **Purple** - Object -- Time elapsed since report -- Tap to navigate and view details - -**✏️ Map Drawing Tools** -Collaborative tactical planning: -- **Line drawings**: Sketch routes, boundaries, or directions -- **Rectangle areas**: Mark zones, perimeters, or sectors -- **8 color palette**: Red, blue, green, yellow, orange, purple, pink, cyan -- **Share drawings**: Send to channel or specific rooms via mesh network -- **Collaborative editing**: All team members see drawings in real-time -- **Ultra-compact format**: Efficient JSON encoding reduces bandwidth usage by 37% - -**🧭 Detailed Compass Dialog** -Ultra-compact location display: -- Current GPS coordinates -- Toggle between Decimal Degrees (DD) and Degrees/Minutes/Seconds (DMS) -- Tap outside to close (no buttons needed) -- Perfect for quick location checks - -**📡 User Location Tracking** -- Blue pulsing circle shows your current position -- Navigation icon for direction -- Tap to center and track your movement -- Configurable accuracy settings - -**📊 Map Legend** -Collapsible legend in top-right corner: -- Team member count -- SAR marker count by type -- Quick reference for marker colors - -**🎯 Smart Navigation** -- Tap any SAR marker in messages to navigate on map -- Automatic tab switching -- Map centers and zooms to selected location -- Clears navigation after viewing - ---- - -## 🔧 Technical Innovation - -### Mesh Network Protocol -- **MeshCore Protocol**: Open-source, battle-tested mesh networking -- **LoRa Radio**: Long-range, low-power wireless technology -- **BLE Connection**: Smartphone connects to companion radio via Bluetooth -- **Intelligent routing**: Self-healing paths through the network -- **Flood mode fallback**: Guaranteed delivery for critical messages - -### Smart Features -- **Adaptive location broadcasting**: Only sends updates when you move significantly -- **Progressive retry logic**: Failed messages retry with increasing timeouts -- **Cayenne LPP telemetry**: Standardized sensor data format -- **Contact synchronization**: Automatic contact list updates -- **Message persistence**: Rooms store messages for later retrieval - -### Built for Reliability -- **Provider-based architecture**: Efficient state management -- **Automatic reconnection**: Handles radio disconnections gracefully -- **Message queue management**: Synchronizes messages in order -- **Battery optimization**: Configurable tracking intervals -- **Memory management**: Automatic history cleanup - ---- - -## 🌟 Real-World Applications - -**Search & Rescue Operations** -- Wilderness rescue coordination -- Missing person searches -- Cave rescue operations -- Mountain rescue teams - -**Disaster Response** -- Hurricane and flood response -- Earthquake emergency communication -- Infrastructure failure scenarios -- Mass casualty incidents - -**Remote Operations** -- Forestry operations -- Mining site communication -- Border patrol and security -- Wildlife management - -**Training & Exercises** -- Team coordination drills -- Radio discipline training -- Navigation exercises -- Emergency preparedness - ---- - -## 🚀 Getting Started - -MeshCore SAR works with affordable LoRa companion radios that connect to your smartphone via Bluetooth. The app handles all the complexity—you just: - -1. **Connect** your radio via Bluetooth -2. **Add contacts** to your team -3. **Start messaging** and tracking locations -4. **Download maps** for your area -5. **Coordinate** your mission - -No cellular service. No internet. No limits. - ---- - -## 🌍 Open Source & Community-Driven - -MeshCore SAR is built on the open-source MeshCore protocol, fostering a community of developers and users who contribute to its continuous improvement. Whether you're a first responder, amateur radio enthusiast, or outdoor adventurer, you're part of a global network working to keep people connected when it matters most. - ---- - -## 💡 The Future of Off-Grid Communication - -In a world increasingly dependent on fragile infrastructure, MeshCore SAR represents a paradigm shift: resilient, decentralized communication that works when everything else fails. As climate change drives more frequent disasters and teams operate in increasingly remote locations, mesh networking isn't just an alternative—it's essential. - -**MeshCore SAR isn't just an app. It's a lifeline.** - ---- - -*MeshCore SAR is compatible with iOS 13+ and Android API 21+. Requires compatible LoRa companion radio hardware.* - -**Repository**: [github.com/meshcore-dev/meshcore.js](https://github.com/meshcore-dev/meshcore.js) - ---- - -## 📸 Screenshots - -*[Placeholder sections for images]* - -### Messages -*Screenshot showing conversation with delivery status, SAR marker messages, and message options* - -### Contacts -*Screenshot showing contact list with GPS locations, battery levels, and routing information* - -### Map - Team View -*Screenshot showing map with multiple team members, distance indicators, and user location* - -### Map - SAR Markers -*Screenshot showing map with various SAR markers (person, fire, staging area) and color coding* - -### Map - Drawing Tools -*Screenshot showing map with line and rectangle drawings, color palette, and toolbar* - -### Map - Offline Layers -*Screenshot showing different map layers (street, topographic, satellite) selection* - -### Settings -*Screenshot showing connection status, radio parameters, and location tracking settings* - -### Contact Details -*Screenshot showing detailed contact information, telemetry data, and message history* - ---- - -**Ready to experience off-grid communication?** Connect your radio and join the mesh network today. diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index b6169a0..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,897 +0,0 @@ -# CLAUDE.md - MeshCore SAR Technical Reference - -AI assistant guide for the MeshCore SAR Flutter application. - -## Table of Contents -1. [Critical Development Rules](#critical-development-rules) -2. [Quick Reference](#quick-reference) -3. [Project Structure](#project-structure) -4. [Protocol Reference](#protocol-reference) -5. [Architecture](#architecture) -6. [Common Tasks](#common-tasks) -7. [Build & Troubleshooting](#build--troubleshooting) - ---- - -## Critical Development Rules - -### Flutter Process Management -**NEVER run or kill Flutter processes:** -- ❌ 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 - -### Key Architecture Constraints -- **Echo Detection**: Uses DJB2-style hash (NO crypto dependency needed) -- **Channel Messages**: NO ACKs (fire-and-forget flood routing) -- **Direct Messages**: Automatic ACKs via `PUSH_CODE_SEND_CONFIRMED` -- **SAR Markers**: MUST be sent to rooms, NOT public channel - ---- - -## Quick Reference - -**Project**: Flutter Mobile App (iOS 13+, Android API 21+) -**Architecture**: Provider pattern + BLE communication -**Protocol**: MeshCore BLE Companion Radio (Little Endian) -**Repository**: https://github.com/meshcore-dev/meshcore.js - -### BLE UUIDs -``` -Service: 6E400001-B5A3-F393-E0A9-E50E24DCCA9E -RX (write): 6E400002-B5A3-F393-E0A9-E50E24DCCA9E -TX (notify): 6E400003-B5A3-F393-E0A9-E50E24DCCA9E -``` - -### Key Dependencies -```yaml -flutter_blue_plus: ^2.0.0 # BLE communication -flutter_map: ^8.2.2 # Mapping + WMS support -provider: ^6.1.0 # State management -geolocator: ^14.0.2 # GPS tracking -proj4dart: ^2.1.0 # Coordinate transformations (EPSG:3794) -flutter_map_tile_caching: ^10.1.1 # Offline tile caching -``` - ---- - -## Project Structure - -``` -lib/ -├── l10n/ # Internationalization (en, hr, sl) -│ ├── app_localizations.dart # Generated (DO NOT EDIT) -│ └── app_*.arb # Translation files -├── models/ # Data models -│ ├── contact.dart, message.dart, sar_marker.dart -│ ├── map_drawing.dart # Drawing shapes -│ └── sent_message_tracker.dart # Echo detection -├── services/ # Business logic -│ ├── meshcore_ble_service.dart # BLE coordinator -│ ├── protocol/ # Frame parsing & building -│ ├── ble/ # Connection, commands, responses -│ ├── location_tracking_service.dart # GPS + broadcast -│ ├── map_marker_service.dart # Marker generation -│ ├── tile_cache_service.dart # Offline tile caching (WMS + standard) -│ └── validation_service.dart # Form validation -├── providers/ # State management -│ ├── connection_provider.dart # BLE state -│ ├── contacts_provider.dart # Contact list -│ ├── messages_provider.dart # Messages + SAR -│ ├── map_provider.dart # Map navigation -│ ├── drawing_provider.dart # Map drawings -│ └── app_provider.dart # Coordinator -├── screens/ # UI screens -│ └── (home, messages, contacts, map, settings, etc.) -├── widgets/ # Reusable components -│ ├── map_markers.dart -│ └── map/ # Map-specific widgets -└── utils/ # Utilities - ├── sar_message_parser.dart - ├── drawing_message_parser.dart - └── slovenian_crs.dart # EPSG:3794 CRS for WMS -``` - ---- - -## Protocol Reference - -### Message Formats - -#### SAR Marker Format -``` -S::,: - -Emojis: - 🧑 or 👤 → Found Person - 🔥 → Fire Location - 🏕️ or ⛺ → Staging Area - -Examples: - S:🧑:37.7749,-122.4194 - S:🔥:40.7128,-74.0060:Large wildfire spreading rapidly -``` - -#### Map Drawing Format -``` -D: - -Line: D:{"t":0,"c":0,"p":[lat1,lon1,lat2,lon2]} -Rectangle: D:{"t":1,"c":1,"b":[topLat,topLon,botLat,botLon]} - -Fields: - t = type (0=line, 1=rectangle) - c = color index (0-7: red,blue,green,yellow,orange,purple,pink,cyan) - p = points array (flat) - b = bounds array (rectangles only) -``` - -#### Cayenne LPP Format -``` -[Channel][Type][Data...] - -Types: - 0x88 (136) → GPS: lat/lon/alt (int32/10000, int32/10000, int32/100) - 0x67 (103) → Temperature (int16/10 for °C) - 0x02 (2) → Analog Input (uint16/100 for volts/battery) -``` - -### Command Quick Reference - -| Code | Command | Response | Description | -|------|---------|----------|-------------| -| 1 | CMD_APP_START | RESP_CODE_SELF_INFO (5) | First command after connection | -| 2 | CMD_SEND_TXT_MSG | RESP_CODE_SENT (6) | Send DM to contact (with ACK) | -| 3 | CMD_SEND_CHANNEL_TXT_MSG | RESP_CODE_SENT (6) | Broadcast to channel (NO ACK) | -| 4 | CMD_GET_CONTACTS | RESP_CODE_CONTACTS_START (2) | Sync contact list | -| 10 | CMD_SYNC_NEXT_MESSAGE | RESP_CODE_CONTACT_MSG_RECV (7) | Pull next message from queue | -| 22 | CMD_DEVICE_QUERY | RESP_CODE_DEVICE_INFO (13) | Get device firmware/hardware info | -| 26 | CMD_SEND_LOGIN | PUSH_CODE_LOGIN_SUCCESS (0x85) | Login to room server | - -### Push Notifications (Async) - -| Code | Name | Purpose | -|------|------|---------| -| 0x80 | PUSH_CODE_ADVERT | New advertisement received | -| 0x82 | PUSH_CODE_SEND_CONFIRMED | Message ACK received (DMs only) | -| 0x83 | PUSH_CODE_MSG_WAITING | New message in queue → sync it | -| 0x88 | PUSH_CODE_LOG_RX_DATA | **Raw packet capture (always-on diagnostic)** | -| 0x85 | PUSH_CODE_LOGIN_SUCCESS | Room login successful | -| 0x86 | PUSH_CODE_LOGIN_FAIL | Room login failed | - -### Constants - -#### Contact Types (ADV_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 -``` - -#### Message Types (TXT_TYPE) -``` -0 = TXT_TYPE_PLAIN # Plain text -1 = TXT_TYPE_CLI_DATA # CLI command -2 = TXT_TYPE_SIGNED_PLAIN # Text + 4-byte pubkey signature -``` - -#### Error Codes (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 -``` - ---- - -## Architecture - -### Provider Hierarchy -``` -MultiProvider -├── ConnectionProvider # BLE connection state -├── ContactsProvider # Contact list management -├── MessagesProvider # Messages + SAR markers -├── MapProvider # Map navigation state -├── DrawingProvider # Map drawing state -└── AppProvider # Coordinator (wires everything) -``` - -### Event Flow -``` -BLE Device → MeshCoreBleService → ConnectionProvider → AppProvider - ↓ - ContactsProvider - MessagesProvider - DrawingProvider - ↓ - UI -``` - -### BLE Keepalive for iOS Background Mode - -**Purpose**: Dual-purpose timer that prevents iOS from terminating BLE connections AND provides fallback message syncing. - -**Implementation** (`lib/services/meshcore_ble_service.dart:654-681`): -- Timer sends `syncNextMessage()` command every 20 seconds -- Automatically starts when connected, stops when disconnected -- Logs: "💚 [BLE] Keepalive: Connection maintained & messages synced" -- Handles errors gracefully (iOS may throttle commands temporarily) - -**Dual Purpose**: -1. **Connection Keepalive**: iOS aggressively kills idle BLE connections to save battery. Periodic commands signal active usage and prevent ~30 second disconnection timeout. -2. **Fallback Message Sync**: Provides redundancy when `PUSH_CODE_MSG_WAITING` (0x83) push notifications fail to trigger. Device responds with `RESP_CODE_NO_MORE_MSG` if queue is empty. - -**Connection Flow**: -- Upon connection, `_sendDeviceQuery()` immediately calls `syncNextMessage()` to fetch any messages received while disconnected -- Keepalive timer then continues periodic syncing for reliability - -**Technical Notes**: -- TX characteristic (6E400003) is notify-only, not readable -- RX characteristic (6E400002) is write-only -- Uses `CMD_SYNC_NEXT_MESSAGE` (10) which is idempotent and safe to call repeatedly - -**Alternative**: Location broadcasts also keep connection alive, but keepalive provides redundancy for stationary scenarios and ensures message delivery. - -### Contact Path Status - -**outPathLen** indicates routing mode: -- **-1 (0xFF)**: Path unknown → **Flood mode** (broadcast to all neighbors) -- **0**: Direct connection → **Direct mode** (zero hops, best quality) -- **1-64**: Multi-hop path → **Direct mode** (uses learned routing) - -**Map Display**: Only `ContactType.chat` contacts with valid GPS coordinates shown. - -### Channels vs. Rooms - -**Channels** (numeric identifiers): -- Channel 0 = "Public Channel" (default broadcast) -- **Ephemeral** - messages NOT persisted -- Uses `CMD_SEND_CHANNEL_TXT_MSG` (3) -- **NO ACKs** - fire-and-forget flood routing -- Pre-configured with secret: `8b3387e9c5cdea6ac9e5edbaa115cd72` (hex) - -**Rooms** (ADV_TYPE_ROOM contacts): -- Named contacts with public keys -- **Persistent storage** on room server -- Uses `CMD_SEND_TXT_MSG` (2) with room's public key -- **Automatic ACKs** when messages stored -- Login via `CMD_SEND_LOGIN` (26) to receive stored messages - -**SAR Routing**: SAR markers MUST go to rooms for reliable delivery. - -### Room Login Protocol - -``` -1. Send CMD_SEND_LOGIN (26) - ↓ -2. Radio generates sender_timestamp & sync_since - ↓ -3. Room validates password, stores sync_since - ↓ -4. Receive PUSH_CODE_LOGIN_SUCCESS (0x85) or PUSH_CODE_LOGIN_FAIL (0x86) - ↓ -5. Room auto-pushes messages (1200ms intervals) - ↓ -6. Receive PUSH_CODE_MSG_WAITING (0x83) → call CMD_SYNC_NEXT_MESSAGE (10) -``` - -**CRITICAL**: Do NOT call `syncAllMessages()` after login. Wait for push notifications. - ---- - -## Echo Detection Feature - -### Overview -**Status**: ✅ Fully implemented and production-ready -**Purpose**: Detect when broadcast messages are rebroadcast by mesh nodes - -### How It Works - -1. **Packet Identification** (via `PUSH_CODE_LOG_RX_DATA` 0x88): - ``` - Packet Structure (PAYLOAD_TYPE_GRP_TXT 0x05): - [Byte 0] = Header (route + payload type + version) - [Byte 1] = Path length - [Byte 2] = Sender's node hash (first byte of public key) ✅ - [Byte 3+] = Rest of path + encrypted payload - ``` - -2. **On Connection**: - - Receive `RESP_CODE_SELF_INFO` with our public key - - Extract **our node hash** (byte 0 of public key) - - Store for packet matching - -3. **Sending a Message**: - - Call `trackSentMessage(messageId)` when user sends channel message - - Status = "pending" (waiting for packet capture) - -4. **Packet Capture** (50-200ms later): - - Radio sends `PUSH_CODE_LOG_RX_DATA` with raw packet - - Extract sender hash from packet[2] - - If sender hash == our node hash → **This is our packet!** - - Calculate DJB2-style hash of entire packet (no crypto dependency) - - Store tracker by packet hash for echo detection - -5. **Echo Detection**: - - Future `PUSH_CODE_LOG_RX_DATA` packets arrive - - Calculate packet hash and lookup in tracker map (O(1)) - - If match found → **Echo detected!** Increment counter - - Track SNR/RSSI signature for path diversity - - UI auto-updates to show "Rebroadcast by X nodes" - -### Implementation Details - -**Files**: -- `lib/models/sent_message_tracker.dart` - Tracker model -- `lib/models/message.dart` - echoCount, firstEchoAt fields -- `lib/services/ble/ble_response_handler.dart` - Detection engine -- `lib/services/meshcore_ble_service.dart` - Callback wiring -- `lib/providers/connection_provider.dart` - Provider callback -- `lib/providers/messages_provider.dart` - handleMessageEcho() - -**Performance**: -- Packet ID: O(1) - byte comparison at offset 2 -- Hash calc: O(n) where n = packet length (~38-200 bytes) -- Echo lookup: O(1) via HashMap -- Memory: ~150 bytes/message, max 100 messages = ~15KB -- TTL: 5-minute expiry, auto-cleanup - -**Limitations**: -- Echo count ≠ exact receiver count (one node can echo multiple times) -- Only detects echoes while app connected -- Network topology dependent (dense networks → more echoes) - ---- - -## Common Tasks - -### Adding a New BLE Command - -1. Add code to `lib/services/meshcore_constants.dart` -2. Build frame in `lib/services/protocol/frame_builder.dart` -3. Add API method in `lib/services/meshcore_ble_service.dart` -4. Parse response in `lib/services/protocol/frame_parser.dart` -5. Handle 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` -2. Add parser logic in `lib/utils/sar_message_parser.dart` -3. Add color mapping in `lib/widgets/map_markers.dart` -4. Update handling in `lib/providers/messages_provider.dart` - -### Adding Localized Strings - -1. **Add to `lib/l10n/app_en.arb`**: - ```json - { - "myNewString": "My new text", - "@myNewString": { - "description": "What this string is for" - } - } - ``` - -2. **Add translations to `app_hr.arb` and `app_sl.arb`** - -3. **Generate**: `flutter gen-l10n` - -4. **Use in code**: - ```dart - import '../l10n/app_localizations.dart'; - - Text(AppLocalizations.of(context)!.myNewString) - ``` - -**IMPORTANT**: Always use relative import path `'../l10n/app_localizations.dart'` - -### Importing/Exporting Map Tiles - -The app supports importing and exporting cached map tiles using the `flutter_map_tile_caching` library's archive format (`.fmtc` files). - -**Export Workflow**: -1. Navigate to Map Management screen -2. Tap "Export Tiles to File" -3. Archive is created in temporary directory with gzip compression -4. System share sheet appears (iOS/Android) -5. Choose where to save: Files app, email, cloud storage, etc. -6. File named: `meshcore_tiles_.fmtc` - -**Import Workflow**: -1. Navigate to Map Management screen -2. Tap "Import Tiles from File" -3. Select `.fmtc` archive file -4. Tiles are merged with existing cache -5. Cache statistics refreshed automatically - -**API Usage**: - -```dart -// Export current cache to file -final tileCount = await tileCacheService.exportStore( - '/path/to/export.fmtc', -); - -// Import tiles from archive (with merge strategy) -final result = await tileCacheService.importStore( - '/path/to/import.fmtc', - storeNames: null, // null = import all stores - strategy: ImportConflictStrategy.merge, // default: merge -); - -// Preview stores in archive before importing -final stores = await tileCacheService.listArchiveStores( - '/path/to/archive.fmtc', -); -``` - -**Use Cases**: -- **Backup**: Export tiles before clearing cache or reinstalling app -- **Sharing**: Pre-download maps once, distribute to team devices -- **Disaster Recovery**: Restore offline maps after device reset -- **Bandwidth Saving**: Reduce cellular data usage by sharing cached tiles - -**Technical Details**: -- Archive format: `.fmtc` (FMTC native format) -- Compression: gzip (built-in by FMTC) -- Import strategy: Merge (tiles combined with existing cache) -- Export location: Temporary directory → system share sheet -- Import source: User-selected via `file_picker` package -- Conflict handling: Automatic merge of tile stores -- Cross-platform: Works on Android and iOS using native share mechanisms - -**File**: `lib/services/tile_cache_service.dart` - Export/import methods -**UI**: `lib/screens/map_management_screen.dart` - Import/export card - -### Map Drawing Workflow - -``` -User selects mode → DrawingProvider.setDrawingMode() - ↓ -User taps map → DrawingLayer captures touches - ↓ -Preview rendered → DrawingProvider.getPreviewDrawing() - ↓ -User completes → Saved to DrawingProvider._drawings - ↓ -User shares → DrawingToolbar._shareDrawingsToChannel/Room() - ↓ -BLE send → ConnectionProvider.sendChannelMessage/sendTextMessage() - ↓ -Receiver parses → DrawingMessageParser.parseDrawingMessage() - ↓ -Display → DrawingProvider.addReceivedDrawing() -``` - ---- - -## Build & Troubleshooting - -### Build Commands - -```bash -# Dependencies -flutter pub get -flutter gen-l10n # After ARB file changes - -# Development -flutter run # Debug mode (user controls this) -# Hot reload: save file | Hot restart: press 'R' in terminal - -# Code Quality -flutter analyze -flutter test -dart format lib/ -flutter clean - -# Release -flutter build ios --release -flutter build ipa -flutter build apk --release -flutter build appbundle --release -``` - -### Common Issues - -**BLE Connection Failed**: -- Check Bluetooth enabled and permissions granted -- Verify device in range (<10m) -- Check service UUID matches: `6E400001-B5A3-F393-E0A9-E50E24DCCA9E` - -**MissingPluginException** (after adding dependencies): -```bash -cd ios && pod install && cd .. -flutter clean -flutter pub get -flutter run -``` - -**iOS Pod Install Fails**: -```bash -cd ios -rm Podfile.lock -rm -rf Pods/ -pod install --repo-update -cd .. -``` - -**Android Gradle Timeout** - Add to `android/gradle.properties`: -``` -org.gradle.daemon=true -org.gradle.parallel=true -org.gradle.jvmargs=-Xmx4096m -``` - -### Performance Tips - -**BLE**: -- Buffer partial packets -- Throttle telemetry (max 1/sec per contact) -- Use `notifyListeners()` sparingly - -**Map**: -- Cluster markers if >100 visible -- Use `RepaintBoundary` for marker widgets -- Implement marker virtualization for large datasets - -**Memory**: -- Dispose controllers in `dispose()` methods -- Limit message history to 1000 messages -- Set tile cache size limits - ---- - -## Services Reference - -### LocationTrackingService (Singleton) -**Purpose**: GPS tracking for map display and trail recording - -**Callbacks**: `onPositionUpdate`, `onError`, `onBroadcastSent`, `onTrackingStateChanged` - -**Broadcasting**: -- **Automatic broadcasting DISABLED** - No automatic position adverts to mesh network -- Manual broadcasting available via `broadcastLocationNow()` method -- User must explicitly use the advert button to broadcast location -- GPS tracking continues for map display, currentPosition tracking, and trail recording - -**File**: `lib/services/location_tracking_service.dart` - -### MapMarkerService (Singleton) -**Purpose**: Map marker generation + geodesic calculations - -**Features**: -- Pure functions (testable) -- Contact markers with battery badge, distance -- SAR markers (color-coded by type) -- Distance/bearing calculations (Haversine) -- "Time ago" formatting - -**File**: `lib/services/map_marker_service.dart` (518 lines) - -### ValidationService (Singleton) -**Purpose**: Form validation + input parsing - -**Returns**: Structured `ValidationResult` or `ParseResult` - -**Validates**: -- Coordinates (lat: -90 to +90, lon: -180 to +180) -- Radio params (freq: 137-1020 MHz, bw: 7.8-500 kHz, sf: 5-12, cr: 5-8, tx: -9 to +22 dBm) -- Text/names, zoom levels (0-19) - -**File**: `lib/services/validation_service.dart` (511 lines) - ---- - -## Map Implementation - -### Tile Layers -1. **OpenStreetMap** (default) - Max zoom 19 -2. **OpenTopoMap** - Max zoom 17, topographic -3. **ESRI World Imagery** - Max zoom 19, satellite - -### WMS Support -**Purpose**: Integration with Web Map Service (WMS) providers for specialized mapping data - -**Slovenian CRS (EPSG:3794)**: -- Projection: Transverse Mercator (Slovenia 1996 / Slovene National Grid) -- Ellipsoid: GRS80 -- Usage: Slovenian government WMS/WMTS services (prostor.zgs.gov.si) -- Zoom levels: 0-15 (GeoWebCache tile matrix) -- Bounds: X: 373217.65-695777.65m, Y: 31118.30-246158.30m -- Origin: Top-left (373217.65, 246158.30) -- Resolutions: Calculated from scale denominators (420m/px at zoom 0 to 0.028m/px at zoom 15) -- **Language Filter**: Only shown when app language is Slovenian (sl) or Croatian (hr) - -**Tile Caching**: -- All WMS layers (base + overlays) use `flutter_map_tile_caching` -- Cache strategy: `cacheFirst` (offline-first with 30-day validity) -- Same caching infrastructure as standard tile layers - -**Files**: -- `lib/utils/slovenian_crs.dart` - EPSG:3794 CRS definition -- `lib/services/tile_cache_service.dart` - getTileProviderForWms() method - -**Example**: -```dart -import 'package:meshcore_sar_app/utils/slovenian_crs.dart'; - -// All WMS layers automatically use the cached tile provider -TileLayer( - wmsOptions: WMSTileLayerOptions( - baseUrl: 'https://prostor.zgs.gov.si/geowebcache/service/wms?', - layers: ['pregledovalnik:DOF_2024'], - format: 'image/jpeg', - crs: slovenianCrs, - ), - tileProvider: tileCacheService.getTileProviderForWms(layer), -) -``` - -### WMS Implementation Details - -#### Overview -The app integrates Slovenian government WMS (Web Map Service) layers using a custom EPSG:3794 coordinate reference system. This enables high-resolution aerial imagery and specialized overlays (cadastral parcels, forest roads) for SAR operations in Slovenia. - -**Language-Based Filtering**: WMS layers are only shown to users when the app language is set to Slovenian (sl) or Croatian (hr), since these layers only cover Slovenia geographically and are irrelevant to users in other regions. - -#### Architecture - -**Layer Types**: -1. **Base Layer**: Slovenian Aerial Imagery 2024 (DOF_2024) - - Source: `https://prostor.zgs.gov.si/geowebcache/service/wms` - - Format: JPEG (better compression for aerial photos) - - Transparency: False (opaque base layer) - - Max zoom: 15 (GeoWebCache limit) - -2. **Overlay Layers**: Cadastral Parcels, Forest Roads - - Format: PNG (supports transparency) - - Transparency: True (overlays on base layer) - - Max zoom: 19 - -**Coordinate System (EPSG:3794)**: -- Official name: Slovenia 1996 / Slovene National Grid -- Projection: Transverse Mercator -- Parameters: `+proj=tmerc +lat_0=0 +lon_0=15 +k=0.9999 +x_0=500000 +y_0=-5000000 +ellps=GRS80` -- Why needed: Slovenian government services use this instead of standard Web Mercator (EPSG:3857) - -**Tile Grid Alignment**: -The critical challenge with WMS layers is aligning the client-side tile grid with the server's GeoWebCache configuration. Misalignment causes 400 Bad Request errors. - -**Correct Configuration** (`lib/utils/slovenian_crs.dart`): -```dart -// Origin MUST match WMTS TileMatrixSet TopLeftCorner -final origin = Point(373217.6542445397, 246158.298050262); - -// Bounds MUST match WMS capabilities extent -final bounds = Rect.fromLTRB( - 373217.65, // min X (west) - 31118.30, // min Y (south) - 695777.65, // max X (east) - 246158.30, // max Y (north) -); - -// Resolutions MUST be calculated from scale denominators -// Formula: resolution = scaleDenominator * 0.00028 (OGC standard) -final resolutions = [ - 420.0, // Zoom 0: 1,500,000 * 0.00028 - 280.0, // Zoom 1: 1,000,000 * 0.00028 - // ... through zoom 15 -]; -``` - -**How to Get Correct Values**: -1. Query WMTS GetCapabilities: - ```bash - curl "https://prostor.zgs.gov.si/geowebcache/service/wmts?REQUEST=GetCapabilities&SERVICE=WMTS" - ``` -2. Find `` for EPSG:3794 -3. Extract `` (origin) -4. Extract `` for each `` (convert to resolutions) -5. Query WMS GetCapabilities for bounds: - ```bash - curl "https://prostor.zgs.gov.si/geowebcache/service/wms?REQUEST=GetCapabilities&SERVICE=WMS" - ``` - -#### Caching Strategy - -**Implementation** (`lib/services/tile_cache_service.dart`): -```dart -FMTCTileProvider getTileProviderForWms(MapLayer layer) { - return _store.getTileProvider( - loadingStrategy: BrowseLoadingStrategy.cacheFirst, - cachedValidDuration: const Duration(days: 30), - ); -} -``` - -**Behavior**: -1. **Cache First**: Check local cache before network request -2. **30-Day Validity**: Tiles expire after 30 days (suitable for aerial imagery that updates infrequently) -3. **Automatic Caching**: All viewed tiles automatically saved to ObjectBox database -4. **Offline Support**: Cached tiles available when device offline - -**Storage Location**: -- Backend: ObjectBox (embedded database) -- Store name: 'meshcore_tiles' (shared with standard tile layers) -- Format: Binary tile data + metadata (URL, timestamp, headers) - -#### Usage in Map - -**Language Filtering** (`lib/screens/map_tab.dart`): - -The following UI elements are only visible when `localeName == 'sl' || localeName == 'hr'`: -1. **Slovenian Aerial 2024 layer** in the base layer selector -2. **Cadastral Parcels overlay toggle** in map options -3. **Forest Roads overlay toggle** in map options -4. **WMS Overlays section divider** and header - -```dart -// Implementation pattern -final locale = AppLocalizations.of(context)!.localeName; -if (locale == 'sl' || locale == 'hr') { - // Show WMS layer option - ListTile( - title: Text(_slovenianAerialLayer.name), - // ... - ); -} -``` - -This ensures users in other regions (English, German, French, Spanish, Italian) don't see geographically irrelevant layers. - -**Base Layer** (`lib/screens/map_tab.dart`): -```dart -if (_currentLayer.isWms && _currentLayer.crs != null) { - TileLayer( - wmsOptions: WMSTileLayerOptions( - baseUrl: _currentLayer.wmsBaseUrl!, - layers: _currentLayer.wmsLayers ?? [], - format: _currentLayer.wmsFormat ?? 'image/jpeg', - crs: _currentLayer.crs!, // EPSG:3794 - ), - tileProvider: _tileCache.getTileProviderForWms(_currentLayer), - maxZoom: _currentLayer.maxZoom, // 15 for Slovenian layers - ); -} -``` - -**Map Options**: -```dart -MapOptions( - // CRITICAL: Use layer's CRS, not default EPSG:3857 - crs: _currentLayer.crs ?? const Epsg3857(), - // ... other options -) -``` - -#### Troubleshooting - -**RangeError: Invalid value: Not in inclusive range 0..15: 16**: -- **Cause**: Map zoom level exceeds the number of resolutions defined in Slovenian CRS -- **Fix**: Zoom level is automatically clamped when switching to WMS layers -- **Implementation**: - - Layer switching clamps zoom to `layer.maxZoom` if current zoom exceeds it - - Layer loading from settings clamps `_savedMapZoom` to layer's maximum - - Prevents crash when switching from high-zoom layer (19+) to WMS layer (15 max) - -**400 Bad Request Errors**: -- **Cause**: Tile grid misalignment (wrong origin, bounds, or resolutions) -- **Fix**: Verify values match WMTS GetCapabilities exactly -- **Debug**: Check WMS URL in error logs for out-of-bounds coordinates - -**Tiles Not Caching**: -- **Cause**: Using wrong tile provider (e.g., NetworkTileProvider instead of FMTC) -- **Fix**: Ensure `getTileProviderForWms()` is used, not `NetworkTileProvider()` or custom providers -- **Verify**: Check `tile_cache_service.dart:75` is being called - -**Layer Not Appearing**: -- **Cause 1**: Layer name mismatch (e.g., `DOF_2024` vs `pregledovalnik:DOF_2024`) -- **Cause 2**: Wrong CRS in MapOptions (using EPSG:3857 instead of EPSG:3794) -- **Fix**: Verify layer name in WMS GetCapabilities, ensure `crs: _currentLayer.crs` in MapOptions - -**Performance Issues**: -- **Issue**: Slow tile loading on first view -- **Expected**: WMS tile generation is slower than pre-rendered tiles (100-500ms per tile) -- **Mitigation**: Pre-download regions using Map Management screen - -#### Adding New WMS Layers - -1. **Find Layer in GetCapabilities**: - ```bash - curl "https://prostor.zgs.gov.si/geowebcache/service/wms?REQUEST=GetCapabilities" | grep "" - ``` - -2. **Add to MapLayer** (`lib/models/map_layer.dart`): - ```dart - static MapLayer getMyNewLayer(Crs slovenianCrs) { - return MapLayer( - type: MapLayerType.wmsBase, // or create new enum value - name: 'My New Layer', - urlTemplate: '', // Not used for WMS - attribution: '© Data Provider', - maxZoom: 15, // Match GeoWebCache capability - isWms: true, - wmsBaseUrl: 'https://prostor.zgs.gov.si/geowebcache/service/wms?', - wmsLayers: const ['workspace:layername'], - wmsFormat: 'image/png', // or 'image/jpeg' - wmsTransparent: true, // true for overlays, false for base - crs: slovenianCrs, - ); - } - ``` - -3. **Verify CRS Support**: Ensure layer supports EPSG:3794 in GetCapabilities - -4. **Test**: Check for 400 errors, verify tiles load correctly - -#### Technical Notes - -**Why Not Use WMTS Instead of WMS?** -- `flutter_map` has excellent WMS support via `WMSTileLayerOptions` -- WMS and WMTS use same GeoWebCache backend (identical tiles) -- WMS is simpler to configure (no manual tile URL template) -- Caching abstracts the protocol difference - -**Proj4dart Integration**: -- Handles coordinate transformation from EPSG:4326 (GPS) to EPSG:3794 (map) -- Projection registered once at app startup: `proj4.Projection.add('EPSG:3794', ...)` -- Flutter Map uses it automatically when `crs: slovenianCrs` is set - -**Memory Considerations**: -- Each CRS instance stores transformation matrices and bounds -- Use singleton pattern: `final Crs slovenianCrs = getSlovenianCrs();` -- Shared across all WMS layers - -### Offline Caching -- Backend: `flutter_map_tile_caching` + ObjectBox -- Behavior: `CacheBehavior.cacheFirst`, 30-day validity -- Downloads: `RectangleRegion(bounds).download.startForeground()` - -### Marker Types -**Team Member**: Blue circle, battery badge, name, distance, tap for details -**SAR Event**: Color-coded (green=person, red=fire, orange=staging), time ago, tap for details - -### Navigation Flow -``` -Messages tab → tap SAR marker - ↓ -MapProvider.navigateToLocation() - ↓ -Switch to Map tab - ↓ -MapTab._handleMapNavigation() → animate to location - ↓ -MapProvider.clearNavigation() -``` - ---- - -## 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) -- [MeshCore FAQ](https://github.com/meshcore-dev/MeshCore/blob/main/docs/faq.md) -- [Cayenne LPP Specification](https://developers.mydevices.com/cayenne/docs/lora/#lora-cayenne-low-power-payload) -- [Provider Package](https://pub.dev/packages/provider) -- [EPSG:3794 Reference](https://epsg.io/3794) - Slovenian CRS definition -- [Proj4dart Package](https://pub.dev/packages/proj4dart) - Coordinate transformation library -- [OGC WMS Specification](https://www.ogc.org/standards/wms) - Web Map Service standard - ---- - -## Security Considerations - -- **BLE**: No authentication in current protocol - consider encryption for production -- **Permissions**: Request minimum required permissions only -- **Logging**: No sensitive data in logs -- **Network**: HTTPS for all tile sources -- **Raw Packets**: `PUSH_CODE_LOG_RX_DATA` exposes all radio traffic (diagnostic feature) diff --git a/IMPLEMENTATION_NOTES.md b/IMPLEMENTATION_NOTES.md deleted file mode 100644 index 99fb419..0000000 --- a/IMPLEMENTATION_NOTES.md +++ /dev/null @@ -1,211 +0,0 @@ -# Vector Map Tiles Implementation - Technical Notes - -## Successfully Implemented! ✅ - -The vector map tiles with MBTiles support has been successfully implemented and the app builds without errors. - -## Final Package Versions - -```yaml -vector_map_tiles: ^9.0.0-beta.8 # flutter_map 8.x compatible! -vector_map_tiles_mbtiles: 1.2.1 # from git repository (latest) -vector_tile_renderer: ^6.0.0 -mbtiles: ^0.4.2 -file_picker: ^8.3.7 -http: 1.5.0 -``` - -### Why Git Dependency? - -The published version of `vector_map_tiles_mbtiles` on pub.dev doesn't support `vector_map_tiles` v9 beta yet. The git version from the flutter_map_plugins repository is compatible: - -```yaml -vector_map_tiles_mbtiles: - git: - url: https://github.com/josxha/flutter_map_plugins.git - path: vector_map_tiles_mbtiles -``` - -## API Compatibility Issues Resolved - -### 1. Name Collision: Theme - -**Problem**: Both Flutter Material and vector_tile_renderer export a `Theme` class. - -**Solution**: Import vector_tile_renderer with alias: -```dart -import 'package:vector_tile_renderer/vector_tile_renderer.dart' as vtr; - -// Usage -vtr.Theme? _vectorTheme; -final theme = vtr.ThemeReader().read(styleJson); -``` - -### 2. Name Collision: TileLayer - -**Problem**: Both flutter_map and vector_tile_renderer export `TileLayer`. - -**Solution**: Import flutter_map with alias for explicit TileLayer usage: -```dart -import 'package:flutter_map/flutter_map.dart' as flutter_map; -import 'package:flutter_map/flutter_map.dart'; // Keep non-aliased for other classes - -// Usage -flutter_map.TileLayer(...) -``` - -### 3. MBTiles API Changes - -**Problem**: The `mbtiles` package v0.4.2 changed from Map-based to object-based API. - -**Old API (v0.3.x)**: -```dart -final metadata = await mbtiles.getMetadata(); -final name = metadata['name']; // Map access -final minZoom = metadata['minzoom']; -``` - -**New API (v0.4.2)**: -```dart -final metadata = await mbtiles.getMetadata(); -final name = metadata.name; // Object property -final minZoom = metadata.minZoom?.toInt(); // Returns double? -``` - -**Key Changes**: -- `getMetadata()` returns `MbTilesMetadata` object, not `Map` -- Properties like `minZoom`, `maxZoom` are now `double?` instead of `int?` -- `bounds` is now `MbTilesBounds` object with no direct property access -- `type` is now `TileLayerType?` enum instead of `String?` -- Some properties removed: `attribution`, `center`, `json` - -**Our Solution**: -```dart -final metadata = await mbtiles.getMetadata(); - -// Convert types appropriately -return MbtilesMetadata( - name: metadata.name ?? _getFileNameWithoutExtension(file), - description: metadata.description, - version: metadata.version?.toString(), // double? to String? - attribution: null, // Not available in new API - bounds: metadata.bounds.toString(), // Object to String - center: null, // Not available in new API - minZoom: metadata.minZoom?.toInt(), // double? to int? - maxZoom: metadata.maxZoom?.toInt(), // double? to int? - format: metadata.format, - type: metadata.type?.name, // TileLayerType? to String? - json: null, // Not available in new API - file: file, - fileSize: fileSize, -); -``` - -### 4. Type Mismatch: maximumZoom - -**Problem**: `VectorTileLayer.maximumZoom` expects `double`, not `int`. - -**Solution**: Remove `.toInt()` call: -```dart -VectorTileLayer( - theme: _vectorTheme!, - tileProviders: TileProviders({...}), - maximumZoom: _currentLayer.maxZoom, // Already double -) -``` - -## File Structure - -``` -lib/ -├── services/ -│ ├── mbtiles_service.dart (280 lines) - NEW -│ └── tile_cache_service.dart (+20 lines) -├── models/ -│ └── map_layer.dart (+40 lines) -├── screens/ -│ ├── map_tab.dart (+50 lines) -│ └── map_management_screen.dart (+180 lines) -└── l10n/ - └── app_en.arb (+80 lines) - -Total: ~650 new lines of code -``` - -## Build Status - -- ✅ iOS: Build successful (28.7MB) -- ⏳ Android: Not tested yet -- ⏳ Runtime: Not tested with actual MBTiles file - -## Testing Checklist - -### Before Runtime Testing - -- [x] Code compiles without errors -- [x] All imports resolved -- [x] API compatibility verified -- [ ] Import MBTiles file -- [ ] Switch to vector layer -- [ ] Verify style loading -- [ ] Verify vector rendering -- [ ] Test offline mode -- [ ] Test file deletion - -### Known Limitations - -1. **Missing Metadata**: `attribution`, `center`, and `json` fields are not available in mbtiles v0.4.2 -2. **Bounds Format**: Bounds are stored as string representation of MbTilesBounds object -3. **Schema Detection**: Limited to checking description and name for "shortbread" or "openmaptiles" keywords - -### Recommendations for Production - -1. **Add Error Handling**: Wrap vector tile rendering in try-catch to fall back to raster -2. **Cache Styles**: Persist downloaded styles to avoid re-downloading -3. **Validate MBTiles**: Add file format validation before import -4. **Add Tests**: Unit tests for MbtilesService, integration tests for rendering -5. **Performance Monitoring**: Track render times and memory usage - -## Quick Start for Testing - -1. **Download Test File**: -```bash -wget https://geodata.maptiler.download/extracts/osm/v3.11/2020-02-10/europe/osm-2020-02-10-v3.11_europe_slovenia.mbtiles -``` - -2. **Run App**: -```bash -flutter run -``` - -3. **Import File**: -- Settings → Map Management -- Tap "Import MBTiles File" -- Select downloaded file - -4. **Switch Layer**: -- Map tab → Layers button -- Select "Slovenia" (or imported name) -- Wait for style download - -5. **Verify**: -- Check map renders vector tiles -- Test zooming (over-zoom should work) -- Test panning -- Toggle airplane mode (should still work) - -## Support - -For issues related to: -- **Package compatibility**: Check flutter_map_plugins repository -- **MBTiles format**: See MBTiles specification -- **Vector styles**: Check versatiles.org documentation -- **App-specific issues**: See CLAUDE.md and VECTOR_MAPS.md - -## References - -- [flutter_map v8 migration guide](https://docs.fleaflet.dev/) -- [vector_map_tiles documentation](https://pub.dev/packages/vector_map_tiles) -- [mbtiles package](https://pub.dev/packages/mbtiles) -- [MBTiles spec](https://github.com/mapbox/mbtiles-spec) -- [Shortbread schema](https://shortbread-tiles.org/) diff --git a/MESHCORE_BLE_PROTOCOL.md b/MESHCORE_BLE_PROTOCOL.md deleted file mode 100644 index 65455f2..0000000 --- a/MESHCORE_BLE_PROTOCOL.md +++ /dev/null @@ -1,1224 +0,0 @@ -# MeshCore BLE Protocol Specification - -Complete BLE command/response protocol extracted from [meshcore.js Connection class](https://github.com/meshcore-dev/meshcore.js). - -## Overview - -The MeshCore BLE protocol provides a command/response interface for smartphone applications to interact with MeshCore devices over Bluetooth Low Energy. This is a **separate protocol** from the mesh packet protocol used for LoRa radio communication. - -**Protocol Characteristics:** -- Uses Nordic UART Service (NUS) profile -- Simple command/response model -- App acts as BLE central, device acts as peripheral -- Commands sent over RX characteristic -- Responses/events received over TX characteristic -- Supports asynchronous push notifications - ---- - -## Service Specification - -**Service UUID:** `6E400001-B5A3-F393-E0A9-E50E24DCCA9E` - -| Characteristic | UUID | Properties | Direction | Description | -|----------------|------|------------|-----------|-------------| -| RX | `6E400002-B5A3-F393-E0A9-E50E24DCCA9E` | Write, Write Without Response | App → Device | Commands | -| TX | `6E400003-B5A3-F393-E0A9-E50E24DCCA9E` | Notify | Device → App | Responses & Events | - ---- - -## Protocol Structure - -### Command Frame Format -``` -[Command Code: 1B] [Parameters...] -``` - -### Response Frame Format -``` -[Response Code: 1B] [Data...] -``` - -### Push Notification Format -``` -[Push Code: 1B] [Data...] -``` - ---- - -## Command Codes - -Commands sent from app to device over RX characteristic: - -| Code | Name | Description | -|------|------|-------------| -| - | `AppStart` | Initialize connection, get device info | -| - | `SendTxtMsg` | Send text message to contact | -| - | `SendChannelTxtMsg` | Send text message to channel | -| - | `GetContacts` | Request list of contacts | -| - | `GetDeviceTime` | Get device's current time | -| - | `SetDeviceTime` | Set device's current time | -| - | `SendSelfAdvert` | Broadcast advertisement | -| - | `SetAdvertName` | Set device's advertised name | -| - | `AddUpdateContact` | Add or update contact details | -| - | `SyncNextMessage` | Retrieve next queued message | -| - | `SetRadioParams` | Configure LoRa radio parameters | -| - | `SetTxPower` | Set transmit power | -| - | `ResetPath` | Reset routing path for contact | -| - | `SetAdvertLatLon` | Set device's GPS coordinates | -| - | `RemoveContact` | Delete contact from device | -| - | `ShareContact` | Send contact to mesh network | -| - | `ExportContact` | Export contact as packet bytes | -| - | `ImportContact` | Import contact from packet bytes | -| - | `Reboot` | Reboot device | -| - | `GetBatteryVoltage` | Read battery voltage | -| - | `DeviceQuery` | Query device firmware info | -| - | `ExportPrivateKey` | Export device's private key | -| - | `ImportPrivateKey` | Import private key to device | -| - | `SendRawData` | Send raw mesh packet | -| - | `SendLogin` | Login to repeater/room | -| - | `SendStatusReq` | Request repeater status | -| - | `SendTelemetryReq` | Request telemetry from contact | -| - | `SendBinaryReq` | Send binary request | -| - | `GetChannel` | Get channel configuration | -| - | `SetChannel` | Set channel configuration | -| - | `SignStart` | Start signing data | -| - | `SignData` | Send data chunk to sign | -| - | `SignFinish` | Finish signing, get signature | -| - | `SendTracePath` | Trace network path | -| - | `SetOtherParams` | Set miscellaneous parameters | - ---- - -## Response Codes - -Responses sent from device to app over TX characteristic: - -| Code | Name | Description | -|------|------|-------------| -| - | `Ok` | Command succeeded | -| - | `Err` | Command failed | -| - | `SelfInfo` | Device information | -| - | `CurrTime` | Current device time | -| - | `NoMoreMessages` | Message queue empty | -| - | `ContactMsgRecv` | Contact message received | -| - | `ChannelMsgRecv` | Channel message received | -| - | `ContactsStart` | Start of contacts list | -| - | `Contact` | Contact entry | -| - | `EndOfContacts` | End of contacts list | -| - | `Sent` | Message sent to mesh | -| - | `ExportContact` | Exported contact data | -| - | `BatteryVoltage` | Battery voltage reading | -| - | `DeviceInfo` | Firmware version info | -| - | `PrivateKey` | Exported private key | -| - | `Disabled` | Feature disabled | -| - | `ChannelInfo` | Channel configuration | -| - | `SignStart` | Signing session started | -| - | `Signature` | Ed25519 signature | - ---- - -## Push Codes - -Asynchronous events pushed from device to app: - -| Code | Name | Description | -|------|------|-------------| -| - | `Advert` | Advertisement broadcast | -| - | `PathUpdated` | Routing path updated | -| - | `SendConfirmed` | Message ACK received | -| - | `MsgWaiting` | Messages queued | -| - | `RawData` | Raw packet received | -| - | `LoginSuccess` | Login succeeded | -| - | `StatusResponse` | Status data received | -| - | `LogRxData` | Raw RX data (debug) | -| - | `TelemetryResponse` | Telemetry data received | -| - | `TraceData` | Path trace completed | -| - | `NewAdvert` | New contact advertised | -| - | `BinaryResponse` | Binary request response | - ---- - -## Command Details - -### AppStart -Initialize BLE connection and retrieve device information. - -**Format:** -``` -[AppStart] [1B: appVer] [6B: reserved] [string: appName] -``` - -**Parameters:** -- `appVer` (uint8): App protocol version (e.g., 1) -- `reserved` (6 bytes): Reserved for future use -- `appName` (string): Application name (null-terminated) - -**Response:** `SelfInfo` - -**Example:** -```javascript -await connection.sendCommandAppStart(); -// Sends: [CMD][0x01][00 00 00 00 00 00]["test\0"] -``` - ---- - -### SendTxtMsg -Send a text message to a contact. - -**Format:** -``` -[SendTxtMsg] [1B: txtType] [1B: attempt] [4B: timestamp] [6B: pubKeyPrefix] [string: text] -``` - -**Parameters:** -- `txtType` (uint8): Message type (0=Plain, 1=Emergency, etc.) -- `attempt` (uint8): Retry attempt number (usually 0) -- `timestamp` (uint32 LE): Unix timestamp -- `pubKeyPrefix` (6 bytes): First 6 bytes of recipient's public key -- `text` (string): UTF-8 message text (null-terminated) - -**Response:** `Sent` - -**Example:** -```javascript -const txtType = 0; // Plain -const attempt = 0; -const timestamp = Math.floor(Date.now() / 1000); -await connection.sendCommandSendTxtMsg(txtType, attempt, timestamp, contactPublicKey, "Hello!"); -``` - ---- - -### SendChannelTxtMsg -Send a text message to a group channel. - -**Format:** -``` -[SendChannelTxtMsg] [1B: txtType] [1B: channelIdx] [4B: timestamp] [string: text] -``` - -**Parameters:** -- `txtType` (uint8): Message type -- `channelIdx` (uint8): Channel index (0-255) -- `timestamp` (uint32 LE): Unix timestamp -- `text` (string): UTF-8 message text - -**Response:** `Ok` or `Err` - ---- - -### GetContacts -Request list of all contacts from device. - -**Format:** -``` -[GetContacts] [4B: since]? -``` - -**Parameters:** -- `since` (uint32 LE, optional): Only return contacts modified after this timestamp - -**Response Sequence:** -1. `ContactsStart` (with count) -2. Multiple `Contact` responses -3. `EndOfContacts` - -**Example:** -```javascript -const contacts = await connection.getContacts(); -// Returns array of contact objects -``` - ---- - -### GetDeviceTime -Get current time from device. - -**Format:** -``` -[GetDeviceTime] -``` - -**Response:** `CurrTime` - -**Response Format:** -``` -[CurrTime] [4B: epochSecs] -``` - ---- - -### SetDeviceTime -Set device's current time. - -**Format:** -``` -[SetDeviceTime] [4B: epochSecs] -``` - -**Parameters:** -- `epochSecs` (uint32 LE): Unix timestamp - -**Response:** `Ok` or `Err` - -**Example:** -```javascript -await connection.syncDeviceTime(); // Sets to current system time -``` - ---- - -### SendSelfAdvert -Broadcast advertisement to mesh network. - -**Format:** -``` -[SendSelfAdvert] [1B: type] -``` - -**Parameters:** -- `type` (uint8): Advertisement type - - `Flood`: Broadcast to entire network - - `ZeroHop`: Only adjacent nodes - -**Response:** `Ok` or `Err` - -**Push:** `Advert` (when broadcast begins) - -**Example:** -```javascript -await connection.sendFloodAdvert(); // Network-wide -await connection.sendZeroHopAdvert(); // Adjacent only -``` - ---- - -### SetAdvertName -Set device's advertised name. - -**Format:** -``` -[SetAdvertName] [string: name] -``` - -**Parameters:** -- `name` (string): Display name (null-terminated, max 31 chars) - -**Response:** `Ok` or `Err` - -**Example:** -```javascript -await connection.setAdvertName("Alice"); -``` - ---- - -### AddUpdateContact -Add new contact or update existing contact. - -**Format:** -``` -[AddUpdateContact] [32B: publicKey] [1B: type] [1B: flags] [1B: outPathLen] - [64B: outPath] [32B: advName] [4B: lastAdvert] - [4B: advLat] [4B: advLon] -``` - -**Parameters:** -- `publicKey` (32 bytes): Ed25519 public key -- `type` (uint8): Contact type (0=none, 1=chat, 2=repeater, 3=room) -- `flags` (uint8): Contact flags -- `outPathLen` (int8): Length of routing path -- `outPath` (64 bytes): Routing path (padded) -- `advName` (32 bytes): Name as C-string -- `lastAdvert` (uint32 LE): Last advertisement timestamp -- `advLat` (uint32 LE): GPS latitude (×10000) -- `advLon` (uint32 LE): GPS longitude (×10000) - -**Response:** `Ok` or `Err` - ---- - -### SyncNextMessage -Retrieve next message from device's queue. - -**Format:** -``` -[SyncNextMessage] -``` - -**Response:** One of: -- `ContactMsgRecv` - Message from contact -- `ChannelMsgRecv` - Message from channel -- `NoMoreMessages` - Queue empty - -**Example:** -```javascript -while (true) { - const msg = await connection.syncNextMessage(); - if (!msg) break; // No more messages - console.log(msg); -} -``` - ---- - -### SetRadioParams -Configure LoRa radio parameters. - -**Format:** -``` -[SetRadioParams] [4B: radioFreq] [4B: radioBw] [1B: radioSf] [1B: radioCr] -``` - -**Parameters:** -- `radioFreq` (uint32 LE): Frequency in Hz (e.g., 915000000) -- `radioBw` (uint32 LE): Bandwidth in Hz (e.g., 125000) -- `radioSf` (uint8): Spreading factor (7-12) -- `radioCr` (uint8): Coding rate (5-8) - -**Response:** `Ok` or `Err` - -**Example:** -```javascript -await connection.setRadioParams( - 915000000, // 915 MHz - 125000, // 125 kHz bandwidth - 7, // SF7 - 5 // CR 4/5 -); -``` - ---- - -### SetTxPower -Set transmit power level. - -**Format:** -``` -[SetTxPower] [1B: txPower] -``` - -**Parameters:** -- `txPower` (uint8): Power level in dBm (device-specific range) - -**Response:** `Ok` or `Err` - ---- - -### ResetPath -Clear routing path for a contact (force rediscovery). - -**Format:** -``` -[ResetPath] [32B: pubKey] -``` - -**Parameters:** -- `pubKey` (32 bytes): Contact's public key - -**Response:** `Ok` or `Err` - ---- - -### SetAdvertLatLon -Set device's GPS coordinates for advertisements. - -**Format:** -``` -[SetAdvertLatLon] [4B: lat] [4B: lon] -``` - -**Parameters:** -- `lat` (int32 LE): Latitude × 10000 -- `lon` (int32 LE): Longitude × 10000 - -**Response:** `Ok` or `Err` - -**Example:** -```javascript -// Set location to 46.0569°N, 14.5058°E -await connection.setAdvertLatLong(460569, 145058); -``` - ---- - -### RemoveContact -Delete contact from device. - -**Format:** -``` -[RemoveContact] [32B: pubKey] -``` - -**Parameters:** -- `pubKey` (32 bytes): Contact's public key - -**Response:** `Ok` or `Err` - ---- - -### ShareContact -Broadcast contact to mesh network. - -**Format:** -``` -[ShareContact] [32B: pubKey] -``` - -**Parameters:** -- `pubKey` (32 bytes): Contact to share - -**Response:** `Ok` or `Err` - ---- - -### ExportContact -Export contact as advertisement packet bytes. - -**Format:** -``` -[ExportContact] [32B: pubKey]? -``` - -**Parameters:** -- `pubKey` (32 bytes, optional): Contact to export, or omit for self - -**Response:** `ExportContact` - -**Response Format:** -``` -[ExportContact] [N bytes: advertPacketBytes] -``` - ---- - -### ImportContact -Import contact from advertisement packet bytes. - -**Format:** -``` -[ImportContact] [N bytes: advertPacketBytes] -``` - -**Parameters:** -- `advertPacketBytes`: Complete advertisement packet - -**Response:** `Ok` or `Err` - ---- - -### Reboot -Reboot the device. - -**Format:** -``` -[Reboot] [string: "reboot"] -``` - -**Response:** None (device reboots) - -**Example:** -```javascript -await connection.reboot(); -// Device will disconnect and reboot -``` - ---- - -### GetBatteryVoltage -Read device's battery voltage. - -**Format:** -``` -[GetBatteryVoltage] -``` - -**Response:** `BatteryVoltage` - -**Response Format:** -``` -[BatteryVoltage] [2B: batteryMilliVolts] -``` - -**Example:** -```javascript -const { batteryMilliVolts } = await connection.getBatteryVoltage(); -console.log(`Battery: ${batteryMilliVolts / 1000}V`); -``` - ---- - -### DeviceQuery -Query device firmware information. - -**Format:** -``` -[DeviceQuery] [1B: appTargetVer] -``` - -**Parameters:** -- `appTargetVer` (uint8): Protocol version app expects (e.g., 1) - -**Response:** `DeviceInfo` - -**Response Format:** -``` -[DeviceInfo] [1B: firmwareVer] [6B: reserved] [12B: buildDate] [string: model] -``` - -**Example:** -```javascript -const info = await connection.deviceQuery(1); -console.log(`Firmware v${info.firmwareVer}, ${info.manufacturerModel}`); -``` - ---- - -### ExportPrivateKey -Export device's Ed25519 private key. - -**Format:** -``` -[ExportPrivateKey] -``` - -**Response:** `PrivateKey` or `Disabled` - -**Response Format (PrivateKey):** -``` -[PrivateKey] [64B: privateKey] -``` - -**Security Note:** May be disabled in firmware for security. - ---- - -### ImportPrivateKey -Import Ed25519 private key to device. - -**Format:** -``` -[ImportPrivateKey] [64B: privateKey] -``` - -**Parameters:** -- `privateKey` (64 bytes): Ed25519 private key - -**Response:** `Ok`, `Err`, or `Disabled` - -**Security Note:** May be disabled in firmware. - ---- - -### SendRawData -Send raw custom data through mesh. - -**Format:** -``` -[SendRawData] [1B: pathLen] [N bytes: path] [M bytes: rawData] -``` - -**Parameters:** -- `pathLen` (uint8): Length of routing path -- `path` (N bytes): Routing path -- `rawData` (M bytes): Custom payload - -**Response:** `Ok` or `Err` - ---- - -### SendLogin -Authenticate with repeater or room server. - -**Format:** -``` -[SendLogin] [32B: publicKey] [string: password] -``` - -**Parameters:** -- `publicKey` (32 bytes): Server's public key -- `password` (string): Login password (max 15 chars) - -**Response:** `Sent` - -**Push:** `LoginSuccess` (when authenticated) - -**Example:** -```javascript -await connection.login(repeaterPublicKey, "mypassword"); -// Waits for LoginSuccess push -``` - ---- - -### SendStatusReq -Request status information from repeater. - -**Format:** -``` -[SendStatusReq] [32B: publicKey] -``` - -**Parameters:** -- `publicKey` (32 bytes): Repeater's public key - -**Response:** `Sent` - -**Push:** `StatusResponse` (with repeater stats) - -**Status Response Format:** -```javascript -{ - batt_milli_volts: uint16, // Battery voltage (mV) - curr_tx_queue_len: uint16, // Transmit queue length - noise_floor: int16, // Noise floor (dBm) - last_rssi: int16, // Last RSSI (dBm) - n_packets_recv: uint32, // Total packets received - n_packets_sent: uint32, // Total packets sent - total_air_time_secs: uint32, // Total air time (seconds) - total_up_time_secs: uint32, // Uptime (seconds) - n_sent_flood: uint32, // Flood packets sent - n_sent_direct: uint32, // Direct packets sent - n_recv_flood: uint32, // Flood packets received - n_recv_direct: uint32, // Direct packets received - err_events: uint16, // Error events - last_snr: int16, // Last SNR (×4) - n_direct_dups: uint16, // Duplicate direct packets - n_flood_dups: uint16, // Duplicate flood packets -} -``` - ---- - -### SendTelemetryReq -Request telemetry data from contact. - -**Format:** -``` -[SendTelemetryReq] [3B: reserved] [32B: publicKey] -``` - -**Parameters:** -- `reserved` (3 bytes): Reserved (set to 0) -- `publicKey` (32 bytes): Contact's public key - -**Response:** `Sent` - -**Push:** `TelemetryResponse` (with Cayenne LPP data) - -**Example:** -```javascript -const telemetry = await connection.getTelemetry(contactPublicKey); -console.log(telemetry.lppSensorData); // Parse with Cayenne LPP parser -``` - ---- - -### SendBinaryReq -Send custom binary request to contact. - -**Format:** -``` -[SendBinaryReq] [32B: publicKey] [N bytes: requestCodeAndParams] -``` - -**Parameters:** -- `publicKey` (32 bytes): Target contact -- `requestCodeAndParams` (variable): Application-specific request - -**Response:** `Sent` - -**Push:** `BinaryResponse` (with tag and response data) - -**Binary Request Types:** -- `GetNeighbours` (0x00): Query repeater for neighbor list - ---- - -### GetChannel -Get channel configuration by index. - -**Format:** -``` -[GetChannel] [1B: channelIdx] -``` - -**Parameters:** -- `channelIdx` (uint8): Channel index (0-N) - -**Response:** `ChannelInfo` or `Err` - -**Response Format:** -``` -[ChannelInfo] [1B: idx] [32B: name] [16B: secret] -``` - -**Example:** -```javascript -const channel = await connection.getChannel(0); -console.log(`Channel: ${channel.name}`); -``` - ---- - -### SetChannel -Set channel configuration. - -**Format:** -``` -[SetChannel] [1B: channelIdx] [32B: name] [16B: secret] -``` - -**Parameters:** -- `channelIdx` (uint8): Channel index -- `name` (32 bytes): Channel name as C-string -- `secret` (16 bytes): AES-128 shared key - -**Response:** `Ok` or `Err` - -**Example:** -```javascript -// Delete channel -await connection.deleteChannel(0); -// Internally: setChannel(0, "", new Uint8Array(16)) -``` - ---- - -### SignStart -Start signing session. - -**Format:** -``` -[SignStart] -``` - -**Response:** `SignStart` - -**Response Format:** -``` -[SignStart] [1B: reserved] [4B: maxSignDataLen] -``` - ---- - -### SignData -Send data chunk to sign. - -**Format:** -``` -[SignData] [N bytes: dataToSign] -``` - -**Parameters:** -- `dataToSign`: Data chunk (max 128 bytes) - -**Response:** `Ok` (send next chunk) - ---- - -### SignFinish -Finish signing and retrieve signature. - -**Format:** -``` -[SignFinish] -``` - -**Response:** `Signature` - -**Response Format:** -``` -[Signature] [64B: signature] -``` - -**Example:** -```javascript -const signature = await connection.sign(data); -// Automatically handles chunking -``` - ---- - -### SendTracePath -Trace path through mesh network. - -**Format:** -``` -[SendTracePath] [4B: tag] [4B: auth] [1B: flags] [N bytes: path] -``` - -**Parameters:** -- `tag` (uint32 LE): Random tag for matching response -- `auth` (uint32 LE): Authentication code (usually 0) -- `flags` (uint8): Trace flags -- `path` (variable): Routing path to trace - -**Response:** `Sent` - -**Push:** `TraceData` - -**Trace Data Format:** -```javascript -{ - reserved: uint8, - pathLen: uint8, - flags: uint8, - tag: uint32, - authCode: uint32, - pathHashes: Uint8Array, // Node IDs - pathSnrs: Uint8Array, // SNR at each hop - lastSnr: float, // Final SNR (÷4) -} -``` - -**Example:** -```javascript -const trace = await connection.tracePath(path); -console.log(`Path length: ${trace.pathLen}`); -console.log(`SNRs: ${trace.pathSnrs}`); -``` - ---- - -### SetOtherParams -Set miscellaneous device parameters. - -**Format:** -``` -[SetOtherParams] [1B: manualAddContacts] -``` - -**Parameters:** -- `manualAddContacts` (uint8): 0=auto-add, 1=manual-add - -**Response:** `Ok` or `Err` - -**Example:** -```javascript -await connection.setAutoAddContacts(); // Auto-add from advertisements -await connection.setManualAddContacts(); // Require manual addition -``` - ---- - -## Binary Request Types - -Sent via `SendBinaryReq` command: - -### GetNeighbours (0x00) -Query repeater for neighbor list. - -**Request Format:** -``` -[0x00] [1B: version] [1B: count] [2B: offset] [1B: orderBy] [1B: prefixLen] [4B: random] -``` - -**Parameters:** -- `version` (uint8): Request version (0) -- `count` (uint8): Max neighbors to return -- `offset` (uint16 LE): Pagination offset -- `orderBy` (uint8): Sort order - - 0: Newest to oldest - - 1: Oldest to newest - - 2: Strongest to weakest (SNR) - - 3: Weakest to strongest -- `prefixLen` (uint8): Public key prefix length (1-32) -- `random` (uint32 LE): Random blob for hash uniqueness - -**Response Format:** -``` -[2B: totalCount] [2B: resultsCount] [repeated: neighbor entries] -``` - -**Neighbor Entry:** -``` -[N bytes: pubKeyPrefix] [4B: heardSecondsAgo] [1B: snr] -``` - -**Example:** -```javascript -const result = await connection.getNeighbours( - repeaterPublicKey, - 10, // count - 0, // offset - 2, // order by strongest - 8 // 8-byte prefix -); -console.log(`Total neighbors: ${result.totalNeighboursCount}`); -result.neighbours.forEach(n => { - console.log(` ${n.publicKeyPrefix.toString('hex')} - SNR: ${n.snr} dB`); -}); -``` - ---- - -## Response Details - -### SelfInfo -Device information response. - -**Format:** -``` -[SelfInfo] [1B: type] [1B: txPower] [1B: maxTxPower] [32B: publicKey] - [4B: advLat] [4B: advLon] [3B: reserved] [1B: manualAddContacts] - [4B: radioFreq] [4B: radioBw] [1B: radioSf] [1B: radioCr] [string: name] -``` - -**Fields:** -```javascript -{ - type: uint8, // Device type - txPower: uint8, // Current TX power (dBm) - maxTxPower: uint8, // Maximum TX power - publicKey: Uint8Array, // 32-byte public key - advLat: int32, // GPS latitude (×10000) - advLon: int32, // GPS longitude (×10000) - reserved: Uint8Array, // 3 bytes reserved - manualAddContacts: uint8, // 0=auto, 1=manual - radioFreq: uint32, // Frequency (Hz) - radioBw: uint32, // Bandwidth (Hz) - radioSf: uint8, // Spreading factor - radioCr: uint8, // Coding rate - name: string, // Device name -} -``` - ---- - -### Contact -Contact entry response. - -**Format:** -``` -[Contact] [32B: publicKey] [1B: type] [1B: flags] [1B: outPathLen] [64B: outPath] - [32B: advName] [4B: lastAdvert] [4B: advLat] [4B: advLon] [4B: lastMod] -``` - -**Fields:** -```javascript -{ - publicKey: Uint8Array, // 32-byte public key - type: uint8, // 0=none, 1=chat, 2=repeater, 3=room - flags: uint8, // Contact flags - outPathLen: int8, // Path length - outPath: Uint8Array, // 64-byte path (padded) - advName: string, // Name (32-byte C-string) - lastAdvert: uint32, // Last advertisement time - advLat: uint32, // GPS latitude (×10000) - advLon: uint32, // GPS longitude (×10000) - lastMod: uint32, // Last modification time -} -``` - ---- - -### ContactMsgRecv -Contact message received. - -**Format:** -``` -[ContactMsgRecv] [6B: pubKeyPrefix] [1B: pathLen] [1B: txtType] [4B: senderTimestamp] [string: text] -``` - -**Fields:** -```javascript -{ - pubKeyPrefix: Uint8Array, // 6-byte sender public key prefix - pathLen: uint8, // Hop count (0xFF=direct) - txtType: uint8, // Message type - senderTimestamp: uint32, // Sender's timestamp - text: string, // Message text -} -``` - ---- - -### ChannelMsgRecv -Channel message received. - -**Format:** -``` -[ChannelMsgRecv] [1B: channelIdx] [1B: pathLen] [1B: txtType] [4B: senderTimestamp] [string: text] -``` - -**Fields:** -```javascript -{ - channelIdx: int8, // Channel index (0=public) - pathLen: uint8, // Hop count (0xFF=direct) - txtType: uint8, // Message type - senderTimestamp: uint32, // Sender's timestamp - text: string, // Message text -} -``` - ---- - -### Sent -Message sent to mesh network. - -**Format:** -``` -[Sent] [1B: result] [4B: expectedAckCrc] [4B: estTimeout] -``` - -**Fields:** -```javascript -{ - result: int8, // Send result code - expectedAckCrc: uint32, // CRC for ACK matching - estTimeout: uint32, // Estimated timeout (ms) -} -``` - ---- - -## Push Notifications - -### PathUpdated -Routing path updated for contact. - -**Format:** -``` -[PathUpdated] [32B: publicKey] -``` - ---- - -### SendConfirmed -Message ACK received from network. - -**Format:** -``` -[SendConfirmed] [4B: ackCode] [4B: roundTrip] -``` - -**Fields:** -```javascript -{ - ackCode: uint32, // ACK code (matches expectedAckCrc) - roundTrip: uint32, // Round-trip time (ms) -} -``` - ---- - -### MsgWaiting -Messages queued on device. - -**Format:** -``` -[MsgWaiting] -``` - -**Action:** Call `SyncNextMessage` to retrieve. - ---- - -### NewAdvert -New contact advertised on network. - -**Format:** -``` -[NewAdvert] [32B: publicKey] [1B: type] [1B: flags] [1B: outPathLen] [64B: outPath] - [32B: advName] [4B: lastAdvert] [4B: advLat] [4B: advLon] [4B: lastMod] -``` - -(Same structure as `Contact` response) - ---- - -## Error Codes - -**Err Response Format:** -``` -[Err] [1B: errCode]? -``` - -Error codes are application-specific. Check firmware documentation for specific codes. - ---- - -## Usage Patterns - -### Initialize Connection -```javascript -// Called automatically on connect -await connection.onConnected(); -// Sends: AppStart with protocol version -``` - -### Send Message -```javascript -const contact = await connection.findContactByName("Alice"); -await connection.sendTextMessage(contact.publicKey, "Hello!"); -``` - -### Sync Messages -```javascript -const messages = await connection.getWaitingMessages(); -messages.forEach(msg => { - if (msg.contactMessage) { - console.log(`From contact: ${msg.contactMessage.text}`); - } else if (msg.channelMessage) { - console.log(`From channel: ${msg.channelMessage.text}`); - } -}); -``` - -### Monitor Events -```javascript -connection.on('NewAdvert', (data) => { - console.log(`New contact: ${data.advName}`); -}); - -connection.on('SendConfirmed', (data) => { - console.log(`Message confirmed in ${data.roundTrip}ms`); -}); -``` - ---- - -## Implementation Notes - -### Timeouts -Most commands have default timeouts. For requests expecting mesh responses: -- Use `estTimeout` from `Sent` response -- Add extra buffer time (e.g., +1000ms) -- Implement exponential backoff for retries - -### Buffering -BLE packets may arrive fragmented: -- Buffer incoming data until complete frame received -- Use frame length headers when available -- Implement packet boundary detection - -### Thread Safety -Connection is event-driven: -- Use promises for request/response pattern -- Remove event listeners after use -- Handle concurrent requests carefully - -### Security -- Verify signatures on received advertisements -- Validate public keys before import -- Sanitize user input (names, messages) -- Rate limit commands to prevent DoS - ---- - -## References - -- [meshcore.js Connection class](https://github.com/meshcore-dev/meshcore.js/blob/main/src/connection/connection.js) -- [MeshCore Firmware](https://github.com/meshcore-dev/MeshCore) -- [MESHCORE_PROTOCOL.md](MESHCORE_PROTOCOL.md) - Mesh packet protocol -- [MESHCORE_QUICK_REFERENCE.md](MESHCORE_QUICK_REFERENCE.md) - Quick reference card - ---- - -**Document Version:** 1.0 -**Last Updated:** 2025-10-14 -**Compatible with:** MeshCore firmware v1.9.0+, meshcore.js v1.x diff --git a/MESHCORE_PACKET_RESEARCH.md b/MESHCORE_PACKET_RESEARCH.md deleted file mode 100644 index 8fa79df..0000000 --- a/MESHCORE_PACKET_RESEARCH.md +++ /dev/null @@ -1,578 +0,0 @@ -# MeshCore Public Channel Message Structure & Detection Research - -## 1. Public Channel Message Creation Flow - -### 1.1 Message Generation (BaseChatMesh::sendGroupMessage) - -File: `/Users/dz0ny/meshcore-sar/MeshCore/src/helpers/BaseChatMesh.cpp` (lines 379-398) - -```cpp -bool BaseChatMesh::sendGroupMessage(uint32_t timestamp, - mesh::GroupChannel& channel, - const char* sender_name, - const char* text, - int text_len) { - uint8_t temp[5+MAX_TEXT_LEN+32]; - - // Step 1: Add timestamp (4 bytes, little-endian) - memcpy(temp, ×tamp, 4); - - // Step 2: Add txt_type flag (1 byte) - 0 = TXT_TYPE_PLAIN - temp[4] = 0; - - // Step 3: Format message as "sender_name: message_text" - sprintf((char *)&temp[5], "%s: ", sender_name); - char *ep = strchr((char *)&temp[5], 0); - int prefix_len = ep - (char *)&temp[5]; - - if (text_len + prefix_len > MAX_TEXT_LEN) - text_len = MAX_TEXT_LEN - prefix_len; - memcpy(ep, text, text_len); - ep[text_len] = 0; - - // Step 4: Create encrypted packet - auto pkt = createGroupDatagram(PAYLOAD_TYPE_GRP_TXT, channel, temp, 5 + prefix_len + text_len); - if (pkt) { - sendFlood(pkt); - return true; - } - return false; -} -``` - -**Key Points:** -- Unencrypted data format: `[4-byte timestamp][1-byte txt_type][variable "name: text"]` -- txt_type = 0x00 for plain text -- Message includes sender name in plaintext -- No message ID or checksum in plaintext data - -### 1.2 Packet Encryption (Mesh::createGroupDatagram) - -File: `/Users/dz0ny/meshcore-sar/MeshCore/src/Mesh.cpp` (lines 509-527) - -```cpp -Packet* Mesh::createGroupDatagram(uint8_t type, const GroupChannel& channel, - const uint8_t* data, size_t data_len) { - if (!(type == PAYLOAD_TYPE_GRP_TXT || type == PAYLOAD_TYPE_GRP_DATA)) - return NULL; - if (data_len + 1 + CIPHER_BLOCK_SIZE-1 > MAX_PACKET_PAYLOAD) - return NULL; - - Packet* packet = obtainNewPacket(); - if (packet == NULL) return NULL; - - packet->header = (type << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later - - int len = 0; - // Step 1: Add channel hash (1 byte) - memcpy(&packet->payload[len], channel.hash, PATH_HASH_SIZE); - len += PATH_HASH_SIZE; - - // Step 2: Encrypt plaintext data + add MAC - len += Utils::encryptThenMAC(channel.secret, &packet->payload[len], - data, data_len); - - packet->payload_len = len; - return packet; -} -``` - -**Key Points:** -- Payload structure: `[1-byte channel_hash][2-byte MAC][16+ bytes encrypted data]` -- PATH_HASH_SIZE = 1 byte -- CIPHER_MAC_SIZE = 2 bytes (V1 protocol) -- CIPHER_BLOCK_SIZE = 16 bytes (AES128) -- Uses AES128-ECB encryption with HMAC-SHA256 truncated to 2 bytes - -### 1.3 Encryption Algorithm (Utils::encryptThenMAC) - -File: `/Users/dz0ny/meshcore-sar/MeshCore/src/Utils.cpp` (lines 63-72) - -```cpp -int Utils::encryptThenMAC(const uint8_t* shared_secret, uint8_t* dest, - const uint8_t* src, int src_len) { - // Step 1: Encrypt plaintext - int enc_len = encrypt(shared_secret, dest + CIPHER_MAC_SIZE, src, src_len); - - // Step 2: Calculate HMAC-SHA256 over ciphertext - SHA256 sha; - sha.resetHMAC(shared_secret, PUB_KEY_SIZE); - sha.update(dest + CIPHER_MAC_SIZE, enc_len); - sha.finalizeHMAC(shared_secret, PUB_KEY_SIZE, dest, CIPHER_MAC_SIZE); - - return CIPHER_MAC_SIZE + enc_len; -} -``` - -**Encryption Details:** -- Plaintext padded with zero bytes to 16-byte block boundary -- AES128 in ECB mode (Electronic Code Book) -- HMAC-SHA256 truncated to 2 bytes -- Order: HMAC-SHA256(SHA256_HMAC(shared_secret, ciphertext)) -> 2 bytes -- Shared secret = channel.secret (pre-shared key for the channel) - -### 1.4 Complete Wire Format for Group Message - -``` -[1 byte] = packet header (type=0x05 PAYLOAD_TYPE_GRP_TXT, route type) -[1 byte] = channel_hash (identifies which channel) -[2 bytes] = MAC (HMAC-SHA256 truncated to 2 bytes) -[16+ bytes] = AES128 encrypted data: - [4 bytes] = timestamp (little-endian) - [1 byte] = txt_type (0x00 for plain) - [variable] = "sender_name: message_text" - [0-15 bytes] = zero padding to reach 16-byte boundary -``` - -**Example for "Alice: Hello":** -``` -Plaintext (13 bytes before padding): - 00 01 02 03 <- timestamp (example) - 00 <- txt_type = 0 - 41 6C 69 63 65 3A 20 48 65 6C 6C 6F <- "Alice: Hello" - -After padding to 16 bytes: - 00 01 02 03 00 41 6C 69 63 65 3A 20 48 65 6C 6C 6F - -After AES128 encryption (16 bytes): - [16 random-looking bytes] - -Final packet: - [header] [channel_hash] [2-byte MAC] [16-byte ciphertext] -``` - ---- - -## 2. Packet Reception & Decryption Flow - -### 2.1 Receiving Group Messages (Mesh::onRecvPacket) - -File: `/Users/dz0ny/meshcore-sar/MeshCore/src/Mesh.cpp` (lines 196-220) - -```cpp -case PAYLOAD_TYPE_GRP_TXT: { - int i = 0; - uint8_t channel_hash = pkt->payload[i++]; // Extract 1-byte hash - - uint8_t* macAndData = &pkt->payload[i]; // Points to MAC + encrypted data - - if (i + 2 >= pkt->payload_len) { - // incomplete data - } else if (!_tables->hasSeen(pkt)) { // Check if we've already processed this - // Search for all matching channels - GroupChannel channels[2]; - int num = searchChannelsByHash(&channel_hash, channels, 2); - - // Try to decrypt with each matching channel - for (int j = 0; j < num; j++) { - uint8_t data[MAX_PACKET_PAYLOAD]; - // Verify MAC, then decrypt - int len = Utils::MACThenDecrypt(channels[j].secret, data, - macAndData, pkt->payload_len - i); - if (len > 0) { // MAC verified - success! - onGroupDataRecv(pkt, pkt->getPayloadType(), channels[j], data, len); - break; - } - } - action = routeRecvPacket(pkt); - } - break; -} -``` - -### 2.2 Processing Decrypted Group Data (BaseChatMesh::onGroupDataRecv) - -File: `/Users/dz0ny/meshcore-sar/MeshCore/src/helpers/BaseChatMesh.cpp` (lines 298-310) - -```cpp -void BaseChatMesh::onGroupDataRecv(mesh::Packet* packet, uint8_t type, - const mesh::GroupChannel& channel, - uint8_t* data, size_t len) { - uint8_t txt_type = data[4]; // Extract txt_type from decrypted data - - if (type == PAYLOAD_TYPE_GRP_TXT && len > 5 && (txt_type >> 2) == 0) { - uint32_t timestamp; - memcpy(×tamp, data, 4); // Extract timestamp - - // Null-terminate the message - data[len] = 0; - - // Notify UI - onChannelMessageRecv(channel, packet, timestamp, - (const char *)&data[5]); // Pass message text - } -} -``` - ---- - -## 3. Packet Deduplication & Matching Mechanism - -### 3.1 Packet Hash Calculation - -File: `/Users/dz0ny/meshcore-sar/MeshCore/src/Packet.cpp` (lines 17-26) - -```cpp -void Packet::calculatePacketHash(uint8_t* hash) const { - SHA256 sha; - uint8_t t = getPayloadType(); - sha.update(&t, 1); - - // Special handling for TRACE packets - if (t == PAYLOAD_TYPE_TRACE) { - sha.update(&path_len, sizeof(path_len)); - } - - // Hash includes payload type + entire payload - sha.update(payload, payload_len); - sha.finalize(hash, MAX_HASH_SIZE); // Truncate to 8 bytes -} -``` - -**Hash = SHA256(payload_type || full_payload) -> 8 bytes** - -### 3.2 Duplicate Detection (MeshTables::hasSeen) - -The `hasSeen()` function maintains a table of recently seen packets: - -- When we **send** a packet: `_tables->hasSeen(packet)` marks it as seen -- When we **receive** a packet: check `!_tables->hasSeen(pkt)` to avoid reprocessing -- Prevents duplicate processing via different network paths - -**Implementation in Mesh::sendFlood (line 600):** -```cpp -_tables->hasSeen(packet); // mark this packet as already sent in case - // it is rebroadcast back to us -``` - -**Implementation in Mesh::sendDirect (line 633):** -```cpp -_tables->hasSeen(packet); // mark this packet as already sent in case - // it is rebroadcast back to us -``` - ---- - -## 4. Echo Detection: Can We Match Sent vs Received Packets? - -### 4.1 What Makes a Packet Unique? - -**Encrypted packets (the wire format) are NOT directly matchable:** -- MAC uses HMAC-SHA256 truncated to 2 bytes - collision resistance but NOT deterministic -- Ciphertext appears random due to AES128-ECB -- Each encryption run produces different ciphertext (due to random key derivation?) - -**Wait - actually they ARE the same:** -- AES128-ECB is deterministic: same plaintext + key = same ciphertext -- HMAC-SHA256 is deterministic: same data + key = same MAC -- **Therefore: Same plaintext + same channel secret = identical encrypted packet** - -### 4.2 How to Match Sent vs Received - -``` -Sent packet generation: - 1. User sends: "Alice: Hello World" - 2. Timestamp T is captured - 3. Plaintext: [T || 0x00 || "Alice: Hello World"] - 4. Channel secret S is used - 5. AES128(S, plaintext) -> ciphertext C - 6. MAC = HMAC-SHA256(S, C) -> M - 7. Packet = [channel_hash || M || C] - -If the same packet echoes back: - - Exact same plaintext - - Exact same channel secret - - Exact same AES128 result - - Exact same MAC - - Exact same final packet -``` - -### 4.3 Matching Strategy - -**Option 1: Full Packet Comparison (Strongest)** -``` -Store sent packet payload: - sent_payload = [channel_hash || MAC || ciphertext] - -When receive PAYLOAD_TYPE_GRP_TXT: - if (received_payload == sent_payload) { - // This is OUR message echoed back! - // Someone received and rebroadcast it - } -``` - -**Option 2: Payload Hash Matching** -``` -Calculate hash: - sent_hash = SHA256(PAYLOAD_TYPE_GRP_TXT || full_payload) -> 8 bytes - -The mesh already does this for deduplication! - Packet::calculatePacketHash() is used in MeshTables::hasSeen() - -If packet hash matches = guaranteed same packet -``` - -**Option 3: Plaintext Content Matching (Weakest)** -``` -Store plaintext: - timestamp + "Alice: Hello World" - -When receive decrypted plaintext: - if (timestamp + sender_name + text) matches { - // Likely same message - // But doesn't prove it came from us (collision risk) - } -``` - -### 4.4 Matching Challenges & Solutions - -| Challenge | Issue | Solution | -|-----------|-------|----------| -| **Timestamp uniqueness** | Same timestamp in plaintext | Use `getRTCClock()->getCurrentTimeUnique()` when sending - increases counter if time doesn't advance | -| **Sender name collision** | Multiple "Alice"s in mesh | Combine timestamp + sender name + text content for match | -| **Text content match** | Same text sent by different user | Timestamp makes it unique (getRTCClock()->getCurrentTimeUnique()) | -| **Encrypted packet change** | Doesn't change if plaintext unchanged | AES128-ECB is deterministic - if plaintext same, ciphertext same | -| **MAC truncation** | 2-byte MAC seems short | HMAC-SHA256 with shared secret - same data = same MAC, truncation doesn't affect determinism | - ---- - -## 5. Flutter App - Packet Interception Points - -### 5.1 BLE Response Handler (ble_response_handler.dart) - -File: `/Users/dz0ny/meshcore-sar/meshcore_sar_app/lib/services/ble/ble_response_handler.dart` - -```dart -void _onDataReceived(List data) { - // All RX data comes here - // Packets are parsed and routed to frame_parser - - // Store packet logs for debugging: - final log = BlePacketLog( - timestamp: DateTime.now(), - direction: PacketDirection.incoming, - rawData: Uint8List.fromList(data), - responseCode: responseCode, - decodedInfo: decodedInfo, - ); - _packetLogs.add(log); -} -``` - -**Access point for intercepting raw packets:** -- All RX data (including echoed messages) flows through `_onDataReceived()` -- Raw packet data is stored in `_packetLogs` -- Can extract and compare encrypted payloads here - -### 5.2 Frame Parser Integration - -File: `/Users/dz0ny/meshcore-sar/meshcore_sar_app/lib/services/protocol/frame_parser.dart` - -The frame parser processes: -- PUSH_CODE values -- Response codes -- Extracts message content from decrypted payloads - ---- - -## 6. Implementation Strategy for Echo Detection - -### 6.1 Store Sent Messages - -```dart -// In MessagesProvider or new EchoDetectionService -class SentMessageRecord { - final DateTime sentTime; - final Uint8List encryptedPayload; // [channel_hash || MAC || ciphertext] - final String plaintext; // "Alice: Hello" - final uint32_t timestamp; // From packet - final uint8_t channelHash; - final Uint8List mac; // 2 bytes - final Uint8List ciphertext; // 16+ bytes - - String get key => '${sentTime.millisecondsSinceEpoch}_${plaintext.hashCode}'; -} -``` - -### 6.2 Intercept Sent Packets - -In `meshcore_ble_service.dart`, before sending: - -```dart -// When sendChannelMessage() is called -Future sendChannelMessage(String channelName, String messageText) async { - // Create message record - final record = SentMessageRecord( - sentTime: DateTime.now(), - plaintext: messageText, - // ... other fields - ); - - // Store for echo detection - _sentMessages.add(record); - - // Send via BLE - // The BLE layer will encrypt and generate the final packet - // We need to intercept AFTER encryption -} -``` - -**Better approach: Intercept at frame builder level** - -In `frame_builder.dart`, capture the encrypted payload: - -```dart -Uint8List buildChannelMessage( - String channelName, - String senderName, - String messageText, - Uint8List channelSecret, - Uint8List channelHash, -) { - // Existing build logic... - final encryptedPayload = [ - ...channelHash, - ...mac, - ...ciphertext, - ]; - - // Store for echo detection - _sentPackets.add({ - 'timestamp': sentTime, - 'payload': encryptedPayload, - 'plaintext': messageText, - }); - - return Uint8List.fromList(encryptedPayload); -} -``` - -### 6.3 Detect Echo in Response Handler - -In `ble_response_handler.dart`, when receiving PAYLOAD_TYPE_GRP_TXT: - -```dart -void _handleGroupMessage(Uint8List payload) { - // payload = [channel_hash || MAC || ciphertext] - - // Check if this matches any sent message - for (var sent in _sentPackets) { - if (listEquals(sent['payload'], payload)) { - // ECHO DETECTED! - print('🔄 ECHO: Our message was rebroadcast by other node!'); - _echoDetectionCallbacks.forEach((cb) => cb(sent['plaintext'])); - return; - } - } - - // Not an echo - process normally - _processNewGroupMessage(payload); -} -``` - -### 6.4 Key Insight: Timing - -The echo will arrive **at different times**: -- **Sent**: T=0ms -- **Echo received**: T=100-5000ms (depending on network/hops) -- Time gap confirms it's an echo, not just local reflection - ---- - -## 7. Constants Reference - -From `/Users/dz0ny/meshcore-sar/MeshCore/src/MeshCore.h`: - -```cpp -#define PUB_KEY_SIZE 32 -#define CIPHER_KEY_SIZE 16 -#define CIPHER_BLOCK_SIZE 16 -#define CIPHER_MAC_SIZE 2 // V1 protocol, truncated HMAC-SHA256 -#define PATH_HASH_SIZE 1 // Channel hash size -#define MAX_PACKET_PAYLOAD 184 // Maximum payload in a packet -#define MAX_TEXT_LEN (10*CIPHER_BLOCK_SIZE) // 160 bytes -``` - -Payload type codes: -```cpp -#define PAYLOAD_TYPE_GRP_TXT 0x05 // Group text message -#define PAYLOAD_TYPE_ADVERT 0x04 // Advertisement -#define PAYLOAD_TYPE_TXT_MSG 0x02 // Direct text message -``` - ---- - -## 8. Packet Structure Summary - -### 8.1 Wire Format (Full Packet) - -``` -[1 byte] PACKET HEADER - ├─ [2 bits] Route type (0=FLOOD+TRANSPORT, 1=FLOOD, 2=DIRECT, 3=DIRECT+TRANSPORT) - ├─ [4 bits] Payload type (0x05 for GRP_TXT) - └─ [2 bits] Payload version (0=V1) - -[0-4 bytes] TRANSPORT CODES (optional, only if route type = 0 or 3) - -[1 byte] PATH_LEN (or omitted for flood mode) - -[0-64 bytes] PATH (route information) - -[1+ bytes] PAYLOAD (encrypted message) - ├─ [1 byte] Channel hash - ├─ [2 bytes] MAC (HMAC-SHA256 truncated) - └─ [16+ bytes] AES128 encrypted data -``` - -### 8.2 Plaintext Structure (Inside Encryption) - -``` -[4 bytes] TIMESTAMP (uint32_t, little-endian) -[1 byte] TXT_TYPE (0=plain, 1=CLI_DATA, 2=signed) -[variable] MESSAGE ("sender: text") -[0-15 bytes] ZERO PADDING (to reach 16-byte boundary) -``` - ---- - -## 9. Conclusion: Echo Detection Feasibility - -### Can We Detect Our Own Broadcast Echo? - -**YES - With High Confidence** - -**Methods:** -1. **Full Payload Matching (Recommended)** - - Store encrypted payload `[channel_hash || MAC || ciphertext]` after sending - - Compare received encrypted payloads - - 100% accurate if payload matches exactly - - No false positives due to deterministic encryption - -2. **Plaintext + Timestamp Matching** - - Use `getRTCClock()->getCurrentTimeUnique()` to ensure unique timestamp - - Store plaintext: `"sender_name: message_text"` + timestamp - - Match against decrypted received messages - - Very high confidence (timestamp uniqueness) - -3. **Packet Hash Matching** - - Calculate `SHA256(PAYLOAD_TYPE_GRP_TXT || payload) -> 8 bytes` - - Store sent packet hash - - Compare with received packet hash - - Collision probability: negligible - -### Why It Works: -- AES128-ECB is **deterministic**: same plaintext + key = identical ciphertext -- HMAC-SHA256 is **deterministic**: same data + key = identical MAC -- Timestamp uniqueness prevents collisions from same sender - -### When Echo Occurs: -- Another node receives our packet -- Rebroadcasts it (forwarding/relaying) -- We receive it back via different path -- Encrypted payload is **identical** to what we sent - -### Implementation Effort: -- **Low**: Store 18-50 bytes per sent message (hash + payload subset) -- **Fast**: Binary comparison or hash lookup -- **Reliable**: No dependencies on network topology or timing - diff --git a/MESHCORE_PROTOCOL.md b/MESHCORE_PROTOCOL.md deleted file mode 100644 index 18c8153..0000000 --- a/MESHCORE_PROTOCOL.md +++ /dev/null @@ -1,1163 +0,0 @@ -# MeshCore Protocol Specification - -Complete protocol documentation extracted from [MeshCore C++ implementation](https://github.com/meshcore-dev/MeshCore) and [meshcore.js](https://github.com/meshcore-dev/meshcore.js). - -## Overview - -MeshCore is a mesh networking protocol designed for low-power, long-range communication using LoRa radio and BLE connectivity. This document describes two distinct protocols: - -1. **Mesh Packet Protocol** - Complex routing protocol for mesh network communication - - Supports flood and direct routing - - End-to-end encryption with Ed25519/AES-128 - - Maximum payload: 184 bytes - - Used for device-to-device communication over LoRa - -2. **BLE Command Protocol** - Simple command/response protocol for local device control - - Nordic UART Service (NUS) profile - - Commands: get contacts, send messages, request telemetry - - Used for smartphone app ↔ MeshCore device communication - -**Key Features:** -- End-to-end encryption (AES-128-CTR) -- Digital signatures (Ed25519) -- Advertisement-based node discovery -- Group messaging support -- Telemetry data (GPS, battery, temperature) -- Anonymous messaging with forward secrecy - -## Table of Contents -1. [Protocol Constants](#protocol-constants) -2. [Packet Structure](#packet-structure) -3. [Header Encoding](#header-encoding) -4. [Payload Types](#payload-types) - - [Payload Structures by Type](#payload-structures-by-type) - - [Advertisement App Data Format](#advertisement-app-data-format) -5. [Route Types](#route-types) -6. [Cryptography](#cryptography) -7. [Binary Serialization](#binary-serialization) -8. [Validation Rules](#validation-rules) -9. [Special Features](#special-features) -10. [JavaScript Implementation Notes](#javascript-implementation-notes) -11. [BLE Transport Layer](#ble-transport-layer) - ---- - -## Protocol Constants - -### Size Limits - -| Constant | Value | Description | -|----------|-------|-------------| -| `MAX_PACKET_PAYLOAD` | 184 bytes | Maximum payload data size | -| `MAX_PATH_SIZE` | 64 bytes | Maximum routing path size | -| `MAX_TRANS_UNIT` | 255 bytes | Maximum transmission unit | -| `MAX_ADVERT_DATA_SIZE` | 32 bytes | Maximum advertisement data | -| `MAX_HASH_SIZE` | 8 bytes | Maximum hash size for routing | -| `PATH_HASH_SIZE` | 1 byte | Path hash size (V1) | - -### Cryptographic Sizes - -| Constant | Value | Description | -|----------|-------|-------------| -| `PUB_KEY_SIZE` | 32 bytes | Ed25519 public key size | -| `PRV_KEY_SIZE` | 64 bytes | Ed25519 private key size | -| `SEED_SIZE` | 32 bytes | Key generation seed size | -| `SIGNATURE_SIZE` | 64 bytes | Ed25519 signature size | -| `CIPHER_KEY_SIZE` | 16 bytes | AES-128 key size | -| `CIPHER_BLOCK_SIZE` | 16 bytes | AES block size | -| `CIPHER_MAC_SIZE` | 2 bytes | Message authentication code size (V1) | - -### Derived Constants - -```cpp -MAX_COMBINED_PATH = MAX_PACKET_PAYLOAD - 2 - CIPHER_BLOCK_SIZE - = 184 - 2 - 16 = 166 bytes -``` - ---- - -## Packet Structure - -### Member Variables - -```cpp -class Packet { - uint8_t header; // 1 byte: route type, payload type, version - uint16_t payload_len; // 2 bytes: payload data length - uint16_t path_len; // 2 bytes: routing path length - uint16_t transport_codes[2]; // 4 bytes: optional transport metadata - uint8_t path[MAX_PATH_SIZE]; // 64 bytes: routing path buffer - uint8_t payload[MAX_PACKET_PAYLOAD]; // 184 bytes: payload data buffer - int8_t _snr; // 1 byte: signal-to-noise ratio (×4) -}; -``` - -### Binary Layout (Wire Format) - -``` -+-------------------+--------+ -| Header | 1 byte | -+-------------------+--------+ -| Transport Code[0] | 2 bytes| (conditional, only if ROUTE_TYPE_TRANSPORT_*) -| Transport Code[1] | 2 bytes| -+-------------------+--------+ -| Path Length | 1 byte | -+-------------------+--------+ -| Path Data | N bytes| (N = path_len, max 64) -+-------------------+--------+ -| Payload Data | M bytes| (M = payload_len, max 184) -+-------------------+--------+ -``` - -**Total Packet Size Formula:** -``` -size = 2 + path_len + payload_len + (hasTransportCodes() ? 4 : 0) -``` - -**Minimum Packet Size:** 2 bytes (header + path_len with empty path and payload) -**Maximum Packet Size:** 250 bytes (2 + 4 + 64 + 184) - ---- - -## Header Encoding - -The header byte encodes three fields using bit manipulation: - -``` -Bit Layout: -+--------+--------+--------+--------+--------+--------+--------+--------+ -| Ver1 | Ver0 | Type3 | Type2 | Type1 | Type0 | Route1 | Route0 | -+--------+--------+--------+--------+--------+--------+--------+--------+ - Bit 7 Bit 6 Bit 5 Bit 4 Bit 3 Bit 2 Bit 1 Bit 0 -``` - -### Encoding Constants - -| Constant | Value | Description | -|----------|-------|-------------| -| `PH_ROUTE_MASK` | 0x03 | Mask for route type (bits 0-1) | -| `PH_TYPE_SHIFT` | 2 | Left shift for payload type | -| `PH_TYPE_MASK` | 0x0F | Mask for payload type (4 bits) | -| `PH_VER_SHIFT` | 6 | Left shift for payload version | -| `PH_VER_MASK` | 0x03 | Mask for payload version (bits 6-7) | - -### Encoding/Decoding Operations - -**Encoding:** -```cpp -header = (route_type & PH_ROUTE_MASK) | - ((payload_type & PH_TYPE_MASK) << PH_TYPE_SHIFT) | - ((payload_ver & PH_VER_MASK) << PH_VER_SHIFT); -``` - -**Decoding:** -```cpp -route_type = header & PH_ROUTE_MASK; -payload_type = (header >> PH_TYPE_SHIFT) & PH_TYPE_MASK; -payload_ver = (header >> PH_VER_SHIFT) & PH_VER_MASK; -``` - -### Example Header Values - -| Route | Type | Ver | Binary | Hex | Description | -|-------|------|-----|--------|-----|-------------| -| FLOOD | REQ | V1 | 00000001 | 0x01 | Flood routed request, version 1 | -| DIRECT | TXT_MSG | V1 | 00001010 | 0x0A | Direct text message, version 1 | -| TRANSPORT_FLOOD | ACK | V1 | 00001100 | 0x0C | Flood ACK with transport codes | - ---- - -## Payload Types - -| Name | Value | Description | Use Case | -|------|-------|-------------|----------| -| `PAYLOAD_TYPE_REQ` | 0x00 | Request message | Command or query to peer | -| `PAYLOAD_TYPE_RESPONSE` | 0x01 | Response message | Reply to REQ packet | -| `PAYLOAD_TYPE_TXT_MSG` | 0x02 | Text message | User-to-user chat message | -| `PAYLOAD_TYPE_ACK` | 0x03 | Acknowledgment | Confirm packet receipt | -| `PAYLOAD_TYPE_ADVERT` | 0x04 | Advertisement | Node presence announcement | -| `PAYLOAD_TYPE_GRP_TXT` | 0x05 | Group text message | Multi-recipient text | -| `PAYLOAD_TYPE_GRP_DATA` | 0x06 | Group data | Multi-recipient binary data | -| `PAYLOAD_TYPE_ANON_REQ` | 0x07 | Anonymous request | Request without sender ID | -| `PAYLOAD_TYPE_PATH` | 0x08 | Path discovery | Route discovery/return | -| `PAYLOAD_TYPE_TRACE` | 0x09 | Trace packet | Network diagnostics | -| `PAYLOAD_TYPE_MULTIPART` | 0x0A | Multi-part message | Large message fragmentation | -| `PAYLOAD_TYPE_RAW_CUSTOM` | 0x0F | Raw custom data | Application-specific payload | - -### Payload Type Categories - -**Control Messages:** -- ACK, PATH, TRACE - -**User Messages:** -- TXT_MSG, GRP_TXT - -**Data Transfer:** -- REQ, RESPONSE, GRP_DATA, RAW_CUSTOM, MULTIPART - -**Network Management:** -- ADVERT, ANON_REQ - -### Payload Structures by Type - -Each payload type has a specific binary structure. All encrypted payloads include a 2-byte MAC at the end. - -#### PAYLOAD_TYPE_REQ (0x00) - -**Structure:** -``` -+-------------------+----------+ -| Dest Hash | 1 byte | First byte of destination public key -| Src Hash | 1 byte | First byte of source public key -| MAC | 2 bytes | Message authentication code -| Encrypted Data | N bytes | AES-128-CTR encrypted payload -+-------------------+----------+ -``` - -**Encrypted Data Contains:** -- Timestamp (variable length) -- Request blob (application-specific) - -**Use Case:** Authenticated request from known sender to known recipient - ---- - -#### PAYLOAD_TYPE_RESPONSE (0x01) - -**Structure:** -``` -+-------------------+----------+ -| Dest Hash | 1 byte | First byte of destination public key -| Src Hash | 1 byte | First byte of source public key -| MAC | 2 bytes | Message authentication code -| Encrypted Data | N bytes | AES-128-CTR encrypted payload -+-------------------+----------+ -``` - -**Encrypted Data Contains:** -- Timestamp (variable length) -- Response blob (application-specific) - -**Use Case:** Reply to REQ or ANON_REQ packet - ---- - -#### PAYLOAD_TYPE_TXT_MSG (0x02) - -**Structure:** -``` -+-------------------+----------+ -| Dest Hash | 1 byte | First byte of destination public key -| Src Hash | 1 byte | First byte of source public key -| MAC | 2 bytes | Message authentication code -| Encrypted Data | N bytes | AES-128-CTR encrypted payload -+-------------------+----------+ -``` - -**Encrypted Data Contains:** -- Timestamp (variable length) -- Text message (UTF-8 string) - -**Use Case:** Person-to-person text messages - ---- - -#### PAYLOAD_TYPE_ACK (0x03) - -**Structure:** -``` -+-------------------+----------+ -| ACK Code | N bytes | Application-specific acknowledgment data -+-------------------+----------+ -``` - -**No Encryption:** ACK packets are typically unencrypted - -**Use Case:** Confirm receipt of packets, simple acknowledgments - ---- - -#### PAYLOAD_TYPE_ADVERT (0x04) - -**Structure:** -``` -+-------------------+----------+ -| Public Key | 32 bytes | Ed25519 public key of advertiser -| Timestamp | 4 bytes | Unix timestamp (uint32, little-endian) -| App Data | N bytes | Application-specific advertisement data -| Signature | 64 bytes | Ed25519 signature over above fields -+-------------------+----------+ -``` - -**Minimum Size:** 100 bytes (32 + 4 + 0 + 64) -**Maximum Size:** 132 bytes (32 + 4 + 32 + 64) with MAX_ADVERT_DATA_SIZE - -**Signature Verification:** -```cpp -// Build message to verify -message = public_key || timestamp || app_data - -// Verify Ed25519 signature -bool valid = ed25519_verify(signature, message, public_key); -``` - -**Use Case:** Node presence announcement, identity broadcast, service discovery - -### Advertisement App Data Format - -The App Data field has a structured format for node advertisements: - -**Binary Structure:** -``` -+-------------------+----------+ -| Flags | 1 byte | Type (4 bits) + Feature flags (4 bits) -| Latitude | 4 bytes | (Optional) int32 LE, divide by 10000 -| Longitude | 4 bytes | (Optional) int32 LE, divide by 10000 -| Battery | 1 byte | (Optional) percentage 0-100 -| Temperature | 1 byte | (Optional) signed int8, degrees Celsius -| Name | N bytes | (Optional) null-terminated UTF-8 string -+-------------------+----------+ -``` - -**Flags Byte Layout:** -``` -Bit Layout: -+--------+--------+--------+--------+--------+--------+--------+--------+ -| Name | Temp | Battery| LatLon | Type3 | Type2 | Type1 | Type0 | -+--------+--------+--------+--------+--------+--------+--------+--------+ - Bit 7 Bit 6 Bit 5 Bit 4 Bit 3 Bit 2 Bit 1 Bit 0 -``` - -**Type Field (bits 0-3):** -```cpp -#define ADV_TYPE_NONE 0x00 // Unknown/undefined type -#define ADV_TYPE_CHAT 0x01 // User/team member node -#define ADV_TYPE_REPEATER 0x02 // Network repeater node -#define ADV_TYPE_ROOM 0x03 // Group chat room/channel -``` - -**Feature Flags (bits 4-7):** -```cpp -#define ADV_LATLON_MASK 0x10 // Bit 4: Latitude/Longitude present -#define ADV_BATTERY_MASK 0x20 // Bit 5: Battery level present -#define ADV_TEMPERATURE_MASK 0x40 // Bit 6: Temperature present -#define ADV_NAME_MASK 0x80 // Bit 7: Name string present -``` - -**Extracting Fields:** -```cpp -uint8_t flags = app_data[0]; -uint8_t type = flags & 0x0F; -bool has_latlon = (flags & ADV_LATLON_MASK) != 0; -bool has_battery = (flags & ADV_BATTERY_MASK) != 0; -bool has_temp = (flags & ADV_TEMPERATURE_MASK) != 0; -bool has_name = (flags & ADV_NAME_MASK) != 0; -``` - -**Example App Data Parsing:** - -```javascript -// Example 1: CHAT node with GPS and name -// Flags: 0x91 (CHAT | LATLON_MASK | NAME_MASK) -// = 10010001 binary -const flags = 0x91; -const type = flags & 0x0F; // 0x01 = CHAT -const hasLatLon = flags & 0x10; // true -const hasName = flags & 0x80; // true - -// Bytes: [0x91] [lat: 4B] [lon: 4B] [name: "Alice\0"] -// Total: 1 + 4 + 4 + 6 = 15 bytes - -// Example 2: REPEATER with GPS, battery, and temp -// Flags: 0x72 (REPEATER | LATLON_MASK | BATTERY_MASK | TEMP_MASK) -const flags = 0x72; -const type = flags & 0x0F; // 0x02 = REPEATER -const hasLatLon = flags & 0x10; // true -const hasBattery = flags & 0x20; // true -const hasTemp = flags & 0x40; // true - -// Bytes: [0x72] [lat: 4B] [lon: 4B] [battery: 1B] [temp: 1B] -// Total: 1 + 4 + 4 + 1 + 1 = 11 bytes - -// Example 3: ROOM with only name -// Flags: 0x83 (ROOM | NAME_MASK) -const flags = 0x83; -const type = flags & 0x0F; // 0x03 = ROOM -const hasName = flags & 0x80; // true - -// Bytes: [0x83] [name: "SAR Team Alpha\0"] -// Total: 1 + 15 = 16 bytes -``` - -**GPS Coordinate Encoding:** -```cpp -// Encoding (on device) -int32_t lat_encoded = (int32_t)(latitude * 10000.0); -int32_t lon_encoded = (int32_t)(longitude * 10000.0); - -// Decoding (on receiver) -double latitude = lat_encoded / 10000.0; -double longitude = lon_encoded / 10000.0; - -// Example: 46.0569°N, 14.5058°E -// Encoded: 460569, 145058 -// 4 decimal places precision (~11m accuracy) -``` - -**Complete Parsing Example (JavaScript):** -```javascript -function parseAdvertAppData(appData) { - const reader = new BufferReader(appData); - const flags = reader.readByte(); - - const type = flags & 0x0F; - const result = { type }; - - // Parse lat/lon if present - if (flags & 0x10) { - result.lat = reader.readInt32LE() / 10000.0; - result.lon = reader.readInt32LE() / 10000.0; - } - - // Parse battery if present - if (flags & 0x20) { - result.battery = reader.readByte(); // 0-100% - } - - // Parse temperature if present - if (flags & 0x40) { - result.temperature = reader.readInt8(); // -128 to +127°C - } - - // Parse name if present (remaining bytes) - if (flags & 0x80) { - result.name = reader.readString(); // null-terminated UTF-8 - } - - return result; -} -``` - -**Advertisement Frequency:** -- Typically broadcast every 30-60 seconds -- Can be triggered on-demand for discovery -- Should include timestamp to detect stale advertisements - -**Security Considerations:** -1. **Always verify signature** before trusting advertisement data -2. **Check timestamp** to reject old/replayed advertisements -3. **Validate GPS coordinates** are within reasonable ranges -4. **Sanitize name strings** before display (max length, valid UTF-8) -5. **Rate limit** advertisement processing to prevent DoS - ---- - -#### PAYLOAD_TYPE_GRP_TXT (0x05) - -**Structure:** -``` -+-------------------+----------+ -| Channel Hash | 1 byte | First byte of group channel hash -| MAC | 2 bytes | Message authentication code -| Encrypted Data | N bytes | AES-128-CTR encrypted payload -+-------------------+----------+ -``` - -**Encrypted Data Contains:** -- Timestamp (variable length) -- Message text in format: `"sender_name: message_text"` - -**Security Note:** Unverified sender identity - anyone with the group key can send - -**Use Case:** Group chat messages to a channel - ---- - -#### PAYLOAD_TYPE_GRP_DATA (0x06) - -**Structure:** -``` -+-------------------+----------+ -| Channel Hash | 1 byte | First byte of group channel hash -| MAC | 2 bytes | Message authentication code -| Encrypted Data | N bytes | AES-128-CTR encrypted payload -+-------------------+----------+ -``` - -**Encrypted Data Contains:** -- Timestamp (variable length) -- Binary data blob (application-specific) - -**Use Case:** Group data broadcast (telemetry, coordinates, binary files) - ---- - -#### PAYLOAD_TYPE_ANON_REQ (0x07) - -**Structure:** -``` -+-------------------+----------+ -| Dest Hash | 1 byte | First byte of destination public key -| Ephemeral Pub Key | 32 bytes | Temporary Ed25519 public key -| MAC | 2 bytes | Message authentication code -| Encrypted Data | N bytes | AES-128-CTR encrypted payload -+-------------------+----------+ -``` - -**Minimum Size:** 35 bytes (1 + 32 + 2 + 0) - -**Encrypted Data Contains:** -- Application-specific request data - -**Key Derivation:** -- Recipient uses their private key + ephemeral public key to derive shared secret -- Sender discards ephemeral private key after sending (forward secrecy) - -**Use Case:** Anonymous requests, forward-secret communications - ---- - -#### PAYLOAD_TYPE_PATH (0x08) - -**Structure:** -``` -+-------------------+----------+ -| Dest Hash | 1 byte | First byte of destination public key -| Src Hash | 1 byte | First byte of source public key -| MAC | 2 bytes | Message authentication code -| Encrypted Data | N bytes | AES-128-CTR encrypted payload -+-------------------+----------+ -``` - -**Encrypted Data Contains:** -- Path data: sequence of node hashes showing discovered route -- Extra metadata (optional) - -**Use Case:** Return discovered routing path to sender - ---- - -#### PAYLOAD_TYPE_TRACE (0x09) - -**Structure:** -``` -+-------------------+----------+ -| Trace Data | N bytes | Application-specific trace payload -+-------------------+----------+ -``` - -**No Standard Format:** Application defines structure - -**Common Usage:** -- Collecting SNR (signal-to-noise ratio) at each hop -- Measuring latency through network -- Network topology discovery -- Debugging routing issues - -**Path Field:** Used to accumulate node IDs and SNR measurements as packet propagates - ---- - -#### PAYLOAD_TYPE_RAW_CUSTOM (0x0F) - -**Structure:** -``` -+-------------------+----------+ -| Custom Data | N bytes | Completely application-defined -+-------------------+----------+ -``` - -**No Standard Format:** Application has full control over: -- Encryption scheme (or no encryption) -- Data encoding -- Protocol semantics - -**Use Case:** Application-specific protocols, custom encryption, proprietary formats - ---- - -### Hash Prefixes - -Several payload types use 1-byte "hash" fields that represent the first byte of a 32-byte public key: - -```cpp -uint8_t dest_hash = public_key[0]; -``` - -**Purpose:** -- Quick filtering: nodes can ignore packets not addressed to them -- Space efficiency: 1 byte vs 32 bytes -- Collision rate: 1/256 (acceptable for mesh routing) - -**Collision Handling:** -- When hash matches, validate full public key after decryption -- If decryption fails, packet was for a different node with same hash prefix - ---- - -### Encrypted Payload Format - -All encrypted payloads use this structure: - -``` -+-------------------+----------+ -| Encrypted Data | N bytes | AES-128-CTR ciphertext -| MAC | 2 bytes | Authentication code (included in total payload_len) -+-------------------+----------+ -``` - -**Decryption Process:** -1. Extract last 2 bytes as MAC -2. Verify MAC over encrypted data (bytes 0 to N-2) -3. If valid, decrypt using AES-128-CTR with derived cipher key -4. If invalid, silently discard packet - -**Common Encrypted Data Structure:** -``` -+-------------------+----------+ -| Timestamp | 4 bytes | Unix timestamp (uint32) for replay protection -| Payload Data | N bytes | Application-specific data -+-------------------+----------+ -``` - ---- - -## Route Types - -| Name | Value | Description | Transport Codes | -|------|-------|-------------|-----------------| -| `ROUTE_TYPE_TRANSPORT_FLOOD` | 0x00 | Flood routing with metadata | Yes (4 bytes) | -| `ROUTE_TYPE_FLOOD` | 0x01 | Simple flood routing | No | -| `ROUTE_TYPE_DIRECT` | 0x02 | Direct peer-to-peer | No | -| `ROUTE_TYPE_TRANSPORT_DIRECT` | 0x03 | Direct with metadata | Yes (4 bytes) | - -### Routing Behavior - -**FLOOD Mode (0x01, 0x00):** -- Packet is retransmitted by all receiving nodes -- Path field accumulates node IDs as packet propagates -- Used for network-wide broadcasts and discovery -- Path prevents routing loops - -**DIRECT Mode (0x02, 0x03):** -- Packet routed only through specified path -- Path field contains complete route to destination -- Used for established peer-to-peer connections -- More efficient than flood routing - -**Transport Codes:** -- When present (types 0x00 and 0x03), add 4 bytes after header -- Two 16-bit unsigned integers for transport layer metadata -- Use cases: sequence numbers, retry counts, QoS flags - ---- - -## Cryptography - -### Encryption Scheme - -**Algorithm:** AES-128-CTR with custom MAC -**Key Derivation:** ECDH using Ed25519 keys -**Authentication:** 2-byte MAC (CIPHER_MAC_SIZE) - -### Shared Secret Calculation - -```cpp -// Given: local private key (64 bytes), remote public key (32 bytes) -uint8_t shared_secret[32]; -calcSharedSecret(local_prv_key, remote_pub_key, shared_secret); -``` - -### Packet Encryption Process - -1. Calculate shared secret from sender private key and recipient public key -2. Generate cipher key from shared secret -3. Encrypt payload using AES-128-CTR -4. Calculate MAC over encrypted payload -5. Append MAC to encrypted data (total: payload_len + 2) - -### Packet Decryption Process - -1. Extract MAC from last 2 bytes of payload -2. Calculate expected MAC over encrypted data -3. Compare MACs (constant-time comparison required) -4. If MAC valid, decrypt payload using AES-128-CTR -5. If MAC invalid, discard packet - -**Security Note:** MAC-then-decrypt pattern requires constant-time MAC comparison to prevent timing attacks. - -### Digital Signatures - -**Algorithm:** Ed25519 -**Signature Size:** 64 bytes - -Used for: -- Advertisement packet authentication -- Path discovery verification -- Identity proofs - ---- - -## Binary Serialization - -### writeTo() Method - -Serializes packet to byte array: - -```cpp -size_t writeTo(uint8_t* buffer, size_t buffer_size) { - size_t offset = 0; - - // 1. Write header byte - buffer[offset++] = header; - - // 2. Write transport codes (if present) - if (hasTransportCodes()) { - buffer[offset++] = (uint8_t)(transport_codes[0] & 0xFF); - buffer[offset++] = (uint8_t)(transport_codes[0] >> 8); - buffer[offset++] = (uint8_t)(transport_codes[1] & 0xFF); - buffer[offset++] = (uint8_t)(transport_codes[1] >> 8); - } - - // 3. Write path length - buffer[offset++] = (uint8_t)path_len; - - // 4. Write path data - memcpy(buffer + offset, path, path_len); - offset += path_len; - - // 5. Write payload data - memcpy(buffer + offset, payload, payload_len); - offset += payload_len; - - return offset; // Total bytes written -} -``` - -### readFrom() Method - -Deserializes packet from byte array: - -```cpp -bool readFrom(const uint8_t* buffer, size_t buffer_size) { - size_t offset = 0; - - // 1. Read header byte - if (offset >= buffer_size) return false; - header = buffer[offset++]; - - // 2. Read transport codes (if present) - if (hasTransportCodes()) { - if (offset + 4 > buffer_size) return false; - transport_codes[0] = buffer[offset] | (buffer[offset+1] << 8); - offset += 2; - transport_codes[1] = buffer[offset] | (buffer[offset+1] << 8); - offset += 2; - } - - // 3. Read path length - if (offset >= buffer_size) return false; - path_len = buffer[offset++]; - - // 4. Validate path length - if (path_len > MAX_PATH_SIZE) return false; - if (offset + path_len > buffer_size) return false; - - // 5. Read path data - memcpy(path, buffer + offset, path_len); - offset += path_len; - - // 6. Calculate and validate payload length - payload_len = buffer_size - offset; - if (payload_len > MAX_PACKET_PAYLOAD) return false; - - // 7. Read payload data - memcpy(payload, buffer + offset, payload_len); - - return true; // Success -} -``` - ---- - -## Validation Rules - -### Packet Acceptance Criteria - -A valid packet must satisfy: - -1. **Header Validation:** - - Route type ≤ 3 (valid route type) - - Payload type ≤ 15 (4-bit field) - - Payload version ≤ 3 (2-bit field) - -2. **Path Validation:** - - `path_len ≤ MAX_PATH_SIZE` (64 bytes) - - Path data must not exceed buffer size - -3. **Payload Validation:** - - `payload_len ≤ MAX_PACKET_PAYLOAD` (184 bytes) - - Payload data must not exceed buffer size - - For encrypted packets: payload_len ≥ CIPHER_MAC_SIZE (2 bytes) - -4. **Size Validation:** - - Total packet size ≤ MAX_TRANS_UNIT (255 bytes) - - Minimum size: 2 bytes (header + path_len) - -5. **Cryptographic Validation (if encrypted):** - - MAC must match calculated value - - Decryption must succeed without errors - -### Error Handling - -**Invalid Packets:** -- Silently discarded (no error response) -- Logged for debugging if trace enabled - -**Malformed Data:** -- `readFrom()` returns `false` -- Packet object left in undefined state -- Caller must not use packet after failed read - ---- - -## Packet Hash Calculation - -Used for duplicate detection and routing loop prevention: - -```cpp -void calculatePacketHash(uint8_t* hash_out, size_t hash_len) { - // Initialize SHA256 - SHA256 sha256; - sha256.reset(); - - // 1. Hash payload type - uint8_t type = (header >> PH_TYPE_SHIFT) & PH_TYPE_MASK; - sha256.update(&type, 1); - - // 2. Hash path length (only for TRACE packets) - if (type == PAYLOAD_TYPE_TRACE) { - uint8_t plen = (uint8_t)path_len; - sha256.update(&plen, 1); - } - - // 3. Hash payload data - sha256.update(payload, payload_len); - - // 4. Finalize and copy to output - uint8_t full_hash[32]; - sha256.finalize(full_hash, 32); - memcpy(hash_out, full_hash, hash_len); -} -``` - -**Hash Properties:** -- Based on SHA256 -- Configurable output length (typically MAX_HASH_SIZE = 8 bytes) -- Includes payload type and payload data -- TRACE packets include path_len to detect routing changes - ---- - -## Protocol Version - -**Current Version:** V1 (PAYLOAD_VER_1 = 0x00) - -**Version Features:** -- V1: 2-byte MAC, 1-byte path hash -- V2-V4: Reserved for future use - -**Version Compatibility:** -- Nodes must reject packets with unsupported versions -- Forward compatibility requires checking version before processing - ---- - -## Implementation Notes - -### Performance Considerations - -**Buffer Management:** -- Pre-allocate packet buffers to avoid dynamic allocation -- Use stack allocation for temporary packets -- Pool frequently used packet objects - -**Crypto Optimization:** -- Cache shared secrets for active connections -- Use hardware AES acceleration if available -- Batch MAC calculations when possible - -**Routing Efficiency:** -- Maintain routing table cache for direct routes -- Limit flood packet retransmissions (hop count) -- Implement exponential backoff for retries - -### Security Best Practices - -1. **Always validate MAC** before decrypting -2. **Use constant-time comparison** for MAC validation -3. **Clear sensitive data** from memory after use -4. **Implement replay protection** using sequence numbers -5. **Rate limit** flood packets to prevent DoS attacks - -### Interoperability - -This specification is based on the [MeshCore C++ implementation](https://github.com/meshcore-dev/MeshCore) and is compatible with: - -- MeshCore firmware (ESP32, nRF52, STM32) -- meshcore.js library -- This Flutter application - -**Byte Order:** All multi-byte integers use **little-endian** encoding. - ---- - -## Special Features - -### Do Not Retransmit Flag - -Packets can be marked to prevent retransmission: - -```javascript -packet.markDoNotRetransmit(); // Sets header to 0xFF -if (packet.isMarkedDoNotRetransmit()) { - // Don't retransmit this packet -} -``` - -**When to Use:** -- Packets already flooded to entire network -- Time-sensitive data that's no longer relevant -- Preventing routing loops in edge cases - -**Implementation:** Header value of `0xFF` is reserved as a special marker - ---- - -## JavaScript Implementation Notes - -The JavaScript implementation (meshcore.js) provides a convenient API for packet parsing: - -```javascript -// Parse packet from bytes -const packet = Packet.fromBytes(bytes); - -// Access parsed header fields -console.log(packet.route_type_string); // "FLOOD" or "DIRECT" -console.log(packet.payload_type_string); // "TXT_MSG", "ADVERT", etc. -console.log(packet.payload_version); // 0, 1, 2, or 3 - -// Parse payload based on type -const parsed = packet.parsePayload(); -if (packet.payload_type === Packet.PAYLOAD_TYPE_ADVERT) { - console.log(parsed.public_key); // 32-byte Uint8Array - console.log(parsed.timestamp); // Unix timestamp - console.log(parsed.app_data); // Application data -} -``` - -**Supported Payload Parsers:** -- `PAYLOAD_TYPE_REQ` → `{ src, dest, encrypted }` -- `PAYLOAD_TYPE_RESPONSE` → `{ src, dest }` -- `PAYLOAD_TYPE_TXT_MSG` → `{ src, dest }` -- `PAYLOAD_TYPE_ACK` → `{ ack_code }` -- `PAYLOAD_TYPE_ADVERT` → `{ public_key, timestamp, app_data }` -- `PAYLOAD_TYPE_ANON_REQ` → `{ src, dest }` (src is 32-byte ephemeral key) -- `PAYLOAD_TYPE_PATH` → `{ src, dest }` - -**Note:** The JavaScript parsers extract the unencrypted header fields only. Encrypted data decryption requires implementing the crypto layer. - ---- - -## BLE Transport Layer - -MeshCore packets are transported over Bluetooth Low Energy (BLE) using the Nordic UART Service (NUS) profile. - -### BLE Service Specification - -**Service UUID:** `6E400001-B5A3-F393-E0A9-E50E24DCCA9E` (Nordic UART Service) - -**Characteristics:** - -| Characteristic | UUID | Properties | Description | -|----------------|------|------------|-------------| -| RX | `6E400002-B5A3-F393-E0A9-E50E24DCCA9E` | Write, Write Without Response | Client → Device (commands) | -| TX | `6E400003-B5A3-F393-E0A9-E50E24DCCA9E` | Notify | Device → Client (responses) | - -### BLE vs Mesh Protocol - -**Important Distinction:** - -The BLE transport layer uses a **different protocol** than the mesh packet protocol documented above. - -**BLE Protocol:** -- Simple command/response format -- Used for **local device communication** only -- Commands to query device state, send messages, request data -- Not forwarded through mesh network - -**Mesh Protocol:** -- Complex packet structure with routing -- Used for **mesh network communication** -- Packets can be flooded or routed through multiple hops -- Carries encrypted user data - -### BLE Command Protocol - -Commands sent over BLE RX characteristic: - -| Command | Code | Description | Parameters | -|---------|------|-------------|------------| -| Get Contacts | `0x04` | Request list of known contacts | None | -| Send Message | `0x02` | Send text message | 32B pubkey, 2B length, text | -| Request Telemetry | `0x27` (39) | Request telemetry data | 32B contact pubkey | - -### BLE Response Protocol - -Responses received over BLE TX characteristic: - -| Response | Code | Description | Structure | -|----------|------|-------------|-----------| -| Contact Info | `0x03` | Contact details | 32B pubkey, 1B type, 64B name, 4B lat, 4B lon | -| Message Received | `0x07` | Incoming message | 1B type, 4B src, 4B dest, 2B length, text | -| Telemetry | `0x8B` (139) | Cayenne LPP telemetry | 4B pubkey prefix, LPP data | - -### BLE Message Format Examples - -**Get Contacts Request:** -``` -RX: [0x04] -Total: 1 byte -``` - -**Send Message Request:** -``` -RX: [0x02] [32 bytes: recipient pubkey] [2 bytes: length] [N bytes: UTF-8 text] -Example (hex): 02 A1B2C3D4...pubkey...E5F6 0B00 48656C6C6F20576F726C64 - ^cmd ^----- 32 bytes -----^ ^len ^----- "Hello World" -----^ -Total: 1 + 32 + 2 + N bytes -``` - -**Contact Response:** -``` -TX: [0x03] [32 bytes: pubkey] [1 byte: type] [64 bytes: name] [4 bytes: lat] [4 bytes: lon] -Type: 0=none, 1=chat, 2=repeater, 3=room -Lat/Lon: int32 little-endian, divide by 10000 for degrees -Total: 105 bytes -``` - -**Message Received:** -``` -TX: [0x07] [1 byte: msg type] [4 bytes: src prefix] [4 bytes: dest prefix] [2 bytes: length] [N bytes: text] -Msg Type: 0=contact, 1=channel -Total: 1 + 1 + 4 + 4 + 2 + N bytes -``` - -**Telemetry Response:** -``` -TX: [0x8B] [4 bytes: contact pubkey prefix] [N bytes: Cayenne LPP payload] -Total: 5 + N bytes -``` - -### Cayenne LPP Format (Telemetry) - -Cayenne Low Power Payload format used for telemetry data: - -**Structure:** -``` -[Channel] [Type] [Data...] -``` - -**Supported Types:** - -| Type | Code | Data Format | Description | -|------|------|-------------|-------------| -| GPS | `0x88` (136) | 12 bytes | lat(4B) + lon(4B) + alt(4B), divide lat/lon by 10000, alt by 100 | -| Temperature | `0x67` (103) | 2 bytes | int16 LE, divide by 10 for °C | -| Analog Input | `0x02` | 2 bytes | uint16 LE, divide by 100 for volts (battery) | - -**Example Telemetry Packet:** -``` -Channel 1, GPS: [01] [88] [A0 C2 06 00] [30 67 02 00] [2C 01 00 00] - ^ch ^type ^-- lat --^ ^-- lon --^ ^-- alt --^ -Decoded: lat=443040/10000=44.304°, lon=157488/10000=15.7488°, alt=300/100=3.00m - -Channel 2, Temp: [02] [67] [0E 01] - ^ch ^type ^-value-^ -Decoded: temp=270/10=27.0°C - -Channel 3, Battery: [03] [02] [90 01] - ^ch ^type ^-value-^ -Decoded: battery=400/100=4.00V -``` - -### BLE vs Mesh Packet Flow - -``` -┌─────────────┐ ┌──────────────┐ -│ Flutter │ ← BLE Commands → │ MeshCore │ -│ App │ (Simple) │ Device │ -└─────────────┘ └──────┬───────┘ - │ - │ Mesh Packets - │ (Complex) - │ - ┌───────▼───────┐ - │ LoRa/Radio │ - │ Mesh │ - │ Network │ - └───────────────┘ -``` - -**Data Flow Example:** - -1. App sends "Get Contacts" (BLE command 0x04) -2. Device responds with Contact Info (BLE response 0x03) for each contact -3. User sends message via app (BLE command 0x02) -4. Device creates **mesh packet** (PAYLOAD_TYPE_TXT_MSG) and broadcasts on LoRa -5. Remote device receives mesh packet, forwards to its BLE-connected app -6. Remote app receives message (BLE response 0x07) - -### Implementation Notes - -**BLE MTU Limitations:** -- Default MTU: 23 bytes (20 bytes usable data) -- Extended MTU: up to 512 bytes (device dependent) -- Long messages may require fragmentation - -**Buffering:** -- BLE TX notifications arrive in chunks -- App must buffer partial packets until complete -- Use packet length headers to detect boundaries - -**Connection Management:** -- Maintain single BLE connection to MeshCore device -- Device acts as BLE peripheral (server) -- App acts as BLE central (client) -- Reconnect automatically on disconnection - -**Flutter Implementation:** -See `lib/services/meshcore_ble_service.dart` for complete BLE protocol implementation. - ---- - -## References - -### Source Code -- [MeshCore GitHub Repository](https://github.com/meshcore-dev/MeshCore) - C++ firmware implementation -- [Packet.h](https://github.com/meshcore-dev/MeshCore/blob/main/src/Packet.h) - C++ packet class definition -- [Packet.cpp](https://github.com/meshcore-dev/MeshCore/blob/main/src/Packet.cpp) - C++ packet serialization -- [Mesh.h](https://github.com/meshcore-dev/MeshCore/blob/main/src/Mesh.h) - C++ mesh networking -- [Mesh.cpp](https://github.com/meshcore-dev/MeshCore/blob/main/src/Mesh.cpp) - C++ routing implementation -- [MeshCore.h](https://github.com/meshcore-dev/MeshCore/blob/main/src/MeshCore.h) - C++ protocol constants -- [meshcore.js Packet.js](https://github.com/meshcore-dev/meshcore.js) - JavaScript implementation - -### Documentation -- This document provides implementation details for the MeshCore SAR Flutter application -- Compatible with MeshCore firmware v1.x protocol specification - ---- - -**Document Version:** 1.1 -**Last Updated:** 2025-10-14 -**Protocol Version:** V1 diff --git a/MESHCORE_QUICK_REFERENCE.md b/MESHCORE_QUICK_REFERENCE.md deleted file mode 100644 index 8c0b64b..0000000 --- a/MESHCORE_QUICK_REFERENCE.md +++ /dev/null @@ -1,216 +0,0 @@ -# MeshCore Quick Reference Card - -Quick lookup for MeshCore protocol constants and structures. - -## BLE Service (App ↔ Device) - -**Service UUID:** `6E400001-B5A3-F393-E0A9-E50E24DCCA9E` -- **RX:** `6E400002-B5A3-F393-E0A9-E50E24DCCA9E` (Write - Commands) -- **TX:** `6E400003-B5A3-F393-E0A9-E50E24DCCA9E` (Notify - Responses) - -### BLE Commands (RX) - -| Code | Command | Format | -|------|---------|--------| -| `0x04` | Get Contacts | `[0x04]` | -| `0x02` | Send Message | `[0x02][32B pubkey][2B len][text]` | -| `0x27` | Get Telemetry | `[0x27][32B pubkey]` | - -### BLE Responses (TX) - -| Code | Response | Format | -|------|----------|--------| -| `0x03` | Contact Info | `[0x03][32B pubkey][1B type][64B name][4B lat][4B lon]` | -| `0x07` | Message | `[0x07][1B type][4B src][4B dest][2B len][text]` | -| `0x8B` | Telemetry | `[0x8B][4B pubkey][Cayenne LPP data]` | - ---- - -## Mesh Packet Structure (LoRa Network) - -``` -[Header: 1B] [Path Len: 1B] [Path: 0-64B] [Payload: 0-184B] -``` - -### Header Encoding - -``` -Bits: [Ver:2][Type:4][Route:2] -Route = header & 0x03 -Type = (header >> 2) & 0x0F -Ver = (header >> 6) & 0x03 -``` - -### Route Types - -| Code | Name | Description | -|------|------|-------------| -| `0x01` | FLOOD | Broadcast to all nodes | -| `0x02` | DIRECT | Point-to-point via path | - -### Payload Types - -| Code | Name | Encrypted | Structure | -|------|------|-----------|-----------| -| `0x00` | REQ | ✓ | `[1B dest][1B src][2B MAC][encrypted]` | -| `0x01` | RESPONSE | ✓ | `[1B dest][1B src][2B MAC][encrypted]` | -| `0x02` | TXT_MSG | ✓ | `[1B dest][1B src][2B MAC][encrypted]` | -| `0x03` | ACK | ✗ | `[ack data]` | -| `0x04` | ADVERT | ✗ | `[32B pubkey][4B ts][app][64B sig]` | -| `0x05` | GRP_TXT | ✓ | `[1B chan][2B MAC][encrypted]` | -| `0x06` | GRP_DATA | ✓ | `[1B chan][2B MAC][encrypted]` | -| `0x07` | ANON_REQ | ✓ | `[1B dest][32B ephemeral][2B MAC][enc]` | -| `0x08` | PATH | ✓ | `[1B dest][1B src][2B MAC][encrypted]` | -| `0x09` | TRACE | ✗ | `[trace data]` | -| `0x0F` | RAW_CUSTOM | ? | Application-defined | - ---- - -## Advertisement App Data - -**Format:** `[Flags:1B][Lat:4B?][Lon:4B?][Battery:1B?][Temp:1B?][Name:NB?]` - -### Flags Byte - -``` -Bits: [Name:1][Temp:1][Batt:1][LatLon:1][Type:4] -Type = flags & 0x0F -Has GPS = flags & 0x10 -Has Batt = flags & 0x20 -Has Temp = flags & 0x40 -Has Name = flags & 0x80 -``` - -### Contact Types - -| Code | Name | Description | -|------|------|-------------| -| `0x00` | NONE | Unknown | -| `0x01` | CHAT | Team member (shown on map) | -| `0x02` | REPEATER | Network node | -| `0x03` | ROOM | Group channel | - ---- - -## Cayenne LPP (Telemetry) - -**Format:** `[Channel:1B][Type:1B][Data]` - -| Type | Code | Data | Decoding | -|------|------|------|----------| -| GPS | `0x88` | 12B | lat/lon ÷ 10000, alt ÷ 100 | -| Temp | `0x67` | 2B | int16 ÷ 10 for °C | -| Analog | `0x02` | 2B | uint16 ÷ 100 for volts | - -**Example:** -``` -[01][88][A0C20600][30670200][2C010000] - ^ch ^gps ^-lat-^ ^-lon-^ ^-alt-^ -GPS: 44.304°N, 15.7488°E, 3.00m -``` - ---- - -## Constants - -### Size Limits -- Max Packet Payload: 184 bytes -- Max Path Size: 64 bytes -- Max Advert Data: 32 bytes -- Public Key: 32 bytes -- Private Key: 64 bytes -- Signature: 64 bytes -- MAC: 2 bytes -- Cipher Block: 16 bytes - -### Coordinate Encoding -```dart -// Encode -int32 encoded = (double degrees * 10000).toInt(); - -// Decode -double degrees = encoded / 10000.0; - -// Precision: 4 decimal places (~11m accuracy) -``` - ---- - -## Common Operations - -### Parse BLE Contact Response -```dart -final pubkey = data.sublist(1, 33); // 32 bytes -final type = data[33]; // 0-3 -final name = data.sublist(34, 98); // 64 bytes -final lat = ByteData.view(data.buffer) - .getInt32(98, Endian.little) / 10000.0; -final lon = ByteData.view(data.buffer) - .getInt32(102, Endian.little) / 10000.0; -``` - -### Parse Mesh Packet Header -```dart -final header = packet[0]; -final routeType = header & 0x03; -final payloadType = (header >> 2) & 0x0F; -final version = (header >> 6) & 0x03; -final isFlood = routeType == 0x01; -final isTxtMsg = payloadType == 0x02; -``` - -### Parse Advertisement Flags -```dart -final flags = appData[0]; -final contactType = flags & 0x0F; -final hasGPS = (flags & 0x10) != 0; -final hasBattery = (flags & 0x20) != 0; -final hasTemp = (flags & 0x40) != 0; -final hasName = (flags & 0x80) != 0; -``` - -### Parse Cayenne LPP GPS -```dart -if (data[1] == 0x88) { // GPS type - final lat = ByteData.view(data.buffer) - .getInt32(2, Endian.little) / 10000.0; - final lon = ByteData.view(data.buffer) - .getInt32(6, Endian.little) / 10000.0; - final alt = ByteData.view(data.buffer) - .getInt32(10, Endian.little) / 100.0; -} -``` - ---- - -## Security Notes - -1. **Always verify signatures** on ADVERT packets -2. **Validate MAC** before decrypting encrypted payloads -3. **Check timestamps** to prevent replay attacks -4. **Sanitize strings** before display (max length, UTF-8 validation) -5. **Rate limit** packet processing to prevent DoS -6. Use **constant-time comparison** for MAC validation - ---- - -## Flutter Implementation - -**Main Files:** -- `lib/services/meshcore_ble_service.dart` - BLE protocol -- `lib/services/buffer_reader.dart` - Binary parsing -- `lib/services/buffer_writer.dart` - Binary encoding -- `lib/services/cayenne_lpp_parser.dart` - Telemetry decoding - ---- - -## Additional Documentation - -- **[MESHCORE_PROTOCOL.md](MESHCORE_PROTOCOL.md)** - Complete mesh packet protocol specification -- **[MESHCORE_BLE_PROTOCOL.md](MESHCORE_BLE_PROTOCOL.md)** - Complete BLE command/response protocol -- **[CLAUDE.md](CLAUDE.md)** - Project overview and development guide - ---- - -**Document Version:** 1.0 -**Last Updated:** 2025-10-14 diff --git a/MESSAGES.md b/MESSAGES.md deleted file mode 100644 index a55e2f6..0000000 --- a/MESSAGES.md +++ /dev/null @@ -1,1322 +0,0 @@ -# MeshCore Messaging System - Complete Implementation Guide - -This document provides complete technical specifications for implementing messaging in MeshCore applications. - -## Table of Contents - -1. [Message Types and Architecture](#1-message-types-and-architecture) -2. [Sending Messages](#2-sending-messages) -3. [Receiving Messages](#3-receiving-messages) -4. [Message Confirmation and ACKs](#4-message-confirmation-and-acks) -5. [Room vs Channel System](#5-room-vs-channel-system) -6. [Binary Protocol Specifications](#6-binary-protocol-specifications) -7. [Implementation Checklist](#7-implementation-checklist) -8. [Common Pitfalls](#8-common-pitfalls) -9. [Testing and Validation](#9-testing-and-validation) - ---- - -## 1. Message Types and Architecture - -### 1.1 Payload Types - -MeshCore defines several payload types for different message purposes: - -```cpp -#define PAYLOAD_TYPE_ADVERT 0x01 // Advertisement packet -#define PAYLOAD_TYPE_PATH 0x02 // Path return packet -#define PAYLOAD_TYPE_TXT_MSG 0x03 // Text message (DM or channel) -#define PAYLOAD_TYPE_DATA 0x04 // Binary data -#define PAYLOAD_TYPE_REQUEST 0x05 // Binary request (telemetry, status) -#define PAYLOAD_TYPE_RESPONSE 0x06 // Binary response -#define PAYLOAD_TYPE_ACK 0x07 // Acknowledgment -#define PAYLOAD_TYPE_TRACE 0x08 // Path trace packet -#define PAYLOAD_TYPE_RAW_CUSTOM 0x09 // Raw custom data -``` - -**Reference**: `/Users/dz0ny/meshcore-sar/MeshCore/src/MeshCore.h`, lines 27-35 - -### 1.2 Text Message Types - -Text messages (PAYLOAD_TYPE_TXT_MSG) have subtypes: - -```cpp -#define TXT_TYPE_PLAIN 0x00 // Plain text message -#define TXT_TYPE_CLI_DATA 0x01 // CLI command -#define TXT_TYPE_SIGNED_PLAIN 0x02 // Plain text, cryptographically signed -``` - -**Usage**: -- **TXT_TYPE_PLAIN**: Standard chat messages, SAR markers -- **TXT_TYPE_CLI_DATA**: Remote administration commands (requires admin permissions) -- **TXT_TYPE_SIGNED_PLAIN**: Future use for message authentication - -**Reference**: `/Users/dz0ny/meshcore-sar/MeshCore/src/MeshCore.h`, lines 37-39 - -### 1.3 Message Length Limits - -**Direct Messages** (CMD_SEND_TXT_MSG): -``` -Maximum: 160 bytes of UTF-8 text -``` - -**Channel Messages** (CMD_SEND_CHANNEL_TXT_MSG): -``` -Maximum: 160 - len(sender_name) - 2 bytes -Example: If sender name is "John", max is 160 - 4 - 2 = 154 bytes -``` - -**Why the difference?** -- Channel messages include sender name in the packet payload -- Direct messages use public key for identification (name stored in contacts) - -**Reference**: `/Users/dz0ny/meshcore-sar/MeshCore/docs/companion.md`, CMD_SEND_TXT_MSG and CMD_SEND_CHANNEL_TXT_MSG sections - -### 1.4 Message Storage and Queuing - -**On Companion Device**: -- Received messages stored in circular buffer (platform-specific size, typically 50-100 messages) -- Messages persist until fetched via `CMD_SYNC_NEXT_MESSAGE` -- Oldest messages overwritten when buffer is full - -**In Rooms**: -- Messages stored persistently in flash memory -- Immutable storage (cannot be deleted) -- Room server pushes messages to logged-in clients automatically -- Messages ordered by timestamp - -**Reference**: `/Users/dz0ny/meshcore-sar/MeshCore/examples/simple_repeater/MyMesh.cpp`, lines 498-542 - ---- - -## 2. Sending Messages - -### 2.1 CMD_SEND_TXT_MSG (Code 2) - Direct Message - -Send a direct message to a specific contact using their public key. - -#### Binary Frame Format - -``` -[Command Code: 1 byte] = 0x02 -[Text Type: 1 byte] = TXT_TYPE_* (0=plain, 1=CLI, 2=signed) -[Attempt: 1 byte] = 0-3 (retry attempt number, 0 for first send) -[Sender Timestamp: 4 bytes] = uint32, Little Endian, epoch seconds -[Recipient Public Key Prefix: 6 bytes] = First 6 bytes of recipient's public key -[Text: N bytes] = UTF-8 encoded text, max 160 bytes -``` - -**Total frame size**: 12 + text_length bytes - -**Reference**: `/Users/dz0ny/meshcore-sar/MeshCore/docs/companion.md`, CMD_SEND_TXT_MSG section - -#### Implementation Example - -```dart -Future sendTextMessage(String recipientPublicKey, String text) async { - // Validate inputs - if (text.length > 160) { - throw Exception('Message exceeds 160 byte limit'); - } - - // Convert hex public key to bytes - final pubKeyBytes = hex.decode(recipientPublicKey); - if (pubKeyBytes.length != 32) { - throw Exception('Invalid public key length'); - } - - // Build frame - final writer = BufferWriter(); - writer.writeByte(2); // CMD_SEND_TXT_MSG - writer.writeByte(0); // TXT_TYPE_PLAIN - writer.writeByte(0); // Attempt 0 (first send) - writer.writeUint32(DateTime.now().millisecondsSinceEpoch ~/ 1000); // Timestamp - writer.writeBytes(pubKeyBytes.sublist(0, 6)); // First 6 bytes of public key - writer.writeString(text); // UTF-8 text - - await _sendCommand(writer.toBytes()); -} -``` - -**Reference**: `/Users/dz0ny/meshcore-sar/meshcore_sar_app/lib/services/meshcore_ble_service.dart`, lines 380-397 - -#### Response: RESP_CODE_SENT (Code 6) - -The device responds immediately with transmission details: - -``` -[Response Code: 1 byte] = 0x06 -[Send Type: 1 byte] = 0=direct route, 1=flood mode -[Expected ACK/TAG: 4 bytes] = uint32, Little Endian, code to expect in PUSH_CODE_SEND_CONFIRMED -[Suggested Timeout: 4 bytes] = uint32, Little Endian, milliseconds to wait for ACK -``` - -**Usage**: -- Store `expected_ack_or_tag` to match with future `PUSH_CODE_SEND_CONFIRMED` -- Start timer using `suggested_timeout_ms` (typically 10000-30000ms) -- If timeout expires without confirmation, consider message failed - -**Reference**: `/Users/dz0ny/meshcore-sar/MeshCore/docs/companion.md`, RESP_CODE_SENT section - -### 2.2 CMD_SEND_CHANNEL_TXT_MSG (Code 3) - Broadcast Message - -Send a message to all nodes in flood mode (public channel). - -#### Binary Frame Format - -``` -[Command Code: 1 byte] = 0x03 -[Text Type: 1 byte] = TXT_TYPE_* (0=plain, 1=CLI, 2=signed) -[Channel Index: 1 byte] = Reserved, always 0 for "public channel" -[Sender Timestamp: 4 bytes] = uint32, Little Endian, epoch seconds -[Text: N bytes] = UTF-8 encoded text, max (160 - len(advert_name) - 2) bytes -``` - -**Total frame size**: 7 + text_length bytes - -**Important**: Channel messages are **ephemeral** - they are NOT stored anywhere. Once broadcast over the air, they're gone. - -**Reference**: `/Users/dz0ny/meshcore-sar/MeshCore/docs/companion.md`, CMD_SEND_CHANNEL_TXT_MSG section - -#### Implementation Example - -```dart -Future sendChannelMessage(String text) async { - // Calculate max length based on device name - final maxLength = 160 - (_deviceName?.length ?? 0) - 2; - if (text.length > maxLength) { - throw Exception('Message exceeds $maxLength byte limit'); - } - - final writer = BufferWriter(); - writer.writeByte(3); // CMD_SEND_CHANNEL_TXT_MSG - writer.writeByte(0); // TXT_TYPE_PLAIN - writer.writeByte(0); // Channel index 0 (public) - writer.writeUint32(DateTime.now().millisecondsSinceEpoch ~/ 1000); // Timestamp - writer.writeString(text); // UTF-8 text - - await _sendCommand(writer.toBytes()); -} -``` - -**Reference**: `/Users/dz0ny/meshcore-sar/meshcore_sar_app/lib/services/meshcore_ble_service.dart`, lines 399-414 - -### 2.3 Retry Logic - -**Manual Retries** (for direct messages): - -```dart -Future sendWithRetry(String recipientPubKey, String text) async { - for (int attempt = 0; attempt < 4; attempt++) { - try { - // Modify sendTextMessage to accept attempt parameter - await sendTextMessage(recipientPubKey, text, attempt: attempt); - - // Wait for ACK or timeout - final confirmed = await waitForConfirmation(timeout: Duration(seconds: 30)); - if (confirmed) return; // Success - - print('Attempt $attempt failed, retrying...'); - } catch (e) { - print('Send failed: $e'); - } - - // Exponential backoff - await Future.delayed(Duration(seconds: 2 << attempt)); - } - - throw Exception('Message failed after 4 attempts'); -} -``` - -**Automatic Retries in MeshCore**: -- The radio layer automatically retries direct messages up to 3 times -- Each retry uses exponentially increasing delay -- Last retry attempt uses flood mode as fallback - -**Reference**: `/Users/dz0ny/meshcore-sar/MeshCore/src/Mesh.cpp`, lines 598-652 - ---- - -## 3. Receiving Messages - -### 3.1 Message Reception Flow - -``` -┌─────────────────────────────────────────────────────────────┐ -│ 1. Message arrives at device via LoRa │ -│ (from direct message or channel broadcast) │ -└────────────────┬────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 2. Device stores message in internal queue │ -│ (circular buffer, typically 50-100 messages) │ -└────────────────┬────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 3. Device sends PUSH_CODE_MSG_WAITING (0x83) │ -│ to connected app via BLE │ -└────────────────┬────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 4. App calls CMD_SYNC_NEXT_MESSAGE (10) │ -│ to fetch the message │ -└────────────────┬────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 5. Device responds with RESP_CODE_CONTACT_MSG_RECV (7) │ -│ or RESP_CODE_CHANNEL_MSG_RECV (8) │ -└────────────────┬────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 6. App parses message and displays to user │ -└────────────────┬────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 7. Repeat steps 4-6 until RESP_CODE_NO_MORE_MESSAGES (10) │ -└─────────────────────────────────────────────────────────────┘ -``` - -### 3.2 PUSH_CODE_MSG_WAITING (0x83) - New Message Notification - -When a new message arrives, the device sends this asynchronous push notification: - -``` -[Push Code: 1 byte] = 0x83 -``` - -**No additional data** - this is just a notification to call `CMD_SYNC_NEXT_MESSAGE`. - -**Implementation**: - -```dart -void _handlePushNotification(int pushCode, Uint8List data) { - switch (pushCode) { - case 0x83: // PUSH_CODE_MSG_WAITING - print('📥 New message waiting'); - onMessageWaiting?.call(); // Trigger callback - break; - // ... other push codes - } -} -``` - -**Reference**: `/Users/dz0ny/meshcore-sar/meshcore_sar_app/lib/services/meshcore_ble_service.dart`, lines 231-234 - -### 3.3 CMD_SYNC_NEXT_MESSAGE (Code 10) - Fetch Next Message - -Pull the next message from the device's queue: - -``` -[Command Code: 1 byte] = 0x0A (10) -``` - -**No parameters** - just send the command code. - -**Reference**: `/Users/dz0ny/meshcore-sar/MeshCore/docs/companion.md`, CMD_SYNC_NEXT_MESSAGE section - -#### Implementation Example - -```dart -Future syncNextMessage() async { - final writer = BufferWriter(); - writer.writeByte(10); // CMD_SYNC_NEXT_MESSAGE - await _sendCommand(writer.toBytes()); -} - -// Fetch all pending messages -Future syncAllMessages() async { - while (true) { - await syncNextMessage(); - // Wait for response (RESP_CODE_CONTACT_MSG_RECV, RESP_CODE_CHANNEL_MSG_RECV, or RESP_CODE_NO_MORE_MESSAGES) - // If NO_MORE_MESSAGES received, break loop - await Future.delayed(Duration(milliseconds: 100)); // Brief delay between fetches - } -} -``` - -**Reference**: `/Users/dz0ny/meshcore-sar/meshcore_sar_app/lib/services/meshcore_ble_service.dart`, lines 416-420 - -### 3.4 RESP_CODE_CONTACT_MSG_RECV (Code 7) - Direct Message - -Response containing a direct message from a contact: - -``` -[Response Code: 1 byte] = 0x07 -[Sender Public Key Prefix: 6 bytes] = First 6 bytes of sender's public key -[Path Length: 1 byte] = 0xFF if direct path, else hop count for flood-mode -[Text Type: 1 byte] = TXT_TYPE_* (0=plain, 1=CLI, 2=signed) -[Sender Timestamp: 4 bytes] = uint32, Little Endian, epoch seconds -[Text: N bytes] = UTF-8 encoded text (remainder of frame) -``` - -**Parsing Example**: - -```dart -void _handleContactMessage(BufferReader reader) { - final senderPubKeyPrefix = reader.readBytes(6); // First 6 bytes of sender's key - final pathLen = reader.readByte(); - final textType = reader.readByte(); - final timestamp = reader.readUint32(); - final text = reader.readString(); // Read remainder as UTF-8 - - // Find full contact by matching public key prefix - final contact = contacts.firstWhere( - (c) => c.publicKey.startsWith(hex.encode(senderPubKeyPrefix)), - orElse: () => null, - ); - - // Create message object - final message = Message( - senderPublicKey: contact?.publicKey ?? hex.encode(senderPubKeyPrefix), - senderName: contact?.name ?? 'Unknown', - text: text, - timestamp: DateTime.fromMillisecondsSinceEpoch(timestamp * 1000), - isDirect: true, - pathLength: pathLen == 0xFF ? null : pathLen, - textType: textType, - ); - - onMessageReceived?.call(message); -} -``` - -**Reference**: `/Users/dz0ny/meshcore-sar/meshcore_sar_app/lib/services/meshcore_ble_service.dart`, lines 280-301 - -### 3.5 RESP_CODE_CHANNEL_MSG_RECV (Code 8) - Channel Message - -Response containing a channel/broadcast message: - -``` -[Response Code: 1 byte] = 0x08 -[Channel Index: 1 byte] = Reserved, 0 for "public channel" -[Path Length: 1 byte] = 0xFF if direct, else hop count -[Text Type: 1 byte] = TXT_TYPE_* (0=plain, 1=CLI, 2=signed) -[Sender Timestamp: 4 bytes] = uint32, Little Endian, epoch seconds -[Text: N bytes] = UTF-8 encoded text (remainder of frame) -``` - -**Key Difference from Contact Messages**: -- **No sender public key prefix** - instead, sender name is embedded in the text -- Text format: `": "` -- Channel index currently unused (always 0) - -**Parsing Example**: - -```dart -void _handleChannelMessage(BufferReader reader) { - final channelIndex = reader.readByte(); - final pathLen = reader.readByte(); - final textType = reader.readByte(); - final timestamp = reader.readUint32(); - final text = reader.readString(); - - // Parse sender name from text (format: "Name: Message") - String senderName = 'Unknown'; - String actualMessage = text; - - if (text.contains(': ')) { - final parts = text.split(': '); - senderName = parts[0]; - actualMessage = parts.sublist(1).join(': '); // Handle multiple colons - } - - final message = Message( - senderPublicKey: null, // Unknown for channel messages - senderName: senderName, - text: actualMessage, - timestamp: DateTime.fromMillisecondsSinceEpoch(timestamp * 1000), - isDirect: false, - channelIndex: channelIndex, - pathLength: pathLen == 0xFF ? null : pathLen, - textType: textType, - ); - - onMessageReceived?.call(message); -} -``` - -**Reference**: `/Users/dz0ny/meshcore-sar/meshcore_sar_app/lib/services/meshcore_ble_service.dart`, lines 303-321 - -### 3.6 RESP_CODE_NO_MORE_MESSAGES (Code 10) - Queue Empty - -Indicates no more messages are in the queue: - -``` -[Response Code: 1 byte] = 0x0A (10) -``` - -**No additional data**. - -**Implementation**: - -```dart -void _handleNoMoreMessages() { - print('✅ All messages synced'); - _isSyncing = false; // Stop sync loop -} -``` - -**Reference**: `/Users/dz0ny/meshcore-sar/MeshCore/docs/companion.md`, RESP_CODE_NO_MORE_MESSAGES section - ---- - -## 4. Message Confirmation and ACKs - -### 4.1 PUSH_CODE_SEND_CONFIRMED (0x82) - Delivery Confirmation - -When a message is acknowledged by the recipient, the device sends this push notification: - -``` -[Push Code: 1 byte] = 0x82 -[ACK Code: 4 bytes] = uint32, Little Endian, matches expected_ack_or_tag from RESP_CODE_SENT -[Round Trip Time: 4 bytes] = uint32, Little Endian, milliseconds -``` - -**Reference**: `/Users/dz0ny/meshcore-sar/MeshCore/docs/companion.md`, PUSH_CODE_SEND_CONFIRMED section - -### 4.2 ACK Tracking Implementation - -```dart -class PendingMessage { - final String messageId; // Generate unique ID - final int expectedAck; // From RESP_CODE_SENT - final DateTime sentAt; - final int timeoutMs; - - PendingMessage({ - required this.messageId, - required this.expectedAck, - required this.sentAt, - required this.timeoutMs, - }); - - bool isExpired() { - return DateTime.now().difference(sentAt).inMilliseconds > timeoutMs; - } -} - -// Track pending messages -Map _pendingMessages = {}; - -// When sending message -void _handleSentResponse(BufferReader reader) { - final sendType = reader.readByte(); // 0=direct, 1=flood - final expectedAck = reader.readUint32(); - final timeoutMs = reader.readUint32(); - - final pending = PendingMessage( - messageId: generateMessageId(), - expectedAck: expectedAck, - sentAt: DateTime.now(), - timeoutMs: timeoutMs, - ); - - _pendingMessages[expectedAck] = pending; - - // Start timeout timer - Future.delayed(Duration(milliseconds: timeoutMs), () { - if (_pendingMessages.containsKey(expectedAck)) { - print('⚠️ Message timeout: ACK $expectedAck not received'); - _pendingMessages.remove(expectedAck); - onMessageFailed?.call(pending.messageId); - } - }); -} - -// When receiving confirmation -void _handleSendConfirmed(BufferReader reader) { - final ackCode = reader.readUint32(); - final rtt = reader.readUint32(); - - final pending = _pendingMessages.remove(ackCode); - if (pending != null) { - print('✅ Message confirmed: RTT ${rtt}ms'); - onMessageConfirmed?.call(pending.messageId, rtt); - } -} -``` - -**Reference**: Implementation pattern derived from `/Users/dz0ny/meshcore-sar/MeshCore/src/Mesh.cpp`, lines 598-652 - -### 4.3 Timeout Handling - -**Recommended Strategy**: - -1. **First attempt**: Send with `attempt=0`, wait for suggested timeout -2. **If timeout expires**: Send with `attempt=1`, wait 2× timeout -3. **If timeout expires**: Send with `attempt=2`, wait 4× timeout -4. **If timeout expires**: Send with `attempt=3` (last attempt uses flood mode) -5. **If timeout expires**: Mark message as failed - -**UI Feedback**: -- Show "Sending..." while waiting for ACK -- Show "Delivered" with RTT when confirmed -- Show "Failed" if all retries timeout -- Show "Sent" for channel messages (no ACK expected) - ---- - -## 5. Room vs Channel System - -### 5.1 Key Differences - -| Feature | Channels (Flood Mode) | Rooms (ADV_TYPE_ROOM) | -|---------|----------------------|------------------------| -| **Persistence** | ❌ Ephemeral (over-the-air only) | ✅ Persistent (stored in flash) | -| **Mutability** | N/A | ❌ Immutable (cannot delete) | -| **Authentication** | ❌ No login required | ✅ Password-protected login | -| **Message Sync** | ❌ No sync (broadcast only) | ✅ Full history sync | -| **Delivery** | ⚠️ Best-effort broadcast | ✅ Guaranteed delivery to logged-in clients | -| **Use Case** | General announcements | Mission-critical logs, SAR markers | -| **Command** | CMD_SEND_CHANNEL_TXT_MSG | CMD_SEND_TXT_MSG (to room's pub key) | -| **Channel Index** | Numeric (0=public) | Named contact (has public key) | - -**CRITICAL FOR SAR OPERATIONS**: -- **Always send SAR markers to rooms** (not public channel) -- Rooms provide immutable audit trail -- Rooms ensure messages are delivered even if recipient is offline - -**Reference**: `/Users/dz0ny/meshcore-sar/MeshCore/examples/simple_repeater/MyMesh.cpp`, lines 498-542 - -### 5.2 Room Login Protocol (CRITICAL) - -#### Step 1: Send Login Request - -``` -[Command Code: 1 byte] = 0x1A (26, CMD_SEND_LOGIN) -[Sender Timestamp: 4 bytes] = uint32, Little Endian, epoch seconds -[Sync Since: 4 bytes] = uint32, Little Endian, epoch seconds (0 for all messages) -[Room Public Key: 32 bytes] = Full 32-byte public key of room -[Password: N bytes] = UTF-8 string, max 15 bytes, null-terminated -``` - -**IMPORTANT**: Room login uses **full 32-byte public key**, not 6-byte prefix! - -**Reference**: `/Users/dz0ny/meshcore-sar/MeshCore/docs/companion.md`, CMD_SEND_LOGIN section - -#### Implementation Example - -```dart -Future loginToRoom(String roomPublicKey, String password, {int syncSince = 0}) async { - final pubKeyBytes = hex.decode(roomPublicKey); - if (pubKeyBytes.length != 32) { - throw Exception('Room login requires full 32-byte public key'); - } - - final writer = BufferWriter(); - writer.writeByte(26); // CMD_SEND_LOGIN - writer.writeUint32(DateTime.now().millisecondsSinceEpoch ~/ 1000); // Sender timestamp - writer.writeUint32(syncSince); // Sync since (0 for all messages) - writer.writeBytes(pubKeyBytes); // Full 32-byte public key - writer.writeString(password); // Password (max 15 bytes) - - await _sendCommand(writer.toBytes()); -} -``` - -**Reference**: `/Users/dz0ny/meshcore-sar/meshcore_sar_app/lib/services/meshcore_ble_service.dart`, lines 422-437 - -#### Step 2: Handle Login Response - -**Success**: `PUSH_CODE_LOGIN_SUCCESS` (0x85) - -``` -[Push Code: 1 byte] = 0x85 -[Permissions: 1 byte] = Lowest bit = is_admin (0=guest, 1=admin) -[Public Key Prefix: 6 bytes] = First 6 bytes of room's public key -[Tag: 4 bytes] = int32, Little Endian (for advanced use) -[New Permissions: 1 byte] = (Firmware v7+) Updated permission flags -``` - -**Failure**: `PUSH_CODE_LOGIN_FAIL` (0x86) - -``` -[Push Code: 1 byte] = 0x86 -[Public Key Prefix: 6 bytes] = First 6 bytes of room's public key -``` - -**Implementation**: - -```dart -void _handleLoginSuccess(BufferReader reader) { - final permissions = reader.readByte(); - final roomPubKeyPrefix = reader.readBytes(6); - final tag = reader.readInt32(); - final isAdmin = (permissions & 0x01) != 0; - - print('✅ Room login success: ${isAdmin ? "Admin" : "Guest"}'); - - // Store login state - _loggedInRooms[hex.encode(roomPubKeyPrefix)] = RoomLoginState( - isLoggedIn: true, - isAdmin: isAdmin, - loginTime: DateTime.now(), - ); - - // DO NOT call syncAllMessages() here! - // Wait for PUSH_CODE_MSG_WAITING notifications instead -} - -void _handleLoginFail(BufferReader reader) { - final roomPubKeyPrefix = reader.readBytes(6); - print('❌ Room login failed: Invalid password'); - - onRoomLoginFailed?.call(hex.encode(roomPubKeyPrefix)); -} -``` - -**Reference**: `/Users/dz0ny/meshcore-sar/meshcore_sar_app/lib/services/meshcore_ble_service.dart`, lines 236-259 - -#### Step 3: Automatic Message Push - -**CRITICAL IMPLEMENTATION RULE**: - -``` -❌ DO NOT call syncAllMessages() immediately after PUSH_CODE_LOGIN_SUCCESS -✅ DO wait for PUSH_CODE_MSG_WAITING push notifications -``` - -**Why?** - -The room server implementation has specific timing: - -```cpp -// Room server code (MyMesh.cpp:324-346) -client->extra.room.sync_since = sender_sync_since; // Store sync point -// ... send login success response ... -next_push = futureMillis(PUSH_NOTIFY_DELAY_MILLIS); // 2000ms delay -``` - -**Room Server Push Loop** (lines 498-542): - -1. Server waits 2000ms after login before first push -2. Every 1200ms (SYNC_PUSH_INTERVAL), server checks each logged-in client -3. For each client, finds next message where `post_timestamp > client->extra.room.sync_since` -4. Sends message directly to client via `PAYLOAD_TYPE_TXT_MSG` -5. Waits for ACK -6. Advances `client->extra.room.sync_since` to `post_timestamp` -7. Repeats until all messages where `timestamp > sync_since` are pushed - -**Client Implementation**: - -```dart -// When login succeeds -void _handleLoginSuccess(BufferReader reader) { - // ... parse login response ... - - // DO NOT DO THIS: - // syncAllMessages(); // ❌ WRONG - will get NO_MORE_MESSAGES too early - - // CORRECT: Just set state and wait for pushes - _loggedInRooms[roomId] = RoomLoginState(isLoggedIn: true); -} - -// When message waiting push arrives -void _handleMessageWaiting() { - // ✅ CORRECT: Now fetch the message - syncAllMessages(); -} -``` - -**Reference**: `/Users/dz0ny/meshcore-sar/MeshCore/examples/simple_repeater/MyMesh.cpp`, lines 324, 346, 498-542 - -### 5.3 Sending Messages to Rooms - -**IMPORTANT**: Use `CMD_SEND_TXT_MSG` (direct message) with the room's **6-byte public key prefix**: - -```dart -// Send SAR marker to room -Future sendSarMarkerToRoom(String roomPublicKey, String sarMarker) async { - // Use first 6 bytes of room's public key - final pubKeyBytes = hex.decode(roomPublicKey); - final pubKeyPrefix = pubKeyBytes.sublist(0, 6); - - final writer = BufferWriter(); - writer.writeByte(2); // CMD_SEND_TXT_MSG (direct message) - writer.writeByte(0); // TXT_TYPE_PLAIN - writer.writeByte(0); // Attempt 0 - writer.writeUint32(DateTime.now().millisecondsSinceEpoch ~/ 1000); - writer.writeBytes(pubKeyPrefix); // 6-byte prefix - writer.writeString(sarMarker); // e.g., "S:🧑:46.0569,14.5058" - - await _sendCommand(writer.toBytes()); -} -``` - -**Reference**: `/Users/dz0ny/meshcore-sar/meshcore_sar_app/lib/providers/connection_provider.dart`, lines 165-178 - ---- - -## 6. Binary Protocol Specifications - -### 6.1 Data Types and Byte Order - -**CRITICAL**: All multi-byte integers use **Little Endian** byte order! - -```dart -// CORRECT Little Endian implementation -void writeUint32LE(int value) { - buffer.add(value & 0xFF); // Least significant byte first - buffer.add((value >> 8) & 0xFF); - buffer.add((value >> 16) & 0xFF); - buffer.add((value >> 24) & 0xFF); // Most significant byte last -} - -uint32 readUint32LE() { - return buffer[offset] | // LSB - (buffer[offset+1] << 8) | - (buffer[offset+2] << 16) | - (buffer[offset+3] << 24); // MSB -} -``` - -**Reference**: `/Users/dz0ny/meshcore-sar/MeshCore/docs/companion.md`, Protocol Overview section - -### 6.2 Public Key Handling - -**Two different formats used**: - -| Context | Size | Usage | -|---------|------|-------| -| **Login** | 32 bytes | Full public key (CMD_SEND_LOGIN) | -| **Messages** | 6 bytes | Public key prefix (CMD_SEND_TXT_MSG) | -| **Contacts** | 32 bytes | Full public key (RESP_CODE_CONTACT) | -| **Path Return** | 32 bytes | Full public key (internal protocol) | - -**Why 6 bytes for messages?** -- Saves bandwidth (26 bytes per message) -- Collision probability: 1 in 281 trillion (2^48) -- Acceptable risk for contact lookup -- Full key stored in contacts table for validation - -**Implementation**: - -```dart -// Extract 6-byte prefix from full public key -Uint8List getPubKeyPrefix(String fullPubKey) { - final bytes = hex.decode(fullPubKey); - return Uint8List.fromList(bytes.sublist(0, 6)); -} - -// Find contact by 6-byte prefix -Contact? findContactByPrefix(Uint8List prefix) { - final prefixHex = hex.encode(prefix); - return contacts.firstWhere( - (c) => c.publicKey.startsWith(prefixHex), - orElse: () => null, - ); -} -``` - -**Reference**: `/Users/dz0ny/meshcore-sar/meshcore_sar_app/lib/services/meshcore_ble_service.dart`, lines 380-397 - -### 6.3 String Encoding - -**All text uses UTF-8 encoding**: - -```dart -// Writing strings -void writeString(String text) { - final bytes = utf8.encode(text); - buffer.addAll(bytes); - // Note: No null terminator for variable-length fields at end of frame -} - -// Reading strings (remainder of frame) -String readString() { - final bytes = buffer.sublist(offset); // Read all remaining bytes - return utf8.decode(bytes); -} - -// Reading null-terminated strings (fixed-size fields) -String readNullTerminatedString(int maxLength) { - final bytes = buffer.sublist(offset, offset + maxLength); - final nullIndex = bytes.indexOf(0); - if (nullIndex != -1) { - return utf8.decode(bytes.sublist(0, nullIndex)); - } - return utf8.decode(bytes); -} -``` - -**Reference**: `/Users/dz0ny/meshcore-sar/meshcore_sar_app/lib/services/buffer_reader.dart`, lines 38-57 - -### 6.4 Complete Frame Examples - -#### Example 1: Send "Hello" to contact - -``` -Hex dump: -02 // CMD_SEND_TXT_MSG -00 // TXT_TYPE_PLAIN -00 // Attempt 0 -E8 76 67 67 // Timestamp: 1734567912 (Little Endian) -8B 33 F2 A1 4C D9 // Public key prefix (6 bytes) -48 65 6C 6C 6F // "Hello" in UTF-8 - -Total: 18 bytes -``` - -#### Example 2: Send "Hi all" to public channel - -``` -Hex dump: -03 // CMD_SEND_CHANNEL_TXT_MSG -00 // TXT_TYPE_PLAIN -00 // Channel index 0 -E8 76 67 67 // Timestamp: 1734567912 (Little Endian) -48 69 20 61 6C 6C // "Hi all" in UTF-8 - -Total: 13 bytes -``` - -#### Example 3: Login to room - -``` -Hex dump: -1A // CMD_SEND_LOGIN -E8 76 67 67 // Sender timestamp: 1734567912 -00 00 00 00 // Sync since: 0 (all messages) -8B 33 F2 A1 4C D9 E7 22 B5 C1 3A 9F 12 45 67 89 -AB CD EF 01 23 45 67 89 AB CD EF 01 23 45 67 89 // 32-byte room public key -70 61 73 73 77 6F 72 64 00 // "password\0" (null-terminated) - -Total: 50 bytes -``` - -**Reference**: Frame formats documented in `/Users/dz0ny/meshcore-sar/MeshCore/docs/companion.md` - ---- - -## 7. Implementation Checklist - -### 7.1 Minimum Viable Implementation - -- [x] **Send direct text messages** (CMD_SEND_TXT_MSG) -- [x] **Send channel messages** (CMD_SEND_CHANNEL_TXT_MSG) -- [x] **Receive push notification** (PUSH_CODE_MSG_WAITING) -- [x] **Fetch messages** (CMD_SYNC_NEXT_MESSAGE) -- [x] **Parse contact messages** (RESP_CODE_CONTACT_MSG_RECV) -- [x] **Parse channel messages** (RESP_CODE_CHANNEL_MSG_RECV) -- [x] **Handle queue empty** (RESP_CODE_NO_MORE_MESSAGES) -- [x] **Match messages to contacts** (using 6-byte public key prefix) - -**Status**: ✅ Fully implemented in current Flutter app - -**Reference**: `/Users/dz0ny/meshcore-sar/meshcore_sar_app/lib/services/meshcore_ble_service.dart` - -### 7.2 Enhanced Implementation - -- [ ] **Track pending messages** (map expected ACK codes) -- [ ] **Handle send confirmations** (PUSH_CODE_SEND_CONFIRMED) -- [ ] **Display delivery status** (Sending/Delivered/Failed UI) -- [ ] **Implement retry logic** (4 attempts with exponential backoff) -- [ ] **Show round-trip time** (from PUSH_CODE_SEND_CONFIRMED) -- [ ] **Message timeout handling** (use suggested timeout from RESP_CODE_SENT) - -**Status**: ⚠️ Not yet implemented - -### 7.3 Room Support - -- [x] **Login to rooms** (CMD_SEND_LOGIN with 32-byte key) -- [x] **Handle login success** (PUSH_CODE_LOGIN_SUCCESS) -- [x] **Handle login failure** (PUSH_CODE_LOGIN_FAIL) -- [x] **Wait for automatic pushes** (do NOT sync immediately after login) -- [x] **Send messages to rooms** (CMD_SEND_TXT_MSG with 6-byte prefix) -- [ ] **Track room login state** (logged in, admin/guest, sync_since) -- [ ] **Re-login on reconnect** (rooms are per-session) - -**Status**: ✅ Partially implemented, needs state tracking enhancement - -**Reference**: `/Users/dz0ny/meshcore-sar/meshcore_sar_app/lib/providers/connection_provider.dart`, lines 158-182 - -### 7.4 SAR-Specific Requirements - -- [x] **Parse SAR marker format** (`S::,`) -- [x] **Highlight SAR messages** (different UI treatment) -- [x] **Send SAR markers to rooms** (NOT to public channel) -- [ ] **Validate SAR marker delivery** (wait for ACK) -- [ ] **Audit trail export** (from room message history) - -**Status**: ✅ SAR parsing implemented, ⚠️ routing needs enforcement - -**Reference**: `/Users/dz0ny/meshcore-sar/meshcore_sar_app/lib/utils/sar_message_parser.dart` - ---- - -## 8. Common Pitfalls - -### 8.1 ❌ Using Wrong Public Key Size - -**WRONG**: -```dart -// Sending message with full 32-byte key -writer.writeBytes(hex.decode(recipientPublicKey)); // 32 bytes - WRONG! -``` - -**CORRECT**: -```dart -// Sending message with 6-byte prefix -final pubKey = hex.decode(recipientPublicKey); -writer.writeBytes(pubKey.sublist(0, 6)); // 6 bytes - CORRECT -``` - -**Exception**: Room login requires full 32-byte key. - -### 8.2 ❌ Wrong Byte Order (Big Endian vs Little Endian) - -**WRONG**: -```dart -// Big Endian (MSB first) -buffer.add((timestamp >> 24) & 0xFF); // MSB -buffer.add((timestamp >> 16) & 0xFF); -buffer.add((timestamp >> 8) & 0xFF); -buffer.add(timestamp & 0xFF); // LSB -``` - -**CORRECT**: -```dart -// Little Endian (LSB first) -buffer.add(timestamp & 0xFF); // LSB first -buffer.add((timestamp >> 8) & 0xFF); -buffer.add((timestamp >> 16) & 0xFF); -buffer.add((timestamp >> 24) & 0xFF); // MSB last -``` - -### 8.3 ❌ Calling syncAllMessages() After Room Login - -**WRONG**: -```dart -void _handleLoginSuccess(BufferReader reader) { - // ... parse response ... - syncAllMessages(); // ❌ WRONG - room hasn't pushed messages yet! -} -``` - -**CORRECT**: -```dart -void _handleLoginSuccess(BufferReader reader) { - // ... parse response ... - // Just set state and wait for PUSH_CODE_MSG_WAITING - _loggedInRooms[roomId] = RoomLoginState(isLoggedIn: true); -} - -// Sync when notified -void _handleMessageWaiting() { - syncAllMessages(); // ✅ CORRECT - room has pushed message -} -``` - -### 8.4 ❌ Not Handling Message Queue Loop - -**WRONG**: -```dart -// Only fetch one message -await syncNextMessage(); -``` - -**CORRECT**: -```dart -// Fetch ALL messages until queue is empty -Future syncAllMessages() async { - while (true) { - await syncNextMessage(); - // The response handler will set _hasMoreMessages = false when NO_MORE_MESSAGES received - if (!_hasMoreMessages) break; - await Future.delayed(Duration(milliseconds: 100)); - } -} -``` - -### 8.5 ❌ Exceeding Message Length Limits - -**WRONG**: -```dart -// Sending 200-byte message -await sendTextMessage(recipientKey, longMessage); // Will fail! -``` - -**CORRECT**: -```dart -// Validate length before sending -Future sendTextMessage(String recipientKey, String text) async { - if (text.length > 160) { - throw Exception('Message exceeds 160 byte limit'); - } - // ... send message ... -} - -// Or split into multiple messages -void sendLongMessage(String recipientKey, String text) { - final chunks = _splitIntoChunks(text, 160); - for (final chunk in chunks) { - await sendTextMessage(recipientKey, chunk); - await Future.delayed(Duration(milliseconds: 500)); // Spacing between chunks - } -} -``` - -### 8.6 ❌ Sending SAR Markers to Public Channel - -**WRONG**: -```dart -// SAR marker sent to ephemeral public channel -await sendChannelMessage('S:🧑:46.0569,14.5058'); // ❌ NOT PERSISTENT! -``` - -**CORRECT**: -```dart -// SAR marker sent to persistent room -final room = contacts.firstWhere((c) => c.type == ContactType.room); -await sendTextMessage(room.publicKey, 'S:🧑:46.0569,14.5058'); // ✅ PERSISTENT -``` - -### 8.7 ❌ Not Matching Contacts by Public Key Prefix - -**WRONG**: -```dart -// Exact match on 6-byte prefix (will fail if contact has full 32-byte key) -final contact = contacts.firstWhere( - (c) => c.publicKey == hex.encode(pubKeyPrefix), -); -``` - -**CORRECT**: -```dart -// Prefix match (works with full or partial keys) -final prefixHex = hex.encode(pubKeyPrefix); -final contact = contacts.firstWhere( - (c) => c.publicKey.startsWith(prefixHex), - orElse: () => null, -); -``` - ---- - -## 9. Testing and Validation - -### 9.1 Unit Tests - -```dart -// Test message frame building -test('Build CMD_SEND_TXT_MSG frame correctly', () { - final writer = BufferWriter(); - writer.writeByte(2); // CMD_SEND_TXT_MSG - writer.writeByte(0); // TXT_TYPE_PLAIN - writer.writeByte(0); // Attempt 0 - writer.writeUint32(1734567912); // Timestamp - writer.writeBytes(hex.decode('8B33F2A14CD9')); // 6-byte pub key - writer.writeString('Hello'); - - final expected = [ - 0x02, 0x00, 0x00, - 0xE8, 0x76, 0x67, 0x67, // Little Endian timestamp - 0x8B, 0x33, 0xF2, 0xA1, 0x4C, 0xD9, - 0x48, 0x65, 0x6C, 0x6C, 0x6F, // "Hello" - ]; - - expect(writer.toBytes(), equals(expected)); -}); - -// Test message parsing -test('Parse RESP_CODE_CONTACT_MSG_RECV correctly', () { - final frame = Uint8List.fromList([ - 0x07, // RESP_CODE_CONTACT_MSG_RECV - 0x8B, 0x33, 0xF2, 0xA1, 0x4C, 0xD9, // Sender pub key prefix - 0xFF, // Path length (direct) - 0x00, // TXT_TYPE_PLAIN - 0xE8, 0x76, 0x67, 0x67, // Timestamp (Little Endian) - 0x48, 0x69, // "Hi" - ]); - - final reader = BufferReader(frame); - reader.readByte(); // Skip response code - - final pubKeyPrefix = reader.readBytes(6); - final pathLen = reader.readByte(); - final textType = reader.readByte(); - final timestamp = reader.readUint32(); - final text = reader.readString(); - - expect(hex.encode(pubKeyPrefix), equals('8b33f2a14cd9')); - expect(pathLen, equals(0xFF)); - expect(textType, equals(0)); - expect(timestamp, equals(1734567912)); - expect(text, equals('Hi')); -}); -``` - -### 9.2 Integration Tests - -```dart -// Test complete message flow -testWidgets('Send and receive message flow', (tester) async { - final service = MeshCoreBleService(); - - // Setup callbacks - Message? receivedMessage; - service.onMessageReceived = (msg) => receivedMessage = msg; - - int? expectedAck; - service.onSentResponse = (ack, timeout) => expectedAck = ack; - - bool confirmed = false; - service.onSendConfirmed = (ack, rtt) => confirmed = true; - - // Send message - await service.sendTextMessage(testContactPubKey, 'Test message'); - await tester.pump(); - - // Verify RESP_CODE_SENT received - expect(expectedAck, isNotNull); - - // Simulate PUSH_CODE_SEND_CONFIRMED - final confirmFrame = Uint8List.fromList([ - 0x82, // PUSH_CODE_SEND_CONFIRMED - ...encodeUint32LE(expectedAck!), - 0x10, 0x27, 0x00, 0x00, // RTT: 10000ms - ]); - service.simulateIncomingData(confirmFrame); - await tester.pump(); - - // Verify confirmation received - expect(confirmed, isTrue); -}); -``` - -### 9.3 Manual Testing Checklist - -**Basic Messaging**: -- [ ] Send direct message to contact -- [ ] Receive direct message from contact -- [ ] Send channel message to public -- [ ] Receive channel message from public -- [ ] Messages display with correct sender name -- [ ] Messages display with correct timestamp - -**Message Delivery**: -- [ ] Verify RESP_CODE_SENT received after sending -- [ ] Verify PUSH_CODE_SEND_CONFIRMED received after ACK -- [ ] Verify timeout triggers if no ACK -- [ ] Verify retry logic works (manual test with device off) - -**Room Operations**: -- [ ] Login to room with correct password -- [ ] Login fails with wrong password -- [ ] Messages automatically sync after login (wait for push) -- [ ] Send message to room (appears for other logged-in clients) -- [ ] Room messages persist (logout, login, verify history) - -**SAR Markers**: -- [ ] SAR marker sent to room (not channel) -- [ ] SAR marker parsed correctly -- [ ] SAR marker appears on map -- [ ] SAR marker delivery confirmed - -**Edge Cases**: -- [ ] Message at 160-byte limit sends successfully -- [ ] Message over 160 bytes rejected -- [ ] Message to unknown contact handled gracefully -- [ ] Multiple rapid messages queued correctly -- [ ] Message sync handles empty queue (NO_MORE_MESSAGES) - ---- - -## 10. Current Implementation Status - -### 10.1 What's Working ✅ - -Based on review of the Flutter app code: - -1. **`meshcore_ble_service.dart`**: ✅ All protocol implementations correct - - `sendTextMessage()` uses 6-byte public key prefix - - `sendChannelMessage()` uses correct format - - `loginToRoom()` sends with sync_since parameter - - `_handleLoginSuccess()` does NOT call syncNextMessage - - Message parsing handles signed messages correctly - -2. **`connection_provider.dart`**: ✅ Message sync logic correct - - Waits for `PUSH_CODE_MSG_WAITING` before syncing - - Calls `syncAllMessages()` when notified - - Room login state tracking implemented - -3. **`messages_tab.dart`**: ✅ SAR marker routing options available - - Allows users to choose between channel (ephemeral) and room (persistent) - - Both sending methods implemented correctly - -### 10.2 What's Missing ⚠️ - -1. **ACK Tracking**: - - App doesn't track expected ACK codes from `RESP_CODE_SENT` - - Missing `PUSH_CODE_SEND_CONFIRMED` handling - - No delivery confirmation UI - -2. **Retry Logic**: - - No automatic retry on timeout - - No exponential backoff - - No manual retry UI - -3. **Room State Management**: - - Room login state not persisted across app restarts - - No UI indication of logged-in rooms - - No automatic re-login on reconnect - -### 10.3 Recommendations - -**Priority 1 (High Impact)**: -1. Implement ACK tracking and delivery confirmation UI -2. Add timeout handling with retry logic -3. Enforce SAR marker routing to rooms (not channel) - -**Priority 2 (Enhancements)**: -1. Persist room login state -2. Add auto-reconnect for rooms -3. Show RTT in message UI - -**Priority 3 (Nice to Have)**: -1. Message read receipts (if protocol supports) -2. Message editing/deletion (if protocol supports) -3. Message search and filtering - ---- - -## File References - -| File | Description | -|------|-------------| -| `/Users/dz0ny/meshcore-sar/MeshCore/docs/companion.md` | Official protocol documentation | -| `/Users/dz0ny/meshcore-sar/MeshCore/src/MeshCore.h` | Protocol constants and definitions | -| `/Users/dz0ny/meshcore-sar/MeshCore/src/Mesh.cpp` | Core message routing logic | -| `/Users/dz0ny/meshcore-sar/MeshCore/examples/simple_repeater/MyMesh.cpp` | Room server implementation | -| `/Users/dz0ny/meshcore-sar/meshcore_sar_app/lib/services/meshcore_ble_service.dart` | Flutter BLE service | -| `/Users/dz0ny/meshcore-sar/meshcore_sar_app/lib/providers/connection_provider.dart` | Message sync provider | -| `/Users/dz0ny/meshcore-sar/meshcore_sar_app/lib/screens/messages_tab.dart` | Messages UI | - ---- - -**Document Version**: 1.0 -**Date**: 2025-10-14 -**Protocol Version**: MeshCore Companion Radio v3-v7 -**Implementation Status**: Production-ready with recommended enhancements - ---- - -## Summary - -This guide provides complete specifications for implementing messaging in MeshCore applications. The key takeaways: - -1. **Two message types**: Direct (to contact) and Channel (broadcast) -2. **Two delivery modes**: Ephemeral (channels) and Persistent (rooms) -3. **Critical protocol details**: Little Endian, 6-byte vs 32-byte keys, UTF-8 encoding -4. **Room login flow**: Send login → wait for success → wait for pushes → sync messages -5. **SAR requirement**: Always send SAR markers to rooms for persistence -6. **Current implementation**: Mostly correct, missing ACK tracking and retry logic - -The Flutter app's current implementation follows the protocol correctly. The main enhancement needed is ACK tracking for delivery confirmation UI. diff --git a/ROOM_LOGIN_FIX.md b/ROOM_LOGIN_FIX.md deleted file mode 100644 index 8a4e231..0000000 --- a/ROOM_LOGIN_FIX.md +++ /dev/null @@ -1,268 +0,0 @@ -# Fixing "Not Found" Error When Logging Into Room - -## Your Current Situation - -You're seeing this sequence: -``` -✅ Room "Repetitor" found in app contacts -📤 Sending SEND_LOGIN command -❌ Companion radio responds: ERR_CODE_NOT_FOUND (2) -``` - -## Root Cause - -Your **Flutter app** and the **companion radio firmware** maintain **separate contact lists**: - -``` -┌─────────────────────────┐ ┌──────────────────────────┐ -│ Flutter App │ │ Companion Radio │ -│ (ContactsProvider) │ │ (Firmware Storage) │ -├─────────────────────────┤ ├──────────────────────────┤ -│ │ │ │ -│ ✅ Repetitor │ │ ❌ Repetitor │ -│ 15:59:89:54:b4:d4 │ BLE │ (NOT FOUND!) │ -│ │ <───> │ │ -│ Other contacts... │ │ Other contacts... │ -│ │ │ │ -└─────────────────────────┘ └──────────────────────────┘ -``` - -When you call `getContacts()`: -- The companion radio sends you a **snapshot** of its contact table -- Your app stores these contacts locally -- But if the radio's contact table changes, your app doesn't know - -**The problem:** The room "Repetitor" exists in your app (from an old sync), but NOT in the radio's firmware anymore. - -## Why This Happens - -1. **Companion radio was factory reset** - Erased all contacts -2. **Contact was manually removed** - Via serial console or config tool -3. **Room never advertised** - Contact was temporary, never saved persistently -4. **Firmware bug** - Contact wasn't properly persisted to flash storage - -## Solutions - -### Solution 1: Force Re-Sync Contacts (Quick Test) - -This will clear your app's contacts and re-fetch from the radio: - -```dart -// In your app, add a button to force full sync: -await contactsProvider.clearContacts(); -await connectionProvider.getContacts(); -await Future.delayed(Duration(milliseconds: 1000)); - -// Now check what rooms exist: -final rooms = contactsProvider.rooms; -print('Rooms on device: ${rooms.length}'); -for (final room in rooms) { - print(' - ${room.advName}'); -} -``` - -If "Repetitor" is NOT in the list after this sync, then the radio truly doesn't have it. - -### Solution 2: Wait for Room to Advertise (Automatic) - -If the room server is running and broadcasting: - -1. The companion radio will receive the advertisement over LoRa -2. If `manual_add_contacts=0`, you'll automatically receive `PUSH_CODE_NEW_ADVERT` (0x8A) -3. The room will be added to both the radio AND your app -4. Then you can login - -**Expected flow:** -``` -Room broadcasts → Companion receives → PUSH_CODE_NEW_ADVERT → Contact added → Login works -``` - -### Solution 3: Manually Add Room Contact (CMD_ADD_UPDATE_CONTACT) - -This requires implementing `CMD_ADD_UPDATE_CONTACT` (command code 9) in your app. - -**Add this to `MeshCoreBleService`:** - -```dart -/// Manually add or update a contact on the companion radio -/// -/// This is useful when you need to add a room that hasn't advertised yet, -/// or restore a contact that was deleted from the radio's table. -/// -/// Protocol format (CMD_ADD_UPDATE_CONTACT): -/// - 1 byte: 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) -Future addOrUpdateContact(Contact contact) async { - final writer = BufferWriter(); - writer.writeByte(MeshCoreConstants.cmdAddUpdateContact); // 0x09 - writer.writeBytes(contact.publicKey); // 32 bytes - writer.writeByte(contact.type.value); // ADV_TYPE_* - writer.writeByte(contact.flags); // flags - writer.writeInt8(contact.outPathLen); // path length (signed byte) - writer.writeBytes(contact.outPath); // 64 bytes - - // Write name as null-terminated string in 32-byte field - final nameBytes = Uint8List(32); - final encoded = utf8.encode(contact.advName); - final copyLen = encoded.length > 31 ? 31 : encoded.length; - nameBytes.setRange(0, copyLen, encoded); - writer.writeBytes(nameBytes); - - writer.writeUInt32LE(contact.lastAdvert); // timestamp - writer.writeInt32LE(contact.advLat); // latitude * 1E6 - writer.writeInt32LE(contact.advLon); // longitude * 1E6 - - await _writeData(writer.toBytes()); - - print('✅ [BLE] Sent CMD_ADD_UPDATE_CONTACT for ${contact.advName}'); - print(' This adds the contact to the radio\'s internal table'); -} -``` - -Add the constant: -```dart -// In meshcore_constants.dart -static const int cmdAddUpdateContact = 9; -``` - -Then in your app, before login: -```dart -// Add the room contact to the radio's table -await connectionProvider.bleService.addOrUpdateContact(widget.contact); - -// Small delay to allow radio to save -await Future.delayed(Duration(milliseconds: 300)); - -// Now login should work -await connectionProvider.loginToRoom(...); -``` - -### Solution 4: Import Room Contact Card - -If you have the room's "business card" (from `CMD_EXPORT_CONTACT`): - -1. Get the card data (usually starts with `meshcore://`) -2. Implement `CMD_IMPORT_CONTACT` (command code 18) -3. Import the card - -```dart -Future importContact(String cardData) async { - final writer = BufferWriter(); - writer.writeByte(MeshCoreConstants.cmdImportContact); // 0x12 - writer.writeString(cardData); // meshcore:// card data - await _writeData(writer.toBytes()); -} -``` - -## Recommended Approach - -**Step 1:** Force re-sync to see current state -```dart -await contactsProvider.clearContacts(); -await connectionProvider.getContacts(); -``` - -**Step 2:** Check if room exists -```dart -final roomExists = contactsProvider.rooms.any( - (r) => r.publicKeyPrefix.matches(targetPrefix) -); -``` - -**Step 3a:** If room doesn't exist → **Implement CMD_ADD_UPDATE_CONTACT** (Solution 3) - -**Step 3b:** Or wait for room to advertise (Solution 2) - -## Implementation Priority - -### Immediate Fix (Easiest) -1. ✅ Add contact sync verification (already done!) -2. ✅ Show helpful error messages (already done!) - -### Short Term (Recommended) -3. 🔧 **Implement `CMD_ADD_UPDATE_CONTACT`** - This lets you manually restore contacts -4. 🔧 **Implement `CMD_EXPORT_CONTACT`** - This lets you backup/share room contacts - -### Long Term (Optional) -5. 📱 Add UI to manually add rooms by public key -6. 💾 Cache room contacts in SharedPreferences -7. 🔄 Auto-restore cached rooms on connect - -## Testing Your Fix - -1. **Clear app contacts:** - ```dart - await contactsProvider.clearContacts(); - ``` - -2. **Force fresh sync:** - ```dart - await connectionProvider.getContacts(); - await Future.delayed(Duration(seconds: 1)); - ``` - -3. **List actual rooms on device:** - ```dart - final rooms = contactsProvider.rooms; - print('═══════════════════════════════'); - print('Rooms on companion radio: ${rooms.length}'); - for (final room in rooms) { - print('📍 ${room.advName}'); - print(' PK: ${room.publicKeyPrefix.map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}'); - print(' Type: ${room.type}'); - } - print('═══════════════════════════════'); - ``` - -4. **If "Repetitor" is NOT in the list:** - - The radio truly doesn't have it - - You need to add it using CMD_ADD_UPDATE_CONTACT - - Or wait for it to advertise - -## Expected Logs After Fix - -**Before (Current - Broken):** -``` -🔍 Checking room "Repetitor"... - App contacts: ✅ Found - Radio contacts: ❌ Not found -📤 Sending LOGIN -❌ ERROR: Not found -``` - -**After (Fixed - Option 1: Re-sync):** -``` -🔍 Checking room "Repetitor"... -📤 Clearing app contacts -📤 Syncing from radio -📥 Got 5 contacts - Room "Repetitor": ❌ NOT on radio -⚠️ Room needs to be added to radio first -``` - -**After (Fixed - Option 2: Manual add):** -``` -🔍 Checking room "Repetitor"... - App contacts: ✅ Found - Radio contacts: ❌ Not found -📤 Sending CMD_ADD_UPDATE_CONTACT -✅ Contact added to radio -📤 Sending LOGIN -✅ LOGIN_SUCCESS -``` - -## Next Steps - -1. Try Solution 1 (force re-sync) to confirm the issue -2. Implement Solution 3 (CMD_ADD_UPDATE_CONTACT) for permanent fix -3. Test by adding the room contact manually before login - -Would you like me to implement `CMD_ADD_UPDATE_CONTACT` for you? diff --git a/ROOM_LOGIN_REVIEW.md b/ROOM_LOGIN_REVIEW.md deleted file mode 100644 index ceeb062..0000000 --- a/ROOM_LOGIN_REVIEW.md +++ /dev/null @@ -1,851 +0,0 @@ -# Room Login Implementation Review - -**Date**: 2025-01-14 -**Reviewer**: AI Code Analysis -**Status**: ✅ PRODUCTION READY - -## Executive Summary - -After comprehensive review of the room login implementation for both **cold start** (automatic login) and **user-initiated login** (manual login via UI), the implementation is **CORRECT** and follows the MeshCore protocol specifications exactly. - -### Key Findings - -- ✅ **Protocol Compliance**: All BLE frame formats are correct -- ✅ **Cold Start Works**: Auto-login on connection is properly implemented -- ✅ **User Login Works**: Manual login with pre-flight checks -- ✅ **No Premature Sync**: Code does NOT call `syncAllMessages()` immediately after login -- ✅ **Message Push Handling**: Correctly waits for `PUSH_CODE_MSG_WAITING` notifications -- ⚠️ **CMD_ADD_UPDATE_CONTACT Already Implemented**: Code already has this functionality! - ---- - -## 1. Cold Start Auto-Login Review - -### 1.1 Entry Point - -**File**: `lib/providers/app_provider.dart` -**Method**: `initialize()` → `_autoLoginToRooms()` (lines 88-153) - -### 1.2 Flow Diagram - -``` -App Starts - ↓ -connect(device) - ↓ -initialize() ← Called after connection established - ↓ -├─ syncDeviceTime() -├─ getContacts() -├─ delay(500ms) -└─ _autoLoginToRooms() - ↓ - ├─ Get all rooms (exclude "Public Channel") - ├─ For each room: - │ ├─ Load saved password (or "hello" default) - │ ├─ _loginToRoomWithCallback() - │ │ ├─ Setup temporary callbacks - │ │ ├─ connectionProvider.loginToRoom() - │ │ ├─ Wait for PUSH_CODE_LOGIN_SUCCESS/FAIL - │ │ └─ Restore original callbacks - │ └─ Delay 300ms between logins - └─ _syncMessages() ← Syncs pre-existing messages from device queue -``` - -### 1.3 Code Review - -#### ✅ Password Loading (lines 131-137) - -```dart -for (final room in rooms) { - try { - // Load saved password for this room - final roomKey = 'room_password_${room.publicKeyHex}'; - final savedPassword = prefs.getString(roomKey) ?? 'hello'; -``` - -**Analysis**: -- Uses SharedPreferences with room-specific keys -- Falls back to "hello" if no saved password -- Correct implementation - -#### ✅ Login with Callback Wrapper (lines 142-145) - -```dart -// Set up one-time callbacks for this room login -await _loginToRoomWithCallback(room, savedPassword); - -// Small delay between logins to avoid overwhelming the device -await Future.delayed(const Duration(milliseconds: 300)); -``` - -**Analysis**: -- Uses temporary callbacks per room (prevents callback mixing) -- 300ms spacing prevents BLE command queue overflow -- Correct implementation - -#### ✅ SUCCESS Handler - NO Premature Sync! (lines 165-173) - -```dart -connectionProvider.onLoginSuccess = (publicKeyPrefix, permissions, isAdmin, tag) async { - // Restore original callbacks - connectionProvider.onLoginSuccess = originalOnSuccess; - connectionProvider.onLoginFail = originalOnFail; - - debugPrint('✅ [AppProvider] Auto-login successful for ${room.advName}'); - debugPrint('📡 [AppProvider] Room server will push messages automatically via PUSH_CODE_MSG_WAITING'); - - completer.complete(true); -}; -``` - -**CRITICAL ANALYSIS**: -- ❌ **DOES NOT** call `syncAllMessages()` -- ❌ **DOES NOT** call `syncNextMessage()` -- ✅ **DOES** print message about automatic message push -- ✅ **CORRECT** per protocol specification - -#### ✅ Message Waiting Handler (connection_provider.dart:125-129) - -```dart -_bleService.onMessageWaiting = () { - print('📥 [Provider] Received MsgWaiting push - auto-fetching messages'); - // Automatically fetch messages when push notification received - syncAllMessages(); -}; -``` - -**Analysis**: -- Only syncs when `PUSH_CODE_MSG_WAITING` (0x83) is received -- This is triggered by room server pushing messages -- Correct implementation per protocol - ---- - -## 2. User-Initiated Login Review - -### 2.1 Entry Point - -**File**: `lib/screens/contacts_tab.dart` -**Method**: `_RoomLoginSheetState._loginToRoom()` (lines 983-1201) - -### 2.2 Flow Diagram - -``` -User clicks "Login to Room" - ↓ -_RoomLoginSheet shown - ↓ -Load saved password (or "hello") - ↓ -User clicks "Login" button - ↓ -_loginToRoom() - ↓ -├─ 🕐 CLOCK DRIFT CHECK (lines 1006-1015) -│ └─ getDeviceTime() - diagnostic only -│ -├─ 🔍 PRE-LOGIN CHECK (lines 1018-1113) -│ ├─ Check: Room in ContactsProvider? -│ │ ├─ YES → Continue -│ │ └─ NO → Sync contacts from device -│ │ ├─ getContacts() -│ │ ├─ Wait 800ms -│ │ ├─ Check again -│ │ │ ├─ Found → Continue -│ │ │ └─ Not Found → ADD MANUALLY -│ │ │ ├─ addOrUpdateContact(room) -│ │ │ ├─ Wait 500ms for flash write -│ │ │ └─ Continue -│ │ └─ Log available rooms for debugging -│ │ -├─ 💾 SAVE PASSWORD (line 1116) -│ └─ SharedPreferences.setString(roomKey, password) -│ -├─ 🔧 SETUP CALLBACKS (lines 1118-1161) -│ ├─ Store original callbacks -│ ├─ Set temporary onLoginSuccess -│ │ └─ Does NOT call syncAllMessages() ← CRITICAL -│ └─ Set temporary onLoginFail -│ -├─ 📤 SEND LOGIN REQUEST (lines 1165-1168) -│ └─ connectionProvider.loginToRoom() -│ -└─ 📥 WAIT FOR RESPONSE - ├─ PUSH_CODE_LOGIN_SUCCESS (0x85) - │ └─ Show: "Logged in successfully! Waiting for room messages..." - └─ PUSH_CODE_LOGIN_FAIL (0x86) - └─ Show: "Login failed - incorrect password" -``` - -### 2.3 Code Review - -#### ✅ Clock Drift Check (lines 1006-1015) - -```dart -// 🕐 CLOCK DRIFT CHECK: Get device time to detect synchronization issues -print('🕐 [RoomLogin] Checking for clock drift between app and radio...'); -try { - await connectionProvider.getDeviceTime(); - await Future.delayed(const Duration(milliseconds: 300)); -} catch (e) { - print('⚠️ [RoomLogin] Failed to get device time: $e'); - // Don't fail login - this is just a diagnostic check -} -``` - -**Analysis**: -- Diagnostic check only, doesn't fail on error -- Response logged in `meshcore_ble_service.dart:1015-1048` -- Good practice for troubleshooting -- Correct implementation - -#### ✅ Pre-Login Contact Check (lines 1018-1113) - -```dart -// 🔍 PRE-LOGIN CHECK: Ensure room contact exists in device -print('🔍 [RoomLogin] Checking if room "${widget.contact.advName}" exists in contacts...'); - -bool roomExists = contactsProvider.rooms.any( - (room) => room.publicKeyHex == widget.contact.publicKeyHex, -); - -if (!roomExists) { - // Try syncing contacts - await connectionProvider.getContacts(); - await Future.delayed(const Duration(milliseconds: 800)); - - // Check again - roomExists = contactsProvider.rooms.any(...); - - if (!roomExists) { - // Manually add the room contact to the radio - try { - await connectionProvider.addOrUpdateContact(widget.contact); - await Future.delayed(const Duration(milliseconds: 500)); - } catch (e) { - // Show error snackbar and exit - return; - } - } -} -``` - -**Analysis**: -- ✅ Checks if room exists before login -- ✅ Attempts sync if not found -- ✅ Falls back to manual add via `CMD_ADD_UPDATE_CONTACT` -- ✅ Shows user-friendly error messages -- ✅ Solves `ERR_CODE_NOT_FOUND` issue -- **EXCELLENT** implementation - -#### ✅ Login Success Handler - NO Premature Sync! (lines 1125-1143) - -```dart -connectionProvider.onLoginSuccess = (publicKeyPrefix, permissions, isAdmin, tag) async { - // Restore original callback - connectionProvider.onLoginSuccess = originalOnSuccess; - connectionProvider.onLoginFail = originalOnFail; - - print('✅ [RoomLogin] Login successful! Tag: $tag, Permissions: $permissions, Admin: $isAdmin'); - print('📡 [RoomLogin] Room server will now push messages automatically via PUSH_CODE_MSG_WAITING'); - print(' Messages will be fetched when onMessageWaiting callback is triggered'); - - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Logged in successfully! Waiting for room messages...'), - backgroundColor: Colors.green, - ), - ); - } -}; -``` - -**CRITICAL ANALYSIS**: -- ❌ **DOES NOT** call `syncAllMessages()` -- ❌ **DOES NOT** call `syncNextMessage()` -- ✅ **DOES** print detailed message about automatic push -- ✅ **DOES** show user-friendly success message -- ✅ **CORRECT** per protocol specification - ---- - -## 3. Protocol Compliance Review - -### 3.1 CMD_SEND_LOGIN Implementation - -**File**: `lib/services/meshcore_ble_service.dart:1390-1416` - -```dart -Future loginToRoom({ - required Uint8List roomPublicKey, - required String password, - int syncSince = 0, // 0 = get all messages -}) async { - if (password.length > 15) { - throw ArgumentError('Password exceeds 15 character limit'); - } - - final now = DateTime.now().millisecondsSinceEpoch ~/ 1000; // epoch seconds - - final writer = BufferWriter(); - writer.writeByte(MeshCoreConstants.cmdSendLogin); // 0x1A (26) - writer.writeUInt32LE(now); // sender timestamp - writer.writeUInt32LE(syncSince); // sync since - writer.writeBytes(roomPublicKey); // 32 bytes - writer.writeString(password); // max 15 bytes - await _writeData(writer.toBytes()); -} -``` - -### 3.2 Frame Structure Verification - -**Documented Format** (MESSAGES.md): -``` -[0x1A] - Command code (26) -[4 bytes] - Sender timestamp (uint32 LE) -[4 bytes] - Sync since (uint32 LE) -[32 bytes] - Room public key -[N bytes] - Password (max 15, null-terminated) -``` - -**Actual Implementation**: -``` -Byte 0: 0x1A ✅ Correct -Bytes 1-4: now (uint32 LE) ✅ Correct -Bytes 5-8: syncSince (uint32 LE) ✅ Correct -Bytes 9-40: roomPublicKey (32) ✅ Correct -Bytes 41+: password (UTF-8) ✅ Correct -``` - -**VERDICT**: ✅ 100% Protocol Compliant - -### 3.3 Response Handlers - -#### PUSH_CODE_LOGIN_SUCCESS (0x85) - -**File**: `meshcore_ble_service.dart:942-981` - -```dart -void _handleLoginSuccess(BufferReader reader) { - if (reader.remainingBytesCount >= 11) { - final permissions = reader.readByte(); - final isAdmin = (permissions & 0x01) != 0; - final publicKeyPrefix = reader.readBytes(6); - final tag = reader.readInt32LE(); - - // V7+ new permissions byte (optional) - int? newPermissions; - if (reader.hasRemaining) { - newPermissions = reader.readByte(); - } - - onLoginSuccess?.call(publicKeyPrefix, permissions, isAdmin, tag); - } -} -``` - -**Analysis**: -- ✅ Parses all documented fields -- ✅ Handles optional V7+ permissions -- ✅ Calls callback with correct parameters -- ✅ **DOES NOT** call any message sync methods -- **CORRECT** implementation - -#### PUSH_CODE_LOGIN_FAIL (0x86) - -**File**: `meshcore_ble_service.dart:983-1009` - -```dart -void _handleLoginFail(BufferReader reader) { - if (reader.remainingBytesCount >= 7) { - final reserved = reader.readByte(); - final publicKeyPrefix = reader.readBytes(6); - onLoginFail?.call(publicKeyPrefix); - } -} -``` - -**Analysis**: -- ✅ Parses reserved byte + 6-byte prefix -- ✅ Calls callback -- **CORRECT** implementation - ---- - -## 4. Message Push Protocol Review - -### 4.1 Room Server Behavior (Reference) - -**Source**: `/Users/dz0ny/meshcore-sar/MeshCore/examples/simple_repeater/MyMesh.cpp` - -```cpp -// Login handler (line 324) -client->extra.room.sync_since = sender_sync_since; - -// Set delay before first push (line 346) -next_push = futureMillis(PUSH_NOTIFY_DELAY_MILLIS); // 2000ms - -// Round-robin polling loop (lines 498-542) -if (post->timestamp > client->extra.room.sync_since) { - pushPostToClient(client, post); // Send PAYLOAD_TYPE_TXT_MSG - // Wait for ACK... - client->extra.room.sync_since = post->timestamp; -} -``` - -### 4.2 Flutter App Message Reception - -**File**: `connection_provider.dart:125-129` - -```dart -_bleService.onMessageWaiting = () { - print('📥 [Provider] Received MsgWaiting push - auto-fetching messages'); - syncAllMessages(); -}; -``` - -### 4.3 Protocol Flow - -``` -Room Server Companion Radio Flutter App - | | | - | LOGIN_SUCCESS | | - |------------------------->|-------------------->| - | | | ✅ onLoginSuccess() called - | | | ❌ Does NOT call syncAllMessages() - | | | - | [Wait 2000ms] | | - | | | - | PAYLOAD_TYPE_TXT_MSG | | - |------------------------->| | - | (direct to client) | | - | | | - | | PUSH_CODE_MSG_WAITING| - | |-------------------->| ✅ Now sync is called! - | | | - | |<---- CMD_SYNC_NEXT --| - | | | - | |---- CONTACT_MSG ---->| - | | | - |<------ ACK -------------| | - | | | - | [Next message...] | | -``` - -**VERDICT**: ✅ Implementation matches protocol exactly - ---- - -## 5. CMD_ADD_UPDATE_CONTACT Implementation - -### 5.1 Discovery - -**File**: `lib/services/meshcore_ble_service.dart:1123-1172` - -```dart -/// Manually add or update a contact on the companion radio -Future addOrUpdateContact(Contact contact) async { - print('📝 [BLE] Adding/updating contact on companion radio:'); - - final writer = BufferWriter(); - writer.writeByte(MeshCoreConstants.cmdAddUpdateContact); // 0x09 - writer.writeBytes(contact.publicKey); // 32 bytes - writer.writeByte(contact.type.value); - writer.writeByte(contact.flags); - writer.writeInt8(contact.outPathLen); - writer.writeBytes(contact.outPath); // 64 bytes - - // Write name as null-terminated string in 32-byte field - final nameBytes = Uint8List(32); - final encoded = utf8.encode(contact.advName); - final copyLen = encoded.length > 31 ? 31 : encoded.length; - nameBytes.setRange(0, copyLen, encoded); - writer.writeBytes(nameBytes); - - writer.writeUInt32LE(contact.lastAdvert); - writer.writeInt32LE(contact.advLat); - writer.writeInt32LE(contact.advLon); - - await _writeData(writer.toBytes()); -} -``` - -### 5.2 Usage in User Login (contacts_tab.dart:1050-1060) - -```dart -// Manually add the room contact to the radio's flash storage -await connectionProvider.addOrUpdateContact(widget.contact); - -print('✅ [RoomLogin] Room contact added via CMD_ADD_UPDATE_CONTACT'); -print(' Waiting 500ms for radio to save to flash...'); - -await Future.delayed(const Duration(milliseconds: 500)); -``` - -**VERDICT**: ✅ Already fully implemented and working! - ---- - -## 6. State Management Review - -### 6.1 RoomLoginState Model - -**File**: `lib/models/room_login_state.dart` - -```dart -class RoomLoginState { - final Uint8List publicKeyPrefix; - final bool isLoggedIn; - final bool isAdmin; - final int permissions; - final int? tag; - final DateTime? loginTime; - final bool hasPassword; - - factory RoomLoginState.loggedIn({...}) { ... } - factory RoomLoginState.loggedOut({...}) { ... } - - String get publicKeyPrefixHex { ... } - Duration? get loginDuration { ... } - String? get loginDurationFormatted { ... } -} -``` - -**Analysis**: -- ✅ Tracks all necessary login state -- ✅ Provides helper methods -- ✅ Immutable design -- **EXCELLENT** implementation - -### 6.2 State Tracking (connection_provider.dart:49-51, 131-164) - -```dart -// Room login state tracking -final Map _roomLoginStates = {}; -Map get roomLoginStates => Map.unmodifiable(_roomLoginStates); - -// In onLoginSuccess callback: -final prefixHex = publicKeyPrefix.map((b) => b.toRadixString(16).padLeft(2, '0')).join(':'); -final hasPassword = await _hasPasswordForRoom(publicKeyPrefix); -_roomLoginStates[prefixHex] = RoomLoginState.loggedIn( - publicKeyPrefix: publicKeyPrefix, - permissions: permissions, - isAdmin: isAdmin, - tag: tag, - hasPassword: hasPassword, -); -notifyListeners(); -``` - -**Analysis**: -- ✅ Stores state per room (by public key prefix) -- ✅ Checks for saved password -- ✅ Notifies UI of state changes -- **CORRECT** implementation - -### 6.3 UI Integration (contacts_tab.dart:157-284) - -Room login badges shown in ContactsTab: -```dart -// Room login status indicator badge -if (contact.type == ContactType.room && roomLoginState != null) - Positioned( - bottom: 0, - right: 0, - child: Container( - decoration: BoxDecoration( - color: _getRoomStatusColor(roomLoginState), - shape: BoxShape.circle, - ), - child: Icon(_getRoomStatusIcon(roomLoginState), ...), - ), - ), -``` - -**Analysis**: -- ✅ Visual indicator of login status -- ✅ Different colors for logged in/out/admin -- ✅ Shows login duration -- **EXCELLENT** UX - ---- - -## 7. Issues and Recommendations - -### 7.1 Issues Found - -**NONE** - Implementation is correct! - -### 7.2 Enhancements Recommended - -#### Priority 1: Login Timeout - -Currently, if room never responds, app waits forever. - -**Recommendation**: -```dart -// In connection_provider.dart loginToRoom() -Future loginToRoom({ - required Uint8List roomPublicKey, - required String password, - Duration timeout = const Duration(seconds: 10), -}) async { - final completer = Completer(); - - // Setup callbacks... - - // Send login - await _bleService.loginToRoom(...); - - // Start timeout - final timeoutFuture = Future.delayed(timeout, () { - if (!completer.isCompleted) { - print('⏱️ Login timeout - no response from room'); - // Restore callbacks - completer.complete(false); - } - }); - - return completer.future; -} -``` - -#### Priority 2: Room State Persistence - -**Current**: Login state lost on app restart -**Recommendation**: Save to SharedPreferences - -```dart -// Save on login success -await prefs.setBool('room_logged_in_${roomId}', true); -await prefs.setInt('room_login_time_${roomId}', DateTime.now().millisecondsSinceEpoch); - -// Restore on app start -if (prefs.getBool('room_logged_in_${roomId}') == true) { - final loginTime = prefs.getInt('room_login_time_${roomId}'); - // Show UI indicator that we were logged in - // Note: Room login is session-based, so we need to re-login -} -``` - -#### Priority 3: "Waiting for Messages" UI - -**Current**: User sees empty messages list after login -**Recommendation**: Show loading indicator - -```dart -// In messages_tab.dart -if (roomLoginState?.isLoggedIn == true && messages.isEmpty) { - return Center( - child: Column([ - CircularProgressIndicator(), - Text('Logged into ${room.name}'), - Text('Waiting for room to push messages...'), - ]), - ); -} -``` - ---- - -## 8. Testing Recommendations - -### 8.1 Cold Start Auto-Login - -✅ Test: First connection (no saved passwords) -- [ ] Uses "hello" as default -- [ ] Saves password after successful login - -✅ Test: Reconnection (has saved passwords) -- [ ] Auto-logs into all rooms -- [ ] Shows success messages -- [ ] Receives pushed messages - -✅ Test: Wrong saved password -- [ ] Login fails gracefully -- [ ] User can manually enter correct password - -### 8.2 User-Initiated Login - -✅ Test: Room not in device contacts -- [ ] Syncs contacts first -- [ ] Adds room manually if still not found -- [ ] Login succeeds after adding - -✅ Test: Clock drift > 60 seconds -- [ ] Warning logged -- [ ] Login may fail (room rejects old timestamps) - -✅ Test: Multiple rapid logins -- [ ] No callback mixing -- [ ] No memory leaks -- [ ] Proper cleanup - -### 8.3 Message Push - -✅ Test: Room has messages waiting -- [ ] Receives PUSH_CODE_MSG_WAITING -- [ ] Messages synced automatically -- [ ] All messages received - -✅ Test: Login with syncSince parameter -- [ ] Only new messages pushed -- [ ] Old messages not re-sent - ---- - -## 9. Conclusion - -### Summary - -The room login implementation is **production-ready and protocol-compliant**. - -### What Works Perfectly ✅ - -1. ✅ **Protocol Compliance**: All frame formats correct -2. ✅ **Cold Start Auto-Login**: Properly implemented -3. ✅ **User-Initiated Login**: Comprehensive pre-flight checks -4. ✅ **No Premature Sync**: Waits for `PUSH_CODE_MSG_WAITING` -5. ✅ **Contact Management**: `CMD_ADD_UPDATE_CONTACT` already implemented! -6. ✅ **State Tracking**: `RoomLoginState` model is excellent -7. ✅ **Error Handling**: User-friendly messages -8. ✅ **Callback Management**: Proper cleanup and restoration - -### Recommended Enhancements 📝 - -1. **Login timeout handling** (10 second timeout) -2. **Room state persistence** (survive app restart) -3. **"Waiting for messages" UI** (loading indicator) - -### Final Verdict - -**✅ APPROVED FOR PRODUCTION** - -No critical bugs found. All enhancements are optional improvements, not bug fixes. - -The implementation demonstrates excellent understanding of the MeshCore protocol and follows all best practices. - ---- - -## 10. Understanding LOG_RX_DATA Push Notifications - -### 10.1 What is LOG_RX_DATA (0x88)? - -`LOG_RX_DATA` (push code 0x88) is a **diagnostic push notification** that reports **raw over-the-air packets** received by the radio. - -**Key Points**: -- **NOT** an application-layer message -- **Encrypted over-the-air packet data** captured by the radio -- Used for debugging and monitoring network activity -- Contains the actual LoRa PHY layer packets - -### 10.2 Frame Format - -``` -[Push Code: 1 byte] = 0x88 -[Data: N bytes] = Raw over-the-air packet (encrypted) -``` - -The data payload contains: -1. **First 4 bytes**: Airtime or packet metadata (varies) -2. **Remaining bytes**: Encrypted packet payload from LoRa - -**Important**: The data is encrypted with the mesh network's shared key, so it appears as high-entropy random bytes. - -### 10.3 Observed LOG_RX_DATA During Message Send - -Example from logs: - -``` -flutter: 📥 [RX] Received: LOG_RX_DATA (0x88) -flutter: Data size: 9 bytes -flutter: Hex: 88 32 a7 0e 00 e2 d8 94 3a -``` - -This is an **ACK packet** being captured over the air: -- **Bytes 0-3**: `32 a7 0e 00` = Airtime/metadata (960306 when interpreted as uint32 LE) -- **Bytes 4-7**: `e2 d8 94 3a` = **ACK code** matching the sent message's expected ACK (982833378) - -This confirms the radio received the acknowledgment packet over the air. - -### 10.4 More Complex LOG_RX_DATA Packets - -``` -flutter: 📥 [RX] Received: LOG_RX_DATA (0x88) -flutter: Data size: 73 bytes -flutter: Hex: 88 25 a4 0a 00 11 15 43 9c 7e 51 ce 2b ... -``` - -This is likely the **original message packet** being retransmitted or repeated by another node: -- **Bytes 0-3**: Airtime metadata -- **Bytes 4-35**: **Sender public key** (32 bytes) = `11 15 43 9c 7e 51 ...` -- **Remaining**: Encrypted payload containing the message - -### 10.5 Why Multiple LOG_RX_DATA Packets? - -When you send a message, you may see **multiple LOG_RX_DATA** notifications because: - -1. **Your own transmission** is captured (loopback from radio) -2. **Repeater nodes re-broadcast** your message (mesh forwarding) -3. **ACK packets** are captured (confirmation from recipient) -4. **Path return packets** might be captured (route discovery) - -**This is normal behavior** - it shows the mesh network is working correctly. - -### 10.6 Should You Handle LOG_RX_DATA? - -**No** - LOG_RX_DATA is **diagnostic only**. Your app should: - -✅ **Ignore** LOG_RX_DATA push notifications -✅ **Focus on** application-layer responses: - - `RESP_CODE_SENT` (0x06) - Message queued for transmission - - `PUSH_CODE_SEND_CONFIRMED` (0x82) - Delivery confirmed - - `RESP_CODE_CONTACT_MSG_RECV` (0x07) - Received message from contact - - `PUSH_CODE_MSG_WAITING` (0x83) - New message notification - -❌ **Do not parse** LOG_RX_DATA payload - it's encrypted mesh-layer data - -### 10.7 Implementation Recommendation - -Your current implementation already handles LOG_RX_DATA correctly: - -```dart -case 0x88: // LOG_RX_DATA - debugPrint('📥 [RX] Received raw over-the-air packet (diagnostic)'); - // Log for debugging, but don't parse - it's encrypted mesh data - break; -``` - -The hex dump analysis you're seeing is helpful for debugging but doesn't need to trigger any action in your app. - -### 10.8 Summary - -1. **LOG_RX_DATA = Diagnostic tool** showing raw encrypted LoRa packets -2. **Multiple LOG_RX_DATA packets are normal** (loopback, repeaters, ACKs) -3. **High entropy is expected** (encrypted with mesh network key) -4. **Your app should ignore these** - focus on application-layer responses -5. **The encrypted "repeats" are the mesh network working** - forwarding messages, sending ACKs, establishing routes - ---- - -## File Reference - -| File | Description | -|------|-------------| -| `lib/providers/app_provider.dart:88-153` | Cold start auto-login | -| `lib/providers/connection_provider.dart:125-129` | Message waiting handler | -| `lib/providers/connection_provider.dart:696-715` | loginToRoom() API | -| `lib/screens/contacts_tab.dart:983-1201` | User-initiated login UI | -| `lib/services/meshcore_ble_service.dart:1390-1416` | CMD_SEND_LOGIN | -| `lib/services/meshcore_ble_service.dart:1123-1172` | CMD_ADD_UPDATE_CONTACT | -| `lib/services/meshcore_ble_service.dart:942-981` | PUSH_CODE_LOGIN_SUCCESS handler | -| `lib/models/room_login_state.dart` | Login state model | -| `MESSAGES.md:Section 5` | Protocol documentation | - ---- - -**Document Version**: 1.0 -**Review Date**: 2025-01-14 -**Next Review**: After implementing recommended enhancements diff --git a/SCREENSHOTS.md b/SCREENSHOTS.md deleted file mode 100644 index 1780e9d..0000000 --- a/SCREENSHOTS.md +++ /dev/null @@ -1,367 +0,0 @@ -# Screenshot Automation Guide - -Comprehensive guide for capturing App Store screenshots for MeshCore SAR app using Flutter integration tests. - -## Overview - -This project includes automated screenshot capture for: -- **App Store submission** (iOS + Android) -- **Documentation and training materials** -- **Marketing assets** -- **Multiple devices and screen sizes** -- **Multiple locales** (English, Croatian, Slovenian) - -## Quick Start - -### Prerequisites - -1. **Flutter SDK** installed and configured -2. **iOS**: Xcode with simulators installed -3. **Android**: Android Studio with emulators configured -4. **Dependencies installed**: - ```bash - flutter pub get - ``` - -### Take Screenshots (All Devices) - -```bash -./scripts/take_screenshots.sh -``` - -Screenshots will be saved to `screenshots/` directory. - -## Detailed Usage - -### Command Line Options - -```bash -# All devices (iOS + Android) -./scripts/take_screenshots.sh - -# iOS devices only -./scripts/take_screenshots.sh --ios - -# Android devices only -./scripts/take_screenshots.sh --android - -# Specific device -./scripts/take_screenshots.sh --device "iPhone 15 Pro Max" - -# List available devices -./scripts/take_screenshots.sh --list - -# Show help -./scripts/take_screenshots.sh --help -``` - -### Manual Test Execution - -You can also run the integration test manually: - -```bash -# iOS -flutter drive \ - --driver=test_driver/integration_test.dart \ - --target=integration_test/app_screenshots_test.dart \ - -d "iPhone 15 Pro Max" - -# Android (start emulator first) -flutter drive \ - --driver=test_driver/integration_test.dart \ - --target=integration_test/app_screenshots_test.dart \ - -d emulator-5554 -``` - -## Screenshot Coverage - -The automated test captures the following screens: - -1. **Home Screen (Disconnected)** - Initial state showing connect button -2. **Messages List** - Messages with SAR markers displayed -3. **SAR Marker Detail** - Detailed view of a SAR event -4. **Contacts List** - Team members and repeaters -5. **Contact Detail** - Individual contact information -6. **Map View** - Map with team markers and SAR markers -7. **Map Legend** - Legend showing marker types -8. **Settings Screen** - App settings and preferences - -## Device Configurations - -### iOS Devices (App Store Requirements) - -The script is configured for App Store screenshot requirements: - -| Device | Screen Size | Resolution | Required for App Store | -|--------|-------------|------------|----------------------| -| iPhone 15 Pro Max | 6.7" | 1290x2796 | ✅ Yes (primary) | -| iPhone 14 Pro Max | 6.7" | 1290x2796 | ✅ Yes (backup) | -| iPhone 8 Plus | 5.5" | 1242x2208 | ✅ Yes (smaller size) | - -**App Store Notes:** -- 6.7" display is **required** as of 2024 -- 5.5" display provides compatibility with older devices -- Screenshots must be in PNG or JPEG format -- Maximum 10 screenshots per device size - -### Android Devices (Google Play Requirements) - -| Device | Type | Resolution | Required for Play Store | -|--------|------|------------|------------------------| -| Pixel 7 Pro | Phone | 1440x3120 | ✅ Recommended | -| Pixel Tablet | Tablet | 2560x1600 | ✅ Recommended | - -**Google Play Notes:** -- Phone screenshots: 16:9 or 9:16 ratio recommended -- Tablet screenshots: Optional but recommended -- Minimum 2 screenshots, maximum 8 per device type -- PNG or JPEG format accepted - -## Project Structure - -``` -meshcore_sar_app/ -├── integration_test/ -│ ├── app_screenshots_test.dart # Main screenshot test -│ └── helpers/ -│ ├── mock_data.dart # Mock contacts, messages, markers -│ └── screenshot_helper.dart # Screenshot utilities -├── test_driver/ -│ └── integration_test.dart # Integration test driver -├── scripts/ -│ └── take_screenshots.sh # Automated screenshot script -└── screenshots/ # Output directory - ├── ios/ - │ ├── iPhone_15_Pro_Max/ - │ ├── iPhone_14_Pro_Max/ - │ └── iPhone_8_Plus/ - └── android/ - ├── pixel_7_pro/ - └── pixel_tablet/ -``` - -## Mock Data - -The test uses predictable mock data for consistent screenshots: - -### Contacts (6 total) -- **Alpha Team Lead** - Battery: 3850mV, 1 hop, -45 dBm -- **Bravo Scout** - Battery: 3700mV, 2 hops, -68 dBm -- **Charlie Base** - Battery: 4100mV, 0 hops, -35 dBm -- **Delta Medic** - Battery: 3600mV, 3 hops, -75 dBm -- **Mountain Repeater 1** - Repeater type -- **SAR Command Room** - Room type - -### Messages (8 total) -- Team communications -- SAR marker messages -- Public channel broadcasts - -### SAR Markers (3 total) -- 🧑 **Found Person** at 46.0589, 14.5078 (Bravo Scout) -- 🏕️ **Staging Area** at 46.0549, 14.5038 (Charlie Base) -- 🔥 **Fire Location** at 46.0620, 14.5120 (Alpha Team Lead) - -All mock data is defined in `integration_test/helpers/mock_data.dart`. - -## Customization - -### Adding More Screens - -Edit `integration_test/app_screenshots_test.dart`: - -```dart -// Navigate to your screen -await tester.tapAndSettle(find.text('Your Screen')); - -// Take screenshot -await screenshotHelper.takeScreenshot( - tester, - 'your_screen_name', -); -``` - -### Changing Mock Data - -Edit `integration_test/helpers/mock_data.dart`: - -```dart -static List getMockContacts() { - return [ - Contact( - publicKey: '0x...', - name: 'Your Contact Name', - // ... more fields - ), - ]; -} -``` - -### Adding Devices - -Edit `scripts/take_screenshots.sh`: - -```bash -# iOS -IOS_DEVICES=( - "iPhone 15 Pro Max" - "Your Device Name" -) - -# Android -ANDROID_DEVICES=( - "pixel_7_pro" - "your_emulator_name" -) -``` - -## Localization - -To capture screenshots in different languages: - -1. **Set system language** on simulator/emulator -2. **Run the screenshot script** -3. **Organize by locale** in output directory - -Example for Croatian screenshots: - -```bash -# 1. Set iOS simulator to Croatian -xcrun simctl spawn booted defaults write "Apple Global Domain" AppleLanguages -array hr - -# 2. Run screenshots -./scripts/take_screenshots.sh --ios - -# 3. Move to locale-specific folder -mkdir -p screenshots/ios/hr-HR -mv screenshots/ios/iPhone_15_Pro_Max/* screenshots/ios/hr-HR/ -``` - -For automation, you can modify the script to handle locale switching. - -## Troubleshooting - -### Simulator Not Found - -```bash -# List available simulators -xcrun simctl list devices available - -# Create new simulator -xcrun simctl create "iPhone 15 Pro Max" "iPhone 15 Pro Max" -``` - -### Emulator Issues - -```bash -# List available emulators -emulator -list-avds - -# Create new emulator (use Android Studio AVD Manager) -# Or via command line: -avdmanager create avd -n pixel_7_pro -k "system-images;android-33;google_apis;x86_64" -``` - -### Screenshots Not Appearing - -1. Check test output for errors -2. Verify `integration_test/app_screenshots_test.dart` runs successfully -3. Check `ScreenshotHelper` is calling `binding.takeScreenshot()` -4. Ensure output directory has write permissions - -### Test Times Out - -```bash -# Increase timeout in test -await tester.pumpAndSettle(const Duration(seconds: 10)); - -# Or modify flutter drive timeout -flutter drive --timeout=120s ... -``` - -### BLE/Permissions Errors in Tests - -Integration tests run in a sandboxed environment. The test uses **mock data** instead of real BLE connections, so: - -- ✅ No actual BLE device needed -- ✅ No location permissions required -- ✅ Predictable, repeatable screenshots -- ❌ Cannot test actual BLE connectivity (use manual testing for that) - -## Best Practices - -### For App Store Screenshots - -1. **Use the largest device first** (iPhone 15 Pro Max, Pixel 7 Pro) -2. **Highlight key features** in each screenshot -3. **Add localization** for target markets -4. **Keep consistent ordering** across all devices -5. **Review before submission** - ensure no sensitive data visible - -### For Quality Screenshots - -1. **Clean state** - Use mock data for predictable content -2. **Good lighting** - Ensure sufficient contrast in UI -3. **Meaningful content** - Show realistic usage scenarios -4. **No debug info** - Disable debug banners/overlays -5. **Proper timing** - Wait for animations to complete - -### File Organization - -Recommended structure for App Store submission: - -``` -screenshots/ -├── en-US/ # English (default) -│ ├── 6.7-inch/ # iPhone 15 Pro Max -│ │ ├── 01_home.png -│ │ ├── 02_messages.png -│ │ └── ... -│ ├── 5.5-inch/ # iPhone 8 Plus -│ └── android-phone/ # Pixel 7 Pro -├── hr-HR/ # Croatian -│ └── ... -└── sl-SI/ # Slovenian - └── ... -``` - -## Advanced: CI/CD Integration - -For automated screenshot generation in CI/CD pipelines: - -```yaml -# .github/workflows/screenshots.yml -name: Generate Screenshots -on: - workflow_dispatch: # Manual trigger - -jobs: - screenshots: - runs-on: macos-latest - steps: - - uses: actions/checkout@v3 - - uses: subosito/flutter-action@v2 - - name: Install dependencies - run: flutter pub get - - name: Take screenshots - run: ./scripts/take_screenshots.sh --ios - - name: Upload screenshots - uses: actions/upload-artifact@v3 - with: - name: screenshots - path: screenshots/ -``` - -## Resources - -- [Flutter Integration Testing](https://docs.flutter.dev/testing/integration-tests) -- [App Store Screenshot Requirements](https://developer.apple.com/help/app-store-connect/reference/screenshot-specifications/) -- [Google Play Screenshot Requirements](https://support.google.com/googleplay/android-developer/answer/9866151) -- [MeshCore SAR Documentation](./CLAUDE.md) - -## Support - -For issues or questions: -1. Check the [Troubleshooting](#troubleshooting) section -2. Review Flutter integration test docs -3. Open an issue in the project repository diff --git a/TEST_COVERAGE.md b/TEST_COVERAGE.md deleted file mode 100644 index 8601d94..0000000 --- a/TEST_COVERAGE.md +++ /dev/null @@ -1,219 +0,0 @@ -# Critical String Formatting Test Coverage - -## Overview - -This document describes the comprehensive test coverage for ensuring S: (SAR marker) and D: (drawing) messages are always sent as **raw strings**, never as object representations. - -## Test Results - -✅ **All 45 tests passing** - -- 17 tests for Drawing Message Parser -- 28 tests for SAR Message Parser - -## Files Modified - -### Production Code -1. `lib/utils/drawing_message_parser.dart` - Added explicit `.toString()` for JSON encoding -2. `lib/utils/sar_message_parser.dart` - Added explicit `.toString()` for coordinates -3. `lib/screens/messages_tab.dart` - Added explicit `.toString()` for color index - -### Test Files (New) -1. `test/utils/drawing_message_parser_test.dart` - 17 comprehensive tests -2. `test/utils/sar_message_parser_test.dart` - 28 comprehensive tests - -## Test Categories - -### Drawing Message Tests (`drawing_message_parser_test.dart`) - -#### Critical String Safety Tests -- ✅ Returns String type, not Object -- ✅ Does NOT contain "Instance of" or "Object" -- ✅ Does NOT contain object class names -- ✅ Produces parseable JSON after D: prefix -- ✅ Coordinates are numbers in JSON, not strings -- ✅ JSON is compact and properly encoded - -#### Format Validation Tests -- ✅ Line drawings (type 0) format correctly -- ✅ Rectangle drawings (type 1) format correctly -- ✅ Color indices (0-7) preserved as integers -- ✅ Coordinates rounded to 5 decimal places -- ✅ Empty points arrays handled -- ✅ Large/extreme coordinate values work - -#### Round-Trip Tests -- ✅ Create → Parse → Create produces consistent output -- ✅ Metadata extraction works correctly -- ✅ Type and color names extracted properly - -#### Security Tests -- ✅ Sender metadata NOT included in network JSON -- ✅ Only compact fields (t, c, p/b) in output -- ✅ Special characters don't break format - -### SAR Message Tests (`sar_message_parser_test.dart`) - -#### Critical String Safety Tests -- ✅ Returns String type, not Object -- ✅ Does NOT contain "Instance of", "Object", or "LatLng" -- ✅ Coordinates converted to string form -- ✅ Color index converted to string form -- ✅ Emoji preserved as UTF-8 character, not code points - -#### Format Validation Tests -- ✅ New format: `S:::,:` -- ✅ Color index defaults to 0 when null -- ✅ All emoji types (🧑, 🔥, 🏕️) work correctly -- ✅ Notes with special characters preserved -- ✅ Empty/null notes handled gracefully - -#### Coordinate Validation Tests -- ✅ Negative coordinates work -- ✅ Extreme valid coordinates (±90°, ±180°) work -- ✅ Invalid coordinates (>90°, >180°) rejected -- ✅ Zero coordinates (0.0, 0.0) work -- ✅ Coordinate precision maintained - -#### Round-Trip Tests -- ✅ Create → Parse → Create preserves format -- ✅ Backward compatible with old format (no color index) -- ✅ Multi-line notes extracted correctly - -#### Specification Compliance -- ✅ CLAUDE.md format specification followed -- ✅ Message format is compact (<50 chars base) -- ✅ No extra whitespace or newlines - -## Critical Safety Checks - -Every test verifies these critical properties: - -### For Drawing Messages (D:) -```dart -// ✅ Must be String type -expect(message, isA()); - -// ✅ Must start with D: prefix -expect(message, startsWith('D:')); - -// ✅ Must NOT contain object representations -expect(message, isNot(contains('Instance of'))); -expect(message, isNot(contains('Object'))); - -// ✅ JSON must be valid after prefix -final jsonStr = message.substring(2); -expect(() => jsonDecode(jsonStr), returnsNormally); -``` - -### For SAR Messages (S:) -```dart -// ✅ Must be String type -expect(message, isA()); - -// ✅ Must start with S: prefix -expect(message, startsWith('S:')); - -// ✅ Must NOT contain object representations -expect(message, isNot(contains('Instance of'))); -expect(message, isNot(contains('LatLng'))); - -// ✅ Coordinates must be string-formatted numbers -expect(message, contains('37.7749')); // Not "LatLng(37.7749, ...)" -``` - -## Example Outputs Verified - -### Drawing Messages -``` -Line: D:{"t":0,"c":1,"p":[37.7749,-122.4194,37.775,-122.4195]} -Rectangle: D:{"t":1,"c":2,"b":[45.5231,-122.6765,45.51,-122.66]} -``` - -### SAR Messages -``` -Person: S:🧑:2:37.7749,-122.4194:Found alive -Fire: S:🔥:0:40.7128,-74.006:Large wildfire -Staging: S:🏕️:4:51.5074,-0.1278:Command center -``` - -## Running Tests - -### Run all utils tests -```bash -flutter test test/utils/ -``` - -### Run individual test files -```bash -flutter test test/utils/drawing_message_parser_test.dart -flutter test test/utils/sar_message_parser_test.dart -``` - -### Run with detailed output -```bash -flutter test test/utils/ --reporter=expanded -``` - -## Code Changes Summary - -### 1. Drawing Message Parser -```dart -// BEFORE -final jsonStr = jsonEncode(json); - -// AFTER -final jsonStr = jsonEncode(json).toString(); // Explicit string conversion -``` - -### 2. SAR Message Parser -```dart -// BEFORE -final text = 'S:${type.emoji}:$colorIdx:${location.latitude},${location.longitude}'; - -// AFTER -final text = 'S:${type.emoji}:$colorIdx:${location.latitude.toString()},${location.longitude.toString()}'; -``` - -### 3. Messages Tab SAR Creation -```dart -// BEFORE -'S:$emoji:$colorIndex:${position.latitude.toStringAsFixed(5)},...' - -// AFTER -'S:$emoji:${colorIndex.toString()}:${position.latitude.toStringAsFixed(5)},...' -``` - -## Why This Matters - -Without explicit `.toString()` calls, edge cases could cause: - -1. **Object Leakage**: `LatLng` objects interpolated as `"Instance of 'LatLng'"` -2. **Type Coercion Failures**: JSON encoding returning non-String types -3. **Network Failures**: Receivers unable to parse malformed messages -4. **Data Loss**: Coordinates lost if object representation sent - -## Confidence Level - -🟢 **HIGH CONFIDENCE** - All critical paths tested with: -- Type safety verification -- Format validation -- Round-trip parsing -- Edge case coverage -- Backward compatibility -- Specification compliance - -## Maintenance - -When modifying message formats: - -1. ✅ Run `flutter test test/utils/` -2. ✅ Verify all 45 tests pass -3. ✅ Add new tests for new message types -4. ✅ Update CLAUDE.md if format changes - -## Related Documentation - -- `CLAUDE.md` - Protocol specification -- `lib/utils/drawing_message_parser.dart` - Drawing message implementation -- `lib/utils/sar_message_parser.dart` - SAR message implementation diff --git a/UNIMPLEMENTED_BLE_COMMANDS.md b/UNIMPLEMENTED_BLE_COMMANDS.md deleted file mode 100644 index 6f2005d..0000000 --- a/UNIMPLEMENTED_BLE_COMMANDS.md +++ /dev/null @@ -1,1725 +0,0 @@ -# Unimplemented BLE Commands - MeshCore SAR - -This document catalogs all BLE commands from the MeshCore protocol that are **not yet implemented** in the Flutter application. Implementations are based on analysis of the C++ reference implementation at `/Users/dz0ny/meshcore-sar/MeshCore/examples/companion_radio/MyMesh.cpp`. - -**Total Commands**: 52 defined in protocol -**Implemented in Flutter**: 30 -**Not Implemented**: 22 (documented below) - ---- - -## Table of Contents - -1. [Commands Not Defined in Flutter](#commands-not-defined-in-flutter) (11 commands) -2. [Commands Defined But Not Implemented](#commands-defined-but-not-implemented) (11 commands) -3. [Implementation Priority Matrix](#implementation-priority-matrix) -4. [Quick Reference Table](#quick-reference-table) - ---- - -## Commands Not Defined in Flutter - -These commands don't exist in `lib/services/meshcore_constants.dart` at all. - -### 1. CMD_SHARE_CONTACT (16) - -**Status**: Not defined -**Priority**: Medium -**Use Case**: Share contact info with nearby mesh nodes - -**Response**: `RESP_CODE_OK` (0) or `RESP_CODE_ERR` (1) - -**Parameters**: -- Offset 0: Command code (16) -- Offset 1-32: Public key (32 bytes) of contact to share - -**C++ Implementation** (`MyMesh.cpp:1059-1070`): -```cpp -else if (cmd_frame[0] == CMD_SHARE_CONTACT) { - uint8_t *pub_key = &cmd_frame[1]; - ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); - if (recipient) { - if (shareContactZeroHop(*recipient)) { - writeOKFrame(); - } else { - writeErrFrame(ERR_CODE_TABLE_FULL); - } - } else { - writeErrFrame(ERR_CODE_NOT_FOUND); - } -} -``` - -**What It Does**: Broadcasts a zero-hop advertisement of a contact in the local network. Used to share another contact's information with nearby mesh nodes. - -**Error Codes**: -- `ERR_CODE_TABLE_FULL` (3): Packet pool exhausted -- `ERR_CODE_NOT_FOUND` (2): Contact not found - -**Implementation Notes**: -- Validates contact exists by public key -- Sends contact advertisement with zero hops (direct only) -- No parameters beyond public key required -- Min frame length: 33 bytes - -**Flutter Implementation Guide**: -```dart -// Constants -static const int cmdShareContact = 16; - -// Frame Builder -static Uint8List buildShareContact(Uint8List contactPublicKey) { - final writer = BufferWriter(); - writer.writeByte(MeshCoreConstants.cmdShareContact); - writer.writeBytes(contactPublicKey); // 32 bytes - return writer.toBytes(); -} - -// Service API -Future shareContact(Uint8List contactPublicKey) async { - final frame = FrameBuilder.buildShareContact(contactPublicKey); - await _commandSender.sendCommand(frame); -} -``` - ---- - -### 2. CMD_HAS_CONNECTION (28) - -**Status**: Not defined -**Priority**: High -**Use Case**: Check if radio has a path to a contact before sending - -**Response**: `RESP_CODE_OK` (0) or `RESP_CODE_ERR` (1) - -**Parameters**: -- Offset 0: Command code (28) -- Offset 1-32: Public key (32 bytes) to check connection - -**C++ Implementation** (`MyMesh.cpp:1383-1389`): -```cpp -else if (cmd_frame[0] == CMD_HAS_CONNECTION && len >= 1 + PUB_KEY_SIZE) { - uint8_t *pub_key = &cmd_frame[1]; - if (hasConnectionTo(pub_key)) { - writeOKFrame(); - } else { - writeErrFrame(ERR_CODE_NOT_FOUND); - } -} -``` - -**What It Does**: Checks if the radio has a known path to a specific contact. Useful for app to determine reachability before sending messages. - -**Error Codes**: -- `ERR_CODE_NOT_FOUND` (2): No connection path known - -**Implementation Notes**: -- Validates minimum frame length: 33 bytes -- Returns OK if connection exists -- Does NOT verify contact is in local contact list -- Min frame length: 33 bytes - -**Flutter Implementation Guide**: -```dart -// Constants -static const int cmdHasConnection = 28; - -// Frame Builder -static Uint8List buildHasConnection(Uint8List contactPublicKey) { - final writer = BufferWriter(); - writer.writeByte(MeshCoreConstants.cmdHasConnection); - writer.writeBytes(contactPublicKey); // 32 bytes - return writer.toBytes(); -} - -// Service API -Future hasConnectionTo(Uint8List contactPublicKey) async { - final frame = FrameBuilder.buildHasConnection(contactPublicKey); - try { - await _commandSender.sendCommand(frame); - return true; // RESP_CODE_OK received - } catch (e) { - return false; // ERR_CODE_NOT_FOUND or timeout - } -} -``` - ---- - -### 3. CMD_LOGOUT (29) - -**Status**: Not defined -**Priority**: Medium -**Use Case**: Disconnect from room servers - -**Response**: `RESP_CODE_OK` (0) - -**Parameters**: -- Offset 0: Command code (29) -- Offset 1-32: Public key (32 bytes) of room/server to disconnect from - -**C++ Implementation** (`MyMesh.cpp:1390-1393`): -```cpp -else if (cmd_frame[0] == CMD_LOGOUT && len >= 1 + PUB_KEY_SIZE) { - uint8_t *pub_key = &cmd_frame[1]; - stopConnection(pub_key); - writeOKFrame(); -} -``` - -**What It Does**: Disconnects/logs out from a room server or chat service. Stops receiving automatic message pushes from the service. - -**Implementation Notes**: -- Also known as "Disconnect" per comment in header -- Always returns OK (success guaranteed) -- Calls internal `stopConnection()` to halt login/message polling -- Min frame length: 33 bytes -- Used with room-type contacts (ADV_TYPE_ROOM) - -**Flutter Implementation Guide**: -```dart -// Constants -static const int cmdLogout = 29; - -// Frame Builder -static Uint8List buildLogout(Uint8List roomPublicKey) { - final writer = BufferWriter(); - writer.writeByte(MeshCoreConstants.cmdLogout); - writer.writeBytes(roomPublicKey); // 32 bytes - return writer.toBytes(); -} - -// Service API -Future logoutFromRoom(Uint8List roomPublicKey) async { - final frame = FrameBuilder.buildLogout(roomPublicKey); - await _commandSender.sendCommand(frame); -} -``` - ---- - -### 4. CMD_GET_CONTACT_BY_KEY (30) - -**Status**: Not defined -**Priority**: Medium -**Use Case**: Retrieve full contact details by public key - -**Response**: `RESP_CODE_CONTACT` (3) or `RESP_CODE_ERR` (1) - -**Parameters**: -- Offset 0: Command code (30) -- Offset 1-32: Public key (32 bytes) to look up - -**C++ Implementation** (`MyMesh.cpp:1071-1078`): -```cpp -else if (cmd_frame[0] == CMD_GET_CONTACT_BY_KEY) { - uint8_t *pub_key = &cmd_frame[1]; - ContactInfo *contact = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); - if (contact) { - writeContactRespFrame(RESP_CODE_CONTACT, *contact); - } else { - writeErrFrame(ERR_CODE_NOT_FOUND); - } -} -``` - -**What It Does**: Retrieves full contact information by public key. Returns all stored contact details including GPS location, path, name, etc. - -**Response Format** (`RESP_CODE_CONTACT` - 3): -- Byte 0: `RESP_CODE_CONTACT` (3) -- Bytes 1-32: Public key (32 bytes) -- Byte 33: Contact type (ADV_TYPE_*) -- Byte 34: Flags -- Byte 35: Out path length -- Bytes 36-77: Out path (MAX_PATH_SIZE = 64) -- Bytes 78-109: Contact name (32 bytes, null-padded) -- Bytes 110-113: Last advertisement timestamp (uint32_t LE) -- Bytes 114-117: GPS latitude (int32_t LE, 1E-6 scale) -- Bytes 118-121: GPS longitude (int32_t LE, 1E-6 scale) - -**Error Codes**: -- `ERR_CODE_NOT_FOUND` (2): Contact not found - -**Implementation Notes**: -- Simple lookup-only operation, no side effects -- Returns complete contact information -- Min frame length: 33 bytes -- Useful when you have a public key but need full contact details - -**Flutter Implementation Guide**: -```dart -// Constants -static const int cmdGetContactByKey = 30; - -// Frame Builder -static Uint8List buildGetContactByKey(Uint8List contactPublicKey) { - final writer = BufferWriter(); - writer.writeByte(MeshCoreConstants.cmdGetContactByKey); - writer.writeBytes(contactPublicKey); // 32 bytes - return writer.toBytes(); -} - -// Service API (use existing parseContact from FrameParser) -Future getContactByKey(Uint8List contactPublicKey) async { - final frame = FrameBuilder.buildGetContactByKey(contactPublicKey); - final completer = Completer(); - - // Wait for RESP_CODE_CONTACT (3) - _responseHandler.onContactReceived = (contact) { - completer.complete(contact); - }; - - await _commandSender.sendCommand(frame); - return completer.future.timeout(Duration(seconds: 5)); -} -``` - ---- - -### 5. CMD_SET_DEVICE_PIN (37) - -**Status**: Not defined -**Priority**: Low -**Use Case**: Secure BLE pairing with PIN - -**Response**: `RESP_CODE_OK` (0) or `RESP_CODE_ERR` (1) - -**Parameters**: -- Offset 0: Command code (37) -- Offset 1-4: BLE PIN (uint32_t LE) - either 0 (disable) or 100000-999999 (6-digit PIN) - -**C++ Implementation** (`MyMesh.cpp:1475-1488`): -```cpp -else if (cmd_frame[0] == CMD_SET_DEVICE_PIN && len >= 5) { - uint32_t pin; - memcpy(&pin, &cmd_frame[1], 4); - if (pin == 0 || (pin >= 100000 && pin <= 999999)) { - _prefs.ble_pin = pin; - savePrefs(); - writeOKFrame(); - } else { - writeErrFrame(ERR_CODE_ILLEGAL_ARG); - } -} -``` - -**What It Does**: Sets or disables the BLE pairing PIN code for the radio device. Used to require a PIN for BLE connections. - -**Error Codes**: -- `ERR_CODE_ILLEGAL_ARG` (6): Invalid PIN (not 0 or 100000-999999) - -**Implementation Notes**: -- Min frame length: 5 bytes -- Validates PIN: must be 0 (disabled) or 6-digit number (100000-999999) -- Persisted to device preferences/EEPROM -- Requires `savePrefs()` to persist to storage - -**Flutter Implementation Guide**: -```dart -// Constants -static const int cmdSetDevicePin = 37; - -// Frame Builder -static Uint8List buildSetDevicePin(int pin) { - if (pin != 0 && (pin < 100000 || pin > 999999)) { - throw ArgumentError('PIN must be 0 (disabled) or 6-digit (100000-999999)'); - } - final writer = BufferWriter(); - writer.writeByte(MeshCoreConstants.cmdSetDevicePin); - writer.writeUInt32LE(pin); - return writer.toBytes(); -} - -// Service API -Future setDevicePin(int pin) async { - final frame = FrameBuilder.buildSetDevicePin(pin); - await _commandSender.sendCommand(frame); -} -``` - ---- - -### 6. CMD_GET_CUSTOM_VARS (40) - -**Status**: Not defined -**Priority**: Low -**Use Case**: Read device-specific sensor settings - -**Response**: `RESP_CODE_CUSTOM_VARS` (21) - -**Parameters**: -- Offset 0: Command code (40) -- No additional parameters - -**C++ Implementation** (`MyMesh.cpp:1489-1502`): -```cpp -else if (cmd_frame[0] == CMD_GET_CUSTOM_VARS) { - out_frame[0] = RESP_CODE_CUSTOM_VARS; - char *dp = (char *)&out_frame[1]; - for (int i = 0; i < sensors.getNumSettings() && dp - (char *)&out_frame[1] < 140; i++) { - if (i > 0) { - *dp++ = ','; - } - strcpy(dp, sensors.getSettingName(i)); - dp = strchr(dp, 0); - *dp++ = ':'; - strcpy(dp, sensors.getSettingValue(i)); - dp = strchr(dp, 0); - } - _serial->writeFrame(out_frame, dp - (char *)out_frame); -} -``` - -**What It Does**: Returns all custom sensor/device configuration variables and their current values. Used to expose device-specific settings. - -**Response Format**: -- Byte 0: `RESP_CODE_CUSTOM_VARS` (21) -- Bytes 1+: Comma-separated key:value pairs (variable length, max ~140 chars) - - Format: `name1:value1,name2:value2,name3:value3` - - Each pair separated by comma - - Key and value separated by colon - - Max buffer: 141 bytes total - -**Implementation Notes**: -- Calls `sensors.getNumSettings()` to enumerate available settings -- Stops building if buffer reaches 140 bytes -- No input validation needed (no parameters) -- Response is variable length - -**Flutter Implementation Guide**: -```dart -// Constants -static const int cmdGetCustomVars = 40; -static const int respCustomVars = 21; - -// Frame Builder -static Uint8List buildGetCustomVars() { - final writer = BufferWriter(); - writer.writeByte(MeshCoreConstants.cmdGetCustomVars); - return writer.toBytes(); -} - -// Frame Parser -static Map parseCustomVars(BufferReader reader) { - final csvData = reader.readRemainingBytes(); - final csvString = utf8.decode(csvData); - final vars = {}; - - for (final pair in csvString.split(',')) { - final parts = pair.split(':'); - if (parts.length == 2) { - vars[parts[0]] = parts[1]; - } - } - return vars; -} - -// Service API -Future> getCustomVars() async { - final frame = FrameBuilder.buildGetCustomVars(); - // TODO: Implement response handler for RESP_CODE_CUSTOM_VARS - await _commandSender.sendCommand(frame); -} -``` - ---- - -### 7. CMD_SET_CUSTOM_VAR (41) - -**Status**: Not defined -**Priority**: Low -**Use Case**: Configure device-specific sensor settings - -**Response**: `RESP_CODE_OK` (0) or `RESP_CODE_ERR` (1) - -**Parameters**: -- Offset 0: Command code (41) -- Offset 1+: Setting as "name:value" string (null-terminated) - -**C++ Implementation** (`MyMesh.cpp:1503-1517`): -```cpp -else if (cmd_frame[0] == CMD_SET_CUSTOM_VAR && len >= 4) { - cmd_frame[len] = 0; // null terminate - char *sp = (char *)&cmd_frame[1]; - char *np = strchr(sp, ':'); - if (np) { - *np++ = 0; - bool success = sensors.setSettingValue(sp, np); - if (success) { - writeOKFrame(); - } else { - writeErrFrame(ERR_CODE_ILLEGAL_ARG); - } - } else { - writeErrFrame(ERR_CODE_ILLEGAL_ARG); - } -} -``` - -**What It Does**: Sets a custom sensor/device configuration variable to a new value. Modifies device-specific settings. - -**Error Codes**: -- `ERR_CODE_ILLEGAL_ARG` (6): No ':' separator found or `setSettingValue()` failed - -**Implementation Notes**: -- Min frame length: 4 bytes -- Format: "name:value" (colon-separated) -- Parses by looking for ':' separator character -- No persistence guarantee - depends on `sensors` implementation - -**Flutter Implementation Guide**: -```dart -// Constants -static const int cmdSetCustomVar = 41; - -// Frame Builder -static Uint8List buildSetCustomVar(String name, String value) { - final writer = BufferWriter(); - writer.writeByte(MeshCoreConstants.cmdSetCustomVar); - writer.writeString('$name:$value'); - return writer.toBytes(); -} - -// Service API -Future setCustomVar(String name, String value) async { - final frame = FrameBuilder.buildSetCustomVar(name, value); - await _commandSender.sendCommand(frame); -} -``` - ---- - -### 8. CMD_GET_ADVERT_PATH (42) - -**Status**: Not defined -**Priority**: Medium -**Use Case**: Network topology analysis and debugging - -**Response**: `RESP_CODE_ADVERT_PATH` (22) or `RESP_CODE_ERR` (1) - -**Parameters**: -- Offset 0: Command code (42) -- Offset 1: Reserved (for future use) -- Offset 2-8: Public key prefix (7 bytes) of advertised node - -**C++ Implementation** (`MyMesh.cpp:1518-1537`): -```cpp -else if (cmd_frame[0] == CMD_GET_ADVERT_PATH && len >= PUB_KEY_SIZE+2) { - uint8_t *pub_key = &cmd_frame[2]; - AdvertPath* found = NULL; - for (int i = 0; i < ADVERT_PATH_TABLE_SIZE; i++) { - auto p = &advert_paths[i]; - if (memcmp(p->pubkey_prefix, pub_key, sizeof(p->pubkey_prefix)) == 0) { - found = p; - break; - } - } - if (found) { - out_frame[0] = RESP_CODE_ADVERT_PATH; - memcpy(&out_frame[1], &found->recv_timestamp, 4); - out_frame[5] = found->path_len; - memcpy(&out_frame[6], found->path, found->path_len); - _serial->writeFrame(out_frame, 6 + found->path_len); - } else { - writeErrFrame(ERR_CODE_NOT_FOUND); - } -} -``` - -**What It Does**: Returns the wireless path from which a node's advertisement was last received. Used to analyze network topology and signal paths. - -**Response Format**: -- Byte 0: `RESP_CODE_ADVERT_PATH` (22) -- Bytes 1-4: Reception timestamp (uint32_t LE) -- Byte 5: Path length (number of hops) -- Bytes 6+: Path data (variable length, max MAX_PATH_SIZE) - -**Error Codes**: -- `ERR_CODE_NOT_FOUND` (2): Node not in recent advertisements - -**Implementation Notes**: -- Min frame length: 35 bytes (32 for pubkey + 2 for cmd + reserved) -- Searches circular table of size ADVERT_PATH_TABLE_SIZE (16 entries) -- Matches only first 7 bytes of public key (pubkey_prefix) -- Timestamp is when advertisement was received -- Table is circular and overwrites oldest entries - -**Flutter Implementation Guide**: -```dart -// Constants -static const int cmdGetAdvertPath = 42; -static const int respAdvertPath = 22; - -// Frame Builder -static Uint8List buildGetAdvertPath(Uint8List publicKeyPrefix) { - if (publicKeyPrefix.length < 7) { - throw ArgumentError('Public key prefix must be at least 7 bytes'); - } - final writer = BufferWriter(); - writer.writeByte(MeshCoreConstants.cmdGetAdvertPath); - writer.writeByte(0); // Reserved - writer.writeBytes(publicKeyPrefix.sublist(0, 7)); - return writer.toBytes(); -} - -// Frame Parser -static Map parseAdvertPath(BufferReader reader) { - final timestamp = reader.readUInt32LE(); - final pathLen = reader.readByte(); - final path = reader.readBytes(pathLen); - return { - 'timestamp': timestamp, - 'pathLen': pathLen, - 'path': path, - }; -} -``` - ---- - -### 9. CMD_GET_TUNING_PARAMS (43) - -**Status**: Not defined -**Priority**: Medium -**Use Case**: Read mesh network timing configuration - -**Response**: `RESP_CODE_TUNING_PARAMS` (23) - -**Parameters**: -- Offset 0: Command code (43) -- No additional parameters - -**C++ Implementation** (`MyMesh.cpp:1175-1181`): -```cpp -else if (cmd_frame[0] == CMD_GET_TUNING_PARAMS) { - uint32_t rx = _prefs.rx_delay_base * 1000, af = _prefs.airtime_factor * 1000; - int i = 0; - out_frame[i++] = RESP_CODE_TUNING_PARAMS; - memcpy(&out_frame[i], &rx, 4); i += 4; - memcpy(&out_frame[i], &af, 4); i += 4; - _serial->writeFrame(out_frame, i); -} -``` - -**What It Does**: Returns mesh network tuning parameters - base RX delay and airtime factor. These control message retransmission timing. - -**Response Format**: -- Byte 0: `RESP_CODE_TUNING_PARAMS` (23) -- Bytes 1-4: RX delay base (uint32_t LE, in milliseconds) -- Bytes 5-8: Airtime factor (uint32_t LE, scaled by 1000) - -**Implementation Notes**: -- No input parameters -- Converts internal floats (milliseconds/factor) to uint32_t by multiplying by 1000 -- Values allow app to understand current mesh timing constraints -- Related to `CMD_SET_TUNING_PARAMS` for configuration -- Pair command with code 21 - -**Flutter Implementation Guide**: -```dart -// Constants -static const int cmdGetTuningParams = 43; -static const int respTuningParams = 23; - -// Frame Builder -static Uint8List buildGetTuningParams() { - final writer = BufferWriter(); - writer.writeByte(MeshCoreConstants.cmdGetTuningParams); - return writer.toBytes(); -} - -// Frame Parser -static Map parseTuningParams(BufferReader reader) { - final rxDelayMs = reader.readUInt32LE(); - final airtimeFactor = reader.readUInt32LE(); - return { - 'rxDelayBase': rxDelayMs / 1000.0, // Convert back to seconds - 'airtimeFactor': airtimeFactor / 1000.0, - }; -} -``` - ---- - -### 10. CMD_FACTORY_RESET (51) - -**Status**: Not defined -**Priority**: Low (destructive) -**Use Case**: Complete device reset - -**Response**: `RESP_CODE_OK` (0) or `RESP_CODE_ERR` (1), then device reboots - -**Parameters**: -- Offset 0: Command code (51) -- Offset 1-5: Magic string "reset" (required for safety) - -**C++ Implementation** (`MyMesh.cpp:1538-1546`): -```cpp -else if (cmd_frame[0] == CMD_FACTORY_RESET && memcmp(&cmd_frame[1], "reset", 5) == 0) { - bool success = _store->formatFileSystem(); - if (success) { - writeOKFrame(); - delay(1000); - board.reboot(); // doesn't return - } else { - writeErrFrame(ERR_CODE_FILE_IO_ERROR); - } -} -``` - -**What It Does**: Performs complete factory reset - erases all file system data (contacts, messages, settings) and reboots device. **DESTRUCTIVE** operation. - -**Error Codes**: -- `ERR_CODE_FILE_IO_ERROR` (5): Erase failed - -**Implementation Notes**: -- Safety check: requires magic string "reset" at offset 1-5 -- Min frame length: 6 bytes -- Erases entire file system via `_store->formatFileSystem()` -- Does not preserve identity/private key - full reset -- Device reboots after 1-second delay (doesn't return from function) -- **CRITICAL**: No recovery possible after execution - -**Flutter Implementation Guide**: -```dart -// Constants -static const int cmdFactoryReset = 51; - -// Frame Builder -static Uint8List buildFactoryReset() { - final writer = BufferWriter(); - writer.writeByte(MeshCoreConstants.cmdFactoryReset); - writer.writeString('reset'); // Magic string - return writer.toBytes(); -} - -// Service API with confirmation dialog -Future factoryReset() async { - // IMPORTANT: Show user confirmation dialog first! - final confirmed = await showConfirmationDialog( - title: 'Factory Reset', - message: 'This will erase ALL data and reboot the device. Continue?', - destructive: true, - ); - - if (!confirmed) return; - - final frame = FrameBuilder.buildFactoryReset(); - await _commandSender.sendCommand(frame); - // Device will reboot, connection will be lost -} -``` - ---- - -### 11. CMD_SEND_PATH_DISCOVERY_REQ (52) - -**Status**: Not defined -**Priority**: Medium -**Use Case**: Network topology discovery - -**Response**: `RESP_CODE_SENT` (6) - -**Parameters**: -- Offset 0: Command code (52) -- Offset 1: Flags byte (currently only 0 is supported) -- Offset 2-33: Public key (32 bytes) of target node - -**C++ Implementation** (`MyMesh.cpp:1298-1326`): -```cpp -else if (cmd_frame[0] == CMD_SEND_PATH_DISCOVERY_REQ && cmd_frame[1] == 0 && len >= 2 + PUB_KEY_SIZE) { - uint8_t *pub_key = &cmd_frame[2]; - ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); - if (recipient) { - uint32_t tag, est_timeout; - uint8_t req_data[9]; - req_data[0] = REQ_TYPE_GET_TELEMETRY_DATA; - req_data[1] = ~(TELEM_PERM_BASE); - memset(&req_data[2], 0, 3); - getRNG()->random(&req_data[5], 4); - auto save = recipient->out_path_len; - recipient->out_path_len = -1; // force flood - int result = sendRequest(*recipient, req_data, sizeof(req_data), tag, est_timeout); - recipient->out_path_len = save; - if (result == MSG_SEND_FAILED) { - writeErrFrame(ERR_CODE_TABLE_FULL); - } else { - clearPendingReqs(); - pending_discovery = tag; - out_frame[0] = RESP_CODE_SENT; - out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0; - memcpy(&out_frame[2], &tag, 4); - memcpy(&out_frame[6], &est_timeout, 4); - _serial->writeFrame(out_frame, 10); - } - } else { - writeErrFrame(ERR_CODE_NOT_FOUND); - } -} -``` - -**What It Does**: Sends a special telemetry request to discover paths to a target node. Forces flood routing to explore network topology and find all available paths. - -**Response Format** (`RESP_CODE_SENT` - 6): -- Byte 0: `RESP_CODE_SENT` (6) -- Byte 1: Flood flag (1 = flooded, 0 = direct) -- Bytes 2-5: Request tag (uint32_t LE) - used to match responses -- Bytes 6-9: Estimated timeout (uint32_t LE, milliseconds) - -**Error Codes**: -- `ERR_CODE_NOT_FOUND` (2): Contact not found -- `ERR_CODE_TABLE_FULL` (3): Packet pool exhausted - -**Implementation Notes**: -- Min frame length: 35 bytes -- Flags byte must be 0 (only current valid value) -- Temporarily forces contact's path to -1 (flood mode) -- Includes telemetry request type with inverted BASE permission mask -- Adds 4 random bytes to make packet unique -- Clears any pending requests before sending -- Stores tag in `pending_discovery` for response matching - -**Flutter Implementation Guide**: -```dart -// Constants -static const int cmdSendPathDiscoveryReq = 52; - -// Frame Builder -static Uint8List buildSendPathDiscoveryReq(Uint8List contactPublicKey) { - final writer = BufferWriter(); - writer.writeByte(MeshCoreConstants.cmdSendPathDiscoveryReq); - writer.writeByte(0); // Flags (must be 0) - writer.writeBytes(contactPublicKey); // 32 bytes - return writer.toBytes(); -} - -// Service API (reuses existing parseSentConfirmation) -Future> discoverPathsTo(Uint8List contactPublicKey) async { - final frame = FrameBuilder.buildSendPathDiscoveryReq(contactPublicKey); - // Wait for RESP_CODE_SENT with tag - await _commandSender.sendCommand(frame); - // Returns: {expectedAckTag, suggestedTimeout, isFloodMode} -} -``` - ---- - -## Commands Defined But Not Implemented - -These commands are defined in `lib/services/meshcore_constants.dart` but have no FrameBuilder method or service API. - -### 12. CMD_EXPORT_CONTACT (17) - -**Status**: Defined (`meshcore_constants.dart:31`) -**Priority**: Medium -**Use Case**: Backup/share contacts in portable format - -**Response**: `RESP_CODE_EXPORT_CONTACT` (11) or `RESP_CODE_ERR` (1) - -**Parameters**: -- Offset 0: Command code (17) -- Offset 1-32: Public key (32 bytes) - optional; if missing, exports SELF - -**C++ Implementation** (`MyMesh.cpp:1079-1108`): -```cpp -else if (cmd_frame[0] == CMD_EXPORT_CONTACT) { - if (len < 1 + PUB_KEY_SIZE) { - // export SELF - mesh::Packet* pkt; - if (_prefs.advert_loc_policy == ADVERT_LOC_NONE) { - pkt = createSelfAdvert(_prefs.node_name); - } else { - pkt = createSelfAdvert(_prefs.node_name, sensors.node_lat, sensors.node_lon); - } - if (pkt) { - pkt->header |= ROUTE_TYPE_FLOOD; - out_frame[0] = RESP_CODE_EXPORT_CONTACT; - uint8_t out_len = pkt->writeTo(&out_frame[1]); - releasePacket(pkt); - _serial->writeFrame(out_frame, out_len + 1); - } else { - writeErrFrame(ERR_CODE_TABLE_FULL); - } - } else { - uint8_t *pub_key = &cmd_frame[1]; - ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); - uint8_t out_len; - if (recipient && (out_len = exportContact(*recipient, &out_frame[1])) > 0) { - out_frame[0] = RESP_CODE_EXPORT_CONTACT; - _serial->writeFrame(out_frame, out_len + 1); - } else { - writeErrFrame(ERR_CODE_NOT_FOUND); - } - } -} -``` - -**What It Does**: Exports a contact (or self) in mesh packet format. Used to share contact information in a portable, encrypted format. - -**Response Format**: -- Byte 0: `RESP_CODE_EXPORT_CONTACT` (11) -- Bytes 1+: Serialized mesh packet (variable length) - -**Error Codes**: -- `ERR_CODE_TABLE_FULL` (3): Packet pool exhausted (self export) -- `ERR_CODE_NOT_FOUND` (2): Contact not found - -**Implementation Notes**: -- Two modes: with/without pubkey parameter -- If no pubkey (len < 33): exports self advertisement - - Respects `advert_loc_policy` (include GPS or not) - - Sets ROUTE_TYPE_FLOOD flag in packet header -- If pubkey provided: exports stored contact via `exportContact()` -- Variable response length based on packet data -- Packet is serialized and ready to transmit - -**Flutter Implementation Guide**: -```dart -// Already defined in constants -// static const int cmdExportContact = 17; -static const int respExportContact = 11; - -// Frame Builder -static Uint8List buildExportContact({Uint8List? contactPublicKey}) { - final writer = BufferWriter(); - writer.writeByte(MeshCoreConstants.cmdExportContact); - if (contactPublicKey != null) { - writer.writeBytes(contactPublicKey); // 32 bytes - } - // If no pubkey, exports SELF - return writer.toBytes(); -} - -// Frame Parser -static Uint8List parseExportContact(BufferReader reader) { - // Returns serialized packet data - return reader.readRemainingBytes(); -} - -// Service API -Future exportContact({Uint8List? contactPublicKey}) async { - final frame = FrameBuilder.buildExportContact(contactPublicKey: contactPublicKey); - // TODO: Wait for RESP_CODE_EXPORT_CONTACT and return packet data - await _commandSender.sendCommand(frame); -} -``` - ---- - -### 13. CMD_IMPORT_CONTACT (18) - -**Status**: Defined (`meshcore_constants.dart:32`) -**Priority**: Medium -**Use Case**: Restore/import contacts from portable format - -**Response**: `RESP_CODE_OK` (0) or `RESP_CODE_ERR` (1) - -**Parameters**: -- Offset 0: Command code (18) -- Offset 1+: Serialized contact packet (min 97 bytes: 1 cmd + 32 pubkey + 64 signature) - -**C++ Implementation** (`MyMesh.cpp:1109-1114`): -```cpp -else if (cmd_frame[0] == CMD_IMPORT_CONTACT && len > 2 + 32 + 64) { - if (importContact(&cmd_frame[1], len - 1)) { - writeOKFrame(); - } else { - writeErrFrame(ERR_CODE_ILLEGAL_ARG); - } -} -``` - -**What It Does**: Imports a contact from a serialized mesh packet. Parses and validates contact data, then adds to local contact list. - -**Error Codes**: -- `ERR_CODE_ILLEGAL_ARG` (6): Packet is invalid/malformed - -**Implementation Notes**: -- Min frame length: 98 bytes (1 cmd + 97 packet minimum) -- Validates packet format (32-byte pubkey, 64-byte signature minimum) -- Calls internal `importContact()` to parse and store -- Contact is added to local storage if successful -- Opposite of `CMD_EXPORT_CONTACT` - -**Flutter Implementation Guide**: -```dart -// Already defined in constants -// static const int cmdImportContact = 18; - -// Frame Builder -static Uint8List buildImportContact(Uint8List packetData) { - if (packetData.length < 96) { // 32 pubkey + 64 signature minimum - throw ArgumentError('Contact packet too small (min 96 bytes)'); - } - final writer = BufferWriter(); - writer.writeByte(MeshCoreConstants.cmdImportContact); - writer.writeBytes(packetData); - return writer.toBytes(); -} - -// Service API -Future importContact(Uint8List packetData) async { - final frame = FrameBuilder.buildImportContact(packetData); - await _commandSender.sendCommand(frame); -} -``` - ---- - -### 14. CMD_REBOOT (19) - -**Status**: Defined (`meshcore_constants.dart:33`) -**Priority**: Low -**Use Case**: Restart radio device - -**Response**: None (device reboots) - -**Parameters**: -- Offset 0: Command code (19) -- Offset 1-6: Magic string "reboot" (required for safety) - -**C++ Implementation** (`MyMesh.cpp:1198-1202`): -```cpp -else if (cmd_frame[0] == CMD_REBOOT && memcmp(&cmd_frame[1], "reboot", 6) == 0) { - if (dirty_contacts_expiry) { - saveContacts(); - } - board.reboot(); -} -``` - -**What It Does**: Reboots the radio device. Gracefully saves any pending contact changes before restart. - -**Implementation Notes**: -- Safety check: requires magic string "reboot" at offset 1-6 -- Min frame length: 7 bytes -- Checks for pending contact writes (dirty_contacts_expiry) -- Saves contacts if needed before rebooting -- Calls `board.reboot()` which doesn't return -- Device goes offline immediately (no response sent) - -**Flutter Implementation Guide**: -```dart -// Already defined in constants -// static const int cmdReboot = 19; - -// Frame Builder -static Uint8List buildReboot() { - final writer = BufferWriter(); - writer.writeByte(MeshCoreConstants.cmdReboot); - writer.writeString('reboot'); // Magic string - return writer.toBytes(); -} - -// Service API -Future rebootDevice() async { - final frame = FrameBuilder.buildReboot(); - await _commandSender.sendCommand(frame); - // Device will reboot, connection will be lost - // App should handle disconnection gracefully -} -``` - ---- - -### 15. CMD_SET_TUNING_PARAMS (21) - -**Status**: Defined (`meshcore_constants.dart:35`) -**Priority**: Medium -**Use Case**: Configure mesh network timing - -**Response**: `RESP_CODE_OK` (0) or `RESP_CODE_ERR` (1) - -**Parameters**: -- Offset 0: Command code (21) -- Offset 1-4: RX delay base (uint32_t LE, milliseconds, scaled by 1000) -- Offset 5-8: Airtime factor (uint32_t LE, scaled by 1000) - -**C++ Implementation** (`MyMesh.cpp:1164-1174`): -```cpp -else if (cmd_frame[0] == CMD_SET_TUNING_PARAMS) { - int i = 1; - uint32_t rx, af; - memcpy(&rx, &cmd_frame[i], 4); i += 4; - memcpy(&af, &cmd_frame[i], 4); i += 4; - _prefs.rx_delay_base = ((float)rx) / 1000.0f; - _prefs.airtime_factor = ((float)af) / 1000.0f; - savePrefs(); - writeOKFrame(); -} -``` - -**What It Does**: Configures mesh network tuning parameters - RX delay and airtime factor. Controls message retransmission behavior and timeout calculations. - -**Implementation Notes**: -- Min frame length: 9 bytes -- RX delay base: milliseconds, stored as float by dividing by 1000 -- Airtime factor: stored as float by dividing by 1000 -- Values control flooding and direct message timeout calculations -- Always persists to preferences via `savePrefs()` -- No validation of ranges (accepts any uint32_t values) -- Pair command: `CMD_GET_TUNING_PARAMS` to read current values - -**Flutter Implementation Guide**: -```dart -// Already defined in constants -// static const int cmdSetTuningParams = 21; - -// Frame Builder -static Uint8List buildSetTuningParams({ - required double rxDelayBase, // seconds - required double airtimeFactor, -}) { - final writer = BufferWriter(); - writer.writeByte(MeshCoreConstants.cmdSetTuningParams); - writer.writeUInt32LE((rxDelayBase * 1000).round()); // Convert to ms - writer.writeUInt32LE((airtimeFactor * 1000).round()); - return writer.toBytes(); -} - -// Service API -Future setTuningParams({ - required double rxDelayBase, - required double airtimeFactor, -}) async { - final frame = FrameBuilder.buildSetTuningParams( - rxDelayBase: rxDelayBase, - airtimeFactor: airtimeFactor, - ); - await _commandSender.sendCommand(frame); -} -``` - ---- - -### 16. CMD_EXPORT_PRIVATE_KEY (23) - -**Status**: Defined (`meshcore_constants.dart:37`) -**Priority**: Low (security risk) -**Use Case**: Device migration/backup - -**Response**: `RESP_CODE_PRIVATE_KEY` (14) or `RESP_CODE_DISABLED` (15) - -**Parameters**: -- Offset 0: Command code (23) -- No additional parameters - -**C++ Implementation** (`MyMesh.cpp:1214-1222`): -```cpp -else if (cmd_frame[0] == CMD_EXPORT_PRIVATE_KEY) { -#if ENABLE_PRIVATE_KEY_EXPORT - uint8_t reply[65]; - reply[0] = RESP_CODE_PRIVATE_KEY; - self_id.writeTo(&reply[1], 64); - _serial->writeFrame(reply, 65); -#else - writeDisabledFrame(); -#endif -} -``` - -**What It Does**: Exports the device's private key/identity. Used for backup or device migration. Can be disabled at compile-time for security. - -**Response Format**: -- Byte 0: `RESP_CODE_PRIVATE_KEY` (14) -- Bytes 1-64: Serialized identity (64 bytes from self_id.writeTo()) - -**Implementation Notes**: -- No input parameters -- Guarded by compile-time flag `ENABLE_PRIVATE_KEY_EXPORT` -- If disabled, returns `RESP_CODE_DISABLED` (15) instead -- Exports complete private identity -- **SECURITY RISK**: Exposes private key over BLE -- Response always exactly 65 bytes when enabled - -**Flutter Implementation Guide**: -```dart -// Already defined in constants -// static const int cmdExportPrivateKey = 23; -// static const int respPrivateKey = 14; -// static const int respDisabled = 15; - -// Frame Builder -static Uint8List buildExportPrivateKey() { - final writer = BufferWriter(); - writer.writeByte(MeshCoreConstants.cmdExportPrivateKey); - return writer.toBytes(); -} - -// Frame Parser -static Uint8List parsePrivateKey(BufferReader reader) { - return reader.readBytes(64); // 64-byte identity -} - -// Service API -Future exportPrivateKey() async { - final frame = FrameBuilder.buildExportPrivateKey(); - // TODO: Handle RESP_CODE_PRIVATE_KEY or RESP_CODE_DISABLED - await _commandSender.sendCommand(frame); -} -``` - ---- - -### 17. CMD_IMPORT_PRIVATE_KEY (24) - -**Status**: Defined (`meshcore_constants.dart:38`) -**Priority**: Low (security risk) -**Use Case**: Device migration/restore - -**Response**: `RESP_CODE_OK` (0) or `RESP_CODE_ERR` (1) or `RESP_CODE_DISABLED` (15) - -**Parameters**: -- Offset 0: Command code (24) -- Offset 1-64: Serialized identity (64 bytes) - -**C++ Implementation** (`MyMesh.cpp:1223-1238`): -```cpp -else if (cmd_frame[0] == CMD_IMPORT_PRIVATE_KEY && len >= 65) { -#if ENABLE_PRIVATE_KEY_IMPORT - mesh::LocalIdentity identity; - identity.readFrom(&cmd_frame[1], 64); - if (_store->saveMainIdentity(identity)) { - self_id = identity; - writeOKFrame(); - resetContacts(); - _store->loadContacts(this); - } else { - writeErrFrame(ERR_CODE_FILE_IO_ERROR); - } -#else - writeDisabledFrame(); -#endif -} -``` - -**What It Does**: Imports a private key/identity from backup or migration. Replaces device identity and reloads all contacts. - -**Error Codes**: -- `ERR_CODE_FILE_IO_ERROR` (5): Save failed - -**Implementation Notes**: -- Min frame length: 65 bytes -- Guarded by compile-time flag `ENABLE_PRIVATE_KEY_IMPORT` -- If disabled, returns `RESP_CODE_DISABLED` (15) -- Parses 64-byte identity via `identity.readFrom()` -- Persists to storage via `_store->saveMainIdentity()` -- Updates internal `self_id` object -- Calls `resetContacts()` to clear existing contacts -- Reloads contacts from storage (recalculates shared secrets) -- **SECURITY RISK**: Changes device identity -- **SIDE EFFECT**: Clears and reloads contact list - -**Flutter Implementation Guide**: -```dart -// Already defined in constants -// static const int cmdImportPrivateKey = 24; - -// Frame Builder -static Uint8List buildImportPrivateKey(Uint8List identity) { - if (identity.length != 64) { - throw ArgumentError('Identity must be exactly 64 bytes'); - } - final writer = BufferWriter(); - writer.writeByte(MeshCoreConstants.cmdImportPrivateKey); - writer.writeBytes(identity); - return writer.toBytes(); -} - -// Service API -Future importPrivateKey(Uint8List identity) async { - final frame = FrameBuilder.buildImportPrivateKey(identity); - await _commandSender.sendCommand(frame); - // Device identity changed, contacts will reload -} -``` - ---- - -### 18. CMD_SEND_RAW_DATA (25) - -**Status**: Defined (`meshcore_constants.dart:39`) -**Priority**: Low (advanced use) -**Use Case**: Custom protocol development - -**Response**: `RESP_CODE_OK` (0) or `RESP_CODE_ERR` (1) - -**Parameters**: -- Offset 0: Command code (25) -- Offset 1: Path length (-1 for flood, 0+ for direct path) -- Offset 2 to 2+pathlen: Path data (if path_len >= 0) -- Offset 2+pathlen+: Raw payload (min 4 bytes) - -**C++ Implementation** (`MyMesh.cpp:1239-1254`): -```cpp -else if (cmd_frame[0] == CMD_SEND_RAW_DATA && len >= 6) { - int i = 1; - int8_t path_len = cmd_frame[i++]; - if (path_len >= 0 && i + path_len + 4 <= len) { - uint8_t *path = &cmd_frame[i]; - i += path_len; - auto pkt = createRawData(&cmd_frame[i], len - i); - if (pkt) { - sendDirect(pkt, path, path_len); - writeOKFrame(); - } else { - writeErrFrame(ERR_CODE_TABLE_FULL); - } - } else { - writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); - } -} -``` - -**What It Does**: Sends raw binary data directly to a contact via specific path. Low-level packet transmission for custom protocols. - -**Error Codes**: -- `ERR_CODE_TABLE_FULL` (3): Packet pool exhausted -- `ERR_CODE_UNSUPPORTED_CMD` (1): Flood mode not supported (path_len == -1) - -**Implementation Notes**: -- Min frame length: 6 bytes -- Path length validation: must be >= 0 (flood not supported yet) -- Validates sufficient payload: min 4 bytes after path -- Validates frame length: i + path_len + 4 <= len -- Creates raw data packet via `createRawData()` -- Sends directly (not flood) via `sendDirect()` -- Currently **ONLY** supports direct path sending (path_len >= 0) - -**Flutter Implementation Guide**: -```dart -// Already defined in constants -// static const int cmdSendRawData = 25; - -// Frame Builder -static Uint8List buildSendRawData({ - required Uint8List path, - required Uint8List payload, -}) { - if (payload.length < 4) { - throw ArgumentError('Payload must be at least 4 bytes'); - } - final writer = BufferWriter(); - writer.writeByte(MeshCoreConstants.cmdSendRawData); - writer.writeByte(path.length); // Path length (must be >= 0) - writer.writeBytes(path); - writer.writeBytes(payload); - return writer.toBytes(); -} - -// Service API -Future sendRawData({ - required Uint8List path, - required Uint8List payload, -}) async { - final frame = FrameBuilder.buildSendRawData(path: path, payload: payload); - await _commandSender.sendCommand(frame); -} -``` - ---- - -### 19. CMD_SIGN_START (33) - -**Status**: Defined (`meshcore_constants.dart:43`) -**Priority**: Low (advanced use) -**Use Case**: Digital signatures for large data - -**Response**: `RESP_CODE_SIGN_START` (19) - -**Parameters**: -- Offset 0: Command code (33) -- No additional parameters - -**C++ Implementation** (`MyMesh.cpp:1423-1434`): -```cpp -else if (cmd_frame[0] == CMD_SIGN_START) { - out_frame[0] = RESP_CODE_SIGN_START; - out_frame[1] = 0; // reserved - uint32_t len = MAX_SIGN_DATA_LEN; - memcpy(&out_frame[2], &len, 4); - _serial->writeFrame(out_frame, 6); - - if (sign_data) { - free(sign_data); - } - sign_data = (uint8_t *)malloc(MAX_SIGN_DATA_LEN); - sign_data_len = 0; -} -``` - -**What It Does**: Initiates a multi-packet digital signature operation. Allocates buffer and resets state for accumulating data to sign. - -**Response Format**: -- Byte 0: `RESP_CODE_SIGN_START` (19) -- Byte 1: Reserved (0) -- Bytes 2-5: Maximum data length (uint32_t LE) - -**Implementation Notes**: -- No input parameters -- Always allocates MAX_SIGN_DATA_LEN bytes (8K per #define) -- Frees any previous sign_data buffer -- Initializes sign_data_len to 0 -- Response always 6 bytes -- Max signature data: 8192 bytes -- Must be followed by `CMD_SIGN_DATA` calls, then `CMD_SIGN_FINISH` -- Overwrites any previous signing session - -**Flutter Implementation Guide**: -```dart -// Already defined in constants -// static const int cmdSignStart = 33; -// static const int respSignStart = 19; - -// Frame Builder -static Uint8List buildSignStart() { - final writer = BufferWriter(); - writer.writeByte(MeshCoreConstants.cmdSignStart); - return writer.toBytes(); -} - -// Frame Parser -static int parseSignStart(BufferReader reader) { - reader.readByte(); // Skip reserved - return reader.readUInt32LE(); // Max data length -} - -// Service API -Future signStart() async { - final frame = FrameBuilder.buildSignStart(); - // TODO: Wait for RESP_CODE_SIGN_START and return max length - await _commandSender.sendCommand(frame); -} -``` - ---- - -### 20. CMD_SIGN_DATA (34) - -**Status**: Defined (`meshcore_constants.dart:44`) -**Priority**: Low (advanced use) -**Use Case**: Accumulate data for digital signature - -**Response**: `RESP_CODE_OK` (0) or `RESP_CODE_ERR` (1) - -**Parameters**: -- Offset 0: Command code (34) -- Offset 1+: Data chunk to accumulate for signing - -**C++ Implementation** (`MyMesh.cpp:1435-1442`): -```cpp -else if (cmd_frame[0] == CMD_SIGN_DATA && len > 1) { - if (sign_data == NULL || sign_data_len + (len - 1) > MAX_SIGN_DATA_LEN) { - writeErrFrame(sign_data == NULL ? ERR_CODE_BAD_STATE : ERR_CODE_TABLE_FULL); - } else { - memcpy(&sign_data[sign_data_len], &cmd_frame[1], len - 1); - sign_data_len += (len - 1); - writeOKFrame(); - } -} -``` - -**What It Does**: Accumulates data chunks to be digitally signed. Can be called multiple times to build up large data blocks. - -**Error Codes**: -- `ERR_CODE_BAD_STATE` (4): Not initialized (sign_data == NULL) -- `ERR_CODE_TABLE_FULL` (3): Accumulated data exceeds MAX_SIGN_DATA_LEN (8K) - -**Implementation Notes**: -- Min frame length: 2 bytes -- Requires `CMD_SIGN_START` to be called first -- Appends data_chunk to sign_data buffer -- Data size: len - 1 bytes (excluding command byte) -- Can be called multiple times to accumulate full message -- No response data, just success/error code - -**Flutter Implementation Guide**: -```dart -// Already defined in constants -// static const int cmdSignData = 34; - -// Frame Builder -static Uint8List buildSignData(Uint8List dataChunk) { - final writer = BufferWriter(); - writer.writeByte(MeshCoreConstants.cmdSignData); - writer.writeBytes(dataChunk); - return writer.toBytes(); -} - -// Service API -Future signData(Uint8List dataChunk) async { - final frame = FrameBuilder.buildSignData(dataChunk); - await _commandSender.sendCommand(frame); -} -``` - ---- - -### 21. CMD_SIGN_FINISH (35) - -**Status**: Defined (`meshcore_constants.dart:45`) -**Priority**: Low (advanced use) -**Use Case**: Complete digital signature operation - -**Response**: `RESP_CODE_SIGNATURE` (20) or `RESP_CODE_ERR` (1) - -**Parameters**: -- Offset 0: Command code (35) -- No additional parameters - -**C++ Implementation** (`MyMesh.cpp:1443-1454`): -```cpp -else if (cmd_frame[0] == CMD_SIGN_FINISH) { - if (sign_data) { - self_id.sign(&out_frame[1], sign_data, sign_data_len); - - free(sign_data); - sign_data = NULL; - - out_frame[0] = RESP_CODE_SIGNATURE; - _serial->writeFrame(out_frame, 1 + SIGNATURE_SIZE); - } else { - writeErrFrame(ERR_CODE_BAD_STATE); - } -} -``` - -**What It Does**: Completes the signing operation. Generates digital signature over accumulated data and returns result. - -**Response Format**: -- Byte 0: `RESP_CODE_SIGNATURE` (20) -- Bytes 1+: Digital signature (SIGNATURE_SIZE bytes) - -**Error Codes**: -- `ERR_CODE_BAD_STATE` (4): Not initialized (sign_data == NULL) - -**Implementation Notes**: -- No input parameters -- Requires `CMD_SIGN_START` and one or more `CMD_SIGN_DATA` calls -- Signs accumulated data via `self_id.sign()` -- Frees sign_data buffer after signing -- Response length: 1 + SIGNATURE_SIZE bytes -- Signs all accumulated bytes from CMD_SIGN_DATA calls -- Signature uses device's private key (self_id) - -**Flutter Implementation Guide**: -```dart -// Already defined in constants -// static const int cmdSignFinish = 35; -// static const int respSignature = 20; - -// Frame Builder -static Uint8List buildSignFinish() { - final writer = BufferWriter(); - writer.writeByte(MeshCoreConstants.cmdSignFinish); - return writer.toBytes(); -} - -// Frame Parser -static Uint8List parseSignature(BufferReader reader) { - // Returns signature bytes (SIGNATURE_SIZE) - return reader.readRemainingBytes(); -} - -// Service API -Future signFinish() async { - final frame = FrameBuilder.buildSignFinish(); - // TODO: Wait for RESP_CODE_SIGNATURE and return signature - await _commandSender.sendCommand(frame); -} -``` - ---- - -### 22. CMD_SEND_TRACE_PATH (36) - -**Status**: Defined (`meshcore_constants.dart:47`) -**Priority**: Medium -**Use Case**: Network diagnostics and topology analysis - -**Response**: `RESP_CODE_SENT` (6) or `RESP_CODE_ERR` (1) - -**Parameters**: -- Offset 0: Command code (36) -- Offset 1-4: Tag (uint32_t LE) - request identifier -- Offset 5-8: Auth code (uint32_t LE) - authentication/validation -- Offset 9: Flags byte -- Offset 10+: Path data (variable length, < MAX_PATH_SIZE) - -**C++ Implementation** (`MyMesh.cpp:1455-1474`): -```cpp -else if (cmd_frame[0] == CMD_SEND_TRACE_PATH && len > 10 && len - 10 < MAX_PATH_SIZE) { - uint32_t tag, auth; - memcpy(&tag, &cmd_frame[1], 4); - memcpy(&auth, &cmd_frame[5], 4); - auto pkt = createTrace(tag, auth, cmd_frame[9]); - if (pkt) { - uint8_t path_len = len - 10; - sendDirect(pkt, &cmd_frame[10], path_len); - - uint32_t t = _radio->getEstAirtimeFor(pkt->payload_len + pkt->path_len + 2); - uint32_t est_timeout = calcDirectTimeoutMillisFor(t, path_len); - - out_frame[0] = RESP_CODE_SENT; - out_frame[1] = 0; - memcpy(&out_frame[2], &tag, 4); - memcpy(&out_frame[6], &est_timeout, 4); - _serial->writeFrame(out_frame, 10); - } else { - writeErrFrame(ERR_CODE_TABLE_FULL); - } -} -``` - -**What It Does**: Sends a trace/path report packet to track network topology. Used for network path discovery and diagnostics. - -**Response Format** (`RESP_CODE_SENT`): -- Byte 0: `RESP_CODE_SENT` (6) -- Byte 1: 0 (reserved/flags) -- Bytes 2-5: Tag (uint32_t LE, echoed from request) -- Bytes 6-9: Estimated timeout (uint32_t LE, milliseconds) - -**Error Codes**: -- `ERR_CODE_TABLE_FULL` (3): Packet pool exhausted - -**Implementation Notes**: -- Min frame length: 11 bytes (1 cmd + 4 tag + 4 auth + 1 flags + min 1 path) -- Max frame length: 10 + MAX_PATH_SIZE -- Path length: len - 10 bytes -- Creates trace packet via `createTrace(tag, auth, flags)` -- Sends directly to specified path via `sendDirect()` -- Calculates estimated airtime based on payload/path -- Response always 10 bytes if successful -- Tag parameter allows matching response to request -- Auth code included in packet for validation/replay protection -- Flags byte passed to createTrace (purpose depends on implementation) - -**Flutter Implementation Guide**: -```dart -// Already defined in constants -// static const int cmdSendTracePath = 36; - -// Frame Builder -static Uint8List buildSendTracePath({ - required int tag, - required int authCode, - required int flags, - required Uint8List path, -}) { - final writer = BufferWriter(); - writer.writeByte(MeshCoreConstants.cmdSendTracePath); - writer.writeUInt32LE(tag); - writer.writeUInt32LE(authCode); - writer.writeByte(flags); - writer.writeBytes(path); - return writer.toBytes(); -} - -// Service API (reuses existing parseSentConfirmation) -Future> sendTracePath({ - required int tag, - required int authCode, - required int flags, - required Uint8List path, -}) async { - final frame = FrameBuilder.buildSendTracePath( - tag: tag, - authCode: authCode, - flags: flags, - path: path, - ); - // Wait for RESP_CODE_SENT - await _commandSender.sendCommand(frame); - // Returns: {expectedAckTag, suggestedTimeout, isFloodMode} -} -``` - ---- - -## Implementation Priority Matrix - -### High Priority (Essential Features) -1. **CMD_HAS_CONNECTION (28)** - Check connectivity before sending -2. **CMD_GET_CONTACT_BY_KEY (30)** - Essential for contact lookup - -### Medium Priority (Useful Features) -3. **CMD_LOGOUT (29)** - Room server management -4. **CMD_SHARE_CONTACT (16)** - Contact distribution -5. **CMD_EXPORT_CONTACT (17)** - Contact backup -6. **CMD_IMPORT_CONTACT (18)** - Contact restore -7. **CMD_SET_TUNING_PARAMS (21)** - Network optimization -8. **CMD_GET_TUNING_PARAMS (43)** - Read current settings -9. **CMD_GET_ADVERT_PATH (42)** - Network diagnostics -10. **CMD_SEND_PATH_DISCOVERY_REQ (52)** - Topology discovery -11. **CMD_SEND_TRACE_PATH (36)** - Path tracking - -### Low Priority (Advanced/Specialized) -12. **CMD_REBOOT (19)** - Device management -13. **CMD_SET_DEVICE_PIN (37)** - BLE security -14. **CMD_GET_CUSTOM_VARS (40)** - Sensor config -15. **CMD_SET_CUSTOM_VAR (41)** - Sensor config -16. **CMD_SEND_RAW_DATA (25)** - Custom protocols -17. **CMD_SIGN_START (33)** - Digital signatures -18. **CMD_SIGN_DATA (34)** - Digital signatures -19. **CMD_SIGN_FINISH (35)** - Digital signatures - -### Very Low Priority (Security Risks / Destructive) -20. **CMD_EXPORT_PRIVATE_KEY (23)** - May be disabled -21. **CMD_IMPORT_PRIVATE_KEY (24)** - May be disabled -22. **CMD_FACTORY_RESET (51)** - Destructive operation - ---- - -## Quick Reference Table - -| Code | Name | Status | Priority | Response | Min Len | Key Feature | -|------|------|--------|----------|----------|---------|-------------| -| 16 | SHARE_CONTACT | Not Defined | Medium | OK/ERR | 33 | Broadcasts contact zero-hop | -| 17 | EXPORT_CONTACT | Defined | Medium | RESP_11/ERR | 1-33 | Exports packet format | -| 18 | IMPORT_CONTACT | Defined | Medium | OK/ERR | 98 | Imports packet format | -| 19 | REBOOT | Defined | Low | None | 7 | Graceful restart | -| 21 | SET_TUNING_PARAMS | Defined | Medium | OK/ERR | 9 | Mesh timing config | -| 23 | EXPORT_PRIVATE_KEY | Defined | Very Low | RESP_14/DIS | 1 | 64B identity (risky) | -| 24 | IMPORT_PRIVATE_KEY | Defined | Very Low | OK/ERR/DIS | 65 | Changes identity (risky) | -| 25 | SEND_RAW_DATA | Defined | Low | OK/ERR | 6 | Custom protocol send | -| 28 | HAS_CONNECTION | Not Defined | High | OK/ERR | 33 | Check path exists | -| 29 | LOGOUT | Not Defined | Medium | OK | 33 | Disconnect from room | -| 30 | GET_CONTACT_BY_KEY | Not Defined | High | RESP_3/ERR | 33 | Full contact lookup | -| 33 | SIGN_START | Defined | Low | RESP_19 | 1 | Init signature (8K) | -| 34 | SIGN_DATA | Defined | Low | OK/ERR | 2 | Accumulate data | -| 35 | SIGN_FINISH | Defined | Low | RESP_20/ERR | 1 | Generate signature | -| 36 | SEND_TRACE_PATH | Defined | Medium | RESP_6/ERR | 11 | Network trace | -| 37 | SET_DEVICE_PIN | Not Defined | Low | OK/ERR | 5 | BLE pairing PIN | -| 40 | GET_CUSTOM_VARS | Not Defined | Low | RESP_21 | 1 | Sensor settings (CSV) | -| 41 | SET_CUSTOM_VAR | Not Defined | Low | OK/ERR | 4 | Set sensor value | -| 42 | GET_ADVERT_PATH | Not Defined | Medium | RESP_22/ERR | 9 | Advert path history | -| 43 | GET_TUNING_PARAMS | Not Defined | Medium | RESP_23 | 1 | Read mesh timing | -| 51 | FACTORY_RESET | Not Defined | Very Low | OK/ERR | 6 | Erase all (risky) | -| 52 | SEND_PATH_DISCOVERY | Not Defined | Medium | RESP_6 | 35 | Flood for paths | - ---- - -## Notes - -- All implementations based on analysis of `/Users/dz0ny/meshcore-sar/MeshCore/examples/companion_radio/MyMesh.cpp` -- Commands marked "Not Defined" need to be added to `lib/services/meshcore_constants.dart` first -- Commands marked "Defined" need FrameBuilder methods and service APIs -- Response codes not yet defined in Flutter: - - `RESP_CODE_EXPORT_CONTACT` (11) - - `RESP_CODE_SIGN_START` (19) - - `RESP_CODE_SIGNATURE` (20) - - `RESP_CODE_CUSTOM_VARS` (21) - - `RESP_CODE_ADVERT_PATH` (22) - - `RESP_CODE_TUNING_PARAMS` (23) -- Magic strings for safety: "reboot" (6 chars), "reset" (5 chars) -- Compile-time flags may disable private key import/export -- Some commands are destructive (factory reset, reboot) -- Digital signing commands (33-35) work as a sequence -- Network diagnostics commands (42, 52, 36) useful for mesh analysis - ---- - -**Document Version**: 1.0 -**Last Updated**: 2025-01-26 -**Reference**: MeshCore Companion Radio Protocol v1 -**Flutter App**: MeshCore SAR Application diff --git a/VECTOR_MAPS.md b/VECTOR_MAPS.md deleted file mode 100644 index 04dbf19..0000000 --- a/VECTOR_MAPS.md +++ /dev/null @@ -1,265 +0,0 @@ -# Vector Map Tiles with MBTiles - User Guide - -This guide explains how to use offline vector map tiles in MeshCore SAR app. - -## Overview - -The app now supports **offline vector map tiles** using the MBTiles format. Vector tiles provide: - -- ✅ **True Offline Maps**: Work without any internet connection after initial import -- ✅ **Smaller File Sizes**: ~70% smaller than raster tiles (e.g., 150MB vs 500MB) -- ✅ **Better Performance**: Smooth zooming with over-zooming support -- ✅ **Customizable Styles**: Change map appearance without re-downloading tiles -- ✅ **SAR-Optimized**: Topographic styles ideal for search & rescue operations - -## Quick Start - -### 1. Download MBTiles File - -Download a vector tile MBTiles file for your region. Recommended sources: - -**Option A: Geofabrik (via MapTiler)** - Recommended for Slovenia -``` -URL: https://geodata.maptiler.download/extracts/osm/v3.11/2020-02-10/europe/osm-2020-02-10-v3.11_europe_slovenia.mbtiles -Schema: Shortbread -Size: ~150MB (Slovenia) -``` - -**Option B: Protomaps** - Global coverage -``` -URL: https://maps.protomaps.com/builds/ -Format: PMTiles (can be converted to MBTiles) -``` - -**Option C: OpenMapTiles** - Self-hosted -``` -URL: https://openmaptiles.org/downloads/ -Schema: OpenMapTiles -Requires: Account (free tier available) -``` - -### 2. Import MBTiles File - -1. Open **Settings** → **Map Management** -2. Scroll to **"Offline Vector Maps"** section -3. Tap **"Import MBTiles File"** -4. Select your downloaded `.mbtiles` file -5. Wait for import to complete - -### 3. Select Vector Layer - -1. Go to the **Map** tab -2. Tap the **Layers** button (bottom-right) -3. Select your imported vector map from the list -4. The app will automatically download the appropriate style - -### 4. Enjoy Offline Maps! - -Your vector maps now work completely offline. No internet required! - -## Detailed Features - -### File Management - -The **Map Management** screen shows: - -- **File Name**: Name from MBTiles metadata -- **File Size**: Human-readable (KB/MB/GB) -- **Format**: PBF (vector) or PNG/JPG (raster) -- **Zoom Levels**: Min and max zoom supported -- **Geographic Bounds**: Coverage area coordinates -- **Vector Schema**: Shortbread, OpenMapTiles, or Unknown - -**Actions:** -- **Expand Card**: Tap to see full metadata -- **Delete**: Tap delete button (confirmation required) -- **Refresh**: Tap refresh icon to reload list - -### Supported Vector Schemas - -#### Shortbread (Geofabrik) -- **Best for**: European regions, SAR operations -- **Style Source**: versatiles.org -- **Compatible MBTiles**: Geofabrik extracts -- **Compression**: Gzipped PBF data - -#### OpenMapTiles -- **Best for**: Global coverage, detailed mapping -- **Style Source**: openmaptiles.org or custom -- **Compatible MBTiles**: OpenMapTiles downloads -- **Compression**: Standard PBF data - -### Map Styles - -Vector tile styles are automatically downloaded from: - -**Versatiles (Shortbread):** -``` -https://tiles.versatiles.org/assets/styles/colorful.json -``` - -**Features:** -- Topographic contours -- Road classification -- Building outlines -- Natural features (forests, water) -- POI markers - -### Technical Details - -#### Storage Location -``` -iOS: /Documents/offline_maps/ -Android: /data/data/com.meshcore.sar/files/offline_maps/ -``` - -#### Supported Formats -- **Vector**: PBF (Protocol Buffer Format), MVT (Mapbox Vector Tile) -- **Compression**: Auto-detected gzip compression -- **Schema**: Shortbread, OpenMapTiles, or custom - -#### Performance - -**Slovenia Example (Geofabrik):** -- File Size: ~150MB -- Zoom Levels: 0-14 -- Tile Count: ~500,000 tiles -- Load Time: <2 seconds - -**Comparison with Raster:** -- Raster (same area, zoom 0-16): ~500MB -- Vector advantage: **70% smaller** - -## Troubleshooting - -### Import Fails - -**Error**: "Failed to import MBTiles file" - -**Solutions:** -1. Verify file is valid MBTiles format (use `mbtiles` CLI to validate) -2. Check file permissions (ensure app can read the file) -3. Ensure sufficient storage space available -4. Try re-downloading the MBTiles file - -### Style Not Loading - -**Error**: "Failed to load map style" - -**Solutions:** -1. Check internet connection (required for first-time style download) -2. Wait and retry (remote servers may be temporarily down) -3. Clear app cache and restart -4. Verify MBTiles schema matches style (Shortbread vs OpenMapTiles) - -### Map Not Displaying - -**Symptoms**: Blank map or only showing other layers - -**Solutions:** -1. Verify layer is selected in layer picker -2. Check zoom level is within MBTiles zoom range -3. Pan to area covered by MBTiles bounds -4. Restart app to reload layers - -### Black Screen on Map - -**Cause**: Vector theme not loaded yet - -**Solution:** Wait for style download to complete (loading indicator shows progress) - -## Advanced Usage - -### Using Custom Styles - -To use custom vector tile styles: - -1. Host your style JSON on a web server -2. Modify `MapLayer.fromMbtilesFile()` to use your style URL -3. Ensure style schema matches your MBTiles schema - -Example style URL format: -``` -https://your-server.com/styles/custom-sar-style.json -``` - -### Converting Other Formats - -**PMTiles → MBTiles:** -```bash -# Using tippecanoe -pmtiles extract region.pmtiles region.mbtiles -``` - -**Shapefile → MBTiles:** -```bash -# Using tippecanoe -tippecanoe -o output.mbtiles input.shp -``` - -### Generating Custom MBTiles - -Use **Tilemaker** to generate MBTiles from OSM data: - -```bash -# Download OSM extract -wget https://download.geofabrik.de/europe/slovenia-latest.osm.pbf - -# Generate MBTiles with Shortbread schema -tilemaker --input slovenia-latest.osm.pbf \ - --output slovenia-custom.mbtiles \ - --config shortbread.json \ - --process shortbread.lua -``` - -## References - -### Documentation -- [Vector Map Tiles Package](https://pub.dev/packages/vector_map_tiles) -- [MBTiles Specification](https://github.com/mapbox/mbtiles-spec) -- [Shortbread Schema](https://shortbread-tiles.org/) -- [Versatiles Styles](https://versatiles.org/) - -### Tools -- [Tilemaker](https://github.com/systemed/tilemaker) - Generate MBTiles from OSM -- [MBTiles CLI](https://github.com/mapbox/mbtiles-spec) - Validate and inspect -- [Tippecanoe](https://github.com/felt/tippecanoe) - Convert and optimize tiles - -### Data Sources -- [Geofabrik](https://download.geofabrik.de/) - OSM extracts -- [Protomaps](https://protomaps.com/) - Pre-generated PMTiles -- [OpenMapTiles](https://openmaptiles.org/) - Commercial and free options - -## FAQ - -**Q: Can I use multiple MBTiles files at once?** -A: Yes! Import multiple files and switch between them using the layer picker. - -**Q: Do I need internet after importing?** -A: Only for the first-time style download. After that, fully offline. - -**Q: What's the maximum file size?** -A: No hard limit. Tested with files up to 2GB successfully. - -**Q: Can I share MBTiles files between devices?** -A: Yes! Export the `.mbtiles` file and import on another device. - -**Q: Do vector tiles work on iOS and Android?** -A: Yes! Fully supported on both platforms. - -**Q: How do I update map data?** -A: Download a new MBTiles file with updated data and import it. - -## Support - -For issues or questions: -- GitHub Issues: [meshcore-sar/issues](https://github.com/meshcore-dev/meshcore-sar/issues) -- Documentation: See CLAUDE.md for technical details -- Community: Join the MeshCore Slack/Discord - -## License - -Vector map tiles implementation uses: -- `vector_map_tiles` - MIT License -- `vector_map_tiles_mbtiles` - MIT License -- Map data copyright OpenStreetMap contributors diff --git a/WMS_IMPLEMENTATION_PLAN.md b/WMS_IMPLEMENTATION_PLAN.md deleted file mode 100644 index 3c091e7..0000000 --- a/WMS_IMPLEMENTATION_PLAN.md +++ /dev/null @@ -1,463 +0,0 @@ -# Implementation Plan: Generic WMS Server Support - -## Overview -Refactor the hardcoded Slovenian WMS implementation to support adding custom WMS servers while preserving existing functionality. - -## Goals -1. **Add WMS Server Management** - UI to add/edit/delete custom WMS servers via GetCapabilities URL -2. **Layer Selection** - Allow users to select base layers and overlays from any WMS server -3. **Preserve Slovenian Layers** - Keep existing EPSG:3794 support and language filtering -4. **Dynamic CRS** - Support common coordinate systems (EPSG:3857, 4326, 3794) -5. **Base Layer Switching** - Allow switching base layers on top of other layers - -## Implementation Phases - -### Phase 1: Core Models & Parsing (Foundation) - -**1.1 Create WMS Server Model** (`lib/models/wms_server.dart`) -- `WmsServer` class with id, name, capabilitiesUrl, version, layers list -- `WmsLayer` class with name, title, abstract, styles, CRS support, bounds -- JSON serialization for persistence - -**1.2 Implement WMS Capabilities Parser** (`lib/services/wms_capabilities_parser.dart`) -- Add `xml: ^7.0.0` dependency to pubspec.yaml -- Parse WMS 1.3.0 GetCapabilities XML -- Extract service metadata, supported formats, CRS list -- Recursively parse layer hierarchy (handle groups vs leaf layers) -- Extract bounding boxes, styles, and metadata - -**1.3 Create CRS Factory** (`lib/utils/crs_factory.dart`) -- Registry for common CRS: EPSG:3857 (Web Mercator), EPSG:4326 (WGS84), EPSG:3794 (Slovenian) -- Cache CRS instances to avoid re-creating projection objects -- Support for retrieving CRS by EPSG code - -**1.4 Create Storage Repository** (`lib/services/wms_server_repository.dart`) -- Store custom WMS servers in SharedPreferences as JSON -- CRUD operations: save, load, delete servers -- Persist layer visibility state per server/layer - -### Phase 2: UI Implementation - -**2.1 WMS Server Management Screen** (`lib/screens/wms_server_management_screen.dart`) -- List of saved WMS servers (edit/delete actions) -- "Add WMS Server" button → capabilities URL input dialog -- Show loading indicator while fetching GetCapabilities -- Parse and preview available layers -- Layer picker with checkboxes (base layers vs overlays) -- Validate CRS compatibility (warn if unsupported) -- Save button stores server + selected layers - -**2.2 Refactor Layer Selector** (`lib/screens/map_tab.dart`) -- Keep existing sections: Standard Online Layers, Built-in WMS (Slovenian - language filtered), MBTiles -- Add new section: "Custom WMS Servers" (expandable) -- Each custom server shows its base layers as selectable tiles -- "Manage WMS Servers" button at bottom → opens management screen - -**2.3 Overlay Management** -- Extend existing cadastral/forest roads overlay pattern to custom WMS layers -- Store overlay visibility per server/layer combination -- Add overlay toggles to layer selector when custom WMS base layer active - -### Phase 3: Integration & Testing - -**3.1 Refactor MapLayer Model** (`lib/models/map_layer.dart`) -- Add fields: `wmsServerId` (reference to custom server), `crsCode` (EPSG string) -- Add `isBuiltIn` flag to distinguish Slovenian layers from custom -- Factory method: `MapLayer.fromWmsServer(WmsServer, WmsLayer, Crs)` -- Keep existing Slovenian layer factories unchanged - -**3.2 Update MapProvider** (`lib/providers/map_provider.dart`) -- Load custom WMS servers on init -- Handle base layer switching with CRS changes -- Automatically clamp zoom level when switching to layers with lower maxZoom -- Persist selected custom WMS layer to SharedPreferences - -**3.3 Tile Caching Integration** -- Verify existing `getTileProviderForWms()` works with custom servers -- No changes needed (already uses `flutter_map`'s WMS tile URL generation) - -**3.4 Testing** -- Test with known WMS servers: NASA GIBS, USGS, OpenStreetMap WMS -- Test Slovenian layers still work (no regression) -- Test CRS switching (3857 ↔ 4326 ↔ 3794) -- Test offline caching with custom WMS tiles -- Test error cases: invalid URL, timeout, unsupported CRS, malformed XML - -### Phase 4: Polish & Documentation - -**4.1 Localization** -- Add strings to `lib/l10n/app_*.arb` files: - - "Add WMS Server", "Capabilities URL", "Custom WMS Servers" - - Error messages: "Invalid URL", "Parsing failed", "Unsupported CRS" -- Generate with `flutter gen-l10n` - -**4.2 Error Handling** -- Network timeout (30s) with user-friendly error -- XML parsing errors with "Invalid GetCapabilities response" -- Unsupported CRS warning with fallback suggestion -- Handle missing layer names, invalid bounds gracefully - -**4.3 Update Documentation** (`CLAUDE.md`) -- Document WMS server management workflow -- Add custom WMS section to "Common Tasks" -- Update architecture diagram with new models/services -- Add troubleshooting section for common WMS issues - -## Technical Decisions - -### Preservation of Slovenian Layers -- **Approach**: Keep existing code paths intact, add `isBuiltIn: true` flag -- **Language Filtering**: Remains unchanged (sl/hr only see Slovenian layers) -- **CRS**: EPSG:3794 remains as singleton in `slovenian_crs.dart` - -### Storage Strategy -- **Choice**: SharedPreferences (JSON serialization) -- **Rationale**: Estimated 700KB for 10 servers × 50 layers (under 1MB limit) -- **Migration Path**: Can move to SQLite if users need >10 servers - -### CRS Support (Initial Release) -- **Supported**: EPSG:3857 (Web Mercator), EPSG:4326 (WGS84), EPSG:3794 (Slovenian) -- **Unsupported**: Show warning, prevent selection -- **Future**: Add manual CRS configuration for advanced users - -### WMS Version Support -- **Phase 1**: WMS 1.3.0 only (most common) -- **Future**: Add WMS 1.1.1 (requires axis order handling) - -## Key Files to Modify - -**New Files**: -- `lib/models/wms_server.dart` -- `lib/services/wms_capabilities_parser.dart` -- `lib/services/wms_server_repository.dart` -- `lib/utils/crs_factory.dart` -- `lib/screens/wms_server_management_screen.dart` - -**Modified Files**: -- `lib/models/map_layer.dart` (add fields, factory method) -- `lib/screens/map_tab.dart` (layer selector UI) -- `lib/providers/map_provider.dart` (load custom servers, persistence) -- `lib/l10n/app_*.arb` (localized strings) -- `pubspec.yaml` (add `xml: ^7.0.0`) -- `CLAUDE.md` (documentation update) - -**Unchanged Files** (critical for backward compatibility): -- `lib/utils/slovenian_crs.dart` ✅ -- `lib/services/tile_cache_service.dart` ✅ - -## Risks & Mitigations - -| Risk | Mitigation | -|------|------------| -| Slow GetCapabilities responses (5-30s) | Show loading indicator, 30s timeout, cache for 24h | -| Unsupported CRS breaks map | Validate CRS before saving, show warning, fallback to 3857 | -| Tile matrix mismatch causes 400 errors | Document limitation, suggest WMTS if available | -| Breaking existing Slovenian functionality | Keep all existing code paths, add `isBuiltIn` flag, extensive testing | - -## Estimated Timeline -- **Phase 1**: 1-2 weeks (models, parser, storage) -- **Phase 2**: 1 week (UI screens) -- **Phase 3**: 1-2 weeks (integration, testing) -- **Phase 4**: 1 week (polish, docs) -- **Total**: 4-6 weeks (single developer, full-time) - -## Success Criteria -✅ Users can add custom WMS servers via GetCapabilities URL -✅ Users can select base layers and overlays from custom servers -✅ Slovenian layers continue to work with language filtering -✅ Base layer switching works (including CRS changes) -✅ Custom WMS tiles are cached for offline use -✅ Clear error messages for invalid/unsupported servers -✅ No regressions in existing map functionality - ---- - -## Detailed Implementation Notes - -### WMS Server Model Structure - -```dart -class WmsServer { - final String id; // UUID for persistence - final String name; // User-friendly name - final String capabilitiesUrl; // GetCapabilities endpoint - final String version; // WMS version (1.1.1, 1.3.0) - final List availableLayers; - final List supportedCrs; - final DateTime lastUpdated; // Cache invalidation - - Map toJson(); - factory WmsServer.fromJson(Map json); -} - -class WmsLayer { - final String name; // Layer identifier - final String title; // Human-readable title - final String? abstract; - final List styles; - final LatLngBounds? boundingBox; - final List supportedCrs; - final bool supportsTransparency; - final String? legendUrl; -} -``` - -### WMS GetCapabilities XML Parsing - -Key elements to extract: - -```xml - - - Server Name - - - - Root Layer - EPSG:4326 - EPSG:3857 - - layer_name - Layer Title - EPSG:3857 - - -180 - 180 - -90 - 90 - - - - - - -``` - -### CRS Factory Implementation - -```dart -class CrsFactory { - static final Map _crsCache = { - 'EPSG:3794': getSlovenianCrs(), - 'EPSG:3857': const Epsg3857(), - 'EPSG:4326': const Epsg4326(), - }; - - static Crs? getCrs(String epsgCode) { - return _crsCache[epsgCode]; - } - - static bool isSupported(String epsgCode) { - return _crsCache.containsKey(epsgCode); - } -} -``` - -### Storage JSON Schema - -```json -{ - "id": "uuid-here", - "name": "My WMS Server", - "capabilitiesUrl": "https://example.com/wms?SERVICE=WMS&REQUEST=GetCapabilities", - "version": "1.3.0", - "lastUpdated": "2025-10-28T12:00:00Z", - "layers": [ - { - "name": "layer_name", - "title": "Layer Title", - "abstract": "Description...", - "supportedCrs": ["EPSG:3857", "EPSG:4326"], - "boundingBox": { - "south": -90, "west": -180, - "north": 90, "east": 180 - }, - "styles": ["default"], - "supportsTransparency": true - } - ], - "supportedCrs": ["EPSG:3857", "EPSG:4326"] -} -``` - -### UI Flow Diagrams - -**Adding a WMS Server**: -1. User taps "Manage WMS Servers" in Map Management screen -2. User taps "Add WMS Server" button -3. Dialog appears with text field for capabilities URL -4. User enters URL (e.g., `https://prostor.zgs.gov.si/geoserver/wms?SERVICE=WMS&REQUEST=GetCapabilities`) -5. App fetches and parses GetCapabilities -6. Layer picker shows available layers with checkboxes -7. User selects layers to use as base layers or overlays -8. User taps "Save" → server stored in SharedPreferences -9. Layers appear in map layer selector - -**Using a Custom WMS Layer**: -1. User opens layer selector in Map tab -2. User scrolls to "Custom WMS Servers" section -3. User taps custom layer → map switches to that layer -4. If CRS differs from previous layer, map CRS updates -5. If zoom level exceeds layer's maxZoom, zoom is clamped -6. Overlay toggles appear if custom server has overlay layers - -### Testing Checklist - -**Functional Tests**: -- [ ] Add WMS server with valid GetCapabilities URL -- [ ] Parse layers with nested hierarchy (group layers) -- [ ] Select base layer from custom WMS server -- [ ] Switch between standard tile layer and custom WMS layer -- [ ] Switch between custom WMS layers with different CRS -- [ ] Toggle overlay layers from custom WMS server -- [ ] Delete custom WMS server -- [ ] Edit custom WMS server (re-fetch capabilities) -- [ ] Persist selected custom WMS layer across app restarts - -**Error Handling Tests**: -- [ ] Invalid URL (malformed) -- [ ] Network timeout (30s) -- [ ] Invalid XML (not a GetCapabilities response) -- [ ] Empty layer list -- [ ] Unsupported CRS (show warning, prevent selection) -- [ ] Missing required fields (layer name, title) - -**Regression Tests**: -- [ ] Slovenian aerial imagery still loads -- [ ] Slovenian overlays (cadastral, forest roads) still work -- [ ] Language filtering (sl/hr) still hides WMS layers for other locales -- [ ] EPSG:3794 CRS still works correctly -- [ ] Standard tile layers (OSM, OpenTopoMap) still work -- [ ] MBTiles offline layers still work -- [ ] Tile caching still works for WMS tiles - -**Performance Tests**: -- [ ] GetCapabilities fetch completes within 30s -- [ ] Large layer lists (>100 layers) render without lag -- [ ] Switching layers is smooth (no UI freeze) -- [ ] SharedPreferences storage under 1MB for 10 servers - -### Example WMS Servers for Testing - -**Public WMS Servers**: -1. **NASA GIBS** (satellite imagery): - - URL: `https://gibs.earthdata.nasa.gov/wms/epsg4326/best/wms.cgi?SERVICE=WMS&REQUEST=GetCapabilities` - - CRS: EPSG:4326 - - Layers: MODIS, VIIRS, Landsat - -2. **USGS National Map** (US topographic): - - URL: `https://basemap.nationalmap.gov/arcgis/services/USGSTopo/MapServer/WMSServer?SERVICE=WMS&REQUEST=GetCapabilities` - - CRS: EPSG:3857 - - Layers: US Topo - -3. **OpenStreetMap WMS** (reference): - - URL: `https://ows.terrestris.de/osm/service?SERVICE=WMS&REQUEST=GetCapabilities` - - CRS: EPSG:3857, EPSG:4326 - - Layers: OSM-WMS - -4. **Slovenian Government** (current built-in): - - URL: `https://prostor.zgs.gov.si/geowebcache/service/wms?SERVICE=WMS&REQUEST=GetCapabilities` - - CRS: EPSG:3794, EPSG:3857 - - Layers: DOF_2024, kn_parcele, gozdne_ceste - -### Critical Gotchas - -**WMS Version Differences**: -- WMS 1.1.1 uses ``, WMS 1.3.0 uses `` -- EPSG:4326 axis order differs (lon,lat vs lat,lon) -- bbox parameter order changes between versions - -**Layer Inheritance**: -- Child layers inherit CRS from parent if not specified -- Root layer CRS applies to all children unless overridden - -**Group vs Leaf Layers**: -- Only layers with `` can be requested in GetMap -- Layers without `` are groups (organizational only) - -**Namespace Prefixes**: -- Layer names may include workspace namespace (e.g., `pregledovalnik:DOF_2024`) -- Must be included in GetMap request exactly as in GetCapabilities - -**Tile Grid Alignment**: -- WMS uses arbitrary bounding boxes (not aligned tile grids) -- Works for dynamic rendering but may have caching issues -- WMTS is better for tile caching but requires separate implementation - -### Future Enhancements (Post-MVP) - -**Phase 5: Advanced Features** (not in initial scope): -- WMS 1.1.1 support (axis order handling) -- WMTS support (better tile caching) -- Custom CRS registration (manual proj4 definition input) -- Layer groups (hierarchical tree view) -- Legend display for overlay layers -- GetFeatureInfo support (tap on map to query layer attributes) -- Layer metadata viewer (abstract, attribution, keywords) -- Batch import/export of WMS server configurations - -**Phase 6: Performance Optimization**: -- Background GetCapabilities refresh (update layer list without blocking UI) -- Lazy loading of layer metadata (only fetch when user expands server) -- Thumbnail preview for layers (GetMap with small bbox) -- Server health check (ping before adding) - ---- - -## References - -- **WMS 1.3.0 Specification**: https://www.ogc.org/standards/wms -- **EPSG Registry**: https://epsg.io/ -- **Proj4 Definitions**: https://proj4.org/ -- **flutter_map WMS Docs**: https://docs.fleaflet.dev/layers/tile-layer/wms-tile-layer -- **xml package**: https://pub.dev/packages/xml -- **Slovenian WMS**: https://prostor.zgs.gov.si/geowebcache/service/wms?REQUEST=GetCapabilities&SERVICE=WMS -- **NASA GIBS**: https://wiki.earthdata.nasa.gov/display/GIBS/GIBS+API+for+Developers -- **OGC WMS Best Practices**: https://www.ogc.org/standards/wms - ---- - -## Implementation Checklist - -### Phase 1: Foundation -- [ ] Add `xml: ^7.0.0` to pubspec.yaml -- [ ] Create `lib/models/wms_server.dart` with JSON serialization -- [ ] Create `lib/services/wms_capabilities_parser.dart` with XML parsing -- [ ] Create `lib/utils/crs_factory.dart` with CRS registry -- [ ] Create `lib/services/wms_server_repository.dart` with SharedPreferences storage -- [ ] Write unit tests for capabilities parser - -### Phase 2: UI -- [ ] Create `lib/screens/wms_server_management_screen.dart` -- [ ] Add "Manage WMS Servers" button to Map Management screen -- [ ] Implement "Add WMS Server" dialog with URL input -- [ ] Implement layer picker with checkbox selection -- [ ] Add custom WMS section to layer selector in `map_tab.dart` -- [ ] Add "Manage WMS Servers" button to layer selector - -### Phase 3: Integration -- [ ] Add `wmsServerId`, `crsCode`, `isBuiltIn` fields to `MapLayer` -- [ ] Add `MapLayer.fromWmsServer()` factory method -- [ ] Update `MapProvider` to load custom WMS servers on init -- [ ] Update layer switching logic to handle CRS changes -- [ ] Update zoom clamping logic for custom layers -- [ ] Add overlay management for custom WMS layers -- [ ] Verify tile caching works with custom WMS - -### Phase 4: Polish -- [ ] Add localized strings to all `app_*.arb` files (en, hr, sl, de, es, fr, it) -- [ ] Add error handling with user-friendly messages -- [ ] Add loading indicators for GetCapabilities fetch -- [ ] Add CRS compatibility warnings -- [ ] Update `CLAUDE.md` with WMS documentation -- [ ] Test with multiple public WMS servers -- [ ] Regression test Slovenian layers -- [ ] Performance test with large layer lists - ---- - -**Document Version**: 1.0 -**Last Updated**: 2025-10-28 -**Status**: Ready for Implementation diff --git a/docs/ios_background_execution.md b/docs/ios_background_execution.md deleted file mode 100644 index 7080a94..0000000 --- a/docs/ios_background_execution.md +++ /dev/null @@ -1,726 +0,0 @@ -# iOS Background Execution Guide - -Complete guide for running MeshCore SAR in the background on iOS devices. - -## Current Configuration Status - -### ✅ Already Configured - -Your app is **already set up** for background execution with the following capabilities: - -#### 1. Info.plist Background Modes ([Info.plist:67-74](../ios/Runner/Info.plist#L67-L74)) - -```xml -UIBackgroundModes - - location - bluetooth-central - processing - external-accessory - fetch - -``` - -#### 2. Location Permissions ([Info.plist:53-60](../ios/Runner/Info.plist#L53-L60)) - -```xml -NSLocationAlwaysAndWhenInUseUsageDescription -MeshCore SAR needs location access for offline map functionality during field operations - -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 -``` - -#### 3. Bluetooth Permissions ([Info.plist:49-52](../ios/Runner/Info.plist#L49-L52)) - -```xml -NSBluetoothAlwaysUsageDescription -MeshCore SAR needs Bluetooth to communicate with MeshCore devices for Search & Rescue operations -``` - -#### 4. Background Task Identifiers ([Info.plist:5-8](../ios/Runner/Info.plist#L5-L8)) - -```xml -BGTaskSchedulerPermittedIdentifiers - - dev.flutter.background.refresh - -``` - ---- - -## How iOS Background Modes Work - -### 1. Location Background Mode (`location`) - -**What it does:** -- Keeps GPS active when app is backgrounded -- Delivers location updates to your app -- Shows blue status bar indicator: "MeshCore SAR is using your location" - -**How to use:** -```dart -// Request "Always" permission (required for background location) -await Geolocator.requestPermission(); - -// Start tracking with distance filter -final settings = LocationSettings( - accuracy: LocationAccuracy.best, - distanceFilter: 10, // meters -); - -final stream = Geolocator.getPositionStream(locationSettings: settings); -stream.listen((position) { - // This callback runs even when app is in background! - debugPrint('Background position: ${position.latitude}, ${position.longitude}'); -}); -``` - -**Current implementation:** [background_location_service.dart:69-75](../lib/services/background_location_service.dart#L69-L75) - -**Battery impact:** Medium-High (depends on accuracy and distance filter) - -**iOS limitations:** -- User must grant "Always Allow" location permission -- iOS shows blue banner when app uses background location -- Location updates may be deferred to save battery -- Maximum accuracy may be reduced after some time - ---- - -### 2. Bluetooth Central Background Mode (`bluetooth-central`) - -**What it does:** -- Keeps BLE connections alive when app is backgrounded -- Receives BLE notifications and read responses -- Can scan for known devices (limited) - -**How to use:** -```dart -// flutter_blue_plus automatically uses background mode -await device.connect(); // Connection stays alive in background - -// Subscribe to characteristics - notifications work in background -await characteristic.setNotifyValue(true); -characteristic.lastValueStream.listen((data) { - // This callback runs even when app is in background! - debugPrint('Background BLE data: $data'); -}); -``` - -**Current implementation:** -- [meshcore_ble_service.dart](../lib/services/meshcore_ble_service.dart) - BLE communication -- Connection and notifications already support background mode - -**Battery impact:** Low-Medium - -**iOS limitations:** -- Cannot start new connections from background (must be initiated in foreground) -- BLE scanning in background only finds previously connected devices -- Scanning is slower and less frequent in background -- Connection timeouts may be more aggressive - ---- - -### 3. Processing Background Mode (`processing`) - -**What it does:** -- Schedules background tasks to run when system conditions are optimal -- Used for non-urgent work (data sync, cache cleanup, etc.) -- System decides when to run tasks (not guaranteed) - -**How to use:** -Requires `workmanager` package: -```yaml -dependencies: - workmanager: ^0.5.0 -``` - -```dart -import 'package:workmanager/workmanager.dart'; - -void callbackDispatcher() { - Workmanager().executeTask((task, inputData) { - // This runs in background (system decides when) - debugPrint('Background task: $task'); - return Future.value(true); - }); -} - -void main() { - Workmanager().initialize(callbackDispatcher); - - Workmanager().registerPeriodicTask( - 'mesh-sync', - 'meshSync', - frequency: Duration(hours: 1), - ); -} -``` - -**Current implementation:** Not currently used (task identifier registered but no handler) - -**Battery impact:** Low (system schedules intelligently) - -**iOS limitations:** -- Only runs when device is idle, plugged in, or has sufficient battery -- Minimum 15-minute intervals -- No guarantees on execution time -- May not run at all if battery is low - ---- - -### 4. Fetch Background Mode (`fetch`) - -**What it does:** -- Allows app to wake up periodically to fetch new content -- System learns usage patterns and schedules fetch intelligently -- More frequent than processing mode, but still not real-time - -**How to use:** -Requires `background_fetch` package: -```yaml -dependencies: - background_fetch: ^1.3.0 -``` - -```dart -import 'package:background_fetch/background_fetch.dart'; - -void backgroundFetchHandler(String taskId) async { - debugPrint('Background fetch: $taskId'); - - // Fetch new messages, sync data, etc. - await syncMessages(); - - BackgroundFetch.finish(taskId); -} - -void initBackgroundFetch() { - BackgroundFetch.configure( - BackgroundFetchConfig( - minimumFetchInterval: 15, // minutes - stopOnTerminate: false, - enableHeadless: true, - ), - backgroundFetchHandler, - ).then((status) { - debugPrint('Background fetch status: $status'); - }); -} -``` - -**Current implementation:** Not currently used - -**Battery impact:** Low-Medium - -**iOS limitations:** -- System decides when to wake app (typically every 15-30 minutes) -- No guarantees on timing -- May not run if battery is low -- Requires network activity to "train" iOS - ---- - -## Best Practices for SAR Operations - -### Recommended Configuration - -For a **Search & Rescue application**, prioritize real-time updates: - -#### 1. Use Location Background Mode Exclusively - -**Why:** Only location mode provides continuous updates while backgrounded. - -**Implementation:** -```dart -// Start location tracking when BLE connects -await BackgroundLocationService().startTracking(distanceThreshold: 10.0); - -// Location updates automatically trigger mesh broadcasts -// See: background_location_service.dart:75-135 -``` - -**User experience:** -- Blue status bar shows "using location" -- Reassures users that tracking is active -- Critical for SAR where real-time location is life-or-death - -#### 2. Request "Always Allow" Location Permission - -**Why:** "When In Use" permission is revoked when app enters background. - -**Implementation:** -```dart -// Check permission -final permission = await Geolocator.checkPermission(); - -if (permission != LocationPermission.always) { - // Show explanation to user - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text('Background Location Required'), - content: Text( - 'For SAR operations, we need "Always Allow" permission to track ' - 'your location even when the app is in the background. This ensures ' - 'your team can always see your position.' - ), - actions: [ - TextButton( - onPressed: () { - Geolocator.openLocationSettings(); - }, - child: Text('Open Settings'), - ), - ], - ), - ); -} -``` - -**User education:** -- Explain why "Always" is needed (safety, team coordination) -- Show value: "Your team can find you in an emergency" -- Emphasize battery impact and mitigation strategies - -#### 3. Optimize Battery Life - -**Distance Filter:** -```dart -// Don't send updates for small movements -const minDistance = 10.0; // meters - -// Adjust based on terrain and operation type: -// - Urban search: 5m (frequent position changes) -// - Wilderness: 20m (sparse updates OK) -// - Vehicle-based: 50m (high speed, less precision needed) -``` - -**Accuracy vs. Battery:** -```dart -// High accuracy (GPS + GLONASS + Galileo) -LocationAccuracy.best // Best for SAR, but drains battery - -// Balanced (GPS only) -LocationAccuracy.high // Good compromise - -// Low accuracy (WiFi/cell towers) -LocationAccuracy.low // NOT recommended for SAR -``` - -**Current implementation:** Uses `LocationAccuracy.best` ([background_location_service.dart:72](../lib/services/background_location_service.dart#L72)) - -#### 4. Handle Background BLE Properly - -**Connection Strategy:** -```dart -// Connect in foreground -await device.connect(autoConnect: true); - -// Keep connection alive -device.connectionState.listen((state) { - if (state == BluetoothConnectionState.disconnected) { - // Attempt reconnect (only works if app is in foreground or shortly after background) - Future.delayed(Duration(seconds: 5), () { - device.connect(autoConnect: true); - }); - } -}); -``` - -**Characteristic Notifications:** -```dart -// Subscribe to notifications (works in background) -await characteristic.setNotifyValue(true); - -// Process incoming data -characteristic.lastValueStream.listen((data) { - // Parse MeshCore frames even when backgrounded - final frame = FrameParser.parse(data); - // Update UI, save to database, trigger notifications -}); -``` - -**Current implementation:** [meshcore_ble_service.dart](../lib/services/meshcore_ble_service.dart) already handles this correctly. - ---- - -## iOS Background Limitations - -### What Works in Background - -✅ **Location Updates** - Continuous GPS tracking -✅ **BLE Notifications** - Receive data from connected device -✅ **BLE Reads/Writes** - Communicate with connected device -✅ **Local Notifications** - Display alerts to user -✅ **Audio Playback** - Play alert sounds -✅ **Network Requests** - Sync data, send telemetry - -### What Doesn't Work in Background - -❌ **BLE Scanning** - Cannot discover new devices (limited scanning only for known UUIDs) -❌ **New BLE Connections** - Cannot initiate connections (must be done in foreground) -❌ **Heavy Processing** - CPU throttled, may cause crashes -❌ **Camera/Photos** - Cannot access camera or photo library -❌ **Screen Rendering** - UI doesn't update (use local notifications instead) - -### System Throttling - -**iOS aggressively throttles background apps:** - -| Time in Background | GPS Accuracy | BLE Performance | CPU Quota | -|--------------------|--------------|-----------------|-----------| -| 0-10 seconds | Full | Full | 100% | -| 10 seconds - 3 minutes | Full | Full | 80% | -| 3-10 minutes | Reduced | Full | 50% | -| 10+ minutes | Deferred | Throttled | 20% | -| 30+ minutes | Significant deferral | Slow | 10% | - -**Mitigation:** -- Keep BLE data payloads small -- Batch location broadcasts (don't send every update) -- Use background tasks for non-critical work - ---- - -## Testing Background Execution - -### 1. Xcode Console Monitoring - -```bash -# Open Console.app and filter by your app -# Look for debug prints with timestamps - -# Expected logs when backgrounded: -📍 [BackgroundLocation] New position: 37.7749, -122.4194 -📤 [BackgroundLocation] Updating device location... -📡 [BackgroundLocation] Broadcasting self advertisement... -✅ [BackgroundLocation] Location update sent successfully -``` - -### 2. Xcode Debug Navigator - -1. Run app from Xcode -2. Press Home button to background app -3. In Xcode: Debug → View Debugging → Background Tasks -4. Verify "location" task is active - -### 3. Background Simulation - -```bash -# Simulate location changes in Simulator -xcrun simctl location set 37.7749 -122.4194 - -# Simulate BLE data (requires real device) -# Send data via nRF Connect or LightBlue -``` - -### 4. Real-World Testing - -**Recommended test procedure:** -1. Start app in foreground -2. Connect to MeshCore device -3. Start location tracking -4. **Lock screen** (simulates background) -5. Walk 50+ meters -6. Unlock and check: - - Location updates in logs - - Mesh broadcasts sent - - Battery usage acceptable - -**Important:** iOS treats locked screen differently than backgrounded app! -- Locked = Full background capabilities -- Backgrounded (app switcher) = Throttled after 3 minutes -- Terminated = No background execution (must use background fetch) - ---- - -## User-Facing Settings - -### Recommended Settings Screen - -```dart -class BackgroundTrackingSettings extends StatelessWidget { - @override - Widget build(BuildContext context) { - return Column( - children: [ - SwitchListTile( - title: Text('Background Location Tracking'), - subtitle: Text('Keep tracking your location when app is closed'), - value: _isEnabled, - onChanged: (enabled) { - if (enabled) { - _startBackgroundTracking(); - } else { - _stopBackgroundTracking(); - } - }, - ), - - ListTile( - title: Text('Update Distance'), - subtitle: Text('Send location update every $_distance meters'), - trailing: Text('$_distance m'), - onTap: () => _showDistanceSlider(), - ), - - ListTile( - title: Text('Battery Impact'), - subtitle: Text(_getBatteryImpactText()), - trailing: Icon(_getBatteryIcon()), - ), - - // Permission check - if (!_hasAlwaysPermission) - ListTile( - title: Text('⚠️ Permission Required'), - subtitle: Text('Tap to enable "Always Allow" location access'), - onTap: () => Geolocator.openLocationSettings(), - ), - ], - ); - } -} -``` - -### Battery Impact Indicators - -```dart -String _getBatteryImpactText() { - if (_distance <= 5) return 'High - Frequent updates'; - if (_distance <= 20) return 'Medium - Balanced'; - if (_distance <= 50) return 'Low - Sparse updates'; - return 'Minimal - Only major movements'; -} - -IconData _getBatteryIcon() { - if (_distance <= 5) return Icons.battery_alert; - if (_distance <= 20) return Icons.battery_std; - return Icons.battery_full; -} -``` - ---- - -## Troubleshooting - -### Location Updates Stop After 10 Minutes - -**Symptom:** GPS stops updating after app is backgrounded for ~10 minutes - -**Cause:** iOS defers location updates to save battery - -**Solution:** -```dart -// Request "Always" permission (not just "When In Use") -await Geolocator.requestPermission(); - -// In Info.plist, ensure you have: -NSLocationAlwaysAndWhenInUseUsageDescription -Your explanation here - -// Use smaller distance filter to signal importance -LocationSettings( - accuracy: LocationAccuracy.best, - distanceFilter: 5, // smaller = less deferral -) -``` - -### BLE Connection Drops in Background - -**Symptom:** BLE device disconnects after ~30 seconds in background - -**Cause:** iOS terminates idle BLE connections to save power - -**Solution:** -```dart -// Keep connection alive by polling -Timer.periodic(Duration(seconds: 20), (timer) async { - if (device.isConnected) { - // Read a characteristic to keep connection alive - await characteristic.read(); - } -}); - -// Or send keepalive messages -Timer.periodic(Duration(seconds: 30), (timer) async { - if (device.isConnected) { - await bleService.sendSelfAdvert(floodMode: false); - } -}); -``` - -**Current implementation:** -- Keepalive timer reads TX characteristic every 20 seconds ([meshcore_ble_service.dart:648-677](../lib/services/meshcore_ble_service.dart#L648-L677)) -- Location updates also keep connection alive ([background_location_service.dart:112-121](../lib/services/background_location_service.dart#L112-L121)) -- Timer automatically starts when connected, stops when disconnected -- Logs connection health: "💚 [BLE] Keepalive: Connection maintained" - -### Blue Status Bar Annoying Users - -**Symptom:** Users complain about persistent blue "using location" banner - -**Explanation:** This is **intentional** iOS behavior for user privacy - -**Options:** -1. **Keep it** - Shows app is working, builds trust -2. **Educate users** - Explain it's a safety feature -3. **Toggle option** - Let users disable background tracking if not in active SAR operation - -**DO NOT try to hide it** - This is an Apple HIG violation and will get your app rejected - -### Notifications Not Appearing in Background - -**Symptom:** Local notifications don't show when app is backgrounded - -**Cause:** Permission not granted or critical alerts not enabled - -**Solution:** -```dart -// Request critical notification permission (bypasses silent mode) -await IOSFlutterLocalNotificationsPlugin().requestPermissions( - alert: true, - badge: true, - sound: true, - critical: true, // Important for SAR alerts -); -``` - -**Current implementation:** Already configured ([notification_service.dart:92-99](../lib/services/notification_service.dart#L92-L99)) - ---- - -## Battery Optimization Recommendations - -### 1. Adaptive Distance Thresholds - -```dart -// Adjust based on movement speed -class AdaptiveLocationTracking { - double _distanceThreshold = 10.0; - - void _adjustThreshold(Position position) { - // If moving fast (in vehicle), use larger threshold - if (position.speed > 5.0) { // 5 m/s = 18 km/h - _distanceThreshold = 50.0; - } - // If stationary, use very large threshold - else if (position.speed < 0.5) { - _distanceThreshold = 100.0; - } - // If walking, use small threshold - else { - _distanceThreshold = 10.0; - } - } -} -``` - -### 2. Time-Based Throttling - -```dart -// Don't broadcast more than once per minute -DateTime? _lastBroadcast; - -void _handleLocationUpdate(Position position) async { - final now = DateTime.now(); - - if (_lastBroadcast != null) { - final elapsed = now.difference(_lastBroadcast!); - if (elapsed < Duration(seconds: 60)) { - return; // Skip this update - } - } - - await _broadcastLocation(position); - _lastBroadcast = now; -} -``` - -### 3. Operation Mode Profiles - -```dart -enum OperationMode { - active, // Full tracking, 5m threshold, 30s min interval - standby, // Medium tracking, 20m threshold, 2m min interval - idle, // Sparse tracking, 100m threshold, 10m min interval -} - -class LocationProfileManager { - void applyProfile(OperationMode mode) { - switch (mode) { - case OperationMode.active: - _distanceThreshold = 5.0; - _minTimeInterval = 30; - break; - case OperationMode.standby: - _distanceThreshold = 20.0; - _minTimeInterval = 120; - break; - case OperationMode.idle: - _distanceThreshold = 100.0; - _minTimeInterval = 600; - break; - } - } -} -``` - ---- - -## Summary - -### ✅ Your App is Ready for Background Execution - -**Current capabilities:** -- ✅ Location tracking in background -- ✅ BLE communication in background -- ✅ Background task scheduling (configured but not used) -- ✅ Background fetch (configured but not used) -- ✅ All required permissions in Info.plist - -**What works now:** -1. User starts app → connects to MeshCore device -2. User enables location tracking → `BackgroundLocationService` starts -3. User backgrounds app → Location updates continue -4. GPS updates trigger → Mesh broadcasts sent via BLE -5. BLE notifications received → Messages processed, notifications shown -6. User returns to app → Full state restored - -**Limitations:** -- BLE connection must be initiated in foreground -- Location updates may be deferred after 10+ minutes -- CPU/network throttled after 3 minutes in background -- Cannot scan for new BLE devices in background - -**Battery life:** -- Current config: **Medium impact** (~10-15% battery per hour of active tracking) -- With optimizations: **Low-Medium impact** (~5-10% per hour) -- Comparable to navigation apps (Google Maps, Waze) - -### Next Steps (Optional Enhancements) - -1. **Add Operation Mode Profiles** - Let users choose Active/Standby/Idle mode -2. **Implement Adaptive Thresholds** - Adjust tracking based on speed -3. **Add Battery Monitoring** - Show real-time battery impact -4. **Background Fetch Integration** - Sync messages when app is terminated -5. **Keepalive Optimization** - Fine-tune BLE connection retention - ---- - -## References - -- [Apple Background Execution Guide](https://developer.apple.com/documentation/uikit/app_and_environment/scenes/preparing_your_ui_to_run_in_the_background) -- [iOS Location Background Mode](https://developer.apple.com/documentation/corelocation/getting_the_user_s_location/handling_location_events_in_the_background) -- [iOS Bluetooth Background Mode](https://developer.apple.com/library/archive/documentation/NetworkingInternetWeb/Conceptual/CoreBluetooth_concepts/CoreBluetoothBackgroundProcessingForIOSApps/PerformingTasksWhileYourAppIsInTheBackground.html) -- [flutter_blue_plus Background Mode](https://pub.dev/packages/flutter_blue_plus#background-mode) -- [Geolocator Package](https://pub.dev/packages/geolocator) -- [Local Notifications Package](https://pub.dev/packages/flutter_local_notifications) - -**Last Updated:** 2025-10-30 -**App Version:** 48 (iOS build 48) diff --git a/docs/wms_layer_analysis.md b/docs/wms_layer_analysis.md deleted file mode 100644 index a642931..0000000 --- a/docs/wms_layer_analysis.md +++ /dev/null @@ -1,304 +0,0 @@ -# Slovenian WMS Layers - SAR Application Analysis - -**Total Layers Found: 108** - -## HIGH PRIORITY - Critical for SAR Operations - -### Aerial Imagery & Base Maps (5 layers) -1. **pregledovalnik:DOF_2024** - Digital Orthophoto 2024 (Latest aerial imagery) - - **USE CASE**: Primary visual reference, current terrain conditions - - **ALREADY IMPLEMENTED** in the app - -2. **pregledovalnik:DOF25** - Digital Orthophoto 25cm resolution - - **USE CASE**: High-resolution aerial imagery for detailed terrain analysis - -3. **pregledovalnik:DOF_IR** - Infrared Orthophoto - - **USE CASE**: Thermal/infrared imagery for detecting heat signatures, useful for night searches - -4. **pregledovalnik:DTK25** - Topographic Map 1:25,000 - - **USE CASE**: Traditional topographic reference with contours, trails, landmarks - -5. **pregledovalnik:dof025_2022_2024** - Combined orthophoto 2022-2024 - - **USE CASE**: Multi-year aerial imagery comparison - -### Administrative Boundaries (6 layers) -6. **pregledovalnik:NEP_RPE_OBCINE** - Municipalities (občine) - - **USE CASE**: Jurisdiction boundaries for coordinating with local authorities - - **RECOMMENDED FOR OVERLAY** - -7. **pregledovalnik:NEP_RPE_NASELJA** - Settlements - - **USE CASE**: Identify populated areas, evacuation points, staging areas - - **RECOMMENDED FOR OVERLAY** - -8. **pregledovalnik:NEP_HISNE_STEVILKE** - House Numbers - - **USE CASE**: Precise location identification for emergency response - -9. **pregledovalnik:NEP_RPE_UPRAVNE_ENOTE** - Administrative Units - - **USE CASE**: Regional administration boundaries - -10. **pregledovalnik:NEP_RPE_STATISTICNE_REGIJE** - Statistical Regions - - **USE CASE**: Broader regional planning - -11. **pregledovalnik:drzavna_meja** - State Border - - **USE CASE**: International coordination for cross-border operations - -### Roads & Transportation (4 layers) -12. **pregledovalnik:KGI_LINIJE_CESTE_G** - Roads - - **USE CASE**: Primary access routes, evacuation routes, vehicle navigation - - **HIGH PRIORITY OVERLAY** - -13. **pregledovalnik:gozdne_ceste** - Forest Roads - - **USE CASE**: Access to remote forest areas, critical for SAR vehicles - - **HIGH PRIORITY OVERLAY** - -14. **pregledovalnik:LINIJE_ZELEZNICE_G** - Railways - - **USE CASE**: Alternative access routes, landmarks, coordination with rail authorities - -15. **pregledovalnik:KGI_LINIJE_PLANINSKE_POTI_G** - Mountain/Hiking Trails - - **USE CASE**: Common search areas, lost hiker routes, access to remote areas - - **HIGH PRIORITY OVERLAY** - -### Fire & Emergency Hazards (5 layers) -16. **pregledovalnik:gozdni_pozari** - Forest Fires (historical) - - **USE CASE**: Historical fire locations, high-risk areas - - **RECOMMENDED FOR OVERLAY** - -17. **pregledovalnik:pozarna_ogrozenost** - Fire Hazard/Risk Areas - - **USE CASE**: Identify high fire risk zones, plan safe evacuation routes - - **HIGH PRIORITY OVERLAY** - -18. **pregledovalnik:pozarisce_goriski_kras** - Fire Site - Goriški Kras - - **USE CASE**: Specific fire hazard area in Karst region - -19. **pregledovalnik:pozarisce_kras** - Fire Site - Kras - - **USE CASE**: Karst region fire zones - -20. **pregledovalnik:protipozarne_preseke** - Firebreaks - - **USE CASE**: Fire containment lines, safe zones - -### Geographic Names & Navigation (1 layer) -21. **pregledovalnik:zemljepisna_imena** - Geographic Names - - **USE CASE**: Place names for communication, location identification - - **RECOMMENDED FOR OVERLAY** - ---- - -## MEDIUM PRIORITY - Useful for Planning & Context - -### Protected Areas & Natural Features (9 layers) -22. **pregledovalnik:natura2000** - Natura 2000 Protected Areas - - **USE CASE**: Environmental restrictions, sensitive areas - -23. **pregledovalnik:zavarovana_obmocja_poligoni** - Protected Areas (polygons) - - **USE CASE**: National parks, nature reserves, access restrictions - -24. **pregledovalnik:zavarovana_obmocja_tocke** - Protected Areas (points) - - **USE CASE**: Point-based protected sites - -25. **pregledovalnik:zavarovana_obmocja_conacija** - Protected Areas (zoning) - - **USE CASE**: Zoning within protected areas - -26. **pregledovalnik:naravne_vrednote_poligoni** - Natural Heritage (polygons) - - **USE CASE**: Notable natural features, landmarks - -27. **pregledovalnik:naravne_vrednote_tocke** - Natural Heritage (points) - - **USE CASE**: Specific natural landmarks (caves, waterfalls, etc.) - -28. **pregledovalnik:epo_poligoni** - Single Trees/Monuments (polygons) - - **USE CASE**: Notable landmark trees - -29. **pregledovalnik:epo_tocke** - Single Trees/Monuments (points) - - **USE CASE**: Point landmarks - -30. **pregledovalnik:gozdni_rezervati** - Forest Reserves - - **USE CASE**: Old-growth forests, restricted access areas - -### Cadastral & Land Parcels (2 layers) -31. **pregledovalnik:kn_parcele** - Cadastral Parcels - - **USE CASE**: Land ownership boundaries, legal jurisdictions - -32. **pregledovalnik:KN_KATASTRSKE_OBCINE** - Cadastral Municipalities - - **USE CASE**: Cadastral administrative units - -### Terrain & Elevation (2 layers) -33. **pregledovalnik:DMK** - Digital Cartographic Model - - **USE CASE**: Contours, terrain features, elevation data - -34. **pregledovalnik:DMR** - Digital Relief Model - - **USE CASE**: Shaded relief, terrain visualization - -### Forest Management Areas (10 layers) -35. **pregledovalnik:gge** - Forest Management Units (GGE) - - **USE CASE**: Forest administrative units, contact local foresters - -36. **pregledovalnik:ggo** - Forest Management Districts (GGO) - - **USE CASE**: Larger forest districts - -37. **pregledovalnik:revirji** - Forest Ranger Districts - - **USE CASE**: Local ranger contact areas - -38. **pregledovalnik:krajevne_enote** - Local Forest Units - - **USE CASE**: Smallest administrative forest units - -39. **pregledovalnik:odseki** - Forest Compartments - - **USE CASE**: Forest subdivisions for management - -40. **pregledovalnik:odseki_gozdni** - Forest Compartments (variant) - - **USE CASE**: Alternative compartment layer - -41. **pregledovalnik:sestoji** - Forest Stands - - **USE CASE**: Specific tree stand types, vegetation density - -42. **pregledovalnik:sestoji_druga_gozdna_zemljisca** - Other Forest Lands - - **USE CASE**: Non-productive forest areas - -43. **pregledovalnik:varovalni_gozdovi** - Protection Forests - - **USE CASE**: Forests with protective function (avalanche, erosion) - -44. **pregledovalnik:lovisca** - Hunting Grounds - - **USE CASE**: Contact with hunting associations for local knowledge - -### Historical Disasters (6 layers) -45. **pregledovalnik:vetrolom_2017** - Windthrow 2017 - - **USE CASE**: Storm damage areas, difficult terrain - -46. **pregledovalnik:vetrolom_2018** - Windthrow 2018 - - **USE CASE**: Storm damage areas from 2018 - -47. **pregledovalnik:zled_2014** - Ice Storm 2014 - - **USE CASE**: Ice damage areas - -48. **pregledovalnik:zled_drugi_gozdovi_2014** - Ice Storm 2014 (other forests) - - **USE CASE**: Ice damage in secondary forests - -49. **pregledovalnik:podlubniki_2015_2019** - Bark Beetle Damage 2015-2019 - - **USE CASE**: Dead wood areas, fire hazard, difficult terrain - -50. **pregledovalnik:krcitve** - Clearcuts - - **USE CASE**: Open areas, recent harvest sites, potential staging areas - -### Agricultural & Land Use (2 layers) -51. **pregledovalnik:povrsine_v_zarascanju** - Overgrown Areas - - **USE CASE**: Abandoned agricultural land, changing terrain - -52. **pregledovalnik:skupna_kmetijska_politika_2023_2027** - Common Agricultural Policy - - **USE CASE**: Agricultural land use planning - ---- - -## LOW PRIORITY - Technical/Specialized Layers - -### Forest Function Layers (ON21 series - 45 layers) -These are highly specialized forest function layers from the 2021 forest management plan. Each has variants for lines (_l), polygons (_p), and points (_t): - -**Categories:** -- **Biotska** (Biodiversity): on21_fun_biotska_l/p/t -- **Druge gozdne dobrine** (Other forest goods): on21_fun_druge_gozdne_dobrine_l/p/t -- **Estetska** (Aesthetic): on21_fun_estetska_l/p/t -- **Hidroloska** (Hydrological): on21_fun_hidroloska_l/p/t -- **Higiensko-zdravstvena** (Health/hygiene): on21_fun_higiensko_zdravstvena_p -- **Klimatska** (Climate): on21_fun_klimatska_l/p -- **Kulturna** (Cultural): on21_fun_kulturna_l/p/t -- **Lesnoproizvodna** (Timber production): on21_fun_lesnoproizvodna_p -- **Lovnogospodarska** (Hunting management): on21_fun_lovnogospodarska_p/t -- **Obrambna** (Defense): on21_fun_obrambna_p/t -- **Poucna** (Educational): on21_fun_poucna_l/p/t -- **Raziskovalna** (Research): on21_fun_raziskovalna_p/t -- **Rekreacijska** (Recreation): on21_fun_rekreacijska_l/p/t -- **Skupaj** (Combined): on21_fun_skupaj_l/p/t -- **Turisticna** (Tourism): on21_fun_turisticna_l/p/t -- **Varovalna** (Protection): on21_fun_varovalna_p -- **Varovanja naravnih vrednot** (Natural heritage protection): on21_fun_varovanja_naravnih_vrednot_l/p/t -- **Zascitna** (Conservation): on21_fun_zascitna_l/p - -**USE CASE**: Very specialized forest planning data. May be useful for: -- **Rekreacijska**: Popular recreation areas (lost hikers) -- **Turisticna**: Tourist areas (search priority) -- **Varovalna**: Avalanche/erosion protection forests (hazard awareness) - -### Miscellaneous Technical (6 layers) -- **pregledovalnik:conacija_gp** - Zonation (technical) -- **pregledovalnik:evrd** - Single-tree selection forests -- **pregledovalnik:koridorji** - Corridors (ecological) -- **pregledovalnik:luo** - Forest landscape units -- **pregledovalnik:pobude_gge** - GGE initiatives -- **pregledovalnik:provenience** - Seed provenance areas -- **pregledovalnik:uvhvvr** - High conservation value forests -- **pregledovalnik:gozdni_sklad_ekocelice** - Forest fund eco-cells -- **pregledovalnik:gozdni_sklad_habitatna_drevesa** - Habitat trees - -### Layer Groups (2 layers) -- **pregledovalnik:ttn_group** - Group layer (container) -- **pregledovalnik:zemljevid_group** - Map group layer (container) - ---- - -## RECOMMENDED IMPLEMENTATION PLAN - -### Phase 1: Critical Overlays (Immediate) -Add these layers as toggleable overlays in the Map Options screen: - -1. **Forest Roads** (`gozdne_ceste`) - PNG, transparent - - Critical for vehicle access in remote areas - -2. **Hiking/Mountain Trails** (`KGI_LINIJE_PLANINSKE_POTI_G`) - PNG, transparent - - Common search areas for lost hikers - -3. **Fire Hazard Zones** (`pozarna_ogrozenost`) - PNG, transparent - - Safety planning, risk assessment - -4. **Settlements** (`NEP_RPE_NASELJA`) - PNG, transparent - - Populated areas, staging areas - -5. **Municipalities** (`NEP_RPE_OBCINE`) - PNG, transparent - - Administrative boundaries - -### Phase 2: Additional Useful Layers -6. **Geographic Names** (`zemljepisna_imena`) -7. **Forest Fires Historical** (`gozdni_pozari`) -8. **Protected Areas** (`zavarovana_obmocja_poligoni`) -9. **Topographic Map** (`DTK25`) - Alternative base layer -10. **Infrared Imagery** (`DOF_IR`) - Alternative base layer - -### Phase 3: Specialized Layers (On Demand) -11. **Windthrow/Disaster Areas** (for post-disaster operations) -12. **Recreation/Tourism Areas** (for prioritizing search areas) -13. **Protection Forests** (avalanche/erosion hazard awareness) - ---- - -## TECHNICAL NOTES - -### CRS Compatibility -- All layers from `prostor.zgs.gov.si` support **EPSG:3794** (Slovenian National Grid) -- The app already has the correct CRS implementation in `lib/utils/slovenian_crs.dart` - -### Recommended Formats -- **Base Layers**: JPEG (better compression for imagery) -- **Overlays**: PNG with transparency=true (for stacking) - -### Caching Strategy -- All layers should use the existing FMTC caching infrastructure -- 30-day validity is appropriate for most layers -- Consider longer validity for static layers (administrative boundaries) - -### Performance Considerations -- Limit active overlays to 3-4 simultaneously to avoid performance issues -- Use appropriate zoom level restrictions (some layers only useful at close zoom) -- Consider pre-downloading critical layers for offline SAR operations - ---- - -## LAYER NAMING CONVENTIONS - -**Slovenian Terms Reference:** -- **DOF** = Digitalni Ortofoto (Digital Orthophoto) -- **DTK** = Državna Topografska Karta (State Topographic Map) -- **DMK** = Digitalni Kartografski Model (Digital Cartographic Model) -- **DMR** = Digitalni Model Reliefa (Digital Relief Model) -- **NEP** = Nacionalni Evidenčni Portal (National Registry Portal) -- **RPE** = Register Prostorskih Enot (Spatial Units Register) -- **GGE** = Gozdnogospodarska Enota (Forest Management Unit) -- **GGO** = Gozdnogospodarska Območje (Forest Management District) -- **ON21** = Območni Načrt 2021 (Regional Plan 2021) - diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index ec97fc6..57b8fb0 100644 --- a/ios/Flutter/Debug.xcconfig +++ b/ios/Flutter/Debug.xcconfig @@ -1,2 +1,3 @@ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" +ENABLE_DEBUG_DYLIB = NO diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 82287e7..19ab91e 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -61,9 +61,9 @@ PODS: - FlutterMacOS - permission_handler_apple (9.3.0): - Flutter - - SDWebImage (5.21.3): - - SDWebImage/Core (= 5.21.3) - - SDWebImage/Core (5.21.3) + - SDWebImage (5.21.7): + - SDWebImage/Core (= 5.21.7) + - SDWebImage/Core (5.21.7) - share_plus (0.0.1): - Flutter - shared_preferences_foundation (0.0.1): @@ -155,7 +155,7 @@ SPEC CHECKSUMS: package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d - SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a + SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 diff --git a/lib/providers/app_provider.dart b/lib/providers/app_provider.dart index f53d664..4dccf98 100644 --- a/lib/providers/app_provider.dart +++ b/lib/providers/app_provider.dart @@ -20,7 +20,8 @@ class AppProvider with ChangeNotifier { final DrawingProvider drawingProvider; final ChannelsProvider channelsProvider; final TileCacheService tileCacheService; - final LocationTrackingService locationTrackingService = LocationTrackingService(); + final LocationTrackingService locationTrackingService = + LocationTrackingService(); bool _isInitialized = false; bool get isInitialized => _isInitialized; @@ -61,7 +62,9 @@ class AppProvider with ChangeNotifier { // Give DrawingProvider a moment to finish loading too await Future.delayed(const Duration(milliseconds: 100)); - debugPrint('🔄 [AppProvider] Early sync: syncing drawings from messages...'); + debugPrint( + '🔄 [AppProvider] Early sync: syncing drawings from messages...', + ); messagesProvider.syncDrawingsWithProvider(drawingProvider); } @@ -129,7 +132,9 @@ class AppProvider with ChangeNotifier { // Setup callbacks locationTrackingService.onPositionUpdate = (position) { - debugPrint('📍 [AppProvider] Position updated: ${position.latitude}, ${position.longitude}'); + debugPrint( + '📍 [AppProvider] Position updated: ${position.latitude}, ${position.longitude}', + ); }; locationTrackingService.onBroadcastSent = (position) { @@ -141,7 +146,9 @@ class AppProvider with ChangeNotifier { }; locationTrackingService.onTrackingStateChanged = (isTracking) { - debugPrint('🔄 [AppProvider] Location tracking state: ${isTracking ? "started" : "stopped"}'); + debugPrint( + '🔄 [AppProvider] Location tracking state: ${isTracking ? "started" : "stopped"}', + ); }; debugPrint('✅ [AppProvider] Location tracking service initialized'); @@ -187,87 +194,103 @@ class AppProvider with ChangeNotifier { }; // When channel info is received - connectionProvider.onChannelInfoReceived = (int channelIdx, String channelName, Uint8List secret, int? flags) { - try { - debugPrint('🔔 [AppProvider] onChannelInfoReceived called: idx=$channelIdx, name="$channelName"'); - - // Check if this is a channel deletion (empty name) - if (channelName.isEmpty && channelIdx != 0) { - debugPrint(' 🗑️ Channel $channelIdx deleted - removing from providers'); - - // Remove from ChannelsProvider - channelsProvider.removeChannel(channelIdx); - debugPrint(' ✅ Removed from ChannelsProvider'); - - // Remove from ContactsProvider using pseudo public key - final publicKeyBytes = Uint8List(32); - publicKeyBytes[0] = 0xFF; // Special marker for channels - publicKeyBytes[1] = channelIdx; // Channel index - final publicKeyHex = publicKeyBytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(''); - - contactsProvider.removeContact(publicKeyHex); - debugPrint(' ✅ Removed from ContactsProvider'); - - return; - } - - // Add/update in ChannelsProvider - channelsProvider.addOrUpdateChannel( - index: channelIdx, - name: channelName, - secret: secret, - flags: flags, - ); - debugPrint(' ✅ Added to ChannelsProvider'); - - // Also add as Contact to ContactsProvider (for UI display) - // Skip if it's public channel (already exists) - debugPrint('📻 [AppProvider] Channel $channelIdx: "$channelName" (isEmpty: ${channelName.isEmpty}, isHashChannel: ${channelName.startsWith('#')})'); - - if (channelName.isNotEmpty && channelIdx != 0) { - debugPrint(' ✅ Adding channel $channelIdx to ContactsProvider as Contact'); - - // Create a pseudo public key for the channel based on its index - // Use channel index as a unique identifier (pad to 32 bytes) - final publicKeyBytes = Uint8List(32); - publicKeyBytes[0] = 0xFF; // Special marker for channels - publicKeyBytes[1] = channelIdx; // Channel index - - final now = DateTime.now().millisecondsSinceEpoch ~/ 1000; - - contactsProvider.addOrUpdateContact( - Contact( - publicKey: publicKeyBytes, - type: ContactType.channel, - flags: flags ?? 0, - outPathLen: -1, // Flood mode for channels - outPath: Uint8List(0), // Empty path for channels - advName: channelName, - lastAdvert: now, - advLat: 0, // Channels don't have location - advLon: 0, - lastMod: now, - isNew: false, // Don't mark channels as new - ), - ); - - debugPrint(' ✅ Channel contact added. Total channels in ContactsProvider: ${contactsProvider.channels.length}'); - } else { - debugPrint(' ⏭️ Skipping channel $channelIdx (empty: ${channelName.isEmpty}, isPublic: ${channelIdx == 0})'); - } - } catch (e, stackTrace) { - debugPrint('❌ [AppProvider] Error in onChannelInfoReceived: $e'); - debugPrint(' Stack trace: $stackTrace'); - } - }; + connectionProvider.onChannelInfoReceived = + (int channelIdx, String channelName, Uint8List secret, int? flags) { + try { + debugPrint( + '🔔 [AppProvider] onChannelInfoReceived called: idx=$channelIdx, name="$channelName"', + ); + + // Check if this is a channel deletion (empty name) + if (channelName.isEmpty && channelIdx != 0) { + debugPrint( + ' 🗑️ Channel $channelIdx deleted - removing from providers', + ); + + // Remove from ChannelsProvider + channelsProvider.removeChannel(channelIdx); + debugPrint(' ✅ Removed from ChannelsProvider'); + + // Remove from ContactsProvider using pseudo public key + final publicKeyBytes = Uint8List(32); + publicKeyBytes[0] = 0xFF; // Special marker for channels + publicKeyBytes[1] = channelIdx; // Channel index + final publicKeyHex = publicKeyBytes + .map((b) => b.toRadixString(16).padLeft(2, '0')) + .join(''); + + contactsProvider.removeContact(publicKeyHex); + debugPrint(' ✅ Removed from ContactsProvider'); + + return; + } + + // Add/update in ChannelsProvider + channelsProvider.addOrUpdateChannel( + index: channelIdx, + name: channelName, + secret: secret, + flags: flags, + ); + debugPrint(' ✅ Added to ChannelsProvider'); + + // Also add as Contact to ContactsProvider (for UI display) + // Skip if it's public channel (already exists) + debugPrint( + '📻 [AppProvider] Channel $channelIdx: "$channelName" (isEmpty: ${channelName.isEmpty}, isHashChannel: ${channelName.startsWith('#')})', + ); + + if (channelName.isNotEmpty && channelIdx != 0) { + debugPrint( + ' ✅ Adding channel $channelIdx to ContactsProvider as Contact', + ); + + // Create a pseudo public key for the channel based on its index + // Use channel index as a unique identifier (pad to 32 bytes) + final publicKeyBytes = Uint8List(32); + publicKeyBytes[0] = 0xFF; // Special marker for channels + publicKeyBytes[1] = channelIdx; // Channel index + + final now = DateTime.now().millisecondsSinceEpoch ~/ 1000; + + contactsProvider.addOrUpdateContact( + Contact( + publicKey: publicKeyBytes, + type: ContactType.channel, + flags: flags ?? 0, + outPathLen: -1, // Flood mode for channels + outPath: Uint8List(0), // Empty path for channels + advName: channelName, + lastAdvert: now, + advLat: 0, // Channels don't have location + advLon: 0, + lastMod: now, + isNew: false, // Don't mark channels as new + ), + ); + + debugPrint( + ' ✅ Channel contact added. Total channels in ContactsProvider: ${contactsProvider.channels.length}', + ); + } else { + debugPrint( + ' ⏭️ Skipping channel $channelIdx (empty: ${channelName.isEmpty}, isPublic: ${channelIdx == 0})', + ); + } + } catch (e, stackTrace) { + debugPrint('❌ [AppProvider] Error in onChannelInfoReceived: $e'); + debugPrint(' Stack trace: $stackTrace'); + } + }; // When a message is received connectionProvider.onMessageReceived = (message) { // Enrich message with sender name from contacts first Message enrichedMessage = message; if (message.senderPublicKeyPrefix != null && message.senderName == null) { - final contact = contactsProvider - .findContactByKey(message.senderPublicKeyPrefix!); + final contact = contactsProvider.findContactByKey( + message.senderPublicKeyPrefix!, + ); if (contact != null) { enrichedMessage = message.copyWith(senderName: contact.advName); } @@ -281,10 +304,13 @@ class AppProvider with ChangeNotifier { final drawing = DrawingMessageParser.parseDrawingMessage( enrichedMessage.text, senderName: senderName, - messageId: enrichedMessage.id, // Pass message ID for navigation linking + messageId: + enrichedMessage.id, // Pass message ID for navigation linking ); if (drawing != null) { - debugPrint('🎨 [AppProvider] Drawing parsed successfully: ${drawing.type.name} from ${drawing.senderName ?? "unknown"}'); + debugPrint( + '🎨 [AppProvider] Drawing parsed successfully: ${drawing.type.name} from ${drawing.senderName ?? "unknown"}', + ); debugPrint(' Drawing linked to message ID: ${enrichedMessage.id}'); drawingProvider.addReceivedDrawing(drawing); @@ -318,7 +344,8 @@ class AppProvider with ChangeNotifier { final contact = contactsProvider.contacts.firstWhere( (c) => c.advName == name, ); - return contact.publicKeyHex.isNotEmpty && contact.publicKeyHex.length >= 12 + return contact.publicKeyHex.isNotEmpty && + contact.publicKeyHex.length >= 12 ? contact.publicKeyHex.substring(0, 12) : ''; } catch (e) { @@ -335,7 +362,9 @@ class AppProvider with ChangeNotifier { // When telemetry is received via PUSH_CODE_TELEMETRY_RESPONSE (0x8B) // Used by older firmware versions for telemetry responses connectionProvider.onTelemetryReceived = (publicKey, lppData) { - debugPrint('📊 [AppProvider] Telemetry response (0x8B) received - updating contact'); + debugPrint( + '📊 [AppProvider] Telemetry response (0x8B) received - updating contact', + ); contactsProvider.updateTelemetry(publicKey, lppData); }; @@ -343,7 +372,9 @@ class AppProvider with ChangeNotifier { // Used by newer firmware versions for telemetry and other binary data // BOTH callbacks (0x8B and 0x8C) must be handled for device compatibility connectionProvider.onBinaryResponse = (publicKeyPrefix, tag, responseData) { - debugPrint('📊 [AppProvider] Binary response (0x8C) received - updating contact telemetry'); + debugPrint( + '📊 [AppProvider] Binary response (0x8C) received - updating contact telemetry', + ); // Binary response tag 0 = telemetry data (Cayenne LPP format) // Other tags may be used for different data types in the future contactsProvider.updateTelemetry(publicKeyPrefix, responseData); @@ -351,7 +382,9 @@ class AppProvider with ChangeNotifier { // When a contact's routing path is updated in the mesh network connectionProvider.onPathUpdated = (publicKey) { - debugPrint('🔄 [AppProvider] Path updated for contact: ${publicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}...'); + debugPrint( + '🔄 [AppProvider] Path updated for contact: ${publicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}...', + ); // Trigger a single contact fetch to get the updated path information // This is much more efficient than fetching all contacts // This happens asynchronously to avoid blocking the event handler @@ -365,11 +398,15 @@ class AppProvider with ChangeNotifier { // When an advertisement is received (PUSH_CODE_ADVERT 0x80) // This may be sent by the radio for existing contacts instead of PUSH_CODE_NEW_ADVERT (0x8A) connectionProvider.onAdvertReceived = (publicKey) { - debugPrint('📡 [AppProvider] Advertisement received: ${publicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}...'); + debugPrint( + '📡 [AppProvider] Advertisement received: ${publicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}...', + ); // Check if this is an existing contact that might have updated location final contact = contactsProvider.findContactByKey(publicKey); if (contact != null) { - debugPrint(' Existing contact "${contact.advName}" - fetching updated contact info (optimized)'); + debugPrint( + ' Existing contact "${contact.advName}" - fetching updated contact info (optimized)', + ); // Trigger a single contact fetch to get the updated contact information // This is much more efficient than fetching all contacts Future.delayed(const Duration(milliseconds: 100), () { @@ -378,44 +415,88 @@ class AppProvider with ChangeNotifier { } }); } else { - debugPrint(' New contact - waiting for PUSH_CODE_NEW_ADVERT (0x8A) with full details'); + contactsProvider.addPendingAdvert( + publicKey, + devicePublicKey: connectionProvider.deviceInfo.publicKey, + ); + debugPrint( + ' Unknown contact - added to pending adverts list and waiting for details', + ); } }; + // When firmware deletes a contact due to contacts table overflow (PUSH_CODE_CONTACT_DELETED 0x8F) + connectionProvider.onContactDeleted = (publicKey) { + final keyHex = publicKey + .map((b) => b.toRadixString(16).padLeft(2, '0')) + .join(''); + final contact = contactsProvider.findContactByKey(publicKey); + final name = contact?.advName ?? keyHex.substring(0, 12); + debugPrint('⚠️ [AppProvider] Contact deleted by firmware: $name'); + contactsProvider.removeContact(keyHex); + messagesProvider.logSystemMessage( + text: 'Contact "$name" was removed — device contacts table is full', + level: 'warning', + ); + }; + + // When firmware reports contacts storage is full (PUSH_CODE_CONTACTS_FULL 0x90) + connectionProvider.onContactsFull = () { + debugPrint('⚠️ [AppProvider] Contacts storage full'); + messagesProvider.logSystemMessage( + text: + 'Device contacts storage is full. New contacts will overwrite old ones.', + level: 'warning', + ); + }; + // When a message is sent (RESP_CODE_SENT received) - connectionProvider.onMessageSent = (messageId, expectedAckTag, suggestedTimeoutMs) { - debugPrint('📤 [AppProvider] Message sent - Message ID: $messageId, ACK tag: $expectedAckTag'); - messagesProvider.markMessageSent(messageId, expectedAckTag, suggestedTimeoutMs); + connectionProvider + .onMessageSent = (messageId, expectedAckTag, suggestedTimeoutMs) { + debugPrint( + '📤 [AppProvider] Message sent - Message ID: $messageId, ACK tag: $expectedAckTag', + ); + messagesProvider.markMessageSent( + messageId, + expectedAckTag, + suggestedTimeoutMs, + ); }; // When a message is delivered (PUSH_CODE_SEND_CONFIRMED received) connectionProvider.onMessageDelivered = (ackCode, roundTripTimeMs) { - debugPrint('✅ [AppProvider] Message delivered - ACK: $ackCode, RTT: ${roundTripTimeMs}ms'); + debugPrint( + '✅ [AppProvider] Message delivered - ACK: $ackCode, RTT: ${roundTripTimeMs}ms', + ); messagesProvider.markMessageDelivered(ackCode, roundTripTimeMs); }; // When an echo is detected for a public channel message (PUSH_CODE_LOG_RX_DATA matched) - connectionProvider.onMessageEchoDetected = (messageId, echoCount, snrRaw, rssiDbm) { - debugPrint('🔊 [AppProvider] Echo detected - Message: $messageId, Count: $echoCount'); + connectionProvider + .onMessageEchoDetected = (messageId, echoCount, snrRaw, rssiDbm) { + debugPrint( + '🔊 [AppProvider] Echo detected - Message: $messageId, Count: $echoCount', + ); messagesProvider.handleMessageEcho(messageId, echoCount, snrRaw, rssiDbm); }; // Wire up MessagesProvider's sendMessageCallback for retry logic - messagesProvider.sendMessageCallback = ({ - required contactPublicKey, - required text, - required messageId, - required contact, - retryAttempt = 0, - }) async { - return await connectionProvider.sendTextMessage( - contactPublicKey: contactPublicKey, - text: text, - messageId: messageId, - contact: contact, - retryAttempt: retryAttempt, - ); - }; + messagesProvider.sendMessageCallback = + ({ + required contactPublicKey, + required text, + required messageId, + required contact, + retryAttempt = 0, + }) async { + return await connectionProvider.sendTextMessage( + contactPublicKey: contactPublicKey, + text: text, + messageId: messageId, + contact: contact, + retryAttempt: retryAttempt, + ); + }; } /// Initialize the app (load contacts, sync time, etc.) @@ -446,19 +527,25 @@ class AppProvider with ChangeNotifier { // In simple mode: only sync first 5 channels for faster startup // In normal mode: sync all channels (up to device max) final channelsToSync = _isSimpleMode ? 5 : null; - debugPrint('📻 [AppProvider] Syncing channels${_isSimpleMode ? ' (simple mode: max 5)' : ''}...'); + debugPrint( + '📻 [AppProvider] Syncing channels${_isSimpleMode ? ' (simple mode: max 5)' : ''}...', + ); await connectionProvider.syncChannels(maxChannels: channelsToSync); debugPrint('✅ [AppProvider] Channel sync complete'); // Configure the default public channel (channel 0) // This must be done before sending any channel messages // Note: Some firmware versions may have this pre-configured - debugPrint('📻 [AppProvider] Configuring default public channel (channel 0)...'); + debugPrint( + '📻 [AppProvider] Configuring default public channel (channel 0)...', + ); try { await connectionProvider.configureDefaultPublicChannel(); debugPrint('✅ [AppProvider] Public channel configured successfully'); } catch (e) { - debugPrint('⚠️ [AppProvider] Public channel configuration failed (may already be configured): $e'); + debugPrint( + '⚠️ [AppProvider] Public channel configuration failed (may already be configured): $e', + ); // Continue anyway - channel might already be configured in firmware } @@ -467,19 +554,27 @@ class AppProvider with ChangeNotifier { // FALLBACK: Sync messages once after connection to catch any missed push notifications // This handles the case where messages arrived while the app was disconnected - debugPrint('🔄 [AppProvider] Performing initial message sync (fallback for missed pushes)'); + debugPrint( + '🔄 [AppProvider] Performing initial message sync (fallback for missed pushes)', + ); final initialMessageCount = await connectionProvider.syncAllMessages(); - debugPrint('📥 [AppProvider] Initial sync retrieved $initialMessageCount message(s)'); + debugPrint( + '📥 [AppProvider] Initial sync retrieved $initialMessageCount message(s)', + ); // Note: Future messages are synced automatically via PUSH_CODE_MSG_WAITING events // Start location tracking AFTER all initialization is complete - debugPrint('📍 [AppProvider] Starting location tracking after successful initialization'); + debugPrint( + '📍 [AppProvider] Starting location tracking after successful initialization', + ); await _startLocationTracking(); // Sync drawing messages with DrawingProvider // This restores any drawings that may be missing from storage - debugPrint('🎨 [AppProvider] Syncing drawing messages with DrawingProvider...'); + debugPrint( + '🎨 [AppProvider] Syncing drawing messages with DrawingProvider...', + ); messagesProvider.syncDrawingsWithProvider(drawingProvider); notifyListeners(); @@ -503,7 +598,9 @@ class AppProvider with ChangeNotifier { return; } - debugPrint('📂 [AppProvider] Found ${rooms.length} room(s), attempting auto-login...'); + debugPrint( + '📂 [AppProvider] Found ${rooms.length} room(s), attempting auto-login...', + ); final prefs = await SharedPreferences.getInstance(); @@ -513,7 +610,9 @@ class AppProvider with ChangeNotifier { final roomKey = 'room_password_${room.publicKeyHex}'; final savedPassword = prefs.getString(roomKey) ?? 'hello'; - debugPrint('🔑 [AppProvider] Auto-logging into room: ${room.advName}'); + debugPrint( + '🔑 [AppProvider] Auto-logging into room: ${room.advName}', + ); // Set up one-time callbacks for this room login await _loginToRoomWithCallback(room, savedPassword); @@ -521,7 +620,9 @@ class AppProvider with ChangeNotifier { // Small delay between logins to avoid overwhelming the device await Future.delayed(const Duration(milliseconds: 300)); } catch (e) { - debugPrint('❌ [AppProvider] Failed to auto-login to ${room.advName}: $e'); + debugPrint( + '❌ [AppProvider] Failed to auto-login to ${room.advName}: $e', + ); } } } catch (e) { @@ -539,13 +640,16 @@ class AppProvider with ChangeNotifier { final originalOnFail = connectionProvider.onLoginFail; // Set up temporary callbacks - connectionProvider.onLoginSuccess = (publicKeyPrefix, permissions, isAdmin, tag) async { + connectionProvider + .onLoginSuccess = (publicKeyPrefix, permissions, isAdmin, tag) async { // Restore original callbacks connectionProvider.onLoginSuccess = originalOnSuccess; connectionProvider.onLoginFail = originalOnFail; debugPrint('✅ [AppProvider] Auto-login successful for ${room.advName}'); - debugPrint('📡 [AppProvider] Room server will push messages automatically via PUSH_CODE_MSG_WAITING'); + debugPrint( + '📡 [AppProvider] Room server will push messages automatically via PUSH_CODE_MSG_WAITING', + ); completer.complete(true); }; @@ -555,7 +659,9 @@ class AppProvider with ChangeNotifier { connectionProvider.onLoginSuccess = originalOnSuccess; connectionProvider.onLoginFail = originalOnFail; - debugPrint('❌ [AppProvider] Auto-login failed for ${room.advName} (incorrect password)'); + debugPrint( + '❌ [AppProvider] Auto-login failed for ${room.advName} (incorrect password)', + ); completer.complete(false); }; @@ -581,7 +687,9 @@ class AppProvider with ChangeNotifier { // Restore callbacks on error connectionProvider.onLoginSuccess = originalOnSuccess; connectionProvider.onLoginFail = originalOnFail; - debugPrint('❌ [AppProvider] Error during auto-login to ${room.advName}: $e'); + debugPrint( + '❌ [AppProvider] Error during auto-login to ${room.advName}: $e', + ); } } @@ -595,11 +703,11 @@ class AppProvider with ChangeNotifier { try { // Sync contacts await connectionProvider.getContacts(); - + // Sync channels (respect simple mode settings) final channelsToSync = _isSimpleMode ? 5 : null; await connectionProvider.syncChannels(maxChannels: channelsToSync); - + // Messages are automatically synced via PUSH_CODE_MSG_WAITING events notifyListeners(); } catch (e) { @@ -614,9 +722,13 @@ class AppProvider with ChangeNotifier { if (!connectionProvider.deviceInfo.isConnected) return 0; try { - debugPrint('🔄 [AppProvider] Manual message sync requested (user initiated)'); + debugPrint( + '🔄 [AppProvider] Manual message sync requested (user initiated)', + ); final messageCount = await connectionProvider.syncAllMessages(); - debugPrint('✅ [AppProvider] Manual sync completed: $messageCount messages'); + debugPrint( + '✅ [AppProvider] Manual sync completed: $messageCount messages', + ); notifyListeners(); return messageCount; } catch (e) { @@ -634,7 +746,9 @@ class AppProvider with ChangeNotifier { // Location tracking will be started AFTER initialization completes if (!isConnected && wasTracking) { // Connection lost - stop location tracking - debugPrint('🔴 [AppProvider] BLE disconnected - stopping location tracking'); + debugPrint( + '🔴 [AppProvider] BLE disconnected - stopping location tracking', + ); _stopLocationTracking(); } } diff --git a/lib/providers/connection_provider.dart b/lib/providers/connection_provider.dart index f75c530..8820fd5 100644 --- a/lib/providers/connection_provider.dart +++ b/lib/providers/connection_provider.dart @@ -157,6 +157,8 @@ class ConnectionProvider with ChangeNotifier { Function(int channelIdx, String channelName, Uint8List secret, int? flags)? onChannelInfoReceived; Function(Uint8List publicKeyPrefix, int tag, Uint8List responseData)? onBinaryResponse; + Function(Uint8List publicKey)? onContactDeleted; + VoidCallback? onContactsFull; Function(Uint8List publicKey)? onAdvertReceived; Function(Uint8List publicKey)? onPathUpdated; Function(Uint8List publicKeyPrefix, int permissions, bool isAdmin, int tag)? @@ -319,6 +321,16 @@ class ConnectionProvider with ChangeNotifier { onChannelInfoReceived?.call(channelIdx, channelName, secret, flags); }; + _bleService.onContactDeleted = (publicKey) { + debugPrint('⚠️ [Provider] Contact deleted by firmware (contacts full overwrite)'); + onContactDeleted?.call(publicKey); + }; + + _bleService.onContactsFull = () { + debugPrint('⚠️ [Provider] Contacts storage is full'); + onContactsFull?.call(); + }; + _bleService.onMessageReceived = (message) { // Parse SAR markers final enhancedMessage = SarMessageParser.enhanceMessage(message); diff --git a/lib/providers/contacts_provider.dart b/lib/providers/contacts_provider.dart index 8d1d56a..c6321a6 100644 --- a/lib/providers/contacts_provider.dart +++ b/lib/providers/contacts_provider.dart @@ -4,9 +4,32 @@ import '../services/cayenne_lpp_parser.dart'; import '../services/contact_storage_service.dart'; import '../utils/key_comparison.dart'; +class PendingAdvert { + final Uint8List publicKey; + final DateTime receivedAt; + + const PendingAdvert({required this.publicKey, required this.receivedAt}); + + String get publicKeyHex => + publicKey.map((b) => b.toRadixString(16).padLeft(2, '0')).join(''); + + String get shortDisplayKey { + final prefix = publicKey.length >= 6 ? publicKey.sublist(0, 6) : publicKey; + return prefix.map((b) => b.toRadixString(16).padLeft(2, '0')).join(':'); + } + + PendingAdvert copyWith({Uint8List? publicKey, DateTime? receivedAt}) { + return PendingAdvert( + publicKey: publicKey ?? this.publicKey, + receivedAt: receivedAt ?? this.receivedAt, + ); + } +} + /// Contacts Provider - manages contact list and telemetry class ContactsProvider with ChangeNotifier { final Map _contacts = {}; + final Map _pendingAdverts = {}; final ContactStorageService _storageService = ContactStorageService(); bool _isInitialized = false; @@ -156,6 +179,9 @@ class ContactsProvider with ChangeNotifier { } List get contacts => _contacts.values.toList(); + List get pendingAdverts => + _pendingAdverts.values.toList() + ..sort((a, b) => b.receivedAt.compareTo(a.receivedAt)); List get chatContacts => contacts.where((c) => c.isChat).toList()..sort(_sortByLastSeen); @@ -195,11 +221,12 @@ class ContactsProvider with ChangeNotifier { /// Add or update a contact /// Excludes contacts that match the device's own public key void addOrUpdateContact(Contact contact, {Uint8List? devicePublicKey}) { - debugPrint('📝 [ContactsProvider] addOrUpdateContact called: ${contact.advName} (type: ${contact.type.displayName}, key: ${contact.publicKeyHex.substring(0, 8)}...)'); - + debugPrint( + '📝 [ContactsProvider] addOrUpdateContact called: ${contact.advName} (type: ${contact.type.displayName}, key: ${contact.publicKeyHex.substring(0, 8)}...)', + ); + // Don't add contacts that match our device's public key - if (devicePublicKey != null && - contact.publicKey.matches(devicePublicKey)) { + if (devicePublicKey != null && contact.publicKey.matches(devicePublicKey)) { debugPrint( 'ℹ️ [ContactsProvider] Ignoring contact with device\'s own public key: ${contact.advName}', ); @@ -208,7 +235,9 @@ class ContactsProvider with ChangeNotifier { // Check if this is a new contact final isNewContact = !_contacts.containsKey(contact.publicKeyHex); - debugPrint(' isNew: $isNewContact, total contacts before: ${_contacts.length}'); + debugPrint( + ' isNew: $isNewContact, total contacts before: ${_contacts.length}', + ); Contact updatedContact; if (isNewContact) { @@ -246,7 +275,10 @@ class ContactsProvider with ChangeNotifier { } _contacts[contact.publicKeyHex] = updatedContact; - debugPrint(' ✅ Contact added/updated. Total contacts: ${_contacts.length}, channels: ${channels.length}'); + _pendingAdverts.remove(contact.publicKeyHex); + debugPrint( + ' ✅ Contact added/updated. Total contacts: ${_contacts.length}, channels: ${channels.length}', + ); _persistContacts(); notifyListeners(); debugPrint(' 🔔 notifyListeners() called'); @@ -267,6 +299,7 @@ class ContactsProvider with ChangeNotifier { continue; } _contacts[contact.publicKeyHex] = contact; + _pendingAdverts.remove(contact.publicKeyHex); } if (excluded > 0) { debugPrint( @@ -303,8 +336,8 @@ class ContactsProvider with ChangeNotifier { // Update contact with new telemetry AND last seen time // lastAdvert is Unix timestamp in seconds - final currentTimestamp = - (DateTime.now().millisecondsSinceEpoch / 1000).round(); + final currentTimestamp = (DateTime.now().millisecondsSinceEpoch / 1000) + .round(); debugPrint(' Old lastAdvert: ${contact.lastAdvert}'); debugPrint(' New lastAdvert: $currentTimestamp'); @@ -351,6 +384,34 @@ class ContactsProvider with ChangeNotifier { return _contacts[keyHex]; } + /// Add or refresh a pending advert entry from PUSH_CODE_ADVERT (0x80). + /// Excludes self key and existing contacts. + void addPendingAdvert(Uint8List publicKey, {Uint8List? devicePublicKey}) { + if (devicePublicKey != null && publicKey.matches(devicePublicKey)) { + return; + } + + final keyHex = publicKey + .map((b) => b.toRadixString(16).padLeft(2, '0')) + .join(''); + if (_contacts.containsKey(keyHex)) { + _pendingAdverts.remove(keyHex); + return; + } + + final existing = _pendingAdverts[keyHex]; + final now = DateTime.now(); + if (existing != null) { + _pendingAdverts[keyHex] = existing.copyWith(receivedAt: now); + } else { + _pendingAdverts[keyHex] = PendingAdvert( + publicKey: Uint8List.fromList(publicKey), + receivedAt: now, + ); + } + notifyListeners(); + } + /// Find contact by name Contact? findContactByName(String name) { return contacts.firstWhere( @@ -404,6 +465,7 @@ class ContactsProvider with ChangeNotifier { /// Clear all contacts void clearContacts() { _contacts.clear(); + _pendingAdverts.clear(); _persistContacts(); notifyListeners(); } @@ -425,6 +487,7 @@ class ContactsProvider with ChangeNotifier { // Then remove from local storage _contacts.remove(publicKeyHex); + _pendingAdverts.remove(publicKeyHex); _persistContacts(); notifyListeners(); } diff --git a/lib/screens/contacts_tab.dart b/lib/screens/contacts_tab.dart index 34dbd18..fd3ec98 100644 --- a/lib/screens/contacts_tab.dart +++ b/lib/screens/contacts_tab.dart @@ -20,6 +20,7 @@ class ContactsTab extends StatefulWidget { class _ContactsTabState extends State { Position? _currentPosition; + final Set _resolvingAdvertKeys = {}; @override void initState() { @@ -59,6 +60,25 @@ class _ContactsTabState extends State { await _getCurrentLocation(); } + Future _handleResolveAdvert(PendingAdvert advert) async { + final keyHex = advert.publicKeyHex; + if (_resolvingAdvertKeys.contains(keyHex)) return; + + setState(() { + _resolvingAdvertKeys.add(keyHex); + }); + + try { + await context.read().getContact(advert.publicKey); + } finally { + if (mounted) { + setState(() { + _resolvingAdvertKeys.remove(keyHex); + }); + } + } + } + /// Calculate distance between two points in meters double _calculateDistanceInMeters( double lat1, @@ -90,6 +110,15 @@ class _ContactsTabState extends State { } } + String _formatRelativeTime(BuildContext context, DateTime when) { + final l10n = AppLocalizations.of(context)!; + final diff = DateTime.now().difference(when); + if (diff.inMinutes < 1) return l10n.justNow; + if (diff.inMinutes < 60) return l10n.minutesAgo(diff.inMinutes); + if (diff.inHours < 24) return l10n.hoursAgo(diff.inHours); + return l10n.daysAgo(diff.inDays); + } + /// Show the add channel dialog Future _showAddChannelDialog(BuildContext context) async { final l10n = AppLocalizations.of(context)!; @@ -140,11 +169,14 @@ class _ContactsTabState extends State { final repeaters = contactsProvider.repeaters; final rooms = contactsProvider.rooms; final channels = contactsProvider.channels; + final pendingAdverts = contactsProvider.pendingAdverts; // Check if there are any displayable contacts (excluding channels) - final hasDisplayableContacts = chatContacts.isNotEmpty || + final hasDisplayableContacts = + chatContacts.isNotEmpty || repeaters.isNotEmpty || - rooms.isNotEmpty; + rooms.isNotEmpty || + pendingAdverts.isNotEmpty; if (!hasDisplayableContacts) { return Center( @@ -177,6 +209,27 @@ class _ContactsTabState extends State { child: ListView( padding: const EdgeInsets.all(8), children: [ + // Pending adverts (public key only; quick resolve) + if (pendingAdverts.isNotEmpty) ...[ + _SectionHeader( + title: l10n.pending, + count: pendingAdverts.length, + icon: Icons.person_add_alt_1, + ), + ...pendingAdverts.map( + (advert) => _PendingAdvertTile( + advert: advert, + subtitle: + '${l10n.publicKey}: ${advert.publicKeyHex}\n${l10n.lastSeen}: ${_formatRelativeTime(context, advert.receivedAt)}', + isResolving: _resolvingAdvertKeys.contains( + advert.publicKeyHex, + ), + onResolve: () => _handleResolveAdvert(advert), + ), + ), + const Divider(height: 32), + ], + // Team Members (Chat contacts) if (chatContacts.isNotEmpty) ...[ _SectionHeader( @@ -280,6 +333,46 @@ class _ContactsTabState extends State { } } +class _PendingAdvertTile extends StatelessWidget { + final PendingAdvert advert; + final String subtitle; + final bool isResolving; + final VoidCallback onResolve; + + const _PendingAdvertTile({ + required this.advert, + required this.subtitle, + required this.isResolving, + required this.onResolve, + }); + + @override + Widget build(BuildContext context) { + return Card( + margin: const EdgeInsets.only(bottom: 8), + child: ListTile( + leading: const CircleAvatar(child: Icon(Icons.campaign_outlined)), + title: Text( + advert.shortDisplayKey, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + subtitle: Text(subtitle), + trailing: isResolving + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : IconButton( + icon: const Icon(Icons.person_add_alt_1), + tooltip: 'Quick add', + onPressed: onResolve, + ), + ), + ); + } +} + class _SectionHeader extends StatelessWidget { final String title; final int count; diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 66c9081..bf30896 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -53,6 +53,8 @@ class _HomeScreenState extends State @override void initState() { super.initState(); + // Initialize synchronously so first build always has a valid controller. + _initTabController(); _loadMapEnabledAndInitTabs(); _loadRxTxPreference(); @@ -67,12 +69,9 @@ class _HomeScreenState extends State Future _loadMapEnabledAndInitTabs() async { final prefs = await SharedPreferences.getInstance(); final mapEnabled = prefs.getBool('map_enabled') ?? true; - if (mapEnabled != _isMapEnabled) { - _isMapEnabled = mapEnabled; - } - _initTabController(); - if (mounted) { - setState(() {}); + if (!mounted) return; + if (_isMapEnabled != mapEnabled) { + _updateTabController(mapEnabled); } } @@ -286,7 +285,8 @@ class _HomeScreenState extends State } // Determine if we should hide the UI (only in fullscreen on map tab) - final shouldHideUI = _isMapEnabled && _isMapFullscreen && _currentIndex == 2; + final shouldHideUI = + _isMapEnabled && _isMapFullscreen && _currentIndex == 2; return Scaffold( appBar: shouldHideUI @@ -296,7 +296,9 @@ class _HomeScreenState extends State actions: [ Consumer( builder: (context, provider, child) { - final isConnected = provider.deviceInfo.isConnected || provider.isSseClientConnected; + final isConnected = + provider.deviceInfo.isConnected || + provider.isSseClientConnected; if (isConnected) { return IconButton( onPressed: () async { @@ -540,11 +542,14 @@ class _HomeScreenState extends State color: isSseConnected ? Colors.green : (deviceInfo.signalRssi != null - ? BatteryDisplayHelper.getSignalColor(deviceInfo.signalRssi!) - : Colors.grey), + ? BatteryDisplayHelper.getSignalColor( + deviceInfo.signalRssi!, + ) + : Colors.grey), size: 13, ), - if (isBleConnected && deviceInfo.signalRssi != null) ...[ + if (isBleConnected && + deviceInfo.signalRssi != null) ...[ const SizedBox(width: 3), Text( '${deviceInfo.signalRssi}', @@ -569,7 +574,9 @@ class _HomeScreenState extends State if (deviceInfo.batteryPercent != null) ...[ const SizedBox(width: 8), Icon( - BatteryDisplayHelper.getBatteryIcon(deviceInfo.batteryPercent!), + BatteryDisplayHelper.getBatteryIcon( + deviceInfo.batteryPercent!, + ), color: BatteryDisplayHelper.getBatteryColor( deviceInfo.batteryPercent!, ), diff --git a/lib/services/ble/ble_response_handler.dart b/lib/services/ble/ble_response_handler.dart index 46e0334..b3f33ab 100644 --- a/lib/services/ble/ble_response_handler.dart +++ b/lib/services/ble/ble_response_handler.dart @@ -98,6 +98,8 @@ class BleResponseHandler { OnChannelInfoCallback? onChannelInfoReceived; OnMessageEchoDetectedCallback? onMessageEchoDetected; VoidCallback? onRxActivity; + void Function(Uint8List publicKey)? onContactDeleted; + VoidCallback? onContactsFull; // Track the last command that was sent, so we can retry if it fails with ERR_CODE_NOT_FOUND Uint8List? _lastContactPublicKey; @@ -183,6 +185,14 @@ class BleResponseHandler { debugPrint(' → Handling ChannelMessage'); _handleChannelMessage(reader); break; + case MeshCoreConstants.respContactMsgRecvV3: + debugPrint(' → Handling ContactMessage V3'); + _handleContactMessageV3(reader); + break; + case MeshCoreConstants.respChannelMsgRecvV3: + debugPrint(' → Handling ChannelMessage V3'); + _handleChannelMessageV3(reader); + break; case MeshCoreConstants.pushTelemetryResponse: debugPrint(' → Handling TelemetryResponse'); _handleTelemetryResponse(reader); @@ -251,6 +261,20 @@ class BleResponseHandler { debugPrint(' → Response: No More Messages'); onNoMoreMessages?.call(); break; + case MeshCoreConstants.pushPathDiscoveryResponse: + debugPrint(' → Path discovery response (not yet handled)'); + break; + case MeshCoreConstants.pushControlData: + debugPrint(' → Control data push (not yet handled)'); + break; + case MeshCoreConstants.pushContactDeleted: + debugPrint(' → Handling ContactDeleted push'); + _handleContactDeleted(reader); + break; + case MeshCoreConstants.pushContactsFull: + debugPrint(' → Contacts storage full'); + onContactsFull?.call(); + break; case MeshCoreConstants.respOk: debugPrint(' → Response: OK'); // Complete any pending ACK command @@ -349,6 +373,43 @@ class BleResponseHandler { } } + /// Handle ContactMessage V3 response (firmware ver >= 3, has SNR header) + void _handleContactMessageV3(BufferReader reader) { + try { + final message = FrameParser.parseContactMessageV3(reader); + debugPrint(' ✅ [ContactMessage V3] Parsed successfully'); + onMessageReceived?.call(message); + } catch (e) { + debugPrint(' ❌ [ContactMessage V3] Parsing error: $e'); + onError?.call('Contact message V3 parsing error: $e'); + } + } + + /// Handle ChannelMessage V3 response (firmware ver >= 3, has SNR header) + void _handleChannelMessageV3(BufferReader reader) { + try { + final message = FrameParser.parseChannelMessageV3(reader); + debugPrint(' ✅ [ChannelMessage V3] Parsed successfully'); + onMessageReceived?.call(message); + } catch (e) { + debugPrint(' ❌ [ChannelMessage V3] Parsing error: $e'); + onError?.call('Channel message V3 parsing error: $e'); + } + } + + /// Handle ContactDeleted push (0x8F) — contact overwritten due to contacts full + void _handleContactDeleted(BufferReader reader) { + try { + if (reader.remainingBytesCount >= 32) { + final publicKey = reader.readBytes(32); + debugPrint(' ✅ [ContactDeleted] Contact removed by firmware'); + onContactDeleted?.call(Uint8List.fromList(publicKey)); + } + } catch (e) { + debugPrint(' ❌ [ContactDeleted] Parsing error: $e'); + } + } + /// Handle TelemetryResponse push void _handleTelemetryResponse(BufferReader reader) { try { diff --git a/lib/services/meshcore_ble_service.dart b/lib/services/meshcore_ble_service.dart index 6a6fee7..817b601 100644 --- a/lib/services/meshcore_ble_service.dart +++ b/lib/services/meshcore_ble_service.dart @@ -91,6 +91,8 @@ class MeshCoreBleService { OnErrorCallback? onError; OnContactNotFoundCallback? onContactNotFound; OnChannelInfoCallback? onChannelInfoReceived; + void Function(Uint8List publicKey)? onContactDeleted; + VoidCallback? onContactsFull; // Activity callbacks (for blinking indicators) VoidCallback? onRxActivity; @@ -213,6 +215,12 @@ class MeshCoreBleService { _responseHandler.onChannelInfoReceived = (int channelIdx, String channelName, Uint8List secret, int? flags) { onChannelInfoReceived?.call(channelIdx, channelName, secret, flags); }; + _responseHandler.onContactDeleted = (publicKey) { + onContactDeleted?.call(publicKey); + }; + _responseHandler.onContactsFull = () { + onContactsFull?.call(); + }; _responseHandler.onRxActivity = () { onRxActivity?.call(); }; diff --git a/lib/services/meshcore_constants.dart b/lib/services/meshcore_constants.dart index 2c9d3c5..8e7bbfc 100644 --- a/lib/services/meshcore_constants.dart +++ b/lib/services/meshcore_constants.dart @@ -1,7 +1,7 @@ /// MeshCore BLE and Protocol Constants class MeshCoreConstants { - // Supported protocol version - static const int supportedCompanionProtocolVersion = 1; + // Supported protocol version (firmware uses this to decide V1 vs V3 message frames) + static const int supportedCompanionProtocolVersion = 3; // BLE Service and Characteristic UUIDs static const String bleServiceUuid = @@ -39,6 +39,8 @@ class MeshCoreConstants { static const int cmdSendRawData = 25; static const int cmdSendLogin = 26; static const int cmdSendStatusReq = 27; + static const int cmdHasConnection = 28; + static const int cmdLogout = 29; static const int cmdGetContactByKey = 30; static const int cmdGetChannel = 31; static const int cmdSetChannel = 32; @@ -46,9 +48,23 @@ class MeshCoreConstants { static const int cmdSignData = 34; static const int cmdSignFinish = 35; static const int cmdSendTracePath = 36; + static const int cmdSetDevicePin = 37; static const int cmdSetOtherParams = 38; static const int cmdSendTelemetryReq = 39; + static const int cmdGetCustomVars = 40; + static const int cmdSetCustomVar = 41; + static const int cmdGetAdvertPath = 42; + static const int cmdGetTuningParams = 43; static const int cmdSendBinaryReq = 50; + static const int cmdFactoryReset = 51; + static const int cmdSendPathDiscoveryReq = 52; + static const int cmdSetFloodScope = 54; // v8+ + static const int cmdSendControlData = 55; // v8+ + static const int cmdGetStats = 56; // v8+ + static const int cmdSendAnonReq = 57; + static const int cmdSetAutoaddConfig = 58; + static const int cmdGetAutoaddConfig = 59; + static const int cmdGetAllowedRepeatFreq = 60; // Response Codes (Device -> App) static const int respOk = 0; @@ -58,8 +74,8 @@ class MeshCoreConstants { static const int respEndOfContacts = 4; static const int respSelfInfo = 5; static const int respSent = 6; - static const int respContactMsgRecv = 7; - static const int respChannelMsgRecv = 8; + static const int respContactMsgRecv = 7; // firmware ver < 3 + static const int respChannelMsgRecv = 8; // firmware ver < 3 static const int respCurrTime = 9; static const int respNoMoreMessages = 10; static const int respExportContact = 11; @@ -67,12 +83,17 @@ class MeshCoreConstants { static const int respDeviceInfo = 13; static const int respPrivateKey = 14; static const int respDisabled = 15; + static const int respContactMsgRecvV3 = 16; // firmware ver >= 3 (adds SNR header) + static const int respChannelMsgRecvV3 = 17; // firmware ver >= 3 (adds SNR header) static const int respChannelInfo = 18; static const int respSignStart = 19; static const int respSignature = 20; static const int respCustomVars = 21; static const int respAdvertPath = 22; - static const int respTuningParams = 21; // Same as respCustomVars per protocol + static const int respTuningParams = 23; + static const int respStats = 24; // v8+ + static const int respAutoaddConfig = 25; + static const int respAllowedRepeatFreq = 26; // Push Codes (Device -> App, unsolicited) static const int pushAdvert = 0x80; @@ -88,6 +109,15 @@ class MeshCoreConstants { static const int pushNewAdvert = 0x8A; static const int pushTelemetryResponse = 0x8B; static const int pushBinaryResponse = 0x8C; + static const int pushPathDiscoveryResponse = 0x8D; + static const int pushControlData = 0x8E; // v8+ + static const int pushContactDeleted = 0x8F; // contact overwritten when contacts full + static const int pushContactsFull = 0x90; // contacts storage is full + + // Stats sub-types for cmdGetStats + static const int statsTypeCore = 0; + static const int statsTypeRadio = 1; + static const int statsTypePackets = 2; // Error Codes static const int errUnsupportedCmd = 1; diff --git a/lib/services/meshcore_opcode_names.dart b/lib/services/meshcore_opcode_names.dart index 07a2dfb..ea7dc1c 100644 --- a/lib/services/meshcore_opcode_names.dart +++ b/lib/services/meshcore_opcode_names.dart @@ -59,6 +59,10 @@ class MeshCoreOpcodeNames { return 'SEND_LOGIN'; case MeshCoreConstants.cmdSendStatusReq: return 'SEND_STATUS_REQ'; + case MeshCoreConstants.cmdHasConnection: + return 'HAS_CONNECTION'; + case MeshCoreConstants.cmdLogout: + return 'LOGOUT'; case MeshCoreConstants.cmdGetContactByKey: return 'GET_CONTACT_BY_KEY'; case MeshCoreConstants.cmdGetChannel: @@ -73,12 +77,40 @@ class MeshCoreOpcodeNames { return 'SIGN_FINISH'; case MeshCoreConstants.cmdSendTracePath: return 'SEND_TRACE_PATH'; + case MeshCoreConstants.cmdSetDevicePin: + return 'SET_DEVICE_PIN'; case MeshCoreConstants.cmdSetOtherParams: return 'SET_OTHER_PARAMS'; case MeshCoreConstants.cmdSendTelemetryReq: return 'SEND_TELEMETRY_REQ'; + case MeshCoreConstants.cmdGetCustomVars: + return 'GET_CUSTOM_VARS'; + case MeshCoreConstants.cmdSetCustomVar: + return 'SET_CUSTOM_VAR'; + case MeshCoreConstants.cmdGetAdvertPath: + return 'GET_ADVERT_PATH'; + case MeshCoreConstants.cmdGetTuningParams: + return 'GET_TUNING_PARAMS'; case MeshCoreConstants.cmdSendBinaryReq: return 'SEND_BINARY_REQ'; + case MeshCoreConstants.cmdFactoryReset: + return 'FACTORY_RESET'; + case MeshCoreConstants.cmdSendPathDiscoveryReq: + return 'SEND_PATH_DISCOVERY_REQ'; + case MeshCoreConstants.cmdSetFloodScope: + return 'SET_FLOOD_SCOPE'; + case MeshCoreConstants.cmdSendControlData: + return 'SEND_CONTROL_DATA'; + case MeshCoreConstants.cmdGetStats: + return 'GET_STATS'; + case MeshCoreConstants.cmdSendAnonReq: + return 'SEND_ANON_REQ'; + case MeshCoreConstants.cmdSetAutoaddConfig: + return 'SET_AUTOADD_CONFIG'; + case MeshCoreConstants.cmdGetAutoaddConfig: + return 'GET_AUTOADD_CONFIG'; + case MeshCoreConstants.cmdGetAllowedRepeatFreq: + return 'GET_ALLOWED_REPEAT_FREQ'; default: return 'CMD_UNKNOWN'; } @@ -119,12 +151,28 @@ class MeshCoreOpcodeNames { return 'PRIVATE_KEY'; case MeshCoreConstants.respDisabled: return 'DISABLED'; + case MeshCoreConstants.respContactMsgRecvV3: + return 'CONTACT_MSG_RECV_V3'; + case MeshCoreConstants.respChannelMsgRecvV3: + return 'CHANNEL_MSG_RECV_V3'; case MeshCoreConstants.respChannelInfo: return 'CHANNEL_INFO'; case MeshCoreConstants.respSignStart: return 'SIGN_START'; case MeshCoreConstants.respSignature: return 'SIGNATURE'; + case MeshCoreConstants.respCustomVars: + return 'CUSTOM_VARS'; + case MeshCoreConstants.respAdvertPath: + return 'ADVERT_PATH'; + case MeshCoreConstants.respTuningParams: + return 'TUNING_PARAMS'; + case MeshCoreConstants.respStats: + return 'STATS'; + case MeshCoreConstants.respAutoaddConfig: + return 'AUTOADD_CONFIG'; + case MeshCoreConstants.respAllowedRepeatFreq: + return 'ALLOWED_REPEAT_FREQ'; default: return 'RESP_UNKNOWN'; } @@ -159,6 +207,14 @@ class MeshCoreOpcodeNames { return 'TELEMETRY_RESPONSE'; case MeshCoreConstants.pushBinaryResponse: return 'BINARY_RESPONSE'; + case MeshCoreConstants.pushPathDiscoveryResponse: + return 'PATH_DISCOVERY_RESPONSE'; + case MeshCoreConstants.pushControlData: + return 'CONTROL_DATA'; + case MeshCoreConstants.pushContactDeleted: + return 'CONTACT_DELETED'; + case MeshCoreConstants.pushContactsFull: + return 'CONTACTS_FULL'; default: return 'PUSH_UNKNOWN'; } diff --git a/lib/services/protocol/frame_parser.dart b/lib/services/protocol/frame_parser.dart index b38c82d..c56c6a2 100644 --- a/lib/services/protocol/frame_parser.dart +++ b/lib/services/protocol/frame_parser.dart @@ -59,6 +59,23 @@ class FrameParser { return {}; } + /// Parse ContactMessage V3 response (firmware ver >= 3) + /// V3 prepends 3 bytes: [snr_scaled(int8)][reserved][reserved] + /// snr_dB = snr_scaled / 4.0 + static Message parseContactMessageV3(BufferReader reader) { + reader.readInt8(); // snr scaled by 4 (ignored for now) + reader.readBytes(2); // reserved + return parseContactMessage(reader); + } + + /// Parse ChannelMessage V3 response (firmware ver >= 3) + /// V3 prepends 3 bytes: [snr_scaled(int8)][reserved][reserved] + static Message parseChannelMessageV3(BufferReader reader) { + reader.readInt8(); // snr scaled by 4 (ignored for now) + reader.readBytes(2); // reserved + return parseChannelMessage(reader); + } + /// Parse ContactMessage response static Message parseContactMessage(BufferReader reader) { final pubKeyPrefix = reader.readBytes(6); diff --git a/lib/widgets/messages/message_bubble.dart b/lib/widgets/messages/message_bubble.dart index 7544945..a9b0dde 100644 --- a/lib/widgets/messages/message_bubble.dart +++ b/lib/widgets/messages/message_bubble.dart @@ -868,11 +868,29 @@ class _MessageBubbleState extends State { ), ], // Time for regular messages (not shown for SAR/drawing as it's already above) - if (!isSarMarker && !message.isDrawing) + if (!isSarMarker && !message.isDrawing) ...[ + // Hop count indicator for received messages + if (!isOwnMessage && message.pathLen < 255) ...[ + const SizedBox(width: 4), + Icon( + Icons.alt_route, + size: 11, + color: Theme.of(context).textTheme.labelSmall?.color?.withValues(alpha: 0.6), + ), + const SizedBox(width: 2), + Text( + message.pathLen == 0 ? 'direct' : '${message.pathLen}hop', + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: Theme.of(context).textTheme.labelSmall?.color?.withValues(alpha: 0.6), + ), + ), + const SizedBox(width: 4), + ], Text( message.getLocalizedTimeAgo(context), style: Theme.of(context).textTheme.labelSmall, ), + ], ], ), const SizedBox(height: 8),