Паттерн 'State Machine': хранение и управление состоянием
Введение в концепцию состояния (State) в автоматизации
В любой системе автоматизации, от простого умного дома до сложного промышленного объекта, ключевую роль играет понятие состояния. Состояние — это набор данных, описывающих текущий статус устройства или системы в конкретный момент времени. Без понимания и правильного управления состоянием невозможно создать по-настоящему интеллектуальную и предсказуемую систему.
Рассмотрим простые примеры:
- Освещение: Лампочка может быть в состоянии «включена» или «выключена». Диммируемая лампа имеет более сложное состояние — помимо «включена/выключена», у нее есть «яркость» (например, 75%) и «цветовая температура» (например, 4000K).
- Климат: Кондиционер может находиться в состояниях «выключен», «охлаждение», «обогрев», «вентиляция». Каждое из этих состояний также характеризуется дополнительными параметрами: целевая температура (уставка), скорость вентилятора, направление потока воздуха.
- Безопасность: Система охраны может быть в состояниях «снята с охраны», «поставлена на охрану», «тревога».
В контексте Node-RED все потоки можно разделить на два типа:
Основная проблема, с которой сталкиваются инженеры при работе с Node-RED, заключается в том, что по своей природе платформа является stateless. Каждый узел `Function` по умолчанию не имеет памяти. Когда в него приходит новое сообщение, он ничего не знает о том, какие сообщения приходили до этого. Вся информация из предыдущего `msg` теряется, если ее специально не сохранить.
Эта особенность требует от разработчика явного внедрения механизмов для хранения и управления состоянием. Без этого невозможно реализовать даже базовые сценарии, такие как переключение режимов, триггеры, включение/выключение по одной команде и любую другую сложную логику, которая делает автоматизацию удобной и эффективной. Игнорирование управления состоянием приводит к созданию хрупких, непредсказуемых и сложных в отладке систем.
---
Способы хранения состояния: Context в Node-RED
пособы хранения состояния: Context в Node-RED
Для решения проблемы хранения состояния в Node-RED встроен специальный механизм, который называется контекст (Context). Контекст — это, по сути, область памяти, доступная для узлов, где можно хранить переменные, которые сохраняют свои значения между различными выполнениями потока.
Существует три области видимости (scope) контекста:
| Область видимости | Доступ | Синтаксис в `Function` | Жизненный цикл | Типичное применение |
| ----------------- | ------ | ---------------------- | ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| Node | Узел | `context.get('key')` | Сохраняется до перезапуска потока или полного развертывания | Внутренние счетчики, флаги, временные данные, необходимые только одному конкретному узлу. |
| Flow | Вкладка| `flow.get('key')` | Сохраняется до перезапуска потока или полного развертывания | Основной инструмент для State Machine. Хранение состояния устройств или систем в рамках одной логической группы (например, состояние климата в гостиной). |
| Global | Везде | `global.get('key')` | Сохраняется до перезапуска потока или полного развертывания | Глобальные переменные: режим «День/Ночь», статус «Дома/В отъезде», общие настройки системы. |
Синтаксис и использование
Для работы с контекстом внутри узла `Function` используются два основных метода: `get` для чтения и `set` для записи.
// Чтение значения из контекста вкладки (flow)
// Если значение 'lightState' еще не установлено, get() вернет undefined.
// Мы используем оператор || для установки значения по умолчанию 'OFF'.
let currentState = flow.get('lightState') || 'OFF';
node.warn("Текущее состояние светильника: " + currentState);
// Логика... предположим, мы решили изменить состояние
let newState = 'ON';
// Запись нового значения в контекст вкладки (flow)
flow.set('lightState', newState);
node.warn("Новое состояние светильника: " + newState);
// Эти же методы работают для node и global контекста:
// node.get('counter'); node.set('counter', 1);
// global.get('dayNightMode'); global.set('dayNightMode', 'DAY');
return msg;
Persistence (постоянное хранение)
По умолчанию все данные в контексте хранятся в оперативной памяти и теряются при перезагрузке контроллера или перезапуске Node-RED. Это неприемлемо для критически важных состояний (например, режим работы котельной или состояние системы безопасности).
Чтобы решить эту проблему, контроллер HI позволяет настроить постоянное хранение (persistence) для `flow` и `global` контекстов. Данные будут сохраняться в файловой системе в директории `/data`, что гарантирует их восстановление после сбоя питания. Эта настройка выполняется в конфигурационном файле Node-RED (`settings.js`). На платформе HI это уже предварительно сконфигурировано для обеспечения надежности: персистентные данные записываются на физический накопитель в раздел `/data`.
> ⚠️ Внимание: Использование файловой системы (путь `/data`) для хранения контекста на контроллерах с SD-картой может привести к ее преждевременному износу из-за большого количества циклов записи. Используйте эту опцию с осторожностью и только для критически важных данных, которые меняются не слишком часто. Не следует сохранять в персистентный контекст быстро меняющиеся значения, например, показания датчиков каждую секунду.
Анализ производительности:- Хранение в памяти (по умолчанию):
* Минусы: Данные теряются при перезагрузке.
- Хранение в файловой системе (persistence):
* Минусы: Медленнее, чем доступ к оперативной памяти. Каждый вызов `flow.set()` или `global.set()` вызывает операцию записи на диск, что создает дополнительную нагрузку и увеличивает износ носителя.
Реализация паттерна 'Конечный автомат' (State Machine)
Конечный автомат (State Machine) — это мощный паттерн проектирования, который позволяет управлять сложным поведением объекта через набор состояний и переходов между ними. Он идеально подходит для реализации stateful-логики в Node-RED, делая ее централизованной, предсказуемой и легко отлаживаемой.Рассмотрим пошаговое создание простого конечного автомата на примере управления диммируемой лампой, которая циклически переключается между тремя состояниями: `OFF` -> `ON` (100% яркости) -> `DIMMED` (50% яркости) -> `OFF`. Управление осуществляется одной кнопкой.
Цель: Каждое новое сообщение (сигнал от кнопки) должно переводить автомат в следующее состояние. Flow Diagram:[Inject: "toggle"] -> [Function: "State Machine Logic"] -> [Debug: "Actuator Command"]
Шаг 1: Инициализация потока
Создайте на новой вкладке узел `Inject`, узел `Function` и узел `Debug`. Соедините их последовательно. Настройте узел `Inject` так, чтобы он отправлял пустой `msg.payload`, это будет имитировать нажатие кнопки.
Шаг 2: Написание логики конечного автомата в узле `Function`Этот узел — сердце нашего автомата. Вся логика будет инкапсулирована здесь.
// --- State Machine for a Dimmable Lamp ---
// 1. Получение текущего состояния из контекста вкладки (flow).
// Если 'lampState' еще не существует (первый запуск), устанавливаем 'OFF' по умолчанию.
let currentState = flow.get('lampState') || 'OFF';
let newState;
let commandPayload;
// 2. Логика переходов (сердце конечного автомата).
// Определяем, каким будет следующее состояние в зависимости от текущего.
switch (currentState) {
case 'OFF':
// Из OFF переходим в ON
newState = 'ON';
commandPayload = { state: 'ON', brightness: 100 };
break;
case 'ON':
// Из ON переходим в DIMMED
newState = 'DIMMED';
commandPayload = { state: 'ON', brightness: 50 };
break;
case 'DIMMED':
// Из DIMMED переходим в OFF
newState = 'OFF';
commandPayload = { state: 'OFF', brightness: 0 };
break;
default:
// Аварийный случай: если состояние некорректно, сбрасываем в OFF.
node.warn(`Unknown state detected: ${currentState}. Resetting to OFF.`);
newState = 'OFF';
commandPayload = { state: 'OFF', brightness: 0 };
break;
}
// 3. Сохранение нового состояния в контексте.
// Это критически важный шаг. Без него автомат не будет "помнить" свое новое состояние.
flow.set('lampState', newState);
// 4. Формирование управляющего сообщения для исполнительного устройства.
// Мы используем стандартный контракт сообщения, как обсуждалось ранее.
msg.payload = {
value: commandPayload,
source: 'state-machine-lamp-01',
ts: Date.now()
};
// 5. Визуализация статуса для удобства отладки.
node.status({
fill: (newState === 'OFF') ? "red" : "green",
shape: "dot",
text: `State: ${newState}, Brightness: ${commandPayload.brightness}%`
});
// Отправляем сформированное сообщение дальше по потоку (на реле, DALI-драйвер и т.д.)
return msg;
Шаг 3: Тестирование и отладка
Нажимая на узел `Inject`, наблюдайте за выводом в панели `Debug` и за статусом под узлом `Function`.
- Первое нажатие: Статус будет `State: ON, Brightness: 100%`. В `msg.payload` придет `{ "state": "ON", "brightness": 100 }`.
- Второе нажатие: Статус `State: DIMMED, Brightness: 50%`. В `msg.payload` — `{ "state": "ON", "brightness": 50 }`.
- Третье нажатие: Статус `State: OFF, Brightness: 0%`. В `msg.payload` — `{ "state": "OFF", "brightness": 0 }`.
- Четвертое нажатие: Цикл начнется заново с `ON`.
Этот простой пример демонстрирует всю мощь паттерна: логика централизована, предсказуема и не зависит от внешних узлов. Чтобы изменить поведение, достаточно отредактировать код в одном месте.
---
Пример: Управление климатом с помощью конечного автомата
Теперь рассмотрим более сложный и реалистичный пример — управление фанкойлом в офисном помещении. Автомат будет управлять режимами работы (`heating`, `cooling`, `fan_only`, `off`) на основе данных с датчика температуры и команд от пользователя.
Входные данные:> 💡 Подсказка: Для сложных устройств удобно хранить состояние в виде объекта в `flow.context`. Например: `flow.set('climate_state', { mode: 'heating', setpoint: 22, fan_speed: 'auto' })`. Это упрощает чтение и модификацию состояния в одном узле `Function`.
Логика конечного автомата:- Состояния: `off`, `heating`, `cooling`, `fan_only`.
- Переменные состояния: `mode` (режим), `setpoint` (уставка), `current_temp` (текущая температура), `fan_speed`.
- Правила переходов:
* Если `mode` = `cooling` и `current_temp` > `setpoint` + 0.5°C, обратно включить `cooling`.
* Аналогичная логика для `heating` (гистерезис в 1°C).
* Пользователь может в любой момент принудительно изменить `mode` или `setpoint`.
Реализация в узле `Function`:// --- State Machine for Climate Control ---
// 1. Получаем объект состояния из контекста. Инициализируем при первом запуске.
let state = flow.get('climate_state') || {
mode: 'off',
setpoint: 22.0,
current_temp: null,
fan_speed: 'auto',
active_state: 'idle' // `idle`, `heating`, `cooling`
};
// 2. Обновляем объект состояния на основе входящего сообщения.
if (msg.topic.includes('temperature')) {
state.current_temp = parseFloat(msg.payload.value);
} else if (msg.topic.includes('setpoint')) {
state.setpoint = parseFloat(msg.payload);
} else if (msg.topic.includes('mode')) {
state.mode = msg.payload;
}
// 3. Основная логика принятия решений.
let new_active_state = state.active_state;
const hysteresis = 0.5; // Гистерезис в градусах Цельсия
// Проверяем, что у нас есть актуальные данные о температуре
if (state.current_temp !== null) {
switch (state.mode) {
case 'cooling':
if (state.current_temp > state.setpoint + hysteresis) {
new_active_state = 'cooling'; // Нужно охлаждать
} else if (state.current_temp < state.setpoint - hysteresis) {
new_active_state = 'idle'; // Температура достигнута, выключаем компрессор
}
break;
case 'heating':
if (state.current_temp < state.setpoint - hysteresis) {
new_active_state = 'heating'; // Нужно греть
} else if (state.current_temp > state.setpoint + hysteresis) {
new_active_state = 'idle'; // Температура достигнута
}
break;
case 'fan_only':
new_active_state = 'fan_only';
break;
case 'off':
new_active_state = 'off';
break;
}
}
// Если режим выключен, принудительно выключаем всё
if (state.mode === 'off') {
new_active_state = 'off';
}
// 4. Генерируем управляющую команду, только если состояние изменилось.
if (new_active_state !== state.active_state) {
state.active_state = new_active_state;
// Формируем команду для Modbus-узла, управляющего фанкойлом
msg.payload = {
value: {
state: state.active_state, // 'heating', 'cooling', 'off', 'fan_only'
fan: (state.active_state !== 'off') ? state.fan_speed : 'off'
},
source: 'climate-fsm-office1',
ts: Date.now()
};
// Сохраняем обновленный объект состояния
flow.set('climate_state', state);
// Обновляем статус
node.status({ fill: "blue", shape: "dot", text: `Mode: ${state.mode} | Active: ${state.active_state} | T: ${state.current_temp}°C` });
// Отправляем команду
return msg;
} else {
// Состояние не изменилось, ничего не отправляем
flow.set('climate_state', state); // Все равно сохраняем обновленные данные (например, новую температуру)
node.status({ fill: "grey", shape: "dot", text: `Mode: ${state.mode} | Active: ${state.active_state} | T: ${state.current_temp}°C` });
return null; // Останавливаем поток
}
Этот пример показывает, как конечный автомат элегантно справляется со сложной логикой, обрабатывая несколько источников данных и управляя состоянием системы централизованно.
---
Интеграция State Machine с другими паттернами
Паттерн «Конечный автомат» редко используется в вакууме. Его настоящая сила раскрывается в комбинации с другими паттернами проектирования, что позволяет создавать по-настоящему надежные и отказоустойчивые потоки.
> 🔗 Связанный материал: Подробное описание этих паттернов вы найдете в уроках COURSE-06-M05-L01 ('Gate') и COURSE-06-M05-L03 ('Debounce/Rate Limit').
Комбинация с 'Debounce'
- Проблема: Физические кнопки и некоторые датчики страдают от «дребезга контактов» (bouncing) — при одном нажатии они генерируют серию быстрых импульсов. Если подать эти импульсы напрямую на наш конечный автомат для лампы, он может быстро пролистать все свои состояния (`OFF` -> `ON` -> `DIMMED` -> `OFF`) за доли секунды.
`[Button Input]` -> `[Debounce: 250ms]` -> `[Function: State Machine]` -> `[Actuator]`
Этот простой прием защищает конечный автомат от «шумных» входных данных, обеспечивая корректный одиночный переход состояния на каждое реальное действие пользователя.
Комбинация с 'Gate'
- Проблема: Представим, что в системе есть глобальный режим «Техобслуживание» или «В отъезде». Когда этот режим активен, мы не хотим, чтобы локальные автоматы (например, управление светом по датчику движения) реагировали на внешние события. Как временно «заморозить» конечный автомат, не переписывая его логику?
[Global State Checker] -> [Control Gate]
|
[Motion Sensor] -> [Gate: blocks/passes] -----+-----> [Function: Light State Machine] -> [Light Actuator]
В данном случае узел `Gate` проверяет значение `global.get('systemMode')`. Если `systemMode` равно `'home'`, ворота открыты. Если `'away'` или `'maintenance'`, ворота закрываются, и сообщения от датчика движения не доходят до конечного автомата, соответственно, свет не включается. Это позволяет создавать иерархию управления, где глобальные состояния могут влиять на локальные подсистемы.
---
Итоги и лучшие практики
Использование паттерна «Конечный автомат» является одним из ключевых навыков для создания профессиональных, надежных и легко поддерживаемых систем автоматизации на Node-RED.
Ключевые преимущества:- Централизованная логика: Все правила переходов между состояниями находятся в одном месте (обычно в одном узле `Function`), а не разбросаны по всему потоку.
- Предсказуемость: Зная текущее состояние и входное событие, вы всегда можете точно предсказать, каким будет следующее состояние и какое действие выполнит система.
- Легкая отладка: Для диагностики проблемы достаточно посмотреть текущее сохраненное состояние в контексте и последнее пришедшее сообщение.
- Масштабируемость: Добавление нового состояния или нового правила перехода требует изменения только в одном, строго определенном месте.
Рекомендации по выбору области видимости контекста:
- `node.context`: Используйте для временных данных, которые нужны только одному узлу и не являются частью общего состояния системы (например, счетчик попыток подключения).
- `flow.context`: Ваш основной инструмент. Идеален для хранения состояния одного устройства или одной логической подсистемы (климат в комнате, освещение в гостиной). Привязка к вкладке (`flow`) позволяет логически изолировать системы друг от друга.
- `global.context`: Используйте с осторожностью. Только для данных, которые действительно являются глобальными для всего проекта (режим «день/ночь», статус «охрана включена/выключена», переменные, влияющие на несколько подсистем).
Важность инициализации
Всегда предусматривайте инициализацию состояния. Ваш код должен корректно обрабатывать ситуацию, когда `context.get()` возвращает `undefined` (например, при самом первом запуске контроллера или после очистки контекста). Паттерн `let state = flow.get('myState') || 'defaultState'` является обязательным. Без него ваш автомат упадет с ошибкой при первом же обращении.
Лучшие практики:
Что дальше
В этом уроке мы глубоко погрузились в управление состоянием — фундаментальный аспект надежной автоматизации. Вы научились использовать контекст и реализовали паттерн «Конечный автомат». В следующих уроках мы продолжим изучать продвинутые техники и рассмотрим, как строить комплексные, многоуровневые системы и интегрировать их с внешними сервисами, используя полученные знания.