Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8017d65662 | ||
|
|
787f875581 |
Binary file not shown.
@@ -1,3 +1,140 @@
|
|||||||
# PLC_Library
|
# PLC_Library
|
||||||
|
|
||||||
Моя библиотека для CoDeSys 3.5
|
Библиотека функций (FUNCTION) и функциональных блоков (FUNCTION_BLOCK) на языке **Structured Text (ST)** для CoDeSys 3.5.
|
||||||
|
Библиотека содержит фильтры сигналов, обработку кнопок и дискретных сигналов, работу со статусами и авариями аналоговых величин, преобразование типов данных, драйверы модулей Fastwel и вспомогательные структуры/алгоритмы.
|
||||||
|
|
||||||
|
## Структура
|
||||||
|
|
||||||
|
```text
|
||||||
|
PLC_Library/
|
||||||
|
├── Filters/ — фильтрация и усреднение сигналов
|
||||||
|
├── Fastwel/ — преобразование сигналов модулей Fastwel (АЦП/ЦАП)
|
||||||
|
├── Преобразование значений/ — преобразование типов данных
|
||||||
|
├── Общие/ — общие блоки (кнопки, таймеры, статусы, очереди/стеки, ПИД, сигнализация)
|
||||||
|
├── Иное/ — прочие алгоритмы
|
||||||
|
└── Structs/ — пользовательские типы данных
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Filters — Фильтры
|
||||||
|
|
||||||
|
| Имя | Тип | Описание |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `ExpRunAverage` | FUNCTION (REAL) | Экспоненциальное скользящее среднее с постоянным коэффициентом `K`. |
|
||||||
|
| `ExpRunAverageAdaptive` | FUNCTION (REAL) | Экспоненциальное скользящее среднее с адаптивным коэффициентом: если изменение входа превышает `Delta`, используется увеличенный коэффициент `K2` вместо `K1` (быстрая реакция на резкие изменения). |
|
||||||
|
| `RunMiddleArifm` | FUNCTION_BLOCK | Скользящее среднее арифметическое по кольцевому буферу размером до 32 отсчётов (`N`), с возможностью сброса/переинициализации (`RST`). |
|
||||||
|
| `MultipleClick` | FUNCTION_BLOCK | Детектор серии нажатий кнопки (одиночный/двойной/тройной клик) по паузе между нажатиями `TimeToWait`, с удержанием результата на `TimeRelease`. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fastwel — Модули ввода/вывода Fastwel
|
||||||
|
|
||||||
|
Функции перевода "сырых" значений АЦП/кодов модулей Fastwel в физические единицы (мА, В, Гц) и обратно (ЦАП), а также формирование статуса аналогового канала.
|
||||||
|
|
||||||
|
| Имя | Тип | Описание |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `AIM721_AI` | FUNCTION (REAL) | АЦП → 0…20 мА. |
|
||||||
|
| `AIM722_AI` | FUNCTION (REAL) | АЦП → 0…20 мА (свой коэффициент модуля). |
|
||||||
|
| `AIM723_AI` | FUNCTION (REAL) | АЦП → 4…20 мА. |
|
||||||
|
| `AIM726_AI` | FUNCTION (REAL) | АЦП (DWORD) → 0…40 В. |
|
||||||
|
| `AIM727_AI` | FUNCTION (REAL) | АЦП (DINT) → 0…40 В. |
|
||||||
|
| `AIM791_AI` | FUNCTION (REAL) | АЦП → 0…5 мА / 0…20 мА / 4…20 мА (выбор диапазона через `RANGE`). |
|
||||||
|
| `AIM730_AO` | FUNCTION (WORD) | мА (0…20 или 4…20, выбор `RANGE`) → код ЦАП модуля AIM730. |
|
||||||
|
| `AIM731_AO` | FUNCTION (WORD) | В (±10 или 0…10, выбор `RANGE`) → код ЦАП модуля AIM731. |
|
||||||
|
| `DIM764_DI` | FUNCTION (REAL) | Период/интервал импульсного входа модуля → частота в Гц (выбор режима `RANGE`). |
|
||||||
|
| `AIM725_Status` | FUNCTION_BLOCK | Формирование статуса аналогового канала (`AnalogValueState`): аварии/предупреждения по верхнему и нижнему порогам с гистерезисом и выдержками времени, а также диагностика обрыва связи по битам `Modul_status` конкретного канала (`Chanel_num`). Дополнительно выдаёт `T_mod` — значение, приведённое к INT. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Преобразование значений — Conversions
|
||||||
|
|
||||||
|
| Имя | Тип | Описание |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `DW_TO_REAL` | FUNCTION (REAL) | Побитовая реинтерпретация DWORD как REAL (без изменения значения, через указатель). |
|
||||||
|
| `REAL_TO_DW` | FUNCTION (DWORD) | Побитовая реинтерпретация REAL как DWORD (обратное преобразование). |
|
||||||
|
| `DWORD_OF_2WORD` | FUNCTION (DWORD) | Сборка DWORD из двух WORD (старшее + младшее слово). |
|
||||||
|
| `WORD_OF_DWORD` | FUNCTION (WORD) | Извлечение младшего или старшего слова (WORD) из DWORD по флагу `N`. |
|
||||||
|
| `Norm_real` | FUNCTION (REAL) | Линейная нормировка значения `X` по двум точкам `(X1,Y1)`–`(X2,Y2)` (масштабирование диапазона). |
|
||||||
|
| `NTC_termistor_T` | FUNCTION (REAL) | Перевод сопротивления NTC-термистора в температуру по уравнению Бета (`R0`, `T0`, `b`). |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Общие — Common
|
||||||
|
|
||||||
|
### Дискретные сигналы / кнопки / таймеры
|
||||||
|
|
||||||
|
| Имя | Тип | Описание |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `Button_acceleration` | FUNCTION_BLOCK | Ускоренное изменение значения (`Out`, VAR_IN_OUT) при удержании кнопки: одиночный клик даёт приращение `D_0`, короткое удержание — приращения `D_1` с шагом `T_D_1` после `T_W_1`, длительное удержание — `D_2` с шагом `T_D_2` после `T_W_2`. Есть опция обнуления `Zeroes`. |
|
||||||
|
| `Pulse_Switch` | FUNCTION_BLOCK | Формирует импульсы по 500 мс по переднему (`Switch_on`) и заднему (`Switch_off`) фронтам управляющего сигнала. |
|
||||||
|
| `TONOF` | FUNCTION_BLOCK | Комбинированная задержка включения/выключения (TON + TOF) для одного сигнала. |
|
||||||
|
| `TOF_RST` | FUNCTION_BLOCK | Таймер выключения (TOF) с принудительным сбросом `RST`. |
|
||||||
|
| `TP_RST` | FUNCTION_BLOCK | Одиночный импульс (TP) с принудительным сбросом `RST`; повторный фронт во время импульса игнорируется. |
|
||||||
|
| `BLINK_LMP` | FUNCTION_BLOCK | Управление лампой по коду состояния `State` (выкл/вкл/мигание с одной из 3 частот, задаваемых внешними пульсарами). |
|
||||||
|
| `BUZZER_CTRL` | FUNCTION_BLOCK | Управление звонком/зуммером по коду состояния, с возможностью временного и перманентного отключения звука (`Sound_off`, `PermanentOFF_Enable`). |
|
||||||
|
| `CONFIRMATION` | FUNCTION_BLOCK | Логика квитирования аварии: формирует сигналы ламп аварии/квитирования и управления звонком. |
|
||||||
|
|
||||||
|
### Пороговые сигналы / гистерезис
|
||||||
|
|
||||||
|
| Имя | Тип | Описание |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `HYST` | FUNCTION_BLOCK | Гистерезисный компаратор с раздельными порогами включения (`ON`) и выключения (`OFF`); поддерживает прямую и обратную логику. |
|
||||||
|
| `ALARM_1` | FUNCTION_BLOCK | Контроль значения `X` на выход за один диапазон `[LO_1…HI_1]` с гистерезисом `HYS`. |
|
||||||
|
| `ALARM_2` | FUNCTION_BLOCK | То же для двух диапазонов одновременно. |
|
||||||
|
| `ALARM_3` | FUNCTION_BLOCK | То же для трёх диапазонов одновременно. |
|
||||||
|
|
||||||
|
### Статус аналоговых сигналов
|
||||||
|
|
||||||
|
| Имя | Тип | Описание |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `STATUS_ANALOG_M` | FUNCTION_BLOCK | Формирование статуса аналогового значения REAL (`AnalogValueState`) с гистерезисом возврата и задержками срабатывания по каждому из уровней (fail/warn hi/low, no_signal); поддерживает принудительную установку каждого статуса извне. |
|
||||||
|
| `STATUS_ANALOG_M_I` | FUNCTION_BLOCK | Аналог `STATUS_ANALOG_M`, но для входа типа INT. |
|
||||||
|
|
||||||
|
### Диагностика связи
|
||||||
|
|
||||||
|
| Имя | Тип | Описание |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `Status_SU_Connect` | FUNCTION_BLOCK | Контроль связи по "сердцебиению" (`HeartBit`): авария, если сигнал не меняет состояние дольше `T_Pulse`. |
|
||||||
|
| `Status_Connect_Counter` | FUNCTION_BLOCK | Контроль связи по счётчику `Counter` от удалённого устройства: авария, если счётчик не меняется дольше `T_Error`. |
|
||||||
|
|
||||||
|
### Регулирование
|
||||||
|
|
||||||
|
| Имя | Тип | Описание |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `PIDcontrol` | FUNCTION_BLOCK | ПИД-регулятор с фильтрацией дифференциальной составляющей, зоной нечувствительности (`DBmin/DBmax`), ограничением выхода, антивиндапом и ручным режимом (`ManOn`/`Manual`). |
|
||||||
|
|
||||||
|
### Структуры данных
|
||||||
|
|
||||||
|
| Имя | Тип | Описание |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `QUEUE_I16` | FUNCTION_BLOCK | Кольцевая очередь FIFO на 16 элементов типа INT (`PUSH`/`POP`, флаги `EMPTY`/`FULL`, сброс `RST`). |
|
||||||
|
| `QUEUE_I32` | FUNCTION_BLOCK | Кольцевая очередь FIFO на 32 элемента типа INT (`PUSH`/`POP`, флаги `EMPTY`/`FULL`, сброс `RST`). |
|
||||||
|
| `STACK_I16` | FUNCTION_BLOCK | Стек LIFO на 16 элементов типа INT (`PUSH`/`POP`, флаги `EMPTY`/`FULL`, сброс `RST`). |
|
||||||
|
| `STACK_I32` | FUNCTION_BLOCK | Стек LIFO на 32 элемента типа INT (`PUSH`/`POP`, флаги `EMPTY`/`FULL`, сброс `RST`). |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Иное — Other
|
||||||
|
|
||||||
|
| Имя | Тип | Описание |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `DFS` | PROGRAM | Обход графа в глубину (Depth-First Search) по матрице смежности `graph` фиксированного размера (12 вершин); использует `STACK_I16`. Возвращает список посещённых из `Start` вершин в массиве `connected`. Предназначен для копирования в пользовательскую программу. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Structs — Пользовательские типы
|
||||||
|
|
||||||
|
| Имя | Тип | Описание |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `AnalogValueState` | DUT (перечисление, `qualified_only`, `strict`) | Статус аналогового значения: `undefined`(0), `normal`(1), `warn_low`(2), `fail_low`(3), `warn_hi`(4), `fail_hi`(5), `no_signal`(-1). Используется в `AIM725_Status`, `STATUS_ANALOG_M`, `STATUS_ANALOG_M_I`. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Замечания по использованию
|
||||||
|
|
||||||
|
- Функции `DW_TO_REAL` / `REAL_TO_DW` выполняют **побитовую** реинтерпретацию через указатель (`ADR`), а не преобразование значения — использовать осторожно, с учётом платформозависимости работы с указателями.
|
||||||
|
- Блоки `STATUS_ANALOG_M[_I]` и `AIM725_Status` используют единый тип `AnalogValueState` и схожую логику приоритетов статусов: `no_signal` > `fail_hi` > `fail_low` > `warn_hi` > `warn_low` > `normal`.
|
||||||
|
- `TOF_RST` и `TP_RST` — версии стандартных МЭК-таймеров TOF/TP с добавленным входом принудительного сброса `RST`.
|
||||||
|
- `QUEUE_I16` и `STACK_I16` имеют фиксированный размер 16 элементов (константа `n:=15`); для изменения размера нужно менять константу `n` и размер массива.
|
||||||
|
- `DFS` рассчитан на графы фиксированного размера (константа `n:=12`) и предназначен для копирования кода в пользовательскую программу, а не для использования "как есть" в виде библиотечного вызова.
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,66 @@
|
|||||||
|
(*
|
||||||
|
===============================================================================
|
||||||
|
Project : PLC_Library
|
||||||
|
Device : -
|
||||||
|
Path : Общие
|
||||||
|
Name : QUEUE_I32
|
||||||
|
Type : FUNCTION_BLOCK
|
||||||
|
===============================================================================
|
||||||
|
*)
|
||||||
|
|
||||||
|
FUNCTION_BLOCK QUEUE_I32
|
||||||
|
(*
|
||||||
|
Очередь FIFO на 32 элемента типа INT
|
||||||
|
*)
|
||||||
|
VAR_INPUT
|
||||||
|
Din : INT; (* Переменная для записи в очередь *)
|
||||||
|
E : BOOL := TRUE; (* Включение очереди *)
|
||||||
|
POP : BOOL; (* Извлечение из очереди *)
|
||||||
|
PUSH : BOOL; (* Добавление в очередь *)
|
||||||
|
RST : BOOL; (* Сброс *)
|
||||||
|
END_VAR
|
||||||
|
VAR_OUTPUT
|
||||||
|
Dout : INT; (* Значение, извлеченное из очереди *)
|
||||||
|
EMPTY : BOOL := TRUE;
|
||||||
|
FULL : BOOL;
|
||||||
|
END_VAR
|
||||||
|
VAR
|
||||||
|
queue : ARRAY[0..n] OF INT;
|
||||||
|
Head : INT; (* индекс чтения *)
|
||||||
|
Tail : INT; (* индекс записи *)
|
||||||
|
Count : INT; (* количество элементов *)
|
||||||
|
END_VAR
|
||||||
|
VAR CONSTANT
|
||||||
|
n : INT := 31; (* 32 элемента *)
|
||||||
|
END_VAR
|
||||||
|
(*----- IMPLEMENTATION -----*)
|
||||||
|
IF RST THEN
|
||||||
|
Head := 0;
|
||||||
|
Tail := 0;
|
||||||
|
Count := 0;
|
||||||
|
EMPTY := TRUE;
|
||||||
|
FULL := FALSE;
|
||||||
|
Dout := 0;
|
||||||
|
ELSIF E THEN
|
||||||
|
(* Извлечение *)
|
||||||
|
IF POP AND NOT EMPTY THEN
|
||||||
|
Dout := queue[Head];
|
||||||
|
Head := Head + 1;
|
||||||
|
IF Head > n THEN
|
||||||
|
Head := 0;
|
||||||
|
END_IF;
|
||||||
|
Count := Count - 1;
|
||||||
|
END_IF;
|
||||||
|
(* Добавление *)
|
||||||
|
IF PUSH AND NOT FULL THEN
|
||||||
|
queue[Tail] := Din;
|
||||||
|
Tail := Tail + 1;
|
||||||
|
IF Tail > n THEN
|
||||||
|
Tail := 0;
|
||||||
|
END_IF;
|
||||||
|
Count := Count + 1;
|
||||||
|
END_IF;
|
||||||
|
EMPTY := (Count = 0);
|
||||||
|
FULL := (Count = n + 1);
|
||||||
|
END_IF;
|
||||||
|
END_FUNCTION_BLOCK
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
FUNCTION_BLOCK STACK_I16
|
FUNCTION_BLOCK STACK_I16
|
||||||
(*
|
(*
|
||||||
Стек на 16 элементов типа INT
|
Стек LIFO на 16 элементов типа INT
|
||||||
*)
|
*)
|
||||||
VAR_INPUT
|
VAR_INPUT
|
||||||
Din : INT; (* Переменная для записи в стек *)
|
Din : INT; (* Переменная для записи в стек *)
|
||||||
@@ -29,12 +29,11 @@ VAR
|
|||||||
pt : INT;
|
pt : INT;
|
||||||
END_VAR
|
END_VAR
|
||||||
VAR CONSTANT
|
VAR CONSTANT
|
||||||
n : INT := 15; (* changing this value will chage the number of stored elements in the fifo *)
|
n : INT := 15; (* 16 элементов *)
|
||||||
END_VAR
|
END_VAR
|
||||||
|
|
||||||
(*----- IMPLEMENTATION -----*)
|
(*----- IMPLEMENTATION -----*)
|
||||||
IF RST THEN
|
IF RST THEN
|
||||||
(* asynchronous reset for the fifo *)
|
|
||||||
pt := 0;
|
pt := 0;
|
||||||
EMPTY := TRUE;
|
EMPTY := TRUE;
|
||||||
FULL := FALSE;
|
FULL := FALSE;
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
(*
|
||||||
|
===============================================================================
|
||||||
|
Project : PLC_Library
|
||||||
|
Device : -
|
||||||
|
Path : Общие
|
||||||
|
Name : STACK_I32
|
||||||
|
Type : FUNCTION_BLOCK
|
||||||
|
===============================================================================
|
||||||
|
*)
|
||||||
|
|
||||||
|
FUNCTION_BLOCK STACK_I32
|
||||||
|
(*
|
||||||
|
Стек LIFO на 32 элемента типа INT
|
||||||
|
*)
|
||||||
|
VAR_INPUT
|
||||||
|
Din : INT; (* Переменная для записи в стек *)
|
||||||
|
E : BOOL := TRUE; (* Включение стека *)
|
||||||
|
POP : BOOL; (* Извлечение из стека *)
|
||||||
|
PUSH : BOOL; (* Запись в стек *)
|
||||||
|
RST : BOOL; (* Обнуление стека *)
|
||||||
|
END_VAR
|
||||||
|
VAR_OUTPUT
|
||||||
|
Dout : INT; (* Значение, извлеченное из стека при POP *)
|
||||||
|
EMPTY : BOOL := TRUE; (* Флаг "стек пуст" *)
|
||||||
|
FULL : BOOL; (* Флаг "стек заполнен" (достигнут предел n) *)
|
||||||
|
END_VAR
|
||||||
|
VAR
|
||||||
|
stack: ARRAY[0..n] OF INT;
|
||||||
|
pt : INT;
|
||||||
|
END_VAR
|
||||||
|
VAR CONSTANT
|
||||||
|
n : INT := 31; (* 32 элемента *)
|
||||||
|
END_VAR
|
||||||
|
|
||||||
|
(*----- IMPLEMENTATION -----*)
|
||||||
|
IF RST THEN
|
||||||
|
pt := 0;
|
||||||
|
EMPTY := TRUE;
|
||||||
|
FULL := FALSE;
|
||||||
|
Dout := 0;
|
||||||
|
ELSIF E THEN
|
||||||
|
IF NOT EMPTY AND POP THEN
|
||||||
|
pt := pt - 1;
|
||||||
|
Dout := stack[pt];
|
||||||
|
EMPTY := pt = 0;
|
||||||
|
FULL := FALSE;
|
||||||
|
END_IF;
|
||||||
|
IF NOT FULL AND PUSH THEN
|
||||||
|
stack[pt] := Din;
|
||||||
|
pt := pt + 1;
|
||||||
|
FULL := pt > n;
|
||||||
|
EMPTY := FALSE;
|
||||||
|
END_IF;
|
||||||
|
END_IF;
|
||||||
|
END_FUNCTION_BLOCK
|
||||||
@@ -0,0 +1,464 @@
|
|||||||
|
# encoding:utf-8
|
||||||
|
# import_plc_src.py
|
||||||
|
# Обратный скрипт к export_plc_src.py
|
||||||
|
# Читает .st файлы из plc_src/ и создаёт/обновляет соответствующие
|
||||||
|
# POU / METHOD / PROPERTY / ACTION / GVL / DUT в открытом проекте CODESYS.
|
||||||
|
#
|
||||||
|
# ВАЖНО про ACTION / END_ACTION / END_PROGRAM и т.п.:
|
||||||
|
# CODESYS сам оборачивает textual_declaration / textual_implementation в
|
||||||
|
# неявный блок в зависимости от типа объекта. Заголовок "ACTION Имя :" и
|
||||||
|
# закрывающие END_* НЕ нужно (и часто нельзя) писать в текст через API --
|
||||||
|
# они генерируются автоматически самим объектом. Поэтому при импорте эти
|
||||||
|
# строки сначала вырезаются (см. strip_end_keyword / parse_block_text),
|
||||||
|
# и в API передаётся только "голое" тело.
|
||||||
|
#
|
||||||
|
# Запуск: аналогично export_plc_src.py --
|
||||||
|
# Tools -> Scripting -> Execute Script File, либо --runscript=...
|
||||||
|
#
|
||||||
|
# ПЕРЕД РЕАЛЬНЫМ ИМПОРТОМ: скрипт по умолчанию работает в режиме DRY_RUN
|
||||||
|
# (см. ниже) -- он только печатает, что бы сделал, ничего не создавая и
|
||||||
|
# не перезаписывая. Проверь вывод, затем поставь DRY_RUN = False.
|
||||||
|
#
|
||||||
|
# ПРЕДУПРЕЖДЕНИЕ: сигнатуры create_pou / create_method / create_property /
|
||||||
|
# create_action / create_global_var_list / create_dut могут отличаться
|
||||||
|
# между версиями CODESYS. Значения ниже рассчитаны на типовой scripting
|
||||||
|
# API (аналог CODESYS V3.5 SP17). Если создание объекта падает с ошибкой --
|
||||||
|
# смотри Tools -> Scripting -> справку по API твоей версии и поправь
|
||||||
|
# соответствующую get_or_create_* функцию.
|
||||||
|
|
||||||
|
from __future__ import print_function
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import codecs
|
||||||
|
|
||||||
|
# Должно совпадать с EXCLUDED_PATH_NAMES в export_plc_src.py --
|
||||||
|
# именно эти узлы дерева были "прозрачно" пропущены при формировании Path.
|
||||||
|
EXCLUDED_PATH_NAMES = set(["application", "plc logic"])
|
||||||
|
|
||||||
|
END_KEYWORDS = {
|
||||||
|
"PROGRAM": "END_PROGRAM",
|
||||||
|
"FUNCTION_BLOCK": "END_FUNCTION_BLOCK",
|
||||||
|
"FUNCTION": "END_FUNCTION",
|
||||||
|
"INTERFACE": "END_INTERFACE",
|
||||||
|
"METHOD": "END_METHOD",
|
||||||
|
"PROPERTY": "END_PROPERTY",
|
||||||
|
"ACTION": "END_ACTION",
|
||||||
|
}
|
||||||
|
|
||||||
|
IMPL_MARKER = "(*----- IMPLEMENTATION -----*)"
|
||||||
|
|
||||||
|
HEADER_RE = re.compile(
|
||||||
|
r"^\(\*\n"
|
||||||
|
r"=+\n"
|
||||||
|
r" Project\s*: (?P<project>[^\n]*)\n"
|
||||||
|
r" Device\s*: (?P<device>[^\n]*)\n"
|
||||||
|
r" Path\s*: (?P<path>[^\n]*)\n"
|
||||||
|
r" Name\s*: (?P<name>[^\n]*)\n"
|
||||||
|
r" Type\s*: (?P<type>[^\n]*)\n"
|
||||||
|
r"=+\n"
|
||||||
|
r"\*\)\n",
|
||||||
|
re.MULTILINE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_newlines(text):
|
||||||
|
"""CODESYS/IronPython на Windows при записи через codecs.open иногда
|
||||||
|
транслирует каждый \\n в \\r\\n даже там, где \\r уже стоит вручную --
|
||||||
|
получается \\r\\r\\n вместо \\r\\n. Лишние оказываются именно \\r, а не
|
||||||
|
\\n, поэтому надёжная нормализация -- просто выбросить все \\r: структура
|
||||||
|
строк (в т.ч. пустые строки) при этом сохраняется, независимо от того,
|
||||||
|
что реально лежит на диске (\\r\\n, \\r\\r\\n, голый \\n, смесь)."""
|
||||||
|
return text.replace("\r", "")
|
||||||
|
|
||||||
|
POU_TYPES = ("PROGRAM", "FUNCTION_BLOCK", "FUNCTION", "INTERFACE")
|
||||||
|
|
||||||
|
# ==== НАСТРОЙКА ====
|
||||||
|
DRY_RUN = False # поставь False, когда убедишься, что вывод корректный
|
||||||
|
MANUAL_INPUT_ROOT = r"" # пусто -- искать plc_src рядом с .project
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- вспомогательные функции (частично повторяют export_plc_src.py) ----------
|
||||||
|
|
||||||
|
def get_project_file_path(proj):
|
||||||
|
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
|
||||||
|
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 is_device_node(obj):
|
||||||
|
try:
|
||||||
|
return bool(getattr(obj, "is_device", False))
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def find_child(node, name):
|
||||||
|
for c in node.get_children(False):
|
||||||
|
if safe_name(c) == name:
|
||||||
|
return c
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def find_device(proj, device_name):
|
||||||
|
for c in proj.get_children(False):
|
||||||
|
if is_device_node(c) and safe_name(c) == device_name:
|
||||||
|
return c
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def find_entry_container(root):
|
||||||
|
"""Спускается через служебные узлы (Application / PLC Logic и т.п.) --
|
||||||
|
те же имена, что фильтровались при построении Path на экспорте."""
|
||||||
|
node = root
|
||||||
|
while True:
|
||||||
|
matched = None
|
||||||
|
for c in node.get_children(False):
|
||||||
|
if safe_name(c).strip().lower() in EXCLUDED_PATH_NAMES:
|
||||||
|
matched = c
|
||||||
|
break
|
||||||
|
if matched is None:
|
||||||
|
return node
|
||||||
|
node = matched
|
||||||
|
|
||||||
|
|
||||||
|
def get_or_create_folder(parent, name):
|
||||||
|
child = find_child(parent, name)
|
||||||
|
if child is not None:
|
||||||
|
return child
|
||||||
|
try:
|
||||||
|
parent.create_folder(name, "")
|
||||||
|
except TypeError:
|
||||||
|
parent.create_folder(name)
|
||||||
|
# create_folder может не возвращать созданный объект (возвращает None) --
|
||||||
|
# поэтому вместо доверия return-значению ищем папку заново по имени.
|
||||||
|
child = find_child(parent, name)
|
||||||
|
if child is None:
|
||||||
|
raise RuntimeError("create_folder('%s') не создал папку и не найден через find_child" % name)
|
||||||
|
return child
|
||||||
|
|
||||||
|
|
||||||
|
def navigate_path(container, path_parts):
|
||||||
|
node = container
|
||||||
|
for part in path_parts:
|
||||||
|
node = get_or_create_folder(node, part)
|
||||||
|
return node
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- разбор .st файла на блоки ----------
|
||||||
|
|
||||||
|
def strip_end_keyword(text, type_label):
|
||||||
|
"""Убирает END_* в конце текста -- CODESYS добавляет его сам, в API
|
||||||
|
он не передаётся. Работает с уже нормализованным (\\n) текстом."""
|
||||||
|
kw = END_KEYWORDS.get(type_label)
|
||||||
|
if not kw:
|
||||||
|
return text
|
||||||
|
pattern = re.compile(r"\n\s*" + re.escape(kw) + r"\s*\n?\s*$")
|
||||||
|
new_text, n = pattern.subn("", text, count=1)
|
||||||
|
return new_text if n else text
|
||||||
|
|
||||||
|
|
||||||
|
def split_blocks(raw_text):
|
||||||
|
"""raw_text уже нормализован (только \\n). Один .st файл может содержать
|
||||||
|
несколько блоков: основной POU и дописанные METHOD/PROPERTY/ACTION.
|
||||||
|
Разбиваем по заголовкам-паспортам."""
|
||||||
|
matches = list(HEADER_RE.finditer(raw_text))
|
||||||
|
blocks = []
|
||||||
|
for i, m in enumerate(matches):
|
||||||
|
start = m.end()
|
||||||
|
end = matches[i + 1].start() if i + 1 < len(matches) else len(raw_text)
|
||||||
|
body = raw_text[start:end]
|
||||||
|
blocks.append((m.groupdict(), body))
|
||||||
|
return blocks
|
||||||
|
|
||||||
|
|
||||||
|
def to_crlf(text):
|
||||||
|
"""CODESYS ожидает обычный текст -- перед передачей в API возвращаем
|
||||||
|
стандартный \\r\\n (на входе текст уже нормализован в \\n)."""
|
||||||
|
if not text:
|
||||||
|
return text
|
||||||
|
return text.replace("\n", "\r\n")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_block_text(body, type_label):
|
||||||
|
"""Возвращает (declaration_text_or_None, implementation_text_or_None)
|
||||||
|
-- зеркало build_pou_text / build_action_text из export_plc_src.py.
|
||||||
|
body уже нормализован (только \\n)."""
|
||||||
|
body = body.strip("\n")
|
||||||
|
if not body:
|
||||||
|
return None, None
|
||||||
|
body = strip_end_keyword(body, type_label)
|
||||||
|
|
||||||
|
if type_label == "ACTION":
|
||||||
|
# первая строка "ACTION Имя :" синтезирована на экспорте вручную --
|
||||||
|
# убираем её, для API нужна только implementation
|
||||||
|
parts = body.split("\n", 1)
|
||||||
|
impl = parts[1].strip("\n") if len(parts) > 1 else ""
|
||||||
|
return None, (to_crlf(impl) + "\r\n" if impl else "")
|
||||||
|
|
||||||
|
if IMPL_MARKER in body:
|
||||||
|
decl, impl = body.split(IMPL_MARKER, 1)
|
||||||
|
decl = decl.strip("\n")
|
||||||
|
impl = impl.strip("\n")
|
||||||
|
return (to_crlf(decl) + "\r\n" if decl else ""), (to_crlf(impl) + "\r\n" if impl else "")
|
||||||
|
|
||||||
|
# GVL / DUT -- только декларация, END_* для них не определён (и не нужен)
|
||||||
|
decl = body.strip("\n")
|
||||||
|
return (to_crlf(decl) + "\r\n" if decl else ""), None
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- запись в объекты проекта ----------
|
||||||
|
|
||||||
|
def set_declaration(obj, text):
|
||||||
|
if not text:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
obj.textual_declaration.replace(text)
|
||||||
|
except Exception as e:
|
||||||
|
print(" ! ошибка записи declaration: %s" % e)
|
||||||
|
|
||||||
|
|
||||||
|
def set_implementation(obj, text):
|
||||||
|
if not text:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
obj.textual_implementation.replace(text)
|
||||||
|
except Exception as e:
|
||||||
|
print(" ! ошибка записи implementation: %s" % e)
|
||||||
|
|
||||||
|
|
||||||
|
def get_or_create_pou(parent, name, type_label):
|
||||||
|
existing = find_child(parent, name)
|
||||||
|
if existing is not None:
|
||||||
|
return existing, False
|
||||||
|
try:
|
||||||
|
if type_label == "INTERFACE":
|
||||||
|
obj = parent.create_interface(name)
|
||||||
|
else:
|
||||||
|
pou_kind = {
|
||||||
|
"PROGRAM": "Program",
|
||||||
|
"FUNCTION_BLOCK": "FunctionBlock",
|
||||||
|
"FUNCTION": "Function",
|
||||||
|
}[type_label]
|
||||||
|
try:
|
||||||
|
pou_type_enum = getattr(PouType, pou_kind) # noqa: F821 (глобал CODESYS)
|
||||||
|
except Exception:
|
||||||
|
pou_type_enum = pou_kind
|
||||||
|
obj = parent.create_pou(name, pou_type_enum, ImplementationLanguages.st) # noqa: F821
|
||||||
|
return obj, True
|
||||||
|
except Exception as e:
|
||||||
|
print(" ! не удалось создать %s '%s': %s" % (type_label, name, e))
|
||||||
|
return None, False
|
||||||
|
|
||||||
|
|
||||||
|
def get_or_create_method(parent_pou, name):
|
||||||
|
existing = find_child(parent_pou, name)
|
||||||
|
if existing is not None:
|
||||||
|
return existing, False
|
||||||
|
try:
|
||||||
|
obj = parent_pou.create_method(name, "", "", ImplementationLanguages.st) # noqa: F821
|
||||||
|
return obj, True
|
||||||
|
except Exception as e:
|
||||||
|
print(" ! не удалось создать METHOD '%s': %s" % (name, e))
|
||||||
|
return None, False
|
||||||
|
|
||||||
|
|
||||||
|
def get_or_create_property(parent_pou, name):
|
||||||
|
existing = find_child(parent_pou, name)
|
||||||
|
if existing is not None:
|
||||||
|
return existing, False
|
||||||
|
try:
|
||||||
|
obj = parent_pou.create_property(name, "", ImplementationLanguages.st) # noqa: F821
|
||||||
|
return obj, True
|
||||||
|
except Exception as e:
|
||||||
|
print(" ! не удалось создать PROPERTY '%s': %s" % (name, e))
|
||||||
|
return None, False
|
||||||
|
|
||||||
|
|
||||||
|
def get_or_create_action(parent_pou, name):
|
||||||
|
existing = find_child(parent_pou, name)
|
||||||
|
if existing is not None:
|
||||||
|
return existing, False
|
||||||
|
try:
|
||||||
|
obj = parent_pou.create_action(name, ImplementationLanguages.st) # noqa: F821
|
||||||
|
return obj, True
|
||||||
|
except Exception as e:
|
||||||
|
print(" ! не удалось создать ACTION '%s': %s" % (name, e))
|
||||||
|
return None, False
|
||||||
|
|
||||||
|
|
||||||
|
def get_or_create_gvl(parent, name):
|
||||||
|
existing = find_child(parent, name)
|
||||||
|
if existing is not None:
|
||||||
|
return existing, False
|
||||||
|
for method_name in ("create_gvl", "create_global_variable_list", "create_global_var_list"):
|
||||||
|
method = getattr(parent, method_name, None)
|
||||||
|
if method is None:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
obj = method(name)
|
||||||
|
return obj, True
|
||||||
|
except Exception as e:
|
||||||
|
print(" ! не удалось создать GVL '%s' через %s: %s" % (name, method_name, e))
|
||||||
|
print(" ! не удалось создать GVL '%s': подходящий метод API не найден" % name)
|
||||||
|
return None, False
|
||||||
|
|
||||||
|
|
||||||
|
def get_or_create_dut(parent, name):
|
||||||
|
existing = find_child(parent, name)
|
||||||
|
if existing is not None:
|
||||||
|
return existing, False
|
||||||
|
try:
|
||||||
|
obj = parent.create_dut(name, DutType.Structure) # noqa: F821 -- перезапишется текстом ниже
|
||||||
|
return obj, True
|
||||||
|
except Exception as e:
|
||||||
|
print(" ! не удалось создать DUT '%s': %s" % (name, e))
|
||||||
|
return None, False
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- обработка одного файла ----------
|
||||||
|
|
||||||
|
def process_st_file(proj, filepath, dry_run):
|
||||||
|
with codecs.open(filepath, "r", encoding="utf-8") as f:
|
||||||
|
raw = f.read()
|
||||||
|
raw = normalize_newlines(raw)
|
||||||
|
|
||||||
|
blocks = split_blocks(raw)
|
||||||
|
if not blocks:
|
||||||
|
print(" (пропущено, заголовок не найден)")
|
||||||
|
return
|
||||||
|
|
||||||
|
parent_pou = None # POU, к которому относятся идущие следом METHOD/PROPERTY/ACTION
|
||||||
|
|
||||||
|
for header, body in blocks:
|
||||||
|
device_name = header["device"].strip()
|
||||||
|
path_str = header["path"].strip()
|
||||||
|
name = header["name"].strip()
|
||||||
|
type_label = header["type"].strip()
|
||||||
|
path_parts = [] if path_str == "-" else path_str.split("/")
|
||||||
|
|
||||||
|
if device_name == "-":
|
||||||
|
root = proj
|
||||||
|
else:
|
||||||
|
root = find_device(proj, device_name)
|
||||||
|
if root is None:
|
||||||
|
print(" ! устройство '%s' не найдено, пропуск '%s'" % (device_name, name))
|
||||||
|
continue
|
||||||
|
|
||||||
|
container = find_entry_container(root)
|
||||||
|
target_parent = navigate_path(container, path_parts)
|
||||||
|
decl_text, impl_text = parse_block_text(body, type_label)
|
||||||
|
|
||||||
|
print(" %s: %s/%s" % (type_label, path_str, name))
|
||||||
|
if dry_run:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if type_label in POU_TYPES:
|
||||||
|
obj, _ = get_or_create_pou(target_parent, name, type_label)
|
||||||
|
if obj is None:
|
||||||
|
parent_pou = None
|
||||||
|
continue
|
||||||
|
set_declaration(obj, decl_text)
|
||||||
|
set_implementation(obj, impl_text)
|
||||||
|
parent_pou = obj
|
||||||
|
|
||||||
|
elif type_label in ("METHOD", "PROPERTY"):
|
||||||
|
if parent_pou is None:
|
||||||
|
print(" ! нет родительского POU в этом файле, пропуск")
|
||||||
|
continue
|
||||||
|
if type_label == "METHOD":
|
||||||
|
obj, _ = get_or_create_method(parent_pou, name)
|
||||||
|
else:
|
||||||
|
obj, _ = get_or_create_property(parent_pou, name)
|
||||||
|
if obj is None:
|
||||||
|
continue
|
||||||
|
set_declaration(obj, decl_text)
|
||||||
|
set_implementation(obj, impl_text)
|
||||||
|
|
||||||
|
elif type_label == "ACTION":
|
||||||
|
if parent_pou is None:
|
||||||
|
print(" ! нет родительского POU в этом файле, пропуск")
|
||||||
|
continue
|
||||||
|
obj, _ = get_or_create_action(parent_pou, name)
|
||||||
|
if obj is None:
|
||||||
|
continue
|
||||||
|
set_implementation(obj, impl_text)
|
||||||
|
|
||||||
|
elif type_label == "GVL":
|
||||||
|
obj, _ = get_or_create_gvl(target_parent, name)
|
||||||
|
if obj is None:
|
||||||
|
continue
|
||||||
|
set_declaration(obj, decl_text)
|
||||||
|
|
||||||
|
elif type_label == "DUT":
|
||||||
|
obj, _ = get_or_create_dut(target_parent, name)
|
||||||
|
if obj is None:
|
||||||
|
continue
|
||||||
|
set_declaration(obj, decl_text)
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(" ! неизвестный тип '%s', пропуск" % type_label)
|
||||||
|
|
||||||
|
|
||||||
|
def find_st_files(root):
|
||||||
|
result = []
|
||||||
|
for dirpath, _dirnames, filenames in os.walk(root):
|
||||||
|
for fn in filenames:
|
||||||
|
if fn.lower().endswith(".st"):
|
||||||
|
result.append(os.path.join(dirpath, fn))
|
||||||
|
return sorted(result)
|
||||||
|
|
||||||
|
|
||||||
|
# ==== ТОЧКА ВХОДА ====
|
||||||
|
|
||||||
|
proj = projects.primary # noqa: F821 -- глобал CODESYS scripting
|
||||||
|
|
||||||
|
if proj is None:
|
||||||
|
print("ОШИБКА: нет открытого проекта. Открой .project перед запуском скрипта.")
|
||||||
|
else:
|
||||||
|
project_file = get_project_file_path(proj)
|
||||||
|
|
||||||
|
if MANUAL_INPUT_ROOT:
|
||||||
|
INPUT_ROOT = MANUAL_INPUT_ROOT
|
||||||
|
print("Папка ввода задана вручную: %s" % INPUT_ROOT)
|
||||||
|
elif project_file:
|
||||||
|
INPUT_ROOT = os.path.join(os.path.dirname(project_file), "plc_src")
|
||||||
|
print("Проект найден: %s" % project_file)
|
||||||
|
print("Папка ввода определена автоматически: %s" % INPUT_ROOT)
|
||||||
|
else:
|
||||||
|
INPUT_ROOT = None
|
||||||
|
print("ОШИБКА: не удалось определить путь к .project. Задай MANUAL_INPUT_ROOT.")
|
||||||
|
|
||||||
|
if INPUT_ROOT and os.path.isdir(INPUT_ROOT):
|
||||||
|
print("--- Импорт из %s (DRY_RUN=%s) ---" % (INPUT_ROOT, DRY_RUN))
|
||||||
|
files = find_st_files(INPUT_ROOT)
|
||||||
|
print("Найдено файлов: %d" % len(files))
|
||||||
|
for fp in files:
|
||||||
|
print("Файл: %s" % fp)
|
||||||
|
process_st_file(proj, fp, DRY_RUN)
|
||||||
|
print("--- Импорт завершён ---")
|
||||||
|
if DRY_RUN:
|
||||||
|
print("Это был DRY-RUN -- ничего не записано в проект.")
|
||||||
|
print("Проверь вывод выше и поставь DRY_RUN = False для реального импорта.")
|
||||||
|
elif INPUT_ROOT:
|
||||||
|
print("ОШИБКА: папка не найдена: %s" % INPUT_ROOT)
|
||||||
Reference in New Issue
Block a user