Таймер на 5 минут вместо 10 секунд: как сверка с руководством по эксплуатации нашла отказавшую защиту в чужом коде

от автора

Текст я писал с языковой моделью: она собирала черновик и вычитывала формулировки. Решения, замеры и разбор находок мои. Это моя вторая публикация здесь; в первой читатели нашли шесть расхождений между текстом и кодом, и с тех пор я цитирую цифры только из файлов и журналов, а не по памяти.

Меня зовут Дмитрий Груздев, я главный конструктор АСУТП. Занимаюсь системами управления испытательными стендами — теми, где изделие после ремонта гоняют по режимам, снимают характеристики и решают, годно ли оно к эксплуатации. Эта статья — про случай, когда пришлось восстанавливать работающую систему без единой строки исходного кода, и про то, что при этом нашлось в оригинале.

Названий предприятий, обозначений изделий и номеров документов здесь не будет — только инженерная суть: архитектура, цифры технических решений, методика сверки, найденные дефекты.

Постановка: система жива, а сопровождать её нечем

Есть действующий испытательный стенд с энергоёмкой нагрузочной установкой. Он работает: ПЛК крутит программу, оператор смотрит на HMI-панель, режимы отрабатываются. И при этом:

  • лицензия на HMI-панель утрачена: панель работает, пока работает, но перенести проект, отредактировать экран или восстановить конфигурацию после отказа железа невозможно;

  • исходников программы ПЛК нет — ни архива, ни резервной копии, ни у эксплуатанта, ни у того, кто это когда-то делал;

  • выгрузить проект из ПЛК нельзя: блоки защищены, а восстановление байт-кода до вменяемого исходника здесь экономически бессмысленно.

Что есть: бумага. Распечатка проекта среды программирования, распечатка экранов HMI, руководство по эксплуатации и сканы электрических схем — наследие приёмки, когда бумажный комплект сдавали как часть документации. Тогда формальность, теперь единственный источник истины.

Задача сформулирована жёстко: восстановить и переписать систему заново так, чтобы поведение полностью совпало с оригиналом. Не «лучше» и не «современнее», а именно совпало: стенд аттестован, методики привязаны к его поведению, любое расхождение — расхождение с методикой.

Почему нельзя было «просто написать заново»

Первая реакция инженера — «напишем с нуля, там всего-то краны, дроссели и генератор». Я её сам испытал, поэтому объясню, почему она неверна.

Стенд — не абстрактный технологический объект, а средство испытаний, чьё поведение зафиксировано в аттестационных документах и методиках. Выбирая режим, оператор ожидает конкретную циклограмму: последовательность включения ступеней, выдержки, пороги перехода. Напиши я «логично и правильно», но с другими выдержками — получится другой стенд, формально работающий, фактически не тот, на котором получены зачётные результаты.

Второй аргумент — аварии. Каталог аварийных ситуаций не декоративный список сообщений: за каждым условием стоит либо физика (перегрев, превышение тока, потеря давления), либо инцидент. Придумать набор заново — выбросить накопленный опыт эксплуатации.

Третий аргумент — монтаж: перебирать шкафы и переназначать клеммы никто не собирался, значит новая программа обязана «сесть» на существующую электрику один в один.

Отсюда решение: не писать новую систему, а восстановить эталон по документации и затем реализовать его заново на современном стеке. Два этапа, путать их нельзя: первый — археология, второй — инженерия.

Когда объект аттестован, «переписать лучше» и «переписать эквивалентно» — разные проекты с разной приёмкой. Отвечать на вопрос, какой из двух вы делаете, надо до начала, а не в середине.

Как читаются 4 900 страниц

Объём эталонного комплекта в цифрах:

Источник

Объём

Что даёт

Распечатка проекта среды программирования

~900 страниц

Все блоки, теги, сети LAD и SCL

Экспорт экранов HMI

3 907 страниц, 26 экранов

Каждая кнопка с действием и привязкой к тегам

Руководство по эксплуатации

93 страницы

Семантика режимов, таблицы, циклограммы

Электрические схемы

сканы

