Пользовательские протоколы — различия между версиями
(→readRegister) |
|||
(не показано 11 промежуточных версий 2 участников) | |||
Строка 1: | Строка 1: | ||
+ | <languages/> | ||
+ | <translate> | ||
+ | <!--T:1--> | ||
Существует большое количество различных устройств автоматизации с нестандартными протоколами обмена данными. Для решения проблемы сбора данных с таких устройств в WebHMI есть возможность создавать пользовательские протоколы на языке Lua [https://ru.wikipedia.org/wiki/Lua]. Эта функция доступна в WebHMI начиная с версии 1.10.0.3420. | Существует большое количество различных устройств автоматизации с нестандартными протоколами обмена данными. Для решения проблемы сбора данных с таких устройств в WebHMI есть возможность создавать пользовательские протоколы на языке Lua [https://ru.wikipedia.org/wiki/Lua]. Эта функция доступна в WebHMI начиная с версии 1.10.0.3420. | ||
− | == О языке Lua == | + | == О языке Lua == <!--T:2--> |
Lua является типичным процедурным языком программирования. Он предоставляет широкие возможности для объектно-ориентированной и функциональной разработки. Lua создавался как мощный и простой язык, обладающий всеми необходимыми выразительными средствами. С документацией по языку можно ознакомится на официальном сайте [http://www.lua.org/manual/5.1/]. Бегло ознакомится с синтаксисом можно на удобном сайте [http://tylerneylon.com/a/learn-lua/]. В WebHMI используется Lua версии 5.1.5. | Lua является типичным процедурным языком программирования. Он предоставляет широкие возможности для объектно-ориентированной и функциональной разработки. Lua создавался как мощный и простой язык, обладающий всеми необходимыми выразительными средствами. С документацией по языку можно ознакомится на официальном сайте [http://www.lua.org/manual/5.1/]. Бегло ознакомится с синтаксисом можно на удобном сайте [http://tylerneylon.com/a/learn-lua/]. В WebHMI используется Lua версии 5.1.5. | ||
+ | <!--T:3--> | ||
Почему именно Lua? | Почему именно Lua? | ||
+ | <!--T:4--> | ||
Lua — язык, который специально создавался для встраивания в приложения, написанные на языке Си. Он обладает отличной производительностью, потребляет очень мало ресурсов и имеет богатые возможности. | Lua — язык, который специально создавался для встраивания в приложения, написанные на языке Си. Он обладает отличной производительностью, потребляет очень мало ресурсов и имеет богатые возможности. | ||
− | == Общая концепция пользовательских протоколов == | + | == Общая концепция пользовательских протоколов == <!--T:5--> |
В WebHMI минимальной единицей информации является регистр. В общем случае обмен данными со всеми устройствами происходит циклически – регистры, которые должны быть опрошены в данном скане, читаются один за одним. Запись в регистры также происходит по одному регистру за раз. | В WebHMI минимальной единицей информации является регистр. В общем случае обмен данными со всеми устройствами происходит циклически – регистры, которые должны быть опрошены в данном скане, читаются один за одним. Запись в регистры также происходит по одному регистру за раз. | ||
+ | <!--T:6--> | ||
WebHMI позволяет создать свой протокол и определить функции чтения и записи регистра. Эти функции должны сформировать запрос, отправить его устройству, принять от него ответ, разобрать его и, в зависимости от результата, вернуть необходимые данные. | WebHMI позволяет создать свой протокол и определить функции чтения и записи регистра. Эти функции должны сформировать запрос, отправить его устройству, принять от него ответ, разобрать его и, в зависимости от результата, вернуть необходимые данные. | ||
− | == Создание протокола == | + | == Создание протокола == <!--T:7--> |
Для перехода к списку пользовательских протоколов нажмите на кнопку "'''Custom protocols'''" на страние '''Setup->Registers'''.<br/> | Для перехода к списку пользовательских протоколов нажмите на кнопку "'''Custom protocols'''" на страние '''Setup->Registers'''.<br/> | ||
[[Файл:Custom-protocol-button.png|600px]]<br/> | [[Файл:Custom-protocol-button.png|600px]]<br/> | ||
+ | <!--T:8--> | ||
Вы попадете на страницу управления протоколами. В данном примере мы видим два демонстрационных протокола – ModBus TCP Demo и ModBus ASCII Demo:<br/> | Вы попадете на страницу управления протоколами. В данном примере мы видим два демонстрационных протокола – ModBus TCP Demo и ModBus ASCII Demo:<br/> | ||
[[Файл:Custom-protocol-list.png|600px]]<br/> | [[Файл:Custom-protocol-list.png|600px]]<br/> | ||
+ | <!--T:9--> | ||
Давайте посмотрим на страницу редактирования протокола ModBus TCP Demo:<br/> | Давайте посмотрим на страницу редактирования протокола ModBus TCP Demo:<br/> | ||
[[Файл:Custom-protocol-demo.png|600px]]<br/> | [[Файл:Custom-protocol-demo.png|600px]]<br/> | ||
+ | <!--T:10--> | ||
Протоколу можно задать: | Протоколу можно задать: | ||
+ | <!--T:11--> | ||
* название, описание | * название, описание | ||
* тип (TCP/IP или Serial) | * тип (TCP/IP или Serial) | ||
Строка 32: | Строка 42: | ||
* код с программой для обработки протокола | * код с программой для обработки протокола | ||
+ | <!--T:12--> | ||
Регулярное выражение должно обеспечить проверку правильности адреса регистра на странице редактирования регистров (при выборе этого протокола). Пример:<br/> | Регулярное выражение должно обеспечить проверку правильности адреса регистра на странице редактирования регистров (при выборе этого протокола). Пример:<br/> | ||
[[Файл:Custom-protocol-validate.png|600px]]<br/> | [[Файл:Custom-protocol-validate.png|600px]]<br/> | ||
+ | <!--T:13--> | ||
Также мы видим удобный редактор кода. В нем поддерживается форматирование, подсветка и валидация синтаксиса. Так что писать код удобно.<br/> | Также мы видим удобный редактор кода. В нем поддерживается форматирование, подсветка и валидация синтаксиса. Так что писать код удобно.<br/> | ||
Если в коде допущена синтаксическая ошибка то в соответствующей строке появится красный крестик. Что бы увидеть детальное сообщение об ошибке достаточно навести на него курсор мышки:<br/> | Если в коде допущена синтаксическая ошибка то в соответствующей строке появится красный крестик. Что бы увидеть детальное сообщение об ошибке достаточно навести на него курсор мышки:<br/> | ||
[[Файл:Custom-protocol-error.png|600px]]<br/> | [[Файл:Custom-protocol-error.png|600px]]<br/> | ||
− | + | <!--T:14--> | |
− | + | ||
После создания протокола он появится в выпадающем списке доступных '''PLC models''' на странице создания новых '''Connections''' и с ним можно будет работать так же как и с обычным встроенным протоколом. <br/> | После создания протокола он появится в выпадающем списке доступных '''PLC models''' на странице создания новых '''Connections''' и с ним можно будет работать так же как и с обычным встроенным протоколом. <br/> | ||
[[Файл:Custom-protocol-select.png|600px]]<br/> | [[Файл:Custom-protocol-select.png|600px]]<br/> | ||
− | == Необходимые функции == | + | == Необходимые функции == <!--T:15--> |
WebHMI ожидает увидеть во введенном коде три функции: | WebHMI ожидает увидеть во введенном коде три функции: | ||
* createDevices | * createDevices | ||
Строка 50: | Строка 61: | ||
* writeRegister | * writeRegister | ||
− | == createDevices == | + | == createDevices == <!--T:16--> |
Процедура '''createDevices''' вызывается один раз при старте WebHMI и создает именованные префиксы для адресов регистров. Что бы лучше понять это давайте рассмотрим пример для устройств ModBUS. Создадим типы регистров для Coils, Discrete Inputs, Holding Registers, Input Registers: | Процедура '''createDevices''' вызывается один раз при старте WebHMI и создает именованные префиксы для адресов регистров. Что бы лучше понять это давайте рассмотрим пример для устройств ModBUS. Создадим типы регистров для Coils, Discrete Inputs, Holding Registers, Input Registers: | ||
+ | <!--T:17--> | ||
<syntaxhighlight lang="lua"> | <syntaxhighlight lang="lua"> | ||
function createDevices () | function createDevices () | ||
Строка 62: | Строка 74: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
+ | <!--T:18--> | ||
Здесь создается четыре типа регистров. Для такого протокола в строке адреса регистра можно будет указывать регистры вида C14, DI4, HR34355, IR145. | Здесь создается четыре типа регистров. Для такого протокола в строке адреса регистра можно будет указывать регистры вида C14, DI4, HR34355, IR145. | ||
Процедура '''addDevice''' вызывается для каждого такого типа адресов. Ей передается таблица с такими параметрами: | Процедура '''addDevice''' вызывается для каждого такого типа адресов. Ей передается таблица с такими параметрами: | ||
+ | <!--T:19--> | ||
* name – строка-перфикс, именно эта часть в адресе будет определять дальнейшую обработку чтения/записи этого регистра | * name – строка-перфикс, именно эта часть в адресе будет определять дальнейшую обработку чтения/записи этого регистра | ||
* shift – константа, будет прибавлена к значению адреса регистра. Т.е. можно сделать что бы регистр с адресом D30 преобразовался в D1030, а D33 – в D1033 и т.п. | * shift – константа, будет прибавлена к значению адреса регистра. Т.е. можно сделать что бы регистр с адресом D30 преобразовался в D1030, а D33 – в D1033 и т.п. | ||
Строка 70: | Строка 84: | ||
* xtraFields – набор дополнительных параметров (максимум 5 штук), будет передан в функции read/write. | * xtraFields – набор дополнительных параметров (максимум 5 штук), будет передан в функции read/write. | ||
+ | == onScanStart == <!--T:20--> | ||
+ | Процедура '''onScanStart''' вызывается каждый раз при начале нового скана. Она может быть полезна для протоколов, где за один запрос читается массив значений для нескольких регистров. Для таких протоколов можно кешировать результат запроса и возвращать значения из кеша. Сброс кеша можно производить в процедуре onScanStart. | ||
+ | |||
+ | <!--T:21--> | ||
+ | onScanStart доступна в прошивках начиная с версии 2.1.3923. | ||
− | == readRegister == | + | == readRegister == <!--T:22--> |
Функция '''readRegister''' должна произвести чтение указанного регистра. | Функция '''readRegister''' должна произвести чтение указанного регистра. | ||
+ | <!--T:23--> | ||
В случае успешного чтения функция '''readRegister''' должна вернуть массив байт, длина которого соответствует указанному типу данных (1, 2 или 4) или же число. В случае неудачи необходимо вернуть false. | В случае успешного чтения функция '''readRegister''' должна вернуть массив байт, длина которого соответствует указанному типу данных (1, 2 или 4) или же число. В случае неудачи необходимо вернуть false. | ||
+ | <!--T:24--> | ||
Ей в качестве аргументов передаются три параметра: | Ей в качестве аргументов передаются три параметра: | ||
* reg - таблица (структура) с параметрами регистра | * reg - таблица (структура) с параметрами регистра | ||
Строка 81: | Строка 102: | ||
* unitId – ID устройства на шине или прочий ID. Примером может быть Slave ID в ModBus RTU или Unit ID в ModBus TCP. | * unitId – ID устройства на шине или прочий ID. Примером может быть Slave ID в ModBus RTU или Unit ID в ModBus TCP. | ||
+ | <!--T:25--> | ||
В структуре '''reg''' есть такие атрибуты: | В структуре '''reg''' есть такие атрибуты: | ||
* internalAddr - пересчитанный адрес регистра. Это число, пересчитанное из указанной системы счисления с прибавлением к нему shift. | * internalAddr - пересчитанный адрес регистра. Это число, пересчитанное из указанной системы счисления с прибавлением к нему shift. | ||
Строка 86: | Строка 108: | ||
* dataType – тип данных, который пользователь указал для регистра. 0 = Bit, 1 = Byte, 2 = Word, 3 = Double Word, 4 = UnixTime | * dataType – тип данных, который пользователь указал для регистра. 0 = Bit, 1 = Byte, 2 = Word, 3 = Double Word, 4 = UnixTime | ||
+ | <!--T:26--> | ||
В структуре '''device''' есть такие атрибуты: | В структуре '''device''' есть такие атрибуты: | ||
* shift - значение shift из соответствующей строки createDevices | * shift - значение shift из соответствующей строки createDevices | ||
Строка 91: | Строка 114: | ||
* xtraFields – значение xtraFields из соответствующей строки createDevices | * xtraFields – значение xtraFields из соответствующей строки createDevices | ||
+ | <!--T:27--> | ||
Эти параметры передаются для того, что бы можно было правильно и полноценно составить запрос согласно протокола. | Эти параметры передаются для того, что бы можно было правильно и полноценно составить запрос согласно протокола. | ||
+ | <!--T:28--> | ||
Для передачи запроса устройству используются функции '''sendBytes''' и '''sendString'''. Для чтения ответа - '''readBytes''', '''readString'''. | Для передачи запроса устройству используются функции '''sendBytes''' и '''sendString'''. Для чтения ответа - '''readBytes''', '''readString'''. | ||
− | На вход '''sendBytes''' принимает таблицу (массив) байт. Результатом будет true в случае успеха и false в случае ошибки. | + | <!--T:29--> |
− | На вход '''sendString''' принимает строку. Результатом будет true в случае успеха и false в случае ошибки. | + | На вход '''sendBytes''' принимает таблицу (массив) байт. Результатом будет true в случае успеха и false в случае ошибки. <br> |
− | На вход '''readBytes''' принимает количество байт, которое необходимо прочитать. Результатом будет таблица (массив) байт в случае успеха и false в случае ошибки. | + | На вход '''sendString''' принимает строку. Результатом будет true в случае успеха и false в случае ошибки. <br> |
− | На вход '''readString''' принимает количество байт, которое необходимо прочитать. Результатом будет строка в случае успеха и false в случае ошибки. | + | На вход '''readBytes''' принимает количество байт, которое необходимо прочитать. Результатом будет таблица (массив) байт в случае успеха и false в случае ошибки.<br> |
+ | На вход '''readString''' принимает количество байт, которое необходимо прочитать. Результатом будет строка в случае успеха и false в случае ошибки.<br> | ||
+ | <!--T:30--> | ||
Если необходимо закрыть соединение (например, в случае множественных ошибок), то можно вызвать процедуру '''closeConnection'''. | Если необходимо закрыть соединение (например, в случае множественных ошибок), то можно вызвать процедуру '''closeConnection'''. | ||
+ | <!--T:31--> | ||
Если необходимо сделать паузу, то можно вызвать функцию '''sleep'''. Ее единственным аргументом должно быть время в микросекундах. Пример: sleep(20000); - произойдет пауза 20 миллисекунд. | Если необходимо сделать паузу, то можно вызвать функцию '''sleep'''. Ее единственным аргументом должно быть время в микросекундах. Пример: sleep(20000); - произойдет пауза 20 миллисекунд. | ||
+ | <!--T:32--> | ||
Для работы с битами можно использовать библиотеку bitop [http://bitop.luajit.org/]. | Для работы с битами можно использовать библиотеку bitop [http://bitop.luajit.org/]. | ||
+ | <!--T:33--> | ||
Для отладки и вывода диагностических сообщений можно использовать процедуры ERROR, INFO, DEBUG и TRACE которые доступны и в обычных сценариях Lua [http://wiki.webhmi.com.ua/index.php/Сценарии_LUA]. | Для отладки и вывода диагностических сообщений можно использовать процедуры ERROR, INFO, DEBUG и TRACE которые доступны и в обычных сценариях Lua [http://wiki.webhmi.com.ua/index.php/Сценарии_LUA]. | ||
+ | <!--T:34--> | ||
Что бы лучше понять как это все работает давайте рассмотрим пример функции readRegister для протокола ModBus TCP: | Что бы лучше понять как это все работает давайте рассмотрим пример функции readRegister для протокола ModBus TCP: | ||
+ | <!--T:35--> | ||
<syntaxhighlight lang="lua"> | <syntaxhighlight lang="lua"> | ||
local transId = 0; | local transId = 0; | ||
local errorCount = 0; | local errorCount = 0; | ||
+ | <!--T:36--> | ||
function readRegister (reg, device, unitId) | function readRegister (reg, device, unitId) | ||
− | local request = {}; | + | <!--T:37--> |
+ | local request = {}; | ||
-- transaction ID | -- transaction ID | ||
Строка 124: | Строка 158: | ||
request[2] = bit.band(transId, 255); | request[2] = bit.band(transId, 255); | ||
− | -- protocol ID | + | <!--T:38--> |
+ | -- protocol ID | ||
request[3] = 0; | request[3] = 0; | ||
request[4] = 0; | request[4] = 0; | ||
Строка 142: | Строка 177: | ||
request[10] = bit.band(reg.internalAddr, 255); | request[10] = bit.band(reg.internalAddr, 255); | ||
− | -- count of registers | + | <!--T:39--> |
+ | -- count of registers | ||
request[11] = 0; | request[11] = 0; | ||
request[12] = 1; | request[12] = 1; | ||
Строка 157: | Строка 193: | ||
end | end | ||
− | local response = {}; | + | <!--T:40--> |
+ | local response = {}; | ||
-- read MBAP Header | -- read MBAP Header | ||
response = readBytes(7); | response = readBytes(7); | ||
Строка 171: | Строка 208: | ||
res = #response; | res = #response; | ||
− | if (res ~= 7) then | + | <!--T:41--> |
+ | if (res ~= 7) then | ||
errorCount = errorCount + 1; | errorCount = errorCount + 1; | ||
if (errorCount > 3) then | if (errorCount > 3) then | ||
Строка 181: | Строка 219: | ||
end | end | ||
− | if (response[1] ~= request[1] or response[2] ~= request[2]) then | + | <!--T:42--> |
+ | if (response[1] ~= request[1] or response[2] ~= request[2]) then | ||
ERROR("Wrong transaction ID. Got #" .. (response[1] * 256 + response[2]) .. " but expected #" .. (request[1] * 256 + request[2])); | ERROR("Wrong transaction ID. Got #" .. (response[1] * 256 + response[2]) .. " but expected #" .. (request[1] * 256 + request[2])); | ||
return false; | return false; | ||
Строка 191: | Строка 230: | ||
end | end | ||
− | if (response[7] ~= request[7]) then | + | <!--T:43--> |
+ | if (response[7] ~= request[7]) then | ||
ERROR("Wrong UnitID in response"); | ERROR("Wrong UnitID in response"); | ||
return false; | return false; | ||
end | end | ||
− | local length = response[5] * 256 + response[6]; | + | <!--T:44--> |
+ | local length = response[5] * 256 + response[6]; | ||
− | if (length < 1) then | + | <!--T:45--> |
+ | if (length < 1) then | ||
ERROR("Wrong length in response"); | ERROR("Wrong length in response"); | ||
return false; | return false; | ||
Строка 206: | Строка 248: | ||
-- read MBAP Header | -- read MBAP Header | ||
− | responsePDU = readBytes(length - 1); | + | <!--T:46--> |
+ | responsePDU = readBytes(length - 1); | ||
if (responsePDU == false) then | if (responsePDU == false) then | ||
errorCount = errorCount + 1; | errorCount = errorCount + 1; | ||
Строка 218: | Строка 261: | ||
res = #responsePDU; | res = #responsePDU; | ||
− | if (responsePDU[1] ~= request[8]) then | + | <!--T:47--> |
+ | if (responsePDU[1] ~= request[8]) then | ||
ERROR("Wrong function in response"); | ERROR("Wrong function in response"); | ||
return false; | return false; | ||
end | end | ||
− | local dataLength = responsePDU[2]; | + | <!--T:48--> |
+ | local dataLength = responsePDU[2]; | ||
if (dataLength ~= length - 3) then | if (dataLength ~= length - 3) then | ||
ERROR("Wrong length in PDU"); | ERROR("Wrong length in PDU"); | ||
Строка 231: | Строка 276: | ||
local result = {}; | local result = {}; | ||
− | if (dataLength >= 1) then | + | <!--T:49--> |
+ | if (dataLength >= 1) then | ||
for i = 1, dataLength do | for i = 1, dataLength do | ||
result[i] = responsePDU[2 + i]; | result[i] = responsePDU[2 + i]; | ||
Строка 237: | Строка 283: | ||
end | end | ||
− | return result; | + | <!--T:50--> |
+ | return result; | ||
end | end | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | == writeRegister == | + | == writeRegister == <!--T:51--> |
Функция '''writeRegister''' должна произвести запись нового значения в указанный регистр. При успешной записи она должна вернуть true. В случае ошибки - false. | Функция '''writeRegister''' должна произвести запись нового значения в указанный регистр. При успешной записи она должна вернуть true. В случае ошибки - false. | ||
+ | <!--T:52--> | ||
Ей передаются все те же параметры, что и для readRegister, а также дополнительно четвертый параметр - новое значение. | Ей передаются все те же параметры, что и для readRegister, а также дополнительно четвертый параметр - новое значение. | ||
+ | <!--T:53--> | ||
Функция '''writeRegister''' может использовать те же способы записи и чтения массивов байт в порт. | Функция '''writeRegister''' может использовать те же способы записи и чтения массивов байт в порт. | ||
+ | <!--T:54--> | ||
Давайте рассмотрим пример этой функции для протокола ModBus TCP: | Давайте рассмотрим пример этой функции для протокола ModBus TCP: | ||
+ | <!--T:55--> | ||
<syntaxhighlight lang="lua"> | <syntaxhighlight lang="lua"> | ||
function writeRegister (reg, device, unitId, newValue) | function writeRegister (reg, device, unitId, newValue) | ||
Строка 263: | Строка 314: | ||
request[4] = 0; | request[4] = 0; | ||
− | if (reg.dataType == 3) then -- double word | + | <!--T:56--> |
+ | if (reg.dataType == 3) then -- double word | ||
-- message length | -- message length | ||
request[5] = 0; | request[5] = 0; | ||
Строка 278: | Строка 330: | ||
request[10] = bit.band(reg.internalAddr, 255); | request[10] = bit.band(reg.internalAddr, 255); | ||
− | -- count of registers | + | <!--T:57--> |
+ | -- count of registers | ||
request[11] = 0; | request[11] = 0; | ||
request[12] = 2; | request[12] = 2; | ||
Строка 285: | Строка 338: | ||
request[13] = 4; | request[13] = 4; | ||
− | -- value of registers | + | <!--T:58--> |
+ | -- value of registers | ||
request[14] = bit.band(bit.rshift(newValue, 24), 255); | request[14] = bit.band(bit.rshift(newValue, 24), 255); | ||
request[15] = bit.band(bit.rshift(newValue, 16), 255); | request[15] = bit.band(bit.rshift(newValue, 16), 255); | ||
Строка 293: | Строка 347: | ||
local res = sendBytes(request); | local res = sendBytes(request); | ||
− | if (res | + | if (res == false) then |
DEBUG("Can't send bytes"); | DEBUG("Can't send bytes"); | ||
return 0; | return 0; | ||
end | end | ||
− | local response = {}; | + | <!--T:59--> |
+ | local response = {}; | ||
− | response = readBytes(7); | + | <!--T:60--> |
+ | response = readBytes(7); | ||
+ | if (response == false) then | ||
+ | DEBUG("Can't read response"); | ||
+ | return false; | ||
+ | end | ||
res = #response; | res = #response; | ||
− | if (res ~= 7) then | + | <!--T:61--> |
+ | if (res ~= 7) then | ||
DEBUG("Wrong response length"); | DEBUG("Wrong response length"); | ||
return false; | return false; | ||
Строка 325: | Строка 386: | ||
local length = response[5] * 256 + response[6]; | local length = response[5] * 256 + response[6]; | ||
− | if (length < 1) then | + | <!--T:62--> |
+ | if (length < 1) then | ||
ERROR("Wrong length in response"); | ERROR("Wrong length in response"); | ||
return false; | return false; | ||
Строка 332: | Строка 394: | ||
local responsePDU = {}; | local responsePDU = {}; | ||
− | responsePDU = readBytes(length - 1); | + | <!--T:63--> |
− | res = #responsePDU; | + | responsePDU = readBytes(length - 1); |
+ | if (responsePDU == false) then | ||
+ | DEBUG("Can't read response PDU"); | ||
+ | return false; | ||
+ | end | ||
+ | |||
+ | <!--T:64--> | ||
+ | res = #responsePDU; | ||
if (responsePDU[1] ~= request[8]) then | if (responsePDU[1] ~= request[8]) then | ||
Строка 345: | Строка 414: | ||
end | end | ||
− | if (responsePDU[4] ~= 0 or responsePDU[5] ~= 2) then | + | <!--T:65--> |
+ | if (responsePDU[4] ~= 0 or responsePDU[5] ~= 2) then | ||
ERROR("Wrong register count in response"); | ERROR("Wrong register count in response"); | ||
return false; | return false; | ||
Строка 366: | Строка 436: | ||
request[10] = bit.band(reg.internalAddr, 255); | request[10] = bit.band(reg.internalAddr, 255); | ||
− | local val = newValue; | + | <!--T:66--> |
+ | local val = newValue; | ||
if (reg.dataType == 0) then | if (reg.dataType == 0) then | ||
if (val > 0) then | if (val > 0) then | ||
Строка 375: | Строка 446: | ||
end | end | ||
− | -- value of registers | + | <!--T:67--> |
+ | -- value of registers | ||
request[11] = bit.band(bit.rshift(val, 8), 255); | request[11] = bit.band(bit.rshift(val, 8), 255); | ||
request[12] = bit.band(val, 255); | request[12] = bit.band(val, 255); | ||
− | local res = sendBytes(request); | + | <!--T:68--> |
+ | local res = sendBytes(request); | ||
− | if (res | + | if (res == false) then |
DEBUG("Can't send bytes"); | DEBUG("Can't send bytes"); | ||
return 0; | return 0; | ||
Строка 391: | Строка 464: | ||
response = readBytes(requestLen); | response = readBytes(requestLen); | ||
− | res = #response; | + | if (response == false) then |
+ | DEBUG("Can't read response"); | ||
+ | return false; | ||
+ | end | ||
+ | |||
+ | <!--T:69--> | ||
+ | res = #response; | ||
− | if (res ~= requestLen) then | + | <!--T:70--> |
+ | if (res ~= requestLen) then | ||
DEBUG("Wrong response length"); | DEBUG("Wrong response length"); | ||
return false; | return false; | ||
Строка 405: | Строка 485: | ||
end | end | ||
− | end | + | <!--T:71--> |
+ | end | ||
− | return true; | + | <!--T:72--> |
+ | return true; | ||
end | end | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | + | == Примеры протоколов == <!--T:73--> | |
− | == Примеры протоколов == | + | В качестве примера мы реализовали несколько протоколов: |
− | В качестве примера мы реализовали | + | *[http://wiki.webhmi.com.ua/index.php/Пример_протокола_ModBus_TCP ModBus TCP] |
+ | *[http://wiki.webhmi.com.ua/index.php/Пример_протокола_ModBus_ASCII ModBus ASCII]. | ||
+ | *[http://wiki.webhmi.com.ua/index.php/Modbus_RTU_%D0%B2_%D0%B2%D0%B8%D0%B4%D0%B5_custom_protocol Пример реализации протокола ModbusRTU]. | ||
+ | На странице подключения к [http://wiki.webhmi.com.ua/index.php/%D0%A2%D0%B5%D0%BD%D0%B7%D0%BE%D0%B4200 тензометрическому контроллеру Тензод200] также есть пример пользовательского протокола. | ||
+ | </translate> |
Текущая версия на 14:47, 30 мая 2018
Существует большое количество различных устройств автоматизации с нестандартными протоколами обмена данными. Для решения проблемы сбора данных с таких устройств в WebHMI есть возможность создавать пользовательские протоколы на языке Lua [1]. Эта функция доступна в WebHMI начиная с версии 1.10.0.3420.
Содержание
О языке Lua
Lua является типичным процедурным языком программирования. Он предоставляет широкие возможности для объектно-ориентированной и функциональной разработки. Lua создавался как мощный и простой язык, обладающий всеми необходимыми выразительными средствами. С документацией по языку можно ознакомится на официальном сайте [2]. Бегло ознакомится с синтаксисом можно на удобном сайте [3]. В WebHMI используется Lua версии 5.1.5.
Почему именно Lua?
Lua — язык, который специально создавался для встраивания в приложения, написанные на языке Си. Он обладает отличной производительностью, потребляет очень мало ресурсов и имеет богатые возможности.
Общая концепция пользовательских протоколов
В WebHMI минимальной единицей информации является регистр. В общем случае обмен данными со всеми устройствами происходит циклически – регистры, которые должны быть опрошены в данном скане, читаются один за одним. Запись в регистры также происходит по одному регистру за раз.
WebHMI позволяет создать свой протокол и определить функции чтения и записи регистра. Эти функции должны сформировать запрос, отправить его устройству, принять от него ответ, разобрать его и, в зависимости от результата, вернуть необходимые данные.
Создание протокола
Для перехода к списку пользовательских протоколов нажмите на кнопку "Custom protocols" на страние Setup->Registers.
Вы попадете на страницу управления протоколами. В данном примере мы видим два демонстрационных протокола – ModBus TCP Demo и ModBus ASCII Demo:
Давайте посмотрим на страницу редактирования протокола ModBus TCP Demo:
Протоколу можно задать:
- название, описание
- тип (TCP/IP или Serial)
- сетевой порт по умолчанию (только для TCP)
- регулярное выражение для проверки допустимости вводимых адресов
- сообщение об ошибке, которе будет выводится при вводе неверного адреса регистра
- код с программой для обработки протокола
Регулярное выражение должно обеспечить проверку правильности адреса регистра на странице редактирования регистров (при выборе этого протокола). Пример:
Также мы видим удобный редактор кода. В нем поддерживается форматирование, подсветка и валидация синтаксиса. Так что писать код удобно.
Если в коде допущена синтаксическая ошибка то в соответствующей строке появится красный крестик. Что бы увидеть детальное сообщение об ошибке достаточно навести на него курсор мышки:
После создания протокола он появится в выпадающем списке доступных PLC models на странице создания новых Connections и с ним можно будет работать так же как и с обычным встроенным протоколом.
Необходимые функции
WebHMI ожидает увидеть во введенном коде три функции:
- createDevices
- readRegister
- writeRegister
createDevices
Процедура createDevices вызывается один раз при старте WebHMI и создает именованные префиксы для адресов регистров. Что бы лучше понять это давайте рассмотрим пример для устройств ModBUS. Создадим типы регистров для Coils, Discrete Inputs, Holding Registers, Input Registers:
function createDevices ()
addDevice({name = "C", shift = 0, base = 10, xtraFields = {1, 5}});
addDevice({name = "DI", shift = 0, base = 10, xtraFields = {2, 0}});
addDevice({name = "HR", shift = 0, base = 10, xtraFields = {3, 6, 16}});
addDevice({name = "IR", shift = 0, base = 10, xtraFields = {4, 0}});
end
Здесь создается четыре типа регистров. Для такого протокола в строке адреса регистра можно будет указывать регистры вида C14, DI4, HR34355, IR145. Процедура addDevice вызывается для каждого такого типа адресов. Ей передается таблица с такими параметрами:
- name – строка-перфикс, именно эта часть в адресе будет определять дальнейшую обработку чтения/записи этого регистра
- shift – константа, будет прибавлена к значению адреса регистра. Т.е. можно сделать что бы регистр с адресом D30 преобразовался в D1030, а D33 – в D1033 и т.п.
- base – система счисления адреса. У некоторых устройств используются адреса в восьмиричной или шестнадцатиричных системах счислений
- xtraFields – набор дополнительных параметров (максимум 5 штук), будет передан в функции read/write.
onScanStart
Процедура onScanStart вызывается каждый раз при начале нового скана. Она может быть полезна для протоколов, где за один запрос читается массив значений для нескольких регистров. Для таких протоколов можно кешировать результат запроса и возвращать значения из кеша. Сброс кеша можно производить в процедуре onScanStart.
onScanStart доступна в прошивках начиная с версии 2.1.3923.
readRegister
Функция readRegister должна произвести чтение указанного регистра.
В случае успешного чтения функция readRegister должна вернуть массив байт, длина которого соответствует указанному типу данных (1, 2 или 4) или же число. В случае неудачи необходимо вернуть false.
Ей в качестве аргументов передаются три параметра:
- reg - таблица (структура) с параметрами регистра
- device - таблица (структура) с параметрами данного типа регистра (те данные, которые определены в createDevices)
- unitId – ID устройства на шине или прочий ID. Примером может быть Slave ID в ModBus RTU или Unit ID в ModBus TCP.
В структуре reg есть такие атрибуты:
- internalAddr - пересчитанный адрес регистра. Это число, пересчитанное из указанной системы счисления с прибавлением к нему shift.
- addr - оригинальный адрес регистра, который ввел пользователь.
- dataType – тип данных, который пользователь указал для регистра. 0 = Bit, 1 = Byte, 2 = Word, 3 = Double Word, 4 = UnixTime
В структуре device есть такие атрибуты:
- shift - значение shift из соответствующей строки createDevices
- base - значение base из соответствующей строки createDevices
- xtraFields – значение xtraFields из соответствующей строки createDevices
Эти параметры передаются для того, что бы можно было правильно и полноценно составить запрос согласно протокола.
Для передачи запроса устройству используются функции sendBytes и sendString. Для чтения ответа - readBytes, readString.
На вход sendBytes принимает таблицу (массив) байт. Результатом будет true в случае успеха и false в случае ошибки.
На вход sendString принимает строку. Результатом будет true в случае успеха и false в случае ошибки.
На вход readBytes принимает количество байт, которое необходимо прочитать. Результатом будет таблица (массив) байт в случае успеха и false в случае ошибки.
На вход readString принимает количество байт, которое необходимо прочитать. Результатом будет строка в случае успеха и false в случае ошибки.
Если необходимо закрыть соединение (например, в случае множественных ошибок), то можно вызвать процедуру closeConnection.
Если необходимо сделать паузу, то можно вызвать функцию sleep. Ее единственным аргументом должно быть время в микросекундах. Пример: sleep(20000); - произойдет пауза 20 миллисекунд.
Для работы с битами можно использовать библиотеку bitop [4].
Для отладки и вывода диагностических сообщений можно использовать процедуры ERROR, INFO, DEBUG и TRACE которые доступны и в обычных сценариях Lua [5].
Что бы лучше понять как это все работает давайте рассмотрим пример функции readRegister для протокола ModBus TCP:
local transId = 0;
local errorCount = 0;
function readRegister (reg, device, unitId)
local request = {};
-- transaction ID
transId = transId + 1;
request[1] = bit.band(bit.rshift(transId, 8), 255);
request[2] = bit.band(transId, 255);
-- protocol ID
request[3] = 0;
request[4] = 0;
-- message length
request[5] = 0;
request[6] = 6;
-- unit ID
request[7] = unitId;
-- function code
request[8] = device.xtraFields[1];
-- address of register
request[9] = bit.band(bit.rshift(reg.internalAddr, 8), 255);
request[10] = bit.band(reg.internalAddr, 255);
-- count of registers
request[11] = 0;
request[12] = 1;
if (reg.dataType == 3) then -- double word
request[12] = 2;
end
local res = sendBytes(request);
if (res == false) then
DEBUG("Can't send bytes");
return false;
end
local response = {};
-- read MBAP Header
response = readBytes(7);
if (response == false) then
errorCount = errorCount + 1;
if (errorCount > 3) then
closeConnection();
errorCount = 0;
end
DEBUG("Can't read MBAP");
return false;
end
res = #response;
if (res ~= 7) then
errorCount = errorCount + 1;
if (errorCount > 3) then
closeConnection();
errorCount = 0;
end
DEBUG("Can't read MBAP");
return false;
end
if (response[1] ~= request[1] or response[2] ~= request[2]) then
ERROR("Wrong transaction ID. Got #" .. (response[1] * 256 + response[2]) .. " but expected #" .. (request[1] * 256 + request[2]));
return false;
end
if (response[3] ~= request[3] or response[4] ~= request[4]) then
ERROR("Wrong protocol");
return false;
end
if (response[7] ~= request[7]) then
ERROR("Wrong UnitID in response");
return false;
end
local length = response[5] * 256 + response[6];
if (length < 1) then
ERROR("Wrong length in response");
return false;
end
local responsePDU = {};
-- read MBAP Header
responsePDU = readBytes(length - 1);
if (responsePDU == false) then
errorCount = errorCount + 1;
if (errorCount > 3) then
closeConnection();
errorCount = 0;
end
DEBUG("Can't read PDU in response");
return false;
end
res = #responsePDU;
if (responsePDU[1] ~= request[8]) then
ERROR("Wrong function in response");
return false;
end
local dataLength = responsePDU[2];
if (dataLength ~= length - 3) then
ERROR("Wrong length in PDU");
return false;
end
local result = {};
if (dataLength >= 1) then
for i = 1, dataLength do
result[i] = responsePDU[2 + i];
end
end
return result;
end
writeRegister
Функция writeRegister должна произвести запись нового значения в указанный регистр. При успешной записи она должна вернуть true. В случае ошибки - false.
Ей передаются все те же параметры, что и для readRegister, а также дополнительно четвертый параметр - новое значение.
Функция writeRegister может использовать те же способы записи и чтения массивов байт в порт.
Давайте рассмотрим пример этой функции для протокола ModBus TCP:
function writeRegister (reg, device, unitId, newValue)
local request = {};
transId = transId + 1;
-- transaction ID
request[1] = bit.band(bit.rshift(transId, 8), 255);
request[2] = bit.band(transId, 255);
-- protocol ID
request[3] = 0;
request[4] = 0;
if (reg.dataType == 3) then -- double word
-- message length
request[5] = 0;
request[6] = 11;
-- unit ID
request[7] = unitId;
-- function code
request[8] = device.xtraFields[3];
-- address of register
request[9] = bit.band(bit.rshift(reg.internalAddr, 8), 255);
request[10] = bit.band(reg.internalAddr, 255);
-- count of registers
request[11] = 0;
request[12] = 2;
-- bytes with data
request[13] = 4;
-- value of registers
request[14] = bit.band(bit.rshift(newValue, 24), 255);
request[15] = bit.band(bit.rshift(newValue, 16), 255);
request[16] = bit.band(bit.rshift(newValue, 8), 255);
request[17] = bit.band(newValue, 255);
local res = sendBytes(request);
if (res == false) then
DEBUG("Can't send bytes");
return 0;
end
local response = {};
response = readBytes(7);
if (response == false) then
DEBUG("Can't read response");
return false;
end
res = #response;
if (res ~= 7) then
DEBUG("Wrong response length");
return false;
end
if (response[1] ~= request[1] or response[2] ~= request[2]) then
ERROR("Wrong transaction ID. Got #" .. (response[1] * 256 + response[2]) .. " but expected #" .. (request[1] * 256 + request[2]));
return false;
end
if (response[3] ~= request[3] or response[4] ~= request[4]) then
ERROR("Wrong protocol");
return false;
end
if (response[7] ~= request[7]) then
ERROR("Wrong UnitID in response");
return false;
end
local length = response[5] * 256 + response[6];
if (length < 1) then
ERROR("Wrong length in response");
return false;
end
local responsePDU = {};
responsePDU = readBytes(length - 1);
if (responsePDU == false) then
DEBUG("Can't read response PDU");
return false;
end
res = #responsePDU;
if (responsePDU[1] ~= request[8]) then
ERROR("Wrong function in response");
return false;
end
if (responsePDU[2] ~= request[9] or responsePDU[3] ~= request[10]) then
ERROR("Wrong register address in response");
return false;
end
if (responsePDU[4] ~= 0 or responsePDU[5] ~= 2) then
ERROR("Wrong register count in response");
return false;
end
else
if (device.xtraFields[2] == 0) then
ERROR("Can't write these type of registers (" .. device.name .. ")");
return 0;
end
-- message length
request[5] = 0;
request[6] = 6;
-- unit ID
request[7] = unitId;
request[8] = device.xtraFields[2];
-- address of register
request[9] = bit.band(bit.rshift(reg.internalAddr, 8), 255);
request[10] = bit.band(reg.internalAddr, 255);
local val = newValue;
if (reg.dataType == 0) then
if (val > 0) then
val = 255*256;
else
val = 0;
end
end
-- value of registers
request[11] = bit.band(bit.rshift(val, 8), 255);
request[12] = bit.band(val, 255);
local res = sendBytes(request);
if (res == false) then
DEBUG("Can't send bytes");
return 0;
end
local response = {};
local requestLen = #request;
response = readBytes(requestLen);
if (response == false) then
DEBUG("Can't read response");
return false;
end
res = #response;
if (res ~= requestLen) then
DEBUG("Wrong response length");
return false;
end
for i = 1,res do
if (response[i] ~= request[i]) then
DEBUG("Wrong response");
return false;
end
end
end
return true;
end
Примеры протоколов
В качестве примера мы реализовали несколько протоколов:
На странице подключения к тензометрическому контроллеру Тензод200 также есть пример пользовательского протокола.