Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
4b85c61
docs: update roadmap and technical guidelines for Desktop and KMP
jamesarich Mar 18, 2026
69efb07
feat(network): Add jSerialComm dependency to jvmMain
jamesarich Mar 18, 2026
b42538a
conductor(plan): Mark task 'Add the jSerialComm library' as complete
jamesarich Mar 18, 2026
f01f412
feat(network): Create jvmMain stub implementation for SerialTransport
jamesarich Mar 18, 2026
74fd4e1
conductor(plan): Mark task 'Create a jvmMain stub implementation for …
jamesarich Mar 18, 2026
407f72d
conductor(checkpoint): Checkpoint end of Phase 1
jamesarich Mar 18, 2026
c169590
conductor(plan): Mark phase 'Phase 1: JVM Setup & Dependency Integrat…
jamesarich Mar 18, 2026
a3feb28
feat(network): Implement port discovery using jSerialComm
jamesarich Mar 18, 2026
2e69322
conductor(plan): Mark task 'Implement port discovery using jSerialCom…
jamesarich Mar 18, 2026
70294fc
feat(network): Implement connect/disconnect logic for serial port
jamesarich Mar 18, 2026
60ea7a1
conductor(plan): Mark task 'Implement connect/disconnect logic' as co…
jamesarich Mar 18, 2026
9ac6def
feat(network): Map serial port IO to KMP stream framing logic
jamesarich Mar 18, 2026
79a57b0
conductor(plan): Mark task 'Map the input/output streams' as complete
jamesarich Mar 18, 2026
22627b1
conductor(checkpoint): Checkpoint end of Phase 2
jamesarich Mar 18, 2026
a9f973d
conductor(plan): Mark phase 'Phase 2: Serial Port Scanning & Connecti…
jamesarich Mar 18, 2026
6b3a385
feat(connections): Poll SerialTransport for available ports in UI
jamesarich Mar 18, 2026
72d046a
conductor(plan): Mark task 'Update the feature:connections UI' as com…
jamesarich Mar 18, 2026
2f39315
feat(desktop): Wire user's serial port selection to initiate connection
jamesarich Mar 18, 2026
3cad720
conductor(plan): Mark task 'Wire the user's serial port selection' as…
jamesarich Mar 18, 2026
8768aaa
conductor(checkpoint): Checkpoint end of Phase 3
jamesarich Mar 18, 2026
e24862f
fix(network): Handle SerialPortTimeoutException in read loop
jamesarich Mar 18, 2026
4568bb8
fix(network): Assert DTR/RTS on serial open and use chunked reads
jamesarich Mar 18, 2026
afaaba7
fix(network): Suppress timeout exceptions on jSerialComm reads
jamesarich Mar 18, 2026
87b2308
conductor(checkpoint): Checkpoint end of Phase 4
jamesarich Mar 18, 2026
262c94d
conductor(plan): Mark phase 'Phase 4: Validation' as complete
jamesarich Mar 18, 2026
72e9e56
chore(conductor): Mark track 'Desktop Serial/USB Transport via jSeria…
jamesarich Mar 18, 2026
78da4fd
docs(conductor): Synchronize docs for track 'Desktop Serial/USB Trans…
jamesarich Mar 18, 2026
df6a50c
style(network): Fix detekt violations in SerialTransport
jamesarich Mar 18, 2026
46b0cc3
style(connections): Fix detekt violations in JvmUsbScanner
jamesarich Mar 18, 2026
a88e3ce
conductor(plan): Mark task 'Apply review suggestions' as complete
jamesarich Mar 18, 2026
e8ecaf6
chore(conductor): Archive track 'Desktop Serial/USB Transport via jSe…
jamesarich Mar 18, 2026
1d1d24d
chore: add license headers and format code in connections and desktop…
jamesarich Mar 18, 2026
faae843
docs(roadmap): Mark Desktop Serial/USB transport as Done
jamesarich Mar 18, 2026
8063272
fix(conductor): Apply review suggestions
jamesarich Mar 18, 2026
6f656a5
fix(network): improve coroutine cancellation handling in SerialTransport
jamesarich Mar 18, 2026
6f9b6fd
refactor(core): improve SerialTransport exception logging and cycloma…
jamesarich Mar 18, 2026
a65144e
docs: reflect desktop serial transport completion
jamesarich Mar 18, 2026
2227021
docs: add BLE to list of supported desktop transports
jamesarich Mar 18, 2026
9adca28
docs: mark desktop serial transport as done in roadmap and update Con…
jamesarich Mar 18, 2026
af4894d
docs: update desktop README to reflect complete transport support
jamesarich Mar 18, 2026
130f088
docs: add BLE transport to desktop README roadmap
jamesarich Mar 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand All @@ -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
Expand All @@ -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.
Expand Down
8 changes: 4 additions & 4 deletions GEMINI.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand All @@ -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
Expand All @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions conductor/archive/desktop_serial_transport_20260317/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Track desktop_serial_transport_20260317 Context

- [Specification](./spec.md)
- [Implementation Plan](./plan.md)
- [Metadata](./metadata.json)
Original file line number Diff line number Diff line change
@@ -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 changes: 21 additions & 0 deletions conductor/archive/desktop_serial_transport_20260317/plan.md
Original file line number Diff line number Diff line change
@@ -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 changes: 20 additions & 0 deletions conductor/archive/desktop_serial_transport_20260317/spec.md
Original file line number Diff line number Diff line change
@@ -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`.
10 changes: 9 additions & 1 deletion conductor/tech-stack.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
8 changes: 7 additions & 1 deletion core/network/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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) }
}
}
Original file line number Diff line number Diff line change
@@ -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
Comment on lines +64 to +67
val buffer = ByteArray(READ_BUFFER_SIZE)
try {
var reading = true
while (isActive && port.isOpen && reading) {
try {
val numRead = input.read(buffer)
Comment on lines +71 to +73
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 }
}
}
Loading
Loading