Initial Platformio project import
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal 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
10
.vscode/extensions.json
vendored
Normal 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
37
include/README
Normal 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
46
lib/README
Normal 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
20
platformio.ini
Normal 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
757
src/main.cpp
Normal 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
11
test/README
Normal 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
|
||||
Reference in New Issue
Block a user