Initialize USB Supoport for Andriod and Desktop

This commit is contained in:
just_stuff_tm
2026-03-01 23:08:51 -05:00
committed by just-stuff-tm
parent 7d8e049745
commit 22a53439b1
45 changed files with 2083 additions and 47 deletions
+2 -1
View File
@@ -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")
}
+1
View File
@@ -19,6 +19,7 @@
<!-- Camera permission for QR code scanning -->
<uses-permission android:name="android.permission.CAMERA"/>
<uses-feature android:name="android.hardware.camera" android:required="false"/>
<uses-feature android:name="android.hardware.usb.host" android:required="false"/>
<application
android:label="meshcore_open"
@@ -1,5 +1,313 @@
package com.meshcore.meshcore_open
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbDeviceConnection
import android.hardware.usb.UsbManager
import android.os.Build
import android.os.Handler
import android.os.Looper
import com.hoho.android.usbserial.driver.UsbSerialDriver
import com.hoho.android.usbserial.driver.UsbSerialPort
import com.hoho.android.usbserial.driver.UsbSerialProber
import com.hoho.android.usbserial.util.SerialInputOutputManager
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import java.util.Locale
class MainActivity : FlutterActivity()
class MainActivity : FlutterActivity() {
private val usbMethodChannelName = "meshcore_open/android_usb_serial"
private val usbEventChannelName = "meshcore_open/android_usb_serial_events"
private val usbPermissionAction = "com.meshcore.meshcore_open.USB_PERMISSION"
private lateinit var usbManager: UsbManager
private val mainHandler = Handler(Looper.getMainLooper())
private var eventSink: EventChannel.EventSink? = null
private var usbConnection: UsbDeviceConnection? = null
private var usbPort: UsbSerialPort? = null
private var ioManager: SerialInputOutputManager? = null
private var pendingConnectResult: MethodChannel.Result? = null
private var pendingConnectPortName: String? = null
private var pendingConnectBaudRate: Int = 115200
private val permissionReceiver =
object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action != usbPermissionAction) {
return
}
val result = pendingConnectResult
val portName = pendingConnectPortName
pendingConnectResult = null
pendingConnectPortName = null
if (result == null || portName == null) {
return
}
val granted =
intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)
if (!granted) {
result.error("usb_permission_denied", "USB permission denied", null)
return
}
val device = findUsbDevice(portName)
if (device == null) {
result.error(
"usb_device_missing",
"USB device no longer available for $portName",
null,
)
return
}
openUsbDevice(device, pendingConnectBaudRate, result)
}
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
usbManager = getSystemService(Context.USB_SERVICE) as UsbManager
registerUsbPermissionReceiver()
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, usbMethodChannelName)
.setMethodCallHandler { call, result ->
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<String> {
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<String>("portName")
val baudRate = call.argument<Int>("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<ByteArray>("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
}
}
+1
View File
@@ -2,6 +2,7 @@ allprojects {
repositories {
google()
mavenCentral()
maven(url = "https://jitpack.io")
}
}
+132 -19
View File
@@ -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<Uint8List>? _usbFrameSubscription;
MeshCoreTransportType _activeTransport = MeshCoreTransportType.bluetooth;
String? _activeUsbPort;
final List<ScanResult> _scanResults = [];
final List<Contact> _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<List<String>> listUsbPorts() => _usbSerialService.listPorts();
Future<void> 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<void> 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<void>.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<bool> _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<void> 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();
+10 -1
View File
@@ -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 устройства. Включете едно и опитайте отново."
}
+10 -1
View File
@@ -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."
}
+9
View File
@@ -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...",
+10 -1
View File
@@ -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."
}
+10 -1
View File
@@ -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."
}
+10 -1
View File
@@ -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."
}
+54
View File
@@ -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:
+31
View File
@@ -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 => 'Сканиране за устройства...';
+32
View File
@@ -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...';
+31
View File
@@ -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...';
+32
View File
@@ -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...';
+32
View File
@@ -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...';
+32
View File
@@ -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...';
+31
View File
@@ -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...';
+31
View File
@@ -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ń...';
+31
View File
@@ -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...';
+32
View File
@@ -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 => 'Поиск устройств...';
+31
View File
@@ -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í...';
+31
View File
@@ -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...';
+31
View File
@@ -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...';
+32
View File
@@ -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 => 'Пошук пристроїв...';
+27
View File
@@ -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 => '正在搜索设备...';
+10 -1
View File
@@ -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."
}
+10 -1
View File
@@ -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."
}
+10 -1
View File
@@ -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."
}
+10 -1
View File
@@ -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. Подключите одно из них и обновите список."
}
+10 -1
View File
@@ -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."
}
+10 -1
View File
@@ -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."
}
+10 -1
View File
@@ -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."
}
+10 -1
View File
@@ -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. Підключіть один і перезавантажте."
}
+10 -1
View File
@@ -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 设备。请插入一个,然后刷新。"
}
+2 -2
View File
@@ -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(),
);
},
),
+201
View File
@@ -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,
),
),
);
},
),
);
}
}
+17 -7
View File
@@ -20,6 +20,7 @@ class ScannerScreen extends StatefulWidget {
class _ScannerScreenState extends State<ScannerScreen> {
bool _changedNavigation = false;
late final MeshCoreConnector _connector;
late final VoidCallback _connectionListener;
BluetoothAdapterState _bluetoothState = BluetoothAdapterState.unknown;
late StreamSubscription<BluetoothAdapterState> _bluetoothStateSubscription;
@@ -27,12 +28,12 @@ class _ScannerScreenState extends State<ScannerScreen> {
@override
void initState() {
super.initState();
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
_connector = Provider.of<MeshCoreConnector>(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<ScannerScreen> {
}
};
connector.addListener(_connectionListener);
_connector.addListener(_connectionListener);
_bluetoothStateSubscription = FlutterBluePlus.adapterState.listen(
(state) {
@@ -53,7 +54,7 @@ class _ScannerScreenState extends State<ScannerScreen> {
});
// 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<ScannerScreen> {
@override
void dispose() {
final connector = Provider.of<MeshCoreConnector>(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,
+456
View File
@@ -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<UsbScreen> createState() => _UsbScreenState();
}
class _UsbScreenState extends State<UsbScreen> {
final List<String> _ports = <String>[];
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<MeshCoreConnector>();
_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<void> _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<void> _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;
}
}
+284
View File
@@ -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<Uint8List> _frameController =
StreamController<Uint8List>.broadcast();
final FlSerial _serial = FlSerial();
final List<int> _rxBuffer = <int>[];
StreamSubscription<dynamic>? _androidDataSubscription;
StreamSubscription<FlSerialEventArgs>? _dataSubscription;
UsbSerialStatus _status = UsbSerialStatus.disconnected;
String? _connectedPortName;
UsbSerialStatus get status => _status;
String? get activePortName => _connectedPortName;
Stream<Uint8List> 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<List<String>> listPorts() async {
if (_useAndroidUsbHost) {
final ports = await _androidMethodChannel.invokeListMethod<String>(
'listPorts',
);
return ports ?? <String>[];
}
return Future.value(FlSerial.listPorts());
}
Future<void> 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<void>('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<void> 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<void>('write', {
'data': packet,
});
} on PlatformException catch (error) {
throw StateError(error.message ?? error.code);
}
} else {
_serial.write(packet);
}
}
Future<void> 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<void>('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 }
+1
View File
@@ -7,6 +7,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
flserial
)
set(PLUGIN_BUNDLED_LIBRARIES)
+1
View File
@@ -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
+5 -3
View File
@@ -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.
+1
View File
@@ -9,6 +9,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
flserial
flutter_local_notifications_windows
)