Continued implementing WiFi functionality for RP2040 based devices (internet connection, network scan). mpy_network

Mon, 20 Feb 2023 11:42:45 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Mon, 20 Feb 2023 11:42:45 +0100
branch
mpy_network
changeset 9781
3112f77f722b
parent 9779
8d3c7c991085
child 9782
67414f28db68

Continued implementing WiFi functionality for RP2040 based devices (internet connection, network scan).

eric7.epj file | annotate | diff | comparison | revisions
src/eric7/MicroPython/Devices/DeviceBase.py file | annotate | diff | comparison | revisions
src/eric7/MicroPython/Devices/RP2040Devices.py file | annotate | diff | comparison | revisions
src/eric7/MicroPython/WifiDialogs/WifiController.py file | annotate | diff | comparison | revisions
src/eric7/MicroPython/WifiDialogs/WifiNetworksWindow.py file | annotate | diff | comparison | revisions
src/eric7/MicroPython/WifiDialogs/WifiNetworksWindow.ui file | annotate | diff | comparison | revisions
--- 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>

eric ide

mercurial