feat: implement XModem file transfers and enhance BLE connection robustness (#4959)
Some checks are pending
Dependency Submission / dependency-submission (push) Waiting to run
Main CI (Verify & Build) / validate-and-build (push) Waiting to run
Main Push Changelog / Generate main push changelog (push) Waiting to run

This commit is contained in:
James Rich 2026-03-30 22:49:31 -05:00 committed by GitHub
parent ae4465d7c8
commit c75c9b34d6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 1100 additions and 120 deletions

View file

@ -18,6 +18,7 @@ package org.meshtastic.core.repository
import kotlinx.coroutines.CoroutineScope
import org.meshtastic.proto.DeviceMetadata
import org.meshtastic.proto.FileInfo
import org.meshtastic.proto.MyNodeInfo
import org.meshtastic.proto.NodeInfo
@ -35,6 +36,14 @@ interface MeshConfigFlowManager {
/** Handles received node information. */
fun handleNodeInfo(info: NodeInfo)
/**
* Handles a [FileInfo] packet received during STATE_SEND_FILEMANIFEST.
*
* Each packet describes one file available on the device. Accumulated into [RadioConfigRepository.fileManifestFlow]
* and cleared at the start of each new handshake.
*/
fun handleFileInfo(info: FileInfo)
/** Returns the number of nodes received in the current stage. */
val newNodeCount: Int

View file

@ -20,6 +20,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow
import org.meshtastic.proto.Channel
import org.meshtastic.proto.Config
import org.meshtastic.proto.DeviceUIConfig
import org.meshtastic.proto.LocalConfig
import org.meshtastic.proto.LocalModuleConfig
import org.meshtastic.proto.ModuleConfig
@ -43,4 +44,10 @@ interface MeshConfigHandler {
/** Handles a received channel configuration. */
fun handleChannel(channel: Channel)
/**
* Handles the [DeviceUIConfig] received during the config handshake (STATE_SEND_UIDATA). This arrives as the 2nd
* packet in every handshake, immediately after my_info.
*/
fun handleDeviceUIConfig(config: DeviceUIConfig)
}

View file

@ -43,4 +43,7 @@ interface MeshRouter {
/** Access to the action handler. */
val actionHandler: MeshActionHandler
/** Access to the XModem file-transfer manager. */
val xmodemManager: XModemManager
}

View file

@ -22,10 +22,13 @@ import org.meshtastic.proto.ChannelSet
import org.meshtastic.proto.ChannelSettings
import org.meshtastic.proto.Config
import org.meshtastic.proto.DeviceProfile
import org.meshtastic.proto.DeviceUIConfig
import org.meshtastic.proto.FileInfo
import org.meshtastic.proto.LocalConfig
import org.meshtastic.proto.LocalModuleConfig
import org.meshtastic.proto.ModuleConfig
@Suppress("TooManyFunctions")
interface RadioConfigRepository {
/** Flow representing the [ChannelSet] data store. */
val channelSetFlow: Flow<ChannelSet>
@ -59,4 +62,30 @@ interface RadioConfigRepository {
/** Flow representing the combined [DeviceProfile] protobuf. */
val deviceProfileFlow: Flow<DeviceProfile>
/**
* Flow of the device's UI configuration, populated from [DeviceUIConfig] during the config handshake
* (STATE_SEND_UIDATA 2nd packet in every handshake). Null until the first handshake completes or after
* [clearDeviceUIConfig] is called.
*/
val deviceUIConfigFlow: Flow<DeviceUIConfig?>
/** Stores the [DeviceUIConfig] received from the device. */
suspend fun setDeviceUIConfig(config: DeviceUIConfig)
/** Clears the stored [DeviceUIConfig]; called at the start of each new handshake. */
suspend fun clearDeviceUIConfig()
/**
* Flow of [FileInfo] packets accumulated during STATE_SEND_FILEMANIFEST.
*
* Cleared at the start of each new handshake via [clearFileManifest].
*/
val fileManifestFlow: Flow<List<FileInfo>>
/** Appends a single [FileInfo] entry to [fileManifestFlow]. */
suspend fun addFileInfo(info: FileInfo)
/** Clears the accumulated file manifest; called at the start of each new handshake. */
suspend fun clearFileManifest()
}

View file

@ -0,0 +1,33 @@
/*
* Copyright (c) 2025-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.repository
/** A file received via an XModem transfer from the connected device. */
data class XModemFile(
/** Filename as set via [XModemManager.setTransferName] before the transfer started. */
val name: String,
/** Raw bytes of the received file (trailing CTRLZ padding stripped). */
val data: ByteArray,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is XModemFile) return false
return name == other.name && data.contentEquals(other.data)
}
override fun hashCode(): Int = 31 * name.hashCode() + data.contentHashCode()
}

View file

@ -0,0 +1,54 @@
/*
* Copyright (c) 2025-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.repository
import kotlinx.coroutines.flow.Flow
import org.meshtastic.proto.XModem
/**
* Handles the XModem-CRC receive protocol for file transfers from the connected device.
*
* The device (sender) initiates transfers in response to admin file-read requests. The Android client (receiver)
* acknowledges each 128-byte block and signals end-of-transfer acceptance.
*
* Usage:
* 1. Optionally call [setTransferName] with the filename being requested so the emitted [XModemFile] is labelled
* correctly.
* 2. Route every [FromRadio.xmodemPacket] here via [handleIncomingXModem].
* 3. Collect [fileTransferFlow] to receive completed files.
*/
interface XModemManager {
/**
* Hot flow that emits once per completed transfer. Backpressure is handled by a small buffer; older transfers are
* dropped if the consumer is slow.
*/
val fileTransferFlow: Flow<XModemFile>
/**
* Sets the name to attach to the next completed transfer.
*
* Call this immediately before (or after) sending the admin file-read request to the device so the emitted
* [XModemFile] is labelled with the correct path.
*/
fun setTransferName(name: String)
/** Routes an incoming XModem packet from the device to the receive state machine. */
fun handleIncomingXModem(packet: XModem)
/** Cancels any in-progress transfer and sends a CAN control byte to the device. */
fun cancel()
}