Browse Source

fix(sensors): improve sensor initialization and handling to prevent hangs and handle growth

This is a medium-ish refactor to attempt to clean up sensor handling logic both for board stability and future potential growth before the code becomes all spaghetti and meatballs.

I'd be curious to see if anyone running sensors out there that knows how to build and flash MeshCore code could give this a try and see how it behaves. It is working fine on my end on multiple nodes.

PR notes are gigantic because it is a fundamental behavior repair for sensors, so I wanted to over-explain. Also, if it hadn't been mentioned previously, push-back is always welcome. I'm just spending my time trying to clean up / fix / enhance this corner of the firmware, and want to contribute my improvements back to the project.

**Problem:**

Current MeshCore code makes no attempt to see what sensors are actually available on the I2C bus at startup and blindly tries to interact with sensors. This has some very bad side-effects, like if a sensor that is unsupported, or has a weird initialization process, the MeshCore node will just hang at boot and never successfully start up and ostensibly looks bricked, or the INA226 and SHT4X both sharing the same address and the code just silently fighting.

The current implementation also gloms sensor readouts from the MCU and environment sensors onto the same telemetry channel, with some arbitrary exceptions for incrementing channels based on certain behavioral situations. The MCU temperature and external temperature sensors would appear on channel 1, and it wouldn't be possible to tell which sensor the temperature value was coming from.

