From d3da51217812bf9f14f8b599a5fff986f60b27b3 Mon Sep 17 00:00:00 2001 From: Iaroshuk Dmitrii Date: Fri, 3 Jul 2026 11:37:46 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20?= =?UTF-8?q?=D1=81=D0=BA=D1=80=D0=B8=D0=BF=D1=82=20=D1=8D=D0=BA=D1=81=D0=BF?= =?UTF-8?q?=D0=BE=D1=80=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Скрипты/export_plc_src.py | 215 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 Скрипты/export_plc_src.py diff --git a/Скрипты/export_plc_src.py b/Скрипты/export_plc_src.py new file mode 100644 index 0000000..e62299b --- /dev/null +++ b/Скрипты/export_plc_src.py @@ -0,0 +1,215 @@ +# 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"] + ))