Skip to content
Snippets Groups Projects
enttecdmxdevice.cpp 8.59 KiB
Newer Older
  • Learn to ignore specific revisions
  • /*
     * 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)),
    	  device(device)
    {
    	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 and jettison them
    	 * from the EnttecDMXDevice. 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);
    		fct->device = 0;
    	}
    }
    
    
    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;
    
    	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;
    		}
    	}
    
    
    
    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;
    	}
    
    
    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)
    {
    	/*
    	 * Transfer complete. The EnttecDMXDevice may or may not still exist; if the device was unplugged,
    	 * fct->device will be set to 0 by ~EnttecDMXDevice().
    	 */
    
    	EnttecDMXDevice::Transfer *fct = static_cast<EnttecDMXDevice::Transfer*>(transfer->user_data);
    	EnttecDMXDevice *self = fct->device;
    
    	if (self) {
    		self->mPending.erase(fct);
    	}
    
    	delete fct;
    }
    
    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 OPCSink::Message &msg)
    {
    
    	/*
    	 * Dispatch an incoming OPC command
    	 */
    
    	switch (msg.command) {
    
    		case OPCSink::SetPixelColors:
    			opcSetPixelColors(msg);
    			writeDMXPacket();
    			return;
    
    		case OPCSink::SystemExclusive:
    			// No relevant SysEx for this device
    			return;
    	}
    
    	if (mVerbose) {
    		std::clog << "Unsupported OPC command: " << unsigned(msg.command) << "\n";
    	}
    }
    
    void EnttecDMXDevice::opcSetPixelColors(const OPCSink::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 OPCSink::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";
        }