Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
b481169
build: fix axionRelease plugin for composite build inclusion
jamesarich May 3, 2026
ef418ad
feat(core): add RadioClient.textMessages Flow<MeshPacket> (Gap B)
jamesarich May 3, 2026
ee5ce03
feat(core): add RadioClient.channels StateFlow<List<Channel>?> (Gap C)
jamesarich May 3, 2026
61b22e6
feat(transport-ble): add BleTransport(address: String) factory for An…
jamesarich May 3, 2026
08fbb42
feat: complete SDK integration gaps and QoL improvements
jamesarich May 4, 2026
1f551bb
fix: firmware compliance audit — P0/P1 bug fixes
jamesarich May 4, 2026
87c49ef
docs: update protocol.md, SPEC.md, api-reference.md for audit findings
jamesarich May 4, 2026
edfa8aa
feat: implement P2 feature gaps — admin ops, telemetry, DeviceUIConfig
jamesarich May 4, 2026
7648ba5
feat: complete AdminApi — add final 9 operations for full proto coverage
jamesarich May 4, 2026
f97d27f
feat: add sendRaw(ToRadio) API for MQTT proxy and XModem
jamesarich May 5, 2026
02d0a26
feat: add protocol utilities, higher-level APIs, and Store-and-Forwar…
jamesarich May 5, 2026
5f3e13f
feat: wire engine — congestion emission, presence timer, retry extens…
jamesarich May 5, 2026
42d65cf
feat: SFPP protocol handling + MeshTopology graph utility
jamesarich May 5, 2026
68b92dd
docs: KDoc improvements + edge case tests
jamesarich May 5, 2026
8ebf8d2
fix: normalize SFPP destination in SfppLinkProvided event + fix MeshT…
jamesarich May 5, 2026
2a67099
feat: add remote admin API (forNode), sendReaction, getDeviceMetadata
jamesarich May 6, 2026
0f4d6b9
feat: add requestNodeInfo, fix editSettings remote routing
jamesarich May 6, 2026
82584ac
feat: AdminResult extensions, MeshTopology cache, close() safety docs
jamesarich May 6, 2026
91c8bd5
fix: critical SDK improvements — thread-safety, timeout, flow caching…
jamesarich May 6, 2026
a812767
feat: AdminResult.getOrThrow() extension + AdminResultException hiera…
jamesarich May 6, 2026
b32f7de
Add missing admin time operation
jamesarich May 6, 2026
81b1cf5
Add admin config DSL builders
jamesarich May 6, 2026
866bb8f
feat: add admin batch getters
jamesarich May 6, 2026
5320434
test: CommandDispatcher RPC and TelemetryApi coverage
jamesarich May 6, 2026
a02b05c
test: expanded Config DSL and AdminBatch coverage
jamesarich May 6, 2026
70d38db
test: comprehensive SDK test coverage — AdminApi, Engine, Handshake, S&F
jamesarich May 6, 2026
5e80d31
docs: sync api-reference, samples, and consumer guides with current A…
jamesarich May 6, 2026
5c225d3
chore: KDoc cleanup, stale comments, and cruft removal
jamesarich May 6, 2026
800c3e4
chore: remove stale Phase 2 labels from implemented features
jamesarich May 6, 2026
7d9a9ff
chore: bump AGP to 9.2.1 to match Meshtastic-Android
jamesarich May 6, 2026
ad2d4ee
fix: auto-resolve broadcast sends to Acked + add cs7 DM conformance
jamesarich May 6, 2026
460e2b7
feat: broadcast sends use want_ack=true for implicit ACK feedback
jamesarich May 6, 2026
a802d25
style: fix spotless and detekt violations
jamesarich May 7, 2026
d3c22e2
Update core ABI dump
jamesarich May 7, 2026
02e01ac
Update testing module ABI dump
jamesarich May 7, 2026
ef0b432
fix(core): address PR review — topology KDoc, immutable cache, RetryP…
jamesarich May 7, 2026
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
24 changes: 18 additions & 6 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,29 @@ plugins {
alias(libs.plugins.spotless)
alias(libs.plugins.detekt) apply false
alias(libs.plugins.kover)
alias(libs.plugins.axionRelease)
// axionRelease applied conditionally below — it fails to apply when this build is
// included as a Gradle composite build because the root project is not yet available.
alias(libs.plugins.axionRelease) apply false
}

