Как написать драйвер
Драйвер объясняет Dynamic Driver, как говорить с вашим устройством: какие команды у него есть, в каком виде их отправлять и как понимать ответы. Эта страница — для разработчика драйвера: из чего он состоит, что где писать и как подключить готовый код к потоку (рабочему листу программы в U-Logic). Пишется драйвер на JavaScript прямо в редакторе драйверов.
Остальное — на соседних страницах:
как открыть редактор, создать и сохранить драйвер — Редактор драйверов;
как разбирать ответы устройства — Ответы устройства;
вход на устройство, HTTP, несколько устройств,
publishCommand,KeepAlive,onDestroy, контекст U-Logic — Расширенные возможности;окна узлов Device Connection,
device - commandиdevice - response - listener— Узлы Dynamic Driver (подключение — Device Connection: подключение драйвера к транспорту, команда — device - command: отправить команду, ответы — device - response - listener: принять ответы);готовый драйвер, который можно проверить без устройства, — Примеры драйверов;
набросок драйвера с помощью ИИ — AI-Driver.
Что нужно до начала
Знание JavaScript.
Описание протокола устройства: какие команды оно принимает, в каком виде (текст, байты, HTTP-запросы), какие ответы присылает и чем каждое сообщение заканчивается.
Доступ к U-Logic (редактору программы управления) на контроллере, где открывается редактор драйверов.
Файлы создавать не нужно: код драйвера пишется и сохраняется в редакторе. Имя, под которым вы его сохраните, — идентификатор драйвера: именно оно появится в списке «Драйвер» узла Device Connection (драйвер плюс транспорт к одному конкретному устройству). В имени допустимы латинские буквы, цифры, дефис и подчёркивание, расширение не пишут. metadata.name на выбор драйвера не влияет, это только подпись.
Из чего состоит драйвер
Драйвер — класс, который расширяет BaseDriver. BaseDriver берёт на себя связь с системой и транспортом (способом связи с устройством: TCP, UDP, HTTP, Telnet, SSH, RS232 и другие), вам остаётся описать устройство.
Часть |
Что делает |
Где описана |
|---|---|---|
|
название, производитель, версия, параметры подключения, настройки транспорта по умолчанию |
раздел «Метаданные» |
|
список команд, из которых строится узел |
раздел «Команды» |
методы команд |
превращают команду и её параметры в данные для устройства |
раздел «Методы команд» |
|
распознают ответы устройства |
|
|
выполняется при каждом подключении |
раздел «Метод initialize» |
служебные методы |
|
раздел «Служебные методы» |
Минимальная заготовка:
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.
Что доступно из кода драйвера
Подключать модули можно только из этого списка:
Модуль |
Что доступно |
|---|---|
|
модуль целиком |
|
|
|
|
|
|
Таймеры 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: { /* см. ниже */ }
};
Поле |
Тип |
Назначение |
|---|---|---|
|
строка |
подпись драйвера (обязательно) |
|
строка |
производитель |
|
строка |
версия драйвера, например |
|
строка |
для чего драйвер |
|
строка |
модель устройства |
|
строка |
необязательная пометка; окна узлов её не используют |
|
массив |
параметры подключения, см. ниже |
|
объект |
настройки транспорта по умолчанию, см. ниже |
Параметры подключения (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, не меняются. Кнопка видна, только если драйвер описал настройки для выбранного вида транспорта.
Ключи для каждого транспорта:
Транспорт |
Ключи |
|---|---|
|
|
|
|
|
|
|
|
|
|
Что означают ключи (в скобках — подпись поля в окне транспорта):
Ключ |
Значение |
|---|---|
|
адрес и порт устройства («Хост», «Порт») |
|
разделитель сообщений: |
|
через сколько миллисекунд отдать неполное сообщение; |
|
оставлять разделитель в сообщении («Сохранять делимитер») |
|
размер буфера, КБ («Размер буфера»); по умолчанию 64 |
|
пауза между попытками подключиться, мс («Интервал переподкл.») |
|
подключаться при запуске и переподключаться при разрыве («Автоподключение») |
|
отладочные сообщения транспорта |
|
вид данных: для |
|
регулярные выражения: запрос логина, запрос пароля, приглашение командной строки («Выражение для логина» и т. д.) |
|
добавлять к команде окончание строки автоматически («Автодобавлять окончание команды») |
|
внешний порт; локальный порт (если задан, в окне включается «Указать вручную»); исходящий IP |
|
версия IP, |
остальные ключи |
параметры 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'
}
]
}
};
Поля команды:
Поле |
Назначение |
|---|---|
|
показывается в окне узла в строке «Описание» |
|
необязательно: |
|
массив параметров; для команды без параметров — пустой |
Поля параметра:
Поле |
Назначение |
|---|---|
|
имя; по нему параметр приходит в метод команды |
|
|
|
подсказка в поле ввода |
|
значение по умолчанию |
|
обязательный ли параметр |
|
границы для числа |
|
список допустимых значений |
Что увидит пользователь в окне device - command:
Описание параметра |
Поле в окне |
|---|---|
|
числовое поле; |
|
список True / False |
есть |
выпадающий список значений; пустой пункт «– Выберите значение –» есть, только если параметр не |
|
текстовое поле |
Предупреждение
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` };
}
Что можно вернуть:
Возврат |
Что произойдёт |
|---|---|
|
содержимое |
строка или |
то же самое |
|
команда не отправляется, и в журнале U-Logic об этом не пишется. Если пользователю нужно знать причину (например, неверный параметр), сообщите её из метода сами: |
|
команда уйдёт, когда он завершится; если за 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.
Служебные методы
Эти методы система вызывает сама, это не команды устройства:
Метод |
Когда вызывается |
Подробнее |
|---|---|---|
|
после подключения |
выше |
|
когда ответ не распознал ни один |
|
|
по таймеру Keep-Alive |
|
|
после обновления параметров |
|
|
при остановке узла: развёртывание, удаление |
Реализуйте их как обычные методы класса. Не объявляйте их в 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;
Что здесь происходит:
Для драйвера задан порт и разделитель по умолчанию, а громкость ограничена параметром подключения
maxVolume.Три команды появятся в списке «Команда» узла
device - command.После подключения
initializeпросит у устройства состояние, и его ответы разбираются правилами изstatic responses: сообщениеPOWER=ONстанет ответомpowerс полемon: true.Как правила
responsesпревращаются в данные на выходеdevice - response - listener— на странице Ответы устройства.
Как подключить и проверить драйвер
Сохраните драйвер в редакторе. Сохранение помечает поток как изменённый. Нажмите «Развернуть» (кнопка U-Logic, которая запускает правки программы в работу), чтобы уже собранные узлы начали использовать новый код.
Добавьте узел Device Connection: выберите драйвер в списке «Драйвер» и транспорт, затем создайте для транспорта настройки: адрес, порт. Если драйвер описал
transportDefaults, нажмите «Apply driver defaults».Добавьте узел
device - command, выберите соединение и команду, заполните параметры.Добавьте узел
device - response - listener, выберите то же соединение и подключите к его выходу узел debug.Нажмите «Развернуть» и запустите команду.
Что вы увидите: в device - command команда выполнилась без ошибки, а в панели отладки U-Logic появился ответ устройства — сообщение, в msg.payload которого лежат поля вашего ответа (on, level и т. д.). Если в Device Connection включить «Driver Debug», в панель отладки попадут все команды и ответы, в том числе «сырые» данные до разбора.
Поля окон узлов описаны на странице Узлы Dynamic Driver; настройки транспортов — Транспорты: как связаться с устройством, проверка транспорта без драйвера — Тестовые узлы: проверить транспорт без драйвера. Если у вас нет устройства, повторите те же шаги на драйвере из страницы Примеры драйверов: он работает без сети.
Если не работает
Общий маршрут проверки по цепочке «панель → программа → устройство → панель» — на странице «Не работает: где смотреть».
Проверьте по порядку:
Имя метода совпадает с ключом в
static commandsдо буквы.Метод возвращает
{ payload }, строку илиBuffer, а неundefinedи не объект безpayload.Соединение установлено:
device - response - listenerпоказывает статус «Подключено», а команды без соединения отбрасываются.Драйвер сохранён, а поток развёрнут после правки.
В Device Connection включён «Driver Debug»: в панели отладки видно, что именно драйвер отправил и что пришло в ответ.
Разделитель в настройках транспорта совпадает с тем, чем устройство заканчивает сообщения.
Если команда уходит, но ответ не распознаётся, причины — на странице Ответы устройства.