Skip to content

Commit

Permalink
Add support for CO2 measurements via Senseair S8
Browse files Browse the repository at this point in the history
  • Loading branch information
hg committed Feb 13, 2021
1 parent 06ba68f commit 9613627
Show file tree
Hide file tree
Showing 8 changed files with 251 additions and 2 deletions.
2 changes: 1 addition & 1 deletion firmware/main/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
idf_component_register(
SRCS app_main.cc time.cc mqtt.cc settings.cc state.cc pms.cc measurement.cc dallas.cc net.cc commands.cc
SRCS app_main.cc time.cc mqtt.cc settings.cc state.cc pms.cc measurement.cc dallas.cc net.cc commands.cc co2.cc crc16.cc
INCLUDE_DIRS "."
EMBED_TXTFILES "ca.pem"
)
Expand Down
5 changes: 5 additions & 0 deletions firmware/main/app_main.cc
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "ca.hh"
#include "co2.hh"
#include "commands.hh"
#include "common.hh"
#include "dallas.hh"
Expand Down Expand Up @@ -69,6 +70,10 @@ extern "C" [[noreturn]] void app_main() {
stat.start(queue);
}

for (co2::Sensor &sensor : co2Sensors) {
sensor.start(queue);
}

appState->wait(AppState::STATE_NET_CONNECTED);

