Система 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. Запустим проект на исполнение и нажмем кнопку. В результате должен будет открыться блокнот.
