Skip to content

feat(ai): Add App Functions for system AI integration#5585

Open
jamesarich wants to merge 16 commits into
mainfrom
jamesarich/crispy-barnacle
Open

feat(ai): Add App Functions for system AI integration#5585
jamesarich wants to merge 16 commits into
mainfrom
jamesarich/crispy-barnacle

Conversation

@jamesarich
Copy link
Copy Markdown
Collaborator

@jamesarich jamesarich commented May 23, 2026

Motivation

Enable system AI assistants (Gemini, etc.) to interact with Meshtastic mesh networks through the Android AppFunctions framework. This gives AI agents read access to mesh status, nodes, channels, messages, and metrics -- plus the ability to send messages -- without opening the app UI.

AppFunctions act as on-device MCP (Model Context Protocol) tools, making the app's capabilities discoverable and invocable by authorized system agents on Android 16+.

Approach

Implements 9 App Functions in a layered KMP architecture:

  • Core layer (core/data/ai/): Platform-agnostic AiFunctionProvider interface and implementation with rate limiting, fuzzy name resolution, and structured error handling
  • Android layer (androidApp/src/google/): @AppFunction declarations with @AppFunctionSerializable response models, wired via Koin DI
  • Google flavor only: Functions are only available in the Google Play build (requires the AppFunctions runtime on API 36+)

Functions implemented

Function Type Description
sendMessage Write Send a text message to a node or channel
getMeshStatus Read Connection state + node counts
getNodeList Read All nodes with name, signal, battery, coordinates
getChannelInfo Read Channel name, role, and PSK status
getDeviceStatus Read Local radio firmware, battery, region
getNodeDetails Read Detailed info for a specific node
getMeshMetrics Read Network health metrics (online ratio, signal stats)
getRecentMessages Read Recent message history with contact resolution
getUnreadSummary Read Unread message counts per contact

Notable design decisions

  • Value constraint annotations: @AppFunctionStringValueConstraint on enum-like string fields (connectionState, chargingStatus) and @AppFunctionIntValueConstraint on bounded integers (channel index, message limit) so AI agents know valid values upfront
  • Semantic exception types: AppFunctionNotSupportedException when disconnected (code 3501), AppFunctionElementNotFoundException when nodes/channels not found (code 2001), AppFunctionInvalidArgumentException for bad input (code 1001)
  • Rate limiting: 10 requests/minute per function to prevent AI loops from overwhelming the radio
  • Fuzzy name resolution: Contacts can be referenced by partial name match (e.g., "James" resolves to the correct node)
  • Graceful disconnection handling: Functions that can answer from local DB (messages, unread) work even when disconnected; others throw NotSupportedException
  • KDoc-driven schema: All @AppFunctionSerializable properties use inline KDoc (not class-level @property tags) per KSP extraction requirements
  • app_metadata.xml: Provides LLM-facing operational patterns and constraints for the AI runtime

Testing

  • Unit tests for core provider logic, fuzzy name resolution, and rate limiter
  • Verified on android-36.1 emulator via adb shell cmd app_function execute-app-function -- all 9 functions respond correctly with proper constraint metadata and error codes
  • Library version: androidx.appfunctions:appfunctions:1.0.0-alpha09 (latest as of 2026-05-06)

Notes for reviewers

  • This PR targets the google build flavor only; fdroid builds are unaffected
  • If refactor: Remove AIDL API and modernize service architecture #5586 merges first, a mechanical rebase is needed to rename DataPacket.ID_BROADCAST to NodeAddress.ID_BROADCAST (7 import references in AiFunctionProviderImpl.kt)
  • The specs/ directory contains the feature spec, plan, and task breakdown used during development

@github-actions github-actions Bot added the enhancement New feature or request label May 23, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 23, 2026

✅ Docs staleness check passed

This PR includes updates to docs/en/ alongside the source changes. Thank you!

@jamesarich jamesarich marked this pull request as ready for review May 29, 2026 17:12
@jamesarich jamesarich added this to the 2.8.0 milestone May 30, 2026
jamesarich added a commit that referenced this pull request May 30, 2026
Squash merge of PR #5585 into release/2.8.0.

