forked from feedc0de/evcharger-app
First try in implementing wifi configuration flow
This commit is contained in:
1 parent
2d95b705cd
commit
65cae8cc9c
2 files changed
+392
-91
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user