Как написать драйвер

Драйвер объясняет Dynamic Driver, как говорить с вашим устройством: какие команды у него есть, в каком виде их отправлять и как понимать ответы. Эта страница — для разработчика драйвера: из чего он состоит, что где писать и как подключить готовый код к потоку (рабочему листу программы в U-Logic). Пишется драйвер на JavaScript прямо в редакторе драйверов.

Остальное — на соседних страницах:

Что нужно до начала

  1. Знание JavaScript.

  2. Описание протокола устройства: какие команды оно принимает, в каком виде (текст, байты, HTTP-запросы), какие ответы присылает и чем каждое сообщение заканчивается.

  3. Доступ к U-Logic (редактору программы управления) на контроллере, где открывается редактор драйверов.

Файлы создавать не нужно: код драйвера пишется и сохраняется в редакторе. Имя, под которым вы его сохраните, — идентификатор драйвера: именно оно появится в списке «Драйвер» узла Device Connection (драйвер плюс транспорт к одному конкретному устройству). В имени допустимы латинские буквы, цифры, дефис и подчёркивание, расширение не пишут. metadata.name на выбор драйвера не влияет, это только подпись.

Из чего состоит драйвер

Драйвер — класс, который расширяет BaseDriver. BaseDriver берёт на себя связь с системой и транспортом (способом связи с устройством: TCP, UDP, HTTP, Telnet, SSH, RS232 и другие), вам остаётся описать устройство.

Часть

Что делает

Где описана

static metadata

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

раздел «Метаданные»

static commands

список команд, из которых строится узел device - command

раздел «Команды»

методы команд

превращают команду и её параметры в данные для устройства

раздел «Методы команд»

static responses, parseResponse

распознают ответы устройства

Ответы устройства

initialize

выполняется при каждом подключении

раздел «Метод initialize»

служебные методы

KeepAlive, onParamsUpdated, onDestroy

раздел «Служебные методы»

Минимальная заготовка:

const BaseDriver = require('base-driver');

class MyDeviceDriver extends BaseDriver {
  static metadata = { name: 'MyDevice' };   // описание драйвера
  static commands = {};                      // команды добавите позже

  constructor(options) {
    super(options);
    this.state = {};
  }
}

module.exports = MyDeviceDriver;

Обязательны require('base-driver'), module.exports, static metadata и static commands (пусть пока и пустой): без них драйвер не загрузится. this.state = {} нужен, если драйвер хранит состояние: BaseDriver сам this.state не создаёт. Первая же команда, которая пишет в this.state, закончится ошибкой Cannot set properties of undefined.

Что доступно из кода драйвера

Подключать модули можно только из этого списка:

Модуль

Что доступно

base-driver, events, net, tls, http, https, dgram, dns, url, crypto, querystring, xml2js

модуль целиком

buffer

Buffer, from, alloc, concat

util

inspect, format, promisify

path

basename, dirname, extname, join, normalize, sep

Таймеры setTimeout, setInterval и setImmediate доступны без подключения. Другие модули подключить нельзя, выполнять код из строки тоже нельзя. Объекты global и env — память U-Logic — описаны в разделе «Доступ к контексту» на странице Расширенные возможности. Память потока (flow) у подключения недоступна, используйте global.

Метаданные (metadata)

Метаданные показываются в окне Device Connection после выбора драйвера и в карточке «Информация о драйвере» в редакторе.

static metadata = {
  name: 'MyDevice',
  manufacturer: 'MyCompany',
  version: '1.0.0',
  description: 'Драйвер для устройства MyDevice от MyCompany',
  model: 'MD-1000',
  parameters: [ /* см. ниже */ ],
  transportDefaults: { /* см. ниже */ }
};

Поле

Тип

Назначение

name

строка

подпись драйвера (обязательно)

manufacturer

строка

производитель

version

строка

версия драйвера, например 1.0.0

description

строка

для чего драйвер

model

строка

модель устройства

category

строка

необязательная пометка; окна узлов её не используют

parameters

массив

параметры подключения, см. ниже

transportDefaults

объект

настройки транспорта по умолчанию, см. ниже

Параметры подключения (parameters)

Параметры — значения, которые пользователь задаёт один раз в окне Device Connection, в списке «Параметры драйвера»: пароль доступа к API, номер зоны, интервал опроса. При выборе драйвера в список добавляются строки с именами и значениями по умолчанию из parameters. Пользователь может изменить значение и выбрать его тип: строка, число, булево, global или env (память потока у подключения недоступна, используйте global). Перед работой значения из контекста подставляются, и драйвер получает готовое значение.

