Перейти к содержанию

Написание скриптов

Запуск редактора

Для запуска "Редактора скриптов" необходимо выбрать соответствующий пункт меню.

Окно редактора включает в себя следующие функциональные области:

  1. Файлы скриптов — область в которой можно создавать, редактировать скрипты и модули проекта.
  2. Рабочая область — область для написания кода для выбранного скрипта на языке Lua.
  3. Примеры — примеры кусков ​​кода ​​​​​наиболее востребованных операций, применяемых в контексте работы с системой. Пример на рабочую область можно добавить, дважды нажав на нем левой кнопкой мыши.
  4. Отладочная консоль — окно сообщений от компилятора.
  5. Компилировать — кнопка запуска сборки скрипта.
  6. Подключиться к серверу — кнопка подключения к серверу для отладки.
  7. Панель переключения режимов — кнопки переключения режимов разработки и отладки скрипта.

В верхней части окна "Список скриптов" доступна панель иснтрументов:

  • ➕ — создать новый файл.
  • ❌ — удалить выбранный файл.
  • 📁 — добавить каталог для группировки файлов.

В области редактирования есть возможность использовать следующие сочетания клавиш:

  • Ctrl+C — копировать выделенные символы.
  • Ctrl+V — вставить данные из буфера обмена.
  • Ctrl+A — выделить все символы в области редактирования.
  • Ctrl+X — вырезать выделенные символы.
  • Ctrl+Z — отменить последнее действие.
  • Ctrl+Y — повторить отмененное действие.

Общая информация

В Симплайт 5 реализован механизм горячей перезагрузки скриптов, позволяющий применять изменения в коде модулей без полной перезагрузки ядра системы. После развертывания проекта на сервер происходит перезагрузка только измененных модулей.

Работа "Редактора скриптов" построена на модели, которая предполагает наличие файла скрипта main.lua и файлов с подключаемыми модулями.

Для создания скрипта необходимо нажать на кнопку ➕ на панели инструментов. Откроется диалоговое окно "Создание файла" и система автоматически предложит создать новый файл с именем main.lua.

Важно

Изменение имени файла скрипта main.lua не допускается.
При именовании последующих файлов проекта (модулей) необходимо обязательно указывать расширение .lua для каждого.

Файл main.lua является основным. Он предоставляет возможность вызова всех дополнительных модулей.

Компиляция

После создания скрипта необходимо его скомпилировать с помощью соответствующей кнопки. В случае успешной компиляции в отладочной консоли появится сообщение "Компиляция выполнена успешно". В противном случае будет представлен список ошибок с указанием строки, позиции и описанием проблемы.

Логические ошибки в скриптах, которые не могут быть обнаружены на этапе компиляции, будут записаны в журнале отладки.

Пользовательская библиотека

Для того, чтобы добавить пользовательскую библиотеку необходимо поместить модуль, написанный на языке Lua, в общий каталог модулей программы: C:\Users\Public\Documents\SimpLight_5.0\scripts\sdk. После этого вызвать его соответствующим образом в скрипте. Такой подход рассмотрен в примере "Время наработки тега".

Печать сообщения

Для того чтобы вывести строку приветствия в журнале сообщений сервера, необходимо добавить в скрипт функцию print и передать ей в качестве аргумента любую строку, например "Hello, World!". Затем необходимо нажать кнопку "Компилировать". Если компиляция выполнена успешно, то следует сохранить проект.

Чтобы увидеть данное сообщение в списке служебных, следует выбрать уровень логирования "DEBUG" в настройках. Далее развернуть проект на сервер. Как только откроется окно клиента необходимо перейти в "Панель управления" для доступа к служебным сообщениям. Среди всех сообщений должна появиться строка приветствия, которую сформировал скрипт main.lua на сервере.

Для добавления дополнительного модуля необходимо повторно нажать кнопку ➕ на панели инструментов, в диалоговом окне указать имя и активировать флаг "Добавить вызов модуля в скрипт main.lua". Это позволит автоматически добавить вызов модуля в основной скрипт.

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

main.lua
require("cycle")            -- вызов модуля cycle в скрипте main проекта

