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