mirror of
https://github.com/meshtastic/firmware.git
synced 2026-06-06 10:29:27 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8f18534ebd | |||
| fd084abbd9 |
@@ -214,6 +214,8 @@ lib_deps =
|
||||
sensirion/Sensirion Core@0.7.2
|
||||
# renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x
|
||||
sensirion/Sensirion I2C SCD4x@1.1.0
|
||||
# renovate: datasource=custom.pio depName=Sensirion I2C STC3x packageName=sensirion/library/Sensirion I2C STC3x
|
||||
sensirion/Sensirion I2C STC3x@1.0.1
|
||||
|
||||
; Same as environmental_extra but without BSEC (saves ~3.5KB DRAM for original ESP32 targets)
|
||||
[environmental_extra_no_bsec]
|
||||
@@ -242,3 +244,5 @@ lib_deps =
|
||||
sensirion/Sensirion Core@0.7.2
|
||||
# renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x
|
||||
sensirion/Sensirion I2C SCD4x@1.1.0
|
||||
# renovate: datasource=custom.pio depName=Sensirion I2C STC3x packageName=sensirion/library/Sensirion I2C STC3x
|
||||
sensirion/Sensirion I2C STC3x@1.0.1
|
||||
|
||||
+1
-1
Submodule protobufs updated: 27591d98c4...e1a6b3a868
@@ -234,6 +234,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#define MAX30102_ADDR 0x57
|
||||
#define SCD4X_ADDR 0x62
|
||||
#define CW2015_ADDR 0x62
|
||||
#define STC31_ADDR 0x2c // RAK12008 uses 0x2C address
|
||||
#define MLX90614_ADDR_DEF 0x5A
|
||||
#define CGRADSENS_ADDR 0x66
|
||||
#define LTR390UV_ADDR 0x53
|
||||
|
||||
@@ -90,7 +90,8 @@ class ScanI2C
|
||||
CHSC6X,
|
||||
CST226SE,
|
||||
CW2015,
|
||||
SEN5X
|
||||
SEN5X,
|
||||
STC31
|
||||
} DeviceType;
|
||||
|
||||
// typedef uint8_t DeviceAddress;
|
||||
|
||||
@@ -154,6 +154,42 @@ String readSEN5xProductName(TwoWire *i2cBus, uint8_t address)
|
||||
return String(productName);
|
||||
}
|
||||
|
||||
/// for STC31 detection - probe by reading product identifier
|
||||
/// Uses two-step sequence: prepareProductIdentifier (0x367c) then readProductIdentifier (0xe102)
|
||||
bool probeSTC31(TwoWire *i2cBus, uint8_t address)
|
||||
{
|
||||
// Step 1: Send prepare product identifier command (0x367c)
|
||||
uint8_t prepCmd[] = {0x36, 0x7c};
|
||||
i2cBus->beginTransmission(address);
|
||||
i2cBus->write(prepCmd, 2);
|
||||
if (i2cBus->endTransmission() != 0)
|
||||
return false;
|
||||
delay(1);
|
||||
|
||||
// Step 2: Send read product identifier command (0xe102)
|
||||
uint8_t readCmd[] = {0xe1, 0x02};
|
||||
i2cBus->beginTransmission(address);
|
||||
i2cBus->write(readCmd, 2);
|
||||
if (i2cBus->endTransmission() != 0)
|
||||
return false;
|
||||
delay(10);
|
||||
|
||||
// Read 6 bytes: product number (4 bytes with CRC) + more data
|
||||
// Product number is 32-bit: STC31 = 0x08010301
|
||||
if (i2cBus->requestFrom(address, (uint8_t)6) != 6)
|
||||
return false;
|
||||
|
||||
uint8_t response[6] = {0};
|
||||
for (int i = 0; i < 6 && i2cBus->available(); ++i) {
|
||||
response[i] = i2cBus->read();
|
||||
}
|
||||
|
||||
uint32_t productId =
|
||||
((uint32_t)response[0] << 24) | ((uint32_t)response[1] << 16) | ((uint32_t)response[3] << 8) | response[4];
|
||||
|
||||
return productId == 0x08010301;
|
||||
}
|
||||
|
||||
#define SCAN_SIMPLE_CASE(ADDR, T, ...) \
|
||||
case ADDR: \
|
||||
logFoundDevice(__VA_ARGS__); \
|
||||
@@ -532,13 +568,23 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
}
|
||||
break;
|
||||
case TSL25911_ADDR:
|
||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xA0 | 0x12), 1);
|
||||
if (registerValue == 0x50) {
|
||||
type = TSL2591;
|
||||
logFoundDevice("TSL25911", (uint8_t)addr.address);
|
||||
// TSL25911_ADDR (0x29) is shared by TSL2591, TSL2561, and STC31
|
||||
// Probe for STC31 first (Sensirion CO2 sensor)
|
||||
LOG_DEBUG("Probing address 0x29 for STC31...");
|
||||
if (probeSTC31(i2cBus, addr.address)) {
|
||||
type = STC31;
|
||||
LOG_DEBUG("STC31 probe successful!");
|
||||
logFoundDevice("STC31", (uint8_t)addr.address);
|
||||
} else {
|
||||
type = TSL2561;
|
||||
logFoundDevice("TSL2561", (uint8_t)addr.address);
|
||||
LOG_DEBUG("STC31 probe failed, checking for TSL sensors...");
|
||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xA0 | 0x12), 1);
|
||||
if (registerValue == 0x50) {
|
||||
type = TSL2591;
|
||||
logFoundDevice("TSL25911", (uint8_t)addr.address);
|
||||
} else {
|
||||
type = TSL2561;
|
||||
logFoundDevice("TSL2561", (uint8_t)addr.address);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -548,6 +594,18 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
SCAN_SIMPLE_CASE(DFROBOT_RAIN_ADDR, DFROBOT_RAIN, "DFRobot Rain Gauge", (uint8_t)addr.address);
|
||||
SCAN_SIMPLE_CASE(LTR390UV_ADDR, LTR390UV, "LTR390UV", (uint8_t)addr.address);
|
||||
SCAN_SIMPLE_CASE(PCT2075_ADDR, PCT2075, "PCT2075", (uint8_t)addr.address);
|
||||
case STC31_ADDR:
|
||||
// RAK12008 uses address 0x2C for STC31
|
||||
LOG_DEBUG("Probing address 0x2c for STC31...");
|
||||
if (probeSTC31(i2cBus, addr.address)) {
|
||||
type = STC31;
|
||||
LOG_DEBUG("STC31 probe successful at 0x2c!");
|
||||
logFoundDevice("STC31", (uint8_t)addr.address);
|
||||
} else {
|
||||
LOG_DEBUG("STC31 probe failed at 0x2c");
|
||||
LOG_INFO("Device found at address 0x%x was not able to be enumerated", (uint8_t)addr.address);
|
||||
}
|
||||
break;
|
||||
case CST328_ADDR:
|
||||
// Do we have the CST328 or the CST226SE
|
||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xAB), 1);
|
||||
|
||||
@@ -680,6 +680,7 @@ void setup()
|
||||
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::ICM20948, meshtastic_TelemetrySensorType_ICM20948);
|
||||
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MAX30102, meshtastic_TelemetrySensorType_MAX30102);
|
||||
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::SCD4X, meshtastic_TelemetrySensorType_SCD4X);
|
||||
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::STC31, meshtastic_TelemetrySensorType_STC31);
|
||||
#endif
|
||||
|
||||
#ifdef HAS_SDCARD
|
||||
|
||||
@@ -25,9 +25,14 @@
|
||||
#if __has_include(<SensirionI2cScd4x.h>)
|
||||
#include "Sensor/SCD4XSensor.h"
|
||||
#endif
|
||||
#if __has_include(<SensirionI2cStc3x.h>)
|
||||
#include "Sensor/STC31Sensor.h"
|
||||
#endif
|
||||
|
||||
void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner)
|
||||
{
|
||||
LOG_DEBUG("AirQualityTelemetryModule::i2cScanFinished called, air_quality_enabled=%d",
|
||||
moduleConfig.telemetry.air_quality_enabled);
|
||||
if (!moduleConfig.telemetry.air_quality_enabled && !AIR_QUALITY_TELEMETRY_MODULE_ENABLE) {
|
||||
return;
|
||||
}
|
||||
@@ -50,6 +55,9 @@ void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner)
|
||||
#if __has_include(<SensirionI2cScd4x.h>)
|
||||
addSensor<SCD4XSensor>(i2cScanner, ScanI2C::DeviceType::SCD4X);
|
||||
#endif
|
||||
#if __has_include(<SensirionI2cStc3x.h>)
|
||||
addSensor<STC31Sensor>(i2cScanner, ScanI2C::DeviceType::STC31);
|
||||
#endif
|
||||
}
|
||||
|
||||
int32_t AirQualityTelemetryModule::runOnce()
|
||||
@@ -92,6 +100,10 @@ int32_t AirQualityTelemetryModule::runOnce()
|
||||
return disable();
|
||||
}
|
||||
|
||||
bool isSensorRole = config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR;
|
||||
// SENSOR role bypasses airtime restrictions - telemetry is its primary purpose
|
||||
bool airTimeAllows = isSensorRole || (airTime->isTxAllowedChannelUtil(true) && airTime->isTxAllowedAirUtil());
|
||||
|
||||
// Wake up the sensors that need it
|
||||
LOG_INFO("Waking up sensors...");
|
||||
for (TelemetrySensor *sensor : sensors) {
|
||||
@@ -102,8 +114,7 @@ int32_t AirQualityTelemetryModule::runOnce()
|
||||
Default::getConfiguredOrDefaultMsScaled(
|
||||
moduleConfig.telemetry.air_quality_interval,
|
||||
default_telemetry_broadcast_interval_secs, numOnlineNodes))) &&
|
||||
airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) &&
|
||||
airTime->isTxAllowedAirUtil()) {
|
||||
airTimeAllows) {
|
||||
if (!sensor->isActive()) {
|
||||
LOG_DEBUG("Waking up: %s", sensor->sensorName);
|
||||
return sensor->wakeUp();
|
||||
@@ -117,18 +128,19 @@ int32_t AirQualityTelemetryModule::runOnce()
|
||||
}
|
||||
}
|
||||
|
||||
if (((lastSentToMesh == 0) ||
|
||||
!Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled(
|
||||
moduleConfig.telemetry.air_quality_interval,
|
||||
default_telemetry_broadcast_interval_secs, numOnlineNodes))) &&
|
||||
airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) &&
|
||||
airTime->isTxAllowedAirUtil()) {
|
||||
bool timeToSend = (lastSentToMesh == 0) ||
|
||||
!Throttle::isWithinTimespanMs(
|
||||
lastSentToMesh,
|
||||
Default::getConfiguredOrDefaultMsScaled(moduleConfig.telemetry.air_quality_interval,
|
||||
default_telemetry_broadcast_interval_secs, numOnlineNodes));
|
||||
|
||||
if (timeToSend && airTimeAllows) {
|
||||
sendTelemetry();
|
||||
lastSentToMesh = millis();
|
||||
} else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) &&
|
||||
(service->isToPhoneQueueEmpty())) {
|
||||
(isSensorRole || service->isToPhoneQueueEmpty())) {
|
||||
// Just send to phone when it's not our time to send to mesh yet
|
||||
// Only send while queue is empty (phone assumed connected)
|
||||
// SENSOR role always sends; others only send while queue is empty (phone assumed connected)
|
||||
sendTelemetry(NODENUM_BROADCAST, true);
|
||||
lastSentToPhone = millis();
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
// Sensors
|
||||
#include "Sensor/CGRadSensSensor.h"
|
||||
#include "Sensor/RCWL9620Sensor.h"
|
||||
#include "Sensor/TelemetrySensor.h"
|
||||
#include "Sensor/nullSensor.h"
|
||||
|
||||
namespace graphics
|
||||
@@ -571,6 +572,13 @@ bool EnvironmentTelemetryModule::getEnvironmentTelemetry(meshtastic_Telemetry *m
|
||||
valid = valid || get_metrics;
|
||||
hasSensor = true;
|
||||
#endif
|
||||
|
||||
// Update shared humidity for cross-sensor compensation (e.g., STC31 CO2 sensor)
|
||||
if (m->variant.environment_metrics.has_relative_humidity) {
|
||||
lastEnvironmentHumidity = m->variant.environment_metrics.relative_humidity;
|
||||
hasValidHumidity = true;
|
||||
}
|
||||
|
||||
return valid && hasSensor;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include(<SensirionI2cStc3x.h>)
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "STC31Sensor.h"
|
||||
|
||||
#define STC31_NO_ERROR 0
|
||||
|
||||
// Binary gas configurations from STC31 datasheet
|
||||
// See: setBinaryGas() - section 3.3.2 in the datasheet
|
||||
#define STC31_BINARY_GAS_CO2_N2_100 0x0000 // CO2 in N2, 0-100 vol%
|
||||
#define STC31_BINARY_GAS_CO2_AIR_100 0x0001 // CO2 in air, 0-100 vol%
|
||||
#define STC31_BINARY_GAS_CO2_N2_25 0x0002 // CO2 in N2, 0-25 vol%
|
||||
#define STC31_BINARY_GAS_CO2_AIR_25 0x0003 // CO2 in air, 0-25 vol%
|
||||
|
||||
STC31Sensor::STC31Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_STC31, "STC31") {}
|
||||
|
||||
bool STC31Sensor::configureBinaryGas()
|
||||
{
|
||||
// STC31 requires binary gas mode to be set before measurements
|
||||
// It resets to default mode (no gas selected) after power cycle or reset
|
||||
LOG_DEBUG("%s: Configuring binary gas mode (CO2 in air, 0-25%%)", sensorName);
|
||||
int16_t error = stc3x.setBinaryGas(STC31_BINARY_GAS_CO2_AIR_25);
|
||||
if (error != STC31_NO_ERROR) {
|
||||
LOG_ERROR("%s: Failed to set binary gas mode, error: %d", sensorName, error);
|
||||
return false;
|
||||
}
|
||||
LOG_DEBUG("%s: Binary gas mode configured successfully", sensorName);
|
||||
binaryGasConfigured = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool STC31Sensor::setHumidityCompensation()
|
||||
{
|
||||
// Use actual humidity from environment sensor if available, otherwise default to 50%
|
||||
float humidity = hasValidHumidity ? lastEnvironmentHumidity : 50.0f;
|
||||
|
||||
LOG_DEBUG("%s: Setting humidity compensation to %.1f%%", sensorName, humidity);
|
||||
int16_t error = stc3x.setRelativeHumidity(humidity);
|
||||
if (error != STC31_NO_ERROR) {
|
||||
LOG_WARN("%s: Failed to set humidity compensation (%.1f%%), error: %d", sensorName, humidity, error);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool STC31Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
|
||||
{
|
||||
LOG_INFO("Init sensor: %s at address 0x%02x", sensorName, dev->address.address);
|
||||
|
||||
_bus = bus;
|
||||
_address = dev->address.address;
|
||||
|
||||
stc3x.begin(*_bus, _address);
|
||||
|
||||
// Allow sensor to fully stabilize after power-on
|
||||
delay(100);
|
||||
|
||||
// Run self-test to verify sensor is working
|
||||
STC3xTestResultT selfTestResult;
|
||||
int16_t testError = stc3x.selfTest(selfTestResult);
|
||||
if (testError != STC31_NO_ERROR) {
|
||||
LOG_WARN("%s: Self-test command failed with error: %d", sensorName, testError);
|
||||
} else if (selfTestResult.value != 0) {
|
||||
LOG_WARN("%s: Self-test reported error: 0x%04x", sensorName, selfTestResult.value);
|
||||
} else {
|
||||
LOG_DEBUG("%s: Self-test passed", sensorName);
|
||||
}
|
||||
|
||||
// Wait after self-test before configuring
|
||||
delay(50);
|
||||
|
||||
if (!configureBinaryGas())
|
||||
return false;
|
||||
|
||||
setHumidityCompensation(); // Non-fatal if this fails
|
||||
|
||||
status = 1;
|
||||
initI2CSensor();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool STC31Sensor::getMetrics(meshtastic_Telemetry *measurement)
|
||||
{
|
||||
float gasConcentration = 0;
|
||||
float temperature = 0;
|
||||
|
||||
// Binary gas mode is configured once during initDevice() and persists
|
||||
// Humidity compensation can be updated for each measurement
|
||||
setHumidityCompensation(); // Non-fatal if this fails
|
||||
|
||||
// Small delay for humidity compensation to take effect
|
||||
delay(10);
|
||||
|
||||
// Retry measurement up to 3 times with increasing delays
|
||||
int16_t error = STC31_NO_ERROR;
|
||||
for (int attempt = 0; attempt < 3; attempt++) {
|
||||
error = stc3x.measureGasConcentration(gasConcentration, temperature);
|
||||
if (error == STC31_NO_ERROR)
|
||||
break;
|
||||
LOG_WARN("%s: Measurement attempt %d failed (error %d), retrying...", sensorName, attempt + 1, error);
|
||||
|
||||
// If we get NotEnoughDataError (0x00F in error), the sensor may have lost its config
|
||||
// Try re-configuring binary gas mode
|
||||
if ((error & 0x00F) == 0x00F && attempt == 1) {
|
||||
LOG_WARN("%s: Sensor may have lost configuration, re-configuring binary gas mode", sensorName);
|
||||
configureBinaryGas();
|
||||
delay(100);
|
||||
} else {
|
||||
delay(100 * (attempt + 1));
|
||||
}
|
||||
}
|
||||
|
||||
if (error != STC31_NO_ERROR) {
|
||||
LOG_ERROR("%s: Error reading measurement after retries: %d", sensorName, error);
|
||||
// Mark that we need to re-configure on next attempt
|
||||
binaryGasConfigured = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Convert percentage to ppm: 1% = 10000 ppm
|
||||
uint32_t co2_ppm = (uint32_t)(gasConcentration * 10000.0f);
|
||||
|
||||
LOG_DEBUG("%s readings: %.2f%% CO2 (=%u ppm), %.2f degC", sensorName, gasConcentration, co2_ppm, temperature);
|
||||
|
||||
measurement->variant.air_quality_metrics.has_co2 = true;
|
||||
measurement->variant.air_quality_metrics.co2 = co2_ppm;
|
||||
|
||||
measurement->variant.air_quality_metrics.has_co2_temperature = true;
|
||||
measurement->variant.air_quality_metrics.co2_temperature = temperature;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,26 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include(<SensirionI2cStc3x.h>)
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include <SensirionI2cStc3x.h>
|
||||
|
||||
class STC31Sensor : public TelemetrySensor
|
||||
{
|
||||
private:
|
||||
SensirionI2cStc3x stc3x;
|
||||
TwoWire *_bus{};
|
||||
uint8_t _address{};
|
||||
bool binaryGasConfigured = false;
|
||||
|
||||
bool configureBinaryGas();
|
||||
bool setHumidityCompensation();
|
||||
|
||||
public:
|
||||
STC31Sensor();
|
||||
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
|
||||
virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -7,4 +7,9 @@
|
||||
#include "TelemetrySensor.h"
|
||||
#include "main.h"
|
||||
|
||||
// Shared humidity value for cross-sensor compensation
|
||||
// Default to 50% if no humidity sensor is available
|
||||
float lastEnvironmentHumidity = 50.0f;
|
||||
bool hasValidHumidity = false;
|
||||
|
||||
#endif
|
||||
@@ -16,6 +16,11 @@ class TwoWire;
|
||||
#define DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS 1000
|
||||
extern std::pair<uint8_t, TwoWire *> nodeTelemetrySensorsMap[_meshtastic_TelemetrySensorType_MAX + 1];
|
||||
|
||||
// Shared humidity value for cross-sensor compensation (e.g., STC31 CO2 sensor)
|
||||
// Updated by environment sensors that measure humidity
|
||||
extern float lastEnvironmentHumidity;
|
||||
extern bool hasValidHumidity;
|
||||
|
||||
class TelemetrySensor
|
||||
{
|
||||
protected:
|
||||
|
||||
Reference in New Issue
Block a user