// Watch Live LED Feed Online Now


//  Video of Finished Installation


Mayor's Office of Cultral Affairs Logo.png

 

5044 is currently installed at TechSquare Labs in Midtown Atlanta

Major support for this project is provided by the City of Atlanta Mayor’s Office of Cultural Affairs

 


// Construction

TSLai1.jpeg
TSLai2.jpeg

The construction of these frames was challenging, but exciting! I wanted to create a piece that would be interactive from both inside and outside the building as well as just as visually engaging. My previous LED work had all been one sided, and while this still had a "back" it's still engaging and less of an eyesore than other installations I've wired up. Prepping my files was very similar to my panels at The Shops, but instead of just having a layer for the front of the panels and one to hold the LEDs in place, they sandwich a clear piece of acrylic that suspends them in the window. I got them all cut in town with Collet and Bit again! 

Initial steps of attaching the front mirrored panels to the clear acrylic and the clear acrylic to the 7'x7' wood frames.

IMG_3544.JPG
IMG_3560.JPG
IMG_3611.JPG
IMG_3620.JPG

One thing I loved during the process but lost during the final installation is how the light reflected on the surfaces around it. You can bet I'm going to be exploring this more in future projects!

Mapping the LED strips was more difficult than I anticipated. You can see when you draw points along the curve they are not evenly spaced, but the LEDs on the strip are. You'd think this wouldn't be an issue, but because the curve intersects itself, attempting to simply map it like this made intersections way off. Luckily I had been messing around with Rhino/Grasshopper and they have a great function that will evenly divide a curve into equal segments and I could just export the altered X and Y positions back into Processing.

TSL strip.jpeg

Ya'll have no idea how worried I was about transporting these to TechSquare Labs. The clear acrylic still had a bit more flexibility than I wanted, but they all made it in one piece in the U-Haul, and fit through the door!

IMG_3671.JPG
IMG_3673.JPG

// Some of the Code

Bridging the LEDs and the web was a challenge for me, that took trying new things and giving up multiple times. Having Processing connect to my Heroku WebSocket Server wasn't working, and neither was sending the LED serial data in p5.js.

My workaround was having a browser window on the local computer that is connected to the WebSockets Server, as well as a local socket server that is using the p5.js Serial Port library and the Processing Websockets Library on the same local port.

Helpful Links:

 

The p5.SerialPort library is two parts, the library that runs in the browser, and more code that makes it all work by starting a local server on your computer to communicate with the serial devices. The write() function will send a socket from your p5.js sketch in the browser to this server, and the list() function will give your p5.js sketch an array of serial devices.

Instead of running the p5.SerialPort Server, I had my Processing sketch start a server with the Websockets Library on the same port (default is port 8081) and send messages that the p5.SerialPort library is expecting to receive. Since the list function works with an array of serial device names, I decided to send it a message replacing the names with the LED color values. It also receives messages using the write() function executed in the browser.

Below are the sections from Processing code that uses the WebSockets Library:

import websockets.*;
WebsocketServer ws;
/.../
void setup() {
 ws= new WebsocketServer(this,8081,"/");
}
/.../
void draw() {
/*
The p5.serialport library is expecting to recieve the list of serial ports as a socket in the following format:

{
"method": "list",
"data" : [THE DATA WE ARE SENDING]
}

*/
ws.sendMessage("{ \"method\": \"list\",\"data\": ["+join(str(wsLED),",")+"] }");
// wsLED is an array (5044*3) of integers with all the LED RGB values 
// str() is a function to convert these integers to Strings
// join(list,separator) is a function that takes creates a single string with all our of values seperated by columns
}
/.../
void webSocketServerEvent(String msg){
  /*
The message we recieve from p5.js will be formatted:
{"LED":[0,1,...,5044*3]}
*/
String message= msg.substring(8); //substring() will return a String starting at a new index (in this case where the numbers start)
LEDdata= int(split(message, ',')); //creates an array of RGB values for the LEDs by splitting our String at every ","
}

If you're curious/patient, here is the code that is running in the browser:

/*
My dirty ugly code, I should take the code that is written to run only on the installation computer's browser and separate that into a differnet page
*/
"use strict";
document.addEventListener('touchmove', function(e) {
  e.preventDefault(); //prevent scrolling on mobile
}, false);

var socket;
var convert = parseInt(getParameterByName('convert'));
var LEDs = []; //array of pixel colors to be sent to Processing Sketch
var circleHue = 0; 
var port = (process.env.PORT || 4000);
var table;
var xpos; //position of LEDs to send data
var ypos; //position of LEDs to send data
var xremap; //positions of live LEDs (remapped for Teensy board)
var yremap; //positions of live LEDs (remapped for Teensy board)
var sc; //scale the x and y positions 
var incomingData;
var loadedPixels;
var loaded;
var pg; //off screen graphics buffer
var mouseIsPressed1;
var agreed; //read the instructions on the page and clicked to continue
var serial;
var data;

