Перейти к содержанию
Материалы раздела

Modbus

Модуль Modbus

Модуль поддерживает ведомое устройство Modbus RTU, сервер Modbus TCP, прозрачный шлюз Modbus TCP и ведущее устройство Modbus RTU. Серверы RTU и TCP используют общую таблицу регистров, а прозрачный шлюз передаёт обработчику Lua готовые и проверенные Modbus TCP ADU.

Обзор API

ПодмодульОписание
modbus.rtuВедомое устройство Modbus RTU
modbus.tcpСервер Modbus TCP
modbus.tcprawПрозрачный шлюз Modbus TCP с обработчиком Lua
modbus.masterВедущее устройство Modbus RTU
modbus.utilsВспомогательные функции
modbus.*Общие функции для работы с регистрами

Быстрый старт

TCP сервер

-- Запуск Modbus TCP сервера на порту 502
modbus.tcp.init(502, 1)

-- Установка значений регистров
modbus.setregs(0, 100, 200, 300)
modbus.setinput(0, 1000, 2000)
modbus.setcoils(0, true, false, true)

-- Проверка статуса
local running, port, unit_id = modbus.tcp.status()
print(string.format("TCP сервер: %s, порт %d, unit_id %d",
    running and "запущен" or "остановлен", port, unit_id))

RTU slave

-- Инициализация RTU slave
modbus.rtu.init(1, 2, 19, 18, 13, 9600, 0)

-- Регистры общие для RTU и TCP
modbus.setregs(0, 100, 200, 300)

RTU master

-- Инициализация master
modbus.master.init(2, 19, 18, 13, 9600, 0)

-- Чтение регистров с удаленного устройства
local regs, err = modbus.master.readholding(1, 0, 10)
if regs then
    for i, v in ipairs(regs) do
        print(string.format("Регистр %d: %d", i-1, v))
    end
end

TCP -> RTU gateway

modbus.master.init(2, 19, 18, 13, 9600, 0)
modbus.master.timeout(1000)

