refactor: Remove AIDL API and modernize service architecture#5586
refactor: Remove AIDL API and modernize service architecture#5586jamesarich wants to merge 35 commits into
Conversation
2777fe8 to
3fde9cc
Compare
3fde9cc to
b304b0d
Compare
❌ 1 Tests Failed:
View the top 1 failed test(s) by shortest run time
To view more test analytics, go to the Test Analytics Dashboard |
✅ Docs staleness check passedThis PR includes updates to |
🖼️ Preview staleness check — advisoryThis PR modifies UI composables but does not update any
Changed UI files: What to check:
Adding previews checklist:
If this PR does not require preview updates (e.g., logic-only change, non-visual refactor), add the |
51fff3c to
6f447c8
Compare
237f5e1 to
059ee97
Compare
f475045 to
66ae840
Compare
Remove the deprecated AIDL/IPC API surface and perform deep architectural
modernization of the radio command pipeline, aligning with the meshtastic-sdk
AdminApiImpl pattern for future SDK migration.
Key changes:
1. AIDL Removal & Infrastructure Cleanup
- Delete core:api module and all AIDL interfaces
- Remove ServiceBroadcasts + CommonParcelable infrastructure
- Remove core:api from CI workflow lint/publish steps
2. Model Modernization
- Introduce NodeAddress sealed class with type-safe addressing
- Remove deprecated DataPacket constants in favor of NodeAddress
- Consolidate dual node maps into single source with getNodeById
- Split large model files, deduplicate NodeEntity, flatten RadioController
3. Service Layer Refactoring (SDK-aligned)
- Remove ServiceAction sealed class, use direct suspend calls
- Convert CommandSender & MeshActionHandler to suspend APIs
- Merge MeshActionHandler into DirectRadioControllerImpl
(ViewModel → RadioController → CommandSender, no intermediate layer)
- Build AdminMessage protos directly with typed protos end-to-end
- Apply structured concurrency to NodeRequestActions/NodeManagementActions
- Fix CancellationException handling throughout
Architecture (before → after):
ViewModel → RadioController → Handler → CommandSender → PacketHandler
ViewModel → RadioController → CommandSender → PacketHandler
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- NodeAddressTest: 37 tests covering sealed class parser, roundtrip, extensions (ContactKey, DataPacket), edge cases - CommandSenderImplTest: 20 tests covering packet ID generation, address resolution, sendData validation, admin messages, position - DirectRadioControllerImplTest: +8 tests for reboot/shutdown/factory reset, importContact, refreshMetadata - NodeManagerImplTest: +7 tests for getMyNodeInfo aggregation, getMyId, null telemetry handling Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
With AGP 9's built-in Kotlin, org.jetbrains.kotlin.android is no longer
applied to Android modules. Convention plugin hooks using
withPlugin("org.jetbrains.kotlin.android") were dead code — replaced
with withPlugin("com.android.application") and
withPlugin("com.android.library") which correctly trigger for
Android-only modules using built-in Kotlin.
- KoinConventionPlugin: split into app + library hooks
- AndroidRoomConventionPlugin: use com.android.library hook
- KotlinXSerializationConventionPlugin: use app + library hooks
- Remove kotlin-android from version catalog and root build.gradle.kts
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
No modules are published externally anymore (core:api was removed).
Remove all publishing-related configuration:
- Delete PublishingConventionPlugin and its registration
- Remove publish-core.yml workflow
- Strip publishing{} blocks from core/model and core/proto
- Remove PUBLISHED_MODULES set and Java 17 compatibility logic
- Unify all modules to JDK 21 toolchain and JVM target
- Remove unused imports (JavaToolchainService, JavaLanguageVersion, Test)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Remove x86/x86_64 from ABI filters (armeabi-v7a/arm64-v8a only) - Remove unused takpacket-sdk-jvm from version catalog - Raise core/proto minSdk from 21 to 26 (ATAK compat no longer needed) - Remove stale FIXME comment about foreground service in manifest - Replace Executors.newSingleThreadExecutor with Dispatchers.Default.asExecutor in BarcodeScannerProvider (removes manual thread pool management) - Convert formatAgo() to @composable with stringResource(), eliminating runBlocking from the UI rendering path. Non-composable callers (map views, accessibility) use a 3-arg overload with pre-resolved strings. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Wrap StateFlow.value read in remember{} to satisfy composition lint
- Wrap derivedStateOf in remember{} (MapView PurgeTileSourceDialog)
- Use mutableIntStateOf to avoid autoboxing (MapStyleDialog)
- Use ResourcesCompat.getDrawable() instead of deprecated getDrawable()
- Fix mixed indentation in MarkerClusterer.java
Lint result: 0 errors, 10 warnings (down from 2 errors, 12 warnings)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This feature flag is now enabled by default in the Compose compiler, making the explicit opt-in unnecessary. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- NodeAddress.idToNum: reject >32-bit hex instead of silently truncating; use toLongOrNull rather than runCatching; drop redundant double prefix-strip. - DirectRadioControllerImpl: compare destNum against the nullable myNodeNum.value (not the ?:0 getter) so local-side-effect branches don't fire spuriously for destNum=0 before connect. - DirectRadioControllerImpl.setModuleConfig: move node_status optimistic write inside the local-node guard so we don't overwrite remote status. - DirectRadioControllerImpl.sendReaction: use ContactKey wrapper instead of manual [0].digitToInt()/substring(1). - DirectRadioControllerImpl.importContact: respect incoming manually_verified flag; early-return on node_num==0 or null user. - DirectRadioControllerImpl.requestPosition: consolidate when-chain; document Position(0,0,0) as protocol "no position" sentinel. - ContactKey: accessors safe against empty value; non-digit first char defaults channel to 0 rather than throwing. - NodeDetailViewModel.openRemoteAdmin: atomic compareAndSet replaces check-then-act TOCTOU that allowed double-tap to queue two passkey exchanges + duplicate navigation events. - MessageViewModel.frequentEmojis: toIntOrNull + mapNotNull so a corrupted pref entry no longer crashes every recomposition. - AndroidMeshLocationManager.restart: log the silent-bail case (no-op until MeshConnectionManagerImpl wires us up). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- RadioController.setDeviceAddress is now suspend. Callers can rely on the
device switch completing (DB switched, node DB cleared, transport
reconfigured) before their next call — the old non-suspend impl wrapped
the work in scope.launch{} and returned immediately, racing against
follow-up operations like OTA disconnect-then-delay.
- DirectRadioControllerImpl: drops the scope.launch wrapper.
- FakeRadioController: matches the new suspend contract so tests no longer
hide ordering bugs that production would exhibit.
- UIViewModel / ScannerViewModel: wrap the suspend call in safeLaunch.
- Esp32OtaUpdateHandler.disconnectMeshService: marked suspend (caller is
already inside a withContext block).
- MeshServiceOrchestrator: replace plain `var scope: CoroutineScope?` with
an atomicfu AtomicRef and compareAndSet on start(). Concurrent start()
invocations could otherwise both observe the slot empty, both allocate
scopes, and the second overwrite the first — orphaning the first scope's
collectors on radioInterfaceService.receivedData.
- PacketHandlerImpl.sendToRadio: document the FIFO invariant. Mutex is
not strictly fair across coroutines, but the only ordering callers rely
on is within a single coroutine (e.g. InstallProfileUseCase issues
beginEditSettings → install* → commitEditSettings sequentially in one
suspend), where sequential mutex acquisition preserves order.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ssertions - MeshServiceOrchestrator.start(): Replace non-atomic dead-scope replacement with a proper CAS loop to prevent duplicate startup from concurrent callers - DirectRadioControllerImplTest: Replace no-op atLeast(0) assertions with exactly(0) so early-return guards are actually verified - FakeRadioController: Default connectionState to Disconnected (matches real ServiceRepositoryImpl initial state); fix mismatched parameter name - SettingsViewModelTest: Update isConnected test to reflect corrected initial state - Remove stale core:api references from .skills/ documentation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…stale comments - MeshServiceOrchestrator: Remove unused MeshRouter injection (was a pure service-locator facade from the old action-dispatch pattern, never called) - MeshService.onBind(): Add clarifying comment that this is a started service - MeshServiceStarter: Replace stale 'binding' comment with accurate description Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
MeshRouter was a pure pass-through with zero routing logic — just 7 lazy property getters wrapping handler interfaces. Inline the actual handler dependencies directly into their consumers: - FromRadioPacketHandlerImpl: inject configFlowManager, configHandler, xmodemManager as Lazy<T> params (was router.value.X) - MeshMessageProcessorImpl: inject dataHandler as Lazy<MeshDataHandler> (was router.value.dataHandler) Delete MeshRouter interface, MeshRouterImpl, and MeshRouterImplTest. Update tests to mock handlers directly instead of mocking the router. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Zero-overhead wrapper eliminates boxing allocation for version comparisons. Move parsing logic to companion with pre-compiled regex for efficiency. Drop Logger side-effect from parsing (invalid versions already return 0 gracefully; callers that need diagnostics log independently). All DeviceVersionTest cases pass unchanged — equality, comparison, and edge-case handling preserved. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Cover the newly-exposed handler (previously hidden behind MeshRouter): - Stores lastNeighborInfo only for own node - Ignores remote node packets - Sets response on serviceRepository - Handles null decoded gracefully - Includes duration when start time recorded Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
importContact previously marked imported contacts as manually_verified; the refactor dropped that, forwarding the proto default (false) instead. A QR-scanned contact arrives unverified, so importing it (scanning the code) is itself an act of manual verification and must be recorded as such. Force manually_verified = true on both the outgoing admin message and the local node update, restoring the original handleImportContact semantics, and assert the flag in the test. Also import the proto types in AdminController/MessagingController interfaces instead of using inline fully-qualified names. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Six call sites hand-rolled the same fragile contact-key parse (`contactKey[0].digitToIntOrNull()` + `substring(1)`). Move that logic into the ContactKey value class introduced with this branch: - Add `channelOrNull: Int?` which preserves the semantically meaningful "no leading channel digit == legacy unprefixed DM" signal that the existing `channel: Int` collapses to 0. SendMessageUseCase relies on this distinction to classify direct messages, so the naive `channel` accessor would have been a regression. - Make `addressString` respect it: returns the whole key when there is no channel prefix (matching the old defensive behaviour) instead of blindly dropping the first character. Migrated SendMessageUseCase, BaseMapViewModel, ReplyReceiver, Message, Contacts, and ContactItem onto the typed accessors. Added ContactKey unit tests for the prefixed / unprefixed / empty cases. Also removed a dead empty `companion object` left on DataPacket after its constants moved to NodeAddress. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
NodeController toggled favorite/ignore state by reading the current node then flipping it (read-modify-write), which races concurrent callers and forced a latent bug in SendMessageUseCase: it only ever wants to favorite a node, but called a toggle, so a node that became favorite between the guard and the call would have been un-favorited. Replace with explicit-target idempotent operations mirroring the SDK's AdminApi: - favoriteNode(num) -> setFavorite(num, Boolean) - ignoreNode(num) -> setIgnored(num, Boolean) - muteNode(num) -> toggleMuted(num) (mute is genuinely a firmware toggle) Impls no-op when the node is already in the requested state. Callers (NodeManagementActions confirm dialogs) pass !node.isFavorite from the state the UI showed the user; SendMessageUseCase passes favorite = true. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The transactional config-write API exposed raw beginEditSettings /
commitEditSettings pairs, so callers had to remember to commit and could
leak a half-open session. Replace with a scoped builder mirroring the
SDK's AdminApi.editSettings { }:
radioController.editSettings(destNum) {
setOwner(user); setConfig(config); setModuleConfig(...)
}
The new AdminEditScope binds operations to the session's destination, so
InstallProfileUseCase no longer threads destNum + getPacketId() through
every call — its install* helpers become AdminEditScope extensions,
dropping a large amount of boilerplate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
DirectRadioControllerImpl had absorbed the old MeshActionHandler and grown into a ~450-line, 17-dependency God object spanning messaging, node management, configuration, device lifecycle, and address switching. Decompose the command logic into four cohesive collaborators, one per RadioController sub-interface, mirroring the SDK's layered API design: - AdminControllerImpl -> AdminController (~SDK AdminApiImpl) - MessagingControllerImpl-> MessagingController(~SDK RadioClient.send*) - NodeControllerImpl -> NodeController (~SDK AdminApi node ops) - RequestControllerImpl -> RequestController (~SDK Telemetry/RoutingApi) DirectRadioControllerImpl becomes a thin composition root that assembles the four via Kotlin interface delegation (`by`) and keeps only the cross-cutting concerns (connection state, packet-id, location, device address). Its constructor signature is unchanged, so DI wiring (Android + desktop) and the existing integration test pass untouched. Each collaborator is now independently testable and becomes a thin SDK adapter at migration time. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
TracerouteHandlerImpl and NeighborInfoHandlerImpl each carried an identical copy of the request-timing machinery: an atomic persistentMapOf<Int, Long> of start times, a recordStartTime that stamps nowMillis, and a "consume the start time, format a `Duration: N s` suffix, log completion" block plus a MILLIS_PER_SECOND constant. Extract it into a single internal RequestTimer with start() and appendDuration(). Both handlers now hold a private RequestTimer and delegate, dropping the duplicated field, imports, formatting, and constant. Behavior is unchanged (covered by the existing NeighborInfoHandlerImplTest duration case); adds focused RequestTimer unit tests for the recorded/not-recorded/single-use/independent-ids cases. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per AGENTS.md governance ("update session_context.md at the end of every
major task"). Records the AIDL/broadcast removal, the RadioController
sub-controller split, typed addressing (NodeAddress/ContactKey), the
idempotent node ops + editSettings DSL, the RequestTimer extraction, and
the deliberate deferral of R4 (AdminResult) and R5 (NodeId) to the SDK
migration.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The :core:service, :core:repository, and :core:model READMEs still described the removed AIDL/broadcast architecture and deleted types. - core/service: replace the `ServiceAction` (intent bus) section with DirectRadioControllerImpl and its four sub-controllers + editSettings. - core/repository: drop MeshActionHandler.kt / MeshRouter.kt from the source listing and add the RadioController sub-interfaces; fix the ServiceRepository sketch (no serviceAction/onServiceAction) and installConfig (List<Node>, not List<NodeInfo>). - core/model: the Parcelable/@parcelize section is gone (Parcelable was removed from :core:common) — models are now @serializable / value classes; replace deleted `NodeInfo` with `Node` and note NodeAddress/ContactKey typed addressing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a "Radio Control" section to the developer architecture guide covering how features issue radio commands: the RadioController composite, its four sub-interfaces (Admin/Messaging/Node/Request), and DirectRadioControllerImpl as the in-process composition root that assembles them via interface delegation. Notes the fire-and-forget admin model and the deliberate alignment with the meshtastic-sdk API shape. Additive only (no stale-reference fix); bumps last_updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Apply Interface Segregation Principle to ServiceRepository and RadioController consumers: New interfaces extracted from ServiceRepository: - ConnectionStateProvider: read-only connectionState access - TracerouteResponseProvider: traceroute response state + clear - NeighborInfoResponseProvider: neighbor info response state + clear - ServiceStateWriter: write-side for handlers (set*, emit*, clear*) RadioController now extends ConnectionStateProvider, and sub-controller interfaces (AdminController, MessagingController, NodeController, RequestController) are bound in DI for fine-grained injection. ViewModel narrowing: - MessageViewModel: RadioController+ServiceRepository → MessagingController+ConnectionStateProvider - NodeDetailViewModel: RadioController → RequestController - NodeListViewModel: RadioController+ServiceRepository → AdminController+ConnectionStateProvider - ContactsViewModel: ServiceRepository → ConnectionStateProvider - MetricsViewModel: ServiceRepository → TracerouteResponseProvider All tests updated to use narrowed interfaces. Koin DI bindings updated for both Android (@single binds) and Desktop (manual single<> declarations). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Update core/repository README and architecture docs to reflect the new focused provider interfaces (ConnectionStateProvider, TracerouteResponseProvider, NeighborInfoResponseProvider, ServiceStateWriter) and ViewModel narrowing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ateProvider Following the ServiceRepository ISP decomposition, the two consumers that only read connectionState now inject ConnectionStateProvider instead of the full ServiceRepository: - ConnectionsViewModel (core:ui) - LocalStatsWidgetStateProvider (feature:widget) The remaining three full-ServiceRepository injections are left as-is because they use members not carried by any narrow provider: RadioConfigViewModel (meshPacketFlow), ScannerViewModel (connectionProgress), and UIViewModel (clientNotification + errorMessage reads). Extracting single-consumer providers for those would be over-segregation. Koin graph verifies on Android + desktop; ConnectionsViewModelTest green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The ISP decomposition extracted ServiceStateWriter but left it with zero adopters — every write-side handler still injected the full ServiceRepository, able to read state it only writes. Complete the adoption: - 9 pure writers now inject ServiceStateWriter (param renamed serviceStateWriter): TracerouteHandlerImpl, NeighborInfoHandlerImpl, MeshMessageProcessorImpl, MeshConfigFlowManagerImpl, MeshConfigHandlerImpl, MeshDataHandlerImpl, FromRadioPacketHandlerImpl, MqttManagerImpl, MeshServiceOrchestrator. - PacketHandlerImpl reads only connectionState -> ConnectionStateProvider. The two genuinely mixed read+write holders (MeshConnectionManagerImpl, DirectRadioControllerImpl) keep the full ServiceRepository. Read-side flows with a single consumer (connectionProgress/clientNotification/ errorMessage/meshPacketFlow, mostly UIViewModel) are intentionally not extracted into providers — that would be over-segregation. Koin graph verifies on Android + desktop; core:data/core:service suites green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Audit-driven renames: - DirectRadioControllerImpl -> RadioControllerImpl: the "Direct" prefix was vestigial (it once contrasted with the AIDL-routed AndroidRadioControllerImpl, now deleted); it is the sole impl and now matches its parts (AdminControllerImpl, etc.). - RadioController.getPacketId() -> generatePacketId(): a `get`-prefixed function that generates a fresh id each call violates the getter-is-idempotent convention; also aligns with CommandSender.generatePacketId(). - RequestController -> QueryController (+ Impl, + the `requestController` params/mocks): clearer intent for the pull/query surface; "request" was generic. - RequestTimer param `label` -> `logLabel`. - AdminControllerImpl DEFAULT_REBOOT_DELAY -> DEFAULT_DELAY_SECONDS (shared by reboot + shutdown; conveys the unit). Interface-consumer-safe; docs/READMEs/architecture guide updated to match. Koin graph verifies on Android + desktop; affected test suites green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…l-api # Conflicts: # .agent_memory/session_context.md
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Squash merge of PR #5586 into release/2.8.0. Remove the deprecated AIDL API and all associated infrastructure. Replaces with a modern, testable, KMP-compatible architecture. 264 files changed with a net reduction of ~2,559 lines. Introduces RadioController, MessageQueue, and typed ContactKey abstractions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔗 Release 2.8.0 Integration ReportBranch: Integration Changes Required
Merge Guidance
|
Wrap database write operations (clearUnreadCount, clearAllUnreadCounts, updateLastReadMessage, insertRoomPacket) with NonCancellable to prevent connection pool leaks when the calling coroutine scope is cancelled mid-write. Found during release/2.8.0 integration testing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sort imports alphabetically and format single-line lambdas properly to pass spotlessCheck and detekt. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Summary
Remove the deprecated AIDL API and all associated infrastructure, unlocking a comprehensive modernization of the radio communication architecture. This PR eliminates thousands of lines of legacy code and replaces it with a modern, testable, KMP-compatible architecture.
262 files changed: +3,978 / −6,537 (net −2,559 lines)
Before: AIDL-based Architecture
graph TD subgraph "Client Apps / UI" VM[ViewModels] end subgraph "core:api (AIDL)" AIDL[IMeshService.aidl] Binder[Bound Service Binder] BC[Broadcast Receivers] end subgraph "Service Layer" MS[MeshService<br/>God-class] MR[MeshRouter<br/>Service Locator] end subgraph "Handlers" FRP[FromRadioPacketHandler] MMP[MeshMessageProcessor] MDH[MeshDataHandler] MCH[MeshConfigHandler] NIH[NeighborInfoHandler] TRH[TracerouteHandler] end subgraph "Radio" DRC[DirectRadioControllerImpl<br/>~450 lines, 17 deps, 44 functions] end VM -->|"IPC / Binder"| AIDL AIDL --> Binder Binder --> MS MS -->|"broadcasts"| BC BC -->|"intents"| VM MS --> MR MR -->|"lazy getters"| FRP MR -->|"lazy getters"| MMP MR -->|"lazy getters"| MDH MR -->|"lazy getters"| MCH MR -->|"lazy getters"| NIH MR -->|"lazy getters"| TRH MS --> DRC style AIDL fill:#ff6b6b,stroke:#c92a2a style Binder fill:#ff6b6b,stroke:#c92a2a style BC fill:#ff6b6b,stroke:#c92a2a style MR fill:#ff6b6b,stroke:#c92a2a style MS fill:#ffa94d,stroke:#e67700 style DRC fill:#ffa94d,stroke:#e67700After: Modern KMP Architecture
graph TD subgraph "UI Layer" VM[ViewModels] end subgraph "core:repository — segregated interfaces" CMD["Command interfaces<br/>Admin · Messaging · Node · Query"] READ["Read providers<br/>ConnectionState · Traceroute · NeighborInfo"] WRITE["ServiceStateWriter<br/>(write-only)"] end subgraph "core:service / core:data" DRC[RadioControllerImpl<br/>delegates to 4 sub-controllers] SR[ServiceRepositoryImpl] CS[CommandSender] H[Packet handlers<br/>direct injection, no service locator] end VM -->|"suspend calls"| CMD VM -->|"observe StateFlow"| READ DRC -.implements.-> CMD SR -.implements.-> READ SR -.implements.-> WRITE DRC --> CS H -->|"set* / emit*"| WRITE CS -->|"sends to radio"| HWhat was removed
core:apimodule — AIDL interfaces, bound service, broadcast-based command dispatchServiceBroadcasts/ServiceAction— intent-based command routing and status broadcastsMeshActionHandler— folded into the radio controllerMeshRouter— service-locator facade with zero routing logicrunBlockingin UI paths, redundant compiler flags, stalekotlin-androidplugin references, Parcelable plumbing incore:modelArchitecture improvements
Radio controller redesign
RadioControllersplit into 4 focused sub-interfaces:AdminController,MessagingController,NodeController,QueryControllerRadioControllerImpl, a thin composition root that assembles four focused impls (AdminControllerImpl,MessagingControllerImpl,NodeControllerImpl,QueryControllerImpl) via Kotlin interface delegation; its constructor is unchanged so DI + the integration test are untouchedServiceRepository ISP decomposition
ServiceRepositorydecomposed into focused sub-interfaces:ConnectionStateProvider,TracerouteResponseProvider,NeighborInfoResponseProvider,ServiceStateWriter(ServiceRepositoryextends all for compatibility)MessageViewModel→MessagingController+ConnectionStateProviderNodeListViewModel→AdminController+ConnectionStateProviderNodeDetailViewModel→QueryControllerContactsViewModel,ConnectionsViewModel,LocalStatsWidgetStateProvider→ConnectionStateProviderMetricsViewModel→TracerouteResponseProviderServiceStateWriterinstead of the full repository (TracerouteHandlerImpl,NeighborInfoHandlerImpl,MeshMessageProcessorImpl,MeshConfigFlowManagerImpl,MeshConfigHandlerImpl,MeshDataHandlerImpl,FromRadioPacketHandlerImpl,MqttManagerImpl,MeshServiceOrchestrator);PacketHandlerImpl(read-only) injectsConnectionStateProvider. The genuinely mixed read+write holders (MeshConnectionManagerImpl,RadioControllerImpl) keep the full interface.@Single(binds=[...])) and Desktop (single<>) Koin modules; verified byKoinVerificationTest(Android) andDesktopKoinTestNew abstractions
NodeAddresssealed class — type-safe node addressing (Local, Broadcast, ByNum, ById)ContactKeyvalue class — consolidated contact-key parsing (was duplicated in 6 places), with a nullable-channel accessor that preserves legacy-DM semanticsCommandSender— replaces stringly-typed sender identificationDeviceVersion@JvmInline value class— zero-overhead version comparison with pre-compiled regexRequestTimer— shared request-timing utility extracted from NeighborInfo/Traceroute handlerseditSettings { }DSL (AdminEditScope) — replaces begin/commitEditSettings ceremony and removes destNum/packetId boilerplateBehavioral improvements
setFavorite/setIgnored(Boolean)skip the radio command when state is unchanged (replacing toggles; mirrors the SDK and fixes a latent un-favorite bug inSendMessageUseCase)start()eliminates a race conditionSDK alignment
The interface shapes deliberately mirror meshtastic-sdk (
AdminApi/TelemetryApi/RoutingApi, layered controllers) to ease a future migration. Two further SDK-alignment items —AdminResult<T>return types (R4) and aNodeIdvalue class (R5) — were investigated and deferred to the SDK migration: R4 would reimplement the SDK's admin RPC engine and reverse the intentional fire-and-forget design; R5 is a ~350-site change that overlapsNodeAddress.ByNum. Both come naturally with the SDK swap.Testing
RadioControllerImplTest,NodeAddressTest,CommandSenderImplTest,NeighborInfoHandlerImplTest,RequestTimerTest,ConnectionsViewModelTestatLeast(0)withexactly(0); added idempotency no-op assertions)Build
OptimizeNonSkippingGroupscompiler flagHttpClienttagDocumentation
core:service,core:repository,core:model); module dependency graphs regenerated