#include #include "secrets.h" // Feature Configuration - Comment out to disable #define ENABLE_SENSORS #define ENABLE_DISPLAY #define ENABLE_WIFI #define ENABLE_NOTIFICATIONS #ifdef ENABLE_WIFI #include #include #include #include #include #endif #ifdef ENABLE_SENSORS #include #endif #ifdef ENABLE_DISPLAY #include #include #endif // WiFi credentials #ifdef ENABLE_WIFI const char* WIFI_SSID = SECRET_WIFI_SSID; const char* WIFI_PASSWORD = SECRET_WIFI_PASSWORD; #endif // ntfy.sh topic 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; // OLED display settings #define SCREEN_WIDTH 128 #define SCREEN_HEIGHT 32 #define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin) #define SCREEN_ADDRESS 0x3C #ifdef ENABLE_DISPLAY Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); #endif // Light sensor #ifdef ENABLE_SENSORS BH1750 lightMeter; #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; // Time tracking for sensor states unsigned long lastWindowStartTime = 0; // OLED display data #ifdef ENABLE_DISPLAY String displayData[5]; int currentDisplayLine = 0; unsigned long lastDisplayScrollTime = 0; const unsigned long DISPLAY_SCROLL_INTERVAL = 2000; // 1000 = 1 second const int displayTextLines = 4; int displayDataLines = displayTextLines; #endif // Network Task & Queue struct NotificationMessage { char message[64]; char title[32]; char priority[10]; }; 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; 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; } } #endif #ifdef ENABLE_WIFI void networkTask(void *parameter) { NotificationMessage msg; unsigned long lastWifiCheckTime = 0; const unsigned long WIFI_CHECK_INTERVAL = 30000; while (true) { unsigned long currentTime = millis(); if (currentTime - lastWifiCheckTime >= WIFI_CHECK_INTERVAL) { lastWifiCheckTime = currentTime; if (WiFi.status() != WL_CONNECTED) { Serial.println("WiFi disconnected. Attempting to reconnect..."); WiFi.reconnect(); } } if (xQueueReceive(notificationQueue, &msg, 1000 / portTICK_PERIOD_MS) == pdTRUE) { isNetworkActive = true; bool sent = false; while (!sent) { if (WiFi.status() == WL_CONNECTED) { HTTPClient http; String url = "http://ntfy.sh/" + String(NTFY_TOPIC); http.begin(url); http.addHeader("Content-Type", "text/plain"); http.addHeader("Title", msg.title); http.addHeader("Priority", msg.priority); int httpResponseCode = http.POST(msg.message); http.end(); if (httpResponseCode > 0) { Serial.print("HTTP Response code: "); Serial.println(httpResponseCode); sent = true; } else { Serial.print("Error on sending POST: "); Serial.println(httpResponseCode); vTaskDelay(2000 / portTICK_PERIOD_MS); } } else { unsigned long now = millis(); if (now - lastWifiCheckTime >= WIFI_CHECK_INTERVAL) { lastWifiCheckTime = now; Serial.println("WiFi disconnected. Attempting to reconnect..."); WiFi.reconnect(); } vTaskDelay(1000 / portTICK_PERIOD_MS); } } isNetworkActive = false; } } } #endif void setup() { delay(500); Serial.begin(9600); delay(100); Serial.println("Setup started."); #if defined(ENABLE_SENSORS) || defined(ENABLE_DISPLAY) Wire.begin(); #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!")); while (1) { } } #endif #ifdef ENABLE_DISPLAY // Initialize OLED display if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) { Serial.println(F("SSD1306 allocation failed")); for (;;); } display.clearDisplay(); display.setTextSize(1); display.setTextColor(WHITE); display.setCursor(0, 0); display.println(F("Initializing...")); display.display(); #endif #ifdef ENABLE_WIFI // Connect to WiFi Serial.print("Connecting to "); Serial.println(WIFI_SSID); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(""); Serial.println("WiFi connected"); Serial.println("IP address: "); Serial.println(WiFi.localIP()); #ifdef ENABLE_NOTIFICATIONS notificationQueue = xQueueCreate(10, sizeof(NotificationMessage)); xTaskCreatePinnedToCore(networkTask, "NetworkTask", 8192, NULL, 1, NULL, 0); #endif #endif #if defined(ENABLE_DISPLAY) && defined(ENABLE_WIFI) display.clearDisplay(); display.setCursor(0,0); display.println("WiFi Connected"); display.display(); delay(1000); #endif Serial.println("Setup finished."); } void updateDisplay(); void loop() { unsigned long currentTime = millis(); // Sensor readings #ifdef ENABLE_SENSORS float currentLightLevel = lightMeter.readLightLevel(); if (currentLightLevel > maxLightInWindow) { maxLightInWindow = currentLightLevel; } #else float currentLightLevel = 0.0; #endif bool previousVibrationState = isVibrationActive; bool previousLightState = isLightActive; // Window Logic if (currentTime - lastWindowStartTime >= WINDOW_DURATION) { lastWindowStartTime = currentTime; // 1. Process Vibration #ifdef ENABLE_SENSORS noInterrupts(); unsigned long now = micros(); unsigned long duration = now - lastChangeTime; lastChangeTime = now; if (digitalRead(VIBRATION_PIN) == LOW) { lowTimeAccumulator += duration; } else { highTimeAccumulator += duration; } vibrationTotalLow = lowTimeAccumulator; lowTimeAccumulator = 0; highTimeAccumulator = 0; interrupts(); bool vibWindowOn = (vibrationTotalLow > VIBRATION_ACTIVE_THRESHOLD); if (vibWindowOn) { vibConsecutiveOn++; vibConsecutiveOff = 0; } else { vibConsecutiveOff++; vibConsecutiveOn = 0; } if (vibConsecutiveOn >= REQUIRED_CONSECUTIVE_WINDOWS && currentTime > WARMUP_DURATION) { isVibrationActive = true; } else if (vibConsecutiveOff >= REQUIRED_CONSECUTIVE_WINDOWS) { isVibrationActive = false; } #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) { String message; String title; String priority; if (isVibrationActive) { message = String(DEVICE1_NAME) + " started."; title = String(DEVICE1_NAME) + " START"; priority = "default"; } else { message = String(DEVICE1_NAME) + " finished."; title = String(DEVICE1_NAME) + " END"; priority = "high"; } #ifdef ENABLE_NOTIFICATIONS #ifdef ENABLE_WIFI NotificationMessage msg; strncpy(msg.message, message.c_str(), sizeof(msg.message) - 1); msg.message[sizeof(msg.message) - 1] = 0; strncpy(msg.title, title.c_str(), sizeof(msg.title) - 1); msg.title[sizeof(msg.title) - 1] = 0; strncpy(msg.priority, priority.c_str(), sizeof(msg.priority) - 1); msg.priority[sizeof(msg.priority) - 1] = 0; xQueueSend(notificationQueue, &msg, 0); #else Serial.println("WiFi Disabled. Notification skipped: " + message); #endif #else Serial.println("Notification (Disabled): " + title + " - " + message); #endif } // Device 2 (Light) Notifications if (isLightActive != previousLightState) { String message; String title; String priority; if (isLightActive) { message = String(DEVICE2_NAME) + " active."; title = String(DEVICE2_NAME) + " ACTIVE"; priority = "default"; } else { message = String(DEVICE2_NAME) + " inactive."; title = String(DEVICE2_NAME) + " INACTIVE"; priority = "high"; } #ifdef ENABLE_NOTIFICATIONS #ifdef ENABLE_WIFI NotificationMessage msg; strncpy(msg.message, message.c_str(), sizeof(msg.message) - 1); msg.message[sizeof(msg.message) - 1] = 0; strncpy(msg.title, title.c_str(), sizeof(msg.title) - 1); msg.title[sizeof(msg.title) - 1] = 0; strncpy(msg.priority, priority.c_str(), sizeof(msg.priority) - 1); msg.priority[sizeof(msg.priority) - 1] = 0; xQueueSend(notificationQueue, &msg, 0); #else Serial.println("WiFi Disabled. Notification skipped: " + message); #endif #else Serial.println("Notification (Disabled): " + title + " - " + message); #endif } #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); displayDataLines = 4; // Update display updateDisplay(); #endif } void updateDisplay() { #ifdef ENABLE_DISPLAY unsigned long currentTime = millis(); // Scroll logic if ((displayDataLines > displayTextLines) && (currentTime - lastDisplayScrollTime > DISPLAY_SCROLL_INTERVAL)) { lastDisplayScrollTime = currentTime; currentDisplayLine = (currentDisplayLine + 1) % displayDataLines; } display.clearDisplay(); display.setTextSize(1); display.setTextColor(WHITE); // Draw active status indicator int rectWidth = 10; int rectHeight = (SCREEN_HEIGHT / 2) - 2; int rectX = 2; int rectY1 = 1; int rectY2 = (SCREEN_HEIGHT / 2) + 1; int textX = rectX + rectWidth + 4; for (int i = 0; i < displayDataLines; i++) { int lineIndex = (currentDisplayLine + i) % displayDataLines; display.setCursor(textX, i * 8); display.println(displayData[lineIndex]); } // Draw Device 1 Indicator (Top) display.drawRect(rectX, rectY1, rectWidth, rectHeight, WHITE); if (isVibrationActive) { int innerWidth = rectWidth - 4; int step = (millis() / 150) % (innerWidth + 1); display.fillRect(rectX + 2, rectY1 + 2, step, rectHeight - 4, WHITE); } // Draw Device 2 Indicator (Bottom) display.drawRect(rectX, rectY2, rectWidth, rectHeight, WHITE); if (isLightActive) { int innerWidth = rectWidth - 4; int step = (millis() / 150) % (innerWidth + 1); display.fillRect(rectX + 2, rectY2 + 2, step, rectHeight - 4, WHITE); } // Network Status Indicator #ifdef ENABLE_WIFI int r = 8; int cx = SCREEN_WIDTH - r - 2; int cy = SCREEN_HEIGHT / 2; if (WiFi.status() != WL_CONNECTED) { display.drawLine(cx - r, cy - r, cx + r, cy + r, WHITE); display.drawLine(cx - r, cy + r, cx + r, cy - r, WHITE); } else if (isNetworkActive) { display.fillCircle(cx, cy, r, WHITE); } else { display.drawCircle(cx, cy, r, WHITE); } #ifdef ENABLE_NOTIFICATIONS if (notificationQueue != NULL) { int waiting = (int)uxQueueMessagesWaiting(notificationQueue); int startY = cy + r + 2; int h = SCREEN_HEIGHT - startY; for (int i = 0; i < waiting; i++) { int x = (cx - r) + (i * 2); if (x < SCREEN_WIDTH && h > 0) display.drawFastVLine(x, startY, h, WHITE); } } #endif #endif // Warmup Indicator if (currentTime < WARMUP_DURATION) { display.drawRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, WHITE); } display.display(); #endif }