#include #include #include // For deep sleep functions #include // For time functions #include // For I2C #include // VEML7700 #include "WiFi.h" // Needed for disabling #include "BluetoothSerial.h" // Needed for disabling #include // LoRa library #include // SPI for LoRa #include // For receivedMessageIds #include // For std::find_if // --- Pin Definitions --- // LoRa Configuration #define LORA_CS 5 #define LORA_RST 14 #define LORA_IRQ 26 // DIO0 - RTC_GPIO17 // Relay Configuration const int relayPin = 12; // PIR sensor - RTC_GPIO14 const int PIR_PIN = 13; // Voltage Sensor Configuration const int voltageSensorPin = 34; const float voltageDividerRatio = 5.0; const float adcResolution = 3.3 / 4095.0; // Needs Calibration const float calibrationFactor = 1.071; // Adjust as needed // --- Sensor Objects --- VEML7700 veml = VEML7700(); // --- Device Configuration --- const char* myDeviceName = "driveway_light"; // *** RENAMED VARIABLE *** const char* allLightsTarget = "all_lights"; const char* allDevicesTarget = "all_devices"; // --- Configuration Constants --- const unsigned long AWAKE_INACTIVITY_TIMEOUT = 30000; const float MIN_LUX_THRESHOLD = 5.0; const float MAX_LUX_THRESHOLD = 500.0; // --- RTC Memory Variables (Persistent State) --- //RTC_DATA_ATTR bool settingsRequestMade = false; RTC_DATA_ATTR time_t currentTime = 0; RTC_DATA_ATTR time_t sunriseTime = 0; RTC_DATA_ATTR time_t sunsetTime = 0; RTC_DATA_ATTR float minimumPower = 11.0; RTC_DATA_ATTR float maximumLight = 50.0; RTC_DATA_ATTR unsigned long lightOnDuration = 30000; RTC_DATA_ATTR String lightMode = "monitor"; RTC_DATA_ATTR unsigned long sleepTime = 15 * 60 * 1000; RTC_DATA_ATTR String lightStatus = "off"; RTC_DATA_ATTR unsigned long motionDetectedTime = 0; RTC_DATA_ATTR std::vector receivedMessageIds; // --- Volatile Variables (Reset on Wake) --- bool isDaylight = true; float lightReading = -1.0; float powerReading = -1.0; bool isSleeping = false; unsigned long lastMessageReceivedTime = 0; // --- Function Declarations --- void initializeLoRa(); void handleReceivedLoRaMessage(String receivedMessage); void handlePirWakeup(); void reportSensorsAndSleep(); void sleepCycle(unsigned long durationMs); void sendLoRaMessage(String jsonPayload); void sendAcknowledgment(String message_id); void sendStatusUpdate(String event_type); void turnLightOn(bool triggeredByPir = false); void turnLightOff(); void updateDaylightStatus(); float readVoltage(); float readLightLevel(); String createMessageId(); void requestSettings(); // --- End Function Declarations --- // --- Setup Function --- // --- Setup Function --- // --- Setup Function --- void setup() { Serial.begin(115200); while (!Serial); Serial.println("\n\n--- Driveway Light Controller (Pin Re-init Fix) ---"); // Disable GPIO hold FIRST gpio_hold_dis(GPIO_NUM_5); gpio_deep_sleep_hold_dis(); // --- Initialize Core Peripherals and Pins on EVERY Wake-up --- Wire.begin(); // Initialize I2C (Using default pins 21, 22) initializeLoRa(); // Initialize LoRa module (includes SPI.begin) pinMode(relayPin, OUTPUT); // *** MOVED HERE: Re-init Relay Pin *** pinMode(PIR_PIN, INPUT); // *** MOVED HERE: Re-init PIR Pin *** // --- End Core Init --- esp_sleep_wakeup_cause_t wakeup_reason = esp_sleep_get_wakeup_cause(); // *** FIX: Synchronize TimeLib from RTC Memory *** if (currentTime != 0) { // If currentTime (from RTC memory) has a valid value (not the initial 0) setTime(currentTime); // Set the TimeLib internal clock Serial.print("TimeLib synchronized from RTC currentTime: "); Serial.println(currentTime); } else { // currentTime is still 0, meaning we haven't received a time update via LoRa yet. Serial.println("RTC currentTime is 0. Waiting for LoRa time update."); } // *** END FIX *** // Now update daylight status using the potentially synchronized time updateDaylightStatus(); // Restore relay state based on RTC variable AFTER pinMode is set digitalWrite(relayPin, (lightStatus == "on") ? HIGH : LOW); Serial.print("Restored relay state based on lightStatus ('"); Serial.print(lightStatus); Serial.println("')"); bool enterLoopAwake = false; // --- SINGLE Switch Statement --- switch(wakeup_reason) { case ESP_SLEEP_WAKEUP_EXT1: { uint64_t wakeup_pin_mask = esp_sleep_get_ext1_wakeup_status(); if (wakeup_pin_mask & (1ULL << LORA_IRQ)) { Serial.println("Wakeup caused by EXT1 (LoRa IRQ)"); enterLoopAwake = true; } else if (wakeup_pin_mask & (1ULL << PIR_PIN)) { Serial.println("Wakeup caused by EXT1 (PIR)"); handlePirWakeup(); // Reads PIR state using digitalRead AFTER pinMode // Stay awake if PIR turned light on in monitor mode if (lightStatus == "on" && lightMode == "monitor") { enterLoopAwake = true; } // If handlePirWakeup didn't turn light on, setup will decide sleep later } else { Serial.println("Wakeup caused by EXT1 (Unknown Pin)"); // Go to sleep if unknown EXT1 source and light isn't meant to be on if (lightStatus != "on") { sleepCycle(sleepTime); } else { // If light is supposed to be on (e.g., mode=on), stay awake enterLoopAwake = true; } } break; // End of EXT1 case } // End of EXT1 block case ESP_SLEEP_WAKEUP_TIMER: { Serial.println("Wakeup caused by Timer"); reportSensorsAndSleep(); // Reads sensors AFTER pinMode/Wire.begin // reportSensorsAndSleep calls sleepCycle, so code execution stops here for this path break; // End of TIMER case (technically unreachable due to sleep) } // End of TIMER block default: { // Initial Boot or other reasons Serial.printf("Initial Boot or other wake-up reason: %d\n", wakeup_reason); // I2C, LoRa, pinMode already done above if (veml.begin()) { Serial.println("VEML7700 connected"); } else { Serial.println("VEML7700 not found"); } WiFi.disconnect(true); WiFi.mode(WIFI_OFF); btStop(); Serial.println("Initial boot setup complete."); // Check if essential time is missing if (currentTime == 0 || sunriseTime == 0 || sunsetTime == 0) { Serial.println("Essential time data missing on initial boot. Requesting settings."); requestSettings(); // Call the function to send the request Serial.println("Proceeding to loop to listen for settings response."); enterLoopAwake = true; // Ensure we enter the loop } else { // Time data IS present from a previous run Serial.println("Time data found in RTC. Reporting sensors and sleeping."); reportSensorsAndSleep(); // Proceed with normal timer-like behavior // reportSensorsAndSleep calls sleepCycle, so code execution stops here } break; // End of default case (may be unreachable if sleeping) } // End of default block } // *** Closing brace for the SINGLE switch statement *** // --- Enter Loop Awake State --- // This logic now runs *after* the switch statement is complete if (enterLoopAwake || (lightStatus == "on")) { Serial.println("Proceeding to loop in AWAKE state."); isSleeping = false; lastMessageReceivedTime = millis(); // Reset inactivity timer LoRa.receive(); // Ensure LoRa is listening } else if (!isSleeping) { // Add check to prevent redundant sleep call Serial.println("Setup complete, conditions not met to stay awake. Entering sleep cycle."); LoRa.receive(); delay(10); sleepCycle(sleepTime); } // If sleepCycle was called inside the switch, execution might not reach here. } // End of setup() void requestSettings() { Serial.println("Requesting settings from base station..."); JsonDocument doc; doc["device"] = myDeviceName; doc["action"] = "request_settings"; // Use a specific action keyword doc["message_id"] = createMessageId(); // Include a message ID String jsonData; serializeJson(doc, jsonData); sendLoRaMessage(jsonData); Serial.println("Settings request sent."); // No need to manage flags here. // The setup() function logic determines if we stay awake. } // --- Main Loop --- // --- Main Loop --- void loop() { bool processedLoRa = false; // 1. Check for LoRa messages FIRST int packetSize = LoRa.parsePacket(); if (packetSize) { String msg = ""; while (LoRa.available()) { msg += (char)LoRa.read(); } handleReceivedLoRaMessage(msg); lastMessageReceivedTime = millis(); processedLoRa = true; if (!isSleeping) { LoRa.receive(); } else { return; } } // 2. Check PIR Light Timeout (Only in monitor mode) if (lightStatus == "on" && lightMode == "monitor" && motionDetectedTime > 0) { if (millis() - motionDetectedTime > lightOnDuration) { Serial.println("PIR light duration expired."); turnLightOff(); return; } } // 3. Check Low Power Condition (Only when lightMode is 'on') if (lightStatus == "on" && lightMode == "on") { static unsigned long lastPowerCheck = 0; if (millis() - lastPowerCheck > 60000) { // Check every 60 seconds lastPowerCheck = millis(); powerReading = readVoltage(); Serial.print("Periodic power check while ON: "); Serial.println(powerReading); if (powerReading >= 0 && powerReading < minimumPower) { Serial.println("LOW POWER DETECTED while lightMode=on. Turning off light."); turnLightOff(); return; } } } // *** 4. ADDED: Check PIR Sensor while lightMode is 'on' *** if (lightMode == "on") { static bool lastPirState = LOW; // Remember last state to detect changes static unsigned long lastPirReportTime = 0; int currentPirState = digitalRead(PIR_PIN); if (currentPirState == HIGH && lastPirState == LOW) { // Motion just started while lightMode is 'on' Serial.println("PIR Motion Detected while lightMode=on."); // Send a status update immediately sendStatusUpdate("pir_motion_while_on"); lastPirReportTime = millis(); // Record time of this report } // Optional: Send periodic updates if motion continues? // else if (currentPirState == HIGH && lastPirState == HIGH) { // // Motion continues // if (millis() - lastPirReportTime > 60000) { // e.g., report every minute if motion persists // sendStatusUpdate("pir_motion_continuing_while_on"); // lastPirReportTime = millis(); // } // } lastPirState = currentPirState; // Update last state } // *** END ADDED PIR CHECK *** // 5. Check for Inactivity Timeout (Only relevant if NOT in 'on' mode) // If mode is 'on', we stay awake indefinitely unless turned off by LoRa/Low Power if (!processedLoRa && lightMode != "on" && lightStatus != "on") { if (millis() - lastMessageReceivedTime > AWAKE_INACTIVITY_TIMEOUT) { Serial.println("Awake inactivity timeout reached (monitor mode, light off). Going to sleep."); sleepCycle(sleepTime); return; } } delay(10); // Prevent tight loop } // --- Core Logic Functions --- void handlePirWakeup() { Serial.println("Handling PIR Wakeup..."); lightReading = readLightLevel(); powerReading = readVoltage(); updateDaylightStatus(); Serial.print("PIR Trig: Daylight="); Serial.print(isDaylight); Serial.print("currentTime="); Serial.print(currentTime); Serial.print("sunriseTime="); Serial.print(sunriseTime); Serial.print("sunsetTime="); Serial.print(sunsetTime); Serial.print(" | Light="); Serial.print(lightReading); Serial.print(" is less than maximumLight="); Serial.print(maximumLight); Serial.print(" | Power="); Serial.print(powerReading); Serial.print(" | Mode="); Serial.println(lightMode); bool conditionsMet = (!isDaylight || (lightReading >= 0 && lightReading < maximumLight)) && (powerReading >= 0 && powerReading > minimumPower) && lightMode == "monitor"; if (conditionsMet) { Serial.println("Conditions met for PIR light ON."); turnLightOn(true); sendStatusUpdate("pir_light_on"); LoRa.receive(); return; // Return to setup -> loop } else { Serial.println("Conditions NOT met for PIR light ON (or mode=on)."); LoRa.receive(); // Ensure LoRa ready if setup enters loop return; // Return to setup -> sleep decision } } void reportSensorsAndSleep() { Serial.println("Timer wake-up: Reporting sensors..."); lightReading = readLightLevel(); powerReading = readVoltage(); updateDaylightStatus(); JsonDocument doc; doc["device"] = myDeviceName; // *** Use consistent "device" key *** if (powerReading >= 0) doc["voltage"] = powerReading; if (lightReading >= 0) doc["lux"] = lightReading; doc["timestamp"] = now(); doc["lightStatus"] = lightStatus; doc["lightMode"] = lightMode; doc["isDaylight"] = isDaylight; doc["message_id"] = createMessageId(); String jsonData; serializeJson(doc, jsonData); sendLoRaMessage(jsonData); sleepCycle(sleepTime); } void sleepCycle(unsigned long durationMs) { Serial.print("Entering sleep cycle for "); Serial.print(durationMs); Serial.println(" ms"); isSleeping = true; LoRa.receive(); // Put LoRa in receive mode before sleep delay(10); esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_ALL); // Disable all previous sources // --- Configure Wake-up Sources --- // 1. Timer Wake-up (Always Enabled) esp_sleep_enable_timer_wakeup((uint64_t)durationMs * 1000); Serial.print("Timer wake-up enabled for "); Serial.print(durationMs); Serial.println(" ms"); // 2. EXT1 Wake-up (ALWAYS Enable LoRa IRQ and PIR) uint64_t ext1_wakeup_mask = (1ULL << LORA_IRQ) | (1ULL << PIR_PIN); esp_sleep_enable_ext1_wakeup(ext1_wakeup_mask, ESP_EXT1_WAKEUP_ANY_HIGH); Serial.println("EXT1 wake-up enabled for LoRa IRQ (26) and PIR (13) - ANY_HIGH"); // --- End Configure Wake-up Sources --- // --- Hold CS pin HIGH during sleep --- gpio_hold_en(GPIO_NUM_5); gpio_deep_sleep_hold_en(); Serial.println("Entering deep sleep NOW..."); Serial.flush(); esp_deep_sleep_start(); Serial.println("!!! Woke up from Deep Sleep Unexpectedly !!!"); // Should not see this } // --- LoRa Communication --- void initializeLoRa() { Serial.print(F("[LoRa] Initializing ... ")); SPI.begin(); LoRa.setPins(LORA_CS, LORA_RST, LORA_IRQ); if (!LoRa.begin(434E6)) { Serial.println("Starting LoRa failed!"); delay(1000); ESP.restart(); } LoRa.setSpreadingFactor(9); LoRa.setSignalBandwidth(125E3); LoRa.setCodingRate4(5); LoRa.setPreambleLength(8); LoRa.setSyncWord(0x34); Serial.println(F("success!")); } void sendLoRaMessage(String jsonPayload) { Serial.print("Sending LoRa Message: "); Serial.println(jsonPayload); LoRa.beginPacket(); LoRa.print(jsonPayload); int success = LoRa.endPacket(); if (!success) { Serial.println("LoRa transmit failed!"); } } void sendAcknowledgment(String message_id) { JsonDocument ackDoc; ackDoc["message_id"] = message_id; ackDoc["message"] = "acknowledged"; ackDoc["device"] = myDeviceName; // *** Use consistent "device" key *** String ackMessage; serializeJson(ackDoc, ackMessage); Serial.print("Sending ACK for message ID: "); Serial.println(message_id); sendLoRaMessage(ackMessage); } void sendStatusUpdate(String event_type) { JsonDocument statusDoc; statusDoc["device"] = myDeviceName; // *** Use consistent "device" key *** statusDoc["event"] = event_type; statusDoc["lightStatus"] = lightStatus; statusDoc["lightMode"] = lightMode; statusDoc["message_id"] = createMessageId(); if (powerReading >= 0) statusDoc["voltage"] = powerReading; if (lightReading >= 0) statusDoc["lux"] = lightReading; String statusMessage; serializeJson(statusDoc, statusMessage); sendLoRaMessage(statusMessage); } // --- UPDATED Message Handler --- // Handles the content of a received LoRa message // Handles the content of a received LoRa message // Handles the content of a received LoRa message void handleReceivedLoRaMessage(String receivedMessage) { Serial.print("handleReceivedLoRaMessage - Raw: "); Serial.println(receivedMessage); // Parse the incoming JSON FIRST JsonDocument doc; // Use JsonDocument for parsing DeserializationError error = deserializeJson(doc, receivedMessage); if (error) { Serial.print("Failed to parse JSON: "); Serial.println(error.c_str()); return; // Exit if parsing fails } Serial.println("Parsed Received JSON"); // --- Always attempt to Update Settings --- bool settingsUpdatedThisMessage = false; // Track if any setting was updated in THIS message bool timeSettingsReceived = false; // Track if essential time settings came in this message if (doc["currentTime"].is()) { currentTime = doc["currentTime"].as(); setTime(currentTime); // Set TimeLib internal clock updateDaylightStatus(); // Update daylight based on new time settingsUpdatedThisMessage = true; timeSettingsReceived = true; Serial.println("Updated currentTime"); } if (doc["sunriseTime"].is()) { sunriseTime = doc["sunriseTime"].as(); settingsUpdatedThisMessage = true; timeSettingsReceived = true; Serial.println("Updated sunriseTime"); } if (doc["sunsetTime"].is()) { sunsetTime = doc["sunsetTime"].as(); settingsUpdatedThisMessage = true; timeSettingsReceived = true; Serial.println("Updated sunsetTime"); } if (doc["minimumPower"].is()) { minimumPower = doc["minimumPower"].as(); settingsUpdatedThisMessage = true; Serial.println("Updated minimumPower"); } if (doc["maximumLightPercent"].is()) { int percent = doc["maximumLightPercent"].as(); percent = constrain(percent, 1, 100); maximumLight = map(percent, 1, 100, (long)MIN_LUX_THRESHOLD, (long)MAX_LUX_THRESHOLD); settingsUpdatedThisMessage = true; Serial.print("Updated maximumLight from Percent("); Serial.print(percent); Serial.print("%) to Lux: "); Serial.println(maximumLight); } if (doc["lightOnDuration"].is()) { unsigned long durationSeconds = doc["lightOnDuration"].as(); lightOnDuration = durationSeconds * 1000UL; // Convert seconds to milliseconds settingsUpdatedThisMessage = true; Serial.print("Updated lightOnDuration from Seconds("); Serial.print(durationSeconds); Serial.print("s) to ms: "); Serial.println(lightOnDuration); } if (doc["sleepTime"].is()) { unsigned long sleepSeconds = doc["sleepTime"].as(); sleepTime = sleepSeconds * 1000UL; // Convert seconds to milliseconds settingsUpdatedThisMessage = true; Serial.print("Updated sleepTime from Seconds("); Serial.print(sleepSeconds); Serial.print("s) to ms: "); Serial.println(sleepTime); } // --- End Update Settings --- // Extract necessary fields FROM THE PARSED 'doc' String message_id = doc["message_id"] | "no_id"; String receivedTargetDevice = doc["targetDevice"] | "no_target"; String action = doc["action"] | "no_action"; bool commandProcessed = false; bool goingToSleep = false; bool ackSent = false; // --- Target Check --- bool targeted = (receivedTargetDevice.equals(myDeviceName) || receivedTargetDevice.equals(allLightsTarget) || receivedTargetDevice.equals(allDevicesTarget)); Serial.print("Message Target Check: Received='"); Serial.print(receivedTargetDevice); Serial.print("', MyName='"); Serial.print(myDeviceName); Serial.print("', Targeted="); Serial.println(targeted); if (!targeted) { Serial.println("Message not targeted for this device."); // Decide if we should sleep or keep listening if not targeted if (lightMode != "on" && lightStatus != "on") { // If light is off and mode isn't forced 'on', sleep sleepCycle(sleepTime); return; // Exit function after starting sleep } else { // Otherwise (light is on or mode is 'on'), stay awake and listen LoRa.receive(); return; // Exit function, stay awake } } Serial.println("Message targeted for this device."); // --- Action Processing --- // Send ACK *early* if possible and command is valid & targeted // Acknowledge specific actions or any message that updated settings if (message_id != "no_id") { if (action == "turn_on" || action == "turn_off" || action == "apply_settings" || action == "request_settings" || settingsUpdatedThisMessage) { sendAcknowledgment(message_id); ackSent = true; delay(50); // Small delay after ACK before potential status update } } if (action == "turn_on") { Serial.println("Received action: turn_on"); powerReading = readVoltage(); // Check power before turning on if (powerReading >= 0 && powerReading < minimumPower) { Serial.print("Cannot turn on - LOW POWER detected: "); Serial.println(powerReading); sendStatusUpdate("turn_on_failed_low_power"); // Send specific status commandProcessed = true; goingToSleep = (lightMode != "on"); // Go to sleep unless mode is forced 'on' } else { if (lightStatus != "on") { turnLightOn(); // Turn light on (sets mode to 'on' implicitly) } else { // If already on, ensure mode is 'on' lightMode = "on"; Serial.println("Light already on, ensuring mode is 'on'."); } commandProcessed = true; // Stay awake because light is now on or mode is 'on' } } else if (action == "turn_off") { Serial.println("Received action: turn_off"); turnLightOff(); // This function handles status update and sleep commandProcessed = true; goingToSleep = true; // turnLightOff initiates sleep // ACK was sent above return; // Exit function as turnLightOff handles sleep } else if (action == "apply_settings") { Serial.println("Received action: apply_settings"); // Logic using settingsRequestMade was removed here. if (settingsUpdatedThisMessage) { Serial.println("Settings values were updated from this message."); } else { Serial.println("Action 'apply_settings' received, but no new settings values were applied."); } commandProcessed = true; // ACK was sent above // Decide sleep based on current state after applying settings goingToSleep = (lightMode != "on" && lightStatus != "on"); } else if (action == "request_settings") { Serial.println("Received action: request_settings (Ignoring, I am receiver)"); // This device doesn't act on receiving a request, it sends requests. commandProcessed = true; // ACK was sent above // Decide sleep based on current state goingToSleep = (lightMode != "on" && lightStatus != "on"); } else if (action != "no_action") { Serial.println("Unknown action received: " + action); if (message_id != "no_id" && !ackSent) { // Send ACK for unknown action if not already sent sendAcknowledgment(message_id); ackSent = true; } commandProcessed = true; // Decide sleep based on current state goingToSleep = (lightMode != "on" && lightStatus != "on"); } else { // No specific 'action' key found in message. Serial.println("No specific 'action' key found in message."); if (settingsUpdatedThisMessage) { Serial.println("Settings values were updated from message with no specific action."); // ACK should have been sent above if message_id exists and settings were updated commandProcessed = true; // Mark as processed if settings were updated } // Decide sleep based on current state goingToSleep = (lightMode != "on" && lightStatus != "on"); } // --- End Action Processing --- // --- Send Status Update --- // Send status update only if a command was processed, we aren't going to sleep immediately, // and it wasn't the turn_off command (which sends its own status before sleeping). if (commandProcessed && !goingToSleep && action != "turn_off") { // ACK should have already been sent if applicable delay(1500); // Keep delay for status update separation from ACK sendStatusUpdate("lora_command_processed"); } else if (message_id == "no_id" && commandProcessed) { Serial.println("Command processed but no message_id received, cannot ACK."); } // --- Final Sleep/Wake Decision --- if (goingToSleep) { Serial.println("Command processed, conditions indicate sleep is appropriate."); sleepCycle(sleepTime); // Execution stops here } else { // Stay awake if light is on, mode is 'on', or loop inactivity timer hasn't expired Serial.println("Command processed, staying awake in loop."); lastMessageReceivedTime = millis(); // Reset inactivity timer as we processed a message LoRa.receive(); // Ensure LoRa is ready for next message } } void updateDaylightStatus() { if (sunriseTime == 0 || sunsetTime == 0 || currentTime == 0) { Serial.println("updateDaylightStatus: Invalid time(s), defaulting to daylight=true"); isDaylight = true; // Default to daylight if times are invalid to prevent unwanted ON state return; } // Handle the normal case where sunrise and sunset are on the same day if (sunriseTime < sunsetTime) { isDaylight = (currentTime > sunriseTime && currentTime < sunsetTime); } // Handle the case where times span midnight (sunrise is 'tomorrow' relative to sunset 'today') else if (sunriseTime > sunsetTime) { isDaylight = (currentTime > sunriseTime || currentTime < sunsetTime); } // If sunriseTime == sunsetTime (unlikely, but handle it) else { isDaylight = false; // Or true? Define behavior for this edge case. False = dark. } Serial.print("updateDaylightStatus: currentTime="); Serial.print(currentTime); Serial.print(", sunriseTime="); Serial.print(sunriseTime); Serial.print(", sunsetTime="); Serial.print(sunsetTime); Serial.print(" -> isDaylight="); Serial.println(isDaylight); } float readVoltage() { int adcValue = analogRead(voltageSensorPin); const int numReadings = 5; static float readings[numReadings]; static int readIndex = 0; static float total = 0; static bool filled = false; total = total - readings[readIndex]; float rawVoltage = (float)adcValue * (3.3 / 4095.0) * voltageDividerRatio; float calibratedVoltage = rawVoltage * calibrationFactor; // Apply calibration readings[readIndex] = calibratedVoltage; total = total + readings[readIndex]; readIndex++; if(readIndex >= numReadings) { readIndex = 0; filled = true; } powerReading = filled ? total / (float)numReadings : total / (float)(readIndex == 0 ? 1 : readIndex); return powerReading; } float readLightLevel() { Wire.begin(); // Re-init I2C delay(20); if (veml.begin()) { lightReading = veml.getAmbientLight(); } else { lightReading = -1.0; Serial.println("VEML7700 read failed (veml.begin() failed)."); } return lightReading; } void turnLightOn(bool triggeredByPir /*= false*/) { if (lightStatus == "off") { digitalWrite(relayPin, HIGH); lightStatus = "on"; Serial.println("Light turned ON"); if (triggeredByPir) { motionDetectedTime = millis(); Serial.print("PIR timer started for "); Serial.print(lightOnDuration); Serial.println(" ms"); } else { motionDetectedTime = 0; lightMode = "on"; Serial.println("Light mode set to: on"); } } else { if (!triggeredByPir && lightMode != "on") { lightMode = "on"; Serial.println("Light already on, mode set to: on"); } } } void turnLightOff() { if (lightStatus == "on") { digitalWrite(relayPin, LOW); lightStatus = "off"; motionDetectedTime = 0; Serial.println("Light turned OFF"); lightMode = "monitor"; Serial.println("Light mode set to: monitor"); sendStatusUpdate("light_turned_off"); sleepCycle(sleepTime); } else { Serial.println("Light already OFF."); if (lightMode != "monitor") { lightMode = "monitor"; Serial.println("Light mode set to: monitor"); sendStatusUpdate("mode_set_monitor"); } sleepCycle(sleepTime); } } String createMessageId() { return String(random(10000, 99999)); }