Smart Water Bottle (with helpful reminders)

Description:

This device is meant to help remind the user how much water they have in their water bottle and give them helpful suggestions to drink water throughout the day. The main functionality of the system is a water sensor made of various wire endpoints on the inside of the water bottle that conduct electricity when they come in contact with the water. This signal is then read by the microcontroller which then calculates water level based on the number of sensor readings that are high. While the resolution of this sensor is only 7, this is all that is necessary for the 7 pixel display showing the water level. In addition to this main sensor, there is also an IMU on board sensing the current acceleration of the water bottle, a buzzer, and the LED light bar. Starting with the LED light bar, this is made of 7 LED pixels that indicate various things to the user including water level and when the user needs to drink water. The IMU on board is used for detecting when the bottle is at rest and thus the water level reading will be accurate. Finally, the buzzer acts in tandem with the light bar for indicating to the user when they need to drink water. All of these components add up to a device that is effectively able to let the user know how much water is in their water bottle and when they get dehydrated.

Videos:

Digital Water Level Sensor Testing

Analog Water Level Sensor Testing

Buzzer Testing

Final Demo

Images:

Side view of new digital water level sensor
Side view of LED light bar indicator mounted to the side of the water bottle
Image of final electronics

Process:

LED light bar used for indication of water level
Image of original water sensor that was too small for the whole bottle
Overall image of first prototype with original analog water sensor
LED panel backside

Code:

#include <Adafruit_NeoPixel.h>
#include <Wire.h>
#include <MPU6050.h>

#define NEOPIXEL_PIN  22
#define WATER_PIN     19
#define NUM_PIXELS    7
#define SENSOR_PIN0   38
#define SENSOR_PIN1   39
#define SENSOR_PIN2   40
#define SENSOR_PIN3   41
#define SENSOR_PIN4   14
#define SENSOR_PIN5   15
#define SENSOR_PIN6   16

int currWaterLevel = 0;
int prevWaterLevel = 0;

int currLightLevel = 0;
int prevLightLevel = 0;

int mod = (int)1024.0 / NUM_PIXELS;
Adafruit_NeoPixel pixels(NUM_PIXELS, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800);

int waterSensors[7] = {SENSOR_PIN0, SENSOR_PIN1, SENSOR_PIN2, SENSOR_PIN3, SENSOR_PIN4, SENSOR_PIN5, SENSOR_PIN6};

//Timer Variables
unsigned long waitCheckWaterLevel = 500;          //Check water level every 0.5sec
unsigned long waitCheckDehydration = 10000; //Alert dehydration every 30 minutes
unsigned long waitCheckAccel = 1000;              //Update MOVING veriable after 1sec of sustained action

unsigned long currTime = 0;

//Accelermoeter variables
double accelThresh = 100000;
double absAccel = 0;
double prevAbsAccel = 0;

int numDataPoints = 100;

bool MOVING = false; //Tracks the motion of the bottle
bool PREV_MOVING = false;
bool CHECK_WATER = false;
bool DEHYDRATED = false;
bool ACCEL_WAIT_CHECK = false;
//--> false if bottle is stationary
//--> true if bottle is moving

int toneFreq = 349;

MPU6050 mpu;


void setup() {

  Serial.begin(115200);

  pixels.begin();

  // Initialize MPU6050
  Serial.println("Initialize MPU6050");
  while (!mpu.begin(MPU6050_SCALE_2000DPS, MPU6050_RANGE_2G))
  {
    Serial.println("Could not find a valid MPU6050 sensor, check wiring!");
    delay(500);
  }

  // If you want, you can set gyroscope offsets
  mpu.setAccelOffsetX(40);
  mpu.setAccelOffsetY(40);
  mpu.setAccelOffsetZ(40);

  // Calibrate gyroscope. The calibration must be at rest.
  // If you don't want calibrate, comment this line.
  mpu.calibrateGyro();

  // Set threshold sensivty. Default 3.
  // If you don't want use threshold, comment this line or set 0.
  mpu.setThreshold(3);

  // Check settings
  checkSettings();

}

void loop() {
  //Read sensors
  Vector rawGyro = mpu.readRawGyro();
  currTime = millis();

  double x = rawGyro.XAxis;
  double y = rawGyro.YAxis;
  double z = rawGyro.ZAxis;

  absAccel = sqrt(x * x + y * y + z * z);
  absAccel = updateAccel(absAccel);

  currWaterLevel = readWaterLevel();

  checkMoving(absAccel);
  checkDehydrated();

  if (!MOVING && !DEHYDRATED) {
    showWaterLevel(currWaterLevel);
  }
  

  if (DEHYDRATED) {
    static unsigned long tStart = 0;
    static bool FLASH = true;
    if ((currTime - tStart) > 500) {
      pixels.clear();
      if (FLASH) {
        for (int i = 0; i < NUM_PIXELS; i++) {
          pixels.setPixelColor(i, pixels.Color(100, 0, 0));
        }
        FLASH = false;
        tone(12, toneFreq);
        
      }
      else {
        Serial.print("Hi");
        for (int i = 0; i < NUM_PIXELS; i++) {
          pixels.setPixelColor(i, pixels.Color(0, 0, 0));
        }
        FLASH = true;
        tone(12, 0);
      }
      pixels.show();
      tStart = currTime;
      Serial.print(FLASH);
    }
    
  }

  prevWaterLevel = currWaterLevel;
  prevAbsAccel = absAccel;
  PREV_MOVING = MOVING;

  Serial.println();
}

int readWaterLevel() {
  int waterLevel = 0;

  for (int i = 0; i < sizeof(waterSensors) / sizeof(waterSensors[0]); i++) {
    waterLevel += digitalRead(waterSensors[i]);
  }

  return waterLevel;
}

void showWaterLevel(int level) {
  pixels.clear();
  for (int i = 0; i < level; i++) {
    pixels.setPixelColor(i, pixels.Color(0, 0, 50));
  }
  pixels.show();
}

void checkMoving(double accel) {
  static unsigned long prevCheckTime = 0;

  bool ACCEL_WAIT_CHECK = false;
  bool ABOVE_THRESH = absAccel > accelThresh;

  //Start timer for passing absAccel thresh
  if (MOVING && ABOVE_THRESH || !MOVING && !ABOVE_THRESH) {
    
    ACCEL_WAIT_CHECK = abs(currTime - prevCheckTime) < waitCheckAccel;
  }
  else {
    prevCheckTime = currTime;
    ACCEL_WAIT_CHECK = false;
  }

  //Only update MOVING if timer is up
  if (ACCEL_WAIT_CHECK) {
    MOVING = !ABOVE_THRESH;
  }

}

void checkDehydrated() {
  static unsigned long prevCheckTime = 0;

  if (!DEHYDRATED) {
    DEHYDRATED = abs(currTime - prevCheckTime) > waitCheckDehydration;
  }
  else if (prevWaterLevel < currWaterLevel || PREV_MOVING != MOVING) {
    DEHYDRATED = false;
  }
  else {
    prevCheckTime = currTime;
  }
}

double updateAccel(double newReading) {
  static double dataArray[100];

  double sum = 0;

  for (int i = numDataPoints - 1; i >= 0; i--) {
    if (i == 0) {
      dataArray[i] = newReading;
    }
    else {
      dataArray[i] = dataArray[i - 1];
    }
    sum += dataArray[i];
  }
  sum = sum / numDataPoints;
  return sum;
}