scmVersion {
tag {
prefix.set("v")
// Only configure SCM versioning when running as a standalone build. When included as a
// composite build (gradle.parent != null), version management is unnecessary and the plugin
// errors out trying to reach the root project too early.
if (gradle.parent == null) {
apply(plugin = "pl.allegro.tech.build.axion-release")
configure<pl.allegro.tech.build.axion.release.domain.VersionConfig> {
tag {
prefix.set("v")
}
versionIncrementer("incrementPatch")
}
versionIncrementer("incrementPatch")
}

val resolvedVersion: String = scmVersion.version
val resolvedVersion: String = if (gradle.parent == null) {
extensions.getByType<pl.allegro.tech.build.axion.release.domain.VersionConfig>().version
} else {
"0.0.0-composite"
}

allprojects {
group = "org.meshtastic"
Expand Down
793 changes: 789 additions & 4 deletions core/api/core.klib.api

Large diffs are not rendered by default.

766 changes: 760 additions & 6 deletions core/api/jvm/core.api

Large diffs are not rendered by default.

210 changes: 206 additions & 4 deletions core/src/commonMain/kotlin/org/meshtastic/sdk/AdminApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,16 @@ package org.meshtastic.sdk
import org.meshtastic.proto.AdminMessage
import org.meshtastic.proto.Channel
import org.meshtastic.proto.Config
import org.meshtastic.proto.DeviceConnectionStatus
import org.meshtastic.proto.DeviceMetadata
import org.meshtastic.proto.DeviceUIConfig
import org.meshtastic.proto.HamParameters
import org.meshtastic.proto.KeyVerificationAdmin
import org.meshtastic.proto.ModuleConfig
import org.meshtastic.proto.NodeRemoteHardwarePinsResponse
import org.meshtastic.proto.Position
import org.meshtastic.proto.SensorConfig
import org.meshtastic.proto.SharedContact
import org.meshtastic.proto.User
import kotlin.time.Duration
import kotlin.time.Instant
Expand All @@ -29,6 +38,44 @@ import kotlin.time.Instant
*/
public interface AdminApi {

// ── Remote targeting ────────────────────────────────────────────────────

/**
* Return an [AdminApi] instance that targets [dest] instead of the local device.
*
* All calls on the returned instance route admin messages to the specified remote node
* over the mesh. Note: `editSettings`, `batch`, `getDeviceConnectionStatus`, and lifecycle
* commands (`reboot`, `shutdown`, `factoryReset`, `nodeDbReset`) work identically — the
* firmware handles admin-over-mesh transparently.
*
* ```kotlin
* val remoteAdmin = client.admin.forNode(NodeId(0x12345678.toInt()))
* remoteAdmin.setConfig(config) // → sent to remote node
* ```
*
* @param dest the target node's [NodeId]
* @return a remote-targeting [AdminApi] instance
* @since 0.2.0
*/
public fun forNode(dest: NodeId): AdminApi

// ── Device info ─────────────────────────────────────────────────────────

/**
* Request [DeviceMetadata] from the device (firmware version, hardware model, etc.).
*
* For the local node, this is cached during handshake and available via
* [RadioClient.deviceConfig]. For remote nodes, use [forNode] to target the desired node:
*
* ```kotlin
* val metadata = client.admin.forNode(remoteNodeId).getDeviceMetadata()
* ```
*
* @return the device's metadata
* @since 0.2.0
*/
public suspend fun getDeviceMetadata(): AdminResult<DeviceMetadata>

// ── Configs ─────────────────────────────────────────────────────────────

/** Read a single [Config] section from the device. */
Expand Down Expand Up @@ -75,6 +122,130 @@ public interface AdminApi {
/** Mark [node] as ignored — packets from it are filtered before reaching apps. */
public suspend fun setIgnored(node: NodeId, ignored: Boolean): AdminResult<Unit>

/**
* Toggle mute state on [node] — muted nodes do not forward packets.
*
* Note: The firmware uses a toggle primitive (`toggle_muted_node`), so calling this
* always flips the current state. Track local mute state if you need idempotent behavior.
*/
public suspend fun toggleMuted(node: NodeId): AdminResult<Unit>

// ── Position ────────────────────────────────────────────────────────────

/** Set a fixed GPS position for the device (disables GPS module). */
public suspend fun setFixedPosition(position: Position): AdminResult<Unit>

/** Remove the fixed position and re-enable GPS. */
public suspend fun removeFixedPosition(): AdminResult<Unit>

// ── Device UI Config ────────────────────────────────────────────────────

/** Read the device's UI configuration (display preferences, language, etc.). */
public suspend fun getUIConfig(): AdminResult<DeviceUIConfig>

/** Write the device's UI configuration. */
public suspend fun storeUIConfig(config: DeviceUIConfig): AdminResult<Unit>

// ── Canned Messages ─────────────────────────────────────────────────────

/** Read the canned message module's preset messages. */
public suspend fun getCannedMessages(): AdminResult<String>

/** Write the canned message module's preset messages (pipe-delimited). */
public suspend fun setCannedMessages(messages: String): AdminResult<Unit>

// ── Ringtone ────────────────────────────────────────────────────────────

/** Read the device's ringtone (RTTTL format). */
public suspend fun getRingtone(): AdminResult<String>

/** Write the device's ringtone (RTTTL format). */
public suspend fun setRingtone(rtttl: String): AdminResult<Unit>

// ── Device status ───────────────────────────────────────────────────────

/** Read the device's connection status (WiFi, BLE, Ethernet, MQTT). */
public suspend fun getDeviceConnectionStatus(): AdminResult<DeviceConnectionStatus>

/** Read the remote hardware pin configuration of [node]. */
public suspend fun getRemoteHardwarePins(): AdminResult<NodeRemoteHardwarePinsResponse>

// ── Ham radio ───────────────────────────────────────────────────────────

/** Configure the device for amateur radio use (sets call sign, disables encryption). */
public suspend fun setHamMode(params: HamParameters): AdminResult<Unit>

// ── DFU / file management ───────────────────────────────────────────────

/**
* Enter DFU (firmware update) mode. The device will reboot into its bootloader.
*
* This is a fire-and-forget admin write; [AdminResult.Success] means the request was queued
* locally, not that the device stayed connected long enough to acknowledge the reboot.
*/
public suspend fun enterDfuMode(): AdminResult<Unit>

/** Delete a file from the device's filesystem. */
public suspend fun deleteFile(path: String): AdminResult<Unit>

// ── Backup / Restore ────────────────────────────────────────────────────

/** Back up device preferences to the specified [location]. */
public suspend fun backupPreferences(
location: AdminMessage.BackupLocation = AdminMessage.BackupLocation.FLASH,
): AdminResult<Unit>

/** Restore device preferences from the specified [location]. */
public suspend fun restorePreferences(
location: AdminMessage.BackupLocation = AdminMessage.BackupLocation.FLASH,
): AdminResult<Unit>

/** Remove a stored preference backup from [location]. */
public suspend fun removeBackupPreferences(
location: AdminMessage.BackupLocation = AdminMessage.BackupLocation.FLASH,
): AdminResult<Unit>

// ── Node removal ────────────────────────────────────────────────────────

/** Remove a node from the device's NodeDB by its node number. */
public suspend fun removeNode(node: NodeId): AdminResult<Unit>

// ── Input / Display ─────────────────────────────────────────────────────

/** Set the device's scale calibration value (e-ink display DPI). */
public suspend fun setScale(scale: Int): AdminResult<Unit>

/** Send a synthetic input event to the device (button press, touch, etc.). */
public suspend fun sendInputEvent(event: AdminMessage.InputEvent): AdminResult<Unit>

// ── Contacts ────────────────────────────────────────────────────────────

/** Add a shared contact to the device's contact list. */
public suspend fun addContact(contact: SharedContact): AdminResult<Unit>

// ── Key verification ────────────────────────────────────────────────────

/** Initiate or respond to a key verification exchange. */
public suspend fun keyVerification(verification: KeyVerificationAdmin): AdminResult<Unit>

// ── OTA updates ─────────────────────────────────────────────────────────

/** Reboot into OTA update mode after [after] (default: immediately). */
public suspend fun rebootOta(after: Duration = Duration.ZERO): AdminResult<Unit>

/** Send an OTA event (firmware update control). */
public suspend fun otaRequest(event: AdminMessage.OTAEvent): AdminResult<Unit>

// ── Sensor ──────────────────────────────────────────────────────────────

/** Configure a sensor attached to the device. */
public suspend fun setSensorConfig(config: SensorConfig): AdminResult<Unit>

// ── Simulator ───────────────────────────────────────────────────────────

/** Exit the firmware simulator mode (development only). */
public suspend fun exitSimulator(): AdminResult<Unit>

// ── Lifecycle ───────────────────────────────────────────────────────────

/**
Expand All @@ -101,14 +272,25 @@ public interface AdminApi {
/**
* Wipe the device's NodeDB, forcing a fresh discovery cycle on the mesh.
*
* @param preserveFavorites when `true` (default), entries marked as favorites are kept. The
* firmware does not currently expose a separate flag for this; on devices that always erase
* the entire NodeDB, the SDK's [setFavorite] state on local entries is the only persistence.
* The firmware always preserves favorite-marked entries during the wipe (this is
* firmware-enforced behavior). The `nodedb_reset` proto field uses proto3 semantics where
* only `true` can be encoded — a "wipe everything including favorites" mode is not
* available through this command.
*
* The device will reboot after the reset completes.
*/
public suspend fun nodeDbReset(preserveFavorites: Boolean = true): AdminResult<Unit>
public suspend fun nodeDbReset(): AdminResult<Unit>

// ── Time ────────────────────────────────────────────────────────────────

/**
* Set the device's wall clock from raw Unix time seconds without sending position data.
*
* This uses `AdminMessage.set_time_only` directly and is fire-and-forget; [AdminResult.Success]
* means the packet was queued locally.
*/
public suspend fun setTimeOnly(unixTime: Int): AdminResult<Unit>

/**
* Set the device's wall clock to [at] (default: `Clock.System.now()`).
*
Expand All @@ -127,6 +309,14 @@ public interface AdminApi {
* or commit fails, the result reflects that failure and the block's return value is discarded.
*/
public suspend fun <T> editSettings(block: suspend AdminEdit.() -> T): AdminResult<T>

/**
* Exception-based counterpart to [editSettings] that also exposes batched getter helpers.
*
* Getter failures throw [AdminResultException] via [getOrThrow]. If [block] throws, the SDK
* does not send `commit_edit_settings`; firmware eventually discards the buffered edits.
*/
public suspend fun <T> batch(block: suspend AdminBatchScope.() -> T): T
}

/**
Expand All @@ -146,3 +336,15 @@ public interface AdminEdit {
public suspend fun setFavorite(node: NodeId, favorite: Boolean)
public suspend fun setIgnored(node: NodeId, ignored: Boolean)
}

/**
* Receiver type for [AdminApi.batch] — combines [AdminEdit] setters with getter helpers.
*
* Getter failures throw [AdminResultException] via [getOrThrow]. Setters share the same deferred
* commit semantics as [AdminApi.editSettings].
*/
public interface AdminBatchScope : AdminEdit {
public suspend fun getConfig(type: AdminMessage.ConfigType): Config
public suspend fun getModuleConfig(type: AdminMessage.ModuleConfigType): ModuleConfig
public suspend fun listChannels(): List<Channel>
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds

/**
* Tunables for the engine's built-in auto-reconnect supervisor.
* Configuration for the engine's built-in auto-reconnect supervisor.
*
* Configure on the [RadioClient.Builder] via
* [autoReconnect(...)][RadioClient.Builder.autoReconnect].
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ package org.meshtastic.sdk
import org.meshtastic.proto.DeviceMetrics
import org.meshtastic.proto.Telemetry
/**
* Curated battery health and state information.
* Curated battery health and state information reported by the device.
*
* @property percent charge level (0..100).
* @property voltageVolts raw battery voltage, if reported.
* @property pluggedIn `true` if the device is currently drawing external power.
* @since 0.1.0
*/
public data class BatteryStatus(
public val percent: Int?,
Expand All @@ -23,9 +24,10 @@ public data class BatteryStatus(
)

/**
* Converts protobuf [DeviceMetrics] to [BatteryStatus].
* Converts protobuf [DeviceMetrics] into a normalized [BatteryStatus] snapshot.
*
* Maps the firmware's `>= 101` level sentinel to [BatteryStatus.pluggedIn].
* @since 0.1.0
*/
public fun DeviceMetrics.toBatteryStatus(): BatteryStatus? {
if (battery_level == null && voltage == null) return null
Expand Down
Loading
Loading