# build command # pyinstaller --onefile --name DebugVarEdit --add-binary "build/libclang.dll;build" --distpath ./ --workpath ./build_temp --specpath ./build_temp var_setup_GUI.py import sys import os import subprocess import lxml.etree as ET from generate_debug_vars import type_map, choose_type_map from enum import IntEnum from tms_debugvar_term import _DemoWindow import threading from generate_debug_vars import run_generate import var_setup from var_selector_window import VariableSelectorDialog from var_table import VariableTableWidget, rows from scan_progress_gui import ProcessOutputWindow import scan_vars import myXML import time from PySide2.QtWidgets import ( QApplication, QWidget, QTableWidget, QTableWidgetItem, QCheckBox, QComboBox, QLineEdit, QVBoxLayout, QHBoxLayout, QPushButton, QCompleter, QAbstractItemView, QLabel, QMessageBox, QFileDialog, QTextEdit, QDialog, QTreeWidget, QTreeWidgetItem, QSizePolicy, QHeaderView, QMenuBar, QMenu, QAction ) from PySide2.QtGui import QTextCursor, QKeyEvent, QIcon, QFont from PySide2.QtCore import Qt, QProcess, QObject, Signal, QSettings var_edit_title = "Редактор переменных для отладки" xml_path_title = "Путь к XML:" proj_path_title = "Путь к проекту:" makefile_path_title = "Пусть к makefile (относительно проекта)" output_path_title = "Путь для для debug_vars.c:" scan_title = "Сканировать переменные" build_title = "Сгенерировать файл" add_vars_title = "Добавить переменные" open_output_title = "Открыть файл" def set_sub_elem_text(parent, tag, text): el = parent.find(tag) if el is None: el = ET.SubElement(parent, tag) el.text = str(text) # 3. UI: таблица с переменными class VarEditor(QWidget): def __init__(self): super().__init__() self.vars_list = [] self.structs = {} self.typedef_map = {} self.proj_path = None self.xml_path = None self.makefile_path = None self.structs_path = None self.output_path = None self._updating = False # Флаг блокировки рекурсии self._resizing = False # флаг блокировки повторного вызова self.target = 'TMS' self.initUI() def initUI(self): self.setWindowTitle(var_edit_title) base_path = scan_vars.get_base_path() icon_path = os.path.join(base_path, "icon.ico") if os.path.exists(icon_path): self.setWindowIcon(QIcon(icon_path)) # --- Поля ввода пути проекта и XML --- # XML Output xml_layout = QHBoxLayout() xml_layout.addWidget(QLabel(xml_path_title)) self.xml_output_edit = QLineEdit() self.xml_output_edit.returnPressed.connect(self.update) self.xml_output_edit.textChanged.connect(self.__on_xml_path_changed) xml_layout.addWidget(self.xml_output_edit) btn_xml_browse = QPushButton("...") btn_xml_browse.setFixedWidth(30) xml_layout.addWidget(btn_xml_browse) btn_xml_browse.clicked.connect(self.__browse_xml_output) # Project Path proj_layout = QHBoxLayout() proj_layout.addWidget(QLabel(proj_path_title)) self.proj_path_edit = QLineEdit() self.proj_path_edit.returnPressed.connect(self.update) self.proj_path_edit.textChanged.connect(self.__on_proj_path_changed) proj_layout.addWidget(self.proj_path_edit) btn_proj_browse = QPushButton("...") btn_proj_browse.setFixedWidth(30) proj_layout.addWidget(btn_proj_browse) btn_proj_browse.clicked.connect(self.__browse_proj_path) # Makefile Path makefile_layout = QHBoxLayout() makefile_layout.addWidget(QLabel(makefile_path_title)) self.makefile_edit = QLineEdit() self.makefile_edit.returnPressed.connect(self.update) self.makefile_edit.textChanged.connect(self.__on_makefile_path_changed) makefile_layout.addWidget(self.makefile_edit) btn_makefile_browse = QPushButton("...") btn_makefile_browse.setFixedWidth(30) makefile_layout.addWidget(btn_makefile_browse) btn_makefile_browse.clicked.connect(self.__browse_makefile) # Source Output File/Directory source_output_layout = QHBoxLayout() source_output_layout.addWidget(QLabel(output_path_title)) self.source_output_edit = QLineEdit() source_output_layout.addWidget(self.source_output_edit) btn_source_output_browse = QPushButton("...") btn_source_output_browse.setFixedWidth(30) source_output_layout.addWidget(btn_source_output_browse) btn_source_output_browse.clicked.connect(self.__browse_source_output) self.btn_update_vars = QPushButton(scan_title) self.btn_update_vars.clicked.connect(self.update_vars_data) # Добавляем чекбокс для выбора типовой карты # --- Создаем верхнее меню --- menubar = QMenuBar(self) menubar.setToolTip('Разные размеры int и кодировки файлов') self.target_menu = QMenu("МК:", menubar) # Создаем действия для выбора Target self.action_tms = QAction("TMS", self, checkable=True) self.action_stm = QAction("STM", self, checkable=True) # Инициализируем QSettings с именем организации и приложения self.settings = QSettings("SET", "DebugVarEdit_MainWindow") # Восстанавливаем сохранённое состояние, если есть mcu = self.settings.value("mcu_choosen", True, type=str) self.on_target_selected(mcu) self.target_menu.setToolTip(f'TMS: Размер int 16 бит. Кодировка cp1251\nSTM: Размер int 32 бита. Кодировка utf-8') # Группируем действия чтобы выбирался только один self.action_tms.triggered.connect(lambda: self.on_target_selected("TMS")) self.action_tms.setToolTip('Размер int 16 бит. Кодировка cp1251') self.action_stm.triggered.connect(lambda: self.on_target_selected("STM")) self.action_stm.setToolTip('Размер int 32 бита. Кодировка utf-8') self.target_menu.addAction(self.action_tms) self.target_menu.addAction(self.action_stm) self.terminal_menu = QMenu("Открыть Терминал", menubar) self.action_terminal_tms = QAction("TMS DemoTerminal", self) self.action_terminal_modbus = QAction("Modbus DemoTerminal", self) self.action_terminal_tms.triggered.connect(lambda: self.open_terminal("TMS")) self.action_terminal_modbus.triggered.connect(lambda: self.open_terminal("MODBUS")) self.terminal_menu.addAction(self.action_terminal_tms) #self.terminal_menu.addAction(self.action_terminal_modbus) menubar.addMenu(self.target_menu) menubar.addMenu(self.terminal_menu) # Кнопка сохранения btn_save = QPushButton(build_title) btn_save.clicked.connect(self.save_build) # Кнопка добавления переменных self.btn_add_vars = QPushButton(add_vars_title) self.btn_add_vars.clicked.connect(self.__open_variable_selector) # Кнопка открыть output-файл с выбором программы btn_open_output = QPushButton(open_output_title) btn_open_output.clicked.connect(self.__open_output_file_with_program) # Таблица self.table = VariableTableWidget() # Основной layout layout = QVBoxLayout() layout.setMenuBar(menubar) # прикрепляем menubar в layout сверху layout.addLayout(xml_layout) layout.addLayout(proj_layout) layout.addLayout(makefile_layout) layout.addWidget(self.btn_update_vars) layout.addWidget(self.table) layout.addWidget(self.btn_add_vars) layout.addLayout(source_output_layout) layout.addWidget(btn_save) layout.addWidget(btn_open_output) self.setLayout(layout) def open_terminal(self, target): target = target.lower() if target == "tms": self.terminal_widget = _DemoWindow() # _DemoWindow наследует QWidget self.terminal_widget.show() elif target == "modbus": a=1 def on_target_selected(self, target): self.target_menu.setTitle(f'МК: {target}') self.settings.setValue("mcu_choosen", target) self.target = target.lower() if self.target == "stm": choose_type_map(True) self.action_stm.setChecked(True) self.action_tms.setChecked(False) else: choose_type_map(False) self.action_tms.setChecked(True) self.action_stm.setChecked(False) def get_xml_path(self): xml_path = self.xml_output_edit.text().strip() return xml_path def get_proj_path(self): proj_path = self.proj_path_edit.text().strip() return proj_path def get_makefile_path(self): proj_path = self.get_proj_path() rel_makefile_path = self.makefile_edit.text().strip() if not rel_makefile_path: return None makefile_path = myXML.make_absolute_path(rel_makefile_path, proj_path) return makefile_path def get_struct_path(self): proj_path = self.get_proj_path() xml_path = self.get_xml_path() root, tree = myXML.safe_parse_xml(xml_path) if root is None: return # --- structs_path из атрибута --- structs_path = root.attrib.get('structs_path', '').strip() structs_path_full = myXML.make_absolute_path(structs_path, proj_path) if structs_path_full and os.path.isfile(structs_path_full): structs_path = structs_path_full else: structs_path = None return structs_path def get_output_path(self): output_path = os.path.abspath(self.source_output_edit.text().strip()) return output_path def update_all_paths(self): self.proj_path = self.get_proj_path() self.xml_path = self.get_xml_path() self.makefile_path = self.get_makefile_path() self.structs_path = self.get_struct_path() self.output_path = self.get_output_path() def update_vars_data(self): self.update_all_paths() if not self.proj_path or not self.xml_path: QMessageBox.warning(self, "Ошибка", "Укажите пути проекта и XML.") return if not os.path.isfile(self.makefile_path): QMessageBox.warning(self, "Ошибка", f"Makefile не найден:\n{self.makefile_path}") return # Создаём окно с кнопкой "Готово" self.proc_win = ProcessOutputWindow(self.proj_path, self.makefile_path, self.xml_path, self.__after_scan_vars_finished, self) self.proc_win.start_scan() def save_build(self): vars_out = [] for row in range(self.table.rowCount()): include_cb = self.table.cellWidget(row, rows.include) if not include_cb.isChecked(): continue name_edit = self.table.cellWidget(row, rows.name) pt_type_combo = self.table.cellWidget(row, rows.pt_type) iq_combo = self.table.cellWidget(row, rows.iq_type) ret_combo = self.table.cellWidget(row, rows.ret_type) short_name_edit = self.table.cellWidget(row, rows.short_name) var_data = { 'name': name_edit.text(), 'type': 'pt_' + pt_type_combo.currentText(), 'iq_type': 't_' + iq_combo.currentText(), 'return_type': 't_' + ret_combo.currentText() if ret_combo.currentText() else 't_iq_none', 'short_name': short_name_edit.text(), } vars_out.append(var_data) self.update_all_paths() if not self.proj_path or not self.xml_path or not self.output_path: QMessageBox.warning(self, "Ошибка", f"Заполните: {xml_path_title[:-1]}, {proj_path_title[:-1]}, {output_path_title[:-1]}.") return try: run_generate(self.proj_path, self.xml_path, self.output_path, self.table._shortname_size) QMessageBox.information(self, "Готово", "Файл debug_vars.c успешно сгенерирован.") self.update() except Exception as e: QMessageBox.critical(self, "Ошибка при генерации", str(e)) def update(self, force=0): if self._updating and (force==0): return # Уже в процессе обновления — выходим, чтобы избежать рекурсии self._updating = True self.update_all_paths() try: if self.xml_path and not os.path.isfile(self.xml_path): return try: root, tree = myXML.safe_parse_xml(self.xml_path) if root is None: return if not self.proj_path: # Если в поле ничего нет, пробуем взять из XML proj_path_from_xml = root.attrib.get('proj_path', '').strip() if proj_path_from_xml and os.path.isdir(proj_path_from_xml): self.proj_path = proj_path_from_xml self.proj_path_edit.setText(proj_path_from_xml) else: QMessageBox.warning( self, "Внимание", "Путь к проекту не найден или не существует.\n" f"Пожалуйста, укажите его вручную в поле '{proj_path_title[:-1]}'." ) else: if not os.path.isdir(self.proj_path): QMessageBox.warning( self, "Внимание", f"Указанный путь к проекту не существует:\n{self.proj_path}\n" "Пожалуйста, исправьте путь в поле '{proj_path_title[:-1]}'." ) if not self.makefile_path and self.proj_path and os.path.isdir(self.proj_path): makefile_path = root.attrib.get('makefile_path', '').strip() makefile_path_full = myXML.make_absolute_path(makefile_path, self.proj_path) if os.path.isfile(makefile_path_full): # Обновляем edit-поле на относительный путь, абсолют сохраняем self.makefile_path = makefile_path_full self.makefile_edit.setText(makefile_path) else: self.makefile_path = None self.makefile_edit.setText("") # --- structs_path из атрибута --- structs_path = root.attrib.get('structs_path', '').strip() structs_path_full = myXML.make_absolute_path(structs_path, self.proj_path) if structs_path_full and os.path.isfile(structs_path_full): self.structs_path = structs_path_full self.structs, self.typedef_map = var_setup.parse_structs(structs_path_full) else: self.structs_path = None self.vars_list = var_setup.parse_vars(self.xml_path, self.typedef_map) self.table.populate(self.vars_list, self.structs, self.write_to_xml) except Exception as e: QMessageBox.warning(self, "Ошибка", f"Ошибка при чтении XML:\n{e}") finally: self._updating = False # Снимаем блокировку при выходе из функции def __browse_proj_path(self): dir_path = QFileDialog.getExistingDirectory(self, "Выберите папку проекта") if dir_path: self.proj_path_edit.setText(dir_path) self.proj_path = dir_path # Сброс makefile, если proj_path изменился if not os.path.isdir(dir_path): self.makefile_path = None self.makefile_edit.setText("") else: if self.makefile_path and os.path.isfile(self.makefile_path): rel_path = myXML.make_relative_path(self.makefile_path, dir_path) self.makefile_edit.setText(rel_path) self.update() self.update() if self.makefile_path and self.proj_path: path = myXML.make_relative_path(self.makefile_path, self.proj_path) self.makefile_edit.setText(path) self.makefile_path = path def __browse_xml_output(self): dialog = QFileDialog(self, "Выберите или создайте XML-файл") dialog.setAcceptMode(QFileDialog.AcceptSave) dialog.setNameFilter("XML files (*.xml);;All Files (*)") dialog.setDefaultSuffix("xml") dialog.setOption(QFileDialog.DontConfirmOverwrite, True) # ⚠️ Не спрашивать про перезапись if dialog.exec_(): file_path = dialog.selectedFiles()[0] if not file_path.endswith(".xml"): file_path += ".xml" self.xml_output_edit.setText(file_path) self.xml_path = file_path def keyPressEvent(self, event: QKeyEvent): if event.key() == Qt.Key_Delete: self.delete_selected_rows() else: super().keyPressEvent(event) def __browse_makefile(self): if self.target == 'stm': file_filter = "Makefile или Keil-проект (*.uvprojx *.uvproj makefile);;Все файлы (*)" dialog_title = "Выберите Makefile или Keil-проект" else: # 'TMS' или по умолчанию file_filter = "Makefile (makefile);;Все файлы (*)" dialog_title = "Выберите Makefile" file_path, _ = QFileDialog.getOpenFileName( self, dialog_title, filter=file_filter ) if file_path: if self.proj_path: path = myXML.make_relative_path(file_path, self.proj_path) else: path = file_path self.makefile_edit.setText(path) self.makefile_path = path def __browse_source_output(self): dir_path = QFileDialog.getExistingDirectory(self, "Выберите папку для debug_vars.c") if dir_path: self.source_output_edit.setText(dir_path) self.output_path = dir_path else: self.output_path = '' def __on_xml_path_changed(self, _): self.xml_path = self.get_xml_path() self.update() def __on_proj_path_changed(self, _): self.proj_path = self.get_proj_path() if not os.path.isdir(self.proj_path): self.makefile_path = None self.makefile_edit.setText("") return # Преждевременно выходим, если проект не существует # Обновим путь к makefile, если он уже задан и абсолютен if self.makefile_path and os.path.isfile(self.makefile_path): rel_path = myXML.make_relative_path(self.makefile_path, self.proj_path) self.makefile_edit.setText(rel_path) self.update() def __on_makefile_path_changed(self, _): self.makefile_path = self.get_makefile_path() if self.makefile_path and self.proj_path: path = myXML.make_relative_path(self.makefile_path, self.proj_path) self.makefile_edit.setText(path) self.update() def __after_scan_vars_finished(self): if not os.path.isfile(self.xml_path): self.makefile_path = None self.structs_path = None self.proj_path = None QMessageBox.critical(self, "Ошибка", f"Файл не найден: {self.xml_path}") return try: self.update(1) except Exception as e: self.makefile_path = None self.structs_path = None self.proj_path = None QMessageBox.critical(self, "Ошибка", f"Не удалось загрузить переменные:\n{e}") def delete_selected_rows(self): # Получаем имена всех выбранных переменных из первого столбца selected_names = self.table.get_selected_var_names() if not selected_names: return # Меняем флаг show_var по имени for var in self.vars_list: if var.get('name') in selected_names: var['show_var'] = 'false' self.table.populate(self.vars_list, self.structs, self.write_to_xml) self.write_to_xml() def __open_variable_selector(self): self.update() if not self.vars_list: QMessageBox.warning(self, "Нет переменных", f"Сначала загрузите переменные ({scan_title}).") return dlg = VariableSelectorDialog(self.table, self.vars_list, self.structs, self.typedef_map, self.xml_path, self) if dlg.exec_(): self.write_to_xml() self.update() def write_to_xml(self, dummy=None): t0 = time.time() self.update_all_paths() def get_val(name, default=''): return str(v_table[name] if v_table and name in v_table else v.get(name, default)) def element_differs(elem, values: dict): for tag, new_val in values.items(): current_elem = elem.find(tag) current_val = (current_elem.text or '').strip() new_val_str = str(new_val or '').strip() if current_val != new_val_str: return True return False if not self.xml_path or not os.path.isfile(self.xml_path): print("XML файл не найден или путь пустой") return if not self.proj_path or not os.path.isdir(self.proj_path): print("Project path не найден или путь пустой") return if not self.makefile_path or not os.path.isfile(self.makefile_path): print("makefile файл не найден или путь пустой") return if os.path.abspath(self.makefile_path) == os.path.abspath(self.proj_path): print("makefile_path совпадает с proj_path — игнор") return try: root, tree = myXML.safe_parse_xml(self.xml_path) if root is None: return root.set("proj_path", self.proj_path.replace("\\", "/")) if self.makefile_path and os.path.isfile(self.makefile_path): rel_makefile = myXML.make_relative_path(self.makefile_path, self.proj_path) # Если результат — абсолютный путь, не записываем if not os.path.isabs(rel_makefile): root.set("makefile_path", rel_makefile) if self.structs_path and os.path.isfile(self.structs_path): rel_struct = myXML.make_relative_path(self.structs_path, self.proj_path) root.set("structs_path", rel_struct) if not os.path.isabs(rel_struct): root.set("structs_path", rel_struct) t1 = time.time() vars_elem = root.find('variables') if vars_elem is None: vars_elem = ET.SubElement(root, 'variables') original_info = {} for var_elem in vars_elem.findall('var'): name = var_elem.attrib.get('name') if name: original_info[name] = { 'file': var_elem.findtext('file', ''), 'extern': var_elem.findtext('extern', ''), 'static': var_elem.findtext('static', '') } var_elements_by_name = {ve.attrib.get('name'): ve for ve in vars_elem.findall('var')} # Читаем переменные из таблицы (активные/изменённые) table_vars = {v['name']: v for v in self.table.read_data()} # Все переменные (в том числе новые, которых нет в таблице) all_vars_by_name = {v['name']: v for v in self.vars_list} # Объединённый список переменных для записи all_names = list(all_vars_by_name.keys()) t2 = time.time() for name in all_names: v = all_vars_by_name[name] v_table = table_vars.get(name) var_elem = None pt_type_val = get_val('pt_type').lower() if 'arr' in pt_type_val or 'struct' in pt_type_val or 'union' in pt_type_val: continue show_var_val = str(v.get('show_var', 'false')).lower() enable_val = get_val('enable').lower() # Тут подтягиваем из таблицы, если есть, иначе из v shortname_val = get_val('shortname') iq_type_val = get_val('iq_type') ret_type_val = get_val('return_type') # file/extern/static: из original_info, либо из v file_val = v.get('file') or original_info.get(name, {}).get('file', '') extern_val = v.get('extern') or original_info.get(name, {}).get('extern', '') static_val = v.get('static') or original_info.get(name, {}).get('static', '') values_to_write = { 'show_var': show_var_val, 'enable': enable_val, 'shortname': shortname_val, 'pt_type': pt_type_val, 'iq_type': iq_type_val, 'return_type': ret_type_val, 'type': v.get('type', ''), 'file': file_val, 'extern': extern_val, 'static': static_val } # Ищем уже существующий в XML var_elem = var_elements_by_name.get(name) # Если элемента нет, это новая переменная — сразу пишем if var_elem is None: var_elem = ET.SubElement(vars_elem, 'var', {'name': name}) var_elements_by_name[name] = var_elem write_all = True # обязательно записать все поля else: write_all = element_differs(var_elem, values_to_write) if not write_all: continue # Пропускаем, если нет изменений for tag, text in values_to_write.items(): set_sub_elem_text(var_elem, tag, text) t3 = time.time() # Преобразуем дерево в строку myXML.fwrite(root, self.xml_path) self.table.check() t4 = time.time() '''print(f"[T1] parse + set paths: {t1 - t0:.3f} сек") print(f"[T2] prepare variables: {t2 - t1:.3f} сек") print(f"[T3] loop + updates: {t3 - t2:.3f} сек") print(f"[T4] write to file: {t4 - t3:.3f} сек") print(f"[TOTAL] write_to_xml total: {t4 - t0:.3f} сек")''' except Exception as e: print(f"Ошибка при сохранении XML: {e}") def __open_output_file_with_program(self): output_path = self.get_output_path() if not output_path: QMessageBox.warning(self, "Ошибка", "Путь к debug_var.c не задан." f"Пожалуйста, укажите его в поле '{output_path_title[:-1]}'.") return output_file = os.path.join(output_path, "debug_vars.c") if not os.path.isfile(output_file): QMessageBox.warning(self, "Ошибка", f"Файл не найден:\n{output_file}") return try: # Открыть стандартное окно Windows "Открыть с помощью..." subprocess.run([ "rundll32.exe", "shell32.dll,OpenAs_RunDLL", output_file ], shell=False) except Exception as e: QMessageBox.critical(self, "Ошибка", f"Не удалось открыть диалог 'Открыть с помощью':\n{e}") if __name__ == "__main__": app = QApplication(sys.argv) editor = VarEditor() editor.resize(900, 600) editor.show() sys.exit(app.exec_())