feat(ai): Add app_metadata.xml and fix KDoc for KSP compliance

Audit findings addressed:
- Add res/xml/app_metadata.xml with LLM-facing operational patterns,
  workflow dependencies, and constraints for the AppFunctions suite
- Register app_metadata in Google flavor AndroidManifest.xml
- Convert all @AppFunctionSerializable class-level @property tags to
  inline KDoc per property (required by KSP for doc extraction)
- Add app_description string resource for displayDescription

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
James Rich
2026-05-22 18:48:50 -05:00
parent 144a3ea66e
commit 877d30cdd7
4 changed files with 123 additions and 120 deletions

View File

@@ -25,6 +25,9 @@
<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>

View File

@@ -18,218 +18,191 @@ package org.meshtastic.app.ai.appfunctions
import androidx.appfunctions.AppFunctionSerializable
/**
* Response returned when a message is successfully sent via the mesh network.
*
* @property messageId The identifier assigned to the outgoing message.
* @property channel The channel or destination the message was sent to.
* @property timestamp The time the message was sent (epoch milliseconds).
*/
/** Response returned when a message is successfully sent via the mesh network. */
@AppFunctionSerializable(isDescribedByKDoc = true)
data class SendMessageResponse(val messageId: Int, val channel: String, val timestamp: Long)
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.
*
* @property connectionState The current radio connection state (e.g., CONNECTED, DISCONNECTED).
* @property onlineNodeCount The number of nodes currently online (heard within the last 15 minutes).
* @property totalNodeCount The total number of nodes known to the network.
* @property localBatteryLevel The battery percentage of the connected Meshtastic device (1-100), or null if
* unavailable.
* @property localNodeName The display name of the local node, or null if not set.
*/
/** 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). */
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?,
)
/**
* Response containing information about a single mesh node.
*
* @property id The unique node identifier in Meshtastic hex format (e.g., !abc12345).
* @property name The human-readable name of the node.
* @property batteryLevel The node's battery percentage (0-100), or null if unavailable.
* @property lastHeard The time this node was last heard from (epoch milliseconds).
* @property isOnline Whether this node is currently considered online.
*/
/** 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.
*
* @property nodes List of nodes sorted by most recently heard first.
*/
/** Response containing a list of nodes visible on the mesh network. */
@AppFunctionSerializable(isDescribedByKDoc = true)
data class GetNodeListResponse(val nodes: List<NodeInfo>)
data class GetNodeListResponse(
/** List of nodes sorted by most recently heard first. */
val nodes: List<NodeInfo>,
)
/**
* Response containing information about a single mesh channel.
*
* @property index The channel index (0-7).
* @property name The human-readable name of the channel.
* @property isPrimary Whether this is the primary/default channel.
* @property uplinkEnabled Whether uplink is enabled for this channel.
* @property downlinkEnabled Whether downlink is enabled for this channel.
*/
/** Information about a single mesh channel. */
@AppFunctionSerializable(isDescribedByKDoc = true)
data class ChannelInfo(
/** The channel index (0-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.
*
* @property channels List of all configured channels.
*/
/** Response containing the list of available mesh channels. */
@AppFunctionSerializable(isDescribedByKDoc = true)
data class GetChannelInfoResponse(val channels: List<ChannelInfo>)
data class GetChannelInfoResponse(
/** List of all configured channels. */
val channels: List<ChannelInfo>,
)
/**
* Response containing the status of the local Meshtastic device.
*
* @property model The hardware model of the device (e.g., "Meshtastic nRF52840").
* @property firmwareVersion The firmware version string.
* @property batteryLevel The device battery percentage (0-100), or null if not battery-powered.
* @property chargingStatus The charging state (CHARGING, NOT_CHARGING, or UNKNOWN).
* @property deviceName The display name of the device, or null if not set.
* @property isActive Whether the radio is currently active and connected.
*/
/** 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). */
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.
*
* @property id Node ID in hex format (e.g., "!abc12345").
* @property userId User ID string for this node.
* @property name Display name of the node.
* @property batteryLevel Battery percentage (0-100), or null if unavailable.
* @property voltage Supply voltage in volts, or null if unavailable.
* @property hardwareModel Hardware model string.
* @property firmwareVersion Firmware version string.
* @property snr Signal-to-noise ratio of strongest signal.
* @property rssi Received signal strength indicator in dB.
* @property hopsAway Number of hops away from local node (-1 if unknown).
* @property channel Channel index this node is on.
* @property lastHeard Last heard timestamp (milliseconds since epoch).
* @property userRole User role or device type.
* @property isLicensed Whether the user is licensed.
* @property latitude Latitude in degrees, or null if unknown.
* @property longitude Longitude in degrees, or null if unknown.
*/
/** 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.
*
* @property totalNodeCount Total number of known nodes.
* @property onlineNodeCount Number of nodes currently online.
* @property averageBatteryLevel Average battery level across mesh, or null.
* @property meshHealthScore Estimated health score (0-100).
* @property mostRecentPacketTime Timestamp of most recent packet (ms since epoch).
* @property meshUptimeSeconds Mesh uptime in seconds.
* @property channelUtilizationPercent Channel utilization percentage, or null if unavailable.
*/
/** 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.
*
* @property messages List of recent messages ordered by most recent first.
*/
/** Response containing recent messages from the mesh network. */
@AppFunctionSerializable(isDescribedByKDoc = true)
data class GetRecentMessagesResponse(val messages: List<MessageInfo>)
data class GetRecentMessagesResponse(
/** List of recent messages ordered by most recent first. */
val messages: List<MessageInfo>,
)
/**
* Information about a single mesh message.
*
* @property senderName Display name of the message sender.
* @property text The message text content.
* @property contactName Name of the channel or contact the message belongs to.
* @property receivedTime Timestamp when the message was received (ms since epoch).
* @property fromLocal True if this message was sent by the local user.
* @property read True if this message has been read by the user.
*/
/** 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.
*
* @property totalUnreadCount Total number of unread messages across all non-muted contacts.
* @property contacts Per-contact breakdown of unread messages, sorted by most recent.
*/
/** Response containing a summary of unread messages across all contacts. */
@AppFunctionSerializable(isDescribedByKDoc = true)
data class GetUnreadSummaryResponse(val totalUnreadCount: Int, val contacts: List<ContactUnreadInfo>)
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.
*
* @property name Display name of the contact or channel.
* @property unreadCount Number of unread messages from this contact.
* @property lastMessagePreview Preview text of the most recent message (up to 100 chars), or null if unavailable.
* @property lastMessageTime Timestamp of the most recent message (ms since epoch), or null if unavailable.
*/
/** 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?,
)

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<AppFunctionAppMetadata xmlns:appfn="http://schemas.android.com/apk/androidx.appfunctions"
appfn:description="Meshtastic is a mesh radio networking app for off-grid communications using LoRa devices.
Operational Patterns:
- Use 'getMeshStatus' to verify the radio is connected before calling functions that require a live connection.
- Use 'getNodeList' or 'getNodeDetails' to resolve node names and IDs before calling 'sendMessage' with a recipientName.
- Use 'getChannelInfo' to discover available channel names before sending broadcast messages to non-primary channels.
- 'getRecentMessages' and 'getUnreadSummary' work offline from local message history and do not require an active connection.
- Prefer 'getUnreadSummary' for a quick overview, then 'getRecentMessages' with a contactName filter for details.
Constraints:
- Message text is limited to 237 bytes.
- Node and channel name resolution is fuzzy (partial matches accepted).
- Rate limit: 10 messages per 60 seconds via 'sendMessage'.
- Message history limit: 1-50 messages per query (default 20)."
appfn:displayDescription="@string/app_description" />

View File

@@ -17,4 +17,5 @@
-->
<resources>
<string name="app_name">Meshtastic</string>
<string name="app_description">Off-grid mesh networking for secure, long-range communications via LoRa radio.</string>
</resources>