ГлавнаяАкадемияИсполнительные устройства: интерлоки, таймауты → Надежное хранение состояния: персистентный контекст

Надежное хранение состояния: персистентный контекст

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

Проблема потери состояния и энергозависимый контекст

⚠️ Внимание: Игнорирование управления состоянием может привести к непредсказуемому поведению оборудования после сбоев питания, создавая риски для безопасности и комфорта. Например, включение насоса в пустом баке или отопления летом.

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

Представьте простой сценарий: пользователь включил свет в гостиной. Контроллер HI отправил команду на реле, и свет загорелся. В этот момент происходит сбой питания. После восстановления питания контроллер загружается заново. Но откуда ему знать, был ли свет включен до сбоя? По умолчанию — ниоткуда. Его "память" была стерта. Система не знает, должна ли она снова включить реле или оставить его выключенным.

Это и есть проблема потери состояния. Она критична для множества устройств:

Причина этой "амнезии" кроется в том, как Node-RED по умолчанию управляет данными. Вся информация, которую вы сохраняете в процессе работы потоков с помощью команд `flow.set()` или `global.set()`, помещается в энергозависимый контекст.

> 📋 Ключевые понятия:

> * Энергозависимый контекст — это область для хранения данных (переменных), которая физически располагается в оперативной памяти (RAM) контроллера.

> * Контекст узла (node context): `node.set('var', value)` — переменная доступна только внутри одного конкретного узла.

> * Контекст потока (flow context): `flow.set('var', value)` — переменная доступна всем узлам на одной вкладке (flow).

> * Глобальный контекст (global context): `global.set('var', value)` — переменная доступна абсолютно всем узлам на всех вкладках.

Оперативная память — это чрезвычайно быстрая, но временная память. Как только питание контроллера прекращается, все ее содержимое безвозвратно исчезает. При следующей загрузке Node-RED начинает работу с "чистого листа", все переменные контекста пусты.

Как мы рассмотрели в уроке `COURSE-05-M08-L01: Проблема 'Replay' при перезагрузке и опасность Retain-сообщений`, эта потеря состояния усугубляется внешними факторами. Например, MQTT-брокер может хранить последнее сообщение с флагом `retain`. После перезапуска контроллер получит это "старое" сообщение и может выполнить неактуальную команду.

Решение этой фундаментальной проблемы — использование персистентного контекста, то есть механизма сохранения критически важных данных в энергонезависимое хранилище, которое "переживает" перезагрузку.

---

Активация и настройка персистентного хранилища

> 💡 Подсказка: Перед редактированием `settings.js` всегда создавайте резервную копию (`sudo cp settings.js settings.js.bak`). Ошибка в синтаксисе этого файла может помешать запуску Node-RED.

На контроллерах HI, работающих под управлением ОС на базе Debian, персистентный контекст включается через редактирование главного конфигурационного файла Node-RED. Этот механизм по умолчанию отключен для максимальной производительности, но его активация является обязательным шагом для создания профессиональных и надежных систем.

Шаг 1: Доступ к файлу конфигурации

