378 lines
11 KiB
C++
378 lines
11 KiB
C++
#include "Particle.h"
|
|
|
|
// Ultra low power system settings for Boron LTE
|
|
SYSTEM_MODE(MANUAL); // Manual control over cellular connection
|
|
SYSTEM_THREAD(ENABLED);
|
|
|
|
// Pin definitions for Boron
|
|
const int MICROSWITCH_PIN = D2; // Microswitch (NC - normally closed)
|
|
const int ALARM_PIN = D3; // 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 = 60000; // 60 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 setup() {
|
|
bootCount++;
|
|
|
|
// 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);
|
|
|
|
// Determine why we woke up
|
|
BoronWakeReason wakeReason = determineWakeReason();
|
|
|
|
// 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;
|
|
}
|
|
|
|
// Always go back to ultra low power sleep
|
|
enterUltraLowPowerSleep();
|
|
}
|
|
|
|
void loop() {
|
|
// Should never reach here in ultra low power mode
|
|
// If we do, go to sleep immediately
|
|
enterUltraLowPowerSleep();
|
|
}
|
|
|
|
BoronWakeReason determineWakeReason() {
|
|
// Check if microswitch is open (security breach)
|
|
// NC switch: HIGH = closed (normal), LOW = open (breach)
|
|
bool switchOpen = (digitalRead(MICROSWITCH_PIN) == LOW);
|
|
|
|
if (switchOpen) {
|
|
return WAKE_SECURITY_BREACH;
|
|
}
|
|
|
|
// Check if it's time for daily battery report
|
|
unsigned long currentTime = Time.now();
|
|
if (deviceInitialized && (currentTime - lastBatteryReport) >= DAILY_BATTERY_REPORT) {
|
|
return WAKE_DAILY_REPORT;
|
|
}
|
|
|
|
// Must be a cold start (power on, reset, or first boot)
|
|
return WAKE_COLD_START;
|
|
}
|
|
|
|
void handleSecurityBreach() {
|
|
// IMMEDIATE ALARM ACTIVATION (before any network activity)
|
|
activateSecurityAlarm();
|
|
|
|
// Status indication
|
|
flashStatusLED(5, 100); // 5 rapid flashes
|
|
|
|
// Now connect and send alert
|
|
if (connectToCellular()) {
|
|
String alertData = createSecurityAlert();
|
|
publishSecurityAlert(alertData);
|
|
|
|
// Brief delay to ensure message is sent
|
|
delay(5000);
|
|
}
|
|
|
|
// Keep alarm on for full duration even if network fails
|
|
maintainAlarmDuration();
|
|
|
|
// Turn off alarm
|
|
digitalWrite(ALARM_PIN, LOW);
|
|
}
|
|
|
|
void handleDailyReport() {
|
|
// Quick status blink
|
|
flashStatusLED(2, 200);
|
|
|
|
// Connect and send daily battery report
|
|
if (connectToCellular()) {
|
|
float currentBattery = System.batteryCharge();
|
|
String reportData = createBatteryReport(currentBattery);
|
|
publishSecurityAlert(reportData);
|
|
|
|
// Update tracking
|
|
lastBatteryReport = Time.now();
|
|
lastReportedBattery = currentBattery;
|
|
|
|
delay(3000); // Ensure transmission completes
|
|
}
|
|
}
|
|
|
|
void handleColdStart() {
|
|
// Device just powered on or reset
|
|
deviceInitialized = false;
|
|
|
|
// Startup indication (3 slow blinks)
|
|
flashStatusLED(3, 500);
|
|
|
|
if (connectToCellular()) {
|
|
String startupData = createStartupAlert();
|
|
publishSecurityAlert(startupData);
|
|
|
|
// Initialize tracking
|
|
deviceInitialized = true;
|
|
lastBatteryReport = Time.now();
|
|
lastReportedBattery = System.batteryCharge();
|
|
|
|
delay(3000);
|
|
}
|
|
}
|
|
|
|
void activateSecurityAlarm() {
|
|
// IMMEDIATE alarm activation - highest priority
|
|
digitalWrite(ALARM_PIN, HIGH);
|
|
digitalWrite(STATUS_LED, HIGH);
|
|
|
|
// Quick beep pattern to confirm activation
|
|
// (Remove if you don't want any delay before network connection)
|
|
for (int i = 0; i < 3; i++) {
|
|
delay(100);
|
|
digitalWrite(STATUS_LED, LOW);
|
|
delay(100);
|
|
digitalWrite(STATUS_LED, HIGH);
|
|
}
|
|
}
|
|
|
|
void maintainAlarmDuration() {
|
|
// Keep alarm on for specified duration
|
|
unsigned long alarmStart = millis();
|
|
|
|
while (millis() - alarmStart < ALARM_DURATION) {
|
|
// 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) {
|
|
// Switch closed again - could end alarm early
|
|
// Comment out next line if you want fixed duration regardless
|
|
// break;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool connectToCellular() {
|
|
unsigned long startTime = millis();
|
|
|
|
// Status indication - connecting
|
|
digitalWrite(STATUS_LED, HIGH);
|
|
|
|
// Enable cellular radio
|
|
Cellular.on();
|
|
|
|
// Connect to Particle cloud via cellular
|
|
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);
|
|
delay(100);
|
|
}
|
|
|
|
bool connected = Particle.connected();
|
|
|
|
// Status indication
|
|
if (connected) {
|
|
// Success - solid LED for 1 second
|
|
digitalWrite(STATUS_LED, HIGH);
|
|
delay(1000);
|
|
} else {
|
|
// Failed - rapid blinks
|
|
flashStatusLED(10, 50);
|
|
}
|
|
|
|
digitalWrite(STATUS_LED, LOW);
|
|
return connected;
|
|
}
|
|
|
|
void publishSecurityAlert(String alertData) {
|
|
if (!Particle.connected()) {
|
|
return;
|
|
}
|
|
|
|
// Try to publish with retries
|
|
for (int attempts = 0; attempts < 3; attempts++) {
|
|
bool success = Particle.publish("Security Alert", alertData, PRIVATE);
|
|
|
|
if (success) {
|
|
// Success indication
|
|
flashStatusLED(2, 100);
|
|
break;
|
|
} else {
|
|
// Retry indication
|
|
flashStatusLED(1, 50);
|
|
delay(2000);
|
|
}
|
|
}
|
|
|
|
// Allow time for message to be sent
|
|
unsigned long publishStart = millis();
|
|
while (millis() - publishStart < PUBLISH_TIMEOUT) {
|
|
Particle.process();
|
|
delay(100);
|
|
}
|
|
}
|
|
|
|
String createSecurityAlert() {
|
|
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
|
|
);
|
|
|
|
return alertData;
|
|
}
|
|
|
|
String createBatteryReport(float batteryLevel) {
|
|
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
|
|
);
|
|
|
|
return alertData;
|
|
}
|
|
|
|
String createStartupAlert() {
|
|
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()
|
|
);
|
|
|
|
return alertData;
|
|
}
|
|
|
|
void flashStatusLED(int count, int duration) {
|
|
for (int i = 0; i < count; i++) {
|
|
digitalWrite(STATUS_LED, HIGH);
|
|
delay(duration);
|
|
digitalWrite(STATUS_LED, LOW);
|
|
delay(duration);
|
|
}
|
|
}
|
|
|
|
void enterUltraLowPowerSleep() {
|
|
// Disconnect from cellular to save maximum power
|
|
if (Particle.connected()) {
|
|
Particle.disconnect();
|
|
delay(2000); // Give time to disconnect cleanly
|
|
}
|
|
|
|
// Turn off cellular radio completely
|
|
Cellular.off();
|
|
|
|
// Turn off status LED
|
|
digitalWrite(STATUS_LED, LOW);
|
|
|
|
// Configure ultra low power sleep for Boron
|
|
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
|
|
|
|
// Enter sleep - device will wake on pin interrupt OR after 24 hours
|
|
SystemSleepResult result = System.sleep(config);
|
|
|
|
// When we wake up, setup() will run again to handle the wake reason
|
|
}
|
|
|
|
// Optional: Handle system events for debugging
|
|
void onCellularConnect() {
|
|
// Cellular connected
|
|
flashStatusLED(1, 200);
|
|
}
|
|
|
|
void onCellularDisconnect() {
|
|
// Cellular disconnected
|
|
flashStatusLED(2, 100);
|
|
}
|
|
|
|
// Uncomment for debugging (will increase power consumption)
|
|
// STARTUP(cellular.on());
|
|
// SYSTEM(SYSTEM_MODE(SEMI_AUTOMATIC));
|