Расширенные возможности

Эта страница — для разработчика драйвера. Основы (структура, команды, 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'];  // эти команды в очередь не ставятся
  // ...
}

После этого в драйвере доступны четыре метода:

Метод

Когда вызывать

Что делает

this._authReset()

первой строкой в initialize()

сбрасывает состояние: снова «ждём вход», очередь пуста

this._authMarkReady()

в parseResponse, когда вход прошёл

накопленные команды уходят на устройство по порядку

this._authMarkFailed(причина)

в parseResponse, когда вход не удался

очередь очищается, новые команды не отправляются до сброса

this._authGetState()

для проверки и отладки

возвращает 'pending' (ждём вход), 'ready' (вход выполнен) или 'failed' (вход не удался)

Повторный вызов _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, можно отправить из потока.

Проверьте перед сохранением:

  1. Объявлен static requiresAuth = true.

  2. Команды входа перечислены и в static commands, и в static authCommands.

  3. initialize() первой строкой вызывает _authReset(), а при пустом пароле — _authMarkReady().

  4. parseResponse вызывает _authMarkReady() при успешном входе и _authMarkFailed(причина) при неудачном.

HTTP-драйверы

HTTP работает в режиме «запрос — ответ» без постоянного соединения. Драйвер для такого устройства отличается от драйвера для TCP тем, что метод команды возвращает не строку для отправки, а описание запроса. Ещё ответ приходит с кодом состояния и заголовками.

Окно транспорта HTTP Config описано в разделе HTTP страницы «Узлы Dynamic Driver». Здесь — то, что связано с кодом драйвера.

Настройки транспорта, которые задаёт драйвер

Часть настроек можно записать в transportDefaults (ключ http), тогда пользователю достаточно нажать «Apply driver defaults» в окне HTTP Config. Ключи и подписи в окне:

Ключ

Подпись в окне

Значение

server

«Сервер»

базовый адрес; к нему добавляется path из команды

method

«Метод»

метод по умолчанию; метод из команды имеет приоритет

paytoqs

«Полезная нагрузка»

для GET: ignore — игнорировать, query — добавить в запрос, body — добавить в тело

useAuth, authType

«Аутентификация», «Тип аутентификации»

basic («Базовая»), digest или bearer; для Bearer поле пароля называется «Токен Bearer»

ret

«Возвращать»

вид тела ответа: txt — строка (UTF-8), bin — байты, obj — разобранный JSON

persist

«Сохранять соединение»

не закрывать соединение между запросами

senderr

«Отправлять ошибки»

передавать ошибки запроса только в узлы перехвата ошибок

rejectUnauthorized

«Проверять SSL сертификаты»

если выключено, разрешены самоподписанные сертификаты

followRedirects

«Следовать переадресациям»

если выключено, переадресация (HTTP 301/302) запрещена

requestTimeout

«Тайм-аут запроса (мс)»

сколько ждать ответ

insecureHTTPParser

«Небезопасный HTTP парсер»

снимает часть проверок разбора ответа

Логин и пароль (или токен) вводятся в окне транспорта: для «Базовой» и Digest — «Имя пользователя» и «Пароль», для Bearer — «Токен Bearer». Значения можно брать из памяти U-Logic, см. раздел «Доступ к контексту U-Logic» ниже.

Что вернуть из метода команды

Метод команды возвращает объект с описанием запроса:

Поле

Что значит

Пример

method

метод запроса

'GET', 'POST', 'PUT', 'DELETE'

path

путь, который добавляется к адресу из поля «Сервер»

'/api/items/5'

url

полный адрес с http:// или https://; поле «Сервер» и path игнорируются

'https://api.example/status'

headers

заголовки запроса

{ 'Accept': 'application/json' }

payload

тело запроса: строка, Buffer, число или объект (объект уходит как JSON, а при заголовке Content-Type: application/x-www-form-urlencoded — как форма)

{ name: 'Стол' }

cookies

куки запроса

{ lang: 'ru' }

requestTimeout, followRedirects, rejectUnauthorized

то же, что одноимённые настройки окна, но для одной команды

3000

Если заголовок задан и в окне транспорта («Заголовки»), и в команде, выигрывает окно. Для метода 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 и приведите пути в методах к путям этого сервиса.

  1. Добавьте узел Device Connection: «Драйвер» — Catalog, «Транспорт» — HTTP. Создайте для него HTTP Config и укажите в поле «Сервер» адрес вашего сервиса (https://api.example).

  2. Добавьте узел device - command, выберите это подключение и команду getItems.

  3. Добавьте узел device - response - listener с тем же подключением и подключите к его выходу узел debug. Типы ответов success и error назначьте на выходы (чтобы они появились в списке, объявите их в static responses, как описано в разделе «Ответы устройства»).

  4. Нажмите «Развернуть» и запустите 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, и метод подставляет значения в строку для устройства.

  1. Объявите команду и её параметр:

    static commands = {
      setVolume: {
        description: 'Установить громкость',
        parameters: [{ name: 'level', type: 'number', min: 0, max: 100, required: true }]
      }
    };
    
  2. В методе команды возьмите значение из 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` };
    }
    
  3. Вызовите команду из драйвера, передав значение:

    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); поле видно, только когда флаг включён.

Как это работает:

  1. Флаг включён — узел запускает таймер с заданным интервалом. У постоянных соединений (TCP, Telnet, SSH, WebSocket) таймер стартует после подключения и останавливается при разрыве, у остальных транспортов — сразу.

  2. По таймеру система вызывает метод KeepAlive() драйвера.

  3. Внутри 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:

Объект

Что это

Методы

global

общая память всех потоков

get(ключ), set(ключ, значение), keys()

flow

недоступна: у подключения нет памяти потока, используйте global

—

context

память самого узла Device Connection

get(ключ), set(ключ, значение), keys()

env

переменные окружения

get(имя)

node

сведения об узле и его журнал

id, name, log(), warn(), error(), status()

console

вывод в журнал

log(), info(), warn(), error()

Сообщения 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 изменится, драйвер об этом не узнает.

Чтобы получить свежие значения, запросите обновление явно:

  1. Вызовите this.requestParamsUpdate().

  2. Система заново вычислит значения параметров из настроек узла, обновит 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. Полный перечень причин — в разделе «Если не работает». Если не срабатывают регулярные выражения ответов, проверьте выражение на образце ответа, как описано в разделе «Если ответ не приходит» страницы «Ответы устройства».