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:
co-authored by
Thomas Göttgens
parent
e78b121d9f
commit
b59cd58d75
7 files changed
+226
-9
No files matched your search
@@ -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]
|
||||
|
||||
@@ -303,6 +303,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#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
|
||||
|
||||
@@ -104,7 +104,8 @@ class ScanI2C
|
||||
ISM330DHCX,
|
||||
SPA06,
|
||||
DS248X,
|
||||
} DeviceType;
|
||||
HM330X
|
||||
} DeviceType;
|
||||
|
||||
// typedef uint8_t DeviceAddress;
|
||||
typedef enum I2CPort {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user