ИНструкция
Преобразователь Ethernet-RS485
Преобразователь Ethernet-RS485 на базе мини-контролера ODPCL-RS485 - это маленький конвертер интерфейсов с программируемой логикой
Ключевые возможности
  • Программирование на Lua
    Вы можете посмотреть программу, которая работает в устройстве и самостоятельно ее поменять. Мы приготовили необходимые библиотеки и примеры кода для легкого погружения в материал.
  • WiFi интерфейс
    Дополнительный WiFi интерфейс для конфигурации и настройки через Web.
    Вы можете подключить его к вашей существующей WiFi сети для доступа в Internet.
  • Bluetooth (Pro)
    Контроллер (в версии Pro) может отправлять BLE Eddystone beacons для индикации дополнительных метрик с помощью Bluetoorh.
  • Сохранение и отправка отчетов
    Контроллер может собирать логи работы: состояние температурных и дискретных датчиков, отправленные команды и т.п. и сохранять это во встроенной памяти, либо отправлять на сервер по tcp/udp/mqtt или на почту (Pro).
  • Telegram
    Прямая интеграция с вашим телеграм ботом без использования сторонних сервисов.
    • Можно отправлять команды на перезагрузку и получать данные о работе, данные датчиков, статистику и т.д.
    • Можно написать свои действия на команды.
  • Обновления и пакетный менеджер
    Устройство может обновляться онлайн, а также имеет пакетный менеджер для установки библиотек по сети, либо обновления исполняемого кода.
Исполнения
Базовая и Pro версии
PRO версия отличается большим количеством энергонезависимой и оперативной памяти.

Дополнительные ресурсы Pro позволяют:
  • исполнять более сложные алгоритмы
  • использовать BLE
  • использовать SSH
  • отправлять данные по почте
Подключение
Интерфейс: Ethernet.
Питание: Passive PoE 7-24В.
По-умолчанию, устройство подключается к DHCP-серверу и получает от него IP-адрес. В настройках можно поменять на статический адрес.
Индикация сети: красный - нет линка, зелёный - есть линк.

Интерфейс: RS485.
Контакты клеммной колодки:
  • +5В
  • B
  • A
  • GND
Web-интерфейс
На момент написания этой заметки некоторые сборки Chrome могут блокировать часть функционала, т.к. устройство работает через http, а не https. Подробнее: https://itecnote.com/tecnote/websocket-connection-fails-on-chrome-without-ssl.
Если у вас возникает эта проблема, пожалуйста, используйте другой браузер.

Основной экран

На данном экране можно увидеть общую информацию об устройстве и сообщения из кода (отправленные из программы пользователя).


Редактор кода

Простой редактор для модификации встроенной программы.
Позволяет изменять только в файле usercode.lua, который загружается автоматически.

Этого хватает для подавляющего большинства задач. Если у вас планируется разработка большого проекта, то вы можете работать с файлами, ходить по файловой системе на устройство через консоль, подключившись по usb.

Если вы перезагружаете устройство, то надо перезагружать и странцу (примерно через 5-10 секунд). Потому что у вас в браузере осталась страница, которая не знает, что устройство перезагружено и она будет вести себя непредсказуемо, исходя из времени жизни кеша, открытых соединений и т.п.
Технические характеристики
1. Габаритные размеры: 90х60х15мм.
2. Интерфейсы: Ethernet,RS485,(WiFi доп.) + Ble (для Pro)
3. Питание: Ethernet Passive PoE 7-24В.
Внимание: корпус не герметичный (предназначен только для защиты платы) и предполагает применение устройства в закрытом помещении. Но мы можем покрыть плату лаком.
Почему Lua ?

Хотя Lua далеко не самый распространенный язык, он имеет ряд больших преимуществ:
  • предназначен для пользователей, не являющихся профессиональными программистами, вследствие чего большое внимание уделено простоте дизайна и лёгкости обучения;
  • скриптовый язык - не требует отдельной компиляции;
  • маленький размер - в контроллер умещается полная версия языка, а не урезанная;
  • простой паскалеподобный синтаксис, известный многим еще со школы;
  • минимум синтаксиса;
  • поддержка ООП;


