mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-03-27 10:11:48 -04:00
feat: Desktop USB serial transport (#4836)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
@@ -38,7 +38,7 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec
|
||||
| `core:repository` | High-level domain interfaces (e.g., `NodeRepository`, `LocationRepository`). |
|
||||
| `core:domain` | Pure KMP business logic and UseCases. |
|
||||
| `core:data` | Core manager implementations and data orchestration. |
|
||||
| `core:network` | KMP networking layer using Ktor, MQTT abstractions, and shared transport (`StreamFrameCodec` in commonMain, `TcpTransport` in jvmAndroidMain). |
|
||||
| `core:network` | KMP networking layer using Ktor, MQTT abstractions, and shared transport (`StreamFrameCodec`, `TcpTransport`, `SerialTransport`). |
|
||||
| `core:di` | Common DI qualifiers and dispatchers. |
|
||||
| `core:navigation` | Shared navigation keys/routes for Navigation 3. |
|
||||
| `core:ui` | Shared Compose UI components (`EmptyDetailPlaceholder`, `MainAppBar`, dialogs, preferences) and platform abstractions. |
|
||||
@@ -47,11 +47,11 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec
|
||||
| `core:prefs` | KMP preferences layer built on DataStore abstractions. |
|
||||
| `core:barcode` | Barcode scanning (Android-only). |
|
||||
| `core:nfc` | NFC abstractions (KMP). Android NFC hardware implementation in `androidMain`. |
|
||||
| `core/ble/` | Bluetooth Low Energy stack using Nordic libraries. |
|
||||
| `core/ble/` | Bluetooth Low Energy stack using Kable. |
|
||||
| `core/resources/` | Centralized string and image resources (Compose Multiplatform). |
|
||||
| `core/testing/` | **Shared test doubles, fakes, and utilities for `commonTest` across all KMP modules.** |
|
||||
| `feature/` | Feature modules (e.g., `settings`, `map`, `messaging`, `node`, `intro`, `connections`). All are KMP with `jvm()` target. Use `meshtastic.kmp.feature` convention plugin. |
|
||||
| `desktop/` | Compose Desktop application — first non-Android KMP target. Nav 3 shell, full Koin DI graph, TCP transport with `want_config` handshake. |
|
||||
| `desktop/` | Compose Desktop application — first non-Android KMP target. Nav 3 shell, full Koin DI graph, TCP, Serial/USB, and BLE transports with `want_config` handshake. |
|
||||
| `mesh_service_example/` | Sample app showing `core:api` service integration. |
|
||||
|
||||
## 3. Development Guidelines & Coding Standards
|
||||
@@ -72,7 +72,7 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec
|
||||
- **Concurrency:** Use Kotlin Coroutines and Flow.
|
||||
- **Dependency Injection:** Use **Koin Annotations** with the K2 compiler plugin (0.4.0+). Keep root graph assembly in `app`.
|
||||
- **ViewModels:** Follow the MVI/UDF pattern. Use the multiplatform `androidx.lifecycle.ViewModel` in `commonMain`.
|
||||
- **BLE:** All Bluetooth communication must route through `core:ble` using Nordic Semiconductor's Android Common Libraries.
|
||||
- **BLE:** All Bluetooth communication must route through `core:ble` using Kable.
|
||||
- **Dependencies:** Check `gradle/libs.versions.toml` before assuming a library is available.
|
||||
- **JetBrains fork aliases:** Version catalog aliases for JetBrains-forked AndroidX artifacts use the `jetbrains-*` prefix (e.g., `jetbrains-lifecycle-runtime-compose`, `jetbrains-navigation3-ui`). Plain `androidx-*` aliases are true Google AndroidX artifacts. Never mix them up in `commonMain`.
|
||||
- **Compose Multiplatform:** Version catalog aliases for Compose Multiplatform artifacts use the `compose-multiplatform-*` prefix (e.g., `compose-multiplatform-material3`, `compose-multiplatform-foundation`). Never use plain `androidx.compose` dependencies in common Main.
|
||||
|
||||
@@ -38,7 +38,7 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec
|
||||
| `core:repository` | High-level domain interfaces (e.g., `NodeRepository`, `LocationRepository`). |
|
||||
| `core:domain` | Pure KMP business logic and UseCases. |
|
||||
| `core:data` | Core manager implementations and data orchestration. |
|
||||
| `core:network` | KMP networking layer using Ktor, MQTT abstractions, and shared transport (`StreamFrameCodec` in commonMain, `TcpTransport` in jvmAndroidMain). |
|
||||
| `core:network` | KMP networking layer using Ktor, MQTT abstractions, and shared transport (`StreamFrameCodec`, `TcpTransport`, `SerialTransport`). |
|
||||
| `core:di` | Common DI qualifiers and dispatchers. |
|
||||
| `core:navigation` | Shared navigation keys/routes for Navigation 3. |
|
||||
| `core:ui` | Shared Compose UI components (`EmptyDetailPlaceholder`, `MainAppBar`, dialogs, preferences) and platform abstractions. |
|
||||
@@ -47,11 +47,11 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec
|
||||
| `core:prefs` | KMP preferences layer built on DataStore abstractions. |
|
||||
| `core:barcode` | Barcode scanning (Android-only). |
|
||||
| `core:nfc` | NFC abstractions (KMP). Android NFC hardware implementation in `androidMain`. |
|
||||
| `core/ble/` | Bluetooth Low Energy stack using Nordic libraries. |
|
||||
| `core/ble/` | Bluetooth Low Energy stack using Kable. |
|
||||
| `core/resources/` | Centralized string and image resources (Compose Multiplatform). |
|
||||
| `core/testing/` | **Shared test doubles, fakes, and utilities for `commonTest` across all KMP modules.** |
|
||||
| `feature/` | Feature modules (e.g., `settings`, `map`, `messaging`, `node`, `intro`, `connections`). All are KMP with `jvm()` target. Use `meshtastic.kmp.feature` convention plugin. |
|
||||
| `desktop/` | Compose Desktop application — first non-Android KMP target. Nav 3 shell, full Koin DI graph, TCP transport with `want_config` handshake. |
|
||||
| `desktop/` | Compose Desktop application — first non-Android KMP target. Nav 3 shell, full Koin DI graph, TCP, Serial/USB, and BLE transports with `want_config` handshake. |
|
||||
| `mesh_service_example/` | Sample app showing `core:api` service integration. |
|
||||
|
||||
## 3. Development Guidelines & Coding Standards
|
||||
@@ -72,7 +72,7 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec
|
||||
- **Concurrency:** Use Kotlin Coroutines and Flow.
|
||||
- **Dependency Injection:** Use **Koin Annotations** with the K2 compiler plugin (0.4.0+). Keep root graph assembly in `app`.
|
||||
- **ViewModels:** Follow the MVI/UDF pattern. Use the multiplatform `androidx.lifecycle.ViewModel` in `commonMain`.
|
||||
- **BLE:** All Bluetooth communication must route through `core:ble` using Nordic Semiconductor's Android Common Libraries.
|
||||
- **BLE:** All Bluetooth communication must route through `core:ble` using Kable.
|
||||
- **Dependencies:** Check `gradle/libs.versions.toml` before assuming a library is available.
|
||||
- **JetBrains fork aliases:** Version catalog aliases for JetBrains-forked AndroidX artifacts use the `jetbrains-*` prefix (e.g., `jetbrains-lifecycle-runtime-compose`, `jetbrains-navigation3-ui`). Plain `androidx-*` aliases are true Google AndroidX artifacts. Never mix them up in `commonMain`.
|
||||
- **Compose Multiplatform:** Version catalog aliases for Compose Multiplatform artifacts use the `compose-multiplatform-*` prefix (e.g., `compose-multiplatform-material3`, `compose-multiplatform-foundation`). Never use plain `androidx.compose` dependencies in common Main.
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# Track desktop_serial_transport_20260317 Context
|
||||
|
||||
- [Specification](./spec.md)
|
||||
- [Implementation Plan](./plan.md)
|
||||
- [Metadata](./metadata.json)
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"track_id": "desktop_serial_transport_20260317",
|
||||
"type": "feature",
|
||||
"status": "new",
|
||||
"created_at": "2026-03-17T12:00:00Z",
|
||||
"updated_at": "2026-03-17T12:00:00Z",
|
||||
"description": "Implement Serial/USB transport for the Desktop target using jSerialComm. This fulfills the medium-term priority for direct radio connections on JVM and uses the shared RadioTransport interface."
|
||||
}
|
||||
21
conductor/archive/desktop_serial_transport_20260317/plan.md
Normal file
21
conductor/archive/desktop_serial_transport_20260317/plan.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Implementation Plan: Desktop Serial/USB Transport
|
||||
|
||||
## Phase 1: JVM Setup & Dependency Integration [checkpoint: a05916d]
|
||||
- [x] Task: Add the `jSerialComm` library to the `jvmMain` dependencies of the networking module. [checkpoint: 8994c66]
|
||||
- [x] Task: Create a `jvmMain` stub implementation for a `SerialTransport` class that implements the shared `RadioTransport` interface. [checkpoint: 83668e4]
|
||||
|
||||
## Phase 2: Serial Port Scanning & Connection Management [checkpoint: 9cda87d]
|
||||
- [x] Task: Implement port discovery using `jSerialComm` to list available serial ports. [checkpoint: c72501d]
|
||||
- [x] Task: Implement connect/disconnect logic for a selected serial port, handling port locking and baud rate configuration. [checkpoint: 23ee815]
|
||||
- [x] Task: Map the input/output streams of the open serial port to the existing KMP stream framing logic (`StreamFrameCodec`). [checkpoint: 04ba9c2]
|
||||
|
||||
## Phase 3: UI Integration
|
||||
- [x] Task: Update the `feature:connections` UI or `DesktopScannerViewModel` to poll the new `SerialTransport` for available ports. [checkpoint: 2e85b5a]
|
||||
- [x] Task: Wire the user's serial port selection to initiate the connection via the DI graph and active service logic. [checkpoint: 94cb97c]
|
||||
|
||||
## Phase 4: Validation [checkpoint: 1055752]
|
||||
- [x] Task: Verify end-to-end communication with a physical Meshtastic device over USB on the desktop target. [checkpoint: 1055752]
|
||||
- [x] Task: Ensure CI builds cleanly and that no `java.*` dependencies leaked into `commonMain`. [checkpoint: 1055752]
|
||||
|
||||
## Phase: Review Fixes
|
||||
- [x] Task: Apply review suggestions [checkpoint: d2f7c82]
|
||||
20
conductor/archive/desktop_serial_transport_20260317/spec.md
Normal file
20
conductor/archive/desktop_serial_transport_20260317/spec.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# Specification: Desktop Serial/USB Transport via jSerialComm
|
||||
|
||||
## Objective
|
||||
Implement direct radio connection via Serial/USB on the Desktop (JVM) target using the `jSerialComm` library. This fulfills the medium-term priority of bringing physical transport parity to the desktop app and validates the newly extracted `RadioTransport` abstraction in `core:repository`.
|
||||
|
||||
## Background
|
||||
Currently, the desktop app supports TCP connections via a shared `StreamFrameCodec`. To provide parity with Android's USB serial connection capabilities, we need to implement a JVM-specific serial transport. The `jSerialComm` library is a widely-used, cross-platform Java library that handles native serial port communication without requiring complex JNI setups.
|
||||
|
||||
## Requirements
|
||||
- Introduce `jSerialComm` dependency to the `jvmMain` source set of the appropriate core module (likely `core:network` or a new `core:serial` module).
|
||||
- Implement the `RadioTransport` interface (defined in `core:repository/commonMain`) for the desktop target, wrapping `jSerialComm`'s port scanning and connection logic.
|
||||
- Ensure the serial data is encoded/decoded using the same protobuf frame structure utilized by the TCP transport (e.g., leveraging the existing `StreamFrameCodec`).
|
||||
- Integrate the new transport into the `feature:connections` UI on the desktop so users can scan for and select connected USB serial devices.
|
||||
- Retain platform purity: keep all `jSerialComm` and `java.io.*` imports strictly within the `jvmMain` source set.
|
||||
|
||||
## Success Criteria
|
||||
- [ ] Desktop application successfully scans for connected Meshtastic devices over USB/Serial.
|
||||
- [ ] Users can select a serial port from the `feature:connections` UI and establish a connection.
|
||||
- [ ] Two-way protobuf communication is verified (e.g., the app receives node info and can send a message).
|
||||
- [ ] The implementation uses the shared `RadioTransport` interface without leaking JVM dependencies into `commonMain`.
|
||||
@@ -24,4 +24,12 @@
|
||||
## Networking & Transport
|
||||
- **Ktor:** Multiplatform HTTP client for web services and TCP streaming.
|
||||
- **Kable:** Multiplatform BLE library used as the primary BLE transport for all targets (Android, Desktop, and future iOS).
|
||||
- **Coroutines & Flows:** For asynchronous programming and state management.
|
||||
- **jSerialComm:** Cross-platform Java library used for direct Serial/USB communication with Meshtastic devices on the Desktop (JVM) target.
|
||||
- **Coroutines & Flows:** For asynchronous programming and state management.
|
||||
|
||||
## Testing (KMP)
|
||||
- **Shared Tests First:** The majority of business logic, ViewModels, and state interactions are tested in the `commonTest` source set using standard `kotlin.test`.
|
||||
- **Coroutines Testing:** Use `kotlinx-coroutines-test` for virtual time management in asynchronous flows.
|
||||
- **Mocking Strategy:** Avoid JVM-specific mocking libraries. Prefer `Mokkery` or `Mockative` for multiplatform-compatible mocking interfaces, alongside handwritten fakes in `core:testing`.
|
||||
- **Flow Assertions:** Use `Turbine` for testing multiplatform `Flow` emissions and state updates.
|
||||
- **Property-Based Testing:** Consider evaluating `Kotest` for multiplatform data-driven and property-based testing scenarios if standard `kotlin.test` becomes insufficient.
|
||||
@@ -48,7 +48,12 @@ kotlin {
|
||||
implementation(libs.kermit)
|
||||
}
|
||||
|
||||
val jvmMain by getting { dependencies { implementation(libs.ktor.client.java) } }
|
||||
val jvmMain by getting {
|
||||
dependencies {
|
||||
implementation(libs.ktor.client.java)
|
||||
implementation(libs.jserialcomm)
|
||||
}
|
||||
}
|
||||
|
||||
androidMain.dependencies {
|
||||
implementation(projects.core.ble)
|
||||
@@ -61,6 +66,7 @@ kotlin {
|
||||
implementation(libs.okhttp3.logging.interceptor)
|
||||
}
|
||||
|
||||
val jvmTest by getting { dependencies { implementation(libs.mockk) } }
|
||||
commonTest.dependencies { implementation(libs.kotlinx.coroutines.test) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.network
|
||||
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.fazecast.jSerialComm.SerialPort
|
||||
import com.fazecast.jSerialComm.SerialPortTimeoutException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import org.meshtastic.core.network.radio.StreamInterface
|
||||
import org.meshtastic.core.repository.RadioInterfaceService
|
||||
|
||||
/**
|
||||
* JVM-specific implementation of [RadioTransport] using jSerialComm. Uses [StreamInterface] for START1/START2 packet
|
||||
* framing.
|
||||
*/
|
||||
class SerialTransport(
|
||||
private val portName: String,
|
||||
private val baudRate: Int = DEFAULT_BAUD_RATE,
|
||||
service: RadioInterfaceService,
|
||||
) : StreamInterface(service) {
|
||||
private var serialPort: SerialPort? = null
|
||||
private var readJob: Job? = null
|
||||
|
||||
/** Attempts to open the serial port and starts the read loop. Returns true if successful, false otherwise. */
|
||||
fun startConnection(): Boolean {
|
||||
return try {
|
||||
val port = SerialPort.getCommPort(portName) ?: return false
|
||||
port.setComPortParameters(baudRate, DATA_BITS, SerialPort.ONE_STOP_BIT, SerialPort.NO_PARITY)
|
||||
port.setComPortTimeouts(SerialPort.TIMEOUT_READ_SEMI_BLOCKING, READ_TIMEOUT_MS, 0)
|
||||
if (port.openPort()) {
|
||||
serialPort = port
|
||||
port.setDTR()
|
||||
port.setRTS()
|
||||
super.connect() // Sends WAKE_BYTES and signals service.onConnect()
|
||||
startReadLoop(port)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} catch (@Suppress("TooGenericExceptionCaught") e: Exception) {
|
||||
Logger.e(e) { "Serial connection failed" }
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
private fun startReadLoop(port: SerialPort) {
|
||||
readJob =
|
||||
service.serviceScope.launch(Dispatchers.IO) {
|
||||
val input = port.inputStream
|
||||
val buffer = ByteArray(READ_BUFFER_SIZE)
|
||||
try {
|
||||
var reading = true
|
||||
while (isActive && port.isOpen && reading) {
|
||||
try {
|
||||
val numRead = input.read(buffer)
|
||||
if (numRead == -1) {
|
||||
reading = false
|
||||
} else if (numRead > 0) {
|
||||
for (i in 0 until numRead) {
|
||||
readChar(buffer[i])
|
||||
}
|
||||
}
|
||||
} catch (_: SerialPortTimeoutException) {
|
||||
// Expected timeout when no data is available
|
||||
} catch (e: kotlinx.coroutines.CancellationException) {
|
||||
throw e
|
||||
} catch (@Suppress("TooGenericExceptionCaught") e: Exception) {
|
||||
if (isActive) {
|
||||
Logger.e(e) { "Serial read IOException: ${e.message}" }
|
||||
} else {
|
||||
Logger.d { "Serial read interrupted by cancellation: ${e.message}" }
|
||||
}
|
||||
reading = false
|
||||
}
|
||||
}
|
||||
} catch (e: kotlinx.coroutines.CancellationException) {
|
||||
throw e
|
||||
} catch (@Suppress("TooGenericExceptionCaught") e: Exception) {
|
||||
if (isActive) {
|
||||
Logger.e(e) { "Serial read loop outer error: ${e.message}" }
|
||||
} else {
|
||||
Logger.d { "Serial read loop outer interrupted by cancellation: ${e.message}" }
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
input.close()
|
||||
} catch (_: Exception) {
|
||||
// Ignore errors during input stream close
|
||||
}
|
||||
try {
|
||||
if (port.isOpen) {
|
||||
port.closePort()
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
// Ignore errors during port close
|
||||
}
|
||||
if (isActive) {
|
||||
onDeviceDisconnect(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun sendBytes(p: ByteArray) {
|
||||
serialPort?.takeIf { it.isOpen }?.outputStream?.write(p)
|
||||
}
|
||||
|
||||
override fun flushBytes() {
|
||||
serialPort?.takeIf { it.isOpen }?.outputStream?.flush()
|
||||
}
|
||||
|
||||
override fun keepAlive() {
|
||||
// Not specifically needed for raw serial unless implemented
|
||||
}
|
||||
|
||||
private fun closePortResources() {
|
||||
serialPort?.takeIf { it.isOpen }?.closePort()
|
||||
serialPort = null
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
readJob?.cancel()
|
||||
readJob = null
|
||||
closePortResources()
|
||||
super.close()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DEFAULT_BAUD_RATE = 115200
|
||||
private const val DATA_BITS = 8
|
||||
private const val READ_BUFFER_SIZE = 1024
|
||||
private const val READ_TIMEOUT_MS = 100
|
||||
|
||||
/**
|
||||
* Discovers and returns a list of available serial ports. Returns a list of the system port names (e.g.,
|
||||
* "COM3", "/dev/ttyUSB0").
|
||||
*/
|
||||
fun getAvailablePorts(): List<String> = SerialPort.getCommPorts().map { it.systemPortName }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.network
|
||||
|
||||
import com.fazecast.jSerialComm.SerialPort
|
||||
import io.mockk.mockk
|
||||
import org.meshtastic.core.repository.RadioInterfaceService
|
||||
import org.meshtastic.core.repository.RadioTransport
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class SerialTransportTest {
|
||||
private val mockService: RadioInterfaceService = mockk(relaxed = true)
|
||||
|
||||
@Test
|
||||
fun testJSerialCommIsAvailable() {
|
||||
val ports = SerialPort.getCommPorts()
|
||||
assertNotNull(ports, "Serial ports array should not be null")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSerialTransportImplementsRadioTransport() {
|
||||
val transport: RadioTransport = SerialTransport("dummyPort", service = mockService)
|
||||
assertTrue(transport is SerialTransport, "Transport should be a SerialTransport")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetAvailablePorts() {
|
||||
val ports = SerialTransport.getAvailablePorts()
|
||||
assertNotNull(ports, "Available ports should not be null")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testConnectToInvalidPortFailsGracefully() {
|
||||
val transport = SerialTransport("invalid_port_name", 115200, mockService)
|
||||
val connected = transport.startConnection()
|
||||
assertFalse(connected, "Connecting to an invalid port should return false")
|
||||
transport.close()
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,7 @@ The module depends on the JVM variants of KMP modules:
|
||||
| `navigation/DesktopSettingsNavigation.kt` | Real settings feature composables wired into nav graph (~35 screens) |
|
||||
| `navigation/DesktopNodeNavigation.kt` | Real adaptive node list-detail + real metrics screens (logs + charts); map routes remain placeholders |
|
||||
| `navigation/DesktopMessagingNavigation.kt` | Real adaptive contacts list-detail + real Messages/Share/QuickChat route screens |
|
||||
| `radio/DesktopRadioInterfaceService.kt` | TCP socket transport with auto-reconnect, heartbeat, and backoff retry |
|
||||
| `radio/DesktopRadioInterfaceService.kt` | TCP, Serial/USB, and BLE transports with auto-reconnect, heartbeat, and backoff retry |
|
||||
| `radio/DesktopMeshServiceController.kt` | Mesh service lifecycle — orchestrates `want_config` handshake chain |
|
||||
| `radio/DesktopMessageQueue.kt` | Message queue for outbound mesh packets |
|
||||
| `ui/firmware/DesktopFirmwareScreen.kt` | Placeholder firmware screen (native DFU is Android-only) |
|
||||
@@ -91,6 +91,7 @@ The module depends on the JVM variants of KMP modules:
|
||||
- [x] Add desktop language picker backed by shared `UiPreferencesDataSource.locale` with live translation updates
|
||||
- [ ] Wire remaining `feature:*` composables (map) into the nav graph
|
||||
- [ ] Move remaining node detail and message composables from `androidMain` to `commonMain`
|
||||
- [ ] Add serial/USB transport for direct radio connection on Desktop
|
||||
- [x] Add serial/USB transport for direct radio connection on Desktop
|
||||
- [x] Add BLE transport (via Kable) for direct radio connection on Desktop
|
||||
- [ ] Add MQTT transport for cloud-connected operation
|
||||
- [x] Package as native distributions (DMG, MSI, DEB) via CI release pipeline
|
||||
|
||||
@@ -56,7 +56,11 @@ class DesktopRadioInterfaceService(
|
||||
) : RadioInterfaceService {
|
||||
|
||||
override val supportedDeviceTypes: List<org.meshtastic.core.model.DeviceType> =
|
||||
listOf(org.meshtastic.core.model.DeviceType.TCP, org.meshtastic.core.model.DeviceType.BLE)
|
||||
listOf(
|
||||
org.meshtastic.core.model.DeviceType.TCP,
|
||||
org.meshtastic.core.model.DeviceType.BLE,
|
||||
org.meshtastic.core.model.DeviceType.USB,
|
||||
)
|
||||
|
||||
private val _connectionState = MutableStateFlow<ConnectionState>(ConnectionState.Disconnected)
|
||||
override val connectionState: StateFlow<ConnectionState> = _connectionState.asStateFlow()
|
||||
@@ -76,6 +80,7 @@ class DesktopRadioInterfaceService(
|
||||
|
||||
private var transport: TcpTransport? = null
|
||||
private var bleTransport: DesktopBleInterface? = null
|
||||
private var serialTransport: org.meshtastic.core.network.SerialTransport? = null
|
||||
|
||||
init {
|
||||
// Observe radioPrefs to handle asynchronous loads from DataStore
|
||||
@@ -136,6 +141,7 @@ class DesktopRadioInterfaceService(
|
||||
serviceScope.handledLaunch {
|
||||
transport?.sendPacket(bytes)
|
||||
bleTransport?.handleSendToRadio(bytes)
|
||||
serialTransport?.handleSendToRadio(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,6 +176,8 @@ class DesktopRadioInterfaceService(
|
||||
private fun startConnection(address: String) {
|
||||
if (address.startsWith("t")) {
|
||||
startTcpConnection(address.removePrefix("t"))
|
||||
} else if (address.startsWith("s")) {
|
||||
startSerialConnection(address.removePrefix("s"))
|
||||
} else if (address.startsWith("x")) {
|
||||
startBleConnection(address.removePrefix("x"))
|
||||
} else {
|
||||
@@ -179,6 +187,18 @@ class DesktopRadioInterfaceService(
|
||||
}
|
||||
}
|
||||
|
||||
private fun startSerialConnection(portName: String) {
|
||||
transport?.stop()
|
||||
bleTransport?.close()
|
||||
serialTransport?.close()
|
||||
|
||||
val serial = org.meshtastic.core.network.SerialTransport(portName = portName, service = this)
|
||||
serialTransport = serial
|
||||
if (!serial.startConnection()) {
|
||||
onDisconnect(isPermanent = true, errorMessage = "Failed to connect to $portName")
|
||||
}
|
||||
}
|
||||
|
||||
private fun startBleConnection(address: String) {
|
||||
transport?.stop()
|
||||
bleTransport?.close()
|
||||
@@ -228,6 +248,9 @@ class DesktopRadioInterfaceService(
|
||||
bleTransport?.close()
|
||||
bleTransport = null
|
||||
|
||||
serialTransport?.close()
|
||||
serialTransport = null
|
||||
|
||||
// Recreate the service scope
|
||||
serviceScope.cancel("stopping interface")
|
||||
serviceScope = CoroutineScope(dispatchers.io + SupervisorJob())
|
||||
|
||||
@@ -27,7 +27,7 @@ Modules that share JVM-specific code between Android and desktop now standardize
|
||||
| `core:database` | ✅ | ✅ | Room KMP |
|
||||
| `core:domain` | ✅ | ✅ | UseCases |
|
||||
| `core:prefs` | ✅ | ✅ | Preferences layer |
|
||||
| `core:network` | ✅ | ✅ | Ktor, `StreamFrameCodec`, `TcpTransport` |
|
||||
| `core:network` | ✅ | ✅ | Ktor, `StreamFrameCodec`, `TcpTransport`, `SerialTransport` |
|
||||
| `core:data` | ✅ | ✅ | Data orchestration |
|
||||
| `core:ble` | ✅ | ✅ | Kable multiplatform BLE abstractions in commonMain |
|
||||
| `core:nfc` | ✅ | ✅ | NFC contract in commonMain; hardware in androidMain |
|
||||
@@ -56,13 +56,14 @@ Modules that share JVM-specific code between Android and desktop now standardize
|
||||
Working Compose Desktop application with:
|
||||
- Navigation 3 shell (`NavigationRail` + `NavDisplay`) using shared routes
|
||||
- Full Koin DI graph (stubs + real implementations)
|
||||
- TCP transport with auto-reconnect and full `want_config` handshake
|
||||
- TCP, Serial/USB, and BLE transports with auto-reconnect and full `want_config` handshake
|
||||
- Adaptive list-detail screens for nodes and contacts
|
||||
- **Dynamic Connections screen** with automatic discovery of platform-supported transports (TCP)
|
||||
- **Dynamic Connections screen** with automatic discovery of platform-supported transports (TCP, Serial/USB, BLE)
|
||||
- **Desktop language picker** backed by `UiPreferencesDataSource.locale`, with immediate Compose Multiplatform resource updates
|
||||
- **Navigation-preserving locale switching** via `Main.kt` `staticCompositionLocalOf` recomposition instead of recreating the Nav3 backstack
|
||||
- Node detail metrics screens (Device, Environment, Signal, Power, Pax) wired with shared KMP + Vico charts
|
||||
- 7 desktop-specific screens (Settings, Device, Position, Network, Security, ExternalNotification, Debug)
|
||||
- **Native notifications and system tray icon** wired via `DesktopNotificationManager`
|
||||
- **Native release pipeline** generating `.dmg` (macOS), `.msi` (Windows), and `.deb` (Linux) installers in CI
|
||||
|
||||
## Scorecard
|
||||
@@ -107,7 +108,7 @@ Based on the latest codebase investigation, the following steps are proposed to
|
||||
| Material 3 Adaptive (JetBrains) | ✅ Done | Version `1.3.0-alpha06` aligned with CMP `1.11.0-alpha04` |
|
||||
| JetBrains lifecycle/nav3 alias alignment | ✅ Done | All forked deps use `jetbrains-*` prefix in version catalog; `core:data` commonMain uses JetBrains lifecycle runtime |
|
||||
| Expect/actual consolidation | ✅ Done | 7 pairs eliminated; 15+ genuinely platform-specific retained |
|
||||
| Transport deduplication | ✅ Done | `StreamFrameCodec` + `TcpTransport` shared in `core:network` |
|
||||
| Transport deduplication | ✅ Done | `StreamFrameCodec`, `TcpTransport`, and `SerialTransport` shared in `core:network` |
|
||||
| **Transport UI Unification** | ✅ Done | `RadioInterfaceService` provides dynamic transport capability to shared UI |
|
||||
| Emoji picker unification | ✅ Done | Single commonMain implementation replacing 3 platform variants |
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ here| **Migrate to JetBrains Compose Multiplatform dependencies** | High | Low |
|
||||
- ✅ **Settings:** ~35 screens with real configuration, including theme/about parity and desktop language picker support
|
||||
- ✅ **Nodes:** Adaptive list-detail with node management
|
||||
- ✅ **Messaging:** Adaptive contacts with message view + send
|
||||
- ✅ **Connections:** Dynamic discovery of platform-supported transports (TCP)
|
||||
- ✅ **Connections:** Dynamic discovery of platform-supported transports (TCP, Serial/USB, BLE)
|
||||
- ❌ **Map:** Placeholder only, needs MapLibre or alternative
|
||||
- ⚠️ **Firmware:** Placeholder wired into nav graph; native DFU not applicable to desktop
|
||||
- ⚠️ **Intro:** Onboarding flow (may not apply to desktop)
|
||||
@@ -41,7 +41,7 @@ here| **Migrate to JetBrains Compose Multiplatform dependencies** | High | Low |
|
||||
- Test navigation flows end-to-end
|
||||
2. **Tier 2: Polish (High Priority)**
|
||||
- Additional desktop-specific settings polish
|
||||
- Keyboard shortcuts
|
||||
- ✅ **MenuBar integration** and Keyboard shortcuts
|
||||
- Window management
|
||||
- State persistence
|
||||
3. **Tier 3: Advanced (Nice-to-have)**
|
||||
@@ -53,9 +53,10 @@ here| **Migrate to JetBrains Compose Multiplatform dependencies** | High | Low |
|
||||
| Transport | Platform | Status |
|
||||
|---|---|---|
|
||||
| TCP | Desktop (JVM) | ✅ Done — shared `StreamFrameCodec` + `TcpTransport` in `core:network` |
|
||||
| Serial/USB | Desktop (JVM) | ❌ Next — jSerialComm |
|
||||
| Serial/USB | Desktop (JVM) | ✅ Done — jSerialComm |
|
||||
| MQTT | All (KMP) | ❌ Planned — Ktor/MQTT (currently Android-only via Eclipse Paho) |
|
||||
| BLE | Desktop | ❌ Future — Kable (JVM) |
|
||||
| BLE | Android | ✅ Done — Kable |
|
||||
| BLE | Desktop | ✅ Done — Kable (JVM) |
|
||||
| BLE | iOS | ❌ Future — Kable/CoreBluetooth |
|
||||
|
||||
### Desktop Feature Gaps
|
||||
@@ -70,6 +71,8 @@ here| **Migrate to JetBrains Compose Multiplatform dependencies** | High | Low |
|
||||
| Map | ❌ Needs MapLibre or equivalent |
|
||||
| Charts | ✅ Vico KMP charts wired in commonMain (Device, Environment, Signal, Power, Pax) |
|
||||
| Debug Panel | ✅ Real screen (mesh log viewer via shared `DebugViewModel`) |
|
||||
| Notifications | ✅ Desktop native notifications with system tray icon support |
|
||||
| MenuBar | ✅ Done — Native application menu bar with File/View menus |
|
||||
| About | ✅ Shared `commonMain` screen (AboutLibraries KMP `produceLibraries` + per-platform JSON) |
|
||||
| Packaging | ✅ Done — Native distribution pipeline in CI (DMG, MSI, DEB) |
|
||||
|
||||
@@ -89,9 +92,9 @@ here| **Migrate to JetBrains Compose Multiplatform dependencies** | High | Low |
|
||||
- ✅ **Done:** Extracted remaining 5 ViewModels: `SettingsViewModel`, `RadioConfigViewModel`, `DebugViewModel`, `MetricsViewModel`, `UIViewModel` to shared KMP modules.
|
||||
- ✅ **Done:** Extracted service, worker, and radio files from `app` to `core:service/androidMain` and `core:network/androidMain`.
|
||||
- **Next:** Extract remaining Android-specific files (e.g., Navigation files, App Widgets, message queues, and root Activity logic) out of `:app` to establish a truly thin app module.
|
||||
2. **Serial/USB transport** — direct radio connection on Desktop via jSerialComm
|
||||
2. ✅ **Done:** **Serial/USB transport** — direct radio connection on Desktop via jSerialComm
|
||||
3. **MQTT transport** — cloud relay operation (KMP, benefits all targets)
|
||||
4. **Evaluate KMP-native mocking** — Evaluate `mockative` or similar to replace `mockk` in `commonMain` of `core:testing` for iOS readiness.
|
||||
4. **Evaluate KMP-native testing tools** — Evaluate `Mokkery` or `Mockative` to replace `mockk` in `commonMain` of `core:testing` for iOS readiness. Integrate `Turbine` for shared `Flow` testing.
|
||||
5. **Desktop ViewModel auto-wiring** — ✅ Done: ensured Koin K2 Compiler Plugin generates ViewModel modules for JVM target; eliminated manual wiring in `DesktopKoinModule`
|
||||
5. **KMP charting** — ✅ Done: Vico charts migrated to `feature:node/commonMain` using KMP artifacts; desktop wires them directly
|
||||
6. **Navigation contract extraction** — ✅ Done: shared `TopLevelDestination` enum in `core:navigation`; icon mapping in `core:ui`; parity tests in place. Both shells derive from the same source of truth.
|
||||
@@ -100,17 +103,23 @@ here| **Migrate to JetBrains Compose Multiplatform dependencies** | High | Low |
|
||||
## Longer-Term (90+ days)
|
||||
|
||||
1. **iOS proof target** — declare `iosArm64()`/`iosSimulatorArm64()` in KMP modules; BLE via Kable/CoreBluetooth
|
||||
2. **Map on Desktop** — evaluate MapLibre for cross-platform maps
|
||||
2. **Platform-Native UI Interop** —
|
||||
- **iOS Maps & Camera:** Implement `MapLibre` or `MKMapView` via Compose Multiplatform's `UIKitView`. Leverage `AVCaptureSession` wrapped in `UIKitView` to fulfill the `LocalBarcodeScannerProvider` contract.
|
||||
- **Desktop Maps:** Implement maps via `SwingPanel` wrapper, utilizing experimental interop blending (`compose.interop.blending=true`) to ensure tooltips and Compose overlays render correctly on top of the native JComponent.
|
||||
- **Web (wasmJs) Integrations:** Leverage `HtmlView` to embed raw DOM elements (e.g., `<video>`, `<iframe>`, or canvas-based maps) directly into the Compose UI tree while binding the root app via `CanvasBasedWindow`.
|
||||
3. **`core:api` contract split** — separate transport-neutral service contracts from Android AIDL packaging
|
||||
4. **Native packaging** — ✅ Done: DMG, MSI, DEB distributions for Desktop via release pipeline
|
||||
5. **Module maturity dashboard** — living inventory of per-module KMP readiness
|
||||
6. **Shared UI vs Shared Logic split** — If the iOS target utilizes native SwiftUI instead of Compose Multiplatform, evaluate splitting feature modules into pure `sharedLogic` (business rules, ViewModels) and `sharedUI` (Compose Multiplatform) to prevent dragging Compose dependencies into pure native iOS apps.
|
||||
|
||||
## Design Principles
|
||||
|
||||
1. **Solve in `commonMain` first.** If it doesn't need platform APIs, it belongs in `commonMain`.
|
||||
2. **Interfaces in `commonMain`, implementations per-target.** The repository pattern is established — extend it.
|
||||
3. **Stubs are a valid first implementation.** Every target starts with no-op stubs, then graduates to real implementations.
|
||||
4. **Feature modules stay target-agnostic in `commonMain`.** Platform UI goes in platform source sets.
|
||||
5. **Transport is a pluggable adapter.** BLE, serial, TCP, MQTT all implement `RadioInterfaceService`.
|
||||
6. **CI validates every target.** If a module declares `jvm()`, CI compiles it. No exceptions.
|
||||
7. **Test in `commonTest` first.** ViewModel and business logic tests belong in `commonTest` so every target runs them.
|
||||
2. **Interfaces in `commonMain`, implementations per-target.** The repository pattern is established — extend it. Prefer dependency injection (Koin) with interfaces over `expect`/`actual` declarations whenever possible to keep architecture decoupled and highly testable.
|
||||
3. **UI Interop Strategies.** When a Compose Multiplatform equivalent doesn't exist (e.g., Maps, Camera), use standard interop APIs rather than extracting the entire screen to native code. Use `AndroidView` for Android, `UIKitView` for iOS, `SwingPanel` for JVM/Desktop, and `HtmlView` for Web (`wasmJs`). Always wrap these in a shared `commonMain` interface contract (like `LocalBarcodeScannerProvider`).
|
||||
4. **Stubs are a valid first implementation.** Every target starts with no-op stubs, then graduates to real implementations.
|
||||
5. **Feature modules stay target-agnostic in `commonMain`.** Platform UI goes in platform source sets. Keep the UI layer dumb and rely on shared ViewModels (Unidirectional Data Flow) to drive state.
|
||||
6. **Transport is a pluggable adapter.** BLE, serial, TCP, MQTT all implement `RadioInterfaceService`.
|
||||
7. **CI validates every target.** If a module declares `jvm()`, CI compiles it. No exceptions. Run tests on appropriate host runners (macOS for iOS, Linux for JVM/Android) to catch platform regressions.
|
||||
8. **Test in `commonTest` first.** ViewModel and business logic tests belong in `commonTest` so every target runs them. Use shared `core:testing` utilities to minimize duplication.
|
||||
9. **Zero Platform Leaks.** Never import `java.*` or `android.*` inside `commonMain`. Use KMP-native alternatives like `kotlinx-datetime` and `Okio`.
|
||||
|
||||
@@ -34,13 +34,15 @@ class CommonGetDiscoveredDevicesUseCase(
|
||||
private val recentAddressesDataSource: RecentAddressesDataSource,
|
||||
private val nodeRepository: NodeRepository,
|
||||
private val databaseManager: DatabaseManager,
|
||||
private val usbScanner: UsbScanner? = null,
|
||||
) : GetDiscoveredDevicesUseCase {
|
||||
private val suffixLength = 4
|
||||
|
||||
override fun invoke(showMock: Boolean): Flow<DiscoveredDevices> {
|
||||
val nodeDb = nodeRepository.nodeDBbyNum
|
||||
val usbFlow = usbScanner?.scanUsbDevices() ?: kotlinx.coroutines.flow.flowOf(emptyList())
|
||||
|
||||
return combine(nodeDb, recentAddressesDataSource.recentAddresses) { db, recentList ->
|
||||
return combine(nodeDb, recentAddressesDataSource.recentAddresses, usbFlow) { db, recentList, usbList ->
|
||||
val recentTcpForUi =
|
||||
recentList
|
||||
.map { DeviceListEntry.Tcp(it.name, it.address) }
|
||||
@@ -63,12 +65,14 @@ class CommonGetDiscoveredDevicesUseCase(
|
||||
DiscoveredDevices(
|
||||
recentTcpDevices = recentTcpForUi,
|
||||
usbDevices =
|
||||
if (showMock) {
|
||||
val demoModeLabel = runCatching { getString(Res.string.demo_mode) }.getOrDefault("Demo Mode")
|
||||
listOf(DeviceListEntry.Mock(demoModeLabel))
|
||||
} else {
|
||||
emptyList()
|
||||
},
|
||||
usbList +
|
||||
if (showMock) {
|
||||
val demoModeLabel =
|
||||
runCatching { getString(Res.string.demo_mode) }.getOrDefault("Demo Mode")
|
||||
listOf(DeviceListEntry.Mock(demoModeLabel))
|
||||
} else {
|
||||
emptyList()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.connections.domain.usecase
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.meshtastic.feature.connections.model.DeviceListEntry
|
||||
|
||||
/** Platform-specific scanner for USB/Serial devices. */
|
||||
interface UsbScanner {
|
||||
fun scanUsbDevices(): Flow<List<DeviceListEntry.Usb>>
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.connections.domain.usecase
|
||||
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.isActive
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.network.SerialTransport
|
||||
import org.meshtastic.feature.connections.model.DeviceListEntry
|
||||
import org.meshtastic.feature.connections.model.JvmUsbDeviceData
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
@Single
|
||||
class JvmUsbScanner : UsbScanner {
|
||||
override fun scanUsbDevices(): Flow<List<DeviceListEntry.Usb>> = flow {
|
||||
while (coroutineContext.isActive) {
|
||||
val ports =
|
||||
SerialTransport.getAvailablePorts().map { portName ->
|
||||
DeviceListEntry.Usb(
|
||||
usbData = JvmUsbDeviceData(portName),
|
||||
name = portName,
|
||||
fullAddress = "s$portName",
|
||||
bonded = true, // Desktop serial ports don't need Android USB permission bonding
|
||||
node = null,
|
||||
)
|
||||
}
|
||||
emit(ports)
|
||||
delay(POLL_INTERVAL_MS)
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
|
||||
companion object {
|
||||
private const val POLL_INTERVAL_MS = 2000L
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.connections.model
|
||||
|
||||
/** JVM-specific implementation of [UsbDeviceData] wrapping the serial port name. */
|
||||
data class JvmUsbDeviceData(val portName: String) : UsbDeviceData
|
||||
@@ -48,6 +48,7 @@ ktor = "3.4.1"
|
||||
|
||||
# Other
|
||||
aboutlibraries = "13.2.1"
|
||||
jserialcomm = "2.11.0"
|
||||
coil = "3.4.0"
|
||||
datadog-gradle = "1.24.0"
|
||||
dd-sdk-android = "3.7.1"
|
||||
@@ -219,6 +220,7 @@ nordic-dfu = { module = "no.nordicsemi.android:dfu", version.ref = "nordic-dfu"
|
||||
kable-core = { module = "com.juul.kable:kable-core", version.ref = "kable" }
|
||||
|
||||
org-eclipse-paho-client-mqttv3 = { module = "org.eclipse.paho:org.eclipse.paho.client.mqttv3", version = "1.2.5" }
|
||||
jserialcomm = { module = "com.fazecast:jSerialComm", version.ref = "jserialcomm" }
|
||||
okio = { module = "com.squareup.okio:okio", version.ref = "okio" }
|
||||
osmbonuspack = { module = "com.github.MKergall:osmbonuspack", version = "6.9.0" }
|
||||
osmdroid-android = { module = "org.osmdroid:osmdroid-android", version.ref = "osmdroid-android" }
|
||||
|
||||
Reference in New Issue
Block a user