void checkSettings()
{
  //  Serial.println();

  Serial.print(" * Sleep Mode:        ");
  Serial.println(mpu.getSleepEnabled() ? "Enabled" : "Disabled");

  Serial.print(" * Clock Source:      ");
  switch (mpu.getClockSource())
  {
    case MPU6050_CLOCK_KEEP_RESET:     Serial.println("Stops the clock and keeps the timing generator in reset"); break;
    case MPU6050_CLOCK_EXTERNAL_19MHZ: Serial.println("PLL with external 19.2MHz reference"); break;
    case MPU6050_CLOCK_EXTERNAL_32KHZ: Serial.println("PLL with external 32.768kHz reference"); break;
    case MPU6050_CLOCK_PLL_ZGYRO:      Serial.println("PLL with Z axis gyroscope reference"); break;
    case MPU6050_CLOCK_PLL_YGYRO:      Serial.println("PLL with Y axis gyroscope reference"); break;
    case MPU6050_CLOCK_PLL_XGYRO:      Serial.println("PLL with X axis gyroscope reference"); break;
    case MPU6050_CLOCK_INTERNAL_8MHZ:  Serial.println("Internal 8MHz oscillator"); break;
  }

  //  Serial.print(" * Gyroscope:         ");
  switch (mpu.getScale())
  {
    case MPU6050_SCALE_2000DPS:        Serial.println("2000 dps"); break;
    case MPU6050_SCALE_1000DPS:        Serial.println("1000 dps"); break;
    case MPU6050_SCALE_500DPS:         Serial.println("500 dps"); break;
    case MPU6050_SCALE_250DPS:         Serial.println("250 dps"); break;
  }

  Serial.print(" * Gyroscope offsets: ");
  Serial.print(mpu.getGyroOffsetX());
  Serial.print(" / ");
  Serial.print(mpu.getGyroOffsetY());
  Serial.print(" / ");
  Serial.println(mpu.getGyroOffsetZ());

  Serial.println();
}

Electrical Schematic:

 

 

Visual Crit – Jud

Problem:

Many rooms in the basements of buildings are very depressing and hard to work in because of the lack of windows and access to nature. Looking out the window is a very comforting and calming action that one can take for granted when working on floors where there is plenty of access to windows. Not only being able to look out the window and see what the weather is currently, but also being able to see nature outside is very helpful to productivity. In order to solve this problem, this project attempts to make a virtual window that allows you to see the outside environment and also interact with it. The p5.js script reads the weather of the desired location and inputs that into the script which then decides what to do based on what the weather is. For example, if it’s raining outside, the visual feedback will be a rain simulation with the rain droplets accumulating at the bottom to show how much it has actually rained. The same thing happens with snow and other weather events that have a physical element falling to the ground. In addition to the simulation of the weather, the device will have a gyroscope inside reading the current tilt angle and sending that back into the p5 script. With this tilt information, the accumulated physical weather element will then move around the screen to be level with the ground.

The following images and videos show part of the system working with light rain, heavy rain, and snow. The current script is able to read the weather and report back to the user what it is outside. The simulation of the physical weather elements are also able to move around corresponding to the tilt angle accumulating at the bottom until the weather type changes.

Images:

Image of snow weather
Physical build of gyroscope sensor and teensy

Videos:

Video of Snow

Video of Light Rain

Video of Heavy Rain

Code:

Arduino

/*
    MPU6050 Triple Axis Gyroscope & Accelerometer. Simple Gyroscope Example.
    Read more: http://www.jarzebski.pl/arduino/czujniki-i-sensory/3-osiowy-zyroskop-i-akcelerometr-mpu6050.html
    GIT: https://github.com/jarzebski/Arduino-MPU6050
    Web: http://www.jarzebski.pl
    (c) 2014 by Korneliusz Jarzebski
*/

#include <Wire.h>
#include <MPU6050.h>

MPU6050 mpu;

double zPrev = 0;

void setup()
{
  Serial.begin(115200);

  // Initialize MPU6050
  Serial.println("Initialize MPU6050");
  while (!mpu.begin(MPU6050_SCALE_2000DPS, MPU6050_RANGE_2G))
  {
    Serial.println("Could not find a valid MPU6050 sensor, check wiring!");
    delay(500);
  }

  // If you want, you can set gyroscope offsets
     mpu.setAccelOffsetX(40);
     mpu.setAccelOffsetY(40);
     mpu.setAccelOffsetZ(40);

  // Calibrate gyroscope. The calibration must be at rest.
  // If you don't want calibrate, comment this line.
  mpu.calibrateGyro();

  // Set threshold sensivty. Default 3.
  // If you don't want use threshold, comment this line or set 0.
  mpu.setThreshold(3);

  // Check settings
  checkSettings();
}

void checkSettings()
{
//  Serial.println();

  Serial.print(" * Sleep Mode:        ");
  Serial.println(mpu.getSleepEnabled() ? "Enabled" : "Disabled");

  Serial.print(" * Clock Source:      ");
  switch (mpu.getClockSource())
  {
    case MPU6050_CLOCK_KEEP_RESET:     Serial.println("Stops the clock and keeps the timing generator in reset"); break;
    case MPU6050_CLOCK_EXTERNAL_19MHZ: Serial.println("PLL with external 19.2MHz reference"); break;
    case MPU6050_CLOCK_EXTERNAL_32KHZ: Serial.println("PLL with external 32.768kHz reference"); break;
    case MPU6050_CLOCK_PLL_ZGYRO:      Serial.println("PLL with Z axis gyroscope reference"); break;
    case MPU6050_CLOCK_PLL_YGYRO:      Serial.println("PLL with Y axis gyroscope reference"); break;
    case MPU6050_CLOCK_PLL_XGYRO:      Serial.println("PLL with X axis gyroscope reference"); break;
    case MPU6050_CLOCK_INTERNAL_8MHZ:  Serial.println("Internal 8MHz oscillator"); break;
  }

//  Serial.print(" * Gyroscope:         ");
  switch (mpu.getScale())
  {
    case MPU6050_SCALE_2000DPS:        Serial.println("2000 dps"); break;
    case MPU6050_SCALE_1000DPS:        Serial.println("1000 dps"); break;
    case MPU6050_SCALE_500DPS:         Serial.println("500 dps"); break;
    case MPU6050_SCALE_250DPS:         Serial.println("250 dps"); break;
  }

  Serial.print(" * Gyroscope offsets: ");
  Serial.print(mpu.getGyroOffsetX());
  Serial.print(" / ");
  Serial.print(mpu.getGyroOffsetY());
  Serial.print(" / ");
  Serial.println(mpu.getGyroOffsetZ());

  Serial.println();
}

void loop()
{
  Vector rawGyro = mpu.readRawAccel();
  Vector normGyro = mpu.readNormalizeAccel();

  //  Serial.print(" Xraw = ");
  //  Serial.print(rawGyro.XAxis);
  //  Serial.print(" Yraw = ");
  //  Serial.print(rawGyro.YAxis);
  //  Serial.print(" Zraw = ");
  //  Serial.println(rawGyro.ZAxis);

  //  Serial.print(" Xnorm = ");
  double x = rawGyro.XAxis;
  double z = rawGyro.ZAxis;
  
  if (z < 20000) {
    z = z + 65000;
  }
  z = z*0.8 + 0.2*zPrev;
//  Serial.print(78000 - z);
//  Serial.print("\t");
  
  z = (78000 - z)/(78000 - 30000)*180;
  z = z - 130;
  Serial.print(z);
//  Serial.print("\t");

  zPrev = z;

  Serial.println();

  delay(10);
}

P5.JS

var minWidth = 600;   //set min width and height for canvas
var minHeight = 400;
var width, height;    // actual width and height for the sketch
var squareWidth = 100;

let serial;
let latestData = "waiting for data";
var outData;

