Added particle project

This commit is contained in:
2025-07-07 18:43:26 -06:00
parent 7aaec00d8f
commit 2ae56d8c4c
7 changed files with 1105 additions and 1 deletions

View File

@ -0,0 +1,37 @@
# Particle Compile Action Workflow
# This workflow uses the Particle compile-action to compile Particle application firmware.
# Make sure to set the particle-platform-name for your project.
# For complete documentation, please refer to https://github.com/particle-iot/compile-action
name: Particle Compile
on:
push:
branches:
- main
jobs:
compile:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
# Particle Compile Action
- name: Compile Firmware
id: compile
uses: particle-iot/compile-action@v1
with:
# Set the particle-platform-name to the platform you're targeting.
# Allowed values: core, photon, p1, electron, argon, boron, xenon, esomx, bsom, b5som, tracker, trackerm, p2, msom
particle-platform-name: 'p2'
# Optional: Upload compiled firmware as an artifact on GitHub.
- name: Upload Firmware as Artifact
uses: actions/upload-artifact@v3
with:
name: firmware-artifact
path: |
${{ steps.compile.outputs.firmware-path }}
${{ steps.compile.outputs.target-path }}

55
Particle/SecurityMonitor/.gitignore vendored Normal file
View File

@ -0,0 +1,55 @@
# Key files
*.der
*.pem
# Ignore build results and bundles
*.bin
*.zip
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
target/*
# Platform-specific settings
.DS_Store
*.crc_block
*.no_crc
# VisualStudioCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Ignore all local history of files
**/.history
# Windows
Thumbs.db
*.stackdump
[Dd]esktop.ini
# C Prerequisites
*.d
# C Object files
*.o
*.ko
*.obj
*.elf
# C Linker output
*.map
# C Debug files
*.dSYM/
*.su
*.idb
*.pdb

View File

