refactor: delete POC ViewModels and stale references

- Delete SdkConfigViewModel, SdkMessagingViewModel, SdkTelemetryViewModel (unused POC)
- Delete RadioClientViewModel, SdkNodeListViewModel (POC logging only)
- Remove POC VM instantiation from Main.kt
- Delete NoopRadioInterfaceService (superseded by SdkRadioInterfaceService)
- Delete dead app/test/Fakes.kt (both classes unused)
- Fix stale KDoc references to MeshConnectionManager, RadioInterfaceService.connectionState

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
James Rich
2026-05-05 10:05:58 -05:00
parent 3cdad0da28
commit f0aa99eaff
12 changed files with 111 additions and 885 deletions

View File

@@ -1,186 +1,141 @@
# SDK Migration — Remaining Work
# SDK Migration — Status & Remaining Work
> Auto-generated from migration session. Tracks what's done vs what remains against
> the [Clean Break Migration Guide](../meshtastic-sdk/docs/architecture/meshtastic-android-migration.md).
> Tracks progress of the Meshtastic-Android clean-break migration to meshtastic-sdk.
> Updated: 2026-05-05
---
## Summary
**Completed:** ~70% of the Clean Break migration. AIDL dropped, SDK storage active,
service broadcasts eliminated, management layers flattened, trivial UseCases deleted,
test infrastructure established.
**Completed:** ~90% of the Clean Break migration. AIDL dropped, SDK is sole radio path,
transport layer fully deleted, Desktop uses shared SDK bridge, dead infrastructure gone.
**Remaining:** ViewModel direct-binding (blocked by module architecture), Desktop SDK
migration, dead infrastructure cleanup, and 4 deferred UseCase deletions.
**Remaining:** VM parameter slimming (optional — currently all SDK-backed), Room table
cleanup, and minor test coverage for new code.
**Net change so far:** ~52 files changed, +162 / -2,395 lines across all sessions.
**Net change:** 145 files changed, +2,752 / -15,059 lines (net -12,307 LOC removed)
---
## What's Done (by migration doc phase)
## Architecture (current state)
### Phase 1: Environment & Dependency Alignment ✅
- SDK composite build integrated (`settings.gradle.kts`)
- Wire proto types used throughout (`org.meshtastic.proto.*`)
- `sdk-core`, `sdk-proto`, `sdk-transport-*`, `sdk-storage-sqldelight`, `sdk-testing` all wired
```
┌──────────────────────────────────────────────────────────────┐
│ Feature VMs (NodeList, Settings, RadioConfig, Messages...) │
│ inject: RadioController, NodeRepository, ServiceRepository │
└───────────────────────────────┬──────────────────────────────┘
┌───────────────────────────────▼──────────────────────────────┐
│ SDK-Backed Adapters (core/data) │
│ SdkRadioController, SdkStateBridge, SdkNodeRepositoryImpl │
│ SdkPacketHandler, SdkRadioInterfaceService │
│ MessagePersistenceHandler │
└───────────────────────────────┬──────────────────────────────┘
┌───────────────────────────────▼──────────────────────────────┐
│ RadioClientAccessor (platform-specific providers) │
│ Android: RadioClientProvider (app/) │
│ Desktop: DesktopRadioClientProvider (desktop/) │
└───────────────────────────────┬──────────────────────────────┘
┌───────────────────────────────▼──────────────────────────────┐
│ meshtastic-sdk │
│ RadioClient → MeshEngine → Transport (BLE/TCP/Serial) │
└──────────────────────────────────────────────────────────────┘
```
### Phase 2: One-Time Data Migration ✅
- Room auto-migration 38→39 with `AutoMigration38to39` spec
- `onPostMigrate` copies favorites/notes/ignored/muted/manuallyVerified from `nodes``node_metadata`
- `NodeMetadataEntity` + `NodeMetadataDao` created
- `SdkNodeRepositoryImpl` enriches SDK nodes with persisted metadata
---
### Phase 3: The Great Deletion (partial) ✅
- `ServiceBroadcasts` — deleted (both `core/service` android + `core/repository` common)
- `MeshConnectionManagerImpl` — deleted (438 LOC)
- `MeshConnectionManager` interface — deleted
- `NodeRepositoryImpl` (old Room-backed) — deleted (290 LOC)
- `NodeInfoWriteDataSource` + `SwitchingNodeInfoWriteDataSource` — deleted
- AIDL — already removed in prior session
## What's Done
### Phase 4: RadioClient as Core Dependency
- `RadioClientProvider` implemented in `app/` with BLE/TCP/Serial support
- Exposed as `StateFlow<RadioClient?>` for reactive observation
- Auto-reconnect enabled
- `SdkClientLifecycle` interface bridges to `core/service` without reverse dependency
### Infrastructure
- AIDL completely removed
- SDK composite build integrated
- `RadioClientProvider` (Android) + `DesktopRadioClientProvider` (Desktop)
- `SdkClientLifecycle` bridges to service layer
- SDK `sendRaw(ToRadio)` API added for MQTT/XModem
### Phase 5: Thin Foreground Service
- `MeshService` stripped to lifecycle holder + notification management
- Uses `ServiceRepository` for connection state (bridged from SDK)
- `MeshServiceOrchestrator` simplified to TAK lifecycle + notifications + DB init + widget
### Transport Layer
- **Fully deleted:** BleRadioTransport, TcpRadioTransport, SerialRadioTransport, StreamTransport, HeartbeatSender, StreamFrameCodec, all transport factories, BleReconnectPolicy, TcpTransport
- `RadioInterfaceService` slimmed to device-address surface only
- `SdkRadioInterfaceService` created (thin adapter over RadioPrefs + RadioClientAccessor)
- Desktop `NoopRadioInterfaceService` updated to match
- `JvmUsbScanner` migrated to SDK's `JvmSerialPorts.list()`
### Phase 6: UI & Domain (partial)
- **6.1 ViewModel Simplification:** `AppMetadataRepository` created ✅ — but VM refactoring blocked (see below)
- **6.2 UseCase Decimation:** 8 trivial UseCases deleted ✅ — 4 deferred, complex ones kept
### Pipeline ✅
- **Fully deleted:** CommandSender, MeshRouter, MeshActionHandler, PacketHandlerImpl, MeshDataHandlerImpl, MeshConnectionManager, MeshConfigFlowManager, ServiceBroadcasts, DirectRadioControllerImpl
- `SdkRadioController` is sole RadioController impl
- `SdkStateBridge` bridges SDK events → repositories
- `SdkPacketHandler` routes MeshPackets via `client.send()`, raw ToRadio via `client.sendRaw()`
### Phase 7: UI/VM Direct Binding
- POC VMs exist (`SdkNodeListViewModel`, `SdkConfigViewModel`, etc.) ✅
- Production VMs still use repository layer (blocked — see below)
### Data Layer ✅
- Room migration 38→39: NodeMetadata persistence
- `SdkNodeRepositoryImpl` enriches SDK nodes with persisted favorites/notes/ignore
- SDK storage (SqlDelight) is source of truth for node data
- `AppMetadataRepository` provides firmware/hardware/model info
### Phase 8: Feature Integrations
- Location publishing moved to `SdkStateBridge`
- TAK integration preserved (uses `ServiceAction` dispatch through `SdkStateBridge`)
### Desktop
- Fully cut over to SDK via shared KMP bridge
- `DesktopRadioClientProvider` manages TCP/Serial transport
- No transport stubs needed — SDK handles everything
### Phase 9: Testing Strategy
- `sdk-testing` dependency added to `app` and `core/data`
- `TestRadioClientProvider` created with `FakeRadioTransport(autoHandshake=true)`
- Integration test validates connect → handshake → node injection → observation
### UseCases Deleted
- ProcessRadioResponse (tests only — impl kept, has real packet parsing logic)
- AdminActions (tests only — impl kept, has real reboot/reset logic)
- SetMeshLogSettings (tests only — impl kept)
- CleanNodeDatabase (tests only — impl kept)
- IsOtaCapable (tests only — impl kept)
- EnsureRemoteAdminSession (tests only — impl kept, complex concurrency)
---
## What Remains
### 1. ViewModel Direct-Binding (Phase 6.1 — BLOCKED)
### 1. Room Table Cleanup (low priority)
- Migration 39→40: DROP legacy `nodes`, `my_node` tables
- Remove old `NodeEntity`, `MyNodeEntity` Room entities + DAOs
- SDK SqlDelight is already source of truth; Room tables are unused dead weight
**Blocker:** Feature modules (`feature/node`, `feature/settings`, `feature/messaging`, etc.)
are KMP `commonMain` and cannot depend on the SDK directly. Only the `app` module has
`implementation(libs.sdk.core)`. The `RadioClientProvider` lives in `app/`.
### 2. VM Parameter Slimming (optional, quality-of-life)
VMs currently inject SDK-backed adapters (RadioController, NodeRepository, etc.)
which work correctly. Direct SDK injection would reduce params but isn't required:
**Current state:** Feature VMs inject `NodeRepository`, `ServiceRepository`,
`RadioConfigRepository`, `RadioController` — all of which are already SDK-backed thin
adapters populated by `SdkStateBridge`. The indirection works correctly but isn't the
"direct binding" the migration doc envisions.
| VM | Current Params | Could Be |
|----|---------------|----------|
| RadioConfigVM | 15 | 8-10 |
| SettingsVM | 12 | 8-10 |
| MessageVM | 12 | 6-8 |
| NodeListVM | 9 | 5-6 |
| NodeDetailVM | 7 | 4-5 |
**To unblock (choose one):**
1. **Option A — SDK dependency in `core/repository`:** Add `api(libs.sdk.core)` to
`core/repository/build.gradle.kts`. Create a `RadioClientAccessor` interface in
`core/repository` exposing `client: StateFlow<RadioClient?>`. Feature modules can then
inject it. Trade-off: couples `core/repository` to SDK API surface.
2. **Option B — New `core/sdk-bridge` module:** Create a thin KMP module that depends on
`sdk-core` and exposes flow-based abstractions (nodes, config, connection, admin).
Feature modules depend on this instead of raw `RadioClient`. More modular but adds a module.
3. **Option C — Move VMs to `app`:** Move production VMs out of `feature/*` into `app/`.
Breaks KMP desktop/iOS target sharing. Not recommended.
### 3. NodeManager Merge (optional)
`NodeManager` (25 methods, 8+ consumers) could merge into `SdkNodeRepositoryImpl`.
Currently SDK feeds it via SdkStateBridge. Works fine as-is.
**VMs to migrate (22 total):**
| Tier | VMs | Current Params | Target |
|------|-----|----------------|--------|
| Critical (5) | RadioConfigVM, SettingsVM, MessageVM, MetricsVM, NodeListVM | 9-19 | 3-5 |
| Moderate (6) | NodeDetailVM, ContactsVM, BaseMapVM, DebugVM, ChannelVM, FilterSettingsVM | 3-8 | 2-3 |
| Simple (3) | CleanNodeDatabaseVM, QuickChatVM, CompassVM | 1-4 | 1-2 |
### 4. MeshActivity Restoration (cosmetic)
`UIViewModel.meshActivity` currently emits `emptyFlow()`. Could be restored by
having `SdkStateBridge` emit send/receive events when SDK delivers/receives packets.
Purely cosmetic — affects connection icon animation only.
### 2. Dead Infrastructure Cleanup (Phase 3 — BLOCKED by Desktop)
**Blocker:** Desktop's `DesktopKoinModule` manually creates `DirectRadioControllerImpl`,
which pulls in the entire old packet-routing chain via Koin.
**Files blocked from deletion (~10 files, ~2,000 LOC):**
- `MeshRouterImpl` + `MeshRouter` interface
- `MeshDataHandlerImpl` + `MeshDataHandler` interface
- `AdminPacketHandlerImpl` + `AdminPacketHandler` interface
- `PacketHandlerImpl` + `PacketHandler` interface
- `MeshConfigFlowManagerImpl` + `MeshConfigFlowManager` interface (gutted but present)
- `MeshActionHandlerImpl` + `MeshActionHandler` interface
- `CommandSenderImpl` + `CommandSender` interface
- `DirectRadioControllerImpl`
**To unblock:** Migrate Desktop to use SDK's `RadioClient` + TCP/Serial transport.
Replace `DirectRadioControllerImpl` in `DesktopKoinModule` with an SDK-backed
`RadioController` impl (similar to how Android's `SdkStateBridge` bridges SDK → repositories).
### 3. Deferred UseCase Deletions (4 remaining)
These UseCases have real logic and depend on the VM migration to be safely inlined:
| UseCase | Reason Kept |
|---------|-------------|
| `EnsureRemoteAdminSessionUseCase` | Session passkey management — needs SDK `admin.session` API |
| `ObserveRemoteAdminSessionStatusUseCase` | Session status observation — needed until VMs use SDK directly |
| `CleanNodeDatabaseUseCase` | Node cleanup logic with age/unknown filtering |
| `IsOtaCapableUseCase` | OTA capability check (firmware + device model) |
Additionally kept (complex orchestration, not candidates for deletion):
- `RadioConfigUseCase`, `MeshLocationUseCase`, `ImportProfileUseCase`,
`ExportProfileUseCase`, `ExportSecurityConfigUseCase`, `InstallProfileUseCase`,
`SetMeshLogSettingsUseCase`, `ExportDataUseCase`
### 4. Remaining Phase C Items (deferred)
| Item | Description | Status |
|------|-------------|--------|
| C3 | Move raw packet forwarding — VMs observe `client.packets` directly | Blocked by VM migration |
| C4 | Delete `ServiceRepository.emitMeshPacket()` / `meshPacketFlow` | Blocked by C3 |
| C5 | Further simplify `MeshServiceOrchestrator` | Minor — mostly done |
| C6 | Remove `SharedRadioInterfaceService` | Complex — SDK owns transport but address management still used |
### 5. Room Table Cleanup
The old `nodes`, `my_node`, and `metadata` Room tables still exist in the schema
(data was copied to `node_metadata` in migration 38→39). A future migration should
DROP these tables to reduce DB size.
### 6. `NodeInfoReadDataSource` Cleanup
`NodeInfoReadDataSource` interface and `SwitchingNodeInfoReadDataSource` impl are still
referenced by `MeshLogRepositoryImpl` (for resolving node names in log entries).
To delete: refactor `MeshLogRepositoryImpl` to get node names from `NodeRepository` or
`AppMetadataRepository` instead.
---
## Recommended Execution Order
1. **Desktop SDK migration** — unblocks item #2 (dead code deletion, ~2,000 LOC)
2. **Module restructuring** (Option A or B above) — unblocks item #1 (VM direct-binding)
3. **VM migration** — migrate 22 VMs to use RadioClient directly (per-VM PRs)
4. **UseCase cleanup** — delete 4 deferred UseCases after VM migration
5. **Phase C completion** — C3/C4/C6 after VMs no longer use ServiceRepository packet flow
6. **Room table cleanup** — DROP legacy tables in a final migration
### 5. Test Coverage
- New code (`SdkRadioInterfaceService`, `SdkPacketHandler`, `MessagePersistenceHandler`)
has no dedicated tests yet (existing integration tests cover happy paths)
- UseCase tests were deleted with the impls — should add back for kept impls
---
## What STAYS (permanent architecture)
These components are NOT candidates for deletion — they serve app-local purposes
the SDK doesn't cover:
These components are NOT migration candidates:
- `PacketRepository` — message persistence (SDK doesn't persist chat history)
- `MeshLogRepository` — debug logging (app-local concern)
- `QuickChatActionRepository` — quick-chat templates (app preference)
- `DeviceHardwareRepository` / `FirmwareReleaseRepository` — GitHub API clients
- `MeshLogRepository` — debug logging (app-local)
- `QuickChatActionRepository` — quick-chat templates
- `DeviceHardwareRepository` / `FirmwareReleaseRepository` — GitHub API
- `NodeMetadataDao` / `AppMetadataRepository` — favorites, notes, ignore, mute
- `MeshServiceOrchestrator` (simplified) — TAK lifecycle, notifications, DB init
- `SdkStateBridge` (reduced) — SDK → repository bridging, location publishing, TAK dispatch
- `RadioClientProvider` — SDK client lifecycle management
- `ContactSettings` table — app-local mute/read state per contact
- `MeshServiceOrchestrator` — TAK lifecycle, notifications, DB init, widget
- `SdkStateBridge` — SDK → repository bridging, notifications, TAK dispatch
- `MqttManager` / `HistoryManager` / `XModemManager` — real features
- `TelemetryPacketHandler` / `NeighborInfoHandler` / `TracerouteHandler` — packet processors
- `ContactSettings` — per-contact mute/read state
- `SessionManager` — per-node admin session passkey management

View File

@@ -1,100 +0,0 @@
/*
* 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.app.radio
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import co.touchlab.kermit.Logger
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import org.koin.core.annotation.KoinViewModel
import org.meshtastic.sdk.ConnectionState
import org.meshtastic.sdk.MeshEvent
/**
* POC ViewModel that exposes the SDK [RadioClient] connection lifecycle to the UI.
*
* **Connection state:** Uses `flatMapLatest` on the `StateFlow<RadioClient?>` so that any screen collecting
* [sdkConnectionState] automatically switches to the new client's connection flow when
* [RadioClientProvider.rebuildAndConnect] replaces the active client. [SharingStarted.WhileSubscribed] with a 5 s
* timeout keeps the upstream active briefly after the last subscriber leaves (e.g., configuration change) so the next
* subscriber doesn't miss a fast `Connected` event.
*
* **Events:** Collected with [SharingStarted.Eagerly] so that [MeshEvent]s (device rebooted, storage degraded, security
* warnings) are never dropped while navigating between screens. The collection is launched in [viewModelScope] which is
* tied to the application lifecycle via Koin's `@KoinViewModel` singleton scope — not to any individual screen.
*
* SDK gaps surfaced here:
* - [ConnectionState.Configuring] has no counterpart in the legacy [org.meshtastic.core.model.ConnectionState]
* - [ConnectionState.Reconnecting] has no counterpart in the legacy model
*/
@KoinViewModel
class RadioClientViewModel(private val provider: RadioClientProvider) : ViewModel() {
/** Live SDK connection state; `null` if no client is active (no radio configured). */
val sdkConnectionState: StateFlow<ConnectionState?> =
provider.client
.flatMapLatest { client -> client?.connection ?: flowOf(null) }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null)
/**
* Human-readable label for the SDK connection state. Useful as a debug overlay in POC builds to see SDK state
* alongside the legacy state.
*/
val sdkConnectionLabel: StateFlow<String> =
sdkConnectionState.map { it.toLabel() }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), "SDK: —")
init {
// Collect events eagerly so none are dropped during navigation.
// This scope lives as long as the ViewModel (application lifetime via Koin singleton).
provider.client
.flatMapLatest { client -> client?.events ?: emptyFlow() }
.onEach { event ->
when (event) {
is MeshEvent.StorageDegraded -> Logger.w { "[SDK] StorageDegraded: ${event.reason}" }
is MeshEvent.DeviceRebooted -> Logger.i { "[SDK] DeviceRebooted" }
is MeshEvent.SecurityWarning -> Logger.w { "[SDK] SecurityWarning: $event" }
else -> Logger.d { "[SDK] Event: $event" }
}
}
.launchIn(viewModelScope)
}
/** Kick off a (re)connect using the current saved radio address. */
fun connect() = provider.rebuildAndConnectAsync()
/** Disconnect the active client. */
fun disconnect() = provider.disconnect()
}
private const val PERCENT = 100
private fun ConnectionState?.toLabel(): String = when (this) {
null -> "SDK: no client"
ConnectionState.Disconnected -> "SDK: Disconnected"
is ConnectionState.Connecting -> "SDK: Connecting (#$attempt)"
is ConnectionState.Configuring -> "SDK: Configuring — ${phase.name} (${(progress * PERCENT).toInt()}%)"
ConnectionState.Connected -> "SDK: Connected ✓"
is ConnectionState.Reconnecting -> "SDK: Reconnecting (#$attempt) — $cause"
}

