diff --git a/AGENTS.md b/AGENTS.md index b35b8d208..def726573 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. diff --git a/GEMINI.md b/GEMINI.md index b35b8d208..def726573 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -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. diff --git a/conductor/archive/desktop_serial_transport_20260317/index.md b/conductor/archive/desktop_serial_transport_20260317/index.md new file mode 100644 index 000000000..1cbe07406 --- /dev/null +++ b/conductor/archive/desktop_serial_transport_20260317/index.md @@ -0,0 +1,5 @@ +# Track desktop_serial_transport_20260317 Context + +- [Specification](./spec.md) +- [Implementation Plan](./plan.md) +- [Metadata](./metadata.json) \ No newline at end of file diff --git a/conductor/archive/desktop_serial_transport_20260317/metadata.json b/conductor/archive/desktop_serial_transport_20260317/metadata.json new file mode 100644 index 000000000..3d1257289 --- /dev/null +++ b/conductor/archive/desktop_serial_transport_20260317/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." +} \ No newline at end of file diff --git a/conductor/archive/desktop_serial_transport_20260317/plan.md b/conductor/archive/desktop_serial_transport_20260317/plan.md new file mode 100644 index 000000000..3d55c7380 --- /dev/null +++ b/conductor/archive/desktop_serial_transport_20260317/plan.md @@ -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] diff --git a/conductor/archive/desktop_serial_transport_20260317/spec.md b/conductor/archive/desktop_serial_transport_20260317/spec.md new file mode 100644 index 000000000..04ff68481 --- /dev/null +++ b/conductor/archive/desktop_serial_transport_20260317/spec.md @@ -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`. diff --git a/conductor/tech-stack.md b/conductor/tech-stack.md index c6ea7ebbd..eb3244a32 100644 --- a/conductor/tech-stack.md +++ b/conductor/tech-stack.md @@ -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. \ No newline at end of file +- **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. \ No newline at end of file diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts index dde171d11..a499f3644 100644 --- a/core/network/build.gradle.kts +++ b/core/network/build.gradle.kts @@ -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) } } } diff --git a/core/network/src/jvmMain/kotlin/org/meshtastic/core/network/SerialTransport.kt b/core/network/src/jvmMain/kotlin/org/meshtastic/core/network/SerialTransport.kt new file mode 100644 index 000000000..7e504f893 --- /dev/null +++ b/core/network/src/jvmMain/kotlin/org/meshtastic/core/network/SerialTransport.kt @@ -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 . + */ +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 = SerialPort.getCommPorts().map { it.systemPortName } + } +} diff --git a/core/network/src/jvmTest/kotlin/org/meshtastic/core/network/SerialTransportTest.kt b/core/network/src/jvmTest/kotlin/org/meshtastic/core/network/SerialTransportTest.kt new file mode 100644 index 000000000..ab1e408ae --- /dev/null +++ b/core/network/src/jvmTest/kotlin/org/meshtastic/core/network/SerialTransportTest.kt @@ -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 . + */ +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() + } +} diff --git a/desktop/README.md b/desktop/README.md index 51485da04..14a66457f 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -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 diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/radio/DesktopRadioInterfaceService.kt b/desktop/src/main/kotlin/org/meshtastic/desktop/radio/DesktopRadioInterfaceService.kt index 22d47e012..c4defd7d1 100644 --- a/desktop/src/main/kotlin/org/meshtastic/desktop/radio/DesktopRadioInterfaceService.kt +++ b/desktop/src/main/kotlin/org/meshtastic/desktop/radio/DesktopRadioInterfaceService.kt @@ -56,7 +56,11 @@ class DesktopRadioInterfaceService( ) : RadioInterfaceService { override val supportedDeviceTypes: List = - 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.Disconnected) override val connectionState: StateFlow = _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()) diff --git a/docs/kmp-status.md b/docs/kmp-status.md index 2f5f2861f..4e9811a3e 100644 --- a/docs/kmp-status.md +++ b/docs/kmp-status.md @@ -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 | diff --git a/docs/roadmap.md b/docs/roadmap.md index 01fb9402e..0dd6adc5e 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -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., `