Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
1 change: 1 addition & 0 deletions src/configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define LTR553ALS_ADDR 0x23
#define SEN5X_ADDR 0x69
#define SCD30_ADDR 0x61
#define HM330X_ADDR 0x40

// -----------------------------------------------------------------------------
// ACCELEROMETER
Expand Down
1 change: 1 addition & 0 deletions src/detect/ScanI2C.h
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ class ScanI2C
SCD30,
ADS1115,
SPA06,
HM330X
} DeviceType;

// typedef uint8_t DeviceAddress;
Expand Down
20 changes: 19 additions & 1 deletion src/detect/ScanI2CTwoWire.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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__); \
Expand Down Expand Up @@ -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);
Expand All @@ -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;
Expand Down
6 changes: 6 additions & 0 deletions src/modules/Telemetry/AirQualityTelemetry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,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)
{
Expand Down Expand Up @@ -66,6 +69,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()
Expand Down
144 changes: 144 additions & 0 deletions src/modules/Telemetry/Sensor/HM330XSensor.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
#include "configuration.h"

#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include(<Seeed_HM330X.h>)

#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;
}
Comment on lines +60 to +64

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect the base lifecycle contract and comparable air-quality sensors.
fd -t f 'TelemetrySensor|Sensor' src/modules/Telemetry |
  xargs rg -n -C3 '\b(canSleep|wakeUp|pendingForReadyMs|isActive)\s*\('

Repository: meshtastic/firmware

Length of output: 20253


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- HM330XSensor.cpp ---'
sed -n '1,220p' src/modules/Telemetry/Sensor/HM330XSensor.cpp

echo
echo '--- TelemetrySensor.h ---'
sed -n '1,140p' src/modules/Telemetry/Sensor/TelemetrySensor.h

echo
echo '--- AirQualityTelemetryModule.cpp ---'
sed -n '1,260p' src/modules/Telemetry/AirQualityTelemetryModule.cpp

Repository: meshtastic/firmware

Length of output: 8147


🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline src/modules/Telemetry/AirQualityTelemetryModule.cpp --view expanded || true
echo
rg -n -C4 'canSleep|wakeUp|pendingForReadyMs|isActive|getMetrics' src/modules/Telemetry/AirQualityTelemetryModule.cpp src/modules/Telemetry/Sensor/*.cpp

Repository: meshtastic/firmware

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd -t f 'AirQualityTelemetryModule.cpp' src/modules/Telemetry
sed -n '1,260p' src/modules/Telemetry/AirQualityTelemetryModule.cpp

Repository: meshtastic/firmware

Length of output: 252


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C4 'pendingForReadyMs\(|wakeUpTimeMs\(|canSleep\(\)|isActive\(\)|sleep\(\)|wakeUp\(\)' src/modules/Telemetry src/modules | sed -n '1,260p'

Repository: meshtastic/firmware

Length of output: 19542


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- HM330XSensor.h ---'
sed -n '1,160p' src/modules/Telemetry/Sensor/HM330XSensor.h

echo
echo '--- AirQualityTelemetry.cpp (top/middle) ---'
sed -n '1,260p' src/modules/Telemetry/AirQualityTelemetry.cpp

echo
echo '--- AirQualityTelemetry.cpp (rest) ---'
sed -n '261,520p' src/modules/Telemetry/AirQualityTelemetry.cpp

Repository: meshtastic/firmware

Length of output: 22122


Keep canSleep() true for HM330X AirQualityTelemetryModule only calls wakeUp() and pendingForReadyMs() when this returns true, so returning false skips the warmup gate and the sensor is read immediately. Leave sleep() as a no-op if needed, but don’t block readiness tracking.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/Telemetry/Sensor/HM330XSensor.cpp` around lines 60 - 64, Update
HM330XSensor::canSleep() to return true so AirQualityTelemetryModule applies
wake-up and pendingForReadyMs readiness tracking before reading the sensor. Keep
sleep() as a no-op if necessary, but do not change the readiness flow.


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
42 changes: 42 additions & 0 deletions src/modules/Telemetry/Sensor/HM330XSensor.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#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{};

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
Loading