View File

@@ -1,132 +0,0 @@
/*
* 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.app.radio
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import co.touchlab.kermit.Logger
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import org.koin.core.annotation.KoinViewModel
import org.meshtastic.proto.Config
import org.meshtastic.proto.ModuleConfig
import org.meshtastic.proto.User
import org.meshtastic.sdk.AdminResult
import org.meshtastic.sdk.ConfigBundle
/**
* POC ViewModel that reads device configuration from the SDK's [ConfigBundle] and writes changes back via
* [org.meshtastic.sdk.AdminApi.editSettings].
*
* **Read path:** [RadioClient.configBundle] is a [StateFlow] cached from the handshake — zero RPCs required. It
* contains all [Config] and [ModuleConfig] entries as they were at connect time.
*
* **Write path:** [editSettings] issues a single-RPC batch write. The SDK auto-refreshes [configBundle] after a
* successful commit (Gap G resolved).
*
* **Gap C resolved:** [RadioClient.channels] is now a reactive StateFlow seeded from the handshake.
*/
@KoinViewModel
class SdkConfigViewModel(private val provider: RadioClientProvider) : ViewModel() {
/** The raw ConfigBundle from the handshake; null until connected+configured. */
val configBundle: StateFlow<ConfigBundle?> =
provider.client
.flatMapLatest { it?.configBundle ?: flowOf(null) }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null)
/** Device config — read directly from SDK configBundle (Gap G resolved, no local overlay needed). */
val deviceConfig: StateFlow<Config.DeviceConfig?> =
configBundle
.map { bundle -> bundle?.configs?.firstOrNull { it.device != null }?.device }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null)
/** LoRa config — read directly from SDK configBundle. */
val loraConfig: StateFlow<Config.LoRaConfig?> =
configBundle
.map { bundle -> bundle?.configs?.firstOrNull { it.lora != null }?.lora }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null)
/** Reactive channel list from the SDK (Gap C resolved — seeded from handshake, updated on setChannel). */
val channels: StateFlow<List<org.meshtastic.proto.Channel>> =
provider.client
.flatMapLatest { client -> client?.channels?.map { it.orEmpty() } ?: flowOf(emptyList()) }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())
/**
* Write a config update to the radio via [AdminApi.editSettings].
*
* The SDK automatically refreshes configBundle after a successful commit.
*/
fun setConfig(config: Config, typeKey: String) {
val client =
provider.client.value
?: run {
Logger.w { "[SDK] setConfig: no active client" }
return
}
viewModelScope.launch {
when (val result = client.admin.editSettings { setConfig(config) }) {
is AdminResult.Success -> Logger.i { "[SDK] setConfig($typeKey) succeeded" }
AdminResult.Timeout -> Logger.w { "[SDK] setConfig($typeKey): Timeout" }
AdminResult.Unauthorized -> Logger.w { "[SDK] setConfig($typeKey): Unauthorized" }
AdminResult.SessionKeyExpired ->
Logger.w { "[SDK] setConfig($typeKey): SessionKeyExpired — reconnect needed" }
AdminResult.NodeUnreachable -> Logger.w { "[SDK] setConfig($typeKey): NodeUnreachable" }
is AdminResult.Failed -> Logger.e { "[SDK] setConfig($typeKey): Failed — ${result.routingError}" }
}
}
}
/** Convenience: update device config. */
fun setDeviceConfig(device: Config.DeviceConfig) = setConfig(Config(device = device), "device")
/** Convenience: update LoRa config. */
fun setLoraConfig(lora: Config.LoRaConfig) = setConfig(Config(lora = lora), "lora")
/** Update owner name on the radio. */
fun setOwner(longName: String, shortName: String) {
val client = provider.client.value ?: return
viewModelScope.launch {
when (val result = client.admin.setOwner(User(long_name = longName, short_name = shortName))) {
is AdminResult.Success -> Logger.i { "[SDK] setOwner succeeded" }
else -> Logger.w { "[SDK] setOwner failed: $result" }
}
}
}
/**
* Diagnostics: log the full ConfigBundle contents. Useful for POC validation — call from a debug menu or
* LaunchedEffect.
*/
fun logConfigBundle() {
val bundle = configBundle.value
if (bundle == null) {
Logger.i { "[SDK] configBundle: null (not yet connected)" }
return
}
Logger.i { "[SDK] myNodeNum=${bundle.myInfo.my_node_num}" }
Logger.i { "[SDK] firmwareVersion=${bundle.metadata.firmware_version}" }
bundle.configs.forEach { c -> Logger.d { "[SDK] Config: $c" } }
bundle.moduleConfigs.forEach { mc -> Logger.d { "[SDK] ModuleConfig: $mc" } }
}
}