Физическая привязка I/O

Итого 4 900 страниц. Цифра сложена из четырёх слагаемых, и одно из них неточное: объём распечатки проекта я оценил на глаз по толщине пачки, страницы там не пронумерованы сквозной нумерацией. Три остальных посчитаны точно. Сканы схем пришлось прогонять через OCR с русским языком: иначе поиск по ним невозможен, а листать схему глазами ради одного адреса — гарантированная ошибка.

Читать такой массив подряд бессмысленно: к пятисотой странице забудешь первую. Поэтому я строил не конспект, а реестры — таблицы, каждая из которых закрывает один аспект поведения:

  1. Реестр функций нагрузки — 17 функций: что делает каждая, какими параметрами управляется, какие блокировки.

  2. Каталог аварий — 13 позиций: код, условие возникновения, приоритет, план действий оператора, условие снятия.

  3. Циклограммы — 6 штук с точными числами: пороги, выдержки, шаги.

  4. Ступени модуля нагрузки — 27 ступеней: 14 по одному вводу и 13 по другому, с составом включаемых силовых элементов.

  5. Таблица I/O — 60 каналов с адресами и назначением: 27 дискретных входов, 32 дискретных выхода, 1 аналоговый выход.

Отдельно про ступени. В оригинале выбор ступени представлен маской в виде единого 32-битного слова: каждый бит — конкретный коммутационный элемент. Удобно для передачи по шине и ужасно для чтения человеком: чтобы понять, что делает значение вроде 16#0004_A801, его нужно разложить в биты и сверить с монтажной схемой. На этом шаге позже вскрылась половина расхождений.

Ключевой методический момент: основным эталоном я сделал руководство по эксплуатации, а не распечатку кода. Код — это то, что кто-то реализовал, возможно с ошибкой; РЭ — то, что было согласовано и что описывает требуемое поведение. Когда источники расходятся, расхождение надо не «примирить», а вынести на решение. Возьми я за эталон распечатку кода — перенёс бы в новую систему все её дефекты, и один из них, как покажет аудит, был опасным.

Эталоном я сделал описание требуемого поведения. Код — одна из его реализаций, и проверять надо её.

Архитектура новой программы ПЛК

Целевая платформа — Siemens S7-1500, CPU 1513-1 PN, TIA Portal V18: ровно та линейка, что стоит на объекте и которую сопровождает служба эксплуатации.

Оригинал — около 95 разрозненных FC/FB с обильным дублированием и кириллицей в именах блоков плюс «плоский» DB, где всё лежало вперемешку. Типичная картина проекта, росшего итерациями: понадобился второй кран — скопировали блок первого и поправили адреса; понадобился третий — скопировали второй. Проблема тут не эстетическая: любое изменение логики нужно вносить N раз, и на N-м разе кто-то ошибётся. Собственно, ошибки там и нашлись.

Что я сделал:

  • вынес повторяющиеся объекты в UDT и multi-instance FB: три крана — один FB, инстанцированный трижды; восемь дросселей — один FB на восемь экземпляров. Логика описана в одном месте, экземпляры отличаются только данными;

  • сделал один движок циклограмм вместо шести реализаций. Циклограмма перестала быть кодом и стала данными — таблицей шагов, которую движок исполняет;

  • разделил данные и логику: ступени нагрузки и параметры циклограмм лежат в retain-DB, поэтому правка выдержки или состава ступени на стенде не требует перепрошивки ПЛК.

Результат: 23 программные единицы — 9 внешних SCL-источников на 2 156 строк, 10 UDT и 4 DB, плюс таблица на 60 каналов I/O. Как считал: строки — wc -l по папке импорта; типы и блоки данных — grep -c '^TYPE' UDT_all.scl даёт 10, grep -c '^DATA_BLOCK' DB_global.scl даёт 4.

Сравнивать 23 с 95 напрямую нельзя: 95 — это FC и FB оригинала, а 23 — все программные единицы новой реализации вместе с типами и блоками данных. Корректное сравнение такое: три крана в оригинале были тремя почти одинаковыми блоками, стали одним FB на три экземпляра; восемь дросселей — восемью блоками, стали одним на восемь; шесть циклограмм были шестью ветками кода, стали одним движком и таблицей.

