mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-07-29 14:58:44 +10:00
Initialize USB Supoport for Andriod and Desktop
This commit is contained in:
committed by
just-stuff-tm
parent
7d8e049745
commit
22a53439b1
@@ -16,7 +16,7 @@ if (keystorePropertiesFile.exists()) {
|
|||||||
android {
|
android {
|
||||||
namespace = "com.meshcore.meshcore_open"
|
namespace = "com.meshcore.meshcore_open"
|
||||||
compileSdk = flutter.compileSdkVersion
|
compileSdk = flutter.compileSdkVersion
|
||||||
ndkVersion = flutter.ndkVersion
|
ndkVersion = "29.0.14206865"
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
@@ -84,4 +84,5 @@ flutter {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
|
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
|
||||||
|
implementation("com.github.mik3y:usb-serial-for-android:3.9.0")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
<!-- Camera permission for QR code scanning -->
|
<!-- Camera permission for QR code scanning -->
|
||||||
<uses-permission android:name="android.permission.CAMERA"/>
|
<uses-permission android:name="android.permission.CAMERA"/>
|
||||||
<uses-feature android:name="android.hardware.camera" android:required="false"/>
|
<uses-feature android:name="android.hardware.camera" android:required="false"/>
|
||||||
|
<uses-feature android:name="android.hardware.usb.host" android:required="false"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="meshcore_open"
|
android:label="meshcore_open"
|
||||||
|
|||||||
@@ -1,5 +1,313 @@
|
|||||||
package com.meshcore.meshcore_open
|
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.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ allprojects {
|
|||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
maven(url = "https://jitpack.io")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import '../services/path_history_service.dart';
|
|||||||
import '../services/app_settings_service.dart';
|
import '../services/app_settings_service.dart';
|
||||||
import '../services/background_service.dart';
|
import '../services/background_service.dart';
|
||||||
import '../services/notification_service.dart';
|
import '../services/notification_service.dart';
|
||||||
|
import '../services/usb_serial_service.dart';
|
||||||
import '../storage/channel_message_store.dart';
|
import '../storage/channel_message_store.dart';
|
||||||
import '../storage/channel_order_store.dart';
|
import '../storage/channel_order_store.dart';
|
||||||
import '../storage/channel_settings_store.dart';
|
import '../storage/channel_settings_store.dart';
|
||||||
@@ -82,6 +83,8 @@ enum MeshCoreConnectionState {
|
|||||||
disconnecting,
|
disconnecting,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum MeshCoreTransportType { bluetooth, usb }
|
||||||
|
|
||||||
class RepeaterBatterySnapshot {
|
class RepeaterBatterySnapshot {
|
||||||
final int millivolts;
|
final int millivolts;
|
||||||
final DateTime updatedAt;
|
final DateTime updatedAt;
|
||||||
@@ -108,6 +111,10 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
String? _lastDeviceId;
|
String? _lastDeviceId;
|
||||||
String? _lastDeviceDisplayName;
|
String? _lastDeviceDisplayName;
|
||||||
bool _manualDisconnect = false;
|
bool _manualDisconnect = false;
|
||||||
|
final UsbSerialService _usbSerialService = UsbSerialService();
|
||||||
|
StreamSubscription<Uint8List>? _usbFrameSubscription;
|
||||||
|
MeshCoreTransportType _activeTransport = MeshCoreTransportType.bluetooth;
|
||||||
|
String? _activeUsbPort;
|
||||||
|
|
||||||
final List<ScanResult> _scanResults = [];
|
final List<ScanResult> _scanResults = [];
|
||||||
final List<Contact> _contacts = [];
|
final List<Contact> _contacts = [];
|
||||||
@@ -154,6 +161,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
bool _hasLoadedChannels = false;
|
bool _hasLoadedChannels = false;
|
||||||
bool _batteryRequested = false;
|
bool _batteryRequested = false;
|
||||||
bool _awaitingSelfInfo = false;
|
bool _awaitingSelfInfo = false;
|
||||||
|
bool _hasReceivedDeviceInfo = false;
|
||||||
|
bool _pendingInitialChannelSync = false;
|
||||||
bool _preserveContactsOnRefresh = false;
|
bool _preserveContactsOnRefresh = false;
|
||||||
static const int _defaultMaxContacts = 32;
|
static const int _defaultMaxContacts = 32;
|
||||||
static const int _defaultMaxChannels = 8;
|
static const int _defaultMaxChannels = 8;
|
||||||
@@ -217,6 +226,12 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
String? get deviceId => _deviceId;
|
String? get deviceId => _deviceId;
|
||||||
String get deviceIdLabel => _deviceId ?? 'Unknown';
|
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 {
|
String get deviceDisplayName {
|
||||||
if (_selfName != null && _selfName!.isNotEmpty) {
|
if (_selfName != null && _selfName!.isNotEmpty) {
|
||||||
return _selfName!;
|
return _selfName!;
|
||||||
@@ -742,12 +757,17 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<String>> listUsbPorts() => _usbSerialService.listPorts();
|
||||||
|
|
||||||
Future<void> connect(BluetoothDevice device, {String? displayName}) async {
|
Future<void> connect(BluetoothDevice device, {String? displayName}) async {
|
||||||
if (_state == MeshCoreConnectionState.connecting ||
|
if (_state == MeshCoreConnectionState.connecting ||
|
||||||
_state == MeshCoreConnectionState.connected) {
|
_state == MeshCoreConnectionState.connected) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_activeTransport = MeshCoreTransportType.bluetooth;
|
||||||
|
_activeUsbPort = null;
|
||||||
|
|
||||||
await stopScan();
|
await stopScan();
|
||||||
_setState(MeshCoreConnectionState.connecting);
|
_setState(MeshCoreConnectionState.connecting);
|
||||||
_device = device;
|
_device = device;
|
||||||
@@ -832,6 +852,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
);
|
);
|
||||||
|
|
||||||
_setState(MeshCoreConnectionState.connected);
|
_setState(MeshCoreConnectionState.connected);
|
||||||
|
_hasReceivedDeviceInfo = false;
|
||||||
|
_pendingInitialChannelSync = true;
|
||||||
|
|
||||||
await _requestDeviceInfo();
|
await _requestDeviceInfo();
|
||||||
_startBatteryPolling();
|
_startBatteryPolling();
|
||||||
@@ -845,9 +867,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
|
|
||||||
// Keep device clock aligned on every connection.
|
// Keep device clock aligned on every connection.
|
||||||
await syncTime();
|
await syncTime();
|
||||||
|
|
||||||
// Fetch channels so we can track unread counts for incoming messages
|
|
||||||
unawaited(getChannels());
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Connection error: $e");
|
debugPrint("Connection error: $e");
|
||||||
await disconnect(manual: false);
|
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 {
|
Future<bool> _waitForSelfInfo({required Duration timeout}) async {
|
||||||
if (_selfPublicKey != null) return true;
|
if (_selfPublicKey != null) return true;
|
||||||
if (!isConnected) return false;
|
if (!isConnected) return false;
|
||||||
@@ -886,7 +962,10 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get _shouldAutoReconnect => !_manualDisconnect && _lastDeviceId != null;
|
bool get _shouldAutoReconnect =>
|
||||||
|
!_manualDisconnect &&
|
||||||
|
_lastDeviceId != null &&
|
||||||
|
_activeTransport == MeshCoreTransportType.bluetooth;
|
||||||
|
|
||||||
void _cancelReconnectTimer() {
|
void _cancelReconnectTimer() {
|
||||||
_reconnectTimer?.cancel();
|
_reconnectTimer?.cancel();
|
||||||
@@ -930,6 +1009,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
|
|
||||||
Future<void> disconnect({bool manual = true}) async {
|
Future<void> disconnect({bool manual = true}) async {
|
||||||
if (_state == MeshCoreConnectionState.disconnecting) return;
|
if (_state == MeshCoreConnectionState.disconnecting) return;
|
||||||
|
final transportAtDisconnect = _activeTransport;
|
||||||
|
|
||||||
if (manual) {
|
if (manual) {
|
||||||
_manualDisconnect = true;
|
_manualDisconnect = true;
|
||||||
@@ -941,6 +1021,10 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
_setState(MeshCoreConnectionState.disconnecting);
|
_setState(MeshCoreConnectionState.disconnecting);
|
||||||
_stopBatteryPolling();
|
_stopBatteryPolling();
|
||||||
|
|
||||||
|
await _usbFrameSubscription?.cancel();
|
||||||
|
_usbFrameSubscription = null;
|
||||||
|
await _usbSerialService.disconnect();
|
||||||
|
|
||||||
await _notifySubscription?.cancel();
|
await _notifySubscription?.cancel();
|
||||||
_notifySubscription = null;
|
_notifySubscription = null;
|
||||||
|
|
||||||
@@ -980,6 +1064,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
_repeaterBatterySnapshots.clear();
|
_repeaterBatterySnapshots.clear();
|
||||||
_batteryRequested = false;
|
_batteryRequested = false;
|
||||||
_awaitingSelfInfo = false;
|
_awaitingSelfInfo = false;
|
||||||
|
_hasReceivedDeviceInfo = false;
|
||||||
|
_pendingInitialChannelSync = false;
|
||||||
_maxContacts = _defaultMaxContacts;
|
_maxContacts = _defaultMaxContacts;
|
||||||
_maxChannels = _defaultMaxChannels;
|
_maxChannels = _defaultMaxChannels;
|
||||||
_isSyncingQueuedMessages = false;
|
_isSyncingQueuedMessages = false;
|
||||||
@@ -993,8 +1079,11 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
_pendingGenericAckQueue.clear();
|
_pendingGenericAckQueue.clear();
|
||||||
_reactionSendQueueSequence = 0;
|
_reactionSendQueueSequence = 0;
|
||||||
|
|
||||||
|
_activeTransport = MeshCoreTransportType.bluetooth;
|
||||||
|
_activeUsbPort = null;
|
||||||
|
|
||||||
_setState(MeshCoreConnectionState.disconnected);
|
_setState(MeshCoreConnectionState.disconnected);
|
||||||
if (!manual) {
|
if (!manual && transportAtDisconnect == MeshCoreTransportType.bluetooth) {
|
||||||
_scheduleReconnect();
|
_scheduleReconnect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1004,24 +1093,29 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
String? channelSendQueueId,
|
String? channelSendQueueId,
|
||||||
bool expectsGenericAck = false,
|
bool expectsGenericAck = false,
|
||||||
}) async {
|
}) async {
|
||||||
if (!isConnected || _rxCharacteristic == null) {
|
if (!isConnected) {
|
||||||
throw Exception("Not connected to a MeshCore device");
|
throw Exception("Not connected to a MeshCore device");
|
||||||
}
|
}
|
||||||
|
|
||||||
_bleDebugLogService?.logFrame(data, outgoing: true);
|
_bleDebugLogService?.logFrame(data, outgoing: true);
|
||||||
|
|
||||||
// Prefer write without response when supported; fall back to write with response.
|
if (_activeTransport == MeshCoreTransportType.usb) {
|
||||||
final properties = _rxCharacteristic!.properties;
|
await _usbSerialService.write(data);
|
||||||
final canWriteWithoutResponse = properties.writeWithoutResponse;
|
} else {
|
||||||
final canWriteWithResponse = properties.write;
|
if (_rxCharacteristic == null) {
|
||||||
if (!canWriteWithoutResponse && !canWriteWithResponse) {
|
throw Exception("MeshCore RX characteristic does not support write");
|
||||||
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(
|
_trackPendingGenericAck(
|
||||||
data,
|
data,
|
||||||
channelSendQueueId: channelSendQueueId,
|
channelSendQueueId: channelSendQueueId,
|
||||||
@@ -2000,10 +2094,12 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
|
|
||||||
// Auto-fetch contacts after getting self info
|
// Auto-fetch contacts after getting self info
|
||||||
getContacts();
|
getContacts();
|
||||||
|
_maybeStartInitialChannelSync();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleDeviceInfo(Uint8List frame) {
|
void _handleDeviceInfo(Uint8List frame) {
|
||||||
if (frame.length < 4) return;
|
if (frame.length < 4) return;
|
||||||
|
_hasReceivedDeviceInfo = true;
|
||||||
_firmwareVerCode = frame[1];
|
_firmwareVerCode = frame[1];
|
||||||
|
|
||||||
// Parse client_repeat from firmware v9+ (byte 80)
|
// Parse client_repeat from firmware v9+ (byte 80)
|
||||||
@@ -2027,12 +2123,25 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
if (nextMaxChannels > previousMaxChannels) {
|
if (nextMaxChannels > previousMaxChannels) {
|
||||||
unawaited(loadChannelSettings(maxChannels: nextMaxChannels));
|
unawaited(loadChannelSettings(maxChannels: nextMaxChannels));
|
||||||
unawaited(loadAllChannelMessages(maxChannels: nextMaxChannels));
|
unawaited(loadAllChannelMessages(maxChannels: nextMaxChannels));
|
||||||
if (isConnected) {
|
if (isConnected && !_pendingInitialChannelSync) {
|
||||||
unawaited(getChannels(maxChannels: nextMaxChannels));
|
unawaited(getChannels(maxChannels: nextMaxChannels));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
_maybeStartInitialChannelSync();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _maybeStartInitialChannelSync() {
|
||||||
|
if (!_pendingInitialChannelSync || !isConnected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_selfPublicKey == null || !_hasReceivedDeviceInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_pendingInitialChannelSync = false;
|
||||||
|
unawaited(getChannels(maxChannels: _maxChannels));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleNoMoreMessages() {
|
void _handleNoMoreMessages() {
|
||||||
@@ -3591,6 +3700,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
_txCharacteristic = null;
|
_txCharacteristic = null;
|
||||||
// Preserve deviceId and displayName for UI display during reconnection
|
// Preserve deviceId and displayName for UI display during reconnection
|
||||||
// They're only cleared on manual disconnect via disconnect() method
|
// They're only cleared on manual disconnect via disconnect() method
|
||||||
|
_hasReceivedDeviceInfo = false;
|
||||||
|
_pendingInitialChannelSync = false;
|
||||||
_maxContacts = _defaultMaxContacts;
|
_maxContacts = _defaultMaxContacts;
|
||||||
_maxChannels = _defaultMaxChannels;
|
_maxChannels = _defaultMaxChannels;
|
||||||
_isSyncingQueuedMessages = false;
|
_isSyncingQueuedMessages = false;
|
||||||
@@ -3671,10 +3782,12 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
void dispose() {
|
void dispose() {
|
||||||
_scanSubscription?.cancel();
|
_scanSubscription?.cancel();
|
||||||
_connectionSubscription?.cancel();
|
_connectionSubscription?.cancel();
|
||||||
|
_usbFrameSubscription?.cancel();
|
||||||
_notifySubscription?.cancel();
|
_notifySubscription?.cancel();
|
||||||
_reconnectTimer?.cancel();
|
_reconnectTimer?.cancel();
|
||||||
_batteryPollTimer?.cancel();
|
_batteryPollTimer?.cancel();
|
||||||
_receivedFramesController.close();
|
_receivedFramesController.close();
|
||||||
|
_usbSerialService.dispose();
|
||||||
|
|
||||||
// Flush pending unread writes before disposal
|
// Flush pending unread writes before disposal
|
||||||
_unreadStore.flush();
|
_unreadStore.flush();
|
||||||
|
|||||||
+10
-1
@@ -1801,5 +1801,14 @@
|
|||||||
"contacts_unread": "Непрочетено",
|
"contacts_unread": "Непрочетено",
|
||||||
"contacts_searchRepeaters": "Търсене на {number}{str} повтарящи се...",
|
"contacts_searchRepeaters": "Търсене на {number}{str} повтарящи се...",
|
||||||
"contacts_searchContactsNoNumber": "Търси контакти...",
|
"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
@@ -1829,5 +1829,14 @@
|
|||||||
"contacts_searchRepeaters": "Suche {number}{str} Repeater...",
|
"contacts_searchRepeaters": "Suche {number}{str} Repeater...",
|
||||||
"contacts_searchFavorites": "Suche {number}{str} Favoriten...",
|
"contacts_searchFavorites": "Suche {number}{str} Favoriten...",
|
||||||
"contacts_searchUsers": "Suche {number}{str} Benutzer...",
|
"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."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scanner_title": "MeshCore Open",
|
"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_scanning": "Scanning for devices...",
|
||||||
"scanner_connecting": "Connecting...",
|
"scanner_connecting": "Connecting...",
|
||||||
"scanner_disconnecting": "Disconnecting...",
|
"scanner_disconnecting": "Disconnecting...",
|
||||||
|
|||||||
+10
-1
@@ -1829,5 +1829,14 @@
|
|||||||
"contacts_searchFavorites": "Buscar {number}{str} Favoritos...",
|
"contacts_searchFavorites": "Buscar {number}{str} Favoritos...",
|
||||||
"contacts_searchUsers": "Buscar {number}{str} Usuarios...",
|
"contacts_searchUsers": "Buscar {number}{str} Usuarios...",
|
||||||
"contacts_searchRepeaters": "Buscar {number}{str} Repetidores...",
|
"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
@@ -1801,5 +1801,14 @@
|
|||||||
"contacts_searchUsers": "Rechercher {number}{str} utilisateurs...",
|
"contacts_searchUsers": "Rechercher {number}{str} utilisateurs...",
|
||||||
"contacts_searchRoomServers": "Rechercher {number}{str} serveurs de salle...",
|
"contacts_searchRoomServers": "Rechercher {number}{str} serveurs de salle...",
|
||||||
"contacts_searchRepeaters": "Rechercher {number}{str} Répéteurs...",
|
"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
@@ -1801,5 +1801,14 @@
|
|||||||
"contacts_searchFavorites": "Cerca {number}{str} Preferiti...",
|
"contacts_searchFavorites": "Cerca {number}{str} Preferiti...",
|
||||||
"contacts_unread": "Non letti",
|
"contacts_unread": "Non letti",
|
||||||
"contacts_searchRepeaters": "Cerca {number}{str} Ripetitori...",
|
"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."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -316,6 +316,60 @@ abstract class AppLocalizations {
|
|||||||
/// **'MeshCore Open'**
|
/// **'MeshCore Open'**
|
||||||
String get scanner_title;
|
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.
|
/// No description provided for @scanner_scanning.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|||||||
@@ -108,6 +108,37 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get scanner_title => 'MeshCore Open';
|
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
|
@override
|
||||||
String get scanner_scanning => 'Сканиране за устройства...';
|
String get scanner_scanning => 'Сканиране за устройства...';
|
||||||
|
|
||||||
|
|||||||
@@ -108,6 +108,38 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get scanner_title => 'MeshCore Open';
|
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
|
@override
|
||||||
String get scanner_scanning => 'Scannen nach Geräten...';
|
String get scanner_scanning => 'Scannen nach Geräten...';
|
||||||
|
|
||||||
|
|||||||
@@ -108,6 +108,37 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get scanner_title => 'MeshCore Open';
|
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
|
@override
|
||||||
String get scanner_scanning => 'Scanning for devices...';
|
String get scanner_scanning => 'Scanning for devices...';
|
||||||
|
|
||||||
|
|||||||
@@ -108,6 +108,38 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get scanner_title => 'MeshCore Open';
|
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
|
@override
|
||||||
String get scanner_scanning => 'Escaneando dispositivos...';
|
String get scanner_scanning => 'Escaneando dispositivos...';
|
||||||
|
|
||||||
|
|||||||
@@ -108,6 +108,38 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get scanner_title => 'MeshCore Open';
|
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
|
@override
|
||||||
String get scanner_scanning => 'Recherche de périphériques...';
|
String get scanner_scanning => 'Recherche de périphériques...';
|
||||||
|
|
||||||
|
|||||||
@@ -108,6 +108,38 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get scanner_title => 'MeshCore Open';
|
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
|
@override
|
||||||
String get scanner_scanning => 'Scansione in corso per i dispositivi...';
|
String get scanner_scanning => 'Scansione in corso per i dispositivi...';
|
||||||
|
|
||||||
|
|||||||
@@ -108,6 +108,37 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get scanner_title => 'MeshCore Open';
|
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
|
@override
|
||||||
String get scanner_scanning => 'Scannen naar apparaten...';
|
String get scanner_scanning => 'Scannen naar apparaten...';
|
||||||
|
|
||||||
|
|||||||
@@ -108,6 +108,37 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get scanner_title => 'MeshCore Open';
|
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
|
@override
|
||||||
String get scanner_scanning => 'Skanowanie urządzeń...';
|
String get scanner_scanning => 'Skanowanie urządzeń...';
|
||||||
|
|
||||||
|
|||||||
@@ -108,6 +108,37 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get scanner_title => 'MeshCore Open';
|
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
|
@override
|
||||||
String get scanner_scanning => 'Procurando por dispositivos...';
|
String get scanner_scanning => 'Procurando por dispositivos...';
|
||||||
|
|
||||||
|
|||||||
@@ -108,6 +108,38 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get scanner_title => 'MeshCore Open';
|
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
|
@override
|
||||||
String get scanner_scanning => 'Поиск устройств...';
|
String get scanner_scanning => 'Поиск устройств...';
|
||||||
|
|
||||||
|
|||||||
@@ -108,6 +108,37 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get scanner_title => 'MeshCore Open';
|
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
|
@override
|
||||||
String get scanner_scanning => 'Skrívania zariadení...';
|
String get scanner_scanning => 'Skrívania zariadení...';
|
||||||
|
|
||||||
|
|||||||
@@ -108,6 +108,37 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get scanner_title => 'MeshCore Open';
|
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
|
@override
|
||||||
String get scanner_scanning => 'Skeniram za naprave...';
|
String get scanner_scanning => 'Skeniram za naprave...';
|
||||||
|
|
||||||
|
|||||||
@@ -108,6 +108,37 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get scanner_title => 'MeshCore Open';
|
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
|
@override
|
||||||
String get scanner_scanning => 'Söker efter enheter...';
|
String get scanner_scanning => 'Söker efter enheter...';
|
||||||
|
|
||||||
|
|||||||
@@ -108,6 +108,38 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get scanner_title => 'MeshCore Open';
|
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
|
@override
|
||||||
String get scanner_scanning => 'Пошук пристроїв...';
|
String get scanner_scanning => 'Пошук пристроїв...';
|
||||||
|
|
||||||
|
|||||||
@@ -108,6 +108,33 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get scanner_title => '连接设备';
|
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
|
@override
|
||||||
String get scanner_scanning => '正在搜索设备...';
|
String get scanner_scanning => '正在搜索设备...';
|
||||||
|
|
||||||
|
|||||||
+10
-1
@@ -1801,5 +1801,14 @@
|
|||||||
"contacts_searchContactsNoNumber": "Zoek contacten...",
|
"contacts_searchContactsNoNumber": "Zoek contacten...",
|
||||||
"contacts_searchUsers": "Zoek {number}{str} gebruikers...",
|
"contacts_searchUsers": "Zoek {number}{str} gebruikers...",
|
||||||
"contacts_searchFavorites": "Zoek {number}{str} favorieten...",
|
"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
@@ -1801,5 +1801,14 @@
|
|||||||
"contacts_searchFavorites": "Wyszukaj {number}{str} ulubione...",
|
"contacts_searchFavorites": "Wyszukaj {number}{str} ulubione...",
|
||||||
"contacts_searchRoomServers": "Wyszukaj {number}{str} serwerów Room...",
|
"contacts_searchRoomServers": "Wyszukaj {number}{str} serwerów Room...",
|
||||||
"contacts_searchUsers": "Wyszukaj {number}{str} Użytkowników...",
|
"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
@@ -1801,5 +1801,14 @@
|
|||||||
"contacts_searchUsers": "Pesquisar {number}{str} Usuários...",
|
"contacts_searchUsers": "Pesquisar {number}{str} Usuários...",
|
||||||
"contacts_searchContactsNoNumber": "Pesquisar Contatos...",
|
"contacts_searchContactsNoNumber": "Pesquisar Contatos...",
|
||||||
"contacts_unread": "Não lido",
|
"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
@@ -1041,5 +1041,14 @@
|
|||||||
"contacts_unread": "Непрочитанное",
|
"contacts_unread": "Непрочитанное",
|
||||||
"contacts_searchRoomServers": "Поиск {number}{str} серверов комнат...",
|
"contacts_searchRoomServers": "Поиск {number}{str} серверов комнат...",
|
||||||
"contacts_searchFavorites": "Поиск {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
@@ -1801,5 +1801,14 @@
|
|||||||
"contacts_searchRepeaters": "Hľadať {number}{str} opakovače...",
|
"contacts_searchRepeaters": "Hľadať {number}{str} opakovače...",
|
||||||
"contacts_searchUsers": "Hľadať {number}{str} používateľov...",
|
"contacts_searchUsers": "Hľadať {number}{str} používateľov...",
|
||||||
"contacts_searchContactsNoNumber": "Hľadať kontakty...",
|
"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
@@ -1801,5 +1801,14 @@
|
|||||||
"contacts_searchRoomServers": "Išči {number}{str} strežnikov sob...",
|
"contacts_searchRoomServers": "Išči {number}{str} strežnikov sob...",
|
||||||
"contacts_searchContactsNoNumber": "Iskanje stikov...",
|
"contacts_searchContactsNoNumber": "Iskanje stikov...",
|
||||||
"contacts_searchRepeaters": "Išči {number}{str} ponavljalnike...",
|
"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
@@ -1801,5 +1801,14 @@
|
|||||||
"contacts_searchRepeaters": "Sök {number}{str} upprepningsenheter...",
|
"contacts_searchRepeaters": "Sök {number}{str} upprepningsenheter...",
|
||||||
"contacts_searchFavorites": "Sök {number}{str} Favoriter...",
|
"contacts_searchFavorites": "Sök {number}{str} Favoriter...",
|
||||||
"contacts_searchUsers": "Sök {number}{str} användare...",
|
"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
@@ -1801,5 +1801,14 @@
|
|||||||
"contacts_searchFavorites": "Пошук {number}{str} улюблених...",
|
"contacts_searchFavorites": "Пошук {number}{str} улюблених...",
|
||||||
"contacts_searchContactsNoNumber": "Пошук контактів...",
|
"contacts_searchContactsNoNumber": "Пошук контактів...",
|
||||||
"contacts_searchRepeaters": "Пошук {number}{str} ретрансляторів...",
|
"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
@@ -1806,5 +1806,14 @@
|
|||||||
"contacts_searchRepeaters": "搜索 {number}{str} 重复器...",
|
"contacts_searchRepeaters": "搜索 {number}{str} 重复器...",
|
||||||
"contacts_searchContactsNoNumber": "搜索联系人...",
|
"contacts_searchContactsNoNumber": "搜索联系人...",
|
||||||
"contacts_searchRoomServers": "搜索 {number}{str} 房间服务器...",
|
"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
@@ -8,7 +8,7 @@ import 'screens/chrome_required_screen.dart';
|
|||||||
import 'utils/platform_info.dart';
|
import 'utils/platform_info.dart';
|
||||||
|
|
||||||
import 'connector/meshcore_connector.dart';
|
import 'connector/meshcore_connector.dart';
|
||||||
import 'screens/scanner_screen.dart';
|
import 'screens/connection_choice_screen.dart';
|
||||||
import 'services/storage_service.dart';
|
import 'services/storage_service.dart';
|
||||||
import 'services/message_retry_service.dart';
|
import 'services/message_retry_service.dart';
|
||||||
import 'services/path_history_service.dart';
|
import 'services/path_history_service.dart';
|
||||||
@@ -192,7 +192,7 @@ class MeshCoreApp extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
home: (PlatformInfo.isWeb && !PlatformInfo.isChrome)
|
home: (PlatformInfo.isWeb && !PlatformInfo.isChrome)
|
||||||
? const ChromeRequiredScreen()
|
? const ChromeRequiredScreen()
|
||||||
: const ScannerScreen(),
|
: const ConnectionChoiceScreen(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ class ScannerScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class _ScannerScreenState extends State<ScannerScreen> {
|
class _ScannerScreenState extends State<ScannerScreen> {
|
||||||
bool _changedNavigation = false;
|
bool _changedNavigation = false;
|
||||||
|
late final MeshCoreConnector _connector;
|
||||||
late final VoidCallback _connectionListener;
|
late final VoidCallback _connectionListener;
|
||||||
BluetoothAdapterState _bluetoothState = BluetoothAdapterState.unknown;
|
BluetoothAdapterState _bluetoothState = BluetoothAdapterState.unknown;
|
||||||
late StreamSubscription<BluetoothAdapterState> _bluetoothStateSubscription;
|
late StreamSubscription<BluetoothAdapterState> _bluetoothStateSubscription;
|
||||||
@@ -27,12 +28,12 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
_connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||||
|
|
||||||
_connectionListener = () {
|
_connectionListener = () {
|
||||||
if (connector.state == MeshCoreConnectionState.disconnected) {
|
if (_connector.state == MeshCoreConnectionState.disconnected) {
|
||||||
_changedNavigation = false;
|
_changedNavigation = false;
|
||||||
} else if (connector.state == MeshCoreConnectionState.connected &&
|
} else if (_connector.state == MeshCoreConnectionState.connected &&
|
||||||
!_changedNavigation) {
|
!_changedNavigation) {
|
||||||
_changedNavigation = true;
|
_changedNavigation = true;
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@@ -43,7 +44,7 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
connector.addListener(_connectionListener);
|
_connector.addListener(_connectionListener);
|
||||||
|
|
||||||
_bluetoothStateSubscription = FlutterBluePlus.adapterState.listen(
|
_bluetoothStateSubscription = FlutterBluePlus.adapterState.listen(
|
||||||
(state) {
|
(state) {
|
||||||
@@ -53,7 +54,7 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
|||||||
});
|
});
|
||||||
// Cancel scan if Bluetooth turns off while scanning
|
// Cancel scan if Bluetooth turns off while scanning
|
||||||
if (state != BluetoothAdapterState.on) {
|
if (state != BluetoothAdapterState.on) {
|
||||||
unawaited(connector.stopScan());
|
unawaited(_connector.stopScan());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -65,16 +66,25 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
_connector.removeListener(_connectionListener);
|
||||||
connector.removeListener(_connectionListener);
|
|
||||||
unawaited(_bluetoothStateSubscription.cancel());
|
unawaited(_bluetoothStateSubscription.cancel());
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final canPop = Navigator.of(context).canPop();
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
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),
|
title: AdaptiveAppBarTitle(context.l10n.scanner_title),
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 }
|
||||||
@@ -7,6 +7,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
flserial
|
||||||
)
|
)
|
||||||
|
|
||||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ dependencies:
|
|||||||
# Use with the CupertinoIcons class for iOS style icons.
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
cupertino_icons: ^1.0.8
|
cupertino_icons: ^1.0.8
|
||||||
flutter_blue_plus: ^2.1.0
|
flutter_blue_plus: ^2.1.0
|
||||||
|
flserial: ^0.3.5
|
||||||
provider: ^6.1.5+1
|
provider: ^6.1.5+1
|
||||||
shared_preferences: ^2.2.2
|
shared_preferences: ^2.2.2
|
||||||
uuid: ^4.3.3
|
uuid: ^4.3.3
|
||||||
|
|||||||
@@ -89,9 +89,11 @@ endif()
|
|||||||
|
|
||||||
# Copy the native assets provided by the build.dart from all packages.
|
# Copy the native assets provided by the build.dart from all packages.
|
||||||
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/")
|
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/")
|
||||||
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
|
if(EXISTS "${NATIVE_ASSETS_DIR}")
|
||||||
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
|
||||||
COMPONENT Runtime)
|
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
endif()
|
||||||
|
|
||||||
# Fully re-copy the assets directory on each build to avoid having stale files
|
# Fully re-copy the assets directory on each build to avoid having stale files
|
||||||
# from a previous install.
|
# from a previous install.
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
flserial
|
||||||
flutter_local_notifications_windows
|
flutter_local_notifications_windows
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user