Главная → Академия → Исполнительные устройства: интерлоки, таймауты → Алгоритм: 'Если ошибка или таймаут -> Перевести в Safe-State'

Алгоритм: 'Если ошибка или таймаут -> Перевести в Safe-State'

Введение: Гарантия отказоустойчивости системы

В основе любой профессионально спроектированной системы автоматизации лежит не только логика управления в штатном режиме, но и, что более важно, предсказуемое поведение в нештатных ситуациях. Любой сбой, будь то обрыв связи с устройством или внутренняя ошибка программного кода, не должен приводить к катастрофическим последствиям. Именно здесь на первый план выходит концепция безопасного состояния (Safe-State).

> 🔗 Связанный материал: Для глубокого понимания философии «Безопасного Состояния», его определения для различных типов устройств и фундаментальной роли в обеспечении надежности, настоятельно рекомендуется повторно изучить материалы урока COURSE-05-M05-L01 «Концепция ‘безопасного состояния’ (Safe-State)».

Кратко напомним, что Safe-State — это предопределенное, заведомо безопасное состояние исполнительного устройства, в которое оно должно быть принудительно переведено в случае потери управления или обнаружения критической ошибки. Например:

  • Для насоса системы отопления — выключить, чтобы предотвратить перегрев и повреждение.
  • Для электромагнитного клапана системы защиты от протечек — закрыть, чтобы остановить подачу воды.
  • Для привода ворот — остановить движение, чтобы не нанести ущерб имуществу или здоровью.

