1863 lines
63 KiB
C++
1863 lines
63 KiB
C++
#include "EnvironmentSensorManager.h"
|
|
#include "I2CAddressClaimPolicy.h"
|
|
#include "EnvironmentI2CConfig.h"
|
|
#include "NmeaSentenceProbe.h"
|
|
#if defined(OTA_RAK_AUTO_STORE) && defined(RAK_BOARD)
|
|
#include "../ota/OtaStoreQspiNrf52.h"
|
|
#endif
|
|
|
|
#include <Wire.h>
|
|
|
|
#if ENV_HAS_SECONDARY_I2C
|
|
#define TELEM_WIRE &Wire1 // Use Wire1 as the I2C bus for Environment Sensors
|
|
#else
|
|
#define TELEM_WIRE &Wire // Use default I2C bus for Environment Sensors
|
|
#endif
|
|
|
|
#ifdef NRF52_PLATFORM
|
|
static bool isValidNrfI2cPinPair(int32_t sda, int32_t scl) {
|
|
if (!mesh::isValidI2cPinPair(sda, scl, PINS_COUNT)) return false;
|
|
#ifdef NRF_P1
|
|
static const uint32_t MAX_PHYSICAL_NRF_PIN = 48;
|
|
#else
|
|
static const uint32_t MAX_PHYSICAL_NRF_PIN = 32;
|
|
#endif
|
|
return digitalPinToPinName(static_cast<uint32_t>(sda))
|
|
< MAX_PHYSICAL_NRF_PIN
|
|
&& digitalPinToPinName(static_cast<uint32_t>(scl))
|
|
< MAX_PHYSICAL_NRF_PIN;
|
|
}
|
|
#endif
|
|
|
|
// The pinned Adafruit nRF52 Wire core has no transaction timeout. Reject a
|
|
// bus which is already held low, and make one bounded standard nine-clock
|
|
// recovery attempt, before entering this manager's blocking transaction loops.
|
|
// This cannot protect I2C users which ran earlier in boot or cure a peripheral
|
|
// which wedges mid-transaction, but it prevents discovery from entering a bus
|
|
// which is already visibly stuck and limits the number of exposed transactions.
|
|
static bool ensureI2cBusReleased(TwoWire* wire) {
|
|
#ifdef NRF52_PLATFORM
|
|
// Board::begin() may remap the primary Wire instance away from the variant's
|
|
// PIN_WIRE_* defaults (ProMicro is one example). Use the same board-level
|
|
// pins first so recovery never probes or drives an unrelated GPIO.
|
|
int32_t sda = -1;
|
|
int32_t scl = -1;
|
|
#if ENV_HAS_SECONDARY_I2C
|
|
if (wire == &Wire1) {
|
|
sda = ENV_PIN_SDA;
|
|
scl = ENV_PIN_SCL;
|
|
} else
|
|
#endif
|
|
{
|
|
#if defined(PIN_BOARD_SDA) && defined(PIN_BOARD_SCL)
|
|
sda = PIN_BOARD_SDA;
|
|
scl = PIN_BOARD_SCL;
|
|
#elif defined(PIN_WIRE_SDA) && defined(PIN_WIRE_SCL)
|
|
sda = PIN_WIRE_SDA;
|
|
scl = PIN_WIRE_SCL;
|
|
#endif
|
|
}
|
|
|
|
if (!isValidNrfI2cPinPair(sda, scl)) return false;
|
|
|
|
if (digitalRead(sda) == HIGH && digitalRead(scl) == HIGH) return true;
|
|
|
|
wire->end();
|
|
auto releaseLine = [](int32_t pin) {
|
|
// OUTPUT_S0D1 is nRF open drain: drive zero, disconnect for one. The input
|
|
// pull-up then observes the external bus without ever driving it high.
|
|
digitalWrite(pin, HIGH);
|
|
pinMode(pin, INPUT_PULLUP);
|
|
};
|
|
auto driveLineLow = [](int32_t pin) {
|
|
// Prime the output latch before enabling an output, avoiding a brief
|
|
// push-pull high pulse if the previous latch happened to contain one.
|
|
digitalWrite(pin, LOW);
|
|
pinMode(pin, OUTPUT_S0D1);
|
|
};
|
|
auto waitLineHigh = [](int32_t pin) {
|
|
for (uint8_t wait = 0; wait < 20; wait++) {
|
|
if (digitalRead(pin) == HIGH) return true;
|
|
delayMicroseconds(5);
|
|
}
|
|
return digitalRead(pin) == HIGH;
|
|
};
|
|
|
|
releaseLine(sda);
|
|
releaseLine(scl);
|
|
delayMicroseconds(10);
|
|
|
|
// A secondary may have stopped halfway through a byte. Clock it to a byte
|
|
// boundary while never driving either open-drain line high.
|
|
if (digitalRead(scl) == HIGH) {
|
|
for (uint8_t pulse = 0; pulse < 9 && digitalRead(sda) == LOW; pulse++) {
|
|
driveLineLow(scl);
|
|
delayMicroseconds(5);
|
|
releaseLine(scl);
|
|
if (!waitLineHigh(scl)) break;
|
|
}
|
|
|
|
if (waitLineHigh(scl)) {
|
|
// Generate a STOP (SDA low-to-high while SCL is confirmed high).
|
|
driveLineLow(sda);
|
|
delayMicroseconds(5);
|
|
releaseLine(scl);
|
|
if (waitLineHigh(scl)) {
|
|
releaseLine(sda);
|
|
delayMicroseconds(10);
|
|
}
|
|
}
|
|
}
|
|
|
|
#if ENV_HAS_SECONDARY_I2C
|
|
if (wire == &Wire1) {
|
|
Wire1.setPins(static_cast<uint8_t>(sda), static_cast<uint8_t>(scl));
|
|
Wire1.begin();
|
|
Wire1.setClock(100000);
|
|
} else
|
|
#endif
|
|
{
|
|
Wire.setPins(static_cast<uint8_t>(sda), static_cast<uint8_t>(scl));
|
|
Wire.begin();
|
|
}
|
|
delayMicroseconds(10);
|
|
return digitalRead(sda) == HIGH && digitalRead(scl) == HIGH;
|
|
#else
|
|
(void)wire;
|
|
return true;
|
|
#endif
|
|
}
|
|
|
|
bool EnvironmentSensorManager::i2c_probe(TwoWire& wire, uint8_t addr) {
|
|
if (!mesh::isValidI2cPeripheralAddress(addr)) return false;
|
|
wire.beginTransmission(addr);
|
|
uint8_t error = wire.endTransmission();
|
|
return error == 0;
|
|
}
|
|
|
|
// ============================================================
|
|
// Sensor library includes and static driver instances
|
|
// ============================================================
|
|
|
|
#if ENV_INCLUDE_BME680_BSEC
|
|
#ifndef TELEM_BME680_ADDRESS
|
|
#define TELEM_BME680_ADDRESS 0x76
|
|
#endif
|
|
#define TELEM_BME680_SEALEVELPRESSURE_HPA (1013.25)
|
|
#include <bsec.h>
|
|
#include <Adafruit_LittleFS.h>
|
|
#include <InternalFileSystem.h>
|
|
static const uint8_t bsec_config_iaq[] = {
|
|
#include "config/generic_33v_3s_28d/bsec_iaq.txt" // 3.3v, LP, 28 day background calibration window
|
|
};
|
|
static Bsec bsec_iaq;
|
|
static float bsec_temperature = 0;
|
|
static float bsec_humidity = 0;
|
|
static float bsec_pressure_hpa = 0;
|
|
static float bsec_iaq_val = 0;
|
|
static uint8_t bsec_accuracy = 0;
|
|
static bool bsec_active = false;
|
|
static bool bsec_data_ready = false;
|
|
static bool bsec_first_save_done = false;
|
|
static uint32_t bsec_last_save_ms = 0;
|
|
#define BSEC_STATE_FILE "/bsec_state.bin"
|
|
#define BSEC_SAVE_INTERVAL_MS (8UL * 60 * 60 * 1000) // 8 hour state-save interval
|
|
#endif
|
|
|
|
#ifdef ENV_INCLUDE_BME680
|
|
#ifndef TELEM_BME680_ADDRESS
|
|
#define TELEM_BME680_ADDRESS 0x76
|
|
#endif
|
|
#define TELEM_BME680_SEALEVELPRESSURE_HPA (1013.25)
|
|
#include <Adafruit_BME680.h>
|
|
static Adafruit_BME680 BME680(TELEM_WIRE);
|
|
#endif
|
|
|
|
#ifdef ENV_INCLUDE_BMP085
|
|
#define TELEM_BMP085_SEALEVELPRESSURE_HPA (1013.25)
|
|
#include <Adafruit_BMP085.h>
|
|
static Adafruit_BMP085 BMP085;
|
|
#endif
|
|
|
|
#if ENV_INCLUDE_AHTX0
|
|
#ifndef TELEM_AHTX_ADDRESS
|
|
#define TELEM_AHTX_ADDRESS 0x38 // AHT10, AHT20 temperature and humidity sensor I2C address
|
|
#endif
|
|
#include <Adafruit_AHTX0.h>
|
|
static Adafruit_AHTX0 AHTX0;
|
|
#endif
|
|
|
|
#if ENV_INCLUDE_BME280
|
|
#ifndef TELEM_BME280_ADDRESS
|
|
#define TELEM_BME280_ADDRESS 0x76 // BME280 environmental sensor I2C address
|
|
#endif
|
|
#define TELEM_BME280_SEALEVELPRESSURE_HPA (1013.25) // Atmospheric pressure at sea level
|
|
#include <Adafruit_BME280.h>
|
|
static Adafruit_BME280 BME280;
|
|
#endif
|
|
|
|
#if ENV_INCLUDE_BMP280
|
|
#ifndef TELEM_BMP280_ADDRESS
|
|
#define TELEM_BMP280_ADDRESS 0x76 // BMP280 environmental sensor I2C address
|
|
#endif
|
|
#define TELEM_BMP280_SEALEVELPRESSURE_HPA (1013.25) // Atmospheric pressure at sea level
|
|
#include <Adafruit_BMP280.h>
|
|
static Adafruit_BMP280 BMP280(TELEM_WIRE);
|
|
#endif
|
|
|
|
#if ENV_INCLUDE_SHTC3
|
|
#include <Adafruit_SHTC3.h>
|
|
static Adafruit_SHTC3 SHTC3;
|
|
#endif
|
|
|
|
#if ENV_INCLUDE_SHT4X
|
|
#ifndef TELEM_SHT4X_ADDRESS
|
|
#define TELEM_SHT4X_ADDRESS 0x44
|
|
#endif
|
|
#include <SensirionI2cSht4x.h>
|
|
static SensirionI2cSht4x SHT4X;
|
|
#endif
|
|
|
|
#if ENV_INCLUDE_LPS22HB
|
|
#include <Arduino_LPS22HB.h>
|
|
LPS22HBClass LPS22HB(*TELEM_WIRE);
|
|
#endif
|
|
|
|
#if ENV_INCLUDE_INA3221
|
|
#ifndef TELEM_INA3221_ADDRESS
|
|
#define TELEM_INA3221_ADDRESS 0x42 // INA3221 3 channel current sensor I2C address
|
|
#endif
|
|
#ifndef TELEM_INA3221_SHUNT_VALUE
|
|
#define TELEM_INA3221_SHUNT_VALUE 0.100 // most variants will have a 0.1 ohm shunts
|
|
#endif
|
|
#ifndef TELEM_INA3221_NUM_CHANNELS
|
|
#define TELEM_INA3221_NUM_CHANNELS 3
|
|
#endif
|
|
#include <Adafruit_INA3221.h>
|
|
static Adafruit_INA3221 INA3221;
|
|
#endif
|
|
|
|
#if ENV_INCLUDE_INA219
|
|
#ifndef TELEM_INA219_ADDRESS
|
|
#define TELEM_INA219_ADDRESS 0x40 // INA219 single channel current sensor I2C address
|
|
#endif
|
|
#include <Adafruit_INA219.h>
|
|
static Adafruit_INA219 INA219(TELEM_INA219_ADDRESS);
|
|
#endif
|
|
|
|
#if ENV_INCLUDE_INA260
|
|
#ifndef TELEM_INA260_ADDRESS
|
|
#define TELEM_INA260_ADDRESS 0x41 // INA260 single channel current sensor I2C address
|
|
#endif
|
|
#include <Adafruit_INA260.h>
|
|
static Adafruit_INA260 INA260;
|
|
#endif
|
|
|
|
#if ENV_INCLUDE_INA226
|
|
#ifndef TELEM_INA226_ADDRESS
|
|
#define TELEM_INA226_ADDRESS 0x44
|
|
#endif
|
|
#define TELEM_INA226_SHUNT_VALUE 0.100
|
|
#define TELEM_INA226_MAX_AMP 0.8
|
|
#include <INA226.h>
|
|
static INA226 INA226(TELEM_INA226_ADDRESS, TELEM_WIRE);
|
|
#endif
|
|
|
|
#if ENV_INCLUDE_MLX90614
|
|
#ifndef TELEM_MLX90614_ADDRESS
|
|
#define TELEM_MLX90614_ADDRESS 0x5A // MLX90614 IR temperature sensor I2C address
|
|
#endif
|
|
#include <Adafruit_MLX90614.h>
|
|
static Adafruit_MLX90614 MLX90614;
|
|
#endif
|
|
|
|
#if ENV_INCLUDE_VL53L0X
|
|
#ifndef TELEM_VL53L0X_ADDRESS
|
|
#define TELEM_VL53L0X_ADDRESS 0x29 // VL53L0X time-of-flight distance sensor I2C address
|
|
#endif
|
|
#include <Adafruit_VL53L0X.h>
|
|
static Adafruit_VL53L0X VL53L0X;
|
|
#endif
|
|
|
|
#if ENV_INCLUDE_RAK12035
|
|
#ifndef TELEM_RAK12035_ADDRESS
|
|
#define TELEM_RAK12035_ADDRESS 0x20 // RAK12035 Soil Moisture sensor I2C address
|
|
#endif
|
|
#include "RAK12035_SoilMoisture.h"
|
|
static RAK12035_SoilMoisture RAK12035;
|
|
#endif
|
|
|
|
#if ENV_INCLUDE_GPS && defined(RAK_BOARD) && !defined(RAK_WISMESH_TAG)
|
|
#define RAK_WISBLOCK_GPS
|
|
#endif
|
|
|
|
#ifdef RAK_WISBLOCK_GPS
|
|
// Release capability manifests scan the linked firmware image rather than
|
|
// trusting build flags. Give the actual WisBlock provider a unique marker and
|
|
// keep an observable reference from begin() so LTO/section GC cannot discard
|
|
// it. The marker lives inside this provider guard, so generic GPS CLI text
|
|
// cannot produce a false positive.
|
|
extern "C" {
|
|
extern const char meshcore_capability_rak_wisblock_gps[];
|
|
const char meshcore_capability_rak_wisblock_gps[] __attribute__((used)) =
|
|
"meshcore.capability.rak_wisblock_gps.v1";
|
|
}
|
|
|
|
// -1 = no enable pin; out-of-range values are no-ops in pinMode/digitalWrite,
|
|
// while 0 would be a real GPIO (P0.00 = LFXO crystal on nRF52)
|
|
static uint32_t gpsResetPin = -1;
|
|
static bool i2cGPSFlag = false;
|
|
static bool serialGPSFlag = false;
|
|
#ifndef TELEM_RAK12500_ADDRESS
|
|
#ifdef GPS_ADDRESS
|
|
#define TELEM_RAK12500_ADDRESS GPS_ADDRESS
|
|
#else
|
|
#define TELEM_RAK12500_ADDRESS 0x42 //RAK12500 Ublox GPS via i2c
|
|
#endif
|
|
#endif
|
|
#include <SparkFun_u-blox_GNSS_Arduino_Library.h>
|
|
static SFE_UBLOX_GNSS ublox_GNSS;
|
|
|
|
static bool configureRakUbloxReceiver() {
|
|
// Retain the receiver's supported constellation defaults/BBR selection.
|
|
// ZOE-M8Q cannot run GPS/Galileo/GLONASS/BeiDou all at once, and blindly
|
|
// enabling every constellation ignores CFG-GNSS constraints/errata. Only
|
|
// require the transport and cadence we actually consume after a cold wake.
|
|
// Do not repeatedly write receiver nonvolatile storage on wake.
|
|
return ublox_GNSS.setI2COutput(COM_TYPE_UBX)
|
|
&& ublox_GNSS.setMeasurementRate(1000);
|
|
}
|
|
|
|
#ifndef RAK_UART_GPS_PROBE_TIMEOUT_MS
|
|
#define RAK_UART_GPS_PROBE_TIMEOUT_MS 1200UL
|
|
#endif
|
|
|
|
static uint8_t rakGpsControlActiveLevel() {
|
|
#ifdef PIN_GPS_EN_ACTIVE
|
|
return PIN_GPS_EN_ACTIVE;
|
|
#else
|
|
return HIGH;
|
|
#endif
|
|
}
|
|
|
|
static void setRakGpsControl(uint8_t pin, bool shared_power_rail,
|
|
bool enabled) {
|
|
pinMode(pin, OUTPUT);
|
|
if (shared_power_rail) {
|
|
// Discovery cannot drop this shared rail. The manager's frozen ownership
|
|
// policy, not a provider's enable pin, handles later GPS-only shutdown.
|
|
digitalWrite(pin, HIGH);
|
|
return;
|
|
}
|
|
const uint8_t active = rakGpsControlActiveLevel();
|
|
digitalWrite(pin, enabled ? active : static_cast<uint8_t>(!active));
|
|
}
|
|
|
|
static bool serialHasValidGpsSentence(Stream& serial, uint32_t timeout_ms) {
|
|
mesh::NmeaSentenceProbe probe;
|
|
const uint32_t started = millis();
|
|
do {
|
|
while (serial.available()) {
|
|
// Continuous non-NMEA input must not bypass the existing probe window.
|
|
// Sample inside the byte loop as well as after an empty FIFO; unsigned
|
|
// elapsed time preserves the deadline across millisecond rollover.
|
|
if (static_cast<uint32_t>(millis() - started) >= timeout_ms) return false;
|
|
if (probe.ingest(static_cast<uint8_t>(serial.read()))) return true;
|
|
}
|
|
delay(5);
|
|
} while (static_cast<uint32_t>(millis() - started) < timeout_ms);
|
|
return false;
|
|
}
|
|
|
|
#if ENV_INCLUDE_INA3221
|
|
static mesh::I2cRegisterProbeStatus readI2cRegister16(TwoWire* wire,
|
|
uint8_t address,
|
|
uint8_t reg,
|
|
uint16_t& value) {
|
|
wire->beginTransmission(address);
|
|
if (wire->write(reg) != 1) {
|
|
return mesh::I2cRegisterProbeStatus::Inconclusive;
|
|
}
|
|
const uint8_t error = wire->endTransmission(false);
|
|
if (error == 2) return mesh::I2cRegisterProbeStatus::NoResponse;
|
|
if (error != 0) return mesh::I2cRegisterProbeStatus::Inconclusive;
|
|
if (wire->requestFrom(address, static_cast<uint8_t>(2)) != 2) {
|
|
return mesh::I2cRegisterProbeStatus::Inconclusive;
|
|
}
|
|
value = static_cast<uint16_t>(wire->read()) << 8;
|
|
value |= static_cast<uint8_t>(wire->read());
|
|
return mesh::I2cRegisterProbeStatus::Success;
|
|
}
|
|
|
|
static mesh::I2cIdentityProbeResult probeIna3221Identity(TwoWire* wire,
|
|
uint8_t address) {
|
|
uint16_t manufacturer = 0;
|
|
const mesh::I2cRegisterProbeStatus manufacturer_result = readI2cRegister16(
|
|
wire, address, INA3221_REG_MANUFACTURER_ID, manufacturer);
|
|
if (manufacturer_result != mesh::I2cRegisterProbeStatus::Success) {
|
|
return mesh::classifyIna3221Identity(
|
|
manufacturer_result, manufacturer,
|
|
mesh::I2cRegisterProbeStatus::Inconclusive, 0,
|
|
INA3221_MANUFACTURER_ID, INA3221_DIE_ID);
|
|
}
|
|
if (manufacturer != INA3221_MANUFACTURER_ID) {
|
|
// Require two successful non-INA reads. The bytes exposed by u-blox at
|
|
// 0xFE/0xFF are dynamic, so their values need not equal one another. This
|
|
// still cannot make a physical address collision safe.
|
|
uint16_t repeated_manufacturer = 0;
|
|
const mesh::I2cRegisterProbeStatus repeated_result = readI2cRegister16(
|
|
wire, address, INA3221_REG_MANUFACTURER_ID, repeated_manufacturer);
|
|
return mesh::classifyIna3221Identity(
|
|
manufacturer_result, manufacturer,
|
|
repeated_result, repeated_manufacturer,
|
|
INA3221_MANUFACTURER_ID, INA3221_DIE_ID);
|
|
}
|
|
|
|
uint16_t die = 0;
|
|
const mesh::I2cRegisterProbeStatus die_result = readI2cRegister16(
|
|
wire, address, INA3221_REG_DIE_ID, die);
|
|
return mesh::classifyIna3221Identity(
|
|
manufacturer_result, manufacturer, die_result, die,
|
|
INA3221_MANUFACTURER_ID, INA3221_DIE_ID);
|
|
}
|
|
#endif
|
|
|
|
class RAK12500LocationProvider : public LocationProvider {
|
|
long _lat = 0;
|
|
long _lng = 0;
|
|
long _alt = 0;
|
|
int _sats = 0;
|
|
long _epoch = 0;
|
|
bool _fix = false;
|
|
mesh::RTCClock* _clock = NULL;
|
|
unsigned long _next_time_check = 0;
|
|
unsigned long _last_time_sync = 0;
|
|
uint8_t _valid_time_samples = 0;
|
|
int _pin_en = -1;
|
|
public:
|
|
void setRTCClock(mesh::RTCClock* clock) { _clock = clock; }
|
|
mesh::RTCClock* getRTCClock() override { return _clock; }
|
|
void syncTime() override {
|
|
_valid_time_samples = 0;
|
|
LocationProvider::syncTime();
|
|
}
|
|
long getLatitude() override { return _lat; }
|
|
long getLongitude() override { return _lng; }
|
|
long getAltitude() override { return _alt; }
|
|
long satellitesCount() override { return _sats; }
|
|
bool isValid() override { return _fix; }
|
|
bool clearPositionFix() override {
|
|
_fix = false;
|
|
_next_time_check = 0;
|
|
ublox_GNSS.flushPVT();
|
|
return true;
|
|
}
|
|
long getTimestamp() override { return _epoch; }
|
|
void sendSentence(const char * sentence) override { }
|
|
void reset() override {
|
|
// Discovery can replace and later reselect this singleton provider. Never
|
|
// expose the previous receiver's fix or time samples while the new device
|
|
// is still acquiring.
|
|
_lat = 0;
|
|
_lng = 0;
|
|
_alt = 0;
|
|
_sats = 0;
|
|
_epoch = 0;
|
|
_fix = false;
|
|
_fresh_time_sync_required = false;
|
|
_next_time_check = 0;
|
|
_last_time_sync = 0;
|
|
_valid_time_samples = 0;
|
|
_time_sync_needed = true;
|
|
_time_sync_applied = false;
|
|
_last_valid_time_sync = 0;
|
|
resetTimeSyncRequestState();
|
|
}
|
|
void begin() override { }
|
|
void stop() override { }
|
|
void loop() override {
|
|
unsigned long now = millis();
|
|
if ((int32_t)(now - _next_time_check) >= 0) {
|
|
_next_time_check = now + 1000;
|
|
|
|
// Scalar getters return retained data even if their implicit poll fails.
|
|
// Require a received PVT in normal operation as well as tracker windows.
|
|
ublox_GNSS.flushPVT();
|
|
if (!ublox_GNSS.getPVT(8)) {
|
|
_fix = false;
|
|
_valid_time_samples = 0;
|
|
return;
|
|
}
|
|
if (ublox_GNSS.getGnssFixOk(8)) {
|
|
_fix = true;
|
|
_lat = ublox_GNSS.getLatitude(2) / 10;
|
|
_lng = ublox_GNSS.getLongitude(2) / 10;
|
|
_alt = ublox_GNSS.getAltitude(2);
|
|
_sats = ublox_GNSS.getSIV(2);
|
|
} else {
|
|
_fix = false;
|
|
}
|
|
bool date_valid = ublox_GNSS.getDateValid(2);
|
|
bool time_valid = ublox_GNSS.getTimeValid(2);
|
|
_epoch = ublox_GNSS.getUnixEpoch(2);
|
|
|
|
if (_fix && _sats >= 5 && date_valid && time_valid && _epoch > 0) {
|
|
if (_valid_time_samples < 0xFF) _valid_time_samples++;
|
|
} else {
|
|
_valid_time_samples = 0;
|
|
}
|
|
|
|
if (!_time_sync_needed && _clock != NULL
|
|
&& static_cast<uint32_t>(now - _last_time_sync)
|
|
>= periodicTimeSyncIntervalMillis()) {
|
|
_time_sync_needed = true;
|
|
}
|
|
if (_time_sync_needed && _clock != NULL && _valid_time_samples > 2) {
|
|
_clock->setCurrentTime((uint32_t)_epoch);
|
|
markTimeSyncApplied();
|
|
_time_sync_needed = false;
|
|
_last_time_sync = now;
|
|
_last_valid_time_sync = _clock->getCurrentTime();
|
|
}
|
|
}
|
|
}
|
|
bool isEnabled() override {
|
|
return _pin_en < 0 || digitalRead(_pin_en) == HIGH;
|
|
}
|
|
void setPinEn(int pin_en) override { _pin_en = pin_en; }
|
|
int getPinEn() override { return _pin_en; }
|
|
};
|
|
|
|
static RAK12500LocationProvider RAK12500_provider;
|
|
#endif
|
|
|
|
// ============================================================
|
|
// Per-sensor init and query functions
|
|
//
|
|
// init(wire, address) - called only when the address was seen
|
|
// on the bus. Returns 0 on failure, or the number of
|
|
// telemetry channels the sensor will consume (1 for all
|
|
// single-output sensors; INA3221 returns one per enabled
|
|
// hardware channel; MLX90614 and RAK12035+calibration
|
|
// return 2).
|
|
//
|
|
// query(channel, sub_channel, lpp) - called once per active
|
|
// sensor entry during querySensors(). sub_channel is always
|
|
// 0 for single-output sensors.
|
|
// ============================================================
|
|
|
|
#if ENV_INCLUDE_AHTX0
|
|
static uint8_t init_ahtx0(TwoWire* wire, uint8_t addr) {
|
|
return AHTX0.begin(wire, 0, addr) ? 1 : 0;
|
|
}
|
|
static void query_ahtx0(uint8_t ch, uint8_t, CayenneLPP& lpp) {
|
|
sensors_event_t humidity, temp;
|
|
AHTX0.getEvent(&humidity, &temp);
|
|
lpp.addTemperature(ch, temp.temperature);
|
|
lpp.addRelativeHumidity(ch, humidity.relative_humidity);
|
|
}
|
|
#endif
|
|
|
|
#ifdef ENV_INCLUDE_BME680
|
|
static uint8_t init_bme680(TwoWire*, uint8_t addr) {
|
|
// Wire was set in the static constructor; begin() takes address only.
|
|
return BME680.begin(addr) ? 1 : 0;
|
|
}
|
|
static void query_bme680(uint8_t ch, uint8_t, CayenneLPP& lpp) {
|
|
if (BME680.performReading()) {
|
|
lpp.addTemperature(ch, BME680.temperature);
|
|
lpp.addRelativeHumidity(ch, BME680.humidity);
|
|
const float pressure_hpa = BME680.pressure / 100.0f;
|
|
lpp.addBarometricPressure(ch, pressure_hpa);
|
|
lpp.addAltitude(ch, 44330.0f * (1.0f - powf(pressure_hpa / (float)TELEM_BME680_SEALEVELPRESSURE_HPA, 0.1903f)));
|
|
lpp.addGenericSensor(ch, BME680.gas_resistance);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#if ENV_INCLUDE_BME280
|
|
static uint8_t init_bme280(TwoWire* wire, uint8_t addr) {
|
|
if (!BME280.begin(addr, wire)) return 0;
|
|
BME280.setSampling(Adafruit_BME280::MODE_FORCED,
|
|
Adafruit_BME280::SAMPLING_X1,
|
|
Adafruit_BME280::SAMPLING_X1,
|
|
Adafruit_BME280::SAMPLING_X1,
|
|
Adafruit_BME280::FILTER_OFF,
|
|
Adafruit_BME280::STANDBY_MS_1000);
|
|
return 1;
|
|
}
|
|
static void query_bme280(uint8_t ch, uint8_t, CayenneLPP& lpp) {
|
|
if (BME280.takeForcedMeasurement()) {
|
|
lpp.addTemperature(ch, BME280.readTemperature());
|
|
lpp.addRelativeHumidity(ch, BME280.readHumidity());
|
|
lpp.addBarometricPressure(ch, BME280.readPressure() / 100);
|
|
lpp.addAltitude(ch, BME280.readAltitude(TELEM_BME280_SEALEVELPRESSURE_HPA));
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#if ENV_INCLUDE_BMP280
|
|
static uint8_t init_bmp280(TwoWire*, uint8_t addr) {
|
|
// BMP280 static instance was constructed with TELEM_WIRE; begin() uses it.
|
|
return BMP280.begin(addr) ? 1 : 0;
|
|
}
|
|
static void query_bmp280(uint8_t ch, uint8_t, CayenneLPP& lpp) {
|
|
lpp.addTemperature(ch, BMP280.readTemperature());
|
|
lpp.addBarometricPressure(ch, BMP280.readPressure() / 100);
|
|
lpp.addAltitude(ch, BMP280.readAltitude(TELEM_BMP280_SEALEVELPRESSURE_HPA));
|
|
}
|
|
#endif
|
|
|
|
#if ENV_INCLUDE_SHTC3
|
|
static uint8_t init_shtc3(TwoWire* wire, uint8_t) {
|
|
// Adafruit_SHTC3::begin() does not accept an address (fixed at 0x70).
|
|
return SHTC3.begin(wire) ? 1 : 0;
|
|
}
|
|
static void query_shtc3(uint8_t ch, uint8_t, CayenneLPP& lpp) {
|
|
sensors_event_t humidity, temp;
|
|
SHTC3.getEvent(&humidity, &temp);
|
|
lpp.addTemperature(ch, temp.temperature);
|
|
lpp.addRelativeHumidity(ch, humidity.relative_humidity);
|
|
}
|
|
#endif
|
|
|
|
#if ENV_INCLUDE_SHT4X
|
|
static uint8_t init_sht4x(TwoWire* wire, uint8_t addr) {
|
|
// SensirionI2cSht4x::begin() does not probe the hardware; use serialNumber()
|
|
// as the actual presence check since it performs a real I2C transaction.
|
|
SHT4X.begin(*wire, addr);
|
|
uint32_t serial = 0;
|
|
return (SHT4X.serialNumber(serial) == 0) ? 1 : 0;
|
|
}
|
|
static void query_sht4x(uint8_t ch, uint8_t, CayenneLPP& lpp) {
|
|
float temperature, humidity;
|
|
if (SHT4X.measureLowestPrecision(temperature, humidity) == 0) {
|
|
lpp.addTemperature(ch, temperature);
|
|
lpp.addRelativeHumidity(ch, humidity);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#if ENV_INCLUDE_LPS22HB
|
|
static uint8_t init_lps22hb(TwoWire*, uint8_t) {
|
|
// LPS22HBClass is constructed with the wire reference; begin() uses it.
|
|
return LPS22HB.begin() ? 1 : 0;
|
|
}
|
|
static void query_lps22hb(uint8_t ch, uint8_t, CayenneLPP& lpp) {
|
|
lpp.addTemperature(ch, LPS22HB.readTemperature());
|
|
lpp.addBarometricPressure(ch, LPS22HB.readPressure() * 10); // convert kPa to hPa
|
|
}
|
|
#endif
|
|
|
|
#if ENV_INCLUDE_INA3221
|
|
static uint8_t init_ina3221(TwoWire* wire, uint8_t addr) {
|
|
if (!INA3221.begin(addr, wire)) return 0;
|
|
for (int i = 0; i < TELEM_INA3221_NUM_CHANNELS; i++) {
|
|
INA3221.setShuntResistance(i, TELEM_INA3221_SHUNT_VALUE);
|
|
}
|
|
// Each enabled hardware channel becomes its own telemetry channel.
|
|
uint8_t enabled = 0;
|
|
for (int i = 0; i < TELEM_INA3221_NUM_CHANNELS; i++) {
|
|
if (INA3221.isChannelEnabled(i)) enabled++;
|
|
}
|
|
return enabled > 0 ? enabled : 1;
|
|
}
|
|
static void query_ina3221(uint8_t ch, uint8_t sub_ch, CayenneLPP& lpp) {
|
|
// sub_ch is the index of the nth enabled hardware channel.
|
|
uint8_t seen = 0;
|
|
for (int i = 0; i < TELEM_INA3221_NUM_CHANNELS; i++) {
|
|
if (INA3221.isChannelEnabled(i)) {
|
|
if (seen == sub_ch) {
|
|
float v = INA3221.getBusVoltage(i);
|
|
float c = INA3221.getCurrentAmps(i);
|
|
lpp.addVoltage(ch, v);
|
|
lpp.addCurrent(ch, c);
|
|
lpp.addPower(ch, v * c);
|
|
return;
|
|
}
|
|
seen++;
|
|
}
|
|
}
|
|
}
|
|
static bool query_ina3221_voltage(uint8_t sub_ch, float& voltage) {
|
|
uint8_t seen = 0;
|
|
for (int i = 0; i < TELEM_INA3221_NUM_CHANNELS; i++) {
|
|
if (!INA3221.isChannelEnabled(i)) continue;
|
|
if (seen++ != sub_ch) continue;
|
|
voltage = INA3221.getBusVoltage(i);
|
|
return isfinite(voltage);
|
|
}
|
|
return false;
|
|
}
|
|
#endif
|
|
|
|
#if ENV_INCLUDE_INA219
|
|
static uint8_t init_ina219(TwoWire* wire, uint8_t) {
|
|
// INA219 static instance was constructed with the address; begin() uses it.
|
|
return INA219.begin(wire) ? 1 : 0;
|
|
}
|
|
static void query_ina219(uint8_t ch, uint8_t, CayenneLPP& lpp) {
|
|
lpp.addVoltage(ch, INA219.getBusVoltage_V());
|
|
lpp.addCurrent(ch, INA219.getCurrent_mA() / 1000.0f);
|
|
lpp.addPower(ch, INA219.getPower_mW() / 1000.0f);
|
|
}
|
|
static bool query_ina219_voltage(uint8_t, float& voltage) {
|
|
voltage = INA219.getBusVoltage_V();
|
|
return INA219.success() && isfinite(voltage);
|
|
}
|
|
#endif
|
|
|
|
#if ENV_INCLUDE_INA260
|
|
static uint8_t init_ina260(TwoWire* wire, uint8_t addr) {
|
|
return INA260.begin(addr, wire) ? 1 : 0;
|
|
}
|
|
static void query_ina260(uint8_t ch, uint8_t, CayenneLPP& lpp) {
|
|
lpp.addVoltage(ch, INA260.readBusVoltage() / 1000.0f);
|
|
lpp.addCurrent(ch, INA260.readCurrent() / 1000.0f);
|
|
lpp.addPower(ch, INA260.readPower() / 1000.0f);
|
|
}
|
|
static bool query_ina260_voltage(uint8_t, float& voltage) {
|
|
voltage = INA260.readBusVoltage() / 1000.0f;
|
|
return isfinite(voltage);
|
|
}
|
|
#endif
|
|
|
|
#if ENV_INCLUDE_INA226
|
|
static uint8_t init_ina226(TwoWire*, uint8_t) {
|
|
// INA226 static instance was constructed with address and wire.
|
|
if (!INA226.begin()) return 0;
|
|
INA226.setMaxCurrentShunt(TELEM_INA226_MAX_AMP, TELEM_INA226_SHUNT_VALUE);
|
|
return 1;
|
|
}
|
|
static void query_ina226(uint8_t ch, uint8_t, CayenneLPP& lpp) {
|
|
lpp.addVoltage(ch, INA226.getBusVoltage());
|
|
lpp.addCurrent(ch, INA226.getCurrent_mA() / 1000.0f);
|
|
lpp.addPower(ch, INA226.getPower_mW() / 1000.0f);
|
|
}
|
|
static bool query_ina226_voltage(uint8_t, float& voltage) {
|
|
voltage = INA226.getBusVoltage();
|
|
return isfinite(voltage);
|
|
}
|
|
#endif
|
|
|
|
#if ENV_INCLUDE_MLX90614
|
|
static uint8_t init_mlx90614(TwoWire* wire, uint8_t addr) {
|
|
return MLX90614.begin(addr, wire) ? 2 : 0; // 2 channels: object temp, ambient temp
|
|
}
|
|
static void query_mlx90614(uint8_t ch, uint8_t sub_ch, CayenneLPP& lpp) {
|
|
if (sub_ch == 0)
|
|
lpp.addTemperature(ch, MLX90614.readObjectTempC());
|
|
else
|
|
lpp.addTemperature(ch, MLX90614.readAmbientTempC());
|
|
}
|
|
#endif
|
|
|
|
#if ENV_INCLUDE_VL53L0X
|
|
static uint8_t init_vl53l0x(TwoWire* wire, uint8_t addr) {
|
|
return VL53L0X.begin(addr, false, wire) ? 1 : 0;
|
|
}
|
|
static void query_vl53l0x(uint8_t ch, uint8_t, CayenneLPP& lpp) {
|
|
VL53L0X_RangingMeasurementData_t measure;
|
|
VL53L0X.rangingTest(&measure, false);
|
|
lpp.addDistance(ch, measure.RangeStatus != 4 ? measure.RangeMilliMeter / 1000.0f : 0.0f);
|
|
}
|
|
#endif
|
|
|
|
#ifdef ENV_INCLUDE_BMP085
|
|
static uint8_t init_bmp085(TwoWire* wire, uint8_t) {
|
|
return BMP085.begin(0, wire) ? 1 : 0; // mode 0 = ULTRALOWPOWER
|
|
}
|
|
static void query_bmp085(uint8_t ch, uint8_t, CayenneLPP& lpp) {
|
|
lpp.addTemperature(ch, BMP085.readTemperature());
|
|
lpp.addBarometricPressure(ch, BMP085.readPressure() / 100);
|
|
lpp.addAltitude(ch, BMP085.readAltitude(TELEM_BMP085_SEALEVELPRESSURE_HPA * 100));
|
|
}
|
|
#endif
|
|
|
|
#if ENV_INCLUDE_RAK12035
|
|
static uint8_t init_rak12035(TwoWire* wire, uint8_t addr) {
|
|
// RAK12035 requires setup() before begin().
|
|
RAK12035.setup(*wire);
|
|
if (!RAK12035.begin(addr)) return 0;
|
|
#ifdef ENABLE_RAK12035_CALIBRATION
|
|
return 2; // moisture channel + calibration channel
|
|
#else
|
|
return 1;
|
|
#endif
|
|
}
|
|
static void query_rak12035(uint8_t ch, uint8_t sub_ch, CayenneLPP& lpp) {
|
|
if (sub_ch == 0) {
|
|
lpp.addTemperature(ch, RAK12035.get_sensor_temperature());
|
|
lpp.addPercentage(ch, RAK12035.get_sensor_moisture());
|
|
} else {
|
|
#ifdef ENABLE_RAK12035_CALIBRATION
|
|
float cap = RAK12035.get_sensor_capacitance();
|
|
float wet = RAK12035.get_humidity_full();
|
|
float dry = RAK12035.get_humidity_zero();
|
|
lpp.addFrequency(ch, cap);
|
|
lpp.addTemperature(ch, wet);
|
|
lpp.addPower(ch, dry);
|
|
if (cap > dry) RAK12035.set_humidity_zero(cap);
|
|
if (cap < wet) RAK12035.set_humidity_full(cap);
|
|
#endif
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#if ENV_INCLUDE_BME680_BSEC
|
|
static void bsec_load_state() {
|
|
using namespace Adafruit_LittleFS_Namespace;
|
|
File f = InternalFS.open(BSEC_STATE_FILE, FILE_O_READ);
|
|
if (!f) return;
|
|
uint8_t state[BSEC_MAX_STATE_BLOB_SIZE];
|
|
f.read(state, BSEC_MAX_STATE_BLOB_SIZE);
|
|
f.close();
|
|
bsec_iaq.setState(state);
|
|
}
|
|
|
|
static void bsec_save_state() {
|
|
using namespace Adafruit_LittleFS_Namespace;
|
|
uint8_t state[BSEC_MAX_STATE_BLOB_SIZE];
|
|
bsec_iaq.getState(state);
|
|
InternalFS.remove(BSEC_STATE_FILE);
|
|
File f = InternalFS.open(BSEC_STATE_FILE, FILE_O_WRITE);
|
|
if (!f) return;
|
|
f.write(state, BSEC_MAX_STATE_BLOB_SIZE);
|
|
f.close();
|
|
}
|
|
|
|
static uint8_t init_bme680_bsec(TwoWire* wire, uint8_t addr) {
|
|
bsec_iaq.begin(addr, *wire);
|
|
if (bsec_iaq.bsecStatus != BSEC_OK) return 0;
|
|
|
|
bsec_iaq.setConfig(bsec_config_iaq);
|
|
if (bsec_iaq.bsecStatus != BSEC_OK) return 0;
|
|
|
|
bsec_virtual_sensor_t outputs[] = {
|
|
BSEC_OUTPUT_IAQ,
|
|
BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE,
|
|
BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY,
|
|
BSEC_OUTPUT_RAW_PRESSURE,
|
|
BSEC_OUTPUT_STABILIZATION_STATUS,
|
|
BSEC_OUTPUT_RUN_IN_STATUS,
|
|
};
|
|
bsec_iaq.updateSubscription(outputs, 6, BSEC_SAMPLE_RATE_LP);
|
|
if (bsec_iaq.bsecStatus != BSEC_OK) return 0;
|
|
|
|
bsec_load_state();
|
|
bsec_active = true;
|
|
return 1;
|
|
}
|
|
|
|
static void query_bme680_bsec(uint8_t ch, uint8_t, CayenneLPP& lpp) {
|
|
if (!bsec_data_ready) return;
|
|
lpp.addTemperature(ch, bsec_temperature);
|
|
lpp.addRelativeHumidity(ch, bsec_humidity);
|
|
lpp.addBarometricPressure(ch, bsec_pressure_hpa);
|
|
lpp.addAltitude(ch, 44330.0f * (1.0f - powf(bsec_pressure_hpa / (float)TELEM_BME680_SEALEVELPRESSURE_HPA, 0.1903f)));
|
|
lpp.addGenericSensor(ch, (uint16_t)bsec_iaq_val);
|
|
lpp.addAnalogInput(ch, (float)bsec_accuracy);
|
|
}
|
|
#endif
|
|
|
|
// ============================================================
|
|
// Sensor descriptor table
|
|
//
|
|
// Each entry maps an I2C address to a sensor's init and query
|
|
// functions. Only entries whose ENV_INCLUDE_* guard is defined
|
|
// are compiled in. The sentinel at the end keeps the array
|
|
// non-empty regardless of which sensors are enabled.
|
|
//
|
|
// Bosch BMP/BME SDO selects 0x76 or 0x77; probe both.
|
|
//
|
|
// Ordering here determines channel assignment at runtime:
|
|
// the first detected+initialized sensor gets channel 2, the
|
|
// next gets channel 3, and so on.
|
|
// ============================================================
|
|
|
|
struct SensorDef {
|
|
uint32_t address; // preserve invalid build overrides until validation
|
|
const char* name;
|
|
uint8_t (*init)(TwoWire* wire, uint8_t address);
|
|
void (*query)(uint8_t channel, uint8_t sub_channel, CayenneLPP& telemetry);
|
|
bool (*query_voltage)(uint8_t sub_channel, float& voltage);
|
|
};
|
|
|
|
#define TELEM_BOSCH_ALT_ADDR(addr) \
|
|
((addr) == 0x76 ? 0x77U : ((addr) == 0x77 ? 0x76U : (addr)))
|
|
|
|
static const SensorDef SENSOR_TABLE[] = {
|
|
#if ENV_INCLUDE_AHTX0
|
|
{ TELEM_AHTX_ADDRESS, "AHT10/AHT20", init_ahtx0, query_ahtx0, NULL },
|
|
#endif
|
|
#ifdef ENV_INCLUDE_BME680
|
|
{ TELEM_BME680_ADDRESS, "BME680", init_bme680, query_bme680, NULL },
|
|
{ TELEM_BOSCH_ALT_ADDR(TELEM_BME680_ADDRESS), "BME680", init_bme680, query_bme680, NULL },
|
|
#endif
|
|
#if ENV_INCLUDE_BME680_BSEC
|
|
{ TELEM_BME680_ADDRESS, "BME680+BSEC", init_bme680_bsec, query_bme680_bsec, NULL },
|
|
{ TELEM_BOSCH_ALT_ADDR(TELEM_BME680_ADDRESS), "BME680+BSEC", init_bme680_bsec, query_bme680_bsec, NULL },
|
|
#endif
|
|
#if ENV_INCLUDE_BME280
|
|
{ TELEM_BME280_ADDRESS, "BME280", init_bme280, query_bme280, NULL },
|
|
{ TELEM_BOSCH_ALT_ADDR(TELEM_BME280_ADDRESS), "BME280", init_bme280, query_bme280, NULL },
|
|
#endif
|
|
#if ENV_INCLUDE_BMP280
|
|
{ TELEM_BMP280_ADDRESS, "BMP280", init_bmp280, query_bmp280, NULL },
|
|
{ TELEM_BOSCH_ALT_ADDR(TELEM_BMP280_ADDRESS), "BMP280", init_bmp280, query_bmp280, NULL },
|
|
#endif
|
|
#if ENV_INCLUDE_SHTC3
|
|
{ 0x70, "SHTC3", init_shtc3, query_shtc3, NULL },
|
|
#endif
|
|
#if ENV_INCLUDE_SHT4X
|
|
{ TELEM_SHT4X_ADDRESS, "SHT4X", init_sht4x, query_sht4x, NULL },
|
|
#endif
|
|
#if ENV_INCLUDE_LPS22HB
|
|
{ 0x5C, "LPS22HB", init_lps22hb, query_lps22hb, NULL },
|
|
#endif
|
|
#if ENV_INCLUDE_INA3221
|
|
{ TELEM_INA3221_ADDRESS, "INA3221", init_ina3221, query_ina3221,
|
|
query_ina3221_voltage },
|
|
#endif
|
|
#if ENV_INCLUDE_INA219
|
|
{ TELEM_INA219_ADDRESS, "INA219", init_ina219, query_ina219,
|
|
query_ina219_voltage },
|
|
#endif
|
|
#if ENV_INCLUDE_INA260
|
|
{ TELEM_INA260_ADDRESS, "INA260", init_ina260, query_ina260,
|
|
query_ina260_voltage },
|
|
#endif
|
|
#if ENV_INCLUDE_INA226
|
|
{ TELEM_INA226_ADDRESS, "INA226", init_ina226, query_ina226,
|
|
query_ina226_voltage },
|
|
#endif
|
|
#if ENV_INCLUDE_MLX90614
|
|
{ TELEM_MLX90614_ADDRESS,"MLX90614", init_mlx90614, query_mlx90614, NULL },
|
|
#endif
|
|
#if ENV_INCLUDE_VL53L0X
|
|
{ TELEM_VL53L0X_ADDRESS, "VL53L0X", init_vl53l0x, query_vl53l0x, NULL },
|
|
#endif
|
|
#ifdef ENV_INCLUDE_BMP085
|
|
{ 0x77, "BMP085", init_bmp085, query_bmp085, NULL },
|
|
#endif
|
|
#if ENV_INCLUDE_RAK12035
|
|
{ TELEM_RAK12035_ADDRESS,"RAK12035", init_rak12035, query_rak12035, NULL },
|
|
#endif
|
|
{ 0, nullptr, nullptr, nullptr, nullptr } // sentinel keeps array non-empty
|
|
};
|
|
|
|
#undef TELEM_BOSCH_ALT_ADDR
|
|
|
|
static const size_t SENSOR_TABLE_SIZE = (sizeof(SENSOR_TABLE) / sizeof(SENSOR_TABLE[0])) - 1;
|
|
|
|
// ============================================================
|
|
// begin() - initialize an optional board GPS, then scan the I2C bus and invoke
|
|
// only table-driven sensor initializers whose address ACKed. GPS uses a
|
|
// device-specific probe outside SENSOR_TABLE; an address it positively claims
|
|
// is not handed to another driver.
|
|
// ============================================================
|
|
|
|
bool EnvironmentSensorManager::begin() {
|
|
_active_sensor_count = 0;
|
|
#if defined(RAK_BOARD) && !defined(RAK_WISMESH_TAG)
|
|
_wisblock_power.setDiscoveryActive(true);
|
|
applyWisBlockRailPower();
|
|
#endif
|
|
|
|
#if ENV_INCLUDE_GPS
|
|
#ifdef RAK_WISBLOCK_GPS
|
|
// A volatile read anchors the externally linked marker even under LTO.
|
|
const volatile char* capability_marker =
|
|
meshcore_capability_rak_wisblock_gps;
|
|
(void)*capability_marker;
|
|
rakGPSInit();
|
|
#else
|
|
initBasicGPS();
|
|
#endif
|
|
#endif
|
|
|
|
#if ENV_HAS_SECONDARY_I2C
|
|
#ifdef NRF52_PLATFORM
|
|
if (!isValidNrfI2cPinPair(ENV_PIN_SDA, ENV_PIN_SCL)) {
|
|
MESH_DEBUG_PRINTLN("Second I2C skipped: invalid SDA/SCL pin pair");
|
|
return finishSensorDiscovery();
|
|
}
|
|
Wire1.setPins(static_cast<uint8_t>(ENV_PIN_SDA),
|
|
static_cast<uint8_t>(ENV_PIN_SCL));
|
|
Wire1.begin();
|
|
Wire1.setClock(100000);
|
|
#else
|
|
Wire1.begin(ENV_PIN_SDA, ENV_PIN_SCL, 100000);
|
|
#endif
|
|
MESH_DEBUG_PRINTLN("Second I2C initialized on pins SDA: %d SCL: %d", ENV_PIN_SDA, ENV_PIN_SCL);
|
|
#endif
|
|
|
|
// Avoid touching a shared I2C bus when no environmental drivers were built.
|
|
if (SENSOR_TABLE_SIZE == 0) {
|
|
return finishSensorDiscovery();
|
|
}
|
|
|
|
if (!ensureI2cBusReleased(TELEM_WIRE)) {
|
|
MESH_DEBUG_PRINTLN(
|
|
"I2C discovery skipped: bus pins unavailable or SDA/SCL remained low");
|
|
return finishSensorDiscovery();
|
|
}
|
|
|
|
// Probe only unique addresses compiled into this image before invoking any
|
|
// table-driven sensor library. The old all-address scan performed 112
|
|
// blocking Wire transactions even when only a few drivers were present.
|
|
bool detected[128] = {};
|
|
bool probed[128] = {};
|
|
for (size_t i = 0; i < SENSOR_TABLE_SIZE; i++) {
|
|
const uint32_t configured_address = SENSOR_TABLE[i].address;
|
|
if (!mesh::isValidI2cPeripheralAddress(configured_address)) {
|
|
MESH_DEBUG_PRINTLN("Skipping invalid I2C sensor address %lX",
|
|
(unsigned long)configured_address);
|
|
continue;
|
|
}
|
|
const uint8_t address = static_cast<uint8_t>(configured_address);
|
|
if (!probed[address]) {
|
|
probed[address] = true;
|
|
detected[address] = i2c_probe(*TELEM_WIRE, address);
|
|
}
|
|
}
|
|
|
|
// Walk the sensor table and initialize only detected devices.
|
|
for (size_t i = 0; i < SENSOR_TABLE_SIZE && _active_sensor_count < MAX_ACTIVE_SENSORS; i++) {
|
|
const SensorDef& def = SENSOR_TABLE[i];
|
|
if (!mesh::isValidI2cPeripheralAddress(def.address)) continue;
|
|
const uint8_t address = static_cast<uint8_t>(def.address);
|
|
#ifdef RAK_WISBLOCK_GPS
|
|
if (mesh::shouldSkipSensorAtClaimedGpsAddress(
|
|
i2cGPSFlag, TELEM_WIRE == &Wire,
|
|
address, TELEM_RAK12500_ADDRESS)) {
|
|
MESH_DEBUG_PRINTLN(
|
|
"Skipping %s at I2C address %02X: address is claimed by RAK12500 GPS",
|
|
def.name, address);
|
|
continue;
|
|
}
|
|
#endif
|
|
// One static driver instance per type: an alternate address is a fallback, not a second device.
|
|
bool already_active = false;
|
|
for (int j = 0; j < _active_sensor_count; j++) {
|
|
if (_active_sensors[j].query == def.query) { already_active = true; break; }
|
|
}
|
|
if (already_active) continue;
|
|
if (!detected[address]) {
|
|
MESH_DEBUG_PRINTLN("%s not detected at I2C address %02X", def.name, address);
|
|
continue;
|
|
}
|
|
uint8_t n = def.init(TELEM_WIRE, address);
|
|
if (n == 0) {
|
|
MESH_DEBUG_PRINTLN("%s found at %02X but failed to initialize", def.name, address);
|
|
continue;
|
|
}
|
|
MESH_DEBUG_PRINTLN("Found %s at address: %02X", def.name, address);
|
|
detected[address] = false; // consumed; later entries must not re-claim this device
|
|
for (uint8_t sub = 0; sub < n && _active_sensor_count < MAX_ACTIVE_SENSORS; sub++) {
|
|
_active_sensors[_active_sensor_count++] = {
|
|
def.query, def.query_voltage, sub
|
|
};
|
|
}
|
|
}
|
|
|
|
return finishSensorDiscovery();
|
|
}
|
|
|
|
// ============================================================
|
|
// querySensors() - GPS stays on channel 1; each active sensor
|
|
// gets the next available channel in the order it was
|
|
// initialized.
|
|
// ============================================================
|
|
|
|
bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) {
|
|
next_available_channel = TELEM_CHANNEL_SELF + 1;
|
|
|
|
#if ENV_INCLUDE_GPS
|
|
queryGpsTelemetry(requester_permissions, telemetry);
|
|
#endif
|
|
|
|
#if defined(RAK_BOARD) && !defined(RAK_WISMESH_TAG)
|
|
if (!_wisblock_power.railEnabled()) return true;
|
|
#endif
|
|
|
|
if (requester_permissions & TELEM_PERM_ENVIRONMENT) {
|
|
for (int i = 0; i < _active_sensor_count; i++) {
|
|
_active_sensors[i].query(next_available_channel, _active_sensors[i].sub_channel, telemetry);
|
|
next_available_channel++;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
uint8_t EnvironmentSensorManager::getVoltageSensorChannels(
|
|
uint8_t channels[], uint8_t capacity) const {
|
|
uint8_t count = 0;
|
|
for (int i = 0; i < _active_sensor_count; i++) {
|
|
if (_active_sensors[i].query_voltage == NULL) continue;
|
|
if (count < capacity && channels != NULL) {
|
|
channels[count] = (uint8_t)(TELEM_CHANNEL_SELF + 1 + i);
|
|
}
|
|
count++;
|
|
}
|
|
return count < capacity ? count : capacity;
|
|
}
|
|
|
|
uint8_t EnvironmentSensorManager::queryVoltageSensors(
|
|
VoltageSensorReading readings[], uint8_t capacity) {
|
|
#if defined(RAK_BOARD) && !defined(RAK_WISMESH_TAG)
|
|
if (!_wisblock_power.railEnabled()) return 0;
|
|
#endif
|
|
uint8_t count = 0;
|
|
for (int i = 0; i < _active_sensor_count && count < capacity; i++) {
|
|
const ActiveSensor& sensor = _active_sensors[i];
|
|
if (sensor.query_voltage == NULL) continue;
|
|
VoltageSensorReading& reading = readings[count++];
|
|
reading.channel = (uint8_t)(TELEM_CHANNEL_SELF + 1 + i);
|
|
reading.voltage = 0.0f;
|
|
reading.valid = sensor.query_voltage(sensor.sub_channel,
|
|
reading.voltage)
|
|
&& isfinite(reading.voltage);
|
|
}
|
|
return count;
|
|
}
|
|
int EnvironmentSensorManager::getNumSettings() const {
|
|
int settings = 0;
|
|
#if ENV_INCLUDE_GPS
|
|
if (gps_detected) settings++; // only show GPS setting if GPS is detected
|
|
#endif
|
|
return settings;
|
|
}
|
|
|
|
const char* EnvironmentSensorManager::getSettingName(int i) const {
|
|
int settings = 0;
|
|
#if ENV_INCLUDE_GPS
|
|
if (gps_detected && i == settings++) {
|
|
return "gps";
|
|
}
|
|
#endif
|
|
return NULL;
|
|
}
|
|
|
|
const char* EnvironmentSensorManager::getSettingValue(int i) const {
|
|
int settings = 0;
|
|
#if ENV_INCLUDE_GPS
|
|
if (gps_detected && i == settings++) {
|
|
return isGpsTelemetryUserEnabled() ? "1" : "0";
|
|
}
|
|
#endif
|
|
return NULL;
|
|
}
|
|
|
|
bool EnvironmentSensorManager::setSettingValue(const char* name, const char* value) {
|
|
#if ENV_INCLUDE_GPS
|
|
bool gps_setting_available = gps_detected;
|
|
#if (defined(ESP32_PLATFORM) || defined(NRF52_PLATFORM)) && !defined(RAK_WISBLOCK_GPS)
|
|
gps_setting_available = gps_setting_available || gps_discovery_pending;
|
|
#endif
|
|
if (gps_setting_available && strcmp(name, "gps") == 0) {
|
|
#if (defined(ESP32_PLATFORM) || defined(NRF52_PLATFORM)) && !defined(RAK_WISBLOCK_GPS)
|
|
// Preferences load before a cold receiver finishes its bounded probe.
|
|
// Retain intent so late discovery cannot replace a saved off/on choice.
|
|
gps_discovery_preference_known = true;
|
|
#endif
|
|
bool enabled = strcmp(value, "0") != 0;
|
|
bool was_active = gps_active;
|
|
_location->setGPSPowerSaving(enabled && powersaving_enabled);
|
|
setGpsTelemetryUserEnabled(enabled);
|
|
if (enabled && powersaving_enabled && was_active) {
|
|
armGpsPowerSavingCycle();
|
|
}
|
|
return true;
|
|
}
|
|
#endif
|
|
return SensorManager::setSettingValue(name, value);
|
|
}
|
|
|
|
void EnvironmentSensorManager::setPowerSavingEnabled(bool enabled) {
|
|
if (powersaving_enabled == enabled) return;
|
|
powersaving_enabled = enabled;
|
|
|
|
#if ENV_INCLUDE_GPS
|
|
if (!gps_detected) return;
|
|
if (isTrackerGpsModeEnabled()) {
|
|
_location->setGPSPowerSaving(false);
|
|
return;
|
|
}
|
|
bool gps_user_enabled = isGpsTelemetryUserEnabled();
|
|
_location->setGPSPowerSaving(enabled && gps_user_enabled);
|
|
if (!gps_user_enabled) return;
|
|
|
|
if (enabled) {
|
|
if (gps_active) {
|
|
armGpsPowerSavingCycle();
|
|
} else {
|
|
start_gps();
|
|
}
|
|
} else if (!gps_active) {
|
|
// Manual GPS-on means continuous operation when node power saving is off.
|
|
start_gps();
|
|
}
|
|
#endif
|
|
}
|
|
|
|
#if ENV_INCLUDE_GPS
|
|
void EnvironmentSensorManager::startGpsSerialTransport() {
|
|
if (gps_serial_open) return;
|
|
Serial1.setPins(PIN_GPS_TX, PIN_GPS_RX);
|
|
#ifdef GPS_BAUD_RATE
|
|
Serial1.begin(GPS_BAUD_RATE);
|
|
#else
|
|
Serial1.begin(9600);
|
|
#endif
|
|
gps_serial_open = true;
|
|
}
|
|
|
|
void EnvironmentSensorManager::stopGpsSerialTransport() {
|
|
// Logical reservation survives GPS-off and bridge handoff. Only this state
|
|
// proves that GPS opened the driver: nRF52 Uart::end() can hang unopened.
|
|
if (!gps_serial_open) return;
|
|
gps_serial_open = false;
|
|
#if defined(ESP32_PLATFORM)
|
|
// A board-specific provider may already have closed the ESP32 driver while
|
|
// switching its receiver rail. Its bool conversion reports that condition.
|
|
if (Serial1)
|
|
#endif
|
|
Serial1.end();
|
|
// UARTE/ESP32 teardown need not restore GPIO configuration. Release BOTH
|
|
// pads before receiver power changes; several boards name them from GPS.
|
|
#if defined(PIN_GPS_TX) && PIN_GPS_TX >= 0
|
|
pinMode(PIN_GPS_TX, INPUT);
|
|
#endif
|
|
#if defined(PIN_GPS_RX) && PIN_GPS_RX >= 0
|
|
pinMode(PIN_GPS_RX, INPUT);
|
|
#endif
|
|
}
|
|
|
|
#if (defined(ESP32_PLATFORM) || defined(NRF52_PLATFORM)) && !defined(RAK_WISBLOCK_GPS)
|
|
void EnvironmentSensorManager::finishBasicGpsDiscovery(bool found) {
|
|
gps_discovery_pending = false;
|
|
gps_detected = found;
|
|
gps_serial_transport = found;
|
|
if (found) {
|
|
MESH_DEBUG_PRINTLN("GPS detected");
|
|
#ifdef PERSISTANT_GPS
|
|
// Keep the established always-on default only when no loaded preference
|
|
// has superseded it. The receiver is already awake from the probe.
|
|
if (!gps_discovery_preference_known || isGpsTelemetryUserEnabled()) {
|
|
gps_active = true;
|
|
if (!gps_discovery_preference_known) setGpsTelemetryUserEnabled(true);
|
|
_location->setGPSPowerSaving(powersaving_enabled);
|
|
armGpsPowerSavingCycle();
|
|
return;
|
|
}
|
|
#endif
|
|
} else {
|
|
MESH_DEBUG_PRINTLN("No GPS detected");
|
|
}
|
|
stopGpsSerialTransport();
|
|
_location->stop();
|
|
gps_active = false;
|
|
if (found && isGpsTelemetryUserEnabled()) {
|
|
_location->setGPSPowerSaving(powersaving_enabled);
|
|
start_gps();
|
|
}
|
|
}
|
|
|
|
void EnvironmentSensorManager::serviceBasicGpsDiscovery() {
|
|
// A bridge may own Serial1 until it explicitly releases the receiver. Keep
|
|
// its pending probe reserved, but never poll or power it while blocked.
|
|
if (!gps_discovery_pending || gps_serial_transport_blocked) return;
|
|
#ifdef ENV_SKIP_GPS_DETECT
|
|
const bool found = true;
|
|
#else
|
|
const bool found = Serial1.available() > 0;
|
|
#endif
|
|
// Unsigned elapsed time keeps the same five-second cold-start window even
|
|
// when the millisecond clock wraps. No startup or command loop waits here.
|
|
if (!found && static_cast<uint32_t>(millis() - gps_discovery_started_at) < 5000UL) return;
|
|
finishBasicGpsDiscovery(found);
|
|
}
|
|
#endif
|
|
|
|
void EnvironmentSensorManager::initBasicGPS() {
|
|
|
|
// A repeated discovery pass must not steal a UART which an active bridge
|
|
// already owns. The bridge release path explicitly restores availability.
|
|
if (gps_serial_transport_blocked) {
|
|
MESH_DEBUG_PRINTLN("GPS discovery skipped: serial transport is owned by bridge");
|
|
return;
|
|
}
|
|
|
|
resetGpsTelemetryTransportState();
|
|
gps_serial_transport = false;
|
|
gps_serial_transport_blocked = false;
|
|
#if (defined(ESP32_PLATFORM) || defined(NRF52_PLATFORM)) && !defined(RAK_WISBLOCK_GPS)
|
|
gps_detected = false;
|
|
gps_active = false;
|
|
#endif
|
|
|
|
startGpsSerialTransport();
|
|
|
|
// Try to detect if GPS is physically connected to determine if we should expose the setting
|
|
_location->begin();
|
|
_location->reset();
|
|
|
|
#ifndef PIN_GPS_EN
|
|
MESH_DEBUG_PRINTLN("No GPS wake/reset pin found for this board. Continuing on...");
|
|
#endif
|
|
|
|
#if (defined(ESP32_PLATFORM) || defined(NRF52_PLATFORM)) && !defined(RAK_WISBLOCK_GPS)
|
|
// Reserve the configured UART immediately, including while presence is
|
|
// still unknown. Continue discovery from loop() so an absent optional GPS
|
|
// cannot consume the website's entire five-second USB bootstrap deadline.
|
|
gps_serial_transport = true;
|
|
gps_discovery_pending = true;
|
|
gps_discovery_started_at = millis();
|
|
serviceBasicGpsDiscovery();
|
|
#else
|
|
// Cold-start receivers can take several seconds to emit their first NMEA
|
|
// data. Check immediately, then wait at most five seconds; an already awake
|
|
// receiver and the explicit bypass need no extra startup delay.
|
|
#ifdef ENV_SKIP_GPS_DETECT
|
|
gps_detected = true;
|
|
#else
|
|
gps_detected = (Serial1.available() > 0);
|
|
for (uint8_t attempt = 0; attempt < 20 && !gps_detected; attempt++) {
|
|
delay(250);
|
|
gps_detected = (Serial1.available() > 0);
|
|
}
|
|
#endif
|
|
|
|
if (gps_detected) {
|
|
gps_serial_transport = true;
|
|
MESH_DEBUG_PRINTLN("GPS detected");
|
|
#ifdef PERSISTANT_GPS
|
|
gps_active = true;
|
|
setGpsTelemetryUserEnabled(true);
|
|
return;
|
|
#endif
|
|
} else {
|
|
MESH_DEBUG_PRINTLN("No GPS detected");
|
|
}
|
|
stopGpsSerialTransport();
|
|
_location->stop();
|
|
gps_active = false; //Set GPS visibility off until setting is changed
|
|
#endif
|
|
}
|
|
#endif // ENV_INCLUDE_GPS
|
|
|
|
// These hooks also exist in no-GPS images. WisBlock slot power can still
|
|
// supply storage, Ethernet, or another peripheral without a GPS provider.
|
|
bool EnvironmentSensorManager::supportsSharedPeripheralMode() const {
|
|
#if defined(RAK_4631) && !defined(RAK_WISMESH_TAG)
|
|
return true;
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
#if defined(RAK_BOARD) && !defined(RAK_WISMESH_TAG)
|
|
void EnvironmentSensorManager::applyWisBlockRailPower() {
|
|
pinMode(WB_IO2, OUTPUT);
|
|
digitalWrite(WB_IO2, _wisblock_power.railEnabled() ? HIGH : LOW);
|
|
}
|
|
|
|
uint8_t EnvironmentSensorManager::detectWisBlockStorageHolds() {
|
|
uint8_t holds = 0;
|
|
#if defined(OTA_RAK_AUTO_STORE)
|
|
uint32_t rak_id = 0, w25_id = 0;
|
|
const uint8_t selected = mesh::ota::OtaStoreQspiNrf52::autoDetect();
|
|
mesh::ota::OtaStoreQspiNrf52::autoProbeIds(rak_id, w25_id);
|
|
// Unsupported but responding NOR parts must not lose their supply either.
|
|
// All-zero/all-one reads are the probe's absent-chip values.
|
|
if (selected != 0 || (rak_id != 0 && rak_id != 0xFFFFFFUL)
|
|
|| (w25_id != 0 && w25_id != 0xFFFFFFUL)) {
|
|
holds |= mesh::gps::ExternalStorage;
|
|
}
|
|
#elif defined(OTA_QSPI_SHARED_WISBLOCK_SPI) || (defined(OTA_SD_STORE) && !defined(OTA_RAK15002_SD_CACHE) && !defined(OTA_RAK15002_SD_STORE))
|
|
holds |= mesh::gps::ExternalStorage;
|
|
#endif
|
|
// Exact RAK15002 uses connector pin 5 (unswitched 3V3), not 3V3_S. Its
|
|
// archive must not accidentally keep a GPS-only sensor rail powered.
|
|
return holds;
|
|
}
|
|
#endif
|
|
|
|
void EnvironmentSensorManager::configureSharedPeripheralMode(bool shared) {
|
|
#if defined(RAK_BOARD) && !defined(RAK_WISMESH_TAG)
|
|
// Called once after boot discovery and validated preference loading. Live
|
|
// setters only save the next-boot choice; they never revoke active owners.
|
|
if (!_wisblock_power.configure(shared ? mesh::gps::WisBlockPeripheralMode::Shared
|
|
: mesh::gps::WisBlockPeripheralMode::GpsOnly,
|
|
detectWisBlockStorageHolds())) return;
|
|
_wisblock_power.setGpsActive(gps_active);
|
|
// Companion loads preferences before sensor discovery, whereas repeaters
|
|
// load them afterwards. Only finishSensorDiscovery releases that interlock.
|
|
applyWisBlockRailPower();
|
|
#else
|
|
(void)shared;
|
|
#endif
|
|
}
|
|
|
|
bool EnvironmentSensorManager::canRemoveSharedStoragePowerForDeepSleep() const {
|
|
#if defined(RAK_BOARD) && !defined(RAK_WISMESH_TAG)
|
|
// Only the boot-frozen, read-only ownership result is relevant. Do not
|
|
// re-probe NOR, cancel a stage, reset a store, or assume unknown WIP ended.
|
|
// Radio and Ethernet are separately quiesced by the terminal board hook.
|
|
const uint8_t stopped_owners = mesh::gps::RadioPower | mesh::gps::EthernetPower;
|
|
return _wisblock_power.isConfigured() && !_wisblock_power.discoveryActive()
|
|
&& (_wisblock_power.holds() & static_cast<uint8_t>(~stopped_owners)) == 0;
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
bool EnvironmentSensorManager::isGpsPowered() const {
|
|
#ifdef RAK_WISBLOCK_GPS
|
|
#if defined(PIN_GPS_EN) && PIN_GPS_EN >= 0
|
|
return digitalRead(PIN_GPS_EN) == rakGpsControlActiveLevel();
|
|
#else
|
|
return digitalRead(WB_IO2) == HIGH;
|
|
#endif
|
|
#else
|
|
return SensorManager::isGpsPowered();
|
|
#endif
|
|
}
|
|
|
|
#if ENV_INCLUDE_GPS
|
|
// gps code for rak might be moved to MicroNMEALoactionProvider
|
|
// or make a new location provider ...
|
|
#ifdef RAK_WISBLOCK_GPS
|
|
void EnvironmentSensorManager::rakGPSInit() {
|
|
#if defined(OTA_RAK_AUTO_STORE) && defined(RAK_BOARD)
|
|
// The 2.54 mm header W25Q16 uses the UART1 and PPS pads. Probe before
|
|
// starting GPS discovery so UART1 never drives a NOR data/clock wire.
|
|
if (mesh::ota::OtaStoreQspiNrf52::headerW25Detected()) {
|
|
gps_active = false;
|
|
gps_detected = false;
|
|
gps_serial_transport = false;
|
|
i2cGPSFlag = false;
|
|
serialGPSFlag = false;
|
|
return;
|
|
}
|
|
#endif
|
|
// Preserve the established owner across repeated begin() calls. Clearing
|
|
// this flag and starting Serial1 here would silently take UART1 back from an
|
|
// active bridge.
|
|
if (gps_serial_transport_blocked) {
|
|
MESH_DEBUG_PRINTLN("RAK GPS discovery skipped: UART1 is owned by bridge");
|
|
return;
|
|
}
|
|
|
|
// A repeated begin() must not retain ownership or a provider from an older
|
|
// hardware probe.
|
|
i2cGPSFlag = false;
|
|
serialGPSFlag = false;
|
|
resetGpsTelemetryTransportState();
|
|
gpsResetPin = static_cast<uint32_t>(-1);
|
|
gps_active = false;
|
|
gps_detected = false;
|
|
gps_serial_transport = false;
|
|
gps_serial_transport_blocked = false;
|
|
_location = _configured_location;
|
|
|
|
startGpsSerialTransport();
|
|
|
|
// A RAK-derived board with a real GPS-enable pin must use it. Stock WisBlock
|
|
// bases have no dedicated GPS switch, so their fallback is shared WB_IO2.
|
|
#if defined(PIN_GPS_EN) && PIN_GPS_EN >= 0
|
|
const uint8_t gps_control_pin = PIN_GPS_EN;
|
|
const bool shared_power_rail = false;
|
|
#else
|
|
const uint8_t gps_control_pin = WB_IO2;
|
|
const bool shared_power_rail = true;
|
|
#endif
|
|
|
|
if (gpsIsAwake(gps_control_pin, shared_power_rail)) {
|
|
// WB_IO2 is not an independently owned GPS pin. A provider must never
|
|
// lower it during reset/begin/stop behind the shared-power policy's back.
|
|
_location->setPinEn(shared_power_rail ? -1 : gps_control_pin);
|
|
if (i2cGPSFlag) stopGpsSerialTransport();
|
|
} else {
|
|
MESH_DEBUG_PRINTLN("No GPS found");
|
|
gps_active = false;
|
|
gps_detected = false;
|
|
stopGpsSerialTransport();
|
|
return;
|
|
}
|
|
|
|
// Keep discovery power available for the remaining slot probes. Loading the
|
|
// saved mode and GPS preference later performs the initial stop, without
|
|
// ever manufacturing a GPS-on preference from a successful UART probe.
|
|
}
|
|
|
|
bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin,
|
|
bool shared_power_rail) {
|
|
if (!shared_power_rail) {
|
|
setRakGpsControl(ioPin, false, false);
|
|
delay(500);
|
|
}
|
|
setRakGpsControl(ioPin, shared_power_rail, true);
|
|
// give the receiver time to become responsive
|
|
delay(500);
|
|
|
|
// Prove the UART device with a complete checksum-valid GPS sentence. This
|
|
// must precede I2C probing so a UART GPS can coexist with INA3221 at 0x42.
|
|
if (serialHasValidGpsSentence(Serial1, RAK_UART_GPS_PROBE_TIMEOUT_MS)) {
|
|
MESH_DEBUG_PRINTLN("RAK12501 UART GPS identified with pin %i", ioPin);
|
|
gpsResetPin = ioPin;
|
|
serialGPSFlag = true;
|
|
gps_serial_transport = true;
|
|
gps_active = true;
|
|
gps_detected = true;
|
|
return true;
|
|
}
|
|
|
|
if (!ensureI2cBusReleased(&Wire)) {
|
|
MESH_DEBUG_PRINTLN(
|
|
"Skipping RAK12500 probe: I2C pins unavailable or bus remains low");
|
|
setRakGpsControl(ioPin, shared_power_rail, false);
|
|
return false;
|
|
}
|
|
|
|
const uint32_t configured_gps_address =
|
|
static_cast<uint32_t>(TELEM_RAK12500_ADDRESS);
|
|
if (!mesh::isValidI2cPeripheralAddress(configured_gps_address)) {
|
|
MESH_DEBUG_PRINTLN("Skipping invalid RAK12500 I2C address");
|
|
setRakGpsControl(ioPin, shared_power_rail, false);
|
|
return false;
|
|
}
|
|
const uint8_t gps_i2c_address =
|
|
static_cast<uint8_t>(configured_gps_address);
|
|
bool probe_i2c_gps = true;
|
|
#if ENV_INCLUDE_INA3221
|
|
// Inspect the actual device at the GPS address. This must not depend on the
|
|
// configured INA address or telemetry bus: an unstrapped INA3221 can still
|
|
// physically remain at 0x42 when firmware expects it at 0x43.
|
|
const mesh::I2cIdentityProbeResult ina_identity =
|
|
probeIna3221Identity(&Wire, gps_i2c_address);
|
|
probe_i2c_gps = mesh::shouldProbeI2cGps(ina_identity);
|
|
if (!probe_i2c_gps) {
|
|
if (ina_identity == mesh::I2cIdentityProbeResult::Match) {
|
|
MESH_DEBUG_PRINTLN(
|
|
"Skipping RAK12500 probe at I2C address %02X: INA3221 identity confirmed",
|
|
gps_i2c_address);
|
|
} else {
|
|
MESH_DEBUG_PRINTLN(
|
|
"Skipping RAK12500 probe at I2C address %02X: INA identity read was inconclusive",
|
|
gps_i2c_address);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// After two successful non-INA manufacturer reads, use the u-blox-specific
|
|
// probe. No software can make two physical devices at one address safe.
|
|
if (probe_i2c_gps
|
|
&& ublox_GNSS.begin(Wire, gps_i2c_address) == true
|
|
&& configureRakUbloxReceiver()) {
|
|
MESH_DEBUG_PRINTLN("RAK12500 GPS init correctly with pin %i", ioPin);
|
|
ublox_GNSS.saveConfigSelective(VAL_CFG_SUBSEC_IOPORT);
|
|
gpsResetPin = ioPin;
|
|
i2cGPSFlag = true;
|
|
gps_serial_transport = false;
|
|
gps_active = true;
|
|
gps_detected = true;
|
|
|
|
RAK12500_provider.setRTCClock(_location->getRTCClock());
|
|
RAK12500_provider.reset();
|
|
_location = &RAK12500_provider;
|
|
return true;
|
|
}
|
|
|
|
setRakGpsControl(ioPin, shared_power_rail, false);
|
|
MESH_DEBUG_PRINTLN("GPS did not init with this IO pin... try the next");
|
|
return false;
|
|
}
|
|
#endif
|
|
|
|
void EnvironmentSensorManager::armGpsPowerSavingCycle() {
|
|
if (!powersaving_enabled || !_location->getGPSPowerSaving()) return;
|
|
_location->syncTimeForPowerSavingCycle();
|
|
_location->setNextGPSOn(0);
|
|
_location->setNextSleep();
|
|
}
|
|
|
|
void EnvironmentSensorManager::start_gps() {
|
|
if (gps_active || gps_serial_transport_blocked) return;
|
|
#ifdef RAK_WISBLOCK_GPS
|
|
const bool was_powered = isGpsPowered();
|
|
_wisblock_power.setGpsActive(true);
|
|
applyWisBlockRailPower();
|
|
#if defined(PIN_GPS_EN) && PIN_GPS_EN >= 0
|
|
setRakGpsControl(PIN_GPS_EN, false, true);
|
|
#endif
|
|
// Supply first, UART second. A cold receiver must settle before its bus or
|
|
// serial transport is used, including a wake after full GPS-only shutdown.
|
|
if (!was_powered) delay(500);
|
|
if (i2cGPSFlag) {
|
|
if (!was_powered) {
|
|
if (!ensureI2cBusReleased(&Wire)
|
|
|| !ublox_GNSS.begin(Wire, TELEM_RAK12500_ADDRESS)
|
|
|| !configureRakUbloxReceiver()) {
|
|
// Do not spin on a failed cold wake on every mesh loop. Keep saved
|
|
// intent, fail this receiver closed, and allow rediscovery on reboot.
|
|
gps_detected = false;
|
|
_wisblock_power.setGpsActive(false);
|
|
applyWisBlockRailPower();
|
|
#if defined(PIN_GPS_EN) && PIN_GPS_EN >= 0
|
|
setRakGpsControl(PIN_GPS_EN, false, false);
|
|
#endif
|
|
_location->stopTimeSync();
|
|
MESH_DEBUG_PRINTLN("RAK12500 cold wake failed; GPS rediscovery needs reboot");
|
|
return;
|
|
}
|
|
} else {
|
|
// Shared mode retains the historical u-blox internal PSM. This is not
|
|
// a promise of full rail shutdown, nor does it silence a UART receiver.
|
|
ublox_GNSS.powerSaveMode(false);
|
|
}
|
|
}
|
|
#endif
|
|
if (gps_serial_transport) {
|
|
startGpsSerialTransport();
|
|
}
|
|
gps_active = true;
|
|
#ifdef RAK_WISBLOCK_GPS
|
|
_location->clearPositionFix();
|
|
#else
|
|
_location->begin();
|
|
_location->reset();
|
|
#endif
|
|
|
|
#ifndef PIN_GPS_EN
|
|
MESH_DEBUG_PRINTLN("Start GPS is N/A on this board. Actual GPS state unchanged");
|
|
#endif
|
|
|
|
armGpsPowerSavingCycle();
|
|
}
|
|
|
|
void EnvironmentSensorManager::stop_gps() {
|
|
if (!gps_active) return;
|
|
gps_active = false;
|
|
|
|
if (powersaving_enabled && _location->getGPSPowerSaving()) {
|
|
_location->stopTimeSync();
|
|
_location->setNextGPSOff(0);
|
|
_location->setNextWake();
|
|
}
|
|
|
|
// End the MCU UART before removing receiver power, never the reverse.
|
|
stopGpsSerialTransport();
|
|
#ifdef RAK_WISBLOCK_GPS
|
|
_wisblock_power.setGpsActive(false);
|
|
#if defined(PIN_GPS_EN) && PIN_GPS_EN >= 0
|
|
setRakGpsControl(PIN_GPS_EN, false, false);
|
|
#else
|
|
if (i2cGPSFlag && _wisblock_power.railEnabled()) {
|
|
ublox_GNSS.powerSaveMode(true);
|
|
}
|
|
#endif
|
|
applyWisBlockRailPower();
|
|
#else
|
|
_location->stop();
|
|
#endif
|
|
|
|
#ifndef PIN_GPS_EN
|
|
MESH_DEBUG_PRINTLN("Stop GPS is N/A on this board. Actual GPS state unchanged");
|
|
#endif
|
|
}
|
|
#endif // ENV_INCLUDE_GPS
|
|
|
|
bool EnvironmentSensorManager::gpsUsesSerialUart(uint8_t uart) const {
|
|
#if ENV_INCLUDE_GPS
|
|
#if (defined(ESP32_PLATFORM) || defined(NRF52_PLATFORM)) && !defined(RAK_WISBLOCK_GPS)
|
|
// The pending probe owns this UART before presence has been established.
|
|
return uart == 1 && gps_serial_transport;
|
|
#else
|
|
return uart == 1 && gps_detected && gps_serial_transport;
|
|
#endif
|
|
#else
|
|
(void)uart;
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
void EnvironmentSensorManager::shutdownGpsTransportForDeepSleep() {
|
|
#if ENV_INCLUDE_GPS
|
|
// gpsUsesSerialUart stays true while a bridge temporarily owns the UART.
|
|
// Do not end that owner's transport or touch an I2C/shared peripheral bus.
|
|
if (!gps_serial_transport_blocked) stopGpsSerialTransport();
|
|
#endif
|
|
}
|
|
|
|
bool EnvironmentSensorManager::gpsSerialTransportMayConflict(
|
|
uint8_t uart) const {
|
|
#if ENV_INCLUDE_GPS
|
|
#if defined(RAK_WISBLOCK_GPS) \
|
|
&& defined(WITH_RS232_BRIDGE_GPS_CONFLICT_UART)
|
|
// Silence is not proof of physical absence: a cold RAK12501/L76K can start
|
|
// emitting NMEA after the bounded boot probe. Reserve the declared shared
|
|
// connector in the merged image. Exact legacy Serial1 bridge targets omit
|
|
// GPS support and this reservation; the merged image retains UART2.
|
|
if (uart == WITH_RS232_BRIDGE_GPS_CONFLICT_UART) return true;
|
|
#endif
|
|
return gpsUsesSerialUart(uart);
|
|
#else
|
|
(void)uart;
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
bool EnvironmentSensorManager::gpsSerialTransportCanYield(uint8_t uart) const {
|
|
#if ENV_INCLUDE_GPS
|
|
if (!gpsUsesSerialUart(uart)) return false;
|
|
#if defined(RAK_WISBLOCK_GPS) && !(defined(PIN_GPS_EN) && PIN_GPS_EN >= 0)
|
|
// WB_IO2 is not independent isolation even in GPS-only mode: a bridge or
|
|
// future GPS request can repower the same rail. Reserve UART1 to avoid two
|
|
// TX drivers; use UART2 or a physically independent GPS enable instead.
|
|
return false;
|
|
#elif defined(RAK_WISBLOCK_GPS)
|
|
// This profile is allowed to remove power with its validated GPS control.
|
|
return true;
|
|
#endif
|
|
// Generic NMEA receivers may share the same serial API but are safe to hand
|
|
// off only when the provider has a real enable pin that stop() deasserts.
|
|
return _location != NULL && _location->getPinEn() >= 0;
|
|
#else
|
|
(void)uart;
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
bool EnvironmentSensorManager::setGpsSerialTransportBlocked(uint8_t uart,
|
|
bool blocked) {
|
|
#if ENV_INCLUDE_GPS
|
|
if (!gpsUsesSerialUart(uart)) return false;
|
|
if (gps_serial_transport_blocked == blocked) return true;
|
|
if (blocked && !gpsSerialTransportCanYield(uart)) return false;
|
|
|
|
if (blocked) {
|
|
// Cancel remote-query acquisition/hold state before releasing the UART.
|
|
setGpsTelemetryTransportAvailable(false);
|
|
gps_serial_transport_blocked = true;
|
|
#if (defined(ESP32_PLATFORM) || defined(NRF52_PLATFORM)) && !defined(RAK_WISBLOCK_GPS)
|
|
if (gps_discovery_pending) {
|
|
stopGpsSerialTransport();
|
|
_location->stop();
|
|
}
|
|
#endif
|
|
if (gps_active) stop_gps();
|
|
stopGpsSerialTransport();
|
|
} else {
|
|
gps_serial_transport_blocked = false;
|
|
#if (defined(ESP32_PLATFORM) || defined(NRF52_PLATFORM)) && !defined(RAK_WISBLOCK_GPS)
|
|
if (gps_discovery_pending) {
|
|
// The bridge's time cannot count as a GPS absence timeout. Reclaim only
|
|
// after release and give the newly awakened receiver its full window.
|
|
initBasicGPS();
|
|
return true;
|
|
}
|
|
#endif
|
|
// Restoring availability restarts the receiver only when the user's GPS
|
|
// preference is on. Otherwise a future authorized location query may
|
|
// acquire it on demand.
|
|
setGpsTelemetryTransportAvailable(true);
|
|
}
|
|
return true;
|
|
#else
|
|
(void)uart;
|
|
(void)blocked;
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
#if ENV_INCLUDE_GPS || defined(ENV_INCLUDE_BME680_BSEC)
|
|
void EnvironmentSensorManager::loop() {
|
|
|
|
#if ENV_INCLUDE_GPS
|
|
static unsigned long next_gps_update = 0;
|
|
unsigned long now = millis();
|
|
#if (defined(ESP32_PLATFORM) || defined(NRF52_PLATFORM)) && !defined(RAK_WISBLOCK_GPS)
|
|
serviceBasicGpsDiscovery();
|
|
#endif
|
|
loopGpsTelemetry(now);
|
|
|
|
if (powersaving_enabled && gps_detected && _location->getGPSPowerSaving()) {
|
|
unsigned long next_off = _location->getNextGPSOff();
|
|
unsigned long next_on = _location->getNextGPSOn();
|
|
if (gps_active && !gpsTelemetryReceiverRequired(now)
|
|
&& ((next_off != 0 && (long)(now - next_off) >= 0)
|
|
|| !_location->waitingTimeSync())) {
|
|
POWERSAVING_DEBUG_PRINTLN("GPS entering sleep");
|
|
stop_gps();
|
|
} else if (!gps_active && ((next_on != 0 && (long)(now - next_on) >= 0)
|
|
|| _location->waitingTimeSync())) {
|
|
POWERSAVING_DEBUG_PRINTLN("GPS waking");
|
|
start_gps();
|
|
}
|
|
}
|
|
|
|
if (gps_active) _location->loop();
|
|
|
|
if ((long)(now - next_gps_update) >= 0) {
|
|
if (gps_active) {
|
|
#ifdef RAK_WISBLOCK_GPS
|
|
if ((i2cGPSFlag || serialGPSFlag) && _location->isValid()) {
|
|
float gps_lat = ((float)_location->getLatitude()) / 1000000.0f;
|
|
float gps_lon = ((float)_location->getLongitude()) / 1000000.0f;
|
|
float gps_altitude = ((float)_location->getAltitude()) / 1000.0f;
|
|
node_lat = gps_lat;
|
|
node_lon = gps_lon;
|
|
MESH_DEBUG_PRINTLN("lat %f lon %f", node_lat, node_lon);
|
|
node_altitude = gps_altitude;
|
|
MESH_DEBUG_PRINTLN("lat %f lon %f alt %f", node_lat, node_lon, node_altitude);
|
|
processGpsTelemetryFix(gps_lat, gps_lon, gps_altitude, now);
|
|
}
|
|
#else
|
|
if (_location->isValid()) {
|
|
float gps_lat = ((float)_location->getLatitude()) / 1000000.0f;
|
|
float gps_lon = ((float)_location->getLongitude()) / 1000000.0f;
|
|
float gps_altitude = ((float)_location->getAltitude()) / 1000.0f;
|
|
node_lat = gps_lat;
|
|
node_lon = gps_lon;
|
|
MESH_DEBUG_PRINTLN("lat %f lon %f", node_lat, node_lon);
|
|
node_altitude = gps_altitude;
|
|
MESH_DEBUG_PRINTLN("lat %f lon %f alt %f", node_lat, node_lon, node_altitude);
|
|
processGpsTelemetryFix(gps_lat, gps_lon, gps_altitude, now);
|
|
}
|
|
#endif
|
|
|
|
}
|
|
next_gps_update = now + getGpsUpdateIntervalMillis();
|
|
}
|
|
#endif
|
|
#if ENV_INCLUDE_BME680_BSEC
|
|
#if defined(RAK_BOARD) && !defined(RAK_WISMESH_TAG)
|
|
if (!_wisblock_power.railEnabled()) return;
|
|
#endif
|
|
if (bsec_active && bsec_iaq.run()) {
|
|
uint8_t prev_accuracy = bsec_accuracy;
|
|
bsec_temperature = bsec_iaq.temperature;
|
|
bsec_humidity = bsec_iaq.humidity;
|
|
bsec_pressure_hpa = bsec_iaq.pressure / 100.0f;
|
|
bsec_iaq_val = bsec_iaq.iaq;
|
|
bsec_accuracy = bsec_iaq.iaqAccuracy;
|
|
bsec_data_ready = true;
|
|
|
|
if (bsec_accuracy == 3) {
|
|
if (!bsec_first_save_done) {
|
|
bsec_save_state();
|
|
bsec_last_save_ms = millis();
|
|
bsec_first_save_done = true;
|
|
} else if ((millis() - bsec_last_save_ms) >= BSEC_SAVE_INTERVAL_MS) {
|
|
bsec_save_state();
|
|
bsec_last_save_ms = millis();
|
|
}
|
|
}
|
|
}
|
|
#endif // ENV_INCLUDE_BME680_BSEC
|
|
}
|
|
#endif // ENV_INCLUDE_GPS || ENV_INCLUDE_BME680_BSEC
|