Прямое управление DMX-512 через встроенный RS-485 на Wiren Board

From Wiren Board
This is the approved revision of this page, as well as being the most recent.

Описание

Решение создано членом сообщества и не тестировалось компанией Wiren Board. Используя его, вы действуете на свой страх и риск.


Физический уровень протоколов DMX512 и RS-485 идентичен, однако стандартные драйверы Wiren Board (например, wb-mqtt-serial) не могут генерировать специфический сигнал BREAK (логический ноль > 88 мкс), необходимый для DMX.

Данное решение позволяет программно эмулировать сигнал BREAK на встроенном порту RS-485, а также автоматически создает виртуальное устройство в веб-интерфейсе контроллера. Вы можете на лету задавать количество используемых DMX-каналов, и скрипт сам создаст или удалит соответствующие ползунки (range) для управления яркостью.

Подготовка контроллера

  1. Освободите порт: В веб-интерфейсе Wiren Board перейдите в Настройки -> Настройки драйвера Serial-устройств и отключите порт, к которому подключена шина DMX (например, /dev/ttyRS485-2). Сохраните изменения.
  2. Установите зависимости: Подключитесь к контроллеру по SSH и установите библиотеки для работы с последовательным портом и MQTT-брокером:
apt-get update
apt-get install python3-serial python3-paho-mqtt

Python-скрипт (Мост MQTT ↔ DMX)

Создайте файл /root/dmx_mqtt.py и вставьте в него следующий код. Обязательно проверьте переменную PORT.

import serial
import time
import paho.mqtt.client as mqtt

# --- Настройки RS-485 и DMX ---
PORT = '/dev/ttyRS485-2'  # Освобожденный порт
DMX_BAUD = 250000
BREAK_BAUD = 38400
MAX_CHANNELS = 512

# --- Настройки MQTT ---
MQTT_BROKER = "127.0.0.1"
MQTT_PORT = 1883
DEVICE_ID = "dmx_python"
DEVICE_NAME = "Dynamic DMX"

dmx_data = bytearray([0] * MAX_CHANNELS)
current_count = 0

# Открываем порт
try:
    ser = serial.Serial(PORT, DMX_BAUD, stopbits=serial.STOPBITS_TWO)
except Exception as e:
    print(f"Ошибка открытия порта {PORT}: {e}")
    exit(1)

def send_dmx_frame(data):
    """Генерация BREAK и отправка кадра DMX"""
    ser.baudrate = BREAK_BAUD
    ser.write(b'\x00')
    ser.flush()
    ser.baudrate = DMX_BAUD
    packet = bytearray([0x00]) + data
    ser.write(packet)
    ser.flush()

def update_controls(client, new_count):
    """Динамическое создание/удаление MQTT-контролов"""
    global current_count
    safe_count = max(0, min(new_count, MAX_CHANNELS))
    
    # Добавление новых каналов
    if safe_count > current_count:
        for i in range(current_count + 1, safe_count + 1):
            prefix = f"/devices/{DEVICE_ID}/controls/Channel {i}"
            client.publish(f"{prefix}/meta/type", "range", retain=True)
            client.publish(f"{prefix}/meta/max", "255", retain=True)
            client.publish(f"{prefix}/meta/order", str(i), retain=True)
            client.publish(prefix, "0", retain=True)
            
    # Удаление лишних каналов (очистка retained-сообщений)
    elif safe_count < current_count:
        for i in range(safe_count + 1, current_count + 1):
            prefix = f"/devices/{DEVICE_ID}/controls/Channel {i}"
            client.publish(f"{prefix}/meta/type", "", retain=True)
            client.publish(f"{prefix}/meta/max", "", retain=True)
            client.publish(f"{prefix}/meta/order", "", retain=True)
            client.publish(prefix, "", retain=True)
            dmx_data[i - 1] = 0 # Гасим канал аппаратно
            
    current_count = safe_count

# --- MQTT Коллбэки ---
def on_connect(client, userdata, flags, rc):
    print("MQTT подключен. Инициализация устройства...")
    client.publish(f"/devices/{DEVICE_ID}/meta/name", DEVICE_NAME, retain=True)
    
    # Базовый контрол для задания количества каналов
    client.publish(f"/devices/{DEVICE_ID}/controls/Topic Count/meta/type", "value", retain=True)
    client.publish(f"/devices/{DEVICE_ID}/controls/Topic Count/meta/readonly", "0", retain=True)
    
    # Подписка на команды от Web UI
    client.subscribe(f"/devices/{DEVICE_ID}/controls/+/on")

def on_message(client, userdata, msg):
    """Обработка ввода пользователя"""
    topic = msg.topic
    payload = msg.payload.decode('utf-8')
    
    if topic == f"/devices/{DEVICE_ID}/controls/Topic Count/on":
        try:
            count = int(float(payload))
            update_controls(client, count)
            # Эхо-ответ для снятия красного статуса ошибки в UI
            client.publish(f"/devices/{DEVICE_ID}/controls/Topic Count", str(count), retain=True)
        except ValueError:
            pass

    elif topic.startswith(f"/devices/{DEVICE_ID}/controls/Channel ") and topic.endswith("/on"):
        try:
            control_name = topic.split('/')[-2]
            ch_num = int(control_name.split(' ')[1])
            val = int(float(payload))
            
            if 1 <= ch_num <= MAX_CHANNELS:
                dmx_data[ch_num - 1] = max(0, min(val, 255))
                
            client.publish(f"/devices/{DEVICE_ID}/controls/{control_name}", str(val), retain=True)
        except (ValueError, IndexError):
            pass

# --- Основной цикл ---
mqtt_client = mqtt.Client()
mqtt_client.on_connect = on_connect
mqtt_client.on_message = on_message
mqtt_client.connect(MQTT_BROKER, MQTT_PORT, 60)
mqtt_client.loop_start()

try:
    while True:
        send_dmx_frame(dmx_data)
        time.sleep(0.025) # Поддержание частоты ~40 Гц
except KeyboardInterrupt:
    print("\nОстановка...")
    send_dmx_frame(bytearray([0] * MAX_CHANNELS))
    mqtt_client.loop_stop()
    mqtt_client.disconnect()
    ser.close()

Представление в веб-интерфейсе контроллера

Устройство будет представлено в стандартном веб-интерфейсе контроллера Wiren Board на вкладке Устройства.

Карточка устройства

Как это работает (Архитектура)

  • Удержание шины: DMX-устройства требуют непрерывного потока данных. Бесконечный цикл while True с задержкой 0.025s обеспечивает частоту обновления около 40 кадров в секунду.
  • Генерация BREAK: Выполняется трюком со снижением baudrate порта до 38400 бит/с и отправкой нулевого байта.
  • Конвенция MQTT Wiren Board:
    • Скрипт публикует retain сообщения в топики /meta для отрисовки интерфейса (создание полей ввода и ползунков).
    • Когда пользователь двигает ползунок в веб-интерфейсе, брокер Mosquitto присылает новое значение в подтопик /on. Скрипт перехватывает его, обновляет массив dmx_data и отправляет подтверждение обратно в основной топик контрола, чтобы интерфейс понял, что команда принята.

Ограничения метода: Операционная система Linux на Wiren Board не является системой жесткого реального времени (RTOS). При высокой загрузке CPU (например, при активной записи истории в базу данных) программные тайминги генерации BREAK могут нестабильно колебаться. На некоторых чувствительных DMX-светильниках это может вызывать легкое мерцание. Для профессиональных инсталляций рекомендуется использовать аппаратные шлюзы (Modbus RTU → DMX512).