Скриптер: логика кодом

Скриптер — узел U-Logic (редактор программы управления), в котором логику управления вы описываете не цепочкой узлов, а кодом на JavaScript: реагируете на нажатия на панели, отправляете команды устройствам, обновляете надписи и кнопки на панели. Устройства и элементы панели вы называете по именам. Подписки на события Скриптер снимает сам при каждом новом «Развернуть» и при остановке узла.

Скриптер нужен тем, кто умеет программировать и хочет вместо десятка узлов change, switch и delay написать несколько строк кода: когда одно нажатие управляет несколькими устройствами, логика зависит от состояния или от времени, а однотипные кнопки обрабатываются по шаблону. Если вы только начинаете, соберите программу цепочкой узлов — это проще и нагляднее, см. «Связь интерфейса с программой». К Скриптеру можно перейти позже, когда цепочки станут громоздкими.

Скриптер не заменяет драйвер. Драйвер объясняет системе, как говорить с устройством определённого типа: какие у него команды и какие ответы. Скриптер описывает, что делать, когда нажали кнопку, и использует команды уже существующих драйверов. Если устройства нет в списке драйверов, сначала добавьте драйвер, см. Dynamic Driver.

Три узла Скриптера

В палитре U-Logic, в категории «функция», есть три узла:

Узел

Зачем нужен

scripter

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

scripter - sender («код → поток»)

отдельная точка выхода из кода: код отправляет сообщение в узел по его ID, а дальше оно идёт по проводам к любым другим узлам программы

scripter - receiver («поток → код»)

отдельная точка входа в код: сообщения, которые пришли в узел по проводам, код получает по ID узла

Обычно хватает одного узла scripter. Узлы scripter - sender и scripter - receiver нужны, когда Скриптеру надо отдать результат другим узлам программы в нескольких местах или принять сообщения из нескольких источников. У узла scripter один вход и один выход.

Как их настроить:

  • scripter - sender: поле ID (например, toLog), поле Выходов (от 1 до 64), необязательное поле Имя. У узла нет входа. Из кода: ctrl.sender('toLog').send(msg); второй аргумент — номер выхода, считая с нуля (по умолчанию 0 — первый выход): ctrl.sender('toLog').send(msg, 1).

  • scripter - receiver: поле ID (например, sensors), необязательное поле Имя. У узла один вход и нет выходов. Из кода: ctrl.receiver('sensors').on(msg => { … }). Нужно несколько входов в код — добавьте несколько узлов scripter - receiver с разными ID.

После добавления узла на холст подпись показывает ID: sender: toLog, receiver: sensors.

Как открыть Скриптер и из чего состоит окно

Есть два способа открыть редактор кода:

  1. Дважды щёлкните по узлу scripter на холсте. Откроется окно «Изменить узел scripter».

  2. Откройте в боковой панели U-Logic вкладку Scripter. В ней кнопка Открыть Scripter IDE открывает расширенный редактор в отдельной вкладке браузера (см. «Редактор Скриптера (IDE)»). Список Скриптеры в проекте показывает все узлы Скриптера (имя узла и имя потока). Щелчок по строке открывает редактор этого узла. Пока в программе нет ни одного узла, в списке написано «Нет узлов scripter в текущих потоках.».

В окне узла:

  • поле Name — подпись узла на холсте; если оставить пустым, узел подписан scripter;

  • поле Outputs — число выходов узла;

  • вкладки кода On Start, On Message и On Stop — три фазы работы узла (следующий раздел);

  • кнопка + добавляет свою именованную вкладку: её код выполняется в составе On Start, в порядке вкладок. Так настройку можно разбить на части, например «Источники», «Звук», «Микрофоны». Двойной щелчок по названию вкладки переименовывает её, крестик удаляет. Три основные вкладки переименовать и удалить нельзя;

  • вкладка Скрипты — ваши скрипты и папки; кнопки Скрипт, Папка, Импорт .js, Из библиотеки, Экспорт всё (.json) и Открыть в отдельной вкладке;

  • вкладка Диагностика — показывает, что сейчас происходит в узле (см. «Где смотреть ошибки»);

  • кнопка Сгенерировать в окне узла;

  • переключатель Включено внизу окна: выключенный узел не работает (см. «Как запустить и остановить Скриптер»);

  • у каждого редактора кнопка разворота на весь экран.

В коде доступны объекты ctrl, node, msg, send, log, env, context, flow, global, console, Buffer, а также таймеры setTimeout и setInterval: их Скриптер остановит сам, когда остановится узел. Объект node содержит id, name, log, warn, error, status и send. Окно узла не проверяет, правильно ли вы написали имена устройств, команд и сигналов: это обычные строки, ошибку в них видно только при работе. Такую проверку делает расширенный редактор.

Три фазы: On Start, On Message, On Stop

Код делится на три части. От того, в какую вкладку вы его положите, зависит, когда он выполняется.

Вкладка

Когда выполняется

Что здесь писать

On Start

один раз: при «Развернуть» и при запуске U-Logic

вся настройка: подписки на нажатия кнопок и ответы устройств, таймеры, начальные надписи на панели. Здесь живёт большая часть программы

On Message

на каждое сообщение, которое пришло в узел по проводу

только действия: отправить команду, обновить панель. Сообщение лежит в переменной msg

On Stop

один раз при остановке узла или повторном «Развернуть»

завершающие действия, если они нужны. Подписки и таймеры снимать не нужно — Скриптер делает это сам

Главное правило: подписки объявляют один раз, в On Start. При каждом «Развернуть» узел запускается заново: старые подписки снимаются, On Start выполняется снова. Поэтому двойных подписок не бывает. Если объявить подписку в On Message, она будет добавляться на каждое сообщение: через 50 сообщений одно нажатие кнопки выполнит обработчик 50 раз. Подробнее — в разделе «Правила» ниже.

В On Start код выполняется синхронно: паузу здесь делают через ctrl.wait (раздел «Таймеры»). В On Message можно писать await — вкладка работает как асинхронная функция, и вызывать done не нужно: достаточно return.

Как запустить и остановить Скриптер

Запустить. Напишите код, закройте окно узла и нажмите Развернуть. Узел запустится, On Start выполнится один раз. После изменения кода снова нажмите Развернуть: только так новый код начинает работать.

Остановить. У узла нет отдельной кнопки «Стоп». Есть два способа, и в обоих нужно нажать Развернуть:

  • откройте окно узла, выключите переключатель Включено и нажмите Развернуть — узел остановится, но останется в программе, его можно включить снова;

  • удалите узел и нажмите Развернуть — Скриптер снят насовсем.

При остановке подписки и таймеры снимаются сами. После «Развернуть» на узле зелёная точка с надписью «onStart» означает, что On Start выполнился.

Где смотреть ошибки. Куда попадёт ошибка, зависит от того, где она случилась:

Где ошибка

Что вы увидите

В самом On Start (например, в строке 3 вызвана несуществующая функция)

на узле красное кольцо со статусом «onStart L3 ReferenceError» (фаза, строка, тип ошибки); в боковой панели отладка — сообщение от узла «scripter onStart: … is not defined»; во вкладке Диагностика → Ошибки — строка «On Start · строка 3 · … is not defined»

Внутри ctrl.wait или ctrl.timer

статус узла остаётся зелёным «onStart»; в отладке — предупреждение «sdk timer handler error: …»; вкладка Диагностика пишет «Ошибок нет»

Внутри обработчика, например onTap (срабатывает при касании на панели)

проверяйте шаги сами: поставьте node.warn('шаг 1') перед каждым шагом и смотрите панель отладка; отправленные команды видны в Диагностике → Трасса

Из последней строки следуют два правила: держите код обработчика коротким и проверяйте его выводом node.warn('дошли до шага 2') (сообщение появится в отладке), а паузы между шагами делайте через ctrl.wait, как в примере ниже.

Ошибка в одном обработчике не останавливает ни узел, ни остальные обработчики и таймеры. Вкладка Диагностика в окне узла (и такая же панель в расширенном редакторе) показывает состояние Скриптера. Кнопка Обновить перечитывает данные; внизу написано, когда был последний прогон.

Раздел

Что показывает

Строки On Start, On Message, On Stop

длительность последнего выполнения каждой фазы (в миллисекундах) и дату

Ошибки

ошибки с указанием фазы, строки и текста; если их нет — «Ошибок нет»

Трасса

по строке на каждую команду: время, кому и что отправлено, например «Демо: матрица → route {«input»:0,»output»:1}»

Значения

последние значения сигналов; если их нет — «Значений нет»

Подключения

состояние подключений к устройствам; если их нет — «Подключений нет»

Консоль

вывод console.log, console.info, console.warn, console.error из вашего кода; если вывода нет — «Консоли нет»; кнопка Скрыть убирает раздел

Если код не удаётся разобрать (например, пропущена скобка), сообщение об ошибке называет узел и фазу, в которой она найдена.

Как Скриптер связан с устройствами и панелью

        flowchart LR
  S["Скриптер<br/>(ваш код)"]
  D["Подключение к устройству<br/>(device - connection)"]
  P["UI-Panel<br/>экран панели"]
  S -- "команда" --> D
  D -- "ответ устройства" --> S
  S -- "изменить кнопку, текст" --> P
  P -- "нажатия на панели" --> S
    
  • Устройства. Код обращается к устройству по имени: ctrl.device('Демо: матрица'). Имя — это поле Name узла подключения device - connection в вашей программе U-Logic, а не тип драйвера. Одинаковых устройств может быть несколько (три камеры одного типа), и имя выбирает нужное. Если узла подключения с таким именем в программе нет, команда никуда не уйдёт (send вернёт false, без ошибки).

  • Команды и ответы устройства берутся из его драйвера. Скриптер не знает протокол устройства: он говорит драйверу «выполни команду route с такими параметрами», а драйвер превращает её в команду устройства. Ответ устройства драйвер превращает в сигнал с именем, например route; на это имя вы подписываетесь. Имена команд, их параметры и имена сигналов смотрите в описании драйвера или в редакторе драйверов.

  • Панель. Код обращается к панели и её элементам по именам: ctrl.panel('Демо-зал').onTap('Src_Cam1_b', …). Имя панели — название интерфейса, из которого панель присылает события; имена элементов — как в UI-Designer. Подробнее про интерфейсы — UI-Panel.

Устройства работают сами по себе: команды им уйдут и без панели. Панель нужна только для методов ctrl.panel.

Первая программа

Задача: при нажатии кнопки Src_Cam1_b на панели «Демо-зал» переключить вход матрицы на камеру 1 и подсветить кнопку. В программе должен быть узел подключения device - connection с именем «Демо: матрица», как в учебной программе «Демо-зала».

Примечание

В готовой программе «Демо-зала» эти кнопки уже обслуживает цепочка узлов. Пробуйте на новой пустой вкладке потока и не оставляйте на одной кнопке две обработки сразу: иначе при нажатии сработают обе.

  1. Перетащите из палитры (категория «функция») узел scripter на вкладку потока.

  2. Дважды щёлкните по узлу и откройте вкладку On Start.

  3. Впишите код:

    ctrl.panel('Демо-зал').onTap('Src_Cam1_b', () => {
      ctrl.device('Демо: матрица').send('route', { input: 1, output: 1 });
      ctrl.panel('Демо-зал').setSelected('Src_Cam1_b');   // подсветить кнопку
    });
    
  4. Закройте окно узла и нажмите Развернуть.

  5. Нажмите кнопку Src_Cam1_b на панели. Матрица переключит выход 1 на вход 1, кнопка подсветится.

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

  • onTap — подписка на короткое нажатие элемента. Она живёт, пока работает узел, и снимается при следующем «Развернуть».

  • send('route', { input: 1, output: 1 }) — команда драйверу матрицы: имя команды и параметры определяет драйвер.

  • setSelected — команда панели: показать кнопку выбранной.

Частые ошибки:

  • Забыли нажать Развернуть — работает прежний код.

  • Имя устройства не совпадает с полем Name узла подключения (лишний пробел, другой регистр) — команда молча не уходит. Проверьте список имён: node.log('Устройства: ' + ctrl.devices().join(', ')).

  • Имя панели или кнопки не совпадает с тем, что в UI-Designer — подписка ничего не ловит.

Связь с остальной программой: msg, return, sender и receiver

Узел scripter стоит в потоке как обычный узел: сообщение, подведённое проводом к его входу, приходит в On Message в переменной msg. Читайте msg.topic и msg.payload и выполняйте действие:

// On Message
if (msg.topic === 'camera') {
  ctrl.device('Демо: камера 1').send('recallPreset', { preset: msg.payload });
}

Чтобы отдать сообщение дальше по проводам, верните его из On Message через return msg либо вызовите send(msg) или node.send(msg). Это подходит, когда сообщений несколько или они появляются позже, например в обработчике кнопки из On Start:

// On Message
msg.payload = { handled: true };
return msg;                      // уйдёт на выход узла

// On Start: сообщение из обработчика кнопки
ctrl.panel('Демо-зал').onTap('Home_Meeting_b', () => {
  node.send({ topic: 'scenario', payload: 'meeting' });
});

Не путайте с ctrl.publish(тема, данные) и ctrl.on(тема, fn): это обмен сообщениями между разными узлами Скриптера, он на выход узла по проводам не идёт.

Если у узла scripter не хватает одного входа и одного выхода, используйте scripter - sender и scripter - receiver (раздел «Три узла Скриптера»).

Справочник API: объект ctrl

В Скриптере ctrl уже готов к работе, ничего подключать не нужно.

Устройства: ctrl.device(имя)

ctrl.device('Демо: матрица') возвращает объект устройства. Его можно сохранить: const matrix = ctrl.device('Демо: матрица');.

send(команда, параметры?) — отправить команду устройству. команда — имя команды из драйвера, параметры — объект (необязательно); какие ключи нужны, определяет драйвер. Возвращает true, если команда ушла, и false, если устройства с таким именем ещё нет.

matrix.send('route', { input: 1, output: 1 });      // выход 1 — вход 1
camera.send('recallPreset', { preset: 2 });         // команда с параметром
codec.send('hangup');                               // команда без параметров

on(сигнал, fn) — вызывать fn(resp) на каждый ответ устройства указанного типа. сигнал — имя ответа из драйвера; в resp приходит весь объект ответа. Возвращает функцию off(): вызов снимает подписку досрочно (обычно не нужно). Если подписка молчит, подпишитесь через onAny и посмотрите, какое значение resp.type приходит на самом деле.

matrix.on('route', (resp) => {
  if (resp.ack) node.log('Матрица подтвердила команду, выход ' + resp.output);
});

onAny(fn) — вызывать fn(resp) на любой ответ устройства. Пригодится при отладке. Ответ без поля type (служебный пакет, пустой кадр) попадает только сюда.

onStatus(fn) — следить за связью: fn({ connected }) вызывается при каждом изменении, connected равно true или false.

onOnline(fn) и onOffline(fn) — вызвать fn(), когда устройство подключилось / отключилось. Если на момент подписки устройство уже в нужном состоянии, fn() вызывается сразу, поэтому начальный запрос состояния сработает и после повторного «Развернуть».

codec.onOnline(() => codec.send('getContacts'));
matrix.onOffline(() => panel.setText('Status_t', 'Нет связи с матрицей'));

isConnected() — true или false; undefined, если события связи ещё не приходили.

connect() и disconnect() — подключить или отключить устройство вручную.

get(сигнал) — последний ответ указанного типа без подписки или undefined, если такого ответа ещё не было. Первый вызов get включает запоминание, поэтому сразу после запуска он может вернуть undefined.

Особенности:

  • Узлы программы запускаются в произвольном порядке. Если устройство ещё не готово, send молча возвращает false. Чтобы гарантированно отправить команду на старте, дождитесь устройства через ctrl.ready (ниже).

  • Подписки on, onStatus и другие можно объявить до появления устройства: они заработают, когда оно появится. Порядок узлов в программе не важен.

  • Все подписки возвращают off(), но снимать их вручную обычно не нужно.

Панель: ctrl.panel(имя)

ctrl.panel('Демо-зал') возвращает объект панели. У него две группы методов: события (узнать, что сделал пользователь) и команды (изменить элемент). Первым аргументом всегда идёт имя элемента.

События

Все методы принимают имя элемента (или массив имён) и обработчик fn(e), возвращают off().

Метод

Когда срабатывает

onTap(имя, fn)

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

onPush(имя, fn) / onRelease(имя, fn)

палец нажал / отпустил элемент. Пара нужна, когда важно удержание («пока держишь»)

onHold(имя, fn)

долгое удержание

onSlideBegin(имя, fn)

начали двигать слайдер

onValueChanged(имя, fn)

значение изменилось: слайдер сдвинули, переключатель переключили. Новое значение в e.value

onSlideEnd(имя, fn)

слайдер отпустили. Подходит, чтобы слать команду один раз в конце движения

onItemSelected(имя, fn)

выбран пункт списка

onAny(fn)

любое событие любого элемента панели (без имени элемента)

on(имя, событие, fn)

любое событие по имени. событие — строка ('tap') или массив (['tap', 'hold'])

Группа кнопок одним обработчиком. Вместо имени можно передать массив имён: обработчик срабатывает на любую из этих кнопок, а e.object подскажет, на какую именно нажали.

// 7 кнопок выбора источника — один обработчик; e.object — нажатая кнопка
panel.onTap(['Src_Cam1_b', 'Src_Cam2_b', 'Src_Cam3_b'], (e) => node.log('источник: ' + e.object));

// слайдер: по отпусканию обновить подпись
panel.onSlideEnd('Snd_Hall_vol_sl', (e) => panel.setText('Snd_Hall_val_lb', Math.round(e.value) + ' %'));

Объект события e:

Поле

Что это

e.panel

имя панели, на которой произошло событие

e.object

имя элемента

e.event

имя события ('tap', 'value_changed' …) — полезно в onAny и on с массивом

e.value

значение: положение слайдера, состояние переключателя

e.bool

true, когда e.value равно 'pressed' или 'selected': пригодится для кнопок-переключателей

e.client

какая панель прислала событие

e.raw

исходное событие целиком, если нужны нестандартные поля

Команды

Вид кнопки. Состояния кнопки: 'default' (обычная), 'selected' (выбрана, подсвечена), 'pressed' (нажата), 'disabled' (заблокирована).

  • setUiState(имя, состояние) — задать состояние (можно и своё, настроенное на панели).

  • setDefault(имя), setSelected(имя), setPressed(имя), setDisabled(имя) — сокращения для частых состояний. setSelected — самый частый: «эта кнопка сейчас активна».

Текст и значения.

  • setText(имя, текст) — надпись кнопки, подписи, заголовка.

  • setValue(имя, значение) — позиция слайдера, значение индикатора.

  • fontColor(имя, цвет) и backgroundColor(имя, цвет) — цвет текста и фона, например '#E53935'.

panel.setText('Status_t', 'Связь есть');
panel.setValue('Snd_Hall_vol_sl', 68);
panel.fontColor('Status_t', '#E53935');

Видимость, положение, размер.

  • hidden(имя, скрыть) — true скрывает элемент, false показывает.

  • moveX(имя, x), moveY(имя, y), width(имя, w), height(имя, h) — положение и размер (целые числа в единицах раскладки панели). Варианты moveXFloat, moveYFloat, widthFloat, heightFloat принимают дробные значения.

Всплывающая страница. overlay(страница, фрейм) показывает всплывающую страницу поверх экрана в указанном фрейме; overlay(null, фрейм) закрывает её.

Списки. setList(имя, список) заполняет список массивом пунктов, clearList(имя) очищает, listItems(имя, ключ, значение) обновляет пункты по ключу, removeListItems(имя, ключ) удаляет пункты по ключу.

Встроенный браузер. webUrl(имя, адрес) открывает адрес в элементе Web URL, reload(имя) перезагружает страницу. Пример использования — «Просмотр RTSP-потока в интерфейсе».

Расширенная кнопка. advancedButton(имя, ключСостояния, свойство, значение) задаёт отдельное свойство расширенной кнопки для указанного состояния.

Страницы. navigate(страница) переключает панель на страницу.

Пакетное обновление. batch(действия) выполняет несколько команд одним сообщением: быстрее и надёжнее, чем по одной. действия — массив объектов { name, event, value }, где name — элемент, event — что меняем ('uiState', 'text' …), value — значение. Пустой массив ничего не делает.

panel.batch([
  { name: 'Src_Cam1_b', event: 'uiState', value: 'selected' },
  { name: 'Src_Cam2_b', event: 'uiState', value: 'default'  },
  { name: 'Src_Cam3_b', event: 'uiState', value: 'default'  }
]);

Несколько панелей с одинаковыми интерфейсами

Если один интерфейс открыт на двух панелях (например, настенной и настольной), имена элементов совпадают, а логика нужна одна. ctrl.panel берёт одну панель, поэтому логику описывают один раз как функцию и вызывают для каждой панели. Команды надо отправлять на все панели, иначе экраны разойдутся:

const panels = ['Панель 1', 'Панель 2'].map((name) => ctrl.panel(name));
const all = (fn) => panels.forEach(fn);

const SOURCES = ['Src_Cam1_b', 'Src_Cam2_b', 'Src_Cam3_b'];
function showSource(active) {
  all((p) => SOURCES.forEach((b) => (b === active ? p.setSelected(b) : p.setDefault(b))));
}

all((panel) => {
  // нажатие на любой панели управляет матрицей; подсветка придёт по ответу устройства
  panel.onTap(SOURCES, (e) => matrix.send('route', { input: SOURCES.indexOf(e.object) + 1, output: 1 }));
});

e.panel в обработчике показывает, с какой панели пришло событие. ctrl.interlock и ctrl.toggle (ниже) берут одну панель: для зеркальных панелей проще ручная подсветка, как в примере.

Реестр и готовность

  • ctrl.devices() — массив имён всех устройств, известных программе.

  • ctrl.ready(имя) — Promise, который выполнится, когда устройство с этим именем станет доступно (сразу, если уже доступно). Так команда на старте не потеряется:

    ctrl.ready('Демо: матрица').then(() => matrix.send('route', { input: 0, output: 1 }));
    
  • ctrl.on(тема, fn) и ctrl.publish(тема, данные) — обмен сообщениями между узлами Скриптера по названию темы. on возвращает off().

  • ctrl.dispose() — снять все подписки вручную. Обычно не нужно.

Таймеры: ctrl.wait и ctrl.timer

Таймеры работают внутри узла, время задаётся в секундах. При остановке узла или новом «Развернуть» они снимаются сами. Ошибка внутри таймера пишется в отладку и таймер не останавливает.

ctrl.wait(секунды, fn) — один раз вызвать fn() через указанное время. Возвращает объект: cancel() — отменить, restart() — начать отсчёт заново, change(секунды) — сменить задержку и начать заново.

const idle = ctrl.wait(300, () => matrix.send('route', { input: 0, output: 1 }));  // через 5 минут
panel.onAny(() => idle.restart());                                                 // любой жест сбрасывает

ctrl.timer(секунды, fn) — вызывать fn(count) каждые секунды, count — номер срабатывания с 1. Возвращает объект: cancel() или stop() — остановить и обнулить счётчик, pause() — приостановить, resume() — продолжить, restart() — начать заново, count — текущий номер.

const poll = ctrl.timer(10, () => codec.send('getCallInfo'));   // каждые 10 секунд

Пауза между шагами. В On Message паузу между командами делают через await и ctrl.wait:

const pause = (sec) => new Promise((resolve) => ctrl.wait(sec, resolve));

codec.send('hangup');
await pause(0.4);
matrix.send('route', { input: 0, output: 1 });

Группа взаимоисключающих кнопок: ctrl.interlock(панель, имена, опции?)

Группа кнопок, из которых выбрана ровно одна: выбранная становится selected, остальные default, одним сообщением.

  • панель — имя панели, имена — массив имён кнопок группы.

  • Методы группы: onSelect(cb) — при нажатии на любую кнопку группы она выбирается, и вызывается cb(имя, событие); select(имя) — выбрать кнопку из кода (например, стартовый источник); selected() — имя последней выбранной кнопки или undefined; off() — снять подписки onSelect.

  • Опции: onState — состояние выбранной кнопки (по умолчанию 'selected'), offState — остальных (по умолчанию 'default'), serverManaged (по умолчанию false): при true отправляется только состояние выбранной кнопки, а остальные гасит сама панель.

const src = ctrl.interlock('Демо-зал', ['Src_Cam1_b', 'Src_Cam2_b', 'Src_Cam3_b']);
src.onSelect((name) => {
  const input = { Src_Cam1_b: 1, Src_Cam2_b: 2, Src_Cam3_b: 3 }[name];
  matrix.send('route', { input, output: 1 });
});
src.select('Src_Cam1_b');   // стартовый источник

Подсветка здесь идёт «по клику». Если нужна подсветка строго по ответу устройства, вместо interlock подпишитесь на ответ матрицы и расставляйте состояния вручную.

Переключатель: ctrl.toggle(панель, имя, опции?)

Наблюдает за двухпозиционной кнопкой (вкл/выкл). Если кнопка настроена на панели как переключатель, панель сама меняет её вид при нажатии, а ctrl.toggle сообщает вам об этом.

  • панель — имя панели, имя — имя кнопки, опции — { onState }, состояние «включено» (по умолчанию 'selected').

  • Методы: onChange(cb) — cb(on, событие), on равно true, когда кнопка стала «включённой»; value() — последнее состояние (true, false или undefined); set(on) — выставить состояние из кода; off() — снять подписку.

const vcsMic = ctrl.toggle('Демо-зал', 'Vcs_Mic_b');
vcsMic.onChange((on) => codec.send('setMute', { mute: !on }));

Память узла: ctrl.context, ctrl.flow, ctrl.global, ctrl.env

Хранилища для состояния между нажатиями:

  • ctrl.context — память этого узла;

  • ctrl.flow — память вкладки потока (общая для всех её узлов);

  • ctrl.global — общая память всей программы;

  • у первых трёх методы get(ключ), set(ключ, значение) и keys();

  • ctrl.env — только чтение: get(имя) возвращает переменную окружения вкладки или группы.

ctrl.flow.set('lastSource', 'Src_Cam1_b');
const last = ctrl.flow.get('lastSource');

Обычные переменные из On Start живут, пока работает узел. Если состояние должно пережить новое «Развернуть» или быть видно другим узлам, храните его в flow или global.

Файлы: ctrl.files

ctrl.files читает и пишет файлы в папке uploads контроллера: выгрузки, журналы, конфигурации. В методах указывайте имя файла (можно с подпапкой, например logs/today.txt) — он всегда лежит внутри uploads. Эти же файлы можно загрузить с компьютера и забрать обратно по SFTP, см. Сетевые порты. Все методы асинхронные, вызывайте их через await.

Метод

Что делает

read(имя, кодировка?)

прочитать файл. С кодировкой ('utf8') вернёт строку, без неё — набор байт

write(имя, данные)

записать файл (перезаписывает); недостающие подпапки создаются

append(имя, данные)

дописать в конец файла

list(подпапка?)

имена файлов в папке или подпапке (без вложенных)

exists(имя)

есть ли файл: true или false

remove(имя)

удалить файл

mkdir(имя)

создать папку

// On Message
await ctrl.files.write('state.json', JSON.stringify({ source: 'Src_Cam1_b' }));
if (await ctrl.files.exists('state.json')) {
  const saved = JSON.parse(await ctrl.files.read('state.json', 'utf8'));
  node.log('Прошлый источник: ' + saved.source);
}

Для обычного состояния между перезапусками достаточно ctrl.flow и ctrl.global; ctrl.files нужен, когда данные — именно файлы.

Подключение прямо из кода: ctrl.connect(опции)

Создаёт подключение к устройству из кода, без узла device - connection. Нужно, когда адрес устройства известен только во время работы. Возвращает то же, что ctrl.device(имя).

// On Start
ctrl.connect({
  name: 'Демо: камера 1',   // как обращаться: ctrl.device('Демо: камера 1')
  driver: 'visca-generic',  // имя драйвера
  transport: 'udp',         // по умолчанию 'tcp'
  host: '192.168.1.21',
  port: 52381
});

Обязательные опции — name и driver; необязательные — transport (tcp по умолчанию, udp, telnet, ssh), host, port, params (дополнительные параметры драйвера), autoConnect, timeout.

Подключение временное: оно существует, пока работает узел, исчезает при новом «Развернуть» и не видно среди узлов подключений. Для постоянных, редактируемых подключений используйте узлы device - connection.

Правила

Все правила сводятся к одному: настройка — один раз в On Start, дальше только реакции.

  • Подписки объявляйте в On Start. Любая подписка (onTap, on, onValueChanged, onStatus …) — это правило, которое должно работать всё время, пока работает узел.

  • Очистка происходит сама. При остановке узла или «Развернуть» Скриптер снимает все подписки на панель и устройства и останавливает таймеры. Вызывать off() и dispose() вручную не нужно.

  • Новое «Развернуть» безопасно. Узел запускается заново, On Start создаёт подписки с нуля, двойных не будет.

  • В On Message только действия. Подписка, объявленная в On Message, создаётся заново на каждое сообщение, а старые остаются. Одно нажатие запускает все накопленные обработчики сразу (после 50 сообщений — 50 раз), а память растёт, пока узел не остановят.

    // On Message: НЕПРАВИЛЬНО
    panel.onTap('Src_Cam1_b', () => matrix.send('route', { input: 1, output: 1 }));
    
    // On Start: правильно — один раз
    panel.onTap('Src_Cam1_b', () => matrix.send('route', { input: 1, output: 1 }));
    // On Message: только действие в ответ на пришедшее сообщение
    if (msg.topic === 'camera') camera.send('recallPreset', { preset: msg.payload });
    
  • Ошибки изолированы. Ошибка в одном обработчике или таймере не останавливает остальные. Шаги обработчика касания проверяйте так, как сказано в «Где смотреть ошибки».

Расширенный редактор Скриптера

Кроме окна узла, у Скриптера есть расширенный редактор (Scripter IDE) — отдельная страница с автодополнением по именам вашей программы, проверкой имён, диагностикой и терминалом. Открывается кнопкой Открыть Scripter IDE на вкладке Scripter боковой панели U-Logic. Как им пользоваться и как отлаживать код — на странице «Редактор Скриптера (IDE)».

Устранение неполадок

Команда устройству не срабатывает (send вернул false). Устройства с таким именем нет. Чаще всего написано имя драйвера вместо поля Name узла device - connection либо узел устройства ещё не запустился. Сверьте имя, выведите список node.log(ctrl.devices().join(', ')); если проблема только на старте, оберните отправку в ctrl.ready('имя').then(…).

Нажатия панели не ловятся (onTap молчит). Проверьте, что имя панели и имя элемента точно совпадают с интерфейсом в UI-Designer. Если имена верные, а панель не реагирует, версия UI-Panel может не поддерживать Скриптер — обратитесь к интегратору.

Команды панели не доходят (setText, navigate). Причины те же: неверное имя панели или элемента либо версия UI-Panel без поддержки Скриптера.

Обработчик срабатывает много раз. Подписки объявлены в On Message. Перенесите их в On Start и нажмите Развернуть.

Новый код не действует. После правки нужно нажать Развернуть; в расширенном редакторе — Deploy.

Окно узла не подсвечивает опечатку в имени. Это ожидаемо: имена устройств, команд и сигналов — строки, и окно узла их не проверяет. Откройте код в расширенном редакторе: он подчёркивает неизвестные имена жёлтым. Ищите ошибку и по боковой панели отладка, вкладке Диагностика (раздел Трасса покажет, какие команды ушли) и списку ctrl.devices().

Не понятно, где ошибка и ушла ли команда. Запустите код тестом и посмотрите диагностику: пошаговый разбор — в разделе «Отладка по шагам» редактора.

Пример: сценарий «Перерыв» в «Демо-зале»

Этот пример уже лежит в программе «Демо-зал» (блок 14 «Скриптер: сценарий «Перерыв»», см. «Программа «Демо-зал»: обзор»). Ниже — что он делает, как его собрать с нуля и как проверить.

Зачем он нужен. Главная страница «Демо-зала» запускает сценарии одной кнопкой: «Начало совещания», «Начало видеоконференции», «Выключить». Четвёртый сценарий, «Перерыв», собран не цепочкой узлов, а одним узлом scripter. Нажатие на «Перерыв» делает сразу несколько действий с паузами между ними. В цепочке для этого нужны узлы change, split, delay, switch, здесь — один небольшой скрипт.

Что делает сценарий. После нажатия кнопки «Перерыв» на главной:

  1. сразу: кнопка «Перерыв» подсвечивается, микрофоны зала выключаются;

  2. через 1 с: с экрана зала убирается источник: на панели «Нет источника»;

  3. через 2 с: камера 1 возвращается в положение «Домой»;

  4. через 3 с: надпись в шапке меняется на «Демо-зал — перерыв».

Когда нажимают другой сценарий («Начало совещания», «Начало видеоконференции» или «Выключить» в окне «Выключить всё?»), перерыв считается оконченным: подсветка с кнопки «Перерыв» снимается, а в шапке снова «Демо-зал».

Примечание

Если вы загрузили программу «Демо-зал» целиком, узел «Сценарий «Перерыв»» в ней уже есть, и кнопку «Перерыв» обрабатывает именно он. Чтобы пройти шаги ниже с нуля, сначала выключите готовый узел (переключатель Включено в его окне) и нажмите Развернуть: иначе при нажатии сработают оба узла и каждая команда уйдёт дважды. Пример рассчитан на подключения «Демо: звуковой процессор», «Демо: матрица», «Демо: камера 1» и интерфейс «Демо-зал»: без них команды не дойдут.

Как собрать сценарий

  1. Откройте U-Logic и вкладку потока, на которой лежит программа «Демо-зал» (или новую вкладку потока).

  2. Найдите в палитре слева узел scripter в категории «функция» и перетащите его на холст.

    ../_images/scr-palette.png

    Палитра, категория «функция»: 1 — scripter, 2 — scripter - sender, 3 — scripter - receiver

  3. Дважды щёлкните по узлу. Откроется окно «Изменить узел scripter».

  4. В поле Name впишите Сценарий «Перерыв». Так узел будет подписан на холсте.

  5. Откройте вкладку On Start.

  6. Впишите в неё код:

    // Сценарий «Перерыв»: кнопка Home_Break_b на главной интерфейса «Демо-зал».
    // Кнопку не ловит ни один узел ui - switch — весь сценарий здесь.
    const panel = ctrl.panel('Демо-зал');                    // интерфейс — по имени, как в UI-Designer
    const sound = ctrl.device('Демо: звуковой процессор');   // устройства — по имени подключения
    const matrix = ctrl.device('Демо: матрица');
    const camera = ctrl.device('Демо: камера 1');
    
    panel.onTap('Home_Break_b', () => {
        // Шаг 1 — сразу: подсветить кнопку и выключить микрофоны зала
        panel.setUiState('Home_Break_b', 'pressed');
        sound.send('setMute', { instanceTag: 'DemoMicMaster', channel: 1, state: true });
    
        // Шаг 2 — через 1 с: убрать источник с экрана (как кнопка Src_Clear_b)
        ctrl.wait(1, () => matrix.send('route', { input: 0, output: 1 }));
    
        // Шаг 3 — через 2 с: камера 1 в положение «Домой» (как кнопка Cam_Home_b)
        ctrl.wait(2, () => camera.send('recallPreset', { preset: 1 }));
    
        // Шаг 4 — через 3 с: надпись в шапке
        ctrl.wait(3, () => panel.setText('Hdr_Title_lb', 'Демо-зал — перерыв'));
    });
    
    // Начался другой сценарий — перерыв окончен: снять подсветку и вернуть заголовок
    panel.onTap(['Home_Meeting_b', 'Home_Vcs_b', 'Pop_Yes_b'], () => {
        panel.setUiState('Home_Break_b', 'default');
        panel.setText('Hdr_Title_lb', 'Демо-зал');
    });
    
    ../_images/scr-node-onstart.png

    Окно «Изменить узел scripter»: 1 — поле Name, 2 — вкладка On Start, 3 — код сценария, 4 — переключатель Включено

  7. Проверьте, что внизу окна переключатель Включено включён, и нажмите Готово.

  8. Нажмите Развернуть.

