diff --git a/plugins/dmxusb/src/CMakeLists.txt b/plugins/dmxusb/src/CMakeLists.txt
index ae2f47d372..2336d2c278 100644
--- a/plugins/dmxusb/src/CMakeLists.txt
+++ b/plugins/dmxusb/src/CMakeLists.txt
@@ -114,6 +114,7 @@ target_sources(${module_name} PRIVATE
enttecdmxusbpro.cpp enttecdmxusbpro.h
stageprofi.cpp stageprofi.h
vinceusbdmx512.cpp vinceusbdmx512.h
+ usbdmxlegacy.cpp usbdmxlegacy.h
)
target_include_directories(${module_name} PRIVATE
diff --git a/plugins/dmxusb/src/DMX_USB_ca_ES.ts b/plugins/dmxusb/src/DMX_USB_ca_ES.ts
index 4628169dc3..35bd8dd26d 100644
--- a/plugins/dmxusb/src/DMX_USB_ca_ES.ts
+++ b/plugins/dmxusb/src/DMX_USB_ca_ES.ts
@@ -200,6 +200,7 @@
+
Protocol
Protocol
@@ -208,6 +209,7 @@
+
Output
Sortida
@@ -225,6 +227,7 @@
+
Serial number
Nombre de Sèrie
diff --git a/plugins/dmxusb/src/DMX_USB_cz_CZ.ts b/plugins/dmxusb/src/DMX_USB_cz_CZ.ts
index 50cd1d5b91..455e22a338 100644
--- a/plugins/dmxusb/src/DMX_USB_cz_CZ.ts
+++ b/plugins/dmxusb/src/DMX_USB_cz_CZ.ts
@@ -200,6 +200,7 @@
+
Protocol
Protokol
@@ -208,6 +209,7 @@
+
Output
Výstup
@@ -225,6 +227,7 @@
+
Serial number
Sériové číslo
diff --git a/plugins/dmxusb/src/DMX_USB_de_DE.ts b/plugins/dmxusb/src/DMX_USB_de_DE.ts
index d29b30a383..7227b3f59d 100644
--- a/plugins/dmxusb/src/DMX_USB_de_DE.ts
+++ b/plugins/dmxusb/src/DMX_USB_de_DE.ts
@@ -200,6 +200,7 @@
+
Protocol
Protokoll
@@ -208,6 +209,7 @@
+
Serial number
Seriennummer
@@ -216,6 +218,7 @@
+
Output
Ausgabe
diff --git a/plugins/dmxusb/src/DMX_USB_es_ES.ts b/plugins/dmxusb/src/DMX_USB_es_ES.ts
index 83e4953871..a15549ef68 100644
--- a/plugins/dmxusb/src/DMX_USB_es_ES.ts
+++ b/plugins/dmxusb/src/DMX_USB_es_ES.ts
@@ -200,6 +200,7 @@
+
Protocol
Protocolo
@@ -208,6 +209,7 @@
+
Serial number
Número de Serie
@@ -216,6 +218,7 @@
+
Output
Salida
diff --git a/plugins/dmxusb/src/DMX_USB_fi_FI.ts b/plugins/dmxusb/src/DMX_USB_fi_FI.ts
index 0d3dbeeceb..9b720ac535 100644
--- a/plugins/dmxusb/src/DMX_USB_fi_FI.ts
+++ b/plugins/dmxusb/src/DMX_USB_fi_FI.ts
@@ -200,6 +200,7 @@
+
Protocol
@@ -208,6 +209,7 @@
+
Serial number
@@ -216,6 +218,7 @@
+
Output
diff --git a/plugins/dmxusb/src/DMX_USB_fr_FR.ts b/plugins/dmxusb/src/DMX_USB_fr_FR.ts
index 25a6708c31..66c2404a9a 100644
--- a/plugins/dmxusb/src/DMX_USB_fr_FR.ts
+++ b/plugins/dmxusb/src/DMX_USB_fr_FR.ts
@@ -201,6 +201,7 @@ NOTE : L'interface VCP FTDI n'est pas supportée par ce plugin.
+
Protocol
Protocole
@@ -209,6 +210,7 @@ NOTE : L'interface VCP FTDI n'est pas supportée par ce plugin.
+
Serial number
N° de série
@@ -217,6 +219,7 @@ NOTE : L'interface VCP FTDI n'est pas supportée par ce plugin.
+
Output
Sortie
diff --git a/plugins/dmxusb/src/DMX_USB_it_IT.ts b/plugins/dmxusb/src/DMX_USB_it_IT.ts
index a670013f28..37b5785656 100644
--- a/plugins/dmxusb/src/DMX_USB_it_IT.ts
+++ b/plugins/dmxusb/src/DMX_USB_it_IT.ts
@@ -200,6 +200,7 @@
+
Protocol
Protocollo
@@ -208,6 +209,7 @@
+
Serial number
Numero di serie
@@ -216,6 +218,7 @@
+
Output
Uscita
diff --git a/plugins/dmxusb/src/DMX_USB_ja_JP.ts b/plugins/dmxusb/src/DMX_USB_ja_JP.ts
index b499750db7..d284c7275f 100644
--- a/plugins/dmxusb/src/DMX_USB_ja_JP.ts
+++ b/plugins/dmxusb/src/DMX_USB_ja_JP.ts
@@ -200,6 +200,7 @@
+
Protocol
Protocol
@@ -208,6 +209,7 @@
+
Output
出力
@@ -225,6 +227,7 @@
+
Serial number
シリアルナンバー
diff --git a/plugins/dmxusb/src/DMX_USB_nl_NL.ts b/plugins/dmxusb/src/DMX_USB_nl_NL.ts
index 655918252e..a90b0da089 100644
--- a/plugins/dmxusb/src/DMX_USB_nl_NL.ts
+++ b/plugins/dmxusb/src/DMX_USB_nl_NL.ts
@@ -200,6 +200,7 @@
+
Protocol
Protocol
@@ -208,6 +209,7 @@
+
Output
Output
@@ -225,6 +227,7 @@
+
Serial number
Serienummer
diff --git a/plugins/dmxusb/src/DMX_USB_pt_BR.ts b/plugins/dmxusb/src/DMX_USB_pt_BR.ts
index f7b885fad9..a0c2072422 100644
--- a/plugins/dmxusb/src/DMX_USB_pt_BR.ts
+++ b/plugins/dmxusb/src/DMX_USB_pt_BR.ts
@@ -200,6 +200,7 @@
+
Protocol
Protocolo
@@ -208,6 +209,7 @@
+
Serial number
Número de série
@@ -216,6 +218,7 @@
+
Output
Saída
diff --git a/plugins/dmxusb/src/dmxusb.cpp b/plugins/dmxusb/src/dmxusb.cpp
index 10c867e585..646879b652 100644
--- a/plugins/dmxusb/src/dmxusb.cpp
+++ b/plugins/dmxusb/src/dmxusb.cpp
@@ -152,7 +152,7 @@ QString DMXUSB::pluginInfo()
str += tr("This plugin provides DMX output support for");
str += QString(" DMXKing ultraDMX range, Enttec DMX USB Pro, "
"Enttec Open DMX USB, FTDI USB COM485 Plus1, "
- "Vince USB-DMX512 ");
+ "Vince USB-DMX512, usbdmx.com (legacy) ");
str += tr("and compatible devices.");
str += QString("
");
diff --git a/plugins/dmxusb/src/dmxusbconfig.cpp b/plugins/dmxusb/src/dmxusbconfig.cpp
index 8452ff6d48..2ef82ef081 100644
--- a/plugins/dmxusb/src/dmxusbconfig.cpp
+++ b/plugins/dmxusb/src/dmxusbconfig.cpp
@@ -151,6 +151,7 @@ QComboBox *DMXUSBConfig::createTypeCombo(DMXUSBWidget *widget)
combo->addItem(QString("DMX4ALL"), DMXUSBWidget::DMX4ALL);
combo->addItem(QString("Vince TX"), DMXUSBWidget::VinceTX);
combo->addItem(QString("Eurolite"), DMXUSBWidget::Eurolite);
+ combo->addItem(QString("usbdmx.com (legacy)"), DMXUSBWidget::USBDMXLegacy);
int index = combo->findData(widget->type());
combo->setCurrentIndex(index);
diff --git a/plugins/dmxusb/src/dmxusbwidget.cpp b/plugins/dmxusb/src/dmxusbwidget.cpp
index 85adb69c52..5ffcbdc63d 100644
--- a/plugins/dmxusb/src/dmxusbwidget.cpp
+++ b/plugins/dmxusb/src/dmxusbwidget.cpp
@@ -33,6 +33,7 @@
#endif
#include "stageprofi.h"
#include "vinceusbdmx512.h"
+#include "usbdmxlegacy.h"
#if defined(WIN32) || defined(Q_OS_WIN)
#include
@@ -168,6 +169,9 @@ QList DMXUSBWidget::widgets()
widgetList << new EuroliteUSBDMXPro(iface, output_id++);
break;
#endif
+ case DMXUSBWidget::USBDMXLegacy:
+ widgetList << new UsbdmxLegacy(iface, output_id++);
+ break;
default:
case DMXUSBWidget::ProRXTX:
widgetList << new EnttecDMXUSBPro(iface, output_id++, input_id++);
diff --git a/plugins/dmxusb/src/dmxusbwidget.h b/plugins/dmxusb/src/dmxusbwidget.h
index 3a95c7dbae..df4420d1ba 100644
--- a/plugins/dmxusb/src/dmxusbwidget.h
+++ b/plugins/dmxusb/src/dmxusbwidget.h
@@ -63,7 +63,8 @@ class DMXUSBWidget
UltraPro, //! DMXKing Ultra Pro widget using 2 TX and 1RX ports
DMX4ALL, //! DMX4ALL widget (only TX)
VinceTX, //! Vince USB-DMX512 widget using the TX side of the dongle
- Eurolite //! Eurolite USB DMX512 Pro widget
+ Eurolite, //! Eurolite USB DMX512 Pro widget
+ USBDMXLegacy //! usbdmx.com legacy interface
};
/** The possible features of a line */
diff --git a/plugins/dmxusb/src/enttecdmxusbpro.h b/plugins/dmxusb/src/enttecdmxusbpro.h
index b4fee3b7c7..9a113fa42a 100644
--- a/plugins/dmxusb/src/enttecdmxusbpro.h
+++ b/plugins/dmxusb/src/enttecdmxusbpro.h
@@ -47,7 +47,7 @@ class EnttecDMXUSBPro : public QThread, public DMXUSBWidget
virtual ~EnttecDMXUSBPro();
/** @reimp */
- Type type() const;
+ DMXUSBWidget::Type type() const;
// DMXking port flags
enum PortType
diff --git a/plugins/dmxusb/src/src.pro b/plugins/dmxusb/src/src.pro
index aae7b4f956..b37703785d 100644
--- a/plugins/dmxusb/src/src.pro
+++ b/plugins/dmxusb/src/src.pro
@@ -112,6 +112,7 @@ HEADERS += dmxusb.h \
dmxusbopenrx.h \
stageprofi.h \
vinceusbdmx512.h \
+ usbdmxlegacy.h \
dmxinterface.h
unix|macx: HEADERS += nanodmx.h euroliteusbdmxpro.h
@@ -127,6 +128,7 @@ SOURCES += dmxinterface.cpp \
enttecdmxusbopen.cpp \
dmxusbopenrx.cpp \
stageprofi.cpp \
+ usbdmxlegacy.cpp \
vinceusbdmx512.cpp
INCLUDEPATH += ../../midi/src/common
diff --git a/plugins/dmxusb/src/usbdmxlegacy.cpp b/plugins/dmxusb/src/usbdmxlegacy.cpp
new file mode 100644
index 0000000000..25c3809e3e
--- /dev/null
+++ b/plugins/dmxusb/src/usbdmxlegacy.cpp
@@ -0,0 +1,182 @@
+/*
+ Q Light Controller Plus
+ usbdmxlegacy.cpp
+
+ Adds support for the "USB/DMX Interface" (usbdmx.com) custom protocol.
+
+ Protocol reference (V1.4):
+ - TX ON : 0x44
+ - TX OFF : 0x46
+ - SET VALUE : 0x48 | 0x49 (high address bit), ,
+ - SET LAST TX : 0x4E | 0x4F (high address bit), (response 0xCE)
+ - Start code defaults to 0; we don't change it here.
+
+ Addressing in the spec is 0-based (0x000 -> channel 1). In QLC+ data
+ buffer the first slot is channel 1 at index 0, so we can pass the index
+ as-is.
+*/
+
+#include "usbdmxlegacy.h"
+
+#define USBDMXLEGACY_DEFAULT_FREQUENCY 40
+#define USBDMXLEGACY_CMD_TX_ON 0x44
+#define USBDMXLEGACY_CMD_TX_OFF 0x46
+#define USBDMXLEGACY_CMD_SET_VALUE 0x48
+#define USBDMXLEGACY_CMD_SET_VALUE_HIGH_ADDRESS_BIT 0x49
+#define USBDMXLEGACY_CMD_SET_LAST_TX 0x4E
+#define USBDMXLEGACY_CMD_SET_LAST_TX_HIGH_ADDRESS_BIT 0x4F
+
+
+UsbdmxLegacy::UsbdmxLegacy(DMXInterface *iface, quint32 outputLine)
+ : DMXUSBWidget(iface, outputLine, USBDMXLEGACY_DEFAULT_FREQUENCY)
+{
+ // configure one output port
+ QList ports;
+ ports << (DMXUSBWidget::DMX | DMXUSBWidget::Output);
+ setPortsMapping(ports);
+}
+
+UsbdmxLegacy::~UsbdmxLegacy()
+{
+ close();
+}
+
+DMXUSBWidget::Type UsbdmxLegacy::type() const
+{
+ return DMXUSBWidget::USBDMXLegacy;
+}
+
+/*************************************************************************
+ * Open & Close
+ *************************************************************************/
+
+bool UsbdmxLegacy::open(quint32 line, bool input)
+{
+ Q_UNUSED(line)
+ Q_UNUSED(input)
+
+ if (DMXUSBWidget::open() == false)
+ return false;
+
+ // Ensure TX is off, set last channel, then enable TX.
+ cmdTxOff();
+ cmdSetLastChannel(DMX_CHANNELS - 1);
+ return cmdTxOn();
+}
+
+bool UsbdmxLegacy::close(quint32 line, bool input)
+{
+ Q_UNUSED(line)
+ Q_UNUSED(input)
+
+ cmdTxOff();
+ return DMXUSBWidget::close();
+}
+
+/*************************************************************************
+ * Outputs
+ *************************************************************************/
+
+bool UsbdmxLegacy::writeUniverse(quint32 universe, quint32 output, const QByteArray& data, bool dataChanged)
+{
+ Q_UNUSED(universe)
+ Q_UNUSED(output)
+
+ if (!isOpen())
+ return false;
+
+ // Initialize local cache once.
+ if (m_portsInfo[0].m_universeData.size() == 0)
+ {
+ m_portsInfo[0].m_universeData = QByteArray(DMX_CHANNELS, 0);
+
+ // Make sure the device does not waste time sending beyond our bounds
+ cmdSetLastChannel(DMX_CHANNELS - 1);
+ cmdTxOn();
+ }
+
+ if (!dataChanged)
+ return true;
+
+ bool result = true;
+ // Update cache and send only diffs as SET VALUE commands, as per spec.
+ for (int i = 0; i < qMin(DMX_CHANNELS, data.size()); i++)
+ {
+ const uchar newVal = static_cast(data.at(i));
+ uchar &oldVal = reinterpret_cast(m_portsInfo[0].m_universeData[i]);
+
+ if (newVal == oldVal)
+ continue;
+
+ if (!cmdSetChannelValue(i, newVal))
+ {
+ result = false;
+ continue;
+ }
+
+ oldVal = newVal;
+ }
+
+ return result;
+}
+
+/*************************************************************************
+ * Info
+ *************************************************************************/
+
+QString UsbdmxLegacy::additionalInfo() const
+{
+ QString info;
+ info += QString("");
+ info += QString("%1: usbdmx.com (legacy) (%3)").arg(QObject::tr("Protocol"))
+ .arg(QObject::tr("Output"));
+ info += QString("
");
+ info += QString("%1: %2").arg(QObject::tr("Serial number"))
+ .arg(serial());
+ info += QString("
");
+ return info;
+}
+
+/*************************************************************************
+ * Command helpers
+ *************************************************************************/
+
+bool UsbdmxLegacy::cmdTxOn()
+{
+ QByteArray msg;
+ msg.append(char(USBDMXLEGACY_CMD_TX_ON));
+ return iface()->write(msg);
+}
+
+bool UsbdmxLegacy::cmdTxOff()
+{
+ QByteArray msg;
+ msg.append(char(USBDMXLEGACY_CMD_TX_OFF));
+ return iface()->write(msg);
+}
+
+bool UsbdmxLegacy::cmdSetLastChannel(int lastIndex)
+{
+ if (lastIndex < 0)
+ lastIndex = 0;
+
+ if (lastIndex > 511)
+ lastIndex = 511;
+
+ QByteArray msg;
+ msg.append(char(hiBit(lastIndex) ? USBDMXLEGACY_CMD_SET_LAST_TX_HIGH_ADDRESS_BIT : USBDMXLEGACY_CMD_SET_LAST_TX));
+ msg.append(char(lo(lastIndex)));
+ return iface()->write(msg);
+}
+
+bool UsbdmxLegacy::cmdSetChannelValue(int index, uchar value)
+{
+ if (index < 0 || index > 511)
+ return false;
+
+ QByteArray msg;
+ msg.append(char(hiBit(index) ? USBDMXLEGACY_CMD_SET_VALUE_HIGH_ADDRESS_BIT : USBDMXLEGACY_CMD_SET_VALUE));
+ msg.append(char(lo(index)));
+ msg.append(char(value));
+ return iface()->write(msg);
+}
diff --git a/plugins/dmxusb/src/usbdmxlegacy.h b/plugins/dmxusb/src/usbdmxlegacy.h
new file mode 100644
index 0000000000..cdaaec3bca
--- /dev/null
+++ b/plugins/dmxusb/src/usbdmxlegacy.h
@@ -0,0 +1,73 @@
+/*
+ Q Light Controller Plus
+ usbdmxlegacy.h
+
+ Adds support for the "USB/DMX Interface" (usbdmx.com) custom protocol
+ as documented in "USB/DMX Interface - Command Specification (V 1.4)".
+
+ Copyright (C) 2025
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0.txt
+*/
+
+#ifndef USBDMXLEGACY_H
+#define USBDMXLEGACY_H
+
+#include "dmxusbwidget.h"
+
+/**
+ * Implements the usbdmx.com interface protocol (Ben Suffolk, v1.4 PDF).
+ * The device is FTDI-based and maintains its own DMX timing once TX is ON.
+ * We only push per-channel updates and set the "last TX channel" bound.
+ */
+class UsbdmxLegacy : public QThread, public DMXUSBWidget
+{
+ Q_OBJECT
+
+ /************************************************************************
+ * Initialization
+ ************************************************************************/
+public:
+ UsbdmxLegacy(DMXInterface *iface, quint32 outputLine);
+ virtual ~UsbdmxLegacy();
+
+ /** @reimp */
+ DMXUSBWidget::Type type() const override;
+
+ /*************************************************************************
+ * Open & Close
+ *************************************************************************/
+ /** @reimp */
+ bool open(quint32 line = 0, bool input = false) override;
+
+ /** @reimp */
+ bool close(quint32 line = 0, bool input = false) override;
+
+ /*************************************************************************
+ * Outputs
+ *************************************************************************/
+ /** @reimp */
+ bool writeUniverse(quint32 universe, quint32 output, const QByteArray& data, bool dataChanged) override;
+
+ /*************************************************************************
+ * Info
+ *************************************************************************/
+ /** @reimp */
+ QString additionalInfo() const override;
+
+private:
+ // usbdmx.com command helpers
+ bool cmdTxOn();
+ bool cmdTxOff();
+ bool cmdSetLastChannel(int lastIndex); // argument in [0..511]
+ bool cmdSetChannelValue(int index, uchar value); // index in [0..511]
+
+ inline uchar lo(int index) const { return static_cast(index & 0xFF); }
+ inline bool hiBit(int index) const { return (index & 0x100) != 0; }
+};
+
+#endif // USBDMXLEGACY_H