diff --git a/platformio.ini b/platformio.ini
index 48cd420e3c8..c6569db5b7e 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/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 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