В данной инструкции описаны шаги по добавлению пользовательских узлов логики в Редактор логики, включая установку, создание и использование узлов.
Перед началом разработки узла, необходимо сделать следующие действия:
nmp --versionВ качестве IDE, в инструкции, будет описана работа с Microsoft Visual Studio Code (далее VS Code).
В данной инструкции будет описано создание пользовательского узла с 4-мя входами и выходами, который будет выполнять простые математические операции с поступающими данными.
Исходный код описанного ниже узла, можно скачать.
mkdir my-new-nodecd my-new-nodenpm initВ результате, в папке появится файл package.json, в котором хранится вся информация о проекте. Откроем этот файл и приведём его к следующему виду:
{
"name": "my-new-node", // имя нашего узла
"version": "1.0.0", // версия
"description": "", // описание
"main": "my-new-node.js", // имя файла скрипта
"node-red": { // описание узла для редактора логики
"nodes": {
"my-node": "my-new-node.js"
}
}
}
В файле package.json можно указывать и другие метаданные, более подробно можно почитать в интернете.
Никаких строгих требований к структуре каталогов, используемой в пакете, нет. Если пакет содержит несколько узлов, они все могут существовать в одном каталоге или каждый из них может быть помещен в свой собственный подкаталог.
Файл my-new-node.html - позволяет настроить внешний вид узла, его цвет, иконку, кол-во входов и выходов. Также, в этом файле настраивается окно параметров узла. Все CSS-стили элементов окна параметров узла должны быть прописаны непосредственно в TEG’ах, чтобы не пересекаться с CSS-стилями других узлов.
Ниже приведён код файла:
<!-- Элементы меню настроек узла -->
<script type="text/html" data-template-name="my-new-node">
<div class="form-row"> <!-- Строка меню настроек узла -->
<label for="node-input-name"><i class="icon-tag"></i> Имя узла</label>
<input type="text" id="node-input-name" style="width: calc(100% - 105px)" placeholder="Имя">
</div>
<div class="form-row"> <!-- Строка меню настроек узла -->
<div class="form-row-container"
style="display: grid;
grid-template-columns: 94px 1fr 100px;
grid-template-rows: 1fr;
column-gap: 10px;"> <!-- Контейнер -->
<label for="node-input-function1">
<span
style="display: grid;
align-content: center;
height: 100%;
margin: 0;">Вход 1</span>
</label> <!-- Название входа -->
<select id="node-input-function1" style="width: 100%;"> <!-- Селектор функции -->
<option value="1" selected>Сложение</option>
<option value="2">Вычитание</option>
<option value="3">Умножение</option>
<option value="4">Деление</option>
</select>
<input type="number" class="value-for function1" style="width: 100%;"> <!-- Поле ввода значения -->
</div>
</div>
<div class="form-row"> <!-- Строка меню настроек узла -->
<div class="form-row-container"
style="display: grid;
grid-template-columns: 94px 1fr 100px;
grid-template-rows: 1fr;
column-gap: 10px;">
<label for="node-input-function2">
<span
style="display: grid;
align-content: center;
height: 100%;
margin: 0;">Вход 2</span>
</label>
<select id="node-input-function2" style="width: 100%;">
<option value="1" selected>Сложение</option>
<option value="2">Вычитание</option>
<option value="3">Умножение</option>
<option value="4">Деление</option>
</select>
<input type="number" class="value-for function2" style="width: 100%;">
</div>
</div>
<div class="form-row"> <!-- Строка меню настроек узла -->
<div class="form-row-container"
style="display: grid;
grid-template-columns: 94px 1fr 100px;
grid-template-rows: 1fr;
column-gap: 10px;">
<label for="node-input-function3">
<span
style="display: grid;
align-content: center;
height: 100%;
margin: 0;">Вход 3</span>
</label>
<select id="node-input-function3" style="width: 100%;">
<option value="1" selected>Сложение</option>
<option value="2">Вычитание</option>
<option value="3">Умножение</option>
<option value="4">Деление</option>
</select>
<input type="number" class="value-for function3" style="width: 100%;">
</div>
</div>
<div class="form-row"> <!-- Строка меню настроек узла. -->
<div class="form-row-container form-row-function4"
style="display: grid;
grid-template-columns: 94px 1fr 100px;
grid-template-rows: 1fr;
column-gap: 10px;">
<!-- Для примера, создаётся динамически, используя JS -->
</div>
</div>
</script>
<!-- Скрипт регистрации узла и управление его настройками -->
<script type="text/javascript">
RED.nodes.registerType('my-new-node', {
category: 'Драйверы', // Категория, в которую будет загружен узел в палитре
color: '#E31B23', // Установка цвета узла
defaults: {
name: {value: ""}, // Имя узла по-умолчанию
inputFunctions: {value: []}, // Массив хранит значения выбранных функций для входов
inputValues: {value: []}, // Массив хранит числовые значения для входов
inputCount: {value: 4}, // Значение кол-ва входов для использования в файле js
outputCount: {value: 4} // Значение кол-ва выходов для использования в файле js
},
customInputs: true, // Этот параметр всегда должен быть true
inputs: 4, // Начальное значение кол-ва входов
outputs: 4, // Начальное значение кол-ва выходов
inputNames: ['Вход 1', 'Вход 2', 'Вход 3', 'Вход 4'], // Названия входов
outputNames: ['Выход 1', 'Выход 2', 'Выход 3', 'Выход 4'], // Названия выходов
paletteLabel: "my-new-node", // Имя узла в палитре редактора
icon: "font-awesome/fa-microchip", // Иконка узла, можно использовать FontAwesome v4.7
label: function() {
return this.name || "my-new-node"; // Если имя узла не задано, назвать “my-new-node”
},
oneditprepare: function() { // Базовая функция срабатывает при открытии окна настроек узла.
/*
Так как, при открытии, окно настроек всегда отрисовывается заново, необходимо синхронизировать значения.
*/
var node = this;
const select_inp1 = document.querySelector('#node-input-function1'); // Получаем селектор первого входа
select_inp1.value = node.inputFunctions[0]; // Присваиваем селектору значение первой ячейки массива функций
const value_inp1 = document.querySelector('.function1'); // Получаем поле ввода значения первого входа
value_inp1.value = node.inputValues[0]; // Присваиваем полю ввода значение первой ячейки массива значений
const select_inp2 = document.querySelector('#node-input-function2');
select_inp2.value = node.inputFunctions[1];
const value_inp2 = document.querySelector('.function2');
value_inp2.value = node.inputValues[1];
const select_inp3 = document.querySelector('#node-input-function3');
select_inp3.value = node.inputFunctions[2];
const value_inp3 = document.querySelector('.function3');
value_inp3.value = node.inputValues[2];
// Динамическое создание настроек для 4-го входа
const container = document.querySelector(".form-row-function4"); // Находим элемент-контейнер, в который будем добавлять элементы
const label = document.createElement("label"); // Создаём элемент <label> который хранит текстовый элемент
label.setAttribute("for", "node-input-function4"); // Применяем необходимые атрибуты
const span = document.createElement("span"); // Создаём элемент <span>
span.textContent = "Вход 4"; // Применяем текст к элементу <span>
span.setAttribute("style", "display: grid; align-content: center; height: 100%; margin: 0;"); // Применяем стили
const select = document.createElement('select'); // Создаём селектор <select>
select.setAttribute("id", "node-input-function4"); // Применяем необходимые атрибуты
select.setAttribute("style", "width: 100%;"); // Применяем стили
const options = [ // Создаём список значений селектора
{ value: '1', text: 'Сложение', selected: true },
{ value: '2', text: 'Вычитание' },
{ value: '3', text: 'Умножение' },
{ value: '4', text: 'Деление' }
];
options.forEach(optionData => { // Проходим по списку значений и добавляем их к селектору
const option = document.createElement('option');
option.value = optionData.value;
option.textContent = optionData.text;
if(optionData.selected) {
option.selected = true;
}
select.appendChild(option);
});
const input = document.createElement('input'); // Создаём элемент <input>
input.setAttribute("type", "number"); // Применяем необходимые атрибуты
input.setAttribute("style", "width: 100%;"); // Применяем стили
input.className = 'value-for function4'; // Применяем необходимые классы
label.appendChild(span); // Добавляем элемент <span> в <label>
container.appendChild(label); // Добавляем элемент <label> в элемент-контейнер
container.appendChild(select); // Добавляем элемент <select> в элемент-контейнер
container.appendChild(input); // Добавляем элемент <input> в элемент-контейнер
select.value = node.inputFunctions[3]; // Присваиваем селектору значение четвёртой ячейки массива функций
input.value = node.inputValues[3]; // Присваиваем полю ввода значение четвёртой ячейки массива значений
// если необходимо создать свою функцию, можно сделать так
node.myNewFunction = function() {
// код функции
}
},
oneditsave: function () { // Базовая функция срабатывает при сохранении настроек узла
var node = this;
let value;
const select_inp1 = document.querySelector('#node-input-function1'); // Получаем селектор первого входа.
node.inputFunctions[0] = parseInt(select_inp1.value); // Сохраняем значение селектора в первую ячейку массива функций.
const value_inp1 = document.querySelector('.function1'); // Получаем поле ввода значения первого входа.
value = parseInt(value_inp1.value); // Получаем значение поля ввода первого входа.
node.inputValues[0] = !value ? 0 : value; // Сохраняем значение в первую ячейку массива значений. Если значение не было введено, присваиваем 0.
const select_inp2 = document.querySelector('#node-input-function2');
node.inputFunctions[1] = parseInt(select_inp2.value);
const value_inp2 = document.querySelector('.function2');
value = parseInt(value_inp2.value);
node.inputValues[1] = !value ? 0 : value;
const select_inp3 = document.querySelector('#node-input-function3');
node.inputFunctions[2] = parseInt(select_inp3.value);
const value_inp3 = document.querySelector('.function3');
value = parseInt(value_inp3.value);
node.inputValues[2] = !value ? 0 : value;
const select_inp4 = document.querySelector('#node-input-function4');
node.inputFunctions[3] = parseInt(select_inp4.value);
const value_inp4 = document.querySelector('.function4');
value = parseInt(value_inp4.value);
node.inputValues[3] = !value ? 0 : value;
// Для отладки кода html-файла можно использовать console.log() и стандартные инструменты разработчика в браузере
console.log(node.inputFunctions); // Выводим массив выбранных функций по входам
console.log(node.inputValues); // Выводим массив выбранных значений по входам
},
oneditresize: function(size) { // Базовая функция срабатывает при изменении размеры окны настроек
},
oneditcancel: function () { // Базовая функция срабатывает при отмене изменений настроек узла
}
});
// Размещать свои функции вне базовых функций - не рекомендуется
</script>
<!-- Скрипт описания инструкции -->
<script type="text/html" data-help-name="my-new-node">
<p>Мой новый узел</p>
</script>
Скопируйте этот код и вставьте его в html файл вашего узла.
Этот файл отвечает за бизнес-логику вашего узла. Он экспортируется как модуль Node.js и используется для создания экземпляров узла.
module.exports = function(RED) {
function myNewNode(config) {
RED.nodes.createNode(this, config);
this.inputFunctions = config.inputFunctions; // Получаем значения массива выбранных функций из html файла объекта defaults
this.inputValues = config.inputValues; // Получаем значения массива числовых значений из html файла объекта defaults
this.inputCount = config.inputCount; // Получаем кол-во входов из html файла объекта defaults
this.outputCount = config.outputCount; // Получаем кол-во выходов из html файла объекта defaults
var node = this;
node.on('input', function(msg) { // Обработка входных данных
let input = msg.ports[node.id]; // Определение номера входа, на который пришли данные
let value = msg.payload; // Сохраняем данные в переменную
let result = 0;
node.warn(`input = ${input}, payload = ${value}`); // Выводим в окно отладки редактора логики номер входа и значение
switch(node.inputFunctions[input - 1]) { // Определяем тип функции
case 1: // 1 = сложение
result = math_sum(value, node.inputValues[input - 1]); // переменной result присваиваем результат функции сложения
sendMsg(node, input, result); // вызываем функцию отправки значения на выход с номеро = input
break
case 2: // 2 = вычитание
result = math_sub(value, node.inputValues[input - 1]);
sendMsg(node, input, result);
break
case 3: // 3 = умножение
result = math_mul(value, node.inputValues[input - 1]);
sendMsg(node, input, result);
break
case 4: // 4 = деление
result = math_div(value, node.inputValues[input - 1]);
sendMsg(node, input, result);
break
}
return;
});
function math_sum(a, b) { // вычисление суммы
return a + b;
}
function math_sub(a, b) { // вычисление разности
return a - b;
}
function math_mul(a, b) { // вычисление произведения
return a * b;
}
function math_div(a, b) { // вычисление частного
if(b === 0) return 0;
return a / b;
}
function sendMsg(node, out, cmd) { // функция отправки сообщения на выход.
const nMsg = { payload: cmd }; // создаём сообщение с payload = cmd.
let output_array = new Array(node.inputCount).fill(null); // создаём массив размером по кол-ву входов и заполняем все ячейки null - [null, null, null, null].
output_array[out - 1] = nMsg; // присваиваем ячейке массива, под номером out - 1, объект nMsg.
node.send(output_array); // отправляем значение на выход. Например: output_array = [null, nMsg, null, null], сообщение выйдет со второго выхода, там где не null.
}
}
RED.nodes.registerType("my-new-node", myNewNode);
}
Узел упакован как модуль Node.js. Модуль экспортирует функцию, которая вызывается при загрузке узла во время запуска среды выполнения. Эта функция вызывается с одним аргументом, RED, который предоставляет модулю доступ к API среды выполнения редактора логики.
Сам узел определяется функцией myNewNode, которая вызывается всякий раз, когда создается новый экземпляр узла. Ей передается объект, содержащий свойства узла, установленные в редакторе потоков.
Узел регистрирует обработчик события input, который вызывается всякий раз, когда на узел поступает сообщение. В обработчике события input, узел обрабатывает входящее сообщение и может изменить его перед тем, как передать дальше с помощью node.send(msg).
При создании узла первым шагом является вызов функции RED.nodes.createNode, которая инициализирует узел с базовыми функциями, такими как работа с потоками сообщениями, регистрация событий и управление состоянием.
После создания узла его необходимо зарегистрировать в системе с помощью RED.nodes.registerType. Это позволяет системе распознать узел и включить его в палитру узлов.
RED.nodes.registerType("my-new-node", myNewNode);
Узел может получать сообщения от других узлов в потоке с помощью слушателя события input. Этот слушатель вызывается каждый раз, когда узел получает сообщение.
Пример регистрации события input:
this.on('input', function(msg) {
// обрабатываем msg
});
msg — это объект сообщения, переданный от предыдущего узла.
Если при обработке сообщения произошла ошибка, её нужно обработать, чтобы другие узлы в потоке могли реагировать соответствующим образом.
Пример обработки ошибок:
this.on('input', function(msg) {
try {
// обрабатываем msg
} catch(err) {
// выводим ошибку
this.error(err, msg);
}
});
Для отправки сообщений другим узлам используется функция send. Если узел генерирует сообщение сам по себе (например, по событию), то вызов выглядит так:
let nMsg = { payload: “hello” };
this.send(nMsg);
Если msg равно null, сообщение не будет отправлено. Также можно повторно использовать уже полученное сообщение, модифицируя данные, а не создавать новый объект сообщения, чтобы сохранить его свойства для других узлов в потоке.
Если у узла несколько выходов, можно отправить разные сообщения на каждый выход, передав массив сообщений в send:
this.send([msg1, msg2]);
Можно отправить несколько сообщений на один выход, передав массив сообщений внутри массива для конкретного выхода:
this.send([[msgA1, msgA2, msgA3], msg2]);
Когда развертывается новый поток или узел удаляется, необходимо освободить ресурсы. Это делается с помощью события close.
node.on('close', function() {
// привести в порядок любое состояние
});
Среда выполнения отключит узел, если он не завершит работу за 15 секунд. Будет зарегистрирована ошибка, среда выполнения продолжит работать.
Для регистрации логов и уведомлений, связанных с поведением узла, можно использовать следующие функции:
Если узлу необходимо записать что-то в консоль, он может использовать одну из следующих функций:
this.log("Узел был инициализирован");
this.warn("Это предупреждение");
this.error("Произошла ошибка");
Сообщения warn и error также отправляются на вкладку отладки редактора потоков.
Во время работы узел может обмениваться информацией о состоянии с пользовательским интерфейсом редактора. Это делается вызовом функции status:
this.status({ fill: "red", shape: "ring", text: "disconnected" });
Оберните структуру в архив .tar или .tgz командой npm pack, без аргументов, находясь в рабочей директории. Перед запуском команды npm pack, предыдущий архив должен быть удалён или перемещён из рабочей папки. Для обновления уже имеющегося узла, в файле package.json необходимо изменить версию сборки. Загрузить узел той же версии не получится.
Далее выполните следующие шаги для его установки:
Если был загружен уже существующий, но более новой версии, необходимо перезагрузить редактор. Перейдите во вкладку “Узлы” и обновите ваш узел, нажав кнопку “Обновить”. Редактор попросит выполнить перезагрузку - соглашаемся.