mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-06-14 22:55:12 +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 {
|
||||
namespace = "com.meshcore.meshcore_open"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
ndkVersion = "29.0.14206865"
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
@@ -84,4 +84,5 @@ flutter {
|
||||
|
||||
dependencies {
|
||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
|
||||
implementation("com.github.mik3y:usb-serial-for-android:3.9.0")
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
<!-- Camera permission for QR code scanning -->
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false"/>
|
||||
<uses-feature android:name="android.hardware.usb.host" android:required="false"/>
|
||||
|
||||
<application
|
||||
android:label="meshcore_open"
|
||||
|
||||
@@ -1,5 +1,313 @@
|
||||
package com.meshcore.meshcore_open
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.hardware.usb.UsbDevice
|
||||
import android.hardware.usb.UsbDeviceConnection
|
||||
import android.hardware.usb.UsbManager
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import com.hoho.android.usbserial.driver.UsbSerialDriver
|
||||
import com.hoho.android.usbserial.driver.UsbSerialPort
|
||||
import com.hoho.android.usbserial.driver.UsbSerialProber
|
||||
import com.hoho.android.usbserial.util.SerialInputOutputManager
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import java.util.Locale
|
||||
|
||||
class MainActivity : FlutterActivity()
|
||||
class MainActivity : FlutterActivity() {
|
||||
private val usbMethodChannelName = "meshcore_open/android_usb_serial"
|
||||
private val usbEventChannelName = "meshcore_open/android_usb_serial_events"
|
||||
private val usbPermissionAction = "com.meshcore.meshcore_open.USB_PERMISSION"
|
||||
|
||||
private lateinit var usbManager: UsbManager
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
private var eventSink: EventChannel.EventSink? = null
|
||||
private var usbConnection: UsbDeviceConnection? = null
|
||||
private var usbPort: UsbSerialPort? = null
|
||||
private var ioManager: SerialInputOutputManager? = null
|
||||
|
||||
private var pendingConnectResult: MethodChannel.Result? = null
|
||||
private var pendingConnectPortName: String? = null
|
||||
private var pendingConnectBaudRate: Int = 115200
|
||||
|
||||
private val permissionReceiver =
|
||||
object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
if (intent?.action != usbPermissionAction) {
|
||||
return
|
||||
}
|
||||
|
||||
val result = pendingConnectResult
|
||||
val portName = pendingConnectPortName
|
||||
pendingConnectResult = null
|
||||
pendingConnectPortName = null
|
||||
|
||||
if (result == null || portName == null) {
|
||||
return
|
||||
}
|
||||
|
||||
val granted =
|
||||
intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)
|
||||
if (!granted) {
|
||||
result.error("usb_permission_denied", "USB permission denied", null)
|
||||
return
|
||||
}
|
||||
|
||||
val device = findUsbDevice(portName)
|
||||
if (device == null) {
|
||||
result.error(
|
||||
"usb_device_missing",
|
||||
"USB device no longer available for $portName",
|
||||
null,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
openUsbDevice(device, pendingConnectBaudRate, result)
|
||||
}
|
||||
}
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
|
||||
usbManager = getSystemService(Context.USB_SERVICE) as UsbManager
|
||||
registerUsbPermissionReceiver()
|
||||
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, usbMethodChannelName)
|
||||
.setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"listPorts" -> result.success(listUsbPorts())
|
||||
"connect" -> handleUsbConnect(call, result)
|
||||
"write" -> handleUsbWrite(call, result)
|
||||
"disconnect" -> {
|
||||
closeUsbConnection()
|
||||
result.success(null)
|
||||
}
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
EventChannel(flutterEngine.dartExecutor.binaryMessenger, usbEventChannelName)
|
||||
.setStreamHandler(
|
||||
object : EventChannel.StreamHandler {
|
||||
override fun onListen(arguments: Any?, events: EventChannel.EventSink) {
|
||||
eventSink = events
|
||||
}
|
||||
|
||||
override fun onCancel(arguments: Any?) {
|
||||
eventSink = null
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
closeUsbConnection()
|
||||
unregisterReceiver(permissionReceiver)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun registerUsbPermissionReceiver() {
|
||||
val filter = IntentFilter(usbPermissionAction)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
registerReceiver(permissionReceiver, filter, RECEIVER_NOT_EXPORTED)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
registerReceiver(permissionReceiver, filter)
|
||||
}
|
||||
}
|
||||
|
||||
private fun listUsbPorts(): List<String> {
|
||||
val drivers = UsbSerialProber.getDefaultProber().findAllDrivers(usbManager)
|
||||
return drivers.map { driver ->
|
||||
val device = driver.device
|
||||
val productName = device.productName ?: "USB Serial Device"
|
||||
val vendorProduct =
|
||||
String.format(
|
||||
Locale.US,
|
||||
"VID:%04X PID:%04X",
|
||||
device.vendorId,
|
||||
device.productId,
|
||||
)
|
||||
"${device.deviceName} - $productName - $vendorProduct"
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleUsbConnect(call: MethodCall, result: MethodChannel.Result) {
|
||||
val portName = call.argument<String>("portName")
|
||||
val baudRate = call.argument<Int>("baudRate") ?: 115200
|
||||
if (portName.isNullOrBlank()) {
|
||||
result.error("usb_invalid_port", "Port name is required", null)
|
||||
return
|
||||
}
|
||||
|
||||
val device = findUsbDevice(portName)
|
||||
if (device == null) {
|
||||
result.error("usb_device_missing", "USB device not found for $portName", null)
|
||||
return
|
||||
}
|
||||
|
||||
if (usbManager.hasPermission(device)) {
|
||||
openUsbDevice(device, baudRate, result)
|
||||
return
|
||||
}
|
||||
|
||||
if (pendingConnectResult != null) {
|
||||
result.error("usb_busy", "Another USB permission request is already pending", null)
|
||||
return
|
||||
}
|
||||
|
||||
pendingConnectResult = result
|
||||
pendingConnectPortName = portName
|
||||
pendingConnectBaudRate = baudRate
|
||||
|
||||
val permissionIntent = PendingIntent.getBroadcast(
|
||||
this,
|
||||
0,
|
||||
Intent(usbPermissionAction).setPackage(packageName),
|
||||
pendingIntentFlags(),
|
||||
)
|
||||
usbManager.requestPermission(device, permissionIntent)
|
||||
}
|
||||
|
||||
private fun handleUsbWrite(call: MethodCall, result: MethodChannel.Result) {
|
||||
val data = call.argument<ByteArray>("data")
|
||||
val port = usbPort
|
||||
if (data == null) {
|
||||
result.error("usb_invalid_data", "Data is required", null)
|
||||
return
|
||||
}
|
||||
if (port == null) {
|
||||
result.error("usb_not_connected", "USB serial port is not connected", null)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
port.write(data, 1000)
|
||||
result.success(null)
|
||||
} catch (error: Exception) {
|
||||
result.error("usb_write_failed", error.message, null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun findUsbDevice(portName: String): UsbDevice? {
|
||||
return usbManager.deviceList.values.firstOrNull { it.deviceName == portName }
|
||||
}
|
||||
|
||||
private fun openUsbDevice(
|
||||
device: UsbDevice,
|
||||
baudRate: Int,
|
||||
result: MethodChannel.Result,
|
||||
) {
|
||||
try {
|
||||
closeUsbConnection()
|
||||
|
||||
val driver = UsbSerialProber.getDefaultProber().probeDevice(device)
|
||||
if (driver == null) {
|
||||
result.error("usb_driver_missing", "No USB serial driver for ${device.deviceName}", null)
|
||||
return
|
||||
}
|
||||
|
||||
val connection = usbManager.openDevice(device)
|
||||
if (connection == null) {
|
||||
result.error(
|
||||
"usb_open_failed",
|
||||
"UsbManager could not open ${device.deviceName}",
|
||||
null,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val port = firstPort(driver)
|
||||
if (port == null) {
|
||||
connection.close()
|
||||
result.error("usb_port_missing", "No USB serial port exposed by ${device.deviceName}", null)
|
||||
return
|
||||
}
|
||||
|
||||
port.open(connection)
|
||||
port.setParameters(
|
||||
baudRate,
|
||||
8,
|
||||
UsbSerialPort.STOPBITS_1,
|
||||
UsbSerialPort.PARITY_NONE,
|
||||
)
|
||||
port.rts = false
|
||||
port.dtr = true
|
||||
|
||||
usbConnection = connection
|
||||
usbPort = port
|
||||
|
||||
ioManager =
|
||||
SerialInputOutputManager(
|
||||
port,
|
||||
object : SerialInputOutputManager.Listener {
|
||||
override fun onNewData(data: ByteArray) {
|
||||
mainHandler.post {
|
||||
eventSink?.success(data)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRunError(e: Exception) {
|
||||
mainHandler.post {
|
||||
eventSink?.error(
|
||||
"usb_io_error",
|
||||
e.message ?: "USB serial I/O error",
|
||||
null,
|
||||
)
|
||||
}
|
||||
closeUsbConnection()
|
||||
}
|
||||
},
|
||||
).also { manager ->
|
||||
manager.start()
|
||||
}
|
||||
|
||||
result.success(null)
|
||||
} catch (error: Exception) {
|
||||
closeUsbConnection()
|
||||
result.error("usb_connect_failed", error.message, null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun firstPort(driver: UsbSerialDriver): UsbSerialPort? {
|
||||
return driver.ports.firstOrNull()
|
||||
}
|
||||
|
||||
private fun closeUsbConnection() {
|
||||
try {
|
||||
ioManager?.stop()
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
ioManager = null
|
||||
|
||||
try {
|
||||
usbPort?.close()
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
usbPort = null
|
||||
|
||||
try {
|
||||
usbConnection?.close()
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
usbConnection = null
|
||||
}
|
||||
|
||||
private fun pendingIntentFlags(): Int {
|
||||
var flags = PendingIntent.FLAG_UPDATE_CURRENT
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
flags = flags or PendingIntent.FLAG_MUTABLE
|
||||
}
|
||||
return flags
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven(url = "https://jitpack.io")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import '../services/path_history_service.dart';
|
||||
import '../services/app_settings_service.dart';
|
||||
import '../services/background_service.dart';
|
||||
import '../services/notification_service.dart';
|
||||
import '../services/usb_serial_service.dart';
|
||||
import '../storage/channel_message_store.dart';
|
||||
import '../storage/channel_order_store.dart';
|
||||
import '../storage/channel_settings_store.dart';
|
||||
@@ -82,6 +83,8 @@ enum MeshCoreConnectionState {
|
||||
disconnecting,
|
||||
}
|
||||
|
||||
enum MeshCoreTransportType { bluetooth, usb }
|
||||
|
||||
class RepeaterBatterySnapshot {
|
||||
final int millivolts;
|
||||
final DateTime updatedAt;
|
||||
@@ -108,6 +111,10 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
String? _lastDeviceId;
|
||||
String? _lastDeviceDisplayName;
|
||||
bool _manualDisconnect = false;
|
||||
final UsbSerialService _usbSerialService = UsbSerialService();
|
||||
StreamSubscription<Uint8List>? _usbFrameSubscription;
|
||||
MeshCoreTransportType _activeTransport = MeshCoreTransportType.bluetooth;
|
||||
String? _activeUsbPort;
|
||||
|
||||
final List<ScanResult> _scanResults = [];
|
||||
final List<Contact> _contacts = [];
|
||||
@@ -154,6 +161,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
bool _hasLoadedChannels = false;
|
||||
bool _batteryRequested = false;
|
||||
bool _awaitingSelfInfo = false;
|
||||
bool _hasReceivedDeviceInfo = false;
|
||||
bool _pendingInitialChannelSync = false;
|
||||
bool _preserveContactsOnRefresh = false;
|
||||
static const int _defaultMaxContacts = 32;
|
||||
static const int _defaultMaxChannels = 8;
|
||||
@@ -217,6 +226,12 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
String? get deviceId => _deviceId;
|
||||
String get deviceIdLabel => _deviceId ?? 'Unknown';
|
||||
|
||||
MeshCoreTransportType get activeTransport => _activeTransport;
|
||||
String? get activeUsbPort => _activeUsbPort;
|
||||
bool get isUsbTransportConnected =>
|
||||
_state == MeshCoreConnectionState.connected &&
|
||||
_activeTransport == MeshCoreTransportType.usb;
|
||||
|
||||
String get deviceDisplayName {
|
||||
if (_selfName != null && _selfName!.isNotEmpty) {
|
||||
return _selfName!;
|
||||
@@ -742,12 +757,17 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<String>> listUsbPorts() => _usbSerialService.listPorts();
|
||||
|
||||
Future<void> connect(BluetoothDevice device, {String? displayName}) async {
|
||||
if (_state == MeshCoreConnectionState.connecting ||
|
||||
_state == MeshCoreConnectionState.connected) {
|
||||
return;
|
||||
}
|
||||
|
||||
_activeTransport = MeshCoreTransportType.bluetooth;
|
||||
_activeUsbPort = null;
|
||||
|
||||
await stopScan();
|
||||
_setState(MeshCoreConnectionState.connecting);
|
||||
_device = device;
|
||||
@@ -832,6 +852,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
);
|
||||
|
||||
_setState(MeshCoreConnectionState.connected);
|
||||
_hasReceivedDeviceInfo = false;
|
||||
_pendingInitialChannelSync = true;
|
||||
|
||||
await _requestDeviceInfo();
|
||||
_startBatteryPolling();
|
||||
@@ -845,9 +867,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
|
||||
// Keep device clock aligned on every connection.
|
||||
await syncTime();
|
||||
|
||||
// Fetch channels so we can track unread counts for incoming messages
|
||||
unawaited(getChannels());
|
||||
} catch (e) {
|
||||
debugPrint("Connection error: $e");
|
||||
await disconnect(manual: false);
|
||||
@@ -855,6 +874,63 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> connectUsb({
|
||||
required String portName,
|
||||
int baudRate = 115200,
|
||||
}) async {
|
||||
if (_state == MeshCoreConnectionState.connecting ||
|
||||
_state == MeshCoreConnectionState.connected) {
|
||||
return;
|
||||
}
|
||||
|
||||
_activeTransport = MeshCoreTransportType.bluetooth;
|
||||
_activeUsbPort = null;
|
||||
|
||||
await stopScan();
|
||||
_cancelReconnectTimer();
|
||||
_manualDisconnect = false;
|
||||
_activeTransport = MeshCoreTransportType.usb;
|
||||
_activeUsbPort = portName;
|
||||
unawaited(_backgroundService?.start());
|
||||
_setState(MeshCoreConnectionState.connecting);
|
||||
|
||||
try {
|
||||
await _usbFrameSubscription?.cancel();
|
||||
_usbFrameSubscription = null;
|
||||
await _usbSerialService.connect(portName: portName, baudRate: baudRate);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 200));
|
||||
_usbFrameSubscription = _usbSerialService.frameStream.listen(
|
||||
_handleFrame,
|
||||
onError: (error, stackTrace) {
|
||||
debugPrint('USB transport error: $error');
|
||||
unawaited(disconnect(manual: false));
|
||||
},
|
||||
onDone: () {
|
||||
unawaited(disconnect(manual: false));
|
||||
},
|
||||
);
|
||||
|
||||
_setState(MeshCoreConnectionState.connected);
|
||||
_hasReceivedDeviceInfo = false;
|
||||
_pendingInitialChannelSync = true;
|
||||
await _requestDeviceInfo();
|
||||
_startBatteryPolling();
|
||||
final gotSelfInfo = await _waitForSelfInfo(
|
||||
timeout: const Duration(seconds: 3),
|
||||
);
|
||||
if (!gotSelfInfo) {
|
||||
await refreshDeviceInfo();
|
||||
await _waitForSelfInfo(timeout: const Duration(seconds: 3));
|
||||
}
|
||||
|
||||
await syncTime();
|
||||
} catch (error) {
|
||||
debugPrint('USB connection error: $error');
|
||||
await disconnect(manual: false);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _waitForSelfInfo({required Duration timeout}) async {
|
||||
if (_selfPublicKey != null) return true;
|
||||
if (!isConnected) return false;
|
||||
@@ -886,7 +962,10 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
return result;
|
||||
}
|
||||
|
||||
bool get _shouldAutoReconnect => !_manualDisconnect && _lastDeviceId != null;
|
||||
bool get _shouldAutoReconnect =>
|
||||
!_manualDisconnect &&
|
||||
_lastDeviceId != null &&
|
||||
_activeTransport == MeshCoreTransportType.bluetooth;
|
||||
|
||||
void _cancelReconnectTimer() {
|
||||
_reconnectTimer?.cancel();
|
||||
@@ -930,6 +1009,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
|
||||
Future<void> disconnect({bool manual = true}) async {
|
||||
if (_state == MeshCoreConnectionState.disconnecting) return;
|
||||
final transportAtDisconnect = _activeTransport;
|
||||
|
||||
if (manual) {
|
||||
_manualDisconnect = true;
|
||||
@@ -941,6 +1021,10 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_setState(MeshCoreConnectionState.disconnecting);
|
||||
_stopBatteryPolling();
|
||||
|
||||
await _usbFrameSubscription?.cancel();
|
||||
_usbFrameSubscription = null;
|
||||
await _usbSerialService.disconnect();
|
||||
|
||||
await _notifySubscription?.cancel();
|
||||
_notifySubscription = null;
|
||||
|
||||
@@ -980,6 +1064,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_repeaterBatterySnapshots.clear();
|
||||
_batteryRequested = false;
|
||||
_awaitingSelfInfo = false;
|
||||
_hasReceivedDeviceInfo = false;
|
||||
_pendingInitialChannelSync = false;
|
||||
_maxContacts = _defaultMaxContacts;
|
||||
_maxChannels = _defaultMaxChannels;
|
||||
_isSyncingQueuedMessages = false;
|
||||
@@ -993,8 +1079,11 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_pendingGenericAckQueue.clear();
|
||||
_reactionSendQueueSequence = 0;
|
||||
|
||||
_activeTransport = MeshCoreTransportType.bluetooth;
|
||||
_activeUsbPort = null;
|
||||
|
||||
_setState(MeshCoreConnectionState.disconnected);
|
||||
if (!manual) {
|
||||
if (!manual && transportAtDisconnect == MeshCoreTransportType.bluetooth) {
|
||||
_scheduleReconnect();
|
||||
}
|
||||
}
|
||||
@@ -1004,24 +1093,29 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
String? channelSendQueueId,
|
||||
bool expectsGenericAck = false,
|
||||
}) async {
|
||||
if (!isConnected || _rxCharacteristic == null) {
|
||||
if (!isConnected) {
|
||||
throw Exception("Not connected to a MeshCore device");
|
||||
}
|
||||
|
||||
_bleDebugLogService?.logFrame(data, outgoing: true);
|
||||
|
||||
// Prefer write without response when supported; fall back to write with response.
|
||||
final properties = _rxCharacteristic!.properties;
|
||||
final canWriteWithoutResponse = properties.writeWithoutResponse;
|
||||
final canWriteWithResponse = properties.write;
|
||||
if (!canWriteWithoutResponse && !canWriteWithResponse) {
|
||||
throw Exception("MeshCore RX characteristic does not support write");
|
||||
if (_activeTransport == MeshCoreTransportType.usb) {
|
||||
await _usbSerialService.write(data);
|
||||
} else {
|
||||
if (_rxCharacteristic == null) {
|
||||
throw Exception("MeshCore RX characteristic does not support write");
|
||||
}
|
||||
// Prefer write without response when supported; fall back to write with response.
|
||||
final properties = _rxCharacteristic!.properties;
|
||||
final canWriteWithoutResponse = properties.writeWithoutResponse;
|
||||
final canWriteWithResponse = properties.write;
|
||||
if (!canWriteWithoutResponse && !canWriteWithResponse) {
|
||||
throw Exception("MeshCore RX characteristic does not support write");
|
||||
}
|
||||
await _rxCharacteristic!.write(
|
||||
data.toList(),
|
||||
withoutResponse: canWriteWithoutResponse,
|
||||
);
|
||||
}
|
||||
|
||||
await _rxCharacteristic!.write(
|
||||
data.toList(),
|
||||
withoutResponse: canWriteWithoutResponse,
|
||||
);
|
||||
_trackPendingGenericAck(
|
||||
data,
|
||||
channelSendQueueId: channelSendQueueId,
|
||||
@@ -2000,10 +2094,12 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
|
||||
// Auto-fetch contacts after getting self info
|
||||
getContacts();
|
||||
_maybeStartInitialChannelSync();
|
||||
}
|
||||
|
||||
void _handleDeviceInfo(Uint8List frame) {
|
||||
if (frame.length < 4) return;
|
||||
_hasReceivedDeviceInfo = true;
|
||||
_firmwareVerCode = frame[1];
|
||||
|
||||
// Parse client_repeat from firmware v9+ (byte 80)
|
||||
@@ -2027,12 +2123,25 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
if (nextMaxChannels > previousMaxChannels) {
|
||||
unawaited(loadChannelSettings(maxChannels: nextMaxChannels));
|
||||
unawaited(loadAllChannelMessages(maxChannels: nextMaxChannels));
|
||||
if (isConnected) {
|
||||
if (isConnected && !_pendingInitialChannelSync) {
|
||||
unawaited(getChannels(maxChannels: nextMaxChannels));
|
||||
}
|
||||
}
|
||||
}
|
||||
notifyListeners();
|
||||
_maybeStartInitialChannelSync();
|
||||
}
|
||||
|
||||
void _maybeStartInitialChannelSync() {
|
||||
if (!_pendingInitialChannelSync || !isConnected) {
|
||||
return;
|
||||
}
|
||||
if (_selfPublicKey == null || !_hasReceivedDeviceInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
_pendingInitialChannelSync = false;
|
||||
unawaited(getChannels(maxChannels: _maxChannels));
|
||||
}
|
||||
|
||||
void _handleNoMoreMessages() {
|
||||
@@ -3591,6 +3700,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_txCharacteristic = null;
|
||||
// Preserve deviceId and displayName for UI display during reconnection
|
||||
// They're only cleared on manual disconnect via disconnect() method
|
||||
_hasReceivedDeviceInfo = false;
|
||||
_pendingInitialChannelSync = false;
|
||||
_maxContacts = _defaultMaxContacts;
|
||||
_maxChannels = _defaultMaxChannels;
|
||||
_isSyncingQueuedMessages = false;
|
||||
@@ -3671,10 +3782,12 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
void dispose() {
|
||||
_scanSubscription?.cancel();
|
||||
_connectionSubscription?.cancel();
|
||||
_usbFrameSubscription?.cancel();
|
||||
_notifySubscription?.cancel();
|
||||
_reconnectTimer?.cancel();
|
||||
_batteryPollTimer?.cancel();
|
||||
_receivedFramesController.close();
|
||||
_usbSerialService.dispose();
|
||||
|
||||
// Flush pending unread writes before disposal
|
||||
_unreadStore.flush();
|
||||
|
||||
+10
-1
@@ -1801,5 +1801,14 @@
|
||||
"contacts_unread": "Непрочетено",
|
||||
"contacts_searchRepeaters": "Търсене на {number}{str} повтарящи се...",
|
||||
"contacts_searchContactsNoNumber": "Търси контакти...",
|
||||
"contacts_searchUsers": "Търсене на {number}{str} потребители..."
|
||||
"contacts_searchUsers": "Търсене на {number}{str} потребители...",
|
||||
"connectionChoiceUsbLabel": "USB",
|
||||
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||
"connectionChoiceTitle": "Изберете метода на връзка.",
|
||||
"connectionChoiceSubtitle": "Изберете как искате да получите вашия устройство MeshCore.",
|
||||
"usbScreenTitle": "Връзката чрез USB ще бъде налична скоро.",
|
||||
"usbScreenSubtitle": "Създаваме път за комуникация, базиран на последователно предаване на данни, за Android и настолни компютри.",
|
||||
"usbScreenStatus": "Ще бъде достъпно скоро",
|
||||
"usbScreenNote": "След като бъде внедрена поддръжката за USB, ще изберете сериен порт и ще се свържете директно към вашето устройство MeshCore.",
|
||||
"usbScreenEmptyState": "Няма открити USB устройства. Включете едно и опитайте отново."
|
||||
}
|
||||
|
||||
+10
-1
@@ -1829,5 +1829,14 @@
|
||||
"contacts_searchRepeaters": "Suche {number}{str} Repeater...",
|
||||
"contacts_searchFavorites": "Suche {number}{str} Favoriten...",
|
||||
"contacts_searchUsers": "Suche {number}{str} Benutzer...",
|
||||
"contacts_searchRoomServers": "Suche {number}{str} Raumserver..."
|
||||
"contacts_searchRoomServers": "Suche {number}{str} Raumserver...",
|
||||
"connectionChoiceSubtitle": "Wählen Sie, wie Sie Ihr MeshCore-Gerät erreichen möchten.",
|
||||
"connectionChoiceUsbLabel": "USB",
|
||||
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||
"connectionChoiceTitle": "Wählen Sie Ihre bevorzugte Verbindungsmethode.",
|
||||
"usbScreenTitle": "Die USB-Verbindung wird bald verfügbar sein.",
|
||||
"usbScreenSubtitle": "Wir entwickeln eine Verbindung, die sowohl für Android- als auch für Desktop-Geräte geeignet ist und auf einer seriellen Schnittstelle basiert.",
|
||||
"usbScreenStatus": "Bald verfügbar",
|
||||
"usbScreenNote": "Sobald die USB-Unterstützung implementiert ist, wählen Sie einen seriellen Anschluss und verbinden Sie ihn direkt mit Ihrem MeshCore-Gerät.",
|
||||
"usbScreenEmptyState": "Keine USB-Geräte gefunden. Schließen Sie eines an und aktualisieren Sie."
|
||||
}
|
||||
|
||||
@@ -46,6 +46,15 @@
|
||||
}
|
||||
},
|
||||
"scanner_title": "MeshCore Open",
|
||||
"connectionChoiceTitle": "Choose your connection method",
|
||||
"connectionChoiceSubtitle": "Select how you would like to reach your MeshCore device.",
|
||||
"connectionChoiceUsbLabel": "USB",
|
||||
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||
"usbScreenTitle": "Connect over USB",
|
||||
"usbScreenSubtitle": "Choose a detected serial device and connect directly to your MeshCore node.",
|
||||
"usbScreenStatus": "Select a USB device",
|
||||
"usbScreenNote": "USB serial is active on supported Android devices and desktop platforms.",
|
||||
"usbScreenEmptyState": "No USB devices found. Plug one in and refresh.",
|
||||
"scanner_scanning": "Scanning for devices...",
|
||||
"scanner_connecting": "Connecting...",
|
||||
"scanner_disconnecting": "Disconnecting...",
|
||||
|
||||
+10
-1
@@ -1829,5 +1829,14 @@
|
||||
"contacts_searchFavorites": "Buscar {number}{str} Favoritos...",
|
||||
"contacts_searchUsers": "Buscar {number}{str} Usuarios...",
|
||||
"contacts_searchRepeaters": "Buscar {number}{str} Repetidores...",
|
||||
"contacts_searchRoomServers": "Buscar {number}{str} servidores de sala..."
|
||||
"contacts_searchRoomServers": "Buscar {number}{str} servidores de sala...",
|
||||
"connectionChoiceTitle": "Seleccione su método de conexión.",
|
||||
"connectionChoiceSubtitle": "Seleccione la forma en que desea acceder a su dispositivo MeshCore.",
|
||||
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||
"connectionChoiceUsbLabel": "USB",
|
||||
"usbScreenTitle": "La conexión USB estará disponible próximamente.",
|
||||
"usbScreenSubtitle": "Estamos creando una conexión en serie para dispositivos Android y de escritorio.",
|
||||
"usbScreenStatus": "Próximamente",
|
||||
"usbScreenNote": "Una vez que se implemente el soporte para USB, seleccionará un puerto serie y se conectará directamente a su dispositivo MeshCore.",
|
||||
"usbScreenEmptyState": "No se detectaron dispositivos USB. Conecte uno y vuelva a intentar."
|
||||
}
|
||||
|
||||
+10
-1
@@ -1801,5 +1801,14 @@
|
||||
"contacts_searchUsers": "Rechercher {number}{str} utilisateurs...",
|
||||
"contacts_searchRoomServers": "Rechercher {number}{str} serveurs de salle...",
|
||||
"contacts_searchRepeaters": "Rechercher {number}{str} Répéteurs...",
|
||||
"contacts_searchContactsNoNumber": "Rechercher des contacts..."
|
||||
"contacts_searchContactsNoNumber": "Rechercher des contacts...",
|
||||
"connectionChoiceTitle": "Choisissez votre méthode de connexion.",
|
||||
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||
"connectionChoiceUsbLabel": "USB",
|
||||
"connectionChoiceSubtitle": "Choisissez la méthode de livraison que vous préférez pour votre appareil MeshCore.",
|
||||
"usbScreenTitle": "La connexion USB sera disponible prochainement.",
|
||||
"usbScreenSubtitle": "Nous mettons en place un chemin de connexion basé sur une série pour les appareils Android et les ordinateurs de bureau.",
|
||||
"usbScreenStatus": "Bientôt",
|
||||
"usbScreenNote": "Une fois que le support USB sera disponible, vous sélectionnerez un port série et vous connecterez directement à votre appareil MeshCore.",
|
||||
"usbScreenEmptyState": "Aucun périphérique USB n'a été trouvé. Connectez-en un et rafraîchissez."
|
||||
}
|
||||
|
||||
+10
-1
@@ -1801,5 +1801,14 @@
|
||||
"contacts_searchFavorites": "Cerca {number}{str} Preferiti...",
|
||||
"contacts_unread": "Non letti",
|
||||
"contacts_searchRepeaters": "Cerca {number}{str} Ripetitori...",
|
||||
"contacts_searchRoomServers": "Cerca {number}{str} server Room..."
|
||||
"contacts_searchRoomServers": "Cerca {number}{str} server Room...",
|
||||
"connectionChoiceTitle": "Scegli il metodo di connessione che preferisci.",
|
||||
"connectionChoiceSubtitle": "Seleziona il metodo che preferisci per accedere al tuo dispositivo MeshCore.",
|
||||
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||
"connectionChoiceUsbLabel": "USB",
|
||||
"usbScreenTitle": "La connessione USB sarà disponibile a breve.",
|
||||
"usbScreenSubtitle": "Stiamo sviluppando un percorso di connessione basato su serie per Android e per i desktop.",
|
||||
"usbScreenStatus": "Arriverà presto",
|
||||
"usbScreenNote": "Una volta che il supporto USB sarà disponibile, selezionerete una porta seriale e vi connetterete direttamente al vostro dispositivo MeshCore.",
|
||||
"usbScreenEmptyState": "Nessun dispositivo USB rilevato. Collegare uno e riavviare."
|
||||
}
|
||||
|
||||
@@ -316,6 +316,60 @@ abstract class AppLocalizations {
|
||||
/// **'MeshCore Open'**
|
||||
String get scanner_title;
|
||||
|
||||
/// No description provided for @connectionChoiceTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Choose your connection method'**
|
||||
String get connectionChoiceTitle;
|
||||
|
||||
/// No description provided for @connectionChoiceSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Select how you would like to reach your MeshCore device.'**
|
||||
String get connectionChoiceSubtitle;
|
||||
|
||||
/// No description provided for @connectionChoiceUsbLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'USB'**
|
||||
String get connectionChoiceUsbLabel;
|
||||
|
||||
/// No description provided for @connectionChoiceBluetoothLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Bluetooth'**
|
||||
String get connectionChoiceBluetoothLabel;
|
||||
|
||||
/// No description provided for @usbScreenTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Connect over USB'**
|
||||
String get usbScreenTitle;
|
||||
|
||||
/// No description provided for @usbScreenSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Choose a detected serial device and connect directly to your MeshCore node.'**
|
||||
String get usbScreenSubtitle;
|
||||
|
||||
/// No description provided for @usbScreenStatus.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Select a USB device'**
|
||||
String get usbScreenStatus;
|
||||
|
||||
/// No description provided for @usbScreenNote.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'USB serial is active on supported Android devices and desktop platforms.'**
|
||||
String get usbScreenNote;
|
||||
|
||||
/// No description provided for @usbScreenEmptyState.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No USB devices found. Plug one in and refresh.'**
|
||||
String get usbScreenEmptyState;
|
||||
|
||||
/// No description provided for @scanner_scanning.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
||||
@@ -108,6 +108,37 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_title => 'MeshCore Open';
|
||||
|
||||
@override
|
||||
String get connectionChoiceTitle => 'Изберете метода на връзка.';
|
||||
|
||||
@override
|
||||
String get connectionChoiceSubtitle =>
|
||||
'Изберете как искате да получите вашия устройство MeshCore.';
|
||||
|
||||
@override
|
||||
String get connectionChoiceUsbLabel => 'USB';
|
||||
|
||||
@override
|
||||
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||
|
||||
@override
|
||||
String get usbScreenTitle => 'Връзката чрез USB ще бъде налична скоро.';
|
||||
|
||||
@override
|
||||
String get usbScreenSubtitle =>
|
||||
'Създаваме път за комуникация, базиран на последователно предаване на данни, за Android и настолни компютри.';
|
||||
|
||||
@override
|
||||
String get usbScreenStatus => 'Ще бъде достъпно скоро';
|
||||
|
||||
@override
|
||||
String get usbScreenNote =>
|
||||
'След като бъде внедрена поддръжката за USB, ще изберете сериен порт и ще се свържете директно към вашето устройство MeshCore.';
|
||||
|
||||
@override
|
||||
String get usbScreenEmptyState =>
|
||||
'Няма открити USB устройства. Включете едно и опитайте отново.';
|
||||
|
||||
@override
|
||||
String get scanner_scanning => 'Сканиране за устройства...';
|
||||
|
||||
|
||||
@@ -108,6 +108,38 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_title => 'MeshCore Open';
|
||||
|
||||
@override
|
||||
String get connectionChoiceTitle =>
|
||||
'Wählen Sie Ihre bevorzugte Verbindungsmethode.';
|
||||
|
||||
@override
|
||||
String get connectionChoiceSubtitle =>
|
||||
'Wählen Sie, wie Sie Ihr MeshCore-Gerät erreichen möchten.';
|
||||
|
||||
@override
|
||||
String get connectionChoiceUsbLabel => 'USB';
|
||||
|
||||
@override
|
||||
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||
|
||||
@override
|
||||
String get usbScreenTitle => 'Die USB-Verbindung wird bald verfügbar sein.';
|
||||
|
||||
@override
|
||||
String get usbScreenSubtitle =>
|
||||
'Wir entwickeln eine Verbindung, die sowohl für Android- als auch für Desktop-Geräte geeignet ist und auf einer seriellen Schnittstelle basiert.';
|
||||
|
||||
@override
|
||||
String get usbScreenStatus => 'Bald verfügbar';
|
||||
|
||||
@override
|
||||
String get usbScreenNote =>
|
||||
'Sobald die USB-Unterstützung implementiert ist, wählen Sie einen seriellen Anschluss und verbinden Sie ihn direkt mit Ihrem MeshCore-Gerät.';
|
||||
|
||||
@override
|
||||
String get usbScreenEmptyState =>
|
||||
'Keine USB-Geräte gefunden. Schließen Sie eines an und aktualisieren Sie.';
|
||||
|
||||
@override
|
||||
String get scanner_scanning => 'Scannen nach Geräten...';
|
||||
|
||||
|
||||
@@ -108,6 +108,37 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_title => 'MeshCore Open';
|
||||
|
||||
@override
|
||||
String get connectionChoiceTitle => 'Choose your connection method';
|
||||
|
||||
@override
|
||||
String get connectionChoiceSubtitle =>
|
||||
'Select how you would like to reach your MeshCore device.';
|
||||
|
||||
@override
|
||||
String get connectionChoiceUsbLabel => 'USB';
|
||||
|
||||
@override
|
||||
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||
|
||||
@override
|
||||
String get usbScreenTitle => 'Connect over USB';
|
||||
|
||||
@override
|
||||
String get usbScreenSubtitle =>
|
||||
'Choose a detected serial device and connect directly to your MeshCore node.';
|
||||
|
||||
@override
|
||||
String get usbScreenStatus => 'Select a USB device';
|
||||
|
||||
@override
|
||||
String get usbScreenNote =>
|
||||
'USB serial is active on supported Android devices and desktop platforms.';
|
||||
|
||||
@override
|
||||
String get usbScreenEmptyState =>
|
||||
'No USB devices found. Plug one in and refresh.';
|
||||
|
||||
@override
|
||||
String get scanner_scanning => 'Scanning for devices...';
|
||||
|
||||
|
||||
@@ -108,6 +108,38 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_title => 'MeshCore Open';
|
||||
|
||||
@override
|
||||
String get connectionChoiceTitle => 'Seleccione su método de conexión.';
|
||||
|
||||
@override
|
||||
String get connectionChoiceSubtitle =>
|
||||
'Seleccione la forma en que desea acceder a su dispositivo MeshCore.';
|
||||
|
||||
@override
|
||||
String get connectionChoiceUsbLabel => 'USB';
|
||||
|
||||
@override
|
||||
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||
|
||||
@override
|
||||
String get usbScreenTitle =>
|
||||
'La conexión USB estará disponible próximamente.';
|
||||
|
||||
@override
|
||||
String get usbScreenSubtitle =>
|
||||
'Estamos creando una conexión en serie para dispositivos Android y de escritorio.';
|
||||
|
||||
@override
|
||||
String get usbScreenStatus => 'Próximamente';
|
||||
|
||||
@override
|
||||
String get usbScreenNote =>
|
||||
'Una vez que se implemente el soporte para USB, seleccionará un puerto serie y se conectará directamente a su dispositivo MeshCore.';
|
||||
|
||||
@override
|
||||
String get usbScreenEmptyState =>
|
||||
'No se detectaron dispositivos USB. Conecte uno y vuelva a intentar.';
|
||||
|
||||
@override
|
||||
String get scanner_scanning => 'Escaneando dispositivos...';
|
||||
|
||||
|
||||
@@ -108,6 +108,38 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_title => 'MeshCore Open';
|
||||
|
||||
@override
|
||||
String get connectionChoiceTitle => 'Choisissez votre méthode de connexion.';
|
||||
|
||||
@override
|
||||
String get connectionChoiceSubtitle =>
|
||||
'Choisissez la méthode de livraison que vous préférez pour votre appareil MeshCore.';
|
||||
|
||||
@override
|
||||
String get connectionChoiceUsbLabel => 'USB';
|
||||
|
||||
@override
|
||||
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||
|
||||
@override
|
||||
String get usbScreenTitle =>
|
||||
'La connexion USB sera disponible prochainement.';
|
||||
|
||||
@override
|
||||
String get usbScreenSubtitle =>
|
||||
'Nous mettons en place un chemin de connexion basé sur une série pour les appareils Android et les ordinateurs de bureau.';
|
||||
|
||||
@override
|
||||
String get usbScreenStatus => 'Bientôt';
|
||||
|
||||
@override
|
||||
String get usbScreenNote =>
|
||||
'Une fois que le support USB sera disponible, vous sélectionnerez un port série et vous connecterez directement à votre appareil MeshCore.';
|
||||
|
||||
@override
|
||||
String get usbScreenEmptyState =>
|
||||
'Aucun périphérique USB n\'a été trouvé. Connectez-en un et rafraîchissez.';
|
||||
|
||||
@override
|
||||
String get scanner_scanning => 'Recherche de périphériques...';
|
||||
|
||||
|
||||
@@ -108,6 +108,38 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_title => 'MeshCore Open';
|
||||
|
||||
@override
|
||||
String get connectionChoiceTitle =>
|
||||
'Scegli il metodo di connessione che preferisci.';
|
||||
|
||||
@override
|
||||
String get connectionChoiceSubtitle =>
|
||||
'Seleziona il metodo che preferisci per accedere al tuo dispositivo MeshCore.';
|
||||
|
||||
@override
|
||||
String get connectionChoiceUsbLabel => 'USB';
|
||||
|
||||
@override
|
||||
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||
|
||||
@override
|
||||
String get usbScreenTitle => 'La connessione USB sarà disponibile a breve.';
|
||||
|
||||
@override
|
||||
String get usbScreenSubtitle =>
|
||||
'Stiamo sviluppando un percorso di connessione basato su serie per Android e per i desktop.';
|
||||
|
||||
@override
|
||||
String get usbScreenStatus => 'Arriverà presto';
|
||||
|
||||
@override
|
||||
String get usbScreenNote =>
|
||||
'Una volta che il supporto USB sarà disponibile, selezionerete una porta seriale e vi connetterete direttamente al vostro dispositivo MeshCore.';
|
||||
|
||||
@override
|
||||
String get usbScreenEmptyState =>
|
||||
'Nessun dispositivo USB rilevato. Collegare uno e riavviare.';
|
||||
|
||||
@override
|
||||
String get scanner_scanning => 'Scansione in corso per i dispositivi...';
|
||||
|
||||
|
||||
@@ -108,6 +108,37 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_title => 'MeshCore Open';
|
||||
|
||||
@override
|
||||
String get connectionChoiceTitle => 'Kies uw verbindingsmethode';
|
||||
|
||||
@override
|
||||
String get connectionChoiceSubtitle =>
|
||||
'Kies hoe u uw MeshCore-apparaat wilt bereiken.';
|
||||
|
||||
@override
|
||||
String get connectionChoiceUsbLabel => 'USB';
|
||||
|
||||
@override
|
||||
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||
|
||||
@override
|
||||
String get usbScreenTitle => 'USB-verbinding is binnenkort beschikbaar.';
|
||||
|
||||
@override
|
||||
String get usbScreenSubtitle =>
|
||||
'We ontwikkelen een verbindingspad op basis van seriële communicatie, zowel voor Android als voor desktop-computers.';
|
||||
|
||||
@override
|
||||
String get usbScreenStatus => 'Komende week';
|
||||
|
||||
@override
|
||||
String get usbScreenNote =>
|
||||
'Zodra de USB-ondersteuning is geïnstalleerd, selecteert u een seriële poort en verbindt u direct met uw MeshCore-apparaat.';
|
||||
|
||||
@override
|
||||
String get usbScreenEmptyState =>
|
||||
'Geen USB-apparaten gevonden. Sluit er een aan en herlaad.';
|
||||
|
||||
@override
|
||||
String get scanner_scanning => 'Scannen naar apparaten...';
|
||||
|
||||
|
||||
@@ -108,6 +108,37 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_title => 'MeshCore Open';
|
||||
|
||||
@override
|
||||
String get connectionChoiceTitle => 'Wybierz metodę połączenia.';
|
||||
|
||||
@override
|
||||
String get connectionChoiceSubtitle =>
|
||||
'Wybierz, w jaki sposób chcesz uzyskać dostęp do swojego urządzenia MeshCore.';
|
||||
|
||||
@override
|
||||
String get connectionChoiceUsbLabel => 'USB';
|
||||
|
||||
@override
|
||||
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||
|
||||
@override
|
||||
String get usbScreenTitle => 'Połączenie USB będzie dostępne wkrótce.';
|
||||
|
||||
@override
|
||||
String get usbScreenSubtitle =>
|
||||
'Tworzymy ścieżkę połączenia opartą na protokole szeregowym, przeznaczoną zarówno dla urządzeń z systemem Android, jak i dla komputerów stacjonarnych.';
|
||||
|
||||
@override
|
||||
String get usbScreenStatus => 'Wkrótce';
|
||||
|
||||
@override
|
||||
String get usbScreenNote =>
|
||||
'Po wdrożeniu wsparcia dla USB, wybierzesz port szeregowy i połączysz się bezpośrednio z urządzeniem MeshCore.';
|
||||
|
||||
@override
|
||||
String get usbScreenEmptyState =>
|
||||
'Nie znaleziono żadnych urządzeń USB. Podłącz jedno i zaktualizuj.';
|
||||
|
||||
@override
|
||||
String get scanner_scanning => 'Skanowanie urządzeń...';
|
||||
|
||||
|
||||
@@ -108,6 +108,37 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_title => 'MeshCore Open';
|
||||
|
||||
@override
|
||||
String get connectionChoiceTitle => 'Escolha o método de conexão desejado.';
|
||||
|
||||
@override
|
||||
String get connectionChoiceSubtitle =>
|
||||
'Selecione a forma como você deseja acessar seu dispositivo MeshCore.';
|
||||
|
||||
@override
|
||||
String get connectionChoiceUsbLabel => 'USB';
|
||||
|
||||
@override
|
||||
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||
|
||||
@override
|
||||
String get usbScreenTitle => 'A conexão USB estará disponível em breve.';
|
||||
|
||||
@override
|
||||
String get usbScreenSubtitle =>
|
||||
'Estamos criando um caminho de conexão baseado em série para dispositivos Android e de desktop.';
|
||||
|
||||
@override
|
||||
String get usbScreenStatus => 'Em breve';
|
||||
|
||||
@override
|
||||
String get usbScreenNote =>
|
||||
'Assim que o suporte USB for implementado, você poderá selecionar uma porta serial e conectar-se diretamente ao seu dispositivo MeshCore.';
|
||||
|
||||
@override
|
||||
String get usbScreenEmptyState =>
|
||||
'Nenhum dispositivo USB encontrado. Conecte um e atualize.';
|
||||
|
||||
@override
|
||||
String get scanner_scanning => 'Procurando por dispositivos...';
|
||||
|
||||
|
||||
@@ -108,6 +108,38 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_title => 'MeshCore Open';
|
||||
|
||||
@override
|
||||
String get connectionChoiceTitle => 'Выберите способ подключения';
|
||||
|
||||
@override
|
||||
String get connectionChoiceSubtitle =>
|
||||
'Выберите, каким способом вы хотите получить свой устройство MeshCore.';
|
||||
|
||||
@override
|
||||
String get connectionChoiceUsbLabel => 'USB';
|
||||
|
||||
@override
|
||||
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||
|
||||
@override
|
||||
String get usbScreenTitle =>
|
||||
'Подключение через USB будет доступно в ближайшее время.';
|
||||
|
||||
@override
|
||||
String get usbScreenSubtitle =>
|
||||
'Мы создаем последовательную схему подключения для устройств на базе Android и настольных компьютеров.';
|
||||
|
||||
@override
|
||||
String get usbScreenStatus => 'Скоро';
|
||||
|
||||
@override
|
||||
String get usbScreenNote =>
|
||||
'Как только появится поддержка USB, вы сможете выбрать последовательный порт и напрямую подключиться к вашему устройству MeshCore.';
|
||||
|
||||
@override
|
||||
String get usbScreenEmptyState =>
|
||||
'Не обнаружено никаких устройств USB. Подключите одно из них и обновите список.';
|
||||
|
||||
@override
|
||||
String get scanner_scanning => 'Поиск устройств...';
|
||||
|
||||
|
||||
@@ -108,6 +108,37 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_title => 'MeshCore Open';
|
||||
|
||||
@override
|
||||
String get connectionChoiceTitle => 'Vyberte si metódu prepojenia.';
|
||||
|
||||
@override
|
||||
String get connectionChoiceSubtitle =>
|
||||
'Vyberte si, ako chcete dosiahnuť váš zariadenie MeshCore.';
|
||||
|
||||
@override
|
||||
String get connectionChoiceUsbLabel => 'USB';
|
||||
|
||||
@override
|
||||
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||
|
||||
@override
|
||||
String get usbScreenTitle => 'Pripojenie cez USB bude k dispozícii čoskoro.';
|
||||
|
||||
@override
|
||||
String get usbScreenSubtitle =>
|
||||
'Vytvárajeme komunikačný systém založený na sériovej komunikácii pre Android a stolné počítače.';
|
||||
|
||||
@override
|
||||
String get usbScreenStatus => 'Čoskoro';
|
||||
|
||||
@override
|
||||
String get usbScreenNote =>
|
||||
'Po implementácii podpory pre USB, budete môcť vybrať sériový port a priamo sa pripojiť k vašmu zariadeniu MeshCore.';
|
||||
|
||||
@override
|
||||
String get usbScreenEmptyState =>
|
||||
'Nenašli sa žiadne USB zariadenia. Pripojte jedno a obnovte.';
|
||||
|
||||
@override
|
||||
String get scanner_scanning => 'Skrívania zariadení...';
|
||||
|
||||
|
||||
@@ -108,6 +108,37 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_title => 'MeshCore Open';
|
||||
|
||||
@override
|
||||
String get connectionChoiceTitle => 'Izberite svoj način povezave.';
|
||||
|
||||
@override
|
||||
String get connectionChoiceSubtitle =>
|
||||
'Izberite, kako želite dostopati do svojega naprave MeshCore.';
|
||||
|
||||
@override
|
||||
String get connectionChoiceUsbLabel => 'USB';
|
||||
|
||||
@override
|
||||
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||
|
||||
@override
|
||||
String get usbScreenTitle => 'Vnos preko USB-ja bo v kratkem na voljo.';
|
||||
|
||||
@override
|
||||
String get usbScreenSubtitle =>
|
||||
'Gradimo pot za serijsko povezavo za Android in računalnike.';
|
||||
|
||||
@override
|
||||
String get usbScreenStatus => 'Čez kratko časa';
|
||||
|
||||
@override
|
||||
String get usbScreenNote =>
|
||||
'Ko bo podpora za USB na voljo, boste izbrali serijsko vrata in se neposredno povezali z vašim napravem MeshCore.';
|
||||
|
||||
@override
|
||||
String get usbScreenEmptyState =>
|
||||
'Niti en USB naprave niso bilo najdeno. Povežite eno in posodobite.';
|
||||
|
||||
@override
|
||||
String get scanner_scanning => 'Skeniram za naprave...';
|
||||
|
||||
|
||||
@@ -108,6 +108,37 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_title => 'MeshCore Open';
|
||||
|
||||
@override
|
||||
String get connectionChoiceTitle => 'Välj din anslutningsmetod';
|
||||
|
||||
@override
|
||||
String get connectionChoiceSubtitle =>
|
||||
'Välj hur du vill komma åt din MeshCore-enhet.';
|
||||
|
||||
@override
|
||||
String get connectionChoiceUsbLabel => 'USB';
|
||||
|
||||
@override
|
||||
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||
|
||||
@override
|
||||
String get usbScreenTitle => 'USB-anslutning kommer snart';
|
||||
|
||||
@override
|
||||
String get usbScreenSubtitle =>
|
||||
'Vi skapar en seriebaserad anslutningsväg för både Android- och skrivbordsenheter.';
|
||||
|
||||
@override
|
||||
String get usbScreenStatus => 'Kommer snart';
|
||||
|
||||
@override
|
||||
String get usbScreenNote =>
|
||||
'När USB-stöd är implementerat, kommer du att välja en seriell port och ansluta direkt till din MeshCore-enhet.';
|
||||
|
||||
@override
|
||||
String get usbScreenEmptyState =>
|
||||
'Inga USB-enheter hittades. Anslut en och uppdatera.';
|
||||
|
||||
@override
|
||||
String get scanner_scanning => 'Söker efter enheter...';
|
||||
|
||||
|
||||
@@ -108,6 +108,38 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_title => 'MeshCore Open';
|
||||
|
||||
@override
|
||||
String get connectionChoiceTitle => 'Виберіть спосіб зв\'язку';
|
||||
|
||||
@override
|
||||
String get connectionChoiceSubtitle =>
|
||||
'Виберіть, яким способом ви бажаєте отримати доступ до вашого пристрою MeshCore.';
|
||||
|
||||
@override
|
||||
String get connectionChoiceUsbLabel => 'USB';
|
||||
|
||||
@override
|
||||
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||
|
||||
@override
|
||||
String get usbScreenTitle =>
|
||||
'Підключення через USB буде доступне найближчим часом.';
|
||||
|
||||
@override
|
||||
String get usbScreenSubtitle =>
|
||||
'Ми створюємо серійний шлях з\'єднання для Android та десктопних комп\'ютерів.';
|
||||
|
||||
@override
|
||||
String get usbScreenStatus => 'Скоро';
|
||||
|
||||
@override
|
||||
String get usbScreenNote =>
|
||||
'Після того, як буде реалізовано підтримку USB, ви виберете серійний порт і підключитесь безпосередньо до вашого пристрою MeshCore.';
|
||||
|
||||
@override
|
||||
String get usbScreenEmptyState =>
|
||||
'Не знайдено жодних пристроїв USB. Підключіть один і перезавантажте.';
|
||||
|
||||
@override
|
||||
String get scanner_scanning => 'Пошук пристроїв...';
|
||||
|
||||
|
||||
@@ -108,6 +108,33 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_title => '连接设备';
|
||||
|
||||
@override
|
||||
String get connectionChoiceTitle => '选择您的连接方式';
|
||||
|
||||
@override
|
||||
String get connectionChoiceSubtitle => '请选择您希望如何访问 MeshCore 设备的选项。';
|
||||
|
||||
@override
|
||||
String get connectionChoiceUsbLabel => 'USB';
|
||||
|
||||
@override
|
||||
String get connectionChoiceBluetoothLabel => '蓝牙';
|
||||
|
||||
@override
|
||||
String get usbScreenTitle => 'USB 连接即将推出';
|
||||
|
||||
@override
|
||||
String get usbScreenSubtitle => '我们正在构建一个基于串行的连接路径,用于Android和桌面设备。';
|
||||
|
||||
@override
|
||||
String get usbScreenStatus => '即将推出';
|
||||
|
||||
@override
|
||||
String get usbScreenNote => '一旦USB支持功能上线,您就可以选择一个串口,并直接连接到您的MeshCore设备。';
|
||||
|
||||
@override
|
||||
String get usbScreenEmptyState => '未找到任何 USB 设备。请插入一个,然后刷新。';
|
||||
|
||||
@override
|
||||
String get scanner_scanning => '正在搜索设备...';
|
||||
|
||||
|
||||
+10
-1
@@ -1801,5 +1801,14 @@
|
||||
"contacts_searchContactsNoNumber": "Zoek contacten...",
|
||||
"contacts_searchUsers": "Zoek {number}{str} gebruikers...",
|
||||
"contacts_searchFavorites": "Zoek {number}{str} favorieten...",
|
||||
"contacts_searchRoomServers": "Zoek {number}{str} Room servers..."
|
||||
"contacts_searchRoomServers": "Zoek {number}{str} Room servers...",
|
||||
"connectionChoiceTitle": "Kies uw verbindingsmethode",
|
||||
"connectionChoiceUsbLabel": "USB",
|
||||
"connectionChoiceSubtitle": "Kies hoe u uw MeshCore-apparaat wilt bereiken.",
|
||||
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||
"usbScreenTitle": "USB-verbinding is binnenkort beschikbaar.",
|
||||
"usbScreenSubtitle": "We ontwikkelen een verbindingspad op basis van seriële communicatie, zowel voor Android als voor desktop-computers.",
|
||||
"usbScreenStatus": "Komende week",
|
||||
"usbScreenNote": "Zodra de USB-ondersteuning is geïnstalleerd, selecteert u een seriële poort en verbindt u direct met uw MeshCore-apparaat.",
|
||||
"usbScreenEmptyState": "Geen USB-apparaten gevonden. Sluit er een aan en herlaad."
|
||||
}
|
||||
|
||||
+10
-1
@@ -1801,5 +1801,14 @@
|
||||
"contacts_searchFavorites": "Wyszukaj {number}{str} ulubione...",
|
||||
"contacts_searchRoomServers": "Wyszukaj {number}{str} serwerów Room...",
|
||||
"contacts_searchUsers": "Wyszukaj {number}{str} Użytkowników...",
|
||||
"contacts_searchRepeaters": "Wyszukaj {number}{str} powtórników..."
|
||||
"contacts_searchRepeaters": "Wyszukaj {number}{str} powtórników...",
|
||||
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||
"connectionChoiceSubtitle": "Wybierz, w jaki sposób chcesz uzyskać dostęp do swojego urządzenia MeshCore.",
|
||||
"connectionChoiceTitle": "Wybierz metodę połączenia.",
|
||||
"connectionChoiceUsbLabel": "USB",
|
||||
"usbScreenTitle": "Połączenie USB będzie dostępne wkrótce.",
|
||||
"usbScreenSubtitle": "Tworzymy ścieżkę połączenia opartą na protokole szeregowym, przeznaczoną zarówno dla urządzeń z systemem Android, jak i dla komputerów stacjonarnych.",
|
||||
"usbScreenStatus": "Wkrótce",
|
||||
"usbScreenNote": "Po wdrożeniu wsparcia dla USB, wybierzesz port szeregowy i połączysz się bezpośrednio z urządzeniem MeshCore.",
|
||||
"usbScreenEmptyState": "Nie znaleziono żadnych urządzeń USB. Podłącz jedno i zaktualizuj."
|
||||
}
|
||||
|
||||
+10
-1
@@ -1801,5 +1801,14 @@
|
||||
"contacts_searchUsers": "Pesquisar {number}{str} Usuários...",
|
||||
"contacts_searchContactsNoNumber": "Pesquisar Contatos...",
|
||||
"contacts_unread": "Não lido",
|
||||
"contacts_searchRoomServers": "Pesquisar {number}{str} servidores de sala..."
|
||||
"contacts_searchRoomServers": "Pesquisar {number}{str} servidores de sala...",
|
||||
"connectionChoiceSubtitle": "Selecione a forma como você deseja acessar seu dispositivo MeshCore.",
|
||||
"connectionChoiceUsbLabel": "USB",
|
||||
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||
"connectionChoiceTitle": "Escolha o método de conexão desejado.",
|
||||
"usbScreenTitle": "A conexão USB estará disponível em breve.",
|
||||
"usbScreenSubtitle": "Estamos criando um caminho de conexão baseado em série para dispositivos Android e de desktop.",
|
||||
"usbScreenStatus": "Em breve",
|
||||
"usbScreenNote": "Assim que o suporte USB for implementado, você poderá selecionar uma porta serial e conectar-se diretamente ao seu dispositivo MeshCore.",
|
||||
"usbScreenEmptyState": "Nenhum dispositivo USB encontrado. Conecte um e atualize."
|
||||
}
|
||||
|
||||
+10
-1
@@ -1041,5 +1041,14 @@
|
||||
"contacts_unread": "Непрочитанное",
|
||||
"contacts_searchRoomServers": "Поиск {number}{str} серверов комнат...",
|
||||
"contacts_searchFavorites": "Поиск {number}{str} избранного...",
|
||||
"contacts_searchUsers": "Поиск {number}{str} пользователей..."
|
||||
"contacts_searchUsers": "Поиск {number}{str} пользователей...",
|
||||
"connectionChoiceSubtitle": "Выберите, каким способом вы хотите получить свой устройство MeshCore.",
|
||||
"connectionChoiceTitle": "Выберите способ подключения",
|
||||
"connectionChoiceUsbLabel": "USB",
|
||||
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||
"usbScreenTitle": "Подключение через USB будет доступно в ближайшее время.",
|
||||
"usbScreenSubtitle": "Мы создаем последовательную схему подключения для устройств на базе Android и настольных компьютеров.",
|
||||
"usbScreenStatus": "Скоро",
|
||||
"usbScreenNote": "Как только появится поддержка USB, вы сможете выбрать последовательный порт и напрямую подключиться к вашему устройству MeshCore.",
|
||||
"usbScreenEmptyState": "Не обнаружено никаких устройств USB. Подключите одно из них и обновите список."
|
||||
}
|
||||
|
||||
+10
-1
@@ -1801,5 +1801,14 @@
|
||||
"contacts_searchRepeaters": "Hľadať {number}{str} opakovače...",
|
||||
"contacts_searchUsers": "Hľadať {number}{str} používateľov...",
|
||||
"contacts_searchContactsNoNumber": "Hľadať kontakty...",
|
||||
"contacts_unread": "Neprečítané"
|
||||
"contacts_unread": "Neprečítané",
|
||||
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||
"connectionChoiceUsbLabel": "USB",
|
||||
"connectionChoiceTitle": "Vyberte si metódu prepojenia.",
|
||||
"connectionChoiceSubtitle": "Vyberte si, ako chcete dosiahnuť váš zariadenie MeshCore.",
|
||||
"usbScreenTitle": "Pripojenie cez USB bude k dispozícii čoskoro.",
|
||||
"usbScreenSubtitle": "Vytvárajeme komunikačný systém založený na sériovej komunikácii pre Android a stolné počítače.",
|
||||
"usbScreenStatus": "Čoskoro",
|
||||
"usbScreenNote": "Po implementácii podpory pre USB, budete môcť vybrať sériový port a priamo sa pripojiť k vašmu zariadeniu MeshCore.",
|
||||
"usbScreenEmptyState": "Nenašli sa žiadne USB zariadenia. Pripojte jedno a obnovte."
|
||||
}
|
||||
|
||||
+10
-1
@@ -1801,5 +1801,14 @@
|
||||
"contacts_searchRoomServers": "Išči {number}{str} strežnikov sob...",
|
||||
"contacts_searchContactsNoNumber": "Iskanje stikov...",
|
||||
"contacts_searchRepeaters": "Išči {number}{str} ponavljalnike...",
|
||||
"contacts_searchUsers": "Išči {number}{str} uporabnikov..."
|
||||
"contacts_searchUsers": "Išči {number}{str} uporabnikov...",
|
||||
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||
"connectionChoiceUsbLabel": "USB",
|
||||
"connectionChoiceTitle": "Izberite svoj način povezave.",
|
||||
"connectionChoiceSubtitle": "Izberite, kako želite dostopati do svojega naprave MeshCore.",
|
||||
"usbScreenTitle": "Vnos preko USB-ja bo v kratkem na voljo.",
|
||||
"usbScreenSubtitle": "Gradimo pot za serijsko povezavo za Android in računalnike.",
|
||||
"usbScreenStatus": "Čez kratko časa",
|
||||
"usbScreenNote": "Ko bo podpora za USB na voljo, boste izbrali serijsko vrata in se neposredno povezali z vašim napravem MeshCore.",
|
||||
"usbScreenEmptyState": "Niti en USB naprave niso bilo najdeno. Povežite eno in posodobite."
|
||||
}
|
||||
|
||||
+10
-1
@@ -1801,5 +1801,14 @@
|
||||
"contacts_searchRepeaters": "Sök {number}{str} upprepningsenheter...",
|
||||
"contacts_searchFavorites": "Sök {number}{str} Favoriter...",
|
||||
"contacts_searchUsers": "Sök {number}{str} användare...",
|
||||
"contacts_searchRoomServers": "Sök {number}{str} Room-servrar..."
|
||||
"contacts_searchRoomServers": "Sök {number}{str} Room-servrar...",
|
||||
"connectionChoiceUsbLabel": "USB",
|
||||
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||
"connectionChoiceSubtitle": "Välj hur du vill komma åt din MeshCore-enhet.",
|
||||
"connectionChoiceTitle": "Välj din anslutningsmetod",
|
||||
"usbScreenTitle": "USB-anslutning kommer snart",
|
||||
"usbScreenSubtitle": "Vi skapar en seriebaserad anslutningsväg för både Android- och skrivbordsenheter.",
|
||||
"usbScreenStatus": "Kommer snart",
|
||||
"usbScreenNote": "När USB-stöd är implementerat, kommer du att välja en seriell port och ansluta direkt till din MeshCore-enhet.",
|
||||
"usbScreenEmptyState": "Inga USB-enheter hittades. Anslut en och uppdatera."
|
||||
}
|
||||
|
||||
+10
-1
@@ -1801,5 +1801,14 @@
|
||||
"contacts_searchFavorites": "Пошук {number}{str} улюблених...",
|
||||
"contacts_searchContactsNoNumber": "Пошук контактів...",
|
||||
"contacts_searchRepeaters": "Пошук {number}{str} ретрансляторів...",
|
||||
"contacts_unread": "Непрочитане"
|
||||
"contacts_unread": "Непрочитане",
|
||||
"connectionChoiceSubtitle": "Виберіть, яким способом ви бажаєте отримати доступ до вашого пристрою MeshCore.",
|
||||
"connectionChoiceUsbLabel": "USB",
|
||||
"connectionChoiceTitle": "Виберіть спосіб зв'язку",
|
||||
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||
"usbScreenTitle": "Підключення через USB буде доступне найближчим часом.",
|
||||
"usbScreenSubtitle": "Ми створюємо серійний шлях з'єднання для Android та десктопних комп'ютерів.",
|
||||
"usbScreenStatus": "Скоро",
|
||||
"usbScreenNote": "Після того, як буде реалізовано підтримку USB, ви виберете серійний порт і підключитесь безпосередньо до вашого пристрою MeshCore.",
|
||||
"usbScreenEmptyState": "Не знайдено жодних пристроїв USB. Підключіть один і перезавантажте."
|
||||
}
|
||||
|
||||
+10
-1
@@ -1806,5 +1806,14 @@
|
||||
"contacts_searchRepeaters": "搜索 {number}{str} 重复器...",
|
||||
"contacts_searchContactsNoNumber": "搜索联系人...",
|
||||
"contacts_searchRoomServers": "搜索 {number}{str} 房间服务器...",
|
||||
"contacts_searchFavorites": "搜索 {number}{str} 收藏..."
|
||||
"contacts_searchFavorites": "搜索 {number}{str} 收藏...",
|
||||
"connectionChoiceSubtitle": "请选择您希望如何访问 MeshCore 设备的选项。",
|
||||
"connectionChoiceBluetoothLabel": "蓝牙",
|
||||
"connectionChoiceTitle": "选择您的连接方式",
|
||||
"connectionChoiceUsbLabel": "USB",
|
||||
"usbScreenTitle": "USB 连接即将推出",
|
||||
"usbScreenSubtitle": "我们正在构建一个基于串行的连接路径,用于Android和桌面设备。",
|
||||
"usbScreenStatus": "即将推出",
|
||||
"usbScreenNote": "一旦USB支持功能上线,您就可以选择一个串口,并直接连接到您的MeshCore设备。",
|
||||
"usbScreenEmptyState": "未找到任何 USB 设备。请插入一个,然后刷新。"
|
||||
}
|
||||
|
||||
+2
-2
@@ -8,7 +8,7 @@ import 'screens/chrome_required_screen.dart';
|
||||
import 'utils/platform_info.dart';
|
||||
|
||||
import 'connector/meshcore_connector.dart';
|
||||
import 'screens/scanner_screen.dart';
|
||||
import 'screens/connection_choice_screen.dart';
|
||||
import 'services/storage_service.dart';
|
||||
import 'services/message_retry_service.dart';
|
||||
import 'services/path_history_service.dart';
|
||||
@@ -192,7 +192,7 @@ class MeshCoreApp extends StatelessWidget {
|
||||
},
|
||||
home: (PlatformInfo.isWeb && !PlatformInfo.isChrome)
|
||||
? const ChromeRequiredScreen()
|
||||
: const ScannerScreen(),
|
||||
: const ConnectionChoiceScreen(),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -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> {
|
||||
bool _changedNavigation = false;
|
||||
late final MeshCoreConnector _connector;
|
||||
late final VoidCallback _connectionListener;
|
||||
BluetoothAdapterState _bluetoothState = BluetoothAdapterState.unknown;
|
||||
late StreamSubscription<BluetoothAdapterState> _bluetoothStateSubscription;
|
||||
@@ -27,12 +28,12 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
_connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
|
||||
_connectionListener = () {
|
||||
if (connector.state == MeshCoreConnectionState.disconnected) {
|
||||
if (_connector.state == MeshCoreConnectionState.disconnected) {
|
||||
_changedNavigation = false;
|
||||
} else if (connector.state == MeshCoreConnectionState.connected &&
|
||||
} else if (_connector.state == MeshCoreConnectionState.connected &&
|
||||
!_changedNavigation) {
|
||||
_changedNavigation = true;
|
||||
if (mounted) {
|
||||
@@ -43,7 +44,7 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
||||
}
|
||||
};
|
||||
|
||||
connector.addListener(_connectionListener);
|
||||
_connector.addListener(_connectionListener);
|
||||
|
||||
_bluetoothStateSubscription = FlutterBluePlus.adapterState.listen(
|
||||
(state) {
|
||||
@@ -53,7 +54,7 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
||||
});
|
||||
// Cancel scan if Bluetooth turns off while scanning
|
||||
if (state != BluetoothAdapterState.on) {
|
||||
unawaited(connector.stopScan());
|
||||
unawaited(_connector.stopScan());
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -65,16 +66,25 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
connector.removeListener(_connectionListener);
|
||||
_connector.removeListener(_connectionListener);
|
||||
unawaited(_bluetoothStateSubscription.cancel());
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final canPop = Navigator.of(context).canPop();
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: canPop
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () {
|
||||
debugPrint('ScannerScreen: back button pressed');
|
||||
Navigator.of(context).maybePop();
|
||||
},
|
||||
)
|
||||
: null,
|
||||
title: AdaptiveAppBarTitle(context.l10n.scanner_title),
|
||||
centerTitle: true,
|
||||
automaticallyImplyLeading: false,
|
||||
|
||||
@@ -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
|
||||
flserial
|
||||
)
|
||||
|
||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||
|
||||
@@ -38,6 +38,7 @@ dependencies:
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^1.0.8
|
||||
flutter_blue_plus: ^2.1.0
|
||||
flserial: ^0.3.5
|
||||
provider: ^6.1.5+1
|
||||
shared_preferences: ^2.2.2
|
||||
uuid: ^4.3.3
|
||||
|
||||
@@ -89,9 +89,11 @@ endif()
|
||||
|
||||
# Copy the native assets provided by the build.dart from all packages.
|
||||
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/")
|
||||
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
|
||||
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
if(EXISTS "${NATIVE_ASSETS_DIR}")
|
||||
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
|
||||
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
endif()
|
||||
|
||||
# Fully re-copy the assets directory on each build to avoid having stale files
|
||||
# from a previous install.
|
||||
|
||||
@@ -9,6 +9,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
flserial
|
||||
flutter_local_notifications_windows
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user