| 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 |