Skip to content

Commit e6b1b99

Browse files
jamesarichCopilot
andauthored
feat: SDK improvements for Meshtastic-Android hard cutover (#1)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 36d8fe1 commit e6b1b99

105 files changed

Lines changed: 15237 additions & 295 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

build.gradle.kts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,29 @@ plugins {
1616
alias(libs.plugins.spotless)
1717
alias(libs.plugins.detekt) apply false
1818
alias(libs.plugins.kover)
19-
alias(libs.plugins.axionRelease)
19+
// axionRelease applied conditionally below — it fails to apply when this build is
20+
// included as a Gradle composite build because the root project is not yet available.
21+
alias(libs.plugins.axionRelease) apply false
2022
}
2123

22-
scmVersion {
23-
tag {
24-
prefix.set("v")
24+
// Only configure SCM versioning when running as a standalone build. When included as a
25+
// composite build (gradle.parent != null), version management is unnecessary and the plugin
26+
// errors out trying to reach the root project too early.
27+
if (gradle.parent == null) {
28+
apply(plugin = "pl.allegro.tech.build.axion-release")
29+
configure<pl.allegro.tech.build.axion.release.domain.VersionConfig> {
30+
tag {
31+
prefix.set("v")
32+
}
33+
versionIncrementer("incrementPatch")
2534
}
26-
versionIncrementer("incrementPatch")
2735
}
2836

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

3143
allprojects {
3244
group = "org.meshtastic"

core/api/core.klib.api

Lines changed: 789 additions & 4 deletions
Large diffs are not rendered by default.

core/api/jvm/core.api

Lines changed: 760 additions & 6 deletions
Large diffs are not rendered by default.

core/src/commonMain/kotlin/org/meshtastic/sdk/AdminApi.kt

Lines changed: 206 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,16 @@ package org.meshtastic.sdk
1010
import org.meshtastic.proto.AdminMessage
1111
import org.meshtastic.proto.Channel
1212
import org.meshtastic.proto.Config
13+
import org.meshtastic.proto.DeviceConnectionStatus
14+
import org.meshtastic.proto.DeviceMetadata
15+
import org.meshtastic.proto.DeviceUIConfig
16+
import org.meshtastic.proto.HamParameters
17+
import org.meshtastic.proto.KeyVerificationAdmin
1318
import org.meshtastic.proto.ModuleConfig
19+
import org.meshtastic.proto.NodeRemoteHardwarePinsResponse
20+
import org.meshtastic.proto.Position
21+
import org.meshtastic.proto.SensorConfig
22+
import org.meshtastic.proto.SharedContact
1423
import org.meshtastic.proto.User
1524
import kotlin.time.Duration
1625
import kotlin.time.Instant
@@ -29,6 +38,44 @@ import kotlin.time.Instant
2938
*/
3039
public interface AdminApi {
3140

41+
// ── Remote targeting ────────────────────────────────────────────────────
42+
43+
/**
44+
* Return an [AdminApi] instance that targets [dest] instead of the local device.
45+
*
46+
* All calls on the returned instance route admin messages to the specified remote node
47+
* over the mesh. Note: `editSettings`, `batch`, `getDeviceConnectionStatus`, and lifecycle
48+
* commands (`reboot`, `shutdown`, `factoryReset`, `nodeDbReset`) work identically — the
49+
* firmware handles admin-over-mesh transparently.
50+
*
51+
* ```kotlin
52+
* val remoteAdmin = client.admin.forNode(NodeId(0x12345678.toInt()))
53+
* remoteAdmin.setConfig(config) // → sent to remote node
54+
* ```
55+
*
56+
* @param dest the target node's [NodeId]
57+
* @return a remote-targeting [AdminApi] instance
58+
* @since 0.2.0
59+
*/
60+
public fun forNode(dest: NodeId): AdminApi
61+
62+
// ── Device info ─────────────────────────────────────────────────────────
63+
64+
/**
65+
* Request [DeviceMetadata] from the device (firmware version, hardware model, etc.).
66+
*
67+
* For the local node, this is cached during handshake and available via
68+
* [RadioClient.deviceConfig]. For remote nodes, use [forNode] to target the desired node:
69+
*
70+
* ```kotlin
71+
* val metadata = client.admin.forNode(remoteNodeId).getDeviceMetadata()
72+
* ```
73+
*
74+
* @return the device's metadata
75+
* @since 0.2.0
76+
*/
77+
public suspend fun getDeviceMetadata(): AdminResult<DeviceMetadata>
78+
3279
// ── Configs ─────────────────────────────────────────────────────────────
3380

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

125+
/**
126+
* Toggle mute state on [node] — muted nodes do not forward packets.
127+
*
128+
* Note: The firmware uses a toggle primitive (`toggle_muted_node`), so calling this
129+
* always flips the current state. Track local mute state if you need idempotent behavior.
130+
*/
131+
public suspend fun toggleMuted(node: NodeId): AdminResult<Unit>
132+
133+
// ── Position ────────────────────────────────────────────────────────────
134+
135+
/** Set a fixed GPS position for the device (disables GPS module). */
136+
public suspend fun setFixedPosition(position: Position): AdminResult<Unit>
137+
138+
/** Remove the fixed position and re-enable GPS. */
139+
public suspend fun removeFixedPosition(): AdminResult<Unit>
140+
141+
// ── Device UI Config ────────────────────────────────────────────────────
142+
143+
/** Read the device's UI configuration (display preferences, language, etc.). */
144+
public suspend fun getUIConfig(): AdminResult<DeviceUIConfig>
145+
146+
/** Write the device's UI configuration. */
147+
public suspend fun storeUIConfig(config: DeviceUIConfig): AdminResult<Unit>
148+
149+
// ── Canned Messages ─────────────────────────────────────────────────────
150+
151+
/** Read the canned message module's preset messages. */
152+
public suspend fun getCannedMessages(): AdminResult<String>
153+
154+
/** Write the canned message module's preset messages (pipe-delimited). */
155+
public suspend fun setCannedMessages(messages: String): AdminResult<Unit>
156+
157+
// ── Ringtone ────────────────────────────────────────────────────────────
158+
159+
/** Read the device's ringtone (RTTTL format). */
160+
public suspend fun getRingtone(): AdminResult<String>
161+
162+
/** Write the device's ringtone (RTTTL format). */
163+
public suspend fun setRingtone(rtttl: String): AdminResult<Unit>
164+
165+
// ── Device status ───────────────────────────────────────────────────────
166+
167+
/** Read the device's connection status (WiFi, BLE, Ethernet, MQTT). */
168+
public suspend fun getDeviceConnectionStatus(): AdminResult<DeviceConnectionStatus>
169+
170+
/** Read the remote hardware pin configuration of [node]. */
171+
public suspend fun getRemoteHardwarePins(): AdminResult<NodeRemoteHardwarePinsResponse>
172+
173+
// ── Ham radio ───────────────────────────────────────────────────────────
174+
175+
/** Configure the device for amateur radio use (sets call sign, disables encryption). */
176+
public suspend fun setHamMode(params: HamParameters): AdminResult<Unit>
177+
178+
// ── DFU / file management ───────────────────────────────────────────────
179+
180+
/**
181+
* Enter DFU (firmware update) mode. The device will reboot into its bootloader.
182+
*
183+
* This is a fire-and-forget admin write; [AdminResult.Success] means the request was queued
184+
* locally, not that the device stayed connected long enough to acknowledge the reboot.
185+
*/
186+
public suspend fun enterDfuMode(): AdminResult<Unit>
187+
188+
/** Delete a file from the device's filesystem. */
189+
public suspend fun deleteFile(path: String): AdminResult<Unit>
190+
191+
// ── Backup / Restore ────────────────────────────────────────────────────
192+
193+
/** Back up device preferences to the specified [location]. */
194+
public suspend fun backupPreferences(
195+
location: AdminMessage.BackupLocation = AdminMessage.BackupLocation.FLASH,
196+
): AdminResult<Unit>
197+
198+
/** Restore device preferences from the specified [location]. */
199+
public suspend fun restorePreferences(
200+
location: AdminMessage.BackupLocation = AdminMessage.BackupLocation.FLASH,
201+
): AdminResult<Unit>
202+
203+
/** Remove a stored preference backup from [location]. */
204+
public suspend fun removeBackupPreferences(
205+
location: AdminMessage.BackupLocation = AdminMessage.BackupLocation.FLASH,
206+
): AdminResult<Unit>
207+
208+
// ── Node removal ────────────────────────────────────────────────────────
209+
210+
/** Remove a node from the device's NodeDB by its node number. */
211+
public suspend fun removeNode(node: NodeId): AdminResult<Unit>
212+
213+
// ── Input / Display ─────────────────────────────────────────────────────
214+
215+
/** Set the device's scale calibration value (e-ink display DPI). */
216+
public suspend fun setScale(scale: Int): AdminResult<Unit>
217+
218+
/** Send a synthetic input event to the device (button press, touch, etc.). */
219+
public suspend fun sendInputEvent(event: AdminMessage.InputEvent): AdminResult<Unit>
220+
221+
// ── Contacts ────────────────────────────────────────────────────────────
222+
223+
/** Add a shared contact to the device's contact list. */
224+
public suspend fun addContact(contact: SharedContact): AdminResult<Unit>
225+
226+
// ── Key verification ────────────────────────────────────────────────────
227+
228+
/** Initiate or respond to a key verification exchange. */
229+
public suspend fun keyVerification(verification: KeyVerificationAdmin): AdminResult<Unit>
230+
231+
// ── OTA updates ─────────────────────────────────────────────────────────
232+
233+
/** Reboot into OTA update mode after [after] (default: immediately). */
234+
public suspend fun rebootOta(after: Duration = Duration.ZERO): AdminResult<Unit>
235+
236+
/** Send an OTA event (firmware update control). */
237+
public suspend fun otaRequest(event: AdminMessage.OTAEvent): AdminResult<Unit>
238+
239+
// ── Sensor ──────────────────────────────────────────────────────────────
240+
241+
/** Configure a sensor attached to the device. */
242+
public suspend fun setSensorConfig(config: SensorConfig): AdminResult<Unit>
243+
244+
// ── Simulator ───────────────────────────────────────────────────────────
245+
246+
/** Exit the firmware simulator mode (development only). */
247+
public suspend fun exitSimulator(): AdminResult<Unit>
248+
78249
// ── Lifecycle ───────────────────────────────────────────────────────────
79250

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

110284
// ── Time ────────────────────────────────────────────────────────────────
111285

286+
/**
287+
* Set the device's wall clock from raw Unix time seconds without sending position data.
288+
*
289+
* This uses `AdminMessage.set_time_only` directly and is fire-and-forget; [AdminResult.Success]
290+
* means the packet was queued locally.
291+
*/
292+
public suspend fun setTimeOnly(unixTime: Int): AdminResult<Unit>
293+
112294
/**
113295
* Set the device's wall clock to [at] (default: `Clock.System.now()`).
114296
*
@@ -127,6 +309,14 @@ public interface AdminApi {
127309
* or commit fails, the result reflects that failure and the block's return value is discarded.
128310
*/
129311
public suspend fun <T> editSettings(block: suspend AdminEdit.() -> T): AdminResult<T>
312+
313+
/**
314+
* Exception-based counterpart to [editSettings] that also exposes batched getter helpers.
315+
*
316+
* Getter failures throw [AdminResultException] via [getOrThrow]. If [block] throws, the SDK
317+
* does not send `commit_edit_settings`; firmware eventually discards the buffered edits.
318+
*/
319+
public suspend fun <T> batch(block: suspend AdminBatchScope.() -> T): T
130320
}
131321

132322
/**
@@ -146,3 +336,15 @@ public interface AdminEdit {
146336
public suspend fun setFavorite(node: NodeId, favorite: Boolean)
147337
public suspend fun setIgnored(node: NodeId, ignored: Boolean)
148338
}
339+
340+
/**
341+
* Receiver type for [AdminApi.batch] — combines [AdminEdit] setters with getter helpers.
342+
*
343+
* Getter failures throw [AdminResultException] via [getOrThrow]. Setters share the same deferred
344+
* commit semantics as [AdminApi.editSettings].
345+
*/
346+
public interface AdminBatchScope : AdminEdit {
347+
public suspend fun getConfig(type: AdminMessage.ConfigType): Config
348+
public suspend fun getModuleConfig(type: AdminMessage.ModuleConfigType): ModuleConfig
349+
public suspend fun listChannels(): List<Channel>
350+
}

core/src/commonMain/kotlin/org/meshtastic/sdk/AutoReconnectConfig.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import kotlin.time.Duration
1111
import kotlin.time.Duration.Companion.seconds
1212

1313
/**
14-
* Tunables for the engine's built-in auto-reconnect supervisor.
14+
* Configuration for the engine's built-in auto-reconnect supervisor.
1515
*
1616
* Configure on the [RadioClient.Builder] via
1717
* [autoReconnect(...)][RadioClient.Builder.autoReconnect].

core/src/commonMain/kotlin/org/meshtastic/sdk/BatteryStatus.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@ package org.meshtastic.sdk
1010
import org.meshtastic.proto.DeviceMetrics
1111
import org.meshtastic.proto.Telemetry
1212
/**
13-
* Curated battery health and state information.
13+
* Curated battery health and state information reported by the device.
1414
*
1515
* @property percent charge level (0..100).
1616
* @property voltageVolts raw battery voltage, if reported.
1717
* @property pluggedIn `true` if the device is currently drawing external power.
18+
* @since 0.1.0
1819
*/
1920
public data class BatteryStatus(
2021
public val percent: Int?,
@@ -23,9 +24,10 @@ public data class BatteryStatus(
2324
)
2425

2526
/**
26-
* Converts protobuf [DeviceMetrics] to [BatteryStatus].
27+
* Converts protobuf [DeviceMetrics] into a normalized [BatteryStatus] snapshot.
2728
*
2829
* Maps the firmware's `>= 101` level sentinel to [BatteryStatus.pluggedIn].
30+
* @since 0.1.0
2931
*/
3032
public fun DeviceMetrics.toBatteryStatus(): BatteryStatus? {
3133
if (battery_level == null && voltage == null) return null

0 commit comments

Comments
 (0)