Data-driven подход я считаю главным архитектурным решением, поэтому покажу не пересказ, а сами файлы. Ниже — куски проекта как они лежат на диске. Единственная правка: обозначения приборов и генераторов в комментариях заменены на нейтральные, потому что публиковать их я не могу. Всё остальное — копипаста.

Ступень нагрузки:

TYPE "udtGenStage"VERSION : 0.1   STRUCT      id       : Int;    // Уникальный ID ступени. Заводские: три группы по три,                         //   по одной группе на каждый источник питания.                         //   Пользовательские добавляйте с 900+ (не пересекаться с заводскими).      mode     : Int;    // Фильтр экрана: 1 = режим первого насоса, 2 = режим второго.      gen      : Int;    // Источник (подпись/группировка): 1 и 2 — Ввод 1 (перем. ток),                         //   3 — Ввод 2 (пост. ток 28,5 В).      mask1    : Word;   // Маска ступеней ВВОДА 1 (перем.ток). Бит k => ступень (101+k),                         //   биты 0..13 = ступени 101..114. Пишется в рег.4 модуля нагрузки.      mask2    : Word;   // Маска ступеней ВВОДА 2 (пост.ток). Бит k => ступень (201+k),                         //   биты 0..12 = ступени 201..213. Пишется в рег.5 модуля нагрузки.      limitMs  : UDInt;  // Лимит удержания, мс. 0 = ПОСТОЯННАЯ. >0 = ВРЕМЕННАЯ:                         //   через limitMs ступень снимается и блокируется до «Сброса».                         //   Диапазон по требованию: 1000 (1 с) … часы.      isTemp   : Bool;   // TRUE=временная (учитывать limitMs+блокировать), FALSE=постоянная.      currentA : Real;   // Ток ступени, А — только ОТОБРАЖЕНИЕ (на логику не влияет).      kw       : Real;   // Мощность, кВт — только отображение.      used     : Bool;   // TRUE = строка действующая; FALSE = свободная (под добавление).                         //   fbGenLoad игнорирует строки used=FALSE.   END_STRUCT;END_TYPE

Здесь видно две вещи, ради которых всё и затевалось. Биты маски объяснены прямо в поле — не надо лезть в монтажную схему, чтобы понять, что означает 16#1911. И limitMs — тот самый таймер, о котором пойдёт речь в разделе про аудит, — не константа в коде, а число в строке таблицы: у ступени на максимальный ток там стоит 10000. Чтобы его поправить, TIA Portal не нужен.

Циклограмма разложена на шаг и массив шагов:

TYPE "udtCycloStep"VERSION : 0.1   STRUCT      solMask    : Word;    // маска СОЛЕНОИДОВ/дросселей шага (бит k = СОЛ k+1)      durMs      : UDInt;   // длительность шага, мс (1000 = 1 с)      targetFlow : Real;    // целевой расход на шаге, л/мин   END_STRUCT;END_TYPETYPE "udtCyclogram"VERSION : 0.1   STRUCT      stepCount : Int;                          // число активных шагов      used      : Bool;                         // TRUE = циклограмма существует      cycName   : String[20];                   // имя для отображения      step      : Array[1..16] of "udtCycloStep";   END_STRUCT;END_TYPE

Шестнадцать шагов на циклограмму — не расчёт, а запас: в самой длинной из шести штатных семь шагов. Массив самих циклограмм объявлен как Array[1..12]: шесть заводских и шесть свободных строк под то, что заказчик придумает потом.

А исполняет их всех один блок. Вот он целиком, 53 строки из 04_functions.scl:

