mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-05-24 14:50:26 -04:00
refactor: remove AIDL API and modernize radio architecture
Remove the deprecated AIDL/IPC API surface and perform deep architectural
modernization of the radio command pipeline, aligning with the meshtastic-sdk
AdminApiImpl pattern for future SDK migration.
Key changes:
1. AIDL Removal & Infrastructure Cleanup
- Delete core:api module and all AIDL interfaces
- Remove ServiceBroadcasts + CommonParcelable infrastructure
- Remove core:api from CI workflow lint/publish steps
2. Model Modernization
- Introduce NodeAddress sealed class with type-safe addressing
- Remove deprecated DataPacket constants in favor of NodeAddress
- Consolidate dual node maps into single source with getNodeById
- Split large model files, deduplicate NodeEntity, flatten RadioController
3. Service Layer Refactoring (SDK-aligned)
- Remove ServiceAction sealed class, use direct suspend calls
- Convert CommandSender & MeshActionHandler to suspend APIs
- Merge MeshActionHandler into DirectRadioControllerImpl
(ViewModel → RadioController → CommandSender, no intermediate layer)
- Build AdminMessage protos directly with typed protos end-to-end
- Apply structured concurrency to NodeRequestActions/NodeManagementActions
- Fix CancellationException handling throughout
Architecture (before → after):
ViewModel → RadioController → Handler → CommandSender → PacketHandler
ViewModel → RadioController → CommandSender → PacketHandler
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -87,6 +87,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
|
||||
@@ -433,7 +434,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
|
||||
|
||||
@@ -50,6 +50,7 @@ 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.NodeAddress
|
||||
import org.meshtastic.core.model.RadioController
|
||||
import org.meshtastic.core.repository.MapPrefs
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
@@ -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 {
|
||||
|
||||
@@ -161,16 +161,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"
|
||||
@@ -272,7 +268,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
|
||||
|
||||
@@ -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() {}
|
||||
|
||||
@@ -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",
|
||||
@@ -115,7 +114,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. :core:proto contains only auto-generated Wire classes (no KDoc value) and
|
||||
@@ -128,6 +127,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" }
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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.")
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,7 +34,6 @@ 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.proto.DeviceMetadata
|
||||
import org.meshtastic.proto.FileInfo
|
||||
@@ -52,7 +51,6 @@ class MeshConfigFlowManagerImpl(
|
||||
private val nodeRepository: NodeRepository,
|
||||
private val radioConfigRepository: RadioConfigRepository,
|
||||
private val serviceRepository: ServiceRepository,
|
||||
private val serviceBroadcasts: ServiceBroadcasts,
|
||||
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" }
|
||||
@@ -191,7 +189,6 @@ class MeshConfigFlowManagerImpl(
|
||||
nodeManager.setNodeDbReady(true)
|
||||
nodeManager.setAllowNodeDbWrites(true)
|
||||
serviceRepository.setConnectionState(ConnectionState.Connected)
|
||||
serviceBroadcasts.broadcastConnection()
|
||||
connectionManager.value.onNodeDbReady()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,7 +53,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
|
||||
@@ -84,9 +88,8 @@ class MeshDataHandlerImpl(
|
||||
private val packetHandler: PacketHandler,
|
||||
private val serviceRepository: ServiceRepository,
|
||||
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) {
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -217,10 +217,8 @@ class MeshMessageProcessorImpl(
|
||||
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
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
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
|
||||
@@ -37,7 +36,6 @@ class MeshRouterImpl(
|
||||
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
|
||||
@@ -58,9 +56,6 @@ class MeshRouterImpl(
|
||||
override val mqttManager: MqttManager
|
||||
get() = mqttManagerLazy.value
|
||||
|
||||
override val actionHandler: MeshActionHandler
|
||||
get() = actionHandlerLazy.value
|
||||
|
||||
override val xmodemManager: XModemManager
|
||||
get() = xmodemManagerLazy.value
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ 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.proto.MeshPacket
|
||||
import org.meshtastic.proto.NeighborInfo
|
||||
@@ -35,7 +34,6 @@ import org.meshtastic.proto.NeighborInfo
|
||||
class NeighborInfoHandlerImpl(
|
||||
private val nodeManager: NodeManager,
|
||||
private val serviceRepository: ServiceRepository,
|
||||
private val serviceBroadcasts: ServiceBroadcasts,
|
||||
private val nodeRepository: NodeRepository,
|
||||
) : NeighborInfoHandler {
|
||||
|
||||
@@ -58,9 +56,6 @@ 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]
|
||||
|
||||
@@ -28,20 +28,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 +54,16 @@ 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>())
|
||||
|
||||
override val nodeDBbyNodeNum: Map<Int, Node>
|
||||
get() = _nodeDBbyNodeNum.value
|
||||
|
||||
override val nodeDBbyID: Map<String, Node>
|
||||
get() = _nodeDBbyID.value
|
||||
override fun getNodeById(id: String): Node? = _nodeDBbyNodeNum.value.values.firstOrNull { it.user.id == id }
|
||||
|
||||
override val isNodeDbReady = MutableStateFlow(false)
|
||||
override val allowNodeDbWrites = MutableStateFlow(false)
|
||||
@@ -105,9 +96,6 @@ class NodeManagerImpl(
|
||||
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)
|
||||
if (myNodeNum.value == null) {
|
||||
myNodeNum.value = nodeRepository.myNodeInfo.value?.myNodeNum
|
||||
}
|
||||
@@ -116,7 +104,6 @@ class NodeManagerImpl(
|
||||
|
||||
override fun clear() {
|
||||
_nodeDBbyNodeNum.value = persistentMapOf()
|
||||
_nodeDBbyID.value = persistentMapOf()
|
||||
isNodeDbReady.value = false
|
||||
allowNodeDbWrites.value = false
|
||||
myNodeNum.value = null
|
||||
@@ -149,21 +136,13 @@ class NodeManagerImpl(
|
||||
return _nodeDBbyNodeNum.value[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) } }
|
||||
_nodeDBbyNodeNum.update { it.remove(nodeNum) }
|
||||
}
|
||||
|
||||
internal fun getOrCreateNode(n: Int, channel: Int = 0): Node = _nodeDBbyNodeNum.value[n]
|
||||
?: run {
|
||||
val userId = DataPacket.nodeNumToDefaultId(n)
|
||||
val userId = NodeAddress.numToDefaultId(n)
|
||||
val defaultUser =
|
||||
User(
|
||||
id = userId,
|
||||
@@ -175,7 +154,7 @@ 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.
|
||||
@@ -187,17 +166,10 @@ class NodeManagerImpl(
|
||||
map.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 +259,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 +306,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)
|
||||
_nodeDBbyNodeNum.value[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
|
||||
@@ -47,7 +45,6 @@ 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
|
||||
@@ -61,7 +58,6 @@ 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,
|
||||
@@ -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,12 @@ 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)
|
||||
override suspend fun sendToRadio(packet: MeshPacket) {
|
||||
queueMutex.withLock {
|
||||
queueStopped = false
|
||||
queuedPackets.add(packet)
|
||||
startPacketQueueLocked()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("TooGenericExceptionCaught", "SwallowedException")
|
||||
@@ -247,7 +226,6 @@ class PacketHandlerImpl(
|
||||
getDataPacketById(packetId)?.let { p ->
|
||||
if (p.status == m) return@handledLaunch
|
||||
packetRepository.value.updateMessageStatus(p, m)
|
||||
serviceBroadcasts.broadcastMessageStatus(packetId, m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
@@ -99,7 +98,7 @@ class StoreForwardPacketHandlerImpl(
|
||||
encryptedPayload = sfpp.message.toByteArray(),
|
||||
to =
|
||||
if (sfpp.encapsulated_to == 0) {
|
||||
DataPacket.NODENUM_BROADCAST
|
||||
NodeAddress.NODENUM_BROADCAST
|
||||
} else {
|
||||
sfpp.encapsulated_to
|
||||
},
|
||||
@@ -125,7 +124,6 @@ class StoreForwardPacketHandlerImpl(
|
||||
rxTime = sfpp.encapsulated_rxtime.toLong() and 0xFFFFFFFFL,
|
||||
myNodeNum = nodeManager.myNodeNum.value ?: 0,
|
||||
)
|
||||
serviceBroadcasts.broadcastMessageStatus(sfpp.encapsulated_id, status)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +175,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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -153,13 +153,13 @@ class NodeRepositoryImpl(
|
||||
|
||||
val fallbackId = userId.takeLast(last4)
|
||||
val defaultLong =
|
||||
if (userId == DataPacket.ID_LOCAL) {
|
||||
if (NodeAddress.fromString(userId) is NodeAddress.Local) {
|
||||
ourNodeInfo.value?.user?.long_name?.takeIf { it.isNotBlank() } ?: "Local"
|
||||
} else {
|
||||
"Meshtastic $fallbackId"
|
||||
}
|
||||
val defaultShort =
|
||||
if (userId == DataPacket.ID_LOCAL) {
|
||||
if (NodeAddress.fromString(userId) is NodeAddress.Local) {
|
||||
ourNodeInfo.value?.user?.short_name?.takeIf { it.isNotBlank() } ?: "Local"
|
||||
} else {
|
||||
fallbackId
|
||||
|
||||
@@ -37,6 +37,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
|
||||
@@ -339,13 +340,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()
|
||||
@@ -353,7 +354,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 " +
|
||||
@@ -373,7 +374,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
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -101,7 +99,6 @@ class MeshConfigFlowManagerImplTest {
|
||||
nodeRepository = nodeRepository,
|
||||
radioConfigRepository = radioConfigRepository,
|
||||
serviceRepository = serviceRepository,
|
||||
serviceBroadcasts = serviceBroadcasts,
|
||||
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)
|
||||
|
||||
@@ -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)
|
||||
@@ -96,7 +95,6 @@ class MeshDataHandlerTest {
|
||||
packetHandler = packetHandler,
|
||||
serviceRepository = 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)
|
||||
|
||||
@@ -251,7 +251,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 +273,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 ----------
|
||||
|
||||
@@ -19,14 +19,7 @@ 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
|
||||
@@ -47,7 +40,6 @@ class MeshRouterImplTest {
|
||||
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 =
|
||||
@@ -70,7 +62,6 @@ class MeshRouterImplTest {
|
||||
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
|
||||
@@ -83,7 +74,6 @@ class MeshRouterImplTest {
|
||||
neighborInfoHandlerLazy = TrackingLazy { neighborInfoHandler }
|
||||
configFlowManagerLazy = TrackingLazy { configFlowManager }
|
||||
mqttManagerLazy = TrackingLazy { mqttManager }
|
||||
actionHandlerLazy = TrackingLazy { actionHandler }
|
||||
xmodemManagerLazy = TrackingLazy { xmodemManager }
|
||||
|
||||
router =
|
||||
@@ -94,36 +84,10 @@ class MeshRouterImplTest {
|
||||
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()
|
||||
@@ -131,31 +95,22 @@ class MeshRouterImplTest {
|
||||
router.tracerouteHandler.recordStartTime(77)
|
||||
|
||||
assertTrue(tracerouteHandlerLazy.isInitialized())
|
||||
assertFalse(actionHandlerLazy.isInitialized())
|
||||
assertFalse(dataHandlerLazy.isInitialized())
|
||||
verify { tracerouteHandler.recordStartTime(77) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `admin command routing uses the action handler lazily`() {
|
||||
fun `handlers are initialized independently`() {
|
||||
assertAllHandlersUninitialized()
|
||||
|
||||
router.actionHandler.handleGetRemoteConfig(id = 42, destNum = 67890, config = 7)
|
||||
|
||||
assertTrue(actionHandlerLazy.isInitialized())
|
||||
router.dataHandler
|
||||
assertTrue(dataHandlerLazy.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) }
|
||||
|
||||
router.configHandler
|
||||
assertTrue(configHandlerLazy.isInitialized())
|
||||
assertFalse(tracerouteHandlerLazy.isInitialized())
|
||||
}
|
||||
|
||||
private fun assertAllHandlersUninitialized() {
|
||||
@@ -165,7 +120,6 @@ class MeshRouterImplTest {
|
||||
assertFalse(neighborInfoHandlerLazy.isInitialized())
|
||||
assertFalse(configFlowManagerLazy.isInitialized())
|
||||
assertFalse(mqttManagerLazy.isInitialized())
|
||||
assertFalse(actionHandlerLazy.isInitialized())
|
||||
assertFalse(xmodemManagerLazy.isInitialized())
|
||||
}
|
||||
|
||||
|
||||
@@ -21,11 +21,10 @@ import dev.mokkery.mock
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import okio.ByteString
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
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 +42,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 +49,7 @@ class NodeManagerImplTest {
|
||||
|
||||
@BeforeTest
|
||||
fun setUp() {
|
||||
nodeManager = NodeManagerImpl(nodeRepository, serviceBroadcasts, notificationManager, testScope)
|
||||
nodeManager = NodeManagerImpl(nodeRepository, notificationManager, testScope)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -62,7 +60,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 +190,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 +216,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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 ----------
|
||||
|
||||
@@ -32,6 +32,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
|
||||
@@ -166,7 +167,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,7 @@ 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,
|
||||
)
|
||||
}
|
||||
fun toModel() = node.toModel().copy(metadata = metadata?.proto, manuallyVerified = node.manuallyVerified)
|
||||
|
||||
fun toEntity() = with(node) {
|
||||
NodeEntity(
|
||||
@@ -211,49 +181,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
|
||||
|
||||
@@ -29,9 +29,8 @@ import kotlinx.coroutines.withTimeoutOrNull
|
||||
import org.koin.core.annotation.Named
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.model.RadioController
|
||||
import org.meshtastic.core.model.SessionStatus
|
||||
import org.meshtastic.core.model.service.ServiceAction
|
||||
import org.meshtastic.core.repository.MeshActionHandler
|
||||
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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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.HomoglyphPrefs
|
||||
|
||||
/** Use case for toggling the homoglyph encoding preference. */
|
||||
@Single
|
||||
open class ToggleHomoglyphEncodingUseCase constructor(private val homoglyphEncodingPrefs: HomoglyphPrefs) {
|
||||
open operator fun invoke() {
|
||||
homoglyphEncodingPrefs.setHomoglyphEncodingEnabled(!homoglyphEncodingPrefs.homoglyphEncodingEnabled.value)
|
||||
}
|
||||
}
|
||||
@@ -34,9 +34,8 @@ import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okio.ByteString
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.model.RadioController
|
||||
import org.meshtastic.core.model.SessionStatus
|
||||
import org.meshtastic.core.model.service.ServiceAction
|
||||
import org.meshtastic.core.repository.MeshActionHandler
|
||||
import org.meshtastic.core.repository.ServiceRepository
|
||||
import org.meshtastic.core.repository.SessionManager
|
||||
import kotlin.test.Test
|
||||
@@ -68,9 +67,14 @@ class EnsureRemoteAdminSessionUseCaseTest {
|
||||
@Test
|
||||
fun `returns Disconnected without dispatching when not connected`() = runTest {
|
||||
val sessionManager = stubSessionManager()
|
||||
val handler = mock<MeshActionHandler>(MockMode.autofill)
|
||||
val controller = mock<RadioController>(MockMode.autofill)
|
||||
val useCase =
|
||||
EnsureRemoteAdminSessionUseCase(sessionManager, handler, connectedRepo(ConnectionState.Disconnected), this)
|
||||
EnsureRemoteAdminSessionUseCase(
|
||||
sessionManager,
|
||||
controller,
|
||||
connectedRepo(ConnectionState.Disconnected),
|
||||
this,
|
||||
)
|
||||
|
||||
val result = useCase(destNum)
|
||||
|
||||
@@ -81,8 +85,8 @@ class EnsureRemoteAdminSessionUseCaseTest {
|
||||
fun `returns AlreadyActive without dispatching when status already Active`() = runTest {
|
||||
val active = SessionStatus.Active(Clock.System.now())
|
||||
val sessionManager = stubSessionManager(initialStatus = active)
|
||||
val handler = mock<MeshActionHandler>(MockMode.autofill)
|
||||
val useCase = EnsureRemoteAdminSessionUseCase(sessionManager, handler, connectedRepo(), this)
|
||||
val controller = mock<RadioController>(MockMode.autofill)
|
||||
val useCase = EnsureRemoteAdminSessionUseCase(sessionManager, controller, connectedRepo(), this)
|
||||
|
||||
val result = useCase(destNum)
|
||||
|
||||
@@ -93,30 +97,30 @@ class EnsureRemoteAdminSessionUseCaseTest {
|
||||
fun `dispatches metadata request and returns Refreshed when refresh flow emits`() = runTest {
|
||||
val refresh = MutableSharedFlow<Int>(extraBufferCapacity = 8)
|
||||
val sessionManager = stubSessionManager(refreshFlow = refresh)
|
||||
val handler = mock<MeshActionHandler>(MockMode.autofill)
|
||||
val controller = mock<RadioController>(MockMode.autofill)
|
||||
// Simulate the radio responding by emitting on the refresh flow when the metadata request fires.
|
||||
everySuspend { handler.onServiceAction(any()) } calls
|
||||
everySuspend { controller.refreshMetadata(any()) } calls
|
||||
{
|
||||
refresh.tryEmit(destNum)
|
||||
Unit
|
||||
}
|
||||
|
||||
val useCase = EnsureRemoteAdminSessionUseCase(sessionManager, handler, connectedRepo(), this)
|
||||
val useCase = EnsureRemoteAdminSessionUseCase(sessionManager, controller, connectedRepo(), this)
|
||||
|
||||
val result = useCase(destNum)
|
||||
|
||||
assertEquals(EnsureSessionResult.Refreshed, result)
|
||||
verifySuspend { handler.onServiceAction(ServiceAction.GetDeviceMetadata(destNum)) }
|
||||
verifySuspend { controller.refreshMetadata(destNum) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns Timeout when no refresh arrives within deadline`() = runTest {
|
||||
val refresh = MutableSharedFlow<Int>(extraBufferCapacity = 8)
|
||||
val sessionManager = stubSessionManager(refreshFlow = refresh)
|
||||
val handler = mock<MeshActionHandler>(MockMode.autofill)
|
||||
everySuspend { handler.onServiceAction(any()) } returns Unit
|
||||
val controller = mock<RadioController>(MockMode.autofill)
|
||||
everySuspend { controller.refreshMetadata(any()) } returns Unit
|
||||
|
||||
val useCase = EnsureRemoteAdminSessionUseCase(sessionManager, handler, connectedRepo(), this)
|
||||
val useCase = EnsureRemoteAdminSessionUseCase(sessionManager, controller, connectedRepo(), this)
|
||||
|
||||
var observed: EnsureSessionResult? = null
|
||||
val job = launch { observed = useCase(destNum) }
|
||||
|
||||
@@ -1,46 +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.meshtastic.core.testing.FakeRadioController
|
||||
import kotlin.test.BeforeTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class MeshLocationUseCaseTest {
|
||||
|
||||
private lateinit var radioController: FakeRadioController
|
||||
private lateinit var useCase: MeshLocationUseCase
|
||||
|
||||
@BeforeTest
|
||||
fun setUp() {
|
||||
radioController = FakeRadioController()
|
||||
useCase = MeshLocationUseCase(radioController)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `startProvidingLocation calls radioController`() {
|
||||
useCase.startProvidingLocation()
|
||||
assertTrue(radioController.startProvideLocationCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `stopProvidingLocation calls radioController`() {
|
||||
useCase.stopProvidingLocation()
|
||||
assertTrue(radioController.stopProvideLocationCalled)
|
||||
}
|
||||
}
|
||||
@@ -1,49 +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 dev.mokkery.mock
|
||||
import dev.mokkery.verify
|
||||
import org.meshtastic.core.common.database.DatabaseManager
|
||||
import org.meshtastic.core.database.DatabaseConstants
|
||||
import kotlin.test.BeforeTest
|
||||
import kotlin.test.Test
|
||||
|
||||
class SetDatabaseCacheLimitUseCaseTest {
|
||||
|
||||
private lateinit var databaseManager: DatabaseManager
|
||||
private lateinit var useCase: SetDatabaseCacheLimitUseCase
|
||||
|
||||
@BeforeTest
|
||||
fun setUp() {
|
||||
databaseManager = mock(dev.mokkery.MockMode.autofill)
|
||||
useCase = SetDatabaseCacheLimitUseCase(databaseManager)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke calls setCacheLimit with clamped value`() {
|
||||
// Act & Assert
|
||||
useCase(0)
|
||||
verify { databaseManager.setCacheLimit(DatabaseConstants.MIN_CACHE_LIMIT) }
|
||||
|
||||
useCase(100)
|
||||
verify { databaseManager.setCacheLimit(DatabaseConstants.MAX_CACHE_LIMIT) }
|
||||
|
||||
useCase(5)
|
||||
verify { databaseManager.setCacheLimit(5) }
|
||||
}
|
||||
}
|
||||
@@ -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.domain.usecase.settings
|
||||
|
||||
import dev.mokkery.answering.returns
|
||||
import dev.mokkery.every
|
||||
import dev.mokkery.matcher.any
|
||||
import dev.mokkery.mock
|
||||
import dev.mokkery.verify
|
||||
import org.meshtastic.core.repository.NotificationPrefs
|
||||
import kotlin.test.BeforeTest
|
||||
import kotlin.test.Test
|
||||
|
||||
class SetNotificationSettingsUseCaseTest {
|
||||
|
||||
private val notificationPrefs: NotificationPrefs = mock()
|
||||
private lateinit var useCase: SetNotificationSettingsUseCase
|
||||
|
||||
@BeforeTest
|
||||
fun setUp() {
|
||||
useCase = SetNotificationSettingsUseCase(notificationPrefs)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `setMessagesEnabled calls notificationPrefs`() {
|
||||
every { notificationPrefs.setMessagesEnabled(any()) } returns Unit
|
||||
useCase.setMessagesEnabled(true)
|
||||
verify { notificationPrefs.setMessagesEnabled(true) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `setNodeEventsEnabled calls notificationPrefs`() {
|
||||
every { notificationPrefs.setNodeEventsEnabled(any()) } returns Unit
|
||||
useCase.setNodeEventsEnabled(false)
|
||||
verify { notificationPrefs.setNodeEventsEnabled(false) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `setLowBatteryEnabled calls notificationPrefs`() {
|
||||
every { notificationPrefs.setLowBatteryEnabled(any()) } returns Unit
|
||||
useCase.setLowBatteryEnabled(true)
|
||||
verify { notificationPrefs.setLowBatteryEnabled(true) }
|
||||
}
|
||||
}
|
||||
@@ -1,48 +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.meshtastic.core.testing.FakeAnalyticsPrefs
|
||||
import kotlin.test.BeforeTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class ToggleAnalyticsUseCaseTest {
|
||||
|
||||
private lateinit var analyticsPrefs: FakeAnalyticsPrefs
|
||||
private lateinit var useCase: ToggleAnalyticsUseCase
|
||||
|
||||
@BeforeTest
|
||||
fun setUp() {
|
||||
analyticsPrefs = FakeAnalyticsPrefs()
|
||||
useCase = ToggleAnalyticsUseCase(analyticsPrefs)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke toggles from false to true`() {
|
||||
analyticsPrefs.setAnalyticsAllowed(false)
|
||||
useCase()
|
||||
assertEquals(true, analyticsPrefs.analyticsAllowed.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke toggles from true to false`() {
|
||||
analyticsPrefs.setAnalyticsAllowed(true)
|
||||
useCase()
|
||||
assertEquals(false, analyticsPrefs.analyticsAllowed.value)
|
||||
}
|
||||
}
|
||||
@@ -1,48 +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.meshtastic.core.testing.FakeHomoglyphPrefs
|
||||
import kotlin.test.BeforeTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class ToggleHomoglyphEncodingUseCaseTest {
|
||||
|
||||
private lateinit var homoglyphPrefs: FakeHomoglyphPrefs
|
||||
private lateinit var useCase: ToggleHomoglyphEncodingUseCase
|
||||
|
||||
@BeforeTest
|
||||
fun setUp() {
|
||||
homoglyphPrefs = FakeHomoglyphPrefs()
|
||||
useCase = ToggleHomoglyphEncodingUseCase(homoglyphPrefs)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke toggles from false to true`() {
|
||||
homoglyphPrefs.setHomoglyphEncodingEnabled(false)
|
||||
useCase()
|
||||
assertEquals(true, homoglyphPrefs.homoglyphEncodingEnabled.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke toggles from true to false`() {
|
||||
homoglyphPrefs.setHomoglyphEncodingEnabled(true)
|
||||
useCase()
|
||||
assertEquals(false, homoglyphPrefs.homoglyphEncodingEnabled.value)
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ Models in this module use the `CommonParcelable` and `CommonParcelize` abstracti
|
||||
- **`Channel`**: Represents a mesh channel configuration.
|
||||
|
||||
## Usage
|
||||
This module is a core dependency of `core:api` and most feature modules.
|
||||
This module is a core dependency of most feature modules.
|
||||
|
||||
```kotlin
|
||||
// In commonMain
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
plugins {
|
||||
alias(libs.plugins.meshtastic.kmp.library)
|
||||
alias(libs.plugins.meshtastic.kotlinx.serialization)
|
||||
alias(libs.plugins.kotlin.parcelize)
|
||||
id("meshtastic.kmp.jvm.android")
|
||||
id("meshtastic.publishing")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* 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.model
|
||||
|
||||
/**
|
||||
* Device configuration and control operations.
|
||||
*
|
||||
* Mirrors the SDK's `AdminApi` interface — local and remote configuration, channel management, owner identity, device
|
||||
* lifecycle commands, and batch edit sessions. When the SDK is adopted, this interface becomes the adapter boundary:
|
||||
* implementations delegate to `RadioClient.admin`.
|
||||
*
|
||||
* @see RadioController which extends this interface for backward compatibility
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
interface AdminController {
|
||||
|
||||
// ── Local configuration ─────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Updates the local radio configuration.
|
||||
*
|
||||
* Fire-and-forget by design: the device is the source of truth. Local persistence is an optimistic cache that will
|
||||
* self-heal on next config refresh.
|
||||
*/
|
||||
suspend fun setLocalConfig(config: org.meshtastic.proto.Config)
|
||||
|
||||
/** Updates a local radio channel. Same fire-and-forget contract as [setLocalConfig]. */
|
||||
suspend fun setLocalChannel(channel: org.meshtastic.proto.Channel)
|
||||
|
||||
// ── Remote configuration ────────────────────────────────────────────────
|
||||
|
||||
/** Updates the owner (user info) on a remote node. */
|
||||
suspend fun setOwner(destNum: Int, user: org.meshtastic.proto.User, packetId: Int)
|
||||
|
||||
/** Updates the general configuration on a remote node. */
|
||||
suspend fun setConfig(destNum: Int, config: org.meshtastic.proto.Config, packetId: Int)
|
||||
|
||||
/** Updates a module configuration on a remote node. */
|
||||
suspend fun setModuleConfig(destNum: Int, config: org.meshtastic.proto.ModuleConfig, packetId: Int)
|
||||
|
||||
/** Updates a channel configuration on a remote node. */
|
||||
suspend fun setRemoteChannel(destNum: Int, channel: org.meshtastic.proto.Channel, packetId: Int)
|
||||
|
||||
/** Sets a fixed position on a remote node. */
|
||||
suspend fun setFixedPosition(destNum: Int, position: Position)
|
||||
|
||||
/** Updates the notification ringtone on a remote node. */
|
||||
suspend fun setRingtone(destNum: Int, ringtone: String)
|
||||
|
||||
/** Updates the canned messages configuration on a remote node. */
|
||||
suspend fun setCannedMessages(destNum: Int, messages: String)
|
||||
|
||||
// ── Remote queries ──────────────────────────────────────────────────────
|
||||
|
||||
/** Requests the current owner (user info) from a remote node. */
|
||||
suspend fun getOwner(destNum: Int, packetId: Int)
|
||||
|
||||
/** Requests a specific configuration section from a remote node. */
|
||||
suspend fun getConfig(destNum: Int, configType: Int, packetId: Int)
|
||||
|
||||
/** Requests a module configuration section from a remote node. */
|
||||
suspend fun getModuleConfig(destNum: Int, moduleConfigType: Int, packetId: Int)
|
||||
|
||||
/** Requests a specific channel configuration from a remote node. */
|
||||
suspend fun getChannel(destNum: Int, index: Int, packetId: Int)
|
||||
|
||||
/** Requests the current ringtone from a remote node. */
|
||||
suspend fun getRingtone(destNum: Int, packetId: Int)
|
||||
|
||||
/** Requests the current canned messages from a remote node. */
|
||||
suspend fun getCannedMessages(destNum: Int, packetId: Int)
|
||||
|
||||
/** Requests the hardware connection status from a remote node. */
|
||||
suspend fun getDeviceConnectionStatus(destNum: Int, packetId: Int)
|
||||
|
||||
// ── Device lifecycle ────────────────────────────────────────────────────
|
||||
|
||||
/** Commands a node to reboot. */
|
||||
suspend fun reboot(destNum: Int, packetId: Int)
|
||||
|
||||
/** Commands a node to reboot into DFU mode. */
|
||||
suspend fun rebootToDfu(nodeNum: Int)
|
||||
|
||||
/** Initiates an OTA reboot request. */
|
||||
suspend fun requestRebootOta(requestId: Int, destNum: Int, mode: Int, hash: ByteArray?)
|
||||
|
||||
/** Commands a node to shut down. */
|
||||
suspend fun shutdown(destNum: Int, packetId: Int)
|
||||
|
||||
/** Performs a factory reset on a node. */
|
||||
suspend fun factoryReset(destNum: Int, packetId: Int)
|
||||
|
||||
/** Resets the NodeDB on a node. */
|
||||
suspend fun nodedbReset(destNum: Int, packetId: Int, preserveFavorites: Boolean)
|
||||
|
||||
// ── Batch edit ──────────────────────────────────────────────────────────
|
||||
|
||||
/** Signals the start of a batch configuration session. */
|
||||
suspend fun beginEditSettings(destNum: Int)
|
||||
|
||||
/** Commits all pending configuration changes in a batch session. */
|
||||
suspend fun commitEditSettings(destNum: Int)
|
||||
}
|
||||
@@ -16,10 +16,6 @@
|
||||
*/
|
||||
package org.meshtastic.core.model
|
||||
|
||||
import org.meshtastic.core.common.util.CommonParcelable
|
||||
import org.meshtastic.core.common.util.CommonParcelize
|
||||
|
||||
@CommonParcelize
|
||||
data class Contact(
|
||||
val contactKey: String,
|
||||
val shortName: String,
|
||||
@@ -31,7 +27,7 @@ data class Contact(
|
||||
val isMuted: Boolean,
|
||||
val isUnmessageable: Boolean,
|
||||
val nodeColors: Pair<Int, Int>? = null,
|
||||
) : CommonParcelable
|
||||
)
|
||||
|
||||
data class ContactSettings(
|
||||
val contactKey: String,
|
||||
|
||||
@@ -16,25 +16,16 @@
|
||||
*/
|
||||
package org.meshtastic.core.model
|
||||
|
||||
import co.touchlab.kermit.Logger
|
||||
import kotlinx.serialization.Serializable
|
||||
import okio.ByteString
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.meshtastic.core.common.util.CommonIgnoredOnParcel
|
||||
import org.meshtastic.core.common.util.CommonParcel
|
||||
import org.meshtastic.core.common.util.CommonParcelable
|
||||
import org.meshtastic.core.common.util.CommonParcelize
|
||||
import org.meshtastic.core.common.util.CommonTypeParceler
|
||||
import org.meshtastic.core.common.util.formatString
|
||||
import org.meshtastic.core.common.util.nowMillis
|
||||
import org.meshtastic.core.model.util.ByteStringParceler
|
||||
import org.meshtastic.core.model.util.ByteStringSerializer
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.PortNum
|
||||
import org.meshtastic.proto.Waypoint
|
||||
|
||||
@CommonParcelize
|
||||
enum class MessageStatus : CommonParcelable {
|
||||
enum class MessageStatus {
|
||||
UNKNOWN, // Not set for this message
|
||||
RECEIVED, // Came in from the mesh
|
||||
QUEUED, // Waiting to send to the mesh as soon as we connect to the device
|
||||
@@ -45,17 +36,14 @@ enum class MessageStatus : CommonParcelable {
|
||||
ERROR, // We received back a nak, message not delivered
|
||||
}
|
||||
|
||||
/** A parcelable version of the protobuf MeshPacket + Data subpacket. */
|
||||
/** A data class version of the protobuf MeshPacket + Data subpacket. */
|
||||
@Serializable
|
||||
@CommonParcelize
|
||||
data class DataPacket(
|
||||
var to: String? = ID_BROADCAST, // a nodeID string, or ID_BROADCAST for broadcast
|
||||
@Serializable(with = ByteStringSerializer::class)
|
||||
@CommonTypeParceler<ByteString?, ByteStringParceler>
|
||||
var bytes: ByteString?,
|
||||
var to: String? = NodeAddress.ID_BROADCAST,
|
||||
@Serializable(with = ByteStringSerializer::class) var bytes: ByteString?,
|
||||
// A port number for this packet
|
||||
var dataType: Int,
|
||||
var from: String? = ID_LOCAL, // a nodeID string, or ID_LOCAL for localhost
|
||||
var from: String? = NodeAddress.ID_LOCAL,
|
||||
var time: Long = nowMillis, // msecs since 1970
|
||||
var id: Int = 0, // 0 means unassigned
|
||||
var status: MessageStatus? = MessageStatus.UNKNOWN,
|
||||
@@ -70,54 +58,13 @@ data class DataPacket(
|
||||
var relays: Int = 0,
|
||||
var viaMqtt: Boolean = false, // True if this packet passed via MQTT somewhere along its path
|
||||
var emoji: Int = 0,
|
||||
@Serializable(with = ByteStringSerializer::class)
|
||||
@CommonTypeParceler<ByteString?, ByteStringParceler>
|
||||
var sfppHash: ByteString? = null,
|
||||
@Serializable(with = ByteStringSerializer::class) var sfppHash: ByteString? = null,
|
||||
/** The transport mechanism this packet arrived over (see [MeshPacket.TransportMechanism]). */
|
||||
var transportMechanism: Int = 0,
|
||||
) : CommonParcelable {
|
||||
|
||||
fun readFromParcel(parcel: CommonParcel) {
|
||||
to = parcel.readString()
|
||||
bytes = ByteStringParceler.create(parcel)
|
||||
dataType = parcel.readInt()
|
||||
from = parcel.readString()
|
||||
time = parcel.readLong()
|
||||
id = parcel.readInt()
|
||||
|
||||
// MessageStatus is a known Parcelable type (enum), so Parcelize writes it optimized:
|
||||
// 1. Presence flag (Int: 1 or 0)
|
||||
// 2. Content (Enum Name as String)
|
||||
status =
|
||||
if (parcel.readInt() != 0) {
|
||||
val name = parcel.readString()
|
||||
try {
|
||||
if (name != null) MessageStatus.valueOf(name) else MessageStatus.UNKNOWN
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Logger.w(e) { "Unknown MessageStatus: $name" }
|
||||
MessageStatus.UNKNOWN
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
hopLimit = parcel.readInt()
|
||||
channel = parcel.readInt()
|
||||
wantAck = (parcel.readInt() != 0)
|
||||
hopStart = parcel.readInt()
|
||||
snr = parcel.readFloat()
|
||||
rssi = parcel.readInt()
|
||||
replyId = if (parcel.readInt() == 0) null else parcel.readInt()
|
||||
relayNode = if (parcel.readInt() == 0) null else parcel.readInt()
|
||||
relays = parcel.readInt()
|
||||
viaMqtt = (parcel.readInt() != 0)
|
||||
emoji = parcel.readInt()
|
||||
sfppHash = ByteStringParceler.create(parcel)
|
||||
transportMechanism = parcel.readInt()
|
||||
}
|
||||
) {
|
||||
|
||||
/** If there was an error with this message, this string describes what was wrong. */
|
||||
@CommonIgnoredOnParcel var errorMessage: String? = null
|
||||
var errorMessage: String? = null
|
||||
|
||||
/** Syntactic sugar to make it easy to create text messages */
|
||||
constructor(
|
||||
@@ -176,24 +123,5 @@ data class DataPacket(
|
||||
val hopsAway: Int
|
||||
get() = if (hopStart == 0 || (hopLimit > hopStart)) -1 else hopStart - hopLimit
|
||||
|
||||
companion object {
|
||||
// Special node IDs that can be used for sending messages
|
||||
|
||||
/** the Node ID for broadcast destinations */
|
||||
const val ID_BROADCAST = "^all"
|
||||
|
||||
/** The Node ID for the local node - used for from when sender doesn't know our local node ID */
|
||||
const val ID_LOCAL = "^local"
|
||||
|
||||
// special broadcast address
|
||||
const val NODENUM_BROADCAST = (0xffffffff).toInt()
|
||||
|
||||
// Public-key cryptography (PKC) channel index
|
||||
const val PKC_CHANNEL_INDEX = 8
|
||||
|
||||
fun nodeNumToDefaultId(n: Int): String = formatString("!%08x", n)
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun idToDefaultNodeNum(id: String?): Int? = runCatching { id?.toLong(16)?.toInt() }.getOrNull()
|
||||
}
|
||||
companion object
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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.model
|
||||
|
||||
import org.meshtastic.core.common.util.nowSeconds
|
||||
|
||||
data class DeviceMetrics(
|
||||
val time: Int = currentTime(), // default to current time in secs (NOT MILLISECONDS!)
|
||||
val batteryLevel: Int = 0,
|
||||
val voltage: Float,
|
||||
val channelUtilization: Float,
|
||||
val airUtilTx: Float,
|
||||
val uptimeSeconds: Int,
|
||||
) {
|
||||
companion object {
|
||||
@Suppress("MagicNumber")
|
||||
fun currentTime() = nowSeconds.toInt()
|
||||
}
|
||||
|
||||
/** Create our model object from a protobuf. */
|
||||
constructor(
|
||||
p: org.meshtastic.proto.DeviceMetrics,
|
||||
telemetryTime: Int = currentTime(),
|
||||
) : this(
|
||||
telemetryTime,
|
||||
p.battery_level ?: 0,
|
||||
p.voltage ?: 0f,
|
||||
p.channel_utilization ?: 0f,
|
||||
p.air_util_tx ?: 0f,
|
||||
p.uptime_seconds ?: 0,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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.model
|
||||
|
||||
import org.meshtastic.core.common.util.nowSeconds
|
||||
|
||||
data class EnvironmentMetrics(
|
||||
val time: Int = currentTime(), // default to current time in secs (NOT MILLISECONDS!)
|
||||
val temperature: Float?,
|
||||
val relativeHumidity: Float?,
|
||||
val soilTemperature: Float?,
|
||||
val soilMoisture: Int?,
|
||||
val barometricPressure: Float?,
|
||||
val gasResistance: Float?,
|
||||
val voltage: Float?,
|
||||
val current: Float?,
|
||||
val iaq: Int?,
|
||||
val lux: Float? = null,
|
||||
val uvLux: Float? = null,
|
||||
) {
|
||||
@Suppress("MagicNumber")
|
||||
companion object {
|
||||
fun currentTime() = nowSeconds.toInt()
|
||||
|
||||
fun fromTelemetryProto(proto: org.meshtastic.proto.EnvironmentMetrics, time: Int): EnvironmentMetrics =
|
||||
EnvironmentMetrics(
|
||||
temperature = proto.temperature?.takeIf { !it.isNaN() },
|
||||
relativeHumidity = proto.relative_humidity?.takeIf { !it.isNaN() && it != 0.0f },
|
||||
soilTemperature = proto.soil_temperature?.takeIf { !it.isNaN() },
|
||||
soilMoisture = proto.soil_moisture?.takeIf { it != Int.MIN_VALUE },
|
||||
barometricPressure = proto.barometric_pressure?.takeIf { !it.isNaN() },
|
||||
gasResistance = proto.gas_resistance?.takeIf { !it.isNaN() },
|
||||
voltage = proto.voltage?.takeIf { !it.isNaN() },
|
||||
current = proto.current?.takeIf { !it.isNaN() },
|
||||
iaq = proto.iaq?.takeIf { it != Int.MIN_VALUE },
|
||||
lux = proto.lux?.takeIf { !it.isNaN() },
|
||||
uvLux = proto.uv_lux?.takeIf { !it.isNaN() },
|
||||
time = time,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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.model
|
||||
|
||||
import org.meshtastic.core.model.util.anonymize
|
||||
import org.meshtastic.proto.HardwareModel
|
||||
|
||||
data class MeshUser(
|
||||
val id: String,
|
||||
val longName: String,
|
||||
val shortName: String,
|
||||
val hwModel: HardwareModel,
|
||||
val isLicensed: Boolean = false,
|
||||
val role: Int = 0,
|
||||
) {
|
||||
|
||||
override fun toString(): String = "MeshUser(id=${id.anonymize}, " +
|
||||
"longName=${longName.anonymize}, " +
|
||||
"shortName=${shortName.anonymize}, " +
|
||||
"hwModel=$hwModelString, " +
|
||||
"isLicensed=$isLicensed, " +
|
||||
"role=$role)"
|
||||
|
||||
/** Create our model object from a protobuf. */
|
||||
constructor(
|
||||
p: org.meshtastic.proto.User,
|
||||
) : this(p.id, p.long_name, p.short_name, p.hw_model, p.is_licensed, p.role.value)
|
||||
|
||||
/**
|
||||
* a string version of the hardware model, converted into pretty lowercase and changing _ to -, and p to dot or null
|
||||
* if unset
|
||||
*/
|
||||
val hwModelString: String?
|
||||
get() =
|
||||
if (hwModel == HardwareModel.UNSET) {
|
||||
null
|
||||
} else {
|
||||
hwModel.name.replace('_', '-').replace('p', '.').lowercase()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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.model
|
||||
|
||||
/**
|
||||
* Messaging operations — sending data packets, reactions, and shared contacts.
|
||||
*
|
||||
* Mirrors the SDK's send/messaging surface. When the SDK is adopted, implementations delegate to `RadioClient.send()` /
|
||||
* `RadioClient.sendText()`.
|
||||
*
|
||||
* @see RadioController which extends this interface for backward compatibility
|
||||
*/
|
||||
interface MessagingController {
|
||||
|
||||
/** Sends a data packet to the mesh. */
|
||||
suspend fun sendMessage(packet: DataPacket)
|
||||
|
||||
/** Sends an emoji reaction to a message. Awaits local DB persistence. */
|
||||
suspend fun sendReaction(emoji: String, replyId: Int, contactKey: String)
|
||||
|
||||
/** Imports a shared contact into the firmware's NodeDB. */
|
||||
suspend fun importContact(contact: org.meshtastic.proto.SharedContact)
|
||||
|
||||
/**
|
||||
* Sends our shared contact information (identity and public key) to the firmware's NodeDB.
|
||||
*
|
||||
* @param nodeNum The destination node number.
|
||||
* @return `true` if the radio accepted the contact, `false` on timeout or failure.
|
||||
*/
|
||||
suspend fun sendSharedContact(nodeNum: Int): Boolean
|
||||
}
|
||||
@@ -16,11 +16,7 @@
|
||||
*/
|
||||
package org.meshtastic.core.model
|
||||
|
||||
import org.meshtastic.core.common.util.CommonParcelable
|
||||
import org.meshtastic.core.common.util.CommonParcelize
|
||||
|
||||
// MyNodeInfo sent via special protobuf from radio
|
||||
@CommonParcelize
|
||||
data class MyNodeInfo(
|
||||
val myNodeNum: Int,
|
||||
val hasGPS: Boolean,
|
||||
@@ -37,7 +33,7 @@ data class MyNodeInfo(
|
||||
val airUtilTx: Float,
|
||||
val deviceId: String?,
|
||||
val pioEnv: String? = null,
|
||||
) : CommonParcelable {
|
||||
) {
|
||||
/** A human readable description of the software/hardware version */
|
||||
val firmwareString: String
|
||||
get() = "$model $firmwareVersion"
|
||||
|
||||
@@ -209,7 +209,7 @@ data class Node(
|
||||
|
||||
/** Creates a fallback [Node] when the node is not found in the database. */
|
||||
fun createFallback(nodeNum: Int, fallbackNamePrefix: String): Node {
|
||||
val userId = DataPacket.nodeNumToDefaultId(nodeNum)
|
||||
val userId = NodeAddress.numToDefaultId(nodeNum)
|
||||
val safeUserId = userId.padStart(DEFAULT_ID_SUFFIX_LENGTH, '0').takeLast(DEFAULT_ID_SUFFIX_LENGTH)
|
||||
val longName = "$fallbackNamePrefix $safeUserId"
|
||||
val defaultUser =
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* 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.model
|
||||
|
||||
import org.meshtastic.core.common.util.formatString
|
||||
import kotlin.jvm.JvmInline
|
||||
|
||||
/**
|
||||
* Type-safe representation of a mesh node address.
|
||||
*
|
||||
* Replaces stringly-typed node addressing (`"^all"`, `"^local"`, `"!hexid"`) with exhaustive sealed dispatch, enabling
|
||||
* compile-time verification of address handling.
|
||||
*/
|
||||
sealed class NodeAddress {
|
||||
/** Broadcast to all nodes in the mesh. */
|
||||
data object Broadcast : NodeAddress()
|
||||
|
||||
/** The local node (used as `from` when the sender's ID is unknown). */
|
||||
data object Local : NodeAddress()
|
||||
|
||||
/** Address by numeric node number (the canonical mesh-level identifier). */
|
||||
data class ByNum(val num: Int) : NodeAddress()
|
||||
|
||||
/** Address by hex string ID (e.g. `"!a1b2c3d4"`). */
|
||||
data class ById(val id: String) : NodeAddress()
|
||||
|
||||
/** Convert back to the legacy string representation used in [DataPacket]. */
|
||||
fun toIdString(): String = when (this) {
|
||||
Broadcast -> ID_BROADCAST
|
||||
Local -> ID_LOCAL
|
||||
is ByNum -> numToDefaultId(num)
|
||||
is ById -> id
|
||||
}
|
||||
|
||||
/** Build a [ContactKey] for this address on the given [channel]. */
|
||||
fun toContactKey(channel: Int): ContactKey = ContactKey("$channel${toIdString()}")
|
||||
|
||||
companion object {
|
||||
/** The broadcast address string `"^all"`. */
|
||||
const val ID_BROADCAST = "^all"
|
||||
|
||||
/** The local node address string `"^local"`. */
|
||||
const val ID_LOCAL = "^local"
|
||||
|
||||
/** The broadcast node number (`0xFFFFFFFF`). */
|
||||
@Suppress("MagicNumber")
|
||||
const val NODENUM_BROADCAST = (0xffffffff).toInt()
|
||||
|
||||
/** Public-key cryptography (PKC) channel index. */
|
||||
const val PKC_CHANNEL_INDEX = 8
|
||||
|
||||
private const val NODE_ID_PREFIX = "!"
|
||||
private const val HEX_RADIX = 16
|
||||
|
||||
/** Parse a legacy string address into a typed [NodeAddress]. */
|
||||
fun fromString(id: String?): NodeAddress = when {
|
||||
id == null || id == ID_BROADCAST -> Broadcast
|
||||
|
||||
id == ID_LOCAL -> Local
|
||||
|
||||
id.startsWith(NODE_ID_PREFIX) -> {
|
||||
val num = idToNum(id.removePrefix(NODE_ID_PREFIX))
|
||||
if (num != null) ByNum(num) else ById(id)
|
||||
}
|
||||
|
||||
else -> ById(id)
|
||||
}
|
||||
|
||||
/** Convert a node number to its canonical hex string ID (e.g. `"!a1b2c3d4"`). */
|
||||
fun numToDefaultId(n: Int): String = formatString("!%08x", n)
|
||||
|
||||
/** Parse a hex node ID string (with or without `!` prefix) to its integer value, or null. */
|
||||
@Suppress("MagicNumber")
|
||||
fun idToNum(id: String?): Int? =
|
||||
runCatching { id?.removePrefix(NODE_ID_PREFIX)?.toLong(HEX_RADIX)?.toInt() }.getOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Type-safe wrapper for contact key strings (channel index + node address).
|
||||
*
|
||||
* Contact keys are persisted as strings in the format `"<channel><nodeId>"` (e.g. `"0^all"`, `"1!a1b2c3d4"`).
|
||||
*/
|
||||
@JvmInline
|
||||
value class ContactKey(val value: String) {
|
||||
/** The channel index (first character). */
|
||||
val channel: Int
|
||||
get() = value[0].digitToInt()
|
||||
|
||||
/** The node address portion (everything after the channel digit). */
|
||||
val addressString: String
|
||||
get() = value.substring(1)
|
||||
|
||||
/** Parsed [NodeAddress] for the contact. */
|
||||
val address: NodeAddress
|
||||
get() = NodeAddress.fromString(addressString)
|
||||
|
||||
companion object {
|
||||
/** Create a broadcast contact key for the given channel. */
|
||||
fun broadcast(channel: Int = 0): ContactKey = NodeAddress.Broadcast.toContactKey(channel)
|
||||
}
|
||||
}
|
||||
|
||||
/** Type-safe interpretation of [DataPacket.to]. */
|
||||
val DataPacket.destination: NodeAddress
|
||||
get() = NodeAddress.fromString(to)
|
||||
|
||||
/** Type-safe interpretation of [DataPacket.from]. */
|
||||
val DataPacket.source: NodeAddress
|
||||
get() = NodeAddress.fromString(from)
|
||||
|
||||
/** Checks whether this packet originated from the local device. */
|
||||
fun DataPacket.isFromLocal(myNodeNum: Int? = null): Boolean {
|
||||
val src = source
|
||||
return src is NodeAddress.Local || (myNodeNum != null && src is NodeAddress.ByNum && src.num == myNodeNum)
|
||||
}
|
||||
|
||||
/** Checks whether this packet is addressed to the broadcast channel. */
|
||||
val DataPacket.isBroadcast: Boolean
|
||||
get() = destination is NodeAddress.Broadcast
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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.model
|
||||
|
||||
/**
|
||||
* Node management operations — favorite, ignore, mute, and remove nodes.
|
||||
*
|
||||
* Mirrors the node management subset of the SDK's `AdminApi` (setFavorite, setIgnored, toggleMuted). When the SDK is
|
||||
* adopted, implementations delegate to `RadioClient.admin.setFavorite(NodeId, Boolean)` etc.
|
||||
*
|
||||
* @see RadioController which extends this interface for backward compatibility
|
||||
*/
|
||||
interface NodeController {
|
||||
|
||||
/** Toggles the favorite status of a node on the radio. */
|
||||
suspend fun favoriteNode(nodeNum: Int)
|
||||
|
||||
/** Toggles the ignore status of a node on the radio. */
|
||||
suspend fun ignoreNode(nodeNum: Int)
|
||||
|
||||
/** Toggles the mute status of a node on the radio. */
|
||||
suspend fun muteNode(nodeNum: Int)
|
||||
|
||||
/** Removes a node from the mesh by its node number. */
|
||||
suspend fun removeByNodenum(packetId: Int, nodeNum: Int)
|
||||
}
|
||||
@@ -1,275 +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.model
|
||||
|
||||
import org.meshtastic.core.common.util.CommonParcelable
|
||||
import org.meshtastic.core.common.util.CommonParcelize
|
||||
import org.meshtastic.core.common.util.bearing
|
||||
import org.meshtastic.core.common.util.latLongToMeter
|
||||
import org.meshtastic.core.common.util.nowSeconds
|
||||
import org.meshtastic.core.model.util.anonymize
|
||||
import org.meshtastic.core.model.util.onlineTimeThreshold
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.HardwareModel
|
||||
|
||||
//
|
||||
// model objects that directly map to the corresponding protobufs
|
||||
//
|
||||
|
||||
@CommonParcelize
|
||||
data class MeshUser(
|
||||
val id: String,
|
||||
val longName: String,
|
||||
val shortName: String,
|
||||
val hwModel: HardwareModel,
|
||||
val isLicensed: Boolean = false,
|
||||
val role: Int = 0,
|
||||
) : CommonParcelable {
|
||||
|
||||
override fun toString(): String = "MeshUser(id=${id.anonymize}, " +
|
||||
"longName=${longName.anonymize}, " +
|
||||
"shortName=${shortName.anonymize}, " +
|
||||
"hwModel=$hwModelString, " +
|
||||
"isLicensed=$isLicensed, " +
|
||||
"role=$role)"
|
||||
|
||||
/** Create our model object from a protobuf. */
|
||||
constructor(
|
||||
p: org.meshtastic.proto.User,
|
||||
) : this(p.id, p.long_name, p.short_name, p.hw_model, p.is_licensed, p.role.value)
|
||||
|
||||
/**
|
||||
* a string version of the hardware model, converted into pretty lowercase and changing _ to -, and p to dot or null
|
||||
* if unset
|
||||
*/
|
||||
val hwModelString: String?
|
||||
get() =
|
||||
if (hwModel == HardwareModel.UNSET) {
|
||||
null
|
||||
} else {
|
||||
hwModel.name.replace('_', '-').replace('p', '.').lowercase()
|
||||
}
|
||||
}
|
||||
|
||||
@CommonParcelize
|
||||
data class Position(
|
||||
val latitude: Double,
|
||||
val longitude: Double,
|
||||
val altitude: Int,
|
||||
val time: Int = currentTime(), // default to current time in secs (NOT MILLISECONDS!)
|
||||
val satellitesInView: Int = 0,
|
||||
val groundSpeed: Int = 0,
|
||||
val groundTrack: Int = 0, // "heading"
|
||||
val precisionBits: Int = 0,
|
||||
) : CommonParcelable {
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
companion object {
|
||||
// / Convert to a double representation of degrees
|
||||
fun degD(i: Int) = i * 1e-7
|
||||
|
||||
fun degI(d: Double) = (d * 1e7).toInt()
|
||||
|
||||
fun currentTime() = nowSeconds.toInt()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create our model object from a protobuf. If time is unspecified in the protobuf, the provided default time will
|
||||
* be used.
|
||||
*/
|
||||
constructor(
|
||||
position: org.meshtastic.proto.Position,
|
||||
defaultTime: Int = currentTime(),
|
||||
) : this(
|
||||
// We prefer the int version of lat/lon but if not available use the depreciated legacy version
|
||||
degD(position.latitude_i ?: 0),
|
||||
degD(position.longitude_i ?: 0),
|
||||
position.altitude ?: 0,
|
||||
if (position.time != 0) position.time else defaultTime,
|
||||
position.sats_in_view,
|
||||
position.ground_speed ?: 0,
|
||||
position.ground_track ?: 0,
|
||||
position.precision_bits,
|
||||
)
|
||||
|
||||
// / @return distance in meters to some other node (or null if unknown)
|
||||
fun distance(o: Position) = latLongToMeter(latitude, longitude, o.latitude, o.longitude)
|
||||
|
||||
// / @return bearing to the other position in degrees
|
||||
fun bearing(o: Position) = bearing(latitude, longitude, o.latitude, o.longitude)
|
||||
|
||||
// If GPS gives a crap position don't crash our app
|
||||
@Suppress("MagicNumber")
|
||||
fun isValid(): Boolean = latitude != 0.0 &&
|
||||
longitude != 0.0 &&
|
||||
(latitude >= -90 && latitude <= 90.0) &&
|
||||
(longitude >= -180 && longitude <= 180)
|
||||
|
||||
override fun toString(): String =
|
||||
"Position(lat=${latitude.anonymize}, lon=${longitude.anonymize}, alt=${altitude.anonymize}, time=$time)"
|
||||
}
|
||||
|
||||
@CommonParcelize
|
||||
data class DeviceMetrics(
|
||||
val time: Int = currentTime(), // default to current time in secs (NOT MILLISECONDS!)
|
||||
val batteryLevel: Int = 0,
|
||||
val voltage: Float,
|
||||
val channelUtilization: Float,
|
||||
val airUtilTx: Float,
|
||||
val uptimeSeconds: Int,
|
||||
) : CommonParcelable {
|
||||
companion object {
|
||||
@Suppress("MagicNumber")
|
||||
fun currentTime() = nowSeconds.toInt()
|
||||
}
|
||||
|
||||
/** Create our model object from a protobuf. */
|
||||
constructor(
|
||||
p: org.meshtastic.proto.DeviceMetrics,
|
||||
telemetryTime: Int = currentTime(),
|
||||
) : this(
|
||||
telemetryTime,
|
||||
p.battery_level ?: 0,
|
||||
p.voltage ?: 0f,
|
||||
p.channel_utilization ?: 0f,
|
||||
p.air_util_tx ?: 0f,
|
||||
p.uptime_seconds ?: 0,
|
||||
)
|
||||
}
|
||||
|
||||
@CommonParcelize
|
||||
data class EnvironmentMetrics(
|
||||
val time: Int = currentTime(), // default to current time in secs (NOT MILLISECONDS!)
|
||||
val temperature: Float?,
|
||||
val relativeHumidity: Float?,
|
||||
val soilTemperature: Float?,
|
||||
val soilMoisture: Int?,
|
||||
val barometricPressure: Float?,
|
||||
val gasResistance: Float?,
|
||||
val voltage: Float?,
|
||||
val current: Float?,
|
||||
val iaq: Int?,
|
||||
val lux: Float? = null,
|
||||
val uvLux: Float? = null,
|
||||
) : CommonParcelable {
|
||||
@Suppress("MagicNumber")
|
||||
companion object {
|
||||
fun currentTime() = nowSeconds.toInt()
|
||||
|
||||
fun fromTelemetryProto(proto: org.meshtastic.proto.EnvironmentMetrics, time: Int): EnvironmentMetrics =
|
||||
EnvironmentMetrics(
|
||||
temperature = proto.temperature?.takeIf { !it.isNaN() },
|
||||
relativeHumidity = proto.relative_humidity?.takeIf { !it.isNaN() && it != 0.0f },
|
||||
soilTemperature = proto.soil_temperature?.takeIf { !it.isNaN() },
|
||||
soilMoisture = proto.soil_moisture?.takeIf { it != Int.MIN_VALUE },
|
||||
barometricPressure = proto.barometric_pressure?.takeIf { !it.isNaN() },
|
||||
gasResistance = proto.gas_resistance?.takeIf { !it.isNaN() },
|
||||
voltage = proto.voltage?.takeIf { !it.isNaN() },
|
||||
current = proto.current?.takeIf { !it.isNaN() },
|
||||
iaq = proto.iaq?.takeIf { it != Int.MIN_VALUE },
|
||||
lux = proto.lux?.takeIf { !it.isNaN() },
|
||||
uvLux = proto.uv_lux?.takeIf { !it.isNaN() },
|
||||
time = time,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@CommonParcelize
|
||||
data class NodeInfo(
|
||||
val num: Int, // This is immutable, and used as a key
|
||||
var user: MeshUser? = null,
|
||||
var position: Position? = null,
|
||||
var snr: Float = Float.MAX_VALUE,
|
||||
var rssi: Int = Int.MAX_VALUE,
|
||||
var lastHeard: Int = 0, // the last time we've seen this node in secs since 1970
|
||||
var deviceMetrics: DeviceMetrics? = null,
|
||||
var channel: Int = 0,
|
||||
var environmentMetrics: EnvironmentMetrics? = null,
|
||||
var hopsAway: Int = 0,
|
||||
var nodeStatus: String? = null,
|
||||
) : CommonParcelable {
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
val colors: Pair<Int, Int>
|
||||
get() { // returns foreground and background @ColorInt for each 'num'
|
||||
val r = (num and 0xFF0000) shr 16
|
||||
val g = (num and 0x00FF00) shr 8
|
||||
val b = num and 0x0000FF
|
||||
val brightness = ((r * 0.299) + (g * 0.587) + (b * 0.114)) / 255
|
||||
val foreground = if (brightness > 0.5) 0xFF000000.toInt() else 0xFFFFFFFF.toInt()
|
||||
val background = (0xFF shl 24) or (r shl 16) or (g shl 8) or b
|
||||
return foreground to background
|
||||
}
|
||||
|
||||
val batteryLevel
|
||||
get() = deviceMetrics?.batteryLevel
|
||||
|
||||
val voltage
|
||||
get() = deviceMetrics?.voltage
|
||||
|
||||
@Suppress("ImplicitDefaultLocale")
|
||||
val batteryStr
|
||||
get() = if (batteryLevel in 1..100) "$batteryLevel%" else ""
|
||||
|
||||
/** true if the device was heard from recently */
|
||||
val isOnline: Boolean
|
||||
get() {
|
||||
return lastHeard > onlineTimeThreshold()
|
||||
}
|
||||
|
||||
// / return the position if it is valid, else null
|
||||
val validPosition: Position?
|
||||
get() {
|
||||
return position?.takeIf { it.isValid() }
|
||||
}
|
||||
|
||||
// / @return distance in meters to some other node (or null if unknown)
|
||||
fun distance(o: NodeInfo?): Int? {
|
||||
val p = validPosition
|
||||
val op = o?.validPosition
|
||||
return if (p != null && op != null) p.distance(op).toInt() else null
|
||||
}
|
||||
|
||||
// / @return bearing to the other position in degrees
|
||||
fun bearing(o: NodeInfo?): Int? {
|
||||
val p = validPosition
|
||||
val op = o?.validPosition
|
||||
return if (p != null && op != null) p.bearing(op).toInt() else null
|
||||
}
|
||||
|
||||
// / @return a nice human readable string for the distance, or null for unknown
|
||||
@Suppress("MagicNumber")
|
||||
fun distanceStr(o: NodeInfo?, prefUnits: Int = 0) = distance(o)?.let { dist ->
|
||||
when {
|
||||
dist == 0 -> null
|
||||
|
||||
// same point
|
||||
prefUnits == Config.DisplayConfig.DisplayUnits.METRIC.value && dist < 1000 -> "$dist m"
|
||||
|
||||
prefUnits == Config.DisplayConfig.DisplayUnits.METRIC.value && dist >= 1000 ->
|
||||
"${(dist / 100).toDouble() / 10.0} km"
|
||||
|
||||
prefUnits == Config.DisplayConfig.DisplayUnits.IMPERIAL.value && dist < 1609 ->
|
||||
"${(dist.toDouble() * 3.281).toInt()} ft"
|
||||
|
||||
prefUnits == Config.DisplayConfig.DisplayUnits.IMPERIAL.value && dist >= 1609 ->
|
||||
"${(dist / 160.9).toInt() / 10.0} mi"
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* 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.model
|
||||
|
||||
import org.meshtastic.core.common.util.bearing
|
||||
import org.meshtastic.core.common.util.latLongToMeter
|
||||
import org.meshtastic.core.common.util.nowSeconds
|
||||
import org.meshtastic.core.model.util.anonymize
|
||||
|
||||
data class Position(
|
||||
val latitude: Double,
|
||||
val longitude: Double,
|
||||
val altitude: Int,
|
||||
val time: Int = currentTime(), // default to current time in secs (NOT MILLISECONDS!)
|
||||
val satellitesInView: Int = 0,
|
||||
val groundSpeed: Int = 0,
|
||||
val groundTrack: Int = 0, // "heading"
|
||||
val precisionBits: Int = 0,
|
||||
) {
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
companion object {
|
||||
fun degD(i: Int) = i * 1e-7
|
||||
|
||||
fun degI(d: Double) = (d * 1e7).toInt()
|
||||
|
||||
fun currentTime() = nowSeconds.toInt()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create our model object from a protobuf. If time is unspecified in the protobuf, the provided default time will
|
||||
* be used.
|
||||
*/
|
||||
constructor(
|
||||
position: org.meshtastic.proto.Position,
|
||||
defaultTime: Int = currentTime(),
|
||||
) : this(
|
||||
degD(position.latitude_i ?: 0),
|
||||
degD(position.longitude_i ?: 0),
|
||||
position.altitude ?: 0,
|
||||
if (position.time != 0) position.time else defaultTime,
|
||||
position.sats_in_view,
|
||||
position.ground_speed ?: 0,
|
||||
position.ground_track ?: 0,
|
||||
position.precision_bits,
|
||||
)
|
||||
|
||||
/** @return distance in meters to some other position */
|
||||
fun distance(o: Position) = latLongToMeter(latitude, longitude, o.latitude, o.longitude)
|
||||
|
||||
/** @return bearing to the other position in degrees */
|
||||
fun bearing(o: Position) = bearing(latitude, longitude, o.latitude, o.longitude)
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun isValid(): Boolean = latitude != 0.0 &&
|
||||
longitude != 0.0 &&
|
||||
(latitude >= -90 && latitude <= 90.0) &&
|
||||
(longitude >= -180 && longitude <= 180)
|
||||
|
||||
override fun toString(): String =
|
||||
"Position(lat=${latitude.anonymize}, lon=${longitude.anonymize}, alt=${altitude.anonymize}, time=$time)"
|
||||
}
|
||||
@@ -22,12 +22,24 @@ import org.meshtastic.proto.ClientNotification
|
||||
/**
|
||||
* Central interface for controlling the radio and mesh network.
|
||||
*
|
||||
* This component provides an abstraction over the underlying communication transport (e.g., BLE, Serial, TCP) and the
|
||||
* low-level mesh protocols. It allows feature modules to interact with the mesh without needing to know about
|
||||
* platform-specific service details or AIDL interfaces.
|
||||
* This is a composite interface that extends the focused sub-interfaces below. Feature modules that need the full
|
||||
* surface inject [RadioController]; modules that need only a subset can inject the narrower interface for better
|
||||
* testability and clearer dependency intent.
|
||||
*
|
||||
* **Sub-interfaces (mirrors SDK's layered API design):**
|
||||
* - [AdminController] — config, channels, owner, device lifecycle (→ SDK `AdminApi`)
|
||||
* - [MessagingController] — send packets, reactions, contacts (→ SDK `RadioClient.send*`)
|
||||
* - [NodeController] — favorite, ignore, mute, remove nodes (→ SDK `AdminApi` node ops)
|
||||
* - [RequestController] — telemetry, traceroute, position queries (→ SDK `TelemetryApi` / `RoutingApi`)
|
||||
*
|
||||
* When migrating to the SDK, each sub-interface becomes a thin adapter over the corresponding SDK API. The composite
|
||||
* [RadioController] can then be deprecated and consumers migrated to the narrower interfaces one at a time.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
interface RadioController {
|
||||
interface RadioController :
|
||||
AdminController,
|
||||
MessagingController,
|
||||
NodeController,
|
||||
RequestController {
|
||||
/**
|
||||
* Canonical app-level connection state, delegated from [ServiceRepository][connectionState].
|
||||
*
|
||||
@@ -47,279 +59,9 @@ interface RadioController {
|
||||
*/
|
||||
val clientNotification: StateFlow<ClientNotification?>
|
||||
|
||||
/**
|
||||
* Sends a data packet to the mesh.
|
||||
*
|
||||
* @param packet The [DataPacket] containing the payload and routing information.
|
||||
*/
|
||||
suspend fun sendMessage(packet: DataPacket)
|
||||
|
||||
/** Clears the current [clientNotification]. */
|
||||
fun clearClientNotification()
|
||||
|
||||
/**
|
||||
* Toggles the favorite status of a node on the radio.
|
||||
*
|
||||
* @param nodeNum The node number to favorite/unfavorite.
|
||||
*/
|
||||
suspend fun favoriteNode(nodeNum: Int)
|
||||
|
||||
/**
|
||||
* Sends our shared contact information (identity and public key) to the firmware's NodeDB.
|
||||
*
|
||||
* This ensures the firmware has the correct public key for the destination node before a PKI-encrypted direct
|
||||
* message is sent. The method suspends until the radio acknowledges the admin packet.
|
||||
*
|
||||
* @param nodeNum The destination node number.
|
||||
* @return `true` if the radio accepted the contact, `false` on timeout or failure.
|
||||
*/
|
||||
suspend fun sendSharedContact(nodeNum: Int): Boolean
|
||||
|
||||
/**
|
||||
* Updates the local radio configuration.
|
||||
*
|
||||
* @param config The new configuration [org.meshtastic.proto.Config].
|
||||
*/
|
||||
suspend fun setLocalConfig(config: org.meshtastic.proto.Config)
|
||||
|
||||
/**
|
||||
* Updates a local radio channel.
|
||||
*
|
||||
* @param channel The channel configuration [org.meshtastic.proto.Channel].
|
||||
*/
|
||||
suspend fun setLocalChannel(channel: org.meshtastic.proto.Channel)
|
||||
|
||||
/**
|
||||
* Updates the owner (user info) on a remote node.
|
||||
*
|
||||
* @param destNum The destination node number.
|
||||
* @param user The new user info [org.meshtastic.proto.User].
|
||||
* @param packetId The request packet ID.
|
||||
*/
|
||||
suspend fun setOwner(destNum: Int, user: org.meshtastic.proto.User, packetId: Int)
|
||||
|
||||
/**
|
||||
* Updates the general configuration on a remote node.
|
||||
*
|
||||
* @param destNum The destination node number.
|
||||
* @param config The new configuration [org.meshtastic.proto.Config].
|
||||
* @param packetId The request packet ID.
|
||||
*/
|
||||
suspend fun setConfig(destNum: Int, config: org.meshtastic.proto.Config, packetId: Int)
|
||||
|
||||
/**
|
||||
* Updates a module configuration on a remote node.
|
||||
*
|
||||
* @param destNum The destination node number.
|
||||
* @param config The new module configuration [org.meshtastic.proto.ModuleConfig].
|
||||
* @param packetId The request packet ID.
|
||||
*/
|
||||
suspend fun setModuleConfig(destNum: Int, config: org.meshtastic.proto.ModuleConfig, packetId: Int)
|
||||
|
||||
/**
|
||||
* Updates a channel configuration on a remote node.
|
||||
*
|
||||
* @param destNum The destination node number.
|
||||
* @param channel The new channel configuration [org.meshtastic.proto.Channel].
|
||||
* @param packetId The request packet ID.
|
||||
*/
|
||||
suspend fun setRemoteChannel(destNum: Int, channel: org.meshtastic.proto.Channel, packetId: Int)
|
||||
|
||||
/**
|
||||
* Sets a fixed position on a remote node.
|
||||
*
|
||||
* @param destNum The destination node number.
|
||||
* @param position The position to set.
|
||||
*/
|
||||
suspend fun setFixedPosition(destNum: Int, position: Position)
|
||||
|
||||
/**
|
||||
* Updates the notification ringtone on a remote node.
|
||||
*
|
||||
* @param destNum The destination node number.
|
||||
* @param ringtone The name/ID of the ringtone.
|
||||
*/
|
||||
suspend fun setRingtone(destNum: Int, ringtone: String)
|
||||
|
||||
/**
|
||||
* Updates the canned messages configuration on a remote node.
|
||||
*
|
||||
* @param destNum The destination node number.
|
||||
* @param messages The canned messages string.
|
||||
*/
|
||||
suspend fun setCannedMessages(destNum: Int, messages: String)
|
||||
|
||||
/**
|
||||
* Requests the current owner (user info) from a remote node.
|
||||
*
|
||||
* @param destNum The remote node number.
|
||||
* @param packetId The request packet ID.
|
||||
*/
|
||||
suspend fun getOwner(destNum: Int, packetId: Int)
|
||||
|
||||
/**
|
||||
* Requests a specific configuration section from a remote node.
|
||||
*
|
||||
* @param destNum The remote node number.
|
||||
* @param configType The numeric type of the configuration section.
|
||||
* @param packetId The request packet ID.
|
||||
*/
|
||||
suspend fun getConfig(destNum: Int, configType: Int, packetId: Int)
|
||||
|
||||
/**
|
||||
* Requests a module configuration section from a remote node.
|
||||
*
|
||||
* @param destNum The remote node number.
|
||||
* @param moduleConfigType The numeric type of the module configuration section.
|
||||
* @param packetId The request packet ID.
|
||||
*/
|
||||
suspend fun getModuleConfig(destNum: Int, moduleConfigType: Int, packetId: Int)
|
||||
|
||||
/**
|
||||
* Requests a specific channel configuration from a remote node.
|
||||
*
|
||||
* @param destNum The remote node number.
|
||||
* @param index The channel index.
|
||||
* @param packetId The request packet ID.
|
||||
*/
|
||||
suspend fun getChannel(destNum: Int, index: Int, packetId: Int)
|
||||
|
||||
/**
|
||||
* Requests the current ringtone from a remote node.
|
||||
*
|
||||
* @param destNum The remote node number.
|
||||
* @param packetId The request packet ID.
|
||||
*/
|
||||
suspend fun getRingtone(destNum: Int, packetId: Int)
|
||||
|
||||
/**
|
||||
* Requests the current canned messages from a remote node.
|
||||
*
|
||||
* @param destNum The remote node number.
|
||||
* @param packetId The request packet ID.
|
||||
*/
|
||||
suspend fun getCannedMessages(destNum: Int, packetId: Int)
|
||||
|
||||
/**
|
||||
* Requests the hardware connection status from a remote node.
|
||||
*
|
||||
* @param destNum The remote node number.
|
||||
* @param packetId The request packet ID.
|
||||
*/
|
||||
suspend fun getDeviceConnectionStatus(destNum: Int, packetId: Int)
|
||||
|
||||
/**
|
||||
* Commands a node to reboot.
|
||||
*
|
||||
* @param destNum The target node number.
|
||||
* @param packetId The request packet ID.
|
||||
*/
|
||||
suspend fun reboot(destNum: Int, packetId: Int)
|
||||
|
||||
/**
|
||||
* Commands a node to reboot into DFU (Device Firmware Update) mode.
|
||||
*
|
||||
* @param nodeNum The target node number.
|
||||
*/
|
||||
suspend fun rebootToDfu(nodeNum: Int)
|
||||
|
||||
/**
|
||||
* Initiates an Over-The-Air (OTA) reboot request.
|
||||
*
|
||||
* @param requestId The request ID.
|
||||
* @param destNum The target node number.
|
||||
* @param mode The OTA mode.
|
||||
* @param hash Optional hash for verification.
|
||||
*/
|
||||
suspend fun requestRebootOta(requestId: Int, destNum: Int, mode: Int, hash: ByteArray?)
|
||||
|
||||
/**
|
||||
* Commands a node to shut down.
|
||||
*
|
||||
* @param destNum The target node number.
|
||||
* @param packetId The request packet ID.
|
||||
*/
|
||||
suspend fun shutdown(destNum: Int, packetId: Int)
|
||||
|
||||
/**
|
||||
* Performs a factory reset on a node.
|
||||
*
|
||||
* @param destNum The target node number.
|
||||
* @param packetId The request packet ID.
|
||||
*/
|
||||
suspend fun factoryReset(destNum: Int, packetId: Int)
|
||||
|
||||
/**
|
||||
* Resets the NodeDB on a node.
|
||||
*
|
||||
* @param destNum The target node number.
|
||||
* @param packetId The request packet ID.
|
||||
* @param preserveFavorites Whether to keep favorite nodes in the database.
|
||||
*/
|
||||
suspend fun nodedbReset(destNum: Int, packetId: Int, preserveFavorites: Boolean)
|
||||
|
||||
/**
|
||||
* Removes a node from the mesh by its node number.
|
||||
*
|
||||
* @param packetId The request packet ID.
|
||||
* @param nodeNum The node number to remove.
|
||||
*/
|
||||
suspend fun removeByNodenum(packetId: Int, nodeNum: Int)
|
||||
|
||||
/**
|
||||
* Requests the current GPS position from a remote node.
|
||||
*
|
||||
* @param destNum The target node number.
|
||||
* @param currentPosition Our current position to provide in the request.
|
||||
*/
|
||||
suspend fun requestPosition(destNum: Int, currentPosition: Position)
|
||||
|
||||
/**
|
||||
* Requests detailed user info from a remote node.
|
||||
*
|
||||
* @param destNum The target node number.
|
||||
*/
|
||||
suspend fun requestUserInfo(destNum: Int)
|
||||
|
||||
/**
|
||||
* Initiates a traceroute request to a remote node.
|
||||
*
|
||||
* @param requestId The request ID.
|
||||
* @param destNum The destination node number.
|
||||
*/
|
||||
suspend fun requestTraceroute(requestId: Int, destNum: Int)
|
||||
|
||||
/**
|
||||
* Requests telemetry data from a remote node.
|
||||
*
|
||||
* @param requestId The request ID.
|
||||
* @param destNum The destination node number.
|
||||
* @param typeValue The numeric type of telemetry requested.
|
||||
*/
|
||||
suspend fun requestTelemetry(requestId: Int, destNum: Int, typeValue: Int)
|
||||
|
||||
/**
|
||||
* Requests neighbor information (detected nodes) from a remote node.
|
||||
*
|
||||
* @param requestId The request ID.
|
||||
* @param destNum The destination node number.
|
||||
*/
|
||||
suspend fun requestNeighborInfo(requestId: Int, destNum: Int)
|
||||
|
||||
/**
|
||||
* Signals the start of a batch configuration session.
|
||||
*
|
||||
* @param destNum The target node number.
|
||||
*/
|
||||
suspend fun beginEditSettings(destNum: Int)
|
||||
|
||||
/**
|
||||
* Commits all pending configuration changes in a batch session.
|
||||
*
|
||||
* @param destNum The target node number.
|
||||
*/
|
||||
suspend fun commitEditSettings(destNum: Int)
|
||||
|
||||
/**
|
||||
* Generates a unique packet ID for a new request.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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.model
|
||||
|
||||
/**
|
||||
* Mesh request operations — position, traceroute, telemetry, user info, and metadata queries.
|
||||
*
|
||||
* These are "pull" operations that request data from remote nodes. When the SDK is adopted, implementations delegate to
|
||||
* `RadioClient.telemetry` and `RadioClient.routing` sub-APIs.
|
||||
*
|
||||
* @see RadioController which extends this interface for backward compatibility
|
||||
*/
|
||||
interface RequestController {
|
||||
|
||||
/** Requests device metadata from a remote node. */
|
||||
suspend fun refreshMetadata(destNum: Int)
|
||||
|
||||
/** Requests the current GPS position from a remote node. */
|
||||
suspend fun requestPosition(destNum: Int, currentPosition: Position)
|
||||
|
||||
/** Requests detailed user info from a remote node. */
|
||||
suspend fun requestUserInfo(destNum: Int)
|
||||
|
||||
/** Initiates a traceroute request to a remote node. */
|
||||
suspend fun requestTraceroute(requestId: Int, destNum: Int)
|
||||
|
||||
/** Requests telemetry data from a remote node. */
|
||||
suspend fun requestTelemetry(requestId: Int, destNum: Int, typeValue: Int)
|
||||
|
||||
/** Requests neighbor information (detected nodes) from a remote node. */
|
||||
suspend fun requestNeighborInfo(requestId: Int, destNum: Int)
|
||||
}
|
||||
@@ -1,49 +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.model.service
|
||||
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.proto.SharedContact
|
||||
|
||||
sealed class ServiceAction {
|
||||
data class GetDeviceMetadata(val destNum: Int) : ServiceAction()
|
||||
|
||||
data class Favorite(val node: Node) : ServiceAction()
|
||||
|
||||
data class Ignore(val node: Node) : ServiceAction()
|
||||
|
||||
data class Mute(val node: Node) : ServiceAction()
|
||||
|
||||
data class Reaction(val emoji: String, val replyId: Int, val contactKey: String) : ServiceAction()
|
||||
|
||||
data class ImportContact(val contact: SharedContact) : ServiceAction()
|
||||
|
||||
/**
|
||||
* Sends a shared contact (identity + public key) to the firmware's NodeDB.
|
||||
*
|
||||
* The [result] deferred is completed with `true` when the radio acknowledges the admin packet, or `false` on
|
||||
* timeout/failure. Callers that need to guarantee the contact is stored before sending a subsequent DM should
|
||||
* `await()` this deferred.
|
||||
*
|
||||
* Not a data class: [result] is a [CompletableDeferred] with identity-based equality that would break data class
|
||||
* equals/hashCode/copy semantics.
|
||||
*/
|
||||
class SendContact(val contact: SharedContact) : ServiceAction() {
|
||||
val result: CompletableDeferred<Boolean> = CompletableDeferred()
|
||||
}
|
||||
}
|
||||
@@ -23,8 +23,6 @@ import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import okio.ByteString
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.meshtastic.core.common.util.CommonParcel
|
||||
import org.meshtastic.core.common.util.CommonParceler
|
||||
|
||||
/** Serializer for Okio [ByteString] using kotlinx.serialization */
|
||||
object ByteStringSerializer : KSerializer<ByteString> {
|
||||
@@ -38,12 +36,3 @@ object ByteStringSerializer : KSerializer<ByteString> {
|
||||
|
||||
override fun deserialize(decoder: Decoder): ByteString = byteArraySerializer.deserialize(decoder).toByteString()
|
||||
}
|
||||
|
||||
/** Parceler for Okio [ByteString] for Android Parcelable support */
|
||||
object ByteStringParceler : CommonParceler<ByteString?> {
|
||||
override fun create(parcel: CommonParcel): ByteString? = parcel.createByteArray()?.toByteString()
|
||||
|
||||
override fun ByteString?.write(parcel: CommonParcel, flags: Int) {
|
||||
parcel.writeByteArray(this?.toByteArray())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ package org.meshtastic.core.model.util
|
||||
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.NodeAddress
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
|
||||
/**
|
||||
@@ -40,7 +41,7 @@ open class MeshDataMapper(private val nodeIdLookup: NodeIdLookup) {
|
||||
dataType = decoded.portnum.value,
|
||||
bytes = decoded.payload.toByteArray().toByteString(),
|
||||
hopLimit = packet.hop_limit,
|
||||
channel = if (packet.pki_encrypted == true) DataPacket.PKC_CHANNEL_INDEX else packet.channel,
|
||||
channel = if (packet.pki_encrypted == true) NodeAddress.PKC_CHANNEL_INDEX else packet.channel,
|
||||
wantAck = packet.want_ack == true,
|
||||
hopStart = packet.hop_start,
|
||||
snr = packet.rx_snr,
|
||||
|
||||
@@ -59,7 +59,7 @@ fun <T : Message<T, *>> ProtoAdapter<T>.decodeOrNull(bytes: ByteArray?, logger:
|
||||
* ```
|
||||
* val data = Data(portnum = PortNum.TEXT_MESSAGE_APP, payload = bytes)
|
||||
* if (!Data.ADAPTER.isWithinSizeLimit(data, MAX_PAYLOAD)) {
|
||||
* throw RemoteException("Payload too large")
|
||||
* error("Payload too large")
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
|
||||
@@ -38,8 +38,8 @@ class DataPacketTest {
|
||||
|
||||
@Test
|
||||
fun nodeNumToDefaultId_formatsHexWithPrefix() {
|
||||
assertEquals("!1234abcd", DataPacket.nodeNumToDefaultId(0x1234ABCD))
|
||||
assertEquals("!ffffffff", DataPacket.nodeNumToDefaultId(DataPacket.NODENUM_BROADCAST))
|
||||
assertEquals("!1234abcd", NodeAddress.numToDefaultId(0x1234ABCD))
|
||||
assertEquals("!ffffffff", NodeAddress.numToDefaultId(NodeAddress.NODENUM_BROADCAST))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
package org.meshtastic.core.model.util
|
||||
|
||||
import okio.ByteString.Companion.encodeUtf8
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.MeshUser
|
||||
import org.meshtastic.core.model.NodeAddress
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.Data
|
||||
import org.meshtastic.proto.DeviceMetrics
|
||||
@@ -104,7 +104,7 @@ class MeshDataMapperTest {
|
||||
val mapped = mapper.toDataPacket(packet)
|
||||
|
||||
assertNotNull(mapped)
|
||||
assertEquals(DataPacket.PKC_CHANNEL_INDEX, mapped.channel)
|
||||
assertEquals(NodeAddress.PKC_CHANNEL_INDEX, mapped.channel)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -281,6 +281,6 @@ class MeshDataMapperTest {
|
||||
}
|
||||
|
||||
private class TestNodeIdLookup : NodeIdLookup {
|
||||
override fun toNodeID(nodeNum: Int): String = DataPacket.nodeNumToDefaultId(nodeNum)
|
||||
override fun toNodeID(nodeNum: Int): String = NodeAddress.numToDefaultId(nodeNum)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import org.meshtastic.core.common.util.registerReceiverCompat
|
||||
|
||||
private const val ACTION_USB_PERMISSION = "com.geeksville.mesh.USB_PERMISSION"
|
||||
private const val ACTION_USB_PERMISSION = "org.meshtastic.app.USB_PERMISSION"
|
||||
|
||||
internal fun UsbManager.requestPermission(context: Context, device: UsbDevice): Flow<Boolean> = callbackFlow {
|
||||
val receiver =
|
||||
|
||||
@@ -24,7 +24,7 @@ import okio.ByteString.Companion.toByteString
|
||||
import org.meshtastic.core.common.util.handledLaunch
|
||||
import org.meshtastic.core.common.util.nowSeconds
|
||||
import org.meshtastic.core.model.Channel
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.NodeAddress
|
||||
import org.meshtastic.core.model.util.getInitials
|
||||
import org.meshtastic.core.repository.RadioTransport
|
||||
import org.meshtastic.core.repository.RadioTransportCallback
|
||||
@@ -333,7 +333,7 @@ class MockRadioTransport(
|
||||
num = numIn,
|
||||
user =
|
||||
User(
|
||||
id = DataPacket.nodeNumToDefaultId(numIn),
|
||||
id = NodeAddress.numToDefaultId(numIn),
|
||||
long_name = "Sim ${numIn.toString(16)}",
|
||||
short_name = getInitials("Sim ${numIn.toString(16)}"),
|
||||
hw_model = HardwareModel.ANDROID_SIM,
|
||||
|
||||
@@ -51,7 +51,6 @@ src/
|
||||
│ ├── RadioConfigRepository.kt
|
||||
│ ├── RadioInterfaceService.kt
|
||||
│ ├── RadioTransportCallback.kt / RadioTransportFactory.kt
|
||||
│ ├── ServiceBroadcasts.kt
|
||||
│ ├── StoreForwardPacketHandler.kt
|
||||
│ ├── TelemetryPacketHandler.kt
|
||||
│ ├── TracerouteHandler.kt / TracerouteSnapshotRepository.kt
|
||||
|
||||
@@ -38,10 +38,10 @@ interface CommandSender {
|
||||
fun generatePacketId(): Int
|
||||
|
||||
/** Sends a data packet to the mesh. */
|
||||
fun sendData(p: DataPacket)
|
||||
suspend fun sendData(p: DataPacket)
|
||||
|
||||
/** Sends an admin message to a specific node. */
|
||||
fun sendAdmin(
|
||||
suspend fun sendAdmin(
|
||||
destNum: Int,
|
||||
requestId: Int = generatePacketId(),
|
||||
wantResponse: Boolean = false,
|
||||
@@ -64,23 +64,23 @@ interface CommandSender {
|
||||
): Boolean
|
||||
|
||||
/** Sends our current position to the mesh. */
|
||||
fun sendPosition(pos: org.meshtastic.proto.Position, destNum: Int? = null, wantResponse: Boolean = false)
|
||||
suspend fun sendPosition(pos: org.meshtastic.proto.Position, destNum: Int? = null, wantResponse: Boolean = false)
|
||||
|
||||
/** Requests the position of a specific node. */
|
||||
fun requestPosition(destNum: Int, currentPosition: Position)
|
||||
suspend fun requestPosition(destNum: Int, currentPosition: Position)
|
||||
|
||||
/** Sets a fixed position for a node. */
|
||||
fun setFixedPosition(destNum: Int, pos: Position)
|
||||
suspend fun setFixedPosition(destNum: Int, pos: Position)
|
||||
|
||||
/** Requests user info from a specific node. */
|
||||
fun requestUserInfo(destNum: Int)
|
||||
suspend fun requestUserInfo(destNum: Int)
|
||||
|
||||
/** Requests a traceroute to a specific node. */
|
||||
fun requestTraceroute(requestId: Int, destNum: Int)
|
||||
suspend fun requestTraceroute(requestId: Int, destNum: Int)
|
||||
|
||||
/** Requests telemetry from a specific node. */
|
||||
fun requestTelemetry(requestId: Int, destNum: Int, typeValue: Int)
|
||||
suspend fun requestTelemetry(requestId: Int, destNum: Int, typeValue: Int)
|
||||
|
||||
/** Requests neighbor info from a specific node. */
|
||||
fun requestNeighborInfo(requestId: Int, destNum: Int)
|
||||
suspend fun requestNeighborInfo(requestId: Int, destNum: Int)
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ interface HistoryManager {
|
||||
* @param storeForwardConfig The store-and-forward module configuration.
|
||||
* @param transport The transport method being used (for logging).
|
||||
*/
|
||||
fun requestHistoryReplay(
|
||||
suspend fun requestHistoryReplay(
|
||||
trigger: String,
|
||||
myNodeNum: Int?,
|
||||
storeForwardConfig: ModuleConfig.StoreForwardConfig?,
|
||||
|
||||
@@ -1,119 +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.repository
|
||||
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.MeshUser
|
||||
import org.meshtastic.core.model.Position
|
||||
import org.meshtastic.core.model.service.ServiceAction
|
||||
|
||||
/** Interface for handling UI-triggered actions and administrative commands for the mesh. */
|
||||
@Suppress("TooManyFunctions")
|
||||
interface MeshActionHandler {
|
||||
/** Processes a service action from the UI. */
|
||||
suspend fun onServiceAction(action: ServiceAction)
|
||||
|
||||
/** Sets the owner of the local node. */
|
||||
fun handleSetOwner(u: MeshUser, myNodeNum: Int)
|
||||
|
||||
/** Sends a data packet through the mesh. */
|
||||
fun handleSend(p: DataPacket, myNodeNum: Int)
|
||||
|
||||
/** Requests the position of a remote node. */
|
||||
fun handleRequestPosition(destNum: Int, position: Position, myNodeNum: Int)
|
||||
|
||||
/** Removes a node from the database by its node number. */
|
||||
fun handleRemoveByNodenum(nodeNum: Int, requestId: Int, myNodeNum: Int)
|
||||
|
||||
/** Sets the owner of a remote node. */
|
||||
fun handleSetRemoteOwner(id: Int, destNum: Int, payload: ByteArray)
|
||||
|
||||
/** Gets the owner of a remote node. */
|
||||
fun handleGetRemoteOwner(id: Int, destNum: Int)
|
||||
|
||||
/** Sets the configuration of the local node. */
|
||||
fun handleSetConfig(payload: ByteArray, myNodeNum: Int)
|
||||
|
||||
/** Sets the configuration of a remote node. */
|
||||
fun handleSetRemoteConfig(id: Int, destNum: Int, payload: ByteArray)
|
||||
|
||||
/** Gets the configuration of a remote node. */
|
||||
fun handleGetRemoteConfig(id: Int, destNum: Int, config: Int)
|
||||
|
||||
/** Sets the module configuration of a remote node. */
|
||||
fun handleSetModuleConfig(id: Int, destNum: Int, payload: ByteArray)
|
||||
|
||||
/** Gets the module configuration of a remote node. */
|
||||
fun handleGetModuleConfig(id: Int, destNum: Int, config: Int)
|
||||
|
||||
/** Sets the ringtone of a remote node. */
|
||||
fun handleSetRingtone(destNum: Int, ringtone: String)
|
||||
|
||||
/** Gets the ringtone of a remote node. */
|
||||
fun handleGetRingtone(id: Int, destNum: Int)
|
||||
|
||||
/** Sets canned messages on a remote node. */
|
||||
fun handleSetCannedMessages(destNum: Int, messages: String)
|
||||
|
||||
/** Gets canned messages from a remote node. */
|
||||
fun handleGetCannedMessages(id: Int, destNum: Int)
|
||||
|
||||
/** Sets a channel configuration on the local node. */
|
||||
fun handleSetChannel(payload: ByteArray?, myNodeNum: Int)
|
||||
|
||||
/** Sets a channel configuration on a remote node. */
|
||||
fun handleSetRemoteChannel(id: Int, destNum: Int, payload: ByteArray?)
|
||||
|
||||
/** Gets a channel configuration from a remote node. */
|
||||
fun handleGetRemoteChannel(id: Int, destNum: Int, index: Int)
|
||||
|
||||
/** Requests neighbor information from a remote node. */
|
||||
fun handleRequestNeighborInfo(requestId: Int, destNum: Int)
|
||||
|
||||
/** Begins editing settings on a remote node. */
|
||||
fun handleBeginEditSettings(destNum: Int)
|
||||
|
||||
/** Commits settings edits on a remote node. */
|
||||
fun handleCommitEditSettings(destNum: Int)
|
||||
|
||||
/** Reboots a remote node into DFU mode. */
|
||||
fun handleRebootToDfu(destNum: Int)
|
||||
|
||||
/** Requests telemetry from a remote node. */
|
||||
fun handleRequestTelemetry(requestId: Int, destNum: Int, type: Int)
|
||||
|
||||
/** Requests a remote node to shut down. */
|
||||
fun handleRequestShutdown(requestId: Int, destNum: Int)
|
||||
|
||||
/** Requests a remote node to reboot. */
|
||||
fun handleRequestReboot(requestId: Int, destNum: Int)
|
||||
|
||||
/** Requests a remote node to reboot in OTA mode. */
|
||||
fun handleRequestRebootOta(requestId: Int, destNum: Int, mode: Int, hash: ByteArray?)
|
||||
|
||||
/** Requests a factory reset on a remote node. */
|
||||
fun handleRequestFactoryReset(requestId: Int, destNum: Int)
|
||||
|
||||
/** Requests a node database reset on a remote node. */
|
||||
fun handleRequestNodedbReset(requestId: Int, destNum: Int, preserveFavorites: Boolean)
|
||||
|
||||
/** Gets the connection status of a remote node. */
|
||||
fun handleGetDeviceConnectionStatus(requestId: Int, destNum: Int)
|
||||
|
||||
/** Updates the last used device address. */
|
||||
fun handleUpdateLastAddress(deviceAddr: String?)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user