refactor: Naming cleanup for clarity and Kotlin conventions

Audit-driven renames:
- DirectRadioControllerImpl -> RadioControllerImpl: the "Direct" prefix
  was vestigial (it once contrasted with the AIDL-routed
  AndroidRadioControllerImpl, now deleted); it is the sole impl and now
  matches its parts (AdminControllerImpl, etc.).
- RadioController.getPacketId() -> generatePacketId(): a `get`-prefixed
  function that generates a fresh id each call violates the
  getter-is-idempotent convention; also aligns with
  CommandSender.generatePacketId().
- RequestController -> QueryController (+ Impl, + the `requestController`
  params/mocks): clearer intent for the pull/query surface; "request" was
  generic.
- RequestTimer param `label` -> `logLabel`.
- AdminControllerImpl DEFAULT_REBOOT_DELAY -> DEFAULT_DELAY_SECONDS
  (shared by reboot + shutdown; conveys the unit).

Interface-consumer-safe; docs/READMEs/architecture guide updated to match.
Koin graph verifies on Android + desktop; affected test suites green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
James Rich
2026-05-29 14:37:54 -05:00
parent 9dc1e500a4
commit 94bcec85b3
26 changed files with 88 additions and 87 deletions

View File

@@ -42,14 +42,14 @@ internal class RequestTimer {
/**
* Consumes the start time recorded for [requestId] and appends a `Duration: N s` line to [text], logging completion
* under [label]. Returns [text] unchanged when no start time was recorded for the id.
* under [logLabel]. Returns [text] unchanged when no start time was recorded for the id.
*/
fun appendDuration(requestId: Int, text: String, label: String): String {
fun appendDuration(requestId: Int, text: String, logLabel: String): String {
val start = startTimes.value[requestId]
startTimes.update { it.remove(requestId) }
if (start == null) return text
val seconds = (nowMillis - start) / MILLIS_PER_SECOND
Logger.i { "$label $requestId complete in $seconds s" }
Logger.i { "$logLabel $requestId complete in $seconds s" }
return "$text\n\nDuration: ${NumberFormatter.format(seconds, 1)} s"
}

View File

@@ -26,7 +26,7 @@ class RequestTimerTest {
fun appendDuration_withoutStart_returnsTextUnchanged() {
val timer = RequestTimer()
assertEquals("base", timer.appendDuration(requestId = 1, text = "base", label = "Test"))
assertEquals("base", timer.appendDuration(requestId = 1, text = "base", logLabel = "Test"))
}
@Test
@@ -34,7 +34,7 @@ class RequestTimerTest {
val timer = RequestTimer()
timer.start(requestId = 7)
val result = timer.appendDuration(requestId = 7, text = "base", label = "Test")
val result = timer.appendDuration(requestId = 7, text = "base", logLabel = "Test")
assertTrue(result.startsWith("base\n\nDuration: "), "expected a duration suffix, got: $result")
assertTrue(result.endsWith(" s"))
@@ -45,9 +45,9 @@ class RequestTimerTest {
val timer = RequestTimer()
timer.start(requestId = 7)
timer.appendDuration(requestId = 7, text = "first", label = "Test")
timer.appendDuration(requestId = 7, text = "first", logLabel = "Test")
// The start time is single-use; a second response for the same id gets no duration.
assertEquals("second", timer.appendDuration(requestId = 7, text = "second", label = "Test"))
assertEquals("second", timer.appendDuration(requestId = 7, text = "second", logLabel = "Test"))
}
@Test
@@ -57,7 +57,7 @@ class RequestTimerTest {
timer.start(requestId = 2)
// Consuming one id must not affect the other.
timer.appendDuration(requestId = 1, text = "a", label = "Test")
assertTrue(timer.appendDuration(requestId = 2, text = "b", label = "Test").contains("Duration: "))
timer.appendDuration(requestId = 1, text = "a", logLabel = "Test")
assertTrue(timer.appendDuration(requestId = 2, text = "b", logLabel = "Test").contains("Duration: "))
}
}

View File

@@ -39,7 +39,7 @@ constructor(
* @return The packet ID of the request.
*/
open suspend fun reboot(destNum: Int): Int {
val packetId = radioController.getPacketId()
val packetId = radioController.generatePacketId()
radioController.reboot(destNum, packetId)
return packetId
}
@@ -51,7 +51,7 @@ constructor(
* @return The packet ID of the request.
*/
open suspend fun shutdown(destNum: Int): Int {
val packetId = radioController.getPacketId()
val packetId = radioController.generatePacketId()
radioController.shutdown(destNum, packetId)
return packetId
}
@@ -64,7 +64,7 @@ constructor(
* @return The packet ID of the request.
*/
open suspend fun factoryReset(destNum: Int, isLocal: Boolean): Int {
val packetId = radioController.getPacketId()
val packetId = radioController.generatePacketId()
radioController.factoryReset(destNum, packetId)
if (isLocal) {
@@ -84,7 +84,7 @@ constructor(
* @return The packet ID of the request.
*/
open suspend fun nodedbReset(destNum: Int, preserveFavorites: Boolean, isLocal: Boolean): Int {
val packetId = radioController.getPacketId()
val packetId = radioController.generatePacketId()
radioController.nodedbReset(destNum, packetId, preserveFavorites)
if (isLocal) {

View File

@@ -58,7 +58,7 @@ constructor(
nodeRepository.deleteNodes(nodeNums)
for (nodeNum in nodeNums) {
val packetId = radioController.getPacketId()
val packetId = radioController.generatePacketId()
radioController.removeByNodenum(packetId, nodeNum)
}
}

View File

@@ -35,7 +35,7 @@ open class RadioConfigUseCase constructor(private val radioController: RadioCont
* @return The packet ID of the request.
*/
open suspend fun setOwner(destNum: Int, user: User): Int {
val packetId = radioController.getPacketId()
val packetId = radioController.generatePacketId()
radioController.setOwner(destNum, user, packetId)
return packetId
}
@@ -47,7 +47,7 @@ open class RadioConfigUseCase constructor(private val radioController: RadioCont
* @return The packet ID of the request.
*/
open suspend fun getOwner(destNum: Int): Int {
val packetId = radioController.getPacketId()
val packetId = radioController.generatePacketId()
radioController.getOwner(destNum, packetId)
return packetId
}
@@ -60,7 +60,7 @@ open class RadioConfigUseCase constructor(private val radioController: RadioCont
* @return The packet ID of the request.
*/
open suspend fun setConfig(destNum: Int, config: Config): Int {
val packetId = radioController.getPacketId()
val packetId = radioController.generatePacketId()
radioController.setConfig(destNum, config, packetId)
return packetId
}
@@ -73,7 +73,7 @@ open class RadioConfigUseCase constructor(private val radioController: RadioCont
* @return The packet ID of the request.
*/
open suspend fun getConfig(destNum: Int, configType: Int): Int {
val packetId = radioController.getPacketId()
val packetId = radioController.generatePacketId()
radioController.getConfig(destNum, configType, packetId)
return packetId
}
@@ -86,7 +86,7 @@ open class RadioConfigUseCase constructor(private val radioController: RadioCont
* @return The packet ID of the request.
*/
open suspend fun setModuleConfig(destNum: Int, config: ModuleConfig): Int {
val packetId = radioController.getPacketId()
val packetId = radioController.generatePacketId()
radioController.setModuleConfig(destNum, config, packetId)
return packetId
}
@@ -99,7 +99,7 @@ open class RadioConfigUseCase constructor(private val radioController: RadioCont
* @return The packet ID of the request.
*/
open suspend fun getModuleConfig(destNum: Int, moduleConfigType: Int): Int {
val packetId = radioController.getPacketId()
val packetId = radioController.generatePacketId()
radioController.getModuleConfig(destNum, moduleConfigType, packetId)
return packetId
}
@@ -112,7 +112,7 @@ open class RadioConfigUseCase constructor(private val radioController: RadioCont
* @return The packet ID of the request.
*/
open suspend fun getChannel(destNum: Int, index: Int): Int {
val packetId = radioController.getPacketId()
val packetId = radioController.generatePacketId()
radioController.getChannel(destNum, index, packetId)
return packetId
}
@@ -125,7 +125,7 @@ open class RadioConfigUseCase constructor(private val radioController: RadioCont
* @return The packet ID of the request.
*/
open suspend fun setRemoteChannel(destNum: Int, channel: org.meshtastic.proto.Channel): Int {
val packetId = radioController.getPacketId()
val packetId = radioController.generatePacketId()
radioController.setRemoteChannel(destNum, channel, packetId)
return packetId
}
@@ -152,7 +152,7 @@ open class RadioConfigUseCase constructor(private val radioController: RadioCont
* @return The packet ID of the request.
*/
open suspend fun getRingtone(destNum: Int): Int {
val packetId = radioController.getPacketId()
val packetId = radioController.generatePacketId()
radioController.getRingtone(destNum, packetId)
return packetId
}
@@ -169,7 +169,7 @@ open class RadioConfigUseCase constructor(private val radioController: RadioCont
* @return The packet ID of the request.
*/
open suspend fun getCannedMessages(destNum: Int): Int {
val packetId = radioController.getPacketId()
val packetId = radioController.generatePacketId()
radioController.getCannedMessages(destNum, packetId)
return packetId
}
@@ -181,7 +181,7 @@ open class RadioConfigUseCase constructor(private val radioController: RadioCont
* @return The packet ID of the request.
*/
open suspend fun getDeviceConnectionStatus(destNum: Int): Int {
val packetId = radioController.getPacketId()
val packetId = radioController.generatePacketId()
radioController.getDeviceConnectionStatus(destNum, packetId)
return packetId
}

View File

@@ -36,7 +36,7 @@ src/
│ ├── AdminController.kt ← config, channels, owner, device lifecycle, editSettings
│ ├── MessagingController.kt ← send packets, reactions, contacts
│ ├── NodeController.kt ← favorite, ignore, mute, remove nodes
│ ├── RequestController.kt ← telemetry, traceroute, position queries
│ ├── QueryController.kt ← telemetry, traceroute, position queries
│ ├── CommandSender.kt
│ ├── AdminPacketHandler.kt
│ ├── FromRadioPacketHandler.kt
@@ -130,7 +130,7 @@ connection state). Handlers inject `ServiceStateWriter` for mutations. The full
`ServiceRepository` union is still available for backward compatibility.
Radio commands are issued through `RadioController` (a composite of `AdminController`,
`MessagingController`, `NodeController`, `RequestController`) rather than an action/intent bus.
`MessagingController`, `NodeController`, `QueryController`) rather than an action/intent bus.
### `NodeRepository`

View File

@@ -19,14 +19,14 @@ package org.meshtastic.core.repository
import org.meshtastic.core.model.Position
/**
* Mesh request operations position, traceroute, telemetry, user info, and metadata queries.
* Mesh query operations position, traceroute, telemetry, user info, and metadata.
*
* These are "pull" operations that request data from remote nodes. When the SDK is adopted, implementations delegate to
* `RadioClient.telemetry` and `RadioClient.routing` sub-APIs.
*
* @see RadioController which extends this interface for backward compatibility
*/
interface RequestController {
interface QueryController {
/** Requests device metadata from a remote node. */
suspend fun refreshMetadata(destNum: Int)

View File

@@ -30,7 +30,7 @@ import org.meshtastic.proto.ClientNotification
* - [AdminController] — config, channels, owner, device lifecycle (→ SDK `AdminApi`)
* - [MessagingController] — send packets, reactions, contacts (→ SDK `RadioClient.send*`)
* - [NodeController] — favorite, ignore, mute, remove nodes (→ SDK `AdminApi` node ops)
* - [RequestController] — telemetry, traceroute, position queries (→ SDK `TelemetryApi` / `RoutingApi`)
* - [QueryController] — telemetry, traceroute, position queries (→ SDK `TelemetryApi` / `RoutingApi`)
*
* When migrating to the SDK, each sub-interface becomes a thin adapter over the corresponding SDK API. The composite
* [RadioController] can then be deprecated and consumers migrated to the narrower interfaces one at a time.
@@ -39,7 +39,7 @@ interface RadioController :
AdminController,
MessagingController,
NodeController,
RequestController,
QueryController,
ConnectionStateProvider {
/**
* Flow of notifications from the radio client.
@@ -56,7 +56,7 @@ interface RadioController :
*
* @return A unique 32-bit integer.
*/
fun getPacketId(): Int
fun generatePacketId(): Int
/** Starts providing the phone's location to the mesh. */
fun startProvideLocation()

View File

@@ -17,8 +17,8 @@ A high-level repository that wraps the service connection and exposes reactive `
### 3. `ConnectionState`
Represents the current state of the radio connection (`Connected`, `Disconnected`, `DeviceSleep`, etc.).
### 4. `DirectRadioControllerImpl`
The in-process `RadioController` composition root (Desktop, iOS, and single-process Android). It assembles four focused sub-controllers — `AdminControllerImpl`, `MessagingControllerImpl`, `NodeControllerImpl`, `RequestControllerImpl` — via Kotlin interface delegation, and owns the cross-cutting concerns (connection state, packet-id, location, device-address switching). Commands are direct suspend calls to `CommandSender`; admin sends are fire-and-forget (the device is the source of truth). Config writes use the `editSettings { }` transaction.
### 4. `RadioControllerImpl`
The in-process `RadioController` composition root (Desktop, iOS, and single-process Android). It assembles four focused sub-controllers — `AdminControllerImpl`, `MessagingControllerImpl`, `NodeControllerImpl`, `QueryControllerImpl` — via Kotlin interface delegation, and owns the cross-cutting concerns (connection state, packet-id, location, device-address switching). Commands are direct suspend calls to `CommandSender`; admin sends are fire-and-forget (the device is the source of truth). Config writes use the `editSettings { }` transaction.
## Dependency Graph

View File

@@ -36,14 +36,14 @@ import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.NotificationManager
import org.meshtastic.core.repository.PacketRepository
import org.meshtastic.core.repository.PlatformAnalytics
import org.meshtastic.core.repository.QueryController
import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.core.repository.RadioController
import org.meshtastic.core.repository.RadioInterfaceService
import org.meshtastic.core.repository.RequestController
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.repository.UiPrefs
import org.meshtastic.core.service.DirectRadioControllerImpl
import org.meshtastic.core.service.MeshService
import org.meshtastic.core.service.RadioControllerImpl
import org.meshtastic.core.service.startService
@Module
@@ -57,7 +57,7 @@ class CoreServiceAndroidModule {
AdminController::class,
MessagingController::class,
NodeController::class,
RequestController::class,
QueryController::class,
],
)
fun radioController(
@@ -78,7 +78,7 @@ class CoreServiceAndroidModule {
messageProcessor: Lazy<MeshMessageProcessor>,
radioConfigRepository: RadioConfigRepository,
@Named("ServiceScope") scope: CoroutineScope,
): RadioController = DirectRadioControllerImpl(
): RadioController = RadioControllerImpl(
serviceRepository = serviceRepository,
nodeRepository = nodeRepository,
commandSender = commandSender,

View File

@@ -38,10 +38,10 @@ import org.meshtastic.proto.User
* [AdminController] implementation: local/remote configuration, channels, owner, device lifecycle, and the
* [editSettings] transaction.
*
* Focused collaborator of [DirectRadioControllerImpl]. Builds [AdminMessage] protos directly and delegates to
* [CommandSender] for transport, mirroring the SDK's `AdminApiImpl` pattern. Config/channel writes use fire-and-forget
* optimistic local persistence ([handledLaunch]): the device is the source of truth and re-sends its full config on
* every connection, so persistence is a cache optimization, not a correctness requirement.
* Focused collaborator of [RadioControllerImpl]. Builds [AdminMessage] protos directly and delegates to [CommandSender]
* for transport, mirroring the SDK's `AdminApiImpl` pattern. Config/channel writes use fire-and-forget optimistic local
* persistence ([handledLaunch]): the device is the source of truth and re-sends its full config on every connection, so
* persistence is a cache optimization, not a correctness requirement.
*/
@Suppress("TooManyFunctions")
internal class AdminControllerImpl(
@@ -159,7 +159,7 @@ internal class AdminControllerImpl(
override suspend fun reboot(destNum: Int, packetId: Int) {
Logger.i { "Reboot requested for node $destNum" }
commandSender.sendAdmin(destNum, packetId) { AdminMessage(reboot_seconds = DEFAULT_REBOOT_DELAY) }
commandSender.sendAdmin(destNum, packetId) { AdminMessage(reboot_seconds = DEFAULT_DELAY_SECONDS) }
}
override suspend fun rebootToDfu(nodeNum: Int) {
@@ -174,7 +174,7 @@ internal class AdminControllerImpl(
}
override suspend fun shutdown(destNum: Int, packetId: Int) {
commandSender.sendAdmin(destNum, packetId) { AdminMessage(shutdown_seconds = DEFAULT_REBOOT_DELAY) }
commandSender.sendAdmin(destNum, packetId) { AdminMessage(shutdown_seconds = DEFAULT_DELAY_SECONDS) }
}
override suspend fun factoryReset(destNum: Int, packetId: Int) {
@@ -211,6 +211,6 @@ internal class AdminControllerImpl(
}
private companion object {
private const val DEFAULT_REBOOT_DELAY = 5
private const val DEFAULT_DELAY_SECONDS = 5
}
}

View File

@@ -42,7 +42,7 @@ import org.meshtastic.proto.User
/**
* [MessagingController] implementation: sends data packets, reactions, and shared contacts.
*
* Focused collaborator of [DirectRadioControllerImpl]. Mirrors the SDK's `RadioClient.send*` surface — when the SDK is
* Focused collaborator of [RadioControllerImpl]. Mirrors the SDK's `RadioClient.send*` surface — when the SDK is
* adopted this becomes a thin adapter over `RadioClient`.
*/
internal class MessagingControllerImpl(

View File

@@ -27,8 +27,8 @@ import org.meshtastic.proto.AdminMessage
/**
* [NodeController] implementation: favorite, ignore, mute, and remove nodes.
*
* Focused collaborator of [DirectRadioControllerImpl]. Favorite/ignore are idempotent (no-op when already in the
* requested state), mirroring the SDK's `AdminApi.setFavorite`/`setIgnored`.
* Focused collaborator of [RadioControllerImpl]. Favorite/ignore are idempotent (no-op when already in the requested
* state), mirroring the SDK's `AdminApi.setFavorite`/`setIgnored`.
*/
internal class NodeControllerImpl(
private val commandSender: CommandSender,

View File

@@ -19,20 +19,20 @@ package org.meshtastic.core.service
import org.meshtastic.core.model.Position
import org.meshtastic.core.repository.CommandSender
import org.meshtastic.core.repository.NodeManager
import org.meshtastic.core.repository.RequestController
import org.meshtastic.core.repository.QueryController
import org.meshtastic.core.repository.UiPrefs
import org.meshtastic.proto.AdminMessage
/**
* [RequestController] implementation: position, traceroute, telemetry, user info, and metadata "pull" queries.
* [QueryController] implementation: position, traceroute, telemetry, user info, and metadata "pull" queries.
*
* Focused collaborator of [DirectRadioControllerImpl]. Mirrors the SDK's `TelemetryApi`/`RoutingApi` surface.
* Focused collaborator of [RadioControllerImpl]. Mirrors the SDK's `TelemetryApi`/`RoutingApi` surface.
*/
internal class RequestControllerImpl(
internal class QueryControllerImpl(
private val commandSender: CommandSender,
private val nodeManager: NodeManager,
private val uiPrefs: UiPrefs,
) : RequestController {
) : QueryController {
private val myNodeNum: Int
get() = nodeManager.myNodeNum.value ?: 0

View File

@@ -34,10 +34,10 @@ import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.NotificationManager
import org.meshtastic.core.repository.PacketRepository
import org.meshtastic.core.repository.PlatformAnalytics
import org.meshtastic.core.repository.QueryController
import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.core.repository.RadioController
import org.meshtastic.core.repository.RadioInterfaceService
import org.meshtastic.core.repository.RequestController
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.repository.UiPrefs
import org.meshtastic.proto.ClientNotification
@@ -48,7 +48,7 @@ import org.meshtastic.proto.ClientNotification
*
* Rather than implementing every command itself, this class **assembles** four focused collaborators one per
* sub-interface and delegates to them via Kotlin interface delegation, mirroring the SDK's layered API design
* ([AdminController] `AdminApi`, [MessagingController] `RadioClient.send*`, [NodeController]/[RequestController]
* ([AdminController] `AdminApi`, [MessagingController] `RadioClient.send*`, [NodeController]/[QueryController]
* `AdminApi`/`TelemetryApi`/`RoutingApi`). When the SDK is adopted, each collaborator becomes a thin adapter and this
* class is the seam where they are wired together.
*
@@ -56,7 +56,7 @@ import org.meshtastic.proto.ClientNotification
* surfacing, packet-id generation, location provisioning, and device-address switching.
*/
@Suppress("LongParameterList")
class DirectRadioControllerImpl(
class RadioControllerImpl(
private val serviceRepository: ServiceRepository,
nodeRepository: NodeRepository,
private val commandSender: CommandSender,
@@ -85,7 +85,7 @@ class DirectRadioControllerImpl(
packetRepository,
),
NodeController by NodeControllerImpl(commandSender, nodeManager, packetRepository, scope),
RequestController by RequestControllerImpl(commandSender, nodeManager, uiPrefs) {
QueryController by QueryControllerImpl(commandSender, nodeManager, uiPrefs) {
// ── Connection State ────────────────────────────────────────────────────
@@ -101,7 +101,7 @@ class DirectRadioControllerImpl(
// ── Packet ID & Location ────────────────────────────────────────────────
override fun getPacketId(): Int = commandSender.generatePacketId()
override fun generatePacketId(): Int = commandSender.generatePacketId()
override fun startProvideLocation() {
locationManager.restart()

View File

@@ -58,7 +58,7 @@ import kotlin.test.assertNull
import kotlin.test.assertSame
import kotlin.test.assertTrue
class DirectRadioControllerImplTest {
class RadioControllerImplTest {
private val nodeRepository: NodeRepository = mock(MockMode.autofill)
private val commandSender: CommandSender = mock(MockMode.autofill)
@@ -80,10 +80,10 @@ class DirectRadioControllerImplTest {
private fun createController(
serviceRepository: ServiceRepository = ServiceRepositoryImpl(),
myNodeNum: Int? = 1234,
): DirectRadioControllerImpl {
): RadioControllerImpl {
every { nodeManager.myNodeNum } returns MutableStateFlow(myNodeNum)
every { meshPrefs.deviceAddress } returns MutableStateFlow(null)
return DirectRadioControllerImpl(
return RadioControllerImpl(
serviceRepository = serviceRepository,
nodeRepository = nodeRepository,
commandSender = commandSender,

View File

@@ -153,14 +153,15 @@ class FakeRadioController :
editSettingsCalled = true
val scope =
object : AdminEditScope {
override suspend fun setOwner(user: User) = setOwner(destNum, user, getPacketId())
override suspend fun setOwner(user: User) = setOwner(destNum, user, generatePacketId())
override suspend fun setConfig(config: Config) = setConfig(destNum, config, getPacketId())
override suspend fun setConfig(config: Config) = setConfig(destNum, config, generatePacketId())
override suspend fun setModuleConfig(config: ModuleConfig) =
setModuleConfig(destNum, config, getPacketId())
setModuleConfig(destNum, config, generatePacketId())
override suspend fun setChannel(channel: Channel) = setRemoteChannel(destNum, channel, getPacketId())
override suspend fun setChannel(channel: Channel) =
setRemoteChannel(destNum, channel, generatePacketId())
override suspend fun setFixedPosition(position: Position) =
this@FakeRadioController.setFixedPosition(destNum, position)
@@ -168,7 +169,7 @@ class FakeRadioController :
scope.block()
}
override fun getPacketId(): Int = 1
override fun generatePacketId(): Int = 1
override fun startProvideLocation() {
startProvideLocationCalled = true

View File

@@ -58,13 +58,13 @@ import org.meshtastic.core.repository.NeighborInfoResponseProvider
import org.meshtastic.core.repository.NodeController
import org.meshtastic.core.repository.NotificationManager
import org.meshtastic.core.repository.PlatformAnalytics
import org.meshtastic.core.repository.QueryController
import org.meshtastic.core.repository.RadioController
import org.meshtastic.core.repository.RadioTransportFactory
import org.meshtastic.core.repository.RequestController
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.repository.ServiceStateWriter
import org.meshtastic.core.repository.TracerouteResponseProvider
import org.meshtastic.core.service.DirectRadioControllerImpl
import org.meshtastic.core.service.RadioControllerImpl
import org.meshtastic.core.service.ServiceRepositoryImpl
import org.meshtastic.desktop.DesktopBuildConfig
import org.meshtastic.desktop.DesktopNotificationManager
@@ -177,7 +177,7 @@ private fun desktopPlatformStubsModule() = module {
)
}
single<RadioController> {
DirectRadioControllerImpl(
RadioControllerImpl(
serviceRepository = get(),
nodeRepository = get(),
commandSender = get(),
@@ -199,7 +199,7 @@ private fun desktopPlatformStubsModule() = module {
single<AdminController> { get<RadioController>() }
single<MessagingController> { get<RadioController>() }
single<NodeController> { get<RadioController>() }
single<RequestController> { get<RadioController>() }
single<QueryController> { get<RadioController>() }
single<NativeNotificationSender> {
when (DesktopOS.current()) {
DesktopOS.Linux -> LinuxNotificationSender()

View File

@@ -130,9 +130,9 @@ focused sub-interfaces so callers can depend on just the slice they need:
| `AdminController` | Config, channels, owner, device lifecycle, `editSettings { }` transactions |
| `MessagingController` | Send packets, reactions, shared contacts |
| `NodeController` | Favorite, ignore, mute, remove nodes |
| `RequestController` | Telemetry, traceroute, position/user-info queries |
| `QueryController` | Telemetry, traceroute, position/user-info queries |
`DirectRadioControllerImpl` (`core:service`) is the in-process composition root for all targets
`RadioControllerImpl` (`core:service`) is the in-process composition root for all targets
(Desktop, iOS, single-process Android). It assembles the four sub-controllers via Kotlin interface
delegation and adds the cross-cutting concerns (connection state, packet-id, location,
device-address switching). Commands are direct suspend calls; admin writes are fire-and-forget

View File

@@ -190,7 +190,7 @@ class Esp32OtaUpdateHandler(
val myInfo = nodeRepository.myNodeInfo.value ?: return
val myNodeNum = myInfo.myNodeNum
Logger.i { "ESP32 OTA: Triggering reboot OTA mode $mode with hash" }
radioController.requestRebootOta(radioController.getPacketId(), myNodeNum, mode, hash)
radioController.requestRebootOta(radioController.generatePacketId(), myNodeNum, mode, hash)
}
/**

View File

@@ -162,7 +162,7 @@ open class BaseMapViewModel(
safeLaunch(context = ioDispatcher, tag = "sendDataPacket") { radioController.sendMessage(p) }
}
fun generatePacketId(): Int = radioController.getPacketId()
fun generatePacketId(): Int = radioController.generatePacketId()
data class MapFilterState(
val onlyFavorites: Boolean,

View File

@@ -67,7 +67,7 @@ constructor(
override suspend fun requestNeighborInfo(destNum: Int, longName: String) {
Logger.i { "Requesting NeighborInfo for '$destNum'" }
val packetId = radioController.getPacketId()
val packetId = radioController.generatePacketId()
radioController.requestNeighborInfo(packetId, destNum)
_lastRequestNeighborTimes.update { it + (destNum to nowMillis) }
showFeedback(UiText.Resource(Res.string.requesting_from, Res.string.neighbor_info, longName))
@@ -81,7 +81,7 @@ constructor(
override suspend fun requestTelemetry(destNum: Int, longName: String, type: TelemetryType) {
Logger.i { "Requesting telemetry for '$destNum'" }
val packetId = radioController.getPacketId()
val packetId = radioController.generatePacketId()
radioController.requestTelemetry(packetId, destNum, type.ordinal)
val typeRes =
@@ -100,7 +100,7 @@ constructor(
override suspend fun requestTraceroute(destNum: Int, longName: String) {
Logger.i { "Requesting traceroute for '$destNum'" }
val packetId = radioController.getPacketId()
val packetId = radioController.generatePacketId()
radioController.requestTraceroute(packetId, destNum)
_lastTracerouteTime.value = nowMillis
showFeedback(UiText.Resource(Res.string.requesting_from, Res.string.traceroute, longName))

View File

@@ -39,7 +39,7 @@ import org.meshtastic.core.model.NodeAddress
import org.meshtastic.core.model.SessionStatus
import org.meshtastic.core.navigation.Route
import org.meshtastic.core.navigation.SettingsRoute
import org.meshtastic.core.repository.RequestController
import org.meshtastic.core.repository.QueryController
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.UiText
import org.meshtastic.core.resources.connect_radio_for_remote_admin
@@ -81,7 +81,7 @@ class NodeDetailViewModel(
private val savedStateHandle: SavedStateHandle,
private val nodeManagementActions: NodeManagementActions,
private val nodeRequestActions: NodeRequestActions,
private val requestController: RequestController,
private val queryController: QueryController,
private val getNodeDetailsUseCase: GetNodeDetailsUseCase,
private val ensureRemoteAdminSession: EnsureRemoteAdminSessionUseCase,
private val observeRemoteAdminSessionStatus: ObserveRemoteAdminSessionStatusUseCase,
@@ -174,7 +174,7 @@ class NodeDetailViewModel(
/**
* Re-fetch device metadata (firmware/edition/role) for [destNum]. Refreshes the session passkey as a side effect.
*/
fun refreshMetadata(destNum: Int) = viewModelScope.launch { requestController.refreshMetadata(destNum) }
fun refreshMetadata(destNum: Int) = viewModelScope.launch { queryController.refreshMetadata(destNum) }
/**
* Ensure a remote-admin session passkey is fresh, then request navigation to the remote-admin screen. Surfaces a

View File

@@ -60,7 +60,7 @@ constructor(
open suspend fun removeNode(nodeNum: Int) {
Logger.i { "Removing node '$nodeNum'" }
val packetId = radioController.getPacketId()
val packetId = radioController.generatePacketId()
radioController.removeByNodenum(packetId, nodeNum)
nodeRepository.deleteNode(nodeNum)
}

View File

@@ -34,7 +34,7 @@ import org.meshtastic.core.domain.usecase.session.EnsureRemoteAdminSessionUseCas
import org.meshtastic.core.domain.usecase.session.ObserveRemoteAdminSessionStatusUseCase
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.SessionStatus
import org.meshtastic.core.repository.RequestController
import org.meshtastic.core.repository.QueryController
import org.meshtastic.core.ui.util.SnackbarManager
import org.meshtastic.feature.node.component.NodeMenuAction
import org.meshtastic.feature.node.domain.usecase.GetNodeDetailsUseCase
@@ -51,7 +51,7 @@ class HandleNodeActionTest {
private val testDispatcher = UnconfinedTestDispatcher()
private val nodeManagementActions: NodeManagementActions = mock()
private val nodeRequestActions: NodeRequestActions = mock()
private val requestController: RequestController = mock()
private val queryController: QueryController = mock()
private val getNodeDetailsUseCase: GetNodeDetailsUseCase = mock()
private val ensureRemoteAdminSession: EnsureRemoteAdminSessionUseCase = mock()
private val observeRemoteAdminSessionStatus: ObserveRemoteAdminSessionStatusUseCase = mock()
@@ -93,7 +93,7 @@ class HandleNodeActionTest {
savedStateHandle = SavedStateHandle(mapOf("destNum" to 1234)),
nodeManagementActions = nodeManagementActions,
nodeRequestActions = nodeRequestActions,
requestController = requestController,
queryController = queryController,
getNodeDetailsUseCase = getNodeDetailsUseCase,
ensureRemoteAdminSession = ensureRemoteAdminSession,
observeRemoteAdminSessionStatus = observeRemoteAdminSessionStatus,

View File

@@ -42,7 +42,7 @@ import org.meshtastic.core.domain.usecase.session.ObserveRemoteAdminSessionStatu
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.SessionStatus
import org.meshtastic.core.navigation.SettingsRoute
import org.meshtastic.core.repository.RequestController
import org.meshtastic.core.repository.QueryController
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.UiText
import org.meshtastic.core.resources.connect_radio_for_remote_admin
@@ -64,7 +64,7 @@ class NodeDetailViewModelTest {
private lateinit var viewModel: NodeDetailViewModel
private val nodeManagementActions: NodeManagementActions = mock()
private val nodeRequestActions: NodeRequestActions = mock()
private val requestController: RequestController = mock()
private val queryController: QueryController = mock()
private val getNodeDetailsUseCase: GetNodeDetailsUseCase = mock()
private val ensureRemoteAdminSession: EnsureRemoteAdminSessionUseCase = mock()
private val observeRemoteAdminSessionStatus: ObserveRemoteAdminSessionStatusUseCase = mock()
@@ -97,7 +97,7 @@ class NodeDetailViewModelTest {
savedStateHandle = SavedStateHandle(if (nodeId != null) mapOf("destNum" to nodeId) else emptyMap()),
nodeManagementActions = nodeManagementActions,
nodeRequestActions = nodeRequestActions,
requestController = requestController,
queryController = queryController,
getNodeDetailsUseCase = getNodeDetailsUseCase,
ensureRemoteAdminSession = ensureRemoteAdminSession,
observeRemoteAdminSessionStatus = observeRemoteAdminSessionStatus,