Примеры кода для устройства
В преобразовании интерфейсов возникают самые разные сценарии, поэтому мы предлагаем базовые сценарии работы и помогаем клиентам адаптировать их под их задачи.
Простой синхронный прозрачный мост UDP-RS485
Синхронный мост Ethernet UDP-RS485 при котором инициатором передачи является сетевой запрос, который инициирует обмен с RS485 устройством и ожидает ответ в течении таймаута.
-- Простой синхронный прозрачный мост UDP-RS485

-- ========== Настройки ==========
local UDP_PORT = 502
local BAUDRATE = 115200
local CHUNK_SIZE = 32         -- Размер читаемого кусочка RS485
local READ_TIMEOUT = 100     -- Таймаут чтения кусочка (мс)
local RESPONSE_TIMEOUT = 1000 -- Общий таймаут ответа от RS485 (мс)

-- ========== Инициализация UART ==========
local UART_PORT = uart.UART1
local UART_RX = pio.GPIO14
local UART_TX = pio.GPIO23  
local UART_RTS = pio.GPIO12

-- Настройка UART
uart.setpins(UART_PORT, UART_RX, UART_TX)
uart.attach(UART_PORT, BAUDRATE, 8, uart.PARNONE, uart.STOP1)
pio.pin.setdir(pio.OUTPUT, UART_RTS)
pio.pin.setlow(UART_RTS)  -- По умолчанию в режиме приема