// A wind direction vector
let wind;
// Circle position
let position;
var weather = 'sn';
var weather_String = 'Snow';
var startHeight = 50;
var currColor;

var currWidth = 0;
var currHeight = 0;

let angle = 0;

let windAngle = 0.0;
let windMag = 0.0;

var startTime = 0;
var waitTime = 1000;
var rainAccumulated = 0;
var numDrops = 0;

let numBalls = 0;
let spring = 0.01;
let gravity = 0.03;
let friction = -0.2;
let balls = [];

function setup() {
  if (window.innerWidth > minWidth){
  width = window.innerWidth;
  } else {
    width = minWidth;
  }
  if (window.innerHeight > minHeight) {
    height = window.innerHeight;
  } else {
    height = minHeight;
  }
  //set up canvas
  createCanvas(width, height);
  noStroke();
  // Request the data from metaweather.com
  // let url = 'https://cors-anywhere.herokuapp.com/https://www.metaweather.com/api/location/2357536/';
  // loadJSON(url,gotWeather);
  // // Circle starts in the middle
  // position = createVector(width/2, height/2);
  // // wind starts as (0,0)
  // wind = createVector();
  // noStroke();
  // fill(255, 204);

  serial = new p5.SerialPort();

  serial.list();
  serial.open('/dev/tty.usbmodem114760901');

  serial.on('connected', serverConnected);

  serial.on('list', gotList);

  serial.on('data', gotData);

  serial.on('error', gotError);

  serial.on('open', gotOpen);

  serial.on('close', gotClose);

  angleMode(DEGREES);

}

function serverConnected() {
  print("Connected to Server");
 }
 
 function gotList(thelist) {
  print("List of Serial Ports:");
 
  for (let i = 0; i < thelist.length; i++) {
   print(i + " " + thelist[i]);
  }
 }
 
 function gotOpen() {
  print("Serial Port is Open");
 }
 
 function gotClose(){
  print("Serial Port is Closed");
  latestData = "Serial Port is Closed";
 }
 
 function gotError(theerror) {
  print(theerror);
 }
 
 function gotData() {
   let currentString = serial.readLine();
   trim(currentString);
   if (!currentString) return;
   console.log(currentString);
   latestData = Number(currentString);
   angle = latestData;
 }

function draw() {
  background(220);
  
  //Draw Text
  // translate(width/2, height/2);
  // rotate(angle);
  fill('black')
  textAlign(CENTER);
  textSize(50);
  text(weather_String, width/2, 60);
  textSize(20);
  var windText = 'Wind Speed: ' + windMag + '\t Wind Angle: ' + windAngle;
  text(windText, width/2, 100);
  text(latestData, width/2, 130);
  
  updateWeather();
  
  // angle = cos(millis()/1000);
  
  if (millis() - startTime > waitTime) {
    balls[numBalls] = new Ball(random(width), 0, random(20, 30), random(30, 50), numBalls, balls);
    numBalls +=1;
    startTime = millis();
  }
  
  balls.forEach(ball => {
    ball.collide();
    ball.move();
    ball.display();
  });
  
  
  // // This section draws an arrow pointing in the direction of wind
  // push();
  // translate(32, height - 32);
  // // Rotate by the wind's angle
  // rotate(wind.heading() + PI/2);
  // noStroke();
  // fill(255);
  // ellipse(0, 0, 48, 48);

  // stroke(45, 123, 182);
  // strokeWeight(3);
  // line(0, -16, 0, 16);

  // noStroke();
  // fill(45, 123, 182);
  // triangle(0, -18, -6, -10, 6, -10);
  // pop();
  
  // // Move in the wind's direction
  // position.add(wind);
  
  // stroke(0);
  // fill(51);
  // ellipse(position.x, position.y, 16, 16);

  // if (position.x > width)  position.x = 0;
  // if (position.x < 0)      position.x = width;
  // if (position.y > height) position.y = 0;
  // if (position.y < 0)      position.y = height;
}

function gotWeather(weather) {
  print(weather.title);
  let weather_today = weather.consolidated_weather[0]
  weather = weather_today.weather_state_abbr;
  weather_String = weather_today.weather_state_name;
  // print(weather);
  // print(weather_String);
  // Get the angle (convert to radians)
  windAngle = radians(Number(weather_today.wind_direction));
  // Get the wind speed
  windMag = Number(weather_today.wind_speed)/100;
  
  // Display as HTML elements
  // let temperatureDiv = createDiv(floor(weather_today.the_temp) + '&deg;C');
  // let windDiv = createDiv("WIND " + windmag + " <small>MPH</small>");
  
  // Make a vector
  wind = p5.Vector.fromAngle(angle);
}

function updateWeather() {
  switch(weather){
    case 'sn':
      waitTime = 300;
      fill('white')
      currWidth = 30;
      currHeight = 30;
      weather_String = 'Snow';
      break;

    case 'sl':
      weather_String = 'Sleet';
      break;
    case 'h':
      weather_String = 'Hail';
      break;
    case 't':
      weather_String = 'Thunderstorms';
      break;

    case 'hr':
      weather_String = 'Heavy Rain';
      waitTime = 100;
      fill(138, 183, 255)
      currWidth = 30;
      currHeight = 30;
      accumulate = true;
      break;

    case 'lr':
      weather_String = 'Light Rain';
      waitTime = 2000;
      fill(138, 183, 255)
      currWidth = 30;
      currHeight = 30;
      accumulate = true;
      break;

    case 's':
      weather_String = 'Showers';
      break;

    case 'hc':
      weather_String = 'Heavy Clouds';

      break;

    case 'lc':
      weather_String = 'Light Clouds';

      break;

    case 'c':
      weather_String = 'Clear';

      break;

    default:
      break;
  }
}

class Ball {
  constructor(xin, yin, win, hin, idin, oin) {
    this.x = xin;
    this.y = yin;
    this.vx = 0;
    this.vy = 0;
    this.w = win;
    this.h = hin
    this.id = idin;
    this.others = oin;
    // this.vxAdd = windMag*cos(windAngle);
    // this.vyAdd = windMag*sin(windAngle);
    this.vxAdd = 0;
    this.vyAdd = 0;
  }

  collide() {
    for (let i = this.id + 1; i < numBalls; i++) {
      // console.log(others[i]);
      let dx = this.others[i].x - this.x;
      let dy = this.others[i].y - this.y;
      let distance = sqrt(dx * dx + dy * dy);
      let minDist = this.others[i].w / 2 + this.w / 2;
      //   console.log(distance);
      //console.log(minDist);
      if (distance < minDist) {
        //console.log("2");
        let angle = atan2(dy, dx);
        let targetX = this.x + cos(angle) * minDist;
        let targetY = this.y + sin(angle) * minDist;
        let ax = (targetX - this.others[i].x) * spring;
        let ay = (targetY - this.others[i].y) * spring;
        this.vx -= ax;
        this.vy -= ay;
        this.others[i].vx += ax;
        this.others[i].vy += ay;
      }
    }
  }

  move() {
    var ygravity = gravity*cos(angle);
    var xgravity = gravity*sin(angle);
    this.vy += ygravity + this.vyAdd;
    this.vx += xgravity + this.vxAdd;
    this.x += this.vx;
    this.y += this.vy;
    if (this.x + this.w / 2 > width) {
      this.vxAdd = 0;
      // print("Hello");
      this.x = width - this.w / 2;
      this.vx *= friction;
    } else if (this.x - this.w / 2 < 0) {
      this.vxAdd = 0;
      this.x = this.w / 2;
      this.vx *= friction;
    }
    if (this.y + this.w / 2 > height) {
      this.vyAdd = 0;
      this.y = height - this.w / 2;
      this.vy *= friction;
    } else if (this.y - this.w / 2 < 0) {
      this.y = this.w / 2;
      this.vy *= friction;
    }
  }

