/* * Fadecandy driver for the Enttec DMX USB Pro. * * Copyright (c) 2013 Micah Elizabeth Scott * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in * the Software without restriction, including without limitation the rights to * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of * the Software, and to permit persons to whom the Software is furnished to do so, * subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include "enttecdmxdevice.h" #include <sstream> #include <iostream> EnttecDMXDevice::Transfer::Transfer(EnttecDMXDevice *device, void *buffer, int length) : transfer(libusb_alloc_transfer(0)), finished(false) { libusb_fill_bulk_transfer(transfer, device->mHandle, OUT_ENDPOINT, (uint8_t*) buffer, length, EnttecDMXDevice::completeTransfer, this, 2000); } EnttecDMXDevice::Transfer::~Transfer() { libusb_free_transfer(transfer); } EnttecDMXDevice::EnttecDMXDevice(libusb_device *device, bool verbose) : USBDevice(device, verbose), mFoundEnttecStrings(false), mConfigMap(0) { mSerial[0] = '\0'; // Initialize a minimal valid DMX packet memset(&mChannelBuffer, 0, sizeof mChannelBuffer); mChannelBuffer.start = START_OF_MESSAGE; mChannelBuffer.label = SEND_DMX_PACKET; mChannelBuffer.data[0] = START_CODE; setChannel(1, 0); } EnttecDMXDevice::~EnttecDMXDevice() { /* * If we have pending transfers, cancel them. * The Transfer objects themselves will be freed once libusb completes them. */ for (std::set<Transfer*>::iterator i = mPending.begin(), e = mPending.end(); i != e; ++i) { Transfer *fct = *i; libusb_cancel_transfer(fct->transfer); } } bool EnttecDMXDevice::probe(libusb_device *device) { /* * Prior to opening the device, all we can do is look for an FT245 device. * We'll take a closer look in probeAfterOpening(), once we can see the * string descriptors. */ libusb_device_descriptor dd; if (libusb_get_device_descriptor(device, &dd) < 0) { // Can't access descriptor? return false; } // FTDI FT245 return dd.idVendor == 0x0403 && dd.idProduct == 0x6001; } int EnttecDMXDevice::open() { libusb_device_descriptor dd; int r = libusb_get_device_descriptor(mDevice, &dd); if (r < 0) { return r; } r = libusb_open(mDevice, &mHandle); if (r < 0) { return r; } /* * Match the manufacturer and product strings! This is the least intrusive way to * determine that the attached device is in fact an Enttec DMX USB Pro, since it doesn't * have a unique vendor/product ID. */ if (dd.iManufacturer && dd.iProduct && dd.iSerialNumber) { char manufacturer[256]; char product[256]; r = libusb_get_string_descriptor_ascii(mHandle, dd.iManufacturer, (uint8_t*)manufacturer, sizeof manufacturer); if (r < 0) { return r; } r = libusb_get_string_descriptor_ascii(mHandle, dd.iProduct, (uint8_t*)product, sizeof product); if (r < 0) { return r; } mFoundEnttecStrings = !strcmp(manufacturer, "ENTTEC") && !strcmp(product, "DMX USB PRO"); } /* * Only go further if we have in fact found evidence that this is the right device. */ if (mFoundEnttecStrings) { // Only relevant on linux; try to detach the FTDI driver. libusb_detach_kernel_driver(mHandle, 0); r = libusb_claim_interface(mHandle, 0); if (r < 0) { return r; } r = libusb_get_string_descriptor_ascii(mHandle, dd.iSerialNumber, (uint8_t*)mSerial, sizeof mSerial); if (r < 0) { return r; } } return 0; } bool EnttecDMXDevice::probeAfterOpening() { // By default, any device is supported by the time we get to opening it. return mFoundEnttecStrings; } bool EnttecDMXDevice::matchConfiguration(const Value &config) { if (matchConfigurationWithTypeAndSerial(config, "enttec", mSerial)) { mConfigMap = findConfigMap(config); return true; } return false; } std::string EnttecDMXDevice::getName() { std::ostringstream s; s << "Enttec DMX USB Pro"; if (mSerial[0]) { s << " (Serial# " << mSerial << ")"; } return s.str(); } void EnttecDMXDevice::setChannel(unsigned n, uint8_t value) { if (n >= 1 && n <= 512) { unsigned len = std::max<unsigned>(mChannelBuffer.length, n + 1); mChannelBuffer.length = len; mChannelBuffer.data[n] = value; mChannelBuffer.data[len] = END_OF_MESSAGE; } } void EnttecDMXDevice::submitTransfer(Transfer *fct) { /* * Submit a new USB transfer. The Transfer object is guaranteed to be freed eventually. * On error, it's freed right away. */ int r = libusb_submit_transfer(fct->transfer); if (r < 0) { if (mVerbose && r != LIBUSB_ERROR_PIPE) { std::clog << "Error submitting USB transfer: " << libusb_strerror(libusb_error(r)) << "\n"; } delete fct; } else { mPending.insert(fct); } } void EnttecDMXDevice::completeTransfer(struct libusb_transfer *transfer) { EnttecDMXDevice::Transfer *fct = static_cast<EnttecDMXDevice::Transfer*>(transfer->user_data); fct->finished = true; } void EnttecDMXDevice::flush() { // Erase any finished transfers std::set<Transfer*>::iterator current = mPending.begin(); while (current != mPending.end()) { std::set<Transfer*>::iterator next = current; next++; Transfer *fct = *current; if (fct->finished) { mPending.erase(current); delete fct; } current = next; } } void EnttecDMXDevice::writeDMXPacket() { /* * Asynchronously write an FTDI packet containing an Enttec packet containing * our set of DMX channels. * * XXX: We should probably throttle this so that we don't send DMX messages * faster than the Enttec device can keep up! */ submitTransfer(new Transfer(this, &mChannelBuffer, mChannelBuffer.length + 5)); } void EnttecDMXDevice::writeMessage(const OPC::Message &msg) { /* * Dispatch an incoming OPC command */ switch (msg.command) { case OPC::SetPixelColors: opcSetPixelColors(msg); writeDMXPacket(); return; case OPC::SystemExclusive: // No relevant SysEx for this device return; } if (mVerbose) { std::clog << "Unsupported OPC command: " << unsigned(msg.command) << "\n"; } } void EnttecDMXDevice::opcSetPixelColors(const OPC::Message &msg) { /* * Parse through our device's mapping, and store any relevant portions of 'msg' * in the framebuffer. */ if (!mConfigMap) { // No mapping defined yet. This device is inactive. return; } const Value &map = *mConfigMap; for (unsigned i = 0, e = map.Size(); i != e; i++) { opcMapPixelColors(msg, map[i]); } } void EnttecDMXDevice::opcMapPixelColors(const OPC::Message &msg, const Value &inst) { /* * Parse one JSON mapping instruction, and copy any relevant parts of 'msg' * into our framebuffer. This looks for any mapping instructions that we * recognize: * * [ OPC Channel, OPC Pixel, Pixel Color, DMX Channel ] */ unsigned msgPixelCount = msg.length() / 3; if (inst.IsArray() && inst.Size() == 4) { // Map a range from an OPC channel to our framebuffer const Value &vChannel = inst[0u]; const Value &vPixelIndex = inst[1]; const Value &vPixelColor = inst[2]; const Value &vDMXChannel = inst[3]; if (vChannel.IsUint() && vPixelIndex.IsUint() && vPixelColor.IsString() && vDMXChannel.IsUint()) { unsigned channel = vChannel.GetUint(); unsigned pixelIndex = vPixelIndex.GetUint(); const char *pixelColor = vPixelColor.GetString(); unsigned dmxChannel = vDMXChannel.GetUint(); if (channel != msg.channel || pixelIndex >= msgPixelCount) { return; } const uint8_t *pixel = msg.data + (pixelIndex * 3); switch (pixelColor[0]) { case 'r': setChannel(dmxChannel, pixel[0]); break; case 'g': setChannel(dmxChannel, pixel[1]); break; case 'b': setChannel(dmxChannel, pixel[2]); break; case 'l': setChannel(dmxChannel, (unsigned(pixel[0]) + unsigned(pixel[1]) + unsigned(pixel[2])) / 3); break; } return; } } // Still haven't found a match? if (mVerbose) { std::clog << "Unsupported JSON mapping instruction\n"; } }