def __init__(self, parent): super(FlashDialog, self).__init__(parent, Qt.WindowCloseButtonHint) self.setupUi(self) self.setModal(True) self._connection_scanner = ConnectionScanner() self._port = None self._flash_output = None self._flash_output_mutex = Lock() self._flashing = False if Settings().python_flash_executable: self.pythonPathEdit.setText(Settings().python_flash_executable) self.pickPythonButton.clicked.connect(self._pick_python) self.pickFirmwareButton.clicked.connect(self._pick_firmware) self.refreshButton.clicked.connect(self._refresh_ports) self.wiringButton.clicked.connect(self._show_wiring) self.eraseButton.clicked.connect(lambda: self._start(False, True)) self.flashButton.clicked.connect(lambda: self._start(True, False)) self._flash_output_signal.connect(self._update_output) self._flash_finished_signal.connect(self._flash_finished) self._refresh_ports()
def __init__(self): super(MainWindow, self).__init__() self.setupUi(self) self.setAttribute(Qt.WA_QuitOnClose) self._connection_scanner = ConnectionScanner() self._connection = None self._root_dir = Settings.root_dir self._mcu_files_model = None self._terminal = Terminal() self._terminal_dialog = None self._code_editor = None self._flash_dialog = None self.actionNavigate.triggered.connect(self.navigate_directory) self.actionTerminal.triggered.connect(self.open_terminal) self.actionCode_Editor.triggered.connect(self.open_code_editor) self.actionUpload.triggered.connect(self.upload_transfer_scripts) self.actionFlash.triggered.connect(self.open_flash_dialog) self.connectionComboBox.currentIndexChanged.connect( self.connection_changed) self.refreshButton.clicked.connect(self.refresh_ports) # Populate baud speed combo box and select default self.baudComboBox.clear() for speed in BaudOptions.speeds: self.baudComboBox.addItem(str(speed)) self.baudComboBox.setCurrentIndex(BaudOptions.speeds.index(115200)) self.presetButton.clicked.connect(self.show_presets) self.connectButton.clicked.connect(self.connect_pressed) self.update_file_tree() self.listButton.clicked.connect(self.list_mcu_files) self.listView.clicked.connect(self.mcu_file_selection_changed) self.listView.doubleClicked.connect(self.read_mcu_file) self.executeButton.clicked.connect(self.execute_mcu_code) self.removeButton.clicked.connect(self.remove_file) self.localPathEdit.setText(self._root_dir) self.treeView.clicked.connect(self.local_file_selection_changed) self.treeView.doubleClicked.connect(self.open_local_file) self.transferToMcuButton.clicked.connect(self.transfer_to_mcu) self.transferToPcButton.clicked.connect(self.transfer_to_pc) self.disconnected()
def __init__(self): super(MainWindow, self).__init__() self.setupUi(self) self.setAttribute(Qt.WA_QuitOnClose) self._connection_scanner = ConnectionScanner() self.refresh_ports() self.refreshButton.clicked.connect(self.refresh_ports) self.connectButton.clicked.connect(self.connect) self.runButton.clicked.connect(self.run) self.inputFileButton.clicked.connect(self.browse_input_file) self.disconnected() self._controller = None self._run_thread = None
def __init__(self): super(MainWindow, self).__init__() self.setupUi(self) self.setAttribute(Qt.WA_QuitOnClose) self._connection_scanner = ConnectionScanner() self.refresh_ports() self.refreshButton.clicked.connect(self.refresh_ports) self.connectButton.clicked.connect(self.connect) self.runButton.clicked.connect(self.run) self.inputFileButton.clicked.connect(self.browse_input_file) self.disconnected() self._controller = None self._run_thread = None
def __init__(self): super(MainWindow, self).__init__() self.setupUi(self) self.setAttribute(Qt.WA_QuitOnClose) geometry = Settings().retrieve_geometry("main") if geometry: self.restoreGeometry(geometry) self._connection_scanner = ConnectionScanner() self._connection = None self._root_dir = Settings().root_dir self._mcu_files_model = None self._terminal = Terminal() self._terminal_dialog = None self._code_editor = None self._flash_dialog = None self._settings_dialog = None self._preset_password = None self.actionNavigate.triggered.connect(self.navigate_directory) self.actionTerminal.triggered.connect(self.open_terminal) self.actionCode_Editor.triggered.connect(self.open_code_editor) self.actionUpload.triggered.connect(self.upload_transfer_scripts) self.actionFlash.triggered.connect(self.open_flash_dialog) self.actionSettings.triggered.connect(self.open_settings_dialog) self.connectionComboBox.currentIndexChanged.connect(self.connection_changed) self.refreshButton.clicked.connect(self.refresh_ports) # Populate baud speed combo box and select default self.baudComboBox.clear() for speed in BaudOptions.speeds: self.baudComboBox.addItem(str(speed)) self.baudComboBox.setCurrentIndex(BaudOptions.speeds.index(115200)) self.presetButton.clicked.connect(self.show_presets) self.connectButton.clicked.connect(self.connect_pressed) self.update_file_tree() self.listButton.clicked.connect(self.list_mcu_files) self.mcuFilesListView.clicked.connect(self.mcu_file_selection_changed) self.mcuFilesListView.doubleClicked.connect(self.read_mcu_file) self.executeButton.clicked.connect(self.execute_mcu_code) self.removeButton.clicked.connect(self.remove_file) self.localPathEdit.setText(self._root_dir) local_selection_model = self.localFilesTreeView.selectionModel() local_selection_model.selectionChanged.connect(self.local_file_selection_changed) self.localFilesTreeView.doubleClicked.connect(self.open_local_file) # Set the "Name" column to always fit the content self.localFilesTreeView.header().setSectionResizeMode(0, QHeaderView.ResizeToContents) self.compileButton.clicked.connect(self.compile_files) self.update_compile_button() self.autoTransferCheckBox.setChecked(Settings().auto_transfer) self.transferToMcuButton.clicked.connect(self.transfer_to_mcu) self.transferToPcButton.clicked.connect(self.transfer_to_pc) self.disconnected()
class MainWindow(QMainWindow, Ui_MainWindow): def __init__(self): super(MainWindow, self).__init__() self.setupUi(self) self.setAttribute(Qt.WA_QuitOnClose) geometry = Settings().retrieve_geometry("main") if geometry: self.restoreGeometry(geometry) self._connection_scanner = ConnectionScanner() self._connection = None self._root_dir = Settings().root_dir self._mcu_files_model = None self._terminal = Terminal() self._terminal_dialog = None self._code_editor = None self._flash_dialog = None self._settings_dialog = None self._preset_password = None self.actionNavigate.triggered.connect(self.navigate_directory) self.actionTerminal.triggered.connect(self.open_terminal) self.actionCode_Editor.triggered.connect(self.open_code_editor) self.actionUpload.triggered.connect(self.upload_transfer_scripts) self.actionFlash.triggered.connect(self.open_flash_dialog) self.actionSettings.triggered.connect(self.open_settings_dialog) self.connectionComboBox.currentIndexChanged.connect(self.connection_changed) self.refreshButton.clicked.connect(self.refresh_ports) # Populate baud speed combo box and select default self.baudComboBox.clear() for speed in BaudOptions.speeds: self.baudComboBox.addItem(str(speed)) self.baudComboBox.setCurrentIndex(BaudOptions.speeds.index(115200)) self.presetButton.clicked.connect(self.show_presets) self.connectButton.clicked.connect(self.connect_pressed) self.update_file_tree() self.listButton.clicked.connect(self.list_mcu_files) self.mcuFilesListView.clicked.connect(self.mcu_file_selection_changed) self.mcuFilesListView.doubleClicked.connect(self.read_mcu_file) self.executeButton.clicked.connect(self.execute_mcu_code) self.removeButton.clicked.connect(self.remove_file) self.localPathEdit.setText(self._root_dir) local_selection_model = self.localFilesTreeView.selectionModel() local_selection_model.selectionChanged.connect(self.local_file_selection_changed) self.localFilesTreeView.doubleClicked.connect(self.open_local_file) # Set the "Name" column to always fit the content self.localFilesTreeView.header().setSectionResizeMode(0, QHeaderView.ResizeToContents) self.compileButton.clicked.connect(self.compile_files) self.update_compile_button() self.autoTransferCheckBox.setChecked(Settings().auto_transfer) self.transferToMcuButton.clicked.connect(self.transfer_to_mcu) self.transferToPcButton.clicked.connect(self.transfer_to_pc) self.disconnected() def closeEvent(self, event): Settings().root_dir = self._root_dir Settings().auto_transfer = self.autoTransferCheckBox.isChecked() Settings().update_geometry("main", self.saveGeometry()) Settings().save() if self._connection is not None and self._connection.is_connected(): self.end_connection() if self._terminal_dialog: self._terminal_dialog.close() if self._code_editor: self._code_editor.close() event.accept() def connection_changed(self): connection = self._connection_scanner.port_list[self.connectionComboBox.currentIndex()] self.connectionStackedWidget.setCurrentIndex(1 if connection == "wifi" else 0) def refresh_ports(self): self._connection_scanner.scan_connections(with_wifi=True) # Populate port combo box and select default self.connectionComboBox.clear() if self._connection_scanner.port_list: for port in self._connection_scanner.port_list: self.connectionComboBox.addItem(port) prefPort = str(Settings().preferred_port) prefPort = prefPort.upper() prefPort = prefPort.join(prefPort.split()) if self.connectionComboBox.findText(prefPort) >= 0: self.connectionComboBox.setCurrentIndex(self.connectionComboBox.findText(prefPort)) else: self.connectionComboBox.setCurrentIndex(0) self.connectButton.setEnabled(True) else: self.connectButton.setEnabled(False) def set_status(self, status): if status == "Connected": self.statusLabel.setStyleSheet("QLabel { background-color : none; color : green; font : bold;}") elif status == "Disconnected": self.statusLabel.setStyleSheet("QLabel { background-color : none; color : red; }") elif status == "Connecting...": self.statusLabel.setStyleSheet("QLabel { background-color : none; color : blue; }") elif status == "Error": self.statusLabel.setStyleSheet("QLabel { background-color : red; color : white; }") elif status == "Password": self.statusLabel.setStyleSheet("QLabel { background-color : red; color : white; }") status = "Wrong Password" else: self.statusLabel.setStyleSheet("QLabel { background-color : red; color : white; }") self.statusLabel.setText(status) QApplication.processEvents(QEventLoop.ExcludeUserInputEvents) def update_compile_button(self): self.compileButton.setEnabled(bool(Settings().mpy_cross_path) and len(self.get_local_file_selection()) > 0) def disconnected(self): self.connectButton.setText("Connect") self.set_status("Disconnected") self.listButton.setEnabled(False) self.connectionComboBox.setEnabled(True) self.baudComboBox.setEnabled(True) self.refreshButton.setEnabled(True) self.mcuFilesListView.setEnabled(False) self.executeButton.setEnabled(False) self.removeButton.setEnabled(False) self.actionTerminal.setEnabled(False) self.actionUpload.setEnabled(False) self.transferToMcuButton.setEnabled(False) self.transferToPcButton.setEnabled(False) # Clear terminal on disconnect self._terminal.clear() if self._terminal_dialog: self._terminal_dialog.close() if self._code_editor: self._code_editor.disconnected() self.refresh_ports() def connected(self): self.connectButton.setText("Disconnect") self.set_status("Connected") self.listButton.setEnabled(True) self.connectionComboBox.setEnabled(False) self.baudComboBox.setEnabled(False) self.refreshButton.setEnabled(False) self.mcuFilesListView.setEnabled(True) self.actionTerminal.setEnabled(True) if isinstance(self._connection, SerialConnection): self.actionUpload.setEnabled(True) self.transferToMcuButton.setEnabled(True) if self._code_editor: self._code_editor.connected(self._connection) self.list_mcu_files() def navigate_directory(self): dialog = QFileDialog() dialog.setDirectory(self._root_dir) dialog.setFileMode(QFileDialog.Directory) dialog.setOption(QFileDialog.ShowDirsOnly) dialog.exec() path = dialog.selectedFiles() if path and path[0]: self._root_dir = path[0] self.localPathEdit.setText(self._root_dir) self.update_file_tree() def update_file_tree(self): model = QFileSystemModel() model.setRootPath(self._root_dir) self.localFilesTreeView.setModel(model) local_selection_model = self.localFilesTreeView.selectionModel() local_selection_model.selectionChanged.connect(self.local_file_selection_changed) self.localFilesTreeView.setRootIndex(model.index(self._root_dir)) def serial_mcu_connection_valid(self): file_list = [] try: file_list = self._connection.list_files() return True except OperationError: file_list = None return False def list_mcu_files(self): file_list = [] try: file_list = self._connection.list_files() except OperationError: QMessageBox().critical(self, "Operation failed", "Could not list files.", QMessageBox.Ok) return self._mcu_files_model = QStringListModel() for file in file_list: idx = self._mcu_files_model.rowCount() self._mcu_files_model.insertRow(idx) self._mcu_files_model.setData(self._mcu_files_model.index(idx), file) self.mcuFilesListView.setModel(self._mcu_files_model) self.mcu_file_selection_changed() def execute_mcu_code(self): idx = self.mcuFilesListView.currentIndex() assert isinstance(idx, QModelIndex) model = self.mcuFilesListView.model() assert isinstance(model, QStringListModel) file_name = model.data(idx, Qt.EditRole) self._connection.run_file(file_name) def remove_file(self): idx = self.mcuFilesListView.currentIndex() assert isinstance(idx, QModelIndex) model = self.mcuFilesListView.model() assert isinstance(model, QStringListModel) file_name = model.data(idx, Qt.EditRole) try: self._connection.remove_file(file_name) except OperationError: QMessageBox().critical(self, "Operation failed", "Could not remove the file.", QMessageBox.Ok) return self.list_mcu_files() def ask_for_password(self, title, label="Password"): if self._preset_password is not None: return self._preset_password input_dlg = QInputDialog(parent=self, flags=Qt.Dialog) input_dlg.setTextEchoMode(QLineEdit.Password) input_dlg.setWindowTitle(title) input_dlg.setLabelText(label) input_dlg.resize(500, 100) input_dlg.exec() return input_dlg.textValue() def start_connection(self): self.set_status("Connecting...") connection = self._connection_scanner.port_list[self.connectionComboBox.currentIndex()] if connection == "wifi": ip_address = self.ipLineEdit.text() port = self.portSpinBox.value() if not IpHelper.is_valid_ipv4(ip_address): QMessageBox().warning(self, "Invalid IP", "The IP address has invalid format", QMessageBox.Ok) return try: self._connection = WifiConnection(ip_address, port, self._terminal, self.ask_for_password) except PasswordException: self.set_status("Password") return except NewPasswordException: QMessageBox().information(self, "Password set", "WebREPL password was not previously configured, so it was set to " "\"passw\" (without quotes). " "You can change it in port_config.py (will require reboot to take effect). " "Caution: Passwords longer than 9 characters will be truncated.\n\n" "Continue by connecting again.", QMessageBox.Ok) return else: baud_rate = BaudOptions.speeds[self.baudComboBox.currentIndex()] self._connection = SerialConnection(connection, baud_rate, self._terminal, self.serialResetCheckBox.isChecked()) if self._connection.is_connected(): if not self.serial_mcu_connection_valid(): self._connection.disconnect() self._connection = None else: # serial connection didn't work, so likely the unplugged the serial device and COM value is stale self.refresh_ports() if self._connection is not None and self._connection.is_connected(): self.connected() if isinstance(self._connection, SerialConnection): if Settings().use_transfer_scripts and not self._connection.check_transfer_scripts_version(): QMessageBox.warning(self, "Transfer scripts problem", "Transfer scripts for UART are either" " missing or have wrong version.\nPlease use 'File->Init transfer files' to" " fix this issue.") else: self._connection = None self.set_status("Error") self.refresh_ports() def end_connection(self): self._connection.disconnect() self._connection = None self.disconnected() def show_presets(self): dialog = WiFiPresetDialog() dialog.accepted.connect(lambda: self.use_preset(dialog.selected_ip, dialog.selected_port, dialog.selected_password)) dialog.exec() def use_preset(self, ip, port, password): self.ipLineEdit.setText(ip) self.portSpinBox.setValue(port) self._preset_password = password def connect_pressed(self): if self._connection is not None and self._connection.is_connected(): self.end_connection() else: self.start_connection() def run_file(self): content = self.codeEdit.toPlainText() self._connection.send_block(content) def open_local_file(self, idx): assert isinstance(idx, QModelIndex) model = self.localFilesTreeView.model() assert isinstance(model, QFileSystemModel) if model.isDir(idx): return local_path = model.filePath(idx) remote_path = local_path.rsplit("/", 1)[1] if local_path.endswith(".py"): if Settings().external_editor_path: self.open_external_editor(local_path) else: with open(local_path) as f: text = "".join(f.readlines()) self.open_code_editor() self._code_editor.set_code(local_path, remote_path, text) else: QMessageBox.information(self, "Unknown file", "Files without .py ending won't open" " in editor, but can still be transferred.") def mcu_file_selection_changed(self): idx = self.mcuFilesListView.currentIndex() assert isinstance(idx, QModelIndex) if idx.row() >= 0: self.executeButton.setEnabled(True) self.removeButton.setEnabled(True) self.transferToPcButton.setEnabled(True) else: self.executeButton.setEnabled(False) self.removeButton.setEnabled(False) self.transferToPcButton.setEnabled(False) def get_local_file_selection(self): """Returns absolute paths for selected local files""" indices = self.localFilesTreeView.selectedIndexes() model = self.localFilesTreeView.model() assert isinstance(model, QFileSystemModel) def filter_indices(x): return x.column() == 0 and not model.isDir(x) # Filter out all but first column (file name) and # don't include directories indices = [x for x in indices if filter_indices(x)] # Return absolute paths return [model.filePath(idx) for idx in indices] def local_file_selection_changed(self): self.update_compile_button() local_file_paths = self.get_local_file_selection() if len(local_file_paths) == 1: self.remoteNameEdit.setText(local_file_paths[0].rsplit("/", 1)[1]) else: self.remoteNameEdit.setText("") def compile_files(self): local_file_paths = self.get_local_file_selection() compiled_file_paths = [] for local_path in local_file_paths: split = os.path.splitext(local_path) if split[1] == ".mpy": title = "COMPILE WARNING!! " + os.path.basename(local_path) QMessageBox.warning(self, title, "Can't compile .mpy files, already bytecode") continue mpy_path = split[0]+".mpy" try: os.remove(mpy_path) # Force view to update itself so that it sees file removed self.localFilesTreeView.repaint() QApplication.processEvents(QEventLoop.ExcludeUserInputEvents) except OSError: pass try: with subprocess.Popen([Settings().mpy_cross_path, os.path.basename(local_path)], cwd=os.path.dirname(local_path), stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE) as proc: proc.wait() # Wait for process to finish out = proc.stderr.read() if out: QMessageBox.warning(self, "Compilation error", out.decode("utf-8")) continue except OSError: QMessageBox.warning(self, "Compilation error", "Failed to run mpy-cross") continue compiled_file_paths += [mpy_path] # Force view to update so that it sees compiled files added self.localFilesTreeView.repaint() QApplication.processEvents(QEventLoop.ExcludeUserInputEvents) # Force view to update last time so that it resorts the content # Without this, the new file will be placed at the bottom of the view no matter what header = self.localFilesTreeView.header() column = header.sortIndicatorSection() order = header.sortIndicatorOrder() # This is necessary so that view actually sorts anything again self.localFilesTreeView.sortByColumn(-1, Qt.AscendingOrder) QApplication.processEvents(QEventLoop.ExcludeUserInputEvents) self.localFilesTreeView.sortByColumn(column, order) selection_model = self.localFilesTreeView.selectionModel() if compiled_file_paths: assert isinstance(selection_model, QItemSelectionModel) selection_model.clearSelection() for mpy_path in compiled_file_paths: idx = self.localFilesTreeView.model().index(mpy_path) selection_model.select(idx, QItemSelectionModel.Select | QItemSelectionModel.Rows) if (self.autoTransferCheckBox.isChecked() and self._connection and self._connection.is_connected() and compiled_file_paths): self.transfer_to_mcu() def finished_read_mcu_file(self, file_name, transfer): assert isinstance(transfer, FileTransfer) result = transfer.read_result text = result.binary_data.decode("utf-8", errors="replace") if result.binary_data is not None else "!Failed to read file!" self.open_code_editor() self._code_editor.set_code(None, file_name, text) def read_mcu_file(self, idx): assert isinstance(idx, QModelIndex) model = self.mcuFilesListView.model() assert isinstance(model, QStringListModel) file_name = model.data(idx, Qt.EditRole) if not file_name.endswith(".py"): QMessageBox.information(self, "Unknown file", "Files without .py ending won't open" " in editor, but can still be transferred.") return progress_dlg = FileTransferDialog(FileTransferDialog.DOWNLOAD) progress_dlg.finished.connect(lambda: self.finished_read_mcu_file(file_name, progress_dlg.transfer)) progress_dlg.show() self._connection.read_file(file_name, progress_dlg.transfer) def upload_transfer_scripts(self): progress_dlg = FileTransferDialog(FileTransferDialog.UPLOAD) progress_dlg.finished.connect(self.list_mcu_files) progress_dlg.show() self._connection.upload_transfer_files(progress_dlg.transfer) def transfer_to_mcu(self): local_file_paths = self.get_local_file_selection() progress_dlg = FileTransferDialog(FileTransferDialog.UPLOAD) progress_dlg.finished.connect(self.list_mcu_files) progress_dlg.show() # Handle single file transfer if len(local_file_paths) == 1: local_path = local_file_paths[0] remote_path = self.remoteNameEdit.text() with open(local_path, "rb") as f: content = f.read() self._connection.write_file(remote_path, content, progress_dlg.transfer) return # Batch file transfer progress_dlg.enable_cancel() progress_dlg.transfer.set_file_count(len(local_file_paths)) self._connection.write_files(local_file_paths, progress_dlg.transfer) def finished_transfer_to_pc(self, file_path, transfer): if not transfer.read_result.binary_data: return try: with open(file_path, "wb") as file: file.write(transfer.read_result.binary_data) except IOError: QMessageBox.critical(self, "Save operation failed", "Couldn't save the file. Check path and permissions.") def transfer_to_pc(self): idx = self.mcuFilesListView.currentIndex() assert isinstance(idx, QModelIndex) model = self.mcuFilesListView.model() assert isinstance(model, QStringListModel) remote_path = model.data(idx, Qt.EditRole) local_path = self.localPathEdit.text() + "/" + remote_path progress_dlg = FileTransferDialog(FileTransferDialog.DOWNLOAD) progress_dlg.finished.connect(lambda: self.finished_transfer_to_pc(local_path, progress_dlg.transfer)) progress_dlg.show() self._connection.read_file(remote_path, progress_dlg.transfer) def open_terminal(self): if self._terminal_dialog is not None: return self._terminal_dialog = TerminalDialog(self, self._connection, self._terminal) self._terminal_dialog.finished.connect(self.close_terminal) self._terminal_dialog.show() def close_terminal(self): self._terminal_dialog = None def open_external_editor(self, file_path): ext_path = Settings().external_editor_path ext_args = [] if Settings().external_editor_args: def wildcard_replace(s): s = s.replace("%f", file_path) return s ext_args = [wildcard_replace(x.strip()) for x in Settings().external_editor_args.split(";")] subprocess.Popen([ext_path] + ext_args) def open_code_editor(self): if self._code_editor is not None: return self._code_editor = CodeEditDialog(self, self._connection) self._code_editor.mcu_file_saved.connect(self.list_mcu_files) self._code_editor.finished.connect(self.close_code_editor) self._code_editor.show() def close_code_editor(self): self._code_editor = None def open_flash_dialog(self): if self._connection is not None and self._connection.is_connected(): self.end_connection() self._flash_dialog = FlashDialog(self) self._flash_dialog.finished.connect(self.close_flash_dialog) self._flash_dialog.show() def close_flash_dialog(self): self._flash_dialog = None def open_settings_dialog(self): if self._settings_dialog is not None: return self._settings_dialog = SettingsDialog(self) self._settings_dialog.finished.connect(self.close_settings_dialog) self._settings_dialog.show() def close_settings_dialog(self): self._settings_dialog = None # Update compile button as mpy-cross path might have been set self.update_compile_button()
class MainWindow(QMainWindow, Ui_MainWindow): def __init__(self): super(MainWindow, self).__init__() self.setupUi(self) self.setAttribute(Qt.WA_QuitOnClose) self._connection_scanner = ConnectionScanner() self._connection = None self._root_dir = Settings.root_dir self._mcu_files_model = None self._terminal = Terminal() self._terminal_dialog = None self._code_editor = None self._flash_dialog = None self.actionNavigate.triggered.connect(self.navigate_directory) self.actionTerminal.triggered.connect(self.open_terminal) self.actionCode_Editor.triggered.connect(self.open_code_editor) self.actionUpload.triggered.connect(self.upload_transfer_scripts) self.actionFlash.triggered.connect(self.open_flash_dialog) self.connectionComboBox.currentIndexChanged.connect( self.connection_changed) self.refreshButton.clicked.connect(self.refresh_ports) # Populate baud speed combo box and select default self.baudComboBox.clear() for speed in BaudOptions.speeds: self.baudComboBox.addItem(str(speed)) self.baudComboBox.setCurrentIndex(BaudOptions.speeds.index(115200)) self.presetButton.clicked.connect(self.show_presets) self.connectButton.clicked.connect(self.connect_pressed) self.update_file_tree() self.listButton.clicked.connect(self.list_mcu_files) self.listView.clicked.connect(self.mcu_file_selection_changed) self.listView.doubleClicked.connect(self.read_mcu_file) self.executeButton.clicked.connect(self.execute_mcu_code) self.removeButton.clicked.connect(self.remove_file) self.localPathEdit.setText(self._root_dir) self.treeView.clicked.connect(self.local_file_selection_changed) self.treeView.doubleClicked.connect(self.open_local_file) self.transferToMcuButton.clicked.connect(self.transfer_to_mcu) self.transferToPcButton.clicked.connect(self.transfer_to_pc) self.disconnected() def closeEvent(self, event): Settings.root_dir = self._root_dir Settings.save() if self._connection is not None and self._connection.is_connected(): self.end_connection() if self._terminal_dialog: assert isinstance(self._terminal_dialog, QDialog) self._terminal_dialog.close() event.accept() def connection_changed(self): connection = self._connection_scanner.port_list[ self.connectionComboBox.currentIndex()] self.connectionStackedWidget.setCurrentIndex(1 if connection == "wifi" else 0) def refresh_ports(self): self._connection_scanner.scan_connections(with_wifi=True) # Populate port combo box and select default self.connectionComboBox.clear() if self._connection_scanner.port_list: for port in self._connection_scanner.port_list: self.connectionComboBox.addItem(port) self.connectionComboBox.setCurrentIndex(0) self.connectButton.setEnabled(True) else: self.connectButton.setEnabled(False) def set_status(self, status): if status == "Connected": self.statusLabel.setStyleSheet( "QLabel { background-color : none; color : green; font : bold;}" ) elif status == "Disconnected": self.statusLabel.setStyleSheet( "QLabel { background-color : none; color : red; }") elif status == "Error": self.statusLabel.setStyleSheet( "QLabel { background-color : red; color : white; }") self.statusLabel.setText(status) def disconnected(self): self.connectButton.setText("Connect") self.set_status("Disconnected") self.listButton.setEnabled(False) self.connectionComboBox.setEnabled(True) self.baudComboBox.setEnabled(True) self.refreshButton.setEnabled(True) self.listView.setEnabled(False) self.executeButton.setEnabled(False) self.removeButton.setEnabled(False) self.actionTerminal.setEnabled(False) self.actionUpload.setEnabled(False) self.transferToMcuButton.setEnabled(False) self.transferToPcButton.setEnabled(False) if self._terminal_dialog: self._terminal_dialog.close() if self._code_editor: self._code_editor.disconnected() self.refresh_ports() def connected(self): self.connectButton.setText("Disconnect") self.set_status("Connected") self.listButton.setEnabled(True) self.connectionComboBox.setEnabled(False) self.baudComboBox.setEnabled(False) self.refreshButton.setEnabled(False) self.listView.setEnabled(True) self.actionTerminal.setEnabled(True) if isinstance(self._connection, SerialConnection): self.actionUpload.setEnabled(True) self.transferToMcuButton.setEnabled(True) if self._code_editor: self._code_editor.connected(self._connection) self.list_mcu_files() def navigate_directory(self): dialog = QFileDialog() dialog.setDirectory(self._root_dir) dialog.setFileMode(QFileDialog.Directory) dialog.setOption(QFileDialog.ShowDirsOnly) dialog.exec() path = dialog.selectedFiles() if path and path[0]: self._root_dir = path[0] self.localPathEdit.setText(self._root_dir) self.update_file_tree() def update_file_tree(self): model = QFileSystemModel() model.setRootPath(self._root_dir) self.treeView.setModel(model) self.treeView.setRootIndex(model.index(self._root_dir)) def list_mcu_files(self): file_list = self._connection.list_files() self._mcu_files_model = QStringListModel() for file in file_list: idx = self._mcu_files_model.rowCount() self._mcu_files_model.insertRow(idx) self._mcu_files_model.setData(self._mcu_files_model.index(idx), file) self.listView.setModel(self._mcu_files_model) self.mcu_file_selection_changed() def execute_mcu_code(self): idx = self.listView.currentIndex() assert isinstance(idx, QModelIndex) model = self.listView.model() assert isinstance(model, QStringListModel) file_name = model.data(idx, Qt.EditRole) self._connection.run_file(file_name) def remove_file(self): idx = self.listView.currentIndex() assert isinstance(idx, QModelIndex) model = self.listView.model() assert isinstance(model, QStringListModel) file_name = model.data(idx, Qt.EditRole) self._connection.remove_file(file_name) self.list_mcu_files() def ask_for_password(self): input_dlg = QInputDialog(parent=self) input_dlg.setTextEchoMode(QLineEdit.Password) input_dlg.setWindowTitle("Enter WebREPL password") input_dlg.setLabelText("Password") input_dlg.resize(500, 100) input_dlg.exec() return input_dlg.textValue() def start_connection(self): connection = self._connection_scanner.port_list[ self.connectionComboBox.currentIndex()] if connection == "wifi": ip_address = self.ipLineEdit.text() port = self.portSpinBox.value() if not IpHelper.is_valid_ipv4(ip_address): QMessageBox.warning(self, "Invalid IP", "The IP address has invalid format") return self._connection = WifiConnection(ip_address, port, self._terminal, lambda: self.ask_for_password()) else: baud_rate = BaudOptions.speeds[self.baudComboBox.currentIndex()] self._connection = SerialConnection( connection, baud_rate, self._terminal, self.serialResetCheckBox.isChecked()) if self._connection is not None and self._connection.is_connected(): self.connected() else: self._connection = None self.set_status("Error") def end_connection(self): self._connection.disconnect() self._connection = None self.disconnected() def show_presets(self): dialog = WiFiPresetDialog() dialog.accepted.connect( lambda: self.use_preset(dialog.selected_ip, dialog.selected_port)) dialog.exec() def use_preset(self, ip, port): self.ipLineEdit.setText(ip) self.portSpinBox.setValue(port) def connect_pressed(self): if self._connection is not None and self._connection.is_connected(): self.end_connection() else: self.start_connection() def run_file(self): content = self.codeEdit.toPlainText() self._connection.send_block(content) def open_local_file(self, idx): assert isinstance(idx, QModelIndex) model = self.treeView.model() assert isinstance(model, QFileSystemModel) if model.isDir(idx): return local_path = model.filePath(idx) remote_path = local_path.rsplit("/", 1)[1] if local_path.endswith(".py"): with open(local_path) as f: text = "".join(f.readlines()) self.open_code_editor() self._code_editor.set_code(local_path, remote_path, text) else: QMessageBox.information( self, "Unknown file", "Files without .py ending won't open" " in editor, but can still be transferred.") def mcu_file_selection_changed(self): idx = self.listView.currentIndex() assert isinstance(idx, QModelIndex) if idx.row() >= 0: self.executeButton.setEnabled(True) self.removeButton.setEnabled(True) self.transferToPcButton.setEnabled(True) else: self.executeButton.setEnabled(False) self.removeButton.setEnabled(False) self.transferToPcButton.setEnabled(False) def local_file_selection_changed(self): idx = self.treeView.currentIndex() assert isinstance(idx, QModelIndex) model = self.treeView.model() assert isinstance(model, QFileSystemModel) if model.isDir(idx): return local_path = model.filePath(idx) self.remoteNameEdit.setText(local_path.rsplit("/", 1)[1]) def finished_read_mcu_file(self, file_name, transfer): assert isinstance(transfer, FileTransfer) result = transfer.read_result text = result.binary_data.decode( "utf-8", errors="replace" ) if result.binary_data is not None else "!Failed to read file!" self.open_code_editor() self._code_editor.set_code(None, file_name, text) def read_mcu_file(self, idx): assert isinstance(idx, QModelIndex) model = self.listView.model() assert isinstance(model, QStringListModel) file_name = model.data(idx, Qt.EditRole) if not file_name.endswith(".py"): QMessageBox.information( self, "Unknown file", "Files without .py ending won't open" " in editor, but can still be transferred.") return progress_dlg = FileTransferDialog(FileTransferDialog.DOWNLOAD) progress_dlg.finished.connect(lambda: self.finished_read_mcu_file( file_name, progress_dlg.transfer)) progress_dlg.show() self._connection.read_file(file_name, progress_dlg.transfer) def upload_transfer_scripts(self): progress_dlg = FileTransferDialog(FileTransferDialog.UPLOAD) progress_dlg.finished.connect(self.list_mcu_files) progress_dlg.show() self._connection.upload_transfer_files(progress_dlg.transfer) def transfer_to_mcu(self): idx = self.treeView.currentIndex() assert isinstance(idx, QModelIndex) model = self.treeView.model() assert isinstance(model, QFileSystemModel) if model.isDir(idx): return local_path = model.filePath(idx) remote_path = self.remoteNameEdit.text() with open(local_path, "rb") as f: content = f.read() progress_dlg = FileTransferDialog(FileTransferDialog.UPLOAD) progress_dlg.finished.connect(self.list_mcu_files) progress_dlg.show() self._connection.write_file(remote_path, content, progress_dlg.transfer) def finished_transfer_to_pc(self, file_path, transfer): if not transfer.read_result.binary_data: return try: with open(file_path, "wb") as file: file.write(transfer.read_result.binary_data) except IOError: QMessageBox.critical( self, "Save operation failed", "Couldn't save the file. Check path and permissions.") def transfer_to_pc(self): idx = self.listView.currentIndex() assert isinstance(idx, QModelIndex) model = self.listView.model() assert isinstance(model, QStringListModel) remote_path = model.data(idx, Qt.EditRole) local_path = self.localPathEdit.text() + "/" + remote_path progress_dlg = FileTransferDialog(FileTransferDialog.DOWNLOAD) progress_dlg.finished.connect(lambda: self.finished_transfer_to_pc( local_path, progress_dlg.transfer)) progress_dlg.show() self._connection.read_file(remote_path, progress_dlg.transfer) def open_terminal(self): if self._terminal_dialog is not None: return self._terminal_dialog = TerminalDialog(self._connection, self._terminal) self._terminal_dialog.finished.connect(self.close_terminal) self._terminal_dialog.show() def close_terminal(self): self._terminal_dialog = None def open_code_editor(self): if self._code_editor is not None: return self._code_editor = CodeEditDialog(self._connection) self._code_editor.mcu_file_saved.connect(self.list_mcu_files) self._code_editor.finished.connect(self.close_code_editor) self._code_editor.show() def close_code_editor(self): self._code_editor = None def open_flash_dialog(self): if self._code_editor is not None: return self._flash_dialog = FlashDialog() self._flash_dialog.finished.connect(self.close_flash_dialog) self._flash_dialog.show() def close_flash_dialog(self): self._flash_dialog = None
class MainWindow(QMainWindow, Ui_MainWindow): def __init__(self): super(MainWindow, self).__init__() self.setupUi(self) self.setAttribute(Qt.WA_QuitOnClose) self._connection_scanner = ConnectionScanner() self.refresh_ports() self.refreshButton.clicked.connect(self.refresh_ports) self.connectButton.clicked.connect(self.connect) self.runButton.clicked.connect(self.run) self.inputFileButton.clicked.connect(self.browse_input_file) self.disconnected() self._controller = None self._run_thread = None def refresh_ports(self): self._connection_scanner.scan_connections(with_wifi=False) # Populate port combo box and select default self.connectionComboBox.clear() if self._connection_scanner.port_list: for port in self._connection_scanner.port_list: self.connectionComboBox.addItem(port) self.connectionComboBox.setCurrentIndex(0) self.runButton.setEnabled(True) else: self.runButton.setEnabled(False) def connect(self): if self._controller is None: try: connection = self._connection_scanner.port_list[ self.connectionComboBox.currentIndex()] self._controller = Controller(connection) self.connected() except serial.SerialException as e: QMessageBox().warning(self, "Serial Error", str(e), QMessageBox.Ok) self.disconnected() else: self._controller = None self.disconnected() def connected(self): self.runButton.setEnabled(True) self.connectButton.setText("Disconnect") self.statusLabel.setStyleSheet("color:green") def disconnected(self): self.runButton.setEnabled(False) self.connectButton.setText("Connect") self.statusLabel.setStyleSheet("color:red") def run(self): try: file_path = self.inputFileEdit.text() gcode_file = GcodeFile(file_path) self.totalCountLabel.setText(str(len(gcode_file))) self.processedCountLabel.setText("0") # TODO: Report progress # TODO: Ability to stop self._controller.run(gcode_file) except FileNotFoundError as e: QMessageBox().warning(self, "File Error", "Can't open specified file", QMessageBox.Ok) def browse_input_file(self): path = QFileDialog().getOpenFileName(caption="Select external editor", options=QFileDialog.ReadOnly) if path[0]: self.inputFileEdit.setText(path[0])
class MainWindow(QMainWindow, Ui_MainWindow): def __init__(self): super(MainWindow, self).__init__() self.setupUi(self) self.setAttribute(Qt.WA_QuitOnClose) self._connection_scanner = ConnectionScanner() self.refresh_ports() self.refreshButton.clicked.connect(self.refresh_ports) self.connectButton.clicked.connect(self.connect) self.runButton.clicked.connect(self.run) self.inputFileButton.clicked.connect(self.browse_input_file) self.disconnected() self._controller = None self._run_thread = None def refresh_ports(self): self._connection_scanner.scan_connections(with_wifi=False) # Populate port combo box and select default self.connectionComboBox.clear() if self._connection_scanner.port_list: for port in self._connection_scanner.port_list: self.connectionComboBox.addItem(port) self.connectionComboBox.setCurrentIndex(0) self.runButton.setEnabled(True) else: self.runButton.setEnabled(False) def connect(self): if self._controller is None: try: connection = self._connection_scanner.port_list[self.connectionComboBox.currentIndex()] self._controller = Controller(connection) self.connected() except serial.SerialException as e: QMessageBox().warning(self, "Serial Error", str(e), QMessageBox.Ok) self.disconnected() else: self._controller = None self.disconnected() def connected(self): self.runButton.setEnabled(True) self.connectButton.setText("Disconnect") self.statusLabel.setStyleSheet("color:green") def disconnected(self): self.runButton.setEnabled(False) self.connectButton.setText("Connect") self.statusLabel.setStyleSheet("color:red") def run(self): try: file_path = self.inputFileEdit.text() gcode_file = GcodeFile(file_path) self.totalCountLabel.setText(str(len(gcode_file))) self.processedCountLabel.setText("0") # TODO: Report progress # TODO: Ability to stop self._controller.run(gcode_file) except FileNotFoundError as e: QMessageBox().warning(self, "File Error", "Can't open specified file", QMessageBox.Ok) def browse_input_file(self): path = QFileDialog().getOpenFileName( caption="Select external editor", options=QFileDialog.ReadOnly) if path[0]: self.inputFileEdit.setText(path[0])
class FlashDialog(QDialog, Ui_FlashDialog): _flash_output_signal = pyqtSignal() _flash_finished_signal = pyqtSignal(int) def __init__(self, parent): super(FlashDialog, self).__init__(parent, Qt.WindowCloseButtonHint) self.setupUi(self) self.setModal(True) self._connection_scanner = ConnectionScanner() self._port = None self._flash_output = None self._flash_output_mutex = Lock() self._flashing = False if Settings().python_flash_executable: self.pythonPathEdit.setText(Settings().python_flash_executable) self.pickPythonButton.clicked.connect(self._pick_python) self.pickFirmwareButton.clicked.connect(self._pick_firmware) self.refreshButton.clicked.connect(self._refresh_ports) self.wiringButton.clicked.connect(self._show_wiring) self.eraseButton.clicked.connect(lambda: self._start(False, True)) self.flashButton.clicked.connect(lambda: self._start(True, False)) self._flash_output_signal.connect(self._update_output) self._flash_finished_signal.connect(self._flash_finished) self._refresh_ports() def closeEvent(self, event): if self._flashing: event.ignore() def keyPressEvent(self, event): if event.key() == Qt.Key_Escape and self._flashing: event.ignore() def _refresh_ports(self): self._connection_scanner.scan_connections(with_wifi=False) # Populate port combo box and select default self.portComboBox.clear() if self._connection_scanner.port_list: for port in self._connection_scanner.port_list: self.portComboBox.addItem(port) self.portComboBox.setCurrentIndex(0) self.eraseButton.setEnabled(True) self.flashButton.setEnabled(True) else: self.eraseButton.setEnabled(False) self.flashButton.setEnabled(False) def _pick_python(self): p = QFileDialog.getOpenFileName(parent=self, caption="Select python executable") path = p[0] if path: self.pythonPathEdit.setText(path) Settings().python_flash_executable = path def _pick_firmware(self): firmware_dir = None if Settings().last_firmware_directory: firmware_dir = Settings().last_firmware_directory p = QFileDialog.getOpenFileName(parent=self, caption="Select python executable", directory=firmware_dir, filter="*.bin") path = p[0] if path: self.firmwarePathEdit.setText(path) Settings().last_firmware_directory = "/".join( path.split("/")[0:-1]) def _show_wiring(self): wiring = "RST\t-> RTS\n" \ "GPIO0\t-> DTR\n" \ "TXD\t-> RXD\n" \ "RXD\t-> TXD\n" \ "VCC\t-> 3V3\n" \ "GND\t-> GND" QMessageBox.about(self, "Wiring", wiring) def _update_output(self): scrollbar = self.outputEdit.verticalScrollBar() assert isinstance(scrollbar, QScrollBar) # Preserve scroll while updating content current_scroll = scrollbar.value() scrolling = scrollbar.isSliderDown() with self._flash_output_mutex: self.outputEdit.setPlainText( self._flash_output.decode('utf-8', errors="ignore")) if not scrolling: scrollbar.setValue(scrollbar.maximum()) else: scrollbar.setValue(current_scroll) def _flash_job(self, python_path, firmware_file, erase_flash): try: params = [python_path, "flash.py", self._port] if firmware_file: params.append("--fw={}".format(firmware_file)) if erase_flash: params.append("--erase") if Settings().debug_mode: params.append("--debug") with subprocess.Popen(params, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=1) as sub: buf = bytearray() delete = 0 Logger.log("Pipe receiving:\r\n") while True: x = sub.stdout.read(1) Logger.log(x) # Flushing content only in large blocks helps with flickering # Flush output if: # - didn't receive any character (timeout?) # - received first backspace (content is going to be deleted) # - received whitespace or dot (used to signal progress) if not x \ or (x[0] == 8 and delete == 0) \ or (x[0] in b"\r\n\t ."): with self._flash_output_mutex: if delete > 0: self._flash_output = self._flash_output[: -delete] self._flash_output.extend(buf) self._flash_output_signal.emit() buf = bytearray() delete = 0 if not x: break if x[0] == 8: delete += 1 else: buf.append(x[0]) Logger.log("\r\nPipe end.\r\n") sub.stdout.close() code = sub.wait() self._flash_finished_signal.emit(code) except (FileNotFoundError, OSError): self._flash_finished_signal.emit(-1) return def _start(self, flash, erase): self._flash_output = bytearray() self.outputEdit.setPlainText("") python_path = self.pythonPathEdit.text() if not python_path: QMessageBox.critical(self, "Error", "Python2 path was not set.") return firmware_file = None if flash: firmware_file = self.firmwarePathEdit.text() if not firmware_file: QMessageBox.critical(self, "Error", "Firmware file was not set.") return self._port = self._connection_scanner.port_list[ self.portComboBox.currentIndex()] job_thread = Thread(target=self._flash_job, args=[python_path, firmware_file, erase]) job_thread.setDaemon(True) job_thread.start() self.eraseButton.setEnabled(False) self.flashButton.setEnabled(False) self._flashing = True def _flash_finished(self, code): Logger.log("Flash output contents:\r\n") Logger.log(self._flash_output) if code == 0: self._flash_output.extend(b"Rebooting from flash mode...\n") self._update_output() try: s = serial.Serial(self._port, 115200) s.dtr = False s.rts = True time.sleep(0.1) s.rts = False time.sleep(0.1) self._flash_output.extend( b"Done, you may now use the device.\n") self._update_output() except (OSError, serial.SerialException): QMessageBox.critical(self, "Flashing Error", "Failed to reboot into working mode.") elif code == -1: QMessageBox.critical( self, "Flashing Error", "Failed to run script.\nCheck that path to python is correct.") else: QMessageBox.critical(self, "Flashing Error", "Failed to flash new firmware") self.eraseButton.setEnabled(True) self.flashButton.setEnabled(True) self._flashing = False