Обновлены вспомогательные файлы и скрипты

This commit is contained in:
2026-08-28 15:20:57 +03:00
parent 297dc62f48
commit edba921ffa
4 changed files with 980 additions and 42 deletions
+15 -1
View File
@@ -41,7 +41,6 @@ _CompileInfo/
*.git_tmp/ *.git_tmp/
# ---- Boot-приложение ---- # ---- Boot-приложение ----
*.app *.app
*.crc *.crc
Boot Project/ Boot Project/
@@ -83,6 +82,21 @@ venv/
*.swp *.swp
*.swo *.swo
# ---- Прочее ----
**/old/*
tools/
libs/
/tmp/*
/.kilo/*
.idea/
.vscode/
.pytest_cache/
.coverage/
.coverage.*
.coverage.xml
.coverage.json
.coverage.yaml
# ============================================================ # ============================================================
# НЕ ИГНОРИРОВАТЬ БЕЗ ПРОВЕРКИ: # НЕ ИГНОРИРОВАТЬ БЕЗ ПРОВЕРКИ:
# *.xml # *.xml
+164
View File
@@ -0,0 +1,164 @@
# Проект разработки для ПЛК210 (ОВЕН)
## Описание проекта
- **ПЛК:** ОВЕН ПЛК210
- **Среда разработки:** CODESYS 3.5 SP17 Patch 3
- **Основной язык программирования:** ST (Structured Text / МЭК 61131-3)
## Правила работы ИИ-ассистента с этим репозиторием (Strict Rules)
1. **Бинарник — источник истины:** Файлы `.st` в `plc_src/` — это лишь зеркало. Никакие правки в `.st` не считаются примененными, пока пользователь не прогонит скрипт импорта в CODESYS.
2. **Паспорт файла неизменен:** Секция `(* ... *)` в начале `.st` файлов содержит служебные метаданные для Python-скриптов. **Категорически запрещено** удалять или изменять структуры этих заголовков.
3. **Запрет гадания API:** Использовать ТОЛЬКО те функциональные блоки, функции и типы из внешних библиотек, которые явно описаны в `libs/<ИмяБиблиотеки>_API.md`. Если блока нет — запросить сигнатуру у пользователя.
4. **Безопасность циклов:** Никогда не использовать неограниченные циклы `WHILE` или `REPEAT`. Вся циклическая логика должна опираться на естественный цикл задачи CODESYS (Main Task), иначе сработает **Watchdog** ПЛК.
5. **Атомарность:** При модификации файла вносить изменения минимально необходимыми диффами. Не переписывать весь блок без необходимости.
6. **Новые объекты — только с подтверждением:** Создание нового POU/GVL/DUT (объекта, которого раньше не было в проекте) — необратимое изменение структуры проекта при реальном импорте. Не создавай новый файл с новым объектом молча в рамках другой задачи — явно предупреди пользователя и дождись подтверждения.
7. **Не гадать по существующим символам проекта:** Если нужно сослаться на переменную/POU/тип, которые предположительно уже есть в проекте (не создаются заново) — проверь их в `plc_src/` (grep/поиск по имени), а не полагайся на память или правдоподобное совпадение.
---
## Архитектура проекта и структуры данных
### Иерархия вызова программ (Main Task)
- `PLC_PRG`: Главный цикл управления. Содержит только вызов подпрограмм (POU).
### Сеть и протоколы
- **Modbus TCP Master и Slave:** Порты `Ethernet1 / Ethernet2`
- **Modbus RTU:** Интерфейсы `RS-485-1 / RS-485-2`
- Modbus не могут быть экспортированы в .st файл
### Глоссарий сокращений
<!-- Расшифровка предметных сокращений в именах POU/переменных, которые не покрываются Hungarian-нотацией типов ниже. -->
-
---
## Структура репозитория
```text
.
├── <Проект>.project # бинарный проект CODESYS — источник истины для компиляции
├── plc_src/ # текстовый экспорт POU/GVL/DUT — ИМЕННО ЭТИ ФАЙЛЫ читает и правит ИИ-ассистент
│ ├── <Устройство>/
│ │ ├── POU/
│ │ ├── GVL/
│ │ └── DUT/
│ └── _common/
├── tools/
│ ├── export_plc_src.py # скрипт экспорта .project -> plc_src (текст)
│ └── import_plc_src.py # скрипт импорта plc_src (текст) -> .project
├── libs/
│ └── <ИмяБиблиотеки>_API.md # справочник по FB/FUNCTION самописной библиотеки (файл может отсутствовать)
└── Algoritm.md # Основной алгоритм работы (файл может отсутствовать)
```
`plc_src/` генерируется скриптом `tools/export_plc_src.py` из `.project` через CODESYS Scripting API.
Это НЕ исходный код в привычном смысле — это зеркало текущего состояния `.project`, нужное для того, чтобы у git был человекочитаемый дифф, а у ИИ-ассистента — текстовые файлы для правки.
---
## Рабочий цикл правки логики (важно!)
Перенос правок туда-обратно автоматизирован через пару скриптов, но применяется он **не мгновенно** — между правкой `.st`-файла и тем, что окажется в проекте, есть шаг, который делает пользователь руками в CODESYS. Порядок действий:
1. ИИ-ассистент правит `.st`-файл(ы) в `plc_src/...`. Заголовок-паспорт файла (блок `(* ... *)` в начале, с полями Project/Device/Path/Name/Type) **не трогать и не удалять** — по нему `import_plc_src.py` определяет, в какой POU/METHOD/GVL/DUT проекта записать текст.
2. Пользователь открывает проект в CODESYS и запускает `tools/import_plc_src.py`.
3. Компилирует и тестирует проект в CODESYS как обычно.
4. **Перед коммитом перезапускает `tools/export_plc_src.py`**, чтобы `plc_src/` снова точно соответствовал `.project` — иначе дифф в git будет показывать код, которого уже нет в бинарнике (или наоборот — не будет показывать то, что уже есть).
5. Коммитит `.project` и `plc_src/` вместе.
ИИ-ассистент: никогда не считай правку `.st`-файла уже применённой к `.project` — это только предложение изменения. Реальный эффект появится только после того, как пользователь прогонит `import_plc_src.py` (сам, руками) и подтвердит результат.
**ИИ-ассистент никогда не запускает `import_plc_src.py` (и команду CODESYS.exe, вызывающую его) самостоятельно — это исключительно ручной шаг пользователя.**
---
## Особенности формата .st файлов (важно при правке)
- Один `.st`-файл может содержать несколько блоков подряд: основной POU и дописанные METHOD/PROPERTY/ACTION (каждый со своим заголовком-паспортом). Если добавляешь новый METHOD/ACTION в существующий POU — дописывай его в конец того же файла со своим заголовком, а не создавай отдельный файл.
- Для ACTION в заголовке не бывает декларации, только implementation — так и должно быть.
- `END_PROGRAM` / `END_FUNCTION_BLOCK` / `END_METHOD` и т.п. в конце блока — оставляй, их пишет `export_plc_src.py`, а `import_plc_src.py` сам их убирает перед отправкой в API. Не убирай их сам и не убирай случайно при правке.
- Новый POU/GVL/DUT, которого раньше не было — можно создавать новым файлом с правильным заголовком (см. правило 6 выше про обязательное подтверждение); `import_plc_src.py` создаст объект в проекте автоматически (`get_or_create_*`). Но: сигнатуры создания через API в скрипте помечены как версионно-хрупкие — если создание упадёт с ошибкой, это ожидаемо, надо смотреть Tools → Scripting → API нужной версии.
---
## Что нельзя трогать без согласования
<!-- Конкретные POU/цепи/переменные, критичные для безопасности — правки в них только после явного обсуждения, а не как побочный эффект другой задачи. -->
-
---
## Библиотека (внешний проект)
Есть самописная библиотека — отдельный CODESYS-проект, её исходники в этот репозиторий **не включаются**.
- Справочник по публичному API лежит в `libs/<ИмяБиблиотеки>_API.md`
- Там для каждого FB/FUNCTION: сигнатура, VAR_INPUT/VAR_OUTPUT/VAR_IN_OUT с типами, краткое описание
- **ИИ-ассистент: используй только то, что описано в `libs/<ИмяБиблиотеки>_API.md`.** Не придумывай сигнатуры блоков библиотеки, которых нет в справочнике — если нужного блока/параметра там нет, спроси, а не предполагай
---
## Соглашения по коду
### Именование элементов (Hungarian Notation / Prefix System)
**Физические входы/выходы**, привязанные к модулям, — в `VAR_GLOBAL` RAW_IO:
- `RDI_` — Raw Digital Input
- `RDO_` — Raw Digital Output
- `RAI_` — Raw Analog Input
- `RAO_` — Raw Analog Output
**Входы/выходы после присвоения и нормировки** — в `VAR_GLOBAL` IO:
- `DI_` — Digital Input
- `DO_` — Digital Output
- `AI_` — Analog Input
- `AO_` — Analog Output
**Типы POU:**
- `FB_` — Функциональный блок
- `FC_` — Функция
- `DUT_` — Пользовательский тип данных (Struct, Enum)
### Правила форматирования и стиля
1. **Язык:** код, имена переменных и блоков — на **английском языке**; комментарии и документация — на **русском**.
2. **Автоматы состояний (State Machines):** всегда использовать `CASE ... OF`, не заменять его цепочкой `IF ... ELSIF`, если по сути нужен автомат.
3. **Безопасность:**
- деление на ноль и выход за границы массива должны быть явно заблокированы проверками *до* операции.
4. **Таймеры:** использовать только стандартные `TON`, `TOF`, `TP` из библиотеки `Standard`.
5. **Циклы:** без неограниченных `WHILE`/`REPEAT` — см. правило 4 в разделе Strict Rules выше (иначе сработает Watchdog).
---
## Особенности оборудования ПЛК210
- **Retain-память:** Энергонезависимые переменные объявлять строго в секции `VAR RETAIN` или `VAR PERSISTENT`. Использовать экономно.
- **Встроенные входы/выходы:** Использовать таргетные переменные из дерева `ПЛК210 -> Входы/Выходы`. Сырые переменные в RAW_IO, именно их привязку делать через `IO_Mapping`.
- **Флеш-память:** Избегать частой циклической записи файлов во внутреннюю память ПЛК (использовать буферизацию в RAM).
---
## Полезные команды
Запуск экспорта из командной строки (без открытия GUI CODESYS):
```powershell
"C:\Program Files (x86)\CODESYS 3.5.17.30\CODESYS\Common\CODESYS.exe" ^
--Profile="CODESYS V3.5 SP17 Patch 3" --runscript="tools/export_plc_src.py" ^
--project="<путь к .project>"
```
Запуск импорта из командной строки (без открытия GUI CODESYS):
```powershell
"C:\Program Files (x86)\CODESYS 3.5.17.30\CODESYS\Common\CODESYS.exe" ^
--Profile="CODESYS V3.5 SP17 Patch 3" --runscript="tools/import_plc_src.py" ^
--project="<путь к .project>"
```
+668
View File
@@ -0,0 +1,668 @@
# PLC_Library — API Reference (для использования LLM/ИИ)
> Формат: справочник интерфейсов (аналог API-документации). Для каждого POU указаны: тип, путь в дереве проекта, входы/выходы с типами, значениями по умолчанию и назначением, а также краткое описание поведения и особые примечания. Тела реализации (код) намеренно не включены — только то, что нужно, чтобы **вызвать** блок правильно.
>
> Язык: CoDeSys 3.5, Structured Text (ST).
> Скармливайте этот файл ИИ вместо исходников — этого достаточно, чтобы сгенерировать корректные вызовы блоков библиотеки.
---
## Индекс
- [Filters — Фильтры](#filters--фильтры)
- [Fastwel — Модули ввода/вывода](#fastwel--модули-вводавывода)
- [Преобразование значений](#преобразование-значений)
- [Общие — Common](#общие--common)
- [Дискретные сигналы / кнопки / таймеры](#дискретные-сигналы--кнопки--таймеры)
- [Пороговые сигналы / гистерезис](#пороговые-сигналы--гистерезис)
- [Статус аналоговых сигналов](#статус-аналоговых-сигналов)
- [Диагностика связи](#диагностика-связи)
- [Регулирование](#регулирование)
- [Структуры данных (очереди/стеки)](#структуры-данных-очередистеки)
- [Иное](#иное)
- [Structs](#structs)
- [Общие правила / соглашения библиотеки](#общие-правила--соглашения-библиотеки)
---
## Filters — Фильтры
`Path: Filters`
### ExpRunAverage — FUNCTION : REAL
Экспоненциальное скользящее среднее с постоянным коэффициентом.
```
FUNCTION ExpRunAverage : REAL
VAR_INPUT
In : REAL; // входное значение
Old : REAL; // предыдущее значение EMA (нужно хранить самому вызывающему)
K : REAL; // коэффициент сглаживания [0..1], чем больше — тем быстрее реакция
END_VAR
```
Формула: `Out := (1-K)*Old + K*In`. Без внутреннего состояния — вызывающий код сам хранит `Old` между циклами.
### ExpRunAverageAdaptive — FUNCTION : REAL
Как `ExpRunAverage`, но коэффициент переключается на увеличенный `K2`, если скачок входа больше `Delta` (быстрая реакция на резкие изменения).
```
FUNCTION ExpRunAverageAdaptive : REAL
VAR_INPUT
In : REAL;
Old : REAL;
K1 : REAL; // обычный коэффициент
Delta : REAL; // порог |In-Old| для переключения на K2
K2 : REAL; // ускоренный коэффициент (обычно K2 > K1)
END_VAR
```
### RunMiddleArifm — FUNCTION_BLOCK
Скользящее среднее арифметическое по кольцевому буферу (собственное состояние, хранит буфер сам).
```
FUNCTION_BLOCK RunMiddleArifm
VAR_INPUT
In : REAL; // входное значение
N : INT := 32; // размер окна усреднения, диапазон 0..32
RST : BOOL; // TRUE => переинициализация буфера текущим In
END_VAR
VAR_OUTPUT
Y : REAL; // результат — скользящее среднее
END_VAR
```
Особенность: при `RST` или на первом скане буфер заполняется значением `In`, а `Y := In`.
### MultipleClick — FUNCTION_BLOCK
Детектор серии кликов кнопки (одиночный/двойной/тройной).
```
FUNCTION_BLOCK MultipleClick
VAR_INPUT
In : BOOL; // сигнал кнопки
TimeToWait : TIME := T#500MS; // макс. пауза между нажатиями, после — фиксация результата
TimeRelease : TIME := T#0S; // время удержания результата на выходах
END_VAR
VAR_OUTPUT
Click1 : BOOL; // TRUE — зафиксирован 1 клик
Click2 : BOOL; // TRUE — зафиксировано 2 клика
Click3 : BOOL; // TRUE — зафиксировано 3 клика
Click : WORD; // число нажатий (0, если результат уже "отпущен")
END_VAR
```
---
## Fastwel — Модули ввода/вывода
`Path: Fastwel`
Преобразование "сырых" значений АЦП/кодов модулей Fastwel в физические единицы и обратно.
### AIM721_AI — FUNCTION : REAL
```
FUNCTION AIM721_AI : REAL
VAR_INPUT
ADC : DINT; // код АЦП
END_VAR
```
АЦП → 0…20 мА (коэффициент модуля AIM721).
### AIM722_AI — FUNCTION : REAL
```
FUNCTION AIM722_AI : REAL
VAR_INPUT
ADC : DINT;
END_VAR
```
АЦП → 0…20 мА (коэффициент модуля AIM722, отличается от AIM721).
### AIM723_AI — FUNCTION : REAL
```
FUNCTION AIM723_AI : REAL
VAR_INPUT
ADC : DINT;
END_VAR
```
АЦП → 4…20 мА.
### AIM726_AI — FUNCTION : REAL
```
FUNCTION AIM726_AI : REAL
VAR_INPUT
ADC : DWORD; // код АЦП (DWORD!)
END_VAR
```
АЦП → 0…40 В.
### AIM727_AI — FUNCTION : REAL
```
FUNCTION AIM727_AI : REAL
VAR_INPUT
ADC : DINT; // код АЦП (DINT — отличие от AIM726)
END_VAR
```
АЦП → 0…40 В.
### AIM791_AI — FUNCTION : REAL
```
FUNCTION AIM791_AI : REAL
VAR_INPUT
ADC : WORD;
RANGE : BYTE := 0; // 0=0..5мА; 1=0..20мА; 2=4..20мА
END_VAR
```
Универсальный вход, диапазон выбирается `RANGE`. Значение по умолчанию `RANGE` вне 0/1/2 → возвращает `0.0`.
### AIM730_AO — FUNCTION : WORD
```
FUNCTION AIM730_AO : WORD
VAR_INPUT
DAC : REAL; // миллиамперы
RANGE : BYTE; // 0=0..20мА; 1=4..20мА
END_VAR
```
мА → код ЦАП. При `RANGE=1` и `DAC<4` принудительно возвращает код, соответствующий 4 мА.
### AIM731_AO — FUNCTION : WORD
```
FUNCTION AIM731_AO : WORD
VAR_INPUT
DAC : REAL; // вольты
RANGE : BYTE; // 0 = -10..+10В; 1 = 0..+10В
END_VAR
```
Вольты → код ЦАП модуля AIM731.
### DIM764_DI — FUNCTION : REAL
```
FUNCTION DIM764_DI : REAL
VAR_INPUT
IN : DWORD; // период/интервал от модуля, единицы АЦП
RANGE : BYTE; // 0=Period; 1=Interval
END_VAR
```
Возвращает частоту в Гц. При `IN=0` и `RANGE=0` — возвращает 0 (защита от деления на 0).
### AIM725_Status — FUNCTION_BLOCK
Формирование статуса аналогового канала с гистерезисом/задержками + диагностика по битам статуса модуля.
```
FUNCTION_BLOCK AIM725_Status
VAR_INPUT
Input : REAL; // контролируемое значение
fail_hi : REAL := 3.402823466E+38;
delta_fail_hi : REAL := 0;
time_fail_hi : TIME := T#0S;
warn_hi : REAL := 3.402823466E+38;
delta_warn_hi : REAL := 0;
time_warn_hi : TIME := T#0S;
warn_low : REAL := -3.402823466E+38;
delta_warn_low : REAL := 0;
time_warn_low : TIME := T#0S;
fail_low : REAL := -3.402823466E+38;
delta_fail_low : REAL := 0;
time_fail_low : TIME := T#0S;
time_no_signal : TIME := T#0S;
Modul_status : BYTE; // байт диагностики модуля АЦП (побитовые флаги ошибок канала)
Chanel_num : BYTE; // номер канала модуля (1 или 2) — выбирает, какие биты Modul_status проверять
END_VAR
VAR_OUTPUT
Status : AnalogValueState; // итоговый статус (см. раздел Structs)
T_mod : INT; // Input, приведённое к INT; -500 при no_signal
END_VAR
```
Приоритет статусов: `no_signal > fail_hi > fail_low > warn_hi > warn_low > normal`.
`no_signal` срабатывает если: `Input <= -200`, либо соответствующие биты `Modul_status` (зависящие от `Chanel_num`), либо биты 2/3 (общие ошибки модуля).
---
## Преобразование значений
`Path: Преобразование значений`
### DW_TO_REAL — FUNCTION : REAL
```
FUNCTION DW_TO_REAL : REAL
VAR_INPUT
X : DWORD;
END_VAR
```
**Побитовая** реинтерпретация DWORD как REAL через указатель (`ADR`) — НЕ конвертация значения. Платформозависимо.
### REAL_TO_DW — FUNCTION : DWORD
```
FUNCTION REAL_TO_DW : DWORD
VAR_INPUT
X : REAL;
END_VAR
```
Обратная операция к `DW_TO_REAL`. Побитовая реинтерпретация.
### DWORD_OF_2WORD — FUNCTION : DWORD
```
FUNCTION DWORD_OF_2WORD : DWORD
VAR_INPUT
W1 : WORD; // старшее слово, биты 16..31 результата
W0 : WORD; // младшее слово, биты 0..15 результата
END_VAR
```
### WORD_OF_DWORD — FUNCTION : WORD
```
FUNCTION WORD_OF_DWORD : WORD
VAR_INPUT
in : DWORD; // исходное значение
N : BYTE; // 0 = младшее слово (биты 0..15), 1 = старшее (биты 16..31)
END_VAR
```
### Norm_real — FUNCTION : REAL
Линейная нормировка (масштабирование по двум опорным точкам).
```
FUNCTION Norm_real : REAL
VAR_INPUT
X : REAL; // входное (ненормированное) значение
X1 : REAL; // точка 1, вход
Y1 : REAL; // точка 1, выход
X2 : REAL; // точка 2, вход
Y2 : REAL; // точка 2, выход
END_VAR
```
Если `X2=X1` — возвращает `Y1` (защита от деления на 0).
### NTC_termistor_T — FUNCTION : REAL
Перевод сопротивления NTC-термистора в температуру (уравнение Beta).
```
FUNCTION NTC_termistor_T : REAL
VAR_INPUT
Res : REAL; // измеренное сопротивление
R0 : REAL; // сопротивление при температуре T0
T0 : REAL; // опорная температура, °C
b : REAL; // коэффициент Beta термистора
END_VAR
```
Возвращает температуру в °C. При `Res<=0`, `R0<=0` или `b=0` возвращает `0.0`.
---
## Общие — Common
`Path: Общие`
### Дискретные сигналы / кнопки / таймеры
#### Button_acceleration — FUNCTION_BLOCK
Ускоренное изменение значения при удержании кнопки (для регулировки уставок и т.п.).
```
FUNCTION_BLOCK Button_acceleration
VAR_INPUT
Button : BOOL;
D_0 : REAL := 0; // приращение при одиночном (коротком) нажатии
T_W_1 : TIME := T#0S; // время до начала "короткого удержания"
T_D_1 : TIME := T#200MS; // шаг приращения при коротком удержании
D_1 : REAL := 0; // величина приращения на шаге T_D_1
T_W_2 : TIME := T#0S; // время до начала "длинного удержания"
T_D_2 : TIME := T#200MS; // шаг приращения при длинном удержании
D_2 : REAL := 0; // величина приращения на шаге T_D_2
Zeroes : BOOL := FALSE; // если TRUE — обнулять Out, когда |Out| < |D_0|
END_VAR
VAR_IN_OUT
Out : REAL; // изменяемое значение (передаётся и изменяется по ссылке!)
END_VAR
```
⚠️ Нет `VAR_OUTPUT` — результат только через `VAR_IN_OUT Out`.
#### Pulse_Switch — FUNCTION_BLOCK
```
FUNCTION_BLOCK Pulse_Switch
VAR_INPUT
Switch_ctrl : BOOL;
END_VAR
VAR_OUTPUT
Switch_on : BOOL; // импульс 500мс при фронте 0->1
Switch_off : BOOL; // импульс 500мс при фронте 1->0
END_VAR
```
#### TONOF — FUNCTION_BLOCK
Комбинированная задержка включения (TON) + выключения (TOF) для одного сигнала.
```
FUNCTION_BLOCK TONOF
VAR_INPUT
IN : BOOL;
T_ON : TIME := T#0S; // задержка появления TRUE на Q
T_OFF : TIME := T#0S; // задержка снятия TRUE с Q
END_VAR
VAR_OUTPUT
Q : BOOL;
END_VAR
```
#### TOF_RST — FUNCTION_BLOCK
Таймер выключения (TOF) с принудительным сбросом.
```
FUNCTION_BLOCK TOF_RST
VAR_INPUT
IN : BOOL;
PT : TIME; // задержка выключения
RST : BOOL; // немедленный сброс Q и таймера, приоритет над IN
END_VAR
VAR_OUTPUT
Q : BOOL;
ET : TIME; // текущее время таймера
END_VAR
```
#### TP_RST — FUNCTION_BLOCK
Одиночный импульс (TP) с принудительным сбросом.
```
FUNCTION_BLOCK TP_RST
VAR_INPUT
IN : BOOL; // фронт 0->1 запускает импульс (повторный фронт во время импульса игнорируется)
PT : TIME; // длительность импульса
RST : BOOL; // немедленно прерывает импульс
END_VAR
VAR_OUTPUT
Q : BOOL;
ET : TIME;
END_VAR
```
#### BLINK_LMP — FUNCTION_BLOCK
Управление лампой по коду состояния.
```
FUNCTION_BLOCK BLINK_LMP
VAR_INPUT
State : WORD := 0; // 0=выкл; 1=вкл; 2/3/4=мигание с частотой 1/2/3
Inp_BL_1 : BOOL := FALSE; // внешний "пульсар" (меандр) частоты 1
Inp_BL_2 : BOOL := FALSE; // частоты 2
Inp_BL_3 : BOOL := FALSE; // частоты 3
END_VAR
VAR_OUTPUT
LMP : BOOL;
END_VAR
```
#### BUZZER_CTRL — FUNCTION_BLOCK
Управление звонком/зуммером по коду состояния с отключением звука (временным и перманентным).
```
FUNCTION_BLOCK BUZZER_CTRL
VAR_INPUT
State : WORD := 0; // 0=выкл;1=вкл;2/3/4=мигание частота1/2/3
Sound_off : BOOL; // кнопка отключения звука
Inp_BL_1 : BOOL := FALSE; // пульсар частоты 1
Inp_BL_2 : BOOL := FALSE; // пульсар частоты 2
Inp_BL_3 : BOOL := FALSE; // пульсар частоты 3
PermanentOFF_Enable : BOOL := FALSE; // разрешить перманентное отключение звука
T_PermanentOFF : TIME := T#10S; // время удержания Sound_off для перманентного откл.
END_VAR
VAR_OUTPUT
BUZZER : BOOL;
END_VAR
```
#### CONFIRMATION — FUNCTION_BLOCK
Логика квитирования аварии (лампы + звонок).
```
FUNCTION_BLOCK CONFIRMATION
VAR_INPUT
Alarm : BOOL; // авария активна
ControlState_On : BOOL; // управление ведётся с данного поста
Sound_Off : BOOL; // сигнал снятия звука
Confirmation : BOOL; // сигнал квитирования
END_VAR
VAR_OUTPUT
Lamp_Alarm_C : WORD; // код для лампы аварии (0/1/2)
Lamp_Confirmation_C : WORD; // код для лампы квитирования (0/1)
Buzzer_C : WORD; // код для зуммера (0/2)
END_VAR
```
Коды ламп совместимы с входом `State`/`Inp_BL_*` блоков `BLINK_LMP`/`BUZZER_CTRL` (2 = "мигание частота 1").
### Пороговые сигналы / гистерезис
#### HYST — FUNCTION_BLOCK
Гистерезисный компаратор с раздельными порогами включения/выключения (прямая и обратная логика).
```
FUNCTION_BLOCK HYST
VAR_INPUT
In : REAL; // контролируемое значение
ON : REAL; // порог срабатывания
OFF : REAL; // порог возврата
END_VAR
VAR_OUTPUT
Q : BOOL; // состояние компаратора
win : BOOL; // TRUE — значение внутри зоны гистерезиса (неопределённая зона)
END_VAR
```
Если `ON >= OFF` — прямая логика (растущий сигнал включает); если `ON < OFF` — обратная (падающий сигнал включает).
#### ALARM_1 — FUNCTION_BLOCK
Контроль выхода `X` за диапазон `[LO_1..HI_1]` с гистерезисом.
```
FUNCTION_BLOCK ALARM_1
VAR_INPUT
X : REAL;
LO_1 : REAL;
HI_1 : REAL;
HYS : REAL; // полный гистерезис (берётся ±HYS/2 от границы)
END_VAR
VAR_OUTPUT
Q1_LO : BOOL; // X ниже LO_1
Q1_HI : BOOL; // X выше HI_1
END_VAR
```
#### ALARM_2 — FUNCTION_BLOCK
То же, для двух независимых диапазонов одновременно.
```
FUNCTION_BLOCK ALARM_2
VAR_INPUT
X : REAL;
LO_1, HI_1 : REAL;
LO_2, HI_2 : REAL;
HYS : REAL;
END_VAR
VAR_OUTPUT
Q1_LO, Q1_HI : BOOL;
Q2_LO, Q2_HI : BOOL;
END_VAR
```
#### ALARM_3 — FUNCTION_BLOCK
То же, для трёх диапазонов одновременно.
```
FUNCTION_BLOCK ALARM_3
VAR_INPUT
X : REAL;
LO_1, HI_1 : REAL;
LO_2, HI_2 : REAL;
LO_3, HI_3 : REAL;
HYS : REAL;
END_VAR
VAR_OUTPUT
Q1_LO, Q1_HI : BOOL;
Q2_LO, Q2_HI : BOOL;
Q3_LO, Q3_HI : BOOL;
END_VAR
```
### Статус аналоговых сигналов
#### STATUS_ANALOG_M — FUNCTION_BLOCK
Статус аналогового значения REAL с гистерезисом возврата и задержками срабатывания по каждому уровню; поддерживает принудительную установку статуса извне.
```
FUNCTION_BLOCK STATUS_ANALOG_M
VAR_INPUT
Input : REAL;
fail_hi : REAL := 3.402823466E+38;
delta_fail_hi : REAL := 0;
time_fail_hi : TIME := T#0S;
warn_hi : REAL := 3.402823466E+38;
delta_warn_hi : REAL := 0;
time_warn_hi : TIME := T#0S;
warn_low : REAL := -3.402823466E+38;
delta_warn_low : REAL := 0;
time_warn_low : TIME := T#0S;
fail_low : REAL := -3.402823466E+38;
delta_fail_low : REAL := 0;
time_fail_low : TIME := T#0S;
no_signal : REAL := -3.402823466E+38;
delta_no_signal : REAL := 0;
time_no_signal : TIME := T#0S;
In_fail_hi : BOOL := FALSE; // принудительно выставить статус fail_hi
In_warn_hi : BOOL := FALSE;
In_warn_low : BOOL := FALSE;
In_fail_low : BOOL := FALSE;
In_no_signal : BOOL := FALSE;
END_VAR
VAR_OUTPUT
Status : AnalogValueState;
END_VAR
```
Приоритет: `no_signal > fail_hi > fail_low > warn_hi > warn_low > normal`. Возврат из состояния — при пересечении порога `± delta_*`, срабатывание — с задержкой `time_*` (TON).
#### STATUS_ANALOG_M_I — FUNCTION_BLOCK
Полный аналог `STATUS_ANALOG_M`, но все пороги/входное значение типа **INT**, а не REAL. Сигнатура идентична по составу переменных (см. выше), только типы `Input`, `fail_hi`, `delta_fail_hi`, `warn_hi`, `delta_warn_hi`, `warn_low`, `delta_warn_low`, `fail_low`, `delta_fail_low`, `no_signal`, `delta_no_signal``INT` (диапазон по умолчанию ±32767/-32768).
### Диагностика связи
#### Status_SU_Connect — FUNCTION_BLOCK
Контроль связи по "сердцебиению" (сигнал должен периодически менять состояние).
```
FUNCTION_BLOCK Status_SU_Connect
VAR_INPUT
HeartBit : BOOL; // меняющийся сигнал от удалённого устройства
T_Pulse : TIME; // макс. допустимая длительность одного состояния HeartBit
END_VAR
VAR_OUTPUT
ERROR_CONNECT : BOOL; // TRUE — HeartBit не менялся дольше T_Pulse
END_VAR
```
#### Status_Connect_Counter — FUNCTION_BLOCK
Контроль связи по инкрементируемому счётчику от удалённого устройства.
```
FUNCTION_BLOCK Status_Connect_Counter
VAR_INPUT
Counter : WORD; // счётчик, инкрементируемый удалённым устройством
T_Error : TIME; // время без изменений счётчика для фиксации обрыва связи
END_VAR
VAR_OUTPUT
CounterIsChanged : BOOL; // TRUE в цикле, где счётчик изменился
ERROR_CONNECT : BOOL; // TRUE — счётчик не менялся дольше T_Error
END_VAR
```
### Регулирование
#### PIDcontrol — FUNCTION_BLOCK
ПИД-регулятор с фильтрацией D-составляющей, зоной нечувствительности, антивиндапом и ручным режимом.
```
FUNCTION_BLOCK PIDcontrol
VAR_INPUT
ProcessVariable : REAL := 0.0; // измеренное значение (обратная связь)
Setpoint : REAL := 0.0; // уставка
Kp : REAL := 0.001; // пропорциональный коэффициент
Ki : REAL := 0.001; // интегральный коэффициент
Kd : REAL := 0.0; // дифференциальный коэффициент
Kdf : REAL := 1.0; // коэффициент фильтра D-части (1/Tdf)
DBmax : REAL := 0.01; // верхняя граница зоны нечувствительности (по ошибке)
DBmin : REAL := -0.01; // нижняя граница зоны нечувствительности
OutMax : REAL := 100.0; // верхний предел выхода
OutMin : REAL := 0.0; // нижний предел выхода
Ts : REAL := 0.1; // период дискретизации, с
Manual : REAL := 25.0; // значение выхода в ручном режиме
ManOn : BOOL := FALSE; // TRUE — ручной режим
Reset : BOOL := FALSE; // сброс интегратора и D-части
AntiWindup : BOOL := TRUE; // включить компенсацию накопления интегратора
END_VAR
VAR_OUTPUT
Out : REAL := 0.0; // выходной сигнал регулятора
Err : REAL := 0.0; // текущая ошибка регулирования (Setpoint-ProcessVariable)
OutLimited : BOOL := FALSE; // TRUE — выход ограничен (насыщение) или некорректны параметры
END_VAR
```
Если `OutMin>=OutMax`, `DBmin>=DBmax` или `Ts<=0` — блок сразу выставляет `Out:=0`, `OutLimited:=TRUE` и выходит.
### Структуры данных (очереди/стеки)
Общая сигнатура для всех 4 блоков (`QUEUE_I16`, `QUEUE_I32`, `STACK_I16`, `STACK_I32`) — идентична, отличаются только ёмкость и дисциплина (FIFO/LIFO):
```
FUNCTION_BLOCK <Имя>
VAR_INPUT
Din : INT; // значение для записи
E : BOOL := TRUE; // включение блока (при FALSE — операции не выполняются)
POP : BOOL; // извлечь элемент
PUSH: BOOL; // добавить элемент
RST : BOOL; // сброс (очистка)
END_VAR
VAR_OUTPUT
Dout : INT; // извлечённое значение (при POP)
EMPTY : BOOL := TRUE; // TRUE — пусто
FULL : BOOL; // TRUE — заполнено (достигнут предел)
END_VAR
```
| Имя | Дисциплина | Ёмкость |
|---|---|---|
| `QUEUE_I16` | FIFO (очередь) | 16 элементов INT |
| `QUEUE_I32` | FIFO (очередь) | 32 элемента INT |
| `STACK_I16` | LIFO (стек) | 16 элементов INT |
| `STACK_I32` | LIFO (стек) | 32 элемента INT |
Примечание: при одновременном `PUSH` и `POP` в одном скане — оба выполняются (POP обрабатывается первым в теле, затем PUSH).
---
## Иное
`Path: Иное`
### DFS — PROGRAM
Обход графа в глубину (Depth-First Search) по матрице смежности фиксированного размера.
```
PROGRAM DFS
VAR_INPUT
graph : POINTER TO ARRAY[1..n, 1..n] OF BOOL; // матрица смежности, редактируется извне: DFS.graph[i,j]
connected : POINTER TO ARRAY[1..n] OF INT; // выход: список посещённых вершин (заполняется программой)
Start : INT := 1; // вершина, с которой начинается обход
END_VAR
VAR CONSTANT
n : INT := 12; // фиксированное количество вершин графа
END_VAR
```
⚠️ Это **PROGRAM**, а не FUNCTION_BLOCK — не инстанцируется через `:` в декларации переменной, а копируется целиком в проект пользователя (как указано в исходном комментарии `Копировать в свою программу`). Использует `STACK_I16` внутри. Результат — заполненный массив `connected^[1..k-1]` с номерами вершин, достижимых из `Start`.
---
## Structs
`Path: Structs`
### AnalogValueState — DUT (перечисление)
```
{attribute 'qualified_only'}
{attribute 'strict'}
TYPE AnalogValueState :
(
undefined := INT#0,
normal := INT#1,
warn_low := INT#2,
fail_low := INT#3,
warn_hi := INT#4,
fail_hi := INT#5,
no_signal := -INT#1
) := undefined;
END_TYPE
```
`qualified_only` — обращаться только как `AnalogValueState.normal` и т.п. (без квалификатора не компилируется).
Используется как тип выхода `Status` в `AIM725_Status`, `STATUS_ANALOG_M`, `STATUS_ANALOG_M_I`.
---
## Общие правила / соглашения библиотеки
1. **Приоритет статусов** во всех блоках статуса аналоговых сигналов (`AIM725_Status`, `STATUS_ANALOG_M`, `STATUS_ANALOG_M_I`) одинаков:
`no_signal > fail_hi > fail_low > warn_hi > warn_low > normal`.
2. **Гистерезис срабатывания/возврата**: срабатывание уровня — по достижению порога с задержкой `time_*` (аналог TON), возврат — при пересечении `порог ± delta_*` (без задержки).
3. Блоки `TOF_RST`/`TP_RST` — это стандартные МЭК TOF/TP с добавленным принудительным входом `RST` (сброс приоритетнее `IN`).
4. `DW_TO_REAL`/`REAL_TO_DW` — это **не** преобразование значения (не `DWORD_TO_REAL`), а побитовая реинтерпретация памяти через указатель. Использовать только когда явно нужна побитовая переупаковка (например, чтение REAL-регистра Modbus, пришедшего как два WORD → DWORD → REAL).
5. `QUEUE_I16`/`STACK_I16` — фиксированный размер 16 (`n:=15` внутри), `QUEUE_I32`/`STACK_I32` — размер 32. Чтобы изменить ёмкость — нужно менять исходник (константу `n` и объявление массива).
6. `DFS` — не библиотечный вызываемый блок, а шаблон программы под конкретный размер графа (`n:=12`); предназначен для копирования и адаптации в проекте пользователя.
7. Все входы с `VAR_IN_OUT` (только у `Button_acceleration.Out`) передаются **по ссылке** — вызывающая сторона должна сама объявить и инициализировать переменную, изменения применяются напрямую.
8. Единицы времени — везде тип `TIME` (`T#...`), единицы физических величин указаны в комментариях к каждому входу/выходу (мА, В, Гц, °C, Ом, °C и т.д.).
+133 -41
View File
@@ -45,18 +45,21 @@ END_KEYWORDS = {
"ACTION": "END_ACTION", "ACTION": "END_ACTION",
} }
IMPL_MARKER = "(*----- IMPLEMENTATION -----*)" # Терпимо к вариациям пробелов/кол-ва дефисов, которые может внести
# автоформатирование редактора, напр. "(* ----- IMPLEMENTATION ----- *)"
# вместо канонического "(*----- IMPLEMENTATION -----*)".
IMPL_MARKER_RE = re.compile(r"\(\*\s*-{3,}\s*IMPLEMENTATION\s*-{3,}\s*\*\)")
HEADER_RE = re.compile( HEADER_RE = re.compile(
r"^\(\*\n" r"^\(\*[ \t]*\n"
r"=+\n" r"=+[ \t]*\n"
r" Project\s*: (?P<project>[^\n]*)\n" r"[ \t]*Project[ \t]*:[ \t]*(?P<project>[^\n]*?)[ \t]*\n"
r" Device\s*: (?P<device>[^\n]*)\n" r"[ \t]*Device[ \t]*:[ \t]*(?P<device>[^\n]*?)[ \t]*\n"
r" Path\s*: (?P<path>[^\n]*)\n" r"[ \t]*Path[ \t]*:[ \t]*(?P<path>[^\n]*?)[ \t]*\n"
r" Name\s*: (?P<name>[^\n]*)\n" r"[ \t]*Name[ \t]*:[ \t]*(?P<name>[^\n]*?)[ \t]*\n"
r" Type\s*: (?P<type>[^\n]*)\n" r"[ \t]*Type[ \t]*:[ \t]*(?P<type>[^\n]*?)[ \t]*\n"
r"=+\n" r"=+[ \t]*\n"
r"\*\)\n", r"[ \t]*\*\)[ \t]*\n",
re.MULTILINE, re.MULTILINE,
) )
@@ -216,8 +219,9 @@ def parse_block_text(body, type_label):
impl = parts[1].strip("\n") if len(parts) > 1 else "" impl = parts[1].strip("\n") if len(parts) > 1 else ""
return None, (to_crlf(impl) + "\r\n" if impl else "") return None, (to_crlf(impl) + "\r\n" if impl else "")
if IMPL_MARKER in body: m = IMPL_MARKER_RE.search(body)
decl, impl = body.split(IMPL_MARKER, 1) if m:
decl, impl = body[:m.start()], body[m.end():]
decl = decl.strip("\n") decl = decl.strip("\n")
impl = impl.strip("\n") impl = impl.strip("\n")
return (to_crlf(decl) + "\r\n" if decl else ""), (to_crlf(impl) + "\r\n" if impl else "") return (to_crlf(decl) + "\r\n" if decl else ""), (to_crlf(impl) + "\r\n" if impl else "")
@@ -227,6 +231,41 @@ def parse_block_text(body, type_label):
return (to_crlf(decl) + "\r\n" if decl else ""), None return (to_crlf(decl) + "\r\n" if decl else ""), None
def extract_return_type(declaration, type_label, object_name):
"""Извлекает return type из FUNCTION/METHOD/PROPERTY declaration."""
if not declaration:
return None
text = normalize_newlines(declaration)
# Удаляем только ведущие прагмы для анализа первой строки.
while True:
new_text = re.sub(r"^\s*\{[^}]*\}\s*", "", text, count=1)
if new_text == text:
break
text = new_text
for line in text.split("\n"):
line = line.strip()
if not line:
continue
# Для анализа нам не нужны inline block-comments.
line = re.sub(r"\(\*.*?\*\)", "", line).strip()
m = re.match(
r"^" + re.escape(type_label) + r"\s+\S+\s*:\s*(.+?)\s*$",
line,
re.IGNORECASE
)
if m:
return m.group(1).strip()
# Если это нужный заголовок, но return type отсутствует.
if line.upper().startswith(type_label + " "):
return None
return None
# ---------- запись в объекты проекта ---------- # ---------- запись в объекты проекта ----------
def set_declaration(obj, text): def set_declaration(obj, text):
@@ -247,63 +286,116 @@ def set_implementation(obj, text):
print(" ! ошибка записи implementation: %s" % e) print(" ! ошибка записи implementation: %s" % e)
def get_or_create_pou(parent, name, type_label): def get_or_create_pou(parent, name, type_label, declaration_text):
"""Создаёт PROGRAM/FB/FUNCTION/INTERFACE через специализированный API."""
existing = find_child(parent, name) existing = find_child(parent, name)
if existing is not None: if existing is not None:
print(" = %s '%s' уже существует" % (type_label, name))
return existing, False return existing, False
try: try:
if type_label == "INTERFACE": if type_label == "PROGRAM":
obj = parent.create_interface(name) create_program = getattr(parent, "create_program", None)
else: if create_program is not None:
pou_kind = { return create_program(name, ImplementationLanguages.st), True
"PROGRAM": "Program", return parent.create_pou(name, PouType.Program, ImplementationLanguages.st), True
"FUNCTION_BLOCK": "FunctionBlock",
"FUNCTION": "Function", if type_label == "FUNCTION_BLOCK":
}[type_label] create_fb = getattr(parent, "create_function_block", None)
if create_fb is not None:
return create_fb(name, ImplementationLanguages.st), True
return parent.create_pou(name, PouType.FunctionBlock, ImplementationLanguages.st), True
if type_label == "FUNCTION":
return_type = extract_return_type(declaration_text, "FUNCTION", name)
if not return_type:
raise RuntimeError("не удалось определить return type FUNCTION '%s'" % name)
print(" -> FUNCTION '%s' : %s" % (name, return_type))
create_function = getattr(parent, "create_function", None)
if create_function is not None:
return create_function(name, return_type, ImplementationLanguages.st), True
# Fallback для старых версий API.
try: try:
pou_type_enum = getattr(PouType, pou_kind) # noqa: F821 (глобал CODESYS) return parent.create_pou(name, PouType.Function, return_type, ImplementationLanguages.st), True
except Exception: except TypeError:
pou_type_enum = pou_kind return parent.create_pou(name, PouType.Function, ImplementationLanguages.st, return_type), True
obj = parent.create_pou(name, pou_type_enum, ImplementationLanguages.st) # noqa: F821
return obj, True if type_label == "INTERFACE":
return parent.create_interface(name), True
raise RuntimeError("неизвестный тип POU '%s'" % type_label)
except Exception as e: except Exception as e:
print(" ! не удалось создать %s '%s': %s" % (type_label, name, e)) print(" ! НЕ УДАЛОСЬ создать %s '%s': %s" % (type_label, name, e))
return None, False return None, False
def get_or_create_method(parent_pou, name): def get_or_create_method(parent_pou, name, declaration_text):
existing = find_child(parent_pou, name) existing = find_child(parent_pou, name)
if existing is not None: if existing is not None:
print(" = METHOD '%s' уже существует" % name)
return existing, False return existing, False
return_type = extract_return_type(declaration_text, "METHOD", name)
try: try:
obj = parent_pou.create_method(name, "", "", ImplementationLanguages.st) # noqa: F821 return parent_pou.create_method(
return obj, True name, return_type, ImplementationLanguages.st
), True
except TypeError:
try:
return parent_pou.create_method(name, return_type), True
except Exception as e:
print(" ! НЕ УДАЛОСЬ создать METHOD '%s': %s" % (name, e))
return None, False
except Exception as e: except Exception as e:
print(" ! не удалось создать METHOD '%s': %s" % (name, e)) print(" ! НЕ УДАЛОСЬ создать METHOD '%s': %s" % (name, e))
return None, False return None, False
def get_or_create_property(parent_pou, name): def get_or_create_property(parent_pou, name, declaration_text):
existing = find_child(parent_pou, name) existing = find_child(parent_pou, name)
if existing is not None: if existing is not None:
print(" = PROPERTY '%s' уже существует" % name)
return existing, False return existing, False
return_type = extract_return_type(declaration_text, "PROPERTY", name)
if not return_type:
print(" ! НЕ УДАЛОСЬ определить return type PROPERTY '%s'" % name)
return None, False
try: try:
obj = parent_pou.create_property(name, "", ImplementationLanguages.st) # noqa: F821 return parent_pou.create_property(
return obj, True name, return_type, ImplementationLanguages.st
), True
except TypeError:
try:
return parent_pou.create_property(name, return_type), True
except Exception as e:
print(" ! НЕ УДАЛОСЬ создать PROPERTY '%s': %s" % (name, e))
return None, False
except Exception as e: except Exception as e:
print(" ! не удалось создать PROPERTY '%s': %s" % (name, e)) print(" ! НЕ УДАЛОСЬ создать PROPERTY '%s': %s" % (name, e))
return None, False return None, False
def get_or_create_action(parent_pou, name): def get_or_create_action(parent_pou, name):
existing = find_child(parent_pou, name) existing = find_child(parent_pou, name)
if existing is not None: if existing is not None:
print(" = ACTION '%s' уже существует" % name)
return existing, False return existing, False
try: try:
obj = parent_pou.create_action(name, ImplementationLanguages.st) # noqa: F821 return parent_pou.create_action(name, ImplementationLanguages.st), True
return obj, True except TypeError:
try:
return parent_pou.create_action(name), True
except Exception as e:
print(" ! НЕ УДАЛОСЬ создать ACTION '%s': %s" % (name, e))
return None, False
except Exception as e: except Exception as e:
print(" ! не удалось создать ACTION '%s': %s" % (name, e)) print(" ! НЕ УДАЛОСЬ создать ACTION '%s': %s" % (name, e))
return None, False return None, False
@@ -374,7 +466,7 @@ def process_st_file(proj, filepath, dry_run):
continue continue
if type_label in POU_TYPES: if type_label in POU_TYPES:
obj, _ = get_or_create_pou(target_parent, name, type_label) obj, _ = get_or_create_pou(target_parent, name, type_label, decl_text)
if obj is None: if obj is None:
parent_pou = None parent_pou = None
continue continue
@@ -387,9 +479,9 @@ def process_st_file(proj, filepath, dry_run):
print(" ! нет родительского POU в этом файле, пропуск") print(" ! нет родительского POU в этом файле, пропуск")
continue continue
if type_label == "METHOD": if type_label == "METHOD":
obj, _ = get_or_create_method(parent_pou, name) obj, _ = get_or_create_method(parent_pou, name, decl_text)
else: else:
obj, _ = get_or_create_property(parent_pou, name) obj, _ = get_or_create_property(parent_pou, name, decl_text)
if obj is None: if obj is None:
continue continue
set_declaration(obj, decl_text) set_declaration(obj, decl_text)