Cool Cloud by Team Peter: Final Documentation

The Intro to Physical Computing final project is to build a device that will help an older friend in way that is unique and specific to them; for us that person was Peter. As he lives in the penthouse of his apartment, the wind gusts on his balcony are really strong. This has caused him to have to tie down all the furniture out there as well as make sure that it is safe enough to go out there. When he babysits his granddaughter, she isn’t allowed on the balcony because it may be unsafe for her. Because of this, we decided to build him a personalized display of the wind speeds on his balcony. For more information about our meeting with him, check more on our process wordpress, and read on to learn more about our final work.

What We Built 

Our project measures the speed of the wind on his balcony and then displays this information in three different ways. The max speed is shown with a speedometer, the current speed is shown on a digital display, and the max wind speeds of the past 7-70 minutes are shown in a bar/line graph based on the setting Peter sets it as. This is all housed on a stand in a compact wooden box with a cloud and the 3 displays on the front with the switches on the right side panel.

 

Detail 1

Detail 2

Detail 3 Numbers

Detail 4 Side

After a windy day, Peter arrives home wondering just how fast it was so high up, so he heads over to Cool Cloud to see the max speed and the history throughout the last hour or so. He’s not surprised that it was 72+mph, but is happy that his suspicions were confirmed that it truly did get that fast on his balcony.

On a nice summer day, Peter wants to have a barbeque on his balcony. However, the news said that there would be mild winds, which might have meant anything up so high in the past. Now though, he knows what the wind is like on such days because of our Cool Cloud. He now knows that it will be perfectly safe and can now better plan future events outside based off what the weather says and the past speeds on such days. He can also finally let his granddaughter see the amazing view his apartment offers without fear of the wind.

How We Got Here

We started off by picking an off-the-shelf sensor that could detect wind speed. The anemometer we chose was a highly durable and weatherproof model that could measure up to 72 mph winds, which seemed ideal for our purposes.

The anemometer

Next, we started prototyping the indoor portion, figuring out what kind of visualizations we would have and how we would package them. We wanted to focus on the aesthetics of the indoor module, as that would set this project apart from a generic weather sensor.

Lasercut wooden grid for the LED bar graph

Detail shot of prototype LED strip

Functional prototype with a potentiometer dummy input

Showing our prototype to Peter

The biggest challenges at this point were all design choices. The scope of the project was pretty wide open, and we had to decide how we were going to present the information to Peter in a useful and appealing way. We spent a lot of time sketching, CADing, and prototyping different approaches.

For the final version, our challenges were to package everything nicely in a box and to weatherproof the outdoor module. This meant figuring out a secure way to mount the anemometer to Peter’s railing as well as building an enclosure that could keep the electronics from getting wet.

To secure the anemometer, we designed a 3-part clamping system that could bolt together on a T-joint of Peter’s railing.

Brainstorming a clamping system

Making the side panels

Clamp prototype

Creating the mounting box to be 3D printed

We 3D printed a final version of the clamping pieces. For the indoor portion, we still had to build our box and solder all of our electronics.

Lasercut pieces for the enclosure

Making the stand

Back panel with some soldered components

All components soldered and mounted

The toughest part of the final was the small last-minute adjustments we had to make to get everything to work smoothly. For instance, we had to cut some of our lasercut pieces to size because they were off by a little bit. We also added a hole to be able to access the Arduino’s USB port. It also took us a few tries to make the standoffs that the servo and LED matrices are mounted to. Technically, none of the components in this project were that challenging to use, and we were confident we could achieve full functionality. Integrating everything seamlessly and mechanically fabricating the whole thing was challenging.

Conclusions and Lessons Learned

The crit made us feel pretty good about our project as most people said it was pretty cool and worked well. One question we got was as to why our graph read from left to right with the left being the most recent instead of the other way around. This is interesting because we thought this was most logical as you read a book from left to right. It was brought up that in most bar graphs the most recent information is on the right and even for a book, the most recent information is technically on the right. This is a pretty sound argument that we never thought of and would have been a pretty quick fix if we chose to do so. One criticism we received from the written feedback is that our indoor component could have looked better, more specifically as two people pointed out that it may have looked better if we used “a different material than wood” because “you can see the burn marks.”  We liked the contrast the wood provided with the frosted acrylic and thought it would have a nice aesthetic on a table or wall; however, we could have chosen another material like a different color acrylic that would have had contrast without burn marks. Another concern was that “it will not survive PGH winter.” This is a concern that we also have about our outdoor component, but the anemometer is said to be able to take the weather and the screws should be able to hold up. One feature that Peter mentioned would have been nice is if he could plug his  computer in and see the history of it over its entire time running or if data could have also been transmitted to his phone. These sound like pretty interesting features, that we may have been able to include had we been informed of the desire earlier as well as if we had more time on the project. Other than that Peter was happy with our cloud display and really liked that we decided to include the toggle switch for him to go between a bar and line graph.