View File

@@ -1,130 +0,0 @@
/*
* 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.app.radio
import androidx.compose.runtime.Immutable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import co.touchlab.kermit.Logger
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import org.koin.core.annotation.KoinViewModel
import org.meshtastic.sdk.ChannelIndex
import org.meshtastic.sdk.MessageHandle
import org.meshtastic.sdk.NodeId
import org.meshtastic.sdk.SendState
import org.meshtastic.sdk.asText
/** Stable Compose model for a received text message. */
@Immutable
data class IncomingTextMessage(val fromNodeNum: Int, val channelIndex: Int, val text: String, val rxTimeSeconds: Int)
/** Stable Compose model for an outbound message's delivery status. */
@Immutable data class OutboundStatus(val messageId: Long, val state: SendState)
/**
* POC ViewModel that wires text messaging to the SDK's [RadioClient].
*
* **Inbound:** Filters [RadioClient.packets] for TEXT_MESSAGE_APP packets using the SDK's [org.meshtastic.sdk.asText]
* extension. Accumulated in [incomingMessages] (capped at 200 for the POC to avoid unbounded memory growth).
*
* **Outbound:** [sendText] calls [RadioClient.sendText] synchronously (non-suspending), receives a [MessageHandle], and
* tracks [SendState] transitions in [outboundStatuses].
*
* **SDK Gap B surfaced:** [RadioClient] has [org.meshtastic.sdk.asText] as a packet-level extension, but no reactive
* `RadioClient.textMessages: Flow<IncomingTextMessage>` convenience. Callers must filter `packets` themselves. Log as
* Gap B for SDK fix.
*
* Note: Inbound packet collection uses `SharingStarted.Eagerly` (via [launchIn]) so messages are never dropped while
* navigating between screens.
*/
@KoinViewModel
class SdkMessagingViewModel(private val provider: RadioClientProvider) : ViewModel() {
private val _incomingMessages = MutableStateFlow<List<IncomingTextMessage>>(emptyList())
val incomingMessages: StateFlow<List<IncomingTextMessage>> = _incomingMessages.asStateFlow()
private val _outboundStatuses = MutableStateFlow<List<OutboundStatus>>(emptyList())
val outboundStatuses: StateFlow<List<OutboundStatus>> = _outboundStatuses.asStateFlow()
init {
// Eagerly collect inbound text packets — must not drop while navigating.
// Gap B: no RadioClient.textMessages flow; manually filter packets.
provider.client
.flatMapLatest { client -> client?.packets ?: emptyFlow() }
.mapNotNull { packet ->
val text = packet.asText() ?: return@mapNotNull null
IncomingTextMessage(
fromNodeNum = packet.from,
channelIndex = packet.channel,
text = text,
rxTimeSeconds = packet.rx_time,
)
}
.onEach { msg ->
Logger.d { "[SDK] Received text from ${msg.fromNodeNum} ch${msg.channelIndex}: ${msg.text}" }
_incomingMessages.update { prev -> (prev + msg).takeLast(MAX_MESSAGES) }
}
.launchIn(viewModelScope)
}
/**
* Send a text message via the SDK.
*
* @param text the message text
* @param channelIndex 07; defaults to primary channel (0)
* @param toNodeNum destination node num; 0xFFFFFFFF (default) = broadcast
*/
fun sendText(text: String, channelIndex: Int = 0, toNodeNum: Int = BROADCAST_NODE_NUM) {
val client =
provider.client.value
?: run {
Logger.w { "[SDK] sendText: no active client" }
return
}
val handle: MessageHandle =
client.sendText(text = text, channel = ChannelIndex(channelIndex), to = NodeId(toNodeNum))
// Track delivery state for this outbound message
handle.state
.onEach { state ->
Logger.d { "[SDK] Message ${handle.id}$state" }
_outboundStatuses.update { prev ->
val updated = OutboundStatus(messageId = handle.id.raw.toLong(), state = state)
val existing = prev.indexOfFirst { it.messageId == updated.messageId }
if (existing >= 0) {
prev.toMutableList().also { it[existing] = updated }
} else {
(prev + updated).takeLast(MAX_MESSAGES)
}
}
}
.launchIn(viewModelScope)
}
companion object {
private const val MAX_MESSAGES = 200
private const val BROADCAST_NODE_NUM = -1 // 0xFFFFFFFF as signed Int
}
}

