Compare commits

...

73 Commits

Author SHA1 Message Date
Winston Lowe f870e77e98 Merge branch 'main' into dev-dbDevicePrefix 2026-03-12 09:37:44 -07:00
Winston Lowe e6658a6026 Migrate storage keys to scoped keys across multiple store classes 2026-03-12 09:29:46 -07:00
Winston Lowe c81791cf1e Migrate legacy storage keys to scoped keys in various store classes (#289) 2026-03-12 08:39:17 -07:00
Winston Lowe 6da54e13c3 Migrate legacy storage keys to scoped keys in various store classes 2026-03-12 08:29:56 -07:00
Winston Lowe 1fba5312a2 Refactor storage classes to include companion's public key (#277)
* Refactor storage classes to include public key handling and improve data loading/saving logic

* Remove redundant publicKeyHex handling from ContactDiscoveryStore and fix key reference in saveContacts method

* Remove unused app_logger import from ContactDiscoveryStore

* Add warning log for empty publicKeyHex in saveChannelMessages method

* Add warning log for empty publicKeyHex in clearMessages method

* Migrate legacy storage keys to scoped keys across multiple stores

* Remove legacy unscoped keys during migration in storage classes

* Update lib/storage/contact_store.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-12 00:14:48 -07:00
zjs81 a1b77bb29b Merge pull request #269 from zjs81/dev-latLonFix
Changed contacts latitude and longitude fields to be null until parsed and set
2026-03-07 13:53:09 -07:00
zjs81 4eecfc92dc Merge pull request #252 from just-stuff-tm/feature/usb
Feature/usb
2026-03-07 13:16:39 -07:00
zjs81 90c8cf5f3e Add TODO to switch flserial to official repo 2026-03-07 13:12:45 -07:00
zjs81 06fa176367 Narrow macOS sandbox entitlement to /dev/cu. and /dev/tty. only
The /dev/ prefix granted read/write to all device nodes. The app only
needs access to serial port devices (/dev/cu.* and /dev/tty.*) for USB
LoRa communication.
2026-03-07 13:10:42 -07:00
zjs81 e4285774a0 Merge branch 'main' into feature/usb 2026-03-07 13:03:15 -07:00
zjs81 b2da695102 Run dart format 2026-03-07 13:01:27 -07:00
zjs81 e1327a93c7 Fix contact sync fallback when channel 0 never arrives
On web BLE, contact sync is deferred until channel 0 arrives via
_handleChannelInfo. If channel 0 times out or channel sync completes
without it, _pendingInitialContactsSync stays true and contacts never
load. Add fallback in _cleanupChannelSync to trigger getContacts() if
the flag is still set when channel sync ends.
2026-03-07 13:00:23 -07:00
zjs81 421bc71bb7 Enhance USB port opening and reading logic with improved error handling and debug logging 2026-03-07 12:55:15 -07:00
zjs81 fef73b7b62 Refactor USB screen, add debug logging, fix UI issues
- Rewrite UsbScreen to mirror ScannerScreen patterns (status bar,
  tap-to-connect port list, bottom FABs, SnackBar errors)
- Extract MeshCoreUsbManager from MeshCoreConnector for cleaner
  USB transport ownership
- Add debug logging throughout USB connection flow (connector,
  manager, web/native services)
- Print debug logs to console in debug mode even when app debug
  log setting is disabled
- Localize remaining hardcoded strings (Web Serial Device fallback
  label, USB status bar keys, companion firmware timeout hint)
- Fix Swedish misspelling in translations (stöderliga → stödda)
- Guard Linux notification init against missing D-Bus session bus
- Fix SNRIndicator hit-test error by adding minimum size constraints
- Update USB flow tests for new UI patterns
2026-03-07 12:38:28 -07:00
Winston Lowe 84ec139ce6 Add latitude and longitude fields to contact handling in MeshCoreConnector 2026-03-07 11:02:47 -08:00
Winston Lowe b748b96237 Enhance contact handling logic in MeshCoreConnector to support conditional addition based on auto-add settings (#268) 2026-03-07 01:45:53 -08:00
Winston Lowe c2671ac2ae Refactor data handling of contacts (#267)
* Refactor data handling in MeshCoreConnector and BufferReader for improved readability and efficiency

* Update lib/connector/meshcore_connector.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Fix pointer tracking in BufferReader's readCString method

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-07 01:23:46 -08:00
just-stuff-tm 8238b6197f Regenerated localization files 2026-03-07 01:16:04 -05:00
just_stuff_tm 435ba89982 Merge branch 'zjs81:main' into feature/usb 2026-03-06 20:41:58 -05:00
just-stuff-tm 0565cee461 Enhance message merging logic and improve USB port listing 2026-03-06 20:38:03 -05:00
just-stuff-tm ab2b509d6a Merge branch 'main' into feature/usb 2026-03-06 20:31:05 -05:00
zjs81 eba95af31f Merge pull request #259 from ericszimmermann/ez_shorten_lastSeen
Shorten lastSeen for en,de,es,fr
2026-03-06 18:11:26 -07:00
zjs81 04c016cfe1 Merge pull request #266 from zjs81/zjs81-patch-1
Fix formatting of cryptocurrency addresses in README
2026-03-06 15:41:21 -07:00
zjs81 ea2354712d Fix formatting of cryptocurrency addresses in README 2026-03-06 15:41:02 -07:00
zach 7a0b8aad3d Added more crypto payment options 2026-03-06 15:39:54 -07:00
zjs81 bd34bb5e88 Merge pull request #264 from zjs81/dev-guessed-locations
Dev guessed locations
2026-03-06 15:19:03 -07:00
just-stuff-tm fb58a3262c addressed codex review cleanup 2026-03-05 02:50:38 -05:00
just-stuff-tm f584c4fba0 added linux notification service 2026-03-05 02:26:37 -05:00
just-stuff-tm b5b930646f Update flserial dependency to a specific commit reference 2026-03-05 02:26:37 -05:00
just-stuff-tm 3452bdae8c Refactor test cases for USB flow and port labels for improved readability 2026-03-05 02:26:37 -05:00
just-stuff-tm 25fc9454a8 Add error handling tests for USB connection and listing ports 2026-03-05 02:26:37 -05:00
just_stuff_tm 524558c511 clean 2026-03-05 02:26:37 -05:00
just_stuff_tm 367e47bb1e Fix USB device name matching and correct localization strings 2026-03-05 02:26:37 -05:00
just_stuff_tm 21ff765e41 Refactor USB permission handling and reset initial channel sync flag 2026-03-05 02:26:37 -05:00
just_stuff_tm 38d40ca0a4 Enhance USB error handling and improve user feedback
- Updated the _friendlyErrorMessage method in UsbScreen to provide more user-friendly error messages based on specific PlatformException codes.
- Added localized error messages for various USB-related errors, improving clarity for users.
- Modified the UsbSerialService to rethrow exceptions instead of throwing StateError, allowing for better error propagation.
- Updated the usb_flow_test to reflect changes in the USB display label behavior, ensuring the test accurately describes the functionality.
2026-03-05 02:26:37 -05:00
just_stuff_tm 5b4535d5dc update flserial dependency reference from main to master 2026-03-05 02:26:37 -05:00
Ben Allfree f9b6299620 gitmodule cleanup 2026-03-05 02:26:37 -05:00
just_stuff_tm 7cb84dbf6f Dart Format 2026-03-05 02:26:37 -05:00
just_stuff_tm 44c0670dae Refine USB transport flow
- replace Android USB dependency with app-owned USB host implementation\n- restore BLE-first scanner flow with USB secondary action\n- tighten Web Serial key handling and disconnect logging\n\nTODO (follow-up):\n- review non-English localization copy for tone and consistency\n- trim remaining unused/awkward localization strings introduced during USB UI changes
2026-03-05 02:26:37 -05:00
Ben Allfree 74da9e82b5 wip 2026-03-05 02:25:46 -05:00
Ben Allfree 63583dadda wip 2026-03-05 02:25:46 -05:00
Ben Allfree 32632669c3 wip 2026-03-05 02:25:46 -05:00
Ben Allfree 3c0c0d1dea wip 2026-03-05 02:25:46 -05:00
Ben Allfree e6c9a3fea7 wip 2026-03-05 02:25:46 -05:00
just_stuff_tm f5154b0033 Improve sender name resolution for room server messages by handling missing room-contact keys 2026-03-05 02:25:46 -05:00
just_stuff_tm 4c7ee3b3b0 Enhance USB serial services with debug logging and reset functionality
- Introduced debug logging in USB serial services for better traceability.
- Added reset method to UsbSerialFrameDecoder to clear buffered data.
- Updated tests to verify the reset functionality of the decoder.
2026-03-05 02:25:46 -05:00
just_stuff_tm c2f544eeba I restored the Web BLE behavior in [meshcore_connector.dart] to the earlier Windows/Chrome-working state aligned with the logic that was present around commit fcef3de57837983a300634aa3e0a77622e945cc2,
What is back:
- Web BLE resets handshake state before connect
- skips `requestMtu()` on web
- retries `discoverServices()` once on the transient web disconnect case
- uses the non-blocking web `setNotifyValue(true)` workaround again
- skips the immediate `SELF_INFO` wait/refresh stack on web BLE
- defers contact loading on web BLE until after channel `0`
- uses the Web-specific bounded `SELF_INFO` retry timer
- re-enables initial channel-sync gating for web BLE
2026-03-05 02:25:05 -05:00
just_stuff_tm 98cdac4309 Refactor MeshCoreConnector to streamline connection handling and remove web-specific logic for contact synchronization... Back to the way it was before.. For some reason the fix worked on my machine but wwhen i built web from upstream it didnt work 2026-03-05 02:25:05 -05:00
just_stuff_tm d6d11eaad2 Update active USB port key and label on connection, notify listeners 2026-03-05 02:25:05 -05:00
just_stuff_tm 3cef9e81b6 Remove unawaited background service start during USB connection initialization 2026-03-05 02:25:05 -05:00
just_stuff_tm 5216e00807 Refactor USB port handling to introduce display labels and improve state management 2026-03-05 02:25:05 -05:00
just_stuff_tm a0feb129e1 Add post-frame callback to disconnect USB transport on dispose if not navigated to contacts 2026-03-05 02:25:05 -05:00
just_stuff_tm f39a22668e Add initial load scheduling and tests for USB screen and frame codec functionality 2026-03-05 02:25:05 -05:00
just_stuff_tm 781090243c Enhance USB functionality by adding request port label management and platform support checks 2026-03-05 02:25:05 -05:00
just_stuff_tm ca5784f3f8 Add post-frame callback to ensure disconnection on dispose when navigation hasn't changed 2026-03-05 02:25:05 -05:00
just_stuff_tm dcad5c586d Refactor USB connection handling to use scheduled closure and improve error management in USB services 2026-03-05 02:25:05 -05:00
just_stuff_tm 4b24506310 Remove unused import of 'dart:typed_data' in usb_serial_service_web.dart 2026-03-05 02:25:05 -05:00
just_stuff_tm 47c4e0fb82 Fix USB permission receiver registration for compatibility with Android Tiramisu 2026-03-05 02:25:05 -05:00
just_stuff_tm c041e05972 Improve error message for unavailable RX characteristic in USB communication 2026-03-05 02:25:05 -05:00
just_stuff_tm 612612795a Update French localization for connection choice subtitle 2026-03-05 02:25:05 -05:00
just_stuff_tm 3cec3dc233 Improve USB disconnection handling and add payload length validation for USB frames 2026-03-05 02:25:05 -05:00
just_stuff_tm 3542adad1d Update USB communication note for clarity in Swedish localization 2026-03-05 02:25:05 -05:00
just_stuff_tm 115689ad95 Improve USB connection handling by preventing connection attempts when already connected 2026-03-05 02:25:05 -05:00
just_stuff_tm 9a0572e8e4 Add payload length validation in USB frame decoder 2026-03-05 02:25:05 -05:00
just_stuff_tm 2d1160d992 Enhance BLE connection handling and improve USB connection messaging
- Wrapped BLE scan and connection methods in try-catch blocks to handle errors gracefully and provide debug output.
- Added retry logic for service discovery on web platforms after transient disconnections.
- Updated USB connection messages in multiple languages to reflect active support on Android and desktop platforms.
- Improved loading indicators for contacts screen to show a spinner during data loading.
2026-03-05 02:25:05 -05:00
just_stuff_tm ee3af52c0f Add initial contacts sync handling for web Bluetooth transport 2026-03-05 02:25:05 -05:00
just_stuff_tm 98f7c3b088 Refactor USB handling to improve connection management and error cleanup 2026-03-05 02:25:05 -05:00
just_stuff_tm f462815775 Refactor USB connection handling and improve notification setup 2026-03-05 02:25:05 -05:00
just_stuff_tm 5f4333398e Enhance Bluetooth scanning and notification handling for web platform 2026-03-05 02:25:05 -05:00
just_stuff_tm c23a1da430 Add web serial support and USB tests 2026-03-05 02:25:05 -05:00
just_stuff_tm 22a53439b1 Initialize USB Supoport for Andriod and Desktop 2026-03-05 02:25:05 -05:00
ericz 3502559fae minus to tilde 2026-03-04 22:49:20 +01:00
ericz e125318137 Shorten lastSeen for en,de,es,fr 2026-03-04 21:41:51 +01:00
77 changed files with 6085 additions and 318 deletions
View File
+5
View File
@@ -231,6 +231,11 @@ If you find MeshCore Open useful and would like to support development, you can
**Solana Address:** `F15YanjZj96YTBtKJYgNa8RLQLCZkx5CEwogPWkqXeoQ`
**Monero Address:** `453TxnpUqjkJtXxzdjMsrgERNkBRXEGamPbpC45ENrvKAk9tH7kZbxWF82Hz66etgDZyXFPEBU2JUEqhLeJyWt9kBvTVy5m`
**Bitcoin Address:** `bc1qh45x28v8dslcg4v4upmqd9g0mvc3lnyffmyzr5`
Your support helps maintain and improve this open-source project!
## Acknowledgments
+1
View File
@@ -19,6 +19,7 @@
<!-- Camera permission for QR code scanning -->
<uses-permission android:name="android.permission.CAMERA"/>
<uses-feature android:name="android.hardware.camera" android:required="false"/>
<uses-feature android:name="android.hardware.usb.host" android:required="false"/>
<application
android:label="meshcore_open"
@@ -1,5 +1,18 @@
package com.meshcore.meshcore_open
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
class MainActivity : FlutterActivity()
class MainActivity : FlutterActivity() {
private val usbFunctions by lazy { MeshcoreUsbFunctions(this) }
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
usbFunctions.configureFlutterEngine(flutterEngine)
}
override fun onDestroy() {
usbFunctions.dispose()
super.onDestroy()
}
}
@@ -0,0 +1,582 @@
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.UsbConstants
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbDeviceConnection
import android.hardware.usb.UsbEndpoint
import android.hardware.usb.UsbInterface
import android.hardware.usb.UsbManager
import android.os.Build
import android.os.Handler
import android.os.Looper
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
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
class MeshcoreUsbFunctions(
private val activity: FlutterActivity,
) {
private companion object {
const val usbRecipientInterface = 0x01
}
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 val usbManager by lazy {
activity.getSystemService(Context.USB_SERVICE) as UsbManager
}
private val mainHandler = Handler(Looper.getMainLooper())
private val usbIoExecutor: ExecutorService = Executors.newSingleThreadExecutor()
@Volatile private var eventSink: EventChannel.EventSink? = null
@Volatile private var usbConnection: UsbDeviceConnection? = null
@Volatile private var usbInEndpoint: UsbEndpoint? = null
@Volatile private var usbOutEndpoint: UsbEndpoint? = null
@Volatile private var controlInterface: UsbInterface? = null
@Volatile private var dataInterface: UsbInterface? = null
private var readThread: Thread? = null
@Volatile private var isReading = false
@Volatile private var connectedDeviceName: String? = null
private var pendingConnectResult: MethodChannel.Result? = null
private var pendingConnectPortName: String? = null
private var pendingConnectBaudRate: Int = 115200
private data class PortConfig(
val controlInterface: UsbInterface?,
val dataInterface: UsbInterface,
val inEndpoint: UsbEndpoint,
val outEndpoint: UsbEndpoint,
)
private val permissionReceiver =
object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
UsbManager.ACTION_USB_DEVICE_DETACHED -> {
handleUsbDetached(intent)
return
}
usbPermissionAction -> Unit
else -> return
}
val result = pendingConnectResult
val portName = pendingConnectPortName
pendingConnectResult = null
pendingConnectPortName = null
if (result == null || portName == null) {
return
}
val device = findUsbDevice(portName)
if (device == null) {
result.error(
"usb_device_missing",
null,
null,
)
return
}
val granted =
intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)
if (!granted || !usbManager.hasPermission(device)) {
result.error("usb_permission_denied", null, null)
return
}
openUsbDevice(device, pendingConnectBaudRate, result)
}
}
fun configureFlutterEngine(flutterEngine: FlutterEngine) {
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" -> {
scheduleCloseUsbConnection {
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
}
},
)
}
fun dispose() {
closeUsbConnection()
usbIoExecutor.shutdownNow()
try {
activity.unregisterReceiver(permissionReceiver)
} catch (_: IllegalArgumentException) {
}
}
private fun registerUsbPermissionReceiver() {
val filter =
IntentFilter().apply {
addAction(usbPermissionAction)
addAction(UsbManager.ACTION_USB_DEVICE_DETACHED)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
activity.registerReceiver(permissionReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
} else {
@Suppress("DEPRECATION")
activity.registerReceiver(permissionReceiver, filter)
}
}
private fun listUsbPorts(): List<String> {
return usbManager.deviceList.values.map { 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", null, null)
return
}
val device = findUsbDevice(portName)
if (device == null) {
result.error("usb_device_missing", null, null)
return
}
if (usbManager.hasPermission(device)) {
openUsbDevice(device, baudRate, result)
return
}
if (pendingConnectResult != null) {
result.error("usb_busy", null, null)
return
}
pendingConnectResult = result
pendingConnectPortName = portName
pendingConnectBaudRate = baudRate
val permissionIntent = PendingIntent.getBroadcast(
activity,
0,
Intent(usbPermissionAction).setPackage(activity.packageName),
pendingIntentFlags(),
)
usbManager.requestPermission(device, permissionIntent)
}
private fun handleUsbWrite(call: MethodCall, result: MethodChannel.Result) {
val data = call.argument<ByteArray>("data")
val connection = usbConnection
val endpoint = usbOutEndpoint
if (data == null) {
result.error("usb_invalid_data", null, null)
return
}
if (connection == null || endpoint == null) {
result.error("usb_not_connected", null, null)
return
}
usbIoExecutor.execute {
try {
writeToDevice(data)
mainHandler.post { result.success(null) }
} catch (error: Exception) {
mainHandler.post {
result.error("usb_write_failed", error.message, null)
}
}
}
}
private fun findUsbDevice(portName: String): UsbDevice? {
val devices = usbManager.deviceList.values
val exactMatch = devices.firstOrNull { it.deviceName == portName }
if (exactMatch != null) {
return exactMatch
}
val normalizedName = portName.substringBefore(" - ").trim()
return devices.firstOrNull { it.deviceName == normalizedName }
}
private fun openUsbDevice(
device: UsbDevice,
baudRate: Int,
result: MethodChannel.Result,
) {
usbIoExecutor.execute {
try {
closeUsbConnection()
val config = resolvePortConfig(device)
if (config == null) {
mainHandler.post {
result.error(
"usb_driver_missing",
null,
null,
)
}
return@execute
}
val connection = usbManager.openDevice(device)
if (connection == null) {
mainHandler.post {
result.error(
"usb_open_failed",
null,
null,
)
}
return@execute
}
if (!connection.claimInterface(config.dataInterface, true)) {
connection.close()
mainHandler.post {
result.error(
"usb_open_failed",
null,
null,
)
}
return@execute
}
if (config.controlInterface != null &&
config.controlInterface.id != config.dataInterface.id &&
!connection.claimInterface(config.controlInterface, true)
) {
connection.releaseInterface(config.dataInterface)
connection.close()
mainHandler.post {
result.error(
"usb_open_failed",
null,
null,
)
}
return@execute
}
usbConnection = connection
usbInEndpoint = config.inEndpoint
usbOutEndpoint = config.outEndpoint
controlInterface = config.controlInterface
dataInterface = config.dataInterface
configureDevice(connection, config, baudRate)
connectedDeviceName = device.deviceName
startReadLoop()
mainHandler.post {
result.success(null)
}
} catch (error: Exception) {
closeUsbConnection()
mainHandler.post {
result.error("usb_connect_failed", error.message, null)
}
}
}
}
private fun resolvePortConfig(device: UsbDevice): PortConfig? {
var preferredDataInterface: UsbInterface? = null
var preferredInEndpoint: UsbEndpoint? = null
var preferredOutEndpoint: UsbEndpoint? = null
var fallbackDataInterface: UsbInterface? = null
var fallbackInEndpoint: UsbEndpoint? = null
var fallbackOutEndpoint: UsbEndpoint? = null
var preferredControlInterface: UsbInterface? = null
for (interfaceIndex in 0 until device.interfaceCount) {
val usbInterface = device.getInterface(interfaceIndex)
var inEndpoint: UsbEndpoint? = null
var outEndpoint: UsbEndpoint? = null
for (endpointIndex in 0 until usbInterface.endpointCount) {
val endpoint = usbInterface.getEndpoint(endpointIndex)
if (endpoint.type != UsbConstants.USB_ENDPOINT_XFER_BULK) {
continue
}
when (endpoint.direction) {
UsbConstants.USB_DIR_IN -> if (inEndpoint == null) inEndpoint = endpoint
UsbConstants.USB_DIR_OUT -> if (outEndpoint == null) outEndpoint = endpoint
}
}
val hasDataPair = inEndpoint != null && outEndpoint != null
when {
usbInterface.interfaceClass == UsbConstants.USB_CLASS_COMM &&
preferredControlInterface == null -> {
preferredControlInterface = usbInterface
}
hasDataPair &&
usbInterface.interfaceClass == UsbConstants.USB_CLASS_CDC_DATA -> {
preferredDataInterface = usbInterface
preferredInEndpoint = inEndpoint
preferredOutEndpoint = outEndpoint
}
hasDataPair && fallbackDataInterface == null -> {
fallbackDataInterface = usbInterface
fallbackInEndpoint = inEndpoint
fallbackOutEndpoint = outEndpoint
}
}
}
val dataInterface = preferredDataInterface ?: fallbackDataInterface ?: return null
val inEndpoint = preferredInEndpoint ?: fallbackInEndpoint ?: return null
val outEndpoint = preferredOutEndpoint ?: fallbackOutEndpoint ?: return null
return PortConfig(preferredControlInterface, dataInterface, inEndpoint, outEndpoint)
}
private fun configureDevice(
connection: UsbDeviceConnection,
config: PortConfig,
baudRate: Int,
) {
val control = config.controlInterface ?: return
val lineCoding =
byteArrayOf(
(baudRate and 0xFF).toByte(),
((baudRate shr 8) and 0xFF).toByte(),
((baudRate shr 16) and 0xFF).toByte(),
((baudRate shr 24) and 0xFF).toByte(),
0, // stop bits: 1
0, // parity: none
8, // data bits
)
val lineCodingResult =
connection.controlTransfer(
UsbConstants.USB_DIR_OUT or
UsbConstants.USB_TYPE_CLASS or
usbRecipientInterface,
0x20,
0,
control.id,
lineCoding,
lineCoding.size,
1000,
)
if (lineCodingResult < 0) {
throw IllegalStateException("Failed to configure USB line coding")
}
val controlLineResult =
connection.controlTransfer(
UsbConstants.USB_DIR_OUT or
UsbConstants.USB_TYPE_CLASS or
usbRecipientInterface,
0x22,
0x0001, // DTR on, RTS off
control.id,
null,
0,
1000,
)
if (controlLineResult < 0) {
throw IllegalStateException("Failed to configure USB control line state")
}
}
private fun startReadLoop() {
val connection = usbConnection ?: return
val endpoint = usbInEndpoint ?: return
isReading = true
readThread =
Thread({
val packetSize = endpoint.maxPacketSize.coerceAtLeast(64)
val buffer = ByteArray(packetSize * 4)
try {
while (isReading) {
val bytesRead = connection.bulkTransfer(endpoint, buffer, buffer.size, 250)
if (!isReading) {
break
}
if (bytesRead <= 0) {
continue
}
val packet = buffer.copyOf(bytesRead)
mainHandler.post {
eventSink?.success(packet)
}
}
} catch (error: Exception) {
if (isReading) {
mainHandler.post {
eventSink?.error(
"usb_io_error",
error.message ?: "USB serial I/O error",
null,
)
}
scheduleCloseUsbConnection()
}
}
}, "MeshCoreUsbRead").also { thread ->
thread.isDaemon = true
thread.start()
}
}
private fun writeToDevice(data: ByteArray) {
val connection = usbConnection ?: throw IllegalStateException("USB connection missing")
val endpoint = usbOutEndpoint ?: throw IllegalStateException("USB output endpoint missing")
var offset = 0
val maxPacketSize = endpoint.maxPacketSize.coerceAtLeast(64)
while (offset < data.size) {
val chunkSize = minOf(maxPacketSize, data.size - offset)
val chunk = data.copyOfRange(offset, offset + chunkSize)
val bytesWritten = connection.bulkTransfer(endpoint, chunk, chunkSize, 1000)
if (bytesWritten != chunkSize) {
throw IllegalStateException("Short USB write: wrote $bytesWritten of $chunkSize bytes")
}
offset += chunkSize
}
}
private fun scheduleCloseUsbConnection(onComplete: (() -> Unit)? = null) {
usbIoExecutor.execute {
closeUsbConnection()
if (onComplete != null) {
mainHandler.post(onComplete)
}
}
}
@Synchronized
private fun closeUsbConnection() {
isReading = false
readThread?.interrupt()
if (readThread != null && readThread !== Thread.currentThread()) {
try {
readThread?.join(300)
} catch (_: InterruptedException) {
Thread.currentThread().interrupt()
}
}
readThread = null
val connection = usbConnection
val claimedControl = controlInterface
val claimedData = dataInterface
usbInEndpoint = null
usbOutEndpoint = null
controlInterface = null
dataInterface = null
usbConnection = null
if (connection != null) {
if (claimedControl != null) {
try {
connection.releaseInterface(claimedControl)
} catch (_: Exception) {
}
}
if (claimedData != null && claimedData.id != claimedControl?.id) {
try {
connection.releaseInterface(claimedData)
} catch (_: Exception) {
}
}
try {
connection.close()
} catch (_: Exception) {
}
}
connectedDeviceName = null
}
private fun handleUsbDetached(intent: Intent) {
val detachedDevice =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra(UsbManager.EXTRA_DEVICE, UsbDevice::class.java)
} else {
@Suppress("DEPRECATION")
intent.getParcelableExtra(UsbManager.EXTRA_DEVICE)
}
val detachedName = detachedDevice?.deviceName ?: return
if (pendingConnectPortName == detachedName) {
pendingConnectResult?.error(
"usb_device_detached",
"USB device was removed before the connection completed",
null,
)
pendingConnectResult = null
pendingConnectPortName = null
}
if (connectedDeviceName == detachedName) {
scheduleCloseUsbConnection {
eventSink?.error(
"usb_device_detached",
"USB device was disconnected",
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
}
}
File diff suppressed because it is too large Load Diff
+73
View File
@@ -0,0 +1,73 @@
import 'dart:typed_data';
import '../services/app_debug_log_service.dart';
import '../services/usb_serial_service.dart';
/// Manages USB serial transport for MeshCore devices.
///
/// Owns the [UsbSerialService] and USB-specific connection state.
/// The main [MeshCoreConnector] delegates all USB operations here.
class MeshCoreUsbManager {
MeshCoreUsbManager();
final UsbSerialService _service = UsbSerialService();
AppDebugLogService? _debugLog;
String? _activePortKey;
String? _activePortLabel;
// --- Getters ---
String? get activePortKey => _activePortKey;
String? get activePortDisplayLabel => _activePortLabel ?? _activePortKey;
bool get isConnected => _service.isConnected;
Stream<Uint8List> get frameStream => _service.frameStream;
// --- Configuration ---
Future<List<String>> listPorts() => _service.listPorts();
void setRequestPortLabel(String label) => _service.setRequestPortLabel(label);
void setFallbackDeviceName(String label) =>
_service.setFallbackDeviceName(label);
void setDebugLogService(AppDebugLogService? service) {
_debugLog = service;
_service.setDebugLogService(service);
}
// --- Connection lifecycle ---
Future<void> connect({
required String portName,
int baudRate = 115200,
}) async {
_debugLog?.info(
'UsbManager.connect: portName=$portName baud=$baudRate',
tag: 'USB',
);
await _service.connect(portName: portName, baudRate: baudRate);
_activePortKey = _service.activePortKey ?? portName;
_activePortLabel = _service.activePortDisplayLabel ?? portName;
_debugLog?.info(
'UsbManager.connect: done, key=$_activePortKey label=$_activePortLabel',
tag: 'USB',
);
}
Future<void> disconnect() async {
_debugLog?.info('UsbManager.disconnect', tag: 'USB');
await _service.disconnect();
_activePortKey = null;
_activePortLabel = null;
}
Future<void> write(Uint8List data) => _service.write(data);
// --- Label management ---
void updateConnectedLabel(String selfName) {
_service.updateConnectedLabel(selfName);
_activePortLabel = _service.activePortDisplayLabel ?? _activePortLabel;
}
void dispose() {
_service.dispose();
}
}
+27 -1
View File
@@ -4,6 +4,7 @@ import 'dart:typed_data';
// Buffer Reader - sequential binary data reader with pointer tracking
class BufferReader {
int _pointer = 0;
int _lastPointer = 0;
final Uint8List _buffer;
BufferReader(Uint8List data) : _buffer = Uint8List.fromList(data);
@@ -13,6 +14,7 @@ class BufferReader {
int readByte() => readBytes(1)[0];
Uint8List readBytes(int count) {
_lastPointer = _pointer;
if (_pointer + count > _buffer.length) {
throw RangeError(
'Attempted to read $count bytes at offset $_pointer, but only $remaining bytes remaining in buffer of length ${_buffer.length}',
@@ -24,6 +26,7 @@ class BufferReader {
}
void skipBytes(int count) {
_lastPointer = _pointer;
if (_pointer + count > _buffer.length) {
throw RangeError(
'Attempted to skip $count bytes at offset $_pointer, but only $remaining bytes remaining in buffer of length ${_buffer.length}',
@@ -35,6 +38,7 @@ class BufferReader {
Uint8List readRemainingBytes() => readBytes(remaining);
String readString() {
_lastPointer = _pointer;
final value = readRemainingBytes();
try {
return utf8.decode(Uint8List.fromList(value), allowMalformed: true);
@@ -43,7 +47,8 @@ class BufferReader {
}
}
String readCString(int maxLength) {
String readCStringGreedy(int maxLength) {
_lastPointer = _pointer;
final value = <int>[];
final bytes = readBytes(maxLength);
for (final byte in bytes) {
@@ -57,6 +62,24 @@ class BufferReader {
}
}
String readCString(int maxLength) {
final backupPointer = _pointer;
final value = <int>[];
int counter = 0;
while (counter < maxLength) {
final byte = readByte();
if (byte == 0) break;
value.add(byte);
counter++;
}
_lastPointer = backupPointer;
try {
return utf8.decode(Uint8List.fromList(value), allowMalformed: true);
} catch (e) {
return String.fromCharCodes(value); // Latin-1 fallback
}
}
int readUInt8() => readBytes(1).buffer.asByteData().getUint8(0);
int readInt8() => readBytes(1).buffer.asByteData().getInt8(0);
int readUInt16LE() =>
@@ -78,6 +101,9 @@ class BufferReader {
if ((value & 0x800000) != 0) value -= 0x1000000;
return value;
}
void resetPointer() => _pointer = 0;
void rewind() => _pointer = _lastPointer;
}
// Buffer Writer - accumulating binary data builder
+32 -1
View File
@@ -1828,5 +1828,36 @@
"discoveredContacts_deleteContactAllContent": "Сигурни ли сте, че искате да изтриете всички открити контакти?",
"common_deleteAll": "Изтрий всичко",
"map_guessedLocation": "Предполагано местоположение",
"map_showGuessedLocations": "Покажете местоположенията на предположените възли."
"map_showGuessedLocations": "Покажете местоположенията на предположените възли.",
"connectionChoiceUsbLabel": "USB",
"usbScreenTitle": "Свържете се чрез USB",
"connectionChoiceBluetoothLabel": "Bluetooth",
"usbScreenSubtitle": "Изберете открития сериен уред и свържете директно към вашия MeshCore възел.",
"usbScreenStatus": "Изберете USB устройство",
"usbScreenNote": "USB серийната връзка е активна на поддържаните Android устройства и настолни платформи.",
"usbScreenEmptyState": "Няма открити USB устройства. Включете едно и опитайте отново.",
"usbErrorPermissionDenied": "Не беше разрешено достъпът през USB.",
"usbErrorDeviceMissing": "Избраното USB устройство вече не е налично.",
"usbErrorInvalidPort": "Изберете валитно USB устройство.",
"usbErrorBusy": "Друг мол за свързване през USB вече е в процес на изпълнение.",
"usbErrorNotConnected": "Няма свързано USB устройство.",
"usbErrorOpenFailed": "Не успях да отворя избраното USB устройство.",
"usbErrorConnectFailed": "Не успях да се свържа с избраното USB устройство.",
"usbErrorUnsupported": "USB серийната комуникация не се поддържа на тази платформа.",
"usbErrorAlreadyActive": "USB връзката вече е активирана.",
"usbErrorNoDeviceSelected": "Няма избран USB устройство.",
"usbErrorPortClosed": "USB връзката не е активна.",
"usbFallbackDeviceName": "Устройство за четене на уеб серийни данни",
"@usbConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"usbStatus_connecting": "Свързване към USB устройство...",
"usbConnectionFailed": "Неуспешно свързване през USB: {error}",
"usbStatus_notConnected": "Изберете USB устройство",
"usbStatus_searching": "Търсене на USB устройства...",
"usbErrorConnectTimedOut": "Връзката прекъсна. Уверете се, че устройството има софтуер за USB връзка."
}
+38 -7
View File
@@ -296,8 +296,8 @@
"contacts_filterContacts": "Filtert Kontakte...",
"contacts_noContactsMatchFilter": "Keine Kontakte passen zu Ihrem Filter",
"contacts_noMembers": "Keine Mitglieder",
"contacts_lastSeenNow": "gerade gesehen",
"contacts_lastSeenMinsAgo": "Letzte Sichtung vor {minutes} Minuten.",
"contacts_lastSeenNow": "kürzlich",
"contacts_lastSeenMinsAgo": "~ {minutes} Min.",
"@contacts_lastSeenMinsAgo": {
"placeholders": {
"minutes": {
@@ -305,8 +305,8 @@
}
}
},
"contacts_lastSeenHourAgo": "Letzte Sichtung vor 1 Stunde.",
"contacts_lastSeenHoursAgo": "Letzte Sichtung vor {hours} Stunden.",
"contacts_lastSeenHourAgo": "~ 1 Std.",
"contacts_lastSeenHoursAgo": "~ {hours} Std.",
"@contacts_lastSeenHoursAgo": {
"placeholders": {
"hours": {
@@ -314,8 +314,8 @@
}
}
},
"contacts_lastSeenDayAgo": "Letzte Sichtung vor 1 Tag",
"contacts_lastSeenDaysAgo": "Letzte Sichtung {days} Tage zuvor",
"contacts_lastSeenDayAgo": "~ 1 Tag",
"contacts_lastSeenDaysAgo": "~ {days} Tage",
"@contacts_lastSeenDaysAgo": {
"placeholders": {
"days": {
@@ -1856,5 +1856,36 @@
"discoveredContacts_deleteContactAllContent": "Sind Sie sicher, dass Sie alle gefundenen Kontakte löschen möchten?",
"discoveredContacts_deleteContactAll": "Alle entdeckten Kontakte löschen",
"map_showGuessedLocations": "Zeige die vermuteten Knotenpositionen",
"map_guessedLocation": "Geschätzter Ort"
"map_guessedLocation": "Geschätzter Ort",
"usbScreenSubtitle": "Wählen Sie ein erkannten serielles Gerät aus und verbinden Sie es direkt mit Ihrem MeshCore-Knoten.",
"connectionChoiceUsbLabel": "USB",
"usbScreenTitle": "Verbinden über USB",
"connectionChoiceBluetoothLabel": "Bluetooth",
"usbScreenStatus": "Wählen Sie ein USB-Gerät aus",
"usbScreenNote": "Die USB-Serielle Schnittstelle ist auf unterstützten Android-Geräten und Desktop-Plattformen aktiv.",
"usbScreenEmptyState": "Keine USB-Geräte gefunden. Schließen Sie eines an und aktualisieren Sie.",
"usbErrorPermissionDenied": "Die USB-Berechtigung wurde abgelehnt.",
"usbErrorDeviceMissing": "Das ausgewählte USB-Gerät ist nicht mehr verfügbar.",
"usbErrorInvalidPort": "Wählen Sie ein gültiges USB-Gerät aus.",
"usbErrorBusy": "Eine weitere Anfrage für eine USB-Verbindung ist bereits in Bearbeitung.",
"usbErrorNotConnected": "Es ist kein USB-Gerät angeschlossen.",
"usbErrorOpenFailed": "Fehlgeschlagen beim Öffnen des ausgewählten USB-Geräts.",
"usbErrorConnectFailed": "Keine Verbindung zum ausgewählten USB-Gerät hergestellt.",
"usbErrorUnsupported": "Die USB-Serielle Schnittstelle wird auf dieser Plattform nicht unterstützt.",
"usbErrorAlreadyActive": "Eine USB-Verbindung ist bereits hergestellt.",
"usbErrorNoDeviceSelected": "Kein USB-Gerät wurde ausgewählt.",
"usbErrorPortClosed": "Die USB-Verbindung ist nicht aktiv.",
"usbFallbackDeviceName": "Web-Serielle Geräte",
"@usbConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"usbStatus_searching": "Suche nach USB-Geräten...",
"usbStatus_notConnected": "Wählen Sie ein USB-Gerät aus",
"usbStatus_connecting": "Verbindung zum USB-Gerät...",
"usbConnectionFailed": "Fehler beim USB-Verbindungsaufbau: {error}",
"usbErrorConnectTimedOut": "Verbindung konnte nicht hergestellt werden. Stellen Sie sicher, dass das Gerät die entsprechende USB-Firmware enthält."
}
+37 -6
View File
@@ -47,6 +47,37 @@
}
},
"scanner_title": "MeshCore Open",
"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.",
"usbErrorPermissionDenied": "USB permission was denied.",
"usbErrorDeviceMissing": "The selected USB device is no longer available.",
"usbErrorInvalidPort": "Select a valid USB device.",
"usbErrorBusy": "Another USB connection request is already in progress.",
"usbErrorNotConnected": "No USB device is connected.",
"usbErrorOpenFailed": "Failed to open the selected USB device.",
"usbErrorConnectFailed": "Failed to connect to the selected USB device.",
"usbErrorUnsupported": "USB serial is not supported on this platform.",
"usbErrorAlreadyActive": "A USB connection is already active.",
"usbErrorNoDeviceSelected": "No USB device was selected.",
"usbErrorPortClosed": "The USB connection is not open.",
"usbErrorConnectTimedOut": "Connection timed out. Make sure the device has USB Companion firmware.",
"usbFallbackDeviceName": "Web Serial Device",
"usbStatus_notConnected": "Select a USB device",
"usbStatus_connecting": "Connecting to USB device...",
"usbStatus_searching": "Searching for USB devices...",
"usbConnectionFailed": "USB connection failed: {error}",
"@usbConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"scanner_scanning": "Scanning for devices...",
"scanner_connecting": "Connecting...",
"scanner_disconnecting": "Disconnecting...",
@@ -369,8 +400,8 @@
"contacts_filterContacts": "Filter contacts...",
"contacts_noContactsMatchFilter": "No contacts match your filter",
"contacts_noMembers": "No members",
"contacts_lastSeenNow": "Last seen now",
"contacts_lastSeenMinsAgo": "Last seen {minutes} mins ago",
"contacts_lastSeenNow": "recently",
"contacts_lastSeenMinsAgo": "~ {minutes} min.",
"@contacts_lastSeenMinsAgo": {
"placeholders": {
"minutes": {
@@ -378,8 +409,8 @@
}
}
},
"contacts_lastSeenHourAgo": "Last seen 1 hour ago",
"contacts_lastSeenHoursAgo": "Last seen {hours} hours ago",
"contacts_lastSeenHourAgo": "~ 1 hour",
"contacts_lastSeenHoursAgo": "~ {hours} hours",
"@contacts_lastSeenHoursAgo": {
"placeholders": {
"hours": {
@@ -387,8 +418,8 @@
}
}
},
"contacts_lastSeenDayAgo": "Last seen 1 day ago",
"contacts_lastSeenDaysAgo": "Last seen {days} days ago",
"contacts_lastSeenDayAgo": "~ 1 day",
"contacts_lastSeenDaysAgo": "~ {days} days",
"@contacts_lastSeenDaysAgo": {
"placeholders": {
"days": {
+37 -6
View File
@@ -297,7 +297,7 @@
"contacts_noContactsMatchFilter": "No hay contactos que coincidan con tu filtro",
"contacts_noMembers": "No miembros",
"contacts_lastSeenNow": "Última vez que se vio ahora",
"contacts_lastSeenMinsAgo": "Última vez visto hace {minutes} minutos.",
"contacts_lastSeenMinsAgo": "~ {minutes} min.",
"@contacts_lastSeenMinsAgo": {
"placeholders": {
"minutes": {
@@ -305,8 +305,8 @@
}
}
},
"contacts_lastSeenHourAgo": "Última vez que se vio hace 1 hora",
"contacts_lastSeenHoursAgo": "Última vez visto hace {hours} horas.",
"contacts_lastSeenHourAgo": "~ 1 hora",
"contacts_lastSeenHoursAgo": "~ {hours} horas",
"@contacts_lastSeenHoursAgo": {
"placeholders": {
"hours": {
@@ -314,8 +314,8 @@
}
}
},
"contacts_lastSeenDayAgo": "Última vez que se vio hace 1 día",
"contacts_lastSeenDaysAgo": "Última vez visto hace {days} días.",
"contacts_lastSeenDayAgo": "~ 1 día",
"contacts_lastSeenDaysAgo": "~ {days} días",
"@contacts_lastSeenDaysAgo": {
"placeholders": {
"days": {
@@ -1856,5 +1856,36 @@
"discoveredContacts_deleteContactAll": "Eliminar Todos los Contactos Descubiertos",
"discoveredContacts_deleteContactAllContent": "¿Está seguro de que desea eliminar todos los contactos descubiertos!",
"map_guessedLocation": "Ubicación estimada",
"map_showGuessedLocations": "Mostrar las ubicaciones estimadas de los nodos."
"map_showGuessedLocations": "Mostrar las ubicaciones estimadas de los nodos.",
"usbScreenTitle": "Conecte mediante USB",
"connectionChoiceUsbLabel": "USB",
"connectionChoiceBluetoothLabel": "Bluetooth",
"usbScreenSubtitle": "Seleccione el dispositivo de serie detectado y conéctelo directamente a su nodo MeshCore.",
"usbScreenStatus": "Seleccione un dispositivo USB",
"usbScreenNote": "La comunicación serial a través de USB está activa en dispositivos Android compatibles y en plataformas de escritorio.",
"usbScreenEmptyState": "No se encontraron dispositivos USB. Conecte uno y vuelva a intentar.",
"usbErrorPermissionDenied": "Se denegó el permiso de acceso a través de USB.",
"usbErrorDeviceMissing": "El dispositivo USB seleccionado ya no está disponible.",
"usbErrorInvalidPort": "Seleccione un dispositivo USB válido.",
"usbErrorBusy": "Ya se ha iniciado una solicitud de conexión USB adicional.",
"usbErrorNotConnected": "No hay ningún dispositivo USB conectado.",
"usbErrorOpenFailed": "No se pudo abrir el dispositivo USB seleccionado.",
"usbErrorConnectFailed": "No se pudo conectar con el dispositivo USB seleccionado.",
"usbErrorUnsupported": "La comunicación serial a través de USB no está soportada en esta plataforma.",
"usbErrorAlreadyActive": "La conexión USB ya está activa.",
"usbErrorNoDeviceSelected": "No se ha seleccionado ningún dispositivo USB.",
"usbErrorPortClosed": "La conexión USB no está activa.",
"usbFallbackDeviceName": "Dispositivo de serie web",
"@usbConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"usbStatus_connecting": "Conectándose al dispositivo USB...",
"usbStatus_searching": "Buscando dispositivos USB...",
"usbStatus_notConnected": "Seleccione un dispositivo USB",
"usbConnectionFailed": "Error al conectar mediante USB: {error}",
"usbErrorConnectTimedOut": "La conexión ha caducado. Asegúrese de que el dispositivo tenga el firmware USB Companion."
}
+37 -6
View File
@@ -297,7 +297,7 @@
"contacts_noContactsMatchFilter": "Aucun contact ne correspond à votre filtre.",
"contacts_noMembers": "Aucun membre",
"contacts_lastSeenNow": "Vu maintenant",
"contacts_lastSeenMinsAgo": "Vu il y a {minutes} minutes",
"contacts_lastSeenMinsAgo": "~ {minutes} min.",
"@contacts_lastSeenMinsAgo": {
"placeholders": {
"minutes": {
@@ -305,8 +305,8 @@
}
}
},
"contacts_lastSeenHourAgo": "Vu il y a 1 heure",
"contacts_lastSeenHoursAgo": "Vu il y a {hours} heures",
"contacts_lastSeenHourAgo": "~ 1 heure",
"contacts_lastSeenHoursAgo": "~ {hours} heures",
"@contacts_lastSeenHoursAgo": {
"placeholders": {
"hours": {
@@ -314,8 +314,8 @@
}
}
},
"contacts_lastSeenDayAgo": "Vu il y a 1 jour",
"contacts_lastSeenDaysAgo": "Vu il y a {days} jours",
"contacts_lastSeenDayAgo": "~ 1 jour",
"contacts_lastSeenDaysAgo": "~ {days} jours",
"@contacts_lastSeenDaysAgo": {
"placeholders": {
"days": {
@@ -1828,5 +1828,36 @@
"discoveredContacts_deleteContactAll": "Supprimer tous les contacts découverts",
"discoveredContacts_deleteContactAllContent": "Êtes-vous sûr de vouloir supprimer tous les contacts découverts ?",
"map_showGuessedLocations": "Afficher les emplacements des nœuds estimés",
"map_guessedLocation": "Lieu deviné"
"map_guessedLocation": "Lieu deviné",
"connectionChoiceUsbLabel": "USB",
"usbScreenTitle": "Connectez via USB",
"connectionChoiceBluetoothLabel": "Bluetooth",
"usbScreenSubtitle": "Sélectionnez un périphérique série détecté et connectez-vous directement à votre nœud MeshCore.",
"usbScreenStatus": "Sélectionnez un périphérique USB",
"usbScreenNote": "La communication série USB est active sur les appareils Android et les plateformes de bureau compatibles.",
"usbScreenEmptyState": "Aucun périphérique USB n'a été trouvé. Veuillez en brancher un et rafraîchir la page.",
"usbErrorPermissionDenied": "L'accès via USB a été refusé.",
"usbErrorDeviceMissing": "Le périphérique USB sélectionné n'est plus disponible.",
"usbErrorInvalidPort": "Sélectionnez un périphérique USB valide.",
"usbErrorBusy": "Une autre demande de connexion USB est déjà en cours.",
"usbErrorNotConnected": "Aucun appareil USB n'est connecté.",
"usbErrorOpenFailed": "Impossible d'ouvrir l'appareil USB sélectionné.",
"usbErrorConnectFailed": "Impossible de se connecter à l'appareil USB sélectionné.",
"usbErrorUnsupported": "La communication série USB n'est pas prise en charge sur cette plateforme.",
"usbErrorAlreadyActive": "Une connexion USB est déjà établie.",
"usbErrorNoDeviceSelected": "Aucun appareil USB n'a été sélectionné.",
"usbErrorPortClosed": "La connexion USB n'est pas établie.",
"usbFallbackDeviceName": "Dispositif de communication série sur le Web",
"@usbConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"usbStatus_notConnected": "Sélectionnez un périphérique USB",
"usbConnectionFailed": "Échec de la connexion USB : {error}",
"usbStatus_connecting": "Connexion au périphérique USB...",
"usbStatus_searching": "Recherche de périphériques USB...",
"usbErrorConnectTimedOut": "La connexion a expiré. Assurez-vous que l'appareil dispose du firmware USB Companion."
}
+32 -1
View File
@@ -1828,5 +1828,36 @@
"discoveredContacts_deleteContactAllContent": "Sei sicuro di voler eliminare tutti i contatti scoperti?",
"discoveredContacts_deleteContactAll": "Eliminare tutti i contatti scoperti",
"map_guessedLocation": "Località indovinata",
"map_showGuessedLocations": "Mostra le posizioni stimate dei nodi"
"map_showGuessedLocations": "Mostra le posizioni stimate dei nodi",
"connectionChoiceBluetoothLabel": "Bluetooth",
"usbScreenSubtitle": "Seleziona il dispositivo seriale rilevato e connettilo direttamente al tuo nodo MeshCore.",
"connectionChoiceUsbLabel": "USB",
"usbScreenTitle": "Connessione tramite USB",
"usbScreenStatus": "Seleziona un dispositivo USB",
"usbScreenNote": "La comunicazione seriale USB è attiva sui dispositivi Android supportati e sulle piattaforme desktop.",
"usbScreenEmptyState": "Nessun dispositivo USB rilevato. Collegare uno e aggiornare.",
"usbErrorPermissionDenied": "È stato negato l'accesso tramite USB.",
"usbErrorDeviceMissing": "Il dispositivo USB selezionato non è più disponibile.",
"usbErrorInvalidPort": "Seleziona un dispositivo USB valido.",
"usbErrorBusy": "Un'altra richiesta di connessione tramite USB è già in corso.",
"usbErrorNotConnected": "Non è collegato alcun dispositivo USB.",
"usbErrorOpenFailed": "Impossibile aprire il dispositivo USB selezionato.",
"usbErrorConnectFailed": "Impossibile connettersi al dispositivo USB selezionato.",
"usbErrorUnsupported": "La comunicazione seriale tramite USB non è supportata su questa piattaforma.",
"usbErrorAlreadyActive": "La connessione USB è già attiva.",
"usbErrorNoDeviceSelected": "Non è stato selezionato alcun dispositivo USB.",
"usbErrorPortClosed": "La connessione USB non è attiva.",
"usbFallbackDeviceName": "Dispositivo per comunicazione seriale su rete",
"@usbConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"usbStatus_searching": "Ricerca di dispositivi USB...",
"usbConnectionFailed": "Errore nella connessione USB: {error}",
"usbStatus_notConnected": "Seleziona un dispositivo USB",
"usbStatus_connecting": "Connessione al dispositivo USB...",
"usbErrorConnectTimedOut": "La connessione è scaduta. Assicurarsi che il dispositivo abbia il firmware USB Companion."
}
+150 -6
View File
@@ -322,6 +322,150 @@ abstract class AppLocalizations {
/// **'MeshCore Open'**
String get scanner_title;
/// 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 @usbErrorPermissionDenied.
///
/// In en, this message translates to:
/// **'USB permission was denied.'**
String get usbErrorPermissionDenied;
/// No description provided for @usbErrorDeviceMissing.
///
/// In en, this message translates to:
/// **'The selected USB device is no longer available.'**
String get usbErrorDeviceMissing;
/// No description provided for @usbErrorInvalidPort.
///
/// In en, this message translates to:
/// **'Select a valid USB device.'**
String get usbErrorInvalidPort;
/// No description provided for @usbErrorBusy.
///
/// In en, this message translates to:
/// **'Another USB connection request is already in progress.'**
String get usbErrorBusy;
/// No description provided for @usbErrorNotConnected.
///
/// In en, this message translates to:
/// **'No USB device is connected.'**
String get usbErrorNotConnected;
/// No description provided for @usbErrorOpenFailed.
///
/// In en, this message translates to:
/// **'Failed to open the selected USB device.'**
String get usbErrorOpenFailed;
/// No description provided for @usbErrorConnectFailed.
///
/// In en, this message translates to:
/// **'Failed to connect to the selected USB device.'**
String get usbErrorConnectFailed;
/// No description provided for @usbErrorUnsupported.
///
/// In en, this message translates to:
/// **'USB serial is not supported on this platform.'**
String get usbErrorUnsupported;
/// No description provided for @usbErrorAlreadyActive.
///
/// In en, this message translates to:
/// **'A USB connection is already active.'**
String get usbErrorAlreadyActive;
/// No description provided for @usbErrorNoDeviceSelected.
///
/// In en, this message translates to:
/// **'No USB device was selected.'**
String get usbErrorNoDeviceSelected;
/// No description provided for @usbErrorPortClosed.
///
/// In en, this message translates to:
/// **'The USB connection is not open.'**
String get usbErrorPortClosed;
/// No description provided for @usbErrorConnectTimedOut.
///
/// In en, this message translates to:
/// **'Connection timed out. Make sure the device has USB Companion firmware.'**
String get usbErrorConnectTimedOut;
/// No description provided for @usbFallbackDeviceName.
///
/// In en, this message translates to:
/// **'Web Serial Device'**
String get usbFallbackDeviceName;
/// No description provided for @usbStatus_notConnected.
///
/// In en, this message translates to:
/// **'Select a USB device'**
String get usbStatus_notConnected;
/// No description provided for @usbStatus_connecting.
///
/// In en, this message translates to:
/// **'Connecting to USB device...'**
String get usbStatus_connecting;
/// No description provided for @usbStatus_searching.
///
/// In en, this message translates to:
/// **'Searching for USB devices...'**
String get usbStatus_searching;
/// No description provided for @usbConnectionFailed.
///
/// In en, this message translates to:
/// **'USB connection failed: {error}'**
String usbConnectionFailed(String error);
/// No description provided for @scanner_scanning.
///
/// In en, this message translates to:
@@ -1519,37 +1663,37 @@ abstract class AppLocalizations {
/// No description provided for @contacts_lastSeenNow.
///
/// In en, this message translates to:
/// **'Last seen now'**
/// **'recently'**
String get contacts_lastSeenNow;
/// No description provided for @contacts_lastSeenMinsAgo.
///
/// In en, this message translates to:
/// **'Last seen {minutes} mins ago'**
/// **'~ {minutes} min.'**
String contacts_lastSeenMinsAgo(int minutes);
/// No description provided for @contacts_lastSeenHourAgo.
///
/// In en, this message translates to:
/// **'Last seen 1 hour ago'**
/// **'~ 1 hour'**
String get contacts_lastSeenHourAgo;
/// No description provided for @contacts_lastSeenHoursAgo.
///
/// In en, this message translates to:
/// **'Last seen {hours} hours ago'**
/// **'~ {hours} hours'**
String contacts_lastSeenHoursAgo(int hours);
/// No description provided for @contacts_lastSeenDayAgo.
///
/// In en, this message translates to:
/// **'Last seen 1 day ago'**
/// **'~ 1 day'**
String get contacts_lastSeenDayAgo;
/// No description provided for @contacts_lastSeenDaysAgo.
///
/// In en, this message translates to:
/// **'Last seen {days} days ago'**
/// **'~ {days} days'**
String contacts_lastSeenDaysAgo(int days);
/// No description provided for @channels_title.
+84
View File
@@ -111,6 +111,90 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
@override
String get connectionChoiceUsbLabel => 'USB';
@override
String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get usbScreenTitle => 'Свържете се чрез USB';
@override
String get usbScreenSubtitle =>
'Изберете открития сериен уред и свържете директно към вашия MeshCore възел.';
@override
String get usbScreenStatus => 'Изберете USB устройство';
@override
String get usbScreenNote =>
'USB серийната връзка е активна на поддържаните Android устройства и настолни платформи.';
@override
String get usbScreenEmptyState =>
'Няма открити USB устройства. Включете едно и опитайте отново.';
@override
String get usbErrorPermissionDenied => 'Не беше разрешено достъпът през USB.';
@override
String get usbErrorDeviceMissing =>
'Избраното USB устройство вече не е налично.';
@override
String get usbErrorInvalidPort => 'Изберете валитно USB устройство.';
@override
String get usbErrorBusy =>
'Друг мол за свързване през USB вече е в процес на изпълнение.';
@override
String get usbErrorNotConnected => 'Няма свързано USB устройство.';
@override
String get usbErrorOpenFailed =>
'Не успях да отворя избраното USB устройство.';
@override
String get usbErrorConnectFailed =>
'Не успях да се свържа с избраното USB устройство.';
@override
String get usbErrorUnsupported =>
'USB серийната комуникация не се поддържа на тази платформа.';
@override
String get usbErrorAlreadyActive => 'USB връзката вече е активирана.';
@override
String get usbErrorNoDeviceSelected => 'Няма избран USB устройство.';
@override
String get usbErrorPortClosed => 'USB връзката не е активна.';
@override
String get usbErrorConnectTimedOut =>
'Връзката прекъсна. Уверете се, че устройството има софтуер за USB връзка.';
@override
String get usbFallbackDeviceName =>
'Устройство за четене на уеб серийни данни';
@override
String get usbStatus_notConnected => 'Изберете USB устройство';
@override
String get usbStatus_connecting => 'Свързване към USB устройство...';
@override
String get usbStatus_searching => 'Търсене на USB устройства...';
@override
String usbConnectionFailed(String error) {
return 'Неуспешно свързване през USB: $error';
}
@override
String get scanner_scanning => 'Сканиране за устройства...';
+91 -6
View File
@@ -111,6 +111,91 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
@override
String get connectionChoiceUsbLabel => 'USB';
@override
String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get usbScreenTitle => 'Verbinden über USB';
@override
String get usbScreenSubtitle =>
'Wählen Sie ein erkannten serielles Gerät aus und verbinden Sie es direkt mit Ihrem MeshCore-Knoten.';
@override
String get usbScreenStatus => 'Wählen Sie ein USB-Gerät aus';
@override
String get usbScreenNote =>
'Die USB-Serielle Schnittstelle ist auf unterstützten Android-Geräten und Desktop-Plattformen aktiv.';
@override
String get usbScreenEmptyState =>
'Keine USB-Geräte gefunden. Schließen Sie eines an und aktualisieren Sie.';
@override
String get usbErrorPermissionDenied =>
'Die USB-Berechtigung wurde abgelehnt.';
@override
String get usbErrorDeviceMissing =>
'Das ausgewählte USB-Gerät ist nicht mehr verfügbar.';
@override
String get usbErrorInvalidPort => 'Wählen Sie ein gültiges USB-Gerät aus.';
@override
String get usbErrorBusy =>
'Eine weitere Anfrage für eine USB-Verbindung ist bereits in Bearbeitung.';
@override
String get usbErrorNotConnected => 'Es ist kein USB-Gerät angeschlossen.';
@override
String get usbErrorOpenFailed =>
'Fehlgeschlagen beim Öffnen des ausgewählten USB-Geräts.';
@override
String get usbErrorConnectFailed =>
'Keine Verbindung zum ausgewählten USB-Gerät hergestellt.';
@override
String get usbErrorUnsupported =>
'Die USB-Serielle Schnittstelle wird auf dieser Plattform nicht unterstützt.';
@override
String get usbErrorAlreadyActive =>
'Eine USB-Verbindung ist bereits hergestellt.';
@override
String get usbErrorNoDeviceSelected => 'Kein USB-Gerät wurde ausgewählt.';
@override
String get usbErrorPortClosed => 'Die USB-Verbindung ist nicht aktiv.';
@override
String get usbErrorConnectTimedOut =>
'Verbindung konnte nicht hergestellt werden. Stellen Sie sicher, dass das Gerät die entsprechende USB-Firmware enthält.';
@override
String get usbFallbackDeviceName => 'Web-Serielle Geräte';
@override
String get usbStatus_notConnected => 'Wählen Sie ein USB-Gerät aus';
@override
String get usbStatus_connecting => 'Verbindung zum USB-Gerät...';
@override
String get usbStatus_searching => 'Suche nach USB-Geräten...';
@override
String usbConnectionFailed(String error) {
return 'Fehler beim USB-Verbindungsaufbau: $error';
}
@override
String get scanner_scanning => 'Scannen nach Geräten...';
@@ -787,27 +872,27 @@ class AppLocalizationsDe extends AppLocalizations {
String get contacts_noMembers => 'Keine Mitglieder';
@override
String get contacts_lastSeenNow => 'gerade gesehen';
String get contacts_lastSeenNow => 'kürzlich';
@override
String contacts_lastSeenMinsAgo(int minutes) {
return 'Letzte Sichtung vor $minutes Minuten.';
return '~ $minutes Min.';
}
@override
String get contacts_lastSeenHourAgo => 'Letzte Sichtung vor 1 Stunde.';
String get contacts_lastSeenHourAgo => '~ 1 Std.';
@override
String contacts_lastSeenHoursAgo(int hours) {
return 'Letzte Sichtung vor $hours Stunden.';
return '~ $hours Std.';
}
@override
String get contacts_lastSeenDayAgo => 'Letzte Sichtung vor 1 Tag';
String get contacts_lastSeenDayAgo => '~ 1 Tag';
@override
String contacts_lastSeenDaysAgo(int days) {
return 'Letzte Sichtung $days Tage zuvor';
return '~ $days Tage';
}
@override
+88 -6
View File
@@ -111,6 +111,88 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
@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 usbErrorPermissionDenied => 'USB permission was denied.';
@override
String get usbErrorDeviceMissing =>
'The selected USB device is no longer available.';
@override
String get usbErrorInvalidPort => 'Select a valid USB device.';
@override
String get usbErrorBusy =>
'Another USB connection request is already in progress.';
@override
String get usbErrorNotConnected => 'No USB device is connected.';
@override
String get usbErrorOpenFailed => 'Failed to open the selected USB device.';
@override
String get usbErrorConnectFailed =>
'Failed to connect to the selected USB device.';
@override
String get usbErrorUnsupported =>
'USB serial is not supported on this platform.';
@override
String get usbErrorAlreadyActive => 'A USB connection is already active.';
@override
String get usbErrorNoDeviceSelected => 'No USB device was selected.';
@override
String get usbErrorPortClosed => 'The USB connection is not open.';
@override
String get usbErrorConnectTimedOut =>
'Connection timed out. Make sure the device has USB Companion firmware.';
@override
String get usbFallbackDeviceName => 'Web Serial Device';
@override
String get usbStatus_notConnected => 'Select a USB device';
@override
String get usbStatus_connecting => 'Connecting to USB device...';
@override
String get usbStatus_searching => 'Searching for USB devices...';
@override
String usbConnectionFailed(String error) {
return 'USB connection failed: $error';
}
@override
String get scanner_scanning => 'Scanning for devices...';
@@ -778,27 +860,27 @@ class AppLocalizationsEn extends AppLocalizations {
String get contacts_noMembers => 'No members';
@override
String get contacts_lastSeenNow => 'Last seen now';
String get contacts_lastSeenNow => 'recently';
@override
String contacts_lastSeenMinsAgo(int minutes) {
return 'Last seen $minutes mins ago';
return '~ $minutes min.';
}
@override
String get contacts_lastSeenHourAgo => 'Last seen 1 hour ago';
String get contacts_lastSeenHourAgo => '~ 1 hour';
@override
String contacts_lastSeenHoursAgo(int hours) {
return 'Last seen $hours hours ago';
return '~ $hours hours';
}
@override
String get contacts_lastSeenDayAgo => 'Last seen 1 day ago';
String get contacts_lastSeenDayAgo => '~ 1 day';
@override
String contacts_lastSeenDaysAgo(int days) {
return 'Last seen $days days ago';
return '~ $days days';
}
@override
+90 -5
View File
@@ -111,6 +111,91 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
@override
String get connectionChoiceUsbLabel => 'USB';
@override
String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get usbScreenTitle => 'Conecte mediante USB';
@override
String get usbScreenSubtitle =>
'Seleccione el dispositivo de serie detectado y conéctelo directamente a su nodo MeshCore.';
@override
String get usbScreenStatus => 'Seleccione un dispositivo USB';
@override
String get usbScreenNote =>
'La comunicación serial a través de USB está activa en dispositivos Android compatibles y en plataformas de escritorio.';
@override
String get usbScreenEmptyState =>
'No se encontraron dispositivos USB. Conecte uno y vuelva a intentar.';
@override
String get usbErrorPermissionDenied =>
'Se denegó el permiso de acceso a través de USB.';
@override
String get usbErrorDeviceMissing =>
'El dispositivo USB seleccionado ya no está disponible.';
@override
String get usbErrorInvalidPort => 'Seleccione un dispositivo USB válido.';
@override
String get usbErrorBusy =>
'Ya se ha iniciado una solicitud de conexión USB adicional.';
@override
String get usbErrorNotConnected => 'No hay ningún dispositivo USB conectado.';
@override
String get usbErrorOpenFailed =>
'No se pudo abrir el dispositivo USB seleccionado.';
@override
String get usbErrorConnectFailed =>
'No se pudo conectar con el dispositivo USB seleccionado.';
@override
String get usbErrorUnsupported =>
'La comunicación serial a través de USB no está soportada en esta plataforma.';
@override
String get usbErrorAlreadyActive => 'La conexión USB ya está activa.';
@override
String get usbErrorNoDeviceSelected =>
'No se ha seleccionado ningún dispositivo USB.';
@override
String get usbErrorPortClosed => 'La conexión USB no está activa.';
@override
String get usbErrorConnectTimedOut =>
'La conexión ha caducado. Asegúrese de que el dispositivo tenga el firmware USB Companion.';
@override
String get usbFallbackDeviceName => 'Dispositivo de serie web';
@override
String get usbStatus_notConnected => 'Seleccione un dispositivo USB';
@override
String get usbStatus_connecting => 'Conectándose al dispositivo USB...';
@override
String get usbStatus_searching => 'Buscando dispositivos USB...';
@override
String usbConnectionFailed(String error) {
return 'Error al conectar mediante USB: $error';
}
@override
String get scanner_scanning => 'Escaneando dispositivos...';
@@ -792,23 +877,23 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String contacts_lastSeenMinsAgo(int minutes) {
return 'Última vez visto hace $minutes minutos.';
return '~ $minutes min.';
}
@override
String get contacts_lastSeenHourAgo => 'Última vez que se vio hace 1 hora';
String get contacts_lastSeenHourAgo => '~ 1 hora';
@override
String contacts_lastSeenHoursAgo(int hours) {
return 'Última vez visto hace $hours horas.';
return '~ $hours horas';
}
@override
String get contacts_lastSeenDayAgo => 'Última vez que se vio hace 1 día';
String get contacts_lastSeenDayAgo => '~ 1 día';
@override
String contacts_lastSeenDaysAgo(int days) {
return 'Última vez visto hace $days días.';
return '~ $days días';
}
@override
+90 -5
View File
@@ -111,6 +111,91 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
@override
String get connectionChoiceUsbLabel => 'USB';
@override
String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get usbScreenTitle => 'Connectez via USB';
@override
String get usbScreenSubtitle =>
'Sélectionnez un périphérique série détecté et connectez-vous directement à votre nœud MeshCore.';
@override
String get usbScreenStatus => 'Sélectionnez un périphérique USB';
@override
String get usbScreenNote =>
'La communication série USB est active sur les appareils Android et les plateformes de bureau compatibles.';
@override
String get usbScreenEmptyState =>
'Aucun périphérique USB n\'a été trouvé. Veuillez en brancher un et rafraîchir la page.';
@override
String get usbErrorPermissionDenied => 'L\'accès via USB a été refusé.';
@override
String get usbErrorDeviceMissing =>
'Le périphérique USB sélectionné n\'est plus disponible.';
@override
String get usbErrorInvalidPort => 'Sélectionnez un périphérique USB valide.';
@override
String get usbErrorBusy =>
'Une autre demande de connexion USB est déjà en cours.';
@override
String get usbErrorNotConnected => 'Aucun appareil USB n\'est connecté.';
@override
String get usbErrorOpenFailed =>
'Impossible d\'ouvrir l\'appareil USB sélectionné.';
@override
String get usbErrorConnectFailed =>
'Impossible de se connecter à l\'appareil USB sélectionné.';
@override
String get usbErrorUnsupported =>
'La communication série USB n\'est pas prise en charge sur cette plateforme.';
@override
String get usbErrorAlreadyActive => 'Une connexion USB est déjà établie.';
@override
String get usbErrorNoDeviceSelected =>
'Aucun appareil USB n\'a été sélectionné.';
@override
String get usbErrorPortClosed => 'La connexion USB n\'est pas établie.';
@override
String get usbErrorConnectTimedOut =>
'La connexion a expiré. Assurez-vous que l\'appareil dispose du firmware USB Companion.';
@override
String get usbFallbackDeviceName =>
'Dispositif de communication série sur le Web';
@override
String get usbStatus_notConnected => 'Sélectionnez un périphérique USB';
@override
String get usbStatus_connecting => 'Connexion au périphérique USB...';
@override
String get usbStatus_searching => 'Recherche de périphériques USB...';
@override
String usbConnectionFailed(String error) {
return 'Échec de la connexion USB : $error';
}
@override
String get scanner_scanning => 'Recherche de périphériques...';
@@ -794,23 +879,23 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String contacts_lastSeenMinsAgo(int minutes) {
return 'Vu il y a $minutes minutes';
return '~ $minutes min.';
}
@override
String get contacts_lastSeenHourAgo => 'Vu il y a 1 heure';
String get contacts_lastSeenHourAgo => '~ 1 heure';
@override
String contacts_lastSeenHoursAgo(int hours) {
return 'Vu il y a $hours heures';
return '~ $hours heures';
}
@override
String get contacts_lastSeenDayAgo => 'Vu il y a 1 jour';
String get contacts_lastSeenDayAgo => '~ 1 jour';
@override
String contacts_lastSeenDaysAgo(int days) {
return 'Vu il y a $days jours';
return '~ $days jours';
}
@override
+86
View File
@@ -111,6 +111,92 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
@override
String get connectionChoiceUsbLabel => 'USB';
@override
String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get usbScreenTitle => 'Connessione tramite USB';
@override
String get usbScreenSubtitle =>
'Seleziona il dispositivo seriale rilevato e connettilo direttamente al tuo nodo MeshCore.';
@override
String get usbScreenStatus => 'Seleziona un dispositivo USB';
@override
String get usbScreenNote =>
'La comunicazione seriale USB è attiva sui dispositivi Android supportati e sulle piattaforme desktop.';
@override
String get usbScreenEmptyState =>
'Nessun dispositivo USB rilevato. Collegare uno e aggiornare.';
@override
String get usbErrorPermissionDenied =>
'È stato negato l\'accesso tramite USB.';
@override
String get usbErrorDeviceMissing =>
'Il dispositivo USB selezionato non è più disponibile.';
@override
String get usbErrorInvalidPort => 'Seleziona un dispositivo USB valido.';
@override
String get usbErrorBusy =>
'Un\'altra richiesta di connessione tramite USB è già in corso.';
@override
String get usbErrorNotConnected => 'Non è collegato alcun dispositivo USB.';
@override
String get usbErrorOpenFailed =>
'Impossibile aprire il dispositivo USB selezionato.';
@override
String get usbErrorConnectFailed =>
'Impossibile connettersi al dispositivo USB selezionato.';
@override
String get usbErrorUnsupported =>
'La comunicazione seriale tramite USB non è supportata su questa piattaforma.';
@override
String get usbErrorAlreadyActive => 'La connessione USB è già attiva.';
@override
String get usbErrorNoDeviceSelected =>
'Non è stato selezionato alcun dispositivo USB.';
@override
String get usbErrorPortClosed => 'La connessione USB non è attiva.';
@override
String get usbErrorConnectTimedOut =>
'La connessione è scaduta. Assicurarsi che il dispositivo abbia il firmware USB Companion.';
@override
String get usbFallbackDeviceName =>
'Dispositivo per comunicazione seriale su rete';
@override
String get usbStatus_notConnected => 'Seleziona un dispositivo USB';
@override
String get usbStatus_connecting => 'Connessione al dispositivo USB...';
@override
String get usbStatus_searching => 'Ricerca di dispositivi USB...';
@override
String usbConnectionFailed(String error) {
return 'Errore nella connessione USB: $error';
}
@override
String get scanner_scanning => 'Scansione in corso per i dispositivi...';
+83
View File
@@ -111,6 +111,89 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
@override
String get connectionChoiceUsbLabel => 'USB';
@override
String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get usbScreenTitle => 'Verbind via USB';
@override
String get usbScreenSubtitle =>
'Selecteer een gedetecteerd seriële apparaat en verbind deze direct met uw MeshCore-node.';
@override
String get usbScreenStatus => 'Selecteer een USB-apparaat';
@override
String get usbScreenNote =>
'USB-serieel is actief op ondersteunde Android-apparaten en desktop-platforms.';
@override
String get usbScreenEmptyState =>
'Geen USB-apparaten gevonden. Sluit er een aan en herlaad.';
@override
String get usbErrorPermissionDenied => 'Toegang via USB is geweigerd.';
@override
String get usbErrorDeviceMissing =>
'Het geselecteerde USB-apparaat is niet meer beschikbaar.';
@override
String get usbErrorInvalidPort => 'Selecteer een geldig USB-apparaat.';
@override
String get usbErrorBusy =>
'Een andere verzoek om een USB-verbinding is al in behandeling.';
@override
String get usbErrorNotConnected => 'Er is geen USB-apparaat aangesloten.';
@override
String get usbErrorOpenFailed =>
'Kon het geselecteerde USB-apparaat niet openen.';
@override
String get usbErrorConnectFailed =>
'Kon niet verbinding maken met het geselecteerde USB-apparaat.';
@override
String get usbErrorUnsupported =>
'USB-serieel is niet ondersteund op deze platform.';
@override
String get usbErrorAlreadyActive => 'Een USB-verbinding is al actief.';
@override
String get usbErrorNoDeviceSelected => 'Geen USB-apparaat is geselecteerd.';
@override
String get usbErrorPortClosed => 'De USB-verbinding is niet actief.';
@override
String get usbErrorConnectTimedOut =>
'Verbinding is verbroken. Zorg ervoor dat het apparaat de juiste USB-firmware heeft.';
@override
String get usbFallbackDeviceName => 'Web-serieapparaat';
@override
String get usbStatus_notConnected => 'Selecteer een USB-apparaat';
@override
String get usbStatus_connecting => 'Verbinding maken met USB-apparaat...';
@override
String get usbStatus_searching => 'Zoeken naar USB-apparaten...';
@override
String usbConnectionFailed(String error) {
return 'Fout bij de USB-verbinding: $error';
}
@override
String get scanner_scanning => 'Scannen naar apparaten...';
+86
View File
@@ -111,6 +111,92 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
@override
String get connectionChoiceUsbLabel => 'USB';
@override
String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get usbScreenTitle => 'Połącz przez USB';
@override
String get usbScreenSubtitle =>
'Wybierz wykryty urządzenie szeregowe i podłącz je bezpośrednio do swojego węzła MeshCore.';
@override
String get usbScreenStatus => 'Wybierz urządzenie USB';
@override
String get usbScreenNote =>
'Port szeregowy USB jest aktywny na urządzeniach z systemem Android i platformach stacjonarnych, które go obsługują.';
@override
String get usbScreenEmptyState =>
'Nie znaleziono żadnych urządzeń USB. Podłącz jedno i zaktualizuj.';
@override
String get usbErrorPermissionDenied =>
'Zostało odrzucone żądanie dostępu przez USB.';
@override
String get usbErrorDeviceMissing =>
'Wybór urządzenia USB już nie jest dostępny.';
@override
String get usbErrorInvalidPort => 'Wybierz prawidłowe urządzenie USB.';
@override
String get usbErrorBusy =>
'Kolejne żądanie połączenia przez USB jest już w trakcie realizacji.';
@override
String get usbErrorNotConnected => 'Brak podłączonego urządzenia USB.';
@override
String get usbErrorOpenFailed =>
'Nie udało się otworzyć wybranego urządzenia USB.';
@override
String get usbErrorConnectFailed =>
'Nie udało się połączyć z wybranym urządzeniem USB.';
@override
String get usbErrorUnsupported =>
'Port szeregowy USB nie jest obsługiwany na tym urządzeniu.';
@override
String get usbErrorAlreadyActive => 'Połączenie USB jest już aktywne.';
@override
String get usbErrorNoDeviceSelected =>
'Nie został wybrany żaden urządzenie USB.';
@override
String get usbErrorPortClosed => 'Połączenie USB nie jest aktywne.';
@override
String get usbErrorConnectTimedOut =>
'Połączenie nie zostało nawiązane. Upewnij się, że urządzenie posiada oprogramowanie \"USB Companion\".';
@override
String get usbFallbackDeviceName =>
'Urządzenie do komunikacji przez sieć (seria)';
@override
String get usbStatus_notConnected => 'Wybierz urządzenie USB';
@override
String get usbStatus_connecting => 'Połączenie z urządzeniem USB...';
@override
String get usbStatus_searching => 'Wyszukiwanie urządzeń USB...';
@override
String usbConnectionFailed(String error) {
return 'Błąd połączenia USB: $error';
}
@override
String get scanner_scanning => 'Skanowanie urządzeń...';
+85
View File
@@ -111,6 +111,91 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
@override
String get connectionChoiceUsbLabel => 'USB';
@override
String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get usbScreenTitle => 'Conecte via USB';
@override
String get usbScreenSubtitle =>
'Selecione um dispositivo serial detectado e conecte-o diretamente ao seu nó MeshCore.';
@override
String get usbScreenStatus => 'Selecione um dispositivo USB';
@override
String get usbScreenNote =>
'A comunicação serial via USB está ativa em dispositivos Android e plataformas de desktop compatíveis.';
@override
String get usbScreenEmptyState =>
'Nenhum dispositivo USB encontrado. Conecte um e atualize.';
@override
String get usbErrorPermissionDenied =>
'A permissão para acesso via USB foi negada.';
@override
String get usbErrorDeviceMissing =>
'O dispositivo USB selecionado não está mais disponível.';
@override
String get usbErrorInvalidPort => 'Selecione um dispositivo USB válido.';
@override
String get usbErrorBusy =>
'Já existe uma solicitação de conexão USB em andamento.';
@override
String get usbErrorNotConnected => 'Não há nenhum dispositivo USB conectado.';
@override
String get usbErrorOpenFailed =>
'Não foi possível abrir o dispositivo USB selecionado.';
@override
String get usbErrorConnectFailed =>
'Não foi possível conectar ao dispositivo USB selecionado.';
@override
String get usbErrorUnsupported =>
'A comunicação serial via USB não é suportada nesta plataforma.';
@override
String get usbErrorAlreadyActive => 'A conexão USB já está ativa.';
@override
String get usbErrorNoDeviceSelected =>
'Nenhum dispositivo USB foi selecionado.';
@override
String get usbErrorPortClosed => 'A conexão USB não está ativa.';
@override
String get usbErrorConnectTimedOut =>
'A conexão expirou. Verifique se o dispositivo possui o firmware USB Companion.';
@override
String get usbFallbackDeviceName => 'Dispositivo de Serial para a Web';
@override
String get usbStatus_notConnected => 'Selecione um dispositivo USB';
@override
String get usbStatus_connecting => 'Conectando ao dispositivo USB...';
@override
String get usbStatus_searching => 'Procurando por dispositivos USB...';
@override
String usbConnectionFailed(String error) {
return 'Falha na conexão USB: $error';
}
@override
String get scanner_scanning => 'Procurando por dispositivos...';
+86
View File
@@ -111,6 +111,92 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
@override
String get connectionChoiceUsbLabel => 'USB';
@override
String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get usbScreenTitle => 'Подключение через USB';
@override
String get usbScreenSubtitle =>
'Выберите обнаруженное устройство с последовательным интерфейсом и подключите его напрямую к вашему узлу MeshCore.';
@override
String get usbScreenStatus => 'Выберите USB-устройство';
@override
String get usbScreenNote =>
'USB-серийный порт активен на поддерживаемых устройствах Android и на настольных платформах.';
@override
String get usbScreenEmptyState =>
'Не обнаружено устройств USB. Подключите одно из них и обновите список.';
@override
String get usbErrorPermissionDenied =>
'Запрос на доступ через USB был отклонен.';
@override
String get usbErrorDeviceMissing =>
'Выбранное USB-устройство больше недоступно.';
@override
String get usbErrorInvalidPort => 'Выберите действительное USB-устройство.';
@override
String get usbErrorBusy =>
'Еще одно запрошенное соединение через USB уже находится в процессе.';
@override
String get usbErrorNotConnected => 'Ни одно USB-устройство не подключено.';
@override
String get usbErrorOpenFailed =>
'Не удалось открыть выбранное USB-устройство.';
@override
String get usbErrorConnectFailed =>
'Не удалось установить соединение с выбранным USB-устройством.';
@override
String get usbErrorUnsupported =>
'Поддержка последовательного USB отсутствует на данной платформе.';
@override
String get usbErrorAlreadyActive => 'USB-соединение уже установлено.';
@override
String get usbErrorNoDeviceSelected =>
'Не было выбрано ни одно устройство USB.';
@override
String get usbErrorPortClosed => 'USB-соединение не установлено.';
@override
String get usbErrorConnectTimedOut =>
'Соединение не установлено. Убедитесь, что устройство имеет установленное программное обеспечение USB Companion.';
@override
String get usbFallbackDeviceName =>
'Устройство для последовательного подключения к сети';
@override
String get usbStatus_notConnected => 'Выберите USB-устройство';
@override
String get usbStatus_connecting => 'Подключение к USB-устройству...';
@override
String get usbStatus_searching => 'Поиск USB-устройств...';
@override
String usbConnectionFailed(String error) {
return 'Не удалось установить соединение через USB: $error';
}
@override
String get scanner_scanning => 'Поиск устройств...';
+85
View File
@@ -111,6 +111,91 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
@override
String get connectionChoiceUsbLabel => 'USB';
@override
String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get usbScreenTitle => 'Pripojte cez USB';
@override
String get usbScreenSubtitle =>
'Vyberte detekovaný sériový zariadenie a pripojte ho priamo k vašej MeshCore uzlu.';
@override
String get usbScreenStatus => 'Vyberte USB zariadenie';
@override
String get usbScreenNote =>
'USB sériová komunikácia je aktívna na podporovaných zariadeniach s Androidom a na desktopových platformách.';
@override
String get usbScreenEmptyState =>
'Nenašli sa žiadne USB zariadenia. Pripojte jedno a obnovte.';
@override
String get usbErrorPermissionDenied =>
'Žiadosť o prístup cez USB bola zamietnutá.';
@override
String get usbErrorDeviceMissing =>
'Vybrané USB zariadenie už nie je dostupné.';
@override
String get usbErrorInvalidPort => 'Vyberte platné USB zariadenie.';
@override
String get usbErrorBusy =>
'Ďalšia požiadavka na pripojenie cez USB je aktuálne v procese.';
@override
String get usbErrorNotConnected => 'Nie je pripojené žiadne USB zariadenie.';
@override
String get usbErrorOpenFailed =>
'Nepodarilo sa otvoriť vybrané USB zariadenie.';
@override
String get usbErrorConnectFailed =>
'Nepodarilo sa sa sa pripojiť k vybranému USB zariadeniu.';
@override
String get usbErrorUnsupported =>
'Podpora USB sériového rozhrania nie je na tejto platforme dostupná.';
@override
String get usbErrorAlreadyActive => 'Pripojenie cez USB je už aktivované.';
@override
String get usbErrorNoDeviceSelected =>
'Nebolo vybrané žiadne USB zariadenie.';
@override
String get usbErrorPortClosed => 'Pripojenie cez USB nie je aktivované.';
@override
String get usbErrorConnectTimedOut =>
'Pripojenie nebolo úspešné. Uistite sa, že zariadenie má nainštalovaný firmware USB Companion.';
@override
String get usbFallbackDeviceName => 'Webový sériový zariadenie';
@override
String get usbStatus_notConnected => 'Vyberte USB zariadenie';
@override
String get usbStatus_connecting => 'Pripojenie k USB zariadeniu...';
@override
String get usbStatus_searching => 'Hľadanie USB zariadení...';
@override
String usbConnectionFailed(String error) {
return 'Neúspešné pripojenie cez USB: $error';
}
@override
String get scanner_scanning => 'Skrívania zariadení...';
+83
View File
@@ -111,6 +111,89 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
@override
String get connectionChoiceUsbLabel => 'USB';
@override
String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get usbScreenTitle => 'Povežite preko USB';
@override
String get usbScreenSubtitle =>
'Izberite zaznano serijsko napravo in se neposredno povežite z vašo MeshCore napravo.';
@override
String get usbScreenStatus => 'Izberite USB naprave';
@override
String get usbScreenNote =>
'USB serijska povezava je aktivna na podprtih napravah Android in na desktop platformah.';
@override
String get usbScreenEmptyState =>
'Niti en USB naprave niso najdeni. Povežite eno in posodobite.';
@override
String get usbErrorPermissionDenied =>
'Dovoljenje za dostop preko USB-ja je bilo zavrnjeno.';
@override
String get usbErrorDeviceMissing => 'Izbrani USB napravej je več ne.';
@override
String get usbErrorInvalidPort => 'Izberite veljavno USB naprave.';
@override
String get usbErrorBusy => 'Že je v teku zahteva za povezavo preko USB.';
@override
String get usbErrorNotConnected => 'Ni priklopljenih USB naprave.';
@override
String get usbErrorOpenFailed =>
'Uspešno ni bilo mogo, da se odpre izbran naprave USB.';
@override
String get usbErrorConnectFailed =>
'Niso bilo mogoče uskladiti povezave z izbranim USB napom.';
@override
String get usbErrorUnsupported =>
'USB serijska komunikacija ni podprta na tej platformi.';
@override
String get usbErrorAlreadyActive => 'USB povezava je že aktivirana.';
@override
String get usbErrorNoDeviceSelected => 'Ni bilo izbranega USB naprave.';
@override
String get usbErrorPortClosed => 'USB povezava ni aktivirana.';
@override
String get usbErrorConnectTimedOut =>
'Vzpostavitve ni bilo mogo. Prosimo, da se prepričate, da ima naprave trenutno nameštan firmware USB Companion.';
@override
String get usbFallbackDeviceName =>
'Naprave za serijsko komunikacijo preko spleta';
@override
String get usbStatus_notConnected => 'Izberite USB naprave.';
@override
String get usbStatus_connecting => 'Povezava z USB napravo...';
@override
String get usbStatus_searching => 'Iskanje USB naprav...';
@override
String usbConnectionFailed(String error) {
return 'Napaka pri povezavi preko USB: $error';
}
@override
String get scanner_scanning => 'Skeniram za naprave...';
+83
View File
@@ -111,6 +111,89 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
@override
String get connectionChoiceUsbLabel => 'USB';
@override
String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get usbScreenTitle => 'Anslut via USB';
@override
String get usbScreenSubtitle =>
'Välj en detekterad seriell enhet och anslut direkt till din MeshCore-nod.';
@override
String get usbScreenStatus => 'Välj en USB-enhet';
@override
String get usbScreenNote =>
'USB-seriell kommunikation är aktiv på stödda Android-enheter och på skrivbordsplattformar.';
@override
String get usbScreenEmptyState =>
'Inga USB-enheter hittades. Anslut en och uppdatera.';
@override
String get usbErrorPermissionDenied => 'Tillgången via USB nekas.';
@override
String get usbErrorDeviceMissing =>
'Den valda USB-enheten är inte längre tillgänglig.';
@override
String get usbErrorInvalidPort => 'Välj en giltig USB-enhet.';
@override
String get usbErrorBusy =>
'En annan förfrågan om USB-anslutning är redan pågående.';
@override
String get usbErrorNotConnected => 'Ingen USB-enhet är ansluten.';
@override
String get usbErrorOpenFailed =>
'Misslyckades med att öppna det valda USB-enheten.';
@override
String get usbErrorConnectFailed =>
'Kunde inte ansluta till det valda USB-enheten.';
@override
String get usbErrorUnsupported =>
'USB-seriell kommunikation stöds inte på denna plattform.';
@override
String get usbErrorAlreadyActive => 'En USB-anslutning är redan aktiv.';
@override
String get usbErrorNoDeviceSelected => 'Ingen USB-enhet valdes.';
@override
String get usbErrorPortClosed => 'USB-anslutningen är inte aktiv.';
@override
String get usbErrorConnectTimedOut =>
'Anslutningen har tidsutgått. Se till att enheten har rätt USB-firmware.';
@override
String get usbFallbackDeviceName => 'Web-serieenhet';
@override
String get usbStatus_notConnected => 'Välj en USB-enhet';
@override
String get usbStatus_connecting => 'Anslutning till USB-enhet...';
@override
String get usbStatus_searching => 'Söker efter USB-enheter...';
@override
String usbConnectionFailed(String error) {
return 'Fel vid USB-anslutning: $error';
}
@override
String get scanner_scanning => 'Söker efter enheter...';
+84
View File
@@ -111,6 +111,90 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
@override
String get connectionChoiceUsbLabel => 'USB';
@override
String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get usbScreenTitle => 'Підключити через USB';
@override
String get usbScreenSubtitle =>
'Виберіть виявлене серійне пристрій і підключіть його безпосередньо до вашого вузла MeshCore.';
@override
String get usbScreenStatus => 'Виберіть пристрій USB';
@override
String get usbScreenNote =>
'USB-серіальний інтерфейс активний на підтримуваних пристроях на базі Android та на десктопних платформах.';
@override
String get usbScreenEmptyState =>
'Не знайдено жодних пристроїв USB. Підключіть один і перезавантажте.';
@override
String get usbErrorPermissionDenied =>
'Було відмовлено у наданні дозволу на використання USB.';
@override
String get usbErrorDeviceMissing => 'Вибране USB-пристрій більше недоступне.';
@override
String get usbErrorInvalidPort => 'Виберіть дійсний USB-пристрій.';
@override
String get usbErrorBusy =>
'Ще один запит на підключення через USB вже обробляється.';
@override
String get usbErrorNotConnected => 'Немає підключених пристроїв USB.';
@override
String get usbErrorOpenFailed => 'Не вдалося відкрити вибране USB-пристрій.';
@override
String get usbErrorConnectFailed =>
'Не вдалося підключитися до вибраного USB-пристрою.';
@override
String get usbErrorUnsupported =>
'Підтримка USB-серіального інтерфейсу не реалізована на цій платформі.';
@override
String get usbErrorAlreadyActive => 'USB-з\'єднання вже встановлено.';
@override
String get usbErrorNoDeviceSelected =>
'Не було вибрано жодного пристрою USB.';
@override
String get usbErrorPortClosed => 'З\'єднання USB не встановлено.';
@override
String get usbErrorConnectTimedOut =>
'З\'єднання не вдалося встановити. Переконайтеся, що пристрій має встановлене програмне забезпечення USB Companion.';
@override
String get usbFallbackDeviceName =>
'Пристрій для передачі даних по веб-серіалах';
@override
String get usbStatus_notConnected => 'Виберіть пристрій USB';
@override
String get usbStatus_connecting => 'Підключення до USB-пристрою...';
@override
String get usbStatus_searching => 'Пошук пристроїв USB...';
@override
String usbConnectionFailed(String error) {
return 'Не вдалося встановити з\'єднання через USB: $error';
}
@override
String get scanner_scanning => 'Пошук пристроїв...';
+74
View File
@@ -111,6 +111,80 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get scanner_title => '连接设备';
@override
String get connectionChoiceUsbLabel => 'USB';
@override
String get connectionChoiceBluetoothLabel => '蓝牙';
@override
String get usbScreenTitle => '通过USB连接';
@override
String get usbScreenSubtitle => '选择已检测到的串行设备,并直接连接到您的 MeshCore 节点。';
@override
String get usbScreenStatus => '选择一个 USB 设备';
@override
String get usbScreenNote => 'USB 串行接口在支持的 Android 设备和桌面平台上处于活动状态。';
@override
String get usbScreenEmptyState => '未找到任何 USB 设备。请插入一个,然后刷新。';
@override
String get usbErrorPermissionDenied => '拒绝了USB权限。';
@override
String get usbErrorDeviceMissing => '所选的USB设备已不再可用。';
@override
String get usbErrorInvalidPort => '选择一个有效的USB设备。';
@override
String get usbErrorBusy => '还有一个 USB 连接请求正在进行中。';
@override
String get usbErrorNotConnected => '没有连接任何USB设备。';
@override
String get usbErrorOpenFailed => '未能打开所选的USB设备。';
@override
String get usbErrorConnectFailed => '未能连接到所选的USB设备。';
@override
String get usbErrorUnsupported => '此平台不支持USB串行通信。';
@override
String get usbErrorAlreadyActive => 'USB 连接已建立。';
@override
String get usbErrorNoDeviceSelected => '未选择任何 USB 设备。';
@override
String get usbErrorPortClosed => 'USB 连接未建立。';
@override
String get usbErrorConnectTimedOut => '连接超时。请确保设备已安装 USB 伴侣固件。';
@override
String get usbFallbackDeviceName => 'Web 串流设备';
@override
String get usbStatus_notConnected => '选择一个 USB 设备';
@override
String get usbStatus_connecting => '连接USB设备...';
@override
String get usbStatus_searching => '正在搜索 USB 设备...';
@override
String usbConnectionFailed(String error) {
return 'USB 连接失败:$error';
}
@override
String get scanner_scanning => '正在搜索设备...';
+32 -1
View File
@@ -1828,5 +1828,36 @@
"discoveredContacts_deleteContactAll": "Verwijder alle ontdekte contacten",
"discoveredContacts_deleteContactAllContent": "Weet u zeker dat u alle ontdekte contacten wilt verwijderen?",
"map_guessedLocation": "Geroerde locatie",
"map_showGuessedLocations": "Toon de voorspelde locaties van de knopen"
"map_showGuessedLocations": "Toon de voorspelde locaties van de knopen",
"connectionChoiceUsbLabel": "USB",
"usbScreenSubtitle": "Selecteer een gedetecteerd seriële apparaat en verbind deze direct met uw MeshCore-node.",
"connectionChoiceBluetoothLabel": "Bluetooth",
"usbScreenTitle": "Verbind via USB",
"usbScreenStatus": "Selecteer een USB-apparaat",
"usbScreenNote": "USB-serieel is actief op ondersteunde Android-apparaten en desktop-platforms.",
"usbScreenEmptyState": "Geen USB-apparaten gevonden. Sluit er een aan en herlaad.",
"usbErrorPermissionDenied": "Toegang via USB is geweigerd.",
"usbErrorDeviceMissing": "Het geselecteerde USB-apparaat is niet meer beschikbaar.",
"usbErrorInvalidPort": "Selecteer een geldig USB-apparaat.",
"usbErrorBusy": "Een andere verzoek om een USB-verbinding is al in behandeling.",
"usbErrorNotConnected": "Er is geen USB-apparaat aangesloten.",
"usbErrorOpenFailed": "Kon het geselecteerde USB-apparaat niet openen.",
"usbErrorConnectFailed": "Kon niet verbinding maken met het geselecteerde USB-apparaat.",
"usbErrorUnsupported": "USB-serieel is niet ondersteund op deze platform.",
"usbErrorAlreadyActive": "Een USB-verbinding is al actief.",
"usbErrorNoDeviceSelected": "Geen USB-apparaat is geselecteerd.",
"usbErrorPortClosed": "De USB-verbinding is niet actief.",
"usbFallbackDeviceName": "Web-serieapparaat",
"@usbConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"usbConnectionFailed": "Fout bij de USB-verbinding: {error}",
"usbStatus_notConnected": "Selecteer een USB-apparaat",
"usbStatus_connecting": "Verbinding maken met USB-apparaat...",
"usbStatus_searching": "Zoeken naar USB-apparaten...",
"usbErrorConnectTimedOut": "Verbinding is verbroken. Zorg ervoor dat het apparaat de juiste USB-firmware heeft."
}
+32 -1
View File
@@ -1828,5 +1828,36 @@
"discoveredContacts_deleteContactAllContent": "Czy na pewno chcesz usunąć wszystkie znalezione kontakty?",
"discoveredContacts_deleteContactAll": "Usuń wszystkie odkryte kontakty",
"map_guessedLocation": "Wydana lokalizacja",
"map_showGuessedLocations": "Wyświetl lokalizacje zgadanych węzłów"
"map_showGuessedLocations": "Wyświetl lokalizacje zgadanych węzłów",
"usbScreenSubtitle": "Wybierz wykryty urządzenie szeregowe i podłącz je bezpośrednio do swojego węzła MeshCore.",
"usbScreenTitle": "Połącz przez USB",
"connectionChoiceUsbLabel": "USB",
"connectionChoiceBluetoothLabel": "Bluetooth",
"usbScreenStatus": "Wybierz urządzenie USB",
"usbScreenNote": "Port szeregowy USB jest aktywny na urządzeniach z systemem Android i platformach stacjonarnych, które go obsługują.",
"usbScreenEmptyState": "Nie znaleziono żadnych urządzeń USB. Podłącz jedno i zaktualizuj.",
"usbErrorPermissionDenied": "Zostało odrzucone żądanie dostępu przez USB.",
"usbErrorDeviceMissing": "Wybór urządzenia USB już nie jest dostępny.",
"usbErrorInvalidPort": "Wybierz prawidłowe urządzenie USB.",
"usbErrorBusy": "Kolejne żądanie połączenia przez USB jest już w trakcie realizacji.",
"usbErrorNotConnected": "Brak podłączonego urządzenia USB.",
"usbErrorOpenFailed": "Nie udało się otworzyć wybranego urządzenia USB.",
"usbErrorConnectFailed": "Nie udało się połączyć z wybranym urządzeniem USB.",
"usbErrorUnsupported": "Port szeregowy USB nie jest obsługiwany na tym urządzeniu.",
"usbErrorAlreadyActive": "Połączenie USB jest już aktywne.",
"usbErrorNoDeviceSelected": "Nie został wybrany żaden urządzenie USB.",
"usbErrorPortClosed": "Połączenie USB nie jest aktywne.",
"usbFallbackDeviceName": "Urządzenie do komunikacji przez sieć (seria)",
"@usbConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"usbStatus_searching": "Wyszukiwanie urządzeń USB...",
"usbStatus_connecting": "Połączenie z urządzeniem USB...",
"usbStatus_notConnected": "Wybierz urządzenie USB",
"usbConnectionFailed": "Błąd połączenia USB: {error}",
"usbErrorConnectTimedOut": "Połączenie nie zostało nawiązane. Upewnij się, że urządzenie posiada oprogramowanie \"USB Companion\"."
}
+32 -1
View File
@@ -1828,5 +1828,36 @@
"discoveredContacts_deleteContactAll": "Excluir Todos os Contatos Descobertos",
"discoveredContacts_deleteContactAllContent": "Tem certeza de que deseja excluir todos os contatos descobertos?",
"map_guessedLocation": "Localização estimada",
"map_showGuessedLocations": "Mostrar as localizações dos nós estimados"
"map_showGuessedLocations": "Mostrar as localizações dos nós estimados",
"connectionChoiceUsbLabel": "USB",
"connectionChoiceBluetoothLabel": "Bluetooth",
"usbScreenTitle": "Conecte via USB",
"usbScreenSubtitle": "Selecione um dispositivo serial detectado e conecte-o diretamente ao seu nó MeshCore.",
"usbScreenStatus": "Selecione um dispositivo USB",
"usbScreenNote": "A comunicação serial via USB está ativa em dispositivos Android e plataformas de desktop compatíveis.",
"usbScreenEmptyState": "Nenhum dispositivo USB encontrado. Conecte um e atualize.",
"usbErrorPermissionDenied": "A permissão para acesso via USB foi negada.",
"usbErrorDeviceMissing": "O dispositivo USB selecionado não está mais disponível.",
"usbErrorInvalidPort": "Selecione um dispositivo USB válido.",
"usbErrorBusy": "Já existe uma solicitação de conexão USB em andamento.",
"usbErrorNotConnected": "Não há nenhum dispositivo USB conectado.",
"usbErrorOpenFailed": "Não foi possível abrir o dispositivo USB selecionado.",
"usbErrorConnectFailed": "Não foi possível conectar ao dispositivo USB selecionado.",
"usbErrorUnsupported": "A comunicação serial via USB não é suportada nesta plataforma.",
"usbErrorAlreadyActive": "A conexão USB já está ativa.",
"usbErrorNoDeviceSelected": "Nenhum dispositivo USB foi selecionado.",
"usbErrorPortClosed": "A conexão USB não está ativa.",
"usbFallbackDeviceName": "Dispositivo de Serial para a Web",
"@usbConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"usbStatus_searching": "Procurando por dispositivos USB...",
"usbStatus_notConnected": "Selecione um dispositivo USB",
"usbConnectionFailed": "Falha na conexão USB: {error}",
"usbStatus_connecting": "Conectando ao dispositivo USB...",
"usbErrorConnectTimedOut": "A conexão expirou. Verifique se o dispositivo possui o firmware USB Companion."
}
+32 -1
View File
@@ -1068,5 +1068,36 @@
"discoveredContacts_deleteContactAllContent": "Вы уверены, что хотите удалить все обнаруженные контакты?",
"discoveredContacts_deleteContactAll": "Удалить Все Обнаруженные Контакты",
"map_guessedLocation": "Угаданное место",
"map_showGuessedLocations": "Отобразить предполагаемые места расположения узлов"
"map_showGuessedLocations": "Отобразить предполагаемые места расположения узлов",
"connectionChoiceUsbLabel": "USB",
"usbScreenSubtitle": "Выберите обнаруженное устройство с последовательным интерфейсом и подключите его напрямую к вашему узлу MeshCore.",
"usbScreenTitle": "Подключение через USB",
"connectionChoiceBluetoothLabel": "Bluetooth",
"usbScreenStatus": "Выберите USB-устройство",
"usbScreenNote": "USB-серийный порт активен на поддерживаемых устройствах Android и на настольных платформах.",
"usbScreenEmptyState": "Не обнаружено устройств USB. Подключите одно из них и обновите список.",
"usbErrorPermissionDenied": "Запрос на доступ через USB был отклонен.",
"usbErrorDeviceMissing": "Выбранное USB-устройство больше недоступно.",
"usbErrorInvalidPort": "Выберите действительное USB-устройство.",
"usbErrorBusy": "Еще одно запрошенное соединение через USB уже находится в процессе.",
"usbErrorNotConnected": "Ни одно USB-устройство не подключено.",
"usbErrorOpenFailed": "Не удалось открыть выбранное USB-устройство.",
"usbErrorConnectFailed": "Не удалось установить соединение с выбранным USB-устройством.",
"usbErrorUnsupported": "Поддержка последовательного USB отсутствует на данной платформе.",
"usbErrorAlreadyActive": "USB-соединение уже установлено.",
"usbErrorNoDeviceSelected": "Не было выбрано ни одно устройство USB.",
"usbErrorPortClosed": "USB-соединение не установлено.",
"usbFallbackDeviceName": "Устройство для последовательного подключения к сети",
"@usbConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"usbStatus_searching": "Поиск USB-устройств...",
"usbStatus_connecting": "Подключение к USB-устройству...",
"usbConnectionFailed": "Не удалось установить соединение через USB: {error}",
"usbStatus_notConnected": "Выберите USB-устройство",
"usbErrorConnectTimedOut": "Соединение не установлено. Убедитесь, что устройство имеет установленное программное обеспечение USB Companion."
}
+32 -1
View File
@@ -1828,5 +1828,36 @@
"common_deleteAll": "Zmazať všetko",
"discoveredContacts_deleteContactAllContent": "Ste si istí, že chcete zmazať všetky objavené kontakty?",
"map_showGuessedLocations": "Zobraziť umiestnenia odhadnutých uzlov",
"map_guessedLocation": "Odhadnutá lokalita"
"map_guessedLocation": "Odhadnutá lokalita",
"usbScreenTitle": "Pripojte cez USB",
"usbScreenSubtitle": "Vyberte detekovaný sériový zariadenie a pripojte ho priamo k vašej MeshCore uzlu.",
"connectionChoiceUsbLabel": "USB",
"connectionChoiceBluetoothLabel": "Bluetooth",
"usbScreenStatus": "Vyberte USB zariadenie",
"usbScreenNote": "USB sériová komunikácia je aktívna na podporovaných zariadeniach s Androidom a na desktopových platformách.",
"usbScreenEmptyState": "Nenašli sa žiadne USB zariadenia. Pripojte jedno a obnovte.",
"usbErrorPermissionDenied": "Žiadosť o prístup cez USB bola zamietnutá.",
"usbErrorDeviceMissing": "Vybrané USB zariadenie už nie je dostupné.",
"usbErrorInvalidPort": "Vyberte platné USB zariadenie.",
"usbErrorBusy": "Ďalšia požiadavka na pripojenie cez USB je aktuálne v procese.",
"usbErrorNotConnected": "Nie je pripojené žiadne USB zariadenie.",
"usbErrorOpenFailed": "Nepodarilo sa otvoriť vybrané USB zariadenie.",
"usbErrorConnectFailed": "Nepodarilo sa sa sa pripojiť k vybranému USB zariadeniu.",
"usbErrorUnsupported": "Podpora USB sériového rozhrania nie je na tejto platforme dostupná.",
"usbErrorAlreadyActive": "Pripojenie cez USB je už aktivované.",
"usbErrorNoDeviceSelected": "Nebolo vybrané žiadne USB zariadenie.",
"usbErrorPortClosed": "Pripojenie cez USB nie je aktivované.",
"usbFallbackDeviceName": "Webový sériový zariadenie",
"@usbConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"usbStatus_searching": "Hľadanie USB zariadení...",
"usbConnectionFailed": "Neúspešné pripojenie cez USB: {error}",
"usbStatus_notConnected": "Vyberte USB zariadenie",
"usbStatus_connecting": "Pripojenie k USB zariadeniu...",
"usbErrorConnectTimedOut": "Pripojenie nebolo úspešné. Uistite sa, že zariadenie má nainštalovaný firmware USB Companion."
}
+32 -1
View File
@@ -1828,5 +1828,36 @@
"discoveredContacts_deleteContactAllContent": "Ste prepričani, da želite izbrisati vse odkrite kontakte?",
"discoveredContacts_deleteContactAll": "Izbriši vse odkrite kontakte",
"map_guessedLocation": "Predpostavljena lokacija",
"map_showGuessedLocations": "Pokaži lokacije domnevnih not."
"map_showGuessedLocations": "Pokaži lokacije domnevnih not.",
"usbScreenTitle": "Povežite preko USB",
"usbScreenSubtitle": "Izberite zaznano serijsko napravo in se neposredno povežite z vašo MeshCore napravo.",
"connectionChoiceBluetoothLabel": "Bluetooth",
"connectionChoiceUsbLabel": "USB",
"usbScreenStatus": "Izberite USB naprave",
"usbScreenNote": "USB serijska povezava je aktivna na podprtih napravah Android in na desktop platformah.",
"usbScreenEmptyState": "Niti en USB naprave niso najdeni. Povežite eno in posodobite.",
"usbErrorPermissionDenied": "Dovoljenje za dostop preko USB-ja je bilo zavrnjeno.",
"usbErrorDeviceMissing": "Izbrani USB napravej je več ne.",
"usbErrorInvalidPort": "Izberite veljavno USB naprave.",
"usbErrorBusy": "Že je v teku zahteva za povezavo preko USB.",
"usbErrorNotConnected": "Ni priklopljenih USB naprave.",
"usbErrorOpenFailed": "Uspešno ni bilo mogo, da se odpre izbran naprave USB.",
"usbErrorConnectFailed": "Niso bilo mogoče uskladiti povezave z izbranim USB napom.",
"usbErrorUnsupported": "USB serijska komunikacija ni podprta na tej platformi.",
"usbErrorAlreadyActive": "USB povezava je že aktivirana.",
"usbErrorNoDeviceSelected": "Ni bilo izbranega USB naprave.",
"usbErrorPortClosed": "USB povezava ni aktivirana.",
"usbFallbackDeviceName": "Naprave za serijsko komunikacijo preko spleta",
"@usbConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"usbStatus_notConnected": "Izberite USB naprave.",
"usbStatus_connecting": "Povezava z USB napravo...",
"usbStatus_searching": "Iskanje USB naprav...",
"usbConnectionFailed": "Napaka pri povezavi preko USB: {error}",
"usbErrorConnectTimedOut": "Vzpostavitve ni bilo mogo. Prosimo, da se prepričate, da ima naprave trenutno nameštan firmware USB Companion."
}
+32 -1
View File
@@ -1828,5 +1828,36 @@
"discoveredContacts_deleteContactAllContent": "Är du säker på att du vill ta bort alla upptäckta kontakter?",
"discoveredContacts_deleteContactAll": "Ta bort alla upptäckta kontakter",
"map_guessedLocation": "Gissad plats",
"map_showGuessedLocations": "Visa upp de antagna nodernas placeringar"
"map_showGuessedLocations": "Visa upp de antagna nodernas placeringar",
"connectionChoiceBluetoothLabel": "Bluetooth",
"connectionChoiceUsbLabel": "USB",
"usbScreenSubtitle": "Välj en detekterad seriell enhet och anslut direkt till din MeshCore-nod.",
"usbScreenTitle": "Anslut via USB",
"usbScreenStatus": "Välj en USB-enhet",
"usbScreenNote": "USB-seriell kommunikation är aktiv på stödda Android-enheter och på skrivbordsplattformar.",
"usbScreenEmptyState": "Inga USB-enheter hittades. Anslut en och uppdatera.",
"usbErrorPermissionDenied": "Tillgången via USB nekas.",
"usbErrorDeviceMissing": "Den valda USB-enheten är inte längre tillgänglig.",
"usbErrorInvalidPort": "Välj en giltig USB-enhet.",
"usbErrorBusy": "En annan förfrågan om USB-anslutning är redan pågående.",
"usbErrorNotConnected": "Ingen USB-enhet är ansluten.",
"usbErrorOpenFailed": "Misslyckades med att öppna det valda USB-enheten.",
"usbErrorConnectFailed": "Kunde inte ansluta till det valda USB-enheten.",
"usbErrorUnsupported": "USB-seriell kommunikation stöds inte på denna plattform.",
"usbErrorAlreadyActive": "En USB-anslutning är redan aktiv.",
"usbErrorNoDeviceSelected": "Ingen USB-enhet valdes.",
"usbErrorPortClosed": "USB-anslutningen är inte aktiv.",
"usbFallbackDeviceName": "Web-serieenhet",
"@usbConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"usbStatus_connecting": "Anslutning till USB-enhet...",
"usbStatus_notConnected": "Välj en USB-enhet",
"usbConnectionFailed": "Fel vid USB-anslutning: {error}",
"usbStatus_searching": "Söker efter USB-enheter...",
"usbErrorConnectTimedOut": "Anslutningen har tidsutgått. Se till att enheten har rätt USB-firmware."
}
+32 -1
View File
@@ -1828,5 +1828,36 @@
"discoveredContacts_deleteContactAll": "Видалити всі виявлені контакти",
"discoveredContacts_deleteContactAllContent": "Ви впевнені, що хочете видалити всі виявлені контакти?",
"map_showGuessedLocations": "Показати місцезнаходження передбачених вузлів",
"map_guessedLocation": "Визначено місцезнаходження"
"map_guessedLocation": "Визначено місцезнаходження",
"usbScreenSubtitle": "Виберіть виявлене серійне пристрій і підключіть його безпосередньо до вашого вузла MeshCore.",
"usbScreenTitle": "Підключити через USB",
"connectionChoiceBluetoothLabel": "Bluetooth",
"connectionChoiceUsbLabel": "USB",
"usbScreenStatus": "Виберіть пристрій USB",
"usbScreenNote": "USB-серіальний інтерфейс активний на підтримуваних пристроях на базі Android та на десктопних платформах.",
"usbScreenEmptyState": "Не знайдено жодних пристроїв USB. Підключіть один і перезавантажте.",
"usbErrorPermissionDenied": "Було відмовлено у наданні дозволу на використання USB.",
"usbErrorDeviceMissing": "Вибране USB-пристрій більше недоступне.",
"usbErrorInvalidPort": "Виберіть дійсний USB-пристрій.",
"usbErrorBusy": "Ще один запит на підключення через USB вже обробляється.",
"usbErrorNotConnected": "Немає підключених пристроїв USB.",
"usbErrorOpenFailed": "Не вдалося відкрити вибране USB-пристрій.",
"usbErrorConnectFailed": "Не вдалося підключитися до вибраного USB-пристрою.",
"usbErrorUnsupported": "Підтримка USB-серіального інтерфейсу не реалізована на цій платформі.",
"usbErrorAlreadyActive": "USB-з'єднання вже встановлено.",
"usbErrorNoDeviceSelected": "Не було вибрано жодного пристрою USB.",
"usbErrorPortClosed": "З'єднання USB не встановлено.",
"usbFallbackDeviceName": "Пристрій для передачі даних по веб-серіалах",
"@usbConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"usbStatus_searching": "Пошук пристроїв USB...",
"usbStatus_notConnected": "Виберіть пристрій USB",
"usbConnectionFailed": "Не вдалося встановити з'єднання через USB: {error}",
"usbStatus_connecting": "Підключення до USB-пристрою...",
"usbErrorConnectTimedOut": "З'єднання не вдалося встановити. Переконайтеся, що пристрій має встановлене програмне забезпечення USB Companion."
}
+32 -1
View File
@@ -1833,5 +1833,36 @@
"discoveredContacts_deleteContactAllContent": "您确定要删除所有发现的联系人吗?",
"discoveredContacts_deleteContactAll": "删除所有发现的联系人",
"map_showGuessedLocations": "显示猜测的节点位置",
"map_guessedLocation": "猜测的位置"
"map_guessedLocation": "猜测的位置",
"connectionChoiceUsbLabel": "USB",
"usbScreenTitle": "通过USB连接",
"usbScreenSubtitle": "选择已检测到的串行设备,并直接连接到您的 MeshCore 节点。",
"connectionChoiceBluetoothLabel": "蓝牙",
"usbScreenStatus": "选择一个 USB 设备",
"usbScreenNote": "USB 串行接口在支持的 Android 设备和桌面平台上处于活动状态。",
"usbScreenEmptyState": "未找到任何 USB 设备。请插入一个,然后刷新。",
"usbErrorPermissionDenied": "拒绝了USB权限。",
"usbErrorDeviceMissing": "所选的USB设备已不再可用。",
"usbErrorInvalidPort": "选择一个有效的USB设备。",
"usbErrorBusy": "还有一个 USB 连接请求正在进行中。",
"usbErrorNotConnected": "没有连接任何USB设备。",
"usbErrorOpenFailed": "未能打开所选的USB设备。",
"usbErrorConnectFailed": "未能连接到所选的USB设备。",
"usbErrorUnsupported": "此平台不支持USB串行通信。",
"usbErrorAlreadyActive": "USB 连接已建立。",
"usbErrorNoDeviceSelected": "未选择任何 USB 设备。",
"usbErrorPortClosed": "USB 连接未建立。",
"usbFallbackDeviceName": "Web 串流设备",
"@usbConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"usbStatus_searching": "正在搜索 USB 设备...",
"usbStatus_connecting": "连接USB设备...",
"usbStatus_notConnected": "选择一个 USB 设备",
"usbConnectionFailed": "USB 连接失败:{error}",
"usbErrorConnectTimedOut": "连接超时。请确保设备已安装 USB 伴侣固件。"
}
+20 -19
View File
@@ -1,4 +1,6 @@
import 'dart:typed_data';
import 'package:meshcore_open/utils/app_logger.dart';
import '../connector/meshcore_protocol.dart';
class Contact {
@@ -166,28 +168,27 @@ class Contact {
static Contact? fromFrame(Uint8List data) {
if (data.isEmpty) return null;
if (data[0] != respCodeContact) return null;
final reader = BufferReader(data);
try {
final pubKey = Uint8List.fromList(
data.sublist(contactPubKeyOffset, contactPubKeyOffset + pubKeySize),
);
final type = data[contactTypeOffset];
final flags = data[contactFlagsOffset];
final pathLen = data[contactPathLenOffset].toSigned(8);
final respCode = reader.readByte();
if (respCode != respCodeContact && respCode != pushCodeNewAdvert) {
return null;
}
final pubKey = reader.readBytes(pubKeySize);
final type = reader.readByte();
final flags = reader.readByte();
final pathLen = reader.readByte();
final safePathLen = pathLen > 0
? (pathLen > maxPathSize ? maxPathSize : pathLen)
: 0;
final pathBytes = safePathLen > 0
? Uint8List.fromList(
data.sublist(contactPathOffset, contactPathOffset + safePathLen),
)
: Uint8List(0);
final name = readCString(data, contactNameOffset, maxNameSize);
final lastmod = readUint32LE(data, contactLastModOffset);
final pathBytes = reader.readBytes(maxPathSize).sublist(0, safePathLen);
final name = reader.readCStringGreedy(maxNameSize);
final lastMod = reader.readUInt32LE();
double? lat, lon;
final latRaw = readInt32LE(data, contactLatOffset);
final lonRaw = readInt32LE(data, contactLonOffset);
final latRaw = reader.readInt32LE();
final lonRaw = reader.readInt32LE();
if (latRaw != 0 || lonRaw != 0) {
lat = latRaw / 1e6;
lon = lonRaw / 1e6;
@@ -198,14 +199,14 @@ class Contact {
name: name.isEmpty ? 'Unknown' : name,
type: type,
flags: flags,
pathLength: pathLen,
pathLength: pathLen > 0 ? (pathLen > maxPathSize ? -1 : pathLen) : -1,
path: pathBytes,
latitude: lat,
longitude: lon,
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastmod * 1000),
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastMod * 1000),
);
} catch (e) {
// If parsing fails, return null
appLogger.error('Failed to parse contact frame: $e');
return null;
}
}
+4
View File
@@ -54,6 +54,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
@override
void initState() {
super.initState();
final connector = context.read<MeshCoreConnector>();
_communityStore.setPublicKeyHex = connector.selfPublicKeyHex;
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<MeshCoreConnector>().getChannels();
_loadCommunities();
@@ -106,7 +108,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
@override
Widget build(BuildContext context) {
final connector = context.watch<MeshCoreConnector>();
final channelMessageStore = ChannelMessageStore();
channelMessageStore.setPublicKeyHex = connector.selfPublicKeyHex;
// Auto-navigate back to scanner if disconnected
if (!checkConnectionAndNavigate(connector)) {
@@ -51,6 +51,9 @@ class _CommunityQrScannerScreenState extends State<CommunityQrScannerScreen> {
_isProcessing = true;
});
final connector = context.read<MeshCoreConnector>();
_communityStore.setPublicKeyHex = connector.selfPublicKeyHex;
try {
// Parse the community data
final community = Community.fromQrData(const Uuid().v4(), data);
@@ -209,6 +212,8 @@ class _CommunityQrScannerScreenState extends State<CommunityQrScannerScreen> {
bool addPublicChannel,
) async {
// Save community to local storage
final connector = context.read<MeshCoreConnector>();
_communityStore.setPublicKeyHex = connector.selfPublicKeyHex;
await _communityStore.addCommunity(community);
// Optionally add the community public channel to the device
+8 -1
View File
@@ -401,8 +401,15 @@ class _ContactsScreenState extends State<ContactsScreen>
Widget _buildContactsBody(BuildContext context, MeshCoreConnector connector) {
final contacts = connector.contacts;
final shouldShowStartupSpinner =
contacts.isEmpty &&
_groups.isEmpty &&
connector.isConnected &&
(connector.isLoadingContacts ||
connector.isLoadingChannels ||
connector.selfPublicKey == null);
if (contacts.isEmpty && connector.isLoadingContacts && _groups.isEmpty) {
if (shouldShowStartupSpinner) {
return const Center(child: CircularProgressIndicator());
}
+83 -36
View File
@@ -6,9 +6,11 @@ import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart';
import '../l10n/l10n.dart';
import '../utils/app_logger.dart';
import '../widgets/adaptive_app_bar_title.dart';
import '../widgets/device_tile.dart';
import 'contacts_screen.dart';
import 'usb_screen.dart';
/// Screen for scanning and connecting to MeshCore devices
class ScannerScreen extends StatefulWidget {
@@ -20,6 +22,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 +30,15 @@ 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) {
final isCurrentRoute = ModalRoute.of(context)?.isCurrent ?? true;
if (_connector.state == MeshCoreConnectionState.disconnected) {
_changedNavigation = false;
} else if (connector.state == MeshCoreConnectionState.connected &&
} else if (_connector.state == MeshCoreConnectionState.connected &&
_connector.activeTransport == MeshCoreTransportType.bluetooth &&
isCurrentRoute &&
!_changedNavigation) {
_changedNavigation = true;
if (mounted) {
@@ -43,7 +49,7 @@ class _ScannerScreenState extends State<ScannerScreen> {
}
};
connector.addListener(_connectionListener);
_connector.addListener(_connectionListener);
_bluetoothStateSubscription = FlutterBluePlus.adapterState.listen(
(state) {
@@ -53,28 +59,42 @@ class _ScannerScreenState extends State<ScannerScreen> {
});
// Cancel scan if Bluetooth turns off while scanning
if (state != BluetoothAdapterState.on) {
unawaited(connector.stopScan());
unawaited(_connector.stopScan());
}
}
},
onError: (Object e) {
debugPrint("Scanner adapterState stream error: $e");
appLogger.warn('Adapter state stream error: $e', tag: 'ScannerScreen');
},
);
}
@override
void dispose() {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
connector.removeListener(_connectionListener);
_connector.removeListener(_connectionListener);
unawaited(_bluetoothStateSubscription.cancel());
if (!_changedNavigation) {
WidgetsBinding.instance.addPostFrameCallback((_) {
unawaited(_connector.disconnect(manual: true));
});
}
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: () {
appLogger.info('Back button pressed', tag: 'ScannerScreen');
Navigator.of(context).maybePop();
},
)
: null,
title: AdaptiveAppBarTitle(context.l10n.scanner_title),
centerTitle: true,
automaticallyImplyLeading: false,
@@ -99,40 +119,67 @@ class _ScannerScreenState extends State<ScannerScreen> {
},
),
),
floatingActionButton: Consumer<MeshCoreConnector>(
bottomNavigationBar: Consumer<MeshCoreConnector>(
builder: (context, connector, child) {
final isScanning =
connector.state == MeshCoreConnectionState.scanning;
final isBluetoothOff = _bluetoothState == BluetoothAdapterState.off;
final usbSupported = PlatformInfo.supportsUsbSerial;
return FloatingActionButton.extended(
onPressed: isBluetoothOff
? null
: () {
if (isScanning) {
connector.stopScan();
} else {
unawaited(
connector.startScan().catchError((e) {
debugPrint("Scanner screen startScan error: $e");
}),
return SafeArea(
top: false,
minimum: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (usbSupported)
FloatingActionButton.extended(
onPressed: () {
appLogger.info(
'USB selected, opening UsbScreen',
tag: 'ScannerScreen',
);
}
},
icon: isScanning
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Icon(Icons.bluetooth_searching),
label: Text(
isScanning
? context.l10n.scanner_stop
: context.l10n.scanner_scan,
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const UsbScreen()),
);
},
heroTag: 'scanner_usb_action',
icon: const Icon(Icons.usb),
label: Text(context.l10n.connectionChoiceUsbLabel),
),
if (usbSupported) const SizedBox(width: 12),
FloatingActionButton.extended(
heroTag: 'scanner_ble_action',
onPressed: isBluetoothOff
? null
: () {
if (isScanning) {
connector.stopScan();
} else {
unawaited(
connector.startScan().catchError((e) {
appLogger.warn(
'startScan error: $e',
tag: 'ScannerScreen',
);
}),
);
}
},
icon: isScanning
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.bluetooth_searching),
label: Text(
isScanning
? context.l10n.scanner_stop
: context.l10n.scanner_scan,
),
),
],
),
);
},
+407
View File
@@ -0,0 +1,407 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart';
import '../l10n/l10n.dart';
import '../utils/app_logger.dart';
import '../utils/platform_info.dart';
import '../utils/usb_port_labels.dart';
import '../widgets/adaptive_app_bar_title.dart';
import 'contacts_screen.dart';
import 'scanner_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 _navigatedToContacts = false;
bool _didScheduleInitialLoad = false;
Timer? _hotPlugTimer;
late final MeshCoreConnector _connector;
late final VoidCallback _connectionListener;
bool get _supportsHotPlug =>
PlatformInfo.isWindows || PlatformInfo.isLinux || PlatformInfo.isMacOS;
@override
void initState() {
super.initState();
_connector = context.read<MeshCoreConnector>();
_connectionListener = () {
if (!mounted) return;
if (_connector.state == MeshCoreConnectionState.disconnected) {
_navigatedToContacts = false;
}
if (_connector.state == MeshCoreConnectionState.connected &&
_connector.isUsbTransportConnected &&
!_navigatedToContacts) {
_navigatedToContacts = true;
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const ContactsScreen()),
);
}
};
_connector.addListener(_connectionListener);
_startHotPlugTimer();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_connector.setUsbRequestPortLabel(context.l10n.usbScreenStatus);
_connector.setUsbFallbackDeviceName(context.l10n.usbFallbackDeviceName);
if (!_didScheduleInitialLoad) {
_didScheduleInitialLoad = true;
unawaited(_loadPorts());
}
}
@override
void dispose() {
_hotPlugTimer?.cancel();
_hotPlugTimer = null;
_connector.removeListener(_connectionListener);
if (!_navigatedToContacts &&
_connector.activeTransport == MeshCoreTransportType.usb &&
_connector.state != MeshCoreConnectionState.disconnected) {
WidgetsBinding.instance.addPostFrameCallback((_) {
unawaited(_connector.disconnect(manual: true));
});
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.of(context).maybePop(),
),
title: AdaptiveAppBarTitle(context.l10n.usbScreenTitle),
centerTitle: true,
),
body: SafeArea(
top: false,
child: Consumer<MeshCoreConnector>(
builder: (context, connector, child) {
return Column(
children: [
_buildStatusBar(context, connector),
Expanded(child: _buildPortList(context, connector)),
],
);
},
),
),
bottomNavigationBar: Consumer<MeshCoreConnector>(
builder: (context, connector, child) {
final isLoading = _isLoadingPorts;
final showBle =
PlatformInfo.isWeb ||
PlatformInfo.isAndroid ||
PlatformInfo.isIOS;
return SafeArea(
top: false,
minimum: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (showBle)
FloatingActionButton.extended(
onPressed: () {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) => const ScannerScreen(),
),
);
},
heroTag: 'usb_ble_action',
icon: const Icon(Icons.bluetooth),
label: Text(context.l10n.connectionChoiceBluetoothLabel),
),
if (showBle) const SizedBox(width: 12),
if (!_supportsHotPlug)
FloatingActionButton.extended(
onPressed: isLoading ? null : _loadPorts,
heroTag: 'usb_refresh_action',
icon: isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.refresh),
label: Text(context.l10n.repeater_refresh),
),
],
),
);
},
),
);
}
Widget _buildStatusBar(BuildContext context, MeshCoreConnector connector) {
final l10n = context.l10n;
String statusText;
Color statusColor;
if (_isLoadingPorts) {
statusText = l10n.usbStatus_searching;
statusColor = Colors.blue;
} else if (connector.isUsbTransportConnected) {
switch (connector.state) {
case MeshCoreConnectionState.connected:
statusText = l10n.scanner_connectedTo(
connector.activeUsbPortDisplayLabel ?? 'USB',
);
statusColor = Colors.green;
case MeshCoreConnectionState.disconnecting:
statusText = l10n.scanner_disconnecting;
statusColor = Colors.orange;
default:
statusText = l10n.usbStatus_notConnected;
statusColor = Colors.grey;
}
} else if (connector.state == MeshCoreConnectionState.connecting &&
connector.activeTransport == MeshCoreTransportType.usb) {
statusText = l10n.usbStatus_connecting;
statusColor = Colors.orange;
} else {
statusText = l10n.usbStatus_notConnected;
statusColor = Colors.grey;
}
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
color: statusColor.withValues(alpha: 0.1),
child: Row(
children: [
Icon(Icons.circle, size: 12, color: statusColor),
const SizedBox(width: 8),
Text(
statusText,
style: TextStyle(color: statusColor, fontWeight: FontWeight.w500),
),
],
),
);
}
Widget _buildPortList(BuildContext context, MeshCoreConnector connector) {
final l10n = context.l10n;
if (_isLoadingPorts) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.usb, size: 64, color: Colors.grey[400]),
const SizedBox(height: 16),
Text(
l10n.usbStatus_searching,
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
),
],
),
);
}
if (_ports.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.usb, size: 64, color: Colors.grey[400]),
const SizedBox(height: 16),
Text(
l10n.usbScreenEmptyState,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
),
],
),
);
}
final isConnecting =
connector.state == MeshCoreConnectionState.connecting &&
connector.activeTransport == MeshCoreTransportType.usb;
return ListView.separated(
padding: const EdgeInsets.all(8),
itemCount: _ports.length,
separatorBuilder: (context, index) => const Divider(),
itemBuilder: (context, index) {
final port = _ports[index];
final displayName = friendlyUsbPortName(port);
final rawName = normalizeUsbPortName(port);
final showRawName =
rawName != displayName && !rawName.startsWith('web:');
return ListTile(
leading: const Icon(Icons.usb),
title: Text(
displayName,
style: const TextStyle(fontWeight: FontWeight.w500),
),
subtitle: showRawName ? Text(rawName) : null,
trailing: ElevatedButton(
onPressed: isConnecting ? null : () => _connectPort(port),
child: Text(l10n.common_connect),
),
onTap: isConnecting ? null : () => _connectPort(port),
);
},
);
}
void _startHotPlugTimer() {
if (!_supportsHotPlug) return;
_hotPlugTimer?.cancel();
_hotPlugTimer = Timer.periodic(const Duration(seconds: 2), (_) {
_pollHotPlug();
});
}
Future<void> _pollHotPlug() async {
if (_isLoadingPorts) return;
if (!mounted) return;
// Don't poll while connecting or connected.
if (_connector.state != MeshCoreConnectionState.disconnected) return;
try {
final ports = await _connector.listUsbPorts();
if (!mounted) return;
final added = ports.where((p) => !_ports.contains(p)).toList();
final removed = _ports.where((p) => !ports.contains(p)).toList();
if (added.isEmpty && removed.isEmpty) return;
setState(() {
_ports
..clear()
..addAll(ports);
});
} catch (_) {
// Silent hot-plug failures are non-critical.
}
}
Future<void> _loadPorts() async {
if (!mounted) return;
_connector.setUsbRequestPortLabel(context.l10n.usbScreenStatus);
setState(() {
_isLoadingPorts = true;
});
try {
final ports = await _connector.listUsbPorts();
if (!mounted) return;
setState(() {
_ports
..clear()
..addAll(ports);
_isLoadingPorts = false;
});
} catch (error) {
if (!mounted) return;
setState(() {
_ports.clear();
_isLoadingPorts = false;
});
_showError(error);
}
}
Future<void> _connectPort(String port) async {
if (_connector.state != MeshCoreConnectionState.disconnected) return;
final rawPortName = normalizeUsbPortName(port);
appLogger.info(
'Connect tapped for $port (raw: $rawPortName)',
tag: 'UsbScreen',
);
try {
await _connector.connectUsb(portName: rawPortName);
} catch (error, stackTrace) {
appLogger.error(
'Connect failed for $rawPortName: $error\n$stackTrace',
tag: 'UsbScreen',
);
if (!mounted) return;
_showError(error);
unawaited(_loadPorts());
}
}
void _showError(Object error) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(_friendlyErrorMessage(error)),
backgroundColor: Colors.red,
),
);
}
String _friendlyErrorMessage(Object error) {
final l10n = context.l10n;
if (error is PlatformException) {
switch (error.code) {
case 'usb_permission_denied':
return l10n.usbErrorPermissionDenied;
case 'usb_device_missing':
case 'usb_device_detached':
return l10n.usbErrorDeviceMissing;
case 'usb_invalid_port':
return l10n.usbErrorInvalidPort;
case 'usb_busy':
return l10n.usbErrorBusy;
case 'usb_not_connected':
return l10n.usbErrorNotConnected;
case 'usb_open_failed':
case 'usb_driver_missing':
return l10n.usbErrorOpenFailed;
case 'usb_connect_failed':
return l10n.usbErrorConnectFailed;
}
}
if (error is UnsupportedError) {
return l10n.usbErrorUnsupported;
}
if (error is StateError) {
final msg = error.message;
if (msg.contains('already active')) return l10n.usbErrorAlreadyActive;
if (msg.contains('No USB serial device selected')) {
return l10n.usbErrorNoDeviceSelected;
}
if (msg.contains('not open') || msg.contains('closed')) {
return l10n.usbErrorPortClosed;
}
if (msg.contains('Timed out')) return l10n.usbErrorConnectTimedOut;
if (msg.contains('Failed to open')) return l10n.usbErrorOpenFailed;
}
if (error is TimeoutException) {
return l10n.usbErrorConnectTimedOut;
}
return error.toString();
}
}
+6 -1
View File
@@ -52,7 +52,12 @@ class AppDebugLogService extends ChangeNotifier {
String tag = 'App',
AppDebugLogLevel level = AppDebugLogLevel.info,
}) {
if (!_enabled) return;
if (!_enabled && !kDebugMode) return;
if (!_enabled) {
// In debug mode, still print to console but don't store entries.
debugPrint('[$tag] $message');
return;
}
_entries.add(
AppDebugLogEntry(
+24
View File
@@ -1,9 +1,11 @@
import 'dart:io' show Platform, File;
import 'dart:ui';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter/foundation.dart';
import '../l10n/app_localizations.dart';
import '../utils/platform_info.dart';
class NotificationService {
static final NotificationService _instance = NotificationService._internal();
@@ -63,14 +65,27 @@ class NotificationService {
appUserModelId: 'org.meshcore.open.app',
guid: 'e7ea8f85-72f5-4f36-91f6-038f740ccf86',
);
const linuxSettings = LinuxInitializationSettings(
defaultActionName: 'Open notification',
);
const initSettings = InitializationSettings(
android: androidSettings,
iOS: iosSettings,
macOS: macSettings,
windows: windowsSettings,
linux: linuxSettings,
);
// On Linux, the notifications plugin opens a D-Bus session bus
// connection whose async subscription can throw an unhandled
// SocketException when the bus socket is missing (e.g. running as
// root or inside a container without a session bus).
if (PlatformInfo.isLinux && !_isDbusSessionAvailable()) {
debugPrint('Skipping notification init: D-Bus session bus unavailable');
return;
}
try {
await _notifications.initialize(
settings: initSettings,
@@ -82,6 +97,15 @@ class NotificationService {
}
}
static bool _isDbusSessionAvailable() {
final addr = Platform.environment['DBUS_SESSION_BUS_ADDRESS'];
if (addr != null && addr.isNotEmpty) return true;
// Fallback: check the default socket for the current user.
final uid = Platform.environment['UID'] ?? Platform.environment['EUID'];
final path = '/run/user/${uid ?? '1000'}/bus';
return File(path).existsSync();
}
Future<bool> _ensureInitialized() async {
if (!_isInitialized) {
await initialize();
+111
View File
@@ -0,0 +1,111 @@
import 'dart:typed_data';
const int usbSerialTxFrameStart = 0x3c;
const int usbSerialRxFrameStart = 0x3e;
const int usbSerialHeaderLength = 3;
const int usbSerialMaxPayloadLength = 172;
Uint8List wrapUsbSerialTxFrame(Uint8List payload) {
if (payload.length > usbSerialMaxPayloadLength) {
throw ArgumentError.value(
payload.length,
'payload.length',
'USB serial payload exceeds $usbSerialMaxPayloadLength bytes',
);
}
final packet = Uint8List(usbSerialHeaderLength + payload.length);
packet[0] = usbSerialTxFrameStart;
packet[1] = payload.length & 0xff;
packet[2] = (payload.length >> 8) & 0xff;
packet.setRange(usbSerialHeaderLength, packet.length, payload);
return packet;
}
class UsbSerialDecodedPacket {
const UsbSerialDecodedPacket({
required this.frameStart,
required this.payload,
});
final int frameStart;
final Uint8List payload;
bool get isRxFrame => frameStart == usbSerialRxFrameStart;
}
class UsbSerialFrameDecoder {
final List<int> _rxBuffer = <int>[];
int _startIndex = 0;
void reset() {
_rxBuffer.clear();
_startIndex = 0;
}
List<UsbSerialDecodedPacket> ingest(Uint8List bytes) {
if (bytes.isEmpty) {
return const <UsbSerialDecodedPacket>[];
}
_rxBuffer.addAll(bytes);
final packets = <UsbSerialDecodedPacket>[];
while (true) {
if (_startIndex >= _rxBuffer.length) {
_rxBuffer.clear();
_startIndex = 0;
return packets;
}
if (_rxBuffer[_startIndex] != usbSerialRxFrameStart &&
_rxBuffer[_startIndex] != usbSerialTxFrameStart) {
_startIndex++;
_compactBufferIfNeeded();
continue;
}
final availableLength = _rxBuffer.length - _startIndex;
if (availableLength < usbSerialHeaderLength) {
_compactBufferIfNeeded(force: true);
return packets;
}
final payloadLength =
_rxBuffer[_startIndex + 1] | (_rxBuffer[_startIndex + 2] << 8);
if (payloadLength > usbSerialMaxPayloadLength) {
_startIndex++;
_compactBufferIfNeeded();
continue;
}
final packetLength = usbSerialHeaderLength + payloadLength;
if (availableLength < packetLength) {
_compactBufferIfNeeded(force: true);
return packets;
}
final frameStart = _rxBuffer[_startIndex];
final payload = Uint8List.fromList(
_rxBuffer.sublist(
_startIndex + usbSerialHeaderLength,
_startIndex + packetLength,
),
);
_startIndex += packetLength;
_compactBufferIfNeeded();
packets.add(
UsbSerialDecodedPacket(frameStart: frameStart, payload: payload),
);
}
}
void _compactBufferIfNeeded({bool force = false}) {
if (_startIndex == 0) {
return;
}
if (!force && _startIndex < 1024 && _startIndex < (_rxBuffer.length ~/ 2)) {
return;
}
_rxBuffer.removeRange(0, _startIndex);
_startIndex = 0;
}
}
+2
View File
@@ -0,0 +1,2 @@
export 'usb_serial_service_native.dart'
if (dart.library.js_interop) 'usb_serial_service_web.dart';
+463
View File
@@ -0,0 +1,463 @@
import 'dart:async';
import 'dart:io';
import 'package:flserial/flserial.dart';
import 'package:flserial/flserial_exception.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'app_debug_log_service.dart';
import '../utils/macos_usb_device_names.dart';
import '../utils/platform_info.dart';
import '../utils/usb_port_labels.dart';
import 'usb_serial_frame_codec.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',
);
final StreamController<Uint8List> _frameController =
StreamController<Uint8List>.broadcast();
final UsbSerialFrameDecoder _frameDecoder = UsbSerialFrameDecoder();
StreamSubscription<dynamic>? _androidDataSubscription;
StreamSubscription<FlSerialEventArgs>? _dataSubscription;
UsbSerialStatus _status = UsbSerialStatus.disconnected;
String? _connectedPortKey;
String? _connectedPortLabel;
FlSerial? _serial;
AppDebugLogService? _debugLogService;
UsbSerialStatus get status => _status;
String? get activePortKey => _connectedPortKey;
String? get activePortDisplayLabel =>
_connectedPortLabel ?? _connectedPortKey;
Stream<Uint8List> get frameStream => _frameController.stream;
bool get _useAndroidUsbHost =>
!kIsWeb && defaultTargetPlatform == TargetPlatform.android;
bool get _useDesktopFlSerial =>
PlatformInfo.isWindows || PlatformInfo.isLinux || PlatformInfo.isMacOS;
bool get _isSupportedPlatform => _useAndroidUsbHost || _useDesktopFlSerial;
// Always-fresh: do NOT use ??= here a cached FlSerial retains stale
// native handle state (flh) from a prior failed open, causing subsequent
// open attempts to fail with "port not exist" even when the device is present.
FlSerial _freshSerial() => FlSerial();
bool get isConnected {
if (!_isSupportedPlatform) {
return false;
}
// Trust _status as the authoritative connection state. Polling
// _serial?.isOpen() via the native FL_CTRL_IS_PORT_OPEN query is
// unreliable during the brief USB re-enumeration window that many
// microcontrollers (e.g. NRF52) trigger in response to DTR assertion.
// Actual port drops are handled by the onDone / onError callbacks on the
// serial data stream subscription, which update _status correctly.
return _status == UsbSerialStatus.connected;
}
Future<List<String>> listPorts() async {
if (!_isSupportedPlatform) {
return const <String>[];
}
if (_useAndroidUsbHost) {
final ports = await _androidMethodChannel.invokeListMethod<String>(
'listPorts',
);
return ports ?? <String>[];
}
final rawPorts = FlSerial.listPorts();
// On macOS, flserial's native device-name lookup is broken on macOS
// 10.15+ because the IOKit class name changed from IOUSBDevice to
// IOUSBHostDevice. We resolve names ourselves via ioreg and rewrite any
// "port - n/a" entries with the real product name.
if (Platform.isMacOS && rawPorts.isNotEmpty) {
return _annotateMacOsPorts(rawPorts);
}
return Future.value(rawPorts);
}
/// Rewrites the flserial port list on macOS by substituting real USB device
/// names (obtained via [ioreg]) for the "n/a" placeholders that flserial
/// returns when it can't find the deprecated IOUSBDevice parent.
Future<List<String>> _annotateMacOsPorts(List<String> rawPorts) async {
final deviceNames = await queryMacOsUsbDeviceNames();
if (deviceNames.isEmpty) return rawPorts;
return rawPorts.map((entry) {
// entry format from fl_ports: "port - description - hardware_id"
final port = normalizeUsbPortName(entry); // e.g. /dev/cu.usbmodem1101
final knownName = deviceNames[port]; // e.g. "Nordic NRF52 DK"
if (knownName == null) return entry; // non-USB port, keep as-is
// Replace description field only; preserve hardware_id for device
// identity (used by normalizeUsbPortName).
final segments = entry.split(' - ');
final hardwareId = segments.length >= 3 ? segments.last : 'n/a';
return '$port - $knownName - $hardwareId';
}).toList();
}
void setDebugLogService(AppDebugLogService? service) {
_debugLogService = service;
}
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');
}
if (!_isSupportedPlatform) {
throw UnsupportedError('USB serial is not supported on this platform.');
}
_status = UsbSerialStatus.connecting;
var normalizedPortName = normalizeUsbPortName(portName);
_frameDecoder.reset();
if (_useAndroidUsbHost) {
try {
await _androidMethodChannel.invokeMethod<void>('connect', {
'portName': normalizedPortName,
'baudRate': baudRate,
});
_debugLogService?.info(
'USB serial opened port=$normalizedPortName on Android via USB host bridge',
tag: 'USB Serial',
);
} on PlatformException catch (error) {
_status = UsbSerialStatus.disconnected;
final msg = error.message ?? error.code;
_debugLogService?.error(
'Android connect failed: $msg',
tag: 'USB Serial',
);
rethrow;
}
} else {
// Hot-restart guard
// On hot restart Dart tears down the isolate without calling dispose().
// The NativeCallable registered by flserial's setCallback() is
// isolate-local and gets freed when the isolate dies, but the native
// SerialThread is still alive and will call it crash.
//
// flserial uses process-global native state. Calling fl_free() kills ALL
// SerialThreads for every open port across all Dart isolates (there is
// only one in a Flutter app). Then fl_init() re-initialises the slot
// table so subsequent fl_open() calls work normally.
//
// This must happen before we register any new NativeCallable, so it must
// be the very first thing we do in the desktop branch.
try {
bindings.fl_free();
bindings.fl_init(16);
} catch (_) {}
// On macOS, flserial lists both cu.* and tty.* device nodes.
// When a cu.* open fails with FL_ERROR_PORT_NOT_EXIST, try the tty.*
// variant as a fallback (and vice-versa) before giving up.
final candidates = _buildPortCandidates(normalizedPortName);
FlSerialException? lastError;
bool opened = false;
for (final candidate in candidates) {
// Always create a fresh FlSerial instance a cached instance retains
// a stale flh handle from prior failed opens, which causes the native
// fl_open() to mis-route the request and report port-not-exist even
// when the device node is physically present.
final serial = _freshSerial();
serial.init();
try {
final openStatus = serial.openPort(candidate, baudRate);
if (openStatus != FlOpenStatus.open) {
final msg =
'Failed to open USB port $candidate (status: $openStatus)';
_debugLogService?.error(msg, tag: 'USB Serial');
// Not a FlSerialException treat as terminal failure
_status = UsbSerialStatus.disconnected;
throw StateError(msg);
}
serial.setByteSize8();
serial.setBitParityNone();
serial.setStopBits1();
serial.setFlowControlNone();
serial.setRTS(false);
serial.setDTR(true);
_serial = serial;
// Update the normalized port name to whichever candidate succeeded.
normalizedPortName = candidate;
_debugLogService?.info(
'USB serial opened port=$candidate cts=${serial.getCTS()} dsr=${serial.getDSR()} dtr=true rts=false',
tag: 'USB Serial',
);
opened = true;
break;
} on FlSerialException catch (error) {
// The native fl_open() already called fl_close() on failure
// internally, so no extra cleanup is needed here for this candidate.
_debugLogService?.warn(
'Failed to open $candidate: ${error.msg} (code ${error.error})',
tag: 'USB Serial',
);
lastError = error;
// Try next candidate
} catch (error, stackTrace) {
_status = UsbSerialStatus.disconnected;
_debugLogService?.error(
'Unexpected error opening $candidate: $error\n$stackTrace',
tag: 'USB Serial',
);
rethrow;
}
}
if (!opened) {
_status = UsbSerialStatus.disconnected;
final primary = candidates.first;
final msg = lastError != null
? 'Failed to open USB port $primary: ${lastError.msg} (code ${lastError.error})'
: 'Failed to open USB port $primary';
_debugLogService?.error(msg, tag: 'USB Serial');
throw StateError(msg);
}
}
_connectedPortKey = normalizedPortName;
_connectedPortLabel = 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 = wrapUsbSerialTxFrame(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;
final portLabel = _connectedPortLabel ?? _connectedPortKey;
_debugLogService?.info(
'USB disconnect starting port=${portLabel ?? 'unknown'}',
tag: 'USB Serial',
);
_status = UsbSerialStatus.disconnecting;
_connectedPortKey = null;
_connectedPortLabel = null;
_frameDecoder.reset();
if (_useAndroidUsbHost) {
await _androidDataSubscription?.cancel();
_androidDataSubscription = null;
try {
await _androidMethodChannel.invokeMethod<void>('disconnect');
} catch (_) {
// Ignore errors while closing.
}
} else {
// IMPORTANT: Close and free the native port FIRST, before cancelling the
// Dart subscription. The native SerialThread is blocked on a read(); once
// closePort() is called it unblocks and the thread exits. If we cancel
// the Dart subscription first (freeing the FFI callback pointer) and the
// thread fires one final callback before noticing the port is gone, Dart
// crashes with "Callback invoked after it has been deleted".
final serial = _serial;
_serial = null;
try {
if (serial?.isOpen() == FlOpenStatus.open) {
serial?.closePort();
}
} catch (_) {
// Ignore errors while closing.
}
// Note: we do NOT call free() here; that would globally reset native
// state for all ports. The global reset is done in connect() instead,
// before the next open, which is the safer place to do it.
// Now it is safe to cancel the Dart subscription the native thread has
// already seen the port close and will not fire any more callbacks.
await _dataSubscription?.cancel();
_dataSubscription = null;
}
_status = UsbSerialStatus.disconnected;
_debugLogService?.info(
'USB disconnect complete port=${portLabel ?? 'unknown'}',
tag: 'USB Serial',
);
}
void setRequestPortLabel(String label) {
// Native implementations do not use a synthetic chooser row.
}
void setFallbackDeviceName(String label) {
// Native implementations use OS-provided device names.
}
void updateConnectedLabel(String label) {
final trimmed = label.trim();
if (trimmed.isEmpty) {
return;
}
_connectedPortLabel = buildUsbDisplayLabel(
basePortLabel: _connectedPortKey ?? trimmed,
deviceName: trimmed,
);
}
void dispose() {
// Synchronously close the native port so the SerialThread exits before
// the Dart isolate is torn down (e.g. on hot restart). The async
// disconnect() path via unawaited() offers no ordering guarantee the
// isolate may die before the Future resolves, leaving the thread alive
// with a dangling NativeCallable pointer.
if (_useDesktopFlSerial) {
final serial = _serial;
try {
if (serial?.isOpen() == FlOpenStatus.open) {
serial?.closePort(); // synchronous C call kills the SerialThread
}
} catch (_) {}
}
// Kick off the full async teardown for anything else (subscription cancel,
// stream controller close). These are best-effort at dispose time.
unawaited(disconnect().whenComplete(_closeFrameController));
}
void _handleSerialData(FlSerialEventArgs event) {
try {
final bytes = event.serial.readList();
if (bytes.isNotEmpty) {
_ingestRawBytes(Uint8List.fromList(bytes));
}
} catch (error, stack) {
_addFrameError(error, stack);
}
}
void _handleAndroidData(dynamic data) {
if (data is Uint8List) {
_ingestRawBytes(data);
return;
}
if (data is ByteData) {
_ingestRawBytes(data.buffer.asUint8List());
return;
}
_addFrameError(
StateError('Unexpected Android USB event payload: ${data.runtimeType}'),
);
}
void _handleSerialError(Object error, [StackTrace? stackTrace]) {
_addFrameError(error, stackTrace);
}
void _handleSerialDone() {
unawaited(disconnect());
}
void _ingestRawBytes(Uint8List bytes) {
for (final packet in _frameDecoder.ingest(bytes)) {
if (!packet.isRxFrame) {
_debugLogService?.info(
'USB ignored packet start=0x${packet.frameStart.toRadixString(16).padLeft(2, '0')} len=${packet.payload.length}',
tag: 'USB Serial',
);
continue;
}
_addFrame(packet.payload);
}
}
void _addFrame(Uint8List payload) {
if (_frameController.isClosed) {
return;
}
_frameController.add(payload);
}
void _addFrameError(Object error, [StackTrace? stackTrace]) {
if (_frameController.isClosed) {
return;
}
_frameController.addError(error, stackTrace);
}
Future<void> _closeFrameController() async {
if (_frameController.isClosed) {
return;
}
await _frameController.close();
}
void _logFrameSummary(String prefix, Uint8List bytes) {
if (bytes.isEmpty) {
_debugLogService?.info('$prefix len=0', tag: 'USB Serial');
return;
}
_debugLogService?.info(
'$prefix code=${bytes[0]} len=${bytes.length}',
tag: 'USB Serial',
);
}
/// Returns an ordered list of port paths to try for [portName].
///
/// On macOS, USB serial devices appear as both `/dev/cu.*` (call-out, the
/// correct mode for outgoing serial connections) and `/dev/tty.*` (dial-in).
/// `flserial` may list one variant while only the other is actually openable
/// at a given moment. We prefer `cu.*` but automatically include the `tty.*`
/// sibling as a fallback, and vice-versa.
List<String> _buildPortCandidates(String normalizedPort) {
if (!Platform.isMacOS) return [normalizedPort];
const cuPrefix = '/dev/cu.';
const ttyPrefix = '/dev/tty.';
if (normalizedPort.startsWith(cuPrefix)) {
final suffix = normalizedPort.substring(cuPrefix.length);
return [normalizedPort, '$ttyPrefix$suffix'];
}
if (normalizedPort.startsWith(ttyPrefix)) {
final suffix = normalizedPort.substring(ttyPrefix.length);
return [normalizedPort, '$cuPrefix$suffix'];
}
return [normalizedPort];
}
}
enum UsbSerialStatus { disconnected, connecting, connected, disconnecting }
+580
View File
@@ -0,0 +1,580 @@
import 'dart:async';
import 'dart:js_interop';
import 'dart:js_interop_unsafe';
import 'package:flutter/foundation.dart';
import 'package:web/web.dart' as web;
import 'app_debug_log_service.dart';
import '../utils/usb_port_labels.dart';
import 'usb_serial_frame_codec.dart';
class UsbSerialService {
UsbSerialService();
static const Map<String, String> _knownUsbNames = <String, String>{
'2886:1667': 'Seeed Wio Tracker L1',
};
static final Map<String, String> _deviceNamesByPortKey = <String, String>{};
static final Map<String, String> _baseLabelsByPortKey = <String, String>{};
static final Map<String, JSObject> _authorizedPortsByKey =
<String, JSObject>{};
static int _nextAuthorizedPortId = 1;
final StreamController<Uint8List> _frameController =
StreamController<Uint8List>.broadcast();
final UsbSerialFrameDecoder _frameDecoder = UsbSerialFrameDecoder();
UsbSerialStatus _status = UsbSerialStatus.disconnected;
JSObject? _port;
JSObject? _reader;
JSObject? _writer;
String? _connectedPortName;
String? _connectedPortKey;
String _requestPortLabel = 'Choose USB Device';
String _fallbackDeviceName = 'Web Serial Device';
AppDebugLogService? _debugLogService;
UsbSerialStatus get status => _status;
String? get activePortKey => _connectedPortKey;
String? get activePortDisplayLabel => _connectedPortName ?? _connectedPortKey;
Stream<Uint8List> get frameStream => _frameController.stream;
bool get isConnected => _status == UsbSerialStatus.connected;
JSObject get _navigator => JSObject.fromInteropObject(web.window.navigator);
bool get _isSupported => _navigator.has('serial');
JSObject? get _serial {
if (!_isSupported) {
return null;
}
final serial = _navigator['serial'];
return serial == null ? null : serial as JSObject;
}
Future<List<String>> listPorts() async {
if (!_isSupported) {
return const <String>[];
}
_resetPortCache();
final ports = await _getAuthorizedPorts();
return <String>[_requestPortListEntry, ...ports.map(_listEntryForPort)];
}
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');
}
if (!_isSupported) {
throw UnsupportedError('Web Serial is not supported by this browser.');
}
_status = UsbSerialStatus.connecting;
_frameDecoder.reset();
try {
final requestedPortName = normalizeUsbPortName(portName);
_debugLogService?.info(
'Web connect: requested=$requestedPortName baud=$baudRate',
tag: 'USB Serial',
);
final selectedPortKey = requestedPortName.startsWith('web:port:')
? requestedPortName
: null;
_port = _authorizedPortsByKey[requestedPortName];
final authorizedPorts = await _getAuthorizedPorts();
_debugLogService?.info(
'Web connect: ${authorizedPorts.length} authorized port(s), cached=${_port != null}',
tag: 'USB Serial',
);
_port ??= _selectPort(authorizedPorts, requestedPortName);
_port ??= await _requestPort();
if (_port == null) {
throw StateError('No USB serial device selected');
}
_debugLogService?.info(
'Web connect: opening port at $baudRate baud…',
tag: 'USB Serial',
);
await _openPort(_port!, baudRate);
_connectedPortKey = _cachePort(_port!, preferredKey: selectedPortKey);
_connectedPortName = _displayLabelForPort(
_port!,
portKey: _connectedPortKey,
);
_writer = _getWriter(_port!);
_reader = _getReader(_port!);
_status = UsbSerialStatus.connected;
unawaited(_pumpReads());
_debugLogService?.info(
'USB serial opened port=$_connectedPortName via Web Serial',
tag: 'USB Serial',
);
} catch (error) {
_debugLogService?.error('Web connect failed: $error', tag: 'USB Serial');
await _cleanupFailedConnect();
_status = UsbSerialStatus.disconnected;
_connectedPortName = null;
_connectedPortKey = null;
rethrow;
}
}
Future<void> write(Uint8List data) async {
if (!isConnected || _writer == null) {
throw StateError('USB serial port is not open');
}
final packet = wrapUsbSerialTxFrame(data);
_logFrameSummary('USB TX frame', data);
final promise = _writer!.callMethod<JSPromise<JSAny?>>(
'write'.toJS,
packet.toJS,
);
await promise.toDart;
}
Future<void> disconnect() async {
if (_status == UsbSerialStatus.disconnected) return;
final portLabel = _connectedPortName ?? _connectedPortKey;
_debugLogService?.info(
'USB disconnect starting port=${portLabel ?? 'unknown'}',
tag: 'USB Serial',
);
_status = UsbSerialStatus.disconnecting;
final reader = _reader;
final writer = _writer;
final port = _port;
_reader = null;
_writer = null;
_port = null;
_connectedPortName = null;
_connectedPortKey = null;
_frameDecoder.reset();
if (reader != null) {
try {
await reader.callMethod<JSPromise<JSAny?>>('cancel'.toJS).toDart;
} catch (_) {
// Ignore errors while closing.
}
_releaseLock(reader);
}
if (writer != null) {
_releaseLock(writer);
}
if (port != null) {
try {
await port.callMethod<JSPromise<JSAny?>>('close'.toJS).toDart;
} catch (_) {
// Ignore errors while closing.
}
}
_status = UsbSerialStatus.disconnected;
_debugLogService?.info(
'USB disconnect complete port=${portLabel ?? 'unknown'}',
tag: 'USB Serial',
);
}
void updateConnectedLabel(String label) {
final trimmed = label.trim();
final portKey = _connectedPortKey;
if (trimmed.isEmpty || portKey == null) {
return;
}
_deviceNamesByPortKey[portKey] = trimmed;
_connectedPortName = _buildDisplayLabel(portKey);
}
void setRequestPortLabel(String label) {
final trimmed = label.trim();
if (trimmed.isEmpty) {
return;
}
_requestPortLabel = trimmed;
}
void setFallbackDeviceName(String label) {
final trimmed = label.trim();
if (trimmed.isEmpty) {
return;
}
_fallbackDeviceName = trimmed;
}
void setDebugLogService(AppDebugLogService? service) {
_debugLogService = service;
}
void dispose() {
unawaited(disconnect().whenComplete(_closeFrameController));
}
Future<List<JSObject>> _getAuthorizedPorts() async {
final serial = _serial;
if (serial == null) {
return const <JSObject>[];
}
final result = await serial
.callMethod<JSPromise<JSAny?>>('getPorts'.toJS)
.toDart;
return _toObjectList(result);
}
Future<JSObject?> _requestPort() async {
final serial = _serial;
if (serial == null) {
return null;
}
final result = await serial
.callMethod<JSPromise<JSAny?>>('requestPort'.toJS)
.toDart;
return result == null ? null : result as JSObject;
}
JSObject? _selectPort(List<JSObject> ports, String requestedPortName) {
if (ports.isEmpty) {
return null;
}
if (requestedPortName.isEmpty || requestedPortName == _requestPortKey) {
return ports.first;
}
if (requestedPortName.startsWith('web:port:')) {
return null;
}
for (final port in ports) {
final description = _describePort(port);
if (description == requestedPortName) {
return port;
}
}
return null;
}
Future<void> _openPort(JSObject port, int baudRate) async {
final options = JSObject()
..['baudRate'] = baudRate.toJS
..['flowControl'] = 'none'.toJS;
await port.callMethod<JSPromise<JSAny?>>('open'.toJS, options).toDart;
// Prevent ESP32 USB-CDC reset: hold DTR=true, RTS=false after open.
try {
final signals = JSObject()
..['dataTerminalReady'] = true.toJS
..['requestToSend'] = false.toJS;
await port
.callMethod<JSPromise<JSAny?>>('setSignals'.toJS, signals)
.toDart;
} catch (_) {
// setSignals may not be supported on all browsers/devices.
}
}
Future<void> _cleanupFailedConnect() async {
final reader = _reader;
final writer = _writer;
final port = _port;
_reader = null;
_writer = null;
_port = null;
if (reader != null) {
try {
await reader.callMethod<JSPromise<JSAny?>>('cancel'.toJS).toDart;
} catch (_) {
// Ignore cleanup errors after a failed connect.
}
_releaseLock(reader);
}
if (writer != null) {
_releaseLock(writer);
}
if (port != null) {
try {
await port.callMethod<JSPromise<JSAny?>>('close'.toJS).toDart;
} catch (_) {
// Ignore cleanup errors after a failed connect.
}
}
}
JSObject? _getReader(JSObject port) {
final readable = port.getProperty<JSAny?>('readable'.toJS);
if (readable == null) {
throw StateError('Web Serial port is not readable');
}
final readableObject = readable as JSObject;
return readableObject.callMethod<JSAny?>('getReader'.toJS) as JSObject;
}
JSObject? _getWriter(JSObject port) {
final writable = port.getProperty<JSAny?>('writable'.toJS);
if (writable == null) {
throw StateError('Web Serial port is not writable');
}
final writableObject = writable as JSObject;
return writableObject.callMethod<JSAny?>('getWriter'.toJS) as JSObject;
}
Future<void> _pumpReads() async {
final reader = _reader;
if (reader == null) {
_debugLogService?.warn('_pumpReads: reader is null', tag: 'USB Serial');
return;
}
_debugLogService?.info('_pumpReads: started', tag: 'USB Serial');
try {
while (_status == UsbSerialStatus.connected &&
identical(reader, _reader)) {
final result = await reader
.callMethod<JSPromise<JSAny?>>('read'.toJS)
.toDart;
if (result == null) {
_debugLogService?.warn('_pumpReads: null result', tag: 'USB Serial');
break;
}
final resultObject = result as JSObject;
final doneValue = resultObject.getProperty<JSAny?>('done'.toJS);
final done = doneValue != null && doneValue.dartify() == true;
if (done) {
_debugLogService?.info('_pumpReads: done=true', tag: 'USB Serial');
break;
}
final value = resultObject.getProperty<JSAny?>('value'.toJS);
final bytes = _coerceBytes(value);
if (bytes != null && bytes.isNotEmpty) {
_debugLogService?.info(
'USB RX raw: ${bytes.length} byte(s)',
tag: 'USB Serial',
);
_ingestRawBytes(bytes);
}
}
} catch (error, stackTrace) {
_debugLogService?.error('_pumpReads error: $error', tag: 'USB Serial');
if (_status == UsbSerialStatus.connected) {
_addFrameError(error, stackTrace);
}
} finally {
_debugLogService?.info('_pumpReads: ended', tag: 'USB Serial');
_releaseLock(reader);
if (_status == UsbSerialStatus.connected && identical(reader, _reader)) {
_addFrameError(StateError('USB serial connection closed'));
}
}
}
Uint8List? _coerceBytes(JSAny? value) {
if (value == null) return null;
try {
return (value as JSUint8Array).toDart;
} catch (_) {
// Fall back to array-like coercion below.
}
final object = value as JSObject;
if (object.has('length')) {
final lengthValue = object.getProperty<JSAny?>('length'.toJS)?.dartify();
if (lengthValue is num) {
final length = lengthValue.toInt();
final bytes = Uint8List(length);
for (var i = 0; i < length; i++) {
final item = object.getProperty<JSAny?>(i.toString().toJS)?.dartify();
if (item is num) {
bytes[i] = item.toInt();
}
}
return bytes;
}
}
return null;
}
List<JSObject> _toObjectList(JSAny? value) {
if (value == null) {
return const <JSObject>[];
}
final object = value as JSObject;
if (!object.has('length')) {
return const <JSObject>[];
}
final lengthValue = object.getProperty<JSAny?>('length'.toJS)?.dartify();
if (lengthValue is! num) {
return const <JSObject>[];
}
final length = lengthValue.toInt();
final items = <JSObject>[];
for (var i = 0; i < length; i++) {
final item = object.getProperty<JSAny?>(i.toString().toJS);
if (item != null) {
items.add(item as JSObject);
}
}
return items;
}
String _describePort(JSObject port) {
final info = _portInfo(port);
if (info == null) {
return _requestPortLabel;
}
final vendorId = info.usbVendorId;
final productId = info.usbProductId;
final hasVendor = vendorId != null;
final hasProduct = productId != null;
return describeWebUsbPort(
vendorId: hasVendor ? vendorId : null,
productId: hasProduct ? productId : null,
requestPortLabel: _requestPortLabel,
fallbackDeviceName: _fallbackDeviceName,
knownUsbNames: _knownUsbNames,
);
}
_WebPortInfo? _portInfo(JSObject port) {
try {
final info = port.callMethod<JSAny?>('getInfo'.toJS);
if (info == null) {
return null;
}
final infoObject = info as JSObject;
final vendorId = infoObject
.getProperty<JSAny?>('usbVendorId'.toJS)
?.dartify();
final productId = infoObject
.getProperty<JSAny?>('usbProductId'.toJS)
?.dartify();
return _WebPortInfo(
usbVendorId: vendorId is num ? vendorId.toInt() : null,
usbProductId: productId is num ? productId.toInt() : null,
);
} catch (_) {
return null;
}
}
String _portKeyFor(JSObject port) {
return _cachePort(port);
}
String _cachePort(JSObject port, {String? preferredKey}) {
final portKey = preferredKey ?? 'web:port:${_nextAuthorizedPortId++}';
_baseLabelsByPortKey[portKey] = _describePort(port);
_authorizedPortsByKey[portKey] = port;
return portKey;
}
String _displayLabelForPort(JSObject port, {String? portKey}) =>
_buildDisplayLabel(portKey ?? _portKeyFor(port));
String _buildDisplayLabel(String portKey) {
return buildUsbDisplayLabel(
basePortLabel: _baseLabelsByPortKey[portKey] ?? portKey,
deviceName: _deviceNamesByPortKey[portKey],
);
}
String _listEntryForPort(JSObject port) {
final portKey = _portKeyFor(port);
return '$portKey - ${_displayLabelForPort(port, portKey: portKey)}';
}
String get _requestPortKey => 'web:request';
String get _requestPortListEntry => '$_requestPortKey - $_requestPortLabel';
void _resetPortCache() {
_authorizedPortsByKey.clear();
_baseLabelsByPortKey.clear();
_deviceNamesByPortKey.clear();
_nextAuthorizedPortId = 1;
}
void _releaseLock(JSObject resource) {
try {
resource.callMethod<JSAny?>('releaseLock'.toJS);
} catch (_) {
// Ignore lock release failures.
}
}
void _ingestRawBytes(Uint8List bytes) {
for (final packet in _frameDecoder.ingest(bytes)) {
if (!packet.isRxFrame) {
_debugLogService?.info(
'USB ignored packet start=0x${packet.frameStart.toRadixString(16).padLeft(2, '0')} len=${packet.payload.length}',
tag: 'USB Serial',
);
continue;
}
_addFrame(packet.payload);
}
}
void _addFrame(Uint8List payload) {
if (_frameController.isClosed) {
return;
}
_frameController.add(payload);
}
void _addFrameError(Object error, [StackTrace? stackTrace]) {
if (_frameController.isClosed) {
return;
}
_frameController.addError(error, stackTrace);
}
Future<void> _closeFrameController() async {
if (_frameController.isClosed) {
return;
}
await _frameController.close();
}
void _logFrameSummary(String prefix, Uint8List bytes) {
if (bytes.isEmpty) {
_debugLogService?.info('$prefix len=0', tag: 'USB Serial');
return;
}
_debugLogService?.info(
'$prefix code=${bytes[0]} len=${bytes.length}',
tag: 'USB Serial',
);
}
}
enum UsbSerialStatus { disconnected, connecting, connected, disconnecting }
final class _WebPortInfo {
const _WebPortInfo({required this.usbVendorId, required this.usbProductId});
final int? usbVendorId;
final int? usbProductId;
}
+44 -7
View File
@@ -1,5 +1,7 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:meshcore_open/utils/app_logger.dart';
import '../models/channel_message.dart';
import '../helpers/smaz.dart';
import 'prefs_manager.dart';
@@ -7,13 +9,25 @@ import 'prefs_manager.dart';
class ChannelMessageStore {
static const String _keyPrefix = 'channel_messages_';
String publicKeyHex = '';
set setPublicKeyHex(String value) =>
publicKeyHex = value.length > 10 ? value.substring(0, 10) : '';
String get keyFor => '$_keyPrefix$publicKeyHex';
/// Save messages for a specific channel
Future<void> saveChannelMessages(
int channelIndex,
List<ChannelMessage> messages,
) async {
if (publicKeyHex.isEmpty) {
appLogger.warn(
'Public key hex is not set. Cannot save channel messages.',
);
return;
}
final prefs = PrefsManager.instance;
final key = '$_keyPrefix$channelIndex';
final key = '$keyFor$channelIndex';
// Convert messages to JSON
final jsonList = messages.map((msg) => _messageToJson(msg)).toList();
@@ -24,12 +38,35 @@ class ChannelMessageStore {
/// Load messages for a specific channel
Future<List<ChannelMessage>> loadChannelMessages(int channelIndex) async {
if (publicKeyHex.isEmpty) {
appLogger.warn(
'Public key hex is not set. Cannot load channel messages.',
);
return [];
}
final prefs = PrefsManager.instance;
final key = '$_keyPrefix$channelIndex';
final jsonString = prefs.getString(key);
if (jsonString == null) return [];
final key = '$keyFor$channelIndex';
final oldKey = '$_keyPrefix$channelIndex';
String? jsonString = prefs.getString(key);
if (jsonString == null || jsonString.isEmpty) {
// Attempt migration from legacy unscoped key on first load
final legacyJsonString = prefs.getString(oldKey);
prefs.remove(oldKey);
if (legacyJsonString != null && legacyJsonString.isNotEmpty) {
appLogger.info(
'Migrating channel messages from legacy key $oldKey to scoped key $key',
);
await prefs.setString(key, legacyJsonString);
jsonString = legacyJsonString;
}
}
if (jsonString == null || jsonString.isEmpty) {
jsonString = prefs.getString(keyFor);
}
if (jsonString == null || jsonString.isEmpty) {
return [];
}
try {
final jsonList = jsonDecode(jsonString) as List<dynamic>;
return jsonList.map((json) => _messageFromJson(json)).toList();
@@ -42,14 +79,14 @@ class ChannelMessageStore {
/// Clear messages for a specific channel
Future<void> clearChannelMessages(int channelIndex) async {
final prefs = PrefsManager.instance;
final key = '$_keyPrefix$channelIndex';
final key = '$keyFor$channelIndex';
await prefs.remove(key);
}
/// Clear all channel messages
Future<void> clearAllChannelMessages() async {
final prefs = PrefsManager.instance;
final keys = prefs.getKeys().where((k) => k.startsWith(_keyPrefix));
final keys = prefs.getKeys().where((k) => k.startsWith(keyFor));
for (var key in keys) {
await prefs.remove(key);
}
+36 -6
View File
@@ -1,20 +1,50 @@
import 'dart:convert';
import '../utils/app_logger.dart';
import 'prefs_manager.dart';
class ChannelOrderStore {
static const String _key = 'channel_order';
static const String _keyPrefix = 'channel_order_';
String publicKeyHex = '';
set setPublicKeyHex(String value) =>
publicKeyHex = value.length > 10 ? value.substring(0, 10) : '';
String get keyFor => '$_keyPrefix$publicKeyHex';
Future<void> saveChannelOrder(List<int> order) async {
if (publicKeyHex.isEmpty) {
appLogger.warn('Public key hex is not set. Cannot save channel order.');
return;
}
final prefs = PrefsManager.instance;
await prefs.setString(_key, jsonEncode(order));
await prefs.setString(keyFor, jsonEncode(order));
}
Future<List<int>> loadChannelOrder() async {
if (publicKeyHex.isEmpty) {
appLogger.warn('Public key hex is not set. Cannot load channel order.');
return [];
}
final prefs = PrefsManager.instance;
final raw = prefs.getString(_key);
if (raw == null || raw.isEmpty) return [];
String? jsonString = prefs.getString(keyFor);
if (jsonString == null || jsonString.isEmpty) {
// Attempt migration from legacy unscoped key on first load
final legacyJsonString = prefs.getString(_keyPrefix);
prefs.remove(_keyPrefix);
if (legacyJsonString != null && legacyJsonString.isNotEmpty) {
appLogger.info(
'Migrating channel order from legacy key $_keyPrefix to scoped key $keyFor',
);
await prefs.setString(keyFor, legacyJsonString);
jsonString = legacyJsonString;
}
}
if (jsonString == null || jsonString.isEmpty) {
return [];
}
try {
final decoded = jsonDecode(raw);
final decoded = jsonDecode(jsonString);
if (decoded is List) {
return decoded
.map((value) => value is int ? value : int.tryParse('$value'))
@@ -24,7 +54,7 @@ class ChannelOrderStore {
} catch (_) {
// fall through to legacy parse
}
return raw
return jsonString
.split(',')
.map((value) => int.tryParse(value))
.whereType<int>()
+35 -3
View File
@@ -1,17 +1,49 @@
import '../utils/app_logger.dart';
import 'prefs_manager.dart';
class ChannelSettingsStore {
static const String _smazKeyPrefix = 'channel_smaz_';
static const String _keyPrefix = 'channel_smaz_';
String publicKeyHex = '';
set setPublicKeyHex(String value) =>
publicKeyHex = value.length > 10 ? value.substring(0, 10) : '';
String get keyFor => '$_keyPrefix$publicKeyHex';
Future<bool> loadSmazEnabled(int channelIndex) async {
if (publicKeyHex.isEmpty) {
appLogger.warn(
'Public key hex is not set. Cannot load channel settings.',
);
return false;
}
final prefs = PrefsManager.instance;
final key = '$_smazKeyPrefix$channelIndex';
final key = '$keyFor$channelIndex';
final oldKey = '$_keyPrefix$channelIndex';
bool? enabled = prefs.getBool(oldKey);
if (enabled == null) {
// Attempt migration from legacy unscoped key on first load
enabled = prefs.getBool(oldKey);
prefs.remove(oldKey);
if (enabled != null) {
appLogger.info(
'Migrating channel settings from legacy key $oldKey to scoped key $key',
);
await prefs.setBool(key, enabled);
}
}
return prefs.getBool(key) ?? false;
}
Future<void> saveSmazEnabled(int channelIndex, bool enabled) async {
if (publicKeyHex.isEmpty) {
appLogger.warn(
'Public key hex is not set. Cannot save channel settings.',
);
return;
}
final prefs = PrefsManager.instance;
final key = '$_smazKeyPrefix$channelIndex';
final key = '$keyFor$channelIndex';
await prefs.setBool(key, enabled);
}
}
+34 -5
View File
@@ -2,18 +2,43 @@ import 'dart:convert';
import 'dart:typed_data';
import '../models/channel.dart';
import '../utils/app_logger.dart';
import 'prefs_manager.dart';
class ChannelStore {
static const String _key = 'channels';
static const String _keyPrefix = 'channels';
String publicKeyHex = '';
set setPublicKeyHex(String value) =>
publicKeyHex = value.length >= 10 ? value.substring(0, 10) : '';
String get keyFor => '$_keyPrefix$publicKeyHex';
Future<List<Channel>> loadChannels() async {
if (publicKeyHex.isEmpty) {
appLogger.warn('Public key hex is not set. Cannot load channels.');
return [];
}
final prefs = PrefsManager.instance;
final jsonStr = prefs.getString(_key);
if (jsonStr == null) return [];
String? jsonString = prefs.getString(keyFor);
if (jsonString == null || jsonString.isEmpty) {
// Attempt migration from legacy unscoped key on first load
final legacyJsonString = prefs.getString(_keyPrefix);
prefs.remove(_keyPrefix);
if (legacyJsonString != null && legacyJsonString.isNotEmpty) {
appLogger.info(
'Migrating channel messages from legacy key $_keyPrefix to scoped key $keyFor',
);
await prefs.setString(keyFor, legacyJsonString);
jsonString = legacyJsonString;
}
}
if (jsonString == null || jsonString.isEmpty) {
return [];
}
try {
final jsonList = jsonDecode(jsonStr) as List<dynamic>;
final jsonList = jsonDecode(jsonString) as List<dynamic>;
return jsonList
.map((entry) => _fromJson(entry as Map<String, dynamic>))
.toList();
@@ -23,9 +48,13 @@ class ChannelStore {
}
Future<void> saveChannels(List<Channel> channels) async {
if (publicKeyHex.isEmpty) {
appLogger.warn('Public key hex is not set. Cannot save channels.');
return;
}
final prefs = PrefsManager.instance;
final jsonList = channels.map(_toJson).toList();
await prefs.setString(_key, jsonEncode(jsonList));
await prefs.setString(keyFor, jsonEncode(jsonList));
}
Map<String, dynamic> _toJson(Channel channel) {
+34 -3
View File
@@ -1,6 +1,7 @@
import 'dart:convert';
import '../models/community.dart';
import '../utils/app_logger.dart';
import 'prefs_manager.dart';
/// Persists communities to local storage using SharedPreferences.
@@ -9,12 +10,38 @@ import 'prefs_manager.dart';
/// Each community contains its secret K, so this data should
/// be considered sensitive (though device encryption handles security).
class CommunityStore {
static const String _communitiesKey = 'communities_v1';
static const String _keyPrefix = 'communities_v1';
String publicKeyHex = '';
set setPublicKeyHex(String value) =>
publicKeyHex = value.length > 10 ? value.substring(0, 10) : '';
String get keyFor => '$_keyPrefix$publicKeyHex';
/// Load all communities from storage
Future<List<Community>> loadCommunities() async {
if (publicKeyHex.isEmpty) {
appLogger.warn('Public key hex is not set. Cannot load communities.');
return [];
}
final prefs = PrefsManager.instance;
final jsonString = prefs.getString(_communitiesKey);
String? jsonString = prefs.getString(keyFor);
if (jsonString == null || jsonString.isEmpty) {
// Attempt migration from legacy unscoped key on first load
final legacyJsonString = prefs.getString(_keyPrefix);
prefs.remove(_keyPrefix);
if (legacyJsonString != null && legacyJsonString.isNotEmpty) {
appLogger.info(
'Migrating communities from legacy key $_keyPrefix to scoped key $keyFor',
);
await prefs.setString(keyFor, legacyJsonString);
jsonString = legacyJsonString;
}
}
if (jsonString == null || jsonString.isEmpty) {
jsonString = prefs.getString(keyFor);
}
if (jsonString == null || jsonString.isEmpty) {
return [];
}
@@ -32,9 +59,13 @@ class CommunityStore {
/// Save all communities to storage
Future<void> saveCommunities(List<Community> communities) async {
if (publicKeyHex.isEmpty) {
appLogger.warn('Public key hex is not set. Cannot save communities.');
return;
}
final prefs = PrefsManager.instance;
final jsonList = communities.map((c) => c.toJson()).toList();
await prefs.setString(_communitiesKey, jsonEncode(jsonList));
await prefs.setString(keyFor, jsonEncode(jsonList));
}
/// Add a new community
+3 -3
View File
@@ -5,11 +5,11 @@ import '../models/discovery_contact.dart';
import 'prefs_manager.dart';
class ContactDiscoveryStore {
static const String _key = 'discovered_contacts';
static const String _keyPrefix = 'discovered_contacts';
Future<List<DiscoveryContact>> loadContacts() async {
final prefs = PrefsManager.instance;
final jsonStr = prefs.getString(_key);
final jsonStr = prefs.getString(_keyPrefix);
if (jsonStr == null) return [];
try {
@@ -25,7 +25,7 @@ class ContactDiscoveryStore {
Future<void> saveContacts(List<DiscoveryContact> contacts) async {
final prefs = PrefsManager.instance;
final jsonList = contacts.map(_toJson).toList();
await prefs.setString(_key, jsonEncode(jsonList));
await prefs.setString(_keyPrefix, jsonEncode(jsonList));
}
Map<String, dynamic> _toJson(DiscoveryContact contact) {
+37 -5
View File
@@ -1,17 +1,45 @@
import 'dart:convert';
import '../models/contact_group.dart';
import '../utils/app_logger.dart';
import 'prefs_manager.dart';
class ContactGroupStore {
static const String _key = 'contact_groups';
static const String _keyPrefix = 'contact_groups';
String publicKeyHex = '';
set setPublicKeyHex(String value) =>
publicKeyHex = value.length > 10 ? value.substring(0, 10) : '';
String get keyFor => '$_keyPrefix$publicKeyHex';
Future<List<ContactGroup>> loadGroups() async {
if (publicKeyHex.isEmpty) {
appLogger.warn('Public key hex is not set. Cannot load contact groups.');
return [];
}
final prefs = PrefsManager.instance;
final raw = prefs.getString(_key);
if (raw == null || raw.isEmpty) return [];
String? jsonString = prefs.getString(keyFor);
if (jsonString == null || jsonString.isEmpty) {
// Attempt migration from legacy unscoped key on first load
final legacyJsonString = prefs.getString(_keyPrefix);
prefs.remove(_keyPrefix);
if (legacyJsonString != null && legacyJsonString.isNotEmpty) {
appLogger.info(
'Migrating channel messages from legacy key $_keyPrefix to scoped key $keyFor',
);
await prefs.setString(keyFor, legacyJsonString);
jsonString = legacyJsonString;
}
}
if (jsonString == null || jsonString.isEmpty) {
jsonString = prefs.getString(keyFor);
}
if (jsonString == null || jsonString.isEmpty) {
return [];
}
try {
final decoded = jsonDecode(raw);
final decoded = jsonDecode(jsonString);
if (decoded is List) {
return decoded
.whereType<Map<String, dynamic>>()
@@ -25,8 +53,12 @@ class ContactGroupStore {
}
Future<void> saveGroups(List<ContactGroup> groups) async {
if (publicKeyHex.isEmpty) {
appLogger.warn('Public key hex is not set. Cannot save contact groups.');
return;
}
final prefs = PrefsManager.instance;
final encoded = jsonEncode(groups.map((group) => group.toJson()).toList());
await prefs.setString(_key, encoded);
await prefs.setString(keyFor, encoded);
}
}
+35 -3
View File
@@ -1,17 +1,49 @@
import '../utils/app_logger.dart';
import 'prefs_manager.dart';
class ContactSettingsStore {
static const String _smazKeyPrefix = 'contact_smaz_';
static const String _keyPrefix = 'contact_smaz_';
String publicKeyHex = '';
set setPublicKeyHex(String value) =>
publicKeyHex = value.length > 10 ? value.substring(0, 10) : '';
String get keyFor => '$_keyPrefix$publicKeyHex';
Future<bool> loadSmazEnabled(String contactKeyHex) async {
if (publicKeyHex.isEmpty) {
appLogger.warn(
'Public key hex is not set. Cannot load contact settings.',
);
return false;
}
final prefs = PrefsManager.instance;
final key = '$_smazKeyPrefix$contactKeyHex';
final key = '$keyFor$contactKeyHex';
final oldKey = '$_keyPrefix$contactKeyHex';
bool? enabled = prefs.getBool(key);
if (enabled == null) {
// Attempt migration from legacy unscoped key on first load
enabled = prefs.getBool(oldKey);
prefs.remove(oldKey);
if (enabled != null) {
appLogger.info(
'Migrating contact settings from legacy key $oldKey to scoped key $key',
);
await prefs.setBool(key, enabled);
}
}
return prefs.getBool(key) ?? false;
}
Future<void> saveSmazEnabled(String contactKeyHex, bool enabled) async {
if (publicKeyHex.isEmpty) {
appLogger.warn(
'Public key hex is not set. Cannot save contact settings.',
);
return;
}
final prefs = PrefsManager.instance;
final key = '$_smazKeyPrefix$contactKeyHex';
final key = '$keyFor$contactKeyHex';
await prefs.setBool(key, enabled);
}
}
+37 -5
View File
@@ -2,18 +2,46 @@ import 'dart:convert';
import 'dart:typed_data';
import '../models/contact.dart';
import '../utils/app_logger.dart';
import 'prefs_manager.dart';
class ContactStore {
static const String _key = 'contacts';
static const String _keyPrefix = 'contacts';
String publicKeyHex = '';
set setPublicKeyHex(String value) =>
publicKeyHex = value.length > 10 ? value.substring(0, 10) : '';
String get keyFor => '$_keyPrefix$publicKeyHex';
Future<List<Contact>> loadContacts() async {
if (publicKeyHex.isEmpty) {
appLogger.warn('Public key hex is not set. Cannot load contacts.');
return [];
}
final prefs = PrefsManager.instance;
final jsonStr = prefs.getString(_key);
if (jsonStr == null) return [];
String? jsonString = prefs.getString(keyFor);
if (jsonString == null || jsonString.isEmpty) {
// Attempt migration from legacy unscoped key on first load
final legacyJsonString = prefs.getString(_keyPrefix);
prefs.remove(_keyPrefix);
if (legacyJsonString != null && legacyJsonString.isNotEmpty) {
appLogger.info(
'Migrating contacts from legacy key $_keyPrefix to scoped key $keyFor',
);
await prefs.setString(keyFor, legacyJsonString);
jsonString = legacyJsonString;
}
}
if (jsonString == null || jsonString.isEmpty) {
jsonString = prefs.getString(keyFor);
}
if (jsonString == null || jsonString.isEmpty) {
return [];
}
try {
final jsonList = jsonDecode(jsonStr) as List<dynamic>;
final jsonList = jsonDecode(jsonString) as List<dynamic>;
return jsonList
.map((entry) => _fromJson(entry as Map<String, dynamic>))
.toList();
@@ -23,9 +51,13 @@ class ContactStore {
}
Future<void> saveContacts(List<Contact> contacts) async {
if (publicKeyHex.isEmpty) {
appLogger.warn('Public key hex is not set. Cannot save contacts.');
return;
}
final prefs = PrefsManager.instance;
final jsonList = contacts.map(_toJson).toList();
await prefs.setString(_key, jsonEncode(jsonList));
await prefs.setString(keyFor, jsonEncode(jsonList));
}
Map<String, dynamic> _toJson(Contact contact) {
+43 -5
View File
@@ -2,26 +2,60 @@ import 'dart:convert';
import 'dart:typed_data';
import '../models/message.dart';
import '../helpers/smaz.dart';
import '../utils/app_logger.dart';
import 'prefs_manager.dart';
class MessageStore {
static const String _keyPrefix = 'messages_';
String publicKeyHex = '';
set setPublicKeyHex(String value) =>
publicKeyHex = value.length > 10 ? value.substring(0, 10) : '';
String get keyFor => '$_keyPrefix$publicKeyHex';
Future<void> saveMessages(
String contactKeyHex,
List<Message> messages,
) async {
if (publicKeyHex.isEmpty) {
appLogger.warn('Public key hex is not set. Cannot save messages.');
return;
}
final prefs = PrefsManager.instance;
final key = '$_keyPrefix$contactKeyHex';
final key = '$keyFor$contactKeyHex';
final jsonList = messages.map(_messageToJson).toList();
await prefs.setString(key, jsonEncode(jsonList));
}
Future<List<Message>> loadMessages(String contactKeyHex) async {
if (publicKeyHex.isEmpty) {
appLogger.warn('Public key hex is not set. Cannot load messages.');
return [];
}
final prefs = PrefsManager.instance;
final key = '$_keyPrefix$contactKeyHex';
final jsonString = prefs.getString(key);
if (jsonString == null) return [];
final key = '$keyFor$contactKeyHex';
final oldKey = '$_keyPrefix$contactKeyHex';
String? jsonString = prefs.getString(key);
if (jsonString == null || jsonString.isEmpty) {
// Attempt migration from legacy unscoped key on first load
final legacyJsonString = prefs.getString(oldKey);
prefs.remove(oldKey);
if (legacyJsonString != null && legacyJsonString.isNotEmpty) {
appLogger.info(
'Migrating messages from legacy key $oldKey to scoped key $key',
);
await prefs.setString(key, legacyJsonString);
jsonString = legacyJsonString;
}
}
if (jsonString == null || jsonString.isEmpty) {
jsonString = prefs.getString(keyFor);
}
if (jsonString == null || jsonString.isEmpty) {
return [];
}
try {
final jsonList = jsonDecode(jsonString) as List<dynamic>;
@@ -32,8 +66,12 @@ class MessageStore {
}
Future<void> clearMessages(String contactKeyHex) async {
if (publicKeyHex.isEmpty) {
appLogger.warn('Public key hex is not set. Cannot clear messages.');
return;
}
final prefs = PrefsManager.instance;
final key = '$_keyPrefix$contactKeyHex';
final key = '$keyFor$contactKeyHex';
await prefs.remove(key);
}
+37 -5
View File
@@ -1,11 +1,18 @@
import 'dart:async';
import 'dart:convert';
import '../utils/app_logger.dart';
import 'prefs_manager.dart';
/// Storage for unread message tracking with debounced writes to reduce I/O.
class UnreadStore {
static const String _contactUnreadCountKey = 'contact_unread_count';
static const String _keyPrefix = 'contact_unread_count';
String publicKeyHex = '';
set setPublicKeyHex(String value) =>
publicKeyHex = value.length >= 10 ? value.substring(0, 10) : '';
String get keyFor => '$_keyPrefix$publicKeyHex';
// Debounce timers to batch rapid writes
Timer? _contactUnreadSaveTimer;
@@ -20,12 +27,33 @@ class UnreadStore {
}
Future<Map<String, int>> loadContactUnreadCount() async {
if (publicKeyHex.isEmpty) {
appLogger.warn('Public key hex is not set. Cannot load unread counts.');
return {};
}
final prefs = PrefsManager.instance;
final jsonStr = prefs.getString(_contactUnreadCountKey);
if (jsonStr == null) return {};
String? jsonString = prefs.getString(keyFor);
if (jsonString == null || jsonString.isEmpty) {
// Attempt migration from legacy unscoped key on first load
final legacyJsonString = prefs.getString(_keyPrefix);
prefs.remove(_keyPrefix);
if (legacyJsonString != null && legacyJsonString.isNotEmpty) {
appLogger.info(
'Migrating channel messages from legacy key $_keyPrefix to scoped key $keyFor',
);
await prefs.setString(keyFor, legacyJsonString);
jsonString = legacyJsonString;
}
}
if (jsonString == null || jsonString.isEmpty) {
jsonString = prefs.getString(keyFor);
}
if (jsonString == null || jsonString.isEmpty) {
return {};
}
try {
final json = jsonDecode(jsonStr) as Map<String, dynamic>;
final json = jsonDecode(jsonString) as Map<String, dynamic>;
return json.map((key, value) => MapEntry(key, value as int));
} catch (_) {
return {};
@@ -33,6 +61,10 @@ class UnreadStore {
}
void saveContactUnreadCount(Map<String, int> counts) {
if (publicKeyHex.isEmpty) {
appLogger.warn('Public key hex is not set. Cannot save unread counts.');
return;
}
_pendingContactUnreadCount = counts;
_contactUnreadSaveTimer?.cancel();
@@ -49,7 +81,7 @@ class UnreadStore {
final prefs = PrefsManager.instance;
final jsonStr = jsonEncode(_pendingContactUnreadCount);
await prefs.setString(_contactUnreadCountKey, jsonStr);
await prefs.setString(keyFor, jsonStr);
_pendingContactUnreadCount = null;
}
+2
View File
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import '../connector/meshcore_connector.dart';
import '../l10n/l10n.dart';
import 'app_logger.dart';
/// Shows a confirmation dialog before disconnecting from the device.
/// Returns true if user confirmed and disconnect completed, false otherwise.
@@ -28,6 +29,7 @@ Future<bool> showDisconnectDialog(
);
if (confirmed == true) {
appLogger.info('Disconnect confirmed from popup', tag: 'Connection');
await connector.disconnect();
return true;
}
+92
View File
@@ -0,0 +1,92 @@
import 'dart:io';
/// Queries the macOS IOKit registry via [ioreg] to build a map of serial port
/// callout device paths to human-readable USB device names.
///
/// The [flserial] native library uses the deprecated [IOUSBDevice] IOKit class
/// to resolve device names, but macOS 10.15+ renamed it to [IOUSBHostDevice].
/// As a result flserial always returns "n/a" for USB product/vendor info on
/// modern macOS. This utility bypasses that limitation by invoking ioreg
/// directly and parsing its output.
///
/// Returns a Map of e.g. `"/dev/cu.usbmodem1101"` `"Nordic NRF52 DK"`.
/// Devices without a USB product name are not included in the map.
Future<Map<String, String>> queryMacOsUsbDeviceNames() async {
assert(Platform.isMacOS);
try {
final result = await Process.run('ioreg', [
'-r',
'-c',
'IOUSBHostDevice',
'-l',
], stdoutEncoding: const SystemEncoding());
if (result.exitCode != 0) return const <String, String>{};
return _parseIoregOutput(result.stdout as String);
} catch (_) {
return const <String, String>{};
}
}
Map<String, String> _parseIoregOutput(String output) {
final lines = output.split('\n');
final result = <String, String>{};
// We accumulate the current device block's properties.
// A new block starts at a line beginning with "+-o " which indicates a
// top-level IOUSBHostDevice entry in the ioreg tree.
String? currentVendor;
String? currentProduct;
final List<String> currentPorts = <String>[];
void flushBlock() {
if (currentPorts.isNotEmpty &&
(currentVendor != null || currentProduct != null)) {
final parts = <String>[
if (currentVendor != null && currentVendor!.isNotEmpty) currentVendor!,
if (currentProduct != null && currentProduct!.isNotEmpty)
currentProduct!,
];
final name = parts.join(' ');
for (final port in currentPorts) {
result[port] = name;
}
}
currentVendor = null;
currentProduct = null;
currentPorts.clear();
}
for (final line in lines) {
// A new top-level device block begins here.
if (line.startsWith('+-o ')) {
flushBlock();
continue;
}
// USB Product Name (appears at multiple depths in the tree, first wins)
final productMatch = _kProductName.firstMatch(line);
if (productMatch != null && currentProduct == null) {
currentProduct = productMatch.group(1)?.trim();
continue;
}
// USB Vendor Name
final vendorMatch = _kVendorName.firstMatch(line);
if (vendorMatch != null && currentVendor == null) {
currentVendor = vendorMatch.group(1)?.trim();
continue;
}
// IOCalloutDevice the /dev/cu.xxx path our app uses
final calloutMatch = _kCalloutDevice.firstMatch(line);
if (calloutMatch != null) {
final port = calloutMatch.group(1)?.trim();
if (port != null && port.isNotEmpty) {
currentPorts.add(port);
}
}
}
flushBlock();
return result;
}
final RegExp _kProductName = RegExp(r'"USB Product Name" = "([^"]*)"');
final RegExp _kVendorName = RegExp(r'"USB Vendor Name" = "([^"]*)"');
final RegExp _kCalloutDevice = RegExp(r'"IOCalloutDevice" = "([^"]*)"');
+11
View File
@@ -33,4 +33,15 @@ class PlatformInfo {
/// Whether the app is running on a desktop platform (macOS, Windows, or Linux).
static bool get isDesktop => isMacOS || isWindows || isLinux;
/// Whether the current platform supports a native USB serial backend.
static bool get supportsNativeUsbSerial =>
isAndroid || isWindows || isLinux || isMacOS;
/// Whether the current browser supports the Web Serial backend.
static bool get supportsWebSerial => isWeb && isChrome;
/// Whether USB serial is expected to be available on the current platform.
static bool get supportsUsbSerial =>
supportsNativeUsbSerial || supportsWebSerial;
}
+66
View File
@@ -0,0 +1,66 @@
String normalizeUsbPortName(String portLabel) {
final separatorIndex = portLabel.indexOf(' - ');
final normalized = separatorIndex >= 0
? portLabel.substring(0, separatorIndex)
: portLabel;
return normalized.trim();
}
/// Returns a human-readable name for a serial port label.
///
/// The native flserial library encodes port info as a ` - `-separated string:
/// `"<port> - <description> - <hardware_id>"`
///
/// This function extracts the *description* field (index 1) and discards the
/// raw hardware_id, which is not user-friendly. If the description is missing
/// or unhelpful (e.g. "n/a"), it falls back to the raw port name.
String friendlyUsbPortName(String portLabel) {
final parts = portLabel.split(' - ');
if (parts.length < 2) {
return portLabel.trim();
}
// parts[0] = port name, parts[1] = description, parts[2+] = hardware id
final description = parts[1].trim();
if (description.isEmpty || description.toLowerCase() == 'n/a') {
return parts[0].trim();
}
return description;
}
String describeWebUsbPort({
required int? vendorId,
required int? productId,
String requestPortLabel = 'Choose USB Device',
String fallbackDeviceName = 'Web Serial Device',
Map<String, String> knownUsbNames = const <String, String>{},
}) {
if (vendorId == null && productId == null) {
return requestPortLabel;
}
final vendorHex = vendorId?.toRadixString(16).padLeft(4, '0').toUpperCase();
final productHex = productId?.toRadixString(16).padLeft(4, '0').toUpperCase();
final knownName = (vendorHex != null && productHex != null)
? knownUsbNames['${vendorHex.toLowerCase()}:${productHex.toLowerCase()}']
: null;
final parts = <String>[knownName ?? fallbackDeviceName];
if (vendorHex != null) {
parts.add('VID:$vendorHex');
}
if (productHex != null) {
parts.add('PID:$productHex');
}
return '${parts.first} (${parts.skip(1).join(' ')})';
}
String buildUsbDisplayLabel({
required String basePortLabel,
String? deviceName,
}) {
final trimmedName = deviceName?.trim() ?? '';
if (trimmedName.isEmpty) {
return basePortLabel;
}
return '$basePortLabel - $trimmedName';
}
+30 -32
View File
@@ -78,40 +78,36 @@ class _SNRIndicatorState extends State<SNRIndicator> {
widget.connector.currentSf,
);
return InkWell(
onTap: () {
if (directRepeater != null) {
_showFullPathDialog(context, directBestRepeaters);
}
},
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(snrUi.icon, size: 18, color: snrUi.color),
Text(
snrUi.text,
style: TextStyle(fontSize: 12, color: snrUi.color),
),
],
),
if (directRepeater != null)
return ConstrainedBox(
constraints: const BoxConstraints(minWidth: 40, minHeight: 40),
child: InkWell(
onTap: directRepeater != null
? () => _showFullPathDialog(context, directBestRepeaters)
: null,
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(snrUi.icon, size: 18, color: snrUi.color),
Text(
'${directRepeaters.length}: ${directRepeater.pubkeyFirstByte.toRadixString(16).padLeft(2, '0')}: ${_formatLastUpdated(directRepeater.lastUpdated)}',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Colors.grey,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
snrUi.text,
style: TextStyle(fontSize: 12, color: snrUi.color),
),
],
if (directRepeater != null)
Text(
'${directRepeaters.length}: ${directRepeater.pubkeyFirstByte.toRadixString(16).padLeft(2, '0')}: ${_formatLastUpdated(directRepeater.lastUpdated)}',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Colors.grey,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
),
);
@@ -148,8 +144,10 @@ class _SNRIndicatorState extends State<SNRIndicator> {
builder: (context) => AlertDialog(
title: Text(l10n.snrIndicator_nearByRepeaters),
content: SizedBox(
width: double.maxFinite,
child: Scrollbar(
child: ListView.separated(
shrinkWrap: true,
padding: const EdgeInsets.symmetric(vertical: 4),
itemCount: directBestRepeaters.length,
separatorBuilder: (_, _) => const Divider(height: 1),
+1
View File
@@ -7,6 +7,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
flserial
)
set(PLUGIN_BUNDLED_LIBRARIES)
+6
View File
@@ -1,4 +1,6 @@
PODS:
- flserial (0.0.1):
- FlutterMacOS
- flutter_blue_plus_darwin (0.0.2):
- Flutter
- FlutterMacOS
@@ -24,6 +26,7 @@ PODS:
- FlutterMacOS
DEPENDENCIES:
- flserial (from `Flutter/ephemeral/.symlinks/plugins/flserial/macos`)
- flutter_blue_plus_darwin (from `Flutter/ephemeral/.symlinks/plugins/flutter_blue_plus_darwin/darwin`)
- flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`)
- FlutterMacOS (from `Flutter/ephemeral`)
@@ -36,6 +39,8 @@ DEPENDENCIES:
- wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`)
EXTERNAL SOURCES:
flserial:
:path: Flutter/ephemeral/.symlinks/plugins/flserial/macos
flutter_blue_plus_darwin:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_blue_plus_darwin/darwin
flutter_local_notifications:
@@ -58,6 +63,7 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos
SPEC CHECKSUMS:
flserial: 3c161e076dfc73458ec5803e7a9a9d2bb85fadf6
flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3
flutter_local_notifications: 4bf37a31afde695b56091b4ae3e4d9c7a7e6cda0
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
+8
View File
@@ -12,6 +12,14 @@
<true/>
<key>com.apple.security.device.bluetooth</key>
<true/>
<key>com.apple.security.device.usb</key>
<true/>
<!-- USB serial ports (/dev/cu.* and /dev/tty.*) for LoRa device communication -->
<key>com.apple.security.temporary-exception.files.absolute-path.read-write</key>
<array>
<string>/dev/cu.</string>
<string>/dev/tty.</string>
</array>
<key>com.apple.security.device.camera</key>
<true/>
</dict>
+8
View File
@@ -8,6 +8,14 @@
<true/>
<key>com.apple.security.device.bluetooth</key>
<true/>
<key>com.apple.security.device.usb</key>
<true/>
<!-- USB serial ports (/dev/cu.* and /dev/tty.*) for LoRa device communication -->
<key>com.apple.security.temporary-exception.files.absolute-path.read-write</key>
<array>
<string>/dev/cu.</string>
<string>/dev/tty.</string>
</array>
<key>com.apple.security.device.camera</key>
<true/>
</dict>
+8 -1
View File
@@ -38,6 +38,11 @@ dependencies:
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
flutter_blue_plus: ^2.1.0
# TODO: Switch to official flserial repo once changes are upstreamed
flserial:
git:
url: https://github.com/MeshEnvy/flserial.git
ref: 48216310061efc8d5d217cc18014fc2cb501646e
provider: ^6.1.5+1
shared_preferences: ^2.2.2
uuid: ^4.3.3
@@ -130,6 +135,8 @@ flutter_launcher_icons:
# For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package
build_pipe:
workflows:
default:
@@ -141,4 +148,4 @@ build_pipe:
build_command: flutter build web --release --pwa-strategy=none
# Strongly recommended: disables the default service worker which often causes more cache headaches
add_version_query_param: true
# This is the key flag! It appends ?v=<your pubspec version> to bootstrap/JS files
# This is the key flag! It appends ?v=<your pubspec version> to bootstrap/JS files
+230
View File
@@ -0,0 +1,230 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';
import 'package:meshcore_open/connector/meshcore_connector.dart';
import 'package:meshcore_open/l10n/app_localizations.dart';
import 'package:meshcore_open/screens/scanner_screen.dart';
import 'package:meshcore_open/screens/usb_screen.dart';
import 'package:meshcore_open/utils/platform_info.dart';
class _FakeMeshCoreConnector extends MeshCoreConnector {
_FakeMeshCoreConnector({
this.initialState = MeshCoreConnectionState.disconnected,
List<String>? ports,
}) : _ports = ports ?? <String>[];
final MeshCoreConnectionState initialState;
final List<String> _ports;
String? requestPortLabel;
String? fallbackDeviceName;
int connectUsbCalls = 0;
String? lastConnectPortName;
String? fakeActiveUsbPort;
String? fakeActiveUsbPortDisplayLabel;
bool fakeUsbTransportConnected = false;
Future<List<String>> Function()? listUsbPortsImpl;
Future<void> Function({required String portName})? connectUsbImpl;
@override
MeshCoreConnectionState get state => initialState;
@override
MeshCoreTransportType get activeTransport => MeshCoreTransportType.usb;
@override
String? get activeUsbPort => fakeActiveUsbPort;
@override
String? get activeUsbPortDisplayLabel =>
fakeActiveUsbPortDisplayLabel ?? fakeActiveUsbPort;
@override
bool get isUsbTransportConnected => fakeUsbTransportConnected;
@override
Future<List<String>> listUsbPorts() async {
if (listUsbPortsImpl != null) {
return listUsbPortsImpl!();
}
return List<String>.from(_ports);
}
@override
Future<void> connectUsb({
required String portName,
int baudRate = 115200,
}) async {
if (connectUsbImpl != null) {
return connectUsbImpl!(portName: portName);
}
connectUsbCalls += 1;
lastConnectPortName = portName;
}
@override
void setUsbRequestPortLabel(String label) {
requestPortLabel = label;
}
@override
void setUsbFallbackDeviceName(String label) {
fallbackDeviceName = label;
}
}
Widget _buildTestApp({
required MeshCoreConnector connector,
required Widget child,
}) {
return ChangeNotifierProvider<MeshCoreConnector>.value(
value: connector,
child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: child,
),
);
}
void main() {
testWidgets('UsbScreen passes localized chooser label to connector', (
tester,
) async {
final connector = _FakeMeshCoreConnector();
await tester.pumpWidget(
_buildTestApp(connector: connector, child: const UsbScreen()),
);
await tester.pumpAndSettle();
expect(connector.requestPortLabel, 'Select a USB device');
});
testWidgets(
'UsbScreen does not call connectUsb when connector is not disconnected',
(tester) async {
final connector = _FakeMeshCoreConnector(
initialState: MeshCoreConnectionState.connected,
ports: <String>['COM6 - USB Serial Device (COM6)'],
);
await tester.pumpWidget(
_buildTestApp(connector: connector, child: const UsbScreen()),
);
await tester.pumpAndSettle();
await tester.tap(
find.ancestor(
of: find.text('Connect'),
matching: find.bySubtype<ElevatedButton>(),
),
);
await tester.pump();
expect(connector.connectUsbCalls, 0);
// UsbScreen.dispose() schedules disconnect work that debounces notify.
// Drain that debounce timer before test teardown.
await tester.pumpWidget(const SizedBox.shrink());
await tester.pump(const Duration(milliseconds: 60));
},
);
testWidgets('UsbScreen sends raw port name when tapping Connect', (
tester,
) async {
final connector = _FakeMeshCoreConnector(
ports: <String>['COM6 - USB Serial Device (COM6)'],
);
await tester.pumpWidget(
_buildTestApp(connector: connector, child: const UsbScreen()),
);
await tester.pumpAndSettle();
await tester.tap(
find.ancestor(
of: find.text('Connect'),
matching: find.bySubtype<ElevatedButton>(),
),
);
await tester.pump();
expect(connector.connectUsbCalls, 1);
expect(connector.lastConnectPortName, 'COM6');
});
testWidgets('ScannerScreen USB action reflects platform support', (
tester,
) async {
final connector = _FakeMeshCoreConnector();
await tester.pumpWidget(
_buildTestApp(connector: connector, child: const ScannerScreen()),
);
await tester.pumpAndSettle();
if (PlatformInfo.supportsUsbSerial) {
expect(find.widgetWithText(FloatingActionButton, 'USB'), findsOneWidget);
} else {
expect(find.widgetWithText(FloatingActionButton, 'USB'), findsNothing);
}
// ScannerScreen.dispose() schedules disconnect work that debounces notify.
// Drain that debounce timer before test teardown.
await tester.pumpWidget(const SizedBox.shrink());
await tester.pump(const Duration(milliseconds: 60));
});
group('Error Handling', () {
testWidgets('shows error SnackBar when listing ports fails', (
tester,
) async {
final connector = _FakeMeshCoreConnector();
connector.listUsbPortsImpl = () async {
throw PlatformException(
code: 'usb_permission_denied',
message: 'Permission denied',
);
};
await tester.pumpWidget(
_buildTestApp(connector: connector, child: const UsbScreen()),
);
await tester.pumpAndSettle();
expect(find.text('USB permission was denied.'), findsOneWidget);
});
testWidgets('connection failure shows SnackBar error', (tester) async {
final connector = _FakeMeshCoreConnector(ports: <String>['COM1']);
var connectAttempted = false;
connector.connectUsbImpl = ({required String portName}) async {
connectAttempted = true;
throw PlatformException(code: 'usb_busy', message: 'Device is busy');
};
await tester.pumpWidget(
_buildTestApp(connector: connector, child: const UsbScreen()),
);
await tester.pumpAndSettle();
await tester.tap(
find.ancestor(
of: find.text('Connect'),
matching: find.bySubtype<ElevatedButton>(),
),
);
await tester.pumpAndSettle();
expect(connectAttempted, isTrue);
expect(
find.text('Another USB connection request is already in progress.'),
findsOneWidget,
);
});
});
}
@@ -0,0 +1,162 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:meshcore_open/services/usb_serial_frame_codec.dart';
void main() {
test('wrapUsbSerialTxFrame prefixes tx header and payload length', () {
final packet = wrapUsbSerialTxFrame(Uint8List.fromList(<int>[0x16, 0x03]));
expect(
packet,
orderedEquals(<int>[usbSerialTxFrameStart, 0x02, 0x00, 0x16, 0x03]),
);
});
test('wrapUsbSerialTxFrame rejects payloads above protocol maximum', () {
final payload = Uint8List(usbSerialMaxPayloadLength + 1);
expect(
() => wrapUsbSerialTxFrame(payload),
throwsA(
isA<ArgumentError>().having(
(error) => error.name,
'name',
'payload.length',
),
),
);
});
test('UsbSerialFrameDecoder buffers partial frames until complete', () {
final decoder = UsbSerialFrameDecoder();
final firstChunk = decoder.ingest(
Uint8List.fromList(<int>[usbSerialRxFrameStart, 0x03]),
);
final secondChunk = decoder.ingest(
Uint8List.fromList(<int>[0x00, 0x05, 0x06, 0x07]),
);
expect(firstChunk, isEmpty);
expect(secondChunk, hasLength(1));
expect(secondChunk.single.isRxFrame, isTrue);
expect(secondChunk.single.payload, orderedEquals(<int>[0x05, 0x06, 0x07]));
});
test(
'UsbSerialFrameDecoder drops leading noise and parses multiple frames',
() {
final decoder = UsbSerialFrameDecoder();
final packets = decoder.ingest(
Uint8List.fromList(<int>[
0x00,
0x01,
usbSerialRxFrameStart,
0x01,
0x00,
0x55,
usbSerialRxFrameStart,
0x02,
0x00,
0x66,
0x77,
]),
);
expect(packets, hasLength(2));
expect(packets[0].payload, orderedEquals(<int>[0x55]));
expect(packets[1].payload, orderedEquals(<int>[0x66, 0x77]));
},
);
test(
'UsbSerialFrameDecoder preserves tx packets so caller can ignore them',
() {
final decoder = UsbSerialFrameDecoder();
final packets = decoder.ingest(
Uint8List.fromList(<int>[
usbSerialTxFrameStart,
0x01,
0x00,
0x22,
usbSerialRxFrameStart,
0x01,
0x00,
0x33,
]),
);
expect(packets, hasLength(2));
expect(packets[0].isRxFrame, isFalse);
expect(packets[0].payload, orderedEquals(<int>[0x22]));
expect(packets[1].isRxFrame, isTrue);
expect(packets[1].payload, orderedEquals(<int>[0x33]));
},
);
test(
'UsbSerialFrameDecoder drops oversized frames and resyncs on the next valid packet',
() {
final decoder = UsbSerialFrameDecoder();
final packets = decoder.ingest(
Uint8List.fromList(<int>[
usbSerialRxFrameStart,
0xAD,
0x00,
0x99,
usbSerialRxFrameStart,
0x01,
0x00,
0x44,
]),
);
expect(packets, hasLength(1));
expect(packets.single.isRxFrame, isTrue);
expect(packets.single.payload, orderedEquals(<int>[0x44]));
},
);
test('UsbSerialFrameDecoder reset clears buffered partial data', () {
final decoder = UsbSerialFrameDecoder();
expect(
decoder.ingest(Uint8List.fromList(<int>[usbSerialRxFrameStart, 0x02])),
isEmpty,
);
decoder.reset();
final packets = decoder.ingest(
Uint8List.fromList(<int>[usbSerialRxFrameStart, 0x01, 0x00, 0x55]),
);
expect(packets, hasLength(1));
expect(packets.single.payload, orderedEquals(<int>[0x55]));
});
test('recovers from invalid frame header', () {
final decoder = UsbSerialFrameDecoder();
final packets = decoder.ingest(
Uint8List.fromList(<int>[
// First, a malformed frame (e.g. from a partial TX echo)
usbSerialRxFrameStart,
usbSerialTxFrameStart,
// Then, a valid frame
usbSerialRxFrameStart,
0x01,
0x00,
0x88,
]),
);
expect(packets, hasLength(1));
expect(packets.single.isRxFrame, isTrue);
expect(packets.single.payload, orderedEquals(<int>[0x88]));
});
}
+131
View File
@@ -0,0 +1,131 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:meshcore_open/utils/usb_port_labels.dart';
void main() {
test('normalizeUsbPortName strips friendly suffix from composite label', () {
expect(
normalizeUsbPortName(
'COM6 - USB Serial Device (COM6) - USB\\VID_2886&PID_1667',
),
'COM6',
);
});
test(
'friendlyUsbPortName returns only description, not hardware_id (3-part label)',
() {
expect(
friendlyUsbPortName(
'COM6 - USB Serial Device (COM6) - USB\\VID_2886&PID_1667',
),
'USB Serial Device (COM6)',
);
},
);
test(
'friendlyUsbPortName works for macOS-style 3-part label with USB product name',
() {
expect(
friendlyUsbPortName(
'/dev/cu.usbmodem1101 - Nordic Semiconductor nRF52 DK - USB VID:PID=1915:520f SNR=ABCDEF',
),
'Nordic Semiconductor nRF52 DK',
);
},
);
test('friendlyUsbPortName works for Linux-style label', () {
expect(
friendlyUsbPortName(
'/dev/ttyACM0 - RAK4631 - USB VID:PID=239A:8029 SER=xxxxxxxx',
),
'RAK4631',
);
});
test('friendlyUsbPortName trims whitespace from label parts', () {
expect(
friendlyUsbPortName(' /dev/ttyS0 - My Serial Port - n/a '),
'My Serial Port',
);
});
test(
'friendlyUsbPortName falls back to port name when description is n/a',
() {
expect(
friendlyUsbPortName('/dev/cu.Bluetooth-Incoming-Port - n/a - n/a'),
'/dev/cu.Bluetooth-Incoming-Port',
);
},
);
test(
'friendlyUsbPortName handles 2-part label (no hardware_id) correctly',
() {
expect(
friendlyUsbPortName('COM6 - USB Serial Device (COM6)'),
'USB Serial Device (COM6)',
);
},
);
test('describeWebUsbPort uses known VID/PID names when available', () {
expect(
describeWebUsbPort(
vendorId: 0x2886,
productId: 0x1667,
knownUsbNames: const <String, String>{
'2886:1667': 'Seeed Wio Tracker L1',
},
),
'Seeed Wio Tracker L1 (VID:2886 PID:1667)',
);
});
test('describeWebUsbPort falls back to generic label for unknown device', () {
expect(
describeWebUsbPort(vendorId: 0x1234, productId: 0x5678),
'Web Serial Device (VID:1234 PID:5678)',
);
});
test('describeWebUsbPort returns chooser label when no usb ids exist', () {
expect(
describeWebUsbPort(vendorId: null, productId: null),
'Choose USB Device',
);
});
test('describeWebUsbPort uses caller-provided chooser label', () {
expect(
describeWebUsbPort(
vendorId: null,
productId: null,
requestPortLabel: 'Select a USB device',
),
'Select a USB device',
);
});
test('buildUsbDisplayLabel appends device-reported name when available', () {
expect(
buildUsbDisplayLabel(
basePortLabel: 'Seeed Wio Tracker L1 (VID:2886 PID:1667)',
deviceName: 'KD3CGK mesh-utility.org',
),
'Seeed Wio Tracker L1 (VID:2886 PID:1667) - KD3CGK mesh-utility.org',
);
});
test('buildUsbDisplayLabel keeps base label when custom name is blank', () {
expect(
buildUsbDisplayLabel(
basePortLabel: 'Seeed Wio Tracker L1 (VID:2886 PID:1667)',
deviceName: ' ',
),
'Seeed Wio Tracker L1 (VID:2886 PID:1667)',
);
});
}
+5 -3
View File
@@ -89,9 +89,11 @@ endif()
# Copy the native assets provided by the build.dart from all packages.
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/")
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
if(EXISTS "${NATIVE_ASSETS_DIR}")
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
+1
View File
@@ -9,6 +9,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
flserial
flutter_local_notifications_windows
)