Текст я писал с языковой моделью: она собирала черновик и вычитывала формулировки. Решения, замеры и разбор находок мои. Это моя вторая публикация здесь; в первой читатели нашли шесть расхождений между текстом и кодом, и с тех пор я цитирую цифры только из файлов и журналов, а не по памяти.
Меня зовут Дмитрий Груздев, я главный конструктор АСУТП. Занимаюсь системами управления испытательными стендами — теми, где изделие после ремонта гоняют по режимам, снимают характеристики и решают, годно ли оно к эксплуатации. Эта статья — про случай, когда пришлось восстанавливать работающую систему без единой строки исходного кода, и про то, что при этом нашлось в оригинале.
Названий предприятий, обозначений изделий и номеров документов здесь не будет — только инженерная суть: архитектура, цифры технических решений, методика сверки, найденные дефекты.
Постановка: система жива, а сопровождать её нечем
Есть действующий испытательный стенд с энергоёмкой нагрузочной установкой. Он работает: ПЛК крутит программу, оператор смотрит на HMI-панель, режимы отрабатываются. И при этом:
лицензия на HMI-панель утрачена: панель работает, пока работает, но перенести проект, отредактировать экран или восстановить конфигурацию после отказа железа невозможно;
исходников программы ПЛК нет — ни архива, ни резервной копии, ни у эксплуатанта, ни у того, кто это когда-то делал;
выгрузить проект из ПЛК нельзя: блоки защищены, а восстановление байт-кода до вменяемого исходника здесь экономически бессмысленно.
Что есть: бумага. Распечатка проекта среды программирования, распечатка экранов HMI, руководство по эксплуатации и сканы электрических схем — наследие приёмки, когда бумажный комплект сдавали как часть документации. Тогда формальность, теперь единственный источник истины.
Задача сформулирована жёстко: восстановить и переписать систему заново так, чтобы поведение полностью совпало с оригиналом. Не «лучше» и не «современнее», а именно совпало: стенд аттестован, методики привязаны к его поведению, любое расхождение — расхождение с методикой.
Почему нельзя было «просто написать заново»
Первая реакция инженера — «напишем с нуля, там всего-то краны, дроссели и генератор». Я её сам испытал, поэтому объясню, почему она неверна.
Стенд — не абстрактный технологический объект, а средство испытаний, чьё поведение зафиксировано в аттестационных документах и методиках. Выбирая режим, оператор ожидает конкретную циклограмму: последовательность включения ступеней, выдержки, пороги перехода. Напиши я «логично и правильно», но с другими выдержками — получится другой стенд, формально работающий, фактически не тот, на котором получены зачётные результаты.
Второй аргумент — аварии. Каталог аварийных ситуаций не декоративный список сообщений: за каждым условием стоит либо физика (перегрев, превышение тока, потеря давления), либо инцидент. Придумать набор заново — выбросить накопленный опыт эксплуатации.
Третий аргумент — монтаж: перебирать шкафы и переназначать клеммы никто не собирался, значит новая программа обязана «сесть» на существующую электрику один в один.
Отсюда решение: не писать новую систему, а восстановить эталон по документации и затем реализовать его заново на современном стеке. Два этапа, путать их нельзя: первый — археология, второй — инженерия.
Когда объект аттестован, «переписать лучше» и «переписать эквивалентно» — разные проекты с разной приёмкой. Отвечать на вопрос, какой из двух вы делаете, надо до начала, а не в середине.
Как читаются 4 900 страниц
Объём эталонного комплекта в цифрах:
Источник | Объём | Что даёт |
|---|---|---|
Распечатка проекта среды программирования | ~900 страниц | Все блоки, теги, сети LAD и SCL |
Экспорт экранов HMI | 3 907 страниц, 26 экранов | Каждая кнопка с действием и привязкой к тегам |
Руководство по эксплуатации | 93 страницы | Семантика режимов, таблицы, циклограммы |
Электрические схемы | сканы | Физическая привязка I/O |
Итого 4 900 страниц. Цифра сложена из четырёх слагаемых, и одно из них неточное: объём распечатки проекта я оценил на глаз по толщине пачки, страницы там не пронумерованы сквозной нумерацией. Три остальных посчитаны точно. Сканы схем пришлось прогонять через OCR с русским языком: иначе поиск по ним невозможен, а листать схему глазами ради одного адреса — гарантированная ошибка.
Читать такой массив подряд бессмысленно: к пятисотой странице забудешь первую. Поэтому я строил не конспект, а реестры — таблицы, каждая из которых закрывает один аспект поведения:
Реестр функций нагрузки — 17 функций: что делает каждая, какими параметрами управляется, какие блокировки.
Каталог аварий — 13 позиций: код, условие возникновения, приоритет, план действий оператора, условие снятия.
Циклограммы — 6 штук с точными числами: пороги, выдержки, шаги.
Ступени модуля нагрузки — 27 ступеней: 14 по одному вводу и 13 по другому, с составом включаемых силовых элементов.
Таблица 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_TYPE TYPE "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_VAR BEGIN 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 ошибок при сборке.
Обновления
Раздел заведён заранее и пуст. Найдёте расхождение между тем, что здесь написано, и тем, что следует из цифр, — напишите в комментариях: правка появится здесь с датой и вашим ником. В прошлый раз этот раздел собрал шесть пунктов, и статья от этого стала лучше.
Методическая часть применима далеко за пределами стендов, и обсудить мне интереснее всего именно её.
Вопрос к читателям: если на объекте работающая система расходится с документацией — что вы считаете эталоном и почему? Мне встречались обе позиции, обе аргументированные.

