Fri, 06 Oct 2023 15:52:33 +0200
Extended the MicroPython code to give an indication, why the connection to a device failed.
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 | 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 | ||
10069
435cc5875135
Corrected and checked some code style issues (unused function arguments).
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10037
diff
changeset
|
171 | def execute(self, commands, *, mode="raw", timeout=0): # noqa: U100 |
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 | |
179 | 'raw'). This is ignored because webrepl always uses 'paste' mode. | |
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 |
91b0939626ff
Optimized the MicroPython execute() functions.
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10019
diff
changeset
|
228 | if result.startswith(b"\x1b]0;"): |
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 | ||
10069
435cc5875135
Corrected and checked some code style issues (unused function arguments).
Detlev Offenbach <detlev@die-offenbachs.de>
parents:
10037
diff
changeset
|
242 | def executeAsync(self, commandsList, submitMode): # noqa: U100 |
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 | |
249 | @param submitMode mode to be used to submit the commands | |
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 |