Generalized-Core-Counter 3.20
Particle-based generalized core counter firmware
Loading...
Searching...
No Matches
Cloud.cpp
Go to the documentation of this file.
1
11
12#include "Cloud.h"
13
14// External firmware version string (defined in Version.cpp)
15extern const char* FIRMWARE_VERSION;
16
18
19// [static]
21 if (!_instance) {
22 _instance = new Cloud();
23 }
24 return *_instance;
25}
26
27Cloud::Cloud() : ledgersSynced(false), lastApplySuccess(true) {
28 lastPublishedStatus = "";
29 pendingStatusPublish = false;
30 pendingConfigApply = false;
31}
32
35
37 Log.info("Setting up Cloud configuration management");
38
39 // Create ledgers - default-settings will be Product scope via Console
40 defaultSettingsLedger = Particle.ledger("default-settings");
41 defaultSettingsLedger.onSync(onDefaultSettingsSync);
42
43 // device-settings is Device scope (default for per-device ledgers)
44 deviceSettingsLedger = Particle.ledger("device-settings");
45 deviceSettingsLedger.onSync(onDeviceSettingsSync);
46
47 deviceStatusLedger = Particle.ledger("device-status");
48 deviceDataLedger = Particle.ledger("device-data");
49
50 Log.info("Ledgers configured:");
51 Log.info(" default-settings: Product defaults (Cloud→Device)");
52 Log.info(" device-settings: Device overrides (Cloud→Device)");
53 Log.info(" device-status: Current config (Device→Cloud)");
54 Log.info(" device-data: Sensor readings (Device→Cloud)");
55}
56
57// Static callbacks
58void Cloud::onDefaultSettingsSync(Ledger ledger) {
59 Log.info("default-settings synced from cloud");
60 // Do not merge/apply inside async callbacks; keep expensive work
61 // in the main application thread/state machine.
62 Cloud::instance().ledgersSynced = true;
63 Cloud::instance().pendingConfigApply = true;
64}
65
66void Cloud::onDeviceSettingsSync(Ledger ledger) {
67 Log.info("device-settings synced from cloud");
68 // Do not merge/apply inside async callbacks; keep expensive work
69 // in the main application thread/state machine.
70 Cloud::instance().ledgersSynced = true;
71 Cloud::instance().pendingConfigApply = true;
72}
73
74void Cloud::mergeConfiguration() {
75 // Get data from both ledgers
76 LedgerData defaults = defaultSettingsLedger.get();
77 LedgerData device = deviceSettingsLedger.get();
78
79 // Start with defaults as base
80 mergedConfig = defaults;
81
82 // Manually merge sensor thresholds using a simple, consistent schema.
83 //
84 // Supported keys:
85 // defaults.sensor.threshold1 / threshold2
86 // defaults.sensorThreshold (applies to both thresholds)
87 // device.sensor.threshold1 / threshold2
88 // device.sensorThreshold (applies to both thresholds)
89 {
90 // Start from sensible defaults; these will be overridden by
91 // ledger values from defaults and then device.
92 int threshold1 = 60;
93 int threshold2 = 60;
94 bool haveDefaultSensor = defaults.has("sensor") && defaults.get("sensor").isMap();
95 bool haveDeviceSensor = device.has("sensor") && device.get("sensor").isMap();
96
97 if (haveDefaultSensor) {
98 Variant defaultSensor = defaults.get("sensor");
99 if (defaultSensor.has("threshold1")) {
100 threshold1 = defaultSensor.get("threshold1").toInt();
101 }
102 if (defaultSensor.has("threshold2")) {
103 threshold2 = defaultSensor.get("threshold2").toInt();
104 }
105 }
106
107 // Allow a single generic default threshold that applies
108 // to both channels when more specific keys are not used.
109 if (defaults.has("sensorThreshold")) {
110 int base = defaults.get("sensorThreshold").toInt();
111 threshold1 = base;
112 threshold2 = base;
113 }
114
115 if (haveDeviceSensor) {
116 Variant deviceSensor = device.get("sensor");
117 if (deviceSensor.has("threshold1")) {
118 int override1 = deviceSensor.get("threshold1").toInt();
119 threshold1 = override1;
120 }
121 if (deviceSensor.has("threshold2")) {
122 int override2 = deviceSensor.get("threshold2").toInt();
123 threshold2 = override2;
124 }
125 }
126 if (device.has("sensorThreshold")) {
127 int override = device.get("sensorThreshold").toInt();
128 threshold1 = override;
129 threshold2 = override;
130 }
131
132 // Build a minimal merged sensor object with only the supported keys
133 VariantMap mergedSensor;
134 mergedSensor["threshold1"] = Variant(threshold1);
135 mergedSensor["threshold2"] = Variant(threshold2);
136
137 mergedConfig.set("sensor", Variant(mergedSensor));
138 }
139
140 // Apply other top-level device overrides (these aren't nested objects)
141 if (device.has("timing")) mergedConfig.set("timing", device.get("timing"));
142 if (device.has("power")) mergedConfig.set("power", device.get("power"));
143 if (device.has("messaging")) mergedConfig.set("messaging", device.get("messaging"));
144 if (device.has("modes")) mergedConfig.set("modes", device.get("modes"));
145
146 lastApplySuccess = applyConfigurationFromLedger();
147
148 if (!lastApplySuccess) {
149 Log.warn("Configuration apply failed");
150 }
151}
152
154 Log.info("Syncing configuration from cloud");
155
156 // Trigger merge and apply configuration. mergeConfiguration() will update
157 // lastApplySuccess based on the result of applyConfigurationFromLedger().
158 mergeConfiguration();
159 return lastApplySuccess;
160}
161
162bool Cloud::applyConfigurationFromLedger() {
163 bool success = true;
164
165 success &= applySensorConfig();
166 success &= applyTimingConfig();
167 success &= applyPowerConfig();
168 success &= applyMessagingConfig();
169 success &= applyModesConfig();
170
171 if (success) {
172 // Do not force synchronous storage flushes here; they can exceed the
173 // 100 ms loop budget. Persistence is handled by sysStatus.loop() and
174 // sensorConfig.loop() (called from the main loop).
175 sysStatus.validate(sizeof(sysStatus));
176 sensorConfig.validate(sizeof(sensorConfig));
177
178 // Defer device-status publishing to Cloud::loop() so it doesn't
179 // execute inside CONNECTING_STATE or async callbacks.
180 pendingStatusPublish = true;
181 } else {
182 Log.warn("Some configuration sections failed to apply");
183 }
184
185 return success;
186}
187
189 // Apply any newly-synced configuration outside callback context.
190 // Do at most one deferred operation per loop() pass.
191 if (pendingConfigApply && Particle.connected()) {
192 pendingConfigApply = false;
193 mergeConfiguration();
194 return;
195 }
196
197 // Publish device-status updates opportunistically when connected.
198 // Do at most one deferred operation per loop() pass.
199 if (pendingStatusPublish && Particle.connected()) {
201 pendingStatusPublish = false;
202 }
203 }
204}
205
206bool Cloud::applyMessagingConfig() {
207 if (!mergedConfig.has("messaging")) return true;
208 Variant messaging = mergedConfig.get("messaging");
209
210 if (!messaging.isMap()) return true;
211
212 bool success = true;
213 bool changed = false;
214
215 if (messaging.has("serial")) {
216 bool serialEnabled = messaging.get("serial").toBool();
217 if (sysStatus.get_serialConnected() != serialEnabled) {
218 sysStatus.set_serialConnected(serialEnabled);
219 Log.info("Config: Serial → %s", serialEnabled ? "ON" : "OFF");
220 changed = true;
221 }
222 }
223
224 if (messaging.has("verboseMode")) {
225 bool verboseMode = messaging.get("verboseMode").toBool();
226 if (sysStatus.get_verboseMode() != verboseMode) {
227 sysStatus.set_verboseMode(verboseMode);
228 Log.info("Config: Verbose → %s", verboseMode ? "ON" : "OFF");
229 changed = true;
230 }
231 }
232
233 if (changed) Log.info("Messaging config updated");
234 return success;
235}
236
237bool Cloud::applyTimingConfig() {
238 if (!mergedConfig.has("timing")) return true;
239 Variant timing = mergedConfig.get("timing");
240
241 if (!timing.isMap()) return true;
242
243 bool success = true;
244 bool changed = false;
245
246 if (timing.has("timezone")) {
247 String timezone = timing.get("timezone").toString();
248 if (timezone.length() > 0 && timezone.length() < 39) {
249 if (strcmp(sysStatus.get_timeZoneStr(), timezone.c_str()) != 0) {
250 sysStatus.set_timeZoneStr(timezone.c_str());
251 Log.info("Config: Timezone → %s", timezone.c_str());
252 changed = true;
253 }
254 } else {
255 Log.warn("Invalid timezone length: %d", timezone.length());
256 success = false;
257 }
258 }
259
260 if (timing.has("reportingIntervalSec")) {
261 int reportingInterval = timing.get("reportingIntervalSec").toInt();
262 if (validateRange(reportingInterval, 300, 86400, "timing.reportingIntervalSec")) {
263 if (sysStatus.get_reportingInterval() != reportingInterval) {
264 sysStatus.set_reportingInterval(reportingInterval);
265 Log.info("Config: Reporting interval → %ds", reportingInterval);
266 changed = true;
267 }
268 } else {
269 success = false;
270 }
271 }
272
273 if (timing.has("pollingRateSec")) {
274 int pollingRate = timing.get("pollingRateSec").toInt();
275 if (validateRange(pollingRate, 0, 3600, "timing.pollingRateSec")) {
276 if (sensorConfig.get_pollingRate() != pollingRate) {
277 sensorConfig.set_pollingRate(pollingRate);
278 Log.info("Config: Polling rate → %ds", pollingRate);
279 changed = true;
280 }
281 } else {
282 success = false;
283 }
284 }
285
286 if (timing.has("openHour")) {
287 int openHour = timing.get("openHour").toInt();
288 if (validateRange(openHour, 0, 23, "timing.openHour")) {
289 if (sysStatus.get_openTime() != openHour) {
290 sysStatus.set_openTime(openHour);
291 Log.info("Config: Open hour → %d", openHour);
292 changed = true;
293 }
294 } else {
295 success = false;
296 }
297 }
298
299 if (timing.has("closeHour")) {
300 int closeHour = timing.get("closeHour").toInt();
301 if (validateRange(closeHour, 0, 23, "timing.closeHour")) {
302 if (sysStatus.get_closeTime() != closeHour) {
303 sysStatus.set_closeTime(closeHour);
304 Log.info("Config: Close hour → %d", closeHour);
305 changed = true;
306 }
307 } else {
308 success = false;
309 }
310 }
311
312 if (changed) Log.info("Timing config updated");
313 return success;
314}
315
316bool Cloud::applyPowerConfig() {
317 if (!mergedConfig.has("power")) return true;
318 Variant power = mergedConfig.get("power");
319
320 if (!power.isMap()) return true;
321
322 bool success = true;
323 bool changed = false;
324
325 if (power.has("solarPowerMode")) {
326 bool solarPowerMode = power.get("solarPowerMode").toBool();
327 if (sysStatus.get_solarPowerMode() != solarPowerMode) {
328 sysStatus.set_solarPowerMode(solarPowerMode);
329 Log.info("Config: Solar power → %s", solarPowerMode ? "ON" : "OFF");
330 changed = true;
331 }
332 }
333
334 if (changed) Log.info("Power config updated");
335 return success;
336}
337
338// Fix the validateRange function - add template declaration
339template<typename T>
340bool Cloud::validateRange(T value, T min, T max, const String& name) {
341 if (value < min || value > max) {
342 Log.warn("Invalid %s value: %d (must be between %d and %d)", name.c_str(), (int)value, (int)min, (int)max);
343 return false;
344 }
345 return true;
346}
347
348bool Cloud::applySensorConfig() {
349 if (!mergedConfig.has("sensor")) return true;
350
351 Variant sensor = mergedConfig.get("sensor");
352 if (!sensor.isMap()) return true;
353
354 bool success = true;
355 bool changed = false;
356
357 // threshold1
358 if (sensor.has("threshold1")) {
359 int threshold1 = sensor.get("threshold1").toInt();
360 if (validateRange(threshold1, 0, 100, "sensor.threshold1")) {
361 if (sensorConfig.get_threshold1() != threshold1) {
362 sensorConfig.set_threshold1(threshold1);
363 Log.info("Config: Threshold1 → %d", threshold1);
364 changed = true;
365 }
366 } else {
367 success = false;
368 }
369 }
370
371 // threshold2
372 if (sensor.has("threshold2")) {
373 int threshold2 = sensor.get("threshold2").toInt();
374 if (validateRange(threshold2, 0, 100, "sensor.threshold2")) {
375 if (sensorConfig.get_threshold2() != threshold2) {
376 sensorConfig.set_threshold2(threshold2);
377 Log.info("Config: Threshold2 → %d", threshold2);
378 changed = true;
379 }
380 } else {
381 success = false;
382 }
383 }
384
385 if (changed) Log.info("Sensor config updated");
386 return success;
387}
388
389bool Cloud::applyModesConfig() {
390 if (!mergedConfig.has("modes")) return true;
391 Variant modes = mergedConfig.get("modes");
392
393 if (!modes.isMap()) {
394 return true;
395 }
396
397 bool success = true;
398 bool changed = false;
399
400 // Counting mode: 0=COUNTING, 1=OCCUPANCY, 2=SCHEDULED (time-based)
401 if (modes.has("countingMode")) {
402 int countingMode = modes.get("countingMode").asInt();
403 if (validateRange(countingMode, 0, 2, "countingMode")) {
404 if (sysStatus.get_countingMode() != static_cast<CountingMode>(countingMode)) {
405 sysStatus.set_countingMode(static_cast<CountingMode>(countingMode));
406 const char *modeStr = countingMode == COUNTING ? "COUNTING" :
407 countingMode == OCCUPANCY ? "OCCUPANCY" : "SCHEDULED";
408 Log.info("Config: Counting mode → %s", modeStr);
409 changed = true;
410 }
411 } else {
412 success = false;
413 }
414 }
415
416 // Operating mode: 0=CONNECTED, 1=LOW_POWER, 2=DISCONNECTED
417 if (modes.has("operatingMode")) {
418 int operatingMode = modes.get("operatingMode").asInt();
419 if (validateRange(operatingMode, 0, 2, "operatingMode")) {
420 if (sysStatus.get_operatingMode() != static_cast<OperatingMode>(operatingMode)) {
421 sysStatus.set_operatingMode(static_cast<OperatingMode>(operatingMode));
422 const char *modeStr = operatingMode == 0 ? "CONNECTED" :
423 operatingMode == 1 ? "LOW_POWER" : "DISCONNECTED";
424 Log.info("Config: Operating mode → %s", modeStr);
425 changed = true;
426 }
427 } else {
428 success = false;
429 }
430 }
431
432 // Occupancy debounce time (milliseconds)
433 if (modes.has("occupancyDebounceMs")) {
434 unsigned long occupancyDebounceMs = modes.get("occupancyDebounceMs").asUInt();
435 if (validateRange(occupancyDebounceMs, 0UL, 600000UL, "occupancyDebounceMs")) {
436 if (sysStatus.get_occupancyDebounceMs() != occupancyDebounceMs) {
437 sysStatus.set_occupancyDebounceMs(occupancyDebounceMs);
438 Log.info("Config: Occupancy debounce → %lu ms", occupancyDebounceMs);
439 changed = true;
440 }
441 } else {
442 success = false;
443 }
444 }
445
446 // Connected mode reporting interval (seconds)
447 if (modes.has("connectedReportingIntervalSec")) {
448 int connectedReportingIntervalSec = modes.get("connectedReportingIntervalSec").asInt();
449 if (validateRange(connectedReportingIntervalSec, 60, 86400, "connectedReportingIntervalSec")) {
450 if (sysStatus.get_connectedReportingIntervalSec() != connectedReportingIntervalSec) {
451 sysStatus.set_connectedReportingIntervalSec(connectedReportingIntervalSec);
452 Log.info("Config: Connected reporting interval → %ds", connectedReportingIntervalSec);
453 changed = true;
454 }
455 } else {
456 success = false;
457 }
458 }
459
460 // Low power mode reporting interval (seconds)
461 if (modes.has("lowPowerReportingIntervalSec")) {
462 int lowPowerReportingIntervalSec = modes.get("lowPowerReportingIntervalSec").asInt();
463 if (validateRange(lowPowerReportingIntervalSec, 300, 86400, "lowPowerReportingIntervalSec")) {
464 if (sysStatus.get_lowPowerReportingIntervalSec() != lowPowerReportingIntervalSec) {
465 sysStatus.set_lowPowerReportingIntervalSec(lowPowerReportingIntervalSec);
466 Log.info("Config: Low power reporting interval → %ds", lowPowerReportingIntervalSec);
467 changed = true;
468 }
469 } else {
470 success = false;
471 }
472 }
473
474 // Maximum connection-attempt budget per wake (seconds)
475 if (modes.has("connectAttemptBudgetSec")) {
476 int connectAttemptBudgetSec = modes.get("connectAttemptBudgetSec").asInt();
477 if (validateRange(connectAttemptBudgetSec, 30, 900, "connectAttemptBudgetSec")) {
478 if (sysStatus.get_connectAttemptBudgetSec() != connectAttemptBudgetSec) {
479 sysStatus.set_connectAttemptBudgetSec((uint16_t)connectAttemptBudgetSec);
480 Log.info("Config: Connect budget → %ds", connectAttemptBudgetSec);
481 changed = true;
482 }
483 } else {
484 success = false;
485 }
486 }
487
488 // Maximum time to wait for cloud disconnect before treating as an error (seconds)
489 if (modes.has("cloudDisconnectBudgetSec")) {
490 int cloudDisconnectBudgetSec = modes.get("cloudDisconnectBudgetSec").asInt();
491 if (validateRange(cloudDisconnectBudgetSec, 5, 120, "cloudDisconnectBudgetSec")) {
492 if (sysStatus.get_cloudDisconnectBudgetSec() != cloudDisconnectBudgetSec) {
493 sysStatus.set_cloudDisconnectBudgetSec((uint16_t)cloudDisconnectBudgetSec);
494 Log.info("Config: Disconnect budget → %ds", cloudDisconnectBudgetSec);
495 changed = true;
496 }
497 } else {
498 success = false;
499 }
500 }
501
502 // Maximum time to wait for modem power-down before treating as an error (seconds)
503 if (modes.has("modemOffBudgetSec")) {
504 int modemOffBudgetSec = modes.get("modemOffBudgetSec").asInt();
505 if (validateRange(modemOffBudgetSec, 5, 120, "modemOffBudgetSec")) {
506 if (sysStatus.get_modemOffBudgetSec() != modemOffBudgetSec) {
507 sysStatus.set_modemOffBudgetSec((uint16_t)modemOffBudgetSec);
508 Log.info("Config: Modem off budget → %ds", modemOffBudgetSec);
509 changed = true;
510 }
511 } else {
512 success = false;
513 }
514 }
515
516 if (changed) Log.info("Modes config updated");
517 return success;
518}
519
521 // Build current configuration as JSON
522 char buffer[512];
523 JSONBufferWriter writer(buffer, sizeof(buffer));
524
525 writer.beginObject();
526
527 // Firmware version
528 writer.name("firmwareVersion").value(FIRMWARE_VERSION);
529
530 // Sensor
531 writer.name("sensor").beginObject();
532 writer.name("threshold1").value(sensorConfig.get_threshold1());
533 writer.name("threshold2").value(sensorConfig.get_threshold2());
534 writer.endObject();
535
536 // Timing
537 writer.name("timing").beginObject();
538 writer.name("timezone").value(sysStatus.get_timeZoneStr());
539 writer.name("reportingIntervalSec").value(sysStatus.get_reportingInterval());
540 writer.name("openHour").value(sysStatus.get_openTime());
541 writer.name("closeHour").value(sysStatus.get_closeTime());
542 writer.endObject();
543
544 // Power
545 writer.name("power").beginObject();
546 writer.name("lowPowerMode").value(sysStatus.get_lowPowerMode());
547 writer.name("solarPowerMode").value(sysStatus.get_solarPowerMode());
548 writer.endObject();
549
550 // Modes
551 writer.name("modes").beginObject();
552 writer.name("countingMode").value((int)sysStatus.get_countingMode());
553 writer.name("operatingMode").value((int)sysStatus.get_operatingMode());
554 writer.name("occupancyDebounceMs").value((int)sysStatus.get_occupancyDebounceMs());
555 writer.name("connectedReportingIntervalSec").value((int)sysStatus.get_connectedReportingIntervalSec());
556 writer.name("lowPowerReportingIntervalSec").value((int)sysStatus.get_lowPowerReportingIntervalSec());
557 writer.name("connectAttemptBudgetSec").value((int)sysStatus.get_connectAttemptBudgetSec());
558
559 writer.endObject();
560 writer.endObject();
561
562 if (!writer.buffer()) {
563 Log.warn("Failed to create status JSON");
564 return false;
565 }
566
567 buffer[writer.dataSize()] = '\0';
568
569 // Only publish if the configuration actually changed
570 String currentStatus = String(buffer);
571 if (lastPublishedStatus == currentStatus) {
572 Log.info("Device status unchanged; skipping device-status ledger update");
573 return true; // Not an error; nothing to do
574 }
575
576 LedgerData data = LedgerData::fromJSON(buffer);
577 int result = deviceStatusLedger.set(data);
578
579 if (result == SYSTEM_ERROR_NONE) {
580 lastPublishedStatus = currentStatus;
581 Log.info("Device status published to cloud");
582 return true;
583 } else {
584 Log.warn("Failed to publish device status: %d", result);
585 return false;
586 }
587}
588
590 Log.info("Publishing sensor data to device-data ledger");
591
592 char buffer[512];
593 JSONBufferWriter writer(buffer, sizeof(buffer));
594
595 writer.beginObject();
596 writer.name("timestamp").value((int)Time.now());
597
598 // Boot/wake diagnostics: included here so it is visible in Console even
599 // when early USB logs are missed after HIBERNATE/cold boot.
600 writer.name("resetReason").value((int)System.resetReason());
601 writer.name("resetReasonData").value((unsigned long)System.resetReasonData());
602
603 uint8_t countingMode = sysStatus.get_countingMode();
604
605 if (countingMode == COUNTING) {
606 writer.name("mode").value("counting");
607 writer.name("hourlyCount").value(current.get_hourlyCount());
608 writer.name("dailyCount").value(current.get_dailyCount());
609 } else if (countingMode == OCCUPANCY) {
610 writer.name("mode").value("occupancy");
611 writer.name("occupied").value(current.get_occupied());
612 writer.name("totalOccupiedSec").value(current.get_totalOccupiedSeconds());
613 } else { // SCHEDULED or any future modes
614 writer.name("mode").value("scheduled");
615 // In scheduled mode we still track counts; include them so
616 // device-data mirrors the webhook payload for analytics.
617 writer.name("hourlyCount").value(current.get_hourlyCount());
618 writer.name("dailyCount").value(current.get_dailyCount());
619 }
620
621 writer.name("battery").value(current.get_stateOfCharge(), 1);
622 writer.name("temp").value(current.get_internalTempC(), 1);
623 writer.endObject();
624
625 if (!writer.buffer()) {
626 Log.warn("Failed to create data JSON");
627 return false;
628 }
629
630 buffer[writer.dataSize()] = '\0';
631
632 LedgerData data = LedgerData::fromJSON(buffer);
633 int result = deviceDataLedger.set(data);
634
635 if (result == SYSTEM_ERROR_NONE) {
636 // Log the key counters and any active alert code so we
637 // can correlate what was actually written to device-data.
638 int mode = sysStatus.get_countingMode();
639 if (mode == COUNTING || mode == SCHEDULED) {
640 Log.info("Sensor data published to cloud - mode=%s hourly=%d daily=%d alert=%d",
641 (mode == COUNTING ? "counting" : "scheduled"),
642 (int)current.get_hourlyCount(),
643 (int)current.get_dailyCount(),
644 (int)current.get_alertCode());
645 } else if (mode == OCCUPANCY) {
646 Log.info("Sensor data published to cloud - mode=occupancy occupied=%d totalSec=%lu alert=%d",
647 (int)current.get_occupied(),
648 (unsigned long)current.get_totalOccupiedSeconds(),
649 (int)current.get_alertCode());
650 } else {
651 Log.info("Sensor data published to cloud - mode=unknown alert=%d", (int)current.get_alertCode());
652 }
653 return true;
654 } else {
655 Log.warn("Failed to publish sensor data: %d", result);
656 return false;
657 }
658}
659
660bool Cloud::hasNonDefaultConfig() {
661 // Check if any current values differ from hardcoded product defaults
662 // This is a simplified check - expand as needed
663 return (sensorConfig.get_threshold1() != 60 ||
664 sensorConfig.get_threshold2() != 60 ||
665 sysStatus.get_openTime() != 6 ||
666 sysStatus.get_closeTime() != 22);
667}
668
669// Explicit template instantiations for validateRange
670template bool Cloud::validateRange<int>(int value, int min, int max, const String& name);
671template bool Cloud::validateRange<unsigned long>(unsigned long value, unsigned long min, unsigned long max, const String& name);
const char * FIRMWARE_VERSION
Definition Version.cpp:6
Cloud Configuration Management - Particle Ledger integration for device configuration.
#define sensorConfig
#define sysStatus
CountingMode
Counting mode defines how the device processes sensor events.
@ SCHEDULED
@ OCCUPANCY
@ COUNTING
#define current
OperatingMode
Operating mode defines power and connectivity behavior.
This class is a singleton; you do not create one as a global, on the stack, or with new.
Definition Cloud.h:48
bool writeDeviceStatusToCloud()
Write current device configuration to device-status ledger.
Definition Cloud.cpp:520
void setup()
Perform setup operations; call this from global application setup().
Definition Cloud.cpp:36
bool loadConfigurationFromCloud()
Load and apply configuration from cloud ledgers.
Definition Cloud.cpp:153
bool publishDataToLedger()
Publish latest sensor data to device-data ledger.
Definition Cloud.cpp:589
static Cloud * _instance
Singleton instance of this class.
Definition Cloud.h:257
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
virtual ~Cloud()
The destructor is protected because the class is a singleton and cannot be deleted.
Definition Cloud.cpp:33
Cloud()
The constructor is protected because the class is a singleton.
Definition Cloud.cpp:27