  display() {
    ellipse(this.x, this.y, this.w, this.h);
  }
}

 

Virtual Window for Windowless Rooms Ideation

The idea for this project comes from an earlier project in this class from someone else involving sensing the weather outside and reporting it to the user. Where this project differs, however, is it will incorporate a visual representation of the weather inside a screen. The general functionality will include sensing the weather either through sensors or getting data from online and then interpreting that into visual representation that can either be displayed through p5.js or an LED screen controlled by a teensy. The two different styles will work the same way just with slightly different implementations.

Make It So Chapters 3, 4, & 5

Chapter 3:

Throughout the chapter, there were three things that were portrayed as critical to an interface’s functionality and perception. The first of these categories was font and its use to convey information through a screen. Font is super important because it conveys information to the user through it’s appearance. Fonts that are more block-like and all uppercase tend to be harder to read and mimic early style computers making the user seem more intelligent or knowledgeable. In addition, fonts that have more character like serif fonts are more readable and can be used to convey very important information that needs to be read. The size of the font is also important providing insight into the importance of the information. All of these characteristics add up to convey information about the importance and urgency of the words being displayed without the user even having to read them.

In addition to font, color is also stressed as a very important visual aspect when it comes to displays. One aspect mentioned throughout chapter 3 is the idea of the blue and glow phenomena that conveys futuristic text. This is interesting because whenever I hear blue and glow I think of hologram which is very futuristic highlighting the cultural implications of this color. In addition, colors like red on a display signify more urgent or error-like messages. While this isn’t always the case, red is described as having a sense of danger. This is an interesting interpretation because red in other cultures also signifies good luck or happiness which is generally thought of to be the opposite of danger. This difference in color perception is very interesting because it brings up the idea that it is pretty much impossible to create a perfectly perceptible user interface. While we can come to a general consensus on what means what, it is very hard to create a display that means the same thing regardless of culture especially when it comes to color schemes.

Finally, in addition to color and font size, one large aspect of displays is the user interaction through buttons or cursors. One example brought up in chapter 3 is the Star Trek LCARS interface that uses shadows to give the digital buttons on the screen a sort of depth and animation. This not only makes them feel more life-like but also gives feedback as to whether the button was pressed or not. These types of visual cues that are missed when translating from physical to digital interfaces are key in user interaction and is something that can contribute to a device being amazing or completely unusable.

Chapter 4:

Volumetric projections are very good at portraying information in such a way that humans are comfortable with. Since we see the world in 3D, VP’s allow the user to interact with the information in such a way that mimics their natural perspective. Where this can become an issue, however, is in the realness of the projection. As discussed in chapter 4, VP’s can be very tricky if they are made too realistic because the user might actually think there is something there when in reality there isn’t. While this isn’t all bad for most applications, when something that has to be physical for the safety of the user is made virtual (like a step or floor) this can create circumstances ready for injury.  In addition to safety and trickery, VP’s also have to portray the 3D information in such a way that looks natural and mimics the proportions of regular space. Otherwise the information might seem very abnormal and not as informative.

Chapter 5:

Gestural interfaces are very intuitive for the most part but also have the caveat of moving the entire body to do one single action. While there are seven distinct motions set by physical intuition but also movies and TV, these motions require sustained use of the arms at heights around heart level requiring lots of strength and endurance to keep up. This results in the user getting very tired after a small amount of use. So while gestural interfaces might seem cool, they are mostly just used for futuristic looking interfaces.

Good and Bad Visual Feedback

Doors:

Many doors that I have encountered are very poorly designed in that they don’t indicate which direction they are supposed to be opened or where they are hinged. While this might look good from an aesthetics stand point, it hinders the ability to quickly recognize which side of the door to push or pull and whether or not to push or pull. One example of a very poorly designed door are those entering the UC from the football field entrance. While they do a good job of showing where the door hits a stop and thus whether it will be a push or pull open, they hide their hinges very well and have no physical indicators to persuade users to one side of the door or another. This has on many occasions resulted in me walking right into the door and being immediately brought to a halt because it’s the wrong side. On doors with a turn handle or ones with a push to open bar that has a direction to is, the side to push or pull from is made much more clear.

Room Indicator for the Blind

Description:

The purpose of this device is to assist people in locating themselves when they enter a new room in their home or other location. The basic idea is that when someone enters a room, a speaker will relay what room they are entering. This is accomplished by tracking the user’s position within a house through IR break beam sensors. One set of IR break beams is installed at each doorway or opening where the user can transition from room to room. When a break is detected in one of the beams, the system checks the current sensor being tripped against the previous room the user was in and adjusts the current room accordingly. Additionally, each room has its own sound that plays indicating to the user what room they are entering upon walking in. Finally, the system sends the current room number to a p5.js script that shows a visual layout of the floor plan with the current room highlighted in green.

Images:

Videos:

Demo of physical build

Code:

 

/*
   Crit 2: Location Feedback w/Sound
   Judson Kyle
   judsonk

   Description:
*/

#include <Audio.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <SerialFlash.h>

AudioPlaySdWav           playWav1;
AudioMixer4              mixer1;         //xy=647,408
AudioOutputMQS           mqs1;           //xy=625,279
AudioConnection          patchCord1(playWav1, 0, mqs1, 0);
AudioConnection          patchCord2(playWav1, 1, mqs1, 1);

#define SDCARD_CS_PIN    BUILTIN_SDCARD
#define SDCARD_MOSI_PIN  11  // not actually used
#define SDCARD_SCK_PIN   13  // not actually used

#define TRIGGER0   17
#define TRIGGER1   18
#define TRIGGER2   19
#define TRIGGER3   20
#define TRIGGER4   21
#define TRIGGER5   33
#define TRIGGER6   33
#define TRIGGER7   33
#define TRIGGER8   33
#define TRIGGER9   33
#define TRIGGER10  33
#define TRIGGER11  33
#define TRIGGER12  33

const char* outside = "Outside.WAV";
const char* sunRoom = "Sun Room.WAV";
const char* livingRoom = "Living Room.WAV";
const char* bed1 = "Bedroom 1.WAV";
const char* bed2 = "Bedroom 2.WAV";
const char* bed3 = "Bedroom 3.WAV";
const char* kitchen = "Kitchen.WAV";
const char* laundryRoom = "Laundry Room.WAV";
const char* diningRoom = "Dining Room.WAV";
const char* bath1 = "Bathroom 1.WAV";
const char* bath2 = "Bathroom 2.WAV";
const char* hall1 = "Hallway 1.WAV";
const char* hall2 = "Hallway 2.WAV";

//char *roomNames[13] = { outside,    sunRoom,      livingRoom,
//                        bed1,       bed2,         bed3,
//                        kitchen,    laundryRoom,  diningRoom,
//                        bath1,      bath2,        hall1,
//                        hall2};

int triggerPins[13] = { TRIGGER0,   TRIGGER2,   TRIGGER3,   TRIGGER4,
                        TRIGGER4,   TRIGGER5,   TRIGGER6,   TRIGGER7,
                        TRIGGER8,   TRIGGER9,   TRIGGER10,  TRIGGER11,
                        TRIGGER12
                      };
int numTriggers = 13;
int currTriggerOn = 0;
int prevTriggerOn = -1;
int prevRoom = 0;
volatile int currRoom = 0;

unsigned long currTime = 0;
unsigned long debounceTime = 100;
volatile unsigned long startTime = 0;
volatile bool DEBOUNCE = false;

const char* currFileName = outside;