@ -0,0 +1,100 @@
# SecurityMonitor
This firmware project was created using [Particle Developer Tools](https://www.particle.io/developer-tools/) and is compatible with all [Particle Devices](https://www.particle.io/devices/).
Feel free to replace this README.md file with your own content, or keep it for reference.
## Table of Contents
- [Introduction](#introduction)
- [Prerequisites To Use This Template](#prerequisites-to-use-this-repository)
- [Getting Started](#getting-started)
- [Particle Firmware At A Glance](#particle-firmware-at-a-glance)
- [Logging](#logging)
- [Setup and Loop](#setup-and-loop)
- [Delays and Timing](#delays-and-timing)
- [Testing and Debugging](#testing-and-debugging)
- [GitHub Actions (CI/CD)](#github-actions-cicd)
- [OTA](#ota)
- [Support and Feedback](#support-and-feedback)
- [Version](#version)
## Introduction
For an in-depth understanding of this project template, please refer to our [documentation](https://docs.particle.io/firmware/best-practices/firmware-template/).
## Prerequisites To Use This Repository
To use this software/firmware on a device, you'll need:
- A [Particle Device](https://www.particle.io/devices/).
- Windows/Mac/Linux for building the software and flashing it to a device.
- [Particle Development Tools](https://docs.particle.io/getting-started/developer-tools/developer-tools/) installed and set up on your computer.
- Optionally, a nice cup of tea (and perhaps a biscuit).
## Getting Started
1. While not essential, we recommend running the [device setup process](https://setup.particle.io/) on your Particle device first. This ensures your device's firmware is up-to-date and you have a solid baseline to start from.
2. If you haven't already, open this project in Visual Studio Code (File -> Open Folder). Then [compile and flash](https://docs.particle.io/getting-started/developer-tools/workbench/#cloud-build-and-flash) your device. Ensure your device's USB port is connected to your computer.
3. Verify the device's operation by monitoring its logging output:
- In Visual Studio Code with the Particle Plugin, open the [command palette](https://docs.particle.io/getting-started/developer-tools/workbench/#particle-commands) and choose "Particle: Serial Monitor".
- Or, using the Particle CLI, execute:
```
particle serial monitor --follow
```
4. Uncomment the code at the bottom of the cpp file in your src directory to publish to the Particle Cloud! Login to console.particle.io to view your devices events in real time.
5. Customize this project! For firmware details, see [Particle firmware](https://docs.particle.io/reference/device-os/api/introduction/getting-started/). For information on the project's directory structure, visit [this link](https://docs.particle.io/firmware/best-practices/firmware-template/#project-overview).
## Particle Firmware At A Glance
### Logging
The firmware includes a [logging library](https://docs.particle.io/reference/device-os/api/logging/logger-class/). You can display messages at different levels and filter them:
```
Log.trace("This is trace message");
Log.info("This is info message");
Log.warn("This is warn message");
Log.error("This is error message");
```
### Setup and Loop
Particle projects originate from the Wiring/Processing framework, which is based on C++. Typically, one-time setup functions are placed in `setup()`, and the main application runs from the `loop()` function.
For advanced scenarios, explore our [threading support](https://docs.particle.io/firmware/software-design/threading-explainer/).
### Delays and Timing
By default, the setup() and loop() functions are blocking whilst they run, meaning that if you put in a delay, your entire application will wait for that delay to finish before anything else can run.
For techniques that allow you to run multiple tasks in parallel without creating threads, checkout the code example [here](https://docs.particle.io/firmware/best-practices/firmware-template/).
(Note: Although using `delay()` isn't recommended for best practices, it's acceptable for testing.)
### Testing and Debugging
For firmware testing and debugging guidance, check [this documentation](https://docs.particle.io/troubleshooting/guides/build-tools-troubleshooting/debugging-firmware-builds/).
### GitHub Actions (CI/CD)
This project provides a YAML file for GitHub, automating firmware compilation whenever changes are pushed. More details on [Particle GitHub Actions](https://docs.particle.io/firmware/best-practices/github-actions/) are available.
### OTA
To learn how to utilize Particle's OTA service for device updates, consult [this documentation](https://docs.particle.io/getting-started/cloud/ota-updates/).
Test OTA with the 'Particle: Cloud Flash' command in Visual Studio Code or the CLI command 'particle flash'!
This firmware supports binary assets in OTA packages, allowing the inclusion of audio, images, configurations, and external microcontroller firmware. More details are [here](https://docs.particle.io/reference/device-os/api/asset-ota/asset-ota/).
## Support and Feedback
For support or feedback on this template or any Particle products, please join our [community](https://community.particle.io)!
## Version
Template version 1.0.2

View File

@ -0,0 +1,2 @@
name=SecurityMonitor
#assetOtaDir=assets

View File

@ -0,0 +1,708 @@
#include "Particle.h"
// Ultra low power system settings for Boron LTE
SYSTEM_MODE(MANUAL); // Manual control over cellular connection
SYSTEM_THREAD(ENABLED);
// DEBUG MODE - Enable for troubleshooting (DISABLE in production!)
// #define DEBUG_MODE
#ifdef DEBUG_MODE
#define DEBUG_SERIAL_SPEED 9600
#define DEBUG_DELAY 100
SerialLogHandler logHandler(LOG_LEVEL_ALL);
#endif
// Pin definitions for Boron
const int MICROSWITCH_PIN = D2; // Microswitch (NC - normally closed)
const int ALARM_PIN = D6; // Alarm output
const int STATUS_LED = D7; // Built-in LED for status
// Ultra low power settings for LTE Boron
const unsigned long DAILY_BATTERY_REPORT = 86400; // 24 hours in seconds
const unsigned long ALARM_DURATION = 10000; // 10 seconds alarm duration
const unsigned long CELLULAR_TIMEOUT = 120000; // 120 seconds to connect to cellular
const unsigned long PUBLISH_TIMEOUT = 30000; // 30 seconds to publish
// Persistent state (survives STOP mode sleep)
retained unsigned long lastBatteryReport = 0;
retained unsigned long bootCount = 0;
retained bool deviceInitialized = false;
retained float lastReportedBattery = 100.0;
// Wake up reasons for Boron
enum BoronWakeReason {
WAKE_SECURITY_BREACH, // Microswitch triggered
WAKE_DAILY_REPORT, // Daily battery check
WAKE_COLD_START // Power on/reset
};
// Function prototypes
BoronWakeReason determineWakeReason();
void handleSecurityBreach();
void handleDailyReport();
void handleColdStart();
void activateSecurityAlarm();
void maintainAlarmDuration();
bool connectToCellular();
void publishSecurityAlert(String alertData);
String createSecurityAlert();
String createBatteryReport(float batteryLevel);
String createStartupAlert();
void flashStatusLED(int count, int duration);
void enterUltraLowPowerSleep();
void handleWakeUp(SystemSleepResult sleepResult);
// Debug helper functions
#ifdef DEBUG_MODE
void debugPrint(String message) {
Serial.printf("[%lu] %s\r\n", millis(), message.c_str());
Serial.flush();
delay(DEBUG_DELAY);
}
void debugSystemState() {
debugPrint("=== SYSTEM STATE ===");
debugPrint("Boot count: " + String(bootCount));
debugPrint("Device initialized: " + String(deviceInitialized ? "YES" : "NO"));
debugPrint("Battery: " + String(System.batteryCharge()) + "%");
debugPrint("Free memory: " + String(System.freeMemory()) + " bytes");
debugPrint("Uptime: " + String(millis() / 1000) + " seconds");
debugPrint("D2 (switch): " + String(digitalRead(MICROSWITCH_PIN) ? "HIGH" : "LOW"));
debugPrint("D3 (alarm): " + String(digitalRead(ALARM_PIN) ? "HIGH" : "LOW"));
debugPrint("Last battery report: " + String(lastBatteryReport));
debugPrint("Time now: " + String(Time.now()));
if (Cellular.ready()) {
debugPrint("Cellular signal: " + String(Cellular.RSSI().getStrength()) + " dBm");
debugPrint("Cellular quality: " + String(Cellular.RSSI().getQuality()));
} else {
debugPrint("Cellular: NOT READY");
}
debugPrint("==================");
}
void debugPinStates() {
debugPrint("=== PIN STATES ===");
debugPrint("D2 (MICROSWITCH): " + String(digitalRead(MICROSWITCH_PIN)));
debugPrint("D3 (ALARM): " + String(digitalRead(ALARM_PIN)));
debugPrint("D7 (STATUS_LED): " + String(digitalRead(STATUS_LED)));
debugPrint("==================");
}
void debugSleepResult(SystemSleepResult result) {
debugPrint("=== SLEEP WAKE DETAILS ===");
debugPrint("Wake reason: " + String((int)result.wakeupReason()));
debugPrint("Wake pin: " + String(result.wakeupPin()));
// debugPrint("RTC wake: " + String(result.rtcWakeup() ? "YES" : "NO"));
debugPrint("Error: " + String((int)result.error()));
debugPrint("========================");
}
#else
#define debugPrint(message)
#define debugSystemState()
#define debugPinStates()
#define debugSleepResult(result)
#endif
void setup() {
bootCount++;
#ifdef DEBUG_MODE
Serial.begin(DEBUG_SERIAL_SPEED);
delay(3000); // Wait for serial connection
debugPrint("=== BORON SECURITY DEVICE STARTUP ===");
debugPrint("Firmware version: 1.0.0");
debugPrint("Build date: " + String(__DATE__) + " " + String(__TIME__));
debugPrint("Device ID: " + System.deviceID());
#endif
// Initialize pins immediately for security
pinMode(MICROSWITCH_PIN, INPUT_PULLUP); // Pullup for NC switch
pinMode(ALARM_PIN, OUTPUT);
pinMode(STATUS_LED, OUTPUT);
// Ensure alarm starts OFF
digitalWrite(ALARM_PIN, LOW);
digitalWrite(STATUS_LED, LOW);
debugPrint("Pin initialization complete");
debugPinStates();
debugSystemState();
// Add delay to stabilize pin readings
delay(500);
// Determine why we woke up
debugPrint("Determining wake reason...");
BoronWakeReason wakeReason = determineWakeReason();
switch (wakeReason) {
case WAKE_SECURITY_BREACH:
debugPrint("Wake reason: SECURITY BREACH");
break;
case WAKE_DAILY_REPORT:
debugPrint("Wake reason: DAILY REPORT");
break;
case WAKE_COLD_START:
debugPrint("Wake reason: COLD START");
break;
}
// Handle based on wake reason
switch (wakeReason) {
case WAKE_SECURITY_BREACH:
handleSecurityBreach();
break;
case WAKE_DAILY_REPORT:
handleDailyReport();
break;
case WAKE_COLD_START:
handleColdStart();
break;
}
debugPrint("Main processing complete, preparing for sleep");
debugSystemState();
debugPinStates();
// Add delay before sleep to ensure all debug output is sent
debugPrint("Waiting before sleep to ensure debug output complete...");
delay(2000);
// Enter ultra low power sleep and handle wake-up in a loop
enterUltraLowPowerSleep();
}
void loop() {
// Should never reach here in ultra low power mode
debugPrint("ERROR: Reached loop() - this should not happen!");
debugPrint("Going to sleep immediately...");
delay(1000);
enterUltraLowPowerSleep();
}
BoronWakeReason determineWakeReason() {
debugPrint("Checking wake reason...");
// Check if microswitch is open (security breach)
// NC switch: HIGH = closed (normal), LOW = open (breach)
bool switchOpen = (digitalRead(MICROSWITCH_PIN) == LOW);
debugPrint("Microswitch raw reading: " + String(digitalRead(MICROSWITCH_PIN)));
debugPrint("Microswitch state: " + String(switchOpen ? "OPEN (BREACH)" : "CLOSED (NORMAL)"));
// Double-check pin reading with delay
delay(100);
bool switchOpen2 = (digitalRead(MICROSWITCH_PIN) == LOW);
debugPrint("Microswitch second reading: " + String(digitalRead(MICROSWITCH_PIN)));
debugPrint("Microswitch confirmed: " + String(switchOpen2 ? "OPEN (BREACH)" : "CLOSED (NORMAL)"));
if (switchOpen && switchOpen2) {
debugPrint("Security breach detected!");
return WAKE_SECURITY_BREACH;
}
// Check if it's time for daily battery report
unsigned long currentTime = Time.now();
debugPrint("Current time: " + String(currentTime));
debugPrint("Last battery report: " + String(lastBatteryReport));
debugPrint("Time since last report: " + String(currentTime - lastBatteryReport) + " seconds");
debugPrint("Report interval: " + String(DAILY_BATTERY_REPORT) + " seconds");
if (deviceInitialized && (currentTime - lastBatteryReport) >= DAILY_BATTERY_REPORT) {
debugPrint("Time for daily battery report");
return WAKE_DAILY_REPORT;
}
// Must be a cold start (power on, reset, or first boot)
debugPrint("Determined as cold start");
return WAKE_COLD_START;
}
void handleSecurityBreach() {
debugPrint("=== HANDLING SECURITY BREACH ===");
// IMMEDIATE ALARM ACTIVATION FIRST - before any other processing
debugPrint("IMMEDIATE: Activating security alarm...");
activateSecurityAlarm();
// Status indication
debugPrint("Flashing status LED...");
flashStatusLED(5, 100); // 5 rapid flashes
// Now connect and send alert
debugPrint("Attempting cellular connection...");
if (connectToCellular()) {
debugPrint("Cellular connected, creating alert...");
String alertData = createSecurityAlert();
debugPrint("Alert data: " + alertData);
publishSecurityAlert(alertData);
// Brief delay to ensure message is sent
debugPrint("Waiting for message transmission...");
delay(5000);
} else {
debugPrint("Failed to connect to cellular!");
}
// Keep alarm on for full duration even if network fails
debugPrint("Maintaining alarm duration...");
maintainAlarmDuration();
// Turn off alarm
debugPrint("Deactivating alarm");
digitalWrite(ALARM_PIN, LOW);
digitalWrite(STATUS_LED, LOW);
debugPrint("Security breach handling complete");
}
void handleDailyReport() {
debugPrint("=== HANDLING DAILY REPORT ===");
// Quick status blink
flashStatusLED(2, 200);
// Connect and send daily battery report
debugPrint("Attempting cellular connection for daily report...");
if (connectToCellular()) {
float currentBattery = System.batteryCharge();
debugPrint("Current battery level: " + String(currentBattery) + "%");
String reportData = createBatteryReport(currentBattery);
debugPrint("Report data: " + reportData);
publishSecurityAlert(reportData);
// Update tracking
lastBatteryReport = Time.now();
lastReportedBattery = currentBattery;
debugPrint("Updated last battery report time: " + String(lastBatteryReport));
delay(3000); // Ensure transmission completes
} else {
debugPrint("Failed to connect to cellular for daily report!");
}
debugPrint("Daily report handling complete");
}
void handleColdStart() {
debugPrint("=== HANDLING COLD START ===");
// Device just powered on or reset
deviceInitialized = false;
// Startup indication (3 slow blinks)
debugPrint("Startup indication...");
flashStatusLED(3, 500);
debugPrint("Attempting cellular connection for startup alert...");
if (connectToCellular()) {
String startupData = createStartupAlert();
debugPrint("Startup data: " + startupData);
publishSecurityAlert(startupData);
// Initialize tracking
deviceInitialized = true;
lastBatteryReport = Time.now();
lastReportedBattery = System.batteryCharge();
debugPrint("Device initialization complete");
debugPrint("Initialized battery report time: " + String(lastBatteryReport));
delay(3000);
} else {
debugPrint("Failed to connect to cellular for startup alert!");
// Still mark as initialized even if we can't report
deviceInitialized = true;
lastBatteryReport = Time.now();
lastReportedBattery = System.batteryCharge();
}
debugPrint("Cold start handling complete");
}
void activateSecurityAlarm() {
debugPrint("ALARM: IMMEDIATE activation - HIGHEST PRIORITY");
// IMMEDIATE alarm activation - highest priority
digitalWrite(ALARM_PIN, HIGH);
digitalWrite(STATUS_LED, HIGH);
debugPrint("ALARM: Alarm pin set HIGH");
debugPrint("ALARM: Current pin state - D3: " + String(digitalRead(ALARM_PIN)));
// Quick beep pattern to confirm activation
for (int i = 0; i < 3; i++) {
debugPrint("ALARM: Beep pattern " + String(i + 1));
delay(100);
digitalWrite(STATUS_LED, LOW);
delay(100);
digitalWrite(STATUS_LED, HIGH);
}
debugPrint("ALARM: Activation complete - alarm should be ON");
}
void maintainAlarmDuration() {
debugPrint("ALARM: Maintaining alarm for " + String(ALARM_DURATION) + "ms");
debugPrint("ALARM: Current alarm pin state: " + String(digitalRead(ALARM_PIN)));
// Ensure alarm is still on
digitalWrite(ALARM_PIN, HIGH);
// Keep alarm on for specified duration
unsigned long alarmStart = millis();
while (millis() - alarmStart < ALARM_DURATION) {
// Ensure alarm stays on
digitalWrite(ALARM_PIN, HIGH);
// Flash status LED while alarm is active
digitalWrite(STATUS_LED, HIGH);
delay(250);
digitalWrite(STATUS_LED, LOW);
delay(250);
// Check if switch closed again (breach ended)
if (digitalRead(MICROSWITCH_PIN) == HIGH) {
debugPrint("ALARM: Switch closed again, breach ended");
// Optionally end alarm early
// break;
}
unsigned long remaining = ALARM_DURATION - (millis() - alarmStart);
if (remaining % 2000 == 0) { // Log every 2 seconds
debugPrint("ALARM: Still active, " + String(remaining / 1000) + "s remaining");
debugPrint("ALARM: Pin state check - D3: " + String(digitalRead(ALARM_PIN)));
}
}
debugPrint("ALARM: Duration complete");
}
bool connectToCellular() {
debugPrint("CELLULAR: Starting connection attempt");
unsigned long startTime = millis();
// // Status indication - connecting
// digitalWrite(STATUS_LED, HIGH);
// // Ensure cellular is off first, then turn on (fresh start)
// debugPrint("CELLULAR: Resetting cellular radio");
// Cellular.off();
// delay(2000);
// // Enable cellular radio
// debugPrint("CELLULAR: Turning on cellular radio");
// Cellular.on();
// // Wait longer for cellular ready
// debugPrint("CELLULAR: Waiting for cellular ready...");
// unsigned long cellularStart = millis();
// while (!Cellular.ready() && (millis() - cellularStart) < (CELLULAR_TIMEOUT / 2)) {
// debugPrint("CELLULAR: Still waiting for radio... " + String((millis() - cellularStart) / 1000) + "s");
// delay(5000); // Check every 5 seconds instead of 2
// }
// if (Cellular.ready()) {
// debugPrint("CELLULAR: Radio ready");
// CellularSignal signal = Cellular.RSSI();
// debugPrint("CELLULAR: Signal strength: " + String(signal.getStrength()) + " dBm");
// debugPrint("CELLULAR: Signal quality: " + String(signal.getQuality()));
// // Check signal strength
// if (signal.getStrength() < -100) {
// debugPrint("CELLULAR: WARNING - Very weak signal!");
// }
// } else {
// debugPrint("CELLULAR: Radio failed to become ready");
// digitalWrite(STATUS_LED, LOW);
// return false;
// }
// Connect to Particle cloud via cellular
debugPrint("CELLULAR: Connecting to Particle cloud");
Particle.connect();
// Wait for connection with timeout
while (!Particle.connected() && (millis() - startTime) < CELLULAR_TIMEOUT) {
Particle.process();
// Blink during connection attempt
digitalWrite(STATUS_LED, (millis() / 500) % 2);
unsigned long elapsed = millis() - startTime;
if (elapsed % 10000 == 0) { // Log every 10 seconds
debugPrint("CELLULAR: Still connecting... " + String(elapsed / 1000) + "s elapsed");
}
delay(100);
}
bool connected = Particle.connected();
if (connected) {
debugPrint("CELLULAR: Successfully connected to Particle cloud");
debugPrint("CELLULAR: Connection time: " + String((millis() - startTime) / 1000) + " seconds");
// Success - solid LED for 1 second
digitalWrite(STATUS_LED, HIGH);
delay(1000);
} else {
debugPrint("CELLULAR: Failed to connect to Particle cloud");
debugPrint("CELLULAR: Timeout after " + String(CELLULAR_TIMEOUT / 1000) + " seconds");
// Failed - rapid blinks
flashStatusLED(10, 50);
}
digitalWrite(STATUS_LED, LOW);
return connected;
}
void publishSecurityAlert(String alertData) {
debugPrint("PUBLISH: Attempting to publish alert");
debugPrint("PUBLISH: Data length: " + String(alertData.length()) + " characters");
if (!Particle.connected()) {
debugPrint("PUBLISH: Not connected to Particle cloud!");
return;
}
// Try to publish with retries
for (int attempts = 0; attempts < 3; attempts++) {
debugPrint("PUBLISH: Attempt " + String(attempts + 1) + " of 3");
bool success = Particle.publish("Security Alert", alertData, PRIVATE);
if (success) {
debugPrint("PUBLISH: Success on attempt " + String(attempts + 1));
// Success indication
flashStatusLED(2, 100);
break;
} else {
debugPrint("PUBLISH: Failed on attempt " + String(attempts + 1));
// Retry indication
flashStatusLED(1, 50);
delay(2000);
}
}
// Allow time for message to be sent
debugPrint("PUBLISH: Waiting for transmission completion");
unsigned long publishStart = millis();
while (millis() - publishStart < PUBLISH_TIMEOUT) {
Particle.process();
delay(100);
if ((millis() - publishStart) % 10000 == 0) { // Log every 10 seconds
debugPrint("PUBLISH: Still waiting... " + String((millis() - publishStart) / 1000) + "s");
}
}
debugPrint("PUBLISH: Transmission window complete");
}
String createSecurityAlert() {
debugPrint("Creating security alert data");
float batteryLevel = System.batteryCharge();
CellularSignal signal = Cellular.RSSI();
String alertData = String::format(
"Type:SECURITY_BREACH|Desc:Microswitch opened - INTRUDER DETECTED|Batt:%.1f%%|Signal:%d|Quality:%d|Boot:%lu|Alarm:ON",
batteryLevel,
signal.getStrength(),
signal.getQuality(),
bootCount
);
debugPrint("Security alert created: " + alertData);
return alertData;
}
String createBatteryReport(float batteryLevel) {
debugPrint("Creating battery report data");
CellularSignal signal = Cellular.RSSI();
String alertType;
String description;
if (batteryLevel <= 10.0 && batteryLevel > 0) {
alertType = "CRITICAL_BATTERY";
description = String::format("CRITICAL: Battery at %.1f%% - Device may shutdown soon", batteryLevel);
} else if (batteryLevel <= 20.0) {
alertType = "LOW_BATTERY";
description = String::format("LOW: Battery at %.1f%% - Consider servicing", batteryLevel);
} else {
alertType = "DAILY_BATTERY_REPORT";
description = String::format("Daily report: Battery at %.1f%%, operating normally", batteryLevel);
}
String alertData = String::format(
"Type:%s|Desc:%s|Batt:%.1f%%|Signal:%d|Quality:%d|Boot:%lu|Days:%lu",
alertType.c_str(),
description.c_str(),
batteryLevel,
signal.getStrength(),
signal.getQuality(),
bootCount,
(Time.now() - lastBatteryReport) / 86400
);
debugPrint("Battery report created: " + alertData);
return alertData;
}
String createStartupAlert() {
debugPrint("Creating startup alert data");
float batteryLevel = System.batteryCharge();
CellularSignal signal = Cellular.RSSI();
String description = String::format(
"Device startup - Boot #%lu, Battery: %.1f%%, Ready for security monitoring",
bootCount,
batteryLevel
);
String alertData = String::format(
"Type:DEVICE_STARTUP|Desc:%s|Batt:%.1f%%|Signal:%d|Quality:%d|Firmware:1.0.0",
description.c_str(),
batteryLevel,
signal.getStrength(),
signal.getQuality()
);
debugPrint("Startup alert created: " + alertData);
return alertData;
}
void flashStatusLED(int count, int duration) {
debugPrint("Flashing LED " + String(count) + " times, " + String(duration) + "ms each");
for (int i = 0; i < count; i++) {
digitalWrite(STATUS_LED, HIGH);
delay(duration);
digitalWrite(STATUS_LED, LOW);
delay(duration);
}
}
void enterUltraLowPowerSleep() {
debugPrint("=== ENTERING ULTRA LOW POWER SLEEP ===");
// Ensure alarm is OFF before sleep
digitalWrite(ALARM_PIN, LOW);
digitalWrite(STATUS_LED, LOW);
debugPrint("SLEEP: Alarm and LED turned OFF");
// Disconnect from cellular to save maximum power
if (Particle.connected()) {
debugPrint("SLEEP: Disconnecting from Particle cloud");
Particle.disconnect();
delay(3000); // Give more time to disconnect cleanly
}
// Turn off cellular radio completely
debugPrint("SLEEP: Turning off cellular radio");
Cellular.off();
delay(2000);
debugPrint("SLEEP: Configuring sleep mode");
debugPrint("SLEEP: Sleep duration: 24 hours");
debugPrint("SLEEP: Wake pin: D2 (FALLING edge)");
SystemSleepConfiguration config;
config.mode(SystemSleepMode::STOP) // STOP mode for Boron (supports RTC wake)
.gpio(MICROSWITCH_PIN, FALLING) // Wake on switch opening (NC switch goes LOW)
.duration(24h); // Wake once per day for battery report
debugPrint("SLEEP: Entering sleep mode NOW");
#ifdef DEBUG_MODE
Serial.flush(); // Ensure all debug output is sent
delay(500); // Extra time for serial flush
#endif
// Enter sleep loop - handle wake-ups and go back to sleep
while (true) {
// Enter sleep - device will wake on pin interrupt OR after 24 hours
SystemSleepResult result = System.sleep(config);
// When we wake up, we're here - handle the wake-up
debugPrint("SLEEP: Woke up from sleep!");
debugSleepResult(result);
// Handle the wake-up event
handleWakeUp(result);
// Ensure we're ready for next sleep
digitalWrite(ALARM_PIN, LOW);
digitalWrite(STATUS_LED, LOW);
debugPrint("SLEEP: Going back to sleep...");
#ifdef DEBUG_MODE
Serial.flush();
delay(500);
#endif
}
}
void handleWakeUp(SystemSleepResult sleepResult) {
debugPrint("=== HANDLING WAKE UP ===");
// Re-initialize pins (they should maintain state, but just to be sure)
pinMode(MICROSWITCH_PIN, INPUT_PULLUP);
pinMode(ALARM_PIN, OUTPUT);
pinMode(STATUS_LED, OUTPUT);
digitalWrite(ALARM_PIN, LOW);
digitalWrite(STATUS_LED, LOW);
debugSystemState();
debugPinStates();
// Determine wake reason based on sleep result
BoronWakeReason wakeReason;
if (sleepResult.wakeupReason() == SystemSleepWakeupReason::BY_GPIO) {
debugPrint("WAKE: Woke by GPIO (microswitch)");
// Verify the microswitch is actually open
delay(100); // Debounce
if (digitalRead(MICROSWITCH_PIN) == LOW) {
wakeReason = WAKE_SECURITY_BREACH;
debugPrint("WAKE: Confirmed security breach");
} else {
debugPrint("WAKE: False alarm - switch is closed");
return; // Go back to sleep immediately
}
} else if (sleepResult.wakeupReason() == SystemSleepWakeupReason::BY_RTC) {
debugPrint("WAKE: Woke by RTC (24-hour timer)");
wakeReason = WAKE_DAILY_REPORT;
} else {
debugPrint("WAKE: Unknown wake reason, treating as daily report");
wakeReason = WAKE_DAILY_REPORT;
}
// Handle the wake reason
switch (wakeReason) {
case WAKE_SECURITY_BREACH:
debugPrint("WAKE: Handling security breach");
handleSecurityBreach();
break;
case WAKE_DAILY_REPORT:
debugPrint("WAKE: Handling daily report");
handleDailyReport();
break;
default:
debugPrint("WAKE: Unknown wake reason");
break;
}
debugPrint("WAKE: Wake-up handling complete");
}
// Optional: Handle system events for debugging
void onCellularConnect() {
debugPrint("EVENT: Cellular connected");
flashStatusLED(1, 200);
}
void onCellularDisconnect() {
debugPrint("EVENT: Cellular disconnected");
flashStatusLED(2, 100);
}
// Uncomment for debugging (will increase power consumption)
// STARTUP(cellular.on());
// SYSTEM(SYSTEM_MODE(SEMI_AUTOMATIC));