Enable system AI assistants (Gemini, etc.) to interact with Meshtastic mesh
networks through the Android AppFunctions framework. Adds read access to mesh
status, nodes, channels, messages, and metrics plus the ability to send
messages without opening the app UI.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jamesarich
Copy link
Copy Markdown
Collaborator Author

🔗 Release 2.8.0 Integration Report

Branch: release/2.8.0 | Status: ✅ Demo verified on emulator with live node

Integration Changes Required

When merging alongside the other 2.8.0 PRs, the following adaptations were needed:

  1. FlavorModule.kt — Had to merge DI declarations from both App Functions and Car App modules into a single FlavorModule. Both PRs independently declare Google-flavor DI; when combined, imports and includes arrays needed unification.

  2. AiFunctionProviderImpl.kt — After AIDL removal (refactor: Remove AIDL API and modernize service architecture #5586), the ServiceRepository interface changed. sendMessage() parameters shifted from positional to the new SendMessageUseCase pattern. Updated to use sendMessageUseCase.invoke() instead of direct service calls.

  3. MeshtasticAppFunctionsTest.kt — Tests needed updating for:

Demo Results (Emulator + Live Node)

All 9 App Functions registered, indexed, and executed successfully:

  • getDeviceStatus → HELTEC_V3, FW 2.7.20, battery 100%
  • getNodeList → 200 nodes
  • getMeshStatus → Connected, 21 online / 200 total
  • sendMessage → ✅ Successfully sent DM to specific node via AI function

Merge Guidance

  • Merge order: This PR should merge first (no dependencies on others)
  • Post-merge: Will need a follow-up fixup if refactor: Remove AIDL API and modernize service architecture #5586 (AIDL removal) merges afterward, to update AiFunctionProviderImpl for the new ServiceRepository API
  • Alternative: Rebase onto the AIDL removal branch before merging to avoid the fixup

@jamesarich
Copy link
Copy Markdown
Collaborator Author

🎉 2.8.0 Integration Demo — All Features Verified

Demo branch: release/2.8.0
Environment: API 36 emulator (arm64) → HELTEC_V3 node (FW 2.7.20) via TCP
Build: assembleGoogleDebug + spotlessApply + detekt + test + allTests all ✅

Features Verified

Feature PR Status Evidence
App Functions (AI) #5585 9/9 functions indexed, all return live mesh data
Car App Library #5633 Module compiles, DI wired, service registered
FTS5 Message Search #5373 FTS5 tables created, queries passing
AIDL Removal #5586 Clean architecture, all tests green
Firmware Lockdown #5439 Proto fields wired, coordinator tests pass
Mesh Discovery #5275 Scan engine + DB + map layers all integrated

App Functions Live Test Results

// getDeviceStatus
{"model":"HELTEC_V3","firmwareVersion":"2.7.20.6658ec2","batteryLevel":100,"isActive":true}

// getMeshStatus  
{"connectionState":"CONNECTED","totalNodeCount":200,"onlineNodeCount":21,"localNodeName":"Meshtastic 18c4"}

// sendMessage (DM to specific node)
{"messageId":1769805483,"channel":"DM to olm3c @ Meshtastic"}

Recommended Merge Order

  1. feat(ai): Add App Functions for system AI integration #5585 (App Functions) — no deps
  2. feat(car): Android Car App Library integration #5633 (Car App) — no deps
  3. feat: FTS5 full-text message search #5373 (FTS5 Search) — no deps, takes schema v39
  4. refactor: Remove AIDL API and modernize service architecture #5586 (AIDL Removal) — architectural pivot
  5. feat: firmware lockdown mode (provision / unlock / lock-now) #5439 (Lockdown) — needs AIDL removal + proto branch merge
  6. feat(discovery): mesh network discovery #5275 (Discovery) — needs FTS5 (schema v40) + AIDL removal

Blockers for Production Merge

See individual PR comments for per-PR integration details.

jamesarich and others added 15 commits May 31, 2026 09:05
Expose Meshtastic mesh networking capabilities (sendMessage, getMeshStatus)
to Android system AI agents via the App Functions API.

Architecture:
- AiFunctionProvider interface in core/data commonMain (platform-agnostic)
- FuzzyNameResolver for node/channel name matching (LCS algorithm)
- RateLimiter with 5-call/60s sliding window to protect mesh radio
- AiFunctionProviderImpl wiring repositories and use cases
- @appfunction declarations in androidApp Google flavor only
- GoogleMeshUtilApplication with AppFunctionConfiguration.Provider
- DI via AppFunctionsModule included in FlavorModule

Key design decisions:
- No confirmation dialog (AI invocation = user intent)
- Fuzzy name matching with 50% LCS threshold, error on ambiguity
- Admin channels excluded from resolution
- 5-second operation timeout
- 237-byte message length limit (Meshtastic standard)

Includes unit tests for RateLimiter and FuzzyNameResolver (LCS algorithm).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Previously, AiFunctionProviderImpl returned a hardcoded messageId of 0
for all successful sends, preventing unique message identification. The
underlying SendMessageUseCase generates a packetId but had no return
value to expose it.

Changes:
- Modified SendMessageUseCase interface to return Int (the packetId)
- Updated SendMessageUseCaseImpl to return the generated packetId
- Updated AiFunctionProviderImpl to capture and use the returned messageId

This enables the AI system to track individual messages and correlate
responses to specific send requests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…fo, getDeviceStatus)