void setup(void)
{
  // Wait for at least 3 seconds for the USB serial connection
  Serial.begin (9600);

  AudioMemory(8);

  pinMode(TRIGGER0, INPUT);
  pinMode(TRIGGER1, INPUT);
  pinMode(TRIGGER2, INPUT);
  pinMode(TRIGGER3, INPUT);
  pinMode(TRIGGER4, INPUT);

  SPI.setMOSI(SDCARD_MOSI_PIN);
  SPI.setSCK(SDCARD_SCK_PIN);
  if (!(SD.begin(SDCARD_CS_PIN))) {
    // stop here, but print a message repetitively
    while (1) {
      Serial.println("Unable to access the SD card");
      delay(500);
    }
  }

}

void loop (void)
{
  currTime = millis();

  if (!DEBOUNCE && !playWav1.isPlaying()) {
    updateCurrRoom();
    DEBOUNCE = true;
    startTime = currTime;
  }

  if (currTriggerOn != prevTriggerOn) {
    updateCurrRoom();
  }

  if (prevRoom != currRoom) {
    if (playWav1.isPlaying()) {
      playWav1.togglePlayPause();
    }
    playWav1.play(currFileName);
    Serial.println(currRoom);
  }

  prevRoom = currRoom;
  prevTriggerOn = currTriggerOn;

  //Update debounce state
  DEBOUNCE = abs(currTime - startTime) < debounceTime;
}

void updateCurrRoom() {
  switch (currRoom) {
    case 0:
      if (digitalRead(TRIGGER0) > 0) {
        currRoom = 1;
        currFileName = sunRoom;
      }
      break;
    case 1:
      if (digitalRead(TRIGGER0) > 0) {
        currRoom = 0;
        currFileName = outside;
      }
      else if (digitalRead(TRIGGER1) > 0) {
        currRoom = 2;
        currFileName = livingRoom;
      }
      break;
    case 2:
      if (digitalRead(TRIGGER2) > 0) {
        currRoom = 8;
        currFileName = diningRoom;
      }
      else if (digitalRead(TRIGGER1) > 0) {
        currRoom = 1;
        currFileName = sunRoom;
      }
      break;
    case 3:
      if (digitalRead(TRIGGER3) > 0) {
        currRoom = 8;
        currFileName = diningRoom;
      }
      break;
    case 8:
      if (digitalRead(TRIGGER2) > 0) {
        currRoom = 2;
        currFileName = livingRoom;
      }
      else if (digitalRead(TRIGGER4) > 0) {
        currRoom = 9;
        currFileName = bath1;
      }
      else if (digitalRead(TRIGGER3) > 0) {
        currRoom = 3;
        currFileName = bed1;
      }
      break;
    case 9:
      if (digitalRead(TRIGGER4) > 0) {
        currRoom = 8;
        currFileName = diningRoom;
      }
      break;
    default:
      break;
  }
}

 

let serial;
let latestData = "waiting for data";
var outData;

var minWidth = 600;   //set min width and height for canvas
var minHeight = 400;
var width, height;    // actual width and height for the sketch
var squareWidth = 100;

var greenColor = '#64d65d';
var redColor = '#d6220c';

var currRoom = 0;

class Room {
  constructor(length, height, xOffset, yOffset, color, name) {
    this.scale = 40;
    this.length = length*this.scale;
    this.height = height*this.scale;
    this.xOffset = xOffset*this.scale;
    this.yOffset = yOffset*this.scale;
    this.color = color; 
    this.name = name;
  }
  getX(canvasWidth) {
    return (canvasWidth - 12*this.scale)/2 + this.xOffset;
  }
  getY(canvasHeight) {
    return (canvasHeight + 17*this.scale)/2 - this.yOffset - this.height;
  }
}

let sunRoom = new Room(6, 2, 0, 0, redColor, "Sun Room");
let livingRoom = new Room(6, 5, 0, 2, redColor, "Living Room");
let dinningRoom = new Room(6, 4, 0, 7, redColor, "Dinning Room");
let bed1 = new Room(6, 7, 6, 0, redColor, "Bedroom 1");
let bed2 = new Room(6, 5, 6, 11, redColor, "Bedroom 2");
let bed3 = new Room(5, 3, 0, 11, redColor, "Bedroom 3");
let kitchen = new Room(4, 3, 2, 14, redColor, "Kitchen");
let laundryRoom = new Room(2, 1, 0, 16, redColor, "Laundry Room");
let bath1 = new Room(5, 4, 7, 7, redColor, "Bathroom 1");
let bath2 = new Room(2, 2, 0, 14, redColor, "Bathroom 2");
let hall1 = new Room(1, 4, 6, 7, redColor, "Hall 1");
let hall2 = new Room(1, 3, 5, 11, redColor, "Hall 2");
let outside = new Room(0, 0, 0, 0, redColor, "Outside");


var rooms = [ outside, sunRoom,  livingRoom, 
              bed1,     bed2,        bed3, 
              kitchen,  laundryRoom, dinningRoom,
              bath1,    bath2, 
              hall1,    hall2];

var scale = 10;


function setup() {
 // set the canvas to match the window size
 if (window.innerWidth > minWidth){
  width = window.innerWidth;
  } else {
    width = minWidth;
  }
  if (window.innerHeight > minHeight) {
    height = window.innerHeight;
  } else {
    height = minHeight;
  }

  originX = width/2 - 5.5*scale;
  originY = height/2 - 8.5*scale;

  //set up canvas
  createCanvas(width, height);
  noStroke();

 serial = new p5.SerialPort();

 serial.list();
 serial.open('/dev/tty.usbmodem114760901');

 serial.on('connected', serverConnected);

 serial.on('list', gotList);

 serial.on('data', gotData);

 serial.on('error', gotError);

 serial.on('open', gotOpen);

 serial.on('close', gotClose);
}

function serverConnected() {
 print("Connected to Server");
}

function gotList(thelist) {
 print("List of Serial Ports:");

 for (let i = 0; i < thelist.length; i++) {
  print(i + " " + thelist[i]);
 }
}

function gotOpen() {
 print("Serial Port is Open");
}

function gotClose(){
 print("Serial Port is Closed");
 latestData = "Serial Port is Closed";
}

function gotError(theerror) {
 print(theerror);
}

function gotData() {
  let currentString = serial.readLine();
  trim(currentString);
  if (!currentString) return;
  console.log(currentString);
  latestData = Number(currentString);
  currRoom = latestData;
  for (i = 0; i < rooms.length; i++) {
    rooms[i].color = redColor;
  }
  rooms[currRoom].color = greenColor;
}

function draw() {  
  background(0);

  //Draw canvas
  strokeWeight(4);
  stroke('white');
  fill('black');
  noStroke();

  fill(255);
  textSize(16);
  text("House Map", 30, 30);
  text("Current Room: " + rooms[currRoom].name, 30, 50);    // displaying the input

  //Draw Rooms
  strokeWeight(4);
  stroke('black');
  // fill(redColor);
  for (i = 0; i < rooms.length; i++) {
    fill(rooms[i].color);
    rect(rooms[i].getX(width), rooms[i].getY(height), rooms[i].length, rooms[i].height);
  }

}

function mousePressed() {
  var currX = mouseX;
  var currY = mouseY;

  for (i = 0; i < rooms.length; i++) {
    var roomX = rooms[i].getX(width);
    var roomY = rooms[i].getY(height);

    var check1 = (currX > roomX) && (currX < roomX + rooms[i].length);
    var check2 = (currY > roomY) && (currY < roomY + rooms[i].height);
    
    if (check1 && check2) {
      currRoom = i;
      rooms[i].color = greenColor;
    }
    else {
      rooms[i].color = redColor;
    }
  }
  
}