-- ========== Обработка UDP → RS485 ==========
local function process_udp_to_rs485(client_ip, client_port, udp_data)
    if #udp_data == 0 then
        print("❌ Пустой UDP пакет")
        return nil
    end

    -- Очищаем буфер UART и отправляем данные
    uart.consume(UART_PORT)
    uart.write(UART_PORT, udp_data, UART_RTS)
    
    -- Читаем ответ от RS485 кусочками
    local response_data = ""
    local start_time = os.uptime(2)
    local first_byte_received = false
    
    while true do
        local current_time = os.uptime(2)
        
        -- Проверяем общий таймаут
        if (current_time - start_time) >= RESPONSE_TIMEOUT then
            if #response_data == 0 then
                print("❌ Таймаут: нет ответа от RS485")
                return nil
            else
                print(string.format("⚠️ Таймаут ответа, получено %d байт", #response_data))
                break
            end
        end
        
        -- Читаем кусочек данных
        local chunk = uart.read(UART_PORT, "*nl", READ_TIMEOUT, CHUNK_SIZE)
        if chunk and #chunk > 0 then
            response_data = response_data .. chunk
            first_byte_received = true
        elseif first_byte_received then
            -- Если уже получали данные, проверяем тишину
            thread.sleepms(50)  -- Небольшая пауза для завершения передачи
            
            local final_chunk = uart.read(UART_PORT, "*nl", 20, CHUNK_SIZE)
            if final_chunk and #final_chunk > 0 then
                response_data = response_data .. final_chunk
            else
                -- Тишина - данные закончились
                break
            end
        end
        
        thread.sleepms(10)
    end
    
    return response_data
end

-- ========== Основной цикл UDP сервера ==========
while not net.connected() do
    print("⏳ Ожидание сети...")
    thread.sleepms(2000)  -- Проверяем каждые 2 секунды
end

print("✅ Сетевое подключение установлено!")
print("🔗 Привязка к UDP порту", UDP_PORT)

socket = net.udp.bind(UDP_PORT)
if not socket then
    print("❌ Не удалось привязаться к UDP порту", UDP_PORT)
    print("💡 Возможно порт уже используется")
    return
end

print("✅ UDP сервер запущен на порту", UDP_PORT)

while true do
    -- Читаем UDP пакет (неблокирующее чтение)  
    local client_ip, client_port, udp_data = net.udp.readfrom(socket, 1024)
    if client_ip and client_port and udp_data and #udp_data > 0 then
        -- Обрабатываем UDP → RS485 → UDP
        local response = process_udp_to_rs485(client_ip, client_port, udp_data)
        
        if response then
            net.udp.sendto(client_ip, client_port, response)
            print("✅ Ответ отправлен в UDP")
        else
            print("⚠️ Нет ответа от RS485 - UDP ответ не отправлен")
        end
    end
    
    -- Небольшая пауза для других задач
    thread.sleepms(5)
end
Простой прозрачный мост между Modbus TCP и RTU
Синхронный мост Modbus TCP-RS485 при котором инициатором передачи является сетевой запрос, который инициирует обмен с RS485 устройством и ожидает ответ в течении таймаута.
-- Modbus TCP/RTU Transparent Bridge
-- Простой прозрачный мост между Modbus TCP и RTU

-- ========== Настройки ==========
local TCP_PORT = 502
local BAUDRATE = 115200

-- ========== Инициализация UART ==========
local UART_PORT = uart.UART1
local UART_RX = pio.GPIO14
local UART_TX = pio.GPIO23  
local UART_RTS = pio.GPIO12

-- Настройка пинов UART
uart.setpins(UART_PORT, UART_RX, UART_TX)
uart.attach(UART_PORT, BAUDRATE, 8, uart.PARNONE, uart.STOP1)
pio.pin.setdir(pio.OUTPUT, UART_RTS)
pio.pin.setlow(UART_RTS)  -- По умолчанию в режиме приема

-- ========== Мьютекс для UART ==========
local uart_mutex = thread.createmutex()

-- ========== Прозрачный мост ==========
local function transparent_bridge(client_ip, tcp_data)
    -- Блокируем UART для исключения одновременных запросов
    uart_mutex:lock()
    
    -- print(string.format("📨 Получен TCP пакет от %s, длина: %d", client_ip, #tcp_data))
    -- Проверяем минимальную длину TCP пакета
    if #tcp_data < 8 then
        print("❌ Слишком короткий TCP пакет")
        uart_mutex:unlock()
        return
    end
    
    -- 1. Разбираем MBAP заголовок
    local trans_id, protocol_id, length, unit_id = modbus.utils.parsembap(tcp_data:sub(1, 7))
    local pdu = tcp_data:sub(8, 7 + length - 1)
    -- print(string.format("📋 MBAP: TransID=%d, UnitID=%d, PDU длина=%d", trans_id, unit_id, #pdu))
    
    -- 2. Формируем RTU пакет: UnitID + PDU + CRC16
    local rtu_request = string.char(unit_id) .. pdu
    local crc = modbus.utils.crc16(rtu_request)
    local crc_hi, crc_lo = modbus.utils.tobytes(crc)
    
    -- В Modbus RTU CRC передается в Little Endian (младший байт первый)
    rtu_request = rtu_request .. string.char(crc_lo, crc_hi)
    -- print(string.format("📤 Отправка RTU: %d байт, CRC=0x%04X", #rtu_request, crc))

    -- 3. Очищаем буфер UART и отправляем запрос
    uart.consume(UART_PORT)
    uart.write(UART_PORT, rtu_request, UART_RTS)

    -- 4. Ожидаем ответ от RTU устройства
    local expected_len = modbus.utils.expectedrtulen(pdu) 
    local rtu_response = uart.read(UART_PORT, "*nl", 200, expected_len)

    if not rtu_response or #rtu_response < 3 then
        print("❌ Таймаут или нет ответа от RTU устройства")
        local error_pdu = modbus.utils.errorresponse(pdu:byte(1), 0x04)
        local mbap = modbus.utils.buildmbap(trans_id, protocol_id, #error_pdu + 1, unit_id)
        local error_response = mbap .. error_pdu
        net.tcpserv.send(error_response, 0, true)
        uart_mutex:unlock()
        return
    end
    -- print(string.format("📥 Получен RTU ответ: %d байт", #rtu_response))
 
    -- 5. Проверяем CRC16 ответа
    if not modbus.utils.verifycrc(rtu_response) then
        print(string.format("❌ CRC ошибка: получен=0x%04X, вычислен=0x%04X", received_crc, calculated_crc))
        local error_pdu = modbus.utils.errorresponse(pdu:byte(1), 0x04)
        local mbap = modbus.utils.buildmbap(trans_id, protocol_id, #error_pdu + 1, unit_id)
        local error_response = mbap .. error_pdu
        net.tcpserv.send(error_response, 0, true)
        uart_mutex:unlock()
        return
    end
    
    -- 6. Проверяем Unit ID в ответе
    local response_unit_id = rtu_response:byte(1)
    if response_unit_id ~= unit_id then
        print(string.format("❌ Неверный Unit ID в ответе: ожидали=%d, получили=%d", unit_id, response_unit_id))
        local error_pdu = modbus.utils.errorresponse(pdu:byte(1), 0x04)
        local mbap = modbus.utils.buildmbap(trans_id, protocol_id, #error_pdu + 1, unit_id)
        local error_response = mbap .. error_pdu
        print(string.format("🚫 Отправка Unit ID ошибки: %s", tohex(error_response)))
        net.tcpserv.send(error_response, 0, true)
        uart_mutex:unlock()
        return
    end
    
    -- 7. Извлекаем PDU из RTU ответа (без Unit ID и CRC)
    local response_pdu = rtu_response:sub(2, -3)
    
    -- 8. Формируем TCP ответ
    local mbap = modbus.utils.buildmbap(trans_id, protocol_id, #response_pdu + 1, unit_id)
    local tcp_response = mbap .. response_pdu
    --print(string.format("📤 Отправка TCP ответа: %d байт", #tcp_response))
    
    -- Отправляем TCP ответ в блокирующем режиме с таймаутом 5000мс + flush
    net.tcpserv.send(tcp_response, 0, true)
    --print("✅ Транзакция завершена успешно")
    
    -- Разблокируем UART после завершения транзакции
    uart_mutex:unlock()
end

-- ========== Запуск сервера ==========
print("🚀 Запуск Modbus TCP/RTU Transparent Bridge")
print("📍 TCP порт:", TCP_PORT)
print("📍 UART:", "UART" .. UART_PORT, "Baudrate=" .. BAUDRATE)
print("📍 Поддерживаются ВСЕ Modbus функции")
print("🔒 Мьютекс UART включен для исключения одновременных запросов")

net.tcpserv.start(TCP_PORT, transparent_bridge, 60, false)

print("✅ Прозрачный мост запущен и готов к работе!")
print("🔗 Подключите Modbus TCP клиент к порту", TCP_PORT)
Восстановление устройства
Порядок действий, если вы "закирпичили" устройство и не можете получить к нему доступ.
  1. Выключить устройство.
  2. Замкнуть контакты HARD RESET на плате устройства.
  3. Включить устройство держа замкнутым разъём HARD RESET.
  4. Подождать несколько секунд (после контакты можно размокнуть).
  5. Устройство создаст WiFi-точку доступа вида OD-[серийный номер].
  6. Сеть без пароля
  7. Устройство запустит Web-сервер и telnet-сервер, чтобы вы могли исправить работать с консолью устройства: изменить настройки/программу и восстановить доступ к устройству.
  8. IP устройства: 192.168.4.1
Дополнительные материалы
1
Консоль
Маленькая шпаргалка "как работать с консолью".
2
Библиотеки и RTOS
Большая документация на нашем отдельном сайте.
3
Справочник по Lua
Русскоязычная онлайн версия с поиском и перекрестными ссылками.
4
Как программировать ODNFC на Lua
Большое количество примеров кода на другое наше устройство, большую часть которых можно использовать и тут.