parameters: [
  { name: 'apiKey', default: '' },
  { name: 'pollInterval', default: 1000 }
]

Окно Device Connection использует из описания параметра только name и default. Поля type, label, description, required, min, max и enum можно писать как пояснение для того, кто будет читать код, но на окно они не влияют.

Примечание

В окне Device Connection выпадающего списка для enum нет. Для логина и пароля устройства используйте поля «Логин» и «Пароль» окна транспорта, если они есть.

В коде параметры лежат в this.config сразу после создания драйвера. Конструктор переопределять не нужно:

initialize() {
  this.pollInterval = Number(this.config.pollInterval) || 1000;
  this.apiKey = this.config.apiKey;
}

Если пользователь меняет значения из контекста во время работы, актуальные значения запрашиваются отдельно — см. раздел «Динамическое обновление параметров» на странице Расширенные возможности.

Настройки транспорта по умолчанию (transportDefaults)

У протокола устройства обычно свой порт, разделитель сообщений и приглашения входа. Чтобы пользователь не вводил их вручную, опишите их в transportDefaults:

transportDefaults: {
  tcp: { port: 23, delimiter: '\r\n' },
  telnet: {
    port: 23,
    usernamePrompt: '[Ll]ogin[: ]*$',
    passwordPrompt: '[Pp]assword[: ]*$',
    shellPrompt: '[$#>] ?$'
  }
}

Как это работает: в окне транспорта (TCP Config, Telnet Config и других) для выбранного драйвера появляется кнопка «Apply driver defaults». Нажатие записывает перечисленные значения в поля окна, перезаписывая то, что там было. Поля, которых нет в transportDefaults, не меняются. Кнопка видна, только если драйвер описал настройки для выбранного вида транспорта.

Ключи для каждого транспорта:

Транспорт

Ключи

tcp

host, port, delimiter, delimiterTimeout, keepDelimiter, bufferSize, reconnectInterval, ret, autoConnect, debug

telnet

host, port, usernamePrompt, passwordPrompt, shellPrompt, appendLineEnding, delimiter, delimiterTimeout, keepDelimiter, bufferSize, reconnectInterval, autoConnect, debug

ssh

host, port, shellPrompt, appendLineEnding, delimiter, delimiterTimeout, keepDelimiter, bufferSize, reconnectInterval, autoConnect, debug

udp

port, outport, outip, ipv, datatype, delimiter, delimiterTimeout, keepDelimiter, bufferSize, autoConnect, debug

http

server, method, ret, paytoqs, persist, senderr, insecureHTTPParser, rejectUnauthorized, followRedirects, requestTimeout, useAuth, authType

Что означают ключи (в скобках — подпись поля в окне транспорта):

Ключ

Значение

host, port

адрес и порт устройства («Хост», «Порт»)

delimiter

разделитель сообщений: '\r\n', '\n' и т. п. («Делимитер»)

delimiterTimeout

через сколько миллисекунд отдать неполное сообщение; 0 — по таймауту не отдавать («Таймаут делимитера»)

keepDelimiter

оставлять разделитель в сообщении («Сохранять делимитер»)

bufferSize

размер буфера, КБ («Размер буфера»); по умолчанию 64

reconnectInterval

пауза между попытками подключиться, мс («Интервал переподкл.»)

autoConnect

подключаться при запуске и переподключаться при разрыве («Автоподключение»)

debug

отладочные сообщения транспорта

ret

вид данных: для tcp — buffer (байты) или string (строка); для http — txt, bin или obj (JSON) («Возвращать»)

usernamePrompt, passwordPrompt, shellPrompt

регулярные выражения: запрос логина, запрос пароля, приглашение командной строки («Выражение для логина» и т. д.)

appendLineEnding

добавлять к команде окончание строки автоматически («Автодобавлять окончание команды»)

port, outport, outip у udp

внешний порт; локальный порт (если задан, в окне включается «Указать вручную»); исходящий IP

ipv, datatype у udp

версия IP, 4 или 6; формат данных: utf8, base64, hex

остальные ключи http

параметры HTTP-запроса — см. Расширенные возможности

Разделитель важен: транспорт режет поток байтов на сообщения именно по нему, и каждое сообщение становится одним ответом для драйвера. Подробнее о неполных сообщениях и поле «Таймаут делимитера» — на странице Ответы устройства.

Команды (static commands)

static commands — каталог действий, которые устройство умеет выполнять. По нему система строит узел device - command: список «Команда» и поля «Параметры команды».

