/* * Copyright (c) 2025 Prevas A/S * * SPDX-License-Identifier: Apache-2.0 * * Datasheet: * https://www.gassensing.co.uk/wp-content/uploads/2023/05/ExplorIR-M-Data-Sheet-Rev-4.13_3.pdf * */ #define DT_DRV_COMPAT gss_explorir_m #include #include #include #include #include #include #include LOG_MODULE_REGISTER(explorir_m_sensor, CONFIG_SENSOR_LOG_LEVEL); #define EXPLORIR_M_BEGIN_CHAR ' ' #define EXPLORIR_M_SET_FILTER_CHAR 'A' #define EXPLORIR_M_GET_FILTER_CHAR 'a' #define EXPLORIR_M_MODE_CHAR 'K' #define EXPLORIR_M_ZERO_POINT_KNOWN_CHAR 'X' #define EXPLORIR_M_CO2_FILTERED_CHAR 'Z' #define EXPLORIR_M_SCALING_CHAR '.' #define EXPLORIR_M_NOT_RECOGNISED_CHAR '?' #define EXPLORIR_M_SEPARATOR_CHAR ' ' #define EXPLORIR_M_PRE_END_CHAR '\r' #define EXPLORIR_M_END_CHAR '\n' #define EXPLORIR_M_TYPE_INDEX 1 #define EXPLORIR_M_VALUE_INDEX 3 #define EXPLORIR_M_BUFFER_LENGTH 16 #define EXPLORIR_M_MAX_RESPONSE_DELAY 300 /* Add margin to the specified 100 in datasheet */ #define EXPLORIR_M_CO2_VALID_DELAY 1200 struct explorir_m_data { struct k_mutex uart_mutex; struct k_sem uart_rx_sem; uint16_t filtered; uint16_t scaling; uint8_t read_index; uint8_t read_buffer[EXPLORIR_M_BUFFER_LENGTH]; }; struct explorir_m_cfg { const struct device *uart_dev; uart_irq_callback_user_data_t cb; }; enum explorir_m_uart_set_usage { EXPLORIR_M_SET_NONE, EXPLORIR_M_SET_VAL_ONE, EXPLORIR_M_SET_VAL_ONE_TWO, }; enum EXPLORIR_M_MODE { EXPLORIR_M_MODE_COMMAND, EXPLORIR_M_MODE_STREAM, EXPLORIR_M_MODE_POLL, }; static void explorir_m_uart_flush(const struct device *uart_dev) { uint8_t tmp; while (uart_fifo_read(uart_dev, &tmp, 1) > 0) { continue; } } static void explorir_m_uart_flush_until_end(const struct device *uart_dev) { uint8_t tmp; uint32_t uptime; uptime = k_uptime_get_32(); while (k_uptime_get_32() - uptime < EXPLORIR_M_MAX_RESPONSE_DELAY) { if (uart_poll_in(uart_dev, &tmp) == 0 && tmp == EXPLORIR_M_END_CHAR) { break; } } } static void explorir_m_buffer_reset(struct explorir_m_data *data) { memset(data->read_buffer, 0, data->read_index); data->read_index = 0; } static int explorir_m_buffer_verify(const struct explorir_m_data *data, char type) { char buffer_type = data->read_buffer[EXPLORIR_M_TYPE_INDEX]; if (data->read_buffer[0] == EXPLORIR_M_NOT_RECOGNISED_CHAR) { LOG_WRN("Sensor did not recognise the command"); return -EIO; } if (buffer_type != type) { LOG_WRN("Expected type %c but got %c", type, buffer_type); return -EIO; } if (data->read_buffer[0] != EXPLORIR_M_BEGIN_CHAR || data->read_buffer[2] != EXPLORIR_M_SEPARATOR_CHAR || data->read_buffer[data->read_index - 2] != EXPLORIR_M_PRE_END_CHAR) { LOG_HEXDUMP_WRN(data->read_buffer, data->read_index, "Invalid buffer"); return -EIO; } return 0; } static int explorir_m_buffer_process(struct explorir_m_data *data, char type, struct sensor_value *val) { if (explorir_m_buffer_verify(data, type) != 0) { return -EIO; } switch (type) { case EXPLORIR_M_SET_FILTER_CHAR: case EXPLORIR_M_MODE_CHAR: case EXPLORIR_M_ZERO_POINT_KNOWN_CHAR: break; case EXPLORIR_M_CO2_FILTERED_CHAR: data->scaling = strtol(&data->read_buffer[EXPLORIR_M_VALUE_INDEX], NULL, 10); break; case EXPLORIR_M_SCALING_CHAR: data->filtered = strtol(&data->read_buffer[EXPLORIR_M_VALUE_INDEX], NULL, 10); break; case EXPLORIR_M_GET_FILTER_CHAR: val->val1 = strtol(&data->read_buffer[EXPLORIR_M_VALUE_INDEX], NULL, 10); break; default: LOG_ERR("Unknown type %c/0x%02x", type, type); return -EIO; } return 0; } static void explorir_m_uart_isr(const struct device *uart_dev, void *user_data) { const struct device *dev = user_data; struct explorir_m_data *data = dev->data; int rc, read_len; if (!device_is_ready(uart_dev)) { LOG_DBG("UART device is not ready"); return; } if (!uart_irq_update(uart_dev)) { LOG_DBG("Unable to process interrupts"); return; } if (!uart_irq_rx_ready(uart_dev)) { LOG_DBG("No RX data"); return; } read_len = EXPLORIR_M_BUFFER_LENGTH - data->read_index; rc = uart_fifo_read(uart_dev, &data->read_buffer[data->read_index], read_len); if (rc < 0 || rc == read_len) { LOG_ERR("UART read failed: %d", rc < 0 ? rc : -ERANGE); explorir_m_uart_flush(uart_dev); LOG_HEXDUMP_WRN(data->read_buffer, data->read_index, "Discarding"); explorir_m_buffer_reset(data); } else { data->read_index += rc; if (data->read_buffer[data->read_index - 1] != EXPLORIR_M_END_CHAR) { return; } } k_sem_give(&data->uart_rx_sem); } static void explorir_m_uart_terminate(const struct device *uart_dev) { uart_poll_out(uart_dev, EXPLORIR_M_PRE_END_CHAR); uart_poll_out(uart_dev, EXPLORIR_M_END_CHAR); } static int explorir_m_uart_transceive(const struct device *dev, char type, struct sensor_value *val, enum explorir_m_uart_set_usage set) { const struct explorir_m_cfg *cfg = dev->config; struct explorir_m_data *data = dev->data; char buf[EXPLORIR_M_BUFFER_LENGTH]; int rc, len; if (val == NULL && set != EXPLORIR_M_SET_NONE) { LOG_ERR("val is NULL but set is not NONE"); return -EINVAL; } k_mutex_lock(&data->uart_mutex, K_FOREVER); uart_poll_out(cfg->uart_dev, type); if (set == EXPLORIR_M_SET_VAL_ONE) { len = snprintf(buf, EXPLORIR_M_BUFFER_LENGTH, "%c%u", EXPLORIR_M_SEPARATOR_CHAR, val->val1); } else if (set == EXPLORIR_M_SET_VAL_ONE_TWO) { len = snprintf(buf, EXPLORIR_M_BUFFER_LENGTH, "%c%u%c%u", EXPLORIR_M_SEPARATOR_CHAR, val->val1, EXPLORIR_M_SEPARATOR_CHAR, val->val2); } else { len = 0; } if (len == EXPLORIR_M_BUFFER_LENGTH) { LOG_WRN("Set value truncated"); } for (int i = 0; i != len; i++) { uart_poll_out(cfg->uart_dev, buf[i]); } explorir_m_buffer_reset(data); k_sem_reset(&data->uart_rx_sem); explorir_m_uart_terminate(cfg->uart_dev); rc = k_sem_take(&data->uart_rx_sem, K_MSEC(EXPLORIR_M_MAX_RESPONSE_DELAY)); if (rc != 0) { LOG_WRN("%c did not receive a response: %d", type, rc); } if (rc == 0) { rc = explorir_m_buffer_process(data, type, val); } k_mutex_unlock(&data->uart_mutex); return rc; } /* * This calibrate function uses a known gas concentration [ppm] via val->val1 to calibrate. * Calibration should be done when temperature is stabile and gas is fully diffused into the sensor. */ static int explorir_m_calibrate(const struct device *dev, struct sensor_value *val) { struct explorir_m_data *data = dev->data; struct sensor_value original; struct sensor_value tmp; int restore_rc = 0; int rc; /* Prevent sensor interaction while using calibration filter value */ k_mutex_lock(&data->uart_mutex, K_FOREVER); rc = explorir_m_uart_transceive(dev, EXPLORIR_M_GET_FILTER_CHAR, &original, EXPLORIR_M_SET_NONE); if (rc != 0) { goto unlock; } /* * From datasheet section "Zero point setting": * To improve zeroing accuracy, the recommended digital filter setting is 32. */ tmp.val1 = 32; rc = explorir_m_uart_transceive(dev, EXPLORIR_M_SET_FILTER_CHAR, &tmp, EXPLORIR_M_SET_VAL_ONE); if (rc == 0) { tmp.val1 = val->val1 / data->filtered; rc = explorir_m_uart_transceive(dev, EXPLORIR_M_ZERO_POINT_KNOWN_CHAR, &tmp, EXPLORIR_M_SET_VAL_ONE); } restore_rc = explorir_m_uart_transceive(dev, EXPLORIR_M_SET_FILTER_CHAR, &original, EXPLORIR_M_SET_VAL_ONE); if (restore_rc != 0) { LOG_ERR("Could not restore filter value"); } unlock: k_mutex_unlock(&data->uart_mutex); return rc != 0 ? rc : restore_rc; } static int explorir_m_attr_get(const struct device *dev, enum sensor_channel chan, enum sensor_attribute attr, struct sensor_value *val) { if (chan != SENSOR_CHAN_CO2) { return -ENOTSUP; } switch (attr) { case SENSOR_ATTR_EXPLORIR_M_FILTER: return explorir_m_uart_transceive(dev, EXPLORIR_M_GET_FILTER_CHAR, val, EXPLORIR_M_SET_NONE); default: return -ENOTSUP; } } static int explorir_m_attr_set(const struct device *dev, enum sensor_channel chan, enum sensor_attribute attr, const struct sensor_value *val) { if (chan != SENSOR_CHAN_CO2) { return -ENOTSUP; } switch (attr) { case SENSOR_ATTR_CALIBRATION: return explorir_m_calibrate(dev, (struct sensor_value *)val); case SENSOR_ATTR_EXPLORIR_M_FILTER: if (val->val1 < 0 || val->val1 > 255) { return -ERANGE; } return explorir_m_uart_transceive(dev, EXPLORIR_M_SET_FILTER_CHAR, (struct sensor_value *)val, EXPLORIR_M_SET_VAL_ONE); default: return -ENOTSUP; } } static int explorir_m_sample_fetch(const struct device *dev, enum sensor_channel chan) { if (chan != SENSOR_CHAN_CO2 && chan != SENSOR_CHAN_ALL) { return -ENOTSUP; } return explorir_m_uart_transceive(dev, EXPLORIR_M_CO2_FILTERED_CHAR, NULL, EXPLORIR_M_SET_NONE); } static int explorir_m_channel_get(const struct device *dev, enum sensor_channel chan, struct sensor_value *val) { struct explorir_m_data *data = dev->data; if (chan != SENSOR_CHAN_CO2) { return -ENOTSUP; } if (k_uptime_get() < EXPLORIR_M_CO2_VALID_DELAY) { return -EAGAIN; } val->val1 = data->filtered * data->scaling; val->val2 = 0; return 0; } static DEVICE_API(sensor, explorir_m_api_funcs) = { .attr_set = explorir_m_attr_set, .attr_get = explorir_m_attr_get, .sample_fetch = explorir_m_sample_fetch, .channel_get = explorir_m_channel_get, }; static int explorir_m_init(const struct device *dev) { const struct explorir_m_cfg *cfg = dev->config; struct explorir_m_data *data = dev->data; struct sensor_value val; int rc; LOG_DBG("Initializing %s", dev->name); if (!device_is_ready(cfg->uart_dev)) { return -ENODEV; } k_mutex_init(&data->uart_mutex); k_sem_init(&data->uart_rx_sem, 0, 1); uart_irq_rx_disable(cfg->uart_dev); uart_irq_tx_disable(cfg->uart_dev); rc = uart_irq_callback_user_data_set(cfg->uart_dev, cfg->cb, (void *)dev); if (rc != 0) { LOG_ERR("UART IRQ setup failed: %d", rc); return rc; } /* Terminate garbled tx due to GPIO setup or crash during unfinished send */ explorir_m_uart_terminate(cfg->uart_dev); explorir_m_uart_flush_until_end(cfg->uart_dev); uart_irq_rx_enable(cfg->uart_dev); val.val1 = EXPLORIR_M_MODE_POLL; explorir_m_uart_transceive(dev, EXPLORIR_M_MODE_CHAR, &val, EXPLORIR_M_SET_VAL_ONE); explorir_m_uart_transceive(dev, EXPLORIR_M_SCALING_CHAR, NULL, EXPLORIR_M_SET_NONE); return rc; } #define EXPLORIR_M_INIT(n) \ \ static struct explorir_m_data explorir_m_data_##n; \ \ static const struct explorir_m_cfg explorir_m_cfg_##n = { \ .uart_dev = DEVICE_DT_GET(DT_INST_BUS(n)), \ .cb = explorir_m_uart_isr, \ }; \ \ SENSOR_DEVICE_DT_INST_DEFINE(n, explorir_m_init, NULL, &explorir_m_data_##n, \ &explorir_m_cfg_##n, POST_KERNEL, \ CONFIG_SENSOR_INIT_PRIORITY, &explorir_m_api_funcs); DT_INST_FOREACH_STATUS_OKAY(EXPLORIR_M_INIT)