mirror of https://github.com/arendst/Tasmota.git
357 lines
13 KiB
C++
357 lines
13 KiB
C++
/*
|
|
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 <http://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
#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;
|
|
}
|
|
|
|
void HM330XInit(void) {
|
|
if (!I2cSetDevice(HM330X_DEFAULT_ADDRESS)) { return; }
|
|
|
|
int8_t set_pin = Pin(GPIO_HM330X_SET);
|
|
if (set_pin >= 0) {
|
|
pinMode(set_pin, OUTPUT);
|
|
digitalWrite(set_pin, 1);
|
|
delay(5);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
HM330Xdata = (HM330XDATA*)calloc(1,sizeof(HM330XDATA));
|
|
if (!HM330Xdata) {
|
|
AddLog(LOG_LEVEL_DEBUG, PSTR("HM3: out of memory"));
|
|
return;
|
|
}
|
|
|
|
HM330Xdata->valid = false;
|
|
HM330Xdata->set_pin = set_pin;
|
|
HM330Xdata->state = HM330X_WARMUP;
|
|
HM330Xdata->warmup_counter = HM330X_WARMUP_DELAY;
|
|
|
|
I2cSetActiveFound(HM330X_DEFAULT_ADDRESS, "HM330X");
|
|
}
|
|
|
|
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} = <tr><th>, {m} = </th><td>, {e} = </td></tr>
|
|
|
|
|
|
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
|