Add HM330x PM Sensor (#11069)

* Add HM330X PM Sensor

* Update HM330X library

* Bring reclock I2C to HM330x sensor

* Fix probeHM330x for variants without AQ telemetry or telemetry in general

* Remove old import

* Bring back SHT2X from develop

* Remove test comment and unused method in HM330X class

* Reorder detection method. Add pending TODO for INA219 detection

* Rework detection order and add INA219 register check

---------

Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com>
This commit is contained in:
oscgonfer
2026-08-05 08:35:59 +00:00
committed by GitHub
co-authored by Thomas Göttgens
parent e78b121d9f
commit b59cd58d75
7 changed files with 226 additions and 9 deletions
@@ -36,6 +36,9 @@ static constexpr uint16_t TX_HISTORY_KEY_AIR_QUALITY_TELEMETRY = 0x8004;
#if __has_include(<SensirionI2cScd30.h>)
#include "Sensor/SCD30Sensor.h"
#endif
#if __has_include(<Seeed_HM330X.h>)
#include "Sensor/HM330XSensor.h"
#endif
void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner)
{
@@ -114,6 +117,9 @@ void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner)
#if __has_include(<SensirionI2cScd30.h>)
addSensor<SCD30Sensor>(i2cScanner, ScanI2C::DeviceType::SCD30);
#endif
#if __has_include(<Seeed_HM330X.h>)
addSensor<HM330XSensor>(i2cScanner, ScanI2C::DeviceType::HM330X);
#endif
}
int32_t AirQualityTelemetryModule::runOnce()
@@ -0,0 +1,135 @@
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include(<Seeed_HM330X.h>)
#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
@@ -0,0 +1,45 @@
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include(<Seeed_HM330X.h>)
#include "../detect/ReClockI2C.h"
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "RTC.h"
#include "TelemetrySensor.h"
#include <Seeed_HM330X.h>
#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