Files

216 lines
8.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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"]
))