function getParameterByName(name, url) {
  if (!url) url = window.location.href;
  name = name.replace(/[\[\]]/g, "\\$&");
  var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
    results = regex.exec(url);
  if (!results) return null;
  if (!results[2]) return '';
  return decodeURIComponent(results[2].replace(/\+/g, " "));

}

function preload() {
  table = loadTable('table1.csv', 'csv', 'header'); //positions of the LEDs, for some reason I couldn't get multiple tables to work so I put it all in one
  loadedPixels = loadJSON('/LEDdata.json'); // live LEDs from Installation
  loaded = true; //have we successfully loaded the JSON?
  mouseIsPressed1 = false;
  agreed = false;
}

function setup() {

  createCanvas(windowWidth, windowHeight);
  pixelDensity(1);
  colorMode(HSB, 255, 255, 255);
  background(0);
  sc = width / 660; //scale the positions of LEDs based on the canvas width
  socket = io.connect(port);
  socket.on('mouse', newDrawing);
  if (convert == 1) {
    serial = new p5.SerialPort(); // using a modification of this SerialPort library to communicate with local WebSockets server running on Installation Computer
  }
  xpos = [];
  ypos = [];
  xremap = [];
  yremap = [];
  incomingData = [];

  //read table to get positions for the LEDs, scaled to window
  for (let i = 0; i < 2100; i++) {
    xpos[i] = (table.getString(i, 1)) * sc;
    ypos[i] = (table.getString(i, 2) * -1) * sc; //my y-positions needed to be reflected
  }
  for (let i = 2100; i < 5044; i++) {
    xpos[i] = (table.getString(i, 1)) * sc;
    ypos[i] = (table.getString(i, 2) * -1 - 160) * sc; //my y-positions needed to be reflected and shifted 
  }

  //read table to get positions for the LEDs, scaled to window    
  for (let i = 0; i < 7200; i++) {
    xremap[i] = (table.getString(i + 5044, 1)) * sc;
    yremap[i] = (table.getString(i + 5044, 2)) * sc;
  }

  for (let i = 0; i < 7200 * 3; i++) {
    incomingData[i] = 0;
  }

  pg = createGraphics(width, height);
}


function newDrawing(data) { //show someone else's drawing on the canvas
  colorMode(HSB);
  fill(data.z, 255, 255);
  ellipse(data.x * sc, data.y * sc, 12 * sc, 12 * sc);
}


function draw() {
  if (agreed) {
    colorMode(RGB);
    if (convert == 1) { //code that is meant for installation's browser
      fill(0, 0, 0, 10);
      noStroke();
      rect(0, 0, width, height); //draw a rectangle to slowly erase drawings
      circleHue++; //cycle through hue

      if (circleHue >= 255) {
        circleHue = 0; //if hue is 255, make hue 0
      }

      if (frameCount % 2 === 0) { // every other frame send canvas pixel information to Installation 

        loadPixels(); //takes canvas pixels and loads into pixels[] with RGBA values
          
          //this is seperated into two blocks because I'm using both RGB and GRB LEDs
        for (let i = 0; i < 2100; i++) {
          LEDs[i * 3] = pixels[(int(ypos[i]) * width + int(xpos[i])) * 4]; //Red
          LEDs[i * 3 + 1] = pixels[(int(ypos[i]) * width + int(xpos[i])) * 4 + 1]; //Green
          LEDs[i * 3 + 2] = pixels[(int(ypos[i]) * width + int(xpos[i])) * 4 + 2]; //Blue
        }

        for (let i = 2100; i < 5044; i++) {
          LEDs[i * 3] = pixels[(int(ypos[i]) * width + int(xpos[i])) * 4 + 1]; //Green
          LEDs[i * 3 + 1] = pixels[(int(ypos[i]) * width + int(xpos[i])) * 4]; //Red
          LEDs[i * 3 + 2] = pixels[(int(ypos[i]) * width + int(xpos[i])) * 4 + 2]; //Blue
        }

        data = {
          LED: LEDs
        }

        serial.write(data); //send to local WebSockets server
        updatePixels();
          
      } else { // every other frame receive canvas pixel information from Installation 
          
        data = {
          LED: serial.serialportList // modified SerialPort library so Processing Installation is sending the live pixel data over local WebSockets server
        }
        socket.emit('convert', data); //send to web server
      }

    } else { //regular code that runs on everyone else's browsers

      fill(0, 0, 0, 20); 
      noStroke();
      rect(0, 0, width, height); //draw a rectangle to make drawings fade out

      circleHue++;
      if (circleHue >= 255) {
        circleHue = 0; 
      }
        
      if (loaded === true && !mouseIsPressed && !mouseIsPressed1) { 
          /*
          Only request new live pixel data if the last frame 
          finished loading and the mouse is not pressed. This 
          process is slow so disabling it while drawing keeps 
          that part running at an acceptable frame rate
          */
          
        loadedPixels = loadJSON('/LEDdata.json', drawPixels);
          /* 
          drawPixels (updating the buffer graphics) is a 
          callback function, so it only happens when the 
          JSON has finished loading
          */
      }

      image(pg, 0, 0); //draw the buffer graphic with live LED data

    }
  } else { // "Instructions/Loading page or something like that" 
    background(55);
    noStroke();
    fill(255);
    textFont('monospace', 24);
    textAlign(CENTER, CENTER);
    textSize(12 * sc);
    text("Live feed of LEDs being sent from TechSquare Labs, frame rate \nis dependent on your device and may run faster on a desktop.\n Click and drag in this window to draw on the display \n(live feed will not update while you are drawing)\n\n\nClick anywhere to continue", width / 2, height / 2);
  }
}