Что вы увидите. Под узлом на холсте появится зелёная точка со статусом «onStart»: On Start выполнился, подписки на кнопки созданы. Красное кольцо вместо точки означает ошибку в On Start, см. «Где смотреть ошибки».

Как устроен код

  • ctrl.panel('Демо-зал') — панель, заданная именем интерфейса (так он назван в UI-Designer). Строка сохраняет её в переменную panel, чтобы не повторять имя.

  • ctrl.device('Демо: матрица') — устройство, заданное именем подключения: это поле Name узла device - connection в программе, а не тип драйвера. Три строки подряд берут звуковой процессор, матрицу и камеру 1.

  • panel.onTap('Home_Break_b', () => { … }) — «когда на панели коротко нажмут элемент Home_Break_b (кнопка «Перерыв»), выполни всё, что в скобках». Это подписка: она создаётся один раз, при «Развернуть», и дальше срабатывает на каждое нажатие.

  • panel.setUiState('Home_Break_b', 'pressed') — команда панели: показать кнопку нажатой (подсвеченной). Имя элемента — как в UI-Designer.

  • sound.send('setMute', { instanceTag: 'DemoMicMaster', channel: 1, state: true }) — команда устройству: имя команды из драйвера и её параметры. Параметры определяет драйвер процессора; здесь команда выключает микрофоны зала.

  • ctrl.wait(1, () => …) — «через 1 секунду выполни то, что после стрелки». Паузы отсчитываются от нажатия: 1, 2 и 3 секунды. Это не «подождать 1 с, потом ещё 2 с»: все три таймера запускаются сразу.

  • matrix.send('route', { input: 0, output: 1 }) — матрица: на выход 1 подать вход 0, то есть отключить выход. Экран зала остаётся без источника.

  • camera.send('recallPreset', { preset: 1 }) — камера 1: вызвать пресет 1 («Домой»).

  • panel.setText('Hdr_Title_lb', 'Демо-зал — перерыв') — поменять надпись в шапке.

  • Вторая подписка panel.onTap([…], …) принимает сразу три имени кнопок — один обработчик на все. Она возвращает кнопку и заголовок в обычный вид.

Код ничего не делает с видом кнопок источников и микрофонов. «Нет источника» на экране и «Микрофоны» в шапке меняются по ответам устройств, как и при нажатии обычных кнопок: команда запускает действие, а вид на панели обновляет ответ устройства.

Паузы сделаны через ctrl.wait, а не через await: в On Start код выполняется синхронно.

Что вы увидите на панели

Откройте интерфейс «Демо-зал» в UI-Panel и нажмите «Перерыв» на главной:

../_images/scr-panel-break.png

Главная «Демо-зала» через 3 секунды после нажатия: 1 — кнопка «Перерыв» подсвечена, 2 — надпись в шапке «Демо-зал — перерыв», 3 — на экране «Нет источника»

Сначала подсвечивается кнопка, через 1–2 секунды экран показывает «Нет источника», а через три секунды меняется надпись в шапке. Нажмите «Начало совещания»: подсветка снимется, в шапке снова «Демо-зал».

Как проверить, что команды ушли

Откройте окно узла, перейдите на вкладку Диагностика и нажмите Обновить.

../_images/scr-diagnostics.png

Вкладка Диагностика: 1 — вкладка, 2 — раздел Трасса, 3 — строки команд: «Демо: звуковой процессор → setMute …», «Демо: матрица → route …», «Демо: камера 1 → recallPreset …»

В разделе Трасса — по строке на каждую отправленную команду, с временем. После нажатия «Перерыв» там должны быть строки setMute, route, recallPreset. Если строк нет, подписка не сработала: проверьте, что вы нажали Развернуть и что имя Home_Break_b и имя интерфейса совпадают с UI-Designer. Если строка есть, а устройство не отреагировало, проверьте подключение устройства.

Все узлы Скриптера видны в боковой панели на вкладке Scripter:

../_images/scr-sidebar.png

Боковая панель: 1 — вкладка Scripter, 2 — кнопка Открыть Scripter IDE, 3 — список Скриптеры в проекте

В списке — строка «Сценарий «Перерыв» · Демо-зал» (имя узла и вкладки потока); щелчок по ней открывает редактор этого узла.

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

  • Статус узла — красное кольцо. Ошибка в On Start: статус называет фазу, строку и тип ошибки, подробности — на вкладке Диагностика → Ошибки и в боковой панели отладка.

  • Кнопка подсвечивается, но дальнейшие шаги не происходят. Добавьте в обработчик node.warn('шаг 1') перед каждым шагом и смотрите панель отладка; проверьте имена устройств через node.log(ctrl.devices().join(', ')).

  • Команды уходят дважды. Кнопку «Перерыв» обрабатывают два узла: готовый из «Демо-зала» и ваш. Оставьте один (см. примечание выше).

  • Нажатие на панели ничего не запускает. Имя интерфейса или кнопки в коде не совпадает с UI-Designer, либо вы не нажали Развернуть после правки.

Как остановить сценарий

Откройте окно узла, выключите переключатель Включено и нажмите Развернуть: нажатия на «Перерыв» перестанут запускать шаги. Можно удалить узел и нажать Развернуть.