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