Нода `Function`: когда без JavaScript не обойтись
Введение в ноду `Function`: Когда стандартных нод недостаточно
В предыдущих уроках мы рассмотрели мощные инструменты для манипуляции и маршрутизации сообщений: ноды `Change`, `Switch`, `Template` и язык запросов JSONata. В подавляющем большинстве задач автоматизации их функционала более чем достаточно для построения надежной и читаемой логики. Однако на практике возникают ситуации, когда требуется более сложная обработка данных, реализация нестандартных алгоритмов или взаимодействие с внешними библиотеками. Именно для таких случаев предназначена нода `Function`.
Нода `Function` — это ваш швейцарский нож в мире Node-RED. Она позволяет выполнять произвольный JavaScript-код над входящим сообщением `msg`, открывая практически безграничные возможности для трансформации данных и реализации самой сложной бизнес-логики.> 💡 Подсказка: Для простых задач, таких как изменение одного поля, простая маршрутизация или форматирование строки, всегда предпочитайте ноды `Change`, `Switch` и `Template`. Это сохраняет визуальную читаемость потока (flow), упрощает его отладку и поддержку другими инженерами. Ноду `Function` следует использовать только тогда, когда стандартные средства не справляются.
Ключевые сценарии использования ноды `Function`:
- Сложные математические вычисления: Расчет среднего значения за период, применение тригонометрических функций, работа с показаниями промышленных счетчиков, требующими побитовых операций и масштабных коэффициентов.
- Работа с циклами и массивами: Обработка каждого элемента в массиве данных, фильтрация или агрегация по сложным условиям, которые не покрываются возможностями JSONata.
- Реализация комплексной логики ветвления: Когда требуется проверить множество условий в определенной последовательности, сохранить промежуточные результаты и принять решение на их основе.
- Динамическое формирование `msg`: Создание структуры исходящего сообщения на лету, где имена полей или их количество зависят от входящих данных.
- Интеграция с внешними JavaScript-библиотеками: (Продвинутая тема) Вы можете подключить в окружение Node-RED дополнительные модули (например, для криптографии или сложного анализа данных) и использовать их внутри ноды `Function`.
Сравнение ноды `Function` с альтернативами
Чтобы лучше понять место `Function` в экосистеме Node-RED, сравним ее с уже изученными нодами:
| Нода | Основное назначение | Сильные стороны | Слабые стороны |
| :--- | :--- | :--- | :--- |
| `Change` | Простые операции: set, change, move, delete | Визуально понятна, быстрая настройка, высокая производительность | Ограниченный набор операций, не умеет в сложную логику |
| `Switch` | Маршрутизация `msg` по условиям | Наглядно показывает ветвление потока, легко настраивается | Не подходит для трансформации данных, только для перенаправления |
| `JSONata` (`Change`) | Сложные запросы и трансформация JSON | Мощный декларативный язык для работы со структурами данных | Требует изучения синтаксиса, не может выполнять пошаговые алгоритмы |
| `Function` | Произвольная логика на JavaScript | Максимальная гибкость, может все, что может JavaScript | Скрывает логику внутри "черного ящика", усложняет чтение потока, выше риск ошибок |
Ключевой принцип работы ноды `Function` прост: она получает на вход объект `msg` и, по завершении выполнения кода, должна вернуть измененный объект `msg` (или несколько объектов), который будет передан дальше по потоку.
// Простейший пример: удваиваем значение в payload
// Вход: msg.payload = 10
msg.payload = msg.payload * 2;
// Выход: msg.payload = 20
return msg;
---
Структура и окружение ноды `Function`: msg, context, flow, global
При написании кода внутри ноды `Function` вы работаете не в вакууме, а в специально подготовленном окружении, которое предоставляет доступ к самому сообщению, а также к переменным разного уровня видимости. Понимание этого окружения — ключ к эффективному использованию ноды.
Работа с объектом `msg`
Как уже было сказано, `msg` — это центральный объект, с которым вы работаете.
// Пример: пропускать только сообщения с температурой выше 20 градусов
if (msg.payload.value < 20) {
// Температура слишком низкая, останавливаем поток для этой ветки.
// Никакое сообщение не будет отправлено из этой ноды.
return null;
}
// Если условие не выполнилось, передаем сообщение дальше
return msg;
// Пример: Нода с двумя выходами.
// Выход 1: для команд включения.
// Выход 2: для команд выключения.
let command = msg.payload.command;
let msg1 = null; // Сообщение для выхода 1
let msg2 = null; // Сообщение для выхода 2
if (command === "ON") {
msg1 = { payload: "включить" }; // Формируем сообщение для первого выхода
} else if (command === "OFF") {
msg2 = { payload: "выключить" }; // Формируем сообщение для второго выхода
}
// Возвращаем массив. На первый выход уйдет msg1, на второй — msg2.
// Если какая-то из переменных равна null, на соответствующий выход ничего не уйдет.
return [msg1, msg2];
Хранение состояния: `context`, `flow`, `global`
Часто возникает необходимость хранить какие-то данные между вызовами одной и той же ноды `Function`. Для этого существует контекст.
> ⚠️ Внимание: Использование `global` переменных — мощный, но опасный инструмент. Он связывает логику разных, не связанных визуально потоков, и может привести к трудноотлавливаемым ошибкам (race conditions), когда несколько потоков одновременно пытаются изменить одну и ту же переменную. Используйте `global` только в крайних случаях, например, для хранения общих настроек, которые редко меняются.
- Контекст ноды (`context`): Переменные, доступные только внутри одной конкретной ноды. Идеально для хранения состояния, специфичного для данного узла (например, счетчик вызовов или время последнего срабатывания). Данные теряются при перезагрузке потока (deploy), если не настроено персистентное хранилище.
* `let myVar = context.get('myVar');` — прочитать значение.
* `let counter = context.get('count') || 0;` — прочитать значение, и если его нет, установить 0.
- Контекст потока (`flow`): Переменные, доступные всем нодам на одной вкладке (flow). Полезно для обмена данными между нодами одного логического блока без необходимости передавать все через `msg`.
* `let state = flow.get('sharedState');`
- Глобальный контекст (`global`): Переменные, доступные абсолютно всем нодам в вашем проекте Node-RED.
* `let mode = global.get('mainMode');`
---
Практический пример: Обработка данных с Modbus-счетчика
Представим типовую задачу для инженера автоматизации. У нас есть Modbus-счетчик электроэнергии, который подключен к контроллеру HI по шине RS-485. При опросе нодой `Modbus-Read` он возвращает не готовые числа, а массив сырых 16-битных регистров. Наша задача — превратить этот массив в понятный JSON-объект.
> ℹ️ Информация: Контроллеры HI часто работают с промышленным оборудованием по протоколам вроде Modbus. Умение обрабатывать сырые данные (raw data) из Buffer или массивов — ключевой навык для инженера автоматизации. Стандартные ноды здесь бессильны.
Сценарий:Счетчик по запросу отдает 4 регистра.
- Регистры 0 и 1: Суммарное потребление (кВт*ч), 32-битное целое, с масштабным коэффициентом 0.01.
- Регистры 2 и 3: Мгновенная мощность (Вт), 32-битное целое, без коэффициента.
Нода `Modbus-Read` вернула нам сообщение, где `msg.payload` выглядит так:
[
17,
133,
2,
24
]
Здесь `[17, 133]` — это 32-битное число для потребления, а `[2, 24]` — для мощности. Чтобы получить из них реальные значения, нужно выполнить побитовые операции и математические преобразования, что невозможно сделать через `Change` или `JSONata`.
Вот как будет выглядеть код в ноде `Function`:
// На входе msg.payload = [17, 133, 2, 24]
const data = msg.payload;
// 1. Проверяем, что получили ожидаемое количество данных
if (!Array.isArray(data) || data.length < 4) {
node.error("Некорректный формат данных от Modbus-счетчика", msg);
return null; // Останавливаем поток, если данные неверны
}
// 2. Декодируем суммарное потребление (регистры 0 и 1)
// Используем побитовый сдвиг влево (<<) для старшего регистра
// и побитовое ИЛИ (|) для объединения с младшим.
// Это стандартная практика для сборки 32-битных чисел из 16-битных.
const rawTotalKwh = (data[0] << 16) | data[1];
// 3. Декодируем мгновенную мощность (регистры 2 и 3)
const rawCurrentPower = (data[2] << 16) | data[3];
// 4. Применяем масштабные коэффициенты
const totalKwh = rawTotalKwh * 0.01;
const currentPowerW = rawCurrentPower; // Коэффициент 1
// 5. Формируем итоговый объект msg.payload в соответствии с "Контрактом сообщения"
msg.payload = {
"total_kwh": parseFloat(totalKwh.toFixed(3)), // Округляем для чистоты
"current_power_w": currentPowerW,
"source": "powermeter-main-01",
"ts": Date.now()
};
// 6. Устанавливаем статус ноды для быстрой диагностики
node.status({
fill: "green",
shape: "dot",
text: `OK: ${currentPowerW} Вт`
});
// Возвращаем полностью сформированное сообщение
return msg;
Результат:
На выходе ноды `Function` мы получим аккуратный `msg` со следующим `payload`:
{
"total_kwh": 1113.73,
"current_power_w": 131096,
"source": "powermeter-main-01",
"ts": 1678886400000
}
Точные числа в примере могут немного отличаться в зависимости от входных регистров, но структура будет именно такой.
Этот объект уже можно легко отправлять в MQTT, сохранять в базу данных MySQL или использовать для дальнейшей логики.
---
Практический пример: Реализация логики "умного" диммера
Рассмотрим еще один распространенный сценарий из "умного дома" — управление светом с одной кнопки, которая поддерживает короткое и длинное нажатие.
> 🔗 Связанный материал: Более сложные паттерны управления состоянием, такие как полноценные конечные автоматы (State Machines) для климат-контроля или систем безопасности, будут подробно рассмотрены в уроке `COURSE-06-M05-L01`.
Сценарий:Настенный выключатель (например, KNX или подключенный к дискретному входу контроллера HI) отправляет сообщение при нажатии и отпускании кнопки.
- Короткое нажатие (нажал и отпустил < 300 мс): инвертировать состояние света (был выключен -> включить на 100%, был включен -> выключить).
- Длинное нажатие (удерживание кнопки): плавно увеличивать яркость. При отпускании — остановить изменение.
Входящее сообщение `msg` имеет `msg.payload`, равное `true` при нажатии и `false` при отпускании.
Нам понадобится хранить состояние (включен/выключен) и время нажатия. Для этого идеально подходит `context` ноды. Настроим ноду `Function` на 2 выхода:
/*
Логика умного диммера
Вход: msg.payload (boolean) - true при нажатии, false при отпускании.
Выходы:
[0]: Команда вкл/выкл. msg.payload = { "state": "ON"/"OFF", "brightness": 100/0 }
[1]: Команда изменения яркости. msg.payload = { "command": "START_DIM_UP" / "STOP_DIM" }
*/
// Получаем текущее состояние кнопки
const isPressed = msg.payload;
// Получаем переменные из контекста ноды. Если их нет, задаем значения по умолчанию.
let lastPressTime = context.get('lastPressTime') || 0;
let lightState = context.get('lightState') || "OFF"; // "ON" или "OFF"
if (isPressed) {
// ---- ЛОГИКА ПРИ НАЖАТИИ КНОПКИ ----
context.set('lastPressTime', Date.now()); // Запоминаем время нажатия
// На данном этапе мы еще не знаем, будет ли нажатие коротким или длинным.
// Поэтому просто ждем события отпускания.
// В более сложной логике здесь можно было бы запустить таймер.
return null; // Ничего не отправляем, ждем отпускания
} else {
// ---- ЛОГИКА ПРИ ОТПУСКАНИИ КНОПКИ ----
const pressDuration = Date.now() - lastPressTime;
let toggleMsg = null; // Сообщение для выхода 1
let dimMsg = null; // Сообщение для выхода 2
if (pressDuration < 300) {
// Это короткое нажатие
if (lightState === "OFF") {
lightState = "ON";
toggleMsg = { payload: { "state": "ON", "brightness": 100 } };
} else {
lightState = "OFF";
toggleMsg = { payload: { "state": "OFF", "brightness": 0 } };
}
context.set('lightState', lightState); // Сохраняем новое состояние
} else {
// Это было длинное нажатие, которое только что закончилось.
// Отправляем команду остановить диммирование.
// Логика запуска диммирования должна быть в другом месте
// (например, по таймеру, который проверяет, что кнопка все еще нажата спустя 300мс).
// Для упрощения примера, мы покажем только простую логику.
// Этот пример можно усложнить, добавив таймеры, но основа останется той же.
node.warn("Long press detected, but dimming logic is not fully implemented in this example.");
}
// Обновляем статус ноды
node.status({ text: `State: ${lightState} | Last press: ${pressDuration}ms`});
// Возвращаем массив сообщений для двух выходов
return [toggleMsg, dimMsg];
}
Этот пример демонстрирует, как с помощью `context` и простой логики на JavaScript можно реализовать нетривиальное поведение, которое часто требуется в проектах умного дома.
---
Отладка и лучшие практики: Пишем чистый и поддерживаемый код
Код в ноде `Function` — это "черный ящик" на схеме потока. Чем он сложнее и запутаннее, тем труднее поддерживать всю систему. Поэтому крайне важно придерживаться принципов чистого кода.
> 💡 Подсказка: Всегда давайте нодам `Function` осмысленные имена в поле `Name`. Имя "Декодировать данные счетчика" гораздо информативнее, чем стандартное "function".
Инструменты отладки
Если что-то идет не так, у вас есть встроенные инструменты для "заглядывания" внутрь ноды:
- `node.log(object)`: Выводит сообщение в панель "Debug" (отладка). В отличие от стандартной `console.log()`, `node.log` позволяет выводить целые объекты в интерактивном виде. Это основной инструмент для проверки значений переменных на разных этапах выполнения кода.
let myData = { a: 1, b: 2 };
node.log("Промежуточные данные:");
node.log(myData);
- `node.warn(object)`: Делает то же самое, что и `node.log`, но выводит сообщение с оранжевой иконкой предупреждения. Используйте для событий, которые не являются критической ошибкой, но требуют внимания.
- `node.error(message, original_msg)`: Генерирует ошибку, которая может быть перехвачена нодой `Catch`. Это правильный способ обработки критических сбоев внутри `Function`. Второй аргумент позволяет передать исходное сообщение, вызвавшее ошибку, что бесценно для анализа проблемы.
Лучшие практики написания кода
* Первая `Function`: Валидация и очистка входящих данных.
* Вторая `Function`: Основные вычисления.
* Третья `Function`: Форматирование исходящего сообщения.
Это делает поток более читаемым и позволяет легко отлаживать каждый шаг.
Золотое правило гласит: код внутри `Function` должен быть максимально простым, коротким и сфокусированным на одной задаче. Чем больше логики вы можете реализовать с помощью стандартных визуальных нод, тем лучше. Прибегайте к JavaScript только тогда, когда без него действительно не обойтись.
Что дальше?
В этом уроке мы глубоко погрузились в возможности ноды `Function`, рассмотрели ее окружение, научились работать с контекстом и разобрали два практических примера, демонстрирующих ее мощь. Теперь вы готовы применять JavaScript для решения самых сложных задач автоматизации. В следующих уроках мы закрепим полученные знания на практике, выполнив лабораторные работы и подготовившись к сертификационному экзамену.