From 088314e31904b03658d1d88335e672f77bb2fc85 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Thu, 16 Jul 2026 19:39:16 +0200 Subject: [PATCH 1/2] Add HM330X PM Sensor --- platformio.ini | 2 + src/configuration.h | 1 + src/detect/ScanI2C.h | 1 + src/detect/ScanI2CTwoWire.cpp | 20 ++- src/modules/Telemetry/AirQualityTelemetry.cpp | 6 + src/modules/Telemetry/Sensor/HM330XSensor.cpp | 144 ++++++++++++++++++ src/modules/Telemetry/Sensor/HM330XSensor.h | 42 +++++ 7 files changed, 215 insertions(+), 1 deletion(-) create mode 100644 src/modules/Telemetry/Sensor/HM330XSensor.cpp create mode 100644 src/modules/Telemetry/Sensor/HM330XSensor.h diff --git a/platformio.ini b/platformio.ini index 48cd420e3c8..489d08db80f 100644 --- a/platformio.ini +++ b/platformio.ini @@ -203,6 +203,8 @@ lib_deps = https://github.com/adafruit/Adafruit_SHTC3/archive/refs/tags/1.0.2.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/bd0f0b38c2320ead43cc35ad1a29874b2c8f54c1.zip ; Common environmental sensor libraries (not included in native / portduino) [environmental_extra_common] diff --git a/src/configuration.h b/src/configuration.h index 87fc3efef6a..ba072a90ab2 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -275,6 +275,7 @@ along with this program. If not, see . #define LTR553ALS_ADDR 0x23 #define SEN5X_ADDR 0x69 #define SCD30_ADDR 0x61 +#define HM330X_ADDR 0x40 // ----------------------------------------------------------------------------- // ACCELEROMETER diff --git a/src/detect/ScanI2C.h b/src/detect/ScanI2C.h index f0d60cb5f2b..346b0f0938d 100644 --- a/src/detect/ScanI2C.h +++ b/src/detect/ScanI2C.h @@ -100,6 +100,7 @@ class ScanI2C SCD30, ADS1115, SPA06, + HM330X } DeviceType; // typedef uint8_t DeviceAddress; diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index e515b46b08f..6fdde019c8a 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -232,6 +232,21 @@ bool detectSHT21SerialNumber(TwoWire *i2cBus, uint8_t address) } #endif +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && HAS_TELEMETRY +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 + #define SCAN_SIMPLE_CASE(ADDR, T, ...) \ case ADDR: \ logFoundDevice(__VA_ARGS__); \ @@ -429,7 +444,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) break; #endif #if !defined(M5STACK_UNITC6L) - case INA_ADDR: + case INA_ADDR: //same as HMM330X_ADDR case INA_ADDR_ALTERNATE: case INA_ADDR_WAVESHARE_UPS: registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFE), 2); @@ -445,6 +460,9 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) logFoundDevice("INA260", (uint8_t)addr.address); type = INA260; } + } else if (probeHM330x(i2cBus, addr.address)){ + logFoundDevice("HM330X", (uint8_t)addr.address); + type = HM330X; } else { // Assume INA219 if INA260 ID is not found logFoundDevice("INA219", (uint8_t)addr.address); type = INA219; diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index ca853d0510e..43e012dbd79 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -35,6 +35,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) { @@ -66,6 +69,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 00000000000..1c28fb7d36e --- /dev/null +++ b/src/modules/Telemetry/Sensor/HM330XSensor.cpp @@ -0,0 +1,144 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include() + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "HM330XSensor.h" +#include "../detect/reClockI2C.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 +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(HM330X_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(HM330X_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return false; +#endif /* CAN_RECLOCK_I2C */ +#endif /* HM330X_I2C_CLOCK_SPEED */ + + if (hm330x.init(_bus) != HM330XErrorCode::NO_ERROR) { + #if defined(HM330X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); + #endif + LOG_WARN("%s error in sensor init", sensorName); + return false; + } + +#if defined(HM330X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + + 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 +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(HM330X_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(HM330X_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return false; +#endif /* CAN_RECLOCK_I2C */ +#endif /* HM330X_I2C_CLOCK_SPEED */ + + if (hm330x.read_sensor_value(buffer, 29)) { + LOG_WARN("%s: read result failed", sensorName); +#if defined(HM330X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + return false; + } + +#if defined(HM330X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + + 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]; }; + + // TODO - This is suspiciously not in terms of PM1, PM2.5 and PM10.0 relationship + // Normally, PM1.0 > PM2.5 > PM10, but in this case it's not. Are the buckets cumulative? + // It's not documented in the datasheet, so safely assuming that it's correct as in the datasheet + 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 00000000000..9d6b242c85d --- /dev/null +++ b/src/modules/Telemetry/Sensor/HM330XSensor.h @@ -0,0 +1,42 @@ +#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{}; + + HM330X hm330x; + HM330XErrorCode checksum(uint8_t* data); + + 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 From 199b215a085c92f0204d3486592588fcf85e2c82 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Fri, 17 Jul 2026 00:47:18 +0200 Subject: [PATCH 2/2] Update HM330X library --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 489d08db80f..c6569db5b7e 100644 --- a/platformio.ini +++ b/platformio.ini @@ -204,7 +204,7 @@ lib_deps = # 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/bd0f0b38c2320ead43cc35ad1a29874b2c8f54c1.zip + https://github.com/meshtastic/Seeed_PM2_5_sensor_HM3301/archive/2704ca254c7e2136c52ac23198dd05f5ba1e2f04.zip ; Common environmental sensor libraries (not included in native / portduino) [environmental_extra_common]