HomieRoomSensor/src/main.cpp

515 lines
17 KiB
C++

/**
* @file main.cpp
* @author Ollo
* @brief
* @version 0.1
* @date 2021-11-05
*
* @copyright Copyright (c) 2021
*
*/
/******************************************************************************
* INCLUDES
******************************************************************************/
#include <Homie.h>
#include <SoftwareSerial.h>
#include "HomieSettings.h"
#include <Adafruit_NeoPixel.h>
#include <Wire.h>
#include <Adafruit_Sensor.h>
#ifdef BME680
#include "Adafruit_BME680.h"
#else
#ifdef BMP280
#include "Adafruit_BMP280.h"
#else
#error "Decition, which BMx??? is used missing"
#endif
#endif
/******************************************************************************
* DEFINES
******************************************************************************/
#define GPIO_WS2812 D4 /**< GPIO2 */
#define SENSOR_PM1006_RX D2 /**< GPIO4 */
#define SENSOR_PM1006_TX -1 /**< Unused */
#define WITTY_RGB_R D8 /**< GPIO15 */
#define WITTY_RGB_G D6 /**< GPIO12 Used as 3.3V Power supply for the I2C Sensor */
#define WITTY_RGB_B D7 /**< GPIO13 */
#define PM1006_BIT_RATE 9600
#define PM1006_MQTT_UPDATE 5000 /**< Check the sensor every 10 seconds; New measurement is done every 20seconds by the sensor */
#define PIXEL_COUNT 3
#define GPIO_BUTTON SENSOR_PM1006_RX /**< Button and software serial share one pin on Witty board */
#define SENSOR_I2C_SCK D1 /**< GPIO14 - I2C clock pin */
#define SENSOR_I2C_SDI D5 /**< GPIO5 - I2C data pin */
#define PM1006_BIT_RATE 9600
#define PM1006_MQTT_UPDATE 5000 /**< Check the sensor every 10 seconds; New measurement is done every 20seconds by the sensor */
#define SEALEVELPRESSURE_HPA (1013.25)
#define BUTTON_MAX_CYCLE 10000U /**< Action: Reset configuration */
#define BUTTON_MIN_ACTION_CYCLE 55U /**< Minimum cycle to react on the button (e.g. 5 second) */
#define BUTTON_CHECK_INTERVALL 100U /**< Check every 100 ms the button state */
#define LOG_TOPIC "log\0"
#define MQTT_LEVEL_ERROR 1
#define MQTT_LEVEL_WARNING 10
#define MQTT_LEVEL_INFO 20
#define MQTT_LEVEL_DEBUG 90
#define MQTT_LOG_PM1006 10
#define MQTT_LOG_I2CINIT 100
#define MQTT_LOG_I2READ 101
#define MQTT_LOG_RGB 200
#define TEMPBORDER 20
#define getTopic(test, topic) \
char *topic = new char[strlen(Homie.getConfiguration().mqtt.baseTopic) + strlen(Homie.getConfiguration().deviceId) + 1 + strlen(test) + 1]; \
strcpy(topic, Homie.getConfiguration().mqtt.baseTopic); \
strcat(topic, Homie.getConfiguration().deviceId); \
strcat(topic, "/"); \
strcat(topic, test);
#define NUMBER_TYPE "Number"
#define NODE_PARTICLE "particle"
#define NODE_TEMPERATUR "temp"
#define NODE_PRESSURE "pressure"
#define NODE_ALTITUDE "altitude"
#define NODE_GAS "gas"
#define NODE_HUMIDITY "humidity"
#define NODE_AMBIENT "ambient"
#define NODE_BUTTON "button"
/******************************************************************************
* TYPE DEFS
******************************************************************************/
/******************************************************************************
* FUNCTION PROTOTYPES
******************************************************************************/
void log(int level, String message, int code);
/******************************************************************************
* LOCAL VARIABLES
******************************************************************************/
bool mConfigured = false;
bool mConnected = false;
bool mFailedI2Cinitialization = false;
long mLastButtonAction = 0;
/******************************* Sensor data **************************/
HomieNode particle(NODE_PARTICLE, "particle", "number"); /**< Measuret in micro gram per quibik meter air volume */
HomieNode temperaturNode(NODE_TEMPERATUR, "Room Temperature", "number");
HomieNode pressureNode(NODE_PRESSURE, "Pressure", "number");
HomieNode altitudeNode(NODE_ALTITUDE, "Altitude", "number");
#ifdef BME680
HomieNode gasNode(NODE_GAS, "Gas", "number");
HomieNode humidityNode(NODE_HUMIDITY, "Humidity", "number");
#endif
HomieNode buttonNode(NODE_BUTTON, "Button", "number");
/****************************** Output control ***********************/
HomieNode ledStripNode /* to rule them all */("led", "RGB led", "color");
/************************** Settings ******************************/
HomieSetting<bool> i2cEnable("i2c",
#ifdef BME680
"BME680 sensor present"
#else
#ifdef BMP280
"BMP280 sensor present"
#else
"No I2C sensor specified in the project"
#endif
#endif
);
HomieSetting<bool> rgbTemp("rgbTemp", "Show temperature via red (>20 °C) and blue (< 20°C)");
static SoftwareSerial pmSerial(SENSOR_PM1006_RX, SENSOR_PM1006_TX);
#ifdef BME680
Adafruit_BME680 bmx(&Wire); // connected via I2C
#else
#ifdef BMP280
Adafruit_BMP280 bmx; // connected via I2C
#endif
#endif
Adafruit_NeoPixel strip(PIXEL_COUNT, GPIO_WS2812, NEO_GRB + NEO_KHZ800);
// Variablen
uint8_t serialRxBuf[80];
uint8_t rxBufIdx = 0;
int spm25 = 0;
int last = 0;
unsigned int mButtonPressed = 0;
bool mSomethingReceived = false;
/******************************************************************************
* LOCAL FUNCTIONS
*****************************************************************************/
/**
* @brief Get the Sensor Data from software serial
*
* @return int PM25 value
*/
int getSensorData() {
uint8_t rxBufIdx = 0;
uint8_t checksum = 0;
// Sensor Serial aushorchen
while ((pmSerial.available() && rxBufIdx < 127) || rxBufIdx < 20)
{
serialRxBuf[rxBufIdx++] = pmSerial.read();
delay(15);
}
// calculate checksum
for (uint8_t i = 0; i < 20; i++)
{
checksum += serialRxBuf[i];
}
/* Debug Print of received bytes */
String dbgBuffer = String("PM1006: ");
for (uint8_t i = 0; i < 20; i++)
{
dbgBuffer += String(serialRxBuf[i], 16);
}
dbgBuffer += String("check: " + String(checksum, 16));
log(MQTT_LEVEL_DEBUG, String(dbgBuffer), MQTT_LOG_PM1006);
// Header und Prüfsumme checken
if (serialRxBuf[0] == 0x16 && serialRxBuf[1] == 0x11 && serialRxBuf[2] == 0x0B /* && checksum == 0 */)
{
return (serialRxBuf[5] << 8 | serialRxBuf[6]);
}
else
{
return (-1);
}
}
/**
* @brief Handle events of the Homie platform
* @param event
*/
void onHomieEvent(const HomieEvent &event)
{
switch (event.type)
{
case HomieEventType::MQTT_READY:
mConnected=true;
digitalWrite(WITTY_RGB_R, LOW);
if (!i2cEnable.get()) { /** keep green LED activated to power I2C sensor */
digitalWrite(WITTY_RGB_G, LOW);
log(MQTT_LEVEL_INFO, F("I2C powersupply deactivated"), MQTT_LOG_I2CINIT);
}
digitalWrite(WITTY_RGB_B, LOW);
strip.fill(strip.Color(0,0,128));
strip.show();
if (mFailedI2Cinitialization) {
log(MQTT_LEVEL_DEBUG, F("Could not find a valid BME680 sensor, check wiring or "
"try a different address!"), MQTT_LOG_I2CINIT);
} else {
log(MQTT_LEVEL_INFO, F("BME680 sensor found"), MQTT_LOG_I2CINIT);
}
break;
case HomieEventType::READY_TO_SLEEP:
break;
case HomieEventType::OTA_STARTED:
break;
case HomieEventType::OTA_SUCCESSFUL:
ESP.restart();
break;
case HomieEventType::WIFI_CONNECTED:
digitalWrite(WITTY_RGB_B, HIGH);
break;
default:
break;
}
}
void bmpPublishValues() {
#ifdef BME680
// Tell BME680 to begin measurement.
unsigned long endTime = bmx.beginReading();
if (endTime == 0) {
log(MQTT_LEVEL_ERROR, F("BME680 not accessible"), MQTT_LOG_I2READ);
return;
}
#endif
// Publish the values
temperaturNode.setProperty(NODE_TEMPERATUR).send(String(bmx.readTemperature()));
pressureNode.setProperty(NODE_PRESSURE).send(String(bmx.readPressure() / 100.0F));
altitudeNode.setProperty(NODE_ALTITUDE).send(String(bmx.readAltitude(SEALEVELPRESSURE_HPA)));
#ifdef BME680
gasNode.setProperty(NODE_GAS).send(String((bmx.gas_resistance / 1000.0)));
humidityNode.setProperty(NODE_HUMIDITY).send(String(bmx.humidity));
#endif
log(MQTT_LEVEL_DEBUG, String("Temp" + String(bmx.readTemperature()) + "\tPressure:" +
String(bmx.readPressure() / 100.0F) + "\t Altitude:"+
String(bmx.readAltitude(SEALEVELPRESSURE_HPA))), MQTT_LOG_I2READ);
if ( (rgbTemp.get()) && (!mSomethingReceived) ) {
if (bmx.readTemperature() < TEMPBORDER) {
strip.setPixelColor(0, strip.Color(0,0,255));
} else {
strip.setPixelColor(0, strip.Color(255,0,0));
}
strip.show();
}
}
/**
* @brief Main loop, triggered by the Homie API
* All logic needs to be done here.
*
* The ranges are defined as followed:
* - green: 0-35 (good+low)
* - orange: 36-85 (OK+medicore)
* - red: 86-... (bad+high)
*
* source @link{https://github.com/Hypfer/esp8266-vindriktning-particle-sensor/issues/16#issuecomment-903116056}
*/
void loopHandler()
{
static long lastRead = 0;
if ((millis() - lastRead) > PM1006_MQTT_UPDATE) {
int pM25 = getSensorData();
if (pM25 >= 0) {
particle.setProperty(NODE_PARTICLE).send(String(pM25));
if (!mSomethingReceived) {
if (pM25 < 35) {
strip.fill(strip.Color(0, 255, 0)); /* green */
} else if (pM25 < 85) {
strip.fill(strip.Color(255, 127, 0)); /* orange */
} else {
strip.fill(strip.Color(255, 0, 0)); /* red */
}
strip.show();
}
}
/* Read BOSCH sensor */
if (i2cEnable.get() && (!mFailedI2Cinitialization)) {
bmpPublishValues();
}
/* Clean cycles buttons */
if (mButtonPressed <= BUTTON_MIN_ACTION_CYCLE) {
buttonNode.setProperty(NODE_BUTTON).send("0");
}
lastRead = millis();
}
/* if the user sees something via the LEDs, inform MQTT, too */
if (mButtonPressed > BUTTON_MIN_ACTION_CYCLE) {
buttonNode.setProperty(NODE_BUTTON).send(String(mButtonPressed));
}
// Feed the dog -> ESP stay alive
ESP.wdtFeed();
}
bool ledHandler(const HomieRange& range, const String& value) {
if (range.isRange) return false; // only one switch is present
Homie.getLogger() << "Received: " << (value) << endl;
if (value.equals("250,250,250")) {
mSomethingReceived = false; // enable animation again
ledStripNode.setProperty(NODE_AMBIENT).send(value);
return true;
} else {
mSomethingReceived = true; // Stop animation
int sep1 = value.indexOf(',');
int sep2 = value.indexOf(',', sep1 + 1);
if ((sep1 > 0) && (sep2 > 0)) {
int red = value.substring(0,sep1).toInt();
int green = value.substring(sep1 + 1, sep2).toInt();
int blue = value.substring(sep2 + 1, value.length()).toInt();
uint8_t r = (red * 255) / 250;
uint8_t g = (green *255) / 250;
uint8_t b = (blue *255) / 250;
uint32_t c = strip.Color(r,g,b);
strip.fill(c);
strip.show();
ledStripNode.setProperty(NODE_AMBIENT).send(value);
return true;
}
}
return false;
}
/******************************************************************************
* GLOBAL FUNCTIONS
*****************************************************************************/
void setup()
{
SPIFFS.begin();
Serial.begin(115200);
Serial.setTimeout(2000);
pinMode(WITTY_RGB_R, OUTPUT);
pinMode(WITTY_RGB_G, OUTPUT);
pinMode(WITTY_RGB_B, OUTPUT);
digitalWrite(WITTY_RGB_R, LOW);
digitalWrite(WITTY_RGB_G, LOW);
digitalWrite(WITTY_RGB_B, LOW);
Homie_setFirmware(HOMIE_FIRMWARE_NAME, HOMIE_FIRMWARE_VERSION);
Homie.setLoopFunction(loopHandler);
Homie.onEvent(onHomieEvent);
i2cEnable.setDefaultValue(false);
rgbTemp.setDefaultValue(false);
memset(serialRxBuf, 0, 80);
pmSerial.begin(PM1006_BIT_RATE);
Homie.setup();
particle.advertise(NODE_PARTICLE).setName("Particle").setDatatype(NUMBER_TYPE).setUnit("micro gram per quibik");
temperaturNode.advertise(NODE_TEMPERATUR).setName("Degrees")
.setDatatype("float")
.setUnit("ºC");
pressureNode.advertise(NODE_PRESSURE).setName("Pressure")
.setDatatype("float")
.setUnit("hPa");
altitudeNode.advertise(NODE_ALTITUDE).setName("Altitude")
.setDatatype("float")
.setUnit("m");
#ifdef BME680
gasNode.advertise(NODE_GAS).setName("Gas")
.setDatatype("float")
.setUnit(" KOhms");
humidityNode.advertise(NODE_HUMIDITY).setName("Humidity")
.setDatatype("float")
.setUnit("%");
#endif
ledStripNode.advertise(NODE_AMBIENT).setName("All Leds")
.setDatatype("color").setFormat("rgb")
.settable(ledHandler);
buttonNode.advertise(NODE_BUTTON).setName("Button pressed")
.setDatatype("integer");
strip.begin();
/* activate I2C for BOSCH sensor */
Wire.begin(SENSOR_I2C_SDI, SENSOR_I2C_SCK);
mConfigured = Homie.isConfigured();
digitalWrite(WITTY_RGB_G, HIGH);
if (mConfigured)
{
if (i2cEnable.get()) {
#ifdef BME680
printf("Wait 1 second...\r\n");
delay(1000);
#endif
/* Extracted from library's example */
mFailedI2Cinitialization = !bmx.begin();
if (!mFailedI2Cinitialization) {
strip.fill(strip.Color(0,64,0));
strip.show();
#ifdef BME680
bmx.setTemperatureOversampling(BME680_OS_8X);
bmx.setHumidityOversampling(BME680_OS_2X);
bmx.setPressureOversampling(BME680_OS_4X);
bmx.setIIRFilterSize(BME680_FILTER_SIZE_3);
bmx.setGasHeater(320, 150); // 320*C for 150 ms
#endif
#ifdef BMP280
/* Default settings from datasheet. */
bmx.setSampling(Adafruit_BMP280::MODE_NORMAL, /* Operating Mode. */
Adafruit_BMP280::SAMPLING_X2, /* Temp. oversampling */
Adafruit_BMP280::SAMPLING_X16, /* Pressure oversampling */
Adafruit_BMP280::FILTER_X16, /* Filtering. */
Adafruit_BMP280::STANDBY_MS_500); /* Standby time. */
#endif
} else {
printf("Failed to initialize I2C bus\r\n");
}
}
strip.fill(strip.Color(0,0,0));
for (int i=0;i < (PIXEL_COUNT / 2); i++) {
strip.setPixelColor(0, strip.Color(0,0,128));
}
strip.show();
} else {
digitalWrite(WITTY_RGB_R, HIGH);
strip.fill(strip.Color(128,0,0));
for (int i=0;i < (PIXEL_COUNT / 2); i++) {
strip.setPixelColor(0, strip.Color(0,0,128));
}
strip.show();
}
}
void loop()
{
Homie.loop();
/* use the pin, receiving the soft serial additionally as button */
if (digitalRead(GPIO_BUTTON) == LOW) {
if ((millis() - mLastButtonAction) > BUTTON_CHECK_INTERVALL) {
mButtonPressed++;
}
if (mButtonPressed > BUTTON_MIN_ACTION_CYCLE) {
digitalWrite(WITTY_RGB_R, HIGH);
digitalWrite(WITTY_RGB_B, LOW);
strip.fill(strip.Color(0,0,0));
strip.setPixelColor(0, strip.Color((mButtonPressed % 100),0,0));
strip.setPixelColor(1, strip.Color((mButtonPressed / 100),0,0));
strip.setPixelColor(2, strip.Color((mButtonPressed / 100),0,0));
strip.show();
}
} else {
mButtonPressed=0U;
digitalWrite(WITTY_RGB_R, LOW);
}
if (mButtonPressed > BUTTON_MAX_CYCLE) {
mButtonPressed = (BUTTON_MAX_CYCLE -1);
if (SPIFFS.exists("/homie/config.json")) {
strip.fill(strip.Color(0,128,0));
strip.show();
printf("Resetting config\r\n");
SPIFFS.remove("/homie/config.json");
SPIFFS.end();
delay(50);
Homie.reboot();
} else {
printf("No config present\r\n");
strip.fill(strip.Color(0,0,128));
strip.show();
}
}
}
void log(int level, String message, int statusCode)
{
String buffer;
StaticJsonDocument<200> doc;
doc["level"] = level;
doc["message"] = message;
doc["statusCode"] = statusCode;
serializeJson(doc, buffer);
if (mConnected)
{
getTopic(LOG_TOPIC, logTopic)
Homie.getMqttClient().publish(logTopic, 2, false, buffer.c_str());
delete logTopic;
}
Homie.getLogger() << (level) << "@" << (statusCode) << " " << (message) << endl;
}