|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2019 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing the device interface class for CircuitPython boards. |
|
8 """ |
|
9 |
|
10 import shutil |
|
11 import os |
|
12 |
|
13 from PyQt5.QtCore import pyqtSlot |
|
14 |
|
15 from E5Gui import E5MessageBox, E5FileDialog |
|
16 |
|
17 from .MicroPythonDevices import MicroPythonDevice |
|
18 from .MicroPythonWidget import HAS_QTCHART |
|
19 |
|
20 import Utilities |
|
21 import Preferences |
|
22 |
|
23 |
|
24 class CircuitPythonDevice(MicroPythonDevice): |
|
25 """ |
|
26 Class implementing the device for CircuitPython boards. |
|
27 """ |
|
28 DeviceVolumeName = "CIRCUITPY" |
|
29 |
|
30 def __init__(self, microPythonWidget, deviceType, parent=None): |
|
31 """ |
|
32 Constructor |
|
33 |
|
34 @param microPythonWidget reference to the main MicroPython widget |
|
35 @type MicroPythonWidget |
|
36 @param deviceType device type assigned to this device interface |
|
37 @type str |
|
38 @param parent reference to the parent object |
|
39 @type QObject |
|
40 """ |
|
41 super().__init__( |
|
42 microPythonWidget, deviceType, parent) |
|
43 |
|
44 self.__workspace = self.__findWorkspace() |
|
45 |
|
46 self.__nonUF2devices = { |
|
47 "teensy": self.__flashTeensy, |
|
48 } |
|
49 |
|
50 def setButtons(self): |
|
51 """ |
|
52 Public method to enable the supported action buttons. |
|
53 """ |
|
54 super().setButtons() |
|
55 self.microPython.setActionButtons( |
|
56 run=True, repl=True, files=True, chart=HAS_QTCHART) |
|
57 |
|
58 if self.__deviceVolumeMounted(): |
|
59 self.microPython.setActionButtons(open=True, save=True) |
|
60 |
|
61 def forceInterrupt(self): |
|
62 """ |
|
63 Public method to determine the need for an interrupt when opening the |
|
64 serial connection. |
|
65 |
|
66 @return flag indicating an interrupt is needed |
|
67 @rtype bool |
|
68 """ |
|
69 return False |
|
70 |
|
71 def deviceName(self): |
|
72 """ |
|
73 Public method to get the name of the device. |
|
74 |
|
75 @return name of the device |
|
76 @rtype str |
|
77 """ |
|
78 return self.tr("CircuitPython") |
|
79 |
|
80 def canStartRepl(self): |
|
81 """ |
|
82 Public method to determine, if a REPL can be started. |
|
83 |
|
84 @return tuple containing a flag indicating it is safe to start a REPL |
|
85 and a reason why it cannot. |
|
86 @rtype tuple of (bool, str) |
|
87 """ |
|
88 return True, "" |
|
89 |
|
90 def canStartPlotter(self): |
|
91 """ |
|
92 Public method to determine, if a Plotter can be started. |
|
93 |
|
94 @return tuple containing a flag indicating it is safe to start a |
|
95 Plotter and a reason why it cannot. |
|
96 @rtype tuple of (bool, str) |
|
97 """ |
|
98 return True, "" |
|
99 |
|
100 def canRunScript(self): |
|
101 """ |
|
102 Public method to determine, if a script can be executed. |
|
103 |
|
104 @return tuple containing a flag indicating it is safe to start a |
|
105 Plotter and a reason why it cannot. |
|
106 @rtype tuple of (bool, str) |
|
107 """ |
|
108 return True, "" |
|
109 |
|
110 def runScript(self, script): |
|
111 """ |
|
112 Public method to run the given Python script. |
|
113 |
|
114 @param script script to be executed |
|
115 @type str |
|
116 """ |
|
117 pythonScript = script.split("\n") |
|
118 self.sendCommands(pythonScript) |
|
119 |
|
120 def canStartFileManager(self): |
|
121 """ |
|
122 Public method to determine, if a File Manager can be started. |
|
123 |
|
124 @return tuple containing a flag indicating it is safe to start a |
|
125 File Manager and a reason why it cannot. |
|
126 @rtype tuple of (bool, str) |
|
127 """ |
|
128 return True, "" |
|
129 |
|
130 def supportsLocalFileAccess(self): |
|
131 """ |
|
132 Public method to indicate file access via a local directory. |
|
133 |
|
134 @return flag indicating file access via local directory |
|
135 @rtype bool |
|
136 """ |
|
137 return self.__deviceVolumeMounted() |
|
138 |
|
139 def __deviceVolumeMounted(self): |
|
140 """ |
|
141 Private method to check, if the device volume is mounted. |
|
142 |
|
143 @return flag indicated a mounted device |
|
144 @rtype bool |
|
145 """ |
|
146 if self.__workspace and not os.path.exists(self.__workspace): |
|
147 self.__workspace = "" # reset |
|
148 |
|
149 return self.DeviceVolumeName in self.getWorkspace(silent=True) |
|
150 |
|
151 def getWorkspace(self, silent=False): |
|
152 """ |
|
153 Public method to get the workspace directory. |
|
154 |
|
155 @param silent flag indicating silent operations |
|
156 @type bool |
|
157 @return workspace directory used for saving files |
|
158 @rtype str |
|
159 """ |
|
160 if self.__workspace: |
|
161 # return cached entry |
|
162 return self.__workspace |
|
163 else: |
|
164 self.__workspace = self.__findWorkspace(silent=silent) |
|
165 return self.__workspace |
|
166 |
|
167 def __findWorkspace(self, silent=False): |
|
168 """ |
|
169 Private method to find the workspace directory. |
|
170 |
|
171 @param silent flag indicating silent operations |
|
172 @type bool |
|
173 @return workspace directory used for saving files |
|
174 @rtype str |
|
175 """ |
|
176 # Attempts to find the paths on the filesystem that represents the |
|
177 # plugged in CIRCUITPY boards. |
|
178 deviceDirectories = Utilities.findVolume(self.DeviceVolumeName, |
|
179 findAll=True) |
|
180 |
|
181 if deviceDirectories: |
|
182 if len(deviceDirectories) == 1: |
|
183 return deviceDirectories[0] |
|
184 else: |
|
185 return self.selectDeviceDirectory(deviceDirectories) |
|
186 else: |
|
187 # return the default workspace and give the user a warning (unless |
|
188 # silent mode is selected) |
|
189 if not silent: |
|
190 E5MessageBox.warning( |
|
191 self.microPython, |
|
192 self.tr("Workspace Directory"), |
|
193 self.tr("Python files for CircuitPython can be edited in" |
|
194 " place, if the device volume is locally" |
|
195 " available. Such a volume was not found. In" |
|
196 " place editing will not be available." |
|
197 ) |
|
198 ) |
|
199 |
|
200 return super().getWorkspace() |
|
201 |
|
202 def addDeviceMenuEntries(self, menu): |
|
203 """ |
|
204 Public method to add device specific entries to the given menu. |
|
205 |
|
206 @param menu reference to the context menu |
|
207 @type QMenu |
|
208 """ |
|
209 connected = self.microPython.isConnected() |
|
210 |
|
211 act = menu.addAction(self.tr("Flash CircuitPython Firmware"), |
|
212 self.__flashCircuitPython) |
|
213 act.setEnabled(not connected) |
|
214 menu.addSeparator() |
|
215 act = menu.addAction(self.tr("Install Library Files"), |
|
216 self.__installLibraryFiles) |
|
217 act.setEnabled(self.__deviceVolumeMounted()) |
|
218 |
|
219 def hasFlashMenuEntry(self): |
|
220 """ |
|
221 Public method to check, if the device has its own flash menu entry. |
|
222 |
|
223 @return flag indicating a specific flash menu entry |
|
224 @rtype bool |
|
225 """ |
|
226 return True |
|
227 |
|
228 @pyqtSlot() |
|
229 def __flashCircuitPython(self): |
|
230 """ |
|
231 Private slot to flash a CircuitPython firmware to the device. |
|
232 """ |
|
233 lBoardName = self.microPython.getCurrentBoard().lower() |
|
234 if lBoardName: |
|
235 for name in self.__nonUF2devices: |
|
236 if name in lBoardName: |
|
237 self.__nonUF2devices[name]() |
|
238 break |
|
239 else: |
|
240 from .UF2FlashDialog import UF2FlashDialog |
|
241 dlg = UF2FlashDialog(boardType="circuitpython") |
|
242 dlg.exec() |
|
243 |
|
244 def __flashTeensy(self): |
|
245 """ |
|
246 Private method to show a message box because Teens does not support |
|
247 the UF2 bootloader yet. |
|
248 """ |
|
249 E5MessageBox.information( |
|
250 self.microPython, |
|
251 self.tr("Flash CircuitPython Firmware"), |
|
252 self.tr("""<p>Teensy 4.0 and Teensy 4.1 do not support the UF2""" |
|
253 """ bootloader. Please use the 'Teensy Loader'""" |
|
254 """ application to flash CircuitPython. Make sure you""" |
|
255 """ downloaded the CircuitPython .hex file.</p>""" |
|
256 """<p>See <a href="{0}">the PJRC Teensy web site</a>""" |
|
257 """ for details.</p>""") |
|
258 .format("https://www.pjrc.com/teensy/loader.html")) |
|
259 |
|
260 @pyqtSlot() |
|
261 def __installLibraryFiles(self): |
|
262 """ |
|
263 Private slot to install Python files into the onboard library. |
|
264 """ |
|
265 if not self.__deviceVolumeMounted(): |
|
266 E5MessageBox.critical( |
|
267 self.microPython, |
|
268 self.tr("Install Library Files"), |
|
269 self.tr("""The device volume "<b>{0}</b>" is not available.""" |
|
270 """ Ensure it is mounted properly and try again.""")) |
|
271 return |
|
272 |
|
273 target = os.path.join(self.getWorkspace(), "lib") |
|
274 # ensure that the library directory exists on the device |
|
275 if not os.path.isdir(target): |
|
276 os.makedirs(target) |
|
277 |
|
278 libraryFiles = E5FileDialog.getOpenFileNames( |
|
279 self.microPython, |
|
280 self.tr("Install Library Files"), |
|
281 os.path.expanduser("~"), |
|
282 self.tr("Compiled Python Files (*.mpy);;" |
|
283 "Python Files (*.py);;" |
|
284 "All Files (*)")) |
|
285 |
|
286 for libraryFile in libraryFiles: |
|
287 if os.path.exists(libraryFile): |
|
288 shutil.copy2(libraryFile, target) |
|
289 |
|
290 def getDocumentationUrl(self): |
|
291 """ |
|
292 Public method to get the device documentation URL. |
|
293 |
|
294 @return documentation URL of the device |
|
295 @rtype str |
|
296 """ |
|
297 return Preferences.getMicroPython("CircuitPythonDocuUrl") |
|
298 |
|
299 def getDownloadMenuEntries(self): |
|
300 """ |
|
301 Public method to retrieve the entries for the downloads menu. |
|
302 |
|
303 @return list of tuples with menu text and URL to be opened for each |
|
304 entry |
|
305 @rtype list of tuple of (str, str) |
|
306 """ |
|
307 return [ |
|
308 (self.tr("CircuitPython Firmware"), |
|
309 Preferences.getMicroPython("CircuitPythonFirmwareUrl")), |
|
310 (self.tr("CircuitPython Libraries"), |
|
311 Preferences.getMicroPython("CircuitPythonLibrariesUrl")) |
|
312 ] |