Generalized-Core-Counter 3.20
Particle-based generalized core counter firmware
Loading...
Searching...
No Matches
State_Sleep.cpp
Go to the documentation of this file.
2#include "Config.h"
3#include "Cloud.h"
4#include "LocalTimeRK.h"
5#include "MyPersistentData.h"
6#include "PublishQueuePosixRK.h"
7#include "SensorManager.h"
8#include "device_pinout.h"
9#include "SensorDefinitions.h"
10#include "AB1805_RK.h"
11
12// NOTE:
13// This file was split from StateHandlers.cpp as a mechanical refactor.
14// No behavioral changes were made.
15
21 bool enteredState = (state != oldState);
22 bool ignoreDisconnectFailure = false;
23
24 if (enteredState) {
26 // One-time diagnostic on entry so logs clearly show the device's view of park hours.
27 if (Time.isValid()) {
28 LocalTimeConvert conv;
29 conv.withConfig(LocalTime::instance().getConfig()).withCurrentTime().convert();
30 uint8_t hour = (uint8_t)(conv.getLocalTimeHMS().toSeconds() / 3600);
31 Log.info("SLEEP entry: parkHours %02u-%02u localHour=%02u => %s",
32 sysStatus.get_openTime(), sysStatus.get_closeTime(), hour,
33 isWithinOpenHours() ? "OPEN" : "CLOSED");
34 } else {
35 Log.info("SLEEP entry: Time invalid => treating as OPEN (per policy)");
36 }
37 Log.info("SLEEP entry: sensorReady=%s", SensorManager::instance().isSensorReady() ? "true" : "false");
38 }
39
40 // If a ledger update (or time progression) moves the park into OPEN hours
41 // while we are in SLEEPING_STATE, abort sleeping immediately in CONNECTED
42 // mode so we stay awake/connected and resume counting.
43 if (Time.isValid() && sysStatus.get_operatingMode() == CONNECTED && isWithinOpenHours()) {
44 ensureSensorEnabled("SLEEP abort: CONNECTED+OPEN");
46 return;
47 }
48
49 // If we are connected and the publish queue is not yet in a sleep-safe
50 // state (events queued or a publish in progress), defer sleeping so we
51 // can finish delivering data. When offline, allow sleep immediately;
52 // queued events will be flushed on the next connection.
53 if (Particle.connected() && !PublishQueuePosix::instance().getCanSleep()) {
54 static size_t lastPendingLogged = (size_t)-1;
55 static unsigned long lastDeferralLogMs = 0;
56
57 size_t pending = PublishQueuePosix::instance().getNumEvents();
58 unsigned long nowMs = millis();
59 bool shouldLog = (pending != lastPendingLogged) || (nowMs - lastDeferralLogMs) > 5000UL;
60 if (shouldLog) {
61 Log.info("Deferring sleep - publish queue has %u pending event(s) or publish in progress",
62 (unsigned)pending);
63 lastPendingLogged = pending;
64 lastDeferralLogMs = nowMs;
65 }
66 // Stay in SLEEPING_STATE until the queue is sleep-safe.
67 return;
68 }
69
70 // ********** Non-blocking disconnect + modem power-down **********
71 // Device OS already manages the asynchronous cloud session teardown once
72 // Particle.disconnect() has been requested. Here, we keep application
73 // logic minimal: request cloud disconnect and radio-off once, then wait
74 // (bounded) until both cloud and modem are actually off before sleeping.
75 static bool disconnectRequested = false;
76 static unsigned long disconnectRequestStartMs = 0;
77
78 if (enteredState) {
79 disconnectRequested = false;
80 disconnectRequestStartMs = 0;
81 }
82
83 bool needDisconnect = Particle.connected() || isRadioPoweredOn();
84 if (needDisconnect) {
85 // Use ledger-configured budgets when available, with conservative defaults.
86 uint16_t cloudBudgetSec = sysStatus.get_cloudDisconnectBudgetSec();
87 if (cloudBudgetSec < 5 || cloudBudgetSec > 120) {
88 cloudBudgetSec = 15;
89 }
90
91 uint16_t modemBudgetSec = sysStatus.get_modemOffBudgetSec();
92 if (modemBudgetSec < 5 || modemBudgetSec > 120) {
93 modemBudgetSec = 30;
94 }
95
96 unsigned long budgetMs = (unsigned long)((modemBudgetSec > cloudBudgetSec) ? modemBudgetSec : cloudBudgetSec) * 1000UL;
97
98 if (!disconnectRequested) {
99 Log.info("SLEEP: requesting cloud disconnect + modem off");
101 disconnectRequested = true;
102 disconnectRequestStartMs = millis();
103 return;
104 }
105
106 bool stillOn = Particle.connected() || isRadioPoweredOn();
107 if (stillOn) {
108 if (disconnectRequestStartMs != 0 && (millis() - disconnectRequestStartMs) > budgetMs) {
109 if (sysStatus.get_operatingMode() != CONNECTED) {
110 Log.warn("SLEEP: disconnect/modem-off exceeded budget (%lu ms) - continuing to sleep",
111 (unsigned long)(millis() - disconnectRequestStartMs));
112 ignoreDisconnectFailure = true;
113 disconnectRequested = false;
114 disconnectRequestStartMs = 0;
115 } else {
116 Log.warn("SLEEP: disconnect/modem-off exceeded budget (%lu ms) - raising alert 15",
117 (unsigned long)(millis() - disconnectRequestStartMs));
118 current.raiseAlert(15);
120 disconnectRequested = false;
121 disconnectRequestStartMs = 0;
122 return;
123 }
124 }
125 if (!ignoreDisconnectFailure) {
126 return;
127 }
128 }
129 }
130
131 int nightSleepSec = -1;
132 if (!isWithinOpenHours()) {
133 // Notify sensor layer we are entering full night sleep so sensors and
134 // indicator LEDs can be powered down. During daytime naps we keep
135 // interrupt-driven sensors (like PIR) powered so they can wake the
136 // device from ULTRA_LOW_POWER sleep.
137 Log.info("CLOSED-hours deep sleep: disabling sensor (onEnterSleep)");
139 Log.info("CLOSED-hours deep sleep: sensorReady after disable=%s", SensorManager::instance().isSensorReady() ? "true" : "false");
140
141 // ********** Night sleep (outside opening hours) **********
142 nightSleepSec = secondsUntilNextOpen();
143 if (nightSleepSec <= 0) {
144 nightSleepSec = 3600; // Fallback
145 }
146
147 // Device OS maximum sleep duration is 546 minutes (~9.1 hours).
148 // Clamp our requested night sleep to this limit so the
149 // underlying platform will reliably honour it.
150 const int MAX_SLEEP_SEC = 546 * 60;
151 if (nightSleepSec > MAX_SLEEP_SEC) {
152 Log.info("Clamping night sleep duration to max supported %d seconds (requested=%d)", MAX_SLEEP_SEC, nightSleepSec);
153 nightSleepSec = MAX_SLEEP_SEC;
154 }
155
156 // First attempt a true HIBERNATE so platforms that support it
157 // still get a cold boot at next opening time.
159 Log.info("Outside opening hours - entering NIGHT HIBERNATE sleep for %d seconds", nightSleepSec);
160
161 ab1805.stopWDT();
162 // Reset sleep configuration so prior ULTRA_LOW_POWER GPIOs do not
163 // accidentally carry into HIBERNATE configuration.
164 config = SystemSleepConfiguration();
165 config.mode(SystemSleepMode::HIBERNATE)
166 .gpio(BUTTON_PIN, FALLING)
167 .duration((uint32_t)nightSleepSec * 1000UL);
168
169 // HIBERNATE should reset the device on wake, so execution should
170 // not resume here under normal conditions.
171 System.sleep(config);
172
173 // If we reach this point, HIBERNATE did not reset as expected on
174 // this hardware/OS combination. Log once, raise an alert, and
175 // permanently disable HIBERNATE for the remainder of this boot so
176 // we can fall back to ULTRA_LOW_POWER instead of thrashing.
177 ab1805.resumeWDT();
178 Log.error("HIBERNATE sleep returned unexpectedly - disabling HIBERNATE for this session");
179 current.raiseAlert(16); // Alert: unexpected return from HIBERNATE
181 // Fall through to ULTRA_LOW_POWER fallback below.
182 }
183 }
184
185 // ********** ULTRA_LOW_POWER sleep (daytime or night fallback) **********
186 // During opening hours we use the reportingIntervalSec as before.
187 // Outside opening hours, if HIBERNATE is disabled or unsupported,
188 // fall back to ULTRA_LOW_POWER with a long sleep equal to the
189 // time until next open to avoid rapid wake/sleep thrashing.
190 uint16_t intervalSec = sysStatus.get_reportingInterval();
191 if (intervalSec == 0) {
192 intervalSec = 1 * 3600; // Preserve 1 hour default if unset
193 }
194
195 int wakeInSeconds;
196 if (!isWithinOpenHours() && nightSleepSec > 0) {
197 wakeInSeconds = nightSleepSec;
198 Log.info("Outside opening hours - using ULTRA_LOW_POWER fallback sleep for %d seconds", wakeInSeconds);
199 } else {
200 // Within opening hours, align wake to the reporting boundary.
201 // Add 1 second margin to ensure we wake slightly after the boundary.
202 if (Time.isValid() && wakeBoundary > 0) {
203 int boundary = wakeBoundary;
204 time_t now = Time.now();
205 int offset = (int)(now % boundary);
206 int aligned = boundary - offset;
207 if (aligned < 1) {
208 aligned = 1;
209 } else if (aligned > boundary) {
210 aligned = boundary;
211 }
212 wakeInSeconds = aligned + 1;
213 Log.info("Sleep alignment: now=%lu boundary=%d offset=%d aligned=%d (+1 for margin)",
214 (unsigned long)now, boundary, offset, aligned);
215 } else {
216 wakeInSeconds = (int)intervalSec;
217 }
218 }
219
220 // If a sensor event is pending or the BLUE LED timer is still
221 // active from a recent count, defer entering deep sleep so we
222 // don't cut off in-progress events or visible indications.
223 if (sensorDetect || countSignalTimer.isActive()) {
224 Log.info("Deferring sleep - sensor event or LED timer active");
226 return;
227 }
228
229 if (digitalRead(BLUE_LED) == HIGH) {
230 digitalWrite(BLUE_LED, LOW);
231 }
232
233 // Reset sleep configuration on each sleep so GPIO selections do not
234 // accumulate across calls.
235 config = SystemSleepConfiguration();
236
237 // ********** WORKING SLEEP CONFIGURATION **********
238 // Based on Connected-Counter-Next which uses system timer + GPIO wake.
239 // NO AB1805 alarms - AB1805 is only used for watchdog + RTC time sync.
240 // This approach works reliably on Photon2!
241
242 Log.info("Entering ULTRA_LOW_POWER sleep for %d seconds (wakes at boundary or on GPIO)", wakeInSeconds);
243
244 ab1805.stopWDT();
245
246 config.mode(SystemSleepMode::ULTRA_LOW_POWER)
247 .gpio(BUTTON_PIN, CHANGE) // Service button wake
248 .gpio(intPin, RISING) // PIR sensor wake (active HIGH on detect)
249 .duration(wakeInSeconds * 1000L); // Timer-based wake at reporting boundary
250
251 SystemSleepResult result = System.sleep(config);
252
253#ifdef DEBUG_SERIAL
254 waitFor(Serial.isConnected, 30000); // Wait for Serial after wake so logs are visible
255#endif
256
257 ab1805.resumeWDT();
258
259 // Determine wake source
260 // When both GPIO and timer wake are configured, the wake source detection can be
261 // ambiguous. The explicit approach: if neither GPIO pin woke us, it's the timer.
262 pin_t wakePin = result.wakeupPin();
263 bool pirWake = (wakePin == intPin);
264 bool buttonWake = (wakePin == BUTTON_PIN);
265 bool timerWake = !pirWake && !buttonWake; // If no GPIO woke us, it's the timer
266
267 // Wake diagnostics - include raw wakeupReason() for debugging
268 SystemSleepWakeupReason reason = result.wakeupReason();
269 Log.info("Woke from ULTRA_LOW_POWER: wakeupReason=%d pin=%d (pir=%d button=%d timer=%d)",
270 (int)reason, (int)wakePin, pirWake, buttonWake, timerWake);
271
272 if (pirWake) {
273 digitalWrite(BLUE_LED, HIGH); // Immediate visual feedback for motion
274 }
275
276 // Diagnostic: confirm open/closed decision at wake.
277 if (Time.isValid()) {
278 LocalTimeConvert convWake;
279 convWake.withConfig(LocalTime::instance().getConfig()).withCurrentTime().convert();
280 uint8_t hour = (uint8_t)(convWake.getLocalTimeHMS().toSeconds() / 3600);
281 Log.info("Wake eval: parkHours %02u-%02u localHour=%02u => %s",
282 sysStatus.get_openTime(), sysStatus.get_closeTime(), hour,
283 isWithinOpenHours() ? "OPEN" : "CLOSED");
284 } else {
285 Log.info("Wake eval: Time invalid => treating as OPEN (per policy)");
286 }
287
288 if (buttonWake) {
289 // User button wake: go directly to CONNECTING_STATE.
291 Log.info("WAKE: Button pressed - reason=SERVICE_REQUEST transitioning to CONNECTING_STATE");
292 userSwitchDetected = false;
294 return;
295 } else {
296 // In this state the device was awoken for hourly reporting or PIR
297 // Re-enable sensors only if within opening hours; otherwise they
298 // remain powered down to minimize sleep current.
299 if (isWithinOpenHours()) {
300 // If the sensor stack was never initialized (for example, device booted
301 // while closed and remained asleep), onExitSleep() will be a no-op.
302 // Ensure we (re)initialize the active sensor when entering open hours.
303 Log.info("Wake: OPEN hours - enabling sensor (onExitSleep)");
305 if (!SensorManager::instance().isSensorReady()) {
306 Log.info("Wake: sensorReady=false - initializing from config");
308 }
309 Log.info("Wake: sensorReady=%s", SensorManager::instance().isSensorReady() ? "true" : "false");
310
311 // In CONNECTED operating mode, the device should reconnect at the
312 // start of open hours so it can resume normal connected behavior.
313 if (sysStatus.get_operatingMode() == CONNECTED && !Particle.connected()) {
314 Log.info("WAKE: CONNECTED mode + OPEN hours - reason=MAINTAIN_CONNECTION transitioning to CONNECTING_STATE");
316 return;
317 }
318 } else {
319 Log.info("Woke outside opening hours; keeping sensors powered down");
320 }
321
322 // If this wake was caused by the PIR interrupt, synthesize a single
323 // detection event so that the motion that woke the device is counted
324 // even if the ISR flag did not survive ULTRA_LOW_POWER sleep.
325 if (pirWake) {
326 if (sysStatus.get_countingMode() == COUNTING) {
327 current.set_hourlyCount(current.get_hourlyCount() + 1);
328 current.set_dailyCount(current.get_dailyCount() + 1);
329 current.set_lastCountTime(Time.now());
330 Log.info("Count detected from PIR wake - Hourly: %d, Daily: %d",
331 current.get_hourlyCount(), current.get_dailyCount());
332 } else if (sysStatus.get_countingMode() == OCCUPANCY) {
333 if (!current.get_occupied()) {
334 current.set_occupied(true);
335 current.set_occupancyStartTime(Time.now());
336 Log.info("Space now OCCUPIED from PIR wake at %s", Time.timeStr().c_str());
337 }
338 current.set_lastOccupancyEvent(millis());
339 }
340 }
341
342 // If this wake was from PIR, keep the BLUE LED on using the
343 // same software timer used for normal count events so the
344 // visible behaviour is consistent.
345 if (pirWake) {
346 digitalWrite(BLUE_LED, HIGH);
347 if (countSignalTimer.isActive()) {
348 countSignalTimer.reset();
349 } else {
350 countSignalTimer.start();
351 }
352 }
353
354 // Timer wake = scheduled report. No checks, no gates.
355 // We trust that the system timer woke us at the correct boundary.
356 if (timerWake) {
357 Log.info("WAKE: Timer wake - reason=SCHEDULED_REPORT transitioning to REPORTING_STATE");
359 return;
360 }
361
362 // For PIR wakes, check if reporting is also due (opportunistic reporting)
363 if (pirWake && Time.isValid() && isWithinOpenHours()) {
364 uint16_t intervalSec = sysStatus.get_reportingInterval();
365 if (intervalSec == 0) intervalSec = 3600;
366
367 time_t now = Time.now();
368 time_t lastReport = sysStatus.get_lastReport();
369 if (lastReport > 0 && (now - lastReport) >= intervalSec) {
370 int overdue = (int)(now - lastReport - intervalSec);
371 Log.info("WAKE: PIR + report overdue (%d sec) - transitioning to REPORTING_STATE", overdue);
373 return;
374 }
375 }
376
377 // If PIR woke us in LOW_POWER or DISCONNECTED mode and no report is needed,
378 // return immediately to sleep. This check comes AFTER opportunistic reporting
379 // so overdue reports are not missed.
380 if (pirWake && sysStatus.get_operatingMode() != CONNECTED) {
382 return;
383 }
384
385 Log.info("WAKE: No immediate action needed - transitioning to IDLE_STATE");
387 }
388}
Cloud Configuration Management - Particle Ledger integration for device configuration.
Global compile-time configuration options and enums.
const int wakeBoundary
int secondsUntilNextOpen()
volatile bool userSwitchDetected
volatile bool sensorDetect
SystemSleepConfiguration config
bool hibernateDisabledForSession
LocalTimeConvert conv
bool isWithinOpenHours()
void publishStateTransition()
Persistent Data Storage Structures - EEPROM/Retained Memory Management.
#define sysStatus
@ OCCUPANCY
@ COUNTING
#define current
@ CONNECTED
Singleton wrapper around ISensor implementations.
void ensureSensorEnabled(const char *context)
bool isRadioPoweredOn()
void requestFullDisconnectAndRadioOff()
void handleSleepingState()
SLEEPING_STATE: deep sleep between reporting intervals.
@ SLEEPING_STATE
@ CONNECTING_STATE
@ REPORTING_STATE
@ ERROR_STATE
Definition StateMachine.h:9
@ IDLE_STATE
AB1805 ab1805
Timer countSignalTimer
void onExitSleep()
Notify the sensor that the device has woken from deep sleep.
static SensorManager & instance()
Get the SensorManager singleton instance.
void initializeFromConfig()
Create and initialize the active sensor based on configuration.
void onEnterSleep()
Notify the sensor that the device is entering deep sleep.
const pin_t BLUE_LED
const pin_t BUTTON_PIN
const pin_t intPin
Pinout definitions for the carrier board and sensors.