From c9d6a285568c18ba47f2e19fa53520b7856400dd Mon Sep 17 00:00:00 2001 From: Phillip Burgess Date: Thu, 5 Jan 2017 23:40:37 -0800 Subject: [PATCH] Add Circuit Playground version (Arduino + Processing) --- .../LEDstream_CircuitPlayground.ino | 134 ++++++++ .../Adalight_CircuitPlayground.pde | 300 ++++++++++++++++++ 2 files changed, 434 insertions(+) create mode 100644 Arduino/LEDstream_CircuitPlayground/LEDstream_CircuitPlayground.ino create mode 100644 Processing/Adalight_CircuitPlayground/Adalight_CircuitPlayground.pde diff --git a/Arduino/LEDstream_CircuitPlayground/LEDstream_CircuitPlayground.ino b/Arduino/LEDstream_CircuitPlayground/LEDstream_CircuitPlayground.ino new file mode 100644 index 0000000..6aa3a8c --- /dev/null +++ b/Arduino/LEDstream_CircuitPlayground/LEDstream_CircuitPlayground.ino @@ -0,0 +1,134 @@ +// This is a pared-down version of the LEDstream sketch specifically +// for Circuit Playground. It is NOT a generic solution to NeoPixel +// support with Adalight! The NeoPixel library disables interrupts +// while issuing data...but Serial transfers depend on interrupts. +// This code works (or appears to work, it hasn't been extensively +// battle-tested) only because of the finite number of pixels (10) +// on the Circuit Playground board. With 10 NeoPixels, interrupts are +// off for about 300 microseconds...but if the incoming data rate is +// sufficiently limited (<= 60 FPS or so with the given number of +// pixels), things seem OK, no data is missed. Balancing act! + +// -------------------------------------------------------------------- +// This file is part of Adalight. + +// Adalight is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. + +// Adalight is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. + +// You should have received a copy of the GNU Lesser General Public +// License along with Adalight. If not, see +// . +// -------------------------------------------------------------------- + +#include "Adafruit_CircuitPlayground.h" + +static const uint8_t magic[] = { 'A','d','a' }; +#define MAGICSIZE sizeof(magic) +#define HEADERSIZE (MAGICSIZE + 3) +static uint8_t + buffer[HEADERSIZE], // Serial input buffer + bytesBuffered = 0; // Amount of data in buffer + +static const unsigned long serialTimeout = 15000; // 15 seconds +static unsigned long lastByteTime; + +void setup() { + CircuitPlayground.begin(); + CircuitPlayground.setBrightness(255); // LEDs full blast! + CircuitPlayground.strip.clear(); + CircuitPlayground.strip.show(); + + Serial.begin(38400); + + lastByteTime = millis(); // Initialize timers +} + +// Function is called when no pending serial data is available. +static boolean timeout( + unsigned long t, // Current time, milliseconds + int nLEDs) { // Number of LEDs + + // If no data received for an extended time, turn off all LEDs. + if((t - lastByteTime) > serialTimeout) { + CircuitPlayground.strip.clear(); + CircuitPlayground.strip.show(); + lastByteTime = t; // Reset counter + bytesBuffered = 0; // Clear serial buffer + return true; + } + + return false; // No timeout +} + +void loop() { + uint8_t i, hi, lo, byteNum; + int c; + long nLEDs, pixelNum; + unsigned long t; + + // HEADER-SEEKING BLOCK: locate 'magic word' at start of frame. + + // If any data in serial buffer, shift it down to starting position. + for(i=0; i= 0) { // Data received? + buffer[bytesBuffered++] = c; // Store in buffer + lastByteTime = t; // Reset timeout counter + } else { // No data, check for timeout... + if(timeout(t, 10000) == true) return; // Start over + } + } + + // Have a header's worth of data. Check for 'magic word' match. + for(i=0; i 0) + nLEDs = 256L * (long)hi + (long)lo + 1L; + bytesBuffered = 0; // Clear serial buffer + byteNum = 0; + + // DATA-FORWARDING BLOCK: move bytes from serial input to NeoPixels. + + for(pixelNum = 0; pixelNum < nLEDs; ) { // While more LED data is expected... + t = millis(); + if((c = Serial.read()) >= 0) { // Successful read? + lastByteTime = t; // Reset timeout counters + buffer[byteNum++] = c; // Store in data buffer + if(byteNum == 3) { // Have a full LED's worth? + CircuitPlayground.strip.setPixelColor(pixelNum++, + buffer[0], buffer[1], buffer[2]); + byteNum = 0; + } + } else { // No data, check for timeout... + if(timeout(t, nLEDs) == true) return; // Start over + } + } + + CircuitPlayground.strip.show(); +} + diff --git a/Processing/Adalight_CircuitPlayground/Adalight_CircuitPlayground.pde b/Processing/Adalight_CircuitPlayground/Adalight_CircuitPlayground.pde new file mode 100644 index 0000000..2ba4c93 --- /dev/null +++ b/Processing/Adalight_CircuitPlayground/Adalight_CircuitPlayground.pde @@ -0,0 +1,300 @@ +// IMPORTANT: change 'serialPortIndex' to make this work on your system. + +// This is a slightly pared-down version of Adalight specifically for +// Circuit Playground, configured for a single screen and 10 LEDs. + +// "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 Circuit +// Playground microcontroller running the accompanying LED streaming code. +// Screen capture adapted from code by Cedrik Kiefer (processing.org forum) + +// -------------------------------------------------------------------- +// This file is part of Adalight. + +// Adalight is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. + +// Adalight is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. + +// You should have received a copy of the GNU Lesser General Public +// License along with Adalight. If not, see +// . +// -------------------------------------------------------------------- + +import java.awt.*; +import java.awt.image.*; +import processing.serial.*; + +// CONFIGURABLE PROGRAM CONSTANTS -------------------------------------------- + +// This selects from the list of serial devices connected to the system. +// Use print(Serial.list()); to get a list of ports. Then, counting from 0, +// set this value to the index corresponding to the Circuit Playground port: + +static final byte serialPortIndex = 2; + +// For multi-screen systems, set this to the index (counting from 0) of the +// display which will have ambient lighting: + +static final byte screenNumber = 0; + +// 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 = 100; + +// 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 = 60; + +// Depending on many factors, it may be faster either to capture full +// screens and process only the pixels needed, or to capture multiple +// smaller sub-blocks bounding each region to be processed. Try both, +// look at the reported frame rates in the Processing output console, +// and run with whichever works best for you. + +static final boolean useFullScreenCaps = true; + +// PER-LED INFORMATION ------------------------------------------------------- + +// The Circuit Playground version of Adalight operates on a fixed 5x5 grid +// encompassing the full display. 10 elements from this grid correspond to +// the 10 NeoPixels on the Circuit Playground board. The following array +// contains the 2D coordinates of each NeoPixel within that 5x5 grid (0,0 is +// top left); board assumed facing away from display, with USB at bottom: +// .4.5. +// 3...6 +// 2...7 +// 1...8 +// .0.9. + +static final int leds[][] = new int[][] { + {1,4}, {0,3}, {0,2}, {0,1}, {1,0}, + {3,0}, {4,1}, {4,2}, {4,3}, {3,4} +}; + +// GLOBAL VARIABLES ---- You probably won't need to modify any of this ------- + +byte serialData[] = new byte[6 + leds.length * 3], + gamma[][] = new byte[256][3]; +short[][] ledColor = new short[leds.length][3], + prevColor = new short[leds.length][3]; +Robot bot; +Rectangle dispBounds, ledBounds[]; +int pixelOffset[][] = new int[leds.length][256], + screenData[]; +PImage preview; +Serial port; + +// INITIALIZATION ------------------------------------------------------------ + +void setup() { + GraphicsEnvironment ge; + GraphicsConfiguration[] gc; + GraphicsDevice[] gd; + int i, row, col; + int[] x = new int[16], y = new int[16]; + float f, range, step, start; + + this.registerMethod("dispose", this); + print(Serial.list()); // Show list of serial devices/ports + // Open serial port. Change serialPortIndex in the globals to + // select a different port: + port = new Serial(this, Serial.list()[serialPortIndex], 38400); + + // Initialize screen capture code for the display's dimensions. + if(useFullScreenCaps == false) ledBounds = new Rectangle[leds.length]; + ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); + gd = ge.getScreenDevices(); + + try { + bot = new Robot(gd[screenNumber]); + } + catch(AWTException e) { + System.out.println("new Robot() failed"); + exit(); + } + gc = gd[screenNumber].getConfigurations(); + dispBounds = gc[0].getBounds(); + dispBounds.x = dispBounds.y = 0; + preview = createImage(5, 5, RGB); + preview.loadPixels(); + + // 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. Number of samples is now fixed at 256; this allows for + // some crazy optimizations in the downsampling code. + for(i=0; i> 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 + 0.5); + gamma[i][1] = (byte)(f * 240.0 + 0.5); + gamma[i][2] = (byte)(f * 220.0 + 0.5); + } +} + +// PER_FRAME PROCESSING ------------------------------------------------------ + +void draw () { + BufferedImage img; + int i, j, o, c, weight, rb, g, sum, deficit, s2; + int[] pxls, offs; + + if(useFullScreenCaps == true ) { + img = bot.createScreenCapture(dispBounds); + // Get location of source pixel data + screenData = + ((DataBufferInt)img.getRaster().getDataBuffer()).getData(); + } + + weight = 257 - fade; // 'Weighting factor' for new frame vs. old + j = 6; // Serial led data follows header / magic word + + // This 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/Java, in practice this didn't look very + // good -- either too pixelated or too blurry, no happy medium. So + // instead, a "manual" downsampling is done here. In the interest of + // speed, it doesn't actually sample every pixel within a block, just + // a selection of 256 pixels spaced within the block...the results still + // look reasonably smooth and are handled quickly enough for video. + + for(i=0; i> 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 = minBrightness / 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.pixels[leds[i][1] * 5 + leds[i][0]] = 0xFF000000 | + (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 + preview.updatePixels(); + scale(40); + image(preview, 0, 0); + + println(frameRate); // How are we doing? + + // Copy LED color data to prior frame array for next pass + arraycopy(ledColor, 0, prevColor, 0, ledColor.length); +} + +// CLEANUP ------------------------------------------------------------------- + +// The DisposeHandler is called on program exit (but before the Serial library +// is shutdown), in order to turn off the LEDs (reportedly more reliable than +// stop()). Seems to work for the window close box and escape key exit, but +// not the 'Quit' menu option. Thanks to phi.lho in the Processing forums. + +void dispose() { + // Fill serialData (after header) with 0's, and issue to Arduino... + java.util.Arrays.fill(serialData, 6, serialData.length, (byte)0); + if(port != null) port.write(serialData); +} \ No newline at end of file