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 ## Features
- **Vibration Sensing:** Detects machine operation by monitoring vibrations. - **Dual Light Sensing:** Monitors two separate devices using two BH1750 light sensors.
- **Light Sensing:** Detects the machine's internal light to determine if the door is open.
- **Status Display:** Shows real-time sensor data and device status on a small OLED display. - **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. - **Notifications:** Sends start and stop notifications to a `ntfy.sh` topic.
- **WiFi Connectivity:** Connects to your local WiFi network. - **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 ## Hardware Requirements
- ESP32 development board - ESP32 development board
- SW-420 vibration sensor module (or similar) - 2x BH1750 light sensor modules
- BH1750 light sensor module
- SSD1306 128x64 OLED display (I2C) - SSD1306 128x64 OLED display (I2C)
- Breadboard and jumper wires - Breadboard and jumper wires
## Pinout ## Pinout
| Component | Pin on ESP32 | | Component | Pin on ESP32 | Notes |
| ------------------- | ------------ | | ------------------- | ------------ | ----- |
| Vibration Sensor | GPIO 4 | | I2C SCL (OLED, Sensors)| GPIO 22 | Shared bus |
| I2C SCL (OLED, BH1750) | GPIO 22 | | I2C SDA (OLED, Sensors)| GPIO 21 | Shared bus |
| I2C SDA (OLED, BH1750) | GPIO 21 |
### Wiring Diagram ### Wiring Diagram
``` ```
+-----------------+ +-----------------+ +-----------------+ +-----------------+ +-----------------+ +-----------------+ +-----------------+
| ESP32 Dev Board | | Vibration Sensor| | BH1750 Sensor | | OLED Display | | ESP32 Dev Board | | BH1750 Sensor | | OLED Display |
+-----------------+ +-----------------+ +-----------------+ +-----------------+ +-----------------+ +-----------------+ +-----------------+
| 3.3V |----->| VCC |----->| VCC |----->| VCC | | 3.3V |---------->| VCC |----->| VCC |
| GND |----->| GND |----->| GND |----->| GND | | GND |---------->| GND |----->| GND |
| GPIO 4 |----->| OUT | | | | | | | | | | |
| | +-----------------+ | | | | | GPIO 22 (SCL) |---------->| SCL |----->| SCL |
| GPIO 22 (SCL) |------------------------------>| SCL |----->| SCL | | GPIO 21 (SDA) |---------->| SDA |----->| SDA |
| 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 ## Setup & Usage
@ -61,19 +58,16 @@ This project turns an ESP32 into a smart device that monitors a washing machine'
- Click the upload button. - Click the upload button.
4. **Deploy the Device:** 4. **Deploy the Device:**
- Place the vibration sensor on the washing machine where it can detect vibrations from the spin cycle. - Place Sensor 1 inside the first machine (e.g., Washer) to detect internal light.
- Place the light sensor inside the machine's drum to detect the internal light. - Place Sensor 2 inside the second machine (e.g., Dryer) to detect internal light.
## Code Explanation ## Code Explanation
The code is structured as follows: 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. - It continuously reads the values from both light sensors.
- **`setup()`:** Initializes serial communication, I2C, sensors, the OLED display, and connects to WiFi. - 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.
- **`loop()`:** This is the main part of the program. - It updates the overall device states (`isDevice1Active`, `isDevice2Active`) based on the individual sensor states.
- 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.
- If the device state changes, it calls `sendNotification()`. - If the device state changes, it calls `sendNotification()`.
- It calls `updateDisplay()` to show the latest data. - It calls `updateDisplay()` to show the latest data.
- **`sendNotification()`:** Sends a POST request to the configured `ntfy.sh` topic with a message. - **`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; const char* NTFY_TOPIC = SECRET_NTFY_TOPIC;
// Device Names // Device Names
const char* DEVICE1_NAME = "Dryer"; // Monitored by Vibration const char* DEVICE1_NAME = "Wash"; // Monitored by Light 1 (0x23)
const char* DEVICE2_NAME = "Washer"; // Monitored by Light const char* DEVICE2_NAME = "Dry "; // Monitored by Light 2 (0x5C)
// Pin definitions
const int VIBRATION_PIN = 4;
// OLED display settings // OLED display settings
#define SCREEN_WIDTH 128 #define SCREEN_WIDTH 128
@ -51,19 +48,19 @@ Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// Light sensor // Light sensor
#ifdef ENABLE_SENSORS #ifdef ENABLE_SENSORS
BH1750 lightMeter; BH1750 lightMeter1(0x23);
BH1750 lightMeter2(0x5C);
#endif #endif
// Thresholds // Thresholds
const float LIGHT_ACTIVATION_THRESHOLD = 50.0; const float LIGHT_ACTIVATION_THRESHOLD = 50.0;
const unsigned long WINDOW_DURATION = 10000; // 10 seconds const unsigned long WINDOW_DURATION = 10000; // 10 seconds
const int REQUIRED_CONSECUTIVE_WINDOWS = 6; const int REQUIRED_CONSECUTIVE_WINDOWS = 6;
const unsigned long VIBRATION_ACTIVE_THRESHOLD = 100000;
const unsigned long WARMUP_DURATION = 30000; // 20 seconds warmup const unsigned long WARMUP_DURATION = 30000; // 20 seconds warmup
// Device state // Device state
bool isVibrationActive = false; bool isDevice1Active = false;
bool isLightActive = false; bool isDevice2Active = false;
// Time tracking for sensor states // Time tracking for sensor states
unsigned long lastWindowStartTime = 0; unsigned long lastWindowStartTime = 0;
@ -88,29 +85,13 @@ QueueHandle_t notificationQueue;
volatile bool isNetworkActive = false; volatile bool isNetworkActive = false;
#ifdef ENABLE_SENSORS #ifdef ENABLE_SENSORS
unsigned long vibrationTotalLow = 0; int light1ConsecutiveOn = 0;
int vibConsecutiveOn = 0; int light1ConsecutiveOff = 0;
int vibConsecutiveOff = 0; float maxLight1InWindow = 0.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;
void IRAM_ATTR onChange() { int light2ConsecutiveOn = 0;
unsigned long now = micros(); int light2ConsecutiveOff = 0;
unsigned long duration = now - lastChangeTime; float maxLight2InWindow = 0.0;
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;
}
}
#endif #endif
#ifdef ENABLE_WIFI #ifdef ENABLE_WIFI
@ -180,14 +161,13 @@ void setup() {
#endif #endif
#ifdef ENABLE_SENSORS #ifdef ENABLE_SENSORS
// Initialize vibration sensor pin // Initialize light sensors
pinMode(VIBRATION_PIN, INPUT_PULLUP); if (!lightMeter1.begin(BH1750::CONTINUOUS_HIGH_RES_MODE, 0x23)) {
lastChangeTime = micros(); Serial.println(F("Could not find valid BH1750 sensor at 0x23, check wiring!"));
attachInterrupt(digitalPinToInterrupt(VIBRATION_PIN), onChange, CHANGE); while (1) { }
}
// Initialize light sensor if (!lightMeter2.begin(BH1750::CONTINUOUS_HIGH_RES_MODE, 0x5C)) {
if (!lightMeter.begin(BH1750::CONTINUOUS_HIGH_RES_MODE)) { Serial.println(F("Could not find valid BH1750 sensor at 0x5C, check wiring!"));
Serial.println(F("Could not find a valid BH1750 sensor, check wiring!"));
while (1) { } while (1) { }
} }
#endif #endif
@ -202,8 +182,17 @@ void setup() {
display.setTextSize(1); display.setTextSize(1);
display.setTextColor(WHITE); display.setTextColor(WHITE);
display.setCursor(0, 0); display.setCursor(0, 0);
display.println(F("Initializing...")); display.println(F("Hello!"));
display.display(); 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 #endif
#ifdef ENABLE_WIFI #ifdef ENABLE_WIFI
@ -234,6 +223,13 @@ void setup() {
delay(1000); delay(1000);
#endif #endif
display.clearDisplay();
display.setCursor(0, 0);
display.println(F("Topic:"));
display.println(NTFY_TOPIC);
display.display();
delay(1000);
#endif
Serial.println("Setup finished."); Serial.println("Setup finished.");
} }
@ -245,83 +241,70 @@ void loop() {
// Sensor readings // Sensor readings
#ifdef ENABLE_SENSORS #ifdef ENABLE_SENSORS
float currentLightLevel = lightMeter.readLightLevel(); float currentLight1 = lightMeter1.readLightLevel();
if (currentLightLevel > maxLightInWindow) { if (currentLight1 > maxLight1InWindow) maxLight1InWindow = currentLight1;
maxLightInWindow = currentLightLevel;
} float currentLight2 = lightMeter2.readLightLevel();
if (currentLight2 > maxLight2InWindow) maxLight2InWindow = currentLight2;
#else #else
float currentLightLevel = 0.0; float currentLight1 = 0.0;
float currentLight2 = 0.0;
#endif #endif
bool previousVibrationState = isVibrationActive; bool previousDevice1State = isDevice1Active;
bool previousLightState = isLightActive; bool previousDevice2State = isDevice2Active;
// Window Logic // Window Logic
if (currentTime - lastWindowStartTime >= WINDOW_DURATION) { if (currentTime - lastWindowStartTime >= WINDOW_DURATION) {
lastWindowStartTime = currentTime; lastWindowStartTime = currentTime;
// 1. Process Vibration // 1. Process Device 1 (Light 1)
#ifdef ENABLE_SENSORS #ifdef ENABLE_SENSORS
noInterrupts(); bool light1WindowOn = (maxLight1InWindow > LIGHT_ACTIVATION_THRESHOLD);
unsigned long now = micros();
unsigned long duration = now - lastChangeTime;
lastChangeTime = now;
if (digitalRead(VIBRATION_PIN) == LOW) { if (light1WindowOn) {
lowTimeAccumulator += duration; light1ConsecutiveOn++;
light1ConsecutiveOff = 0;
} else { } else {
highTimeAccumulator += duration; light1ConsecutiveOff++;
light1ConsecutiveOn = 0;
} }
vibrationTotalLow = lowTimeAccumulator; if (light1ConsecutiveOn >= REQUIRED_CONSECUTIVE_WINDOWS && currentTime > WARMUP_DURATION) {
lowTimeAccumulator = 0; isDevice1Active = true;
highTimeAccumulator = 0; } else if (light1ConsecutiveOff >= REQUIRED_CONSECUTIVE_WINDOWS) {
interrupts(); isDevice1Active = false;
}
bool vibWindowOn = (vibrationTotalLow > VIBRATION_ACTIVE_THRESHOLD); // 2. Process Device 2 (Light 2)
bool light2WindowOn = (maxLight2InWindow > LIGHT_ACTIVATION_THRESHOLD);
if (vibWindowOn) { if (light2WindowOn) {
vibConsecutiveOn++; light2ConsecutiveOn++;
vibConsecutiveOff = 0; light2ConsecutiveOff = 0;
} else { } else {
vibConsecutiveOff++; light2ConsecutiveOff++;
vibConsecutiveOn = 0; light2ConsecutiveOn = 0;
} }
if (vibConsecutiveOn >= REQUIRED_CONSECUTIVE_WINDOWS && currentTime > WARMUP_DURATION) { if (light2ConsecutiveOn >= REQUIRED_CONSECUTIVE_WINDOWS && currentTime > WARMUP_DURATION) {
isVibrationActive = true; isDevice2Active = true;
} else if (vibConsecutiveOff >= REQUIRED_CONSECUTIVE_WINDOWS) { } else if (light2ConsecutiveOff >= REQUIRED_CONSECUTIVE_WINDOWS) {
isVibrationActive = false; isDevice2Active = false;
} }
// Reset Light Accumulators
maxLight1InWindow = 0.0;
maxLight2InWindow = 0.0;
#endif #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 // Device 1 Notifications
if (isVibrationActive != previousVibrationState) { if (isDevice1Active != previousDevice1State) {
String message; String message;
String title; String title;
String priority; String priority;
if (isVibrationActive) { if (isDevice1Active) {
message = String(DEVICE1_NAME) + " started."; message = String(DEVICE1_NAME) + " started.";
title = String(DEVICE1_NAME) + " START"; title = String(DEVICE1_NAME) + " START";
priority = "default"; priority = "default";
@ -349,18 +332,18 @@ void loop() {
#endif #endif
} }
// Device 2 (Light) Notifications // Device 2 Notifications
if (isLightActive != previousLightState) { if (isDevice2Active != previousDevice2State) {
String message; String message;
String title; String title;
String priority; String priority;
if (isLightActive) { if (isDevice2Active) {
message = String(DEVICE2_NAME) + " active."; message = String(DEVICE2_NAME) + " started.";
title = String(DEVICE2_NAME) + " ACTIVE"; title = String(DEVICE2_NAME) + " START";
priority = "default"; priority = "default";
} else { } else {
message = String(DEVICE2_NAME) + " inactive."; message = String(DEVICE2_NAME) + " finished.";
title = String(DEVICE2_NAME) + " INACTIVE"; title = String(DEVICE2_NAME) + " END";
priority = "high"; priority = "high";
} }
@ -384,10 +367,10 @@ void loop() {
#ifdef ENABLE_DISPLAY #ifdef ENABLE_DISPLAY
// Update display data // Update display data
displayData[0] = String(DEVICE1_NAME) + ": " + (vibrationTotalLow > VIBRATION_ACTIVE_THRESHOLD ? "YES" : "NO"); displayData[0] = String(DEVICE1_NAME) + ": " + String(currentLight1, 0) + (currentLight1 > LIGHT_ACTIVATION_THRESHOLD ? " = ON" : " = OFF");
displayData[1] = "Vibra: " + String(isVibrationActive ? "OFF? " + String(vibConsecutiveOff) : "ON? " + String(vibConsecutiveOn)) + "/" + String(REQUIRED_CONSECUTIVE_WINDOWS); displayData[1] = " " + String(isDevice1Active ? "OFF? " + String(light1ConsecutiveOff) : "ON? " + String(light1ConsecutiveOn)) + "/" + String(REQUIRED_CONSECUTIVE_WINDOWS);
displayData[2] = String(DEVICE2_NAME) + ": " + String(currentLightLevel, 0); displayData[2] = String(DEVICE2_NAME) + ": " + String(currentLight2, 0) + (currentLight2 > LIGHT_ACTIVATION_THRESHOLD ? " = ON" : " = OFF");
displayData[3] = "Light: " + String(isLightActive ? "OFF? " + String(lightConsecutiveOff) : "ON? " + String(lightConsecutiveOn)) + "/" + String(REQUIRED_CONSECUTIVE_WINDOWS); displayData[3] = " " + String(isDevice2Active ? "OFF? " + String(light2ConsecutiveOff) : "ON? " + String(light2ConsecutiveOn)) + "/" + String(REQUIRED_CONSECUTIVE_WINDOWS);
displayDataLines = 4; displayDataLines = 4;
// Update display // Update display
@ -426,7 +409,7 @@ void updateDisplay() {
// Draw Device 1 Indicator (Top) // Draw Device 1 Indicator (Top)
display.drawRect(rectX, rectY1, rectWidth, rectHeight, WHITE); display.drawRect(rectX, rectY1, rectWidth, rectHeight, WHITE);
if (isVibrationActive) { if (isDevice1Active) {
int innerWidth = rectWidth - 4; int innerWidth = rectWidth - 4;
int step = (millis() / 150) % (innerWidth + 1); int step = (millis() / 150) % (innerWidth + 1);
display.fillRect(rectX + 2, rectY1 + 2, step, rectHeight - 4, WHITE); display.fillRect(rectX + 2, rectY1 + 2, step, rectHeight - 4, WHITE);
@ -434,7 +417,7 @@ void updateDisplay() {
// Draw Device 2 Indicator (Bottom) // Draw Device 2 Indicator (Bottom)
display.drawRect(rectX, rectY2, rectWidth, rectHeight, WHITE); display.drawRect(rectX, rectY2, rectWidth, rectHeight, WHITE);
if (isLightActive) { if (isDevice2Active) {
int innerWidth = rectWidth - 4; int innerWidth = rectWidth - 4;
int step = (millis() / 150) % (innerWidth + 1); int step = (millis() / 150) % (innerWidth + 1);
display.fillRect(rectX + 2, rectY2 + 2, step, rectHeight - 4, WHITE); display.fillRect(rectX + 2, rectY2 + 2, step, rectHeight - 4, WHITE);