Per [CayenneLPP](https://github.com/myDevicesIoT/CayenneLPP): *Data Channel: Uniquely identifies each sensor in the device across frames, eg. “indoor sensor”* So this channel division implementation falls inline with what CayenneLPP intended. There are up to 256 channels available. So I tried to model this change in that behavioral style.

**Proposed Improvement:**

This implementation scans the I2C bus for what devices are present, sets each sensor to its own CayenneLPP channel, and keeps MCU telemetry on channel 1 only. So Channel 1 is always "self" and no confusion can result.

Details:
  - Channel 1 is always the MCU and things about it, so you always know that telemetry is from the board itself. Exception is GPS, GPS stays on channel 1 as well since it is "about the board" even though it's a bit gray-area as GPS can often be a secondary chip.
  - Each sensor board is allocated to a dedicated CayenneLPP channel, so if you are reading from that channel, you know the data is from that sensor only. (Sensors emitting more than one of the same type of measurement are exceptions.)
  - `scanI2CBus()` probes addresses 0x08–0x77 with raw `beginTransmission`/`endTransmission`. No sensor library is touched until after this completes. This will prevent sensor-based boot hangs, unknown or unresponsive devices never reach a library init call.
  - Created `SENSOR_TABLE` a compile-time array that is gated by the existing `ENV_INCLUDE_*` macros. A sentinel `{ 0, nullptr, nullptr, nullptr }` at the end keeps the array non-empty regardless of which sensors are enabled, avoiding zero-length array warnings.
  - When `begin()` is called, scan first, then loop: skip if address not detected, skip if `init()` returns 0, otherwise register one ActiveSensor entry per sub-channel.
  - `querySensors()` I replaced the entire #ifdef chain with a 3-line loop.
  - T1000-E has its own T1000SensorManager, so it should be completely unaffected by this change.
  - SHT4X quirky initialization behavior is retained.
  - MLX90614  - git commits around this didn't have any notes as to why it is reporting ambient temperature on a separate channel as well as the object temperature, as the ambient temperature is used internally to compute the object temperature and not really needed for the sensor's purpose - just the same, kept the existing behavior of reporting the ambient temperature one channel above the channel assigned to the sensor
  - All `bool *_initialized` fields are gone, replaced with `ActiveSensor _active_sensors[16]` (query function pointer and sub-channel index) and `_active_sensor_count. SensorDef` lives entirely in the `.cpp` so the header has no dependency on it.
  - Details on the INA226 and SHT4X: both default to address 0x44, the old code had a bug and would have both begin() calls fire and they would just fight each other silently. In the new code, the respective sensor code is only called if the device is actually present, however, if both were present simultaneously, SHT4X comes first in the table and would win, and INA226 would return false and be skipped. The INA226 has 16 possible addresses that are configurable in the hardware itself, so in a potential scenario where both sensors would be present, the person implementing that design could take that into account.
  - BME680 gas resistance will now transmit on the same channel as the rest of BME680 telemetry which is inline with CayenneLPP standards. Coupling this PR with https://github.com/meshcore-dev/MeshCore/pull/2146 streamline the whole sensor telemetry, and with https://github.com/meshcore-dev/MeshCore/pull/2149 will overall improve BME680 handling. The gas resistance sensor actually has a binary library to make it more useful, calibration, accounting for age of sensor, and other improvements, but since that adds more flash consumption, I have omitted that in PRs thus far.
 - RAK12035 and other current upstream dev branch changes integrated.
pull/2327/head
Nick Dunklee 2 months ago
parent
commit
c7be216f27
  1. 729
      src/helpers/sensors/EnvironmentSensorManager.cpp
  2. 34
      src/helpers/sensors/EnvironmentSensorManager.h

729
src/helpers/sensors/EnvironmentSensorManager.cpp

@ -6,6 +6,10 @@
#define TELEM_WIRE &Wire // Use default I2C bus for Environment Sensors #define TELEM_WIRE &Wire // Use default I2C bus for Environment Sensors
#endif #endif
// ============================================================
// Sensor library includes and static driver instances
// ============================================================
#ifdef ENV_INCLUDE_BME680 #ifdef ENV_INCLUDE_BME680
#ifndef TELEM_BME680_ADDRESS #ifndef TELEM_BME680_ADDRESS
#define TELEM_BME680_ADDRESS 0x76 #define TELEM_BME680_ADDRESS 0x76
@ -31,7 +35,7 @@ static Adafruit_AHTX0 AHTX0;
#ifndef TELEM_BME280_ADDRESS #ifndef TELEM_BME280_ADDRESS
#define TELEM_BME280_ADDRESS 0x76 // BME280 environmental sensor I2C address #define TELEM_BME280_ADDRESS 0x76 // BME280 environmental sensor I2C address
#endif #endif
#define TELEM_BME280_SEALEVELPRESSURE_HPA (1013.25) // Athmospheric pressure at sea level #define TELEM_BME280_SEALEVELPRESSURE_HPA (1013.25) // Atmospheric pressure at sea level
#include <Adafruit_BME280.h> #include <Adafruit_BME280.h>
static Adafruit_BME280 BME280; static Adafruit_BME280 BME280;
#endif #endif
@ -40,7 +44,7 @@ static Adafruit_BME280 BME280;
#ifndef TELEM_BMP280_ADDRESS #ifndef TELEM_BMP280_ADDRESS
#define TELEM_BMP280_ADDRESS 0x76 // BMP280 environmental sensor I2C address #define TELEM_BMP280_ADDRESS 0x76 // BMP280 environmental sensor I2C address
#endif #endif
#define TELEM_BMP280_SEALEVELPRESSURE_HPA (1013.25) // Athmospheric pressure at sea level #define TELEM_BMP280_SEALEVELPRESSURE_HPA (1013.25) // Atmospheric pressure at sea level
#include <Adafruit_BMP280.h> #include <Adafruit_BMP280.h>
static Adafruit_BMP280 BMP280(TELEM_WIRE); static Adafruit_BMP280 BMP280(TELEM_WIRE);
#endif #endif
@ -51,7 +55,7 @@ static Adafruit_SHTC3 SHTC3;
#endif #endif
#if ENV_INCLUDE_SHT4X #if ENV_INCLUDE_SHT4X
#define TELEM_SHT4X_ADDRESS 0x44 //0x44 - 0x46 #define TELEM_SHT4X_ADDRESS 0x44
#include <SensirionI2cSht4x.h> #include <SensirionI2cSht4x.h>
static SensirionI2cSht4x SHT4X; static SensirionI2cSht4x SHT4X;
#endif #endif
@ -63,7 +67,7 @@ LPS22HBClass LPS22HB(*TELEM_WIRE);
#if ENV_INCLUDE_INA3221 #if ENV_INCLUDE_INA3221
#ifndef TELEM_INA3221_ADDRESS #ifndef TELEM_INA3221_ADDRESS
#define TELEM_INA3221_ADDRESS 0x42 // INA3221 3 channel current sensor I2C address #define TELEM_INA3221_ADDRESS 0x42 // INA3221 3 channel current sensor I2C address
#endif #endif
#ifndef TELEM_INA3221_SHUNT_VALUE #ifndef TELEM_INA3221_SHUNT_VALUE
#define TELEM_INA3221_SHUNT_VALUE 0.100 // most variants will have a 0.1 ohm shunts #define TELEM_INA3221_SHUNT_VALUE 0.100 // most variants will have a 0.1 ohm shunts
@ -88,9 +92,9 @@ static Adafruit_INA260 INA260;
#endif #endif
#if ENV_INCLUDE_INA226 #if ENV_INCLUDE_INA226
#define TELEM_INA226_ADDRESS 0x44 #define TELEM_INA226_ADDRESS 0x44
#define TELEM_INA226_SHUNT_VALUE 0.100 #define TELEM_INA226_SHUNT_VALUE 0.100
#define TELEM_INA226_MAX_AMP 0.8 #define TELEM_INA226_MAX_AMP 0.8
#include <INA226.h> #include <INA226.h>
static INA226 INA226(TELEM_INA226_ADDRESS, TELEM_WIRE); static INA226 INA226(TELEM_INA226_ADDRESS, TELEM_WIRE);
#endif #endif
@ -161,381 +165,414 @@ public:
static RAK12500LocationProvider RAK12500_provider; static RAK12500LocationProvider RAK12500_provider;
#endif #endif
bool EnvironmentSensorManager::begin() { // ============================================================
#if ENV_INCLUDE_GPS // I2C bus scanner
#ifdef RAK_WISBLOCK_GPS // Probes every valid address and records which ones ACK.
rakGPSInit(); //probe base board/sockets for GPS // This runs before any sensor library is touched, so a missing
#else // or misbehaving device cannot stall or crash the boot sequence.
initBasicGPS(); // ============================================================
#endif
#endif static void scanI2CBus(TwoWire* wire, bool found[128]) {
for (uint8_t addr = 0x08; addr < 0x78; addr++) {
#if ENV_PIN_SDA && ENV_PIN_SCL wire->beginTransmission(addr);
#ifdef NRF52_PLATFORM found[addr] = (wire->endTransmission() == 0);
Wire1.setPins(ENV_PIN_SDA, ENV_PIN_SCL);
Wire1.setClock(100000);
Wire1.begin();
#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
#if ENV_INCLUDE_AHTX0
if (AHTX0.begin(TELEM_WIRE, 0, TELEM_AHTX_ADDRESS)) {
MESH_DEBUG_PRINTLN("Found AHT10/AHT20 at address: %02X", TELEM_AHTX_ADDRESS);
AHTX0_initialized = true;
} else {
AHTX0_initialized = false;
MESH_DEBUG_PRINTLN("AHT10/AHT20 was not found at I2C address %02X", TELEM_AHTX_ADDRESS);
} }
#endif }
#if ENV_INCLUDE_BME680 // ============================================================
if (BME680.begin(TELEM_BME680_ADDRESS)) { // Per-sensor init and query functions
MESH_DEBUG_PRINTLN("Found BME680 at address: %02X", TELEM_BME680_ADDRESS); //
BME680_initialized = true; // init(wire, address) — called only when the address was seen
} else { // on the bus. Returns 0 on failure, or the number of
BME680_initialized = false; // telemetry channels the sensor will consume (1 for all
MESH_DEBUG_PRINTLN("BME680 was not found at I2C address %02X", TELEM_BME680_ADDRESS); // single-output sensors; INA3221 returns one per enabled
} // hardware channel; MLX90614 and RAK12035+calibration
#endif // 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_BME280 #if ENV_INCLUDE_AHTX0
if (BME280.begin(TELEM_BME280_ADDRESS, TELEM_WIRE)) { static uint8_t init_ahtx0(TwoWire* wire, uint8_t addr) {
MESH_DEBUG_PRINTLN("Found BME280 at address: %02X", TELEM_BME280_ADDRESS); return AHTX0.begin(wire, 0, addr) ? 1 : 0;
MESH_DEBUG_PRINTLN("BME sensor ID: %02X", BME280.sensorID()); }
// Reduce self-heating: single-shot conversions, light oversampling, long standby. static void query_ahtx0(uint8_t ch, uint8_t, CayenneLPP& lpp) {
BME280.setSampling(Adafruit_BME280::MODE_FORCED, sensors_event_t humidity, temp;
Adafruit_BME280::SAMPLING_X1, // temperature AHTX0.getEvent(&humidity, &temp);
Adafruit_BME280::SAMPLING_X1, // pressure lpp.addTemperature(ch, temp.temperature);
Adafruit_BME280::SAMPLING_X1, // humidity lpp.addRelativeHumidity(ch, humidity.relative_humidity);
Adafruit_BME280::FILTER_OFF, }
Adafruit_BME280::STANDBY_MS_1000); #endif
BME280_initialized = true;
} else {
BME280_initialized = false;
MESH_DEBUG_PRINTLN("BME280 was not found at I2C address %02X", TELEM_BME280_ADDRESS);
}
#endif
#if ENV_INCLUDE_BMP280 #ifdef ENV_INCLUDE_BME680
if (BMP280.begin(TELEM_BMP280_ADDRESS)) { static uint8_t init_bme680(TwoWire*, uint8_t addr) {
MESH_DEBUG_PRINTLN("Found BMP280 at address: %02X", TELEM_BMP280_ADDRESS); // Wire was set in the static constructor; begin() takes address only.
MESH_DEBUG_PRINTLN("BMP sensor ID: %02X", BMP280.sensorID()); return BME680.begin(addr) ? 1 : 0;
BMP280_initialized = true; }
} else { static void query_bme680(uint8_t ch, uint8_t, CayenneLPP& lpp) {
BMP280_initialized = false; if (BME680.performReading()) {
MESH_DEBUG_PRINTLN("BMP280 was not found at I2C address %02X", TELEM_BMP280_ADDRESS); lpp.addTemperature(ch, BME680.temperature);
lpp.addRelativeHumidity(ch, BME680.humidity);
lpp.addBarometricPressure(ch, BME680.pressure / 100);
lpp.addAltitude(ch, 44330.0 * (1.0 - pow((BME680.pressure / 100) / TELEM_BME680_SEALEVELPRESSURE_HPA, 0.1903)));
lpp.addAnalogInput(ch, BME680.gas_resistance);
} }
#endif }
#endif
#if ENV_INCLUDE_SHTC3 #if ENV_INCLUDE_BME280
if (SHTC3.begin(TELEM_WIRE)) { static uint8_t init_bme280(TwoWire* wire, uint8_t addr) {
MESH_DEBUG_PRINTLN("Found sensor: SHTC3"); if (!BME280.begin(addr, wire)) return 0;
SHTC3_initialized = true; BME280.setSampling(Adafruit_BME280::MODE_FORCED,
} else { Adafruit_BME280::SAMPLING_X1,
SHTC3_initialized = false; Adafruit_BME280::SAMPLING_X1,
MESH_DEBUG_PRINTLN("SHTC3 was not found at I2C address %02X", 0x70); 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 }
#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_SHT4X #if ENV_INCLUDE_SHTC3
SHT4X.begin(*TELEM_WIRE, TELEM_SHT4X_ADDRESS); static uint8_t init_shtc3(TwoWire* wire, uint8_t) {
uint32_t serialNumber = 0; // Adafruit_SHTC3::begin() does not accept an address (fixed at 0x70).
int16_t sht4x_error; return SHTC3.begin(wire) ? 1 : 0;
sht4x_error = SHT4X.serialNumber(serialNumber); }
if (sht4x_error == 0) { static void query_shtc3(uint8_t ch, uint8_t, CayenneLPP& lpp) {
MESH_DEBUG_PRINTLN("Found SHT4X at address: %02X", TELEM_SHT4X_ADDRESS); sensors_event_t humidity, temp;
SHT4X_initialized = true; SHTC3.getEvent(&humidity, &temp);
} else { lpp.addTemperature(ch, temp.temperature);
SHT4X_initialized = false; lpp.addRelativeHumidity(ch, humidity.relative_humidity);
MESH_DEBUG_PRINTLN("SHT4X was not found at I2C address %02X", TELEM_SHT4X_ADDRESS); }
} #endif
#endif
#if ENV_INCLUDE_LPS22HB #if ENV_INCLUDE_SHT4X
if (LPS22HB.begin()) { static uint8_t init_sht4x(TwoWire* wire, uint8_t addr) {
MESH_DEBUG_PRINTLN("Found sensor: LPS22HB"); // SensirionI2cSht4x::begin() does not probe the hardware; use serialNumber()
LPS22HB_initialized = true; // as the actual presence check since it performs a real I2C transaction.
} else { SHT4X.begin(*wire, addr);
LPS22HB_initialized = false; uint32_t serial = 0;
MESH_DEBUG_PRINTLN("LPS22HB was not found at I2C address %02X", 0x5C); 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 }
#endif
#if ENV_INCLUDE_INA3221 #if ENV_INCLUDE_LPS22HB
if (INA3221.begin(TELEM_INA3221_ADDRESS, TELEM_WIRE)) { static uint8_t init_lps22hb(TwoWire*, uint8_t) {
MESH_DEBUG_PRINTLN("Found INA3221 at address: %02X", TELEM_INA3221_ADDRESS); // LPS22HBClass is constructed with the wire reference; begin() uses it.
MESH_DEBUG_PRINTLN("%04X %04X", INA3221.getDieID(), INA3221.getManufacturerID()); 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
for(int i = 0; i < 3; i++) { #if ENV_INCLUDE_INA3221
INA3221.setShuntResistance(i, TELEM_INA3221_SHUNT_VALUE); 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++;
} }
INA3221_initialized = true;
} else {
INA3221_initialized = false;
MESH_DEBUG_PRINTLN("INA3221 was not found at I2C address %02X", TELEM_INA3221_ADDRESS);
} }
#endif }
#endif
#if ENV_INCLUDE_INA219 #if ENV_INCLUDE_INA219
if (INA219.begin(TELEM_WIRE)) { static uint8_t init_ina219(TwoWire* wire, uint8_t) {
MESH_DEBUG_PRINTLN("Found INA219 at address: %02X", TELEM_INA219_ADDRESS); // INA219 static instance was constructed with the address; begin() uses it.
INA219_initialized = true; return INA219.begin(wire) ? 1 : 0;
} else { }
INA219_initialized = false; static void query_ina219(uint8_t ch, uint8_t, CayenneLPP& lpp) {
MESH_DEBUG_PRINTLN("INA219 was not found at I2C address %02X", TELEM_INA219_ADDRESS); lpp.addVoltage(ch, INA219.getBusVoltage_V());
} lpp.addCurrent(ch, INA219.getCurrent_mA() / 1000.0f);
#endif lpp.addPower(ch, INA219.getPower_mW() / 1000.0f);
}
#endif
#if ENV_INCLUDE_INA260 #if ENV_INCLUDE_INA260
if (INA260.begin(TELEM_INA260_ADDRESS, TELEM_WIRE)) { static uint8_t init_ina260(TwoWire* wire, uint8_t addr) {
MESH_DEBUG_PRINTLN("Found INA260 at address: %02X", TELEM_INA260_ADDRESS); return INA260.begin(addr, wire) ? 1 : 0;
INA260_initialized = true; }
} else { static void query_ina260(uint8_t ch, uint8_t, CayenneLPP& lpp) {
INA260_initialized = false; lpp.addVoltage(ch, INA260.readBusVoltage() / 1000.0f);
MESH_DEBUG_PRINTLN("INA260 was not found at I2C address %02X", TELEM_INA260_ADDRESS); lpp.addCurrent(ch, INA260.readCurrent() / 1000.0f);
} lpp.addPower(ch, INA260.readPower() / 1000.0f);
#endif }
#endif
#if ENV_INCLUDE_INA226 #if ENV_INCLUDE_INA226
if (INA226.begin()) { static uint8_t init_ina226(TwoWire*, uint8_t) {
MESH_DEBUG_PRINTLN("Found INA226 at address: %02X", TELEM_INA226_ADDRESS); // INA226 static instance was constructed with address and wire.
INA226.setMaxCurrentShunt(TELEM_INA226_MAX_AMP, TELEM_INA226_SHUNT_VALUE); if (!INA226.begin()) return 0;
INA226_initialized = true; INA226.setMaxCurrentShunt(TELEM_INA226_MAX_AMP, TELEM_INA226_SHUNT_VALUE);
} else { return 1;
INA226_initialized = false; }
MESH_DEBUG_PRINTLN("INA226 was not found at I2C address %02X", TELEM_INA226_ADDRESS); static void query_ina226(uint8_t ch, uint8_t, CayenneLPP& lpp) {
} lpp.addVoltage(ch, INA226.getBusVoltage());
#endif lpp.addCurrent(ch, INA226.getCurrent_mA() / 1000.0f);
lpp.addPower(ch, INA226.getPower_mW() / 1000.0f);
}
#endif
#if ENV_INCLUDE_MLX90614 #if ENV_INCLUDE_MLX90614
if (MLX90614.begin(TELEM_MLX90614_ADDRESS, TELEM_WIRE)) { static uint8_t init_mlx90614(TwoWire* wire, uint8_t addr) {
MESH_DEBUG_PRINTLN("Found MLX90614 at address: %02X", TELEM_MLX90614_ADDRESS); return MLX90614.begin(addr, wire) ? 2 : 0; // 2 channels: object temp, ambient temp
MLX90614_initialized = true; }
} else { static void query_mlx90614(uint8_t ch, uint8_t sub_ch, CayenneLPP& lpp) {
MLX90614_initialized = false; if (sub_ch == 0)
MESH_DEBUG_PRINTLN("MLX90614 was not found at I2C address %02X", TELEM_MLX90614_ADDRESS); lpp.addTemperature(ch, MLX90614.readObjectTempC());
} else
#endif lpp.addTemperature(ch, MLX90614.readAmbientTempC());
}
#endif
#if ENV_INCLUDE_VL53L0X #if ENV_INCLUDE_VL53L0X
if (VL53L0X.begin(TELEM_VL53L0X_ADDRESS, false, TELEM_WIRE)) { static uint8_t init_vl53l0x(TwoWire* wire, uint8_t addr) {
MESH_DEBUG_PRINTLN("Found VL53L0X at address: %02X", TELEM_VL53L0X_ADDRESS); return VL53L0X.begin(addr, false, wire) ? 1 : 0;
VL53L0X_initialized = true; }
} else { static void query_vl53l0x(uint8_t ch, uint8_t, CayenneLPP& lpp) {
VL53L0X_initialized = false; VL53L0X_RangingMeasurementData_t measure;
MESH_DEBUG_PRINTLN("VL53L0X was not found at I2C address %02X", TELEM_VL53L0X_ADDRESS); VL53L0X.rangingTest(&measure, false);
} lpp.addDistance(ch, measure.RangeStatus != 4 ? measure.RangeMilliMeter / 1000.0f : 0.0f);
#endif }
#endif
#if ENV_INCLUDE_BMP085 #ifdef ENV_INCLUDE_BMP085
// First argument is MODE (aka oversampling) static uint8_t init_bmp085(TwoWire* wire, uint8_t) {
// choose ULTRALOWPOWER return BMP085.begin(0, wire) ? 1 : 0; // mode 0 = ULTRALOWPOWER
if (BMP085.begin(0, TELEM_WIRE)) { }
MESH_DEBUG_PRINTLN("Found sensor BMP085"); static void query_bmp085(uint8_t ch, uint8_t, CayenneLPP& lpp) {
BMP085_initialized = true; lpp.addTemperature(ch, BMP085.readTemperature());
} else { lpp.addBarometricPressure(ch, BMP085.readPressure() / 100);
BMP085_initialized = false; lpp.addAltitude(ch, BMP085.readAltitude(TELEM_BMP085_SEALEVELPRESSURE_HPA * 100));
MESH_DEBUG_PRINTLN("BMP085 was not found at I2C address %02X", 0x77); }
} #endif
#endif
#if ENV_INCLUDE_RAK12035 #if ENV_INCLUDE_RAK12035
RAK12035.setup(*TELEM_WIRE); static uint8_t init_rak12035(TwoWire* wire, uint8_t addr) {
if (RAK12035.begin(TELEM_RAK12035_ADDRESS)) { // RAK12035 requires setup() before begin().
MESH_DEBUG_PRINTLN("Found sensor RAK12035 at address: %02X", TELEM_RAK12035_ADDRESS); RAK12035.setup(*wire);
RAK12035_initialized = true; 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 { } else {
RAK12035_initialized = false; #ifdef ENABLE_RAK12035_CALIBRATION
MESH_DEBUG_PRINTLN("RAK12035 was not found at I2C address %02X", TELEM_RAK12035_ADDRESS); 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
return true;
} }
#endif
bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) { // ============================================================
next_available_channel = TELEM_CHANNEL_SELF + 1; // 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.
//
// 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 {
uint8_t address;
const char* name;
uint8_t (*init)(TwoWire* wire, uint8_t address);
void (*query)(uint8_t channel, uint8_t sub_channel, CayenneLPP& telemetry);
};
if (requester_permissions & TELEM_PERM_LOCATION && gps_active) { static const SensorDef SENSOR_TABLE[] = {
telemetry.addGPS(TELEM_CHANNEL_SELF, node_lat, node_lon, node_altitude); // allow lat/lon via telemetry even if no GPS is detected #if ENV_INCLUDE_AHTX0
} { TELEM_AHTX_ADDRESS, "AHT10/AHT20", init_ahtx0, query_ahtx0 },
#endif
#ifdef ENV_INCLUDE_BME680
{ TELEM_BME680_ADDRESS, "BME680", init_bme680, query_bme680 },
#endif
#if ENV_INCLUDE_BME280
{ TELEM_BME280_ADDRESS, "BME280", init_bme280, query_bme280 },
#endif
#if ENV_INCLUDE_BMP280
{ TELEM_BMP280_ADDRESS, "BMP280", init_bmp280, query_bmp280 },
#endif
#if ENV_INCLUDE_SHTC3
{ 0x70, "SHTC3", init_shtc3, query_shtc3 },
#endif
#if ENV_INCLUDE_SHT4X
{ TELEM_SHT4X_ADDRESS, "SHT4X", init_sht4x, query_sht4x },
#endif
#if ENV_INCLUDE_LPS22HB
{ 0x5C, "LPS22HB", init_lps22hb, query_lps22hb },
#endif
#if ENV_INCLUDE_INA3221
{ TELEM_INA3221_ADDRESS, "INA3221", init_ina3221, query_ina3221 },
#endif
#if ENV_INCLUDE_INA219
{ TELEM_INA219_ADDRESS, "INA219", init_ina219, query_ina219 },
#endif
#if ENV_INCLUDE_INA260
{ TELEM_INA260_ADDRESS, "INA260", init_ina260, query_ina260 },
#endif
#if ENV_INCLUDE_INA226
{ TELEM_INA226_ADDRESS, "INA226", init_ina226, query_ina226 },
#endif
#if ENV_INCLUDE_MLX90614
{ TELEM_MLX90614_ADDRESS,"MLX90614", init_mlx90614, query_mlx90614 },
#endif
#if ENV_INCLUDE_VL53L0X
{ TELEM_VL53L0X_ADDRESS, "VL53L0X", init_vl53l0x, query_vl53l0x },
#endif
#ifdef ENV_INCLUDE_BMP085
{ 0x77, "BMP085", init_bmp085, query_bmp085 },
#endif
#if ENV_INCLUDE_RAK12035
{ TELEM_RAK12035_ADDRESS,"RAK12035", init_rak12035, query_rak12035 },
#endif
{ 0, nullptr, nullptr, nullptr } // sentinel — keeps the array non-empty
};
if (requester_permissions & TELEM_PERM_ENVIRONMENT) { static const size_t SENSOR_TABLE_SIZE = (sizeof(SENSOR_TABLE) / sizeof(SENSOR_TABLE[0])) - 1;
#if ENV_INCLUDE_AHTX0 // ============================================================
if (AHTX0_initialized) { // begin() — scan the I2C bus, then initialize only what was
sensors_event_t humidity, temp; // found. A sensor whose address does not ACK during the scan
AHTX0.getEvent(&humidity, &temp); // is never touched by a library call, preventing hangs or
telemetry.addTemperature(TELEM_CHANNEL_SELF, temp.temperature); // crashes caused by absent or misbehaving hardware.
telemetry.addRelativeHumidity(TELEM_CHANNEL_SELF, humidity.relative_humidity); // ============================================================
}
#endif
#if ENV_INCLUDE_BME680 bool EnvironmentSensorManager::begin() {
if (BME680_initialized) { #if ENV_INCLUDE_GPS
if (BME680.performReading()) { #ifdef RAK_WISBLOCK_GPS
telemetry.addTemperature(TELEM_CHANNEL_SELF, BME680.temperature); rakGPSInit();
telemetry.addRelativeHumidity(TELEM_CHANNEL_SELF, BME680.humidity); #else
telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, BME680.pressure / 100); initBasicGPS();
telemetry.addAltitude(TELEM_CHANNEL_SELF, 44330.0 * (1.0 - pow((BME680.pressure / 100) / TELEM_BME680_SEALEVELPRESSURE_HPA, 0.1903))); #endif
telemetry.addAnalogInput(next_available_channel, BME680.gas_resistance); #endif
next_available_channel++;
}
}
#endif
#if ENV_INCLUDE_BME280 #if ENV_PIN_SDA && ENV_PIN_SCL
if (BME280_initialized) { #ifdef NRF52_PLATFORM
if (BME280.takeForcedMeasurement()) { // trigger a fresh reading in forced mode Wire1.setPins(ENV_PIN_SDA, ENV_PIN_SCL);
telemetry.addTemperature(TELEM_CHANNEL_SELF, BME280.readTemperature()); Wire1.setClock(100000);
telemetry.addRelativeHumidity(TELEM_CHANNEL_SELF, BME280.readHumidity()); Wire1.begin();
telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, BME280.readPressure()/100); #else
telemetry.addAltitude(TELEM_CHANNEL_SELF, BME280.readAltitude(TELEM_BME280_SEALEVELPRESSURE_HPA)); Wire1.begin(ENV_PIN_SDA, ENV_PIN_SCL, 100000);
}
}
#endif #endif
MESH_DEBUG_PRINTLN("Second I2C initialized on pins SDA: %d SCL: %d", ENV_PIN_SDA, ENV_PIN_SCL);
#endif
#if ENV_INCLUDE_BMP280 // Scan the I2C bus before touching any sensor library.
if (BMP280_initialized) { bool detected[128] = {};
telemetry.addTemperature(TELEM_CHANNEL_SELF, BMP280.readTemperature()); scanI2CBus(TELEM_WIRE, detected);
telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, BMP280.readPressure()/100);
telemetry.addAltitude(TELEM_CHANNEL_SELF, BMP280.readAltitude(TELEM_BMP280_SEALEVELPRESSURE_HPA)); // Walk the sensor table and initialize only detected devices.
_active_sensor_count = 0;
for (size_t i = 0; i < SENSOR_TABLE_SIZE && _active_sensor_count < MAX_ACTIVE_SENSORS; i++) {
const SensorDef& def = SENSOR_TABLE[i];
if (!detected[def.address]) {
MESH_DEBUG_PRINTLN("%s not detected at I2C address %02X", def.name, def.address);
continue;
} }
#endif uint8_t n = def.init(TELEM_WIRE, def.address);
if (n == 0) {
#if ENV_INCLUDE_SHTC3 MESH_DEBUG_PRINTLN("%s found at %02X but failed to initialize", def.name, def.address);
if (SHTC3_initialized) { continue;
sensors_event_t humidity, temp;
SHTC3.getEvent(&humidity, &temp);
telemetry.addTemperature(TELEM_CHANNEL_SELF, temp.temperature);
telemetry.addRelativeHumidity(TELEM_CHANNEL_SELF, humidity.relative_humidity);
} }
#endif MESH_DEBUG_PRINTLN("Found %s at address: %02X", def.name, def.address);
for (uint8_t sub = 0; sub < n && _active_sensor_count < MAX_ACTIVE_SENSORS; sub++) {
#if ENV_INCLUDE_SHT4X _active_sensors[_active_sensor_count++] = { def.query, sub };
if (SHT4X_initialized) {
float sht4x_humidity, sht4x_temperature;
int16_t sht4x_error;
sht4x_error = SHT4X.measureLowestPrecision(sht4x_temperature, sht4x_humidity);
if (sht4x_error == 0) {
telemetry.addTemperature(TELEM_CHANNEL_SELF, sht4x_temperature);
telemetry.addRelativeHumidity(TELEM_CHANNEL_SELF, sht4x_humidity);
}
} }
#endif }
#if ENV_INCLUDE_LPS22HB return true;
if (LPS22HB_initialized) { }
telemetry.addTemperature(TELEM_CHANNEL_SELF, LPS22HB.readTemperature());
telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, LPS22HB.readPressure() * 10); // convert kPa to hPa
}
#endif
#if ENV_INCLUDE_INA3221 // ============================================================
if (INA3221_initialized) { // querySensors() — GPS stays on channel 1; each active sensor
for(int i = 0; i < TELEM_INA3221_NUM_CHANNELS; i++) { // gets the next available channel in the order it was
// add only enabled INA3221 channels to telemetry // initialized.
if (INA3221.isChannelEnabled(i)) { // ============================================================
float voltage = INA3221.getBusVoltage(i);
float current = INA3221.getCurrentAmps(i);
telemetry.addVoltage(next_available_channel, voltage);
telemetry.addCurrent(next_available_channel, current);
telemetry.addPower(next_available_channel, voltage * current);
next_available_channel++;
}
}
}
#endif
#if ENV_INCLUDE_INA219 bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) {
if (INA219_initialized) { next_available_channel = TELEM_CHANNEL_SELF + 1;
telemetry.addVoltage(next_available_channel, INA219.getBusVoltage_V());
telemetry.addCurrent(next_available_channel, INA219.getCurrent_mA() / 1000);
telemetry.addPower(next_available_channel, INA219.getPower_mW() / 1000);
next_available_channel++;
}
#endif
#if ENV_INCLUDE_INA260 if (requester_permissions & TELEM_PERM_LOCATION && gps_active) {
if (INA260_initialized) { telemetry.addGPS(TELEM_CHANNEL_SELF, node_lat, node_lon, node_altitude);
telemetry.addVoltage(next_available_channel, INA260.readBusVoltage() / 1000); }
telemetry.addCurrent(next_available_channel, INA260.readCurrent() / 1000);
telemetry.addPower(next_available_channel, INA260.readPower() / 1000);
next_available_channel++;
}
#endif
#if ENV_INCLUDE_INA226 if (requester_permissions & TELEM_PERM_ENVIRONMENT) {
if (INA226_initialized) { for (int i = 0; i < _active_sensor_count; i++) {
telemetry.addVoltage(next_available_channel, INA226.getBusVoltage()); _active_sensors[i].query(next_available_channel, _active_sensors[i].sub_channel, telemetry);
telemetry.addCurrent(next_available_channel, INA226.getCurrent_mA() / 1000.0);
telemetry.addPower(next_available_channel, INA226.getPower_mW() / 1000.0);
next_available_channel++; next_available_channel++;
} }
#endif
#if ENV_INCLUDE_MLX90614
if (MLX90614_initialized) {
telemetry.addTemperature(TELEM_CHANNEL_SELF, MLX90614.readObjectTempC());
telemetry.addTemperature(TELEM_CHANNEL_SELF + 1, MLX90614.readAmbientTempC());
}
#endif
#if ENV_INCLUDE_VL53L0X
if (VL53L0X_initialized) {
VL53L0X_RangingMeasurementData_t measure;
VL53L0X.rangingTest(&measure, false); // pass in 'true' to get debug data
if (measure.RangeStatus != 4) { // phase failures
telemetry.addDistance(TELEM_CHANNEL_SELF, measure.RangeMilliMeter / 1000.0f); // convert mm to m
} else {
telemetry.addDistance(TELEM_CHANNEL_SELF, 0.0f); // no valid measurement
}
}
#endif
#if ENV_INCLUDE_BMP085
if (BMP085_initialized) {
telemetry.addTemperature(TELEM_CHANNEL_SELF, BMP085.readTemperature());
telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, BMP085.readPressure() / 100);
telemetry.addAltitude(TELEM_CHANNEL_SELF, BMP085.readAltitude(TELEM_BMP085_SEALEVELPRESSURE_HPA * 100));
}
#endif
#if ENV_INCLUDE_RAK12035
if (RAK12035_initialized) {
// RAK12035 Telemetry is Channel 2
telemetry.addTemperature(2, RAK12035.get_sensor_temperature());
telemetry.addPercentage(2, RAK12035.get_sensor_moisture());
// RAK12035 CALIBRATION Telemetry is Channel 3, if enabled
#ifdef ENABLE_RAK12035_CALIBRATION
// Calibration Data Screen is Channel 3
float cap = RAK12035.get_sensor_capacitance();
float _wet = RAK12035.get_humidity_full();
float _dry = RAK12035.get_humidity_zero();
telemetry.addFrequency(3, cap);
telemetry.addTemperature(3, _wet);
telemetry.addPower(3, _dry);
if(cap > _dry){
RAK12035.set_humidity_zero(cap);
}
if(cap < _wet){
RAK12035.set_humidity_full(cap);
}
#endif
}
#endif
} }
return true; return true;
} }
@ -555,8 +592,6 @@ const char* EnvironmentSensorManager::getSettingName(int i) const {
return "gps"; return "gps";
} }
#endif #endif
// convenient way to add params (needed for some tests)
// if (i == settings++) return "param.2";
return NULL; return NULL;
} }
@ -567,8 +602,6 @@ const char* EnvironmentSensorManager::getSettingValue(int i) const {
return gps_active ? "1" : "0"; return gps_active ? "1" : "0";
} }
#endif #endif
// convenient way to add params ...
// if (i == settings++) return "2";
return NULL; return NULL;
} }
@ -584,11 +617,7 @@ bool EnvironmentSensorManager::setSettingValue(const char* name, const char* val
} }
if (strcmp(name, "gps_interval") == 0) { if (strcmp(name, "gps_interval") == 0) {
uint32_t interval_seconds = atoi(value); uint32_t interval_seconds = atoi(value);
if (interval_seconds > 0) { gps_update_interval_sec = interval_seconds > 0 ? interval_seconds : 1;
gps_update_interval_sec = interval_seconds;
} else {
gps_update_interval_sec = 1; // Default to 1 second if 0
}
return true; return true;
} }
#endif #endif
@ -652,16 +681,10 @@ void EnvironmentSensorManager::rakGPSInit(){
//search for the correct IO standby pin depending on socket used //search for the correct IO standby pin depending on socket used
if(gpsIsAwake(WB_IO2)){ if(gpsIsAwake(WB_IO2)){
// MESH_DEBUG_PRINTLN("RAK base board is RAK19007/10");
// MESH_DEBUG_PRINTLN("GPS is installed on Socket A");
} }
else if(gpsIsAwake(WB_IO4)){ else if(gpsIsAwake(WB_IO4)){
// MESH_DEBUG_PRINTLN("RAK base board is RAK19003/9");
// MESH_DEBUG_PRINTLN("GPS is installed on Socket C");
} }
else if(gpsIsAwake(WB_IO5)){ else if(gpsIsAwake(WB_IO5)){
// MESH_DEBUG_PRINTLN("RAK base board is RAK19001/11");
// MESH_DEBUG_PRINTLN("GPS is installed on Socket F");
} }
else{ else{
MESH_DEBUG_PRINTLN("No GPS found"); MESH_DEBUG_PRINTLN("No GPS found");
@ -716,7 +739,7 @@ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){
gps_detected = true; gps_detected = true;
return true; return true;
} }
pinMode(ioPin, INPUT); pinMode(ioPin, INPUT);
MESH_DEBUG_PRINTLN("GPS did not init with this IO pin... try the next"); MESH_DEBUG_PRINTLN("GPS did not init with this IO pin... try the next");
return false; return false;

34
src/helpers/sensors/EnvironmentSensorManager.h

@ -6,27 +6,22 @@
class EnvironmentSensorManager : public SensorManager { class EnvironmentSensorManager : public SensorManager {
protected: protected:
int next_available_channel = TELEM_CHANNEL_SELF + 1; static const int MAX_ACTIVE_SENSORS = 16;
bool AHTX0_initialized = false; // Query function pointer + sub-channel index (for multi-channel sensors like INA3221).
bool BME280_initialized = false; // Sub-channel is 0 for all single-output sensors.
bool BMP280_initialized = false; struct ActiveSensor {
bool INA3221_initialized = false; void (*query)(uint8_t channel, uint8_t sub_channel, CayenneLPP& telemetry);
bool INA219_initialized = false; uint8_t sub_channel;
bool INA260_initialized = false; };
bool INA226_initialized = false;
bool SHTC3_initialized = false;
bool LPS22HB_initialized = false;
bool MLX90614_initialized = false;
bool VL53L0X_initialized = false;
bool SHT4X_initialized = false;
bool BME680_initialized = false;
bool BMP085_initialized = false;
bool RAK12035_initialized = false;
bool gps_detected = false; ActiveSensor _active_sensors[MAX_ACTIVE_SENSORS];
bool gps_active = false; int _active_sensor_count = 0;
uint32_t gps_update_interval_sec = 1; // Default 1 second uint8_t next_available_channel = TELEM_CHANNEL_SELF + 1;
bool gps_detected = false;
bool gps_active = false;
uint32_t gps_update_interval_sec = 1;
#if ENV_INCLUDE_GPS #if ENV_INCLUDE_GPS
LocationProvider* _location; LocationProvider* _location;
@ -39,7 +34,6 @@ protected:
#endif #endif
#endif #endif
public: public:
#if ENV_INCLUDE_GPS #if ENV_INCLUDE_GPS
EnvironmentSensorManager(LocationProvider &location): _location(&location){}; EnvironmentSensorManager(LocationProvider &location): _location(&location){};

Loading…
Cancel
Save