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>
Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com>
This commit is contained in:
James Rich
2026-04-22 21:04:16 -05:00
committed by GitHub
parent e500c9df66
commit c2b4826717
11 changed files with 264 additions and 9 deletions

View File

@@ -1306,6 +1306,21 @@
<string name="wifi_provision_ssid_placeholder">Enter or select a network</string>
<string name="wifi_provision_status_applied">WiFi configured successfully!</string>
<string name="wifi_provision_status_failed">Failed to apply WiFi configuration</string>
<string name="wifi_provision_success_device_connected">Device Connected</string>
<string name="wifi_provision_success_description">Your mPWRD-OS device has joined the Wi-Fi network.</string>
<string name="wifi_provision_success_ip_address">IP Address</string>
<string name="wifi_provision_success_setup_title">Complete Device Setup</string>
<string name="wifi_provision_success_setup_description">Sign in over SSH to change the default username and password.</string>
<string name="wifi_provision_success_username">Username</string>
<string name="wifi_provision_success_username_value" translatable="false">root</string>
<string name="wifi_provision_success_password_value" translatable="false">1234</string>
<string name="wifi_provision_success_ssh_label">SSH Command</string>
<string name="wifi_provision_success_ssh_command">ssh %1$s@%2$s</string>
<string name="wifi_provision_success_ssh_unavailable">SSH command available after IP is assigned.</string>
<string name="wifi_provision_success_open_ssh">Open SSH Client</string>
<string name="wifi_provision_success_open_ssh_fallback">If no app opens, copy the SSH command and paste it into your SSH client.</string>
<string name="wifi_provision_success_missing_ip">IP unavailable</string>
<string name="wifi_provision_success_done">Done</string>
<string name="desktop_tray_tooltip">Meshtastic Desktop</string>
<string name="desktop_tray_show">Show Meshtastic</string>
<string name="desktop_tray_quit">Quit</string>

View File

