/*
xsns_92_hm330x.ino - Driver for Seedstudio Grove HM3301 particle sensor
Copyright (C) 2021 Barbudor, SeedStudio
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
#ifdef USE_I2C
#ifdef USE_HM330X
/**************************************************************************************************
* Driver for Particule sensor SeedStudio Grove HM3301
* ================================================================================================
* This driver use the I2C mode. Only address 0x40 is supported by the sensor so it is hardcoded in
* the driver without any possibility to change it (without editing and recompiling)
* By default the sensor remains active all the time but no data are provided for the first
* HM330X_WARMUP_DELAY seconds (to allow the sensor to warm up). During warmup, no data are
* returned in the GUI or through SENSOR message in order to provide invalid data. In case
* of any doubt, logs with level 3 will confirm right at boot if the sensor has been detected.
*
* Low power / life extension
* --------------------------
* It is possible to connect a GPIO to pin SET of the module and assign it to "HM330X SET" for
* automatic sleep in order to extend the life time of the sensor. When this is used, activity of
* the sensor in synced with Teleperiod as follow
* 1) At start, sensor is immediately enabled by setting "HM330X SET" HI and start warmup
* If any Telemetry occurs before the end of the warmup, no data are provided and warmup
* goes on.
* 2) After HM330X_WARMUP_DELAY, data are available and reported in web and SENSOR
* 3) Immediately after the next teleperiod SENSOR message, if Teleperiod is greater than
* (HM330X_WARMUP_DELAY + 2) the sensor is put to sleep by taking the pin "HM330X SET" LOW
* and a new cycle start
* New cycle
* 4) (HM330X_WARMUP_DELAY + 2) seconds before the next teleperiod, the sensor is awaken by taking
* the pin "HM330X SET" HI for warmup. No data are available yet
* 5) 2 seconds before Telemetry, warmup is completed and data are available to Web until
* Telemetry messages are sent
* 6) Back to 2)
*
* Overridables (in user_config_override.h)
* ----------------------------------------
* HM330X_DEFAULT_ADDRESS 0x40 in case SeedStudio makes this changeable in the future
* HM330X_WARMUP_DELAY 30 time for warmup, data are not pulled before that delay
* HM330X_HIDE_OUT_OF_DATE false if changed to true web data and sensor will not be
* published when outdated
*************************************************************************************************/
//#define HM330X_SIM
#define XSNS_93 93
#define XI2C_63 63 // See I2CDEVICES.md
#ifndef HM330X_DEFAULT_ADDRESS
#define HM330X_DEFAULT_ADDRESS 0x40
#endif
#define HM330X_SELECT_COMM_CMD 0X88
#define HM330X_FRAME_SIZE 29
#ifndef HM330X_WARMUP_DELAY
#define HM330X_WARMUP_DELAY 30 // Ignore HM330X data for XX seconds after start
#endif
#define HM330X_PRE_TELEPERIOD_ENABLE 3 // Sensor made active before teleperiod
#ifndef HM330X_HIDE_OUT_OF_DATE
#define HM330X_HIDE_OUT_OF_DATE false // override to true to disable display when the data are out of date
#endif
enum {
HM330X_NO_ERROR = 0,
HM330X_ERROR_PARAM = -1,
HM330X_ERROR_NOT_FOUND = -2,
HM330X_ERROR_COMM = -3,
HM330X_ERROR_TIMEOUT = -4,
HM330X_ERROR_CHECKSUM = -5,
HM330X_ERROR_MEMALLOC = -6,
HM330X_ERROR_OTHERS = -128
};
struct HM330XFrame {
uint16_t reserved;
uint16_t sensor_num;
uint16_t pm1_0_standard, pm2_5_standard, pm10_0_standard;
uint16_t pm1_0_env, pm2_5_env, pm10_0_env;
uint16_t particles_0_3um, particles_0_5um, particles_1_0um, particles_2_5um, particles_5_0um, particles_10_0um;
uint8_t checksum;
};
enum HM330X_State {
HM330X_SLEEP = 0, // sensor is sleeping
HM330X_WARMUP = 1, // sensor is in warmup
HM330X_ACTIVE = 2 // sensor is active and can be polled for data
};
struct HM330XDATA {
uint8_t warmup_counter; // count for warmup
bool valid; // valid data have been retrieved from the sensor
uint8_t state; // state machine
int8_t set_pin; // pin to set the device asleep or active
struct HM330XFrame rx_buffer;
};
struct HM330XDATA *HM330Xdata = nullptr;
int HM330XUpdate() {
#ifdef HM330X_SIM
HM330Xdata->rx_buffer.pm1_0_standard = 10;
HM330Xdata->rx_buffer.pm2_5_standard = 25;
HM330Xdata->rx_buffer.pm10_0_standard = 100;
HM330Xdata->rx_buffer.pm1_0_env = 1010;
HM330Xdata->rx_buffer.pm2_5_env = 1025;
HM330Xdata->rx_buffer.pm10_0_env = 1100;
HM330Xdata->rx_buffer.particles_0_3um = 2003;
HM330Xdata->rx_buffer.particles_0_5um = 2005;
HM330Xdata->rx_buffer.particles_1_0um = 2010;
HM330Xdata->rx_buffer.particles_2_5um = 2025;
HM330Xdata->rx_buffer.particles_5_0um = 2050;
HM330Xdata->rx_buffer.particles_10_0um = 2100;
#else
uint32_t time_out_count = 0;
#if HM330X_HIDE_OUT_OF_DATE
HM330Xdata->valid = false;
#endif
Wire.requestFrom((uint8_t)HM330X_DEFAULT_ADDRESS, (uint8_t)HM330X_FRAME_SIZE);
while (HM330X_FRAME_SIZE != Wire.available()) {
time_out_count++;
if (time_out_count > 10) {
return HM330X_ERROR_TIMEOUT;
}
delay(1);
}
uint8_t checksum = 0;
uint8_t *data = (uint8_t*)&(HM330Xdata->rx_buffer);
int i = 0;
for (; i < HM330X_FRAME_SIZE-1; i++) {
checksum += data[i] = Wire.read();
}
data[i] = Wire.read();
AddLogBuffer(LOG_LEVEL_DEBUG_MORE, data, HM330X_FRAME_SIZE);
if (checksum != data[i]) {
AddLog(LOG_LEVEL_DEBUG, PSTR("HM3: checksum error"));
return HM330X_ERROR_CHECKSUM;
}
for (i = 0; i < (HM330X_FRAME_SIZE/2); i++) {
uint8_t tmp = data[2*i]; // revert endianness
data[2*i] = data[2*i+1],
data[2*i+1] = tmp;
}
#endif
HM330Xdata->valid = true;
return HM330X_NO_ERROR;
}
int HM330XSendI2CCommand(uint8_t cmd) {
int ret = HM330X_NO_ERROR;
#ifdef HM330X_SIM
#else
Wire.beginTransmission(HM330X_DEFAULT_ADDRESS);
Wire.write(cmd);
ret = Wire.endTransmission();
if (ret != 0) {
ret = HM330X_ERROR_NOT_FOUND;
}
#endif
return ret;
}
int HM330XInit() {
int8_t set_pin = Pin(GPIO_HM330X_SET);
if (set_pin >= 0) {
pinMode(set_pin, OUTPUT);
digitalWrite(set_pin, 1);
delay(5);
}
if (I2cActive(HM330X_DEFAULT_ADDRESS)) {
AddLog(LOG_LEVEL_DEBUG, PSTR("HM3: Address 0x%02X already in use"), HM330X_DEFAULT_ADDRESS);
return HM330X_ERROR_PARAM;
}
if (HM330X_NO_ERROR != HM330XSendI2CCommand(HM330X_SELECT_COMM_CMD)) {
AddLog(LOG_LEVEL_DEBUG, PSTR("HM3: no response from address 0x%02X"), HM330X_DEFAULT_ADDRESS);
return HM330X_ERROR_COMM;
}
HM330Xdata = (HM330XDATA*)calloc(1,sizeof(HM330XDATA));
if (!HM330Xdata) {
AddLog(LOG_LEVEL_DEBUG, PSTR("HM3: out of memory"));
return HM330X_ERROR_MEMALLOC;
}
HM330Xdata->valid = false;
HM330Xdata->set_pin = set_pin;
HM330Xdata->state = HM330X_WARMUP;
HM330Xdata->warmup_counter = HM330X_WARMUP_DELAY;
I2cSetActiveFound(HM330X_DEFAULT_ADDRESS, "HM330X");
return HM330X_NO_ERROR;
}
void HM330XEverySecond() {
uint16_t time_until_tp = Settings->tele_period - TasmotaGlobal.tele_period;
// if (HM330Xdata)
// AddLog(LOG_LEVEL_DEBUG_MORE,PSTR("HM3: EverySec wu:%d, v:%d, s:%d, sp:%d"),
// HM330Xdata->warmup_counter,
// HM330Xdata->valid,
// HM330Xdata->state,
// HM330Xdata->set_pin
// );
// else
// AddLog(LOG_LEVEL_DEBUG_MORE,PSTR("HM3: nullptr"));
if (HM330X_SLEEP == HM330Xdata->state) {
if (time_until_tp < (HM330X_WARMUP_DELAY+HM330X_PRE_TELEPERIOD_ENABLE)) {
//AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("HM3: Exit sleep mode"));
if (HM330Xdata->set_pin >= 0) {
digitalWrite(HM330Xdata->set_pin, 1);
}
HM330Xdata->state = HM330X_WARMUP;
HM330Xdata->warmup_counter = HM330X_WARMUP_DELAY;
#if HM330X_HIDE_OUT_OF_DATE
HM330Xdata->valid = false;
#endif
}
}
if (HM330X_WARMUP == HM330Xdata->state) {
HM330Xdata->warmup_counter--;
if (0 == HM330Xdata->warmup_counter) {
//AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("HM3: Warmup done"));
if (HM330X_NO_ERROR != HM330XSendI2CCommand(HM330X_SELECT_COMM_CMD)) {
AddLog(LOG_LEVEL_DEBUG, PSTR("HM3: no response from address 0x%02X"), HM330X_DEFAULT_ADDRESS);
}
HM330Xdata->state = HM330X_ACTIVE;
}
}
if (HM330X_ACTIVE == HM330Xdata->state) {
//AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("HM3: Update"));
HM330XUpdate();
}
}
void HM330XEnterSleep()
{
if (HM330Xdata->set_pin >= 0 && HM330X_ACTIVE == HM330Xdata->state && Settings->tele_period > (HM330X_WARMUP_DELAY+HM330X_PRE_TELEPERIOD_ENABLE)) {
//AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("HM3: Enter sleep mode"));
digitalWrite(HM330Xdata->set_pin, 0);
HM330Xdata->state = HM330X_SLEEP;
#if HM330X_HIDE_OUT_OF_DATE
HM330Xdata->valid = false;
#endif
}
}
/*********************************************************************************************\
* SNS Interface
\*********************************************************************************************/
const char JSON_HM330X_SNS[] PROGMEM = ",\"HM330X\":{"
"\"CF1\":%d,\"CF2.5\":%d,\"CF10\":%d,"
"\"PM1\":%d,\"PM2.5\":%d,\"PM10\":%d,"
"\"PB0.3\":%d,\"PB0.5\":%d,\"PB1\":%d,\"PB2.5\":%d,\"PB5\":%d,\"PB10\":%d}";
const char HTTP_HM330X_SNS[] PROGMEM =
// "{s}HM330X " D_STANDARD_CONCENTRATION " 1 " D_UNIT_MICROMETER "{m}%d " D_UNIT_MICROGRAM_PER_CUBIC_METER "{e}"
// "{s}HM330X " D_STANDARD_CONCENTRATION " 2.5 " D_UNIT_MICROMETER "{m}%d " D_UNIT_MICROGRAM_PER_CUBIC_METER "{e}"
// "{s}HM330X " D_STANDARD_CONCENTRATION " 10 " D_UNIT_MICROMETER "{m}%d " D_UNIT_MICROGRAM_PER_CUBIC_METER "{e}"
"{s}HM330X " D_ENVIRONMENTAL_CONCENTRATION " 1 " D_UNIT_MICROMETER "{m}%d " D_UNIT_MICROGRAM_PER_CUBIC_METER "{e}"
"{s}HM330X " D_ENVIRONMENTAL_CONCENTRATION " 2.5 " D_UNIT_MICROMETER "{m}%d " D_UNIT_MICROGRAM_PER_CUBIC_METER "{e}"
"{s}HM330X " D_ENVIRONMENTAL_CONCENTRATION " 10 " D_UNIT_MICROMETER "{m}%d " D_UNIT_MICROGRAM_PER_CUBIC_METER "{e}"
"{s}HM330X " D_PARTICALS_BEYOND " 0.3 " D_UNIT_MICROMETER "{m}%d " D_UNIT_PARTS_PER_DECILITER "{e}"
"{s}HM330X " D_PARTICALS_BEYOND " 0.5 " D_UNIT_MICROMETER "{m}%d " D_UNIT_PARTS_PER_DECILITER "{e}"
"{s}HM330X " D_PARTICALS_BEYOND " 1 " D_UNIT_MICROMETER "{m}%d " D_UNIT_PARTS_PER_DECILITER "{e}"
"{s}HM330X " D_PARTICALS_BEYOND " 2.5 " D_UNIT_MICROMETER "{m}%d " D_UNIT_PARTS_PER_DECILITER "{e}"
"{s}HM330X " D_PARTICALS_BEYOND " 5 " D_UNIT_MICROMETER "{m}%d " D_UNIT_PARTS_PER_DECILITER "{e}"
"{s}HM330X " D_PARTICALS_BEYOND " 10 " D_UNIT_MICROMETER "{m}%d " D_UNIT_PARTS_PER_DECILITER "{e}"; // {s} =
, {m} = | , {e} = |
void HM330XShow(bool json)
{
if (HM330Xdata->valid)
{
if (json) {
ResponseAppend_P(JSON_HM330X_SNS,
HM330Xdata->rx_buffer.pm1_0_standard, HM330Xdata->rx_buffer.pm2_5_standard, HM330Xdata->rx_buffer.pm10_0_standard,
HM330Xdata->rx_buffer.pm1_0_env, HM330Xdata->rx_buffer.pm2_5_env, HM330Xdata->rx_buffer.pm10_0_env,
HM330Xdata->rx_buffer.particles_0_3um, HM330Xdata->rx_buffer.particles_0_5um, HM330Xdata->rx_buffer.particles_1_0um,
HM330Xdata->rx_buffer.particles_2_5um, HM330Xdata->rx_buffer.particles_5_0um, HM330Xdata->rx_buffer.particles_10_0um);
#ifdef USE_DOMOTICZ
if (0 == TasmotaGlobal.tele_period) {
DomoticzSensor(DZ_COUNT, HM330Xdata->rx_buffer.pm1_0_env); // PM1
DomoticzSensor(DZ_VOLTAGE, HM330Xdata->rx_buffer.pm2_5_env); // PM2.5
DomoticzSensor(DZ_CURRENT, HM330Xdata->rx_buffer.pm10_0_env); // PM10
}
#endif // USE_DOMOTICZ
#ifdef USE_WEBSERVER
} else {
WSContentSend_PD(HTTP_HM330X_SNS,
// HM330Xdata->rx_buffer.pm1_0_standard, HM330Xdata->rx_buffer.pm2_5_standard, HM330Xdata->rx_buffer.pm10_0_standard,
HM330Xdata->rx_buffer.pm1_0_env, HM330Xdata->rx_buffer.pm2_5_env, HM330Xdata->rx_buffer.pm10_0_env,
HM330Xdata->rx_buffer.particles_0_3um, HM330Xdata->rx_buffer.particles_0_5um, HM330Xdata->rx_buffer.particles_1_0um,
HM330Xdata->rx_buffer.particles_2_5um, HM330Xdata->rx_buffer.particles_5_0um, HM330Xdata->rx_buffer.particles_10_0um);
#endif // USE_WEBSERVER
}
}
}
/*********************************************************************************************\
* Interface
\*********************************************************************************************/
bool Xsns93(byte function)
{
if (!I2cEnabled(XI2C_63)) { return false; }
bool result = false;
if (FUNC_INIT == function) {
HM330XInit();
}
else if (HM330Xdata) {
switch (function) {
case FUNC_EVERY_SECOND:
HM330XEverySecond();
break;
case FUNC_AFTER_TELEPERIOD:
HM330XEnterSleep();
break;
case FUNC_JSON_APPEND:
HM330XShow(1);
break;
#ifdef USE_WEBSERVER
case FUNC_WEB_SENSOR:
HM330XShow(0);
break;
#endif // USE_WEBSERVER
}
}
return result;
}
#endif // USE_HM330X
#endif // USE_I2C