FUNCTION_BLOCK "fbCyclogram"{ S7_Optimized_Access := 'TRUE' }VERSION : 0.1   VAR_INPUT      start   : Bool;          // запустить цикл (уровень)      cycleNo : Int;           // номер циклограммы 1..3   END_VAR   VAR_OUTPUT      solMask  : Word;      stepIdx  : Int;      finished : Bool;   END_VAR   VAR      sw        : "fbStopwatch";      idx       : Int;      running   : Bool;      startPrev : Bool;   END_VAR   VAR_TEMP      nSteps   : Int;      stepDone : Bool;      dummyEl  : UDInt;   END_VARBEGIN   IF (#cycleNo < 1) OR (#cycleNo > 12) THEN   // 1..6 заводские + 7..12 пользовательские      #solMask := 0; #finished := FALSE; RETURN;   END_IF;   #nSteps := "gConfig".cyclo[#cycleNo].stepCount;   // фронт запуска -> инициализация   IF #start AND NOT #startPrev THEN      #idx := 1; #running := TRUE; #finished := FALSE;   END_IF;   IF NOT #start THEN      #running := FALSE; #solMask := 0; #idx := 0;   END_IF;   #startPrev := #start;   IF #running AND (#idx >= 1) AND (#idx <= #nSteps) THEN      #sw(run := TRUE, reset := FALSE,          preset := "gConfig".cyclo[#cycleNo].step[#idx].durMs,          elapsed => #dummyEl, done => #stepDone);      #solMask := "gConfig".cyclo[#cycleNo].step[#idx].solMask;      IF #stepDone THEN         #sw(run := FALSE, reset := TRUE, preset := 0, elapsed => #dummyEl, done => #stepDone);         #idx := #idx + 1;         IF #idx > #nSteps THEN            #running := FALSE; #finished := TRUE; #solMask := 0;         END_IF;      END_IF;   END_IF;   #stepIdx := #idx;END_FUNCTION_BLOCK

Комментарий // номер циклограммы 1..3 во входной переменной устарел — проверка ниже пропускает 1…12. Не заметил, пока не вставлял сюда. Ошибки в этом нет, блок работает по проверке, а не по комментарию, но это ровно тот сорт мусора, который копится в проекте и через год вводит в заблуждение следующего. Поправлю в файле; здесь оставляю как есть, потому что цитата — это цитата.

Пятьдесят три строки вместо шести веток кода. Добавление режима перестало быть задачей программиста: строка в таблице, а не блок в проекте.

Смысл не в том, что стало «красивее». Логика крана теперь живёт в одном месте, и правка вносится один раз, а не трижды.

Карта Modbus как контракт

Между ПК верхнего уровня и ПЛК нужен был явный и стабильный интерфейс. Я сделал его картой Holding-регистров 0…499 с жёсткой разбивкой по зонам:

0   – 99    статус системы (режим, состояние приводов, флаги готовности)100 – 199   телеметрия (токи, напряжения, температуры, давления, расходы)200 – 253   команды (54 регистра, пишутся одним блоком)300 – 399   уставки400 – 431   аварии (битовые слова по каталогу)

Зонирование даёт читаемость (по номеру регистра сразу понятен класс, при отладке не нужно лезть в таблицу) и пакетность обмена: статус с телеметрией читаются непрерывными блоками, команды пишутся одним блоком из 54 регистров.

Последнее принципиально: при записи по одному регистру ПЛК какое-то время видит несогласованное состояние — например, новую ступень со старым признаком ввода. Запись блоком делает набор команд атомарным для прикладной логики.

ПЛК в этой схеме работает одновременно как Modbus TCP сервер и как клиент: сервером он отдаёт данные верхнему уровню, клиентом опрашивает периферию через шлюз RS-485↔TCP. Совмещение ролей — обычная практика, но требует аккуратности с тайм-аутами: медленный опрос периферии не должен приводить к «залипшей» телеметрии наверху, поэтому у каждой группы данных в зоне статуса есть признак актуальности.

Карта регистров — контракт между двумя командами. Зонирование, атомарность записи и признак протухания данных в нём такие же обязательные пункты, как сами адреса.

Замена HMI: вместо панели — ПК и браузер

Лицензия на панель утрачена, а покупать её заново означало через несколько лет оказаться в той же ловушке, поэтому человеко-машинный интерфейс переехал на ПК.

