Хранение состояния: переменные потока (flow.context)
Введение: Зачем хранить состояние?
В предыдущих уроках мы подробно рассмотрели, что такое взаимная блокировка (Interlock) и почему она критически важна для безопасной эксплуатации исполнительных устройств. Мы выяснили, что одновременная активация определенных механизмов, таких как контакторы систем отопления и охлаждения или реле реверсивного двигателя, может привести к аварийным ситуациям, короткому замыканию и выходу оборудования из строя.
> 🔗 Связанный материал: Этот урок является прямым продолжением урока `COURSE-05-M03-L01`, где мы рассмотрели саму концепцию и важность взаимных блокировок. Убедитесь, что вы понимаете базовые сценарии, прежде чем переходить к практической реализации.
До сих пор наша логика в Node-RED была преимущественно "stateless" (без состояния). Это означает, что каждый узел обрабатывал входящее сообщение (`msg`) независимо от предыдущих сообщений. Например, при поступлении команды `{"command": "ON"}` на включение отопления, поток просто отправлял сигнал на соответствующее реле. Он не имел никакой информации о том, что происходит в остальной системе.
Такой подход имеет серьезные ограничения:
- Невозможность реализации сложных блокировок: Как поток, управляющий отоплением, может узнать, что в данный момент уже работает система охлаждения, если он не "помнит" ее состояние?
- Слепая отправка команд: Без информации о текущем статусе устройства, мы рискуем отправить дублирующую команду (например, включить уже включенное реле) или, что хуже, конфликтующую команду.
- Гонка состояний (Race Condition): Если две команды (например, "Включить отопление" и "Включить охлаждение") приходят почти одновременно, stateless-система может успеть обработать и выполнить обе, не успев предотвратить конфликт.
Для решения этих проблем нам необходимо внедрить концепцию "состояния" (state). Состояние — это, по сути, память нашей системы автоматизации. Это информация о предыдущих событиях, командах и текущем статусе оборудования, которая хранится внутри контроллера HI и доступна для анализа в потоках Node-RED.
Существует несколько способов хранения состояния, но основным и наиболее гибким инструментом на уровне потока являются переменные контекста (context variables). Они позволяют нам создавать "флаги" и сохранять значения, которые один узел потока может записать, а другой — прочитать в любой момент времени. Именно переменные контекста превратят наши простые потоки в интеллектуальные системы, способные принимать решения на основе полной картины происходящего.
---
Переменные контекста в Node-RED: flow, global, node
Node-RED предоставляет три уровня (или области видимости) для хранения переменных контекста. Правильный выбор уровня определяет, насколько доступными и долговечными будут ваши данные, что напрямую влияет на надежность и архитектуру всей системы.
> 💡 Подсказка: Используйте `flow.context` для логики, специфичной для одного потока (например, управление одной группой устройств на одной вкладке). Прибегайте к `global.context` только для данных, которые должны быть доступны абсолютно всем потокам в проекте (например, общий режим 'Отпуск' или глобальные уставки безопасности).
Рассмотрим каждый уровень подробно:
1. Контекст узла (node.context)
- Область видимости: Переменная доступна только внутри того узла `Function`, в котором она была создана. Другие узлы, даже на той же вкладке, не могут ее прочитать.
- Жизненный цикл: Данные сохраняются между различными вызовами одного и того же узла, но сбрасываются при перезапуске или развертывании (deploy) потока.
- Применение: Крайне редко используется. В основном для внутренних счетчиков или кеширования данных в рамках одного сложного узла `Function`. Для задач блокировки он не подходит, так как нам нужен обмен данными между разными узлами.
2. Контекст потока (flow.context)
- Область видимости: Это наш основной инструмент. Переменная, сохраненная в `flow.context`, доступна всем узлам на одной вкладке (flow) в редакторе Node-RED.
- Жизненный цикл: Данные живут до тех пор, пока Node-RED запущен. Они сохраняются между развертываниями, но по умолчанию сбрасываются при перезагрузке контроллера HI. Позже мы рассмотрим, как это исправить.
- Применение: Идеально для реализации взаимных блокировок, хранения состояний устройств (включено/выключено, открыто/закрыто), промежуточных вычислений и управления логикой в рамках одной функциональной группы (например, "Климат гостиной", "Управление воротами").
3. Глобальный контекст (global.context)
- Область видимости: Переменная доступна абсолютно всем узлам во всем проекте Node-RED, на всех вкладках.
- Жизненный цикл: Аналогичен `flow.context` — живет до перезагрузки Node-RED.
- Применение: Для хранения данных, имеющих глобальное значение для всего объекта. Например:
* Статус глобальных систем (`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
Давайте реализуем классический сценарий взаимной блокировки для системы климат-контроля. Наша задача — не допустить одновременной работы контактора отопления и контактора системы кондиционирования.
Сценарий:- У нас есть две команды, приходящие по MQTT в топики `hvac/heating/set` и `hvac/cooling/set`.
- Сообщения могут быть `ON` или `OFF`.
- Реле отопления подключено к выходу `RL-01`, реле охлаждения — к `RL-02`.
- Правило: Перед включением отопления мы должны убедиться, что охлаждение выключено. И наоборот.
Мы будем использовать одну переменную контекста потока: `flow.hvac_lock`. Она может принимать три значения: `"HEATING"`, `"COOLING"` или `null` (система свободна).
Пошаговая настройка потока
* Первый для топика `hvac/heating/set`.
* Второй для топика `hvac/cooling/set`.
// Получаем команду из входящего сообщения (предполагаем "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;
// ... (получаем 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;
}
//...
Теперь, если вы отправите команду на включение отопления, `flow.hvac_lock` станет `"HEATING"`. Любая последующая команда на включение охлаждения будет заблокирована до тех пор, пока отопление не будет выключено и `flow.hvac_lock` не вернется в `null`.
---
Пример: Блокировка управления реверсивным двигателем
Рассмотрим еще один критически важный пример — управление приводом ворот или штор. Двигатель имеет два управляющих реле: одно для движения "Вверх" (или "Открыть"), другое для движения "Вниз" (или "Закрыть"). Одновременное включение этих реле вызовет короткое замыкание в цепи питания двигателя.
Задача:- Создать блокировку, которая не позволит отправить команду "Вниз", пока выполняется команда "Вверх", и наоборот.
- Блокировка должна действовать в течение всего времени движения ворот, а затем автоматически сниматься.
Здесь мы используем простой флаг `flow.motor_active` и узел `trigger`.
Логика потока
* При получении любой команды (`OPEN` или `CLOSE`), он первым делом проверяет флаг `flow.get('motor_active')`.
* Если `true`, это означает, что мотор уже в движении. Новая команда игнорируется, в лог пишется предупреждение. `return null`.
* Если `false`, мотор свободен. Узел устанавливает флаг `flow.set('motor_active', true)` и пропускает сообщение дальше. В `msg` можно добавить информацию о направлении, например, `msg.direction = "OPEN"`.
* Направляет сообщение на нужное реле в зависимости от `msg.payload` (`"OPEN"` или `"CLOSE"`).
* Включают соответствующее реле (`RL-UP` или `RL-DOWN`).
* Выход узла `Function` ("Motor Interlock") также подключается к узлу `trigger`.
* Настройка `trigger`:
* `Send`: `Ничего` (Nothing).
* `then wait for`: `20 seconds` (установите время, достаточное для полного открытия/закрытия ворот).
* `and then send`: `{"reset": true}` (специальное сообщение для сброса флага).
* Этот узел будет молчать 20 секунд после начала движения, а затем отправит сообщение о необходимости сбросить блокировку.
* Принимает сообщение `{"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 все данные из контекста теряются.
Последствия могут быть катастрофическими для логики блокировок:
- Система после сбоя питания "забывает", что контактор отопления был включен.
- Приходит команда на включение охлаждения.
- Так как `flow.hvac_lock` теперь `null`, блокировка не срабатывает.
- Оба контактора включаются одновременно, что приводит к аварии.
Для предотвращения таких ситуаций контроллер HI и Node-RED поддерживают механизм персистентного хранения контекста (persistent context). Он позволяет сохранять данные контекста в файловой системе на встроенном накопителе контроллера.
> ⚠️ Внимание: Частая запись на flash-накопитель контроллера (EEPROM или SD-карта) может сократить срок его службы. Не сохраняйте в персистентный контекст быстро меняющиеся данные (например, показания датчиков каждую секунду). Используйте его только для хранения критически важных состояний, которые меняются редко, — например, флаги блокировок, режимы работы, уставки.
Настройка `contextStorage`
Конфигурация персистентного хранилища выполняется в главном конфигурационном файле Node-RED — `settings.js`. На контроллере HI он обычно находится в директории `/root/.node-red/`.
nano /root/.node-red/settings.js
contextStorage: {
default: { module: "memory" }
},
contextStorage: {
// Определяем стандартное хранилище в памяти.
// Мы можем использовать его для временных, некритичных данных.
memoryOnly: { module: "memory" },
// Определяем персистентное файловое хранилище.
// Данные будут сохраняться в поддиректории 'context' в папке пользователя Node-RED.
fileSystem: { module: "localfilesystem" },
// Указываем, какое хранилище использовать по умолчанию.
// Теперь все вызовы flow.get и flow.set будут работать с файловым хранилищем.
default: { module: "localfilesystem" }
},
node-red-restart
После этого все данные, которые вы записываете в `flow.context` и `global.context`, будут автоматически сохраняться в файлах. При перезагрузке контроллера Node-RED прочитает эти файлы и восстановит последнее известное состояние, обеспечивая надежность вашей логики блокировок.
---
Итоги и лучшие практики
В этом уроке мы сделали ключевой шаг от простых stateless-потоков к созданию надежной логики с памятью о прошлых событиях.
Резюме:- Переменные контекста являются основным инструментом для управления состоянием в Node-RED.
- `flow.context` — ваш главный помощник для реализации логики в рамках одной функциональной группы (например, на одной вкладке), включая взаимные блокировки.
- `global.context` следует использовать с осторожностью только для данных, действительно общих для всего проекта.
- Реализация надежной блокировки всегда следует циклу: проверить состояние -> принять решение -> выполнить действие -> обновить состояние.
- По умолчанию контекст хранится в оперативной памяти и теряется при перезагрузке. Для критических данных необходимо настроить персистентное хранилище (`contextStorage`) в `settings.js`.
Лучшие практики по работе с контекстом
Освоив управление состоянием, вы получаете возможность создавать по-настоящему умные, безопасные и отказоустойчивые системы автоматизации на платформе контроллера HI.