View File

@@ -1,143 +0,0 @@
/*
* 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.app.radio
import androidx.compose.runtime.Immutable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import co.touchlab.kermit.Logger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.scan
import kotlinx.coroutines.flow.stateIn
import org.koin.core.annotation.KoinViewModel
import org.meshtastic.proto.NodeInfo
import org.meshtastic.sdk.ConnectionQuality
import org.meshtastic.sdk.MeshNode
import org.meshtastic.sdk.NodeChange
import org.meshtastic.sdk.NodeId
import org.meshtastic.sdk.SignalQuality
import org.meshtastic.sdk.toMeshNode
/**
* Stable, Compose-safe representation of a mesh node.
*
* Wire-generated [NodeInfo] is NOT `@Stable`; never pass it directly to Compose. This wrapper holds the
* fields the node list UI needs for display, filtering, and sorting.
*/
@Immutable
data class UiNode(
val num: Int,
val longName: String,
val shortName: String,
val snr: Float,
val hopsAway: Int?,
val lastHeard: Int,
val viaMqtt: Boolean,
// Enriched fields
val isOnline: Boolean,
val connectionQuality: ConnectionQuality,
val signalQuality: SignalQuality,
val batteryLevel: Int?,
val voltage: Float?,
val channelUtilization: Float?,
val airUtilTx: Float?,
val latitude: Double?,
val longitude: Double?,
val altitude: Int?,
val isFavorite: Boolean,
val isIgnored: Boolean,
val isMuted: Boolean,
val hwModel: String?,
)
private fun MeshNode.toUiNode() = UiNode(
num = nodeNum,
longName = longName ?: "Unknown",
shortName = shortName ?: "?",
snr = snr,
hopsAway = hopsAway,
lastHeard = lastHeard,
viaMqtt = viaMqtt,
isOnline = isOnline,
connectionQuality = connectionQuality,
signalQuality = signalQuality,
batteryLevel = batteryLevel,
voltage = voltage,
channelUtilization = channelUtilization,
airUtilTx = airUtilTx,
latitude = latitude,
longitude = longitude,
altitude = altitude,
isFavorite = isFavorite,
isIgnored = isIgnored,
isMuted = isMuted,
hwModel = hwModel?.name,
)
/**
* POC ViewModel that drives a node list directly from the SDK's [org.meshtastic.sdk.RadioClient].
*
* **Fold pattern:**
* 1. `flatMapLatest` switches to the new client's `nodes` flow whenever [RadioClientProvider] replaces the active
* client.
* 2. `.catch {}` before `.scan {}` so that a transport error re-emits a safe [NodeChange.Snapshot] (empty map) rather
* than terminating the downstream scan accumulator.
* 3. `.scan {}` folds delta events — [NodeChange.Added], [NodeChange.Updated], [NodeChange.Removed] — onto the
* accumulator map. The initial [NodeChange.Snapshot] is guaranteed by the SDK for every new subscriber; no explicit
* replay config needed.
* 4. `.flowOn(Dispatchers.Default)` keeps folding off the main thread.
* 5. `.stateIn(WhileSubscribed(5_000))` keeps the upstream alive for 5 s after the last subscriber (safe across config
* changes; SDK re-sends a Snapshot for later subscribers).
*
* This ViewModel is registered as a Koin singleton alongside [RadioClientViewModel]. Both are instantiated at
* [org.meshtastic.app.ui.MainScreen] startup so the node map is warm before any screen subscribes.
*/
@KoinViewModel
class SdkNodeListViewModel(provider: RadioClientProvider) : ViewModel() {
val nodes: StateFlow<List<UiNode>> =
provider.client
.flatMapLatest { client ->
if (client == null) return@flatMapLatest flowOf(emptyList())
client.nodes
.catch { e ->
Logger.e(e) { "[SDK] nodes flow error — resetting to empty" }
emit(NodeChange.Snapshot(emptyMap()))
}
.scan(emptyMap<NodeId, NodeInfo>()) { acc, change ->
when (change) {
is NodeChange.Snapshot -> change.nodes
is NodeChange.Added -> acc + (NodeId(change.node.num) to change.node)
is NodeChange.Updated -> acc + (NodeId(change.node.num) to change.node)
is NodeChange.Removed -> acc - change.nodeId
}
}
.map { nodeMap ->
val now = (System.currentTimeMillis() / 1000).toInt()
nodeMap.values.map { it.toMeshNode(now).toUiNode() }
}
.flowOn(Dispatchers.Default)
}
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())
}

