Sun, 16 Mar 2025 12:53:12 +0100
Added the Adafruit Feather nRF52840 to the list of known NRF52 boards and changed the list of known CircuitPython boards to be more explicit with respect to Adafruit boards (i.e. VID 0x239A).
10008 | 1 | # -*- coding: utf-8 -*- |
2 | ||
11090
f5f5f5803935
Updated copyright for 2025.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10683
diff
changeset
|
3 | # Copyright (c) 2023 - 2025 Detlev Offenbach <detlev@die-offenbachs.de> |
10008 | 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 | def connectToDevice(self, connection): | |
62 | """ | |
10229
e50bbf250343
Extended the MicroPython code to give an indication, why the connection to a device failed.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10069
diff
changeset
|
63 | Public method to connect to the device. |
10008 | 64 | |
65 | @param connection name of the connection to be used in the form of an URL string | |
66 | (ws://password@host:port) | |
67 | @type str | |
10229
e50bbf250343
Extended the MicroPython code to give an indication, why the connection to a device failed.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10069
diff
changeset
|
68 | @return flag indicating success and an error message |
e50bbf250343
Extended the MicroPython code to give an indication, why the connection to a device failed.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10069
diff
changeset
|
69 | @rtype tuple of (bool, str) |
10008 | 70 | """ |
71 | connection = connection.replace("ws://", "") | |
72 | try: | |
73 | password, hostPort = connection.split("@", 1) | |
74 | except ValueError: | |
75 | password, hostPort = None, connection | |
76 | if password is None: | |
77 | password, ok = QInputDialog.getText( | |
78 | None, | |
10016
8db27a64d434
Updated translations.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10012
diff
changeset
|
79 | self.tr("WebREPL Password"), |
8db27a64d434
Updated translations.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10012
diff
changeset
|
80 | self.tr("Enter the WebREPL password:"), |
10008 | 81 | QLineEdit.EchoMode.Password, |
82 | ) | |
83 | if not ok: | |
10229
e50bbf250343
Extended the MicroPython code to give an indication, why the connection to a device failed.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10069
diff
changeset
|
84 | return False, self.tr("No password given") |
10008 | 85 | |
86 | try: | |
87 | host, port = hostPort.split(":", 1) | |
88 | port = int(port) | |
89 | except ValueError: | |
90 | host, port = hostPort, 8266 # default port is 8266 | |
91 | ||
92 | self.__blockReadyRead = True | |
10229
e50bbf250343
Extended the MicroPython code to give an indication, why the connection to a device failed.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10069
diff
changeset
|
93 | ok, error = self.__socket.connectToDevice(host, port) |
10008 | 94 | if ok: |
10229
e50bbf250343
Extended the MicroPython code to give an indication, why the connection to a device failed.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10069
diff
changeset
|
95 | ok, error = self.__socket.login(password) |
10008 | 96 | if not ok: |
97 | EricMessageBox.warning( | |
98 | None, | |
10016
8db27a64d434
Updated translations.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10012
diff
changeset
|
99 | self.tr("WebREPL Login"), |
10008 | 100 | self.tr( |
101 | "The login to the selected device 'webrepl' failed. The given" | |
102 | " password may be incorrect." | |
103 | ), | |
104 | ) | |
105 | ||
106 | self.__connected = ok | |
107 | self.__blockReadyRead = False | |
108 | ||
10229
e50bbf250343
Extended the MicroPython code to give an indication, why the connection to a device failed.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10069
diff
changeset
|
109 | return self.__connected, error |
10008 | 110 | |
111 | @pyqtSlot() | |
112 | def disconnectFromDevice(self): | |
113 | """ | |
114 | Public slot to disconnect from the device. | |
115 | """ | |
116 | self.__socket.disconnect() | |
117 | self.__connected = False | |
118 | ||
119 | def isConnected(self): | |
120 | """ | |
121 | Public method to get the connection status. | |
122 | ||
123 | @return flag indicating the connection status | |
124 | @rtype bool | |
125 | """ | |
126 | return self.__connected | |
127 | ||
128 | @pyqtSlot() | |
129 | def handlePreferencesChanged(self): | |
130 | """ | |
131 | Public slot to handle a change of the preferences. | |
132 | """ | |
133 | self.__socket.setTimeout(Preferences.getMicroPython("WebreplTimeout")) | |
134 | ||
135 | def write(self, data): | |
136 | """ | |
137 | Public method to write data to the connected device. | |
138 | ||
139 | @param data data to be written | |
140 | @type bytes or bytearray | |
141 | """ | |
142 | self.__connected and self.__socket.writeTextMessage(data) | |
143 | ||
144 | def probeDevice(self): | |
145 | """ | |
146 | Public method to check the device is responding. | |
147 | ||
148 | If the device has not been flashed with a MicroPython firmware, the | |
149 | probe will fail. | |
150 | ||
151 | @return flag indicating a communicating MicroPython device | |
152 | @rtype bool | |
153 | """ | |
154 | if not self.__connected: | |
155 | return False | |
156 | ||
157 | # switch on paste mode | |
158 | self.__blockReadyRead = True | |
159 | ok = self.__pasteOn() | |
160 | if not ok: | |
161 | self.__blockReadyRead = False | |
162 | return False | |
163 | ||
164 | # switch off raw mode | |
165 | QThread.msleep(10) | |
166 | self.__pasteOff() | |
167 | self.__blockReadyRead = False | |
168 | ||
169 | return True | |
170 | ||
11148
15e30f0c76a8
Adjusted the code to the modified issue codes.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
11090
diff
changeset
|
171 | def execute(self, commands, *, mode="raw", timeout=0): # noqa: U-100 |
10008 | 172 | """ |
173 | Public method to send commands to the connected device and return the | |
174 | result. | |
175 | ||
176 | @param commands list of commands to be executed | |
177 | @type str or list of str | |
178 | @keyparam mode submit mode to be used (one of 'raw' or 'paste') (defaults to | |
10683
779cda568acb
Changed the source code and the source code documentation to improve the indication of unused method/function arguments.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10439
diff
changeset
|
179 | 'raw'). This is ignored because webrepl always uses 'paste' mode. (unused) |
10008 | 180 | @type str |
181 | @keyparam timeout per command timeout in milliseconds (0 for configured default) | |
182 | (defaults to 0) | |
183 | @type int (optional) | |
184 | @return tuple containing stdout and stderr output of the device | |
185 | @rtype tuple of (bytes, bytes) | |
186 | """ | |
187 | if not self.__connected: | |
188 | return b"", b"Device is not connected." | |
189 | ||
190 | if isinstance(commands, list): | |
191 | commands = "\n".join(commands) | |
192 | ||
193 | # switch on paste mode | |
194 | self.__blockReadyRead = True | |
195 | ok = self.__pasteOn() | |
196 | if not ok: | |
197 | self.__blockReadyRead = False | |
198 | return (b"", b"Could not switch to paste mode. Is the device switched on?") | |
199 | ||
200 | # 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
|
201 | 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
|
202 | # 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
|
203 | 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
|
204 | 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
|
205 | 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
|
206 | 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
|
207 | 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
|
208 | 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
|
209 | 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
|
210 | "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
|
211 | 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
|
212 | ).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
|
213 | ) |
10008 | 214 | |
215 | # switch off paste mode causing the commands to be executed | |
216 | self.__pasteOff() | |
217 | ||
218 | # read until Python prompt | |
219 | result = ( | |
220 | self.__socket.readUntil(b">>> ", timeout=timeout) | |
221 | .replace(b">>> ", b"") | |
222 | .strip() | |
223 | ) | |
224 | if self.__socket.hasTimedOut(): | |
10037
e5d8dbcae771
Corrected a code formatting issue.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10033
diff
changeset
|
225 | 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
|
226 | else: |
91b0939626ff
Optimized the MicroPython execute() functions.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10019
diff
changeset
|
227 | # get rid of any OSD string and send it |
10230
1311cd5d117e
MicroPython interface
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10229
diff
changeset
|
228 | while result.startswith(b"\x1b]0;"): |
10033
91b0939626ff
Optimized the MicroPython execute() functions.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10019
diff
changeset
|
229 | osd, result = result.split(b"\x1b\\", 1) |
91b0939626ff
Optimized the MicroPython execute() functions.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10019
diff
changeset
|
230 | self.osdInfo.emit(osd[4:].decode("utf-8")) |
10008 | 231 | |
10033
91b0939626ff
Optimized the MicroPython execute() functions.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10019
diff
changeset
|
232 | if self.TracebackMarker in result: |
91b0939626ff
Optimized the MicroPython execute() functions.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10019
diff
changeset
|
233 | errorIndex = result.find(self.TracebackMarker) |
91b0939626ff
Optimized the MicroPython execute() functions.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10019
diff
changeset
|
234 | out, err = result[:errorIndex], result[errorIndex:].replace(">>> ", "") |
91b0939626ff
Optimized the MicroPython execute() functions.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10019
diff
changeset
|
235 | else: |
91b0939626ff
Optimized the MicroPython execute() functions.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10019
diff
changeset
|
236 | out = result |
91b0939626ff
Optimized the MicroPython execute() functions.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10019
diff
changeset
|
237 | err = b"" |
10008 | 238 | |
239 | self.__blockReadyRead = False | |
240 | return out, err | |
241 | ||
10683
779cda568acb
Changed the source code and the source code documentation to improve the indication of unused method/function arguments.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10439
diff
changeset
|
242 | def executeAsync(self, commandsList, _submitMode): |
10008 | 243 | """ |
244 | Public method to execute a series of commands over a period of time | |
245 | without returning any result (asynchronous execution). | |
246 | ||
247 | @param commandsList list of commands to be execute on the device | |
248 | @type list of str | |
10683
779cda568acb
Changed the source code and the source code documentation to improve the indication of unused method/function arguments.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10439
diff
changeset
|
249 | @param _submitMode mode to be used to submit the commands (unused) |
10008 | 250 | @type str (one of 'raw' or 'paste') |
251 | """ | |
252 | self.__blockReadyRead = True | |
253 | self.__pasteOn() | |
254 | command = b"\n".join(c.encode("utf-8)") for c in commandsList) | |
255 | self.__socket.writeTextMessage(command) | |
256 | self.__socket.readUntil(command) | |
257 | self.__blockReadyRead = False | |
258 | self.__pasteOff() | |
259 | self.executeAsyncFinished.emit() | |
260 | ||
261 | def __pasteOn(self): | |
262 | """ | |
263 | Private method to switch the connected device to 'paste' mode. | |
264 | ||
265 | Note: switching to paste mode is done with synchronous writes. | |
266 | ||
267 | @return flag indicating success | |
268 | @rtype bool | |
269 | """ | |
270 | if not self.__connected: | |
271 | return False | |
272 | ||
273 | pasteMessage = b"paste mode; Ctrl-C to cancel, Ctrl-D to finish\r\n=== " | |
274 | ||
275 | self.__socket.writeTextMessage(b"\x02") # end raw mode if required | |
276 | for _i in range(3): | |
277 | # CTRL-C three times to break out of loops | |
278 | self.__socket.writeTextMessage(b"\r\x03") | |
279 | # time out after 500ms if device is not responding | |
280 | self.__socket.readAll() # read all data and discard it | |
281 | self.__socket.writeTextMessage(b"\r\x05") # send CTRL-E to enter paste mode | |
282 | self.__socket.readUntil(pasteMessage) | |
283 | ||
284 | if self.__socket.hasTimedOut(): | |
285 | # it timed out; try it again and than fail | |
286 | self.__socket.writeTextMessage(b"\r\x05") # send CTRL-E again | |
287 | self.__socket.readUntil(pasteMessage) | |
288 | if self.__socket.hasTimedOut(): | |
289 | return False | |
290 | ||
291 | self.__socket.readAll() # read all data and discard it | |
292 | return True | |
293 | ||
294 | def __pasteOff(self): | |
295 | """ | |
296 | Private method to switch 'paste' mode off. | |
297 | """ | |
298 | if self.__connected: | |
299 | self.__socket.writeTextMessage(b"\x04") # send CTRL-D to cancel paste mode |