From 664ebf218ef6b24f70d8175742b3845cf9fee9c4 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Mon, 23 Mar 2026 18:17:50 -0500 Subject: [PATCH] refactor: null safety, update date/time libraries, and migrate tests (#4900) Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> --- .../app/map/FdroidMapViewProvider.kt | 1 + .../kotlin/org/meshtastic/app/map/MapView.kt | 66 +- .../meshtastic/app/map/MapViewExtensions.kt | 21 +- .../app/map/component/EditWaypointDialog.kt | 26 +- .../kotlin/org/meshtastic/app/map/MapView.kt | 50 +- .../org/meshtastic/app/map/MapViewModel.kt | 47 +- .../org/meshtastic/buildlogic/Spotless.kt | 4 +- .../android_kable_migration_20260314/index.md | 5 - .../metadata.json | 8 - .../android_kable_migration_20260314/plan.md | 44 -- .../android_kable_migration_20260314/spec.md | 28 - .../archive/deep_dive_docs_20260316/index.md | 5 - .../deep_dive_docs_20260316/metadata.json | 8 - .../archive/deep_dive_docs_20260316/plan.md | 19 - .../archive/deep_dive_docs_20260316/spec.md | 19 - .../desktop_ble_kable_20260314/index.md | 5 - .../desktop_ble_kable_20260314/metadata.json | 8 - .../desktop_ble_kable_20260314/plan.md | 37 - .../desktop_ble_kable_20260314/spec.md | 31 - .../desktop_di_autowiring_20260313/index.md | 5 - .../metadata.json | 8 - .../desktop_di_autowiring_20260313/plan.md | 16 - .../desktop_di_autowiring_20260313/spec.md | 25 - .../archive/desktop_parity_20260311/index.md | 5 - .../desktop_parity_20260311/metadata.json | 8 - .../archive/desktop_parity_20260311/plan.md | 41 - .../archive/desktop_parity_20260311/spec.md | 25 - .../index.md | 5 - .../metadata.json | 8 - .../desktop_serial_transport_20260317/plan.md | 21 - .../desktop_serial_transport_20260317/spec.md | 20 - .../desktop_ux_enhancements_20260316/index.md | 8 - .../metadata.json | 7 - .../desktop_ux_enhancements_20260316/plan.md | 19 - .../desktop_ux_enhancements_20260316/spec.md | 10 - .../doc_consolidation_20260311/index.md | 5 - .../doc_consolidation_20260311/metadata.json | 8 - .../doc_consolidation_20260311/plan.md | 35 - .../doc_consolidation_20260311/spec.md | 13 - .../archive/expand_testing_20260318/index.md | 5 - .../expand_testing_20260318/metadata.json | 8 - .../archive/expand_testing_20260318/plan.md | 32 - .../archive/expand_testing_20260318/spec.md | 4 - .../index.md | 5 - .../metadata.json | 8 - .../plan.md | 33 - .../spec.md | 19 - .../index.md | 9 - .../metadata.json | 8 - .../plan.md | 25 - .../spec.md | 24 - .../index.md | 5 - .../metadata.json | 8 - .../plan.md | 37 - .../spec.md | 22 - .../index.md | 9 - .../metadata.json | 8 - .../plan.md | 23 - .../spec.md | 20 - .../index.md | 5 - .../metadata.json | 8 - .../plan.md | 29 - .../spec.md | 22 - .../extract_services_20260317/index.md | 5 - .../extract_services_20260317/metadata.json | 8 - .../archive/extract_services_20260317/plan.md | 44 -- .../archive/extract_services_20260317/spec.md | 32 - .../extract_viewmodels_20260316/index.md | 5 - .../extract_viewmodels_20260316/metadata.json | 8 - .../extract_viewmodels_20260316/plan.md | 20 - .../extract_viewmodels_20260316/spec.md | 20 - .../fix_android_animations_20260313/index.md | 5 - .../metadata.json | 8 - .../fix_android_animations_20260313/plan.md | 27 - .../fix_android_animations_20260313/spec.md | 25 - .../archive/kmp_doc_review_20260313/index.md | 5 - .../kmp_doc_review_20260313/metadata.json | 8 - .../archive/kmp_doc_review_20260313/plan.md | 23 - .../archive/kmp_doc_review_20260313/spec.md | 24 - .../kmp_test_migration_20260318/index.md | 5 - .../kmp_test_migration_20260318/metadata.json | 8 - .../kmp_test_migration_20260318/plan.md | 18 - .../kmp_test_migration_20260318/spec.md | 4 - .../migrate_debug_panel_20260319/index.md | 5 - .../metadata.json | 8 - .../migrate_debug_panel_20260319/plan.md | 23 - .../migrate_debug_panel_20260319/spec.md | 24 - .../archive/migrate_room3_20260320/index.md | 5 - .../migrate_room3_20260320/metadata.json | 8 - .../archive/migrate_room3_20260320/plan.md | 36 - .../archive/migrate_room3_20260320/spec.md | 28 - .../archive/mqtt_transport_20260318/index.md | 5 - .../mqtt_transport_20260318/metadata.json | 8 - .../archive/mqtt_transport_20260318/plan.md | 32 - .../archive/mqtt_transport_20260318/spec.md | 33 - .../archive/wire_up_notifs_20260316/index.md | 5 - .../wire_up_notifs_20260316/metadata.json | 8 - .../archive/wire_up_notifs_20260316/plan.md | 34 - .../archive/wire_up_notifs_20260316/spec.md | 17 - conductor/desktop-uri-import-plan.md | 31 - conductor/doc-consolidation-plan.md | 53 -- .../core/ble/AndroidBluetoothRepository.kt | 7 +- .../core/common/util/SequentialJob.kt | 21 +- .../data/manager/MeshConfigFlowManagerImpl.kt | 9 +- .../data/manager/MeshConnectionManagerImpl.kt | 42 +- .../core/data/manager/MeshDataHandlerImpl.kt | 9 +- .../data/manager/MeshMessageProcessorImpl.kt | 13 +- .../core/data/manager/NodeManagerImpl.kt | 5 +- .../core/data/manager/PacketHandlerImpl.kt | 45 +- .../TracerouteSnapshotRepositoryImpl.kt | 17 +- .../core/data/manager/MeshDataHandlerTest.kt | 27 +- .../core/database/DatabaseManager.kt | 5 +- .../meshtastic/core/database/dao/PacketDao.kt | 34 +- .../meshtastic/core/database/entity/Packet.kt | 9 +- .../core/datastore/ModuleConfigDataSource.kt | 3 +- .../kotlin/org/meshtastic/core/model/Node.kt | 9 +- .../core/navigation/TopLevelDestination.kt | 5 +- .../network/radio/BleRadioInterfaceTest.kt | 11 +- .../core/network/radio/StreamInterfaceTest.kt | 106 --- .../core/network/radio/MockInterface.kt | 2 +- .../network/repository/MQTTRepositoryImpl.kt | 29 +- .../core/network/transport/TcpTransport.kt | 13 +- .../core/network/radio/TCPInterfaceTest.kt | 4 +- .../core/prefs/filter/FilterPrefsTest.kt | 22 +- .../notification/NotificationPrefsTest.kt | 25 +- .../service/SharedRadioInterfaceService.kt | 11 +- .../core/ui/component/AlertDialogs.kt | 27 +- .../core/ui/component/DropDownPreference.kt | 13 +- .../core/ui/share/SharedContactViewModel.kt | 5 +- docs/BUILD_LOGIC_CONVENTIONS_GUIDE.md | 2 +- .../BUILD_LOGIC_OPTIMIZATIONS_COMPLETE.md | 233 ------ .../BUILD_LOGIC_OPTIMIZATION_ANALYSIS.md | 80 -- .../BUILD_LOGIC_OPTIMIZATION_SUMMARY.md | 285 ------- docs/archive/README.md | 22 - docs/archive/ble-kmp-abstraction-plan.md | 34 - docs/archive/ble-kmp-strategy.md | 111 --- .../desktop-and-multi-target-roadmap.md | 243 ------ .../kmp-adaptive-compose-evaluation.md | 174 ----- docs/archive/kmp-app-migration-assessment.md | 127 --- docs/archive/kmp-feature-migration-plan.md | 188 ----- docs/archive/kmp-migration.md | 82 -- .../kmp-phase3-testing-consolidation.md | 64 -- docs/archive/kmp-progress-review-2026.md | 728 ------------------ docs/archive/kmp-progress-review-evidence.md | 222 ------ docs/archive/koin-migration-plan.md | 122 --- docs/decisions/ble-strategy.md | 5 - docs/decisions/koin-migration.md | 2 - .../feature/connections/ScannerViewModel.kt | 35 +- .../firmware/FirmwareUpdateViewModel.kt | 204 +++-- .../feature/intro/IntroViewModelTest.kt | 73 -- .../HomoglyphCharacterTransformTest.kt | 6 +- .../feature/messaging/MessageViewModel.kt | 5 +- .../feature/node/compass/CompassViewModel.kt | 16 +- .../node/metrics/EnvironmentMetricsState.kt | 15 +- .../feature/node/metrics/MetricsViewModel.kt | 2 +- .../feature/node/metrics/TracerouteLog.kt | 68 +- .../settings/component/NotificationSection.kt | 4 +- .../feature/settings/debugging/Debug.kt | 19 +- .../settings/debugging/DebugFilters.kt | 13 +- .../radio/component/DeviceConfigScreen.kt | 12 +- .../radio/component/NetworkConfigItemList.kt | 4 +- gradle/libs.versions.toml | 1 + test.gradle.kts | 2 - 163 files changed, 503 insertions(+), 4993 deletions(-) delete mode 100644 conductor/archive/android_kable_migration_20260314/index.md delete mode 100644 conductor/archive/android_kable_migration_20260314/metadata.json delete mode 100644 conductor/archive/android_kable_migration_20260314/plan.md delete mode 100644 conductor/archive/android_kable_migration_20260314/spec.md delete mode 100644 conductor/archive/deep_dive_docs_20260316/index.md delete mode 100644 conductor/archive/deep_dive_docs_20260316/metadata.json delete mode 100644 conductor/archive/deep_dive_docs_20260316/plan.md delete mode 100644 conductor/archive/deep_dive_docs_20260316/spec.md delete mode 100644 conductor/archive/desktop_ble_kable_20260314/index.md delete mode 100644 conductor/archive/desktop_ble_kable_20260314/metadata.json delete mode 100644 conductor/archive/desktop_ble_kable_20260314/plan.md delete mode 100644 conductor/archive/desktop_ble_kable_20260314/spec.md delete mode 100644 conductor/archive/desktop_di_autowiring_20260313/index.md delete mode 100644 conductor/archive/desktop_di_autowiring_20260313/metadata.json delete mode 100644 conductor/archive/desktop_di_autowiring_20260313/plan.md delete mode 100644 conductor/archive/desktop_di_autowiring_20260313/spec.md delete mode 100644 conductor/archive/desktop_parity_20260311/index.md delete mode 100644 conductor/archive/desktop_parity_20260311/metadata.json delete mode 100644 conductor/archive/desktop_parity_20260311/plan.md delete mode 100644 conductor/archive/desktop_parity_20260311/spec.md delete mode 100644 conductor/archive/desktop_serial_transport_20260317/index.md delete mode 100644 conductor/archive/desktop_serial_transport_20260317/metadata.json delete mode 100644 conductor/archive/desktop_serial_transport_20260317/plan.md delete mode 100644 conductor/archive/desktop_serial_transport_20260317/spec.md delete mode 100644 conductor/archive/desktop_ux_enhancements_20260316/index.md delete mode 100644 conductor/archive/desktop_ux_enhancements_20260316/metadata.json delete mode 100644 conductor/archive/desktop_ux_enhancements_20260316/plan.md delete mode 100644 conductor/archive/desktop_ux_enhancements_20260316/spec.md delete mode 100644 conductor/archive/doc_consolidation_20260311/index.md delete mode 100644 conductor/archive/doc_consolidation_20260311/metadata.json delete mode 100644 conductor/archive/doc_consolidation_20260311/plan.md delete mode 100644 conductor/archive/doc_consolidation_20260311/spec.md delete mode 100644 conductor/archive/expand_testing_20260318/index.md delete mode 100644 conductor/archive/expand_testing_20260318/metadata.json delete mode 100644 conductor/archive/expand_testing_20260318/plan.md delete mode 100644 conductor/archive/expand_testing_20260318/spec.md delete mode 100644 conductor/archive/extract_android_navigation_20260318/index.md delete mode 100644 conductor/archive/extract_android_navigation_20260318/metadata.json delete mode 100644 conductor/archive/extract_android_navigation_20260318/plan.md delete mode 100644 conductor/archive/extract_android_navigation_20260318/spec.md delete mode 100644 conductor/archive/extract_database_manager_kmp_20260320/index.md delete mode 100644 conductor/archive/extract_database_manager_kmp_20260320/metadata.json delete mode 100644 conductor/archive/extract_database_manager_kmp_20260320/plan.md delete mode 100644 conductor/archive/extract_database_manager_kmp_20260320/spec.md delete mode 100644 conductor/archive/extract_hardware_transport_20260311/index.md delete mode 100644 conductor/archive/extract_hardware_transport_20260311/metadata.json delete mode 100644 conductor/archive/extract_hardware_transport_20260311/plan.md delete mode 100644 conductor/archive/extract_hardware_transport_20260311/spec.md delete mode 100644 conductor/archive/extract_radio_interface_kmp_20260320/index.md delete mode 100644 conductor/archive/extract_radio_interface_kmp_20260320/metadata.json delete mode 100644 conductor/archive/extract_radio_interface_kmp_20260320/plan.md delete mode 100644 conductor/archive/extract_radio_interface_kmp_20260320/spec.md delete mode 100644 conductor/archive/extract_remaining_background_20260318/index.md delete mode 100644 conductor/archive/extract_remaining_background_20260318/metadata.json delete mode 100644 conductor/archive/extract_remaining_background_20260318/plan.md delete mode 100644 conductor/archive/extract_remaining_background_20260318/spec.md delete mode 100644 conductor/archive/extract_services_20260317/index.md delete mode 100644 conductor/archive/extract_services_20260317/metadata.json delete mode 100644 conductor/archive/extract_services_20260317/plan.md delete mode 100644 conductor/archive/extract_services_20260317/spec.md delete mode 100644 conductor/archive/extract_viewmodels_20260316/index.md delete mode 100644 conductor/archive/extract_viewmodels_20260316/metadata.json delete mode 100644 conductor/archive/extract_viewmodels_20260316/plan.md delete mode 100644 conductor/archive/extract_viewmodels_20260316/spec.md delete mode 100644 conductor/archive/fix_android_animations_20260313/index.md delete mode 100644 conductor/archive/fix_android_animations_20260313/metadata.json delete mode 100644 conductor/archive/fix_android_animations_20260313/plan.md delete mode 100644 conductor/archive/fix_android_animations_20260313/spec.md delete mode 100644 conductor/archive/kmp_doc_review_20260313/index.md delete mode 100644 conductor/archive/kmp_doc_review_20260313/metadata.json delete mode 100644 conductor/archive/kmp_doc_review_20260313/plan.md delete mode 100644 conductor/archive/kmp_doc_review_20260313/spec.md delete mode 100644 conductor/archive/kmp_test_migration_20260318/index.md delete mode 100644 conductor/archive/kmp_test_migration_20260318/metadata.json delete mode 100644 conductor/archive/kmp_test_migration_20260318/plan.md delete mode 100644 conductor/archive/kmp_test_migration_20260318/spec.md delete mode 100644 conductor/archive/migrate_debug_panel_20260319/index.md delete mode 100644 conductor/archive/migrate_debug_panel_20260319/metadata.json delete mode 100644 conductor/archive/migrate_debug_panel_20260319/plan.md delete mode 100644 conductor/archive/migrate_debug_panel_20260319/spec.md delete mode 100644 conductor/archive/migrate_room3_20260320/index.md delete mode 100644 conductor/archive/migrate_room3_20260320/metadata.json delete mode 100644 conductor/archive/migrate_room3_20260320/plan.md delete mode 100644 conductor/archive/migrate_room3_20260320/spec.md delete mode 100644 conductor/archive/mqtt_transport_20260318/index.md delete mode 100644 conductor/archive/mqtt_transport_20260318/metadata.json delete mode 100644 conductor/archive/mqtt_transport_20260318/plan.md delete mode 100644 conductor/archive/mqtt_transport_20260318/spec.md delete mode 100644 conductor/archive/wire_up_notifs_20260316/index.md delete mode 100644 conductor/archive/wire_up_notifs_20260316/metadata.json delete mode 100644 conductor/archive/wire_up_notifs_20260316/plan.md delete mode 100644 conductor/archive/wire_up_notifs_20260316/spec.md delete mode 100644 conductor/desktop-uri-import-plan.md delete mode 100644 conductor/doc-consolidation-plan.md rename core/network/src/{androidUnitTest => androidHostTest}/kotlin/org/meshtastic/core/network/radio/BleRadioInterfaceTest.kt (96%) delete mode 100644 core/network/src/androidUnitTest/kotlin/org/meshtastic/core/network/radio/StreamInterfaceTest.kt rename core/network/src/{androidUnitTest => jvmAndroidTest}/kotlin/org/meshtastic/core/network/radio/TCPInterfaceTest.kt (96%) rename core/prefs/src/{androidUnitTest => androidHostTest}/kotlin/org/meshtastic/core/prefs/filter/FilterPrefsTest.kt (84%) rename core/prefs/src/{androidUnitTest => androidHostTest}/kotlin/org/meshtastic/core/prefs/notification/NotificationPrefsTest.kt (83%) delete mode 100644 docs/archive/BUILD_LOGIC_OPTIMIZATIONS_COMPLETE.md delete mode 100644 docs/archive/BUILD_LOGIC_OPTIMIZATION_ANALYSIS.md delete mode 100644 docs/archive/BUILD_LOGIC_OPTIMIZATION_SUMMARY.md delete mode 100644 docs/archive/README.md delete mode 100644 docs/archive/ble-kmp-abstraction-plan.md delete mode 100644 docs/archive/ble-kmp-strategy.md delete mode 100644 docs/archive/desktop-and-multi-target-roadmap.md delete mode 100644 docs/archive/kmp-adaptive-compose-evaluation.md delete mode 100644 docs/archive/kmp-app-migration-assessment.md delete mode 100644 docs/archive/kmp-feature-migration-plan.md delete mode 100644 docs/archive/kmp-migration.md delete mode 100644 docs/archive/kmp-phase3-testing-consolidation.md delete mode 100644 docs/archive/kmp-progress-review-2026.md delete mode 100644 docs/archive/kmp-progress-review-evidence.md delete mode 100644 docs/archive/koin-migration-plan.md delete mode 100644 feature/intro/src/androidUnitTest/kotlin/org/meshtastic/feature/intro/IntroViewModelTest.kt rename feature/messaging/src/{androidUnitTest => androidHostTest}/kotlin/org/meshtastic/feature/messaging/HomoglyphCharacterTransformTest.kt (97%) delete mode 100644 test.gradle.kts diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/FdroidMapViewProvider.kt b/app/src/fdroid/kotlin/org/meshtastic/app/map/FdroidMapViewProvider.kt index 99f184efc..a5069fb59 100644 --- a/app/src/fdroid/kotlin/org/meshtastic/app/map/FdroidMapViewProvider.kt +++ b/app/src/fdroid/kotlin/org/meshtastic/app/map/FdroidMapViewProvider.kt @@ -39,6 +39,7 @@ class FdroidMapViewProvider : MapViewProvider { ) { val mapViewModel: MapViewModel = koinViewModel() LaunchedEffect(waypointId) { mapViewModel.setWaypointId(waypointId) } + @Suppress("UNCHECKED_CAST") org.meshtastic.app.map.MapView( modifier = modifier, mapViewModel = mapViewModel, diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/MapView.kt b/app/src/fdroid/kotlin/org/meshtastic/app/map/MapView.kt index afbedfa0b..a6c575af7 100644 --- a/app/src/fdroid/kotlin/org/meshtastic/app/map/MapView.kt +++ b/app/src/fdroid/kotlin/org/meshtastic/app/map/MapView.kt @@ -449,7 +449,7 @@ fun MapView( if (!mapFilterStateValue.showPrecisionCircle) { setPrecisionBits(0) } else { - setPrecisionBits(p.precision_bits ?: 0) + setPrecisionBits(p.precision_bits) } setOnLongClickListener { navigateToNodeDetails(node.num) @@ -469,7 +469,7 @@ fun MapView( Logger.d { "User deleted waypoint ${waypoint.id} for me" } mapViewModel.deleteWaypoint(waypoint.id) } - if ((waypoint.locked_to ?: 0) in setOf(0, mapViewModel.myNodeNum ?: 0) && isConnected) { + if (waypoint.locked_to in setOf(0, mapViewModel.myNodeNum ?: 0) && isConnected) { builder.setPositiveButton(getString(Res.string.delete_for_everyone)) { _, _ -> Logger.d { "User deleted waypoint ${waypoint.id} for everyone" } mapViewModel.sendWaypoint(waypoint.copy(expire = 1)) @@ -497,7 +497,7 @@ fun MapView( Logger.d { "marker long pressed id=$id" } val waypoint = waypoints[id]?.waypoint ?: return // edit only when unlocked or lockedTo myNodeNum - if ((waypoint.locked_to ?: 0) in setOf(0, mapViewModel.myNodeNum ?: 0) && isConnected) { + if (waypoint.locked_to in setOf(0, mapViewModel.myNodeNum ?: 0) && isConnected) { showEditWaypointDialog = waypoint } else { showDeleteMarkerDialog(waypoint) @@ -515,15 +515,15 @@ fun MapView( return waypoints.mapNotNull { waypoint -> val pt = waypoint.waypoint ?: return@mapNotNull null if (!mapFilterState.showWaypoints) return@mapNotNull null // Use collected mapFilterState - val lock = if ((pt.locked_to ?: 0) != 0) "\uD83D\uDD12" else "" + val lock = if (pt.locked_to != 0) "\uD83D\uDD12" else "" val time = DateFormatter.formatDateTime(waypoint.time) - val label = (pt.name ?: "") + " " + formatAgo((waypoint.time / 1000).toInt()) - val emoji = String(Character.toChars(if ((pt.icon ?: 0) == 0) 128205 else pt.icon!!)) + val label = pt.name + " " + formatAgo((waypoint.time / 1000).toInt()) + val emoji = String(Character.toChars(if (pt.icon == 0) 128205 else pt.icon)) val now = nowMillis - val expireTimeMillis = (pt.expire ?: 0) * 1000L + val expireTimeMillis = pt.expire * 1000L val expireTimeStr = when { - (pt.expire ?: 0) == 0 || pt.expire == Int.MAX_VALUE -> "Never" + pt.expire == 0 || pt.expire == Int.MAX_VALUE -> "Never" expireTimeMillis <= now -> "Expired" else -> DateFormatter.formatRelativeTime(expireTimeMillis) } @@ -693,10 +693,9 @@ fun MapView( if (nodeTracks == null || focusedNodeNum == null) return emptyList() to emptyList() val lastHeardTrackFilter = mapFilterState.lastHeardTrackFilter - val timeFilteredPositions = - nodeTracks.filter { - lastHeardTrackFilter == LastHeardFilter.Any || it.time > nowSeconds - lastHeardTrackFilter.seconds - } + val timeFilteredPositions = nodeTracks.filter { + lastHeardTrackFilter == LastHeardFilter.Any || it.time > nowSeconds - lastHeardTrackFilter.seconds + } val sortedPositions = timeFilteredPositions.sortedBy { it.time } val focusedNode = nodes.find { it.num == focusedNodeNum } ?: return emptyList() to emptyList() @@ -719,18 +718,17 @@ fun MapView( } } - val trackMarkers = - sortedPositions.mapIndexedNotNull { index, position -> - if (index == sortedPositions.lastIndex) return@mapIndexedNotNull null + val trackMarkers = sortedPositions.mapIndexedNotNull { index, position -> + if (index == sortedPositions.lastIndex) return@mapIndexedNotNull null - Marker(this).apply { - this.position = GeoPoint((position.latitude_i ?: 0) * 1e-7, (position.longitude_i ?: 0) * 1e-7) - icon = AppCompatResources.getDrawable(context, R.drawable.ic_map_location_dot) - setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER) - title = getString(Res.string.position) - snippet = formatAgo(position.time) - } + Marker(this).apply { + this.position = GeoPoint((position.latitude_i ?: 0) * 1e-7, (position.longitude_i ?: 0) * 1e-7) + icon = AppCompatResources.getDrawable(context, R.drawable.ic_map_location_dot) + setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER) + title = getString(Res.string.position) + snippet = formatAgo(position.time) } + } return trackMarkers to trackPolylines } @@ -941,12 +939,11 @@ fun MapView( Logger.d { "User clicked send waypoint ${waypoint.id}" } showEditWaypointDialog = null - val newId = - if (waypoint.id == 0) mapViewModel.generatePacketId() ?: return@EditWaypointDialog else waypoint.id + val newId = if (waypoint.id == 0) mapViewModel.generatePacketId() else waypoint.id val newName = if (waypoint.name.isNullOrEmpty()) "Dropped Pin" else waypoint.name - val newExpire = if ((waypoint.expire ?: 0) == 0) Int.MAX_VALUE else (waypoint.expire ?: Int.MAX_VALUE) - val newLockedTo = if ((waypoint.locked_to ?: 0) != 0) mapViewModel.myNodeNum ?: 0 else 0 - val newIcon = if ((waypoint.icon ?: 0) == 0) 128205 else waypoint.icon + val newExpire = if (waypoint.expire == 0) Int.MAX_VALUE else waypoint.expire + val newLockedTo = if (waypoint.locked_to != 0) mapViewModel.myNodeNum ?: 0 else 0 + val newIcon = if (waypoint.icon == 0) 128205 else waypoint.icon mapViewModel.sendWaypoint( waypoint.copy( @@ -1161,16 +1158,15 @@ private fun offsetPolyline( val headingPoints = headingReferencePoints.takeIf { it.size >= 2 } ?: points if (points.size < 2 || headingPoints.size < 2 || offsetMeters == 0.0) return points - val headings = - headingPoints.mapIndexed { index, _ -> - when (index) { - 0 -> bearingRad(headingPoints[0], headingPoints[1]) - headingPoints.lastIndex -> - bearingRad(headingPoints[headingPoints.lastIndex - 1], headingPoints[headingPoints.lastIndex]) + val headings = headingPoints.mapIndexed { index, _ -> + when (index) { + 0 -> bearingRad(headingPoints[0], headingPoints[1]) + headingPoints.lastIndex -> + bearingRad(headingPoints[headingPoints.lastIndex - 1], headingPoints[headingPoints.lastIndex]) - else -> bearingRad(headingPoints[index - 1], headingPoints[index + 1]) - } + else -> bearingRad(headingPoints[index - 1], headingPoints[index + 1]) } + } return points.mapIndexed { index, point -> val heading = headings[index.coerceIn(0, headings.lastIndex)] diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/MapViewExtensions.kt b/app/src/fdroid/kotlin/org/meshtastic/app/map/MapViewExtensions.kt index a5f27e8e9..04f896d18 100644 --- a/app/src/fdroid/kotlin/org/meshtastic/app/map/MapViewExtensions.kt +++ b/app/src/fdroid/kotlin/org/meshtastic/app/map/MapViewExtensions.kt @@ -126,19 +126,18 @@ fun MapView.addPolyline(density: Density, geoPoints: List, onClick: () fun MapView.addPositionMarkers(positions: List, onClick: () -> Unit): List { val navIcon = ContextCompat.getDrawable(context, R.drawable.ic_map_navigation) - val markers = - positions.map { - Marker(this).apply { - icon = navIcon - rotation = ((it.ground_track ?: 0) * 1e-5).toFloat() - setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER) - position = GeoPoint((it.latitude_i ?: 0) * 1e-7, (it.longitude_i ?: 0) * 1e-7) - setOnMarkerClickListener { _, _ -> - onClick() - true - } + val markers = positions.map { + Marker(this).apply { + icon = navIcon + rotation = ((it.ground_track ?: 0) * 1e-5).toFloat() + setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER) + position = GeoPoint((it.latitude_i ?: 0) * 1e-7, (it.longitude_i ?: 0) * 1e-7) + setOnMarkerClickListener { _, _ -> + onClick() + true } } + } overlays.addAll(markers) return markers diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/component/EditWaypointDialog.kt b/app/src/fdroid/kotlin/org/meshtastic/app/map/component/EditWaypointDialog.kt index 83dc24880..fbdf28e40 100644 --- a/app/src/fdroid/kotlin/org/meshtastic/app/map/component/EditWaypointDialog.kt +++ b/app/src/fdroid/kotlin/org/meshtastic/app/map/component/EditWaypointDialog.kt @@ -60,7 +60,6 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import kotlinx.datetime.Instant import kotlinx.datetime.LocalDateTime import kotlinx.datetime.Month import kotlinx.datetime.toInstant @@ -85,6 +84,7 @@ import org.meshtastic.core.ui.emoji.EmojiPickerDialog import org.meshtastic.core.ui.theme.AppTheme import org.meshtastic.proto.Waypoint import kotlin.time.Duration.Companion.hours +import kotlin.time.Instant @Suppress("LongMethod", "CyclomaticComplexMethod") @OptIn(ExperimentalLayoutApi::class) @@ -100,7 +100,7 @@ fun EditWaypointDialog( val title = if (waypoint.id == 0) Res.string.waypoint_new else Res.string.waypoint_edit @Suppress("MagicNumber") - val emoji = if ((waypointInput.icon ?: 0) == 0) 128205 else waypointInput.icon!! + val emoji = if (waypointInput.icon == 0) 128205 else waypointInput.icon var showEmojiPickerView by remember { mutableStateOf(false) } // Get current context for dialogs @@ -115,11 +115,11 @@ fun EditWaypointDialog( val currentInstant = remember(waypointInput.expire) { - val expire = waypointInput.expire ?: 0 + val expire = waypointInput.expire if (expire != 0 && expire != Int.MAX_VALUE) { - Instant.fromEpochSeconds(expire.toLong()) + kotlin.time.Instant.fromEpochSeconds(expire.toLong()) } else { - kotlinx.datetime.Clock.System.now() + 8.hours + kotlin.time.Clock.System.now() + 8.hours } } @@ -127,7 +127,7 @@ fun EditWaypointDialog( var selectedDate by remember(currentInstant) { mutableStateOf( - if ((waypointInput.expire ?: 0) != 0 && waypointInput.expire != Int.MAX_VALUE) { + if (waypointInput.expire != 0 && waypointInput.expire != Int.MAX_VALUE) { dateFormat.format(java.util.Date(currentInstant.toEpochMilliseconds())) } else { "" @@ -137,7 +137,7 @@ fun EditWaypointDialog( var selectedTime by remember(currentInstant) { mutableStateOf( - if ((waypointInput.expire ?: 0) != 0 && waypointInput.expire != Int.MAX_VALUE) { + if (waypointInput.expire != 0 && waypointInput.expire != Int.MAX_VALUE) { timeFormat.format(java.util.Date(currentInstant.toEpochMilliseconds())) } else { "" @@ -162,7 +162,7 @@ fun EditWaypointDialog( ) EditTextPreference( title = stringResource(Res.string.name), - value = waypointInput.name ?: "", + value = waypointInput.name, maxSize = 29, enabled = true, isError = false, @@ -185,7 +185,7 @@ fun EditWaypointDialog( ) EditTextPreference( title = stringResource(Res.string.description), - value = waypointInput.description ?: "", + value = waypointInput.description, maxSize = 99, enabled = true, isError = false, @@ -202,7 +202,7 @@ fun EditWaypointDialog( Text(stringResource(Res.string.locked)) Switch( modifier = Modifier.fillMaxWidth().wrapContentWidth(Alignment.End), - checked = (waypointInput.locked_to ?: 0) != 0, + checked = waypointInput.locked_to != 0, onCheckedChange = { waypointInput = waypointInput.copy(locked_to = if (it) 1 else 0) }, ) } @@ -225,7 +225,7 @@ fun EditWaypointDialog( waypointInput = waypointInput.copy(expire = newLdt.toInstant(tz).epochSeconds.toInt()) }, ldt.year, - ldt.monthNumber - 1, + ldt.month.ordinal, ldt.day, ) @@ -261,7 +261,7 @@ fun EditWaypointDialog( Text(stringResource(Res.string.expires)) Switch( modifier = Modifier.fillMaxWidth().wrapContentWidth(Alignment.End), - checked = waypointInput.expire != Int.MAX_VALUE && (waypointInput.expire ?: 0) != 0, + checked = waypointInput.expire != Int.MAX_VALUE && waypointInput.expire != 0, onCheckedChange = { isChecked -> if (isChecked) { waypointInput = waypointInput.copy(expire = currentInstant.epochSeconds.toInt()) @@ -272,7 +272,7 @@ fun EditWaypointDialog( ) } - if (waypointInput.expire != Int.MAX_VALUE && (waypointInput.expire ?: 0) != 0) { + if (waypointInput.expire != Int.MAX_VALUE && waypointInput.expire != 0) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally), diff --git a/app/src/google/kotlin/org/meshtastic/app/map/MapView.kt b/app/src/google/kotlin/org/meshtastic/app/map/MapView.kt index bbda314d9..98c779308 100644 --- a/app/src/google/kotlin/org/meshtastic/app/map/MapView.kt +++ b/app/src/google/kotlin/org/meshtastic/app/map/MapView.kt @@ -302,17 +302,16 @@ fun MapView( } val myNodeNum = mapViewModel.myNodeNum - val nodeClusterItems = - displayNodes.map { node -> - val latLng = LatLng((node.position.latitude_i ?: 0) * DEG_D, (node.position.longitude_i ?: 0) * DEG_D) - NodeClusterItem( - node = node, - nodePosition = latLng, - nodeTitle = "${node.user.short_name} ${formatAgo(node.position.time)}", - nodeSnippet = "${node.user.long_name}", - myNodeNum = myNodeNum, - ) - } + val nodeClusterItems = displayNodes.map { node -> + val latLng = LatLng((node.position.latitude_i ?: 0) * DEG_D, (node.position.longitude_i ?: 0) * DEG_D) + NodeClusterItem( + node = node, + nodePosition = latLng, + nodeTitle = "${node.user.short_name} ${formatAgo(node.position.time)}", + nodeSnippet = "${node.user.long_name}", + myNodeNum = myNodeNum, + ) + } val isConnected by mapViewModel.isConnected.collectAsStateWithLifecycle() val theme by mapViewModel.theme.collectAsStateWithLifecycle() val dark = @@ -492,11 +491,9 @@ fun MapView( if (nodeTracks != null && focusedNodeNum != null) { val lastHeardTrackFilter = mapFilterState.lastHeardTrackFilter - val timeFilteredPositions = - nodeTracks.filter { - lastHeardTrackFilter == LastHeardFilter.Any || - it.time > nowSeconds - lastHeardTrackFilter.seconds - } + val timeFilteredPositions = nodeTracks.filter { + lastHeardTrackFilter == LastHeardFilter.Any || it.time > nowSeconds - lastHeardTrackFilter.seconds + } val sortedPositions = timeFilteredPositions.sortedBy { it.time } allNodes .find { it.num == focusedNodeNum } @@ -872,19 +869,18 @@ private fun offsetPolyline( val headingPoints = headingReferencePoints.takeIf { it.size >= 2 } ?: points if (points.size < 2 || headingPoints.size < 2 || offsetMeters == 0.0) return points - val headings = - headingPoints.mapIndexed { index, _ -> - when (index) { - 0 -> SphericalUtil.computeHeading(headingPoints[0], headingPoints[1]) - headingPoints.lastIndex -> - SphericalUtil.computeHeading( - headingPoints[headingPoints.lastIndex - 1], - headingPoints[headingPoints.lastIndex], - ) + val headings = headingPoints.mapIndexed { index, _ -> + when (index) { + 0 -> SphericalUtil.computeHeading(headingPoints[0], headingPoints[1]) + headingPoints.lastIndex -> + SphericalUtil.computeHeading( + headingPoints[headingPoints.lastIndex - 1], + headingPoints[headingPoints.lastIndex], + ) - else -> SphericalUtil.computeHeading(headingPoints[index - 1], headingPoints[index + 1]) - } + else -> SphericalUtil.computeHeading(headingPoints[index - 1], headingPoints[index + 1]) } + } return points.mapIndexed { index, point -> val heading = headings[index.coerceIn(0, headings.lastIndex)] diff --git a/app/src/google/kotlin/org/meshtastic/app/map/MapViewModel.kt b/app/src/google/kotlin/org/meshtastic/app/map/MapViewModel.kt index 35057485f..70ff4858d 100644 --- a/app/src/google/kotlin/org/meshtastic/app/map/MapViewModel.kt +++ b/app/src/google/kotlin/org/meshtastic/app/map/MapViewModel.kt @@ -412,33 +412,32 @@ class MapViewModel( if (persistedLayerFiles != null) { val hiddenLayerUrls = googleMapsPrefs.hiddenLayerUrls.value - val loadedItems = - persistedLayerFiles.mapNotNull { file -> - if (file.isFile) { - val layerType = - when (file.extension.lowercase()) { - "kml", - "kmz", - -> LayerType.KML - "geojson", - "json", - -> LayerType.GEOJSON - else -> null - } - - layerType?.let { - val uri = Uri.fromFile(file) - MapLayerItem( - name = file.nameWithoutExtension, - uri = uri, - isVisible = !hiddenLayerUrls.contains(uri.toString()), - layerType = it, - ) + val loadedItems = persistedLayerFiles.mapNotNull { file -> + if (file.isFile) { + val layerType = + when (file.extension.lowercase()) { + "kml", + "kmz", + -> LayerType.KML + "geojson", + "json", + -> LayerType.GEOJSON + else -> null } - } else { - null + + layerType?.let { + val uri = Uri.fromFile(file) + MapLayerItem( + name = file.nameWithoutExtension, + uri = uri, + isVisible = !hiddenLayerUrls.contains(uri.toString()), + layerType = it, + ) } + } else { + null } + } val networkItems = googleMapsPrefs.networkMapLayers.value.mapNotNull { networkString -> diff --git a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/Spotless.kt b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/Spotless.kt index 0f74f84ef..7a657320c 100644 --- a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/Spotless.kt +++ b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/Spotless.kt @@ -27,7 +27,7 @@ internal fun Project.configureSpotless(extension: SpotlessExtension) { kotlin { target("src/*/kotlin/**/*.kt", "src/*/java/**/*.kt") targetExclude("**/build/**/*.kt") - ktfmt().kotlinlangStyle().configure { it.setMaxWidth(120) } + ktfmt(libs.version("ktfmt")).kotlinlangStyle().configure { it.setMaxWidth(120) } ktlint(ktlintVersion) .setEditorConfigPath(rootProject.file("config/spotless/.editorconfig").path) licenseHeaderFile(rootProject.file("config/spotless/copyright.kt")) @@ -35,7 +35,7 @@ internal fun Project.configureSpotless(extension: SpotlessExtension) { kotlinGradle { target("**/*.gradle.kts") targetExclude("**/build/**", "**/dependencies/**") - ktfmt().kotlinlangStyle().configure { it.setMaxWidth(120) } + ktfmt(libs.version("ktfmt")).kotlinlangStyle().configure { it.setMaxWidth(120) } ktlint(ktlintVersion) .setEditorConfigPath(rootProject.file("config/spotless/.editorconfig").path) licenseHeaderFile( diff --git a/conductor/archive/android_kable_migration_20260314/index.md b/conductor/archive/android_kable_migration_20260314/index.md deleted file mode 100644 index 418db43a5..000000000 --- a/conductor/archive/android_kable_migration_20260314/index.md +++ /dev/null @@ -1,5 +0,0 @@ -# Track android_kable_migration_20260314 Context - -- [Specification](./spec.md) -- [Implementation Plan](./plan.md) -- [Metadata](./metadata.json) \ No newline at end of file diff --git a/conductor/archive/android_kable_migration_20260314/metadata.json b/conductor/archive/android_kable_migration_20260314/metadata.json deleted file mode 100644 index 8dd4dc82b..000000000 --- a/conductor/archive/android_kable_migration_20260314/metadata.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "track_id": "android_kable_migration_20260314", - "type": "feature", - "status": "completed", - "created_at": "2026-03-14T17:15:00Z", - "updated_at": "2026-03-14T17:15:00Z", - "description": "Replace Nordic with Kable on Android" -} \ No newline at end of file diff --git a/conductor/archive/android_kable_migration_20260314/plan.md b/conductor/archive/android_kable_migration_20260314/plan.md deleted file mode 100644 index 454298e8a..000000000 --- a/conductor/archive/android_kable_migration_20260314/plan.md +++ /dev/null @@ -1,44 +0,0 @@ -# Implementation Plan: Replace Nordic with Kable on Android (Deduplication Pass) - -## Phase 1: Deduplicate Kable Abstractions into `commonMain` [checkpoint: 709f6e3] -- [x] Task: Extract common Kable state mapping logic from jvmMain to commonMain 10cdd16 - - [x] Create `commonMain` tests for `BleConnectionState` mapping using Kable `State` - - [x] Move `KableMeshtasticRadioProfile` and `KableBleConnection` logic that doesn't depend on platform specifics to `commonMain` -- [x] Task: Implement common Kable `Scanner` and `Peripheral` wrappers 2691d70 - - [x] Extract generic connection lifecycle (connect, reconnect, close) to `commonMain` using Kable's `Peripheral` interface -- [x] Task: Conductor - User Manual Verification 'Phase 1: Deduplicate Kable Abstractions into commonMain' (Protocol in workflow.md) 709f6e3 - -## Phase 2: Implement Kable Backend for Android (`androidMain`) [checkpoint: 12217de] -- [x] Task: Add Kable dependency to Android source set in `core:ble/build.gradle.kts` 011d619 -- [x] Task: Implement Android-specific `BleConnectionFactory` and `BleScanner` using the deduplicated `commonMain` logic 589ee93 - - [x] Write failing integration tests for Android Kable scanner (using fakes/mocks) - - [x] Implement `KableBleScanner` for `androidMain` - - [x] Write failing integration tests for Android Kable connection (using fakes/mocks) - - [x] Implement `KableBleConnection` for `androidMain` (handling Android-specific MTU requests if necessary) -- [x] Task: Conductor - User Manual Verification 'Phase 2: Implement Kable Backend for Android' (Protocol in workflow.md) 12217de - -## Phase 3: Migrate OTA Firmware Update Logic [checkpoint: 663c8e2] -- [x] Task: Deprecate `NordicDfuHandler` and replace with Kable-based DFU 06fe4f5 - - [x] Write failing tests for Kable DFU integration - - [x] Implement new DFU handler in `feature:firmware` using `MeshtasticRadioProfile` / Kable abstraction -- [x] Task: Conductor - User Manual Verification 'Phase 3: Migrate OTA Firmware Update Logic' (Protocol in workflow.md) 663c8e2 - -## Phase 4: Wire Kable into Android App and Remove Nordic [checkpoint: ebe1617] -- [x] Task: Deprecate and remove `NordicBleInterface` and `AndroidBleConnection` ebe1617 - - [x] Remove `NordicAndroidCommonLibraries` and `NordicDfuLibrary` from `gradle/libs.versions.toml` and build files - - [x] Delete `NordicBleInterface.kt` and associated Nordic-specific radio implementations -- [x] Task: Wire new `androidMain` Kable implementation into the Koin DI graph ebe1617 - - [x] Update `AndroidRadioControllerImpl` or DI modules to provide the new Kable `BleConnectionFactory` and `BleScanner` -- [x] Task: Conductor - User Manual Verification 'Phase 4: Wire Kable into Android App and Remove Nordic' (Protocol in workflow.md) ebe1617 - -## Phase 5: Final Testing and Integration [checkpoint: 4778c0e] -- [x] Task: Update Android `app` UI tests and BLE unit tests to use Kable fakes 4778c0e - - [x] Fix any failing tests related to the Nordic removal -- [x] Task: Manual end-to-end verification 4778c0e - - [x] Build and run the Android app, verify BLE scanning, connecting, and messaging - - [x] Verify OTA updates work via BLE - - [x] Verify the Desktop app still functions correctly -- [x] Task: Conductor - User Manual Verification 'Phase 5: Final Testing and Integration' (Protocol in workflow.md) 4778c0e - -## Phase: Review Fixes -- [x] Task: Apply review suggestions e5dffd9 \ No newline at end of file diff --git a/conductor/archive/android_kable_migration_20260314/spec.md b/conductor/archive/android_kable_migration_20260314/spec.md deleted file mode 100644 index f59fbaa59..000000000 --- a/conductor/archive/android_kable_migration_20260314/spec.md +++ /dev/null @@ -1,28 +0,0 @@ -# Specification: Replace Nordic with Kable on Android (Deduplication Pass) - -## Overview -This track executes a full migration of the Android application's BLE transport layer from the legacy Nordic Android Common Libraries to the multiplatform Kable library. Building upon the successful `MeshtasticRadioProfile` abstraction introduced for the Desktop target, this track aims to unify the BLE transport layer across all platforms (Android, Desktop, iOS) under a single KMP technology stack. Crucially, this pass focuses on **maximal code deduplication**, moving as much BLE logic as possible into `commonMain` to share it across all targets, including OTA firmware update logic. - -## Functional Requirements -- **Kable Integration:** Implement the `MeshtasticRadioProfile` using Kable for the `androidMain` source set, replacing the existing Nordic implementation. -- **Maximal Deduplication:** Refactor the existing Kable `jvmMain` implementation and the new `androidMain` implementation to extract common connection management, scanning logic, and characteristic observation into `core:ble/commonMain`. -- **OTA Firmware Updates:** Migrate the Android OTA firmware update logic (currently handled by `NordicDfuHandler`) to use the new Kable/KMP abstraction. -- **Full Migration:** The Android app must exclusively use the new Kable backend for all BLE operations (scanning, connecting, data transfer, firmware updates). -- **Deprecation/Removal:** Remove all dependencies on the Nordic Android Common Libraries and Nordic DFU libraries from the project configuration (`build.gradle.kts`, version catalogs). -- **Feature Parity:** The new Kable implementation on Android must maintain full feature parity with the previous Nordic implementation, including connection stability, MTU negotiation, and data throughput. - -## Non-Functional Requirements -- **Expanded Testing:** Adapt existing Android BLE tests to use Kable fakes and write new `commonMain` tests to expand test coverage for the shared KMP BLE abstraction. -- **Architecture:** Maintain strict adherence to the MVI/UDF patterns and the pure KMP DI architecture (Koin annotations). - -## Acceptance Criteria -- [ ] Kable backend is fully implemented for Android (`androidMain`). -- [ ] Nordic Android Common Libraries and DFU dependencies are completely removed from the project. -- [ ] Android application successfully scans, connects, and transfers data via BLE using Kable. -- [ ] BLE logic (connection state, profile mapping, retry logic) is heavily deduplicated into `core:ble/commonMain`. -- [ ] OTA firmware update logic is successfully migrated to use the Kable backend. -- [ ] Existing BLE tests are updated or replaced, and all test suites pass. -- [ ] New KMP BLE tests are added, improving overall test coverage. - -## Out of Scope -- Migrating USB or TCP network transports. \ No newline at end of file diff --git a/conductor/archive/deep_dive_docs_20260316/index.md b/conductor/archive/deep_dive_docs_20260316/index.md deleted file mode 100644 index aea19983d..000000000 --- a/conductor/archive/deep_dive_docs_20260316/index.md +++ /dev/null @@ -1,5 +0,0 @@ -# Track deep_dive_docs_20260316 Context - -- [Specification](./spec.md) -- [Implementation Plan](./plan.md) -- [Metadata](./metadata.json) \ No newline at end of file diff --git a/conductor/archive/deep_dive_docs_20260316/metadata.json b/conductor/archive/deep_dive_docs_20260316/metadata.json deleted file mode 100644 index 4851ae35a..000000000 --- a/conductor/archive/deep_dive_docs_20260316/metadata.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "track_id": "deep_dive_docs_20260316", - "type": "chore", - "status": "completed", - "created_at": "2026-03-16T12:00:00Z", - "updated_at": "2026-03-16T12:00:00Z", - "description": "do a deep dive of project docs and plans in /docs - verify against actual project/codebase state, then validate against modern best practices for android, kotlin, kmp, and the dependencies used. be thorough - check all the major dependencies. Update docs and plans accordingly." -} \ No newline at end of file diff --git a/conductor/archive/deep_dive_docs_20260316/plan.md b/conductor/archive/deep_dive_docs_20260316/plan.md deleted file mode 100644 index 85cfc5d7c..000000000 --- a/conductor/archive/deep_dive_docs_20260316/plan.md +++ /dev/null @@ -1,19 +0,0 @@ -# Implementation Plan: Deep Dive & Validation of Project Docs & Plans - -## Phase 1: Audit & Discovery [checkpoint: 105763b] -- [x] Task: Audit Gradle dependencies (`libs.versions.toml`) against 2026 KMP best practices (Koin, Compose, Navigation 3, etc.). baed3d6 -- [x] Task: Analyze Core Logic (`core:*`) and platform modules (Android, Desktop) for architectural alignment (MVI/Shared ViewModels). baed3d6 -- [x] Task: Review current UI and feature module implementations for Compose Multiplatform standard adherence. baed3d6 -- [x] Task: Evaluate testing patterns, coverage, and the use of shared test doubles (`core:testing`). baed3d6 -- [x] Task: Compile a list of discrepancies between current documentation/plans and the actual codebase. baed3d6 -- [x] Task: Conductor - User Manual Verification 'Phase 1: Audit & Discovery' (Protocol in workflow.md) 105763b - -## Phase 2: Documentation Updates [checkpoint: 7212ff1] -- [x] Task: Update `/docs` and root-level guides (e.g., `GEMINI.md`, `kmp-status.md`, `roadmap.md`) to reflect the current, verified codebase state. baed3d6 -- [x] Task: Add explicit documentation for areas where the codebase diverges from documented best practices (flagging for future refactoring). baed3d6 -- [x] Task: Conductor - User Manual Verification 'Phase 2: Documentation Updates' (Protocol in workflow.md) 7212ff1 - -## Phase 3: Plan Adjustment -- [x] Task: Create new, actionable tasks in the project's main `plan.md` (roadmap.md) to address the flagged discrepancies (e.g., refactoring non-compliant Koin modules, updating deprecated APIs). baed3d6 -- [x] Task: Review and finalize the overall project roadmap and status based on the audit findings. baed3d6 -- [x] Task: Conductor - User Manual Verification 'Phase 3: Plan Adjustment' (Protocol in workflow.md) 7212ff1 \ No newline at end of file diff --git a/conductor/archive/deep_dive_docs_20260316/spec.md b/conductor/archive/deep_dive_docs_20260316/spec.md deleted file mode 100644 index baa50bda7..000000000 --- a/conductor/archive/deep_dive_docs_20260316/spec.md +++ /dev/null @@ -1,19 +0,0 @@ -# Specification: Deep Dive & Validation of Project Docs & Plans - -## Overview -This track involves a comprehensive review and deep dive into the project's documentation (`/docs`, `GEMINI.md`, etc.) and plans. The goal is to verify the documented state against the actual Kotlin Multiplatform (KMP) codebase and validate it against modern 2026 KMP and Android best practices. The outcome will be updated documentation reflecting the current state and flagged/planned changes for areas not following best practices. - -## Functional Requirements -- **Codebase Verification:** Analyze all major areas including Core Logic (`core:*`), UI & Features (Compose Multiplatform), Dependencies (Gradle version catalogs), and Platform-specific implementations (Android, Desktop). -- **Best Practice Validation:** Evaluate the codebase against modern standards, specifically focusing on Architecture (MVI/Shared ViewModels), Navigation (Navigation 3), Dependency Injection (Koin Annotations K2), and Testing patterns. -- **Documentation Update:** Modify existing documentation and plans to accurately reflect the current state of the codebase and dependencies. -- **Refactoring Proposals:** Identify and flag code or architectural decisions that deviate from best practices, outlining necessary refactoring steps in the project's plans. - -## Acceptance Criteria -- All documentation in `/docs` and root-level guides accurately reflect the current codebase. -- A comprehensive audit of major dependencies has been performed and validated against 2026 KMP standards. -- Discrepancies between the codebase and best practices are clearly flagged and actionable tasks are added to the project plans. -- The `plan.md` reflects the updated status and any new tasks generated from the audit. - -## Out of Scope -- Direct refactoring or modification of the actual Kotlin/Android codebase during this specific track (this track focuses on documentation, planning, and flagging). \ No newline at end of file diff --git a/conductor/archive/desktop_ble_kable_20260314/index.md b/conductor/archive/desktop_ble_kable_20260314/index.md deleted file mode 100644 index dd1da9350..000000000 --- a/conductor/archive/desktop_ble_kable_20260314/index.md +++ /dev/null @@ -1,5 +0,0 @@ -# Track desktop_ble_kable_20260314 Context - -- [Specification](./spec.md) -- [Implementation Plan](./plan.md) -- [Metadata](./metadata.json) \ No newline at end of file diff --git a/conductor/archive/desktop_ble_kable_20260314/metadata.json b/conductor/archive/desktop_ble_kable_20260314/metadata.json deleted file mode 100644 index 813ef1cab..000000000 --- a/conductor/archive/desktop_ble_kable_20260314/metadata.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "track_id": "desktop_ble_kable_20260314", - "type": "feature", - "status": "completed", - "created_at": "2026-03-14T12:00:00Z", - "updated_at": "2026-03-14T12:00:00Z", - "description": "Kable swap Keep Nordic on Android short-term. Add Kable backend only for jvmMain in core:ble first (desktop BLE enablement). Introduce a MeshtasticRadioProfile abstraction in core:ble/commonMain so NordicBleInterface no longer depends on Android/Nordic classes. Once that seam is clean, decide whether Android should stay Nordic or move to Kable." -} \ No newline at end of file diff --git a/conductor/archive/desktop_ble_kable_20260314/plan.md b/conductor/archive/desktop_ble_kable_20260314/plan.md deleted file mode 100644 index e5f84f48e..000000000 --- a/conductor/archive/desktop_ble_kable_20260314/plan.md +++ /dev/null @@ -1,37 +0,0 @@ -# Implementation Plan: Desktop BLE Enablement via Kable - -## Phase 1: Define `MeshtasticRadioProfile` Abstraction [checkpoint: 1206e87] -- [x] Task: Define `MeshtasticRadioProfile` interface in `core:ble/commonMain` eaa623a - - [ ] Write tests for expected profile behavior (e.g., state flow emission) using a simple fake - - [ ] Implement `MeshtasticRadioProfile` interface, data classes for states, and configuration -- [x] Task: Conductor - User Manual Verification 'Phase 1: Define `MeshtasticRadioProfile` Abstraction' (Protocol in workflow.md) 1206e87 - -## Phase 2: Refactor Nordic Implementation to use Abstraction [checkpoint: dc700a5] -- [x] Task: Implement `MeshtasticRadioProfile` in the existing Nordic implementation (`androidMain`) 83a8a9b - - [ ] Write/adapt existing Android tests to verify `MeshtasticRadioProfile` adherence - - [ ] Implement wrapper/adapter for Nordic classes to fulfill `MeshtasticRadioProfile` -- [x] Task: Decouple app-level BLE transport from Nordic types 2dfedde - - [ ] Write tests to ensure BLE transport only relies on `MeshtasticRadioProfile` - - [ ] Refactor transport layer (e.g., `NordicBleInterface` usages) to use the new profile interface -- [x] Task: Conductor - User Manual Verification 'Phase 2: Refactor Nordic Implementation to use Abstraction' (Protocol in workflow.md) dc700a5 - -## Phase 3: Implement Kable Backend for Desktop [checkpoint: ed2a459] -- [x] Task: Setup Kable dependencies for `jvmMain` in `core:ble` b152eff - - [ ] Update `build.gradle.kts` to include Kable dependency for Desktop -- [x] Task: Implement Kable `MeshtasticRadioProfile` backend (`jvmMain`) fa5cc82 - - [ ] Write `commonMain` unit tests with Kable fakes to verify scanning, connection, and read/write operations - - [ ] Implement Kable scanning logic - - [ ] Implement Kable connection and characteristic management - - [ ] Implement Kable read/write data transfer logic -- [x] Task: Conductor - User Manual Verification 'Phase 3: Implement Kable Backend for Desktop' (Protocol in workflow.md) ed2a459 - -## Phase 4: Integration and Final Testing [checkpoint: af6d3b3] -- [x] Task: Integrate Kable backend into Desktop app DI graph 28afcad - - [ ] Wire up the Kable implementation in `desktop` module DI -- [x] Task: End-to-end verification 84aae75 - - [ ] Verify Android app still compiles and connects using Nordic - - [ ] Verify Desktop app compiles and connects using Kable -- [x] Task: Conductor - User Manual Verification 'Phase 4: Integration and Final Testing' (Protocol in workflow.md) af6d3b3 - -## Phase: Review Fixes -- [x] Task: Apply review suggestions b36da82 diff --git a/conductor/archive/desktop_ble_kable_20260314/spec.md b/conductor/archive/desktop_ble_kable_20260314/spec.md deleted file mode 100644 index 7848283ce..000000000 --- a/conductor/archive/desktop_ble_kable_20260314/spec.md +++ /dev/null @@ -1,31 +0,0 @@ -# Specification: Desktop BLE Enablement via Kable - -## Overview -This track introduces a Kable BLE backend specifically for the `jvmMain` (Desktop) target within `core:ble`. To facilitate this without breaking the existing Android implementation, we will introduce a `MeshtasticRadioProfile` abstraction in `core:ble/commonMain`. This abstraction will ensure that the app-level BLE transport path no longer depends on Android-specific or Nordic-specific classes. Initially, Android will continue to use the Nordic BLE implementation, while Desktop will use Kable. Once this seam is proven, a future decision will determine whether Android should fully migrate to Kable. This approach lays the groundwork for seamless integration of future targets (e.g., iOS) under the same KMP abstraction. - -## Functional Requirements -- **MeshtasticRadioProfile Abstraction:** Introduce a multiplatform interface (`MeshtasticRadioProfile`) in `core:ble/commonMain` to abstract all BLE operations. -- **Remove Nordic Dependencies:** Ensure that the app-level BLE transport path is entirely decoupled from Nordic types, relying solely on the new abstraction. -- **Kable Backend (jvmMain):** Implement the Kable backend for the Desktop target. This backend must support all core BLE operations: - - Scanning for nearby Meshtastic devices. - - Establishing and managing BLE connections. - - Reading from and writing to characteristics (sending/receiving protobuf payloads). -- **Nordic Backend Preservation (androidMain):** Update the existing Android Nordic implementation to implement the new `MeshtasticRadioProfile` interface without changing its core behavior. -- **Future-Proofing:** Design the abstraction in a way that is generic enough to support adding an iOS or other future target's BLE implementation with minimal refactoring. - -## Non-Functional Requirements -- **Testing:** New `commonMain` unit tests must be written utilizing fakes for the Kable implementation. This is crucial as we cannot rely on Nordic's ready-made mocks in a multiplatform context or if a full migration to Kable occurs. -- **Architecture:** The abstraction must adhere to the project's KMP goals, keeping `core:ble/commonMain` completely free of platform-specific imports (e.g., `java.*`, `android.*`). -- **Compatibility:** The Android build and BLE functionality must remain fully functional using the existing Nordic library. - -## Acceptance Criteria -- [ ] `MeshtasticRadioProfile` is defined in `core:ble/commonMain`. -- [ ] No Nordic-specific or Android-specific types are present in the app-level BLE transport path. -- [ ] Desktop application can successfully scan, connect, and perform read/write operations with a Meshtastic device using Kable. -- [ ] Android application continues to function normally using the Nordic library. -- [ ] New unit tests using Kable fakes are added to `commonMain` and pass successfully. -- [ ] The abstraction architecture provides a clear path for future platform support (like iOS). - -## Out of Scope -- Migrating the Android application to use the Kable backend (this will be evaluated after this track is complete). -- Modifying non-BLE network transports (e.g., USB, TCP). \ No newline at end of file diff --git a/conductor/archive/desktop_di_autowiring_20260313/index.md b/conductor/archive/desktop_di_autowiring_20260313/index.md deleted file mode 100644 index 1bc0ce56b..000000000 --- a/conductor/archive/desktop_di_autowiring_20260313/index.md +++ /dev/null @@ -1,5 +0,0 @@ -# Track desktop_di_autowiring_20260313 Context - -- [Specification](./spec.md) -- [Implementation Plan](./plan.md) -- [Metadata](./metadata.json) \ No newline at end of file diff --git a/conductor/archive/desktop_di_autowiring_20260313/metadata.json b/conductor/archive/desktop_di_autowiring_20260313/metadata.json deleted file mode 100644 index 940262ddd..000000000 --- a/conductor/archive/desktop_di_autowiring_20260313/metadata.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "track_id": "desktop_di_autowiring_20260313", - "type": "chore", - "status": "completed", - "created_at": "2026-03-13T12:00:00Z", - "updated_at": "2026-03-13T12:00:00Z", - "description": "Architecture Health & DI (Immediate Priority) * Desktop Koin checkModules() test: Add a test to ensure Desktop DI bindings are validated at compile-time/test-time so we catch missing interfaces early. * Auto-wire Desktop ViewModels: Configure KSP so we can eliminate the manual ViewModel wiring in DesktopKoinModule and rely on @KoinViewModel annotations like Android does." -} \ No newline at end of file diff --git a/conductor/archive/desktop_di_autowiring_20260313/plan.md b/conductor/archive/desktop_di_autowiring_20260313/plan.md deleted file mode 100644 index b5d55c6ed..000000000 --- a/conductor/archive/desktop_di_autowiring_20260313/plan.md +++ /dev/null @@ -1,16 +0,0 @@ -# Implementation Plan: Desktop DI Auto-Wiring and Validation - -## Phase 1: Setup KSP for Desktop and Test Scaffolding -- [x] Task: Update the `meshtastic.koin` convention plugin (or equivalent `build-logic` files) to apply KSP to the `jvmMain` (Desktop) target for `@KoinViewModel` auto-wiring. -- [x] Task: Write Failing Test: Create `DesktopKoinTest.kt` in `desktop/src/test/kotlin/org/meshtastic/desktop/di/` using `kotlin.test`. - - [x] Initialize Koin application. - - [x] Include `desktopModule()`, `desktopPlatformModule()`, and `desktopPlatformStubsModule()`. - - [x] Call `checkModules()` inside the test and ensure it fails if there are missing interfaces. -- [x] Task: Implement to Pass Tests: Add any missing stubs or correct module includes in `desktopPlatformStubsModule()` to ensure the basic Koin graph resolves. -- [x] Task: Conductor - User Manual Verification 'Phase 1: Setup KSP for Desktop and Test Scaffolding' (Protocol in workflow.md) - -## Phase 2: Auto-wire ViewModels and Clean Up -- [x] Task: Refactor: Remove manual `viewModel { ... }` blocks from `DesktopKoinModule.kt` (if any are present). -- [x] Task: Implement: Ensure the desktop build configuration (`desktop/build.gradle.kts`) correctly includes the KSP-generated Koin modules and that KSP targets the JVM platform. -- [x] Task: Implement to Pass Tests: Verify that running `./gradlew :desktop:test` succeeds and that `DesktopKoinTest.kt` validates the new KSP-wired graph. -- [x] Task: Conductor - User Manual Verification 'Phase 2: Auto-wire ViewModels and Clean Up' (Protocol in workflow.md) \ No newline at end of file diff --git a/conductor/archive/desktop_di_autowiring_20260313/spec.md b/conductor/archive/desktop_di_autowiring_20260313/spec.md deleted file mode 100644 index 5c91bb14a..000000000 --- a/conductor/archive/desktop_di_autowiring_20260313/spec.md +++ /dev/null @@ -1,25 +0,0 @@ -# Specification: Desktop DI Auto-Wiring and Validation - -## Overview -This track addresses immediate architecture health priorities for the Desktop KMP target: -1. **Desktop Koin `checkModules()` test:** Add a compile-time/test-time validation test to ensure Desktop DI bindings resolve correctly and catch missing interfaces early. -2. **Auto-wire Desktop ViewModels:** Configure KSP to generate Koin modules for ViewModels annotated with `@KoinViewModel` in the JVM target, eliminating the need for manual ViewModel wiring in `DesktopKoinModule`. - -## Functional Requirements -- **KSP Configuration:** Update the `meshtastic.koin` (or equivalent) convention plugin to apply KSP and Koin annotations processing to the `jvmMain` (Desktop) target. -- **ViewModel Auto-Wiring:** Remove all manual `viewModel { ... }` definitions in `DesktopKoinModule` and ensure they are successfully replaced by the KSP-generated Koin modules. -- **DI Validation Test:** Implement a new test file (e.g., `DesktopKoinTest.kt`) in `desktop/src/test/kotlin/org/meshtastic/desktop/di/` using `kotlin.test`. -- **Test Scope:** The `checkModules()` test must include and validate all active Desktop Koin modules, including `desktopModule()`, `desktopPlatformModule()`, `desktopPlatformStubsModule()`, and any KSP-generated modules. - -## Non-Functional Requirements -- **Build Performance:** The addition of KSP to the JVM target should not unnecessarily degrade build times. Cacheability must be maintained. -- **Style:** Adhere strictly to the project's existing Kotlin code style and Koin best practices. - -## Acceptance Criteria -- [ ] Running `./gradlew :desktop:test` executes the new `checkModules()` test successfully. -- [ ] No manual ViewModel definitions remain in `DesktopKoinModule` for shared ViewModels (they are auto-wired). -- [ ] If a dependency is missing from the Desktop DI graph, the `checkModules()` test fails explicitly. - -## Out of Scope -- Migrating other platforms (Android, iOS) DI implementations. -- Refactoring the internal logic of the ViewModels themselves. \ No newline at end of file diff --git a/conductor/archive/desktop_parity_20260311/index.md b/conductor/archive/desktop_parity_20260311/index.md deleted file mode 100644 index c034c2f20..000000000 --- a/conductor/archive/desktop_parity_20260311/index.md +++ /dev/null @@ -1,5 +0,0 @@ -# Track desktop_parity_20260311 Context - -- [Specification](./spec.md) -- [Implementation Plan](./plan.md) -- [Metadata](./metadata.json) \ No newline at end of file diff --git a/conductor/archive/desktop_parity_20260311/metadata.json b/conductor/archive/desktop_parity_20260311/metadata.json deleted file mode 100644 index d4c5031c4..000000000 --- a/conductor/archive/desktop_parity_20260311/metadata.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "track_id": "desktop_parity_20260311", - "type": "feature", - "status": "completed", - "created_at": "2026-03-11T12:00:00Z", - "updated_at": "2026-03-11T12:00:00Z", - "description": "continue bringing desktop up to parity with android" -} \ No newline at end of file diff --git a/conductor/archive/desktop_parity_20260311/plan.md b/conductor/archive/desktop_parity_20260311/plan.md deleted file mode 100644 index 381d89d92..000000000 --- a/conductor/archive/desktop_parity_20260311/plan.md +++ /dev/null @@ -1,41 +0,0 @@ -# Implementation Plan - -## Phase 1: Navigation Parity [checkpoint: 5b8e194] -- [x] Task: Extract shared navigation contracts f7e0c2e - - [x] Define shared top-level destinations and route metadata in `core:navigation`. - - [x] Update Android `TopLevelDestination` to use the shared contract. - - [x] Update Desktop `DesktopDestination` to use the shared contract. - - [x] Add parity tests for navigation routing. -- [x] Task: Conductor - User Manual Verification 'Phase 1: Navigation Parity' (Protocol in workflow.md) - -## Phase 2: DI Parity [checkpoint: 5bdc099] -- [x] Task: Migrate Desktop Koin Modules 93fd600 - - [x] Configure KSP for the JVM target in necessary modules. - - [x] Ensure Koin annotations are processed for Desktop. - - [x] Replace manual ViewModel wiring in `DesktopKoinModule` with generated modules. -- [x] Task: Conductor - User Manual Verification 'Phase 2: DI Parity' (Protocol in workflow.md) - -## Phase 3: Connections Parity [checkpoint: 4be5732] -- [x] Task: Create `feature:connections` module 242faa6 - - [x] Set up the KMP module structure with `commonMain`, `androidMain`, and `jvmMain` (or `desktopMain`). - - [x] Move device discovery UI and ViewModels from `app` and `desktop` into the new module. - - [x] Consolidate the Connections UI into a shared screen in `feature:connections`. -- [x] Task: Conductor - User Manual Verification 'Phase 3: Connections Parity' (Protocol in workflow.md) - -## Phase 4: UI/Feature Parity [checkpoint: e83a07a] -- [x] Task: Implement missing Map and Chart features on Desktop 128ee3b - - [x] Evaluate and implement a KMP-friendly mapping library or placeholder for Desktop. - - [x] Refactor Vico charts or provide a KMP charting alternative/placeholder for Desktop. -- [x] Task: Refinement - Connections UI and Messaging Parity c98db4f - - [x] Hide unsupported transports (BLE/USB) on Desktop via BuildUtils proxy. - - [x] Update message titles to resolve channel names for broadcasts. - - [x] Add snackbar for no-op gaps (delivery info). - - [x] Shared AnimatedConnectionsNavIcon for "blinky light" parity. - - *Note: Connection type filtering is currently hardcoded via BuildUtils.sdkInt. This should be refactored to use dynamic transport discovery once the 'Extract hardware transport' track is complete.* -- [x] Task: Conductor - User Manual Verification 'Phase 4: UI/Feature Parity' (Protocol in workflow.md) e83a07a - -## Phase 5: Multi-Target Hardening [checkpoint: 91784a9] -- [x] Task: Clean up remaining platform-specific leaks f5f1e29 - - [x] Ensure `commonMain` is free of any `java.*` dependencies. - - [x] Verify test suite passes on both Android and Desktop JVM targets. -- [x] Task: Conductor - User Manual Verification 'Phase 5: Multi-Target Hardening' (Protocol in workflow.md) 91784a9 \ No newline at end of file diff --git a/conductor/archive/desktop_parity_20260311/spec.md b/conductor/archive/desktop_parity_20260311/spec.md deleted file mode 100644 index 27fef2b6f..000000000 --- a/conductor/archive/desktop_parity_20260311/spec.md +++ /dev/null @@ -1,25 +0,0 @@ -# Track Specification: Desktop Parity & Multi-Target Hardening - -## Overview -This track aims to bring the Desktop target up to parity with the Android app and lay the foundation for future targets (like iOS). This involves eliminating duplicated code, fixing structural gaps, and sharing UI, navigation, and DI contracts across platforms. - -## Functional Requirements -- **Connections Parity:** Consolidate device discovery (BLE/USB/TCP) from the app and desktop into a shared `feature:connections` module. -- **DI Parity:** Remove manual ViewModel wiring in `DesktopKoinModule` and transition to using KSP-generated Koin modules for Desktop. -- **UI/Feature Parity:** Implement missing map and charting functionality on Desktop, or provide robust KMP abstractions where direct translation isn't possible. -- **Navigation Parity:** Extract shared navigation contracts to stop drift between Android and Desktop shells (following `decisions/navigation3-parity-2026-03.md`). - -## Non-Functional Requirements -- **Architecture Readiness:** Ensure code abstractions support the subsequent addition of an iOS target. -- **Structural Purity:** `commonMain` must be completely free of platform-specific APIs (like `java.*` or Android-specific APIs). - -## Acceptance Criteria -- Device discovery screens share UI and view models in `feature:connections`. -- Desktop DI uses generated modules without manual ViewModel instantiation. -- Map and charting features are either functioning on Desktop or have solid KMP placeholders. -- Android and Desktop Navigation shells utilize shared configuration and metadata. -- Both functional and structural parity goals are verified through automated builds and testing where applicable. - -## Out of Scope -- Full deployment to iOS or other unannounced platforms (only preparing the architecture). -- Deep refactoring of underlying hardware interactions beyond what is necessary to expose a shared UI contract. \ No newline at end of file diff --git a/conductor/archive/desktop_serial_transport_20260317/index.md b/conductor/archive/desktop_serial_transport_20260317/index.md deleted file mode 100644 index 1cbe07406..000000000 --- a/conductor/archive/desktop_serial_transport_20260317/index.md +++ /dev/null @@ -1,5 +0,0 @@ -# Track desktop_serial_transport_20260317 Context - -- [Specification](./spec.md) -- [Implementation Plan](./plan.md) -- [Metadata](./metadata.json) \ No newline at end of file diff --git a/conductor/archive/desktop_serial_transport_20260317/metadata.json b/conductor/archive/desktop_serial_transport_20260317/metadata.json deleted file mode 100644 index 0d31a3eb1..000000000 --- a/conductor/archive/desktop_serial_transport_20260317/metadata.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "track_id": "desktop_serial_transport_20260317", - "type": "feature", - "status": "completed", - "created_at": "2026-03-17T12:00:00Z", - "updated_at": "2026-03-17T12:00:00Z", - "description": "Implement Serial/USB transport for the Desktop target using jSerialComm. This fulfills the medium-term priority for direct radio connections on JVM and uses the shared RadioTransport interface." -} \ No newline at end of file diff --git a/conductor/archive/desktop_serial_transport_20260317/plan.md b/conductor/archive/desktop_serial_transport_20260317/plan.md deleted file mode 100644 index 3d55c7380..000000000 --- a/conductor/archive/desktop_serial_transport_20260317/plan.md +++ /dev/null @@ -1,21 +0,0 @@ -# Implementation Plan: Desktop Serial/USB Transport - -## Phase 1: JVM Setup & Dependency Integration [checkpoint: a05916d] -- [x] Task: Add the `jSerialComm` library to the `jvmMain` dependencies of the networking module. [checkpoint: 8994c66] -- [x] Task: Create a `jvmMain` stub implementation for a `SerialTransport` class that implements the shared `RadioTransport` interface. [checkpoint: 83668e4] - -## Phase 2: Serial Port Scanning & Connection Management [checkpoint: 9cda87d] -- [x] Task: Implement port discovery using `jSerialComm` to list available serial ports. [checkpoint: c72501d] -- [x] Task: Implement connect/disconnect logic for a selected serial port, handling port locking and baud rate configuration. [checkpoint: 23ee815] -- [x] Task: Map the input/output streams of the open serial port to the existing KMP stream framing logic (`StreamFrameCodec`). [checkpoint: 04ba9c2] - -## Phase 3: UI Integration -- [x] Task: Update the `feature:connections` UI or `DesktopScannerViewModel` to poll the new `SerialTransport` for available ports. [checkpoint: 2e85b5a] -- [x] Task: Wire the user's serial port selection to initiate the connection via the DI graph and active service logic. [checkpoint: 94cb97c] - -## Phase 4: Validation [checkpoint: 1055752] -- [x] Task: Verify end-to-end communication with a physical Meshtastic device over USB on the desktop target. [checkpoint: 1055752] -- [x] Task: Ensure CI builds cleanly and that no `java.*` dependencies leaked into `commonMain`. [checkpoint: 1055752] - -## Phase: Review Fixes -- [x] Task: Apply review suggestions [checkpoint: d2f7c82] diff --git a/conductor/archive/desktop_serial_transport_20260317/spec.md b/conductor/archive/desktop_serial_transport_20260317/spec.md deleted file mode 100644 index 04ff68481..000000000 --- a/conductor/archive/desktop_serial_transport_20260317/spec.md +++ /dev/null @@ -1,20 +0,0 @@ -# Specification: Desktop Serial/USB Transport via jSerialComm - -## Objective -Implement direct radio connection via Serial/USB on the Desktop (JVM) target using the `jSerialComm` library. This fulfills the medium-term priority of bringing physical transport parity to the desktop app and validates the newly extracted `RadioTransport` abstraction in `core:repository`. - -## Background -Currently, the desktop app supports TCP connections via a shared `StreamFrameCodec`. To provide parity with Android's USB serial connection capabilities, we need to implement a JVM-specific serial transport. The `jSerialComm` library is a widely-used, cross-platform Java library that handles native serial port communication without requiring complex JNI setups. - -## Requirements -- Introduce `jSerialComm` dependency to the `jvmMain` source set of the appropriate core module (likely `core:network` or a new `core:serial` module). -- Implement the `RadioTransport` interface (defined in `core:repository/commonMain`) for the desktop target, wrapping `jSerialComm`'s port scanning and connection logic. -- Ensure the serial data is encoded/decoded using the same protobuf frame structure utilized by the TCP transport (e.g., leveraging the existing `StreamFrameCodec`). -- Integrate the new transport into the `feature:connections` UI on the desktop so users can scan for and select connected USB serial devices. -- Retain platform purity: keep all `jSerialComm` and `java.io.*` imports strictly within the `jvmMain` source set. - -## Success Criteria -- [ ] Desktop application successfully scans for connected Meshtastic devices over USB/Serial. -- [ ] Users can select a serial port from the `feature:connections` UI and establish a connection. -- [ ] Two-way protobuf communication is verified (e.g., the app receives node info and can send a message). -- [ ] The implementation uses the shared `RadioTransport` interface without leaking JVM dependencies into `commonMain`. diff --git a/conductor/archive/desktop_ux_enhancements_20260316/index.md b/conductor/archive/desktop_ux_enhancements_20260316/index.md deleted file mode 100644 index cb8939351..000000000 --- a/conductor/archive/desktop_ux_enhancements_20260316/index.md +++ /dev/null @@ -1,8 +0,0 @@ -# Desktop UX Enhancements - -This track focuses on integrating desktop-specific Compose Multiplatform APIs to improve the native feel and functionality of the desktop client. - -## Track Files -- [Specification](./spec.md) -- [Plan](./plan.md) -- [Metadata](./metadata.json) \ No newline at end of file diff --git a/conductor/archive/desktop_ux_enhancements_20260316/metadata.json b/conductor/archive/desktop_ux_enhancements_20260316/metadata.json deleted file mode 100644 index 826d38551..000000000 --- a/conductor/archive/desktop_ux_enhancements_20260316/metadata.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "id": "desktop_ux_enhancements_20260316", - "name": "Desktop UX Enhancements", - "status": "completed", - "priority": "medium", - "tags": ["desktop", "ux", "compose"] -} \ No newline at end of file diff --git a/conductor/archive/desktop_ux_enhancements_20260316/plan.md b/conductor/archive/desktop_ux_enhancements_20260316/plan.md deleted file mode 100644 index a78fe5bdb..000000000 --- a/conductor/archive/desktop_ux_enhancements_20260316/plan.md +++ /dev/null @@ -1,19 +0,0 @@ -# Implementation Plan: Desktop UX Enhancements - -## Phase 1: Tray & Notifications (Current Focus) -- [x] Add `isAppVisible` state to `Main.kt`. -- [x] Introduce `rememberTrayState()` and the `Tray` composable. -- [x] Update `Window` `onCloseRequest` to toggle visibility instead of exiting the app. -- [x] Add a `DesktopNotificationService` interface and implementation using `TrayState`. - -## Phase 2: Window State Persistence -- [x] Create `DesktopPreferencesDataSource` via DataStore. -- [x] Intercept window bounds changes and write to preferences. -- [x] Read preferences on startup to initialize `rememberWindowState(...)`. - -## Phase 3: Menu Bar & Shortcuts -- [x] Integrate the `MenuBar` composable into the `Window`. -- [x] Implement global application shortcuts. - -## Phase: Review Fixes -- [x] Task: Apply review suggestions 3bda1c007 \ No newline at end of file diff --git a/conductor/archive/desktop_ux_enhancements_20260316/spec.md b/conductor/archive/desktop_ux_enhancements_20260316/spec.md deleted file mode 100644 index 546b4e5c8..000000000 --- a/conductor/archive/desktop_ux_enhancements_20260316/spec.md +++ /dev/null @@ -1,10 +0,0 @@ -# Specification: Desktop UX Enhancements - -## Goal -To implement native desktop behaviors like a system tray, notifications, a menu bar, and persistent window state for the Compose Multiplatform Desktop app. - -## Requirements -1. **System Tray & Notifications**: The app should show a tray icon with a basic context menu ("Open", "Settings", "Quit"). It should support a "Minimize to Tray" flow rather than exiting immediately when closed. Notifications should be dispatchable via `TrayState` for key mesh events. -2. **Window State Persistence**: The app should remember its last window size, position, and maximized state across launches. -3. **Menu Bar**: A native MenuBar (File, Edit, View, Window, Help) should provide standard navigation and controls. -4. **Keyboard Shortcuts**: Common actions should be bound to standard native keyboard shortcuts (e.g. `Cmd/Ctrl+,` for Settings). \ No newline at end of file diff --git a/conductor/archive/doc_consolidation_20260311/index.md b/conductor/archive/doc_consolidation_20260311/index.md deleted file mode 100644 index 0ed0c002c..000000000 --- a/conductor/archive/doc_consolidation_20260311/index.md +++ /dev/null @@ -1,5 +0,0 @@ -# Track doc_consolidation_20260311 Context - -- [Specification](./spec.md) -- [Implementation Plan](./plan.md) -- [Metadata](./metadata.json) \ No newline at end of file diff --git a/conductor/archive/doc_consolidation_20260311/metadata.json b/conductor/archive/doc_consolidation_20260311/metadata.json deleted file mode 100644 index 5720a7d88..000000000 --- a/conductor/archive/doc_consolidation_20260311/metadata.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "track_id": "doc_consolidation_20260311", - "type": "feature", - "status": "completed", - "created_at": "2026-03-11T00:00:00Z", - "updated_at": "2026-03-11T00:00:00Z", - "description": "Implement document consolidation plan" -} \ No newline at end of file diff --git a/conductor/archive/doc_consolidation_20260311/plan.md b/conductor/archive/doc_consolidation_20260311/plan.md deleted file mode 100644 index 692ebe8be..000000000 --- a/conductor/archive/doc_consolidation_20260311/plan.md +++ /dev/null @@ -1,35 +0,0 @@ -# Implementation Plan: Implement document consolidation plan - -## Phase 1: Prune and Consolidate Session Artifacts -- [x] Task: Consolidate session artifacts into `docs/archive/kmp-phase3-testing-consolidation.md`. [d8becb2] - - [x] Write Tests (Verify documentation structure) - - [x] Read all 12+ session update files. - - [x] Create `kmp-phase3-testing-consolidation.md` with merged key findings and test coverage metrics. -- [x] Task: Delete redundant point-in-time files from `docs/agent-playbooks/`. [d8becb2] - - [x] Write Tests (Verify file removal) - - [x] Delete `CHECKLIST-testing-consolidation.md` and other 11 listed files. -- [x] Task: Relocate remaining planning documents. [d8becb2] - - [x] Write Tests (Verify correct destination paths) - - [x] Merge `phase-4-desktop-completion-plan.md` into `docs/roadmap.md` under Phase 4 Desktop section and delete the original. - - [x] Move `kmp-feature-migration-plan.md` to `docs/archive/`. -- [x] Task: Conductor - User Manual Verification 'Phase 1: Prune and Consolidate Session Artifacts' (Protocol in workflow.md) [checkpoint: d8becb2] - -## Phase 2: Synthesize Status & Roadmap -- [x] Task: Update `docs/kmp-status.md`. [37fd055] - - [x] Write Tests (Verify updated metric output) - - [x] Update testing score to reflect Phase 3 completion (80 tests across 6 features). -- [x] Task: Update `docs/roadmap.md`. [37fd055] - - [x] Write Tests (Verify roadmap section exists) - - [x] Mark Phase 3 as substantially complete. -- [x] Task: Conductor - User Manual Verification 'Phase 2: Synthesize Status & Roadmap' (Protocol in workflow.md) [checkpoint: 37fd055] - -## Phase 3: Verify and Validate Best Practices -- [x] Task: Update `AGENTS.md` and playbooks for 2026 KMP Best Practices. [85db394] - - [x] Write Tests (Verify updated content) - - [x] Document Koin Annotations (K2) best practices in `AGENTS.md` and `di-navigation3-anti-patterns-playbook.md`. - - [x] Document Shared ViewModels (MVI) recommendations. -- [x] Task: Documentation Quality Checks. [85db394] - - [x] Write Tests (Verify links resolve) - - [x] Update `docs/agent-playbooks/README.md`. - - [x] Rename `testing-quick-ref.sh` to `testing-quick-ref.md` and update internal references. -- [x] Task: Conductor - User Manual Verification 'Phase 3: Verify and Validate Best Practices' (Protocol in workflow.md) [checkpoint: 85db394] \ No newline at end of file diff --git a/conductor/archive/doc_consolidation_20260311/spec.md b/conductor/archive/doc_consolidation_20260311/spec.md deleted file mode 100644 index 3f4e512c6..000000000 --- a/conductor/archive/doc_consolidation_20260311/spec.md +++ /dev/null @@ -1,13 +0,0 @@ -# Track Specification: Implement document consolidation plan - -## Objective -Consolidate, prune, verify, and validate project plans and documentation against 2026 Kotlin Multiplatform (KMP) best practices and the latest dependency standards. - -## Background & Motivation -The `docs/agent-playbooks/` directory has accumulated numerous point-in-time session summaries, checklists, and status reports (e.g., `SESSION-FINAL-SUMMARY.md`, `TEST-VERIFICATION-REPORT.md`) during the Phase 3 testing consolidation sprint. These files clutter the directory and dilute the actual "playbooks" (reusable guides). Additionally, the project documentation (`kmp-status.md`, `roadmap.md`, `AGENTS.md`) needs to be synthesized to reflect the recently completed work and validated against 2026 KMP industry standards (e.g., Koin K2 compiler plugin best practices, shared ViewModels, Navigation 3). - -## Scope -1. **Prune and Consolidate Session Artifacts:** Merge the key findings into a single historical record (`docs/archive/kmp-phase3-testing-consolidation.md`) and delete 12+ redundant point-in-time files. Relocate `phase-4-desktop-completion-plan.md` into `docs/roadmap.md` and move `kmp-feature-migration-plan.md` to `docs/archive/`. -2. **Synthesize Status & Roadmap:** Update `docs/kmp-status.md` and `docs/roadmap.md` with new testing metrics (80 tests across 6 features) and expanded Phase 4 Desktop tasks. -3. **Verify and Validate against 2026 KMP Best Practices:** Validate the usage of Koin `@Module` and `@KoinViewModel` annotations in `commonMain` according to Koin 4.2 native compiler plugin best practices. Update `AGENTS.md` and `di-navigation3-anti-patterns-playbook.md` to officially recommend this pattern and multiplatform `androidx.lifecycle.ViewModel` in `commonMain`. -4. **Documentation Quality Checks:** Verify `README.md` in playbooks correctly points to retained playbooks. Rename `testing-quick-ref.sh` to `testing-quick-ref.md` and update internal references. \ No newline at end of file diff --git a/conductor/archive/expand_testing_20260318/index.md b/conductor/archive/expand_testing_20260318/index.md deleted file mode 100644 index f0d281e23..000000000 --- a/conductor/archive/expand_testing_20260318/index.md +++ /dev/null @@ -1,5 +0,0 @@ -# Track expand_testing_20260318 Context - -- [Specification](./spec.md) -- [Implementation Plan](./plan.md) -- [Metadata](./metadata.json) \ No newline at end of file diff --git a/conductor/archive/expand_testing_20260318/metadata.json b/conductor/archive/expand_testing_20260318/metadata.json deleted file mode 100644 index 36897869c..000000000 --- a/conductor/archive/expand_testing_20260318/metadata.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "track_id": "expand_testing_20260318", - "type": "chore", - "status": "completed", - "created_at": "2026-03-18T10:00:00Z", - "updated_at": "2026-03-18T10:00:00Z", - "description": "Expand Testing Coverage" -} \ No newline at end of file diff --git a/conductor/archive/expand_testing_20260318/plan.md b/conductor/archive/expand_testing_20260318/plan.md deleted file mode 100644 index e6bd01565..000000000 --- a/conductor/archive/expand_testing_20260318/plan.md +++ /dev/null @@ -1,32 +0,0 @@ -# Implementation Plan: Expand Testing Coverage - -## Phase 1: Baseline Measurement [checkpoint: 6d9ad46] -- [x] Task: Execute `./gradlew koverLog` and record current project test coverage. 8bdd673a1 -- [x] Task: Conductor - User Manual Verification 'Phase 1: Baseline Measurement' (Protocol in workflow.md) 6d9ad468c - -## Phase 2: Feature ViewModel Migration to Turbine [checkpoint: 61b9595] -- [x] Task: Refactor `MetricsViewModelTest` to use `Turbine` and `Mokkery` in `commonTest`. 79e059286 -- [x] Task: Refactor `MessageViewModelTest` to use `Turbine` and `Mokkery` in `commonTest`. b45697b53 -- [x] Task: Refactor `RadioConfigViewModelTest` to use `Turbine` and `Mokkery` in `commonTest`. 33e10fc6c -- [x] Task: Refactor `NodeListViewModelTest` to use `Turbine` and `Mokkery` in `commonTest`. 33e10fc6c -- [x] Task: Refactor remaining `feature` ViewModels to use `Turbine` and `Mokkery`. 33e10fc6c -- [x] Task: Conductor - User Manual Verification 'Phase 2: Feature ViewModel Migration to Turbine' (Protocol in workflow.md) 61b959506 - -## Phase 3: Property-Based Parsing Tests (Kotest) [checkpoint: cb71c85] -- [x] Task: Add `Kotest` property-based tests for `StreamFrameCodec` in `core:network`. 2c8fd6a8f -- [x] Task: Add `Kotest` property-based tests for `PacketHandler` implementations in `core:data`. 7d56c3fef -- [x] Task: Add `Kotest` property-based tests for `TcpTransport` and/or `SerialTransport` in `core:network`. 2fd68d67e -- [x] Task: Conductor - User Manual Verification 'Phase 3: Property-Based Parsing Tests (Kotest)' (Protocol in workflow.md) cb71c8588 - -## Phase 4: Domain Logic Gap Fill [checkpoint: 5735aa1] -- [x] Task: Identify and fill testing gaps in `core:domain` use cases not fully covered during the initial Mokkery migration. 7b815130f -- [x] Task: Conductor - User Manual Verification 'Phase 4: Domain Logic Gap Fill' (Protocol in workflow.md) 5735aa148 - -## Phase 5: Final Measurement & Verification [checkpoint: e321cf0] -- [x] Task: Execute full test suite (`./gradlew test`) to ensure stability. 02fa96f37 -- [x] Task: Execute `./gradlew koverLog` to generate and document the final coverage metrics. e3fe4ba1e -- [x] Task: Conductor - User Manual Verification 'Phase 5: Final Measurement & Verification' (Protocol in workflow.md) e321cf0 - -## Phase 6: Documentation and Wrap-up [checkpoint: d950e5e] -- [x] Task: Review previous steps and update project documentation (e.g., `README.md`, testing guides). b2c9d3e -- [x] Task: Conductor - User Manual Verification 'Phase 6: Documentation and Wrap-up' (Protocol in workflow.md) d950e5e \ No newline at end of file diff --git a/conductor/archive/expand_testing_20260318/spec.md b/conductor/archive/expand_testing_20260318/spec.md deleted file mode 100644 index 2747e5918..000000000 --- a/conductor/archive/expand_testing_20260318/spec.md +++ /dev/null @@ -1,4 +0,0 @@ -# Specification: Expand Testing Coverage - -## Overview -This track focuses on expanding the test suite across all core modules, specifically targeting `feature` ViewModels and `core:network` data parsing logic. The goal is to fully leverage the newly integrated `Turbine` and `Kotest` frameworks to ensure robust property-based testing and asynchronous flow verification. \ No newline at end of file diff --git a/conductor/archive/extract_android_navigation_20260318/index.md b/conductor/archive/extract_android_navigation_20260318/index.md deleted file mode 100644 index 7d7d434fd..000000000 --- a/conductor/archive/extract_android_navigation_20260318/index.md +++ /dev/null @@ -1,5 +0,0 @@ -# Track extract_android_navigation_20260318 Context - -- [Specification](./spec.md) -- [Implementation Plan](./plan.md) -- [Metadata](./metadata.json) \ No newline at end of file diff --git a/conductor/archive/extract_android_navigation_20260318/metadata.json b/conductor/archive/extract_android_navigation_20260318/metadata.json deleted file mode 100644 index ac855c487..000000000 --- a/conductor/archive/extract_android_navigation_20260318/metadata.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "track_id": "extract_android_navigation_20260318", - "type": "refactor", - "status": "completed", - "created_at": "2026-03-18T00:00:00Z", - "updated_at": "2026-03-18T00:00:00Z", - "description": "Extract Android Navigation graphs to feature modules for app thinning" -} \ No newline at end of file diff --git a/conductor/archive/extract_android_navigation_20260318/plan.md b/conductor/archive/extract_android_navigation_20260318/plan.md deleted file mode 100644 index d4184e1d7..000000000 --- a/conductor/archive/extract_android_navigation_20260318/plan.md +++ /dev/null @@ -1,33 +0,0 @@ -# Implementation Plan: Extract Android Navigation - -## Phase 1: Preparation & Base Module Abstraction [checkpoint: 421a587] -- [x] Task: Review current navigation graph assembly in `app/src/main/kotlin/org/meshtastic/app/navigation/`. - - [x] Identify dependencies between feature navigation graphs and core routing definitions. - - [x] Create missing directory structures in feature modules' `androidMain/kotlin/org/meshtastic/feature/*/navigation` if they don't exist. -- [x] Task: Conductor - User Manual Verification 'Phase 1: Preparation & Base Module Abstraction' (Protocol in workflow.md) - -## Phase 2: Feature Module Extraction [checkpoint: 9a27cce] -- [x] Task: Extract Settings Navigation. - - [x] Move `SettingsNavigation.kt` to `feature:settings/androidMain`. - - [x] Fix package declarations and broken imports. -- [x] Task: Extract Nodes & Connections Navigation. - - [x] Move `NodesNavigation.kt` to `feature:node/androidMain`. - - [x] Move `ConnectionsNavigation.kt` to `feature:connections/androidMain`. - - [x] Fix package declarations and broken imports. -- [x] Task: Extract Messaging & Remaining Navigation. - - [x] Move `ContactsNavigation.kt` to `feature:messaging/androidMain`. - - [x] Move `ChannelsNavigation.kt` to `feature:settings/androidMain` or `feature:node`. - - [x] Move `FirmwareNavigation.kt` to `feature:firmware/androidMain`. - - [x] Move `MapNavigation.kt` to `feature:map/androidMain`. - - [x] Fix package declarations and broken imports. -- [x] Task: Conductor - User Manual Verification 'Phase 2: Feature Module Extraction' (Protocol in workflow.md) - -## Phase 3: Root Assembly & Testing [checkpoint: a1e9da3] -- [x] Task: Refactor Root App Graph. - - [x] Update root composition to import the newly relocated navigation extension functions. - - [x] Remove any leftover navigation wiring from the `app` module. -- [x] Task: Implement Navigation Assembly Tests. - - [x] Add basic Android instrumented or Roboelectric tests in `:app` to verify that the `NavHost` successfully constructs all feature graphs without crashing. -- [x] Task: Review previous steps and update project documentation. - - [x] Update `conductor/tech-stack.md` and `conductor/product.md` if necessary to reflect the thinned app module and JetBrains Navigation 3 common usage. -- [x] Task: Conductor - User Manual Verification 'Phase 3: Root Assembly & Testing' (Protocol in workflow.md) \ No newline at end of file diff --git a/conductor/archive/extract_android_navigation_20260318/spec.md b/conductor/archive/extract_android_navigation_20260318/spec.md deleted file mode 100644 index 7b4650573..000000000 --- a/conductor/archive/extract_android_navigation_20260318/spec.md +++ /dev/null @@ -1,19 +0,0 @@ -# Specification: Extract Android Navigation graphs to feature modules for app thinning - -## Overview -The primary goal of this track is to thin out the app module by moving the Android-specific navigation graph wiring (e.g., SettingsNavigation.kt, NodesNavigation.kt, ConnectionsNavigation.kt) into their respective feature modules (e.g., feature:settings, feature:node, feature:connections). This aligns the Android implementation with the Desktop application's architecture, where navigation logic is collocated with the features it routes. - -## Functional Requirements -- **Target Modules:** Move all feature-specific navigation files from `app/src/main/kotlin/org/meshtastic/app/navigation/` to the `androidMain` source sets of their corresponding `feature:*` modules. -- **Architecture:** Implement JetBrains Navigation 3 best practices for common usage across KMP modules. This involves ensuring the feature modules expose their navigation graphs seamlessly to the root NavHost in the app module, minimizing tight coupling. -- **Root App Shell:** The app module should only retain the root MainActivity, the root DI graph assembly, and the top-level NavHost (e.g., MeshtasticApp.kt or similar entry point), calling into the feature modules' exposed graph functions. - -## Non-Functional Requirements -- **Testability:** Add or update tests to verify that the complete navigation graph is correctly assembled from the individual feature modules without errors. -- **Maintainability:** The extraction must preserve all existing deep links, arguments, and navigation transitions currently defined in the Android app. - -## Acceptance Criteria -- [ ] The `app/src/main/kotlin/org/meshtastic/app/navigation/` directory only contains the root graph assembly. -- [ ] All Android feature navigation graphs are successfully extracted to their respective `feature:*` modules. -- [ ] The Android app compiles and runs successfully, with all navigation flows working identically to the previous implementation. -- [ ] New graph assembly tests are added and pass in CI/local environments. \ No newline at end of file diff --git a/conductor/archive/extract_database_manager_kmp_20260320/index.md b/conductor/archive/extract_database_manager_kmp_20260320/index.md deleted file mode 100644 index c5da8cfd3..000000000 --- a/conductor/archive/extract_database_manager_kmp_20260320/index.md +++ /dev/null @@ -1,9 +0,0 @@ -# Track: Extract DatabaseManager to KMP - -## Documents -- [Specification](./spec.md) -- [Implementation Plan](./plan.md) -- [Metadata](./metadata.json) - -## Context -Meshtastic-Android is designed to support per-node databases. Currently, the logic for managing these databases is in `androidMain`, and the desktop module stubs this out, which leads to a lack of feature parity. This track aims to extract that logic into `commonMain`. diff --git a/conductor/archive/extract_database_manager_kmp_20260320/metadata.json b/conductor/archive/extract_database_manager_kmp_20260320/metadata.json deleted file mode 100644 index 7dff1187a..000000000 --- a/conductor/archive/extract_database_manager_kmp_20260320/metadata.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "id": "extract_database_manager_kmp_20260320", - "name": "Extract DatabaseManager to KMP", - "description": "Move core database management logic (per-node databases, LRU) to commonMain for target parity.", - "status": "completed", - "tags": ["core", "database", "kmp", "desktop"], - "created_at": "2026-03-20T12:00:00Z" -} diff --git a/conductor/archive/extract_database_manager_kmp_20260320/plan.md b/conductor/archive/extract_database_manager_kmp_20260320/plan.md deleted file mode 100644 index c1db4e8b2..000000000 --- a/conductor/archive/extract_database_manager_kmp_20260320/plan.md +++ /dev/null @@ -1,25 +0,0 @@ -# Implementation Plan - Extract DatabaseManager to KMP - -## Phase 1: Multiplatform Database Abstraction -- [x] Define `expect fun buildRoomDb(dbName: String): MeshtasticDatabase` in `commonMain`. -- [x] Implement `actual fun buildRoomDb` for Android (using `Application.getDatabasePath`). -- [x] Implement `actual fun buildRoomDb` for JVM/Desktop (using the established `~/.meshtastic` data directory). -- [x] Implement `actual fun buildRoomDb` for iOS (using `NSDocumentDirectory`). -- [x] Update `DatabaseConstants` with shared keys and default values. - -## Phase 2: KMP DataStore & File I/O -- [x] Replace Android `SharedPreferences` in `DatabaseManager` with a KMP-ready `DataStore` instance named `DatabasePrefs`. -- [x] Introduce an `expect fun deleteDatabase(dbName: String)` or similar Okio-based deletion helper. -- [x] Refactor database file listing to use `okio.FileSystem.SYSTEM` instead of `java.io.File`. - -## Phase 3: Logic Extraction -- [x] Move `DatabaseManager.kt` from `core:database/androidMain` to `core:database/commonMain`. -- [x] Refactor `DatabaseManager` to use the new `buildRoomDb`, `DataStore`, and `FileSystem` abstractions. -- [x] Ensure `DatabaseManager` is annotated with Koin `@Single` and correctly binds to `DatabaseProvider` and `SharedDatabaseManager` (from `core:common`). -- [x] Remove `DesktopDatabaseManager` from `desktop` module. -- [x] Update the DI (Koin) graph in `app` and `desktop` to wire the new shared `DatabaseManager`. - -## Phase 4: Verification -- [x] Add unit tests in `core:database/commonTest` to verify that `switchActiveDatabase` correctly swaps databases and that the LRU eviction limit is respected. -- [x] Perform manual verification on Desktop to ensure that connecting to different nodes creates separate `.db` files in `~/.meshtastic/`. -- [x] Verify that the `core:database` module still compiles for Android and iOS targets. diff --git a/conductor/archive/extract_database_manager_kmp_20260320/spec.md b/conductor/archive/extract_database_manager_kmp_20260320/spec.md deleted file mode 100644 index d0f522753..000000000 --- a/conductor/archive/extract_database_manager_kmp_20260320/spec.md +++ /dev/null @@ -1,24 +0,0 @@ -# Specification - Extract DatabaseManager to KMP - -## Overview -Meshtastic-Android is designed to support per-node databases (e.g., `db_!1234abcd.db`). Currently, the logic for managing these databases (switching, LRU caching, eviction) is trapped in `core:database/androidMain`. The Desktop implementation stubs this out, forcing all nodes to share a single database, which is a major architectural regression and leads to data pollution across different devices. - -This track will move the core `DatabaseManager` logic to `commonMain`, enabling full feature parity for database management on Android, Desktop, and iOS. - -## Functional Requirements -- **Per-Node Databases**: Desktop and iOS must support creating and switching between separate databases based on the connected device's address. -- **LRU Eviction**: Implement an LRU (Least Recently Used) cache for database instances on all platforms. -- **Cache Limits**: The database cache limit must be configurable and respected across all platforms. -- **Legacy Cleanup**: Maintain logic for cleaning up legacy databases where applicable. - -## Non-Functional Requirements -- **KMP Purity**: Use only Kotlin Multiplatform-ready libraries (`kotlinx-coroutines`, `okio`, `androidx-datastore`). -- **Dependency Injection**: Use Koin to wire the shared `DatabaseManager` into all app targets. -- **Platform Specifics**: Isolate platform-specific path resolution (e.g., Android `getDatabasePath` vs. JVM `user.home`) using the `expect`/`actual` pattern. - -## Acceptance Criteria -1. `DatabaseManager` resides in `core:database/commonMain`. -2. `DesktopDatabaseManager` (the stub) is deleted. -3. Desktop creates unique database files when connecting to different nodes. -4. Unit tests in `commonTest` verify the LRU eviction logic using an Okio in-memory filesystem (or temporary test directory). -5. No `android.*` or `java.*` imports remain in the shared database management logic. diff --git a/conductor/archive/extract_hardware_transport_20260311/index.md b/conductor/archive/extract_hardware_transport_20260311/index.md deleted file mode 100644 index 0c9c915e4..000000000 --- a/conductor/archive/extract_hardware_transport_20260311/index.md +++ /dev/null @@ -1,5 +0,0 @@ -# Track extract_hardware_transport_20260311 Context - -- [Specification](./spec.md) -- [Implementation Plan](./plan.md) -- [Metadata](./metadata.json) \ No newline at end of file diff --git a/conductor/archive/extract_hardware_transport_20260311/metadata.json b/conductor/archive/extract_hardware_transport_20260311/metadata.json deleted file mode 100644 index a9dc547bf..000000000 --- a/conductor/archive/extract_hardware_transport_20260311/metadata.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "track_id": "extract_hardware_transport_20260311", - "type": "feature", - "status": "completed", - "created_at": "2026-03-11T00:00:00Z", - "updated_at": "2026-03-11T00:00:00Z", - "description": "extract hardware/transport layers out of :app into dedicated :core modules" -} \ No newline at end of file diff --git a/conductor/archive/extract_hardware_transport_20260311/plan.md b/conductor/archive/extract_hardware_transport_20260311/plan.md deleted file mode 100644 index 87b43b632..000000000 --- a/conductor/archive/extract_hardware_transport_20260311/plan.md +++ /dev/null @@ -1,37 +0,0 @@ -# Implementation Plan: Extract hardware/transport layers out of :app into dedicated :core modules - -## Phase 1: Define Shared Interface and Extract Stream Framing [checkpoint: 80a39a5] -- [x] Task: Create `RadioTransport` interface in `core:repository/commonMain`. a47f399 - - [x] Write Tests - - [x] Implement Feature -- [x] Task: Move `StreamFrameCodec` logic to `core:network/commonMain`. cc1ff26 - - [x] Write Tests - - [x] Implement Feature -- [x] Task: Refactor existing `IRadioInterface` usages to point to the new `RadioTransport` interface (preparation step). 1b4cec6 - - [x] Write Tests - - [x] Implement Feature -- [x] Task: Conductor - User Manual Verification 'Phase 1: Define Shared Interface and Extract Stream Framing' (Protocol in workflow.md) 80a39a5 - -## Phase 2: Extract Platform Transports -- [x] Task: Move TCP transport implementation to `core:network/jvmAndroidMain`. [8688070] - - [x] Write Tests - - [x] Implement Feature -- [x] Task: Move BLE transport implementation to `core:ble/androidMain`. [8688070] - - [x] Write Tests - - [x] Implement Feature -- [x] Task: Move Serial/USB transport implementation to `core:service/androidMain`. [8688070] - - [x] Write Tests - - [x] Implement Feature -- [x] Task: Conductor - User Manual Verification 'Phase 2: Extract Platform Transports' (Protocol in workflow.md) [checkpoint: 8688070] - -## Phase 3: Desktop Unification and Cleanup -- [x] Task: Retire `DesktopRadioInterfaceService` in the `desktop` module. - - [x] Write Tests - - [x] Implement Feature -- [x] Task: Update the `desktop` DI graph to inject the shared `TcpTransport` implementation. - - [x] Write Tests - - [x] Implement Feature -- [x] Task: Delete the old `app/repository/radio/` directory. - - [x] Write Tests - - [x] Implement Feature -- [x] Task: Conductor - User Manual Verification 'Phase 3: Desktop Unification and Cleanup' (Protocol in workflow.md) [checkpoint: 8688070] \ No newline at end of file diff --git a/conductor/archive/extract_hardware_transport_20260311/spec.md b/conductor/archive/extract_hardware_transport_20260311/spec.md deleted file mode 100644 index 0a52436a9..000000000 --- a/conductor/archive/extract_hardware_transport_20260311/spec.md +++ /dev/null @@ -1,22 +0,0 @@ -# Track Specification: Extract hardware/transport layers out of :app into dedicated :core modules - -## Overview -This track addresses a critical modularity gap identified in the KMP architecture review: the Radio interface layer is currently locked within the `app` module and is non-KMP. The goal is to define a shared `RadioTransport` interface in `core:repository` and fully extract all transport implementations (BLE, TCP, USB) from `app/repository/radio/` into their appropriate `core` modules. - -## Functional Requirements -- **Define `RadioTransport` Interface:** Create a new `RadioTransport` interface in `core:repository/commonMain` to replace the existing `IRadioInterface`. -- **Extract Stream Framing:** Move `StreamFrameCodec`-based framing logic to `core:network/commonMain`. -- **Extract BLE Transport:** Move the BLE transport implementation (`NordicBleInterface`, etc.) to `core:ble/androidMain`. -- **Extract TCP Transport:** Move the TCP transport implementation to `core:network/jvmAndroidMain`. -- **Extract Serial/USB Transport:** Move the Serial/USB transport implementation to `core:service/androidMain`. -- **Unify Desktop Transport:** Retire Desktop's parallel `DesktopRadioInterfaceService` and migrate it to use the shared `RadioTransport` and `TcpTransport`. - -## Acceptance Criteria -- [ ] A `RadioTransport` interface exists in `core:repository/commonMain`. -- [ ] No transport logic (BLE, TCP, USB) remains in `app/repository/radio/`. -- [ ] The `app` and `desktop` modules successfully compile and run using the extracted transport layers. -- [ ] The `desktop` module uses the shared `TcpTransport` implementation instead of its own duplicate logic. - -## Out of Scope -- Rewriting the underlying logic of the transports (e.g., changing how Nordic BLE works). This is purely a structural extraction and KMP alignment. -- Extracting non-transport components (like the Connections UI) from the `app` module. \ No newline at end of file diff --git a/conductor/archive/extract_radio_interface_kmp_20260320/index.md b/conductor/archive/extract_radio_interface_kmp_20260320/index.md deleted file mode 100644 index 47aea8762..000000000 --- a/conductor/archive/extract_radio_interface_kmp_20260320/index.md +++ /dev/null @@ -1,9 +0,0 @@ -# Track: Extract RadioInterfaceService to KMP - -## Documents -- [Specification](./spec.md) -- [Implementation Plan](./plan.md) -- [Metadata](./metadata.json) - -## Context -Meshtastic-Android and Desktop orchestrate their hardware connections (TCP, Serial, BLE) independently using `AndroidRadioInterfaceService` and `DesktopRadioInterfaceService`. This duplicates complex logic like reconnect loops and state emission. This track aims to unify that logic into `commonMain`. diff --git a/conductor/archive/extract_radio_interface_kmp_20260320/metadata.json b/conductor/archive/extract_radio_interface_kmp_20260320/metadata.json deleted file mode 100644 index 736a106ca..000000000 --- a/conductor/archive/extract_radio_interface_kmp_20260320/metadata.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "id": "extract_radio_interface_kmp_20260320", - "name": "Extract RadioInterfaceService to KMP", - "description": "Unify the connection orchestration lifecycle (TCP, Serial, BLE) into a shared multiplatform service.", - "status": "completed", - "tags": ["core", "service", "kmp", "desktop", "radio", "connection"], - "created_at": "2026-03-20T12:00:00Z" -} diff --git a/conductor/archive/extract_radio_interface_kmp_20260320/plan.md b/conductor/archive/extract_radio_interface_kmp_20260320/plan.md deleted file mode 100644 index 24d0f60f5..000000000 --- a/conductor/archive/extract_radio_interface_kmp_20260320/plan.md +++ /dev/null @@ -1,23 +0,0 @@ -# Implementation Plan - Extract RadioInterfaceService to KMP - -## Phase 1: Research & Abstraction -- [x] Review `AndroidRadioInterfaceService` and `DesktopRadioInterfaceService` to identify identical connection loop logic. -- [x] Identify platform-specific dependencies in both implementations (e.g., Android `BluetoothDevice`, notifications). -- [x] Define shared abstractions (e.g., `TransportFactory`, `NotificationDelegate`) if needed to decouple platform-specific side effects. - -## Phase 2: Logic Extraction -- [x] Create `SharedRadioInterfaceService` in `core:service/commonMain`. -- [x] Move the core connection loop, state management, and retry logic into the shared service. -- [x] Adapt Android and Desktop to use the new shared service. - -## Phase 3: Cleanup & Wiring -- [x] Remove `DesktopRadioInterfaceService`. -- [x] Refactor or remove `AndroidRadioInterfaceService` if entirely superseded. -- [x] Update Koin DI graph in `core:service/commonMain` to provide the unified service. - -## Phase 4: Verification -- [x] Verify that `core:service` and `:app` compile cleanly for Android and Desktop. -- [x] Write or update unit tests in `commonTest` to cover the shared connection lifecycle logic. (Skipped due to coroutine test hanging on infinite heartbeat loop) - -## Phase: Review Fixes -- [x] Task: Apply review suggestions eeeeb11df diff --git a/conductor/archive/extract_radio_interface_kmp_20260320/spec.md b/conductor/archive/extract_radio_interface_kmp_20260320/spec.md deleted file mode 100644 index 15605ece6..000000000 --- a/conductor/archive/extract_radio_interface_kmp_20260320/spec.md +++ /dev/null @@ -1,20 +0,0 @@ -# Specification - Extract RadioInterfaceService to KMP - -## Overview -Currently, the connection orchestration logic for establishing, monitoring, and tearing down connections with Meshtastic radios is duplicated. Android uses `AndroidRadioInterfaceService` in `core:service/androidMain`, and Desktop uses `DesktopRadioInterfaceService` in the `desktop` module. This duplicates core state management (connecting, connected, disconnecting) and the interactions with the shared `TcpTransport`, `SerialTransport`, and `BleTransport`. - -This track aims to abstract the remaining platform-specific connection logic (if any) and move the bulk of `RadioInterfaceService` into `core:repository/commonMain` or `core:service/commonMain`, unifying the connection lifecycle across all targets. - -## Functional Requirements -- **Unified Connection Lifecycle**: A single `RadioInterfaceService` implementation in `commonMain` should handle connection state management (connecting, active, disconnect, reconnect loops). -- **Transport Abstraction**: The service must interact with connections via a multiplatform interface, presumably standardizing around `RadioTransport` or `ConnectionFactory`. -- **Platform Parity**: Desktop and Android must use the exact same logic for detecting disconnects and issuing reconnects. - -## Non-Functional Requirements -- **KMP Purity**: The unified service must not depend on `android.*` or `java.*` specific APIs for its core lifecycle management. -- **Dependency Injection**: Utilize Koin in `commonMain` to provide the unified service. - -## Acceptance Criteria -1. `DesktopRadioInterfaceService` is removed. -2. `AndroidRadioInterfaceService` is replaced by a shared implementation in `commonMain` (e.g., `SharedRadioInterfaceService`). -3. Both Android and Desktop can successfully connect, disconnect, and handle unexpected drops using the shared logic. diff --git a/conductor/archive/extract_remaining_background_20260318/index.md b/conductor/archive/extract_remaining_background_20260318/index.md deleted file mode 100644 index e234976f6..000000000 --- a/conductor/archive/extract_remaining_background_20260318/index.md +++ /dev/null @@ -1,5 +0,0 @@ -# Track extract_remaining_background_20260318 Context - -- [Specification](./spec.md) -- [Implementation Plan](./plan.md) -- [Metadata](./metadata.json) \ No newline at end of file diff --git a/conductor/archive/extract_remaining_background_20260318/metadata.json b/conductor/archive/extract_remaining_background_20260318/metadata.json deleted file mode 100644 index 52498f9fc..000000000 --- a/conductor/archive/extract_remaining_background_20260318/metadata.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "track_id": "extract_remaining_background_20260318", - "type": "refactor", - "status": "completed", - "created_at": "2026-03-18T14:55:00Z", - "updated_at": "2026-03-18T14:55:00Z", - "description": "Extract remaining background services and workers from app module" -} diff --git a/conductor/archive/extract_remaining_background_20260318/plan.md b/conductor/archive/extract_remaining_background_20260318/plan.md deleted file mode 100644 index aa8bcba0e..000000000 --- a/conductor/archive/extract_remaining_background_20260318/plan.md +++ /dev/null @@ -1,29 +0,0 @@ -# Implementation Plan: Extract remaining background services and workers from app module - -## Phase 1: Preparation & Location Manager Abstraction [checkpoint: 57052fc] -- [x] Task: Review current implementations in `app/src/main/kotlin/org/meshtastic/app/service/AndroidMeshLocationManager.kt` and `app/src/main/kotlin/org/meshtastic/app/MeshServiceClient.kt`. -- [x] Task: Create KMP shared interface or base class in `core:service/commonMain` for the Location Manager if applicable, aligning with KMP best practices. -- [x] Task: Relocate `AndroidMeshLocationManager.kt` and `MeshServiceClient.kt` to `core:service/src/androidMain/...`. -- [x] Task: Update package declarations and resolve broken imports in the app module. -- [x] Task: Conductor - User Manual Verification 'Phase 1: Preparation & Location Manager Abstraction' (Protocol in workflow.md) - -## Phase 2: Message Queue Abstraction [checkpoint: dda10b4] -- [x] Task: Review `app/src/main/kotlin/org/meshtastic/app/messaging/domain/worker/WorkManagerMessageQueue.kt`. -- [x] Task: Identify opportunities to extract non-Android specific queue logic to `feature:messaging/commonMain`. -- [x] Task: Relocate `WorkManagerMessageQueue.kt` to `feature:messaging/src/androidMain/...`. -- [x] Task: Update package declarations and resolve broken imports. -- [x] Task: Conductor - User Manual Verification 'Phase 2: Message Queue Abstraction' (Protocol in workflow.md) - -## Phase 3: Widget Extraction [checkpoint: 0c027e3] -- [x] Task: Review the contents of `app/src/main/kotlin/org/meshtastic/app/widget/`. -- [x] Task: Decide whether to move widgets to an existing module (e.g. `core:ui` or `feature:node`) or create a new `feature:widget` module. -- [x] Task: Relocate `LocalStatsWidget.kt`, `LocalStatsWidgetReceiver.kt`, `LocalStatsWidgetState.kt`, `RefreshLocalStatsAction.kt`, and `AndroidAppWidgetUpdater.kt`. -- [x] Task: Relocate necessary widget resources, strings, and AndroidManifest declarations. -- [x] Task: Conductor - User Manual Verification 'Phase 3: Widget Extraction' (Protocol in workflow.md) - -## Phase 4: Dependency Injection Refactoring [checkpoint: c5f09dc] -- [x] Task: Review `app/src/main/kotlin/org/meshtastic/app/MainKoinModule.kt` and `di/AppKoinModule.kt`. -- [x] Task: Move DI bindings for the relocated classes to their new respective modules (e.g., `ServiceKoinModule`, `MessagingKoinModule`). -- [x] Task: Ensure the root app module's DI configuration successfully includes the feature and core Koin modules. -- [x] Task: Run Android instrumented/unit tests to verify graph compilation. -- [x] Task: Conductor - User Manual Verification 'Phase 4: Dependency Injection Refactoring' (Protocol in workflow.md) \ No newline at end of file diff --git a/conductor/archive/extract_remaining_background_20260318/spec.md b/conductor/archive/extract_remaining_background_20260318/spec.md deleted file mode 100644 index 69e8a5224..000000000 --- a/conductor/archive/extract_remaining_background_20260318/spec.md +++ /dev/null @@ -1,22 +0,0 @@ -# Specification: Extract remaining background services and workers from app module - -## Overview -The primary goal of this track is to continue the app module thinning effort by extracting the remaining Android-specific background services, workers, and widgets from the `app` module into appropriate core or feature modules. Where possible, business logic from these components should be abstracted and moved to `commonMain` to support KMP targets. This will leave the app module as a thin entry point shell. - -## Functional Requirements -- **Core Services:** Extract `AndroidMeshLocationManager.kt` and `MeshServiceClient.kt` to `core:service/androidMain`. Refactor underlying logic to `core:service/commonMain` where applicable. -- **Messaging Workers:** Extract `WorkManagerMessageQueue.kt` to `feature:messaging/androidMain`. Analyze logic for potential `commonMain` abstraction. -- **Widgets:** Extract the `LocalStatsWidget` implementation to a new or existing appropriate feature module (e.g. `feature:widget/androidMain`) following KMP feature module conventions. -- **Dependency Injection:** Update the DI graph (`MainKoinModule.kt` / `AppKoinModule.kt`) to resolve these implementations from their new module locations using Koin compiler plugin annotations where applicable. - -## Non-Functional Requirements -- **Testability:** Existing tests related to these services and workers should pass after relocation. -- **Maintainability:** The extraction must preserve all existing app functionality, including background synchronization, location tracking, and widget updates. - -## Acceptance Criteria -- [ ] `AndroidMeshLocationManager.kt` and `MeshServiceClient.kt` are successfully moved to `core:service`. -- [ ] `WorkManagerMessageQueue.kt` is successfully moved to `feature:messaging`. -- [ ] App Widgets are extracted out of the `app` module into an appropriate feature module. -- [ ] Any logic that can be abstracted to `commonMain` has been extracted and shared. -- [ ] `MainKoinModule.kt` is refactored, and DI wires everything correctly. -- [ ] The Android app compiles and runs successfully, with background tasks and widgets working identically to the previous implementation. \ No newline at end of file diff --git a/conductor/archive/extract_services_20260317/index.md b/conductor/archive/extract_services_20260317/index.md deleted file mode 100644 index a8c5021ba..000000000 --- a/conductor/archive/extract_services_20260317/index.md +++ /dev/null @@ -1,5 +0,0 @@ -# Track extract_services_20260317 Context - -- [Specification](./spec.md) -- [Implementation Plan](./plan.md) -- [Metadata](./metadata.json) \ No newline at end of file diff --git a/conductor/archive/extract_services_20260317/metadata.json b/conductor/archive/extract_services_20260317/metadata.json deleted file mode 100644 index adf7d650c..000000000 --- a/conductor/archive/extract_services_20260317/metadata.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "track_id": "extract_services_20260317", - "type": "refactor", - "status": "completed", - "created_at": "2026-03-17T00:00:00Z", - "updated_at": "2026-03-17T00:00:00Z", - "description": "Extract service/worker/radio files from `app` to `core:service/androidMain` and `core:network/androidMain`" -} \ No newline at end of file diff --git a/conductor/archive/extract_services_20260317/plan.md b/conductor/archive/extract_services_20260317/plan.md deleted file mode 100644 index 6425e3954..000000000 --- a/conductor/archive/extract_services_20260317/plan.md +++ /dev/null @@ -1,44 +0,0 @@ -# Implementation Plan: Extract service/worker/radio files from `app` - -## Phase 1: Preparation & Analysis [checkpoint: 72022ed] -- [x] Task: Identify all Android-specific classes to be moved (Services, WorkManager workers, Radio connections in `app`) [fd916e3] - - [ ] Locate `Service` classes in `app/src/main/java/org/meshtastic/app` - - [ ] Locate WorkManager `Worker` classes - - [ ] Locate Radio connection classes -- [x] Task: Conductor - User Manual Verification 'Preparation & Analysis' (Protocol in workflow.md) - -## Phase 2: Extraction to `core:service` [checkpoint: ff47af8] -- [x] Task: Setup `core:service` module for Android and Common targets (if not already fully configured) [a114084] -- [x] Task: Move Android `Service` implementations to `core:service/androidMain` [965def0] - - [x] Move the files - - [x] Update imports and Koin injections -- [x] Task: Abstract shared service logic into `core:service/commonMain` [a85e282] - - [x] Write failing tests for abstracted shared logic (TDD Red) - - [x] Extract interfaces and platform-agnostic logic (TDD Green) - - [x] Refactor the implementations to use these shared abstractions -- [x] Task: Conductor - User Manual Verification 'Extraction to core:service' (Protocol in workflow.md) - -## Phase 3: Extraction to `core:network` [checkpoint: 97a5b62] -- [x] Task: Move Radio connection and networking files from `app` to `core:network/androidMain` [b5233cf] - - [x] Move the files - - [x] Update imports and Koin injections -- [x] Task: Abstract shared radio/network logic into `core:network/commonMain` [cc1581d] - - [x] Write failing tests for abstracted radio logic (TDD Red) - - [x] Extract platform-agnostic business logic (TDD Green) - - [x] Refactor implementations to use shared abstractions -- [x] Task: Conductor - User Manual Verification 'Extraction to core:network' (Protocol in workflow.md) - -## Phase 4: Desktop Integration [checkpoint: fffcedc] -- [x] Task: Integrate newly extracted shared abstractions into the `desktop` module [f39df2f] - - [x] Implement desktop-specific actuals or Koin bindings for the shared interfaces - - [x] Wire up abstracted services/radio logic in desktop Koin graph -- [x] Task: Conductor - User Manual Verification 'Desktop Integration' (Protocol in workflow.md) - -## Phase 5: Verification & Cleanup [checkpoint: a0866e0] -- [x] Task: Build project and verify no regressions in background processing or radio connectivity [a9edc2e] -- [x] Task: Verify test coverage (>80%) for all extracted and refactored code [9cff9bc] -- [x] Task: Remove any lingering unused dependencies or dead code in `app` [e39d2e2] -- [x] Task: Conductor - User Manual Verification 'Verification & Cleanup' (Protocol in workflow.md) - -## Phase: Review Fixes -- [x] Task: Apply review suggestions [1ae9fb6] \ No newline at end of file diff --git a/conductor/archive/extract_services_20260317/spec.md b/conductor/archive/extract_services_20260317/spec.md deleted file mode 100644 index 32d1eb803..000000000 --- a/conductor/archive/extract_services_20260317/spec.md +++ /dev/null @@ -1,32 +0,0 @@ -# Specification: Extract service/worker/radio files from `app` - -## Overview -This track aims to decouple the main `app` module by extracting Android-specific service, WorkManager worker, and radio connection files into `core:service` and `core:network` modules. The goal is to maximize code reuse across Kotlin Multiplatform (KMP) targets, clarify class responsibilities, and improve unit testability by isolating the network and service layers. - -## Goals -- **Decouple `app`:** Remove Android-specific service dependencies from the main app module. -- **KMP Preparation:** Migrate as much logic as possible into `commonMain` for reuse across platforms. -- **Desktop Integration:** If logic is successfully abstracted into `commonMain`, integrate and use it within the `desktop` target to ensure reusability. -- **Testability:** Isolate service and network layers to facilitate better unit testing. -- **Simplification:** Refactor logic during the move to clarify and simplify responsibilities. - -## Functional Requirements -- Identify all service, worker, and radio-related classes currently residing in the `app` module. -- Move Android-specific implementations (e.g., `Service`, `Worker`) to `core:service/androidMain` and `core:network/androidMain`. -- Extract platform-agnostic business logic and interfaces into `commonMain` within those core modules. -- Refactor existing logic where necessary to establish a clear delineation of responsibility. -- Update all dependency injections (Koin modules) and imports across the project to reflect the new locations. -- Attempt to wire up the newly abstracted shared logic within the `desktop` module if applicable. - -## Non-Functional Requirements -- **Architecture Compliance:** Changes must adhere to the MVI / Unidirectional Data Flow and KMP structures defined in `tech-stack.md`. -- **Performance:** Refactoring should not negatively impact app startup time or background processing efficiency. -- **Code Coverage:** Maintain or improve overall test coverage for the extracted components (>80% target). - -## Acceptance Criteria -- [ ] No service, worker, or radio connection classes remain in the `app` module. -- [ ] Extracted Android-specific classes compile successfully in `core:service/androidMain` and `core:network/androidMain`. -- [ ] Shared business logic compiles successfully in `core:service/commonMain` and `core:network/commonMain`. -- [ ] If logic is abstracted for reuse, it is integrated and utilized in the `desktop` target where applicable. -- [ ] The app compiles, installs, and runs without regressions in background processing or radio connectivity. -- [ ] Unit tests for the moved and refactored classes pass. \ No newline at end of file diff --git a/conductor/archive/extract_viewmodels_20260316/index.md b/conductor/archive/extract_viewmodels_20260316/index.md deleted file mode 100644 index aeedeb73a..000000000 --- a/conductor/archive/extract_viewmodels_20260316/index.md +++ /dev/null @@ -1,5 +0,0 @@ -# Track extract_viewmodels_20260316 Context - -- [Specification](./spec.md) -- [Implementation Plan](./plan.md) -- [Metadata](./metadata.json) \ No newline at end of file diff --git a/conductor/archive/extract_viewmodels_20260316/metadata.json b/conductor/archive/extract_viewmodels_20260316/metadata.json deleted file mode 100644 index 5b56ec476..000000000 --- a/conductor/archive/extract_viewmodels_20260316/metadata.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "track_id": "extract_viewmodels_20260316", - "type": "refactor", - "status": "completed", - "created_at": "2026-03-16T12:00:00Z", - "updated_at": "2026-03-16T12:00:00Z", - "description": "Extract remaining 5 App-Only ViewModels (AndroidSettingsViewModel, AndroidRadioConfigViewModel, AndroidDebugViewModel, AndroidMetricsViewModel, UIViewModel) to shared KMP feature/core modules by isolating Android-specific dependencies (Uri, Location, Locale) behind abstractions." -} \ No newline at end of file diff --git a/conductor/archive/extract_viewmodels_20260316/plan.md b/conductor/archive/extract_viewmodels_20260316/plan.md deleted file mode 100644 index 12946e2f9..000000000 --- a/conductor/archive/extract_viewmodels_20260316/plan.md +++ /dev/null @@ -1,20 +0,0 @@ -# Implementation Plan: Extract Remaining App-Only ViewModels - -## Phase 1: Infrastructure & Abstractions [checkpoint: 89c6fd5] -- [x] Task: Implement `MeshtasticUri` (expect/actual wrapper for `android.net.Uri`) in `core:common`. 81e5a4a -- [x] Task: Define `FileService` and `LocationService` interfaces in `core:repository/commonMain`. 1ffa7d2 -- [x] Task: Create Android implementations for these services in `core:service/androidMain`. 1ffa7d2 -- [x] Task: Conductor - User Manual Verification 'Phase 1: Infrastructure & Abstractions' (Protocol in workflow.md) 89c6fd5 - -## Phase 2: Feature Module Extractions (Settings & Node) [checkpoint: 3ea2b2a] -- [x] Task: Extract `AndroidSettingsViewModel` & `AndroidRadioConfigViewModel` to `feature:settings/commonMain`. 091452a -- [x] Task: Extract `AndroidMetricsViewModel` to `feature:node/commonMain`. 52c2f6e -- [x] Task: Extract `AndroidDebugViewModel` to `feature:settings/commonMain`. e1a0387 -- [x] Task: Update Koin modules in `feature:settings` and `feature:node` to wire the new shared ViewModels. (Handled automatically by Koin Annotations K2 plugin) e1a0387 -- [x] Task: Conductor - User Manual Verification 'Phase 2: Feature Module Extractions' (Protocol in workflow.md) 3ea2b2a - -## Phase 3: Core UI & Cleanup [checkpoint: c59243d] -- [x] Task: Extract `UIViewModel` logic to `core:ui/commonMain`. 3ea2b2a -- [x] Task: Verify the `app` module thinning progress and finalize any remaining DI cleanup in `AppKoinModule`. 3ea2b2a -- [x] Task: Ensure all new shared ViewModels have baseline `commonTest` coverage using `core:testing` fakes. fdf34f5 -- [x] Task: Conductor - User Manual Verification 'Phase 3: Core UI & Cleanup' (Protocol in workflow.md) c59243d \ No newline at end of file diff --git a/conductor/archive/extract_viewmodels_20260316/spec.md b/conductor/archive/extract_viewmodels_20260316/spec.md deleted file mode 100644 index 2b782bd95..000000000 --- a/conductor/archive/extract_viewmodels_20260316/spec.md +++ /dev/null @@ -1,20 +0,0 @@ -# Specification: Extract Remaining App-Only ViewModels - -## Overview -This track aims to migrate the final 5 ViewModels currently trapped in the `app` module to their respective KMP `feature:*` or `core:*` modules. These ViewModels contain business logic that should be shared across platforms, but are currently coupled to Android-specific APIs. - -## Functional Requirements -- **Isolate Dependencies:** Identify and abstract Android-specific APIs using a hybrid approach (expect/actual for low-level types and injected interfaces for services). -- **Relocate ViewModels:** Move the core logic of these ViewModels to `commonMain` in the target modules: - - `SettingsViewModel` & `RadioConfigViewModel` -> `feature:settings` - - `DebugViewModel` -> `feature:settings` - - `MetricsViewModel` -> `feature:node` - - `UIViewModel` logic -> `core:ui` -- **Dependency Injection:** Update Koin modules to provide platform-specific implementations of the abstracted interfaces. -- **Maintain Parity:** Ensure existing functionality is preserved on Android while enabling these features on Desktop. - -## Acceptance Criteria -- All 5 ViewModels are extracted from the `app` module and logic resides in `commonMain`. -- `commonTest` coverage is established for the shared logic in each respective module. -- The `app` module file count is further reduced. -- Desktop target can instantiate and use the shared ViewModels. \ No newline at end of file diff --git a/conductor/archive/fix_android_animations_20260313/index.md b/conductor/archive/fix_android_animations_20260313/index.md deleted file mode 100644 index 35c1f67ac..000000000 --- a/conductor/archive/fix_android_animations_20260313/index.md +++ /dev/null @@ -1,5 +0,0 @@ -# Track fix_android_animations_20260313 Context - -- [Specification](./spec.md) -- [Implementation Plan](./plan.md) -- [Metadata](./metadata.json) diff --git a/conductor/archive/fix_android_animations_20260313/metadata.json b/conductor/archive/fix_android_animations_20260313/metadata.json deleted file mode 100644 index 987eb12b7..000000000 --- a/conductor/archive/fix_android_animations_20260313/metadata.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "track_id": "fix_android_animations_20260313", - "type": "bug", - "status": "completed", - "created_at": "2026-03-13T12:00:00Z", - "updated_at": "2026-03-13T12:00:00Z", - "description": "Android animations broken - mainly noticeable on Connections screen, the indescriminate circular and linear progress bars don't move, and the MeshActivity animation is not firing, investigate recomposition and threading strangely enough they're working on Desktop" -} diff --git a/conductor/archive/fix_android_animations_20260313/plan.md b/conductor/archive/fix_android_animations_20260313/plan.md deleted file mode 100644 index 09138e3ee..000000000 --- a/conductor/archive/fix_android_animations_20260313/plan.md +++ /dev/null @@ -1,27 +0,0 @@ -# Implementation Plan: Fix Android Animation Stalls - -## Phase 1: Research and Reproduction -- [x] Task: Historical Regression Analysis - - [x] Compare current code with pre-2.7.14-internal versions to identify changes in threading or UI state management. - - [x] Check `gh` history for commits related to `ConnectionsScreen` and `MeshActivity` transitions. -- [x] Task: Reproduction and Diagnosis - - [x] Create a reproduction case (manual or automated) that consistently shows stalled progress bars on Android. - - [x] Inspect Recomposition counts using Layout Inspector or logging. - - [x] Verify Coroutine Dispatchers used for UI state updates. -- [x] Task: Conductor - User Manual Verification 'Phase 1: Research and Reproduction' (Protocol in workflow.md) - -## Phase 2: Fix Implementation -- [x] Task: Core Animation Fix - - [x] Apply fix to resolve threading/recomposition stalls (e.g., correct `Dispatcher.Main` usage or state hoisting). - - [x] Verify progress bars on Connections screen are animating. -- [x] Task: MeshActivity Transition Fix - - [x] Fix animation firing for `MeshActivity` entries and exits. -- [ ] Task: Conductor - User Manual Verification 'Phase 2: Fix Implementation' (Protocol in workflow.md) - -## Phase 3: Project-wide Audit and Final Verification -- [x] Task: Audit App Animations - - [x] Scan other screens for similar animation stalls and apply fixes where necessary. -- [x] Task: Automated Testing - - [x] Write/Update Compose UI tests to ensure animations are running on Android. - - [x] Verify no regressions on Desktop. -- [x] Task: Conductor - User Manual Verification 'Phase 3: Project-wide Audit and Final Verification' (Protocol in workflow.md) diff --git a/conductor/archive/fix_android_animations_20260313/spec.md b/conductor/archive/fix_android_animations_20260313/spec.md deleted file mode 100644 index c8d3cfe63..000000000 --- a/conductor/archive/fix_android_animations_20260313/spec.md +++ /dev/null @@ -1,25 +0,0 @@ -# Track Specification: Fix Android Animation Stalls (Regression) - -## Overview -This track aims to diagnose and resolve a regression introduced in recent `2.7.14-internal` releases where animations (standard Compose progress indicators and custom transitions) fail to fire on Android. While these animations work correctly on Desktop, they are "stuck" or "stalled" on Android, likely due to threading issues or recomposition failures. - -## Historical Context -- **Introduction**: This issue appeared during the `2.7.14-internal` release cycle. -- **Comparison**: Older versions or the current Desktop build can be used as references to identify code changes that might have triggered the regression. - -## Functional Requirements -- **Animation Restoration**: Restore movement to indeterminate circular and linear progress bars, particularly on the Connections screen. -- **Transition Fixes**: Ensure `MeshActivity` animations (entry/exit/transitions) fire as expected. -- **Project-wide Audit**: Audit other screens for similar "stuck" animations. -- **KMP Parity**: Ensure shared `commonMain` code functions correctly on both Android and Desktop. - -## Non-Functional Requirements -- **Performance**: Ensure no UI jank or excessive recompositions. -- **Verification**: Use historical code comparison (via `gh` or temporary copies) to isolate the breaking change. - -## Acceptance Criteria -- [ ] Indeterminate progress bars on the Connections screen animate continuously. -- [ ] `MeshActivity` animations fire correctly. -- [ ] Root cause identified (Regression since 2.7.14-internal). -- [ ] Automated UI tests verify animation behavior on Android. -- [ ] Unit tests verify state flow if threading/ViewModels are involved. diff --git a/conductor/archive/kmp_doc_review_20260313/index.md b/conductor/archive/kmp_doc_review_20260313/index.md deleted file mode 100644 index a503dd5bd..000000000 --- a/conductor/archive/kmp_doc_review_20260313/index.md +++ /dev/null @@ -1,5 +0,0 @@ -# Track kmp_doc_review_20260313 Context - -- [Specification](./spec.md) -- [Implementation Plan](./plan.md) -- [Metadata](./metadata.json) \ No newline at end of file diff --git a/conductor/archive/kmp_doc_review_20260313/metadata.json b/conductor/archive/kmp_doc_review_20260313/metadata.json deleted file mode 100644 index 25a90e45b..000000000 --- a/conductor/archive/kmp_doc_review_20260313/metadata.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "track_id": "kmp_doc_review_20260313", - "type": "chore", - "status": "completed", - "created_at": "2026-03-13T12:00:00Z", - "updated_at": "2026-03-13T12:00:00Z", - "description": "do a thorough review of the project docs for quality and veracity against the current codebase and recent changes - use tooling as needed. Evaluate updating project documentation for clarity and context. Synthesize and condense documentation and plans as needed. Be sure to thoroughly investigate the current state of the codebase and it's migration to kmp." -} \ No newline at end of file diff --git a/conductor/archive/kmp_doc_review_20260313/plan.md b/conductor/archive/kmp_doc_review_20260313/plan.md deleted file mode 100644 index 87f83f8d1..000000000 --- a/conductor/archive/kmp_doc_review_20260313/plan.md +++ /dev/null @@ -1,23 +0,0 @@ -# Implementation Plan - -## Phase 1: Context Gathering and Codebase Investigation [checkpoint: b644b50] -- [x] Task: Investigate current state of KMP migration [42c36f0] - - [x] Run tooling to analyze KMP modules (`core:*`) vs Android-only modules. - - [x] Identify discrepancies between actual code structure and current documentation. -- [x] Task: Review existing documentation [d87b7a2] - - [x] Review Conductor strategy docs (`conductor/`). - - [x] Review Root docs (`README.md`, `AGENTS.md`, `GEMINI.md`). - - [x] Review `docs/` directory contents. -- [x] Task: Conductor - User Manual Verification 'Context Gathering and Codebase Investigation' (Protocol in workflow.md) [b644b50] - -## Phase 2: Synthesis and Condensation [checkpoint: 40e7c58] -- [x] Task: Synthesize documentation [8c57f14] - - [x] Consolidate related guides into single sources of truth. - - [x] Update documentation to reflect recent KMP migration findings. -- [x] Task: Archive legacy documentation [14b19c5] - - [x] Identify outdated or redundant documents. - - [x] Move identified documents into an `archive/` directory. -- [x] Task: Formulate next steps proposal [2bd7655] - - [x] Draft a proposed plan for remaining KMP migrations based on investigation. - - [x] Document the proposal in the relevant file (e.g., `kmp-status.md`). -- [x] Task: Conductor - User Manual Verification 'Synthesis and Condensation' (Protocol in workflow.md) [40e7c58] \ No newline at end of file diff --git a/conductor/archive/kmp_doc_review_20260313/spec.md b/conductor/archive/kmp_doc_review_20260313/spec.md deleted file mode 100644 index a15e676d0..000000000 --- a/conductor/archive/kmp_doc_review_20260313/spec.md +++ /dev/null @@ -1,24 +0,0 @@ -# Overview -This track involves a thorough review, synthesis, and condensation of the project's documentation for quality and veracity against the current codebase and recent changes. It includes a deep investigation into the current state of the codebase, specifically focusing on its migration to Kotlin Multiplatform (KMP). - -# Functional Requirements -- Conduct a comprehensive review of Conductor strategy docs (`conductor/`), Root repository docs (e.g., `README.md`, `AGENTS.md`), the `docs/` directory, and inline source code docstrings. -- Investigate the current state of KMP migration across the codebase. -- Synthesize and condense existing documentation into clarified, updated guides. -- Archive old, redundant, or outdated documentation. -- Formulate a proposed plan and next steps for the remaining KMP migrations. - -# Non-Functional Requirements -- Ensure documentation is accurate, clear, and contextually aligned with recent codebase changes. -- Use appropriate tooling to analyze the codebase and verify documentation claims. - -# Acceptance Criteria -- [ ] A consolidated, up-to-date documentation structure exists. -- [ ] Legacy or redundant documents are moved to an archive folder. -- [ ] An accurate report of the current KMP migration status is produced. -- [ ] A proposal for the next steps in the KMP migration is documented. -- [ ] Conductor docs, Root docs, the `docs/` directory, and key docstrings align with the actual codebase implementation. - -# Out of Scope -- Actually executing the proposed KMP migrations (this track is purely documentation and planning). -- Modifying application business logic or UI code. \ No newline at end of file diff --git a/conductor/archive/kmp_test_migration_20260318/index.md b/conductor/archive/kmp_test_migration_20260318/index.md deleted file mode 100644 index d448caca6..000000000 --- a/conductor/archive/kmp_test_migration_20260318/index.md +++ /dev/null @@ -1,5 +0,0 @@ -# Track kmp_test_migration_20260318 Context - -- [Specification](./spec.md) -- [Implementation Plan](./plan.md) -- [Metadata](./metadata.json) \ No newline at end of file diff --git a/conductor/archive/kmp_test_migration_20260318/metadata.json b/conductor/archive/kmp_test_migration_20260318/metadata.json deleted file mode 100644 index 73b8373cc..000000000 --- a/conductor/archive/kmp_test_migration_20260318/metadata.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "track_id": "kmp_test_migration_20260318", - "type": "chore", - "status": "completed", - "created_at": "2026-03-18T10:00:00Z", - "updated_at": "2026-03-18T10:00:00Z", - "description": "Migrate tests to KMP best practices and expand coverage" -} \ No newline at end of file diff --git a/conductor/archive/kmp_test_migration_20260318/plan.md b/conductor/archive/kmp_test_migration_20260318/plan.md deleted file mode 100644 index 2f701569a..000000000 --- a/conductor/archive/kmp_test_migration_20260318/plan.md +++ /dev/null @@ -1,18 +0,0 @@ -# Implementation Plan: KMP Test Migration and Coverage Expansion - -## Phase 1: Tool Evaluation & Integration [checkpoint: 3ccc7a7] -- [x] Task: Evaluate Mocking Frameworks -- [x] Task: Integrate Selected Tools (Mokkery, Turbine, Kotest) [b4ba582] -- [x] Task: Conductor - User Manual Verification 'Phase 1: Tool Evaluation & Integration' (Protocol in workflow.md) [3ccc7a7] - -## Phase 2: Mockk Replacement [checkpoint: c8afaef] -- [x] Task: Refactor core modules to Mokkery [7522d38] -- [x] Task: Refactor feature modules to Mokkery [87c7eb6] -- [x] Task: Conductor - User Manual Verification 'Phase 2: Mockk Replacement' (Protocol in workflow.md) [c8afaef] - -## Phase 3: Coverage Expansion -- [x] Task: Expand ViewModels coverage with Turbine [c813be8] -- [x] Task: Conductor - User Manual Verification 'Phase 3: Coverage Expansion' (Protocol in workflow.md) [2395cb9] - -## Phase: Review Fixes -- [x] Task: Apply review suggestions [1739021] \ No newline at end of file diff --git a/conductor/archive/kmp_test_migration_20260318/spec.md b/conductor/archive/kmp_test_migration_20260318/spec.md deleted file mode 100644 index 6141d7ae6..000000000 --- a/conductor/archive/kmp_test_migration_20260318/spec.md +++ /dev/null @@ -1,4 +0,0 @@ -# Specification: KMP Test Migration and Coverage Expansion - -## Overview -Migrate the project's test suite to KMP best practices based on JetBrains guidance, expanding coverage and replacing JVM-specific `mockk` with `dev.mokkery` in `commonMain` to ensure iOS readiness. \ No newline at end of file diff --git a/conductor/archive/migrate_debug_panel_20260319/index.md b/conductor/archive/migrate_debug_panel_20260319/index.md deleted file mode 100644 index 30a087a64..000000000 --- a/conductor/archive/migrate_debug_panel_20260319/index.md +++ /dev/null @@ -1,5 +0,0 @@ -# Track migrate_debug_panel_20260319 Context - -- [Specification](./spec.md) -- [Implementation Plan](./plan.md) -- [Metadata](./metadata.json) \ No newline at end of file diff --git a/conductor/archive/migrate_debug_panel_20260319/metadata.json b/conductor/archive/migrate_debug_panel_20260319/metadata.json deleted file mode 100644 index 0e0ab5b5d..000000000 --- a/conductor/archive/migrate_debug_panel_20260319/metadata.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "track_id": "migrate_debug_panel_20260319", - "type": "feature", - "status": "completed", - "created_at": "2026-03-19T00:00:00Z", - "updated_at": "2026-03-19T10:00:00Z", - "description": "migrate the fully featured debug panel to common source for use in other targets, wire it up in desktop" -} \ No newline at end of file diff --git a/conductor/archive/migrate_debug_panel_20260319/plan.md b/conductor/archive/migrate_debug_panel_20260319/plan.md deleted file mode 100644 index f1e15d3a7..000000000 --- a/conductor/archive/migrate_debug_panel_20260319/plan.md +++ /dev/null @@ -1,23 +0,0 @@ -# Implementation Plan: Debug Panel KMP Migration - -## Phase 1: Analysis and Relocation [checkpoint: a2e83eb] -- [x] Task: Locate all source files for the Android Debug Panel (UI, ViewModels, States). -- [x] Task: Move these files from the Android-specific source sets (e.g., `feature/settings/src/androidMain`) into `feature/settings/src/commonMain`. -- [x] Task: Conductor - User Manual Verification 'Phase 1: Analysis and Relocation' (Protocol in workflow.md) - -## Phase 2: Adaptation to KMP [checkpoint: 834f42c] -- [x] Task: Resolve compilation errors by removing Android-specific imports (`android.*`, `java.*`). -- [x] Task: Migrate Android Jetpack Compose imports (`androidx.compose`) to Compose Multiplatform equivalents (`org.jetbrains.compose.*` or ensuring the standard Multiplatform aliases are used). -- [x] Task: Ensure the Debug Panel ViewModel uses the multiplatform `androidx.lifecycle.ViewModel`. -- [x] Task: Abstract any necessary platform-specific logging or hardware interactions using `expect`/`actual` or KMP interfaces. -- [x] Task: Write or migrate corresponding unit tests to `commonTest`. -- [x] Task: Conductor - User Manual Verification 'Phase 2: Adaptation to KMP' (Protocol in workflow.md) - -## Phase 3: Desktop Integration [checkpoint: de2ae06] -- [x] Task: Wire the Debug Panel into the Desktop target's settings menu (`DesktopSettingsNavigation.kt`). -- [x] Task: Add DI bindings for the Desktop module if the Debug Panel requires specific dependencies. -- [x] Task: Verify the Debug Panel screen can be opened and navigated to from the Desktop app. -- [x] Task: Conductor - User Manual Verification 'Phase 3: Desktop Integration' (Protocol in workflow.md) - -## Phase: Review Fixes -- [x] Task: Apply review suggestions ac69e73 \ No newline at end of file diff --git a/conductor/archive/migrate_debug_panel_20260319/spec.md b/conductor/archive/migrate_debug_panel_20260319/spec.md deleted file mode 100644 index 526d336d4..000000000 --- a/conductor/archive/migrate_debug_panel_20260319/spec.md +++ /dev/null @@ -1,24 +0,0 @@ -# Specification: Debug Panel KMP Migration - -## Overview -Migrate the existing Android-specific Debug Panel to `commonMain` to enable its use across all Kotlin Multiplatform targets, specifically wiring it up for the Desktop target. - -## Functional Requirements -- The complete Android debug panel implementation will be moved and adapted to `commonMain`. -- All capabilities from the existing Android debug panel should be preserved and made functional on the Desktop target if possible. -- The Debug Panel will be accessible within the Desktop Settings menu, mirroring the Android application's navigation structure. -- Any platform-specific system logging (e.g., Logcat) that cannot be migrated will be appropriately abstracted or gracefully degraded. - -## Non-Functional Requirements -- **Architecture:** Follow the project's MVI/UDF architecture. -- **UI:** Leverage Compose Multiplatform for the shared UI, removing any Android-specific Jetpack Compose dependencies from the core shared UI logic. -- **Testing:** Add `commonTest` coverage for the migrated ViewModels and presentation logic. - -## Acceptance Criteria -- [ ] The Debug Panel source code resides in a `commonMain` module (e.g., `feature/settings/src/commonMain`). -- [ ] The Debug Panel compiles and runs successfully on both the Android and Desktop targets. -- [ ] The Desktop application can navigate to the Debug Panel from the Settings menu. -- [ ] Essential debug features (transport logs, packet inspection, etc.) function on the Desktop. - -## Out of Scope -- Creating new debug capabilities that do not already exist in the Android implementation. \ No newline at end of file diff --git a/conductor/archive/migrate_room3_20260320/index.md b/conductor/archive/migrate_room3_20260320/index.md deleted file mode 100644 index 42a6102e3..000000000 --- a/conductor/archive/migrate_room3_20260320/index.md +++ /dev/null @@ -1,5 +0,0 @@ -# Track migrate_room3_20260320 Context - -- [Specification](./spec.md) -- [Implementation Plan](./plan.md) -- [Metadata](./metadata.json) diff --git a/conductor/archive/migrate_room3_20260320/metadata.json b/conductor/archive/migrate_room3_20260320/metadata.json deleted file mode 100644 index e63de1f58..000000000 --- a/conductor/archive/migrate_room3_20260320/metadata.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "track_id": "migrate_room3_20260320", - "type": "chore", - "status": "new", - "created_at": "2026-03-20T00:00:00Z", - "updated_at": "2026-03-20T00:00:00Z", - "description": "Migrate to room3, prepare to support all targets (Android, Desktop, iOS) with bundled SQLite driver and full idiomatic migration." -} diff --git a/conductor/archive/migrate_room3_20260320/plan.md b/conductor/archive/migrate_room3_20260320/plan.md deleted file mode 100644 index a67023655..000000000 --- a/conductor/archive/migrate_room3_20260320/plan.md +++ /dev/null @@ -1,36 +0,0 @@ -# Implementation Plan - Room 3 Migration - -## Phase 1: Dependency Update & Build Logic Refinement -- Update `libs.versions.toml` to Room 3.0. -- Update `AndroidRoomConventionPlugin.kt` to align with Room 3 best practices (e.g., ensuring `room.generateKotlin` is correctly set and using the `androidx.room` Gradle plugin). -- Verify all modules (`core:database`, `core:data`, `app`, etc.) can build with the new dependencies. -- [x] Task: Update `libs.versions.toml` with Room 3.0 and related dependencies. -- [x] Task: Refactor `AndroidRoomConventionPlugin.kt` for Room 3.0. -- [x] Task: Conductor - User Manual Verification 'Phase 1' (Protocol in workflow.md) - -## Phase 2: Core Database Implementation (KMP) -- Refactor `MeshtasticDatabase.kt` and `MeshtasticDatabaseConstructor.kt` to use the new Room 3 `RoomDatabase.Builder` for KMP. -- Configure the `BundledSQLiteDriver` in `commonMain` to ensure consistent SQL behavior across all targets. -- Ensure that DAOs and Entities are using `room-runtime` in `commonMain` correctly. -- Implement platform-specific database setup for Android, Desktop, and iOS in their respective `Main` source sets. -- [x] Task: Refactor `MeshtasticDatabase.kt` for Room 3.0 KMP APIs. -- [x] Task: Configure `BundledSQLiteDriver` in `DatabaseProvider.kt`. -- [x] Task: Implement platform-specific database path logic for Desktop and iOS. -- [x] Task: Conductor - User Manual Verification 'Phase 2' (Protocol in workflow.md) - -## Phase 3: Multi-target Support (iOS) -- Add iOS targets (`iosX64`, `iosArm64`, `iosSimulatorArm64`) to `core:database/build.gradle.kts`. -- Configure the database file path logic for iOS. -- Verify that the `core:database` module compiles for iOS. -- [x] Task: Add iOS targets to `core:database/build.gradle.kts`. -- [x] Task: Verify iOS compilation (Skipped: Linux host). -- [x] Task: Conductor - User Manual Verification 'Phase 3' (Protocol in workflow.md) - -## Phase 4: Verification and Testing -- Update existing database tests in `commonTest`, `androidHostTest`, and `androidDeviceTest` to Room 3. -- Run tests on Android and Desktop to ensure no regressions in behavior. -- Perform manual verification on Android and Desktop apps to ensure the database initializes and functions correctly. -- [x] Task: Update and run DAO unit tests in `commonTest`. -- [x] Task: Run Android instrumented tests (`androidDeviceTest`). -- [x] Task: Manual verification on Desktop. -- [x] Task: Conductor - User Manual Verification 'Phase 4' (Protocol in workflow.md) diff --git a/conductor/archive/migrate_room3_20260320/spec.md b/conductor/archive/migrate_room3_20260320/spec.md deleted file mode 100644 index c6fef5f63..000000000 --- a/conductor/archive/migrate_room3_20260320/spec.md +++ /dev/null @@ -1,28 +0,0 @@ -# Specification - Room 3 Migration - -## Overview -Migrate the existing database implementation from Room 2.8.x to Room 3.0. This migration aims to modernize the persistence layer by adopting Room's new Kotlin Multiplatform (KMP) capabilities, ensuring consistent behavior across Android, Desktop (JVM), and iOS targets. Following best practice from reference projects. - -## Functional Requirements -- **Room 3.0 Update**: Update all Room-related dependencies to version 3.0 (alpha/beta/stable as per latest). -- **KMP Support**: Ensure `core:database` is fully compatible with Android, Desktop (JVM), and iOS targets. -- **Bundled SQLite Driver**: Configure the project to use the `androidx.sqlite:sqlite-bundled` driver for all platforms to ensure consistent SQL behavior and versioning. -- **Schema Management**: Maintain existing database schemas and ensure migrations (if any) are compatible with Room 3. -- **DAO & Entity Optimization**: Refactor DAOs and Entities to use Room 3's idiomatic Kotlin APIs (e.g., using `RoomDatabase.Builder` for KMP). - -## Non-Functional Requirements -- **Performance**: Ensure no significant regression in database performance after the migration. -- **Reliability**: All existing database tests must pass on Android. -- **Maintainability**: Adopt the new Room Gradle plugin for schema export and generation. - -## Acceptance Criteria -1. All modules (`core:database`, `core:data`, etc.) build successfully with Room 3.0. -2. Database initialization works correctly on Android and Desktop. -3. Unit tests for DAOs pass in `commonTest` (where applicable) and `androidDeviceTest`. -4. The `androidx.sqlite:sqlite-bundled` driver is used for database connections. -5. iOS target is added to `core:database` (if not already present) and compiles. - -## Out of Scope -- Migrating to a different database engine (e.g., SQLDelight). -- Major schema changes unrelated to the Room 3 migration. -- Implementing complex iOS-specific UI related to the database. diff --git a/conductor/archive/mqtt_transport_20260318/index.md b/conductor/archive/mqtt_transport_20260318/index.md deleted file mode 100644 index 8f255c832..000000000 --- a/conductor/archive/mqtt_transport_20260318/index.md +++ /dev/null @@ -1,5 +0,0 @@ -# Track mqtt_transport_20260318 Context - -- [Specification](./spec.md) -- [Implementation Plan](./plan.md) -- [Metadata](./metadata.json) \ No newline at end of file diff --git a/conductor/archive/mqtt_transport_20260318/metadata.json b/conductor/archive/mqtt_transport_20260318/metadata.json deleted file mode 100644 index f2ac1628d..000000000 --- a/conductor/archive/mqtt_transport_20260318/metadata.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "track_id": "mqtt_transport_20260318", - "type": "feature", - "status": "completed", - "created_at": "2026-03-18T00:00:00Z", - "updated_at": "2026-03-18T00:00:00Z", - "description": "MQTT transport" -} \ No newline at end of file diff --git a/conductor/archive/mqtt_transport_20260318/plan.md b/conductor/archive/mqtt_transport_20260318/plan.md deleted file mode 100644 index 5788491c1..000000000 --- a/conductor/archive/mqtt_transport_20260318/plan.md +++ /dev/null @@ -1,32 +0,0 @@ -# Implementation Plan: MQTT Transport - -## Phase 1: Core Networking & Library Integration -- [x] Task: Evaluate and add KMP MQTT library dependency (e.g. Kmqtt) to `core:network` or `libs.versions.toml`. [2a4aa35] - - [x] Add dependency to `libs.versions.toml`. - - [x] Apply dependency in `core:network/build.gradle.kts`. -- [x] Task: Implement `MqttTransport` class in `commonMain` of `core:network`. [99d35b3] - - [x] Create failing tests in `commonTest` for MqttTransport initialization and configuration parsing. - - [x] Implement MqttTransport to parse URL (mqtt://, mqtts://), credentials, and configure the underlying MQTT client. - - [x] Write failing tests for connection state flows. - - [x] Implement connection lifecycle handling (connect, disconnect, reconnect). -- [x] Task: Conductor - User Manual Verification 'Phase 1: Core Networking & Library Integration' (Protocol in workflow.md) [93d9a50] - -## Phase 2: Publishing & Subscribing -- [x] Task: Implement message subscription and payload parsing. [4900f69] - - [x] Create failing tests for receiving and mapping standard Meshtastic JSON payloads from subscribed topics. - - [x] Implement topic subscription management in `MqttTransport`. - - [x] Implement payload parsing and integration with `core:model` definitions. -- [x] Task: Implement publishing mechanism. [0991210] - - [x] Create failing tests for formatting and publishing node information/messages to custom topics. - - [x] Implement publish functionality in `MqttTransport`. -- [x] Task: Conductor - User Manual Verification 'Phase 2: Publishing & Subscribing' (Protocol in workflow.md) [7418e53] - -## Phase 3: Service & UI Integration -- [x] Task: Integrate `MqttTransport` into `core:service` and `core:data`. [d414556, e172f53] - - [x] Create failing tests for orchestrating MQTT connection based on user preferences. - - [x] Implement service-level bindings to maintain background connection. -- [x] Task: Implement MQTT UI Configuration Settings. (Verified existing implementation) - - [x] Verified existing `MQTTConfigItemList.kt` correctly manages UI inputs. - - [x] Verified MQTT broker URL, username, password, and custom topic inputs exist in UI. - - [x] Verified UI inputs correctly wire to `ModuleConfig.MQTTConfig` used by `MQTTRepositoryImpl`. -- [x] Task: Conductor - User Manual Verification 'Phase 3: Service & UI Integration' (Protocol in workflow.md) [deaa324] \ No newline at end of file diff --git a/conductor/archive/mqtt_transport_20260318/spec.md b/conductor/archive/mqtt_transport_20260318/spec.md deleted file mode 100644 index e1e213646..000000000 --- a/conductor/archive/mqtt_transport_20260318/spec.md +++ /dev/null @@ -1,33 +0,0 @@ -# Specification: MQTT Transport - -## Overview -Implement an MQTT transport layer for the Meshtastic-Android Kotlin Multiplatform (KMP) application to enable communication with Meshtastic devices over MQTT. This will support Android, Desktop, iOS, and potentially Web platforms in the future. - -## Functional Requirements -- **Platforms:** Ensure the MQTT transport operates correctly across Android, Desktop, and iOS platforms, using KMP best practices (with considerations for Web compatibility if technically feasible). -- **Core Library:** Utilize a dedicated Kotlin Multiplatform MQTT client library (e.g., Kmqtt) within the `core:network` module. -- **Connection Features:** - - Support for both standard (`mqtt://`) and secure TLS/SSL (`mqtts://`) connections. - - Support for username and password authentication. -- **Messaging Features:** - - Subscribe to and publish on user-defined custom topics. - - Parse and serialize standard Meshtastic JSON payloads. -- **UI Integration:** - - Follow the existing Android UX patterns for network/device connections. - - Integrate MQTT configuration seamlessly into the connection or advanced settings menus. - -## Non-Functional Requirements -- **Architecture:** Business logic for MQTT communication must reside in the `core:network` (or a new `core:mqtt`) `commonMain` source set. -- **Testability:** Implement shared tests in `commonTest` to verify connection states, topic parsing, and payload serialization without relying on JVM-specific mocks. -- **Performance:** Ensure background execution and resource management align with the `core:service` architecture. - -## Acceptance Criteria -- [ ] Users can enter an MQTT broker URL (including TLS), username, and password in the UI. -- [ ] The app successfully connects to the specified MQTT broker and maintains the connection in the background. -- [ ] The app can publish Meshtastic node information/messages to the broker. -- [ ] The app can receive and process incoming Meshtastic payloads from subscribed topics. -- [ ] Unit tests cover at least 80% of the new MQTT client logic. - -## Out of Scope -- Direct firmware updates via MQTT (if not natively supported by the standard payload). -- Implementing a full local MQTT broker on the device. \ No newline at end of file diff --git a/conductor/archive/wire_up_notifs_20260316/index.md b/conductor/archive/wire_up_notifs_20260316/index.md deleted file mode 100644 index 10475a87b..000000000 --- a/conductor/archive/wire_up_notifs_20260316/index.md +++ /dev/null @@ -1,5 +0,0 @@ -# Track wire_up_notifs_20260316 Context - -- [Specification](./spec.md) -- [Implementation Plan](./plan.md) -- [Metadata](./metadata.json) \ No newline at end of file diff --git a/conductor/archive/wire_up_notifs_20260316/metadata.json b/conductor/archive/wire_up_notifs_20260316/metadata.json deleted file mode 100644 index c0a345cb9..000000000 --- a/conductor/archive/wire_up_notifs_20260316/metadata.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "track_id": "wire_up_notifs_20260316", - "type": "feature", - "status": "completed", - "created_at": "2026-03-16T00:00:00Z", - "updated_at": "2026-03-16T00:00:00Z", - "description": "wire up notifs" -} \ No newline at end of file diff --git a/conductor/archive/wire_up_notifs_20260316/plan.md b/conductor/archive/wire_up_notifs_20260316/plan.md deleted file mode 100644 index f599f7d1d..000000000 --- a/conductor/archive/wire_up_notifs_20260316/plan.md +++ /dev/null @@ -1,34 +0,0 @@ -# Implementation Plan: Wire Up Notifications - -## Phase 1: Shared Abstraction (commonMain) [checkpoint: 930ce02] -- [x] Task: Define `NotificationManager` interface in `core:service/src/commonMain` 4f2107d - - [x] Create `Notification` data model (title, message, type) - - [x] Define `dispatch(notification: Notification)` method -- [x] Task: Create `NotificationPreferencesDataSource` using DataStore in `core:prefs` 346c2a4 - - [x] Define boolean preferences for categories (e.g., Messages, Node Events) -- [x] Task: Conductor - User Manual Verification 'Phase 1: Shared Abstraction (commonMain)' (Protocol in workflow.md) - -## Phase 2: Migrate Android Implementation (androidMain) [checkpoint: 1eb3cb0] -- [x] Task: Audit existing Android notifications 930ce02 - - [x] Locate current implementation for local push notifications - - [x] Analyze triggers and UX (channels, icons, sounds) -- [x] Task: Implement `AndroidNotificationManager` 31c2a1e - - [x] Adapt existing Android notification code to the new `NotificationManager` interface - - [x] Inject `Context` and `NotificationPreferencesDataSource` - - [x] Respect user notification preferences -- [x] Task: Wire `AndroidNotificationManager` into Koin DI 31c2a1e -- [x] Task: Replace old Android notification calls with the new unified interface 81fd10b -- [x] Task: Conductor - User Manual Verification 'Phase 2: Migrate Android Implementation (androidMain)' (Protocol in workflow.md) - -## Phase 3: Desktop Implementation (desktop) [checkpoint: 759914f] -- [x] Task: Implement `DesktopNotificationManager` 1eb3cb0 - - [x] Inject `TrayState` and `NotificationPreferencesDataSource` - - [x] Delegate `dispatch()` to `TrayState.sendNotification()` respecting user preferences -- [x] Task: Wire `DesktopNotificationManager` into Koin DI 1eb3cb0 -- [x] Task: Conductor - User Manual Verification 'Phase 3: Desktop Implementation (desktop)' (Protocol in workflow.md) - - -## Phase 4: UI Preferences Integration [checkpoint: 3af1e4c] -- [x] Task: Create UI for notification preferences 7ed59c6 - - [x] Add toggles for categories in the Settings screen -- [x] Task: Conductor - User Manual Verification 'Phase 4: UI Preferences Integration' (Protocol in workflow.md) \ No newline at end of file diff --git a/conductor/archive/wire_up_notifs_20260316/spec.md b/conductor/archive/wire_up_notifs_20260316/spec.md deleted file mode 100644 index 0cce32a61..000000000 --- a/conductor/archive/wire_up_notifs_20260316/spec.md +++ /dev/null @@ -1,17 +0,0 @@ -# Specification: Wire Up Notifications - -## Goal -To implement a unified, cross-platform notification system that abstracts platform-specific implementations (Android local push, Desktop TrayState) into a common API for the Kotlin Multiplatform (KMP) core. This will enable consistent notification dispatching for key mesh events. - -## Requirements -1. **Abstraction Layer:** Create a shared `NotificationManager` interface in `commonMain` to handle notification dispatching across all targets. -2. **Platform Implementations:** - - **Android:** Implement native local notifications following the existing Android app behavior and Material Design guidance. - - **Desktop:** Implement system notifications using the `TrayState` API. -3. **Trigger Events:** Replicate the existing Android notification triggers (e.g., new messages, connections) and adapt them to use the new shared abstraction. -4. **User Preferences:** Provide a unified UI for users to opt in or out of specific notification categories, respecting their choices globally. -5. **Foreground Handling & Behavior:** Defer to platform-specific UX guidelines and the established Android implementation for aspects like sound, vibration, and in-app display (e.g., suppressing system notifications if the conversation is active). - -## Out of Scope -- Changes to the underlying networking or Bluetooth layers. -- Remote Push Notifications (FCM/APNs) – this is strictly for local, mesh-driven events. \ No newline at end of file diff --git a/conductor/desktop-uri-import-plan.md b/conductor/desktop-uri-import-plan.md deleted file mode 100644 index f863484ea..000000000 --- a/conductor/desktop-uri-import-plan.md +++ /dev/null @@ -1,31 +0,0 @@ -# Desktop URI Import Plan - -## Objective -Wire up `SharedContact` and `ChannelSet` import logic for the Desktop target. This enables the Desktop app to process deep links or URIs passed on startup via arguments or intercepted by the OS using `java.awt.Desktop`'s `OpenURIHandler`. - -## Key Files & Context -- `desktop/src/main/kotlin/org/meshtastic/desktop/Main.kt`: Desktop app entry point. Must be updated to parse command line arguments and handle OS-level URI opening events. -- `desktop/src/main/kotlin/org/meshtastic/desktop/ui/DesktopMainScreen.kt`: The main UI composition. Must be updated to inject the shared `UIViewModel` and render the `SharedContactDialog` / `ScannedQrCodeDialog` when `requestChannelSet` or `sharedContactRequested` are present. -- `core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/viewmodel/UIViewModel.kt`: Already handles URI dispatch and holds the requests, so no changes are needed here. - -## Implementation Steps - -1. **Update `DesktopMainScreen.kt`** - - Import `org.meshtastic.core.ui.viewmodel.UIViewModel`, `org.koin.compose.viewmodel.koinViewModel`, `org.meshtastic.core.ui.share.SharedContactDialog`, `org.meshtastic.core.ui.qr.ScannedQrCodeDialog`, and `org.meshtastic.core.model.ConnectionState`. - - Inject `UIViewModel` directly into `DesktopMainScreen` via `val uiViewModel = koinViewModel()`. - - Add observations for `uiViewModel.sharedContactRequested` and `uiViewModel.requestChannelSet`. - - Just like in Android's `MainScreen`, conditionally render `SharedContactDialog` and `ScannedQrCodeDialog` if `connectionState == ConnectionState.Connected` and either state contains a valid request. - - Wire `onDismiss` closures to `uiViewModel.clearSharedContactRequested()` and `uiViewModel.clearRequestChannelUrl()`. - -2. **Update `Main.kt` (Desktop)** - - Alter `fun main()` to `fun main(args: Array)`. - - Resolve `UIViewModel` after `koinApp` initialization: `val uiViewModel = koinApp.koin.get()`. - - Process the initial `args` and invoke `uiViewModel.handleScannedUri` using `MeshtasticUri` for any arguments that look like valid Meshtastic URIs (starting with `http` or `meshtastic://`). - - Attempt to attach a `java.awt.desktop.OpenURIHandler` if `java.awt.Desktop.Action.APP_OPEN_URI` is supported. When triggered, process the incoming `event.uri` string using the same `handleScannedUri` logic. - -## Verification & Testing -1. Compile the desktop target with `./gradlew desktop:run --args="meshtastic://meshtastic/v/contact..."`. -2. Connect to a device via Desktop Connections or wait for connection. -3. Validate that the corresponding Shared Contact or Channel Set dialog renders on screen. -4. Verify that dismissing the dialogs properly clears the state in the view model. -5. (Optional, macOS) If testing via packaged DMG, verify that opening a `.webloc` or invoking `open meshtastic://...` triggers the `APP_OPEN_URI` handler and routes through the UI. \ No newline at end of file diff --git a/conductor/doc-consolidation-plan.md b/conductor/doc-consolidation-plan.md deleted file mode 100644 index 1ce4cfe07..000000000 --- a/conductor/doc-consolidation-plan.md +++ /dev/null @@ -1,53 +0,0 @@ -# Objective -Consolidate, prune, verify, and validate project plans and documentation against 2026 Kotlin Multiplatform (KMP) best practices and the latest dependency standards. - -# Background & Motivation -The `docs/agent-playbooks/` directory has accumulated numerous point-in-time session summaries, checklists, and status reports (e.g., `SESSION-FINAL-SUMMARY.md`, `TEST-VERIFICATION-REPORT.md`) during the Phase 3 testing consolidation sprint. These files clutter the directory and dilute the actual "playbooks" (reusable guides). Additionally, the project documentation (`kmp-status.md`, `roadmap.md`, `AGENTS.md`) needs to be synthesized to reflect the recently completed work and validated against 2026 KMP industry standards (e.g., Koin K2 compiler plugin best practices, shared ViewModels, Navigation 3). - -# Proposed Solution - -## 1. Prune and Consolidate Session Artifacts -- **Consolidate:** Merge the key findings, test coverage metrics (80 tests across 6 features), and testing patterns from the 12+ session update files into a single historical record: `docs/archive/kmp-phase3-testing-consolidation.md`. -- **Prune:** Delete the following redundant point-in-time files from `docs/agent-playbooks/`: - - `CHECKLIST-testing-consolidation.md` - - `FINAL-STATUS-tests-fixed.md` - - `MIGRATION-COMPLETE-SUMMARY.md` - - `SESSION-FINAL-SUMMARY.md` - - `SESSION-STATUS-2026-03-11.md` - - `TEST-VERIFICATION-REPORT.md` - - `fix-core-domain-tests.md` - - `kmp-testing-consolidation-slice.md` - - `phase-1-feature-commontest-bootstrap.md` - - `phase-3-completion.md` - - `phase-3-implementation-plan.md` - - `phase-3-integration-tests-started.md` -- **Relocate:** - - Extract the contents of `phase-4-desktop-completion-plan.md` and merge them into `docs/roadmap.md` under the Phase 4 Desktop section. Delete the original file. - - Move `kmp-feature-migration-plan.md` to `docs/archive/` since Phase 3 is mostly complete. - -## 2. Synthesize Status & Roadmap -- **Update `docs/kmp-status.md`:** Update the testing score (currently 5/10) to reflect the completion of Phase 3 integration testing (80 tests across 6 features, test doubles in `core:testing`). -- **Update `docs/roadmap.md`:** Mark Phase 3 as substantially complete. Expand the Phase 4 (Desktop Feature Completion) section using the consolidated plan details. - -## 3. Verify and Validate against 2026 KMP Best Practices -Based on a review of 2026 KMP standards and the project's current dependencies (`Koin 4.2.0-RC1`, `Compose Multiplatform 1.11.0-alpha03`, `Navigation 3 1.1.0-alpha03`): -- **Koin Annotations (K2):** The project's decision to move Koin `@Module` and `@KoinViewModel` annotations into `commonMain` aligns perfectly with Koin 4.2 native compiler plugin best practices. The documentation (`AGENTS.md`, `docs/decisions/architecture-review-2026-03.md`) will be validated and explicitly updated to affirm that this is the correct architectural pattern, not a "portability tradeoff". -- **Shared ViewModels (MVI):** Ensure playbook documentation explicitly recommends utilizing the multiplatform `androidx.lifecycle.ViewModel` in `commonMain` to maintain a single source of truth, heavily relying on `StateFlow`. -- **Navigation 3:** The hybrid parity strategy (shared route contracts, platform adapters) is validated as the 2026 standard for Compose Multiplatform. - -## 4. Documentation Quality Checks -- Verify `docs/agent-playbooks/README.md` correctly points only to the retained playbooks. -- Rename `testing-quick-ref.sh` to `testing-quick-ref.md` for proper markdown rendering and update internal references. - -# Implementation Steps -1. Create `docs/archive/kmp-phase3-testing-consolidation.md` and synthesize the 12+ session artifacts into it. -2. Delete the 12+ redundant session files from `docs/agent-playbooks/`. -3. Update `docs/kmp-status.md` and `docs/roadmap.md` with the new testing metrics and Phase 4 desktop tasks. -4. Rename `testing-quick-ref.sh` to `testing-quick-ref.md` and update internal references. -5. Update `docs/agent-playbooks/README.md` to reflect the pruned directory. -6. Refine `AGENTS.md` and `docs/agent-playbooks/di-navigation3-anti-patterns-playbook.md` to validate Koin K2 multiplatform annotations as the officially recommended pattern. - -# Verification & Testing -- Run `ls docs/agent-playbooks/` to ensure only high-signal playbooks remain. -- Ensure `docs/kmp-status.md` reflects an updated test maturity score (e.g., 8/10). -- Run `git status` and `git diff` to ensure changes are accurate. \ No newline at end of file diff --git a/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/AndroidBluetoothRepository.kt b/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/AndroidBluetoothRepository.kt index c471e2261..097001d1f 100644 --- a/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/AndroidBluetoothRepository.kt +++ b/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/AndroidBluetoothRepository.kt @@ -34,6 +34,7 @@ import kotlinx.coroutines.launch import org.koin.core.annotation.Named import org.koin.core.annotation.Single import org.meshtastic.core.di.CoroutineDispatchers +import kotlin.coroutines.resume /** Android implementation of [BluetoothRepository]. */ @Single @@ -92,8 +93,10 @@ class AndroidBluetoothRepository( override fun onReceive(c: Context, intent: android.content.Intent) { if (intent.action == android.bluetooth.BluetoothDevice.ACTION_BOND_STATE_CHANGED) { val d = - intent.getParcelableExtra( + androidx.core.content.IntentCompat.getParcelableExtra( + intent, android.bluetooth.BluetoothDevice.EXTRA_DEVICE, + android.bluetooth.BluetoothDevice::class.java, ) if (d?.address?.equals(macAddress, ignoreCase = true) == true) { val state = @@ -111,7 +114,7 @@ class AndroidBluetoothRepository( try { context.unregisterReceiver(this) } catch (ignored: Exception) {} - if (cont.isActive) cont.resume(Unit) {} + if (cont.isActive) cont.resume(Unit) } else if ( state == android.bluetooth.BluetoothDevice.BOND_NONE && prevState == android.bluetooth.BluetoothDevice.BOND_BONDING diff --git a/core/common/src/commonMain/kotlin/org/meshtastic/core/common/util/SequentialJob.kt b/core/common/src/commonMain/kotlin/org/meshtastic/core/common/util/SequentialJob.kt index 97c9eec18..353758c2a 100644 --- a/core/common/src/commonMain/kotlin/org/meshtastic/core/common/util/SequentialJob.kt +++ b/core/common/src/commonMain/kotlin/org/meshtastic/core/common/util/SequentialJob.kt @@ -43,19 +43,18 @@ class SequentialJob { */ fun launch(scope: CoroutineScope, timeoutMs: Long = 0, block: suspend CoroutineScope.() -> Unit) { cancel() - val newJob = - scope.handledLaunch { - if (timeoutMs > 0) { - try { - withTimeout(timeoutMs, block) - } catch (e: TimeoutCancellationException) { - Logger.w { "SequentialJob timed out after ${timeoutMs}ms" } - throw e - } - } else { - block() + val newJob = scope.handledLaunch { + if (timeoutMs > 0) { + try { + withTimeout(timeoutMs, block) + } catch (e: TimeoutCancellationException) { + Logger.w { "SequentialJob timed out after ${timeoutMs}ms" } + throw e } + } else { + block() } + } job.value = newJob newJob.invokeOnCompletion { job.compareAndSet(newJob, null) } diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImpl.kt index 774c29ce3..88c376887 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImpl.kt @@ -119,11 +119,10 @@ class MeshConfigFlowManagerImpl( private fun handleNodeInfoComplete() { Logger.i { "NodeInfo complete (Stage 2)" } - val entities = - newNodes.map { info -> - nodeManager.installNodeInfo(info, withBroadcast = false) - nodeManager.nodeDBbyNodeNum[info.num]!! - } + val entities = newNodes.map { info -> + nodeManager.installNodeInfo(info, withBroadcast = false) + nodeManager.nodeDBbyNodeNum[info.num]!! + } newNodes.clear() scope.handledLaunch { diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt index c41a9e3da..12e3a6631 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt @@ -188,20 +188,19 @@ class MeshConnectionManagerImpl( private fun startHandshakeStallGuard(stage: Int, action: () -> Unit) { handshakeTimeout?.cancel() - handshakeTimeout = - scope.handledLaunch { + handshakeTimeout = scope.handledLaunch { + delay(HANDSHAKE_TIMEOUT) + if (serviceRepository.connectionState.value is ConnectionState.Connecting) { + Logger.w { "Handshake stall detected! Retrying Stage $stage." } + action() + // Recursive timeout for one more try delay(HANDSHAKE_TIMEOUT) if (serviceRepository.connectionState.value is ConnectionState.Connecting) { - Logger.w { "Handshake stall detected! Retrying Stage $stage." } - action() - // Recursive timeout for one more try - delay(HANDSHAKE_TIMEOUT) - if (serviceRepository.connectionState.value is ConnectionState.Connecting) { - Logger.e { "Handshake still stalled after retry. Resetting connection." } - onConnectionChanged(ConnectionState.Disconnected) - } + Logger.e { "Handshake still stalled after retry. Resetting connection." } + onConnectionChanged(ConnectionState.Disconnected) } } + } } private fun handleDeviceSleep() { @@ -220,19 +219,18 @@ class MeshConnectionManagerImpl( ) } - sleepTimeout = - scope.handledLaunch { - try { - val localConfig = radioConfigRepository.localConfigFlow.first() - val timeout = (localConfig.power?.ls_secs ?: 0) + DEVICE_SLEEP_TIMEOUT_SECONDS - Logger.d { "Waiting for sleeping device, timeout=$timeout secs" } - delay(timeout.seconds) - Logger.w { "Device timeout out, setting disconnected" } - onConnectionChanged(ConnectionState.Disconnected) - } catch (_: CancellationException) { - Logger.d { "device sleep timeout cancelled" } - } + sleepTimeout = scope.handledLaunch { + try { + val localConfig = radioConfigRepository.localConfigFlow.first() + val timeout = (localConfig.power?.ls_secs ?: 0) + DEVICE_SLEEP_TIMEOUT_SECONDS + Logger.d { "Waiting for sleeping device, timeout=$timeout secs" } + delay(timeout.seconds) + Logger.w { "Device timeout out, setting disconnected" } + onConnectionChanged(ConnectionState.Disconnected) + } catch (_: CancellationException) { + Logger.d { "device sleep timeout cancelled" } } + } serviceBroadcasts.broadcastConnection() } diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt index 6931a44d1..7873dc82e 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt @@ -630,8 +630,7 @@ class MeshDataHandlerImpl( // Find the original packet to get the contactKey packetRepository.value.getPacketByPacketId(decoded.reply_id)?.let { originalPacket -> // Skip notification if the original message was filtered - val targetId = - if (originalPacket.from == DataPacket.ID_LOCAL) originalPacket.to else originalPacket.from + val targetId = if (originalPacket.from == DataPacket.ID_LOCAL) originalPacket.to else originalPacket.from val contactKey = "${originalPacket.channel}$targetId" val conversationMuted = packetRepository.value.getContactSettings(contactKey).isMuted val nodeMuted = nodeManager.nodeDBbyID[fromId]?.isMuted == true @@ -640,11 +639,7 @@ class MeshDataHandlerImpl( if (!isSilent) { val channelName = if (originalPacket.to == DataPacket.ID_BROADCAST) { - radioConfigRepository.channelSetFlow - .first() - .settings - .getOrNull(originalPacket.channel) - ?.name + radioConfigRepository.channelSetFlow.first().settings.getOrNull(originalPacket.channel)?.name } else { null } diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImpl.kt index 0c9ceaa2b..3c0644cb6 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImpl.kt @@ -162,13 +162,12 @@ class MeshMessageProcessorImpl( private fun flushEarlyReceivedPackets(reason: String) { scope.launch { - val packets = - earlyMutex.withLock { - if (earlyReceivedPackets.isEmpty()) return@withLock emptyList() - val list = earlyReceivedPackets.toList() - earlyReceivedPackets.clear() - list - } + val packets = earlyMutex.withLock { + if (earlyReceivedPackets.isEmpty()) return@withLock emptyList() + val list = earlyReceivedPackets.toList() + earlyReceivedPackets.clear() + list + } if (packets.isEmpty()) return@launch Logger.d { "replayEarlyPackets reason=$reason count=${packets.size}" } diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt index d674c4b5a..803ded5af 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt @@ -271,8 +271,9 @@ class NodeManagerImpl( if (shouldPreserveExistingUser(node.user, user)) { // keep existing names } else { - var newUser = - user.let { if (it.is_licensed == true) it.copy(public_key = ByteString.EMPTY) else it } + var newUser = user.let { + if (it.is_licensed == true) it.copy(public_key = ByteString.EMPTY) else it + } if (info.via_mqtt) { newUser = newUser.copy(long_name = "${newUser.long_name} (MQTT)") } diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/PacketHandlerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/PacketHandlerImpl.kt index 0ac6ef9ce..3b4715029 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/PacketHandlerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/PacketHandlerImpl.kt @@ -146,32 +146,31 @@ class PacketHandlerImpl( private fun startPacketQueue() { if (queueJob?.isActive == true) return - queueJob = - scope.handledLaunch { - try { - while (serviceRepository.connectionState.value == ConnectionState.Connected) { - val packet = queueMutex.withLock { queuedPackets.removeFirstOrNull() } ?: break - @Suppress("TooGenericExceptionCaught", "SwallowedException") - try { - val response = sendPacket(packet) - Logger.d { "queueJob packet id=${packet.id.toUInt()} waiting" } - val success = withTimeout(TIMEOUT) { response.await() } - Logger.d { "queueJob packet id=${packet.id.toUInt()} success $success" } - } catch (e: TimeoutCancellationException) { - Logger.d { "queueJob packet id=${packet.id.toUInt()} timeout" } - } catch (e: Exception) { - Logger.d { "queueJob packet id=${packet.id.toUInt()} failed" } - } finally { - responseMutex.withLock { queueResponse.remove(packet.id) } - } - } - } finally { - queueJob = null - if (queueMutex.withLock { queuedPackets.isNotEmpty() }) { - startPacketQueue() + queueJob = scope.handledLaunch { + try { + while (serviceRepository.connectionState.value == ConnectionState.Connected) { + val packet = queueMutex.withLock { queuedPackets.removeFirstOrNull() } ?: break + @Suppress("TooGenericExceptionCaught", "SwallowedException") + try { + val response = sendPacket(packet) + Logger.d { "queueJob packet id=${packet.id.toUInt()} waiting" } + val success = withTimeout(TIMEOUT) { response.await() } + Logger.d { "queueJob packet id=${packet.id.toUInt()} success $success" } + } catch (e: TimeoutCancellationException) { + Logger.d { "queueJob packet id=${packet.id.toUInt()} timeout" } + } catch (e: Exception) { + Logger.d { "queueJob packet id=${packet.id.toUInt()} failed" } + } finally { + responseMutex.withLock { queueResponse.remove(packet.id) } } } + } finally { + queueJob = null + if (queueMutex.withLock { queuedPackets.isNotEmpty() }) { + startPacketQueue() + } } + } } private fun changeStatus(packetId: Int, m: MessageStatus) = scope.handledLaunch { diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/TracerouteSnapshotRepositoryImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/TracerouteSnapshotRepositoryImpl.kt index c4712967f..81c1c5ed6 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/TracerouteSnapshotRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/TracerouteSnapshotRepositoryImpl.kt @@ -48,15 +48,14 @@ class TracerouteSnapshotRepositoryImpl( val dao = dbManager.currentDb.value.tracerouteNodePositionDao() dao.deleteByLogUuid(logUuid) if (positions.isEmpty()) return@withContext - val entities = - positions.map { (nodeNum, position) -> - TracerouteNodePositionEntity( - logUuid = logUuid, - requestId = requestId, - nodeNum = nodeNum, - position = position, - ) - } + val entities = positions.map { (nodeNum, position) -> + TracerouteNodePositionEntity( + logUuid = logUuid, + requestId = requestId, + nodeNum = nodeNum, + position = position, + ) + } dao.insertAll(entities) } } diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerTest.kt index 0f3c5dfdb..e1a502dd8 100644 --- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerTest.kt +++ b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerTest.kt @@ -490,8 +490,10 @@ class MeshDataHandlerTest { MeshPacket( id = 42, from = 456, - decoded = - Data(portnum = PortNum.TEXT_MESSAGE_APP, payload = "hello".encodeToByteArray().toByteString()), + decoded = Data( + portnum = PortNum.TEXT_MESSAGE_APP, + payload = "hello".encodeToByteArray().toByteString(), + ), ) val dataPacket = DataPacket( @@ -508,8 +510,7 @@ class MeshDataHandlerTest { // Provide sender node so getSenderName() doesn't fall back to getString (requires Skiko) every { nodeManager.nodeDBbyID } returns mapOf( - "!remote" to - Node(num = 456, user = User(id = "!remote", long_name = "Remote User", short_name = "RU")), + "!remote" to Node(num = 456, user = User(id = "!remote", long_name = "Remote User", short_name = "RU")), ) handler.handleReceivedData(packet, 123) @@ -524,8 +525,10 @@ class MeshDataHandlerTest { MeshPacket( id = 42, from = 456, - decoded = - Data(portnum = PortNum.TEXT_MESSAGE_APP, payload = "hello".encodeToByteArray().toByteString()), + decoded = Data( + portnum = PortNum.TEXT_MESSAGE_APP, + payload = "hello".encodeToByteArray().toByteString(), + ), ) val dataPacket = DataPacket( @@ -597,8 +600,7 @@ class MeshDataHandlerTest { MeshPacket( id = 55, from = 456, - decoded = - Data(portnum = PortNum.RANGE_TEST_APP, payload = "test".encodeToByteArray().toByteString()), + decoded = Data(portnum = PortNum.RANGE_TEST_APP, payload = "test".encodeToByteArray().toByteString()), ) val dataPacket = DataPacket( @@ -614,8 +616,7 @@ class MeshDataHandlerTest { every { messageFilter.shouldFilter(any(), any()) } returns false every { nodeManager.nodeDBbyID } returns mapOf( - "!remote" to - Node(num = 456, user = User(id = "!remote", long_name = "Remote User", short_name = "RU")), + "!remote" to Node(num = 456, user = User(id = "!remote", long_name = "Remote User", short_name = "RU")), ) handler.handleReceivedData(packet, 123) @@ -687,8 +688,10 @@ class MeshDataHandlerTest { MeshPacket( id = 88, from = 456, - decoded = - Data(portnum = PortNum.TEXT_MESSAGE_APP, payload = "hello".encodeToByteArray().toByteString()), + decoded = Data( + portnum = PortNum.TEXT_MESSAGE_APP, + payload = "hello".encodeToByteArray().toByteString(), + ), ) val dataPacket = DataPacket( diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/DatabaseManager.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/DatabaseManager.kt index af46bcecd..7b6360cd2 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/DatabaseManager.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/DatabaseManager.kt @@ -184,8 +184,9 @@ open class DatabaseManager( val limit = getCurrentCacheLimit() val all = listExistingDbNames() // Only enforce the limit over device-specific DBs; exclude legacy and default DBs - val deviceDbs = - all.filterNot { it == DatabaseConstants.LEGACY_DB_NAME || it == DatabaseConstants.DEFAULT_DB_NAME } + val deviceDbs = all.filterNot { + it == DatabaseConstants.LEGACY_DB_NAME || it == DatabaseConstants.DEFAULT_DB_NAME + } if (deviceDbs.size <= limit) return@withLock val usageSnapshot = deviceDbs.associateWith { lastUsed(it) } diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/PacketDao.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/PacketDao.kt index ba0f34056..1419d51e7 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/PacketDao.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/PacketDao.kt @@ -361,21 +361,20 @@ interface PacketDao { @Transaction suspend fun setMuteUntil(contacts: List, until: Long) { - val contactList = - contacts.map { contact -> - // Always mute - val absoluteMuteUntil = - if (until == Long.MAX_VALUE) { - Long.MAX_VALUE - } else if (until == 0L) { // unmute - 0L - } else { - nowMillis + until - } + val contactList = contacts.map { contact -> + // Always mute + val absoluteMuteUntil = + if (until == Long.MAX_VALUE) { + Long.MAX_VALUE + } else if (until == 0L) { // unmute + 0L + } else { + nowMillis + until + } - getContactSettings(contact)?.copy(muteUntil = absoluteMuteUntil) - ?: ContactSettings(contact_key = contact, muteUntil = absoluteMuteUntil) - } + getContactSettings(contact)?.copy(muteUntil = absoluteMuteUntil) + ?: ContactSettings(contact_key = contact, muteUntil = absoluteMuteUntil) + } upsertContactSettings(contactList) } @@ -480,10 +479,9 @@ interface PacketDao { val indexMap = oldSettings .mapIndexed { oldIndex, oldChannel -> - val pskMatches = - newSettings.mapIndexedNotNull { index, channel -> - if (channel.psk == oldChannel.psk) index to channel else null - } + val pskMatches = newSettings.mapIndexedNotNull { index, channel -> + if (channel.psk == oldChannel.psk) index to channel else null + } val newIndex = when { diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/Packet.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/Packet.kt index 859913c23..16b1e66e4 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/Packet.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/Packet.kt @@ -98,12 +98,9 @@ data class Packet( fun getRelayNode(relayNodeId: Int, nodes: List, ourNodeNum: Int?): Node? { val relayNodeIdSuffix = relayNodeId and RELAY_NODE_SUFFIX_MASK - val candidateRelayNodes = - nodes.filter { - it.num != ourNodeNum && - it.lastHeard != 0 && - (it.num and RELAY_NODE_SUFFIX_MASK) == relayNodeIdSuffix - } + val candidateRelayNodes = nodes.filter { + it.num != ourNodeNum && it.lastHeard != 0 && (it.num and RELAY_NODE_SUFFIX_MASK) == relayNodeIdSuffix + } val closestRelayNode = if (candidateRelayNodes.size == 1) { diff --git a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/ModuleConfigDataSource.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/ModuleConfigDataSource.kt index 54db1ad0b..b4f573377 100644 --- a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/ModuleConfigDataSource.kt +++ b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/ModuleConfigDataSource.kt @@ -51,8 +51,7 @@ class ModuleConfigDataSource( when { config.mqtt != null -> current.copy(mqtt = config.mqtt) config.serial != null -> current.copy(serial = config.serial) - config.external_notification != null -> - current.copy(external_notification = config.external_notification) + config.external_notification != null -> current.copy(external_notification = config.external_notification) config.store_forward != null -> current.copy(store_forward = config.store_forward) config.range_test != null -> current.copy(range_test = config.range_test) config.telemetry != null -> current.copy(telemetry = config.telemetry) diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Node.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Node.kt index fe6cad31b..13eccae2a 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Node.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Node.kt @@ -199,12 +199,9 @@ data class Node( fun getRelayNode(relayNodeId: Int, nodes: List, ourNodeNum: Int?): Node? { val relayNodeIdSuffix = relayNodeId and RELAY_NODE_SUFFIX_MASK - val candidateRelayNodes = - nodes.filter { - it.num != ourNodeNum && - it.lastHeard != 0 && - (it.num and RELAY_NODE_SUFFIX_MASK) == relayNodeIdSuffix - } + val candidateRelayNodes = nodes.filter { + it.num != ourNodeNum && it.lastHeard != 0 && (it.num and RELAY_NODE_SUFFIX_MASK) == relayNodeIdSuffix + } val closestRelayNode = if (candidateRelayNodes.size == 1) { diff --git a/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/TopLevelDestination.kt b/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/TopLevelDestination.kt index aed27c7af..b25a61081 100644 --- a/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/TopLevelDestination.kt +++ b/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/TopLevelDestination.kt @@ -40,7 +40,8 @@ enum class TopLevelDestination(val label: StringResource, val route: Route) { ; companion object { - fun fromNavKey(key: NavKey?): TopLevelDestination? = - entries.find { dest -> key?.let { it::class == dest.route::class } == true } + fun fromNavKey(key: NavKey?): TopLevelDestination? = entries.find { dest -> + key?.let { it::class == dest.route::class } == true + } } } diff --git a/core/network/src/androidUnitTest/kotlin/org/meshtastic/core/network/radio/BleRadioInterfaceTest.kt b/core/network/src/androidHostTest/kotlin/org/meshtastic/core/network/radio/BleRadioInterfaceTest.kt similarity index 96% rename from core/network/src/androidUnitTest/kotlin/org/meshtastic/core/network/radio/BleRadioInterfaceTest.kt rename to core/network/src/androidHostTest/kotlin/org/meshtastic/core/network/radio/BleRadioInterfaceTest.kt index 180cfb173..2de9f6884 100644 --- a/core/network/src/androidUnitTest/kotlin/org/meshtastic/core/network/radio/BleRadioInterfaceTest.kt +++ b/core/network/src/androidHostTest/kotlin/org/meshtastic/core/network/radio/BleRadioInterfaceTest.kt @@ -17,6 +17,7 @@ package org.meshtastic.core.network.radio import dev.mokkery.MockMode +import dev.mokkery.answering.returns import dev.mokkery.every import dev.mokkery.everySuspend import dev.mokkery.matcher.any @@ -28,9 +29,6 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest -import org.junit.Assert.assertEquals -import org.junit.Before -import org.junit.Test import org.meshtastic.core.ble.BleConnection import org.meshtastic.core.ble.BleConnectionFactory import org.meshtastic.core.ble.BleConnectionState @@ -39,6 +37,9 @@ import org.meshtastic.core.ble.BleScanner import org.meshtastic.core.ble.BluetoothRepository import org.meshtastic.core.ble.BluetoothState import org.meshtastic.core.repository.RadioInterfaceService +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals @OptIn(ExperimentalCoroutinesApi::class) class BleRadioInterfaceTest { @@ -54,8 +55,8 @@ class BleRadioInterfaceTest { private val connectionStateFlow = MutableSharedFlow(replay = 1) private val bluetoothStateFlow = MutableStateFlow(BluetoothState()) - @Before - fun setUp() { + @BeforeTest + fun setup() { every { connectionFactory.create(any(), any()) } returns connection every { connection.connectionState } returns connectionStateFlow every { bluetoothRepository.state } returns bluetoothStateFlow.asStateFlow() diff --git a/core/network/src/androidUnitTest/kotlin/org/meshtastic/core/network/radio/StreamInterfaceTest.kt b/core/network/src/androidUnitTest/kotlin/org/meshtastic/core/network/radio/StreamInterfaceTest.kt deleted file mode 100644 index fad59f8a4..000000000 --- a/core/network/src/androidUnitTest/kotlin/org/meshtastic/core/network/radio/StreamInterfaceTest.kt +++ /dev/null @@ -1,106 +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.core.network.radio - -import dev.mokkery.MockMode -import dev.mokkery.matcher.any -import dev.mokkery.mock -import dev.mokkery.verify -import dev.mokkery.verify.VerifyMode -import dev.mokkery.verifyNoMoreCalls -import org.junit.Test -import org.meshtastic.core.repository.RadioInterfaceService - -class StreamInterfaceTest { - - private val service: RadioInterfaceService = mock(MockMode.autofill) - - // Concrete implementation for testing - private class TestStreamInterface(service: RadioInterfaceService) : StreamInterface(service) { - override fun sendBytes(p: ByteArray) {} - - fun testReadChar(c: Byte) = readChar(c) - } - - private val streamInterface = TestStreamInterface(service) - - @Test - fun `readChar delivers a 1-byte packet`() { - // Header: START1, START2, LenMSB=0, LenLSB=1 - val packet = byteArrayOf(0x94.toByte(), 0xc3.toByte(), 0x00, 0x01, 0x42) - - packet.forEach { streamInterface.testReadChar(it) } - - verify { service.handleFromRadio(byteArrayOf(0x42)) } - } - - @Test - fun `readChar handles zero length packet`() { - // Header: START1, START2, LenMSB=0, LenLSB=0 - val packet = byteArrayOf(0x94.toByte(), 0xc3.toByte(), 0x00, 0x00) - - packet.forEach { streamInterface.testReadChar(it) } - - verify { service.handleFromRadio(byteArrayOf()) } - } - - @Test - fun `readChar loses sync on invalid START2`() { - // START1, wrong START2, START1, START2, LenMSB=0, LenLSB=1, payload - val data = byteArrayOf(0x94.toByte(), 0x00, 0x94.toByte(), 0xc3.toByte(), 0x00, 0x01, 0x55) - - data.forEach { streamInterface.testReadChar(it) } - - verify { service.handleFromRadio(byteArrayOf(0x55)) } - } - - @Test - fun `readChar handles multiple packets sequentially`() { - val packet1 = byteArrayOf(0x94.toByte(), 0xc3.toByte(), 0x00, 0x01, 0x11) - val packet2 = byteArrayOf(0x94.toByte(), 0xc3.toByte(), 0x00, 0x01, 0x22) - - packet1.forEach { streamInterface.testReadChar(it) } - packet2.forEach { streamInterface.testReadChar(it) } - - verify { service.handleFromRadio(byteArrayOf(0x11)) } - verify { service.handleFromRadio(byteArrayOf(0x22)) } - verifyNoMoreCalls(service) - } - - @Test - fun `readChar handles large packet up to MAX_TO_FROM_RADIO_SIZE`() { - val size = 512 - val payload = ByteArray(size) { it.toByte() } - val header = byteArrayOf(0x94.toByte(), 0xc3.toByte(), (size shr 8).toByte(), (size and 0xff).toByte()) - - header.forEach { streamInterface.testReadChar(it) } - payload.forEach { streamInterface.testReadChar(it) } - - verify { service.handleFromRadio(payload) } - } - - @Test - fun `readChar loses sync on overly large packet length`() { - // 513 bytes is > 512 - val header = byteArrayOf(0x94.toByte(), 0xc3.toByte(), 0x02, 0x01) - - header.forEach { streamInterface.testReadChar(it) } - - // Should ignore and reset, not expecting handleFromRadio - verify(mode = VerifyMode.exactly(0)) { service.handleFromRadio(any()) } - } -} diff --git a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/MockInterface.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/MockInterface.kt index 5a5a1314a..4990ee7ab 100644 --- a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/MockInterface.kt +++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/MockInterface.kt @@ -298,7 +298,7 @@ class MockInterface(private val service: RadioInterfaceService, val address: Str private fun sendFakeAck(pr: ToRadio) = service.serviceScope.handledLaunch { val packet = pr.packet ?: return@handledLaunch delay(2000) - service.handleFromRadio(makeAck(MY_NODE + 1, packet.from ?: 0, packet.id).encode()) + service.handleFromRadio(makeAck(MY_NODE + 1, packet.from, packet.id).encode()) } private fun sendConfigResponse(configId: Int) { diff --git a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImpl.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImpl.kt index aa0da5efc..46307675a 100644 --- a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImpl.kt +++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImpl.kt @@ -123,22 +123,21 @@ class MQTTRepositoryImpl( client = newClient - clientJob = - scope.launch { - try { - Logger.i { "MQTT Starting client loop for $host:$port" } - newClient.runSuspend() - } catch (e: io.github.davidepianca98.mqtt.MQTTException) { - Logger.e(e) { "MQTT Client loop error (MQTT)" } - close(e) - } catch (e: io.github.davidepianca98.socket.IOException) { - Logger.e(e) { "MQTT Client loop error (IO)" } - close(e) - } catch (e: kotlinx.coroutines.CancellationException) { - Logger.i { "MQTT Client loop cancelled" } - throw e - } + clientJob = scope.launch { + try { + Logger.i { "MQTT Starting client loop for $host:$port" } + newClient.runSuspend() + } catch (e: io.github.davidepianca98.mqtt.MQTTException) { + Logger.e(e) { "MQTT Client loop error (MQTT)" } + close(e) + } catch (e: io.github.davidepianca98.socket.IOException) { + Logger.e(e) { "MQTT Client loop error (IO)" } + close(e) + } catch (e: kotlinx.coroutines.CancellationException) { + Logger.i { "MQTT Client loop cancelled" } + throw e } + } // Subscriptions val subscriptions = mutableListOf() diff --git a/core/network/src/jvmAndroidMain/kotlin/org/meshtastic/core/network/transport/TcpTransport.kt b/core/network/src/jvmAndroidMain/kotlin/org/meshtastic/core/network/transport/TcpTransport.kt index 973c6dbb4..e4861f0e5 100644 --- a/core/network/src/jvmAndroidMain/kotlin/org/meshtastic/core/network/transport/TcpTransport.kt +++ b/core/network/src/jvmAndroidMain/kotlin/org/meshtastic/core/network/transport/TcpTransport.kt @@ -288,14 +288,13 @@ class TcpTransport( private fun startHeartbeat(address: String) { heartbeatJob?.cancel() - heartbeatJob = - scope.launch { - while (true) { - delay(HEARTBEAT_INTERVAL_MILLIS) - Logger.d { "$logTag: [$address] Sending heartbeat" } - sendHeartbeat() - } + heartbeatJob = scope.launch { + while (true) { + delay(HEARTBEAT_INTERVAL_MILLIS) + Logger.d { "$logTag: [$address] Sending heartbeat" } + sendHeartbeat() } + } } // endregion diff --git a/core/network/src/androidUnitTest/kotlin/org/meshtastic/core/network/radio/TCPInterfaceTest.kt b/core/network/src/jvmAndroidTest/kotlin/org/meshtastic/core/network/radio/TCPInterfaceTest.kt similarity index 96% rename from core/network/src/androidUnitTest/kotlin/org/meshtastic/core/network/radio/TCPInterfaceTest.kt rename to core/network/src/jvmAndroidTest/kotlin/org/meshtastic/core/network/radio/TCPInterfaceTest.kt index 814ac1fd8..76c9cb1a8 100644 --- a/core/network/src/androidUnitTest/kotlin/org/meshtastic/core/network/radio/TCPInterfaceTest.kt +++ b/core/network/src/jvmAndroidTest/kotlin/org/meshtastic/core/network/radio/TCPInterfaceTest.kt @@ -17,11 +17,11 @@ package org.meshtastic.core.network.radio import kotlinx.coroutines.test.runTest -import org.junit.Assert.assertEquals -import org.junit.Test import org.meshtastic.core.network.transport.StreamFrameCodec import org.meshtastic.proto.Heartbeat import org.meshtastic.proto.ToRadio +import kotlin.test.Test +import kotlin.test.assertEquals class TCPInterfaceTest { diff --git a/core/prefs/src/androidUnitTest/kotlin/org/meshtastic/core/prefs/filter/FilterPrefsTest.kt b/core/prefs/src/androidHostTest/kotlin/org/meshtastic/core/prefs/filter/FilterPrefsTest.kt similarity index 84% rename from core/prefs/src/androidUnitTest/kotlin/org/meshtastic/core/prefs/filter/FilterPrefsTest.kt rename to core/prefs/src/androidHostTest/kotlin/org/meshtastic/core/prefs/filter/FilterPrefsTest.kt index 5a0661fbd..5a97ee1b1 100644 --- a/core/prefs/src/androidUnitTest/kotlin/org/meshtastic/core/prefs/filter/FilterPrefsTest.kt +++ b/core/prefs/src/androidHostTest/kotlin/org/meshtastic/core/prefs/filter/FilterPrefsTest.kt @@ -19,20 +19,18 @@ package org.meshtastic.core.prefs.filter import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.PreferenceDataStoreFactory import androidx.datastore.preferences.core.Preferences -import dev.mokkery.every -import dev.mokkery.mock import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Before import org.junit.Rule -import org.junit.Test import org.junit.rules.TemporaryFolder import org.meshtastic.core.di.CoroutineDispatchers import org.meshtastic.core.repository.FilterPrefs +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue class FilterPrefsTest { @get:Rule val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build() @@ -44,23 +42,23 @@ class FilterPrefsTest { private val testDispatcher = UnconfinedTestDispatcher() private val testScope = TestScope(testDispatcher) - @Before + @BeforeTest fun setup() { dataStore = PreferenceDataStoreFactory.create( scope = testScope, produceFile = { tmpFolder.newFile("test.preferences_pb") }, ) - dispatchers = mock() - every { dispatchers.default } returns testDispatcher + dispatchers = CoroutineDispatchers(testDispatcher, testDispatcher, testDispatcher) filterPrefs = FilterPrefsImpl(dataStore, dispatchers) } @Test fun `filterEnabled defaults to false`() = testScope.runTest { assertFalse(filterPrefs.filterEnabled.value) } @Test - fun `filterWords defaults to empty set`() = - testScope.runTest { assertTrue(filterPrefs.filterWords.value.isEmpty()) } + fun `filterWords defaults to empty set`() = testScope.runTest { + assertTrue(filterPrefs.filterWords.value.isEmpty()) + } @Test fun `setting filterEnabled updates preference`() = testScope.runTest { diff --git a/core/prefs/src/androidUnitTest/kotlin/org/meshtastic/core/prefs/notification/NotificationPrefsTest.kt b/core/prefs/src/androidHostTest/kotlin/org/meshtastic/core/prefs/notification/NotificationPrefsTest.kt similarity index 83% rename from core/prefs/src/androidUnitTest/kotlin/org/meshtastic/core/prefs/notification/NotificationPrefsTest.kt rename to core/prefs/src/androidHostTest/kotlin/org/meshtastic/core/prefs/notification/NotificationPrefsTest.kt index b5d844ce2..7f3de302f 100644 --- a/core/prefs/src/androidUnitTest/kotlin/org/meshtastic/core/prefs/notification/NotificationPrefsTest.kt +++ b/core/prefs/src/androidHostTest/kotlin/org/meshtastic/core/prefs/notification/NotificationPrefsTest.kt @@ -19,19 +19,17 @@ package org.meshtastic.core.prefs.notification import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.PreferenceDataStoreFactory import androidx.datastore.preferences.core.Preferences -import dev.mokkery.every -import dev.mokkery.mock import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Before import org.junit.Rule -import org.junit.Test import org.junit.rules.TemporaryFolder import org.meshtastic.core.di.CoroutineDispatchers import org.meshtastic.core.repository.NotificationPrefs +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue class NotificationPrefsTest { @get:Rule val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build() @@ -43,15 +41,14 @@ class NotificationPrefsTest { private val testDispatcher = UnconfinedTestDispatcher() private val testScope = TestScope(testDispatcher) - @Before + @BeforeTest fun setup() { dataStore = PreferenceDataStoreFactory.create( scope = testScope, produceFile = { tmpFolder.newFile("test.preferences_pb") }, ) - dispatchers = mock() - every { dispatchers.default } returns testDispatcher + dispatchers = CoroutineDispatchers(testDispatcher, testDispatcher, testDispatcher) notificationPrefs = NotificationPrefsImpl(dataStore, dispatchers) } @@ -59,12 +56,14 @@ class NotificationPrefsTest { fun `messagesEnabled defaults to true`() = testScope.runTest { assertTrue(notificationPrefs.messagesEnabled.value) } @Test - fun `nodeEventsEnabled defaults to true`() = - testScope.runTest { assertTrue(notificationPrefs.nodeEventsEnabled.value) } + fun `nodeEventsEnabled defaults to true`() = testScope.runTest { + assertTrue(notificationPrefs.nodeEventsEnabled.value) + } @Test - fun `lowBatteryEnabled defaults to true`() = - testScope.runTest { assertTrue(notificationPrefs.lowBatteryEnabled.value) } + fun `lowBatteryEnabled defaults to true`() = testScope.runTest { + assertTrue(notificationPrefs.lowBatteryEnabled.value) + } @Test fun `setting messagesEnabled updates preference`() = testScope.runTest { diff --git a/core/service/src/commonMain/kotlin/org/meshtastic/core/service/SharedRadioInterfaceService.kt b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/SharedRadioInterfaceService.kt index b5a4617a9..8a5e23787 100644 --- a/core/service/src/commonMain/kotlin/org/meshtastic/core/service/SharedRadioInterfaceService.kt +++ b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/SharedRadioInterfaceService.kt @@ -227,13 +227,12 @@ class SharedRadioInterfaceService( private fun startHeartbeat() { heartbeatJob?.cancel() - heartbeatJob = - serviceScope.launch { - while (true) { - delay(HEARTBEAT_INTERVAL_MILLIS) - keepAlive() - } + heartbeatJob = serviceScope.launch { + while (true) { + delay(HEARTBEAT_INTERVAL_MILLIS) + keepAlive() } + } } fun keepAlive(now: Long = nowMillis) { diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/AlertDialogs.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/AlertDialogs.kt index 019afe557..085adb10e 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/AlertDialogs.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/AlertDialogs.kt @@ -90,21 +90,20 @@ fun MeshtasticDialog( val confirmButtonText = confirmText ?: confirmTextRes?.let { stringResource(it) } val dismissButtonText = dismissText ?: dismissTextRes?.let { stringResource(it) } - val htmlAnnotated = - html?.let { - annotatedStringFromHtml( - it, - linkStyles = - TextLinkStyles( - style = - SpanStyle( - textDecoration = TextDecoration.Underline, - fontStyle = FontStyle.Italic, - color = MaterialTheme.colorScheme.primary, - ), + val htmlAnnotated = html?.let { + annotatedStringFromHtml( + it, + linkStyles = + TextLinkStyles( + style = + SpanStyle( + textDecoration = TextDecoration.Underline, + fontStyle = FontStyle.Italic, + color = MaterialTheme.colorScheme.primary, ), - ) - } + ), + ) + } AlertDialog( onDismissRequest = { if (dismissable) onDismiss?.invoke() }, diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/DropDownPreference.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/DropDownPreference.kt index 22c6bfaf5..9d41d5f5a 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/DropDownPreference.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/DropDownPreference.kt @@ -62,13 +62,12 @@ fun > DropDownPreference( enumEntriesOf(selectedItem).filter { it.name != "UNRECOGNIZED" && !it.isDeprecatedEnumEntry() } } - val items = - enumConstants.map { - val label = itemLabel?.invoke(it) ?: it.name - val icon = itemIcon?.invoke(it) - val color = itemColor?.invoke(it) - DropDownItem(it, label, icon, color) - } + val items = enumConstants.map { + val label = itemLabel?.invoke(it) ?: it.name + val icon = itemIcon?.invoke(it) + val color = itemColor?.invoke(it) + DropDownItem(it, label, icon, color) + } DropDownPreference( title = title, diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/share/SharedContactViewModel.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/share/SharedContactViewModel.kt index 345c5b8ed..9f96b00d3 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/share/SharedContactViewModel.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/share/SharedContactViewModel.kt @@ -35,6 +35,7 @@ class SharedContactViewModel(nodeRepository: NodeRepository, private val service val unfilteredNodes: StateFlow> = nodeRepository.getNodes().stateInWhileSubscribed(initialValue = emptyList()) - fun addSharedContact(sharedContact: SharedContact) = - viewModelScope.launch { serviceRepository.onServiceAction(ServiceAction.ImportContact(sharedContact)) } + fun addSharedContact(sharedContact: SharedContact) = viewModelScope.launch { + serviceRepository.onServiceAction(ServiceAction.ImportContact(sharedContact)) + } } diff --git a/docs/BUILD_LOGIC_CONVENTIONS_GUIDE.md b/docs/BUILD_LOGIC_CONVENTIONS_GUIDE.md index ddaa8732b..681e2f04d 100644 --- a/docs/BUILD_LOGIC_CONVENTIONS_GUIDE.md +++ b/docs/BUILD_LOGIC_CONVENTIONS_GUIDE.md @@ -287,7 +287,7 @@ tasks.withType().configureEach { - `AGENTS.md` - Development guidelines (Section 3.B testing, Section 4.A build protocol) - `docs/BUILD_LOGIC_INDEX.md` - Current build-logic doc entry point (with links to active references) -- `docs/archive/BUILD_LOGIC_OPTIMIZATIONS_COMPLETE.md` - Historical optimization deep-dive + - `build-logic/convention/build.gradle.kts` - Convention plugin build config - `.github/copilot-instructions.md` - Build & test commands diff --git a/docs/archive/BUILD_LOGIC_OPTIMIZATIONS_COMPLETE.md b/docs/archive/BUILD_LOGIC_OPTIMIZATIONS_COMPLETE.md deleted file mode 100644 index 769119dea..000000000 --- a/docs/archive/BUILD_LOGIC_OPTIMIZATIONS_COMPLETE.md +++ /dev/null @@ -1,233 +0,0 @@ -# Build-Logic Optimizations Summary - -## Overview -During review of the `build-logic/` convention plugins, we identified and addressed several optimization opportunities while maintaining backward compatibility and clarity. - -## Changes Implemented - -### 1. **Test Dependencies Convention** ✅ COMPLETED -**Status:** DEPLOYED AND TESTED - -**What:** Centralized `kotlin("test")` dependency configuration for all KMP modules. - -**How:** Created `configureKmpTestDependencies()` function in `KotlinAndroid.kt` and integrated it into `KmpLibraryConventionPlugin`. - -**Impact:** -- Removed duplicate `implementation(kotlin("test"))` from 7 feature modules -- Single source of truth for test framework configuration -- All new KMP modules automatically get correct test dependencies -- Build files cleaner (7 build.gradle.kts files simplified) - -**Files Modified:** -- `build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/KotlinAndroid.kt` - Added `configureKmpTestDependencies()` -- `build-logic/convention/src/main/kotlin/KmpLibraryConventionPlugin.kt` - Integrated test dependency function -- `feature/{messaging,firmware,intro,map,node,settings,connections}/build.gradle.kts` - Removed redundant dependencies -- `AGENTS.md` - Updated testing documentation - ---- - -### 2. **Compose Plugin Documentation** ✅ COMPLETED -**Status:** ANALYZED AND DOCUMENTED - -**What:** Identified that `AndroidApplicationComposeConventionPlugin` and `AndroidLibraryComposeConventionPlugin` are identical. - -**Analysis:** -- Both apply the same plugins (`compose-compiler`, `compose-multiplatform`) -- Both call identical `configureAndroidCompose()` function -- Differ only in extension type (ApplicationExtension vs LibraryExtension) - -**Decision:** Keep separate with documentation -- **Reason 1:** Explicit intent in `build.gradle.kts` (clarity wins over DRY) -- **Reason 2:** Low cost of duplication (~20 lines per file) -- **Reason 3:** Potential future divergence between app/library compose config -- **Future Path:** Can be consolidated using `CommonExtension` when benefits outweigh clarity costs - -**Files Modified:** -- `AndroidApplicationComposeConventionPlugin.kt` - Added optimization documentation -- `AndroidLibraryComposeConventionPlugin.kt` - Added optimization documentation - ---- - -### 3. **Flavor Configuration Documentation** ✅ COMPLETED -**Status:** ANALYZED AND DOCUMENTED - -**What:** Identified that `AndroidApplicationFlavorsConventionPlugin` and `AndroidLibraryFlavorsConventionPlugin` are nearly identical. - -**Analysis:** -- Both only configure flavor dimensions using `configureFlavors()` function -- Underlying `configureFlavors()` function already handles both `ApplicationExtension` and `LibraryExtension` via pattern matching -- Could technically be consolidated using `CommonExtension` - -**Decision:** Keep separate with documentation -- **Reason 1:** Explicit intent in `build.gradle.kts` (clarity wins over DRY) -- **Reason 2:** Low cost of duplication (~30 lines per file) -- **Reason 3:** Gradle/AGP conventions expect specific extension types -- **Future Path:** Can consolidate if flavor config diverges from application/library handling - -**Files Modified:** -- `AndroidApplicationFlavorsConventionPlugin.kt` - Added consolidation opportunity note -- `AndroidLibraryFlavorsConventionPlugin.kt` - Added consolidation opportunity note - ---- - -### 4. **KotlinAndroid.kt Cleanup** ✅ COMPLETED -**Status:** IMPROVED IMPORT ORGANIZATION - -**What:** Added missing import for `RepositoryHandler` (identified during optimization review) - -**Impact:** Minor - improves import clarity for future use - -**Files Modified:** -- `KotlinAndroid.kt` - Added unused import for future extensibility - ---- - -### 5. **`jvmAndroidMain` Hierarchy Convention** ✅ COMPLETED -**Status:** DEPLOYED AND TESTED - -**What:** Replaced manual `jvmAndroidMain` source-set wiring in core KMP modules with an opt-in convention plugin backed by Kotlin's hierarchy template API. - -**Analysis:** -- `core:common`, `core:model`, `core:network`, and `core:ui` all used identical hand-written `dependsOn(...)` graphs -- Kotlin emitted `Default Kotlin Hierarchy Template Not Applied Correctly` for those modules -- The shared pattern was real and intentional, not module-specific behavior - -**Implementation:** -- Added `configureJvmAndroidMainHierarchy()` to `KotlinAndroid.kt` -- Added `KmpJvmAndroidConventionPlugin` with id `meshtastic.kmp.jvm.android` -- Migrated the four affected core modules to the plugin - -**Files Modified:** -- `build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/KotlinAndroid.kt` -- `build-logic/convention/src/main/kotlin/KmpJvmAndroidConventionPlugin.kt` -- `build-logic/convention/build.gradle.kts` -- `core/common/build.gradle.kts` -- `core/model/build.gradle.kts` -- `core/network/build.gradle.kts` -- `core/ui/build.gradle.kts` -- `AGENTS.md` -- `docs/kmp-status.md` -- `docs/BUILD_LOGIC_CONVENTIONS_GUIDE.md` -- `docs/BUILD_LOGIC_INDEX.md` - ---- - -## Build-Logic Plugin Inventory - -| Plugin | Type | Duplication | Status | -|--------|------|-------------|--------| -| `KmpLibraryConventionPlugin` | Base KMP | None | ✅ Optimized (test deps added) | -| `KmpJvmAndroidConventionPlugin` | KMP hierarchy | None | ✅ New opt-in convention | -| `AndroidApplicationConventionPlugin` | Base Android | Common baseline | ⚠️ Documented | -| `AndroidLibraryConventionPlugin` | Base Android | Common baseline | ⚠️ Documented | -| `AndroidApplicationComposeConventionPlugin` | Compose | **Identical** to Library | ✅ Documented | -| `AndroidLibraryComposeConventionPlugin` | Compose | **Identical** to App | ✅ Documented | -| `AndroidApplicationFlavorsConventionPlugin` | Flavors | **Nearly identical** to Library | ✅ Documented | -| `AndroidLibraryFlavorsConventionPlugin` | Flavors | **Nearly identical** to App | ✅ Documented | -| `KoinConventionPlugin` | DI | No duplication | ✅ Good | -| `DetektConventionPlugin` | Lint | No duplication | ✅ Good | -| `SpotlessConventionPlugin` | Format | No duplication | ✅ Good | -| Others | Various | Low/None | ✅ Good | - ---- - -## Future Optimization Opportunities - -### A. **Common Android Baseline Function** (MEDIUM EFFORT) -**Current Status:** DOCUMENTED ONLY - -Both `AndroidApplicationConventionPlugin` and `AndroidLibraryConventionPlugin` share common patterns: -- Same plugin applications (lint, detekt, spotless, dokka, kover, test-retry) -- Both call `configureKotlinAndroid()` and `configureTestOptions()` -- Both configure test instrumentation runner - -**Potential Optimization:** -```kotlin -internal fun Project.configureAndroidBaseConvention( - extension: CommonExtension -) { - // Shared setup - extension.apply { - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - testOptions.animationsDisabled = true - } -} -``` - -**Effort:** ~2 hours (extract logic, verify no regressions, add tests) -**Savings:** ~50 lines of code -**Risk:** Low (consolidating already-tested patterns) - -### B. **Unified Flavor/Compose Convention** (LOW PRIORITY) -When Application and Library compose/flavor handling diverges, could create specialized variants. -Not recommended now—cost of duplication << cost of wrong abstraction. - -### C. **Plugin Validation Test Suite** (MEDIUM EFFORT) -Add unit tests to `build-logic` verifying: -- Convention plugins apply correct defaults -- Test dependencies are properly configured -- Flavor configuration is consistent across app/library - -**Benefit:** Prevent future regressions - ---- - -## Performance Impact - -### Build Time -- No change (optimizations are configuration-time only) -- Test dependencies now resolve faster (centralized, no duplication) -- `jvmAndroidMain` configuration now uses a single convention instead of repeated manual source-set graphs - -### Code Size -- **Before:** 155+ lines of near-duplicate code -- **After:** Optimized, documented duplication (intentional for clarity) - -### Maintainability -- **Before:** Changes to test config required updates in 7+ places -- **After:** Single source of truth for test framework setup -- **Future:** Documented consolidation paths for other duplications - ---- - -## Testing & Verification - -✅ All tests pass: -```bash -./gradlew spotlessCheck detekt # BUILD SUCCESSFUL -./gradlew :core:model:compileAndroidMain :core:common:compileAndroidMain :core:network:compileAndroidMain :core:ui:compileAndroidMain # BUILD SUCCESSFUL -./gradlew test # BUILD SUCCESSFUL -./gradlew :feature:node:testAndroidHostTest :feature:settings:testAndroidHostTest # BUILD SUCCESSFUL -./gradlew :feature:messaging:jvmTest :feature:node:jvmTest # BUILD SUCCESSFUL -./gradlew assembleDebug test # BUILD SUCCESSFUL -``` - ---- - -## Recommendations - -### Immediate Actions -1. ✅ Done: Test dependency centralization (DEPLOYED) -2. ✅ Done: Document Compose duplication (DOCUMENTED) -3. ✅ Done: Document Flavor duplication (DOCUMENTED) -4. ✅ Done: Standardize `jvmAndroidMain` hierarchy setup (DEPLOYED) - -### Short-Term (Next Sprint) -- Monitor if Application/Library Compose handling needs to diverge -- Monitor if Flavor configuration needs specialization -- Review `configureTestOptions()` to ensure all test config is centralized - -### Long-Term (Future) -- If `AndroidApplicationConventionPlugin` and `AndroidLibraryConventionPlugin` patterns stabilize, consider extracting common baseline -- Implement plugin validation tests to prevent future regressions -- Create agent playbook for "build-logic optimization" with clear criteria - ---- - -## Related Documentation - -- `docs/BUILD_CONVENTION_TEST_DEPS.md` - Details on test dependency centralization -- `docs/archive/BUILD_LOGIC_OPTIMIZATION_ANALYSIS.md` - Full analysis of optimization opportunities -- `AGENTS.md` - Updated testing + KMP hierarchy guidelines (Section 3.B) - - diff --git a/docs/archive/BUILD_LOGIC_OPTIMIZATION_ANALYSIS.md b/docs/archive/BUILD_LOGIC_OPTIMIZATION_ANALYSIS.md deleted file mode 100644 index 8094181aa..000000000 --- a/docs/archive/BUILD_LOGIC_OPTIMIZATION_ANALYSIS.md +++ /dev/null @@ -1,80 +0,0 @@ -# Build-Logic Optimization Analysis - -## Identified Issues & Solutions - -### 1. **Identical Compose Plugins** (HIGH PRIORITY) -**Problem:** `AndroidApplicationComposeConventionPlugin` and `AndroidLibraryComposeConventionPlugin` are identical. - -**Current State:** -- Both apply the same plugins and call `configureAndroidCompose()` -- Only difference in name, which suggests copy-paste - -**Solution:** Create a shared `BaseAndroidComposeConventionPlugin` or consolidate logic into `KmpLibraryComposeConventionPlugin` - ---- - -### 2. **Duplicated Flavor Configuration** (MEDIUM PRIORITY) -**Problem:** `AndroidApplicationFlavorsConventionPlugin` and `AndroidLibraryFlavorsConventionPlugin` are nearly identical. - -**Current State:** -```kotlin -// ApplicationFlavors -extensions.configure { configureFlavors(this) } - -// LibraryFlavors -extensions.configure { configureFlavors(this) } -``` - -**Solution:** Both `ApplicationExtension` and `LibraryExtension` are subtypes of `CommonExtension`. Create a base function that works with `CommonExtension`. - ---- - -### 3. **Duplicate Common Android Configuration** (MEDIUM PRIORITY) -**Problem:** Both `AndroidApplicationConventionPlugin` and `AndroidLibraryConventionPlugin` repeat: -- Common plugin applications (lint, detekt, spotless, dokka, kover, test-retry) -- `configureKotlinAndroid()` call -- `configureTestOptions()` call -- Test instrumentation runner setup - -**Current State:** -```kotlin -// Both plugins apply identical plugin lists and call same config functions -apply(plugin = "meshtastic.android.lint") -apply(plugin = "meshtastic.detekt") -apply(plugin = "meshtastic.spotless") -apply(plugin = "meshtastic.dokka") -apply(plugin = "meshtastic.kover") -apply(plugin = "org.gradle.test-retry") -configureKotlinAndroid(this) -configureTestOptions() -``` - -**Solution:** Extract common Android baseline configuration to a shared function. - ---- - -### 4. **Missing Test Configuration Consolidation** (LOW PRIORITY) -**Problem:** Test-related configuration is scattered: -- `AndroidLibraryConventionPlugin`: `testOptions.animationsDisabled = true` -- `AndroidApplicationConventionPlugin`: Same -- Test instrumentation runner set in multiple places -- `configureTestOptions()` called in both, but plugin structure doesn't guarantee execution order - -**Solution:** Centralize all test configuration in `configureTestOptions()` function. - ---- - -## Implementation Priority - -1. **HIGH:** Consolidate duplicate Compose plugins (saves ~75 lines) -2. **MEDIUM:** Consolidate Flavor plugins (saves ~30 lines) -3. **MEDIUM:** Extract shared Android base config (saves ~50 lines) -4. **LOW:** Verify test configuration centralization (audit `configureTestOptions()`) - -## Impact - -- **Total lines of code reduced:** ~155 lines -- **Maintainability:** ↑↑ (single source of truth) -- **Risk of inconsistency:** ↓↓ (less duplication) -- **Future changes:** Easier (one place to update) - diff --git a/docs/archive/BUILD_LOGIC_OPTIMIZATION_SUMMARY.md b/docs/archive/BUILD_LOGIC_OPTIMIZATION_SUMMARY.md deleted file mode 100644 index deaabf95a..000000000 --- a/docs/archive/BUILD_LOGIC_OPTIMIZATION_SUMMARY.md +++ /dev/null @@ -1,285 +0,0 @@ -# Build-Logic Optimization Complete ✅ - -**Date:** March 12, 2026 -**Status:** DEPLOYED AND VERIFIED - -## Executive Summary - -Completed comprehensive review and optimization of `build-logic/` convention plugins. Implemented high-impact centralization of test dependencies, added a reusable `jvmAndroidMain` hierarchy convention for Android + desktop JVM shared code, and documented other optimization opportunities. All changes tested and verified. - ---- - -## Completed Optimizations - -### 1. Test Dependency Centralization ✅ DEPLOYED - -**What:** Consolidated `kotlin("test")` configuration across all KMP modules - -**Implementation:** -- Created `configureKmpTestDependencies()` function in `KotlinAndroid.kt` -- Integrated into `KmpLibraryConventionPlugin` -- Removed manual dependencies from 7 feature modules - -**Impact:** -``` -BEFORE: -- 7+ build.gradle.kts files with duplicate kotlin("test") -- Risk of missing dependencies in new modules -- Inconsistent configuration patterns - -AFTER: -- Single source of truth in build-logic -- All 15+ KMP modules automatically benefit -- Clear, maintainable pattern for future test frameworks -``` - -**Files Changed:** 9 files modified -- `build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/KotlinAndroid.kt` -- `build-logic/convention/src/main/kotlin/KmpLibraryConventionPlugin.kt` -- 7 feature module `build.gradle.kts` files (simplified) -- `AGENTS.md` (documentation updated) - -**Verification:** -```bash -✅ ./gradlew spotlessCheck detekt # BUILD SUCCESSFUL -✅ ./gradlew test # BUILD SUCCESSFUL (516 tasks) -✅ ./gradlew assembleDebug # BUILD SUCCESSFUL -``` - ---- - -### 2. Duplication Analysis & Documentation ✅ COMPLETED - -**Identified Duplications:** - -| Duplication | Plugin Pair | Lines | Status | -|-------------|------------|-------|--------| -| **Identical** | `AndroidApplicationComposeConventionPlugin` ↔ `AndroidLibraryComposeConventionPlugin` | ~40 | 📝 Documented | -| **Nearly Identical** | `AndroidApplicationFlavorsConventionPlugin` ↔ `AndroidLibraryFlavorsConventionPlugin` | ~30 | 📝 Documented | -| **Consolidation Opportunity** | `AndroidApplicationConventionPlugin` ↔ `AndroidLibraryConventionPlugin` | ~50 | 📋 Planned | - -**Decision:** Keep Compose & Flavor plugins separate (for now) -- **Reason:** Different extension types + explicit intent matters -- **Cost:** ~70 lines of intentional duplication -- **Benefit:** Clear plugin purpose in `build.gradle.kts` -- **Future:** Can consolidate when benefits outweigh clarity costs - -**Documentation Added:** -- Both Compose plugins: Explicit note explaining identical implementation -- Both Flavor plugins: Note about consolidation opportunity using `CommonExtension` -- Future optimization path clearly marked - ---- - -### 3. `jvmAndroidMain` Hierarchy Convention ✅ DEPLOYED - -**What:** Standardized shared JVM+Android source-set wiring for KMP modules that need `src/jvmAndroidMain`. - -**Implementation:** -- Added `configureJvmAndroidMainHierarchy()` in `KotlinAndroid.kt` -- Added opt-in `meshtastic.kmp.jvm.android` convention plugin (`KmpJvmAndroidConventionPlugin`) -- Migrated `core:common`, `core:model`, `core:network`, and `core:ui` off manual `dependsOn(...)` edges - -**Impact:** -``` -BEFORE: -- 4 modules manually created jvmAndroidMain -- Kotlin emitted "Default Kotlin Hierarchy Template Not Applied Correctly" -- Source-set wiring lived in each module build.gradle.kts - -AFTER: -- 1 opt-in convention plugin for shared JVM+Android code -- No manual hierarchy edges in affected modules -- The original hierarchy-template warning is removed for those modules -``` - -**Files Changed:** -- `build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/KotlinAndroid.kt` -- `build-logic/convention/src/main/kotlin/KmpJvmAndroidConventionPlugin.kt` -- `build-logic/convention/build.gradle.kts` -- `core/{common,model,network,ui}/build.gradle.kts` -- `AGENTS.md`, `docs/kmp-status.md`, `docs/BUILD_LOGIC_CONVENTIONS_GUIDE.md`, `docs/BUILD_LOGIC_INDEX.md` - ---- - -## Documentation Created - -### 1. `docs/BUILD_CONVENTION_TEST_DEPS.md` -- Details on test dependency centralization -- Summary of changes and impact -- Benefits for module developers - -### 2. `docs/archive/BUILD_LOGIC_OPTIMIZATION_ANALYSIS.md` -- Complete analysis of 4 optimization opportunities -- High/Medium/Low priority classification -- Implementation cost/benefit analysis -- Future recommendations - -### 3. `docs/archive/BUILD_LOGIC_OPTIMIZATIONS_COMPLETE.md` ⭐ PRIMARY REFERENCE -- Full summary of all optimizations -- Build-logic plugin inventory with duplication status -- Future opportunities with effort estimates -- Testing & verification procedures -- Performance impact analysis - -### 4. `docs/BUILD_LOGIC_CONVENTIONS_GUIDE.md` ⭐ DEVELOPER GUIDE -- Quick reference for maintaining build-logic -- Core principles and best practices -- How to add new conventions (with examples) -- Duplication heuristics (when to consolidate vs keep separate) -- Common pitfalls and solutions -- Testing requirements for changes - ---- - -## Testing & Verification - -### Build Quality Checks ✅ -```bash -✅ Code Formatting: ./gradlew spotlessCheck detekt -✅ Full Assembly: ./gradlew clean assembleDebug assembleRelease -✅ Unit Tests: ./gradlew test (516 tasks, all passing) -✅ Feature Tests: ./gradlew :feature:messaging:jvmTest -✅ Android Host Tests: ./gradlew :feature:node:testAndroidHostTest -``` - -### Test Coverage -- All feature modules compile with new test dependency convention -- All `jvmAndroidMain` core modules compile with the new hierarchy convention -- Both JVM and Android host test targets verified -- Gradle configuration cache works correctly -- No regressions in existing functionality - ---- - -## Architecture Improvements - -### Test Dependency Pattern (NEW) - -**Problem Solved:** Scattered test framework configuration -``` -BEFORE: 7 places to add test dependencies - feature/messaging/build.gradle.kts - feature/node/build.gradle.kts - feature/settings/build.gradle.kts - ... (4 more) - -AFTER: 1 place for all KMP modules - build-logic/convention/src/main/kotlin/ - org/meshtastic/buildlogic/KotlinAndroid.kt -``` - -### Benefits -1. **DRY Principle:** Single source of truth -2. **Scalability:** New modules automatically get correct config -3. **Maintainability:** One place to add new test frameworks -4. **Clarity:** Explicit intent preserved in build.gradle.kts - -### Shared `jvmAndroidMain` Pattern (NEW) - -**Problem Solved:** Hand-wired shared JVM/Android source-set graphs -``` -BEFORE: manual dependsOn(...) in 4 modules - core/common/build.gradle.kts - core/model/build.gradle.kts - core/network/build.gradle.kts - core/ui/build.gradle.kts - -AFTER: 1 opt-in convention plugin - id("meshtastic.kmp.jvm.android") -``` - -### Benefits -1. **Supported API:** Uses Kotlin hierarchy templates instead of manual `dependsOn(...)` -2. **Signal Reduction:** Removes the default hierarchy template warning in affected modules -3. **Consistency:** One pattern for future Android + desktop JVM shared code -4. **Smaller build files:** Modules only declare target-specific dependencies - ---- - -## Recommendations - -### Immediate ✅ -- [x] Deploy test dependency centralization -- [x] Document Compose duplication -- [x] Document Flavor duplication - -### Short-Term (Next Sprint) -- [ ] Implement plugin validation test suite -- [ ] Review `configureTestOptions()` for other centralization opportunities -- [ ] Consider `RootConventionPlugin` audit for similar patterns - -### Long-Term (Future Roadmap) -- [ ] If AndroidApplication/Library diverge significantly, extract common baseline (~2 hours effort) -- [ ] If Compose or Flavor handling becomes complex, revisit consolidation decision -- [ ] Build agent playbook for "build-logic analysis & optimization" - ---- - -## Key Learnings - -### ✅ What Worked Well -1. **Clear duplication analysis:** Identified exactly which plugins were identical -2. **Principled decisions:** "Clarity wins over DRY" is a valid architectural choice -3. **Documentation focus:** Marked consolidation opportunities for future maintainers -4. **Verified thoroughly:** All changes tested before deployment - -### ⚠️ What Could Improve -1. Earlier discovery: Could have added test dependency convention at module creation time -2. Plugin testing: Consider adding Gradle plugin tests to `build-logic` -3. Consolidation threshold: Define when duplication justifies consolidation vs clarity - -### 📚 Best Practices Established -1. Convention plugins document their duplication status -2. Consolidation opportunities are marked for future work -3. Test dependencies centralized by module type (KMP, Android, etc.) -4. All changes validated with spotless + detekt + tests - ---- - -## Files Summary - -| File | Purpose | Status | -|------|---------|--------| -| `KotlinAndroid.kt` | New test dependency function | ✅ Deployed | -| `KmpLibraryConventionPlugin.kt` | Integrated test config | ✅ Deployed | -| `KmpJvmAndroidConventionPlugin.kt` | Opt-in jvmAndroid hierarchy config | ✅ Deployed | -| `AndroidApplicationComposeConventionPlugin.kt` | Documented duplication | ✅ Documented | -| `AndroidLibraryComposeConventionPlugin.kt` | Documented duplication | ✅ Documented | -| `AndroidApplicationFlavorsConventionPlugin.kt` | Documented opportunity | ✅ Documented | -| `AndroidLibraryFlavorsConventionPlugin.kt` | Documented opportunity | ✅ Documented | -| `feature/*/build.gradle.kts` (7 files) | Simplified dependencies | ✅ Deployed | -| `core/{common,model,network,ui}/build.gradle.kts` | Switched to jvmAndroid convention | ✅ Deployed | -| `AGENTS.md` | Updated testing section | ✅ Updated | -| `BUILD_LOGIC_CONVENTIONS_GUIDE.md` | Developer guide | ✅ Created | -| `BUILD_LOGIC_OPTIMIZATIONS_COMPLETE.md` | Complete analysis | ✅ Created | -| `BUILD_LOGIC_OPTIMIZATION_ANALYSIS.md` | Detailed analysis | ✅ Created | -| `BUILD_CONVENTION_TEST_DEPS.md` | Test deps summary | ✅ Created | - ---- - -## Maintenance Going Forward - -### For Developers -- Use `docs/BUILD_LOGIC_CONVENTIONS_GUIDE.md` when modifying build-logic -- Follow test dependency patterns when creating new KMP modules -- Reference `docs/archive/BUILD_LOGIC_OPTIMIZATIONS_COMPLETE.md` for consolidation opportunities - -### For Code Reviewers -- Watch for duplicate convention plugins (can consolidate if appropriate) -- Ensure test dependencies use convention pattern (not hardcoded in modules) -- Check that new conventions are documented - -### For Maintainers -- Review consolidation opportunities yearly (cost/benefit changes over time) -- Monitor if Application/Library handling diverges (may justify separate plugins) -- Expand test dependency convention if new frameworks are adopted - ---- - -## Conclusion - -Successfully optimized build-logic with **zero breaking changes** while establishing patterns for future improvements. Test dependency centralization deployed and verified across all modules. Documentation provides clear path for future consolidations when appropriate. - -**Status: READY FOR PRODUCTION** ✅ - diff --git a/docs/archive/README.md b/docs/archive/README.md deleted file mode 100644 index e44ed3ad8..000000000 --- a/docs/archive/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# Archive - -Historical and completed planning documents. Kept for git history and reference. - -For current state, see [`docs/kmp-status.md`](../kmp-status.md). -For the forward-looking roadmap, see [`docs/roadmap.md`](../roadmap.md). -For decision records, see [`docs/decisions/`](../decisions/). - -## Contents - -| Document | Original Purpose | Status | -|---|---|---| -| `kmp-progress-review-2026.md` | Evidence-backed KMP re-baseline (729 lines) | Superseded by `kmp-status.md` | -| `kmp-progress-review-evidence.md` | Raw evidence appendix | Superseded by `kmp-status.md` | -| `kmp-migration.md` | Historical migration narrative | Superseded by `kmp-status.md` | -| `desktop-and-multi-target-roadmap.md` | Desktop roadmap + 41-item execution log | Superseded by `roadmap.md` | -| `kmp-adaptive-compose-evaluation.md` | JetBrains Material 3 Adaptive evaluation | All phases complete | -| `kmp-app-migration-assessment.md` | Expect/actual consolidation + app module assessment | All work complete | -| `ble-kmp-abstraction-plan.md` | BLE KMP abstraction execution plan | Complete | -| `ble-kmp-strategy.md` | BLE library comparison (Nordic vs KABLE) | Decision made; see `decisions/ble-strategy.md` | -| `koin-migration-plan.md` | Hilt → Koin step-by-step plan | Complete; see `decisions/koin-migration.md` | - diff --git a/docs/archive/ble-kmp-abstraction-plan.md b/docs/archive/ble-kmp-abstraction-plan.md deleted file mode 100644 index 8e7f9f01e..000000000 --- a/docs/archive/ble-kmp-abstraction-plan.md +++ /dev/null @@ -1,34 +0,0 @@ -# Phase 8: `core:ble` KMP Abstraction - -## Objective -Migrate `core:ble` from an Android-only library (`meshtastic.android.library`) to a Kotlin Multiplatform library (`meshtastic.kmp.library`). The goal is to provide a unified, platform-agnostic Bluetooth Low Energy (BLE) interface for the rest of the application (e.g., `core:domain`, `core:data`), while explicitly supporting future Desktop and Web targets. - -## Strategy: The "Nordic Hybrid" Abstraction -We will use an Interface-Driven (Dependency Injection) approach rather than relying directly on Nordic's KMM library in `commonMain` or using raw `expect`/`actual` for the entire BLE stack. - -Nordic's [KMM-BLE-Library](https://github.com/NordicSemiconductor/Kotlin-BLE-Library) provides excellent, battle-tested Coroutine/Flow APIs for Android and iOS. However, it **does not support Desktop (JVM/Windows/Linux/macOS) or Web (Wasm/JS)**. If we expose Nordic's classes directly in `commonMain`, the project will fail to compile for Desktop/Web targets. - -To resolve this, we will build a custom abstraction layer: - -### 1. The Common Interfaces (`commonMain`) -Define pure Kotlin interfaces and data classes representing BLE operations. The rest of the app will only know about these interfaces. -* `BleScanner`: For discovering devices. -* `BleDevice`: Represents a remote peripheral. -* `BleConnectionManager`: Handles connect/disconnect, MTU negotiation, and characteristic read/write/subscribe operations. -* *Note: No Nordic dependencies will exist in `commonMain`.* - -### 2. The Android & iOS Implementations (`androidMain` & `iosMain`) -These source sets will depend on the Nordic `KMM-BLE-Library`. We will write concrete implementations of our common interfaces (e.g., `NordicBleConnectionManager`) that delegate operations to Nordic's `CentralManager` and `Peripheral` classes. - -### 3. The Future Implementations (`desktopMain` / `webMain`) -By keeping `commonMain` free of Nordic dependencies, we reserve the ability to implement our BLE interfaces using other libraries (like [Kable](https://github.com/JuulLabs/kable) or Web Bluetooth APIs) on unsupported platforms without rewriting the core application logic. - -## Execution Plan -1. ✅ **Refactor Build Script:** Convert `core/ble/build.gradle.kts` to use the KMP plugin and define `commonMain` and `androidMain` source sets. Move Nordic dependencies to `androidMain`. -2. ✅ **Define Abstractions:** Create pure Kotlin interfaces (`BleScanner`, `BleConnection`, etc.) in `commonMain`. -3. ✅ **Implement Wrappers:** Move the existing Android-specific Nordic implementation into `androidMain` and adapt it to implement the new `commonMain` interfaces. -4. ✅ **Update DI:** Adjust the Hilt/DI modules in `app` or `androidMain` to bind the Android-specific Nordic wrappers to the common interfaces. -5. ✅ **Verify:** Ensure the Android app builds and tests pass, confirming the abstraction works correctly. - -## Status: Completed -This phase was successfully executed. The Nordic SDK is now fully wrapped by common KMP interfaces (`BleDevice`, `BleScanner`, etc.). The DI modules have been relocated to the `app` module to accommodate Hilt limitations with KMP projects. All tests and integrations have been updated to use the new abstracted interfaces. \ No newline at end of file diff --git a/docs/archive/ble-kmp-strategy.md b/docs/archive/ble-kmp-strategy.md deleted file mode 100644 index 4cccf60f4..000000000 --- a/docs/archive/ble-kmp-strategy.md +++ /dev/null @@ -1,111 +0,0 @@ -# `core:ble` KMP Strategy Analysis - -> Date: 2026-03-10 -> -> Context: Nordic responded to [our inquiry](https://github.com/NordicSemiconductor/Kotlin-BLE-Library/issues/183#issuecomment-4030710057) confirming KMP is on their roadmap but not yet available, and recommended KABLE for projects needing KMP now. - -## Current State — Already Well-Architected - -Our `core:ble` is **already one of the best-structured modules in the repo** for KMP: - -| Layer | What exists | KMP-ready? | -|---|---|---| -| `commonMain` interfaces | `BleConnection`, `BleScanner`, `BleDevice`, `BleConnectionFactory`, `BluetoothRepository`, `BleConnectionState`, `BleService`, `BleRetry`, `MeshtasticBleConstants` | ✅ Pure Kotlin — zero platform imports | -| `androidMain` implementations | `AndroidBleConnection`, `AndroidBleScanner`, `AndroidBleDevice`, `AndroidBleConnectionFactory`, `AndroidBluetoothRepository`, `AndroidBleService` | ✅ Properly isolated | -| DI | `CoreBleModule` (commonMain), `CoreBleAndroidModule` (androidMain) | ✅ Clean split | - -**The abstraction boundary is already drawn exactly where it needs to be.** No Nordic types leak into `commonMain`. - -## The JVM Target Question - -Adding `jvm()` to `core:ble` is **easy right now** — the `commonMain` has zero platform dependencies. The only blocker would be providing `jvmMain` implementations of the BLE interfaces, but for JVM (headless/desktop) we have two options: - -### Option A: No-op / Stub JVM Implementation (Minimal, Unblocks CI Now) - -Add `jvm()` and provide no-op or stub implementations in `jvmMain` (or don't — `commonMain` is just interfaces, it'll compile fine with no `jvmMain` source at all). Consumers on JVM would get `BleScanner`/`BleConnection` etc. from DI; a headless JVM app would simply not wire BLE into the graph. - -**Effort: ~10 minutes. Unblocks JVM smoke compile immediately.** - -### Option B: KABLE-backed JVM Implementation (Real Desktop BLE) - -Replace or supplement the Nordic `androidMain` implementation with KABLE in `commonMain` or platform-specific source sets. - -## Library Comparison - -### Nordic Kotlin-BLE-Library (current: `2.0.0-alpha16`) - -| Aspect | Status | -|---|---| -| Module structure | `core` and `client-core` are **pure JVM** (no Android dependencies). `client-android`, `environment-android` etc. are Android-only. | -| KMP status | **Not KMP yet.** `core` & `client-core` are JVM-only modules (not KMP multiplatform). No `iosMain`, no `commonMain` with `expect`/`actual`. | -| Roadmap | Nordic says: _"The library is intended to eventually be multiplatform on its own"_ but _"I don't have much KMP experience yet, we just started experimenting."_ | -| Our coupling | 5 Nordic imports across 6 `androidMain` files. All wrapped behind our `commonMain` interfaces. | -| Mocking | ✅ Has `client-android-mock`, `core-mock` modules — we use these in tests | -| Stability | Alpha (`2.0.0-alpha16`) — API still changing (recent breaking change in alpha16: `services()` emission) | - -### KABLE (JuulLabs, current: `0.42.0`) - -| Aspect | Status | -|---|---| -| KMP targets | ✅ Android, iOS, macOS, JVM, JavaScript, Wasm | -| API style | Coroutines/Flow-first. `Scanner`, `Peripheral`, `connect()`, `observe()`, `read()`, `write()` | -| JVM support | ✅ Uses Bluetooth on macOS/Linux/Windows via native bindings | -| Mocking | ❌ No mock module (Nordic's advantage) | -| Maturity | More mature than Nordic's KMP story, actively maintained | -| License | Apache 2.0 | -| Our coupling cost | Would need to rewrite 6 `androidMain` files (~400 lines total) | - -## Recommended Strategy - -### Phase 1: Add `jvm()` Target Now (No Library Change) ✅ COMPLETED - -Since `commonMain` is already pure Kotlin interfaces, `jvm()` has been added to `core:ble/build.gradle.kts`. No JVM BLE implementation is needed — the interfaces compile fine and a headless JVM app simply wouldn't inject BLE bindings. - -This unblocked `core:ble` in the JVM smoke compile. CI now validates `core:ble:compileKotlinJvm` on every PR. - -### Phase 2: Evaluate Whether to Migrate to KABLE (Strategic Decision) - -There are three paths, and the right one depends on project goals: - -#### Path A: Stay on Nordic, Wait for Their KMP Support -- **Pro:** Zero migration work, we're already well-abstracted -- **Pro:** Nordic's mock modules are valuable for testing -- **Con:** Nordic says KMP is "intended" but has no timeline and "just started experimenting" -- **Con:** Nordic library is still alpha (API instability risk) -- **Risk:** Could be waiting 1+ years - -#### Path B: Migrate to KABLE for `commonMain`, Keep Nordic as Optional Android Backend -- **Pro:** Real KMP BLE across all targets immediately -- **Pro:** KABLE is production-ready and actively maintained -- **Con:** ~400 lines of adapter code to rewrite -- **Con:** No built-in mock support (would need our own test doubles) -- **Con:** Two BLE library dependencies during transition - -#### Path C: Dual-Backend Architecture (Best of Both Worlds) -Keep `commonMain` interfaces as-is. Add a `kableMain` or use KABLE in `commonMain` only for platforms that need it (JVM/iOS), keep Nordic on Android. - -This is **overkill for now** but the architecture already supports it — our `BleConnection`/`BleScanner` interfaces would have multiple implementations selected via DI. - -### Recommendation - -**Phase 1 completed** (`jvm()` added, CI validates it). - -For Phase 2: **Path A (stay on Nordic, wait)** is the pragmatic choice for now because: - -1. Our abstraction layer is already clean — switching BLE backends later is a bounded, mechanical task -2. Nordic is actively developing (alpha16 released March 4, 2026 — 6 days ago) -3. We don't currently need real BLE on JVM/iOS -4. The mock modules are genuinely useful for testing - -If Nordic hasn't shipped KMP by the time we're ready for iOS, revisit KABLE. The migration cost is predictable: ~6 files, ~400 lines, all in `androidMain` → `commonMain`. - -## Potential Contribution to Nordic - -Nordic is open to help. High-impact contributions we could make: - -1. **File an issue or PR** showing how `core` and `client-core` could become `kotlin("multiplatform")` modules with `commonMain` + `jvmMain` source sets (they're pure JVM already — it's a build config change) -2. **Propose the `expect`/`actual` pattern** for `CentralManager` / `Peripheral` interfaces, showing how our wrapper demonstrates the abstraction boundary -3. **Share our `commonMain` interface design** as a reference for what a KMP-ready API surface looks like - -This would accelerate their timeline and reduce our eventual migration friction. - diff --git a/docs/archive/desktop-and-multi-target-roadmap.md b/docs/archive/desktop-and-multi-target-roadmap.md deleted file mode 100644 index b08732cf0..000000000 --- a/docs/archive/desktop-and-multi-target-roadmap.md +++ /dev/null @@ -1,243 +0,0 @@ -# Desktop & Multi-Target Roadmap - -> Date: 2026-03-11 -> -> Desktop is the first non-Android target, but every decision here is designed to benefit **all future targets** (iOS, web, etc.). The guiding principle: solve problems in `commonMain` or behind shared interfaces — never in a target-specific way when it can be avoided. - -## Current State - -### What works today - -| Layer | Status | -|---|---| -| Desktop scaffold | ✅ Compiles, runs, Navigation 3 shell with NavigationRail | -| Koin bootstrap | ✅ Full DI graph — stubs for all repository interfaces | -| Core KMP modules with `jvm()` | ✅ 16/16 (all core KMP modules) | -| Feature modules with `jvm()` | ✅ 6/6 — all feature modules compile on JVM | -| CI JVM smoke compile | ✅ 16 core + 6 feature modules + `desktop:test` | -| Repository stubs for non-Android | ✅ Full set in `desktop/src/main/kotlin/org/meshtastic/desktop/stub/` | -| Navigation 3 shell | ✅ Shared routes, NavigationRail, NavDisplay with placeholder screens | -| JetBrains lifecycle/nav3 forks | ✅ `org.jetbrains.androidx.lifecycle` + `org.jetbrains.androidx.navigation3` | -| Real settings feature screens | ✅ ~35 settings composables wired via `DesktopSettingsNavigation.kt` (all config + module screens) | -| Real node feature screens | ✅ Adaptive node list with real `NodeDetailContent`, TracerouteLog, NeighborInfoLog, HostMetricsLog | -| Real messaging feature screens | ✅ Adaptive contacts list with real `DesktopMessageContent` (non-paged message view with send) | -| Real connections screen | ✅ `DesktopConnectionsScreen` with TCP address entry, connection state display | -| Real TCP transport | ✅ Shared `StreamFrameCodec` + `TcpTransport` in `core:network`, used by both `app` and `desktop` | -| Mesh service controller | ✅ `DesktopMeshServiceController` — full `want_config` handshake, config/nodeinfo exchange | -| Remaining feature screens | ❌ Map, chart-based metrics (DeviceMetrics, etc.) | -| Remaining transport | ❌ Serial/USB, MQTT | - -### Module JVM target inventory - -**Core modules with `jvm()` target (16):** -`core:proto`, `core:common`, `core:model`, `core:repository`, `core:di`, `core:navigation`, `core:resources`, `core:datastore`, `core:database`, `core:domain`, `core:prefs`, `core:network`, `core:data`, `core:ble`, `core:service`, `core:ui` - -**Core modules that are Android-only by design (3):** -`core:api` (AIDL), `core:barcode` (camera), `core:nfc` (NFC hardware) - -**Feature modules (6) — all have `jvm()` target and compile on JVM:** -`feature:intro`, `feature:messaging`, `feature:map`, `feature:node`, `feature:settings`, `feature:firmware` - -**Modules with `jvmMain` source sets (hand-written actuals):** -`core:common` (4 files), `core:model` (via `jvmAndroidMain`, 3 files), `core:network` (via `jvmAndroidMain`, 1 file — `TcpTransport.kt`), `core:repository` (1 file — `Location.kt`), `core:ui` (6 files — QR, clipboard, HTML, platform utils, time tick, dynamic color) - -**Desktop feature wiring:** -`feature:settings` — fully wired with ~35 real composables via `DesktopSettingsNavigation.kt`, including 5 desktop-specific config screens (Device, Position, Network, Security, ExternalNotification). Other features remain placeholder. - ---- - -## KMP Gaps — Resolved - -These were pre-existing issues where `commonMain` code used symbols only available on Android. The JVM target surfaced them during Phase 1; all have been fixed. - -### `feature:node` ✅ Fixed -- `formatUptime()` moved from `core:model/androidMain` → `commonMain` (pure `kotlin.time` — no platform deps) -- Material 3 Expressive APIs (`ExperimentalMaterial3ExpressiveApi`, `titleMediumEmphasized`, `IconButtonDefaults.mediumIconSize`, `shapes` param) replaced with standard Material 3 equivalents -- `androidMain/DateTimeUtils.kt` renamed to `AndroidDateTimeUtils.kt` to avoid JVM class name collision - -### `feature:settings` ✅ Fixed -- Material 3 dependency wiring corrected (CMP `compose.material3` in commonMain) - -**Fix pattern applied:** When `commonMain` code references APIs not in Compose Multiplatform, use the standard Material 3 equivalent. Don't create expect/actual wrappers unless the behavior genuinely differs by platform. - ---- - -## Phased Roadmap - -### Phase 0 — No-op Stubs for Repository Interfaces (target-agnostic foundation) - -**Goal:** Let any non-Android target bootstrap a full Koin DI graph without crashing. - -**Approach:** Create a `NoopStubs.kt` file in `desktop/` that provides no-op/empty implementations of every repository interface the graph requires. These are explicitly "does nothing" implementations — they return empty flows, no-op on mutations, and log warnings on write calls. This unblocks DI graph assembly for desktop AND establishes the stub pattern future targets will reuse. - -**Why target-agnostic:** When iOS arrives, it will need the same stubs initially. The interfaces are all in `commonMain` already, so the stub pattern is inherently shared. Once real implementations exist (e.g., serial transport for desktop, CoreBluetooth for iOS), they replace the stubs per-target. - -**Interfaces to stub (priority order):** - -| Interface | Module | Notes | -|---|---|---| -| `ServiceRepository` | `core:repository` | Connection state, mesh packets, errors | -| `NodeRepository` | `core:repository` | Node DB, our node info | -| `RadioConfigRepository` | `core:repository` | Channel/config flows | -| `RadioInterfaceService` | `core:repository` | Raw radio bytes | -| `RadioController` | `core:model` | High-level radio commands | -| `PacketRepository` | `core:repository` | Message/packet queries | -| `MeshLogRepository` | `core:repository` | Log storage | -| `MeshServiceNotifications` | `core:repository` | Notifications (no-op on desktop) | -| `PacketHandler` | `core:repository` | Packet dispatch | -| `CommandSender` | `core:repository` | Command dispatch | -| `AlertManager` | `core:ui` | Alert dialog state | -| Preference interfaces | `core:repository` | `UiPrefs`, `MapPrefs`, `MeshPrefs`, etc. | - -### Phase 1 — Add `jvm()` Target to Feature Modules ✅ COMPLETE - -**Goal:** Feature modules compile on JVM, unblocking desktop (and future JVM-based targets) from using shared ViewModels and UI. - -**Result:** All 6 feature modules have `jvm()` target and compile clean on JVM. KMP gaps discovered during this phase (Material 3 Expressive APIs, `formatUptime` placement) have been resolved. - -**CI update:** All 6 feature module `:compileKotlinJvm` tasks added to the JVM smoke compile step. - -### Phase 2 — Desktop Koin Graph Assembly - -**Goal:** Desktop boots with a complete Koin graph — stubs for all platform services, real implementations where possible (database, datastore, network). - -**Approach:** Create `desktop/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinModule.kt` that mirrors `AppKoinModule` but uses: -- No-op stubs for radio/BLE/notifications -- Real Room KMP database (already has JVM constructor) -- Real DataStore preferences (already KMP) -- Real Ktor HTTP client (already KMP in `core:network`) -- Real firmware release repository (network + database) - -This pattern directly transfers to iOS: replace `DesktopKoinModule` with `IosKoinModule`, swap stubs for CoreBluetooth-backed implementations. - -### Phase 3 — Shared Navigation Shell 🔄 IN PROGRESS - -**Goal:** Desktop shows a real multi-screen app with navigation, not a smoke report. - -**Completed:** -- ✅ Switched Navigation 3 + lifecycle artifacts to JetBrains multiplatform forks (`org.jetbrains.androidx.navigation3` `1.1.0-alpha03`, `org.jetbrains.androidx.lifecycle` `2.10.0-alpha08`) -- ✅ Desktop app shell with `NavigationRail` for top-level destinations (Conversations, Nodes, Map, Settings, Connections) -- ✅ `NavDisplay` + `entryProvider` pattern matching the Android app's nav graph shape -- ✅ `SavedStateConfiguration` with polymorphic `SerializersModule` for non-Android NavKey serialization -- ✅ Shared routes from `core:navigation` used for both Android and Desktop navigation -- ✅ Placeholder screens for all top-level destinations -- ✅ **`feature:settings` wired with real composables** — ~30 screens including DeviceConfiguration, ModuleConfiguration, Administration, CleanNodeDatabase, FilterSettings, radio config routes (User, Channels, Power, Display, LoRa, Bluetooth), and module config routes (MQTT, Serial, StoreForward, RangeTest, Telemetry, CannedMessage, Audio, RemoteHardware, NeighborInfo, AmbientLighting, DetectionSensor, Paxcounter, StatusMessage, TrafficManagement, TAK) -- ✅ Desktop-specific top-level settings screen (`DesktopSettingsScreen.kt`) replacing Android-only `SettingsScreen` - -**Remaining:** -- ~~Wire real feature composables from `feature:node`, `feature:messaging`, and `feature:map` into the desktop nav graph~~ → node and messaging done; map still placeholder -- ~~Some settings config sub-screens still use placeholders (Device Config, Position, Network, Security, ExtNotification, Debug, About)~~ → 5 config screens replaced with real desktop implementations; Debug and About remain placeholders -- Platform-specific screens (map, BLE scan) show "not available" placeholders -- Evaluate sidebar/tab hybrid for secondary navigation within features - -### Phase 4 — Real Transport Layer 🔄 IN PROGRESS - -**Goal:** Desktop can actually talk to a Meshtastic radio. - -**Completed:** -- ✅ `DesktopRadioInterfaceService` — TCP socket transport with auto-reconnect, heartbeat, and backoff retry -- ✅ `DesktopMeshServiceController` — orchestrates the full `want_config` handshake (config → channels → nodeinfo exchange) -- ✅ `DesktopConnectionsScreen` — TCP address entry, service-level connection state display, recent addresses -- ✅ Transport state architecture — transport layer (`RadioInterfaceService`) reports binary connected/disconnected; service layer (`ServiceRepository`) manages Connecting state during handshake - -**Transports (in priority order):** - -| Transport | Platform | Library | Status | -|---|---|---|---| -| TCP | Desktop (JVM) | Ktor/Okio | ✅ Implemented | -| Serial/USB | Desktop (JVM) | jSerialComm | ❌ Not started | -| MQTT | All (KMP) | Ktor/MQTT | ❌ Not started | -| BLE | iOS | Kable/CoreBluetooth | ❌ Not started | -| BLE | Desktop | Kable (JVM) | ❌ Not started | - -**Architecture:** The `RadioInterfaceService` contract in `core:repository` already defines the transport abstraction. Each transport is an implementation of that interface, registered via Koin. Desktop initially gets serial + TCP. iOS gets BLE. - -### Phase 5 — Feature Parity Roadmap - -| Feature | Desktop | iOS | Web | -|---|---|---|---| -| Node list | Phase 3 | Phase 3 | Later | -| Messaging | Phase 3 | Phase 3 | Later | -| Settings | Phase 3 | Phase 3 | Later | -| Map | Phase 4+ (MapLibre) | Phase 4+ (MapKit) | Later | -| Firmware update | Phase 5+ | Phase 5+ | N/A | -| BLE scanning | Phase 5+ (Kable) | Phase 3 (CoreBluetooth) | N/A | -| NFC/Barcode | N/A | Later | N/A | - ---- - -## Cross-Target Design Principles - -1. **Solve in `commonMain` first.** If logic doesn't need platform APIs, it belongs in `commonMain`. Period. -2. **Interfaces in `commonMain`, implementations per-target.** The repository pattern is already established — extend it. -3. **Stubs are a valid first implementation.** Every target starts with no-op stubs, then graduates to real implementations. This is intentional, not lazy. -4. **Feature modules stay target-agnostic in `commonMain`.** Android-specific UI goes in `androidMain`, desktop-specific UI goes in `jvmMain`, iOS-specific UI goes in `iosMain`. -5. **Transport is a pluggable adapter.** BLE, serial, TCP, MQTT are all implementations of the same radio interface contract. -6. **CI validates every target.** If a module declares `jvm()`, CI compiles it on JVM. No exceptions. - ---- - -## Execution Status (updated 2026-03-11) - -1. ✅ Create this roadmap document -2. ✅ Create no-op repository stubs in `desktop/stub/NoopStubs.kt` (all 30+ interfaces) -3. ✅ Create desktop Koin module in `desktop/di/DesktopKoinModule.kt` -4. ✅ Add `jvm()` to all 6 feature modules — **6/6 compile clean on JVM** -5. ✅ Update CI to include all feature module JVM smoke compile (6 modules) -6. ✅ Update docs: `AGENTS.md`, `.github/copilot-instructions.md`, `docs/agent-playbooks/task-playbooks.md` -7. ✅ Fix KMP debt in `feature:node` (Material 3 Expressive → standard M3, `formatUptime` → commonMain) -8. ✅ Fix KMP debt in `feature:settings` (dependency wiring) -9. ✅ Move `ConnectionsViewModel` to `core:ui` commonMain -10. ✅ Split `UIViewModel` into shared `BaseUIViewModel` + Android adapter -11. ✅ Switch Navigation 3 to JetBrains fork (`org.jetbrains.androidx.navigation3:navigation3-ui:1.1.0-alpha03`) -12. ✅ Switch lifecycle-runtime-compose and lifecycle-viewmodel-compose to JetBrains forks (`org.jetbrains.androidx.lifecycle:2.10.0-alpha08`) -13. ✅ Implement desktop Navigation 3 shell with `NavigationRail` + `NavDisplay` + placeholder screens -14. ✅ Wire `feature:settings` composables into desktop nav graph (~30 real screens) -15. ✅ Create desktop-specific `DesktopSettingsScreen` (replaces Android-only `SettingsScreen`) -16. ✅ Delete passthrough Android ViewModel wrappers (11 wrappers removed) -17. ✅ Migrate `feature:node` UI components from `androidMain` → `commonMain` -18. ✅ Migrate `feature:settings` UI components from `androidMain` → `commonMain` -19. ✅ Wire `feature:node` composables into the desktop nav graph (real `DesktopNodeListScreen` with shared `NodeListViewModel`, `NodeItem`, `NodeFilterTextField`) -20. ✅ Wire `feature:messaging` composables into the desktop nav graph (real `DesktopContactsScreen` with shared `ContactsViewModel`) -21. ✅ Add `feature:node`, `feature:messaging`, `feature:map` module dependencies to `desktop/build.gradle.kts` -22. ✅ Add JetBrains Material 3 Adaptive (`1.3.0-alpha05`) to version catalog and desktop module — see [`docs/kmp-adaptive-compose-evaluation.md`](./kmp-adaptive-compose-evaluation.md) -23. ✅ Create `DesktopAdaptiveContactsScreen` using `ListDetailPaneScaffold` (contacts list + message detail placeholder) -24. ✅ Create `DesktopAdaptiveNodeListScreen` using `ListDetailPaneScaffold` (node list + node detail placeholder, context menu) -25. ✅ Provide Ktor `HttpClient` (Java engine) in desktop Koin module — fixes `ApiServiceImpl` → `DeviceHardwareRemoteDataSource` → `IsOtaCapableUseCase` → `SettingsViewModel` injection chain -26. ✅ Wire real `NodeDetailContent` from commonMain into adaptive node list detail pane (replacing placeholder) -27. ✅ Move `ContactItem.kt` from `feature:messaging/androidMain` → `commonMain` (pure M3, no Android deps) -28. ✅ Extract `MetricLogComponents.kt` (shared `MetricLogItem`/`DeleteItem`) and move `TracerouteLog`, `NeighborInfoLog`, `TimeFrameSelector`, `HardwareModelExtensions` to commonMain -29. ✅ Wire TracerouteLog, NeighborInfoLog, HostMetricsLog as real screens in `DesktopNodeNavigation.kt` (replacing placeholders) with `MetricsViewModel` registered in desktop Koin module -30. ✅ Move `MessageBubble.kt` from `feature:messaging/androidMain` → `commonMain` (pure Compose, zero Android deps, made public) -31. ✅ Build `DesktopMessageContent` composable — non-paged message list with send input for contacts detail pane (replaces placeholder) -32. ✅ Add `getMessagesFlow()` to `MessageViewModel` — non-paged `Flow>` for desktop (avoids paging-compose dependency) -33. ✅ Implement `DesktopRadioInterfaceService` — TCP socket transport with auto-reconnect, heartbeat, and configurable backoff retry -34. ✅ Implement `DesktopMeshServiceController` — mesh service lifecycle orchestrator wiring `want_config` handshake chain (config → channels → nodeinfo) -35. ✅ Create `DesktopConnectionsScreen` — TCP address entry UI with service-level connection state display and recent address history -36. ✅ Fix transport state architecture — removed transport-level `Connecting` emission that blocked `want_config` handshake; transport now reports binary connected/disconnected, service layer owns the Connecting state during config exchange -37. ✅ Create 5 desktop-specific config screens replacing placeholders: `DesktopDeviceConfigScreen` (role, rebroadcast, timezone via JVM `ZoneId`), `DesktopPositionConfigScreen` (fixed position, GPS, position flags — omits Android Location), `DesktopNetworkConfigScreen` (WiFi, Ethernet, IPv4 — omits QR/NFC), `DesktopSecurityConfigScreen` (keys, admin, key regeneration via JVM `SecureRandom` — omits file export), `DesktopExternalNotificationConfigScreen` (GPIO, ringtone — omits MediaPlayer/file import) -38. ✅ **Transport Deduplication:** Extracted `StreamFrameCodec` (commonMain) and `TcpTransport` (jvmAndroidMain) into `core:network` — eliminates ~450 lines of duplicated framing/TCP code between `app` and `desktop`. `StreamInterface` and `TCPInterface` in `app` now delegate to shared codec/transport. `DesktopRadioInterfaceService` reduced from 455 → 178 lines. Added `StreamFrameCodecTest` in `core:network/commonTest`. -39. ✅ **EmojiPickerDialog — unified commonMain implementation:** Replaced the `expect`/`actual` split with a single fully-featured emoji picker in `core:ui/commonMain`. Features: 9 category tabs with bidirectional scroll-tab sync, keyword search, recently-used tracking (persisted via `EmojiPickerViewModel`/`CustomEmojiPrefs`), Fitzpatrick skin-tone selector, and ~1000+ emoji catalog with `EmojiData.kt`. Deleted Android `EmojiPicker.kt` (AndroidView wrapper), `CustomRecentEmojiProvider.kt`, and JVM `EmojiPickerDialog.kt` (flat grid). Removed `androidx-emoji2-emojipicker` and `guava` dependencies from `core:ui`. -40. ✅ **Messaging component migration:** Moved `MessageActions.kt`, `MessageActionsBottomSheet.kt`, `Reaction.kt` (minus previews), `DeliveryInfoDialog.kt` from `feature:messaging/androidMain` → `commonMain`. Extracted `MessageStatusIcon` from `MessageItem.kt` into shared `MessageStatusIcon.kt`. Removed `ExperimentalMaterial3ExpressiveApi` (Android-only). Preview functions remain in `androidMain/ReactionPreviews.kt`. -41. ✅ **PositionLog table migration:** Extracted `PositionLogHeader`, `PositionItem`, `PositionList` composables from `feature:node/androidMain` into shared `PositionLogComponents.kt` in `commonMain`. Android `PositionLogScreen` with CSV export stays in `androidMain`. - -### Next: Connections UI, chart migration, remaining screens, and serial transport -Desktop now has: -- **TCP connectivity** with full `want_config` handshake and config exchange -- **Shared transport layer** — `StreamFrameCodec` and `TcpTransport` in `core:network` used by both `app` and `desktop` -- **Shared messaging components** — `MessageActions`, `ReactionRow`, `ReactionDialog`, `MessageStatusIcon`, `DeliveryInfo` all in commonMain -- **Shared position log** — `PositionLogHeader`, `PositionItem`, `PositionList` in commonMain -- Adaptive list-detail screens for **nodes** (with real `NodeDetailContent`) and **contacts** (with real `DesktopMessageContent`) -- Real screens for **TracerouteLog**, **NeighborInfoLog**, **HostMetricsLog** metrics -- ~35 real **settings** screens (all config + module routes — only Debug Panel and About remain placeholder) - -Next priorities: -- **Connections UI Unification:** Create `feature:connections` to merge the fragmented Android and Desktop connection screens, abstracting discovery mechanisms (BLE, USB, TCP) behind a shared interface. -- Evaluate KMP charting replacement for Vico (DeviceMetrics, EnvironmentMetrics, SignalMetrics, PowerMetrics, PaxMetrics) -- Wire serial/USB transport for direct radio connection on Desktop -- Wire MQTT transport for cloud relay operation -- **Hardware Abstraction:** Abstract `core:barcode` and `core:nfc` into `commonMain` interfaces with `androidMain` implementations. -- **iOS CI:** Turn on iOS compilation (`iosArm64()`, `iosSimulatorArm64()`) in the GitHub Actions CI pipeline to ensure the shared codebase remains LLVM-compatible. -- **Dependency Tracking:** Track stable releases for currently required alpha/RC dependencies (Compose Multiplatform `1.11.0-alpha03` for Adaptive layouts, Koin `4.2.0-RC1` for K2 plugin). Do not downgrade these prematurely as they enable critical KMP functionality. - - diff --git a/docs/archive/kmp-adaptive-compose-evaluation.md b/docs/archive/kmp-adaptive-compose-evaluation.md deleted file mode 100644 index 5b3cb61d3..000000000 --- a/docs/archive/kmp-adaptive-compose-evaluation.md +++ /dev/null @@ -1,174 +0,0 @@ -# KMP Material 3 Adaptive Compose — Evaluation - -> Date: 2026-03-10 -> -> This evaluation assesses the availability and readiness of Compose Material 3 Adaptive libraries for Kotlin Multiplatform, specifically for enabling shared list-detail layouts (nodes, messaging) across Android and Desktop. - -## Executive Summary - -**Material 3 Adaptive is available as a multiplatform library** via JetBrains forks, with desktop and iOS targets. Version `1.3.0-alpha05` is built against the exact same CMP and Navigation 3 versions the project already uses. This unblocks moving `ListDetailPaneScaffold`-based screens into `commonMain` and wiring real adaptive layouts on desktop — no more placeholder screens for nodes and messaging. - -## Current State in the Project - -### What the project uses today - -| API | File | Source Set | Maven Coordinates | -|---|---|---|---| -| `ListDetailPaneScaffold` | `app/.../AdaptiveNodeListScreen.kt` | `app` (Android-only) | `androidx.compose.material3.adaptive:adaptive-layout:1.2.0` | -| `ListDetailPaneScaffold` | `feature/messaging/.../AdaptiveContactsScreen.kt` | `androidMain` | `androidx.compose.material3.adaptive:adaptive-layout:1.2.0` | -| `NavigationSuiteScaffold` | `app/.../Main.kt` | `app` (Android-only) | `androidx.compose.material3:material3-adaptive-navigation-suite` (BOM) | -| `currentWindowAdaptiveInfo` | `app/.../Main.kt` | `app` (Android-only) | `androidx.compose.material3.adaptive:adaptive:1.2.0` | - -### Imports used across the codebase - -``` -import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi -import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo -import androidx.compose.material3.adaptive.layout.AnimatedPane -import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold -import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole -import androidx.compose.material3.adaptive.navigation.BackNavigationBehavior -import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator -import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold -import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffoldDefaults -import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteType -``` - -### Where the dependencies are declared - -- `gradle/libs.versions.toml`: `androidxComposeMaterial3Adaptive = "1.2.0"` → AndroidX (Android-only) -- `app/build.gradle.kts`: `androidMain` only -- `feature/messaging/build.gradle.kts`: `androidMain` only - -## JetBrains Multiplatform Adaptive Artifacts - -JetBrains publishes multiplatform forks of Material 3 Adaptive with full target coverage: - -### Artifact inventory - -| JetBrains Artifact | AndroidX Equivalent | Desktop | iOS | Status | -|---|---|---|---|---| -| `org.jetbrains.compose.material3.adaptive:adaptive` | `androidx.compose.material3.adaptive:adaptive` | ✅ | ✅ | Published on Maven Central | -| `org.jetbrains.compose.material3.adaptive:adaptive-layout` | `androidx.compose.material3.adaptive:adaptive-layout` | ✅ | ✅ | Published on Maven Central | -| `org.jetbrains.compose.material3.adaptive:adaptive-navigation` | `androidx.compose.material3.adaptive:adaptive-navigation` | ✅ | ✅ | Published on Maven Central | -| `org.jetbrains.compose.material3.adaptive:adaptive-navigation3` | _(new, no AndroidX equivalent)_ | ✅ | ✅ | Published on Maven Central (1.3.0+ only) | -| `org.jetbrains.compose.material3:material3-adaptive-navigation-suite` | `androidx.compose.material3:material3-adaptive-navigation-suite` | ✅ | ✅ | Bundled with CMP `material3` at `composeMaterial3Version` | - -### Package names are identical - -The JetBrains forks use the same `androidx.compose.material3.adaptive.*` package names as AndroidX. **No import changes are needed** — only the Maven coordinates in `build.gradle.kts` change. - -### Version compatibility matrix - -| JB Adaptive Version | CMP Version | Navigation 3 | Kotlin | Match? | -|---|---|---|---|---| -| **`1.3.0-alpha05`** | **`1.11.0-alpha03`** | **`1.1.0-alpha03`** | `2.2.20` | ✅ **Exact match** on CMP + Nav3 | -| `1.2.0` | `1.9.0` | — | `2.1.21` | ❌ Too old for this project | -| `1.1.2` | `1.8.x` | — | — | ❌ Too old | - -**`1.3.0-alpha05` is the correct version** — it is built against `foundation:1.11.0-alpha03` and `navigation3-ui:1.1.0-alpha03`, both of which are the exact versions the project uses today. - -### `adaptive-navigation3` — new Navigation 3 integration - -The `adaptive-navigation3` artifact is a brand-new addition at `1.3.0`. It provides Navigation 3-aware adaptive scaffolding. Its POM shows dependencies on: -- `navigation3-ui-desktop:1.1.0-alpha03` ✅ -- `navigationevent-compose-desktop:1.0.1` - -This could eventually enable deeper Nav3 + adaptive integration (e.g., `ListDetailPaneScaffold` directly managing Nav3 back stacks), but it's not required for the initial migration. - -## What This Enables - -### Immediate opportunity: shared `ListDetailPaneScaffold` - -The `ListDetailPaneScaffold` and its navigator can move into `commonMain` code. This directly enables: - -1. **`AdaptiveNodeListScreen`** — currently in `app` (Android-only) — can be restructured so the scaffold pattern works cross-platform -2. **`AdaptiveContactsScreen`** — currently in `feature:messaging/androidMain` — same opportunity -3. **Desktop gets real list-detail layouts** instead of placeholder text - -### Remaining Android-only blockers per file - -Even with adaptive layouts available in `commonMain`, each file has additional Android-specific code that must be handled separately: - -| File | Android-Only APIs Used | Migration Strategy | -|---|---|---| -| `AdaptiveNodeListScreen.kt` | `BackHandler`, `LocalFocusManager` | `BackHandler` → `expect/actual`; `LocalFocusManager` is already in CMP | -| `AdaptiveContactsScreen.kt` | `BackHandler` (same pattern) | Same as above | -| `NodeListScreen.kt` | `ExperimentalMaterial3ExpressiveApi`, `animateFloatingActionButton`, `LocalContext`, `showToast` | Expressive APIs → standard M3; toast → platform callback | -| `NodeDetailScreen.kt` | `android.Manifest`, `Intent`, `ActivityResultContracts`, `tooling.preview` | Heavy Android — keep in `androidMain`, create desktop variant | -| `Main.kt` (app) | `currentWindowAdaptiveInfo`, `NavigationSuiteScaffold` | App-only, desktop already uses `NavigationRail` — no migration needed | - -### `NavigationSuiteScaffold` in desktop - -The desktop already uses `NavigationRail` directly (in `DesktopMainScreen.kt`). The `NavigationSuiteScaffold` from the main `material3` group is already available multiplatform via `compose.material3AdaptiveNavigationSuite` in the CMP DSL (`composeMaterial3Version = "1.9.0"`), but it's not needed — the desktop's `NavigationRail` is a deliberate design choice that works better for desktop form factors. - -## Risk Assessment - -| Factor | Assessment | -|---|---| -| Library stability | Alpha, but same stability tier as CMP `1.11.0-alpha03` and Nav3 `1.1.0-alpha03` already in use | -| API surface stability | `ListDetailPaneScaffold` API is stable in practice (widely adopted since AndroidX `1.0.0`) | -| Build pipeline alignment | `1.3.0-alpha05` is produced by the same JetBrains compose-multiplatform build that produces CMP `1.11.0-alpha03` | -| Breaking change risk | Low — API surface matches AndroidX; only coordinates change | -| Dependency policy alignment | Follows project rule: "alpha only behind hard abstraction seams" (adaptive is behind feature module boundaries) | - -## Recommended Approach - -### Phase 1 — Add JetBrains adaptive dependencies ✅ DONE - -Added to `gradle/libs.versions.toml`: - -```toml -jetbrains-adaptive = "1.3.0-alpha05" - -jetbrains-compose-material3-adaptive = { module = "org.jetbrains.compose.material3.adaptive:adaptive", version.ref = "jetbrains-adaptive" } -jetbrains-compose-material3-adaptive-layout = { module = "org.jetbrains.compose.material3.adaptive:adaptive-layout", version.ref = "jetbrains-adaptive" } -jetbrains-compose-material3-adaptive-navigation = { module = "org.jetbrains.compose.material3.adaptive:adaptive-navigation", version.ref = "jetbrains-adaptive" } -``` - -Added to `desktop/build.gradle.kts`: -```kotlin -implementation(libs.jetbrains.compose.material3.adaptive) -implementation(libs.jetbrains.compose.material3.adaptive.layout) -implementation(libs.jetbrains.compose.material3.adaptive.navigation) -``` - -Desktop compile verified: `./gradlew :desktop:compileKotlin` — **BUILD SUCCESSFUL**. - -### Phase 2 — Desktop adaptive contacts screen ✅ DONE - -1. Moved `adaptive`, `adaptive-layout`, `adaptive-navigation` dependencies from `androidMain.dependencies` → `commonMain.dependencies` in `feature:messaging/build.gradle.kts` (using JetBrains coordinates, replacing AndroidX adaptive) -2. Created `desktop/.../DesktopAdaptiveContactsScreen.kt` using `ListDetailPaneScaffold` with: - - List pane: shared `ContactItem` composable with `isActive` highlighting on selected contact - - Detail pane: real `DesktopMessageContent` — non-paged message list with send input using shared `MessageViewModel` -3. Wired into `DesktopMessagingNavigation.kt` for `ContactsRoutes.ContactsGraph` and `ContactsRoutes.Contacts` -4. Verified: `./gradlew :desktop:compileKotlin :feature:messaging:compileKotlinJvm :app:compileFdroidDebugKotlin` — **BUILD SUCCESSFUL** - -### Phase 3 — Desktop adaptive node list screen ✅ DONE - -1. Added JetBrains adaptive dependencies to `feature:node/build.gradle.kts` `commonMain.dependencies` -2. Created `desktop/.../DesktopAdaptiveNodeListScreen.kt` using `ListDetailPaneScaffold` with: - - List pane: shared `NodeItem`, `NodeFilterTextField`, `MainAppBar` composables; context menu for favorite/ignore/mute/remove; `isActive` highlighting - - Detail pane: real `NodeDetailContent` from commonMain — shared `NodeDetailList` with identity, device actions, position, hardware, notes, admin sections -3. Wired into `DesktopNodeNavigation.kt` for `NodesRoutes.NodesGraph` and `NodesRoutes.Nodes` -4. Metrics log screens (TracerouteLog, NeighborInfoLog, HostMetricsLog) wired as real screens with `MetricsViewModel` (replacing placeholders) -5. Verified: `./gradlew :desktop:compileKotlin :feature:node:compileKotlinJvm :app:compileFdroidDebugKotlin` — **BUILD SUCCESSFUL** - -### Phase 4 — Optional: evaluate `adaptive-navigation3` - -The new `adaptive-navigation3` artifact may offer cleaner Nav3 integration for list-detail patterns. Evaluate once the basic adaptive migration is stable. - -## Decision - -**Proceed with JetBrains adaptive `1.3.0-alpha05`.** - -The version alignment is perfect, the risk profile matches what the project already accepts for CMP/Nav3/lifecycle, and the payoff is significant: shared list-detail layouts for nodes and messaging across Android and Desktop. - -## References - -- Maven Central: [`org.jetbrains.compose.material3.adaptive:adaptive`](https://repo1.maven.org/maven2/org/jetbrains/compose/material3/adaptive/adaptive/) -- Maven Central: [`adaptive-navigation3`](https://repo1.maven.org/maven2/org/jetbrains/compose/material3/adaptive/adaptive-navigation3/) -- AndroidX source: [`ListDetailPaneScaffold.kt` in `commonMain`](https://github.com/androidx/androidx/blob/main/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ListDetailPaneScaffold.kt) -- Current project dependency: `androidxComposeMaterial3Adaptive = "1.2.0"` in `gradle/libs.versions.toml` - - diff --git a/docs/archive/kmp-app-migration-assessment.md b/docs/archive/kmp-app-migration-assessment.md deleted file mode 100644 index 13fe9c052..000000000 --- a/docs/archive/kmp-app-migration-assessment.md +++ /dev/null @@ -1,127 +0,0 @@ -# KMP Migration Assessment — App Module & Expect/Actual Evaluation - -> Date: 2026-03-10 - -## Summary of Changes Made - -### Expect/Actual Consolidation (Completed) - -| Expect/Actual | Resolution | Rationale | -|---|---|---| -| `Base64Factory` | ✅ **Replaced** with pure `commonMain` using `kotlin.io.encoding.Base64` | Both Android/JVM used `java.util.Base64` — Kotlin stdlib provides a cross-platform equivalent | -| `isDebug` | ✅ **Replaced** with `commonMain` constant `false` | Both actuals returned `false`; runtime debug detection uses `BuildConfigProvider.isDebug` via DI | -| `NumberFormatter` | ✅ **Replaced** with pure Kotlin `commonMain` implementation | Both actuals used identical `String.format(Locale.ROOT, ...)` — pure math-based formatting works everywhere | -| `UrlUtils` | ✅ **Replaced** with pure Kotlin `commonMain` RFC 3986 encoder | Both actuals used `URLEncoder.encode` — simple byte-level encoding is trivially portable | -| `SfppHasher` | ✅ **Consolidated** into `jvmAndroidMain` intermediate source set | Byte-for-byte identical implementations using `java.security.MessageDigest` | -| `platformRandomBytes` | ✅ **Consolidated** into `jvmAndroidMain` intermediate source set | Byte-for-byte identical implementations using `java.security.SecureRandom` | -| `getShortDateTime` | ✅ **Consolidated** into `jvmAndroidMain` intermediate source set | Functionally identical `java.text.DateFormat` usage | - -### Expect/Actual Retained (Genuinely Platform-Specific) - -| Expect/Actual | Why It Must Remain | -|---|---| -| `BuildUtils` (isEmulator, sdkInt) | Android uses `Build.FINGERPRINT`/`Build.VERSION.SDK_INT`; JVM stubs return defaults | -| `CommonUri` | Android wraps `android.net.Uri`; JVM wraps `java.net.URI` — different parsing semantics | -| `CommonUri.toPlatformUri()` | Returns platform-native URI type for interop | -| `Parcelable` abstractions (6 declarations) | AIDL/Android Parcel is a fundamentally Android-only concept | -| `Location` | Android wraps `android.location.Location`; JVM is an empty stub | -| `DateFormatter` | Android uses `DateUtils`/`ContextServices.app`; JVM uses `java.time` formatters | -| `MeasurementSystem` | Android uses ICU `LocaleData` with API-level branching; JVM uses `Locale.getDefault()` | -| `NetworkUtils.isValidAddress` | Android uses `InetAddresses`/`Patterns`; JVM uses regex/`InetAddress` | -| `core:ui` expects (7 declarations) | Dynamic color, lifecycle, clipboard, HTML, toast, map, URL, QR, brightness — all genuinely platform-specific UI | - ---- - -## App Module Evaluation — What's Left - -### Already Migrated to Shared KMP Modules - -The vast majority of business logic now lives in `core:*` and `feature:*` modules. The following pure passthrough wrappers have been eliminated from `:app`: - -- `AndroidCompassViewModel` (was wrapping `feature:node → CompassViewModel`) -- `AndroidContactsViewModel` (was wrapping `feature:messaging → ContactsViewModel`) -- `AndroidQuickChatViewModel` (was wrapping `feature:messaging → QuickChatViewModel`) -- `AndroidSharedMapViewModel` (was wrapping `feature:map → SharedMapViewModel`) -- `AndroidFilterSettingsViewModel` (was wrapping `feature:settings → FilterSettingsViewModel`) -- `AndroidCleanNodeDatabaseViewModel` (was wrapping `feature:settings → CleanNodeDatabaseViewModel`) -- `AndroidFirmwareUpdateViewModel` (was wrapping `feature:firmware → FirmwareUpdateViewModel`) -- `AndroidIntroViewModel` (was wrapping `feature:intro → IntroViewModel`) -- `AndroidNodeListViewModel` (was wrapping `feature:node → NodeListViewModel`) -- `AndroidNodeDetailViewModel` (was wrapping `feature:node → NodeDetailViewModel`) -- `AndroidMessageViewModel` (was wrapping `feature:messaging → MessageViewModel`) - -The remaining `app` ViewModels are ones with **genuine Android-specific logic**: - -| App ViewModel | Shared Base Class | Extra Android Logic | -|---|---|---| -| `AndroidSettingsViewModel` | `feature:settings → SettingsViewModel` | File I/O via `android.net.Uri` | -| `AndroidRadioConfigViewModel` | `feature:settings → RadioConfigViewModel` | Location permissions, file I/O | -| `AndroidDebugViewModel` | `feature:settings → DebugViewModel` | `Locale`-aware hex formatting | -| `AndroidMetricsViewModel` | `feature:node → MetricsViewModel` | CSV export via `android.net.Uri` | - -### Candidates for Migration (Medium Effort) - -| Component | Current Location | Target | Blockers | -|---|---|---|---| -| `GetDiscoveredDevicesUseCase` | `app/domain/usecase/` | `core:domain` | Depends on BLE/USB/NSD discovery — needs platform abstraction | -| `UIViewModel` (266 lines) | `app/model/` | Split: shared → `core:ui`, Android → `app` | `android.net.Uri` deep links, alert management mostly portable | -| `SavedStateHandle`-driven ViewModels | `feature:messaging`, `feature:node` | Shared route-arg abstraction | Replace direct `SavedStateHandle` dependency in shared VMs with route params/interface | -| `DeviceListEntry` (sealed class) | `app/model/` | `core:model` (Ble, Tcp, Mock); `app` (Usb) | `Usb` variant needs `UsbManager`/`UsbSerialDriver` | - -### Permanently Android-Only in `:app` - -| Component | Reason | -|---|---| -| `MeshService` (392 lines) | Android `Service` with foreground notifications, AIDL `IBinder` | -| `MeshServiceClient` | Android `Activity` lifecycle `ServiceConnection` bindings | -| `BootCompleteReceiver` | Android `BroadcastReceiver` | -| `MeshServiceStarter` | Android service lifecycle management | -| `MarkAsReadReceiver`, `ReplyReceiver`, `ReactionReceiver` | Android notification action receivers | -| `MeshLogCleanupWorker`, `ServiceKeepAliveWorker` | Android `WorkManager` workers | -| `LocalStatsWidget*` | Android Glance widget | -| `AppKoinModule`, `NetworkModule`, `FlavorModule` | Android-specific DI assembly with `ConnectivityManager`, `NsdManager`, `ImageLoader`, etc. | -| `MainActivity`, `MeshUtilApplication` | Android entry points | -| `repository/radio/*` (22 files) | USB serial, BLE interface, NSD discovery — hardware-level Android APIs | -| `repository/usb/*` | `UsbSerialDriver`, `ProbeTableProvider` | -| `*Navigation.kt` (7 files) | Android Navigation 3 composable wiring | - ---- - -## Desktop Module (formerly `jvm_demo`) - -### Changes Made -- **Renamed** `:jvm_demo` → `:desktop` as the first full non-Android target -- **Added** Compose Desktop (JetBrains Compose) with Material 3 windowed UI -- **Registered** `:desktop` in `settings.gradle.kts` -- **Added** dependencies on all core KMP modules with JVM targets, including `core:ui` -- **Implemented** Koin DI bootstrap with `BuildConfigProvider` stub -- **Implemented** `DemoScenario.renderReport()` exercising Base64, NumberFormatter, UrlUtils, DateFormatter, CommonUri, DeviceVersion, Capabilities, SfppHasher, platformRandomBytes, getShortDateTime, Channel key generation -- **Implemented** JUnit tests validating report output -- **Implemented** Navigation 3 shell with `NavigationRail` + `NavDisplay` + `SavedStateConfiguration` -- **Wired** `feature:settings` with ~30 real composable screens via `DesktopSettingsNavigation.kt` -- **Created** desktop-specific `DesktopSettingsScreen.kt` (replaces Android-only `SettingsScreen`) - -### Roadmap for Desktop -1. ~~Implement real navigation with shared `core:navigation` keys~~ ✅ -2. ~~Wire `feature:settings` with real composables~~ ✅ (~30 screens) -3. Wire `feature:node` and `feature:messaging` composables into the desktop nav graph -4. Add serial/USB transport for direct radio connection on Desktop -5. Add MQTT transport for cloud-connected operation -6. Package native distributions (DMG, MSI, DEB) - ---- - -## Architecture Improvement: `jvmAndroidMain` Source Set - -Added `jvmAndroidMain` intermediate source sets to `core:common` and `core:model` for sharing JVM-specific code (like `java.security.*` usage) between the `androidMain` and `jvmMain` targets without duplication. - -``` -commonMain - └── jvmAndroidMain ← NEW: shared JVM code - ├── androidMain - └── jvmMain -``` - -This pattern should be adopted by other modules as they add JVM targets to eliminate duplicate actual implementations. - - diff --git a/docs/archive/kmp-feature-migration-plan.md b/docs/archive/kmp-feature-migration-plan.md deleted file mode 100644 index 582fa12d7..000000000 --- a/docs/archive/kmp-feature-migration-plan.md +++ /dev/null @@ -1,188 +0,0 @@ -# KMP Feature Migration Slice - Plan - -**Objective:** Establish standardized patterns for migrating feature modules to full KMP + comprehensive test coverage. - -**Status:** Planning - -## Current State - -✅ **Core Infrastructure Ready:** -- core:testing module with shared test doubles -- All feature modules have KMP structure (jvm() target) -- All features have commonMain UI (Compose Multiplatform) - -❌ **Gaps to Address:** -- Incomplete commonTest coverage (only feature:messaging has bootstrap) -- Inconsistent test patterns across features -- No systematic approach for adding ViewModel tests -- Desktop module not fully integrated with all features - -## Migration Phases - -### Phase 1: Feature commonTest Bootstrap (THIS SLICE) -**Scope:** Establish patterns and add bootstrap tests to key features - -Features to bootstrap: -1. feature:settings -2. feature:node -3. feature:intro -4. feature:firmware -5. feature:map - -**What constitutes a bootstrap test:** -- ViewModel initialization test -- Simple state flow emission test -- Demonstration of using FakeNodeRepository/FakeRadioController -- Clear path for future expansion - -**Effort:** Low (pattern-driven, minimal logic tests) - -### Phase 2: Feature-Specific Integration Tests -**Scope:** Add domain-specific test doubles and integration scenarios - -Example: feature:messaging might have: -- FakeMessageRepository -- FakeContactRepository -- Message send/receive simulation - -**Effort:** Medium (requires understanding feature logic) - -### Phase 3: Desktop Feature Completion -**Scope:** Wire all features fully into desktop app - -Current status: -- ✅ Settings (~35 screens) -- ✅ Node (adaptive list-detail) -- ✅ Messaging (adaptive contacts) -- ❌ Map (needs implementation) -- ❌ Firmware (needs implementation) - -**Effort:** Medium-High - -### Phase 4: Remaining Transports -**Scope:** Complete transport layer (Serial/USB, MQTT) - -Current: -- ✅ TCP (JVM) -- ❌ Serial/USB -- ❌ MQTT (KMP version) - -**Effort:** High - -## Standards to Establish - -### 1. ViewModel Test Structure -```kotlin -// In src/commonTest/kotlin/ -class MyViewModelTest { - private val fakeRepo = FakeNodeRepository() - - private fun createViewModel(): MyViewModel { - // Create with fakes - } - - @Test - fun testInitialization() = runTest { - // Verify ViewModel initializes without errors - } - - @Test - fun testStateFlowEmissions() = runTest { - // Test primary state emissions - } -} -``` - -### 2. UseCase Test Structure -```kotlin -class MyUseCaseTest { - private val fakeRadio = FakeRadioController() - - private fun createUseCase(): MyUseCase { - // Create with fakes - } - - @Test - fun testHappyPath() = runTest { - // Test normal operation - } - - @Test - fun testErrorHandling() = runTest { - // Test error scenarios - } -} -``` - -### 3. Feature-Specific Fakes Template -```kotlin -// In core:testing/src/commonMain if reusable -// Otherwise in feature/*/src/commonTest -class FakeMyRepository : MyRepository { - val callHistory = mutableListOf() - - override suspend fun doSomething() { - callHistory.add("doSomething") - } -} -``` - -## Files to Create - -### Core:Testing Extensions -- FakeContactRepository (for feature:messaging) -- FakeMessageRepository (for feature:messaging) -- (Others as needed) - -### Feature:Settings Tests -- SettingsViewModelTest.kt -- Build.gradle.kts update (commonTest block if needed) - -### Feature:Node Tests -- NodeListViewModelTest.kt -- NodeDetailViewModelTest.kt - -### Feature:Intro Tests -- IntroViewModelTest.kt - -### Feature:Firmware Tests -- FirmwareViewModelTest.kt - -### Feature:Map Tests -- MapViewModelTest.kt - -## Success Criteria - -✅ All feature modules have commonTest with: -- At least one ViewModel bootstrap test -- Using FakeNodeRepository or similar -- Pattern clear for future expansion - -✅ All tests compile cleanly on all targets (JVM, Android) - -✅ Documentation updated with examples - -✅ Developer guide for adding new tests - -## Next Steps After This Slice - -1. Measure test coverage (current baseline) -2. Create integration test patterns -3. Add feature-specific fakes to core:testing -4. Complete Desktop feature wiring -5. Address remaining transport layers - -## Estimated Effort - -- Phase 1: 2-3 hours (pattern establishment + bootstrap) -- Phase 2: 4-6 hours (feature-specific integration) -- Phase 3: 6-8 hours (desktop completion) -- Phase 4: 8-12 hours (transport layer) - -**Total:** ~20-30 hours for complete KMP + test coverage - ---- - -**Status:** Ready to implement Phase 1 -**Next Action:** Create SettingsViewModelTest pattern and replicate across features - diff --git a/docs/archive/kmp-migration.md b/docs/archive/kmp-migration.md deleted file mode 100644 index 55f5ae1ee..000000000 --- a/docs/archive/kmp-migration.md +++ /dev/null @@ -1,82 +0,0 @@ -# Kotlin Multiplatform (KMP) Migration Guide - -> [!IMPORTANT] -> This document is now primarily a **historical migration guide**. -> For the current evidence-backed status snapshot, see [`docs/kmp-progress-review-2026.md`](./kmp-progress-review-2026.md). - -## Overview -Meshtastic-Android is actively migrating its core logic layers to Kotlin Multiplatform (KMP). This migration decouples the business logic, domain models, local storage, network protocols, and dependency injection from the Android JVM framework. The ultimate goal is a modular, highly testable `core` that can be shared across multiple platforms (e.g., Android, Desktop, and potentially iOS). - -## Historical Status Snapshot - -By early 2026, the migration had successfully decoupled the foundational data and domain layers, and the primary namespace had been unified to `org.meshtastic`. - -For the current state of completion, blockers, and remaining effort, use [`docs/kmp-progress-review-2026.md`](./kmp-progress-review-2026.md). - -### Accomplished Milestones - -* **Early Foundations (2022-2025):** - * ✅ **Storage and repository groundwork:** DataStore adoption, repository-pattern refactors, and service/data decoupling began well before the explicit KMP conversion wave. - * ✅ **`core:model` & `core:proto`:** Migrated early as pure data layers. - * ✅ **`core:strings` / `core:resources`:** Migrated to Compose Multiplatform for unified string resources (#3617, #3669). - * ✅ **Logging:** Replaced Android-bound `Timber` with KMP-ready `Kermit` (#4083). - * ✅ **`core:common`:** Decoupled basic utilities and cleanly extracted away from Android constraints (#4026). -* **Namespace Modernization:** - * The `app` module source code was completely relocated from `com.geeksville.mesh` to `org.meshtastic.app`. - * **Legacy Compatibility:** External integrations (like ATAK) rely on legacy Android Intents. `AndroidManifest.xml` preserves the `` signatures to ensure unbroken backwards compatibility. -* **Module Conversions (`meshtastic.android.library` -> `meshtastic.kmp.library`):** - * ✅ **`core:repository`:** Interfaces extracted to `commonMain`. - * ✅ **`core:domain`:** Use cases migrated. Android `Handler` and `java.io.File` logic replaced with Coroutines and Okio (#4731, #4685). - * ✅ **`core:prefs`:** Android SharedPreferences replaced with Multiplatform DataStore (#4731). - * ✅ **`core:network`:** Extracted KMP interfaces for MQTT and local network abstractions. - * ✅ **`core:di`:** Coroutine dispatchers mapped to standard Kotlin abstractions instead of Android thread pools. - * ✅ **`core:database`:** Migrated to Room Kotlin Multiplatform (#4702). - * ✅ **`core:data`:** Concrete repository implementations moved to `commonMain`. Android-specific logic (e.g., parsing `device_hardware.json` from `assets`) was abstracted behind KMP interfaces with implementations provided in `androidMain`. -* **Architecture Refinements:** - * `core:analytics` was completely dissolved. Abstract tracking interfaces were moved to `core:repository`, and concrete SDK implementations (Firebase, DataDog) were moved to the `app` module. - * Test stability greatly improved by eliminating Robolectric for core logic tests in favor of pure MockK stubs. - -* ✅ **`core:ble` / `core:bluetooth`:** Implemented a "Nordic Hybrid" Interface-Driven abstraction. Defined pure KMP interfaces (`BleConnectionManager`, `BleDevice`, etc.) in `commonMain` so that Desktop and Web targets can compile, while using Nordic's `KMM-BLE-Library` specifically inside the `androidMain` source set. - * ✅ **`core:service`:** Converted to a KMP module, isolating Android service bindings and lifecycle concerns to `androidMain`. - * ℹ️ **`core:api`:** Remains an Android-specific integration module because AIDL is Android-only. Treat it as a platform adapter rather than a shared KMP target. - -### Remaining Work for Broader KMP Maturity -The main bottleneck is no longer simply “moving code into KMP modules.” The remaining work is now about validating and hardening that architecture for non-Android targets. - -1. **Android-edge modules still remain platform-specific:** - * **`core:barcode` / `core:nfc`:** Android-specific hardware integrations. *Partially addressed:* `core:ui` no longer depends on them directly and abstracts scanning via `CompositionLocalProvider`. - * **`core:api`:** Intentionally Android-specific because AIDL is Android-only. Any transport-neutral contracts should continue to be separated from the Android adapter layer. -2. **Feature modules are structurally migrated, but cleanup continues:** - * *Current State:* all `feature/*` modules now build as KMP libraries, and `androidx.lifecycle.ViewModel` is KMP-compatible. - * **`feature:messaging`, `feature:intro`, `feature:map`, `feature:settings`, `feature:node`, `feature:firmware`:** all have major logic/UI in shared modules, with Android-specific adapters isolated where still required. - * Remaining work is mostly about boundary cleanup, platform adapter consistency, and ensuring future non-Android targets can compile cleanly. -3. **Cross-target validation is still incomplete:** - * Most KMP modules currently declare only Android targets in practice. - * CI still validates Android builds and tests, but not a broad JVM/iOS/Desktop target matrix. -4. **`core:ui` & Navigation are largely complete, but now need target hardening rather than migration work:** - * ✅ **Navigation:** Migrated fully to **AndroidX Navigation 3**. The backstack is now a simple state list (`List`), enabling trivial sharing across multiplatform targets without relying on Android's legacy `NavController` or `navigation-compose`. - * ✅ **`core:ui`:** Converted to a pure KMP library (`meshtastic.kmp.library.compose`). - * Abstracted Clipboard, Intents, and Bitmaps via `PlatformUtils` and `expect`/`actual`. - * Replaced Android's `Linkify` with a pure Kotlin Regex and `AnnotatedString` solution. - * Ensured all shared UI components rely solely on Compose Multiplatform. - * The remaining work here is mostly validation on additional targets and continued isolation of Android-only framework hooks. - -### Dependency Injection -The project currently uses **Koin Annotations**. -* **Current State:** `core:di` is a KMP module that exposes `javax.inject` annotations (`@Inject`), and the app root still assembles the graph in `AppKoinModule`. -* **Important Update:** The original plan was to keep all DI-dependent components centralized in the `app` module, but the current implementation now includes some Koin `@Module`, `@ComponentScan`, and `@KoinViewModel` usage directly in `commonMain` shared modules. See [`docs/kmp-progress-review-2026.md`](./kmp-progress-review-2026.md) for the current architecture assessment. -* **Accomplished:** We have successfully migrated from Hilt (Dagger) to **Koin 4.x** using the compiler plugin, completely removing Hilt from the project to enable deeper Multiplatform adoption. - -## Best Practices & Guidelines (2026) -When contributing to `core` modules, adhere to the following KMP standards: - -* **No Android Context in `commonMain`:** Never pass `Context`, `Application`, or `Activity` into `commonMain`. Use Dependency Injection to provide platform-specific implementations from `androidMain` or `app`. -* **ViewModels:** Use `androidx.lifecycle.ViewModel` and `viewModelScope` within `commonMain` for platform-agnostic state management. The original target pattern was to keep shared ViewModels DI-agnostic and provide app-level Koin wrappers, but the current codebase now contains some Koin annotations directly in shared modules. Prefer the more framework-light pattern for new code unless there is a clear reason to couple a shared ViewModel to Koin. -* **Testing:** Use pure `kotlin.test` and `MockK` for unit tests in `commonTest`. Avoid `Robolectric` unless explicitly testing an `androidMain` component. Platform-specific unit tests (e.g. for Workers) should be relocated to the `app` module's `test` source set if they depend on Koin components. -* **Resources:** Use Compose Multiplatform Resources (`core:resources`) for all strings and drawables. Never use Android `strings.xml` in `commonMain`. -* **Coroutines & Flows:** Use `StateFlow` and `SharedFlow` for all asynchronous state management across the domain layer. -* **Persistence:** Use `androidx.datastore` for preferences and Room KMP for complex relational data. -* **Dependency Injection:** We use **Koin Annotations + K2 Compiler Plugin**. Per 2026 KMP industry standards, it is recommended to push Koin `@Module`, `@ComponentScan`, and `@KoinViewModel` annotations into `commonMain`. This encapsulates dependency graphs per feature, providing a Hilt-like experience (compile-time validation) while remaining fully multiplatform-compatible. - ---- -*Document refreshed on 2026-03-10 as a historical companion to `docs/kmp-progress-review-2026.md`.* diff --git a/docs/archive/kmp-phase3-testing-consolidation.md b/docs/archive/kmp-phase3-testing-consolidation.md deleted file mode 100644 index e1327d398..000000000 --- a/docs/archive/kmp-phase3-testing-consolidation.md +++ /dev/null @@ -1,64 +0,0 @@ -# KMP Phase 3 Testing Consolidation - -> **Date:** March 2026 -> **Status:** Phase 3 Substantially Complete - -This document serves as an archive of the key findings, test coverage metrics, and testing patterns established during the Phase 3 testing consolidation sprint. It synthesizes multiple point-in-time session updates and status reports into a single historical record. - -## 1. Overview and Achievements -The testing consolidation sprint focused on establishing a robust, unified testing infrastructure for the Kotlin Multiplatform (KMP) migration. - -### Key Milestones -- **Core Testing Module:** Created the `core:testing` module to serve as a lightweight, reusable test infrastructure with minimal dependencies. -- **Test Doubles:** Implemented reusable fakes across all modules, completely eliminating circular dependencies. Key fakes include: - - `FakeRadioController` - - `FakeNodeRepository` - - `FakePacketRepository` - - `FakeContactRepository` - - `TestDataFactory` -- **Dependency Consolidation:** Reduced test dependency duplication across 7+ modules by 80%. Unified all feature modules to rely on `core:testing`. - -## 2. Test Coverage Metrics -By the end of Phase 3, test coverage expanded significantly from basic bootstrap tests to comprehensive integration and error handling tests. - -**Total Tests Created: 80** -- **Bootstrap Tests:** 6 (Establishing ViewModel initialization and state flows) -- **Integration Tests:** 45 (Multi-component interactions, scenarios, and feature flows) -- **Error Handling Tests:** 29 (Failure recovery, edge cases, and disconnections) - -**Coverage Breakdown by Feature:** -- `feature:messaging`: 18 tests -- `feature:node`: 18 tests -- `feature:settings`: 19 tests -- `feature:intro`: 9 tests -- `feature:firmware`: 10 tests -- `feature:map`: 6 tests - -**Build Quality:** -- Compilation Success: 100% across all JVM and Android targets. -- Test Failures: 0 -- Regressions: 0 - -## 3. Established Testing Patterns -The sprint successfully codified three primary testing patterns to be used by all developers moving forward: - -1. **Bootstrap Tests:** - - Demonstrate basic feature initialization. - - Verify ViewModel creation, state flow access, and repository integration. - - Use real fakes (`FakeNodeRepository`, `FakeRadioController`) from the start. - -2. **Integration Tests:** - - Test multi-component interactions and end-to-end feature flows. - - Scenarios include: message sending flows, node discovery and management, settings persistence, feature navigation, device positioning, and firmware updates. - -3. **Error Handling Tests:** - - Explicitly test failure scenarios and recovery mechanisms. - - Scenarios include: disconnection handling, nonexistent resource operations, connection state transitions, large dataset handling, concurrent operations, and recovery after failures. - -## 4. Architectural Impact -- **Clean Dependency Graph:** The testing infrastructure is strictly isolated to `commonTest` source sets. `core:testing` depends only on lightweight modules (`core:model`, `core:repository`) preventing transitive dependency bloat during tests. -- **KMP Purity:** Tests are completely agnostic to Android framework dependencies (no `java.*` or `android.*` in test code). All tests are fully compatible with current JVM targets and future iOS targets. -- **Fixed Domain Compilation:** Resolved pre-existing compilation issues in `core:domain` tests related to `kotlin-test` library exports and implicit JUnit conflicts. - -## 5. Next Steps Post-Phase 3 -With the testing foundation fully established and verified, the next phase of the KMP migration (Phase 4) focuses on completing the Desktop feature wiring and non-Android target exploration, confident that the shared business logic is strictly verified by this comprehensive test suite. \ No newline at end of file diff --git a/docs/archive/kmp-progress-review-2026.md b/docs/archive/kmp-progress-review-2026.md deleted file mode 100644 index 2ce52744b..000000000 --- a/docs/archive/kmp-progress-review-2026.md +++ /dev/null @@ -1,728 +0,0 @@ -# KMP Progress Re-evaluation — March 2026 - -> Snapshot date: 2026-03-10 -> -> This document is an evidence-backed re-baseline of Meshtastic-Android's Kotlin Multiplatform migration progress. It supplements and partially corrects the historical narrative in [`docs/kmp-migration.md`](./kmp-migration.md). - -## Scope - -This review covers: - -- all `core:*` and `feature:*` modules in [`settings.gradle.kts`](../settings.gradle.kts) -- build conventions in [`build-logic/convention`](../build-logic/convention) -- current DI wiring in [`app/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt`](../app/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt) -- current application startup in [`app/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt`](../app/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt) -- local git history through 2026-03-10 -- current dependency state in [`gradle/libs.versions.toml`](../gradle/libs.versions.toml) - ---- - -## Executive summary - -Meshtastic-Android has made **substantial structural KMP progress** very quickly in early 2026. - -The migration is **farther along than a normal Android app**, but **not as far along as the existing migration guide sometimes implies**. - -### Headline assessment - -| Dimension | Status | Assessment | -|---|---:|---| -| Core + feature module structural KMP conversion | **23 / 25** | Strong | -| Core-only structural KMP conversion | **17 / 19** | Strong | -| Feature module structural KMP conversion | **6 / 6** | Excellent | -| Explicit non-Android target declarations | **23 / 25** | Strong — all KMP modules have `jvm()` | -| Android-only blocker modules left | **2** | Clear, bounded | -| Cross-target CI verification | **1 JVM smoke step** | Full coverage — 17 core + 6 feature + desktop:test | - -### Bottom line - -- **If the question is "Have we mostly moved business logic into shared KMP modules?"** → **yes**. -- **If the question is "Could we realistically add iOS/Desktop with limited cleanup?"** → **getting close** — full JVM validation is passing, desktop boots with a Navigation 3 shell using shared routes, real feature screen wiring is next. -- **If the question is "Are we now on the right architecture path?"** → **yes, strongly**. - -### Progress scorecard - -| Area | Score | Notes | -|---|---:|---| -| Shared business/data logic | **8.5 / 10** | `core:data`, `core:domain`, `core:database`, `core:prefs`, `core:network`, `core:repository` are structurally shared | -| Shared feature/UI logic | **9.5 / 10** | All 6 feature modules are KMP with `jvm()` target and compile clean; `feature:node` and `feature:settings` UI fully in `commonMain`; `core:ui` and Navigation 3 are in place | -| Android decoupling | **8.5 / 10** | `commonMain` is clean; 11 passthrough Android ViewModel wrappers eliminated; `BaseUIViewModel` extracted to `core:ui` | -| Multi-target readiness | **8 / 10** | 23/25 modules have JVM target; desktop has Navigation 3 shell with shared routes; TCP transport with `want_config` handshake working; `feature:settings` wired with ~35 real screens on desktop (including 5 desktop-specific config screens); all feature modules validated on JVM | -| DI portability hygiene | **5 / 10** | Koin works, but `commonMain` now contains Koin modules/annotations despite prior architectural guidance | -| CI confidence for future iOS/Desktop | **8.5 / 10** | CI JVM smoke compile covers all 17 core + all 6 feature modules + `desktop:test` | - -```mermaid -pie showData - title Core + Feature module state - "KMP modules" : 23 - "Android-only modules" : 2 -``` - ---- - -## What is genuinely complete - -### 1. The architectural center of gravity has moved into shared modules - -This is the biggest success. - -Evidence in current build files shows these are already on `meshtastic.kmp.library`: - -- `core:ble` -- `core:common` -- `core:data` -- `core:database` -- `core:datastore` -- `core:di` -- `core:domain` -- `core:model` -- `core:navigation` -- `core:network` -- `core:nfc` -- `core:prefs` -- `core:proto` -- `core:repository` -- `core:resources` -- `core:service` -- `core:ui` -- all feature modules: `intro`, `messaging`, `map`, `node`, `settings`, `firmware` - -That is a major milestone. The repo is no longer “Android app with a few shared helpers”; it is now “Android app with a shared KMP core and KMP feature stack.” - -### 2. Shared UI architecture is materially real, not aspirational - -Current evidence supports the following: - -- `core:ui` is KMP via [`core/ui/build.gradle.kts`](../core/ui/build.gradle.kts) — with `commonMain`, `androidMain`, and `jvmMain` source sets -- `core:ui` includes shared `BaseUIViewModel` in `commonMain` and `ConnectionsViewModel` in `commonMain` -- `core:resources` uses Compose Multiplatform resources via [`core/resources/build.gradle.kts`](../core/resources/build.gradle.kts) -- `core:navigation` uses Navigation 3 runtime in `commonMain` via [`core/navigation/build.gradle.kts`](../core/navigation/build.gradle.kts) -- feature modules are KMP Compose modules via their `build.gradle.kts` files -- `feature:node` UI components have been migrated from `androidMain` → `commonMain` -- `feature:settings` UI components have been migrated from `androidMain` → `commonMain` -- `feature:settings` is the first feature **fully wired on desktop** with ~35 real composable screens (including 5 desktop-specific config screens for Device, Position, Network, Security, and ExternalNotification) -- Desktop has a **working TCP transport** (`DesktopRadioInterfaceService`) with auto-reconnect and a **mesh service controller** (`DesktopMeshServiceController`) that orchestrates the full `want_config` handshake - -This is unusually advanced for an Android-first app. - -### 3. The Hilt → Koin migration is complete enough to unblock KMP - -Current app startup and root assembly are clearly Koin-based: - -- [`MeshUtilApplication.kt`](../app/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt) -- [`AppKoinModule.kt`](../app/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt) - -This is strategically important because Hilt would have remained one of the strongest barriers to deeper KMP adoption. - -### 4. The BLE architecture is moving in the correct direction - -The repo's BLE direction is good: - -- `core:ble` is KMP -- Android Nordic dependencies are isolated to `androidMain` in [`core/ble/build.gradle.kts`](../core/ble/build.gradle.kts) -- the repo already adopted an abstraction-first BLE shape instead of leaking vendor APIs through the domain layer - -That makes future alternative platform implementations possible. - ---- - -## What is **not** complete yet - -## 1. The repo is structurally KMP, but not yet truly multi-target - -This is the single most important correction. - -Most KMP modules currently use the Android KMP library plugin and define only an Android target. - -The clearest evidence is in build logic: - -- [`KmpLibraryConventionPlugin.kt`](../build-logic/convention/src/main/kotlin/KmpLibraryConventionPlugin.kt) applies: - - `org.jetbrains.kotlin.multiplatform` - - `com.android.kotlin.multiplatform.library` -- [`KotlinAndroid.kt`](../build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/KotlinAndroid.kt) configures Android KMP targets automatically -- [`core/proto/build.gradle.kts`](../core/proto/build.gradle.kts) explicitly adds `jvm()` -- [`core/common/build.gradle.kts`](../core/common/build.gradle.kts) explicitly adds `jvm()` -- [`core:model/build.gradle.kts`](../core/model/build.gradle.kts) explicitly adds `jvm()` -- [`core:repository/build.gradle.kts`](../core/repository/build.gradle.kts) explicitly adds `jvm()` -- [`core/di/build.gradle.kts`](../core/di/build.gradle.kts) explicitly adds `jvm()` -- [`core/navigation/build.gradle.kts`](../core/navigation/build.gradle.kts) explicitly adds `jvm()` -- [`core/resources/build.gradle.kts`](../core/resources/build.gradle.kts) explicitly adds `jvm()` -- [`core/datastore/build.gradle.kts`](../core/datastore/build.gradle.kts) explicitly adds `jvm()` -- [`core/database/build.gradle.kts`](../core/database/build.gradle.kts) explicitly adds `jvm()` -- [`core/domain/build.gradle.kts`](../core/domain/build.gradle.kts) explicitly adds `jvm()` -- [`core/prefs/build.gradle.kts`](../core/prefs/build.gradle.kts) explicitly adds `jvm()` -- [`core/network/build.gradle.kts`](../core/network/build.gradle.kts) explicitly adds `jvm()` -- [`core/nfc/build.gradle.kts`](../core/nfc/build.gradle.kts) explicitly adds `jvm()` -- [`feature/settings/build.gradle.kts`](../feature/settings/build.gradle.kts) explicitly adds `jvm()` -- [`feature/firmware/build.gradle.kts`](../feature/firmware/build.gradle.kts) explicitly adds `jvm()` -- [`feature/intro/build.gradle.kts`](../feature/intro/build.gradle.kts) explicitly adds `jvm()` -- [`feature/messaging/build.gradle.kts`](../feature/messaging/build.gradle.kts) explicitly adds `jvm()` -- [`feature/map/build.gradle.kts`](../feature/map/build.gradle.kts) explicitly adds `jvm()` -- [`feature/node/build.gradle.kts`](../feature/node/build.gradle.kts) explicitly adds `jvm()` - -So today the repo has: - -- **broad shared source-set adoption** -- **meaningful explicit second-target validation**, with a repo-wide JVM pilot across all current KMP modules - -That means the current state is best described as: - -> **"Android-first KMP with full JVM cross-compilation"** — the entire shared graph (17 core + 6 feature modules) compiles on JVM, desktop boots with a full DI graph, and CI enforces it. - -## 2. Two core modules remain plainly Android-only - -These are the remaining structural holdouts: - -- [`core/api/build.gradle.kts`](../core/api/build.gradle.kts) → `meshtastic.android.library` -- [`core/barcode/build.gradle.kts`](../core/barcode/build.gradle.kts) → `meshtastic.android.library` - -`core:nfc` was previously Android-only but has been converted to a KMP module with its NFC hardware code isolated to `androidMain`. - -CI has also begun to enforce that pilot with a dedicated JVM smoke compile step covering all 17 core + 6 feature modules + `desktop:test` in [`.github/workflows/reusable-check.yml`](../.github/workflows/reusable-check.yml). - -These are not minor details; they sit exactly at the platform edge: - -- AIDL / service API surface -- camera + barcode scanning -- NFC hardware integration - -This is acceptable in the short term, but it means the “full KMP core” is not done. - -## 3. The historical migration narrative overstated `core:api` - -Earlier migration wording grouped `core:service` and `core:api` together as if both had become KMP modules. - -Current code shows a split reality: - -- `core:service` **is** KMP -- `core:api` **is not**; it is still Android-only, which makes sense because AIDL is Android-only - -The accurate statement is: - -> `core:service` is KMP, while `core:api` remains an Android adapter/public integration module. - -## 4. Shared-module DI became a real architecture change during the migration sprint - -Earlier migration guidance aimed to keep DI-dependent components centralized in `app`. - -That is **not how the current codebase ended up**. - -Current codebase evidence: - -- [`core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/di/CoreDomainModule.kt`](../core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/di/CoreDomainModule.kt) contains `@Module` + `@ComponentScan` -- [`feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/di/FeatureMapModule.kt`](../feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/di/FeatureMapModule.kt) contains `@Module` -- [`feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/di/FeatureSettingsModule.kt`](../feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/di/FeatureSettingsModule.kt) contains `@Module` -- [`feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/SharedMapViewModel.kt`](../feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/SharedMapViewModel.kt) contains `@KoinViewModel` - -So the real state is: - -> Koin has been pushed down into shared modules already. - -That is not necessarily wrong, but it is a **material architectural change** from the old migration mandate and should be treated explicitly. - ---- - -## Git-history timeline - -Before the explicit KMP conversion wave in 2026, the repo spent roughly **20+ months** accumulating the architectural preconditions for KMP. - -### Long-runway foundations before explicit KMP - -- **2022-06-11 — `54f611290`**: LocalConfig moved to **DataStore** - - This was an early signal away from Android-only preference plumbing and toward serializable/shared state management. -- **2024-02-06 — `c8f93db00`**: Repository pattern for **NodeDB** - - This started separating storage/service concerns from direct consumers. -- **2024-08-25 — `0b7718f8d`**: Write to proto **DataStore** using dynamic field updates - - Important because it normalized protobuf-backed state handling in a way that later mapped cleanly into shared logic. -- **2024-09-13 — `39a18e641`**: Replace service local node DB with **Room NodeDB** - - A precursor to the later Room KMP move. -- **2024-11-21 — `80f8f2a59`**: Repository-pattern replacement for **AIDL methods** - - Important platform-edge cleanup ahead of any `core:api` / `core:service` separation. -- **2024-11-30 — `716a3f535`**: **NavGraph decoupled** from ViewModel and entity types - - This is classic KMP-enabling work: remove Android-navigation entanglement before trying to share navigation state. -- **2025-04-24 — `5cd3a0229`**: `DeviceHardwareRepository` moved toward **local + network data sources** - - Strengthened repository boundaries and data-source isolation. -- **2025-05-22 — `02bb3f02e`**: Introduce **network module** - - Module boundaries became real rather than conceptual. -- **2025-08-16 — `acc3e3f63`**: **Mesh service bind decoupled** from `MainActivity` - - A high-value Android untangling step before service logic could be shared. -- **2025-08-18 to 2025-08-19 — prefs repo migration sweep** - - This was a major cleanup of app-level preference access into repository abstractions. -- **2025-09-15 to 2025-10-12 — modularization burst** - - `build-logic` modularized, nav routes moved to `:core:navigation`, new `:core:model/:core:navigation/:core:network/:core:prefs` modules added, then `:core:ui`, `:core:service`, `:feature:node`, `:feature:intro`, settings, map, and messaging code were progressively extracted. -- **2025-11-10 — `28590bfcd`**: `:core:strings` became a **Compose Multiplatform** library - - This is one of the clearest pre-KMP waypoints because it introduced shared resource infrastructure ahead of wider KMP conversion. -- **2025-11-15 — `0f8e47538`**: BLE scanning/bonding moved to the **Nordic BLE library** - - A major modernization that later made the BLE abstraction strategy viable. -- **2025-12-17 — `61bc9bfdd`**: `core:common` migrated to **KMP** -- **2025-12-28 — `0776e029f`**: **Timber → Kermit** - - A direct removal of an Android/JVM-centric logging dependency. - -```mermaid -gantt - title Meshtastic Android KMP timeline - dateFormat YYYY-MM-DD - axisFormat %b %d - - section Early runway - DataStore foundations begin :milestone, a1, 2022-06-11, 1d - NodeDB repository pattern :milestone, a2, 2024-02-06, 1d - Proto DataStore dynamic updates :milestone, a3, 2024-08-25, 1d - Room-backed NodeDB service move :milestone, a4, 2024-09-13, 1d - AIDL methods moved behind repositories :milestone, a5, 2024-11-21, 1d - NavGraph decoupled from VM/entities :milestone, a6, 2024-11-30, 1d - - section Modular architecture runway - network module introduced :milestone, b1, 2025-05-22, 1d - Mesh service bind decoupled :milestone, b2, 2025-08-16, 1d - prefs repo migration sweep :active, b3, 2025-08-18, 2025-08-19 - App Intro -> Navigation 3 :milestone, b4, 2025-09-05, 1d - build-logic modularized :milestone, b5, 2025-09-15, 1d - nav routes -> core:navigation :milestone, b6, 2025-09-17, 1d - new core modules land :milestone, b7, 2025-09-19, 1d - core:ui extracted :milestone, b8, 2025-09-25, 1d - core:service extracted :milestone, b9, 2025-09-30, 1d - feature:node extracted :milestone, b10, 2025-10-01, 1d - settings + messaging modularization :active, b11, 2025-10-06, 2025-10-12 - - section KMP enablers - core:strings -> Compose MP :milestone, c1, 2025-11-10, 1d - KMP strings cleanup :milestone, c2, 2025-11-11, 1d - Nordic BLE migration :milestone, c3, 2025-11-15, 1d - Navigation3 stable dep adopted :milestone, c4, 2025-11-19, 1d - DataStore 1.2 adopted :milestone, c5, 2025-11-20, 1d - firmware update module lands :milestone, c6, 2025-11-24, 1d - core:common -> KMP :milestone, c7, 2025-12-17, 1d - Timber -> Kermit :milestone, c8, 2025-12-28, 1d - - section Explicit KMP execution wave - core:api created :milestone, d1, 2026-01-29, 1d - Hilt -> Koin migration wave :active, d2, 2026-02-20, 2026-02-24 - core:data / datastore / database KMP :active, d3, 2026-02-21, 2026-03-03 - repository interfaces to common :milestone, d4, 2026-03-02, 1d - prefs + domain KMP :milestone, d5, 2026-03-05, 1d - network + di + service KMP :milestone, d6, 2026-03-06, 1d - messaging + intro KMP :milestone, d7, 2026-03-06, 1d - settings/node/firmware KMP :active, d8, 2026-03-08, 2026-03-10 - core:ui KMP + Navigation 3 split :milestone, d9, 2026-03-09, 1d -``` - -### Interpreting the timeline - -The earlier version of this review understated how long the repo had been preparing for KMP. - -The better reading is: - -- **2022-2024:** early storage and repository abstraction groundwork -- **2025:** deliberate modularization, decoupling, shared resources, Navigation 3, BLE modernization, and logging abstraction -- **late 2025 to early 2026:** explicit KMP conversion work - -So while the visible conversion burst did happen from **2026-02-20 through 2026-03-10**, it was built on a **much longer, roughly 18–24 month architectural runway**. - -That suggests two things: - -1. the migration momentum is real and recent -2. the team had already been systematically removing Android lock-in well before the KMP label appeared in commit messages -3. the architecture likely still has some “first-pass” decisions that need hardening before declaring the migration mature - ---- - -## Main blockers, ranked - -```mermaid -flowchart TD - A[Full cross-platform readiness] --> B[Wire remaining features on desktop] - A --> C[Finish Android-edge module isolation] - A --> D[Harden DI portability rules] - A --> E[Add iOS CI + real desktop transport] - - B --> B1[feature:node wiring] - B --> B2[feature:messaging wiring] - B --> B3[feature:map desktop provider] - - C --> C1[core:api split remains Android-only] - C --> C2[core:barcode camera stack is Android-only] - C --> C3[core:nfc uses Android NFC APIs] - - D --> D1[Koin annotations live in commonMain] - D --> D2[App-only DI mandate is no longer true] - - E --> E1[No iOS target declarations] - E --> E2[Desktop has TCP transport, serial/MQTT remain] -``` - -### Blocker 1 — ~~No real non-Android target expansion yet~~ → Largely resolved - -JVM target expansion is now complete: all 23 KMP modules (17 core + 6 feature) declare `jvm()` and compile clean on JVM. Desktop boots with a full Koin DI graph and a Navigation 3 shell using shared routes. `feature:settings` is fully wired with ~35 real composable screens on desktop (including 5 desktop-specific config screens). TCP transport is working with full `want_config` handshake. CI enforces this. - -**Remaining:** iOS targets (`iosArm64()`/`iosSimulatorArm64()`) are not yet declared. Map feature still uses placeholder on desktop. Serial/USB and MQTT transports not yet implemented. - -**Impact:** medium-low (was high) - -### Blocker 2 — Android-edge modules are partially resolved - -The remaining Android-only modules have been narrowed: - -- `core:api` bundles Android AIDL concerns directly (intentionally Android-only) -- `core:barcode` bundles camera + scanning + flavor-specific engines in one Android module (shared contract in `core:ui/commonMain`) -- ~~`core:nfc` bundles Android NFC APIs directly~~ → ✅ converted to KMP with shared contract in `core:ui/commonMain` - -**Impact:** medium (was high) - -**Why it matters:** these modules define some of the user-facing input and integration surfaces. - -### Blocker 3 — DI portability discipline drifted during the migration sprint - -The repo originally aimed to keep DI packaging centralized in `app`, but now shared modules include Koin annotations and Koin component scans. - -That may still be workable, but it creates two risks: - -- cross-target packaging/tooling complexity grows inside shared modules -- the documentation and the implementation no longer agree - -**Impact:** medium-high - -**Why it matters:** DI entropy spreads silently and becomes expensive later. - -### Blocker 4 — Platform-heavy integrations still dominate the outer shell - -These are not failures; they are the expected “last 20%” items: - -- BLE vendor SDKs -- DFU/update flows -- map engines -- camera stack -- NFC stack -- WorkManager, widgets, notifications, analytics, Play Services integrations - -**Impact:** medium - -**Why it matters:** the deeper your KMP story goes, the more these must be isolated as adapters instead of mixed into shared logic. - -### Blocker 5 — ~~CI only partially enforces the future architecture~~ → Largely resolved for JVM - -CI JVM smoke compile now covers 23 modules + `desktop:test`. Every KMP module with a `jvm()` target is verified on every PR. - -**Remaining:** No iOS CI target. Desktop runs tests but doesn't verify the app starts or navigates. - -**Impact:** low-medium (was medium) - -Current CI in [`.github/workflows/reusable-check.yml`](../.github/workflows/reusable-check.yml) now runs a JVM smoke compile for the entire KMP graph: all 17 core modules, all 6 feature modules, and `desktop:test`, alongside the Android build, lint, unit-test, and instrumented-test paths. It does **not** yet validate iOS targets. - - `core:domain` - - then likely `core:database` or `core:data`, depending on which layer proves cheaper to isolate - - keep using the pilot to surface shared-contract leaks (for example, database entity types escaping repository APIs) - -This will expose library compatibility gaps quickly without forcing iOS immediately. - -### Phase C — Finish the platform-edge seams - -**Effort:** 1–3 weeks - -Priorities: - -1. split transport-neutral API/service contracts from Android AIDL packaging -2. turn barcode into a shared scan contract + platform camera implementations -3. keep NFC as a platform adapter, but make the interface intentionally shared - -### Phase D — Bring up iOS/Desktop experimentation - -**Effort:** 2–6 weeks depending on scope - -- iOS is the cleaner next target for BLE relevance -- Desktop/JVM is the faster smoke target for compilation discipline -- Web remains longest-tail because of BLE, maps, scanning, and service assumptions - -### Revised completion estimate - -| Lens | Completion | -|---|---:| -| Android-first structural KMP migration | **~97%** | -| Shared business-logic migration | **~93%** | -| Shared feature/UI migration | **~93%** | -| True multi-target readiness | **~72%** | -| End-to-end "add iOS/Desktop without surprises" readiness | **~66%** | - ---- - -## Best-practice review against the 2026 KMP ecosystem - -### Where the repo aligns well with current guidance - -### Strong alignment - -1. **Use KMP for business logic and state, not for every platform concern** - - The repo is doing this well in `core:data`, `core:domain`, `core:repository`, `core:model`, and most features. - -2. **Prefer thin platform adapters over shared platform conditionals** - - BLE direction is good. - - Map providers being pushed to `app` is good. - - `CommonUri` and file-handling abstractions in firmware are good. - -3. **Use Compose Multiplatform resources for shared UI** - - The repo already does this in `core:resources`. - -4. **Keep Android framework imports out of `commonMain`** - - Current grep checks show no direct Android imports in `core/**/src/commonMain` or `feature/**/src/commonMain`. - -5. **Adopt Room KMP and Flow-based state for shared persistence/state** - - Current architecture is aligned here. - -6. **Use Navigation 3 shared backstack state** - - This is one of the repo's most forward-looking choices. - -### Where the repo diverges from the latest best-practice direction - -### ~~Divergence 1~~ — Resolved: KMP modules are now validated on a second target - -All 23 KMP modules declare `jvm()` and compile clean. CI enforces this on every PR. - -### ~~Divergence 2~~ — Resolved: Shared modules use Koin annotations (Standard 2026 KMP Practice) - -The repo uses Koin `@Module`, `@ComponentScan`, and `@KoinViewModel` in `commonMain` modules. While early KMP guidance advised keeping DI isolated to the app layer, by 2026 standards, **this is actually the recommended Koin KMP pattern** for Koin 4.0+. Koin Annotations natively supports module scanning in shared code, neatly encapsulating dependency graphs per feature. - -Meshtastic's current Koin setup is not a "portability tradeoff"—it is a modern, valid KMP architecture. - -### ~~Divergence 3~~ — Resolved: CI now enforces cross-target compilation - -The JVM smoke compile step covers all 23 KMP modules and `desktop:test` on every PR. This is aligned with 2026 KMP best practice. - ---- - -## Dependency review: prerelease and high-risk choices - -Current prerelease entries in [`gradle/libs.versions.toml`](../gradle/libs.versions.toml) deserve explicit policy, not passive inheritance. - -| Dependency | Current | Assessment | Recommendation | -|---|---|---|---| -| Compose Multiplatform | `1.11.0-alpha03` | Required for KMP Adaptive | Do not downgrade; `1.11.0-alpha03` is strictly required to support JetBrains Material 3 Adaptive `1.3.0-alpha05` and Nav3 `1.1.0-alpha03` | -| JetBrains Material 3 Adaptive | `1.3.0-alpha05` (version catalog + desktop) | Available at `1.3.0-alpha05` | ✅ Added to version catalog and desktop module; version-aligned with CMP `1.11.0-alpha03` and Nav3 `1.1.0-alpha03`; see [`docs/kmp-adaptive-compose-evaluation.md`](./kmp-adaptive-compose-evaluation.md) | -| Koin | `4.2.0-RC1` | Reasonable short-term | Keep for now if Navigation 3 + compiler plugin behavior is required; switch to stable `4.2.x` once available | -| JetBrains Lifecycle fork | `2.10.0-alpha08` | Required for KMP | Needed for multiplatform `lifecycle-viewmodel-compose` and `lifecycle-runtime-compose`; track JetBrains releases | -| JetBrains Navigation 3 fork | `1.1.0-alpha03` | Required for KMP | Needed for `navigation3-ui` on non-Android targets; the AndroidX `1.0.x` line is Android-only | -| Dokka | `2.2.0-Beta` | Unnecessary risk | Prefer stable `2.1.0` unless a verified `2.2` feature is needed | -| Wire | `6.0.0-alpha03` | Moderate risk | Keep isolated to `core:proto`; avoid wider adoption until 6.x stabilizes | -| Nordic BLE | `2.0.0-alpha16` | High-value but alpha | Keep behind `core:ble` abstraction only; do not let it leak outward | -| Glance | `1.2.0-rc01` | Low KMP relevance | Fine to keep app-only if needed | -| AndroidX Compose BOM | alpha channel | App-side risk only | Reassess if instability shows up in previews/tests | -| Core location altitude | beta | Low impact | Acceptable if scoped and stable in practice | - -### What the latest release signals suggest - -- **Koin**: current repo version matches the latest GitHub release (`4.2.0-RC1`). This is defensible because it adds Navigation 3 support and compiler-plugin improvements. -- **Compose Multiplatform**: repo uses `1.11.0-alpha03` explicitly because it is the foundational requirement for the JetBrains Material 3 Adaptive multiplatform layout libraries. Do not downgrade until a stable version aligns with the Adaptive layout requirements. -- **Dokka**: repo is on beta while latest stable is `2.1.0`. This is a good downgrade candidate. -- **Nordic BLE**: repo is already on the latest alpha (`2.0.0-alpha16`). Acceptable only because the abstraction boundary is solid. - -### Dependency policy recommendation - -Use this rule: - -- **stable by default** for infrastructure and docs tooling -- **RC only when it directly unlocks needed KMP functionality** -- **alpha only behind hard abstraction seams** - -By that rule: - -- keep **Nordic BLE alpha** short-term -- probably keep **Koin RC** short-term -- strongly consider stabilizing **Dokka** (but keep **Compose Multiplatform** pinned to support KMP Adaptive layouts) - ---- - -## Replacement candidates for Android-blocking dependencies - -### 1. BLE - -### Current state - -- Android implementation depends on Nordic Kotlin BLE -- common abstraction shape is already present - -### Recommendation - -Keep current architecture, but evaluate **Kable** as a future non-Android implementation candidate for desktop/web-oriented expansion. - -### Why - -The current repo already did the hard part: it separated the interface from the implementation. - -### 2. DFU / firmware updates - -### Current state - -- firmware feature is KMP, but Nordic DFU remains Android-side - -### Recommendation - -Do **not** force DFU into shared code prematurely. - -Keep a shared firmware orchestration layer and separate platform update engines. - -### Why - -DFU is highly platform- and vendor-specific. Treat it as an adapter boundary, not a KMP purity target. - -### 3. Maps - -### Current state - -- map feature is KMP -- actual map engines live in the `app` module by flavor - -### Recommendation - -Current direction is correct. If Android+iOS map unification becomes a real product goal, evaluate a **MapLibre-centered** provider strategy. - -### Why - -Google Maps and OSMdroid are not a future-proof shared-map stack. - -### 4. Barcode scanning - -### Current state - -- `core:barcode` remains Android-only due to product flavors (ML Kit / ZXing) and CameraX -- Shared scan contract (`BarcodeScanner` interface + `LocalBarcodeScannerProvider`) is already in `core:ui/commonMain` -- Pure Kotlin utility (`extractWifiCredentials`) has been moved to `core:common/commonMain` - -### Recommendation - -Keep `core:barcode` as an Android platform adapter. The shared contract is already properly abstracted: - -- `BarcodeScanner` interface in `core:ui/commonMain` -- `LocalBarcodeScannerProvider` compositionLocal in `core:ui/commonMain` -- Platform implementations injected via `CompositionLocalProvider` from `app` - -For future platforms (Desktop/iOS), provide alternative scanner implementations (e.g., file-based QR import on Desktop, iOS AVFoundation on iOS) via the existing `LocalBarcodeScannerProvider` pattern. - -### 5. NFC - -### Current state - -- ✅ `core:nfc` has been converted to a KMP module -- Android NFC hardware code (`NfcScannerEffect`) is isolated to `androidMain` -- Shared capability contract (`LocalNfcScannerProvider`) is in `core:ui/commonMain` -- JVM target compiles clean and is included in CI smoke compile - -### Recommendation - -✅ Done. The shared capability contract pattern using `CompositionLocal` (provided by the app layer) is the correct architecture. No further structural work needed unless a non-Android NFC implementation becomes relevant. - -### Why - -NFC support varies too much by platform to justify a premature common implementation. - -### 5. Transport Layer Duplication (TCP & Stream Framing) - -### Current state - -- The Android `app` module implements `TCPInterface.kt`, `StreamInterface.kt`, and `MockInterface.kt` using `java.net.Socket` and `java.io.*`. -- The `desktop` module implements `DesktopRadioInterfaceService.kt` which completely duplicates the TCP socket logic and the Meshtastic stream framing protocol (START1/START2 byte parsing). - -### Recommendation - -Extract the stream-framing protocol and TCP socket management into `core:network` or a new `core:transport` module. -- Use `ktor-network` sockets for a pure `commonMain` implementation, OR -- Move the existing `java.net.Socket` implementation to a shared `jvmAndroidMain` or `jvmMain` source set to immediately deduplicate the JVM targets. -- Move `MockInterface` to `commonMain` so all platforms can use it for UI tests or demo modes. - -### 6. Connections UI Fragmentation - -### Current state - -- Android connections UI (`app/ui/connections`) is tightly bound to the app module because `ScannerViewModel` directly mixes BLE, USB, and Android Network Service Discovery (NSD) logic. -- Desktop connections UI (`desktop/.../DesktopConnectionsScreen.kt`) is a completely separate implementation built solely for TCP. - -### Recommendation - -Create a `feature:connections` KMP module. -- Abstract device discovery behind a `DiscoveryRepository` or `DeviceScanner` interface in `commonMain`. -- Move the `ScannerViewModel` to `feature:connections`. -- Inject platform-specific scanners (BLE/USB/NSD for Android, TCP/Serial for Desktop) via DI. -- Unify the UI into a shared `ConnectionsScreen`. - -### 7. Android service API / AIDL - -### Current state - -- `core:api` is Android-only and should remain so at the transport layer - -### Recommendation - -Split any transport-neutral contracts from the Android AIDL packaging if reuse is desired, but keep AIDL itself Android-only. - -### Why - -AIDL is not a KMP concern; it is an Android integration concern. - ---- - -## Recommended next moves - -### Next 30 days - -1. ~~add this review to the KMP docs canon~~ ✅ -2. ~~expand the current JVM smoke pilot beyond `core:repository`~~ ✅ — now covers all 23 modules -3. ~~keep the non-Android CI smoke set and status docs in sync~~ ✅ -4. ~~wire shared Navigation 3 backstack into the desktop app shell~~ ✅ — desktop has NavigationRail + NavDisplay with shared routes from `core:navigation`; JetBrains lifecycle/nav3 forks adopted -5. ~~wire real feature composables into the desktop nav graph (replacing placeholder screens)~~ ✅ — `feature:settings` fully wired (~35 real screens including 5 desktop-specific config screens); `feature:node` wired (real `DesktopNodeListScreen`); `feature:messaging` wired (real `DesktopContactsScreen`); TCP transport with `want_config` handshake working -6. ~~evaluate replacing real Room KMP database and DataStore in desktop (graduating from no-op stubs)~~ in progress -7. ~~add JetBrains Material 3 Adaptive `1.3.0-alpha05` to version catalog and desktop module~~ ✅ — deps added and desktop compile verified; see [`docs/kmp-adaptive-compose-evaluation.md`](./kmp-adaptive-compose-evaluation.md) -8. migrate `AdaptiveContactsScreen` and node adaptive scaffold to `commonMain` using JetBrains adaptive deps (Phase 2-3 in evaluation doc) -9. ~~fill remaining placeholder settings sub-screens~~ ✅ — 5 desktop-specific config screens created (Device, Position, Network, Security, ExtNotification); only Debug Panel and About remain as placeholders -10. wire serial/USB transport for direct radio connection on Desktop -11. wire MQTT transport for cloud relay operation -12. ~~**Abstract the "Holdout" Modules:**~~ Partially done — `core:nfc` converted to KMP with Android NFC code in `androidMain`. Pure `extractWifiCredentials()` utility moved from `core:barcode` to `core:common`. `core:barcode` remains Android-only due to product flavors (ML Kit / ZXing) and CameraX dependencies; its shared contract (`BarcodeScanner` interface + `LocalBarcodeScannerProvider`) already lives in `core:ui/commonMain`. -13. **Turn on iOS Compilation in CI:** Add `iosArm64()` and `iosSimulatorArm64()` targets to KMP convention plugins and CI to catch strict memory/concurrency bugs at compile time. -14. **Dependency Tracking:** Track stable releases for currently required alpha/RC dependencies (Compose 1.11.0-alpha03, Koin 4.2.0-RC1). Do not downgrade these prematurely, as they specifically enable critical KMP features (JetBrains Material 3 Adaptive layouts, Navigation 3, Koin K2 Compiler Plugin). - -### Next 60 days - -1. **Deduplicate TCP & Stream Transport:** Move the TCP socket and START1/START2 stream-framing protocol out of `app` and `desktop` into a shared `core:network` or `core:transport` module using Ktor Network or `jvmMain`. -2. **Unify Connections UI:** Create `feature:connections`, abstract device discovery into a shared interface, and unify the Android and Desktop connection screens. -3. split `core:api` narrative into "shared service core" vs "Android adapter API" -4. ~~define shared contracts for barcode and NFC boundaries~~ ✅ — `BarcodeScanner` + `LocalBarcodeScannerProvider` + `LocalNfcScannerProvider` already in `core:ui/commonMain`; `core:nfc` converted to KMP -3. ~~wire desktop TCP transport for radio connectivity~~ ✅ — wire remaining serial/USB transport -4. decide whether Koin-in-`commonMain` is the long-term architecture or a temporary migration convenience -5. add `feature:map` dependency to desktop (MapLibre evaluation for cross-platform maps) - -### Next 90 days - -1. bring up a small iOS proof target (start with `iosArm64()/iosSimulatorArm64()` declarations) -2. stabilize dependency policy around prerelease libraries -3. publish a living module maturity dashboard - ---- - -## Recommended canonical wording - -If you want one sentence that is accurate today, use this: - -> Meshtastic-Android has completed its **Android-first structural KMP migration** across core logic and feature modules, with **full JVM cross-compilation validated in CI** for all 23 KMP modules. The desktop target has a **Navigation 3 shell with shared routes**, **TCP transport with full `want_config` handshake**, and **`feature:settings` fully wired with ~35 real composable screens** (including 5 desktop-specific config screens), using JetBrains multiplatform forks of lifecycle and navigation3 libraries. Eleven passthrough Android ViewModel wrappers have been eliminated, and both `feature:node` and `feature:settings` UI have been migrated to `commonMain`. The remaining work for true multi-platform delivery centers on **serial/MQTT transport layers**, **chart-based metric screens**, and completing **platform-edge abstraction** for barcode scanning. - ---- - -## References - -### Repository evidence - -- [`docs/kmp-migration.md`](./kmp-migration.md) -- [`docs/koin-migration-plan.md`](./koin-migration-plan.md) -- [`docs/ble-kmp-abstraction-plan.md`](./ble-kmp-abstraction-plan.md) -- [`gradle/libs.versions.toml`](../gradle/libs.versions.toml) -- [`build-logic/convention/src/main/kotlin/KmpLibraryConventionPlugin.kt`](../build-logic/convention/src/main/kotlin/KmpLibraryConventionPlugin.kt) -- [`build-logic/convention/src/main/kotlin/KmpLibraryComposeConventionPlugin.kt`](../build-logic/convention/src/main/kotlin/KmpLibraryComposeConventionPlugin.kt) -- [`build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/KotlinAndroid.kt`](../build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/KotlinAndroid.kt) -- [`.github/workflows/reusable-check.yml`](../.github/workflows/reusable-check.yml) - -### Official ecosystem references reviewed for this snapshot - -- Kotlin Multiplatform docs: -- Android KMP guidance: -- Compose Multiplatform + Jetpack Compose: -- Koin Multiplatform docs: -- AndroidX Room release notes: -- Ktor client docs: - -For raw evidence tables, see [`docs/kmp-progress-review-evidence.md`](./kmp-progress-review-evidence.md). - diff --git a/docs/archive/kmp-progress-review-evidence.md b/docs/archive/kmp-progress-review-evidence.md deleted file mode 100644 index 40528f91c..000000000 --- a/docs/archive/kmp-progress-review-evidence.md +++ /dev/null @@ -1,222 +0,0 @@ -# KMP Progress Review — Evidence Appendix - -This appendix records the concrete repo evidence behind [`docs/kmp-progress-review-2026.md`](./kmp-progress-review-2026.md). - -## Module inventory - -### Core modules - -| Module | Build plugin state | Current reality | Key evidence | -|---|---|---|---| -| `core:api` | Android library | **Android-only** | [`core/api/build.gradle.kts`](../core/api/build.gradle.kts) | -| `core:barcode` | Android library + compose + flavors | **Android-only** | [`core/barcode/build.gradle.kts`](../core/barcode/build.gradle.kts) | -| `core:ble` | KMP library | **KMP with explicit `jvm()`** | [`core/ble/build.gradle.kts`](../core/ble/build.gradle.kts) | -| `core:common` | KMP library | **KMP with explicit `jvm()`, `jvmAndroidMain` source set** | [`core/common/build.gradle.kts`](../core/common/build.gradle.kts) | -| `core:data` | KMP library | **KMP with explicit `jvm()`** | [`core/data/build.gradle.kts`](../core/data/build.gradle.kts) | -| `core:database` | KMP library | **KMP with explicit `jvm()`** | [`core/database/build.gradle.kts`](../core/database/build.gradle.kts) | -| `core:datastore` | KMP library | **KMP with explicit `jvm()`** | [`core/datastore/build.gradle.kts`](../core/datastore/build.gradle.kts) | -| `core:di` | KMP library | **KMP with explicit `jvm()`** | [`core/di/build.gradle.kts`](../core/di/build.gradle.kts) | -| `core:domain` | KMP library | **KMP with explicit `jvm()`** | [`core/domain/build.gradle.kts`](../core/domain/build.gradle.kts) | -| `core:model` | KMP library | **KMP with explicit `jvm()`, `jvmAndroidMain` source set, published** | [`core/model/build.gradle.kts`](../core/model/build.gradle.kts) | -| `core:navigation` | KMP library | **KMP with explicit `jvm()`** | [`core/navigation/build.gradle.kts`](../core/navigation/build.gradle.kts) | -| `core:network` | KMP library | **KMP with explicit `jvm()`** | [`core/network/build.gradle.kts`](../core/network/build.gradle.kts) | -| `core:nfc` | Android library + compose | **Android-only** | [`core/nfc/build.gradle.kts`](../core/nfc/build.gradle.kts) | -| `core:prefs` | KMP library | **KMP with explicit `jvm()`** | [`core/prefs/build.gradle.kts`](../core/prefs/build.gradle.kts) | -| `core:proto` | KMP library | **KMP with explicit `jvm()`** | [`core/proto/build.gradle.kts`](../core/proto/build.gradle.kts) | -| `core:repository` | KMP library | **KMP with explicit `jvm()`** | [`core/repository/build.gradle.kts`](../core/repository/build.gradle.kts) | -| `core:resources` | KMP library + compose | **KMP with explicit `jvm()`** | [`core/resources/build.gradle.kts`](../core/resources/build.gradle.kts) | -| `core:service` | KMP library | **KMP with explicit `jvm()`** | [`core/service/build.gradle.kts`](../core/service/build.gradle.kts) | -| `core:ui` | KMP library + compose | **KMP with explicit `jvm()`, `jvmMain` actuals** | [`core/ui/build.gradle.kts`](../core/ui/build.gradle.kts) | - -### Feature modules - -| Module | Build plugin state | Current reality | Key evidence | -|---|---|---|---| -| `feature:intro` | KMP library + compose | **KMP with explicit `jvm()`** | [`feature/intro/build.gradle.kts`](../feature/intro/build.gradle.kts) | -| `feature:messaging` | KMP library + compose | **KMP with explicit `jvm()`** | [`feature/messaging/build.gradle.kts`](../feature/messaging/build.gradle.kts) | -| `feature:map` | KMP library + compose | **KMP with explicit `jvm()`** | [`feature/map/build.gradle.kts`](../feature/map/build.gradle.kts) | -| `feature:node` | KMP library + compose | **KMP with explicit `jvm()`, UI in `commonMain`** | [`feature/node/build.gradle.kts`](../feature/node/build.gradle.kts) | -| `feature:settings` | KMP library + compose | **KMP with explicit `jvm()`, UI in `commonMain`, wired on Desktop** | [`feature/settings/build.gradle.kts`](../feature/settings/build.gradle.kts) | -| `feature:firmware` | KMP library + compose | **KMP with explicit `jvm()`** | [`feature/firmware/build.gradle.kts`](../feature/firmware/build.gradle.kts) | - -### Inventory totals - -- Core modules: **19** -- Feature modules: **6** -- KMP modules across core + feature: **22 / 25** -- Android-only modules across core + feature: **3 / 25** -- Modules with explicit non-Android target declarations: **22 / 25** (all KMP modules declare `jvm()`) -- Modules with `jvmMain` source sets (hand-written actuals): `core:common` (4 files), `core:model` (via `jvmAndroidMain`, 3 files), `core:repository` (1 file — `Location.kt`), `core:ui` (6 files — QR, clipboard, HTML, platform utils, time tick, dynamic color) - ---- - -## Build-logic evidence - -### KMP convention setup - -- [`KmpLibraryConventionPlugin.kt`](../build-logic/convention/src/main/kotlin/KmpLibraryConventionPlugin.kt) applies: - - `org.jetbrains.kotlin.multiplatform` - - `com.android.kotlin.multiplatform.library` -- [`KmpLibraryComposeConventionPlugin.kt`](../build-logic/convention/src/main/kotlin/KmpLibraryComposeConventionPlugin.kt) adds Compose Multiplatform runtime/resources to `commonMain` -- [`KotlinAndroid.kt`](../build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/KotlinAndroid.kt) configures the Android KMP target and general Kotlin compiler options - -### Important implication - -The repo has standardized on the **Android KMP library path** for shared modules, but does **not** yet automatically add a second target like `jvm()` or `ios*()`. - ---- - -## Historical documentation gaps this review corrects - -| Topic | Historical narrative gap | Current code reality | Evidence | -|---|---|---|---| -| `core:api` | earlier migration wording grouped `core:service` and `core:api` together as KMP | `core:service` is KMP, `core:api` is still Android-only | [`docs/kmp-migration.md`](./kmp-migration.md), [`core/api/build.gradle.kts`](../core/api/build.gradle.kts), [`core/service/build.gradle.kts`](../core/service/build.gradle.kts) | -| DI centralization | original plan kept DI-dependent components in `app` | several `commonMain` modules contain Koin `@Module`, `@ComponentScan`, and `@KoinViewModel` | [`feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/SharedMapViewModel.kt`](../feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/SharedMapViewModel.kt), [`core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/di/CoreDomainModule.kt`](../core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/di/CoreDomainModule.kt) | -| Cross-platform readiness impression | early migration narrative emphasized Desktop/iOS end goals more than active target verification | the repo now has a small JVM pilot (`core:proto`, `core:common`, `core:model`, `core:repository`, `core:di`, `core:navigation`, `core:resources`, `core:datastore`) rather than only a single explicitly validated second target | broad scan of module `build.gradle.kts` files | - ---- - -## Git history milestones used for the timeline - -These were extracted from local git history on 2026-03-10. - -| Date | Commit | Theme | Milestone | Why it mattered | -|---|---|---|---|---| -| 2022-06-11 | `54f611290` | storage | create LocalConfig DataStore | Early shift away from raw app-only preference handling | -| 2024-02-06 | `c8f93db00` | repositories | implement repository pattern for `NodeDB` | Began decoupling data access from service/UI consumers | -| 2024-08-25 | `0b7718f8d` | storage | write to proto DataStore using dynamic field updates | Normalized protobuf-backed state management | -| 2024-09-13 | `39a18e641` | database | replace service local node db with Room NodeDB | Precursor to later Room KMP adoption | -| 2024-11-21 | `80f8f2a59` | api/service | implement repository pattern replacement for AIDL methods | Reduced direct platform/service coupling at the API edge | -| 2024-11-30 | `716a3f535` | navigation | decouple `NavGraph` from ViewModel and NodeEntity | Important cleanup before shared navigation state | -| 2025-04-24 | `5cd3a0229` | repositories | `DeviceHardwareRepository` to local + network data sources | Clearer data-source boundaries | -| 2025-05-22 | `02bb3f02e` | modularization | introduce network module | Early module extraction toward sharable layers | -| 2025-08-16 | `acc3e3f63` | service decoupling | decouple mesh service bind from `MainActivity` | Removed a high-value Android lifecycle coupling | -| 2025-08-18 | `a46065865` | prefs/repositories | add prefs repos and DI providers | Started the broader prefs-to-repository sweep | -| 2025-08-19 | `c913bb047` | prefs/repositories | migrate remaining prefs usages to repo | Consolidated state access behind repository abstractions | -| 2025-09-05 | `4ab588cda` | navigation | Migrate App Intro to Navigation 3 | First major Navigation 3 adoption waypoint | -| 2025-09-15 | `22a5521b9` | build logic | modularize `build-logic` | Strengthened convention-based architecture for later KMP rollout | -| 2025-09-17 | `7afab1601` | modularization | move nav routes to new `:navigation` project module | Formalized navigation as sharable architecture state | -| 2025-09-19 | `0d2c1f151` | modularization | new core modules for `:model`, `:navigation`, `:network`, `:prefs` | One of the clearest runway commits toward KMP | -| 2025-09-25 | `c5360086b` | modularization | add `:core:ui` | Created a natural shared UI landing zone | -| 2025-09-30 | `db2ef75e0` | modularization | add `:core:service` | Separated service logic from app shell concerns | -| 2025-10-01 | `d553cdfee` | modularization | add `:feature:node` | Started feature-level module extraction | -| 2025-10-06 | `95ec4877d` | modularization | modularize settings code | Continued decomposition of app screens into sharable feature modules | -| 2025-10-12 | `886e9cfed` | modularization | modularize messaging code | Another major feature extraction step | -| 2025-11-10 | `28590bfcd` | resources | make `:core:strings` a Compose Multiplatform library | Introduced shared Compose resource infrastructure | -| 2025-11-11 | `57ef889ca` | resources | Kmp strings cleanup | Follow-through cleanup to make shared resources practical | -| 2025-11-15 | `0f8e47538` | BLE | migrate to Nordic BLE Library for scanning and bonding | Modernized BLE stack before abstracting it for KMP | -| 2025-11-19 | `295753d97` | navigation | update `navigation3-runtime` to `1.0.0` | Stabilized the shared-navigation direction | -| 2025-11-20 | `a2285a87a` | storage | update androidx datastore to `1.2.0` | Kept a key KMP-friendly persistence layer current | -| 2025-11-24 | `4b93065c7` | firmware | add firmware update module | Created a distinct module later migrated to KMP | -| 2025-12-17 | `61bc9bfdd` | explicit KMP | `core/common` migrated to KMP | First strong shared-foundation KMP conversion milestone | -| 2025-12-28 | `0776e029f` | logging | replace Timber with Kermit | Removed a non-KMP logging dependency | -| 2026-01-29 | `15760da07` | modularization/public api | create `core:api` module and publishing | Clarified Android API surface vs shared core artifacts | -| 2026-02-20 | `ff3f44318` | DI + explicit KMP | Hilt → Koin and `core:model` KMP pivot | Unblocked broad KMP expansion across modules | -| 2026-02-21 | `8a3d82ca7` | explicit KMP | `core:network` + `core:prefs` to KMP | Shared transport and preference abstractions moved into KMP | -| 2026-02-21 | `8a3c83ebf` | explicit KMP | `core:database` Room KMP structure | Shared persistence layer became materially multiplatform-ready | -| 2026-02-21 | `cd8e32ebf` | explicit KMP | `core:data` to KMP | Concrete repositories moved into shared source sets | -| 2026-02-21 | `3157bdd7d` | explicit KMP | `core:datastore` to KMP | Shared preferences/storage infrastructure consolidated | -| 2026-02-21 | `727f48b45` | explicit KMP | `core:ui` to KMP | Shared UI layer became real instead of aspirational | -| 2026-03-02 | `f3cddf5a1` | explicit KMP | repository interfaces/models to common KMP modules | Finished pushing core contracts into shared code | -| 2026-03-03 | `6a858acb4` | explicit KMP | `core:database` to Room Kotlin Multiplatform | Reinforced the Room KMP migration | -| 2026-03-05 | `b9b68d277` | explicit KMP | preferences to DataStore, `core:domain` decoupling | Reduced Android/JVM-specific domain assumptions | -| 2026-03-06 | `8b13b947a` | explicit KMP | `core:service` to KMP | Shared service orchestration moved out of app-only code | -| 2026-03-06 | `62b5f127d` | explicit KMP | `feature:messaging` to KMP | Shared feature migration accelerated | -| 2026-03-06 | `4089ba913` | explicit KMP | `feature:intro` to KMP | Same pattern extended to another feature | -| 2026-03-08 | `4e3bb4a83` | explicit KMP | `feature:node` and `feature:settings` to KMP | Major user-facing features moved into shared modules | -| 2026-03-08 | `50bcefd31` | explicit KMP | `feature:firmware` to KMP | Firmware orchestration became largely shareable | -| 2026-03-09 | `875cf1cff` | DI + explicit KMP | Hilt → Koin finalized and KMP common modules expanded | Completed the DI pivot that supports current KMP architecture | -| 2026-03-09 | `4320c6bd4` | navigation | Navigation 3 split | Cemented shared backstack/state direction | -| 2026-03-09 | `fb0a9a180` | explicit KMP | `core:ui` KMP follow-up | Stabilization after migration | -| 2026-03-10 | `5ff6b1ff8` | docs | docs mark `feature:node` UI migration completed | Documentation catch-up after the migration burst | -| 2026-03-10 | `6f2b1a781` | desktop | Navigation 3 shell for Desktop with shared routes and `feature:settings` wired | First real feature wired on desktop — ~30 composable screens | - ---- - -## DI evidence - -### App root assembly - -- [`AppKoinModule.kt`](../app/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt) includes shared Koin modules from: - - `core:*` - - `feature:*` - - `app` -- [`MeshUtilApplication.kt`](../app/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt) starts Koin directly via `startKoin { ... modules(AppKoinModule().module()) }` - -### Shared-module Koin evidence - -| Location | Evidence | -|---|---| -| [`core/domain/.../CoreDomainModule.kt`](../core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/di/CoreDomainModule.kt) | `@Module` + `@ComponentScan` in `commonMain` | -| [`feature/map/.../FeatureMapModule.kt`](../feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/di/FeatureMapModule.kt) | `@Module` in `commonMain` | -| [`feature/settings/.../FeatureSettingsModule.kt`](../feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/di/FeatureSettingsModule.kt) | `@Module` in `commonMain` | -| [`feature/map/.../SharedMapViewModel.kt`](../feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/SharedMapViewModel.kt) | `@KoinViewModel` in `commonMain` | - -### Conclusion - -The codebase has functionally adopted **shared-module Koin annotations** even though the old guide still describes an `app`-centralized DI policy. Additionally, 11 passthrough Android ViewModel wrappers have been eliminated — shared ViewModels are now resolved directly via `koinViewModel()` in both the Android app navigation and the desktop nav graph. - ---- - -## CommonMain Android-import check - -A grep scan across: - -- `core/**/src/commonMain/**/*.kt` -- `feature/**/src/commonMain/**/*.kt` - -found **no direct `import android.*` lines**. - -This is one of the strongest signals that the migration is architecturally healthy. - ---- - -## CI evidence - -Current reusable CI workflow: - -- [`.github/workflows/reusable-check.yml`](../.github/workflows/reusable-check.yml) - -What it verifies today: - -- `spotlessCheck` -- `detekt` -- JVM smoke compile: all 16 core KMP modules + all 6 feature modules: - `:core:proto:compileKotlinJvm :core:common:compileKotlinJvm :core:model:compileKotlinJvm :core:repository:compileKotlinJvm :core:di:compileKotlinJvm :core:navigation:compileKotlinJvm :core:resources:compileKotlinJvm :core:datastore:compileKotlinJvm :core:database:compileKotlinJvm :core:domain:compileKotlinJvm :core:prefs:compileKotlinJvm :core:network:compileKotlinJvm :core:data:compileKotlinJvm :core:ble:compileKotlinJvm :core:service:compileKotlinJvm :core:ui:compileKotlinJvm :feature:intro:compileKotlinJvm :feature:messaging:compileKotlinJvm :feature:map:compileKotlinJvm :feature:node:compileKotlinJvm :feature:settings:compileKotlinJvm :feature:firmware:compileKotlinJvm` -- `:desktop:test` -- Android assemble -- Android unit tests -- Android instrumented tests -- Kover reports - -What it does **not** verify: - -- iOS target compilation -- Desktop application startup or navigation integration tests -| Wire | `6.0.0-alpha03` | alpha | -| Nordic BLE | `2.0.0-alpha16` | alpha | -| AndroidX core location altitude | `1.0.0-beta01` | beta | -| AndroidX Compose BOM | `2026.02.01` alpha BOM channel | alpha | - -### Latest release signals referenced in the main review - -| Dependency | Observed signal | -|---|---| -| Koin | Latest GitHub release matches current `4.2.0-RC1` | -| Compose Multiplatform | Latest GitHub stable release observed: `1.10.2` | -| Dokka | Latest GitHub stable release observed: `2.1.0` | -| Nordic BLE | Latest GitHub release matches current `2.0.0-alpha16` | - ---- - -## Best-practice evidence anchors - -The following current ecosystem references were reviewed while producing the main report: - -- Kotlin Multiplatform overview: -- Android KMP guidance: -- Compose Multiplatform + Jetpack Compose guidance: -- Koin KMP reference: -- AndroidX Room release notes: -- Ktor client guidance: - diff --git a/docs/archive/koin-migration-plan.md b/docs/archive/koin-migration-plan.md deleted file mode 100644 index 442e68c22..000000000 --- a/docs/archive/koin-migration-plan.md +++ /dev/null @@ -1,122 +0,0 @@ -# Koin Migration Implementation Plan (Annotations & K2 Compiler Plugin) - -This document outlines the meticulous, step-by-step strategy for migrating Meshtastic-Android from Hilt (Dagger) to **Koin with Annotations**. This approach leverages the new native **Koin Compiler Plugin (K2)** to automatically generate Koin DSL at compile time, providing a developer experience nearly identical to Hilt/Dagger but with pure, boilerplate-free KMP compatibility. We are targeting Koin 4.2.0-RC1+ and the Koin Compiler Plugin for maximum Compose Multiplatform support and optimal build performance. - -## 1. Goal & Objectives -- **Remove Hilt/Dagger completely** from the project. -- **Adopt Koin Annotations** for declarative, compile-time verified DI using the native K2 Compiler Plugin. -- **Eliminate Android*ViewModel Wrappers** by injecting KMP ViewModels (`@KoinViewModel`) directly. -- **Improve Build Times** by replacing Dagger KAPT/KSP with the lightweight, native Koin Compiler Plugin. -- **Maintain Incremental Progress** using the Strangler Fig Pattern. - -## 2. Phase 1: Infrastructure Setup -**Objective:** Add Koin Annotations and Koin Compiler Plugin to the build system. - -1. **Add Dependencies** in `gradle/libs.versions.toml`: - - Ensure versions are at least Koin `4.2.0-RC1` (or stable when available) and Koin Compiler Plugin. - - Dependencies: `koin-core`, `koin-android`, `koin-annotations`, `koin-compose-viewmodel`. - - Plugins: `io.insert-koin.compiler.plugin`. -2. **Configure Root Compiler Plugin** in `build.gradle.kts` (root or build-logic): - - Ensure the plugin is available and applied in KMP modules (`alias(libs.plugins.koin.compiler)`). -3. **Setup Koin Application** in `MeshUtilApplication.kt`: - - Initialize Koin with `startKoin { androidContext(this@MeshUtilApplication); modules(AppModule().module) }`. - - *Note:* `.module` is an extension property automatically generated by the compiler plugin for classes annotated with `@Module`. - - *Note:* In Koin 4.1+, standard native Context handling is unified, making explicit `androidContext` passing into KMP modules significantly simpler than in Koin 3.x. - -## 3. Phase 2: Core Modules Migration (`core:*`) -**Objective:** Replace Hilt modules with Koin Annotated modules. - -1. **Annotate Classes**: - - Replace `@Singleton` + `@Inject constructor` with just `@Single`. - - Koin automatically binds implementations to their interfaces if it's the only interface implemented. - - Standard constructor injection requires no explicit `@Inject` annotations—the compiler auto-detects constructors from the class-level scope annotation (`@Single`, `@Factory`, etc.). -2. **Define Koin Modules (`expect` / `actual` Pattern)**: - - KMP Best Practice: In `commonMain`, declare an `expect val platformModule: Module`. - - In each platform source set (e.g., `androidMain`, `iosMain`), implement this with `actual val platformModule: Module = module { includes(AndroidModule().module) }`. - - Use `@Module` and `@ComponentScan("org.meshtastic.core.module")` on these platform-specific classes so the plugin builds the platform dependency graphs correctly. -3. **Bridge Hilt/Koin (Incremental Step)**: - - If a Hilt class needs a Koin dependency, provide a temporary Hilt `@Provides` that fetches from `GlobalContext.get().get()`. -4. **`expect` / `actual` Class Injection**: - - When you have an `expect class` that you want to inject, do *not* annotate the `expect` declaration. - - Instead, annotate each platform's `actual class` with `@Single` or `@Factory`. The compiler plugin will automatically compile-time link the injected interface to the correct platform implementation. - -## 4. Phase 3: Feature & ViewModel Migration [COMPLETED] -**Objective:** Migrate ViewModels and eliminate Android-specific wrappers using latest mapping features. - -1. **Migrate ViewModels**: - - Replace `@HiltViewModel` with `@KoinViewModel`. - - Move ViewModels to `commonMain` where applicable to share logic across targets. -2. **Update Compose Navigation**: - - Replace `hiltViewModel()` with `koinViewModel()` in `app/navigation/`. - - *Nitty-Gritty:* If using nested Jetpack Navigation graphs, leverage Koin 4.1's `koinNavViewModel()` to replicate Hilt's graph-scoped ViewModels securely. -3. **Compose Previews Integration (Experimental)**: - - Replace dummy Hilt setups in `@Preview` with Koin's `KoinApplicationPreview` to inject dummy modules specifically for rendering Compose previews. -4. **Purge Wrappers**: - - Delete `AndroidMetricsViewModel`, `AndroidRadioConfigViewModel`, etc. - -## 5. Phase 4: Advanced Edge Cases (`@AssistedInject` & WorkManager) -**Objective:** Address Dagger-specific advanced injection patterns. - -1. **WorkManager & `@HiltWorker`**: - - Add `io.insert-koin:koin-androidx-workmanager` to dependencies. - - Replace `@HiltWorker` and `@AssistedInject` on Workers with `@KoinWorker`. - - Initialize WorkManager factory in `MeshUtilApplication` via `WorkManagerFactory()`. -2. **`@AssistedInject` (Non-Worker classes)**: - - Meshtastic heavily uses AssistedInject for Radio Interfaces (`NordicBleInterface`, `MockInterface`, etc.). - - Replace `@AssistedInject` with Koin's `@Factory` on the class. - - Replace `@Assisted` parameters in the constructor with `@InjectedParam`. - - In Koin Annotations, when injecting this factory, you pass parameters dynamically: `val radio: RadioInterface = get { parametersOf(address) }`. -3. **Dagger Custom `@Qualifier`s**: - - Project uses many custom qualifiers (e.g., `@UiDataStore`, `@MapDataStore`) for DataStore instances. - - Replace these custom annotations with Koin's `@Named("UiDataStore")`. - - Apply `@Named` to both the provided dependency (e.g., inside the `@Module` function) *and* the constructor parameter where it is injected. -4. **Compiler Plugin Multiplatform Benefit**: - - By using the new `io.insert-koin.compiler.plugin`, we completely bypass the old KSP boilerplate. There is no need for `kspCommonMainMetadata` or complex KSP target wiring in KMP modules. - -## 6. Phase 5: Testing & Final Cleanup -**Objective:** Complete Hilt eradication and verify tests. - -1. **Update Tests**: - - Replace `@HiltAndroidTest` with Koin testing utilities. - - Use `KoinTest` interface and `KoinTestRule` in your Android instrumented tests and Robolectric unit tests to supply mock modules. -2. **Remove Hilt Annotations**: - - Delete `@HiltAndroidApp`, `@AndroidEntryPoint`, `@InstallIn`, etc. -3. **Clean Build Scripts**: - - Remove Hilt plugins and dependencies from all `build.gradle.kts` and `libs.versions.toml`. -4. **Final Verification**: - - Run `./gradlew clean assembleDebug test` to ensure successful compilation and structural integrity. - -## 6. Migration Key mappings (Cheat Sheet) -| Hilt/Dagger | Koin Annotations | -| :--- | :--- | -| `@Singleton class X @Inject constructor(...)` | `@Single class X(...)` | -| `@Module` + `@InstallIn` | `@Module` + `@ComponentScan` | -| `@Provides` | `@Single` or `@Factory` on a module function | -| `@Binds` | Automatic (or `@Single` on implementation) | -| `@HiltViewModel` | `@KoinViewModel` | -| `hiltViewModel()` | `koinViewModel()` or `koinNavViewModel()` | -| `Lazy` | `Lazy` (Native Kotlin) | -| Dummy `@Preview` ViewModels | `KoinApplicationPreview { ... }` | - -## 7. Troubleshooting & Lessons Learned (March 2026) -### Koin K2 Compiler Plugin Signature Collision -During Phase 3, we discovered a bug in the Koin K2 Compiler Plugin (v0.3.0) where multiple `@Single` provider functions in the same module with identical JVM signatures (e.g., several `DataStore` providers taking `(Context, CoroutineScope)`) were incorrectly mapped to the same internal lambda. This caused `ClassCastException` at runtime (e.g., `LocalStats` being cast to `Preferences`). - -**Solution:** Split providers with identical signatures into separate `@Module` classes. This forces the compiler plugin to generate unique mapping classes, preventing the collision. - -### Circular Dependencies in Koin 4.2.0 -True circular dependencies (e.g., `Service -> InterfaceFactory -> Spec -> Factory -> Service`) can cause `StackOverflowError` during graph resolution even with `Lazy` injection if the `Lazy` is accessed too early (e.g., in a coroutine launched from `init`). - -**Solution:** Break cycles by passing dependencies as function parameters instead of constructor parameters where possible (e.g., passing `service` to `InterfaceSpec.createInterface(...)`). - -### Robolectric Tests & KoinApplicationAlreadyStartedException -When running Robolectric tests, `MeshUtilApplication` is recreated for each test. If `startKoin` is called in `onCreate` but not stopped, subsequent tests will fail with `org.koin.core.error.KoinApplicationAlreadyStartedException`. - -**Solution:** Explicitly call `org.koin.core.context.stopKoin()` in the application's `onTerminate` method, which is invoked by Robolectric during teardown. - ---- -**Status:** **Fully Completed & Stable.** -- Hilt completely removed. -- Koin Annotations and K2 Compiler Plugin fully integrated. -- All DataStore and Circular Dependency issues resolved. -- App verified stable on device via Logcat audit. diff --git a/docs/decisions/ble-strategy.md b/docs/decisions/ble-strategy.md index b3d14d705..81ffcdcb3 100644 --- a/docs/decisions/ble-strategy.md +++ b/docs/decisions/ble-strategy.md @@ -24,8 +24,3 @@ However, as Desktop integration advanced, we found the need for a unified BLE tr - **Maximal Code Deduplication:** The BLE implementation is completely shared across Android and Desktop in `core:ble/commonMain`. - **Future-Proofing:** Adding an `iosMain` target in the future will be trivial, as it can leverage the same shared Kable abstractions. - **Lost Nordic Mocks:** Kable lacks the comprehensive mock infrastructure of the Nordic library. Consequently, several complex BLE OTA unit tests had to be deprecated. Re-establishing this test coverage using custom Kable fakes is an ongoing technical debt item. - -## Archive - -- Original Hybrid Analysis: [`archive/ble-kmp-strategy.md`](../archive/ble-kmp-strategy.md) -- Original Abstraction Plan: [`archive/ble-kmp-abstraction-plan.md`](../archive/ble-kmp-abstraction-plan.md) \ No newline at end of file diff --git a/docs/decisions/koin-migration.md b/docs/decisions/koin-migration.md index 9b83eb900..8bd7db7f4 100644 --- a/docs/decisions/koin-migration.md +++ b/docs/decisions/koin-migration.md @@ -31,6 +31,4 @@ Key choices: - Desktop bootstraps its own `DesktopKoinModule` with stubs + real implementations - 11 passthrough Android ViewModel wrappers eliminated -## Archive -Full migration plan: [`archive/koin-migration-plan.md`](../archive/koin-migration-plan.md) diff --git a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ScannerViewModel.kt b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ScannerViewModel.kt index 426e8476d..9a7a5a661 100644 --- a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ScannerViewModel.kt +++ b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ScannerViewModel.kt @@ -77,26 +77,25 @@ open class ScannerViewModel( isBleScanningState.value = true scannedBleDevices.value = emptyMap() - scanJob = - viewModelScope.launch { - try { - bleScanner - .scan( - timeout = kotlin.time.Duration.INFINITE, - serviceUuid = org.meshtastic.core.ble.MeshtasticBleConstants.SERVICE_UUID, - ) - .flowOn(ioDispatcher) - .collect { device -> - if (!scannedBleDevices.value.containsKey(device.address)) { - scannedBleDevices.update { current -> current + (device.address to device) } - } + scanJob = viewModelScope.launch { + try { + bleScanner + .scan( + timeout = kotlin.time.Duration.INFINITE, + serviceUuid = org.meshtastic.core.ble.MeshtasticBleConstants.SERVICE_UUID, + ) + .flowOn(ioDispatcher) + .collect { device -> + if (!scannedBleDevices.value.containsKey(device.address)) { + scannedBleDevices.update { current -> current + (device.address to device) } } - } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { - co.touchlab.kermit.Logger.w(e) { "BLE scan failed" } - } finally { - isBleScanningState.value = false - } + } + } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { + co.touchlab.kermit.Logger.w(e) { "BLE scan failed" } + } finally { + isBleScanningState.value = false } + } } fun stopBleScan() { diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt index 90ff1ff91..03a088e2a 100644 --- a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt +++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt @@ -149,67 +149,66 @@ class FirmwareUpdateViewModel( @Suppress("LongMethod") fun checkForUpdates() { updateJob?.cancel() - updateJob = - viewModelScope.launch { - _state.value = FirmwareUpdateState.Checking - runCatching { - val ourNode = nodeRepository.myNodeInfo.value - val address = radioPrefs.devAddr.value?.drop(1) - if (address == null || ourNode == null) { - _state.value = FirmwareUpdateState.Error(getString(Res.string.firmware_update_no_device)) - return@launch - } - getDeviceHardware(ourNode)?.let { deviceHardware -> - _deviceHardware.value = deviceHardware - _currentFirmwareVersion.value = ourNode.firmwareVersion + updateJob = viewModelScope.launch { + _state.value = FirmwareUpdateState.Checking + runCatching { + val ourNode = nodeRepository.myNodeInfo.value + val address = radioPrefs.devAddr.value?.drop(1) + if (address == null || ourNode == null) { + _state.value = FirmwareUpdateState.Error(getString(Res.string.firmware_update_no_device)) + return@launch + } + getDeviceHardware(ourNode)?.let { deviceHardware -> + _deviceHardware.value = deviceHardware + _currentFirmwareVersion.value = ourNode.firmwareVersion - val releaseFlow = - if (_selectedReleaseType.value == FirmwareReleaseType.LOCAL) { - kotlinx.coroutines.flow.flowOf(null) - } else { - firmwareReleaseRepository.getReleaseFlow(_selectedReleaseType.value) - } - - releaseFlow.collectLatest { release -> - _selectedRelease.value = release - val dismissed = bootloaderWarningDataSource.isDismissed(address) - val firmwareUpdateMethod = - when { - radioPrefs.isSerial() -> { - // ESP32 Serial updates are not supported from the app yet. - if (deviceHardware.isEsp32Arc) { - FirmwareUpdateMethod.Unknown - } else { - FirmwareUpdateMethod.Usb - } - } - - radioPrefs.isBle() -> FirmwareUpdateMethod.Ble - radioPrefs.isTcp() -> FirmwareUpdateMethod.Wifi - else -> FirmwareUpdateMethod.Unknown - } - _state.value = - FirmwareUpdateState.Ready( - release = release, - deviceHardware = deviceHardware, - address = address, - showBootloaderWarning = - deviceHardware.requiresBootloaderUpgradeForOta == true && - !dismissed && - radioPrefs.isBle(), - updateMethod = firmwareUpdateMethod, - currentFirmwareVersion = ourNode.firmwareVersion, - ) + val releaseFlow = + if (_selectedReleaseType.value == FirmwareReleaseType.LOCAL) { + kotlinx.coroutines.flow.flowOf(null) + } else { + firmwareReleaseRepository.getReleaseFlow(_selectedReleaseType.value) } + + releaseFlow.collectLatest { release -> + _selectedRelease.value = release + val dismissed = bootloaderWarningDataSource.isDismissed(address) + val firmwareUpdateMethod = + when { + radioPrefs.isSerial() -> { + // ESP32 Serial updates are not supported from the app yet. + if (deviceHardware.isEsp32Arc) { + FirmwareUpdateMethod.Unknown + } else { + FirmwareUpdateMethod.Usb + } + } + + radioPrefs.isBle() -> FirmwareUpdateMethod.Ble + radioPrefs.isTcp() -> FirmwareUpdateMethod.Wifi + else -> FirmwareUpdateMethod.Unknown + } + _state.value = + FirmwareUpdateState.Ready( + release = release, + deviceHardware = deviceHardware, + address = address, + showBootloaderWarning = + deviceHardware.requiresBootloaderUpgradeForOta == true && + !dismissed && + radioPrefs.isBle(), + updateMethod = firmwareUpdateMethod, + currentFirmwareVersion = ourNode.firmwareVersion, + ) } } - .onFailure { e -> - if (e is CancellationException) throw e - Logger.e(e) { "Error checking for updates" } - val unknownError = getString(Res.string.firmware_update_unknown_error) - _state.value = FirmwareUpdateState.Error(e.message ?: unknownError) - } } + .onFailure { e -> + if (e is CancellationException) throw e + Logger.e(e) { "Error checking for updates" } + val unknownError = getString(Res.string.firmware_update_unknown_error) + _state.value = FirmwareUpdateState.Error(e.message ?: unknownError) + } + } } fun startUpdate() { @@ -220,30 +219,29 @@ class FirmwareUpdateViewModel( viewModelScope.launch { if (checkBatteryLevel()) { updateJob?.cancel() - updateJob = - viewModelScope.launch { - try { - tempFirmwareFile = - firmwareUpdateManager.startUpdate( - release = release, - hardware = currentState.deviceHardware, - address = currentState.address, - updateState = { _state.value = it }, - ) + updateJob = viewModelScope.launch { + try { + tempFirmwareFile = + firmwareUpdateManager.startUpdate( + release = release, + hardware = currentState.deviceHardware, + address = currentState.address, + updateState = { _state.value = it }, + ) - if (_state.value is FirmwareUpdateState.Success) { - verifyUpdateResult(originalDeviceAddress) - } - } catch (e: CancellationException) { - Logger.i { "Firmware update cancelled" } - _state.value = FirmwareUpdateState.Idle - checkForUpdates() - throw e - } catch (e: Exception) { - val failedMsg = getString(Res.string.firmware_update_failed) - _state.value = FirmwareUpdateState.Error(e.message ?: failedMsg) + if (_state.value is FirmwareUpdateState.Success) { + verifyUpdateResult(originalDeviceAddress) } + } catch (e: CancellationException) { + Logger.i { "Firmware update cancelled" } + _state.value = FirmwareUpdateState.Idle + checkForUpdates() + throw e + } catch (e: Exception) { + val failedMsg = getString(Res.string.firmware_update_failed) + _state.value = FirmwareUpdateState.Error(e.message ?: failedMsg) } + } } } } @@ -293,38 +291,36 @@ class FirmwareUpdateViewModel( originalDeviceAddress = currentState.address updateJob?.cancel() - updateJob = - viewModelScope.launch { - try { - val extractingMsg = getString(Res.string.firmware_update_extracting) - _state.value = FirmwareUpdateState.Processing(ProgressState(extractingMsg)) - val extension = if (currentState.updateMethod is FirmwareUpdateMethod.Ble) ".zip" else ".uf2" - val extractedFile = fileHandler.extractFirmware(uri, currentState.deviceHardware, extension) + updateJob = viewModelScope.launch { + try { + val extractingMsg = getString(Res.string.firmware_update_extracting) + _state.value = FirmwareUpdateState.Processing(ProgressState(extractingMsg)) + val extension = if (currentState.updateMethod is FirmwareUpdateMethod.Ble) ".zip" else ".uf2" + val extractedFile = fileHandler.extractFirmware(uri, currentState.deviceHardware, extension) - tempFirmwareFile = extractedFile - val firmwareUri = if (extractedFile != null) CommonUri.parse("file://$extractedFile") else uri + tempFirmwareFile = extractedFile + val firmwareUri = if (extractedFile != null) CommonUri.parse("file://$extractedFile") else uri - tempFirmwareFile = - firmwareUpdateManager.startUpdate( - release = - FirmwareRelease(id = "local", title = "Local File", zipUrl = "", releaseNotes = ""), - hardware = currentState.deviceHardware, - address = currentState.address, - updateState = { _state.value = it }, - firmwareUri = firmwareUri, - ) + tempFirmwareFile = + firmwareUpdateManager.startUpdate( + release = FirmwareRelease(id = "local", title = "Local File", zipUrl = "", releaseNotes = ""), + hardware = currentState.deviceHardware, + address = currentState.address, + updateState = { _state.value = it }, + firmwareUri = firmwareUri, + ) - if (_state.value is FirmwareUpdateState.Success) { - verifyUpdateResult(originalDeviceAddress) - } - } catch (e: CancellationException) { - throw e - } catch (e: Exception) { - Logger.e(e) { "Error starting update from file" } - val failedMsg = getString(Res.string.firmware_update_local_failed) - _state.value = FirmwareUpdateState.Error(e.message ?: failedMsg) + if (_state.value is FirmwareUpdateState.Success) { + verifyUpdateResult(originalDeviceAddress) } + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + Logger.e(e) { "Error starting update from file" } + val failedMsg = getString(Res.string.firmware_update_local_failed) + _state.value = FirmwareUpdateState.Error(e.message ?: failedMsg) } + } } fun dismissBootloaderWarningForCurrentDevice() { diff --git a/feature/intro/src/androidUnitTest/kotlin/org/meshtastic/feature/intro/IntroViewModelTest.kt b/feature/intro/src/androidUnitTest/kotlin/org/meshtastic/feature/intro/IntroViewModelTest.kt deleted file mode 100644 index dfb129543..000000000 --- a/feature/intro/src/androidUnitTest/kotlin/org/meshtastic/feature/intro/IntroViewModelTest.kt +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright (c) 2025-2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.feature.intro - -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotNull -import org.junit.Assert.assertNull -import org.junit.Test - -class IntroViewModelTest { - - @Test - fun `viewModel can be initialized`() { - val viewModel = IntroViewModel() - assertNotNull(viewModel) - } - - @Test - fun `getNextKey returns Bluetooth after Welcome`() { - val viewModel = IntroViewModel() - val next = viewModel.getNextKey(Welcome, false) - assertEquals(Bluetooth, next) - } - - @Test - fun `getNextKey returns Location after Bluetooth`() { - val viewModel = IntroViewModel() - val next = viewModel.getNextKey(Bluetooth, false) - assertEquals(Location, next) - } - - @Test - fun `getNextKey returns Notifications after Location`() { - val viewModel = IntroViewModel() - val next = viewModel.getNextKey(Location, false) - assertEquals(Notifications, next) - } - - @Test - fun `getNextKey returns CriticalAlerts after Notifications if granted`() { - val viewModel = IntroViewModel() - val next = viewModel.getNextKey(Notifications, true) - assertEquals(CriticalAlerts, next) - } - - @Test - fun `getNextKey returns null after Notifications if not granted`() { - val viewModel = IntroViewModel() - val next = viewModel.getNextKey(Notifications, false) - assertNull(next) - } - - @Test - fun `getNextKey returns null after CriticalAlerts`() { - val viewModel = IntroViewModel() - val next = viewModel.getNextKey(CriticalAlerts, false) - assertNull(next) - } -} diff --git a/feature/messaging/src/androidUnitTest/kotlin/org/meshtastic/feature/messaging/HomoglyphCharacterTransformTest.kt b/feature/messaging/src/androidHostTest/kotlin/org/meshtastic/feature/messaging/HomoglyphCharacterTransformTest.kt similarity index 97% rename from feature/messaging/src/androidUnitTest/kotlin/org/meshtastic/feature/messaging/HomoglyphCharacterTransformTest.kt rename to feature/messaging/src/androidHostTest/kotlin/org/meshtastic/feature/messaging/HomoglyphCharacterTransformTest.kt index b5b634d6a..f75031fa8 100644 --- a/feature/messaging/src/androidUnitTest/kotlin/org/meshtastic/feature/messaging/HomoglyphCharacterTransformTest.kt +++ b/feature/messaging/src/androidHostTest/kotlin/org/meshtastic/feature/messaging/HomoglyphCharacterTransformTest.kt @@ -16,10 +16,10 @@ */ package org.meshtastic.feature.messaging -import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue -import org.junit.Test import org.meshtastic.core.common.util.HomoglyphCharacterStringTransformer +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue class HomoglyphCharacterTransformTest { diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt index 1c84c26f1..7c57b46af 100644 --- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt @@ -214,8 +214,9 @@ class MessageViewModel( viewModelScope.launch { sendMessageUseCase.invoke(str, contactKey, replyId) } } - fun sendReaction(emoji: String, replyId: Int, contactKey: String) = - viewModelScope.launch { serviceRepository.onServiceAction(ServiceAction.Reaction(emoji, replyId, contactKey)) } + fun sendReaction(emoji: String, replyId: Int, contactKey: String) = viewModelScope.launch { + serviceRepository.onServiceAction(ServiceAction.Reaction(emoji, replyId, contactKey)) + } fun deleteMessages(uuidList: List) = viewModelScope.launch(ioDispatcher) { packetRepository.deleteMessages(uuidList) } diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/compass/CompassViewModel.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/compass/CompassViewModel.kt index c261b7aab..b7c5f35bd 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/compass/CompassViewModel.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/compass/CompassViewModel.kt @@ -92,17 +92,13 @@ class CompassViewModel( updatesJob?.cancel() - updatesJob = - viewModelScope.launch { - combine(headingProvider.headingUpdates(), phoneLocationProvider.locationUpdates()) { - heading, - location, - -> - buildState(heading, location) - } - .flowOn(dispatchers.default) - .collect { _uiState.value = it } + updatesJob = viewModelScope.launch { + combine(headingProvider.headingUpdates(), phoneLocationProvider.locationUpdates()) { heading, location -> + buildState(heading, location) } + .flowOn(dispatchers.default) + .collect { _uiState.value = it } + } } fun stop() { diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetricsState.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetricsState.kt index 0e042d9e7..1d0524500 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetricsState.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetricsState.kt @@ -114,8 +114,9 @@ data class EnvironmentMetricsState(val environmentMetrics: List = emp } // Relative Humidity - val humidities = - telemetries.mapNotNull { it.environment_metrics?.relative_humidity?.takeIf { !it.isNaN() && it != 0.0f } } + val humidities = telemetries.mapNotNull { + it.environment_metrics?.relative_humidity?.takeIf { !it.isNaN() && it != 0.0f } + } if (humidities.isNotEmpty()) { minValues.add(humidities.minOf { it }) maxValues.add(humidities.maxOf { it }) @@ -123,8 +124,9 @@ data class EnvironmentMetricsState(val environmentMetrics: List = emp } // Soil Temperature - val soilTemperatures = - telemetries.mapNotNull { it.environment_metrics?.soil_temperature?.takeIf { !it.isNaN() } } + val soilTemperatures = telemetries.mapNotNull { + it.environment_metrics?.soil_temperature?.takeIf { !it.isNaN() } + } if (soilTemperatures.isNotEmpty()) { var minSoilTemperatureValue = soilTemperatures.minOf { it } var maxSoilTemperatureValue = soilTemperatures.maxOf { it } @@ -138,8 +140,9 @@ data class EnvironmentMetricsState(val environmentMetrics: List = emp } // Soil Moisture - val soilMoistures = - telemetries.mapNotNull { it.environment_metrics?.soil_moisture?.takeIf { it != Int.MIN_VALUE } } + val soilMoistures = telemetries.mapNotNull { + it.environment_metrics?.soil_moisture?.takeIf { it != Int.MIN_VALUE } + } if (soilMoistures.isNotEmpty()) { minValues.add(soilMoistures.minOf { it.toFloat() }) maxValues.add(soilMoistures.maxOf { it.toFloat() }) diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt index 4d18c00bf..bafa7a2b0 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt @@ -35,7 +35,6 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlinx.datetime.Instant import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime import okio.ByteString.Companion.decodeBase64 @@ -71,6 +70,7 @@ import org.meshtastic.feature.node.model.MetricsState import org.meshtastic.feature.node.model.TimeFrame import org.meshtastic.proto.PortNum import org.meshtastic.proto.Telemetry +import kotlin.time.Instant import org.meshtastic.proto.Paxcount as ProtoPaxcount /** diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/TracerouteLog.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/TracerouteLog.kt index e5321773b..49d870da2 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/TracerouteLog.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/TracerouteLog.kt @@ -148,47 +148,43 @@ fun TracerouteLogScreen( val (text, icon) = route.getTextAndIcon() var expanded by remember { mutableStateOf(false) } - val tracerouteDetailsAnnotated: AnnotatedString? = - result?.let { res -> - if (route != null && route.route.isNotEmpty() && route.route_back.isNotEmpty()) { - val seconds = - (res.received_date - log.received_date).coerceAtLeast(0).toDouble() / MS_PER_SEC - val annotatedBase = - annotateTraceroute( - res.fromRadio.packet?.getTracerouteResponse( - ::getUsername, - headerTowards = stringResource(Res.string.traceroute_route_towards_dest), - headerBack = headerBackStr, - ), - statusGreen = statusGreen, - statusYellow = statusYellow, - statusOrange = statusOrange, - ) - val durationText = - stringResource(Res.string.traceroute_duration, formatString("%.1f", seconds)) - buildAnnotatedString { - append(annotatedBase) - append("\n\n$durationText") - } - } else { - // For cases where there's a result but no full route, display plain text - res.fromRadio.packet - ?.getTracerouteResponse( + val tracerouteDetailsAnnotated: AnnotatedString? = result?.let { res -> + if (route != null && route.route.isNotEmpty() && route.route_back.isNotEmpty()) { + val seconds = (res.received_date - log.received_date).coerceAtLeast(0).toDouble() / MS_PER_SEC + val annotatedBase = + annotateTraceroute( + res.fromRadio.packet?.getTracerouteResponse( ::getUsername, headerTowards = stringResource(Res.string.traceroute_route_towards_dest), headerBack = headerBackStr, - ) - ?.let { AnnotatedString(it) } + ), + statusGreen = statusGreen, + statusYellow = statusYellow, + statusOrange = statusOrange, + ) + val durationText = stringResource(Res.string.traceroute_duration, formatString("%.1f", seconds)) + buildAnnotatedString { + append(annotatedBase) + append("\n\n$durationText") } + } else { + // For cases where there's a result but no full route, display plain text + res.fromRadio.packet + ?.getTracerouteResponse( + ::getUsername, + headerTowards = stringResource(Res.string.traceroute_route_towards_dest), + headerBack = headerBackStr, + ) + ?.let { AnnotatedString(it) } } - val overlay = - route?.let { - TracerouteOverlay( - requestId = log.fromRadio.packet?.id ?: 0, - forwardRoute = it.route, - returnRoute = it.route_back, - ) - } + } + val overlay = route?.let { + TracerouteOverlay( + requestId = log.fromRadio.packet?.id ?: 0, + forwardRoute = it.route, + returnRoute = it.route_back, + ) + } Box { MetricLogItem( diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/NotificationSection.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/NotificationSection.kt index fb27e947e..96e848a12 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/NotificationSection.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/NotificationSection.kt @@ -17,8 +17,8 @@ package org.meshtastic.feature.settings.component import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.Message import androidx.compose.material.icons.rounded.BatteryAlert -import androidx.compose.material.icons.rounded.Message import androidx.compose.material.icons.rounded.PersonAdd import androidx.compose.runtime.Composable import org.jetbrains.compose.resources.stringResource @@ -44,7 +44,7 @@ fun NotificationSection( ExpressiveSection(title = stringResource(Res.string.app_notifications)) { SwitchListItem( text = stringResource(Res.string.meshtastic_messages_notifications), - leadingIcon = Icons.Rounded.Message, + leadingIcon = Icons.AutoMirrored.Rounded.Message, checked = messagesEnabled, onClick = { onToggleMessages(!messagesEnabled) }, ) diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/Debug.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/Debug.kt index 84e279814..1316ebb49 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/Debug.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/Debug.kt @@ -165,16 +165,15 @@ fun DebugScreen(onNavigateUp: () -> Unit, viewModel: DebugViewModel) { filterMode = filterMode, onFilterModeChange = { filterMode = it }, onExportLogs = { - val format = - LocalDateTime.Format { - year() - monthNumber() - day() - char('_') - hour() - minute() - second() - } + val format = LocalDateTime.Format { + year() + monthNumber() + day() + char('_') + hour() + minute() + second() + } val timestamp = fromEpochMilliseconds(nowMillis).toLocalDateTime(TimeZone.UTC).format(format) val fileName = "meshtastic_debug_$timestamp.txt" diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/DebugFilters.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/DebugFilters.kt index 09185904c..2429f0abd 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/DebugFilters.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/DebugFilters.kt @@ -117,14 +117,13 @@ fun DebugPresetFilters( onFilterTextsChange: (List) -> Unit, modifier: Modifier = Modifier, ) { - val availableFilters = - presetFilters.filter { filter -> - logs.any { log -> - log.logMessage.contains(filter, ignoreCase = true) || - log.messageType.contains(filter, ignoreCase = true) || - log.formattedReceivedDate.contains(filter, ignoreCase = true) - } + val availableFilters = presetFilters.filter { filter -> + logs.any { log -> + log.logMessage.contains(filter, ignoreCase = true) || + log.messageType.contains(filter, ignoreCase = true) || + log.formattedReceivedDate.contains(filter, ignoreCase = true) } + } Column(modifier = modifier) { Text( text = stringResource(Res.string.debug_filter_preset_title), diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/DeviceConfigScreen.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/DeviceConfigScreen.kt index cf7b0ef2b..6f469269b 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/DeviceConfigScreen.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/DeviceConfigScreen.kt @@ -180,7 +180,7 @@ fun DeviceConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Unit ) { item { TitledCard(title = stringResource(Res.string.options)) { - val currentRole = formState.value.role ?: Config.DeviceConfig.Role.CLIENT + val currentRole = formState.value.role DropDownPreference( title = stringResource(Res.string.role), enabled = state.connected, @@ -193,7 +193,7 @@ fun DeviceConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Unit HorizontalDivider() - val currentRebroadcastMode = formState.value.rebroadcast_mode ?: Config.DeviceConfig.RebroadcastMode.ALL + val currentRebroadcastMode = formState.value.rebroadcast_mode DropDownPreference( title = stringResource(Res.string.rebroadcast_mode), enabled = state.connected, @@ -207,7 +207,7 @@ fun DeviceConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Unit val nodeInfoBroadcastIntervals = remember { IntervalConfiguration.NODE_INFO_BROADCAST.allowedIntervals } DropDownPreference( title = stringResource(Res.string.nodeinfo_broadcast_interval), - selectedItem = (formState.value.node_info_broadcast_secs ?: 0).toLong(), + selectedItem = formState.value.node_info_broadcast_secs.toLong(), enabled = state.connected, items = nodeInfoBroadcastIntervals.map { it.value to it.toDisplayString() }, onItemSelected = { formState.value = formState.value.copy(node_info_broadcast_secs = it.toInt()) }, @@ -255,7 +255,7 @@ fun DeviceConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Unit EditTextPreference( title = "", - value = formState.value.tzdef ?: "", + value = formState.value.tzdef, summary = stringResource(Res.string.config_device_tzdef_summary), maxSize = 64, // tzdef max_size:65 enabled = state.connected, @@ -292,7 +292,7 @@ fun DeviceConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Unit TitledCard(title = stringResource(Res.string.gpio)) { EditTextPreference( title = stringResource(Res.string.button_gpio), - value = formState.value.button_gpio ?: 0, + value = formState.value.button_gpio, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), onValueChanged = { formState.value = formState.value.copy(button_gpio = it) }, @@ -302,7 +302,7 @@ fun DeviceConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Unit EditTextPreference( title = stringResource(Res.string.buzzer_gpio), - value = formState.value.buzzer_gpio ?: 0, + value = formState.value.buzzer_gpio, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), onValueChanged = { formState.value = formState.value.copy(buzzer_gpio = it) }, diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/NetworkConfigItemList.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/NetworkConfigItemList.kt index 9497a9778..4e471be24 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/NetworkConfigItemList.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/NetworkConfigItemList.kt @@ -168,7 +168,7 @@ fun NetworkConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, onO if (wifiStatus.is_connected) { ListItem( text = stringResource(Res.string.wifi_ip), - supportingText = formatIpAddress(wifiStatus.ip_address ?: 0), + supportingText = formatIpAddress(wifiStatus.ip_address), trailingIcon = null, ) } @@ -177,7 +177,7 @@ fun NetworkConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, onO if (ethernetStatus.is_connected) { ListItem( text = stringResource(Res.string.ethernet_ip), - supportingText = formatIpAddress(ethernetStatus.ip_address ?: 0), + supportingText = formatIpAddress(ethernetStatus.ip_address), trailingIcon = null, ) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1ce5e46f2..7ce5d08fc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -23,6 +23,7 @@ kotlinx-coroutines-android = "1.10.2" kotlinx-datetime = "0.7.1-0.6.x-compat" kotlinx-serialization = "1.10.0" ktlint = "1.7.1" +ktfmt = "0.62" kover = "0.9.7" mokkery = "3.3.0" kotest = "6.1.7" diff --git a/test.gradle.kts b/test.gradle.kts deleted file mode 100644 index 78d975ab9..000000000 --- a/test.gradle.kts +++ /dev/null @@ -1,2 +0,0 @@ -import com.android.build.api.dsl.KotlinMultiplatformAndroidLibraryExtension -println(KotlinMultiplatformAndroidLibraryExtension::class.java.name)