mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-06-13 08:25:07 -04:00
refactor: Remove AIDL API and modernize service architecture (#5586)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
51
.github/workflows/publish-core.yml
vendored
51
.github/workflows/publish-core.yml
vendored
@@ -1,51 +0,0 @@
|
||||
name: Publish Core Libraries
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version_suffix:
|
||||
description: 'Version suffix (e.g. -alpha01, -SNAPSHOT)'
|
||||
required: false
|
||||
default: '-SNAPSHOT'
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Gradle Setup
|
||||
uses: ./.github/actions/gradle-setup
|
||||
with:
|
||||
gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
|
||||
|
||||
- name: Configure Version
|
||||
id: version
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
||||
VERSION_SUFFIX: ${{ inputs.version_suffix }}
|
||||
run: |
|
||||
if [[ "$EVENT_NAME" == "release" ]]; then
|
||||
echo "VERSION_NAME=$RELEASE_TAG" >> $GITHUB_ENV
|
||||
else
|
||||
# Use a timestamp-based version for manual/branch builds to avoid collisions
|
||||
# or use the base version + suffix
|
||||
BASE_VERSION=$(grep "VERSION_NAME_BASE" config.properties | cut -d'=' -f2)
|
||||
echo "VERSION_NAME=${BASE_VERSION}${VERSION_SUFFIX}" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Publish to GitHub Packages
|
||||
run: ./gradlew :core:api:publish :core:model:publish :core:proto:publish
|
||||
env:
|
||||
GITHUB_ACTOR: ${{ github.actor }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
2
.github/workflows/reusable-check.yml
vendored
2
.github/workflows/reusable-check.yml
vendored
@@ -113,7 +113,7 @@ jobs:
|
||||
|
||||
- name: Lint, Analysis & KMP Smoke Compile
|
||||
if: inputs.run_lint == true
|
||||
run: ./gradlew spotlessCheck detekt androidApp:lintFdroidDebug androidApp:lintGoogleDebug core:barcode:lintFdroidDebug core:barcode:lintGoogleDebug core:api:lintDebug kmpSmokeCompile -Pci=true --continue
|
||||
run: ./gradlew spotlessCheck detekt androidApp:lintFdroidDebug androidApp:lintGoogleDebug core:barcode:lintFdroidDebug core:barcode:lintGoogleDebug kmpSmokeCompile -Pci=true --continue
|
||||
|
||||
- name: KMP Smoke Compile (lint skipped)
|
||||
if: inputs.run_lint == false
|
||||
|
||||
@@ -5,7 +5,7 @@ Module directory, namespacing conventions, environment setup, and troubleshootin
|
||||
|
||||
- **Build System:** Gradle (Kotlin DSL). JDK 21 REQUIRED. Target SDK: API 36. Min SDK: API 26.
|
||||
- **Flavors:** `fdroid` (OSS only) · `google` (Maps + DataDog analytics)
|
||||
- **Android-only Modules:** `core:api` (AIDL), `core:barcode` (CameraX). Shared contracts abstracted into `core:ui/commonMain`.
|
||||
- **Android-only Modules:** `core:barcode` (CameraX). Shared contracts abstracted into `core:ui/commonMain`.
|
||||
|
||||
## Codebase Map
|
||||
|
||||
@@ -28,7 +28,6 @@ Module directory, namespacing conventions, environment setup, and troubleshootin
|
||||
| `core:navigation` | Shared navigation keys/routes for Navigation 3 using `@Serializable sealed interface` hierarchies. `DeepLinkRouter` for typed backstack synthesis, and `MeshtasticNavSavedStateConfig` with `subclassesOfSealed()` for automatic polymorphic backstack persistence. |
|
||||
| `core:ui` | Shared Compose UI components (`MeshtasticAppShell`, `MeshtasticNavDisplay`, `MeshtasticNavigationSuite`, `AlertHost`, `SharedDialogs`, `PlaceholderScreen`, `MainAppBar`, dialogs, preferences) and platform abstractions. |
|
||||
| `core:service` | KMP service layer; Android bindings stay in `androidMain`. |
|
||||
| `core:api` | Public AIDL/API integration module for external clients. |
|
||||
| `core:prefs` | KMP preferences layer built on DataStore abstractions. |
|
||||
| `core:barcode` | Barcode scanning (Android-only). |
|
||||
| `core:nfc` | NFC abstractions (KMP). Android NFC hardware implementation in `androidMain`. |
|
||||
|
||||
@@ -17,7 +17,7 @@ Run in a single invocation for routine changes to ensure code formatting, analys
|
||||
> In KMP modules, the `test` task name is **ambiguous**. Gradle matches both `testAndroid` and
|
||||
> `testAndroidHostTest` and refuses to run either, silently skipping KMP modules.
|
||||
> `allTests` is the `KotlinTestReport` lifecycle task registered by the KMP plugin.
|
||||
> Conversely, `allTests` does **not** cover pure-Android modules (`:androidApp`, `:core:api`, etc.), which is why both `test` and `allTests` are needed.
|
||||
> Conversely, `allTests` does **not** cover pure-Android modules (`:androidApp`, `:core:barcode`, etc.), which is why both `test` and `allTests` are needed.
|
||||
|
||||
*Note: If testing Compose UI on the JVM (Robolectric) with Java 21, pin tests to `@Config(sdk = [34])` to avoid SDK 35 compatibility crashes.*
|
||||
|
||||
|
||||
@@ -87,7 +87,6 @@ Each module has its own README with details on its responsibilities, API surface
|
||||
|
||||
| Module | Description |
|
||||
|---|---|
|
||||
| [core/api](core/api/README.md) | AIDL service API for third-party integrations |
|
||||
| [core/domain](core/domain/README.md) | Business-logic use cases (radio config, sessions, exports) |
|
||||
| [core/repository](core/repository/README.md) | Data & infrastructure contracts (RadioTransport, NodeRepository, ServiceRepository) |
|
||||
| [core/takserver](core/takserver/README.md) | Meshtastic ↔ TAK (ATAK/iTAK) bridge — CoT server & conversion |
|
||||
@@ -123,13 +122,9 @@ Each module has its own README with details on its responsibilities, API surface
|
||||
|
||||
You can help translate the app into your native language using [Crowdin](https://crowdin.meshtastic.org/android).
|
||||
|
||||
## API & Integration
|
||||
## Integration
|
||||
|
||||
Developers can integrate with the Meshtastic Android app using our published API library via **JitPack**. This allows third-party applications (like the ATAK plugin) to communicate with the mesh service via AIDL.
|
||||
|
||||
For detailed integration instructions, see [core/api/README.md](core/api/README.md).
|
||||
|
||||
Additionally, the app includes a built-in **Local TAK Server** feature that can be enabled in settings. This runs a local TCP server on port 8089 to allow ATAK clients to connect directly and route their traffic over the mesh.
|
||||
The app includes a built-in **Local TAK Server** feature that can be enabled in settings. This runs a local TCP server on port 8089 to allow ATAK clients to connect directly and route their traffic over the mesh.
|
||||
|
||||
## Building the Android App
|
||||
> [!WARNING]
|
||||
|
||||
@@ -112,7 +112,7 @@ configure<ApplicationExtension> {
|
||||
),
|
||||
)
|
||||
}
|
||||
ndk { abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64") }
|
||||
ndk { abiFilters += listOf("armeabi-v7a", "arm64-v8a") }
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
@@ -126,7 +126,7 @@ configure<ApplicationExtension> {
|
||||
abi {
|
||||
isEnable = !disableSplits
|
||||
reset()
|
||||
include("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||
include("armeabi-v7a", "arm64-v8a")
|
||||
isUniversalApk = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,8 +127,8 @@ public abstract class MarkerClusterer extends Overlay {
|
||||
int zoomLevel = mapView.getZoomLevel();
|
||||
if (zoomLevel != mLastZoomLevel && !mapView.isAnimating()){
|
||||
hideInfoWindows();
|
||||
mClusters = clusterer(mapView);
|
||||
renderer(mClusters, canvas, mapView);
|
||||
mClusters = clusterer(mapView);
|
||||
renderer(mClusters, canvas, mapView);
|
||||
mLastZoomLevel = zoomLevel;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import androidx.core.content.res.ResourcesCompat;
|
||||
|
||||
import org.meshtastic.app.map.model.MarkerWithLabel;
|
||||
|
||||
import org.osmdroid.bonuspack.R;
|
||||
@@ -72,7 +74,7 @@ public class RadiusMarkerClusterer extends MarkerClusterer {
|
||||
mTextPaint.setFakeBoldText(true);
|
||||
mTextPaint.setTextAlign(Paint.Align.CENTER);
|
||||
mTextPaint.setAntiAlias(true);
|
||||
Drawable clusterIconD = ctx.getResources().getDrawable(R.drawable.marker_cluster);
|
||||
Drawable clusterIconD = ResourcesCompat.getDrawable(ctx.getResources(), R.drawable.marker_cluster, ctx.getTheme());
|
||||
Bitmap clusterIcon = ((BitmapDrawable) clusterIconD).getBitmap();
|
||||
setIcon(clusterIcon);
|
||||
mAnimated = true;
|
||||
|
||||
@@ -52,6 +52,7 @@ import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableDoubleStateOf
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -87,6 +88,7 @@ import org.meshtastic.core.common.util.nowMillis
|
||||
import org.meshtastic.core.common.util.nowSeconds
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.model.NodeAddress
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.calculating
|
||||
import org.meshtastic.core.resources.cancel
|
||||
@@ -113,9 +115,11 @@ import org.meshtastic.core.resources.map_purge_success
|
||||
import org.meshtastic.core.resources.map_style_selection
|
||||
import org.meshtastic.core.resources.map_subDescription
|
||||
import org.meshtastic.core.resources.map_tile_source
|
||||
import org.meshtastic.core.resources.now
|
||||
import org.meshtastic.core.resources.only_favorites
|
||||
import org.meshtastic.core.resources.show_precision_circle
|
||||
import org.meshtastic.core.resources.show_waypoints
|
||||
import org.meshtastic.core.resources.unknown
|
||||
import org.meshtastic.core.resources.waypoint_delete
|
||||
import org.meshtastic.core.resources.you
|
||||
import org.meshtastic.core.ui.component.BasicListItem
|
||||
@@ -239,6 +243,9 @@ fun MapView(
|
||||
val haptic = LocalHapticFeedback.current
|
||||
fun performHapticFeedback() = haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
|
||||
val unknownText = stringResource(Res.string.unknown)
|
||||
val nowText = stringResource(Res.string.now)
|
||||
|
||||
// Accompanist permissions state for location
|
||||
val locationPermissionsState =
|
||||
rememberMultiplePermissionsState(permissions = listOf(Manifest.permission.ACCESS_FINE_LOCATION))
|
||||
@@ -355,36 +362,37 @@ fun MapView(
|
||||
|
||||
val (p, u) = node.position to node.user
|
||||
val nodePosition = GeoPoint(node.latitude, node.longitude)
|
||||
MarkerWithLabel(mapView = this, label = "${u.short_name} ${formatAgo(p.time)}").apply {
|
||||
id = u.id
|
||||
title = u.long_name
|
||||
snippet =
|
||||
getString(
|
||||
Res.string.map_node_popup_details,
|
||||
node.gpsString(),
|
||||
formatAgo(node.lastHeard),
|
||||
formatAgo(p.time),
|
||||
if (node.batteryStr != "") node.batteryStr else "?",
|
||||
)
|
||||
ourNode?.distanceStr(node, displayUnits)?.let { dist ->
|
||||
ourNode.bearing(node)?.let { bearing ->
|
||||
subDescription = getString(Res.string.map_subDescription, bearing, dist)
|
||||
MarkerWithLabel(mapView = this, label = "${u.short_name} ${formatAgo(p.time, unknownText, nowText)}")
|
||||
.apply {
|
||||
id = u.id
|
||||
title = u.long_name
|
||||
snippet =
|
||||
getString(
|
||||
Res.string.map_node_popup_details,
|
||||
node.gpsString(),
|
||||
formatAgo(node.lastHeard, unknownText, nowText),
|
||||
formatAgo(p.time, unknownText, nowText),
|
||||
if (node.batteryStr != "") node.batteryStr else "?",
|
||||
)
|
||||
ourNode?.distanceStr(node, displayUnits)?.let { dist ->
|
||||
ourNode.bearing(node)?.let { bearing ->
|
||||
subDescription = getString(Res.string.map_subDescription, bearing, dist)
|
||||
}
|
||||
}
|
||||
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
|
||||
position = nodePosition
|
||||
icon = markerIcon
|
||||
setNodeColors(node.colors)
|
||||
if (!mapFilterStateValue.showPrecisionCircle) {
|
||||
setPrecisionBits(0)
|
||||
} else {
|
||||
setPrecisionBits(p.precision_bits)
|
||||
}
|
||||
setOnLongClickListener {
|
||||
navigateToNodeDetails(node.num)
|
||||
true
|
||||
}
|
||||
}
|
||||
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
|
||||
position = nodePosition
|
||||
icon = markerIcon
|
||||
setNodeColors(node.colors)
|
||||
if (!mapFilterStateValue.showPrecisionCircle) {
|
||||
setPrecisionBits(0)
|
||||
} else {
|
||||
setPrecisionBits(p.precision_bits)
|
||||
}
|
||||
setOnLongClickListener {
|
||||
navigateToNodeDetails(node.num)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -433,7 +441,7 @@ fun MapView(
|
||||
}
|
||||
}
|
||||
|
||||
fun getUsername(id: String?) = if (id == DataPacket.ID_LOCAL || (myId != null && id == myId)) {
|
||||
fun getUsername(id: String?) = if (id == NodeAddress.ID_LOCAL || (myId != null && id == myId)) {
|
||||
getString(Res.string.you)
|
||||
} else {
|
||||
mapViewModel.getUser(id).long_name
|
||||
@@ -446,7 +454,7 @@ fun MapView(
|
||||
if (!mapFilterState.showWaypoints) return@mapNotNull null // Use collected mapFilterState
|
||||
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 label = pt.name + " " + formatAgo((waypoint.time / 1000).toInt(), unknownText, nowText)
|
||||
val emoji = String(Character.toChars(if (pt.icon == 0) 128205 else pt.icon))
|
||||
val now = nowMillis
|
||||
val expireTimeMillis = pt.expire * 1000L
|
||||
@@ -818,15 +826,15 @@ private fun FdroidMainMapFilterDropdown(
|
||||
|
||||
@Composable
|
||||
private fun MapStyleDialog(selectedMapStyle: Int, onDismiss: () -> Unit, onSelectMapStyle: (Int) -> Unit) {
|
||||
val selected = remember { mutableStateOf(selectedMapStyle) }
|
||||
val selected = remember { mutableIntStateOf(selectedMapStyle) }
|
||||
|
||||
MapsDialog(onDismiss = onDismiss) {
|
||||
CustomTileSource.mTileSources.values.forEachIndexed { index, style ->
|
||||
ListItem(
|
||||
text = style,
|
||||
trailingIcon = if (index == selected.value) MeshtasticIcons.Check else null,
|
||||
trailingIcon = if (index == selected.intValue) MeshtasticIcons.Check else null,
|
||||
onClick = {
|
||||
selected.value = index
|
||||
selected.intValue = index
|
||||
onSelectMapStyle(index)
|
||||
onDismiss()
|
||||
},
|
||||
@@ -879,7 +887,7 @@ private fun PurgeTileSourceDialog(onDismiss: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
val cache = SqlTileWriterExt()
|
||||
|
||||
val sourceList by derivedStateOf { cache.sources.map { it.source as String } }
|
||||
val sourceList by remember { derivedStateOf { cache.sources.map { it.source as String } } }
|
||||
|
||||
val selected = remember { mutableStateListOf<Int>() }
|
||||
|
||||
|
||||
@@ -22,11 +22,11 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import org.koin.core.annotation.KoinViewModel
|
||||
import org.meshtastic.core.common.BuildConfigProvider
|
||||
import org.meshtastic.core.model.RadioController
|
||||
import org.meshtastic.core.repository.MapPrefs
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
import org.meshtastic.core.repository.PacketRepository
|
||||
import org.meshtastic.core.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.repository.RadioController
|
||||
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
|
||||
import org.meshtastic.feature.map.BaseMapViewModel
|
||||
import org.meshtastic.proto.LocalConfig
|
||||
|
||||
@@ -33,6 +33,7 @@ import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import org.meshtastic.app.R
|
||||
import org.meshtastic.app.map.MapViewModel
|
||||
@@ -44,6 +45,9 @@ import org.meshtastic.app.map.rememberMapViewWithLifecycle
|
||||
import org.meshtastic.app.map.zoomIn
|
||||
import org.meshtastic.core.model.TracerouteOverlay
|
||||
import org.meshtastic.core.model.util.GeoConstants.EARTH_RADIUS_METERS
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.now
|
||||
import org.meshtastic.core.resources.unknown
|
||||
import org.meshtastic.core.ui.theme.TracerouteColors
|
||||
import org.meshtastic.core.ui.util.formatAgo
|
||||
import org.meshtastic.feature.map.tracerouteNodeSelection
|
||||
@@ -95,6 +99,9 @@ fun TracerouteOsmMap(
|
||||
val displayNodes = tracerouteSelection.nodesForMarkers
|
||||
val nodeLookup = tracerouteSelection.nodeLookup
|
||||
|
||||
val unknownText = stringResource(Res.string.unknown)
|
||||
val nowText = stringResource(Res.string.now)
|
||||
|
||||
// Report mappable count
|
||||
LaunchedEffect(tracerouteOverlay, displayNodes) {
|
||||
if (tracerouteOverlay != null) {
|
||||
@@ -191,7 +198,10 @@ fun TracerouteOsmMap(
|
||||
displayNodes.forEach { node ->
|
||||
val position = GeoPoint(node.latitude, node.longitude)
|
||||
val marker =
|
||||
MarkerWithLabel(mapView = map, label = "${node.user.short_name} ${formatAgo(node.position.time)}")
|
||||
MarkerWithLabel(
|
||||
mapView = map,
|
||||
label = "${node.user.short_name} ${formatAgo(node.position.time, unknownText, nowText)}",
|
||||
)
|
||||
.apply {
|
||||
id = node.user.id
|
||||
title = node.user.long_name
|
||||
|
||||
@@ -50,11 +50,12 @@ import org.meshtastic.app.map.model.CustomTileProviderConfig
|
||||
import org.meshtastic.app.map.prefs.map.GoogleMapsPrefs
|
||||
import org.meshtastic.app.map.repository.CustomTileProviderRepository
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.model.RadioController
|
||||
import org.meshtastic.core.model.NodeAddress
|
||||
import org.meshtastic.core.repository.MapPrefs
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
import org.meshtastic.core.repository.PacketRepository
|
||||
import org.meshtastic.core.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.repository.RadioController
|
||||
import org.meshtastic.core.repository.UiPrefs
|
||||
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
|
||||
import org.meshtastic.feature.map.BaseMapViewModel
|
||||
@@ -670,8 +671,7 @@ class MapViewModel(
|
||||
(currentTileProvider as? MBTilesProvider)?.close()
|
||||
}
|
||||
|
||||
override fun getUser(userId: String?) =
|
||||
nodeRepository.getUser(userId ?: org.meshtastic.core.model.DataPacket.ID_BROADCAST)
|
||||
override fun getUser(userId: String?) = nodeRepository.getUser(userId ?: NodeAddress.ID_BROADCAST)
|
||||
}
|
||||
|
||||
enum class LayerType {
|
||||
|
||||
@@ -67,7 +67,6 @@
|
||||
-->
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
<!-- We run our mesh code as a foreground service - FIXME, find a way to stop doing this -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
||||
@@ -157,16 +156,12 @@
|
||||
android:name="google_analytics_default_allow_analytics_storage"
|
||||
android:value="false" />
|
||||
|
||||
<!-- This is the public API for doing mesh radio operations from android apps -->
|
||||
<!-- In-app mesh foreground service -->
|
||||
<service
|
||||
android:name="org.meshtastic.core.service.MeshService"
|
||||
android:enabled="true"
|
||||
android:foregroundServiceType="connectedDevice|location"
|
||||
android:exported="true" tools:ignore="ExportedActivity">
|
||||
<intent-filter>
|
||||
<action android:name="com.geeksville.mesh.Service" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||
@@ -268,7 +263,7 @@
|
||||
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
|
||||
|
||||
<!-- for testing -->
|
||||
<action android:name="com.geeksville.mesh.SIM_BOOT" />
|
||||
<action android:name="org.meshtastic.app.SIM_BOOT" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Also restart our service if the app gets upgraded -->
|
||||
|
||||
@@ -63,7 +63,8 @@ import org.meshtastic.core.network.repository.UsbRepository
|
||||
import org.meshtastic.core.nfc.NfcScannerEffect
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.channel_invalid
|
||||
import org.meshtastic.core.service.MeshServiceClient
|
||||
import org.meshtastic.core.service.MeshService
|
||||
import org.meshtastic.core.service.startService
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
import org.meshtastic.core.ui.theme.MODE_DYNAMIC
|
||||
import org.meshtastic.core.ui.util.LocalAnalyticsIntroProvider
|
||||
@@ -95,18 +96,9 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
private val usbRepository: UsbRepository by inject()
|
||||
|
||||
/**
|
||||
* Activity-lifecycle-aware client that binds to the mesh service. Note: This is used implicitly as it registers
|
||||
* itself as a LifecycleObserver in its init block.
|
||||
*/
|
||||
internal val meshServiceClient: MeshServiceClient by inject { parametersOf(this) }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
installSplashScreen()
|
||||
|
||||
// Eagerly evaluate lazy Koin dependency so it registers its LifecycleObserver
|
||||
meshServiceClient.hashCode()
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
enableEdgeToEdge()
|
||||
@@ -168,6 +160,11 @@ class MainActivity : AppCompatActivity() {
|
||||
handleIntent(intent)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
MeshService.startService(this)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
// Belt-and-suspenders for the Android 12+ attach-intent quirk: if the activity is
|
||||
|
||||
@@ -111,7 +111,7 @@ class NetworkModule {
|
||||
if (buildConfigProvider.isDebug) {
|
||||
install(plugin = Logging) {
|
||||
logger = KermitHttpLogger
|
||||
level = LogLevel.BODY
|
||||
level = LogLevel.INFO
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.safeDrawingPadding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
@@ -57,12 +58,13 @@ fun MainScreen() {
|
||||
val viewModel: UIViewModel = koinViewModel()
|
||||
// Land on Connections for first-run / no-device-selected; otherwise on Nodes. Read synchronously
|
||||
// from the StateFlow (seeded from persisted prefs) so the initial tab is set in one shot.
|
||||
val initialTab =
|
||||
val initialTab = remember {
|
||||
if (viewModel.currentDeviceAddressFlow.value.isNullOrSelectedNone()) {
|
||||
TopLevelDestination.Connect.route
|
||||
} else {
|
||||
NodesRoute.Nodes
|
||||
}
|
||||
}
|
||||
val multiBackstack = rememberMultiBackstack(initialTab)
|
||||
val backStack = multiBackstack.activeBackStack
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ package org.meshtastic.app.service
|
||||
import dev.mokkery.MockMode
|
||||
import dev.mokkery.mock
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.repository.MeshServiceNotifications
|
||||
import org.meshtastic.core.repository.MeshNotificationManager
|
||||
import org.meshtastic.core.repository.RadioInterfaceService
|
||||
import org.meshtastic.proto.ClientNotification
|
||||
import org.meshtastic.proto.Telemetry
|
||||
@@ -28,7 +28,7 @@ class Fakes {
|
||||
val service: RadioInterfaceService = mock(MockMode.autofill)
|
||||
}
|
||||
|
||||
class FakeMeshServiceNotifications : MeshServiceNotifications {
|
||||
class FakeMeshNotificationManager : MeshNotificationManager {
|
||||
override fun clearNotifications() {}
|
||||
|
||||
override fun initChannels() {}
|
||||
|
||||
@@ -199,11 +199,6 @@ gradlePlugin {
|
||||
implementationClass = "org.meshtastic.buildlogic.DocsTasks"
|
||||
}
|
||||
|
||||
register("publishing") {
|
||||
id = "meshtastic.publishing"
|
||||
implementationClass = "PublishingConventionPlugin"
|
||||
}
|
||||
|
||||
register("aboutLibraries") {
|
||||
id = "meshtastic.aboutlibraries"
|
||||
implementationClass = "AboutLibrariesConventionPlugin"
|
||||
|
||||
@@ -53,7 +53,7 @@ class AndroidRoomConventionPlugin : Plugin<Project> {
|
||||
dependencies { add("kspJvm", roomCompiler) }
|
||||
}
|
||||
|
||||
pluginManager.withPlugin("org.jetbrains.kotlin.android") {
|
||||
pluginManager.withPlugin("com.android.library") {
|
||||
val hasAndroidTest = projectDir.resolve("src/androidTest").exists()
|
||||
dependencies {
|
||||
"implementation"(roomRuntime)
|
||||
|
||||
@@ -48,7 +48,7 @@ class KoinConventionPlugin : Plugin<Project> {
|
||||
}
|
||||
}
|
||||
|
||||
pluginManager.withPlugin("org.jetbrains.kotlin.android") {
|
||||
pluginManager.withPlugin("com.android.application") {
|
||||
// If this is *only* an Android module (no KMP plugin)
|
||||
if (!pluginManager.hasPlugin("org.jetbrains.kotlin.multiplatform")) {
|
||||
dependencies {
|
||||
@@ -58,6 +58,16 @@ class KoinConventionPlugin : Plugin<Project> {
|
||||
}
|
||||
}
|
||||
|
||||
pluginManager.withPlugin("com.android.library") {
|
||||
// If this is *only* an Android library module (no KMP plugin)
|
||||
if (!pluginManager.hasPlugin("org.jetbrains.kotlin.multiplatform")) {
|
||||
dependencies {
|
||||
add("implementation", koinCore)
|
||||
add("implementation", koinAnnotations)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pluginManager.withPlugin("org.jetbrains.kotlin.jvm") {
|
||||
// If this is *only* a JVM module (no KMP plugin)
|
||||
if (!pluginManager.hasPlugin("org.jetbrains.kotlin.multiplatform")) {
|
||||
|
||||
@@ -36,10 +36,16 @@ class KotlinXSerializationConventionPlugin : Plugin<Project> {
|
||||
}
|
||||
}
|
||||
|
||||
pluginManager.withPlugin("org.jetbrains.kotlin.android") {
|
||||
pluginManager.withPlugin("com.android.application") {
|
||||
dependencies { "implementation"(serializationLib) }
|
||||
}
|
||||
|
||||
pluginManager.withPlugin("com.android.library") {
|
||||
if (!pluginManager.hasPlugin("org.jetbrains.kotlin.multiplatform")) {
|
||||
dependencies { "implementation"(serializationLib) }
|
||||
}
|
||||
}
|
||||
|
||||
pluginManager.withPlugin("org.jetbrains.kotlin.jvm") { dependencies { "implementation"(serializationLib) } }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.publish.PublishingExtension
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
import org.meshtastic.buildlogic.configProperties
|
||||
|
||||
class PublishingConventionPlugin : Plugin<Project> {
|
||||
override fun apply(target: Project) {
|
||||
with(target) {
|
||||
pluginManager.apply("maven-publish")
|
||||
|
||||
group = "org.meshtastic"
|
||||
|
||||
if (version == "unspecified") {
|
||||
version =
|
||||
providers.environmentVariable("VERSION").orNull
|
||||
?: providers.environmentVariable("VERSION_NAME").orNull
|
||||
?: configProperties.getProperty("VERSION_NAME_BASE")
|
||||
?: "0.0.0-SNAPSHOT"
|
||||
}
|
||||
|
||||
val githubActor = providers.environmentVariable("GITHUB_ACTOR")
|
||||
val githubToken = providers.environmentVariable("GITHUB_TOKEN")
|
||||
|
||||
if (githubActor.isPresent && githubToken.isPresent) {
|
||||
extensions.configure<PublishingExtension> {
|
||||
repositories {
|
||||
maven {
|
||||
name = "GitHubPackages"
|
||||
url = uri("https://maven.pkg.github.com/meshtastic/Meshtastic-Android")
|
||||
credentials {
|
||||
username = githubActor.get()
|
||||
password = githubToken.get()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,7 +81,6 @@ private val DEVICE_TEST_MODULES = listOf(":core:database", ":core:model")
|
||||
private val ALL_MODULES_FULL =
|
||||
listOf(
|
||||
":androidApp",
|
||||
":core:api",
|
||||
":core:barcode",
|
||||
":core:ble",
|
||||
":core:common",
|
||||
@@ -114,7 +113,7 @@ private val ALL_MODULES_FULL =
|
||||
)
|
||||
|
||||
/** Android-only modules that don't apply the KMP plugin. */
|
||||
private val ANDROID_ONLY_MODULES = setOf(":androidApp", ":core:api", ":core:barcode", ":feature:widget")
|
||||
private val ANDROID_ONLY_MODULES = setOf(":androidApp", ":core:barcode", ":feature:widget")
|
||||
|
||||
/**
|
||||
* Modules excluded from Dokka aggregation. Empty now that :core:proto has been replaced by
|
||||
@@ -126,6 +125,6 @@ private fun allModules(): List<String> = ALL_MODULES_FULL
|
||||
|
||||
/**
|
||||
* Modules that apply the KMP plugin and should be compiled for JVM + iOS targets. Excludes pure-Android modules
|
||||
* (:androidApp, :core:api, :core:barcode, :feature:widget) and the desktop JVM-only module.
|
||||
* (:androidApp, :core:barcode, :feature:widget) and the desktop JVM-only module.
|
||||
*/
|
||||
private fun kmpModules(): List<String> = allModules().filter { it !in ANDROID_ONLY_MODULES + ":desktopApp" }
|
||||
|
||||
@@ -20,7 +20,6 @@ import org.gradle.api.Project
|
||||
import org.gradle.api.provider.Provider
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
import org.jetbrains.kotlin.compose.compiler.gradle.ComposeCompilerGradlePluginExtension
|
||||
import org.jetbrains.kotlin.compose.compiler.gradle.ComposeFeatureFlag
|
||||
|
||||
internal fun Project.configureComposeCompiler() {
|
||||
extensions.configure<ComposeCompilerGradlePluginExtension> {
|
||||
@@ -40,6 +39,5 @@ internal fun Project.configureComposeCompiler() {
|
||||
.relativeToRootProject("compose-reports")
|
||||
.let(reportsDestination::set)
|
||||
stabilityConfigurationFiles.add(isolated.rootProject.projectDirectory.file("compose_compiler_config.conf"))
|
||||
featureFlags.add(ComposeFeatureFlag.OptimizeNonSkippingGroups)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,9 +22,6 @@ import com.android.build.api.dsl.KotlinMultiplatformAndroidLibraryTarget
|
||||
import dev.mokkery.gradle.MokkeryGradleExtension
|
||||
import org.gradle.api.JavaVersion
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.tasks.testing.Test
|
||||
import org.gradle.jvm.toolchain.JavaLanguageVersion
|
||||
import org.gradle.jvm.toolchain.JavaToolchainService
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
import org.gradle.kotlin.dsl.findByType
|
||||
import org.gradle.kotlin.dsl.withType
|
||||
@@ -53,9 +50,8 @@ internal fun Project.configureKotlinAndroid(commonExtension: CommonExtension) {
|
||||
defaultConfig.targetSdk = targetSdkVersion
|
||||
}
|
||||
|
||||
val javaVersion = if (project.name in PUBLISHED_MODULES) JavaVersion.VERSION_17 else JavaVersion.VERSION_21
|
||||
compileOptions.sourceCompatibility = javaVersion
|
||||
compileOptions.targetCompatibility = javaVersion
|
||||
compileOptions.sourceCompatibility = JavaVersion.VERSION_21
|
||||
compileOptions.targetCompatibility = JavaVersion.VERSION_21
|
||||
|
||||
testOptions.animationsDisabled = true
|
||||
testOptions.unitTests.isReturnDefaultValues = true
|
||||
@@ -222,9 +218,6 @@ internal fun Project.configureKotlinJvm() {
|
||||
configureKotlin<KotlinJvmProjectExtension>()
|
||||
}
|
||||
|
||||
/** Modules published for external consumers — use Java 17 for broader compatibility. */
|
||||
private val PUBLISHED_MODULES = setOf("api", "model", "proto")
|
||||
|
||||
/** Compiler args shared across all Kotlin targets (JVM, Android, iOS, etc.). */
|
||||
private val SHARED_COMPILER_ARGS =
|
||||
listOf(
|
||||
@@ -237,18 +230,12 @@ private val SHARED_COMPILER_ARGS =
|
||||
"-Xbackend-threads=0",
|
||||
)
|
||||
|
||||
private const val PUBLISHED_MODULE_JDK = 17
|
||||
private const val APP_JDK = 21
|
||||
private const val JDK_VERSION = 21
|
||||
|
||||
/** Configure base Kotlin options */
|
||||
private inline fun <reified T : KotlinBaseExtension> Project.configureKotlin() {
|
||||
val isPublishedModule = project.name in PUBLISHED_MODULES
|
||||
|
||||
extensions.configure<T> {
|
||||
// Using Java 17 for published modules for better compatibility with consumers (e.g. plugins, older
|
||||
// environments), and Java 21 for the rest of the app.
|
||||
val javaVersion = if (isPublishedModule) PUBLISHED_MODULE_JDK else APP_JDK
|
||||
jvmToolchain(javaVersion)
|
||||
jvmToolchain(JDK_VERSION)
|
||||
|
||||
if (this is KotlinMultiplatformExtension) {
|
||||
targets.configureEach {
|
||||
@@ -256,9 +243,7 @@ private inline fun <reified T : KotlinBaseExtension> Project.configureKotlin() {
|
||||
compilations.configureEach {
|
||||
compileTaskProvider.configure {
|
||||
compilerOptions {
|
||||
if (!isPublishedModule) {
|
||||
freeCompilerArgs.add("-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi")
|
||||
}
|
||||
freeCompilerArgs.add("-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi")
|
||||
freeCompilerArgs.addAll(SHARED_COMPILER_ARGS)
|
||||
if (isJvmTarget) {
|
||||
freeCompilerArgs.add("-jvm-default=no-compatibility")
|
||||
@@ -274,28 +259,16 @@ private inline fun <reified T : KotlinBaseExtension> Project.configureKotlin() {
|
||||
|
||||
tasks.withType<KotlinCompile>().configureEach {
|
||||
compilerOptions {
|
||||
jvmTarget.set(if (isPublishedModule) JvmTarget.JVM_17 else JvmTarget.JVM_21)
|
||||
jvmTarget.set(JvmTarget.JVM_21)
|
||||
allWarningsAsErrors.set(warningsAsErrors)
|
||||
|
||||
// For non-KMP modules, configure compiler args here since they don't use targets.compilations.
|
||||
// KMP modules already set these via the targets block above — only jvmTarget/warnings needed here.
|
||||
if (T::class != KotlinMultiplatformExtension::class) {
|
||||
if (!isPublishedModule) {
|
||||
freeCompilerArgs.add("-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi")
|
||||
}
|
||||
freeCompilerArgs.add("-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi")
|
||||
freeCompilerArgs.addAll(SHARED_COMPILER_ARGS)
|
||||
freeCompilerArgs.add("-jvm-default=no-compatibility")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Published modules compile to JVM 17 for binary compatibility, but their test runtime
|
||||
// classpath includes non-published dependencies compiled to JVM 21. Override the test
|
||||
// launcher to JDK 21 so the JVM can load all class file versions at runtime.
|
||||
if (isPublishedModule) {
|
||||
val toolchains = extensions.getByType(JavaToolchainService::class.java)
|
||||
tasks.withType<Test>().configureEach {
|
||||
javaLauncher.set(toolchains.launcherFor { languageVersion.set(JavaLanguageVersion.of(APP_JDK)) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ plugins {
|
||||
alias(libs.plugins.firebase.crashlytics) apply false
|
||||
alias(libs.plugins.google.services) apply false
|
||||
alias(libs.plugins.room) apply false
|
||||
alias(libs.plugins.kotlin.android) apply false
|
||||
alias(libs.plugins.kotlin.jvm) apply false
|
||||
alias(libs.plugins.kotlin.multiplatform) apply false
|
||||
alias(libs.plugins.kotlin.parcelize) apply false
|
||||
|
||||
@@ -61,8 +61,6 @@ component_management:
|
||||
ignore:
|
||||
- "**/build/**"
|
||||
- "**/*.pb.kt" # Generated Protobuf code
|
||||
- "**/*.aidl" # AIDL interface files
|
||||
- "**/aidl/**" # Generated AIDL code
|
||||
- "core/resources/**" # Centralized resources
|
||||
- "**/test/**" # Unit tests
|
||||
- "**/androidTest/**" # Instrumented tests
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
# `:core:api` (Meshtastic Android API)
|
||||
|
||||
> **Deprecation notice**
|
||||
>
|
||||
> The AIDL-based service integration (`IMeshService`) is deprecated and will be removed in a future
|
||||
> release. The recommended integration path for ATAK and other external apps is the built-in
|
||||
> **Local TAK Server** introduced in `core:takserver`. Connect ATAK to `127.0.0.1:8087` (TCP) and
|
||||
> import the DataPackage exported from the TAK Config screen to complete setup. No AIDL binding or
|
||||
> JitPack dependency is required.
|
||||
|
||||
## Overview
|
||||
The `:core:api` module contains the AIDL interface and dependencies for third-party applications
|
||||
that currently integrate with the Meshtastic Android app via service binding. New integrations
|
||||
should use the Local TAK Server instead (see deprecation notice above).
|
||||
|
||||
## Integration
|
||||
|
||||
To communicate with the Meshtastic Android service from your own application, we recommend using **JitPack**.
|
||||
|
||||
### Dependencies
|
||||
Add the following to your `build.gradle.kts`:
|
||||
|
||||
```kotlin
|
||||
dependencies {
|
||||
// The core AIDL interface and Intent constants
|
||||
implementation("com.github.meshtastic.Meshtastic-Android:meshtastic-android-api:v2.x.x")
|
||||
|
||||
// Data models (DataPacket, MeshUser, NodeInfo, etc.) - Kotlin Multiplatform
|
||||
implementation("com.github.meshtastic.Meshtastic-Android:meshtastic-android-model:v2.x.x")
|
||||
|
||||
// Protobuf definitions (PortNum, Telemetry, etc.) - Kotlin Multiplatform
|
||||
implementation("com.github.meshtastic.Meshtastic-Android:meshtastic-android-proto:v2.x.x")
|
||||
}
|
||||
```
|
||||
*(Replace `v2.x.x` with the latest stable version).*
|
||||
|
||||
## Usage
|
||||
|
||||
### 1. Bind to the Service
|
||||
Use the `IMeshService` interface to bind to the Meshtastic service.
|
||||
|
||||
```kotlin
|
||||
val intent = Intent("com.geeksville.mesh.Service")
|
||||
// ... query package manager and bind
|
||||
```
|
||||
|
||||
### 2. Interact with the API
|
||||
Once bound, cast the `IBinder` to `IMeshService`.
|
||||
|
||||
### 3. Register a BroadcastReceiver
|
||||
Use `MeshtasticIntent` constants for actions. Remember to use `RECEIVER_EXPORTED` on Android 13+.
|
||||
|
||||
## Key Components
|
||||
- **`IMeshService.aidl`**: The primary AIDL interface.
|
||||
- **`MeshtasticIntent.kt`**: Defines Intent actions for received messages and status changes.
|
||||
|
||||
## Module dependency graph
|
||||
|
||||
<!--region graph-->
|
||||
```mermaid
|
||||
graph TB
|
||||
:core:api[api]:::android-library
|
||||
:core:api --> :core:model
|
||||
|
||||
classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
|
||||
classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
|
||||
classDef compose-desktop-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
|
||||
classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000;
|
||||
classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000;
|
||||
classDef android-library-compose fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000;
|
||||
classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000;
|
||||
classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000;
|
||||
classDef kmp-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000;
|
||||
classDef kmp-library-compose fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000;
|
||||
classDef kmp-library fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000;
|
||||
classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000;
|
||||
|
||||
```
|
||||
<!--endregion-->
|
||||
@@ -1,52 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
plugins {
|
||||
alias(libs.plugins.meshtastic.android.library)
|
||||
id("meshtastic.publishing")
|
||||
}
|
||||
|
||||
configure<com.android.build.api.dsl.LibraryExtension> {
|
||||
namespace = "org.meshtastic.core.api"
|
||||
buildFeatures { aidl = true }
|
||||
|
||||
defaultConfig {
|
||||
// Lowering minSdk to 21 for better compatibility with ATAK and other plugins
|
||||
minSdk = 21
|
||||
}
|
||||
|
||||
publishing { singleVariant("release") { withSourcesJar() } }
|
||||
}
|
||||
|
||||
// Suppress dep-ann warnings from AIDL-generated code where Javadoc @deprecated
|
||||
// doesn't produce @Deprecated annotations on Stub/Proxy override methods.
|
||||
tasks.withType<JavaCompile>().configureEach { options.compilerArgs.add("-Xlint:-dep-ann") }
|
||||
|
||||
// Map the Android component to a Maven publication.
|
||||
// afterEvaluate is required because AGP registers the "release" component lazily
|
||||
// after the android.publishing.singleVariant("release") configuration runs.
|
||||
afterEvaluate {
|
||||
publishing {
|
||||
publications {
|
||||
register<MavenPublication>("release") {
|
||||
from(components["release"])
|
||||
artifactId = "meshtastic-android-api"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies { api(projects.core.model) }
|
||||
@@ -1 +0,0 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
|
||||
@@ -1,3 +0,0 @@
|
||||
package org.meshtastic.core.model;
|
||||
|
||||
parcelable DataPacket;
|
||||
@@ -1,3 +0,0 @@
|
||||
package org.meshtastic.core.model;
|
||||
|
||||
parcelable MeshUser;
|
||||
@@ -1,3 +0,0 @@
|
||||
package org.meshtastic.core.model;
|
||||
|
||||
parcelable MyNodeInfo;
|
||||
@@ -1,3 +0,0 @@
|
||||
package org.meshtastic.core.model;
|
||||
|
||||
parcelable NodeInfo;
|
||||
@@ -1,3 +0,0 @@
|
||||
package org.meshtastic.core.model;
|
||||
|
||||
parcelable Position;
|
||||
@@ -1,207 +0,0 @@
|
||||
package org.meshtastic.core.service;
|
||||
|
||||
// Declare any non-default types here with import statements
|
||||
import org.meshtastic.core.model.DataPacket;
|
||||
import org.meshtastic.core.model.NodeInfo;
|
||||
import org.meshtastic.core.model.MeshUser;
|
||||
import org.meshtastic.core.model.Position;
|
||||
import org.meshtastic.core.model.MyNodeInfo;
|
||||
|
||||
/**
|
||||
This is the public android API for talking to meshtastic radios.
|
||||
|
||||
@deprecated The AIDL service integration is deprecated and will be removed in a future release.
|
||||
New integrations should connect via the built-in Local TAK Server on 127.0.0.1:8087 (TCP).
|
||||
Import the DataPackage from the TAK Config screen in the Meshtastic app to configure ATAK.
|
||||
|
||||
To connect to meshtastic you should bind to it per https://developer.android.com/guide/components/bound-services
|
||||
|
||||
The intent you use to reach the service should ideally use the action string:
|
||||
|
||||
val intent = Intent("com.geeksville.mesh.Service")
|
||||
|
||||
Or if using an explicit intent:
|
||||
|
||||
val intent = Intent().apply {
|
||||
setClassName(
|
||||
"com.geeksville.mesh",
|
||||
"org.meshtastic.core.service.MeshService"
|
||||
)
|
||||
}
|
||||
|
||||
In Android 11+ you *may* need to add the following to the client app's manifest to allow binding of the mesh service:
|
||||
<queries>
|
||||
<package android:name="com.geeksville.mesh" />
|
||||
</queries>
|
||||
For additional information, see https://developer.android.com/guide/topics/manifest/queries-element
|
||||
|
||||
|
||||
Once you have bound to the service you should register your broadcast receivers per https://developer.android.com/guide/components/broadcasts#context-registered-receivers
|
||||
|
||||
// com.geeksville.mesh.x broadcast intents, where x is:
|
||||
|
||||
// RECEIVED.<portnumm> - will **only** deliver packets for the specified port number. If a wellknown portnums.proto name for portnum is known it will be used
|
||||
// (i.e. com.geeksville.mesh.RECEIVED.TEXT_MESSAGE_APP) else the numeric portnum will be included as a base 10 integer (com.geeksville.mesh.RECEIVED.4403 etc...)
|
||||
|
||||
// NODE_CHANGE for new IDs appearing or disappearing
|
||||
// CONNECTION_CHANGED for losing/gaining connection to the packet radio
|
||||
// MESSAGE_STATUS_CHANGED for any message status changes (for sent messages only, payload will contain a message ID and a MessageStatus)
|
||||
|
||||
Note - these calls might throw RemoteException to indicate mesh error states
|
||||
*/
|
||||
interface IMeshService {
|
||||
/// Tell the service where to send its broadcasts of received packets
|
||||
/// This call is only required for manifest declared receivers. If your receiver is context-registered
|
||||
/// you don't need this.
|
||||
void subscribeReceiver(String packageName, String receiverName);
|
||||
|
||||
/**
|
||||
* Set the user info for this node
|
||||
*/
|
||||
void setOwner(in MeshUser user);
|
||||
|
||||
void setRemoteOwner(in int requestId, in int destNum, in byte []payload);
|
||||
void getRemoteOwner(in int requestId, in int destNum);
|
||||
|
||||
/// Return my unique user ID string
|
||||
String getMyId();
|
||||
|
||||
/// Return a unique packet ID
|
||||
int getPacketId();
|
||||
|
||||
/*
|
||||
Send a packet to a specified node name
|
||||
|
||||
typ is defined in mesh.proto Data.Type. For now juse use 0 to mean opaque bytes.
|
||||
|
||||
destId can be null to indicate "broadcast message"
|
||||
|
||||
messageStatus and id of the provided message will be updated by this routine to indicate
|
||||
message send status and the ID that can be used to locate the message in the future
|
||||
*/
|
||||
void send(inout DataPacket packet);
|
||||
|
||||
/**
|
||||
Get the IDs of everyone on the mesh. You should also subscribe for NODE_CHANGE broadcasts.
|
||||
*/
|
||||
List<NodeInfo> getNodes();
|
||||
|
||||
/// This method is only intended for use in our GUI, so the user can set radio options
|
||||
/// It returns a DeviceConfig protobuf.
|
||||
byte []getConfig();
|
||||
/// It sets a Config protobuf via admin packet
|
||||
void setConfig(in byte []payload);
|
||||
|
||||
/// Set and get a Config protobuf via admin packet
|
||||
void setRemoteConfig(in int requestId, in int destNum, in byte []payload);
|
||||
void getRemoteConfig(in int requestId, in int destNum, in int configTypeValue);
|
||||
|
||||
/// Set and get a ModuleConfig protobuf via admin packet
|
||||
void setModuleConfig(in int requestId, in int destNum, in byte []payload);
|
||||
void getModuleConfig(in int requestId, in int destNum, in int moduleConfigTypeValue);
|
||||
|
||||
/// Set and get the Ext Notification Ringtone string via admin packet
|
||||
void setRingtone(in int destNum, in String ringtone);
|
||||
void getRingtone(in int requestId, in int destNum);
|
||||
|
||||
/// Set and get the Canned Message Messages string via admin packet
|
||||
void setCannedMessages(in int destNum, in String messages);
|
||||
void getCannedMessages(in int requestId, in int destNum);
|
||||
|
||||
/// This method is only intended for use in our GUI, so the user can set radio options
|
||||
/// It sets a Channel protobuf via admin packet
|
||||
void setChannel(in byte []payload);
|
||||
|
||||
/// Set and get a Channel protobuf via admin packet
|
||||
void setRemoteChannel(in int requestId, in int destNum, in byte []payload);
|
||||
void getRemoteChannel(in int requestId, in int destNum, in int channelIndex);
|
||||
|
||||
/// Send beginEditSettings admin packet to nodeNum
|
||||
void beginEditSettings(in int destNum);
|
||||
|
||||
/// Send commitEditSettings admin packet to nodeNum
|
||||
void commitEditSettings(in int destNum);
|
||||
|
||||
/// delete a specific nodeNum from nodeDB
|
||||
void removeByNodenum(in int requestID, in int nodeNum);
|
||||
|
||||
/// Send position packet with wantResponse to nodeNum
|
||||
void requestPosition(in int destNum, in Position position);
|
||||
|
||||
/// Send setFixedPosition admin packet (or removeFixedPosition if Position is empty)
|
||||
void setFixedPosition(in int destNum, in Position position);
|
||||
|
||||
/// Send traceroute packet with wantResponse to nodeNum
|
||||
void requestTraceroute(in int requestId, in int destNum);
|
||||
|
||||
/// Send neighbor info packet with wantResponse to nodeNum
|
||||
void requestNeighborInfo(in int requestId, in int destNum);
|
||||
|
||||
/// Send Shutdown admin packet to nodeNum
|
||||
void requestShutdown(in int requestId, in int destNum);
|
||||
|
||||
/// Send Reboot admin packet to nodeNum
|
||||
void requestReboot(in int requestId, in int destNum);
|
||||
|
||||
/// Send FactoryReset admin packet to nodeNum
|
||||
void requestFactoryReset(in int requestId, in int destNum);
|
||||
|
||||
/// Send reboot to DFU admin packet
|
||||
void rebootToDfu(in int destNum);
|
||||
|
||||
/// Send NodedbReset admin packet to nodeNum
|
||||
void requestNodedbReset(in int requestId, in int destNum, in boolean preserveFavorites);
|
||||
|
||||
/// Returns a ChannelSet protobuf
|
||||
byte []getChannelSet();
|
||||
|
||||
/**
|
||||
Is the packet radio currently connected to the phone? Returns a ConnectionState string.
|
||||
*/
|
||||
String connectionState();
|
||||
|
||||
/**
|
||||
* @deprecated For internal use only. External callers must not invoke this method;
|
||||
* it will be removed from the public API in a future release.
|
||||
*/
|
||||
boolean setDeviceAddress(String deviceAddr);
|
||||
|
||||
/// Get basic device hardware info about our connected radio. Will never return NULL. Will return NULL
|
||||
/// if no my node info is available (i.e. it will not throw an exception)
|
||||
MyNodeInfo getMyNodeInfo();
|
||||
|
||||
/**
|
||||
* @deprecated No-op stub — firmware update is now handled entirely by the in-app OTA system.
|
||||
* This method will be removed from the public API in a future release.
|
||||
*/
|
||||
void startFirmwareUpdate();
|
||||
|
||||
/**
|
||||
* @deprecated Always returns {@code -4}, which is outside the documented range.
|
||||
* Firmware update progress is now tracked internally by the in-app OTA system.
|
||||
* This method will be removed from the public API in a future release.
|
||||
*/
|
||||
int getUpdateStatus();
|
||||
|
||||
/// Start providing location (from phone GPS) to mesh
|
||||
void startProvideLocation();
|
||||
|
||||
/// Stop providing location (from phone GPS) to mesh
|
||||
void stopProvideLocation();
|
||||
|
||||
/// Send request for node UserInfo
|
||||
void requestUserInfo(in int destNum);
|
||||
|
||||
/// Request device connection status from the radio
|
||||
void getDeviceConnectionStatus(in int requestId, in int destNum);
|
||||
|
||||
/// Send request for telemetry to nodeNum
|
||||
void requestTelemetry(in int requestId, in int destNum, in int type);
|
||||
|
||||
/**
|
||||
* Tell the node to reboot into OTA mode for firmware update via BLE or WiFi (ESP32 only)
|
||||
* mode is 1 for BLE, 2 for WiFi
|
||||
* hash is the 32-byte firmware SHA256 hash (optional, can be null)
|
||||
*/
|
||||
void requestRebootOta(in int requestId, in int destNum, in int mode, in byte []hash);
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.api
|
||||
|
||||
import org.meshtastic.core.api.MeshtasticIntent.EXTRA_CONNECTED
|
||||
import org.meshtastic.core.api.MeshtasticIntent.EXTRA_NODEINFO
|
||||
import org.meshtastic.core.api.MeshtasticIntent.EXTRA_PACKET_ID
|
||||
import org.meshtastic.core.api.MeshtasticIntent.EXTRA_STATUS
|
||||
|
||||
/**
|
||||
* Constants for Meshtastic Android Intents. These are used by external applications to communicate with the Meshtastic
|
||||
* service.
|
||||
*/
|
||||
object MeshtasticIntent {
|
||||
private const val PREFIX = "com.geeksville.mesh"
|
||||
|
||||
/** Broadcast when a node's information changes. Extra: [EXTRA_NODEINFO] */
|
||||
const val ACTION_NODE_CHANGE = "$PREFIX.NODE_CHANGE"
|
||||
|
||||
/** Broadcast when the mesh radio connects. Extra: [EXTRA_CONNECTED] */
|
||||
const val ACTION_MESH_CONNECTED = "$PREFIX.MESH_CONNECTED"
|
||||
|
||||
/** Broadcast when the mesh radio disconnects. */
|
||||
const val ACTION_MESH_DISCONNECTED = "$PREFIX.MESH_DISCONNECTED"
|
||||
|
||||
/**
|
||||
* Legacy broadcast for connection changes. Extra: [EXTRA_CONNECTED]
|
||||
*
|
||||
* Prefer [ACTION_MESH_CONNECTED] / [ACTION_MESH_DISCONNECTED] instead. This constant will be removed from the
|
||||
* public API in a future release.
|
||||
*/
|
||||
@Deprecated(
|
||||
message = "Use ACTION_MESH_CONNECTED / ACTION_MESH_DISCONNECTED instead.",
|
||||
replaceWith = ReplaceWith("ACTION_MESH_CONNECTED"),
|
||||
)
|
||||
const val ACTION_CONNECTION_CHANGED = "$PREFIX.CONNECTION_CHANGED"
|
||||
|
||||
/** Broadcast for message status updates. Extras: [EXTRA_PACKET_ID], [EXTRA_STATUS] */
|
||||
const val ACTION_MESSAGE_STATUS = "$PREFIX.MESSAGE_STATUS"
|
||||
|
||||
/** Received a text message. */
|
||||
const val ACTION_RECEIVED_TEXT_MESSAGE_APP = "$PREFIX.RECEIVED.TEXT_MESSAGE_APP"
|
||||
|
||||
/** Received a position update. */
|
||||
const val ACTION_RECEIVED_POSITION_APP = "$PREFIX.RECEIVED.POSITION_APP"
|
||||
|
||||
/** Received node info. */
|
||||
const val ACTION_RECEIVED_NODEINFO_APP = "$PREFIX.RECEIVED.NODEINFO_APP"
|
||||
|
||||
/** Received telemetry data. */
|
||||
const val ACTION_RECEIVED_TELEMETRY_APP = "$PREFIX.RECEIVED.TELEMETRY_APP"
|
||||
|
||||
/** Received ATAK Plugin data. */
|
||||
const val ACTION_RECEIVED_ATAK_PLUGIN = "$PREFIX.RECEIVED.ATAK_PLUGIN"
|
||||
|
||||
/** Received ATAK Forwarder data. */
|
||||
const val ACTION_RECEIVED_ATAK_FORWARDER = "$PREFIX.RECEIVED.ATAK_FORWARDER"
|
||||
|
||||
/** Received detection sensor data. */
|
||||
const val ACTION_RECEIVED_DETECTION_SENSOR_APP = "$PREFIX.RECEIVED.DETECTION_SENSOR_APP"
|
||||
|
||||
/** Received private app data. */
|
||||
const val ACTION_RECEIVED_PRIVATE_APP = "$PREFIX.RECEIVED.PRIVATE_APP"
|
||||
|
||||
// standard EXTRA bundle definitions
|
||||
const val EXTRA_CONNECTED = "$PREFIX.Connected"
|
||||
const val EXTRA_PAYLOAD = "$PREFIX.Payload"
|
||||
const val EXTRA_NODEINFO = "$PREFIX.NodeInfo"
|
||||
const val EXTRA_PACKET_ID = "$PREFIX.PacketId"
|
||||
const val EXTRA_STATUS = "$PREFIX.Status"
|
||||
}
|
||||
@@ -32,7 +32,6 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -56,13 +55,14 @@ import co.touchlab.kermit.Logger
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.asExecutor
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.close
|
||||
import org.meshtastic.core.ui.icon.Close
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
import org.meshtastic.core.ui.util.BarcodeScanner
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
@Composable
|
||||
fun rememberBarcodeScanner(onResult: (String?) -> Unit): BarcodeScanner {
|
||||
@@ -179,11 +179,9 @@ private fun ScannerReticule() {
|
||||
private fun ScannerView(onResult: (String?) -> Unit, onCameraReady: (Boolean) -> Unit) {
|
||||
val context = LocalContext.current
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
val cameraExecutor = remember { Executors.newSingleThreadExecutor() }
|
||||
val cameraExecutor = remember { Dispatchers.Default.asExecutor() }
|
||||
var surfaceRequest by remember { mutableStateOf<SurfaceRequest?>(null) }
|
||||
|
||||
DisposableEffect(Unit) { onDispose { cameraExecutor.shutdown() } }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
|
||||
cameraProviderFuture.addListener(
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.meshtastic.kmp.library)
|
||||
alias(libs.plugins.kotlin.parcelize)
|
||||
id("meshtastic.kmp.jvm.android")
|
||||
id("meshtastic.koin")
|
||||
}
|
||||
|
||||
@@ -20,32 +20,14 @@ import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.IntentCompat
|
||||
import androidx.core.os.ParcelCompat
|
||||
|
||||
/** Reads a [Parcelable] from a [Parcel] in a backward-compatible way. */
|
||||
inline fun <reified T : Parcelable> Parcel.readParcelableCompat(loader: ClassLoader?): T? =
|
||||
ParcelCompat.readParcelable(this, loader, T::class.java)
|
||||
|
||||
/** Retrieves a [Parcelable] extra from an [Intent] in a backward-compatible way. */
|
||||
inline fun <reified T : Parcelable> Intent.getParcelableExtraCompat(key: String?): T? =
|
||||
IntentCompat.getParcelableExtra(this, key, T::class.java)
|
||||
|
||||
/** Retrieves [PackageInfo] for a given package name in a backward-compatible way. */
|
||||
fun PackageManager.getPackageInfoCompat(packageName: String, flags: Int = 0): PackageInfo =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(flags.toLong()))
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
getPackageInfo(packageName, flags)
|
||||
}
|
||||
|
||||
/** Registers a [BroadcastReceiver] using [ContextCompat] to ensure consistent behavior across Android versions. */
|
||||
fun Context.registerReceiverCompat(
|
||||
receiver: BroadcastReceiver,
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.common.util
|
||||
|
||||
import android.os.RemoteException
|
||||
import co.touchlab.kermit.Logger
|
||||
|
||||
/**
|
||||
* Wraps an operation and converts any thrown exceptions into [RemoteException] for safe return through an AIDL
|
||||
* interface.
|
||||
*/
|
||||
fun <T> toRemoteExceptions(inner: () -> T): T = try {
|
||||
inner()
|
||||
} catch (@Suppress("TooGenericExceptionCaught") ex: Exception) {
|
||||
Logger.e(ex) { "Uncaught exception in service call, returning RemoteException to client" }
|
||||
when (ex) {
|
||||
is RemoteException -> throw ex
|
||||
else -> throw RemoteException(ex.message).apply { initCause(ex) }
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.common.util
|
||||
|
||||
import android.os.Parcelable
|
||||
|
||||
actual typealias CommonParcelable = Parcelable
|
||||
|
||||
actual typealias CommonParcelize = kotlinx.parcelize.Parcelize
|
||||
|
||||
actual typealias CommonIgnoredOnParcel = kotlinx.parcelize.IgnoredOnParcel
|
||||
|
||||
actual typealias CommonParceler<T> = kotlinx.parcelize.Parceler<T>
|
||||
|
||||
actual typealias CommonTypeParceler<T, P> = kotlinx.parcelize.TypeParceler<T, P>
|
||||
|
||||
actual typealias CommonParcel = android.os.Parcel
|
||||
@@ -1,58 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.common.util
|
||||
|
||||
/** Platform-agnostic Parcelable interface. */
|
||||
expect interface CommonParcelable
|
||||
|
||||
/** Platform-agnostic Parcelize annotation. */
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
expect annotation class CommonParcelize()
|
||||
|
||||
/** Platform-agnostic IgnoredOnParcel annotation. */
|
||||
@Target(AnnotationTarget.PROPERTY)
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
expect annotation class CommonIgnoredOnParcel()
|
||||
|
||||
/** Platform-agnostic Parceler interface. */
|
||||
expect interface CommonParceler<T> {
|
||||
fun create(parcel: CommonParcel): T
|
||||
|
||||
fun T.write(parcel: CommonParcel, flags: Int)
|
||||
}
|
||||
|
||||
/** Platform-agnostic TypeParceler annotation. */
|
||||
@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY)
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
@Repeatable
|
||||
expect annotation class CommonTypeParceler<T, P : CommonParceler<in T>>()
|
||||
|
||||
/** Platform-agnostic Parcel representation for manual parceling (e.g. AIDL support). */
|
||||
expect class CommonParcel {
|
||||
fun readString(): String?
|
||||
|
||||
fun readInt(): Int
|
||||
|
||||
fun readLong(): Long
|
||||
|
||||
fun readFloat(): Float
|
||||
|
||||
fun createByteArray(): ByteArray?
|
||||
|
||||
fun writeByteArray(b: ByteArray?)
|
||||
}
|
||||
@@ -45,38 +45,3 @@ actual fun currentLocaleCode(): String = "en"
|
||||
actual fun currentLocaleQualifier(): String = "en"
|
||||
|
||||
actual fun String?.isValidAddress(): Boolean = false
|
||||
|
||||
actual interface CommonParcelable
|
||||
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
actual annotation class CommonParcelize actual constructor()
|
||||
|
||||
@Target(AnnotationTarget.PROPERTY)
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
actual annotation class CommonIgnoredOnParcel actual constructor()
|
||||
|
||||
actual interface CommonParceler<T> {
|
||||
actual fun create(parcel: CommonParcel): T
|
||||
|
||||
actual fun T.write(parcel: CommonParcel, flags: Int)
|
||||
}
|
||||
|
||||
@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY)
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
@Repeatable
|
||||
actual annotation class CommonTypeParceler<T, P : CommonParceler<in T>> actual constructor()
|
||||
|
||||
actual class CommonParcel {
|
||||
actual fun readString(): String? = null
|
||||
|
||||
actual fun readInt(): Int = 0
|
||||
|
||||
actual fun readLong(): Long = 0L
|
||||
|
||||
actual fun readFloat(): Float = 0.0f
|
||||
|
||||
actual fun createByteArray(): ByteArray? = null
|
||||
|
||||
actual fun writeByteArray(b: ByteArray?) {}
|
||||
}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.common.util
|
||||
|
||||
actual interface CommonParcelable
|
||||
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
actual annotation class CommonParcelize
|
||||
|
||||
@Target(AnnotationTarget.PROPERTY)
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
actual annotation class CommonIgnoredOnParcel
|
||||
|
||||
actual interface CommonParceler<T> {
|
||||
actual fun create(parcel: CommonParcel): T
|
||||
|
||||
actual fun T.write(parcel: CommonParcel, flags: Int)
|
||||
}
|
||||
|
||||
@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY)
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
@Repeatable
|
||||
actual annotation class CommonTypeParceler<T, P : CommonParceler<in T>>
|
||||
|
||||
actual class CommonParcel {
|
||||
actual fun readString(): String? = unsupportedParcelOperation()
|
||||
|
||||
actual fun readInt(): Int = unsupportedParcelOperation()
|
||||
|
||||
actual fun readLong(): Long = unsupportedParcelOperation()
|
||||
|
||||
actual fun readFloat(): Float = unsupportedParcelOperation()
|
||||
|
||||
actual fun createByteArray(): ByteArray? = unsupportedParcelOperation()
|
||||
|
||||
actual fun writeByteArray(b: ByteArray?) = unsupportedParcelOperation<Unit>()
|
||||
}
|
||||
|
||||
private fun <T> unsupportedParcelOperation(): T =
|
||||
error("CommonParcel is unavailable on JVM smoke targets. Manual parcel operations remain Android-only.")
|
||||
@@ -20,7 +20,7 @@ import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.NodeAddress
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
import org.meshtastic.core.repository.PacketRepository
|
||||
import org.meshtastic.core.repository.RadioConfigRepository
|
||||
@@ -377,7 +377,7 @@ class AiFunctionProviderImpl(
|
||||
val unreadCount = packetRepository.getUnreadCount(contactKey)
|
||||
if (unreadCount <= 0) return@mapNotNull null
|
||||
|
||||
val isBroadcast = lastPacket.to == DataPacket.ID_BROADCAST
|
||||
val isBroadcast = lastPacket.to == NodeAddress.ID_BROADCAST
|
||||
val displayName =
|
||||
if (isBroadcast) {
|
||||
val channelIndex = contactKey.firstOrNull()?.digitToIntOrNull() ?: 0
|
||||
@@ -420,7 +420,7 @@ class AiFunctionProviderImpl(
|
||||
// Try node name first
|
||||
when (val nodeResult = fuzzyNameResolver.resolveNodeName(name)) {
|
||||
is NodeNameResult.Found -> {
|
||||
val channelIndex = DataPacket.PKC_CHANNEL_INDEX
|
||||
val channelIndex = NodeAddress.PKC_CHANNEL_INDEX
|
||||
return "${channelIndex}${nodeResult.userId}"
|
||||
}
|
||||
|
||||
@@ -433,7 +433,7 @@ class AiFunctionProviderImpl(
|
||||
|
||||
// Try channel name
|
||||
return when (val channelResult = fuzzyNameResolver.resolveChannelName(name)) {
|
||||
is ChannelNameResult.Found -> "${channelResult.channelIndex}${DataPacket.ID_BROADCAST}"
|
||||
is ChannelNameResult.Found -> "${channelResult.channelIndex}${NodeAddress.ID_BROADCAST}"
|
||||
is ChannelNameResult.Ambiguous -> null
|
||||
is ChannelNameResult.NotFound -> null
|
||||
}
|
||||
@@ -457,7 +457,7 @@ class AiFunctionProviderImpl(
|
||||
is NodeNameResult.Found -> {
|
||||
// DM contact key format: channel_index + nodeId
|
||||
// For PKC DMs, use channel index 8; for legacy use no channel prefix
|
||||
val channelIndex = DataPacket.PKC_CHANNEL_INDEX
|
||||
val channelIndex = NodeAddress.PKC_CHANNEL_INDEX
|
||||
ResolvedContact.Resolved(
|
||||
contactKey = "${channelIndex}${result.userId}",
|
||||
channelName = "DM to $recipientName",
|
||||
@@ -477,7 +477,7 @@ class AiFunctionProviderImpl(
|
||||
return when (val result = fuzzyNameResolver.resolveChannelName(channelName)) {
|
||||
is ChannelNameResult.Found ->
|
||||
ResolvedContact.Resolved(
|
||||
contactKey = "${result.channelIndex}${DataPacket.ID_BROADCAST}",
|
||||
contactKey = "${result.channelIndex}${NodeAddress.ID_BROADCAST}",
|
||||
channelName = result.name,
|
||||
)
|
||||
|
||||
@@ -490,7 +490,7 @@ class AiFunctionProviderImpl(
|
||||
// Default: broadcast on primary channel (index 0)
|
||||
val channelSet = radioConfigRepository.channelSetFlow.first()
|
||||
val primaryName = channelSet.settings.firstOrNull()?.name?.ifBlank { "Primary" } ?: "Primary"
|
||||
return ResolvedContact.Resolved(contactKey = "0${DataPacket.ID_BROADCAST}", channelName = primaryName)
|
||||
return ResolvedContact.Resolved(contactKey = "0${NodeAddress.ID_BROADCAST}", channelName = primaryName)
|
||||
}
|
||||
|
||||
private sealed class ResolvedContact {
|
||||
|
||||
@@ -29,6 +29,7 @@ import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.common.util.nowMillis
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.model.NodeAddress
|
||||
import org.meshtastic.core.model.Position
|
||||
import org.meshtastic.core.model.TelemetryType
|
||||
import org.meshtastic.core.model.util.isWithinSizeLimit
|
||||
@@ -99,7 +100,7 @@ class CommandSenderImpl(
|
||||
/**
|
||||
* Resolves the correct channel index for sending a packet to [toNum].
|
||||
*
|
||||
* PKI encryption ([DataPacket.PKC_CHANNEL_INDEX]) is only used for **admin** packets, where end-to-end encryption
|
||||
* PKI encryption ([NodeAddress.PKC_CHANNEL_INDEX]) is only used for **admin** packets, where end-to-end encryption
|
||||
* is appropriate. Protocol-level requests (traceroute, telemetry, position, nodeinfo, neighborinfo) must NOT use
|
||||
* PKI because relay nodes need to read and/or modify the inner payload (e.g. traceroute appends each hop's node
|
||||
* number). These requests fall back to the node's heard-on channel.
|
||||
@@ -112,7 +113,7 @@ class CommandSenderImpl(
|
||||
return when {
|
||||
myNum == toNum -> 0
|
||||
|
||||
myNode?.hasPKC == true && destNode?.hasPKC == true -> DataPacket.PKC_CHANNEL_INDEX
|
||||
myNode?.hasPKC == true && destNode?.hasPKC == true -> NodeAddress.PKC_CHANNEL_INDEX
|
||||
|
||||
else ->
|
||||
channelSet.value.settings
|
||||
@@ -127,7 +128,7 @@ class CommandSenderImpl(
|
||||
*/
|
||||
private fun getChannelIndex(toNum: Int): Int = nodeManager.nodeDBbyNodeNum[toNum]?.channel ?: 0
|
||||
|
||||
override fun sendData(p: DataPacket) {
|
||||
override suspend fun sendData(p: DataPacket) {
|
||||
if (p.id == 0) p.id = generatePacketId()
|
||||
val bytes = p.bytes ?: ByteString.EMPTY
|
||||
require(p.dataType != 0) { "Port numbers must be non-zero!" }
|
||||
@@ -152,10 +153,10 @@ class CommandSenderImpl(
|
||||
sendNow(p)
|
||||
}
|
||||
|
||||
private fun sendNow(p: DataPacket) {
|
||||
private suspend fun sendNow(p: DataPacket) {
|
||||
val meshPacket =
|
||||
buildMeshPacket(
|
||||
to = resolveNodeNum(p.to ?: DataPacket.ID_BROADCAST),
|
||||
to = resolveNodeNum(NodeAddress.fromString(p.to)),
|
||||
id = p.id,
|
||||
wantAck = p.wantAck,
|
||||
hopLimit = if (p.hopLimit > 0) p.hopLimit else computeHopLimit(),
|
||||
@@ -172,7 +173,7 @@ class CommandSenderImpl(
|
||||
packetHandler.sendToRadio(meshPacket)
|
||||
}
|
||||
|
||||
override fun sendAdmin(destNum: Int, requestId: Int, wantResponse: Boolean, initFn: () -> AdminMessage) {
|
||||
override suspend fun sendAdmin(destNum: Int, requestId: Int, wantResponse: Boolean, initFn: () -> AdminMessage) {
|
||||
val adminMsg = initFn().copy(session_passkey = sessionManager.getPasskey(destNum))
|
||||
val packet =
|
||||
buildAdminPacket(to = destNum, id = requestId, wantResponse = wantResponse, adminMessage = adminMsg)
|
||||
@@ -191,7 +192,7 @@ class CommandSenderImpl(
|
||||
return packetHandler.sendToRadioAndAwait(packet)
|
||||
}
|
||||
|
||||
override fun sendPosition(pos: ProtoPosition, destNum: Int?, wantResponse: Boolean) {
|
||||
override suspend fun sendPosition(pos: ProtoPosition, destNum: Int?, wantResponse: Boolean) {
|
||||
val myNum = nodeManager.myNodeNum.value ?: return
|
||||
val idNum = destNum ?: myNum
|
||||
Logger.d { "Sending our position/time to=$idNum $pos" }
|
||||
@@ -215,7 +216,7 @@ class CommandSenderImpl(
|
||||
)
|
||||
}
|
||||
|
||||
override fun requestPosition(destNum: Int, currentPosition: Position) {
|
||||
override suspend fun requestPosition(destNum: Int, currentPosition: Position) {
|
||||
val meshPosition =
|
||||
ProtoPosition(
|
||||
latitude_i = Position.degI(currentPosition.latitude),
|
||||
@@ -238,7 +239,7 @@ class CommandSenderImpl(
|
||||
)
|
||||
}
|
||||
|
||||
override fun setFixedPosition(destNum: Int, pos: Position) {
|
||||
override suspend fun setFixedPosition(destNum: Int, pos: Position) {
|
||||
val meshPos =
|
||||
ProtoPosition(
|
||||
latitude_i = Position.degI(pos.latitude),
|
||||
@@ -255,7 +256,7 @@ class CommandSenderImpl(
|
||||
nodeManager.handleReceivedPosition(destNum, nodeManager.myNodeNum.value ?: 0, meshPos, nowMillis)
|
||||
}
|
||||
|
||||
override fun requestUserInfo(destNum: Int) {
|
||||
override suspend fun requestUserInfo(destNum: Int) {
|
||||
val myNum = nodeManager.myNodeNum.value ?: return
|
||||
val myNode = nodeManager.nodeDBbyNodeNum[myNum] ?: return
|
||||
packetHandler.sendToRadio(
|
||||
@@ -272,7 +273,7 @@ class CommandSenderImpl(
|
||||
)
|
||||
}
|
||||
|
||||
override fun requestTraceroute(requestId: Int, destNum: Int) {
|
||||
override suspend fun requestTraceroute(requestId: Int, destNum: Int) {
|
||||
tracerouteHandler.recordStartTime(requestId)
|
||||
packetHandler.sendToRadio(
|
||||
buildMeshPacket(
|
||||
@@ -285,7 +286,7 @@ class CommandSenderImpl(
|
||||
)
|
||||
}
|
||||
|
||||
override fun requestTelemetry(requestId: Int, destNum: Int, typeValue: Int) {
|
||||
override suspend fun requestTelemetry(requestId: Int, destNum: Int, typeValue: Int) {
|
||||
val type = TelemetryType.entries.getOrNull(typeValue) ?: TelemetryType.DEVICE
|
||||
|
||||
val portNum: PortNum
|
||||
@@ -319,7 +320,7 @@ class CommandSenderImpl(
|
||||
)
|
||||
}
|
||||
|
||||
override fun requestNeighborInfo(requestId: Int, destNum: Int) {
|
||||
override suspend fun requestNeighborInfo(requestId: Int, destNum: Int) {
|
||||
neighborInfoHandler.recordStartTime(requestId)
|
||||
val myNum = nodeManager.myNodeNum.value ?: 0
|
||||
if (destNum == myNum) {
|
||||
@@ -373,20 +374,16 @@ class CommandSenderImpl(
|
||||
}
|
||||
}
|
||||
|
||||
fun resolveNodeNum(toId: String): Int = when (toId) {
|
||||
DataPacket.ID_BROADCAST -> DataPacket.NODENUM_BROADCAST
|
||||
fun resolveNodeNum(address: NodeAddress): Int = when (address) {
|
||||
NodeAddress.Broadcast -> NodeAddress.NODENUM_BROADCAST
|
||||
|
||||
else -> {
|
||||
val numericNum =
|
||||
if (toId.startsWith(NODE_ID_PREFIX)) {
|
||||
toId.substring(NODE_ID_START_INDEX).toLongOrNull(HEX_RADIX)?.toInt()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
numericNum
|
||||
?: nodeManager.nodeDBbyID[toId]?.num
|
||||
?: throw IllegalArgumentException("Unknown node ID $toId")
|
||||
}
|
||||
NodeAddress.Local -> nodeManager.myNodeNum.value ?: 0
|
||||
|
||||
is NodeAddress.ByNum -> address.num
|
||||
|
||||
is NodeAddress.ById ->
|
||||
nodeManager.getNodeById(address.id)?.num
|
||||
?: throw IllegalArgumentException("Unknown node ID ${address.id}")
|
||||
}
|
||||
|
||||
private fun buildMeshPacket(
|
||||
@@ -404,7 +401,7 @@ class CommandSenderImpl(
|
||||
var publicKey: ByteString = ByteString.EMPTY
|
||||
var actualChannel = channel
|
||||
|
||||
if (channel == DataPacket.PKC_CHANNEL_INDEX) {
|
||||
if (channel == NodeAddress.PKC_CHANNEL_INDEX) {
|
||||
pkiEncrypted = true
|
||||
val destNode = nodeManager.nodeDBbyNodeNum[to]
|
||||
// Resolve the public key using the same fallback as Node.hasPKC:
|
||||
@@ -457,9 +454,6 @@ class CommandSenderImpl(
|
||||
private const val PACKET_ID_SHIFT_BITS = 32
|
||||
|
||||
private const val ADMIN_CHANNEL_NAME = "admin"
|
||||
private const val NODE_ID_PREFIX = "!"
|
||||
private const val NODE_ID_START_INDEX = 1
|
||||
private const val HEX_RADIX = 16
|
||||
|
||||
private const val DEFAULT_HOP_LIMIT = 3
|
||||
}
|
||||
|
||||
@@ -23,12 +23,14 @@ import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.common.util.handledLaunch
|
||||
import org.meshtastic.core.common.util.ioDispatcher
|
||||
import org.meshtastic.core.repository.FromRadioPacketHandler
|
||||
import org.meshtastic.core.repository.MeshRouter
|
||||
import org.meshtastic.core.repository.MeshConfigFlowManager
|
||||
import org.meshtastic.core.repository.MeshConfigHandler
|
||||
import org.meshtastic.core.repository.MqttManager
|
||||
import org.meshtastic.core.repository.Notification
|
||||
import org.meshtastic.core.repository.NotificationManager
|
||||
import org.meshtastic.core.repository.PacketHandler
|
||||
import org.meshtastic.core.repository.ServiceRepository
|
||||
import org.meshtastic.core.repository.ServiceStateWriter
|
||||
import org.meshtastic.core.repository.XModemManager
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.client_notification
|
||||
import org.meshtastic.core.resources.duplicated_public_key_title
|
||||
@@ -43,8 +45,10 @@ import org.meshtastic.proto.FromRadio
|
||||
/** Implementation of [FromRadioPacketHandler] that dispatches [FromRadio] variants to specialized handlers. */
|
||||
@Single
|
||||
class FromRadioPacketHandlerImpl(
|
||||
private val serviceRepository: ServiceRepository,
|
||||
private val router: Lazy<MeshRouter>,
|
||||
private val serviceStateWriter: ServiceStateWriter,
|
||||
private val configFlowManager: Lazy<MeshConfigFlowManager>,
|
||||
private val configHandler: Lazy<MeshConfigHandler>,
|
||||
private val xmodemManager: Lazy<XModemManager>,
|
||||
private val mqttManager: MqttManager,
|
||||
private val packetHandler: PacketHandler,
|
||||
private val notificationManager: NotificationManager,
|
||||
@@ -71,34 +75,34 @@ class FromRadioPacketHandlerImpl(
|
||||
val xmodemPacket = proto.xmodemPacket
|
||||
|
||||
when {
|
||||
myInfo != null -> router.value.configFlowManager.handleMyInfo(myInfo)
|
||||
myInfo != null -> configFlowManager.value.handleMyInfo(myInfo)
|
||||
|
||||
// deviceuiConfig arrives immediately after my_info (STATE_SEND_UIDATA). It carries
|
||||
// the device's display, theme, node-filter, and other UI preferences.
|
||||
deviceUIConfig != null -> router.value.configHandler.handleDeviceUIConfig(deviceUIConfig)
|
||||
deviceUIConfig != null -> configHandler.value.handleDeviceUIConfig(deviceUIConfig)
|
||||
|
||||
metadata != null -> router.value.configFlowManager.handleLocalMetadata(metadata)
|
||||
metadata != null -> configFlowManager.value.handleLocalMetadata(metadata)
|
||||
|
||||
nodeInfo != null -> {
|
||||
router.value.configFlowManager.handleNodeInfo(nodeInfo)
|
||||
serviceRepository.setConnectionProgress("Nodes (${router.value.configFlowManager.newNodeCount})")
|
||||
configFlowManager.value.handleNodeInfo(nodeInfo)
|
||||
serviceStateWriter.setConnectionProgress("Nodes (${configFlowManager.value.newNodeCount})")
|
||||
}
|
||||
|
||||
configCompleteId != null -> router.value.configFlowManager.handleConfigComplete(configCompleteId)
|
||||
configCompleteId != null -> configFlowManager.value.handleConfigComplete(configCompleteId)
|
||||
|
||||
mqttProxyMessage != null -> mqttManager.handleMqttProxyMessage(mqttProxyMessage)
|
||||
|
||||
queueStatus != null -> packetHandler.handleQueueStatus(queueStatus)
|
||||
|
||||
config != null -> router.value.configHandler.handleDeviceConfig(config)
|
||||
config != null -> configHandler.value.handleDeviceConfig(config)
|
||||
|
||||
moduleConfig != null -> router.value.configHandler.handleModuleConfig(moduleConfig)
|
||||
moduleConfig != null -> configHandler.value.handleModuleConfig(moduleConfig)
|
||||
|
||||
channel != null -> router.value.configHandler.handleChannel(channel)
|
||||
channel != null -> configHandler.value.handleChannel(channel)
|
||||
|
||||
fileInfo != null -> router.value.configFlowManager.handleFileInfo(fileInfo)
|
||||
fileInfo != null -> configFlowManager.value.handleFileInfo(fileInfo)
|
||||
|
||||
xmodemPacket != null -> router.value.xmodemManager.handleIncomingXModem(xmodemPacket)
|
||||
xmodemPacket != null -> xmodemManager.value.handleIncomingXModem(xmodemPacket)
|
||||
|
||||
clientNotification != null -> handleClientNotification(clientNotification)
|
||||
|
||||
@@ -106,13 +110,13 @@ class FromRadioPacketHandlerImpl(
|
||||
// Re-handshake immediately rather than waiting for the 30s stall guard.
|
||||
proto.rebooted != null -> {
|
||||
Logger.w { "Firmware rebooted (rebooted=${proto.rebooted}), re-initiating handshake" }
|
||||
router.value.configFlowManager.triggerWantConfig()
|
||||
configFlowManager.value.triggerWantConfig()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleClientNotification(cn: ClientNotification) {
|
||||
serviceRepository.setClientNotification(cn)
|
||||
serviceStateWriter.setClientNotification(cn)
|
||||
|
||||
scope.handledLaunch {
|
||||
val inform = cn.key_verification_number_inform
|
||||
|
||||
@@ -68,7 +68,7 @@ class HistoryManagerImpl(private val meshPrefs: MeshPrefs, private val packetHan
|
||||
private fun activeDeviceAddress(): String? =
|
||||
meshPrefs.deviceAddress.value?.takeIf { !it.equals(NO_DEVICE_SELECTED, ignoreCase = true) && it.isNotBlank() }
|
||||
|
||||
override fun requestHistoryReplay(
|
||||
override suspend fun requestHistoryReplay(
|
||||
trigger: String,
|
||||
myNodeNum: Int?,
|
||||
storeForwardConfig: ModuleConfig.StoreForwardConfig?,
|
||||
|
||||
@@ -1,404 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.data.manager
|
||||
|
||||
import co.touchlab.kermit.Logger
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import okio.ByteString
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.koin.core.annotation.Named
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.common.database.DatabaseManager
|
||||
import org.meshtastic.core.common.util.handledLaunch
|
||||
import org.meshtastic.core.common.util.ignoreExceptionSuspend
|
||||
import org.meshtastic.core.common.util.nowMillis
|
||||
import org.meshtastic.core.common.util.safeCatching
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.MeshUser
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.model.Position
|
||||
import org.meshtastic.core.model.Reaction
|
||||
import org.meshtastic.core.model.service.ServiceAction
|
||||
import org.meshtastic.core.repository.CommandSender
|
||||
import org.meshtastic.core.repository.DataPair
|
||||
import org.meshtastic.core.repository.MeshActionHandler
|
||||
import org.meshtastic.core.repository.MeshDataHandler
|
||||
import org.meshtastic.core.repository.MeshMessageProcessor
|
||||
import org.meshtastic.core.repository.MeshPrefs
|
||||
import org.meshtastic.core.repository.NodeManager
|
||||
import org.meshtastic.core.repository.NotificationManager
|
||||
import org.meshtastic.core.repository.PacketRepository
|
||||
import org.meshtastic.core.repository.PlatformAnalytics
|
||||
import org.meshtastic.core.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.repository.ServiceBroadcasts
|
||||
import org.meshtastic.core.repository.UiPrefs
|
||||
import org.meshtastic.proto.AdminMessage
|
||||
import org.meshtastic.proto.Channel
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.ModuleConfig
|
||||
import org.meshtastic.proto.OTAMode
|
||||
import org.meshtastic.proto.PortNum
|
||||
import org.meshtastic.proto.User
|
||||
|
||||
@Suppress("LongParameterList", "TooManyFunctions", "CyclomaticComplexMethod")
|
||||
@Single
|
||||
class MeshActionHandlerImpl(
|
||||
private val nodeManager: NodeManager,
|
||||
private val commandSender: CommandSender,
|
||||
private val packetRepository: Lazy<PacketRepository>,
|
||||
private val serviceBroadcasts: ServiceBroadcasts,
|
||||
private val dataHandler: Lazy<MeshDataHandler>,
|
||||
private val analytics: PlatformAnalytics,
|
||||
private val meshPrefs: MeshPrefs,
|
||||
private val uiPrefs: UiPrefs,
|
||||
private val databaseManager: DatabaseManager,
|
||||
private val notificationManager: NotificationManager,
|
||||
private val messageProcessor: Lazy<MeshMessageProcessor>,
|
||||
private val radioConfigRepository: RadioConfigRepository,
|
||||
@Named("ServiceScope") private val scope: CoroutineScope,
|
||||
) : MeshActionHandler {
|
||||
|
||||
companion object {
|
||||
private const val DEFAULT_REBOOT_DELAY = 5
|
||||
private const val EMOJI_INDICATOR = 1
|
||||
}
|
||||
|
||||
override suspend fun onServiceAction(action: ServiceAction) {
|
||||
Logger.d { "ServiceAction dispatched: ${action::class.simpleName}" }
|
||||
ignoreExceptionSuspend {
|
||||
val myNodeNum = nodeManager.myNodeNum.value
|
||||
if (myNodeNum == null) {
|
||||
Logger.w { "MeshActionHandlerImpl: myNodeNum is null, skipping ServiceAction!" }
|
||||
if (action is ServiceAction.SendContact) {
|
||||
action.result.complete(false)
|
||||
}
|
||||
return@ignoreExceptionSuspend
|
||||
}
|
||||
when (action) {
|
||||
is ServiceAction.Favorite -> handleFavorite(action, myNodeNum)
|
||||
|
||||
is ServiceAction.Ignore -> handleIgnore(action, myNodeNum)
|
||||
|
||||
is ServiceAction.Mute -> handleMute(action, myNodeNum)
|
||||
|
||||
is ServiceAction.Reaction -> handleReaction(action, myNodeNum)
|
||||
|
||||
is ServiceAction.ImportContact -> handleImportContact(action, myNodeNum)
|
||||
|
||||
is ServiceAction.SendContact -> {
|
||||
val accepted =
|
||||
safeCatching {
|
||||
commandSender.sendAdminAwait(myNodeNum) { AdminMessage(add_contact = action.contact) }
|
||||
}
|
||||
.getOrDefault(false)
|
||||
action.result.complete(accepted)
|
||||
}
|
||||
|
||||
is ServiceAction.GetDeviceMetadata -> {
|
||||
commandSender.sendAdmin(action.destNum, wantResponse = true) {
|
||||
AdminMessage(get_device_metadata_request = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleFavorite(action: ServiceAction.Favorite, myNodeNum: Int) {
|
||||
val node = action.node
|
||||
commandSender.sendAdmin(myNodeNum) {
|
||||
if (node.isFavorite) {
|
||||
AdminMessage(remove_favorite_node = node.num)
|
||||
} else {
|
||||
AdminMessage(set_favorite_node = node.num)
|
||||
}
|
||||
}
|
||||
nodeManager.updateNode(node.num) { it.copy(isFavorite = !node.isFavorite) }
|
||||
}
|
||||
|
||||
private fun handleIgnore(action: ServiceAction.Ignore, myNodeNum: Int) {
|
||||
val node = action.node
|
||||
val newIgnoredStatus = !node.isIgnored
|
||||
commandSender.sendAdmin(myNodeNum) {
|
||||
if (newIgnoredStatus) {
|
||||
AdminMessage(set_ignored_node = node.num)
|
||||
} else {
|
||||
AdminMessage(remove_ignored_node = node.num)
|
||||
}
|
||||
}
|
||||
nodeManager.updateNode(node.num) { it.copy(isIgnored = newIgnoredStatus) }
|
||||
scope.handledLaunch { packetRepository.value.updateFilteredBySender(node.user.id, newIgnoredStatus) }
|
||||
}
|
||||
|
||||
private fun handleMute(action: ServiceAction.Mute, myNodeNum: Int) {
|
||||
val node = action.node
|
||||
commandSender.sendAdmin(myNodeNum) { AdminMessage(toggle_muted_node = node.num) }
|
||||
nodeManager.updateNode(node.num) { it.copy(isMuted = !node.isMuted) }
|
||||
}
|
||||
|
||||
private fun handleReaction(action: ServiceAction.Reaction, myNodeNum: Int) {
|
||||
val channel = action.contactKey[0].digitToInt()
|
||||
val destId = action.contactKey.substring(1)
|
||||
val dataPacket =
|
||||
DataPacket(
|
||||
to = destId,
|
||||
dataType = PortNum.TEXT_MESSAGE_APP.value,
|
||||
bytes = action.emoji.encodeToByteArray().toByteString(),
|
||||
channel = channel,
|
||||
replyId = action.replyId,
|
||||
wantAck = true,
|
||||
emoji = EMOJI_INDICATOR,
|
||||
)
|
||||
.apply { from = nodeManager.getMyId().takeIf { it.isNotEmpty() } ?: DataPacket.ID_LOCAL }
|
||||
commandSender.sendData(dataPacket)
|
||||
rememberReaction(action, dataPacket.id, myNodeNum)
|
||||
}
|
||||
|
||||
private fun handleImportContact(action: ServiceAction.ImportContact, myNodeNum: Int) {
|
||||
val verifiedContact = action.contact.copy(manually_verified = true)
|
||||
commandSender.sendAdmin(myNodeNum) { AdminMessage(add_contact = verifiedContact) }
|
||||
nodeManager.handleReceivedUser(
|
||||
verifiedContact.node_num,
|
||||
verifiedContact.user ?: User(),
|
||||
manuallyVerified = true,
|
||||
)
|
||||
}
|
||||
|
||||
private fun rememberReaction(action: ServiceAction.Reaction, packetId: Int, myNodeNum: Int) {
|
||||
scope.handledLaunch {
|
||||
val user = nodeManager.nodeDBbyNodeNum[myNodeNum]?.user ?: User(id = nodeManager.getMyId())
|
||||
val reaction =
|
||||
Reaction(
|
||||
replyId = action.replyId,
|
||||
user = user,
|
||||
emoji = action.emoji,
|
||||
timestamp = nowMillis,
|
||||
snr = 0f,
|
||||
rssi = 0,
|
||||
hopsAway = 0,
|
||||
packetId = packetId,
|
||||
status = MessageStatus.QUEUED,
|
||||
to = action.contactKey.substring(1),
|
||||
channel = action.contactKey[0].digitToInt(),
|
||||
)
|
||||
packetRepository.value.insertReaction(reaction, myNodeNum)
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleSetOwner(u: MeshUser, myNodeNum: Int) {
|
||||
Logger.d { "Setting owner: longName=${u.longName}, shortName=${u.shortName}" }
|
||||
val newUser = User(id = u.id, long_name = u.longName, short_name = u.shortName, is_licensed = u.isLicensed)
|
||||
commandSender.sendAdmin(myNodeNum) { AdminMessage(set_owner = newUser) }
|
||||
nodeManager.handleReceivedUser(myNodeNum, newUser)
|
||||
}
|
||||
|
||||
override fun handleSend(p: DataPacket, myNodeNum: Int) {
|
||||
commandSender.sendData(p)
|
||||
serviceBroadcasts.broadcastMessageStatus(p.id, p.status ?: MessageStatus.UNKNOWN)
|
||||
dataHandler.value.rememberDataPacket(p, myNodeNum, false)
|
||||
val bytes = p.bytes ?: ByteString.EMPTY
|
||||
analytics.track("data_send", DataPair("num_bytes", bytes.size), DataPair("type", p.dataType))
|
||||
}
|
||||
|
||||
override fun handleRequestPosition(destNum: Int, position: Position, myNodeNum: Int) {
|
||||
if (destNum != myNodeNum) {
|
||||
val provideLocation = uiPrefs.shouldProvideNodeLocation(myNodeNum).value
|
||||
val currentPosition =
|
||||
when {
|
||||
provideLocation && position.isValid() -> position
|
||||
|
||||
provideLocation ->
|
||||
nodeManager.nodeDBbyNodeNum[myNodeNum]?.position?.let { Position(it) }?.takeIf { it.isValid() }
|
||||
?: Position(0.0, 0.0, 0)
|
||||
|
||||
else -> Position(0.0, 0.0, 0)
|
||||
}
|
||||
commandSender.requestPosition(destNum, currentPosition)
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleRemoveByNodenum(nodeNum: Int, requestId: Int, myNodeNum: Int) {
|
||||
nodeManager.removeByNodenum(nodeNum)
|
||||
commandSender.sendAdmin(myNodeNum, requestId) { AdminMessage(remove_by_nodenum = nodeNum) }
|
||||
}
|
||||
|
||||
override fun handleSetRemoteOwner(id: Int, destNum: Int, payload: ByteArray) {
|
||||
val u = User.ADAPTER.decode(payload)
|
||||
commandSender.sendAdmin(destNum, id) { AdminMessage(set_owner = u) }
|
||||
nodeManager.handleReceivedUser(destNum, u)
|
||||
}
|
||||
|
||||
override fun handleGetRemoteOwner(id: Int, destNum: Int) {
|
||||
commandSender.sendAdmin(destNum, id, wantResponse = true) { AdminMessage(get_owner_request = true) }
|
||||
}
|
||||
|
||||
override fun handleSetConfig(payload: ByteArray, myNodeNum: Int) {
|
||||
val c = Config.ADAPTER.decode(payload)
|
||||
commandSender.sendAdmin(myNodeNum) { AdminMessage(set_config = c) }
|
||||
// Optimistically persist the config locally so CommandSender picks up
|
||||
// the new values (e.g. hop_limit) immediately instead of waiting for
|
||||
// the next want_config handshake.
|
||||
scope.handledLaunch { radioConfigRepository.setLocalConfig(c) }
|
||||
}
|
||||
|
||||
override fun handleSetRemoteConfig(id: Int, destNum: Int, payload: ByteArray) {
|
||||
val c = Config.ADAPTER.decode(payload)
|
||||
commandSender.sendAdmin(destNum, id) { AdminMessage(set_config = c) }
|
||||
// When targeting the local node, optimistically persist the config so the
|
||||
// UI reflects changes immediately (matching handleSetConfig behaviour).
|
||||
if (destNum == nodeManager.myNodeNum.value) {
|
||||
scope.handledLaunch { radioConfigRepository.setLocalConfig(c) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleGetRemoteConfig(id: Int, destNum: Int, config: Int) {
|
||||
commandSender.sendAdmin(destNum, id, wantResponse = true) {
|
||||
if (config == AdminMessage.ConfigType.SESSIONKEY_CONFIG.value) {
|
||||
AdminMessage(get_device_metadata_request = true)
|
||||
} else {
|
||||
AdminMessage(get_config_request = AdminMessage.ConfigType.fromValue(config))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleSetModuleConfig(id: Int, destNum: Int, payload: ByteArray) {
|
||||
val c = ModuleConfig.ADAPTER.decode(payload)
|
||||
commandSender.sendAdmin(destNum, id) { AdminMessage(set_module_config = c) }
|
||||
c.statusmessage?.let { sm -> nodeManager.updateNodeStatus(destNum, sm.node_status) }
|
||||
// Optimistically persist module config locally so the UI reflects the
|
||||
// new values immediately instead of waiting for the next want_config handshake.
|
||||
if (destNum == nodeManager.myNodeNum.value) {
|
||||
scope.handledLaunch { radioConfigRepository.setLocalModuleConfig(c) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleGetModuleConfig(id: Int, destNum: Int, config: Int) {
|
||||
commandSender.sendAdmin(destNum, id, wantResponse = true) {
|
||||
AdminMessage(get_module_config_request = AdminMessage.ModuleConfigType.fromValue(config))
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleSetRingtone(destNum: Int, ringtone: String) {
|
||||
commandSender.sendAdmin(destNum) { AdminMessage(set_ringtone_message = ringtone) }
|
||||
}
|
||||
|
||||
override fun handleGetRingtone(id: Int, destNum: Int) {
|
||||
commandSender.sendAdmin(destNum, id, wantResponse = true) { AdminMessage(get_ringtone_request = true) }
|
||||
}
|
||||
|
||||
override fun handleSetCannedMessages(destNum: Int, messages: String) {
|
||||
commandSender.sendAdmin(destNum) { AdminMessage(set_canned_message_module_messages = messages) }
|
||||
}
|
||||
|
||||
override fun handleGetCannedMessages(id: Int, destNum: Int) {
|
||||
commandSender.sendAdmin(destNum, id, wantResponse = true) {
|
||||
AdminMessage(get_canned_message_module_messages_request = true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleSetChannel(payload: ByteArray?, myNodeNum: Int) {
|
||||
if (payload != null) {
|
||||
val c = Channel.ADAPTER.decode(payload)
|
||||
commandSender.sendAdmin(myNodeNum) { AdminMessage(set_channel = c) }
|
||||
// Optimistically persist the channel settings locally so the UI
|
||||
// reflects changes immediately instead of waiting for the next
|
||||
// want_config handshake.
|
||||
scope.handledLaunch { radioConfigRepository.updateChannelSettings(c) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleSetRemoteChannel(id: Int, destNum: Int, payload: ByteArray?) {
|
||||
if (payload != null) {
|
||||
val c = Channel.ADAPTER.decode(payload)
|
||||
commandSender.sendAdmin(destNum, id) { AdminMessage(set_channel = c) }
|
||||
// When targeting the local node, optimistically persist the channel so
|
||||
// the UI reflects changes immediately (matching handleSetChannel behaviour).
|
||||
if (destNum == nodeManager.myNodeNum.value) {
|
||||
scope.handledLaunch { radioConfigRepository.updateChannelSettings(c) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleGetRemoteChannel(id: Int, destNum: Int, index: Int) {
|
||||
commandSender.sendAdmin(destNum, id, wantResponse = true) { AdminMessage(get_channel_request = index + 1) }
|
||||
}
|
||||
|
||||
override fun handleRequestNeighborInfo(requestId: Int, destNum: Int) {
|
||||
commandSender.requestNeighborInfo(requestId, destNum)
|
||||
}
|
||||
|
||||
override fun handleBeginEditSettings(destNum: Int) {
|
||||
commandSender.sendAdmin(destNum) { AdminMessage(begin_edit_settings = true) }
|
||||
}
|
||||
|
||||
override fun handleCommitEditSettings(destNum: Int) {
|
||||
commandSender.sendAdmin(destNum) { AdminMessage(commit_edit_settings = true) }
|
||||
}
|
||||
|
||||
override fun handleRebootToDfu(destNum: Int) {
|
||||
commandSender.sendAdmin(destNum) { AdminMessage(enter_dfu_mode_request = true) }
|
||||
}
|
||||
|
||||
override fun handleRequestTelemetry(requestId: Int, destNum: Int, type: Int) {
|
||||
commandSender.requestTelemetry(requestId, destNum, type)
|
||||
}
|
||||
|
||||
override fun handleRequestShutdown(requestId: Int, destNum: Int) {
|
||||
commandSender.sendAdmin(destNum, requestId) { AdminMessage(shutdown_seconds = DEFAULT_REBOOT_DELAY) }
|
||||
}
|
||||
|
||||
override fun handleRequestReboot(requestId: Int, destNum: Int) {
|
||||
Logger.i { "Reboot requested for node $destNum" }
|
||||
commandSender.sendAdmin(destNum, requestId) { AdminMessage(reboot_seconds = DEFAULT_REBOOT_DELAY) }
|
||||
}
|
||||
|
||||
override fun handleRequestRebootOta(requestId: Int, destNum: Int, mode: Int, hash: ByteArray?) {
|
||||
val otaMode = OTAMode.fromValue(mode) ?: OTAMode.NO_REBOOT_OTA
|
||||
val otaEvent =
|
||||
AdminMessage.OTAEvent(reboot_ota_mode = otaMode, ota_hash = hash?.toByteString() ?: ByteString.EMPTY)
|
||||
commandSender.sendAdmin(destNum, requestId) { AdminMessage(ota_request = otaEvent) }
|
||||
}
|
||||
|
||||
override fun handleRequestFactoryReset(requestId: Int, destNum: Int) {
|
||||
Logger.i { "Factory reset requested for node $destNum" }
|
||||
commandSender.sendAdmin(destNum, requestId) { AdminMessage(factory_reset_device = 1) }
|
||||
}
|
||||
|
||||
override fun handleRequestNodedbReset(requestId: Int, destNum: Int, preserveFavorites: Boolean) {
|
||||
commandSender.sendAdmin(destNum, requestId) { AdminMessage(nodedb_reset = preserveFavorites) }
|
||||
}
|
||||
|
||||
override fun handleGetDeviceConnectionStatus(requestId: Int, destNum: Int) {
|
||||
commandSender.sendAdmin(destNum, requestId, wantResponse = true) {
|
||||
AdminMessage(get_device_connection_status_request = true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleUpdateLastAddress(deviceAddr: String?) {
|
||||
val currentAddr = meshPrefs.deviceAddress.value
|
||||
if (deviceAddr != currentAddr) {
|
||||
Logger.i { "Device address changed, switching database and clearing node DB" }
|
||||
meshPrefs.setDeviceAddress(deviceAddr)
|
||||
scope.handledLaunch {
|
||||
nodeManager.clear()
|
||||
messageProcessor.value.clearEarlyPackets()
|
||||
databaseManager.switchActiveDatabase(deviceAddr)
|
||||
notificationManager.cancelAll()
|
||||
nodeManager.loadCachedNodeDB()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,8 +34,7 @@ import org.meshtastic.core.repository.NodeRepository
|
||||
import org.meshtastic.core.repository.NotificationPrefs
|
||||
import org.meshtastic.core.repository.PlatformAnalytics
|
||||
import org.meshtastic.core.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.repository.ServiceBroadcasts
|
||||
import org.meshtastic.core.repository.ServiceRepository
|
||||
import org.meshtastic.core.repository.ServiceStateWriter
|
||||
import org.meshtastic.proto.DeviceMetadata
|
||||
import org.meshtastic.proto.FileInfo
|
||||
import org.meshtastic.proto.FirmwareEdition
|
||||
@@ -51,8 +50,7 @@ class MeshConfigFlowManagerImpl(
|
||||
private val connectionManager: Lazy<MeshConnectionManager>,
|
||||
private val nodeRepository: NodeRepository,
|
||||
private val radioConfigRepository: RadioConfigRepository,
|
||||
private val serviceRepository: ServiceRepository,
|
||||
private val serviceBroadcasts: ServiceBroadcasts,
|
||||
private val serviceStateWriter: ServiceStateWriter,
|
||||
private val analytics: PlatformAnalytics,
|
||||
private val commandSender: CommandSender,
|
||||
private val heartbeatSender: DataLayerHeartbeatSender,
|
||||
@@ -177,7 +175,7 @@ class MeshConfigFlowManagerImpl(
|
||||
|
||||
val entities =
|
||||
state.nodes.mapNotNull { nodeInfo ->
|
||||
nodeManager.installNodeInfo(nodeInfo, withBroadcast = false)
|
||||
nodeManager.installNodeInfo(nodeInfo)
|
||||
nodeManager.nodeDBbyNodeNum[nodeInfo.num]
|
||||
?: run {
|
||||
Logger.w { "Node ${nodeInfo.num} missing from DB after installNodeInfo; skipping" }
|
||||
@@ -190,8 +188,7 @@ class MeshConfigFlowManagerImpl(
|
||||
analytics.setDeviceAttributes(info.firmwareVersion ?: "unknown", info.model ?: "unknown")
|
||||
nodeManager.setNodeDbReady(true)
|
||||
nodeManager.setAllowNodeDbWrites(true)
|
||||
serviceRepository.setConnectionState(ConnectionState.Connected)
|
||||
serviceBroadcasts.broadcastConnection()
|
||||
serviceStateWriter.setConnectionState(ConnectionState.Connected)
|
||||
connectionManager.value.onNodeDbReady()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ import org.meshtastic.core.common.util.handledLaunch
|
||||
import org.meshtastic.core.repository.MeshConfigHandler
|
||||
import org.meshtastic.core.repository.NodeManager
|
||||
import org.meshtastic.core.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.repository.ServiceRepository
|
||||
import org.meshtastic.core.repository.ServiceStateWriter
|
||||
import org.meshtastic.proto.Channel
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.DeviceUIConfig
|
||||
@@ -39,7 +39,7 @@ import org.meshtastic.proto.ModuleConfig
|
||||
@Single
|
||||
class MeshConfigHandlerImpl(
|
||||
private val radioConfigRepository: RadioConfigRepository,
|
||||
private val serviceRepository: ServiceRepository,
|
||||
private val serviceStateWriter: ServiceStateWriter,
|
||||
private val nodeManager: NodeManager,
|
||||
@Named("ServiceScope") private val scope: CoroutineScope,
|
||||
) : MeshConfigHandler {
|
||||
@@ -58,13 +58,13 @@ class MeshConfigHandlerImpl(
|
||||
override fun handleDeviceConfig(config: Config) {
|
||||
Logger.d { "Device config received: ${config.summarize()}" }
|
||||
scope.handledLaunch { radioConfigRepository.setLocalConfig(config) }
|
||||
serviceRepository.setConnectionProgress("Device config received")
|
||||
serviceStateWriter.setConnectionProgress("Device config received")
|
||||
}
|
||||
|
||||
override fun handleModuleConfig(config: ModuleConfig) {
|
||||
Logger.d { "Module config received: ${config.summarize()}" }
|
||||
scope.handledLaunch { radioConfigRepository.setLocalModuleConfig(config) }
|
||||
serviceRepository.setConnectionProgress("Module config received")
|
||||
serviceStateWriter.setConnectionProgress("Module config received")
|
||||
|
||||
config.statusmessage?.let { sm ->
|
||||
nodeManager.myNodeNum.value?.let { num -> nodeManager.updateNodeStatus(num, sm.node_status) }
|
||||
@@ -79,9 +79,9 @@ class MeshConfigHandlerImpl(
|
||||
val mi = nodeManager.getMyNodeInfo()
|
||||
val index = channel.index
|
||||
if (mi != null) {
|
||||
serviceRepository.setConnectionProgress("Channels (${index + 1} / ${mi.maxChannels})")
|
||||
serviceStateWriter.setConnectionProgress("Channels (${index + 1} / ${mi.maxChannels})")
|
||||
} else {
|
||||
serviceRepository.setConnectionProgress("Channels (${index + 1})")
|
||||
serviceStateWriter.setConnectionProgress("Channels (${index + 1})")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ import org.meshtastic.core.repository.HandshakeConstants
|
||||
import org.meshtastic.core.repository.HistoryManager
|
||||
import org.meshtastic.core.repository.MeshConnectionManager
|
||||
import org.meshtastic.core.repository.MeshLocationManager
|
||||
import org.meshtastic.core.repository.MeshServiceNotifications
|
||||
import org.meshtastic.core.repository.MeshNotificationManager
|
||||
import org.meshtastic.core.repository.MeshWorkerManager
|
||||
import org.meshtastic.core.repository.MqttManager
|
||||
import org.meshtastic.core.repository.NodeManager
|
||||
@@ -52,7 +52,6 @@ import org.meshtastic.core.repository.PacketRepository
|
||||
import org.meshtastic.core.repository.PlatformAnalytics
|
||||
import org.meshtastic.core.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.repository.RadioInterfaceService
|
||||
import org.meshtastic.core.repository.ServiceBroadcasts
|
||||
import org.meshtastic.core.repository.ServiceRepository
|
||||
import org.meshtastic.core.repository.SessionManager
|
||||
import org.meshtastic.core.repository.UiPrefs
|
||||
@@ -70,8 +69,7 @@ import kotlin.time.DurationUnit
|
||||
class MeshConnectionManagerImpl(
|
||||
private val radioInterfaceService: RadioInterfaceService,
|
||||
private val serviceRepository: ServiceRepository,
|
||||
private val serviceBroadcasts: ServiceBroadcasts,
|
||||
private val serviceNotifications: MeshServiceNotifications,
|
||||
private val serviceNotifications: MeshNotificationManager,
|
||||
private val uiPrefs: UiPrefs,
|
||||
private val packetHandler: PacketHandler,
|
||||
private val nodeRepository: NodeRepository,
|
||||
@@ -200,7 +198,6 @@ class MeshConnectionManagerImpl(
|
||||
if (serviceRepository.connectionState.value != ConnectionState.Connected) {
|
||||
serviceRepository.setConnectionState(ConnectionState.Connecting)
|
||||
}
|
||||
serviceBroadcasts.broadcastConnection()
|
||||
connectTimeMsec = nowMillis
|
||||
|
||||
// Send a wake-up heartbeat before the config request. The firmware may be in a
|
||||
@@ -276,8 +273,6 @@ class MeshConnectionManagerImpl(
|
||||
Logger.d { "device sleep timeout cancelled" }
|
||||
}
|
||||
}
|
||||
|
||||
serviceBroadcasts.broadcastConnection()
|
||||
}
|
||||
|
||||
private fun handleDisconnected() {
|
||||
@@ -290,8 +285,6 @@ class MeshConnectionManagerImpl(
|
||||
DataPair(KEY_NUM_ONLINE, nodeManager.nodeDBbyNodeNum.values.count { it.isOnline }),
|
||||
)
|
||||
analytics.track(EVENT_NUM_NODES, DataPair(KEY_NUM_NODES, nodeManager.nodeDBbyNodeNum.size))
|
||||
|
||||
serviceBroadcasts.broadcastConnection()
|
||||
}
|
||||
|
||||
override fun startConfigOnly() {
|
||||
@@ -319,7 +312,7 @@ class MeshConnectionManagerImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNodeDbReady() {
|
||||
override suspend fun onNodeDbReady() {
|
||||
handshakeTimeout?.cancel()
|
||||
handshakeTimeout = null
|
||||
|
||||
|
||||
@@ -31,14 +31,19 @@ import org.meshtastic.core.common.util.nowSeconds
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.model.NodeAddress
|
||||
import org.meshtastic.core.model.Reaction
|
||||
import org.meshtastic.core.model.destination
|
||||
import org.meshtastic.core.model.isBroadcast
|
||||
import org.meshtastic.core.model.isFromLocal
|
||||
import org.meshtastic.core.model.source
|
||||
import org.meshtastic.core.model.util.MeshDataMapper
|
||||
import org.meshtastic.core.model.util.decodeOrNull
|
||||
import org.meshtastic.core.model.util.toOneLiner
|
||||
import org.meshtastic.core.repository.AdminPacketHandler
|
||||
import org.meshtastic.core.repository.DataPair
|
||||
import org.meshtastic.core.repository.MeshDataHandler
|
||||
import org.meshtastic.core.repository.MeshServiceNotifications
|
||||
import org.meshtastic.core.repository.MeshNotificationManager
|
||||
import org.meshtastic.core.repository.MessageFilter
|
||||
import org.meshtastic.core.repository.NeighborInfoHandler
|
||||
import org.meshtastic.core.repository.NodeManager
|
||||
@@ -48,8 +53,7 @@ import org.meshtastic.core.repository.PacketHandler
|
||||
import org.meshtastic.core.repository.PacketRepository
|
||||
import org.meshtastic.core.repository.PlatformAnalytics
|
||||
import org.meshtastic.core.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.repository.ServiceBroadcasts
|
||||
import org.meshtastic.core.repository.ServiceRepository
|
||||
import org.meshtastic.core.repository.ServiceStateWriter
|
||||
import org.meshtastic.core.repository.StoreForwardPacketHandler
|
||||
import org.meshtastic.core.repository.TelemetryPacketHandler
|
||||
import org.meshtastic.core.repository.TracerouteHandler
|
||||
@@ -82,11 +86,10 @@ import org.meshtastic.proto.Waypoint
|
||||
class MeshDataHandlerImpl(
|
||||
private val nodeManager: NodeManager,
|
||||
private val packetHandler: PacketHandler,
|
||||
private val serviceRepository: ServiceRepository,
|
||||
private val serviceStateWriter: ServiceStateWriter,
|
||||
private val packetRepository: Lazy<PacketRepository>,
|
||||
private val serviceBroadcasts: ServiceBroadcasts,
|
||||
private val notificationManager: NotificationManager,
|
||||
private val serviceNotifications: MeshServiceNotifications,
|
||||
private val serviceNotifications: MeshNotificationManager,
|
||||
private val analytics: PlatformAnalytics,
|
||||
private val dataMapper: MeshDataMapper,
|
||||
private val tracerouteHandler: TracerouteHandler,
|
||||
@@ -112,11 +115,8 @@ class MeshDataHandlerImpl(
|
||||
val fromUs = myNodeNum == packet.from
|
||||
dataPacket.status = MessageStatus.RECEIVED
|
||||
|
||||
val shouldBroadcast = handleDataPacket(packet, dataPacket, myNodeNum, fromUs, logUuid, logInsertJob)
|
||||
handleDataPacket(packet, dataPacket, myNodeNum, fromUs, logUuid, logInsertJob)
|
||||
|
||||
if (shouldBroadcast) {
|
||||
serviceBroadcasts.broadcastReceivedData(dataPacket)
|
||||
}
|
||||
analytics.track("num_data_receive", DataPair("num_data_receive", 1))
|
||||
}
|
||||
|
||||
@@ -127,50 +127,35 @@ class MeshDataHandlerImpl(
|
||||
fromUs: Boolean,
|
||||
logUuid: String?,
|
||||
logInsertJob: Job?,
|
||||
): Boolean {
|
||||
var shouldBroadcast = !fromUs
|
||||
val decoded = packet.decoded ?: return shouldBroadcast
|
||||
) {
|
||||
val decoded = packet.decoded ?: return
|
||||
when (decoded.portnum) {
|
||||
PortNum.TEXT_MESSAGE_APP -> handleTextMessage(packet, dataPacket, myNodeNum)
|
||||
|
||||
PortNum.NODE_STATUS_APP -> handleNodeStatus(packet, dataPacket, myNodeNum)
|
||||
|
||||
PortNum.ALERT_APP -> rememberDataPacket(dataPacket, myNodeNum)
|
||||
|
||||
PortNum.WAYPOINT_APP -> handleWaypoint(packet, dataPacket, myNodeNum)
|
||||
|
||||
PortNum.POSITION_APP -> handlePosition(packet, dataPacket, myNodeNum)
|
||||
|
||||
PortNum.NODEINFO_APP -> if (!fromUs) handleNodeInfo(packet)
|
||||
|
||||
PortNum.TELEMETRY_APP -> telemetryHandler.handleTelemetry(packet, dataPacket, myNodeNum)
|
||||
|
||||
else ->
|
||||
shouldBroadcast =
|
||||
handleSpecializedDataPacket(packet, dataPacket, myNodeNum, fromUs, logUuid, logInsertJob)
|
||||
else -> handleSpecializedDataPacket(packet, dataPacket, myNodeNum, logUuid, logInsertJob)
|
||||
}
|
||||
return shouldBroadcast
|
||||
}
|
||||
|
||||
private fun handleSpecializedDataPacket(
|
||||
packet: MeshPacket,
|
||||
dataPacket: DataPacket,
|
||||
myNodeNum: Int,
|
||||
fromUs: Boolean,
|
||||
logUuid: String?,
|
||||
logInsertJob: Job?,
|
||||
): Boolean {
|
||||
var shouldBroadcast = !fromUs
|
||||
val decoded = packet.decoded ?: return shouldBroadcast
|
||||
) {
|
||||
val decoded = packet.decoded ?: return
|
||||
when (decoded.portnum) {
|
||||
PortNum.TRACEROUTE_APP -> {
|
||||
tracerouteHandler.handleTraceroute(packet, logUuid, logInsertJob)
|
||||
shouldBroadcast = false
|
||||
}
|
||||
|
||||
PortNum.ROUTING_APP -> {
|
||||
handleRouting(packet, dataPacket)
|
||||
shouldBroadcast = true
|
||||
}
|
||||
|
||||
PortNum.PAXCOUNTER_APP -> {
|
||||
@@ -191,30 +176,21 @@ class MeshDataHandlerImpl(
|
||||
|
||||
PortNum.NEIGHBORINFO_APP -> {
|
||||
neighborInfoHandler.handleNeighborInfo(packet)
|
||||
shouldBroadcast = true
|
||||
}
|
||||
|
||||
PortNum.ATAK_PLUGIN,
|
||||
PortNum.ATAK_PLUGIN_V2,
|
||||
PortNum.PRIVATE_APP,
|
||||
-> {
|
||||
shouldBroadcast = true
|
||||
}
|
||||
-> {}
|
||||
|
||||
PortNum.RANGE_TEST_APP,
|
||||
PortNum.DETECTION_SENSOR_APP,
|
||||
-> {
|
||||
handleRangeTest(dataPacket, myNodeNum)
|
||||
shouldBroadcast = true
|
||||
}
|
||||
|
||||
else -> {
|
||||
// By default, if we don't know what it is, we should probably broadcast it
|
||||
// so that external apps can handle it.
|
||||
shouldBroadcast = true
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
return shouldBroadcast
|
||||
}
|
||||
|
||||
private fun handleRangeTest(dataPacket: DataPacket, myNodeNum: Int) {
|
||||
@@ -279,7 +255,7 @@ class MeshDataHandlerImpl(
|
||||
val r = Routing.ADAPTER.decodeOrNull(payload, Logger) ?: return
|
||||
if (r.error_reason == Routing.Error.DUTY_CYCLE_LIMIT) {
|
||||
scope.launch {
|
||||
serviceRepository.setErrorMessage(getStringSuspend(Res.string.error_duty_cycle), Severity.Warn)
|
||||
serviceStateWriter.setErrorMessage(getStringSuspend(Res.string.error_duty_cycle), Severity.Warn)
|
||||
}
|
||||
}
|
||||
handleAckNak(
|
||||
@@ -326,16 +302,13 @@ class MeshDataHandlerImpl(
|
||||
packetRepository.value.updateReaction(updated)
|
||||
}
|
||||
}
|
||||
|
||||
serviceBroadcasts.broadcastMessageStatus(requestId, m)
|
||||
}
|
||||
}
|
||||
|
||||
override fun rememberDataPacket(dataPacket: DataPacket, myNodeNum: Int, updateNotification: Boolean) {
|
||||
if (dataPacket.dataType !in rememberDataType) return
|
||||
val fromLocal =
|
||||
dataPacket.from == DataPacket.ID_LOCAL || dataPacket.from == DataPacket.nodeNumToDefaultId(myNodeNum)
|
||||
val toBroadcast = dataPacket.to == DataPacket.ID_BROADCAST
|
||||
val fromLocal = dataPacket.isFromLocal(myNodeNum)
|
||||
val toBroadcast = dataPacket.isBroadcast
|
||||
val contactId = if (fromLocal || toBroadcast) dataPacket.to else dataPacket.from
|
||||
|
||||
// contactKey: unique contact key filter (channel)+(nodeId)
|
||||
@@ -374,7 +347,7 @@ class MeshDataHandlerImpl(
|
||||
|
||||
@Suppress("ReturnCount")
|
||||
private suspend fun PacketRepository.shouldFilterMessage(dataPacket: DataPacket, contactKey: String): Boolean {
|
||||
val isIgnored = nodeManager.nodeDBbyID[dataPacket.from]?.isIgnored == true
|
||||
val isIgnored = nodeManager.getNodeById(dataPacket.from.orEmpty())?.isIgnored == true
|
||||
if (isIgnored) return true
|
||||
|
||||
if (dataPacket.dataType != PortNum.TEXT_MESSAGE_APP.value) return false
|
||||
@@ -388,7 +361,7 @@ class MeshDataHandlerImpl(
|
||||
updateNotification: Boolean,
|
||||
) {
|
||||
val conversationMuted = packetRepository.value.getContactSettings(contactKey).isMuted
|
||||
val nodeMuted = nodeManager.nodeDBbyID[dataPacket.from]?.isMuted == true
|
||||
val nodeMuted = nodeManager.getNodeById(dataPacket.from.orEmpty())?.isMuted == true
|
||||
val isSilent = conversationMuted || nodeMuted
|
||||
if (dataPacket.dataType == PortNum.ALERT_APP.value && !isSilent) {
|
||||
scope.launch {
|
||||
@@ -407,19 +380,21 @@ class MeshDataHandlerImpl(
|
||||
}
|
||||
|
||||
private suspend fun getSenderName(packet: DataPacket): String {
|
||||
if (packet.from == DataPacket.ID_LOCAL) {
|
||||
if (packet.source is NodeAddress.Local) {
|
||||
val myId = nodeManager.getMyId()
|
||||
return nodeManager.nodeDBbyID[myId]?.user?.long_name ?: getStringSuspend(Res.string.unknown_username)
|
||||
return nodeManager.getNodeById(myId)?.user?.long_name ?: getStringSuspend(Res.string.unknown_username)
|
||||
}
|
||||
return nodeManager.nodeDBbyID[packet.from]?.user?.long_name ?: getStringSuspend(Res.string.unknown_username)
|
||||
return nodeManager.getNodeById(packet.from.orEmpty())?.user?.long_name
|
||||
?: getStringSuspend(Res.string.unknown_username)
|
||||
}
|
||||
|
||||
private suspend fun updateNotification(contactKey: String, dataPacket: DataPacket, isSilent: Boolean) {
|
||||
when (dataPacket.dataType) {
|
||||
PortNum.TEXT_MESSAGE_APP.value -> {
|
||||
val message = dataPacket.text!!
|
||||
val isBroadcast = dataPacket.destination is NodeAddress.Broadcast
|
||||
val channelName =
|
||||
if (dataPacket.to == DataPacket.ID_BROADCAST) {
|
||||
if (isBroadcast) {
|
||||
radioConfigRepository.channelSetFlow.first().settings.getOrNull(dataPacket.channel)?.name
|
||||
} else {
|
||||
null
|
||||
@@ -428,7 +403,7 @@ class MeshDataHandlerImpl(
|
||||
contactKey,
|
||||
getSenderName(dataPacket),
|
||||
message,
|
||||
dataPacket.to == DataPacket.ID_BROADCAST,
|
||||
isBroadcast,
|
||||
channelName,
|
||||
isSilent,
|
||||
)
|
||||
@@ -496,15 +471,16 @@ class MeshDataHandlerImpl(
|
||||
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
|
||||
if (originalPacket.source is NodeAddress.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
|
||||
val nodeMuted = nodeManager.getNodeById(fromId)?.isMuted == true
|
||||
val isSilent = conversationMuted || nodeMuted
|
||||
|
||||
if (!isSilent) {
|
||||
val isBroadcast = originalPacket.destination is NodeAddress.Broadcast
|
||||
val channelName =
|
||||
if (originalPacket.to == DataPacket.ID_BROADCAST) {
|
||||
if (isBroadcast) {
|
||||
radioConfigRepository.channelSetFlow
|
||||
.first()
|
||||
.settings
|
||||
@@ -517,7 +493,7 @@ class MeshDataHandlerImpl(
|
||||
contactKey,
|
||||
getSenderName(dataMapper.toDataPacket(packet)!!),
|
||||
emoji,
|
||||
originalPacket.to == DataPacket.ID_BROADCAST,
|
||||
isBroadcast,
|
||||
channelName,
|
||||
isSilent,
|
||||
)
|
||||
|
||||
@@ -36,11 +36,11 @@ import org.meshtastic.core.model.util.isLora
|
||||
import org.meshtastic.core.model.util.toOneLineString
|
||||
import org.meshtastic.core.model.util.toPIIString
|
||||
import org.meshtastic.core.repository.FromRadioPacketHandler
|
||||
import org.meshtastic.core.repository.MeshDataHandler
|
||||
import org.meshtastic.core.repository.MeshLogRepository
|
||||
import org.meshtastic.core.repository.MeshMessageProcessor
|
||||
import org.meshtastic.core.repository.MeshRouter
|
||||
import org.meshtastic.core.repository.NodeManager
|
||||
import org.meshtastic.core.repository.ServiceRepository
|
||||
import org.meshtastic.core.repository.ServiceStateWriter
|
||||
import org.meshtastic.proto.FromRadio
|
||||
import org.meshtastic.proto.LogRecord
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
@@ -53,9 +53,9 @@ import kotlin.uuid.Uuid
|
||||
@Single
|
||||
class MeshMessageProcessorImpl(
|
||||
private val nodeManager: NodeManager,
|
||||
private val serviceRepository: ServiceRepository,
|
||||
private val serviceStateWriter: ServiceStateWriter,
|
||||
private val meshLogRepository: Lazy<MeshLogRepository>,
|
||||
private val router: Lazy<MeshRouter>,
|
||||
private val dataHandler: Lazy<MeshDataHandler>,
|
||||
private val fromRadioDispatcher: FromRadioPacketHandler,
|
||||
@Named("ServiceScope") private val scope: CoroutineScope,
|
||||
) : MeshMessageProcessor {
|
||||
@@ -212,15 +212,13 @@ class MeshMessageProcessorImpl(
|
||||
}
|
||||
}
|
||||
|
||||
scope.handledLaunch { serviceRepository.emitMeshPacket(packet) }
|
||||
scope.handledLaunch { serviceStateWriter.emitMeshPacket(packet) }
|
||||
|
||||
myNodeNum?.let { myNum ->
|
||||
val from = packet.from
|
||||
val isOtherNode = myNum != from
|
||||
nodeManager.updateNode(myNum, withBroadcast = isOtherNode) { node: Node ->
|
||||
node.copy(lastHeard = nowSeconds.toInt())
|
||||
}
|
||||
nodeManager.updateNode(from, withBroadcast = false, channel = packet.channel) { node: Node ->
|
||||
nodeManager.updateNode(myNum) { node: Node -> node.copy(lastHeard = nowSeconds.toInt()) }
|
||||
nodeManager.updateNode(from, channel = packet.channel) { node: Node ->
|
||||
val viaMqtt = packet.via_mqtt == true
|
||||
val isDirect = packet.hop_start == packet.hop_limit
|
||||
|
||||
@@ -255,7 +253,7 @@ class MeshMessageProcessorImpl(
|
||||
}
|
||||
|
||||
try {
|
||||
router.value.dataHandler.handleReceivedData(packet, myNum, log.uuid, logJob)
|
||||
dataHandler.value.handleReceivedData(packet, myNum, log.uuid, logJob)
|
||||
} finally {
|
||||
scope.launch {
|
||||
mapsMutex.withLock {
|
||||
@@ -284,7 +282,7 @@ class MeshMessageProcessorImpl(
|
||||
lastLocalNodeRefreshMs = now
|
||||
|
||||
val myNum = nodeManager.myNodeNum.value ?: return
|
||||
nodeManager.updateNode(myNum, withBroadcast = false) { node: Node -> node.copy(lastHeard = nowSeconds.toInt()) }
|
||||
nodeManager.updateNode(myNum) { node: Node -> node.copy(lastHeard = nowSeconds.toInt()) }
|
||||
}
|
||||
|
||||
private fun insertMeshLog(log: MeshLog): Job = scope.handledLaunch { meshLogRepository.value.insert(log) }
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.data.manager
|
||||
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.repository.MeshActionHandler
|
||||
import org.meshtastic.core.repository.MeshConfigFlowManager
|
||||
import org.meshtastic.core.repository.MeshConfigHandler
|
||||
import org.meshtastic.core.repository.MeshDataHandler
|
||||
import org.meshtastic.core.repository.MeshRouter
|
||||
import org.meshtastic.core.repository.MqttManager
|
||||
import org.meshtastic.core.repository.NeighborInfoHandler
|
||||
import org.meshtastic.core.repository.TracerouteHandler
|
||||
import org.meshtastic.core.repository.XModemManager
|
||||
|
||||
/** Implementation of [MeshRouter] that orchestrates specialized mesh packet handlers. */
|
||||
@Suppress("LongParameterList")
|
||||
@Single
|
||||
class MeshRouterImpl(
|
||||
private val dataHandlerLazy: Lazy<MeshDataHandler>,
|
||||
private val configHandlerLazy: Lazy<MeshConfigHandler>,
|
||||
private val tracerouteHandlerLazy: Lazy<TracerouteHandler>,
|
||||
private val neighborInfoHandlerLazy: Lazy<NeighborInfoHandler>,
|
||||
private val configFlowManagerLazy: Lazy<MeshConfigFlowManager>,
|
||||
private val mqttManagerLazy: Lazy<MqttManager>,
|
||||
private val actionHandlerLazy: Lazy<MeshActionHandler>,
|
||||
private val xmodemManagerLazy: Lazy<XModemManager>,
|
||||
) : MeshRouter {
|
||||
override val dataHandler: MeshDataHandler
|
||||
get() = dataHandlerLazy.value
|
||||
|
||||
override val configHandler: MeshConfigHandler
|
||||
get() = configHandlerLazy.value
|
||||
|
||||
override val tracerouteHandler: TracerouteHandler
|
||||
get() = tracerouteHandlerLazy.value
|
||||
|
||||
override val neighborInfoHandler: NeighborInfoHandler
|
||||
get() = neighborInfoHandlerLazy.value
|
||||
|
||||
override val configFlowManager: MeshConfigFlowManager
|
||||
get() = configFlowManagerLazy.value
|
||||
|
||||
override val mqttManager: MqttManager
|
||||
get() = mqttManagerLazy.value
|
||||
|
||||
override val actionHandler: MeshActionHandler
|
||||
get() = actionHandlerLazy.value
|
||||
|
||||
override val xmodemManager: XModemManager
|
||||
get() = xmodemManagerLazy.value
|
||||
}
|
||||
@@ -37,7 +37,7 @@ import org.meshtastic.core.network.repository.resolveEndpoint
|
||||
import org.meshtastic.core.repository.MqttManager
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
import org.meshtastic.core.repository.PacketHandler
|
||||
import org.meshtastic.core.repository.ServiceRepository
|
||||
import org.meshtastic.core.repository.ServiceStateWriter
|
||||
import org.meshtastic.mqtt.ConnectionState
|
||||
import org.meshtastic.mqtt.MqttClient
|
||||
import org.meshtastic.mqtt.MqttException
|
||||
@@ -51,7 +51,7 @@ import kotlin.uuid.Uuid
|
||||
class MqttManagerImpl(
|
||||
private val mqttRepository: MQTTRepository,
|
||||
private val packetHandler: PacketHandler,
|
||||
private val serviceRepository: ServiceRepository,
|
||||
private val serviceStateWriter: ServiceStateWriter,
|
||||
private val nodeRepository: NodeRepository,
|
||||
@Named("ServiceScope") private val scope: CoroutineScope,
|
||||
) : MqttManager {
|
||||
@@ -79,7 +79,7 @@ class MqttManagerImpl(
|
||||
is MqttException.ConnectionLost -> "MQTT: connection lost"
|
||||
else -> "MQTT proxy failed: ${throwable.message}"
|
||||
}
|
||||
serviceRepository.setErrorMessage(text = message, severity = Severity.Warn)
|
||||
serviceStateWriter.setErrorMessage(text = message, severity = Severity.Warn)
|
||||
}
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
@@ -17,35 +17,26 @@
|
||||
package org.meshtastic.core.data.manager
|
||||
|
||||
import co.touchlab.kermit.Logger
|
||||
import kotlinx.atomicfu.atomic
|
||||
import kotlinx.atomicfu.update
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.common.util.NumberFormatter
|
||||
import org.meshtastic.core.common.util.nowMillis
|
||||
import org.meshtastic.core.repository.NeighborInfoHandler
|
||||
import org.meshtastic.core.repository.NodeManager
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
import org.meshtastic.core.repository.ServiceBroadcasts
|
||||
import org.meshtastic.core.repository.ServiceRepository
|
||||
import org.meshtastic.core.repository.ServiceStateWriter
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.NeighborInfo
|
||||
|
||||
@Single
|
||||
class NeighborInfoHandlerImpl(
|
||||
private val nodeManager: NodeManager,
|
||||
private val serviceRepository: ServiceRepository,
|
||||
private val serviceBroadcasts: ServiceBroadcasts,
|
||||
private val serviceStateWriter: ServiceStateWriter,
|
||||
private val nodeRepository: NodeRepository,
|
||||
) : NeighborInfoHandler {
|
||||
|
||||
private val startTimes = atomic(persistentMapOf<Int, Long>())
|
||||
private val requestTimer = RequestTimer()
|
||||
|
||||
override var lastNeighborInfo: NeighborInfo? = null
|
||||
|
||||
override fun recordStartTime(requestId: Int) {
|
||||
startTimes.update { it.put(requestId, nowMillis) }
|
||||
}
|
||||
override fun recordStartTime(requestId: Int) = requestTimer.start(requestId)
|
||||
|
||||
override fun handleNeighborInfo(packet: MeshPacket) {
|
||||
val payload = packet.decoded?.payload ?: return
|
||||
@@ -58,13 +49,8 @@ class NeighborInfoHandlerImpl(
|
||||
Logger.d { "Stored last neighbor info from connected radio" }
|
||||
}
|
||||
|
||||
// Update Node DB
|
||||
nodeManager.nodeDBbyNodeNum[from]?.let { serviceBroadcasts.broadcastNodeChange(it) }
|
||||
|
||||
// Format for UI response
|
||||
val requestId = packet.decoded?.request_id ?: 0
|
||||
val start = startTimes.value[requestId]
|
||||
startTimes.update { it.remove(requestId) }
|
||||
|
||||
val neighbors =
|
||||
ni.neighbors.joinToString("\n") { n ->
|
||||
@@ -76,20 +62,8 @@ class NeighborInfoHandlerImpl(
|
||||
val fromUser = nodeRepository.getUser(from)
|
||||
val formatted = "Neighbors of ${fromUser.long_name}:\n$neighbors"
|
||||
|
||||
val responseText =
|
||||
if (start != null) {
|
||||
val elapsedMs = nowMillis - start
|
||||
val seconds = elapsedMs / MILLIS_PER_SECOND
|
||||
Logger.i { "Neighbor info $requestId complete in $seconds s" }
|
||||
"$formatted\n\nDuration: ${NumberFormatter.format(seconds, 1)} s"
|
||||
} else {
|
||||
formatted
|
||||
}
|
||||
val responseText = requestTimer.appendDuration(requestId, formatted, "Neighbor info")
|
||||
|
||||
serviceRepository.setNeighborInfoResponse(responseText)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MILLIS_PER_SECOND = 1000.0
|
||||
serviceStateWriter.setNeighborInfoResponse(responseText)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.meshtastic.core.data.manager
|
||||
import co.touchlab.kermit.Logger
|
||||
import kotlinx.atomicfu.atomic
|
||||
import kotlinx.atomicfu.update
|
||||
import kotlinx.collections.immutable.PersistentMap
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -28,20 +29,14 @@ import org.koin.core.annotation.Named
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.common.util.clampTimestampToNow
|
||||
import org.meshtastic.core.common.util.handledLaunch
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.DeviceMetrics
|
||||
import org.meshtastic.core.model.EnvironmentMetrics
|
||||
import org.meshtastic.core.model.MeshUser
|
||||
import org.meshtastic.core.model.MyNodeInfo
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.model.NodeInfo
|
||||
import org.meshtastic.core.model.Position
|
||||
import org.meshtastic.core.model.NodeAddress
|
||||
import org.meshtastic.core.model.util.NodeIdLookup
|
||||
import org.meshtastic.core.repository.NodeManager
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
import org.meshtastic.core.repository.Notification
|
||||
import org.meshtastic.core.repository.NotificationManager
|
||||
import org.meshtastic.core.repository.ServiceBroadcasts
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.getStringSuspend
|
||||
import org.meshtastic.core.resources.new_node_seen
|
||||
@@ -60,19 +55,57 @@ import org.meshtastic.proto.Position as ProtoPosition
|
||||
@Single(binds = [NodeManager::class, NodeIdLookup::class])
|
||||
class NodeManagerImpl(
|
||||
private val nodeRepository: NodeRepository,
|
||||
private val serviceBroadcasts: ServiceBroadcasts,
|
||||
private val notificationManager: NotificationManager,
|
||||
@Named("ServiceScope") private val scope: CoroutineScope,
|
||||
) : NodeManager {
|
||||
|
||||
private val _nodeDBbyNodeNum = atomic(persistentMapOf<Int, Node>())
|
||||
private val _nodeDBbyID = atomic(persistentMapOf<String, Node>())
|
||||
// Two indices over the same node set: byNum is the canonical store (mesh-level identifier), byId is a secondary
|
||||
// O(1) lookup for the user-facing hex string. Both are held in a single atomic ref so updates are observed
|
||||
// consistently — concurrent readers never see an entry present in one index but not the other.
|
||||
private data class NodeIndex(
|
||||
val byNum: PersistentMap<Int, Node> = persistentMapOf(),
|
||||
val byId: PersistentMap<String, Node> = persistentMapOf(),
|
||||
) {
|
||||
fun put(num: Int, node: Node): NodeIndex {
|
||||
val previous = byNum[num]
|
||||
var nextById = byId
|
||||
// If the user.id changed (e.g. firmware reassigned the hex id) drop the stale id entry.
|
||||
if (previous != null && previous.user.id.isNotEmpty() && previous.user.id != node.user.id) {
|
||||
nextById = nextById.remove(previous.user.id)
|
||||
}
|
||||
if (node.user.id.isNotEmpty()) {
|
||||
nextById = nextById.put(node.user.id, node)
|
||||
}
|
||||
return NodeIndex(byNum = byNum.put(num, node), byId = nextById)
|
||||
}
|
||||
|
||||
fun remove(num: Int): NodeIndex {
|
||||
val previous = byNum[num] ?: return this
|
||||
return NodeIndex(
|
||||
byNum = byNum.remove(num),
|
||||
byId = if (previous.user.id.isNotEmpty()) byId.remove(previous.user.id) else byId,
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromByNum(nodes: Map<Int, Node>): NodeIndex {
|
||||
var byNum = persistentMapOf<Int, Node>()
|
||||
var byId = persistentMapOf<String, Node>()
|
||||
for ((num, node) in nodes) {
|
||||
byNum = byNum.put(num, node)
|
||||
if (node.user.id.isNotEmpty()) byId = byId.put(node.user.id, node)
|
||||
}
|
||||
return NodeIndex(byNum, byId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val nodeIndex = atomic(NodeIndex())
|
||||
|
||||
override val nodeDBbyNodeNum: Map<Int, Node>
|
||||
get() = _nodeDBbyNodeNum.value
|
||||
get() = nodeIndex.value.byNum
|
||||
|
||||
override val nodeDBbyID: Map<String, Node>
|
||||
get() = _nodeDBbyID.value
|
||||
override fun getNodeById(id: String): Node? = nodeIndex.value.byId[id]
|
||||
|
||||
override val isNodeDbReady = MutableStateFlow(false)
|
||||
override val allowNodeDbWrites = MutableStateFlow(false)
|
||||
@@ -104,10 +137,7 @@ class NodeManagerImpl(
|
||||
override fun loadCachedNodeDB() {
|
||||
scope.handledLaunch {
|
||||
val nodes = nodeRepository.nodeDBbyNum.first()
|
||||
_nodeDBbyNodeNum.value = persistentMapOf<Int, Node>().putAll(nodes)
|
||||
val byId = mutableMapOf<String, Node>()
|
||||
nodes.values.forEach { byId[it.user.id] = it }
|
||||
_nodeDBbyID.value = persistentMapOf<String, Node>().putAll(byId)
|
||||
nodeIndex.value = NodeIndex.fromByNum(nodes)
|
||||
if (myNodeNum.value == null) {
|
||||
myNodeNum.value = nodeRepository.myNodeInfo.value?.myNodeNum
|
||||
}
|
||||
@@ -115,8 +145,7 @@ class NodeManagerImpl(
|
||||
}
|
||||
|
||||
override fun clear() {
|
||||
_nodeDBbyNodeNum.value = persistentMapOf()
|
||||
_nodeDBbyID.value = persistentMapOf()
|
||||
nodeIndex.value = NodeIndex()
|
||||
isNodeDbReady.value = false
|
||||
allowNodeDbWrites.value = false
|
||||
myNodeNum.value = null
|
||||
@@ -125,7 +154,7 @@ class NodeManagerImpl(
|
||||
|
||||
override fun getMyNodeInfo(): MyNodeInfo? {
|
||||
val mi = nodeRepository.myNodeInfo.value ?: return null
|
||||
val myNode = _nodeDBbyNodeNum.value[mi.myNodeNum]
|
||||
val myNode = nodeIndex.value.byNum[mi.myNodeNum]
|
||||
return MyNodeInfo(
|
||||
myNodeNum = mi.myNodeNum,
|
||||
hasGPS = (myNode?.position?.latitude_i ?: 0) != 0,
|
||||
@@ -146,24 +175,16 @@ class NodeManagerImpl(
|
||||
|
||||
override fun getMyId(): String {
|
||||
val num = myNodeNum.value ?: nodeRepository.myNodeInfo.value?.myNodeNum ?: return ""
|
||||
return _nodeDBbyNodeNum.value[num]?.user?.id ?: ""
|
||||
return nodeIndex.value.byNum[num]?.user?.id ?: ""
|
||||
}
|
||||
|
||||
override fun getNodes(): List<NodeInfo> = _nodeDBbyNodeNum.value.values.map { it.toNodeInfo() }
|
||||
|
||||
override fun removeByNodenum(nodeNum: Int) {
|
||||
val removed = atomic<Node?>(null)
|
||||
_nodeDBbyNodeNum.update { map ->
|
||||
val node = map[nodeNum]
|
||||
removed.value = node
|
||||
map.remove(nodeNum)
|
||||
}
|
||||
removed.value?.let { node -> _nodeDBbyID.update { it.remove(node.user.id) } }
|
||||
nodeIndex.update { it.remove(nodeNum) }
|
||||
}
|
||||
|
||||
internal fun getOrCreateNode(n: Int, channel: Int = 0): Node = _nodeDBbyNodeNum.value[n]
|
||||
internal fun getOrCreateNode(n: Int, channel: Int = 0): Node = nodeIndex.value.byNum[n]
|
||||
?: run {
|
||||
val userId = DataPacket.nodeNumToDefaultId(n)
|
||||
val userId = NodeAddress.numToDefaultId(n)
|
||||
val defaultUser =
|
||||
User(
|
||||
id = userId,
|
||||
@@ -175,29 +196,22 @@ class NodeManagerImpl(
|
||||
Node(num = n, user = defaultUser, channel = channel)
|
||||
}
|
||||
|
||||
override fun updateNode(nodeNum: Int, withBroadcast: Boolean, channel: Int, transform: (Node) -> Node) {
|
||||
override fun updateNode(nodeNum: Int, channel: Int, transform: (Node) -> Node) {
|
||||
// Perform read + transform inside update{} to ensure atomicity.
|
||||
// Without this, concurrent calls for the same nodeNum could read the same snapshot
|
||||
// and the last writer would silently overwrite the other's changes.
|
||||
var next: Node? = null
|
||||
_nodeDBbyNodeNum.update { map ->
|
||||
val current = map[nodeNum] ?: getOrCreateNode(nodeNum, channel)
|
||||
nodeIndex.update { index ->
|
||||
val current = index.byNum[nodeNum] ?: getOrCreateNode(nodeNum, channel)
|
||||
val transformed = transform(current)
|
||||
next = transformed
|
||||
map.put(nodeNum, transformed)
|
||||
index.put(nodeNum, transformed)
|
||||
}
|
||||
val result = next ?: return
|
||||
if (result.user.id.isNotEmpty()) {
|
||||
_nodeDBbyID.update { it.put(result.user.id, result) }
|
||||
}
|
||||
|
||||
if (result.user.id.isNotEmpty() && isNodeDbReady.value) {
|
||||
scope.handledLaunch { nodeRepository.upsert(result) }
|
||||
}
|
||||
|
||||
if (withBroadcast) {
|
||||
serviceBroadcasts.broadcastNodeChange(result)
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleReceivedUser(fromNum: Int, p: User, channel: Int, manuallyVerified: Boolean) {
|
||||
@@ -287,8 +301,8 @@ class NodeManagerImpl(
|
||||
updateNode(nodeNum) { it.copy(nodeStatus = status?.takeIf { s -> s.isNotEmpty() }) }
|
||||
}
|
||||
|
||||
override fun installNodeInfo(info: ProtoNodeInfo, withBroadcast: Boolean) {
|
||||
updateNode(info.num, withBroadcast = withBroadcast) { node ->
|
||||
override fun installNodeInfo(info: ProtoNodeInfo) {
|
||||
updateNode(info.num) { node ->
|
||||
var next = node
|
||||
val user = info.user
|
||||
if (user != null) {
|
||||
@@ -334,48 +348,9 @@ class NodeManagerImpl(
|
||||
return hasExistingUser && isDefaultName && isDefaultHwModel
|
||||
}
|
||||
|
||||
override fun toNodeID(nodeNum: Int): String = if (nodeNum == DataPacket.NODENUM_BROADCAST) {
|
||||
DataPacket.ID_BROADCAST
|
||||
override fun toNodeID(nodeNum: Int): String = if (nodeNum == NodeAddress.NODENUM_BROADCAST) {
|
||||
NodeAddress.ID_BROADCAST
|
||||
} else {
|
||||
_nodeDBbyNodeNum.value[nodeNum]?.user?.id ?: DataPacket.nodeNumToDefaultId(nodeNum)
|
||||
nodeIndex.value.byNum[nodeNum]?.user?.id ?: NodeAddress.numToDefaultId(nodeNum)
|
||||
}
|
||||
|
||||
private fun Node.toNodeInfo(): NodeInfo = NodeInfo(
|
||||
num = num,
|
||||
user =
|
||||
MeshUser(
|
||||
id = user.id,
|
||||
longName = user.long_name,
|
||||
shortName = user.short_name,
|
||||
hwModel = user.hw_model,
|
||||
role = user.role.value,
|
||||
),
|
||||
position =
|
||||
Position(
|
||||
latitude = latitude,
|
||||
longitude = longitude,
|
||||
altitude = position.altitude ?: 0,
|
||||
time = position.time,
|
||||
satellitesInView = position.sats_in_view,
|
||||
groundSpeed = position.ground_speed ?: 0,
|
||||
groundTrack = position.ground_track ?: 0,
|
||||
precisionBits = position.precision_bits,
|
||||
)
|
||||
.takeIf { latitude != 0.0 || longitude != 0.0 },
|
||||
snr = snr,
|
||||
rssi = rssi,
|
||||
lastHeard = lastHeard,
|
||||
deviceMetrics =
|
||||
DeviceMetrics(
|
||||
batteryLevel = deviceMetrics.battery_level ?: 0,
|
||||
voltage = deviceMetrics.voltage ?: 0f,
|
||||
channelUtilization = deviceMetrics.channel_utilization ?: 0f,
|
||||
airUtilTx = deviceMetrics.air_util_tx ?: 0f,
|
||||
uptimeSeconds = deviceMetrics.uptime_seconds ?: 0,
|
||||
),
|
||||
channel = channel,
|
||||
environmentMetrics = EnvironmentMetrics.fromTelemetryProto(environmentMetrics, 0),
|
||||
hopsAway = hopsAway,
|
||||
nodeStatus = nodeStatus,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -24,9 +24,7 @@ import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import kotlinx.coroutines.asDeferred
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.consumeAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
@@ -43,12 +41,11 @@ import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.model.RadioNotConnectedException
|
||||
import org.meshtastic.core.model.util.toOneLineString
|
||||
import org.meshtastic.core.model.util.toPIIString
|
||||
import org.meshtastic.core.repository.ConnectionStateProvider
|
||||
import org.meshtastic.core.repository.MeshLogRepository
|
||||
import org.meshtastic.core.repository.PacketHandler
|
||||
import org.meshtastic.core.repository.PacketRepository
|
||||
import org.meshtastic.core.repository.RadioInterfaceService
|
||||
import org.meshtastic.core.repository.ServiceBroadcasts
|
||||
import org.meshtastic.core.repository.ServiceRepository
|
||||
import org.meshtastic.proto.FromRadio
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.QueueStatus
|
||||
@@ -61,10 +58,9 @@ import kotlin.uuid.Uuid
|
||||
@Single
|
||||
class PacketHandlerImpl(
|
||||
private val packetRepository: Lazy<PacketRepository>,
|
||||
private val serviceBroadcasts: ServiceBroadcasts,
|
||||
private val radioInterfaceService: RadioInterfaceService,
|
||||
private val meshLogRepository: Lazy<MeshLogRepository>,
|
||||
private val serviceRepository: ServiceRepository,
|
||||
private val connectionStateProvider: ConnectionStateProvider,
|
||||
@Named("ServiceScope") private val scope: CoroutineScope,
|
||||
) : PacketHandler {
|
||||
|
||||
@@ -77,11 +73,6 @@ class PacketHandlerImpl(
|
||||
private val queueMutex = Mutex()
|
||||
private val queuedPackets = mutableListOf<MeshPacket>()
|
||||
|
||||
// Unbounded channel preserves FIFO ordering of fire-and-forget sendToRadio(MeshPacket)
|
||||
// calls. The non-suspend entry point does trySend (always succeeds for UNLIMITED) and
|
||||
// a single consumer coroutine enqueues packets under queueMutex in arrival order.
|
||||
private val outboundChannel = Channel<MeshPacket>(Channel.UNLIMITED)
|
||||
|
||||
// Set to true by stopPacketQueue() under queueMutex. Checked by startPacketQueueLocked()
|
||||
// and the queue processor's finally block to prevent restarting a stopped queue.
|
||||
private var queueStopped = false
|
||||
@@ -89,20 +80,6 @@ class PacketHandlerImpl(
|
||||
private val responseMutex = Mutex()
|
||||
private val queueResponse = mutableMapOf<Int, CompletableDeferred<Boolean>>()
|
||||
|
||||
init {
|
||||
// Single consumer serializes enqueues from the non-suspend sendToRadio(MeshPacket)
|
||||
// entry point, preserving FIFO across rapid concurrent callers.
|
||||
scope.launch {
|
||||
outboundChannel.consumeAsFlow().collect { packet ->
|
||||
queueMutex.withLock {
|
||||
queueStopped = false // Allow queue to resume after a disconnect/reconnect cycle.
|
||||
queuedPackets.add(packet)
|
||||
startPacketQueueLocked()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun sendToRadio(p: ToRadio) {
|
||||
Logger.d { "Sending to radio ${p.toPIIString()}" }
|
||||
val b = p.encode()
|
||||
@@ -126,10 +103,18 @@ class PacketHandlerImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override fun sendToRadio(packet: MeshPacket) {
|
||||
// Non-suspend entry point — order-preserving via unbounded channel drained by
|
||||
// a single consumer coroutine. trySend on UNLIMITED never fails for capacity.
|
||||
outboundChannel.trySend(packet)
|
||||
/**
|
||||
* Enqueue [packet] for transmission. Order is preserved for sequential calls from the same coroutine (mutex
|
||||
* acquisition is uncontested between sequential calls). Transactional sequences that require strict ordering across
|
||||
* multiple calls — e.g. an `editSettings { … }` begin → writes → commit sequence — MUST be issued from a single
|
||||
* coroutine; concurrent senders share FIFO only at the per-call grain.
|
||||
*/
|
||||
override suspend fun sendToRadio(packet: MeshPacket) {
|
||||
queueMutex.withLock {
|
||||
queueStopped = false
|
||||
queuedPackets.add(packet)
|
||||
startPacketQueueLocked()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("TooGenericExceptionCaught", "SwallowedException")
|
||||
@@ -206,7 +191,7 @@ class PacketHandlerImpl(
|
||||
queueJob =
|
||||
scope.handledLaunch {
|
||||
try {
|
||||
while (serviceRepository.connectionState.value == ConnectionState.Connected) {
|
||||
while (connectionStateProvider.connectionState.value == ConnectionState.Connected) {
|
||||
val packet = queueMutex.withLock { queuedPackets.removeFirstOrNull() } ?: break
|
||||
@Suppress("TooGenericExceptionCaught", "SwallowedException")
|
||||
try {
|
||||
@@ -247,7 +232,6 @@ class PacketHandlerImpl(
|
||||
getDataPacketById(packetId)?.let { p ->
|
||||
if (p.status == m) return@handledLaunch
|
||||
packetRepository.value.updateMessageStatus(p, m)
|
||||
serviceBroadcasts.broadcastMessageStatus(packetId, m)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -266,7 +250,7 @@ class PacketHandlerImpl(
|
||||
// Reuse a deferred pre-registered by sendToRadioAndAwait, or create a new one.
|
||||
val deferred = responseMutex.withLock { queueResponse.getOrPut(packet.id) { CompletableDeferred() } }
|
||||
try {
|
||||
if (serviceRepository.connectionState.value != ConnectionState.Connected) {
|
||||
if (connectionStateProvider.connectionState.value != ConnectionState.Connected) {
|
||||
throw RadioNotConnectedException()
|
||||
}
|
||||
sendToRadio(ToRadio(packet = packet))
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.data.manager
|
||||
|
||||
import co.touchlab.kermit.Logger
|
||||
import kotlinx.atomicfu.atomic
|
||||
import kotlinx.atomicfu.update
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import org.meshtastic.core.common.util.NumberFormatter
|
||||
import org.meshtastic.core.common.util.nowMillis
|
||||
|
||||
/**
|
||||
* Tracks per-request start times and reports round-trip durations for request/response handlers.
|
||||
*
|
||||
* Request handlers (traceroute, neighbor-info, …) call [start] when issuing a request keyed by its id, then
|
||||
* [appendDuration] when the matching response arrives to annotate the user-facing text with how long the round trip
|
||||
* took. Start times are stored in an atomic immutable map so [start] (any coroutine) and [appendDuration] (the handler
|
||||
* scope) never race.
|
||||
*/
|
||||
internal class RequestTimer {
|
||||
|
||||
private val startTimes = atomic(persistentMapOf<Int, Long>())
|
||||
|
||||
/** Records the start time for [requestId]. */
|
||||
fun start(requestId: Int) {
|
||||
startTimes.update { it.put(requestId, nowMillis) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Consumes the start time recorded for [requestId] and appends a `Duration: N s` line to [text], logging completion
|
||||
* under [logLabel]. Returns [text] unchanged when no start time was recorded for the id.
|
||||
*/
|
||||
fun appendDuration(requestId: Int, text: String, logLabel: String): String {
|
||||
val start = startTimes.value[requestId]
|
||||
startTimes.update { it.remove(requestId) }
|
||||
if (start == null) return text
|
||||
val seconds = (nowMillis - start) / MILLIS_PER_SECOND
|
||||
Logger.i { "$logLabel $requestId complete in $seconds s" }
|
||||
return "$text\n\nDuration: ${NumberFormatter.format(seconds, 1)} s"
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val MILLIS_PER_SECOND = 1000.0
|
||||
}
|
||||
}
|
||||
@@ -25,12 +25,12 @@ import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.common.util.handledLaunch
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.model.NodeAddress
|
||||
import org.meshtastic.core.model.util.SfppHasher
|
||||
import org.meshtastic.core.repository.HistoryManager
|
||||
import org.meshtastic.core.repository.MeshDataHandler
|
||||
import org.meshtastic.core.repository.NodeManager
|
||||
import org.meshtastic.core.repository.PacketRepository
|
||||
import org.meshtastic.core.repository.ServiceBroadcasts
|
||||
import org.meshtastic.core.repository.StoreForwardPacketHandler
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.PortNum
|
||||
@@ -43,7 +43,6 @@ import kotlin.time.Duration.Companion.milliseconds
|
||||
class StoreForwardPacketHandlerImpl(
|
||||
private val nodeManager: NodeManager,
|
||||
private val packetRepository: Lazy<PacketRepository>,
|
||||
private val serviceBroadcasts: ServiceBroadcasts,
|
||||
private val historyManager: HistoryManager,
|
||||
private val dataHandler: Lazy<MeshDataHandler>,
|
||||
@Named("ServiceScope") private val scope: CoroutineScope,
|
||||
@@ -105,7 +104,7 @@ class StoreForwardPacketHandlerImpl(
|
||||
encryptedPayload = sfpp.message.toByteArray(),
|
||||
to =
|
||||
if (sfpp.encapsulated_to == 0) {
|
||||
DataPacket.NODENUM_BROADCAST
|
||||
NodeAddress.NODENUM_BROADCAST
|
||||
} else {
|
||||
sfpp.encapsulated_to
|
||||
},
|
||||
@@ -131,7 +130,6 @@ class StoreForwardPacketHandlerImpl(
|
||||
rxTime = sfpp.encapsulated_rxtime.toLong() and 0xFFFFFFFFL,
|
||||
myNodeNum = nodeManager.myNodeNum.value ?: 0,
|
||||
)
|
||||
serviceBroadcasts.broadcastMessageStatus(sfpp.encapsulated_id, status)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,7 +181,7 @@ class StoreForwardPacketHandlerImpl(
|
||||
|
||||
s.text != null -> {
|
||||
if (s.rr == StoreAndForward.RequestResponse.ROUTER_TEXT_BROADCAST) {
|
||||
dataPacket.to = DataPacket.ID_BROADCAST
|
||||
dataPacket.to = NodeAddress.ID_BROADCAST
|
||||
}
|
||||
val u = dataPacket.copy(bytes = s.text, dataType = PortNum.TEXT_MESSAGE_APP.value)
|
||||
dataHandler.value.rememberDataPacket(u, myNodeNum)
|
||||
|
||||
@@ -16,22 +16,16 @@
|
||||
*/
|
||||
package org.meshtastic.core.data.manager
|
||||
|
||||
import co.touchlab.kermit.Logger
|
||||
import kotlinx.atomicfu.atomic
|
||||
import kotlinx.atomicfu.update
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import org.koin.core.annotation.Named
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.common.util.NumberFormatter
|
||||
import org.meshtastic.core.common.util.handledLaunch
|
||||
import org.meshtastic.core.common.util.nowMillis
|
||||
import org.meshtastic.core.model.fullRouteDiscovery
|
||||
import org.meshtastic.core.model.getTracerouteResponse
|
||||
import org.meshtastic.core.model.service.TracerouteResponse
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
import org.meshtastic.core.repository.ServiceRepository
|
||||
import org.meshtastic.core.repository.ServiceStateWriter
|
||||
import org.meshtastic.core.repository.TracerouteHandler
|
||||
import org.meshtastic.core.repository.TracerouteSnapshotRepository
|
||||
import org.meshtastic.core.resources.Res
|
||||
@@ -42,17 +36,15 @@ import org.meshtastic.proto.MeshPacket
|
||||
|
||||
@Single
|
||||
class TracerouteHandlerImpl(
|
||||
private val serviceRepository: ServiceRepository,
|
||||
private val serviceStateWriter: ServiceStateWriter,
|
||||
private val tracerouteSnapshotRepository: TracerouteSnapshotRepository,
|
||||
private val nodeRepository: NodeRepository,
|
||||
@Named("ServiceScope") private val scope: CoroutineScope,
|
||||
) : TracerouteHandler {
|
||||
|
||||
private val startTimes = atomic(persistentMapOf<Int, Long>())
|
||||
private val requestTimer = RequestTimer()
|
||||
|
||||
override fun recordStartTime(requestId: Int) {
|
||||
startTimes.update { it.put(requestId, nowMillis) }
|
||||
}
|
||||
override fun recordStartTime(requestId: Int) = requestTimer.start(requestId)
|
||||
|
||||
override fun handleTraceroute(packet: MeshPacket, logUuid: String?, logInsertJob: Job?) {
|
||||
// Decode the route discovery once — avoids triple protobuf decode
|
||||
@@ -85,21 +77,11 @@ class TracerouteHandlerImpl(
|
||||
tracerouteSnapshotRepository.upsertSnapshotPositions(logUuid, requestId, snapshotPositions)
|
||||
}
|
||||
|
||||
val start = startTimes.value[requestId]
|
||||
startTimes.update { it.remove(requestId) }
|
||||
val responseText =
|
||||
if (start != null) {
|
||||
val elapsedMs = nowMillis - start
|
||||
val seconds = elapsedMs / MILLIS_PER_SECOND
|
||||
Logger.i { "Traceroute $requestId complete in $seconds s" }
|
||||
"$full\n\nDuration: ${NumberFormatter.format(seconds, 1)} s"
|
||||
} else {
|
||||
full
|
||||
}
|
||||
val responseText = requestTimer.appendDuration(requestId, full, "Traceroute")
|
||||
|
||||
val destination = forwardRoute.firstOrNull() ?: returnRoute.lastOrNull() ?: 0
|
||||
|
||||
serviceRepository.setTracerouteResponse(
|
||||
serviceStateWriter.setTracerouteResponse(
|
||||
TracerouteResponse(
|
||||
message = responseText,
|
||||
destinationNodeNum = destination,
|
||||
@@ -111,8 +93,4 @@ class TracerouteHandlerImpl(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MILLIS_PER_SECOND = 1000.0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,10 +43,10 @@ import org.meshtastic.core.database.entity.MyNodeEntity
|
||||
import org.meshtastic.core.database.entity.NodeEntity
|
||||
import org.meshtastic.core.datastore.LocalStatsDataSource
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.MeshLog
|
||||
import org.meshtastic.core.model.MyNodeInfo
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.model.NodeAddress
|
||||
import org.meshtastic.core.model.NodeSortOption
|
||||
import org.meshtastic.core.model.util.onlineTimeThreshold
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
@@ -137,10 +137,10 @@ class NodeRepositoryImpl(
|
||||
|
||||
/** Returns the [Node] associated with a given [userId]. Falls back to a generic node if not found. */
|
||||
override fun getNode(userId: String): Node = nodeDBbyNum.value.values.find { it.user.id == userId }
|
||||
?: Node(num = DataPacket.idToDefaultNodeNum(userId) ?: 0, user = getUser(userId))
|
||||
?: Node(num = NodeAddress.idToNum(userId) ?: 0, user = getUser(userId))
|
||||
|
||||
/** Returns the [User] info for a given [nodeNum]. */
|
||||
override fun getUser(nodeNum: Int): User = getUser(DataPacket.nodeNumToDefaultId(nodeNum))
|
||||
override fun getUser(nodeNum: Int): User = getUser(NodeAddress.numToDefaultId(nodeNum))
|
||||
|
||||
private val last4 = 4
|
||||
|
||||
@@ -152,14 +152,17 @@ class NodeRepositoryImpl(
|
||||
}
|
||||
|
||||
val fallbackId = userId.takeLast(last4)
|
||||
// Single equality check replaces two NodeAddress.fromString calls — getUser is called per paged contact
|
||||
// and per text-message arrival, so the parser allocations add up.
|
||||
val isLocal = userId == NodeAddress.ID_LOCAL
|
||||
val defaultLong =
|
||||
if (userId == DataPacket.ID_LOCAL) {
|
||||
if (isLocal) {
|
||||
ourNodeInfo.value?.user?.long_name?.takeIf { it.isNotBlank() } ?: "Local"
|
||||
} else {
|
||||
"Meshtastic $fallbackId"
|
||||
}
|
||||
val defaultShort =
|
||||
if (userId == DataPacket.ID_LOCAL) {
|
||||
if (isLocal) {
|
||||
ourNodeInfo.value?.user?.short_name?.takeIf { it.isNotBlank() } ?: "Local"
|
||||
} else {
|
||||
fallbackId
|
||||
|
||||
@@ -20,6 +20,7 @@ import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.map
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
@@ -38,6 +39,7 @@ import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.Message
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.model.NodeAddress
|
||||
import org.meshtastic.core.model.Reaction
|
||||
import org.meshtastic.proto.ChannelSettings
|
||||
import org.meshtastic.proto.PortNum
|
||||
@@ -90,13 +92,15 @@ class PacketRepositoryImpl(private val dbManager: DatabaseProvider, private val
|
||||
dbManager.currentDb.flatMapLatest { db -> db.packetDao().getUnreadCountTotal() }
|
||||
|
||||
override suspend fun clearUnreadCount(contact: String, timestamp: Long) =
|
||||
withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().clearUnreadCount(contact, timestamp) }
|
||||
withContext(dispatchers.io + NonCancellable) {
|
||||
dbManager.currentDb.value.packetDao().clearUnreadCount(contact, timestamp)
|
||||
}
|
||||
|
||||
override suspend fun clearAllUnreadCounts() =
|
||||
withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().clearAllUnreadCounts() }
|
||||
withContext(dispatchers.io + NonCancellable) { dbManager.currentDb.value.packetDao().clearAllUnreadCounts() }
|
||||
|
||||
override suspend fun updateLastReadMessage(contact: String, messageUuid: Long, lastReadTimestamp: Long) =
|
||||
withContext(dispatchers.io) {
|
||||
withContext(dispatchers.io + NonCancellable) {
|
||||
val dao = dbManager.currentDb.value.packetDao()
|
||||
val current = dao.getContactSettings(contact)
|
||||
val existingTimestamp = current?.lastReadMessageTimestamp ?: Long.MIN_VALUE
|
||||
@@ -116,7 +120,7 @@ class PacketRepositoryImpl(private val dbManager: DatabaseProvider, private val
|
||||
}
|
||||
|
||||
suspend fun insertRoomPacket(packet: RoomPacket) =
|
||||
withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().insert(packet) }
|
||||
withContext(dispatchers.io + NonCancellable) { dbManager.currentDb.value.packetDao().insert(packet) }
|
||||
|
||||
override suspend fun savePacket(
|
||||
myNodeNum: Int,
|
||||
@@ -342,13 +346,13 @@ class PacketRepositoryImpl(private val dbManager: DatabaseProvider, private val
|
||||
val dao = dbManager.currentDb.value.packetDao()
|
||||
val packets = findPacketsWithIdInternal(packetId)
|
||||
val reactions = findReactionsWithIdInternal(packetId)
|
||||
val fromId = DataPacket.nodeNumToDefaultId(from)
|
||||
val fromId = NodeAddress.numToDefaultId(from)
|
||||
val isFromLocalNode = myNodeNum != null && from == myNodeNum
|
||||
val toId =
|
||||
if (to == 0 || to == DataPacket.NODENUM_BROADCAST) {
|
||||
DataPacket.ID_BROADCAST
|
||||
if (to == 0 || to == NodeAddress.NODENUM_BROADCAST) {
|
||||
NodeAddress.ID_BROADCAST
|
||||
} else {
|
||||
DataPacket.nodeNumToDefaultId(to)
|
||||
NodeAddress.numToDefaultId(to)
|
||||
}
|
||||
|
||||
val hashByteString = hash.toByteString()
|
||||
@@ -356,7 +360,7 @@ class PacketRepositoryImpl(private val dbManager: DatabaseProvider, private val
|
||||
packets.forEach { packet ->
|
||||
// For sent messages, from is stored as ID_LOCAL, but SFPP packet has node number
|
||||
val fromMatches =
|
||||
packet.data.from == fromId || (isFromLocalNode && packet.data.from == DataPacket.ID_LOCAL)
|
||||
packet.data.from == fromId || (isFromLocalNode && packet.data.from == NodeAddress.ID_LOCAL)
|
||||
co.touchlab.kermit.Logger.d {
|
||||
"SFPP match check: packetFrom=${packet.data.from} fromId=$fromId " +
|
||||
"isFromLocal=$isFromLocalNode fromMatches=$fromMatches " +
|
||||
@@ -376,7 +380,7 @@ class PacketRepositoryImpl(private val dbManager: DatabaseProvider, private val
|
||||
reactions.forEach { reaction ->
|
||||
val reactionFrom = reaction.userId
|
||||
// For sent reactions, from is stored as ID_LOCAL, but SFPP packet has node number
|
||||
val fromMatches = reactionFrom == fromId || (isFromLocalNode && reactionFrom == DataPacket.ID_LOCAL)
|
||||
val fromMatches = reactionFrom == fromId || (isFromLocalNode && reactionFrom == NodeAddress.ID_LOCAL)
|
||||
|
||||
val toMatches = reaction.to == toId
|
||||
|
||||
@@ -529,7 +533,7 @@ class PacketRepositoryImpl(private val dbManager: DatabaseProvider, private val
|
||||
packets.map { packet ->
|
||||
val node = getNode(packet.data.from)
|
||||
val isFromLocal =
|
||||
node.user.id == DataPacket.ID_LOCAL ||
|
||||
node.user.id == NodeAddress.ID_LOCAL ||
|
||||
(packet.myNodeNum != 0 && node.num == packet.myNodeNum)
|
||||
Message(
|
||||
uuid = packet.uuid,
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.data.manager
|
||||
|
||||
import dev.mokkery.MockMode
|
||||
import dev.mokkery.answering.returns
|
||||
import dev.mokkery.every
|
||||
import dev.mokkery.everySuspend
|
||||
import dev.mokkery.matcher.any
|
||||
import dev.mokkery.mock
|
||||
import dev.mokkery.verify
|
||||
import dev.mokkery.verifySuspend
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okio.ByteString
|
||||
import okio.ByteString.Companion.encodeUtf8
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.model.NodeAddress
|
||||
import org.meshtastic.core.repository.NeighborInfoHandler
|
||||
import org.meshtastic.core.repository.NodeManager
|
||||
import org.meshtastic.core.repository.PacketHandler
|
||||
import org.meshtastic.core.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.repository.SessionManager
|
||||
import org.meshtastic.core.repository.TracerouteHandler
|
||||
import org.meshtastic.proto.ChannelSet
|
||||
import org.meshtastic.proto.LocalConfig
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.NeighborInfo
|
||||
import org.meshtastic.proto.PortNum
|
||||
import org.meshtastic.proto.User
|
||||
import kotlin.test.BeforeTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertNotEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@Suppress("LargeClass")
|
||||
class CommandSenderImplTest {
|
||||
private val packetHandler = mock<PacketHandler>(MockMode.autofill)
|
||||
private val nodeManager = mock<NodeManager>(MockMode.autofill)
|
||||
private val radioConfigRepository = mock<RadioConfigRepository>(MockMode.autofill)
|
||||
private val tracerouteHandler = mock<TracerouteHandler>(MockMode.autofill)
|
||||
private val neighborInfoHandler = mock<NeighborInfoHandler>(MockMode.autofill)
|
||||
private val sessionManager = mock<SessionManager>(MockMode.autofill)
|
||||
|
||||
private lateinit var commandSender: CommandSenderImpl
|
||||
|
||||
@BeforeTest
|
||||
fun setup() {
|
||||
every { radioConfigRepository.localConfigFlow } returns flowOf(LocalConfig())
|
||||
every { radioConfigRepository.channelSetFlow } returns flowOf(ChannelSet())
|
||||
every { nodeManager.myNodeNum } returns MutableStateFlow(MY_NODE_NUM)
|
||||
every { nodeManager.nodeDBbyNodeNum } returns emptyMap()
|
||||
every { sessionManager.getPasskey(any()) } returns ByteString.EMPTY
|
||||
|
||||
commandSender =
|
||||
CommandSenderImpl(
|
||||
packetHandler = packetHandler,
|
||||
nodeManager = nodeManager,
|
||||
radioConfigRepository = radioConfigRepository,
|
||||
tracerouteHandler = tracerouteHandler,
|
||||
neighborInfoHandler = neighborInfoHandler,
|
||||
sessionManager = sessionManager,
|
||||
scope = TestScope(),
|
||||
)
|
||||
}
|
||||
|
||||
// --- generatePacketId ---
|
||||
|
||||
@Test
|
||||
fun generatePacketId_returnsNonZero() {
|
||||
val id = commandSender.generatePacketId()
|
||||
assertNotEquals(0, id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun generatePacketId_isIncrementing() {
|
||||
val first = commandSender.generatePacketId()
|
||||
val second = commandSender.generatePacketId()
|
||||
assertNotEquals(first, second)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun generatePacketId_staysNonZeroOverManyIterations() {
|
||||
repeat(100) { assertNotEquals(0, commandSender.generatePacketId()) }
|
||||
}
|
||||
|
||||
// --- resolveNodeNum ---
|
||||
|
||||
@Test
|
||||
fun resolveNodeNum_broadcast_returnsNodeNumBroadcast() {
|
||||
val result = commandSender.resolveNodeNum(NodeAddress.Broadcast)
|
||||
assertEquals(NodeAddress.NODENUM_BROADCAST, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun resolveNodeNum_local_returnsMyNodeNum() {
|
||||
val result = commandSender.resolveNodeNum(NodeAddress.Local)
|
||||
assertEquals(MY_NODE_NUM, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun resolveNodeNum_local_returnsZeroWhenMyNodeNumNull() {
|
||||
every { nodeManager.myNodeNum } returns MutableStateFlow(null)
|
||||
commandSender =
|
||||
CommandSenderImpl(
|
||||
packetHandler = packetHandler,
|
||||
nodeManager = nodeManager,
|
||||
radioConfigRepository = radioConfigRepository,
|
||||
tracerouteHandler = tracerouteHandler,
|
||||
neighborInfoHandler = neighborInfoHandler,
|
||||
sessionManager = sessionManager,
|
||||
scope = TestScope(),
|
||||
)
|
||||
assertEquals(0, commandSender.resolveNodeNum(NodeAddress.Local))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun resolveNodeNum_byNum_returnsPassthrough() {
|
||||
assertEquals(42, commandSender.resolveNodeNum(NodeAddress.ByNum(42)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun resolveNodeNum_byId_looksUpAndReturns() {
|
||||
val node = Node(num = 99, user = User(id = "!deadbeef"))
|
||||
every { nodeManager.getNodeById("!deadbeef") } returns node
|
||||
assertEquals(99, commandSender.resolveNodeNum(NodeAddress.ById("!deadbeef")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun resolveNodeNum_byId_throwsForUnknown() {
|
||||
every { nodeManager.getNodeById("!unknown") } returns null
|
||||
assertFailsWith<IllegalArgumentException> { commandSender.resolveNodeNum(NodeAddress.ById("!unknown")) }
|
||||
}
|
||||
|
||||
// --- sendData ---
|
||||
|
||||
@Test
|
||||
fun sendData_setsIdWhenZero() = runTest {
|
||||
val packet = DataPacket(to = "^all", bytes = "hi".encodeUtf8(), dataType = PortNum.TEXT_MESSAGE_APP.value)
|
||||
packet.id = 0
|
||||
everySuspend { packetHandler.sendToRadio(any<MeshPacket>()) } returns Unit
|
||||
|
||||
commandSender.sendData(packet)
|
||||
assertNotEquals(0, packet.id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sendData_setsStatusQueued() = runTest {
|
||||
val packet = DataPacket(to = "^all", bytes = "hello".encodeUtf8(), dataType = PortNum.TEXT_MESSAGE_APP.value)
|
||||
everySuspend { packetHandler.sendToRadio(any<MeshPacket>()) } returns Unit
|
||||
|
||||
commandSender.sendData(packet)
|
||||
assertEquals(MessageStatus.QUEUED, packet.status)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sendData_rejectsOversizedPayload() = runTest {
|
||||
val oversizedBytes = ByteString.of(*ByteArray(300) { 0x42 })
|
||||
val packet = DataPacket(to = "^all", bytes = oversizedBytes, dataType = PortNum.TEXT_MESSAGE_APP.value)
|
||||
|
||||
val ex = assertFailsWith<IllegalStateException> { commandSender.sendData(packet) }
|
||||
assertTrue(ex.message!!.contains("Message too long"))
|
||||
assertEquals(MessageStatus.ERROR, packet.status)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sendData_requiresNonZeroDataType() = runTest {
|
||||
val packet = DataPacket(to = "^all", bytes = "test".encodeUtf8(), dataType = 0)
|
||||
assertFailsWith<IllegalArgumentException> { commandSender.sendData(packet) }
|
||||
}
|
||||
|
||||
// --- sendAdmin ---
|
||||
|
||||
@Test
|
||||
fun sendAdmin_injectsSessionPasskey() = runTest {
|
||||
val passkey = "secret".encodeUtf8()
|
||||
every { sessionManager.getPasskey(DEST_NODE) } returns passkey
|
||||
everySuspend { packetHandler.sendToRadio(any<MeshPacket>()) } returns Unit
|
||||
|
||||
commandSender.sendAdmin(DEST_NODE) { org.meshtastic.proto.AdminMessage(get_owner_request = true) }
|
||||
|
||||
verifySuspend { packetHandler.sendToRadio(any<MeshPacket>()) }
|
||||
}
|
||||
|
||||
// --- requestTraceroute ---
|
||||
|
||||
@Test
|
||||
fun requestTraceroute_recordsStartTime() = runTest {
|
||||
everySuspend { packetHandler.sendToRadio(any<MeshPacket>()) } returns Unit
|
||||
|
||||
commandSender.requestTraceroute(requestId = 42, destNum = DEST_NODE)
|
||||
|
||||
verify { tracerouteHandler.recordStartTime(42) }
|
||||
}
|
||||
|
||||
// --- requestNeighborInfo ---
|
||||
|
||||
@Test
|
||||
fun requestNeighborInfo_localNode_usesCachedNeighborInfo() = runTest {
|
||||
val cached = NeighborInfo(node_id = MY_NODE_NUM, last_sent_by_id = MY_NODE_NUM)
|
||||
every { neighborInfoHandler.lastNeighborInfo } returns cached
|
||||
everySuspend { packetHandler.sendToRadio(any<MeshPacket>()) } returns Unit
|
||||
|
||||
commandSender.requestNeighborInfo(requestId = 1, destNum = MY_NODE_NUM)
|
||||
|
||||
verifySuspend { packetHandler.sendToRadio(any<MeshPacket>()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun requestNeighborInfo_localNode_generatesDummyWhenNoCached() = runTest {
|
||||
every { neighborInfoHandler.lastNeighborInfo } returns null
|
||||
everySuspend { packetHandler.sendToRadio(any<MeshPacket>()) } returns Unit
|
||||
|
||||
commandSender.requestNeighborInfo(requestId = 1, destNum = MY_NODE_NUM)
|
||||
|
||||
verifySuspend { packetHandler.sendToRadio(any<MeshPacket>()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun requestNeighborInfo_remoteNode_sendsRequest() = runTest {
|
||||
everySuspend { packetHandler.sendToRadio(any<MeshPacket>()) } returns Unit
|
||||
|
||||
commandSender.requestNeighborInfo(requestId = 1, destNum = DEST_NODE)
|
||||
|
||||
verifySuspend { packetHandler.sendToRadio(any<MeshPacket>()) }
|
||||
}
|
||||
|
||||
// --- sendPosition ---
|
||||
|
||||
@Test
|
||||
fun sendPosition_updatesLocalPositionWhenNotFixed() = runTest {
|
||||
everySuspend { packetHandler.sendToRadio(any<MeshPacket>()) } returns Unit
|
||||
|
||||
val pos = org.meshtastic.proto.Position(latitude_i = 10000000, longitude_i = 20000000)
|
||||
commandSender.sendPosition(pos)
|
||||
|
||||
verify { nodeManager.handleReceivedPosition(MY_NODE_NUM, MY_NODE_NUM, any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sendPosition_skipsLocalUpdateWhenFixedPosition() = runTest {
|
||||
// Use MutableStateFlow so the init launchIn picks it up immediately in TestScope
|
||||
val configFlow =
|
||||
MutableStateFlow(LocalConfig(position = org.meshtastic.proto.Config.PositionConfig(fixed_position = true)))
|
||||
every { radioConfigRepository.localConfigFlow } returns configFlow
|
||||
every { radioConfigRepository.channelSetFlow } returns MutableStateFlow(ChannelSet())
|
||||
val testScope = TestScope()
|
||||
val fixedSender =
|
||||
CommandSenderImpl(
|
||||
packetHandler = packetHandler,
|
||||
nodeManager = nodeManager,
|
||||
radioConfigRepository = radioConfigRepository,
|
||||
tracerouteHandler = tracerouteHandler,
|
||||
neighborInfoHandler = neighborInfoHandler,
|
||||
sessionManager = sessionManager,
|
||||
scope = testScope,
|
||||
)
|
||||
testScope.testScheduler.advanceUntilIdle()
|
||||
everySuspend { packetHandler.sendToRadio(any<MeshPacket>()) } returns Unit
|
||||
|
||||
val pos = org.meshtastic.proto.Position(latitude_i = 10000000, longitude_i = 20000000)
|
||||
fixedSender.sendPosition(pos)
|
||||
|
||||
verify(mode = dev.mokkery.verify.VerifyMode.not) {
|
||||
nodeManager.handleReceivedPosition(any(), any(), any(), any())
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MY_NODE_NUM = 100
|
||||
private const val DEST_NODE = 200
|
||||
}
|
||||
}
|
||||
@@ -23,11 +23,11 @@ import dev.mokkery.mock
|
||||
import dev.mokkery.verify
|
||||
import org.meshtastic.core.repository.MeshConfigFlowManager
|
||||
import org.meshtastic.core.repository.MeshConfigHandler
|
||||
import org.meshtastic.core.repository.MeshRouter
|
||||
import org.meshtastic.core.repository.MqttManager
|
||||
import org.meshtastic.core.repository.NotificationManager
|
||||
import org.meshtastic.core.repository.PacketHandler
|
||||
import org.meshtastic.core.repository.ServiceRepository
|
||||
import org.meshtastic.core.repository.XModemManager
|
||||
import org.meshtastic.proto.Channel
|
||||
import org.meshtastic.proto.ClientNotification
|
||||
import org.meshtastic.proto.Config
|
||||
@@ -49,19 +49,18 @@ class FromRadioPacketHandlerImplTest {
|
||||
private val notificationManager: NotificationManager = mock(MockMode.autofill)
|
||||
private val configFlowManager: MeshConfigFlowManager = mock(MockMode.autofill)
|
||||
private val configHandler: MeshConfigHandler = mock(MockMode.autofill)
|
||||
private val router: MeshRouter = mock(MockMode.autofill)
|
||||
private val xmodemManager: XModemManager = mock(MockMode.autofill)
|
||||
|
||||
private lateinit var handler: FromRadioPacketHandlerImpl
|
||||
|
||||
@BeforeTest
|
||||
fun setup() {
|
||||
every { router.configFlowManager } returns configFlowManager
|
||||
every { router.configHandler } returns configHandler
|
||||
|
||||
handler =
|
||||
FromRadioPacketHandlerImpl(
|
||||
serviceRepository,
|
||||
lazy { router },
|
||||
lazy { configFlowManager },
|
||||
lazy { configHandler },
|
||||
lazy { xmodemManager },
|
||||
mqttManager,
|
||||
packetHandler,
|
||||
notificationManager,
|
||||
|
||||
@@ -1,587 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.data.manager
|
||||
|
||||
import dev.mokkery.MockMode
|
||||
import dev.mokkery.answering.returns
|
||||
import dev.mokkery.every
|
||||
import dev.mokkery.everySuspend
|
||||
import dev.mokkery.matcher.any
|
||||
import dev.mokkery.mock
|
||||
import dev.mokkery.verify
|
||||
import dev.mokkery.verify.VerifyMode.Companion.not
|
||||
import dev.mokkery.verifySuspend
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.meshtastic.core.common.database.DatabaseManager
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.MeshUser
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.model.Position
|
||||
import org.meshtastic.core.model.service.ServiceAction
|
||||
import org.meshtastic.core.repository.CommandSender
|
||||
import org.meshtastic.core.repository.MeshDataHandler
|
||||
import org.meshtastic.core.repository.MeshMessageProcessor
|
||||
import org.meshtastic.core.repository.MeshPrefs
|
||||
import org.meshtastic.core.repository.NodeManager
|
||||
import org.meshtastic.core.repository.NotificationManager
|
||||
import org.meshtastic.core.repository.PacketRepository
|
||||
import org.meshtastic.core.repository.PlatformAnalytics
|
||||
import org.meshtastic.core.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.repository.ServiceBroadcasts
|
||||
import org.meshtastic.core.repository.UiPrefs
|
||||
import org.meshtastic.proto.AdminMessage
|
||||
import org.meshtastic.proto.Channel
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.HardwareModel
|
||||
import org.meshtastic.proto.ModuleConfig
|
||||
import org.meshtastic.proto.SharedContact
|
||||
import org.meshtastic.proto.User
|
||||
import kotlin.test.BeforeTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class MeshActionHandlerImplTest {
|
||||
|
||||
private val nodeManager = mock<NodeManager>(MockMode.autofill)
|
||||
private val commandSender = mock<CommandSender>(MockMode.autofill)
|
||||
private val packetRepository = mock<PacketRepository>(MockMode.autofill)
|
||||
private val serviceBroadcasts = mock<ServiceBroadcasts>(MockMode.autofill)
|
||||
private val dataHandler = mock<MeshDataHandler>(MockMode.autofill)
|
||||
private val analytics = mock<PlatformAnalytics>(MockMode.autofill)
|
||||
private val meshPrefs = mock<MeshPrefs>(MockMode.autofill)
|
||||
private val uiPrefs = mock<UiPrefs>(MockMode.autofill)
|
||||
private val databaseManager = mock<DatabaseManager>(MockMode.autofill)
|
||||
private val notificationManager = mock<NotificationManager>(MockMode.autofill)
|
||||
private val messageProcessor = mock<MeshMessageProcessor>(MockMode.autofill)
|
||||
private val radioConfigRepository = mock<RadioConfigRepository>(MockMode.autofill)
|
||||
|
||||
private val myNodeNumFlow = MutableStateFlow<Int?>(MY_NODE_NUM)
|
||||
|
||||
private lateinit var handler: MeshActionHandlerImpl
|
||||
|
||||
private val testDispatcher = UnconfinedTestDispatcher()
|
||||
private val testScope = TestScope(testDispatcher)
|
||||
|
||||
companion object {
|
||||
private const val MY_NODE_NUM = 12345
|
||||
private const val REMOTE_NODE_NUM = 67890
|
||||
}
|
||||
|
||||
@BeforeTest
|
||||
fun setUp() {
|
||||
every { nodeManager.myNodeNum } returns myNodeNumFlow
|
||||
every { nodeManager.getMyId() } returns "!12345678"
|
||||
every { nodeManager.nodeDBbyNodeNum } returns emptyMap()
|
||||
}
|
||||
|
||||
private fun createHandler(scope: CoroutineScope): MeshActionHandlerImpl = MeshActionHandlerImpl(
|
||||
nodeManager = nodeManager,
|
||||
commandSender = commandSender,
|
||||
packetRepository = lazy { packetRepository },
|
||||
serviceBroadcasts = serviceBroadcasts,
|
||||
dataHandler = lazy { dataHandler },
|
||||
analytics = analytics,
|
||||
meshPrefs = meshPrefs,
|
||||
uiPrefs = uiPrefs,
|
||||
databaseManager = databaseManager,
|
||||
notificationManager = notificationManager,
|
||||
messageProcessor = lazy { messageProcessor },
|
||||
radioConfigRepository = radioConfigRepository,
|
||||
scope = scope,
|
||||
)
|
||||
|
||||
// ---- handleUpdateLastAddress (device-switch path — P0 critical) ----
|
||||
|
||||
@Test
|
||||
fun handleUpdateLastAddress_differentAddress_switchesDatabaseAndClearsState() = runTest(testDispatcher) {
|
||||
handler = createHandler(backgroundScope)
|
||||
|
||||
every { meshPrefs.deviceAddress } returns MutableStateFlow("old_addr")
|
||||
everySuspend { databaseManager.switchActiveDatabase(any()) } returns Unit
|
||||
|
||||
handler.handleUpdateLastAddress("new_addr")
|
||||
advanceUntilIdle()
|
||||
|
||||
verify { meshPrefs.setDeviceAddress("new_addr") }
|
||||
verify { nodeManager.clear() }
|
||||
verifySuspend { messageProcessor.clearEarlyPackets() }
|
||||
verifySuspend { databaseManager.switchActiveDatabase("new_addr") }
|
||||
verify { notificationManager.cancelAll() }
|
||||
verify { nodeManager.loadCachedNodeDB() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleUpdateLastAddress_sameAddress_noOp() = runTest(testDispatcher) {
|
||||
handler = createHandler(backgroundScope)
|
||||
|
||||
every { meshPrefs.deviceAddress } returns MutableStateFlow("same_addr")
|
||||
|
||||
handler.handleUpdateLastAddress("same_addr")
|
||||
advanceUntilIdle()
|
||||
|
||||
verify(not) { meshPrefs.setDeviceAddress(any()) }
|
||||
verify(not) { nodeManager.clear() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleUpdateLastAddress_nullAddress_switchesIfDifferent() = runTest(testDispatcher) {
|
||||
handler = createHandler(backgroundScope)
|
||||
|
||||
every { meshPrefs.deviceAddress } returns MutableStateFlow("old_addr")
|
||||
everySuspend { databaseManager.switchActiveDatabase(any()) } returns Unit
|
||||
|
||||
handler.handleUpdateLastAddress(null)
|
||||
advanceUntilIdle()
|
||||
|
||||
verify { meshPrefs.setDeviceAddress(null) }
|
||||
verify { nodeManager.clear() }
|
||||
verifySuspend { databaseManager.switchActiveDatabase(null) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleUpdateLastAddress_nullToNull_noOp() = runTest(testDispatcher) {
|
||||
handler = createHandler(backgroundScope)
|
||||
|
||||
every { meshPrefs.deviceAddress } returns MutableStateFlow(null)
|
||||
|
||||
handler.handleUpdateLastAddress(null)
|
||||
advanceUntilIdle()
|
||||
|
||||
verify(not) { meshPrefs.setDeviceAddress(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleUpdateLastAddress_executesStepsInOrder() = runTest(testDispatcher) {
|
||||
handler = createHandler(backgroundScope)
|
||||
|
||||
every { meshPrefs.deviceAddress } returns MutableStateFlow("old")
|
||||
everySuspend { databaseManager.switchActiveDatabase(any()) } returns Unit
|
||||
|
||||
handler.handleUpdateLastAddress("new")
|
||||
advanceUntilIdle()
|
||||
|
||||
// Verify critical sequence: clear -> switchDB -> cancelNotifications -> loadCachedNodeDB
|
||||
verify { nodeManager.clear() }
|
||||
verifySuspend { databaseManager.switchActiveDatabase("new") }
|
||||
verify { notificationManager.cancelAll() }
|
||||
verify { nodeManager.loadCachedNodeDB() }
|
||||
}
|
||||
|
||||
// ---- onServiceAction: null myNodeNum early-return ----
|
||||
|
||||
@Test
|
||||
fun onServiceAction_nullMyNodeNum_doesNothing() = runTest(testDispatcher) {
|
||||
handler = createHandler(backgroundScope)
|
||||
myNodeNumFlow.value = null
|
||||
|
||||
val node = createTestNode(REMOTE_NODE_NUM)
|
||||
handler.onServiceAction(ServiceAction.Favorite(node))
|
||||
advanceUntilIdle()
|
||||
|
||||
verify(not) { commandSender.sendAdmin(any(), any(), any(), any()) }
|
||||
}
|
||||
|
||||
// ---- onServiceAction: Favorite ----
|
||||
|
||||
@Test
|
||||
fun onServiceAction_favorite_sendsSetFavoriteWhenNotFavorite() = runTest(testDispatcher) {
|
||||
handler = createHandler(backgroundScope)
|
||||
val node = createTestNode(REMOTE_NODE_NUM, isFavorite = false)
|
||||
|
||||
handler.onServiceAction(ServiceAction.Favorite(node))
|
||||
advanceUntilIdle()
|
||||
|
||||
verify { commandSender.sendAdmin(any(), any(), any(), any()) }
|
||||
verify { nodeManager.updateNode(any(), any(), any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onServiceAction_favorite_sendsRemoveFavoriteWhenAlreadyFavorite() = runTest(testDispatcher) {
|
||||
handler = createHandler(backgroundScope)
|
||||
val node = createTestNode(REMOTE_NODE_NUM, isFavorite = true)
|
||||
|
||||
handler.onServiceAction(ServiceAction.Favorite(node))
|
||||
advanceUntilIdle()
|
||||
|
||||
verify { commandSender.sendAdmin(any(), any(), any(), any()) }
|
||||
verify { nodeManager.updateNode(any(), any(), any(), any()) }
|
||||
}
|
||||
|
||||
// ---- onServiceAction: Ignore ----
|
||||
|
||||
@Test
|
||||
fun onServiceAction_ignore_togglesAndUpdatesFilteredBySender() = runTest(testDispatcher) {
|
||||
handler = createHandler(backgroundScope)
|
||||
val node = createTestNode(REMOTE_NODE_NUM, isIgnored = false)
|
||||
|
||||
handler.onServiceAction(ServiceAction.Ignore(node))
|
||||
advanceUntilIdle()
|
||||
|
||||
verify { commandSender.sendAdmin(any(), any(), any(), any()) }
|
||||
verify { nodeManager.updateNode(any(), any(), any(), any()) }
|
||||
verifySuspend { packetRepository.updateFilteredBySender(any(), any()) }
|
||||
}
|
||||
|
||||
// ---- onServiceAction: Mute ----
|
||||
|
||||
@Test
|
||||
fun onServiceAction_mute_togglesMutedState() = runTest(testDispatcher) {
|
||||
handler = createHandler(backgroundScope)
|
||||
val node = createTestNode(REMOTE_NODE_NUM, isMuted = false)
|
||||
|
||||
handler.onServiceAction(ServiceAction.Mute(node))
|
||||
advanceUntilIdle()
|
||||
|
||||
verify { commandSender.sendAdmin(any(), any(), any(), any()) }
|
||||
verify { nodeManager.updateNode(any(), any(), any(), any()) }
|
||||
}
|
||||
|
||||
// ---- onServiceAction: GetDeviceMetadata ----
|
||||
|
||||
@Test
|
||||
fun onServiceAction_getDeviceMetadata_sendsAdminRequest() = runTest(testDispatcher) {
|
||||
handler = createHandler(backgroundScope)
|
||||
|
||||
handler.onServiceAction(ServiceAction.GetDeviceMetadata(REMOTE_NODE_NUM))
|
||||
advanceUntilIdle()
|
||||
|
||||
verify { commandSender.sendAdmin(any(), any(), any(), any()) }
|
||||
}
|
||||
|
||||
// ---- onServiceAction: SendContact ----
|
||||
|
||||
@Test
|
||||
fun onServiceAction_sendContact_completesWithTrueOnSuccess() = runTest(testDispatcher) {
|
||||
handler = createHandler(backgroundScope)
|
||||
everySuspend { commandSender.sendAdminAwait(any(), any(), any(), any()) } returns true
|
||||
|
||||
val action = ServiceAction.SendContact(SharedContact())
|
||||
handler.onServiceAction(action)
|
||||
advanceUntilIdle()
|
||||
|
||||
assertTrue(action.result.isCompleted)
|
||||
assertTrue(action.result.await())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onServiceAction_sendContact_completesWithFalseOnFailure() = runTest(testDispatcher) {
|
||||
handler = createHandler(backgroundScope)
|
||||
everySuspend { commandSender.sendAdminAwait(any(), any(), any(), any()) } returns false
|
||||
|
||||
val action = ServiceAction.SendContact(SharedContact())
|
||||
handler.onServiceAction(action)
|
||||
advanceUntilIdle()
|
||||
|
||||
assertTrue(action.result.isCompleted)
|
||||
assertFalse(action.result.await())
|
||||
}
|
||||
|
||||
// ---- onServiceAction: ImportContact ----
|
||||
|
||||
@Test
|
||||
fun onServiceAction_importContact_sendsAdminAndUpdatesNode() = runTest(testDispatcher) {
|
||||
handler = createHandler(backgroundScope)
|
||||
|
||||
val contact =
|
||||
SharedContact(node_num = REMOTE_NODE_NUM, user = User(id = "!abcdef12", long_name = "TestUser"))
|
||||
handler.onServiceAction(ServiceAction.ImportContact(contact))
|
||||
advanceUntilIdle()
|
||||
|
||||
verify { commandSender.sendAdmin(any(), any(), any(), any()) }
|
||||
verify { nodeManager.handleReceivedUser(any(), any(), any(), any()) }
|
||||
}
|
||||
|
||||
// ---- handleSetOwner ----
|
||||
|
||||
@Test
|
||||
fun handleSetOwner_sendsAdminAndUpdatesLocalNode() {
|
||||
handler = createHandler(testScope)
|
||||
val meshUser =
|
||||
MeshUser(
|
||||
id = "!12345678",
|
||||
longName = "Test Long",
|
||||
shortName = "TL",
|
||||
hwModel = HardwareModel.UNSET,
|
||||
isLicensed = false,
|
||||
)
|
||||
|
||||
handler.handleSetOwner(meshUser, MY_NODE_NUM)
|
||||
|
||||
verify { commandSender.sendAdmin(any(), any(), any(), any()) }
|
||||
verify { nodeManager.handleReceivedUser(any(), any(), any(), any()) }
|
||||
}
|
||||
|
||||
// ---- handleSend ----
|
||||
|
||||
@Test
|
||||
fun handleSend_sendsDataAndBroadcastsStatus() {
|
||||
handler = createHandler(testScope)
|
||||
val packet = DataPacket(to = "!deadbeef", dataType = 1, bytes = null, channel = 0)
|
||||
|
||||
handler.handleSend(packet, MY_NODE_NUM)
|
||||
|
||||
verify { commandSender.sendData(any()) }
|
||||
verify { serviceBroadcasts.broadcastMessageStatus(any(), any()) }
|
||||
verify { dataHandler.rememberDataPacket(any(), any(), any()) }
|
||||
}
|
||||
|
||||
// ---- handleRequestPosition: 3 branches ----
|
||||
|
||||
@Test
|
||||
fun handleRequestPosition_sameNode_doesNothing() {
|
||||
handler = createHandler(testScope)
|
||||
|
||||
handler.handleRequestPosition(MY_NODE_NUM, Position(0.0, 0.0, 0), MY_NODE_NUM)
|
||||
|
||||
verify(not) { commandSender.requestPosition(any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleRequestPosition_provideLocation_validPosition_usesGivenPosition() {
|
||||
handler = createHandler(testScope)
|
||||
every { uiPrefs.shouldProvideNodeLocation(MY_NODE_NUM) } returns MutableStateFlow(true)
|
||||
|
||||
val validPosition = Position(37.7749, -122.4194, 10)
|
||||
handler.handleRequestPosition(REMOTE_NODE_NUM, validPosition, MY_NODE_NUM)
|
||||
|
||||
verify { commandSender.requestPosition(REMOTE_NODE_NUM, validPosition) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleRequestPosition_provideLocation_invalidPosition_fallsBackToNodeDB() {
|
||||
handler = createHandler(testScope)
|
||||
every { uiPrefs.shouldProvideNodeLocation(MY_NODE_NUM) } returns MutableStateFlow(true)
|
||||
every { nodeManager.nodeDBbyNodeNum } returns emptyMap()
|
||||
|
||||
val invalidPosition = Position(0.0, 0.0, 0)
|
||||
handler.handleRequestPosition(REMOTE_NODE_NUM, invalidPosition, MY_NODE_NUM)
|
||||
|
||||
// Falls back to Position(0.0, 0.0, 0) when node has no position in DB
|
||||
verify { commandSender.requestPosition(any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleRequestPosition_doNotProvide_sendsZeroPosition() {
|
||||
handler = createHandler(testScope)
|
||||
every { uiPrefs.shouldProvideNodeLocation(MY_NODE_NUM) } returns MutableStateFlow(false)
|
||||
|
||||
val validPosition = Position(37.7749, -122.4194, 10)
|
||||
handler.handleRequestPosition(REMOTE_NODE_NUM, validPosition, MY_NODE_NUM)
|
||||
|
||||
// Should send zero position regardless of valid input
|
||||
verify { commandSender.requestPosition(any(), any()) }
|
||||
}
|
||||
|
||||
// ---- handleSetConfig: optimistic persist ----
|
||||
|
||||
@Test
|
||||
fun handleSetConfig_decodesAndSendsAdmin_thenPersistsLocally() = runTest(testDispatcher) {
|
||||
handler = createHandler(backgroundScope)
|
||||
everySuspend { radioConfigRepository.setLocalConfig(any()) } returns Unit
|
||||
|
||||
val config = Config(lora = Config.LoRaConfig(hop_limit = 5))
|
||||
val payload = config.encode()
|
||||
|
||||
handler.handleSetConfig(payload, MY_NODE_NUM)
|
||||
advanceUntilIdle()
|
||||
|
||||
verify { commandSender.sendAdmin(any(), any(), any(), any()) }
|
||||
verifySuspend { radioConfigRepository.setLocalConfig(any()) }
|
||||
}
|
||||
|
||||
// ---- handleSetModuleConfig: conditional persist ----
|
||||
|
||||
@Test
|
||||
fun handleSetModuleConfig_ownNode_persistsLocally() = runTest(testDispatcher) {
|
||||
handler = createHandler(backgroundScope)
|
||||
myNodeNumFlow.value = MY_NODE_NUM
|
||||
everySuspend { radioConfigRepository.setLocalModuleConfig(any()) } returns Unit
|
||||
|
||||
val moduleConfig = ModuleConfig(mqtt = ModuleConfig.MQTTConfig(enabled = true))
|
||||
val payload = moduleConfig.encode()
|
||||
|
||||
handler.handleSetModuleConfig(0, MY_NODE_NUM, payload)
|
||||
advanceUntilIdle()
|
||||
|
||||
verify { commandSender.sendAdmin(any(), any(), any(), any()) }
|
||||
verifySuspend { radioConfigRepository.setLocalModuleConfig(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleSetModuleConfig_remoteNode_doesNotPersistLocally() = runTest(testDispatcher) {
|
||||
handler = createHandler(backgroundScope)
|
||||
myNodeNumFlow.value = MY_NODE_NUM
|
||||
|
||||
val moduleConfig = ModuleConfig(mqtt = ModuleConfig.MQTTConfig(enabled = true))
|
||||
val payload = moduleConfig.encode()
|
||||
|
||||
handler.handleSetModuleConfig(0, REMOTE_NODE_NUM, payload)
|
||||
advanceUntilIdle()
|
||||
|
||||
verify { commandSender.sendAdmin(any(), any(), any(), any()) }
|
||||
verifySuspend(not) { radioConfigRepository.setLocalModuleConfig(any()) }
|
||||
}
|
||||
|
||||
// ---- handleSetChannel: null payload guard ----
|
||||
|
||||
@Test
|
||||
fun handleSetChannel_nonNullPayload_decodesAndPersists() = runTest(testDispatcher) {
|
||||
handler = createHandler(backgroundScope)
|
||||
everySuspend { radioConfigRepository.updateChannelSettings(any()) } returns Unit
|
||||
|
||||
val channel = Channel(index = 1)
|
||||
val payload = channel.encode()
|
||||
|
||||
handler.handleSetChannel(payload, MY_NODE_NUM)
|
||||
advanceUntilIdle()
|
||||
|
||||
verify { commandSender.sendAdmin(any(), any(), any(), any()) }
|
||||
verifySuspend { radioConfigRepository.updateChannelSettings(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleSetChannel_nullPayload_doesNothing() {
|
||||
handler = createHandler(testScope)
|
||||
|
||||
handler.handleSetChannel(null, MY_NODE_NUM)
|
||||
|
||||
verify(not) { commandSender.sendAdmin(any(), any(), any(), any()) }
|
||||
}
|
||||
|
||||
// ---- handleRemoveByNodenum ----
|
||||
|
||||
@Test
|
||||
fun handleRemoveByNodenum_removesAndSendsAdmin() {
|
||||
handler = createHandler(testScope)
|
||||
|
||||
handler.handleRemoveByNodenum(REMOTE_NODE_NUM, 99, MY_NODE_NUM)
|
||||
|
||||
verify { nodeManager.removeByNodenum(REMOTE_NODE_NUM) }
|
||||
verify { commandSender.sendAdmin(any(), any(), any(), any()) }
|
||||
}
|
||||
|
||||
// ---- handleSetRemoteOwner ----
|
||||
|
||||
@Test
|
||||
fun handleSetRemoteOwner_decodesAndSendsAdmin() {
|
||||
handler = createHandler(testScope)
|
||||
|
||||
val user = User(id = "!remote01", long_name = "Remote", short_name = "RM")
|
||||
val payload = user.encode()
|
||||
|
||||
handler.handleSetRemoteOwner(1, REMOTE_NODE_NUM, payload)
|
||||
|
||||
verify { commandSender.sendAdmin(any(), any(), any(), any()) }
|
||||
verify { nodeManager.handleReceivedUser(any(), any(), any(), any()) }
|
||||
}
|
||||
|
||||
// ---- handleGetRemoteConfig: sessionkey vs regular ----
|
||||
|
||||
@Test
|
||||
fun handleGetRemoteConfig_sessionkeyConfig_sendsDeviceMetadataRequest() {
|
||||
handler = createHandler(testScope)
|
||||
|
||||
handler.handleGetRemoteConfig(1, REMOTE_NODE_NUM, AdminMessage.ConfigType.SESSIONKEY_CONFIG.value)
|
||||
|
||||
verify { commandSender.sendAdmin(any(), any(), any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleGetRemoteConfig_regularConfig_sendsConfigRequest() {
|
||||
handler = createHandler(testScope)
|
||||
|
||||
handler.handleGetRemoteConfig(1, REMOTE_NODE_NUM, AdminMessage.ConfigType.LORA_CONFIG.value)
|
||||
|
||||
verify { commandSender.sendAdmin(any(), any(), any(), any()) }
|
||||
}
|
||||
|
||||
// ---- handleSetRemoteChannel: null payload guard ----
|
||||
|
||||
@Test
|
||||
fun handleSetRemoteChannel_nullPayload_doesNothing() {
|
||||
handler = createHandler(testScope)
|
||||
|
||||
handler.handleSetRemoteChannel(1, REMOTE_NODE_NUM, null)
|
||||
|
||||
verify(not) { commandSender.sendAdmin(any(), any(), any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleSetRemoteChannel_nonNullPayload_decodesAndSendsAdmin() {
|
||||
handler = createHandler(testScope)
|
||||
|
||||
val channel = Channel(index = 2)
|
||||
val payload = channel.encode()
|
||||
|
||||
handler.handleSetRemoteChannel(1, REMOTE_NODE_NUM, payload)
|
||||
|
||||
verify { commandSender.sendAdmin(any(), any(), any(), any()) }
|
||||
}
|
||||
|
||||
// ---- handleRequestRebootOta: null hash ----
|
||||
|
||||
@Test
|
||||
fun handleRequestRebootOta_withNullHash_sendsAdmin() {
|
||||
handler = createHandler(testScope)
|
||||
|
||||
handler.handleRequestRebootOta(1, REMOTE_NODE_NUM, 0, null)
|
||||
|
||||
verify { commandSender.sendAdmin(any(), any(), any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleRequestRebootOta_withHash_sendsAdmin() {
|
||||
handler = createHandler(testScope)
|
||||
|
||||
val hash = byteArrayOf(0x01, 0x02, 0x03)
|
||||
handler.handleRequestRebootOta(1, REMOTE_NODE_NUM, 1, hash)
|
||||
|
||||
verify { commandSender.sendAdmin(any(), any(), any(), any()) }
|
||||
}
|
||||
|
||||
// ---- handleRequestNodedbReset ----
|
||||
|
||||
@Test
|
||||
fun handleRequestNodedbReset_sendsAdminWithPreserveFavorites() {
|
||||
handler = createHandler(testScope)
|
||||
|
||||
handler.handleRequestNodedbReset(1, REMOTE_NODE_NUM, preserveFavorites = true)
|
||||
|
||||
verify { commandSender.sendAdmin(any(), any(), any(), any()) }
|
||||
}
|
||||
|
||||
// ---- Helper ----
|
||||
|
||||
private fun createTestNode(
|
||||
num: Int,
|
||||
isFavorite: Boolean = false,
|
||||
isIgnored: Boolean = false,
|
||||
isMuted: Boolean = false,
|
||||
): Node = Node(
|
||||
num = num,
|
||||
user = User(id = "!${num.toString(16).padStart(8, '0')}", long_name = "Node $num", short_name = "N$num"),
|
||||
isFavorite = isFavorite,
|
||||
isIgnored = isIgnored,
|
||||
isMuted = isMuted,
|
||||
)
|
||||
}
|
||||
@@ -41,7 +41,6 @@ import org.meshtastic.core.repository.NotificationPrefs
|
||||
import org.meshtastic.core.repository.PacketHandler
|
||||
import org.meshtastic.core.repository.PlatformAnalytics
|
||||
import org.meshtastic.core.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.repository.ServiceBroadcasts
|
||||
import org.meshtastic.core.repository.ServiceRepository
|
||||
import org.meshtastic.proto.DeviceMetadata
|
||||
import org.meshtastic.proto.FileInfo
|
||||
@@ -61,7 +60,6 @@ class MeshConfigFlowManagerImplTest {
|
||||
private val nodeRepository = mock<NodeRepository>(MockMode.autofill)
|
||||
private val radioConfigRepository = mock<RadioConfigRepository>(MockMode.autofill)
|
||||
private val serviceRepository = mock<ServiceRepository>(MockMode.autofill)
|
||||
private val serviceBroadcasts = mock<ServiceBroadcasts>(MockMode.autofill)
|
||||
private val analytics = mock<PlatformAnalytics>(MockMode.autofill)
|
||||
private val commandSender = mock<CommandSender>(MockMode.autofill)
|
||||
private val packetHandler = mock<PacketHandler>(MockMode.autofill)
|
||||
@@ -100,8 +98,7 @@ class MeshConfigFlowManagerImplTest {
|
||||
connectionManager = lazy { connectionManager },
|
||||
nodeRepository = nodeRepository,
|
||||
radioConfigRepository = radioConfigRepository,
|
||||
serviceRepository = serviceRepository,
|
||||
serviceBroadcasts = serviceBroadcasts,
|
||||
serviceStateWriter = serviceRepository,
|
||||
analytics = analytics,
|
||||
commandSender = commandSender,
|
||||
heartbeatSender = DataLayerHeartbeatSender(packetHandler),
|
||||
@@ -306,11 +303,10 @@ class MeshConfigFlowManagerImplTest {
|
||||
manager.handleConfigComplete(HandshakeConstants.NODE_INFO_NONCE)
|
||||
advanceUntilIdle()
|
||||
|
||||
verify { nodeManager.installNodeInfo(any(), withBroadcast = false) }
|
||||
verify { nodeManager.installNodeInfo(any()) }
|
||||
verify { nodeManager.setNodeDbReady(true) }
|
||||
verify { nodeManager.setAllowNodeDbWrites(true) }
|
||||
verify { serviceBroadcasts.broadcastConnection() }
|
||||
verify { connectionManager.onNodeDbReady() }
|
||||
verifySuspend { connectionManager.onNodeDbReady() }
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -334,7 +330,7 @@ class MeshConfigFlowManagerImplTest {
|
||||
advanceUntilIdle()
|
||||
|
||||
verify { nodeManager.setNodeDbReady(true) }
|
||||
verify { connectionManager.onNodeDbReady() }
|
||||
verifySuspend { connectionManager.onNodeDbReady() }
|
||||
}
|
||||
|
||||
// ---------- Unknown config_complete_id ----------
|
||||
@@ -402,7 +398,7 @@ class MeshConfigFlowManagerImplTest {
|
||||
advanceUntilIdle()
|
||||
|
||||
verify { nodeManager.setNodeDbReady(true) }
|
||||
verify { connectionManager.onNodeDbReady() }
|
||||
verifySuspend { connectionManager.onNodeDbReady() }
|
||||
|
||||
// After complete, newNodeCount should be 0 (state is Complete)
|
||||
assertEquals(0, manager.newNodeCount)
|
||||
|
||||
@@ -65,7 +65,7 @@ class MeshConfigHandlerImplTest {
|
||||
|
||||
private fun createHandler(scope: CoroutineScope): MeshConfigHandlerImpl = MeshConfigHandlerImpl(
|
||||
radioConfigRepository = radioConfigRepository,
|
||||
serviceRepository = serviceRepository,
|
||||
serviceStateWriter = serviceRepository,
|
||||
nodeManager = nodeManager,
|
||||
scope = scope,
|
||||
)
|
||||
|
||||
@@ -24,6 +24,7 @@ import dev.mokkery.everySuspend
|
||||
import dev.mokkery.matcher.any
|
||||
import dev.mokkery.mock
|
||||
import dev.mokkery.verify
|
||||
import dev.mokkery.verifySuspend
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
@@ -39,7 +40,7 @@ import org.meshtastic.core.repository.AppWidgetUpdater
|
||||
import org.meshtastic.core.repository.CommandSender
|
||||
import org.meshtastic.core.repository.HistoryManager
|
||||
import org.meshtastic.core.repository.MeshLocationManager
|
||||
import org.meshtastic.core.repository.MeshServiceNotifications
|
||||
import org.meshtastic.core.repository.MeshNotificationManager
|
||||
import org.meshtastic.core.repository.MeshWorkerManager
|
||||
import org.meshtastic.core.repository.MqttManager
|
||||
import org.meshtastic.core.repository.NodeManager
|
||||
@@ -48,7 +49,6 @@ import org.meshtastic.core.repository.PacketRepository
|
||||
import org.meshtastic.core.repository.PlatformAnalytics
|
||||
import org.meshtastic.core.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.repository.RadioInterfaceService
|
||||
import org.meshtastic.core.repository.ServiceBroadcasts
|
||||
import org.meshtastic.core.repository.ServiceRepository
|
||||
import org.meshtastic.core.repository.SessionManager
|
||||
import org.meshtastic.core.repository.UiPrefs
|
||||
@@ -66,8 +66,8 @@ import kotlin.test.assertEquals
|
||||
class MeshConnectionManagerImplTest {
|
||||
private val radioInterfaceService = mock<RadioInterfaceService>(MockMode.autofill)
|
||||
private val serviceRepository = mock<ServiceRepository>(MockMode.autofill)
|
||||
private val serviceBroadcasts = mock<ServiceBroadcasts>(MockMode.autofill)
|
||||
private val serviceNotifications = mock<MeshServiceNotifications>(MockMode.autofill)
|
||||
|
||||
private val serviceNotifications = mock<MeshNotificationManager>(MockMode.autofill)
|
||||
private val uiPrefs = mock<UiPrefs>(MockMode.autofill)
|
||||
private val packetHandler = mock<PacketHandler>(MockMode.autofill)
|
||||
private val nodeRepository = FakeNodeRepository()
|
||||
@@ -105,7 +105,7 @@ class MeshConnectionManagerImplTest {
|
||||
connectionStateFlow.value = call.arg<ConnectionState>(0)
|
||||
}
|
||||
every { serviceNotifications.updateServiceStateNotification(any(), any()) } returns Unit
|
||||
every { commandSender.sendAdmin(any(), any(), any(), any()) } returns Unit
|
||||
everySuspend { commandSender.sendAdmin(any(), any(), any(), any()) } returns Unit
|
||||
every { packetHandler.stopPacketQueue() } returns Unit
|
||||
every { locationManager.stop() } returns Unit
|
||||
every { mqttManager.stop() } returns Unit
|
||||
@@ -116,7 +116,6 @@ class MeshConnectionManagerImplTest {
|
||||
private fun createManager(scope: CoroutineScope): MeshConnectionManagerImpl = MeshConnectionManagerImpl(
|
||||
radioInterfaceService,
|
||||
serviceRepository,
|
||||
serviceBroadcasts,
|
||||
serviceNotifications,
|
||||
uiPrefs,
|
||||
packetHandler,
|
||||
@@ -149,7 +148,6 @@ class MeshConnectionManagerImplTest {
|
||||
serviceRepository.connectionState.value,
|
||||
"State should be Connecting after radio Connected",
|
||||
)
|
||||
verify { serviceBroadcasts.broadcastConnection() }
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -290,10 +288,10 @@ class MeshConnectionManagerImplTest {
|
||||
store_forward = ModuleConfig.StoreForwardConfig(enabled = true),
|
||||
)
|
||||
moduleConfigFlow.value = moduleConfig
|
||||
every { commandSender.requestTelemetry(any(), any(), any()) } returns Unit
|
||||
everySuspend { commandSender.requestTelemetry(any(), any(), any()) } returns Unit
|
||||
every { nodeManager.myNodeNum } returns MutableStateFlow(123)
|
||||
every { mqttManager.startProxy(any(), any()) } returns Unit
|
||||
every { historyManager.requestHistoryReplay(any(), any(), any(), any()) } returns Unit
|
||||
everySuspend { historyManager.requestHistoryReplay(any(), any(), any(), any()) } returns Unit
|
||||
every { nodeManager.getMyNodeInfo() } returns null
|
||||
|
||||
manager = createManager(backgroundScope)
|
||||
@@ -301,7 +299,7 @@ class MeshConnectionManagerImplTest {
|
||||
advanceUntilIdle()
|
||||
|
||||
verify { mqttManager.startProxy(true, true) }
|
||||
verify { historyManager.requestHistoryReplay(any(), any(), any(), any()) }
|
||||
verifySuspend { historyManager.requestHistoryReplay(any(), any(), any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -34,9 +34,10 @@ import okio.ByteString.Companion.toByteString
|
||||
import org.meshtastic.core.model.ContactSettings
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.model.NodeAddress
|
||||
import org.meshtastic.core.model.util.MeshDataMapper
|
||||
import org.meshtastic.core.repository.AdminPacketHandler
|
||||
import org.meshtastic.core.repository.MeshServiceNotifications
|
||||
import org.meshtastic.core.repository.MeshNotificationManager
|
||||
import org.meshtastic.core.repository.MessageFilter
|
||||
import org.meshtastic.core.repository.NeighborInfoHandler
|
||||
import org.meshtastic.core.repository.NodeManager
|
||||
@@ -45,7 +46,6 @@ import org.meshtastic.core.repository.PacketHandler
|
||||
import org.meshtastic.core.repository.PacketRepository
|
||||
import org.meshtastic.core.repository.PlatformAnalytics
|
||||
import org.meshtastic.core.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.repository.ServiceBroadcasts
|
||||
import org.meshtastic.core.repository.ServiceRepository
|
||||
import org.meshtastic.core.repository.StoreForwardPacketHandler
|
||||
import org.meshtastic.core.repository.TelemetryPacketHandler
|
||||
@@ -72,9 +72,8 @@ class MeshDataHandlerTest {
|
||||
private val packetHandler: PacketHandler = mock(MockMode.autofill)
|
||||
private val serviceRepository: ServiceRepository = mock(MockMode.autofill)
|
||||
private val packetRepository: PacketRepository = mock(MockMode.autofill)
|
||||
private val serviceBroadcasts: ServiceBroadcasts = mock(MockMode.autofill)
|
||||
private val notificationManager: NotificationManager = mock(MockMode.autofill)
|
||||
private val serviceNotifications: MeshServiceNotifications = mock(MockMode.autofill)
|
||||
private val serviceNotifications: MeshNotificationManager = mock(MockMode.autofill)
|
||||
private val analytics: PlatformAnalytics = mock(MockMode.autofill)
|
||||
private val dataMapper: MeshDataMapper = mock(MockMode.autofill)
|
||||
private val tracerouteHandler: TracerouteHandler = mock(MockMode.autofill)
|
||||
@@ -94,9 +93,8 @@ class MeshDataHandlerTest {
|
||||
MeshDataHandlerImpl(
|
||||
nodeManager = nodeManager,
|
||||
packetHandler = packetHandler,
|
||||
serviceRepository = serviceRepository,
|
||||
serviceStateWriter = serviceRepository,
|
||||
packetRepository = lazy { packetRepository },
|
||||
serviceBroadcasts = serviceBroadcasts,
|
||||
notificationManager = notificationManager,
|
||||
serviceNotifications = serviceNotifications,
|
||||
analytics = analytics,
|
||||
@@ -114,7 +112,7 @@ class MeshDataHandlerTest {
|
||||
// Default: mapper returns null for empty packets, which is the safe default
|
||||
every { dataMapper.toDataPacket(any()) } returns null
|
||||
// Stub commonly accessed properties to avoid NPE from autofill
|
||||
every { nodeManager.nodeDBbyID } returns emptyMap()
|
||||
every { nodeManager.getNodeById(any()) } returns null
|
||||
every { nodeManager.nodeDBbyNodeNum } returns emptyMap()
|
||||
every { radioConfigRepository.channelSetFlow } returns MutableStateFlow(ChannelSet())
|
||||
}
|
||||
@@ -132,7 +130,6 @@ class MeshDataHandlerTest {
|
||||
handler.handleReceivedData(packet, 123)
|
||||
|
||||
// Should not broadcast if dataMapper returns null
|
||||
verify(mode = dev.mokkery.verify.VerifyMode.not) { serviceBroadcasts.broadcastReceivedData(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -146,8 +143,8 @@ class MeshDataHandlerTest {
|
||||
)
|
||||
val dataPacket =
|
||||
DataPacket(
|
||||
from = DataPacket.nodeNumToDefaultId(myNodeNum),
|
||||
to = DataPacket.ID_BROADCAST,
|
||||
from = NodeAddress.numToDefaultId(myNodeNum),
|
||||
to = NodeAddress.ID_BROADCAST,
|
||||
bytes = position.encode().toByteString(),
|
||||
dataType = PortNum.POSITION_APP.value,
|
||||
time = 1000L,
|
||||
@@ -156,8 +153,7 @@ class MeshDataHandlerTest {
|
||||
|
||||
handler.handleReceivedData(packet, myNodeNum)
|
||||
|
||||
// Position from local node: shouldBroadcast stays as !fromUs = false
|
||||
verify(mode = dev.mokkery.verify.VerifyMode.not) { serviceBroadcasts.broadcastReceivedData(any()) }
|
||||
// Position from local node — no further action expected
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -167,16 +163,14 @@ class MeshDataHandlerTest {
|
||||
val packet = MeshPacket(from = remoteNum, decoded = Data(portnum = PortNum.PRIVATE_APP))
|
||||
val dataPacket =
|
||||
DataPacket(
|
||||
from = DataPacket.nodeNumToDefaultId(remoteNum),
|
||||
to = DataPacket.ID_BROADCAST,
|
||||
from = NodeAddress.numToDefaultId(remoteNum),
|
||||
to = NodeAddress.ID_BROADCAST,
|
||||
bytes = null,
|
||||
dataType = PortNum.PRIVATE_APP.value,
|
||||
)
|
||||
every { dataMapper.toDataPacket(packet) } returns dataPacket
|
||||
|
||||
handler.handleReceivedData(packet, myNodeNum)
|
||||
|
||||
verify { serviceBroadcasts.broadcastReceivedData(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -185,7 +179,7 @@ class MeshDataHandlerTest {
|
||||
val dataPacket =
|
||||
DataPacket(
|
||||
from = "!other",
|
||||
to = DataPacket.ID_BROADCAST,
|
||||
to = NodeAddress.ID_BROADCAST,
|
||||
bytes = null,
|
||||
dataType = PortNum.PRIVATE_APP.value,
|
||||
)
|
||||
@@ -211,7 +205,7 @@ class MeshDataHandlerTest {
|
||||
val dataPacket =
|
||||
DataPacket(
|
||||
from = "!remote",
|
||||
to = DataPacket.ID_BROADCAST,
|
||||
to = NodeAddress.ID_BROADCAST,
|
||||
bytes = position.encode().toByteString(),
|
||||
dataType = PortNum.POSITION_APP.value,
|
||||
time = 1000L,
|
||||
@@ -238,7 +232,7 @@ class MeshDataHandlerTest {
|
||||
val dataPacket =
|
||||
DataPacket(
|
||||
from = "!remote",
|
||||
to = DataPacket.ID_BROADCAST,
|
||||
to = NodeAddress.ID_BROADCAST,
|
||||
bytes = user.encode().toByteString(),
|
||||
dataType = PortNum.NODEINFO_APP.value,
|
||||
)
|
||||
@@ -261,7 +255,7 @@ class MeshDataHandlerTest {
|
||||
val dataPacket =
|
||||
DataPacket(
|
||||
from = "!local",
|
||||
to = DataPacket.ID_BROADCAST,
|
||||
to = NodeAddress.ID_BROADCAST,
|
||||
bytes = user.encode().toByteString(),
|
||||
dataType = PortNum.NODEINFO_APP.value,
|
||||
)
|
||||
@@ -286,7 +280,7 @@ class MeshDataHandlerTest {
|
||||
val dataPacket =
|
||||
DataPacket(
|
||||
from = "!remote",
|
||||
to = DataPacket.ID_BROADCAST,
|
||||
to = NodeAddress.ID_BROADCAST,
|
||||
bytes = pax.encode().toByteString(),
|
||||
dataType = PortNum.PAXCOUNTER_APP.value,
|
||||
)
|
||||
@@ -318,7 +312,6 @@ class MeshDataHandlerTest {
|
||||
handler.handleReceivedData(packet, 123)
|
||||
|
||||
verify { tracerouteHandler.handleTraceroute(packet, any(), any()) }
|
||||
verify(mode = dev.mokkery.verify.VerifyMode.not) { serviceBroadcasts.broadcastReceivedData(any()) }
|
||||
}
|
||||
|
||||
// --- NeighborInfo handling ---
|
||||
@@ -334,7 +327,7 @@ class MeshDataHandlerTest {
|
||||
val dataPacket =
|
||||
DataPacket(
|
||||
from = "!remote",
|
||||
to = DataPacket.ID_BROADCAST,
|
||||
to = NodeAddress.ID_BROADCAST,
|
||||
bytes = ni.encode().toByteString(),
|
||||
dataType = PortNum.NEIGHBORINFO_APP.value,
|
||||
)
|
||||
@@ -343,7 +336,6 @@ class MeshDataHandlerTest {
|
||||
handler.handleReceivedData(packet, 123)
|
||||
|
||||
verify { neighborInfoHandler.handleNeighborInfo(packet) }
|
||||
verify { serviceBroadcasts.broadcastReceivedData(any()) }
|
||||
}
|
||||
|
||||
// --- Store-and-Forward handling ---
|
||||
@@ -358,7 +350,7 @@ class MeshDataHandlerTest {
|
||||
val dataPacket =
|
||||
DataPacket(
|
||||
from = "!remote",
|
||||
to = DataPacket.ID_BROADCAST,
|
||||
to = NodeAddress.ID_BROADCAST,
|
||||
bytes = byteArrayOf().toByteString(),
|
||||
dataType = PortNum.STORE_FORWARD_APP.value,
|
||||
)
|
||||
@@ -383,7 +375,7 @@ class MeshDataHandlerTest {
|
||||
val dataPacket =
|
||||
DataPacket(
|
||||
from = "!remote",
|
||||
to = DataPacket.ID_BROADCAST,
|
||||
to = NodeAddress.ID_BROADCAST,
|
||||
bytes = routing.encode().toByteString(),
|
||||
dataType = PortNum.ROUTING_APP.value,
|
||||
)
|
||||
@@ -407,7 +399,7 @@ class MeshDataHandlerTest {
|
||||
val dataPacket =
|
||||
DataPacket(
|
||||
from = "!remote",
|
||||
to = DataPacket.ID_BROADCAST,
|
||||
to = NodeAddress.ID_BROADCAST,
|
||||
bytes = routing.encode().toByteString(),
|
||||
dataType = PortNum.ROUTING_APP.value,
|
||||
)
|
||||
@@ -415,8 +407,6 @@ class MeshDataHandlerTest {
|
||||
every { nodeManager.toNodeID(456) } returns "!remote"
|
||||
|
||||
handler.handleReceivedData(packet, 123)
|
||||
|
||||
verify { serviceBroadcasts.broadcastReceivedData(any()) }
|
||||
}
|
||||
|
||||
// --- Telemetry handling ---
|
||||
@@ -436,7 +426,7 @@ class MeshDataHandlerTest {
|
||||
val dataPacket =
|
||||
DataPacket(
|
||||
from = "!remote",
|
||||
to = DataPacket.ID_BROADCAST,
|
||||
to = NodeAddress.ID_BROADCAST,
|
||||
bytes = telemetry.encode().toByteString(),
|
||||
dataType = PortNum.TELEMETRY_APP.value,
|
||||
time = 2000000L,
|
||||
@@ -464,7 +454,7 @@ class MeshDataHandlerTest {
|
||||
val dataPacket =
|
||||
DataPacket(
|
||||
from = "!local",
|
||||
to = DataPacket.ID_BROADCAST,
|
||||
to = NodeAddress.ID_BROADCAST,
|
||||
bytes = telemetry.encode().toByteString(),
|
||||
dataType = PortNum.TELEMETRY_APP.value,
|
||||
time = 2000000L,
|
||||
@@ -491,7 +481,7 @@ class MeshDataHandlerTest {
|
||||
DataPacket(
|
||||
id = 42,
|
||||
from = "!remote",
|
||||
to = DataPacket.ID_BROADCAST,
|
||||
to = NodeAddress.ID_BROADCAST,
|
||||
bytes = "hello".encodeToByteArray().toByteString(),
|
||||
dataType = PortNum.TEXT_MESSAGE_APP.value,
|
||||
)
|
||||
@@ -500,11 +490,8 @@ class MeshDataHandlerTest {
|
||||
everySuspend { packetRepository.getContactSettings(any()) } returns ContactSettings(contactKey = "test")
|
||||
every { messageFilter.shouldFilter(any(), any()) } returns false
|
||||
// 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")),
|
||||
)
|
||||
every { nodeManager.getNodeById("!remote") } returns
|
||||
Node(num = 456, user = User(id = "!remote", long_name = "Remote User", short_name = "RU"))
|
||||
|
||||
handler.handleReceivedData(packet, 123)
|
||||
advanceUntilIdle()
|
||||
@@ -525,7 +512,7 @@ class MeshDataHandlerTest {
|
||||
DataPacket(
|
||||
id = 42,
|
||||
from = "!remote",
|
||||
to = DataPacket.ID_BROADCAST,
|
||||
to = NodeAddress.ID_BROADCAST,
|
||||
bytes = "hello".encodeToByteArray().toByteString(),
|
||||
dataType = PortNum.TEXT_MESSAGE_APP.value,
|
||||
)
|
||||
@@ -598,7 +585,7 @@ class MeshDataHandlerTest {
|
||||
DataPacket(
|
||||
id = 55,
|
||||
from = "!remote",
|
||||
to = DataPacket.ID_BROADCAST,
|
||||
to = NodeAddress.ID_BROADCAST,
|
||||
bytes = "test".encodeToByteArray().toByteString(),
|
||||
dataType = PortNum.RANGE_TEST_APP.value,
|
||||
)
|
||||
@@ -606,11 +593,8 @@ class MeshDataHandlerTest {
|
||||
everySuspend { packetRepository.findPacketsWithId(55) } returns emptyList()
|
||||
everySuspend { packetRepository.getContactSettings(any()) } returns ContactSettings(contactKey = "test")
|
||||
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")),
|
||||
)
|
||||
every { nodeManager.getNodeById("!remote") } returns
|
||||
Node(num = 456, user = User(id = "!remote", long_name = "Remote User", short_name = "RU"))
|
||||
|
||||
handler.handleReceivedData(packet, 123)
|
||||
advanceUntilIdle()
|
||||
@@ -629,7 +613,7 @@ class MeshDataHandlerTest {
|
||||
val dataPacket =
|
||||
DataPacket(
|
||||
from = "!local",
|
||||
to = DataPacket.ID_BROADCAST,
|
||||
to = NodeAddress.ID_BROADCAST,
|
||||
bytes = admin.encode().toByteString(),
|
||||
dataType = PortNum.ADMIN_APP.value,
|
||||
)
|
||||
@@ -658,13 +642,13 @@ class MeshDataHandlerTest {
|
||||
DataPacket(
|
||||
id = 77,
|
||||
from = "!remote",
|
||||
to = DataPacket.ID_BROADCAST,
|
||||
to = NodeAddress.ID_BROADCAST,
|
||||
bytes = "spam content".encodeToByteArray().toByteString(),
|
||||
dataType = PortNum.TEXT_MESSAGE_APP.value,
|
||||
)
|
||||
every { dataMapper.toDataPacket(packet) } returns dataPacket
|
||||
everySuspend { packetRepository.findPacketsWithId(77) } returns emptyList()
|
||||
every { nodeManager.nodeDBbyID } returns emptyMap()
|
||||
every { nodeManager.getNodeById(any()) } returns null
|
||||
everySuspend { packetRepository.getContactSettings(any()) } returns ContactSettings(contactKey = "test")
|
||||
every { messageFilter.shouldFilter("spam content", false) } returns true
|
||||
|
||||
@@ -688,14 +672,14 @@ class MeshDataHandlerTest {
|
||||
DataPacket(
|
||||
id = 88,
|
||||
from = "!remote",
|
||||
to = DataPacket.ID_BROADCAST,
|
||||
to = NodeAddress.ID_BROADCAST,
|
||||
bytes = "hello".encodeToByteArray().toByteString(),
|
||||
dataType = PortNum.TEXT_MESSAGE_APP.value,
|
||||
)
|
||||
every { dataMapper.toDataPacket(packet) } returns dataPacket
|
||||
everySuspend { packetRepository.findPacketsWithId(88) } returns emptyList()
|
||||
every { nodeManager.nodeDBbyID } returns
|
||||
mapOf("!remote" to Node(num = 456, user = User(id = "!remote"), isIgnored = true))
|
||||
every { nodeManager.getNodeById("!remote") } returns
|
||||
Node(num = 456, user = User(id = "!remote"), isIgnored = true)
|
||||
everySuspend { packetRepository.getContactSettings(any()) } returns ContactSettings(contactKey = "test")
|
||||
|
||||
handler.handleReceivedData(packet, 123)
|
||||
|
||||
@@ -33,7 +33,6 @@ import okio.ByteString
|
||||
import org.meshtastic.core.repository.FromRadioPacketHandler
|
||||
import org.meshtastic.core.repository.MeshDataHandler
|
||||
import org.meshtastic.core.repository.MeshLogRepository
|
||||
import org.meshtastic.core.repository.MeshRouter
|
||||
import org.meshtastic.core.repository.NodeManager
|
||||
import org.meshtastic.core.repository.ServiceRepository
|
||||
import org.meshtastic.proto.Data
|
||||
@@ -50,7 +49,6 @@ class MeshMessageProcessorImplTest {
|
||||
private val nodeManager = mock<NodeManager>(MockMode.autofill)
|
||||
private val serviceRepository = mock<ServiceRepository>(MockMode.autofill)
|
||||
private val meshLogRepository = mock<MeshLogRepository>(MockMode.autofill)
|
||||
private val router = mock<MeshRouter>(MockMode.autofill)
|
||||
private val fromRadioDispatcher = mock<FromRadioPacketHandler>(MockMode.autofill)
|
||||
private val dataHandler = mock<MeshDataHandler>(MockMode.autofill)
|
||||
|
||||
@@ -65,14 +63,13 @@ class MeshMessageProcessorImplTest {
|
||||
fun setUp() {
|
||||
every { nodeManager.isNodeDbReady } returns isNodeDbReady
|
||||
every { nodeManager.myNodeNum } returns MutableStateFlow<Int?>(myNodeNum)
|
||||
every { router.dataHandler } returns dataHandler
|
||||
}
|
||||
|
||||
private fun createProcessor(scope: CoroutineScope): MeshMessageProcessorImpl = MeshMessageProcessorImpl(
|
||||
nodeManager = nodeManager,
|
||||
serviceRepository = serviceRepository,
|
||||
serviceStateWriter = serviceRepository,
|
||||
meshLogRepository = lazy { meshLogRepository },
|
||||
router = lazy { router },
|
||||
dataHandler = lazy { dataHandler },
|
||||
fromRadioDispatcher = fromRadioDispatcher,
|
||||
scope = scope,
|
||||
)
|
||||
@@ -251,7 +248,7 @@ class MeshMessageProcessorImplTest {
|
||||
advanceUntilIdle()
|
||||
|
||||
// Should have called updateNode for myNodeNum (lastHeard update)
|
||||
verify { nodeManager.updateNode(myNodeNum, withBroadcast = true, any(), any()) }
|
||||
verify { nodeManager.updateNode(myNodeNum, any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -273,7 +270,7 @@ class MeshMessageProcessorImplTest {
|
||||
advanceUntilIdle()
|
||||
|
||||
// Should have called updateNode for the sender
|
||||
verify { nodeManager.updateNode(senderNode, withBroadcast = false, any(), any()) }
|
||||
verify { nodeManager.updateNode(senderNode, any(), any()) }
|
||||
}
|
||||
|
||||
// ---------- handleReceivedMeshPacket: null decoded ----------
|
||||
|
||||
@@ -1,189 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.data.manager
|
||||
|
||||
import dev.mokkery.MockMode
|
||||
import dev.mokkery.mock
|
||||
import dev.mokkery.verify
|
||||
import dev.mokkery.verifySuspend
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.model.Position
|
||||
import org.meshtastic.core.model.service.ServiceAction
|
||||
import org.meshtastic.core.repository.MeshActionHandler
|
||||
import org.meshtastic.core.repository.MeshConfigFlowManager
|
||||
import org.meshtastic.core.repository.MeshConfigHandler
|
||||
import org.meshtastic.core.repository.MeshDataHandler
|
||||
import org.meshtastic.core.repository.MqttManager
|
||||
import org.meshtastic.core.repository.NeighborInfoHandler
|
||||
import org.meshtastic.core.repository.TracerouteHandler
|
||||
import org.meshtastic.core.repository.XModemManager
|
||||
import org.meshtastic.proto.LocalConfig
|
||||
import org.meshtastic.proto.LocalModuleConfig
|
||||
import kotlin.test.BeforeTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class MeshRouterImplTest {
|
||||
private val dataHandler = mock<MeshDataHandler>(MockMode.autofill)
|
||||
private val tracerouteHandler = mock<TracerouteHandler>(MockMode.autofill)
|
||||
private val neighborInfoHandler = mock<NeighborInfoHandler>(MockMode.autofill)
|
||||
private val configFlowManager = mock<MeshConfigFlowManager>(MockMode.autofill)
|
||||
private val mqttManager = mock<MqttManager>(MockMode.autofill)
|
||||
private val actionHandler = mock<MeshActionHandler>(MockMode.autofill)
|
||||
private val xmodemManager = mock<XModemManager>(MockMode.autofill)
|
||||
|
||||
private val configHandler =
|
||||
object : MeshConfigHandler {
|
||||
override val localConfig = MutableStateFlow(LocalConfig())
|
||||
override val moduleConfig = MutableStateFlow(LocalModuleConfig())
|
||||
|
||||
override fun handleDeviceConfig(config: org.meshtastic.proto.Config) = Unit
|
||||
|
||||
override fun handleModuleConfig(config: org.meshtastic.proto.ModuleConfig) = Unit
|
||||
|
||||
override fun handleChannel(channel: org.meshtastic.proto.Channel) = Unit
|
||||
|
||||
override fun handleDeviceUIConfig(config: org.meshtastic.proto.DeviceUIConfig) = Unit
|
||||
}
|
||||
|
||||
private lateinit var dataHandlerLazy: TrackingLazy<MeshDataHandler>
|
||||
private lateinit var configHandlerLazy: TrackingLazy<MeshConfigHandler>
|
||||
private lateinit var tracerouteHandlerLazy: TrackingLazy<TracerouteHandler>
|
||||
private lateinit var neighborInfoHandlerLazy: TrackingLazy<NeighborInfoHandler>
|
||||
private lateinit var configFlowManagerLazy: TrackingLazy<MeshConfigFlowManager>
|
||||
private lateinit var mqttManagerLazy: TrackingLazy<MqttManager>
|
||||
private lateinit var actionHandlerLazy: TrackingLazy<MeshActionHandler>
|
||||
private lateinit var xmodemManagerLazy: TrackingLazy<XModemManager>
|
||||
|
||||
private lateinit var router: MeshRouterImpl
|
||||
|
||||
@BeforeTest
|
||||
fun setUp() {
|
||||
dataHandlerLazy = TrackingLazy { dataHandler }
|
||||
configHandlerLazy = TrackingLazy { configHandler }
|
||||
tracerouteHandlerLazy = TrackingLazy { tracerouteHandler }
|
||||
neighborInfoHandlerLazy = TrackingLazy { neighborInfoHandler }
|
||||
configFlowManagerLazy = TrackingLazy { configFlowManager }
|
||||
mqttManagerLazy = TrackingLazy { mqttManager }
|
||||
actionHandlerLazy = TrackingLazy { actionHandler }
|
||||
xmodemManagerLazy = TrackingLazy { xmodemManager }
|
||||
|
||||
router =
|
||||
MeshRouterImpl(
|
||||
dataHandlerLazy = dataHandlerLazy,
|
||||
configHandlerLazy = configHandlerLazy,
|
||||
tracerouteHandlerLazy = tracerouteHandlerLazy,
|
||||
neighborInfoHandlerLazy = neighborInfoHandlerLazy,
|
||||
configFlowManagerLazy = configFlowManagerLazy,
|
||||
mqttManagerLazy = mqttManagerLazy,
|
||||
actionHandlerLazy = actionHandlerLazy,
|
||||
xmodemManagerLazy = xmodemManagerLazy,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `send message routing uses the action handler lazily`() {
|
||||
val packet = DataPacket(to = "!deadbeef", dataType = 1, bytes = null, channel = 0)
|
||||
|
||||
assertAllHandlersUninitialized()
|
||||
|
||||
router.actionHandler.handleSend(packet, 12345)
|
||||
|
||||
assertTrue(actionHandlerLazy.isInitialized())
|
||||
assertFalse(dataHandlerLazy.isInitialized())
|
||||
assertFalse(tracerouteHandlerLazy.isInitialized())
|
||||
verify { actionHandler.handleSend(packet, 12345) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `request position routing uses the action handler lazily`() {
|
||||
val position = Position(latitude = 37.7749, longitude = -122.4194, altitude = 10)
|
||||
|
||||
router.actionHandler.handleRequestPosition(destNum = 67890, position = position, myNodeNum = 12345)
|
||||
|
||||
assertTrue(actionHandlerLazy.isInitialized())
|
||||
assertFalse(tracerouteHandlerLazy.isInitialized())
|
||||
verify { actionHandler.handleRequestPosition(67890, position, 12345) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `traceroute routing uses the traceroute handler lazily`() {
|
||||
assertAllHandlersUninitialized()
|
||||
|
||||
router.tracerouteHandler.recordStartTime(77)
|
||||
|
||||
assertTrue(tracerouteHandlerLazy.isInitialized())
|
||||
assertFalse(actionHandlerLazy.isInitialized())
|
||||
verify { tracerouteHandler.recordStartTime(77) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `admin command routing uses the action handler lazily`() {
|
||||
assertAllHandlersUninitialized()
|
||||
|
||||
router.actionHandler.handleGetRemoteConfig(id = 42, destNum = 67890, config = 7)
|
||||
|
||||
assertTrue(actionHandlerLazy.isInitialized())
|
||||
assertFalse(configHandlerLazy.isInitialized())
|
||||
verify { actionHandler.handleGetRemoteConfig(42, 67890, 7) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `service actions are passed through unchanged to the action handler`() = runTest {
|
||||
val action = ServiceAction.Favorite(Node(num = 67890))
|
||||
|
||||
router.actionHandler.onServiceAction(action)
|
||||
|
||||
assertTrue(actionHandlerLazy.isInitialized())
|
||||
assertFalse(dataHandlerLazy.isInitialized())
|
||||
assertFalse(tracerouteHandlerLazy.isInitialized())
|
||||
verifySuspend { actionHandler.onServiceAction(action) }
|
||||
}
|
||||
|
||||
private fun assertAllHandlersUninitialized() {
|
||||
assertFalse(dataHandlerLazy.isInitialized())
|
||||
assertFalse(configHandlerLazy.isInitialized())
|
||||
assertFalse(tracerouteHandlerLazy.isInitialized())
|
||||
assertFalse(neighborInfoHandlerLazy.isInitialized())
|
||||
assertFalse(configFlowManagerLazy.isInitialized())
|
||||
assertFalse(mqttManagerLazy.isInitialized())
|
||||
assertFalse(actionHandlerLazy.isInitialized())
|
||||
assertFalse(xmodemManagerLazy.isInitialized())
|
||||
}
|
||||
|
||||
private class TrackingLazy<T>(private val initializer: () -> T) : Lazy<T> {
|
||||
private var cached: Any? = Uninitialized
|
||||
|
||||
override val value: T
|
||||
get() {
|
||||
if (cached === Uninitialized) {
|
||||
cached = initializer()
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return cached as T
|
||||
}
|
||||
|
||||
override fun isInitialized(): Boolean = cached !== Uninitialized
|
||||
|
||||
private object Uninitialized
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.data.manager
|
||||
|
||||
import dev.mokkery.MockMode
|
||||
import dev.mokkery.answering.returns
|
||||
import dev.mokkery.every
|
||||
import dev.mokkery.matcher.any
|
||||
import dev.mokkery.mock
|
||||
import dev.mokkery.verify
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.meshtastic.core.repository.NodeManager
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
import org.meshtastic.core.repository.ServiceRepository
|
||||
import org.meshtastic.proto.Data
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.Neighbor
|
||||
import org.meshtastic.proto.NeighborInfo
|
||||
import org.meshtastic.proto.User
|
||||
import kotlin.test.BeforeTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class NeighborInfoHandlerImplTest {
|
||||
|
||||
private val nodeManager = mock<NodeManager>(MockMode.autofill)
|
||||
private val serviceRepository = mock<ServiceRepository>(MockMode.autofill)
|
||||
private val nodeRepository = mock<NodeRepository>(MockMode.autofill)
|
||||
|
||||
private lateinit var handler: NeighborInfoHandlerImpl
|
||||
|
||||
private val myNodeNum = 12345
|
||||
|
||||
@BeforeTest
|
||||
fun setUp() {
|
||||
every { nodeManager.myNodeNum } returns MutableStateFlow<Int?>(myNodeNum)
|
||||
handler = NeighborInfoHandlerImpl(nodeManager, serviceRepository, nodeRepository)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleNeighborInfo stores lastNeighborInfo when from own node`() {
|
||||
val ni = NeighborInfo(node_id = myNodeNum, neighbors = listOf(Neighbor(node_id = 100, snr = 5.0f)))
|
||||
val packet = createPacketWithNeighborInfo(from = myNodeNum, ni = ni)
|
||||
|
||||
every { nodeRepository.getUser(100) } returns User(long_name = "Alice", short_name = "AL")
|
||||
every { nodeRepository.getUser(myNodeNum) } returns User(long_name = "Me", short_name = "ME")
|
||||
|
||||
handler.handleNeighborInfo(packet)
|
||||
|
||||
assertEquals(ni, handler.lastNeighborInfo)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleNeighborInfo does not store lastNeighborInfo when from remote node`() {
|
||||
val remoteNode = 99999
|
||||
val ni = NeighborInfo(node_id = remoteNode, neighbors = listOf(Neighbor(node_id = 200, snr = 3.0f)))
|
||||
val packet = createPacketWithNeighborInfo(from = remoteNode, ni = ni)
|
||||
|
||||
every { nodeRepository.getUser(200) } returns User(long_name = "Bob", short_name = "BO")
|
||||
every { nodeRepository.getUser(remoteNode) } returns User(long_name = "Remote", short_name = "RM")
|
||||
|
||||
handler.handleNeighborInfo(packet)
|
||||
|
||||
assertNull(handler.lastNeighborInfo)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleNeighborInfo sets response on serviceRepository`() {
|
||||
val ni =
|
||||
NeighborInfo(
|
||||
node_id = myNodeNum,
|
||||
neighbors = listOf(Neighbor(node_id = 100, snr = 5.5f), Neighbor(node_id = 200, snr = -2.0f)),
|
||||
)
|
||||
val packet = createPacketWithNeighborInfo(from = myNodeNum, ni = ni)
|
||||
|
||||
every { nodeRepository.getUser(100) } returns User(long_name = "Alice", short_name = "AL")
|
||||
every { nodeRepository.getUser(200) } returns User(long_name = "Bob", short_name = "BO")
|
||||
every { nodeRepository.getUser(myNodeNum) } returns User(long_name = "Me", short_name = "ME")
|
||||
|
||||
handler.handleNeighborInfo(packet)
|
||||
|
||||
verify { serviceRepository.setNeighborInfoResponse(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleNeighborInfo ignores packet with null decoded`() {
|
||||
val packet = MeshPacket(from = myNodeNum)
|
||||
handler.handleNeighborInfo(packet)
|
||||
assertNull(handler.lastNeighborInfo)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `recordStartTime and handleNeighborInfo includes duration`() {
|
||||
val requestId = 42
|
||||
val ni = NeighborInfo(node_id = myNodeNum, neighbors = listOf(Neighbor(node_id = 100, snr = 1.0f)))
|
||||
val packet = createPacketWithNeighborInfo(from = myNodeNum, ni = ni, requestId = requestId)
|
||||
|
||||
every { nodeRepository.getUser(100) } returns User(long_name = "Alice", short_name = "AL")
|
||||
every { nodeRepository.getUser(myNodeNum) } returns User(long_name = "Me", short_name = "ME")
|
||||
|
||||
handler.recordStartTime(requestId)
|
||||
handler.handleNeighborInfo(packet)
|
||||
|
||||
verify { serviceRepository.setNeighborInfoResponse(any()) }
|
||||
}
|
||||
|
||||
private fun createPacketWithNeighborInfo(from: Int, ni: NeighborInfo, requestId: Int = 0): MeshPacket {
|
||||
val encoded = NeighborInfo.ADAPTER.encode(ni).toByteString()
|
||||
return MeshPacket(from = from, decoded = Data(payload = encoded, request_id = requestId))
|
||||
}
|
||||
}
|
||||
@@ -17,15 +17,18 @@
|
||||
package org.meshtastic.core.data.manager
|
||||
|
||||
import dev.mokkery.MockMode
|
||||
import dev.mokkery.answering.returns
|
||||
import dev.mokkery.every
|
||||
import dev.mokkery.mock
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import okio.ByteString
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.MyNodeInfo
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.model.NodeAddress
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
import org.meshtastic.core.repository.NotificationManager
|
||||
import org.meshtastic.core.repository.ServiceBroadcasts
|
||||
import org.meshtastic.proto.DeviceMetrics
|
||||
import org.meshtastic.proto.EnvironmentMetrics
|
||||
import org.meshtastic.proto.HardwareModel
|
||||
@@ -43,7 +46,6 @@ import org.meshtastic.proto.Position as ProtoPosition
|
||||
class NodeManagerImplTest {
|
||||
|
||||
private val nodeRepository: NodeRepository = mock(MockMode.autofill)
|
||||
private val serviceBroadcasts: ServiceBroadcasts = mock(MockMode.autofill)
|
||||
private val notificationManager: NotificationManager = mock(MockMode.autofill)
|
||||
private val testScope = TestScope()
|
||||
|
||||
@@ -51,7 +53,7 @@ class NodeManagerImplTest {
|
||||
|
||||
@BeforeTest
|
||||
fun setUp() {
|
||||
nodeManager = NodeManagerImpl(nodeRepository, serviceBroadcasts, notificationManager, testScope)
|
||||
nodeManager = NodeManagerImpl(nodeRepository, notificationManager, testScope)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -62,7 +64,7 @@ class NodeManagerImplTest {
|
||||
assertNotNull(result)
|
||||
assertEquals(nodeNum, result.num)
|
||||
assertTrue(result.user.long_name.startsWith("Meshtastic"))
|
||||
assertEquals(DataPacket.nodeNumToDefaultId(nodeNum), result.user.id)
|
||||
assertEquals(NodeAddress.numToDefaultId(nodeNum), result.user.id)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -192,20 +194,20 @@ class NodeManagerImplTest {
|
||||
nodeManager.clear()
|
||||
|
||||
assertTrue(nodeManager.nodeDBbyNodeNum.isEmpty())
|
||||
assertTrue(nodeManager.nodeDBbyID.isEmpty())
|
||||
assertNull(nodeManager.getNodeById("!000004d2"))
|
||||
assertNull(nodeManager.myNodeNum.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toNodeID returns broadcast ID for broadcast nodeNum`() {
|
||||
val result = nodeManager.toNodeID(DataPacket.NODENUM_BROADCAST)
|
||||
assertEquals(DataPacket.ID_BROADCAST, result)
|
||||
val result = nodeManager.toNodeID(NodeAddress.NODENUM_BROADCAST)
|
||||
assertEquals(NodeAddress.ID_BROADCAST, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toNodeID returns default hex ID for unknown node`() {
|
||||
val result = nodeManager.toNodeID(0x1234)
|
||||
assertEquals(DataPacket.nodeNumToDefaultId(0x1234), result)
|
||||
assertEquals(NodeAddress.numToDefaultId(0x1234), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -218,18 +220,18 @@ class NodeManagerImplTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `removeByNodenum removes node from both maps`() {
|
||||
fun `removeByNodenum removes node from map`() {
|
||||
val nodeNum = 1234
|
||||
nodeManager.updateNode(nodeNum) {
|
||||
Node(num = nodeNum, user = User(id = "!testnode", long_name = "Test", short_name = "T"))
|
||||
}
|
||||
assertTrue(nodeManager.nodeDBbyNodeNum.containsKey(nodeNum))
|
||||
assertTrue(nodeManager.nodeDBbyID.containsKey("!testnode"))
|
||||
assertNotNull(nodeManager.getNodeById("!testnode"))
|
||||
|
||||
nodeManager.removeByNodenum(nodeNum)
|
||||
|
||||
assertTrue(!nodeManager.nodeDBbyNodeNum.containsKey(nodeNum))
|
||||
assertTrue(!nodeManager.nodeDBbyID.containsKey("!testnode"))
|
||||
assertNull(nodeManager.getNodeById("!testnode"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -330,4 +332,111 @@ class NodeManagerImplTest {
|
||||
assertEquals(ByteString.EMPTY, result.publicKey)
|
||||
assertEquals(ByteString.EMPTY, result.user.public_key)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getMyNodeInfo returns null when repository has no info`() {
|
||||
every { nodeRepository.myNodeInfo } returns MutableStateFlow(null)
|
||||
|
||||
val result = nodeManager.getMyNodeInfo()
|
||||
|
||||
assertNull(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getMyNodeInfo synthesizes from repository and nodeDB`() {
|
||||
val myNum = 1234
|
||||
val repoInfo =
|
||||
MyNodeInfo(
|
||||
myNodeNum = myNum,
|
||||
hasGPS = false,
|
||||
model = "tbeam",
|
||||
firmwareVersion = "2.5.0",
|
||||
couldUpdate = false,
|
||||
shouldUpdate = false,
|
||||
currentPacketId = 100L,
|
||||
messageTimeoutMsec = 5000,
|
||||
minAppVersion = 30000,
|
||||
maxChannels = 8,
|
||||
hasWifi = false,
|
||||
channelUtilization = 0f,
|
||||
airUtilTx = 0f,
|
||||
deviceId = null,
|
||||
)
|
||||
every { nodeRepository.myNodeInfo } returns MutableStateFlow(repoInfo)
|
||||
|
||||
// Add node with position (non-zero lat → hasGPS = true)
|
||||
nodeManager.handleReceivedPosition(myNum, myNum, ProtoPosition(latitude_i = 100), 0)
|
||||
nodeManager.updateNode(myNum) { it.copy(user = it.user.copy(id = "!mydevice", hw_model = HardwareModel.TBEAM)) }
|
||||
|
||||
val result = nodeManager.getMyNodeInfo()
|
||||
|
||||
assertNotNull(result)
|
||||
assertEquals(myNum, result.myNodeNum)
|
||||
assertTrue(result.hasGPS)
|
||||
assertEquals("tbeam", result.model)
|
||||
assertEquals("!mydevice", result.deviceId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getMyNodeInfo falls back to nodeDB model when repository model is null`() {
|
||||
val myNum = 1234
|
||||
val repoInfo =
|
||||
MyNodeInfo(
|
||||
myNodeNum = myNum,
|
||||
hasGPS = false,
|
||||
model = null,
|
||||
firmwareVersion = "2.5.0",
|
||||
couldUpdate = false,
|
||||
shouldUpdate = false,
|
||||
currentPacketId = 100L,
|
||||
messageTimeoutMsec = 5000,
|
||||
minAppVersion = 30000,
|
||||
maxChannels = 8,
|
||||
hasWifi = false,
|
||||
channelUtilization = 0f,
|
||||
airUtilTx = 0f,
|
||||
deviceId = null,
|
||||
)
|
||||
every { nodeRepository.myNodeInfo } returns MutableStateFlow(repoInfo)
|
||||
|
||||
nodeManager.updateNode(myNum) { it.copy(user = it.user.copy(hw_model = HardwareModel.HELTEC_V3)) }
|
||||
|
||||
val result = nodeManager.getMyNodeInfo()
|
||||
|
||||
assertNotNull(result)
|
||||
assertEquals("HELTEC_V3", result.model)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleReceivedTelemetry with null metrics does not crash`() {
|
||||
val nodeNum = 1234
|
||||
nodeManager.updateNode(nodeNum) { it.copy(lastHeard = 1000) }
|
||||
|
||||
// Telemetry with no metrics at all
|
||||
val telemetry = Telemetry(time = 3000)
|
||||
|
||||
nodeManager.handleReceivedTelemetry(nodeNum, telemetry)
|
||||
|
||||
val result = nodeManager.nodeDBbyNodeNum[nodeNum]
|
||||
assertNotNull(result)
|
||||
assertEquals(3000, result.lastHeard)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getMyId returns empty when disconnected`() {
|
||||
every { nodeRepository.myNodeInfo } returns MutableStateFlow(null)
|
||||
|
||||
val result = nodeManager.getMyId()
|
||||
assertEquals("", result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getMyId returns user ID when connected`() {
|
||||
val myNum = 1234
|
||||
nodeManager.setMyNodeNum(myNum)
|
||||
nodeManager.updateNode(myNum) { it.copy(user = it.user.copy(id = "!mynode42")) }
|
||||
|
||||
val result = nodeManager.getMyId()
|
||||
assertEquals("!mynode42", result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.repository.MeshLogRepository
|
||||
import org.meshtastic.core.repository.PacketRepository
|
||||
import org.meshtastic.core.repository.RadioInterfaceService
|
||||
import org.meshtastic.core.repository.ServiceBroadcasts
|
||||
import org.meshtastic.core.repository.ServiceRepository
|
||||
import org.meshtastic.proto.Data
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
@@ -48,7 +47,6 @@ import kotlin.test.assertNotNull
|
||||
class PacketHandlerImplTest {
|
||||
|
||||
private val packetRepository: PacketRepository = mock(MockMode.autofill)
|
||||
private val serviceBroadcasts: ServiceBroadcasts = mock(MockMode.autofill)
|
||||
private val radioInterfaceService: RadioInterfaceService = mock(MockMode.autofill)
|
||||
private val meshLogRepository: MeshLogRepository = mock(MockMode.autofill)
|
||||
private val serviceRepository: ServiceRepository = mock(MockMode.autofill)
|
||||
@@ -67,7 +65,6 @@ class PacketHandlerImplTest {
|
||||
handler =
|
||||
PacketHandlerImpl(
|
||||
lazy { packetRepository },
|
||||
serviceBroadcasts,
|
||||
radioInterfaceService,
|
||||
lazy { meshLogRepository },
|
||||
serviceRepository,
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.data.manager
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class RequestTimerTest {
|
||||
|
||||
@Test
|
||||
fun appendDuration_withoutStart_returnsTextUnchanged() {
|
||||
val timer = RequestTimer()
|
||||
|
||||
assertEquals("base", timer.appendDuration(requestId = 1, text = "base", logLabel = "Test"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun appendDuration_afterStart_appendsDurationLine() {
|
||||
val timer = RequestTimer()
|
||||
timer.start(requestId = 7)
|
||||
|
||||
val result = timer.appendDuration(requestId = 7, text = "base", logLabel = "Test")
|
||||
|
||||
assertTrue(result.startsWith("base\n\nDuration: "), "expected a duration suffix, got: $result")
|
||||
assertTrue(result.endsWith(" s"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun appendDuration_consumesStartTime_soSecondCallIsUnchanged() {
|
||||
val timer = RequestTimer()
|
||||
timer.start(requestId = 7)
|
||||
|
||||
timer.appendDuration(requestId = 7, text = "first", logLabel = "Test")
|
||||
// The start time is single-use; a second response for the same id gets no duration.
|
||||
assertEquals("second", timer.appendDuration(requestId = 7, text = "second", logLabel = "Test"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun start_tracksRequestsIndependently() {
|
||||
val timer = RequestTimer()
|
||||
timer.start(requestId = 1)
|
||||
timer.start(requestId = 2)
|
||||
|
||||
// Consuming one id must not affect the other.
|
||||
timer.appendDuration(requestId = 1, text = "a", logLabel = "Test")
|
||||
assertTrue(timer.appendDuration(requestId = 2, text = "b", logLabel = "Test").contains("Duration: "))
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.NodeAddress
|
||||
import org.meshtastic.core.repository.MeshConnectionManager
|
||||
import org.meshtastic.core.repository.NodeManager
|
||||
import org.meshtastic.core.repository.NotificationManager
|
||||
@@ -78,8 +79,8 @@ class TelemetryPacketHandlerImplTest {
|
||||
private fun makeDataPacket(from: Int): DataPacket = DataPacket(
|
||||
id = 1,
|
||||
time = 1700000000000L,
|
||||
to = DataPacket.ID_BROADCAST,
|
||||
from = DataPacket.nodeNumToDefaultId(from),
|
||||
to = NodeAddress.ID_BROADCAST,
|
||||
from = NodeAddress.numToDefaultId(from),
|
||||
bytes = null,
|
||||
dataType = PortNum.TELEMETRY_APP.value,
|
||||
)
|
||||
@@ -97,7 +98,7 @@ class TelemetryPacketHandlerImplTest {
|
||||
advanceUntilIdle()
|
||||
|
||||
verify { connectionManager.updateTelemetry(any()) }
|
||||
verify { nodeManager.updateNode(myNodeNum, any(), any(), any()) }
|
||||
verify { nodeManager.updateNode(myNodeNum, any(), any()) }
|
||||
}
|
||||
|
||||
// ---------- Device metrics from remote node ----------
|
||||
@@ -112,7 +113,7 @@ class TelemetryPacketHandlerImplTest {
|
||||
handler.handleTelemetry(packet, dataPacket, myNodeNum)
|
||||
advanceUntilIdle()
|
||||
|
||||
verify { nodeManager.updateNode(remoteNodeNum, any(), any(), any()) }
|
||||
verify { nodeManager.updateNode(remoteNodeNum, any(), any()) }
|
||||
}
|
||||
|
||||
// ---------- Environment metrics ----------
|
||||
@@ -130,7 +131,7 @@ class TelemetryPacketHandlerImplTest {
|
||||
handler.handleTelemetry(packet, dataPacket, myNodeNum)
|
||||
advanceUntilIdle()
|
||||
|
||||
verify { nodeManager.updateNode(remoteNodeNum, any(), any(), any()) }
|
||||
verify { nodeManager.updateNode(remoteNodeNum, any(), any()) }
|
||||
}
|
||||
|
||||
// ---------- Power metrics ----------
|
||||
@@ -144,7 +145,7 @@ class TelemetryPacketHandlerImplTest {
|
||||
handler.handleTelemetry(packet, dataPacket, myNodeNum)
|
||||
advanceUntilIdle()
|
||||
|
||||
verify { nodeManager.updateNode(remoteNodeNum, any(), any(), any()) }
|
||||
verify { nodeManager.updateNode(remoteNodeNum, any(), any()) }
|
||||
}
|
||||
|
||||
// ---------- Telemetry time handling ----------
|
||||
@@ -158,7 +159,7 @@ class TelemetryPacketHandlerImplTest {
|
||||
handler.handleTelemetry(packet, dataPacket, myNodeNum)
|
||||
advanceUntilIdle()
|
||||
|
||||
verify { nodeManager.updateNode(myNodeNum, any(), any(), any()) }
|
||||
verify { nodeManager.updateNode(myNodeNum, any(), any()) }
|
||||
}
|
||||
|
||||
// ---------- Null payload ----------
|
||||
|
||||
@@ -32,11 +32,11 @@ import kotlinx.coroutines.test.runTest
|
||||
import okio.ByteString
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.NodeAddress
|
||||
import org.meshtastic.core.repository.HistoryManager
|
||||
import org.meshtastic.core.repository.MeshDataHandler
|
||||
import org.meshtastic.core.repository.NodeManager
|
||||
import org.meshtastic.core.repository.PacketRepository
|
||||
import org.meshtastic.core.repository.ServiceBroadcasts
|
||||
import org.meshtastic.proto.Data
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.PortNum
|
||||
@@ -50,7 +50,6 @@ class StoreForwardPacketHandlerImplTest {
|
||||
|
||||
private val nodeManager = mock<NodeManager>(MockMode.autofill)
|
||||
private val packetRepository = mock<PacketRepository>(MockMode.autofill)
|
||||
private val serviceBroadcasts = mock<ServiceBroadcasts>(MockMode.autofill)
|
||||
private val historyManager = mock<HistoryManager>(MockMode.autofill)
|
||||
private val dataHandler = mock<MeshDataHandler>(MockMode.autofill)
|
||||
|
||||
@@ -69,7 +68,6 @@ class StoreForwardPacketHandlerImplTest {
|
||||
StoreForwardPacketHandlerImpl(
|
||||
nodeManager = nodeManager,
|
||||
packetRepository = lazy { packetRepository },
|
||||
serviceBroadcasts = serviceBroadcasts,
|
||||
historyManager = historyManager,
|
||||
dataHandler = lazy { dataHandler },
|
||||
scope = testScope,
|
||||
@@ -89,8 +87,8 @@ class StoreForwardPacketHandlerImplTest {
|
||||
private fun makeDataPacket(from: Int): DataPacket = DataPacket(
|
||||
id = 1,
|
||||
time = 1700000000000L,
|
||||
to = DataPacket.ID_BROADCAST,
|
||||
from = DataPacket.nodeNumToDefaultId(from),
|
||||
to = NodeAddress.ID_BROADCAST,
|
||||
from = NodeAddress.numToDefaultId(from),
|
||||
bytes = null,
|
||||
dataType = PortNum.STORE_FORWARD_APP.value,
|
||||
)
|
||||
@@ -222,7 +220,6 @@ class StoreForwardPacketHandlerImplTest {
|
||||
advanceUntilIdle()
|
||||
|
||||
verifySuspend { packetRepository.updateSFPPStatus(any(), any(), any(), any(), any(), any(), any()) }
|
||||
verify { serviceBroadcasts.broadcastMessageStatus(42, any()) }
|
||||
}
|
||||
|
||||
// ---------- SF++: CANON_ANNOUNCE ----------
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.meshtastic.core.database.MeshtasticDatabaseConstructor
|
||||
import org.meshtastic.core.database.entity.MyNodeEntity
|
||||
import org.meshtastic.core.database.entity.Packet
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.NodeAddress
|
||||
import org.meshtastic.proto.ChannelSettings
|
||||
import org.meshtastic.proto.PortNum
|
||||
import org.robolectric.annotation.Config
|
||||
@@ -168,7 +169,7 @@ class MigrationTest {
|
||||
contact_key = "$channel!broadcast",
|
||||
received_time = nowMillis,
|
||||
read = false,
|
||||
data = DataPacket(to = DataPacket.ID_BROADCAST, channel = channel, text = text),
|
||||
data = DataPacket(to = NodeAddress.ID_BROADCAST, channel = channel, text = text),
|
||||
)
|
||||
packetDao.insert(packet)
|
||||
}
|
||||
|
||||
@@ -26,12 +26,7 @@ import okio.ByteString
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.meshtastic.core.common.util.nowMillis
|
||||
import org.meshtastic.core.common.util.nowSeconds
|
||||
import org.meshtastic.core.model.DeviceMetrics
|
||||
import org.meshtastic.core.model.EnvironmentMetrics
|
||||
import org.meshtastic.core.model.MeshUser
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.model.NodeInfo
|
||||
import org.meshtastic.core.model.Position
|
||||
import org.meshtastic.core.model.util.onlineTimeThreshold
|
||||
import org.meshtastic.proto.DeviceMetadata
|
||||
import org.meshtastic.proto.HardwareModel
|
||||
@@ -46,32 +41,32 @@ data class NodeWithRelations(
|
||||
@Relation(entity = MetadataEntity::class, parentColumns = ["num"], entityColumns = ["num"])
|
||||
val metadata: MetadataEntity?,
|
||||
) {
|
||||
fun toModel() = with(node) {
|
||||
Node(
|
||||
num = num,
|
||||
metadata = metadata?.proto,
|
||||
user = user,
|
||||
position = position,
|
||||
snr = snr,
|
||||
rssi = rssi,
|
||||
lastHeard = lastHeard,
|
||||
deviceMetrics = deviceMetrics ?: org.meshtastic.proto.DeviceMetrics(),
|
||||
channel = channel,
|
||||
viaMqtt = viaMqtt,
|
||||
hopsAway = hopsAway,
|
||||
isFavorite = isFavorite,
|
||||
isIgnored = isIgnored,
|
||||
isMuted = isMuted,
|
||||
environmentMetrics = environmentMetrics ?: org.meshtastic.proto.EnvironmentMetrics(),
|
||||
powerMetrics = powerMetrics ?: org.meshtastic.proto.PowerMetrics(),
|
||||
paxcounter = paxcounter,
|
||||
publicKey = publicKey ?: user.public_key,
|
||||
notes = notes,
|
||||
manuallyVerified = manuallyVerified,
|
||||
nodeStatus = nodeStatus,
|
||||
lastTransport = lastTransport,
|
||||
)
|
||||
}
|
||||
// Direct construction avoids the previous `node.toModel().copy(metadata = …, manuallyVerified = …)` pattern,
|
||||
// which allocated the Node twice per DB row (once from toModel, once from copy). Hot path on every DB emission.
|
||||
fun toModel() = Node(
|
||||
num = node.num,
|
||||
user = node.user,
|
||||
position = node.position,
|
||||
snr = node.snr,
|
||||
rssi = node.rssi,
|
||||
lastHeard = node.lastHeard,
|
||||
deviceMetrics = node.deviceMetrics ?: org.meshtastic.proto.DeviceMetrics(),
|
||||
channel = node.channel,
|
||||
viaMqtt = node.viaMqtt,
|
||||
hopsAway = node.hopsAway,
|
||||
isFavorite = node.isFavorite,
|
||||
isIgnored = node.isIgnored,
|
||||
isMuted = node.isMuted,
|
||||
environmentMetrics = node.environmentMetrics ?: org.meshtastic.proto.EnvironmentMetrics(),
|
||||
powerMetrics = node.powerMetrics ?: org.meshtastic.proto.PowerMetrics(),
|
||||
paxcounter = node.paxcounter,
|
||||
publicKey = node.publicKey ?: node.user.public_key,
|
||||
notes = node.notes,
|
||||
nodeStatus = node.nodeStatus,
|
||||
lastTransport = node.lastTransport,
|
||||
metadata = metadata?.proto,
|
||||
manuallyVerified = node.manuallyVerified,
|
||||
)
|
||||
|
||||
fun toEntity() = with(node) {
|
||||
NodeEntity(
|
||||
@@ -211,49 +206,4 @@ data class NodeEntity(
|
||||
nodeStatus = nodeStatus,
|
||||
lastTransport = lastTransport,
|
||||
)
|
||||
|
||||
fun toNodeInfo() = NodeInfo(
|
||||
num = num,
|
||||
user =
|
||||
MeshUser(
|
||||
id = user.id,
|
||||
longName = user.long_name,
|
||||
shortName = user.short_name,
|
||||
hwModel = user.hw_model,
|
||||
role = user.role.value,
|
||||
)
|
||||
.takeIf { user.id.isNotEmpty() },
|
||||
position =
|
||||
Position(
|
||||
latitude = latitude,
|
||||
longitude = longitude,
|
||||
altitude = position.altitude ?: 0,
|
||||
time = position.time,
|
||||
satellitesInView = position.sats_in_view,
|
||||
groundSpeed = position.ground_speed ?: 0,
|
||||
groundTrack = position.ground_track ?: 0,
|
||||
precisionBits = position.precision_bits,
|
||||
)
|
||||
.takeIf { it.isValid() },
|
||||
snr = snr,
|
||||
rssi = rssi,
|
||||
lastHeard = lastHeard,
|
||||
deviceMetrics =
|
||||
DeviceMetrics(
|
||||
time = deviceTelemetry.time,
|
||||
batteryLevel = deviceMetrics?.battery_level ?: 0,
|
||||
voltage = deviceMetrics?.voltage ?: 0f,
|
||||
channelUtilization = deviceMetrics?.channel_utilization ?: 0f,
|
||||
airUtilTx = deviceMetrics?.air_util_tx ?: 0f,
|
||||
uptimeSeconds = deviceMetrics?.uptime_seconds ?: 0,
|
||||
),
|
||||
channel = channel,
|
||||
environmentMetrics =
|
||||
EnvironmentMetrics.fromTelemetryProto(
|
||||
environmentTelemetry.environment_metrics ?: org.meshtastic.proto.EnvironmentMetrics(),
|
||||
environmentTelemetry.time,
|
||||
),
|
||||
hopsAway = hopsAway,
|
||||
nodeStatus = nodeStatus,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.Message
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.model.NodeAddress
|
||||
import org.meshtastic.core.model.Reaction
|
||||
import org.meshtastic.core.model.util.getShortDateTime
|
||||
|
||||
@@ -38,7 +39,7 @@ data class PacketEntity(
|
||||
) {
|
||||
suspend fun toMessage(getNode: suspend (userId: String?) -> Node) = with(packet) {
|
||||
val node = getNode(data.from)
|
||||
val isFromLocal = node.user.id == DataPacket.ID_LOCAL || (myNodeNum != 0 && node.num == myNodeNum)
|
||||
val isFromLocal = node.user.id == NodeAddress.ID_LOCAL || (myNodeNum != 0 && node.num == myNodeNum)
|
||||
Message(
|
||||
uuid = uuid,
|
||||
receivedTime = received_time,
|
||||
|
||||
@@ -20,6 +20,7 @@ import okio.ByteString
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.model.NodeAddress
|
||||
import org.meshtastic.proto.DeviceMetadata
|
||||
import org.meshtastic.proto.DeviceMetrics
|
||||
import org.meshtastic.proto.FromRadio
|
||||
@@ -41,7 +42,7 @@ class ConvertersTest {
|
||||
fun `data packet string converter round trips`() {
|
||||
val packet =
|
||||
DataPacket(
|
||||
to = DataPacket.ID_BROADCAST,
|
||||
to = NodeAddress.ID_BROADCAST,
|
||||
bytes = "hello mesh".encodeToByteArray().toByteString(),
|
||||
dataType = 1,
|
||||
from = "!12345678",
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.meshtastic.core.database.entity.ReactionEntity
|
||||
import org.meshtastic.core.database.getInMemoryDatabaseBuilder
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.model.NodeAddress
|
||||
import org.meshtastic.proto.PortNum
|
||||
import kotlin.test.AfterTest
|
||||
import kotlin.test.Test
|
||||
@@ -57,7 +58,7 @@ abstract class CommonPacketDaoTest {
|
||||
private val myNodeNum: Int
|
||||
get() = myNodeInfo.myNodeNum
|
||||
|
||||
private val testContactKeys = listOf("0${DataPacket.ID_BROADCAST}", "1!test1234")
|
||||
private val testContactKeys = listOf("0${NodeAddress.ID_BROADCAST}", "1!test1234")
|
||||
|
||||
private fun generateTestPackets(myNodeNum: Int) = testContactKeys.flatMap { contactKey ->
|
||||
List(SAMPLE_SIZE) {
|
||||
@@ -70,7 +71,7 @@ abstract class CommonPacketDaoTest {
|
||||
read = false,
|
||||
data =
|
||||
DataPacket(
|
||||
to = DataPacket.ID_BROADCAST,
|
||||
to = NodeAddress.ID_BROADCAST,
|
||||
bytes = "Message $it!".encodeToByteArray().toByteString(),
|
||||
dataType = PortNum.TEXT_MESSAGE_APP.value,
|
||||
),
|
||||
@@ -157,7 +158,7 @@ abstract class CommonPacketDaoTest {
|
||||
read = true,
|
||||
data =
|
||||
DataPacket(
|
||||
to = DataPacket.ID_BROADCAST,
|
||||
to = NodeAddress.ID_BROADCAST,
|
||||
bytes = "Queued".encodeToByteArray().toByteString(),
|
||||
dataType = PortNum.TEXT_MESSAGE_APP.value,
|
||||
status = MessageStatus.QUEUED,
|
||||
@@ -191,12 +192,12 @@ abstract class CommonPacketDaoTest {
|
||||
uuid = 0L,
|
||||
myNodeNum = myNodeNum,
|
||||
port_num = PortNum.WAYPOINT_APP.value,
|
||||
contact_key = "0${DataPacket.ID_BROADCAST}",
|
||||
contact_key = "0${NodeAddress.ID_BROADCAST}",
|
||||
received_time = nowMillis,
|
||||
read = true,
|
||||
data =
|
||||
DataPacket(
|
||||
to = DataPacket.ID_BROADCAST,
|
||||
to = NodeAddress.ID_BROADCAST,
|
||||
bytes = "Waypoint".encodeToByteArray().toByteString(),
|
||||
dataType = PortNum.WAYPOINT_APP.value,
|
||||
),
|
||||
@@ -231,7 +232,7 @@ abstract class CommonPacketDaoTest {
|
||||
read = false,
|
||||
data =
|
||||
DataPacket(
|
||||
to = DataPacket.ID_BROADCAST,
|
||||
to = NodeAddress.ID_BROADCAST,
|
||||
bytes = text.encodeToByteArray().toByteString(),
|
||||
dataType = PortNum.TEXT_MESSAGE_APP.value,
|
||||
),
|
||||
@@ -251,7 +252,7 @@ abstract class CommonPacketDaoTest {
|
||||
read = true,
|
||||
data =
|
||||
DataPacket(
|
||||
to = DataPacket.ID_BROADCAST,
|
||||
to = NodeAddress.ID_BROADCAST,
|
||||
bytes = text.encodeToByteArray().toByteString(),
|
||||
dataType = PortNum.TEXT_MESSAGE_APP.value,
|
||||
),
|
||||
@@ -293,7 +294,7 @@ abstract class CommonPacketDaoTest {
|
||||
read = false,
|
||||
data =
|
||||
DataPacket(
|
||||
to = DataPacket.ID_BROADCAST,
|
||||
to = NodeAddress.ID_BROADCAST,
|
||||
bytes = "Chunk $id".encodeToByteArray().toByteString(),
|
||||
dataType = PortNum.TEXT_MESSAGE_APP.value,
|
||||
),
|
||||
|
||||
@@ -35,7 +35,6 @@ src/commonMain/kotlin/org/meshtastic/core/domain/
|
||||
├── ImportProfileUseCase.kt
|
||||
├── InstallProfileUseCase.kt
|
||||
├── IsOtaCapableUseCase.kt
|
||||
├── MeshLocationUseCase.kt
|
||||
├── ProcessRadioResponseUseCase.kt
|
||||
├── RadioConfigUseCase.kt
|
||||
├── SetAppIntroCompletedUseCase.kt
|
||||
|
||||
@@ -30,8 +30,7 @@ import org.koin.core.annotation.Named
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.model.SessionStatus
|
||||
import org.meshtastic.core.model.service.ServiceAction
|
||||
import org.meshtastic.core.repository.MeshActionHandler
|
||||
import org.meshtastic.core.repository.RadioController
|
||||
import org.meshtastic.core.repository.ServiceRepository
|
||||
import org.meshtastic.core.repository.SessionManager
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
@@ -55,7 +54,7 @@ import kotlin.time.Duration.Companion.seconds
|
||||
@Single
|
||||
open class EnsureRemoteAdminSessionUseCase(
|
||||
private val sessionManager: SessionManager,
|
||||
private val meshActionHandler: MeshActionHandler,
|
||||
private val radioController: RadioController,
|
||||
private val serviceRepository: ServiceRepository,
|
||||
@Named("ServiceScope") private val serviceScope: CoroutineScope,
|
||||
) {
|
||||
@@ -94,7 +93,7 @@ open class EnsureRemoteAdminSessionUseCase(
|
||||
sessionManager.sessionRefreshFlow.filter { it == destNum }.first()
|
||||
}
|
||||
try {
|
||||
meshActionHandler.onServiceAction(ServiceAction.GetDeviceMetadata(destNum))
|
||||
radioController.refreshMetadata(destNum)
|
||||
refreshed.await()
|
||||
EnsureSessionResult.Refreshed
|
||||
} finally {
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.model.RadioController
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
import org.meshtastic.core.repository.RadioController
|
||||
|
||||
/**
|
||||
* Use case for performing administrative and destructive actions on mesh nodes.
|
||||
@@ -39,7 +39,7 @@ constructor(
|
||||
* @return The packet ID of the request.
|
||||
*/
|
||||
open suspend fun reboot(destNum: Int): Int {
|
||||
val packetId = radioController.getPacketId()
|
||||
val packetId = radioController.generatePacketId()
|
||||
radioController.reboot(destNum, packetId)
|
||||
return packetId
|
||||
}
|
||||
@@ -51,7 +51,7 @@ constructor(
|
||||
* @return The packet ID of the request.
|
||||
*/
|
||||
open suspend fun shutdown(destNum: Int): Int {
|
||||
val packetId = radioController.getPacketId()
|
||||
val packetId = radioController.generatePacketId()
|
||||
radioController.shutdown(destNum, packetId)
|
||||
return packetId
|
||||
}
|
||||
@@ -64,7 +64,7 @@ constructor(
|
||||
* @return The packet ID of the request.
|
||||
*/
|
||||
open suspend fun factoryReset(destNum: Int, isLocal: Boolean): Int {
|
||||
val packetId = radioController.getPacketId()
|
||||
val packetId = radioController.generatePacketId()
|
||||
radioController.factoryReset(destNum, packetId)
|
||||
|
||||
if (isLocal) {
|
||||
@@ -84,7 +84,7 @@ constructor(
|
||||
* @return The packet ID of the request.
|
||||
*/
|
||||
open suspend fun nodedbReset(destNum: Int, preserveFavorites: Boolean, isLocal: Boolean): Int {
|
||||
val packetId = radioController.getPacketId()
|
||||
val packetId = radioController.generatePacketId()
|
||||
radioController.nodedbReset(destNum, packetId, preserveFavorites)
|
||||
|
||||
if (isLocal) {
|
||||
|
||||
@@ -18,8 +18,8 @@ package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.model.RadioController
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
import org.meshtastic.core.repository.RadioController
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
/** Use case for cleaning up nodes from the database. */
|
||||
@@ -58,7 +58,7 @@ constructor(
|
||||
|
||||
nodeRepository.deleteNodes(nodeNums)
|
||||
for (nodeNum in nodeNums) {
|
||||
val packetId = radioController.getPacketId()
|
||||
val packetId = radioController.generatePacketId()
|
||||
radioController.removeByNodenum(packetId, nodeNum)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,8 @@ package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.model.Position
|
||||
import org.meshtastic.core.model.RadioController
|
||||
import org.meshtastic.core.repository.AdminEditScope
|
||||
import org.meshtastic.core.repository.RadioController
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.DeviceProfile
|
||||
import org.meshtastic.proto.LocalConfig
|
||||
@@ -37,118 +38,72 @@ open class InstallProfileUseCase constructor(private val radioController: RadioC
|
||||
* @param currentUser The current user configuration of the destination node (to preserve names if not in profile).
|
||||
*/
|
||||
open suspend operator fun invoke(destNum: Int, profile: DeviceProfile, currentUser: User?) {
|
||||
radioController.beginEditSettings(destNum)
|
||||
|
||||
installOwner(destNum, profile, currentUser)
|
||||
installConfig(destNum, profile.config)
|
||||
installFixedPosition(destNum, profile.fixed_position)
|
||||
installModuleConfig(destNum, profile.module_config)
|
||||
|
||||
radioController.commitEditSettings(destNum)
|
||||
radioController.editSettings(destNum) {
|
||||
installOwner(profile, currentUser)
|
||||
installConfig(profile.config)
|
||||
installFixedPosition(profile.fixed_position)
|
||||
installModuleConfig(profile.module_config)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun installOwner(destNum: Int, profile: DeviceProfile, currentUser: User?) {
|
||||
private suspend fun AdminEditScope.installOwner(profile: DeviceProfile, currentUser: User?) {
|
||||
if (profile.long_name != null || profile.short_name != null) {
|
||||
currentUser?.let {
|
||||
val user =
|
||||
setOwner(
|
||||
it.copy(
|
||||
long_name = profile.long_name ?: it.long_name,
|
||||
short_name = profile.short_name ?: it.short_name,
|
||||
)
|
||||
radioController.setOwner(destNum, user, radioController.getPacketId())
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun installConfig(destNum: Int, config: LocalConfig?) {
|
||||
private suspend fun AdminEditScope.installConfig(config: LocalConfig?) {
|
||||
config?.let { lc ->
|
||||
lc.device?.let { radioController.setConfig(destNum, Config(device = it), radioController.getPacketId()) }
|
||||
lc.position?.let {
|
||||
radioController.setConfig(destNum, Config(position = it), radioController.getPacketId())
|
||||
}
|
||||
lc.power?.let { radioController.setConfig(destNum, Config(power = it), radioController.getPacketId()) }
|
||||
lc.network?.let { radioController.setConfig(destNum, Config(network = it), radioController.getPacketId()) }
|
||||
lc.display?.let { radioController.setConfig(destNum, Config(display = it), radioController.getPacketId()) }
|
||||
lc.lora?.let { radioController.setConfig(destNum, Config(lora = it), radioController.getPacketId()) }
|
||||
lc.bluetooth?.let {
|
||||
radioController.setConfig(destNum, Config(bluetooth = it), radioController.getPacketId())
|
||||
}
|
||||
lc.security?.let {
|
||||
radioController.setConfig(destNum, Config(security = it), radioController.getPacketId())
|
||||
}
|
||||
lc.device?.let { setConfig(Config(device = it)) }
|
||||
lc.position?.let { setConfig(Config(position = it)) }
|
||||
lc.power?.let { setConfig(Config(power = it)) }
|
||||
lc.network?.let { setConfig(Config(network = it)) }
|
||||
lc.display?.let { setConfig(Config(display = it)) }
|
||||
lc.lora?.let { setConfig(Config(lora = it)) }
|
||||
lc.bluetooth?.let { setConfig(Config(bluetooth = it)) }
|
||||
lc.security?.let { setConfig(Config(security = it)) }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun installFixedPosition(destNum: Int, fixedPosition: org.meshtastic.proto.Position?) {
|
||||
private suspend fun AdminEditScope.installFixedPosition(fixedPosition: org.meshtastic.proto.Position?) {
|
||||
if (fixedPosition != null) {
|
||||
radioController.setFixedPosition(destNum, Position(fixedPosition))
|
||||
setFixedPosition(Position(fixedPosition))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun installModuleConfig(destNum: Int, moduleConfig: LocalModuleConfig?) {
|
||||
private suspend fun AdminEditScope.installModuleConfig(moduleConfig: LocalModuleConfig?) {
|
||||
moduleConfig?.let { lmc ->
|
||||
installModuleConfigPart1(destNum, lmc)
|
||||
installModuleConfigPart2(destNum, lmc)
|
||||
installModuleConfigPart1(lmc)
|
||||
installModuleConfigPart2(lmc)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun installModuleConfigPart1(destNum: Int, lmc: LocalModuleConfig) {
|
||||
lmc.mqtt?.let {
|
||||
radioController.setModuleConfig(destNum, ModuleConfig(mqtt = it), radioController.getPacketId())
|
||||
}
|
||||
lmc.serial?.let {
|
||||
radioController.setModuleConfig(destNum, ModuleConfig(serial = it), radioController.getPacketId())
|
||||
}
|
||||
lmc.external_notification?.let {
|
||||
radioController.setModuleConfig(
|
||||
destNum,
|
||||
ModuleConfig(external_notification = it),
|
||||
radioController.getPacketId(),
|
||||
)
|
||||
}
|
||||
lmc.store_forward?.let {
|
||||
radioController.setModuleConfig(destNum, ModuleConfig(store_forward = it), radioController.getPacketId())
|
||||
}
|
||||
lmc.range_test?.let {
|
||||
radioController.setModuleConfig(destNum, ModuleConfig(range_test = it), radioController.getPacketId())
|
||||
}
|
||||
lmc.telemetry?.let {
|
||||
radioController.setModuleConfig(destNum, ModuleConfig(telemetry = it), radioController.getPacketId())
|
||||
}
|
||||
lmc.canned_message?.let {
|
||||
radioController.setModuleConfig(destNum, ModuleConfig(canned_message = it), radioController.getPacketId())
|
||||
}
|
||||
lmc.audio?.let {
|
||||
radioController.setModuleConfig(destNum, ModuleConfig(audio = it), radioController.getPacketId())
|
||||
}
|
||||
private suspend fun AdminEditScope.installModuleConfigPart1(lmc: LocalModuleConfig) {
|
||||
lmc.mqtt?.let { setModuleConfig(ModuleConfig(mqtt = it)) }
|
||||
lmc.serial?.let { setModuleConfig(ModuleConfig(serial = it)) }
|
||||
lmc.external_notification?.let { setModuleConfig(ModuleConfig(external_notification = it)) }
|
||||
lmc.store_forward?.let { setModuleConfig(ModuleConfig(store_forward = it)) }
|
||||
lmc.range_test?.let { setModuleConfig(ModuleConfig(range_test = it)) }
|
||||
lmc.telemetry?.let { setModuleConfig(ModuleConfig(telemetry = it)) }
|
||||
lmc.canned_message?.let { setModuleConfig(ModuleConfig(canned_message = it)) }
|
||||
lmc.audio?.let { setModuleConfig(ModuleConfig(audio = it)) }
|
||||
}
|
||||
|
||||
private suspend fun installModuleConfigPart2(destNum: Int, lmc: LocalModuleConfig) {
|
||||
lmc.remote_hardware?.let {
|
||||
radioController.setModuleConfig(destNum, ModuleConfig(remote_hardware = it), radioController.getPacketId())
|
||||
}
|
||||
lmc.neighbor_info?.let {
|
||||
radioController.setModuleConfig(destNum, ModuleConfig(neighbor_info = it), radioController.getPacketId())
|
||||
}
|
||||
lmc.ambient_lighting?.let {
|
||||
radioController.setModuleConfig(destNum, ModuleConfig(ambient_lighting = it), radioController.getPacketId())
|
||||
}
|
||||
lmc.detection_sensor?.let {
|
||||
radioController.setModuleConfig(destNum, ModuleConfig(detection_sensor = it), radioController.getPacketId())
|
||||
}
|
||||
lmc.paxcounter?.let {
|
||||
radioController.setModuleConfig(destNum, ModuleConfig(paxcounter = it), radioController.getPacketId())
|
||||
}
|
||||
lmc.statusmessage?.let {
|
||||
radioController.setModuleConfig(destNum, ModuleConfig(statusmessage = it), radioController.getPacketId())
|
||||
}
|
||||
lmc.traffic_management?.let {
|
||||
radioController.setModuleConfig(
|
||||
destNum,
|
||||
ModuleConfig(traffic_management = it),
|
||||
radioController.getPacketId(),
|
||||
)
|
||||
}
|
||||
lmc.tak?.let { radioController.setModuleConfig(destNum, ModuleConfig(tak = it), radioController.getPacketId()) }
|
||||
private suspend fun AdminEditScope.installModuleConfigPart2(lmc: LocalModuleConfig) {
|
||||
lmc.remote_hardware?.let { setModuleConfig(ModuleConfig(remote_hardware = it)) }
|
||||
lmc.neighbor_info?.let { setModuleConfig(ModuleConfig(neighbor_info = it)) }
|
||||
lmc.ambient_lighting?.let { setModuleConfig(ModuleConfig(ambient_lighting = it)) }
|
||||
lmc.detection_sensor?.let { setModuleConfig(ModuleConfig(detection_sensor = it)) }
|
||||
lmc.paxcounter?.let { setModuleConfig(ModuleConfig(paxcounter = it)) }
|
||||
lmc.statusmessage?.let { setModuleConfig(ModuleConfig(statusmessage = it)) }
|
||||
lmc.traffic_management?.let { setModuleConfig(ModuleConfig(traffic_management = it)) }
|
||||
lmc.tak?.let { setModuleConfig(ModuleConfig(tak = it)) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,9 +24,9 @@ import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.model.RadioController
|
||||
import org.meshtastic.core.repository.DeviceHardwareRepository
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
import org.meshtastic.core.repository.RadioController
|
||||
import org.meshtastic.core.repository.RadioPrefs
|
||||
import org.meshtastic.core.repository.isBle
|
||||
import org.meshtastic.core.repository.isSerial
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.model.RadioController
|
||||
|
||||
/** Use case for controlling location sharing with the mesh. */
|
||||
@Single
|
||||
open class MeshLocationUseCase constructor(private val radioController: RadioController) {
|
||||
/** Starts providing the phone's location to the mesh. */
|
||||
fun startProvidingLocation() {
|
||||
radioController.startProvideLocation()
|
||||
}
|
||||
|
||||
/** Stops providing the phone's location to the mesh. */
|
||||
fun stopProvidingLocation() {
|
||||
radioController.stopProvideLocation()
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.model.Position
|
||||
import org.meshtastic.core.model.RadioController
|
||||
import org.meshtastic.core.repository.RadioController
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.ModuleConfig
|
||||
import org.meshtastic.proto.User
|
||||
@@ -35,7 +35,7 @@ open class RadioConfigUseCase constructor(private val radioController: RadioCont
|
||||
* @return The packet ID of the request.
|
||||
*/
|
||||
open suspend fun setOwner(destNum: Int, user: User): Int {
|
||||
val packetId = radioController.getPacketId()
|
||||
val packetId = radioController.generatePacketId()
|
||||
radioController.setOwner(destNum, user, packetId)
|
||||
return packetId
|
||||
}
|
||||
@@ -47,7 +47,7 @@ open class RadioConfigUseCase constructor(private val radioController: RadioCont
|
||||
* @return The packet ID of the request.
|
||||
*/
|
||||
open suspend fun getOwner(destNum: Int): Int {
|
||||
val packetId = radioController.getPacketId()
|
||||
val packetId = radioController.generatePacketId()
|
||||
radioController.getOwner(destNum, packetId)
|
||||
return packetId
|
||||
}
|
||||
@@ -60,7 +60,7 @@ open class RadioConfigUseCase constructor(private val radioController: RadioCont
|
||||
* @return The packet ID of the request.
|
||||
*/
|
||||
open suspend fun setConfig(destNum: Int, config: Config): Int {
|
||||
val packetId = radioController.getPacketId()
|
||||
val packetId = radioController.generatePacketId()
|
||||
radioController.setConfig(destNum, config, packetId)
|
||||
return packetId
|
||||
}
|
||||
@@ -73,7 +73,7 @@ open class RadioConfigUseCase constructor(private val radioController: RadioCont
|
||||
* @return The packet ID of the request.
|
||||
*/
|
||||
open suspend fun getConfig(destNum: Int, configType: Int): Int {
|
||||
val packetId = radioController.getPacketId()
|
||||
val packetId = radioController.generatePacketId()
|
||||
radioController.getConfig(destNum, configType, packetId)
|
||||
return packetId
|
||||
}
|
||||
@@ -86,7 +86,7 @@ open class RadioConfigUseCase constructor(private val radioController: RadioCont
|
||||
* @return The packet ID of the request.
|
||||
*/
|
||||
open suspend fun setModuleConfig(destNum: Int, config: ModuleConfig): Int {
|
||||
val packetId = radioController.getPacketId()
|
||||
val packetId = radioController.generatePacketId()
|
||||
radioController.setModuleConfig(destNum, config, packetId)
|
||||
return packetId
|
||||
}
|
||||
@@ -99,7 +99,7 @@ open class RadioConfigUseCase constructor(private val radioController: RadioCont
|
||||
* @return The packet ID of the request.
|
||||
*/
|
||||
open suspend fun getModuleConfig(destNum: Int, moduleConfigType: Int): Int {
|
||||
val packetId = radioController.getPacketId()
|
||||
val packetId = radioController.generatePacketId()
|
||||
radioController.getModuleConfig(destNum, moduleConfigType, packetId)
|
||||
return packetId
|
||||
}
|
||||
@@ -112,7 +112,7 @@ open class RadioConfigUseCase constructor(private val radioController: RadioCont
|
||||
* @return The packet ID of the request.
|
||||
*/
|
||||
open suspend fun getChannel(destNum: Int, index: Int): Int {
|
||||
val packetId = radioController.getPacketId()
|
||||
val packetId = radioController.generatePacketId()
|
||||
radioController.getChannel(destNum, index, packetId)
|
||||
return packetId
|
||||
}
|
||||
@@ -125,7 +125,7 @@ open class RadioConfigUseCase constructor(private val radioController: RadioCont
|
||||
* @return The packet ID of the request.
|
||||
*/
|
||||
open suspend fun setRemoteChannel(destNum: Int, channel: org.meshtastic.proto.Channel): Int {
|
||||
val packetId = radioController.getPacketId()
|
||||
val packetId = radioController.generatePacketId()
|
||||
radioController.setRemoteChannel(destNum, channel, packetId)
|
||||
return packetId
|
||||
}
|
||||
@@ -152,7 +152,7 @@ open class RadioConfigUseCase constructor(private val radioController: RadioCont
|
||||
* @return The packet ID of the request.
|
||||
*/
|
||||
open suspend fun getRingtone(destNum: Int): Int {
|
||||
val packetId = radioController.getPacketId()
|
||||
val packetId = radioController.generatePacketId()
|
||||
radioController.getRingtone(destNum, packetId)
|
||||
return packetId
|
||||
}
|
||||
@@ -169,7 +169,7 @@ open class RadioConfigUseCase constructor(private val radioController: RadioCont
|
||||
* @return The packet ID of the request.
|
||||
*/
|
||||
open suspend fun getCannedMessages(destNum: Int): Int {
|
||||
val packetId = radioController.getPacketId()
|
||||
val packetId = radioController.generatePacketId()
|
||||
radioController.getCannedMessages(destNum, packetId)
|
||||
return packetId
|
||||
}
|
||||
@@ -181,7 +181,7 @@ open class RadioConfigUseCase constructor(private val radioController: RadioCont
|
||||
* @return The packet ID of the request.
|
||||
*/
|
||||
open suspend fun getDeviceConnectionStatus(destNum: Int): Int {
|
||||
val packetId = radioController.getPacketId()
|
||||
val packetId = radioController.generatePacketId()
|
||||
radioController.getDeviceConnectionStatus(destNum, packetId)
|
||||
return packetId
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.repository.UiPrefs
|
||||
|
||||
@Single
|
||||
open class SetAppIntroCompletedUseCase constructor(private val uiPrefs: UiPrefs) {
|
||||
operator fun invoke(value: Boolean) {
|
||||
uiPrefs.setAppIntroCompleted(value)
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.common.database.DatabaseManager
|
||||
import org.meshtastic.core.database.DatabaseConstants
|
||||
|
||||
/** Use case for setting the database cache limit. */
|
||||
@Single
|
||||
open class SetDatabaseCacheLimitUseCase constructor(private val databaseManager: DatabaseManager) {
|
||||
operator fun invoke(limit: Int) {
|
||||
val clamped = limit.coerceIn(DatabaseConstants.MIN_CACHE_LIMIT, DatabaseConstants.MAX_CACHE_LIMIT)
|
||||
databaseManager.setCacheLimit(clamped)
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.repository.UiPrefs
|
||||
|
||||
@Single
|
||||
open class SetLocaleUseCase constructor(private val uiPrefs: UiPrefs) {
|
||||
operator fun invoke(value: String) {
|
||||
uiPrefs.setLocale(value)
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.repository.NotificationPrefs
|
||||
|
||||
/** Use case for updating application-level notification preferences. */
|
||||
@Single
|
||||
class SetNotificationSettingsUseCase(private val notificationPrefs: NotificationPrefs) {
|
||||
fun setMessagesEnabled(enabled: Boolean) = notificationPrefs.setMessagesEnabled(enabled)
|
||||
|
||||
fun setNodeEventsEnabled(enabled: Boolean) = notificationPrefs.setNodeEventsEnabled(enabled)
|
||||
|
||||
fun setLowBatteryEnabled(enabled: Boolean) = notificationPrefs.setLowBatteryEnabled(enabled)
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.repository.UiPrefs
|
||||
|
||||
@Single
|
||||
open class SetProvideLocationUseCase constructor(private val uiPrefs: UiPrefs) {
|
||||
operator fun invoke(myNodeNum: Int, provideLocation: Boolean) {
|
||||
uiPrefs.setShouldProvideNodeLocation(myNodeNum, provideLocation)
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.repository.UiPrefs
|
||||
|
||||
@Single
|
||||
open class SetThemeUseCase constructor(private val uiPrefs: UiPrefs) {
|
||||
operator fun invoke(value: Int) {
|
||||
uiPrefs.setTheme(value)
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.repository.AnalyticsPrefs
|
||||
|
||||
/** Use case for toggling the analytics preference. */
|
||||
@Single
|
||||
open class ToggleAnalyticsUseCase constructor(private val analyticsPrefs: AnalyticsPrefs) {
|
||||
open operator fun invoke() {
|
||||
analyticsPrefs.setAnalyticsAllowed(!analyticsPrefs.analyticsAllowed.value)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user