First try in implementing wifi configuration flow

This commit is contained in:
feedc0de committed 2026-08-13 14:16:27 +02:00
1 parent 2d95b705cd
commit 65cae8cc9c
2 files changed
+392 -91

No files matched your search

+11 -2
View File
@@ -103,8 +103,17 @@ NavigationPage {
apiKey: "scan"
}
text: qsTr("(%0) Scan Result").arg(wifiScanResult.value == null ? 0 : wifiScanResult.value.length)
onClicked: stackView.push(wiFiScanPageComponent, {wifiScanResult} )
ApiKeyValueHelper {
id: configuredWifis
deviceConnection: theDeviceConnection
apiKey: "wifis"
}
text: qsTr("(%0) Available networks").arg(wifiScanResult.value == null ? 0 : wifiScanResult.value.length)
onClicked: stackView.push(wiFiScanPageComponent, {
wifiScanResult: wifiScanResult,
configuredWifis: configuredWifis
})
enabled: wifiScanResult.value != null
Component {
+381 -89
View File
@@ -5,118 +5,410 @@ import EVChargerApp
BaseNavigationPage {
property ApiKeyValueHelper wifiScanResult
property ApiKeyValueHelper configuredWifis
property var selectedNetwork: null
property int selectedWifiSlot: -1
title: qsTr("Wi-Fi Scan Result")
title: qsTr("Wi-Fi networks")
ListView {
id: listView
function configuredNetworks() {
const result = []
const wifis = configuredWifis === null ? null : configuredWifis.value
if (wifis === null || wifis === undefined)
return result
for (let i = 0; i < wifis.length; ++i) {
const wifi = wifis[i]
if (wifi !== null && wifi !== undefined && wifi.ssid)
result.push({slot: i, ssid: wifi.ssid, hasKey: wifi.key === true})
}
return result
}
function isConfigured(ssid) {
const networks = configuredNetworks()
for (let i = 0; i < networks.length; ++i) {
if (networks[i].ssid === ssid)
return true
}
return false
}
function firstFreeWifiSlot() {
const wifis = configuredWifis === null ? null : configuredWifis.value
if (wifis === null || wifis === undefined)
return -2
for (let i = 0; i < wifis.length; ++i) {
if (wifis[i] === null || wifis[i] === undefined || !wifis[i].ssid)
return i
}
return -1
}
function availableNetworks() {
const result = []
const scan = wifiScanResult === null ? null : wifiScanResult.value
if (scan === null || scan === undefined)
return result
// Android presents an SSID once. Keep the strongest BSSID when the
// same network was observed through multiple access points.
for (let i = 0; i < scan.length; ++i) {
const candidate = scan[i]
if (candidate === null || candidate === undefined || !candidate.ssid)
continue
let existing = -1
for (let j = 0; j < result.length; ++j) {
if (result[j].ssid === candidate.ssid) {
existing = j
break
}
}
if (existing < 0)
result.push(candidate)
else if ((candidate.rssi ?? -1000) > (result[existing].rssi ?? -1000))
result[existing] = candidate
}
result.sort((left, right) => (right.rssi ?? -1000) - (left.rssi ?? -1000))
return result
}
function securityName(encryptionType) {
switch (encryptionType) {
case 0: return qsTr("Open")
case 1: return qsTr("WEP")
case 2: return qsTr("WPA")
case 3: return qsTr("WPA2")
case 4: return qsTr("WPA/WPA2")
case 5: return qsTr("WPA2 Enterprise")
case 6: return qsTr("WPA3")
case 7: return qsTr("WPA2/WPA3")
case 8: return qsTr("WAPI")
case 9: return qsTr("OWE")
case 10: return qsTr("WPA3 Enterprise")
case 11: return qsTr("WPA3 External PSK")
case 12: return qsTr("WPA3 External PSK mixed")
default: return qsTr("Unknown security")
}
}
function needsPassword(network) {
return network.encryptionType !== 0 && network.encryptionType !== 9
}
function signalBars(rssi) {
if (rssi >= -55)
return "▂▄▆█"
if (rssi >= -67)
return "▂▄▆"
if (rssi >= -75)
return "▂▄"
return "▂"
}
function selectNetwork(network) {
if (isConfigured(network.ssid))
return
const slot = firstFreeWifiSlot()
if (slot === -2) {
noSlotDialog.text = qsTr("The Wi-Fi configuration has not been received yet.")
noSlotDialog.open()
return
}
if (slot < 0) {
noSlotDialog.text = qsTr("All Wi-Fi configuration slots are occupied. Delete a configured network first.")
noSlotDialog.open()
return
}
selectedNetwork = network
selectedWifiSlot = slot
passwordInput.clear()
passwordDialog.open()
}
function provisionSelectedNetwork() {
if (selectedNetwork === null || selectedWifiSlot < 0)
return
const value = []
for (let i = 0; i < selectedWifiSlot; ++i)
value.push(null)
value.push({
ssid: selectedNetwork.ssid,
key: needsPassword(selectedNetwork) ? passwordInput.text : ""
})
provisionWifi.setValue(value)
}
headerItems: RowLayout {
BusyIndicator {
visible: provisionWifi.pending || deleteWifi.pending || triggerScan.pending
}
Button {
text: qsTr("Scan again")
enabled: !triggerScan.pending
onClicked: triggerScan.sendMessage({type: "wifiScan"})
}
}
SetValueHelper {
id: provisionWifi
deviceConnection: theDeviceConnection
apiKey: "wifis"
}
SetValueHelper {
id: deleteWifi
deviceConnection: theDeviceConnection
apiKey: "delw"
}
SendMessageHelper {
id: triggerScan
deviceConnection: theDeviceConnection
}
Flickable {
Layout.fillWidth: true
Layout.fillHeight: true
model: wifiScanResult.value
spacing: 5
clip: true
contentHeight: networksColumn.implicitHeight
ScrollBar.vertical: ScrollBar {}
delegate: WhiteBox {
required property var modelData
property var theModelData: modelData
ColumnLayout {
id: networksColumn
width: parent.width - 30
x: 15
spacing: 5
width: listView.width
Text {
Layout.fillWidth: true
text: qsTr("Configured networks")
font.bold: true
visible: configuredRepeater.count > 0
}
GridLayout {
anchors.fill: parent
columns: 2
Repeater {
id: configuredRepeater
model: configuredNetworks()
Text {
text: qsTr("SSID:")
font.bold: true
}
delegate: WhiteBox {
required property var modelData
Layout.fillWidth: true
implicitHeight: configuredRow.implicitHeight + 20
Text {
text: modelData.ssid
}
RowLayout {
id: configuredRow
anchors.fill: parent
anchors.margins: 10
Text {
text: qsTr("Authmode:")
font.bold: true
}
ColumnLayout {
Layout.fillWidth: true
Text {
text: {
switch (modelData.encryptionType)
{
case 0: return qsTr("OPEN")
case 1: return qsTr("WEP")
case 2: return qsTr("WPA_PSK")
case 3: return qsTr("WPA2_PSK")
case 4: return qsTr("WPA_WPA2_PSK")
case 5: return qsTr("WPA2_ENTERPRISE")
case 6: return qsTr("WPA3_PSK")
case 7: return qsTr("WPA2_WPA3_PSK")
case 8: return qsTr("WAPI_PSK")
case 9: return qsTr("OWE")
case 10: return qsTr("WPA3_ENT_192")
case 11: return qsTr("WPA3_EXT_PSK")
case 12: return qsTr("WPA3_EXT_PSK_MIXED_MODE")
Text {
Layout.fillWidth: true
text: modelData.ssid
font.bold: true
elide: Text.ElideRight
}
Text {
text: qsTr("Saved in slot %1").arg(modelData.slot + 1)
color: "gray"
}
}
Button {
text: qsTr("Delete")
enabled: !deleteWifi.pending
onClicked: {
selectedWifiSlot = modelData.slot
deleteDialog.networkName = modelData.ssid
deleteDialog.open()
}
}
return modelData.encryptionType
}
}
}
function wiFiCipherToString(cipher) {
const names = ['NONE', 'WEP40', 'WEP104', 'TKIP', 'CCMP', 'TKIP_CCMP', 'AES_CMAC128', 'SMS4', 'GCMP', 'GCMP256', 'AES_GMAC128', 'AES_GMAC256', 'UNKNOWN'];
Text {
Layout.fillWidth: true
Layout.topMargin: configuredRepeater.count > 0 ? 10 : 0
text: qsTr("Available networks")
font.bold: true
}
if (cipher >= 0 && cipher < names.length)
return names[cipher];
Repeater {
model: availableNetworks()
return cipher;
delegate: WhiteItemDelegate {
required property var modelData
readonly property bool alreadyConfigured: isConfigured(modelData.ssid)
Layout.fillWidth: true
implicitHeight: availableRow.implicitHeight + 20
enabled: !alreadyConfigured && !provisionWifi.pending
onClicked: selectNetwork(modelData)
contentItem: RowLayout {
id: availableRow
ColumnLayout {
Layout.fillWidth: true
Text {
Layout.fillWidth: true
text: modelData.ssid
font.bold: true
elide: Text.ElideRight
}
Text {
Layout.fillWidth: true
text: alreadyConfigured
? qsTr("Saved • %1").arg(securityName(modelData.encryptionType))
: securityName(modelData.encryptionType)
color: "gray"
elide: Text.ElideRight
}
}
Text {
text: signalBars(modelData.rssi)
font.bold: true
Accessible.name: qsTr("Signal strength %1 dBm").arg(modelData.rssi)
}
Text {
text: alreadyConfigured ? "✓" : ">"
}
}
}
}
Text {
text: qsTr("Pairwise cipher:")
font.bold: true
}
Text {
Layout.fillWidth: true
text: qsTr("No networks found. Start another scan.")
visible: availableNetworks().length === 0
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
}
Text {
text: parent.wiFiCipherToString("f" in modelData ? modelData.f[0] : modelData.pairwiseCipher)
}
RequestStatusText {
Layout.fillWidth: true
request: provisionWifi
}
Text {
text: qsTr("Group cipher:")
font.bold: true
}
Text {
text: parent.wiFiCipherToString("f" in modelData ? modelData.f[1] : modelData.groupCipher)
}
Text {
text: qsTr("RSSI:")
font.bold: true
}
Text {
text: modelData.rssi
}
Text {
text: qsTr("Channel:")
font.bold: true
}
Text {
text: modelData.channel
}
Text {
text: qsTr("BSSID:")
font.bold: true
}
Text {
text: modelData.bssid
}
RequestStatusText {
Layout.fillWidth: true
request: deleteWifi
}
}
}
CenteredDialog {
id: passwordDialog
title: selectedNetwork === null ? qsTr("Connect to Wi-Fi") : selectedNetwork.ssid
standardButtons: Dialog.Ok | Dialog.Cancel
modal: true
focus: true
onOpened: {
passwordInput.forceActiveFocus()
standardButton(Dialog.Ok).enabled = !needsPassword(selectedNetwork)
}
onAccepted: provisionSelectedNetwork()
contentItem: ColumnLayout {
Text {
Layout.fillWidth: true
text: selectedNetwork === null ? "" : securityName(selectedNetwork.encryptionType)
color: "gray"
}
TextField {
id: passwordInput
Layout.minimumWidth: 240
Layout.fillWidth: true
visible: selectedNetwork !== null && needsPassword(selectedNetwork)
placeholderText: qsTr("Password")
echoMode: showPassword.checked ? TextInput.Normal : TextInput.Password
maximumLength: 64
inputMethodHints: Qt.ImhSensitiveData | Qt.ImhNoPredictiveText
onTextChanged: {
if (passwordDialog.visible)
passwordDialog.standardButton(Dialog.Ok).enabled = text.length >= 8
}
onAccepted: {
if (text.length >= 8)
passwordDialog.accept()
}
}
CheckBox {
id: showPassword
visible: passwordInput.visible
text: qsTr("Show password")
}
Text {
Layout.fillWidth: true
visible: passwordInput.visible && passwordInput.text.length > 0 && passwordInput.text.length < 8
text: qsTr("The password must contain at least 8 characters.")
color: "red"
wrapMode: Text.Wrap
}
Text {
Layout.fillWidth: true
visible: selectedNetwork !== null && !needsPassword(selectedNetwork)
text: qsTr("This network does not require a password.")
wrapMode: Text.Wrap
}
Text {
Layout.fillWidth: true
text: qsTr("The network will be saved in slot %1.").arg(selectedWifiSlot + 1)
wrapMode: Text.Wrap
}
}
}
CenteredDialog {
id: deleteDialog
property string networkName
title: qsTr("Delete Wi-Fi network?")
standardButtons: Dialog.Yes | Dialog.No
modal: true
focus: true
onAccepted: deleteWifi.setValue(selectedWifiSlot)
contentItem: Text {
text: qsTr("Delete %1 and all of its IP and DNS settings?").arg(deleteDialog.networkName)
wrapMode: Text.Wrap
}
}
CenteredDialog {
id: noSlotDialog
property alias text: noSlotText.text
title: qsTr("Cannot add Wi-Fi network")
standardButtons: Dialog.Ok
modal: true
focus: true
contentItem: Text {
id: noSlotText
wrapMode: Text.Wrap
}
}
}