diff --git a/platformio.ini b/platformio.ini index f77df9813..a89ec0d9d 100644 --- a/platformio.ini +++ b/platformio.ini @@ -227,6 +227,8 @@ lib_deps = https://github.com/Sensirion/arduino-sht/archive/refs/tags/v1.2.6.zip # renovate: datasource=github-tags depName=SPA06_003 packageName=adafruit/Adafruit_SPA06_003 https://github.com/adafruit/Adafruit_SPA06_003/archive/refs/tags/1.0.2.zip + # renovate: datasource=github-tags depName=Seeed_PM2_5_sensor_HM3301 packageName=meshtastic/Seeed_PM2_5_sensor_HM3301 + https://github.com/meshtastic/Seeed_PM2_5_sensor_HM3301/archive/2704ca254c7e2136c52ac23198dd05f5ba1e2f04.zip ; Common environmental sensor libraries (not included in native / portduino) [environmental_extra_common] diff --git a/src/configuration.h b/src/configuration.h index ccf4ba41f..f9be2fc46 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -303,6 +303,8 @@ along with this program. If not, see . #define DS248X_ADDR_ALT5 0x1D // same as DFROBOT_RAIN_ADDR #define DS248X_ADDR_ALT6 0x1E // same as HMC5883L_ADDR #define DS248X_ADDR_ALT7 0x1F // same as BBQ10_KB_ADDR +#define HM330X_ADDR 0x40 + // ----------------------------------------------------------------------------- // ACCELEROMETER diff --git a/src/detect/ScanI2C.h b/src/detect/ScanI2C.h index 491038878..70f848b33 100644 --- a/src/detect/ScanI2C.h +++ b/src/detect/ScanI2C.h @@ -104,7 +104,8 @@ class ScanI2C ISM330DHCX, SPA06, DS248X, - } DeviceType; + HM330X + } DeviceType; // typedef uint8_t DeviceAddress; typedef enum I2CPort { diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index 5f04755e6..0aa2301e3 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -165,6 +165,20 @@ bool probeSEN5X(TwoWire *i2cBus, uint8_t address, ScanI2C::I2CPort port) SEN5XSensor sen5xsensor; return sen5xsensor.probe(i2cBus, address, port); } + +bool probeHM330x(TwoWire *i2cBus, uint8_t address) +{ + + i2cBus->beginTransmission(address); + i2cBus->write(0X88); + byte ret = i2cBus->endTransmission(); + + if (ret == 0) { + return true; + } + + return false; +} #endif #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR @@ -497,7 +511,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) break; #endif #if !defined(M5STACK_UNITC6L) - case INA_ADDR: // Same as SHT2X + case INA_ADDR: // same as HM330X, ES7210 and SHT2X case INA_ADDR_ALTERNATE: case INA_ADDR_WAVESHARE_UPS: { uint16_t mfg = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFE), 2); @@ -514,39 +528,51 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) if (mfg == 0x5449 && die == 0x2260) { logFoundDevice("INA226", (uint8_t)addr.address); type = INA226; + break; } // Silergy SQ52201 (INA226-compatible with different IDs) else if (mfg == 0x190F && die == 0x0000) { logFoundDevice("INA226 (SQ52201)", (uint8_t)addr.address); type = INA226; + break; } // TI INA260 else if (mfg == 0x5449) { logFoundDevice("INA260", (uint8_t)addr.address); type = INA260; + break; } } // The ES7210 audio ADC shares this address on some boards (T-Deck). It answers // none of the checks above, so identify it here and leave the type unset - driving // an audio codec as if it were a power monitor crashes the device. See #11115. - if (type == NONE && detectES7210(i2cBus, (uint8_t)addr.address)) { + if (detectES7210(i2cBus, (uint8_t)addr.address)) { LOG_INFO("ES7210 audio codec at 0x%x, not a power sensor", (uint8_t)addr.address); break; } -#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR - if (type == NONE && detectSHT21SerialNumber(i2cBus, (uint8_t)addr.address)) { +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && HAS_TELEMETRY + if (detectSHT21SerialNumber(i2cBus, (uint8_t)addr.address)) { logFoundDevice("SHTXX (SHT2X)", (uint8_t)addr.address); type = SHTXX; + break; } #endif - // Guarded on the type rather than chained to the SHT2X check above, so that a - // positively identified INA226/INA260 isn't overwritten right after being found. - if (type == NONE) { // Assume INA219 if none of the above ones are found + // Get INA219 configuration register 0x00 expecting 0x399F after power-on reset + uint16_t registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 2); + if (registerValue == 0x399F) { logFoundDevice("INA219", (uint8_t)addr.address); type = INA219; + break; } - break; +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && HAS_TELEMETRY + // Assume HM330x as the weakest detection method if none of the above ones are found + if (probeHM330x(i2cBus, addr.address)) { + logFoundDevice("HM330X", (uint8_t)addr.address); + type = HM330X; + break; + } +#endif } case INA3221_ADDR: registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFE), 2); diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index e3e05dac5..dc4612c70 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -36,6 +36,9 @@ static constexpr uint16_t TX_HISTORY_KEY_AIR_QUALITY_TELEMETRY = 0x8004; #if __has_include() #include "Sensor/SCD30Sensor.h" #endif +#if __has_include() +#include "Sensor/HM330XSensor.h" +#endif void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) { @@ -114,6 +117,9 @@ void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::SCD30); #endif +#if __has_include() + addSensor(i2cScanner, ScanI2C::DeviceType::HM330X); +#endif } int32_t AirQualityTelemetryModule::runOnce() diff --git a/src/modules/Telemetry/Sensor/HM330XSensor.cpp b/src/modules/Telemetry/Sensor/HM330XSensor.cpp new file mode 100644 index 000000000..1d44cd133 --- /dev/null +++ b/src/modules/Telemetry/Sensor/HM330XSensor.cpp @@ -0,0 +1,135 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include() + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "HM330XSensor.h" + +HM330XSensor::HM330XSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_HM330X, "HM330X"){}; + +bool HM330XSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) +{ + LOG_INFO("Init sensor: %s", sensorName); + + _bus = bus; + _address = dev->address.address; + +#ifdef HM330X_I2C_CLOCK_SPEED + _port = dev->address.port; + reClockI2C.setup(_bus, _port); + + LOG_INFO("%s: attempting to reclock speed to %uHz", sensorName, HM330X_I2C_CLOCK_SPEED); + reClockI2C.setClock(HM330X_I2C_CLOCK_SPEED); +#endif /* HM330X_I2C_CLOCK_SPEED */ + + if (hm330x.init(_bus) != HM330XErrorCode::NO_ERROR) { +#ifdef HM330X_I2C_CLOCK_SPEED + LOG_INFO("%s: restoring clock speed", sensorName); + reClockI2C.restoreClock(); +#endif /* HM330X_I2C_CLOCK_SPEED */ + LOG_WARN("%s error in sensor init", sensorName); + return false; + } + +#ifdef HM330X_I2C_CLOCK_SPEED + LOG_INFO("%s: restoring clock speed", sensorName); + reClockI2C.restoreClock(); +#endif /* HM330X_I2C_CLOCK_SPEED */ + + status = 1; + LOG_INFO("%s Enabled", sensorName); + + initI2CSensor(); + return true; +}; + +uint32_t HM330XSensor::wakeUp() +{ + state = State::ACTIVE; + measureStarted = getTime(); + return HM330X_WARMUP_MS; +} + +int32_t HM330XSensor::wakeUpTimeMs() +{ + return HM330X_WARMUP_MS; +} + +bool HM330XSensor::canSleep() +{ + // Sleep not available in this sensor + return false; +} + +bool HM330XSensor::isActive() +{ + return state == State::ACTIVE; +} + +int32_t HM330XSensor::pendingForReadyMs() +{ + uint32_t now; + now = getTime(); + uint32_t sincePMMeasureStarted = (now - measureStarted) * 1000; + LOG_DEBUG("%s: Since measure started: %ums", sensorName, sincePMMeasureStarted); + + if (sincePMMeasureStarted < HM330X_WARMUP_MS) { + LOG_INFO("%s: not enough time passed since starting measurement", sensorName); + return HM330X_WARMUP_MS - sincePMMeasureStarted; + } + return 0; +} + +bool HM330XSensor::getMetrics(meshtastic_Telemetry *measurement) +{ +#ifdef HM330X_I2C_CLOCK_SPEED + LOG_DEBUG("%s: attempting to reclock speed to %uHz", sensorName, HM330X_I2C_CLOCK_SPEED); + reClockI2C.setClock(HM330X_I2C_CLOCK_SPEED); +#endif /* HM330X_I2C_CLOCK_SPEED */ + + if (hm330x.read_sensor_value(buffer, 29)) { + LOG_WARN("%s: read result failed", sensorName); +#ifdef HM330X_I2C_CLOCK_SPEED + LOG_INFO("%s: restoring clock speed", sensorName); + reClockI2C.restoreClock(); +#endif /* HM330X_I2C_CLOCK_SPEED */ + return false; + } + +#ifdef HM330X_I2C_CLOCK_SPEED + LOG_INFO("%s: restoring clock speed", sensorName); + reClockI2C.restoreClock(); +#endif /* HM330X_I2C_CLOCK_SPEED */ + + if (hm330x.checksum_calc(buffer) != HM330XErrorCode::NO_ERROR) { + LOG_ERROR("%s: Checksum error", sensorName); + return false; + } + + auto read16 = [](const uint8_t *data, uint8_t idx) -> uint16_t { return (data[idx] << 8) | data[idx + 1]; }; + + measurement->variant.air_quality_metrics.has_pm10_standard = true; + measurement->variant.air_quality_metrics.has_pm25_standard = true; + measurement->variant.air_quality_metrics.has_pm100_standard = true; + + measurement->variant.air_quality_metrics.pm10_standard = read16(buffer, 4); + measurement->variant.air_quality_metrics.pm25_standard = read16(buffer, 6); + measurement->variant.air_quality_metrics.pm100_standard = read16(buffer, 8); + + // TODO - Add admin command to remove environmental metrics to save protobuf space + // TODO - Decide if these should be enabled + // measurement->variant.air_quality_metrics.has_pm10_environmental = true; + // measurement->variant.air_quality_metrics.has_pm25_environmental = true; + // measurement->variant.air_quality_metrics.has_pm100_environmental = true; + + // measurement->variant.air_quality_metrics.pm10_environmental = read16(buffer, 10); + // measurement->variant.air_quality_metrics.pm25_environmental = read16(buffer, 12); + // measurement->variant.air_quality_metrics.pm100_environmental = read16(buffer, 14); + + LOG_DEBUG("%s: Got readings: pM1p0_standard=%u, pM2p5_standard=%u, pM10p0_standard=%u", sensorName, + measurement->variant.air_quality_metrics.pm10_standard, measurement->variant.air_quality_metrics.pm25_standard, + measurement->variant.air_quality_metrics.pm100_standard); + + return true; +} +#endif diff --git a/src/modules/Telemetry/Sensor/HM330XSensor.h b/src/modules/Telemetry/Sensor/HM330XSensor.h new file mode 100644 index 000000000..76312b04c --- /dev/null +++ b/src/modules/Telemetry/Sensor/HM330XSensor.h @@ -0,0 +1,45 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include() + +#include "../detect/ReClockI2C.h" +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "RTC.h" +#include "TelemetrySensor.h" +#include + +#define HM330X_I2C_CLOCK_SPEED 400000 +#define HM330X_WARMUP_MS 30000 +#define HM330X_NO_ERROR 0 +#define HM330X_FRAME_LENGTH 30 + +class HM330XSensor : public TelemetrySensor +{ + private: + enum class State { IDLE, ACTIVE }; + State state = State::IDLE; + uint32_t measureStarted = 0; + uint8_t buffer[HM330X_FRAME_LENGTH]{}; + TwoWire *_bus{}; + uint8_t _address{}; +#ifdef HM330X_I2C_CLOCK_SPEED + ScanI2C::I2CPort _port = ScanI2C::I2CPort::NO_I2C; + ReClockI2C reClockI2C; +#endif + + HM330X hm330x; + + public: + HM330XSensor(); + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + + virtual bool isActive() override; + // virtual void sleep() override; // no need for this on this sensor + virtual uint32_t wakeUp() override; + virtual bool canSleep() override; + virtual int32_t wakeUpTimeMs() override; + virtual int32_t pendingForReadyMs() override; +}; + +#endif