diff --git a/Readme.md b/Readme.md index 358ebbf..0a424ff 100644 --- a/Readme.md +++ b/Readme.md @@ -54,7 +54,15 @@ The following pins are used: * BMP280 sensor * some wire +# Webserver + +This version has a webserver activated, if MQTT server is set to **localhost**. + +Every 20 seconds a measurement is performed. +This is shown in a diagram via chart.js + # Sources For the Witty board * [https://github.com/amkuipers/witty Witty pinout] * [https://arduino.ua/products_pictures/large_AOC361-5.jpg Schematics] +* [https://www.chartjs.org/docs/latest/getting-started/integration.html] diff --git a/data/chart.htm b/data/chart.htm new file mode 100644 index 0000000..b7a5587 --- /dev/null +++ b/data/chart.htm @@ -0,0 +1,37 @@ + + +
+ +| Sensor | +Value | ++ |
| Temperatur | +temp | +°C | +
| Humidity | +humidity | +% | +
| Gas | +gas | +KOhms | +
| Altitude | +altitude | +m | +
| Pressure | +pressure | +hPa | +
| Particle | +particle | +micro gram per quibik | +
+ Can be found on the next page. +
+ + diff --git a/platformio.ini b/platformio.ini index b9c1666..5a62172 100644 --- a/platformio.ini +++ b/platformio.ini @@ -12,7 +12,7 @@ platform = espressif8266 board = d1_mini framework = arduino -build_flags = -D PIO_FRAMEWORK_ARDUINO_LWIP2_LOW_MEMORY -D BME680 +build_flags = -D PIO_FRAMEWORK_ARDUINO_LWIP2_LOW_MEMORY -DBMP280 ; build_flag needs define the Bosch sensor... ; -D BMP280 ;or @@ -25,4 +25,4 @@ lib_deps = https://github.com/homieiot/homie-esp8266.git#develop adafruit/Adafruit BMP280 Library @ ^2.4.2 adafruit/Adafruit BME680 Library @ ^2.0.1 -upload_port = /dev/ttyUSB0 \ No newline at end of file +upload_port = /dev/ttyUSB1 \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index f3016bc..b66764a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -39,15 +39,12 @@ #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 PM1006_MQTT_UPDATE 30000 /**< Check the sensor every 30 seconds; New measurement is done every 20seconds by the PM1006 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 D5 /**< GPIO14 - I2C clock pin */ #define SENSOR_I2C_SDI D1 /**< 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 */ @@ -84,11 +81,29 @@ #define NODE_GAS "gas" #define NODE_HUMIDITY "humidity" #define NODE_AMBIENT "ambient" + #define NODE_BUTTON "button" + +#define PORTNUMBER_HTTP 80 /**< IANA TCP/IP port number, used for HTTP / web traffic */ + +#define SERIAL_RCEVBUF_MAX 80 /**< Maximum 80 characters can be received from the PM1006 sensor */ +#define MEASURE_POINT_MAX 1024 /**< Amount of measure point, that can be stored */ + /****************************************************************************** * TYPE DEFS ******************************************************************************/ +typedef struct s_point { + long timestamp; + float temp; +#ifdef BME680 + uint32_t gas; + float humidity; +#endif + float pressure; + int pm25; +} sensor_point; + /****************************************************************************** * FUNCTION PROTOTYPES ******************************************************************************/ @@ -102,6 +117,7 @@ void log(int level, String message, int code); bool mConfigured = false; bool mConnected = false; bool mFailedI2Cinitialization = false; +AsyncWebServer* mHttp = NULL; long mLastButtonAction = 0; /******************************* Sensor data **************************/ @@ -146,12 +162,16 @@ Adafruit_BMP280 bmx; // connected via I2C Adafruit_NeoPixel strip(PIXEL_COUNT, GPIO_WS2812, NEO_GRB + NEO_KHZ800); // Variablen -uint8_t serialRxBuf[80]; +uint8_t serialRxBuf[SERIAL_RCEVBUF_MAX]; uint8_t rxBufIdx = 0; -int spm25 = 0; +int mParticle_pM25 = 0; int last = 0; unsigned int mButtonPressed = 0; bool mSomethingReceived = false; +bool mConnectedNonMQTT = false; + +sensor_point* mMeasureSeries; +uint32_t mMeasureIndex = 0; /****************************************************************************** * LOCAL FUNCTIONS @@ -207,8 +227,7 @@ void onHomieEvent(const HomieEvent &event) { switch (event.type) { - case HomieEventType::MQTT_READY: - mConnected=true; + case HomieEventType::WIFI_CONNECTED: digitalWrite(WITTY_RGB_R, LOW); if (!i2cEnable.get()) { /** keep green LED activated to power I2C sensor */ digitalWrite(WITTY_RGB_G, LOW); @@ -217,9 +236,25 @@ void onHomieEvent(const HomieEvent &event) digitalWrite(WITTY_RGB_B, LOW); strip.fill(strip.Color(0,0,128)); strip.show(); + if (mHttp != NULL) { + strip.fill(strip.Color(0,64,0)); + mConnectedNonMQTT = true; + } + break; + case HomieEventType::MQTT_READY: + mConnected=true; if (mFailedI2Cinitialization) { - log(MQTT_LEVEL_DEBUG, F("Could not find a valid BME680 sensor, check wiring or " - "try a different address!"), MQTT_LOG_I2CINIT); + log(MQTT_LEVEL_DEBUG, +#ifdef BME680 + "Could not find a valid BME680 sensor, check wiring or try a different address!" +#else +#ifdef BMP280 + "Could not find a valid BMP280 sensor, check wiring or try a different address!" +#else + "no I2C sensor defined" +#endif +#endif + , MQTT_LOG_I2CINIT); } else { log(MQTT_LEVEL_INFO, F("BME680 sensor found"), MQTT_LOG_I2CINIT); } @@ -231,23 +266,35 @@ void onHomieEvent(const HomieEvent &event) case HomieEventType::OTA_SUCCESSFUL: ESP.restart(); break; - case HomieEventType::WIFI_CONNECTED: - digitalWrite(WITTY_RGB_B, HIGH); - break; default: break; } } -void bmpPublishValues() { +void updateLEDs() { #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); + log(MQTT_LEVEL_ERROR, "BME680 not accessible", MQTT_LOG_I2READ); return; } #endif + 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(); + } else { +#ifdef BME680 + bmx.performReading(); +#endif + } +} + +void bmpPublishValues() { // Publish the values temperaturNode.setProperty(NODE_TEMPERATUR).send(String(bmx.readTemperature())); pressureNode.setProperty(NODE_PRESSURE).send(String(bmx.readPressure() / 100.0F)); @@ -269,6 +316,130 @@ void bmpPublishValues() { } } +/** + * @brief Generate JSON with last measured values + * For the update interval, please check @see PM1006_MQTT_UPDATE + * @return String with JSON + */ +String sensorAsJSON(void) { + String buffer; + StaticJsonDocument<500> doc; + + if (mMeasureIndex > 0) { + int lastIdx = mMeasureIndex - 1; + doc["temp"] = String(mMeasureSeries[lastIdx].temp); +#ifdef BME680 + doc["gas"] = String(mMeasureSeries[lastIdx].gas); + doc["humidity"] = String(mMeasureSeries[lastIdx].humidity); +#endif + float atmospheric = mMeasureSeries[lastIdx].pressure; + float altitude = 44330.0 * (1.0 - pow(atmospheric / SEALEVELPRESSURE_HPA, 0.1903)); + doc["altitude"] = String(altitude); + doc["pressure"] = String(atmospheric); + doc["particle"] = String(mMeasureSeries[lastIdx].pm25); + } + doc["cycle"] = String(mMeasureIndex); + serializeJson(doc, buffer); + return buffer; +} + +/** + * @brief Generate JSON with all available sensors + * + * @return String + */ +String sensorHeader(void) { + String buffer; + StaticJsonDocument<500> doc; + doc.add("milli"); + doc.add("temp"); +#ifdef BME680 + doc.add("gas"); + doc.add("humidity"); +#endif + doc.add("altitude"); + doc.add("pressure"); + doc.add("particle"); + serializeJson(doc, buffer); + return buffer; +} + +/** + * @brief Generate JSON with all sensor values + * Array of JSON with all measured elements to show in the web browser. + * Example: + *
+ * dynamicData = {
+ * labels: [ "0", "-30", "-60", "-90", "-120", "-160" ],
+ * datasets: [{
+ * label: 'temp',
+ * data: [24, 22, 23, 25, 22, 23],
+ * borderColor: "rgba(255,0,0,1)"
+ * }, {
+ * label: 'pressure',
+ * data: [1000, 1002, 1003, 999, 996, 1000],
+ * borderColor: "rgba(0,0,255,1)"
+ * }
+ * ]
+ * }
+ *
+ *
+ * @return String
+ */
+String diagramJson(void) {
+ int i;
+ String buffer;
+ String bufferLabels = "\"\"";
+ String bufferDataTemp = "\"\"";
+ String bufferDataPressure = "\"\"";
+ String bufferDataPM = "\"\"";
+ String bufferDatasets;
+ String bufferData;
+ if (mMeasureSeries == NULL) {
+ buffer = "{ \"error\": \"Malloc failed\" }";
+ return buffer;
+ }
+
+ long now = millis();
+ if (mMeasureIndex > 0) {
+ bufferLabels = "[ \"" + String((now - mMeasureSeries[0].timestamp) / 1000) + "s";
+ bufferDataTemp = "[ \"" + String(mMeasureSeries[0].temp);
+ bufferDataPressure = "[ \"" + String(mMeasureSeries[0].pressure);
+ bufferDataPM = "[ \"" + String(mMeasureSeries[0].pm25);
+ }
+ for(i=1; i < mMeasureIndex; i++) {
+ bufferLabels += "\", \"" + String((now - mMeasureSeries[i].timestamp) / 1000) + "s";
+ bufferDataTemp += "\", \"" + String(mMeasureSeries[i].temp);
+ bufferDataPressure += "\", \"" + String(mMeasureSeries[i].pressure);
+ bufferDataPM += "\", \"" + String(mMeasureSeries[i].pm25);
+ }
+ if (mMeasureIndex > 0) {
+ bufferLabels += "\" ]";
+ bufferDataTemp += "\" ]";
+ bufferDataPressure += "\" ]";
+ bufferDataPM += "\" ]";
+ }
+ /* Generate label */
+ buffer = "{ \"labels\" : " + bufferLabels + ",\n";
+ buffer += "\"datasets\" : [";
+
+ /* generate first block for Temperature */
+ buffer += "{\n \"label\" : \"Temp\",\n \"data\" : " + bufferDataTemp + ",\n \"borderColor\" : \"rgba(255,0,0,1)\" \n}";
+
+ /* generate second block for Pressure */
+ buffer += ", ";
+ buffer += "{\n \"label\" : \"Pressure\",\n \"data\" : " + bufferDataPressure + ",\n \"borderColor\" : \"rgba(0,0,255,1)\" \n}";
+
+ /* generate third block for PM2.5 values */
+ buffer += ", ";
+ buffer += "{\n \"label\" : \"PM 2.5\",\n \"data\" : " + bufferDataPM + ",\n \"borderColor\" : \"rgba(0,255,0,1)\" \n}";
+
+ /* TODO, next ones ... */
+
+ buffer += "]\n }";
+ return buffer;
+}
+
/**
* @brief Main loop, triggered by the Homie API
* All logic needs to be done here.
@@ -284,13 +455,15 @@ 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));
+ mParticle_pM25 = getSensorData();
+ if (mParticle_pM25 >= 0) {
+ if (!mConnectedNonMQTT) {
+ particle.setProperty(NODE_PARTICLE).send(String(mParticle_pM25));
+ }
if (!mSomethingReceived) {
- if (pM25 < 35) {
+ if (mParticle_pM25 < 35) {
strip.fill(strip.Color(0, PERCENT2FACTOR(127, rgbDim), 0)); /* green */
- } else if (pM25 < 85) {
+ } else if (mParticle_pM25 < 85) {
strip.fill(strip.Color(PERCENT2FACTOR(127, rgbDim), PERCENT2FACTOR(64, rgbDim), 0)); /* orange */
} else {
strip.fill(strip.Color(PERCENT2FACTOR(127, rgbDim), 0, 0)); /* red */
@@ -301,18 +474,35 @@ void loopHandler()
/* Read BOSCH sensor */
if (i2cEnable.get() && (!mFailedI2Cinitialization)) {
- bmpPublishValues();
+ updateLEDs();
+ if (!mConnectedNonMQTT) {
+ bmpPublishValues();
+ }
+ }
+
+ // FIXME: add the measured data into the big list
+ if (mMeasureSeries != NULL) {
+ mMeasureSeries[mMeasureIndex].timestamp = millis();
+ mMeasureSeries[mMeasureIndex].temp = bmx.readTemperature();
+#ifdef BME680
+ mMeasureSeries[mMeasureIndex].gas = (bmx.gas_resistance / 1000.0);
+ mMeasureSeries[mMeasureIndex].humidity = bmx.humidity;
+#endif
+ float atmospheric = bmx.readPressure() / 100.0F;
+ mMeasureSeries[mMeasureIndex].pressure = atmospheric;
+ mMeasureSeries[mMeasureIndex].pm25 = mParticle_pM25;
+ mMeasureIndex++;
}
/* Clean cycles buttons */
- if (mButtonPressed <= BUTTON_MIN_ACTION_CYCLE) {
+ if ((!mConnectedNonMQTT) && (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) {
+ if ((!mConnectedNonMQTT) && (mButtonPressed > BUTTON_MIN_ACTION_CYCLE)) {
buttonNode.setProperty(NODE_BUTTON).send(String(mButtonPressed));
}
@@ -353,8 +543,6 @@ bool ledHandler(const HomieRange& range, const String& value) {
return false;
}
-
-
/******************************************************************************
* GLOBAL FUNCTIONS
*****************************************************************************/
@@ -374,12 +562,12 @@ void setup()
Homie_setFirmware(HOMIE_FIRMWARE_NAME, HOMIE_FIRMWARE_VERSION);
Homie.setLoopFunction(loopHandler);
Homie.onEvent(onHomieEvent);
- i2cEnable.setDefaultValue(false);
+ i2cEnable.setDefaultValue(true);
rgbTemp.setDefaultValue(false);
rgbDim.setDefaultValue(100).setValidator([] (long candidate) {
return (candidate > 1) && (candidate <= 200);
});
- memset(serialRxBuf, 0, 80);
+ memset(serialRxBuf, 0, SERIAL_RCEVBUF_MAX);
pmSerial.begin(PM1006_BIT_RATE);
Homie.setup();
@@ -414,6 +602,8 @@ void setup()
digitalWrite(WITTY_RGB_G, HIGH);
if (mConfigured)
{
+ mMeasureSeries = (sensor_point *) malloc(sizeof(sensor_point) * MEASURE_POINT_MAX);
+ memset(mMeasureSeries, 0, sizeof(sensor_point) * MEASURE_POINT_MAX);
if (i2cEnable.get()) {
#ifdef BME680
printf("Wait 1 second...\r\n");
@@ -443,28 +633,44 @@ void setup()
Adafruit_BMP280::FILTER_X16, /* Filtering. */
Adafruit_BMP280::STANDBY_MS_500); /* Standby time. */
#endif
+ printf("Sensor found on I2C bus\r\n");
} 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 * rgbDim.get()));
- }
- 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));
- }
+
+ for (int i=0;i < (PIXEL_COUNT / 2); i++) {
+ strip.setPixelColor(0, strip.Color(0,128,0));
+ }
+ mHttp = new AsyncWebServer(PORTNUMBER_HTTP);
+ mHttp->on("/heap", HTTP_GET, [](AsyncWebServerRequest *request){
+ request->send(200, "text/plain", String(ESP.getFreeHeap()));
+ });
+ mHttp->on("/sensors", HTTP_GET, [](AsyncWebServerRequest *request){
+ request->send(200, "application/json", sensorAsJSON());
+ });
+ mHttp->on("/header", HTTP_GET, [](AsyncWebServerRequest *request){
+ request->send(200, "application/json", sensorHeader());
+ });
+ mHttp->on("/diagram", HTTP_GET, [](AsyncWebServerRequest *request){
+ request->send(200, "application/json", diagramJson());
+ });
+ mHttp->serveStatic("/", SPIFFS, "/").setDefaultFile("standalone.htm");
+ mHttp->begin();
+ Homie.getLogger() << "Webserver started" << endl;
strip.show();
}
}
void loop()
{
- Homie.loop();
+ if (!mConnectedNonMQTT) {
+ Homie.loop();
+ } else {
+ /* call the custom loop directly, as Homie is deactivated */
+ loopHandler();
+ }
/* use the pin, receiving the soft serial additionally as button */
if (digitalRead(GPIO_BUTTON) == LOW) {
if ((millis() - mLastButtonAction) > BUTTON_CHECK_INTERVALL) {
@@ -484,8 +690,7 @@ void loop()
digitalWrite(WITTY_RGB_R, LOW);
}
- if (mButtonPressed > BUTTON_MAX_CYCLE) {
- mButtonPressed = (BUTTON_MAX_CYCLE -1);
+ if (mButtonPressed > BUTTON_MAX_CYCLE) {
if (SPIFFS.exists("/homie/config.json")) {
strip.fill(strip.Color(0,PERCENT2FACTOR(127, rgbDim),0));
strip.show();