ГлавнаяАкадемияИсполнительные устройства: интерлоки, таймауты → Хранение состояния: переменные потока (flow.context)

Хранение состояния: переменные потока (flow.context)

Урок 1 · Исполнительные устройства: интерлоки, таймауты · 30 мин · theory

Введение: Зачем хранить состояние?

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

> 🔗 Связанный материал: Этот урок является прямым продолжением урока `COURSE-05-M03-L01`, где мы рассмотрели саму концепцию и важность взаимных блокировок. Убедитесь, что вы понимаете базовые сценарии, прежде чем переходить к практической реализации.

До сих пор наша логика в Node-RED была преимущественно "stateless" (без состояния). Это означает, что каждый узел обрабатывал входящее сообщение (`msg`) независимо от предыдущих сообщений. Например, при поступлении команды `{"command": "ON"}` на включение отопления, поток просто отправлял сигнал на соответствующее реле. Он не имел никакой информации о том, что происходит в остальной системе.

Такой подход имеет серьезные ограничения:

Для решения этих проблем нам необходимо внедрить концепцию "состояния" (state). Состояние — это, по сути, память нашей системы автоматизации. Это информация о предыдущих событиях, командах и текущем статусе оборудования, которая хранится внутри контроллера HI и доступна для анализа в потоках Node-RED.

Существует несколько способов хранения состояния, но основным и наиболее гибким инструментом на уровне потока являются переменные контекста (context variables). Они позволяют нам создавать "флаги" и сохранять значения, которые один узел потока может записать, а другой — прочитать в любой момент времени. Именно переменные контекста превратят наши простые потоки в интеллектуальные системы, способные принимать решения на основе полной картины происходящего.

---

Переменные контекста в Node-RED: flow, global, node

Node-RED предоставляет три уровня (или области видимости) для хранения переменных контекста. Правильный выбор уровня определяет, насколько доступными и долговечными будут ваши данные, что напрямую влияет на надежность и архитектуру всей системы.

> 💡 Подсказка: Используйте `flow.context` для логики, специфичной для одного потока (например, управление одной группой устройств на одной вкладке). Прибегайте к `global.context` только для данных, которые должны быть доступны абсолютно всем потокам в проекте (например, общий режим 'Отпуск' или глобальные уставки безопасности).

Рассмотрим каждый уровень подробно:

1. Контекст узла (node.context)

2. Контекст потока (flow.context)

3. Глобальный контекст (global.context)

* Режим работы объекта (`normal`, `away`, `vacation`).

* Статус глобальных систем (`security_armed`, `fire_alarm`).

* Общие уставки, которые могут использоваться в разных сценариях (например, `global_target_temperature`).

* Избегайте "загрязнения" глобального контекста локальными переменными, чтобы не усложнять отладку.

Способы работы с контекстом

Существует два основных способа установки и получения значений из контекста:

| Способ | Описание | Пример |

| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |

| Узел `Change` | Графический способ, не требующий написания кода. Идеален для простых операций установки, изменения или удаления переменных. | `Set` `flow.heating_active` `to` `true` (boolean). |

| Узел `Function` | Программный способ с использованием JavaScript. Предоставляет максимальную гибкость для чтения, записи и сложной логики. | `flow.set('heating_active', true);`
`let is_active = flow.get('heating_active');` |

Использование узла `Function` является предпочтительным для реализации логики блокировок, так как позволяет в одном месте прочитать текущее состояние, принять решение и записать новое состояние.

---

Практика: Настройка Interlock с помощью flow.context

Давайте реализуем классический сценарий взаимной блокировки для системы климат-контроля. Наша задача — не допустить одновременной работы контактора отопления и контактора системы кондиционирования.

Сценарий:

Мы будем использовать одну переменную контекста потока: `flow.hvac_lock`. Она может принимать три значения: `"HEATING"`, `"COOLING"` или `null` (система свободна).

