//#define LOG_LOCAL_LEVEL ESP_LOG_DEBUG #include #include #include #include #include #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 1000 // Config overrides for BSEC #define BME68X_PERIOD_POLL UINT32_C(5000) #include "bme68x.h" #include "bme68x_defs.h" #include "bsec2.h" #include "periph_init.h" struct sensor_itf { uint8_t dev_addr; }; static struct sensor_itf gas_sensor_intf = {}; static struct bme68x_dev gas_sensor = {}; static struct bsec2 gas_sensor_bsec = {}; void bsec2_errormsg(const char *msg) { ESP_LOGE("BSEC", "%s", msg); } uint32_t bsec2_timestamp_millis() { #if 0 struct timeval tv; gettimeofday(&tv, NULL); return tv.tv_sec*1000 + (tv.tv_usec / 1000000); #else return pdTICKS_TO_MS(xTaskGetTickCount()); // This is no-op when using 1ms tick #endif } static void user_delay_us(uint32_t period, void *intf_ptr) { (void) intf_ptr; uint32_t millis = period / 1000; uint32_t usec = period % 1000; #if configTICK_RATE_HZ != 1000 uint32_t remainder = millis % pdTICKS_TO_MS(1); millis -= remainder; usec += remainder * 1000; #endif if (millis) { vTaskDelay(pdMS_TO_TICKS(millis)); } if (usec) { ets_delay_us(usec); } } static BME68X_INTF_RET_TYPE user_i2c_read(uint8_t reg_addr, uint8_t *reg_data, uint32_t length, void *intf_ptr) { ESP_LOGD(TAG, "BME680 i2c read"); 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); esp_err_t ret = i2c_master_cmd_begin(VOC_I2C_NUM, cmd, pdMS_TO_TICKS(VOC_I2C_TO_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), 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; } static BME68X_INTF_RET_TYPE user_i2c_write(uint8_t reg_addr, const uint8_t *reg_data, uint32_t length, void *intf_ptr) { ESP_LOGD(TAG, "BME680 i2c write"); 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); esp_err_t ret = i2c_master_cmd_begin(VOC_I2C_NUM, cmd, pdMS_TO_TICKS(VOC_I2C_TO_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), itf->dev_addr, reg_addr, length); } xSemaphoreGive(g_mux_i2c); return ret == ESP_OK ? BME68X_OK : BME68X_E_COM_FAIL; } static esp_err_t voc_init(void) { int8_t rslt; gas_sensor_intf.dev_addr = BME68X_I2C_ADDR_LOW; gas_sensor.intf_ptr = &gas_sensor_intf; gas_sensor.amb_temp = 25; // TODO set this from senseair! gas_sensor.intf = BME68X_I2C_INTF; gas_sensor.read = user_i2c_read; gas_sensor.write = user_i2c_write; gas_sensor.delay_us = user_delay_us; for (int retry = 0; retry < 3; retry++) { ESP_LOGD(TAG, "BME680 initializing"); rslt = bme68x_init(&gas_sensor); if (rslt == BME68X_OK) { break; } } if (rslt != BME68X_OK) { ESP_LOGE(TAG, "Error from bme680_init: %d", rslt); return ESP_FAIL; } return ESP_OK; } static void new_data_callback( const struct bme68x_data *data, const bsecOutputs *outputs, const struct bsec2 *bsec ) { if (!outputs->nOutputs) { return; } // 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); my_report.iaq_voc_ppm_equiv = output.signal; break; case BSEC_OUTPUT_RAW_PRESSURE: ESP_LOGI(TAG, "\tRAW_PRESSURE = %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); break; case BSEC_OUTPUT_RUN_IN_STATUS: ESP_LOGI(TAG, "\tRUN_IN_STATUS = %d", (int) output.signal); break; default: 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) { ESP_LOGI(TAG, "VOC sensor init"); if (ESP_OK != voc_init()) { ESP_LOGE(TAG, "Fail to init sensor!"); goto abort; } int rv = bsec2_init(&gas_sensor_bsec, &gas_sensor); if (rv != 0) { ESP_LOGE(TAG, "Error in bsec init: %d", rv); goto abort; } bsecSensor sensorList[] = { BSEC_OUTPUT_IAQ, BSEC_OUTPUT_STATIC_IAQ, BSEC_OUTPUT_CO2_EQUIVALENT, BSEC_OUTPUT_BREATH_VOC_EQUIVALENT, BSEC_OUTPUT_RAW_PRESSURE, BSEC_OUTPUT_RAW_GAS, BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE, BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY, BSEC_OUTPUT_STABILIZATION_STATUS, BSEC_OUTPUT_RUN_IN_STATUS, }; rv = bsec2_updateSubscription(&gas_sensor_bsec, sensorList, sizeof(sensorList) / sizeof(bsecSensor), BSEC_SAMPLE_RATE_LP); if (false == rv) { ESP_LOGE(TAG, "Fail to update bsec subscriptions!"); goto abort; } bsec2_attachCallback(&gas_sensor_bsec, new_data_callback); ESP_LOGI(TAG, "BSEC library version %d.%d.%d.%d", gas_sensor_bsec.version.major, gas_sensor_bsec.version.minor, gas_sensor_bsec.version.major_bugfix, gas_sensor_bsec.version.minor_bugfix); // TODO add bsec state persistence to NVS at some landmark intervals, e.g. 1 day // TODO periodic updating of sensor ambient temp while (1) { rv = bsec2_run(&gas_sensor_bsec); if (false == rv) { ESP_LOGE(TAG, "Error in bsec run!"); } vTaskDelay(pdMS_TO_TICKS(100)); } abort: vTaskDelete(NULL); }