function mouseDragged() {
  if (!agreed) {
    background(0); //draw over initial instruction page
  }
  agreed = true; //have read the instructions and clicked to continue

  if (mouseY < 160 * sc) { //only draw in area where LEDs are
    colorMode(HSB);
    var data = {
      x: mouseX / sc, //scaled position
      y: mouseY / sc, //scaled position
      z: circleHue
    }
    fill(circleHue, 255, 255);
    ellipse(mouseX, mouseY, 12 * sc, 12 * sc); //draw a circle at mouse position

    socket.emit('mouse', data); //send WebSocket with position and color to other browsers
  }

}


function mousePressed() {
  if (!agreed) {
    background(0); //draw over initial instruction page
  }
  agreed = true; //have read the instructions and clicked to continue

  if (mouseY < 160 * sc) {  //only draw in area where LEDs are
    colorMode(HSB);
    var data = {
      x: mouseX / sc, //scaled position
      y: mouseY / sc, //scaled position
      z: circleHue
    }
    fill(circleHue, 255, 255);
    ellipse(mouseX, mouseY, 12 * sc, 12 * sc); //draw a circle at mouse position

    socket.emit('mouse', data); //send WebSocket with position and color to other browsers
  }
}

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  pg = createGraphics(width, height);
  background(0);
  if (convert != 1) {
    drawPixels(); //don't draw the live LED data from Installation in our browser window
  }
  sc = width / 660; //set scale with new dimensions
    
    //reset positions with new scale
  for (let i = 0; i < 2100; i++) {
    xpos[i] = (table.getString(i, 1)) * sc;
    ypos[i] = (table.getString(i, 2) * -1) * sc;
  }
  for (let i = 2100; i < 5044; i++) {
    xpos[i] = (table.getString(i, 1)) * sc;
    ypos[i] = (table.getString(i, 2) * -1 - 160) * sc;
  }

  for (let i = 0; i < 7200; i++) {
    xremap[i] = (table.getString(i + 5044, 1)) * sc;
    yremap[i] = (table.getString(i + 5044, 2)) * sc;
  }
}

// update the buffer graphic with the LEDs being sent from installation
function drawPixels() {
//  console.log("loaded new pixels");
  loaded = false; //haven't finished redrawing, don't load JSON again
  if (!mouseIsPressed && !mouseIsPressed1) { //only draw when mouse not pressed
    incomingData = loadedPixels;
    pg.noStroke();
    sc = width / 660;

    for (let i = 0; i < 7200; i++) {
      if (i % 1800 < 900) { // accounts for my wiring that includes both RGB and GRB LEDs
        pg.fill(incomingData[i * 3 + 1], //Green
          incomingData[i * 3 + 0], //Red
          incomingData[i * 3 + 2]); //Blue
        pg.ellipse(xremap[i], yremap[i], 3 * sc, 3 * sc);
      } else {
        pg.fill(incomingData[i * 3 + 0], //Red
          incomingData[i * 3 + 1], //Green
          incomingData[i * 3 + 2]); //Blue
        pg.ellipse(xremap[i], yremap[i] - 160 * sc, 3 * sc, 3 * sc);
      }

    }
  }
  loaded = true; //finished redrawing, ready to load JSON again
}

function touchStarted() {
  if (!agreed) {
    background(0); //draw over initial instruction page
  }
  mouseIsPressed1 = true; //added because default mouseIsPressed variable does not work with touch devices
  agreed = true; //have read the instructions and clicked to continue
}

function touchEnded() {
  mouseIsPressed1 = false; //added because default mouseIsPressed variable does not work with touch devices
}