def set_shared_library_path(): shared_lib_dir = get_shared_library_path() if shared_lib_dir: if sys.platform == "win32": current_path = os.environ.get("PATH", '') if not current_path.startswith(shared_lib_dir): os.environ["PATH"] = shared_lib_dir + os.pathsep + current_path else: # LD_LIBRARY_PATH will not be considered at runtime so we explicitly load the .so's we need exts = [".so"] if sys.platform == "linux" else [".so", ".dylib"] import ctypes libs = sorted(os.listdir(shared_lib_dir)) libusb = next((lib for lib in libs if "libusb" in lib), None) if libusb: # Ensure libusb is loaded first libs.insert(0, libs.pop(libs.index(libusb))) for lib in libs: if lib.lower().startswith("lib") and any(ext in lib for ext in exts): lib_path = os.path.join(shared_lib_dir, lib) if os.path.isfile(lib_path): try: ctypes.cdll.LoadLibrary(lib_path) except Exception as e: logger.exception(e)
def stop_tx_mode(self, msg): try: self.parent_ctrl_conn.send(self.Command.STOP.name) except (BrokenPipeError, OSError) as e: logger.debug("Closing parent control connection: " + str(e)) logger.info("{0}: Stopping TX Mode: {1}".format( self.__class__.__name__, msg)) if hasattr(self, "transmit_process") and self.transmit_process.is_alive(): self.transmit_process.join(self.JOIN_TIMEOUT) if self.transmit_process.is_alive(): logger.warning( "{0}: Transmit process is still alive, terminating it". format(self.__class__.__name__)) self.transmit_process.terminate() self.transmit_process.join() self.is_transmitting = False try: self.parent_ctrl_conn.close() except OSError as e: logger.exception(e) try: self.child_ctrl_conn.close() except OSError as e: logger.exception(e)
def read_receiving_queue(self): while self.is_receiving: try: byte_buffer = self.parent_data_conn.recv_bytes() samples = self.unpack_complex(byte_buffer) n_samples = len(samples) if n_samples == 0: continue except OSError as e: logger.exception(e) continue except EOFError: logger.info("EOF Error: Ending receive thread") break if self.current_recv_index + n_samples >= len(self.receive_buffer): if self.resume_on_full_receive_buffer: self.current_recv_index = 0 if n_samples >= len(self.receive_buffer): n_samples = len(self.receive_buffer) - 1 else: self.stop_rx_mode( "Receiving buffer is full {0}/{1}".format( self.current_recv_index + n_samples, len(self.receive_buffer))) return self.receive_buffer[self. current_recv_index:self.current_recv_index + n_samples] = samples[:n_samples] self.current_recv_index += n_samples logger.debug("Exiting read_receive_queue thread.")
def generate_file(self): try: total_samples = self.total_modulated_samples buffer = self.prepare_modulation_buffer(total_samples, show_error=False) if buffer is None: Errors.generic_error( self.tr("File too big"), self.tr("This file would get too big to save.")) self.unsetCursor() return modulated_samples = self.modulate_data(buffer) try: sample_rate = self.modulators[0].sample_rate except Exception as e: logger.exception(e) sample_rate = 1e6 FileOperator.save_data_dialog("generated.complex", modulated_samples, sample_rate=sample_rate, parent=self) except Exception as e: Errors.generic_error(self.tr("Failed to generate data"), str(e), traceback.format_exc()) self.unsetCursor()
def show_protocol_selection_in_interpretation(self, start_message, start, end_message, end): try: cfc = self.compare_frame_controller msg_total = 0 last_sig_frame = None for protocol in cfc.protocol_list: if not protocol.show: continue n = protocol.num_messages view_type = cfc.ui.cbProtoView.currentIndex() messages = [i - msg_total for i in range(msg_total, msg_total + n) if start_message <= i <= end_message] if len(messages) > 0: try: signal_frame = next((sf for sf, pf in self.signal_protocol_dict.items() if pf == protocol)) except StopIteration: QMessageBox.critical(self, self.tr("Error"), self.tr("Could not find corresponding signal frame.")) return signal_frame.set_roi_from_protocol_analysis(min(messages), start, max(messages), end + 1, view_type) last_sig_frame = signal_frame msg_total += n focus_frame = last_sig_frame if last_sig_frame is not None: self.signal_tab_controller.ui.scrollArea.ensureWidgetVisible(last_sig_frame, 0, 0) QApplication.instance().processEvents() self.ui.tabWidget.setCurrentIndex(0) if focus_frame is not None: focus_frame.ui.txtEdProto.setFocus() except Exception as e: logger.exception(e)
def init_device(cls, ctrl_connection: Connection, is_tx: bool, parameters: OrderedDict) -> bool: identifier = parameters["identifier"] try: device_list = sdrplay.get_devices() device_number = int(identifier) ctrl_connection.send("CONNECTED DEVICES: {}".format(", ".join( map(cls.device_dict_to_string, device_list)))) ret = sdrplay.set_device_index(device_number) ctrl_connection.send("SET DEVICE NUMBER to {}:{}".format( device_number, ret)) except (TypeError, ValueError) as e: logger.exception(e) return False device_model = device_list[device_number]["hw_version"] sdrplay.set_gr_mode_for_dev_model(device_model) if device_model == 2: antenna = parameters[cls.Command.SET_ANTENNA_INDEX.name] cls.process_command((cls.Command.SET_ANTENNA_INDEX.name, antenna), ctrl_connection, is_tx=False) else: ctrl_connection.send("Skipping antenna selection for RSP1 device") cls.sdrplay_initial_freq = parameters[cls.Command.SET_FREQUENCY.name] cls.sdrplay_initial_sample_rate = parameters[ cls.Command.SET_SAMPLE_RATE.name] cls.sdrplay_initial_bandwidth = parameters[ cls.Command.SET_BANDWIDTH.name] cls.sdrplay_initial_gain = parameters[cls.Command.SET_RF_GAIN.name] cls.sdrplay_initial_if_gain = parameters[cls.Command.SET_IF_GAIN.name] cls.sdrplay_device_index = identifier return True
def read_receiving_queue(self): while self.is_receiving: try: byte_buffer = self.parent_data_conn.recv_bytes() samples = self.unpack_complex(byte_buffer) n_samples = len(samples) if n_samples == 0: continue except OSError as e: logger.exception(e) continue except EOFError: logger.info("EOF Error: Ending receive thread") break if self.current_recv_index + n_samples >= len(self.receive_buffer): if self.resume_on_full_receive_buffer: self.current_recv_index = 0 if n_samples >= len(self.receive_buffer): n_samples = len(self.receive_buffer) - 1 else: self.stop_rx_mode( "Receiving buffer is full {0}/{1}".format(self.current_recv_index + n_samples, len(self.receive_buffer))) return self.receive_buffer[self.current_recv_index:self.current_recv_index + n_samples] = samples[:n_samples] self.current_recv_index += n_samples if self.emit_data_received_signal: self.data_received.emit(samples) logger.debug("Exiting read_receive_queue thread.")
def stop_tx_mode(self, msg): try: self.parent_ctrl_conn.send(self.Command.STOP.name) except (BrokenPipeError, OSError) as e: logger.debug("Closing parent control connection: " + str(e)) logger.info("{0}: Stopping TX Mode: {1}".format(self.__class__.__name__, msg)) if hasattr(self, "transmit_process") and self.transmit_process.is_alive(): self.transmit_process.join(self.JOIN_TIMEOUT) if self.transmit_process.is_alive(): logger.warning("{0}: Transmit process is still alive, terminating it".format(self.__class__.__name__)) self.transmit_process.terminate() self.transmit_process.join() self.is_transmitting = False try: self.parent_ctrl_conn.close() except OSError as e: logger.exception(e) try: self.child_ctrl_conn.close() except OSError as e: logger.exception(e)
def start(self): self.abort.value = 0 try: self.process = Process(target=self.modulate_continuously, args=(self.num_repeats, ), daemon=True) self.process.start() except RuntimeError as e: logger.exception(e)
def setup_device(cls, ctrl_connection: Connection, device_identifier): ctrl_connection.send("Initializing pyaudio...") try: cls.pyaudio_handle = pyaudio.PyAudio() ctrl_connection.send("Initialized pyaudio") return True except Exception as e: logger.exception(e) ctrl_connection.send("Failed to initialize pyaudio")
def run(self): logger.debug("Spectrum Thread: Init Process") self.initialize_process() logger.debug("Spectrum Thread: Process initialized") self.init_recv_socket() logger.debug("Spectrum Thread: Socket initialized") recv = self.socket.recv rcvd = b"" try: logger.debug("Spectrum Thread: Enter main loop") while not self.isInterruptionRequested(): try: rcvd += recv(32768) # Receive Buffer = 32768 Byte except Exception as e: logger.exception(e) if len(rcvd) < 8: self.stop("Stopped receiving, because no data transmitted anymore") return if len(rcvd) % 8 != 0: continue try: tmp = np.fromstring(rcvd, dtype=np.complex64) len_tmp = len(tmp) if self.data is None: self.data = np.zeros(self.buf_size, dtype=np.complex64) # type: np.ndarray if self.current_index + len_tmp >= len(self.data): self.data[self.current_index:] = tmp[:len(self.data) - self.current_index] tmp = tmp[len(self.data) - self.current_index:] w = np.abs(np.fft.fft(self.data)) freqs = np.fft.fftfreq(len(w), 1 / self.sample_rate) idx = np.argsort(freqs) self.x = freqs[idx].astype(np.float32) self.y = w[idx].astype(np.float32) self.data = np.zeros(len(self.data), dtype=np.complex64) self.data[0:len(tmp)] = tmp self.current_index = len(tmp) continue self.data[self.current_index:self.current_index + len_tmp] = tmp self.current_index += len_tmp rcvd = b"" except ValueError: self.stop("Could not receive data. Is your Hardware ok?") except RuntimeError as e: logger.error("Spectrum thread crashed", str(e.args))
def prepare_sync_send(cls, ctrl_connection: Connection): try: cls.pyaudio_stream = cls.pyaudio_handle.open(format=pyaudio.paFloat32, channels=2, rate=cls.SAMPLE_RATE, output=True) ctrl_connection.send("Successfully started pyaudio stream") return 0 except Exception as e: logger.exception(e) ctrl_connection.send("Failed to start pyaudio stream")
def on_graphics_view_save_as_clicked(self): filename = FileOperator.get_save_file_name("signal.complex") if filename: try: try: self.scene_manager.signal.sample_rate = self.device.sample_rate except Exception as e: logger.exception(e) self.scene_manager.signal.save_as(filename) except Exception as e: QMessageBox.critical(self, self.tr("Error saving signal"), e.args[0])
def prepare_sync_receive(cls, ctrl_connection: Connection): try: cls.pyaudio_stream = cls.pyaudio_handle.open(format=pyaudio.paFloat32, channels=2, rate=cls.SAMPLE_RATE, input=True, frames_per_buffer=cls.CHUNK_SIZE) ctrl_connection.send("Successfully started pyaudio stream") return 0 except Exception as e: logger.exception(e) ctrl_connection.send("Failed to start pyaudio stream")
def shutdown_device(cls, ctrl_connection, is_tx: bool): logger.debug("shutting down pyaudio...") try: if cls.pyaudio_stream: cls.pyaudio_stream.stop_stream() cls.pyaudio_stream.close() if cls.pyaudio_handle: cls.pyaudio_handle.terminate() ctrl_connection.send("CLOSE:0") except Exception as e: logger.exception(e) ctrl_connection.send("Failed to shut down pyaudio")
def prepare_sync_send(cls, ctrl_connection: Connection): try: cls.pyaudio_stream = cls.pyaudio_handle.open( format=pyaudio.paFloat32, channels=2, rate=cls.SAMPLE_RATE, output=True) ctrl_connection.send("Successfully started pyaudio stream") return 0 except Exception as e: logger.exception(e) ctrl_connection.send("Failed to start pyaudio stream")
def read_receiving_queue(self): my_num_samples = SettingsProxy.get_receive_buffer_size(False, False) self.my_receive_buffer = IQArray(None, dtype=self.DATA_TYPE, n=int(my_num_samples)) self.my_current_recv_index = 0 while self.is_receiving: try: byte_buffer = self.parent_data_conn.recv_bytes() samples = self.bytes_to_iq(byte_buffer) n_samples = len(samples) if n_samples == 0: continue if self.apply_dc_correction: samples = samples - np.mean(samples, axis=0) except OSError as e: logger.exception(e) continue except EOFError: logger.info("EOF Error: Ending receive thread") break if self.current_recv_index + n_samples >= len(self.receive_buffer): if self.resume_on_full_receive_buffer: self.current_recv_index = 0 if n_samples >= len(self.receive_buffer): n_samples = len(self.receive_buffer) - 1 else: self.stop_rx_mode( "Receiving buffer is full {0}/{1}".format( self.current_recv_index + n_samples, len(self.receive_buffer))) return self.receive_buffer[self. current_recv_index:self.current_recv_index + n_samples] = samples[:n_samples] self.current_recv_index += n_samples if self.my_acess_record_iq: self.my_receive_buffer[self.my_current_recv_index:self. my_current_recv_index + n_samples] = samples[:n_samples] self.my_current_recv_index += n_samples logger.debug("Exiting read_receive_queue thread.")
def on_btn_send_clicked(self): try: total_samples = self.total_modulated_samples buffer = self.prepare_modulation_buffer(total_samples) if buffer is not None: modulated_data = self.modulate_data(buffer) else: # Enter continuous mode modulated_data = None try: if modulated_data is not None: try: dialog = SendDialog( self.project_manager, modulated_data=modulated_data, modulation_msg_indices=self.modulation_msg_indices, parent=self) except MemoryError: # Not enough memory for device buffer so we need to create a continuous send dialog del modulated_data Errors.not_enough_ram_for_sending_precache(None) dialog = ContinuousSendDialog( self.project_manager, self.table_model.protocol.messages, self.modulators, total_samples, parent=self) else: dialog = ContinuousSendDialog( self.project_manager, self.table_model.protocol.messages, self.modulators, total_samples, parent=self) except OSError as e: logger.exception(e) return if dialog.has_empty_device_list: Errors.no_device() dialog.close() return dialog.device_parameters_changed.connect( self.project_manager.set_device_parameters) dialog.show() dialog.graphics_view.show_full_scene(reinitialize=True) except Exception as e: Errors.generic_error(self.tr("Failed to generate data"), str(e), traceback.format_exc()) self.unsetCursor()
def stop(self, clear_buffer=True): self.abort.value = 1 if self.process.is_alive(): try: self.process.join(1.5) except RuntimeError as e: logger.exception(e) self.process.terminate() if clear_buffer: self.ring_buffer.clear() logger.debug("Stopped continuous modulation")
def run(self): if self.data is None: self.init_recv_buffer() self.initialize_process() logger.info("Initialize receive socket") self.init_recv_socket() recv = self.socket.recv rcvd = b"" try: while not self.isInterruptionRequested(): try: rcvd += recv(32768) # Receive Buffer = 32768 Byte+ except Exception as e: logger.exception(e) if len(rcvd) < 8: self.stop("Stopped receiving: No data received anymore") return if len(rcvd) % 8 != 0: continue try: tmp = np.fromstring(rcvd, dtype=np.complex64) num_samples = len(tmp) if self.data is None: # seems to be sometimes None in rare cases self.init_recv_buffer() if self.current_index + num_samples >= len(self.data): if self.resume_on_full_receive_buffer: self.current_index = 0 if num_samples >= len(self.data): self.stop("Receiving buffer too small.") else: self.stop("Receiving Buffer is full.") return self.data[self.current_index:self.current_index + num_samples] = tmp self.current_index += num_samples rcvd = b"" except ValueError: self.stop("Could not receive data. Is your Hardware ok?") except RuntimeError: logger.error("Receiver Thread crashed.")
def generate_file(self): try: total_samples = self.total_modulated_samples buffer = self.prepare_modulation_buffer(total_samples, show_error=False) if buffer is None: Errors.generic_error(self.tr("File too big"), self.tr("This file would get too big to save.")) self.unsetCursor() return modulated_samples = self.modulate_data(buffer) try: sample_rate = self.modulators[0].sample_rate except Exception as e: logger.exception(e) sample_rate = 1e6 FileOperator.save_data_dialog("generated.complex", modulated_samples, sample_rate=sample_rate, parent=self) except Exception as e: Errors.generic_error(self.tr("Failed to generate data"), str(e), traceback.format_exc()) self.unsetCursor()
def set_shared_library_path(): shared_lib_dir = get_shared_library_path() if shared_lib_dir: if sys.platform == "win32": current_path = os.environ.get("PATH", '') if not current_path.startswith(shared_lib_dir): os.environ["PATH"] = shared_lib_dir + os.pathsep + current_path else: # LD_LIBRARY_PATH will not be considered at runtime so we explicitly load the .so's we need exts = [".so"] if sys.platform == "linux" else [".so", ".dylib"] import ctypes for lib in sorted(os.listdir(shared_lib_dir)): if any(lib.endswith(ext) for ext in exts): lib_path = os.path.join(shared_lib_dir, lib) if os.path.isfile(lib_path): try: ctypes.cdll.LoadLibrary(lib_path) except Exception as e: logger.exception(e)
def on_btn_send_clicked(self): try: total_samples = self.total_modulated_samples buffer = self.prepare_modulation_buffer(total_samples) if buffer is not None: modulated_data = self.modulate_data(buffer) else: # Enter continuous mode modulated_data = None try: if modulated_data is not None: try: dialog = SendDialog(self.project_manager, modulated_data=modulated_data, modulation_msg_indices=self.modulation_msg_indices, parent=self) except MemoryError: # Not enough memory for device buffer so we need to create a continuous send dialog del modulated_data Errors.not_enough_ram_for_sending_precache(None) dialog = ContinuousSendDialog(self.project_manager, self.table_model.protocol.messages, self.modulators, total_samples, parent=self) else: dialog = ContinuousSendDialog(self.project_manager, self.table_model.protocol.messages, self.modulators, total_samples, parent=self) except OSError as e: logger.exception(e) return if dialog.has_empty_device_list: Errors.no_device() dialog.close() return dialog.device_parameters_changed.connect(self.project_manager.set_device_parameters) dialog.show() dialog.graphics_view.show_full_scene(reinitialize=True) except Exception as e: Errors.generic_error(self.tr("Failed to generate data"), str(e), traceback.format_exc()) self.unsetCursor()
def generate_file(self): try: total_samples = self.total_modulated_samples buffer = self.prepare_modulation_buffer(total_samples, show_error=False) if buffer is None: Errors.generic_error( self.tr("File too big"), self.tr("This file would get too big to save.")) self.unsetCursor() return modulated_samples = self.modulate_data(buffer) try: sample_rate = self.modulators[0].sample_rate except Exception as e: logger.exception(e) sample_rate = 1e6 FileOperator.ask_signal_file_name_and_save("generated", modulated_samples, sample_rate=sample_rate, parent=self) except Exception as e: Errors.exception(e) self.unsetCursor()
def load_simulator_file(self, filename: str): try: tree = ET.parse(filename) self.load_config_from_xml_tag(tree.getroot(), update_before=False) except Exception as e: logger.exception(e)
def set_project_folder(self, path, ask_for_new_project=True, close_all=True): if self.project_file is not None or close_all: # Close existing project (if any) or existing files if requested self.main_controller.close_all_files() FileOperator.RECENT_PATH = path util.PROJECT_PATH = path self.project_path = path self.project_file = os.path.join(self.project_path, constants.PROJECT_FILE) collapse_project_tabs = False if not os.path.isfile(self.project_file): if ask_for_new_project: reply = QMessageBox.question( self.main_controller, "Project File", "Do you want to create a Project File for this folder?\n" "If you chose No, you can do it later via File->Convert Folder to Project.", QMessageBox.Yes | QMessageBox.No) if reply == QMessageBox.Yes: self.main_controller.show_project_settings() else: self.project_file = None if self.project_file is not None: root = ET.Element("UniversalRadioHackerProject") tree = ET.ElementTree(root) tree.write(self.project_file) self.modulation_was_edited = False else: tree = ET.parse(self.project_file) root = tree.getroot() collapse_project_tabs = bool( int(root.get("collapse_project_tabs", 0))) self.modulation_was_edited = bool( int(root.get("modulation_was_edited", 0))) cfc = self.main_controller.compare_frame_controller self.read_parameters(root) self.participants[:] = Participant.read_participants_from_xml_tag( xml_tag=root.find("protocol")) self.main_controller.add_files(self.read_opened_filenames()) self.read_compare_frame_groups(root) self.decodings = Encoding.read_decoders_from_xml_tag( root.find("protocol")) cfc.proto_analyzer.message_types[:] = self.read_message_types() cfc.message_type_table_model.update() cfc.proto_analyzer.from_xml_tag(root=root.find("protocol"), participants=self.participants, decodings=cfc.decodings) cfc.updateUI() try: for message_type in cfc.proto_analyzer.message_types: for lbl in filter(lambda x: not x.show, message_type): cfc.set_protocol_label_visibility(lbl) except Exception as e: logger.exception(e) self.modulators = self.read_modulators_from_project_file() self.main_controller.simulator_tab_controller.load_config_from_xml_tag( root.find("simulator_config")) if len(self.project_path) > 0 and self.project_file is None: self.main_controller.ui.actionConvert_Folder_to_Project.setEnabled( True) else: self.main_controller.ui.actionConvert_Folder_to_Project.setEnabled( False) self.main_controller.adjust_for_current_file(path) self.main_controller.filemodel.setRootPath(path) self.main_controller.ui.fileTree.setRootIndex( self.main_controller.file_proxy_model.mapFromSource( self.main_controller.filemodel.index(path))) self.main_controller.ui.fileTree.setToolTip(path) self.main_controller.ui.splitter.setSizes([1, 1]) if collapse_project_tabs: self.main_controller.collapse_project_tab_bar() else: self.main_controller.expand_project_tab_bar() self.main_controller.setWindowTitle("Universal Radio Hacker [" + path + "]") self.project_loaded_status_changed.emit(self.project_loaded) self.project_updated.emit()
def set_project_folder(self, path, ask_for_new_project=True, close_all=True): if self.project_file is not None or close_all: # Close existing project (if any) or existing files if requested self.main_controller.close_all_files() FileOperator.RECENT_PATH = path util.PROJECT_PATH = path self.project_path = path self.project_file = os.path.join(self.project_path, constants.PROJECT_FILE) collapse_project_tabs = False if not os.path.isfile(self.project_file): if ask_for_new_project: reply = QMessageBox.question(self.main_controller, "Project File", "Do you want to create a Project File for this folder?\n" "If you chose No, you can do it later via File->Convert Folder to Project.", QMessageBox.Yes | QMessageBox.No) if reply == QMessageBox.Yes: self.main_controller.show_project_settings() else: self.project_file = None if self.project_file is not None: root = ET.Element("UniversalRadioHackerProject") tree = ET.ElementTree(root) tree.write(self.project_file) self.modulation_was_edited = False else: tree = ET.parse(self.project_file) root = tree.getroot() collapse_project_tabs = bool(int(root.get("collapse_project_tabs", 0))) self.modulation_was_edited = bool(int(root.get("modulation_was_edited", 0))) cfc = self.main_controller.compare_frame_controller self.read_parameters(root) self.participants[:] = Participant.read_participants_from_xml_tag(xml_tag=root.find("protocol")) self.main_controller.add_files(self.read_opened_filenames()) self.read_compare_frame_groups(root) self.decodings = Encoding.read_decoders_from_xml_tag(root.find("protocol")) cfc.proto_analyzer.message_types[:] = self.read_message_types() cfc.message_type_table_model.update() cfc.proto_analyzer.from_xml_tag(root=root.find("protocol"), participants=self.participants, decodings=cfc.decodings) cfc.updateUI() try: for message_type in cfc.proto_analyzer.message_types: for lbl in filter(lambda x: not x.show, message_type): cfc.set_protocol_label_visibility(lbl) except Exception as e: logger.exception(e) self.modulators = self.read_modulators_from_project_file() self.main_controller.simulator_tab_controller.load_config_from_xml_tag(root.find("simulator_config")) if len(self.project_path) > 0 and self.project_file is None: self.main_controller.ui.actionConvert_Folder_to_Project.setEnabled(True) else: self.main_controller.ui.actionConvert_Folder_to_Project.setEnabled(False) self.main_controller.adjust_for_current_file(path) self.main_controller.filemodel.setRootPath(path) self.main_controller.ui.fileTree.setRootIndex( self.main_controller.file_proxy_model.mapFromSource(self.main_controller.filemodel.index(path))) self.main_controller.ui.fileTree.setToolTip(path) self.main_controller.ui.splitter.setSizes([1, 1]) if collapse_project_tabs: self.main_controller.collapse_project_tab_bar() else: self.main_controller.expand_project_tab_bar() self.main_controller.setWindowTitle("Universal Radio Hacker [" + path + "]") self.project_loaded_status_changed.emit(self.project_loaded) self.project_updated.emit()
def exception(exception: Exception): logger.exception(exception) w = QWidget() msg = "Error: <b>" + str(exception).replace("\n", "<br>") + "</b><hr>" msg += traceback.format_exc().replace("\n", "<br>") QMessageBox.critical(w, "An error occurred", msg)