Sat, 18 Feb 2023 10:11:44 +0100
Created new branch <mpy_network>.
--- a/eric7.epj Sat Feb 18 09:21:42 2023 +0100 +++ b/eric7.epj Sat Feb 18 10:11:44 2023 +0100 @@ -347,6 +347,7 @@ "src/eric7/MicroPython/ShowModulesDialog.ui", "src/eric7/MicroPython/UF2FlashDialog.ui", "src/eric7/MicroPython/UnknownDevicesDialog.ui", + "src/eric7/MicroPython/WifiStatusDialog.ui", "src/eric7/MultiProject/AddProjectDialog.ui", "src/eric7/MultiProject/PropertiesDialog.ui", "src/eric7/Network/IRC/IrcChannelEditDialog.ui", @@ -1312,6 +1313,7 @@ "src/eric7/MicroPython/ShowModulesDialog.py", "src/eric7/MicroPython/UF2FlashDialog.py", "src/eric7/MicroPython/UnknownDevicesDialog.py", + "src/eric7/MicroPython/WifiStatusDialog.py", "src/eric7/MicroPython/__init__.py", "src/eric7/MultiProject/AddProjectDialog.py", "src/eric7/MultiProject/MultiProject.py",
--- a/src/eric7/MicroPython/Devices/DeviceBase.py Sat Feb 18 09:21:42 2023 +0100 +++ b/src/eric7/MicroPython/Devices/DeviceBase.py Sat Feb 18 10:11:44 2023 +0100 @@ -93,6 +93,9 @@ if connected: with contextlib.suppress(OSError): self._deviceData = self.__getDeviceData() + self._deviceData["wifi"], self._deviceData["wifi_type"] = self.hasWifi() + self._deviceData["bluetooth"] = self.hasBluetooth() + self._deviceData["ethernet"] = self.hasEthernet() def getDeviceType(self): """ @@ -103,14 +106,19 @@ """ return self._deviceType - def getDeviceData(self): + def getDeviceData(self, key=None): """ Public method to get a copy of the determined device data. + @param key name of the data to get (None to get all data) (defaults to None) + @type str (optional) @return dictionary containing the essential device data @rtype dict """ - return copy.deepcopy(self._deviceData) + if key is None: + return copy.deepcopy(self._deviceData) + else: + return self._deviceData[key] def checkDeviceData(self): """ @@ -1147,6 +1155,65 @@ if err: raise OSError(self._shortError(err)) + ################################################################## + ## Methods below implement WiFi related methods + ################################################################## + + def hasWifi(self): + """ + Public method to check the availability of WiFi. + + @return tuple containing a flag indicating the availability of WiFi + and the WiFi type (picow or pimoroni) + @rtype tuple of (bool, str) + @exception OSError raised to indicate an issue with the device + """ + return False, "" + + def getWifiData(self): + """ + Public method to get data related to the current WiFi status + + @return tuple of two dictionaries containing the WiFi status data + for the WiFi client and access point + @rtype tuple of (dict, dict) + """ + return {}, {} + + def addDeviceWifiEntries(self, menu): + """ + Public method to add device specific entries to the given menu. + + @param menu reference to the context menu + @type QMenu + """ + pass + + ################################################################## + ## Methods below implement Ethernet related methods + ################################################################## + + def hasEthernet(self): + """ + Public method to check the availability of Ethernet. + + @return flag indicating the availability of Ethernet + @rtype bool + """ + return False + + ################################################################## + ## Methods below implement Bluetooth related methods + ################################################################## + + def hasBluetooth(self): + """ + Public method to check the availability of Bluetooth. + + @return flag indicating the availability of Bluetooth + @rtype bool + """ + return False # # eflag: noqa = M613
--- a/src/eric7/MicroPython/Devices/RP2040Devices.py Sat Feb 18 09:21:42 2023 +0100 +++ b/src/eric7/MicroPython/Devices/RP2040Devices.py Sat Feb 18 10:11:44 2023 +0100 @@ -8,6 +8,9 @@ (e.g. Raspberry Pi Pico). """ +import ast +import json + from PyQt6.QtCore import QUrl, pyqtSlot from PyQt6.QtNetwork import QNetworkRequest from PyQt6.QtWidgets import QMenu @@ -41,6 +44,20 @@ self.__createRP2040Menu() + self.__statusTranslations = { + "picow": { + -3: self.tr('wrong password'), + -2: self.tr('no access point found'), + -1: self.tr('connection failed'), + 0: self.tr('idle'), + 1: self.tr('connecting'), + 3: self.tr('connection successful'), + }, + "pimoroni": { + # TODO: not yet implemented + }, + } + def setButtons(self): """ Public method to enable the supported action buttons. @@ -340,6 +357,113 @@ rtc.datetime(rtc_time[:7] + (0,)) """ + ################################################################## + ## Methods below implement network related methods + ################################################################## + + def hasWifi(self): + """ + Public method to check the availability of WiFi. + + @return tuple containing a flag indicating the availability of WiFi + and the WiFi type (picow or pimoroni) + @rtype tuple of (bool, str) + @exception OSError raised to indicate an issue with the device + """ + command = """ +def has_wifi(): + try: + import network + if hasattr(network, 'WLAN'): + return True, 'picow' + except ImportError: + try: + import picowireless + if picowireless.get_fw_version() != '': + return True, 'pimoroni' + except ImportError: + pass + + return False, '' + +print(has_wifi()) +del has_wifi +""" + out, err = self._interface.execute(command, timeout=10000) + if err: + raise OSError(self._shortError(err)) + return ast.literal_eval(out.decode("utf-8")) + + def getWifiData(self): + """ + Public method to get data related to the current WiFi status + + @return tuple of two dictionaries containing the WiFi status data + for the WiFi client and access point + @rtype tuple of (dict, dict) + """ + if self._deviceData["wifi_type"] == "picow": + command = """ +def wifi_status(): + import ubinascii + import ujson + import network + + wifi = network.WLAN(network.STA_IF) + station = { + 'active': wifi.active(), + 'connected': wifi.isconnected(), + 'status': wifi.status(), + 'ifconfig': wifi.ifconfig(), + 'mac': ubinascii.hexlify(wifi.config('mac'), ':').decode(), + 'channel': wifi.config('channel'), + 'txpower': wifi.config('txpower'), + } + print(ujson.dumps(station)) + + wifi = network.WLAN(network.AP_IF) + ap = { + 'active': wifi.active(), + 'connected': wifi.isconnected(), + 'status': wifi.status(), + 'ifconfig': wifi.ifconfig(), + 'mac': ubinascii.hexlify(wifi.config('mac'), ':').decode(), + 'channel': wifi.config('channel'), + 'txpower': wifi.config('txpower'), + 'essid': wifi.config('essid'), + } + print(ujson.dumps(ap)) + +wifi_status() +del wifi_status +""" + elif self._deviceData["wifi_type"] == "pimoroni": + # TODO: not yet implemented + pass + else: + return super().getWifiData() + + out, err = self._interface.execute(command, timeout=10000) + if err: + raise OSError(self._shortError(err)) + + stationStr, apStr = out.decode("utf-8").splitlines() + station = json.loads(stationStr) + ap = json.loads(apStr) + try: + station["status"] = self.__statusTranslations[ + self._deviceData["wifi_type"] + ][station["status"]] + except KeyError: + station["status"] = str(station["status"]) + try: + ap["status"] = self.__statusTranslations[ + self._deviceData["wifi_type"] + ][ap["status"]] + except KeyError: + ap["status"] = str(ap["status"]) + return station, ap + def createDevice(microPythonWidget, deviceType, vid, pid, boardName, serialNumber): """
--- a/src/eric7/MicroPython/MicroPythonDeviceInterface.py Sat Feb 18 09:21:42 2023 +0100 +++ b/src/eric7/MicroPython/MicroPythonDeviceInterface.py Sat Feb 18 10:11:44 2023 +0100 @@ -189,7 +189,7 @@ return True - def execute(self, commands): + def execute(self, commands, timeout=0): """ Public method to send commands to the connected device and return the result. @@ -198,6 +198,9 @@ @param commands list of commands to be executed @type str or list of str + @param timeout per command timeout in milliseconds (0 for configured default) + (defaults to 0) + @type int (optional) @return tuple containing stdout and stderr output of the device @rtype tuple of (bytes, bytes) """ @@ -239,7 +242,7 @@ ) # read until prompt - response = self.__serial.readUntil(b"\x04>") + response = self.__serial.readUntil(b"\x04>", timeout=timeout) if self.__serial.hasTimedOut(): self.__blockReadyRead = False return b"", b"Timeout while processing commands."
--- a/src/eric7/MicroPython/MicroPythonSerialPort.py Sat Feb 18 09:21:42 2023 +0100 +++ b/src/eric7/MicroPython/MicroPythonSerialPort.py Sat Feb 18 10:11:44 2023 +0100 @@ -92,21 +92,27 @@ """ return self.__timedOut - def readUntil(self, expected=b"\n", size=None): + def readUntil(self, expected=b"\n", size=None, timeout=0): r""" Public method to read data until an expected sequence is found (default: \n) or a specific size is exceeded. @param expected expected bytes sequence @type bytes - @param size maximum data to be read - @type int + @param size maximum data to be read (defaults to None) + @type int (optional) + @param timeout timeout in milliseconds (0 for configured default) + (defaults to 0) + @type int (optional) @return bytes read from the device including the expected sequence @rtype bytes """ data = bytearray() self.__timedOut = False + if timeout == 0: + timeout = self.__timeout + t = QTime.currentTime() while True: QCoreApplication.processEvents( @@ -119,7 +125,7 @@ break if size is not None and len(data) >= size: break - if t.msecsTo(QTime.currentTime()) > self.__timeout: + if t.msecsTo(QTime.currentTime()) > timeout: self.__timedOut = True break
--- a/src/eric7/MicroPython/MicroPythonWidget.py Sat Feb 18 09:21:42 2023 +0100 +++ b/src/eric7/MicroPython/MicroPythonWidget.py Sat Feb 18 10:11:44 2023 +0100 @@ -1436,6 +1436,13 @@ else: downloadMenu = None + # prepare the WiFi menu + if self.__device and self.__connected and self.__device.getDeviceData("wifi"): + wifiMenu = QMenu(self.tr("WiFi Functions"), self.__superMenu) + wifiMenu.addAction(self.tr("Show WiFi Status"), self.__showWifiStatus) + else: + wifiMenu = None + # populate the super menu hasTime = self.__device.hasTimeCommands() if self.__device else False @@ -1479,6 +1486,9 @@ if self.__device: self.__device.addDeviceMenuEntries(self.__superMenu) self.__superMenu.addSeparator() + if wifiMenu is not None: + self.__superMenu.addMenu(wifiMenu) + self.__superMenu.addSeparator() if downloadMenu is None: # generic download action self.__superMenu.addAction( @@ -1988,3 +1998,22 @@ dlg.show() except Exception as exc: self.__showError("getModules()", str(exc)) + + ############################################################################ + ## WiFi related methods below + ############################################################################ + + def __showWifiStatus(self): + """ + Private method to show a dialog with the WiFi status of the current device. + """ + from .WifiStatusDialog import WifiStatusDialog + + try: + clientStatus, apStatus = self.__device.getWifiData() + + dlg = WifiStatusDialog(clientStatus, apStatus) + dlg.exec() + except Exception as exc: + self.__showError("getWifiData()", str(exc)) +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/MicroPython/WifiStatusDialog.py Sat Feb 18 10:11:44 2023 +0100 @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2023 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to show the WiFi status of the connected device. +""" + +from PyQt6.QtWidgets import QDialog, QTreeWidgetItem + +from .Ui_WifiStatusDialog import Ui_WifiStatusDialog + + +class WifiStatusDialog(QDialog, Ui_WifiStatusDialog): + """ + Class implementing a dialog to show the WiFi status of the connected device. + """ + + def __init__(self, clientStatus, apStatus, parent=None): + """ + Constructor + + @param clientStatus dictionary containing the WiFi status data of the + client interface + @type dict + @param apStatus dictionary containing the WiFi status data of the + access point interface + @type dict + @param parent reference to the parent widget (defaults to None) + @type QWidget (optional) + """ + super().__init__(parent) + self.setupUi(self) + + self.statusTree.setColumnCount(2) + + # client interface + if clientStatus: + header = self.__createHeader(self.tr("Client")) + QTreeWidgetItem( + header, + [ + self.tr("Active"), + self.tr("Yes") if clientStatus["active"] else self.tr("No"), + ], + ) + if clientStatus["active"]: + QTreeWidgetItem( + header, + [ + self.tr("Connected"), + self.tr("Yes") if clientStatus["connected"] else self.tr("No"), + ], + ) + QTreeWidgetItem(header, [self.tr("Status"), clientStatus["status"]]) + QTreeWidgetItem( + header, [self.tr("IPv4 Address"), clientStatus["ifconfig"][0]] + ) + QTreeWidgetItem( + header, [self.tr("Netmask"), clientStatus["ifconfig"][1]] + ) + QTreeWidgetItem( + header, [self.tr("Gateway"), clientStatus["ifconfig"][2]] + ) + QTreeWidgetItem(header, [self.tr("DNS"), clientStatus["ifconfig"][3]]) + QTreeWidgetItem(header, [self.tr("MAC-Address"), clientStatus["mac"]]) + QTreeWidgetItem( + header, [self.tr("Channel"), str(clientStatus["channel"])] + ) + QTreeWidgetItem( + header, + [ + self.tr("Tx-Power"), + self.tr("{0} dBm").format(clientStatus["txpower"]), + ], + ) + + # access point interface + if apStatus: + header = self.__createHeader(self.tr("Access Point")) + QTreeWidgetItem( + header, + [ + self.tr("Active"), + self.tr("Yes") if apStatus["active"] else self.tr("No"), + ], + ) + if apStatus["active"]: + QTreeWidgetItem( + header, + [ + self.tr("Connected"), + self.tr("Yes") if apStatus["connected"] else self.tr("No"), + ], + ) + QTreeWidgetItem(header, [self.tr("Status"), apStatus["status"]]) + QTreeWidgetItem( + header, [self.tr("IPv4 Address"), apStatus["ifconfig"][0]] + ) + QTreeWidgetItem(header, [self.tr("Netmask"), apStatus["ifconfig"][1]]) + QTreeWidgetItem(header, [self.tr("Gateway"), apStatus["ifconfig"][2]]) + QTreeWidgetItem(header, [self.tr("DNS"), apStatus["ifconfig"][3]]) + QTreeWidgetItem(header, [self.tr("SSID"), apStatus["essid"]]) + QTreeWidgetItem(header, [self.tr("MAC-Address"), apStatus["mac"]]) + QTreeWidgetItem(header, [self.tr("Channel"), str(apStatus["channel"])]) + QTreeWidgetItem( + header, + [ + self.tr("Tx-Power"), + self.tr("{0} dBm").format(apStatus["txpower"]), + ], + ) + + for col in range(self.statusTree.columnCount()): + self.statusTree.resizeColumnToContents(col) + + def __createHeader(self, headerText): + """ + Private method to create a header item. + + @param headerText text for the header item + @type str + @return reference to the created header item + @rtype QTreeWidgetItem + """ + headerItem = QTreeWidgetItem(self.statusTree, [headerText]) + headerItem.setExpanded(True) + headerItem.setFirstColumnSpanned(True) + + font = headerItem.font(0) + font.setBold(True) + + headerItem.setFont(0, font) + + return headerItem
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/MicroPython/WifiStatusDialog.ui Sat Feb 18 10:11:44 2023 +0100 @@ -0,0 +1,95 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>WifiStatusDialog</class> + <widget class="QDialog" name="WifiStatusDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>600</width> + <height>650</height> + </rect> + </property> + <property name="windowTitle"> + <string>WiFi Status</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QLabel" name="label"> + <property name="font"> + <font> + <pointsize>14</pointsize> + <bold>true</bold> + </font> + </property> + <property name="text"> + <string>WiFi Status</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <widget class="QTreeWidget" name="statusTree"> + <property name="headerHidden"> + <bool>true</bool> + </property> + <column> + <property name="text"> + <string notr="true">1</string> + </property> + </column> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>WifiStatusDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>WifiStatusDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>