Files
Driveway_Light/Driveway Light/src/main.cpp

758 lines
29 KiB
C++
Executable File

#include <Arduino.h>
#include <ArduinoJson.h>
#include <esp_sleep.h> // For deep sleep functions
#include <TimeLib.h> // For time functions
#include <Wire.h> // For I2C
#include <SparkFun_VEML7700_Arduino_Library.h> // VEML7700
#include "WiFi.h" // Needed for disabling
#include "BluetoothSerial.h" // Needed for disabling
#include <LoRa.h> // LoRa library
#include <SPI.h> // SPI for LoRa
#include <vector> // For receivedMessageIds
#include <algorithm> // 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<String> 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<time_t>()) {
currentTime = doc["currentTime"].as<time_t>();
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<time_t>()) {
sunriseTime = doc["sunriseTime"].as<time_t>();
settingsUpdatedThisMessage = true;
timeSettingsReceived = true;
Serial.println("Updated sunriseTime");
}
if (doc["sunsetTime"].is<time_t>()) {
sunsetTime = doc["sunsetTime"].as<time_t>();
settingsUpdatedThisMessage = true;
timeSettingsReceived = true;
Serial.println("Updated sunsetTime");
}
if (doc["minimumPower"].is<float>()) {
minimumPower = doc["minimumPower"].as<float>();
settingsUpdatedThisMessage = true;
Serial.println("Updated minimumPower");
}
if (doc["maximumLightPercent"].is<int>()) {
int percent = doc["maximumLightPercent"].as<int>();
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>()) {
unsigned long durationSeconds = doc["lightOnDuration"].as<unsigned long>();
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>()) {
unsigned long sleepSeconds = doc["sleepTime"].as<unsigned long>();
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));
}