GCC Code Coverage Report


Directory: ./
File: channeloutput/UDMX.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 109 149 73.2%
Functions: 15 16 93.8%
Branches: 87 226 38.5%

Line Branch Exec Source
1 /*
2 * This file is part of the Falcon Player (FPP) and is Copyright (C)
3 * 2013-2022 by the Falcon Player Developers.
4 *
5 * The Falcon Player (FPP) is free software, and is covered under
6 * multiple Open Source licenses. Please see the included 'LICENSES'
7 * file for descriptions of what files are covered by each license.
8 *
9 * This source file is covered under the GPL v2 as described in the
10 * included LICENSE.GPL file.
11 */
12
13 #include "fpp-pch.h"
14 #include "fpp-json.h"
15
16 #include <fcntl.h>
17 #include <termios.h>
18
19 #include <cstdint>
20 #include <string>
21
22 #include "../log.h"
23 #include "../common_mini.h"
24
25 #include "UDMX.h"
26 #include "ChannelOutputSetup.h"
27
28 #include <libusb-1.0/libusb.h>
29
30 #define DMX_MAX_CHANNELS 512
31
32 #define UDMX_SHARED_VENDOR 0x16C0 /* VOTI */
33 #define UDMX_SHARED_PRODUCT 0x05DC /* Obdev's free shared PID */
34
35 #define UDMX_AVLDIY_D512_CLONE_VENDOR 0x03EB /* Atmel Corp. (Clone VID)*/
36 #define UDMX_AVLDIY_D512_CLONE_PRODUCT 0x8888 /* Clone PID */
37 #define UDMX_SET_CHANNEL_RANGE 0x0002 /* Command to set n channel values */
38
39 #include "Plugin.h"
40 class UDMXPlugin : public FPPPlugins::Plugin, public FPPPlugins::ChannelOutputPlugin {
41 public:
42 5 UDMXPlugin() :
43
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
15 FPPPlugins::Plugin("UDMX") {
44 5 }
45 5 virtual ChannelOutput* createChannelOutput(unsigned int startChannel, unsigned int channelCount) override {
46
2/6
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 5 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
10 return new UDMXOutput(startChannel, channelCount);
47 }
48 };
49
50 extern "C" {
51 5 FPPPlugins::Plugin* createPlugin() {
52
2/6
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 5 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
10 return new UDMXPlugin();
53 }
54 }
55
56 /////////////////////////////////////////////////////////////////////////////
57
58 5 UDMXOutput::UDMXOutput(unsigned int startChannel, unsigned int channelCount) :
59 5 ThreadedChannelOutput(startChannel, channelCount) {
60
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 LogDebug(VB_CHANNELOUT, "UDMXOutput::UDMXOutput(%u, %u)\n",
61 startChannel, channelCount);
62
63 5 m_useDoubleBuffer = 1;
64 //DMX protocol requires data to be sent at least every 250ms
65 5 m_maxWait = 250;
66 5 m_dataOffset = 1;
67 5 memset(m_outputData, 0, sizeof(m_outputData));
68
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 4 times.
5 if (m_channelCount > DMX_MAX_CHANNELS) {
69
3/6
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
1 AddOutputConfigWarning(28, "UDMX: Invalid Config. Channel count of " + std::to_string(m_channelCount) + " exceeds the DMX universe size of 512");
70 1 m_channelCount = DMX_MAX_CHANNELS;
71 }
72
73
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 5 times.
5 if (libusb_init(&m_ctx) != 0) {
74 ✗ LogErr(VB_CHANNELOUT, "Error Starting LibUSB\n");
75 ✗ AddOutputConfigWarning(28, "USBDMX: Error Starting LibUSB");
76 ✗ return;
77 }
78 ✗ }
79
80 20 UDMXOutput::~UDMXOutput() {
81 10 LogDebug(VB_CHANNELOUT, "UDMXOutput::~UDMXOutput()\n");
82
83
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
10 if (nullptr != m_device) {
84 10 libusb_unref_device(m_device);
85 10 m_device = nullptr;
86 }
87
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
10 if (nullptr != m_ctx) {
88 10 libusb_exit(m_ctx);
89 10 m_ctx = nullptr;
90 }
91 20 }
92
93 6 std::string UDMXOutput::GetDevicePath(struct libusb_device* dev) const {
94 // Matches the Linux sysfs device name, e.g. "1-1.2" (bus-port.port).
95
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 std::string path = std::to_string(libusb_get_bus_number(dev));
96 uint8_t ports[8];
97
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 int n = libusb_get_port_numbers(dev, ports, sizeof(ports));
98
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 6 times.
12 for (int i = 0; i < n; i++) {
99
2/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
6 path += (i == 0) ? "-" : ".";
100
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 path += std::to_string(ports[i]);
101 }
102 6 return path;
103 ✗ }
104
105 6 std::string UDMXOutput::GetSysfsSerial(const std::string& path) const {
106 // The kernel reads and caches the serial when the device enumerates, so it can
107 // be had from sysfs without putting any traffic on the wire. Empty on non-Linux
108 // or if the device has no serial, in which case the caller falls back to libusb.
109
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (path.empty()) {
110 ✗ return "";
111 }
112
3/6
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 6 times.
✗ Branch 5 not taken.
6 std::string serial = GetFileContents("/sys/bus/usb/devices/" + path + "/serial");
113
6/8
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✓ Branch 3 taken 6 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 6 times.
✓ Branch 6 taken 6 times.
✓ Branch 7 taken 6 times.
12 while (!serial.empty() && (serial.back() == '\n' || serial.back() == '\r')) {
114 6 serial.pop_back();
115 }
116 6 return serial;
117 6 }
118
119 ✗ std::string UDMXOutput::GetDeviceSerial(struct libusb_device* dev,
120 const struct libusb_device_descriptor* desc) const {
121 ✗ if (desc->iSerialNumber == 0) {
122 ✗ return "";
123 }
124 // Reading the serial string descriptor requires briefly opening the device.
125 // uDMX hardware implements USB in firmware and can intermittently stop
126 // answering control transfers, so retry rather than treating a single
127 // timeout as "this is not the device we are looking for".
128 ✗ for (int attempt = 0; attempt < 3; attempt++) {
129 ✗ libusb_device_handle* h = nullptr;
130 ✗ if (libusb_open(dev, &h) != 0 || nullptr == h) {
131 ✗ return "";
132 }
133 ✗ unsigned char buf[256] = { 0 };
134 ✗ int r = libusb_get_string_descriptor_ascii(h, desc->iSerialNumber, buf, sizeof(buf) - 1);
135 ✗ libusb_close(h);
136 ✗ if (r > 0) {
137 ✗ return std::string(reinterpret_cast<char*>(buf));
138 }
139 ✗ LogWarn(VB_CHANNELOUT, "Could not read UDMX serial descriptor (%s), attempt %d of 3\n",
140 libusb_error_name(r), attempt + 1);
141 }
142 ✗ return "";
143 }
144
145 5 void UDMXOutput::FindDevice(const std::string& wanted) {
146
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 5 times.
5 if (nullptr == m_ctx || nullptr != m_device) {
147 ✗ return;
148 }
149
150 5 libusb_device** devices = nullptr;
151
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 ssize_t count = libusb_get_device_list(m_ctx, &devices);
152 5 libusb_device* firstMatch = nullptr;
153
154
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 2 times.
10 for (ssize_t i = 0; i < count; i++) {
155 8 libusb_device* dev = devices[i];
156
157 libusb_device_descriptor desc;
158
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 8 times.
8 if (libusb_get_device_descriptor(dev, &desc) < 0) {
159 2 continue;
160 }
161
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 8 times.
8 if (!isValidDevice(&desc)) {
162 ✗ continue;
163 }
164
165
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 3 times.
8 if (nullptr == firstMatch) {
166 5 firstMatch = dev;
167 }
168
169
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 6 times.
8 if (wanted.empty()) {
170 // No specific device requested (legacy configs) - first match wins.
171 2 continue;
172 }
173
174
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 std::string path = GetDevicePath(dev);
175 // Prefer the kernel's cached serial; only fall back to asking the device
176 // directly if sysfs cannot answer.
177
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 std::string serial = GetSysfsSerial(path);
178
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (serial.empty()) {
179 ✗ serial = GetDeviceSerial(dev, &desc);
180 }
181
6/6
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 3 times.
✓ Branch 4 taken 3 times.
✓ Branch 5 taken 3 times.
6 if (wanted == serial || wanted == path) {
182
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 LogDebug(VB_CHANNELOUT, "UDMX Device Found (serial '%s', path '%s')\n",
183 serial.c_str(), path.c_str());
184 3 m_device = dev;
185 3 break;
186 }
187
4/4
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 3 times.
✓ Branch 3 taken 3 times.
9 }
188
189
5/6
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 3 times.
5 if (nullptr == m_device && wanted.empty()) {
190 // Backward compatible: auto-select the first uDMX found.
191 2 m_device = firstMatch;
192 }
193
194
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 if (nullptr != m_device) {
195
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 libusb_ref_device(m_device);
196 }
197
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 libusb_free_device_list(devices, 1);
198 }
199 10 void UDMXOutput::GetRequiredChannelRanges(const std::function<void(int, int)>& addRange) {
200 10 addRange(m_startChannel, m_startChannel + m_channelCount - 1);
201 10 }
202
203 5 int UDMXOutput::Init(Json::Value config) {
204
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 LogDebug(VB_CHANNELOUT, "UDMXOutput::Init()\n");
205
206
3/6
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 5 times.
✗ Branch 5 not taken.
5 std::string wantedDevice = config.get("device", "").asString();
207
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 FindDevice(wantedDevice);
208
209
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
5 if (nullptr == m_device) {
210 ✗ if (wantedDevice.empty()) {
211 ✗ LogErr(VB_CHANNELOUT, "No UDMX Device Found\n");
212 ✗ AddOutputConfigWarning(28, "USBDMX: No UDMX Device Found");
213 } else {
214 ✗ LogErr(VB_CHANNELOUT, "Configured UDMX Device '%s' Not Found\n", wantedDevice.c_str());
215 ✗ AddOutputConfigWarning(28, "USBDMX: Configured uDMX Device '" + wantedDevice + "' Not Found");
216 }
217 ✗ return 0;
218 }
219
220
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 if (nullptr == m_handle) {
221
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 int ret = libusb_open(m_device, &m_handle);
222
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
5 if (ret < 0) {
223 ✗ LogErr(VB_CHANNELOUT, "Error Opening USB Device (bus %d, device %d)",
224 libusb_get_bus_number(m_device), libusb_get_device_address(m_device));
225
226 ✗ AddOutputConfigWarning(28, "USBDMX: Error Opening USB Device");
227 ✗ return 0;
228 }
229 }
230
231
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
5 if (nullptr == m_handle) {
232 ✗ LogErr(VB_CHANNELOUT, "USBDMX: Error Opening uDMX Device\n");
233 ✗ AddOutputConfigWarning(28, "USBDMX: Error Opening uDMX Device");
234 ✗ return 0;
235 }
236
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 return ThreadedChannelOutput::Init(config);
237 5 }
238
239 5 int UDMXOutput::Close(void) {
240 5 LogDebug(VB_CHANNELOUT, "UDMXOutput::Close()\n");
241
242
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 if (nullptr != m_device && nullptr != m_handle) {
243 5 libusb_close(m_handle);
244 }
245
246 5 m_handle = nullptr;
247
248 5 return ThreadedChannelOutput::Close();
249 }
250
251 35 int UDMXOutput::RawSendData(unsigned char* channelData) {
252 35 LogExcess(VB_CHANNELOUT, "UDMXOutput::RawSendData(%p)\n", channelData);
253 35 memcpy(m_outputData + m_dataOffset, channelData, m_channelCount);
254 35 WaitTimedOut();
255 35 return m_channelCount;
256 }
257 35 void UDMXOutput::WaitTimedOut() {
258
2/4
✓ Branch 0 taken 35 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 35 times.
✗ Branch 3 not taken.
35 if (nullptr != m_device && nullptr != m_handle) {
259 35 libusb_control_transfer(m_handle,
260 (uint8_t)LIBUSB_REQUEST_TYPE_VENDOR |
261 (uint8_t)LIBUSB_RECIPIENT_INTERFACE |
262 (uint8_t)LIBUSB_ENDPOINT_OUT,
263 UDMX_SET_CHANNEL_RANGE, /* Command */
264 35 m_channelCount, /* Number of channels to set */
265 0, /* Starting index */
266 35 (unsigned char*)(m_outputData + m_dataOffset), /* Values to set */
267 35 m_channelCount, /* Size of values */
268 500); /* Timeout 500ms */
269 }
270 35 }
271
272 10 void UDMXOutput::DumpConfig(void) {
273 10 LogDebug(VB_CHANNELOUT, "UDMXOutput::DumpConfig()\n");
274 10 ThreadedChannelOutput::DumpConfig();
275 10 }
276
277 8 bool UDMXOutput::isValidDevice(const struct libusb_device_descriptor* desc) const {
278
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 if (nullptr == desc) {
279 ✗ return false;
280 }
281
282
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 if (desc->idVendor != UDMX_SHARED_VENDOR &&
283 ✗ desc->idVendor != UDMX_AVLDIY_D512_CLONE_VENDOR) {
284 ✗ return false;
285 }
286
287
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 if (desc->idProduct != UDMX_SHARED_PRODUCT &&
288 ✗ desc->idProduct != UDMX_AVLDIY_D512_CLONE_PRODUCT) {
289 ✗ return false;
290 }
291
292 8 return true;
293 }
294