mirror of
https://github.com/alexhopeoconnor/firmware.git
synced 2026-10-04 03:18:10 +10:00
Add SEN6x sensors (#11390)
* Add SEN6X * Adds new SENXX class for SEN5X and SEN6X * Adds CO2 sensor calibration class to be shared among othre CO2 sensors * Make existing CO2 sensor draw from CO2Sensor class * Minor coment for CO2 sensor class * Move away from getRTC in SENXX class to keep track of time changes. * Change all sensors to millis for tracking time, instead of using getRTC * Add comments regarding VOC state * Avoid storing non-valid RTC Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Avoid CO2 sensor warm-up time to be below PM measured started Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Fix limits in CO2 sensor calibration Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Add pragma once on headers * Avoid non-working ASC commands Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Fix data poll * Move pm measure started before warmup check * Make cleaning non-blocking * Restore previous state if cleaning fails. Fix data ready condition. * Fix CO2 sensor checks for calibration --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
coderabbitai[bot]
parent
3b608b8fc5
commit
944fd26580
@@ -300,6 +300,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#define BQ25896_ADDR 0x6B
|
||||
#define LTR553ALS_ADDR 0x23
|
||||
#define SEN5X_ADDR 0x69
|
||||
#define SEN6X_ADDR 0x6B // same as QMI8658_ADDR and BQ25896_ADDR
|
||||
#define SCD30_ADDR 0x61
|
||||
#define ADS1X15_ADDR 0x48
|
||||
#define ADS1X15_ADDR_ALT1 0x49
|
||||
|
||||
@@ -50,8 +50,8 @@ ScanI2C::FoundDevice ScanI2C::firstMagnetometer() const
|
||||
|
||||
ScanI2C::FoundDevice ScanI2C::firstAQI() const
|
||||
{
|
||||
ScanI2C::DeviceType types[] = {PMSA003I, SEN5X, SCD4X, SFA30};
|
||||
return firstOfOrNONE(4, types);
|
||||
ScanI2C::DeviceType types[] = {PMSA003I, SEN5X, SEN6X, SCD4X, SFA30};
|
||||
return firstOfOrNONE(5, types);
|
||||
}
|
||||
|
||||
ScanI2C::FoundDevice ScanI2C::firstRGBLED() const
|
||||
|
||||
@@ -96,6 +96,7 @@ class ScanI2C
|
||||
CST3530,
|
||||
BMI270,
|
||||
SEN5X,
|
||||
SEN6X,
|
||||
SFA30,
|
||||
CW2015,
|
||||
SCD30,
|
||||
|
||||
@@ -160,12 +160,19 @@ bool ScanI2CTwoWire::i2cCommandResponseLength(ScanI2C::DeviceAddress addr, uint1
|
||||
|
||||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
|
||||
#include "../modules/Telemetry/Sensor/SEN5XSensor.h"
|
||||
#include "../modules/Telemetry/Sensor/SEN6XSensor.h"
|
||||
bool probeSEN5X(TwoWire *i2cBus, uint8_t address, ScanI2C::I2CPort port)
|
||||
{
|
||||
SEN5XSensor sen5xsensor;
|
||||
return sen5xsensor.probe(i2cBus, address, port);
|
||||
}
|
||||
|
||||
bool probeSEN6X(TwoWire *i2cBus, uint8_t address, ScanI2C::I2CPort port)
|
||||
{
|
||||
SEN6XSensor sen6xsensor;
|
||||
return sen6xsensor.probe(i2cBus, address, port);
|
||||
}
|
||||
|
||||
bool probeHM330x(TwoWire *i2cBus, uint8_t address)
|
||||
{
|
||||
|
||||
@@ -700,7 +707,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
logFoundDevice("QMC6310U", (uint8_t)addr.address);
|
||||
break;
|
||||
|
||||
case QMI8658_ADDR:
|
||||
case QMI8658_ADDR: // same as BQ25896_ADDR and SEN6X_ADDR
|
||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0A), 1); // get ID
|
||||
if (registerValue == 0xC0) {
|
||||
type = BQ24295;
|
||||
@@ -721,6 +728,13 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
type = ISM330DHCX;
|
||||
logFoundDevice("ISM330DHCX", (uint8_t)addr.address);
|
||||
} else {
|
||||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
|
||||
if (probeSEN6X(i2cBus, addr.address, port)) {
|
||||
type = SEN6X;
|
||||
logFoundDevice("SEN6X", addr.address);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
type = QMI8658;
|
||||
logFoundDevice("QMI8658", (uint8_t)addr.address);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ static constexpr uint16_t TX_HISTORY_KEY_AIR_QUALITY_TELEMETRY = 0x8004;
|
||||
#include "Sensor/AddI2CSensorTemplate.h"
|
||||
#include "Sensor/PMSA003ISensor.h"
|
||||
#include "Sensor/SEN5XSensor.h"
|
||||
#include "Sensor/SEN6XSensor.h"
|
||||
#if __has_include(<SensirionI2cScd4x.h>)
|
||||
#include "Sensor/SCD4XSensor.h"
|
||||
#endif
|
||||
@@ -66,6 +67,8 @@ void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner)
|
||||
supportedSensors[PMSA003I_ADDR] = ScanI2C::DeviceType::PMSA003I;
|
||||
if (!supportedSensors.count(SEN5X_ADDR))
|
||||
supportedSensors[SEN5X_ADDR] = ScanI2C::DeviceType::SEN5X;
|
||||
if (!supportedSensors.count(SEN6X_ADDR))
|
||||
supportedSensors[SEN6X_ADDR] = ScanI2C::DeviceType::SEN6X;
|
||||
#if __has_include(<SensirionI2cScd4x.h>)
|
||||
if (!supportedSensors.count(SCD4X_ADDR))
|
||||
supportedSensors[SCD4X_ADDR] = ScanI2C::DeviceType::SCD4X;
|
||||
@@ -108,6 +111,7 @@ void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner)
|
||||
// order by priority of metrics/values (low top, high bottom)
|
||||
addSensor<PMSA003ISensor>(i2cScanner, ScanI2C::DeviceType::PMSA003I);
|
||||
addSensor<SEN5XSensor>(i2cScanner, ScanI2C::DeviceType::SEN5X);
|
||||
addSensor<SEN6XSensor>(i2cScanner, ScanI2C::DeviceType::SEN6X);
|
||||
#if __has_include(<SensirionI2cScd4x.h>)
|
||||
addSensor<SCD4XSensor>(i2cScanner, ScanI2C::DeviceType::SCD4X);
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
#pragma once
|
||||
|
||||
#include "MeshModule.h"
|
||||
|
||||
/*
|
||||
Shared CO2 calibration interface + admin-message dispatch for any sensor that
|
||||
exposes Sensirion-style CO2 auto/forced calibration: automatic self-calibration
|
||||
(ASC), forced recalibration (FRC), altitude/ambient-pressure compensation, and
|
||||
a calibration-history factory reset. SCD4XSensor, SCD30Sensor and the
|
||||
CO2-capable SEN6X variants (SEN63C/SEN66/SEN69C, via SENXXSensor) all implement
|
||||
this instead of duplicating the same admin-message branching logic.
|
||||
|
||||
Concrete classes only need to implement the low-level co2* operations against
|
||||
their own I2C command set; handleCo2AdminRequest() below is the one shared
|
||||
place that decides *when* to call FRC vs ASC, validates that a target CO2 was
|
||||
supplied for FRC, and reverts ASC on a failed FRC attempt.
|
||||
*/
|
||||
class CO2CalibrationSensor
|
||||
{
|
||||
protected:
|
||||
virtual ~CO2CalibrationSensor() {}
|
||||
|
||||
// Forced recalibration against a known reference CO2 concentration (ppm).
|
||||
virtual bool co2PerformFRC(uint32_t targetCO2ppm) = 0;
|
||||
|
||||
// Automatic self-calibration on/off.
|
||||
virtual bool co2GetASC(bool &ascEnabled) = 0;
|
||||
virtual bool co2SetASC(bool ascEnabled) = 0;
|
||||
// Optional: not every sensor exposes a settable ASC baseline (e.g. SCD30/SEN6X don't).
|
||||
virtual bool co2SetASCBaseline(uint32_t targetCO2ppm) { return true; }
|
||||
|
||||
// Altitude/pressure compensation. altitude in meters above sea level,
|
||||
// ambientPressure in Pa (implementations convert to whatever unit their
|
||||
// own command set expects).
|
||||
virtual bool co2SetAltitude(uint32_t altitude) = 0;
|
||||
virtual bool co2SetAmbientPressure(uint32_t ambientPressurePa) { return false; }
|
||||
|
||||
// Erases the sensor's FRC/ASC calibration history. Optional.
|
||||
virtual bool co2FactoryReset() { return false; }
|
||||
|
||||
// Snapshot of whichever *_config admin message fields were populated,
|
||||
// translated once by the caller into this sensor-agnostic shape.
|
||||
struct Co2AdminRequest {
|
||||
bool hasFactoryReset = false;
|
||||
bool hasSetAsc = false;
|
||||
bool setAsc = false;
|
||||
bool hasTargetCo2 = false;
|
||||
uint32_t targetCo2 = 0;
|
||||
bool hasSetAltitude = false;
|
||||
uint32_t setAltitude = 0;
|
||||
bool hasSetAmbientPressure = false;
|
||||
uint32_t setAmbientPressure = 0;
|
||||
};
|
||||
|
||||
// Returns false if a requested operation failed - callers should map
|
||||
// that to AdminMessageHandleResult::NOT_HANDLED like they already do for
|
||||
// their sensor-specific fields (e.g. temperature offset, power mode).
|
||||
bool handleCo2AdminRequest(const Co2AdminRequest &cfg, const char *sensorName)
|
||||
{
|
||||
if (cfg.hasFactoryReset) {
|
||||
LOG_DEBUG("%s: Requested CO2 calibration factory reset", sensorName);
|
||||
return co2FactoryReset();
|
||||
}
|
||||
|
||||
if (cfg.hasSetAsc) {
|
||||
if (!cfg.setAsc) {
|
||||
bool currentASC = false;
|
||||
if (!co2GetASC(currentASC)) {
|
||||
return false;
|
||||
}
|
||||
// Disabling ASC is how you request a forced recalibration (FRC).
|
||||
if (!cfg.hasTargetCo2) {
|
||||
LOG_ERROR("%s: target CO2 not provided for FRC", sensorName);
|
||||
return false;
|
||||
}
|
||||
LOG_DEBUG("%s: Request for FRC", sensorName);
|
||||
if (!co2SetASC(false)) {
|
||||
return false;
|
||||
}
|
||||
if (!co2PerformFRC(cfg.targetCo2)) {
|
||||
// Restore previous ASC state since the FRC attempt failed.
|
||||
co2SetASC(currentASC);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
LOG_DEBUG("%s: Request for ASC", sensorName);
|
||||
if (!co2SetASC(true)) {
|
||||
return false;
|
||||
}
|
||||
// ASC with target CO2 is only available in SCD4X
|
||||
if (cfg.hasTargetCo2) {
|
||||
if (!co2SetASCBaseline(cfg.targetCo2)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cfg.hasSetAltitude) {
|
||||
if (!co2SetAltitude(cfg.setAltitude)) {
|
||||
return false;
|
||||
}
|
||||
} else if (cfg.hasSetAmbientPressure) {
|
||||
if (!co2SetAmbientPressure(cfg.setAmbientPressure)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
@@ -42,7 +42,7 @@ bool HM330XSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
|
||||
uint32_t HM330XSensor::wakeUp()
|
||||
{
|
||||
state = State::ACTIVE;
|
||||
measureStarted = getTime();
|
||||
measureStarted = millis();
|
||||
return HM330X_WARMUP_MS;
|
||||
}
|
||||
|
||||
@@ -64,9 +64,7 @@ bool HM330XSensor::isActive()
|
||||
|
||||
int32_t HM330XSensor::pendingForReadyMs()
|
||||
{
|
||||
uint32_t now;
|
||||
now = getTime();
|
||||
uint32_t sincePMMeasureStarted = (now - measureStarted) * 1000;
|
||||
uint32_t sincePMMeasureStarted = millis() - measureStarted;
|
||||
LOG_DEBUG("%s: Since measure started: %ums", sensorName, sincePMMeasureStarted);
|
||||
|
||||
if (sincePMMeasureStarted < HM330X_WARMUP_MS) {
|
||||
|
||||
@@ -18,6 +18,8 @@ class HM330XSensor : public TelemetrySensor
|
||||
private:
|
||||
enum class State { IDLE, ACTIVE };
|
||||
State state = State::IDLE;
|
||||
// millis()-based, not wall-clock: this only measures in-session warmup elapsed time,
|
||||
// and getTime() can jump discontinuously when RTC quality improves mid-session.
|
||||
uint32_t measureStarted = 0;
|
||||
uint8_t buffer[HM330X_FRAME_LENGTH]{};
|
||||
TwoWire *_bus{};
|
||||
|
||||
@@ -157,9 +157,7 @@ int32_t PMSA003ISensor::wakeUpTimeMs()
|
||||
int32_t PMSA003ISensor::pendingForReadyMs()
|
||||
{
|
||||
#ifdef PMSA003I_ENABLE_PIN
|
||||
uint32_t now;
|
||||
now = getTime();
|
||||
uint32_t sincePmMeasureStarted = (now - pmMeasureStarted) * 1000;
|
||||
uint32_t sincePmMeasureStarted = millis() - pmMeasureStarted;
|
||||
LOG_DEBUG("%s: Since measure started: %ums", sensorName, sincePmMeasureStarted);
|
||||
|
||||
if (sincePmMeasureStarted < PMSA003I_WARMUP_MS) {
|
||||
@@ -195,7 +193,7 @@ uint32_t PMSA003ISensor::wakeUp()
|
||||
LOG_INFO("%s Waking", sensorName);
|
||||
digitalWrite(PMSA003I_ENABLE_PIN, HIGH);
|
||||
state = PMSA003I_ACTIVE;
|
||||
pmMeasureStarted = getTime();
|
||||
pmMeasureStarted = millis();
|
||||
|
||||
return PMSA003I_WARMUP_MS;
|
||||
#endif
|
||||
|
||||
@@ -39,6 +39,8 @@ class PMSA003ISensor : public TelemetrySensor
|
||||
|
||||
uint16_t computedChecksum = 0;
|
||||
uint16_t receivedChecksum = 0;
|
||||
// millis()-based, not wall-clock: this only measures in-session warmup elapsed time,
|
||||
// and getTime() can jump discontinuously when RTC quality improves mid-session.
|
||||
uint32_t pmMeasureStarted = 0;
|
||||
|
||||
uint8_t buffer[PMSA003I_FRAME_LENGTH]{};
|
||||
|
||||
@@ -424,37 +424,34 @@ AdminMessageHandleResult SCD30Sensor::handleAdminMessage(const meshtastic_MeshPa
|
||||
LOG_DEBUG("%s: Requested soft reset", sensorName);
|
||||
this->softReset();
|
||||
} else {
|
||||
const auto &cfg = request->sensor_config.scd30_config;
|
||||
|
||||
if (request->sensor_config.scd30_config.has_set_asc) {
|
||||
this->setASC(request->sensor_config.scd30_config.set_asc);
|
||||
if (request->sensor_config.scd30_config.set_asc == false) {
|
||||
LOG_DEBUG("%s: Request for FRC", sensorName);
|
||||
if (request->sensor_config.scd30_config.has_set_target_co2_conc) {
|
||||
this->performFRC(request->sensor_config.scd30_config.set_target_co2_conc);
|
||||
} else {
|
||||
// FRC requested but no target CO2 provided
|
||||
LOG_ERROR("%s: target CO2 not provided", sensorName);
|
||||
result = AdminMessageHandleResult::NOT_HANDLED;
|
||||
break;
|
||||
}
|
||||
// ASC/FRC/altitude calibration branching is shared with SCD4XSensor and the
|
||||
// CO2-capable SEN6X variants via CO2CalibrationSensor.
|
||||
if (cfg.has_set_asc || cfg.has_set_altitude) {
|
||||
Co2AdminRequest co2req;
|
||||
co2req.hasSetAsc = cfg.has_set_asc;
|
||||
co2req.setAsc = cfg.set_asc;
|
||||
co2req.hasTargetCo2 = cfg.has_set_target_co2_conc;
|
||||
co2req.targetCo2 = cfg.set_target_co2_conc;
|
||||
co2req.hasSetAltitude = cfg.has_set_altitude;
|
||||
co2req.setAltitude = cfg.set_altitude;
|
||||
if (!this->handleCo2AdminRequest(co2req, sensorName)) {
|
||||
result = AdminMessageHandleResult::NOT_HANDLED;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for temperature offset
|
||||
// NOTE: this requires to have a sensor working on stable environment
|
||||
// And to make it between readings
|
||||
if (request->sensor_config.scd30_config.has_set_temperature) {
|
||||
this->setTemperature(request->sensor_config.scd30_config.set_temperature);
|
||||
}
|
||||
|
||||
// Check for altitude
|
||||
if (request->sensor_config.scd30_config.has_set_altitude) {
|
||||
this->setAltitude(request->sensor_config.scd30_config.set_altitude);
|
||||
if (cfg.has_set_temperature) {
|
||||
this->setTemperature(cfg.set_temperature);
|
||||
}
|
||||
|
||||
// Check for set measuremen interval
|
||||
if (request->sensor_config.scd30_config.has_set_measurement_interval) {
|
||||
this->setMeasurementInterval(request->sensor_config.scd30_config.set_measurement_interval);
|
||||
if (cfg.has_set_measurement_interval) {
|
||||
this->setMeasurementInterval(cfg.set_measurement_interval);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,12 +4,13 @@
|
||||
|
||||
#include "../detect/ReClockI2C.h"
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "CO2Sensor.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include <SensirionI2cScd30.h>
|
||||
|
||||
#define SCD30_I2C_CLOCK_SPEED 100000
|
||||
|
||||
class SCD30Sensor : public TelemetrySensor
|
||||
class SCD30Sensor : public TelemetrySensor, public CO2CalibrationSensor
|
||||
{
|
||||
private:
|
||||
SensirionI2cScd30 scd30;
|
||||
@@ -29,6 +30,27 @@ class SCD30Sensor : public TelemetrySensor
|
||||
bool startMeasurement();
|
||||
bool stopMeasurement();
|
||||
|
||||
// CO2CalibrationSensor overrides - thin wrappers, shared with SCD4XSensor and
|
||||
// the CO2-capable SEN6X variants via CO2CalibrationSensor::handleCo2AdminRequest().
|
||||
// SCD30 has no ambient-pressure command or calibration-history factory reset, so
|
||||
// those two are left at CO2CalibrationSensor's default (unsupported) implementation.
|
||||
bool co2PerformFRC(uint32_t targetCO2ppm) override
|
||||
{
|
||||
return targetCO2ppm <= UINT16_MAX && performFRC(static_cast<uint16_t>(targetCO2ppm));
|
||||
}
|
||||
bool co2GetASC(bool &ascEnabled) override
|
||||
{
|
||||
uint16_t v = 0;
|
||||
bool ok = getASC(v);
|
||||
ascEnabled = v != 0;
|
||||
return ok;
|
||||
}
|
||||
bool co2SetASC(bool ascEnabled) override { return setASC(ascEnabled); }
|
||||
bool co2SetAltitude(uint32_t altitude) override
|
||||
{
|
||||
return altitude <= UINT16_MAX && setAltitude(static_cast<uint16_t>(altitude));
|
||||
}
|
||||
|
||||
// Parameters
|
||||
uint16_t ascActive = 1;
|
||||
uint16_t measurementInterval = 2;
|
||||
|
||||
@@ -705,7 +705,7 @@ uint32_t SCD4XSensor::wakeUp()
|
||||
#endif /* SCD4X_I2C_CLOCK_SPEED */
|
||||
|
||||
if (startMeasurement()) {
|
||||
co2MeasureStarted = getTime();
|
||||
co2MeasureStarted = millis();
|
||||
#ifdef SCD4X_I2C_CLOCK_SPEED
|
||||
reClockI2C.restoreClock();
|
||||
#endif /* SCD4X_I2C_CLOCK_SPEED */
|
||||
@@ -755,9 +755,7 @@ int32_t SCD4XSensor::wakeUpTimeMs()
|
||||
|
||||
int32_t SCD4XSensor::pendingForReadyMs()
|
||||
{
|
||||
uint32_t now;
|
||||
now = getTime();
|
||||
uint32_t sinceCO2MeasureStarted = (now - co2MeasureStarted) * 1000;
|
||||
uint32_t sinceCO2MeasureStarted = millis() - co2MeasureStarted;
|
||||
LOG_DEBUG("%s: Since measure started: %ums", sensorName, sinceCO2MeasureStarted);
|
||||
|
||||
if (sinceCO2MeasureStarted < SCD4X_WARMUP_MS) {
|
||||
@@ -785,85 +783,48 @@ AdminMessageHandleResult SCD4XSensor::handleAdminMessage(const meshtastic_MeshPa
|
||||
break;
|
||||
}
|
||||
|
||||
if (request->sensor_config.scd4x_config.has_factory_reset) {
|
||||
LOG_DEBUG("%s: Requested factory reset", sensorName);
|
||||
if (!this->factoryReset()) {
|
||||
{
|
||||
const auto &cfg = request->sensor_config.scd4x_config;
|
||||
bool ok = true;
|
||||
|
||||
// FRC/ASC/altitude/pressure/factory-reset calibration branching is shared with
|
||||
// SCD30Sensor and the CO2-capable SEN6X variants via CO2CalibrationSensor.
|
||||
if (cfg.has_factory_reset || cfg.has_set_asc || cfg.has_set_altitude || cfg.has_set_ambient_pressure) {
|
||||
Co2AdminRequest co2req;
|
||||
co2req.hasFactoryReset = cfg.has_factory_reset;
|
||||
co2req.hasSetAsc = cfg.has_set_asc;
|
||||
co2req.setAsc = cfg.set_asc;
|
||||
co2req.hasTargetCo2 = cfg.has_set_target_co2_conc;
|
||||
co2req.targetCo2 = cfg.set_target_co2_conc;
|
||||
co2req.hasSetAltitude = cfg.has_set_altitude;
|
||||
co2req.setAltitude = cfg.set_altitude;
|
||||
co2req.hasSetAmbientPressure = cfg.has_set_ambient_pressure;
|
||||
co2req.setAmbientPressure = cfg.set_ambient_pressure;
|
||||
ok &= this->handleCo2AdminRequest(co2req, sensorName);
|
||||
}
|
||||
|
||||
// A factory reset erases calibration history outright - matches the original
|
||||
// behavior of skipping every other field when it's requested.
|
||||
if (ok && !cfg.has_factory_reset) {
|
||||
// Check for temperature offset
|
||||
// NOTE: this requires to have a sensor working on stable environment
|
||||
// And to make it between readings
|
||||
if (cfg.has_set_temperature) {
|
||||
ok &= this->setTemperature(cfg.set_temperature);
|
||||
}
|
||||
|
||||
// Check for low power mode
|
||||
// NOTE: to switch from one mode to another do:
|
||||
// setPowerMode -> startMeasurement
|
||||
if (cfg.has_set_power_mode) {
|
||||
ok &= this->setPowerMode(cfg.set_power_mode);
|
||||
}
|
||||
}
|
||||
|
||||
if (!ok) {
|
||||
result = AdminMessageHandleResult::NOT_HANDLED;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if (request->sensor_config.scd4x_config.has_set_asc) {
|
||||
getASC(ascActive);
|
||||
bool currentASC = ascActive;
|
||||
if (request->sensor_config.scd4x_config.set_asc == false) {
|
||||
LOG_DEBUG("%s: Request for FRC", sensorName);
|
||||
if (request->sensor_config.scd4x_config.has_set_target_co2_conc) {
|
||||
if (this->setASC(request->sensor_config.scd4x_config.set_asc)) {
|
||||
if (!this->performFRC(request->sensor_config.scd4x_config.set_target_co2_conc)) {
|
||||
result = AdminMessageHandleResult::NOT_HANDLED;
|
||||
// Set it back to ASC if failed
|
||||
setASC(currentASC);
|
||||
break;
|
||||
};
|
||||
} else {
|
||||
result = AdminMessageHandleResult::NOT_HANDLED;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// FRC requested but no target CO2 provided
|
||||
LOG_ERROR("%s: target CO2 not provided", sensorName);
|
||||
result = AdminMessageHandleResult::NOT_HANDLED;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
LOG_DEBUG("%s: Request for ASC", sensorName);
|
||||
if (this->setASC(request->sensor_config.scd4x_config.set_asc)) {
|
||||
if (request->sensor_config.scd4x_config.has_set_target_co2_conc) {
|
||||
LOG_DEBUG("%s: Request has target CO2", sensorName);
|
||||
this->setASCBaseline(request->sensor_config.scd4x_config.set_target_co2_conc);
|
||||
// NOTE - in this situation, if we set ASC, but baseline set fails, we stay on ASC
|
||||
} else {
|
||||
LOG_DEBUG("%s: Request doesn't have target CO2", sensorName);
|
||||
}
|
||||
} else {
|
||||
result = AdminMessageHandleResult::NOT_HANDLED;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for temperature offset
|
||||
// NOTE: this requires to have a sensor working on stable environment
|
||||
// And to make it between readings
|
||||
if (request->sensor_config.scd4x_config.has_set_temperature) {
|
||||
if (!this->setTemperature(request->sensor_config.scd4x_config.set_temperature)) {
|
||||
result = AdminMessageHandleResult::NOT_HANDLED;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for altitude or pressure offset
|
||||
if (request->sensor_config.scd4x_config.has_set_altitude) {
|
||||
if (!this->setAltitude(request->sensor_config.scd4x_config.set_altitude)) {
|
||||
result = AdminMessageHandleResult::NOT_HANDLED;
|
||||
break;
|
||||
}
|
||||
} else if (request->sensor_config.scd4x_config.has_set_ambient_pressure) {
|
||||
if (!this->setAmbientPressure(request->sensor_config.scd4x_config.set_ambient_pressure)) {
|
||||
result = AdminMessageHandleResult::NOT_HANDLED;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for low power mode
|
||||
// NOTE: to switch from one mode to another do:
|
||||
// setPowerMode -> startMeasurement
|
||||
if (request->sensor_config.scd4x_config.has_set_power_mode) {
|
||||
if (!this->setPowerMode(request->sensor_config.scd4x_config.set_power_mode)) {
|
||||
result = AdminMessageHandleResult::NOT_HANDLED;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result = AdminMessageHandleResult::HANDLED;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "../detect/ReClockI2C.h"
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "CO2Sensor.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include "gps/RTC.h"
|
||||
#include <SensirionI2cScd4x.h>
|
||||
@@ -13,7 +14,7 @@
|
||||
#define SCD4X_WARMUP_MS 5000
|
||||
#define SCD4X_MAX_RETRIES 3
|
||||
|
||||
class SCD4XSensor : public TelemetrySensor
|
||||
class SCD4XSensor : public TelemetrySensor, public CO2CalibrationSensor
|
||||
{
|
||||
private:
|
||||
SensirionI2cScd4x scd4x;
|
||||
@@ -35,10 +36,45 @@ class SCD4XSensor : public TelemetrySensor
|
||||
bool startMeasurement();
|
||||
bool stopMeasurement();
|
||||
|
||||
// CO2CalibrationSensor overrides - thin wrappers around the methods above,
|
||||
// shared with SCD30Sensor and the CO2-capable SEN6X variants via
|
||||
// CO2CalibrationSensor::handleCo2AdminRequest().
|
||||
bool co2PerformFRC(uint32_t targetCO2ppm) override
|
||||
{
|
||||
return targetCO2ppm <= UINT16_MAX && performFRC(static_cast<uint16_t>(targetCO2ppm));
|
||||
}
|
||||
bool co2GetASC(bool &ascEnabled) override
|
||||
{
|
||||
uint16_t v = 0;
|
||||
bool ok = getASC(v);
|
||||
ascEnabled = v != 0;
|
||||
return ok;
|
||||
}
|
||||
bool co2SetASC(bool ascEnabled) override { return setASC(ascEnabled); }
|
||||
bool co2SetASCBaseline(uint32_t targetCO2ppm) override
|
||||
{
|
||||
return targetCO2ppm <= UINT16_MAX && setASCBaseline(static_cast<uint16_t>(targetCO2ppm));
|
||||
}
|
||||
bool co2SetAltitude(uint32_t altitude) override
|
||||
{
|
||||
if (altitude > 3000)
|
||||
return false;
|
||||
return altitude <= UINT16_MAX && setAltitude(static_cast<uint16_t>(altitude));
|
||||
}
|
||||
bool co2SetAmbientPressure(uint32_t ambientPressurePa) override
|
||||
{
|
||||
if (ambientPressurePa < 70000 || ambientPressurePa > 120000)
|
||||
return false;
|
||||
return setAmbientPressure(ambientPressurePa);
|
||||
}
|
||||
bool co2FactoryReset() override { return factoryReset(); }
|
||||
|
||||
uint16_t ascActive = 1;
|
||||
// low power measurement mode (on sensirion side). Disables sleep mode
|
||||
// Improvement and testing needed for timings
|
||||
bool lowPower = true;
|
||||
// millis()-based, not wall-clock: this only measures in-session warmup elapsed time,
|
||||
// and getTime() can jump discontinuously when RTC quality improves mid-session.
|
||||
uint32_t co2MeasureStarted = 0;
|
||||
|
||||
public:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,200 +1,17 @@
|
||||
#pragma once
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
|
||||
|
||||
#include "../detect/ReClockI2C.h"
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include "Wire.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "SENXXSensor.h"
|
||||
|
||||
// Warm up times for SEN5X from the datasheet
|
||||
#ifndef SEN5X_WARMUP_MS_1
|
||||
#define SEN5X_WARMUP_MS_1 15000
|
||||
#endif
|
||||
|
||||
#ifndef SEN5X_WARMUP_MS_2
|
||||
#define SEN5X_WARMUP_MS_2 30000
|
||||
#endif
|
||||
|
||||
#ifndef SEN5X_POLL_INTERVAL
|
||||
#define SEN5X_POLL_INTERVAL 1000
|
||||
#endif
|
||||
|
||||
#ifndef SEN5X_I2C_CLOCK_SPEED
|
||||
#define SEN5X_I2C_CLOCK_SPEED 100000
|
||||
#endif
|
||||
|
||||
/*
|
||||
Time after which the sensor can go to sleep, as the warmup period has passed
|
||||
and the VOCs sensor will is allowed to stop (although needs to recover the state
|
||||
each time)
|
||||
*/
|
||||
#ifndef SEN5X_VOC_STATE_WARMUP_S
|
||||
/* Note for Testing 5' is enough
|
||||
Sensirion recommends 1h
|
||||
This can be bypassed completely if switching to low-power RHT/Gas mode and setting
|
||||
SEN5X_VOC_STATE_WARMUP_S 0
|
||||
*/
|
||||
#define SEN5X_VOC_STATE_WARMUP_S 3600
|
||||
#endif
|
||||
|
||||
#define ONE_WEEK_IN_SECONDS 604800
|
||||
|
||||
struct _SEN5XMeasurements {
|
||||
uint16_t pM1p0;
|
||||
uint16_t pM2p5;
|
||||
uint16_t pM4p0;
|
||||
uint16_t pM10p0;
|
||||
uint32_t pN0p5;
|
||||
uint32_t pN1p0;
|
||||
uint32_t pN2p5;
|
||||
uint32_t pN4p0;
|
||||
uint32_t pN10p0;
|
||||
float tSize;
|
||||
float humidity;
|
||||
float temperature;
|
||||
float vocIndex;
|
||||
float noxIndex;
|
||||
};
|
||||
|
||||
class SEN5XSensor : public TelemetrySensor
|
||||
// Thin identity wrapper around SENXXSensor for the SEN5X family (SEN50/54/55,
|
||||
// I2C address SEN5X_ADDR / 0x69). All protocol/state-machine logic lives in
|
||||
// SENXXSensor; the exact model is auto-detected in probe()/initDevice().
|
||||
class SEN5XSensor : public SENXXSensor
|
||||
{
|
||||
private:
|
||||
#ifdef SEN5X_I2C_CLOCK_SPEED
|
||||
ReClockI2C reClockI2C;
|
||||
#endif
|
||||
|
||||
bool getVersion();
|
||||
float firmwareVer = -1;
|
||||
float hardwareVer = -1;
|
||||
float protocolVer = -1;
|
||||
bool findModel();
|
||||
|
||||
// Commands
|
||||
#define SEN5X_RESET 0xD304
|
||||
#define SEN5X_GET_PRODUCT_NAME 0xD014
|
||||
#define SEN5X_GET_FIRMWARE_VERSION 0xD100
|
||||
#define SEN5X_START_MEASUREMENT 0x0021
|
||||
#define SEN5X_START_MEASUREMENT_RHT_GAS 0x0037
|
||||
#define SEN5X_STOP_MEASUREMENT 0x0104
|
||||
#define SEN5X_READ_DATA_READY 0x0202
|
||||
#define SEN5X_START_FAN_CLEANING 0x5607
|
||||
#define SEN5X_RW_VOCS_STATE 0x6181
|
||||
|
||||
#define SEN5X_READ_VALUES 0x03C4
|
||||
#define SEN5X_READ_RAW_VALUES 0x03D2
|
||||
#define SEN5X_READ_PM_VALUES 0x0413
|
||||
|
||||
// Values the sensor reports when a reading is unavailable
|
||||
#define SEN5X_UINT_INVALID 0xFFFF
|
||||
#define SEN5X_INT_INVALID 0x7FFF
|
||||
|
||||
// Reply payload sizes in data bytes; the raw I2C transfer adds one CRC byte
|
||||
// per 2-byte group, so requests are <size> + <size> / 2 raw bytes
|
||||
#define SEN5X_VERSION_BUFFER_SIZE 8
|
||||
#define SEN5X_PRODUCT_NAME_BUFFER_SIZE 32
|
||||
#define SEN5X_DATA_READY_BUFFER_SIZE 2
|
||||
#define SEN5X_READ_VALUES_BUFFER_SIZE 16
|
||||
#define SEN5X_READ_PM_BUFFER_SIZE 20
|
||||
|
||||
#define SEN5X_VOC_VALID_TIME 600
|
||||
#define SEN5X_VOC_VALID_DATE 1514764800
|
||||
|
||||
enum SEN5Xmodel { SEN5X_UNKNOWN = 0, SEN50 = 0b001, SEN54 = 0b010, SEN55 = 0b100 };
|
||||
SEN5Xmodel model = SEN5X_UNKNOWN;
|
||||
|
||||
enum SEN5XState {
|
||||
SEN5X_OFF,
|
||||
SEN5X_IDLE,
|
||||
SEN5X_RHTGAS_ONLY,
|
||||
SEN5X_MEASUREMENT,
|
||||
SEN5X_MEASUREMENT_2,
|
||||
SEN5X_CLEANING,
|
||||
SEN5X_NOT_DETECTED
|
||||
};
|
||||
SEN5XState state = SEN5X_OFF;
|
||||
// Flag to work on one-shot (read and sleep), or continuous mode
|
||||
bool oneShotMode = true;
|
||||
void setMode(bool setOneShot);
|
||||
bool vocStateValid();
|
||||
/* Sensirion recommends taking a reading after 15 seconds,
|
||||
if the Particle number reading is over 100#/cm3 the reading is OK,
|
||||
but if it is lower wait until 30 seconds and take it again.
|
||||
See: https://sensirion.com/resource/application_note/low_power_mode/sen5x
|
||||
*/
|
||||
#define SEN5X_PN4P0_CONC_THD 100
|
||||
|
||||
bool sendCommand(uint16_t command);
|
||||
/**
|
||||
* @brief Send a command word followed by a data payload; a CRC byte is
|
||||
* computed and inserted on the wire after every 2-byte pair.
|
||||
* @param command 16-bit command code, sent big-endian
|
||||
* @param buffer payload data bytes, without CRCs
|
||||
* @param byteNumber payload size in data bytes; must be even
|
||||
* @return true when the full transfer is written and acknowledged
|
||||
*/
|
||||
bool sendCommand(uint16_t command, uint8_t *buffer, uint8_t byteNumber = 0);
|
||||
/**
|
||||
* @brief Read a reply, verifying and stripping the interleaved CRC bytes.
|
||||
* @param buffer destination for the data bytes (byteNumber * 2 / 3 of them)
|
||||
* @param byteNumber raw transfer size including CRCs; must be a multiple
|
||||
* of 3 (2 data bytes + 1 CRC per group)
|
||||
* @return the number of data bytes written to buffer, or 0 on any error
|
||||
*/
|
||||
uint8_t readBuffer(uint8_t *buffer, uint8_t byteNumber);
|
||||
uint8_t sen5xCRC(const uint8_t *buffer);
|
||||
bool startCleaning();
|
||||
uint8_t getMeasurements();
|
||||
// bool readRawValues();
|
||||
bool readPNValues(bool cumulative);
|
||||
bool readValues();
|
||||
|
||||
uint32_t pmMeasureStarted = 0;
|
||||
uint32_t rhtGasMeasureStarted = 0;
|
||||
uint32_t lastDataPoll = 0;
|
||||
_SEN5XMeasurements sen5xmeasurement{};
|
||||
|
||||
bool idle(bool checkState = true);
|
||||
|
||||
protected:
|
||||
// Store status of the sensor in this file
|
||||
const char *sen5XStateFileName = "/prefs/sen5X.dat";
|
||||
meshtastic_SEN5XState sen5xstate = meshtastic_SEN5XState_init_zero;
|
||||
|
||||
bool loadState();
|
||||
bool saveState();
|
||||
|
||||
// Cleaning State
|
||||
uint32_t lastCleaning = 0;
|
||||
bool lastCleaningValid = false;
|
||||
|
||||
// VOC State
|
||||
#define SEN5X_VOC_STATE_BUFFER_SIZE 8
|
||||
uint8_t vocState[SEN5X_VOC_STATE_BUFFER_SIZE]{};
|
||||
uint32_t vocTime = 0;
|
||||
bool vocValid = false;
|
||||
|
||||
bool vocStateFromSensor();
|
||||
bool vocStateToSensor();
|
||||
bool vocStateStable();
|
||||
bool vocStateRecent(uint32_t now);
|
||||
|
||||
public:
|
||||
SEN5XSensor();
|
||||
bool probe(TwoWire *bus, uint8_t address, ScanI2C::I2CPort port);
|
||||
virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override;
|
||||
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
|
||||
|
||||
virtual bool isActive() override;
|
||||
virtual void sleep() override;
|
||||
virtual uint32_t wakeUp() override;
|
||||
virtual bool canSleep() override { return true; }
|
||||
virtual int32_t wakeUpTimeMs() override;
|
||||
virtual int32_t pendingForReadyMs() override;
|
||||
|
||||
AdminMessageHandleResult handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request,
|
||||
meshtastic_AdminMessage *response) override;
|
||||
SEN5XSensor() : SENXXSensor(meshtastic_TelemetrySensorType_SEN5X, "SEN5X") { senXXStateFileName = "/prefs/sen5X.dat"; }
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
|
||||
|
||||
#include "SENXXSensor.h"
|
||||
|
||||
// Thin identity wrapper around SENXXSensor for the SEN6X family (SEN62, SEN63C,
|
||||
// SEN65, SEN66, SEN68, SEN69C - I2C address SEN6X_ADDR / 0x6B). All
|
||||
// protocol/state-machine logic lives in SENXXSensor; the exact model is
|
||||
// auto-detected in probe()/initDevice().
|
||||
class SEN6XSensor : public SENXXSensor
|
||||
{
|
||||
public:
|
||||
SEN6XSensor() : SENXXSensor(meshtastic_TelemetrySensorType_SEN6X, "SEN6X") { senXXStateFileName = "/prefs/sen6X.dat"; }
|
||||
};
|
||||
|
||||
#endif
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,310 @@
|
||||
#pragma once
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
|
||||
|
||||
#include "../detect/ReClockI2C.h"
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "CO2Sensor.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include "Wire.h"
|
||||
#include "gps/RTC.h"
|
||||
|
||||
/*
|
||||
Shared driver for Sensirion's SEN5X and SEN6X particulate-matter sensor families
|
||||
(SEN50/54/55 and SEN62/63C/65/66/68/69C). All of these sensors speak the same
|
||||
16-bit-command + CRC8-framed word I2C protocol (reset, product name, start/stop
|
||||
measurement, data-ready, fan cleaning, VOC algorithm state, ...). The families
|
||||
differ only in:
|
||||
- I2C address (SEN5X_ADDR 0x69 vs SEN6X_ADDR 0x6B)
|
||||
- which physical quantities a given model exposes (PM is universal; RHT, VOC,
|
||||
NOx, CO2 and HCHO are present on some models and not others)
|
||||
- the opcode used to read measured values (SEN5X always uses one fixed-format
|
||||
command; each SEN6X model has its own opcode returning only the words that
|
||||
model supports)
|
||||
This class implements the shared protocol, state machine and admin handling
|
||||
once. SEN5XSensor / SEN6XSensor (see SEN5XSensor.h / SEN6XSensor.h) are thin
|
||||
subclasses that only supply the sensorType/sensorName identity.
|
||||
*/
|
||||
#define SENXX_PM_WARMUP_MS_1 15000
|
||||
#define SENXX_PM_WARMUP_MS_2 30000
|
||||
#define SENXX_POLL_INTERVAL 1000
|
||||
#define SENXX_I2C_CLOCK_SPEED 100000
|
||||
// How long a fan-cleaning cycle takes once started; polled via pendingForReadyMs()
|
||||
// rather than blocked on, see SENXX_CLEANING in SENXXState.
|
||||
#define SENXX_CLEANING_DURATION_MS 10500
|
||||
|
||||
/*
|
||||
Time after which the co2 sensor in some SEN6X variants give stable data
|
||||
*/
|
||||
#define SEN6X_CO2_WARMUP_MS 24000
|
||||
#define SENXX_VOC_VALID_TIME 600
|
||||
#define SENXX_VOC_VALID_DATE 1514764800
|
||||
|
||||
/*
|
||||
Time after which the sensor can go to sleep, as the warmup period has passed
|
||||
and the VOCs sensor will is allowed to stop (although needs to recover the state
|
||||
each time)
|
||||
Note: for Testing 5' is enough. Sensirion recommends 1h
|
||||
This can be bypassed completely if switching to low-power RHT/Gas mode and setting
|
||||
SENXX_VOC_STATE_WARMUP_S 0
|
||||
*/
|
||||
#define SENXX_VOC_STATE_WARMUP_S 3600
|
||||
#define SENXX_VOC_STATE_BUFFER_SIZE 8
|
||||
|
||||
/* Sensirion recommends taking a reading after 15 seconds,
|
||||
if the Particle number reading is over 100#/cm3 the reading is OK,
|
||||
but if it is lower wait until 30 seconds and take it again.
|
||||
See: https://sensirion.com/resource/application_note/low_power_mode/sen5x
|
||||
*/
|
||||
#define SENXX_PN4P0_CONC_THD 100
|
||||
#ifndef ONE_WEEK_IN_SECONDS
|
||||
#define ONE_WEEK_IN_SECONDS 604800
|
||||
#endif
|
||||
|
||||
// Commands shared identically by every SEN5X/SEN6X model
|
||||
#define SENXX_RESET 0xD304
|
||||
#define SENXX_GET_PRODUCT_NAME 0xD014
|
||||
#define SENXX_GET_FIRMWARE_VERSION 0xD100
|
||||
#define SENXX_START_MEASUREMENT 0x0021
|
||||
#define SENXX_STOP_MEASUREMENT 0x0104
|
||||
#define SENXX_READ_DATA_READY 0x0202
|
||||
#define SENXX_START_FAN_CLEANING 0x5607
|
||||
#define SENXX_RW_VOCS_STATE 0x6181
|
||||
|
||||
// SEN5X-only: low-power "RHT/Gas only" measurement mode and fixed-format read commands
|
||||
#define SEN5X_START_MEASUREMENT_RHT_GAS 0x0037
|
||||
#define SEN5X_READ_VALUES 0x03C4
|
||||
#define SEN5X_READ_PM_VALUES 0x0413
|
||||
|
||||
// SEN6X-only: shared number-concentration read command (per-model measured-values
|
||||
// opcode lives in readMeasuredValuesCmd, set once the model is known)
|
||||
#define SEN6X_READ_NUMBER_CONCENTRATION_VALUES 0x0316
|
||||
|
||||
// Values the sensor reports when a reading is unavailable (same sentinels across
|
||||
// the whole SEN5X/SEN6X family per Sensirion's datasheets)
|
||||
#define SENXX_UINT_INVALID 0xFFFF
|
||||
#define SENXX_INT_INVALID 0x7FFF
|
||||
|
||||
// Reply payload sizes in data bytes; the raw I2C transfer adds one CRC byte per
|
||||
// 2-byte group, so requests are <size> + <size> / 2 raw bytes
|
||||
#define SENXX_VERSION_BUFFER_SIZE 8
|
||||
#define SENXX_PRODUCT_NAME_BUFFER_SIZE 32
|
||||
#define SENXX_DATA_READY_BUFFER_SIZE 2
|
||||
#define SEN5X_READ_VALUES_BUFFER_SIZE 16
|
||||
#define SEN5X_READ_PM_BUFFER_SIZE 20
|
||||
|
||||
// SEN6X-only commands (all models: SEN62/63C/65/66/68/69C)
|
||||
#define SEN6X_GET_SET_TEMP_OFFSET 0x60B2
|
||||
#define SEN6X_READ_DEVICE_STATUS 0xD206
|
||||
// SEN6X-only, CO2-capable models only (SEN63C/66/69C)
|
||||
#define SEN6X_PERFORM_FORCED_CO2_RECAL 0x6707
|
||||
#define SEN6X_CO2_FACTORY_RESET 0x6754
|
||||
#define SEN6X_GET_SET_CO2_ASC 0x6711
|
||||
#define SEN6X_GET_SET_AMBIENT_PRESSURE 0x6720
|
||||
#define SEN6X_GET_SET_ALTITUDE 0x6736
|
||||
|
||||
struct _SENXXMeasurements {
|
||||
uint16_t pM1p0;
|
||||
uint16_t pM2p5;
|
||||
uint16_t pM4p0;
|
||||
uint16_t pM10p0;
|
||||
uint32_t pN0p5;
|
||||
uint32_t pN1p0;
|
||||
uint32_t pN2p5;
|
||||
uint32_t pN4p0;
|
||||
uint32_t pN10p0;
|
||||
float tSize;
|
||||
float humidity;
|
||||
float temperature;
|
||||
float vocIndex;
|
||||
float noxIndex;
|
||||
float co2;
|
||||
float hcho;
|
||||
};
|
||||
|
||||
class SENXXSensor : public TelemetrySensor, public CO2CalibrationSensor
|
||||
{
|
||||
protected:
|
||||
// Only subclasses (SEN5XSensor / SEN6XSensor) construct this; they supply the
|
||||
// proto sensorType/sensorName identity, everything else is auto-detected via
|
||||
// findModel() at probe/init time.
|
||||
SENXXSensor(meshtastic_TelemetrySensorType sensorType, const char *sensorName) : TelemetrySensor(sensorType, sensorName) {}
|
||||
|
||||
private:
|
||||
#ifdef SENXX_I2C_CLOCK_SPEED
|
||||
ReClockI2C reClockI2C;
|
||||
#endif
|
||||
|
||||
bool getVersion();
|
||||
float firmwareVer = -1;
|
||||
float hardwareVer = -1;
|
||||
float protocolVer = -1;
|
||||
bool findModel();
|
||||
|
||||
enum SENXXmodel {
|
||||
SENXX_UNKNOWN = 0,
|
||||
// SEN5X family - I2C address SEN5X_ADDR (0x69)
|
||||
SEN50,
|
||||
SEN54,
|
||||
SEN55,
|
||||
// SEN6X family - I2C address SEN6X_ADDR (0x6B)
|
||||
SEN62,
|
||||
SEN63C,
|
||||
SEN65,
|
||||
SEN66,
|
||||
SEN68,
|
||||
SEN69C,
|
||||
};
|
||||
SENXXmodel model = SENXX_UNKNOWN;
|
||||
|
||||
// True for any SEN6X-family model (SEN62/63C/65/66/68/69C)
|
||||
bool isSen6xFamily() { return model >= SEN62; }
|
||||
|
||||
// Per-model capabilities, derived once in updateCapabilities() right after
|
||||
// findModel() succeeds. Every read/state routine below is written against
|
||||
// these flags rather than against individual model checks, so adding a new
|
||||
// family member only means extending findModel()/updateCapabilities().
|
||||
bool hasRHT = false;
|
||||
bool hasVOC = false;
|
||||
bool hasNOx = false;
|
||||
bool hasCO2 = false;
|
||||
bool hasHCHO = false;
|
||||
void updateCapabilities();
|
||||
|
||||
// SEN6X: opcode for "Read Measured Values" - differs per model, see updateCapabilities()
|
||||
uint16_t readMeasuredValuesCmd = 0;
|
||||
|
||||
// Device Status Register bit positions (SEN6X only - see datasheet Figure 7)
|
||||
static constexpr uint32_t SEN6X_STATUS_FAN_ERROR = 1u << 4;
|
||||
static constexpr uint32_t SEN6X_STATUS_RHT_ERROR = 1u << 6;
|
||||
static constexpr uint32_t SEN6X_STATUS_GAS_ERROR = 1u << 7;
|
||||
static constexpr uint32_t SEN6X_STATUS_CO2_2_ERROR = 1u << 9; // SEN66 only
|
||||
static constexpr uint32_t SEN6X_STATUS_HCHO_ERROR = 1u << 10;
|
||||
static constexpr uint32_t SEN6X_STATUS_PM_ERROR = 1u << 11;
|
||||
static constexpr uint32_t SEN6X_STATUS_CO2_1_ERROR = 1u << 12; // SEN63C/SEN69C only
|
||||
static constexpr uint32_t SEN6X_STATUS_FAN_SPEED_WARNING = 1u << 21;
|
||||
|
||||
bool readDeviceStatus(uint32_t &statusFlags);
|
||||
void logDeviceStatus(uint32_t statusFlags);
|
||||
|
||||
// Sets the SEN6X RHT temperature-offset compensation (slot 0, applied immediately)
|
||||
// from the most recently measured temperature vs. a known-good reference. Unlike
|
||||
// SCD4X/SCD30 there is no "get current offset" command to accumulate against, so
|
||||
// this simply computes offset = lastMeasuredTemperature - tempReference.
|
||||
bool setTemperatureOffset(float tempReference);
|
||||
|
||||
// CO2CalibrationSensor overrides - only meaningful when hasCO2 (SEN63C/66/69C);
|
||||
// return false/no-op otherwise.
|
||||
bool co2PerformFRC(uint32_t targetCO2ppm) override;
|
||||
bool co2GetASC(bool &ascEnabled) override;
|
||||
bool co2SetASC(bool ascEnabled) override;
|
||||
bool co2SetAltitude(uint32_t altitude) override;
|
||||
bool co2SetAmbientPressure(uint32_t ambientPressurePa) override;
|
||||
bool co2FactoryReset() override;
|
||||
|
||||
enum SENXXState {
|
||||
SENXX_OFF,
|
||||
SENXX_IDLE,
|
||||
SENXX_RHTGAS_ONLY, // SEN5X Only
|
||||
SENXX_MEASUREMENT,
|
||||
SENXX_MEASUREMENT_2,
|
||||
SENXX_CLEANING,
|
||||
SENXX_NOT_DETECTED
|
||||
};
|
||||
SENXXState state = SENXX_OFF;
|
||||
// Flag to work on one-shot (read and sleep), or continuous mode
|
||||
// Recommendation: if it has VOC / NOx, suggest NOT to use oneShot mode
|
||||
bool oneShotMode = true;
|
||||
void setMode(bool setOneShot);
|
||||
bool vocStateValid();
|
||||
|
||||
// Tracks getRTCQuality() across calls so we can notice the moment a real clock
|
||||
// becomes available (e.g. the phone/WiFi/GPS sets it well after boot), rather than
|
||||
// only checking once in initDevice(). See checkRTCQualityImproved()/
|
||||
// reconcileTimeDependentState() for how this is used.
|
||||
RTCQuality lastRTCQuality = RTCQualityNone;
|
||||
bool checkRTCQualityImproved();
|
||||
void reconcileTimeDependentState(uint32_t now);
|
||||
|
||||
bool sendCommand(uint16_t command);
|
||||
/**
|
||||
* @brief Send a command word followed by a data payload; a CRC byte is
|
||||
* computed and inserted on the wire after every 2-byte pair.
|
||||
* @param command 16-bit command code, sent big-endian
|
||||
* @param buffer payload data bytes, without CRCs
|
||||
* @param byteNumber payload size in data bytes; must be even
|
||||
* @return true when the full transfer is written and acknowledged
|
||||
*/
|
||||
bool sendCommand(uint16_t command, uint8_t *buffer, uint8_t byteNumber = 0);
|
||||
/**
|
||||
* @brief Read a reply, verifying and stripping the interleaved CRC bytes.
|
||||
* @param buffer destination for the data bytes (byteNumber * 2 / 3 of them)
|
||||
* @param byteNumber raw transfer size including CRCs; must be a multiple
|
||||
* of 3 (2 data bytes + 1 CRC per group)
|
||||
* @return the number of data bytes written to buffer, or 0 on any error
|
||||
*/
|
||||
uint8_t readBuffer(uint8_t *buffer, uint8_t byteNumber);
|
||||
uint8_t senxxCRC(const uint8_t *buffer);
|
||||
// Starts a fan-cleaning cycle and returns immediately (does not block for the
|
||||
// ~10.5s the cycle takes); pendingForReadyMs() polls SENXX_CLEANING to completion
|
||||
// and calls finishCleaning() once done.
|
||||
bool startCleaning();
|
||||
void finishCleaning();
|
||||
uint8_t getMeasurements();
|
||||
bool readPNValues(bool cumulative);
|
||||
bool readValues();
|
||||
|
||||
// Monotonic (millis()) timers for warmup/poll intervals. Deliberately not
|
||||
// wall-clock (getTime()) based: getTime() can jump discontinuously the moment the RTC
|
||||
// quality improves mid-session (see checkRTCQualityImproved()), which would corrupt
|
||||
// these short elapsed-time computations. millis() is immune to that and wraps only every ~49 days.
|
||||
uint32_t pmMeasureStarted = 0;
|
||||
uint32_t rhtGasMeasureStarted = 0;
|
||||
uint32_t lastDataPoll = 0;
|
||||
uint32_t cleaningStarted = 0;
|
||||
_SENXXMeasurements senxxmeasurement{};
|
||||
|
||||
bool idle(bool checkState = true);
|
||||
|
||||
protected:
|
||||
// Store status of the sensor in this file. SEN5X and SEN6X keep separate prefs
|
||||
// files/proto messages so existing SEN5X saved state is unaffected.
|
||||
const char *senXXStateFileName = nullptr;
|
||||
meshtastic_SEN5XState sen5xstate = meshtastic_SEN5XState_init_zero;
|
||||
meshtastic_SEN6XState sen6xstate = meshtastic_SEN6XState_init_zero;
|
||||
|
||||
bool loadState();
|
||||
bool saveState();
|
||||
|
||||
// Cleaning State
|
||||
uint32_t lastCleaning = 0;
|
||||
bool lastCleaningValid = false;
|
||||
|
||||
// VOC State
|
||||
uint8_t vocState[SENXX_VOC_STATE_BUFFER_SIZE]{};
|
||||
uint32_t vocTime = 0;
|
||||
bool vocValid = false;
|
||||
|
||||
bool vocStateFromSensor();
|
||||
bool vocStateToSensor();
|
||||
bool vocStateStable();
|
||||
bool vocStateRecent(uint32_t now);
|
||||
|
||||
public:
|
||||
bool probe(TwoWire *bus, uint8_t address, ScanI2C::I2CPort port);
|
||||
virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override;
|
||||
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
|
||||
|
||||
virtual bool isActive() override;
|
||||
virtual void sleep() override;
|
||||
virtual uint32_t wakeUp() override;
|
||||
virtual bool canSleep() override { return true; }
|
||||
virtual int32_t wakeUpTimeMs() override;
|
||||
virtual int32_t pendingForReadyMs() override;
|
||||
|
||||
AdminMessageHandleResult handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request,
|
||||
meshtastic_AdminMessage *response) override;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -46,8 +46,8 @@ bool SFA30Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
|
||||
|
||||
status = 1;
|
||||
state = State::ACTIVE;
|
||||
measureStarted = getTime();
|
||||
LOG_INFO("%s: Enabled", sensorName);
|
||||
measureStarted = millis();
|
||||
LOG_INFO("%s Enabled", sensorName);
|
||||
|
||||
initI2CSensor();
|
||||
return true;
|
||||
@@ -102,7 +102,7 @@ uint32_t SFA30Sensor::wakeUp()
|
||||
#endif /* SFA30_I2C_CLOCK_SPEED */
|
||||
|
||||
state = State::ACTIVE;
|
||||
measureStarted = getTime();
|
||||
measureStarted = millis();
|
||||
return SFA30_WARMUP_MS;
|
||||
}
|
||||
|
||||
@@ -125,9 +125,7 @@ bool SFA30Sensor::isActive()
|
||||
|
||||
int32_t SFA30Sensor::pendingForReadyMs()
|
||||
{
|
||||
uint32_t now;
|
||||
now = getTime();
|
||||
uint32_t sinceHchoMeasureStarted = (now - measureStarted) * 1000;
|
||||
uint32_t sinceHchoMeasureStarted = millis() - measureStarted;
|
||||
LOG_DEBUG("%s: Since measure started: %ums", sensorName, sinceHchoMeasureStarted);
|
||||
|
||||
if (sinceHchoMeasureStarted < SFA30_WARMUP_MS) {
|
||||
|
||||
@@ -17,6 +17,8 @@ class SFA30Sensor : public TelemetrySensor
|
||||
private:
|
||||
enum class State { IDLE, ACTIVE };
|
||||
State state = State::IDLE;
|
||||
// millis()-based, not wall-clock: this only measures in-session warmup elapsed time,
|
||||
// and getTime() can jump discontinuously when RTC quality improves mid-session.
|
||||
uint32_t measureStarted = 0;
|
||||
|
||||
SensirionI2cSfa3x sfa30;
|
||||
|
||||
Reference in New Issue
Block a user