Sat, 08 Jul 2023 16:38:40 +0200
MicroPython
- Updated the list of known CircuitPython boards.
- Updated the list of known UF2 capable boards.
10008 | 1 | # -*- coding: utf-8 -*- |
2 | ||
3 | # Copyright (c) 2023 Detlev Offenbach <detlev@die-offenbachs.de> | |
4 | # | |
5 | ||
6 | """ | |
7 | Module implementing an interface to talk to a connected MicroPython device via | |
8 | a webrepl connection. | |
9 | """ | |
10 | ||
11 | from PyQt6.QtCore import QThread, pyqtSlot | |
12 | from PyQt6.QtWidgets import QInputDialog, QLineEdit | |
13 | ||
14 | from eric7 import Preferences | |
15 | from eric7.EricWidgets import EricMessageBox | |
16 | ||
17 | from .MicroPythonDeviceInterface import MicroPythonDeviceInterface | |
18 | from .MicroPythonWebreplSocket import MicroPythonWebreplSocket | |
19 | ||
20 | ||
21 | class MicroPythonWebreplDeviceInterface(MicroPythonDeviceInterface): | |
22 | """ | |
23 | Class implementing an interface to talk to a connected MicroPython device via | |
24 | a webrepl connection. | |
25 | """ | |
26 | ||
27 | def __init__(self, parent=None): | |
28 | """ | |
29 | Constructor | |
30 | ||
31 | @param parent reference to the parent object | |
32 | @type QObject | |
33 | """ | |
34 | super().__init__(parent) | |
35 | ||
36 | self.__blockReadyRead = False | |
37 | ||
38 | self.__socket = MicroPythonWebreplSocket( | |
39 | timeout=Preferences.getMicroPython("WebreplTimeout"), parent=self | |
40 | ) | |
41 | self.__connected = False | |
42 | self.__socket.readyRead.connect(self.__readSocket) | |
43 | ||
44 | @pyqtSlot() | |
45 | def __readSocket(self): | |
46 | """ | |
47 | Private slot to read all available data and emit it with the | |
48 | "dataReceived" signal for further processing. | |
49 | """ | |
50 | if not self.__blockReadyRead: | |
51 | data = bytes(self.__socket.readAll()) | |
52 | self.dataReceived.emit(data) | |
53 | ||
54 | def __readAll(self): | |
55 | """ | |
56 | Private method to read all data and emit it for further processing. | |
57 | """ | |
58 | data = self.__socket.readAll() | |
59 | self.dataReceived.emit(data) | |
60 | ||
61 | @pyqtSlot() | |
62 | def connectToDevice(self, connection): | |
63 | """ | |
64 | Public slot to connect to the device. | |
65 | ||
66 | @param connection name of the connection to be used in the form of an URL string | |
67 | (ws://password@host:port) | |
68 | @type str | |
69 | @return flag indicating success | |
70 | @rtype bool | |
71 | """ | |
72 | connection = connection.replace("ws://", "") | |
73 | try: | |
74 | password, hostPort = connection.split("@", 1) | |
75 | except ValueError: | |
76 | password, hostPort = None, connection | |
77 | if password is None: | |
78 | password, ok = QInputDialog.getText( | |
79 | None, | |
10016
8db27a64d434
Updated translations.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10012
diff
changeset
|
80 | self.tr("WebREPL Password"), |
8db27a64d434
Updated translations.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10012
diff
changeset
|
81 | self.tr("Enter the WebREPL password:"), |
10008 | 82 | QLineEdit.EchoMode.Password, |
83 | ) | |
84 | if not ok: | |
85 | return False | |
86 | ||
87 | try: | |
88 | host, port = hostPort.split(":", 1) | |
89 | port = int(port) | |
90 | except ValueError: | |
91 | host, port = hostPort, 8266 # default port is 8266 | |
92 | ||
93 | self.__blockReadyRead = True | |
94 | ok = self.__socket.connectToDevice(host, port) | |
95 | if ok: | |
96 | ok = self.__socket.login(password) | |
97 | if not ok: | |
98 | EricMessageBox.warning( | |
99 | None, | |
10016
8db27a64d434
Updated translations.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10012
diff
changeset
|
100 | self.tr("WebREPL Login"), |
10008 | 101 | self.tr( |
102 | "The login to the selected device 'webrepl' failed. The given" | |
103 | " password may be incorrect." | |
104 | ), | |
105 | ) | |
106 | ||
107 | self.__connected = ok | |
108 | self.__blockReadyRead = False | |
109 | ||
110 | return self.__connected | |
111 | ||
112 | @pyqtSlot() | |
113 | def disconnectFromDevice(self): | |
114 | """ | |
115 | Public slot to disconnect from the device. | |
116 | """ | |
117 | self.__socket.disconnect() | |
118 | self.__connected = False | |
119 | ||
120 | def isConnected(self): | |
121 | """ | |
122 | Public method to get the connection status. | |
123 | ||
124 | @return flag indicating the connection status | |
125 | @rtype bool | |
126 | """ | |
127 | return self.__connected | |
128 | ||
129 | @pyqtSlot() | |
130 | def handlePreferencesChanged(self): | |
131 | """ | |
132 | Public slot to handle a change of the preferences. | |
133 | """ | |
134 | self.__socket.setTimeout(Preferences.getMicroPython("WebreplTimeout")) | |
135 | ||
136 | def write(self, data): | |
137 | """ | |
138 | Public method to write data to the connected device. | |
139 | ||
140 | @param data data to be written | |
141 | @type bytes or bytearray | |
142 | """ | |
143 | self.__connected and self.__socket.writeTextMessage(data) | |
144 | ||
145 | def probeDevice(self): | |
146 | """ | |
147 | Public method to check the device is responding. | |
148 | ||
149 | If the device has not been flashed with a MicroPython firmware, the | |
150 | probe will fail. | |
151 | ||
152 | @return flag indicating a communicating MicroPython device | |
153 | @rtype bool | |
154 | """ | |
155 | if not self.__connected: | |
156 | return False | |
157 | ||
158 | # switch on paste mode | |
159 | self.__blockReadyRead = True | |
160 | ok = self.__pasteOn() | |
161 | if not ok: | |
162 | self.__blockReadyRead = False | |
163 | return False | |
164 | ||
165 | # switch off raw mode | |
166 | QThread.msleep(10) | |
167 | self.__pasteOff() | |
168 | self.__blockReadyRead = False | |
169 | ||
170 | return True | |
171 | ||
10069
435cc5875135
Corrected and checked some code style issues (unused function arguments).
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10037
diff
changeset
|
172 | def execute(self, commands, *, mode="raw", timeout=0): # noqa: U100 |
10008 | 173 | """ |
174 | Public method to send commands to the connected device and return the | |
175 | result. | |
176 | ||
177 | @param commands list of commands to be executed | |
178 | @type str or list of str | |
179 | @keyparam mode submit mode to be used (one of 'raw' or 'paste') (defaults to | |
180 | 'raw'). This is ignored because webrepl always uses 'paste' mode. | |
181 | @type str | |
182 | @keyparam timeout per command timeout in milliseconds (0 for configured default) | |
183 | (defaults to 0) | |
184 | @type int (optional) | |
185 | @return tuple containing stdout and stderr output of the device | |
186 | @rtype tuple of (bytes, bytes) | |
187 | """ | |
188 | if not self.__connected: | |
189 | return b"", b"Device is not connected." | |
190 | ||
191 | if isinstance(commands, list): | |
192 | commands = "\n".join(commands) | |
193 | ||
194 | # switch on paste mode | |
195 | self.__blockReadyRead = True | |
196 | ok = self.__pasteOn() | |
197 | if not ok: | |
198 | self.__blockReadyRead = False | |
199 | return (b"", b"Could not switch to paste mode. Is the device switched on?") | |
200 | ||
201 | # send commands | |
10019
e56089d00750
Fixed a few issue in the MicroPython support related to behavior of devices and change caused by MicroPython release 1.20.0 on ESP32 devices.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10016
diff
changeset
|
202 | for command in commands.splitlines(keepends=True): |
e56089d00750
Fixed a few issue in the MicroPython support related to behavior of devices and change caused by MicroPython release 1.20.0 on ESP32 devices.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10016
diff
changeset
|
203 | # send the data as single lines |
e56089d00750
Fixed a few issue in the MicroPython support related to behavior of devices and change caused by MicroPython release 1.20.0 on ESP32 devices.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10016
diff
changeset
|
204 | commandBytes = command.encode("utf-8") |
e56089d00750
Fixed a few issue in the MicroPython support related to behavior of devices and change caused by MicroPython release 1.20.0 on ESP32 devices.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10016
diff
changeset
|
205 | self.__socket.writeTextMessage(commandBytes) |
e56089d00750
Fixed a few issue in the MicroPython support related to behavior of devices and change caused by MicroPython release 1.20.0 on ESP32 devices.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10016
diff
changeset
|
206 | ok = self.__socket.readUntil(commandBytes) |
e56089d00750
Fixed a few issue in the MicroPython support related to behavior of devices and change caused by MicroPython release 1.20.0 on ESP32 devices.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10016
diff
changeset
|
207 | if ok != commandBytes: |
e56089d00750
Fixed a few issue in the MicroPython support related to behavior of devices and change caused by MicroPython release 1.20.0 on ESP32 devices.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10016
diff
changeset
|
208 | self.__blockReadyRead = False |
e56089d00750
Fixed a few issue in the MicroPython support related to behavior of devices and change caused by MicroPython release 1.20.0 on ESP32 devices.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10016
diff
changeset
|
209 | return ( |
e56089d00750
Fixed a few issue in the MicroPython support related to behavior of devices and change caused by MicroPython release 1.20.0 on ESP32 devices.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10016
diff
changeset
|
210 | b"", |
e56089d00750
Fixed a few issue in the MicroPython support related to behavior of devices and change caused by MicroPython release 1.20.0 on ESP32 devices.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10016
diff
changeset
|
211 | "Expected '{0}', got '{1}', followed by '{2}'".format( |
e56089d00750
Fixed a few issue in the MicroPython support related to behavior of devices and change caused by MicroPython release 1.20.0 on ESP32 devices.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10016
diff
changeset
|
212 | commandBytes, ok, self.__socket.readAll() |
e56089d00750
Fixed a few issue in the MicroPython support related to behavior of devices and change caused by MicroPython release 1.20.0 on ESP32 devices.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10016
diff
changeset
|
213 | ).encode("utf-8"), |
e56089d00750
Fixed a few issue in the MicroPython support related to behavior of devices and change caused by MicroPython release 1.20.0 on ESP32 devices.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10016
diff
changeset
|
214 | ) |
10008 | 215 | |
216 | # switch off paste mode causing the commands to be executed | |
217 | self.__pasteOff() | |
218 | ||
219 | # read until Python prompt | |
220 | result = ( | |
221 | self.__socket.readUntil(b">>> ", timeout=timeout) | |
222 | .replace(b">>> ", b"") | |
223 | .strip() | |
224 | ) | |
225 | if self.__socket.hasTimedOut(): | |
10037
e5d8dbcae771
Corrected a code formatting issue.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10033
diff
changeset
|
226 | out, err = b"", b"Timeout while processing commands." |
10033
91b0939626ff
Optimized the MicroPython execute() functions.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10019
diff
changeset
|
227 | else: |
91b0939626ff
Optimized the MicroPython execute() functions.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10019
diff
changeset
|
228 | # get rid of any OSD string and send it |
91b0939626ff
Optimized the MicroPython execute() functions.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10019
diff
changeset
|
229 | if result.startswith(b"\x1b]0;"): |
91b0939626ff
Optimized the MicroPython execute() functions.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10019
diff
changeset
|
230 | osd, result = result.split(b"\x1b\\", 1) |
91b0939626ff
Optimized the MicroPython execute() functions.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10019
diff
changeset
|
231 | self.osdInfo.emit(osd[4:].decode("utf-8")) |
10008 | 232 | |
10033
91b0939626ff
Optimized the MicroPython execute() functions.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10019
diff
changeset
|
233 | if self.TracebackMarker in result: |
91b0939626ff
Optimized the MicroPython execute() functions.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10019
diff
changeset
|
234 | errorIndex = result.find(self.TracebackMarker) |
91b0939626ff
Optimized the MicroPython execute() functions.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10019
diff
changeset
|
235 | out, err = result[:errorIndex], result[errorIndex:].replace(">>> ", "") |
91b0939626ff
Optimized the MicroPython execute() functions.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10019
diff
changeset
|
236 | else: |
91b0939626ff
Optimized the MicroPython execute() functions.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10019
diff
changeset
|
237 | out = result |
91b0939626ff
Optimized the MicroPython execute() functions.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10019
diff
changeset
|
238 | err = b"" |
10008 | 239 | |
240 | self.__blockReadyRead = False | |
241 | return out, err | |
242 | ||
10069
435cc5875135
Corrected and checked some code style issues (unused function arguments).
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10037
diff
changeset
|
243 | def executeAsync(self, commandsList, submitMode): # noqa: U100 |
10008 | 244 | """ |
245 | Public method to execute a series of commands over a period of time | |
246 | without returning any result (asynchronous execution). | |
247 | ||
248 | @param commandsList list of commands to be execute on the device | |
249 | @type list of str | |
250 | @param submitMode mode to be used to submit the commands | |
251 | @type str (one of 'raw' or 'paste') | |
252 | """ | |
253 | self.__blockReadyRead = True | |
254 | self.__pasteOn() | |
255 | command = b"\n".join(c.encode("utf-8)") for c in commandsList) | |
256 | self.__socket.writeTextMessage(command) | |
257 | self.__socket.readUntil(command) | |
258 | self.__blockReadyRead = False | |
259 | self.__pasteOff() | |
260 | self.executeAsyncFinished.emit() | |
261 | ||
262 | def __pasteOn(self): | |
263 | """ | |
264 | Private method to switch the connected device to 'paste' mode. | |
265 | ||
266 | Note: switching to paste mode is done with synchronous writes. | |
267 | ||
268 | @return flag indicating success | |
269 | @rtype bool | |
270 | """ | |
271 | if not self.__connected: | |
272 | return False | |
273 | ||
274 | pasteMessage = b"paste mode; Ctrl-C to cancel, Ctrl-D to finish\r\n=== " | |
275 | ||
276 | self.__socket.writeTextMessage(b"\x02") # end raw mode if required | |
277 | for _i in range(3): | |
278 | # CTRL-C three times to break out of loops | |
279 | self.__socket.writeTextMessage(b"\r\x03") | |
280 | # time out after 500ms if device is not responding | |
281 | self.__socket.readAll() # read all data and discard it | |
282 | self.__socket.writeTextMessage(b"\r\x05") # send CTRL-E to enter paste mode | |
283 | self.__socket.readUntil(pasteMessage) | |
284 | ||
285 | if self.__socket.hasTimedOut(): | |
286 | # it timed out; try it again and than fail | |
287 | self.__socket.writeTextMessage(b"\r\x05") # send CTRL-E again | |
288 | self.__socket.readUntil(pasteMessage) | |
289 | if self.__socket.hasTimedOut(): | |
290 | return False | |
291 | ||
292 | self.__socket.readAll() # read all data and discard it | |
293 | return True | |
294 | ||
295 | def __pasteOff(self): | |
296 | """ | |
297 | Private method to switch 'paste' mode off. | |
298 | """ | |
299 | if self.__connected: | |
300 | self.__socket.writeTextMessage(b"\x04") # send CTRL-D to cancel paste mode |