cycle.lua
setInterval(function()      -- объявление циклически выполняемой функции 
    print("cyclic message") -- печать сообщения "cyclic message"
end, 1000)                  -- с интервалом 1000 мс (1 секунда)
После сборки и отправки проекта на сервер скрипт активирует модуль cycle, который будет генерировать служебные сообщения на сервере с частотой один раз в секунду. В этих сообщениях будет содержаться строка "cyclic message".

Чтение и запись значений в теги

В Симплайт 5 поддерживаются два основных подхода к реализации скриптов:

  • асинхронный — основан на использовании блока async/await и применяется для операций, требующих ожидания внешних событий — таких как запросы к оборудованию, ожидание значений тегов или работа с базами данных. Данный подход не блокирует основной поток выполнения системы.
  • синхронный — используется для определенных вычислений, обработки данных в реальном времени и критичных по времени операций, где выполнение скрипта должно быть завершено за минимальный промежуток времени.

Выбор подхода зависит от характера решаемой задачи: асинхронные функции рекомендуются для операций ввода-вывода и длительных процессов, тогда как синхронные — для вычислений с гарантированным временем выполнения и обработки данных в циклах управления.

Ниже представлен пример работы асинхронного подхода. Модуль async упрощает работу с асинхронным кодом, позволяя описывать последовательности асинхронных операций так, как если бы они были синхронными. Функция, объявленная с помощью модуля async, может содержать вызовы с оператором await, который "приостанавливает" выполнение кода до завершения указанной асинхронной операции.

Пример:
Необходимо вычислить среднее значение тегов supplyWaterTemp и tankTemp, которые представляют собой температуру воды в подающем трубопроводе и температуру воды в баке-аккумуляторе соответственно, и присвоить значение тегу averageTemp.

averageTemp.lua
local async = require("async")                      -- загрузка модуля для выполнения асинхронных функций
local tagApi = require("simplight.api.channels")    -- загрузка модуля чтения/записи тегов через функции readMany и writeMany

setInterval(function()                              -- функция циклического выполнения
 async(function()                                   -- async принимает анонимную функцию

    -- await приостанавливает выполнение async до получения функцией readMany значений тегов
    local tags = await(tagApi.readMany({ "supplyWaterTemp", "tankTemp"})) 

    local supply = tags[1].value        -- извлечение значения тега "supplyWaterTemp" 
    local tank = tags[2].value          -- извлечение значения тега "tankTemp" 
    local average = (supply + tank)/2   -- вычисление среднего значения 

    -- запись в тег averageTemp значения переменной average и признака качества сигнала (0xC0 — хорошее качество)
    await(tagApi.writeMany({values = {{ name="averageTemp", value=average, quality=0xC0 }} 
    }))
  end):catch(function(err)              -- обработка возможных ошибок
    print("error", err)
  end)
end, 1000) -- интервал выполнения функции setInterval (1000 мс)

Результат вычислений значения тега averageTemp скрипта после развёртывания проекта на сервере можно проверить с помощью теста тегов проекта.

Обработка сигнала с задержкой

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

Пример:
Если значение температуры averageTemp, рассчитанное предыдущим скриптом, будет превышать 10 °C в течение 5 секунд, то будет считаться, что возникла аварийная ситуация (тег alarmTag будет установлен в значение true).

alarmDelay.lua
local async = require("async")                      -- загрузка модуля асинхронных операций                  
local tagApi = require("simplight.api.channels")    -- загрузка модуля работы с тегами
local time = require('time')                        -- загрузка модуля работы со временем

local tempAlTime                                    -- таймер аварии. Если аварии нет, то значение остаётся nil      

setInterval(function() 
    async(function()  
        local tempAvg = (await(tagApi.readMany({"averageTemp"})))[1].value
        if tempAvg > 10 then                        -- проверка условия превышения температуры
            if not tempAlTime then
                tempAlTime = time.now() + 5 * 1000  -- расчет времени задержки: текущее время +  5 секунд  
            elseif time.now() > tempAlTime then     -- если текущее значение времени превышает время задержки
                await(tagApi.writeMany({values = {{name = "alarmTag", value = true, quality = 0xC0}}  -- в аварийный тег записывается true и пр.качества
                }))
                tempAlTime = nil                    -- и выполняется сброс переменной таймера для следующего цикла
            end
        else
            tempAlTime = nil
            -- если условие превышения температуры не выполнилось в аварийный тег записывается false
            await(tagApi.writeMany({values = {{ name = "alarmTag", value = false, quality = 0xC0}} 
            }))
        end
    end):catch(function(err)                      -- обработка возможных ошибок
    print("error", err)
  end)
end, 1000)

Для проверки корректности перехода тега alarmTag в аварийное состояние после разворачивания проекта на сервер, необходимо выполнить проверку с использованием теста тегов проекта.

Пользовательские события

Все пользовательские события в Симплайт 5 обрабатываются с помощью скриптов.

Пример:
Необходимо сформировать 2 типа пользовательских событий:

  • Аварийное событие — переход тега alarmTag из состояния false в состояние true.
  • Информационное событие — переход тега alarmTag из состояния true в состояние false.

В зависимости от типа события на мнемосхеме оператора должно отображаться соответствующее сообщение. В окне "Редактор категорий событий" нет возможности отредактировать текст сообщения пользовательских событий — доступно только изменение цвета текста и фона сообщения. Все остальные параметры будут обрабатываться программно.

В данном скрипте отсутствует циклический опрос, присутствовавший в предыдущих примерах. Работа программы подразумевает программную реакцию на прошедшее событие в виде перехода значения тега tagAlarm из одного состояние в другое.

userEvents.lua
local async = require("async")               -- загрузка модуля для выполнения асинхронных функций
local events = require("simp-events")        -- загрузка модуля для работы с событиями
local funcs = require("funcs")               -- загрузка модуля вспомогательных функций

local EventType = funcs.eventType            -- переменная для работы с событиями

events.on("channelChanges", function(event)  -- подписка на событие "channelChanges" (изменение тегов)
  local changes = event.changes

  if changes[1].name == "alarmTag" then      -- проверка, если имя измененного канала "alarmTag"
    Actions_LogEvents(changes[1].value)      -- вызывается функция с новым значением тега
  end
end)

function Actions_LogEvents(newValue)
  async(function()
    print("Actions.LogEvents")
    if newValue == true then                 -- если новое значение TRUE формируется аварийное событие
      funcs.logEvents({
        type=EventType.alarm,
        text="Авария средней температуры воды",
      })
    else                                     -- если новое значение FALSE формируется информационное событие
      funcs.logEvents({
        type=EventType.info,
        text="Средняя температура воды в норме",
      })
    end
  end):catch(function(err)                   -- обработка возможных ошибок
    print("error", err)
  end)
end
Для проверки корректности формирования событий необходимо разместить компонент "Последнее событие" на мнемосхеме и развернуть проект на сервер. Изменение значения тега alarmTag фиксируется в таблице событий соответствующим уведомлением.

Также система регистрирует данные о событии в журнале событий.

Время наработки тега

В данном разделе будет рассмотрен пример добавления пользовательской библиотеки. Для этого необходимо поместить модуль, написанный на языке Lua, в рабочий каталог программы.

Например, для решения задачи подсчета времени наработки агрегата в системе требуется создать скрипт с именем motoHours.lua, а затем разместить его в специально созданном каталоге для пользовательских библиотек, например, с именем lib. Данный каталог должен быть расположен по пути: C:\Users\Public\Documents\SimpLight_5.0\scripts\sdk.

motoHours.lua
local async = require("async")
local tagApi = require("simplight.api.channels")
local time = require("time")
local promise = require("promise")

local function timeSince(t)
  local elapsedTime = time.now() - t
  return elapsedTime
end

local function localStorage_getNumberDef(key, default)
  local v = localStorage:getItem(key, default)
  if v == nil then
    return default
  end

  return tonumber(v)
end

local function composeLabel(tagName)
  return string.format('$simplight.motoHours.%s.totalWorkTime', tagName)
end

local M = {}
local watchByTagNameMap = {}

