refactor(service): harden KMP service layer — database init, connection reliability, handler decomposition (#4992)

This commit is contained in:
James Rich 2026-04-04 13:07:44 -05:00 committed by GitHub
parent e111b61e4e
commit 6af3ad6f0c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
62 changed files with 3808 additions and 735 deletions

View file

@ -0,0 +1,30 @@
/*
* 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 org.meshtastic.proto.MeshPacket
/** Interface for handling admin messages from the mesh (config, metadata, session passkey). */
interface AdminPacketHandler {
/**
* Processes an admin message packet.
*
* @param packet The received mesh packet.
* @param myNodeNum The local node number.
*/
fun handleAdminMessage(packet: MeshPacket, myNodeNum: Int)
}

View file

@ -56,6 +56,21 @@ interface CommandSender {
initFn: () -> AdminMessage,
)
/**
* Sends an admin message and suspends until the radio acknowledges it.
*
* This is used when the caller needs to guarantee a packet has been accepted by the radio before proceeding, such
* as sending a shared contact before the first DM to a node.
*
* @return `true` if the radio accepted the packet, `false` on timeout or failure.
*/
suspend fun sendAdminAwait(
destNum: Int,
requestId: Int = generatePacketId(),
wantResponse: Boolean = false,
initFn: () -> AdminMessage,
): Boolean
/** Sends our current position to the mesh. */
fun sendPosition(pos: org.meshtastic.proto.Position, destNum: Int? = null, wantResponse: Boolean = false)

View file

@ -29,7 +29,7 @@ interface MeshActionHandler {
fun start(scope: CoroutineScope)
/** Processes a service action from the UI. */
fun onServiceAction(action: ServiceAction)
suspend fun onServiceAction(action: ServiceAction)
/** Sets the owner of the local node. */
fun handleSetOwner(u: MeshUser, myNodeNum: Int)

View file

@ -54,8 +54,11 @@ interface NodeManager : NodeIdLookup {
/** Starts the node manager with the given coroutine scope. */
fun start(scope: CoroutineScope)
/** The local node number. */
var myNodeNum: Int?
/** The local node number as a thread-safe [StateFlow]. */
val myNodeNum: StateFlow<Int?>
/** Sets the local node number. */
fun setMyNodeNum(num: Int?)
/** Loads the cached node database from the repository. */
fun loadCachedNodeDB()

View file

@ -32,6 +32,17 @@ interface PacketHandler {
/** Adds a mesh packet to the queue for sending. */
fun sendToRadio(packet: MeshPacket)
/**
* Adds a mesh packet to the queue and suspends until the radio acknowledges it via [QueueStatus].
*
* Unlike [sendToRadio], which is fire-and-forget, this method provides back-pressure so the caller can ensure a
* packet has been accepted by the radio before proceeding. This is critical for operations where ordering matters
* (e.g., sending a shared contact before the first DM).
*
* @return `true` if the radio accepted the packet, `false` on timeout or failure.
*/
suspend fun sendToRadioAndAwait(packet: MeshPacket): Boolean
/** Processes queue status updates from the radio. */
fun handleQueueStatus(queueStatus: QueueStatus)

View file

@ -68,6 +68,9 @@ interface RadioInterfaceService {
/** Called by an interface when it has received raw data from the radio. */
fun handleFromRadio(bytes: ByteArray)
/** Flow of user-facing connection error messages (e.g. permission failures). */
val connectionError: SharedFlow<String>
/** The scope in which interface-related coroutines should run. */
val serviceScope: CoroutineScope
}

View file

@ -0,0 +1,36 @@
/*
* 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.CoroutineScope
import org.meshtastic.core.model.DataPacket
import org.meshtastic.proto.MeshPacket
/** Interface for handling telemetry packets from the mesh, including battery notifications. */
interface TelemetryPacketHandler {
/** Starts the handler with the given coroutine scope. */
fun start(scope: CoroutineScope)
/**
* Processes a telemetry packet.
*
* @param packet The received mesh packet.
* @param dataPacket The decoded data packet.
* @param myNodeNum The local node number.
*/
fun handleTelemetry(packet: MeshPacket, dataPacket: DataPacket, myNodeNum: Int)
}

View file

@ -130,7 +130,10 @@ class SendMessageUseCaseImpl(
private suspend fun sendSharedContact(node: Node) {
try {
radioController.sendSharedContact(node.num)
val accepted = radioController.sendSharedContact(node.num)
if (!accepted) {
Logger.w { "Shared contact for node ${node.num} was not acknowledged by the radio" }
}
} catch (ex: Exception) {
Logger.e(ex) { "Send shared contact error" }
}