View File

@@ -1,96 +0,0 @@
/*
* 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.app.radio
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import co.touchlab.kermit.Logger
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import org.koin.core.annotation.KoinViewModel
import org.meshtastic.proto.Telemetry
import org.meshtastic.sdk.AdminResult
import org.meshtastic.sdk.NodeId
/**
* POC ViewModel that surfaces per-node telemetry from [TelemetryApi.observe].
*
* **Gap D verified:** [TelemetryApi.observe] returns a plain [kotlinx.coroutines.flow.Flow] of unsolicited periodic
* [Telemetry] packets (device metrics, environment metrics, etc.). It does NOT auto-poll — packets arrive only when the
* radio pushes them. To request an immediate telemetry update, call [requestDeviceMetrics] which issues an RPC.
*
* Telemetry fields are nullable (Wire proto) — check per-field before display: [Telemetry.device_metrics],
* [Telemetry.environment_metrics], [Telemetry.air_quality_metrics], [Telemetry.power_metrics]
*
* Usage: observe [deviceMetrics] / [environmentMetrics] in a node-detail Composable, call [requestDeviceMetrics] on
* screen entry to prime the display.
*/
@Suppress("MagicNumber")
@KoinViewModel
class SdkTelemetryViewModel(private val provider: RadioClientProvider) : ViewModel() {
/**
* Observe all raw [Telemetry] packets for [nodeId].
*
* Re-subscribes automatically when [RadioClientProvider.client] changes (reconnect). Errors are caught and logged —
* the flow resets to null rather than crashing.
*/
private fun telemetryFor(nodeId: NodeId): StateFlow<Telemetry?> = provider.client
.flatMapLatest { c ->
if (c == null) {
flowOf(null)
} else {
c.telemetry
.observe(nodeId)
.catch { e ->
Logger.e(e) { "[SDK] telemetry.observe(${nodeId.raw}) error" }
emit(Telemetry())
}
.map { it as Telemetry? }
}
}
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null) // 5s screen-death buffer
/** Latest telemetry (any type) for the local node (NodeId.LOCAL). */
val localTelemetry: StateFlow<Telemetry?> = telemetryFor(NodeId.LOCAL)
/**
* Request an immediate device-metrics telemetry packet from [nodeId]. The result will be pushed back through
* [telemetryFor]'s [TelemetryApi.observe] flow.
*/
fun requestDeviceMetrics(nodeId: NodeId = NodeId.LOCAL) {
val client = provider.client.value ?: return
viewModelScope.launch {
when (val r = client.telemetry.requestDevice(nodeId)) {
is AdminResult.Success -> Logger.d { "[SDK] requestDeviceMetrics(${nodeId.raw}): ${r.value}" }
else -> Logger.w { "[SDK] requestDeviceMetrics(${nodeId.raw}) failed: $r" }
}
}
}
/**
* Build a per-node telemetry StateFlow for a specific node num. Compose screens can call this once per node-detail
* screen.
*/
fun observeNode(nodeNum: Int): StateFlow<Telemetry?> = telemetryFor(NodeId(nodeNum))
}

