From 820a2567c65e69c5c940a5655b9477202855e37e Mon Sep 17 00:00:00 2001
From: Micah Elizabeth Scott <micah@scanlime.org>
Date: Sat, 27 Jul 2013 00:15:14 -0700
Subject: [PATCH] Basic DMX support, with the Enttec DMX USB Pro

---
 examples/config-basic.json |  10 ++
 server/README.md           |  48 +++++-
 server/enttecdmxdevice.cpp | 292 ++++++++++++++++++++++++++++++++++++-
 server/enttecdmxdevice.h   |  38 +++++
 server/fcdevice.cpp        |  50 +------
 server/fcserver.cpp        |   6 +
 server/usbdevice.cpp       |  56 +++++++
 server/usbdevice.h         |   7 +
 8 files changed, 457 insertions(+), 50 deletions(-)

diff --git a/examples/config-basic.json b/examples/config-basic.json
index ba98c22..7f656aa 100644
--- a/examples/config-basic.json
+++ b/examples/config-basic.json
@@ -29,6 +29,16 @@
 			"map": [
 				[ 0, 0, 0, 512 ]
 			]
+		},
+		{
+			"type": "enttec",
+			"serial": "EN075577",
+			"map": [
+                [ 0, 0, "r", 1 ],
+                [ 0, 0, "g", 2 ],
+                [ 0, 0, "b", 3 ],
+                [ 0, 1, "l", 4 ]
+			]
 		}
 	]
 }
diff --git a/server/README.md b/server/README.md
index be0b73b..2bb7af3 100644
--- a/server/README.md
+++ b/server/README.md
@@ -54,7 +54,9 @@ Byte   | **Set Global Color Correction** command
 Configuration
 -------------
 
-The JSON configuration file is a dictionary which contains global configuration and an array of device objects. For each device, a dictionary includes device properties as well as a mapping table with commands which wire outputs to their corresponding OPC inputs. The map is a list of objects which act as mapping commands. Supported mapping objects:
+The JSON configuration file is a dictionary which contains global configuration and an array of device objects. For each device, a dictionary includes device properties as well as a mapping table with commands which wire outputs to their corresponding OPC inputs. The map is a list of objects which act as mapping commands. 
+
+Supported mapping objects for Fadecandy devices:
 
 * [ *OPC Channel*, *First OPC Pixel*, *First output pixel*, *pixel count* ]
 	* Map a contiguous range of pixels from the specified OPC channel to the current device
@@ -111,3 +113,47 @@ On Debian or Ubuntu Linux (including the Raspberry Pi) libev can be installed wi
 	$ ./configure
 	$ make
 	$ sudo make install
