ГлавнаяАкадемияNode-RED: установка, flows, msg/JSON, отладка → Нода `Function`: когда без JavaScript не обойтись

Нода `Function`: когда без JavaScript не обойтись

Урок 6 · Node-RED: установка, flows, msg/JSON, отладка · 30 мин · theory

Введение в ноду `Function`: Когда стандартных нод недостаточно

В предыдущих уроках мы рассмотрели мощные инструменты для манипуляции и маршрутизации сообщений: ноды `Change`, `Switch`, `Template` и язык запросов JSONata. В подавляющем большинстве задач автоматизации их функционала более чем достаточно для построения надежной и читаемой логики. Однако на практике возникают ситуации, когда требуется более сложная обработка данных, реализация нестандартных алгоритмов или взаимодействие с внешними библиотеками. Именно для таких случаев предназначена нода `Function`.

Нода `Function` — это ваш швейцарский нож в мире Node-RED. Она позволяет выполнять произвольный JavaScript-код над входящим сообщением `msg`, открывая практически безграничные возможности для трансформации данных и реализации самой сложной бизнес-логики.

> 💡 Подсказка: Для простых задач, таких как изменение одного поля, простая маршрутизация или форматирование строки, всегда предпочитайте ноды `Change`, `Switch` и `Template`. Это сохраняет визуальную читаемость потока (flow), упрощает его отладку и поддержку другими инженерами. Ноду `Function` следует использовать только тогда, когда стандартные средства не справляются.

Ключевые сценарии использования ноды `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` — это центральный объект, с которым вы работаете.

  • Передача сообщения дальше: Чтобы сообщение продолжило свой путь по потоку, функция должна завершиться командой `return msg;`. Если вы изменили `msg.payload` или добавили новое свойство `msg.topic`, именно измененный объект будет передан на вход следующей ноды.
  • Остановка потока: Если по результатам вашей логики сообщение не должно идти дальше (например, оно не прошло валидацию), вы можете остановить поток, вернув `null`.
  •     // Пример: пропускать только сообщения с температурой выше 20 градусов

    if (msg.payload.value < 20) {

    // Температура слишком низкая, останавливаем поток для этой ветки.

    // Никакое сообщение не будет отправлено из этой ноды.

    return null;

    }

    // Если условие не выполнилось, передаем сообщение дальше

    return msg;

  • Ветвление логики (несколько выходов): Нода `Function` может иметь несколько выходных портов. Это настраивается в ее интерфейсе (`Setup` -> `Outputs`). Чтобы отправить сообщения на разные выходы, нужно вернуть массив сообщений. Индекс элемента в массиве соответствует номеру выхода (начиная с 0).
  •     // Пример: Нода с двумя выходами.

    // Выход 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.set('myVar', 123);` — сохранить значение.

    * `let myVar = context.get('myVar');` — прочитать значение.

    * `let counter = context.get('count') || 0;` — прочитать значение, и если его нет, установить 0.

    * `flow.set('sharedState', 'active');`

    * `let state = flow.get('sharedState');`

    * `global.set('mainMode', 'day');`

    * `let mode = global.get('mainMode');`

    ---

    Практический пример: Обработка данных с Modbus-счетчика

    Представим типовую задачу для инженера автоматизации. У нас есть Modbus-счетчик электроэнергии, который подключен к контроллеру HI по шине RS-485. При опросе нодой `Modbus-Read` он возвращает не готовые числа, а массив сырых 16-битных регистров. Наша задача — превратить этот массив в понятный JSON-объект.

    > ℹ️ Информация: Контроллеры HI часто работают с промышленным оборудованием по протоколам вроде Modbus. Умение обрабатывать сырые данные (raw data) из Buffer или массивов — ключевой навык для инженера автоматизации. Стандартные ноды здесь бессильны.

    Сценарий:

    Счетчик по запросу отдает 4 регистра.

    Нода `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) отправляет сообщение при нажатии и отпускании кнопки.

    Входящее сообщение `msg` имеет `msg.payload`, равное `true` при нажатии и `false` при отпускании.

    Нам понадобится хранить состояние (включен/выключен) и время нажатия. Для этого идеально подходит `context` ноды. Настроим ноду `Function` на 2 выхода:

  • Выход 1: Команды ВКЛ/ВЫКЛ (для отправки на реле или диммер).
  • Выход 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".

    Инструменты отладки

    Если что-то идет не так, у вас есть встроенные инструменты для "заглядывания" внутрь ноды:

        let myData = { a: 1, b: 2 };
    

    node.log("Промежуточные данные:");

    node.log(myData);

    Лучшие практики написания кода

  • Комментируйте код осмысленно: Не пишите комментарии, объясняющие, что делает код (например, `// увеличиваем счетчик на 1`). Это и так видно. Пишите комментарии, объясняющие, зачем он это делает (`// Увеличиваем счетчик попыток подключения, чтобы перезапустить модуль после 3 неудач`).
  • Держите функции короткими: Нода `Function` должна решать одну, четко определенную задачу. Если ваш код разрастается до 50+ строк и выполняет несколько разных действий, это сигнал к рефакторингу.
  • Разбивайте сложную логику: Вместо одной гигантской ноды `Function` используйте цепочку из нескольких. Например:
  • * Первая `Function`: Валидация и очистка входящих данных.

    * Вторая `Function`: Основные вычисления.

    * Третья `Function`: Форматирование исходящего сообщения.

    Это делает поток более читаемым и позволяет легко отлаживать каждый шаг.

  • Выносите повторяющийся код в Subflows: Если вы обнаружили, что копируете одну и ту же ноду `Function` в разные потоки, создайте на ее основе переиспользуемый компонент — Subflow.
  • Золотое правило гласит: код внутри `Function` должен быть максимально простым, коротким и сфокусированным на одной задаче. Чем больше логики вы можете реализовать с помощью стандартных визуальных нод, тем лучше. Прибегайте к JavaScript только тогда, когда без него действительно не обойтись.

    Что дальше?

    В этом уроке мы глубоко погрузились в возможности ноды `Function`, рассмотрели ее окружение, научились работать с контекстом и разобрали два практических примера, демонстрирующих ее мощь. Теперь вы готовы применять JavaScript для решения самых сложных задач автоматизации. В следующих уроках мы закрепим полученные знания на практике, выполнив лабораторные работы и подготовившись к сертификационному экзамену.