@@ -70,6 +70,9 @@ internal object NymeaBleConstants {
/** Maximum time to wait for a command response. */
val RESPONSE_TIMEOUT = 15.seconds
/** Timeout for optional GetConnection metadata lookup after a successful connect command. */
val CONNECTION_INFO_TIMEOUT = 2.seconds
/** Settle time after subscribing to notifications before sending commands. */
val SUBSCRIPTION_SETTLE = 300.milliseconds
// endregion
@@ -87,6 +90,9 @@ internal object NymeaBleConstants {
/** Trigger a fresh WiFi scan. */
const val CMD_SCAN = 4
/** Request current connection details (includes IP address if connected). */
const val CMD_GET_CONNECTION = 5
// endregion
// region Response error codes

View File

@@ -42,6 +42,8 @@ data class WifiProvisionUiState(
val error: WifiProvisionError? = null,
/** Name of the BLE device we connected to, shown in the DeviceFound confirmation. */
val deviceName: String? = null,
/** IPv4 address reported by nymea after successful provisioning (if available). */
val ipAddress: String? = null,
/** Provisioning outcome shown as inline status (matches web flasher pattern). */
val provisionStatus: ProvisionStatus = ProvisionStatus.Idle,
) {
@@ -175,6 +177,7 @@ class WifiProvisionViewModel(
it.copy(
phase = WifiProvisionUiState.Phase.Provisioning,
error = null,
ipAddress = null,
provisionStatus = WifiProvisionUiState.ProvisionStatus.Idle,
)
}
@@ -186,6 +189,7 @@ class WifiProvisionViewModel(
_uiState.update {
it.copy(
phase = WifiProvisionUiState.Phase.Connected,
ipAddress = result.ipAddress,
provisionStatus = WifiProvisionUiState.ProvisionStatus.Success,
)
}

View File

@@ -75,6 +75,8 @@ internal data class NymeaResponse(
@SerialName("c") val command: Int = -1,
/** 0 = success; non-zero = error code. */
@SerialName("r") val responseCode: Int = 0,
/** Optional payload (used by GetConnection and custom Connect responses). */
@SerialName("p") val connectionInfo: NymeaConnectionInfo? = null,
)
/** One entry in the GetNetworks (`c=0`) response payload. */
@@ -97,3 +99,18 @@ internal data class NymeaNetworksResponse(
@SerialName("r") val responseCode: Int = 0,
@SerialName("p") val networks: List<NymeaNetworkEntry> = emptyList(),
)
/** Connection info payload (`p`) returned by GetConnection (`c=5`). */
@Serializable
internal data class NymeaConnectionInfo(
/** ESSID / network name (nymea key: `e`). */
@SerialName("e") val ssid: String = "",
/** BSSID / MAC address (nymea key: `m`). */
@SerialName("m") val bssid: String = "",
/** Signal strength in dBm (nymea key: `s`). */
@SerialName("s") val signalStrength: Int = 0,
/** 0 = open, 1 = protected (nymea key: `p`). */
@SerialName("p") val protection: Int = 0,
/** IPv4 address of current connection (nymea key: `i`). */
@SerialName("i") val ipAddress: String = "",
)

View File

@@ -39,9 +39,11 @@ import org.meshtastic.core.common.util.safeCatching
import org.meshtastic.feature.wifiprovision.NymeaBleConstants
import org.meshtastic.feature.wifiprovision.NymeaBleConstants.CMD_CONNECT
import org.meshtastic.feature.wifiprovision.NymeaBleConstants.CMD_CONNECT_HIDDEN
import org.meshtastic.feature.wifiprovision.NymeaBleConstants.CMD_GET_CONNECTION
import org.meshtastic.feature.wifiprovision.NymeaBleConstants.CMD_GET_NETWORKS
import org.meshtastic.feature.wifiprovision.NymeaBleConstants.CMD_SCAN
import org.meshtastic.feature.wifiprovision.NymeaBleConstants.COMMANDER_RESPONSE_UUID
import org.meshtastic.feature.wifiprovision.NymeaBleConstants.CONNECTION_INFO_TIMEOUT
import org.meshtastic.feature.wifiprovision.NymeaBleConstants.RESPONSE_SUCCESS
import org.meshtastic.feature.wifiprovision.NymeaBleConstants.RESPONSE_TIMEOUT
import org.meshtastic.feature.wifiprovision.NymeaBleConstants.SCAN_TIMEOUT
@@ -50,6 +52,7 @@ import org.meshtastic.feature.wifiprovision.NymeaBleConstants.WIRELESS_COMMANDER
import org.meshtastic.feature.wifiprovision.NymeaBleConstants.WIRELESS_SERVICE_UUID
import org.meshtastic.feature.wifiprovision.model.ProvisionResult
import org.meshtastic.feature.wifiprovision.model.WifiNetwork
import kotlin.time.Duration
/**
* GATT client for the nymea-networkmanager WiFi provisioning profile.
@@ -68,7 +71,6 @@ class NymeaWifiService(
connectionFactory: BleConnectionFactory,
dispatcher: CoroutineDispatcher,
) {
private val serviceScope = CoroutineScope(SupervisorJob() + dispatcher)
private val bleConnection = connectionFactory.create(serviceScope, TAG)
@@ -184,7 +186,9 @@ class NymeaWifiService(
sendCommand(json)
val response = NymeaJson.decodeFromString<NymeaResponse>(waitForResponse())
if (response.responseCode == RESPONSE_SUCCESS) {
ProvisionResult.Success
val ipAddress =
response.connectionInfo?.ipAddress?.takeIf { it.isNotBlank() } ?: fetchConnectionIpAddress()
ProvisionResult.Success(ipAddress = ipAddress)
} else {
ProvisionResult.Failure(response.responseCode, nymeaErrorMessage(response.responseCode))
}
@@ -229,7 +233,25 @@ class NymeaWifiService(
}
/** Wait up to [RESPONSE_TIMEOUT] for a complete JSON response from the notification channel. */
private suspend fun waitForResponse(): String = withTimeout(RESPONSE_TIMEOUT) { responseChannel.receive() }
private suspend fun waitForResponse(timeout: Duration = RESPONSE_TIMEOUT): String =
withTimeout(timeout) { responseChannel.receive() }
/**
* Best-effort query for current connection info (`CMD_GET_CONNECTION`), returning the reported IP address.
*
* Uses a short timeout because this is an optional enrichment for UX, not a provisioning success criterion.
*/
private suspend fun fetchConnectionIpAddress(): String? = safeCatching {
sendCommand(NymeaJson.encodeToString(NymeaSimpleCommand(CMD_GET_CONNECTION)))
val response =
NymeaJson.decodeFromString<NymeaResponse>(waitForResponse(timeout = CONNECTION_INFO_TIMEOUT))
if (response.responseCode == RESPONSE_SUCCESS) {
response.connectionInfo?.ipAddress?.takeIf { it.isNotBlank() }
} else {
null
}
}
.getOrNull()
private fun nymeaErrorMessage(code: Int): String = when (code) {
NymeaBleConstants.RESPONSE_INVALID_COMMAND -> "Invalid command"

View File

@@ -30,7 +30,10 @@ data class WifiNetwork(
/** Result of a WiFi provisioning attempt. */
sealed interface ProvisionResult {
data object Success : ProvisionResult
data class Success(
/** IPv4 address reported by nymea for the active Wi-Fi connection. */
val ipAddress: String? = null,
) : ProvisionResult
data class Failure(val errorCode: Int, val message: String) : ProvisionResult
}

View File

@@ -127,6 +127,7 @@ private fun ConnectedWithNetworksPreview() {
ConnectedContent(
networks = sampleNetworks,
provisionStatus = ProvisionStatus.Idle,
ipAddress = null,
isProvisioning = false,
isScanning = false,
onScanNetworks = noOp,
@@ -145,6 +146,7 @@ private fun ConnectedEmptyNetworksPreview() {
ConnectedContent(
networks = emptyList(),
provisionStatus = ProvisionStatus.Idle,
ipAddress = null,
isProvisioning = false,
isScanning = false,
onScanNetworks = noOp,
@@ -163,6 +165,7 @@ private fun ConnectedScanningPreview() {
ConnectedContent(
networks = sampleNetworks,
provisionStatus = ProvisionStatus.Idle,
ipAddress = null,
isProvisioning = false,
isScanning = true,
onScanNetworks = noOp,
@@ -181,6 +184,7 @@ private fun ConnectedProvisioningPreview() {
ConnectedContent(
networks = sampleNetworks,
provisionStatus = ProvisionStatus.Idle,
ipAddress = null,
isProvisioning = true,
isScanning = false,
onScanNetworks = noOp,
@@ -199,6 +203,7 @@ private fun ConnectedSuccessPreview() {
ConnectedContent(
networks = sampleNetworks,
provisionStatus = ProvisionStatus.Success,
ipAddress = "10.10.10.61",
isProvisioning = false,
isScanning = false,
onScanNetworks = noOp,
@@ -217,6 +222,7 @@ private fun ConnectedFailedPreview() {
ConnectedContent(
networks = sampleNetworks,
provisionStatus = ProvisionStatus.Failed,
ipAddress = null,
isProvisioning = false,
isScanning = false,
onScanNetworks = noOp,
@@ -239,6 +245,7 @@ private fun ConnectedLongSsidPreview() {
ConnectedContent(
networks = edgeCaseNetworks,
provisionStatus = ProvisionStatus.Idle,
ipAddress = null,
isProvisioning = false,
isScanning = false,
onScanNetworks = noOp,
@@ -257,6 +264,7 @@ private fun ConnectedManyNetworksPreview() {
ConnectedContent(
networks = manyNetworks,
provisionStatus = ProvisionStatus.Idle,
ipAddress = null,
isProvisioning = false,
isScanning = false,
onScanNetworks = noOp,

View File

@@ -14,6 +14,8 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@file:Suppress("TooManyFunctions", "LongMethod")
package org.meshtastic.feature.wifiprovision.ui
import androidx.compose.animation.AnimatedVisibility
@@ -63,6 +65,7 @@ import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -74,6 +77,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.semantics.Role
@@ -82,6 +86,7 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.painterResource
@@ -112,15 +117,35 @@ import org.meshtastic.core.resources.wifi_provision_sending_credentials
import org.meshtastic.core.resources.wifi_provision_signal_strength
import org.meshtastic.core.resources.wifi_provision_ssid_label
import org.meshtastic.core.resources.wifi_provision_ssid_placeholder
import org.meshtastic.core.resources.wifi_provision_success_description
import org.meshtastic.core.resources.wifi_provision_success_device_connected
import org.meshtastic.core.resources.wifi_provision_success_done
import org.meshtastic.core.resources.wifi_provision_success_ip_address
import org.meshtastic.core.resources.wifi_provision_success_missing_ip
import org.meshtastic.core.resources.wifi_provision_success_open_ssh
import org.meshtastic.core.resources.wifi_provision_success_open_ssh_fallback
import org.meshtastic.core.resources.wifi_provision_success_password_value
import org.meshtastic.core.resources.wifi_provision_success_setup_description
import org.meshtastic.core.resources.wifi_provision_success_setup_title
import org.meshtastic.core.resources.wifi_provision_success_ssh_command
import org.meshtastic.core.resources.wifi_provision_success_ssh_label
import org.meshtastic.core.resources.wifi_provision_success_ssh_unavailable
import org.meshtastic.core.resources.wifi_provision_success_username
import org.meshtastic.core.resources.wifi_provision_success_username_value
import org.meshtastic.core.resources.wifi_provisioning
import org.meshtastic.core.ui.component.AutoLinkText
import org.meshtastic.core.ui.component.CopyIconButton
import org.meshtastic.core.ui.icon.ArrowBack
import org.meshtastic.core.ui.icon.Bluetooth
import org.meshtastic.core.ui.icon.CheckCircle
import org.meshtastic.core.ui.icon.Lock
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Serial
import org.meshtastic.core.ui.icon.Visibility
import org.meshtastic.core.ui.icon.VisibilityOff
import org.meshtastic.core.ui.icon.Wifi
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.util.rememberOpenUrl
import org.meshtastic.feature.wifiprovision.WifiProvisionError
import org.meshtastic.feature.wifiprovision.WifiProvisionUiState
import org.meshtastic.feature.wifiprovision.WifiProvisionUiState.Phase
@@ -191,6 +216,7 @@ fun WifiProvisionScreen(
ConnectedContent(
networks = uiState.networks,
provisionStatus = uiState.provisionStatus,
ipAddress = uiState.ipAddress,
isProvisioning = uiState.phase == Phase.Provisioning,
isScanning = uiState.phase == Phase.LoadingNetworks,
onScanNetworks = viewModel::scanNetworks,
@@ -311,12 +337,18 @@ internal fun ScanningNetworksContent() {
internal fun ConnectedContent(
networks: List<WifiNetwork>,
provisionStatus: ProvisionStatus,
ipAddress: String?,
isProvisioning: Boolean,
isScanning: Boolean,
onScanNetworks: () -> Unit,
onProvision: (ssid: String, password: String) -> Unit,
onDisconnect: () -> Unit,
) {
if (provisionStatus == ProvisionStatus.Success) {
ProvisionSuccessContent(ipAddress = ipAddress, onDone = onDisconnect)
return
}
var ssid by rememberSaveable { mutableStateOf("") }
var password by rememberSaveable { mutableStateOf("") }
var passwordVisible by rememberSaveable { mutableStateOf(false) }
@@ -467,6 +499,121 @@ internal fun ConnectedContent(
}
}
@Composable
private fun ProvisionSuccessContent(ipAddress: String?, onDone: () -> Unit) {
val openUrl = rememberOpenUrl()
val defaultUsername = stringResource(Res.string.wifi_provision_success_username_value)
val defaultPassword = stringResource(Res.string.wifi_provision_success_password_value)
val resolvedIp = ipAddress ?: stringResource(Res.string.wifi_provision_success_missing_ip)
val sshCommand =
ipAddress?.let { stringResource(Res.string.wifi_provision_success_ssh_command, defaultUsername, it) }
?: stringResource(Res.string.wifi_provision_success_ssh_unavailable)
val sshUri = ipAddress?.let { "ssh://$defaultUsername@$it" }
Column(
modifier =
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 24.dp, vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
Icon(
imageVector = MeshtasticIcons.CheckCircle,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(72.dp).align(Alignment.CenterHorizontally),
)
Text(
text = stringResource(Res.string.wifi_provision_success_device_connected),
style = MaterialTheme.typography.headlineMediumEmphasized,
modifier = Modifier.align(Alignment.CenterHorizontally),
)
Text(
text = stringResource(Res.string.wifi_provision_success_description),
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.align(Alignment.CenterHorizontally),
)
Card(
shape = MaterialTheme.shapes.large,
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerHigh),
) {
ProvisionInfoItem(
label = stringResource(Res.string.wifi_provision_success_ip_address),
value = resolvedIp,
copyEnabled = ipAddress != null,
)
}
Card(
shape = MaterialTheme.shapes.large,
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerHigh),
) {
Column(modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp)) {
Text(
text = stringResource(Res.string.wifi_provision_success_setup_title),
style = MaterialTheme.typography.titleLargeEmphasized,
modifier = Modifier.padding(horizontal = 16.dp).padding(bottom = 12.dp),
)
Text(
text = stringResource(Res.string.wifi_provision_success_setup_description),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = 16.dp).padding(bottom = 8.dp),
)
ProvisionInfoItem(
label = stringResource(Res.string.wifi_provision_success_username),
value = defaultUsername,
)
ProvisionInfoItem(label = stringResource(Res.string.password), value = defaultPassword)
ProvisionInfoItem(
label = stringResource(Res.string.wifi_provision_success_ssh_label),
value = sshCommand,
copyEnabled = ipAddress != null,
)
FilledTonalButton(
onClick = { sshUri?.let(openUrl) },
enabled = sshUri != null,
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp).padding(top = 8.dp, bottom = 12.dp),
) {
Icon(
imageVector = MeshtasticIcons.Serial,
contentDescription = null,
modifier = Modifier.size(18.dp),
)
Spacer(Modifier.width(8.dp))
Text(stringResource(Res.string.wifi_provision_success_open_ssh))
}
Text(
text = stringResource(Res.string.wifi_provision_success_open_ssh_fallback),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = 16.dp),
)
}
}
Button(onClick = onDone, modifier = Modifier.fillMaxWidth()) {
Text(stringResource(Res.string.wifi_provision_success_done))
}
}
}
@Composable
private fun ProvisionInfoItem(label: String, value: String, copyEnabled: Boolean = true) {
ListItem(
overlineContent = { Text(text = label, style = MaterialTheme.typography.labelLarge) },
headlineContent = { Text(text = value, style = MaterialTheme.typography.bodyLargeEmphasized) },
trailingContent = {
if (copyEnabled) {
CopyIconButton(valueToCopy = value)
}
},
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
)
}
@Composable
internal fun NetworkRow(network: WifiNetwork, isSelected: Boolean, onClick: () -> Unit) {
val containerColor =
@@ -548,3 +695,9 @@ private fun CenteredStatusContent(content: @Composable () -> Unit) {
content()
}
}
@PreviewLightDark
@Composable
private fun ProvisionSuccessContentPreview() {
AppTheme { Surface { ProvisionSuccessContent(ipAddress = "192.168.1.100", onDone = {}) } }
}

View File

@@ -90,6 +90,7 @@ class WifiProvisionViewModelTest {
assertTrue(state.networks.isEmpty())
assertNull(state.error)
assertNull(state.deviceName)
assertNull(state.ipAddress)
assertEquals(ProvisionStatus.Idle, state.provisionStatus)
}
@@ -233,12 +234,13 @@ class WifiProvisionViewModelTest {
advanceUntilIdle()
// Now provision — enqueue success response
emitNymeaResponse("""{"c":1,"r":0}""")
emitNymeaResponse("""{"c":1,"r":0,"p":{"i":"10.10.10.61"}}""")
viewModel.provisionWifi("Net", "password123")
advanceUntilIdle()
val state = viewModel.uiState.value
assertEquals(Phase.Connected, state.phase)
assertEquals("10.10.10.61", state.ipAddress)
assertEquals(ProvisionStatus.Success, state.provisionStatus)
}
@@ -305,6 +307,7 @@ class WifiProvisionViewModelTest {
assertEquals(Phase.Idle, state.phase)
assertTrue(state.networks.isEmpty())
assertNull(state.deviceName)
assertNull(state.ipAddress)
assertEquals(ProvisionStatus.Idle, state.provisionStatus)
}

View File

@@ -82,6 +82,7 @@ class NymeaProtocolTest {
val response = NymeaJson.decodeFromString<NymeaResponse>("""{"c":4,"r":0}""")
assertEquals(4, response.command)
assertEquals(0, response.responseCode)
assertEquals(null, response.connectionInfo)
}
@Test
@@ -91,6 +92,14 @@ class NymeaProtocolTest {
assertEquals(3, response.responseCode)
}
@Test
fun `response deserializes connection info payload`() {
val response = NymeaJson.decodeFromString<NymeaResponse>("""{"c":5,"r":0,"p":{"i":"10.10.10.61"}}""")
assertEquals(5, response.command)
assertEquals(0, response.responseCode)
assertEquals("10.10.10.61", response.connectionInfo?.ipAddress)
}
@Test
fun `response ignores unknown keys`() {
val response = NymeaJson.decodeFromString<NymeaResponse>("""{"c":0,"r":0,"extra":"field"}""")

View File

@@ -221,10 +221,25 @@ class NymeaWifiServiceTest {
val (service, scanner) = createService(connection = connection)
connectService(service, scanner)
emitResponse(connection, """{"c":1,"r":0}""")
emitResponse(connection, """{"c":1,"r":0,"p":{"i":"10.10.10.61"}}""")
val result = service.provision("MyNet", "password")
assertIs<ProvisionResult.Success>(result)
val success = assertIs<ProvisionResult.Success>(result)
assertEquals("10.10.10.61", success.ipAddress)
}
@Test
fun `provision falls back to GetConnection for IP when connect response has no payload`() = runTest {
val connection = FakeBleConnection()
val (service, scanner) = createService(connection = connection)
connectService(service, scanner)
emitResponse(connection, """{"c":1,"r":0}""")
emitResponse(connection, """{"c":5,"r":0,"p":{"i":"10.10.10.62"}}""")
val result = service.provision("MyNet", "password")
val success = assertIs<ProvisionResult.Success>(result)
assertEquals("10.10.10.62", success.ipAddress)
}
@Test
@@ -247,7 +262,7 @@ class NymeaWifiServiceTest {
val (service, scanner) = createService(connection = connection)
connectService(service, scanner)
emitResponse(connection, """{"c":1,"r":0}""")
emitResponse(connection, """{"c":1,"r":0,"p":{"i":"10.10.10.61"}}""")
service.provision("Net", "pass", hidden = false)
val writes =
@@ -266,7 +281,7 @@ class NymeaWifiServiceTest {
val (service, scanner) = createService(connection = connection)
connectService(service, scanner)
emitResponse(connection, """{"c":2,"r":0}""")
emitResponse(connection, """{"c":2,"r":0,"p":{"i":"10.10.10.61"}}""")
service.provision("HiddenNet", "pass", hidden = true)
val writes =