The Altar – Experimental Art Installation
The Altar is an experimental generative art installation that emphasizes time and place. By displaying a unique color visualization that changes from day to day and from space to space, the piece emphasizes here-and-now. The art and designs are unique to the surrounding of that moment in time, and can never be recaptured or recreated.
Materials:
Arduino Uno
Breadboard
Seeed Humidity Barometer
Adafruit Ultimate GPS Breakout
SD card reader /microSD
Background/Initial Goals & Ideas:
At first we really were committed to a device that would be a fortune teller or track users entire day and output a "portrait of your day". The idea was that our device would clip onto your backpack and collect sensory data randomly throughout your day. The device would 'dock' back onto a port where the data would upload and print a visualization of your day. Having a mobile device that could easily transfer the sensors reads in a practical way quickly became quite the battle. After spending countless hours troubleshooting our SD, we transitioned towards a stationary device. We wanted the experience to be surprising as well as produce a printed takeaway. After searching the UW surplus we found the perfect furniture piece with a hole perfect for our button to fit through!
We used our Arduino to connect our Seeed humi barometer, Adafruit Ultimate GPS breakout, and our button together. Both the GPS and Humi/Barometer had a tremendous amount of data that we could potentially pull from them. We ended up focusing on GPS coordinates as well as humidity and temperature because we felt those were the most useful in framing a moment in time and space.
We had a tough time getting our sensors to work the way we needed out of the box. The first UNO we had was giving us funny reads and we ended up needing to swap it out for another mid-way through the project. The Adafruit ultimate GPS sensor was also giving us weird reads and would only connect outdoors. After inserting a watch battery, we found the GPS was able to get a fix much quicker and was able to remember the previous reads. Anyone looking to get the most out their GPS reader should invest in a battery, without it the reads are much less accurate and indoor use becomes near impossible.
The button was particularly tough to setup, as it required us to solder longer wires so that we could have the wires extend long enough to reach the hole on our housing. We laser cut a button plate which we strung the wires through and plugged into the breadboard below. The solder job was tough and required us to hot glue. The connection worked fine for awhile but quickly began short circuiting.
We had a tough time getting our sensors to work the way we needed out of the box. The first UNO we had was giving us funny reads and we ended up needing to swap it out for another mid-way through the project. The Adafruit ultimate GPS sensor was also giving us weird reads and would only connect outdoors. After inserting a watch battery, we found the GPS was able to get a fix much quicker and was able to remember the previous reads. Anyone looking to get the most out their GPS reader should invest in a battery, without it the reads are much less accurate and indoor use becomes near impossible.
The button was particularly tough to setup, as it required us to solder longer wires so that we could have the wires extend long enough to reach the hole on our housing. We laser cut a button plate which we strung the wires through and plugged into the breadboard below. The solder job was tough and required us to hot glue. The connection worked fine for awhile but quickly began short circuiting.
The sensors were coded through Arduino, but our images were generated using a processing sketch we compiled which generated a square with a circle at its center. The outer square's color was represented through an RGB value connected to the humidity and temperature of that unique situation. The inner circle was generated by mapping the latitude and longitude to RGB values as well.
We knew early on we wanted to the final deliverable to be functional but also artful and ominous, so we decided to build in a 'key' which exposed how the design was being generated, and what senses were suggesting the outcome.
The final design was output to Automator, a built Mac app which allows users to automate specific task flows. In our case we had our piece upload to a folder on the desktop, which was then automated to send directly to a deskjet printer in the back of the room.
We knew early on we wanted to the final deliverable to be functional but also artful and ominous, so we decided to build in a 'key' which exposed how the design was being generated, and what senses were suggesting the outcome.
The final design was output to Automator, a built Mac app which allows users to automate specific task flows. In our case we had our piece upload to a folder on the desktop, which was then automated to send directly to a deskjet printer in the back of the room.
Conclusion:
The project really opened our eyes to the possibilities of generative design and how coding could be used to help build design parameters. This is just a building block towards the possibilities of the collaboration between man and machine. We're excited to experiment with processing in the future and see how we could better create more complex and unique artwork with more sensing.
Code:
Arduino
//GPS Libraries
#include <Adafruit_GPS.h>
#include <SoftwareSerial.h>
//Environmental Libraries
#include "Seeed_BME280.h"
#include <Wire.h>
SoftwareSerial mySerial(3, 2);
Adafruit_GPS GPS(&mySerial);
BME280 bme280;
//Button Pin
const int buttonPin = 7;
boolean buttonState = 0;
// If you're using a GPS module:
// Connect the GPS Power pin to 5V
// Connect the GPS Ground pin to ground
// If using software serial (sketch example default):
// Connect the GPS TX (transmit) pin to Digital 3
// Connect the GPS RX (receive) pin to Digital 2
// If using hardware serial (e.g. Arduino Mega):
// Connect the GPS TX (transmit) pin to Arduino RX1, RX2 or RX3
// Connect the GPS RX (receive) pin to matching TX1, TX2 or TX3
// If you're using the Adafruit GPS shield, change
// SoftwareSerial mySerial(3, 2); -> SoftwareSerial mySerial(8, 7);
// and make sure the switch is set to SoftSerial
// If using hardware serial (e.g. Arduino Mega), comment out the
// above SoftwareSerial line, and enable this line instead
// (you can change the Serial number to match your wiring):
//HardwareSerial mySerial = Serial1;
// Set GPSECHO to 'false' to turn off echoing the GPS data to the Serial console
// Set to 'true' if you want to debug and listen to the raw GPS sentences.
#define GPSECHO false
// this keeps track of whether we're using the interrupt
// off by default!
boolean usingInterrupt = false;
void useInterrupt(boolean); // Func prototype keeps Arduino 0023 happy
void setup()
{
// connect at 115200 so we can read the GPS fast enough and echo without dropping chars
// also spit it out
Serial.begin(115200);
//Serial.println("Begin");
if(!bme280.init()){
Serial.println("Device error!");
}
pinMode(buttonPin, INPUT);
// 9600 NMEA is the default baud rate for Adafruit MTK GPS's- some use 4800
GPS.begin(9600);
// uncomment this line to turn on RMC (recommended minimum) and GGA (fix data) including altitude
GPS.sendCommand(PMTK_SET_NMEA_OUTPUT_RMCGGA);
// uncomment this line to turn on only the "minimum recommended" data
//GPS.sendCommand(PMTK_SET_NMEA_OUTPUT_RMCONLY);
// For parsing data, we don't suggest using anything but either RMC only or RMC+GGA since
// the parser doesn't care about other sentences at this time
// Set the update rate
GPS.sendCommand(PMTK_SET_NMEA_UPDATE_1HZ); // 1 Hz update rate
// For the parsing code to work nicely and have time to sort thru the data, and
// print it out we don't suggest using anything higher than 1 Hz
// Request updates on antenna status, comment out to keep quiet
GPS.sendCommand(PGCMD_ANTENNA);
// the nice thing about this code is you can have a timer0 interrupt go off
// every 1 millisecond, and read data from the GPS for you. that makes the
// loop code a heck of a lot easier!
useInterrupt(true);
delay(1000);
// Ask for firmware version
mySerial.println(PMTK_Q_RELEASE);
}
// Interrupt is called once a millisecond, looks for any new GPS data, and stores it
SIGNAL(TIMER0_COMPA_vect) {
char c = GPS.read();
// if you want to debug, this is a good time to do it!
#ifdef UDR0
if (GPSECHO)
if (c) UDR0 = c;
// writing direct to UDR0 is much much faster than Serial.print
// but only one character can be written at a time.
#endif
}
void useInterrupt(boolean v) {
if (v) {
// Timer0 is already used for millis() - we'll just interrupt somewhere
// in the middle and call the "Compare A" function above
OCR0A = 0xAF;
TIMSK0 |= _BV(OCIE0A);
usingInterrupt = true;
} else {
// do not call the interrupt function COMPA anymore
TIMSK0 &= ~_BV(OCIE0A);
usingInterrupt = false;
}
}
uint32_t timer = millis();
void loop() // run over and over again
{
// in case you are not using the interrupt above, you'll
// need to 'hand query' the GPS, not suggested :(
if (! usingInterrupt) {
// read data from the GPS in the 'main loop'
char c = GPS.read();
// if you want to debug, this is a good time to do it!
if (GPSECHO)
if (c) Serial.print(c);
}
// if a sentence is received, we can check the checksum, parse it...
if (GPS.newNMEAreceived()) {
// a tricky thing here is if we print the NMEA sentence, or data
// we end up not listening and catching other sentences!
// so be very wary if using OUTPUT_ALLDATA and trytng to print out data
//Serial.println(GPS.lastNMEA()); // this also sets the newNMEAreceived() flag to false
if (!GPS.parse(GPS.lastNMEA())) // this also sets the newNMEAreceived() flag to false
return; // we can fail to parse a sentence in which case we should just wait for another
}
// if millis() or timer wraps around, we'll just reset it
if (timer > millis()) timer = millis();
// approximately every x seconds or so, print out the current stats
if (millis() - timer > 1000) {
timer = millis(); // reset the timer
if (GPS.fix) {
buttonState = digitalRead(buttonPin);
Serial.print(buttonState);
Serial.print(",");
Serial.print(GPS.latitudeDegrees, 3);
Serial.print(",");
Serial.print(GPS.longitudeDegrees, 3);
Serial.print(",");
Serial.print(bme280.getHumidity());
Serial.print(",");
Serial.println(bme280.getTemperature());
}
else {
Serial.print("");
}
}
}
Processing
import processing.serial.*;
int s = second();
int m = minute();
int h = hour();
int d = day();
int mo = month();
int y = year(); // For saving a unique file name
float butt = 0; // button pressed
float lat = 0; // latitude value
float lon = 0; // longitude value
float humi = 0; // humididy in %
float temp = 0; // temp in degrees C
int color1 = 0;
int color2 = 0;
int color3 = 0;
int color4 = 0;
//int middle;
//int edge;
float inter;
Serial mySerial;
PFont myFont;
int lf = 10;
String inString;
void setup() {
size(640, 360);
background(255);
noStroke();
ellipseMode(RADIUS);
//col1 = color(color1, color2, color4);
//col2 = color(color3, color4, color4);
mySerial = new Serial(this, Serial.list()[23], 115200);
// don't generate a serialEvent() unless you get a newline character:
mySerial.bufferUntil(lf);
}
void draw() {
background(255);
noStroke();
rectMode(RADIUS); // Set ellipseMode to RADIUS
fill(color2,color3,color4); // Set fill to white
rect(320, 180, 100, 100); // Draw white ellipse using RADIUS mode
ellipseMode(CENTER); // Set ellipseMode to CENTER
fill(color1,color2,0); // Set fill to gray
ellipse(320, 182, 50, 50); // Draw gray ellipse using CENTER mode
textAlign(RIGHT);
textSize(10);
fill(color1,color2,0);
text(lat + "°", 200, 270);
text(abs(lon) + "°", 200, 280);
textAlign(LEFT);
textSize(10);
fill(color2,color3,color4);
text(humi + "%", 440, 270);
text(temp + "°C", 440, 280);
textAlign(CENTER);
textSize(10);
fill(0);
text(y + "-" + mo + "-" + d + " " + h + ":" + m + ":" + s, 320, 310);
if (butt == 1){
save("/Users/elikahn/Desktop/autoprint/"+("test"+y+"-"+mo+"-"+d+"-"+h+"-"+m+"-"+s+".png")); //Send to designated file on computer, Automator picks up slack
delay(10000);
}
}
void serialEvent(Serial mySerial) {
// get the ASCII string:
String inString = mySerial.readStringUntil(lf);
if (inString != null) {
// trim off any whitespace:
inString = trim(inString);
// split the string on the commas and convert the resulting substrings
// into an integer array:
float[] value = float(split(inString, ","));
// if the array has at least four elements, you know you got the whole
// thing. Put the numbers in the color variables:
if (value.length >= 5) {
butt = value[0];
lat = value[1];
lon = value[2];
humi = value[3];
temp = value[4];
color1 = int(map(lat, -90, 90, 0, 255));
color2 = int(map(lon, -180, 180, 0, 255));
color3 = int(map(humi, 0, 80, 0, 255));
color4 = int(map(temp, 0, 40, 0, 255));
}
}
}
/*
void drawGradient(float x, float y) {
int radius = 150;
for (int r = radius; r > 0; --r) {
inter = map(r, 0, 150, 0, 1);
color c = lerpColor(col1, col2, inter);
fill(c);
ellipse(x, y, r, r);
}
}
*/