Стек: приложение .NET 8, где оболочка Avalonia служит мостом и держит жизненный цикл, внутри поднимается веб-сервер Kestrel (HTTP + WebSocket), рядом — драйвер Modbus TCP, журналирование и мониторинг ПК через WMI. Отдельно написан симулятор ПЛК (Modbus TCP slave), позволяющий запускать приложение целиком без единого куска железа: забегая вперёд, именно он сделал возможной верификацию.

Интерфейс оператора — браузер в режиме киоска, веб-HMI полностью офлайн, чистый JS без сборщиков и внешних зависимостей: на стенде нет интернета, а зависимость от CDN или node_modules — отложенная проблема, которая проявится через два года при переустановке.

Объём: 19 экранов, включая WYSIWYG-конструктор экранов и редактор карты Modbus. Инженерная часть — 21 файл C#, около 2 145 строк; клиентская — app.js на 1 693 строки и 157 обработчиков.

Из эксплуатационных требований заложено:

  • три уровня доступа и 8 функций, закрытых правами: оператор не должен иметь возможности переписать карту регистров;

  • журналы append-only с пофайловой хеш-цепочкой: каждая запись содержит хеш предыдущей, отредактировать журнал задним числом без разрыва цепочки нельзя. Журнал — часть доказательной базы испытаний;

  • аварийная подсистема по ISA-18.2: приоритизация, shelving — временное отключение надоедливой сигнализации с фиксацией факта и причины в журнале, детектирование дребезга — чтобы сигнализация, мигающая раз в секунду, не превращала список аварий в мусор;

  • NAMUR NE 107 для диагностики оборудования: отказ, требуется обслуживание, вне спецификации, проверка функции. Оператор различает «датчик врёт» и «параметр вышел за границы» — это разные действия.

Аудит: 60 совпадений и 7 расхождений

Когда новая система была написана, я сверил «оригинал ↔ новая программа» построчно по всем реестрам: I/O, ступени, циклограммы, аварии, карта регистров, экраны.

Сразу про границы, чтобы не выглядело сильнее, чем есть. I/O, аварии, циклограммы и карту регистров я прошёл целиком. Маски ступеней — 9 из 27: на каждую уходило минут двадцать ручной раскладки в биты, и после девятой, где восемь оказались с расхождениями, стало ясно, что проверять надо все, но времени до сдачи уже не было. Остальные 18 масок не проверены до сих пор. Это самая большая дыра в моей же методике, и я не знаю, сколько там ещё расхождений.

Хорошая новость: I/O сошлись 60 из 60 — 27 дискретных входов, 32 дискретных выхода и 1 аналоговый выход; адреса, назначение и логика (нормально открытый / нормально закрытый) совпали полностью. Практический смысл: перемонтаж не требуется — новая программа садится на существующие шкафы без единого перекинутого провода.

Плохая: нашлось 7 расхождений, из них 4 критических.

1. Таймер удержания максимального тока: 5 минут вместо 10 секунд

В режиме выхода на максимальный ток нагрузки — 1 200 А — руководство по эксплуатации предписывает удержание не более 10 секунд, после чего система обязана сбросить нагрузку. Ограничение физическое: обмотки на таком токе греются быстро, запас по времени определяется тепловой постоянной, а не удобством оператора.

В коде оригинала на этом таймере стояло 5 минут.

Тридцатикратное превышение допустимого времени.

Что за этим стоит физически — я не считал. Тепловой расчёт обмоток не делал, постоянную нагрева не измерял, к оборудованию доступа не было. Опираюсь на то, что написано в руководстве: десять секунд, дальше сброс нагрузки. Почему именно десять и что будет на трёхсотой — знает тот, кто это ограничение вносил. Мне достаточно того, что защита, рассчитанная на десять секунд, физически не сработает раньше пяти минут.

Дальше самое неприятное: дефект не в новом коде. Он унаследованный и жил в работающей системе, не проявляясь, потому что операторы, зная стенд, снимали режим руками задолго до срабатывания таймера. Защита существовала формально, а фактическую обеспечивала дисциплина персонала.