Implement three Phase 2a read-only query functions for AI agent integration:

Core Data (commonMain):
- Extended AiFunctionProvider interface with 3 new methods
- Implemented getNodeList() - queries all mesh nodes with battery and online status
- Implemented getChannelInfo() - lists mesh channels with settings
- Implemented getDeviceStatus() - returns local device info and status
- Added result types: GetNodeListResult, GetChannelInfoResult, GetDeviceStatusResult
- Added data models: NodeSummary, ChannelSummary, DeviceStatus
- All functions protected by timeout (5s), rate limiter (5 calls/60s), connection check

Android AppFunctions:
- Added 3 @appfunction methods with KSP annotations
- Response models marked @AppFunctionSerializable for AI runtime
- Proper exception handling and timeout protection

Constants:
- HEX_RADIX (16): For node ID formatting
- MS_PER_SEC (1000): Time unit conversions
- ONLINE_THRESHOLD_MS (30000): Node online detection threshold

Fixes:
- Use node.user.long_name (not longName)
- Use node.deviceMetrics.battery_level?.coerceIn() for nullable battery
- Use nodeRepository.nodeDBbyNum (not nodes)
- Convert node.lastHeard (seconds) to milliseconds
- Suppress MagicNumber and ReturnCount lints appropriately

All builds and tests pass:
✓ :core:data:compileKotlinJvm
✓ :androidApp:compileGoogleDebugKotlin
✓ :androidApp:compileFdroidDebugKotlin
✓ detekt clean
✓ spotlessApply clean
✓ :core:data:allTests pass

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Extend AiFunctionProvider with two new suspend methods for advanced queries
- getNodeDetails: Retrieve per-node telemetry (16 fields) by hex or user ID
- getMeshMetrics: Aggregate mesh statistics and compute health score
- Add result types (GetNodeDetailsResult, GetMeshMetricsResult) and data models
- Add response models (@AppFunctionSerializable) for KSP serialization
- Both methods support timeout protection and rate limiting
- Health score calculation: 50 base + 50 online ratio, clamped 0-100
- All JVM, Android, detekt, spotless checks passing

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fix location filtering: Only treat (0,0) as invalid if position.time is 0
  - Previously filtered all (0,0) coords as null, losing valid equatorial data
  - Now checks position.time to distinguish 'no fix' from real coordinates

- Fix mostRecentPacketTime: Use max lastHeard from all nodes, not current time
  - Previously returned current time, making mesh appear always active
  - Now computes from actual node activity data

- Fix meshUptimeSeconds: Use local device's actual uptime, not epoch time
  - Previously returned epoch seconds (~1.7B), not elapsed time
  - Now uses device's DeviceMetrics.uptime_seconds

All checks passing: Android (Google/fdroid), detekt, spotless, unit tests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…Heard=0

