Compare commits
5 Commits
f89aff1b1c
...
v1.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
910bf0a585 | ||
|
|
502046091c | ||
|
|
e99de603e6 | ||
|
|
788ad19464 | ||
|
|
96496a0256 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,3 +6,4 @@
|
|||||||
/DebugVarEdit_GUI.dist
|
/DebugVarEdit_GUI.dist
|
||||||
/DebugVarEdit_GUI.onefile-build
|
/DebugVarEdit_GUI.onefile-build
|
||||||
/parse_xml/build/
|
/parse_xml/build/
|
||||||
|
/parse_xml/Src/__pycache__/
|
||||||
|
|||||||
BIN
DebugVarEdit.exe
BIN
DebugVarEdit.exe
Binary file not shown.
BIN
DebugVarTerminal.exe
Normal file
BIN
DebugVarTerminal.exe
Normal file
Binary file not shown.
142
README.md
142
README.md
@@ -1,4 +1,11 @@
|
|||||||
# DebugTools - Просмотр переменных по указателям
|
# DebugTools - Просмотр переменных по указателям
|
||||||
|
## Содержание
|
||||||
|
1. [Описание модуля](#программный-модуль-debugtools)
|
||||||
|
2. [Описание приложения для настройки](#debugvaredit---настройка-переменных)
|
||||||
|
3. [Описание терминалки для считывания](#debugvarterminal---считывание-переменных-для-tms)
|
||||||
|
4. [Для разработчиков](#для-разработчиков)
|
||||||
|
|
||||||
|
# Программный модуль DebugTools
|
||||||
Модуль состоит из трех файлов:
|
Модуль состоит из трех файлов:
|
||||||
- **debug_tools.c** - реализация считывания переменных
|
- **debug_tools.c** - реализация считывания переменных
|
||||||
- **debug_tools.h** - объявление всякого для считывания переменных
|
- **debug_tools.h** - объявление всякого для считывания переменных
|
||||||
@@ -9,11 +16,26 @@
|
|||||||
Для чтения переменных можно использовать функции:
|
Для чтения переменных можно использовать функции:
|
||||||
|
|
||||||
```c
|
```c
|
||||||
|
/* Читает значение переменной по индексу */
|
||||||
int Debug_ReadVar(int var_ind, int32_t *return_long);
|
int Debug_ReadVar(int var_ind, int32_t *return_long);
|
||||||
int Debug_ReadVarName(int var_ind, DebugVarName_t name_ptr);
|
/* Читает имя переменной по индексу */
|
||||||
```
|
int Debug_ReadVarName(int var_ind, DebugVarName_t name_ptr, int *length);
|
||||||
|
/* Читает возвращаемый тип (IQ) переменной по индексу */
|
||||||
|
int Debug_ReadVarReturnType(int var_ind, int *vartype);
|
||||||
|
/* Читает тип переменной по индексу */
|
||||||
|
int Debug_ReadVarType(int var_ind, int *vartype);
|
||||||
|
|
||||||
|
|
||||||
|
/* Читает значение переменной с нижнего уровня */
|
||||||
|
int Debug_LowLevel_ReadVar(int32_t *return_long);
|
||||||
|
/* Инициализирует отладку нижнего уровня */
|
||||||
|
int Debug_LowLevel_Initialize(DateTime_t *external_date);
|
||||||
|
/* Читает возвращаемый тип (IQ) низкоуровнено заданной переменной */
|
||||||
|
int Debug_LowLevel_ReadVarReturnType(int *vartype);
|
||||||
|
/* Читает тип низкоуровнено заданной переменной.*/
|
||||||
|
int Debug_LowLevel_ReadVarType(int *vartype);
|
||||||
|
```
|
||||||
|
|
||||||
Переменные доступные для чтения определяются в **debug_vars.c** (их можно прописывать вручную или генерировать через **DebugVarEdit**):
|
Переменные доступные для чтения определяются в **debug_vars.c** (их можно прописывать вручную или генерировать через **DebugVarEdit**):
|
||||||
|
|
||||||
```c
|
```c
|
||||||
@@ -32,6 +54,26 @@ DebugVar_t dbg_vars[] = {\
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## LowLevel - Просмотр абсолютно любых переменных
|
||||||
|
Также присутствует утилита `parse_xml.exe`, которая генерирует `.xml` файл с всеми переменными в программе и их адрессами
|
||||||
|
Для её подключения:
|
||||||
|
- В Pre-Build Steps добавить следующую команду. Это удаление `debug_tools.obj` файла для его перекомпиялции с актуальной датой компиляции
|
||||||
|
```
|
||||||
|
cmd /c del /Q "${CWD}\Src\DebugTools\debug_tools.obj"
|
||||||
|
```
|
||||||
|
- В Post-Build Steps добавить следующую команду. Это:
|
||||||
|
- формирование с помощью утилиты компилятора `ofd2000` `.xml` файла с полной отладочной информацией.
|
||||||
|
- формирование отладочной информации в текстовом файле (для получения таймштампа)
|
||||||
|
- запуск утилиты `parse_xml.exe` для формирования итогового `<projname>_allVars.xml` с информацией о переменных и адресах
|
||||||
|
|
||||||
|
```
|
||||||
|
"${CG_TOOL_ROOT}/bin/ofd2000" --obj_display=symbols --dwarf --dwarf_display=all --xml --xml_indent=1 --output="${BuildArtifactFileBaseName}_ofd_dump.xml" "${BuildArtifactFilePath}"
|
||||||
|
"${CG_TOOL_ROOT}/bin/ofd2000" "${BuildArtifactFilePath}" > ${CCS_PROJECT_DIR}/bin/temp.txt
|
||||||
|
"${CCS_PROJECT_DIR}/Src/DebugTools/parse_xml/parse_xml.exe" ${BuildArtifactFileBaseName}_ofd_dump.xml ${CCS_PROJECT_DIR}/bin/temp.txt ${BuildArtifactFileBaseName}_allVars.xml"
|
||||||
|
```
|
||||||
|
После, с использованием терминалки можно прочитать любые переменные по адресам. (должен совпадать таймштапм в прошивке и `.xml` файла, иначе контроллер ответит ошибкой)
|
||||||
|
|
||||||
|
|
||||||
# DebugVarEdit - Настройка переменных
|
# DebugVarEdit - Настройка переменных
|
||||||
**DebugVarEdit** — графическое приложение для Windows, предназначенное для настройки и генерации отладочных переменных (`debug_vars.c`) на основе исходного C-проекта. Работает с `makefile` проекта, сохраняет изменения в XML и позволяет удобно редактировать переменные и их типы через интерфейс.
|
**DebugVarEdit** — графическое приложение для Windows, предназначенное для настройки и генерации отладочных переменных (`debug_vars.c`) на основе исходного C-проекта. Работает с `makefile` проекта, сохраняет изменения в XML и позволяет удобно редактировать переменные и их типы через интерфейс.
|
||||||
|
|
||||||
@@ -113,6 +155,68 @@ DebugVar_t dbg_vars[] = {\
|
|||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
|
# DebugVarTerminal - Считывание переменных (для TMS)
|
||||||
|
**DebugVarTerminal** — терминалка для считывания переменных по RS-232 протоколу RS_Functions.
|
||||||
|
Программа — один исполняемый файл `DebugVarTerminal.exe`, не требующий установки и дополнительных зависимостей.
|
||||||
|
> Требуется Windows 7 или новее.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Как использовать приложение
|
||||||
|
|
||||||
|
1. Запустите **DebugVarTerminal.exe.**
|
||||||
|
|
||||||
|
2. Выберите COM Port и скорость Baud. Нажмиите **Open**
|
||||||
|
|
||||||
|
3. Для считывания переменных заданных в `*debug_vars.c`:
|
||||||
|
- Откройте вкладку **Watch**.
|
||||||
|
- Выберите стартовый индекс переменной и количество переменных для считывания.
|
||||||
|
- Нажмите одну из кнопок:
|
||||||
|
- **Update Service** - считывает информацию об именах и возвращаемых IQ типах переемнных
|
||||||
|
- **Read Value(s)** - считывает и форматирует значения переменных в соответствии с IQ типами
|
||||||
|
- **Start Polling** - начать опрос выбранных переменных с заданным интервалом. При старте опроса, имя и тип переменных считываются автоматически
|
||||||
|
|
||||||
|
4. Для считывания переменных по адресам:
|
||||||
|
- Откройте вкладку **LowLevel**.
|
||||||
|
- Выберите `projname_allVars.xml` в папке bin рядом с бинарником. Из него подгрузятся все доступные для считывания переменные
|
||||||
|
- Выберите переменные кнопкной **Выбрать переменные**
|
||||||
|
- Задайте IQ тип и возвращаемый IQ тип если требуется
|
||||||
|
- Нажмите одну из кнопок:
|
||||||
|
- **Read Once** - считывает выбранные переменные один раз
|
||||||
|
- **Start Polling** - начать опрос выбранных переменных с заданным интервалом
|
||||||
|
|
||||||
|
5. Запись в CSV:
|
||||||
|
- Можно записывавать считываемые переменные в CSV файл, с разделителем `;`
|
||||||
|
- Нажмите кнопку **Начать запись в CSV**
|
||||||
|
- Когда нужная выборка будет накоплена нажмите **Остаовить запись в CSV**
|
||||||
|
- Нажмите **Выбрать файл CSV** для выбора пути для сохранения файла и нажмите **Сохранить данные в CSV** чтобы сохранить
|
||||||
|
- Генерируется совместимый с LogView `.csv` файл
|
||||||
|
|
||||||
|
Все параметры выбираются из выпадающих списков, которые можно настроить, чтобы отображались только нужные опции.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Возможности
|
||||||
|
Режим "Watch":
|
||||||
|
- Быстрое и удобное чтение одной или нескольких переменных по их индексу.
|
||||||
|
- Автоматическое получение имен, типов и параметров масштабирования (IQ) переменных.
|
||||||
|
- Запуск постоянного опроса для отслеживания изменений значений в реальном времени.
|
||||||
|
- Наглядное представление данных в таблице с отображением как сырых, так и масштабированных значений.
|
||||||
|
Режим "LowLevel":
|
||||||
|
- Прямое чтение данных из памяти по заданным адресам.
|
||||||
|
- Возможность указания типов указателей, IQ-масштабирования и форматов возвращаемых данных.
|
||||||
|
- Аналогично режиму "Watch", поддерживается постоянный опрос выбранных низкоуровневых переменных.
|
||||||
|
- Умное автодополнение имён переменных и полей структур.
|
||||||
|
Логирование в CSV
|
||||||
|
- Записывайте все полученные значения в файл формата CSV для последующего анализа.
|
||||||
|
- Легкое управление записью: запуск, остановка и сохранение данных в любой момент.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Для разработчиков
|
# Для разработчиков
|
||||||
@@ -122,19 +226,25 @@ DebugVar_t dbg_vars[] = {\
|
|||||||
```bash
|
```bash
|
||||||
Src
|
Src
|
||||||
├── build/
|
├── build/
|
||||||
│ └── build_and_clean.py # Билдинг проекта в .exe (через nuitka или pyinstaller)
|
│ └── build_and_clean.py # Билдинг проекта в .exe (через nuitka или pyinstaller)
|
||||||
├── DebugVarEdit_GUI.py # Главное окно
|
├── DebugVarEdit_GUI.py # Главное окно
|
||||||
├── var_table.py # Таблица выбранных переменных
|
├── tms_debugvar_term.py # Терминал DebugVarTerminal
|
||||||
├── var_selector_window.py # Окно выбора переменных
|
├── tms_debugvar_lowlevel.py # Виджет для выбора переменных для LowLevel Watch
|
||||||
├── var_selector_table.py # Таблица переменных в окне выбора переменных
|
├── var_table.py # Таблица выбранных переменных
|
||||||
├── scan_progress_gui.py # Отображение процесса сканирования переменных
|
├── var_selector_window.py # Окно выбора переменных
|
||||||
├── scan_vars.py # Сканирование переменных среди .c/.h файлов
|
├── var_selector_table.py # Таблица переменных в окне выбора переменных
|
||||||
├── generate_debug_vars.py # Генерация debug_vars.c
|
├── scan_progress_gui.py # Отображение процесса сканирования переменных
|
||||||
├── myXML.py # Утилиты для XML
|
├── scan_vars.py # Сканирование переменных среди .c/.h файлов
|
||||||
├── makefile_parser.py # Парсинг makefile на .c/.h файлы
|
├── generate_debug_vars.py # Генерация debug_vars.c
|
||||||
├── var_setup.py # Подготовка переменных для окна выбора переменных
|
├── allvars_xml_parser.py # Парсинг XML со всеми переменными и структурами
|
||||||
├── libclang.dll # Бибилиотека clang
|
├── csv_logger.py # Логирование переменных в CSV
|
||||||
├── icon.ico # Иконка
|
├── myXML.py # Утилиты для XML
|
||||||
|
├── makefile_parser.py # Парсинг makefile на .c/.h файлы
|
||||||
|
├── var_setup.py # Подготовка переменных для окна выбора переменных
|
||||||
|
├── path_hints.py # Подсказки для автодополнения путей переменных
|
||||||
|
├── libclang.dll # Бибилиотека clang
|
||||||
|
├── icon.ico # Иконка
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Зависимости
|
### Зависимости
|
||||||
@@ -161,7 +271,7 @@ Src
|
|||||||
- Очищает временные папки после сборки:
|
- Очищает временные папки после сборки:
|
||||||
- `build_temp`
|
- `build_temp`
|
||||||
- `__pycache__`
|
- `__pycache__`
|
||||||
- `DebugVarEdit_GUI.*`
|
- `<MAIN_SCRIPT_NAME>.*`
|
||||||
|
|
||||||
> Все пути, имена файлов, временные папки и выбор между Nuitka и PyInstaller можно настроить в начале файла `build_and_clean.py`.
|
> Все пути, имена файлов, временные папки и выбор между Nuitka и PyInstaller можно настроить в начале файла `build_and_clean.py`.
|
||||||
|
|
||||||
|
|||||||
@@ -191,14 +191,39 @@ class VarEditor(QWidget):
|
|||||||
|
|
||||||
self.setLayout(layout)
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
|
||||||
def open_terminal(self, target):
|
def open_terminal(self, target):
|
||||||
target = target.lower()
|
target = target.lower()
|
||||||
|
|
||||||
if target == "tms":
|
if target == "tms":
|
||||||
self.terminal_widget = _DemoWindow() # _DemoWindow наследует QWidget
|
exe_name = "DebugVarTerminal.exe"
|
||||||
self.terminal_widget.show()
|
# Путь к exe в текущей директории запуска программы
|
||||||
|
exe_path = os.path.join(os.getcwd(), exe_name)
|
||||||
|
|
||||||
|
if not os.path.isfile(exe_path):
|
||||||
|
# Файл не найден — попросим пользователя выбрать путь к exe
|
||||||
|
msg = QMessageBox()
|
||||||
|
msg.setIcon(QMessageBox.Warning)
|
||||||
|
msg.setWindowTitle("Файл не найден")
|
||||||
|
msg.setText(f"Файл {exe_name} не найден в текущей папке.\nВыберите путь к {exe_name}.")
|
||||||
|
msg.exec_()
|
||||||
|
|
||||||
|
# Открываем диалог выбора файла
|
||||||
|
selected_path, _ = QFileDialog.getOpenFileName(
|
||||||
|
None, "Выберите файл " + exe_name, os.getcwd(), "Executable Files (*.exe)"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not selected_path:
|
||||||
|
# Пользователь отменил выбор — ничего не делаем
|
||||||
|
return
|
||||||
|
|
||||||
|
exe_path = selected_path
|
||||||
|
|
||||||
|
# Запускаем exe (отдельное окно терминала)
|
||||||
|
subprocess.Popen([exe_path], creationflags=subprocess.CREATE_NEW_CONSOLE)
|
||||||
|
|
||||||
elif target == "modbus":
|
elif target == "modbus":
|
||||||
a=1
|
a = 1
|
||||||
|
|
||||||
|
|
||||||
def on_target_selected(self, target):
|
def on_target_selected(self, target):
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
# Для разработчиков
|
# Для разработчиков
|
||||||
|
|
||||||
### Структура проекта:
|
### Структура проекта:
|
||||||
@@ -5,33 +6,39 @@
|
|||||||
```bash
|
```bash
|
||||||
Src
|
Src
|
||||||
├── build/
|
├── build/
|
||||||
│ └── build_and_clean.py # Билдинг проекта в .exe (через nuitka или pyinstaller)
|
│ └── build_and_clean.py # Билдинг проекта в .exe (через nuitka или pyinstaller)
|
||||||
├── DebugVarEdit_GUI.py # Главное окно
|
├── DebugVarEdit_GUI.py # Главное окно
|
||||||
├── var_table.py # Таблица выбранных переменных
|
├── tms_debugvar_term.py # Терминал DebugVarTerminal
|
||||||
├── var_selector_window.py # Окно выбора переменных
|
├── tms_debugvar_lowlevel.py # Виджет для выбора переменных для LowLevel Watch
|
||||||
├── var_selector_table.py # Таблица переменных в окне выбора переменных
|
├── var_table.py # Таблица выбранных переменных
|
||||||
├── scan_progress_gui.py # Отображение процесса сканирования переменных
|
├── var_selector_window.py # Окно выбора переменных
|
||||||
├── scan_vars.py # Сканирование переменных среди .c/.h файлов
|
├── var_selector_table.py # Таблица переменных в окне выбора переменных
|
||||||
├── generate_debug_vars.py # Генерация debug_vars.c
|
├── scan_progress_gui.py # Отображение процесса сканирования переменных
|
||||||
├── myXML.py # Утилиты для XML
|
├── scan_vars.py # Сканирование переменных среди .c/.h файлов
|
||||||
├── makefile_parser.py # Парсинг makefile на .c/.h файлы
|
├── generate_debug_vars.py # Генерация debug_vars.c
|
||||||
├── var_setup.py # Подготовка переменных для окна выбора переменных
|
├── allvars_xml_parser.py # Парсинг XML со всеми переменными и структурами
|
||||||
├── libclang.dll # Бибилиотека clang
|
├── csv_logger.py # Логирование переменных в CSV
|
||||||
├── icon.ico # Иконка
|
├── myXML.py # Утилиты для XML
|
||||||
|
├── makefile_parser.py # Парсинг makefile на .c/.h файлы
|
||||||
|
├── var_setup.py # Подготовка переменных для окна выбора переменных
|
||||||
|
├── path_hints.py # Подсказки для автодополнения путей переменных
|
||||||
|
├── libclang.dll # Бибилиотека clang
|
||||||
|
├── icon.ico # Иконка
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Зависимости
|
### Зависимости
|
||||||
|
|
||||||
Для запуска приложения:
|
Для запуска приложения:
|
||||||
- **Python 3.7+**
|
- **Python 3.7+**
|
||||||
- **clang** — используется для парсинга C-кода (требуется `libclang.dll`)
|
- **clang** — используется для парсинга C-кода (требуется `libclang.dll`, лежит в папке Src)
|
||||||
- **PySide2** — GUI-фреймворк
|
- **PySide2** — GUI-фреймворк
|
||||||
- **lxml** — работа с XML
|
- **lxml** — работа с XML
|
||||||
> Python 3.7 и PySide2 рекомендуется для совместимости с Windows 7
|
> Python 3.7 и PySide2 рекомендуется для совместимости с Windows 7
|
||||||
|
|
||||||
Для сборки `.exe`:
|
Для сборки `.exe`:
|
||||||
- **Nuitka** — создает полностью автономный `.exe` без внешних зависимостей
|
- **Nuitka** — создает полностью автономный `.exe` без внешних зависимостей
|
||||||
- **PyInstaller** — создает `.exe` с зависимостью от `pythonXX.dll` (Python должен быть установлен)
|
- **PyInstaller** — создает `.exe` с зависимостью от `pythonXX.dll` (Python должен быть установлен для запуска `.exe`)
|
||||||
|
|
||||||
|
|
||||||
### Сборка:
|
### Сборка:
|
||||||
@@ -44,12 +51,12 @@ Src
|
|||||||
- Очищает временные папки после сборки:
|
- Очищает временные папки после сборки:
|
||||||
- `build_temp`
|
- `build_temp`
|
||||||
- `__pycache__`
|
- `__pycache__`
|
||||||
- `DebugVarEdit_GUI.*`
|
- `<MAIN_SCRIPT_NAME>.*`
|
||||||
|
|
||||||
> Все пути, имена файлов, временные папки и выбор между Nuitka и PyInstaller можно настроить в начале файла `build_and_clean.py`.
|
> Все пути, имена файлов, временные папки и выбор между Nuitka и PyInstaller можно настроить в начале файла `build_and_clean.py`.
|
||||||
|
|
||||||
### Установка зависимостей
|
### Установка зависимостей
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install PySide2 lxml nuitka pyinstaller
|
pip install clang PySide2 lxml nuitka pyinstaller
|
||||||
```
|
```
|
||||||
500
Src/allvars_xml_parser.py
Normal file
500
Src/allvars_xml_parser.py
Normal file
@@ -0,0 +1,500 @@
|
|||||||
|
"""
|
||||||
|
VariablesXML + get_all_vars_data
|
||||||
|
---------------------------------
|
||||||
|
Поддержка вложенных структур, указателей на структуры ("->"),
|
||||||
|
и многомерных массивов (индексация [i][j]...).
|
||||||
|
|
||||||
|
Требования пользователя:
|
||||||
|
- size (без индекса) = общий размер массива в байтах (НЕ измерение!).
|
||||||
|
- size1..sizeN = размеры измерений массива.
|
||||||
|
- В результирующем плоском списке (flattened) должны присутствовать ВСЕ промежуточные
|
||||||
|
пути: var, var[0], var[0][0], var[0][0].field, var[0][0].field->subfield, ...
|
||||||
|
- Аналогично для членов структур.
|
||||||
|
|
||||||
|
Пример желаемого формата:
|
||||||
|
project
|
||||||
|
project.adc
|
||||||
|
project.adc[0]
|
||||||
|
project.adc[0][0]
|
||||||
|
project.adc[0][0].bus
|
||||||
|
project.adc[0][0].bus->status
|
||||||
|
|
||||||
|
Данный модуль реализует:
|
||||||
|
- Разбор XML (parse) с извлечением размеров размерностей в поле `dims`.
|
||||||
|
- Генерацию плоского списка словарей `flattened()`.
|
||||||
|
- Построение иерархии словарей `get_all_vars_data()`.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import List, Dict, Optional, Tuple, Any
|
||||||
|
|
||||||
|
import var_setup # ожидается split_path(...)
|
||||||
|
from generate_debug_vars import choose_type_map, type_map # используется для выбора карт типов
|
||||||
|
|
||||||
|
# --------------------------- константы ----------------------------
|
||||||
|
|
||||||
|
DATE_FIELD_SET = {"year", "month", "day", "hour", "minute"}
|
||||||
|
|
||||||
|
# --------------------------- dataclasses --------------------------
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MemberNode:
|
||||||
|
name: str
|
||||||
|
offset: int = 0
|
||||||
|
type_str: str = ""
|
||||||
|
size: Optional[int] = None # общий размер (байты), если известен
|
||||||
|
children: List["MemberNode"] = field(default_factory=list)
|
||||||
|
# --- доп.поля ---
|
||||||
|
kind: Optional[str] = None # 'array', 'union', ...
|
||||||
|
dims: Optional[List[int]] = None
|
||||||
|
|
||||||
|
def is_date_struct(self) -> bool:
|
||||||
|
if not self.children:
|
||||||
|
return False
|
||||||
|
child_names = {c.name for c in self.children}
|
||||||
|
return DATE_FIELD_SET.issubset(child_names)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class VariableNode:
|
||||||
|
name: str
|
||||||
|
address: int
|
||||||
|
type_str: str
|
||||||
|
size: Optional[int]
|
||||||
|
members: List[MemberNode] = field(default_factory=list)
|
||||||
|
# --- доп.поля ---
|
||||||
|
kind: Optional[str] = None # 'array'
|
||||||
|
dims: Optional[List[int]] = None # полный список размеров [size1, size2, ...]
|
||||||
|
|
||||||
|
def base_address_hex(self) -> str:
|
||||||
|
return f"0x{self.address:06X}"
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------- класс парсера -----------------------
|
||||||
|
|
||||||
|
class VariablesXML:
|
||||||
|
"""
|
||||||
|
Читает XML и предоставляет методы:
|
||||||
|
- flattened(): плоский список всех путей.
|
||||||
|
- date_struct_candidates(): как раньше.
|
||||||
|
|
||||||
|
Правила формирования путей:
|
||||||
|
* Структурные поля: '.'
|
||||||
|
* Поля через указатель на структуру: '->'
|
||||||
|
* Массивы: [index] (каждое измерение).
|
||||||
|
"""
|
||||||
|
|
||||||
|
# предполагаемые размеры примитивов (MCU: int=2)
|
||||||
|
_PRIM_SIZE = {
|
||||||
|
'char': 1, 'signed char': 1, 'unsigned char': 1,
|
||||||
|
'uint8_t': 1, 'int8_t': 1,
|
||||||
|
'short': 2, 'short int': 2, 'signed short': 2, 'unsigned short': 2,
|
||||||
|
'uint16_t': 2, 'int16_t': 2,
|
||||||
|
'int': 2, 'signed int': 2, 'unsigned int': 2,
|
||||||
|
'long': 4, 'unsigned long': 4, 'int32_t': 4, 'uint32_t': 4,
|
||||||
|
'float': 4,
|
||||||
|
'long long': 8, 'unsigned long long': 8, 'int64_t': 8, 'uint64_t': 8, 'double': 8,
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, path: str):
|
||||||
|
self.path = path
|
||||||
|
self.timestamp: str = ""
|
||||||
|
self.variables: List[VariableNode] = []
|
||||||
|
choose_type_map(0) # инициализация карт типов (если требуется)
|
||||||
|
self._parse()
|
||||||
|
|
||||||
|
# ------------------ утилиты ------------------
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parse_int_guess(txt: Optional[str]) -> Optional[int]:
|
||||||
|
if not txt:
|
||||||
|
return None
|
||||||
|
txt = txt.strip()
|
||||||
|
if txt.startswith(('0x', '0X')):
|
||||||
|
return int(txt, 16)
|
||||||
|
# если в строке есть A-F, попробуем hex
|
||||||
|
if any(c in 'abcdefABCDEF' for c in txt):
|
||||||
|
try:
|
||||||
|
return int(txt, 16)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
return int(txt, 10)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _is_pointer_to_struct(t: str) -> bool:
|
||||||
|
if not t:
|
||||||
|
return False
|
||||||
|
low = t.replace('\t', ' ').replace('\n', ' ')
|
||||||
|
return 'struct ' in low and '*' in low
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _is_struct_or_union(t: str) -> bool:
|
||||||
|
if not t:
|
||||||
|
return False
|
||||||
|
low = t.strip()
|
||||||
|
return low.startswith('struct ') or low.startswith('union ')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _is_union(t: str) -> bool:
|
||||||
|
if not t:
|
||||||
|
return False
|
||||||
|
low = t.strip()
|
||||||
|
return low.startswith('union ')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _strip_array_suffix(t: str) -> str:
|
||||||
|
t = t.strip()
|
||||||
|
while t.endswith('[]'):
|
||||||
|
t = t[:-2].strip()
|
||||||
|
return t
|
||||||
|
|
||||||
|
def _guess_primitive_size(self, type_str: str) -> Optional[int]:
|
||||||
|
if not type_str:
|
||||||
|
return None
|
||||||
|
base = type_str
|
||||||
|
for tok in ('volatile', 'const'):
|
||||||
|
base = base.replace(tok, '')
|
||||||
|
base = base.replace('*', ' ')
|
||||||
|
base = base.replace('[', ' ').replace(']', ' ')
|
||||||
|
base = ' '.join(base.split()).strip()
|
||||||
|
return self._PRIM_SIZE.get(base)
|
||||||
|
|
||||||
|
# ------------------ XML read ------------------
|
||||||
|
|
||||||
|
def _parse(self) -> None:
|
||||||
|
try:
|
||||||
|
tree = ET.parse(self.path)
|
||||||
|
root = tree.getroot()
|
||||||
|
|
||||||
|
ts = root.find('timestamp')
|
||||||
|
self.timestamp = ts.text.strip() if ts is not None and ts.text else ''
|
||||||
|
|
||||||
|
def parse_member(elem: ET.Element, base_offset=0) -> MemberNode:
|
||||||
|
name = elem.get('name', '')
|
||||||
|
offset = int(elem.get('offset', '0'), 16) if elem.get('offset') else 0
|
||||||
|
t = elem.get('type', '') or ''
|
||||||
|
size_attr = elem.get('size')
|
||||||
|
size = int(size_attr, 16) if size_attr else None
|
||||||
|
kind = elem.get('kind')
|
||||||
|
|
||||||
|
abs_offset = base_offset + offset
|
||||||
|
|
||||||
|
|
||||||
|
# Собираем размеры, если есть
|
||||||
|
dims: List[int] = []
|
||||||
|
i = 1
|
||||||
|
while True:
|
||||||
|
size_key = f"size{i}"
|
||||||
|
size_val = elem.get(size_key)
|
||||||
|
if size_val is None:
|
||||||
|
break
|
||||||
|
parsed = self._parse_int_guess(size_val) # предполагается твоя функция парсинга int
|
||||||
|
if parsed is not None:
|
||||||
|
dims.append(parsed)
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
node = MemberNode(
|
||||||
|
name=name,
|
||||||
|
offset=abs_offset,
|
||||||
|
type_str=t,
|
||||||
|
size=size,
|
||||||
|
kind=kind,
|
||||||
|
dims=dims if dims else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Для детей
|
||||||
|
for ch in elem.findall('member'):
|
||||||
|
if kind == 'union':
|
||||||
|
# Для union детей НЕ добавляем их offset, просто передаём abs_offset
|
||||||
|
child = parse_member(ch, base_offset=abs_offset)
|
||||||
|
child.offset = abs_offset # выравниваем offset, игнорируем offset детей
|
||||||
|
else:
|
||||||
|
# Для struct/array суммируем offset нормально
|
||||||
|
child = parse_member(ch, base_offset=abs_offset)
|
||||||
|
node.children.append(child)
|
||||||
|
|
||||||
|
# Аналогично для pointee
|
||||||
|
pointee_elem = elem.find('pointee')
|
||||||
|
if pointee_elem is not None:
|
||||||
|
for ch in pointee_elem.findall('member'):
|
||||||
|
if kind == 'union':
|
||||||
|
child = parse_member(ch, base_offset=abs_offset)
|
||||||
|
child.offset = abs_offset
|
||||||
|
else:
|
||||||
|
child = parse_member(ch, base_offset=abs_offset)
|
||||||
|
node.children.append(child)
|
||||||
|
size_p = pointee_elem.get('size')
|
||||||
|
if size_p:
|
||||||
|
node.size = int(size_p, 16)
|
||||||
|
|
||||||
|
return node
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
for var in root.findall('variable'):
|
||||||
|
addr = int(var.get('address', '0'), 16)
|
||||||
|
name = var.get('name', '')
|
||||||
|
t = var.get('type', '') or ''
|
||||||
|
size_attr = var.get('size') # общий размер байт
|
||||||
|
size = int(size_attr, 16) if size_attr else None
|
||||||
|
kind = var.get('kind')
|
||||||
|
|
||||||
|
dims: List[int] = []
|
||||||
|
i = 1
|
||||||
|
while True:
|
||||||
|
key = f'size{i}'
|
||||||
|
val = var.get(key)
|
||||||
|
if val is None:
|
||||||
|
break
|
||||||
|
parsed = self._parse_int_guess(val)
|
||||||
|
if parsed is not None:
|
||||||
|
dims.append(parsed)
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
members = [parse_member(m) for m in var.findall('member')]
|
||||||
|
|
||||||
|
v = VariableNode(
|
||||||
|
name=name,
|
||||||
|
address=addr,
|
||||||
|
type_str=t,
|
||||||
|
size=size,
|
||||||
|
members=members,
|
||||||
|
kind=kind,
|
||||||
|
dims=dims if dims else None,
|
||||||
|
)
|
||||||
|
self.variables.append(v)
|
||||||
|
except FileNotFoundError:
|
||||||
|
self.variables = []
|
||||||
|
except ET.ParseError:
|
||||||
|
self.variables = []
|
||||||
|
|
||||||
|
# ------------------ helpers для flattened ---------------------
|
||||||
|
|
||||||
|
def _elem_size_bytes(self, total_size: Optional[int], dims: List[int], base_type: str, members: List[MemberNode]) -> int:
|
||||||
|
"""Оценка размера одного *листового* элемента (последнего измерения).
|
||||||
|
Если total_size и dims все известны — берём size / prod(dims).
|
||||||
|
Иначе — пробуем примитивный размер; иначе 1.
|
||||||
|
(Не учитываем выравнивание структур; при необходимости можно расширить.)
|
||||||
|
"""
|
||||||
|
if total_size is not None and dims:
|
||||||
|
prod = 1
|
||||||
|
for d in dims:
|
||||||
|
if d is None or d == 0:
|
||||||
|
prod = None
|
||||||
|
break
|
||||||
|
prod *= d
|
||||||
|
if prod and prod > 0:
|
||||||
|
return max(1, total_size // prod)
|
||||||
|
prim = self._guess_primitive_size(base_type)
|
||||||
|
if prim:
|
||||||
|
return prim
|
||||||
|
# Если структура и у неё есть size по детям? Пока fallback=1.
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# ------------------ flattened ------------------
|
||||||
|
|
||||||
|
def flattened(self, max_array_elems: Optional[int] = None) -> List[Dict[str, Any]]:
|
||||||
|
"""Возвращает плоский список всех путей (каждый путь = dict).
|
||||||
|
Включает промежуточные узлы массивов (var[0], var[0][0], ...).
|
||||||
|
"""
|
||||||
|
out: List[Dict[str, Any]] = []
|
||||||
|
|
||||||
|
def mk(name: str, addr: Optional[int], type_str: str, size: Optional[int], kind: Optional[str], dims_for_node: Optional[List[int]]):
|
||||||
|
if 'Bender' in name:
|
||||||
|
a=1
|
||||||
|
out.append({
|
||||||
|
'name': name,
|
||||||
|
'address': addr,
|
||||||
|
'type': type_str,
|
||||||
|
'size': size,
|
||||||
|
'kind': kind,
|
||||||
|
'dims': dims_for_node[:] if dims_for_node else None,
|
||||||
|
})
|
||||||
|
|
||||||
|
def expand_members(prefix: str, base_addr: int, members: List[MemberNode], parent_is_ptr_struct: bool, parent_is_union: bool) -> None:
|
||||||
|
# Выбираем разделитель пути: '.' если обычный член, '->' если указатель на структуру
|
||||||
|
join = '->' if parent_is_ptr_struct else '.'
|
||||||
|
|
||||||
|
for m in members:
|
||||||
|
path_m = f"{prefix}{join}{m.name}" if prefix else m.name
|
||||||
|
is_union = m.kind == 'union' or parent_is_union
|
||||||
|
if is_union:
|
||||||
|
# Все поля union начинаются с одного адреса
|
||||||
|
addr_m = base_addr
|
||||||
|
else:
|
||||||
|
addr_m = base_addr + m.offset
|
||||||
|
|
||||||
|
dims = m.dims or []
|
||||||
|
|
||||||
|
mk(path_m, addr_m, m.type_str, m.size, m.kind, dims)
|
||||||
|
|
||||||
|
if m.kind == 'array' and dims:
|
||||||
|
base_t = self._strip_array_suffix(m.type_str)
|
||||||
|
elem_sz = m.size
|
||||||
|
|
||||||
|
# Для массива внутри структуры: первый уровень — '.' для доступа,
|
||||||
|
# внутри массива раскрываем по обычной логике с parent_is_ptr_struct=False
|
||||||
|
expand_dims(path_m, addr_m, dims, base_t, m.children, elem_sz, parent_is_ptr_struct=False)
|
||||||
|
else:
|
||||||
|
if m.children:
|
||||||
|
# Проверяем, является ли поле указателем на структуру
|
||||||
|
is_ptr = self._is_pointer_to_struct(m.type_str)
|
||||||
|
# Рекурсивно раскрываем дочерние поля, выбирая правильный разделитель
|
||||||
|
expand_members(path_m, addr_m, m.children, is_ptr, is_union)
|
||||||
|
|
||||||
|
|
||||||
|
def expand_dims(name: str, base_addr: int, dims: List[int], base_type: str, children: List[MemberNode], elem_size: int, parent_is_ptr_struct: bool) -> None:
|
||||||
|
prods: List[int] = []
|
||||||
|
acc = 1
|
||||||
|
for d in reversed(dims[1:]):
|
||||||
|
acc *= (d if d else 1)
|
||||||
|
prods.append(acc)
|
||||||
|
prods.reverse()
|
||||||
|
|
||||||
|
def rec(k: int, cur_name: str, cur_addr: int) -> None:
|
||||||
|
if k == len(dims):
|
||||||
|
# Листовой элемент массива
|
||||||
|
mk(cur_name, cur_addr, base_type, elem_size, None, None)
|
||||||
|
# Если элемент — структура или указатель на структуру, раскрываем вложения
|
||||||
|
if children and self._is_struct_or_union(base_type):
|
||||||
|
expand_members(cur_name, cur_addr, children, parent_is_ptr_struct=False, parent_is_union=self._is_union(base_type))
|
||||||
|
elif self._is_pointer_to_struct(base_type):
|
||||||
|
expand_members(cur_name, cur_addr, children, parent_is_ptr_struct=True, parent_is_union=self._is_union(base_type))
|
||||||
|
return
|
||||||
|
|
||||||
|
dim_sz = dims[k] or 0
|
||||||
|
if max_array_elems is not None:
|
||||||
|
dim_sz = min(dim_sz, max_array_elems)
|
||||||
|
|
||||||
|
stride = elem_size * prods[k] if k < len(prods) else elem_size
|
||||||
|
if len(dims) > 2:
|
||||||
|
a=1
|
||||||
|
for i in range(dim_sz):
|
||||||
|
child_name = f"{cur_name}[{i}]"
|
||||||
|
child_addr = (cur_addr + i * stride) if cur_addr is not None else None
|
||||||
|
remaining = dims[k+1:]
|
||||||
|
mk(child_name, child_addr, base_type + '[]' * len(remaining), stride if remaining else elem_size, 'array' if remaining else None, remaining)
|
||||||
|
rec(k + 1, child_name, child_addr)
|
||||||
|
|
||||||
|
rec(0, name, base_addr)
|
||||||
|
|
||||||
|
|
||||||
|
# --- цикл по топ‑левел переменным ---
|
||||||
|
for v in self.variables:
|
||||||
|
dims = v.dims or []
|
||||||
|
mk(v.name, v.address, v.type_str, v.size, v.kind, dims)
|
||||||
|
if (v.kind == 'array' or v.type_str.endswith('[]')) and dims:
|
||||||
|
base_t = self._strip_array_suffix(v.type_str)
|
||||||
|
elem_sz = v.size
|
||||||
|
expand_dims(v.name, v.address, dims, base_t, v.members, elem_sz, parent_is_ptr_struct=False)
|
||||||
|
else:
|
||||||
|
if v.members:
|
||||||
|
is_ptr = self._is_pointer_to_struct(v.type_str)
|
||||||
|
is_union = self._is_union(v.type_str)
|
||||||
|
expand_members(v.name, v.address, v.members, is_ptr, is_union)
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
# -------------------- date candidates (как раньше) -------------
|
||||||
|
|
||||||
|
def date_struct_candidates(self) -> List[Tuple[str, int]]:
|
||||||
|
cands = []
|
||||||
|
for v in self.variables:
|
||||||
|
# top level (if all date fields are present)
|
||||||
|
direct_names = {mm.name for mm in v.members}
|
||||||
|
if DATE_FIELD_SET.issubset(direct_names):
|
||||||
|
cands.append((v.name, v.address))
|
||||||
|
# check first-level members
|
||||||
|
for m in v.members:
|
||||||
|
if m.is_date_struct():
|
||||||
|
cands.append((f"{v.name}.{m.name}", v.address + m.offset))
|
||||||
|
return cands
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Построение иерархического дерева из flattened()
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def get_all_vars_data(self) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Строит иерархию словарей из плоского списка переменных.
|
||||||
|
|
||||||
|
Каждый узел = {
|
||||||
|
'name': <полный путь>,
|
||||||
|
'address': <адрес или None>,
|
||||||
|
'type': <тип>,
|
||||||
|
'size': <байты>,
|
||||||
|
'kind': <'array' | ...>,
|
||||||
|
'dims': [size1, size2, ...] или None,
|
||||||
|
'children': [...список дочерних узлов]
|
||||||
|
}
|
||||||
|
|
||||||
|
Возвращает список корневых узлов (top-level переменных).
|
||||||
|
"""
|
||||||
|
flat_data = self.flattened(max_array_elems=None)
|
||||||
|
|
||||||
|
# Быстрое отображение имя -> узел (словарь с детьми)
|
||||||
|
all_nodes: Dict[str, Dict[str, Any]] = {}
|
||||||
|
for item in flat_data:
|
||||||
|
node = dict(item)
|
||||||
|
node['children'] = []
|
||||||
|
all_nodes[item['name']] = node
|
||||||
|
|
||||||
|
def _parent_struct_split(path: str) -> Optional[str]:
|
||||||
|
# Ищем последний '.' или '->' для определения родителя
|
||||||
|
dot_idx = path.rfind('.')
|
||||||
|
arrow_idx = path.rfind('->')
|
||||||
|
cut_idx = max(dot_idx, arrow_idx)
|
||||||
|
if cut_idx == -1:
|
||||||
|
return None
|
||||||
|
# '->' занимает 2 символа, нужно взять срез до начала '->'
|
||||||
|
if arrow_idx > dot_idx:
|
||||||
|
return path[:arrow_idx]
|
||||||
|
else:
|
||||||
|
return path[:dot_idx]
|
||||||
|
|
||||||
|
def find_parent(path: str) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Возвращает полный путь родителя, учитывая '.', '->' и индексы [] в конце.
|
||||||
|
|
||||||
|
Если путь заканчивается индексом [k], удаляет последний индекс и проверяет наличие родителя.
|
||||||
|
Иначе пытается найти последний сепаратор '.' или '->'.
|
||||||
|
"""
|
||||||
|
# Если есть trailing индекс в конце, убираем его
|
||||||
|
m = re.search(r'\[[0-9]+\]$', path)
|
||||||
|
if m:
|
||||||
|
base = path[:m.start()] # убираем последний [k]
|
||||||
|
# Если базовый путь есть в узлах, считаем его родителем
|
||||||
|
if base in all_nodes:
|
||||||
|
return base
|
||||||
|
# Иначе пытаемся найти родителя от базового пути
|
||||||
|
return _parent_struct_split(base)
|
||||||
|
else:
|
||||||
|
# Если нет индекса, просто ищем последний разделитель
|
||||||
|
return _parent_struct_split(path)
|
||||||
|
|
||||||
|
# Строим иерархию: parent -> children
|
||||||
|
roots: List[Dict[str, Any]] = []
|
||||||
|
for full_name, node in all_nodes.items():
|
||||||
|
parent_name = find_parent(full_name)
|
||||||
|
if parent_name and parent_name in all_nodes:
|
||||||
|
all_nodes[parent_name]['children'].append(node)
|
||||||
|
else:
|
||||||
|
roots.append(node)
|
||||||
|
|
||||||
|
# Рекурсивно сортируем детей по имени для порядка
|
||||||
|
def sort_nodes(nodes: List[Dict[str, Any]]):
|
||||||
|
nodes.sort(key=lambda n: n['name'])
|
||||||
|
for n in nodes:
|
||||||
|
if n['children']:
|
||||||
|
sort_nodes(n['children'])
|
||||||
|
|
||||||
|
sort_nodes(roots)
|
||||||
|
return roots
|
||||||
@@ -12,10 +12,12 @@ from PyInstaller.utils.hooks import collect_data_files
|
|||||||
|
|
||||||
# === Конфигурация ===
|
# === Конфигурация ===
|
||||||
USE_NUITKA = True # True — сборка через Nuitka, False — через PyInstaller
|
USE_NUITKA = True # True — сборка через Nuitka, False — через PyInstaller
|
||||||
|
MAIN_SCRIPT_NAME = "tms_debugvar_term"
|
||||||
|
OUTPUT_NAME = "DebugVarTerminal"
|
||||||
|
|
||||||
|
|
||||||
SRC_PATH = Path("./Src/")
|
SRC_PATH = Path("./Src/")
|
||||||
SCRIPT_PATH = SRC_PATH / "DebugVarEdit_GUI.py"
|
SCRIPT_PATH = SRC_PATH / (MAIN_SCRIPT_NAME + ".py")
|
||||||
OUTPUT_NAME = "DebugVarEdit"
|
|
||||||
|
|
||||||
DIST_PATH = Path("./").resolve()
|
DIST_PATH = Path("./").resolve()
|
||||||
WORK_PATH = Path("./build_temp").resolve()
|
WORK_PATH = Path("./build_temp").resolve()
|
||||||
@@ -26,9 +28,9 @@ ICON_ICO_PATH = SRC_PATH / "icon.ico"
|
|||||||
TEMP_FOLDERS = [
|
TEMP_FOLDERS = [
|
||||||
"build_temp",
|
"build_temp",
|
||||||
"__pycache__",
|
"__pycache__",
|
||||||
"DebugVarEdit_GUI.build",
|
MAIN_SCRIPT_NAME + ".build",
|
||||||
"DebugVarEdit_GUI.onefile-build",
|
MAIN_SCRIPT_NAME + ".onefile-build",
|
||||||
"DebugVarEdit_GUI.dist"
|
MAIN_SCRIPT_NAME + ".dist"
|
||||||
]
|
]
|
||||||
# === Пути к DLL и прочим зависимостям ===
|
# === Пути к DLL и прочим зависимостям ===
|
||||||
LIBS = {
|
LIBS = {
|
||||||
|
|||||||
224
Src/csv_logger.py
Normal file
224
Src/csv_logger.py
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
import csv
|
||||||
|
import numbers
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
from PySide2 import QtWidgets
|
||||||
|
|
||||||
|
|
||||||
|
class CsvLogger:
|
||||||
|
"""
|
||||||
|
Логгер, совместимый по формату с C-реализацией CSV_AddTitlesLine / CSV_AddLogLine.
|
||||||
|
|
||||||
|
Публичный API сохранён:
|
||||||
|
set_titles(varnames)
|
||||||
|
set_value(timestamp, varname, varvalue)
|
||||||
|
select_file(parent=None) -> bool
|
||||||
|
write_to_csv()
|
||||||
|
|
||||||
|
Использование:
|
||||||
|
1) set_titles([...])
|
||||||
|
2) многократно set_value(ts, name, value)
|
||||||
|
3) select_file() (по желанию)
|
||||||
|
4) write_to_csv()
|
||||||
|
"""
|
||||||
|
def __init__(self, filename="log.csv", delimiter=';'):
|
||||||
|
self._filename = filename
|
||||||
|
self._delimiter = delimiter
|
||||||
|
|
||||||
|
# Пользовательские заголовки
|
||||||
|
self.variable_names_ordered = []
|
||||||
|
# Полные заголовки CSV (Ticks(X), Ticks(Y), Time(Y), ...)
|
||||||
|
self.headers = ['t'] # до вызова set_titles placeholder
|
||||||
|
|
||||||
|
# Данные: {timestamp_key: {varname: value, ...}}
|
||||||
|
# timestamp_key = то, что передано в set_value (float/int/etc)
|
||||||
|
self.data_rows = {}
|
||||||
|
|
||||||
|
# Внутренние структуры для генерации CSV-формата С
|
||||||
|
self._row_wall_dt = {} # {timestamp_key: datetime при первой записи}
|
||||||
|
self._base_ts = None # timestamp_key первой строки (число)
|
||||||
|
self._base_ts_val = 0.0 # float значение первой строки (для delta)
|
||||||
|
self._tick_x_start = 0 # начальный тик (можно менять вручную при необходимости)
|
||||||
|
|
||||||
|
# ---- Свойства ----
|
||||||
|
@property
|
||||||
|
def filename(self):
|
||||||
|
return self._filename
|
||||||
|
|
||||||
|
# ---- Публичные методы ----
|
||||||
|
def set_titles(self, varnames):
|
||||||
|
"""
|
||||||
|
Устанавливает имена переменных.
|
||||||
|
Формирует полные заголовки CSV в формате С-лога.
|
||||||
|
"""
|
||||||
|
if not isinstance(varnames, list):
|
||||||
|
raise TypeError("Varnames must be a list of strings.")
|
||||||
|
if not all(isinstance(name, str) for name in varnames):
|
||||||
|
raise ValueError("All variable names must be strings.")
|
||||||
|
|
||||||
|
self.variable_names_ordered = varnames
|
||||||
|
self.headers = ["Ticks(X)", "Ticks(Y)", "Time(Y)"] + self.variable_names_ordered
|
||||||
|
|
||||||
|
# Сброс данных (структура изменилась)
|
||||||
|
self.data_rows.clear()
|
||||||
|
self._row_wall_dt.clear()
|
||||||
|
self._base_ts = None
|
||||||
|
self._base_ts_val = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def set_value(self, timestamp, varname, varvalue):
|
||||||
|
"""
|
||||||
|
Установить ОДНО значение в ОДНУ колонку для заданного timestamp’а.
|
||||||
|
timestamp — float секунд с эпохи (time.time()).
|
||||||
|
"""
|
||||||
|
if varname not in self.variable_names_ordered:
|
||||||
|
return # игнор, как у тебя было
|
||||||
|
|
||||||
|
# Новая строка?
|
||||||
|
if timestamp not in self.data_rows:
|
||||||
|
# Инициализируем поля переменных значением None
|
||||||
|
self.data_rows[timestamp] = {vn: None for vn in self.variable_names_ordered}
|
||||||
|
|
||||||
|
# Дата/время строки из ПЕРЕДАННОГО timestamp (а не datetime.now()!)
|
||||||
|
try:
|
||||||
|
ts_float = float(timestamp)
|
||||||
|
except Exception:
|
||||||
|
# если какая-то дичь прилетела, пусть будет 0 (эпоха) чтобы не упасть
|
||||||
|
ts_float = 0.0
|
||||||
|
self._row_wall_dt[timestamp] = datetime.fromtimestamp(ts_float)
|
||||||
|
|
||||||
|
# База для расчёта Ticks(Y) — первая строка
|
||||||
|
if self._base_ts is None:
|
||||||
|
self._base_ts = timestamp
|
||||||
|
self._base_ts_val = ts_float
|
||||||
|
|
||||||
|
# Записываем значение
|
||||||
|
self.data_rows[timestamp][varname] = varvalue
|
||||||
|
|
||||||
|
def select_file(self, parent=None) -> bool:
|
||||||
|
"""
|
||||||
|
Диалог выбора файла.
|
||||||
|
"""
|
||||||
|
options = QtWidgets.QFileDialog.Options()
|
||||||
|
filename, _ = QtWidgets.QFileDialog.getSaveFileName(
|
||||||
|
parent,
|
||||||
|
"Сохранить данные CSV",
|
||||||
|
self._filename,
|
||||||
|
"CSV Files (*.csv);;All Files (*)",
|
||||||
|
options=options
|
||||||
|
)
|
||||||
|
if filename:
|
||||||
|
if not filename.lower().endswith('.csv'):
|
||||||
|
filename += '.csv'
|
||||||
|
self._filename = filename
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def write_to_csv(self):
|
||||||
|
"""
|
||||||
|
Формирует CSV в формате C:
|
||||||
|
Ticks(X);Ticks(Y);Time(Y);Var1;Var2;...
|
||||||
|
0;0,000000;22/07/2025 13:45:12:0123;...;...
|
||||||
|
|
||||||
|
Правила значений:
|
||||||
|
- Тик X: автоинкремент от 0 (или self._tick_x_start) по порядку сортировки timestamp.
|
||||||
|
- Ticks(Y): дельта (секунды,микросекунды) между текущим timestamp и первым timestamp.
|
||||||
|
- Time(Y): wallclock строки (datetime.now() при первом появлении timestamp).
|
||||||
|
- Значение < 0 -> пустая ячейка (как if(raw_data[i] >= 0) else ;)
|
||||||
|
- None -> пустая ячейка.
|
||||||
|
"""
|
||||||
|
if len(self.headers) <= 3: # только служебные поля без переменных
|
||||||
|
print("Ошибка: Заголовки не установлены или не содержат переменных. Вызовите set_titles() перед записью.")
|
||||||
|
return
|
||||||
|
if not self._filename:
|
||||||
|
print("Ошибка: Имя файла не определено. select_file() или задайте при инициализации.")
|
||||||
|
return
|
||||||
|
if not self.data_rows:
|
||||||
|
print("Предупреждение: Нет данных для записи.")
|
||||||
|
# всё равно создадим файл с одними заголовками
|
||||||
|
try:
|
||||||
|
with open(self._filename, 'w', newline='', encoding='utf-8') as csvfile:
|
||||||
|
# QUOTE_NONE + escapechar для чистого формата без кавычек (как в С-строке)
|
||||||
|
writer = csv.writer(
|
||||||
|
csvfile,
|
||||||
|
delimiter=self._delimiter,
|
||||||
|
quoting=csv.QUOTE_NONE,
|
||||||
|
escapechar='\\',
|
||||||
|
lineterminator='\r\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Пишем заголовки
|
||||||
|
writer.writerow(self.headers)
|
||||||
|
|
||||||
|
if self.data_rows:
|
||||||
|
sorted_ts = sorted(self.data_rows.keys(), key=self._ts_sort_key)
|
||||||
|
# убедимся, что база была зафиксирована
|
||||||
|
if self._base_ts is None:
|
||||||
|
self._base_ts = sorted_ts[0]
|
||||||
|
self._base_ts_val = self._coerce_ts_to_float(self._base_ts)
|
||||||
|
|
||||||
|
tick_x = self._tick_x_start
|
||||||
|
for ts in sorted_ts:
|
||||||
|
row_dict = self.data_rows[ts]
|
||||||
|
# delta по timestamp
|
||||||
|
cur_ts_val = self._coerce_ts_to_float(ts)
|
||||||
|
delta_us = int(round((cur_ts_val - self._base_ts_val) * 1_000_000))
|
||||||
|
if delta_us < 0:
|
||||||
|
delta_us = 0 # защита
|
||||||
|
|
||||||
|
seconds = delta_us // 1_000_000
|
||||||
|
micros = delta_us % 1_000_000
|
||||||
|
|
||||||
|
# wallclock строки
|
||||||
|
dt = self._row_wall_dt.get(ts, datetime.now())
|
||||||
|
# Формат DD/MM/YYYY HH:MM:SS:мммм (4 цифры ms, как в C: us/1000)
|
||||||
|
time_str = dt.strftime("%d/%m/%Y %H:%M:%S") + f":{dt.microsecond // 1000:04d}"
|
||||||
|
|
||||||
|
# Значения
|
||||||
|
row_vals = []
|
||||||
|
for vn in self.variable_names_ordered:
|
||||||
|
v = row_dict.get(vn)
|
||||||
|
if v is None:
|
||||||
|
row_vals.append("") # нет данных
|
||||||
|
else:
|
||||||
|
# если числовое и <0 -> пусто (как в C: если raw_data[i] >= 0 else ;)
|
||||||
|
if isinstance(v, numbers.Number) and v < 0:
|
||||||
|
row_vals.append("")
|
||||||
|
else:
|
||||||
|
row_vals.append(v)
|
||||||
|
|
||||||
|
csv_row = [tick_x, f"{seconds},{micros:06d}", time_str] + row_vals
|
||||||
|
writer.writerow(csv_row)
|
||||||
|
tick_x += 1
|
||||||
|
|
||||||
|
print(f"Данные успешно записаны в '{self._filename}'")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка при записи в файл '{self._filename}': {e}")
|
||||||
|
|
||||||
|
# ---- Вспомогательные ----
|
||||||
|
def _coerce_ts_to_float(self, ts):
|
||||||
|
"""
|
||||||
|
Пробуем привести переданный timestamp к float.
|
||||||
|
Разрешаем int/float/str, остальное -> индекс по порядку (0).
|
||||||
|
"""
|
||||||
|
if isinstance(ts, numbers.Number):
|
||||||
|
return float(ts)
|
||||||
|
try:
|
||||||
|
return float(ts)
|
||||||
|
except Exception:
|
||||||
|
# fallback: нечисловой ключ -> используем порядковый индекс
|
||||||
|
# (таких почти не должно быть, но на всякий)
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def _ts_sort_key(self, ts):
|
||||||
|
"""
|
||||||
|
Ключ сортировки timestamp’ов — сначала попытка float, потом str.
|
||||||
|
"""
|
||||||
|
if isinstance(ts, numbers.Number):
|
||||||
|
return (0, float(ts))
|
||||||
|
try:
|
||||||
|
return (0, float(ts))
|
||||||
|
except Exception:
|
||||||
|
return (1, str(ts))
|
||||||
|
|
||||||
@@ -59,6 +59,47 @@ def split_path_tokens(path: str) -> List[str]:
|
|||||||
tokens.append(token)
|
tokens.append(token)
|
||||||
return tokens
|
return tokens
|
||||||
|
|
||||||
|
def split_path_tokens_with_spans(path: str) -> List[Tuple[str, int, int]]:
|
||||||
|
"""
|
||||||
|
Возвращает список кортежей (токен, start_pos, end_pos)
|
||||||
|
Токены — так же, как в split_path_tokens, но с позициями в исходной строке.
|
||||||
|
"""
|
||||||
|
tokens = []
|
||||||
|
i = 0
|
||||||
|
L = len(path)
|
||||||
|
while i < L:
|
||||||
|
c = path[i]
|
||||||
|
start = i
|
||||||
|
# '->'
|
||||||
|
if c == '-' and i + 1 < L and path[i:i+2] == '->':
|
||||||
|
tokens.append(('->', start, start + 2))
|
||||||
|
i += 2
|
||||||
|
continue
|
||||||
|
if c == '.':
|
||||||
|
tokens.append(('.', start, start + 1))
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
if c == '[':
|
||||||
|
# захватим весь индекс с ']'
|
||||||
|
j = i
|
||||||
|
while j < L and path[j] != ']':
|
||||||
|
j += 1
|
||||||
|
if j < L and path[j] == ']':
|
||||||
|
j += 1
|
||||||
|
tokens.append((path[i:j], i, j))
|
||||||
|
i = j
|
||||||
|
continue
|
||||||
|
# иначе - обычное имя (до точки, стрелки или скобок)
|
||||||
|
j = i
|
||||||
|
while j < L and path[j] not in ['.', '-', '[']:
|
||||||
|
if path[j] == '-' and j + 1 < L and path[j:j+2] == '->':
|
||||||
|
break
|
||||||
|
j += 1
|
||||||
|
tokens.append((path[i:j], i, j))
|
||||||
|
i = j
|
||||||
|
# фильтруем из списка токены-разделители '.' и '->' чтобы оставить только логические части
|
||||||
|
filtered = [t for t in tokens if t[0] not in ['.', '->']]
|
||||||
|
return filtered
|
||||||
|
|
||||||
def canonical_key(path: str) -> str:
|
def canonical_key(path: str) -> str:
|
||||||
"""
|
"""
|
||||||
@@ -92,6 +133,11 @@ class PathNode:
|
|||||||
def add_child(self, child: "PathNode") -> None:
|
def add_child(self, child: "PathNode") -> None:
|
||||||
self.children[child.name] = child
|
self.children[child.name] = child
|
||||||
|
|
||||||
|
def get_children(self) -> List["PathNode"]:
|
||||||
|
"""
|
||||||
|
Вернуть список дочерних узлов, отсортированных по имени.
|
||||||
|
"""
|
||||||
|
return sorted(self.children.values(), key=lambda n: n.name)
|
||||||
|
|
||||||
class PathHints:
|
class PathHints:
|
||||||
"""
|
"""
|
||||||
@@ -132,28 +178,19 @@ class PathHints:
|
|||||||
self._paths.append(full_path)
|
self._paths.append(full_path)
|
||||||
self._types[full_path] = type_str
|
self._types[full_path] = type_str
|
||||||
|
|
||||||
toks = split_path_tokens(full_path)
|
tokens_spans = split_path_tokens_with_spans(full_path)
|
||||||
if not toks:
|
if not tokens_spans:
|
||||||
return
|
return
|
||||||
|
|
||||||
cur_dict = self._root_children
|
cur_dict = self._root_children
|
||||||
cur_full = ''
|
cur_full = ''
|
||||||
parent_node: Optional[PathNode] = None
|
parent_node: Optional[PathNode] = None
|
||||||
|
|
||||||
for i, tok in enumerate(toks):
|
for i, (tok, start, end) in enumerate(tokens_spans):
|
||||||
# Собираем ПОЛНЫЙ путь
|
cur_full = full_path[:end] # подстрока с начала до конца токена включительно
|
||||||
if cur_full == '':
|
|
||||||
cur_full = tok
|
|
||||||
else:
|
|
||||||
if tok.startswith('['):
|
|
||||||
cur_full += tok
|
|
||||||
else:
|
|
||||||
cur_full += '.' + tok
|
|
||||||
|
|
||||||
# Если узел уже есть
|
|
||||||
node = cur_dict.get(tok)
|
node = cur_dict.get(tok)
|
||||||
if node is None:
|
if node is None:
|
||||||
# --- ВАЖНО: full_path = cur_full ---
|
|
||||||
node = PathNode(name=tok, full_path=cur_full)
|
node = PathNode(name=tok, full_path=cur_full)
|
||||||
cur_dict[tok] = node
|
cur_dict[tok] = node
|
||||||
|
|
||||||
@@ -173,6 +210,15 @@ class PathHints:
|
|||||||
def find_node(self, path: str) -> Optional[PathNode]:
|
def find_node(self, path: str) -> Optional[PathNode]:
|
||||||
return self._index.get(canonical_key(path))
|
return self._index.get(canonical_key(path))
|
||||||
|
|
||||||
|
def get_children(self, full_path: str) -> List[PathNode]:
|
||||||
|
"""
|
||||||
|
Вернуть список дочерних узлов PathNode для заданного полного пути.
|
||||||
|
Если узел не найден — вернуть пустой список.
|
||||||
|
"""
|
||||||
|
node = self.find_node(full_path)
|
||||||
|
if node is None:
|
||||||
|
return []
|
||||||
|
return node.get_children()
|
||||||
# ------------ Подсказки ------------
|
# ------------ Подсказки ------------
|
||||||
|
|
||||||
def suggest(self,
|
def suggest(self,
|
||||||
@@ -227,6 +273,27 @@ class PathHints:
|
|||||||
res.append(child.full_path)
|
res.append(child.full_path)
|
||||||
return sorted(res)
|
return sorted(res)
|
||||||
|
|
||||||
|
def add_separator(self, full_path: str) -> str:
|
||||||
|
"""
|
||||||
|
Возвращает full_path с добавленным разделителем ('.' или '['),
|
||||||
|
если у узла есть дети и пользователь ещё не поставил разделитель.
|
||||||
|
Если первый ребёнок — массивный токен ('[0]') → добавляем '['.
|
||||||
|
Позже можно допилить '->' для указателей.
|
||||||
|
"""
|
||||||
|
node = self.find_node(full_path)
|
||||||
|
text = full_path
|
||||||
|
|
||||||
|
if node and node.children and not (
|
||||||
|
text.endswith('.') or text.endswith('->') or text.endswith('[')
|
||||||
|
):
|
||||||
|
first_child = next(iter(node.children.values()))
|
||||||
|
if first_child.name.startswith('['):
|
||||||
|
text += '[' # сразу начинаем индекс
|
||||||
|
else:
|
||||||
|
text += '.' # обычный переход
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
# ------------ внутренние вспомогательные ------------
|
# ------------ внутренние вспомогательные ------------
|
||||||
|
|
||||||
def _root_full_names(self) -> List[str]:
|
def _root_full_names(self) -> List[str]:
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -292,23 +292,7 @@ class VariableSelectWidget(QWidget):
|
|||||||
return suggestions
|
return suggestions
|
||||||
|
|
||||||
def insert_completion(self, full_path: str):
|
def insert_completion(self, full_path: str):
|
||||||
"""
|
text = self.hints.add_separator(full_path)
|
||||||
Пользователь выбрал подсказку (full_path).
|
|
||||||
Если у узла есть дети и пользователь не поставил разделитель —
|
|
||||||
добавим '.'. Для массивного токена ('[0]') → добавим '.' тоже.
|
|
||||||
(Позже допилим '->' при наличии метаданных.)
|
|
||||||
"""
|
|
||||||
node = self.hints.find_node(full_path)
|
|
||||||
text = full_path
|
|
||||||
|
|
||||||
if node and node.children and not (
|
|
||||||
text.endswith('.') or text.endswith('->') or text.endswith('[')
|
|
||||||
):
|
|
||||||
first_child = next(iter(node.children.values()))
|
|
||||||
if first_child.name.startswith('['):
|
|
||||||
text += '[' # пользователь сразу начнёт ввод индекса
|
|
||||||
else:
|
|
||||||
text += '.' # обычный переход
|
|
||||||
if not self._bckspc_pressed:
|
if not self._bckspc_pressed:
|
||||||
self.search_input.setText(text)
|
self.search_input.setText(text)
|
||||||
self.search_input.setCursorPosition(len(text))
|
self.search_input.setCursorPosition(len(text))
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ class VariableSelectorDialog(QDialog):
|
|||||||
'enable': 'true',
|
'enable': 'true',
|
||||||
'shortname': name,
|
'shortname': name,
|
||||||
'pt_type': '',
|
'pt_type': '',
|
||||||
'iq_type': '',
|
'iq_type': 't_iq_none',
|
||||||
'return_type': 't_iq_none',
|
'return_type': 't_iq_none',
|
||||||
'file': file_val,
|
'file': file_val,
|
||||||
'extern': str(extern_val).lower() if extern_val else 'false',
|
'extern': str(extern_val).lower() if extern_val else 'false',
|
||||||
@@ -304,7 +304,7 @@ class VariableSelectorDialog(QDialog):
|
|||||||
# Проверка пути к XML
|
# Проверка пути к XML
|
||||||
if not hasattr(self, 'xml_path') or not self.xml_path:
|
if not hasattr(self, 'xml_path') or not self.xml_path:
|
||||||
from PySide2.QtWidgets import QMessageBox
|
from PySide2.QtWidgets import QMessageBox
|
||||||
QMessageBox.warning(self, "Ошибка", "Путь к XML не задан, невозможно обновить переменные.")
|
#QMessageBox.warning(self, "Ошибка", "Путь к XML не задан, невозможно обновить переменные.")
|
||||||
return
|
return
|
||||||
|
|
||||||
root, tree = myXML.safe_parse_xml(self.xml_path)
|
root, tree = myXML.safe_parse_xml(self.xml_path)
|
||||||
|
|||||||
140
Src/var_table.py
140
Src/var_table.py
@@ -64,7 +64,8 @@ class SetSizeDialog(QDialog):
|
|||||||
"""
|
"""
|
||||||
Диалоговое окно для выбора числового значения (размера).
|
Диалоговое окно для выбора числового значения (размера).
|
||||||
"""
|
"""
|
||||||
def __init__(self, parent=None, initial_value=10, min_value=1, max_value=50, title="Укажите размер короткого имени"):
|
def __init__(self, parent=None, initial_value=10, min_value=1, max_value=50, title="Укажите размер короткого имени",
|
||||||
|
label_text="Количество символов:"):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.setWindowTitle(title)
|
self.setWindowTitle(title)
|
||||||
self.setFixedSize(320, 120) # Задаем фиксированный размер для аккуратного вида
|
self.setFixedSize(320, 120) # Задаем фиксированный размер для аккуратного вида
|
||||||
@@ -74,7 +75,7 @@ class SetSizeDialog(QDialog):
|
|||||||
|
|
||||||
# Макет для ввода значения
|
# Макет для ввода значения
|
||||||
input_layout = QHBoxLayout()
|
input_layout = QHBoxLayout()
|
||||||
label = QLabel("Количество символов:", self)
|
label = QLabel(label_text, self)
|
||||||
|
|
||||||
self.spin_box = QSpinBox(self)
|
self.spin_box = QSpinBox(self)
|
||||||
self.spin_box.setRange(min_value, max_value) # Устанавливаем диапазон допустимых значений
|
self.spin_box.setRange(min_value, max_value) # Устанавливаем диапазон допустимых значений
|
||||||
@@ -119,84 +120,92 @@ class CtrlScrollComboBox(QComboBox):
|
|||||||
event.ignore()
|
event.ignore()
|
||||||
|
|
||||||
class VariableTableWidget(QTableWidget):
|
class VariableTableWidget(QTableWidget):
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None, show_value_instead_of_shortname=0):
|
||||||
super().__init__(0, 8, parent)
|
|
||||||
# Таблица переменных
|
# Таблица переменных
|
||||||
self.setHorizontalHeaderLabels([
|
if show_value_instead_of_shortname:
|
||||||
'№', # новый столбец
|
super().__init__(0, 8, parent)
|
||||||
'En',
|
self.setHorizontalHeaderLabels([
|
||||||
'Name',
|
'№',
|
||||||
'Origin Type',
|
'En',
|
||||||
'Base Type',
|
'Name',
|
||||||
'IQ Type',
|
'Origin Type',
|
||||||
'Return Type',
|
'Base Type',
|
||||||
'Short Name'
|
'IQ Type',
|
||||||
])
|
'Return Type',
|
||||||
|
'Value'
|
||||||
|
])
|
||||||
|
self._show_value = True
|
||||||
|
else:
|
||||||
|
super().__init__(0, 8, parent)
|
||||||
|
self.setHorizontalHeaderLabels([
|
||||||
|
'№',
|
||||||
|
'En',
|
||||||
|
'Name',
|
||||||
|
'Origin Type',
|
||||||
|
'Base Type',
|
||||||
|
'IQ Type',
|
||||||
|
'Return Type',
|
||||||
|
'Short Name'
|
||||||
|
])
|
||||||
|
self._show_value = False
|
||||||
self.setEditTriggers(QAbstractItemView.AllEditTriggers)
|
self.setEditTriggers(QAbstractItemView.AllEditTriggers)
|
||||||
self.var_list = []
|
self.var_list = []
|
||||||
# Инициализируем QSettings с именем организации и приложения
|
|
||||||
|
# QSettings
|
||||||
self.settings = QSettings("SET", "DebugVarEdit_VarTable")
|
self.settings = QSettings("SET", "DebugVarEdit_VarTable")
|
||||||
# Восстанавливаем сохранённое состояние, если есть
|
|
||||||
shortsize = self.settings.value("shortname_size", True, type=int)
|
shortsize = self.settings.value("shortname_size", True, type=int)
|
||||||
self._shortname_size = shortsize
|
self._shortname_size = shortsize
|
||||||
|
|
||||||
|
if(self._show_value):
|
||||||
|
self._shortname_size = 3
|
||||||
|
|
||||||
self.type_options = list(dict.fromkeys(type_map.values()))
|
self.type_options = list(dict.fromkeys(type_map.values()))
|
||||||
self.pt_types_all = [t.replace('pt_', '') for t in self.type_options]
|
self.pt_types_all = [t.replace('pt_', '') for t in self.type_options]
|
||||||
self.iq_types_all = ['iq_none', 'iq'] + [f'iq{i}' for i in range(1, 31)]
|
self.iq_types_all = ['iq_none', 'iq'] + [f'iq{i}' for i in range(1, 31)]
|
||||||
# Задаём базовые iq-типы (без префикса 'iq_')
|
|
||||||
self.iq_types = ['iq_none', 'iq', 'iq10', 'iq15', 'iq19', 'iq24']
|
self.iq_types = ['iq_none', 'iq', 'iq10', 'iq15', 'iq19', 'iq24']
|
||||||
# Фильтруем типы из type_map.values() исключая те, что содержат 'arr' или 'ptr'
|
type_options = [t for t in dict.fromkeys(type_map.values()) if 'arr' not in t and 'ptr' not in t
|
||||||
type_options = [t for t in dict.fromkeys(type_map.values()) if 'arr' not in t and 'ptr' not in t and
|
and 'struct' not in t and 'union' not in t and '64' not in t]
|
||||||
'struct' not in t and 'union' not in t and '64' not in t]
|
|
||||||
# Формируем display_type_options без префикса 'pt_'
|
|
||||||
self.pt_types = [t.replace('pt_', '') for t in type_options]
|
self.pt_types = [t.replace('pt_', '') for t in type_options]
|
||||||
|
|
||||||
self._iq_type_filter = list(self.iq_types) # Текущий фильтр iq типов (по умолчанию все)
|
self._iq_type_filter = list(self.iq_types)
|
||||||
self._pt_type_filter = list(self.pt_types)
|
self._pt_type_filter = list(self.pt_types)
|
||||||
self._ret_type_filter = list(self.iq_types)
|
self._ret_type_filter = list(self.iq_types)
|
||||||
header = self.horizontalHeader()
|
|
||||||
# Для остальных колонок — растяжение (Stretch), чтобы они заняли всю оставшуюся ширину
|
|
||||||
|
|
||||||
|
header = self.horizontalHeader()
|
||||||
for col in range(self.columnCount()):
|
for col in range(self.columnCount()):
|
||||||
if col == self.columnCount() - 1:
|
if col == self.columnCount() - 1:
|
||||||
header.setSectionResizeMode(col, QHeaderView.Stretch)
|
header.setSectionResizeMode(col, QHeaderView.Stretch)
|
||||||
else:
|
else:
|
||||||
header.setSectionResizeMode(col, QHeaderView.Interactive)
|
header.setSectionResizeMode(col, QHeaderView.Interactive)
|
||||||
|
|
||||||
parent_widget = self.parentWidget()
|
|
||||||
# Сделаем колонки с номерами фиксированной ширины
|
|
||||||
self.setColumnWidth(rows.No, 30)
|
self.setColumnWidth(rows.No, 30)
|
||||||
self.setColumnWidth(rows.include, 30)
|
self.setColumnWidth(rows.include, 30)
|
||||||
self.setColumnWidth(rows.pt_type, 85)
|
self.setColumnWidth(rows.pt_type, 85)
|
||||||
self.setColumnWidth(rows.iq_type, 85)
|
self.setColumnWidth(rows.iq_type, 85)
|
||||||
self.setColumnWidth(rows.ret_type, 85)
|
self.setColumnWidth(rows.ret_type, 85)
|
||||||
|
|
||||||
self.setColumnWidth(rows.name, 300)
|
self.setColumnWidth(rows.name, 300)
|
||||||
self.setColumnWidth(rows.type, 100)
|
self.setColumnWidth(rows.type, 100)
|
||||||
|
|
||||||
self._resizing = False
|
self._resizing = False
|
||||||
self.horizontalHeader().sectionResized.connect(self.on_section_resized)
|
self.horizontalHeader().sectionResized.connect(self.on_section_resized)
|
||||||
self.horizontalHeader().sectionClicked.connect(self.on_header_clicked)
|
self.horizontalHeader().sectionClicked.connect(self.on_header_clicked)
|
||||||
|
|
||||||
|
|
||||||
def populate(self, vars_list, structs, on_change_callback):
|
def populate(self, vars_list, structs, on_change_callback):
|
||||||
self.var_list = vars_list
|
self.var_list = vars_list
|
||||||
|
self.setUpdatesEnabled(False)
|
||||||
|
self.blockSignals(True)
|
||||||
|
|
||||||
# --- ДО: удаляем отображение структур и union-переменных
|
|
||||||
for var in vars_list:
|
for var in vars_list:
|
||||||
pt_type = var.get('pt_type', '')
|
pt_type = var.get('pt_type', '')
|
||||||
if 'struct' in pt_type or 'union' in pt_type:
|
if 'struct' in pt_type or 'union' in pt_type:
|
||||||
var['show_var'] = 'false'
|
var['show_var'] = 'false'
|
||||||
var['enable'] = 'false'
|
var['enable'] = 'false'
|
||||||
|
|
||||||
|
|
||||||
filtered_vars = [v for v in vars_list if v.get('show_var', 'false') == 'true']
|
filtered_vars = [v for v in vars_list if v.get('show_var', 'false') == 'true']
|
||||||
self.setRowCount(len(filtered_vars))
|
self.setRowCount(len(filtered_vars))
|
||||||
self.verticalHeader().setVisible(False)
|
self.verticalHeader().setVisible(False)
|
||||||
style_with_padding = "padding-left: 5px; padding-right: 5px; font-size: 14pt; font-family: 'Segoe UI';"
|
style_with_padding = "padding-left: 5px; padding-right: 5px; font-size: 14pt; font-family: 'Segoe UI';"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
for row, var in enumerate(filtered_vars):
|
for row, var in enumerate(filtered_vars):
|
||||||
# №
|
# №
|
||||||
no_item = QTableWidgetItem(str(row))
|
no_item = QTableWidgetItem(str(row))
|
||||||
@@ -212,25 +221,21 @@ class VariableTableWidget(QTableWidget):
|
|||||||
|
|
||||||
# Name
|
# Name
|
||||||
name_edit = QLineEdit(var['name'])
|
name_edit = QLineEdit(var['name'])
|
||||||
if var['type'] in structs:
|
|
||||||
completer = QCompleter(structs[var['type']].keys())
|
|
||||||
completer.setCaseSensitivity(Qt.CaseInsensitive)
|
|
||||||
name_edit.setCompleter(completer)
|
|
||||||
name_edit.textChanged.connect(on_change_callback)
|
name_edit.textChanged.connect(on_change_callback)
|
||||||
name_edit.setStyleSheet(style_with_padding)
|
name_edit.setStyleSheet(style_with_padding)
|
||||||
self.setCellWidget(row, rows.name, name_edit)
|
self.setCellWidget(row, rows.name, name_edit)
|
||||||
|
|
||||||
# Origin Type (readonly)
|
# Origin Type
|
||||||
origin_item = QTableWidgetItem(var.get('type', ''))
|
origin_item = QTableWidgetItem(var.get('type', ''))
|
||||||
origin_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
|
origin_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
|
||||||
origin_item.setToolTip(var.get('type', '')) # Всплывающая подсказка
|
origin_item.setToolTip(var.get('type', ''))
|
||||||
origin_item.setForeground(QBrush(Qt.black))
|
origin_item.setForeground(QBrush(Qt.black))
|
||||||
self.setItem(row, rows.type, origin_item)
|
self.setItem(row, rows.type, origin_item)
|
||||||
|
|
||||||
# pt_type
|
# pt_type
|
||||||
pt_combo = CtrlScrollComboBox()
|
pt_combo = CtrlScrollComboBox()
|
||||||
pt_combo.addItems(self.pt_types)
|
pt_combo.addItems(self.pt_types)
|
||||||
value = var['pt_type'].replace('pt_', '')
|
value = var.get('pt_type', 'unknown').replace('pt_', '')
|
||||||
if value not in self.pt_types:
|
if value not in self.pt_types:
|
||||||
pt_combo.addItem(value)
|
pt_combo.addItem(value)
|
||||||
pt_combo.setCurrentText(value)
|
pt_combo.setCurrentText(value)
|
||||||
@@ -241,7 +246,7 @@ class VariableTableWidget(QTableWidget):
|
|||||||
# iq_type
|
# iq_type
|
||||||
iq_combo = CtrlScrollComboBox()
|
iq_combo = CtrlScrollComboBox()
|
||||||
iq_combo.addItems(self.iq_types)
|
iq_combo.addItems(self.iq_types)
|
||||||
value = var['iq_type'].replace('t_', '')
|
value = var.get('iq_type', 'iq_none').replace('t_', '')
|
||||||
if value not in self.iq_types:
|
if value not in self.iq_types:
|
||||||
iq_combo.addItem(value)
|
iq_combo.addItem(value)
|
||||||
iq_combo.setCurrentText(value)
|
iq_combo.setCurrentText(value)
|
||||||
@@ -252,7 +257,7 @@ class VariableTableWidget(QTableWidget):
|
|||||||
# return_type
|
# return_type
|
||||||
ret_combo = CtrlScrollComboBox()
|
ret_combo = CtrlScrollComboBox()
|
||||||
ret_combo.addItems(self.iq_types)
|
ret_combo.addItems(self.iq_types)
|
||||||
value = var['return_type'].replace('t_', '')
|
value = var.get('return_type', 'iq_none').replace('t_', '')
|
||||||
if value not in self.iq_types:
|
if value not in self.iq_types:
|
||||||
ret_combo.addItem(value)
|
ret_combo.addItem(value)
|
||||||
ret_combo.setCurrentText(value)
|
ret_combo.setCurrentText(value)
|
||||||
@@ -260,13 +265,38 @@ class VariableTableWidget(QTableWidget):
|
|||||||
ret_combo.setStyleSheet(style_with_padding)
|
ret_combo.setStyleSheet(style_with_padding)
|
||||||
self.setCellWidget(row, rows.ret_type, ret_combo)
|
self.setCellWidget(row, rows.ret_type, ret_combo)
|
||||||
|
|
||||||
# short_name
|
# Последний столбец
|
||||||
short_name_val = var.get('shortname', var['name'])
|
if self._show_value:
|
||||||
short_name_edit = QLineEdit(short_name_val)
|
if self._show_value:
|
||||||
short_name_edit.textChanged.connect(on_change_callback)
|
val = var.get('value', '')
|
||||||
short_name_edit.setStyleSheet(style_with_padding)
|
if val is None:
|
||||||
self.setCellWidget(row, rows.short_name, short_name_edit)
|
val = ''
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
f_val = float(val)
|
||||||
|
# Форматируем число с учетом self._shortname_size
|
||||||
|
if f_val.is_integer():
|
||||||
|
val = str(int(f_val))
|
||||||
|
else:
|
||||||
|
precision = getattr(self, "_shortname_size", 3) # по умолчанию 3
|
||||||
|
val = f"{f_val:.{precision}f}"
|
||||||
|
except ValueError:
|
||||||
|
# Если значение не число (строка и т.п.), оставляем как есть
|
||||||
|
val = str(val)
|
||||||
|
|
||||||
|
val_edit = QLineEdit(val)
|
||||||
|
val_edit.textChanged.connect(on_change_callback)
|
||||||
|
val_edit.setStyleSheet(style_with_padding)
|
||||||
|
self.setCellWidget(row, rows.short_name, val_edit)
|
||||||
|
else:
|
||||||
|
short_name_val = var.get('shortname', var['name'])
|
||||||
|
short_name_edit = QLineEdit(short_name_val)
|
||||||
|
short_name_edit.textChanged.connect(on_change_callback)
|
||||||
|
short_name_edit.setStyleSheet(style_with_padding)
|
||||||
|
self.setCellWidget(row, rows.short_name, short_name_edit)
|
||||||
|
|
||||||
|
self.blockSignals(False)
|
||||||
|
self.setUpdatesEnabled(True)
|
||||||
self.check()
|
self.check()
|
||||||
|
|
||||||
def check(self):
|
def check(self):
|
||||||
@@ -298,8 +328,9 @@ class VariableTableWidget(QTableWidget):
|
|||||||
color = error_color
|
color = error_color
|
||||||
tooltip = tooltip_missing
|
tooltip = tooltip_missing
|
||||||
elif long_shortname:
|
elif long_shortname:
|
||||||
color = warning_color
|
if not self._show_value:
|
||||||
tooltip = tooltip_shortname
|
color = warning_color
|
||||||
|
tooltip = tooltip_shortname
|
||||||
|
|
||||||
self.highlight_row(row, color, tooltip)
|
self.highlight_row(row, color, tooltip)
|
||||||
t4 = time.time()
|
t4 = time.time()
|
||||||
@@ -353,10 +384,15 @@ class VariableTableWidget(QTableWidget):
|
|||||||
self.update_comboboxes({rows.ret_type: self._ret_type_filter})
|
self.update_comboboxes({rows.ret_type: self._ret_type_filter})
|
||||||
|
|
||||||
elif logicalIndex == rows.short_name:
|
elif logicalIndex == rows.short_name:
|
||||||
dlg = SetSizeDialog(self)
|
if self._show_value:
|
||||||
|
dlg = SetSizeDialog(self, title="Укажите точность", label_text="Кол-во знаков после запятой", initial_value=3)
|
||||||
|
else:
|
||||||
|
dlg = SetSizeDialog(self)
|
||||||
|
|
||||||
if dlg.exec_():
|
if dlg.exec_():
|
||||||
self._shortname_size = dlg.get_selected_size()
|
self._shortname_size = dlg.get_selected_size()
|
||||||
self.settings.setValue("shortname_size", self._shortname_size)
|
if not self._show_value:
|
||||||
|
self.settings.setValue("shortname_size", self._shortname_size)
|
||||||
self.check()
|
self.check()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,7 @@
|
|||||||
#define DEBUG_ERR_ADDR_ALIGN (1<<3) | DEBUG_ERR
|
#define DEBUG_ERR_ADDR_ALIGN (1<<3) | DEBUG_ERR
|
||||||
#define DEBUG_ERR_INTERNAL (1<<4) | DEBUG_ERR
|
#define DEBUG_ERR_INTERNAL (1<<4) | DEBUG_ERR
|
||||||
#define DEBUG_ERR_DATATIME (1<<5) | DEBUG_ERR
|
#define DEBUG_ERR_DATATIME (1<<5) | DEBUG_ERR
|
||||||
|
#define DEBUG_ERR_RS (1<<6) | DEBUG_ERR
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,29 @@
|
|||||||
# pyinstaller --onefile --distpath . --workpath ./build --specpath ./build parse_xml.py
|
# pyinstaller --onefile --distpath ./parse_xml --workpath ./parse_xml/build --specpath ./build parse_xml/Src/parse_xml.py
|
||||||
# python -m nuitka --standalone --onefile --output-dir=./build parse_xml.py
|
# python -m nuitka --standalone --onefile --output-dir=./parse_xml parse_xml/Src/parse_xml.py
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
import xml.dom.minidom
|
import xml.dom.minidom
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if len(sys.argv) < 3:
|
if len(sys.argv) < 3:
|
||||||
print("Usage: python simplify_dwarf.py <input.xml> <info.txt> [output.xml]")
|
print("Usage: python parse_xml.exe <input.xml> <info.txt> [output.xml]")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
input_path = sys.argv[1]
|
input_path = sys.argv[1]
|
||||||
info_path = sys.argv[2]
|
info_path = sys.argv[2]
|
||||||
|
|
||||||
|
base_type_sizes = {
|
||||||
|
"char": 2,
|
||||||
|
"short": 2,
|
||||||
|
"int": 2,
|
||||||
|
"long": 4,
|
||||||
|
"long long": 8,
|
||||||
|
"float": 4,
|
||||||
|
"double": 8,
|
||||||
|
}
|
||||||
|
|
||||||
if len(sys.argv) >= 4:
|
if len(sys.argv) >= 4:
|
||||||
output_path = sys.argv[3]
|
output_path = sys.argv[3]
|
||||||
else:
|
else:
|
||||||
@@ -42,13 +54,25 @@ def get_attr(die, attr_type):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def get_die_size(die):
|
def get_die_size(die):
|
||||||
"""Вернуть размер DIE в байтах из атрибута DW_AT_byte_size."""
|
"""Вернуть размер DIE в байтах из атрибута DW_AT_byte_size или по ключевым словам имени типа."""
|
||||||
|
|
||||||
|
# Сначала пытаемся получить размер из DW_AT_byte_size
|
||||||
for attr in die.findall("attribute"):
|
for attr in die.findall("attribute"):
|
||||||
type_elem = attr.find("type")
|
type_elem = attr.find("type")
|
||||||
if type_elem is not None and type_elem.text == "DW_AT_byte_size":
|
if type_elem is not None and type_elem.text == "DW_AT_byte_size":
|
||||||
const_elem = attr.find("value/const")
|
const_elem = attr.find("value/const")
|
||||||
if const_elem is not None:
|
if const_elem is not None:
|
||||||
return int(const_elem.text, 0)
|
return int(const_elem.text, 0)
|
||||||
|
|
||||||
|
# Если не нашли, пробуем определить размер по ключевым словам в имени типа
|
||||||
|
name_elem = die.find("attribute[@name='DW_AT_name']/value/const")
|
||||||
|
if name_elem is not None:
|
||||||
|
type_name = name_elem.text.lower()
|
||||||
|
|
||||||
|
for key, size in base_type_sizes.items():
|
||||||
|
if key in type_name:
|
||||||
|
return size
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def resolve_type_die(type_id):
|
def resolve_type_die(type_id):
|
||||||
@@ -136,48 +160,100 @@ def parse_offset(offset_text):
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def get_array_dimensions(array_die):
|
|
||||||
"""Рекурсивно получить размеры всех измерений массива из DIE с тегом DW_TAG_array_type."""
|
|
||||||
dims = []
|
|
||||||
|
|
||||||
# Ищем размер текущего измерения
|
def get_base_type_die(array_die):
|
||||||
# Размер может быть в DW_AT_upper_bound, либо вычисляться из DW_AT_byte_size и типа элемента
|
"""Спускаемся по цепочке DW_AT_type, пока не дойдем до не-массива (базового типа)."""
|
||||||
# Но часто в DWARF размер указывается через дочерние die с тегом DW_TAG_subrange_type
|
current_die = array_die
|
||||||
|
while True:
|
||||||
subrange = None
|
ref = get_attr(current_die, "DW_AT_type")
|
||||||
for child in array_die.findall("die"):
|
if ref is None or ref.find("ref") is None:
|
||||||
if child.findtext("tag") == "DW_TAG_subrange_type":
|
|
||||||
subrange = child
|
|
||||||
break
|
break
|
||||||
|
next_die = resolve_type_die(ref.find("ref").attrib.get("idref"))
|
||||||
|
if next_die is None:
|
||||||
|
break
|
||||||
|
if next_die.findtext("tag") == "DW_TAG_array_type":
|
||||||
|
current_die = next_die
|
||||||
|
else:
|
||||||
|
return next_die
|
||||||
|
return current_die
|
||||||
|
|
||||||
dim_size = None
|
def get_array_dimensions(array_die):
|
||||||
if subrange is not None:
|
dims = []
|
||||||
# Ищем атрибут DW_AT_upper_bound
|
# Итерируем по всем DIE с тегом DW_TAG_subrange_type, потомки текущего массива
|
||||||
ub_attr = get_attr(subrange, "DW_AT_upper_bound")
|
for child in array_die.findall("die"):
|
||||||
|
if child.findtext("tag") != "DW_TAG_subrange_type":
|
||||||
|
continue
|
||||||
|
|
||||||
|
dim_size = None
|
||||||
|
ub_attr = get_attr(child, "DW_AT_upper_bound")
|
||||||
if ub_attr is not None:
|
if ub_attr is not None:
|
||||||
val = ub_attr.find("value/const")
|
# Попробуем разные варианты получить значение upper_bound
|
||||||
if val is not None:
|
# 1) value/const
|
||||||
# Размер измерения равен верхней границе + 1 (т.к. верхняя граница индексируется с 0)
|
val_const = ub_attr.find("const")
|
||||||
dim_size = int(val.text, 0) + 1
|
if val_const is not None:
|
||||||
|
try:
|
||||||
|
dim_size = int(val_const.text, 0) + 1
|
||||||
|
#print(f"[DEBUG] Found DW_AT_upper_bound const: {val_const.text}, size={dim_size}")
|
||||||
|
except Exception as e:
|
||||||
|
a=1#print(f"[WARN] Error parsing upper_bound const: {e}")
|
||||||
|
else:
|
||||||
|
# 2) value/block (DW_OP_constu / DW_OP_plus_uconst, etc.)
|
||||||
|
val_block = ub_attr.find("block")
|
||||||
|
if val_block is not None:
|
||||||
|
block_text = val_block.text
|
||||||
|
# Можно попытаться парсить DWARF expr (например DW_OP_plus_uconst 7)
|
||||||
|
if block_text and "DW_OP_plus_uconst" in block_text:
|
||||||
|
try:
|
||||||
|
parts = block_text.split()
|
||||||
|
val = int(parts[-1], 0)
|
||||||
|
dim_size = val + 1
|
||||||
|
#print(f"[DEBUG] Parsed upper_bound block: {val} + 1 = {dim_size}")
|
||||||
|
except Exception as e:
|
||||||
|
a=1#print(f"[WARN] Error parsing upper_bound block: {e}")
|
||||||
|
else:
|
||||||
|
a=1#print(f"[WARN] Unexpected DW_AT_upper_bound block content: {block_text}")
|
||||||
|
else:
|
||||||
|
a=1#print(f"[WARN] DW_AT_upper_bound has no const or block value")
|
||||||
|
|
||||||
if dim_size is None:
|
if dim_size is None:
|
||||||
# Если размер не нашли, попробуем вычислить через общий размер / размер элемента
|
# fallback по DW_AT_count — редко встречается
|
||||||
|
ct_attr = get_attr(child, "DW_AT_count")
|
||||||
|
if ct_attr is not None:
|
||||||
|
val_const = ct_attr.find("value/const")
|
||||||
|
if val_const is not None:
|
||||||
|
try:
|
||||||
|
dim_size = int(val_const.text, 0)
|
||||||
|
#print(f"[DEBUG] Found DW_AT_count: {dim_size}")
|
||||||
|
except Exception as e:
|
||||||
|
a=1#print(f"[WARN] Error parsing DW_AT_count const: {e}")
|
||||||
|
|
||||||
|
if dim_size is None:
|
||||||
|
print("[DEBUG] No dimension size found for this subrange, defaulting to 0")
|
||||||
|
dim_size = 0
|
||||||
|
|
||||||
|
dims.append(dim_size)
|
||||||
|
|
||||||
|
# Если не нашли измерений — пытаемся вычислить размер массива по общему размеру
|
||||||
|
if not dims:
|
||||||
arr_size = get_die_size(array_die)
|
arr_size = get_die_size(array_die)
|
||||||
|
elem_size = None
|
||||||
element_type_ref = get_attr(array_die, "DW_AT_type")
|
element_type_ref = get_attr(array_die, "DW_AT_type")
|
||||||
if element_type_ref is not None and element_type_ref.find("ref") is not None:
|
if element_type_ref is not None and element_type_ref.find("ref") is not None:
|
||||||
element_type_id = element_type_ref.find("ref").attrib.get("idref")
|
element_type_id = element_type_ref.find("ref").attrib.get("idref")
|
||||||
element_type_die = resolve_type_die(element_type_id)
|
elem_die = resolve_type_die(element_type_id)
|
||||||
elem_size = get_die_size(element_type_die) if element_type_die is not None else None
|
if elem_die is not None:
|
||||||
|
elem_size = get_die_size(elem_die)
|
||||||
|
#print(f"[DEBUG] Fallback: arr_size={arr_size}, elem_size={elem_size}")
|
||||||
|
|
||||||
if arr_size is not None and elem_size:
|
if arr_size is not None and elem_size:
|
||||||
dim_size = arr_size // elem_size
|
dim_calc = arr_size // elem_size
|
||||||
|
dims.append(dim_calc)
|
||||||
|
#print(f"[DEBUG] Calculated dimension size from total size: {dim_calc}")
|
||||||
|
else:
|
||||||
|
dims.append(0)
|
||||||
|
print("[DEBUG] Could not calculate dimension size, set 0")
|
||||||
|
|
||||||
if dim_size is None:
|
# Рекурсивно обрабатываем вложенные массивы
|
||||||
dim_size = 0 # Неизвестно
|
|
||||||
|
|
||||||
dims.append(dim_size)
|
|
||||||
|
|
||||||
# Рекурсивно проверяем, если элемент типа тоже массив (многомерный)
|
|
||||||
element_type_ref = get_attr(array_die, "DW_AT_type")
|
element_type_ref = get_attr(array_die, "DW_AT_type")
|
||||||
if element_type_ref is not None and element_type_ref.find("ref") is not None:
|
if element_type_ref is not None and element_type_ref.find("ref") is not None:
|
||||||
element_type_id = element_type_ref.find("ref").attrib.get("idref")
|
element_type_id = element_type_ref.find("ref").attrib.get("idref")
|
||||||
@@ -185,63 +261,69 @@ def get_array_dimensions(array_die):
|
|||||||
if element_type_die is not None and element_type_die.findtext("tag") == "DW_TAG_array_type":
|
if element_type_die is not None and element_type_die.findtext("tag") == "DW_TAG_array_type":
|
||||||
dims.extend(get_array_dimensions(element_type_die))
|
dims.extend(get_array_dimensions(element_type_die))
|
||||||
|
|
||||||
|
#print(f"[DEBUG] Array dimensions: {dims}")
|
||||||
return dims
|
return dims
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def handle_array_type(member_elem, resolved_type, offset=0):
|
def handle_array_type(member_elem, resolved_type, offset=0):
|
||||||
dims = get_array_dimensions(resolved_type)
|
dims = get_array_dimensions(resolved_type)
|
||||||
|
|
||||||
# Получаем элементарный тип массива (наибольший элемент в цепочке массивов)
|
base_die = get_base_type_die(resolved_type)
|
||||||
def get_base_element_type(die):
|
base_name = "unknown"
|
||||||
ref = get_attr(die, "DW_AT_type")
|
base_size = None
|
||||||
if ref is not None and ref.find("ref") is not None:
|
if base_die is not None:
|
||||||
type_id = ref.find("ref").attrib.get("idref")
|
base_id = base_die.attrib.get("id")
|
||||||
type_die = resolve_type_die(type_id)
|
if base_id:
|
||||||
if type_die is not None and type_die.findtext("tag") == "DW_TAG_array_type":
|
base_name = get_type_name(base_id)
|
||||||
return get_base_element_type(type_die)
|
base_size = get_die_size(base_die)
|
||||||
else:
|
else:
|
||||||
return type_die
|
base_name = get_type_name(base_die.attrib.get("id", ""))
|
||||||
return None
|
#print(f"[DEBUG] Base type name: {base_name}, base size: {base_size}")
|
||||||
|
|
||||||
element_type_die = get_base_element_type(resolved_type)
|
member_elem.set("type", base_name + "[]" * len(dims))
|
||||||
element_type_name = get_type_name(element_type_die.attrib.get("id")) if element_type_die is not None else "unknown"
|
|
||||||
|
|
||||||
# Формируем строку типа с нужным количеством []
|
if base_size is None:
|
||||||
type_with_array = element_type_name + "[]" * len(dims)
|
base_size = 0
|
||||||
member_elem.set("type", type_with_array)
|
|
||||||
|
|
||||||
# Размер всего массива
|
total_elements = 1
|
||||||
arr_size = get_die_size(resolved_type)
|
for d in dims:
|
||||||
if arr_size is not None:
|
if d == 0:
|
||||||
member_elem.set("size", str(arr_size))
|
total_elements = 0
|
||||||
|
print(f"[WARN] Dimension size is zero, setting total elements to 0")
|
||||||
|
break
|
||||||
|
total_elements *= d
|
||||||
|
|
||||||
|
total_size = total_elements * base_size if base_size is not None else 0
|
||||||
|
if total_size:
|
||||||
|
member_elem.set("size", str(base_size if base_size is not None else 1))
|
||||||
|
else:
|
||||||
|
arr_size = get_die_size(resolved_type)
|
||||||
|
if arr_size:
|
||||||
|
member_elem.set("size", str(arr_size))
|
||||||
|
#print(f"[DEBUG] Used fallback size from resolved_type: {arr_size}")
|
||||||
|
else:
|
||||||
|
print(f"[WARN] Could not determine total size for array")
|
||||||
|
|
||||||
# Добавляем атрибуты size1, size2, ...
|
|
||||||
for i, dim in enumerate(dims, 1):
|
for i, dim in enumerate(dims, 1):
|
||||||
member_elem.set(f"size{i}", str(dim))
|
member_elem.set(f"size{i}", str(dim))
|
||||||
|
#print(f"[DEBUG] Setting size{i} = {dim}")
|
||||||
|
|
||||||
member_elem.set("kind", "array")
|
member_elem.set("kind", "array")
|
||||||
|
|
||||||
# Если базовый элемент - структура, рекурсивно добавляем её члены
|
if base_die is not None and base_die.findtext("tag") == "DW_TAG_structure_type":
|
||||||
if element_type_die is not None and element_type_die.findtext("tag") == "DW_TAG_structure_type":
|
add_members_recursive(member_elem, base_die, offset)
|
||||||
add_members_recursive(member_elem, element_type_die, offset)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def add_members_recursive(parent_elem, struct_die, base_offset=0):
|
def add_members_recursive(parent_elem, struct_die, base_offset=0):
|
||||||
tag = struct_die.findtext("tag")
|
is_union = struct_die.findtext("tag") == "DW_TAG_union_type"
|
||||||
is_union = tag == "DW_TAG_union_type"
|
|
||||||
|
|
||||||
|
|
||||||
# Получаем размер структуры/объединения
|
|
||||||
size = get_die_size(struct_die)
|
size = get_die_size(struct_die)
|
||||||
if size is not None:
|
if size is not None:
|
||||||
parent_elem.set("size", hex(size))
|
parent_elem.set("size", hex(size))
|
||||||
|
|
||||||
|
|
||||||
for member in struct_die.findall("die"):
|
for member in struct_die.findall("die"):
|
||||||
if member.findtext("tag") != "DW_TAG_member":
|
if member.findtext("tag") != "DW_TAG_member":
|
||||||
continue
|
continue
|
||||||
@@ -249,39 +331,41 @@ def add_members_recursive(parent_elem, struct_die, base_offset=0):
|
|||||||
name_attr = get_attr(member, "DW_AT_name")
|
name_attr = get_attr(member, "DW_AT_name")
|
||||||
offset_attr = get_attr(member, "DW_AT_data_member_location")
|
offset_attr = get_attr(member, "DW_AT_data_member_location")
|
||||||
type_attr = get_attr(member, "DW_AT_type")
|
type_attr = get_attr(member, "DW_AT_type")
|
||||||
|
|
||||||
if name_attr is None or offset_attr is None or type_attr is None:
|
if name_attr is None or offset_attr is None or type_attr is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
name = name_attr.findtext("string")
|
name = name_attr.findtext("string")
|
||||||
offset_text = offset_attr.findtext("block")
|
offset = parse_offset(offset_attr.findtext("block")) + base_offset
|
||||||
offset = parse_offset(offset_text) + base_offset
|
|
||||||
type_id = type_attr.find("ref").attrib.get("idref")
|
type_id = type_attr.find("ref").attrib.get("idref")
|
||||||
resolved_type = resolve_type_die(type_id)
|
resolved_type = resolve_type_die(type_id)
|
||||||
type_name = get_type_name(type_id)
|
type_name = get_type_name(type_id)
|
||||||
|
|
||||||
if type_name == "unknown":
|
if type_name == "unknown":
|
||||||
continue
|
continue
|
||||||
|
|
||||||
member_elem = ET.SubElement(
|
member_elem = ET.SubElement(parent_elem, "member", name=name, offset=hex(offset), type=type_name)
|
||||||
parent_elem, "member", name=name, offset=hex(offset), type=type_name
|
|
||||||
)
|
|
||||||
|
|
||||||
if is_union:
|
if is_union:
|
||||||
member_elem.set("kind", "union")
|
member_elem.set("kind", "union")
|
||||||
|
|
||||||
if resolved_type is not None:
|
if resolved_type is not None:
|
||||||
subtag = resolved_type.findtext("tag")
|
tag = resolved_type.findtext("tag")
|
||||||
|
if tag == "DW_TAG_array_type":
|
||||||
# Обработка массива
|
|
||||||
if subtag == "DW_TAG_array_type":
|
|
||||||
handle_array_type(member_elem, resolved_type, offset)
|
handle_array_type(member_elem, resolved_type, offset)
|
||||||
# Обработка структур и объединений
|
elif tag in ("DW_TAG_structure_type", "DW_TAG_union_type"):
|
||||||
elif subtag in ("DW_TAG_structure_type", "DW_TAG_union_type"):
|
|
||||||
member_elem.set("type", type_name)
|
member_elem.set("type", type_name)
|
||||||
add_members_recursive(member_elem, resolved_type, offset)
|
add_members_recursive(member_elem, resolved_type, offset)
|
||||||
else:
|
elif tag == "DW_TAG_pointer_type":
|
||||||
member_elem.set("type", type_name)
|
# Проверяем тип, на который указывает указатель
|
||||||
|
pointee_ref = get_attr(resolved_type, "DW_AT_type")
|
||||||
|
if pointee_ref is not None and pointee_ref.find("ref") is not None:
|
||||||
|
pointee_id = pointee_ref.find("ref").attrib.get("idref")
|
||||||
|
pointee_die = resolve_type_die(pointee_id)
|
||||||
|
if pointee_die is not None:
|
||||||
|
pointee_tag = pointee_die.findtext("tag")
|
||||||
|
if pointee_tag in ("DW_TAG_structure_type", "DW_TAG_union_type"):
|
||||||
|
# Добавляем подэлементы для структуры, на которую указывает указатель
|
||||||
|
pointer_elem = ET.SubElement(member_elem, "pointee", type=get_type_name(pointee_id))
|
||||||
|
add_members_recursive(pointer_elem, pointee_die, 0)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
output_root = ET.Element("variables")
|
output_root = ET.Element("variables")
|
||||||
@@ -292,13 +376,10 @@ for die in root.iter("die"):
|
|||||||
name_attr = get_attr(die, "DW_AT_name")
|
name_attr = get_attr(die, "DW_AT_name")
|
||||||
addr_attr = get_attr(die, "DW_AT_location")
|
addr_attr = get_attr(die, "DW_AT_location")
|
||||||
type_attr = get_attr(die, "DW_AT_type")
|
type_attr = get_attr(die, "DW_AT_type")
|
||||||
|
|
||||||
if name_attr is None or addr_attr is None or type_attr is None:
|
if name_attr is None or addr_attr is None or type_attr is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
name = name_attr.findtext("string")
|
name = name_attr.findtext("string")
|
||||||
|
|
||||||
# Пропускаем переменные с '$' в имени
|
|
||||||
if "$" in name:
|
if "$" in name:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -310,40 +391,24 @@ for die in root.iter("die"):
|
|||||||
type_id = type_attr.find("ref").attrib.get("idref")
|
type_id = type_attr.find("ref").attrib.get("idref")
|
||||||
resolved_type = resolve_type_die(type_id)
|
resolved_type = resolve_type_die(type_id)
|
||||||
type_name = get_type_name(type_id)
|
type_name = get_type_name(type_id)
|
||||||
# Пропускаем переменные, находящиеся в памяти периферии
|
if 0x800 <= addr < 0x8000 or type_name == "unknown":
|
||||||
if 0x800 <= addr < 0x8000:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Проверка на DW_TAG_subroutine_type - пропускаем такие переменные
|
|
||||||
if type_name == "unknown":
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
var_elem = ET.SubElement(output_root, "variable", name=name, address=hex(addr), type=type_name)
|
var_elem = ET.SubElement(output_root, "variable", name=name, address=hex(addr), type=type_name)
|
||||||
if resolved_type is not None:
|
if resolved_type is not None:
|
||||||
tag = resolved_type.findtext("tag")
|
tag = resolved_type.findtext("tag")
|
||||||
|
if tag == "DW_TAG_array_type":
|
||||||
if tag == "DW_TAG_array_type":
|
handle_array_type(var_elem, resolved_type)
|
||||||
handle_array_type(var_elem, resolved_type)
|
elif tag in ("DW_TAG_structure_type", "DW_TAG_union_type"):
|
||||||
|
add_members_recursive(var_elem, resolved_type)
|
||||||
elif tag in ("DW_TAG_structure_type", "DW_TAG_union_type"):
|
|
||||||
add_members_recursive(var_elem, resolved_type)
|
|
||||||
|
|
||||||
|
|
||||||
timestamp = extract_timestamp(info_path)
|
timestamp = extract_timestamp(info_path)
|
||||||
|
|
||||||
# Создаём новый элемент <timestamp> с текстом timestamp
|
|
||||||
timestamp_elem = ET.Element("timestamp")
|
timestamp_elem = ET.Element("timestamp")
|
||||||
timestamp_elem.text = timestamp
|
timestamp_elem.text = timestamp
|
||||||
|
output_root.insert(0, timestamp_elem)
|
||||||
# Вставляем тег timestamp в начало (или куда хочешь)
|
|
||||||
output_root.insert(0, timestamp_elem) # В начало списка дочерних элементов
|
|
||||||
|
|
||||||
# Красивый вывод
|
|
||||||
|
|
||||||
rough_string = ET.tostring(output_root, encoding="utf-8")
|
rough_string = ET.tostring(output_root, encoding="utf-8")
|
||||||
reparsed = xml.dom.minidom.parseString(rough_string)
|
pretty_xml = xml.dom.minidom.parseString(rough_string).toprettyxml(indent=" ")
|
||||||
pretty_xml = reparsed.toprettyxml(indent=" ")
|
|
||||||
|
|
||||||
with open(output_path, "w", encoding="utf-8") as f:
|
with open(output_path, "w", encoding="utf-8") as f:
|
||||||
f.write(pretty_xml)
|
f.write(pretty_xml)
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user