Обнаружился он ровно по одной причине: сверка велась построчно с руководством по эксплуатации, а не с исходным кодом. Возьми я за эталон распечатку проекта — а это интуитивно кажется правильным, ведь там «как оно реально работает», — я добросовестно перенёс бы 5 минут в новую программу и был бы уверен в полном соответствии оригиналу. Формально да, фактически — воспроизвёл бы отказ защиты.

Здесь общий принцип: реверс-инжиниринг неявно предполагает, что существующая система правильна — она работает, её приняли, на ней трудятся годами. Презумпция сильная и опасная. Работающая система доказывает лишь то, что она не отказала в тех сценариях, которые случались.

2. Маски ступеней нагрузки: 8 несовпадений из 9 проверенных

При раскладке 32-битных масок в биты и сверке с монтажной схемой выяснилось, что 8 из 9 проверенных масок не совпадают с оригинальной таблицей. Различия в отдельных битах: при выборе ступени включался не совсем тот набор коммутационных элементов.

Причина типична для копипастной архитектуры: маски правились в разных местах и в разное время, часть правок не доехала до всех копий. Все приведены к точным битам по таблице. Нашлось только потому, что маска была разложена в биты и сопоставлена с физикой — в шестнадцатеричном виде расхождение в одном бите глазом не видно.

3. Коллизия адресов HR220/221

В карте регистров команды калибровки и продувки оказались назначены на те же адреса, что выбор расходомера и выбор шкафа: два разных смысла на одном регистре. Следствие наглядное: нажатие «Продувка» ложно переключало генераторный шкаф во время испытаний. Команда обслуживающего характера меняла силовую конфигурацию стенда под нагрузкой. Раньше не замечали, потому что продувку в ходе испытаний обычно не жмут: сценарий не встречался — значит, дефект «не существовал».

Обе команды перенесены на свободные адреса, карта зафиксирована как документ и автоматически проверяется на уникальность назначений: теперь коллизия ловится проверкой, а не наблюдательностью.

Чего я так и не понял: как эта коллизия пережила приёмку. Либо продувку при сдаче не нажимали, либо нажимали и не связали с переключением шкафа. Спросить некого — тех, кто сдавал стенд, я не нашёл.

4. Двенадцать необъявленных символов в SCL

В восстановленных SCL-источниках нашлось 12 обращений к необъявленным символам — при импорте в TIA Portal проект упал бы на первой же компиляции. Найдены статическим анализом до импорта и объявлены.

Остальные три

  • Таймеры других режимов: в нескольких режимах вместо предписанных 5 минут стояло фактически бесконечное удержание. Тот же класс дефекта, что и первый, но с меньшим риском.

  • HMI показывал 6 приводов вместо физических 8. Два привода существовали в железе и в программе, но не были представлены на экране. Оператор не видел их состояния.

  • Блок записи команд моста: 34 регистра вместо 54. Верхний уровень писал в ПЛК усечённый блок, и часть правок, сделанных в редакторах, молча не доходила до ПЛК — не с ошибкой, не с диагностикой, просто не доходила. Человек менял уставку, видел её на экране и был уверен, что она применена.

Последний пункт я считаю вторым по неприятности после таймера: ошибка, приводящая к отказу, обнаруживается, а ошибка, приводящая к молчаливому игнорированию части команд, живёт годами и портит результаты испытаний.

Здесь я в первой редакции этого текста написал, что все расхождения найдены сверкой и ни одно — тестированием. Перечитал собственную статью и понял, что это неправда. Честная разбивка:

Расхождение

Чем найдено

Таймер 5 минут вместо 10 секунд

сверка кода с руководством

Маски ступеней, 8 из 9

раскладка масок в биты и сверка с монтажной схемой

Таймеры других режимов

сверка кода с руководством

HMI показывал 6 приводов вместо 8

сверка экранов с реестром I/O

Коллизия HR220/221

глазами, при чтении карты регистров

12 необъявленных символов

статический анализ, поймал бы и компилятор

34 регистра вместо 54

прокликивание интерфейса со снимком состояния симулятора

