Generalized-Core-Counter 3.20
Particle-based generalized core counter firmware
Loading...
Searching...
No Matches
Generalized-Core-Counter.cpp
Go to the documentation of this file.
1/*
2 * Project: Generalized-Core-Counter
3 * Description: Generalized IoT device core for outdoor counting and occupancy tracking.
4 * Supports multiple sensor types (PIR, ultrasonic, gesture detection, etc.) with
5 * flexible operating modes (counting vs occupancy) and power modes (connected vs low-power).
6 * Designed for remote deployment with robust error handling and cloud configuration.
7 *
8 * Author: Charles McClelland
9 *
10 * Date: 12/10/2025
11 * License: MIT
12 * Repo: https://github.com/chipmc/Generalized-Core-Counter
13 */
14
15// Version History (high level):
16// v1.x - Initial gesture sensor implementation and refinements
17// v2.x - Generalized sensor architecture with ISensor interface,
18// counting vs occupancy modes, PIR support, improved error handling
19// v3.0 - Switched to Particle Ledger-based configuration with
20// product defaults and per-device overrides
21
22// Include Particle Device OS APIs
23#include "Particle.h"
24
25// Global configuration (includes DEBUG_SERIAL define)
26#include "Config.h"
27
28// Firmware version recognized by Particle Product firmware management
29// Bump this integer whenever you cut a new production release.
31#include "AB1805_RK.h"
32#include "Cloud.h"
33#include "LocalTimeRK.h"
34#include "MyPersistentData.h"
35#include "Particle_Functions.h"
36#include "PublishQueuePosixRK.h"
37#include "SensorManager.h"
38#include "device_pinout.h"
39#include "ISensor.h"
40#include "SensorFactory.h"
41#include "SensorDefinitions.h"
42#include "Version.h"
43#include "StateMachine.h"
44#include "StateHandlers.h"
45#include "ProjectConfig.h"
46
47// Forward declarations in case Version.h is not picked up correctly
48// by the build system in this translation unit.
49extern const char* FIRMWARE_VERSION;
50extern const char* FIRMWARE_RELEASE_NOTES;
51
52/*
53 * Architectural overview
54 * ----------------------
55 * - State machine: setup()/loop() implement a simple state machine
56 * (INITIALIZATION, CONNECTING, IDLE, SLEEPING, REPORTING, ERROR)
57 * that drives sensing, reporting, and power management.
58 * - Sensor abstraction: ISensor + SensorFactory + SensorManager allow
59 * different physical sensors (PIR, ultrasonic, etc.) behind one API.
60 * - Cloud configuration: the Cloud singleton uses Particle Ledger to
61 * merge product defaults (default-settings) with per-device overrides
62 * (device-settings), then applies the merged config to persistent data.
63 * - Data publishing: publishData() builds a JSON payload and sends it
64 * via PublishQueuePosix (webhook) and also updates the device-data
65 * ledger for Console visibility.
66 * - Connectivity: compile-time macros (Wiring_WiFi / Wiring_Cellular)
67 * select WiFi vs. cellular for radio control; Particle.connect() is
68 * used to bring up the cloud session on both.
69 */
70
71// Forward declarations
72static void appWatchdogHandler(); // Application watchdog handler
73void publishData(); // Publish the data to the cloud
74void userSwitchISR(); // Interrupt for the user switch
75void sensorISR(); // Interrupt for legacy tire-counting sensor
76void countSignalTimerISR(); // Timer ISR to turn off BLUE LED
77void dailyCleanup(); // Reset daily counters and housekeeping
78void UbidotsHandler(const char *event, const char *data); // Webhook response handler
79void publishStartupStatus(); // One-time status summary at boot
80bool publishDiagnosticSafe(const char* eventName, const char* data, PublishFlags flags = PRIVATE); // Safe diagnostic publish with queue guard
81
82// One-shot software timer to keep BLUE_LED on long enough
83// to be visible for each count or PIR-triggered wake event.
85
86// Sleep configuration
87SystemSleepConfiguration config; // Sleep 2.0 configuration
88void outOfMemoryHandler(system_event_t event, int param);
89LocalTimeConvert conv; // For converting UTC time to local time
90AB1805 ab1805(Wire); // AB1805 RTC / Watchdog
91
92// System Health Variables
93int outOfMemory = -1; // Set by outOfMemoryHandler when heap is exhausted
94
95// ********** State Machine **********
96char stateNames[7][16] = {"Initialize", "Error", "Idle",
97 "Sleeping", "Connecting", "Reporting",
98 "FirmwareUpdate"};
101
102// ********** Global Flags **********
103volatile bool userSwitchDetected = false;
104volatile bool sensorDetect = false; // Flag for sensor interrupt
106 false; // Flag for whether we are waiting for a response from the webhook
107
108// Track first-connection queue behaviour for observability
111
113
114// Track when we connected to enforce max connected time in LOW_POWER/DISCONNECTED modes
115unsigned long connectedStartMs = 0;
116
117// Suppress alert 40 (webhook timeout) after waking from overnight closed-hours hibernate
119
120// ********** Timing **********
121const int wakeBoundary = 1 * 3600; // Reporting boundary (1 hour)
122const unsigned long resetWait = 30000; // Error state dwell before reset
123const unsigned long maxConnectAttemptMs = 5UL * 60UL * 1000UL; // Max time to spend trying to connect per wake
124
125void setup() {
126 // Wait for serial connection when DEBUG_SERIAL is enabled
127#ifdef DEBUG_SERIAL
128 waitFor(Serial.isConnected, 10000);
129 delay(1000);
130#endif
131
132 Log.info("===== Firmware Version %s =====", FIRMWARE_VERSION);
133 Log.info("===== Release Notes: %s =====", FIRMWARE_RELEASE_NOTES);
134
135 System.on(out_of_memory,
136 outOfMemoryHandler); // Enabling an out of memory handler is a good
137 // safety tip. If we run out of memory a
138 // System.reset() is done.
139
140 // Application watchdog: reset if loop() doesn't execute within 60 seconds.
141 // This catches state machine hangs, blocking operations, and cellular/cloud
142 // stalls that exceed our non-blocking design intent. The AB1805 hardware
143 // watchdog (124s) provides ultimate backstop if this software watchdog fails.
144 static ApplicationWatchdog appWatchdog(60000, appWatchdogHandler, 1536);
145 Log.info("Application watchdog enabled: 60s timeout");
146
147 // Subscribe to the Ubidots integration response event so we can track
148 // successful webhook deliveries and update lastHookResponse.
149 {
150 char responseTopic[125];
151 String deviceID = System.deviceID();
152 deviceID.toCharArray(responseTopic, sizeof(responseTopic));
153 Particle.subscribe(responseTopic, UbidotsHandler);
154 }
155
156 // Configure network stack but keep radio OFF at startup.
157 // In SEMI_AUTOMATIC mode we explicitly control when the radio is turned on
158 // by calling Particle.connect() from CONNECTING_STATE.
159#if Wiring_WiFi
160 Log.info("Platform connectivity: WiFi (radio off until CONNECTING_STATE)");
161 WiFi.disconnect();
162 WiFi.off();
163#elif Wiring_Cellular
164 Log.info("Platform connectivity: Cellular (radio off until CONNECTING_STATE)");
165 Cellular.disconnect();
166 Cellular.off();
167#else
168 Log.info("Platform connectivity: default (Particle.connect only)");
169 // Fallback: rely on Particle.connect() in CONNECTING_STATE
170#endif
171
172 Particle_Functions::instance().setup(); // Initialize the Particle functions
173
174 initializePinModes(); // Initialize the pin modes
175
176 sysStatus.setup(); // Initialize persistent storage
177 sensorConfig.setup(); // Initialize the sensor configuration
178 current.setup(); // Initialize the current status data
179
180 // Testing: clear sticky sleep-failure alert to avoid reset/deep-power loops.
181 if (current.get_alertCode() == 16) {
182 Log.info("Clearing alert 16 on boot");
183 current.set_alertCode(0);
184 current.set_lastAlertTime(0);
185 }
186
187 // Track how often the device has been resetting so the error supervisor
188 // can apply backoffs and avoid permanent reset loops. Only count resets
189 // that are likely to be recoverable by firmware (pin/user/watchdog).
190 switch (System.resetReason()) {
191 case RESET_REASON_PIN_RESET:
192 case RESET_REASON_USER:
193 case RESET_REASON_WATCHDOG:
194 sysStatus.set_resetCount(sysStatus.get_resetCount() + 1);
195 break;
196 case RESET_REASON_UPDATE:
197 // After OTA firmware update, force connection to reload
198 // configuration from ledger. This ensures device-settings
199 // (operatingMode, etc.) override any stale FRAM values.
200 Log.info("OTA update detected - forcing connection to reload config");
202 break;
203 case RESET_REASON_POWER_MANAGEMENT:
204 // Waking from sleep. If current local hour matches opening hour (e.g., 07:00),
205 // we likely just woke from overnight closed-hours hibernate.
206 // Suppress alert 40 this session since 8+ hours without webhook responses
207 // during closed hours is expected, not an error.
208 // Error escalation/hard resets during other hours (08:00-22:00) will still
209 // correctly trigger alert 40 if there's a real connectivity issue.
210 // Note: Local time calculation happens after timezone setup, so we check this
211 // later in setup() rather than here where timezone isn't configured yet.
212 break;
213 default:
214 break;
215 }
216
217 // Ensure sensor-board LED power default matches configured sensor type
218 pinMode(ledPower, OUTPUT);
219 SensorType configuredType = static_cast<SensorType>(sysStatus.get_sensorType());
220 const SensorDefinition* sensorDef = SensorDefinitions::getDefinition(configuredType);
221 if (sensorDef && sensorDef->ledDefaultOn) {
222 digitalWrite(ledPower, HIGH);
223 } else {
224 digitalWrite(ledPower, LOW);
225 }
226
227 // Configure publish queue to retain ~30+ days of hourly reports
228 // across all supported platforms (P2, Boron, Argon). With an
229 // hourly reporting interval, 800 file-backed events provide
230 // headroom over the 720 events needed for a full 30 days.
231 PublishQueuePosix::instance()
232 .withFileQueueSize(800)
233 .setup(); // Initialize the publish queue
234
235 // Initialize AB1805 RTC and watchdog
236 const bool timeValidBeforeRtc = Time.isValid();
237 ab1805.withFOUT(WKP).setup(); // Initialize AB1805 RTC - WKP is D10 on Photon2
238 ab1805.setWDT(AB1805::WATCHDOG_MAX_SECONDS); // Enable watchdog
239
240 time_t rtcTime = 0;
241 const bool rtcReadOk = ab1805.getRtcAsTime(rtcTime);
242 const bool timeValidAfterRtc = Time.isValid();
243 if (!timeValidBeforeRtc && timeValidAfterRtc) {
244 if (rtcReadOk) {
245 Log.info("RTC restored system time: %s (rtc=%s)",
246 Time.timeStr().c_str(),
247 Time.format(rtcTime, TIME_FORMAT_DEFAULT).c_str());
248 } else {
249 Log.info("RTC restored system time: %s (rtc read failed)",
250 Time.timeStr().c_str());
251 }
252 } else if (!timeValidAfterRtc) {
253 Log.warn("RTC did not restore time (rtcSet=%s rtcReadOk=%s)",
254 ab1805.isRTCSet() ? "true" : "false",
255 rtcReadOk ? "true" : "false");
256 }
257
258 Cloud::instance().setup(); // Initialize the cloud functions
259
260 // Enqueue a one-time status snapshot so the cloud can see
261 // firmware version, reset reason, and any outstanding alert
262 // soon after the first successful connection.
264
265 // ===== TIME AND TIMEZONE CONFIGURATION =====
266 // Setup local time from persisted timezone string (POSIX TZ format).
267 // This must be configured before we can make any open/close hour decisions.
268 String tz = sysStatus.get_timeZoneStr();
269 if (tz.length() == 0) {
270 tz = "SGT-8"; // Fallback default
271 sysStatus.set_timeZoneStr(tz.c_str());
272 }
273 LocalTime::instance().withConfig(LocalTimePosixTimezone(tz.c_str()));
274
275 // Validate time and configure local time converter
276 if (!Time.isValid()) {
277 Log.info("Time is invalid - %s so connecting", Time.timeStr().c_str());
279 } else {
280 Log.info("Time is valid - %s", Time.timeStr().c_str());
281
282 // Now that time is valid, configure local time converter for timezone-aware operations
283 conv.withCurrentTime().convert();
284 Log.info("Timezone: %s, Local time: %s", tz.c_str(), conv.format(TIME_FORMAT_DEFAULT).c_str());
285 Log.info("Open hours %02u:00-%02u:00, currently: %s",
286 sysStatus.get_openTime(), sysStatus.get_closeTime(),
287 isWithinOpenHours() ? "OPEN" : "CLOSED");
288
289 // Check if waking from overnight hibernate - suppress alert 40 since
290 // 8+ hours without webhook during closed hours is expected, not an error.
291 if (System.resetReason() == RESET_REASON_POWER_MANAGEMENT) {
292 uint8_t localHour = (uint8_t)(conv.getLocalTimeHMS().toSeconds() / 3600);
293 if (localHour == sysStatus.get_openTime()) {
294 Log.info("Wake from overnight hibernate at opening hour - suppressing alert 40");
296 }
297 }
298
299 // In CONNECTED operating mode, always connect on boot to reload
300 // configuration from ledger and prevent stuck-in-IDLE power drain.
301 if (sysStatus.get_operatingMode() == CONNECTED) {
302 Log.info("CONNECTED mode - connecting on boot to reload config");
304 }
305 }
306
307 Log.info("Sensor ready at startup: %s", SensorManager::instance().isSensorReady() ? "true" : "false");
308
309 // ===== SENSOR ABSTRACTION LAYER =====
310 // Initialize the sensor based on configuration using *local* time. This
311 // runs after timezone configuration so open/close checks are correct.
312 Log.info("Initial operatingMode: %d (%s)", sysStatus.get_operatingMode(),
313 sysStatus.get_operatingMode() == 0 ? "CONNECTED" :
314 sysStatus.get_operatingMode() == 1 ? "LOW_POWER" : "DISCONNECTED");
315
316 if (!SensorManager::instance().isSensorReady()) {
317 if (isWithinOpenHours()) {
318 Log.info("Initializing sensor after timezone setup");
320
321 if (!SensorManager::instance().isSensorReady()) {
322 Log.error("Sensor failed to initialize after timezone setup; connecting to report error");
324 }
325 } else {
326 Log.info("Outside opening hours at startup; sensor will remain powered down");
327 // Ensure carrier sensor power rails are actually turned off even if
328 // we skipped sensor initialization while closed.
329 Log.info("Startup CLOSED: forcing sensor power down before sleep");
331 Log.info("Sensor ready after startup power-down: %s", SensorManager::instance().isSensorReady() ? "true" : "false");
332 }
333 }
334 // ===================================
335
336 attachInterrupt(BUTTON_PIN, userSwitchISR,
337 FALLING); // We may need to monitor the user switch to change
338 // behaviours / modes
339
341 state = IDLE_STATE; // Default to IDLE; CONNECTING only when explicitly requested
342 Log.info("Startup complete");
343 digitalWrite(BLUE_LED, LOW); // Signal the end of startup
344}
345
346void loop() {
347 // Main state machine driving sensing, reporting, power management
348 switch (state) {
349 case IDLE_STATE:
351 break;
352
353 case SLEEPING_STATE:
355 break;
356
357 case REPORTING_STATE:
359 break;
360
361 case CONNECTING_STATE:
363 break;
364
367 break;
368
369 case ERROR_STATE:
371 break;
372 }
373
374 ab1805.loop(); // Keeps the RTC synchronized with the device clock
375
376 // Housekeeping for each transit of the main loop
377 current.loop();
378 sysStatus.loop();
379 sensorConfig.loop();
380
381 // Service deferred cloud work (ledger status publishes, etc.)
383
384 // Service outgoing publish queue
385 PublishQueuePosix::instance().loop();
386
387 // If an out-of-memory event occurred, go to error state
388 if (outOfMemory >= 0) {
389 Log.info("Resetting due to low memory");
390 // Out-of-memory is treated as a critical alert; only overwrite any
391 // existing alert if this is more severe.
392 current.raiseAlert(14);
394 }
395
396 // If the user switch is pressed, force a connection to drain queue.
397 if (userSwitchDetected) {
398 Log.info("User switch pressed - connecting to drain queue");
399 userSwitchDetected = false;
401 }
402
403 // ********** Centralized sensor event handling **********
404 // Service sensor interrupts regardless of current state. This ensures
405 // counts are captured even during long-running operations like cellular
406 // connection attempts (which can take minutes) or firmware updates.
407 // SCHEDULED mode is time-based (handled in IDLE only), not interrupt-driven.
408 uint8_t countingMode = sysStatus.get_countingMode();
409 if (countingMode == COUNTING) {
410 handleCountingMode(); // Count each sensor event
411 } else if (countingMode == OCCUPANCY) {
412 handleOccupancyMode(); // Track occupied/unoccupied state
413 }
414
415} // End of loop
416
417// ********** Helper Functions **********
418
419// ApplicationWatchdog expects a plain function pointer.
420static void appWatchdogHandler() {
421 System.reset();
422}
423
424// Helper to determine whether current *local* time is within park open hours.
425// Local time is derived from LocalTimeRK using the configured timezone.
426// If time is not yet valid, we treat it as "open" so the device can start
427// sensing while it acquires time and configuration.
429 if (!Time.isValid()) {
430 return true;
431 }
432
433 uint8_t openHour = sysStatus.get_openTime();
434 uint8_t closeHour = sysStatus.get_closeTime();
435 // Use LocalTimeRK to convert UTC to local hour based on the
436 // configured timezone (see setup()).
437 LocalTimeConvert tempConv;
438 tempConv.withConfig(LocalTime::instance().getConfig()).withCurrentTime().convert();
439 uint8_t hour = (uint8_t)(tempConv.getLocalTimeHMS().toSeconds() / 3600);
440
441 if (openHour < closeHour) {
442 // Simple daytime window, e.g. 6 -> 22
443 return (hour >= openHour) && (hour < closeHour);
444 } else if (openHour > closeHour) {
445 // Overnight window, e.g. 20 -> 6
446 return (hour >= openHour) || (hour < closeHour);
447 } else {
448 // openHour == closeHour: treat as always open
449 return true;
450 }
451}
452
453// Helper to compute seconds until next park opening time (local time)
455 if (!Time.isValid()) {
456 // Fallback: 1 hour if time is not yet valid
457 return 3600;
458 }
459
460 uint8_t openHour = sysStatus.get_openTime();
461 uint8_t closeHour = sysStatus.get_closeTime();
462
463 LocalTimeConvert tempConv;
464 tempConv.withConfig(LocalTime::instance().getConfig()).withCurrentTime().convert();
465 uint32_t secondsOfDay = tempConv.getLocalTimeHMS().toSeconds();
466
467 uint32_t openSec = (uint8_t)openHour * 3600;
468 uint32_t closeSec = (uint8_t)closeHour * 3600;
469
470 // Normalize: if we're currently within opening hours, next open is tomorrow
471 if (isWithinOpenHours()) {
472 return (int)((24 * 3600UL - secondsOfDay) + openSec);
473 }
474
475 if (openHour < closeHour) {
476 // Simple daytime window, closed before open or after close
477 if (secondsOfDay < openSec) {
478 // Before opening today
479 return (int)(openSec - secondsOfDay);
480 } else {
481 // After closing, next open is tomorrow
482 return (int)((24 * 3600UL - secondsOfDay) + openSec);
483 }
484 } else if (openHour > closeHour) {
485 // Overnight window; closed between closeHour and openHour
486 if (secondsOfDay < openSec && secondsOfDay >= closeSec) {
487 // During the closed gap today
488 return (int)(openSec - secondsOfDay);
489 } else {
490 // Otherwise next open is later today or tomorrow, but isWithinOpenHours()
491 // was already false so this path will generally be rare; fall back to 1 hour
492 return 3600;
493 }
494 } else {
495 // openHour == closeHour: always open; should not normally reach here
496 return 3600;
497 }
498}
499
510 // Legacy Ubidots context strings describing battery state
511 static const char *batteryContext[7] = {
512 "Unknown", "Not Charging", "Charging",
513 "Charged", "Discharging", "Fault",
514 "Diconnected"
515 };
516
517 char data[256];
518
519 // Compute the timestamp as the last second of the previous hour so the
520 // webhook data aggregates correctly into hourly buckets in Ubidots.
521 unsigned long timeStampValue = Time.now() - (Time.minute() * 60L + Time.second() + 1L);
522
523 // Bounds check battery state index for safety
524 uint8_t battState = current.get_batteryState();
525 if (battState > 6) {
526 battState = 0;
527 }
528
529 // Correct Ubidots webhook JSON structure
530 snprintf(data, sizeof(data),
531 "{\"hourly\":%i, \"daily\":%i, \"battery\":%4.2f,\"key1\":\"%s\", \"temp\":%4.2f, \"resets\":%i, \"alerts\":%i,\"connecttime\":%i,\"timestamp\":%lu000}",
532 current.get_hourlyCount(),
533 current.get_dailyCount(),
534 current.get_stateOfCharge(),
535 batteryContext[battState],
536 current.get_internalTempC(),
537 sysStatus.get_resetCount(),
538 current.get_alertCode(),
539 sysStatus.get_lastConnectionDuration(),
540 timeStampValue);
541
542 // Explicitly log the counts and alert code used in this report
543 Log.info("Report payload: hourly=%d daily=%d alert=%d",
544 (int)current.get_hourlyCount(),
545 (int)current.get_dailyCount(),
546 (int)current.get_alertCode());
547
548 PublishQueuePosix::instance().publish(ProjectConfig::webhookEventName(), data, PRIVATE | WITH_ACK);
549 Log.info("Ubidots Webhook: %s", data);
550
551 // Also update device-data ledger with structured JSON snapshot
552 if (!Cloud::instance().publishDataToLedger()) {
553 // Data ledger publish failure; escalate via alert so the error
554 // supervisor can decide on corrective action.
555 current.raiseAlert(42);
556 }
557}
558
568 char status[192];
569
570 int resetReason = System.resetReason();
571 uint32_t resetReasonData = System.resetReasonData();
572 int8_t alertCode = current.get_alertCode();
573 time_t lastAlert = current.get_lastAlertTime();
574
575 snprintf(status, sizeof(status),
576 "{\"version\":\"%s\",\"resetReason\":%d,\"resetReasonData\":%lu,\"alert\":%d,\"lastAlert\":%ld}",
578 resetReason,
579 (unsigned long)resetReasonData,
580 (int)alertCode,
581 (long)lastAlert);
582
583 PublishQueuePosix::instance().publish("status", status, PRIVATE | WITH_ACK);
584 Log.info("Startup status: %s", status);
585}
586
587void UbidotsHandler(const char *event, const char *data) {
588 // Handle response from Ubidots webhook (legacy integration)
589 char responseString[64];
590 // Response is only a single number thanks to Template
591 if (!strlen(data)) { // No data in response - Error
592 snprintf(responseString, sizeof(responseString), "No Data");
593 } else if (atoi(data) == 200 || atoi(data) == 201) {
594 snprintf(responseString, sizeof(responseString), "Response Received");
596 false; // We have received a response - so we can send another
597 sysStatus.set_lastHookResponse(
598 Time.now()); // Record the last successful Webhook Response
599
600 // If a webhook supervision alert (40) was active, clear it now that
601 // we have a confirmed successful response, so future reports reflect
602 // the healthy state.
603 if (current.get_alertCode() == 40) {
604 current.set_alertCode(0);
605 current.set_lastAlertTime(0);
606 }
607 } else {
608 snprintf(responseString, sizeof(responseString),
609 "Unknown response recevied %i", atoi(data));
610 }
611 if (sysStatus.get_verboseMode() && Particle.connected()) {
612 publishDiagnosticSafe("Ubidots Hook", responseString, PRIVATE);
613 }
614 Log.info(responseString);
615}
616
633bool publishDiagnosticSafe(const char* eventName, const char* data, PublishFlags flags) {
634 // Guard: only add diagnostics when queue has capacity for them.
635 // Reserve headroom for critical data payloads (hourly reports, alerts).
636 // Threshold: allow diagnostics if queue has <10 events pending.
637 const size_t DIAGNOSTIC_QUEUE_THRESHOLD = 10;
638
639 size_t queueDepth = PublishQueuePosix::instance().getNumEvents();
640
641 if (queueDepth >= DIAGNOSTIC_QUEUE_THRESHOLD) {
642 Log.info("Diagnostic publish skipped (queue depth=%u): %s", (unsigned)queueDepth, eventName);
643 return false;
644 }
645
646 // Queue has capacity; safe to add diagnostic message
647 PublishQueuePosix::instance().publish(eventName, data, flags | WITH_ACK);
648 return true;
649}
650
652 char stateTransitionString[256];
653 if (state == IDLE_STATE) {
654 if (!Time.isValid())
655 snprintf(stateTransitionString, sizeof(stateTransitionString),
656 "From %s to %s with invalid time", stateNames[oldState],
658 else
659 snprintf(stateTransitionString, sizeof(stateTransitionString),
660 "From %s to %s", stateNames[oldState], stateNames[state]);
661 } else
662 snprintf(stateTransitionString, sizeof(stateTransitionString),
663 "From %s to %s", stateNames[oldState], stateNames[state]);
664 oldState = state;
665 Log.info(stateTransitionString);
666}
667
668// ********** Interrupt Service Routines **********
669void outOfMemoryHandler(system_event_t event, int param) {
670 outOfMemory = param;
671}
672
674
675void sensorISR() {
676 static bool frontTireFlag = false;
677 if (frontTireFlag || sysStatus.get_sensorType() == 1) { // Counts the rear tire for pressure sensors and once for PIR (sensor type 1)
678 sensorDetect = true; // sets the sensor flag for the main loop
679 frontTireFlag = false;
680 } else
681 frontTireFlag = true;
682}
683
684void countSignalTimerISR() { digitalWrite(BLUE_LED, LOW); }
685
694 if (Particle.connected()) {
695 publishDiagnosticSafe("Daily Cleanup", "Running", PRIVATE);
696
697 // Force time sync once per day to prevent clock drift
698 Log.info("Daily time sync requested");
699 Particle.syncTime();
700 sysStatus.set_lastTimeSync(Time.now());
701 }
702
703 Log.info("Running Daily Cleanup");
704 // Leave verbose mode enabled for now to aid debugging
705 if (sysStatus.get_solarPowerMode() ||
706 current.get_stateOfCharge() <=
707 65) { // If Solar or if the battery is being discharged
708 // setLowPowerMode("1");
709 }
710 current
711 .resetEverything(); // If so, we need to Zero the counts for the new day
712}
const char * FIRMWARE_VERSION
Definition Version.cpp:6
Cloud Configuration Management - Particle Ledger integration for device configuration.
Global compile-time configuration options and enums.
PRODUCT_VERSION(3)
void userSwitchISR()
void outOfMemoryHandler(system_event_t event, int param)
void publishStartupStatus()
Enqueue a one-time startup status event summarizing firmware version, reset reason,...
const int wakeBoundary
void dailyCleanup()
Cleanup function that is run at the beginning of the day.
int secondsUntilNextOpen()
const unsigned long resetWait
bool suppressAlert40ThisSession
bool firstConnectionObserved
bool publishDiagnosticSafe(const char *eventName, const char *data, PublishFlags flags=PRIVATE)
Publish a state transition to the log handler.
bool firstConnectionQueueDrainedLogged
void publishData()
Publish sensor data to Ubidots webhook and device-data ledger.
const char * FIRMWARE_RELEASE_NOTES
Definition Version.cpp:7
volatile bool userSwitchDetected
volatile bool sensorDetect
unsigned long connectedStartMs
void countSignalTimerISR()
SystemSleepConfiguration config
char stateNames[7][16]
bool hibernateDisabledForSession
LocalTimeConvert conv
void UbidotsHandler(const char *event, const char *data)
bool isWithinOpenHours()
const unsigned long maxConnectAttemptMs
void publishStateTransition()
Persistent Data Storage Structures - EEPROM/Retained Memory Management.
#define sensorConfig
#define sysStatus
@ OCCUPANCY
@ COUNTING
#define current
@ CONNECTED
This file initilizes the Particle functions and variables needed for control from the console / API c...
SensorType
Enumeration of available sensor types (backward-compatible IDs).
const char * batteryContext[7]
Singleton wrapper around ISensor implementations.
void handleConnectingState()
CONNECTING_STATE: establish cloud connection using a phased, non-blocking state machine.
void handleFirmwareUpdateState()
void handleErrorState()
void handleIdleState()
void handleOccupancyMode()
Handle sensor events in OCCUPANCY mode.
void handleCountingMode()
Handle sensor events in COUNTING mode.
void handleReportingState()
void handleSleepingState()
SLEEPING_STATE: deep sleep between reporting intervals.
State
Definition StateMachine.h:7
@ SLEEPING_STATE
@ CONNECTING_STATE
@ FIRMWARE_UPDATE_STATE
@ REPORTING_STATE
@ ERROR_STATE
Definition StateMachine.h:9
@ INITIALIZATION_STATE
Definition StateMachine.h:8
@ IDLE_STATE
AB1805 ab1805
Timer countSignalTimer
Firmware release metadata (version and notes).
void setup()
Perform setup operations; call this from global application setup().
Definition Cloud.cpp:36
static Cloud & instance()
Gets the singleton instance of this class, allocating it if necessary.
Definition Cloud.cpp:20
void loop()
Service deferred cloud work; call from main loop.
Definition Cloud.cpp:188
void setup()
Perform setup operations; call this from global application setup().
static Particle_Functions & instance()
Gets the singleton instance of this class, allocating it if necessary.
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.
bool initializePinModes()
const pin_t BLUE_LED
const pin_t ledPower
const pin_t BUTTON_PIN
Pinout definitions for the carrier board and sensors.
const SensorDefinition * getDefinition(SensorType type)
Lookup helper to get the SensorDefinition for a given SensorType.
Static metadata for each supported sensor type.
bool ledDefaultOn
true if LED should be ON at boot (polarity-specific)