- Add takeIf check to distinguish lastHeard=0 (never heard) from no nodes
- Previously: maxOfOrNull returns 0, Elvis operator doesn't trigger (0 is not null)
- Now: takeIf { it > 0 } filters out zero, falling back to current time
- Ensures API returns meaningful timestamp instead of epoch 1970

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add TimeoutCancellationException handling to getNodeDetails and getMeshMetrics
  AppFunctions (consistent with Phase 1 functions)
- Rethrow CancellationException in all provider catch blocks to preserve
  structured concurrency semantics
- Fix voltage documentation: millivolts → volts (matches actual Float field)
- Fix stale test comment referencing non-existent test class

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- SendMessageUseCase now rethrows exceptions after logging (Finding #1)
- AiFunctionProviderImpl catches send failures and returns InvalidArgument
- Added AiFunctionProviderImplTest with 10 unit tests covering:
  - Disconnection checks for all three function groups
  - Node lookup (found, not found, null position, invalid hex)
  - Metrics aggregation (active nodes, empty, zero lastHeard, degraded health)
  - Rate limiting behavior
- Expanded FuzzyNameResolverTest with 8 behavioral tests (Finding #5):
  - resolveNodeName: exact, fuzzy, ambiguous, not found
  - resolveChannelName: exact, admin exclusion, empty channels

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The model's isOnline property uses the project-standard 2-hour window
(onlineTimeThreshold), consistent with getMeshStatus().onlineNodeCount.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The interface now returns Int (packetId), so the test mock must
return an Int instead of Unit.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Phase 3 of App Functions integration: read-only message history
functions that enable 'catch me up' voice queries via system AI.

New functions:
- getRecentMessages: Retrieve recent messages with optional contact
  filter and configurable limit (1-50, default 20)
- getUnreadSummary: Per-contact unread breakdown excluding muted
  contacts, sorted by most recent

Implementation details:
- KMP interface + sealed result types in core:data
- Android @appfunction declarations with @AppFunctionSerializable models
- Fuzzy name resolution for contact filtering
- Channel name resolution for broadcast contacts
- Tests for contact-not-found and empty unread scenarios

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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>
- Add @AppFunctionStringValueConstraint to connectionState and
  chargingStatus properties for agent-visible enum values
- Add @AppFunctionIntValueConstraint to ChannelInfo.index (0-7) and
  getRecentMessages limit parameter (1,5,10,20,50)
- Replace AppFunctionInvalidArgumentException with semantic types:
  - NotConnected states -> AppFunctionNotSupportedException (code 3501)
  - NotFound states -> AppFunctionElementNotFoundException (code 2001)
- Keeps AppFunctionInvalidArgumentException for actual bad input
  (ambiguous names, invalid args, rate limits, generic errors)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add a settings sub-screen that allows users to control which App
Functions are exposed to system AI (Gemini). Includes a master toggle
and per-function granularity for all 9 functions.

- AppFunctionsPrefs interface + DataStore implementation in core/prefs
- AppFunctionsSettingsScreen with master + per-function toggles
- AppFunctionStateSync in Google flavor syncs prefs to AppFunctionManager
- Settings visibility is flavor-gated via Koin-provided boolean
  (googleServicesAvailable) -- hidden on fdroid, shown on Google
- Analytics toggle visibility also now properly flavor-gated

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add MeshtasticAppFunctionsTest with coverage for all 9 AI functions
  (getDeviceStatus, getNodeList, sendMessage, etc.)
- Fix GoogleAiModule to inject NodeRepository into GeminiNanoDocAssistant
  (constructor changed on main)
- Add App Check TODO note for future cloud fallback consideration

Found during release/2.8.0 integration testing.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jamesarich jamesarich force-pushed the jamesarich/crispy-barnacle branch from 439e821 to b46d1a2 Compare May 31, 2026 14:07
@codecov
Copy link
Copy Markdown

codecov Bot commented May 31, 2026

⚠️ JUnit XML file not found

The CLI was unable to find any JUnit XML files to upload.
For more help, visit our troubleshooting guide.

The App Functions branch accidentally carried a proto pointer to a
non-public commit. Reset to main's v2.7.24-2 tag.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant