From 15760da0746e01f1dc2fc1ef6a17424dd1b02367 Mon Sep 17 00:00:00 2001
From: James Rich <2199651+jamesarich@users.noreply.github.com>
Date: Thu, 29 Jan 2026 13:45:00 -0600
Subject: [PATCH] Refactor: Create `core:api` module and set up publishing
(#4362)
---
.github/workflows/publish-packages.yml | 36 ++++++++++
README.md | 6 ++
.../main/java/com/geeksville/mesh/ui/Main.kt | 2 +-
core/api/README.md | 72 +++++++++++++++++++
core/api/build.gradle.kts | 41 +++++++++++
core/api/src/main/AndroidManifest.xml | 1 +
.../meshtastic/core/service/IMeshService.aidl | 0
core/model/README.md | 19 +++++
core/model/build.gradle.kts | 17 ++++-
.../org/meshtastic/core/model/Channel.kt | 4 +-
.../meshtastic/core/model/ChannelOption.kt | 37 ++++------
.../meshtastic/core/model/RouteDiscovery.kt | 10 ---
.../meshtastic/core/model/util/CommonUtils.kt | 24 +++++++
core/proto/build.gradle.kts | 21 +++++-
core/service/build.gradle.kts | 1 +
.../core/ui/util/ModelExtensions.kt | 55 ++++++++++++++
.../feature/node/metrics/TracerouteLog.kt | 2 +-
.../radio/component/LoRaConfigItemList.kt | 4 +-
gradle/publishing.gradle.kts | 34 +++++++++
mesh_service_example/build.gradle.kts | 3 +-
.../meshserviceexample/MainActivity.kt | 4 +-
.../android/meshserviceexample/MainScreen.kt | 41 +++++++++--
settings.gradle.kts | 2 +
23 files changed, 381 insertions(+), 55 deletions(-)
create mode 100644 .github/workflows/publish-packages.yml
create mode 100644 core/api/README.md
create mode 100644 core/api/build.gradle.kts
create mode 100644 core/api/src/main/AndroidManifest.xml
rename core/{service => api}/src/main/aidl/org/meshtastic/core/service/IMeshService.aidl (100%)
create mode 100644 core/model/src/main/kotlin/org/meshtastic/core/model/util/CommonUtils.kt
create mode 100644 core/ui/src/main/kotlin/org/meshtastic/core/ui/util/ModelExtensions.kt
create mode 100644 gradle/publishing.gradle.kts
diff --git a/.github/workflows/publish-packages.yml b/.github/workflows/publish-packages.yml
new file mode 100644
index 000000000..3d20ec9e2
--- /dev/null
+++ b/.github/workflows/publish-packages.yml
@@ -0,0 +1,36 @@
+name: Publish Packages
+
+on:
+ release:
+ types: [created]
+ workflow_dispatch:
+
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ submodules: 'recursive'
+
+ - name: Set up JDK 21
+ uses: actions/setup-java@v5
+ with:
+ java-version: '21'
+ distribution: 'jetbrains'
+
+ - name: Setup Gradle
+ uses: gradle/actions/setup-gradle@v5
+ with:
+ cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
+
+ - name: Publish to GitHub Packages
+ run: ./gradlew :core:api:publish :core:model:publish :core:proto:publish
+ env:
+ GITHUB_ACTOR: ${{ github.actor }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/README.md b/README.md
index 033d29cad..59c4824f5 100644
--- a/README.md
+++ b/README.md
@@ -60,6 +60,12 @@ You can generate the documentation locally to preview your changes.
You can help translate the app into your native language using [Crowdin](https://crowdin.meshtastic.org/android).
+## API & 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).
+
## Building the Android App
> [!WARNING]
> Debug and release builds can be installed concurrently. This is solely to enable smoother development, and you should avoid running both apps simultaneously. To ensure proper function, force quit the app not in use.
diff --git a/app/src/main/java/com/geeksville/mesh/ui/Main.kt b/app/src/main/java/com/geeksville/mesh/ui/Main.kt
index f45d749eb..c45ac99f6 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/Main.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/Main.kt
@@ -105,7 +105,6 @@ import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.getString
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.model.DeviceVersion
-import org.meshtastic.core.model.toMessageRes
import org.meshtastic.core.navigation.ConnectionsRoutes
import org.meshtastic.core.navigation.ContactsRoutes
import org.meshtastic.core.navigation.MapRoutes
@@ -149,6 +148,7 @@ import org.meshtastic.core.ui.qr.ScannedQrCodeDialog
import org.meshtastic.core.ui.share.SharedContactDialog
import org.meshtastic.core.ui.theme.StatusColors.StatusBlue
import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
+import org.meshtastic.core.ui.util.toMessageRes
import org.meshtastic.feature.node.metrics.annotateTraceroute
import org.meshtastic.proto.MeshProtos
diff --git a/core/api/README.md b/core/api/README.md
new file mode 100644
index 000000000..b90733145
--- /dev/null
+++ b/core/api/README.md
@@ -0,0 +1,72 @@
+# Meshtastic Android API
+
+This module contains the stable AIDL interface and dependencies required to integrate with the Meshtastic Android app.
+
+## Integration
+
+To communicate with the Meshtastic Android service from your own application, we recommend using **JitPack**.
+
+Add the JitPack repository to your root `build.gradle.kts` (or `settings.gradle.kts`):
+
+```kotlin
+dependencyResolutionManagement {
+ repositories {
+ google()
+ mavenCentral()
+ maven { url = uri("https://jitpack.io") }
+ }
+}
+```
+
+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"
+
+ // The core AIDL interface
+ implementation("com.github.meshtastic.Meshtastic-Android:core-api:$meshtasticVersion")
+
+ // Data models (DataPacket, MeshUser, NodeInfo, etc.)
+ implementation("com.github.meshtastic.Meshtastic-Android:core-model:$meshtasticVersion")
+
+ // Protobuf definitions (Portnums, 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.
+
+ ```kotlin
+ val intent = Intent("com.geeksville.mesh.Service")
+ intent.setClassName("com.geeksville.mesh", "com.geeksville.mesh.service.MeshService")
+ bindService(intent, serviceConnection, 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 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)
+ }
+ ```
+
+## 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`.
diff --git a/core/api/build.gradle.kts b/core/api/build.gradle.kts
new file mode 100644
index 000000000..65a88a57e
--- /dev/null
+++ b/core/api/build.gradle.kts
@@ -0,0 +1,41 @@
+/*
+ * 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 .
+ */
+plugins {
+ alias(libs.plugins.meshtastic.android.library)
+ `maven-publish`
+}
+
+apply(from = rootProject.file("gradle/publishing.gradle.kts"))
+
+afterEvaluate {
+ publishing {
+ publications {
+ create("release") {
+ from(components["googleRelease"])
+ artifactId = "core-api"
+ }
+ }
+ }
+}
+
+configure {
+ namespace = "org.meshtastic.core.api"
+ buildFeatures { aidl = true }
+ publishing { singleVariant("googleRelease") { withSourcesJar() } }
+}
+
+dependencies { api(projects.core.model) }
diff --git a/core/api/src/main/AndroidManifest.xml b/core/api/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..94cbbcfc3
--- /dev/null
+++ b/core/api/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
diff --git a/core/service/src/main/aidl/org/meshtastic/core/service/IMeshService.aidl b/core/api/src/main/aidl/org/meshtastic/core/service/IMeshService.aidl
similarity index 100%
rename from core/service/src/main/aidl/org/meshtastic/core/service/IMeshService.aidl
rename to core/api/src/main/aidl/org/meshtastic/core/service/IMeshService.aidl
diff --git a/core/model/README.md b/core/model/README.md
index 9edc886dd..f1f1714b5 100644
--- a/core/model/README.md
+++ b/core/model/README.md
@@ -60,3 +60,22 @@ classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000;
+
+## Meshtastic Core Models
+
+This module contains the Parcelable data classes used by the Meshtastic Android app and its API. These models are designed to be shared between the service and client applications via AIDL.
+
+### Key Classes
+
+* **`DataPacket`**: Represents a mesh packet (text, telemetry, etc.).
+* **`MeshUser`**: Represents a user/node on the mesh.
+* **`NodeInfo`**: Contains detailed information about a node (position, SNR, battery, etc.).
+* **`Position`**: GPS location data.
+
+### Usage
+
+This module is typically used as a dependency of `core:api` but can be used independently if you need to work with Meshtastic data structures.
+
+```kotlin
+implementation("com.github.meshtastic.Meshtastic-Android:core-model:v2.7.12")
+```
\ No newline at end of file
diff --git a/core/model/build.gradle.kts b/core/model/build.gradle.kts
index 8c6bc6c63..045132519 100644
--- a/core/model/build.gradle.kts
+++ b/core/model/build.gradle.kts
@@ -20,6 +20,20 @@ plugins {
alias(libs.plugins.meshtastic.android.library)
alias(libs.plugins.meshtastic.kotlinx.serialization)
alias(libs.plugins.kotlin.parcelize)
+ `maven-publish`
+}
+
+apply(from = rootProject.file("gradle/publishing.gradle.kts"))
+
+afterEvaluate {
+ publishing {
+ publications {
+ create("release") {
+ from(components["googleRelease"])
+ artifactId = "core-model"
+ }
+ }
+ }
}
configure {
@@ -28,12 +42,11 @@ configure {
aidl = true
}
namespace = "org.meshtastic.core.model"
+ publishing { singleVariant("googleRelease") { withSourcesJar() } }
}
dependencies {
- implementation(projects.core.common)
implementation(projects.core.proto)
- implementation(projects.core.strings)
implementation(libs.androidx.annotation)
implementation(libs.kotlinx.serialization.json)
diff --git a/core/model/src/main/kotlin/org/meshtastic/core/model/Channel.kt b/core/model/src/main/kotlin/org/meshtastic/core/model/Channel.kt
index b33368172..4a3c2a7be 100644
--- a/core/model/src/main/kotlin/org/meshtastic/core/model/Channel.kt
+++ b/core/model/src/main/kotlin/org/meshtastic/core/model/Channel.kt
@@ -17,8 +17,8 @@
package org.meshtastic.core.model
import com.google.protobuf.ByteString
-import org.meshtastic.core.common.byteArrayOfInts
-import org.meshtastic.core.common.xorHash
+import org.meshtastic.core.model.util.byteArrayOfInts
+import org.meshtastic.core.model.util.xorHash
import org.meshtastic.proto.ChannelProtos
import org.meshtastic.proto.ConfigKt.loRaConfig
import org.meshtastic.proto.ConfigProtos
diff --git a/core/model/src/main/kotlin/org/meshtastic/core/model/ChannelOption.kt b/core/model/src/main/kotlin/org/meshtastic/core/model/ChannelOption.kt
index 958977053..0e8712059 100644
--- a/core/model/src/main/kotlin/org/meshtastic/core/model/ChannelOption.kt
+++ b/core/model/src/main/kotlin/org/meshtastic/core/model/ChannelOption.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2025 Meshtastic LLC
+ * Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -14,23 +14,10 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-
@file:Suppress("MagicNumber")
package org.meshtastic.core.model
-import org.jetbrains.compose.resources.StringResource
-import org.meshtastic.core.strings.Res
-import org.meshtastic.core.strings.label_long_fast
-import org.meshtastic.core.strings.label_long_moderate
-import org.meshtastic.core.strings.label_long_slow
-import org.meshtastic.core.strings.label_long_turbo
-import org.meshtastic.core.strings.label_medium_fast
-import org.meshtastic.core.strings.label_medium_slow
-import org.meshtastic.core.strings.label_short_fast
-import org.meshtastic.core.strings.label_short_slow
-import org.meshtastic.core.strings.label_short_turbo
-import org.meshtastic.core.strings.label_very_long_slow
import org.meshtastic.proto.ConfigProtos.Config.LoRaConfig
import org.meshtastic.proto.ConfigProtos.Config.LoRaConfig.ModemPreset
import org.meshtastic.proto.ConfigProtos.Config.LoRaConfig.RegionCode
@@ -308,18 +295,18 @@ enum class RegionInfo(
}
}
-enum class ChannelOption(val modemPreset: ModemPreset, val labelRes: StringResource, val bandwidth: Float) {
+enum class ChannelOption(val modemPreset: ModemPreset, val bandwidth: Float) {
// Grouped by range and speed for better readability
- VERY_LONG_SLOW(ModemPreset.VERY_LONG_SLOW, Res.string.label_very_long_slow, 0.0625f),
- LONG_TURBO(ModemPreset.LONG_TURBO, Res.string.label_long_turbo, 0.500f),
- LONG_FAST(ModemPreset.LONG_FAST, Res.string.label_long_fast, 0.250f),
- LONG_MODERATE(ModemPreset.LONG_MODERATE, Res.string.label_long_moderate, 0.125f),
- LONG_SLOW(ModemPreset.LONG_SLOW, Res.string.label_long_slow, 0.125f),
- MEDIUM_FAST(ModemPreset.MEDIUM_FAST, Res.string.label_medium_fast, 0.250f),
- MEDIUM_SLOW(ModemPreset.MEDIUM_SLOW, Res.string.label_medium_slow, 0.250f),
- SHORT_FAST(ModemPreset.SHORT_FAST, Res.string.label_short_fast, 0.250f),
- SHORT_SLOW(ModemPreset.SHORT_SLOW, Res.string.label_short_slow, 0.250f),
- SHORT_TURBO(ModemPreset.SHORT_TURBO, Res.string.label_short_turbo, 0.500f),
+ VERY_LONG_SLOW(ModemPreset.VERY_LONG_SLOW, 0.0625f),
+ LONG_TURBO(ModemPreset.LONG_TURBO, 0.500f),
+ LONG_FAST(ModemPreset.LONG_FAST, 0.250f),
+ LONG_MODERATE(ModemPreset.LONG_MODERATE, 0.125f),
+ LONG_SLOW(ModemPreset.LONG_SLOW, 0.125f),
+ MEDIUM_FAST(ModemPreset.MEDIUM_FAST, 0.250f),
+ MEDIUM_SLOW(ModemPreset.MEDIUM_SLOW, 0.250f),
+ SHORT_FAST(ModemPreset.SHORT_FAST, 0.250f),
+ SHORT_SLOW(ModemPreset.SHORT_SLOW, 0.250f),
+ SHORT_TURBO(ModemPreset.SHORT_TURBO, 0.500f),
;
companion object {
diff --git a/core/model/src/main/kotlin/org/meshtastic/core/model/RouteDiscovery.kt b/core/model/src/main/kotlin/org/meshtastic/core/model/RouteDiscovery.kt
index 9d65255af..de47abc4d 100644
--- a/core/model/src/main/kotlin/org/meshtastic/core/model/RouteDiscovery.kt
+++ b/core/model/src/main/kotlin/org/meshtastic/core/model/RouteDiscovery.kt
@@ -16,10 +16,6 @@
*/
package org.meshtastic.core.model
-import org.jetbrains.compose.resources.StringResource
-import org.meshtastic.core.strings.Res
-import org.meshtastic.core.strings.traceroute_endpoint_missing
-import org.meshtastic.core.strings.traceroute_map_no_data
import org.meshtastic.proto.MeshProtos
import org.meshtastic.proto.MeshProtos.RouteDiscovery
import org.meshtastic.proto.Portnums
@@ -129,9 +125,3 @@ fun evaluateTracerouteMapAvailability(
val hasAnyMappable = relatedNodeNums.any { positionedNodeNums.contains(it) }
return if (hasAnyMappable) TracerouteMapAvailability.Ok else TracerouteMapAvailability.NoMappableNodes
}
-
-fun TracerouteMapAvailability.toMessageRes(): StringResource? = when (this) {
- TracerouteMapAvailability.Ok -> null
- TracerouteMapAvailability.MissingEndpoints -> Res.string.traceroute_endpoint_missing
- TracerouteMapAvailability.NoMappableNodes -> Res.string.traceroute_map_no_data
-}
diff --git a/core/model/src/main/kotlin/org/meshtastic/core/model/util/CommonUtils.kt b/core/model/src/main/kotlin/org/meshtastic/core/model/util/CommonUtils.kt
new file mode 100644
index 000000000..0faad412a
--- /dev/null
+++ b/core/model/src/main/kotlin/org/meshtastic/core/model/util/CommonUtils.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2025-2026 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.meshtastic.core.model.util
+
+/** Utility function to make it easy to declare byte arrays */
+fun byteArrayOfInts(vararg ints: Int) = ByteArray(ints.size) { pos -> ints[pos].toByte() }
+
+fun xorHash(b: ByteArray) = b.fold(0) { acc, x -> acc xor (x.toInt() and BYTE_MASK) }
+
+private const val BYTE_MASK = 0xff
diff --git a/core/proto/build.gradle.kts b/core/proto/build.gradle.kts
index 217e5c018..1e20691a2 100644
--- a/core/proto/build.gradle.kts
+++ b/core/proto/build.gradle.kts
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2025 Meshtastic LLC
+ * Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -36,9 +36,26 @@ plugins {
alias(libs.plugins.meshtastic.android.library)
alias(libs.plugins.meshtastic.android.library.compose)
alias(libs.plugins.protobuf)
+ `maven-publish`
}
-configure { namespace = "org.meshtastic.core.proto" }
+apply(from = rootProject.file("gradle/publishing.gradle.kts"))
+
+afterEvaluate {
+ publishing {
+ publications {
+ create("release") {
+ from(components["googleRelease"])
+ artifactId = "core-proto"
+ }
+ }
+ }
+}
+
+configure {
+ namespace = "org.meshtastic.core.proto"
+ publishing { singleVariant("googleRelease") { withSourcesJar() } }
+}
// per protobuf-gradle-plugin docs, this is recommended for android
protobuf {
diff --git a/core/service/build.gradle.kts b/core/service/build.gradle.kts
index 309deb29d..609028f7d 100644
--- a/core/service/build.gradle.kts
+++ b/core/service/build.gradle.kts
@@ -26,6 +26,7 @@ configure {
}
dependencies {
+ api(projects.core.api)
implementation(projects.core.database)
implementation(projects.core.model)
implementation(projects.core.prefs)
diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/ModelExtensions.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/ModelExtensions.kt
new file mode 100644
index 000000000..e52108883
--- /dev/null
+++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/ModelExtensions.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2025-2026 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.meshtastic.core.ui.util
+
+import org.jetbrains.compose.resources.StringResource
+import org.meshtastic.core.model.ChannelOption
+import org.meshtastic.core.model.TracerouteMapAvailability
+import org.meshtastic.core.strings.Res
+import org.meshtastic.core.strings.label_long_fast
+import org.meshtastic.core.strings.label_long_moderate
+import org.meshtastic.core.strings.label_long_slow
+import org.meshtastic.core.strings.label_long_turbo
+import org.meshtastic.core.strings.label_medium_fast
+import org.meshtastic.core.strings.label_medium_slow
+import org.meshtastic.core.strings.label_short_fast
+import org.meshtastic.core.strings.label_short_slow
+import org.meshtastic.core.strings.label_short_turbo
+import org.meshtastic.core.strings.label_very_long_slow
+import org.meshtastic.core.strings.traceroute_endpoint_missing
+import org.meshtastic.core.strings.traceroute_map_no_data
+
+val ChannelOption.labelRes: StringResource
+ get() =
+ when (this) {
+ ChannelOption.VERY_LONG_SLOW -> Res.string.label_very_long_slow
+ ChannelOption.LONG_TURBO -> Res.string.label_long_turbo
+ ChannelOption.LONG_FAST -> Res.string.label_long_fast
+ ChannelOption.LONG_MODERATE -> Res.string.label_long_moderate
+ ChannelOption.LONG_SLOW -> Res.string.label_long_slow
+ ChannelOption.MEDIUM_FAST -> Res.string.label_medium_fast
+ ChannelOption.MEDIUM_SLOW -> Res.string.label_medium_slow
+ ChannelOption.SHORT_FAST -> Res.string.label_short_fast
+ ChannelOption.SHORT_SLOW -> Res.string.label_short_slow
+ ChannelOption.SHORT_TURBO -> Res.string.label_short_turbo
+ }
+
+fun TracerouteMapAvailability.toMessageRes(): StringResource? = when (this) {
+ TracerouteMapAvailability.Ok -> null
+ TracerouteMapAvailability.MissingEndpoints -> Res.string.traceroute_endpoint_missing
+ TracerouteMapAvailability.NoMappableNodes -> Res.string.traceroute_map_no_data
+}
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/TracerouteLog.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/TracerouteLog.kt
index 60ca71d99..3847654d5 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/TracerouteLog.kt
+++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/TracerouteLog.kt
@@ -59,7 +59,6 @@ import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.model.evaluateTracerouteMapAvailability
import org.meshtastic.core.model.fullRouteDiscovery
import org.meshtastic.core.model.getTracerouteResponse
-import org.meshtastic.core.model.toMessageRes
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.close
import org.meshtastic.core.strings.routing_error_no_response
@@ -86,6 +85,7 @@ import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
import org.meshtastic.core.ui.theme.StatusColors.StatusOrange
import org.meshtastic.core.ui.theme.StatusColors.StatusYellow
+import org.meshtastic.core.ui.util.toMessageRes
import org.meshtastic.feature.map.model.TracerouteOverlay
import org.meshtastic.feature.node.component.CooldownIconButton
import org.meshtastic.feature.node.detail.NodeRequestEffect
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/LoRaConfigItemList.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/LoRaConfigItemList.kt
index a83dba382..8e2a46754 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/LoRaConfigItemList.kt
+++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/LoRaConfigItemList.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2025 Meshtastic LLC
+ * Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -14,7 +14,6 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-
package org.meshtastic.feature.settings.radio.component
import androidx.compose.foundation.text.KeyboardActions
@@ -61,6 +60,7 @@ import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.SignedIntegerEditTextPreference
import org.meshtastic.core.ui.component.SwitchPreference
import org.meshtastic.core.ui.component.TitledCard
+import org.meshtastic.core.ui.util.labelRes
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
import org.meshtastic.feature.settings.util.hopLimits
import org.meshtastic.proto.config
diff --git a/gradle/publishing.gradle.kts b/gradle/publishing.gradle.kts
new file mode 100644
index 000000000..adaa9de7a
--- /dev/null
+++ b/gradle/publishing.gradle.kts
@@ -0,0 +1,34 @@
+import java.util.Properties
+import java.io.FileInputStream
+
+val configProperties = Properties()
+val configFile = rootProject.file("config.properties")
+if (configFile.exists()) {
+ FileInputStream(configFile).use { configProperties.load(it) }
+}
+
+val versionBase = configProperties.getProperty("VERSION_NAME_BASE") ?: "0.0.0-SNAPSHOT"
+val appVersion = System.getenv("VERSION_NAME") ?: versionBase
+
+project.version = appVersion
+project.group = "org.meshtastic"
+
+val GITHUB_ACTOR = System.getenv("GITHUB_ACTOR")
+val GITHUB_TOKEN = System.getenv("GITHUB_TOKEN")
+
+if (!GITHUB_ACTOR.isNullOrEmpty() && !GITHUB_TOKEN.isNullOrEmpty()) {
+ configure {
+ repositories {
+ maven {
+ name = "GitHubPackages"
+ url = uri("https://maven.pkg.github.com/meshtastic/Meshtastic-Android")
+ credentials {
+ username = GITHUB_ACTOR
+ password = GITHUB_TOKEN
+ }
+ }
+ }
+ }
+} else {
+ println("Skipping GitHub Packages repository configuration: GITHUB_ACTOR or GITHUB_TOKEN not set.")
+}
diff --git a/mesh_service_example/build.gradle.kts b/mesh_service_example/build.gradle.kts
index 33d20268b..8b083656a 100644
--- a/mesh_service_example/build.gradle.kts
+++ b/mesh_service_example/build.gradle.kts
@@ -37,10 +37,9 @@ configure {
}
dependencies {
+ implementation(projects.core.api)
implementation(projects.core.model)
implementation(projects.core.proto)
- implementation(projects.core.service)
- implementation(projects.core.ui)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.lifecycle.viewmodel.compose)
diff --git a/mesh_service_example/src/main/kotlin/com/meshtastic/android/meshserviceexample/MainActivity.kt b/mesh_service_example/src/main/kotlin/com/meshtastic/android/meshserviceexample/MainActivity.kt
index 178b09e96..08766a9c0 100644
--- a/mesh_service_example/src/main/kotlin/com/meshtastic/android/meshserviceexample/MainActivity.kt
+++ b/mesh_service_example/src/main/kotlin/com/meshtastic/android/meshserviceexample/MainActivity.kt
@@ -30,8 +30,8 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
+import androidx.compose.material3.MaterialTheme
import org.meshtastic.core.service.IMeshService
-import org.meshtastic.core.ui.theme.AppTheme
private const val TAG: String = "MeshServiceExample"
@@ -89,7 +89,7 @@ class MainActivity : ComponentActivity() {
registerReceiver(meshtasticReceiver, intentFilter)
}
- setContent { AppTheme { MainScreen(viewModel) } }
+ setContent { MaterialTheme { MainScreen(viewModel) } }
}
override fun onDestroy() {
diff --git a/mesh_service_example/src/main/kotlin/com/meshtastic/android/meshserviceexample/MainScreen.kt b/mesh_service_example/src/main/kotlin/com/meshtastic/android/meshserviceexample/MainScreen.kt
index 1004c2bce..d12dc11d6 100644
--- a/mesh_service_example/src/main/kotlin/com/meshtastic/android/meshserviceexample/MainScreen.kt
+++ b/mesh_service_example/src/main/kotlin/com/meshtastic/android/meshserviceexample/MainScreen.kt
@@ -75,14 +75,43 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import org.meshtastic.core.model.NodeInfo
-import org.meshtastic.core.ui.component.ListItem
-import org.meshtastic.core.ui.component.TitledCard
-import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
+
+@Composable
+fun ListItem(
+ text: String,
+ supportingText: String? = null,
+ leadingIcon: ImageVector? = null,
+ trailingIcon: ImageVector? = null,
+) {
+ androidx.compose.material3.ListItem(
+ headlineContent = { Text(text) },
+ supportingContent = supportingText?.let { { Text(it) } },
+ leadingContent = leadingIcon?.let { { Icon(it, contentDescription = null) } },
+ trailingContent = trailingIcon?.let { { Icon(it, contentDescription = null) } },
+ )
+}
+
+@Composable
+fun TitledCard(title: String, content: @Composable () -> Unit) {
+ Card(modifier = Modifier.fillMaxWidth()) {
+ Column(modifier = Modifier.padding(16.dp)) {
+ Text(
+ text = title,
+ style = MaterialTheme.typography.titleMedium,
+ color = MaterialTheme.colorScheme.primary,
+ modifier = Modifier.padding(bottom = 12.dp),
+ )
+ content()
+ }
+ }
+}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -132,7 +161,7 @@ private fun TopBarTitle(isConnected: Boolean, connectionState: String) {
Row(verticalAlignment = Alignment.CenterVertically) {
val statusColor =
if (isConnected) {
- MaterialTheme.colorScheme.StatusGreen
+ Color.Green
} else {
MaterialTheme.colorScheme.error
}
@@ -331,7 +360,7 @@ private fun NodeItemHeader(node: NodeInfo) {
.background(MaterialTheme.colorScheme.surface)
.padding(2.dp)
.clip(CircleShape)
- .background(MaterialTheme.colorScheme.StatusGreen),
+ .background(Color.Green),
)
}
}
@@ -393,7 +422,7 @@ private fun NodeItemActions(isOnline: Boolean, onAction: (String) -> Unit) {
Icon(
imageVector = Icons.Rounded.Router,
contentDescription = "Online",
- tint = MaterialTheme.colorScheme.StatusGreen.copy(alpha = 0.5f),
+ tint = androidx.compose.ui.graphics.Color.Green.copy(alpha = 0.5f),
modifier = Modifier.padding(start = 8.dp).size(20.dp),
)
}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index f30b55b09..c22659eb2 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -18,6 +18,7 @@
include(
":app",
":core:analytics",
+ ":core:api",
":core:common",
":core:data",
":core:database",
@@ -58,6 +59,7 @@ pluginManagement {
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
+ mavenLocal()
google()
mavenCentral()
maven { url = uri("https://jitpack.io") }