function mouseDragged() {

}

Electrical Schematic:

 

Assignment 7: Sound that has meaning

Description:

This project was meant to convey emotions through sound by playing a song corresponding to the user’s emotions when a button is pressed. Currently there are only 3 buttons for 3 different sounds/songs but the idea is to be able to hook up more and more as songs stick out as conveying your emotions. The main components for this build are buttons, resistors, a speaker and amplifier, and the Teensy 4.1. When each button is pressed, a certain song corresponding to a different emotion will start to play through. When the song is over, it waits for the next button to be pressed and does not repeat the current sound. When each button is pressed, the current song being played will stop playing and switch to the next song corresponding to the button press. As of now, button 0 plays the Jaws intense sound that signifies a stress and anxiety. Button 1 plays an African Safari game sound that signifies being happy and adventurous. Button 2 plays a relaxing song to convey peace and relaxation.

Video:

Demo Video

Images:

Code:

/*
 * Assignment 7: Sound with Meaning
 * Judson Kyle
 * judsonk
 * 
 * Description: This sketch plays a sound corresponding to the users mood at the
 *              time. Depending on the button pressed, a sound corresponding to
 *              one of three emotions will play. These emotions are intense/stressed,
 *              happy/upbeat, and peaceful/relaxed corresponding to buttons 0, 1,
 *              and 2 respectively. Each sound will only play once through to the
 *              end and not repeat after that.
 */

#include <Audio.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <SerialFlash.h>

AudioPlaySdWav           playWav1;
AudioMixer4              mixer1;         //xy=647,408
AudioOutputMQS           mqs1;           //xy=625,279
AudioConnection          patchCord1(playWav1, 0, mqs1, 0);
AudioConnection          patchCord2(playWav1, 1, mqs1, 1);

#define SDCARD_CS_PIN    BUILTIN_SDCARD
#define SDCARD_MOSI_PIN  11  // not actually used
#define SDCARD_SCK_PIN   13  // not actually used

#define SWITCH0   33
#define SWITCH1   34
#define SWITCH2   35
#define SWITCH3   37
#define SWITCH4   38
#define SWITCH5   39

#define BUTTON0   16
#define BUTTON1   17
#define BUTTON2   18

unsigned long currTime = 0;
unsigned long debounceTime = 0;
volatile unsigned long startTime = 0;

volatile bool DEBOUNCE = false;

int numButtons = 3;

volatile int state = 0;

const char* currFileName = "Shark Attack Shortened.WAV";

void setup(void)
{
  // Wait for at least 3 seconds for the USB serial connection
  Serial.begin (9600);

  AudioMemory(8);

  pinMode(SWITCH0, INPUT);
  pinMode(SWITCH1, INPUT);
  pinMode(SWITCH2, INPUT);
  pinMode(SWITCH3, INPUT);
  pinMode(SWITCH4, INPUT);
  pinMode(SWITCH5, INPUT);

  attachInterrupt(digitalPinToInterrupt(BUTTON0), button0Pressed, CHANGE);
  attachInterrupt(digitalPinToInterrupt(BUTTON1), button1Pressed, CHANGE);
  attachInterrupt(digitalPinToInterrupt(BUTTON2), button2Pressed, CHANGE);

  SPI.setMOSI(SDCARD_MOSI_PIN);
  SPI.setSCK(SDCARD_SCK_PIN);
  if (!(SD.begin(SDCARD_CS_PIN))) {
    // stop here, but print a message repetitively
    while (1) {
      Serial.println("Unable to access the SD card");
      delay(500);
    }
  }

}

void loop (void)
{
  currTime = millis();
  static int prevState = 0;

  if (prevState != state) {
    if (playWav1.isPlaying()) {
      playWav1.stop();
    }
    switch (state) {
      case 0: //Play intense sound
        currFileName = "Shark Attack Shortened.WAV";
        playWav1.play(currFileName);
        printPlayState(currFileName);
        delay(5);
        break;
      case 1: //Play fun/upbeat sound
        currFileName = "African_fun_long.WAV";
        playWav1.play(currFileName);
        printPlayState(currFileName);
        delay(5);
        break;
      case 2: //Play peaceful sound
        currFileName = "With-You-in-My-Arms-SSJ011001.WAV";
        playWav1.play(currFileName);
        printPlayState(currFileName);
        delay(5);
        break;
    }
  }
  prevState = state;
  
  //Update debounce state
  DEBOUNCE = abs(currTime - startTime) < debounceTime;
}

//Button 0 interrupt function
void button0Pressed() {
  if ((digitalRead(BUTTON0) < 1) && !DEBOUNCE) {
    state = 0;
    startTime = currTime;
    DEBOUNCE = true;
  }
}

//Button 1 interrupt function
void button1Pressed() {
  if ((digitalRead(BUTTON1) < 1) && !DEBOUNCE) {
    state = 1;
    startTime = currTime;
    DEBOUNCE = true;
  }
}

//Button 2 interrupt function
void button2Pressed() {
  if ((digitalRead(BUTTON2) < 1) && !DEBOUNCE) {
    state = 2;
    startTime = currTime;
    DEBOUNCE = true;
  }
}

//Print out the current cong being played or return an error message if the song isn't being played
void printPlayState(const char* fileName) {
  if (!playWav1.isPlaying()) {
    Serial.print("Error playing: ");
    Serial.println(fileName);
    delay(5);
  }
  else {
    Serial.print("Playing: ");
    Serial.println(fileName);
    delay(5);
  }
}

Electrical Schematic:

Error Sounds – MATLAB’s Assortment of Error Noises

When running a MATLAB script, there are a few things that trigger a sound to go off. The first of these is when a break in the code is set for debugging which for a while triggered a nice sounding double beat almost like a wooden percussion instrument. The second is when an error happens in the code and it stops running as a result invoking the same noise. While the first use case is very informative, the second case is not so. Recently, my perspective on these sounds changed as I listened to another friends computer make a screaming sound as the program encounters an error. This slight change in the error noise changed my whole perspective on what was happening in the script.

Crit 1 (Using the outdoors for heating/cooling) – Jud

Idea:

The main idea behind this project is to open the window to cool or heat your room when the outside temperature is what you want your room temperature to be. For example, in the summer, if it is a super nice day out and your room is very hot, you could enter a temperature into the system and it would automatically open the window to let outside air in to adjust the room temperature. This also has a dual purpose in the winter for if your house is too hot and the system could open the window to let a cool breeze in.

The main components of this system are two thermistors for measuring outside and inside temperature, a motor with an encoder to actuate the window, a motor driver to control the speed of the motor, and a p5 script animating the information from the Arduino in a helpful way. The Arduino sends the data over serial communication to the p5 script which then displays this information to the user and animates the virtual window corresponding to the current motor position.

As shown in the demo video below, the system works very well detecting a need for opening the window and acting upon it. In addition, the p5 script very accurately shows the degree of openness of the window. While these component work very well together, the controller is still fairly simple. Currently, there is just one setting for open and closed meaning the window doesn’t open a certain degree based off the temperature difference between the room and the goal temp. With more time this feature could be implemented, but for demonstration purposes of this concept, the current controller works well.

Images:

Here are images of the final system concept. The first image is an overall image showing both the physical build and the virtual animation. The second image is a close up of the virtual animation screen showing the displayed outside and inside temperatures as well as the window itself. Finally, the third image shows a top view of the physical wiring for the system.

Videos:

Video demonstration of the motor position feedback controlling the amount the virtual window is open.

Here is a video of the full system demo. As shown, when the outside temperature rises above the inside temperature, the window opens. When the outside temperature falls below the inside temperature, the window closes.

