Внимание! На данный момент система поддерживает работу со скриптами только на языке 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 |}}