Skip to content

Commit 2a7182b

Browse files
jamesarichCopilot
andcommitted
feat(ai): Add App Functions for system AI integration (#5585)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent bfe0bf4 commit 2a7182b

37 files changed

Lines changed: 4471 additions & 8 deletions

File tree

.skills/compose-ui/strings-index.txt

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

androidApp/build.gradle.kts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ plugins {
3232
alias(libs.plugins.secrets)
3333
id("meshtastic.aboutlibraries")
3434
id("dev.mokkery")
35+
alias(libs.plugins.devtools.ksp)
3536
}
3637

3738
val keystorePropertiesFile = rootProject.file("keystore.properties")
@@ -178,6 +179,8 @@ secrets {
178179
propertiesFileName = "secrets.properties"
179180
}
180181

182+
ksp { arg("appfunctions:aggregateAppFunctions", "true") }
183+
181184
androidComponents {
182185
onVariants(selector().withBuildType("debug")) { variant ->
183186
variant.flavorName?.let { flavor -> variant.applicationId.set("com.geeksville.mesh.$flavor.debug") }
@@ -283,6 +286,10 @@ dependencies {
283286
googleImplementation(libs.firebase.ai.ondevice)
284287
googleImplementation(libs.mlkit.translate)
285288

289+
googleImplementation(libs.androidx.appfunctions)
290+
googleImplementation(libs.androidx.appfunctions.service)
291+
add("kspGoogle", libs.androidx.appfunctions.compiler)
292+
286293
fdroidImplementation(libs.osmdroid.android)
287294
fdroidImplementation(libs.osmdroid.geopackage) { exclude(group = "com.j256.ormlite") }
288295
fdroidImplementation(libs.osmbonuspack)

androidApp/src/fdroid/kotlin/org/meshtastic/app/di/FlavorModule.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@
1717
package org.meshtastic.app.di
1818

1919
import org.koin.core.annotation.Module
20+
import org.koin.core.annotation.Named
21+
import org.koin.core.annotation.Single
2022

2123
@Module(includes = [FDroidNetworkModule::class, FdroidAiModule::class])
22-
class FlavorModule
24+
class FlavorModule {
25+
@Single
26+
@Named("googleServicesAvailable")
27+
fun googleServicesAvailable(): Boolean = false
28+
}

androidApp/src/google/AndroidManifest.xml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,18 @@
1616
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
1717
-->
1818

19-
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
19+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
20+
xmlns:tools="http://schemas.android.com/tools">
2021

21-
<application>
22+
<application
23+
android:name="org.meshtastic.app.GoogleMeshUtilApplication"
24+
tools:replace="android:name">
2225
<meta-data
2326
android:name="com.google.android.geo.API_KEY"
2427
android:value="${MAPS_API_KEY}" />
28+
<property
29+
android:name="android.app.appfunctions.app_metadata"
30+
android:resource="@xml/app_metadata" />
2531
</application>
2632

2733
</manifest>
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright (c) 2026 Meshtastic LLC
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
*/
17+
package org.meshtastic.app
18+
19+
import androidx.appfunctions.service.AppFunctionConfiguration
20+
import org.koin.java.KoinJavaComponent.getKoin
21+
import org.meshtastic.app.ai.appfunctions.MeshtasticAppFunctions
22+
23+
/**
24+
* Google flavor Application subclass that configures App Functions.
25+
*
26+
* Registers a custom factory so the AppFunctions runtime can instantiate [MeshtasticAppFunctions] with its Koin-managed
27+
* dependencies.
28+
*/
29+
class GoogleMeshUtilApplication :
30+
MeshUtilApplication(),
31+
AppFunctionConfiguration.Provider {
32+
33+
override val appFunctionConfiguration: AppFunctionConfiguration
34+
get() =
35+
AppFunctionConfiguration.Builder()
36+
.addEnclosingClassFactory(MeshtasticAppFunctions::class.java) {
37+
getKoin().get<MeshtasticAppFunctions>()
38+
}
39+
.build()
40+
}
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
/*
2+
* Copyright (c) 2026 Meshtastic LLC
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
*/
17+
package org.meshtastic.app.ai.appfunctions
18+
19+
import androidx.appfunctions.AppFunctionIntValueConstraint
20+
import androidx.appfunctions.AppFunctionSerializable
21+
import androidx.appfunctions.AppFunctionStringValueConstraint
22+
23+
/** Response returned when a message is successfully sent via the mesh network. */
24+
@AppFunctionSerializable(isDescribedByKDoc = true)
25+
data class SendMessageResponse(
26+
/** The identifier assigned to the outgoing message. */
27+
val messageId: Int,
28+
/** The channel or destination the message was sent to. */
29+
val channel: String,
30+
/** The time the message was sent (epoch milliseconds). */
31+
val timestamp: Long,
32+
)
33+
34+
/** Response containing the current status of the Meshtastic mesh network. */
35+
@AppFunctionSerializable(isDescribedByKDoc = true)
36+
data class MeshStatusResponse(
37+
/** The current radio connection state (e.g., CONNECTED, DISCONNECTED). */
38+
@property:AppFunctionStringValueConstraint(enumValues = ["CONNECTED", "DISCONNECTED", "DEVICE_SLEEP"])
39+
val connectionState: String,
40+
/** The number of nodes currently online (heard within the last 2 hours). */
41+
val onlineNodeCount: Int,
42+
/** The total number of nodes known to the network. */
43+
val totalNodeCount: Int,
44+
/** The battery percentage of the connected Meshtastic device (1-100), or null if unavailable. */
45+
val localBatteryLevel: Int?,
46+
/** The display name of the local node, or null if not set. */
47+
val localNodeName: String?,
48+
)
49+
50+
/** Information about a single mesh node. */
51+
@AppFunctionSerializable(isDescribedByKDoc = true)
52+
data class NodeInfo(
53+
/** The unique node identifier in Meshtastic hex format (e.g., !abc12345). */
54+
val id: String,
55+
/** The human-readable name of the node. */
56+
val name: String,
57+
/** The node's battery percentage (0-100), or null if unavailable. */
58+
val batteryLevel: Int?,
59+
/** The time this node was last heard from (epoch milliseconds). */
60+
val lastHeard: Long,
61+
/** Whether this node is currently considered online. */
62+
val isOnline: Boolean,
63+
)
64+
65+
/** Response containing a list of nodes visible on the mesh network. */
66+
@AppFunctionSerializable(isDescribedByKDoc = true)
67+
data class GetNodeListResponse(
68+
/** List of nodes sorted by most recently heard first. */
69+
val nodes: List<NodeInfo>,
70+
)
71+
72+
/** Information about a single mesh channel. */
73+
@AppFunctionSerializable(isDescribedByKDoc = true)
74+
data class ChannelInfo(
75+
/** The channel index (0-7). */
76+
@property:AppFunctionIntValueConstraint(enumValues = [0, 1, 2, 3, 4, 5, 6, 7]) val index: Int,
77+
/** The human-readable name of the channel. */
78+
val name: String,
79+
/** Whether this is the primary/default channel. */
80+
val isPrimary: Boolean,
81+
/** Whether uplink is enabled for this channel. */
82+
val uplinkEnabled: Boolean,
83+
/** Whether downlink is enabled for this channel. */
84+
val downlinkEnabled: Boolean,
85+
)
86+
87+
/** Response containing the list of available mesh channels. */
88+
@AppFunctionSerializable(isDescribedByKDoc = true)
89+
data class GetChannelInfoResponse(
90+
/** List of all configured channels. */
91+
val channels: List<ChannelInfo>,
92+
)
93+
94+
/** Response containing the status of the local Meshtastic device. */
95+
@AppFunctionSerializable(isDescribedByKDoc = true)
96+
data class GetDeviceStatusResponse(
97+
/** The hardware model of the device (e.g., "Meshtastic nRF52840"). */
98+
val model: String,
99+
/** The firmware version string. */
100+
val firmwareVersion: String,
101+
/** The device battery percentage (0-100), or null if not battery-powered. */
102+
val batteryLevel: Int?,
103+
/** The charging state (CHARGING, NOT_CHARGING, or UNKNOWN). */
104+
@property:AppFunctionStringValueConstraint(enumValues = ["CHARGING", "NOT_CHARGING", "UNKNOWN"])
105+
val chargingStatus: String,
106+
/** The display name of the device, or null if not set. */
107+
val deviceName: String?,
108+
/** Whether the radio is currently active and connected. */
109+
val isActive: Boolean,
110+
)
111+
112+
/** Response containing detailed telemetry for a specific mesh node. */
113+
@AppFunctionSerializable(isDescribedByKDoc = true)
114+
data class GetNodeDetailsResponse(
115+
/** Node ID in hex format (e.g., "!abc12345"). */
116+
val id: String,
117+
/** User ID string for this node. */
118+
val userId: String,
119+
/** Display name of the node. */
120+
val name: String,
121+
/** Battery percentage (0-100), or null if unavailable. */
122+
val batteryLevel: Int?,
123+
/** Supply voltage in volts, or null if unavailable. */
124+
val voltage: Float?,
125+
/** Hardware model string. */
126+
val hardwareModel: String,
127+
/** Firmware version string. */
128+
val firmwareVersion: String,
129+
/** Signal-to-noise ratio of strongest signal. */
130+
val snr: Float,
131+
/** Received signal strength indicator in dB. */
132+
val rssi: Int,
133+
/** Number of hops away from local node (-1 if unknown). */
134+
val hopsAway: Int,
135+
/** Channel index this node is on. */
136+
val channel: Int,
137+
/** Last heard timestamp (milliseconds since epoch). */
138+
val lastHeard: Long,
139+
/** User role or device type. */
140+
val userRole: String,
141+
/** Whether the user is licensed. */
142+
val isLicensed: Boolean,
143+
/** Latitude in degrees, or null if unknown. */
144+
val latitude: Double?,
145+
/** Longitude in degrees, or null if unknown. */
146+
val longitude: Double?,
147+
)
148+
149+
/** Response containing aggregate mesh network metrics. */
150+
@AppFunctionSerializable(isDescribedByKDoc = true)
151+
data class GetMeshMetricsResponse(
152+
/** Total number of known nodes. */
153+
val totalNodeCount: Int,
154+
/** Number of nodes currently online. */
155+
val onlineNodeCount: Int,
156+
/** Average battery level across mesh, or null if no data. */
157+
val averageBatteryLevel: Int?,
158+
/** Estimated health score (0-100). */
159+
val meshHealthScore: Int,
160+
/** Timestamp of most recent packet (ms since epoch). */
161+
val mostRecentPacketTime: Long,
162+
/** Mesh uptime in seconds. */
163+
val meshUptimeSeconds: Long,
164+
/** Channel utilization percentage, or null if unavailable. */
165+
val channelUtilizationPercent: Int?,
166+
)
167+
168+
/** Response containing recent messages from the mesh network. */
169+
@AppFunctionSerializable(isDescribedByKDoc = true)
170+
data class GetRecentMessagesResponse(
171+
/** List of recent messages ordered by most recent first. */
172+
val messages: List<MessageInfo>,
173+
)
174+
175+
/** Information about a single mesh message. */
176+
@AppFunctionSerializable(isDescribedByKDoc = true)
177+
data class MessageInfo(
178+
/** Display name of the message sender. */
179+
val senderName: String,
180+
/** The message text content. */
181+
val text: String,
182+
/** Name of the channel or contact the message belongs to. */
183+
val contactName: String,
184+
/** Timestamp when the message was received (ms since epoch). */
185+
val receivedTime: Long,
186+
/** True if this message was sent by the local user. */
187+
val fromLocal: Boolean,
188+
/** True if this message has been read by the user. */
189+
val read: Boolean,
190+
)
191+
192+
/** Response containing a summary of unread messages across all contacts. */
193+
@AppFunctionSerializable(isDescribedByKDoc = true)
194+
data class GetUnreadSummaryResponse(
195+
/** Total number of unread messages across all non-muted contacts. */
196+
val totalUnreadCount: Int,
197+
/** Per-contact breakdown of unread messages, sorted by most recent. */
198+
val contacts: List<ContactUnreadInfo>,
199+
)
200+
201+
/** Unread message details for a single contact or channel. */
202+
@AppFunctionSerializable(isDescribedByKDoc = true)
203+
data class ContactUnreadInfo(
204+
/** Display name of the contact or channel. */
205+
val name: String,
206+
/** Number of unread messages from this contact. */
207+
val unreadCount: Int,
208+
/** Preview text of the most recent message (up to 100 chars), or null if unavailable. */
209+
val lastMessagePreview: String?,
210+
/** Timestamp of the most recent message (ms since epoch), or null if unavailable. */
211+
val lastMessageTime: Long?,
212+
)

0 commit comments

Comments
 (0)