Skip to content

Commit cf834a7

Browse files
jamesarichCopilot
andauthored
feat: Enhance mPWRD-os WiFi provisioning success state and UI components (#5225)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent e501ade commit cf834a7

11 files changed

Lines changed: 263 additions & 8 deletions

File tree

core/resources/src/commonMain/composeResources/values/strings.xml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1306,6 +1306,21 @@
13061306
<string name="wifi_provision_ssid_placeholder">Enter or select a network</string>
13071307
<string name="wifi_provision_status_applied">WiFi configured successfully!</string>
13081308
<string name="wifi_provision_status_failed">Failed to apply WiFi configuration</string>
1309+
<string name="wifi_provision_success_device_connected">Device Connected</string>
1310+
<string name="wifi_provision_success_description">Your mPWRD-OS device has joined the Wi-Fi network.</string>
1311+
<string name="wifi_provision_success_ip_address">IP Address</string>
1312+
<string name="wifi_provision_success_setup_title">Complete Device Setup</string>
1313+
<string name="wifi_provision_success_setup_description">Sign in over SSH to change the default username and password.</string>
1314+
<string name="wifi_provision_success_username">Username</string>
1315+
<string name="wifi_provision_success_username_value" translatable="false">root</string>
1316+
<string name="wifi_provision_success_password_value" translatable="false">1234</string>
1317+
<string name="wifi_provision_success_ssh_label">SSH Command</string>
1318+
<string name="wifi_provision_success_ssh_command">ssh %1$s@%2$s</string>
1319+
<string name="wifi_provision_success_ssh_unavailable">SSH command available after IP is assigned.</string>
1320+
<string name="wifi_provision_success_open_ssh">Open SSH Client</string>
1321+
<string name="wifi_provision_success_open_ssh_fallback">If no app opens, copy the SSH command and paste it into your SSH client.</string>
1322+
<string name="wifi_provision_success_missing_ip">IP unavailable</string>
1323+
<string name="wifi_provision_success_done">Done</string>
13091324
<string name="desktop_tray_tooltip">Meshtastic Desktop</string>
13101325
<string name="desktop_tray_show">Show Meshtastic</string>
13111326
<string name="desktop_tray_quit">Quit</string>

feature/wifi-provision/src/commonMain/kotlin/org/meshtastic/feature/wifiprovision/NymeaBleConstants.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ internal object NymeaBleConstants {
7070
/** Maximum time to wait for a command response. */
7171
val RESPONSE_TIMEOUT = 15.seconds
7272

73+
/** Timeout for optional GetConnection metadata lookup after a successful connect command. */
74+
val CONNECTION_INFO_TIMEOUT = 2.seconds
75+
7376
/** Settle time after subscribing to notifications before sending commands. */
7477
val SUBSCRIPTION_SETTLE = 300.milliseconds
7578
// endregion
@@ -87,6 +90,9 @@ internal object NymeaBleConstants {
8790

8891
/** Trigger a fresh WiFi scan. */
8992
const val CMD_SCAN = 4
93+
94+
/** Request current connection details (includes IP address if connected). */
95+
const val CMD_GET_CONNECTION = 5
9096
// endregion
9197

9298
// region Response error codes

feature/wifi-provision/src/commonMain/kotlin/org/meshtastic/feature/wifiprovision/WifiProvisionViewModel.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ data class WifiProvisionUiState(
4242
val error: WifiProvisionError? = null,
4343
/** Name of the BLE device we connected to, shown in the DeviceFound confirmation. */
4444
val deviceName: String? = null,
45+
/** IPv4 address reported by nymea after successful provisioning (if available). */
46+
val ipAddress: String? = null,
4547
/** Provisioning outcome shown as inline status (matches web flasher pattern). */
4648
val provisionStatus: ProvisionStatus = ProvisionStatus.Idle,
4749
) {
@@ -175,6 +177,7 @@ class WifiProvisionViewModel(
175177
it.copy(
176178
phase = WifiProvisionUiState.Phase.Provisioning,
177179
error = null,
180+
ipAddress = null,
178181
provisionStatus = WifiProvisionUiState.ProvisionStatus.Idle,
179182
)
180183
}
@@ -186,6 +189,7 @@ class WifiProvisionViewModel(
186189
_uiState.update {
187190
it.copy(
188191
phase = WifiProvisionUiState.Phase.Connected,
192+
ipAddress = result.ipAddress,
189193
provisionStatus = WifiProvisionUiState.ProvisionStatus.Success,
190194
)
191195
}

feature/wifi-provision/src/commonMain/kotlin/org/meshtastic/feature/wifiprovision/domain/NymeaProtocol.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ internal data class NymeaResponse(
7575
@SerialName("c") val command: Int = -1,
7676
/** 0 = success; non-zero = error code. */
7777
@SerialName("r") val responseCode: Int = 0,
78+
/** Optional payload (used by GetConnection and custom Connect responses). */
79+
@SerialName("p") val connectionInfo: NymeaConnectionInfo? = null,
7880
)
7981

8082
/** One entry in the GetNetworks (`c=0`) response payload. */
@@ -97,3 +99,18 @@ internal data class NymeaNetworksResponse(
9799
@SerialName("r") val responseCode: Int = 0,
98100
@SerialName("p") val networks: List<NymeaNetworkEntry> = emptyList(),
99101
)
102+
103+
/** Connection info payload (`p`) returned by GetConnection (`c=5`). */
104+
@Serializable
105+
internal data class NymeaConnectionInfo(
106+
/** ESSID / network name (nymea key: `e`). */
107+
@SerialName("e") val ssid: String = "",
108+
/** BSSID / MAC address (nymea key: `m`). */
109+
@SerialName("m") val bssid: String = "",
110+
/** Signal strength in dBm (nymea key: `s`). */
111+
@SerialName("s") val signalStrength: Int = 0,
112+
/** 0 = open, 1 = protected (nymea key: `p`). */
113+
@SerialName("p") val protection: Int = 0,
114+
/** IPv4 address of current connection (nymea key: `i`). */
115+
@SerialName("i") val ipAddress: String = "",
116+
)

feature/wifi-provision/src/commonMain/kotlin/org/meshtastic/feature/wifiprovision/domain/NymeaWifiService.kt

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,11 @@ import org.meshtastic.core.common.util.safeCatching
3939
import org.meshtastic.feature.wifiprovision.NymeaBleConstants
4040
import org.meshtastic.feature.wifiprovision.NymeaBleConstants.CMD_CONNECT
4141
import org.meshtastic.feature.wifiprovision.NymeaBleConstants.CMD_CONNECT_HIDDEN
42+
import org.meshtastic.feature.wifiprovision.NymeaBleConstants.CMD_GET_CONNECTION
4243
import org.meshtastic.feature.wifiprovision.NymeaBleConstants.CMD_GET_NETWORKS
4344
import org.meshtastic.feature.wifiprovision.NymeaBleConstants.CMD_SCAN
4445
import org.meshtastic.feature.wifiprovision.NymeaBleConstants.COMMANDER_RESPONSE_UUID
46+
import org.meshtastic.feature.wifiprovision.NymeaBleConstants.CONNECTION_INFO_TIMEOUT
4547
import org.meshtastic.feature.wifiprovision.NymeaBleConstants.RESPONSE_SUCCESS
4648
import org.meshtastic.feature.wifiprovision.NymeaBleConstants.RESPONSE_TIMEOUT
4749
import org.meshtastic.feature.wifiprovision.NymeaBleConstants.SCAN_TIMEOUT
@@ -50,6 +52,7 @@ import org.meshtastic.feature.wifiprovision.NymeaBleConstants.WIRELESS_COMMANDER
5052
import org.meshtastic.feature.wifiprovision.NymeaBleConstants.WIRELESS_SERVICE_UUID
5153
import org.meshtastic.feature.wifiprovision.model.ProvisionResult
5254
import org.meshtastic.feature.wifiprovision.model.WifiNetwork
55+
import kotlin.time.Duration
5356

5457
/**
5558
* GATT client for the nymea-networkmanager WiFi provisioning profile.
@@ -68,7 +71,6 @@ class NymeaWifiService(
6871
connectionFactory: BleConnectionFactory,
6972
dispatcher: CoroutineDispatcher,
7073
) {
71-
7274
private val serviceScope = CoroutineScope(SupervisorJob() + dispatcher)
7375
private val bleConnection = connectionFactory.create(serviceScope, TAG)
7476

@@ -184,7 +186,9 @@ class NymeaWifiService(
184186
sendCommand(json)
185187
val response = NymeaJson.decodeFromString<NymeaResponse>(waitForResponse())
186188
if (response.responseCode == RESPONSE_SUCCESS) {
187-
ProvisionResult.Success
189+
val ipAddress =
190+
response.connectionInfo?.ipAddress?.takeIf { it.isNotBlank() } ?: fetchConnectionIpAddress()
191+
ProvisionResult.Success(ipAddress = ipAddress)
188192
} else {
189193
ProvisionResult.Failure(response.responseCode, nymeaErrorMessage(response.responseCode))
190194
}
@@ -229,7 +233,25 @@ class NymeaWifiService(
229233
}
230234

231235
/** Wait up to [RESPONSE_TIMEOUT] for a complete JSON response from the notification channel. */
232-
private suspend fun waitForResponse(): String = withTimeout(RESPONSE_TIMEOUT) { responseChannel.receive() }
236+
private suspend fun waitForResponse(timeout: Duration = RESPONSE_TIMEOUT): String =
237+
withTimeout(timeout) { responseChannel.receive() }
238+
239+
/**
240+
* Best-effort query for current connection info (`CMD_GET_CONNECTION`), returning the reported IP address.
241+
*
242+
* Uses a short timeout because this is an optional enrichment for UX, not a provisioning success criterion.
243+
*/
244+
private suspend fun fetchConnectionIpAddress(): String? = safeCatching {
245+
sendCommand(NymeaJson.encodeToString(NymeaSimpleCommand(CMD_GET_CONNECTION)))
246+
val response =
247+
NymeaJson.decodeFromString<NymeaResponse>(waitForResponse(timeout = CONNECTION_INFO_TIMEOUT))
248+
if (response.responseCode == RESPONSE_SUCCESS) {
249+
response.connectionInfo?.ipAddress?.takeIf { it.isNotBlank() }
250+
} else {
251+
null
252+
}
253+
}
254+
.getOrNull()
233255

234256
private fun nymeaErrorMessage(code: Int): String = when (code) {
235257
NymeaBleConstants.RESPONSE_INVALID_COMMAND -> "Invalid command"

feature/wifi-provision/src/commonMain/kotlin/org/meshtastic/feature/wifiprovision/model/WifiNetwork.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ data class WifiNetwork(
3030

3131
/** Result of a WiFi provisioning attempt. */
3232
sealed interface ProvisionResult {
33-
data object Success : ProvisionResult
33+
data class Success(
34+
/** IPv4 address reported by nymea for the active Wi-Fi connection. */
35+
val ipAddress: String? = null,
36+
) : ProvisionResult
3437

3538
data class Failure(val errorCode: Int, val message: String) : ProvisionResult
3639
}

feature/wifi-provision/src/commonMain/kotlin/org/meshtastic/feature/wifiprovision/ui/WifiProvisionPreviews.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ private fun ConnectedWithNetworksPreview() {
127127
ConnectedContent(
128128
networks = sampleNetworks,
129129
provisionStatus = ProvisionStatus.Idle,
130+
ipAddress = null,
130131
isProvisioning = false,
131132
isScanning = false,
132133
onScanNetworks = noOp,
@@ -145,6 +146,7 @@ private fun ConnectedEmptyNetworksPreview() {
145146
ConnectedContent(
146147
networks = emptyList(),
147148
provisionStatus = ProvisionStatus.Idle,
149+
ipAddress = null,
148150
isProvisioning = false,
149151
isScanning = false,
150152
onScanNetworks = noOp,
@@ -163,6 +165,7 @@ private fun ConnectedScanningPreview() {
163165
ConnectedContent(
164166
networks = sampleNetworks,
165167
provisionStatus = ProvisionStatus.Idle,
168+
ipAddress = null,
166169
isProvisioning = false,
167170
isScanning = true,
168171
onScanNetworks = noOp,
@@ -181,6 +184,7 @@ private fun ConnectedProvisioningPreview() {
181184
ConnectedContent(
182185
networks = sampleNetworks,
183186
provisionStatus = ProvisionStatus.Idle,
187+
ipAddress = null,
184188
isProvisioning = true,
185189
isScanning = false,
186190
onScanNetworks = noOp,
@@ -199,6 +203,7 @@ private fun ConnectedSuccessPreview() {
199203
ConnectedContent(
200204
networks = sampleNetworks,
201205
provisionStatus = ProvisionStatus.Success,
206+
ipAddress = "10.10.10.61",
202207
isProvisioning = false,
203208
isScanning = false,
204209
onScanNetworks = noOp,
@@ -217,6 +222,7 @@ private fun ConnectedFailedPreview() {
217222
ConnectedContent(
218223
networks = sampleNetworks,
219224
provisionStatus = ProvisionStatus.Failed,
225+
ipAddress = null,
220226
isProvisioning = false,
221227
isScanning = false,
222228
onScanNetworks = noOp,
@@ -239,6 +245,7 @@ private fun ConnectedLongSsidPreview() {
239245
ConnectedContent(
240246
networks = edgeCaseNetworks,
241247
provisionStatus = ProvisionStatus.Idle,
248+
ipAddress = null,
242249
isProvisioning = false,
243250
isScanning = false,
244251
onScanNetworks = noOp,
@@ -257,6 +264,7 @@ private fun ConnectedManyNetworksPreview() {
257264
ConnectedContent(
258265
networks = manyNetworks,
259266
provisionStatus = ProvisionStatus.Idle,
267+
ipAddress = null,
260268
isProvisioning = false,
261269
isScanning = false,
262270
onScanNetworks = noOp,

0 commit comments

Comments
 (0)