Mon, 20 Feb 2023 11:42:45 +0100
Continued implementing WiFi functionality for RP2040 based devices (internet connection, network scan).
--- a/eric7.epj Sun Feb 19 14:45:16 2023 +0100 +++ b/eric7.epj Mon Feb 20 11:42:45 2023 +0100 @@ -349,6 +349,7 @@ "src/eric7/MicroPython/UnknownDevicesDialog.ui", "src/eric7/MicroPython/WifiDialogs/WifiConnectionDialog.ui", "src/eric7/MicroPython/WifiDialogs/WifiCountryDialog.ui", + "src/eric7/MicroPython/WifiDialogs/WifiNetworksWindow.ui", "src/eric7/MicroPython/WifiDialogs/WifiStatusDialog.ui", "src/eric7/MultiProject/AddProjectDialog.ui", "src/eric7/MultiProject/PropertiesDialog.ui", @@ -1318,6 +1319,7 @@ "src/eric7/MicroPython/WifiDialogs/WifiConnectionDialog.py", "src/eric7/MicroPython/WifiDialogs/WifiController.py", "src/eric7/MicroPython/WifiDialogs/WifiCountryDialog.py", + "src/eric7/MicroPython/WifiDialogs/WifiNetworksWindow.py", "src/eric7/MicroPython/WifiDialogs/WifiStatusDialog.py", "src/eric7/MicroPython/WifiDialogs/__init__.py", "src/eric7/MicroPython/__init__.py",
--- a/src/eric7/MicroPython/Devices/DeviceBase.py Sun Feb 19 14:45:16 2023 +0100 +++ b/src/eric7/MicroPython/Devices/DeviceBase.py Mon Feb 20 11:42:45 2023 +0100 @@ -1221,6 +1221,25 @@ """ return True, "" + def checkInternet(self): + """ + Public method to check, if the internet can be reached. + + @return tuple containing a flag indicating reachability and an error string + @rtype tuple of (bool, str) + """ + return False, "" + + def scanNetworks(self): + """ + Public method to scan for available WiFi networks. + + @return tuple containing the list of available networks as a tuple of 'Name', + 'MAC-Address', 'channel', 'RSSI' and 'security' and an error string + @rtype tuple of (list of tuple of (str, str, int, int, str), str) + """ + return [], "" + def addDeviceWifiEntries(self, menu): """ Public method to add device specific entries to the given menu.
--- a/src/eric7/MicroPython/Devices/RP2040Devices.py Sun Feb 19 14:45:16 2023 +0100 +++ b/src/eric7/MicroPython/Devices/RP2040Devices.py Mon Feb 20 11:42:45 2023 +0100 @@ -9,6 +9,7 @@ """ import ast +import binascii import json from PyQt6.QtCore import QUrl, pyqtSlot @@ -47,18 +48,28 @@ self.__statusTranslations = { "picow": { - -3: self.tr('wrong password'), - -2: self.tr('no access point found'), + -3: self.tr('authentication failed'), + -2: self.tr('no matching access point found'), -1: self.tr('connection failed'), 0: self.tr('idle'), 1: self.tr('connecting'), - 2: self.tr('getting IP address'), - 3: self.tr('connection successful'), + 2: self.tr('connected, waiting for IP address'), + 3: self.tr('connected'), }, "pimoroni": { # TODO: not yet implemented }, } + self.__securityTranslations = { + 0: self.tr("open", "open WiFi network"), + 1: "WEP", + 2: "WPA", + 3: "WPA2", + 4: "WPA/WPA2", + 5: "WPA2 (CCMP)", + 6: "WPA3", + 7: "WPA2/WPA3" + } def setButtons(self): """ @@ -581,6 +592,101 @@ return out.decode("utf-8").strip() == "True", "" + def checkInternet(self): + """ + Public method to check, if the internet can be reached. + + @return tuple containing a flag indicating reachability and an error string + @rtype tuple of (bool, str) + """ + if self._deviceData["wifi_type"] == "picow": + command = """ +def check_internet(): + import network + import socket + + wifi = network.WLAN(network.STA_IF) + if wifi.isconnected(): + s = socket.socket() + try: + s.connect(socket.getaddrinfo('google.com', 80)[0][-1]) + s.close() + print(True) + except: + print(False) + else: + print(False) + +check_internet() +del check_internet +""" + elif self._deviceData["wifi_type"] == "pimoroni": + # TODO: not yet implemented + pass + else: + return super().checkInternet() + + out, err = self._interface.execute(command) + if err: + return False, err + + return out.decode("utf-8").strip() == "True", "" + + def scanNetworks(self): + """ + Public method to scan for available WiFi networks. + + @return tuple containing the list of available networks as a tuple of 'Name', + 'MAC-Address', 'channel', 'RSSI' and 'security' and an error string + @rtype tuple of (list of tuple of (str, str, int, int, str), str) + """ + if self._deviceData["wifi_type"] == "picow": + command = """ +def scan_networks(): + import network + + wifi = network.WLAN(network.STA_IF) + active = wifi.active() + if not active: + wifi.active(True) + network_list = wifi.scan() + if not active: + wifi.active(False) + print(network_list) + +scan_networks() +del scan_networks +""" + elif self._deviceData["wifi_type"] == "pimoroni": + # TODO: not yet implemented + pass + else: + return super().checkInternet() + + out, err = self._interface.execute(command, timeout=15000) + if err: + return [], err + + networksList = ast.literal_eval(out.decode("utf-8")) + networks = [] + for network in networksList: + if network[0]: + ssid = network[0].decode("utf-8") + mac = binascii.hexlify(network[1], ":").decode("utf-8") + channel = network[2] + rssi = network[3] + try: + security = self.__securityTranslations[network[4]] + except KeyError: + security = self.tr("unknown ({0})").format(network[4]) + networks.append((ssid, mac, channel, rssi, security)) + + return networks, "" + + ############################################################################ + ## RP2 only methods below + ############################################################################ + @pyqtSlot() def __setCountry(self): """
--- a/src/eric7/MicroPython/WifiDialogs/WifiController.py Sun Feb 19 14:45:16 2023 +0100 +++ b/src/eric7/MicroPython/WifiDialogs/WifiController.py Mon Feb 20 11:42:45 2023 +0100 @@ -55,15 +55,18 @@ wifiMenu.addAction(self.tr("Remove WiFi Credentials"), self.__removeCredentials) wifiMenu.addSeparator() wifiMenu.addAction(self.tr("Start WiFi Access Point"), self.__startAccessPoint) + wifiMenu.addAction( + self.tr("Show Connected Clients"), self.__showConnectedClients + ) wifiMenu.addAction(self.tr("Stop WiFi Access Point"), self.__stopAccessPoint) wifiMenu.addSeparator() wifiMenu.addAction( self.tr("Deactivate Client Interface"), - lambda: self.__deactivateInterface("STA") + lambda: self.__deactivateInterface("STA"), ) wifiMenu.addAction( self.tr("Deactivate Access Point Interface"), - lambda: self.__deactivateInterface("AP") + lambda: self.__deactivateInterface("AP"), ) # add device specific entries (if there are any) @@ -129,17 +132,14 @@ EricMessageBox.information( None, self.tr("Disconnect WiFi"), - self.tr( - "<p>The device was disconnected from the WiFi network.</p>" - ), + self.tr("<p>The device was disconnected from the WiFi network.</p>"), ) else: EricMessageBox.critical( None, self.tr("Disconnect WiFi"), self.tr( - "<p>The device could not be disconnected.</p>" - "<p>Reason: {0}</p>" + "<p>The device could not be disconnected.</p><p>Reason: {0}</p>" ).format(error if error else self.tr("unknown")), ) @@ -148,16 +148,37 @@ """ Private slot to check the availability of an internet connection. """ - # TODO: not implemented yet - pass + success, error = self.__mpy.getDevice().checkInternet() + if not error: + msg = ( + self.tr("<p>The internet connection is <b>available</b>.</p>") + if success + else self.tr("<p>The internet connection is <b>not available</b>.</p>") + ) + EricMessageBox.information( + None, + self.tr("Check Internet Connection"), + msg, + ) + else: + EricMessageBox.critical( + None, + self.tr("Check Internet Connection"), + self.tr( + "<p>The internet is not available.</p><p>Reason: {0}</p>" + ).format(error if error else self.tr("unknown")), + ) @pyqtSlot() def __scanNetwork(self): """ Private slot to scan for visible WiFi networks. """ - # TODO: not implemented yet - pass + from .WifiNetworksWindow import WifiNetworksWindow + + win = WifiNetworksWindow(self.__mpy.getDevice(), self.__mpy) + win.show() + win.scanNetworks() @pyqtSlot() def __writeCredentials(self): @@ -196,6 +217,15 @@ # TODO: not implemented yet pass + @pyqtSlot() + def __showConnectedClients(self): + """ + Private slot to show a list of WiFi clients connected to the Access Point + interface. + """ + # TODO: not implemented yet + pass + def __deactivateInterface(self, interface): """ Private method to deactivate a given WiFi interface of the connected device. @@ -206,4 +236,3 @@ """ # TODO: not implemented yet pass -
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/MicroPython/WifiDialogs/WifiNetworksWindow.py Mon Feb 20 11:42:45 2023 +0100 @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2023 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog showing the available WiFi networks. +""" + +from PyQt6.QtCore import Qt, QTimer, pyqtSlot +from PyQt6.QtWidgets import QHeaderView, QTreeWidgetItem, QWidget + +from eric7.EricGui.EricOverrideCursor import EricOverrideCursor +from eric7.EricWidgets import EricMessageBox + +from .Ui_WifiNetworksWindow import Ui_WifiNetworksWindow + + +class WifiNetworksWindow(QWidget, Ui_WifiNetworksWindow): + """ + Class implementing a dialog showing the available WiFi networks. + """ + + def __init__(self, device, parent=None): + """ + Constructor + + @param device reference to the connected device + @type BaseDevice + @param parent reference to the parent widget (defaults to None) + @type QWidget (optional) + """ + super().__init__(parent) + self.setupUi(self) + + windowFlags = self.windowFlags() + windowFlags |= Qt.WindowType.Window + windowFlags |= Qt.WindowType.WindowContextHelpButtonHint + self.setWindowFlags(windowFlags) + + self.__device = device + + self.__scanTimer = QTimer(self) + self.__scanTimer.timeout.connect(self.scanNetworks) + + self.scanButton.clicked.connect(self.scanNetworks) + + self.networkList.sortByColumn(0, Qt.SortOrder.AscendingOrder) + + def scanNetworks(self): + """ + Private method to ask the device for a network scan and display the result. + """ + self.networkList.clear() + self.statusLabel.clear() + + if not self.periodicCheckBox.isChecked(): + self.scanButton.setEnabled(False) + with EricOverrideCursor(): + networks, error = self.__device.scanNetworks() + if not self.periodicCheckBox.isChecked(): + self.scanButton.setEnabled(True) + + if error: + EricMessageBox.warning( + self, + self.tr("Scan WiFi Networks"), + self.tr( + """<p>The scan for available WiFi networks failed.</p>""" + """<p>Reason: {0}</p>"""), + ) + if self.periodicCheckBox.isChecked(): + self.periodicCheckBox.setChecked(False) + + else: + self.statusLabel.setText( + self.tr("<p>Detected <b>%n</b> network(s).</p>", "", len(networks)) + ) + for network in networks: + itm = QTreeWidgetItem( + self.networkList, + [ + network[0], + str(network[2]), + network[1], + str(network[3]), + network[4] + ], + ) + itm.setTextAlignment(1, Qt.AlignmentFlag.AlignHCenter) + itm.setTextAlignment(2, Qt.AlignmentFlag.AlignHCenter) + itm.setTextAlignment(3, Qt.AlignmentFlag.AlignHCenter) + + self.__resizeColumns() + self.__resort() + + def __resort(self): + """ + Private method to resort the networks list. + """ + self.networkList.sortItems( + self.networkList.sortColumn(), + self.networkList.header().sortIndicatorOrder(), + ) + + def __resizeColumns(self): + """ + Private method to resize the columns of the result list. + """ + self.networkList.header().resizeSections( + QHeaderView.ResizeMode.ResizeToContents + ) + self.networkList.header().setStretchLastSection(True) + + def closeEvent(self, evt): + """ + Public method to handle a window close event. + + @param evt reference to the close event + @type QCloseEvent + """ + self.__scanTimer.stop() + + @pyqtSlot(bool) + def on_periodicCheckBox_toggled(self, checked): + """ + Private slot handling the selection of a periodic scan. + + @param checked flag indicating a periodic scan + @type bool + """ + self.scanButton.setEnabled(not checked) + if checked: + self.__scanTimer.setInterval(self.intervalSpinBox.value() * 1000) + self.__scanTimer.start() + else: + self.__scanTimer.stop() + + @pyqtSlot(int) + def on_intervalSpinBox_valueChanged(self, interval): + """ + Private slot handling a change of the periodic scan interval. + + @param interval periodic scan interval + @type int + """ + if self.periodicCheckBox.isChecked(): + self.__scanTimer.setInterval(interval* 1000)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/MicroPython/WifiDialogs/WifiNetworksWindow.ui Mon Feb 20 11:42:45 2023 +0100 @@ -0,0 +1,156 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>WifiNetworksWindow</class> + <widget class="QWidget" name="WifiNetworksWindow"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>650</width> + <height>600</height> + </rect> + </property> + <property name="windowTitle"> + <string>WiFi Networks</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QTreeWidget" name="networkList"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="selectionMode"> + <enum>QAbstractItemView::NoSelection</enum> + </property> + <property name="rootIsDecorated"> + <bool>false</bool> + </property> + <property name="itemsExpandable"> + <bool>false</bool> + </property> + <property name="sortingEnabled"> + <bool>true</bool> + </property> + <column> + <property name="text"> + <string>Name</string> + </property> + </column> + <column> + <property name="text"> + <string>Channel</string> + </property> + </column> + <column> + <property name="text"> + <string>MAC-Address</string> + </property> + </column> + <column> + <property name="text"> + <string>RSSI [dBm]</string> + </property> + </column> + <column> + <property name="text"> + <string>Security</string> + </property> + </column> + </widget> + </item> + <item> + <widget class="QLabel" name="statusLabel"/> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QPushButton" name="scanButton"> + <property name="toolTip"> + <string>Press to scan for available WiFi networks.</string> + </property> + <property name="text"> + <string>Scan</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Scan Interval:</string> + </property> + </widget> + </item> + <item> + <widget class="QSpinBox" name="intervalSpinBox"> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <property name="suffix"> + <string> s</string> + </property> + <property name="minimum"> + <number>5</number> + </property> + <property name="maximum"> + <number>120</number> + </property> + <property name="value"> + <number>15</number> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="periodicCheckBox"> + <property name="text"> + <string>Periodic Scan</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="standardButtons"> + <set>QDialogButtonBox::Close</set> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>networkList</tabstop> + <tabstop>scanButton</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>WifiNetworksWindow</receiver> + <slot>close()</slot> + <hints> + <hint type="sourcelabel"> + <x>269</x> + <y>563</y> + </hint> + <hint type="destinationlabel"> + <x>271</x> + <y>517</y> + </hint> + </hints> + </connection> + </connections> +</ui>