Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .skills/compose-ui/strings-index.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions androidApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ plugins {
alias(libs.plugins.secrets)
id("meshtastic.aboutlibraries")
id("dev.mokkery")
alias(libs.plugins.devtools.ksp)
}

val keystorePropertiesFile = rootProject.file("keystore.properties")
Expand Down Expand Up @@ -177,6 +178,8 @@ secrets {
propertiesFileName = "secrets.properties"
}

ksp { arg("appfunctions:aggregateAppFunctions", "true") }

androidComponents {
onVariants(selector().withBuildType("debug")) { variant ->
variant.flavorName?.let { flavor -> variant.applicationId.set("com.geeksville.mesh.$flavor.debug") }
Expand Down Expand Up @@ -282,6 +285,10 @@ dependencies {
googleImplementation(libs.firebase.ai.ondevice)
googleImplementation(libs.mlkit.translate)

googleImplementation(libs.androidx.appfunctions)
googleImplementation(libs.androidx.appfunctions.service)
add("kspGoogle", libs.androidx.appfunctions.compiler)

fdroidImplementation(libs.osmdroid.android)
fdroidImplementation(libs.osmdroid.geopackage) { exclude(group = "com.j256.ormlite") }
fdroidImplementation(libs.osmbonuspack)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@
package org.meshtastic.app.di

import org.koin.core.annotation.Module
import org.koin.core.annotation.Named
import org.koin.core.annotation.Single

@Module(includes = [FDroidNetworkModule::class, FdroidAiModule::class])
class FlavorModule
class FlavorModule {
@Single
@Named("googleServicesAvailable")
fun googleServicesAvailable(): Boolean = false
}
10 changes: 8 additions & 2 deletions androidApp/src/google/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,18 @@
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<application>
<application
android:name="org.meshtastic.app.GoogleMeshUtilApplication"
tools:replace="android:name">
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="${MAPS_API_KEY}" />
<property
android:name="android.app.appfunctions.app_metadata"
android:resource="@xml/app_metadata" />
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -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.app

import androidx.appfunctions.service.AppFunctionConfiguration
import org.koin.java.KoinJavaComponent.getKoin
import org.meshtastic.app.ai.appfunctions.MeshtasticAppFunctions

/**
* Google flavor Application subclass that configures App Functions.
*
* Registers a custom factory so the AppFunctions runtime can instantiate [MeshtasticAppFunctions] with its Koin-managed
* dependencies.
*/
class GoogleMeshUtilApplication :
MeshUtilApplication(),
AppFunctionConfiguration.Provider {

override val appFunctionConfiguration: AppFunctionConfiguration
get() =
AppFunctionConfiguration.Builder()
.addEnclosingClassFactory(MeshtasticAppFunctions::class.java) {
getKoin().get<MeshtasticAppFunctions>()
}
.build()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
/*
* 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.app.ai.appfunctions

import androidx.appfunctions.AppFunctionIntValueConstraint
import androidx.appfunctions.AppFunctionSerializable
import androidx.appfunctions.AppFunctionStringValueConstraint

/** Response returned when a message is successfully sent via the mesh network. */
@AppFunctionSerializable(isDescribedByKDoc = true)
data class SendMessageResponse(
/** The identifier assigned to the outgoing message. */
val messageId: Int,
/** The channel or destination the message was sent to. */
val channel: String,
/** The time the message was sent (epoch milliseconds). */
val timestamp: Long,
)

/** Response containing the current status of the Meshtastic mesh network. */
@AppFunctionSerializable(isDescribedByKDoc = true)
data class MeshStatusResponse(
/** The current radio connection state (e.g., CONNECTED, DISCONNECTED). */
@property:AppFunctionStringValueConstraint(enumValues = ["CONNECTED", "DISCONNECTED", "DEVICE_SLEEP"])
val connectionState: String,
/** The number of nodes currently online (heard within the last 2 hours). */
val onlineNodeCount: Int,
/** The total number of nodes known to the network. */
val totalNodeCount: Int,
/** The battery percentage of the connected Meshtastic device (1-100), or null if unavailable. */
val localBatteryLevel: Int?,
/** The display name of the local node, or null if not set. */
val localNodeName: String?,
)

/** Information about a single mesh node. */
@AppFunctionSerializable(isDescribedByKDoc = true)
data class NodeInfo(
/** The unique node identifier in Meshtastic hex format (e.g., !abc12345). */
val id: String,
/** The human-readable name of the node. */
val name: String,
/** The node's battery percentage (0-100), or null if unavailable. */
val batteryLevel: Int?,
/** The time this node was last heard from (epoch milliseconds). */
val lastHeard: Long,
/** Whether this node is currently considered online. */
val isOnline: Boolean,
)

/** Response containing a list of nodes visible on the mesh network. */
@AppFunctionSerializable(isDescribedByKDoc = true)
data class GetNodeListResponse(
/** List of nodes sorted by most recently heard first. */
val nodes: List<NodeInfo>,
)

/** Information about a single mesh channel. */
@AppFunctionSerializable(isDescribedByKDoc = true)
data class ChannelInfo(
/** The channel index (0-7). */
@property:AppFunctionIntValueConstraint(enumValues = [0, 1, 2, 3, 4, 5, 6, 7]) val index: Int,
/** The human-readable name of the channel. */
val name: String,
/** Whether this is the primary/default channel. */
val isPrimary: Boolean,
/** Whether uplink is enabled for this channel. */
val uplinkEnabled: Boolean,
/** Whether downlink is enabled for this channel. */
val downlinkEnabled: Boolean,
)

/** Response containing the list of available mesh channels. */
@AppFunctionSerializable(isDescribedByKDoc = true)
data class GetChannelInfoResponse(
/** List of all configured channels. */
val channels: List<ChannelInfo>,
)

/** Response containing the status of the local Meshtastic device. */
@AppFunctionSerializable(isDescribedByKDoc = true)
data class GetDeviceStatusResponse(
/** The hardware model of the device (e.g., "Meshtastic nRF52840"). */
val model: String,
/** The firmware version string. */
val firmwareVersion: String,
/** The device battery percentage (0-100), or null if not battery-powered. */
val batteryLevel: Int?,
/** The charging state (CHARGING, NOT_CHARGING, or UNKNOWN). */
@property:AppFunctionStringValueConstraint(enumValues = ["CHARGING", "NOT_CHARGING", "UNKNOWN"])
val chargingStatus: String,
/** The display name of the device, or null if not set. */
val deviceName: String?,
/** Whether the radio is currently active and connected. */
val isActive: Boolean,
)

/** Response containing detailed telemetry for a specific mesh node. */
@AppFunctionSerializable(isDescribedByKDoc = true)
data class GetNodeDetailsResponse(
/** Node ID in hex format (e.g., "!abc12345"). */
val id: String,
/** User ID string for this node. */
val userId: String,
/** Display name of the node. */
val name: String,
/** Battery percentage (0-100), or null if unavailable. */
val batteryLevel: Int?,
/** Supply voltage in volts, or null if unavailable. */
val voltage: Float?,
/** Hardware model string. */
val hardwareModel: String,
/** Firmware version string. */
val firmwareVersion: String,
/** Signal-to-noise ratio of strongest signal. */
val snr: Float,
/** Received signal strength indicator in dB. */
val rssi: Int,
/** Number of hops away from local node (-1 if unknown). */
val hopsAway: Int,
/** Channel index this node is on. */
val channel: Int,
/** Last heard timestamp (milliseconds since epoch). */
val lastHeard: Long,
/** User role or device type. */
val userRole: String,
/** Whether the user is licensed. */
val isLicensed: Boolean,
/** Latitude in degrees, or null if unknown. */
val latitude: Double?,
/** Longitude in degrees, or null if unknown. */
val longitude: Double?,
)

/** Response containing aggregate mesh network metrics. */
@AppFunctionSerializable(isDescribedByKDoc = true)
data class GetMeshMetricsResponse(
/** Total number of known nodes. */
val totalNodeCount: Int,
/** Number of nodes currently online. */
val onlineNodeCount: Int,
/** Average battery level across mesh, or null if no data. */
val averageBatteryLevel: Int?,
/** Estimated health score (0-100). */
val meshHealthScore: Int,
/** Timestamp of most recent packet (ms since epoch). */
val mostRecentPacketTime: Long,
/** Mesh uptime in seconds. */
val meshUptimeSeconds: Long,
/** Channel utilization percentage, or null if unavailable. */
val channelUtilizationPercent: Int?,
)

/** Response containing recent messages from the mesh network. */
@AppFunctionSerializable(isDescribedByKDoc = true)
data class GetRecentMessagesResponse(
/** List of recent messages ordered by most recent first. */
val messages: List<MessageInfo>,
)

/** Information about a single mesh message. */
@AppFunctionSerializable(isDescribedByKDoc = true)
data class MessageInfo(
/** Display name of the message sender. */
val senderName: String,
/** The message text content. */
val text: String,
/** Name of the channel or contact the message belongs to. */
val contactName: String,
/** Timestamp when the message was received (ms since epoch). */
val receivedTime: Long,
/** True if this message was sent by the local user. */
val fromLocal: Boolean,
/** True if this message has been read by the user. */
val read: Boolean,
)

/** Response containing a summary of unread messages across all contacts. */
@AppFunctionSerializable(isDescribedByKDoc = true)
data class GetUnreadSummaryResponse(
/** Total number of unread messages across all non-muted contacts. */
val totalUnreadCount: Int,
/** Per-contact breakdown of unread messages, sorted by most recent. */
val contacts: List<ContactUnreadInfo>,
)

/** Unread message details for a single contact or channel. */
@AppFunctionSerializable(isDescribedByKDoc = true)
data class ContactUnreadInfo(
/** Display name of the contact or channel. */
val name: String,
/** Number of unread messages from this contact. */
val unreadCount: Int,
/** Preview text of the most recent message (up to 100 chars), or null if unavailable. */
val lastMessagePreview: String?,
/** Timestamp of the most recent message (ms since epoch), or null if unavailable. */
val lastMessageTime: Long?,
)
Loading