# encoding:utf-8 # export_plc_src.py # Экспорт POU / GVL / DUT из открытого проекта CODESYS в текстовые файлы # для хранения в git и работы с Claude Code. # # Запуск: # 1) Внутри CODESYS: Tools -> Scripting -> Execute Script File -> выбрать этот файл # 2) Из командной строки (без открытия GUI): # "C:\Program Files (x86)\CODESYS 3.5.17.30\CODESYS\Common\CODESYS.exe" ^ # --Profile="CODESYS V3.5 SP17 Patch 3" --runscript="export_plc_src.py" ^ # --project="D:\path\to\ТвойПроект.project" # # Проверено для установки: CODESYS V3.5 SP17 Patch 3 (32-bit) # # По умолчанию папка plc_src создаётся автоматически рядом с файлом .project. # Если авто-определение не сработает, впиши путь вручную в MANUAL_OUTPUT_ROOT ниже. from __future__ import print_function import os import codecs import re PRAGMA_RE = re.compile(r"^\s*\{[^}]*\}\s*") def strip_leading_pragmas(text): """Убирает ведущие блоки вида {attribute 'qualified_only'} перед проверкой ключевого слова.""" while True: new_text = PRAGMA_RE.sub("", text, 1) if new_text == text: return text text = new_text def get_project_file_path(proj): """Пытается получить полный путь к .project файлу разными способами, так как имя атрибута может отличаться между версиями CODESYS.""" for attr in ("path", "get_path", "project_path", "full_path"): try: val = getattr(proj, attr) val = val() if callable(val) else val if val: return val except Exception: pass # Фолбэк: вытащить путь из текстового представления объекта, # например "Project(Project=0, stPath=C:\...\*.project)" try: s = str(proj) m = re.search(r"stPath=(.*?)\)", s) if m: return m.group(1) except Exception: pass return None def safe_name(obj): try: return obj.get_name(False) except Exception: return "unnamed" def get_declaration_text(obj): try: if hasattr(obj, "textual_declaration") and obj.textual_declaration is not None: return obj.textual_declaration.text except Exception: return None return None def get_text(obj, declaration_text): """Собирает декларацию + реализацию (если есть) в единый текст.""" text = declaration_text or "" try: if hasattr(obj, "has_textual_implementation") and obj.has_textual_implementation: text += "\r\n(*----- IMPLEMENTATION -----*)\r\n" text += obj.textual_implementation.text except Exception as e: text += "(* ошибка чтения implementation: %s *)\r\n" % e return text def classify(declaration_text): """Определяет тип объекта по первому ключевому слову декларации.""" if not declaration_text: return None stripped = strip_leading_pragmas(declaration_text.strip()).strip().upper() if stripped.startswith(("PROGRAM", "FUNCTION_BLOCK", "FUNCTION", "INTERFACE", "METHOD", "ACTION", "PROPERTY")): return "pou" if stripped.startswith("VAR_GLOBAL"): return "gvl" if stripped.startswith("TYPE"): return "dut" return None def write_file(folder, name, text): path = os.path.join(folder, name + ".st") with codecs.open(path, "w", encoding="utf-8") as f: f.write(text) print(" -> %s" % path) def sanitize_folder_name(name): """Убирает символы, недопустимые в имени папки Windows.""" invalid = '\\/:*?"<>|' for ch in invalid: name = name.replace(ch, "_") name = name.strip() return name if name else "device" def make_dirs(base): d = { "pou": os.path.join(base, "POU"), "gvl": os.path.join(base, "GVL"), "dut": os.path.join(base, "DUT"), } for p in d.values(): if not os.path.exists(p): os.makedirs(p) return d def is_device_node(obj): try: return bool(getattr(obj, "is_device", False)) except Exception: return False def walk(node, dirs, exported): for child in node.get_children(False): name = safe_name(child) decl = get_declaration_text(child) kind = classify(decl) if kind == "pou": print("POU: %s" % name) write_file(dirs["pou"], name, get_text(child, decl)) exported["pou"] += 1 elif kind == "gvl": print("GVL: %s" % name) write_file(dirs["gvl"], name, get_text(child, decl)) exported["gvl"] += 1 elif kind == "dut": print("DUT: %s" % name) write_file(dirs["dut"], name, get_text(child, decl)) exported["dut"] += 1 else: exported["skipped"] += 1 # рекурсивно обходим детей (для FB с методами, папок и т.д.) walk(child, dirs, exported) # ==== НАСТРОЙКА ==== # Оставь пустой строкой "" чтобы папка plc_src создавалась автоматически # рядом с файлом .project. Укажи путь вручную только если авто-определение # не сработает (см. вывод скрипта при запуске). MANUAL_OUTPUT_ROOT = r"" # ===================================================== proj = projects.primary if proj is None: print("ОШИБКА: нет открытого проекта. Открой .project перед запуском скрипта.") else: if MANUAL_OUTPUT_ROOT: OUTPUT_ROOT = MANUAL_OUTPUT_ROOT print("Папка вывода задана вручную: %s" % OUTPUT_ROOT) else: project_file = get_project_file_path(proj) if project_file: project_dir = os.path.dirname(project_file) OUTPUT_ROOT = os.path.join(project_dir, "plc_src") print("Проект найден: %s" % project_file) print("Папка вывода определена автоматически: %s" % OUTPUT_ROOT) else: OUTPUT_ROOT = None print("ОШИБКА: не удалось определить путь к .project автоматически.") print("Задай путь вручную в переменной MANUAL_OUTPUT_ROOT и запусти снова.") if OUTPUT_ROOT: exported = {"pou": 0, "gvl": 0, "dut": 0, "skipped": 0} print("--- Экспорт в %s ---" % OUTPUT_ROOT) top_children = proj.get_children(False) device_nodes = [c for c in top_children if is_device_node(c)] if device_nodes: for device in device_nodes: dev_name = sanitize_folder_name(safe_name(device)) print("=== Устройство: %s ===" % dev_name) dev_dirs = make_dirs(os.path.join(OUTPUT_ROOT, dev_name)) walk(device, dev_dirs, exported) # на случай текстовых объектов вне дерева устройств (редко, но на всякий случай) other_dirs = make_dirs(os.path.join(OUTPUT_ROOT, "_common")) for child in top_children: if not is_device_node(child): walk(child, other_dirs, exported) else: # устройств не нашлось (или is_device не сработал) -- работаем как раньше, без разбивки print("Устройства верхнего уровня не найдены, экспорт без разбивки по папкам.") dirs = make_dirs(OUTPUT_ROOT) walk(proj, dirs, exported) print("--- Готово: POU=%d, GVL=%d, DUT=%d, пропущено=%d ---" % ( exported["pou"], exported["gvl"], exported["dut"], exported["skipped"] ))