mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
feat(example): Add packet log and UI improvements (#4455)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
c44d2f3268
commit
f1520eb383
6 changed files with 258 additions and 97 deletions
|
|
@ -24,8 +24,8 @@ Add the dependencies to your module's `build.gradle.kts`:
|
|||
|
||||
```kotlin
|
||||
dependencies {
|
||||
// Replace 'v2.7.12' with the specific version you need
|
||||
val meshtasticVersion = "v2.7.12"
|
||||
// Replace 'v2.7.13' with the specific version you need
|
||||
val meshtasticVersion = "v2.7.13"
|
||||
|
||||
// The core AIDL interface
|
||||
implementation("com.github.meshtastic.Meshtastic-Android:core-api:$meshtasticVersion")
|
||||
|
|
@ -33,42 +33,70 @@ dependencies {
|
|||
// Data models (DataPacket, MeshUser, NodeInfo, etc.)
|
||||
implementation("com.github.meshtastic.Meshtastic-Android:core-model:$meshtasticVersion")
|
||||
|
||||
// Protobuf definitions (Portnums, Telemetry, etc.)
|
||||
// Protobuf definitions (PortNum, Telemetry, etc.)
|
||||
implementation("com.github.meshtastic.Meshtastic-Android:core-proto:$meshtasticVersion")
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
1. **Bind to the Service:**
|
||||
Use the `IMeshService` interface to bind to the Meshtastic service.
|
||||
### 1. Bind to the Service
|
||||
|
||||
```kotlin
|
||||
val intent = Intent("com.geeksville.mesh.Service")
|
||||
intent.setClassName("com.geeksville.mesh", "com.geeksville.mesh.service.MeshService")
|
||||
bindService(intent, serviceConnection, BIND_AUTO_CREATE)
|
||||
```
|
||||
Use the `IMeshService` interface to bind to the Meshtastic service. It is recommended to query the package manager to find the correct service component, as the package name may vary between build flavors (e.g., Play Store vs. F-Droid).
|
||||
|
||||
2. **Interact with the API:**
|
||||
Once bound, cast the `IBinder` to `IMeshService`:
|
||||
```kotlin
|
||||
val intent = Intent("com.geeksville.mesh.Service")
|
||||
val resolveInfo = packageManager.queryIntentServices(intent, 0)
|
||||
|
||||
```kotlin
|
||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||
val meshService = IMeshService.Stub.asInterface(service)
|
||||
|
||||
// Example: Send a text message
|
||||
val packet = DataPacket(
|
||||
to = DataPacket.ID_BROADCAST,
|
||||
bytes = "Hello Meshtastic!".toByteArray(),
|
||||
dataType = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE,
|
||||
// ... other fields
|
||||
)
|
||||
meshService.send(packet)
|
||||
}
|
||||
```
|
||||
if (resolveInfo.isNotEmpty()) {
|
||||
val serviceInfo = resolveInfo[0].serviceInfo
|
||||
intent.setClassName(serviceInfo.packageName, serviceInfo.name)
|
||||
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Interact with the API
|
||||
|
||||
Once bound, cast the `IBinder` to `IMeshService`:
|
||||
|
||||
```kotlin
|
||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||
val meshService = IMeshService.Stub.asInterface(service)
|
||||
|
||||
// Example: Send a broadcast text message
|
||||
val packet = DataPacket(
|
||||
to = DataPacket.ID_BROADCAST,
|
||||
bytes = "Hello Meshtastic!".encodeToByteArray().toByteString(),
|
||||
dataType = PortNum.TEXT_MESSAGE_APP.value,
|
||||
id = meshService.packetId,
|
||||
wantAck = true
|
||||
)
|
||||
meshService.send(packet)
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Register a BroadcastReceiver
|
||||
|
||||
To receive packets and status updates, register a `BroadcastReceiver`.
|
||||
|
||||
**Important:** On Android 13+ (API 33), you **must** use `RECEIVER_EXPORTED` since you are receiving broadcasts from a different application.
|
||||
|
||||
```kotlin
|
||||
val intentFilter = IntentFilter().apply {
|
||||
addAction("com.geeksville.mesh.RECEIVED.TEXT_MESSAGE_APP")
|
||||
addAction("com.geeksville.mesh.NODE_CHANGE")
|
||||
addAction("com.geeksville.mesh.CONNECTION_CHANGED")
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
registerReceiver(meshtasticReceiver, intentFilter, Context.RECEIVER_EXPORTED)
|
||||
} else {
|
||||
registerReceiver(meshtasticReceiver, intentFilter)
|
||||
}
|
||||
```
|
||||
|
||||
## Modules
|
||||
|
||||
* **`core:api`**: Contains `IMeshService.aidl`.
|
||||
* **`core:model`**: Contains Parcelable data classes like `DataPacket`, `MeshUser`, `NodeInfo`.
|
||||
* **`core:proto`**: Contains the generated Protobuf code from `meshtastic/protobufs`.
|
||||
* **`core:proto`**: Contains the generated Protobuf code (Wire).
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import okio.ByteString.Companion.toByteString
|
|||
|
||||
@Suppress("unused") // These are extension functions meant to be imported elsewhere
|
||||
fun <T : Message<T, *>> ProtoAdapter<T>.decodeOrNull(bytes: ByteString?, logger: Logger? = null): T? {
|
||||
if (bytes == null || bytes.size == 0) return null
|
||||
if (bytes == null) return null
|
||||
return runCatching { decode(bytes) }
|
||||
.onFailure { exception -> logger?.e(exception) { "Failed to decode proto message" } }
|
||||
.getOrNull()
|
||||
|
|
@ -40,7 +40,7 @@ fun <T : Message<T, *>> ProtoAdapter<T>.decodeOrNull(bytes: ByteString?, logger:
|
|||
* @return The decoded message, or null if bytes is null or decoding fails
|
||||
*/
|
||||
fun <T : Message<T, *>> ProtoAdapter<T>.decodeOrNull(bytes: ByteArray?, logger: Logger? = null): T? {
|
||||
if (bytes == null || bytes.isEmpty()) return null
|
||||
if (bytes == null) return null
|
||||
return decodeOrNull(bytes.toByteString(), logger)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -74,12 +74,14 @@ class WireExtensionsTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `decodeOrNull with empty ByteString returns null`() {
|
||||
fun `decodeOrNull with empty ByteString returns empty message`() {
|
||||
// Act
|
||||
val result = Position.ADAPTER.decodeOrNull(ByteString.EMPTY, testLogger)
|
||||
|
||||
// Assert
|
||||
assertNull(result)
|
||||
assertNotNull(result)
|
||||
// An empty position should have null/default values
|
||||
assertNull(result!!.latitude_i)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -107,18 +109,20 @@ class WireExtensionsTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `decodeOrNull with empty ByteArray returns null`() {
|
||||
fun `decodeOrNull with empty ByteArray returns empty message`() {
|
||||
// Act
|
||||
val result = Position.ADAPTER.decodeOrNull(ByteArray(0), testLogger)
|
||||
|
||||
// Assert
|
||||
assertNull(result)
|
||||
assertNotNull(result)
|
||||
assertNull(result!!.latitude_i)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `decodeOrNull with invalid data returns null`() {
|
||||
// Arrange
|
||||
val invalidBytes = byteArrayOfInts(0xFF, 0xFF, 0xFF, 0xFF).toByteString()
|
||||
// A single byte 0xFF is an invalid field tag (field 0 is reserved and tags are varints)
|
||||
val invalidBytes = ByteString.of(0xFF.toByte())
|
||||
|
||||
// Act - should not throw, should return null
|
||||
val result = Position.ADAPTER.decodeOrNull(invalidBytes, testLogger)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue