Система DevelSCADA поддерживает широкий спектр возможностей по расширению функционала с помощью скриптов, однако эти возможности все равно ограничены средствами, предоставляемыми самой SCADA системой, заложенной в нее разработчиками системы. При этом не редко есть необходимость расширить данный функционал, и зачастую для этого единственный вариант - просить разработчиков его реализовать внутри SCADA системы. Чаще всего такие запросы просто игнорируются, либо сильно растягиваются по срокам.
DevelSCADA изначально была спроектирована как дружественная к разработчику система, и она позволяет самостоятельно расширять свой функционал. Для этого в системе предусмотрен механизм «Приложений», позволяющий без каких либо ограничений добавлять необходимый функционал в ядро системы.
Данный механизм может быть полезен, к примеру, для интеграции DevelSCADA с какими-то сторонними системами или сервисами, имеющими специфичные интерфейсы взаимодействия, не предусмотренные в базовой поставке SCADA системы, а так же с устройствами, имеющие специфичные, свои собственные протоколы.
Сама DevelSCADA так же разработана как набор приложений, взаимодействующих между собой. Поэтому в дистрибутиве имеется уже набор встроенных приложений, таких как:
Менеджер процессов (pm)
Сервер (server)
Редактор проектов (editor)
Нативный клиент (client)
Каждое приложение имеет свой интерфейс взаимодействия, который описан в разделе документации «Функции системных приложений».
Список приложений системы можно увидеть в менеджере процессов во кладке «Диспетчер приложений».

В данном разделе имеется возможность контролировать и управлять работой приложений.
Создание собственного приложения
При первом запуске DevelSCADA создаст (если ее не было) папку «DevelSCADA» в домашней папке пользователя «Документы», а в ней, в свою очередь подпапку «app», в которой SCADA система и будет искать приложения пользователей.
Приложение должно состоять из одного файла *.js, являющимся JavaScript скриптом. Имя файла должно быть уникальным и не пересекаться с именами системных приложений. Если имя файла будет таким же как у системного приложения, то DevelSCADA попытается использовать его взамен системного. В теории это позволяет заменять встроенные компоненты системы на собственные, если есть такое желание.
Для примера создадим в данной папке файл «myapp.js».