Пошаговая настройка потока

  • Создайте два узла `mqtt in`:
  • * Первый для топика `hvac/heating/set`.

    * Второй для топика `hvac/cooling/set`.

  • Создайте узел `Function` для управления отоплением. Назовите его "Heating Interlock Logic". Подключите к нему выход первого узла `mqtt in`.
  • Создайте аналогичный узел `Function` для охлаждения. Назовите его "Cooling Interlock Logic" и подключите ко второму `mqtt in`.
  • Напишите код для узла "Heating Interlock Logic". Этот код будет выполнять основную проверку.
  • // Получаем команду из входящего сообщения (предполагаем "ON" или "OFF")
    

    const command = msg.payload;

    // Получаем текущее состояние блокировки из контекста потока

    const hvacLock = flow.get('hvac_lock') || null;

    // Обрабатываем команду на включение

    if (command === "ON") {

    // Проверяем, не занята ли система охлаждением

    if (hvacLock === "COOLING") {

    // Система занята, блокируем действие

    node.warn("Блокировка: попытка включить отопление при работающем охлаждении.");

    node.status({fill:"red", shape:"dot", text:"BLOCKED: cooling active"});

    return null; // Прерываем поток, команда не будет выполнена

    }

    // Система свободна, можно включать

    node.status({fill:"green", shape:"dot", text:"Turning ON"});

    // Устанавливаем блокировку: теперь система занята отоплением

    flow.set('hvac_lock', 'HEATING');

    // Формируем сообщение для отправки на реле

    msg.payload = true; // Команда на включение реле

    return msg;

    }

    // Обрабатываем команду на выключение

    if (command === "OFF") {

    node.status({fill:"grey", shape:"dot", text:"Turning OFF"});

    // Снимаем блокировку, если она была установлена этим устройством

    if (hvacLock === "HEATING") {

    flow.set('hvac_lock', null);

    }

    // Формируем команду на выключение реле

    msg.payload = false;

    return msg;

    }

    // Если пришла неизвестная команда

    node.error("Неизвестная команда: " + command, msg);

    return null;

  • Скопируйте и адаптируйте код для узла "Cooling Interlock Logic". Логика будет зеркальной:
  • // ... (получаем command)
    

    const hvacLock = flow.get('hvac_lock') || null;

    if (command === "ON") {

    if (hvacLock === "HEATING") {

    node.warn("Блокировка: попытка включить охлаждение при работающем отоплении.");

    node.status({fill:"red", shape:"dot", text:"BLOCKED: heating active"});

    return null;

    }

    node.status({fill:"blue", shape:"dot", text:"Turning ON"});

    flow.set('hvac_lock', 'COOLING');

    msg.payload = true;

    return msg;

    }

    if (command === "OFF") {

    node.status({fill:"grey", shape:"dot", text:"Turning OFF"});

    if (hvacLock === "COOLING") {

    flow.set('hvac_lock', null);

    }

    msg.payload = false;

    return msg;

    }

    //...

  • Подключите выходы узлов `Function` к соответствующим узлам управления реле (например, `rpi gpio out` или `modbus-write`).
  • Теперь, если вы отправите команду на включение отопления, `flow.hvac_lock` станет `"HEATING"`. Любая последующая команда на включение охлаждения будет заблокирована до тех пор, пока отопление не будет выключено и `flow.hvac_lock` не вернется в `null`.

    ---

    Пример: Блокировка управления реверсивным двигателем

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

    Задача:

    Здесь мы используем простой флаг `flow.motor_active` и узел `trigger`.

    Логика потока

  • Входные команды: Две команды, например, `msg.topic = "gate/control"` с `msg.payload = "OPEN"` и `msg.payload = "CLOSE"`.
  • Узел `Function` "Motor Interlock":
  • * При получении любой команды (`OPEN` или `CLOSE`), он первым делом проверяет флаг `flow.get('motor_active')`.

    * Если `true`, это означает, что мотор уже в движении. Новая команда игнорируется, в лог пишется предупреждение. `return null`.

    * Если `false`, мотор свободен. Узел устанавливает флаг `flow.set('motor_active', true)` и пропускает сообщение дальше. В `msg` можно добавить информацию о направлении, например, `msg.direction = "OPEN"`.

  • Узел `Switch`:
  • * Направляет сообщение на нужное реле в зависимости от `msg.payload` (`"OPEN"` или `"CLOSE"`).

  • Узлы управления реле:
  • * Включают соответствующее реле (`RL-UP` или `RL-DOWN`).

  • Узел `trigger` для автоматического сброса:
  • * Выход узла `Function` ("Motor Interlock") также подключается к узлу `trigger`.

    * Настройка `trigger`:

    * `Send`: `Ничего` (Nothing).

    * `then wait for`: `20 seconds` (установите время, достаточное для полного открытия/закрытия ворот).

    * `and then send`: `{"reset": true}` (специальное сообщение для сброса флага).

    * Этот узел будет молчать 20 секунд после начала движения, а затем отправит сообщение о необходимости сбросить блокировку.

  • Узел `Function` "Reset Lock":
  • * Принимает сообщение `{"reset": true}` от узла `trigger`.

    * Выполняет единственную команду: `flow.set('motor_active', false);`.

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

    Пример Flow (JSON для импорта)

    [
    

    {

    "id": "f8d3c7a1.e8a6b8",

    "type": "function",

    "z": "YOUR_FLOW_ID",

    "name": "Motor Interlock",

    "func": "const motorActive = flow.get('motor_active') || false;\n\nif (motorActive) {\n node.warn(\"Блокировка: мотор уже в движении.\");\n node.status({fill:\"red\", shape:\"dot\", text:\"BLOCKED\"});\n return null;\n}\n\nflow.set('motor_active', true);\nnode.status({fill:\"green\", shape:\"dot\", text:\"Движение...\"});\n\n// Добавляем информацию о направлении для следующего узла\nmsg.direction = msg.payload;\n\nreturn msg;",

    "outputs": 1,

    "noerr": 0,

    "x": 370,

    "y": 240,

    "wires": [

    [

    "c9e2b1d3.1d2f",

    "a1b4c5d6.7e8f9"

    ]

    ]

    },

    {

    "id": "c9e2b1d3.1d2f",

    "type": "switch",

    "z": "YOUR_FLOW_ID",

    "name": "Route Direction",

    "property": "direction",

    "propertyType": "msg",

    "rules": [

    {

    "t": "eq",

    "v": "OPEN",

    "vt": "str"

    },

    {

    "t": "eq",

    "v": "CLOSE",

    "vt": "str"

    }

    ],

    "checkall": "true",

    "repair": false,

    "outputs": 2,

    "x": 580,

    "y": 240,

    "wires": [

    [

    "deadbeef.1"

    ],

    [

    "deadbeef.2"

    ]

    ]

    },

    {

    "id": "a1b4c5d6.7e8f9",

    "type": "trigger",

    "z": "YOUR_FLOW_ID",

    "name": "Таймер движения (20с)",

    "op1": "",

    "op2": "{\"reset\": true}",

    "op1type": "nul",

    "op2type": "json",

    "duration": "20",

    "extend": false,

    "units": "s",

    "reset": "",

    "bytopic": "all",

    "topic": "topic",

    "outputs": 1,

    "x": 390,

    "y": 320,

    "wires": [

    [

    "b2c3d4e5.f6g7h8"

    ]

    ]

    },

    {

    "id": "b2c3d4e5.f6g7h8",

    "type": "function",

    "z": "YOUR_FLOW_ID",

    "name": "Reset Lock",

    "func": "// Снимаем флаг блокировки\nflow.set('motor_active', false);\n\n// Дополнительная безопасность: выключаем оба реле\n// В реальном потоке здесь будут команды на выключение\nnode.warn(\"Таймаут движения. Блокировка снята, реле выключены.\");\nnode.status({fill:\"grey\", shape:\"dot\", text:\"Свободен\"});\n\n// Этот узел не производит дальнейших сообщений\nreturn null;",

    "outputs": 1,

    "noerr": 0,

    "x": 600,

    "y": 320,

    "wires": [[]]

    },

    { "id": "deadbeef.1", "type": "debug", "name": "КОМАНДА НА РЕЛЕ 'ОТКРЫТЬ'", "active": true, "x": 800, "y": 220, "wires": [] },

    { "id": "deadbeef.2", "type": "debug", "name": "КОМАНДА НА РЕЛЕ 'ЗАКРЫТЬ'", "active": true, "x": 800, "y": 260, "wires": [] },

    { "id": "injector.open", "type": "inject", "name": "Открыть", "topic": "gate/control", "payload": "OPEN", "payloadType": "str", "x": 160, "y": 220, "wires": [["f8d3c7a1.e8a6b8"]] },

    { "id": "injector.close", "type": "inject", "name": "Закрыть", "topic": "gate/control", "payload": "CLOSE", "payloadType": "str", "x": 160, "y": 260, "wires": [["f8d3c7a1.e8a6b8"]] }

    ]

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

    ---

    Сохранение состояния при перезагрузке: Persistent Context

    Мы установили, что переменные `flow` и `global` контекста по умолчанию хранятся в оперативной памяти (RAM) контроллера HI. Это быстро и эффективно, но имеет один фатальный недостаток: при перезагрузке контроллера или сервиса Node-RED все данные из контекста теряются.

    Последствия могут быть катастрофическими для логики блокировок:

    Для предотвращения таких ситуаций контроллер HI и Node-RED поддерживают механизм персистентного хранения контекста (persistent context). Он позволяет сохранять данные контекста в файловой системе на встроенном накопителе контроллера.

    > ⚠️ Внимание: Частая запись на flash-накопитель контроллера (EEPROM или SD-карта) может сократить срок его службы. Не сохраняйте в персистентный контекст быстро меняющиеся данные (например, показания датчиков каждую секунду). Используйте его только для хранения критически важных состояний, которые меняются редко, — например, флаги блокировок, режимы работы, уставки.

    Настройка `contextStorage`

    Конфигурация персистентного хранилища выполняется в главном конфигурационном файле Node-RED — `settings.js`. На контроллере HI он обычно находится в директории `/root/.node-red/`.

  • Подключитесь к контроллеру по SSH.
  • Откройте файл `settings.js` в текстовом редакторе, например, `nano`:
  •     nano /root/.node-red/settings.js

  • Найдите секцию `contextStorage`. По умолчанию она может выглядеть так:
  •     contextStorage: {

    default: { module: "memory" }

    },

  • Измените ее, чтобы добавить второе хранилище, основанное на файловой системе, и сделать его хранилищем по умолчанию для `flow` и `global` контекстов.
  • Пример конфигурации `settings.js`:
        contextStorage: {
    

    // Определяем стандартное хранилище в памяти.

    // Мы можем использовать его для временных, некритичных данных.

    memoryOnly: { module: "memory" },

    // Определяем персистентное файловое хранилище.

    // Данные будут сохраняться в поддиректории 'context' в папке пользователя Node-RED.

    fileSystem: { module: "localfilesystem" },

    // Указываем, какое хранилище использовать по умолчанию.

    // Теперь все вызовы flow.get и flow.set будут работать с файловым хранилищем.

    default: { module: "localfilesystem" }

    },

  • Сохраните файл (`Ctrl+O` в `nano`, затем `Enter`) и выйдите (`Ctrl+X`).
  • Перезапустите сервис Node-RED, чтобы изменения вступили в силу.
  •     node-red-restart

    После этого все данные, которые вы записываете в `flow.context` и `global.context`, будут автоматически сохраняться в файлах. При перезагрузке контроллера Node-RED прочитает эти файлы и восстановит последнее известное состояние, обеспечивая надежность вашей логики блокировок.

    ---

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

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

    Резюме:

    Лучшие практики по работе с контекстом

  • Осмысленные имена: Давайте переменным понятные имена, отражающие их суть. Вместо `flow.set('lock', 1)` используйте `flow.set('interlock_heating_active', true)`. Это кардинально упрощает отладку.
  • Инициализация при старте: Для переменных, которые должны иметь начальное значение, используйте узел `Inject` (с опцией `Inject once after XX seconds`) в паре с узлом `Change` или `Function` для их установки при запуске потока. Это предотвратит ошибки `undefined` при первой проверке.
  • Единообразие типов данных: Определите и придерживайтесь одного типа данных для переменной. Если флаг — то всегда `boolean` (`true`/`false`), а не смесь из `1`, `0`, `"ON"`, `"OFF"`.
  • Своевременный сброс: Убедитесь, что ваша логика корректно сбрасывает флаги и состояния. Зависший флаг блокировки может остановить работу системы. Используйте команды `OFF`, узлы `trigger` или концевые выключатели для надежного сброса.
  • Документация: В сложных потоках используйте узлы `Comment` для описания того, какие переменные контекста используются, что они означают и какие значения могут принимать.
  • Освоив управление состоянием, вы получаете возможность создавать по-настоящему умные, безопасные и отказоустойчивые системы автоматизации на платформе контроллера HI.