From db2ff234c4d6b6d0596766ef61bcc4d97d430375 Mon Sep 17 00:00:00 2001 From: Paint Your Dragon Date: Sun, 20 Nov 2011 14:51:14 -0800 Subject: [PATCH] Multi-monitor support, speed improvements, LED auto-off and other good stuff --- Arduino/LEDstream/LEDstream.pde | 32 ++- Processing/Adalight/Adalight.pde | 441 ++++++++++++++++++++++--------- 2 files changed, 345 insertions(+), 128 deletions(-) diff --git a/Arduino/LEDstream/LEDstream.pde b/Arduino/LEDstream/LEDstream.pde index 665d535..091faa1 100644 --- a/Arduino/LEDstream/LEDstream.pde +++ b/Arduino/LEDstream/LEDstream.pde @@ -61,6 +61,11 @@ static const uint8_t magic[] = {'A','d','a'}; #define MODE_HOLD 1 #define MODE_DATA 2 +// If no serial data is received for a while, the LEDs are shut off +// automatically. This avoids the annoying "stuck pixel" look when +// quitting LED display programs on the host computer. +static const unsigned long serialTimeout = 15000; // 15 seconds + void setup() { // Dirty trick: the circular buffer for serial data is 256 bytes, @@ -82,7 +87,10 @@ void setup() int32_t bytesRemaining; unsigned long - startTime = micros(); + startTime, + lastByteTime, + lastAckTime, + t; LED_DDR |= LED_PIN; // Enable output for LED LED_PORT &= ~LED_PIN; // LED off @@ -116,6 +124,11 @@ void setup() delay(1); // One millisecond pause = latch } + Serial.print("Ada\n"); // Send ACK string to host + + startTime = micros(); + lastByteTime = lastAckTime = millis(); + // loop() is avoided as even that small bit of function overhead // has a measurable impact on this code's overall throughput. @@ -123,9 +136,26 @@ void setup() // Implementation is a simple finite-state machine. // Regardless of mode, check for serial input each time: + t = millis(); if((bytesBuffered < 256) && ((c = Serial.read()) >= 0)) { buffer[indexIn++] = c; bytesBuffered++; + lastByteTime = lastAckTime = t; // Reset timeout counters + } else { + // No data received. If this persists, send an ACK packet + // to host once every second to alert it to our presence. + if((t - lastAckTime) > 1000) { + Serial.print("Ada\n"); // Send ACK string to host + lastAckTime = t; // Reset counter + } + // If no data received for an extended time, turn off all LEDs. + if((t - lastByteTime) > serialTimeout) { + for(c=0; c<32767; c++) { + for(SPDR=0; !(SPSR & _BV(SPIF)); ); + } + delay(1); // One millisecond pause = latch + lastByteTime = t; // Reset counter + } } switch(mode) { diff --git a/Processing/Adalight/Adalight.pde b/Processing/Adalight/Adalight.pde index 5fcb3af..9911cce 100644 --- a/Processing/Adalight/Adalight.pde +++ b/Processing/Adalight/Adalight.pde @@ -1,172 +1,359 @@ // "Adalight" is a do-it-yourself facsimile of the Philips Ambilight concept // for desktop computers and home theater PCs. This is the host PC-side code -// written in Processing; intended for use with a USB-connected Arduino +// written in Processing, intended for use with a USB-connected Arduino // microcontroller running the accompanying LED streaming code. Requires one -// strand of Digital RGB LED Pixels (Adafruit product ID #322, specifically -// the newer WS2801-based type, strand of 25) and a 5 Volt power supply (such -// as Adafruit #276). You may need to adapt the code and the hardware -// arrangement for your specific display configuration. +// or more strands of Digital RGB LED Pixels (Adafruit product ID #322, +// specifically the newer WS2801-based type, strand of 25) and a 5 Volt power +// supply (such as Adafruit #276). You may need to adapt the code and the +// hardware arrangement for your specific display configuration. // Screen capture adapted from code by Cedrik Kiefer (processing.org forum) import java.awt.*; import java.awt.image.*; import processing.serial.*; -// This array contains the 2D image coordinates corresponding to each pixel -// in the LED strand, which forms a ring around the perimeter of the screen -// (with a one pixel gap at the bottom to accommodate the monitor stand). +// CONFIGURABLE PROGRAM CONSTANTS -------------------------------------------- -static final int coord[][] = new int[][] { - {3,5}, {2,5}, {1,5}, {0,5}, // Bottom edge, left half - {0,4}, {0,3}, {0,2}, {0,1}, // Left edge - {0,0}, {1,0}, {2,0}, {3,0}, {4,0}, {5,0}, {6,0}, {7,0}, {8,0}, // Top edge - {8,1}, {8,2}, {8,3}, {8,4}, // Right edge - {8,5}, {7,5}, {6,5}, {5,5} // Bottom edge, right half +// Minimum LED brightness; some users prefer a small amount of backlighting +// at all times, regardless of screen content. Higher values are brighter, +// or set to 0 to disable this feature. + +static final short minBrightness = 120; + +// LED transition speed; it's sometimes distracting if LEDs instantaneously +// track screen contents (such as during bright flashing sequences), so this +// feature enables a gradual fade to each new LED state. Higher numbers yield +// slower transitions (max of 255), or set to 0 to disable this feature +// (immediate transition of all LEDs). + +static final short fade = 75; + +// Pixel size for the live preview image. + +static final int pixelSize = 20; + +// Serial device timeout (in milliseconds), for locating Arduino device +// running the corresponding LEDstream code. See notes later in the code... +// in some situations you may want to entirely comment out that block. + +static final int timeout = 5000; // 5 seconds + +// PER-DISPLAY INFORMATION --------------------------------------------------- + +// This array contains details for each display that the software will +// process. If you have screen(s) attached that are not among those being +// "Adalighted," they should not be in this list. Each triplet in this +// array represents one display. The first number is the system screen +// number...typically the "primary" display on most systems is identified +// as screen #1, but since arrays are indexed from zero, use 0 to indicate +// the first screen, 1 to indicate the second screen, and so forth. This +// is the ONLY place system screen numbers are used...ANY subsequent +// references to displays are an index into this list, NOT necessarily the +// same as the system screen number. For example, if you have a three- +// screen setup and are illuminating only the third display, use '2' for +// the screen number here...and then, in subsequent section, '0' will be +// used to refer to the first/only display in this list. +// The second and third numbers of each triplet represent the width and +// height of a grid of LED pixels attached to the perimeter of this display. +// For example, '9,6' = 9 LEDs across, 6 LEDs down. + +static final int displays[][] = new int[][] { + {0,9,6} // Screen 0, 9 LEDs across, 6 LEDs down +//,{1,9,6} // Screen 1, also 9 LEDs across and 6 LEDs down }; -static final int arrayWidth = 9, // Width of Adalight array, in LED pixels - arrayHeight = 6, // Height of Adalight array, in LED pixels - imgScale = 20, // Size of displayed preview - samples = 20, // Samples (per axis) when down-scaling - s2 = samples * samples; +// PER-LED INFORMATION ------------------------------------------------------- -byte[] buffer = new byte[6 + coord.length * 3]; -byte[][] gamma = new byte[256][3]; -GraphicsDevice[] gs; -PImage preview = createImage(arrayWidth, arrayHeight, RGB); -Rectangle bounds; +// This array contains the 2D coordinates corresponding to each pixel in the +// LED strand, in the order that they're connected (i.e. the first element +// here belongs to the first LED in the strand, second element is the second +// LED, and so forth). Each triplet in this array consists of a display +// number (an index into the display array above, NOT necessarily the same as +// the system screen number) and an X and Y coordinate specified in the grid +// units given for that display. {0,0,0} is the top-left corner of the first +// display in the array. +// For our example purposes, the coordinate list below forms a ring around +// the perimeter of a single screen, with a one pixel gap at the bottom to +// accommodate a monitor stand. Modify this to match your own setup: + +static final int leds[][] = new int[][] { + {0,3,5}, {0,2,5}, {0,1,5}, {0,0,5}, // Bottom edge, left half + {0,0,4}, {0,0,3}, {0,0,2}, {0,0,1}, // Left edge + {0,0,0}, {0,1,0}, {0,2,0}, {0,3,0}, {0,4,0}, // Top edge + {0,5,0}, {0,6,0}, {0,7,0}, {0,8,0}, // More top edge + {0,8,1}, {0,8,2}, {0,8,3}, {0,8,4}, // Right edge + {0,8,5}, {0,7,5}, {0,6,5}, {0,5,5} // Bottom edge, right half + +/* Hypothetical second display has the same arrangement as the first. + But you might not want both displays completely ringed with LEDs; + the screens might be positioned where they share an edge in common. + ,{1,3,5}, {1,2,5}, {1,1,5}, {1,0,5}, // Bottom edge, left half + {1,0,4}, {1,0,3}, {1,0,2}, {1,0,1}, // Left edge + {1,0,0}, {1,1,0}, {1,2,0}, {1,3,0}, {1,4,0}, // Top edge + {1,5,0}, {1,6,0}, {1,7,0}, {1,8,0}, // More top edge + {1,8,1}, {1,8,2}, {1,8,3}, {1,8,4}, // Right edge + {1,8,5}, {1,7,5}, {1,6,5}, {1,5,5} // Bottom edge, right half +*/ +}; + +// GLOBAL VARIABLES ---- You probably won't need to modify any of this ------- + +byte[] serialData = new byte[6 + leds.length * 3]; +short[][] ledColor = new short[leds.length][3], + prevColor = new short[leds.length][3]; +byte[][] gamma = new byte[256][3]; +int nDisplays = displays.length; +Rectangle[] dispBounds = new Rectangle[displays.length]; +int[][] screenData = new int[displays.length][], + pixelOffset = new int[leds.length][256]; +PImage[] preview = new PImage[displays.length]; Serial port; +GraphicsDevice[] gd; DisposeHandler dh; // For disabling LEDs on exit + +// INITIALIZATION ------------------------------------------------------------ + void setup() { - GraphicsEnvironment ge; - DisplayMode mode; - int i; - float f; + GraphicsEnvironment ge; + GraphicsConfiguration[] gc; + int d, i, totalWidth, maxHeight, row, col, rowOffset; + float f, startX, curX, curY, incX, incY; - dh = new DisposeHandler(this); - port = new Serial(this, Serial.list()[0], 115200); + dh = new DisposeHandler(this); // Init DisposeHandler ASAP + // Comment out this line to test the software without Arduino: + port = openPort(); // Open serial port to Arduino - size(arrayWidth * imgScale, arrayHeight * imgScale, JAVA2D); + // Initialize screen capture code for each display's dimensions: + ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); + gd = ge.getScreenDevices(); + if(nDisplays > gd.length) nDisplays = gd.length; + totalWidth = maxHeight = 0; + for(d=0; d 0) totalWidth++; + if(displays[d][2] > maxHeight) maxHeight = displays[d][2]; + } - // Initialize capture code for full screen dimensions: - ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); - gs = ge.getScreenDevices(); - mode = gs[0].getDisplayMode(); - bounds = new Rectangle(0, 0, screen.width, screen.height); + // Precompute locations of every pixel to read when downsampling. + // Saves a bunch of math on each frame, at the expense of a chunk of RAM; + // but hey, it's not like the screen captures are petite either. + for(i=0; i> 8); // LED count high byte - buffer[4] = byte((coord.length - 1) & 0xff); // LED count low byte - buffer[5] = byte(buffer[3] ^ buffer[4] ^ 0x55); // Checksum + serialData[0] = 'A'; // Magic word + serialData[1] = 'd'; + serialData[2] = 'a'; + serialData[3] = (byte)((leds.length - 1) >> 8); // LED count high byte + serialData[4] = (byte)((leds.length - 1) & 0xff); // LED count low byte + serialData[5] = (byte)(serialData[3] ^ serialData[4] ^ 0x55); // Checksum // Pre-compute gamma correction table for LED brightness levels: - for(i = 0; i < 256; i++) { - f = pow(float(i) / 255.0, 2.8); - gamma[i][0] = byte(f * 255.0); - gamma[i][1] = byte(f * 240.0); - gamma[i][2] = byte(f * 220.0); + for(i=0; i<256; i++) { + f = pow((float)i / 255.0, 2.8); + gamma[i][0] = (byte)(f * 255.0); + gamma[i][1] = (byte)(f * 240.0); + gamma[i][2] = (byte)(f * 220.0); } } +// Open and return serial connection to Arduino running LEDstream code. This +// attempts to open and read from each serial device on the system, until the +// matching "Ada\n" acknowledgement string is found. Due to the serial +// timeout, if you have multiple serial devices/ports and the Arduino is late +// in the list, this can take seemingly forever...so if you KNOW the Arduino +// will always be on a specific port (e.g. "COM6"), you might want to comment +// out most of this to bypass the checks and instead just open that port +// directly! (Modify last line in this method with the serial port name.) + +Serial openPort() { + String[] ports; + String ack; + int i, start; + Serial s; + + ports = Serial.list(); // List of all serial ports/devices on system. + + for(i=0; i= 4) && + ((ack = s.readString()) != null) && + ack.contains("Ada\n")) { + return s; // Got it! + } + } + // Connection timed out. Close port and move on to the next. + s.stop(); + } + + // Didn't locate a device returning the acknowledgment string. + // Maybe it's out there but running the old LEDstream code, which + // didn't have the ACK. Can't say for sure, so we'll take our + // changes with the first/only serial device out there... + return new Serial(this, ports[0], 115200); +} + + +// PER_FRAME PROCESSING ------------------------------------------------------ + void draw () { - BufferedImage desktop; - PImage screenShot; - int i, j, c; + BufferedImage img; + int d, i, j, o, c, weight, rb, g, sum, deficit, s2; + int[] pxls, offs; - // Capture screen - try { - desktop = new Robot(gs[0]).createScreenCapture(bounds); - } - catch(AWTException e) { - System.err.println("Screen capture failed."); - return; - } - screenShot = new PImage(desktop); // Convert Image to PImage - screenShot.loadPixels(); // Make pixel array readable - - // Downsample blocks of interest into LED output buffer: - preview.loadPixels(); // Also display in preview image - j = 6; // Data follows LED header / magic word - for(i = 0; i < coord.length; i++) { // For each LED... - c = block(screenShot, coord[i][0], coord[i][1]); - buffer[j++] = gamma[(c >> 16) & 0xff][0]; - buffer[j++] = gamma[(c >> 8) & 0xff][1]; - buffer[j++] = gamma[ c & 0xff][2]; - preview.pixels[coord[i][1] * arrayWidth + coord[i][0]] = c; - } - preview.updatePixels(); - - // Show preview image - scale(imgScale); - image(preview,0,0); - println(frameRate); - - port.write(buffer); -} - -// This method computes a single pixel value filtered down from a rectangular -// section of the screen. While it would seem tempting to use the native -// image scaling in Processing, in practice this didn't look very good -- the -// extreme downsampling, coupled with the native interpolation mode, results -// in excessive scintillation with video content. An alternate approach -// using the Java AWT AreaAveragingScaleFilter filter produces wonderfully -// smooth results, but is too slow for filtering full-screen video. So -// instead, a "manual" downsampling method is used here. In the interest of -// speed, it doesn't actually sample every pixel within a block, just a 20x20 -// grid...the results still look reasonably smooth and are handled quickly -// enough for video. Scaling the full screen image also wastes a lot of -// cycles on center pixels that are never used for the LED output; this -// method gets called only for perimeter pixels. Even then, you may want to -// set your monitor for a lower resolution before running this sketch. - -color block(PImage image, int x, int y) { - int c, r, g, b, row, col, rowOffset; - float startX, curX, curY, incX, incY; - - startX = float(screen.width / arrayWidth ) * - (float(x) + (0.5 / float(samples))); - curY = float(screen.height / arrayHeight) * - (float(y) + (0.5 / float(samples))); - incX = float(screen.width / arrayWidth ) / float(samples); - incY = float(screen.height / arrayHeight) / float(samples); - - r = g = b = 0; - for(row = 0; row < samples; row++) { - rowOffset = int(curY) * screen.width; - curX = startX; - for(col = 0; col < samples; col++) { - c = image.pixels[rowOffset + int(curX)]; - r += (c >> 16) & 0xff; - g += (c >> 8) & 0xff; - b += c & 0xff; - curX += incX; + // Capture each screen in the displays array. Full screens are captured, + // even though typically only the perimeter is used. Logically it might + // seem that capturing just the sampled areas would be faster, but in + // practice this is not the case...there's a certain latency associated with + // each capture action, and so one large block capture generally finishes + // sooner than a multitude of smaller ones. + for(d=0; d> 24) & 0xff) * weight + prevColor[i][0] * fade) >> 8); + ledColor[i][1] = (short)(((( g >> 16) & 0xff) * weight + prevColor[i][1] * fade) >> 8); + ledColor[i][2] = (short)((((rb >> 8) & 0xff) * weight + prevColor[i][2] * fade) >> 8); + + // Boost pixels that fall below the minimum brightness + sum = ledColor[i][0] + ledColor[i][1] + ledColor[i][2]; + if(sum < minBrightness) { + if(sum == 0) { // To avoid divide-by-zero + deficit = sum / 3; // Spread equally to R,G,B + ledColor[i][0] += deficit; + ledColor[i][1] += deficit; + ledColor[i][2] += deficit; + } else { + deficit = minBrightness - sum; + s2 = sum * 2; + // Spread the "brightness deficit" back into R,G,B in proportion to + // their individual contribition to that deficit. Rather than simply + // boosting all pixels at the low end, this allows deep (but saturated) + // colors to stay saturated...they don't "pink out." + ledColor[i][0] += deficit * (sum - ledColor[i][0]) / s2; + ledColor[i][1] += deficit * (sum - ledColor[i][1]) / s2; + ledColor[i][2] += deficit * (sum - ledColor[i][2]) / s2; + } + } + + // Apply gamma curve and place in serial output buffer + serialData[j++] = gamma[ledColor[i][0]][0]; + serialData[j++] = gamma[ledColor[i][1]][1]; + serialData[j++] = gamma[ledColor[i][2]][2]; + // Update pixels in preview image + preview[d].pixels[leds[i][2] * displays[d][1] + leds[i][1]] = + (ledColor[i][0] << 16) | (ledColor[i][1] << 8) | ledColor[i][2]; + } + + if(port != null) port.write(serialData); // Issue data to Arduino + + // Show live preview image(s) + scale(pixelSize); + for(i=d=0; d