Code:

P5.js

let serial;
let latestData = "waiting for data";
var outData;

var minWidth = 600;   //set min width and height for canvas
var minHeight = 400;
var width, height;    // actual width and height for the sketch
var squareWidth = 100;

var OuterWindowHeight = 800;
var OuterWindowLength = 600;

var InnerWindowHeight = OuterWindowHeight/2;
var InnerWindowLength = OuterWindowLength;

var panelThickness = 10;
var RoomLength = 800;

var bottomInnerWindowY;
var y = 0;

var roomColor = '#f7c07c';
var skyColor = '#c2f9ff';

var outsideTemp = 100;
var insideTemp = 100;

var prevString;


function setup() {
 // set the canvas to match the window size
 if (window.innerWidth > minWidth){
  width = window.innerWidth;
  } else {
    width = minWidth;
  }
  if (window.innerHeight > minHeight) {
    height = window.innerHeight;
  } else {
    height = minHeight;
  }

  bottomInnerWindowY = (height - OuterWindowHeight)/2 + 50 + OuterWindowHeight - InnerWindowHeight/2;

  //set up canvas
  createCanvas(width, height);
  noStroke();

 serial = new p5.SerialPort();

 serial.list();
 serial.open('/dev/tty.usbmodem144101');

 serial.on('connected', serverConnected);

 serial.on('list', gotList);

 serial.on('data', gotData);

 serial.on('error', gotError);

 serial.on('open', gotOpen);

 serial.on('close', gotClose);
}

function serverConnected() {
 print("Connected to Server");
}

function gotList(thelist) {
 print("List of Serial Ports:");

 for (let i = 0; i < thelist.length; i++) {
  print(i + " " + thelist[i]);
 }
}

function gotOpen() {
 print("Serial Port is Open");
}

function gotClose(){
 print("Serial Port is Closed");
 latestData = "Serial Port is Closed";
}

function gotError(theerror) {
 print(theerror);
}

function gotData() {
  let currentString = serial.readLine();
  trim(currentString);
  if (!currentString) return;
  console.log(currentString);
  // latestData = currentString;
  // currentString = Number(currentString) - 10000;
  if ((Number(currentString) - 10000) < 10000) {
    insideTemp = (Number(currentString) - 10000);
    return;
  }
  else if ((Number(currentString) - 20000) < 10000) {
    outsideTemp = (Number(currentString) - 20000);
    return;
  }
  else if ((Number(currentString) - 30000) < 10000) {
    y = (Number(currentString) - 30000)/1000;
    return;
  }
}

function draw() {

  var outerWindowBottomY = (height - OuterWindowHeight)/2 + 50;
  var topInnerWindowY = outerWindowBottomY + InnerWindowHeight/2;

  //Update bottom window position
  bottomInnerWindowY = (height - OuterWindowHeight)/2 + 50 + OuterWindowHeight - InnerWindowHeight/2 - (y/3.1415)*(OuterWindowHeight - InnerWindowHeight);
  
  background(0);

  //Draw canvas
  strokeWeight(4);
  stroke('white');
  fill('black');
  noStroke();

  // //Draw Room
  strokeWeight(4);
  stroke('black');
  fill(roomColor);
  rect((width - RoomLength)/2, 0, RoomLength, height);

  fill('black');
  stroke(roomColor);
  textSize(30);
  textAlign(CENTER);
  text("Window Opener", width/2, 80);
  textSize(16);
  text("Outside Temp: " + outsideTemp + "\t\t\t\t\t\t\t\t\t\t Inside Temp: " + insideTemp, width/2, 125);    // displaying the input

  //Draw outer window
  strokeWeight(4);
  stroke('black');
  fill(skyColor);
  rect((width - OuterWindowLength)/2, outerWindowBottomY, OuterWindowLength, OuterWindowHeight);

  //Draw top inner window
  strokeWeight(4);
  stroke('black');
  strokeWeight(10);
  noFill();
  rect((width - InnerWindowLength)/2, topInnerWindowY - InnerWindowHeight/2, InnerWindowLength, InnerWindowHeight);
  strokeWeight(10);
  line((width - InnerWindowLength)/2, topInnerWindowY, (width - InnerWindowLength)/2 + InnerWindowLength, topInnerWindowY);//y line
  line(width/2, outerWindowBottomY, width/2, outerWindowBottomY + InnerWindowHeight);//x line

  //Draw bottom inner window
  strokeWeight(4);
  stroke('black');
  strokeWeight(10);
  noFill();
  rect((width - InnerWindowLength)/2, bottomInnerWindowY - InnerWindowHeight/2, InnerWindowLength, InnerWindowHeight);
  strokeWeight(10);
  line((width - InnerWindowLength)/2, bottomInnerWindowY, (width - InnerWindowLength)/2 + InnerWindowLength, bottomInnerWindowY);//y line
  line(width/2, bottomInnerWindowY - InnerWindowHeight/2, width/2, bottomInnerWindowY + InnerWindowHeight/2);//x line

}


function mousePressed() {
  
}

function mouseDragged() {

}

Arduino

/*
 * Window Climate Controller
 * Judson Kyle
 * judsonk
 * 
 * Collaboration:
 *  - Used thermistor code from this site: https://www.circuitbasics.com/arduino-thermistor-temperature-sensor-tutorial/
 *  
 */

#define THERMISTOR1_PIN   A0
#define THERMISTOR2_PIN   A1
#define encoderPinA   18
#define encoderPinB   19
#define motorPin1     10
#define motorPin2     11
#define motorPWM      9

#define P_I 3.14159

int encoderCountRev = 48; //48 encoder counts per revolution
int incomingByte = 0;

// https://www.pololu.com/product/4824
float gearRatio = 2*(22.0*20.0*22.0*22.0*23.0)/(12.0*12.0*10.0*10.0*10.0); 

bool motorSpinning = false;

// Motor distance measuring variables
volatile long currentEncoder_pos_f = 0, prevEncoder_pos = 0, revs = 0;
float motorCurPos = 0;

//PID Variables
float ep = 0; //Proportional error
float ei = 0; //Integral error
float ed = 0; //Derivative error
float prev_ep = 0;
double curTime = 0;
double prevTime = 0;

int pwmCommand = 0;

float kp = 50; // P gain
float ki = 0; // I gain
float kd = 0; // D gain
float motorGoalPos = P_I; // desired angle [radians]

float temp1, temp2;
float goalTemp = 480; //Degrees F

float insideOutsideDiff = 0;
float insideGoalDiff = 0;

float windowOpeningLength = P_I; //radians at the moment
float maxDiff = 100;

void setup() {
  //Initialize motor control pins
  pinMode(motorPin1, OUTPUT);
  pinMode(motorPin2, OUTPUT);
  pinMode(motorPWM, OUTPUT);

  //Initialize interrupts
  attachInterrupt(digitalPinToInterrupt(encoderPinA), encoderA, CHANGE);
  attachInterrupt(digitalPinToInterrupt(encoderPinB), encoderB, CHANGE);
  
  Serial.begin(9600);
}

