Расширенные возможности
Эта страница — для разработчика драйвера. Основы (структура, команды, initialize) описаны в разделе «Как написать драйвер», разбор ответов — в разделе «Ответы устройства». Здесь — то, что нужно не каждому драйверу: вход на устройство, работа по HTTP, нестандартные протоколы, команды, которые драйвер отправляет сам, периодический опрос, освобождение ресурсов, доступ к памяти U-Logic и обновление параметров на лету.
Вход на устройство (auth-gate, автоматическая авторизация)
В коде драйвера этот механизм называют auth-gate (AuthGate): он включается флагом static requiresAuth = true и управляется методами _authReset(), _authMarkReady() и _authMarkFailed(). Всё о нём — в этом разделе.
Зачем это нужно
Многие устройства сразу после подключения требуют вход — логин и пароль или токен — и до его завершения не принимают команды. Что происходит, если отправить команду раньше:
Telnet и TCP. Команда, пришедшая до запроса пароля, молча пропадает: ни ответа, ни ошибки.
Веб-API (HTTP). Команда без токена получает ответ
401; если включены повторы, это превращается в поток бесполезных запросов.Устройства с шифрованием. Пока не вычислен ключ, команда уходит искажённой, и устройство может разорвать соединение.
Автоматическая авторизация решает это так: пока драйвер не сообщил, что вход выполнен, все команды пользователя складываются в очередь (не более 64), а после успешного входа уходят на устройство по порядку. Узлам в потоке (рабочем листе программы) про вход знать не нужно: кнопка, нажатая сразу после переподключения, сработает правильно, только с небольшой задержкой.
Нужна ли она вашему драйверу
Нужна не каждому драйверу: у Telnet и SSH вход часто выполняет сам транспорт по логину и паролю из своего окна.
Транспорт |
Входит ли сам |
Нужна ли авторизация в драйвере |
|---|---|---|
HTTP / HTTPS |
нет |
да — для входа по токену |
TCP |
нет |
да — если устройство требует вход по протоколу |
RS232 |
нет |
да — то же, что для TCP |
WebSocket |
нет |
да — для входа по токену после подключения |
Telnet или SSH, пароль (для SSH — пароль или ключ) заполнен в окне транспорта |
да |
нет — транспорт уже вошёл |
Telnet или SSH, пароль в окне транспорта пуст |
нет |
да — вход выполняет драйвер |
UDP, IR, Local |
нет |
не применимо |
Предупреждение
Вход должен выполнять кто-то один: либо транспорт, либо драйвер. Если пароль заполнен в окне транспорта Telnet или SSH, а в драйвере включена авторизация, вход выполнится дважды: у части устройств это заканчивается блокировкой или сбросом сеанса. В этом случае U-Logic выведет предупреждение о возможном двойном входе. Если вход делает драйвер — оставьте пароль в окне транспорта пустым и храните его в параметре драйвера (см. «Параметры подключения»).
Как включить
В драйвере объявите флаг static requiresAuth = true и, при необходимости, список команд-исключений static authCommands:
class MyAuthDriver extends BaseDriver {
static requiresAuth = true; // команды ждут окончания входа
static authCommands = ['sendPassword']; // эти команды в очередь не ставятся
// ...
}
После этого в драйвере доступны четыре метода:
Метод |
Когда вызывать |
Что делает |
|---|---|---|
|
первой строкой в |
сбрасывает состояние: снова «ждём вход», очередь пуста |
|
в |
накопленные команды уходят на устройство по порядку |
|
в |
очередь очищается, новые команды не отправляются до сброса |
|
для проверки и отладки |
возвращает |
Повторный вызов _authMarkReady() безвреден. После неудачного входа вызов _authMarkReady() снова разрешает отправку команд.
static authCommands — это команды, которые драйвер сам отправляет из parseResponse, когда видит запрос входа (например, sendPassword). Их нельзя ставить в очередь: именно они и завершают вход. Эти команды всё равно нужно объявить в static commands, иначе драйвер не сможет их отправить.
Пример: вход по паролю
const BaseDriver = require('base-driver');
class ExampleAuthDriver extends BaseDriver {
static metadata = {
name: 'ExampleAuth',
version: '1.0.0',
description: 'Вход по паролю',
parameters: [{ name: 'password', default: '' }] // пароль задаётся в окне Device Connection
};
static requiresAuth = true;
static authCommands = ['sendPassword'];
static commands = {
sendPassword: { description: 'Ответ на запрос пароля', parameters: [] },
getPower: { description: 'Запрос питания', parameters: [] }
};
initialize() {
this._authReset();
// Пароль не задан — вход не нужен
if (!this.config.password) this._authMarkReady();
this.publishCommand('getPower'); // встанет в очередь до входа
}
sendPassword() {
return { payload: `${this.config.password}\r\n` };
}
getPower() {
return { payload: 'PWR?\r\n' };
}
parseResponse(data) {
const text = String(data?.data ?? data?.payload ?? '');
if (/^Password:/.test(text)) {
if (this.config.password) this.publishCommand('sendPassword');
return { type: 'auth', authenticated: false };
}
if (/^Login OK/.test(text)) {
this._authMarkReady(); // накопленные команды уходят
return { type: 'auth', authenticated: true };
}
if (/^Login failed/.test(text)) {
this._authMarkFailed('Login failed');
return { type: 'auth', authenticated: false, reason: 'failed' };
}
return null;
}
}
module.exports = ExampleAuthDriver;
Как это выглядит на устройстве: после подключения оно присылает Password:, драйвер отвечает командой sendPassword, устройство отвечает Login OK, и тогда уходит команда getPower. В отладочной панели U-Logic, подключённой к выходу device - response - listener, придут два ответа типа auth: сначала authenticated: false, затем authenticated: true.
Тот же код входа подходит для нескольких транспортов (например, TCP, Telnet и RS232): отличается только то, заполнен ли пароль в окне транспорта (правило выше).
Другие варианты входа устроены так же, меняется только момент вызова _authMarkReady():
Вход по токену через веб-API.
initialize()сбрасывает состояние и отправляет запрос входа; устройство возвращает токен; драйвер запоминает его и вызывает_authMarkReady(). Каждая следующая команда добавляет токен в заголовки. Если приходит401, драйвер вызывает_authMarkFailed('токен истёк')или входит заново.Ключ шифрования. Вход — это готовность ключа.
initialize()сбрасывает состояние, вычисляет ключ и сразу вызывает_authMarkReady(): обращаться к устройству по сети не нужно. Команды, пришедшие, пока считался ключ, уйдут после того, как ключ готов.
Частые ошибки
Нет
this._authReset()первой строкойinitialize(). Система вызываетinitialize()при каждом подключении; без сброса после переподключения останется прошлое состояние входа.Нет
_authMarkReady()при успешном входе: команды копятся в очереди, после 64-й новые теряются.Нет
_authMarkFailed(причина)при неудаче: очередь продолжает расти, а причину сбоя трудно найти.Команда входа не объявлена в
static commandsили не перечислена вstatic authCommands.В
KeepAlive()отправляются команды, пока вход не выполнен. СамKeepAlive()вызывается напрямую, но команды, которые он отправляет черезpublishCommand, проходят через ту же очередь. Чтобы они не заполнили её, начните метод с проверки:if (this._authGetState() !== 'ready') return;.У HTTP
initialize()срабатывает только после первого ответа сервера (см. предупреждение в разделе «HTTP-драйверы» ниже). Поэтому не рассчитывайте, что запрос входа успеет уйти изinitialize()раньше первой команды: команду входа, перечисленную вstatic authCommands, можно отправить из потока.
Проверьте перед сохранением:
Объявлен
static requiresAuth = true.Команды входа перечислены и в
static commands, и вstatic authCommands.initialize()первой строкой вызывает_authReset(), а при пустом пароле —_authMarkReady().parseResponseвызывает_authMarkReady()при успешном входе и_authMarkFailed(причина)при неудачном.
HTTP-драйверы
HTTP работает в режиме «запрос — ответ» без постоянного соединения. Драйвер для такого устройства отличается от драйвера для TCP тем, что метод команды возвращает не строку для отправки, а описание запроса. Ещё ответ приходит с кодом состояния и заголовками.
Окно транспорта HTTP Config описано в разделе HTTP страницы «Узлы Dynamic Driver». Здесь — то, что связано с кодом драйвера.
Настройки транспорта, которые задаёт драйвер
Часть настроек можно записать в transportDefaults (ключ http), тогда пользователю достаточно нажать «Apply driver defaults» в окне HTTP Config. Ключи и подписи в окне:
Ключ |
Подпись в окне |
Значение |
|---|---|---|
|
«Сервер» |
базовый адрес; к нему добавляется |
|
«Метод» |
метод по умолчанию; метод из команды имеет приоритет |
|
«Полезная нагрузка» |
для GET: |
|
«Аутентификация», «Тип аутентификации» |
|
|
«Возвращать» |
вид тела ответа: |
|
«Сохранять соединение» |
не закрывать соединение между запросами |
|
«Отправлять ошибки» |
передавать ошибки запроса только в узлы перехвата ошибок |
|
«Проверять SSL сертификаты» |
если выключено, разрешены самоподписанные сертификаты |
|
«Следовать переадресациям» |
если выключено, переадресация (HTTP 301/302) запрещена |
|
«Тайм-аут запроса (мс)» |
сколько ждать ответ |
|
«Небезопасный HTTP парсер» |
снимает часть проверок разбора ответа |
Логин и пароль (или токен) вводятся в окне транспорта: для «Базовой» и Digest — «Имя пользователя» и «Пароль», для Bearer — «Токен Bearer». Значения можно брать из памяти U-Logic, см. раздел «Доступ к контексту U-Logic» ниже.
Что вернуть из метода команды
Метод команды возвращает объект с описанием запроса:
Поле |
Что значит |
Пример |
|---|---|---|
|
метод запроса |
|
|
путь, который добавляется к адресу из поля «Сервер» |
|
|
полный адрес с |
|
|
заголовки запроса |
|
|
тело запроса: строка, |
|
|
куки запроса |
|
|
то же, что одноимённые настройки окна, но для одной команды |
|
Если заголовок задан и в окне транспорта («Заголовки»), и в команде, выигрывает окно. Для метода GET payload по умолчанию в запрос не попадает: это зависит от поля «Полезная нагрузка».
Что приходит в parseResponse
В data приходят поля, о которых написано в разделе «Ответы устройства»: data.payload (тело), data.statusCode (код), data.headers (заголовки), data.responseUrl (итоговый адрес после переадресаций) и data.responseCookies (куки от сервера). Ответы с кодом 4xx и 5xx тоже приходят в parseResponse: проверяйте statusCode сами.
Вид data.payload зависит от поля «Возвращать»: при «UTF-8» это строка, при «Бинарные данные» — Buffer, при «JSON» — уже разобранный объект. Если в драйвере вызывается JSON.parse(data.payload), выбирайте «UTF-8», иначе вызов получит объект и завершится ошибкой.
Куки, которые прислал сервер, транспорт запоминает сам и отправляет в следующих запросах. Поле cookies в команде нужно, только чтобы добавить или заменить куку вручную.
Полный пример
Драйвер для условного веб-сервиса api.example: получить список, получить элемент, создать, изменить и удалить.
const BaseDriver = require('base-driver');
const { URLSearchParams } = require('url');
class CatalogDriver extends BaseDriver {
static metadata = {
name: 'Catalog',
manufacturer: 'Example',
version: '1.0.0',
description: 'Работа с каталогом через веб-API',
transportDefaults: {
http: { ret: 'txt', requestTimeout: 5000, followRedirects: true }
}
};
static commands = {
getItems: {
description: 'Получить страницу списка',
parameters: [
{ name: 'page', type: 'number', description: 'Номер страницы', default: 1 },
{ name: 'limit', type: 'number', description: 'Элементов на странице', default: 10 }
]
},
getItem: {
description: 'Получить элемент',
parameters: [{ name: 'id', type: 'string', description: 'Идентификатор', required: true }]
},
createItem: {
description: 'Создать элемент',
parameters: [{ name: 'name', type: 'string', description: 'Название', required: true }]
},
updateItem: {
description: 'Изменить название',
parameters: [
{ name: 'id', type: 'string', description: 'Идентификатор', required: true },
{ name: 'name', type: 'string', description: 'Новое название', required: true }
]
},
deleteItem: {
description: 'Удалить элемент',
parameters: [{ name: 'id', type: 'string', description: 'Идентификатор', required: true }]
}
};
getItems({ page = 1, limit = 10 }) {
const query = new URLSearchParams({ page: String(page), limit: String(limit) });
return { method: 'GET', path: `/api/items?${query}`, headers: { Accept: 'application/json' } };
}
getItem({ id }) {
return { method: 'GET', path: `/api/items/${encodeURIComponent(id)}`, headers: { Accept: 'application/json' } };
}
createItem({ name }) {
return {
method: 'POST',
path: '/api/items',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
payload: { name }
};
}
updateItem({ id, name }) {
return {
method: 'PUT',
path: `/api/items/${encodeURIComponent(id)}`,
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
payload: { name }
};
}
deleteItem({ id }) {
return { method: 'DELETE', path: `/api/items/${encodeURIComponent(id)}` };
}
parseResponse(data) {
const status = data?.statusCode;
const body = String(data?.payload ?? '');
if (status >= 400) {
return { type: 'error', statusCode: status, message: `HTTP ${status}` };
}
// 204 и пустое тело — успех без данных
if (!body) return { type: 'success', statusCode: status };
try {
return { type: 'success', statusCode: status, data: JSON.parse(body) };
} catch (error) {
return { type: 'parseError', statusCode: status, message: 'Ответ не является JSON' };
}
}
}
module.exports = CatalogDriver;
Чтобы проверить драйвер, нужен HTTP-сервис, который отвечает JSON: подставьте его адрес вместо api.example и приведите пути в методах к путям этого сервиса.
Добавьте узел Device Connection: «Драйвер» —
Catalog, «Транспорт» — HTTP. Создайте для него HTTP Config и укажите в поле «Сервер» адрес вашего сервиса (https://api.example).Добавьте узел
device - command, выберите это подключение и командуgetItems.Добавьте узел
device - response - listenerс тем же подключением и подключите к его выходу узел debug. Типы ответовsuccessиerrorназначьте на выходы (чтобы они появились в списке, объявите их вstatic responses, как описано в разделе «Ответы устройства»).Нажмите «Развернуть» и запустите
device - command.
Что вы увидите: в панели отладки появится сообщение, в msg.payload которого — type: 'success', код ответа в statusCode и разобранное тело в data. При ошибке сервиса придёт type: 'error' с кодом.
Приёмы и ловушки
Передача файла (multipart/form-data): укажите заголовок Content-Type: multipart/form-data, а в payload — объект, в котором файл описан как { value: Buffer, options: { filename, contentType } }:
uploadFile({ file, description }) {
return {
method: 'POST',
path: '/api/upload',
headers: { 'Content-Type': 'multipart/form-data' },
payload: {
file: { value: file, options: { filename: 'document.pdf', contentType: 'application/pdf' } },
description
}
};
}
Куки в запросе добавляются так: cookies: { lang: 'ru' }. Если нужно задать куку с другими параметрами, передайте объект: cookies: { session: { value: 'abc' } }.
Предупреждение
initialize() у HTTP срабатывает после первого полученного ответа сервера, а не при запуске узла: постоянного соединения нет, и «подключённым» транспорт считается, когда сервер ответил. Не используйте initialize() как единственный способ послать первый запрос.
Сложные протоколы
Некоторые устройства обмениваются не текстом, а байтами: кадр начинается и заканчивается служебными байтами и содержит контрольную сумму. В команде верните Buffer:
static commands = {
setLevel: {
description: 'Установить уровень',
parameters: [{ name: 'level', type: 'number', required: true, min: 0, max: 255 }]
}
};
setLevel({ level }) {
const sum = (0x02 + level + 0x03) & 0xFF; // контрольная сумма
return { payload: Buffer.from([0x02, level, 0x03, sum]) };
}
Ответ разберите в parseResponse по байтам:
parseResponse(data) {
const frame = data?.data;
if (!Buffer.isBuffer(frame) || frame.length < 4) return null;
if (frame[0] !== 0x02 || frame[2] !== 0x03) return null; // не наш кадр
return { type: 'level', value: frame[1] };
}
Что учесть:
В окне TCP в поле «Возвращать» выберите «Байты (Buffer)» (у UDP — «Формат данных», Транспорты: как связаться с устройством), иначе ответ придёт строкой и байты со значением выше 127 исказятся.
Описание ответа через
matcherдля двоичных данных не подходит: данные для регулярного выражения превращаются в текст UTF-8. ИспользуйтеparseResponse, как выше.Поле «Делимитер» оставьте пустым, если у кадров нет текстового разделителя. Тогда данные приходят так, как отдала их сеть: кадр может прийти по частям или два кадра — слитно. Если так возможно, копите байты в поле драйвера и разбирайте целые кадры.
Несколько устройств на одном подключении
Если протокол позволяет управлять несколькими устройствами по одной линии (например, на шине адреса 1, 2, 3), добавьте номер устройства параметром команды и подставляйте его в кадр:
static commands = {
setPower: {
description: 'Питание устройства с адресом',
parameters: [
{ name: 'value', type: 'boolean', required: true },
{ name: 'address', type: 'number', description: 'Адрес устройства', default: 1 }
]
}
};
setPower({ value, address = 1 }) {
return { payload: `${address}:PWR ${value ? 'ON' : 'OFF'}\r\n` };
}
В ответе верните тот же адрес отдельным полем и опишите его как ключ: тогда ответы разных устройств можно различать (см. раздел «Описание полей ответа для интерфейса»). Если устройств мало, можно вместо этого создать по одному узлу Device Connection на каждое: у каждого подключения свой экземпляр драйвера со своим состоянием.
Инициирование команд и publishCommand
Иногда драйвер должен сам отправить команду устройству: при initialize(), по таймеру, в ответ на событие. Для этого есть метод publishCommand:
publishCommand(команда, параметры = {})
Команда уходит по тому же пути, что и команда из узла device - command, поэтому она должна быть объявлена в static commands.
Запрос состояния после подключения:
initialize() {
this.publishCommand('getStatus');
this.publishCommand('getVolume');
}
Команда с параметрами: от объявления до байтов
Параметры передаются вторым аргументом, объектом: ключи в нём совпадают с name параметров из static commands. В метод команды этот объект приходит как params, и метод подставляет значения в строку для устройства.
Объявите команду и её параметр:
static commands = { setVolume: { description: 'Установить громкость', parameters: [{ name: 'level', type: 'number', min: 0, max: 100, required: true }] } };
В методе команды возьмите значение из
paramsи подставьте его в строку:setVolume(params) { const level = Math.round(Number(params.level)); if (Number.isNaN(level) || level < 0 || level > 100) return null; return { payload: `VOL ${level}\r\n` }; }
Вызовите команду из драйвера, передав значение:
this.publishCommand('setVolume', { level: 25 });
Что уйдёт на устройство: строка VOL 25 и перевод строки. Ключ level в вызове — то же имя, что name: 'level' в объявлении и params.level в методе. Если ключ назвать иначе, в методе params.level будет undefined, и команда не уйдёт.
Подставить значение из переменной
Обычно значение не пишут в вызове числом, а берут из того, что драйвер уже знает. Во всех примерах ниже команды setVolume, getLevel и setInput объявлены в static commands со своими параметрами, так же, как setVolume выше.
Из параметра подключения. Пользователь задаёт значение в окне Device Connection («Параметры драйвера»), драйвер читает его из this.config:
static metadata = {
name: 'ExampleAmp',
version: '1.0.0',
parameters: [{ name: 'startVolume', default: 30 }]
};
initialize() {
// громкость, с которой зал включается после подключения
this.publishCommand('setVolume', { level: Number(this.config.startVolume) });
}
Из ответа устройства. Устройство сообщило, сколько у него каналов, — драйвер запрашивает уровень каждого:
parseResponse(data) {
const text = String(data?.data ?? data?.payload ?? '');
const m = text.match(/^CHANNELS (\d+)/);
if (m) {
const count = Number(m[1]);
for (let ch = 1; ch <= count; ch++) {
this.publishCommand('getLevel', { channel: ch });
}
return { type: 'channels', count };
}
return null;
}
Несколько параметров сразу. Значения можно вычислить заранее и передать одним объектом:
const input = this.config.defaultInput || 'HDMI1';
const output = 1;
this.publishCommand('setInput', { source: input, output });
Готовым объектом. Если команда и параметры уже собраны в объект, передайте его первым аргументом:
const cmd = { command: 'setVolume', parameters: { level: 40 } };
this.publishCommand(cmd);
Что важно знать о параметрах publishCommand
В метод команды приходят те же ключи и значения, которые вы передали. Значения по умолчанию (
default) изstatic commandsсами не подставляются, а строка'25'не превращается в число: это делает только узелdevice - command. Передавайте все параметры, которые нужны методу, и приводите типы сами (Number(...)), как в примерах выше.Границы
minиmaxпри вызове из драйвера тоже никто не проверяет: проверяйте значение в методе команды.Если в драйвере включён вход на устройство, команды из
publishCommandдо входа встают в очередь вместе с остальными и уходят после_authMarkReady()(кроме команд изstatic authCommands).Пока транспорт не подключён, команда из
publishCommandне отправляется и в очередь не встаёт. Поэтому команды, которые нужны сразу после подключения, вызывайте вinitialize(): он выполняется при каждом подключении.
Чтобы подставить в команду значение с панели (например, положение ползунка), параметры собирают в потоке, а не в драйвере (см. «Параметры команд из потока»).
Предупреждение
Не вызывайте publishCommand (и publishResponse) в конструкторе: драйвер подключается к узлу только после создания, и до этого вызовы ничего не делают. Выполняйте такие действия в initialize().
Про publishResponse — публикацию нескольких ответов — читайте в разделе «Ответы устройства».
Автоматический Keep-Alive
Узел Device Connection может сам периодически вызывать метод драйвера KeepAlive(), чтобы опрашивать устройство и не давать ему закрыть соединение по неактивности. В окне узла:
«Keep-Alive» — флаг «Периодически проверять соединение» (по умолчанию выключен);
«Интервал Keep-Alive» — период в секундах (по умолчанию 30, минимум 1); поле видно, только когда флаг включён.
Как это работает:
Флаг включён — узел запускает таймер с заданным интервалом. У постоянных соединений (TCP, Telnet, SSH, WebSocket) таймер стартует после подключения и останавливается при разрыве, у остальных транспортов — сразу.
По таймеру система вызывает метод
KeepAlive()драйвера.Внутри
KeepAlive()вы отправляете одну или несколько команд черезpublishCommand.
KeepAlive() {
this.publishCommand('getStatus');
this.publishCommand('getVolume');
}
Лучше, чтобы KeepAlive() ничего не возвращал, а отправлял команды из static commands через publishCommand. Если метод всё же вернёт { payload: ... }, система запишет это в транспорт. Но такой способ обходит список команд, и в новых драйверах его лучше не использовать. Если драйвер не реализует KeepAlive(), при включённом флаге ничего не отправляется.
Как убедиться: включите «Driver Debug» в узле Device Connection. В панели отладки будут видны команды, которые уходят по таймеру.
Управление ресурсами и onDestroy
Если драйвер создаёт собственные таймеры (setInterval), открывает дополнительные соединения или подписывается на события, освободите их при остановке драйвера. Для этого реализуйте метод onDestroy(): система вызывает его при остановке или удалении узла. Не переопределяйте метод destroy() напрямую: иначе потеряется очистка, которую выполняет система.
initialize() {
// initialize вызывается при каждом переподключении — сначала снимаем прошлый таймер
if (this.pollTimer) { clearInterval(this.pollTimer); this.pollTimer = null; }
this.pollTimer = setInterval(() => this.publishCommand('getStatus'),
this.config.pollInterval || 5000);
}
onDestroy() {
if (this.pollTimer) { clearInterval(this.pollTimer); this.pollTimer = null; }
if (this.extraSocket) this.extraSocket.destroy();
// Если очистка асинхронная — верните Promise, система его дождётся:
// return this.closeEverything();
}
Ошибка внутри onDestroy() записывается в журнал U-Logic и остановку не прерывает. Для обычного периодического опроса вместо собственного таймера проще использовать встроенный Keep-Alive (см. выше): тогда onDestroy() не нужен.
Доступ к контексту U-Logic
Память U-Logic (переменные global и env) доступна и в окнах транспортов, и в коде драйвера. Память потока (flow) у подключения недоступна: используйте global. С такой памятью адрес, порт и пароль не нужно вшивать в драйвер, а токен можно положить в общую память из другого узла.
В окнах транспортов и Device Connection
Поля подключения в окнах TCP, UDP, SSH, Telnet и HTTP принимают текст или значение из памяти. Слева от поля выберите тип значения — global или переменная окружения env — и впишите в поле только имя переменной, без префикса и без фигурных скобок.
Транспорт |
Поля с выбором типа |
|---|---|
TCP |
«Хост», «Порт» |
UDP |
«Внешний порт», «Номер локального порта», «Исходящий IP» |
SSH, Telnet |
«Хост», «Порт», «Логин», «Пароль» |
HTTP |
«Сервер», «Имя пользователя», «Пароль» |
Например, в поле «Хост» выберите global и впишите deviceIp; в поле «Порт» — global и devicePort; в поле «Логин» — env и DEVICE_USER. RS232 и IR работают через встроенный контроллер, контекстные переменные для них не применяются.
Если переменная ещё не задана при запуске, подключение не установится, а транспорт повторит попытку через интервал из поля «Интервал переподкл. (мс)» (при включённом «Автоподключении»). Как только значение появится, подключение произойдёт само. Значения в память кладёт, например, узел function: global.set('deviceIp', '192.168.1.100').
Тот же выбор типа есть у строк в списке «Параметры драйвера» узла Device Connection: значение параметра можно взять из global или env, см. раздел «Динамическое обновление параметров» ниже.
В коде драйвера
Объекты памяти доступны в коде драйвера напрямую, без this., как в узле function:
Объект |
Что это |
Методы |
|---|---|---|
|
общая память всех потоков |
|
|
недоступна: у подключения нет памяти потока, используйте |
— |
|
память самого узла Device Connection |
|
|
переменные окружения |
|
|
сведения об узле и его журнал |
|
|
вывод в журнал |
|
Сообщения console попадают в журнал U-Logic с пометкой имени драйвера. Вызова node.send() в драйвере нет: чтобы отправить ответ или команду, используйте publishResponse и publishCommand.
const siteId = global.get('siteId'); // общая память
global.set('lastActivity', Date.now());
context.set('requestCount', (context.get('requestCount') || 0) + 1); // память узла
const token = env.get('DEVICE_TOKEN'); // переменная окружения
node.log('Информационное сообщение');
node.status({ fill: 'green', shape: 'dot', text: 'connected' });
Если один драйвер используется в нескольких узлах Device Connection, у каждого будет своя память context, а global — общая для всех.
Пример: драйвер, который запоминает состояние
class StatefulDriver extends BaseDriver {
constructor(options) {
super(options);
// состояние хранится в this.state
this.state = {};
}
initialize() {
// Восстанавливаем состояние, сохранённое раньше
this.state = global.get('deviceState') || this.state;
}
setPower({ value }) {
this.state.power = value;
global.set('deviceState', this.state); // состояние переживёт перезапуск узла
node.log(`Питание: ${value}`);
return { payload: `PWR ${value ? 'ON' : 'OFF'}\r\n` };
}
}
Динамическое обновление параметров
Значения параметров драйвера (из metadata.parameters) вычисляются один раз: при запуске или развёртывании. Если параметр берёт значение из global или env, оно читается именно в этот момент. Автоматического отслеживания нет: если позже значение в global изменится, драйвер об этом не узнает.
Чтобы получить свежие значения, запросите обновление явно:
Вызовите
this.requestParamsUpdate().Система заново вычислит значения параметров из настроек узла, обновит
this.configи вызовет методonParamsUpdated(params)вашего драйвера, если он есть.
constructor(options) {
super(options);
this.apiKey = this.config.apiKey;
}
parseResponse(data) {
if (data?.statusCode === 401) {
// Ключ мог измениться — перечитать параметры
this.requestParamsUpdate();
}
return null;
}
// Вызывается после обновления параметров; params — объект со свежими значениями
onParamsUpdated(params) {
if (this.apiKey !== params.apiKey) {
this.apiKey = params.apiKey;
this.publishCommand('login'); // повторный вход с новым ключом
}
}
Что вы увидите: после смены значения в памяти и вызова requestParamsUpdate() в драйвере сработает onParamsUpdated с новым значением.
Параметры команд из потока
Таблицы полей параметра и то, как они выглядят в окне device - command, описаны в разделе «Команды». Здесь — то, что относится к потоку.
Значения параметров можно задать не в окне узла, а сообщением. Узел function перед device - command записывает их в msg.parameters, а имя команды — в msg.command:
msg.command = 'multiSet';
msg.parameters = { brightness: 80, contrast: 55 };
return msg;
Значения из msg.parameters перекрывают те, что введены в окне. Через msg.parameters в команду подставляют значение с панели. Например, положение ползунка пересчитывают в децибелы выражением в узле change и кладут в msg.parameters; пошагово это разобрано в разделе «Группа 1. Команды: кнопка и ползунок → процессор» страницы «Первая программа по шагам». Узел приводит числа и булевы значения к типу, заявленному в static commands, но границы не проверяет. Подробности и что делать в методе команды — в предупреждении раздела «Команды».
Если команда «не отправляется как положено», проверьте по порядку: метод называется так же, как ключ в static commands, и возвращает { payload }, строку или Buffer. Полный перечень причин — в разделе «Если не работает». Если не срабатывают регулярные выражения ответов, проверьте выражение на образце ответа, как описано в разделе «Если ответ не приходит» страницы «Ответы устройства».