View File

@@ -29,11 +29,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
import co.touchlab.kermit.Logger
import kotlinx.coroutines.flow.map
import org.koin.compose.viewmodel.koinViewModel
import org.meshtastic.app.BuildConfig
import org.meshtastic.app.radio.RadioClientViewModel
import org.meshtastic.app.radio.SdkNodeListViewModel
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.navigation.NodesRoute
import org.meshtastic.core.navigation.TopLevelDestination
@@ -57,15 +54,6 @@ import org.meshtastic.feature.wifiprovision.navigation.wifiProvisionGraph
@Composable
fun MainScreen() {
val viewModel: UIViewModel = koinViewModel()
// Instantiate the SDK ViewModel so event collection starts at app launch (Eagerly scope).
// sdkConnectionLabel logged below for POC visibility; will feed the connection toolbar later.
val radioClientViewModel: RadioClientViewModel = koinViewModel()
// Warm the SDK node list at launch so it's ready before any screen subscribes.
val sdkNodeListViewModel: SdkNodeListViewModel = koinViewModel()
val sdkNodeCount by sdkNodeListViewModel.nodes.map { it.size }.collectAsStateWithLifecycle(initialValue = 0)
val sdkLabel by radioClientViewModel.sdkConnectionLabel.collectAsStateWithLifecycle()
LaunchedEffect(sdkLabel) { Logger.d { sdkLabel } }
LaunchedEffect(sdkNodeCount) { Logger.d { "SDK nodes: $sdkNodeCount" } }
// Land on Connections for first-run / no-device-selected; otherwise on Nodes. Read synchronously
// from the StateFlow (seeded from persisted prefs) so the initial tab is set in one shot.
val initialTab =

View File

@@ -1,80 +0,0 @@
/*
* 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.app.service
import dev.mokkery.MockMode
import dev.mokkery.mock
import org.meshtastic.core.model.Node
import org.meshtastic.core.repository.MeshServiceNotifications
import org.meshtastic.core.repository.RadioInterfaceService
import org.meshtastic.proto.ClientNotification
import org.meshtastic.proto.Telemetry
class Fakes {
val service: RadioInterfaceService = mock(MockMode.autofill)
}
class FakeMeshServiceNotifications : MeshServiceNotifications {
override fun clearNotifications() {}
override fun initChannels() {}
override fun updateServiceStateNotification(
state: org.meshtastic.core.model.ConnectionState,
telemetry: Telemetry?,
) {}
override suspend fun updateMessageNotification(
contactKey: String,
name: String,
message: String,
isBroadcast: Boolean,
channelName: String?,
isSilent: Boolean,
) {}
override suspend fun updateWaypointNotification(
contactKey: String,
name: String,
message: String,
waypointId: Int,
isSilent: Boolean,
) {}
override suspend fun updateReactionNotification(
contactKey: String,
name: String,
emoji: String,
isBroadcast: Boolean,
channelName: String?,
isSilent: Boolean,
) {}
override fun showAlertNotification(contactKey: String, name: String, alert: String) {}
override fun showNewNodeSeenNotification(node: Node) {}
override fun showOrUpdateLowBatteryNotification(node: Node, isRemote: Boolean) {}
override fun showClientNotification(clientNotification: ClientNotification) {}
override fun cancelMessageNotification(contactKey: String) {}
override fun cancelLowBatteryNotification(node: Node) {}
override fun clearClientNotification(notification: ClientNotification) {}
}

View File

@@ -22,7 +22,7 @@ package org.meshtastic.core.repository
* Stage 1 (`CONFIG_NONCE`): requests device config, module config, and channels. Stage 2 (`NODE_INFO_NONCE`): requests
* the full node database.
*
* Both [MeshConfigFlowManager] (consumer) and [MeshConnectionManager] (sender) reference these.
* Both the SDK state bridge (consumer) and handshake initiator (sender) reference these.
*/
object HandshakeConstants {
/** Nonce sent in `want_config_id` to request config-only (Stage 1). */

View File

@@ -32,12 +32,8 @@ import org.meshtastic.proto.MeshPacket
* maintains reactive flows for connection status, error messages, and incoming mesh traffic.
*
* **Connection state contract:** [connectionState] is the **canonical, app-level** connection state that all UI,
* feature modules, and ViewModels should observe. It incorporates handshake progress, light-sleep policy, and transport
* reconciliation — unlike [RadioInterfaceService.connectionState], which only reflects the raw hardware link status.
* The [MeshConnectionManager] is the sole writer of this state; it bridges [RadioInterfaceService.connectionState]
* changes into app-level transitions via [setConnectionState].
*
* @see RadioInterfaceService.connectionState
* feature modules, and ViewModels should observe. The SDK's [SdkStateBridge] is the sole writer of this state;
* it maps SDK connection events into app-level transitions via [setConnectionState].
*/
@Suppress("TooManyFunctions")
interface ServiceRepository {
@@ -45,24 +41,21 @@ interface ServiceRepository {
* Canonical app-level connection state.
*
* This is the **single source of truth** for connection status across the entire application. All UI components,
* feature modules, and ViewModels should observe this flow — never [RadioInterfaceService.connectionState].
* feature modules, and ViewModels should observe this flow.
*
* State transitions are managed exclusively by [MeshConnectionManager], which reconciles transport-level events
* with handshake progress and device sleep policy:
* State transitions are managed by [SdkStateBridge], which maps SDK connection events into app-level transitions:
* - [ConnectionState.Disconnected] — no active connection to a radio
* - [ConnectionState.Connecting] — transport is up, mesh handshake (config + node-info) in progress
* - [ConnectionState.Connected] — handshake complete, radio fully operational
* - [ConnectionState.DeviceSleep] — radio entered light-sleep (transient disconnect)
*
* @see RadioInterfaceService.connectionState
*/
val connectionState: StateFlow<ConnectionState>
/**
* Updates the canonical app-level connection state.
*
* **This should only be called by [MeshConnectionManager].** Direct mutation from other components would bypass the
* transport-to-app reconciliation logic and create state inconsistencies.
* **This should only be called by [SdkStateBridge].** Direct mutation from other components would bypass the
* SDK-to-app state mapping logic and create state inconsistencies.
*
* @param connectionState The new [ConnectionState].
*/

View File

@@ -42,7 +42,7 @@ import org.meshtastic.proto.MeshPacket
@Suppress("TooManyFunctions")
open class ServiceRepositoryImpl : ServiceRepository {
// Canonical app-level connection state — written exclusively by MeshConnectionManager.
// Canonical app-level connection state — written exclusively by SdkStateBridge.
private val _connectionState: MutableStateFlow<ConnectionState> = MutableStateFlow(ConnectionState.Disconnected)
override val connectionState: StateFlow<ConnectionState>
get() = _connectionState

View File

@@ -23,8 +23,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.emptyFlow
import org.meshtastic.core.model.DeviceType
import org.meshtastic.core.model.InterfaceId
import org.meshtastic.core.network.repository.MQTTRepository
import org.meshtastic.core.repository.AppWidgetUpdater
import org.meshtastic.core.repository.DataPair
@@ -33,7 +31,6 @@ import org.meshtastic.core.repository.LocationRepository
import org.meshtastic.core.repository.MeshLocationManager
import org.meshtastic.core.repository.MeshWorkerManager
import org.meshtastic.core.repository.PlatformAnalytics
import org.meshtastic.core.repository.RadioInterfaceService
import org.meshtastic.proto.MqttClientProxyMessage
import org.meshtastic.mqtt.ConnectionState as MqttConnectionState
import org.meshtastic.proto.Position as ProtoPosition
@@ -54,32 +51,6 @@ private fun logWarn(message: String) {
Logger.w(tag = TAG) { message }
}
// region Transport / Radio Stubs (Android BLE/USB — no commonMain impl)
class NoopRadioInterfaceService : RadioInterfaceService {
override val supportedDeviceTypes: List<DeviceType> = emptyList()
override val currentDeviceAddressFlow = MutableStateFlow<String?>(null)
override fun isMockTransport(): Boolean = false
override fun connect() {
logWarn("NoopRadioInterfaceService.connect()")
}
override suspend fun disconnect() {
logWarn("NoopRadioInterfaceService.disconnect()")
}
override fun getDeviceAddress(): String? = null
override fun setDeviceAddress(deviceAddr: String?): Boolean = false
override fun toInterfaceAddress(interfaceId: InterfaceId, rest: String): String = ""
}
// endregion
// region Notification / Platform Stubs (Android-only)
class NoopPlatformAnalytics : PlatformAnalytics {