Tue, 07 Mar 2023 16:22:07 +0100
MicroPython
- Added support for Bluetooth enabled boards.
--- a/docs/changelog.md Mon Mar 06 17:03:40 2023 +0100 +++ b/docs/changelog.md Tue Mar 07 16:22:07 2023 +0100 @@ -6,6 +6,7 @@ - Added functionality to search for known boot volumes in the UF2 flash dialog. - Added functionality to install packages using `mip` or `upip`. - Added support for WiFi enabled boards. + - Added support for Bluetooth enabled boards. - Third Party packages - Upgraded eradicate to version 2.2.0. - Upgraded pipdeptree to version 2.5.2.
--- a/eric7.epj Mon Mar 06 17:03:40 2023 +0100 +++ b/eric7.epj Tue Mar 07 16:22:07 2023 +0100 @@ -333,6 +333,7 @@ "src/eric7/IconEditor/IconSizeDialog.ui", "src/eric7/JediInterface/RefactoringPreviewDialog.ui", "src/eric7/MicroPython/AddEditDevicesDialog.ui", + "src/eric7/MicroPython/BluetoothDialogs/BluetoothStatusDialog.ui", "src/eric7/MicroPython/BoardDataDialog.ui", "src/eric7/MicroPython/ConnectionSelectionDialog.ui", "src/eric7/MicroPython/Devices/CircuitPythonUpdater/RequirementsDialog.ui", @@ -1289,6 +1290,9 @@ "src/eric7/JediInterface/RefactoringPreviewDialog.py", "src/eric7/JediInterface/__init__.py", "src/eric7/MicroPython/AddEditDevicesDialog.py", + "src/eric7/MicroPython/BluetoothDialogs/BluetoothController.py", + "src/eric7/MicroPython/BluetoothDialogs/BluetoothStatusDialog.py", + "src/eric7/MicroPython/BluetoothDialogs/__init__.py", "src/eric7/MicroPython/BoardDataDialog.py", "src/eric7/MicroPython/ConnectionSelectionDialog.py", "src/eric7/MicroPython/Devices/CircuitPythonDevices.py",
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/MicroPython/BluetoothDialogs/BluetoothController.py Tue Mar 07 16:22:07 2023 +0100 @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2023 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the Bluetooth related functionality. +""" + +from PyQt6.QtCore import QObject, pyqtSlot +from PyQt6.QtWidgets import QMenu + +from eric7.EricWidgets import EricMessageBox + + +class BluetoothController(QObject): + """ + Class implementing the Bluetooth related functionality. + """ + + def __init__(self, microPython, parent=None): + """ + Constructor + + @param microPython reference to the MicroPython widget + @type MicroPythonWidgep + @param parent reference to the parent object (defaults to None) + @type QObject (optional) + """ + super().__init__(parent) + + self.__mpy = microPython + + def createMenu(self, menu): + """ + Public method to create the Bluetooth submenu. + + @param menu reference to the parent menu + @type QMenu + @return reference to the created menu + @rtype QMenu + """ + btMenu = QMenu(self.tr("Bluetooth Functions"), menu) + btMenu.setTearOffEnabled(True) + btMenu.addAction(self.tr("Show Bluetooth Status"), self.__showBtStatus) + btMenu.addSeparator() + btMenu.addSeparator() + btMenu.addAction( + self.tr("Activate Bluetooth Interface"), + lambda: self.__activateInterface(), + ) + btMenu.addAction( + self.tr("Deactivate Bluetooth Interface"), + lambda: self.__deactivateInterface(), + ) + + # add device specific entries (if there are any) + self.__mpy.getDevice().addDeviceBluetoothEntries(btMenu) + + return btMenu + + @pyqtSlot() + def __showBtStatus(self): + """ + Private slot to show the status and some parameters of the Bluetooth interface. + """ + from .BluetoothStatusDialog import BluetoothStatusDialog + + try: + status = self.__mpy.getDevice().getBluetoothStatus() + # status is a list of user labels and associated values + + dlg = BluetoothStatusDialog(status, self.__mpy) + dlg.exec() + except Exception as exc: + self.__mpy.showError("getBluetoothStatus()", str(exc)) + + @pyqtSlot() + def __activateInterface(self): + """ + Private slot to activate the Bluetooth interface. + """ + try: + status = self.__mpy.getDevice().activateBluetoothInterface() + if status: + EricMessageBox.information( + None, + self.tr("Activate Bluetooth Interface"), + self.tr("""Bluetooth was activated successfully."""), + ) + else: + EricMessageBox.warning( + None, + self.tr("Activate Bluetooth Interface"), + self.tr("""Bluetooth could not be activated."""), + ) + except Exception as exc: + self.__mpy.showError("activateBluetoothInterface()", str(exc)) + + @pyqtSlot() + def __deactivateInterface(self): + """ + Private slot to deactivate the Bluetooth interface. + """ + try: + status = self.__mpy.getDevice().deactivateBluetoothInterface() + if not status: + EricMessageBox.information( + None, + self.tr("Deactivate Bluetooth Interface"), + self.tr("""Bluetooth was deactivated successfully."""), + ) + else: + EricMessageBox.warning( + None, + self.tr("Deactivate Bluetooth Interface"), + self.tr("""Bluetooth could not be deactivated."""), + ) + except Exception as exc: + self.__mpy.showError("deactivateBluetoothInterface()", str(exc))
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/MicroPython/BluetoothDialogs/BluetoothStatusDialog.py Tue Mar 07 16:22:07 2023 +0100 @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- + +""" +Module implementing BluetoothStatusDialog. +""" + +from PyQt6.QtCore import Qt +from PyQt6.QtWidgets import QDialog, QDialogButtonBox, QTreeWidgetItem + +from .Ui_BluetoothStatusDialog import Ui_BluetoothStatusDialog + + +class BluetoothStatusDialog(QDialog, Ui_BluetoothStatusDialog): + """ + Class documentation goes here. + """ + + def __init__(self, status, parent=None): + """ + Constructor + + @param status status data to be show + @type list of tuples of (str, str) + @param parent reference to the parent widget (defaults to None) + @type QWidget (optional) + """ + super().__init__(parent) + self.setupUi(self) + + self.statusTree.setColumnCount(2) + + for topic, value in status: + QTreeWidgetItem(self.statusTree, [topic, str(value)]) + + for col in range(self.statusTree.columnCount()): + self.statusTree.resizeColumnToContents(col) + + self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setDefault(True) + self.buttonBox.setFocus(Qt.FocusReason.OtherFocusReason)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/MicroPython/BluetoothDialogs/BluetoothStatusDialog.ui Tue Mar 07 16:22:07 2023 +0100 @@ -0,0 +1,101 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>BluetoothStatusDialog</class> + <widget class="QDialog" name="BluetoothStatusDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>600</width> + <height>650</height> + </rect> + </property> + <property name="windowTitle"> + <string>Bluetooth 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>Bluetooth Status</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <widget class="QTreeWidget" name="statusTree"> + <property name="rootIsDecorated"> + <bool>false</bool> + </property> + <property name="itemsExpandable"> + <bool>false</bool> + </property> + <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::Close</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>BluetoothStatusDialog</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>BluetoothStatusDialog</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>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/MicroPython/BluetoothDialogs/__init__.py Tue Mar 07 16:22:07 2023 +0100 @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2023 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Package implementing Bluetooth related dialogs. +"""
--- a/src/eric7/MicroPython/Devices/CircuitPythonDevices.py Mon Mar 06 17:03:40 2023 +0100 +++ b/src/eric7/MicroPython/Devices/CircuitPythonDevices.py Tue Mar 07 16:22:07 2023 +0100 @@ -83,6 +83,12 @@ 6: "[wifi.AuthMode.WPA3, wifi.AuthMode.PSK]", 7: "[wifi.AuthMode.WPA2, wifi.AuthMode.WPA3, wifi.AuthMode.PSK]", } + self.__bleAddressType = { + 0: self.tr("Public"), + 1: self.tr("Random Static"), + 2: self.tr("Random Private Resolvable"), + 3: self.tr("Random Private Non-Resolvable"), + } def setConnected(self, connected): """ @@ -1137,6 +1143,142 @@ self.tr("CircuitPython does not support reporting of connected clients."), ) + ################################################################## + ## 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 + """ + command = """ +def has_bt(): + try: + import _bleio + if hasattr(_bleio, 'adapter'): + return True + except ImportError: + pass + + return False + +print(has_bt()) +del has_bt +""" + out, err = self._interface.execute( + command, mode=self._submitMode, timeout=10000 + ) + if err: + raise OSError(self._shortError(err)) + return out.strip() == b"True" + + def getBluetoothStatus(self): + """ + Public method to get Bluetooth status data of the connected board. + + @return list of tuples containing the translated status data label and + the associated value + @rtype list of tuples of (str, str) + """ + command = """ +def ble_status(): + import _bleio + import binascii + import json + + a = _bleio.adapter + + ble_enabled = a.enabled + if not ble_enabled: + a.enabled = True + + res = { + 'active': ble_enabled, + 'mac': binascii.hexlify(bytes(reversed(a.address.address_bytes)), ':').decode(), + 'addr_type': a.address.type, + 'name': a.name, + 'advertising': a.advertising, + 'connected': a.connected, + } + + if not ble_enabled: + a.enabled = False + + print(json.dumps(res)) + +ble_status() +del ble_status +""" + out, err = self._interface.execute(command, mode=self._submitMode) + if err: + raise OSError(self._shortError(err)) + + status = [] + bleStatus = json.loads(out.decode("utf-8")) + status.append((self.tr("Active"), self.bool2str(bleStatus["active"]))) + status.append((self.tr("Name"), bleStatus["name"])) + status.append((self.tr("MAC-Address"), bleStatus["mac"])) + status.append( + (self.tr("Address Type"), self.__bleAddressType[bleStatus["addr_type"]]) + ) + status.append((self.tr("Connected"), self.bool2str(bleStatus["connected"]))) + status.append((self.tr("Advertising"), self.bool2str(bleStatus["advertising"]))) + + return status + + def activateBluetoothInterface(self): + """ + Public method to activate the Bluetooth interface. + + @return flag indicating the new state of the Bluetooth interface + @rtype bool + """ + command = """ +def activate_ble(): + import _bleio + + a = _bleio.adapter + if not a.enabled: + a.enabled = True + print(a.enabled) + +activate_ble() +del activate_ble +""" + out, err = self._interface.execute(command, mode=self._submitMode) + if err: + raise OSError(self._shortError(err)) + + return out.strip() == b"True" + + def deactivateBluetoothInterface(self): + """ + Public method to deactivate the Bluetooth interface. + + @return flag indicating the new state of the Bluetooth interface + @rtype bool + """ + command = """ +def deactivate_ble(): + import _bleio + + a = _bleio.adapter + if a.enabled: + a.enabled = False + print(a.enabled) + +deactivate_ble() +del deactivate_ble +""" + out, err = self._interface.execute(command, mode=self._submitMode) + if err: + raise OSError(self._shortError(err)) + + return out.strip() == b"True" + def createDevice(microPythonWidget, deviceType, vid, pid, boardName, serialNumber): """
--- a/src/eric7/MicroPython/Devices/DeviceBase.py Mon Mar 06 17:03:40 2023 +0100 +++ b/src/eric7/MicroPython/Devices/DeviceBase.py Tue Mar 07 16:22:07 2023 +0100 @@ -76,6 +76,13 @@ <li>stopAccessPoint: stop the access point</li> <li>getConnectedClients: get a list of connected WiFi clients</li> </ul> + + Supported Bluetooth commands are: + <ul> + <li>hasBluetooth: check, if the board has Bluetooth functionality</li> + <li>getBluetoothStatus: get Bluetooth status data</li> + <li>deactivateBluetoothInterface: deactivate a Bluetooth interface</li> + </ul> """ def __init__(self, microPythonWidget, deviceType, parent=None): @@ -1466,6 +1473,62 @@ """ return False + def addDeviceBluetoothEntries(self, menu): + """ + Public method to add device specific entries to the given menu. + + @param menu reference to the context menu + @type QMenu + """ + pass + + def getBluetoothStatus(self): + """ + Public method to get Bluetooth status data of the connected board. + + @return list of tuples containing the translated status data label and + the associated value + @rtype list of tuples of (str, str) + """ + return [] + + def activateBluetoothInterface(self): + """ + Public method to activate the Bluetooth interface. + + @return flag indicating the new state of the Bluetooth interface + @rtype bool + """ + return False + + def deactivateBluetoothInterface(self): + """ + Public method to deactivate the Bluetooth interface. + + @return flag indicating the new state of the Bluetooth interface + @rtype bool + """ + return False + + ################################################################## + ## Methods below implement some utility methods + ################################################################## + + def bool2str(self, val, capitalized=True): + """ + Public method to generate a yes/no string given a truth value. + + @param val truth value to be converted + @type bool + @param capitalized flag indicating a capitalized variant + @type bool + @return string with 'yes' or 'no' + @rtype str + """ + if capitalized: + return self.tr("Yes") if val else self.tr("No") + else: + return self.tr("yes") if val else self.tr("no") # # eflag: noqa = M613
--- a/src/eric7/MicroPython/Devices/EspDevices.py Mon Mar 06 17:03:40 2023 +0100 +++ b/src/eric7/MicroPython/Devices/EspDevices.py Tue Mar 07 16:22:07 2023 +0100 @@ -1038,6 +1038,149 @@ clientsList = ast.literal_eval(out.decode("utf-8")) return clientsList, "" + ################################################################## + ## 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 + """ + command = """ +def has_bt(): + try: + import bluetooth + if hasattr(bluetooth, 'BLE'): + return True + except ImportError: + pass + + return False + +print(has_bt()) +del has_bt +""" + out, err = self._interface.execute( + command, mode=self._submitMode, timeout=10000 + ) + if err: + raise OSError(self._shortError(err)) + return out.strip() == b"True" + + def getBluetoothStatus(self): + """ + Public method to get Bluetooth status data of the connected board. + + @return list of tuples containing the translated status data label and + the associated value + @rtype list of tuples of (str, str) + """ + command = """ +def ble_status(): + import bluetooth + import ubinascii + import ujson + + ble = bluetooth.BLE() + + ble_active = ble.active() + if not ble_active: + ble.active(True) + + res = { + 'active': ble_active, + 'mac': ubinascii.hexlify(ble.config('mac')[1], ':').decode(), + 'addr_type': ble.config('mac')[0], + 'name': ble.config('gap_name'), + 'rxbuf': ble.config('rxbuf'), + 'mtu': ble.config('mtu'), + } + + if not ble_active: + ble.active(False) + + print(ujson.dumps(res)) + +ble_status() +del ble_status +""" + out, err = self._interface.execute(command, mode=self._submitMode) + if err: + raise OSError(self._shortError(err)) + + status = [] + bleStatus = json.loads(out.decode("utf-8")) + status.append((self.tr("Active"), self.bool2str(bleStatus["active"]))) + status.append((self.tr("Name"), bleStatus["name"])) + status.append((self.tr("MAC-Address"), bleStatus["mac"])) + status.append( + ( + self.tr("Address Type"), + self.tr("Public") if bleStatus == 0 else self.tr("Random"), + ) + ) + status.append( + (self.tr("Rx-Buffer"), self.tr("{0} Bytes").format(bleStatus["rxbuf"])) + ) + status.append( + (self.tr("MTU"), self.tr("{0} Bytes").format(bleStatus["mtu"])) + ) + + return status + + def activateBluetoothInterface(self): + """ + Public method to activate the Bluetooth interface. + + @return flag indicating the new state of the Bluetooth interface + @rtype bool + """ + command = """ +def activate_ble(): + import bluetooth + + ble = bluetooth.BLE() + if not ble.active(): + ble.active(True) + print(ble.active()) + +activate_ble() +del activate_ble +""" + out, err = self._interface.execute(command, mode=self._submitMode) + if err: + raise OSError(self._shortError(err)) + + return out.strip() == b"True" + + def deactivateBluetoothInterface(self): + """ + Public method to deactivate the Bluetooth interface. + + @return flag indicating the new state of the Bluetooth interface + @rtype bool + """ + command = """ +def deactivate_ble(): + import bluetooth + + ble = bluetooth.BLE() + if ble.active(): + ble.active(False) + print(ble.active()) + +deactivate_ble() +del deactivate_ble +""" + out, err = self._interface.execute(command, mode=self._submitMode) + if err: + raise OSError(self._shortError(err)) + + return out.strip() == b"True" + def createDevice(microPythonWidget, deviceType, vid, pid, boardName, serialNumber): """
--- a/src/eric7/MicroPython/MicroPythonWidget.py Mon Mar 06 17:03:40 2023 +0100 +++ b/src/eric7/MicroPython/MicroPythonWidget.py Tue Mar 07 16:22:07 2023 +0100 @@ -42,6 +42,7 @@ from eric7.UI.Info import BugAddress from . import Devices, UF2FlashDialog +from .BluetoothDialogs.BluetoothController import BluetoothController from .MicroPythonFileManager import MicroPythonFileManager from .MicroPythonFileManagerWidget import MicroPythonFileManagerWidget from .Ui_MicroPythonWidget import Ui_MicroPythonWidget @@ -231,6 +232,9 @@ self.__wifiController = WifiController(self, self) self.__wifiMenu = None + self.__bluetoothController = BluetoothController(self, self) + self.__btMenu = None + self.__superMenu = QMenu(self) self.__superMenu.aboutToShow.connect(self.__aboutToShowSuperMenu) @@ -1494,6 +1498,18 @@ else: self.__wifiMenu = None + # prepare the Bluetooth menu + if ( + self.__device + and self.__connected + and self.__device.getDeviceData("bluetooth") + ): + if self.__btMenu is not None: + self.__btMenu.deleteLater() + self.__btMenu = self.__bluetoothController.createMenu(self.__superMenu) + else: + self.__btMenu = None + # populate the super menu hasTime = self.__device.hasTimeCommands() if self.__device else False @@ -1547,6 +1563,9 @@ self.__superMenu.addSeparator() if self.__wifiMenu is not None: self.__superMenu.addMenu(self.__wifiMenu) + if self.__btMenu is not None: + self.__superMenu.addMenu(self.__btMenu) + if self.__wifiMenu is not None or self.__btMenu is not None: self.__superMenu.addSeparator() if downloadMenu is None: # generic download action