<< Click to Display Table of Contents >> Navigation: Modbus Universal MasterOPC Server > Руководство по языку Lua 5.1 > Примеры и полезности > Реализация протокола RNet > Создание скрипта |
Реализацию протокола начнем с написания функции подсчета контрольной суммы. В описании протокола, есть пример ее реализации на языках С и Pascal. Ниже приведен ее код переведенный на Lua:
Не будем разбирать код этой функции – он лишь повторяет указанный производителем алгоритм. Эту функцию мы расположим в устройстве – в этом случае мы сможем вызывать ее из всех тегов устройства. В качестве аргумента в функцию нужно передать кадр запроса, по которому нужно произвести подсчет контрольной суммы. Кадр передается в функцию как массив, упакованный в строку – это связано с особенностями реализации языка Lua, из-за которых передача таблиц во внешние функции невозможна. Ниже будет описано, как обходить данное ограничение.
Нам необходимо вызывать данную функцию перед посылкой запроса в прибор, считать контрольную сумму и добавлять полученный байт к запросу. При ответе, мы должны убрать последний байт из запроса, посчитать контрольную сумму на оставшийся пакет, а затем сравнить рассчитанный и полученный байт – если они совпадают можно продолжать обработку кадра.
Для упрощения решения данной задачи в функции server.SendAndReciveDataByMask есть дополнительные параметры 7 и 8 – в данных параметрах нужно указать строковые имена функций которые будут вызываться перед посылкой запроса (параметр 7) и после получения ответа (параметр 8). В качестве аргумента данные функции должны принимать таблицу байт. В коде можно сделать любую модификацию данной таблицы – убрать лишние байты (например, если в конце есть символ окончания кадра), добавить байт контрольной суммы и т.д. После выполнения обработки, функции должны возвращать первым параметром – целое число, которое указывает или количество байт в таблице, или, если число отрицательное - код ошибки. Данное значение будет передано переменной err (первый выходной аргумент функции SendAndReciveDataByMask). При этом не рекомендуется в качестве кода ошибки возвращать -1, так как это число зарезервировано за отсутствием ответа от прибора. Вторым параметром функции должна возвращаться измененная таблица байт. На эту таблицу байт будет наложена таблица масок посылки/запроса.
Начнем написание скрипта с того, с того что напишем данные функции – назовем их SendCrc и GetCrc. Функции будут находится в коде скрипта тега – включим у тега "Вход" выполнение скрипта, и напишем вне основных функций (например, в самом начале скрипта):
--функция вызываемая перед отправкой в прибор
function SendCrc(SendTable)
--вызываем функцию расчета контрольной суммы из устройства
Byte1=server.RunFunctionFromDevice("CRCsum",1,server.TableToString(SendTable));
table.insert(SendTable,Byte1);
--возвращаем количество байт и измененную таблицу
return table.maxn(SendTable),SendTable;
end
--функция вызываемая после приема ответа
function GetCrc(GetTable)
--вызываем функцию расчета контрольной суммы из устройства
GetByte1=table.remove(GetTable); --удаляем последний и сохраняем в GetByte
CalcByte1=server.RunFunctionFromDevice("CRCsum",1,server.TableToString(GetTable));
if GetByte1~=CalcByte1 then
return -10,GetTable; --возвращаем ошибку контрольной суммы
end
--возвращаем количество байт и измененную таблицу
return table.maxn(GetTable),GetTable;
end
Разберем код функции GetCrc. В первой строке мы убираем последний байт таблицы с помощью функции table.remove, при этом происходит сохранение удаляемого значения в переменную GetByte1.
Затем происходит вызов функции, находящейся в скрипте устройства – код функции был приведен ранее. Для вызова внешних функций используются специальные функции, в частности для вызова функции в устройстве - server.RunFunctionFromDevice(). В качестве аргументов в функцию нужно передать:
1)Имя функции – в данном случае "CRCsum"
2)Количество параметров, которое возвращает функция – в данном случае один (результат контрольной суммы)
3)Передаваемые параметры – в данном случае кадр (таблица) запроса.
Как мы говорили ранее, во внешние функции (то есть находящиеся в узле, устройстве или подустройстве) нельзя передавать таблицы. Это ограничение можно обойти если преобразовать таблицу в строку – для этого есть специальная функция server.TableToString(). Именно результат этой функции передается 3 параметром. В вызываемой внешней функции нужно будет сделать обратное преобразование – из строки в таблицу – при помощи функции server.StringToTable().
Затем мы сравниваем сохраненный байт GetByte1 и байтом высчитанным функцией CRCSum. Если байты не совпадают – возвращаем код ошибки -10 (данный код закреплен за ошибкой контрольной суммы) и таблицу. В противном случае с помощью функции table.maxn подсчитываем количество элементов в таблице и возвращаем это значение, вместе с самой таблицей. Обратите внимание, что таблица будет уже без байта контрольной суммы, поэтому указывать его в таблице маски приема не нужно (аналогично и функцией приема GetCrc).
Функция SendCrc работает аналогично – по байтам запроса считается контрольная сумма, и результат добавляется в таблицу с помощью функции table.insert. Затем функция возвращает количество байт в новой таблице и саму таблицу. Если по какой-то причине расчет контрольной суммы не возможен (например, не достаточно байт), то первым параметром можно вернуть -1 – в таком случае запрос к устройству выполнен не будет.
Функции обработки буфера приема и отправки готовы, можно переходить к коду скрипта – он будет располагаться в теле функции OnRead. Приведем код функции OnRead для выполнения запроса к прибору полностью, а потом разберем его построчно.
function OnRead()
local Addr=server.GetCurrentDeviceAddress();
local send = {Addr,0,1,0}; --кадр запроса чтения без контрольной суммы
local sendmask={"byte","byte","byte","byte"}; --маска запроса
local dest={}; --таблица принятых значений по маске
local destmask={"byte","byte","byte","byte","byte","int16:1:01"}; --маска ответа
--дополнительные переменные: ошибка, количество элементов в таблице dest, количество принятых байт
local err,len;
local n=0; --количество попыток запроса
--выполняем запросы в цикле
repeat
--запрос к устройству
err,dest,len=server.SendAndReceiveDataByMask(0,table.maxn(send), sendmask,send,destmask,8,"SendCrc","GetCrc");
if err==-1 then --нет ответа от устройства
break; --выходим из опроса
end
--проверим что количество ожидаемых элементов равно принятому
if err>0 and table.maxn(dest)==table.maxn(destmask) then
--проверяем что совпадает адрес прибора, номер канал и номер регистра
if dest[1]~=send[1] or dest[2]~=send[2] or dest[3]~=send[3] then
err=-2; --ошибка
end;
end;
n=n+1;
--выход из цикла - корректный прием или превышение количества попыток
until err>=0 or n>=3
if err<0 then
--данные не приняты или данные некорректы. Устанавливаем плохой признак качества
server.WriteCurrentTag(0,OPC_QUALITY_BAD);
else
--приняты корректные данные. Записываем в тег принятое значение
server.WriteCurrentTag(dest[4],OPC_QUALITY_GOOD);
end;
end
Вначале:
local Addr=server.GetCurrentDeviceAddress();
local send = {Addr,0,1,0}; --кадр запроса чтения без контрольной суммы
Мы создаем таблицу send (эти данные мы будем отправлять в порт) и инициализируем ее значениями. Первый элемент – адрес устройства; второй элемент – канал (нумерация идет с нуля); третий элемент – регистр (1 регистр – измеренное значение); четвертый элемент – тип запроса (0 – чтение).
local sendmask={"byte","byte","byte","byte","byte"}; --маска запроса
local dest={}; --таблица принятых значений по маске
local destmask={"byte","byte","byte","byte","byte","int16:1:01","byte"}; --маска ответа
В данных строках происходит инициализация таблиц – масок запроса и ответа.
err,dest,len=server.SendAndReceiveDataByMask (0,table.maxn(send), sendmask,send,destmask,8,"SendCrc","GetCrc");
Затем происходит непосредственное выполнение запроса к устройству с помощью функции sendAndReciveDataByMask, 6 и 7 аргументом указываются строковые(!) имена функций SendCrc и GetCrc. Как и в предыдущем примере, запрос упакован в цикл. Также обратите внимание на параметр 6 - константу 8, это количество ожидаемых байт, т.е. в данном случае ответ ожидается длиной в 8 байт (7 байт данных и 1 байт контрольной суммы). Очень важно правильно задать значение этого параметра - если указать большее чем на самом деле количество байт, то функция вернет результат по истечению таймаута (настройки "Время ожидания ответа"), поэтому если тегов много, то опрос будет медленным. Если указать правильное количество байт, то сразу после их приема происходит выход из функции и начинается обработка принятых байт.
--проверим что количество ожидаемых элементов равно принятому
if err>0 and table.maxn(dest)==table.maxn(destmask) then
--проверяем что совпадает адрес прибора, номер канал и номер регистра
if dest[1]~=send[1] or dest[2]~=send[2] or dest[3]~=send[3] then
err=-2; --ошибка
end;
end;
После получения ответа сначала нужно проверить переменную err на число -1, за которым закреплено отсутствие ответа от прибора. При этом лучше сразу выйти из цикла с помощью функции break, и не делать повторы запросов – дело в том, что если от прибора не пришло ответа, то ОРС сервер сам делает нужное количество попыток опроса.
Если же данные получены нужно убедиться, что нет ошибок контрольной суммы – число err больше нуля, а также сравнить что количество принятых элементов в таблицу dest равно ожидаемому количеству элементов по таблице destmask.
После этого рекомендуется проверить, что первый элемент таблицы равен адресу, второй номеру каналу, третий – типу запроса. Для этого можно сравнить эти элементы со значениями таблицы запроса.
После этого полученные данные, а именно - четвертый элемент из таблицы dest можно записать в тег. Если значение не корректно, то в тег записывается 0 и плохой признак качества.
Теперь созданный тег можно тиражировать - сделать копии и изменить адреса регистров, для опроса уставок и состояния выходов.
Прибор передает измеренное значение как целое число, положение десятичной точки устанавливается в приборе при настройке на конкретный тип датчика и должно быть учтено при записи значения в тег. Таким образом, перед тем как записать измеренное значение в тег, нужно сместить положение десятичной точки на заданное пользователем значение – то есть поделить измеренное значение на 10 в заданной степени.
Данную настройку лучше вынести из кода скрипта – для облегчения создания конфигурации пользователю. Для этого в MasterOPC есть возможность добавить к устройству или подустройству дополнительные свойства. Рассмотрим создание дополнительного свойства "Положение десятичной точки". Скомпилируем скрипт и закроем редактор скрипта.
Вызовем контекстное меню у подустройства и выберем пункт Дополнительные свойства.
В окне дополнительных свойств добавим новое свойство и назовем его "Pos", в поле Описание напишем "Положение десятичной точки", тип свойства – целое число. Нажмем ОК.
Сделаем, чтобы максимальное вводимое значение было равно четырем - это максимальное значение точки, которое может ввести пользователь. Сохраним свойство.
Теперь в свойствах подустройства появилось дополнительное свойство, которое может изменять пользователь.
Теперь введенное пользователем значение мы можем прочитать из скрипта. Снова откроем редактор скрипта тега "Вход". В той части кода, где осуществляется запись в тег, произведем деление.
Чтобы получить введенное у подустройства пользователем значение дополнительного свойства, воспользуемся функцией server.ReadSubDeviceExtProperty(). В качестве аргумента ей нужно передать имя нужного свойства. Код будет выглядеть следующим образом:
if err<0 then
--данные не приняты или данные некорректы. Устанавливаем плохой признак качества
server.WriteCurrentTag(0,OPC_QUALITY_BAD);
else
--приняты корректные данные получаем значение положения десятичной точки
local Pos=server.ReadSubDeviceExtProperty("Pos");
--Записываем в тег принятое значение учитывая смещение
server.WriteCurrentTag(dest[4]/10^Pos,OPC_QUALITY_GOOD);
end;
Аналогичным образом можно сделать свойство Номер канала у каждого подустройства – что облегчит их копирование для создания конфигураций многоканальных приборов. Дополнительные свойства также можно создавать у устройств.
Примечание. Конфигурация OPC сервера с полным кодом данного примера находится по ссылке и называется Контравт.mbp