Initial Platformio project import

This commit is contained in:
Sam
2025-05-06 16:12:25 +10:00
commit 8a326f76e0
7 changed files with 886 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
.pio
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch

10
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,10 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [
"platformio.platformio-ide"
],
"unwantedRecommendations": [
"ms-vscode.cpptools-extension-pack"
]
}

37
include/README Normal file
View File

@ -0,0 +1,37 @@
This directory is intended for project header files.
A header file is a file containing C declarations and macro definitions
to be shared between several project source files. You request the use of a
header file in your project source file (C, C++, etc) located in `src` folder
by including it, with the C preprocessing directive `#include'.
```src/main.c
#include "header.h"
int main (void)
{
...
}
```
Including a header file produces the same results as copying the header file
into each source file that needs it. Such copying would be time-consuming
and error-prone. With a header file, the related declarations appear
in only one place. If they need to be changed, they can be changed in one
place, and programs that include the header file will automatically use the
new version when next recompiled. The header file eliminates the labor of
finding and changing all the copies as well as the risk that a failure to
find one copy will result in inconsistencies within a program.
In C, the convention is to give header files names that end with `.h'.
Read more about using header files in official GCC documentation:
* Include Syntax
* Include Operation
* Once-Only Headers
* Computed Includes
https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html

46
lib/README Normal file
View File

@ -0,0 +1,46 @@
This directory is intended for project specific (private) libraries.
PlatformIO will compile them to static libraries and link into the executable file.
The source code of each library should be placed in a separate directory
("lib/your_library_name/[Code]").
For example, see the structure of the following example libraries `Foo` and `Bar`:
|--lib
| |
| |--Bar
| | |--docs
| | |--examples
| | |--src
| | |- Bar.c
| | |- Bar.h
| | |- library.json (optional. for custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
| |
| |--Foo
| | |- Foo.c
| | |- Foo.h
| |
| |- README --> THIS FILE
|
|- platformio.ini
|--src
|- main.c
Example contents of `src/main.c` using Foo and Bar:
```
#include <Foo.h>
#include <Bar.h>
int main (void)
{
...
}
```
The PlatformIO Library Dependency Finder will find automatically dependent
libraries by scanning project source files.
More information about PlatformIO Library Dependency Finder
- https://docs.platformio.org/page/librarymanager/ldf.html

20
platformio.ini Normal file
View File

@ -0,0 +1,20 @@
; PlatformIO Project Configuration File
;
; Build options: build flags, source filter
; Upload options: custom upload port, speed and extra flags
; Library options: dependencies, extra library storages
; Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
lib_deps =
ArduinoJson
Time
SparkFun VEML7700
LoRa
sparkfun/SparkFun VEML7700 Arduino Library@^1.0.0

757
src/main.cpp Normal file
View File

@ -0,0 +1,757 @@
#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 = "porch_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));
}

11
test/README Normal file
View File

@ -0,0 +1,11 @@
This directory is intended for PlatformIO Test Runner and project tests.
Unit Testing is a software testing method by which individual units of
source code, sets of one or more MCU program modules together with associated
control data, usage procedures, and operating procedures, are tested to
determine whether they are fit for use. Unit testing finds problems early
in the development cycle.
More information about PlatformIO Unit Testing:
- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html