Generalized-Core-Counter 3.20
Particle-based generalized core counter firmware
Loading...
Searching...
No Matches
State_Idle.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
11// NOTE:
12// This file was split from StateHandlers.cpp as a mechanical refactor.
13// No behavioral changes were made.
14
15void ensureSensorEnabled(const char* context) {
16 if (SensorManager::instance().isSensorReady()) {
17 return;
18 }
19
20 Log.info("%s - enabling sensor", context);
22 if (!SensorManager::instance().isSensorReady()) {
24 }
25 Log.info("%s - sensorReady=%s", context, SensorManager::instance().isSensorReady() ? "true" : "false");
26}
27
28// IDLE_STATE: Awake, monitoring sensor and deciding what to do next
30 if (state != oldState) {
32 }
33
34 // If configuration changes (for example, device-settings ledger updates)
35 // move the park from CLOSED->OPEN while the device is already awake,
36 // ensure the sensor stack is enabled. Previously this only happened on
37 // wake from sleep, which caused "awake but not counting".
38 {
39 static bool enableAttemptedThisOpenWindow = false;
40 bool openNow = isWithinOpenHours();
41
42 if (!openNow) {
43 enableAttemptedThisOpenWindow = false;
44 } else if (!SensorManager::instance().isSensorReady() && !enableAttemptedThisOpenWindow) {
45 enableAttemptedThisOpenWindow = true;
46 ensureSensorEnabled("IDLE: park OPEN but sensorReady=false");
47 }
48 }
49
50 // ********** CONNECTED Mode Park-Hours Policy **********
51 // In CONNECTED operating mode, the device stays awake during park open hours.
52 // When the park is closed, it should disconnect, power down the sensor, and
53 // deep-sleep until the next opening time.
54 if (Time.isValid() && sysStatus.get_operatingMode() == CONNECTED) {
55 if (!isWithinOpenHours()) {
56 Log.info("CONNECTED mode: park CLOSED - transitioning to SLEEPING_STATE for overnight sleep");
58 return;
59 }
60 // Park is open: remain awake in CONNECTED mode.
61
62 }
63
64 // ********** Scheduled Mode Sampling **********
65 // SCHEDULED mode uses time-based sampling (non-interrupt).
66 // Interrupt-driven modes (COUNTING/OCCUPANCY) are handled centrally in main loop().
67 if (sysStatus.get_countingMode() == SCHEDULED) {
68 if (Time.isValid()) {
69 static time_t lastScheduledSample = 0;
70 uint16_t intervalSec = sysStatus.get_reportingInterval();
71 if (intervalSec == 0) {
72 intervalSec = 3600; // Fallback to 1 hour
73 }
74
75 time_t now = Time.now();
76 if (lastScheduledSample == 0 || (now - lastScheduledSample) >= intervalSec) {
77 measure.batteryState();
78 Log.info("Scheduled trigger sample - battery SoC: %4.2f%%", (double)current.get_stateOfCharge());
79 lastScheduledSample = now;
80 }
81 }
82 }
83
84 // ********** First-connection queue drain visibility **********
85 // After the first successful cloud connection, log once when the
86 // publish queue has fully drained so we can confirm that any
87 // pending offline events (for example, from before boot) have
88 // been flushed.
89 if (firstConnectionObserved && !firstConnectionQueueDrainedLogged && Particle.connected()) {
90 if (PublishQueuePosix::instance().getCanSleep() &&
91 PublishQueuePosix::instance().getNumEvents() == 0) {
92 Log.info("First connection queue drained - all pending events flushed");
94 }
95 }
96
97 // ********** Scheduled Reporting **********
98 // Use the configured reportingIntervalSec to determine when to
99 // generate a periodic report, regardless of trigger mode.
100 if (Time.isValid() && isWithinOpenHours()) {
101 uint16_t intervalSec = sysStatus.get_reportingInterval();
102 if (intervalSec == 0) {
103 intervalSec = 3600; // Fallback to 1 hour
104 }
105
106 time_t now = Time.now();
107 time_t lastReport = sysStatus.get_lastReport();
108 if (lastReport == 0 || (now - lastReport) >= intervalSec) {
109 int secondsOverdue = (lastReport == 0) ? 0 : (int)(now - lastReport - intervalSec);
110 if (secondsOverdue > 0) {
111 Log.info("IDLE: Report overdue by %d seconds - transitioning to REPORTING_STATE", secondsOverdue);
112 } else {
113 Log.info("IDLE: Scheduled report interval reached - transitioning to REPORTING_STATE");
114 }
116 return;
117 }
118 }
119
120 // ********** Power Management **********
121 // In LOW_POWER (1) or DISCONNECTED (2) modes, manage connection lifecycle.
122 if (sysStatus.get_operatingMode() != CONNECTED) {
123 // In LOW_POWER or DISCONNECTED modes, enforce maximum connected time.
124 // Use connectAttemptBudgetSec as the max connected duration.
125 if (Particle.connected() && connectedStartMs != 0) {
126 uint16_t budgetSec = sysStatus.get_connectAttemptBudgetSec();
127 if (budgetSec >= 30 && budgetSec <= 900) {
128 unsigned long connectedMs = millis() - connectedStartMs;
129 unsigned long budgetMs = (unsigned long)budgetSec * 1000UL;
130 if (connectedMs > budgetMs) {
131 Log.info("Connection timeout (%lu ms > %lu ms) - returning to sleep",
132 (unsigned long)connectedMs, (unsigned long)budgetMs);
135 return;
136 }
137 }
138 }
139
140 // In CONNECTED mode during open hours, never auto-sleep.
141 if (Time.isValid() && sysStatus.get_operatingMode() == CONNECTED && isWithinOpenHours()) {
142 return;
143 }
144
145 bool updatesPending = System.updatesPending();
146
147 // In low-power mode, once all work for this connection cycle is
148 // complete (no updates pending), we can safely enter SLEEPING_STATE
149 // to turn off the radio and save power. We only require the publish
150 // queue to be fully drained when we are actually connected; when
151 // offline, it's expected to have a non-zero queue and we still want
152 // to sleep, flushing the queue on the next connection.
153 bool canSleepGate = true;
154 if (Particle.connected()) {
155 canSleepGate = PublishQueuePosix::instance().getCanSleep();
156 }
157
158 if (!updatesPending && canSleepGate) {
159 // If a sensor event is still pending or the BLUE LED timer is
160 // active from a recent count, defer transitioning into the
161 // SLEEPING_STATE. This avoids rapid Idle<->Sleeping ping-pong
162 // and the associated extra logging while still honouring the
163 // low-power policy once the indication has finished.
164 if (sensorDetect || countSignalTimer.isActive()) {
165 return;
166 }
167
168 size_t pending = PublishQueuePosix::instance().getNumEvents();
169 if (!Particle.connected() && pending > 0) {
170 Log.info("Low-power idle: offline with %u queued event(s) - sleeping and will flush on next connect",
171 (unsigned)pending);
172 } else {
173 Log.info("Low-power idle: queue drained and no updates pending - entering SLEEPING_STATE");
174 }
176 return; // Go back to sleep when there's no work this hour
177 }
178 }
179}
Cloud Configuration Management - Particle Ledger integration for device configuration.
Global compile-time configuration options and enums.
bool firstConnectionObserved
bool firstConnectionQueueDrainedLogged
volatile bool sensorDetect
unsigned long connectedStartMs
bool isWithinOpenHours()
void publishStateTransition()
Persistent Data Storage Structures - EEPROM/Retained Memory Management.
#define sysStatus
@ SCHEDULED
#define current
@ CONNECTED
Singleton wrapper around ISensor implementations.
#define measure
Convenience macro for accessing the SensorManager singleton.
void ensureSensorEnabled(const char *context)
void handleIdleState()
@ SLEEPING_STATE
@ REPORTING_STATE
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.
Pinout definitions for the carrier board and sensors.