diff --git a/MIGRATION-REMAINING.md b/MIGRATION-REMAINING.md index 3a86cf3f0..00a3a5013 100644 --- a/MIGRATION-REMAINING.md +++ b/MIGRATION-REMAINING.md @@ -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` 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`. 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 diff --git a/app/src/main/kotlin/org/meshtastic/app/radio/RadioClientViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/radio/RadioClientViewModel.kt deleted file mode 100644 index 6e10501a1..000000000 --- a/app/src/main/kotlin/org/meshtastic/app/radio/RadioClientViewModel.kt +++ /dev/null @@ -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 . - */ -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` 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 = - 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 = - 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" -} diff --git a/app/src/main/kotlin/org/meshtastic/app/radio/SdkConfigViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/radio/SdkConfigViewModel.kt deleted file mode 100644 index 9f0f55b46..000000000 --- a/app/src/main/kotlin/org/meshtastic/app/radio/SdkConfigViewModel.kt +++ /dev/null @@ -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 . - */ -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 = - 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 = - 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 = - 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> = - 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" } } - } -} diff --git a/app/src/main/kotlin/org/meshtastic/app/radio/SdkMessagingViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/radio/SdkMessagingViewModel.kt deleted file mode 100644 index ce01c1fbd..000000000 --- a/app/src/main/kotlin/org/meshtastic/app/radio/SdkMessagingViewModel.kt +++ /dev/null @@ -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 . - */ -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` 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>(emptyList()) - val incomingMessages: StateFlow> = _incomingMessages.asStateFlow() - - private val _outboundStatuses = MutableStateFlow>(emptyList()) - val outboundStatuses: StateFlow> = _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 0–7; 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 - } -} diff --git a/app/src/main/kotlin/org/meshtastic/app/radio/SdkNodeListViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/radio/SdkNodeListViewModel.kt deleted file mode 100644 index 684af8f12..000000000 --- a/app/src/main/kotlin/org/meshtastic/app/radio/SdkNodeListViewModel.kt +++ /dev/null @@ -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 . - */ -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> = - 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()) { 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()) -} diff --git a/app/src/main/kotlin/org/meshtastic/app/radio/SdkTelemetryViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/radio/SdkTelemetryViewModel.kt deleted file mode 100644 index 2eaa5fb43..000000000 --- a/app/src/main/kotlin/org/meshtastic/app/radio/SdkTelemetryViewModel.kt +++ /dev/null @@ -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 . - */ -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 = 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 = 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 = telemetryFor(NodeId(nodeNum)) -} diff --git a/app/src/main/kotlin/org/meshtastic/app/ui/Main.kt b/app/src/main/kotlin/org/meshtastic/app/ui/Main.kt index 4adb1b472..46409b14e 100644 --- a/app/src/main/kotlin/org/meshtastic/app/ui/Main.kt +++ b/app/src/main/kotlin/org/meshtastic/app/ui/Main.kt @@ -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 = diff --git a/app/src/test/kotlin/org/meshtastic/app/service/Fakes.kt b/app/src/test/kotlin/org/meshtastic/app/service/Fakes.kt deleted file mode 100644 index 0da77c524..000000000 --- a/app/src/test/kotlin/org/meshtastic/app/service/Fakes.kt +++ /dev/null @@ -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 . - */ -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) {} -} diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/HandshakeConstants.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/HandshakeConstants.kt index f4a0d1fb3..f6455a8d3 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/HandshakeConstants.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/HandshakeConstants.kt @@ -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). */ diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/ServiceRepository.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/ServiceRepository.kt index 2a09e95c8..98104d05d 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/ServiceRepository.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/ServiceRepository.kt @@ -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 /** * 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]. */ diff --git a/core/service/src/commonMain/kotlin/org/meshtastic/core/service/ServiceRepositoryImpl.kt b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/ServiceRepositoryImpl.kt index 5ad5c2d00..2832a6de9 100644 --- a/core/service/src/commonMain/kotlin/org/meshtastic/core/service/ServiceRepositoryImpl.kt +++ b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/ServiceRepositoryImpl.kt @@ -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 = MutableStateFlow(ConnectionState.Disconnected) override val connectionState: StateFlow get() = _connectionState diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/stub/NoopStubs.kt b/desktop/src/main/kotlin/org/meshtastic/desktop/stub/NoopStubs.kt index 7a3e4b563..8eeec2dab 100644 --- a/desktop/src/main/kotlin/org/meshtastic/desktop/stub/NoopStubs.kt +++ b/desktop/src/main/kotlin/org/meshtastic/desktop/stub/NoopStubs.kt @@ -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 = emptyList() - - override val currentDeviceAddressFlow = MutableStateFlow(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 {