Файл приложения подключается к ядру системы в процессе запуска DevelSCADA как модуль JavaScript, и для этого модуль должен содержать набор экспортируемых полей, необходимый для работы в системы, в частности:
поле «about» - структура, описывающая информацию о приложении и настроек его работы;
поле «main» - функция, запускаемая при старте приложения.
Для начала можно использовать шаблон приложения следующего вида:
'use strict'; let logCtx = null, log = null; let rpc = null, cfg = null; async function main(ctx) { ({ logCtx, rpc, cfg } = ctx); ({ log } = logCtx); module.paths.push(ctx.modPath); // } exports.main = main; exports.about = { mark: '**', descr: 'Application name', isSingle: true, isGui: false, isPm: false, order: 1000, depend: [] };
В данном коде производятся все минимально необходимые для работы приложения операции.
Поле «about» содержит следующие поля:
mark - двухсимвольная метка приложения, которой будут помечаться сообщения, выводимые в системный журнал;
descr - название приложения, которое будет отображаться в диспетчере приложений;
isSingle - указывает системе, может ли данное приложение запускаться несколько раз (значение false), или может запускаться только один экземпляр приложения (значение true);
isGui - указывает системе, имеет ли приложение графический интерфейс в виде окна (значение true), или будет работать в фоне, управляемое только через диспетчер приложений (значение false);
isPm - указывает системе, является ли приложение менеджером процессов (значение true), не стоит включать это поле, если приложение не разрабатывается как замена системному менеджеру процессов;
order - порядок сортировки приложений в списке диспетчера приложений;
depend - список зависимостей от других приложений, если они не были запущены до запуска текущего, то они будут автоматически предварительно запущены.
В функцию main при запуске передается аргумент ctx, содержащий в себе набор объектов, посредством которых можно в дальнейшем взаимодействовать с ядром системы и другими приложениями. В начале функции из нее выбираются наиболее часто используемые объекты, и сохраняются в глобальной области видимости, для удобства дальнейшей работы с ними. В частности это:
logCtx - объект, содержащий набор функций для работы с журналом системы, такие как: log - для вывода отладочных сообщений, logError - для вывода сообщений об ошибках, logInfo - для вывода информационных сообщений;
rpc - объект, реализующий механизмы межпроцессорного взаимодействия;
cfg - содержит различные настройки системы и приложения;
modPath - содержит путь к модулям node.js, установленных в системе (можно их использовать в своих приложениях, чтобы не устанавливать повторно).
В шаблоне поправим поле about, чтобы описать наше приложение, а так же, по классике, добавим код, выводящий отладочное сообщение о том, что приложение удачно запустилось. После правок код должен иметь следующий вид:
'use strict'; let logCtx = null, log = null; let rpc = null, cfg = null; async function main(ctx) { ({ logCtx, rpc, cfg } = ctx); ({ log } = logCtx); module.paths.push(ctx.modPath); log('Привет!'); } exports.main = main; exports.about = { mark: 'MA', descr: 'Мое приложение', isSingle: true, isGui: false, isPm: false, order: 1000, depend: [] };
Для того чтобы DevelSCADA увидела новое приложение, ее необходимо перезапустить, и оно появится в списке приложений. Если код содержит какие-то ошибки, то можно в системном журнале посмотреть, что их вызвало. В дальнейшем, при правке кода приложения, перезапуск всей системы DevelSCADA не требуется, достаточно будет остановить и запустить приложение в диспетчере приложений.
Если все сделано правильно, в списке мы должны увидеть наше приложение.

Для запуска приложения необходимо нажать кнопку «Запустить».

В результате чего, при удачном запуске приложения, его статус должен поменяться на «запущен».

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

Взаимодействие с системой DevelSCADA
Ранее написанное приложение способно выполняться внутри среды DevelSCADA, но при этом практически никак не взаимодействует с ней. Для взаимодействия с системой используется объект rpc, который передается в функцию main при ее запуске. Данный объект содержит в себе функции описания собственного интерфейса приложения, а так же функции взаимодействия с интерфейсом других приложений.
Система DevelSCADA предоставляет два основных механизма взаимодействия приложений:
вызов функций;
получение сигналов.
Механизм вызова функций аналогичен процедуре вызова обычных функций внутри кода программы, но при этом выполняется из стороннего приложения. При вызове функции так же в качестве аргументов можно передать какие-то параметры и принять какие-то значения - результат выполнения функции. Механизм получения сигналов похож на вызов функции, только работает наоборот. Приложение подписывается на какой-то «сигнал» стороннего приложения (описанного в его интерфейсе), и вешает на него функцию-обработчик. При возникновении данного сигнала (возникновения события) в стороннем приложении, наше приложение его примет и вызовет функцию-обработчик. Сигналы так же могут передавать какие-то дополнительные данные в функцию-обработчик.
При всем этом приложения могут взаимодействовать между собой как в пределах одной среды исполнения (одного ПК), так и по сети с приложениями, работающими на удаленных системах.
Основные отличия между этими механизмами: функции мы вызываем в нужный нам момент, из конкретного приложения, и можем получить результат вызова, а сигнал может выдаваться нескольким приложениям сразу, или ни одному (в зависимости от того, кто подписан на этот сигнал), при этом обратно в метод, испускающий сигнал, никакие данные вернуться не могут.
Важно! Так как механизмы взаимодействия передают данные между разными приложениями, в качестве аргументов не могут использоваться внутриязыковые объекты языка программирования, которые не могут быть представлены в качестве примитивных типов или JSON (к примеру нельзя передать объект Date, для него, к примеру, можно использовать преобразование в unixstamp).
Вызов функции стороннего приложения
Для вызова функций процессов используется метод call объекта rpc. В качестве первого аргумента ему передается имя процесса и имя вызываемой функции процесса, разделенных точкой. Если вызывается функция из менеджера процессов, то его имя (pm) и точку можно опустить. В качестве второго аргумента передается массив данных, который будет использоваться в качестве аргументов вызова функции приложения. Если функция приложения не имеет аргументов, то можно передать пустой массив, или опустить его вовсе.
Разработчик приложения должен предоставлять описание интерфейса своего приложения в сопутствующей документации. У нас имеется документация к системному приложению «Менеджер процессов» (pm). Для примера воспользуемся его функцией pm.getVersion.
Вставим следующий код в функцию main нашего приложения:
let v = await rpc.call('pm.getVersion', []); log('Версия платформы:', v);
Перезапустим наше приложение через диспетчер приложений, и в системном журнале мы должны увидеть текущую версию платформы.

Получение сигналов из стороннего приложения
Чтобы получить сигнал стороннего приложения, объект rpc имеет метод listen, который принимает в качестве первого аргумента имя приложения и имя сигнала, разделенные точкой, который будем отслеживать. Вторым аргументом можно передать объект-фильтр, который в зависимости от имени поля и значения будет перехватывать только те сообщения, в данных которого эти поля будут совпадать. Если же хотим перехватывать все сообщения данного сигнала, то в качестве второго аргумента необходимо указать значение null. Третьим аргументом передается функция обработчик, которая будет вызываться при возникновении сигнала. Функция обработчик может иметь единственный аргумент, в который приложение, испустившее сигнал, может передать какие-либо сопутствующие данные.
Менеджер процессов может испускать сигналы при запуске и остановке процессов, называемые соответственно run и stop. Напишем следующий код, который будет их отслеживать:
rpc.listen('pm.run', null, (data) => { log('Данные запущенного приложения:', data); }); rpc.listen('pm.stop', null, (data) => { log('Данные остановленного приложения:', data); });
Перезапустим наше приложение и во время его работы откроем на редактирование какой-нибудь проект. После чего закроем редактор. В результате чего в систем��ом журнале мы должны увидеть сообщения следующего вида:

Создания интерфейса функции
Чтобы создать функцию приложения, доступную для других приложений системы, ее необходимо зарегистрировать в ядре с помощью метода regFunc объекта rpc. Данный метод в качестве первого аргумента принимает имя функции, по которому его будут вызывать сторонние приложения. Вторым аргументом должна быть функция, которая и будет вызываться и возвращать данные. При этом функция может быть асинхронной.
Для упрощения примера можно весь код создать в пределах одного приложения, и он будет работать так же через механизмы взаимодействия приложений SCADA системы. Для этого создадим код следующего вида:
// код приложения 1 // создание внешней функции приложения rpc.regFunc('myFunc', (v) => { log('Вызов myFunc с аргументом:', v); return v + 10; });
// код приложения 2 // вызов внешней функции приложения let cnt = 0; setInterval(async () => { let res = await rpc.call('myapp.myFunc', [ cnt++ ]); log('Результат вызова:', res); }, 1000);
В коде приложения 1 мы создаем функцию с именем myFunc и аргументом v, доступную для вызова из стороннего приложения, в коде функции с аргументом производим некие математические операции и возвращаем результат исполнения функции вызывающей ее приложению. в коде приложения 2 создаем счетчик cnt, и таймер, срабатывающий раз в секунду, который вызывает нашу созданную функцию как внешнюю из нашего же приложения, передавая в качестве аргумента вызова значение счетчика cnt, после чего инкрементируя его. Результаты работы данного кода мы можем увидеть в системном журнале.

Создания интерфейса сигнала
Сигнал, так же как и функцию, необходимо зарегистрировать в системе, чтобы сторонние приложения могли подключиться к нему для отслеживания. Регистрация выполняется методом regEvent объекта rpc. Данный метод принимает только один аргумент - имя сигнала. Регистрация сигнала не вызывает самого сигнала, а только сообщает системе о его наличие у приложения. Вызов же события сигнала выполняется методом emit объекта rpc, в качестве первого аргумента которого передается имя вызываемого сигнала, а вторым - данные, передаваемые приложению, отслеживающему этот сигнал. Так же, для упрощения примера, весь этот код можно разместить в одном приложении.
Для примера создадим код следующего вида:
// код приложения 1 // создание сигнала rpc.regEvent('myEvent'); // вызов созданного сигнала let cnt = 0; setInterval(async () => { log('Вызов сигнала', cnt); rpc.emit('myEvent', cnt++); }, 1000);
// код приложения 2 // отслеживание и обработка сигнала rpc.listen('myapp.myEvent', null, (data) => { log('Получение сигнала', data); });
В коде приложения 1 мы создаем сигнал с именем myEvent, далее вы вызываем сигнал с интервалом раз в секунду. В коде приложения 2 мы отслеживаем события нашего сигнала. В результате выполнения данного кода мы должны увидеть в системном журнале сообщения следующего вида:

Вызов функции из скриптов DevelSCADA
Помимо взаимодействия между приложениями, данный механизм позволяет работать и с пользовательскими скриптами самой системы DevelSCADA при разработке проектов посредством вызова функции ds.rpcCall ядра исполнения системы. Для примера, создадим в нашем приложении функцию, и вызовем ее из скриптов проекта.
Код приложения:
'use strict'; let logCtx = null, log = null; let rpc = null, cfg = null; async function main(ctx) { ({ logCtx, rpc, cfg } = ctx); ({ log } = logCtx); module.paths.push(ctx.modPath); rpc.regFunc('myFunc', () => { const { exec } = require('node:child_process'); exec('notepad'); }); } exports.main = main; exports.about = { mark: 'MA', descr: 'Мое приложение', isSingle: true, isGui: false, isPm: false, order: 1000, depend: [] };
В данном приложении мы создали единственную функцию myFunc, которая будет из операционной системы запускать блокнот. Запустим это приложение в диспетчере приложений.

Далее в редакторе проектов создадим новый проект, на экране разместим кнопку, и в событии кнопки «Нажатие», выберем действие «Скрипт».


Далее напишем следующий код скрипта:
async function main(val) { await ds.rpcCall('myapp.myFunc'); }
Данный код должен будет вызвать myFunc из приложения myapp. Запустим проект на исполнение и нажмем кнопку. В результате должен будет открыться блокнот.