local function on_tcp_request(remote_ip, request_adu)
    local trans_id, protocol_id, length, unit_id = modbus.utils.parsembap(request_adu:sub(1, 7))
    if protocol_id ~= 0 then
        return nil
    end

    local pdu = request_adu:sub(8, 6 + length)
    local rtu_response = modbus.master.request(string.char(unit_id) .. pdu)
    if not rtu_response then
        local error_pdu = modbus.utils.errorresponse(pdu:byte(1), 0x0B)
        return modbus.utils.buildmbap(trans_id, 0, #error_pdu + 1, unit_id) .. error_pdu
    end

    local response_pdu = rtu_response:sub(2)
    return modbus.utils.buildmbap(trans_id, 0, #response_pdu + 1, unit_id) .. response_pdu
end

modbus.tcpraw.start(502, on_tcp_request, 300)

Modbus TCP

Сервер Modbus TCP использует те же регистры, что и ведомое устройство Modbus RTU. Запросы с unit_id, отличным от настроенного значения, игнорируются без ответа; unit_id = 0 обрабатывается как общий запрос.

Поддерживаемые функции Modbus

FCНазваниеОписание
01Read CoilsЧтение состояния coils
02Read Discrete InputsЧтение дискретных входов
03Read Holding RegistersЧтение holding регистров
04Read Input RegistersЧтение input регистров
05Write Single CoilЗапись одного coil
06Write Single RegisterЗапись одного регистра
15Write Multiple CoilsЗапись нескольких coils
16Write Multiple RegistersЗапись нескольких регистров
23Read/Write Multiple RegistersЧтение и запись регистров

modbus.tcp.init(port, unit_id)

Запуск Modbus TCP сервера.

Аргументы:

  • port (необязательно): TCP порт (1-65535, по умолчанию 502).
  • unit_id (необязательно): идентификатор устройства (1-247, по умолчанию 1).

Возвращает: ничего или исключение.

-- Запуск на стандартном порту 502
modbus.tcp.init()

-- Запуск на альтернативном порту с unit_id = 10
modbus.tcp.init(1502, 10)

modbus.tcp.stop()

Остановка Modbus TCP сервера.

modbus.tcp.stop()

running, port, unit_id = modbus.tcp.status()

Получение статуса TCP сервера.

Возвращает:

  • running: true если сервер работает.
  • port: текущий порт.
  • unit_id: идентификатор устройства.
local running, port, unit_id = modbus.tcp.status()
if running then
    print(string.format("Сервер работает на порту %d", port))
end

Прозрачный шлюз Modbus TCP

Этот режим передаёт запрос Modbus TCP внешнему обработчику — например, ведущему устройству RTU в шлюзе TCP → RTU. Сервер собирает входящий поток в полные Modbus TCP ADU и проверяет заголовок MBAP до вызова обработчика Lua.

modbus.tcpraw не использует общую таблицу регистров. Обработчик получает полный Modbus TCP ADU без CRC и должен вернуть полный ADU ответа. Если он возвращает nil, завершается с ошибкой или формирует некорректный ADU, сервер отвечает исключением Modbus 0x04 (Slave Device Failure) для кода функции запроса.

modbus.tcpraw.start(port, callback, idle_timeout_sec)

Запускает прозрачный сервер Modbus TCP.

Аргументы:

  • port: TCP порт (1-65535, обычно 502).
  • callback: функция callback(remote_ip, request_adu) -> response_adu;
  • idle_timeout_sec (необязательно): таймаут простоя клиента в секундах (по умолчанию 300, 0 отключает автоотключение).

Обработчик получает:

  • remote_ip: строка с адресом клиента.
  • request_adu: полный Modbus TCP ADU (MBAP + PDU). CRC в Modbus TCP отсутствует.

Обработчик возвращает:

  • response_adu: полный Modbus TCP ADU ответа (MBAP + PDU) или nil, чтобы вернуть исключение 0x04.
local function on_request(ip, adu)
    local tid, proto, len, unit = modbus.utils.parsembap(adu:sub(1, 7))
    if proto ~= 0 then
        return nil
    end

    local pdu = adu:sub(8, 6 + len)
    local rtu_response = modbus.master.request(string.char(unit) .. pdu)
    if not rtu_response then
        local error_pdu = modbus.utils.errorresponse(pdu:byte(1), 0x0B)
        return modbus.utils.buildmbap(tid, 0, #error_pdu + 1, unit) .. error_pdu
    end

    local response_pdu = rtu_response:sub(2)
    return modbus.utils.buildmbap(tid, 0, #response_pdu + 1, unit) .. response_pdu
end

modbus.tcpraw.start(502, on_request, 300)

modbus.tcpraw.stop()

Останавливает сервер и активные клиентские соединения.

modbus.tcpraw.stop()

running, port, active_clients, max_clients = modbus.tcpraw.status()

Возвращает состояние прозрачного TCP-сервера.

Возвращает:

  • running: true если сервер работает.
  • port: текущий порт.
  • active_clients: количество активных TCP клиентов.
  • max_clients: максимальное количество одновременных клиентов.
local running, port, active, max = modbus.tcpraw.status()
print(string.format("TCP raw: %s, port %d, clients %d/%d",
    running and "running" or "stopped", port, active, max))

Modbus RTU

modbus.rtu.init(addr, port, rxpin, txpin, rtspin, baudrate, parity, clear_holding_on_read)

Инициализирует ведомое устройство Modbus RTU.

Все параметры необязательны. Для пропущенных аргументов используются значения, заданные для модели устройства.

Аргументы:

  • addr (необязательно): адрес устройства в сети Modbus (1-247).
  • port (необязательно): номер UART порта.
  • rxpin (необязательно): номер GPIO пина для приема данных.
  • txpin (необязательно): номер GPIO пина для передачи данных.
  • rtspin (необязательно): номер GPIO пина RTS/DE для управления направлением RS485.
  • baudrate (необязательно): скорость UART.
  • parity (необязательно): четность (0 = none, 2 = even, 3 = odd).
  • clear_holding_on_read (необязательно): очищать holding-регистры после внешнего FC03-чтения (по умолчанию true для обратной совместимости). Установите false, если регистры представляют текущее состояние; тогда приложение должно само очищать устаревшие значения.

Возвращает: ничего или исключение.

modbus.rtu.init()

-- Явно задаем только часть параметров
modbus.rtu.init(1)
modbus.rtu.init(1, 2, 19, 18, 13, 9600, 0)

-- FC03 не изменяет holding-регистры
modbus.rtu.init(1, nil, nil, nil, nil, 115200, 0, false)

После успешной инициализации обработка запросов запускается в фоновом режиме. При clear_holding_on_read=true модуль ставит FC03-чтения в локальную очередь и очищает прочитанный диапазон после формирования ответа. Если приложение или другой Modbus-запрос успели записать регистр во время передачи ответа, новое значение сохраняется и отложенная очистка его не затрагивает. При false FC03 возвращает значения без изменения holding-регистров.

Межбайтовый тайм-аут RTU рассчитывается по t1.5: через время символа для скоростей до 19200 бод и по фиксированному интервалу 750 мкс для более высоких скоростей (с округлением вверх до миллисекунды).

modbus.rtu.poll()

Ручная обработка локальной очереди уведомлений Modbus. Очистка прочитанных holding-регистров выполняется только в режиме clear_holding_on_read=true. Сам приём кадров RTU выполняется в фоне.

modbus.rtu.poll()

interval_ms = modbus.rtu.pollstart([interval_ms])

Запускает или перезапускает фоновую обработку с указанным интервалом.

Аргументы:

  • interval_ms (необязательно): интервал опроса в миллисекундах (минимум 1).

Возвращает:

  • interval_ms: фактический интервал, с которым запущена задача.
local interval = modbus.rtu.pollstart(10)
print("RTU poll interval:", interval)

modbus.rtu.pollstop()

Останавливает фоновую обработку.

modbus.rtu.pollstop()

modbus.rtu.close()

Остановка Modbus RTU интерфейса и освобождение ресурсов.

modbus.rtu.close()

Общие функции для работы с регистрами

Эти функции управляют регистрами, общими для RTU slave и TCP slave. Изменения видны обоим транспортам.

Holding Registers (FC03/FC06/FC16)

modbus.setregs(addr, value1, value2, …)

Установка значений holding регистров.

Аргументы:

  • addr: начальный адрес регистра (0-255).
  • value1, value2, …: значения регистров (0-65535).
modbus.setregs(0, 100, 200, 300)
modbus.setregs(100, 1234)

modbus.setregs8(addr, byte1, byte2, …)

modbus.setregs8(addr, str)

Установка holding регистров в байтовом представлении.

Аргументы:

  • addr: начальный адрес регистра (0-255).
  • byte1, byte2, …, либо строка: байтовые значения (0-255). Каждые два байта формируют один регистр.
  -- Режим 1: Строка байтов
  modbus.setregs8(100, "Hello")
  modbus.setregs8(100, "\x48\x65\x6C\x6C\x6F")

  -- Режим 2: Отдельные байты как аргументы (было setregs8)
  modbus.setregs8(100, 0x48, 0x65, 0x6C, 0x6C, 0x6F)

  -- Комбинация с unhexlify
  local data = cutils.unhexlify("48656C6C6F", true)
  modbus.setregs8(100, data)

values = modbus.getregs(addr, count, clear)

Чтение holding регистров.

Аргументы:

  • addr: начальный адрес регистра (0-255).
  • count (необязательно): количество регистров (по умолчанию 1).
  • clear (необязательно): очистить после чтения (по умолчанию false).

Возвращает: таблицу со значениями.

local values = modbus.getregs(0, 10)
for i, v in ipairs(values) do
    print(string.format("Регистр %d: %d", i-1, v))
end

bytes = modbus.getregs8(addr, count, clear)

Чтение holding регистров в байтовом представлении.

Возвращает: таблицу с байтами (2 байта на регистр).

local bytes = modbus.getregs8(100, 2)
print(string.format("0x%02X%02X 0x%02X%02X", bytes[1], bytes[2], bytes[3], bytes[4]))

Input Registers (FC04)

modbus.setinput(addr, value1, value2, …)

Установка значений input регистров.

modbus.setinput(0, 1000, 2000, 3000)

values = modbus.getinput(addr, count)

Чтение input регистров.

local values = modbus.getinput(0, 5)

Coils (FC01/FC05/FC15)

modbus.setcoils(addr, value1, value2, …)

Установка coils.

Аргументы:

  • addr: начальный адрес coil (0-255).
  • value1, value2, …: boolean значения (true/false).

Числа, строки, nil и другие типы не преобразуются в логические значения, а вызывают ошибку аргумента.

modbus.setcoils(0, true, false, true, true)

values = modbus.getcoils(addr, count)

Чтение coils.

Возвращает: таблицу с boolean значениями.

local coils = modbus.getcoils(0, 8)
for i, v in ipairs(coils) do
    print(string.format("Coil %d: %s", i-1, v and "ON" or "OFF"))
end

Discrete Inputs (FC02)

modbus.setdiscrete(addr, value1, value2, …)

Установка discrete inputs.

Все значения должны иметь тип boolean (true или false); автоматического преобразования чисел и других типов нет.

modbus.setdiscrete(0, true, true, false, false)

values = modbus.getdiscrete(addr, count)

Чтение discrete inputs.

local inputs = modbus.getdiscrete(0, 8)

Modbus Master

Функции для работы в режиме ведущего устройства Modbus RTU.

ФункцияFCОписание
init-Инициализация интерфейса
close-Закрытие интерфейса
timeout-Установка таймаута
readholding03Чтение Holding регистров
readinput04Чтение Input регистров
writesingle06Запись одного регистра
writemultiple16Запись нескольких регистров
readcoils01Чтение Coils
writecoil05Запись одного Coil
writecoils15Запись нескольких Coils
readdiscrete02Чтение Discrete Inputs
request-Произвольный запрос

modbus.master.init(port, rxpin, txpin, rtspin, baudrate, parity)

Инициализация Modbus Master интерфейса.

Аргументы:

  • port: номер UART порта (1, 2).
  • rxpin: номер GPIO пина для приема данных.
  • txpin: номер GPIO пина для передачи данных.
  • rtspin: номер GPIO пина для управления направлением RS485.
  • baudrate: скорость передачи данных (9600, 19200, 38400, 57600, 115200).
  • parity (необязательно): четность (0=none, 2=even, 3=odd, по умолчанию 0).
modbus.master.init(2, 19, 18, 13, 9600, 0)

modbus.master.close()

Закрытие Modbus Master интерфейса.

modbus.master.close()

modbus.master.timeout(ms)

Установка таймаута ожидания ответа.

Аргументы:

  • ms: таймаут в миллисекундах (10-30000, по умолчанию 1000).
modbus.master.timeout(500)

values, err = modbus.master.readholding(slave, addr, count)

Чтение Holding регистров (функция 03).

Аргументы:

  • slave: адрес slave устройства (1-247).
  • addr: начальный адрес регистра (0-65535).
  • count: количество регистров (1-125).

Возвращает: таблицу со значениями или nil и строку ошибки.

local regs, err = modbus.master.readholding(1, 0, 10)
if regs then
    for i, v in ipairs(regs) do
        print(string.format("Регистр %d: %d", i-1, v))
    end
else
    print("Ошибка:", err)
end

values, err = modbus.master.readinput(slave, addr, count)

Чтение Input регистров (функция 04).

local regs, err = modbus.master.readinput(1, 100, 5)
if regs then
    print("Температура:", regs[1] / 10, "°C")
end

ok, err = modbus.master.writesingle(slave, addr, value)

Запись одного Holding регистра (функция 06).

local ok, err = modbus.master.writesingle(1, 100, 12345)
if ok then
    print("Регистр записан")
end

ok, err = modbus.master.writemultiple(slave, addr, values)

Запись нескольких Holding регистров (функция 16).

local ok, err = modbus.master.writemultiple(1, 0, {100, 200, 300, 400})

values, err = modbus.master.readcoils(slave, addr, count)

Чтение Coils (функция 01).

local coils, err = modbus.master.readcoils(1, 0, 16)
if coils then
    for i, v in ipairs(coils) do
        print(string.format("Coil %d: %s", i-1, v and "ON" or "OFF"))
    end
end

ok, err = modbus.master.writecoil(slave, addr, value)

Запись одного Coil (функция 05).

Аргумент value должен иметь тип boolean (true или false).

modbus.master.writecoil(1, 0, true)

ok, err = modbus.master.writecoils(slave, addr, values)

Запись нескольких Coils (функция 15).

Каждый элемент таблицы values должен иметь тип boolean (true или false).

modbus.master.writecoils(1, 0, {true, false, true, true})

values, err = modbus.master.readdiscrete(slave, addr, count)

Чтение Discrete Inputs (функция 02).

local inputs, err = modbus.master.readdiscrete(1, 0, 8)

response, err = modbus.master.request(data)

Отправляет произвольный запрос Modbus RTU. Функция предназначена в том числе для прозрачного шлюза TCP → RTU: на вход передаётся RTU ADU без CRC (unit_id + PDU), CRC добавляется автоматически, а ответ также возвращается без CRC.

Аргументы:

  • data: строка с RTU ADU без CRC (unit_id + function code + data).

Возвращает: строку с ответом (без CRC) или nil и строку ошибки.

local request = string.char(0x01, 0x03, 0x00, 0x00, 0x00, 0x01)
local response, err = modbus.master.request(request)

Modbus Utils

Вспомогательные функции для работы с протоколом Modbus.

value = modbus.utils.frombytes(hi, lo)

Преобразование двух байтов в 16-битное число (Big Endian).

local value = modbus.utils.frombytes(0x12, 0x34)
print(value)  -- 4660 (0x1234)

hi, lo = modbus.utils.tobytes(value)

Преобразование 16-битного числа в два байта (Big Endian).

local hi, lo = modbus.utils.tobytes(4660)
print(string.format("0x%02X, 0x%02X", hi, lo))  -- 0x12, 0x34

value = modbus.utils.frombytes32(b3, b2, b1, b0)

Преобразование четырех байтов в 32-битное число (Big Endian).

local value = modbus.utils.frombytes32(0x12, 0x34, 0x56, 0x78)
print(value)  -- 305419896 (0x12345678)

b3, b2, b1, b0 = modbus.utils.tobytes32(value)

Преобразование 32-битного числа в четыре байта (Big Endian).

local b3, b2, b1, b0 = modbus.utils.tobytes32(305419896)

crc = modbus.utils.crc16(data)

Вычисление CRC16 для данных (стандарт Modbus RTU).

local data = string.char(0x01, 0x03, 0x00, 0x00, 0x00, 0x01)
local crc = modbus.utils.crc16(data)
print(string.format("CRC16: 0x%04X", crc))

mbap = modbus.utils.buildmbap(trans_id, protocol_id, length, unit_id)

Построение MBAP заголовка для Modbus TCP.

local mbap = modbus.utils.buildmbap(1234, 0, 6, 1)

trans_id, protocol_id, length, unit_id = modbus.utils.parsembap(data)

Разбор MBAP заголовка из строки.

local trans_id, protocol_id, length, unit_id = modbus.utils.parsembap(data:sub(1, 7))

packed = modbus.utils.packbytes(byte1, byte2, …)

Упаковка байтов в строку.

local packet = modbus.utils.packbytes(0x01, 0x03, 0x00, 0x00, 0x00, 0x01)

bytes = modbus.utils.unpackbytes(data)

Распаковка строки в массив байтов.

local bytes = modbus.utils.unpackbytes(data)

byte_data = modbus.utils.wordstobytes(words)

Преобразование массива 16-битных слов в строку байтов.

local words = {0x1234, 0x5678}
local byte_data = modbus.utils.wordstobytes(words)

words = modbus.utils.bytestowords(data)

Преобразование строки байтов в массив 16-битных слов.

local words = modbus.utils.bytestowords("\x12\x34\x56\x78")

error_pdu = modbus.utils.errorresponse(func_code, error_code)

Создание ответа с ошибкой Modbus.

local error_pdu = modbus.utils.errorresponse(3, 1)

is_error, [func_code, error_code] = modbus.utils.iserror(data)

Проверка, является ли ответ ошибкой Modbus.

local is_error, func_code, error_code = modbus.utils.iserror(response)
if is_error then
    print(string.format("Ошибка в функции %d: код %d", func_code, error_code))
end

expected_len = modbus.utils.expectedrtulen(pdu)

Расчет ожидаемой длины RTU-ответа на основе PDU запроса.

Поддерживаемая логика расчета в модуле:

FCФункцияОжидаемая длина ответа RTU
01Read Coils1 + 1 + 1 + ceil(quantity/8) + 2
02Read Discrete Inputs1 + 1 + 1 + ceil(quantity/8) + 2
03Read Holding Registers1 + 1 + 1 + quantity*2 + 2
04Read Input Registers1 + 1 + 1 + quantity*2 + 2
05Write Single Coil8
06Write Single Register8
07Read Exception Status5
08Diagnostics1 + len(pdu) + 2
11Get Comm Event Counter8
15Write Multiple Coils8
16Write Multiple Registers8
17Report Slave ID~10 (оценка)
22Mask Write Register10
23Read/Write Multiple1 + 1 + 1 + read_quantity*2 + 2

Для неизвестных FC функция возвращает минимальную длину error-response: 5.

local expected_len = modbus.utils.expectedrtulen(pdu)

is_valid = modbus.utils.verifycrc(rtu_data)

Проверка CRC16 в RTU пакете.

if modbus.utils.verifycrc(rtu_response) then
    print("CRC корректен")
end

Коды ошибок Modbus

КодОписание
0x01Недопустимая функция
0x02Недопустимый адрес данных
0x03Недопустимое значение данных
0x04Сбой slave-устройства
0x05Подтверждение (acknowledge)
0x06Slave-устройство занято
0x08Ошибка четности памяти
0x0AШлюз недоступен
0x0BУстройство не отвечает

Примеры использования

RTU + TCP шлюз

-- Запуск TCP сервера
modbus.tcp.init(502, 1)

-- Запуск RTU slave на том же устройстве
modbus.rtu.init(1, 2, 19, 18, 13, 9600, 0)

-- Регистры общие для обоих транспортов
modbus.setregs(0, 100, 200, 300)
modbus.setinput(0, 1000, 2000)

print("Modbus RTU/TCP шлюз запущен")
print("TCP: порт 502")
print("RTU: UART2, 9600 бод")

Симуляция датчиков

modbus.tcp.init(502, 1)

-- Обновление показаний в цикле
while true do
    local temp = math.random(200, 300)  -- 20.0-30.0°C
    local humidity = math.random(400, 600)  -- 40-60%
    local pressure = math.random(1000, 1020)  -- мбар

    modbus.setinput(0, temp, humidity, pressure)
    modbus.setcoils(0, temp > 250, humidity > 500)

    tmr.delay(1000)
end

Modbus Master - опрос устройства

modbus.master.init(2, 19, 18, 13, 9600, 0)
modbus.master.timeout(500)

local function poll_device(slave_addr)
    -- Чтение holding регистров
    local holding, err = modbus.master.readholding(slave_addr, 0, 10)
    if not holding then
        print("Ошибка чтения holding:", err)
        return
    end

    -- Чтение input регистров
    local input = modbus.master.readinput(slave_addr, 0, 5)

    -- Чтение coils
    local coils = modbus.master.readcoils(slave_addr, 0, 8)

    print("Holding:", table.concat(holding, ", "))
    if input then print("Input:", table.concat(input, ", ")) end
    if coils then
        local status = {}
        for i, v in ipairs(coils) do
            status[i] = v and "1" or "0"
        end
        print("Coils:", table.concat(status, ""))
    end
end

while true do
    poll_device(1)
    tmr.delay(1000)
end

Управление реле через Master

modbus.master.init(2, 19, 18, 13, 9600, 0)
modbus.master.timeout(300)

-- Включить реле 0-3
modbus.master.writecoils(1, 0, {true, true, true, true})
print("Реле включены")

tmr.delay(5000)

-- Выключить все
modbus.master.writecoils(1, 0, {false, false, false, false})
print("Реле выключены")

modbus.master.close()

TCP/RTU преобразователь

-- Настройка TCP сервера для приема запросов
modbus.master.init(2, 19, 18, 13, 9600, 0)
modbus.master.timeout(1000)

local function process_tcp_request(remote_ip, request_adu)
    local trans_id, protocol_id, length, unit_id = modbus.utils.parsembap(request_adu:sub(1, 7))
    if protocol_id ~= 0 then
        return nil
    end

    local pdu = request_adu:sub(8, 6 + length)

    -- Формирование RTU запроса
    local rtu_request = string.char(unit_id) .. pdu

    -- Отправка через master.request
    local response, err = modbus.master.request(rtu_request)
    if not response then
        return nil, err
    end

    -- Формирование TCP ответа
    local response_pdu = response:sub(2)  -- без Unit ID
    local mbap = modbus.utils.buildmbap(trans_id, 0, #response_pdu + 1, unit_id)
    return mbap .. response_pdu
end

modbus.tcpraw.start(502, process_tcp_request, 300)

Работа с байтовыми данными

-- Запись UID карты RFID (8 байт = 4 регистра)
local uid = "1234567890ABCDEF"
local uid_bytes = {}

for i = 1, #uid, 2 do
    table.insert(uid_bytes, tonumber(uid:sub(i, i+1), 16))
end

modbus.setregs8(100, table.unpack(uid_bytes))

-- Чтение обратно
local read_bytes = modbus.getregs8(100, 4)
local reconstructed_uid = ""
for _, byte in ipairs(read_bytes) do
    reconstructed_uid = reconstructed_uid .. string.format("%02X", byte)
end

print("Исходный UID:", uid)
print("Восстановленный:", reconstructed_uid)