В мене дома розведена мережа RS485, яка об’єднує лічильник електроенергії SDM230, годинник, метеостанцію, водяні лічильники. Протоколом обміну вибрано Modbus RTU, яка вимагає мастера, який ініціалізує звертання до пристроїв. Канальна швидкість 9600 байт.
Можна викорстати апаратний міст, наприклад ComPoint II AS від AK-NORD на 2 порти, розглянемо його: конектор DB-9. Може бути або 2 RS-232 або RS232+RS485.
З іншої сторони LAN 10/100 mbit, індикатори статусів, конектор живлення.
Живлення на LDO, запустилася і працювала від 5 вольт. Є версії на імпульскнику. Контролер 16 бітний від NXP MC9S12NE64VTUE.
Інтерфейс RS-232 ADM560. Драйвер RS-485 на Sipex 3072E. Перемичками виставляється режим роботи і підятгуючі резистори.
Конфігурація текстова навіть через web, пароль XT. DHCP імя XT-MICRO-AABBCC.
А можна заставити роутер бути і шлюзом і керувати логікою.
Оскільки є в наявності роутер MR3020 із LEDE 17.01.7 (OpenWRT) із збільшеним flash з 8 мб (на 4 мб не поміститься) і оперативною пам’ятю 32 мб замість 16, який має USB порт, підключаємо китайський USB-RS485 адаптер ціною 1$.
Оскільки ресурси роутера обмежені, використаємо інтерпретатор, який вже інстальований, в даному випадку це LUA. Синтаксис специфічний (не дорівнює ~=, є тип даних nil). Довстановлюємо потрібні модулі:
opkg update opkg install kmod-usb-serial opkg install kmod-usb-serial-ch341 opkg install lua-rs232
Тепер можемо писати наш файл програми modbus.lua. І почнемо з функцій.
Відкриємо порт із швидкістю 9600, перед тим перевіривши чи адаптер розпізнався, переважно /dev/ttyUSB0:
local function openport() local out = nil e, p = rs232.open("/dev/ttyUSB0") if e ~= rs232.RS232_ERR_NOERROR then print(string.format("Can't open serial port %s, %s", "ttyUSB0", rs232.error_tostring(e))) else onport = true p:set_baud_rate(rs232.RS232_BAUD_9600) p:set_data_bits(rs232.RS232_DATA_8) p:set_parity(rs232.RS232_PARITY_NONE) p:set_stop_bits(rs232.RS232_STOP_1) p:set_flow_control(rs232.RS232_FLOW_OFF) out = p end return out end
Прокол Modbus RTU для контролю цілісності пакетів використовує 2 байтну CRC-16. Перевірити коректність формування контрольну суму можна на сайті crccalc.com. Тому пишемо калькулятор суми, який на вході отримує масив байт:
local function crc16(s) local crc = 0xffff for i,val in ipairs(s) do local c = val crc = bit.bxor(crc, c) for j = 1, 8 do local k = bit.band(crc, 1) crc = bit.rshift(crc, 1) if k ~= 0 then crc = bit.bxor(crc, 0xA001) end end end return crc end
Алгоритм використовує бітову операцію сумування по модулю 2 XOR, використаємо бібліотеку бітових операцій numberlua.lua і покладемо у
/usr/lib/lua/bit/numberlua.lua
І фукнцію яка дописує контрольну суму до пакета даних:
local function crc16_add(req) crc = crc16(req) Hi = bit.rshift(crc, 8) Lo = crc - Hi*256 table.insert(req, Lo) table.insert(req, Hi) return req end
Напишемо функцію, яка запитує один регістр із пристрою функцією 04 Read Input Registers і вертатиме результат.
function modbus_req(addr, reg, bytes, IEEE754) local valid=1 local func = 4 reg_hi = bit.rshift(reg, 8) reg_lo = reg - reg_hi*256 req = {addr, func, reg_hi, reg_lo, 0, bytes} req = crc16_add(req) port_w(req) res = port_r() if (res ~= nil) then if res[1] ~= addr then valid=0 end if res[2] ~= func then valid=0 end local datsize = res[3] local crc_now = res[datsize+5]*256+res[datsize+4] table.remove(res,datsize+5) table.remove(res,datsize+4) if (crc_now ~= crc16(res)) then valid=0 print('Bad CRC for packet') end table.remove(res,3) table.remove(res,2) table.remove(res,1) if valid==1 then if (IEEE754 == true) then return UnpackIEEE754(res) else return res end else return nil end else return res end end
Функція записує масив байт у фізичний порт:
function port_w(req) local data = "" for i,val in ipairs(req) do data = data .. string.char(val) end local err, len_written = p:write(data) p:flush() return len_written end
І читання із порту і виходом з функції при відсутності відповіді:
function port_r() local out = nil local receive = 0 local takt = 1 local a=0 while (a == 0) do local read_len, timeout = 1 , 100 local err, data_read, size = p:read(read_len, timeout) if size > 0 then if (receive == 0) then out = {string.byte(data_read)} else table.insert(out, string.byte(data_read)) end receive = receive + size end takt = takt + 1 if takt - receive > 2 then a=1 end end return out end
Оскільки лічильник віддає дані у форматі IEEE754 перетворювач даних:
function UnpackIEEE754(x) local b1, b2, b3, b4 = x[1], x[2], x[3], x[4] local exponent = (b1 % 0x80) * 0x02 + math.floor(b2 / 0x80) local mantissa = math.ldexp(((b2 % 0x80) * 0x100 + b3) * 0x100 + b4, -23) if exponent == 0xFF then if mantissa > 0 then return 0 / 0 else mantissa = math.huge exponent = 0x7F end elseif exponent > 0 then mantissa = mantissa + 1 else exponent = exponent + 1 end if b1 >= 0x80 then mantissa = -mantissa end return math.floor(math.ldexp(mantissa, exponent - 0x7F)*1000)/1000 end
Для запису у регістр функцією 06 Preset Single Register:
function modbus_w(addr, reg, val) local func = 6 reg_hi = bit.rshift(reg, 8) reg_lo = reg - reg_hi*256 val_hi = bit.rshift(val, 8) val_lo = val - val_hi*256 req = {addr, func, reg_hi, reg_lo, val_hi, val_lo} req = crc16_add(req) port_w(req) end
Тепер можна написати саму програму:
p = openport() -- отримаємо показник лічильника і напругу meter = modbus_req(10,0x0156,2,true) volts = modbus_req(10,0x0000,2,true) -- отримані дані відправляємо через wget на сервер local handle = io.popen("/bin/wget -q -O - 'http://****.net.ua/****.php?meter=" .. meter .. "&volts=" .. volts) local result = handle:read("*a") handle:close() -- отримані дані записуємо на індикатор modbus_w(45, 0x0000, result) p:close()
Для періодичного запуску в певний час (наприклад щохвилини) пропишемо у crontab час запуску (для виконання має бути пустий останній рядок):
* * * * * /usr/bin/lua /root/modbus.lua
Тепер на сервері робимо потрібну логіку збереження, обробки і віддачі потрібних команд.
Це швидка реалізація поставленного завдання з того шо було у наявності, зараз йде тестування шлюзу із логікою на базі ESP32 із GPRS модулем.
Цікаве відкриття: у Siemens SIMATIC S7 позначення ліній A і B не відповідає загально прийнятим. Просто навпаки.
Юрій Р. ◯ 0009-0005-3702-9223. (2019). Modbus RTU на роутері (LEDE OpenWRT & LUA). Блог UA ID. Взято з: https://blog.uaid.net.ua/modbus-rtu-router-openwrt-lede-lua