Цель данного урока — выйти за рамки теоретических концепций и спроектировать, а затем реализовать в среде Node-RED, универсальный и переиспользуемый алгоритм, который гарантированно переводит исполнительное устройство в его «безопасное состояние» при возникновении любого из двух типов отказов:

  • Явный отказ: Ошибка выполнения, перехваченная системой мониторинга.
  • Неявный отказ: Отсутствие подтверждения от устройства в течение заданного времени (таймаут).
  • Для достижения этой цели мы будем использовать комбинацию мощных инструментов Node-RED, которые формируют основу отказоустойчивой логики на платформе HI:

    • Нода `Catch`: Наш главный инструмент для перехвата явных ошибок, который мы подробно рассмотрели в предыдущем уроке.
    • Нода `Trigger`: Ключевой компонент для реализации механизма «сторожевого таймера» (watchdog), отслеживающего таймауты.
    • Нода `Function`: Сердце нашей логики, где мы будем анализировать тип сбоя и формировать управляющую команду.
    • Субпотоки (Subflows): Механизм инкапсуляции, который позволит нам создать универсальный, легко конфигурируемый и многократно используемый блок «Переход в Safe-State».

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

    ---

    Проблема таймаутов: когда молчание — не золото

    В системах автоматизации, взаимодействующих с физическим миром через промышленные протоколы, такие как Modbus, DALI или CAN, существует два типа ответа от устройства:

  • Положительный/Отрицательный ответ: Устройство получило команду и сообщает о результате («Команда выполнена» или «Ошибка: неверный параметр»).
  • Молчание: Устройство не отвечает вовсе.
  • На первый взгляд, второй вариант может показаться менее критичным — возможно, произошел кратковременный сбой связи, который скоро восстановится. Однако именно молчание является наиболее опасным и коварным видом отказа. Отсутствие ответа порождает неопределенность.

    > ⚠️ Внимание: Игнорирование таймаутов приводит к десинхронизации состояния (state desynchronization) — ситуации, когда «цифровой двойник» системы в памяти контроллера перестает соответствовать реальному физическому состоянию объекта. Это является первопричиной непредсказуемого поведения, ложной логики и потенциально аварийных ситуаций.

    Представим простой сценарий на объекте «умный коттедж»:

  • Контроллер HI отправляет по шине RS-485 команду Modbus «Закрыть клапан подачи воды в систему полива».
  • Из-за помех на линии или временного сбоя питания на модуле реле, пакет с командой не доходит до исполнителя. Модуль не отвечает.
  • Программная логика, не получив сообщения об ошибке, наивно полагает, что команда была выполнена, и обновляет внутреннее состояние `flow.valve_pool = 'CLOSED'`.
  • Через 5 минут, согласно сценарию, включается насос подкачки давления в системе. Поскольку контроллер «уверен», что клапан закрыт, он считает это действие безопасным.
  • Результат: Насос начинает качать воду в открытый контур, что приводит к затоплению технического помещения.
  • Таймаут (Timeout) — это ситуация, когда контроллер (Master) не получает ответа от исполнительного устройства (Slave) в течение заранее определенного, ожидаемого промежутка времени. Обработка таймаутов — это не опция, а обязательное требование для создания надежной системы. Если мы не получили подтверждения, мы должны исходить из худшего сценария: команда не была выполнена, и состояние объекта неизвестно.

    Для решения этой задачи в Node-RED существует стандартный и очень гибкий узел — `Trigger`. В нашем контексте мы будем использовать его не для простой задержки, а в режиме «сторожевого таймера» (watchdog timer). Логика его работы предельно проста и эффективна:

    • «Взвести» таймер при отправке команды.
    • «Сбросить» таймер при получении ожидаемого подтверждения.
    • Если подтверждение не пришло и таймер не был сброшен, по истечении времени он сгенерирует сообщение о таймауте.

    Этот механизм позволяет нам превратить опасное «молчание» устройства во вполне конкретное и обрабатываемое событие в нашем потоке.

    ---

    Практикум: Настройка ноды 'Trigger' для отслеживания таймаутов

    Рассмотрим на практическом примере, как реализовать механизм «сторожевого таймера» для управления Modbus-реле, отвечающим за включение насоса.

    Задача: Отправить команду на включение реле. Если в течение 2 секунд не придет подтверждение о смене состояния, сгенерировать сообщение о таймауте. Логика потока:
  • Основной поток инициирует отправку команды на включение реле. Это сообщение разветвляется на два пути.
  • Путь А: Сообщение уходит на узел `Modbus-Write` для физического исполнения.
  • Путь Б: То же самое сообщение поступает на вход ноды `Trigger` и «взводит» таймер. Одновременно оно содержит специальное свойство для сброса, например, `msg.reset = true`.
  • Поток подтверждения: Отдельный поток постоянно опрашивает состояние реле (например, через `Modbus-Read`). Когда он фиксирует, что реле включилось, он формирует сообщение-подтверждение и отправляет его на вход той же ноды `Trigger`.
  • Нода `Trigger`, получив сообщение-подтверждение, сбрасывает свой таймер и не генерирует событие таймаута.
  • Сценарий отказа: Если поток подтверждения не пришлет сообщение в течение 2 секунд, таймер в ноде `Trigger` сработает и отправит на свой выход сообщение о таймауте.
  • Пошаговая настройка:
  • Разместите на холсте ноду `Trigger`.
  • Откройте ее настройки. Они выглядят сложнее, чем у других нод, но их логика очень стройная.
  • Настройте ее следующим образом:
  • * Send: `Nothing`. (Нам не нужно ничего отправлять немедленно).

    * then wait for: `2` seconds. (Это и есть время нашего таймаута).

    * then send: Выберите `the original msg.payload` и измените его на JSON. Введите следующий объект:

            {

    "status": "error",

    "type": "timeout",

    "description": "Устройство не подтвердило выполнение команды"

    }

    * Handling incoming messages: Выберите `if msg arrives, reset the timer`. Это ключевая настройка для режима "watchdog".

    * Поставьте галочку `Extend delay if new message arrives`.

    Схема потока в ASCII:
    // ========= Поток отправки команды и контроля исполнения =========
    
    

    // 1. Инициация команды

    [Inject: "Включить насос"] --> [Function: "Формировать команду"] -->+

    |

    // 2. Разветвление потока |

    |

    +------------------------------------------------------------------+

    |

    +--> [Trigger: "Watchdog 2s"] --> [Function: "Обработка таймаута"]--> [Debug: "ALARM!"]

    | ^

    | | (сброс таймера)

    +-------+----------------------------+

    | |

    +--> [Modbus-Write: "Включить реле"] |

    |

    // ========= Поток получения подтверждения =========

    |

    // 3. Опрос состояния реле (может быть инициирован после записи)

    [Modbus-Read: "Проверить состояние реле"] --> [Function: "Анализ ответа"] --> [Debug: "Подтверждение получено"]

    Пример сообщений:
  • Сообщение, инициирующее команду (выход из `Function: "Формировать команду"`):
  • Оно отправляется одновременно и на `Modbus-Write`, и на `Trigger`.

        {

    "topic": "commands/pump/well-1/set",

    "payload": {

    "value": true,

    "fc": 5,

    "unitid": 1,

    "address": 0

    },

    "device_id": "pump-well-1"

    }

  • Сообщение-подтверждение (выход из `Function: "Анализ ответа"`):
  • Оно отправляется только на вход `Trigger` для сброса таймера. Содержимое не так важно, главное — сам факт прихода сообщения.

  • Сообщение о таймауте (выход из `Trigger` при сбое):
  • Это сообщение будет отправлено, если подтверждение не пришло за 2 секунды. Мы передаем в нем `msg.topic` из оригинального сообщения, чтобы знать, какое именно устройство отказало.

        {

    "topic": "commands/pump/well-1/set",

    "payload": {

    "status": "error",

    "type": "timeout",

    "description": "Устройство не подтвердило выполнение команды"

    },

    "device_id": "pump-well-1"

    }

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

    ---

    Объединение логики: обработка ошибок и таймаутов в одном потоке

    Итак, к текущему моменту в нашем арсенале есть два независимых механизма обнаружения нештатных ситуаций:

    | Тип отказа | Механизм обнаружения | Описание |

    | :------------- | :------------------- | :------------------------------------------------------------------------------------------------------ |

    | Явный отказ | Нода `Catch` | Перехватывает ошибки, генерируемые узлами (например, `Modbus-Write` сообщает об ошибке CRC или обрыве связи). |

    | Неявный отказ | Нода `Trigger` | Генерирует событие таймаута, если ожидаемое сообщение-подтверждение не пришло вовремя. |

    Оба этих механизма генерируют на выходе сообщения, сигнализирующие о проблеме. Держать для каждого из них отдельную логику обработки — плохая практика. Это приводит к дублированию кода и усложняет поддержку системы. Профессиональный подход заключается в создании единой точки обработки отказов.

    > 🔗 Связанный материал: Вспомните принципы работы с нодой `Catch` из урока COURSE-05-M05-L02 «Отлов ошибок выполнения с помощью ноды Catch». Мы будем подключать ее выход напрямую к нашему новому унифицированному механизму.

    Идея состоит в том, чтобы создать своего рода «шину безопасности» (Safety Bus) в нашем проекте Node-RED. Все сообщения об ошибках, независимо от их источника, должны направляться на эту шину.

    Проектирование архитектуры:
  • На каждой вкладке (flow) вашего проекта размещается нода `Catch`, настроенная на перехват ошибок со всех узлов на этой вкладке (`All nodes on current flow`).
  • В каждом потоке, где реализован контроль исполнения с помощью ноды `Trigger`, ее «таймаут-выход» также считается источником ошибки.
  • Выходы всех нод `Catch` и всех «таймаут-выходов» от нод `Trigger` мы соединяем с нодой `Link Out` с говорящим названием, например, `To Safety Handler`.
  • На отдельной, выделенной вкладке «Safety Logic» мы размещаем ноду `Link In` (`From Safety Handler`), которая будет принимать все эти сообщения об ошибках.
  • Эта простая архитектура позволяет централизовать всю логику реагирования на сбои в одном месте. Теперь, вместо того чтобы размазывать обработку ошибок по всему проекту, мы можем сосредоточить ее в одном, легко поддерживаемом и тестируемом потоке.

    Идеальным инструментом для реализации самой логики обработки является субпоток (Subflow). Мы создадим субпоток `Go To Safe-State`, который будет нашим универсальным «черным ящиком». На вход он будет получать сообщение об ошибке, а внутри него будет реализована вся логика по приведению отказавшего устройства в безопасное состояние.

    Преимущества такого подхода:
    • Переиспользование: Один и тот же субпоток можно использовать для десятков и сотен устройств.
    • Простота поддержки: Если вам нужно изменить логику перехода в Safe-State (например, добавить отправку уведомления в Telegram), вы делаете это в одном месте, и изменение применяется ко всей системе.
    • Стандартизация: Все отказы обрабатываются по единому, предсказуемому алгоритму.

    Следующий шаг — практическая реализация этого универсального субпотока.

    ---

    Практикум: Создание универсального субпотока 'Safe-State'

    Создадим субпоток, который будет принимать на вход сообщение об ошибке (от `Catch` или `Trigger`), определять, какое устройство отказало, и отправлять ему команду на переход в безопасное состояние. В качестве универсальной шины команд будем использовать MQTT, как это принято на платформе HI.

    > 💡 Подсказка: Для того чтобы сделать субпоток по-настоящему универсальным, используйте его свойства (Subflow Properties). Например, можно создать свойство `DEFAULT_SAFE_STATE_PAYLOAD`, где будет храниться JSON-объект команды для безопасного состояния (например, `{"value": false}`). Это позволит настраивать поведение субпотока для разных классов устройств без изменения его внутреннего кода.

    Пошаговая инструкция:
  • В меню Node-RED выберите `☰ -> Subflows -> Create Subflow`.
  • Назовите новый субпоток `Safe-State Handler`.
  • Перейдите во вкладку `Properties` субпотока. Добавьте свойство `SAFE_STATE_COMMAND` с типом `JSON` и значением по умолчанию:
  •     { "value": false }

  • Внутри субпотока разместите один вход (`Input`) и три выхода (`Output`). Назовем выходы:
  • * Выход 1: `MQTT Command` (для отправки команды)

    * Выход 2: `Log Entry` (для записи в журнал аудита)

    * Выход 3: `Notification` (для отправки уведомлений администратору)

  • Между входом и выходами разместите ноду `Function`. Это будет мозг нашего субпотока.
  • Логика внутри ноды `Function`:

    Нода должна уметь обрабатывать два типа входящих сообщений:

    • От ноды `Catch`: Сообщение содержит объект `msg.error` с информацией об источнике.
    • От ноды `Trigger`: Сообщение содержит payload, который мы настроили ранее, и `msg.topic` изначальной команды.
    Код для ноды `Function`:
    // Получаем доступ к свойствам субпотока
    

    const safeStatePayload = env.get("SAFE_STATE_COMMAND");

    let deviceTopic = null;

    let originalMessage = null;

    let errorInfo = {};

    // 1. Определяем источник ошибки и извлекаем топик устройства

    if (msg.error) {

    // === Ошибка пришла от ноды Catch ===

    node.status({ fill: "red", shape: "dot", text: "Error from Catch" });

    // Пытаемся извлечь топик из источника ошибки

    if (msg.error.source && msg.error.source.node && msg.error.source.node.topic) {

    deviceTopic = msg.error.source.node.topic;

    } else if (msg.topic) {

    // Если в источнике нет, ищем в самом сообщении

    deviceTopic = msg.topic;

    }

    originalMessage = msg.error.message;

    errorInfo = {

    type: "Execution Error",

    source_id: msg.error.source.id,

    source_name: msg.error.source.name,

    message: msg.error.message

    };

    } else if (msg.payload && msg.payload.type === 'timeout') {

    // === Ошибка пришла от ноды Trigger (таймаут) ===

    node.status({ fill: "orange", shape: "dot", text: "Timeout from Trigger" });

    deviceTopic = msg.topic; // Топик был в оригинальном сообщении

    originalMessage = msg.payload.description;

    errorInfo = {

    type: "Timeout",

    message: "Device did not acknowledge command in time."

    };

    } else {

    // Неизвестный тип ошибки, просто логируем и останавливаем

    node.warn("Unknown error type received in Safe-State subflow");

    return null;

    }

    // 2. Проверяем, удалось ли определить топик устройства

    if (!deviceTopic || !deviceTopic.includes('/set')) {

    node.error("Could not determine device command topic. Cannot enforce Safe-State.", msg);

    return null; // Не можем ничего сделать, если не знаем, кому отправлять команду

    }

    // 3. Формируем команду для перехода в Safe-State

    // Заменяем "/set" на конце топика, чтобы гарантированно попасть в командный топик.

    const commandTopic = deviceTopic.replace(/\/state$/, '/set'); // На случай если пришел топик состояния

    const safeStateMsg = {

    topic: commandTopic,

    payload: safeStatePayload,

    retain: false // Команды безопасности не должны быть retained

    };

    // 4. Формируем сообщение для журнала аудита (MySQL, InfluxDB и т.д.)

    const logMsg = {

    topic: "system/audit/log",

    payload: {

    ts: Date.now(),

    level: "CRITICAL",

    event: "SAFE_STATE_ENFORCED",

    target_topic: commandTopic,

    reason: errorInfo,

    original_msg: originalMessage

    }

    };

    // 5. Формируем сообщение для уведомления

    const notifyMsg = {

    topic: "system/admin/notify",

    payload: `ВНИМАНИЕ! Устройство ${commandTopic} принудительно переведено в безопасное состояние. Причина: ${errorInfo.type} - ${errorInfo.message}`

    };

    // 6. Отправляем сообщения на соответствующие выходы

    // node.send([msg1, msg2, msg3])

    return [safeStateMsg, logMsg, notifyMsg];

    Схема внутри субпотока:
    [Input] --> [Function: "Safe-State Logic"] --+--> [Output 1: MQTT Command]
    

    +--> [Output 2: Log Entry]

    +--> [Output 3: Notification]

    После создания и настройки этого субпотока вы можете просто перетаскивать его на холст, как любую обычную ноду, и подключать к нему выходы от `Catch` и `Trigger`. Ваша система станет несоизмеримо более надежной и предсказуемой.

    ---

    Итоги и лучшие практики

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

    Краткое резюме итогового алгоритма:
  • Отправка любой критически важной команды исполнительному устройству инициирует «сторожевой таймер» на базе ноды `Trigger`.
  • Система ожидает получения логического или физического подтверждения выполнения команды.
  • Успешное получение подтверждения сбрасывает таймер, и система продолжает работать в штатном режиме.
  • В случае возникновения явной ошибки (например, отказ Modbus-устройства), она перехватывается нодой `Catch`.
  • В случае неявного отказа (молчание устройства), срабатывает таймер в ноде `Trigger`.
  • Оба типа отказов направляются в единую точку обработки — универсальный субпоток `Safe-State Handler`.
  • Субпоток анализирует инцидент, формирует и отправляет команду для принудительного перевода устройства в его заранее определенное безопасное состояние.
  • Параллельно система регистрирует инцидент в журнале аудита и может отправлять уведомление администратору.
  • Ключевые лучшие практики (Best Practices):
    • Всегда определяйте Safe-State: Для любой системы, управляющей инженерным оборудованием (насосы, клапаны, вентиляция, приводы, системы контроля доступа), «безопасное состояние» должно быть определено еще на этапе проектирования.
    • Инкапсулируйте логику безопасности: Используйте субпотоки для реализации логики перехода в Safe-State. Это упрощает поддержку, масштабирование и аудит системы. Ваш проект становится чище и понятнее.
    • Логируйте каждый инцидент: Каждый случай принудительного перевода в Safe-State — это важный сигнал о проблеме в системе. Детальное логирование (какое устройство, когда, по какой причине) является бесценным источником данных для диагностики и предотвращения будущих сбоев.
    • Не доверяйте, а проверяйте: Принцип «Trust, but verify» является основой надежной автоматизации. Отправка команды — это только половина дела. Контроль ее исполнения — вторая, и не менее важная, половина.
    • Разделяйте командные топики и топики состояния: В MQTT используйте отдельные топики для команд (`.../set`) и для обратной связи (`.../state`). Это делает логику контроля исполнения прозрачной и надежной.

    Что дальше?

    Освоив принципы построения отказоустойчивых потоков для отдельных устройств, в следующем модуле мы перейдем на более высокий уровень и рассмотрим, как создавать сложные сценарии, координирующие работу десятков устройств, и как управлять состояниями целых подсистем (климат, освещение, безопасность) с помощью паттерна «Конечный автомат» (Finite State Machine).