This was a really refreshing and fulfilling experience because we got to build something that we know will be used and is helpful to someone other than ourselves or simply to meet an assigned task. It was surprising how long it took us to figure out that this project is what Peter clearly wanted from the very beginning. Talking to Peter throughout the designing and building process was also a lot more seamless than originally anticipated. We do however think it would also be a good idea to ask if there were any additional features that he wanted us to include while making the final product (after the prototype meeting). This way we could discuss with him the feasibility of such things and maybe incorporate an aspect of it. Overall we are really happy with how our project turned out in that it worked exactly as we meant it too.

While we are happy with our project, there are still some things that we would have done differently if we were making it again. Hinging the back of the box so that it can be opened back up in case something somehow went wrong with the wiring last second is something to keep in mind for all our future projects and would have been very helpful for our peace of mind. Additionally, it would be a good idea to test everything together earlier in case there wasn’t as simple of a fix as we found and it were to take longer to figure it out.

Technical Details

/*
 * Cool Cloud
 * 
 * Alton Olson, Vicky Zhou, Seema Kamath
 * 
 * This is the transmitter that lives outside and sends
 * wind data to the receiver indoors.
 * 
 * Pins:
 * A0 - anemometer output
 * 7 - radio CE
 * 8 - radio CSN
 */

#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>

RF24 radio(7, 8); // CE, CSN
const byte address[6] = "00001";

void setup() {
  // set up radio
  radio.begin();
  radio.openWritingPipe(address);
  radio.setPALevel(RF24_PA_HIGH);
  radio.stopListening();
}

void loop() {
  // read anemometer value
  int reading = analogRead(A0);
  // send anemometer value
  radio.write(&reading, sizeof(reading));
  delay(1000);
}
/*
 * Cool Cloud
 * 
 * Alton Olson, Vicky Zhou, Seema Kamath
 * 
 * This is the receiver that lives indoors and displays
 * all the visualizations.
 * 
 * Pins:
 * A0 - slider pot for adjusting graph interval
 * 3 - switch for line graph/bar graph
 * 7 - radio CE
 * 8 - radio CSN
 * 9 - servo
 * 10 - LED strip
 */

#include <PololuLedStrip.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include "Adafruit_LEDBackpack.h"
#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>
#include <Adafruit_TiCoServo.h>

const int LED_COUNT = 49;
const int SERVO_PIN = 9;
const int LED_PIN = 10;
const int POT_PIN = A0;
const int SWITCH_PIN = 3;
const int RADIO_CE_PIN = 7;
const int RADIO_CSN_PIN = 8;
const int TENS_MATRIX_ADDR = 0x70;
const int ONES_MATRIX_ADDR = 0x71;
const int GRID_WIDTH = 7;
const int GRID_HEIGHT = 7;
const int DAY_MILLIS = 86400000;

// binary data for the 8x8 matrix images
const uint64_t TENS_IMAGES[] = {
  0x0000000000000000,
  0x7020202020203020,
  0xf810204080808870,
  0x7088808060808870,
  0x8080f88890a0c080,
  0x70888080780808f8,
  0x7088888878088870,
  0x10101020408080f8,
  0x7088888870888870,
  0x708880f088888870
};
const uint64_t ONES_IMAGES[] = {
  0x0e1111111111110e,
  0x0e04040404040604,
  0x1f0204081010110e,
  0x0e1110100c10110e,
  0x10101f1112141810,
  0x0e1110100f01011f,
  0x0e1111110f01110e,
  0x020202040810101f,
  0x0e1111110e11110e,
  0x0e11101e1111110e
};

// display one of the binary coded images
void displayImage(Adafruit_8x8matrix matrix, uint64_t image) {
  matrix.clear();
  for (int i = 0; i < 8; i++) {
    byte row = (image >> i * 8) & 0xFF;
    for (int j = 0; j < 8; j++) {
      matrix.drawPixel(i, 7 - j, bitRead(row, j));
    }
  }
  matrix.writeDisplay();
}

// convert from (x, y) to an LED index on the strip
int getLEDnum(int x, int y) {
  if (y % 2 == 0) {
    return (6 - x) + 7 * y;
  }
  else {
    return x + 7 * y;
  }
}

// convert from a speed to a bar graph height
int getYval(int currentSpeed) {
  if (currentSpeed <= 5) {
    return 0;
  } else if (currentSpeed <= 15) {
    return 1;
  } else if (currentSpeed <= 25) {
    return 2;
  } else if (currentSpeed <= 35) {
    return 3;
  } else if (currentSpeed <= 45) {
    return 4;
  } else if (currentSpeed <= 55) {
    return 5;
  } else {
    return 6;
  }
}

