Mirror of espurna firmware for wireless switches and more
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

225 lines
6.3 KiB

// -----------------------------------------------------------------------------
// SenseAir S8 CO2 Sensor
// Contribution by Yonsm Guo
// -----------------------------------------------------------------------------
#if SENSOR_SUPPORT && SENSEAIR_SUPPORT
#pragma once
#include "BaseSensor.h"
// SenseAir sensor utils. Notice that we only read a single register.
// 0xFE is the address aka "Any sensor"
class SenseAir {
public:
struct ValueResult {
int16_t value { 0 };
uint8_t error { 0 };
bool status { false };
};
static ValueResult readCo2(Stream& stream) {
return sendFrame(stream, buildFrame(0xFE, 0x04, 0x03, 1));
}
private:
using Frame = std::array<uint8_t, 8>;
static uint16_t modRTU_CRC(const uint8_t* begin, const uint8_t* end) {
uint16_t crc = 0xFFFF;
for (auto it = begin; it != end; ++it) {
crc ^= (uint16_t)(*it); // XOR byte into least sig. byte of crc
for (size_t i = 8; i != 0; i--) { // Loop over each bit
if ((crc & 0x0001) != 0) { // If the LSB is set
crc >>= 1; // Shift right and XOR 0xA001
crc ^= 0xA001;
} else { // Else LSB is not set
crc >>= 1; // Just shift right
}
}
}
return crc;
}
static Frame buildFrame(
uint8_t slaveAddress,
uint8_t functionCode,
uint16_t startAddress,
uint16_t numberOfRegisters)
{
Frame out;
out[0] = slaveAddress;
out[1] = functionCode;
out[2] = (uint8_t)(startAddress >> 8);
out[3] = (uint8_t)(startAddress);
out[4] = (uint8_t)(numberOfRegisters >> 8);
out[5] = (uint8_t)(numberOfRegisters);
// Note, this number has low and high bytes swapped, so use it accordingly (or swap bytes)
uint16_t crc = modRTU_CRC(&out[0], &out[6]);
out[6] = (uint8_t)(crc & 0xFF);
out[7] = (uint8_t)((crc >> 8) & 0xFF);
return out;
}
static void discardData(Stream& stream) {
while (stream.available() > 0) {
stream.read();
}
}
// Since we only care about the CO2 value, frame size is known in advance
// Only exceptional things are:
// - read timeout, read values would be 0 (zero-init'ed)
// - error code, reported through the result object
// - unexpected value length, in case address / register numbers change
// - invalid crc due to faulty transmission
static ValueResult sendFrame(Stream& stream, const Frame& frame) {
stream.write(frame.data(), frame.size());
delay(50);
ValueResult out;
uint8_t buffer[7] = {0};
stream.readBytes(buffer, 3);
if (buffer[0] != 0xFE) {
discardData(stream);
return out;
}
if (buffer[1] & 0x80) {
out.error = buffer[2];
discardData(stream);
return out;
}
if (buffer[2] != 2) {
discardData(stream);
return out;
}
stream.readBytes(&buffer[3], 4);
const uint16_t received = (buffer[6] << 8) | buffer[5];
const uint16_t calculated = modRTU_CRC(std::begin(buffer), std::end(buffer) - 2);
if (received != calculated) {
return out;
}
out.value = (buffer[4] << 8) | (buffer[3]);
out.status = true;
return out;
}
};
class SenseAirSensor : public BaseSensor, SenseAir {
public:
void setPort(Stream* port) {
_serial = port;
_dirty = true;
}
// ---------------------------------------------------------------------
// Sensor API
// ---------------------------------------------------------------------
unsigned char id() const override {
return SENSOR_SENSEAIR_ID;
}
unsigned char count() const override {
return 1;
}
// Initialization method, must be idempotent
void begin() override {
if (!_dirty) return;
_startTime = TimeSource::now();
_warmedUp = false;
_ready = true;
_dirty = false;
}
// Descriptive name of the sensor
String description() const override {
return F("SenseAir");
}
// Address of the sensor (it could be the GPIO or I2C address)
String address(unsigned char) const override {
return String(SENSEAIR_PORT, 10);
}
// Type for slot # index
unsigned char type(unsigned char index) const override {
if (index == 0) {
return MAGNITUDE_CO2;
}
return MAGNITUDE_NONE;
}
void pre() override {
static constexpr auto WarmupDuration = espurna::duration::Seconds(20);
if (!_warmedUp && (TimeSource::now() - _startTime < WarmupDuration)) {
_error = SENSOR_ERROR_WARM_UP;
return;
}
_warmedUp = true;
const auto result = readCo2(*_serial);
if (!result.status) {
if (result.error) {
DEBUG_MSG_P(PSTR("[SENSEAIR] Modbus error: 0x%02X\n"), result.error);
}
_error = SENSOR_ERROR_OTHER;
return;
}
const auto co2 = result.value;
if (co2 >= 5000 || co2 < 100)
{
_co2 = _lastCo2;
_error = SENSOR_ERROR_OUT_OF_RANGE;
}
else
{
_co2 = (co2 > _lastCo2 + 2000) ? _lastCo2 : co2;
_lastCo2 = co2;
_error = SENSOR_ERROR_OK;
}
}
// Current value for slot # index
double value(unsigned char index) override {
if (index == 0) {
return _co2;
}
return 0.0;
}
private:
Stream* _serial;
using TimeSource = espurna::time::CoreClock;
TimeSource::time_point _startTime;
bool _warmedUp = false;
int16_t _co2 = 0;
int16_t _lastCo2 = 0;
};
#endif // SENSOR_SUPPORT && SENSEAIR_SUPPORT