// Improved performance version of your drink-can monitor
#include "AlarmManager.h" // Handles different alarm tones
#include <LiquidCrystal_I2C.h>
#include <dht.h>
#include <Wire.h>
#include <GY521.h>
#include <math.h>
//I/O pins
#define DHT22_PIN 13
#define BUZZER_PIN 7
#define BUTTON_PIN 3
#define lowLEDPin 8
#define medLEDPin 9
#define highLEDPin 10
#define TempLEDPin 11
//The state of the button when not pressed (HIGH/LOW)
bool BUTTON_DOWN = LOW;
bool ButtonState;
constexpr float MAX_SHAKE = 2.0f;
//Thresholds for each warning type.
constexpr float LOW_SHAKE_PCT = 10.0f;
constexpr float MED_SHAKE_PCT = 50.0f;
constexpr float HIGH_SHAKE_PCT = 80.0f;
constexpr unsigned long DECAY_MS = 1UL * 30UL * 1000UL;
//5UL * 60UL * 1000UL;
//optimal temp values
constexpr int TEMP_THRESHOLD = 22; //if above temp alarm triggers
constexpr int OPTIMAL_TEMP = 18; //any higher is considered luke warm
constexpr unsigned long TEMP_INTERVAL_MS = 1000; //The curation of the clock
//Time, temp and accelerometer readings
unsigned long now;
float accAvg;
float netAccel;
float instantPct;
unsigned long dt;
int temp;
int chk;
//Component initialisation
GY521 sensor(0x68);
LiquidCrystal_I2C lcd(0x27, 16, 2);
dht DHT;
AlarmManager alarm(BUZZER_PIN, BUTTON_PIN, BUTTON_DOWN, false);
//Calibration
float baselineAvg = 0.0f;
bool baselineSet = false;
//clocks, delays and alarms
float shakePercent = 0.0f;
unsigned long lastDecayTime = 0;
unsigned long lastTempRead = 0;
bool lowAlarmTriggered = false;
bool medAlarmTriggered = false;
bool tempAlarmTriggered = false;
bool bigAlarmTriggered = false;
void setup() {
lcd.init();
lcd.backlight();
pinMode(BUTTON_PIN, INPUT);
pinMode(BUZZER_PIN, OUTPUT);
pinMode(lowLEDPin, OUTPUT);
pinMode(medLEDPin, OUTPUT);
pinMode(highLEDPin, OUTPUT);
pinMode(TempLEDPin, OUTPUT);
Serial.begin(115200);
Wire.begin();
// wait for gyro to be active
while (!sensor.wakeup()) {
lcd.setCursor(0, 0);
lcd.print("Gyro not ready");
}
// calibrates the baseline to counteract drift
lcd.clear();
lcd.print("Put flat then");
lcd.setCursor(0,1);
lcd.print("Push button");
while (digitalRead(BUTTON_PIN) == BUTTON_DOWN){digitalRead(BUTTON_PIN) == BUTTON_DOWN;}
calibrate();
}
void calibrate(){
//Takes 50 readings of the gyro then averages out the results
const uint8_t CAL_SAMPLES = 50;
float sum = 0;
for (uint8_t i = 0; i < CAL_SAMPLES; ++i) {
sensor.read();
sum += fabs(sensor.getAccelX())
+ fabs(sensor.getAccelY())
+ fabs(sensor.getAccelZ());
} //adds together the X Y and Z axis to get a total shake
baselineAvg = (sum / CAL_SAMPLES) / 3.0f; //This will be subtracted from the readings to determine how much it moved
baselineSet = true;
lastDecayTime = millis();
lcd.clear();
}
void UpdateSensors()
{
now = millis();
sensor.read();
ButtonState = digitalRead(BUTTON_PIN);
alarm.update(now, ButtonState);
accAvg = (
fabs(sensor.getAccelX()) +
fabs(sensor.getAccelY()) +
fabs(sensor.getAccelZ()) ) / 3.0f;
Serial.println(baselineAvg);
netAccel = fabs(accAvg - baselineAvg);
instantPct = min(netAccel / MAX_SHAKE * 100.0f, 100.0f);
shakePercent = max(shakePercent, instantPct);
//decays proportionally to how much time passed, it takes roughly 2-4 minutes to go from 100-0
/*
The formulae here aren't mine. I prompted chatGPT to give the formula in psudocode
then converted it to working arduino code.
This was a pain to get right...
*/
dt = now - lastDecayTime;
shakePercent -= (100.0f / DECAY_MS) * dt;
shakePercent = max(shakePercent, 0.0f);
lastDecayTime = now;
//Clock for checking the temperature as checking the DHT too quickly causes errors
if (now - lastTempRead >= TEMP_INTERVAL_MS) {
int chk = DHT.read22(DHT22_PIN);
temp = DHT.temperature;
lastTempRead = now;
}
}
void Draw()
{
lcd.home();
if (chk != DHTLIB_ERROR_TIMEOUT) {
lcd.print("Temp:");
lcd.print(temp);
lcd.write(223); //degrees symbol
lcd.print("C");
lcd.print(" "); //Quick fix for when degrees goes from 10 degrees to 5
} else {
lcd.print("Temp Err");
}
lcd.setCursor(0,1);
lcd.print("Shake:"); lcd.print(shakePercent, 1);
lcd.print("%");
lcd.print(" ");
}
void loop() {
UpdateSensors();
if (shakePercent <= LOW_SHAKE_PCT) {
lowAlarmTriggered = medAlarmTriggered = bigAlarmTriggered = false;
digitalWrite(lowLEDPin, LOW);
digitalWrite(medLEDPin, LOW);
digitalWrite(highLEDPin, LOW);
}
if(temp <= OPTIMAL_TEMP)
{
tempAlarmTriggered = false;
digitalWrite(TempLEDPin, LOW);
}
if(temp >= TEMP_THRESHOLD && tempAlarmTriggered == false)
{
alarm.start(AlarmManager::TOO_WARM, now);
tempAlarmTriggered = true;
digitalWrite(TempLEDPin, HIGH);
}
//Triggers the alarms when over the level
//BUG: If more than 1 alarm sounds at once nothing plays
//UPDATE: Issue is actually the button is "sticky"
//FIXED: Wiring was wrong and logic for button wasn't right
//
if (shakePercent > HIGH_SHAKE_PCT && bigAlarmTriggered == false) {
bigAlarmTriggered = true;
alarm.start(AlarmManager::BIG_IMP, now);
digitalWrite(highLEDPin, HIGH);
} else if (shakePercent > MED_SHAKE_PCT && medAlarmTriggered == false) {
alarm.start(AlarmManager::MED_IMP, now);
digitalWrite(medLEDPin, HIGH);
medAlarmTriggered = true;
} else if (shakePercent > LOW_SHAKE_PCT && lowAlarmTriggered == false) {
alarm.start(AlarmManager::SMALL_IMP, now);
digitalWrite(lowLEDPin, HIGH);
lowAlarmTriggered = true;
}
Draw();
}