// color coding (red, yellow, green)
rgb_color getColor(int y) {
  if (y == 6) {
    return rgb_color(255,0,0);
  } else if (y == 5) {
    return rgb_color(255,0,0);
  } else if (y == 4) {
    return rgb_color(255,255,0);
  } else if (y == 3) {
    return rgb_color(255,255,0);
  } else if (y == 2) {
    return rgb_color(255,255,0);
  } else if (y == 1) {
    return rgb_color(0,255,0);
  } else {
    return rgb_color(0,255,0);
  }
}

// global variables
RF24 radio(RADIO_CE_PIN, RADIO_CSN_PIN);
const byte address[6] = "00001";

Adafruit_TiCoServo servo;
PololuLedStrip<LED_PIN> ledStrip;
rgb_color colors[LED_COUNT];

Adafruit_8x8matrix tensMatrix = Adafruit_8x8matrix();
Adafruit_8x8matrix onesMatrix = Adafruit_8x8matrix();

// history for bar graph
int maxRecentVals[GRID_WIDTH];
// toggle for line graph/bar graph
bool isLineGraph = false;
// interval to shift bar graph
unsigned long graphShiftInterval = 1000; // in ms
// timer for bar graph shift
unsigned long loopStartTime;
// variables to track peak speed
int currentSpeed, peakcurrentSpeed;
unsigned long peakcurrentSpeedTime;
// servo angle
int motorAngle = 0;

void setup() {
  Serial.begin(9600);
  pinMode(POT_PIN, INPUT);
  pinMode(SWITCH_PIN, INPUT_PULLUP);
  servo.attach(SERVO_PIN);

  tensMatrix.begin(TENS_MATRIX_ADDR);
  onesMatrix.begin(ONES_MATRIX_ADDR);

  loopStartTime = millis();

  radio.begin();
  radio.openReadingPipe(0, address);
  radio.setPALevel(RF24_PA_HIGH);
  radio.startListening();

  for (int i = 0; i < GRID_WIDTH; i++) maxRecentVals[i] = 0;
}

void loop() {
  // get radio input from transmitter
  if (radio.available()) {
    int anemometerVal = 77;
    radio.read(&anemometerVal, sizeof(anemometerVal));
    // convert raw analog value to wind speed in mph
    currentSpeed = map(anemometerVal, 77, 412, 0, 72);
  }
  // track daily peak speed
  if (currentSpeed > peakcurrentSpeed || millis() - peakcurrentSpeedTime > DAY_MILLIS) {
    peakcurrentSpeed = currentSpeed;
    peakcurrentSpeedTime = millis();
  }
  // get graph shift interval
  int potVal = analogRead(POT_PIN);
  // map to 1 minute - 10 minute range
  unsigned long graphShiftInterval = map(potVal, 0, 1023, 60000, 600000);

  int prevMotorAngle = motorAngle;
  // convert speed to angle
  motorAngle = map(peakcurrentSpeed, 0, 70, 15, 180);
  // only write to servo on angle change
  if (motorAngle != prevMotorAngle) servo.write(motorAngle);

  // shift graph
  if (millis() - loopStartTime > graphShiftInterval) {
    loopStartTime = millis();
    for (int i = GRID_WIDTH - 1; i > 0; i--) {
      maxRecentVals[i] = maxRecentVals[i - 1];
    }
    maxRecentVals[0] = 0;
  }
  // continuously update newest graph column
  maxRecentVals[0] = max(currentSpeed, maxRecentVals[0]);

  // clear LED strip
  for (int i = 0; i < GRID_WIDTH * GRID_HEIGHT; i++)
    colors[i] = rgb_color(0, 0, 0);

  // display bar graph
  isLineGraph = digitalRead(SWITCH_PIN);
  for (int x = 0; x < GRID_WIDTH; x++) {
    int val = maxRecentVals[x];
    if (isLineGraph) {
      int y = getYval(maxRecentVals[x]);
      colors[getLEDnum(x,y)] = getColor(y);
    } else {
      for (int y = 0; y <= getYval(val); y++) {
        colors[getLEDnum(x,y)] = getColor(y);
      }
    }
  }
  ledStrip.write(colors, LED_COUNT);

  // display instantaneous reading on 8x8 LEDs
  int tensIndex = (currentSpeed / 10) % 10;
  int onesIndex = currentSpeed % 10;
  displayImage(tensMatrix, TENS_IMAGES[tensIndex]);
  displayImage(onesMatrix, ONES_IMAGES[onesIndex]);
}

design files