<< Click to Display Table of Contents >> Navigation: Modbus Universal MasterOPC Server > Руководство по языку Lua 5.1 > Примеры и полезности > Чтение архивов счетчика "Пульсар" > Создание скрипта |
Приведем код скрипта полностью, а потом разберем его построчно.
--Скрипт считывания часового архива счетчика Пульсар.
local LastTime; --метка времени последнего считанного значения
local NumRecStart; --количество считываемых регистров при старте
local TypeArch=1; -- тип архива - часовой
local GetInput={}; --наличие входов в подустройстве
local NameTagStatic="Вход"; --статическая часть имени входа
local NoInput=true; --в подустройстве нет входов
--функция разбора времени с учетом типа архива
function UnPack(TimeVal)
local Table=time.UnpackTime(TimeVal); --разбиваем время начала на составляющие
Table[5]=0; --задаем начало часа
Table[6]=0;
--формируем новое время
local err,NewTime=time.PackTime(Table[1],Table[2],Table[3],Table[4],Table[5],Table[6]);
Table[1]=Table[1]-2000; --вычитаем 2000 из года
return Table,NewTime;
end;
--преобразования отдельных байт в регистр типа int16
function ByteToInt16(ByteH,ByteL)
local reg=bit.BitLshift(ByteH,8);
reg=bit.BitOr(reg,ByteL);
return reg;
end;
-- инициализация
function OnInit()
--считываем настройку количества считываемых записей при старте
NumRecStart=server.ReadSubDeviceExtProperty("NumRecStart" );
local TimeNow=time.TimeNow(); --получаем текущее время
--получаем метку времени значения с которого начинаем чтение
LastTime=time.TimeAddHour(TimeNow,NumRecStart*(-1));
--проверяем наличие тегов в подустройстве
for i=1,10,1 do
local NameTag=NameTagStatic..(i);
--считываем атрибуты тега
local Err=server.GetAttributeTagByRelativeName(NameTag);
GetInput[i]=not Err; --проверяем существование тега
if Err==false then NoInput=false; end; --теги в подустройстве есть
end;
end
-- функция,выполняющаяся перед чтением тегов
function OnBeforeReading()
if NoInput==true then return; end; --нет ни одного тега в подустройстве – выходим
local StartTime; --время начала архива
local EndTime; --время конца архива
repeat
local SaveLastTime=LastTime; --сохраняем прошлую метку времени
local TimeStartTable={};
TimeStartTable,StartTime=UnPack(LastTime); --разбиваем время начала на составляющие с учетом типа архива
EndTime=time.TimeAddHour(StartTime,62); --получаем время конца
--разбиваем время конца на составляющие с учетом типа архива
local TimeEndTable=UnPack(EndTime);
local Send={}; --таблица отправляемых регистров
--упаковка байт в регистры
Send[1]=ByteToInt16(TimeStartTable[1],TimeStartTable[2]);
Send[2]=ByteToInt16(TimeStartTable[3],TimeStartTable[4]);
Send[3]=ByteToInt16(TimeStartTable[5],TimeStartTable[6]);
Send[4]=ByteToInt16(TimeEndTable[1],TimeEndTable[2]);
Send[5]=ByteToInt16(TimeEndTable[3],TimeEndTable[4]);
Send[6]=ByteToInt16(TimeEndTable[5],TimeEndTable[6]);
local err=false; --объявление переменной ошибки
for i=1,10,1 do
--если данный номер тега есть в подустройстве опрашиваем его архив
if GetInput[i]==true then
Send[7]=ByteToInt16(TypeArch,i); --тип архива и номер канала
err=modbus.WriteHoldingRegistersAsUInt16(3,table.maxn(Send),true,"10325476",false,Send);
if err==true then break; end; --ошибка - выходим из цикла
server.Sleep(100); --ожидание 100 мс.
local Dest={}; --архив принятых значений
local TimeS=StartTime; --метка времени запрос чтения данных
err,Dest=modbus.ReadHoldingRegistersAsFloat(0x100,62,true,"32105476");
if err==true then break; end; --если ошибка - выходим им цикла
for j=1,table.maxn(Dest),1 do --разбор принятых значений
--если значение корректно, и метка времени не превысила текущее время
if Dest[j]>(-3590324220) and TimeS<=time.TimeNow() then
LastTime=TimeS; --сохраяем время как последнее считанное
local TimeStamp=time.TimeToTimeStamp(TimeS,0); --преобразуем в формат TimeStamp
--записываем значение в HDA
server.WriteTagByRelativeNameToHda(NameTagStatic..(i),Dest[j],OPC_QUALITY_GOOD,TimeStamp );
end;
--прибавляем к метке времени 1 час
TimeS=time.TimeAddHour(TimeS,1);
end;
end;
end;
if err==true then --если ошибка - выходим из цикла
server.Message("Ошибка получения данных");
break;
end;
--если метка последнего значения не изменилась
if SaveLastTime==LastTime and EndTime<time.TimeNow() then
LastTime=time.TimeAddHour(LastTime,61);
end;
until EndTime>=time.TimeNow();
server.Message("цикл считывания закончен" );
end
Рассмотрим код построчно. В начале происходит объявление глобальных переменных –код очевиден, поэтому не будем его пояснять. Функции UnPack и ByteToInt16 будут вызываться из основного кода, поэтому их код мы рассмотрим позже.
Перейдем сразу к коду функции OnInit – этот код вызывается один раз при старте OPC сервера.
function OnInit()
--считываем настройку количества считываемых записей при старте
NumRecStart=server.ReadSubDeviceExtProperty("NumRecStart" );
В данной строке, происходит считывание пользовательской настройки – количество считываемых регистров при старте, которую мы назвали "NumRecStart". Ее результат сохраняется в глобальную переменную NumRecStart.
local TimeNow=time.TimeNow(); --получаем текущее время
--получаем метку времени значения с которого начинаем чтение
LastTime=time.TimeAddHour(TimeNow,NumRecStart*(-1));
В этих строках мы получаем время, с которого нужно начать производить считывание записей. Для работы со временем в OPC сервере есть специальные функции – в разделе time. Для получения текущего времени используется функция time.TimeNow(). Функция time.TimeAddHour() предназначена для прибавления заданного количества часов к переменной времени. Первый аргумент функции – время, второй – количество прибавляемых часов. Если нужно вычесть время, то количество прибавляемых часов должны быть отрицательными. Поэтому при вызове этой функции в нашем коде переменная NumRecStart умножается на "минус один".
--проверяем наличие тегов в подустройстве
for i=1,10,1 do
local NameTag=NameTagStatic..(i);
--считываем атрибуты тега
local Err=server.GetAttributeTagByRelativeName(NameTag);
GetInput[i]=not Err; --проверяем существование тега
if Err==false then NoInput=false; end; --теги в подустройстве есть
end;
Возможна ситуация, когда некоторые из входов у пользователя не задействованы – в этом случае опрашивать их тоже нецелесообразно. В этом случае пользователь может просто удалить их из конфигурации.
Данный код производит обработку такой ситуации. Функция server.GetAttributeTagByRelativeName() предназначена для получения атрибутов тега – имени, типа регистра, типа данных, номер регистра и т.д. Если функция возвращает nil – значит тег не найден. При помощи цикла последовательно проверяются существование всех входов Вход1-Вход10. Если тег существует, то в соответствующий индекс массива GetInput записывается true, иначе – false. Глобальная переменная NoInput в начале программы инициализирована значением true. Если в подустройстве найден хоть один тег, то в переменную NoInput записывается – false.
Перейдем к функции OnBeforeReading.
function OnBeforeReading()
--нет ни одного тега в подустройстве - выходим
if NoInput==true then return; end;
Вначале функции мы проверяем состояние переменной NoInput – если оно равно true, то в подустройстве нет ни одного тега и опрашивать нечего. В этом случае происходит выход из функции – вызывается оператор return.
local StartTime; --время начала архива
local EndTime; --время конца архива
repeat
local SaveLastTime=LastTime; --сохраняем прошлую метку времени
local TimeStartTable={};
--разбиваем время начала на составляющие с учетом типа архива
TimeStartTable,StartTime=UnPack(LastTime);
EndTime=time.TimeAddHour(StartTime,62); --получаем время конца
--разбиваем время конца на составляющие с учетом типа архива
local TimeEndTable=UnPack(EndTime);
Данный код определяет время, с которого мы начнем считывать значения из архива и время конца считывания. Согласно спецификации к прибору мы должны послать ему дату и время в отдельных байтах трех регистров. Поэтому сначала нам необходимо разобрать время на составляющие (год, месяц, день, час, минута, секунда) – эту задачу выполняет функция UnPack().
Рассмотрим ее код:
function UnPack(time)
local Table=time.UnPackTime(time); --разбиваем время начала на составляющие
Table[5]=0; --задаем начало часа
Table[6]=0;
--формируем новое время
local NewTime= time.PackTime(Table[1],Table[2],Table[3],Table[4],Table[5],Table[6]);
Table[1]=Table[1]-2000; --вычитаем 2000 из года
return Table,NewTime;
end;
В качестве аргумента, в функцию передается время, которое нужно разобрать. Для разбора времени используется функция time.UnPackTime(). Данная функция возвращает таблицу из 6 элементов (1 элемент – год, 2 – месяц, 3 – день, 4 – час, 5 – минута, 6 – секунда). 5 и 6 элемент мы устанавливаем равным нулю. Дело в том что в системах АСКУЭ, как правило, требуются значения на начало часа. Но поскольку OPC сервер может быть запущен в любое время, то необходимо произвести округление до начала часа – установить часы и минуты равными нулю.
В дальнейшем коде, нам также нужно будет работать с округленным временем, поэтому мы записываем округленное время в переменную NewTime – собираем время из отдельных элементов при помощи функции time.PackTime().
Из первого элемента таблицы - года, необходимо вычесть 2000 – это требование самого прибора.
Затем функция возвращает таблицу с элементами времени и новое, округленное, время начала считывания архива.
В основном коде, к округленному значению мы прибавляем 62 часа (максимальное количество считываемых записей) – это будет время конца. Это время мы также разбираем на составляющие.
В итоге у нас получены две переменные-таблицы TimeStartTable и TimeEndTable, а также переменная StartTime – метка начала.
local Send={}; --таблица отправляемых регистров
--упаковка байт в регистры
Send[1]=ByteToInt16(TimeStartTable[1],TimeStartTable[2]);
Send[2]=ByteToInt16(TimeStartTable[3],TimeStartTable[4]);
Send[3]=ByteToInt16(TimeStartTable[5],TimeStartTable[6]);
Send[4]=ByteToInt16(TimeEndTable[1],TimeEndTable[2]);
Send[5]=ByteToInt16(TimeEndTable[3],TimeEndTable[4]);
Send[6]=ByteToInt16(TimeEndTable[5],TimeEndTable[6]);
Как мы указывали ранее, прибор требует, чтобы время было передано в отдельных байтах трех регистров. Функция ByteToInt16 собирает из двух чисел одно число Int16, которое можно будет послать в прибор. Рассмотрим код функции:
function ByteToInt16(ByteH,ByteL)
local reg=bit.BitLshift(ByteH,8);
reg=bit.BitOr(reg,ByteL);
return reg;
end;
В качестве аргумента, функция получает два числа (байта). Первое число смещается на 8 бит влево, при помощи функции "Побитовый сдвиг влево" - bit.BitLshift() – так формируется старший байт. Младший байт при этом будет забит нулями. Чтобы заполнить его битами второго числа выполняется операция "побитовое ИЛИ" при помощи специальной функции bit.BitOr(). Сформированная переменная reg, отправляется на выход функции.
В итоге, в основном коде у нас сформируется таблица Send, чьи элементы заполнены числами содержащими дату начала и конца запроса архива.
local err=false; --объявление переменной ошибки
for i=1,10,1 do
--если данный номер тега есть в подустройстве опрашиваем его архив
if GetInput[i]==true then
Send[7]=ByteToInt16(TypeArch,i); --тип архива и номер канала
err=modbus.WriteHoldingRegistersAsUInt16(3,table.maxn(Send),true,"10325476",false,Send);
if err==true then break; end; --ошибка - выходим из цикла
Данный код выполняет запись времени в устройство. Поскольку входов у нас 10, то необходимо произвести последовательный опрос всех тегов. Вначале проверяется наличие входа в конфигурации. Если элемент таблицы GetInput с данным номером содержит true – то запрос выполняется.
Элемент 7 таблицы Send, заполняется двумя значениями – типом архива (он установлен как константа 1 – часовой), и номером канала – переменная i.
Теперь таблица Send полностью сформирована, и ее можно отправлять в устройство. Для выполнения запроса записи, используется функция modbus.WriteHoldingRegistersAsUInt16(). В качестве аргументов в нее нужно передать стартовый номер регистра для записи (в устройстве он равен 3), количество передаваемых элементов (у нас их семь – по количеству элементов таблицы Send), также задаем чередование байт, и передаем таблицу со значениями – таблицу Send.
Если операция не будет выполнена (например не будет связи с устройством), то переменной err будет присвоено true. В этом случае вызывается оператор break – выход из цикла.
server.Sleep(100); --ожидание 100 мс.
local Dest={}; --архив принятых значений
local TimeS=StartTime; --метка времени
--запрос чтения данных
err,Dest=modbus.ReadHoldingRegistersAsFloat(0x100,62,true,"32105476");
if err==true then break; end; --если ошибка - выходим им цикла
for j=1,table.maxn(Dest),1 do --разбор принятых значений
--если значение корректно, и метка времени не превысила текущее время
if Dest[j]>(-3590324220) and TimeS<=time.TimeNow() then
LastTime=TimeS; --сохраяем время как последнее считанное
local TimeStamp=time.TimeToTimeStamp(TimeS,0); --преобразуем в формат TimeStamp
server.WriteTagByRelativeNameToHda(NameTagStatic..(i),Dest[j],OPC_QUALITY_GOOD,TimeStamp ); --записываем значение в HDA
end;
TimeS=time.TimeAddHour(TimeS,1); --прибавляем к метке времени 1 час
end;
Согласно инструкции прибора, после отправки запроса и получения ответа, нужно выждать 100 мс, это делается при помощи функции server.Sleep().
После выдержки времени можно приступать к опросу регистров с данными. Для этого используется функция modbus.ReadHoldingRegistersAsFloat(). В качестве аргументов ей нужно передать стартовый адрес – 0x100 (256 в десятичной системе), количество запрашиваемых элементов – 62, и чередование байт.
Если запрос не будет выполнен корректно, то переменной err будет присвоено true, и будет вызвана команда выхода из цикла. Если же данные получены, то они будут записаны в таблицу Dest.
После того как данные получены, можно приступить к их анализу и записи в архив тега. Для этого каждый элемент таблицы поочередно обрабатывается в цикле for.
Строчка
if Dest[j]>(-3590324220) and TimeS<=time.TimeNow() then
Предназначена для проверки правильности принятого числа. Согласно описанию прибора, если в запрашиваемый момент времени значения в архиве нет, то в регистры будет записано FF FF FF FF, что в переводе во Float соответствует числу -3590324220. То есть если принятое число больше этого значения – то значение достоверное и его можно обработать. Значение также не обрабатывается если время значения превысило текущее время (мы обрабатываем значение, которого еще не существует).
После проверки на достоверность, в переменную LastTime сохраняется время считываемого значения. Это необходимо, чтобы после завершения всего цикла считывания у нас сохранилась последняя метка времени – именно с нее мы и начнем считывание в следующем цикле.
При помощи функции time.TimeToTimeStamp() время преобразуется в формат TimeStamp – в нему добавляются миллисекунды.
После того как значение и метка времени получены, и их можно записать в нужный HDA тег, при помощи функции server.WriteTagByRelativeNameToHDA(). Затем, к метке времени значения – к переменной TimeS прибавляется 1 час.
После этого следует код проверки.
if err==true then --если ошибка - выходим из цикла
server.Message("Ошибка получения данных");
break;
end;
--если метка последнего значения не изменилась
if SaveLastTime==LastTime and EndTime<time.TimeNow() then
LastTime=time.TimeAddHour(LastTime,61);
end;
until EndTime>=time.TimeNow();
Первая часть кода не нуждается в особых пояснениях – если при считывании возникла ошибка (переменная err стала равна true), то в лог записывается сообщение и происходит выход из цикла.
Вторая часть кода требует пояснения. Возможна ситуация, когда переменная с последней меткой времени значения (переменная LastTime) после опроса не изменилась. Такая ситуация может возникнуть, если мы начнем считывать архив за очень давний промежуток времени – когда значений в архиве еще нет. В этом случае не изменится и значение переменной EndTime, но цикл опроса завершается именно тогда, когда значение этой переменной превысит текущее время.
В таком случае, чтобы избежать "зацикливания" программы, необходимо увеличить LastTime на 61 час – то есть пропустить пустой промежуток архива. Именно эту функцию и реализует вторая часть кода.
Данный код упрощен – например, скрипт реализует только чтения часового архива. Также может быть полезным ограничение количества получаемых записей – чтобы сделать запросы ответа более короткими (что бывает полезным при сильных помехах).
К OPC серверу прилагается конфигурация "Пульсар", в которой реализовано чтение всех типов архивов, ограничение количества считываемых записей, а также ряд дополнительных программных защит. Код скрипта конфигурации открыт, поэтому читатель может изучить его самостоятельно.
Примечание. Конфигурация OPC сервера с полным кодом данного примера находится по ссылке и называется Пульсар (учебный).mbp