The purpose of this device is to measure my vitals and the vitals of my computers (two at the moment) and then assess how I’m doing in relation to the computers. The input is numerical data in the form of CPU + GPU die temperatures from the two PCs, as well as my Galvanic Skin Response (GSR, used in polygraph tests), heart-rate, and sleep pattern (when I went to sleep and for how long I slept). The output is a message accompanied by a sound of my choosing.

The “front” of the device. Due to mounting issues, the final installation makes the keypad side the front of the device.

The “back” of the device. The small speaker was eventually ditched in favor of a full-range speaker from a laptop.

When all vitals are good…

When one of the PC’s vitals are bad (my desktop, in this case)…

When PC vitals are bad, but your vitals are OK…

When the PC vitals are OK, but your vitals are not (stress ended up being the bad vital)… some swearing because I thought I broke the GSR sensor… right before the project documentation was due…)

When both your vitals and the PC vitals are bad…

Progress Images

I had originally intended to use a heart-rate monitor that worked over an analog pin, but this was interfering with the GSR sensor, so I ended up using an I2C Pulse Oximeter to get the heart-rate.

An early prototype… this is a mess…

GSR and heart-rate monitor were reading wonky…

I got really stressed about the wonky readings, and then the GSR started reading properly… a lower value means you are more stressed (given the way this particular sensor was reading).

All assembled with a 3D printed structure for the Arduino and LCD.

A 9V battery apparently isn’t enough for this…

Process Reflection

Similar to my other endeavors, I procrastinated a lot on this. What I had ready for the initial due date was more like what I should have had ready a week prior for the 90% complete presentation. With that said, though, the device has finally taken shape, though not necessarily what I had envisioned. I had planned to mount this on the other side of my bed, but command strips were neither commanding nor stripping. Double-sided tape was a no-go as well. Nothing would stick to the bottom of the 3D printed mount for the Arduino Uno and LCD. Chances are, it just needs to be sanded to smooth it out.

