src/eric7/MicroPython/Devices/CircuitPythonDevices.py

branch
mpy_network
changeset 9885
05cbf70e8f10
parent 9881
5ce653f9dac8
child 9898
5bfb3c70e30b
equal deleted inserted replaced
9883:7e073ff57760 9885:05cbf70e8f10
16 from PyQt6.QtCore import QUrl, pyqtSlot 16 from PyQt6.QtCore import QUrl, pyqtSlot
17 from PyQt6.QtNetwork import QNetworkReply, QNetworkRequest 17 from PyQt6.QtNetwork import QNetworkReply, QNetworkRequest
18 from PyQt6.QtWidgets import QMenu 18 from PyQt6.QtWidgets import QMenu
19 19
20 from eric7 import Globals, Preferences 20 from eric7 import Globals, Preferences
21 from eric7.EricGui.EricOverrideCursor import EricOverrideCursor 21 from eric7.EricGui.EricOverrideCursor import EricOverrideCursor, EricOverridenCursor
22 from eric7.EricWidgets import EricFileDialog, EricMessageBox 22 from eric7.EricWidgets import EricFileDialog, EricMessageBox
23 from eric7.EricWidgets.EricApplication import ericApp 23 from eric7.EricWidgets.EricApplication import ericApp
24 from eric7.SystemUtilities import FileSystemUtilities 24 from eric7.SystemUtilities import FileSystemUtilities
25 25
26 from ..EthernetDialogs import WiznetUtilities
26 from ..MicroPythonWidget import HAS_QTCHART 27 from ..MicroPythonWidget import HAS_QTCHART
27 from . import FirmwareGithubUrls 28 from . import FirmwareGithubUrls
28 from .CircuitPythonUpdater.CircuitPythonUpdaterInterface import ( 29 from .CircuitPythonUpdater.CircuitPythonUpdaterInterface import (
29 CircuitPythonUpdaterInterface, 30 CircuitPythonUpdaterInterface,
30 isCircupAvailable, 31 isCircupAvailable,
38 """ 39 """
39 40
40 DeviceVolumeName = "CIRCUITPY" 41 DeviceVolumeName = "CIRCUITPY"
41 42
42 def __init__( 43 def __init__(
43 self, microPythonWidget, deviceType, boardName, hasWorkspace=True, parent=None 44 self,
45 microPythonWidget,
46 deviceType,
47 boardName,
48 vid=0,
49 pid=0,
50 hasWorkspace=True,
51 parent=None,
44 ): 52 ):
45 """ 53 """
46 Constructor 54 Constructor
47 55
48 @param microPythonWidget reference to the main MicroPython widget 56 @param microPythonWidget reference to the main MicroPython widget
49 @type MicroPythonWidget 57 @type MicroPythonWidget
50 @param deviceType device type assigned to this device interface 58 @param deviceType device type assigned to this device interface
51 @type str 59 @type str
52 @param boardName name of the board 60 @param boardName name of the board
53 @type str 61 @type str
62 @param vid vendor ID (defaults to 0)
63 @type int (optional)
64 @param pid product ID (defaults to 0)
65 @type int (optional)
54 @param hasWorkspace flag indicating that the devices supports access via 66 @param hasWorkspace flag indicating that the devices supports access via
55 a mounted volume (defaults to True) 67 a mounted volume (defaults to True)
56 @type bool (optional) 68 @type bool (optional)
57 @param parent reference to the parent object 69 @param parent reference to the parent object
58 @type QObject 70 @type QObject
60 super().__init__(microPythonWidget, deviceType, parent) 72 super().__init__(microPythonWidget, deviceType, parent)
61 73
62 self._submitMode = "paste" # use 'paste' mode to avoid loosing state 74 self._submitMode = "paste" # use 'paste' mode to avoid loosing state
63 75
64 self.__boardName = boardName 76 self.__boardName = boardName
77 self.__vidpid = (vid, pid)
65 78
66 self.__workspace = self.__findWorkspace() if hasWorkspace else None 79 self.__workspace = self.__findWorkspace() if hasWorkspace else None
67 80
68 self.__updater = CircuitPythonUpdaterInterface(self) 81 self.__updater = CircuitPythonUpdaterInterface(self)
69 82
70 self.__createCPyMenu() 83 self.__createCPyMenu()
84
85 self.__wiznetVidPid = (
86 (0x2E8A, 0x1027), # WIZnet W5100S-EVB-Pico
87 (0x2E8A, 0x1029), # WIZnet W5500-EVB-Pico
88 )
71 89
72 self.__securityTranslations = { 90 self.__securityTranslations = {
73 "OPEN": self.tr("open", "open WiFi network"), 91 "OPEN": self.tr("open", "open WiFi network"),
74 "WEP": "WEP", 92 "WEP": "WEP",
75 "WPA_PSK": "WPA", 93 "WPA_PSK": "WPA",
108 126
109 if self.__flashMenu.isTearOffMenuVisible(): 127 if self.__flashMenu.isTearOffMenuVisible():
110 self.__flashMenu.hideTearOffMenu() 128 self.__flashMenu.hideTearOffMenu()
111 129
112 super().setConnected(connected) 130 super().setConnected(connected)
131
132 if (
133 connected
134 and not self._deviceData["ethernet"]
135 and self.__vidpid in self.__wiznetVidPid
136 ):
137 with EricOverridenCursor():
138 EricMessageBox.warning(
139 None,
140 self.tr("WIZnet 5x00 Ethernet"),
141 self.tr(
142 "<p>Support for <b>WIZnet 5x00</b> Ethernet boards could not be"
143 " detected. Is the module <b>adafruit_wiznet5k</b> installed?"
144 "</p>"
145 ),
146 )
113 147
114 def setButtons(self): 148 def setButtons(self):
115 """ 149 """
116 Public method to enable the supported action buttons. 150 Public method to enable the supported action buttons.
117 """ 151 """
892 ).format(filename), 926 ).format(filename),
893 icon=EricMessageBox.Warning, 927 icon=EricMessageBox.Warning,
894 ) 928 )
895 if not ok: 929 if not ok:
896 return False, self.tr("Aborted") 930 return False, self.tr("Aborted")
897 # step 2: create the auto-connect script (wifi_connect.py)
898 try: 931 try:
899 with open(filename, "w") as f: 932 with open(filename, "w") as f:
900 f.write(contents) 933 f.write(contents)
901 except OSError as err: 934 except OSError as err:
902 return False, str(err) 935 return False, str(err)
936
937 # step 2: create the auto-connect script (wifi_connect.py)
903 scriptFile = os.path.join( 938 scriptFile = os.path.join(
904 os.path.dirname(__file__), "MCUScripts", "circuitPy7WiFiConnect.py" 939 os.path.dirname(__file__), "MCUScripts", "circuitPy7WiFiConnect.py"
905 ) 940 )
906 targetFile = os.path.join(workspace, "wifi_connect.py") 941 targetFile = os.path.join(workspace, "wifi_connect.py")
907 try: 942 try:
1148 [], 1183 [],
1149 self.tr("CircuitPython does not support reporting of connected clients."), 1184 self.tr("CircuitPython does not support reporting of connected clients."),
1150 ) 1185 )
1151 1186
1152 ################################################################## 1187 ##################################################################
1188 ## Methods below implement Ethernet related methods
1189 ##################################################################
1190
1191 def hasEthernet(self):
1192 """
1193 Public method to check the availability of Ethernet.
1194
1195 @return tuple containing a flag indicating the availability of Ethernet
1196 and the Ethernet type
1197 @rtype tuple of (bool, str)
1198 @exception OSError raised to indicate an issue with the device
1199 """
1200 command = """
1201 def has_eth():
1202 try:
1203 from adafruit_wiznet5k import adafruit_wiznet5k
1204 if hasattr(adafruit_wiznet5k, 'WIZNET5K'):
1205 return True, 'cpypicowiz'
1206 except ImportError:
1207 pass
1208
1209 return False, ''
1210
1211 print(has_eth())
1212 del has_eth
1213 """
1214
1215 out, err = self._interface.execute(
1216 command, mode=self._submitMode, timeout=10000
1217 )
1218 if err:
1219 raise OSError(self._shortError(err))
1220
1221 return ast.literal_eval(out.decode("utf-8"))
1222
1223 def getEthernetStatus(self):
1224 """
1225 Public method to get Ethernet status data of the connected board.
1226
1227 @return list of tuples containing the translated status data label and
1228 the associated value
1229 @rtype list of tuples of (str, str)
1230 @exception OSError raised to indicate an issue with the device
1231 """
1232 command = """{0}
1233 def ethernet_status():
1234 import binascii
1235 import json
1236
1237 w5x00_init()
1238
1239 res = {{
1240 'active': nic.link_status != 0,
1241 'connected': nic.link_status == 1 and nic.ifconfig[0] != b'\x00\x00\x00\x00',
1242 'ifconfig': (
1243 nic.pretty_ip(nic.ifconfig[0]),
1244 nic.pretty_ip(nic.ifconfig[1]),
1245 nic.pretty_ip(nic.ifconfig[2]),
1246 nic.pretty_ip(nic.ifconfig[3]),
1247 ),
1248 'mac': binascii.hexlify(nic.mac_address, ':').decode(),
1249 'chip': nic.chip,
1250 'max_sockets': nic.max_sockets,
1251 }}
1252 print(json.dumps(res))
1253
1254 ethernet_status()
1255 del ethernet_status, w5x00_init
1256 """.format(
1257 WiznetUtilities.cpyWiznetInit()
1258 )
1259
1260 out, err = self._interface.execute(
1261 command, mode=self._submitMode, timeout=10000
1262 )
1263 if err:
1264 raise OSError(self._shortError(err))
1265
1266 status = []
1267 ethStatus = json.loads(out.decode("utf-8"))
1268 status.append((self.tr("Active"), self.bool2str(ethStatus["active"])))
1269 status.append((self.tr("Connected"), self.bool2str(ethStatus["connected"])))
1270 status.append((self.tr("IPv4 Address"), ethStatus["ifconfig"][0]))
1271 status.append((self.tr("Netmask"), ethStatus["ifconfig"][1]))
1272 status.append((self.tr("Gateway"), ethStatus["ifconfig"][2]))
1273 status.append((self.tr("DNS"), ethStatus["ifconfig"][3]))
1274 status.append((self.tr("MAC-Address"), ethStatus["mac"]))
1275 status.append((self.tr("Chip Type"), ethStatus["chip"]))
1276 status.append((self.tr("max. Sockets"), ethStatus["max_sockets"]))
1277
1278 return status
1279
1280 def connectToLan(self, config):
1281 """
1282 Public method to connect the connected device to the LAN.
1283
1284 Note: The MAC address of the interface is configured with the WIZ
1285
1286 @param config configuration for the connection (either the string 'dhcp'
1287 for a dynamic address or a tuple of four strings with the IPv4 parameters.
1288 @type str or tuple of (str, str, str, str)
1289 @return tuple containing a flag indicating success and an error message
1290 @rtype tuple of (bool, str)
1291 """
1292 command = """{0}
1293 def connect_lan(config):
1294 from adafruit_wiznet5k import adafruit_wiznet5k
1295
1296 w5x00_init()
1297
1298 nic.mac_address = adafruit_wiznet5k._DEFAULT_MAC
1299 if config == 'dhcp':
1300 nic.set_dhcp(response_timeout=14)
1301 else:
1302 nic.ifconfig = (
1303 nic.unpretty_ip(config[0]),
1304 nic.unpretty_ip(config[1]),
1305 nic.unpretty_ip(config[2]),
1306 tuple(int(a) for a in config[3].split('.')),
1307 )
1308 print(nic.ifconfig[0] != b'\x00\x00\x00\x00')
1309
1310 connect_lan({1})
1311 del connect_lan, w5x00_init
1312 """.format(
1313 WiznetUtilities.cpyWiznetInit(), "'dhcp'" if config == "dhcp" else config
1314 )
1315
1316 with EricOverrideCursor():
1317 out, err = self._interface.execute(
1318 command, mode=self._submitMode, timeout=15000
1319 )
1320 if err:
1321 return False, err
1322
1323 return out.strip() == b"True", ""
1324
1325 def disconnectFromLan(self):
1326 """
1327 Public method to disconnect from the LAN.
1328
1329 @return tuple containing a flag indicating success and an error message
1330 @rtype tuple of (bool, str)
1331 """
1332 command = """{0}
1333 def disconnect_lan():
1334 import time
1335
1336 w5x00_init()
1337
1338 nic.sw_reset()
1339 time.sleep(1)
1340 print(nic.ifconfig[0] == b'\x00\x00\x00\x00')
1341
1342 disconnect_lan()
1343 del disconnect_lan, w5x00_init
1344 """.format(
1345 WiznetUtilities.cpyWiznetInit(),
1346 )
1347
1348 with EricOverrideCursor():
1349 out, err = self._interface.execute(
1350 command, mode=self._submitMode, timeout=15000
1351 )
1352 if err:
1353 return False, err
1354
1355 return out.strip() == b"True", ""
1356
1357 def checkInternetViaLan(self):
1358 """
1359 Public method to check, if the internet can be reached (LAN variant).
1360
1361 @return tuple containing a flag indicating reachability and an error string
1362 @rtype tuple of (bool, str)
1363 """
1364 command = """{0}
1365 def check_internet():
1366 w5x00_init()
1367
1368 if nic.ifconfig[0] != b'\x00\x00\x00\x00':
1369 sock = nic.get_socket()
1370 try:
1371 nic.socket_connect(sock, nic.get_host_by_name('quad9.net'), 80)
1372 nic.socket_disconnect(sock)
1373 print(True)
1374 except:
1375 print(False)
1376 nic.socket_close(sock)
1377 else:
1378 print(False)
1379
1380 check_internet()
1381 del check_internet, w5x00_init
1382 """.format(
1383 WiznetUtilities.cpyWiznetInit(),
1384 )
1385
1386 out, err = self._interface.execute(
1387 command, mode=self._submitMode, timeout=15000
1388 )
1389 if err:
1390 return False, err
1391
1392 return out.strip() == b"True", ""
1393
1394 def deactivateEthernet(self):
1395 """
1396 Public method to deactivate the Ethernet interface of the connected device.
1397
1398 @return tuple containg a flag indicating success and an error message
1399 @rtype tuple of (bool, str)
1400 """
1401 # The WIZnet 5x00 interface cannot be switched off explicitly. That means,
1402 # disconnect from the LAN is all we can do.
1403
1404 return self.disconnectFromLan()
1405
1406 def writeLanAutoConnect(self, config):
1407 """
1408 Public method to generate a script and associated configuration to connect the
1409 device to the LAN during boot time.
1410
1411 @param config configuration for the connection (either the string 'dhcp'
1412 for a dynamic address or a tuple of four strings with the IPv4 parameters.
1413 @type str or tuple of (str, str, str, str)
1414 @return tuple containing a flag indicating success and an error message
1415 @rtype tuple of (bool, str)
1416 """
1417 if not self.__deviceVolumeMounted():
1418 return False, self.tr("The device volume is not available.")
1419
1420 workspace = self.getWorkspace()
1421
1422 if Globals.versionToTuple(self._deviceData["release"]) >= (8, 0, 0):
1423 # CircuitPython >= 8.0.0: generate 'settings.toml' file
1424 newConfig = (
1425 {
1426 "WIZNET_IFCONFIG_0": '"dhcp"',
1427 "WIZNET_IFCONFIG_1": "",
1428 "WIZNET_IFCONFIG_2": "",
1429 "WIZNET_IFCONFIG_3": "",
1430 }
1431 if config == "dhcp"
1432 else {
1433 "WIZNET_IFCONFIG_0": '"{0}"'.format(config[0]),
1434 "WIZNET_IFCONFIG_1": '"{0}"'.format(config[1]),
1435 "WIZNET_IFCONFIG_2": '"{0}"'.format(config[2]),
1436 "WIZNET_IFCONFIG_3": '"{0}"'.format(config[3]),
1437 }
1438 )
1439 ok, err = self.__modifySettings(newConfig)
1440 if not ok:
1441 return False, err
1442
1443 scriptFile = os.path.join(
1444 os.path.dirname(__file__), "MCUScripts", "picoWiznetConnectCpy8.py"
1445 )
1446
1447 else:
1448 # step 1: generate the wiznet_config.py file
1449 ifconfig = "ifconfig = {0}\n".format(
1450 "'dhcp'" if config == "dhcp" else config
1451 )
1452 filename = os.path.join(workspace, "wiznet_config.py")
1453 if os.path.exists(filename):
1454 ok = EricMessageBox.yesNo(
1455 None,
1456 self.tr("Write Connect Script"),
1457 self.tr(
1458 """<p>The file <b>{0}</b> exists already. Shall it be"""
1459 """ replaced?</p>"""
1460 ).format(filename),
1461 icon=EricMessageBox.Warning,
1462 )
1463 if not ok:
1464 return False, self.tr("Aborted")
1465 try:
1466 with open(filename, "w") as f:
1467 f.write(ifconfig)
1468 except OSError as err:
1469 return False, str(err)
1470
1471 scriptFile = os.path.join(
1472 os.path.dirname(__file__), "MCUScripts", "picoWiznetConnectCpy7.py"
1473 )
1474
1475 # step 2: create the auto-connect script (wiznet_connect.py)
1476 targetFile = os.path.join(workspace, "wiznet_connect.py")
1477 try:
1478 shutil.copy2(scriptFile, targetFile)
1479 except OSError as err:
1480 return False, str(err)
1481 # Note: code.py will not be modified because the connection will be
1482 # reset anyway
1483 return True, ""
1484
1485 def removeLanAutoConnect(self):
1486 """
1487 Public method to remove the saved IPv4 parameters from the connected device.
1488
1489 Note: This disables the LAN auto-connect feature.
1490
1491 @return tuple containing a flag indicating success and an error message
1492 @rtype tuple of (bool, str)
1493 """
1494 if not self.__deviceVolumeMounted():
1495 return False, self.tr("The device volume is not available.")
1496
1497 workspace = self.getWorkspace()
1498
1499 if Globals.versionToTuple(self._deviceData["release"]) >= (8, 0, 0):
1500 # CircuitPython >= 8.0.0: generate 'settings.toml' file
1501 newConfig = {
1502 "WIZNET_IFCONFIG_0": "",
1503 "WIZNET_IFCONFIG_1": "",
1504 "WIZNET_IFCONFIG_2": "",
1505 "WIZNET_IFCONFIG_3": "",
1506 }
1507 self.__modifySettings(newConfig)
1508
1509 for name in ("wiznet_config.py", "wiznet_connect.py"):
1510 filename = os.path.join(workspace, name)
1511 if os.path.exists(filename):
1512 os.remove(filename)
1513
1514 return True, ""
1515 # TODO: not implemented yet
1516
1517 ##################################################################
1153 ## Methods below implement Bluetooth related methods 1518 ## Methods below implement Bluetooth related methods
1154 ################################################################## 1519 ##################################################################
1155 1520
1156 def hasBluetooth(self): 1521 def hasBluetooth(self):
1157 """ 1522 """
1385 if hasattr(adafruit_ntp, 'NTP'): 1750 if hasattr(adafruit_ntp, 'NTP'):
1386 return True 1751 return True
1387 except ImportError: 1752 except ImportError:
1388 pass 1753 pass
1389 1754
1755 try:
1756 from adafruit_wiznet5k import adafruit_wiznet5k_ntp
1757 if hasattr(adafruit_wiznet5k_ntp, 'NTP'):
1758 return True
1759 except ImportError:
1760 pass
1761
1390 return False 1762 return False
1391 1763
1392 print(has_ntp()) 1764 print(has_ntp())
1393 del has_ntp 1765 del has_ntp
1394 """ 1766 """
1411 (defaults to 10) 1783 (defaults to 10)
1412 @type int 1784 @type int
1413 @return tuple containing a flag indicating success and an error string 1785 @return tuple containing a flag indicating success and an error string
1414 @rtype tuple of (bool, str) 1786 @rtype tuple of (bool, str)
1415 """ 1787 """
1416 command = """ 1788 if self.getDeviceData("ethernet"):
1789 # WIZnet 5x00 Ethernet interface
1790 # Note: The Adafruit NTP implementation does not close the socket after
1791 # calling get_time(). That causes follow-on calls to fail. We
1792 # close the socket in our code as a workaround.
1793 command = """{0}
1794 def set_ntp_time(server, tz_offset):
1795 import rtc
1796
1797 from adafruit_wiznet5k import adafruit_wiznet5k_ntp
1798
1799 w5x00_init()
1800
1801 server_ip = nic.pretty_ip(nic.get_host_by_name(server))
1802 ntp = adafruit_wiznet5k_ntp.NTP(iface=nic, ntp_address=server_ip, utc=tz_offset)
1803 rtc.RTC().datetime = ntp.get_time()
1804 ntp._sock.close()
1805 return True
1806
1807 try:
1808 print({{
1809 'result': set_ntp_time({1}, {2}),
1810 'error': '',
1811 }})
1812 except Exception as err:
1813 print({{
1814 'result': False,
1815 'error': str(err),
1816 }})
1817 del set_ntp_time, w5x00_init
1818 """.format(
1819 WiznetUtilities.cpyWiznetInit(), repr(server), tzOffset
1820 )
1821
1822 elif self.getDeviceData("wifi"):
1823 # WiFi enabled board
1824 command = """
1417 def set_ntp_time(server, tz_offset, timeout): 1825 def set_ntp_time(server, tz_offset, timeout):
1418 import rtc 1826 import rtc
1419 import socketpool 1827 import socketpool
1420 import wifi 1828 import wifi
1421 1829
1443 'result': False, 1851 'result': False,
1444 'error': str(err), 1852 'error': str(err),
1445 }}) 1853 }})
1446 del set_ntp_time 1854 del set_ntp_time
1447 """.format( 1855 """.format(
1448 repr(server), tzOffset, timeout 1856 repr(server), tzOffset, timeout
1449 ) 1857 )
1858
1450 out, err = self._interface.execute( 1859 out, err = self._interface.execute(
1451 command, mode=self._submitMode, timeout=(timeout + 2) * 1000 1860 command, mode=self._submitMode, timeout=(timeout + 2) * 1000
1452 ) 1861 )
1453 if err: 1862 if err:
1454 return False, err 1863 return False, err
1455 else: 1864 else:
1456 res = ast.literal_eval(out.decode("utf-8")) 1865 res = ast.literal_eval(out.decode("utf-8"))
1457 return res["result"], res["error"] 1866 return res["result"], res["error"]
1867
1868 ##################################################################
1869 ## Methods below implement some utility methods
1870 ##################################################################
1871
1872 def __modifySettings(self, changedEntries):
1873 """
1874 Private method to modify the 'settings.toml' file as of CircuitPython 8.0.0.
1875
1876 @param changedEntries dictionary containing the TOML entries to be changed
1877 @type dict of {str: str}
1878 @return tuple containing a success flag and an error message
1879 @rtype tuple of (bool, str)
1880 """
1881 workspace = self.getWorkspace()
1882 filename = os.path.join(workspace, "settings.toml")
1883 if os.path.exists(filename):
1884 try:
1885 with open(filename, "r") as f:
1886 lines = f.read().splitlines()
1887 except OSError as err:
1888 return False, str(err)
1889 else:
1890 lines = []
1891
1892 for key, value in changedEntries.items():
1893 newLine = "{0} = {1}".format(key, value)
1894 for row in range(len(lines)):
1895 if lines[row].split("=")[0].strip() == key:
1896 if value == "":
1897 del lines[row]
1898 else:
1899 lines[row] = newLine
1900 break
1901 else:
1902 if value != "":
1903 lines.append(newLine)
1904
1905 try:
1906 with open(filename, "w") as f:
1907 f.write("\n".join(lines))
1908 except OSError as err:
1909 return False, str(err)
1910
1911 return True, ""
1458 1912
1459 1913
1460 def createDevice(microPythonWidget, deviceType, vid, pid, boardName, serialNumber): 1914 def createDevice(microPythonWidget, deviceType, vid, pid, boardName, serialNumber):
1461 """ 1915 """
1462 Function to instantiate a MicroPython device object. 1916 Function to instantiate a MicroPython device object.
1474 @param serialNumber serial number of the board 1928 @param serialNumber serial number of the board
1475 @type str 1929 @type str
1476 @return reference to the instantiated device object 1930 @return reference to the instantiated device object
1477 @rtype CircuitPythonDevice 1931 @rtype CircuitPythonDevice
1478 """ 1932 """
1479 return CircuitPythonDevice(microPythonWidget, deviceType, boardName) 1933 return CircuitPythonDevice(
1934 microPythonWidget, deviceType, boardName, vid=vid, pid=pid
1935 )

eric ide

mercurial