/* * Simple Open Pixel Control client for Processing, * designed to sample each LED's color from some point on the canvas. * * Micah Elizabeth Scott, 2013 * This file is released into the public domain. */ import java.net.*; import java.util.Arrays; public class OPC implements Runnable { Thread thread; Socket socket; OutputStream output, pending; String host; int port; int[] pixelLocations; byte[] packetData; byte firmwareConfig; String colorCorrection; boolean enableShowLocations; OPC(PApplet parent, String host, int port) { this.host = host; this.port = port; thread = new Thread(this); thread.start(); this.enableShowLocations = true; parent.registerMethod("draw", this); } // Set the location of a single LED void led(int index, int x, int y) { // For convenience, automatically grow the pixelLocations array. We do want this to be an array, // instead of a HashMap, to keep draw() as fast as it can be. if (pixelLocations == null) { pixelLocations = new int[index + 1]; } else if (index >= pixelLocations.length) { pixelLocations = Arrays.copyOf(pixelLocations, index + 1); } pixelLocations[index] = x + width * y; } // Set the location of several LEDs arranged in a strip. // Angle is in radians, measured clockwise from +X. // (x,y) is the center of the strip. void ledStrip(int index, int count, float x, float y, float spacing, float angle, boolean reversed) { float s = sin(angle); float c = cos(angle); for (int i = 0; i < count; i++) { led(reversed ? (index + count - 1 - i) : (index + i), (int)(x + (i - (count-1)/2.0) * spacing * c + 0.5), (int)(y + (i - (count-1)/2.0) * spacing * s + 0.5)); } } // Set the locations of a ring of LEDs. The center of the ring is at (x, y), // with "radius" pixels between the center and each LED. The first LED is at // the indicated angle, in radians, measured clockwise from +X. void ledRing(int index, int count, float x, float y, float radius, float angle) { for (int i = 0; i < count; i++) { float a = angle + i * 2 * PI / count; led(index + i, (int)(x - radius * cos(a) + 0.5), (int)(y - radius * sin(a) + 0.5)); } } // Set the location of several LEDs arranged in a grid. The first strip is // at 'angle', measured in radians clockwise from +X. // (x,y) is the center of the grid. void ledGrid(int index, int stripLength, int numStrips, float x, float y, float ledSpacing, float stripSpacing, float angle, boolean zigzag) { float s = sin(angle + HALF_PI); float c = cos(angle + HALF_PI); for (int i = 0; i < numStrips; i++) { ledStrip(index + stripLength * i, stripLength, x + (i - (numStrips-1)/2.0) * stripSpacing * c, y + (i - (numStrips-1)/2.0) * stripSpacing * s, ledSpacing, angle, zigzag && (i % 2) == 1); } } // Set the location of 64 LEDs arranged in a uniform 8x8 grid. // (x,y) is the center of the grid. void ledGrid8x8(int index, float x, float y, float spacing, float angle, boolean zigzag) { ledGrid(index, 8, 8, x, y, spacing, spacing, angle, zigzag); } // Should the pixel sampling locations be visible? This helps with debugging. // Showing locations is enabled by default. You might need to disable it if our drawing // is interfering with your processing sketch, or if you'd simply like the screen to be // less cluttered. void showLocations(boolean enabled) { enableShowLocations = enabled; } // Enable or disable dithering. Dithering avoids the "stair-stepping" artifact and increases color // resolution by quickly jittering between adjacent 8-bit brightness levels about 400 times a second. // Dithering is on by default. void setDithering(boolean enabled) { if (enabled) firmwareConfig &= ~0x01; else firmwareConfig |= 0x01; sendFirmwareConfigPacket(); } // Enable or disable frame interpolation. Interpolation automatically blends between consecutive frames // in hardware, and it does so with 16-bit per channel resolution. Combined with dithering, this helps make // fades very smooth. Interpolation is on by default. void setInterpolation(boolean enabled) { if (enabled) firmwareConfig &= ~0x02; else firmwareConfig |= 0x02; sendFirmwareConfigPacket(); } // Put the Fadecandy onboard LED under automatic control. It blinks any time the firmware processes a packet. // This is the default configuration for the LED. void statusLedAuto() { firmwareConfig &= 0x0C; sendFirmwareConfigPacket(); } // Manually turn the Fadecandy onboard LED on or off. This disables automatic LED control. void setStatusLed(boolean on) { firmwareConfig |= 0x04; // Manual LED control if (on) firmwareConfig |= 0x08; else firmwareConfig &= ~0x08; sendFirmwareConfigPacket(); } // Set the color correction parameters void setColorCorrection(float gamma, float red, float green, float blue) { colorCorrection = "{ \"gamma\": " + gamma + ", \"whitepoint\": [" + red + "," + green + "," + blue + "]}"; sendColorCorrectionPacket(); } // Set custom color correction parameters from a string void setColorCorrection(String s) { colorCorrection = s; sendColorCorrectionPacket(); } // Send a packet with the current firmware configuration settings void sendFirmwareConfigPacket() { if (pending == null) { // We'll do this when we reconnect return; } byte[] packet = new byte[9]; packet[0] = (byte)0x00; // Channel (reserved) packet[1] = (byte)0xFF; // Command (System Exclusive) packet[2] = (byte)0x00; // Length high byte packet[3] = (byte)0x05; // Length low byte packet[4] = (byte)0x00; // System ID high byte packet[5] = (byte)0x01; // System ID low byte packet[6] = (byte)0x00; // Command ID high byte packet[7] = (byte)0x02; // Command ID low byte packet[8] = (byte)firmwareConfig; try { pending.write(packet); } catch (Exception e) { dispose(); } } // Send a packet with the current color correction settings void sendColorCorrectionPacket() { if (colorCorrection == null) { // No color correction defined return; } if (pending == null) { // We'll do this when we reconnect return; } byte[] content = colorCorrection.getBytes(); int packetLen = content.length + 4; byte[] header = new byte[8]; header[0] = (byte)0x00; // Channel (reserved) header[1] = (byte)0xFF; // Command (System Exclusive) header[2] = (byte)(packetLen >> 8); // Length high byte header[3] = (byte)(packetLen & 0xFF); // Length low byte header[4] = (byte)0x00; // System ID high byte header[5] = (byte)0x01; // System ID low byte header[6] = (byte)0x00; // Command ID high byte header[7] = (byte)0x01; // Command ID low byte try { pending.write(header); pending.write(content); } catch (Exception e) { dispose(); } } // Automatically called at the end of each draw(). // This handles the automatic Pixel to LED mapping. // If you aren't using that mapping, this function has no effect. // In that case, you can call setPixelCount(), setPixel(), and writePixels() // separately. void draw() { if (pixelLocations == null) { // No pixels defined yet return; } if (output == null) { return; } int numPixels = pixelLocations.length; int ledAddress = 4; setPixelCount(numPixels); loadPixels(); for (int i = 0; i < numPixels; i++) { int pixelLocation = pixelLocations[i]; int pixel = pixels[pixelLocation]; packetData[ledAddress] = (byte)(pixel >> 16); packetData[ledAddress + 1] = (byte)(pixel >> 8); packetData[ledAddress + 2] = (byte)pixel; ledAddress += 3; if (enableShowLocations) { pixels[pixelLocation] = 0xFFFFFF ^ pixel; } } writePixels(); if (enableShowLocations) { updatePixels(); } } // Change the number of pixels in our output packet. // This is normally not needed; the output packet is automatically sized // by draw() and by setPixel(). void setPixelCount(int numPixels) { int numBytes = 3 * numPixels; int packetLen = 4 + numBytes; if (packetData == null || packetData.length != packetLen) { // Set up our packet buffer packetData = new byte[packetLen]; packetData[0] = (byte)0x00; // Channel packetData[1] = (byte)0x00; // Command (Set pixel colors) packetData[2] = (byte)(numBytes >> 8); // Length high byte packetData[3] = (byte)(numBytes & 0xFF); // Length low byte } } // Directly manipulate a pixel in the output buffer. This isn't needed // for pixels that are mapped to the screen. void setPixel(int number, color c) { int offset = 4 + number * 3; if (packetData == null || packetData.length < offset + 3) { setPixelCount(number + 1); } packetData[offset] = (byte) (c >> 16); packetData[offset + 1] = (byte) (c >> 8); packetData[offset + 2] = (byte) c; } // Read a pixel from the output buffer. If the pixel was mapped to the display, // this returns the value we captured on the previous frame. color getPixel(int number) { int offset = 4 + number * 3; if (packetData == null || packetData.length < offset + 3) { return 0; } return (packetData[offset] << 16) | (packetData[offset + 1] << 8) | packetData[offset + 2]; } // Transmit our current buffer of pixel values to the OPC server. This is handled // automatically in draw() if any pixels are mapped to the screen, but if you haven't // mapped any pixels to the screen you'll want to call this directly. void writePixels() { if (packetData == null || packetData.length == 0) { // No pixel buffer return; } if (output == null) { return; } try { output.write(packetData); } catch (Exception e) { dispose(); } } void dispose() { // Destroy the socket. Called internally when we've disconnected. // (Thread continues to run) if (output != null) { println("Disconnected from OPC server"); } socket = null; output = pending = null; } public void run() { // Thread tests server connection periodically, attempts reconnection. // Important for OPC arrays; faster startup, client continues // to run smoothly when mobile servers go in and out of range. for(;;) { if(output == null) { // No OPC connection? try { // Make one! socket = new Socket(host, port); socket.setTcpNoDelay(true); pending = socket.getOutputStream(); // Avoid race condition... println("Connected to OPC server"); sendColorCorrectionPacket(); // These write to 'pending' sendFirmwareConfigPacket(); // rather than 'output' before output = pending; // rest of code given access. // pending not set null, more config packets are OK! } catch (ConnectException e) { dispose(); } catch (IOException e) { dispose(); } } // Pause thread to avoid massive CPU load try { Thread.sleep(500); } catch(InterruptedException e) { } } } }