<< Click to Display Table of Contents >> Navigation: Modbus Universal MasterOPC Server > Руководство по языку Lua 5.1 > Примеры и полезности > Формирование расширенного Modbus запроса > Создание скрипта |
Скрипт устройства содержит три функции:
OnInit () - код из этой функции вызывается при инициализации узла. То есть при старте OPC сервера.
OnClose() – код из этой функции вызывается при закрытии узла, то есть при остановке OPC сервера.
OnRead() – код из этой функции вызывается каждый раз перед опросом тега.
Для отправки запроса в COM порт и получения ответа в OPC сервере есть специальная функция server.SendAndReceiveDataByMask(). Данная функция отправляет и получает данные заданные по маске, что существенно упрощает формирование и обработку данных.
Добавить функцию в код можно двойным щелчком в окне функций.
Нам необходимо опросить два регистра – 0x200 и 0х202. Чтобы избежать дублирования кода, мы создадим собственную функцию, в которую будем передавать номер запрашиваемого регистра. Функция будет возвращать два параметра – флаг корректности запроса (истина или ложь) и вычисленное значение.
Приведем весь код функции (назовем ее Query), необходимый для отправки Modbus запроса и получения ответа, а затем разберем его построчно.
function Query(num_reg)
local send={}; --массив отправляемых чисел
local Addr=server.GetCurrentDeviceAddress();--получения адреса устройства
table.insert(send,Addr); --добавляем в таблицу первый элемент - адрес
table.insert(send,0x19); --добавляем в таблицу второй элемент - идентификатор команды
table.insert(send,num_reg); --добавляем третий элемент - номер регистра
local sendmask={"byte","byte","int16:10"}; --маска отправляемого запроса
local dest={}; --массив полученных чисел
local destmask={"byte","byte","int16:10"}; --маска принимаемого запроса
local err,len;
local n=0;
repeat
--посылка и получение запроса в устройство
err,dest,len=server.SendAndReceiveDataByMask(2,3,sendmask,send,destmask,6);
n=n+1;
--условие выхода - корректный ответ или превышение запросов
until err>=0 or n>=server.GetCurrentDeviceRetry()
--обрабатываем полученные данные
if err>=0 then
--запрос выполнен корректно
return true, dest[3]; --возвращаем флаг что запрос корректен и третий элемент массива – значение
else
return false,0; --запрос некорректен, возвращаем соответствующий флаг
end;
end;
Итак, функция Query получает аргумент – номер регистра.
Начинаем формировать данные для запроса.
local send={} – в данной строчке происходит объявление локальной переменной send и ее инициализация (инициализация происходит в фигурных скобках – в данном случае создается пустая таблица). Данная переменная представляет собой таблицу (массив), в которой будут содержаться числа для отправки в устройство.
local Addr=server.GetCurrentDeviceAddress() – в данной строчке происходит объявление локальной переменной Addr. В данную переменную, при помощи специальной функции (ее также можно добавить из окна функций), записывается заданный адрес устройства.
table.insert(send,Addr) – в данной строчке происходит добавление элемента в таблицу, при помощи функции table.insert(). В качестве аргумента в эту функцию нужно передать таблицу, в которую нужно произвести вставку, и вставляемый элемент. Мы добавляем первый элемент – адрес.
Примечание. Для работы с таблицами (массивами) используется раздел функций table – с их помощью можно добавлять, удалять, сортировать элементы в таблице.
В последующих строчках, мы добавляем в массив остальные элементы – номер функции (0х19), номер регистра (переменная num_reg).
table.insert(send,0x19); --добавляем в таблицу второй элемент - идентификатор команды
table.insert(send, num_reg); --добавляем третий элемент - номер регистра.
Теперь мы сформировали массив send в котором содержаться элементы – (0x01,0x19,0x200).
local sendmask={"byte","byte","int16:10"} – в данной строчке объявляется структура маски запроса. Переменная sendmask – переменная типа таблица. При инициализации, в нее добавляются строковые элементы, каждый из которых будет описывать элемент отправляемой таблицы (в данном случае переменной send). Формат маски – String1:String2, где String1 – формат данных, String2 – строка перестановки. Переменные типа string и byte не требуют строки перестановки, для остальных форматов, ecли строка перестановки не введена, используется "10325476". Рассмотрим каждый элемент таблицы масок. Первые два элемента – типа byte, это соответственно элементы "адрес" и "идентификатор команды". Третий элемент – типа int16 (двухбайтовое знаковое целое), перестановка байт – старшим вперед. Этот элементы описывает "номер регистра". Контрольная сумма ни в таблице send, ни в маске sendmask не описана – так как мы будем использовать стандартную контрольную сумму Modbus. Если контрольная сумма будет использоваться нестандартная – то ее элементы также нужно добавить в маску.
Примечание. Подробнее про работу функции server.SendAndReceiveDataByMask с нестандартной контрольной суммой будет рассказано в одном из следующих примеров.
local dest={} – в данной строчке объявляется переменная-таблица для принимаемых байт.
local destmask={"byte","byte","int16:10"} – в данной строчке объявляется структура маски ответа. Поскольку прибор отвечаем "эхом", то маска ответа имеет такую же структуру как маска запроса.
local err,len - в данной строчке объявляются переменные ошибки и длины ответа.
И наконец, выполнение запроса:
err,dest,len=server.SendAndReceiveDataByMask(2,4,sendmask,send,destmask,6) – в данной строчке происходит посылка данных в устройство и ожидание ответа.
Функция server.SendAndReceiveDataByMask, имеет 6 входных параметров. Разберем их:
Параметр №1 – константа 2. Данный параметр определяет способ подсчета контрольной суммы. Поддержаны контрольные суммы протокола Modbus и DCON. Параметр установлен равный двум – это подсчет контрольной CRC16 (Modbus). Если установить параметр равным 0, то контрольная сумма не будет использоваться, и в этом случае пользователь должен проверять ее корректность самостоятельно.
Параметр №2 – константа 4. Данный параметр задает количество элементов в исходной таблице. В таблице send у нас находится 4 значения.
Параметр №3 – переменная sendmask. Данный параметр – это переменная маски запроса.
Параметр №4 – переменная send. Данный параметр – таблица отправляемых значений.
Параметр №5 – переменная destmask. Данный параметр - таблица масок байт принимаемого ответа. Или nil (пусто) – если функция работает только на передачу.
Параметр №6 – константа 6. Максимальное количество принимаемых байт. Если указать 0, то функция работает только на передачу. В данном случае ожидается 6 байт - 4 байта данных и 2 байта контрольной суммы. Очень важно правильно задать значение этого параметра - если указать большее чем на самом деле количество байт, то функция вернет результат по истечению таймаута (настройки "Время ожидания ответа"), поэтому если тегов много, то опрос будет медленным. Если указать правильное количество байт, то сразу после их приема происходит выход из функция и начинается обработка принятых байт.
Функция возвращает 5 значений.
Первый возвращаемый параметр (присваивается переменной err) – код ошибки. Если код меньше нуля, то произошла ошибка. Расшифровка кодов ошибок есть в справке.
Второй возвращаемый параметр (присваивается dest) – таблица, каждый элемент которой равен числу (или строке) сформированный согласно маске преобразований.
Третий элемент (присваивается len) – количество элементов в принятой таблице.
Четвертый элемент (в данном примере не используется) – строка ответа принятого от устройства. Данный элемент нужен, если используется нестандартная контрольная сумма, и необходимо произвести ее расчет вручную используя каждый байт ответа.
Пятый элемент (в данном примере не используется) – количество принятых от устройства байт. Данный элемент также нужен для ручного расчета нестандартной контрольной суммы.
Таким образом, в случае корректного выполнения запроса и получения ответа, в переменной dest будет содержаться набор преобразованных по маске чисел, в переменной len – количество принятых элементов.
Но если возникнет ошибка, то в переменной err будет отрицательное число - код ошибки. В этом случае нам нужно попытаться выполнить запрос снова – например, на линии возникла помеха и наш запрос был поврежден. Для этого упакуем вызов функции server.SendAndReceiveDataByMask в цикл.
Для подобных случаев, наиболее удобным является цикл repeat – until, это цикл с выходом по условию. Этот цикл всегда выполняется как минимум один раз (что нам и нужно). Цикл имеет следующий синтаксис:
repeat
--код цикла
until условие_выхода
Запрос нам нужно послать такое количество раз, какое задал пользователь у устройства, в параметре "Повторы при ошибке", по умолчанию – 3.
Чтобы получить заданное пользователем количество повторов нужно воспользоваться функцией server.GetCurrentDeviceRetry() – данная функция возвращает значение настройки у текущего устройства.
Условием выхода из цикла должно быть либо корректное получение данных, либо превышенное количество запросов. Для количества выполненных запросов заведем специальную переменную n и инициализируем ее нулем, после каждого запроса мы будем ее инкрементировать. Получим следующий код:
local n=0;
repeat
--посылка и получение запроса в устройство
err,dest,len=server.SendAndReceiveDataByMask(2,4,sendmask,send,destmask,20);
n=n+1;
--условие выхода - корректный ответ или превышение запросов
until err>=0 or n>=server.GetCurrentDeviceRetry()
После этого, если запрос был выполнен корректно, мы можем вернуть (используя оператор return) флаг корректности запроса и полученное от прибора значение. Для этого нужно вернуть третий элемент таблицы dest. Если запрос выполнить не удалось (возникла ошибка), то возвращаем флаг ошибки и ноль, в качестве значения.
if err>=0 then
--запрос выполнен корректно
return true, dest[3]; --возвращаем флаг что запрос корректен и третий элемент массива – значение
else
return false,0; --запрос некорректен, возвращаем соответствующий флаг
end;
Теперь мы можем вызывать функцию Query из основной функции OnRead() и обрабатывать полученные значения.
function OnRead()
noerr,RegH=Query(0x200); --выполняем запрос чтения регистра 200
if noerr==false then --если ответ некорректный
--записываем сообщение в лог и записываем в тег плохой признак качества
server.Message("Ошибка получения данных");
server.WriteCurrentTag(0,OPC_QUALITY_BAD );
return;
end;
noerr,RegL=Query(0x202); --выполняем запрос чтения регистра 202
if noerr==false then
server.Message("Ошибка получения данных");
server.WriteCurrentTag(0,OPC_QUALITY_BAD );
return;
end;
local F=ConvertToFloat(RegH,RegL);
server.WriteCurrentTag(F,OPC_QUALITY_GOOD );
end
В первой строчке кода, мы вызываем написанную нами функцию Query, и передаем ей в качестве аргумента константу – номер регистра 0x200. После выполнения функции, нам необходимо проверить – корректно ли был выполнен запрос. Если запрос был выполнен некорректно и переменная noerr равна false, то нам нужно записать сообщение в журнал сервера и записать значение в тег значение с плохим признаком качества, после чего выйти из функции при помощи оператора return.
Для записи сообщения в журнал используется функция server.Message() – в качестве аргументов ей можно передавать любое количество аргументов (как строковых, так и числовых), они будут преобразованы в строку и выданы в раздел журнала "Сообщения от скриптов", а также будут выведены в файл-лог. На рисунке пример записи в журнал значения переменной.
Для записи значения в текущий тег мы используем функцию server.WriteCurrentTag(). В качестве первого аргумента ей нужно передать значение тега, а вторым – его признак качества. Если наш запрос был выполнен некорректно, то мы запишем признак качества Bad ("Плохой"). Константу признака качества можно добавить из дерева функций – из раздела Константы – OPC.
Затем аналогичные операции мы проделываем с регистром 0x202.
Если оба запроса были выполнены корректно, то мы получим две переменных RegH и RegL, из которых можно получить 32-битное число, для последующего преобразования в вещественное число. Для преобразования этих регистров во float число, напишем специальную функцию ConvertToFloat, в которую передадим исходные регистры.
Чтобы получить вещественное число нам необходимо:
1)Собрать из двух чисел int16 одно число с 32 битами
2)Получить знак числа (s) – для этого нужно получить состояние 23 бита
3)Получить экспоненту (e) – биты с 24 по 31
4)Получить мантиссу (M) – биты с 0 по 22
Для того чтобы из сформированных элементов получить вещественное число, можно воспользоваться следующей формулой:
Рассмотрим код функции выполняющий данные действия.
function ConvertToFloat(RegH,RegL)
local val=bit.BitLshift(RegH,16);--побитовый сдвиг влево
val=bit.BitOr(val,RegL); --побитовое или - заполнение младших бит
local znak=bit.BitFromData(val,23); --получения знака числа
local exp=bit.BitRshift(val,24 ); --получение экспоненты сдвигом вправо
local mant=bit.BitAnd(val,8388607 ); --получение мантиссы
local F=2^(exp-127)*(1+mant/2^23);--получения искомого числа
if znak==true then F=F*(-1); end;
return F;
end;
В первой строчке кода, мы производим сдвиг старшего регистра на 16 битов влево, при помощи функции bit.BitLshift(). При этом младшие биты переменной val заполняются нулями. Теперь можно разместить в младших битах значения битов из младшего регистра RegL – это делается при помощи функции bit.BitOr() ("Побитовое ИЛИ").
В результате мы сформировали 32-битное значение, которое сохранили в переменную val. Теперь можно работать с отдельными его битами. Сначала получаем знак – для этого, при помощи функции bit.BitFromData(), получаем 23 бит числа и сохраняем его в переменную znak.
После этого получаем значение экспоненты. Экспонента находится в битах 24-31, поэтому, чтобы получить ее значение, достаточно просто выполнить сдвиг регистра на 24 бита вправо (старшие биты заполняться нулями) – для этого используем функциюbit.BitRshift(), и сохраняем результат в переменную exp.
Чтобы получить значение мантиссы, нам нужно получить значение из битов 0-22. Для этого достаточно просто заполнить нулями биты 23-31. Это можно сделать выполнив операцию "Побитовое И" между исходным числом и числом, у которого биты с 0 по 22 забиты единицами, а биты 23-31 – нулями. В десятичном видео это число 8388607. Операцию "побитовое И" можно выполнить используя функцию bit.BitAnd(). Результат сохраним в переменную mant.
Теперь мы получили все необходимые нам элементы формулы и можем найти значение вещественного числа. Результат сохраним в переменную F. После этого, если знак числа – отрицательный, то умножим наш результат на "минус единицу". Результат (число F) – вернем в вызываемый код.
После того как мы получили вещественное число, его можно записать в тег. Для этого мы будем использовать, уже знакомую нам функцию server.WriteCurrentTag(). Но теперь в качестве аргументов мы передадим ей вычисленное значение и хороший признак качества.
Код готов.