- Updated supported languages to include Hungarian, Japanese, and Korean.
- Added new on-device message translation feature with detailed usage instructions.
- Introduced emoji reactions in chat with user interface and functionality details.
- Implemented linkification for automatic detection of URLs and meshcore URIs in messages.
- Added GPX export functionality for contacts with GPS coordinates.
- Enabled pinch-to-zoom for chat text scaling.
- Documented background service for Android to maintain BLE connection in the background.
- Revised BLE protocol documentation to reflect changes in connection state machine and command codes.
- Updated channels documentation to clarify message display and interaction options.
- Enhanced chat and messaging documentation with new translation button and message metadata.
- Clarified contact actions in contacts documentation.
- Adjusted map and location documentation for improved node name visibility and filter options.
- Revised navigation documentation to streamline disconnection process.
- Improved notification documentation to specify batch notification behavior.
- Updated repeater management documentation to reflect new features and settings.
- Enhanced scanner and connection documentation for device filtering and connection timeout.
- Expanded settings documentation to include new translation options.
- Removed jni plugin references from generated plugin files for Linux and Windows.
Open-source Flutter client for MeshCore LoRa mesh networking devices.
Open-source Flutter client for MeshCore LoRa mesh networking devices. Connects to MeshCore-compatible radios over **BLE, TCP, or USB serial** and provides direct/channel chat, contact and channel management, on-map node tracking, repeater administration, and on-device message translation.
| `UiViewStateService` | Contacts/channels sort/filter/search state |
| `TimeoutPredictionService` | ML linear regression for ACK timeout prediction |
| `StorageService` | Path history + delivery observation persistence |
| `MapTileCacheService` | OSM tile pre-cache |
Screens consume these via `Consumer<T>` (or `context.watch<T>()` / `context.read<T>()`) for reactive UI.
### Storage / Persistence
All stores in `lib/storage/` use `PrefsManager` (a `SharedPreferences` singleton initialized in `main()`). Most stores **scope keys by the first 10 hex chars of the connected device's public key**, so per-radio data is isolated.
| Store | Persists |
|---|---|
| `message_store`, `channel_message_store` | Direct + channel messages |
| `contact_store`, `contact_discovery_store` | Known + discovered contacts |
- `lib/theme/mesh_theme.dart` defines a warm-dark `MeshPalette` (phosphor-green accents) but is **not currently wired** in `main.dart` — available for a future redesign
## BLE Protocol
### Localization
### Nordic UART Service (NUS)
18 locales supported via Flutter's standard ARB pipeline (`lib/l10n/`): en, de, es, fr, it, pt, ru, uk, bg, hu, ja, ko, nl, pl, sk, sl, sv, zh. Language override comes from `AppSettingsService.settings.languageOverride`. Use the `context.l10n` extension (`lib/l10n/l10n.dart`) for translated strings; contact-type names live in `contact_localization.dart`.
- **RX Characteristic**: `6e400002-b5a3-f393-e0a9-e50e24dcca9e` (Write to device)
- **TX Characteristic**: `6e400003-b5a3-f393-e0a9-e50e24dcca9e` (Notify from device)
### Device Discovery
## Transports
- Scans for devices with known name prefixes
- Filters by `platformName` or `advertisementData.advName`
### Connection States
`MeshCoreConnector` unifies all three transports under one `ChangeNotifier`. There is **no shared base class** — selection is via the `MeshCoreTransportType { bluetooth, usb, tcp }` enum, and BLE/TCP/USB share the same connection-state enum, send/receive API, and frame protocol.
- **RX Characteristic** (write to device): `6e400002-b5a3-f393-e0a9-e50e24dcca9e`
- **TX Characteristic** (notify from device): `6e400003-b5a3-f393-e0a9-e50e24dcca9e`
- **Discovery**: scans for devices whose name starts with `MeshCore-`, `Whisper-`, `WisCore-`, `Seeed`, `Lilygo`, `HT-`, or `LowMesh_MC_` (filters on both `platformName` and `advertisementData.advName`)
- **Linux**: `linux_ble_pairing_service.dart` falls back to `bluetoothctl` when BlueZ agent prompts fail
### TCP
- Manual host/port entry, persisted via `AppSettingsService` (`tcpServerAddress`, `tcpServerPort`)
- `NSCameraUsageDescription` (QR scanning to join communities)
- Background modes: `bluetooth-central`
- `LSApplicationQueriesSchemes`: `http`, `https`
### Web (`web/`)
PWA scaffold present but boilerplate (`manifest.json` and `index.html` are unmodified Flutter defaults). BLE is unsupported in browsers; TCP and Web Serial USB may work in Chrome only. `ChromeRequiredScreen` gates non-Chrome web users. Versioned releases are produced via `build_pipe` (`?v=<pubspecversion>` cache busting, no service worker).
### Desktop
`linux/`, `windows/`, and `macos/` directories are present as Flutter scaffolds. No app-specific native config has been added; BLE on desktop has not been validated.
- All UI strings go through Flutter's ARB localization system
- All UI strings go through Flutter's ARB localization system
@ -185,3 +185,77 @@ An ML-based service that predicts expected delivery timeouts:
- Blends per-contact statistics with ML predictions
- Blends per-contact statistics with ML predictions
- Falls back to `3000 + 3000 × pathLength` ms when insufficient data
- Falls back to `3000 + 3000 × pathLength` ms when insufficient data
- Observations are persisted to storage via a 2-second debounced timer (observations within 2s of app termination may be lost)
- Observations are persisted to storage via a 2-second debounced timer (observations within 2s of app termination may be lost)
---
## On-Device Message Translation
### What It Is
An optional on-device translation service powered by an embedded LLM (llamadart, running GGUF models). Translation runs entirely on-device — no data leaves the app.
### How to Access
Tap the translate button on any received message. On first use, the GGUF model file is downloaded and cached locally.
### How It Works
- Model files are managed by `TranslationFileStore`; download progress is shown in-place
- Translation runs via `TranslationService` using the llamadart CPU backend (arm64 and x64 on Android)
- Translated text is shown in `TranslatedMessageContent` as an inline overlay on the original message bubble
- Each translation is cached; re-tapping shows the cached result without re-running inference
---
## Emoji Reactions
### How to Access
Long-press a message bubble in any direct or channel chat, then select a reaction emoji.
### What the User Sees
An emoji picker inline with common reactions. Selected reactions appear below the message bubble with a count.
### How It Works
- Implemented via `emoji_picker.dart` and `reaction_helper.dart`
- Reactions are transmitted as a special message type visible to all participants with MeshCore Open
---
## Linkification
### What It Does
URLs and `meshcore://` URIs in received messages are automatically detected and rendered as tappable links.
### How It Works
- Powered by the `flutter_linkify` package via `link_handler.dart`
- Tapping a URL opens the system browser; tapping a `meshcore://` URI imports the contact
Exports contacts with GPS coordinates to a `.gpx` file via the OS share sheet. Not available on web.
---
## Pinch-to-Zoom Chat Text
### What It Does
Users can pinch to scale all chat text up or down within a session.
### How It Works
- Implemented via `ChatTextScaleService` and `ChatZoomWrapper`
- Scale range: 0.8× to 1.8×
- The chosen scale persists across the session via the service
---
## Background Service (Android)
### What It Does
On Android, a foreground service (`background_service.dart`) keeps the BLE connection and message handling alive when the app is in the background. On other platforms this is a no-op.
### User Impact
- A persistent notification appears while the service is running
- Messages are received and retry logic continues even when the app is not in the foreground
- **Translation button** (optional, between GIF and text field): Shown only when translation is enabled in App Settings. Tap to configure outgoing-message translation language and on/off toggle.
@ -66,8 +67,8 @@ Outgoing messages display a status indicator:
When enabled in App Settings, additional metadata appears inside each bubble:
When enabled in App Settings, additional metadata appears inside each bubble:
- Timestamp (HH:MM)
- Timestamp (HH:MM)
- Retry count (e.g., "Retry 2 of 4")
- Retry count (e.g., "Retry 2 of 4") — only shown for outgoing messages where at least one retry has occurred
- Status icon
- Status icon (outgoing only)
- Round-trip time in seconds (if delivered)
- Round-trip time in seconds (if delivered)
## Message Length Limits
## Message Length Limits
@ -86,7 +87,7 @@ When a direct message is sent:
1. The app computes an expected ACK hash: `SHA256([timestamp][attempt][text][selfPubKey])[0:4]` — matching the firmware's hash calculation. If SMAZ compression is enabled, the compressed text (not the original) is hashed
1. The app computes an expected ACK hash: `SHA256([timestamp][attempt][text][selfPubKey])[0:4]` — matching the firmware's hash calculation. If SMAZ compression is enabled, the compressed text (not the original) is hashed
2. On device acknowledgment (`RESP_CODE_SENT`), the message transitions to "sent" and a timeout timer starts
2. On device acknowledgment (`RESP_CODE_SENT`), the message transitions to "sent" and a timeout timer starts
3. **Timeout duration**: Preferably from the ML timeout prediction service; otherwise `3000 + 3000 × path_length` milliseconds (15000ms for flood)
3. **Timeout duration**: Preferably from the ML timeout prediction service; otherwise calculated from LoRa airtime physics: `500 + (airtime × 6 + 250) × (pathLength + 1)` ms for direct paths, `500 + 16 × airtime` ms for flood (airtime is estimated from the radio's current spreading factor, bandwidth, and coding rate)
4. On timeout, the message is retried with **exponential backoff**: `1000 × 2^retryCount` ms (1s, 2s, 4s, 8s, 16s...)
4. On timeout, the message is retried with **exponential backoff**: `1000 × 2^retryCount` ms (1s, 2s, 4s, 8s, 16s...)
5. **Max retries**: Configurable (default 5, range 2–10)
5. **Max retries**: Configurable (default 5, range 2–10)
6. After max retries, the message is marked "failed" — but a **30-second grace window** remains during which a late ACK can still resolve the message to "delivered"
6. After max retries, the message is marked "failed" — but a **30-second grace window** remains during which a late ACK can still resolve the message to "delivered"
@ -115,6 +116,7 @@ Add emoji reactions to incoming messages (not your own):
| Path Trace | Rooms (always); Chat/Sensor if `pathLength > 0` | Opens PathTraceMapScreen. For rooms, label shows "Ping" when no path bytes are known, "Path Trace" when path bytes are available |
@ -22,31 +22,16 @@ The QuickSwitchBar is a Material 3 `NavigationBar` with a frosted-glass visual t
Tapping a tab replaces the current screen with a subtle fade + slight horizontal nudge transition (220ms forward, 200ms reverse). The back button is suppressed on all three main screens — navigation between them is flat, not stacked. All icons use outline variants (`people_outline`, `tag`, `map_outlined`) following Material 3 conventions.
Tapping a tab replaces the current screen with a subtle fade + slight horizontal nudge transition (220ms forward, 200ms reverse). The back button is suppressed on all three main screens — navigation between them is flat, not stacked. All icons use outline variants (`people_outline`, `tag`, `map_outlined`) following Material 3 conventions.
## Device Screen
## Disconnection
The Device Screen is a transitional hub that shows after connection. In practice, the app navigates directly to Contacts after connecting, but the Device Screen is reachable via the QuickSwitchBar.
- The disconnect button (available in the Settings screen and other main screens) shows a confirmation dialog before disconnecting
### What the User Sees
**App Bar**:
- Left: Battery indicator chip (tappable — toggles between percentage and voltage display). Icon changes based on level: `battery_unknown` when data unavailable, `battery_alert` (orange) at 15% or below, `battery_full` otherwise
- Left-aligned title (`centerTitle: false`): Two-line layout — small grey "MeshCore" label above the device name in bold
- **Quick Switch** section: The QuickSwitchBar widget for navigating to Contacts/Channels/Map
### Disconnection
- The disconnect button shows a confirmation dialog before disconnecting
- If the device disconnects unexpectedly, the app automatically navigates back to the Scanner screen (fires after the current frame completes via a post-frame callback)
- If the device disconnects unexpectedly, the app automatically navigates back to the Scanner screen (fires after the current frame completes via a post-frame callback)
- This auto-navigation behavior (`DisconnectNavigationMixin`) is shared across all main screens
- This auto-navigation behavior (`DisconnectNavigationMixin`) is shared across all main screens
## Theme and Locale
## Theme and Locale
- **Theme mode** is user-configurable in App Settings (System / Light / Dark) — not locked to system
- **Theme mode** is user-configurable in App Settings (System / Light / Dark) — not locked to system
- **Language** can be overridden to one of 15 supported languages, or follow the system locale
- **Language** can be overridden to one of 18 supported languages, or follow the system locale
- On web, if a non-Chromium browser is detected, the app shows a `ChromeRequiredScreen` instead of the Scanner (Web Bluetooth requires Chromium)
- On web, if a non-Chromium browser is detected, the app shows a `ChromeRequiredScreen` instead of the Scanner (Web Bluetooth requires Chromium)
The notification system prevents notification storms:
The notification system prevents notification storms:
- **Minimum interval**: 3 seconds between individual notifications
- **Minimum interval**: 3 seconds between individual notifications
- **Batch window**: If multiple notifications arrive within 5 seconds, they are combined into a single summary notification on a fourth Android channel (`batch_summary`): "MeshCore Activity — 2 messages, 1 channel message, 3 new nodes". Note: batch summaries are Android-only; on Apple platforms individual notifications are shown
- **Batch window**: If multiple notifications arrive within 5 seconds, they are combined into a single summary notification on a fourth Android channel (`batch_summary`). The title is "MeshCore Activity" and the body lists the grouped counts (e.g., "2 messages, 1 channel message, 3 new nodes"). Batch summaries are Android-only; queued notifications that overflow the batch window are silently dropped on other platforms
- "Save password" checkbox (persists for future logins). If a saved password exists, it is pre-filled and the checkbox is pre-checked, making login one-tap
- "Save password" checkbox (persists for future logins). If a saved password exists, it is pre-filled and the checkbox is pre-checked, making login one-tap
- Routing mode selector and "Manage Paths" link are available directly in the dialog (configure routing before login)
- Routing mode selector and "Manage Paths" link are available directly in the dialog (configure routing before login)
- Auto-retries up to 5 times on timeout, showing progress ("Attempt 2 of 5"). A wrong password stops immediately after the first attempt — only timeouts trigger retries
- Auto-retries up to 5 times on timeout, showing progress ("Attempt 2 of 5"). A wrong password (explicit failure response) stops immediately — only timeouts trigger retries
- After 5 failed attempts, further login attempts are blocked
- If auto-clock-sync is enabled for this repeater (configured in Repeater Settings), a `clock sync` command is sent automatically on successful login
---
---
@ -28,15 +28,17 @@ The central management screen showing:
- **Header card**: Repeater name, short public key, path label, GPS coordinates (if known)
- **Header card**: Repeater name, short public key, path label, GPS coordinates (if known)
- **Management tool cards** (full-width cards with chevron arrows, not a grid). Title dynamically shows "Repeater Management" or "Room Management" based on contact type:
- **Management tool cards** (full-width cards with chevron arrows, not a grid). Title dynamically shows "Repeater Management" or "Room Management" (admin) or "Repeater Guest" / "Room Guest" (guest) based on contact type and login result:
| Card | Destination |
| Card | Destination | Visibility |
|---|---|
|---|---|---|
| Status | Repeater Status Screen |
| Status | Repeater Status Screen | All users |
| Telemetry | Telemetry Screen |
| Telemetry | Telemetry Screen | All users |
| CLI | Repeater CLI Screen |
| CLI | Repeater CLI Screen | Admin only |
| Neighbors | Neighbors Screen |
| Neighbors | Neighbors Screen | All users |
| Settings | Repeater Settings Screen |
| Settings | Repeater Settings Screen | Admin only |
The battery chemistry selector and CLI/Settings cards are hidden from guest users.
---
---
@ -47,26 +49,28 @@ The central management screen showing:
Three information cards:
Three information cards:
**System Information**:
**System Information**:
- Battery percentage
- Battery percentage and voltage (e.g. "85% / 3.95V"), using the battery chemistry set in the hub screen
- Uptime
- Queue length
- Error flags
- Clock at login time
- Clock at login time
- Uptime (days/hours/minutes/seconds)
- Queue length
- Debug flags (error event count)
**Radio Statistics**:
**Radio Statistics**:
- Last RSSI and SNR
- Last RSSI and SNR
- Noise floor
- Noise floor
- TX and RX airtime
- TX airtime and RX airtime
**Packet Statistics**:
**Packet Statistics**:
- Packets sent, received, and duplicates
- Packets sent and received, each broken down by flood vs. direct
- Broken down by flood vs. direct
- Duplicates, broken down by flood vs. direct
- Channel utilization (% of uptime used by TX + RX)
### Key Interactions
### Key Interactions
- Auto-queries the repeater on open; shows a loading spinner until data arrives
- Auto-queries the repeater on open; shows a loading spinner until data arrives
- On timeout: red snackbar error. On success: data appears with a green snackbar confirmation
- On timeout: red snackbar error. On success: data appears in-place (no extra snackbar)
- Pull-to-refresh or refresh button to re-query
- Pull-to-refresh or refresh button in the app bar to re-query
- Routing mode popup and path management dialog in app bar (these controls appear on **all** management sub-screens, not just Status)
- Routing mode popup and path management dialog in app bar (these controls appear on **all** management sub-screens, not just Status)
- Accepts both binary `RESP_CODE_STATUS_RESPONSE` frames and legacy JSON text responses
---
---
@ -76,7 +80,7 @@ A terminal-style interface for sending commands directly to the repeater.
### What the User Sees
### What the User Sees
- **Quick-command bar** (horizontal scroll): Shortcut buttons for common commands (get name, get radio, get tx, neighbors, ver, advert, clock)
- **Quick-command bar** (horizontal scroll): Shortcut buttons for 9 common commands (advert, get name, get radio, get tx, discover.neighbors, neighbors, ver, clock, clock sync)
- **Command history list**: Sent commands in primary color, responses in secondary color
- **Command history list**: Sent commands in primary color, responses in secondary color
- **Input bar**: Up/down history arrows, monospace text field with `> ` prefix, send button
- **Input bar**: Up/down history arrows, monospace text field with `> ` prefix, send button
@ -92,9 +96,13 @@ A terminal-style interface for sending commands directly to the repeater.
- AGC reset interval slider (0–240s in multiples of 4; `set agc.reset.interval`)
**6. Danger Zone** (red-styled card)
**Danger Zone** (red-styled card)
- Reboot repeater
- Reboot repeater (sends `reboot` with confirmation dialog)
- Erase filesystem (serial-only warning)
- Erase filesystem (serial-only; shows informational snackbar only — no command is sent over the air)
### Key Interactions
### Key Interactions
- **Settings are NOT auto-fetched on open**. Only name and location are pre-filled from locally cached contact data. You must tap each section's refresh button to fetch live values from the repeater
- **Settings are NOT auto-fetched on open**. Name is pre-filled from cached contact data. Each section has its own refresh button to fetch live values from the repeater
- TX Power has its own separate refresh button, independent from the main Radio Settings refresh
- TX Power, RX Gain, latitude, longitude, and advanced fields each have independent inline refresh buttons
- Save button appears when changes are detected
- Save button in app bar appears when any change is detected; failed commands keep those fields dirty for retry
- Settings are sent sequentially with 200ms delays between commands (fire-and-forget, no per-command acknowledgment wait)
- Settings are sent sequentially with 200ms delays between commands; firmware responses are checked and partial failures are reported in a snackbar
- Some changes (e.g. radio frequency) require a reboot; the firmware response triggers an orange "reboot needed" snackbar
- Advertisement interval sliders reset to defaults when re-enabled (local: 60 min, flood: 3 hours)
- Advertisement interval sliders reset to defaults when re-enabled (local: 60 min, flood: 3 hours)
- **Erase Filesystem** does NOT send any command over the air — tapping it only shows a snackbar explaining the operation requires physical serial access. It is effectively non-functional when connected wirelessly
- **Erase Filesystem** does NOT send any command over the air — tapping it only shows a snackbar explaining the operation requires physical serial access
@ -46,7 +46,7 @@ A dedicated sub-screen for app-level preferences (nothing here is sent to the de
### Appearance
### Appearance
- **Theme**: System / Light / Dark
- **Theme**: System / Light / Dark
- **Language**: System default or one of 15 languages (English, French, Spanish, German, Polish, Slovenian, Portuguese, Italian, Chinese, Swedish, Dutch, Slovak, Bulgarian, Russian, Ukrainian)
- **Language**: System default or one of 18 languages (English, French, Spanish, German, Polish, Slovenian, Portuguese, Italian, Chinese, Swedish, Dutch, Slovak, Bulgarian, Russian, Ukrainian, Hungarian, Japanese, Korean)
- **Enable Message Tracing**: Shows path trace overlays and extra metadata on messages
- **Enable Message Tracing**: Shows path trace overlays and extra metadata on messages
### Notifications
### Notifications
@ -57,6 +57,7 @@ A dedicated sub-screen for app-level preferences (nothing here is sent to the de
### Messaging
### Messaging
- **Clear Path on Max Retry**: Erases the stored routing path after all retries fail
- **Clear Path on Max Retry**: Erases the stored routing path after all retries fail
- **Jump to Oldest Unread**: When opening a chat, scrolls to the oldest unread message instead of the newest
- **Auto Route Rotation**: Enables weighted routing algorithm. When enabled, expands to show five slider sub-settings (hidden when off):
- **Auto Route Rotation**: Enables weighted routing algorithm. When enabled, expands to show five slider sub-settings (hidden when off):
- Max Route Weight (1–10, default 5, integer steps)
- Max Route Weight (1–10, default 5, integer steps)
- Initial Route Weight (0.5–5.0, default 3.0)
- Initial Route Weight (0.5–5.0, default 3.0)
@ -67,6 +68,15 @@ A dedicated sub-screen for app-level preferences (nothing here is sent to the de
### Battery
### Battery
- **Battery Chemistry**: NMC / LiFePO4 / LiPo (per device, used to calibrate percentage from voltage)
- **Battery Chemistry**: NMC / LiFePO4 / LiPo (per device, used to calibrate percentage from voltage)
### Translation
Not shown on web. Controls on-device message translation powered by a locally-downloaded ML model:
- **Enable Translation**: Translates incoming messages into the selected target language
- **Translate Composer**: Translates outgoing messages from the target language back before sending
- **Target Language**: Language to translate into (searchable list; defaults to the app language)
- **Downloaded Model**: Dropdown to select among already-downloaded translation models
- **Preset Model**: Download a curated preset model with one tap
- **Custom Model URL**: Enter a URL to download a custom GGUF-format model; shows download progress and a cancel button
### Map Display
### Map Display
- **Show Repeaters**: Toggle repeater markers on map
- **Show Repeaters**: Toggle repeater markers on map
- **Show Chat Nodes**: Toggle chat node markers
- **Show Chat Nodes**: Toggle chat node markers
@ -91,7 +101,7 @@ These settings are sent directly to the connected device firmware.
### Radio Settings
### Radio Settings
Opens a dialog pre-populated with the device's current radio settings. Contains:
Opens a dialog pre-populated with the device's current radio settings. Contains:
- **Preset dropdown**: 19 regional presets — selecting a preset immediately fills all fields below. Full list: Australia, Australia (Narrow), Australia SA/WA/QLD, Czech Republic, EU 433MHz, EU/UK (Long Range), EU/UK (Medium Range), EU/UK (Narrow), New Zealand, New Zealand (Narrow), Portugal 433, Portugal 869, Switzerland, USA Arizona, USA/Canada, Vietnam, Off-Grid 433, Off-Grid 869, Off-Grid 918
- **Preset dropdown**: 19 regional presets — selecting a preset immediately fills all fields below. Full list: Australia, Australia (Narrow), Australia SA, WA, QLD, Czech Republic, EU 433MHz, EU/UK (Long Range), EU/UK (Medium Range), EU/UK (Narrow), New Zealand, New Zealand (Narrow), Portugal 433, Portugal 869, Switzerland, USA Arizona, USA/Canada, Vietnam, Off-Grid 433, Off-Grid 869, Off-Grid 918