/
dAshkova
/
luna_CoDeSys
Обзор
Документация
Войти
/
dAshkova
/
luna_CoDeSys
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
develop
parser.py
746 строк
39 KB
parovoZZ
Допилен ACTION и PROPERTY
15 июн 2026, 14:40
15 июн 2026, 14:40
9c8efb1
Код
Авторство
О чём код?
# -*- coding: utf-8 -*- # Единый реестр GUID типов CODESYS для всего класса class CodesysTypes: # Контейнеры PLC_DEVICE = "225bfe47-7336-4dbc-9419-4105a7c831fa" PLC_LOGIC = "40b404f9-e5dc-42c6-907f-c89f4a517386" FOLDER = "738bea1e-99bb-4f04-90bb-a7a567e74e3a" # Основные объекты (Файлы) POU = "6f9dac99-8de1-4efc-8465-68ac443b7d08" INTERFACE = "6654496c-404d-479a-aad2-8551054e5f1e" DUT = "2db5746d-d284-4425-9f7f-2663a34b0ebc" GVL = "ffbfa93a-b94d-45fc-a329-229860183b1d" NVL = "722bc402-e00b-490a-8138-0a7d01b3426e" TASK_CONFIG= "ae1de277-a207-4a28-9efb-456c06bd52f3" # Внутренние элементы (Методы/Свойства) METHOD = "f8a58466-d7f6-439f-bbb8-d4600e41d099" METHOD_ABSTRACT = "62ebfd1c-d342-43e5-8efb-f22b6d8e4a04" METHOD_INTERFACE = "f89f7675-27f1-46b3-8abb-b7da8e774ffd" PROPERTY = "5a3b8626-d3e9-4f37-98b5-66420063d91e" PROP_ACCESSOR = "792f2eb6-721e-4e64-ba20-bc98351056db" PROP_ACC_INT = "28747452-a93d-4b34-8d05-d2c6018edd7d" ACTION = "8ac092e5-3128-4e26-9e7e-11016c6684f2" # Системные объекты CODESYS, которые нужно игнорировать при выгрузке кода SYSTEM_IGNORED = [ "adb5cb65-8e1d-4a00-b70a-375ea27582f3", # Library Manager "21d4fe94-4123-4e23-9091-ead220afbd1f", # SymbolConfig_Application ] @classmethod def get_type_name(cls, guid_str): """Маппит GUID в понятную строку типа""" g = guid_str.lower() if g in [cls.METHOD, cls.METHOD_ABSTRACT, cls.METHOD_INTERFACE]: return "METHOD" if g in [cls.PROP_ACCESSOR, cls.PROP_ACC_INT]: return "PROPERTY_ACCESSOR" if g == cls.PROPERTY: return "PROPERTY" if g == cls.ACTION: return "ACTION" if g == cls.POU: return "POU" if g == cls.INTERFACE: return "INTERFACE" if g == cls.DUT: return "DUT" if g == cls.GVL: return "GVL" if g == cls.NVL: return "NVL" if g == cls.FOLDER: return "FOLDER" if g == cls.TASK_CONFIG: return "TASK_CONFIG" return None class ASTParser(object): def __init__(self, project_node, journal, projects_root=None): self.project = project_node self.journal = journal # Если projects_root передали — берем его, иначе пытаемся взять из глобальной видимости if projects_root: self.projects_root = projects_root else: import __main__ self.projects_root = getattr(__main__, 'projects', None) def get_project_map(self): """Собирает список всех путей объектов (без контента)""" self.journal.LogInfo("Сбор структуры проекта...") folders = [] objects = [] members = [] self._collect_ordered(self.project, "", folders, objects, members) return folders + objects + members def _collect_ordered(self, parent, current_path, folders, objects, members): # Проверяем, является ли parent корнем всего проекта (для секции POU) is_project_root = not hasattr(parent, 'type') children = parent.get_children(False) for child in children: name = child.get_name() guid = str(child.type).lower() # ЗАЩИТА: Если обходим корень проекта, игнорируем "железную" часть ПЛК if is_project_root and guid in [CodesysTypes.PLC_DEVICE, CodesysTypes.PLC_LOGIC]: continue # Игнорируем известные системные объекты CODESYS, чтобы не спамить ворнингами if guid in CodesysTypes.SYSTEM_IGNORED: continue # Определяем тип узла node_type = CodesysTypes.get_type_name(guid) if not node_type: self.journal.LogWarning("Пропущен неизвестный GUID: {0} ({1})".format(guid, name)) continue # Логика формирования путей (ST-классы/POU через точку, папки через слэш) is_pou_parent = hasattr(parent, 'has_textual_declaration') and not parent.is_folder if not current_path: new_path = name elif is_pou_parent: new_path = "{0}.{1}".format(current_path, name) else: new_path = "{0}/{1}".format(current_path, name) # Распределяем по спискам на основе типа if node_type == "FOLDER": folders.append({"path": new_path, "type": "FOLDER", "pou_kind": None, "obj": child}) self._collect_ordered(child, new_path, folders, objects, members) elif node_type in ["POU", "INTERFACE"]: objects.append({"path": new_path, "type": node_type, "pou_kind": None, "obj": child}) # Рекурсивно собираем внутренние методы/свойства класса/POU self._collect_ordered(child, new_path, folders, objects, members) elif node_type in ["DUT", "GVL", "NVL", "TASK_CONFIG"]: objects.append({"path": new_path, "type": node_type, "pou_kind": None, "obj": child}) elif node_type in ["METHOD", "ACTION", "PROPERTY"]: members.append({"path": new_path, "type": node_type, "pou_kind": None, "obj": child}) # Если это свойство (PROPERTY), соберем его GET/SET акцессоры if node_type == "PROPERTY": self._collect_property_accessors(child, new_path, members) def _collect_property_accessors(self, prop_node, current_path, members): """Вспомогательный метод сбора GET/SET для свойств""" try: for acc in prop_node.get_children(False): acc_name = acc.get_name() acc_type = CodesysTypes.get_type_name(str(acc.type)) if acc_type == "PROPERTY_ACCESSOR": members.append({ "path": "{0}.{1}".format(current_path, acc_name), "type": "PROPERTY_ACCESSOR", "pou_kind": None, "obj": acc }) except Exception as e: self.journal.LogWarning("Ошибка сбора акцессоров для {0}: {1}".format(current_path, str(e))) def get_node_payload(self, node_info): """Формирует payload с контентом для отправки на сервер""" obj = node_info['obj'] node_type = node_info['type'] payload = { "path": node_info['path'], "type": node_type, "declaration": "", "implementation": "" } pou_kind = node_info.get('pou_kind') if pou_kind is not None: payload["pou_kind"] = pou_kind # Для папок или пустых узлов контент не читаем if node_type == "FOLDER" or obj is None: return payload # Чтение декларации if hasattr(obj, 'has_textual_declaration') and obj.has_textual_declaration: try: payload["declaration"] = obj.textual_declaration.text except Exception as e: self.journal.LogWarning("Не удалось прочитать declaration для {0}: {1}".format( node_info['path'], str(e) )) # Чтение реализации if hasattr(obj, 'has_textual_implementation') and obj.has_textual_implementation: try: payload["implementation"] = obj.textual_implementation.text except Exception as e: self.journal.LogWarning("Не удалось прочитать implementation для {0}: {1}".format( node_info['path'], str(e) )) return payload # ========================================== # ВЕТКА DOWNLOAD / UPDATE (ЗАГРУЗКА В CODESYS) # ========================================== def sync_node(self, node_data): # type: (dict) -> bool full_path = node_data.get('path') node_type = node_data.get('type') action = node_data.get('action', 'update') plc_name, app_name, relative_path, target_app = self._parse_and_find_app(node_data) if not target_app: self.journal.LogError("Не удалось найти приложение для пути: " + str(full_path)) return False self.journal.LogInfo("Распознано: plc={0}, app={1}, relative_path={2}, action={3}, type={4}".format( plc_name, app_name, relative_path, action, node_type )) if action == 'delete': path_for_delete = relative_path if relative_path else app_name return self._handle_deletion(path_for_delete, target_app) obj = None is_new_root_pou = (plc_name == "POU" and target_app == self.projects_root.primary) if plc_name == "POU" and not relative_path and not is_new_root_pou: self.journal.LogInfo("Целевой объект найден прямо в корне POU. Имя: " + app_name) obj = target_app else: if plc_name == "POU" and not relative_path: target_name = app_name.split('.')[0] found = target_app.find(target_name, recursive=False) else: # Определяем путь поиска в зависимости от типа объекта if node_type in ["METHOD", "ACTION", "PROPERTY", "PROPERTY_ACCESSOR"]: # Все вложенные элементы используют точку: "Типы/T_fs_Zone.fire" # Разделяем на путь к папке и полное имя с точкой if '/' in relative_path: # Есть папки: "Типы/T_fs_Zone.fire" -> folder="Типы", qualified="T_fs_Zone.fire" folder_part = relative_path.rsplit('/', 1)[0] qualified_name = relative_path.rsplit('/', 1)[1] else: # Нет папок: "T_fs_Zone.fire" -> folder=None, qualified="T_fs_Zone.fire" folder_part = None qualified_name = relative_path parts = qualified_name.split('.') pou_name = parts[0] # Ищем родительский POU found = target_app.find(pou_name, recursive=True) if found and len(found) > 0: pou_obj = found[0] self.journal.LogInfo("POU найден: {0}".format(pou_name)) if node_type in ["METHOD", "ACTION"]: # Для METHOD и ACTION: ищем элемент по последнему сегменту member_name = parts[-1] self.journal.LogInfo("Ищем {0} в POU: {1}".format(node_type, member_name)) found = pou_obj.find(member_name, False) elif node_type == "PROPERTY": # Для PROPERTY: ищем свойство (второй элемент) if len(parts) >= 2: prop_name = parts[1] self.journal.LogInfo("Ищем PROPERTY в POU: {0}".format(prop_name)) found = pou_obj.find(prop_name, False) else: self.journal.LogError("Некорректный путь для PROPERTY: {0}".format(relative_path)) found = [] elif node_type == "PROPERTY_ACCESSOR": # Для PROPERTY_ACCESSOR: POU.Property.Get/Set if len(parts) >= 3: prop_name = parts[1] accessor_name = parts[2] self.journal.LogInfo("Ищем PROPERTY: {0}, затем аксессор: {1}".format(prop_name, accessor_name)) prop_found = pou_obj.find(prop_name, False) if prop_found: found = prop_found[0].find(accessor_name, False) else: self.journal.LogWarning("Property {0} не найдено".format(prop_name)) found = [] else: self.journal.LogError("Некорректный путь для PROPERTY_ACCESSOR: {0}".format(relative_path)) found = [] else: self.journal.LogWarning("POU не найден: {0}".format(pou_name)) found = [] else: # Для обычных объектов (POU, DUT, GVL) target_name = relative_path.split("/")[-1].split(".")[0] found = target_app.find(target_name, recursive=True) target_name = relative_path.split('.')[-1] if '.' in relative_path else relative_path.split('/')[-1] self.journal.LogInfo("target_name для поиска: " + target_name) if found and len(found) > 0: obj = found[0] self.journal.LogInfo("Объект найден, обновляем: " + target_name) else: if action == 'update': has_content = node_data.get('declaration') or node_data.get('implementation') if has_content: self.journal.LogInfo("Объект не найден, переходим к созданию: " + target_name) creation_path = relative_path if relative_path else app_name obj = self._handle_creation({ 'path': creation_path, 'type': node_type, 'pou_kind': node_data.get('pou_kind'), 'declaration': node_data.get('declaration'), 'implementation': node_data.get('implementation') }, target_app) else: self.journal.LogWarning("Объект не найден и нет контента для создания: " + str(full_path)) return False else: self.journal.LogWarning("Пропуск: Объект {0} не найден для действия {1}".format(full_path, action)) return False if not obj: self.journal.LogError("Не удалось создать/найти объект: " + str(full_path)) return False if node_type in ['POU', 'METHOD', 'PROPERTY', 'ACTION', 'PROPERTY_ACCESSOR']: return self._update_executable(obj, node_data) elif node_type in ['GVL', 'DUT']: return self._update_declaration_only(obj, node_data) return True def _parse_and_find_app(self, node_data): """ Парсит путь, пришедший от сервера, и находит контекст. Пример POU: "POU/Накопительный таймер с ресетом/TONR" -> plc="POU", app="Накопительный таймер с ресетом", relative_path="TONR" Пример ПЛК: "FFS_SAU/Application/Папка/Блок" -> plc="FFS_SAU", app="Application", relative_path="Папка/Блок" """ try: full_path = node_data.get('path', '') if not full_path: self.journal.LogError("Передан пустой путь") return None, None, None, None path_parts = full_path.split('/') if len(path_parts) < 2: self.journal.LogError("Путь слишком короткий для определения контекста: " + full_path) return None, None, None, None first_part = path_parts[0] # "POU" или Имя ПЛК (например, "FFS_SAU") second_part = path_parts[1] # Имя корневой папки в POU или "Application" в ПЛК # Относительный путь для поиска внутри найденного контекста relative_path = "/".join(path_parts[2:]) if len(path_parts) > 2 else "" # ========================================== # СЛУЧАЙ 1: Глобальная секция POU # ========================================== if first_part == "POU": # Ищем корневой элемент (папку или файл) во вкладке POU проекта for node in self.projects_root.primary.get_children(): if node.get_name() == second_part: # Возвращаем найденный узел (это может быть папка "Накопительный таймер с ресетом" # или одиночный файл "MyGVL" в корне POU) return "POU", second_part, relative_path, node # ЕСЛИ НЕ НАШЛИ: Объект новый (например, создали новую папку или GVL в корне POU во внешнем редакторе) self.journal.LogInfo("Корневой объект '{0}' не найден в POU. Возвращаем корень проекта для создания.".format(second_part)) # Чтобы _handle_creation создал объект в правильном месте, relative_path должен # содержать весь путь начиная со второго сегмента. creation_path = "/".join(path_parts[1:]) return "POU", second_part, creation_path, self.projects_root.primary # ========================================== # СЛУЧАЙ 2: Секция Устройства (ПЛК) # ========================================== else: # Ищем ПЛК с именем first_part for plc_root in self.projects_root.primary.get_children(): if plc_root.get_name() == first_part: logic_nodes = plc_root.find("Plc Logic", recursive=False) if logic_nodes: for logic_node in logic_nodes: for app in logic_node.get_children(): if app.get_name() == second_part: # Обычно "Application" return first_part, second_part, relative_path, app self.journal.LogError("Не удалось сопоставить путь с проектом CODESYS: " + full_path) return None, None, None, None except Exception as e: self.journal.LogError("Ошибка парсинга пути {0}: {1}".format(node_data.get('path'), str(e))) return None, None, None, None def _update_declaration_only(self, obj, data): """Обновление объектов только с текстовым контентом (GVL, DUT)""" try: if hasattr(obj, 'has_textual_declaration') and obj.has_textual_declaration: if 'declaration' in data: # obj.textual_declaration — это ScriptTextDocument # Используем метод replace(new_text=...) obj.textual_declaration.replace(new_text=data['declaration']) return True except Exception as e: print("Error updating declaration {0}: {1}".format(obj.get_name(), e)) return False def _handle_creation(self, data, target_app): """Создает целевой объект (POU/Method/Action/Property/DUT) в родителе""" path = data['path'] node_type = data['type'] self.journal.LogInfo("_handle_creation: path={0}, type={1}".format(path, node_type)) folder_node = self._ensure_path(path, target_app) if not folder_node: self.journal.LogError("_ensure_path вернул None для пути: " + path) return None # БЕЗОПАСНОЕ ЛОГИРОВАНИЕ: Защита от отсутствия метода get_name() у корня проекта parent_name = folder_node.get_name() if hasattr(folder_node, 'get_name') else "Корень проекта (POU)" self.journal.LogInfo("Родительская папка найдена: " + parent_name) # Извлекаем имя объекта target_name = path.split('.')[-1] if '.' in path else path.split('/')[-1] self.journal.LogInfo("Имя для создания: " + target_name) import __main__ lang_st = getattr(__main__, 'ImplementationLanguages', None) lang_st = lang_st.st if lang_st else None sys_pou_type = getattr(__main__, 'PouType', None) def _get_pou_kind(kind_id): if not sys_pou_type: return None if kind_id == 0: return sys_pou_type.Program if kind_id == 2: return sys_pou_type.Function return sys_pou_type.FunctionBlock try: # 1. Создание GVL if node_type == "GVL": self.journal.LogInfo("Создание GVL: " + target_name) obj = folder_node.create_gvl(name=target_name) self.journal.LogInfo("GVL создан успешно") return obj # 2. Создание DUT elif node_type == "DUT": self.journal.LogInfo("Создание DUT: " + target_name) obj = folder_node.create_dut(name=target_name) self.journal.LogInfo("DUT создан успешно") return obj # 3. Создание МЕТОДА elif node_type == 'METHOD': pou_parts = path.split('.') if len(pou_parts) < 2: self.journal.LogError("Некорректный путь для метода: " + path) return None pou_path = '.'.join(pou_parts[:-1]) method_name = pou_parts[-1] pou_folder = self._ensure_path(pou_path, target_app) if not pou_folder: self.journal.LogError("Не найдена папка для POU-родителя: " + pou_path) return None pou_name = pou_path.split('/')[-1] res = pou_folder.find(pou_name, recursive=False) if res and len(res) > 0: pou_node = res[0] # ЗАЩИТА: проверяем, не существует ли уже такой метод existing = pou_node.find(method_name, False) if existing and len(existing) > 0: self.journal.LogInfo("Метод {0} уже существует в {1}, возвращаем его".format(method_name, pou_name)) return existing[0] else: self.journal.LogInfo("Авто-создание POU-контейнера: " + pou_name) target_kind = _get_pou_kind(data.get('pou_kind', 1)) pou_folder.create_pou(name=pou_name, type=target_kind, language=lang_st) pou_node = pou_folder.find(pou_name, recursive=False)[0] self.journal.LogInfo("Создание метода {0} в {1}".format(method_name, pou_name)) obj = pou_node.create_method(name=method_name, return_type="BOOL", language=lang_st) self.journal.LogInfo("Метод создан успешно") return obj # 4. Создание ACTION elif node_type == 'ACTION': # ACTION приходит как "TfpsZone.count_event" (через точку) parts = path.split('.') if len(parts) < 2: self.journal.LogError("Некорректный путь для action: " + path) return None # Извлекаем путь к POU и имя ACTION pou_path = '.'.join(parts[:-1]) action_name = parts[-1] self.journal.LogInfo("ACTION: pou_path={0}, action_name={1}".format(pou_path, action_name)) # Находим папку, где должен лежать POU pou_folder = self._ensure_path(pou_path, target_app) if not pou_folder: self.journal.LogError("Не найдена папка для POU-родителя: " + pou_path) return None # Имя POU — последний элемент пути после слэша pou_name = pou_path.split('/')[-1] res = pou_folder.find(pou_name, recursive=False) if res and len(res) > 0: pou_node = res[0] self.journal.LogInfo("POU найден: " + pou_name) # ЗАЩИТА: проверяем, не существует ли уже такой action existing = pou_node.find(action_name, False) if existing and len(existing) > 0: self.journal.LogInfo("Action {0} уже существует в {1}, возвращаем его".format(action_name, pou_name)) return existing[0] else: self.journal.LogInfo("Авто-создание POU-контейнера: " + pou_name) target_kind = _get_pou_kind(data.get('pou_kind', 1)) pou_folder.create_pou(name=pou_name, type=target_kind, language=lang_st) pou_node = pou_folder.find(pou_name, recursive=False)[0] self.journal.LogInfo("Создание action {0} в {1}".format(action_name, pou_name)) obj = pou_node.create_action(name=action_name, language=lang_st) self.journal.LogInfo("Action создан успешно") return obj # 5. Создание PROPERTY elif node_type == 'PROPERTY': pou_parts = path.split('.') if len(pou_parts) < 2: self.journal.LogError("Некорректный путь для property: " + path) return None pou_path = '.'.join(pou_parts[:-1]) prop_name = pou_parts[-1] pou_folder = self._ensure_path(pou_path, target_app) if not pou_folder: self.journal.LogError("Не найдена папка для POU-родителя: " + pou_path) return None pou_name = pou_path.split('/')[-1] res = pou_folder.find(pou_name, recursive=False) if res and len(res) > 0: pou_node = res[0] # ЗАЩИТА: проверяем, не существует ли уже такое property existing = pou_node.find(prop_name, False) if existing and len(existing) > 0: self.journal.LogInfo("Property {0} уже существует в {1}, возвращаем его".format(prop_name, pou_name)) return existing[0] else: self.journal.LogInfo("Авто-создание POU-контейнера: " + pou_name) target_kind = _get_pou_kind(data.get('pou_kind', 1)) pou_folder.create_pou(name=pou_name, type=target_kind, language=lang_st) pou_node = pou_folder.find(pou_name, recursive=False)[0] self.journal.LogInfo("Создание property {0} в {1}".format(prop_name, pou_name)) obj = pou_node.create_property(name=prop_name, return_type="BOOL") self.journal.LogInfo("Property создан успешно") return obj # 6. Создание PROPERTY_ACCESSOR (Get/Set) elif node_type == 'PROPERTY_ACCESSOR': parts = path.split('.') if len(parts) < 3: self.journal.LogError("Некорректный путь для property accessor: " + path) return None pou_path = parts[0] prop_name = parts[1] accessor_name = parts[2] # Get или Set pou_folder = self._ensure_path(pou_path, target_app) if not pou_folder: self.journal.LogError("Не найдена папка для POU-родителя: " + pou_path) return None pou_name = pou_path.split('/')[-1] pou_res = pou_folder.find(pou_name, recursive=False) if not pou_res or len(pou_res) == 0: self.journal.LogError("POU не найден: " + pou_name) return None pou_node = pou_res[0] # Ищем существующее property prop_res = pou_node.find(prop_name, recursive=False) if not prop_res or len(prop_res) == 0: self.journal.LogInfo("Property не найдено, создаем: " + prop_name) pou_node.create_property(name=prop_name, return_type="BOOL") prop_res = pou_node.find(prop_name, recursive=False) prop_node = prop_res[0] # Аксессоры создаются автоматически, просто находим нужный acc_res = prop_node.find(accessor_name, recursive=False) if acc_res and len(acc_res) > 0: self.journal.LogInfo("Accessor {0} найден".format(accessor_name)) return acc_res[0] else: self.journal.LogError("Не удалось найти accessor: " + accessor_name) return None # 7. Создание POU elif node_type == 'POU': kind_val = data.get('pou_kind', 1) target_kind = _get_pou_kind(kind_val) ret_type = "BOOL" if kind_val == 2 else None self.journal.LogInfo("Создание POU: {0}, type={1}".format(target_name, target_kind)) obj = folder_node.create_pou( name=target_name, type=target_kind, language=lang_st, return_type=ret_type ) self.journal.LogInfo("POU создан успешно") return obj else: self.journal.LogError("Неизвестный тип объекта: " + node_type) return None except Exception as e: self.journal.LogError("Ошибка в _handle_creation для {0}: {1}".format(target_name, str(e))) import traceback self.journal.LogError("Traceback: " + traceback.format_exc()) return None def _ensure_path(self, full_path, root_app): """ Создает структуру папок СТРОГО до родителя целевого объекта. root_app - корневой узел приложения/библиотеки Примеры: "Типы/T_cmd_copy" -> создаст папку "Типы", вернёт её "FFS/POU/FB_Motor" -> создаст "FFS", потом "POU", вернёт "POU" "FB_Motor" -> вернёт root_app (объект в корне) """ # 1. Вычисляем чистый путь до папки родителя if "." in full_path: # Это метод/свойство: "Folder/FB_Motor.Method" -> parent_part = "Folder/FB_Motor" parent_part = full_path.rsplit(".", 1)[0] folder_path = parent_part.rsplit("/", 1)[0] if "/" in parent_part else "" else: # Это обычный объект: "Типы/T_cmd_copy" -> folder_path = "Типы" folder_path = full_path.rsplit("/", 1)[0] if "/" in full_path else "" # Если объект должен лежать прямо в корне (нет папок в пути) if not folder_path: return root_app current_node = root_app folder_steps = [step for step in folder_path.split("/") if step] # 2. Итерируемся по шагам и создаем недостающие папки for step in folder_steps: res = current_node.find(step, recursive=False) if res and len(res) > 0: current_node = res[0] self.journal.LogInfo("Папка уже существует: " + step) # Заменили LogText на LogInfo else: try: self.journal.LogInfo("Создаю папку в CODESYS: " + step) # Заменили LogText на LogInfo current_node.create_folder(step) # Перепроверяем создание узла retry = current_node.find(step, recursive=False) if retry and len(retry) > 0: current_node = retry[0] else: self.journal.LogError("Не удалось найти созданную папку после create_folder: " + step) return None except Exception as e: self.journal.LogError("Ошибка создания папки {0}: {1}".format(step, str(e))) return None return current_node def _update_executable(self, obj, data): """Обновление исполняемых объектов (POU, METHOD, PROPERTY, ACTION, PROPERTY_ACCESSOR)""" try: obj_name = obj.get_name() if hasattr(obj, 'get_name') else "Unknown" node_type = data.get('type', 'Unknown') self.journal.LogInfo("Обновление {0}: {1}".format(node_type, obj_name)) # ACTION имеет ТОЛЬКО implementation, declaration всегда пустая if node_type == 'ACTION': if hasattr(obj, 'has_textual_implementation') and obj.has_textual_implementation: impl_content = data.get('implementation', '') if impl_content: obj.textual_implementation.replace(new_text=impl_content) self.journal.LogInfo(" ✓ ACTION implementation обновлена") else: self.journal.LogWarning(" ⚠ ACTION: пустая implementation") return True # Для остальных типов: обновляем declaration и/или implementation # 1. Обновляем декларацию (переменные VAR ... END_VAR) if hasattr(obj, 'has_textual_declaration') and obj.has_textual_declaration: decl_content = data.get('declaration', '') if decl_content: obj.textual_declaration.replace(new_text=decl_content) self.journal.LogInfo(" ✓ Declaration обновлена") # 2. Обновляем реализацию (тело кода на ST) if hasattr(obj, 'has_textual_implementation') and obj.has_textual_implementation: impl_content = data.get('implementation', '') if impl_content: obj.textual_implementation.replace(new_text=impl_content) self.journal.LogInfo(" ✓ Implementation обновлена") return True except Exception as e: self.journal.LogError("Ошибка в _update_executable для {0}: {1}".format( obj.get_name() if hasattr(obj, 'get_name') else "Unknown", str(e) )) import traceback self.journal.LogError("Traceback: " + traceback.format_exc()) return False def _handle_deletion(self, path, target_app): """Безопасное удаление объекта из проекта CODESYS""" try: # Извлекаем только имя конечного объекта для точечного удаления target_name = path.split('.')[-1] if '.' in path else path.split('/')[-1] found = target_app.find(target_name, recursive=True) if found and len(found) > 0: obj_to_remove = found[0] # Безопасно получаем имя для красивого лога obj_name = obj_to_remove.get_name() if hasattr(obj_to_remove, 'get_name') else target_name obj_to_remove.remove() self.journal.LogInfo("Успешно удалено из CODESYS: {0} ({1})".format(path, obj_name)) return True else: self.journal.LogInfo("Удаление пропущено: объект по пути '{0}' не найден".format(path)) return True except Exception as e: self.journal.LogError("Ошибка при удалении {0}: {1}".format(path, str(e))) return False