From 557b0745271dd80f92243abac5bf68ff6fb694b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Mon, 13 Dec 2021 01:20:40 +0100 Subject: [PATCH] implemented co2 and web access --- main/CMakeLists.txt | 2 + main/app_main.c | 4 +- main/co2_sensor.c | 133 ++++++++++++++++++++++++++++++++++++++++++++ main/co2_sensor.h | 12 ++++ main/data_report.c | 4 ++ main/data_report.h | 35 ++++++++++++ main/periph_init.c | 18 ++++++ main/periph_init.h | 4 ++ main/voc_sensor.c | 64 +++++++++++++++++---- main/web/websrv.c | 42 ++++++++++++++ 10 files changed, 307 insertions(+), 11 deletions(-) create mode 100644 main/co2_sensor.c create mode 100644 main/co2_sensor.h create mode 100644 main/data_report.c create mode 100644 main/data_report.h diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 72cb60a..4d997c4 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -6,6 +6,8 @@ idf_component_register(SRCS utils.c wifi_conn.c voc_sensor.c + co2_sensor.c + data_report.c periph_init.c i2c_utils.c console/console_ioimpl.c diff --git a/main/app_main.c b/main/app_main.c index 5a7e459..a821df8 100644 --- a/main/app_main.c +++ b/main/app_main.c @@ -22,6 +22,7 @@ #include "periph_init.h" #include "voc_sensor.h" #include "tasks.h" +#include "co2_sensor.h" static const char *TAG = "main"; @@ -56,7 +57,8 @@ void app_main(void) { periph_init(); - xTaskCreate(voc_read_task, "VOC", 4096, NULL, PRIO_NORMAL, NULL); + xTaskCreatePinnedToCore(voc_read_task, "VOC", 4096, NULL, PRIO_NORMAL, NULL, 1); + xTaskCreatePinnedToCore(co2_read_task, "CO2", 4096, NULL, PRIO_NORMAL, NULL, 1); console_init(NULL); register_console_commands(); diff --git a/main/co2_sensor.c b/main/co2_sensor.c new file mode 100644 index 0000000..2b2fa2e --- /dev/null +++ b/main/co2_sensor.c @@ -0,0 +1,133 @@ +#include "co2_sensor.h" + +#include +#include +#include +#include +#include +#include +#include "voc_sensor.h" +#include "i2c_utils.h" +#include "periph_init.h" +#include "data_report.h" + +static const char *TAG = "co2"; + +#define CO2_ADDR 104 +#define CO2_I2C_NUM I2C_NUM_0 +#define TIMEOUT_MS 1000 + +static esp_err_t do_reg_read(uint8_t reg_addr, uint8_t *reg_data, uint32_t length, uint32_t timeout_ms) { + i2c_cmd_handle_t cmd = i2c_cmd_link_create(); + i2c_reg_read(cmd, CO2_ADDR, reg_addr, reg_data, length); + esp_err_t ret = i2c_master_cmd_begin(CO2_I2C_NUM, cmd, pdMS_TO_TICKS(TIMEOUT_MS)); + i2c_cmd_link_delete(cmd); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "I2C read error: %s, devaddr %x, reg %d, len %d", esp_err_to_name(ret), CO2_ADDR, reg_addr, length); + } + ESP_LOG_BUFFER_HEXDUMP(TAG, reg_data, length, ESP_LOG_DEBUG); + return ret; +} + +bool wake_up() { + esp_err_t suc; +#define TRY(x) suc=x; if(suc!=ESP_OK) return suc; +// for (int i = 0; i < 10; i++) { + uint8_t dummy; + i2c_cmd_handle_t chain = i2c_cmd_link_create(); + TRY(i2c_master_start(chain)); + TRY(i2c_master_write_byte(chain, (CO2_ADDR << 1) | I2C_MASTER_WRITE, false)); + TRY(i2c_master_write_byte(chain, 0x30, false)); + TRY(i2c_master_start(chain)); + TRY(i2c_master_write_byte(chain, (CO2_ADDR << 1) | I2C_MASTER_READ, false)); // TODO expect ack? + TRY(i2c_master_read(chain, &dummy, 1, I2C_MASTER_LAST_NACK)); + TRY(i2c_master_stop(chain)); + /*esp_err_t ret = */ i2c_master_cmd_begin(CO2_I2C_NUM, chain, pdMS_TO_TICKS(10)); + i2c_cmd_link_delete(chain); +// if (ret == ESP_OK) { + vTaskDelay(pdMS_TO_TICKS(14)); + return true; +// } +//// } +// ESP_LOGE(TAG, "Fail to wake up"); +// return false; +#undef TRY +} + +static esp_err_t reg_read(uint8_t reg_addr, uint8_t *reg_data, uint32_t length, uint32_t timeout_ms) { + BaseType_t suc = xSemaphoreTake(g_mux_i2c, pdMS_TO_TICKS(500)); + if (suc != pdPASS) { + ESP_LOGE(TAG, "Sema fail"); + return ESP_ERR_TIMEOUT; + } + if (!wake_up()) { + xSemaphoreGive(g_mux_i2c); + return ESP_ERR_TIMEOUT; + } + esp_err_t rv = do_reg_read(reg_addr, reg_data, length, timeout_ms); + xSemaphoreGive(g_mux_i2c); + ets_delay_us(12000); + return rv; +} + +static esp_err_t do_reg_write(uint8_t reg_addr, const uint8_t *reg_data, uint32_t length, uint32_t timeout_ms) { + i2c_cmd_handle_t cmd = i2c_cmd_link_create(); + i2c_reg_write(cmd, CO2_ADDR, reg_addr, reg_data, length); + esp_err_t ret = i2c_master_cmd_begin(CO2_I2C_NUM, cmd, pdMS_TO_TICKS(timeout_ms)); + i2c_cmd_link_delete(cmd); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "I2C write error: %s, devaddr %x, reg %d, len %d", esp_err_to_name(ret), CO2_ADDR, reg_addr, length); + } + return ret; +} + +static esp_err_t reg_write(uint8_t reg_addr, const uint8_t *reg_data, uint32_t length, uint32_t timeout_ms) { + BaseType_t suc = xSemaphoreTake(g_mux_i2c, pdMS_TO_TICKS(500)); + if (suc != pdPASS) { + ESP_LOGE(TAG, "Sema fail"); + return ESP_ERR_TIMEOUT; + } + if (!wake_up()) { + xSemaphoreGive(g_mux_i2c); + return ESP_ERR_TIMEOUT; + } + esp_err_t rv = do_reg_write(reg_addr, reg_data, length, timeout_ms); + xSemaphoreGive(g_mux_i2c); + vTaskDelay(pdMS_TO_TICKS(12)); + return rv; +} + +void co2_read_task(void *param) { + // i2c is protected by a semaphore inside the driver, no need to worry about it here + + // continuous is the default + + vTaskDelay(pdMS_TO_TICKS(35)); + + { + uint8_t pld[] = {0x00, 0x07, 0x00, 0x05}; + reg_write(0x96, pld, 4, TIMEOUT_MS); + } + + while (1) { + vTaskDelay(pdMS_TO_TICKS(7000)); + + uint8_t data[4] = {}; + if (ESP_OK == reg_read(0x06, data, 4, TIMEOUT_MS)) { + uint16_t filtered = (data[0] << 8) | data[1]; + uint16_t unfiltered = (data[2] << 8) | data[3]; + + ESP_LOGI(TAG, "CO2 ppm %d, raw %d", filtered, unfiltered); + + if (pdPASS == xSemaphoreTake(g_mux_data_report, pdMS_TO_TICKS(1000))) { + if (filtered > 400 && filtered < 5000) { + g_data_report.co2_ppm = (float) filtered; + g_data_report.co2_ready = true; + } else { + g_data_report.co2_ready = false; + } + } + xSemaphoreGive(g_mux_data_report); + } + } +} diff --git a/main/co2_sensor.h b/main/co2_sensor.h new file mode 100644 index 0000000..d54e0a0 --- /dev/null +++ b/main/co2_sensor.h @@ -0,0 +1,12 @@ +/** + * TODO file description + * + * Created on 2021/12/12. + */ + +#ifndef ESPNODE_CO2_SENSOR_H +#define ESPNODE_CO2_SENSOR_H + +void co2_read_task(void *param); + +#endif //ESPNODE_CO2_SENSOR_H diff --git a/main/data_report.c b/main/data_report.c new file mode 100644 index 0000000..580de9a --- /dev/null +++ b/main/data_report.c @@ -0,0 +1,4 @@ +#include "data_report.h" + +SemaphoreHandle_t g_mux_data_report; +struct data_report g_data_report = {}; diff --git a/main/data_report.h b/main/data_report.h new file mode 100644 index 0000000..26c2e49 --- /dev/null +++ b/main/data_report.h @@ -0,0 +1,35 @@ +/** + * TODO file description + * + * Created on 2021/12/13. + */ + +#ifndef ESPNODE_DATA_REPORT_H +#define ESPNODE_DATA_REPORT_H + +#include +#include +#include +#include + +struct data_report { + bool iaq_ready; + float iaq; + float iaq_static; + float iaq_co2_ppm_equiv; + float iaq_voc_ppm_equiv; + + bool thpg_ready; + float temperature; + float pressure; + float humidity; + float gasr; + + bool co2_ready; + float co2_ppm; +}; + +extern SemaphoreHandle_t g_mux_data_report; +extern struct data_report g_data_report; + +#endif //ESPNODE_DATA_REPORT_H diff --git a/main/periph_init.c b/main/periph_init.c index b493fb0..08a8944 100644 --- a/main/periph_init.c +++ b/main/periph_init.c @@ -1,12 +1,21 @@ #include #include "periph_init.h" #include "driver/i2c.h" +#include "data_report.h" static const char *TAG = "periph_init"; +SemaphoreHandle_t g_mux_i2c; + esp_err_t periph_init() { esp_err_t rv; + g_mux_i2c = xSemaphoreCreateMutex(); + assert(g_mux_i2c); + + g_mux_data_report = xSemaphoreCreateMutex(); // XXX move elsewhere + assert(g_mux_data_report); + ESP_LOGI(TAG, "Init I2C"); int i2c_master_port = I2C_NUM_0; @@ -29,5 +38,14 @@ esp_err_t periph_init() { return rv; } + gpio_config_t gconf = { + .pin_bit_mask = (1 << CONFIG_PIN_SENSEAIR_NRDY), + .mode = GPIO_MODE_INPUT, + .pull_up_en = GPIO_PULLUP_ENABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE, // TODO + }; + gpio_config(&gconf); + return ESP_OK; } diff --git a/main/periph_init.h b/main/periph_init.h index c57cf54..e40bbcf 100644 --- a/main/periph_init.h +++ b/main/periph_init.h @@ -8,6 +8,10 @@ #define ESPNODE_PERIPH_INIT_H #include +#include +#include + +extern SemaphoreHandle_t g_mux_i2c; esp_err_t periph_init(); diff --git a/main/voc_sensor.c b/main/voc_sensor.c index 7149c6f..70cbe9e 100644 --- a/main/voc_sensor.c +++ b/main/voc_sensor.c @@ -1,7 +1,6 @@ //#define LOG_LOCAL_LEVEL ESP_LOG_DEBUG #include -#include #include #include #include @@ -9,11 +8,12 @@ #include #include "voc_sensor.h" #include "i2c_utils.h" +#include "data_report.h" static const char *TAG = "voc"; #define VOC_I2C_NUM I2C_NUM_0 -#define VOC_I2C_TO_MS 250 +#define VOC_I2C_TO_MS 1000 // Config overrides for BSEC #define BME68X_PERIOD_POLL UINT32_C(5000) @@ -21,6 +21,7 @@ static const char *TAG = "voc"; #include "bme68x.h" #include "bme68x_defs.h" #include "bsec2.h" +#include "periph_init.h" struct sensor_itf { uint8_t dev_addr; @@ -69,6 +70,11 @@ static BME68X_INTF_RET_TYPE user_i2c_read(uint8_t reg_addr, uint8_t *reg_data, u if (length == 0) { return BME68X_OK; } + BaseType_t suc = xSemaphoreTake(g_mux_i2c, pdMS_TO_TICKS(500)); + if (suc != pdPASS) { + ESP_LOGE(TAG, "Sema fail"); + return -1; + } struct sensor_itf *itf = intf_ptr; i2c_cmd_handle_t cmd = i2c_cmd_link_create(); i2c_reg_read(cmd, itf->dev_addr, reg_addr, reg_data, length); @@ -78,6 +84,7 @@ static BME68X_INTF_RET_TYPE user_i2c_read(uint8_t reg_addr, uint8_t *reg_data, u ESP_LOGE(TAG, "I2C read error: %s, devaddr %x, reg %d, len %d", esp_err_to_name(ret), itf->dev_addr, reg_addr, length); } ESP_LOG_BUFFER_HEXDUMP(TAG, reg_data, length, ESP_LOG_DEBUG); + xSemaphoreGive(g_mux_i2c); return ret == ESP_OK ? BME68X_OK : BME68X_E_COM_FAIL; } @@ -86,6 +93,11 @@ static BME68X_INTF_RET_TYPE user_i2c_write(uint8_t reg_addr, const uint8_t *reg_ if (length == 0) { return BME68X_OK; } + BaseType_t suc = xSemaphoreTake(g_mux_i2c, pdMS_TO_TICKS(500)); + if (suc != pdPASS) { + ESP_LOGE(TAG, "Sema fail"); + return -1; + } struct sensor_itf *itf = intf_ptr; i2c_cmd_handle_t cmd = i2c_cmd_link_create(); i2c_reg_write(cmd, itf->dev_addr, reg_addr, reg_data, length); @@ -94,6 +106,7 @@ static BME68X_INTF_RET_TYPE user_i2c_write(uint8_t reg_addr, const uint8_t *reg_ if (ret != ESP_OK) { ESP_LOGE(TAG, "I2C write error: %s, devaddr %x, reg %d, len %d", esp_err_to_name(ret), itf->dev_addr, reg_addr, length); } + xSemaphoreGive(g_mux_i2c); return ret == ESP_OK ? BME68X_OK : BME68X_E_COM_FAIL; } @@ -133,39 +146,48 @@ static void new_data_callback( // TODO do something with this + struct data_report my_report = {}; + ESP_LOGI(TAG, "BSEC outputs: timestamp = %d", (int) (outputs->output[0].time_stamp / INT64_C(1000000))); for (uint8_t i = 0; i < outputs->nOutputs; i++) { const bsecData output = outputs->output[i]; switch (output.sensor_id) { case BSEC_OUTPUT_IAQ: ESP_LOGI(TAG, "\tIAQ = %f, accuracy %d", output.signal, (int) output.accuracy); + my_report.iaq = output.signal; + my_report.iaq_ready = (3 == (int) output.accuracy); break; case BSEC_OUTPUT_STATIC_IAQ: ESP_LOGI(TAG, "\tSTATIC_IAQ = %f, accuracy %d", output.signal, (int) output.accuracy); + my_report.iaq_static = output.signal; break; case BSEC_OUTPUT_CO2_EQUIVALENT: ESP_LOGI(TAG, "\tCO2_EQUIVALENT = %f, accuracy %d", output.signal, (int) output.accuracy); + my_report.iaq_co2_ppm_equiv = output.signal; break; case BSEC_OUTPUT_BREATH_VOC_EQUIVALENT: ESP_LOGI(TAG, "\tBREATH_VOC_EQUIVALENT = %f, accuracy %d", output.signal, (int) output.accuracy); - break; - case BSEC_OUTPUT_RAW_TEMPERATURE: - ESP_LOGI(TAG, "\tRAW_TEMPERATURE = %f", output.signal); + my_report.iaq_voc_ppm_equiv = output.signal; break; case BSEC_OUTPUT_RAW_PRESSURE: ESP_LOGI(TAG, "\tRAW_PRESSURE = %f", output.signal); - break; - case BSEC_OUTPUT_RAW_HUMIDITY: - ESP_LOGI(TAG, "\tRAW_HUMIDITY = %f", output.signal); + my_report.pressure = output.signal; + my_report.thpg_ready = true; break; case BSEC_OUTPUT_RAW_GAS: ESP_LOGI(TAG, "\tRAW_GAS = %f", output.signal); + my_report.gasr = output.signal; + my_report.thpg_ready = true; break; case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE: ESP_LOGI(TAG, "\tSENSOR_HEAT_COMPENSATED_TEMPERATURE = %f", output.signal); + my_report.temperature = output.signal; + my_report.thpg_ready = true; break; case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY: ESP_LOGI(TAG, "\tSENSOR_HEAT_COMPENSATED_HUMIDITY = %f", output.signal); + my_report.humidity = output.signal; + my_report.thpg_ready = true; break; case BSEC_OUTPUT_STABILIZATION_STATUS: ESP_LOGI(TAG, "\tSTABILIZATION_STATUS status = %d", (int) output.signal); @@ -177,6 +199,30 @@ static void new_data_callback( break; } } + + if (pdPASS == xSemaphoreTake(g_mux_data_report, pdMS_TO_TICKS(1000))) { + if (my_report.thpg_ready) { + g_data_report.thpg_ready = true; + g_data_report.humidity = my_report.humidity; + g_data_report.temperature = my_report.temperature; + g_data_report.pressure = my_report.pressure; + g_data_report.gasr = my_report.gasr; + } else { + g_data_report.thpg_ready = false; + } + + if (my_report.iaq_ready) { + g_data_report.iaq_ready = true; + g_data_report.iaq = my_report.iaq; + g_data_report.iaq_static = my_report.iaq_static; + g_data_report.iaq_co2_ppm_equiv = my_report.iaq_co2_ppm_equiv; + g_data_report.iaq_voc_ppm_equiv = my_report.iaq_voc_ppm_equiv; + } else { + g_data_report.iaq_ready = false; + } + + xSemaphoreGive(g_mux_data_report); + } } void voc_read_task(void *param) { @@ -199,9 +245,7 @@ void voc_read_task(void *param) { BSEC_OUTPUT_CO2_EQUIVALENT, BSEC_OUTPUT_BREATH_VOC_EQUIVALENT, - BSEC_OUTPUT_RAW_TEMPERATURE, // XXX probably useless, we can get these directly from the data struct BSEC_OUTPUT_RAW_PRESSURE, - BSEC_OUTPUT_RAW_HUMIDITY, BSEC_OUTPUT_RAW_GAS, BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE, diff --git a/main/web/websrv.c b/main/web/websrv.c index b875853..91a8ad8 100644 --- a/main/web/websrv.c +++ b/main/web/websrv.c @@ -10,6 +10,7 @@ #include "utils.h" #include "www_files_enum.h" +#include "data_report.h" static const char *TAG = "websrv"; @@ -81,6 +82,42 @@ static esp_err_t handler_reboot(httpd_req_t *req) { esp_restart(); } +static esp_err_t handler_sample(httpd_req_t *req) { + char *msg = malloc(256); + if (!msg) return ESP_ERR_NO_MEM; + + // TODO use cJSON + + char *wp = msg; + wp += sprintf(wp, "{"); + + bool need_comma = false; + if (g_data_report.iaq_ready) { + wp += sprintf(wp, "\"iaq\":%.2f,\"iaq_s\":%.2f,\"iaq_co2\":%.2f,\"iaq_voc\":%.2f", + g_data_report.iaq,g_data_report.iaq_static, g_data_report.iaq_co2_ppm_equiv, g_data_report.iaq_co2_ppm_equiv); + need_comma = true; + } + + if (g_data_report.thpg_ready) { + wp += sprintf(wp, "%s\"temp\":%.2f,\"hum\":%.2f,\"pres\":%.1f,\"gasr\":%.1f", + need_comma?",":"", + g_data_report.temperature,g_data_report.humidity, g_data_report.pressure, g_data_report.gasr); + need_comma = true; + } + + if (g_data_report.co2_ready) { + wp += sprintf(wp, "%s\"co2\":%.2f", + need_comma?",":"", + g_data_report.co2_ppm); + } + + sprintf(wp, "}"); + + esp_err_t rv = httpd_resp_send(req, msg, -1); + free(msg); + return rv; +} + /* An HTTP GET handler */ static esp_err_t handler_staticfiles(httpd_req_t *r) { const struct embedded_file_info *file; @@ -133,6 +170,11 @@ static const httpd_uri_t routes[] = { .method = HTTP_GET, .handler = handler_reboot, }, + { + .uri = "/sample", + .method = HTTP_GET, + .handler = handler_sample, + }, { .uri = "*", // any file except protected (e.g. not HTML, PEM etc) .method = HTTP_GET,