MeshCore PT (lusoapp) is a Flutter companion app for MeshCore radios, built by and for the Portuguese amateur radio and mesh networking community.
- Channel Messaging — Send and receive messages on MeshCore channels
- Private Chat — End-to-end encrypted 1:1 messaging via Ed25519/X25519
- Radio Configuration — Full LoRa parameter control (frequency, bandwidth, SF, CR, TX power)
- Contact Management — View and manage discovered mesh nodes (chat, repeaters, rooms, sensors)
- BLE Connection — Connect to ESP32 and nRF52 MeshCore radios via Bluetooth Low Energy
- Serial Connection — Connect via USB OTG serial (115200 8N1)
- EU868 Presets — Quick radio presets compliant with Portuguese/EU regulations
- Portuguese UI — Full Portuguese (Portugal) interface
- Map View — Live GPS map of all contacts and mesh nodes (OpenStreetMap, no API key required)
- Offline Map — Browse-cached tiles via
flutter_map_tile_caching; tiles are stored automatically as you pan/zoom and served from disk when offline
Note on offline map caching: Tile caching is purely browse-based — tiles are saved as you explore the map while online and replayed when offline. There is no "download this area" bulk pre-download feature. This is intentional: OSM's tile server policy forbids bulk pre-downloading of regions.
- Flutter SDK >= 3.29.0
- Android Studio or VS Code with Flutter extensions
- A MeshCore radio (ESP32 or nRF52 based)
# Clone the repository
git clone <repo-url> lusoapp
cd lusoapp
# Get dependencies
flutter pub get
# Run on connected device
flutter run
# Build release APK
flutter build apk --release
# MacinCloud iOS release build (with icon generation)
./scripts/build.macincloud.sh --release| Platform | Transport | Status |
|---|---|---|
| Android | BLE + Serial | Primary target |
| iOS | BLE | Planned |
| Windows | Serial | Planned |
| Linux | Serial | Planned |
lib/
├── main.dart # App entry point
├── protocol/ # MeshCore protocol implementation
│ ├── kiss.dart # KISS TNC framing
│ ├── commands.dart # Command/response constants
│ ├── models.dart # Data models (Contact, Message, RadioConfig)
│ ├── companion_encoder.dart # App→Radio frame encoder
│ ├── companion_decoder.dart # Radio→App frame decoder
│ └── companion_responses.dart # Response/push DTO classes
├── transport/ # Communication layer
│ ├── radio_transport.dart # Abstract transport interface
│ ├── ble_transport.dart # BLE (Nordic UART) transport
│ └── serial_transport.dart # USB Serial transport
├── services/
│ └── radio_service.dart # High-level radio communication coordinator
├── providers/
│ ├── radio_providers.dart # Riverpod state management (entry point)
│ └── parts/ # Notifiers split by domain
│ ├── connection_notifier.dart
│ ├── messages_notifier.dart
│ └── advert_auto_add.dart
└── ui/
├── theme.dart # Material 3 dark/light theme
├── router.dart # GoRouter navigation
├── screens/ # Core screens (chat, contacts, settings…)
│ └── parts/ # Per-screen widget parts
└── apps/ # Self-contained "apps" launched from the Apps tab
├── plan333/
├── telemetry/
├── topology/
├── rx_log/
└── noise_floor/
Each entry on the Apps tab lives in its own folder under lib/ui/apps/<name>/
so it can grow without bloating shared screens. To add one:
- Create the folder and screen —
lib/ui/apps/<name>/<name>_screen.dart. Expose a public widget (e.g.class MyAppScreen extends ConsumerWidget). Optional sub-widgets go inlib/ui/apps/<name>/parts/as Dartpartfiles (part of '../<name>_screen.dart';). - Register the route in
lib/ui/router.dart:import 'apps/<name>/<name>_screen.dart'; // … GoRoute(path: '/apps/<name>', builder: (_, _) => const MyAppScreen()),
- Add the launcher tile in
lib/ui/screens/apps_screen.dartby appending a new_AppEntryto the_appslist withroute: '/apps/<name>'. - (Optional) localise any user-visible strings via the ARB files in
lib/l10n/— see the Translations section.
That's it: the app appears on the Apps grid, is reachable via deep link, and keeps its widgets, helpers, and tests isolated from the rest of the codebase.
The app implements the MeshCore Companion Radio Protocol v3:
- All App→Radio commands (APP_START, SEND_MSG, SEND_CHAN_MSG, GET_CONTACTS, SET_RADIO_PARAMS, etc.)
- All Radio→App responses (SELF_INFO, CONTACT, CHANNEL_MSG_RECV_V3, etc.)
- Unsolicited push notifications (ADVERT, MSG_WAITING, SEND_CONFIRMED, etc.)
- BLE: Nordic UART Service (
6E400001-B5A3-F393-E0A9-E50E24DCCA9E) - Serial: 115200 baud, 8N1, DTR+RTS
Default LoRa parameters comply with EU868 SRD regulations:
- Frequency: 869.618 MHz
- Bandwidth: 62.5 kHz
- TX Power: 14 dBm ERP max
- Duty Cycle: Users must respect 10% limits
Users are responsible for compliance with ANACOM regulations and amateur radio licence conditions.
Contributions are welcome! Please see the ROADMAP.md for planned features and the project direction.
For build-time app enable/disable presets and per-feature overrides, see:
- docs/feature-toggles.md (English)
- docs/feature-toggles.pt-PT.md (Portuguese - Portugal)
The app uses Flutter's built-in flutter_localizations / intl ARB pipeline.
lib/l10n/
├── app_pt.arb ← Template (source of truth, Portuguese PT)
├── app_en.arb ← English translation
├── app_es.arb ← Spanish translation
├── app_localizations.dart ← Generated — do NOT edit by hand
├── app_localizations_pt.dart ← Generated
├── app_localizations_en.dart ← Generated
├── app_localizations_es.dart ← Generated
└── l10n.dart ← BuildContext extension: context.l10n.<key>
Configuration lives in l10n.yaml (project root):
arb-dir: lib/l10n
template-arb-file: app_pt.arb # PT is the template locale
output-localization-file: app_localizations.dart
output-dir: lib/l10n
nullable-getter: false-
Edit
lib/l10n/app_pt.arb— add the new key and its Portuguese value.
Keys follow the naming convention<screenOrGroup><PascalCaseName>, e.g.commonSave,navChannels,settingsTitle."myNewKey": "Texto em português", "@myNewKey": {}
-
Repeat for every other ARB file (
app_en.arb,app_es.arb, …) adding the translated text for each locale. -
Regenerate the Dart classes:
flutter gen-l10n
This regenerates
app_localizations.dartand allapp_localizations_<locale>.dartfiles.
Never edit those files manually — they are overwritten on every run. -
Use the string in code:
import '../../l10n/l10n.dart'; // Inside a Widget build method: Text(context.l10n.myNewKey)
- Create
lib/l10n/app_<locale>.arb— copyapp_en.arbas a starting point and translate all values. - Run
flutter gen-l10n— the new locale is picked up automatically. - No changes to
pubspec.yamlormain.dartare needed;AppLocalizations.supportedLocalesis generated from the ARB files.
Flutter ARB supports ICU message syntax. Example with a parameter:
"unreadCount": "{count} mensagens não lidas",
"@unreadCount": {
"placeholders": {
"count": { "type": "int" }
}
}Usage: context.l10n.unreadCount(42)
# Regenerate translations (run from project root)
flutter gen-l10nMIT License — see LICENSE for details.