local function checkTag(tagName, tagValue)
  local watch = watchByTagNameMap[tagName]
  if watch == nil or watch.isCalcing then
    return promise.resolve(true)
  end

  return async(function()
    watch.isCalcing = true
    local label = composeLabel(watch.watchTagName)
    local totalWorkTime = localStorage_getNumberDef(label, 0)

    local v = watch.valueGetter(tagValue)
    if v == 1 or v == true then
      if not watch.isRun then
        watch.lastUpdateTime = time.now()
        watch.isRun = true
      end

      totalWorkTime = totalWorkTime + timeSince(watch.lastUpdateTime)
      watch.lastUpdateTime = time.now()
    else
      if watch.isRun then
        totalWorkTime = totalWorkTime + timeSince(watch.lastUpdateTime)
        watch.lastUpdateTime = 0
        watch.isRun = false
      end
    end

    if watch.resetTagName ~= '' then
      local resetTag = await(tagApi.read(watch.resetTagName))
      if resetTag.value then
        totalWorkTime = 0
        watch.lastUpdateTime = 0
        watch.isRun = false
        await(tagApi.write(watch.resetTagName, 0, 0xC0))
      end
    end

    watch.totalWorkTime = totalWorkTime

    localStorage:setItem(label, totalWorkTime)
  end):catch(function(err)
    print("MotoHours.checkTag:", err)
  end):finally(function()
    watch.isCalcing = false
  end)
end

function M.watch(options)
  local watchTagName = options.watchTagName

  if type(watchTagName) ~= "string" then
    error("motoHours.watch: watchTagName must be string")
  end

  local label = composeLabel(watchTagName)
  local totalWorkTime = localStorage_getNumberDef(label, 0)

  local lWatch = {
    lastUpdateTime = 0,
    isRun = false,
    resetTagName = options.resetTagName,
    valueGetter = options.valueGetter,
    totalWorkTime = totalWorkTime,
    watchTagName = watchTagName,
  }

  if valueGetter == nil then
    lWatch.valueGetter = function(srcValue)
      return srcValue
    end
  end

  watchByTagNameMap[watchTagName] = lWatch

  local intervalId = setInterval(function()
    async(function()
      local tag = await(tagApi.read(watchTagName))
      await(checkTag(tag.name, tag.value))
    end):catch(function(err)
      print("motoHours.watch: interval:", err)
    end)
  end, 10000)

  return {
    cancel = function()
      clearInterval(intervalId)
      watchByTagNameMap[watchTagName] = nil
    end,
    getTotalWorkTime = function()
      return lWatch.totalWorkTime or 0
    end
  }
end

return M

Чтобы подключить данный модуль в скрипте требуется оформить вызов:

local motoHours = require("lib.motoHours")

Пример:
Выводить сообщение с временем наработки тега waterPump в служебных сообщениях сервера каждые 10 секунд.

getWorkTime.lua
local async = require("async")
local motoHours = require("lib.motoHours")

local watch = motoHours.watch({
  watchTagName = "waterPump",
  resetTagName = "resetHours",
})

setInterval(function()
  local t = watch.getTotalWorkTime() / 1000
  print(string.format("Время наработки тега: %d сек", t))
end, 10 * 1000)

Формирование отчета

Этот раздел содержит пример по формированию отчета напрямую из скрипта. Скрипты позволяют программно генерировать документы, извлекать данные, а также настраивать выходные форматы для последующего использования.

Пример:
Необходимо формировать отчет при записи значения в тег, а также в соответствии с расписанием в 7:00 и 21:00.

local async = require("async")
local time = require("time")
local reportApi = require("reports")
local events = require("simp-events")
local tagApi = require("simplight.api.channels")

local function build()
  async(function()
    print("build")

    local lEnd = time.now()                               -- конечная временная точка
    local lStart = lEnd - 60 * 60 * 1000                  -- вычисление начальной точки (за час до текущего времени)

    await(tagApi.write("Report.StartTime", lStart, 0xC0)) -- запись начальной временной точки
    await(tagApi.write("Report.EndTime", lEnd, 0xC0))     -- запись конечной временной точки

    await(reportApi.buildReport({                         -- вызов функции построения отчета с указанием пути сохранения отчета
      templatePath = "УтроВечер",
      actions = {
        save = {
          filepath = "C:/Users/User/Desktop/moringEvening.pdf",
        }
      },
    }))
  end):catch(function(err)
    print("error", err)
  end)
end

events.on("channelWrite", function(event)                 -- формирование отчета по записи в тег СформироватьОтчет
  for _, w in ipairs(event.writes) do
    if w.name == "СформироватьОтчет" then
      build()
    end
  end
end)

setInterval(function()                                    -- планирование построение отчета в 7 утра и 21 час вечера.
  local parts = time.toTable(time.now())

  if (parts.hour == 7 and parts.min == 0)
      or (parts.hour == 21 and parts.min == 0) then
    build()
  end
end, 60 * 1000)