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
Final Demo
Images:
Process:
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: