Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
104 commits
Select commit Hold shift + click to select a range
0719697
inital working LibreMap
MrAlders0n Apr 11, 2026
9df1c63
Enhance map widget state management by adding flags to prevent concur…
MrAlders0n Apr 12, 2026
4cdad50
Merge remote-tracking branch 'origin/dev' into maplibre_gl
MrAlders0n Apr 14, 2026
190cb62
add offline management
446564 Apr 15, 2026
82ee9d7
format to match dev
446564 Apr 15, 2026
41f0adb
format with dart
MrAlders0n Apr 16, 2026
a7cfd44
Fix power level hint directing users to Settings instead of Connect tab
MrAlders0n Apr 16, 2026
85fe733
Merge branch 'dev' into maplibre_gl (graph fix)
MrAlders0n Apr 16, 2026
85f0a13
Merge offline-mgmt into maplibre_gl
MrAlders0n Apr 16, 2026
a7a0c32
Fix power level hint: Settings → Connect tab
MrAlders0n Apr 16, 2026
8f3c2da
Center download selection map on user last known or current location
446564 Apr 16, 2026
f16c459
update latest deps
446564 Apr 16, 2026
cbaf99f
clean up missing braces
446564 Apr 16, 2026
ac0a6d3
Merge pull request #26 from MeshMapper/maplibre_gl
MrAlders0n Apr 19, 2026
9f8f5ac
feat(offline-maps): add download cancellation, queueing, and storage …
MrAlders0n Apr 19, 2026
524435d
Making download tiles easier.
MrAlders0n Apr 19, 2026
8b0fe33
Modifications to offline maps and cache management
MrAlders0n Apr 20, 2026
3e5242f
feat(offline-maps): update offline region management and cache handling
MrAlders0n Apr 20, 2026
bec7fd3
feat(repeater-popup): enhance logging and match count for repeater ID…
MrAlders0n Apr 21, 2026
41b5c4a
- New Flood Traffic toggle at the top of Settings → Modes (default OF…
MrAlders0n Apr 22, 2026
c43ac04
- Fixed Hive preferences being wiped by transient open failures. The…
MrAlders0n Apr 25, 2026
dcb57a7
- Fixed an iOS crash where backgrounded sessions could be killed hour…
MrAlders0n Apr 27, 2026
170e55f
- **Repeater Marker Clustering:** Stacked repeater markers at the sam…
MrAlders0n Apr 27, 2026
6995cdc
- Fixed the GPS marker drifting toward the top of the map (and off-sc…
MrAlders0n Apr 27, 2026
6d13f45
- **RX Hop Path:** Tapping an RX marker now shows the full mesh path …
MrAlders0n Apr 28, 2026
dba07f9
- Fixed spiderfy pulling in markers from neighbouring clusters when t…
MrAlders0n Apr 28, 2026
c05cbea
- **Stale Repeater Filtering:** Repeaters not heard in the past 30 da…
MrAlders0n Apr 28, 2026
db29741
Improvements
MrAlders0n Apr 30, 2026
c96dd42
- **Per-Region Stale Repeater Threshold:** The stale repeater cutoff …
MrAlders0n May 1, 2026
12ae67b
- **Battery & Performance:** Countdown timers (cooldown, auto-ping, R…
MrAlders0n May 1, 2026
e0a9f5f
- **(iOS) Launch & Resume Crash:** Fixed a crash on app launch and re…
MrAlders0n May 3, 2026
bc35531
Fixed Anonymous mode getting permanently stuck after an unexpected Bl…
MrAlders0n May 9, 2026
4744360
Minor fix to the last commit
MrAlders0n May 9, 2026
d818c0f
- **(Android) Fixed a crash when returning to the app after extended …
MrAlders0n May 9, 2026
5ec79e8
- Fixed being unable to switch from offline back to online mode while…
MrAlders0n May 9, 2026
4b1214c
- Fixed misleading "No empty channel slots" error when BLE disconnect…
MrAlders0n May 9, 2026
225064d
- Fixed offline sessions getting stuck at "partial upload" and failin…
MrAlders0n May 9, 2026
a66debd
- Fixed offline sessions getting stuck at "partial upload" and failin…
MrAlders0n May 9, 2026
e4034da
- Fixed auto-ping mode buttons disappearing after reconnect timeout a…
MrAlders0n May 10, 2026
d24cd30
TCP/USB transport support
MrAlders0n May 13, 2026
21395ac
- **Auto-Ping Grace Period:** Fixed auto-ping timers not actually sto…
MrAlders0n May 14, 2026
f6ae59a
Add zoneDisabled enum value and new offline upload diagnostic script
MrAlders0n May 17, 2026
dae5bf4
- Fixed auto-ping mode not validated against new zone permissions on …
MrAlders0n May 17, 2026
a6edfca
Add design spec for focus mode minimize feature
MrAlders0n May 17, 2026
e13e5f8
Add implementation plan for focus mode minimize feature
MrAlders0n May 17, 2026
918b78b
feat: add focus panel state fields, update _activatePingFocus and _di…
MrAlders0n May 17, 2026
417d2ae
feat: add minimize button to TX ping details sheet
MrAlders0n May 17, 2026
d835cf3
Add minimized focus panel pill and wire into map Stack
MrAlders0n May 17, 2026
af2e771
Hide control panel during focus mode so minimized pill is visible
MrAlders0n May 17, 2026
ef1d2c8
feat: enhance repeater ID display with ambiguity indicator and update…
MrAlders0n May 18, 2026
415f11d
- Session History: The "Graph" tab is now "History." View past sessio…
MrAlders0n May 18, 2026
73cad99
- Coverage overlay tile refresh debounce increased from 5s to 30s to …
MrAlders0n May 18, 2026
6e4ec3d
- Fixed generic "Timestamp is too old" registration failure now diagn…
MrAlders0n May 18, 2026
22da229
feat: add TCP health check after app resume to ensure connection stab…
MrAlders0n May 18, 2026
51cc2aa
- Fixed auto-ping permanently stopping when crossing a regional zone …
MrAlders0n May 21, 2026
5d56fe2
- "Clear Map Markers" now also removes discovery and trace markers
MrAlders0n May 21, 2026
3b5c115
⏺ - Added a toggle button to the map control panel to show/hide the r…
MrAlders0n May 21, 2026
95f02d8
Shortened the "DUPLICATE ID" label on focus mode lines to "DUP" so it…
MrAlders0n May 21, 2026
043220a
- Multi-hop TX echoes are now grouped under their parent TX ping inst…
MrAlders0n May 21, 2026
fd619b7
Enhance map widget: add multi-hop only icon and regional boundary toggle
MrAlders0n May 26, 2026
804fa9f
Refactor map widget to improve tile load failure handling and sync fo…
MrAlders0n May 26, 2026
0bbb6fa
feat: The app now sends your radio preset to the server on connect, a…
MrAlders0n Jun 11, 2026
3cb8b17
Fixed formatting of radio preset in app
MrAlders0n Jun 11, 2026
a8805dd
Privacy: your location is no longer broadcast over the air
MrAlders0n Jun 11, 2026
d74f7ac
Added Vector Tile Support and Refreshing single tiles by injecting Ge…
MrAlders0n Jun 13, 2026
af44cf9
perf: stop the map rebuilding on every notifyListeners (wardrive over…
MrAlders0n Jun 14, 2026
b80bc1a
docs: document map rebuild isolation (mapRevision) architecture
MrAlders0n Jun 14, 2026
489064d
perf: make the mode-button glow static (stop the all-session GPU fram…
MrAlders0n Jun 15, 2026
3c27820
perf: stop the map relayouting on every GPS tick (wardrive overheat fix)
MrAlders0n Jun 15, 2026
d5b556d
feat: bake repeater hex into Detailed-mode chip icons; rename "Covera…
MrAlders0n Jun 16, 2026
8eadf84
fix: reword coverage mode, fix map draw from backgrounded
MrAlders0n Jun 17, 2026
5d7d3d3
feat: in-app cell GRID SUMMARY + repeater popup; full-hex token resol…
MrAlders0n Jun 18, 2026
402dcbd
Fix path-mode warning: app restores original setting on clean disconnect
MrAlders0n Jun 18, 2026
87e4454
Detailed-mode cell tap: blob-aware summary + clicked-tile 3x3 highlight
MrAlders0n Jun 18, 2026
14e45c1
TX wire-tag: send tag + plaintext coords in Broadcast-Coordinates mode
MrAlders0n Jun 18, 2026
4a6a90b
Offline sessions: enable mobile download via the native share sheet
MrAlders0n Jun 18, 2026
a18be02
Apply map style + CVD palette changes immediately (bump mapRevision)
MrAlders0n Jun 18, 2026
c93540a
perf: incremental coverage-marker sync (stop re-pushing every pin eac…
MrAlders0n Jun 19, 2026
117c0ce
- Fixed offline sessions failing to upload — the app now waits for th…
MrAlders0n Jun 19, 2026
1357328
Offline sessions: capture device model, power level, and app version …
MrAlders0n Jun 19, 2026
77107e9
Fix app crash on launch / resume-from-background: reject invalid map …
MrAlders0n Jun 19, 2026
146a2e5
Detailed-mode cell tap: filled dominant-colour footprint + dimmed bac…
MrAlders0n Jun 19, 2026
a74b530
Cell-summary & repeater-detail popups: bright map (no scrim) + minimi…
MrAlders0n Jun 20, 2026
d8a9346
Minimized cell/repeater pill: hide the control bar (like focus mode)
MrAlders0n Jun 20, 2026
2b64b35
Sounds follow media volume; landscape controls can minimize to a cent…
MrAlders0n Jun 20, 2026
2ae6d33
TX 'Broadcast My Coordinates': drop combined wire-tag+coords on-air f…
MrAlders0n Jun 20, 2026
6a7e939
Revert "TX 'Broadcast My Coordinates': drop combined wire-tag+coords …
MrAlders0n Jun 20, 2026
d99687f
Coverage tap-to-inspect: connection lines + repeater coverage cells
MrAlders0n Jun 20, 2026
85bbb35
Coverage tap-to-inspect fixes + tweaks
MrAlders0n Jun 20, 2026
f3277e1
Unify focus camera across coverage views; fix spider in Detailed Mode
MrAlders0n Jun 20, 2026
63038d3
feat(offline): per-region placement summary after upload
MrAlders0n Jun 21, 2026
6e13325
Fix recurring iOS launch/focus crash: map fit-to-bounds could send Ma…
MrAlders0n Jun 21, 2026
0e22a3f
Resume ping counter from /auth resume_counter on session reuse
MrAlders0n Jun 21, 2026
da66ff7
Vendor patched maplibre_gl 0.25.0 with camera-viewport crash guard
MrAlders0n Jun 21, 2026
3323e4e
Fix RX pin z-order: newest pin always renders on top
MrAlders0n Jun 21, 2026
3e78a43
fix: text out of bounds
MrAlders0n Jun 21, 2026
be7d5be
update: podfile
MrAlders0n Jun 21, 2026
a2ae740
Offline upload: distinguish network/timeout from auth failure
MrAlders0n Jun 21, 2026
42446e6
add test
MrAlders0n Jun 21, 2026
c72cd64
Fix multi-hop TX display in history; GPS puck z-order; cooldown UI
MrAlders0n Jun 22, 2026
f8979a0
Encode session date in TX wire-tag to fix cross-day collision (DEAD t…
MrAlders0n Jun 22, 2026
0e8858b
Ping controls: instant start-mode feedback + cut rebuild churn
MrAlders0n Jun 22, 2026
d145dd4
Fix GPS puck blink; log stuck-timer ping lockout
MrAlders0n Jun 23, 2026
222a300
Hide donation link on iOS to satisfy App Store guideline 3.1.1
MrAlders0n Jun 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .build_version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.2.2
1.3.0
13 changes: 12 additions & 1 deletion Build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@

set -e # Exit on any error

# maplibre_gl 0.25.0 plugin requires JDK 21 to compile.
# Force the build to use Homebrew openjdk@21, regardless of the user's shell JAVA_HOME.
JDK21_HOME="/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home"
if [ ! -d "$JDK21_HOME" ]; then
echo "Error: JDK 21 not found at $JDK21_HOME"
echo "Install with: brew install openjdk@21"
exit 1
fi
export JAVA_HOME="$JDK21_HOME"
export PATH="$JAVA_HOME/bin:$PATH"

# Semver comparison: returns 0 (true) if $1 >= $2
version_gte() {
local IFS=.
Expand Down Expand Up @@ -154,7 +165,7 @@ echo ""
# Build iOS IPA
echo "[3/3] Building iOS IPA..."
(cd ios && pod install)
flutter build ipa --release --build-name="$VERSION_NUMBER" --build-number="$EPOCH" --dart-define="APP_VERSION=$APP_VERSION" --dart-define="API_KEY=$MESHMAPPER_API_KEY"
flutter build ipa --release --build-name="$VERSION_NUMBER" --build-number="$EPOCH" --dart-define="APP_VERSION=$APP_VERSION" --dart-define="API_KEY=$MESHMAPPER_API_KEY" --export-options-plist=ios/ExportOptions.plist
cp build/ios/ipa/mesh_mapper.ipa "$IOS_DIR/MeshMapper-$FILE_TAG.ipa"
echo "✓ Built: MeshMapper-$FILE_TAG.ipa"
echo ""
Expand Down
162 changes: 158 additions & 4 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,20 @@ MESHMAPPER_API_KEY=<your-key> ./Build.sh
The app uses a layered service architecture with clear separation of concerns:

**Bluetooth Abstraction Layer** (`lib/services/bluetooth/`):
- `BluetoothService`: Abstract interface for BLE operations
- `BluetoothService`: Abstract interface for BLE operations, implements `CompanionTransport`
- `MobileBluetoothService`: Android/iOS implementation using `flutter_blue_plus`
- `WebBluetoothService`: Web implementation using `flutter_web_bluetooth`
- Platform selection happens at runtime in `main.dart` using `kIsWeb`

**Transport Layer** (`lib/services/transport/`):
- `CompanionTransport`: Transport-agnostic interface for MeshCore companion connections (BLE, TCP, USB Serial)
- `StreamFrameCodec`: Framing codec for TCP/USB Serial (`[0x3C][len_lo][len_hi][payload]` out, `[0x3E][len_lo][len_hi][payload]` in)
- `StreamTransportBase`: Abstract base for TCP and USB Serial transports, owns codec and connection lifecycle
- `TcpService`: TCP socket transport with saved connections persistence (Android/iOS)
- `AndroidSerialService`: USB Serial via USB OTG on Android using `usb_serial` package
- `WebSerialService`: USB Serial via Web Serial API (Chrome/Edge) using `dart:js_interop`
- Platform matrix: BLE (all platforms), TCP (Android/iOS), USB Serial (Android/Web)

**MeshCore Protocol Layer** (`lib/services/meshcore/`):
- `MeshCoreConnection`: Implements the 9-step connection workflow and MeshCore companion protocol
- `PacketParser`: Binary packet parsing with BufferReader/Writer utilities
Expand All @@ -81,11 +90,54 @@ The app uses a layered service architecture with clear separation of concerns:
- `AppStateProvider`: Single ChangeNotifier for all app state using Provider pattern
- All UI updates happen via `notifyListeners()` after state mutations

### Map Rebuild Isolation

The MapLibre `MapWidget` is by far the most expensive subtree. It is therefore
**not** subscribed to the whole provider — that previously made it rebuild on
every `notifyListeners()` (including noise-floor/battery/stats every few seconds
and the dense-mesh passive-RX pin storm at 10–20×/sec), which pinned the CPU/GPU
and overheated the device during wardriving.

Instead the map is isolated:
- `AppStateProvider` exposes `mapRevision`, an integer bumped only when
**map-rendered** state changes (TX/RX/disc/trace markers, echoes, zone
repeater load, history view, marker/log clears, marker-style prefs).
- Two helpers drive it: `_notifyMapNow()` (bump + immediate notify, for
low-frequency changes) and `_notifyMapThrottled()` (bump + ~250 ms
leading+trailing coalescing, for the high-frequency RX/echo storm — caps map
rebuilds at ~4/sec while pin data updates immediately).
- `MapWidget` is wrapped in a `Selector` (`home_screen.dart` `_buildMapSelector`)
keyed on `(mapRevision, focus, history, padding, controls)` and uses
`context.read` internally, so it is cached across all UI-only notifies.
- UI-only state (noise floor, battery, live stats) calls plain
`notifyListeners()` and leaves `mapRevision` untouched, so the status bar
updates without rebuilding the map.

**GPS position does NOT bump `mapRevision`.** Position updates ~1–2×/sec while
driving; rebuilding the map that often relayouts the iOS platform view (~24 ms
each) — a dominant heat source. Instead, the GPS listener calls plain
`notifyListeners()`, and `MapWidget` drives camera-follow, derived heading, and
the GPS puck from a **direct provider listener** (`_onPositionNotify` →
`_handleGpsPosition`) that calls the native controller (`animateCamera` /
`updateSymbol`) every tick — real-time nav, no widget rebuild. The GPS-info
overlay rebuilds only when the map itself does.

**The Selector MUST be memoized (identity-stable).** `HomeScreen.build()` uses
`context.watch`, so it rebuilds on every notify (incl. the 2 Hz GPS one).
provider's `Selector` invalidates its cache whenever `oldWidget != widget`
(`selector.dart:77`), so a fresh inline `Selector(...)` instance each build
forces `MapWidget` to rebuild **before** the value comparison ever runs —
silently defeating the isolation. `_buildMapSelector` therefore caches the
`Selector` instance, keyed only on the State fields its closures capture
(`isLandscape` / `_isControlsMinimized` / `_mapControlsExpanded`), so its
identity survives parent rebuilds and the value comparison actually gates the
map.

### 9-Step Connection Workflow

Critical safety: The connection sequence MUST complete in order.

1. **BLE GATT Connect**: Platform-specific BLE connection
1. **Transport Connect**: Platform-specific transport connection (BLE GATT, TCP socket, or USB Serial port)
2. **Protocol Handshake**: `deviceQuery()` with protocol version
3. **Device Info**: `deviceQuery()` returns manufacturer string, then `getSelfInfo()` acquires device public key (required for geo-auth API authentication). If `getSelfInfo()` fails, the entire connection fails.
4. **Device Identification**: Parse manufacturer string, match against `device-models.json` (does NOT modify radio settings)
Expand Down Expand Up @@ -281,7 +333,75 @@ Keeps the screen on during auto-ping to prevent device sleep during wardriving s
- **Platform**: Android and iOS only (Web N/A — always requires active tab)
- **File**: `lib/services/wakelock_service.dart`

## Critical Protocol Details
### Coverage Overlay (vector tiles)

The MeshMapper coverage layer is rendered from the region server's vector tiles
(`vector_tile.php`, z7–14, overzoom beyond) as a MapLibre source+layer pair. The app is
vector-only — every region server must serve `vector_tile.php` (the legacy raster
`tiles.php` overlay was removed from the app 2026-06). Contract reference:
`MeshMapper_Server/docs/VECTOR_TILES.md`.

- **Styling is client-side**: each cell carries an integer status category `st`; colours
come from `match` expressions built by `lib/utils/coverage_tile_palette.dart` (kept in
sync with the server's `dev/cvd_palettes.php`, including all colour-vision palettes).
- **Coverage Grid preference (`prefs.coverageGridSize`)**: Simplified (300 m, default) or
Detailed (100 m + blob), mirroring the web's Grid Mode; baked into the tile URL. The
grid is locked to the chosen preset at every zoom — cells never resize.
- **Post-wardrive live refresh**: on upload success the queue hands the uploaded items to
`AppStateProvider`; +7 s later the server re-renders the affected tiles at z11–14
(`fresh=1`, incl. neighbouring tiles within ~0.005° — blob/border spill lands in the
next tile over), and the user's own cells are decoded from the fresh z14 bodies
(`lib/utils/mvt_cells.dart`) into a session **patch layer**: a GeoJSON source updated
in place above the base layer, with the base layer's copies hidden via `setFilter`.
The base source is never swapped — nothing visibly changes except the changed cells.
A second check runs at +10 s only when the first found no changes. Logged under
`[COVERAGE]`.
- **GOTCHA — never partial-update a fill layer**: `setLayerProperties` serializes with
`skipNulls: false`; any `FillLayerProperties` field left null is RESET to its
style-spec default on iOS/web (`fill-color` → black). Always resend the full colour
expressions with an opacity change (see `_applyCoverageOverlayOpacity`).
- **GOTCHA — feature ids don't survive Android's filter bridge**: the platform converter
parses filter JSON numbers as float32, which rounds the 42-bit cell ids. Filter on the
small-int `i`/`j` properties (as an `"i_j"` string) instead — see
`_applyBasePatchFilter`.
- **Files**: `lib/widgets/map_widget.dart` (`_addCoverageOverlay`, `_applyCoveragePatch`),
`lib/providers/app_state_provider.dart` (`_freshenAffectedVectorTiles`),
`lib/services/api_service.dart` (`freshenVectorTile`),
`lib/utils/coverage_tile_palette.dart`, `lib/utils/mvt_cells.dart`.

### Coverage Connection Lines (tap-to-inspect)

Tapping coverage data draws connection lines from points the tap flow ALREADY
fetches (no extra network calls), matching the web client's exact matching +
fan-out logic (`MeshMapper_Server/dev/index.php`):

- **Tap a coverage tile (Feature A)**: fans out a theme-aware blue dashed line
from the cell centre to every UNIQUE repeater that heard the cell's pings (with
a distance pill per line) and hides the repeaters that didn't. Hooks the
blob-filtered points already computed in `_showCellSummary`. Port of
`updateAllActiveLines`/`updateActiveLinesInternal` via `heardEndpointsForCell`.
- **Tap a repeater (Feature B)**: draws the repeater's matched coverage cells
(status/tile-coloured fills, deduped per grid cell with highest-priority status
winning, red/DROP hidden) plus a status-coloured dashed line from the repeater
to each cell centre. The base coverage tiles DIM and every OTHER repeater is
hidden so the focused repeater's cells/lines pop (web `setSoloCircle` +
tile-dim parity); both restored on close.
Reuses the points fetched in `_showRepeaterDetails`. Port of
`buildChartFromPoints` (`RepeaterStats.fromCoverageWithPoints`) +
`drawRepeaterCoverageFromCache` (`repeaterCoverageCells`).
- **Volume cap**: both cap at the farthest 250 lines/cells (longest reach kept),
logged under `[COVERAGE]` when truncated.
- **Layers** (`map_widget.dart`): `coverage-lines-layer` (shared A/B, per-feature
`color`) and `coverage-cells-layer` (per-feature fill) — install-once empty,
updated via `setGeoJsonSource`, kept separate from the focus-mode lines so the
two features never wipe each other. Imperative draws (no `mapRevision` bump).
Teardown funnels through `_clearCellHighlight` (A) and `_clearRepeaterIsolation`
(B), which also restore the dimmed backdrop and the hidden/all repeaters.
- **Files**: `lib/widgets/map_widget.dart` (`_updateCoverageLines`,
`_updateCoverageCells`, `_drawRepeaterCoverage`, `_syncCoverageDistanceLabels`),
`lib/utils/coverage_summary.dart` (`heardEndpointsForCell`,
`repeaterCoverageCells`, `RepeaterStats.fromCoverageWithPoints`),
`lib/utils/coverage_tile_palette.dart` (`colorsForStatus`).

### BLE Service UUIDs (MeshCore Companion Protocol)
- Service: `6E400001-B5A3-F393-E0A9-E50E24DCCA9E`
Expand Down Expand Up @@ -337,11 +457,39 @@ Key packages used in this project:
- `flutter_blue_plus`: Mobile Bluetooth (Android/iOS)
- `flutter_web_bluetooth`: Web Bluetooth (Chrome/Edge)
- `geolocator`: GPS/Location
- `flutter_map`: Map rendering
- `maplibre_gl`: Map rendering (MapLibre GL vector tiles via OpenFreeMap) — **vendored & patched**, see below
- `hive`: Local storage
- `provider`: State management
- `http`: API requests
- `pointycastle`: Encryption (AES-ECB, SHA-256)
- `usb_serial`: USB Serial communication on Android (USB OTG)

### Vendored `maplibre_gl` (`third_party/maplibre_gl`)

`maplibre_gl` is consumed from an in-repo copy of the pub.dev `0.25.0` release via
`dependency_overrides` in `pubspec.yaml`, **not** from pub. The ONLY delta from upstream is a
native camera-viewport guard.

**Why:** MapLibre's transform unprojects against the live viewport. When the GL surface is
degenerate/zero-sized (e.g. a launch where tiles never finish loading, so the surface never renders
a real frame), the very first animated `flyTo`/`setCamera` makes `unproject` produce NaN, and
`mbgl::LatLng`'s constructor throws an **uncaught C++ `std::domain_error` → SIGABRT**. That throw
crosses the Obj-C++→Swift/JNI boundary and **cannot be caught from Dart**, so the only place it can
be reliably stopped is inside the plugin's camera handlers.

**The patch:** the `camera#animate` / `camera#move` / `camera#ease` cases in
`MapLibreMapController.swift` (iOS) and `MapLibreMapController.java` (Android) bail (completing the
method-channel result so the Dart `await` returns) when the map view has no usable size
(`bounds.width/height < 1` / `getWidth()/getHeight() < 1`). Search the patch with the tag
`MESHMAPPER GUARD`.

The Dart side (`map_widget.dart`) is defense-in-depth: `_mapHasRenderedOnce` (set on the first
`onMapIdle`) is folded into `_canAnimateCamera`, so no programmatic camera move is even attempted
until the map has rendered once; the one-shot initial GPS zoom re-attempts on later ticks instead of
burning. See the `_canAnimateCamera` getter and `_onMapIdle`.

**On upgrade:** re-apply the `MESHMAPPER GUARD` blocks to the new plugin version (or drop the
override if upstream gains an equivalent guard).

## Development Workflow Requirements

Expand Down Expand Up @@ -506,6 +654,12 @@ All API endpoints may return maintenance mode:
- `lib/services/meshcore/tx_tracker.dart` - Repeater echo detection (7s window)
- `lib/services/meshcore/disc_tracker.dart` - Discovery response tracking (7s window)
- `lib/services/meshcore/rx_logger.dart` - Passive observation logging
- `lib/services/transport/companion_transport.dart` - Transport-agnostic interface for companion connections
- `lib/services/transport/stream_frame_codec.dart` - TCP/USB Serial framing codec
- `lib/services/transport/stream_transport_base.dart` - Shared base for TCP/USB Serial transports
- `lib/services/transport/tcp_service.dart` - TCP socket transport with saved connections
- `lib/services/transport/android_serial_service.dart` - USB Serial transport for Android (USB OTG)
- `lib/services/transport/web_serial_service.dart` - USB Serial transport for Web (Web Serial API)
- `lib/services/ping_service.dart` - TX/RX/Discovery ping orchestration
- `lib/services/gps_service.dart` - GPS tracking and geofencing
- `lib/services/api_queue_service.dart` - Persistent upload queue
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,5 @@ This project is licensed under the MIT License — see [LICENSE](LICENSE) for de

- Original [MeshMapper WebClient](https://github.com/MeshMapper/MeshMapper_WebClient)
- [MeshCore](https://github.com/meshcore-dev/MeshCore) firmware project
- [meshcore-open](https://github.com/zjs81/meshcore-open) by zjs81 — Android USB Serial support is based heavily on its native USB host implementation (MIT License, see [THIRD_PARTY_LICENSES](THIRD_PARTY_LICENSES))
- [The Greater Ottawa Mesh Radio Enthusiasts community](https://ottawamesh.ca/)
29 changes: 29 additions & 0 deletions THIRD_PARTY_LICENSES
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
================================================================================
meshcore-open
https://github.com/zjs81/meshcore-open
================================================================================

The Android USB Serial implementation (MeshMapperUsbService.kt) is based on
the USB host code from meshcore-open.

MIT License

Copyright (c) 2025 zjs81

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
4 changes: 4 additions & 0 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,7 @@ linter:
analyzer:
errors:
invalid_annotation_target: ignore
exclude:
# Vendored, patched copy of maplibre_gl (see pubspec.yaml dependency_overrides).
# Analyze it under its own options, not ours.
- third_party/**
9 changes: 8 additions & 1 deletion android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ android {
applicationId = "net.meshmapper.app"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
minSdk = flutter.minSdkVersion // MapLibre GL requires 23+
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
Expand All @@ -60,6 +60,7 @@ android {
buildTypes {
release {
signingConfig = signingConfigs.getByName("release")
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
}
Expand All @@ -71,4 +72,10 @@ flutter {
dependencies {
// Required for flutter_local_notifications core library desugaring
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")

// MapLibre Android SDK — already pulled transitively by maplibre_gl, but
// declaring it explicitly gives MainActivity.kt compile-time access to
// OfflineManager for the tile cache MethodChannel handlers. Version must
// match maplibre_gl-0.25.0's transitive dep.
implementation("org.maplibre.gl:android-sdk:12.3.1")
}
7 changes: 7 additions & 0 deletions android/app/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# flutter_local_notifications uses Gson to serialize/deserialize scheduled
# notification data. R8 strips the generic signature from TypeToken
# subclasses, causing "TypeToken must be created with a type argument" at
# runtime when cancel() tries to load the notification cache.
-keep class com.google.gson.reflect.TypeToken { *; }
-keep class * extends com.google.gson.reflect.TypeToken
-keepattributes Signature
6 changes: 5 additions & 1 deletion android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
<!-- BLE hardware feature -->
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />

<!-- USB host for USB OTG serial connections -->
<uses-feature android:name="android.hardware.usb.host" android:required="false" />

<!-- Internet for API uploads -->
<uses-permission android:name="android.permission.INTERNET" />

Expand All @@ -24,6 +27,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<application
Expand Down Expand Up @@ -61,7 +65,7 @@
<!-- Background service for continuous wardriving -->
<service
android:name="id.flutter.flutter_background_service.BackgroundService"
android:foregroundServiceType="location|connectedDevice"
android:foregroundServiceType="location|connectedDevice|dataSync"
android:exported="true"
tools:replace="android:exported,android:foregroundServiceType" />
</application>
Expand Down
Loading
Loading