mqtt::Client client{appSettings.mqtt.broker, caPemStart,
Expand Down
123 changes: 123 additions & 0 deletions firmware/main/co2.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
#include "co2.hh"
#include "crc16.hh"
#include "driver/uart.h"
#include "esp_err.h"
#include "measurement.hh"
#include "time.hh"
#include <cstdio>
#include <esp_log.h>
#include <sys/cdefs.h>

static const char *const kTag = "co2";

namespace co2::cmd {

static Command initCmd(Command &&cmd) {
const auto *const bytes = reinterpret_cast<const uint8_t *>(&cmd);
cmd.crc = crc16(bytes, sizeof(cmd) - sizeof(cmd.crc));
return cmd;
}

static const Command kCmdReadCo2Level = initCmd({
.address = kAddressAny,
.functionCode = kReadInputRegisters,
.startingAddress = PP_HTONS(3),
.quantityOfRegisters = PP_HTONS(1),
});

} // namespace co2::cmd

namespace co2 {

[[noreturn]] void Sensor::collectionTask(void *const arg) {
Sensor &sensor{*reinterpret_cast<Sensor *>(arg)};
Measurement ms{.type = MeasurementType::CO2, .sensor = sensor.name};

TickType_t lastWake = xTaskGetTickCount();

while (true) {
const auto written = sensor.writeCommand(cmd::kCmdReadCo2Level);

if (written != sizeof(cmd::kCmdReadCo2Level)) {
ESP_LOGE(kTag, "could not send command (written %d bytes)", written);
vTaskDelay(secToTicks(5));
sensor.flushInput();
sensor.flushOutput(secToTicks(1));
continue;
}

const std::optional<Co2Level> co2 = sensor.readCo2();

if (co2.has_value()) {
ms.time = getTimestamp();
ms.co2 = co2.value();
sensor.queue->put(ms, portMAX_DELAY);

ESP_LOGI(kTag, "CO2 concentration: %d PPM", ms.co2);
} else {
ESP_LOGE(kTag, "could not receive CO2 data from sensor");
}

vTaskDelayUntil(&lastWake, secToTicks(4));
}
}

int Sensor::writeCommand(const cmd::Command &cmd) {
return uart_write_bytes(port, &cmd, sizeof(cmd));
}

esp_err_t Sensor::flushInput() { return uart_flush_input(port); }

esp_err_t Sensor::flushOutput(const TickType_t wait) {
return uart_wait_tx_done(port, wait);
}

void Sensor::start(Queue<Measurement> &msQueue) {
queue = &msQueue;

constexpr uart_config_t conf{.baud_rate = 9600,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.source_clk = UART_SCLK_APB};

constexpr size_t rxBuf = sizeof(cmd::Co2Response) * 24;
static_assert(rxBuf >= UART_FIFO_LEN);

ESP_ERROR_CHECK(uart_driver_install(port, rxBuf, 0, 0, nullptr, 0));
ESP_ERROR_CHECK(uart_param_config(port, &conf));
ESP_ERROR_CHECK(
uart_set_pin(port, txPin, rxPin, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));

char buf[32];
snprintf(buf, sizeof(buf), "co2_%s", name);

xTaskCreate(collectionTask, buf, KiB(2), this, 4, nullptr);
}

[[nodiscard]] std::optional<Co2Level> Sensor::readCo2() {
cmd::Co2Response response{};

const auto read =
uart_read_bytes(port, &response, sizeof(response), portMAX_DELAY);

if (read != sizeof(cmd::Co2Response)) {
ESP_LOGE(kTag, "invalid response length %d bytes", read);
return std::nullopt;
}

const uint16_t crc = crc16(reinterpret_cast<const uint8_t *>(&response),
sizeof(response) - sizeof(response.crc));

if (crc != response.crc) {
ESP_LOGE(kTag, "invalid CRC 0x%x (expected 0x%x)", crc, response.crc);
return std::nullopt;
}

const auto level = static_cast<Co2Level>(PP_NTOHS(response.co2));

return std::make_optional(level);
}

} // namespace co2
58 changes: 58 additions & 0 deletions firmware/main/co2.hh
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#pragma once

#include "measurement.hh"
#include "queue.hh"
#include <optional>

namespace co2 {

using Co2Level = uint16_t;

namespace cmd {

using Address = uint8_t;
using FunctionCode = uint8_t;

static constexpr Address kAddressAny = 0xfe;
static constexpr FunctionCode kReadInputRegisters = 0x04;

struct Command {
Address address;
FunctionCode functionCode;
uint16_t startingAddress;
uint16_t quantityOfRegisters;
uint16_t crc;
} __attribute__((packed));

struct Co2Response {
Address address;
FunctionCode functionCode;
uint8_t len;
uint16_t co2;
uint16_t crc;
} __attribute__((packed));

static_assert(sizeof(Co2Response) == 7);

} // namespace cmd

struct Sensor {
const char *name;
const uart_port_t port;
const gpio_num_t rxPin;
const gpio_num_t txPin;
Queue<Measurement> *queue;

void start(Queue<Measurement> &msQueue);

private:
[[noreturn]] static void collectionTask(void *arg);

[[nodiscard]] std::optional<Co2Level> readCo2();

int writeCommand(const cmd::Command &cmd);
esp_err_t flushInput();
esp_err_t flushOutput(TickType_t wait);
};

} // namespace co2
49 changes: 49 additions & 0 deletions firmware/main/crc16.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#include "crc16.hh"
#include <cassert>

static constexpr uint16_t kCrcTable[] = {
0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241, 0xc601,
0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440, 0xcc01, 0x0cc0,
0x0d80, 0xcd41, 0x0f00, 0xcfc1, 0xce81, 0x0e40, 0x0a00, 0xcac1, 0xcb81,
0x0b40, 0xc901, 0x09c0, 0x0880, 0xc841, 0xd801, 0x18c0, 0x1980, 0xd941,
0x1b00, 0xdbc1, 0xda81, 0x1a40, 0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01,
0x1dc0, 0x1c80, 0xdc41, 0x1400, 0xd4c1, 0xd581, 0x1540, 0xd701, 0x17c0,
0x1680, 0xd641, 0xd201, 0x12c0, 0x1380, 0xd341, 0x1100, 0xd1c1, 0xd081,
0x1040, 0xf001, 0x30c0, 0x3180, 0xf141, 0x3300, 0xf3c1, 0xf281, 0x3240,
0x3600, 0xf6c1, 0xf781, 0x3740, 0xf501, 0x35c0, 0x3480, 0xf441, 0x3c00,
0xfcc1, 0xfd81, 0x3d40, 0xff01, 0x3fc0, 0x3e80, 0xfe41, 0xfa01, 0x3ac0,
0x3b80, 0xfb41, 0x3900, 0xf9c1, 0xf881, 0x3840, 0x2800, 0xe8c1, 0xe981,
0x2940, 0xeb01, 0x2bc0, 0x2a80, 0xea41, 0xee01, 0x2ec0, 0x2f80, 0xef41,
0x2d00, 0xedc1, 0xec81, 0x2c40, 0xe401, 0x24c0, 0x2580, 0xe541, 0x2700,
0xe7c1, 0xe681, 0x2640, 0x2200, 0xe2c1, 0xe381, 0x2340, 0xe101, 0x21c0,
0x2080, 0xe041, 0xa001, 0x60c0, 0x6180, 0xa141, 0x6300, 0xa3c1, 0xa281,
0x6240, 0x6600, 0xa6c1, 0xa781, 0x6740, 0xa501, 0x65c0, 0x6480, 0xa441,
0x6c00, 0xacc1, 0xad81, 0x6d40, 0xaf01, 0x6fc0, 0x6e80, 0xae41, 0xaa01,
0x6ac0, 0x6b80, 0xab41, 0x6900, 0xa9c1, 0xa881, 0x6840, 0x7800, 0xb8c1,
0xb981, 0x7940, 0xbb01, 0x7bc0, 0x7a80, 0xba41, 0xbe01, 0x7ec0, 0x7f80,
0xbf41, 0x7d00, 0xbdc1, 0xbc81, 0x7c40, 0xb401, 0x74c0, 0x7580, 0xb541,
0x7700, 0xb7c1, 0xb681, 0x7640, 0x7200, 0xb2c1, 0xb381, 0x7340, 0xb101,
0x71c0, 0x7080, 0xb041, 0x5000, 0x90c1, 0x9181, 0x5140, 0x9301, 0x53c0,
0x5280, 0x9241, 0x9601, 0x56c0, 0x5780, 0x9741, 0x5500, 0x95c1, 0x9481,
0x5440, 0x9c01, 0x5cc0, 0x5d80, 0x9d41, 0x5f00, 0x9fc1, 0x9e81, 0x5e40,
0x5a00, 0x9ac1, 0x9b81, 0x5b40, 0x9901, 0x59c0, 0x5880, 0x9841, 0x8801,
0x48c0, 0x4980, 0x8941, 0x4b00, 0x8bc1, 0x8a81, 0x4a40, 0x4e00, 0x8ec1,
0x8f81, 0x4f40, 0x8d01, 0x4dc0, 0x4c80, 0x8c41, 0x4400, 0x84c1, 0x8581,
0x4540, 0x8701, 0x47c0, 0x4680, 0x8641, 0x8201, 0x42c0, 0x4380, 0x8341,
0x4100, 0x81c1, 0x8081, 0x4040};

uint16_t crc16(const uint8_t *data, size_t len) {
assert(data);
assert(len > 0);

uint8_t temp = 0;
uint16_t crc = 0xffff;

while (len--) {
temp = *data++ ^ crc;
crc >>= 8;
crc ^= kCrcTable[temp];
}

return crc;
}
4 changes: 4 additions & 0 deletions firmware/main/crc16.hh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#include <cstddef>
#include <cstdint>

extern uint16_t crc16(const uint8_t *data, size_t len);
9 changes: 9 additions & 0 deletions firmware/main/measurement.cc
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ const char *Measurement::getType() const {
case MeasurementType::PARTICULATES:
return "meas/part";

case MeasurementType::CO2:
return "meas/co2";

default:
configASSERT(false);
return nullptr;
Expand All @@ -44,6 +47,12 @@ bool Measurement::formatMsg(char *const buf, const size_t size) const {
return true;
}

case MeasurementType::CO2: {
constexpr auto tpl = R"({"dev":"%s","time":%ld,"sens":"%s","co2":%d})";
snprintf(buf, size, tpl, appSettings.devName.c_str(), time, sensor, co2);
return true;
}

default:
ESP_LOGE(logTag, "invalid message type %d", static_cast<int>(type));
return false;
Expand Down
3 changes: 2 additions & 1 deletion firmware/main/measurement.hh
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
#include "utils.hh"
#include <ctime>

enum class MeasurementType { TEMPERATURE, PARTICULATES };
enum class MeasurementType { TEMPERATURE, PARTICULATES, CO2 };

struct Measurement {
MeasurementType type;
time_t time;
const char *sensor;
union {
float temp;
uint16_t co2;
Pm<uint16_t> pm;
};

Expand Down

0 comments on commit 9613627

Please sign in to comment.