Примеры реализации TCP MODBUS
Примеры для устройств с Lua
TCP MODBUS сервер с использованием LuaSocket
Шаблон простого tcp modbus сервера. Считыватель сохраняет последнюю поднесенную метку в формате: время в unix timestamp в регистрах 0-1 и uid в регистрах 2-5.
LuaSocket не включен по-умолчанию в сборку, его нужно установить из репозитория.
local rfid = require("rfid")
local indication = require("indication")

local PORT = 502

local wdt_on = Settings.get("sys_wdt") == "on"
local snd = indication.Sound.new()
local leds = indication.Leds.new({RGB=true})
local mtx = thread.createmutex()
local holding_registers = {0, 0, 0, 0, 0, 0}

local function pad_uid(uid)
    return string.format("%016s", uid):gsub(" ", "0")
end

local function split_into_words_32(number)
    return (number >> 16) & 0xFFFF,  number & 0xFFFF
end

local function split_uid_into_words(uid)
  return tonumber(string.sub(uid, 1, 4), 16) or 0,
         tonumber(string.sub(uid, 5, 8), 16) or 0,
         tonumber(string.sub(uid, 9, 12), 16) or 0,
         tonumber(string.sub(uid, 13, 16), 16) or 0
end

local function to_holding_registers(regs, ts, uid)
  regs[1], regs[2] = split_into_words_32(ts)
  regs[3], regs[4], regs[5], regs[6] = split_uid_into_words(uid)
end

thread.start(function()
  -- определяем классы
  reader = rfid.Reader({})
  -- стартовая индикация
  leds:start()
  snd:start()
  -- запускаем 
  reader.process({
      timeout_ms = 200, -- таймаут в цикле; всегда должен быть больше 0
      mode = rfid.MODE_LOOP,
      wdt = wdt_on,
      halt = true,
      checkfunc = function(uid) -- функция для проверки метки
        mtx:lock()
        to_holding_registers(holding_registers,  os.time(), pad_uid(uid))
        mtx:unlock()
        leds:ok() -- индикация
        snd:ok() -- звук
      end
  })
end)

--tcp modbus server
local function to_bytes(value)
  return string.pack(">I2", value)
end

local function from_bytes(b1, b2)
  return b1*256 + b2
end

local function build_mbap_header(trans_id_hi, trans_id_lo, protocol_id_hi, protocol_id_lo, length)
  return string.pack(">BBBBH", trans_id_hi, trans_id_lo, protocol_id_hi, protocol_id_lo, length)
end

local function send_response(client, trans_id_hi, trans_id_lo, protocol_id_hi, protocol_id_lo, unit_id, pdu)
  local response = string.pack(">HHHBc"..#pdu, trans_id_hi * 256 + trans_id_lo, protocol_id_hi * 256 + protocol_id_lo, #pdu + 1, unit_id, pdu)
  client:send(response)
end

local function send_error(client, trans_id_hi, trans_id_lo, protocol_id_hi, protocol_id_lo, unit_id, func_code, error_code)
  local error_func_code = string.char(func_code + 0x80)
  local error_pdu = error_func_code .. string.char(error_code)
  send_response(client, trans_id_hi, trans_id_lo, protocol_id_hi, protocol_id_lo, unit_id, error_pdu)
end

local server = assert(socket.bind("*", PORT))
--server:settimeout(0)

 cacheutils.addLog(log, "Modbus TCP устройство запущено на порту " .. PORT, cacheutils.Info)

while true do
  local client = server:accept()
  if client then
    -- client:settimeout(10)
    -- Читаем MBAP Header (7 байт)
    local header, err = client:receive(7)
    if header then
      local trans_id_hi, trans_id_lo,
      protocol_id_hi, protocol_id_lo,
      length_hi, length_lo,
      unit_id = header:byte(1, 7)
      
      local length = from_bytes(length_hi, length_lo)
      -- Длина включает в себя байты Unit ID и следующие за ним байты.
      -- Значит, нам нужно прочитать (length - 1) байт, чтобы получить PDU
      local pdu_len = length - 1
      local pdu, err = client:receive(pdu_len)
      if pdu then
        local fcode = pdu:byte(1)

        if fcode == 3 then
          -- Чтение holding регистров
          -- Формат запроса: Function Code (1 байт), Start Address Hi, Start Address Lo, Quantity Hi, Quantity Lo
          local start_hi, start_lo, qty_hi, qty_lo = pdu:byte(2, 5)
          local start_addr = from_bytes(start_hi, start_lo) + 1  -- +1, т.к. Lua-индексация с 1
          local quantity = from_bytes(qty_hi, qty_lo)

          -- Проверяем, не выходит ли за пределы массива регистров
          if start_addr + quantity - 1 <= #holding_registers then
              local byte_count = quantity * 2
              mtx:lock() -- Блокируем доступ к holding_registers для безопасного чтения
              local format = ">BB" .. string.rep("H", quantity)
              -- Упаковываем Function Code, Byte Count и данные регистров в одну строку
              local response_pdu = string.pack(format, fcode, byte_count, table.unpack(holding_registers, start_addr, start_addr + quantity -1))
              mtx:unlock()
              send_response(client, trans_id_hi, trans_id_lo, protocol_id_hi, protocol_id_lo, unit_id, response_pdu)
          else
            -- Если запрос выходит за пределы, возвращаем ошибку Modbus
            send_error(client, trans_id_hi, trans_id_lo, protocol_id_hi, protocol_id_lo, unit_id, fcode, 0x02)
          end
        else
          -- Неподдерживаемая функция
          send_error(client, trans_id_hi, trans_id_lo, protocol_id_hi, protocol_id_lo, unit_id, fcode, 0x01)
        end
      end
    end
    client:close()
  end

  socket.sleep(0.01)
end
Python-клиент для тестирования
from pymodbus.client import ModbusTcpClient

# Конфигурация подключения
HOST = "192.168.8.220"
PORT = 502

# Создаём клиента
client = ModbusTcpClient(HOST, port=PORT)
client.connect()

# Пример: считаем 6 регистров, начиная с адреса 0 (что будет 1-й регистр в lua-коде)
# В lua-примере holding_registers = [...]
# При запросе от Python мы запрашиваем регистры с 0-адреса:
start_address = 0
count = 6

# Отправляем запрос к устройству
response = client.read_holding_registers(address=start_address, count=count, unit=1)

# Проверяем ответ
if not response.isError():
    print("Считанные регистры:", response.registers)
else:
    print("Ошибка при чтении:", response)

client.close()