Четыре из семи — сверка двух независимых описаний. Три остальных нашлись инструментами и вниманием. Тестирование «нажали — работает» действительно не нашло бы ни одного из первых четырёх, и это существенно. Но утверждать, что сверка — единственный способ, было преувеличением.

Верификация без доступа к железу

Стенд в эксплуатации, останавливать его ради отладки никто не даст — значит, всё проверяемое должно быть проверено до выезда.

Статический анализ. Балансы SCL (парность конструкций, полнота ветвлений, объявленность символов), node --check по всему клиентскому JS, автоматический рендер всех экранов веб-HMI с контролем ошибок консоли. Дёшево и ловит целый класс дефектов, включая те 12 необъявленных символов.

Автономные смоук-тесты в браузере — два набора, четыре сценария: подключение, обмен, отрисовка, потеря связи. Все прошли.

Сплошная UI-проверка. Все 19 экранов и около 90 элементов управления прокликаны реальными кликами через браузерную автоматизацию — не просмотрены, а нажаты. После каждого действия контролировались снимок состояния симулятора ПЛК (что ушло в регистры) и защищённый журнал (появилась ли запись и та ли).

Именно она вскрыла дефект с 34 регистрами вместо 54: нажатие проходило, экран показывал изменение, а снимок ПЛК — что часть регистров не изменилась.

Целостность журнала. Хеш-цепочка проверена на 181 записи, разрывов нет.

Если объект недоступен, первым делом пишется его модель. Симулятор ПЛК обошёлся мне в два дня и окупился на первом же выезде, которого не пришлось делать.

Ложный след: полдня на несуществующую проблему

Один SCL-источник — тот, что отвечал за Modbus-клиент — при импорте в TIA давал 37 ошибок компиляции, после правок их стало 51. Все концентрировались вокруг вызова инструкции Modbus-клиента: несоответствие типов, неизвестный формальный параметр, неверное число аргументов.

Гипотеза родилась мгновенно и выглядела убедительно: несовместимость версии инструкции. Версии библиотек коммуникации в TIA действительно различаются по сигнатурам, это известная боль с документированными проявлениями. Я честно пошёл по этому пути: сверял версии, читал описание параметров, пробовал разные варианты вызова, менял типы. Полдня.

Настоящая причина оказалась другой. В TIA импортировалась устаревшая копия файла. В ней отсутствовало приведение типа, которое я добавил, и присутствовал формальный параметр, которого в актуальной версии инструкции нет. Я правил один файл, а компилировался другой. Все ошибки были абсолютно корректными — просто относились к тексту, который я уже исправил.

Мораль из тех, что усваиваются только через собственные полдня: прежде чем чинить код, убедись, что компилируется именно тот файл, который ты правил. Проверяется за минуту — вставить заведомо ошибочную строку и посмотреть, сообщит ли о ней компилятор. Не сообщил — вы отлаживаете не тот файл. И отдельно: правдоподобная гипотеза опаснее неправдоподобной — «несовместимость версий» объясняла симптомы так хорошо, что я перестал проверять предпосылки.

Условия работы и сборка без интернета

Работа шла с 3 июня по 11 июля, по вечерам и выходным, на кухонном столе: два монитора, распечатки схем на полу, потому что на столе они не помещаются.

Часть сеансов шла через удалённый доступ к машине на объекте, и канал рвался каждые 1–3 минуты. Нормальная работа в IDE в таких условиях невозможна — не успеваешь набрать строку. Процесс пришлось перестроить: правки файлов делались однострочниками PowerShell, а ввод текста дробился на куски не длиннее 14 символов — эмпирически подобранная длина, успевавшая уходить между обрывами. Медленно, но детерминированно: операция либо прошла целиком, либо не прошла вовсе.

Целевая машина не имела доступа в интернет, как и положено машине технологического контура. Под сборку .NET-приложения был подготовлен офлайн-кэш NuGet: 39 пакетов, около 138 МБ. При первой сборке с нуля вылезли 3 реальные ошибки компиляции — не проблемы окружения, а мои ошибки в коде, маскировавшиеся тем, что часть кода писалась без сборки. Исправлены, итоговая сборка — 0 ошибок, 0 предупреждений.

