Files
meshcore-sar_android/CLAUDE.md

28 KiB

CLAUDE.md - MeshCore SAR Technical Reference

AI assistant guide for the MeshCore SAR Flutter application.

Table of Contents

  1. Critical Development Rules
  2. Quick Reference
  3. Project Structure
  4. Protocol Reference
  5. Architecture
  6. Common Tasks
  7. 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

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:<emoji>:<latitude>,<longitude>:<optional_message>

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:<json>

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

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:

    {
      "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:

    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_<timestamp>.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:

// 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

# 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):

cd ios && pod install && cd ..
flutter clean
flutter pub get
flutter run

iOS Pod Install Fails:

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 + intelligent mesh location broadcasting

Callbacks: onPositionUpdate, onError, onBroadcastSent, onTrackingStateChanged

Thresholds:

  • Min distance: 5.0m
  • Max distance: 100.0m
  • Min time: 30s

Logic:

  • First update → broadcast immediately
  • ≥100m moved → broadcast immediately
  • ≥5m + ≥30s → broadcast

File: lib/services/location_tracking_service.dart (501 lines)

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<T> or ParseResult<T>

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:

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):

// Origin MUST match WMTS TileMatrixSet TopLeftCorner
final origin = Point<double>(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:
    curl "https://prostor.zgs.gov.si/geowebcache/service/wmts?REQUEST=GetCapabilities&SERVICE=WMTS"
    
  2. Find <TileMatrixSet> for EPSG:3794
  3. Extract <TopLeftCorner> (origin)
  4. Extract <ScaleDenominator> for each <TileMatrix> (convert to resolutions)
  5. Query WMS GetCapabilities for bounds:
    curl "https://prostor.zgs.gov.si/geowebcache/service/wms?REQUEST=GetCapabilities&SERVICE=WMS"
    

Caching Strategy

Implementation (lib/services/tile_cache_service.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
// 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):

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:

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:

    curl "https://prostor.zgs.gov.si/geowebcache/service/wms?REQUEST=GetCapabilities" | grep "<Name>"
    
  2. Add to MapLayer (lib/models/map_layer.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


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)