void loop() {

  //Update motor current position
  motorCurPos = (2*P_I*currentEncoder_pos_f) / (gearRatio*encoderCountRev);

  temp1 = readTemp('v', THERMISTOR1_PIN);
  temp2 = readTemp('v', THERMISTOR2_PIN);

  insideOutsideDiff = temp1 - temp2; //Add in conditionals for if temperature difference is negative
  insideGoalDiff = temp1 - goalTemp; //Add in conditionals for if temperature difference is negative

  //Update motor goal position based off of difference in temperature
  if (insideGoalDiff < 0) { //Goal temp is higher than inside temp
    if (insideOutsideDiff < 0) { //Outside temp higher than inside temp
//      int openDistance = abs(insideGoalDiff/maxDiff)*windowOpeningLength;
      motorGoalPos = P_I;
    }
    else { //Ouside temp lower than or equal to inside temp
      motorGoalPos = 0;
    }
  }
  else if (insideGoalDiff > 0) { //Goal temp lower than inside temp
    if (insideOutsideDiff > 0) { //Outside temp lower than inside temp
//      int openDistance = (insideGoalDiff/maxDiff)*windowOpeningLength;
      motorGoalPos = P_I;
    }
    else { //Ouside temp higher than or equal to inside temp
      motorGoalPos = 0;
    }
  }

  updateMotorCmd(updatePID(motorGoalPos, motorCurPos));

  //Send data to p5 script for visualization
  sendData();
}

float readTemp(char degreeType, int ThermistorPin) {
  float logR2, R2, T;
  float c1 = 1.009249522e-03, c2 = 2.378405444e-04, c3 = 2.019202697e-07;
  
  int R1 = 1000;
  
  int Vo = analogRead(ThermistorPin);
  R2 = R1 * (1023.0 / (float)Vo - 1.0);
  logR2 = log(R2);

  T = (1.0 / (c1 + c2*logR2 + c3*logR2*logR2*logR2));

  switch (degreeType){
    case 'f':
    case 'F':
      T = T - 273.15;
      T = (T * 9.0)/ 5.0 + 32.0;
      break;
    case 'c':
    case 'C':
      T = T - 273.15;
      break;
    default:
      return Vo;
      break;
  }

  return T;
}

int updatePID(float goalPos, float curPos) {
  int pwmCmd;
  
  // PID Controller
  curTime = millis();
  ep = (goalPos - curPos); // error in position (p)
  ei = ei + ep; // integral error in position (i)
  ed = (ep - prev_ep) / (curTime - prevTime); // derivative error in position (d)

  prev_ep = ep;
  prevTime = curTime;

  pwmCmd = (ep*kp + ei*ki + ed*kd);

  return pwmCmd;
}

void sendData() {
  int temp1Send = 10000 + temp1;
  Serial.println(temp1Send); //Inside temp
  int temp2Send = 20000 + temp2;
  Serial.println(temp2Send); //Outside temp
  int sendData = 30000 + (int) (motorCurPos*1000);
  Serial.println(sendData); //Wnidow Opening
}

void updateMotorCmd(int pwmCmd) {
  // switch directions if pass set point
  if(ep < 0) {
    // Turn on motor A & B
    digitalWrite(motorPin1, LOW);
    digitalWrite(motorPin2, HIGH);
  }
  else {
    // Turn on motor A & B
    digitalWrite(motorPin1, HIGH);
    digitalWrite(motorPin2, LOW);
  }

  pwmCmd = abs(pwmCmd);
  if (pwmCmd > 255) {
    pwmCmd = 255;
  }
  analogWrite(motorPWM, pwmCmd);
}

void encoderA(){
  // look for a low-to-high on channel A
  if (digitalRead(encoderPinA) == HIGH) { 
    // check channel B to see which way encoder is turning
    if (digitalRead(encoderPinB) == LOW) {  
      
        currentEncoder_pos_f = currentEncoder_pos_f + 1;         // CW
    } 
    else {
        currentEncoder_pos_f = currentEncoder_pos_f - 1;        // CCW
    }
  }
  else   // must be a high-to-low edge on channel A                                       
  { 
    // check channel B to see which way encoder is turning  
    if (digitalRead(encoderPinB) == HIGH) {   
      
        currentEncoder_pos_f = currentEncoder_pos_f + 1;          // CW
    } 
    else {
        currentEncoder_pos_f = currentEncoder_pos_f - 1;          // CCW
    }
  }
}

void encoderB(){

  // look for a low-to-high on channel B
  if (digitalRead(encoderPinB) == HIGH) {   
   // check channel A to see which way encoder is turning
    if (digitalRead(encoderPinA) == HIGH) {  
       
        currentEncoder_pos_f = currentEncoder_pos_f + 1;         // CW
    } 
    else {
        currentEncoder_pos_f = currentEncoder_pos_f - 1;         // CCW
    }
  }
  // Look for a high-to-low on channel B
  else { 
    // check channel B to see which way encoder is turning  
    if (digitalRead(encoderPinA) == LOW) {   
        currentEncoder_pos_f = currentEncoder_pos_f + 1;          // CW
    } 
    else {
        currentEncoder_pos_f = currentEncoder_pos_f - 1;         // CCW
    }
  }
}

 

Electrical Schematic:

Assignment 6 – Jud

Description:

This circuit turns the data that the sound sensor gets into an angle based off of a moving average of the amplitude of the data received. The data from the sensor captures the raw sound waves from the air and sends them to the arduino which has a very spiky and unsmooth nature. In addition, the data transitions for increasing to decreasing very rapidly so a traditional filter or tracking the change in the data does not work well at all. Instead, I have implemented a moving average method where the code tracks a certain number of data points, calculates the max and min of the data, and then uses the difference between these values as the new data. This is then multiplied by some multiplier to increase the variance and translated into an angle for a servo to travel to. The algorithm for calculating the angle works wonders while the servo isn’t connected, however, as depicted in the video, the noise of the servo creates an uncontrollable feedback look that results in the servo moving out of control. Regardless, the system responds to noise in the way that it should translating a noise in the environment into an angle for the servo to move to.

Video:

Code:

#include <Servo.h>

//Define Accelerometer Variables for pins
#define MICROPHONE    A0
#define SERVO_PIN     9

Servo servo;

double servoAngle = 0;

double alpha = 0.9;
double multiplier = 5;

//Array to hold data for moving average calculation
double speakerData[100]; // {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

double speakerLevel = 0;
double prevSpeakerLevel = 0;
double maxSpeakerLevel = 200;
double minSpeakerLevel = 50;


void setup() {
  //Setup pins as inputs
  pinMode(MICROPHONE, INPUT);

  //Create servo object
  servo.attach(SERVO_PIN);
  
}

void loop() {

  //Update moving average array
  for (int i = (sizeof(speakerData)/sizeof(speakerData[0]) - 1); i >= 0; i--) {
    if (i != 0) {
      speakerData[i] = speakerData[i - 1];
    }
    else {
      speakerData[i] = analogRead(MICROPHONE);
    }
  }

  double MIN = findMin();
  double MAX = findMax();

  speakerLevel = MAX - MIN;
  speakerLevel = multiplier*(alpha*speakerLevel + (1 - alpha)*prevSpeakerLevel);  //Filter data

  //Put upper and lower bounds on data
  if (speakerLevel >= maxSpeakerLevel) {
    speakerLevel = maxSpeakerLevel;
  }
  else if (speakerLevel <= minSpeakerLevel) {
    speakerLevel = minSpeakerLevel;
  }

  servoAngle = 180*(speakerLevel - minSpeakerLevel)/(maxSpeakerLevel - minSpeakerLevel);
  servo.write((int) servoAngle);

  prevSpeakerLevel = speakerLevel;
  
}

int findMin() {
  double minimum = 0;
  for (int i = 0; i < sizeof(speakerData)/sizeof(speakerData[0]); i++) {
    if (speakerData[i] < minimum || i == 0) {
      minimum = speakerData[i];
    }
  }
  return minimum;
}

int findMax() {
  double maximum = 0;
  for (int i = 0; i < sizeof(speakerData)/sizeof(speakerData[0]); i++) {
    if (speakerData[i] > maximum) {
      maximum = speakerData[i];
    }
  }
  return maximum;
}