Regardless, the device functions as intended (though the sensors can be temperamental at times. If I were to do this again differently, of course I’d change a few things. For starters, I’d take more time early on, but that can be said for all of my endeavors–see “I procrastinated A LOT on this.” But aside from that, I’d also use an Arduino Due or Mega, since I was starting to run out of dynamic memory (SRAM) on the Arduino Uno. If I start to implement long-term data logging with an SD card reader like I intended to originally and run into issues, then that’s what I’ll do. I did learn quite a bit about working with biometric sensors, though. They can be really finnicky. Both the GSR sensor and heart-rate monitor can read completely differently just based on how hard it’s pressed against your skin (moreso the GSR sensor).

Code

/*
   Vitals Alarm (62-362 Project 2)
   Seth Geiser (sgeiser)

   Collaboration: Any example code used can be found
   as follows:

   GSR Sensor: https://wiki.seeedstudio.com/Grove-GSR_Sensor/
   MAX30102 PulseOx: Example 5: Heart-rate
   Keypad: Same as Project 1, Workaholic's Clock
   DFPlayer Mini: Same as Project 1, Workaholic's Clock

   This code initializes a keypad, 20x4 I2C LCD, Pulse
   Oximeter, Grove GSR sensor, and a DFRobot DFPlayer
   Mini. The keypad is for entering the reported CPU
   and GPU die temperatures for my two computers, as
   well as when I went to sleep and how long I slept.
   The GSR sensor measures galvanic skin response,
   similar to a polygraph test. The Pulse Oximeter is
   currently only being used as a heart-rate monitor,
   since the SPO2 functionality appears to not be
   working right now (tested with the SPO2 example
   code.

   Pin mapping:

   pin      | mode       | description
   ---------|------------|------------
   SDA/SCL   I2C          MAX30102 PulseOx + 20x4 I2C LCD
   2-8       Drive/Sense  Keypad
   9         RX           DFPlayer Mini TX
   10        TX           DFPlayer Mini RX
   A0        INPUT        GSR Sensor (through Grove shield)
*/

#include <SoftwareSerial.h>
#include <DFRobotDFPlayerMini.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <Keypad.h>
#include <MAX30105.h>
#include <heartRate.h>

SoftwareSerial mySoftwareSerial(9, 10); // RX, TX
DFRobotDFPlayerMini myDFPlayer;

MAX30105 particleSensor;

LiquidCrystal_I2C lcd(0x27, 20, 4);
// set the LCD address to 0x27 for 20x4 display

char customKey = 0;
const byte ROWS = 4;
const byte COLS = 3;
char hexaKeys[ROWS][COLS] = {
  {'1', '2', '3'},
  {'4', '5', '6'},
  {'7', '8', '9'},
  {'*', '0', '#'}
};

byte rowPins[ROWS] = {5, 3, 2, 7};
byte colPins[COLS] = {4, 8, 6};
Keypad customKeypad = Keypad(makeKeymap(hexaKeys), rowPins, colPins, ROWS, COLS);

const byte RATE_SIZE = 4; // Increase this for more averaging. 4 is good.
byte rates[RATE_SIZE]; // Array of heart rates
byte rateSpot = 0;
long lastBeat = 0; // Time at which the last beat occurred

float beatsPerMinute;
int beatAvg;

const int GSR = A0;
int sensorValue = 0;
int gsr_average = 0;

int j = 0;
int jarvisCPUDieTemp;
int jarvisGPUDieTemp;
int tarsCPUDieTemp;
int tarsGPUDieTemp;
int sleepStart;
int sleepAmount;

// For accepting input from the keypad, these get converted
// to the ints above...
char temp[2];
char sleep[2];

bool pcVitalsGood = true;
bool humanVitalsGood = true;

void(* resetFunc) (void) = 0; // reset the Arduino when done
void setup() {
  lcd.init();
  lcd.backlight();
  mySoftwareSerial.begin(9600);
  Serial.begin(115200);

  lcd.setCursor(0, 0);
  lcd.print(F("Arduino UNO R3 SMD"));
  lcd.setCursor(0, 1);
  lcd.print(F("Starting DFPlayer..."));
  lcd.setCursor(0, 2);
  lcd.print(F("Starting MAX30102..."));
  lcd.setCursor(0, 3);
  lcd.print(F("Please wait..."));
  delay(2000);
  // Initialize sensor
  if (!particleSensor.begin(Wire, I2C_SPEED_FAST))
    // Use default I2C port, 400kHz speed
  {
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print(F("MAX30102 not found."));
    lcd.setCursor(0, 1);
    lcd.print(F("Please check wiring."));
    while (1);
  }

  particleSensor.setup();
  // Configure sensor with default settings
  particleSensor.setPulseAmplitudeRed(0x0A);
  // Turn Red LED to low to indicate sensor is running
  particleSensor.setPulseAmplitudeGreen(0);
  // Turn off Green LED

  if (!myDFPlayer.begin(mySoftwareSerial)) {
    // Use softwareSerial to communicate with mp3.
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print(F("Unable to begin:"));
    lcd.setCursor(0, 1);
    lcd.print(F("Please check wiring!"));
    lcd.setCursor(0, 2);
    lcd.print(F("Please insert SD!"));
  }
  lcd.setCursor(0, 3);
  lcd.print(F("DFPlayerMini online."));
  myDFPlayer.volume(20);  // Set volume value. From 0 to 30
  delay(2000);
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print(F("CMU IDeATe PhysComp"));
  lcd.setCursor(0, 1);
  lcd.print(F("Vitals Alarm"));
  lcd.setCursor(0, 2);
  lcd.print(F("Powered by sgeiser"));
  lcd.setCursor(0, 3);
  lcd.print(F("BOOTING DEVICE..."));
  delay(2500);
  myDFPlayer.play(1); // 0001_ps2.mp3
  delay(2500);
  lcd.clear();
  customKey = customKeypad.getKey();
  lcd.setCursor(0, 0);
  lcd.print(F("Input the CPU and"));
  lcd.setCursor(0, 1);
  lcd.print(F("GPU die temps for"));
  lcd.setCursor(0, 2);
  lcd.print(F("your computers (in"));
  lcd.setCursor(0, 3);
  lcd.print(F("degrees Celsius)."));
  delay(5000);
  lcd.clear();
  inputPCVitals();
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print(F("Input when you went"));
  lcd.setCursor(0, 1);
  lcd.print(F("to sleep and how"));
  lcd.setCursor(0, 2);
  lcd.print(F("long you slept in"));
  lcd.setCursor(0, 3);
  lcd.print(F("24-hour time."));
  delay(5000);
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print(F("If any value is less"));
  lcd.setCursor(0, 1);
  lcd.print(F("than 10, then prefix"));
  lcd.setCursor(0, 2);
  lcd.print(F("with a zero."));
  delay(5000);
  lcd.clear();
  inputSleepPattern();
}

void loop() {
  customKey = customKeypad.getKey();
  long sum = 0;
  for (int i = 0; i < 10; i++) {
    sensorValue = analogRead(GSR);
    sum += sensorValue;
  }
  gsr_average = sum / 10;
  long irValue = particleSensor.getIR();

  if (checkForBeat(irValue) == true)
  {
    // We sensed a beat!
    long delta = millis() - lastBeat;
    lastBeat = millis();

    beatsPerMinute = 60 / (delta / 1000.0);

    if (beatsPerMinute < 255 && beatsPerMinute > 20)
    {
      rates[rateSpot++] = (byte)beatsPerMinute;
      // Store this reading in the array
      rateSpot %= RATE_SIZE; // Wrap variable

      // Take average of readings
      beatAvg = 0;
      for (byte x = 0 ; x < RATE_SIZE ; x++)
        beatAvg += rates[x];
      beatAvg /= RATE_SIZE;
    }
  }
  lcd.setCursor(0, 0);
  lcd.print((String)"Average GSR: " + gsr_average + "    ");
  lcd.setCursor(0, 1);
  lcd.print((String)"Average BPM: " + beatAvg + "    ");
  lcd.setCursor(0, 2);
  lcd.print(F("#: Assess vitals"));
  if (customKey == '#') {
    lcd.clear();
    assess();
  }
}

void inputJarvisCPUTemp() {
  lcd.setCursor(0, 0);
  lcd.print(F("Jarvis CPU Temp: "));
  while (j < 2) {
    char key = customKeypad.getKey();
    if (key) {
      temp[j++] = key;
      lcd.print(key);
    }
    key = 0;
  }
  jarvisCPUDieTemp = atoi(temp);
  // Convert the char array to int and store it. Note
  // thatatoi is deprecated, am going to change it soon.
  j = 0;
}

void inputJarvisGPUTemp() {
  lcd.setCursor(0, 1);
  lcd.print(F("Jarvis GPU Temp: "));
  while (j < 2) {
    char key = customKeypad.getKey();
    if (key) {
      temp[j++] = key;
      lcd.print(key);
    }
    key = 0;
  }
  jarvisGPUDieTemp = atoi(temp);
  // Convert the char array to int and store it. Note
  // thatatoi is deprecated, am going to change it soon.
  j = 0;
}

void inputTarsCPUTemp() {
  lcd.setCursor(0, 2);
  lcd.print(F("TARS CPU Temp: "));
  while (j < 2) {
    char key = customKeypad.getKey();
    if (key) {
      temp[j++] = key;
      lcd.print(key);
    }
    key = 0;
  }
  tarsCPUDieTemp = atoi(temp);
  // Convert the char array to int and store it. Note
  // thatatoi is deprecated, am going to change it soon.
  j = 0;
}

void inputTarsGPUTemp() {
  lcd.setCursor(0, 3);
  lcd.print(F("TARS GPU Temp: "));
  while (j < 2) {
    char key = customKeypad.getKey();
    if (key) {
      temp[j++] = key;
      lcd.print(key);
    }
    key = 0;
  }
  tarsGPUDieTemp = atoi(temp);
  // Convert the char array to int and store it. Note
  // thatatoi is deprecated, am going to change it soon.
  j = 0;
}

// I am not fond of how I wrote the above 4 functions.
// I could probably condense them into one and save some
// progmem (though I'm not really short on that). The code
// would certainly be cleaner and easier to read, though.

void inputPCVitals() {
  customKey = customKeypad.getKey();
  inputJarvisCPUTemp();
  inputJarvisGPUTemp();
  inputTarsCPUTemp();
  inputTarsGPUTemp();
  lcd.clear();
}

void inputSleepStart() {
  lcd.setCursor(0, 0);
  lcd.print(F("Sleep start: "));
  while (j < 2) {
    char key = customKeypad.getKey();
    if (key) {
      sleep[j++] = key;
      lcd.print(key);
    }
    key = 0;
  }
  sleepStart = atoi(sleep);
  // Convert the char array to int and store it. Note
  // thatatoi is deprecated, am going to change it soon.
  j = 0;
}

void inputSleepAmount() {
  lcd.setCursor(0, 1);
  lcd.print(F("Sleep amount: "));
  while (j < 2) {
    char key = customKeypad.getKey();
    if (key) {
      sleep[j++] = key;
      lcd.print(key);
    }
    key = 0;
  }
  sleepAmount = atoi(sleep);
  // Convert the char array to int and store it. Note
  // thatatoi is deprecated, am going to change it soon.
  j = 0;
}

// As with the 4 functions that deal with inputting the
// die temps in the PCs, I am not fond of how I wrote
// these two functions. I hope to condense them, too.

void inputSleepPattern() {
  inputSleepStart();
  inputSleepAmount();
  lcd.clear();
}

void assess() {
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print(F("Assessing PC vitals"));
  if ((jarvisCPUDieTemp >= 85 || jarvisGPUDieTemp >= 90) &&
      (tarsCPUDieTemp >= 85 || tarsGPUDieTemp >= 85)) {
    bothPCsBad();
  }
  else if (jarvisCPUDieTemp >= 85 || jarvisGPUDieTemp >= 90) {
    jarvisBad();
  }
  else if (tarsCPUDieTemp >= 85 || tarsGPUDieTemp >= 85) {
    tarsBad();
  }
  else {
    lcd.setCursor(0, 1);
    lcd.print(F("PC vitals OK!"));
    delay(1000);
  }
  lcd.setCursor(0, 2);
  lcd.print(F("Checking your vitals"));
  delay(2500);
  if (gsr_average > 500) {
    lcd.setCursor(0, 3);
    lcd.print("Could not read GSR.");
    delay(2500);
  }
  if (beatAvg <= 40) {
    lcd.setCursor(0, 3);
    lcd.print("Could not get pulse.");
    delay(2500);
  }
  if (gsr_average < 150 || beatAvg >= 130) {
    stressed();
  }
  if (sleepStart <= 6 || (sleepAmount < 7 || sleepAmount > 12)) {
    badSleep();
  }
  if (!humanVitalsGood && !pcVitalsGood) {
    allBad();
  }
  else if (!pcVitalsGood) {
    pcBadHumanGood();
  }
  else if (!humanVitalsGood) {
    pcGoodHumanBad();
  }
  else {
    allGood();
  }
  resetFunc();
}

void badSleep() {
  humanVitalsGood = false;
  lcd.setCursor(0, 3);
  lcd.print(F("Get better sleep!   "));
  delay(5000);
}

void jarvisBad() {
  pcVitalsGood = false;
  lcd.setCursor(0, 1);
  lcd.print(F("Warning on Jarvis!"));
  delay(5000);
}

void tarsBad() {
  pcVitalsGood = false;
  lcd.setCursor(0, 1);
  lcd.print(F("Warning on TARS!"));
  delay(5000);
}

void bothPCsBad() {
  pcVitalsGood = false;
  lcd.setCursor(0, 1);
  lcd.print(F("Warning on BOTH PCs!"));
  delay(5000);
}

void stressed() {
  humanVitalsGood = false;
  lcd.setCursor(0, 3);
  lcd.print(F("You're stressed..."));
  delay(2500);
  lcd.setCursor(0, 3);
  lcd.print(F("Take a break, dude."));
  delay(5000);
}

void pcGoodHumanBad() {
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print(F("Your PCs appear to"));
  lcd.setCursor(0, 1);
  lcd.print(F("be running great..."));
  lcd.setCursor(0, 2);
  lcd.print(F("How about you take"));
  lcd.setCursor(0, 3);
  lcd.print(F("care of yourself???"));
  myDFPlayer.play(4); // 0004_bruh.wav
  delay(5000);
}

void pcBadHumanGood() {
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print(F("Something is up with"));
  lcd.setCursor(0, 1);
  lcd.print(F("at least one of your"));
  lcd.setCursor(0, 2);
  lcd.print(F("PCs. You should prob"));
  lcd.setCursor(0, 3);
  lcd.print(F("check that out..."));
  myDFPlayer.play(2); // 0002_ohno.mp3
  delay(5000);
}

void allBad() {
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print(F("Oh my God, it's all"));
  lcd.setCursor(0, 1);
  lcd.print(F("broken! Seriously,"));
  lcd.setCursor(0, 2);
  lcd.print(F("you need to get on"));
  lcd.setCursor(0, 3);
  lcd.print(F("top of this..."));
  myDFPlayer.play(5); // 0005_battery_low.mp3
  delay(5000);
}

void allGood() {
  lcd.setCursor(0, 3);
  lcd.print(F("Your vitals are OK!"));
  delay(2500);
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print(F("Nicely done! Your"));
  lcd.setCursor(0, 1);
  lcd.print(F("vitals look alright,"));
  lcd.setCursor(0, 2);
  lcd.print(F("and your systems are"));
  lcd.setCursor(0, 3);
  lcd.print(F("running smoothly!"));
  myDFPlayer.play(3); // 0003_scatman_world.mp3
  delay(13000);
}