This commit is contained in:
Dejvino 2026-02-22 17:25:26 +01:00
parent eba13c6181
commit fa6c2a753a
2 changed files with 111 additions and 134 deletions

View File

@ -1,11 +1,10 @@
# ESP32 Washing Machine Activity Notifier
# ESP32 Dual Machine Activity Notifier
This project turns an ESP32 into a smart device that monitors a washing machine's activity using vibration and light sensors and sends notifications when a cycle starts and finishes. It also displays the current status on an OLED display.
This project turns an ESP32 into a smart device that monitors two machines (e.g., washer and dryer) using light sensors and sends notifications when a cycle starts and finishes. It also displays the current status on an OLED display.
## Features
- **Vibration Sensing:** Detects machine operation by monitoring vibrations.
- **Light Sensing:** Detects the machine's internal light to determine if the door is open.
- **Dual Light Sensing:** Monitors two separate devices using two BH1750 light sensors.
- **Status Display:** Shows real-time sensor data and device status on a small OLED display.
- **Notifications:** Sends start and stop notifications to a `ntfy.sh` topic.
- **WiFi Connectivity:** Connects to your local WiFi network.
@ -13,33 +12,31 @@ This project turns an ESP32 into a smart device that monitors a washing machine'
## Hardware Requirements
- ESP32 development board
- SW-420 vibration sensor module (or similar)
- BH1750 light sensor module
- 2x BH1750 light sensor modules
- SSD1306 128x64 OLED display (I2C)
- Breadboard and jumper wires
## Pinout
| Component | Pin on ESP32 |
| ------------------- | ------------ |
| Vibration Sensor | GPIO 4 |
| I2C SCL (OLED, BH1750) | GPIO 22 |
| I2C SDA (OLED, BH1750) | GPIO 21 |
| Component | Pin on ESP32 | Notes |
| ------------------- | ------------ | ----- |
| I2C SCL (OLED, Sensors)| GPIO 22 | Shared bus |
| I2C SDA (OLED, Sensors)| GPIO 21 | Shared bus |
### Wiring Diagram
```
+-----------------+ +-----------------+ +-----------------+ +-----------------+
| ESP32 Dev Board | | Vibration Sensor| | BH1750 Sensor | | OLED Display |
+-----------------+ +-----------------+ +-----------------+ +-----------------+
| 3.3V |----->| VCC |----->| VCC |----->| VCC |
| GND |----->| GND |----->| GND |----->| GND |
| GPIO 4 |----->| OUT | | | | |
| | +-----------------+ | | | |
| GPIO 22 (SCL) |------------------------------>| SCL |----->| SCL |
| GPIO 21 (SDA) |------------------------------>| SDA |----->| SDA |
+-----------------+ +-----------------+ +-----------------+
| ESP32 Dev Board | | BH1750 Sensor | | OLED Display |
+-----------------+ +-----------------+ +-----------------+
| 3.3V |---------->| VCC |----->| VCC |
| GND |---------->| GND |----->| GND |
| | | | | |
| GPIO 22 (SCL) |---------->| SCL |----->| SCL |
| GPIO 21 (SDA) |---------->| SDA |----->| SDA |
+-----------------+ +-----------------+ +-----------------+
```
**Note:** For Sensor 1, connect the ADDR pin to GND (or leave floating if default is low). For Sensor 2, connect the ADDR pin to 3.3V to change its I2C address to 0x5C.
## Setup & Usage
@ -61,19 +58,16 @@ This project turns an ESP32 into a smart device that monitors a washing machine'
- Click the upload button.
4. **Deploy the Device:**
- Place the vibration sensor on the washing machine where it can detect vibrations from the spin cycle.
- Place the light sensor inside the machine's drum to detect the internal light.
- Place Sensor 1 inside the first machine (e.g., Washer) to detect internal light.
- Place Sensor 2 inside the second machine (e.g., Dryer) to detect internal light.
## Code Explanation
The code is structured as follows:
- **Includes and Definitions:** Includes the necessary libraries for WiFi, I2C devices, and the display. It also defines pins, thresholds, and other constants.
- **`setup()`:** Initializes serial communication, I2C, sensors, the OLED display, and connects to WiFi.
- **`loop()`:** This is the main part of the program.
- It continuously reads the values from the vibration and light sensors.
- It implements a non-blocking logic using `millis()` to check if the sensors have been in a certain state (e.g., vibration active or light level high) for a continuous period of 5 seconds.
- It updates the overall device state (`isDeviceActive`) based on the individual sensor states.
- It continuously reads the values from both light sensors.
- It implements a non-blocking logic using `millis()` to check if the sensors have been in a certain state (light level high) for a continuous period.
- It updates the overall device states (`isDevice1Active`, `isDevice2Active`) based on the individual sensor states.
- If the device state changes, it calls `sendNotification()`.
- It calls `updateDisplay()` to show the latest data.
- **`sendNotification()`:** Sends a POST request to the configured `ntfy.sh` topic with a message.

