From 22a53439b1b7b0a103795165ec5636159b2e86c5 Mon Sep 17 00:00:00 2001
From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com>
Date: Sun, 1 Mar 2026 23:08:51 -0500
Subject: [PATCH] Initialize USB Supoport for Andriod and Desktop
---
android/app/build.gradle.kts | 3 +-
android/app/src/main/AndroidManifest.xml | 1 +
.../meshcore/meshcore_open/MainActivity.kt | 310 +++++++++++-
android/build.gradle.kts | 1 +
lib/connector/meshcore_connector.dart | 151 +++++-
lib/l10n/app_bg.arb | 11 +-
lib/l10n/app_de.arb | 11 +-
lib/l10n/app_en.arb | 9 +
lib/l10n/app_es.arb | 11 +-
lib/l10n/app_fr.arb | 11 +-
lib/l10n/app_it.arb | 11 +-
lib/l10n/app_localizations.dart | 54 +++
lib/l10n/app_localizations_bg.dart | 31 ++
lib/l10n/app_localizations_de.dart | 32 ++
lib/l10n/app_localizations_en.dart | 31 ++
lib/l10n/app_localizations_es.dart | 32 ++
lib/l10n/app_localizations_fr.dart | 32 ++
lib/l10n/app_localizations_it.dart | 32 ++
lib/l10n/app_localizations_nl.dart | 31 ++
lib/l10n/app_localizations_pl.dart | 31 ++
lib/l10n/app_localizations_pt.dart | 31 ++
lib/l10n/app_localizations_ru.dart | 32 ++
lib/l10n/app_localizations_sk.dart | 31 ++
lib/l10n/app_localizations_sl.dart | 31 ++
lib/l10n/app_localizations_sv.dart | 31 ++
lib/l10n/app_localizations_uk.dart | 32 ++
lib/l10n/app_localizations_zh.dart | 27 ++
lib/l10n/app_nl.arb | 11 +-
lib/l10n/app_pl.arb | 11 +-
lib/l10n/app_pt.arb | 11 +-
lib/l10n/app_ru.arb | 11 +-
lib/l10n/app_sk.arb | 11 +-
lib/l10n/app_sl.arb | 11 +-
lib/l10n/app_sv.arb | 11 +-
lib/l10n/app_uk.arb | 11 +-
lib/l10n/app_zh.arb | 11 +-
lib/main.dart | 4 +-
lib/screens/connection_choice_screen.dart | 201 ++++++++
lib/screens/scanner_screen.dart | 24 +-
lib/screens/usb_screen.dart | 456 ++++++++++++++++++
lib/services/usb_serial_service.dart | 284 +++++++++++
linux/flutter/generated_plugins.cmake | 1 +
pubspec.yaml | 1 +
windows/CMakeLists.txt | 8 +-
windows/flutter/generated_plugins.cmake | 1 +
45 files changed, 2083 insertions(+), 47 deletions(-)
create mode 100644 lib/screens/connection_choice_screen.dart
create mode 100644 lib/screens/usb_screen.dart
create mode 100644 lib/services/usb_serial_service.dart
diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index e0a80290..2e8f47f0 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -16,7 +16,7 @@ if (keystorePropertiesFile.exists()) {
android {
namespace = "com.meshcore.meshcore_open"
compileSdk = flutter.compileSdkVersion
- ndkVersion = flutter.ndkVersion
+ ndkVersion = "29.0.14206865"
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
@@ -84,4 +84,5 @@ flutter {
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
+ implementation("com.github.mik3y:usb-serial-for-android:3.9.0")
}
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index b8dd623d..4ff626f2 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -19,6 +19,7 @@
+
+ when (call.method) {
+ "listPorts" -> result.success(listUsbPorts())
+ "connect" -> handleUsbConnect(call, result)
+ "write" -> handleUsbWrite(call, result)
+ "disconnect" -> {
+ closeUsbConnection()
+ result.success(null)
+ }
+ else -> result.notImplemented()
+ }
+ }
+
+ EventChannel(flutterEngine.dartExecutor.binaryMessenger, usbEventChannelName)
+ .setStreamHandler(
+ object : EventChannel.StreamHandler {
+ override fun onListen(arguments: Any?, events: EventChannel.EventSink) {
+ eventSink = events
+ }
+
+ override fun onCancel(arguments: Any?) {
+ eventSink = null
+ }
+ },
+ )
+ }
+
+ override fun onDestroy() {
+ closeUsbConnection()
+ unregisterReceiver(permissionReceiver)
+ super.onDestroy()
+ }
+
+ private fun registerUsbPermissionReceiver() {
+ val filter = IntentFilter(usbPermissionAction)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ registerReceiver(permissionReceiver, filter, RECEIVER_NOT_EXPORTED)
+ } else {
+ @Suppress("DEPRECATION")
+ registerReceiver(permissionReceiver, filter)
+ }
+ }
+
+ private fun listUsbPorts(): List {
+ val drivers = UsbSerialProber.getDefaultProber().findAllDrivers(usbManager)
+ return drivers.map { driver ->
+ val device = driver.device
+ val productName = device.productName ?: "USB Serial Device"
+ val vendorProduct =
+ String.format(
+ Locale.US,
+ "VID:%04X PID:%04X",
+ device.vendorId,
+ device.productId,
+ )
+ "${device.deviceName} - $productName - $vendorProduct"
+ }
+ }
+
+ private fun handleUsbConnect(call: MethodCall, result: MethodChannel.Result) {
+ val portName = call.argument("portName")
+ val baudRate = call.argument("baudRate") ?: 115200
+ if (portName.isNullOrBlank()) {
+ result.error("usb_invalid_port", "Port name is required", null)
+ return
+ }
+
+ val device = findUsbDevice(portName)
+ if (device == null) {
+ result.error("usb_device_missing", "USB device not found for $portName", null)
+ return
+ }
+
+ if (usbManager.hasPermission(device)) {
+ openUsbDevice(device, baudRate, result)
+ return
+ }
+
+ if (pendingConnectResult != null) {
+ result.error("usb_busy", "Another USB permission request is already pending", null)
+ return
+ }
+
+ pendingConnectResult = result
+ pendingConnectPortName = portName
+ pendingConnectBaudRate = baudRate
+
+ val permissionIntent = PendingIntent.getBroadcast(
+ this,
+ 0,
+ Intent(usbPermissionAction).setPackage(packageName),
+ pendingIntentFlags(),
+ )
+ usbManager.requestPermission(device, permissionIntent)
+ }
+
+ private fun handleUsbWrite(call: MethodCall, result: MethodChannel.Result) {
+ val data = call.argument("data")
+ val port = usbPort
+ if (data == null) {
+ result.error("usb_invalid_data", "Data is required", null)
+ return
+ }
+ if (port == null) {
+ result.error("usb_not_connected", "USB serial port is not connected", null)
+ return
+ }
+
+ try {
+ port.write(data, 1000)
+ result.success(null)
+ } catch (error: Exception) {
+ result.error("usb_write_failed", error.message, null)
+ }
+ }
+
+ private fun findUsbDevice(portName: String): UsbDevice? {
+ return usbManager.deviceList.values.firstOrNull { it.deviceName == portName }
+ }
+
+ private fun openUsbDevice(
+ device: UsbDevice,
+ baudRate: Int,
+ result: MethodChannel.Result,
+ ) {
+ try {
+ closeUsbConnection()
+
+ val driver = UsbSerialProber.getDefaultProber().probeDevice(device)
+ if (driver == null) {
+ result.error("usb_driver_missing", "No USB serial driver for ${device.deviceName}", null)
+ return
+ }
+
+ val connection = usbManager.openDevice(device)
+ if (connection == null) {
+ result.error(
+ "usb_open_failed",
+ "UsbManager could not open ${device.deviceName}",
+ null,
+ )
+ return
+ }
+
+ val port = firstPort(driver)
+ if (port == null) {
+ connection.close()
+ result.error("usb_port_missing", "No USB serial port exposed by ${device.deviceName}", null)
+ return
+ }
+
+ port.open(connection)
+ port.setParameters(
+ baudRate,
+ 8,
+ UsbSerialPort.STOPBITS_1,
+ UsbSerialPort.PARITY_NONE,
+ )
+ port.rts = false
+ port.dtr = true
+
+ usbConnection = connection
+ usbPort = port
+
+ ioManager =
+ SerialInputOutputManager(
+ port,
+ object : SerialInputOutputManager.Listener {
+ override fun onNewData(data: ByteArray) {
+ mainHandler.post {
+ eventSink?.success(data)
+ }
+ }
+
+ override fun onRunError(e: Exception) {
+ mainHandler.post {
+ eventSink?.error(
+ "usb_io_error",
+ e.message ?: "USB serial I/O error",
+ null,
+ )
+ }
+ closeUsbConnection()
+ }
+ },
+ ).also { manager ->
+ manager.start()
+ }
+
+ result.success(null)
+ } catch (error: Exception) {
+ closeUsbConnection()
+ result.error("usb_connect_failed", error.message, null)
+ }
+ }
+
+ private fun firstPort(driver: UsbSerialDriver): UsbSerialPort? {
+ return driver.ports.firstOrNull()
+ }
+
+ private fun closeUsbConnection() {
+ try {
+ ioManager?.stop()
+ } catch (_: Exception) {
+ }
+ ioManager = null
+
+ try {
+ usbPort?.close()
+ } catch (_: Exception) {
+ }
+ usbPort = null
+
+ try {
+ usbConnection?.close()
+ } catch (_: Exception) {
+ }
+ usbConnection = null
+ }
+
+ private fun pendingIntentFlags(): Int {
+ var flags = PendingIntent.FLAG_UPDATE_CURRENT
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ flags = flags or PendingIntent.FLAG_MUTABLE
+ }
+ return flags
+ }
+}
diff --git a/android/build.gradle.kts b/android/build.gradle.kts
index dbee657b..eeea4583 100644
--- a/android/build.gradle.kts
+++ b/android/build.gradle.kts
@@ -2,6 +2,7 @@ allprojects {
repositories {
google()
mavenCentral()
+ maven(url = "https://jitpack.io")
}
}
diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart
index c57a85ac..f514f15a 100644
--- a/lib/connector/meshcore_connector.dart
+++ b/lib/connector/meshcore_connector.dart
@@ -20,6 +20,7 @@ import '../services/path_history_service.dart';
import '../services/app_settings_service.dart';
import '../services/background_service.dart';
import '../services/notification_service.dart';
+import '../services/usb_serial_service.dart';
import '../storage/channel_message_store.dart';
import '../storage/channel_order_store.dart';
import '../storage/channel_settings_store.dart';
@@ -82,6 +83,8 @@ enum MeshCoreConnectionState {
disconnecting,
}
+enum MeshCoreTransportType { bluetooth, usb }
+
class RepeaterBatterySnapshot {
final int millivolts;
final DateTime updatedAt;
@@ -108,6 +111,10 @@ class MeshCoreConnector extends ChangeNotifier {
String? _lastDeviceId;
String? _lastDeviceDisplayName;
bool _manualDisconnect = false;
+ final UsbSerialService _usbSerialService = UsbSerialService();
+ StreamSubscription? _usbFrameSubscription;
+ MeshCoreTransportType _activeTransport = MeshCoreTransportType.bluetooth;
+ String? _activeUsbPort;
final List _scanResults = [];
final List _contacts = [];
@@ -154,6 +161,8 @@ class MeshCoreConnector extends ChangeNotifier {
bool _hasLoadedChannels = false;
bool _batteryRequested = false;
bool _awaitingSelfInfo = false;
+ bool _hasReceivedDeviceInfo = false;
+ bool _pendingInitialChannelSync = false;
bool _preserveContactsOnRefresh = false;
static const int _defaultMaxContacts = 32;
static const int _defaultMaxChannels = 8;
@@ -217,6 +226,12 @@ class MeshCoreConnector extends ChangeNotifier {
String? get deviceId => _deviceId;
String get deviceIdLabel => _deviceId ?? 'Unknown';
+ MeshCoreTransportType get activeTransport => _activeTransport;
+ String? get activeUsbPort => _activeUsbPort;
+ bool get isUsbTransportConnected =>
+ _state == MeshCoreConnectionState.connected &&
+ _activeTransport == MeshCoreTransportType.usb;
+
String get deviceDisplayName {
if (_selfName != null && _selfName!.isNotEmpty) {
return _selfName!;
@@ -742,12 +757,17 @@ class MeshCoreConnector extends ChangeNotifier {
}
}
+ Future> listUsbPorts() => _usbSerialService.listPorts();
+
Future connect(BluetoothDevice device, {String? displayName}) async {
if (_state == MeshCoreConnectionState.connecting ||
_state == MeshCoreConnectionState.connected) {
return;
}
+ _activeTransport = MeshCoreTransportType.bluetooth;
+ _activeUsbPort = null;
+
await stopScan();
_setState(MeshCoreConnectionState.connecting);
_device = device;
@@ -832,6 +852,8 @@ class MeshCoreConnector extends ChangeNotifier {
);
_setState(MeshCoreConnectionState.connected);
+ _hasReceivedDeviceInfo = false;
+ _pendingInitialChannelSync = true;
await _requestDeviceInfo();
_startBatteryPolling();
@@ -845,9 +867,6 @@ class MeshCoreConnector extends ChangeNotifier {
// Keep device clock aligned on every connection.
await syncTime();
-
- // Fetch channels so we can track unread counts for incoming messages
- unawaited(getChannels());
} catch (e) {
debugPrint("Connection error: $e");
await disconnect(manual: false);
@@ -855,6 +874,63 @@ class MeshCoreConnector extends ChangeNotifier {
}
}
+ Future connectUsb({
+ required String portName,
+ int baudRate = 115200,
+ }) async {
+ if (_state == MeshCoreConnectionState.connecting ||
+ _state == MeshCoreConnectionState.connected) {
+ return;
+ }
+
+ _activeTransport = MeshCoreTransportType.bluetooth;
+ _activeUsbPort = null;
+
+ await stopScan();
+ _cancelReconnectTimer();
+ _manualDisconnect = false;
+ _activeTransport = MeshCoreTransportType.usb;
+ _activeUsbPort = portName;
+ unawaited(_backgroundService?.start());
+ _setState(MeshCoreConnectionState.connecting);
+
+ try {
+ await _usbFrameSubscription?.cancel();
+ _usbFrameSubscription = null;
+ await _usbSerialService.connect(portName: portName, baudRate: baudRate);
+ await Future.delayed(const Duration(milliseconds: 200));
+ _usbFrameSubscription = _usbSerialService.frameStream.listen(
+ _handleFrame,
+ onError: (error, stackTrace) {
+ debugPrint('USB transport error: $error');
+ unawaited(disconnect(manual: false));
+ },
+ onDone: () {
+ unawaited(disconnect(manual: false));
+ },
+ );
+
+ _setState(MeshCoreConnectionState.connected);
+ _hasReceivedDeviceInfo = false;
+ _pendingInitialChannelSync = true;
+ await _requestDeviceInfo();
+ _startBatteryPolling();
+ final gotSelfInfo = await _waitForSelfInfo(
+ timeout: const Duration(seconds: 3),
+ );
+ if (!gotSelfInfo) {
+ await refreshDeviceInfo();
+ await _waitForSelfInfo(timeout: const Duration(seconds: 3));
+ }
+
+ await syncTime();
+ } catch (error) {
+ debugPrint('USB connection error: $error');
+ await disconnect(manual: false);
+ rethrow;
+ }
+ }
+
Future _waitForSelfInfo({required Duration timeout}) async {
if (_selfPublicKey != null) return true;
if (!isConnected) return false;
@@ -886,7 +962,10 @@ class MeshCoreConnector extends ChangeNotifier {
return result;
}
- bool get _shouldAutoReconnect => !_manualDisconnect && _lastDeviceId != null;
+ bool get _shouldAutoReconnect =>
+ !_manualDisconnect &&
+ _lastDeviceId != null &&
+ _activeTransport == MeshCoreTransportType.bluetooth;
void _cancelReconnectTimer() {
_reconnectTimer?.cancel();
@@ -930,6 +1009,7 @@ class MeshCoreConnector extends ChangeNotifier {
Future disconnect({bool manual = true}) async {
if (_state == MeshCoreConnectionState.disconnecting) return;
+ final transportAtDisconnect = _activeTransport;
if (manual) {
_manualDisconnect = true;
@@ -941,6 +1021,10 @@ class MeshCoreConnector extends ChangeNotifier {
_setState(MeshCoreConnectionState.disconnecting);
_stopBatteryPolling();
+ await _usbFrameSubscription?.cancel();
+ _usbFrameSubscription = null;
+ await _usbSerialService.disconnect();
+
await _notifySubscription?.cancel();
_notifySubscription = null;
@@ -980,6 +1064,8 @@ class MeshCoreConnector extends ChangeNotifier {
_repeaterBatterySnapshots.clear();
_batteryRequested = false;
_awaitingSelfInfo = false;
+ _hasReceivedDeviceInfo = false;
+ _pendingInitialChannelSync = false;
_maxContacts = _defaultMaxContacts;
_maxChannels = _defaultMaxChannels;
_isSyncingQueuedMessages = false;
@@ -993,8 +1079,11 @@ class MeshCoreConnector extends ChangeNotifier {
_pendingGenericAckQueue.clear();
_reactionSendQueueSequence = 0;
+ _activeTransport = MeshCoreTransportType.bluetooth;
+ _activeUsbPort = null;
+
_setState(MeshCoreConnectionState.disconnected);
- if (!manual) {
+ if (!manual && transportAtDisconnect == MeshCoreTransportType.bluetooth) {
_scheduleReconnect();
}
}
@@ -1004,24 +1093,29 @@ class MeshCoreConnector extends ChangeNotifier {
String? channelSendQueueId,
bool expectsGenericAck = false,
}) async {
- if (!isConnected || _rxCharacteristic == null) {
+ if (!isConnected) {
throw Exception("Not connected to a MeshCore device");
}
-
_bleDebugLogService?.logFrame(data, outgoing: true);
- // Prefer write without response when supported; fall back to write with response.
- final properties = _rxCharacteristic!.properties;
- final canWriteWithoutResponse = properties.writeWithoutResponse;
- final canWriteWithResponse = properties.write;
- if (!canWriteWithoutResponse && !canWriteWithResponse) {
- throw Exception("MeshCore RX characteristic does not support write");
+ if (_activeTransport == MeshCoreTransportType.usb) {
+ await _usbSerialService.write(data);
+ } else {
+ if (_rxCharacteristic == null) {
+ throw Exception("MeshCore RX characteristic does not support write");
+ }
+ // Prefer write without response when supported; fall back to write with response.
+ final properties = _rxCharacteristic!.properties;
+ final canWriteWithoutResponse = properties.writeWithoutResponse;
+ final canWriteWithResponse = properties.write;
+ if (!canWriteWithoutResponse && !canWriteWithResponse) {
+ throw Exception("MeshCore RX characteristic does not support write");
+ }
+ await _rxCharacteristic!.write(
+ data.toList(),
+ withoutResponse: canWriteWithoutResponse,
+ );
}
-
- await _rxCharacteristic!.write(
- data.toList(),
- withoutResponse: canWriteWithoutResponse,
- );
_trackPendingGenericAck(
data,
channelSendQueueId: channelSendQueueId,
@@ -2000,10 +2094,12 @@ class MeshCoreConnector extends ChangeNotifier {
// Auto-fetch contacts after getting self info
getContacts();
+ _maybeStartInitialChannelSync();
}
void _handleDeviceInfo(Uint8List frame) {
if (frame.length < 4) return;
+ _hasReceivedDeviceInfo = true;
_firmwareVerCode = frame[1];
// Parse client_repeat from firmware v9+ (byte 80)
@@ -2027,12 +2123,25 @@ class MeshCoreConnector extends ChangeNotifier {
if (nextMaxChannels > previousMaxChannels) {
unawaited(loadChannelSettings(maxChannels: nextMaxChannels));
unawaited(loadAllChannelMessages(maxChannels: nextMaxChannels));
- if (isConnected) {
+ if (isConnected && !_pendingInitialChannelSync) {
unawaited(getChannels(maxChannels: nextMaxChannels));
}
}
}
notifyListeners();
+ _maybeStartInitialChannelSync();
+ }
+
+ void _maybeStartInitialChannelSync() {
+ if (!_pendingInitialChannelSync || !isConnected) {
+ return;
+ }
+ if (_selfPublicKey == null || !_hasReceivedDeviceInfo) {
+ return;
+ }
+
+ _pendingInitialChannelSync = false;
+ unawaited(getChannels(maxChannels: _maxChannels));
}
void _handleNoMoreMessages() {
@@ -3591,6 +3700,8 @@ class MeshCoreConnector extends ChangeNotifier {
_txCharacteristic = null;
// Preserve deviceId and displayName for UI display during reconnection
// They're only cleared on manual disconnect via disconnect() method
+ _hasReceivedDeviceInfo = false;
+ _pendingInitialChannelSync = false;
_maxContacts = _defaultMaxContacts;
_maxChannels = _defaultMaxChannels;
_isSyncingQueuedMessages = false;
@@ -3671,10 +3782,12 @@ class MeshCoreConnector extends ChangeNotifier {
void dispose() {
_scanSubscription?.cancel();
_connectionSubscription?.cancel();
+ _usbFrameSubscription?.cancel();
_notifySubscription?.cancel();
_reconnectTimer?.cancel();
_batteryPollTimer?.cancel();
_receivedFramesController.close();
+ _usbSerialService.dispose();
// Flush pending unread writes before disposal
_unreadStore.flush();
diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb
index 2dbcf5e6..0d64508f 100644
--- a/lib/l10n/app_bg.arb
+++ b/lib/l10n/app_bg.arb
@@ -1801,5 +1801,14 @@
"contacts_unread": "Непрочетено",
"contacts_searchRepeaters": "Търсене на {number}{str} повтарящи се...",
"contacts_searchContactsNoNumber": "Търси контакти...",
- "contacts_searchUsers": "Търсене на {number}{str} потребители..."
+ "contacts_searchUsers": "Търсене на {number}{str} потребители...",
+ "connectionChoiceUsbLabel": "USB",
+ "connectionChoiceBluetoothLabel": "Bluetooth",
+ "connectionChoiceTitle": "Изберете метода на връзка.",
+ "connectionChoiceSubtitle": "Изберете как искате да получите вашия устройство MeshCore.",
+ "usbScreenTitle": "Връзката чрез USB ще бъде налична скоро.",
+ "usbScreenSubtitle": "Създаваме път за комуникация, базиран на последователно предаване на данни, за Android и настолни компютри.",
+ "usbScreenStatus": "Ще бъде достъпно скоро",
+ "usbScreenNote": "След като бъде внедрена поддръжката за USB, ще изберете сериен порт и ще се свържете директно към вашето устройство MeshCore.",
+ "usbScreenEmptyState": "Няма открити USB устройства. Включете едно и опитайте отново."
}
diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb
index 07190a9a..0c49f8df 100644
--- a/lib/l10n/app_de.arb
+++ b/lib/l10n/app_de.arb
@@ -1829,5 +1829,14 @@
"contacts_searchRepeaters": "Suche {number}{str} Repeater...",
"contacts_searchFavorites": "Suche {number}{str} Favoriten...",
"contacts_searchUsers": "Suche {number}{str} Benutzer...",
- "contacts_searchRoomServers": "Suche {number}{str} Raumserver..."
+ "contacts_searchRoomServers": "Suche {number}{str} Raumserver...",
+ "connectionChoiceSubtitle": "Wählen Sie, wie Sie Ihr MeshCore-Gerät erreichen möchten.",
+ "connectionChoiceUsbLabel": "USB",
+ "connectionChoiceBluetoothLabel": "Bluetooth",
+ "connectionChoiceTitle": "Wählen Sie Ihre bevorzugte Verbindungsmethode.",
+ "usbScreenTitle": "Die USB-Verbindung wird bald verfügbar sein.",
+ "usbScreenSubtitle": "Wir entwickeln eine Verbindung, die sowohl für Android- als auch für Desktop-Geräte geeignet ist und auf einer seriellen Schnittstelle basiert.",
+ "usbScreenStatus": "Bald verfügbar",
+ "usbScreenNote": "Sobald die USB-Unterstützung implementiert ist, wählen Sie einen seriellen Anschluss und verbinden Sie ihn direkt mit Ihrem MeshCore-Gerät.",
+ "usbScreenEmptyState": "Keine USB-Geräte gefunden. Schließen Sie eines an und aktualisieren Sie."
}
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index f0b05874..9719c7c5 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -46,6 +46,15 @@
}
},
"scanner_title": "MeshCore Open",
+ "connectionChoiceTitle": "Choose your connection method",
+ "connectionChoiceSubtitle": "Select how you would like to reach your MeshCore device.",
+ "connectionChoiceUsbLabel": "USB",
+ "connectionChoiceBluetoothLabel": "Bluetooth",
+ "usbScreenTitle": "Connect over USB",
+ "usbScreenSubtitle": "Choose a detected serial device and connect directly to your MeshCore node.",
+ "usbScreenStatus": "Select a USB device",
+ "usbScreenNote": "USB serial is active on supported Android devices and desktop platforms.",
+ "usbScreenEmptyState": "No USB devices found. Plug one in and refresh.",
"scanner_scanning": "Scanning for devices...",
"scanner_connecting": "Connecting...",
"scanner_disconnecting": "Disconnecting...",
diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb
index 47765c6f..48431e38 100644
--- a/lib/l10n/app_es.arb
+++ b/lib/l10n/app_es.arb
@@ -1829,5 +1829,14 @@
"contacts_searchFavorites": "Buscar {number}{str} Favoritos...",
"contacts_searchUsers": "Buscar {number}{str} Usuarios...",
"contacts_searchRepeaters": "Buscar {number}{str} Repetidores...",
- "contacts_searchRoomServers": "Buscar {number}{str} servidores de sala..."
+ "contacts_searchRoomServers": "Buscar {number}{str} servidores de sala...",
+ "connectionChoiceTitle": "Seleccione su método de conexión.",
+ "connectionChoiceSubtitle": "Seleccione la forma en que desea acceder a su dispositivo MeshCore.",
+ "connectionChoiceBluetoothLabel": "Bluetooth",
+ "connectionChoiceUsbLabel": "USB",
+ "usbScreenTitle": "La conexión USB estará disponible próximamente.",
+ "usbScreenSubtitle": "Estamos creando una conexión en serie para dispositivos Android y de escritorio.",
+ "usbScreenStatus": "Próximamente",
+ "usbScreenNote": "Una vez que se implemente el soporte para USB, seleccionará un puerto serie y se conectará directamente a su dispositivo MeshCore.",
+ "usbScreenEmptyState": "No se detectaron dispositivos USB. Conecte uno y vuelva a intentar."
}
diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb
index b742dc97..61f6551d 100644
--- a/lib/l10n/app_fr.arb
+++ b/lib/l10n/app_fr.arb
@@ -1801,5 +1801,14 @@
"contacts_searchUsers": "Rechercher {number}{str} utilisateurs...",
"contacts_searchRoomServers": "Rechercher {number}{str} serveurs de salle...",
"contacts_searchRepeaters": "Rechercher {number}{str} Répéteurs...",
- "contacts_searchContactsNoNumber": "Rechercher des contacts..."
+ "contacts_searchContactsNoNumber": "Rechercher des contacts...",
+ "connectionChoiceTitle": "Choisissez votre méthode de connexion.",
+ "connectionChoiceBluetoothLabel": "Bluetooth",
+ "connectionChoiceUsbLabel": "USB",
+ "connectionChoiceSubtitle": "Choisissez la méthode de livraison que vous préférez pour votre appareil MeshCore.",
+ "usbScreenTitle": "La connexion USB sera disponible prochainement.",
+ "usbScreenSubtitle": "Nous mettons en place un chemin de connexion basé sur une série pour les appareils Android et les ordinateurs de bureau.",
+ "usbScreenStatus": "Bientôt",
+ "usbScreenNote": "Une fois que le support USB sera disponible, vous sélectionnerez un port série et vous connecterez directement à votre appareil MeshCore.",
+ "usbScreenEmptyState": "Aucun périphérique USB n'a été trouvé. Connectez-en un et rafraîchissez."
}
diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb
index 82adad80..827d1e79 100644
--- a/lib/l10n/app_it.arb
+++ b/lib/l10n/app_it.arb
@@ -1801,5 +1801,14 @@
"contacts_searchFavorites": "Cerca {number}{str} Preferiti...",
"contacts_unread": "Non letti",
"contacts_searchRepeaters": "Cerca {number}{str} Ripetitori...",
- "contacts_searchRoomServers": "Cerca {number}{str} server Room..."
+ "contacts_searchRoomServers": "Cerca {number}{str} server Room...",
+ "connectionChoiceTitle": "Scegli il metodo di connessione che preferisci.",
+ "connectionChoiceSubtitle": "Seleziona il metodo che preferisci per accedere al tuo dispositivo MeshCore.",
+ "connectionChoiceBluetoothLabel": "Bluetooth",
+ "connectionChoiceUsbLabel": "USB",
+ "usbScreenTitle": "La connessione USB sarà disponibile a breve.",
+ "usbScreenSubtitle": "Stiamo sviluppando un percorso di connessione basato su serie per Android e per i desktop.",
+ "usbScreenStatus": "Arriverà presto",
+ "usbScreenNote": "Una volta che il supporto USB sarà disponibile, selezionerete una porta seriale e vi connetterete direttamente al vostro dispositivo MeshCore.",
+ "usbScreenEmptyState": "Nessun dispositivo USB rilevato. Collegare uno e riavviare."
}
diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart
index c48994c6..162b7603 100644
--- a/lib/l10n/app_localizations.dart
+++ b/lib/l10n/app_localizations.dart
@@ -316,6 +316,60 @@ abstract class AppLocalizations {
/// **'MeshCore Open'**
String get scanner_title;
+ /// No description provided for @connectionChoiceTitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Choose your connection method'**
+ String get connectionChoiceTitle;
+
+ /// No description provided for @connectionChoiceSubtitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Select how you would like to reach your MeshCore device.'**
+ String get connectionChoiceSubtitle;
+
+ /// No description provided for @connectionChoiceUsbLabel.
+ ///
+ /// In en, this message translates to:
+ /// **'USB'**
+ String get connectionChoiceUsbLabel;
+
+ /// No description provided for @connectionChoiceBluetoothLabel.
+ ///
+ /// In en, this message translates to:
+ /// **'Bluetooth'**
+ String get connectionChoiceBluetoothLabel;
+
+ /// No description provided for @usbScreenTitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Connect over USB'**
+ String get usbScreenTitle;
+
+ /// No description provided for @usbScreenSubtitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Choose a detected serial device and connect directly to your MeshCore node.'**
+ String get usbScreenSubtitle;
+
+ /// No description provided for @usbScreenStatus.
+ ///
+ /// In en, this message translates to:
+ /// **'Select a USB device'**
+ String get usbScreenStatus;
+
+ /// No description provided for @usbScreenNote.
+ ///
+ /// In en, this message translates to:
+ /// **'USB serial is active on supported Android devices and desktop platforms.'**
+ String get usbScreenNote;
+
+ /// No description provided for @usbScreenEmptyState.
+ ///
+ /// In en, this message translates to:
+ /// **'No USB devices found. Plug one in and refresh.'**
+ String get usbScreenEmptyState;
+
/// No description provided for @scanner_scanning.
///
/// In en, this message translates to:
diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart
index c168b7c4..861bf6a5 100644
--- a/lib/l10n/app_localizations_bg.dart
+++ b/lib/l10n/app_localizations_bg.dart
@@ -108,6 +108,37 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
+ @override
+ String get connectionChoiceTitle => 'Изберете метода на връзка.';
+
+ @override
+ String get connectionChoiceSubtitle =>
+ 'Изберете как искате да получите вашия устройство MeshCore.';
+
+ @override
+ String get connectionChoiceUsbLabel => 'USB';
+
+ @override
+ String get connectionChoiceBluetoothLabel => 'Bluetooth';
+
+ @override
+ String get usbScreenTitle => 'Връзката чрез USB ще бъде налична скоро.';
+
+ @override
+ String get usbScreenSubtitle =>
+ 'Създаваме път за комуникация, базиран на последователно предаване на данни, за Android и настолни компютри.';
+
+ @override
+ String get usbScreenStatus => 'Ще бъде достъпно скоро';
+
+ @override
+ String get usbScreenNote =>
+ 'След като бъде внедрена поддръжката за USB, ще изберете сериен порт и ще се свържете директно към вашето устройство MeshCore.';
+
+ @override
+ String get usbScreenEmptyState =>
+ 'Няма открити USB устройства. Включете едно и опитайте отново.';
+
@override
String get scanner_scanning => 'Сканиране за устройства...';
diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart
index c7eb9273..f768dbbe 100644
--- a/lib/l10n/app_localizations_de.dart
+++ b/lib/l10n/app_localizations_de.dart
@@ -108,6 +108,38 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
+ @override
+ String get connectionChoiceTitle =>
+ 'Wählen Sie Ihre bevorzugte Verbindungsmethode.';
+
+ @override
+ String get connectionChoiceSubtitle =>
+ 'Wählen Sie, wie Sie Ihr MeshCore-Gerät erreichen möchten.';
+
+ @override
+ String get connectionChoiceUsbLabel => 'USB';
+
+ @override
+ String get connectionChoiceBluetoothLabel => 'Bluetooth';
+
+ @override
+ String get usbScreenTitle => 'Die USB-Verbindung wird bald verfügbar sein.';
+
+ @override
+ String get usbScreenSubtitle =>
+ 'Wir entwickeln eine Verbindung, die sowohl für Android- als auch für Desktop-Geräte geeignet ist und auf einer seriellen Schnittstelle basiert.';
+
+ @override
+ String get usbScreenStatus => 'Bald verfügbar';
+
+ @override
+ String get usbScreenNote =>
+ 'Sobald die USB-Unterstützung implementiert ist, wählen Sie einen seriellen Anschluss und verbinden Sie ihn direkt mit Ihrem MeshCore-Gerät.';
+
+ @override
+ String get usbScreenEmptyState =>
+ 'Keine USB-Geräte gefunden. Schließen Sie eines an und aktualisieren Sie.';
+
@override
String get scanner_scanning => 'Scannen nach Geräten...';
diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart
index 44580624..2c287f7b 100644
--- a/lib/l10n/app_localizations_en.dart
+++ b/lib/l10n/app_localizations_en.dart
@@ -108,6 +108,37 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
+ @override
+ String get connectionChoiceTitle => 'Choose your connection method';
+
+ @override
+ String get connectionChoiceSubtitle =>
+ 'Select how you would like to reach your MeshCore device.';
+
+ @override
+ String get connectionChoiceUsbLabel => 'USB';
+
+ @override
+ String get connectionChoiceBluetoothLabel => 'Bluetooth';
+
+ @override
+ String get usbScreenTitle => 'Connect over USB';
+
+ @override
+ String get usbScreenSubtitle =>
+ 'Choose a detected serial device and connect directly to your MeshCore node.';
+
+ @override
+ String get usbScreenStatus => 'Select a USB device';
+
+ @override
+ String get usbScreenNote =>
+ 'USB serial is active on supported Android devices and desktop platforms.';
+
+ @override
+ String get usbScreenEmptyState =>
+ 'No USB devices found. Plug one in and refresh.';
+
@override
String get scanner_scanning => 'Scanning for devices...';
diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart
index ce4b615c..32680ab4 100644
--- a/lib/l10n/app_localizations_es.dart
+++ b/lib/l10n/app_localizations_es.dart
@@ -108,6 +108,38 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
+ @override
+ String get connectionChoiceTitle => 'Seleccione su método de conexión.';
+
+ @override
+ String get connectionChoiceSubtitle =>
+ 'Seleccione la forma en que desea acceder a su dispositivo MeshCore.';
+
+ @override
+ String get connectionChoiceUsbLabel => 'USB';
+
+ @override
+ String get connectionChoiceBluetoothLabel => 'Bluetooth';
+
+ @override
+ String get usbScreenTitle =>
+ 'La conexión USB estará disponible próximamente.';
+
+ @override
+ String get usbScreenSubtitle =>
+ 'Estamos creando una conexión en serie para dispositivos Android y de escritorio.';
+
+ @override
+ String get usbScreenStatus => 'Próximamente';
+
+ @override
+ String get usbScreenNote =>
+ 'Una vez que se implemente el soporte para USB, seleccionará un puerto serie y se conectará directamente a su dispositivo MeshCore.';
+
+ @override
+ String get usbScreenEmptyState =>
+ 'No se detectaron dispositivos USB. Conecte uno y vuelva a intentar.';
+
@override
String get scanner_scanning => 'Escaneando dispositivos...';
diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart
index 61184443..dae34782 100644
--- a/lib/l10n/app_localizations_fr.dart
+++ b/lib/l10n/app_localizations_fr.dart
@@ -108,6 +108,38 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
+ @override
+ String get connectionChoiceTitle => 'Choisissez votre méthode de connexion.';
+
+ @override
+ String get connectionChoiceSubtitle =>
+ 'Choisissez la méthode de livraison que vous préférez pour votre appareil MeshCore.';
+
+ @override
+ String get connectionChoiceUsbLabel => 'USB';
+
+ @override
+ String get connectionChoiceBluetoothLabel => 'Bluetooth';
+
+ @override
+ String get usbScreenTitle =>
+ 'La connexion USB sera disponible prochainement.';
+
+ @override
+ String get usbScreenSubtitle =>
+ 'Nous mettons en place un chemin de connexion basé sur une série pour les appareils Android et les ordinateurs de bureau.';
+
+ @override
+ String get usbScreenStatus => 'Bientôt';
+
+ @override
+ String get usbScreenNote =>
+ 'Une fois que le support USB sera disponible, vous sélectionnerez un port série et vous connecterez directement à votre appareil MeshCore.';
+
+ @override
+ String get usbScreenEmptyState =>
+ 'Aucun périphérique USB n\'a été trouvé. Connectez-en un et rafraîchissez.';
+
@override
String get scanner_scanning => 'Recherche de périphériques...';
diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart
index 20873b9e..b138671b 100644
--- a/lib/l10n/app_localizations_it.dart
+++ b/lib/l10n/app_localizations_it.dart
@@ -108,6 +108,38 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
+ @override
+ String get connectionChoiceTitle =>
+ 'Scegli il metodo di connessione che preferisci.';
+
+ @override
+ String get connectionChoiceSubtitle =>
+ 'Seleziona il metodo che preferisci per accedere al tuo dispositivo MeshCore.';
+
+ @override
+ String get connectionChoiceUsbLabel => 'USB';
+
+ @override
+ String get connectionChoiceBluetoothLabel => 'Bluetooth';
+
+ @override
+ String get usbScreenTitle => 'La connessione USB sarà disponibile a breve.';
+
+ @override
+ String get usbScreenSubtitle =>
+ 'Stiamo sviluppando un percorso di connessione basato su serie per Android e per i desktop.';
+
+ @override
+ String get usbScreenStatus => 'Arriverà presto';
+
+ @override
+ String get usbScreenNote =>
+ 'Una volta che il supporto USB sarà disponibile, selezionerete una porta seriale e vi connetterete direttamente al vostro dispositivo MeshCore.';
+
+ @override
+ String get usbScreenEmptyState =>
+ 'Nessun dispositivo USB rilevato. Collegare uno e riavviare.';
+
@override
String get scanner_scanning => 'Scansione in corso per i dispositivi...';
diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart
index 323981da..f582abc9 100644
--- a/lib/l10n/app_localizations_nl.dart
+++ b/lib/l10n/app_localizations_nl.dart
@@ -108,6 +108,37 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
+ @override
+ String get connectionChoiceTitle => 'Kies uw verbindingsmethode';
+
+ @override
+ String get connectionChoiceSubtitle =>
+ 'Kies hoe u uw MeshCore-apparaat wilt bereiken.';
+
+ @override
+ String get connectionChoiceUsbLabel => 'USB';
+
+ @override
+ String get connectionChoiceBluetoothLabel => 'Bluetooth';
+
+ @override
+ String get usbScreenTitle => 'USB-verbinding is binnenkort beschikbaar.';
+
+ @override
+ String get usbScreenSubtitle =>
+ 'We ontwikkelen een verbindingspad op basis van seriële communicatie, zowel voor Android als voor desktop-computers.';
+
+ @override
+ String get usbScreenStatus => 'Komende week';
+
+ @override
+ String get usbScreenNote =>
+ 'Zodra de USB-ondersteuning is geïnstalleerd, selecteert u een seriële poort en verbindt u direct met uw MeshCore-apparaat.';
+
+ @override
+ String get usbScreenEmptyState =>
+ 'Geen USB-apparaten gevonden. Sluit er een aan en herlaad.';
+
@override
String get scanner_scanning => 'Scannen naar apparaten...';
diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart
index e033359e..2f103b55 100644
--- a/lib/l10n/app_localizations_pl.dart
+++ b/lib/l10n/app_localizations_pl.dart
@@ -108,6 +108,37 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
+ @override
+ String get connectionChoiceTitle => 'Wybierz metodę połączenia.';
+
+ @override
+ String get connectionChoiceSubtitle =>
+ 'Wybierz, w jaki sposób chcesz uzyskać dostęp do swojego urządzenia MeshCore.';
+
+ @override
+ String get connectionChoiceUsbLabel => 'USB';
+
+ @override
+ String get connectionChoiceBluetoothLabel => 'Bluetooth';
+
+ @override
+ String get usbScreenTitle => 'Połączenie USB będzie dostępne wkrótce.';
+
+ @override
+ String get usbScreenSubtitle =>
+ 'Tworzymy ścieżkę połączenia opartą na protokole szeregowym, przeznaczoną zarówno dla urządzeń z systemem Android, jak i dla komputerów stacjonarnych.';
+
+ @override
+ String get usbScreenStatus => 'Wkrótce';
+
+ @override
+ String get usbScreenNote =>
+ 'Po wdrożeniu wsparcia dla USB, wybierzesz port szeregowy i połączysz się bezpośrednio z urządzeniem MeshCore.';
+
+ @override
+ String get usbScreenEmptyState =>
+ 'Nie znaleziono żadnych urządzeń USB. Podłącz jedno i zaktualizuj.';
+
@override
String get scanner_scanning => 'Skanowanie urządzeń...';
diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart
index dd194529..283bada5 100644
--- a/lib/l10n/app_localizations_pt.dart
+++ b/lib/l10n/app_localizations_pt.dart
@@ -108,6 +108,37 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
+ @override
+ String get connectionChoiceTitle => 'Escolha o método de conexão desejado.';
+
+ @override
+ String get connectionChoiceSubtitle =>
+ 'Selecione a forma como você deseja acessar seu dispositivo MeshCore.';
+
+ @override
+ String get connectionChoiceUsbLabel => 'USB';
+
+ @override
+ String get connectionChoiceBluetoothLabel => 'Bluetooth';
+
+ @override
+ String get usbScreenTitle => 'A conexão USB estará disponível em breve.';
+
+ @override
+ String get usbScreenSubtitle =>
+ 'Estamos criando um caminho de conexão baseado em série para dispositivos Android e de desktop.';
+
+ @override
+ String get usbScreenStatus => 'Em breve';
+
+ @override
+ String get usbScreenNote =>
+ 'Assim que o suporte USB for implementado, você poderá selecionar uma porta serial e conectar-se diretamente ao seu dispositivo MeshCore.';
+
+ @override
+ String get usbScreenEmptyState =>
+ 'Nenhum dispositivo USB encontrado. Conecte um e atualize.';
+
@override
String get scanner_scanning => 'Procurando por dispositivos...';
diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart
index 5f5591dc..16877827 100644
--- a/lib/l10n/app_localizations_ru.dart
+++ b/lib/l10n/app_localizations_ru.dart
@@ -108,6 +108,38 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
+ @override
+ String get connectionChoiceTitle => 'Выберите способ подключения';
+
+ @override
+ String get connectionChoiceSubtitle =>
+ 'Выберите, каким способом вы хотите получить свой устройство MeshCore.';
+
+ @override
+ String get connectionChoiceUsbLabel => 'USB';
+
+ @override
+ String get connectionChoiceBluetoothLabel => 'Bluetooth';
+
+ @override
+ String get usbScreenTitle =>
+ 'Подключение через USB будет доступно в ближайшее время.';
+
+ @override
+ String get usbScreenSubtitle =>
+ 'Мы создаем последовательную схему подключения для устройств на базе Android и настольных компьютеров.';
+
+ @override
+ String get usbScreenStatus => 'Скоро';
+
+ @override
+ String get usbScreenNote =>
+ 'Как только появится поддержка USB, вы сможете выбрать последовательный порт и напрямую подключиться к вашему устройству MeshCore.';
+
+ @override
+ String get usbScreenEmptyState =>
+ 'Не обнаружено никаких устройств USB. Подключите одно из них и обновите список.';
+
@override
String get scanner_scanning => 'Поиск устройств...';
diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart
index 82e4d060..89490908 100644
--- a/lib/l10n/app_localizations_sk.dart
+++ b/lib/l10n/app_localizations_sk.dart
@@ -108,6 +108,37 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
+ @override
+ String get connectionChoiceTitle => 'Vyberte si metódu prepojenia.';
+
+ @override
+ String get connectionChoiceSubtitle =>
+ 'Vyberte si, ako chcete dosiahnuť váš zariadenie MeshCore.';
+
+ @override
+ String get connectionChoiceUsbLabel => 'USB';
+
+ @override
+ String get connectionChoiceBluetoothLabel => 'Bluetooth';
+
+ @override
+ String get usbScreenTitle => 'Pripojenie cez USB bude k dispozícii čoskoro.';
+
+ @override
+ String get usbScreenSubtitle =>
+ 'Vytvárajeme komunikačný systém založený na sériovej komunikácii pre Android a stolné počítače.';
+
+ @override
+ String get usbScreenStatus => 'Čoskoro';
+
+ @override
+ String get usbScreenNote =>
+ 'Po implementácii podpory pre USB, budete môcť vybrať sériový port a priamo sa pripojiť k vašmu zariadeniu MeshCore.';
+
+ @override
+ String get usbScreenEmptyState =>
+ 'Nenašli sa žiadne USB zariadenia. Pripojte jedno a obnovte.';
+
@override
String get scanner_scanning => 'Skrívania zariadení...';
diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart
index 9b1bfc97..bd0c4d57 100644
--- a/lib/l10n/app_localizations_sl.dart
+++ b/lib/l10n/app_localizations_sl.dart
@@ -108,6 +108,37 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
+ @override
+ String get connectionChoiceTitle => 'Izberite svoj način povezave.';
+
+ @override
+ String get connectionChoiceSubtitle =>
+ 'Izberite, kako želite dostopati do svojega naprave MeshCore.';
+
+ @override
+ String get connectionChoiceUsbLabel => 'USB';
+
+ @override
+ String get connectionChoiceBluetoothLabel => 'Bluetooth';
+
+ @override
+ String get usbScreenTitle => 'Vnos preko USB-ja bo v kratkem na voljo.';
+
+ @override
+ String get usbScreenSubtitle =>
+ 'Gradimo pot za serijsko povezavo za Android in računalnike.';
+
+ @override
+ String get usbScreenStatus => 'Čez kratko časa';
+
+ @override
+ String get usbScreenNote =>
+ 'Ko bo podpora za USB na voljo, boste izbrali serijsko vrata in se neposredno povezali z vašim napravem MeshCore.';
+
+ @override
+ String get usbScreenEmptyState =>
+ 'Niti en USB naprave niso bilo najdeno. Povežite eno in posodobite.';
+
@override
String get scanner_scanning => 'Skeniram za naprave...';
diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart
index ddfcf41a..183250dc 100644
--- a/lib/l10n/app_localizations_sv.dart
+++ b/lib/l10n/app_localizations_sv.dart
@@ -108,6 +108,37 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
+ @override
+ String get connectionChoiceTitle => 'Välj din anslutningsmetod';
+
+ @override
+ String get connectionChoiceSubtitle =>
+ 'Välj hur du vill komma åt din MeshCore-enhet.';
+
+ @override
+ String get connectionChoiceUsbLabel => 'USB';
+
+ @override
+ String get connectionChoiceBluetoothLabel => 'Bluetooth';
+
+ @override
+ String get usbScreenTitle => 'USB-anslutning kommer snart';
+
+ @override
+ String get usbScreenSubtitle =>
+ 'Vi skapar en seriebaserad anslutningsväg för både Android- och skrivbordsenheter.';
+
+ @override
+ String get usbScreenStatus => 'Kommer snart';
+
+ @override
+ String get usbScreenNote =>
+ 'När USB-stöd är implementerat, kommer du att välja en seriell port och ansluta direkt till din MeshCore-enhet.';
+
+ @override
+ String get usbScreenEmptyState =>
+ 'Inga USB-enheter hittades. Anslut en och uppdatera.';
+
@override
String get scanner_scanning => 'Söker efter enheter...';
diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart
index b44a7cb3..19feaac1 100644
--- a/lib/l10n/app_localizations_uk.dart
+++ b/lib/l10n/app_localizations_uk.dart
@@ -108,6 +108,38 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
+ @override
+ String get connectionChoiceTitle => 'Виберіть спосіб зв\'язку';
+
+ @override
+ String get connectionChoiceSubtitle =>
+ 'Виберіть, яким способом ви бажаєте отримати доступ до вашого пристрою MeshCore.';
+
+ @override
+ String get connectionChoiceUsbLabel => 'USB';
+
+ @override
+ String get connectionChoiceBluetoothLabel => 'Bluetooth';
+
+ @override
+ String get usbScreenTitle =>
+ 'Підключення через USB буде доступне найближчим часом.';
+
+ @override
+ String get usbScreenSubtitle =>
+ 'Ми створюємо серійний шлях з\'єднання для Android та десктопних комп\'ютерів.';
+
+ @override
+ String get usbScreenStatus => 'Скоро';
+
+ @override
+ String get usbScreenNote =>
+ 'Після того, як буде реалізовано підтримку USB, ви виберете серійний порт і підключитесь безпосередньо до вашого пристрою MeshCore.';
+
+ @override
+ String get usbScreenEmptyState =>
+ 'Не знайдено жодних пристроїв USB. Підключіть один і перезавантажте.';
+
@override
String get scanner_scanning => 'Пошук пристроїв...';
diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart
index b3d85ed5..fdb9f1da 100644
--- a/lib/l10n/app_localizations_zh.dart
+++ b/lib/l10n/app_localizations_zh.dart
@@ -108,6 +108,33 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get scanner_title => '连接设备';
+ @override
+ String get connectionChoiceTitle => '选择您的连接方式';
+
+ @override
+ String get connectionChoiceSubtitle => '请选择您希望如何访问 MeshCore 设备的选项。';
+
+ @override
+ String get connectionChoiceUsbLabel => 'USB';
+
+ @override
+ String get connectionChoiceBluetoothLabel => '蓝牙';
+
+ @override
+ String get usbScreenTitle => 'USB 连接即将推出';
+
+ @override
+ String get usbScreenSubtitle => '我们正在构建一个基于串行的连接路径,用于Android和桌面设备。';
+
+ @override
+ String get usbScreenStatus => '即将推出';
+
+ @override
+ String get usbScreenNote => '一旦USB支持功能上线,您就可以选择一个串口,并直接连接到您的MeshCore设备。';
+
+ @override
+ String get usbScreenEmptyState => '未找到任何 USB 设备。请插入一个,然后刷新。';
+
@override
String get scanner_scanning => '正在搜索设备...';
diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb
index 35cb3751..2f094058 100644
--- a/lib/l10n/app_nl.arb
+++ b/lib/l10n/app_nl.arb
@@ -1801,5 +1801,14 @@
"contacts_searchContactsNoNumber": "Zoek contacten...",
"contacts_searchUsers": "Zoek {number}{str} gebruikers...",
"contacts_searchFavorites": "Zoek {number}{str} favorieten...",
- "contacts_searchRoomServers": "Zoek {number}{str} Room servers..."
+ "contacts_searchRoomServers": "Zoek {number}{str} Room servers...",
+ "connectionChoiceTitle": "Kies uw verbindingsmethode",
+ "connectionChoiceUsbLabel": "USB",
+ "connectionChoiceSubtitle": "Kies hoe u uw MeshCore-apparaat wilt bereiken.",
+ "connectionChoiceBluetoothLabel": "Bluetooth",
+ "usbScreenTitle": "USB-verbinding is binnenkort beschikbaar.",
+ "usbScreenSubtitle": "We ontwikkelen een verbindingspad op basis van seriële communicatie, zowel voor Android als voor desktop-computers.",
+ "usbScreenStatus": "Komende week",
+ "usbScreenNote": "Zodra de USB-ondersteuning is geïnstalleerd, selecteert u een seriële poort en verbindt u direct met uw MeshCore-apparaat.",
+ "usbScreenEmptyState": "Geen USB-apparaten gevonden. Sluit er een aan en herlaad."
}
diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb
index 23c4cbcd..8988e02a 100644
--- a/lib/l10n/app_pl.arb
+++ b/lib/l10n/app_pl.arb
@@ -1801,5 +1801,14 @@
"contacts_searchFavorites": "Wyszukaj {number}{str} ulubione...",
"contacts_searchRoomServers": "Wyszukaj {number}{str} serwerów Room...",
"contacts_searchUsers": "Wyszukaj {number}{str} Użytkowników...",
- "contacts_searchRepeaters": "Wyszukaj {number}{str} powtórników..."
+ "contacts_searchRepeaters": "Wyszukaj {number}{str} powtórników...",
+ "connectionChoiceBluetoothLabel": "Bluetooth",
+ "connectionChoiceSubtitle": "Wybierz, w jaki sposób chcesz uzyskać dostęp do swojego urządzenia MeshCore.",
+ "connectionChoiceTitle": "Wybierz metodę połączenia.",
+ "connectionChoiceUsbLabel": "USB",
+ "usbScreenTitle": "Połączenie USB będzie dostępne wkrótce.",
+ "usbScreenSubtitle": "Tworzymy ścieżkę połączenia opartą na protokole szeregowym, przeznaczoną zarówno dla urządzeń z systemem Android, jak i dla komputerów stacjonarnych.",
+ "usbScreenStatus": "Wkrótce",
+ "usbScreenNote": "Po wdrożeniu wsparcia dla USB, wybierzesz port szeregowy i połączysz się bezpośrednio z urządzeniem MeshCore.",
+ "usbScreenEmptyState": "Nie znaleziono żadnych urządzeń USB. Podłącz jedno i zaktualizuj."
}
diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb
index 05792c68..296590fc 100644
--- a/lib/l10n/app_pt.arb
+++ b/lib/l10n/app_pt.arb
@@ -1801,5 +1801,14 @@
"contacts_searchUsers": "Pesquisar {number}{str} Usuários...",
"contacts_searchContactsNoNumber": "Pesquisar Contatos...",
"contacts_unread": "Não lido",
- "contacts_searchRoomServers": "Pesquisar {number}{str} servidores de sala..."
+ "contacts_searchRoomServers": "Pesquisar {number}{str} servidores de sala...",
+ "connectionChoiceSubtitle": "Selecione a forma como você deseja acessar seu dispositivo MeshCore.",
+ "connectionChoiceUsbLabel": "USB",
+ "connectionChoiceBluetoothLabel": "Bluetooth",
+ "connectionChoiceTitle": "Escolha o método de conexão desejado.",
+ "usbScreenTitle": "A conexão USB estará disponível em breve.",
+ "usbScreenSubtitle": "Estamos criando um caminho de conexão baseado em série para dispositivos Android e de desktop.",
+ "usbScreenStatus": "Em breve",
+ "usbScreenNote": "Assim que o suporte USB for implementado, você poderá selecionar uma porta serial e conectar-se diretamente ao seu dispositivo MeshCore.",
+ "usbScreenEmptyState": "Nenhum dispositivo USB encontrado. Conecte um e atualize."
}
diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb
index 9460f445..65b3792f 100644
--- a/lib/l10n/app_ru.arb
+++ b/lib/l10n/app_ru.arb
@@ -1041,5 +1041,14 @@
"contacts_unread": "Непрочитанное",
"contacts_searchRoomServers": "Поиск {number}{str} серверов комнат...",
"contacts_searchFavorites": "Поиск {number}{str} избранного...",
- "contacts_searchUsers": "Поиск {number}{str} пользователей..."
+ "contacts_searchUsers": "Поиск {number}{str} пользователей...",
+ "connectionChoiceSubtitle": "Выберите, каким способом вы хотите получить свой устройство MeshCore.",
+ "connectionChoiceTitle": "Выберите способ подключения",
+ "connectionChoiceUsbLabel": "USB",
+ "connectionChoiceBluetoothLabel": "Bluetooth",
+ "usbScreenTitle": "Подключение через USB будет доступно в ближайшее время.",
+ "usbScreenSubtitle": "Мы создаем последовательную схему подключения для устройств на базе Android и настольных компьютеров.",
+ "usbScreenStatus": "Скоро",
+ "usbScreenNote": "Как только появится поддержка USB, вы сможете выбрать последовательный порт и напрямую подключиться к вашему устройству MeshCore.",
+ "usbScreenEmptyState": "Не обнаружено никаких устройств USB. Подключите одно из них и обновите список."
}
diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb
index 5bc00c65..7178166c 100644
--- a/lib/l10n/app_sk.arb
+++ b/lib/l10n/app_sk.arb
@@ -1801,5 +1801,14 @@
"contacts_searchRepeaters": "Hľadať {number}{str} opakovače...",
"contacts_searchUsers": "Hľadať {number}{str} používateľov...",
"contacts_searchContactsNoNumber": "Hľadať kontakty...",
- "contacts_unread": "Neprečítané"
+ "contacts_unread": "Neprečítané",
+ "connectionChoiceBluetoothLabel": "Bluetooth",
+ "connectionChoiceUsbLabel": "USB",
+ "connectionChoiceTitle": "Vyberte si metódu prepojenia.",
+ "connectionChoiceSubtitle": "Vyberte si, ako chcete dosiahnuť váš zariadenie MeshCore.",
+ "usbScreenTitle": "Pripojenie cez USB bude k dispozícii čoskoro.",
+ "usbScreenSubtitle": "Vytvárajeme komunikačný systém založený na sériovej komunikácii pre Android a stolné počítače.",
+ "usbScreenStatus": "Čoskoro",
+ "usbScreenNote": "Po implementácii podpory pre USB, budete môcť vybrať sériový port a priamo sa pripojiť k vašmu zariadeniu MeshCore.",
+ "usbScreenEmptyState": "Nenašli sa žiadne USB zariadenia. Pripojte jedno a obnovte."
}
diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb
index 226a715d..416106ac 100644
--- a/lib/l10n/app_sl.arb
+++ b/lib/l10n/app_sl.arb
@@ -1801,5 +1801,14 @@
"contacts_searchRoomServers": "Išči {number}{str} strežnikov sob...",
"contacts_searchContactsNoNumber": "Iskanje stikov...",
"contacts_searchRepeaters": "Išči {number}{str} ponavljalnike...",
- "contacts_searchUsers": "Išči {number}{str} uporabnikov..."
+ "contacts_searchUsers": "Išči {number}{str} uporabnikov...",
+ "connectionChoiceBluetoothLabel": "Bluetooth",
+ "connectionChoiceUsbLabel": "USB",
+ "connectionChoiceTitle": "Izberite svoj način povezave.",
+ "connectionChoiceSubtitle": "Izberite, kako želite dostopati do svojega naprave MeshCore.",
+ "usbScreenTitle": "Vnos preko USB-ja bo v kratkem na voljo.",
+ "usbScreenSubtitle": "Gradimo pot za serijsko povezavo za Android in računalnike.",
+ "usbScreenStatus": "Čez kratko časa",
+ "usbScreenNote": "Ko bo podpora za USB na voljo, boste izbrali serijsko vrata in se neposredno povezali z vašim napravem MeshCore.",
+ "usbScreenEmptyState": "Niti en USB naprave niso bilo najdeno. Povežite eno in posodobite."
}
diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb
index bfccfffb..9bbcd31c 100644
--- a/lib/l10n/app_sv.arb
+++ b/lib/l10n/app_sv.arb
@@ -1801,5 +1801,14 @@
"contacts_searchRepeaters": "Sök {number}{str} upprepningsenheter...",
"contacts_searchFavorites": "Sök {number}{str} Favoriter...",
"contacts_searchUsers": "Sök {number}{str} användare...",
- "contacts_searchRoomServers": "Sök {number}{str} Room-servrar..."
+ "contacts_searchRoomServers": "Sök {number}{str} Room-servrar...",
+ "connectionChoiceUsbLabel": "USB",
+ "connectionChoiceBluetoothLabel": "Bluetooth",
+ "connectionChoiceSubtitle": "Välj hur du vill komma åt din MeshCore-enhet.",
+ "connectionChoiceTitle": "Välj din anslutningsmetod",
+ "usbScreenTitle": "USB-anslutning kommer snart",
+ "usbScreenSubtitle": "Vi skapar en seriebaserad anslutningsväg för både Android- och skrivbordsenheter.",
+ "usbScreenStatus": "Kommer snart",
+ "usbScreenNote": "När USB-stöd är implementerat, kommer du att välja en seriell port och ansluta direkt till din MeshCore-enhet.",
+ "usbScreenEmptyState": "Inga USB-enheter hittades. Anslut en och uppdatera."
}
diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb
index 6063bc87..9a9919cb 100644
--- a/lib/l10n/app_uk.arb
+++ b/lib/l10n/app_uk.arb
@@ -1801,5 +1801,14 @@
"contacts_searchFavorites": "Пошук {number}{str} улюблених...",
"contacts_searchContactsNoNumber": "Пошук контактів...",
"contacts_searchRepeaters": "Пошук {number}{str} ретрансляторів...",
- "contacts_unread": "Непрочитане"
+ "contacts_unread": "Непрочитане",
+ "connectionChoiceSubtitle": "Виберіть, яким способом ви бажаєте отримати доступ до вашого пристрою MeshCore.",
+ "connectionChoiceUsbLabel": "USB",
+ "connectionChoiceTitle": "Виберіть спосіб зв'язку",
+ "connectionChoiceBluetoothLabel": "Bluetooth",
+ "usbScreenTitle": "Підключення через USB буде доступне найближчим часом.",
+ "usbScreenSubtitle": "Ми створюємо серійний шлях з'єднання для Android та десктопних комп'ютерів.",
+ "usbScreenStatus": "Скоро",
+ "usbScreenNote": "Після того, як буде реалізовано підтримку USB, ви виберете серійний порт і підключитесь безпосередньо до вашого пристрою MeshCore.",
+ "usbScreenEmptyState": "Не знайдено жодних пристроїв USB. Підключіть один і перезавантажте."
}
diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb
index db4953ef..660b221a 100644
--- a/lib/l10n/app_zh.arb
+++ b/lib/l10n/app_zh.arb
@@ -1806,5 +1806,14 @@
"contacts_searchRepeaters": "搜索 {number}{str} 重复器...",
"contacts_searchContactsNoNumber": "搜索联系人...",
"contacts_searchRoomServers": "搜索 {number}{str} 房间服务器...",
- "contacts_searchFavorites": "搜索 {number}{str} 收藏..."
+ "contacts_searchFavorites": "搜索 {number}{str} 收藏...",
+ "connectionChoiceSubtitle": "请选择您希望如何访问 MeshCore 设备的选项。",
+ "connectionChoiceBluetoothLabel": "蓝牙",
+ "connectionChoiceTitle": "选择您的连接方式",
+ "connectionChoiceUsbLabel": "USB",
+ "usbScreenTitle": "USB 连接即将推出",
+ "usbScreenSubtitle": "我们正在构建一个基于串行的连接路径,用于Android和桌面设备。",
+ "usbScreenStatus": "即将推出",
+ "usbScreenNote": "一旦USB支持功能上线,您就可以选择一个串口,并直接连接到您的MeshCore设备。",
+ "usbScreenEmptyState": "未找到任何 USB 设备。请插入一个,然后刷新。"
}
diff --git a/lib/main.dart b/lib/main.dart
index 9e53e215..dd503fd8 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -8,7 +8,7 @@ import 'screens/chrome_required_screen.dart';
import 'utils/platform_info.dart';
import 'connector/meshcore_connector.dart';
-import 'screens/scanner_screen.dart';
+import 'screens/connection_choice_screen.dart';
import 'services/storage_service.dart';
import 'services/message_retry_service.dart';
import 'services/path_history_service.dart';
@@ -192,7 +192,7 @@ class MeshCoreApp extends StatelessWidget {
},
home: (PlatformInfo.isWeb && !PlatformInfo.isChrome)
? const ChromeRequiredScreen()
- : const ScannerScreen(),
+ : const ConnectionChoiceScreen(),
);
},
),
diff --git a/lib/screens/connection_choice_screen.dart b/lib/screens/connection_choice_screen.dart
new file mode 100644
index 00000000..a2ea183b
--- /dev/null
+++ b/lib/screens/connection_choice_screen.dart
@@ -0,0 +1,201 @@
+import 'dart:math' as math;
+
+import 'package:flutter/material.dart';
+
+import '../l10n/l10n.dart';
+import 'scanner_screen.dart';
+import 'usb_screen.dart';
+
+/// Entry point that lets the user choose between USB or Bluetooth.
+class ConnectionChoiceScreen extends StatelessWidget {
+ const ConnectionChoiceScreen({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final l10n = context.l10n;
+ final theme = Theme.of(context);
+ return Scaffold(
+ appBar: AppBar(
+ title: FittedBox(
+ fit: BoxFit.scaleDown,
+ child: Text(l10n.appTitle, textAlign: TextAlign.center),
+ ),
+ centerTitle: true,
+ automaticallyImplyLeading: false,
+ ),
+ body: SafeArea(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32),
+ child: LayoutBuilder(
+ builder: (context, constraints) {
+ final availableHeight = constraints.maxHeight.isFinite
+ ? constraints.maxHeight
+ : 600.0;
+ final gap = math.max(
+ 8.0,
+ math.min(20.0, availableHeight * 0.035),
+ );
+
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Flexible(
+ flex: 3,
+ child: Center(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Flexible(
+ child: FittedBox(
+ fit: BoxFit.scaleDown,
+ child: Text(
+ l10n.connectionChoiceTitle,
+ textAlign: TextAlign.center,
+ style: theme.textTheme.headlineSmall?.copyWith(
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ),
+ ),
+ SizedBox(height: math.max(4.0, gap * 0.5)),
+ Flexible(
+ child: FittedBox(
+ fit: BoxFit.scaleDown,
+ child: Text(
+ l10n.connectionChoiceSubtitle,
+ textAlign: TextAlign.center,
+ style: theme.textTheme.bodyMedium?.copyWith(
+ color: theme.colorScheme.onSurfaceVariant,
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ SizedBox(height: gap),
+ Expanded(
+ flex: 4,
+ child: _ConnectionMethodButton(
+ icon: Icons.usb,
+ label: l10n.connectionChoiceUsbLabel,
+ color: theme.colorScheme.primaryContainer,
+ iconColor: theme.colorScheme.onPrimaryContainer,
+ onPressed: () {
+ debugPrint(
+ 'ConnectionChoiceScreen: USB selected, opening UsbScreen',
+ );
+ Navigator.of(context).push(
+ MaterialPageRoute(builder: (_) => const UsbScreen()),
+ );
+ },
+ ),
+ ),
+ SizedBox(height: gap),
+ Expanded(
+ flex: 4,
+ child: _ConnectionMethodButton(
+ icon: Icons.bluetooth,
+ label: l10n.connectionChoiceBluetoothLabel,
+ color: theme.colorScheme.surfaceContainerHighest,
+ iconColor: theme.colorScheme.onSurfaceVariant,
+ onPressed: () {
+ debugPrint(
+ 'ConnectionChoiceScreen: Bluetooth selected, opening ScannerScreen',
+ );
+ Navigator.of(context).push(
+ MaterialPageRoute(
+ builder: (_) => const ScannerScreen(),
+ ),
+ );
+ },
+ ),
+ ),
+ ],
+ );
+ },
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+class _ConnectionMethodButton extends StatelessWidget {
+ const _ConnectionMethodButton({
+ required this.icon,
+ required this.label,
+ required this.onPressed,
+ required this.color,
+ required this.iconColor,
+ });
+
+ final IconData icon;
+ final String label;
+ final VoidCallback onPressed;
+ final Color color;
+ final Color iconColor;
+
+ @override
+ Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+ return ElevatedButton(
+ style: ElevatedButton.styleFrom(
+ backgroundColor: color,
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)),
+ minimumSize: const Size.fromHeight(0),
+ ),
+ onPressed: onPressed,
+ child: LayoutBuilder(
+ builder: (context, constraints) {
+ final availableHeight = constraints.maxHeight.isFinite
+ ? constraints.maxHeight
+ : 200.0;
+ final availableWidth = constraints.maxWidth.isFinite
+ ? constraints.maxWidth
+ : 320.0;
+ final isCompact = availableHeight < 72.0 || availableWidth < 180.0;
+ final baseGap = isCompact ? 8.0 : 12.0;
+ final content = Flex(
+ direction: isCompact ? Axis.horizontal : Axis.vertical,
+ mainAxisSize: MainAxisSize.min,
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(icon, size: isCompact ? 24.0 : 60.0, color: iconColor),
+ SizedBox(
+ width: isCompact ? baseGap : 0,
+ height: isCompact ? 0 : baseGap,
+ ),
+ Text(
+ label,
+ textAlign: TextAlign.center,
+ maxLines: 1,
+ overflow: TextOverflow.visible,
+ style:
+ (isCompact
+ ? theme.textTheme.titleMedium
+ : theme.textTheme.titleLarge)
+ ?.copyWith(fontWeight: FontWeight.w600),
+ ),
+ ],
+ );
+
+ return Center(
+ child: FittedBox(
+ fit: BoxFit.scaleDown,
+ child: ConstrainedBox(
+ constraints: BoxConstraints(
+ maxWidth: math.max(0, availableWidth - 12),
+ maxHeight: math.max(0, availableHeight - 12),
+ ),
+ child: content,
+ ),
+ ),
+ );
+ },
+ ),
+ );
+ }
+}
diff --git a/lib/screens/scanner_screen.dart b/lib/screens/scanner_screen.dart
index 4017408c..713239ff 100644
--- a/lib/screens/scanner_screen.dart
+++ b/lib/screens/scanner_screen.dart
@@ -20,6 +20,7 @@ class ScannerScreen extends StatefulWidget {
class _ScannerScreenState extends State {
bool _changedNavigation = false;
+ late final MeshCoreConnector _connector;
late final VoidCallback _connectionListener;
BluetoothAdapterState _bluetoothState = BluetoothAdapterState.unknown;
late StreamSubscription _bluetoothStateSubscription;
@@ -27,12 +28,12 @@ class _ScannerScreenState extends State {
@override
void initState() {
super.initState();
- final connector = Provider.of(context, listen: false);
+ _connector = Provider.of(context, listen: false);
_connectionListener = () {
- if (connector.state == MeshCoreConnectionState.disconnected) {
+ if (_connector.state == MeshCoreConnectionState.disconnected) {
_changedNavigation = false;
- } else if (connector.state == MeshCoreConnectionState.connected &&
+ } else if (_connector.state == MeshCoreConnectionState.connected &&
!_changedNavigation) {
_changedNavigation = true;
if (mounted) {
@@ -43,7 +44,7 @@ class _ScannerScreenState extends State {
}
};
- connector.addListener(_connectionListener);
+ _connector.addListener(_connectionListener);
_bluetoothStateSubscription = FlutterBluePlus.adapterState.listen(
(state) {
@@ -53,7 +54,7 @@ class _ScannerScreenState extends State {
});
// Cancel scan if Bluetooth turns off while scanning
if (state != BluetoothAdapterState.on) {
- unawaited(connector.stopScan());
+ unawaited(_connector.stopScan());
}
}
},
@@ -65,16 +66,25 @@ class _ScannerScreenState extends State {
@override
void dispose() {
- final connector = Provider.of(context, listen: false);
- connector.removeListener(_connectionListener);
+ _connector.removeListener(_connectionListener);
unawaited(_bluetoothStateSubscription.cancel());
super.dispose();
}
@override
Widget build(BuildContext context) {
+ final canPop = Navigator.of(context).canPop();
return Scaffold(
appBar: AppBar(
+ leading: canPop
+ ? IconButton(
+ icon: const Icon(Icons.arrow_back),
+ onPressed: () {
+ debugPrint('ScannerScreen: back button pressed');
+ Navigator.of(context).maybePop();
+ },
+ )
+ : null,
title: AdaptiveAppBarTitle(context.l10n.scanner_title),
centerTitle: true,
automaticallyImplyLeading: false,
diff --git a/lib/screens/usb_screen.dart b/lib/screens/usb_screen.dart
new file mode 100644
index 00000000..e542d616
--- /dev/null
+++ b/lib/screens/usb_screen.dart
@@ -0,0 +1,456 @@
+import 'dart:async';
+import 'dart:math' as math;
+
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+
+import '../connector/meshcore_connector.dart';
+import '../l10n/l10n.dart';
+import 'contacts_screen.dart';
+
+class UsbScreen extends StatefulWidget {
+ const UsbScreen({super.key});
+
+ @override
+ State createState() => _UsbScreenState();
+}
+
+class _UsbScreenState extends State {
+ final List _ports = [];
+ bool _isLoadingPorts = true;
+ bool _isConnecting = false;
+ bool _navigatedToContacts = false;
+ String? _selectedPort;
+ String? _errorText;
+ late final MeshCoreConnector _connector;
+ late final VoidCallback _connectionListener;
+
+ @override
+ void initState() {
+ super.initState();
+ _connector = context.read();
+ _connectionListener = () {
+ if (!mounted) return;
+ if (_connector.state == MeshCoreConnectionState.disconnected) {
+ _navigatedToContacts = false;
+ if (_isConnecting) {
+ setState(() {
+ _isConnecting = false;
+ });
+ }
+ }
+ if (_connector.state == MeshCoreConnectionState.connected &&
+ _connector.isUsbTransportConnected &&
+ !_navigatedToContacts) {
+ _navigatedToContacts = true;
+ Navigator.of(context).pushReplacement(
+ MaterialPageRoute(builder: (_) => const ContactsScreen()),
+ );
+ }
+ };
+ _connector.addListener(_connectionListener);
+ unawaited(_loadPorts());
+ }
+
+ @override
+ void dispose() {
+ _connector.removeListener(_connectionListener);
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+ final l10n = context.l10n;
+
+ return Scaffold(
+ appBar: AppBar(
+ leading: IconButton(
+ icon: const Icon(Icons.arrow_back),
+ onPressed: () {
+ debugPrint('UsbScreen: back button pressed');
+ Navigator.of(context).maybePop();
+ },
+ ),
+ title: Text(
+ l10n.connectionChoiceUsbLabel,
+ style: theme.textTheme.titleLarge,
+ ),
+ centerTitle: true,
+ ),
+ body: SafeArea(
+ child: LayoutBuilder(
+ builder: (context, constraints) {
+ final availableHeight = constraints.maxHeight.isFinite
+ ? constraints.maxHeight
+ : 600.0;
+ final availableWidth = constraints.maxWidth.isFinite
+ ? constraints.maxWidth
+ : 800.0;
+ final gap = math.max(8.0, math.min(16.0, availableHeight * 0.025));
+ final iconSize = math.max(
+ 28.0,
+ math.min(72.0, availableHeight * 0.12),
+ );
+ final isNarrow = availableWidth < 460.0;
+
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Flexible(
+ flex: 3,
+ child: Center(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(
+ Icons.usb,
+ size: iconSize,
+ color: theme.colorScheme.primary,
+ ),
+ SizedBox(height: gap),
+ Flexible(
+ child: FittedBox(
+ fit: BoxFit.scaleDown,
+ child: Text(
+ l10n.usbScreenTitle,
+ textAlign: TextAlign.center,
+ style: theme.textTheme.headlineSmall?.copyWith(
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ),
+ ),
+ SizedBox(height: math.max(4.0, gap * 0.5)),
+ Flexible(
+ child: FittedBox(
+ fit: BoxFit.scaleDown,
+ child: Text(
+ l10n.usbScreenSubtitle,
+ textAlign: TextAlign.center,
+ style: theme.textTheme.bodyMedium?.copyWith(
+ color: theme.colorScheme.onSurfaceVariant,
+ ),
+ ),
+ ),
+ ),
+ SizedBox(height: gap),
+ FittedBox(
+ fit: BoxFit.scaleDown,
+ child: Chip(
+ label: Text(
+ _selectedPort == null
+ ? l10n.usbScreenStatus
+ : _friendlyPortName(_selectedPort!),
+ overflow: TextOverflow.ellipsis,
+ ),
+ backgroundColor:
+ theme.colorScheme.surfaceContainerHighest,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ SizedBox(height: gap),
+ Expanded(child: _buildPortList(context)),
+ if (_errorText != null) ...[
+ SizedBox(height: gap),
+ Flexible(
+ child: FittedBox(
+ fit: BoxFit.scaleDown,
+ child: Text(
+ _errorText!,
+ textAlign: TextAlign.center,
+ style: theme.textTheme.bodySmall?.copyWith(
+ color: theme.colorScheme.error,
+ ),
+ ),
+ ),
+ ),
+ ],
+ SizedBox(height: gap),
+ if (isNarrow)
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ OutlinedButton.icon(
+ onPressed: _isLoadingPorts || _isConnecting
+ ? null
+ : () {
+ debugPrint(
+ 'UsbScreen: refresh ports pressed',
+ );
+ _loadPorts();
+ },
+ icon: const Icon(Icons.refresh),
+ label: Text(l10n.repeater_refresh),
+ ),
+ SizedBox(height: gap),
+ FilledButton.icon(
+ onPressed: _canConnect
+ ? () {
+ final rawPortName = _normalizedPortName(
+ _selectedPort!,
+ );
+ debugPrint(
+ 'UsbScreen: connect pressed for $_selectedPort (raw: $rawPortName)',
+ );
+ _connectSelectedPort();
+ }
+ : null,
+ icon: _isConnecting
+ ? const SizedBox(
+ width: 18,
+ height: 18,
+ child: CircularProgressIndicator(
+ strokeWidth: 2,
+ ),
+ )
+ : const Icon(Icons.usb),
+ label: Text(l10n.common_connect),
+ ),
+ ],
+ )
+ else
+ Row(
+ children: [
+ Expanded(
+ child: OutlinedButton.icon(
+ onPressed: _isLoadingPorts || _isConnecting
+ ? null
+ : () {
+ debugPrint(
+ 'UsbScreen: refresh ports pressed',
+ );
+ _loadPorts();
+ },
+ icon: const Icon(Icons.refresh),
+ label: Text(l10n.repeater_refresh),
+ ),
+ ),
+ SizedBox(width: gap),
+ Expanded(
+ child: FilledButton.icon(
+ onPressed: _canConnect
+ ? () {
+ final rawPortName = _normalizedPortName(
+ _selectedPort!,
+ );
+ debugPrint(
+ 'UsbScreen: connect pressed for $_selectedPort (raw: $rawPortName)',
+ );
+ _connectSelectedPort();
+ }
+ : null,
+ icon: _isConnecting
+ ? const SizedBox(
+ width: 18,
+ height: 18,
+ child: CircularProgressIndicator(
+ strokeWidth: 2,
+ ),
+ )
+ : const Icon(Icons.usb),
+ label: Text(l10n.common_connect),
+ ),
+ ),
+ ],
+ ),
+ SizedBox(height: math.max(4.0, gap * 0.75)),
+ Flexible(
+ child: FittedBox(
+ fit: BoxFit.scaleDown,
+ child: Text(
+ l10n.usbScreenNote,
+ textAlign: TextAlign.center,
+ style: theme.textTheme.bodySmall?.copyWith(
+ color: theme.colorScheme.onSurfaceVariant,
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ },
+ ),
+ ),
+ );
+ }
+
+ bool get _canConnect =>
+ !_isLoadingPorts &&
+ !_isConnecting &&
+ _selectedPort != null &&
+ _selectedPort!.isNotEmpty;
+
+ Widget _buildPortList(BuildContext context) {
+ final theme = Theme.of(context);
+ final l10n = context.l10n;
+
+ if (_isLoadingPorts) {
+ return Center(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ const CircularProgressIndicator(),
+ const SizedBox(height: 12),
+ Text(l10n.common_loading),
+ ],
+ ),
+ );
+ }
+
+ if (_ports.isEmpty) {
+ return Center(
+ child: Text(
+ l10n.usbScreenEmptyState,
+ textAlign: TextAlign.center,
+ style: theme.textTheme.bodyMedium?.copyWith(
+ color: theme.colorScheme.onSurfaceVariant,
+ ),
+ ),
+ );
+ }
+
+ return ListView.separated(
+ itemCount: _ports.length,
+ itemBuilder: (context, index) {
+ final port = _ports[index];
+ final isSelected = port == _selectedPort;
+ final displayName = _friendlyPortName(port);
+ final rawName = _normalizedPortName(port);
+ final showRawName = rawName != displayName;
+ return Material(
+ color: isSelected
+ ? theme.colorScheme.primaryContainer
+ : theme.colorScheme.surfaceContainerLow,
+ borderRadius: BorderRadius.circular(16),
+ child: ListTile(
+ onTap: _isConnecting
+ ? null
+ : () {
+ setState(() {
+ _selectedPort = port;
+ _errorText = null;
+ });
+ debugPrint('UsbScreen: selected port $port');
+ },
+ leading: Icon(
+ Icons.usb,
+ color: isSelected
+ ? theme.colorScheme.onPrimaryContainer
+ : theme.colorScheme.onSurfaceVariant,
+ ),
+ title: Text(
+ displayName,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ style: theme.textTheme.titleMedium?.copyWith(
+ color: isSelected ? theme.colorScheme.onPrimaryContainer : null,
+ ),
+ ),
+ subtitle: showRawName
+ ? Text(
+ rawName,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ style: theme.textTheme.bodySmall?.copyWith(
+ color: isSelected
+ ? theme.colorScheme.onPrimaryContainer
+ : theme.colorScheme.onSurfaceVariant,
+ ),
+ )
+ : null,
+ trailing: isSelected
+ ? Icon(
+ Icons.check_circle,
+ color: theme.colorScheme.onPrimaryContainer,
+ )
+ : null,
+ ),
+ );
+ },
+ separatorBuilder: (context, index) => const SizedBox(height: 10),
+ );
+ }
+
+ Future _loadPorts() async {
+ if (!mounted) return;
+
+ setState(() {
+ _isLoadingPorts = true;
+ _errorText = null;
+ });
+
+ try {
+ final ports = await _connector.listUsbPorts();
+ if (!mounted) return;
+ setState(() {
+ _ports
+ ..clear()
+ ..addAll(ports);
+ if (_ports.isEmpty) {
+ _selectedPort = null;
+ } else if (!_ports.contains(_selectedPort)) {
+ _selectedPort = _ports.first;
+ }
+ _isLoadingPorts = false;
+ });
+ } catch (error) {
+ if (!mounted) return;
+ setState(() {
+ _ports.clear();
+ _selectedPort = null;
+ _errorText = error.toString();
+ _isLoadingPorts = false;
+ });
+ }
+ }
+
+ Future _connectSelectedPort() async {
+ final selectedPort = _selectedPort;
+ if (selectedPort == null || selectedPort.isEmpty) {
+ return;
+ }
+ final rawPortName = _normalizedPortName(selectedPort);
+
+ setState(() {
+ _isConnecting = true;
+ _errorText = null;
+ });
+
+ try {
+ await _connector.connectUsb(portName: rawPortName);
+ } catch (error) {
+ if (!mounted) return;
+ setState(() {
+ _isConnecting = false;
+ _errorText = error.toString();
+ });
+ }
+ }
+
+ String _normalizedPortName(String portLabel) {
+ final separatorIndex = portLabel.indexOf(' - ');
+ final normalized = separatorIndex >= 0
+ ? portLabel.substring(0, separatorIndex)
+ : portLabel;
+ return normalized.trim();
+ }
+
+ String _friendlyPortName(String portLabel) {
+ final separatorIndex = portLabel.indexOf(' - ');
+ if (separatorIndex < 0) {
+ return portLabel.trim();
+ }
+ final friendlyName = portLabel.substring(separatorIndex + 3).trim();
+ if (friendlyName.isEmpty) {
+ return _normalizedPortName(portLabel);
+ }
+ return friendlyName;
+ }
+}
diff --git a/lib/services/usb_serial_service.dart b/lib/services/usb_serial_service.dart
new file mode 100644
index 00000000..7e9027df
--- /dev/null
+++ b/lib/services/usb_serial_service.dart
@@ -0,0 +1,284 @@
+import 'dart:async';
+
+import 'package:flserial/flserial.dart';
+import 'package:flserial/flserial_exception.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/services.dart';
+
+/// Wraps the native flserial plugin to expose a stream of raw bytes for the
+/// MeshCore connector to consume.
+class UsbSerialService {
+ UsbSerialService();
+
+ static const MethodChannel _androidMethodChannel = MethodChannel(
+ 'meshcore_open/android_usb_serial',
+ );
+ static const EventChannel _androidEventChannel = EventChannel(
+ 'meshcore_open/android_usb_serial_events',
+ );
+ static const int _serialTxFrameStart = 0x3c;
+ static const int _serialRxFrameStart = 0x3e;
+ static const int _serialHeaderLength = 3;
+
+ final StreamController _frameController =
+ StreamController.broadcast();
+ final FlSerial _serial = FlSerial();
+ final List _rxBuffer = [];
+ StreamSubscription? _androidDataSubscription;
+ StreamSubscription? _dataSubscription;
+ UsbSerialStatus _status = UsbSerialStatus.disconnected;
+ String? _connectedPortName;
+
+ UsbSerialStatus get status => _status;
+ String? get activePortName => _connectedPortName;
+ Stream get frameStream => _frameController.stream;
+ bool get _useAndroidUsbHost =>
+ !kIsWeb && defaultTargetPlatform == TargetPlatform.android;
+
+ bool get isConnected {
+ if (_useAndroidUsbHost) {
+ return _status == UsbSerialStatus.connected;
+ }
+ return _status == UsbSerialStatus.connected &&
+ _serial.isOpen() == FlOpenStatus.open;
+ }
+
+ Future> listPorts() async {
+ if (_useAndroidUsbHost) {
+ final ports = await _androidMethodChannel.invokeListMethod(
+ 'listPorts',
+ );
+ return ports ?? [];
+ }
+ return Future.value(FlSerial.listPorts());
+ }
+
+ Future connect({
+ required String portName,
+ int baudRate = 115200,
+ }) async {
+ if (_status == UsbSerialStatus.connected ||
+ _status == UsbSerialStatus.connecting) {
+ throw StateError('USB serial transport is already active');
+ }
+
+ _status = UsbSerialStatus.connecting;
+ final normalizedPortName = _normalizePortName(portName);
+
+ if (_useAndroidUsbHost) {
+ try {
+ await _androidMethodChannel.invokeMethod('connect', {
+ 'portName': normalizedPortName,
+ 'baudRate': baudRate,
+ });
+ debugPrint(
+ 'USB serial opened port=$normalizedPortName on Android via USB host bridge',
+ );
+ } on PlatformException catch (error) {
+ _status = UsbSerialStatus.disconnected;
+ throw StateError(error.message ?? error.code);
+ }
+ } else {
+ _serial.init();
+
+ try {
+ final status = _serial.openPort(normalizedPortName, baudRate);
+ if (status != FlOpenStatus.open) {
+ throw StateError(
+ 'Failed to open USB port $normalizedPortName ($status)',
+ );
+ }
+ _serial.setByteSize8();
+ _serial.setBitParityNone();
+ _serial.setStopBits1();
+ _serial.setFlowControlNone();
+ _serial.setRTS(false);
+ _serial.setDTR(true);
+ debugPrint(
+ 'USB serial opened port=$normalizedPortName cts=${_serial.getCTS()} dsr=${_serial.getDSR()} dtr=true rts=false',
+ );
+ } on FlSerialException catch (error) {
+ _serial.free();
+ _status = UsbSerialStatus.disconnected;
+ throw StateError(
+ 'Failed to open USB port $normalizedPortName: ${error.msg} (${error.error})',
+ );
+ } catch (error) {
+ _serial.free();
+ _status = UsbSerialStatus.disconnected;
+ rethrow;
+ }
+ }
+
+ _connectedPortName = normalizedPortName;
+ if (_useAndroidUsbHost) {
+ _androidDataSubscription = _androidEventChannel
+ .receiveBroadcastStream()
+ .listen(
+ _handleAndroidData,
+ onError: _handleSerialError,
+ onDone: _handleSerialDone,
+ );
+ } else {
+ _dataSubscription = _serial.onSerialData.stream.listen(
+ _handleSerialData,
+ onError: _handleSerialError,
+ onDone: _handleSerialDone,
+ );
+ }
+ _status = UsbSerialStatus.connected;
+ }
+
+ Future write(Uint8List data) async {
+ if (!isConnected) {
+ throw StateError('USB serial port is not open');
+ }
+ final packet = Uint8List(_serialHeaderLength + data.length);
+ packet[0] = _serialTxFrameStart;
+ packet[1] = data.length & 0xff;
+ packet[2] = (data.length >> 8) & 0xff;
+ packet.setRange(_serialHeaderLength, packet.length, data);
+ _logFrameSummary('USB TX frame', data);
+ if (_useAndroidUsbHost) {
+ try {
+ await _androidMethodChannel.invokeMethod('write', {
+ 'data': packet,
+ });
+ } on PlatformException catch (error) {
+ throw StateError(error.message ?? error.code);
+ }
+ } else {
+ _serial.write(packet);
+ }
+ }
+
+ Future disconnect() async {
+ if (_status == UsbSerialStatus.disconnected) return;
+
+ _status = UsbSerialStatus.disconnecting;
+ _connectedPortName = null;
+ await _androidDataSubscription?.cancel();
+ _androidDataSubscription = null;
+ await _dataSubscription?.cancel();
+ _dataSubscription = null;
+
+ if (_useAndroidUsbHost) {
+ try {
+ await _androidMethodChannel.invokeMethod('disconnect');
+ } catch (_) {
+ // Ignore errors while closing.
+ }
+ } else {
+ try {
+ if (_serial.isOpen() == FlOpenStatus.open) {
+ _serial.closePort();
+ }
+ } catch (_) {
+ // Ignore errors while closing.
+ }
+
+ _serial.free();
+ }
+ _status = UsbSerialStatus.disconnected;
+ }
+
+ void dispose() {
+ unawaited(disconnect());
+ unawaited(_frameController.close());
+ }
+
+ void _handleSerialData(FlSerialEventArgs event) {
+ try {
+ final bytes = event.serial.readList();
+ if (bytes.isNotEmpty) {
+ _ingestRawBytes(Uint8List.fromList(bytes));
+ }
+ } catch (error, stack) {
+ _frameController.addError(error, stack);
+ }
+ }
+
+ void _handleAndroidData(dynamic data) {
+ if (data is Uint8List) {
+ _ingestRawBytes(data);
+ return;
+ }
+ if (data is ByteData) {
+ _ingestRawBytes(data.buffer.asUint8List());
+ return;
+ }
+ _frameController.addError(
+ StateError('Unexpected Android USB event payload: ${data.runtimeType}'),
+ );
+ }
+
+ void _handleSerialError(Object error, [StackTrace? stackTrace]) {
+ _frameController.addError(error, stackTrace);
+ }
+
+ void _handleSerialDone() {
+ unawaited(disconnect());
+ }
+
+ String _normalizePortName(String portName) {
+ final separatorIndex = portName.indexOf(' - ');
+ final normalized = separatorIndex >= 0
+ ? portName.substring(0, separatorIndex)
+ : portName;
+ return normalized.trim();
+ }
+
+ void _ingestRawBytes(Uint8List bytes) {
+ if (bytes.isEmpty) {
+ return;
+ }
+ _rxBuffer.addAll(bytes);
+ _drainRxBuffer();
+ }
+
+ void _drainRxBuffer() {
+ while (true) {
+ if (_rxBuffer.isEmpty) {
+ return;
+ }
+
+ if (_rxBuffer.first != _serialRxFrameStart &&
+ _rxBuffer.first != _serialTxFrameStart) {
+ _rxBuffer.removeAt(0);
+ continue;
+ }
+
+ if (_rxBuffer.length < _serialHeaderLength) {
+ return;
+ }
+
+ final payloadLength = _rxBuffer[1] | (_rxBuffer[2] << 8);
+ final packetLength = _serialHeaderLength + payloadLength;
+ if (_rxBuffer.length < packetLength) {
+ return;
+ }
+
+ final frameStart = _rxBuffer.first;
+ final payload = Uint8List.fromList(
+ _rxBuffer.sublist(_serialHeaderLength, packetLength),
+ );
+ _rxBuffer.removeRange(0, packetLength);
+ if (frameStart != _serialRxFrameStart) {
+ debugPrint(
+ 'USB ignored packet start=0x${frameStart.toRadixString(16).padLeft(2, '0')} len=${payload.length}',
+ );
+ }
+ _frameController.add(payload);
+ }
+ }
+
+ void _logFrameSummary(String prefix, Uint8List bytes) {
+ if (bytes.isEmpty) {
+ debugPrint('$prefix len=0');
+ return;
+ }
+ debugPrint('$prefix code=${bytes[0]} len=${bytes.length}');
+ }
+}
+
+enum UsbSerialStatus { disconnected, connecting, connected, disconnecting }
diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake
index f16b4c34..379e36fa 100644
--- a/linux/flutter/generated_plugins.cmake
+++ b/linux/flutter/generated_plugins.cmake
@@ -7,6 +7,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
+ flserial
)
set(PLUGIN_BUNDLED_LIBRARIES)
diff --git a/pubspec.yaml b/pubspec.yaml
index f85530f4..9e82770f 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -38,6 +38,7 @@ dependencies:
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
flutter_blue_plus: ^2.1.0
+ flserial: ^0.3.5
provider: ^6.1.5+1
shared_preferences: ^2.2.2
uuid: ^4.3.3
diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt
index daf32c2f..97c813c0 100644
--- a/windows/CMakeLists.txt
+++ b/windows/CMakeLists.txt
@@ -89,9 +89,11 @@ endif()
# Copy the native assets provided by the build.dart from all packages.
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/")
-install(DIRECTORY "${NATIVE_ASSETS_DIR}"
- DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
- COMPONENT Runtime)
+if(EXISTS "${NATIVE_ASSETS_DIR}")
+ install(DIRECTORY "${NATIVE_ASSETS_DIR}"
+ DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+ COMPONENT Runtime)
+endif()
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake
index 4c358e7f..f02857f4 100644
--- a/windows/flutter/generated_plugins.cmake
+++ b/windows/flutter/generated_plugins.cmake
@@ -9,6 +9,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
+ flserial
flutter_local_notifications_windows
)