diff --git a/.gitignore b/.gitignore index be4a876..b3e9443 100644 --- a/.gitignore +++ b/.gitignore @@ -41,7 +41,6 @@ _CompileInfo/ *.git_tmp/ # ---- Boot-приложение ---- - *.app *.crc Boot Project/ @@ -83,6 +82,21 @@ venv/ *.swp *.swo +# ---- Прочее ---- +**/old/* +tools/ +libs/ +/tmp/* +/.kilo/* +.idea/ +.vscode/ +.pytest_cache/ +.coverage/ +.coverage.* +.coverage.xml +.coverage.json +.coverage.yaml + # ============================================================ # НЕ ИГНОРИРОВАТЬ БЕЗ ПРОВЕРКИ: # *.xml diff --git a/AI/AGENTS.md b/AI/AGENTS.md new file mode 100644 index 0000000..bfa3f10 --- /dev/null +++ b/AI/AGENTS.md @@ -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 файл + +### Глоссарий сокращений + + + +- + +--- + +## Структура репозитория + +```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 нужной версии. + +--- + +## Что нельзя трогать без согласования + + + +- + +--- + +## Библиотека (внешний проект) + +Есть самописная библиотека — отдельный 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>" +``` diff --git a/AI/PLC_Library_API.md b/AI/PLC_Library_API.md new file mode 100644 index 0000000..b6479f8 --- /dev/null +++ b/AI/PLC_Library_API.md @@ -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 и т.д.). diff --git a/Скрипты/import_plc_src.py b/Скрипты/import_plc_src.py index fae1bf1..555fef5 100644 --- a/Скрипты/import_plc_src.py +++ b/Скрипты/import_plc_src.py @@ -45,18 +45,21 @@ END_KEYWORDS = { "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( - r"^\(\*\n" - r"=+\n" - r" Project\s*: (?P[^\n]*)\n" - r" Device\s*: (?P[^\n]*)\n" - r" Path\s*: (?P[^\n]*)\n" - r" Name\s*: (?P[^\n]*)\n" - r" Type\s*: (?P[^\n]*)\n" - r"=+\n" - r"\*\)\n", + r"^\(\*[ \t]*\n" + r"=+[ \t]*\n" + r"[ \t]*Project[ \t]*:[ \t]*(?P[^\n]*?)[ \t]*\n" + r"[ \t]*Device[ \t]*:[ \t]*(?P[^\n]*?)[ \t]*\n" + r"[ \t]*Path[ \t]*:[ \t]*(?P[^\n]*?)[ \t]*\n" + r"[ \t]*Name[ \t]*:[ \t]*(?P[^\n]*?)[ \t]*\n" + r"[ \t]*Type[ \t]*:[ \t]*(?P[^\n]*?)[ \t]*\n" + r"=+[ \t]*\n" + r"[ \t]*\*\)[ \t]*\n", re.MULTILINE, ) @@ -216,8 +219,9 @@ def parse_block_text(body, type_label): 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) + m = IMPL_MARKER_RE.search(body) + if m: + decl, impl = body[:m.start()], body[m.end():] 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 "") @@ -227,6 +231,41 @@ def parse_block_text(body, type_label): 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): @@ -247,63 +286,116 @@ def set_implementation(obj, text): 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) if existing is not None: + print(" = %s '%s' уже существует" % (type_label, name)) 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] + if type_label == "PROGRAM": + create_program = getattr(parent, "create_program", None) + if create_program is not None: + return create_program(name, ImplementationLanguages.st), True + return parent.create_pou(name, PouType.Program, ImplementationLanguages.st), True + + if type_label == "FUNCTION_BLOCK": + 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: - 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 + return parent.create_pou(name, PouType.Function, return_type, ImplementationLanguages.st), True + except TypeError: + return parent.create_pou(name, PouType.Function, ImplementationLanguages.st, return_type), True + + if type_label == "INTERFACE": + return parent.create_interface(name), True + + raise RuntimeError("неизвестный тип POU '%s'" % type_label) + except Exception as e: - print(" ! не удалось создать %s '%s': %s" % (type_label, name, e)) + print(" ! НЕ УДАЛОСЬ создать %s '%s': %s" % (type_label, name, e)) 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) if existing is not None: + print(" = METHOD '%s' уже существует" % name) return existing, False + + return_type = extract_return_type(declaration_text, "METHOD", name) + try: - obj = parent_pou.create_method(name, "", "", ImplementationLanguages.st) # noqa: F821 - return obj, True + return parent_pou.create_method( + 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: - print(" ! не удалось создать METHOD '%s': %s" % (name, e)) + print(" ! НЕ УДАЛОСЬ создать METHOD '%s': %s" % (name, e)) 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) if existing is not None: + print(" = PROPERTY '%s' уже существует" % name) 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: - obj = parent_pou.create_property(name, "", ImplementationLanguages.st) # noqa: F821 - return obj, True + return parent_pou.create_property( + 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: - print(" ! не удалось создать PROPERTY '%s': %s" % (name, 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: + print(" = ACTION '%s' уже существует" % name) return existing, False try: - obj = parent_pou.create_action(name, ImplementationLanguages.st) # noqa: F821 - return obj, True + return parent_pou.create_action(name, ImplementationLanguages.st), 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: - print(" ! не удалось создать ACTION '%s': %s" % (name, e)) + print(" ! НЕ УДАЛОСЬ создать ACTION '%s': %s" % (name, e)) return None, False @@ -374,7 +466,7 @@ def process_st_file(proj, filepath, dry_run): continue 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: parent_pou = None continue @@ -387,9 +479,9 @@ def process_st_file(proj, filepath, dry_run): print(" ! нет родительского POU в этом файле, пропуск") continue if type_label == "METHOD": - obj, _ = get_or_create_method(parent_pou, name) + obj, _ = get_or_create_method(parent_pou, name, decl_text) else: - obj, _ = get_or_create_property(parent_pou, name) + obj, _ = get_or_create_property(parent_pou, name, decl_text) if obj is None: continue set_declaration(obj, decl_text)