// ============================================================
// LIBRARIES
// ============================================================
#include <Wire.h> // Lets the ESP32 communicate with I2C devices
#include <LiquidCrystal_I2C.h> // Lets us control the I2C LCD screen
#include <Adafruit_LTR390.h> // Lets us read the LTR390 UV/light sensor
#include <DHTesp.h> // Lets us read the DHT temperature/humidity sensor
#include <ESP32Servo.h> // Lets us control a servo motor on ESP32 boards
// ============================================================
// PIN DEFINITIONS
// ============================================================
const int SOUND_PIN = D0; // Sound sensor analogue output pin
const int LED_PIN = D2; // Red power LED pin
const int DHT_PIN = D3; // DHT data pin
const int BUZZER_PIN = D6; // Passive buzzer pin
const int SERVO_PIN = D7; // Servo signal pin
const int FAN_PIN = D8; // Fan motor control pin (through transistor/MOSFET)
// ============================================================
// I2C PIN DEFINITIONS
// ============================================================
const int SDA_PIN = D4; // I2C data pin
const int SCL_PIN = D5; // I2C clock pin
// ============================================================
// THRESHOLD VALUES
// ============================================================
const float TEMP_FAN_THRESHOLD = 28.0; // Turn fan on above 28°C
const float SOUND_DB_THRESHOLD = 65.0; // Warn user if estimated sound is below 65 dB
const float UV_WARN_INDEX = 6.0; // Recommend sunscreen at UV index 6 or above
const uint32_t LUX_SUNGLASSES_THRESHOLD = 10000; // Recommend sunglasses above this light level
// ============================================================
// SERVO ANGLE CONSTANTS
// ============================================================
const unsigned int SERVO_CLOSED_ANGLE = 0;
const unsigned int SERVO_OPEN_ANGLE = 90;
// ============================================================
// SOUND CALIBRATION VALUES
// ============================================================
// Replace these example ADC values with our measured values
// from the KY-038 using a reference sound level meter.
//
// Example process:
// - play or produce about 60 dB, record ADC value
// - play or produce about 80 dB, record ADC value
// - play or produce about 90 dB, record ADC value
//
// IMPORTANT:
// These are placeholder values for demonstration only.
const int ADC_AT_60DB = 500;
const int ADC_AT_80DB = 1400;
const int ADC_AT_90DB = 2200;
// ============================================================
// HARDWARE OBJECTS
// ============================================================
LiquidCrystal_I2C lcd(0x27, 16, 2); // LCD at I2C address 0x27, 16 columns, 2 rows
Adafruit_LTR390 ltr = Adafruit_LTR390(); // Create LTR390 sensor object
DHTesp dhtSensor; // Create DHT sensor object
Servo hatServo; // Create servo object
// ============================================================
// SENSOR VALUE VARIABLES
// ============================================================
float temperatureC = 0.0; // Stores temperature in degrees Celsius
float humidityRH = 0.0; // Stores relative humidity in percent
float uvIndex = 0.0; // Stores estimated UV index
uint32_t lux = 0; // Stores light level in lux
int soundValue = 0; // Stores raw sound sensor ADC reading
float soundDb = 0.0; // Stores estimated sound level in dB
// ============================================================
// SYSTEM STATE VARIABLES
// ============================================================
bool fanOn = false; // True if the fan is currently on
bool buzzerOn = false; // True if the buzzer is currently sounding
bool ltrAvailable = false; // True if the LTR390 sensor was found
// ============================================================
// TIMING VARIABLES
// ============================================================
unsigned long lastSensorRead = 0; // Time of last sensor reading
unsigned long lastDisplayUpdate = 0; // Time of last LCD update
unsigned long lastFanMove = 0; // Time of last fan/servo movement
unsigned long lastSerialPrint = 0; // Time of last Serial print
const unsigned long SENSOR_INTERVAL = 1000; // Read sensors every 1000 ms = 1 second
const unsigned long DISPLAY_INTERVAL = 1500; // Update LCD every 1500 ms = 1.5 seconds
const unsigned long FAN_MOVE_INTERVAL = 30000; // Reposition fan every 30000 ms = 30 seconds
const unsigned long SERIAL_PRINT_INTERVAL = 5000; // Print to Serial every 5000 ms = 5 seconds
// ============================================================
// HELPER FUNCTION: CLAMP FLOAT
// ============================================================
float clampFloat(float value, float minValue, float maxValue) {
if (value < minValue) return minValue;
if (value > maxValue) return maxValue;
return value;
}
// ============================================================
// HELPER FUNCTION: MAP FLOAT
// ============================================================
float mapFloat(float x, float in_min, float in_max, float out_min, float out_max) {
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}
// ============================================================
// HELPER FUNCTION: CONVERT ADC TO ESTIMATED dB
// ============================================================
// Uses piecewise linear interpolation between three calibration points.
// Below the 60 dB point, the value is clamped to 60 dB.
// Above the 90 dB point, the value is clamped to 90 dB.
//
// This is only a rough estimate, not a proper sound level meter.
float estimateDbFromADC(int adcValue) {
if (adcValue <= ADC_AT_60DB) {
return 60.0;
}
if (adcValue <= ADC_AT_80DB) {
return mapFloat(adcValue, ADC_AT_60DB, ADC_AT_80DB, 60.0, 80.0);
}
if (adcValue <= ADC_AT_90DB) {
return mapFloat(adcValue, ADC_AT_80DB, ADC_AT_90DB, 80.0, 90.0);
}
return 90.0;
}
// ============================================================
// HELPER FUNCTION: READ TEMPERATURE AND HUMIDITY
// ============================================================
void readTempHumidity() {
TempAndHumidity data = dhtSensor.getTempAndHumidity();
if (!isnan(data.temperature) && !isnan(data.humidity)) {
temperatureC = data.temperature;
humidityRH = data.humidity;
}
}
// ============================================================
// HELPER FUNCTION: READ SOUND LEVEL
// ============================================================
// Reads several ADC samples, averages them, then estimates dB.
// Averaging makes the reading less twitchy.
void readSound() {
const int NUM_SAMPLES = 10;
long total = 0;
for (int i = 0; i < NUM_SAMPLES; i++) {
total += analogRead(SOUND_PIN);
delay(2);
}
soundValue = total / NUM_SAMPLES;
soundDb = estimateDbFromADC(soundValue);
soundDb = clampFloat(soundDb, 60.0, 90.0);
}
// ============================================================
// HELPER FUNCTION: READ UV LEVEL
// ============================================================
void readUV() {
if (!ltrAvailable) {
uvIndex = 3.0; // Fake value for simulation
return;
}
ltr.setMode(LTR390_MODE_UVS);
delay(50);
if (ltr.newDataAvailable()) {
uint32_t uvs = ltr.readUVS();
uvIndex = uvs / 25.0;
}
}
// ============================================================
// HELPER FUNCTION: READ LIGHT LEVEL
// ============================================================
void readALS() {
if (!ltrAvailable) {
lux = 8000; // Fake value for simulation
return;
}
ltr.setMode(LTR390_MODE_ALS);
delay(50);
if (ltr.newDataAvailable()) {
lux = ltr.readALS();
}
}
// ============================================================
// HELPER FUNCTION: PRINT DATA TO SERIAL MONITOR
// ============================================================
void printToSerial() {
Serial.print("Temp: ");
Serial.print(temperatureC);
Serial.print(" C, Humidity: ");
Serial.print(humidityRH);
Serial.print(" %, UV index: ");
Serial.print(uvIndex);
Serial.print(", Lux: ");
Serial.print(lux);
Serial.print(", Sound ADC: ");
Serial.print(soundValue);
Serial.print(", Est. dB: ");
Serial.println(soundDb);
}
// ============================================================
// HELPER FUNCTION: CONTROL THE FAN AND SERVO
// ============================================================
void controlFan(unsigned long now) {
if (temperatureC >= TEMP_FAN_THRESHOLD) {
if (!fanOn) {
hatServo.write(SERVO_OPEN_ANGLE);
delay(1000);
digitalWrite(FAN_PIN, HIGH);
fanOn = true;
lastFanMove = now;
}
else if (now - lastFanMove >= FAN_MOVE_INTERVAL) {
hatServo.write(SERVO_CLOSED_ANGLE);
delay(500);
hatServo.write(SERVO_OPEN_ANGLE);
delay(500);
lastFanMove = now;
}
} else {
if (fanOn) {
digitalWrite(FAN_PIN, LOW);
delay(200);
hatServo.write(SERVO_CLOSED_ANGLE);
fanOn = false;
lastFanMove = now;
}
}
}
// ============================================================
// HELPER FUNCTION: CONTROL THE BUZZER
// ============================================================
// Turn the buzzer on if estimated sound level is below threshold.
void controlBuzzer() {
if (soundDb < SOUND_DB_THRESHOLD) {
digitalWrite(BUZZER_PIN, HIGH);
buzzerOn = true;
} else {
digitalWrite(BUZZER_PIN, LOW);
buzzerOn = false;
}
}
// ============================================================
// HELPER FUNCTION: UPDATE THE LCD
// ============================================================
void updateDisplay() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("T:");
lcd.print(temperatureC, 1);
lcd.print((char)223);
lcd.print("C S:");
lcd.print(soundDb, 0);
lcd.print("dB");
lcd.setCursor(0, 1);
if (uvIndex >= UV_WARN_INDEX) {
lcd.print("Use sunscreen");
} else if (lux >= LUX_SUNGLASSES_THRESHOLD) {
lcd.print("Wear shades");
} else if (fanOn) {
lcd.print("Fan on");
} else if (buzzerOn) {
lcd.print("Make noise!");
} else {
lcd.print("Conditions OK");
}
}
// ============================================================
// SETUP FUNCTION
// ============================================================
void setup() {
Serial.begin(115200);
delay(1000);
pinMode(SOUND_PIN, INPUT);
pinMode(LED_PIN, OUTPUT);
pinMode(BUZZER_PIN, OUTPUT);
pinMode(FAN_PIN, OUTPUT);
digitalWrite(LED_PIN, HIGH);
digitalWrite(FAN_PIN, LOW);
digitalWrite(BUZZER_PIN, LOW);
Wire.begin(SDA_PIN, SCL_PIN);
lcd.init();
lcd.backlight();
lcd.setCursor(0, 0);
lcd.print("Hiking Hat");
lcd.setCursor(0, 1);
lcd.print("Starting...");
dhtSensor.setup(DHT_PIN, DHTesp::DHT22);
ltrAvailable = ltr.begin();
if (!ltrAvailable) {
Serial.println("LTR390 not found - sim mode");
} else {
ltr.setGain(LTR390_GAIN_3);
ltr.setResolution(LTR390_RESOLUTION_16BIT);
}
hatServo.attach(SERVO_PIN);
hatServo.write(SERVO_CLOSED_ANGLE);
delay(1000);
lcd.clear();
}
// ============================================================
// LOOP FUNCTION
// ============================================================
void loop() {
unsigned long now = millis();
if (now - lastSensorRead >= SENSOR_INTERVAL) {
lastSensorRead = now;
readTempHumidity();
readUV();
readALS();
readSound();
controlFan(now);
controlBuzzer();
}
if (now - lastDisplayUpdate >= DISPLAY_INTERVAL) {
lastDisplayUpdate = now;
updateDisplay();
}
if (now - lastSerialPrint >= SERIAL_PRINT_INTERVAL) {
lastSerialPrint = now;
printToSerial();
}
}