+
+Using Open Pixel Control with DMX
+---------------------------------
+
+The Fadecandy server is designed to make it easy to drive all your lighting via Open Pixel Control, even when you're using a mixture of addressable LED strips and DMX devices.
+
+For DMX, `fcserver` supports the common [Enttec DMX USB Pro adapter](http://www.enttec.com/index.php?main_menu=Products&pn=70304). This device attaches over USB, has inputs and outputs for one DMX universe, and it has an LED indicator. With Fadecandy, the LED will flash any time we process a new frame of video.
+
+The Enttec adapter uses an FTDI FT245 USB FIFO chip internally. For the smoothest USB performance and the simplest configuration, we do not use FTDI's serial port emulation driver. Instead, we talk directly to the FT232 chip using libusb. On Linux this happens without any special consideration. On Mac OS, libusb does not support detaching existing drivers from a device. If you've installed the official FTDI driver, you can temporarily unload it until your next reboot by running:
+
+	sudo kextunload -b com.FTDI.driver.FTDIUSBSerialDriver
+
+Enttec DMX devices can be configured in the same way as a Fadecandy device. For example:
+
+	{
+	        "listen": [null, 7890],
+	        "verbose": true,
+
+	        "devices": [
+	                {
+	                        "type": "fadecandy",
+	                        "map": [
+	                                [ 0, 0, 0, 512 ]
+	                        ]
+	                },
+	                {
+	                        "type": "enttec",
+	                        "serial": "EN075577",
+	                        "map": [
+	                                [ 0, 0, "r", 1 ],
+	                                [ 0, 0, "g", 2 ],
+	                                [ 0, 0, "b", 3 ],
+	                                [ 0, 1, "l", 4 ]
+	                        ]
+	                }
+	        ]
+	}
+
+Enttec DMX devices use a different format for their mapping objects:
+
+* [ *OPC Channel*, *OPC Pixel*, *Pixel Color*, *DMX Channel* ]
+    * Map a single OPC pixel to a single DMX channel
+    * The "Pixel color" can be "r", "g", or "b" to sample a single color channel from the pixel, or "l" to use an average luminosity.
+    * DMX channels are numbered from 1 to 512.
diff --git a/server/enttecdmxdevice.cpp b/server/enttecdmxdevice.cpp
index 0856f57..c61b5af 100644
--- a/server/enttecdmxdevice.cpp
+++ b/server/enttecdmxdevice.cpp
@@ -22,35 +22,313 @@
  */
 
 #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)
-{}
+	: 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)
 {
-	return false;
+	/*
+	 * 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)
+{
+	/*
+	 * 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]);
+	}
 }
 
-std::string EnttecDMXDevice::getName()
+void EnttecDMXDevice::opcMapPixelColors(const OPCSink::Message &msg, const Value &inst)
 {
-	return "Enttec DMX Pro";
+	/*
+	 * 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";
+    }
 }
diff --git a/server/enttecdmxdevice.h b/server/enttecdmxdevice.h
index bbc1c75..a73f6e0 100644
--- a/server/enttecdmxdevice.h
+++ b/server/enttecdmxdevice.h
@@ -23,6 +23,7 @@
 
 #pragma once
 #include "usbdevice.h"
+#include <set>
 
 
 class EnttecDMXDevice : public USBDevice
@@ -34,7 +35,44 @@ public:
     static bool probe(libusb_device *device);
 
     virtual int open();
+    virtual bool probeAfterOpening();
     virtual bool matchConfiguration(const Value &config);
     virtual void writeMessage(const OPCSink::Message &msg);
     virtual std::string getName();
+
+    void writeDMXPacket();
+    void setChannel(unsigned n, uint8_t value);
+
+private:
+    static const unsigned OUT_ENDPOINT = 2;
+    static const unsigned START_OF_MESSAGE = 0x7e;
+    static const unsigned END_OF_MESSAGE = 0xe7;
+    static const unsigned SEND_DMX_PACKET = 0x06;
+    static const unsigned START_CODE = 0x00;
+
+    struct Packet {
+        uint8_t start;
+        uint8_t label;
+        uint16_t length;
+        uint8_t data[514];
+    };
+
+    struct Transfer {
+        Transfer(EnttecDMXDevice *device, void *buffer, int length);
+        ~Transfer();
+        libusb_transfer *transfer;
+        EnttecDMXDevice *device;
+    };
+
+    char mSerial[256];
+    bool mFoundEnttecStrings;
+    const Value *mConfigMap;
+    Packet mChannelBuffer;
+    std::set<Transfer*> mPending;
+
+    void submitTransfer(Transfer *fct);
+    static void completeTransfer(struct libusb_transfer *transfer);
+
+    void opcSetPixelColors(const OPCSink::Message &msg);
+    void opcMapPixelColors(const OPCSink::Message &msg, const Value &inst);
 };
diff --git a/server/fcdevice.cpp b/server/fcdevice.cpp
index 5caf1bb..20cc833 100644
--- a/server/fcdevice.cpp
+++ b/server/fcdevice.cpp
@@ -111,49 +111,12 @@ int FCDevice::open()
 
 bool FCDevice::matchConfiguration(const Value &config)
 {
-	/*
-	 * Parse out the portions of our JSON configuration document which matter to us.
-	 */
-
-	if (!config.IsObject()) {
-		return false;
-	}
-
-	const Value &vtype = config["type"];
-	const Value &vserial = config["serial"];
-	const Value &vmap = config["map"];
-
-	if (!vtype.IsString() || strcmp(vtype.GetString(), "fadecandy")) {
-		// Wrong type
-		return false;
+	if (matchConfigurationWithTypeAndSerial(config, "fadecandy", mSerial)) {
+		mConfigMap = findConfigMap(config);
+		return true;
 	}
 
-	if (!vserial.IsNull()) {
-		// Not a wildcard serial number?
-		// If a serial was not specified, it matches any device.
-
-		if (!vserial.IsString()) {
-			// Non-string serial number. Bad form.
-			return false;
-		}
-
-		if (strcmp(vserial.GetString(), mSerial)) {
-			// Not a match
-			return false;
-		}
-	}
-
-	if (vmap.IsArray()) {
-		// The map is optional, but if it exists it needs to be an array.
-		mConfigMap = &vmap;
-	} else {
-		mConfigMap = 0;
-		if (!vmap.IsNull() && mVerbose) {
-			std::clog << "Device configuration 'map' must be an array.\n";
-		}
-	}
-
-	return true;
+	return false;
 }
 
 void FCDevice::submitTransfer(Transfer *fct)
@@ -446,7 +409,10 @@ void FCDevice::opcSetGlobalColorCorrection(const OPCSink::Message &msg)
 std::string FCDevice::getName()
 {
 	std::ostringstream s;
-	s << "Fadecandy (Serial# " << mSerial << ")";
+	s << "Fadecandy";
+	if (mSerial[0]) {
+		s << " (Serial# " << mSerial << ")";
+	}
 	return s.str();
 }
 	
\ No newline at end of file
diff --git a/server/fcserver.cpp b/server/fcserver.cpp
index 3fe5085..1b63558 100644
--- a/server/fcserver.cpp
+++ b/server/fcserver.cpp
@@ -172,6 +172,12 @@ void FCServer::usbDeviceArrived(libusb_device *device)
 		return;
 	}
 
