Generalized-Core-Counter 3.20
Particle-based generalized core counter firmware
Loading...
Searching...
No Matches
SensorManager.cpp
Go to the documentation of this file.
1// Battery conect information -
2// https://docs.particle.io/reference/device-os/firmware/boron/#batterystate-
3const char *batteryContext[7] = {"Unknown", "Not Charging", "Charging",
4 "Charged", "Discharging", "Fault",
5 "Diconnected"};
6
7// Particle Functions
8#include "SensorManager.h"
9#include "MyPersistentData.h" // Access sysStatus/sensorConfig
10#include "SensorFactory.h"
11#include "device_pinout.h" // TMP36_SENSE_PIN for enclosure temperature
12
13// Device-specific includes and definitions
14// Use Particle feature detection for automatic platform identification
15
16// FuelGauge fuelGauge; // Needed to address
17// issue with updates in low battery state
18
20
21// [static]
23 if (!_instance) {
25 }
26 return *_instance;
27}
29
31
33 Log.info("Initializing SensorManager");
34
35 if (!_sensor) {
36 Log.error("No sensor assigned! Call setSensor() first.");
37 return;
38 }
39
40 if (!_sensor->setup()) {
41 Log.error("Sensor setup failed");
42 } else {
43 Log.info("Sensor setup completed: %s", _sensor->getSensorType());
44 }
45}
46
48 if (sensor) {
49 _sensor = sensor;
50 Log.info("Sensor set: %s", sensor->getSensorType());
51 } else {
52 Log.error("Attempted to set null sensor");
53 }
54}
55
57 Log.info("Initializing sensor from configuration");
58
59 SensorType sensorType = static_cast<SensorType>(sysStatus.get_sensorType());
60 ISensor* sensor = SensorFactory::createSensor(sensorType);
61
62 if (!sensor) {
63 Log.error("SensorFactory failed for type %d", (int)sensorType);
64 _sensor = nullptr;
65 return;
66 }
67
68 setSensor(sensor);
69
70 if (!_sensor->initializeHardware()) {
71 Log.error("Sensor hardware initialization failed for type %d", (int)sensorType);
72 } else {
73 Log.info("Sensor hardware initialized; type=%d, usesInterrupt=%s", (int)sensorType,
74 _sensor->usesInterrupt() ? "true" : "false");
75 }
76 }
77
79 if (!_sensor || !_sensor->isReady()) {
80 return false;
81 }
82
83 unsigned long currentTime = millis();
84 uint16_t pollingRate = sensorConfig.get_pollingRate() * 1000; // Convert to ms
85
86 // Interrupt-driven sensors should be serviced on every pass through
87 // the main loop regardless of pollingRate.
88 if (_sensor->usesInterrupt() || pollingRate == 0) {
89 bool event = _sensor->loop();
90 if (event && sysStatus.get_verboseMode()) {
91 Log.info("SensorManager: event reported by interrupt-driven sensor");
92 }
93 return event;
94 }
95
96 // Polling mode - check sensor at specified intervals
97 if (currentTime - _lastPollTime >= pollingRate) {
98 _lastPollTime = currentTime;
99 return _sensor->loop();
100 }
101
102 return false;
103}
104
106 if (_sensor) {
107 return _sensor->getData();
108 }
109 return SensorData();
110}
111
113 return _sensor && _sensor->isReady();
114}
115
117 if (_sensor) {
118 Log.info("SensorManager onEnterSleep: notifying sensor %s", _sensor->getSensorType());
119 _sensor->onSleep();
120 return;
121 }
122
123 // If a concrete sensor hasn't been initialized (common when booting while
124 // outside open hours), still force the carrier sensor power rails off.
125 Log.info("SensorManager onEnterSleep: no sensor instance; forcing sensor power rails OFF");
126 pinMode(disableModule, OUTPUT);
127 pinMode(ledPower, OUTPUT);
128 digitalWrite(disableModule, HIGH); // active-low enable
129 digitalWrite(ledPower, HIGH); // active-low LED power
130}
131
133 if (_sensor) {
134 Log.info("SensorManager onExitSleep: waking sensor %s", _sensor->getSensorType());
135 if (!_sensor->onWake()) {
136 Log.error("Sensor %s failed to wake correctly", _sensor->getSensorType());
137 }
138 Log.info("SensorManager onExitSleep: sensorReady=%s", _sensor->isReady() ? "true" : "false");
139 } else {
140 Log.info("SensorManager onExitSleep: no sensor instance (sensorReady=false)");
141 }
142}
143
145 // Analog inputs have values from 0-4095, or
146 // 12-bit precision. 0 = 0V, 4095 = 3.3V, 0.0008 volts (0.8 mV) per unit
147 // The temperature sensor docs use millivolts (mV), so use 3300 as the factor
148 // instead of 3.3.
149 float mV = ((float)adcValue) * 3300 / 4095;
150
151 // According to the TMP36 docs:
152 // Offset voltage 500 mV, scaling 10 mV/deg C, output voltage at 25C = 750 mV
153 // (77F) The offset voltage is subtracted from the actual voltage, allowing
154 // negative temperatures with positive voltages.
155
156 // Example value=969 mV=780.7 tempC=28.06884765625 tempF=82.52392578125
157
158 // With the TMP36, with the flat side facing you, the pins are:
159 // Vcc | Analog Out | Ground
160 // You must put a 0.1 uF capacitor between the analog output and ground or
161 // you'll get crazy inaccurate values!
162 return (mV - 500) / 10;
163}
164
166 // TMP112A default 7-bit I2C address is 0x48.
167 // Allow override at compile time for unusual board strapping.
168#if defined(MUON_TMP112_I2C_ADDR)
169 const uint8_t addr = (uint8_t)MUON_TMP112_I2C_ADDR;
170#else
171 const uint8_t addr = 0x48;
172#endif
173
174 // Temperature register pointer is 0x00.
175 const uint8_t tempReg = 0x00;
176
177 // Guard against interference with other I2C users (AB1805, FRAM, etc.).
178 Wire.lock();
179
180 Wire.beginTransmission(addr);
181 Wire.write(tempReg);
182 int status = Wire.endTransmission(false);
183 if (status != 0) {
184 Wire.unlock();
185 return false;
186 }
187
188 const uint8_t toRead = 2;
189 (void)Wire.requestFrom((int)addr, (int)toRead);
190 if (Wire.available() < toRead) {
191 Wire.unlock();
192 return false;
193 }
194
195 uint8_t msb = (uint8_t)Wire.read();
196 uint8_t lsb = (uint8_t)Wire.read();
197 Wire.unlock();
198
199 // TMP112A temperature is a signed 12-bit value left-justified in 16 bits.
200 // Resolution is 0.0625 C per LSB.
201 int16_t raw = (int16_t)((((uint16_t)msb) << 8) | (uint16_t)lsb);
202 raw = (int16_t)(raw >> 4);
203 // Sign-extend the 12-bit value (bit 11 is the sign after shifting).
204 if (raw & 0x0800) {
205 raw = (int16_t)(raw | 0xF000);
206 }
207
208 tempC = ((float)raw) * 0.0625f;
209 return true;
210}
211
212namespace {
213
214bool probeTmp112Present(uint8_t addr) {
215 // Probe device presence without changing its configuration.
216 Wire.lock();
217 Wire.beginTransmission(addr);
218 int status = Wire.endTransmission();
219 Wire.unlock();
220 return status == 0;
221}
222
223} // namespace
224
226
227#if HAL_PLATFORM_CELLULAR || PLATFORM_ID == PLATFORM_ARGON
228 // Boron (cellular) and Argon (Wi-Fi) Gen 3 devices:
229 // Use built-in System battery APIs backed by the fuel gauge
230 // (and a BQ24195 PMIC on Boron only).
231 uint8_t battState = System.batteryState();
232 float soc = System.batteryCharge();
233 int powerSource = System.powerSource();
234
235 // Log battery diagnostics to help identify charging state issues
236 Log.info("Battery: state=%s (%d), SoC=%.2f%%, powerSource=%d",
237 batteryContext[battState], battState, (double)soc, powerSource);
238
239 current.set_batteryState(battState);
240 current.set_stateOfCharge(soc);
241
242#if HAL_PLATFORM_CELLULAR && (PLATFORM_ID != PLATFORM_MSOM)
243 // =========================================================================
244 // PMIC Health Monitoring & Smart Remediation (BQ24195 PMIC)
245 // =========================================================================
246 // Supported platforms: Boron (Gen 3 cellular with BQ24195 PMIC)
247 // Excluded platforms: M-SoM/Muon (uses Particle Power Module with MAX17043, not BQ24195)
248 //
249 // Detects charging faults (1Hz amber LED = fault register set) and attempts
250 // automatic recovery with escalating remediation levels to prevent thrashing.
251 //
252 // Alert Codes (auto-reported via webhook):
253 // 20 = PMIC Thermal Shutdown (critical - charging stopped due to temp)
254 // 21 = PMIC Charge Timeout (critical - safety timer expired, stuck charging)
255 // 23 = PMIC Battery Fault (major - general charging issue)
256 //
257 // Log-Only Diagnostics (NOT alerted - transient/normal conditions):
258 // Input Fault: VBUS out of range (solar undervoltage common, backend detects sustained issues)
259 //
260 // Remediation Strategy:
261 // Level 0: Monitor only (log diagnostics, raise alert)
262 // Level 1: Soft reset (cycle charging off/on after 2+ consecutive faults)
263 // Level 2: Power cycle with watchdog (after 3+ consecutive faults)
264 // Cooldown: 1 hour minimum between remediation attempts
265 // Auto-Clear: Resets all counters when charging returns to healthy state
266 //
267 // This prevents the common "loss of charge until power cycle" issue by
268 // detecting PMIC faults early and automatically attempting recovery before
269 // requiring manual intervention.
270 // =========================================================================
271
272 // PMIC health monitoring (BQ24195 PMIC - Boron only)
273 // Tracks charging faults and attempts smart remediation with escalation
274 static unsigned long lastRemediationAttempt = 0;
275 static uint8_t remediationLevel = 0; // 0=none, 1=soft reset, 2=power cycle
276 static uint8_t consecutiveFaults = 0;
277 const unsigned long REMEDIATION_COOLDOWN = 3600000; // 1 hour between attempts
278
279 // Check if charging is intentionally disabled due to temperature BEFORE attempting remediation
280 bool safeToCharge = isItSafeToCharge();
281
282 PMIC pmic(true); // true = lock I2C during operations
283
284 // Read REG09 (Fault Register)
285 byte faultReg = pmic.readFaultRegister();
286
287 // Check for charging faults (bits 3-5: CHRG_FAULT)
288 if (faultReg & 0x38) {
289 uint8_t chargeFault = (faultReg >> 3) & 0x07;
290 consecutiveFaults++;
291
292 switch(chargeFault) {
293 case 0x01: // Input fault (VBUS overvoltage or undervoltage)
294 // This triggers when VIN < powerSourceMinVoltage (5.08V) or > max
295 // Most common cause: obscured/faulty solar panel insufficient voltage
296 // LOG ONLY - transient voltage dips are normal (clouds, trees, dawn/dusk)
297 // Backend detects sustained panel failures via multi-day SoC decline
298 Log.info("PMIC: Input fault - VBUS out of range (likely solar variation)");
299 break;
300 case 0x02: // Thermal shutdown
301 Log.error("PMIC: Thermal shutdown - charging stopped due to temperature");
302 current.raiseAlert(20); // Alert code 20: PMIC Thermal (critical)
303 break;
304 case 0x03: // Charge safety timer expired
305 Log.error("PMIC: Charge safety timer expired - charging timeout (common stuck charging indicator)");
306 current.raiseAlert(21); // Alert code 21: PMIC Charge Timeout (critical)
307 break;
308 default:
309 Log.warn("PMIC: Charge fault detected (code=0x%02x)", chargeFault);
310 current.raiseAlert(23); // Alert code 23: PMIC Battery Fault
311 break;
312 }
313
314 // Smart remediation with escalation and thrash prevention
315 // CRITICAL SAFETY CHECK: Never attempt remediation if charging is disabled due to temperature
316 if (!safeToCharge) {
317 Log.info("PMIC: Fault detected but charging disabled due to temperature (%.1fC) - skipping remediation",
318 (double)current.get_internalTempC());
319 // Don't escalate fault counters when temperature is the issue
320 // Temperature will recover naturally without intervention
321 } else {
322 unsigned long now = millis();
323 if (now - lastRemediationAttempt > REMEDIATION_COOLDOWN) {
324 // Escalate remediation level based on consecutive faults
325 if (consecutiveFaults >= 3 && remediationLevel < 2) {
326 remediationLevel = 2; // Escalate to power cycle reset
327 } else if (consecutiveFaults >= 2 && remediationLevel < 1) {
328 remediationLevel = 1; // Escalate to disable/enable charging
329 }
330
331 // Apply remediation based on level
332 switch(remediationLevel) {
333 case 1:
334 Log.warn("PMIC: Attempting soft remediation - cycle charging (level 1)");
335 pmic.disableCharging();
336 delay(500);
337 pmic.enableCharging();
338 Log.info("PMIC: Charging re-enabled after soft reset");
339 break;
340
341 case 2:
342 Log.error("PMIC: Attempting aggressive remediation - power cycle reset (level 2)");
343 pmic.disableCharging();
344 delay(1000);
345 // Set watchdog to force reset in 10 seconds if charging doesn't recover
346 pmic.setWatchdog(0b01); // 40 seconds
347 pmic.enableCharging();
348 Log.info("PMIC: Charging re-enabled with watchdog supervision");
349 remediationLevel = 0; // Reset level after power cycle attempt
350 break;
351
352 default:
353 Log.info("PMIC: Fault detected but remediation level 0 - monitoring only");
354 break;
355 }
356
357 lastRemediationAttempt = now;
358 } else {
359 unsigned long remainingCooldown = (REMEDIATION_COOLDOWN - (now - lastRemediationAttempt)) / 60000;
360 Log.info("PMIC: Fault detected but in cooldown period (%lu min remaining)", remainingCooldown);
361 }
362 }
363 } else {
364 // No faults detected - clear counters if charging is healthy
365 if (consecutiveFaults > 0) {
366 Log.info("PMIC: Charging healthy - clearing fault counters");
367 consecutiveFaults = 0;
368 remediationLevel = 0;
369
370 // Clear PMIC-related alerts if they were active
371 int8_t currentAlert = current.get_alertCode();
372 if (currentAlert >= 20 && currentAlert <= 23) {
373 Log.info("PMIC: Clearing battery/charging alert %d - charging resumed", currentAlert);
374 current.set_alertCode(0);
375 current.set_lastAlertTime(0);
376 }
377 }
378 }
379
380 // Read REG08 (System Status Register) for additional diagnostics
381 byte systemStatus = pmic.readSystemStatusRegister();
382 uint8_t chargeStatus = (systemStatus >> 4) & 0x03;
383 bool vbusGood = (systemStatus & 0x80) != 0;
384 uint8_t thermalStatus = systemStatus & 0x03;
385
386 const char* chargeStatusStr[] = {"Not Charging", "Pre-charge", "Fast Charging", "Charge Done"};
387 const char* thermalStr[] = {"Normal", "Warm", "Hot", "Cold"};
388
389 Log.info("PMIC Status: charge=%s, VBUS=%s, thermal=%s, faultReg=0x%02x",
390 chargeStatusStr[chargeStatus],
391 vbusGood ? "Good" : "Fault",
392 thermalStr[thermalStatus],
393 faultReg);
394
395 // Detect stuck charging state (charging for >6 hours at same SoC)
396 static uint8_t lastChargeStatus = 0xFF;
397 static float lastSoC = -1.0f;
398 static unsigned long chargeStateStartTime = 0;
399
400 if (chargeStatus == 2) { // Fast Charging
401 if (lastChargeStatus == 2) {
402 // Still in fast charging
403 if (abs(soc - lastSoC) < 1.0f) { // SoC not increasing
404 if (chargeStateStartTime == 0) {
405 chargeStateStartTime = millis();
406 } else if (millis() - chargeStateStartTime > 6UL * 3600000UL) { // 6 hours
407 Log.error("PMIC: Stuck in Fast Charging for 6+ hours with no SoC increase (%.1f%%) - possible fault", (double)soc);
408 current.raiseAlert(21); // Charge timeout alert
409 }
410 } else {
411 chargeStateStartTime = 0; // SoC increasing, reset timer
412 }
413 } else {
414 chargeStateStartTime = millis(); // Just entered fast charging
415 }
416 } else {
417 chargeStateStartTime = 0; // Not charging or charge done
418 }
419
420 lastChargeStatus = chargeStatus;
421 lastSoC = soc;
422#endif // HAL_PLATFORM_CELLULAR && (PLATFORM_ID != PLATFORM_MSOM)
423
424#elif PLATFORM_ID == 32 || PLATFORM_ID == 34
425 // Photon 2 and P2:
426 // Measure battery voltage (VBAT_MEAS on Photon 2, or same pin on P2
427 // carrier) using A6 as described in the Photon 2 battery voltage docs.
428
429 int raw = analogRead(A6);
430 float voltage = raw / 819.2f; // Map ADC count (0-4095) to 0-5V
431
432 // Approximate state-of-charge from voltage for a LiPo battery.
433 // Treat 3.0V as 0% and 4.2V as 100%.
434 float soc = (voltage - 3.0f) * (100.0f / (4.2f - 3.0f));
435 if (soc < 0.0f) {
436 soc = 0.0f;
437 } else if (soc > 100.0f) {
438 soc = 100.0f;
439 }
440 current.set_stateOfCharge(soc);
441
442 // Photon 2/P2 cannot reliably determine charging state without a PMIC.
443 // Always report "Unknown" since voltage alone can't distinguish between
444 // charging and discharging at the same voltage level.
445 uint8_t battState = 0; // Unknown
446
447 // Log battery diagnostics (Photon 2/P2 voltage-based estimation)
448 Log.info("Battery: voltage=%.2fV, state=%s (%d), SoC=%.2f%% (estimated from voltage)",
449 (double)voltage, batteryContext[battState], battState, (double)soc);
450
451 current.set_batteryState(battState);
452
453#else
454 // Other Wi-Fi / SoM platforms: leave battery fields unchanged for now.
455#endif
456
457 // -------------------------------------------------------------------------
458 // Temperature source selection
459 // -------------------------------------------------------------------------
460 // Default behavior:
461 // - If a TMP112A is present on the I2C bus (Muon), prefer it.
462 // - Otherwise, use TMP36 (if wired) or platform-specific stub.
463 //
464 // Compile-time controls:
465 // - Define MUON_HAS_TMP112 to force enable the TMP112A path.
466 // - Define DISABLE_TMP112_AUTODETECT to skip probing for TMP112A.
467
468#if defined(MUON_TMP112_I2C_ADDR)
469 const uint8_t tmp112Addr = (uint8_t)MUON_TMP112_I2C_ADDR;
470#else
471 const uint8_t tmp112Addr = 0x48;
472#endif
473
474 static bool tmp112ProbeDone = false;
475 static bool tmp112Present = false;
476
477#if !defined(DISABLE_TMP112_AUTODETECT)
478 if (!tmp112ProbeDone) {
479 // Safe to call multiple times; ensures I2C is initialized even if nothing
480 // else has yet started Wire.
481 Wire.begin();
482 tmp112Present = probeTmp112Present(tmp112Addr);
483 tmp112ProbeDone = true;
484 if (sysStatus.get_verboseMode()) {
485 Log.info("TMP112A probe at 0x%02X: %s", tmp112Addr, tmp112Present ? "present" : "not found");
486 }
487 }
488#endif
489
490#if defined(MUON_HAS_TMP112)
491 tmp112Present = true;
492 tmp112ProbeDone = true;
493#endif
494
495 if (tmp112Present) {
496 float tempC;
497 if (!readTmp112TemperatureC(tempC) || !(tempC > -50.0f && tempC < 120.0f)) {
498 float prev = current.get_internalTempC();
499 tempC = (prev > -50.0f && prev < 120.0f) ? prev : 25.0f;
500 Log.warn("TMP112A read failed/invalid - falling back to %4.2f C", (double)tempC);
501 }
502 current.set_internalTempC(tempC);
503 }
504
505#if (PLATFORM_ID == 32 || PLATFORM_ID == 34) && !defined(MUON_HAS_TMP36)
506 // Photon 2 and P2 development platforms:
507 // There is no TMP36 wired to an ADC-capable pin on the Photon 2 dev
508 // carrier, so we cannot take a real analog temperature reading here.
509 // Instead, use whatever value has been stored in internalTempC (for
510 // example, set manually for testing), falling back to 25C if unset.
511
512 float tempC = current.get_internalTempC();
513 if (!(tempC > -50.0f && tempC < 120.0f)) {
514 tempC = 25.0f;
515 }
516
517 if (sysStatus.get_verboseMode()) {
518 Log.info("P2/Photon2 stub: using internalTempC=%4.2f C (no TMP36 ADC)", (double)tempC);
519 }
520
521 current.set_internalTempC(tempC);
522
523#else
524 // Measure enclosure temperature using the TMP36 on the carrier board
525 // (connected to TMP36_SENSE_PIN, typically A4).
526 // Non-blocking sampling: spread 8 samples across multiple batteryState()
527 // calls to avoid blocking the main loop. Each call takes one sample (~5µs ADC
528 // read) until all samples are collected, then computes the average.
529 if (tmp112Present) {
530 // TMP112A already provided a temperature this cycle; skip TMP36 sampling.
531 // This avoids unnecessary ADC activity on boards where both might exist.
533 return current.get_stateOfCharge() > 20.0f;
534 }
535 pinMode(TMP36_SENSE_PIN, INPUT);
536
537 const int TMP36_SAMPLES = 8;
538 static int sampleIndex = 0; // Tracks how many samples taken this cycle
539 static int tmpRawSum = 0; // Running sum of ADC readings
540
541 if (sampleIndex < TMP36_SAMPLES) {
542 // Take one ADC sample per call, accumulate into sum
543 int v = analogRead(TMP36_SENSE_PIN);
544 tmpRawSum += v;
545 sampleIndex++;
546
547 // Not done yet; use previous temperature value and return early.
548 // Caller can call batteryState() again on next loop to continue sampling.
549 return current.get_stateOfCharge() > 20.0f;
550 }
551
552 // All samples collected; compute average and reset for next cycle
553 int tmpRaw = tmpRawSum / TMP36_SAMPLES;
554 sampleIndex = 0;
555 tmpRawSum = 0;
556
557 // Consider extremely low readings as "sensor not present". With a TMP36,
558 // even very cold temperatures should still be around 100mV (roughly 120
559 // ADC counts on a 3.3V/12-bit ADC), so an average below ~50 counts is
560 // effectively 0V at the pin.
561 bool sensorOk = (tmpRaw > 50 && tmpRaw < 4000);
562 float tempC = tmp36TemperatureC(tmpRaw);
563
564 // If the TMP36 reading is clearly out of a plausible enclosure range
565 // (for example, -50C from a raw 0 reading), or the sensor appears to be
566 // disconnected, fall back to a prior stored value or a conservative
567 // default so that charging guard rails and telemetry still operate with
568 // a realistic value.
569 if (!sensorOk || tempC < -20.0f || tempC > 80.0f) {
570 float prev = current.get_internalTempC();
571 float fallback = 25.0f; // conservative room-temperature default
572
573 if (prev > -20.0f && prev < 80.0f) {
574 fallback = prev;
575 }
576
577 Log.warn("TMP36 reading invalid or out of range (tmp36=%4.2f C, raw=%d, sensorOk=%s) - falling back to %4.2f C",
578 (double)tempC, tmpRaw, sensorOk ? "true" : "false", (double)fallback);
579 tempC = fallback;
580 }
581
582 current.set_internalTempC(tempC);
583
584 // Optional debug: log enclosure temperature when verbose logging is enabled
585 if (sysStatus.get_verboseMode()) {
586 Log.info("Enclosure temperature (effective): %4.2f C (raw=%d)", (double)tempC, tmpRaw);
587 }
588
589#endif // PLATFORM_ID == 32 || PLATFORM_ID == 34
590
591 // Apply temperature-based charging guard rails (see reference implementation).
592 // On cellular platforms this will enable/disable PMIC charging based on
593 // current.get_internalTempC(); on others it is a no-op.
595
596 // Convenience: indicate whether battery is in a healthy range.
597 return current.get_stateOfCharge() > 20.0f;
598}
599
600bool SensorManager::isItSafeToCharge() // Returns a true or false if the battery
601 // is in a safe charging range based on
602 // enclosure temperature
603{
604 float temp = current.get_internalTempC();
605 // Apply simple hysteresis around the recommended LiPo charge range
606 // to avoid rapid toggling near the temperature boundaries. When
607 // charging is currently allowed, we disable if temp < 0C or > 45C.
608 // When charging is currently disallowed, we only re-enable once
609 // temp has returned to a tighter 2C-43C window.
610 static bool lastSafe = true;
611
612 bool safe;
613 if (lastSafe) {
614 safe = !(temp < 0.0f || temp > 45.0f);
615 } else {
616 safe = !(temp < 2.0f || temp > 43.0f);
617 }
618 lastSafe = safe;
619
620#if HAL_PLATFORM_CELLULAR
621 // On Boron (cellular Gen 3), a BQ24195 PMIC is available so we
622 // actually enable/disable charging based on the enclosure
623 // temperature.
624 PMIC pmic(true);
625
626 if (!safe) {
627 pmic.disableCharging();
628 current.set_batteryState(1); // Reflect that we are "Not Charging"
629
630 Log.warn("Charging disabled due to enclosure temperature: %4.2f C", (double)temp);
631 } else {
632 pmic.enableCharging();
633
634 if (sysStatus.get_verboseMode()) {
635 Log.info("Charging enabled; enclosure temperature: %4.2f C", (double)temp);
636 }
637 }
638#else
639 // On platforms without a PMIC API (such as Argon, Photon 2 / P2), we
640 // do not control charging, but we still evaluate and log whether it
641 // would be considered safe based on the same temperature range.
642 if (!safe) {
643 Log.warn("Charging would be disabled due to enclosure temperature: %4.2f C (no PMIC on this platform)", (double)temp);
644 } else if (sysStatus.get_verboseMode()) {
645 Log.info("Charging would be enabled; enclosure temperature: %4.2f C (no PMIC on this platform)", (double)temp);
646 }
647#endif
648
649 return safe;
650}
651
653 char signalStr[64]; // Declare outside platform-specific blocks
654
655#if HAL_PLATFORM_CELLULAR
656 const char *radioTech[10] = {"Unknown", "None", "WiFi", "GSM",
657 "UMTS", "CDMA", "LTE", "IEEE802154",
658 "LTE_CAT_M1", "LTE_CAT_NB1"};
659 // New Signal Strength capability -
660 // https://community.particle.io/t/boron-lte-and-cellular-rssi-funny-values/45299/8
661 CellularSignal sig = Cellular.RSSI();
662
663 auto rat = sig.getAccessTechnology();
664
665 // float strengthVal = sig.getStrengthValue();
666 float strengthPercentage = sig.getStrength();
667
668 // float qualityVal = sig.getQualityValue();
669 float qualityPercentage = sig.getQuality();
670
671 snprintf(signalStr, sizeof(signalStr), "%s S:%2.0f%%, Q:%2.0f%% ",
672 radioTech[rat], strengthPercentage, qualityPercentage);
673 Log.info(signalStr);
674#elif HAL_PLATFORM_WIFI
675 WiFiSignal sig = WiFi.RSSI();
676 float strengthPercentage = sig.getStrength();
677 float qualityPercentage = sig.getQuality();
678
679 snprintf(signalStr, sizeof(signalStr), "WiFi S:%2.0f%%, Q:%2.0f%% ",
680 strengthPercentage, qualityPercentage);
681 Log.info(signalStr);
682#endif
683}
Persistent Data Storage Structures - EEPROM/Retained Memory Management.
#define sensorConfig
#define sysStatus
#define current
SensorType
Enumeration of available sensor types (backward-compatible IDs).
const char * batteryContext[7]
Singleton wrapper around ISensor implementations.
char signalStr[64]
Abstract interface for all sensors.
Definition ISensor.h:75
virtual const char * getSensorType() const =0
Get sensor type identifier.
static ISensor * createSensor(SensorType type)
Create a sensor instance based on the specified type.
float tmp36TemperatureC(int adcValue)
Convert TMP36 ADC reading to degrees Celsius.
void setup()
Initialize the active sensor and any manager state.
virtual ~SensorManager()
void onExitSleep()
Notify the sensor that the device has woken from deep sleep.
unsigned long _lastPollTime
Timestamp of the last sensor poll (millis).
static SensorManager & instance()
Get the SensorManager singleton instance.
bool isSensorReady() const
Check whether the active sensor is initialized and ready.
bool readTmp112TemperatureC(float &tempC)
Read TMP112A temperature (I2C) in degrees Celsius.
ISensor * _sensor
Currently active sensor implementation (not owned).
SensorData getSensorData() const
Get the latest sensor data from the active sensor.
void initializeFromConfig()
Create and initialize the active sensor based on configuration.
void setSensor(ISensor *sensor)
Set the concrete ISensor implementation to use.
void onEnterSleep()
Notify the sensor that the device is entering deep sleep.
bool isItSafeToCharge()
Determine whether it is safe to charge the battery.
bool batteryState()
Determine whether the battery is present and not critically low.
bool loop()
Poll the active sensor; call from the main loop.
void getSignalStrength()
Update global signal strength strings for logging/telemetry.
static SensorManager * _instance
Pointer to the singleton instance.
const pin_t TMP36_SENSE_PIN
const pin_t ledPower
const pin_t disableModule
Pinout definitions for the carrier board and sensors.
Generic sensor data structure.
Definition ISensor.h:23