Внимание! На данный момент система поддерживает работу со скриптами только на языке JavaScript, в связи с этим для понимания этой статьи необходимо обладать базовыми навыками работы с этим языком.
====== Приложения в DevelSCADA ======
Система DevelSCADA поддерживает широкий спектр возможностей по расширению функционала с помощью [[script|скриптов]], однако эти возможности все равно ограничены средствами, предоставляемыми самой SCADA системой, заложенной в нее разработчиками системы. При этом не редко есть необходимость расширить данный функционал, и зачастую для этого единственный вариант - просить разработчиков его реализовать внутри SCADA системы. Чаще всего такие запросы просто игнорируются, либо сильно растягиваются по срокам.
DevelSCADA изначально была спроектирована как дружественная к разработчику система, и она позволяет самостоятельно расширять свой функционал. Для этого в системе предусмотрен механизм **"Приложений"**, позволяющий без каких либо ограничений добавлять необходимый функционал в ядро системы.
Данный механизм может быть полезен, к примеру, для интеграции DevelSCADA с какими-то сторонними системами или сервисами, имеющими специфичные интерфейсы взаимодействия, не предусмотренные в базовой поставке SCADA системы, а так же с устройствами, имеющие специфичные, свои собственные протоколы.
Сама DevelSCADA так же разработана как набор приложений, взаимодействующих между собой. Поэтому в дистрибутиве имеется уже набор встроенных приложений, таких как:
\\
* Менеджер процессов (pm)
* Сервер (server)
* Редактор проектов (editor)
* Нативный клиент (client)
\\
Каждое приложение имеет свой интерфейс взаимодействия, который описан в разделе [[start#функции_системных_приложений|Функции системных приложений]]
Список приложений системы можно увидеть в менеджере процессов во кладке "Диспетчер приложений".
{{ ::app1.png?nolink |}}
В данном разделе имеется возможность контролировать и управлять работой приложений.
====== Создание собственного приложения ======
При первом запуске DevelSCADA создаст (если ее не было) папку "DevelSCADA" в домашней папке пользователя "Документы", а в ней, в свою очередь подпапку "app", в которой SCADA система и будет искать приложения пользователей.
Приложение должно состоять из одного файла *.js, являющимся JavaScript скриптом. Имя файла должно быть уникальным и не пересекаться с именами системных приложений. Если имя файла будет таким же как у системного приложения, то DevelSCADA попытается использовать его взамен системного. В теории это позволяет заменять встроенные компоненты системы на собственные, если есть такое желание.
Для примера создадим в данной папке файл **"myapp.js"**.
{{ ::app2.png?nolink |}}
Файл приложения подключается к ядру системы в процессе запуска 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 не требуется, достаточно будет остановить и запустить приложение в диспетчере приложений.
Если все сделано правильно, в списке мы должны увидеть наше приложение.
{{ ::app3.png?nolink |}}
Для запуска приложения необходимо нажать кнопку **"Запустить"**.
{{ ::app4.png?nolink |}}
В результате чего, при удачном запуске приложения, его статус должен поменяться на **"запущен"**.
{{ ::app5.png?nolink |}}
Если открыть системный журнал, то в нем увидим сообщения о запуске нашего приложения и сообщение, выводимое нами.
{{ ::app6.png?nolink |}}
====== Взаимодействие с системой DevelSCADA ======
Ранее написанное приложение способно выполняться внутри среды DevelSCADA, но при этом практически никак не взаимодействует с ней. Для взаимодействия с системой используется объект **rpc**, который передается в функцию **main** при ее запуске. Данный объект содержит в себе функции описания собственного интерфейса приложения, а так же функции взаимодействия с интерфейсом других приложений.
Система DevelSCADA предоставляет два основных механизма взаимодействия приложений:
- **вызов функций**;
- **получение сигналов**.
Механизм **вызова функций** аналогичен процедуре вызова обычных функций внутри кода программы, но при этом выполняется из стороннего приложения. При вызове функции так же в качестве аргументов можно передать какие-то параметры и принять какие-то значения - результат выполнения функции. Механизм **получения сигналов** похож на вызов функции, только работает наоборот. Приложение подписывается на какой-то "сигнал" стороннего приложения (описанного в его интерфейсе), и вешает на него функцию-обработчик. При возникновении данного сигнала (возникновения события) в стороннем приложении, наше приложение его примет и вызовет функцию-обработчик. Сигналы так же могут передавать какие-то дополнительные данные в функцию-обработчик.
При всем этом приложения могут взаимодействовать между собой как в пределах одной среды исполнения (одного ПК), так и по сети с приложениями, работающими на удаленных системах.
Основные отличия между этими механизмами: функции мы вызываем в нужный нам момент, из конкретного приложения, и можем получить результат вызова, а сигнал может выдаваться нескольким приложениям сразу, или ни одному (в зависимости от того, кто подписан на этот сигнал), при этом обратно в метод, испускающий сигнал, никакие данные вернуться не могут.
//Важно! Так как механизмы взаимодействия передают данные между разными приложениями, в качестве аргументов не могут использоваться внутриязыковые объекты языка программирования, которые не могут быть представлены в качестве примитивных типов или JSON (к примеру нельзя передать объект Date, для него, к примеру, можно использовать преобразование в unixstamp).//
===== Вызов функции стороннего приложения =====
Для вызова функций процессов используется метод **call** объекта **rpc**. В качестве первого аргумента ему передается имя процесса и имя вызываемой функции процесса, разделенных точкой. Если вызывается функция из менеджера процессов, то его имя (pm) и точку можно опустить. В качестве второго аргумента передается массив данных, который будет использоваться в качестве аргументов вызова функции приложения. Если функция приложения не имеет аргументов, то можно передать пустой массив, или опустить его вовсе.
Разработчик приложения должен предоставлять описание интерфейса своего приложения в сопутствующей документации. У нас имеется [[start#менеджер_процессов|документация]] к системному приложению "Менеджер процессов" (pm). Для примера воспользуемся его функцией [[pm.getversion|pm.getVersion]].
Вставим следующий код в функцию **main** нашего приложения:
let v = await rpc.call('pm.getVersion', []);
log('Версия платформы:', v);
Перезапустим наше приложение через диспетчер приложений, и в системном журнале мы должны увидеть текущую версию платформы.
{{ ::app7.png?nolink |}}
===== Получение сигналов из стороннего приложения =====
Чтобы получить сигнал стороннего приложения, объект **rpc** имеет метод **listen**, который принимает в качестве первого аргумента имя приложения и имя сигнала, разделенные точкой, который будем отслеживать. Вторым аргументом можно передать объект-фильтр, который в зависимости от имени поля и значения будет перехватывать только те сообщения, в данных которого эти поля будут совпадать. Если же хотим перехватывать все сообщения данного сигнала, то в качестве второго аргумента необходимо указать значение **null**. Третьим аргументом передается функция обработчик, которая будет вызываться при возникновении сигнала. Функция обработчик может иметь единственный аргумент, в который приложение, испустившее сигнал, может передать какие-либо сопутствующие данные.
Менеджер процессов может испускать сигналы при запуске и остановке процессов, называемые соответственно **run** и **stop**. Напишем следующий код, который будет их отслеживать:
rpc.listen('pm.run', null, (data) => {
log('Данные запущенного приложения:', data);
});
rpc.listen('pm.stop', null, (data) => {
log('Данные остановленного приложения:', data);
});
Перезапустим наше приложение и во время его работы откроем на редактирование какой-нибудь проект. После чего закроем редактор. В результате чего в системном журнале мы должны увидеть сообщения следующего вида:
{{ ::app8.png?nolink |}}
===== Создания интерфейса функции =====
Чтобы создать функцию приложения, доступную для других приложений системы, ее необходимо зарегистрировать в ядре с помощью метода **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**, после чего инкрементируя его. Результаты работы данного кода мы можем увидеть в системном журнале.
{{ ::app9.png?nolink |}}
===== Создания интерфейса сигнала =====
Сигнал, так же как и функцию, необходимо зарегистрировать в системе, чтобы сторонние приложения могли подключиться к нему для отслеживания. Регистрация выполняется методом **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 мы отслеживаем события нашего сигнала. В результате выполнения данного кода мы должны увидеть в системном журнале сообщения следующего вида:
{{ ::app10.png?nolink |}}
====== Вызов функции из скриптов DevelSCADA ======
Помимо взаимодействия между приложениями, данный механизм позволяет работать и с пользовательскими скриптами самой системы DevelSCADA при разработке проектов посредством вызова функции [[ds.rpccall|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**, которая будет из операционной системы запускать блокнот. Запустим это приложение в диспетчере приложений.
{{ ::app5.png?nolink |}}
Далее в редакторе проектов создадим новый проект, на экране разместим кнопку, и в событии кнопки "Нажатие", выберем действие "Скрипт".
{{ ::app11.png?nolink |}}
{{ ::app12.png?nolink |}}
Далее напишем следующий код скрипта:
async function main(val) {
await ds.rpcCall('myapp.myFunc');
}
Данный код должен будет вызвать **myFunc** из приложения **myapp**. Запустим проект на исполнение и нажмем кнопку. В результате должен будет открыться блокнот.
{{ ::app13.png?nolink |}}