Файл `settings.js` — это сердце конфигурации Node-RED. Он содержит все настройки, от сетевых портов до тем интерфейса.

  • Подключитесь к контроллеру HI по SSH, используя учетные данные администратора.
  •     ssh admin@

  • Файл конфигурации обычно находится в скрытой директории `.node-red` в домашнем каталоге пользователя, от имени которого запущен сервис. Откройте его с помощью текстового редактора `nano`.
  •     nano ~/.node-red/settings.js

    Шаг 2: Настройка `contextStorage`

    Пролистайте файл `settings.js` вниз, пока не найдете секцию с заголовком `contextStorage`. По умолчанию она выглядит так (закомментирована):

    // contextStorage: {
    

    // default: { module: "memory" }

    // },

    Эта настройка означает, что по умолчанию для хранения всех контекстов (node, flow, global) используется только оперативная память (`memory`). Наша задача — активировать хранилище на базе файловой системы.

    Для этого раскомментируйте блок и приведите его к следующему виду:

    contextStorage: {
    

    default: "file",

    memoryOnly: { module: "memory" },

    file: { module: "localfilesystem" }

    },

    Разберем эту конфигурацию подробно:

    Шаг 3: Сохранение и перезапуск

  • В редакторе `nano` нажмите `Ctrl+X` для выхода.
  • Система спросит, хотите ли вы сохранить изменения. Нажмите `Y` (Yes).
  • Нажмите `Enter` для подтверждения имени файла.
  • Теперь необходимо перезапустить сервис Node-RED, чтобы изменения вступили в силу.
  •     sudo systemctl restart nodered

    После перезапуска Node-RED начнет автоматически сохранять данные контекста на диск.

    > ℹ️ Информация: Можно создавать несколько именованных хранилищ. Например, для хранения настроек и состояний в разных местах:

    >

    > contextStorage: {

    > default: { module: "memory" },

    > persistent_states: { module: "localfilesystem" },

    > device_settings: { module: "localfilesystem", config: { dir: '/data/settings' }}

    > },

    >

    > В этом случае для сохранения/чтения нужно будет явно указывать имя хранилища: `flow.set('brightness', 80, 'persistent_states')`.

    Для начала работы достаточно простой конфигурации с `default: "file"`.

    ---

    Практический пример: восстановление состояния Modbus-диммера

    Рассмотрим реальную задачу инсталлятора: есть светильник, яркость которого управляется по Modbus TCP. Управление осуществляется через MQTT-сообщения, например, с настенной панели или из мобильного приложения. Нам нужно, чтобы после перезагрузки контроллера светильник возвращался к той же яркости, которая была установлена последней.

    Компоненты сценария: Архитектура потока в Node-RED:

    Поток будет состоять из двух логических частей:

  • Сохранение состояния: Получение команды по MQTT, запись значения в персистентный контекст и отправка команды на Modbus-устройство.
  • Восстановление состояния: При запуске Node-RED — чтение значения из контекста и отправка его на Modbus-устройство.
  • ASCII-схема потока:
    // Часть 1: Обработка команд в реальном времени
    

    [mqtt in]------------------>[function: "Сохранить и подготовить"]--+

    `hi/.../set` |

    v

    // Часть 2: Восстановление при старте [modbus-write]

    [inject]------------------->[function: "Загрузить и восстановить"]--+--->`Диммер (HR10)`

    `Once on start`

    Пошаговое создание потока

  • Узел `mqtt in`:
  • * Сервер: Настроенный MQTT-брокер.

    * Топик: `hi/living_room/main_light/brightness/set`

    * Выход: `разобранный объект JSON` (если панель шлет JSON) или `строка` (если панель шлет просто число).

  • Узел `function` ("Сохранить и подготовить"):
  • Этот узел — ядро логики сохранения.

        // 1. Валидация входящего значения

    let brightness = parseInt(msg.payload);

    if (isNaN(brightness) || brightness < 0 || brightness > 100) {

    node.error("Некорректное значение яркости: " + msg.payload, msg);

    node.status({fill:"red", shape:"dot", text:"Ошибка: " + msg.payload});

    return null; // Останавливаем поток

    }

    // 2. Сохранение значения в персистентный контекст потока

    // Так как в settings.js мы установили 'file' как 'default',

    // третий параметр можно не указывать.

    flow.set("lastBrightness", brightness);

    // 3. Подготовка сообщения для узла Modbus-Write

    // Контракт сообщения для `modbus-write`

    msg.payload = {

    'value': brightness,

    'fc': 6, // FC 6: Preset Single Register

    'unitid': 1,

    'address': 10,

    'quantity': 1

    };

    node.status({fill:"green", shape:"dot", text:"Уст: " + brightness + "%"});

    return msg;

    > 💡 Подсказка: Если бы мы не меняли хранилище по умолчанию, строка сохранения выглядела бы так: `flow.set("lastBrightness", brightness, "file");`

  • Узел `inject`:
  • * Payload: `timestamp` (неважно).

    * Topic: (пусто).

    * Поставьте галочку "Inject once after" и выберите задержку, например, `5` секунд. Эта задержка важна, чтобы все сервисы контроллера, включая Modbus-клиент, успели запуститься и стабилизироваться.

  • Узел `function` ("Загрузить и восстановить"):
  • Этот узел сработает один раз после перезагрузки.

        // 1. Чтение сохраненного значения из персистентного контекста

    let lastBrightness = flow.get("lastBrightness");

    // 2. Проверка, есть ли сохраненное значение

    if (lastBrightness === undefined) {

    // Первый запуск, или контекст был очищен. Ничего не делаем.

    node.warn("Сохраненное состояние яркости не найдено.");

    return null;

    }

    // 3. Подготовка сообщения для узла Modbus-Write

    // Используем тот же формат, что и в первом function-узле

    msg.payload = {

    'value': lastBrightness,

    'fc': 6,

    'unitid': 1,

    'address': 10,

    'quantity': 1

    };

    node.status({fill:"blue", shape:"dot", text:"Восст: "+ lastBrightness + "%"});

    return msg;

  • Узел `modbus-write`:
  • * Сервер: Настроенный Modbus TCP клиент для IP `192.168.1.120`.

    * Имя: `Диммер Гостиная (HR10)`

    * Все остальные параметры (FC, Address и т.д.) будут взяты из `msg.payload`, который мы формируем в `function`-узлах.

    Соедините выходы обоих `function`-узлов со входом одного и того же `modbus-write`. Теперь ваша система не только управляет диммером, но и надежно помнит его состояние между перезагрузками.

    ---

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

    🔗 Связанный материал: Для долговременного хранения истории состояний и сложной аналитики рекомендуется использовать специализированные СУБД. Подробнее об этом в курсе по интеграции с базами данных: `COURSE-09-M02-L01`.

    Использование персистентного контекста — мощный инструмент, но, как и любой инструмент, он требует грамотного применения. Неправильное использование может привести к снижению производительности и преждевременному износу оборудования.

    Принцип №1: Сохраняйте только необходимое

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

    Категорически не следует сохранять:

    Принцип №2: Берегите Flash-память

    Контроллер HI, как и большинство встраиваемых систем, использует flash-память для хранения операционной системы и данных. У этого типа памяти есть ограничение на количество циклов перезаписи. Каждое `flow.set()` в персистентный контекст (`file`) инициирует операцию записи на диск. Если делать это слишком часто, можно сократить срок службы накопителя.

    Стратегия минимизации записей:

    Используйте оба типа хранилища, которые мы настроили в `settings.js`.

    | Характеристика | Контекст в RAM (`memoryOnly`) | Персистентный контекст (`file`) |

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

    | Скорость | Очень высокая | Низкая (ограничена скоростью диска) |

    | Надежность | Низкая (данные теряются при перезагрузке) | Высокая (данные сохраняются) |

    | Влияние на износ | Нулевое | Есть (каждая запись — цикл перезаписи) |

    | Сценарий | Временные переменные, счетчики, часто обновляемые флаги | Финальные состояния, уставки, режимы |

    Пример: Вам нужно посчитать, сколько раз за час сработал датчик движения. Не стоит писать `flow.set("counter", new_value, "file")` при каждом срабатывании. Правильный подход:
  • Все расчеты вести в контексте `memoryOnly`.
  • `flow.set("motion_counter", current_count + 1, "memoryOnly");`

  • И только раз в час (или по другому событию) сохранять итоговое значение в персистентный контекст.
  • `flow.set("hourly_motion_count", final_count, "file");`

    Для этого в файле `settings.js` лучше оставить `default: "memoryOnly"` и явно указывать файловое хранилище, когда это необходимо.

    Альтернативные подходы

    Когда файловый контекст становится недостаточным?

    В таких случаях стоит рассмотреть использование легковесной базы данных, например SQLite, которая также хранит данные в одном файле, но предоставляет гораздо более мощный инструментарий для управления данными через SQL-запросы. Интеграция с SQLite через палитру `node-red-node-sqlite` является логичным следующим шагом для систем, где требуется сложная аналитика и ведение исторических архивов.

    Что дальше

    В этом уроке мы рассмотрели фундаментальный механизм обеспечения надежности — персистентный контекст. Вы научились активировать его на контроллере HI и применять для сохранения и восстановления состояния исполнительных устройств. В следующем уроке мы объединим эти знания с ранее изученными паттернами, такими как "Команда-Подтверждение-Таймаут", для создания комплексных и отказоустойчивых сценариев управления моторизированными приводами.