Об инструменте

Работа велась с использованием LLM-ассистента как основного инструмента разработки. Что он дал: скорость чтения 4 900 страниц (вручную построение пяти реестров заняло бы месяцы), генерацию значительной части SCL и C# по уже принятым решениям и построчную сверку — монотонное сопоставление таблиц, на котором человеческое внимание отказывает первым, и именно оно дало четыре расхождения из семи. Чего не дал: ни одного архитектурного решения — переход от 95 блоков к 23, data-driven хранение ступеней и циклограмм, зонирование карты регистров, выбор веб-HMI, решение написать симулятор ставились человеком; разрешение противоречий в эталоне, где выбор делается из понимания физики; постановку задачи — что считать эталоном и что вообще является дефектом. Таймер на 5 минут ассистент нашёл не сам: он нашёл его потому, что я поставил задачу сверять код с руководством по эксплуатации, а не с распечаткой кода.

Что бы я сделал иначе

Начал бы с OCR и реестров, а не с чтения. Первые дни я читал документы «как книгу», составляя общее впечатление. Полезной работой стало только построение реестров — начинать надо было сразу с них.

Раньше сделал бы симулятор ПЛК. Он появился ближе к концу, когда HMI уже был написан. Существуй он с самого начала, часть интеграционных дефектов (те же 34 регистра вместо 54) обнаружилась бы сразу.

Сразу автоматизировал бы проверку карты регистров на коллизии. HR220/221 я нашёл глазами, и это удача. Проверка уникальности назначений пишется за полчаса и должна была появиться в первый же день существования карты. Туда же — проверка тождественности правимого и компилируемого файла: полдня на ложный след это её цена.

Сообщал бы о расхождениях сразу, а не пакетом. Первые находки я копил, чтобы предъявить единым перечнем. Это ошибка коммуникации: про таймер на 1 200 А надо было сообщать в тот же час, а не в составе отчёта.

Не переоценивал бы «оно же работает». Я потратил заметное время, объясняя себе, что расхождения могут быть не ошибками, а сознательными решениями предшественников. По части — правда, по критическим четырём — нет.

Выводы

Бумажный комплект документации — не формальность. Именно распечатки, сданные когда-то по требованию приёмки, позволили восстановить систему. Если есть выбор между осмысленной распечаткой и отпиской, сдавайте осмысленную.

Эталоном должно быть требуемое поведение, а не существующая реализация. Это дало четыре критические находки, включая тридцатикратное превышение допустимого времени удержания максимального тока.

Data-driven архитектура окупается на объекте. Ступени и циклограммы данными в retain-DB означают, что настройка стенда не требует ни TIA Portal, ни перепрошивки, ни программиста.

Работающая система ничего не доказывает про сценарии, которые не случались. Пять минут вместо десяти секунд прожили в эксплуатации ровно потому, что операторы были дисциплинированы. Защита, работающая за счёт дисциплины персонала, защитой не является.

Итог одной строкой: 4 900 страниц эталона, 17 функций нагрузки, 13 аварий, 6 циклограмм, 27 ступеней (проверено 9), 95 блоков оригинала против 23 программных единиц новой реализации, 2 156 строк SCL, 19 экранов веб-HMI, 60 из 60 совпавших каналов I/O, 7 расхождений, 4 критических, 0 ошибок при сборке.

Обновления

Раздел заведён заранее и пуст. Найдёте расхождение между тем, что здесь написано, и тем, что следует из цифр, — напишите в комментариях: правка появится здесь с датой и вашим ником. В прошлый раз этот раздел собрал шесть пунктов, и статья от этого стала лучше.


Методическая часть применима далеко за пределами стендов, и обсудить мне интереснее всего именно её.

Вопрос к читателям: если на объекте работающая система расходится с документацией — что вы считаете эталоном и почему? Мне встречались обе позиции, обе аргументированные.

ссылка на оригинал статьи https://habr.com/ru/articles/1075020/