Система 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 предоставляет два основных механизма взаимодействия приложений:

  1. вызов функций;

  2. получение сигналов.

Механизм вызова функций аналогичен процедуре вызова обычных функций внутри кода программы, но при этом выполняется из стороннего приложения. При вызове функции так же в качестве аргументов можно передать какие-то параметры и принять какие-то значения - результат выполнения функции. Механизм получения сигналов похож на вызов функции, только работает наоборот. Приложение подписывается на какой-то «сигнал» стороннего приложения (описанного в его интерфейсе), и вешает на него функцию-обработчик. При возникновении данного сигнала (возникновения события) в стороннем приложении, наше приложение его примет и вызовет функцию-обработчик. Сигналы так же могут передавать какие-то дополнительные данные в функцию-обработчик.

При всем этом приложения могут взаимодействовать между собой как в пределах одной среды исполнения (одного ПК), так и по сети с приложениями, работающими на удаленных системах.

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

Важно! Так как механизмы взаимодействия передают данные между разными приложениями, в качестве аргументов не могут использоваться внутриязыковые объекты языка программирования, которые не могут быть представлены в качестве примитивных типов или 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. Запустим проект на исполнение и нажмем кнопку. В результате должен будет открыться блокнот.

Комментарии (0)