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:

 

 

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.