+	if (!dev->probeAfterOpening()) {
+		// We were mistaken, this device isn't actually one we want.
+		delete dev;
+		return;
+	}
+
 	for (unsigned i = 0; i < mDevices.Size(); ++i) {
 		if (dev->matchConfiguration(mDevices[i])) {
 			// Found a matching configuration for this device. We're keeping it!
diff --git a/server/usbdevice.cpp b/server/usbdevice.cpp
index 4568533..783a028 100644
--- a/server/usbdevice.cpp
+++ b/server/usbdevice.cpp
@@ -22,6 +22,7 @@
  */
 
 #include "usbdevice.h"
+#include <iostream>
 
 
 USBDevice::USBDevice(libusb_device *device, bool verbose)
@@ -40,7 +41,62 @@ USBDevice::~USBDevice()
     }
 }
 
+bool USBDevice::probeAfterOpening()
+{
+    // By default, any device is supported by the time we get to opening it.
+    return true;
+}
+
 void USBDevice::writeColorCorrection(const Value &color)
 {
     // Optional. By default, ignore color correction messages.
 }
+
+bool USBDevice::matchConfigurationWithTypeAndSerial(const Value &config, const char *type, const char *serial)
+{
+    if (!config.IsObject()) {
+        return false;
+    }
+
+    const Value &vtype = config["type"];
+    const Value &vserial = config["serial"];
+
+    if (!vtype.IsString() || strcmp(vtype.GetString(), type)) {
+        // Wrong type
+        return false;
+    }
+
+    if (!vserial.IsNull()) {
+        // Not a wildcard serial number?
+        // If a serial was not specified, it matches any device.
+
+        if (!vserial.IsString()) {
+            // Non-string serial number. Bad form.
+            return false;
+        }
+
+        if (strcmp(vserial.GetString(), serial)) {
+            // Not a match
+            return false;
+        }
+    }
+
+    return true;
+}
+
+
+const USBDevice::Value *USBDevice::findConfigMap(const Value &config)
+{
+    const Value &vmap = config["map"];
+
+    if (vmap.IsArray()) {
+        // The map is optional, but if it exists it needs to be an array.
+        return &vmap;
+    }
+    
+    if (!vmap.IsNull() && mVerbose) {
+        std::clog << "Device configuration 'map' must be an array.\n";
+    }
+
+    return 0;
+}
diff --git a/server/usbdevice.h b/server/usbdevice.h
index 3f03d40..2a16e75 100644
--- a/server/usbdevice.h
+++ b/server/usbdevice.h
@@ -39,6 +39,9 @@ public:
     // Must be opened before any other methods are called.
     virtual int open() = 0;
 
+    // Some drivers can't determine whether this is a supported device prior to open()
+    virtual bool probeAfterOpening();
+
     // Check a configuration. If it describes this device, load it and return true. If not, return false.
     virtual bool matchConfiguration(const Value &config) = 0;
 
@@ -55,4 +58,8 @@ protected:
     libusb_device *mDevice;
     libusb_device_handle *mHandle;
 	bool mVerbose;
+
+    // Utilities
+    bool matchConfigurationWithTypeAndSerial(const Value &config, const char *type, const char *serial);
+    const Value *findConfigMap(const Value &config);
 };
-- 
GitLab