View File

@ -34,11 +34,8 @@ const char* WIFI_PASSWORD = SECRET_WIFI_PASSWORD;
const char* NTFY_TOPIC = SECRET_NTFY_TOPIC;
// Device Names
const char* DEVICE1_NAME = "Dryer"; // Monitored by Vibration
const char* DEVICE2_NAME = "Washer"; // Monitored by Light
// Pin definitions
const int VIBRATION_PIN = 4;
const char* DEVICE1_NAME = "Wash"; // Monitored by Light 1 (0x23)
const char* DEVICE2_NAME = "Dry "; // Monitored by Light 2 (0x5C)
// OLED display settings
#define SCREEN_WIDTH 128
@ -51,19 +48,19 @@ Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// Light sensor
#ifdef ENABLE_SENSORS
BH1750 lightMeter;
BH1750 lightMeter1(0x23);
BH1750 lightMeter2(0x5C);
#endif
// Thresholds
const float LIGHT_ACTIVATION_THRESHOLD = 50.0;
const unsigned long WINDOW_DURATION = 10000; // 10 seconds
const int REQUIRED_CONSECUTIVE_WINDOWS = 6;
const unsigned long VIBRATION_ACTIVE_THRESHOLD = 100000;
const unsigned long WARMUP_DURATION = 30000; // 20 seconds warmup
// Device state
bool isVibrationActive = false;
bool isLightActive = false;
bool isDevice1Active = false;
bool isDevice2Active = false;
// Time tracking for sensor states
unsigned long lastWindowStartTime = 0;
@ -88,29 +85,13 @@ QueueHandle_t notificationQueue;
volatile bool isNetworkActive = false;
#ifdef ENABLE_SENSORS
unsigned long vibrationTotalLow = 0;
int vibConsecutiveOn = 0;
int vibConsecutiveOff = 0;
int lightConsecutiveOn = 0;
int lightConsecutiveOff = 0;
float maxLightInWindow = 0.0;
volatile unsigned long lastChangeTime = 0;
volatile unsigned long lowTimeAccumulator = 0;
volatile unsigned long highTimeAccumulator = 0;
int light1ConsecutiveOn = 0;
int light1ConsecutiveOff = 0;
float maxLight1InWindow = 0.0;
void IRAM_ATTR onChange() {
unsigned long now = micros();
unsigned long duration = now - lastChangeTime;
lastChangeTime = now;
if (digitalRead(VIBRATION_PIN) == HIGH) {
// Changed to HIGH, so previous state was LOW
lowTimeAccumulator += duration;
} else {
// Changed to LOW, so previous state was HIGH
highTimeAccumulator += duration;
}
}
int light2ConsecutiveOn = 0;
int light2ConsecutiveOff = 0;
float maxLight2InWindow = 0.0;
#endif
#ifdef ENABLE_WIFI
@ -180,14 +161,13 @@ void setup() {
#endif
#ifdef ENABLE_SENSORS
// Initialize vibration sensor pin
pinMode(VIBRATION_PIN, INPUT_PULLUP);
lastChangeTime = micros();
attachInterrupt(digitalPinToInterrupt(VIBRATION_PIN), onChange, CHANGE);
// Initialize light sensor
if (!lightMeter.begin(BH1750::CONTINUOUS_HIGH_RES_MODE)) {
Serial.println(F("Could not find a valid BH1750 sensor, check wiring!"));
// Initialize light sensors
if (!lightMeter1.begin(BH1750::CONTINUOUS_HIGH_RES_MODE, 0x23)) {
Serial.println(F("Could not find valid BH1750 sensor at 0x23, check wiring!"));
while (1) { }
}
if (!lightMeter2.begin(BH1750::CONTINUOUS_HIGH_RES_MODE, 0x5C)) {
Serial.println(F("Could not find valid BH1750 sensor at 0x5C, check wiring!"));
while (1) { }
}
#endif
@ -202,8 +182,17 @@ void setup() {
display.setTextSize(1);
display.setTextColor(WHITE);
display.setCursor(0, 0);
display.println(F("Initializing..."));
display.println(F("Hello!"));
display.display();
delay(500);
#ifdef ENABLE_WIFI
display.clearDisplay();
display.setCursor(0, 0);
display.println(F("WiFi:"));
display.println(WIFI_SSID);
display.display();
delay(1000);
#endif
#ifdef ENABLE_WIFI
@ -234,6 +223,13 @@ void setup() {
delay(1000);
#endif
display.clearDisplay();
display.setCursor(0, 0);
display.println(F("Topic:"));
display.println(NTFY_TOPIC);
display.display();
delay(1000);
#endif
Serial.println("Setup finished.");
}
@ -245,83 +241,70 @@ void loop() {
// Sensor readings
#ifdef ENABLE_SENSORS
float currentLightLevel = lightMeter.readLightLevel();
if (currentLightLevel > maxLightInWindow) {
maxLightInWindow = currentLightLevel;
}
float currentLight1 = lightMeter1.readLightLevel();
if (currentLight1 > maxLight1InWindow) maxLight1InWindow = currentLight1;
float currentLight2 = lightMeter2.readLightLevel();
if (currentLight2 > maxLight2InWindow) maxLight2InWindow = currentLight2;
#else
float currentLightLevel = 0.0;
float currentLight1 = 0.0;
float currentLight2 = 0.0;
#endif
bool previousVibrationState = isVibrationActive;
bool previousLightState = isLightActive;
bool previousDevice1State = isDevice1Active;
bool previousDevice2State = isDevice2Active;
// Window Logic
if (currentTime - lastWindowStartTime >= WINDOW_DURATION) {
lastWindowStartTime = currentTime;
// 1. Process Vibration
// 1. Process Device 1 (Light 1)
#ifdef ENABLE_SENSORS
noInterrupts();
unsigned long now = micros();
unsigned long duration = now - lastChangeTime;
lastChangeTime = now;
bool light1WindowOn = (maxLight1InWindow > LIGHT_ACTIVATION_THRESHOLD);
if (digitalRead(VIBRATION_PIN) == LOW) {
lowTimeAccumulator += duration;
if (light1WindowOn) {
light1ConsecutiveOn++;
light1ConsecutiveOff = 0;
} else {
highTimeAccumulator += duration;
light1ConsecutiveOff++;
light1ConsecutiveOn = 0;
}
vibrationTotalLow = lowTimeAccumulator;
lowTimeAccumulator = 0;
highTimeAccumulator = 0;
interrupts();
if (light1ConsecutiveOn >= REQUIRED_CONSECUTIVE_WINDOWS && currentTime > WARMUP_DURATION) {
isDevice1Active = true;
} else if (light1ConsecutiveOff >= REQUIRED_CONSECUTIVE_WINDOWS) {
isDevice1Active = false;
}
bool vibWindowOn = (vibrationTotalLow > VIBRATION_ACTIVE_THRESHOLD);
// 2. Process Device 2 (Light 2)
bool light2WindowOn = (maxLight2InWindow > LIGHT_ACTIVATION_THRESHOLD);
if (vibWindowOn) {
vibConsecutiveOn++;
vibConsecutiveOff = 0;
if (light2WindowOn) {
light2ConsecutiveOn++;
light2ConsecutiveOff = 0;
} else {
vibConsecutiveOff++;
vibConsecutiveOn = 0;
light2ConsecutiveOff++;
light2ConsecutiveOn = 0;
}
if (vibConsecutiveOn >= REQUIRED_CONSECUTIVE_WINDOWS && currentTime > WARMUP_DURATION) {
isVibrationActive = true;
} else if (vibConsecutiveOff >= REQUIRED_CONSECUTIVE_WINDOWS) {
isVibrationActive = false;
if (light2ConsecutiveOn >= REQUIRED_CONSECUTIVE_WINDOWS && currentTime > WARMUP_DURATION) {
isDevice2Active = true;
} else if (light2ConsecutiveOff >= REQUIRED_CONSECUTIVE_WINDOWS) {
isDevice2Active = false;
}
// Reset Light Accumulators
maxLight1InWindow = 0.0;
maxLight2InWindow = 0.0;
#endif
// 2. Process Light
bool lightWindowOn = (maxLightInWindow > LIGHT_ACTIVATION_THRESHOLD);
if (lightWindowOn) {
lightConsecutiveOn++;
lightConsecutiveOff = 0;
} else {
lightConsecutiveOff++;
lightConsecutiveOn = 0;
}
if (lightConsecutiveOn >= REQUIRED_CONSECUTIVE_WINDOWS && currentTime > WARMUP_DURATION) {
isLightActive = true;
} else if (lightConsecutiveOff >= REQUIRED_CONSECUTIVE_WINDOWS) {
isLightActive = false;
}
// Reset Light Accumulator
maxLightInWindow = 0.0;
}
// Device 1 (Vibration) Notifications
if (isVibrationActive != previousVibrationState) {
// Device 1 Notifications
if (isDevice1Active != previousDevice1State) {
String message;
String title;
String priority;
if (isVibrationActive) {
if (isDevice1Active) {
message = String(DEVICE1_NAME) + " started.";
title = String(DEVICE1_NAME) + " START";
priority = "default";
@ -349,18 +332,18 @@ void loop() {
#endif
}
// Device 2 (Light) Notifications
if (isLightActive != previousLightState) {
// Device 2 Notifications
if (isDevice2Active != previousDevice2State) {
String message;
String title;
String priority;
if (isLightActive) {
message = String(DEVICE2_NAME) + " active.";
title = String(DEVICE2_NAME) + " ACTIVE";
if (isDevice2Active) {
message = String(DEVICE2_NAME) + " started.";
title = String(DEVICE2_NAME) + " START";
priority = "default";
} else {
message = String(DEVICE2_NAME) + " inactive.";
title = String(DEVICE2_NAME) + " INACTIVE";
message = String(DEVICE2_NAME) + " finished.";
title = String(DEVICE2_NAME) + " END";
priority = "high";
}
@ -384,10 +367,10 @@ void loop() {
#ifdef ENABLE_DISPLAY
// Update display data
displayData[0] = String(DEVICE1_NAME) + ": " + (vibrationTotalLow > VIBRATION_ACTIVE_THRESHOLD ? "YES" : "NO");
displayData[1] = "Vibra: " + String(isVibrationActive ? "OFF? " + String(vibConsecutiveOff) : "ON? " + String(vibConsecutiveOn)) + "/" + String(REQUIRED_CONSECUTIVE_WINDOWS);
displayData[2] = String(DEVICE2_NAME) + ": " + String(currentLightLevel, 0);
displayData[3] = "Light: " + String(isLightActive ? "OFF? " + String(lightConsecutiveOff) : "ON? " + String(lightConsecutiveOn)) + "/" + String(REQUIRED_CONSECUTIVE_WINDOWS);
displayData[0] = String(DEVICE1_NAME) + ": " + String(currentLight1, 0) + (currentLight1 > LIGHT_ACTIVATION_THRESHOLD ? " = ON" : " = OFF");
displayData[1] = " " + String(isDevice1Active ? "OFF? " + String(light1ConsecutiveOff) : "ON? " + String(light1ConsecutiveOn)) + "/" + String(REQUIRED_CONSECUTIVE_WINDOWS);
displayData[2] = String(DEVICE2_NAME) + ": " + String(currentLight2, 0) + (currentLight2 > LIGHT_ACTIVATION_THRESHOLD ? " = ON" : " = OFF");
displayData[3] = " " + String(isDevice2Active ? "OFF? " + String(light2ConsecutiveOff) : "ON? " + String(light2ConsecutiveOn)) + "/" + String(REQUIRED_CONSECUTIVE_WINDOWS);
displayDataLines = 4;
// Update display
@ -426,7 +409,7 @@ void updateDisplay() {
// Draw Device 1 Indicator (Top)
display.drawRect(rectX, rectY1, rectWidth, rectHeight, WHITE);
if (isVibrationActive) {
if (isDevice1Active) {
int innerWidth = rectWidth - 4;
int step = (millis() / 150) % (innerWidth + 1);
display.fillRect(rectX + 2, rectY1 + 2, step, rectHeight - 4, WHITE);
@ -434,7 +417,7 @@ void updateDisplay() {
// Draw Device 2 Indicator (Bottom)
display.drawRect(rectX, rectY2, rectWidth, rectHeight, WHITE);
if (isLightActive) {
if (isDevice2Active) {
int innerWidth = rectWidth - 4;
int step = (millis() / 150) % (innerWidth + 1);
display.fillRect(rectX + 2, rectY2 + 2, step, rectHeight - 4, WHITE);