static commands = {
  setInput: {
    description: 'Выбор входного источника',
    returns: { format: 'text', example: 'INPUT HDMI1' },
    parameters: [
      {
        name: 'source',
        type: 'string',
        description: 'Имя источника',
        required: true,
        enum: ['HDMI1', 'HDMI2', 'USB'],
        default: 'HDMI1'
      }
    ]
  }
};

Поля команды:

Поле

Назначение

description

показывается в окне узла в строке «Описание»

returns

необязательно: format и example показываются в строке «Возвращает»

parameters

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

Поля параметра:

Поле

Назначение

name

имя; по нему параметр приходит в метод команды

type

string, number или boolean

description

подсказка в поле ввода

default

значение по умолчанию

required

обязательный ли параметр

min, max

границы для числа

enum

список допустимых значений

Что увидит пользователь в окне device - command:

Описание параметра

Поле в окне

type: 'number'

числовое поле; min и max ограничивают ввод в самом поле

type: 'boolean'

список True / False

есть enum

выпадающий список значений; пустой пункт «– Выберите значение –» есть, только если параметр не required

type: 'string'

текстовое поле

Предупреждение

min, max, enum и required — подсказки для окна узла. Значения могут прийти и мимо окна (в msg.parameters из потока или из контекста), и тогда никто, кроме драйвера, их не проверит. Узел лишь приводит числа и булевы значения к заявленному типу. Проверяйте и ограничивайте значения в теле метода команды.

Список команд — только для действий над устройством. Служебные методы (см. ниже) в него не вносят.

Методы команд

Для каждой команды из static commands напишите метод с тем же именем. Метод получает объект с параметрами и возвращает то, что нужно отправить устройству:

setInput(params) {
  const { source } = params;
  // Окно узла значение не проверяет — проверяем сами
  if (!['HDMI1', 'HDMI2', 'USB'].includes(source)) return null;
  this.state.input = source;
  return { payload: `INPUT ${source}\r\n` };
}

Что можно вернуть:

Возврат

Что произойдёт

{ payload: строка или Buffer }

содержимое payload отправится в транспорт как есть

строка или Buffer

то же самое

null

команда не отправляется, и в журнале U-Logic об этом не пишется. Если пользователю нужно знать причину (например, неверный параметр), сообщите её из метода сами: console.warn('…'). Предупреждение «Драйвер … не поддерживает команду …» появляется, только когда команды нет в static commands

Promise (асинхронный метод)

команда уйдёт, когда он завершится; если за 30 секунд не завершился — отбрасывается

Предупреждение

Число или объект без поля payload не сериализуются: объект превратится в строку [object Object]. Всегда возвращайте { payload }, строку или Buffer.

Форма результата зависит от транспорта: для TCP, UDP, SSH, Telnet, RS232 и Local — это данные, а для HTTP — описание запроса (метод, путь, тело); про HTTP см. Расширенные возможности.

Команда не уходит, пока нет соединения: если транспорт не подключён, она молча отбрасывается. Если команда «не доходит» — проверьте статус «Подключено» у device - response - listener (device - response - listener: принять ответы).

Метод initialize

initialize система вызывает после каждого успешного подключения: при первом и при каждом переподключении. Он нужен, чтобы сразу узнать состояние устройства или выполнить вход:

initialize() {
  this.publishCommand('getStatus');
}

publishCommand отправляет команду из самого драйвера — так, как если бы её прислал узел device - command; подробности — в разделе «Инициирование команд» на странице Расширенные возможности. Раз initialize выполняется при каждом подключении, он должен быть безопасен при повторе: завершайте таймеры и ресурсы, созданные прошлым вызовом, прежде чем создавать новые.

Предупреждение

Метод this.sendCommand(имя, параметры) только форматирует команду и возвращает результат — в транспорт он ничего не отправляет. Чтобы отправить команду из драйвера, используйте this.publishCommand.

Служебные методы

Эти методы система вызывает сама, это не команды устройства:

Метод

Когда вызывается

Подробнее

initialize

после подключения

выше

parseResponse

когда ответ не распознал ни один matcher

Ответы устройства

KeepAlive

по таймеру Keep-Alive

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

onParamsUpdated

после обновления параметров

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

onDestroy

при остановке узла: развёртывание, удаление

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

Реализуйте их как обычные методы класса. Не объявляйте их в static commands и не вызывайте через sendCommand или publishCommand: в sendCommand попадают только команды из static commands, остальное возвращает null.

Полный пример драйвера

Условное устройство принимает текстовые команды PWR ON, VOL 40, STATUS и отвечает строками POWER=ON и VOLUME=40. Каждое сообщение заканчивается \r\n.

const BaseDriver = require('base-driver');

class MyDeviceDriver extends BaseDriver {
  static metadata = {
    name: 'MyDevice',
    manufacturer: 'MyCompany',
    version: '1.0.0',
    description: 'Питание и громкость MyDevice по TCP',
    transportDefaults: {
      tcp: { port: 4001, delimiter: '\r\n' }
    },
    parameters: [
      { name: 'maxVolume', default: 80 }
    ]
  };

  static commands = {
    setPower: {
      description: 'Включение и выключение',
      parameters: [
        { name: 'value', type: 'boolean', description: 'true — включить', required: true }
      ]
    },
    setVolume: {
      description: 'Установка громкости',
      parameters: [
        { name: 'level', type: 'number', description: 'Уровень 0-100', required: true, min: 0, max: 100 }
      ]
    },
    getStatus: {
      description: 'Запрос состояния',
      parameters: []
    }
  };

  static responses = {
    power: {
      description: 'Состояние питания',
      matcher: { pattern: /POWER=(ON|OFF)/ },
      extract: m => ({ on: m[1] === 'ON' })
    },
    volume: {
      description: 'Громкость',
      matcher: { pattern: /VOLUME=(\d+)/ },
      extract: m => ({ level: parseInt(m[1], 10) })
    }
  };

  constructor(options) {
    super(options);
    this.state = {};
  }

  initialize() {
    this.publishCommand('getStatus');
  }

  setPower(params) {
    this.state.power = params.value === true;
    return { payload: `PWR ${this.state.power ? 'ON' : 'OFF'}\r\n` };
  }

  setVolume(params) {
    const max = Number(this.config.maxVolume) || 100;
    const level = Number(params.level);
    if (Number.isNaN(level)) return null;
    this.state.volume = Math.max(0, Math.min(max, level));
    return { payload: `VOL ${this.state.volume}\r\n` };
  }

  getStatus() {
    return { payload: 'STATUS\r\n' };
  }
}

module.exports = MyDeviceDriver;

Что здесь происходит:

  1. Для драйвера задан порт и разделитель по умолчанию, а громкость ограничена параметром подключения maxVolume.

  2. Три команды появятся в списке «Команда» узла device - command.

  3. После подключения initialize просит у устройства состояние, и его ответы разбираются правилами из static responses: сообщение POWER=ON станет ответом power с полем on: true.

  4. Как правила responses превращаются в данные на выходе device - response - listener — на странице Ответы устройства.

Как подключить и проверить драйвер

  1. Сохраните драйвер в редакторе. Сохранение помечает поток как изменённый. Нажмите «Развернуть» (кнопка U-Logic, которая запускает правки программы в работу), чтобы уже собранные узлы начали использовать новый код.

  2. Добавьте узел Device Connection: выберите драйвер в списке «Драйвер» и транспорт, затем создайте для транспорта настройки: адрес, порт. Если драйвер описал transportDefaults, нажмите «Apply driver defaults».

  3. Добавьте узел device - command, выберите соединение и команду, заполните параметры.

  4. Добавьте узел device - response - listener, выберите то же соединение и подключите к его выходу узел debug.

  5. Нажмите «Развернуть» и запустите команду.

Что вы увидите: в device - command команда выполнилась без ошибки, а в панели отладки U-Logic появился ответ устройства — сообщение, в msg.payload которого лежат поля вашего ответа (on, level и т. д.). Если в Device Connection включить «Driver Debug», в панель отладки попадут все команды и ответы, в том числе «сырые» данные до разбора.

Поля окон узлов описаны на странице Узлы Dynamic Driver; настройки транспортов — Транспорты: как связаться с устройством, проверка транспорта без драйвера — Тестовые узлы: проверить транспорт без драйвера. Если у вас нет устройства, повторите те же шаги на драйвере из страницы Примеры драйверов: он работает без сети.

Если не работает

Общий маршрут проверки по цепочке «панель → программа → устройство → панель» — на странице «Не работает: где смотреть».

Проверьте по порядку:

  1. Имя метода совпадает с ключом в static commands до буквы.

  2. Метод возвращает { payload }, строку или Buffer, а не undefined и не объект без payload.

  3. Соединение установлено: device - response - listener показывает статус «Подключено», а команды без соединения отбрасываются.

  4. Драйвер сохранён, а поток развёрнут после правки.

  5. В Device Connection включён «Driver Debug»: в панели отладки видно, что именно драйвер отправил и что пришло в ответ.

  6. Разделитель в настройках транспорта совпадает с тем, чем устройство заканчивает сообщения.

Если команда уходит, но ответ не распознаётся, причины — на странице Ответы устройства.