diff options
-rw-r--r-- | Makefile | 4 | ||||
-rw-r--r-- | include/driver/sen66.h | 102 | ||||
-rw-r--r-- | src/app/datalogger/main.cc | 62 | ||||
-rw-r--r-- | src/driver/Kconfig | 4 | ||||
-rw-r--r-- | src/driver/sen66.cc | 113 |
5 files changed, 285 insertions, 0 deletions
@@ -122,6 +122,10 @@ ifdef CONFIG_driver_sen5x CXX_TARGETS += src/driver/sen5x.cc endif +ifdef CONFIG_driver_sen66 + CXX_TARGETS += src/driver/sen66.cc +endif + ifdef CONFIG_driver_veml6075 CXX_TARGETS += src/driver/veml6075.cc endif diff --git a/include/driver/sen66.h b/include/driver/sen66.h new file mode 100644 index 0000000..117c32b --- /dev/null +++ b/include/driver/sen66.h @@ -0,0 +1,102 @@ +/* + * Copyright 2025 Birte Kristina Friesel + * + * SPDX-License-Identifier: BSD-2-Clause + */ +#ifndef SEN66_H +#define SEN66_H + +class SEN66 { + private: + SEN66(const SEN66 ©); + unsigned char const address = 0x6b; + unsigned char txbuf[2]; + unsigned char rxbuf[27]; + + unsigned char crcWord(unsigned char byte1, unsigned char byte2); + bool crcValid(unsigned char* data, unsigned char length); + + public: + SEN66() {} + + unsigned short const PM_INVALID = 0xffff; + signed short const TEMPERATURE_INVALID = 0x7fff; + signed short const HUMIDITY_INVALID = 0x7fff; + signed short const VOC_INVALID = 0x7fff; + signed short const NOX_INVALID = 0x7fff; + signed short const CO2_INVALID = 0xffff; + + struct { + unsigned int fan_speed_warning : 1; + unsigned int gas_sensor_error : 1; + unsigned int rht_sensor_error : 1; + unsigned int co2_sensor_error : 1; + unsigned int pm_sensor_error : 1; + unsigned int fan_error : 1; + }; + + /* + * PM1.0 value, scaled by 10. + * PM1.0 [µg/m³] = pm10 / 10 + */ + unsigned short pm1; + + /* + * PM2.5 value, scaled by 10. + * PM2.5 [µg/m3] = pm2_5 / 10 + */ + unsigned short pm2_5; + + /* + * PM4.0 value, scaled by 10. + * PM4.0 [µg/m3] = pm4 / 10 + */ + unsigned short pm4; + + /* + * PM10 value, scaled by 10. + * PM10 [µg/m3] = pm10 / 10 + */ + unsigned short pm10; + + /* + * Temperature, scaled by 200. + * Temperature [°c] = temperature / 200 + */ + signed short temperature; + + /* + * Relative Humidity, scaled by 100. + * Relative Humidity [%] = humidity / 100 + */ + signed short humidity; + + /* + * VOC Index, scaled by 10. + * VOC index = voc / 10 + */ + signed short voc; + + /* + * NOx Index, scaled by 10. + * NOx index = nox / 10 + */ + signed short nox; + + /* + * CO₂ concentration [ppm]. + */ + unsigned short co2; + + void start(); + void stop(); + + void cleanFan(); + + bool read(); + bool readStatus(); +}; + +extern SEN66 sen66; + +#endif diff --git a/src/app/datalogger/main.cc b/src/app/datalogger/main.cc index de0a782..d2cfdba 100644 --- a/src/app/datalogger/main.cc +++ b/src/app/datalogger/main.cc @@ -60,6 +60,9 @@ #ifdef CONFIG_driver_sen5x #include "driver/sen5x.h" #endif +#ifdef CONFIG_driver_sen66 +#include "driver/sen66.h" +#endif #ifdef CONFIG_driver_veml6075 #include "driver/veml6075.h" #endif @@ -266,6 +269,61 @@ void loop(void) } #endif +#ifdef CONFIG_driver_sen66 + if (sen66.read()) { + kout << dec; + if (sen66.co2 != sen66.CO2_INVALID) { + kout << "CO₂ : " << sen66.co2 << " ppm" << endl; + } + if (sen66.pm1 != sen66.PM_INVALID) { + kout << "PM1.0: " << (sen66.pm1 / 10) << "." << (sen66.pm1 % 10) << " µg/m³" << endl; + } + if (sen66.pm2_5 != sen66.PM_INVALID) { + kout << "PM2.5: " << (sen66.pm2_5 / 10) << "." << (sen66.pm2_5 % 10) << " µg/m³" << endl; + } + if (sen66.pm4 != sen66.PM_INVALID) { + kout << "PM4.0: " << (sen66.pm4 / 10) << "." << (sen66.pm4 % 10) << " µg/m³" << endl; + } + if (sen66.pm10 != sen66.PM_INVALID) { + kout << "PM10 : " << (sen66.pm10 / 10) << "." << (sen66.pm10 % 10) << " µg/m³" << endl; + } + if (sen66.humidity != sen66.HUMIDITY_INVALID) { + kout << "Humidity: " << (sen66.humidity / 100) << "." << ((sen66.humidity % 100) / 10) << " %" << endl; + } + if (sen66.temperature != sen66.TEMPERATURE_INVALID) { + kout << "Temperature: " << (sen66.temperature / 200) << "." << ((sen66.temperature % 200) / 20) << " °c" << endl; + } + if (sen66.voc != sen66.VOC_INVALID) { + kout << "VOC index: " << (sen66.voc / 10) << "." << (sen66.voc % 10) << endl; + } + if (sen66.nox != sen66.NOX_INVALID) { + kout << "NOx index: " << (sen66.nox / 10) << "." << (sen66.nox % 10) << endl; + } + } else { + kout << "SEN66 error" << endl; + } + if (sen66.readStatus()) { + if (sen66.fan_speed_warning) { + kout << "SEN66 warning: fan speed out of range" << endl; + } + if (sen66.co2_sensor_error) { + kout << "SEN66 error: CO₂ sensor" << endl; + } + if (sen66.gas_sensor_error) { + kout << "SEN66 error: Gas (VOC, NOx) sensor" << endl; + } + if (sen66.rht_sensor_error) { + kout << "SEN66 error: Temperature and Humidity sensor" << endl; + } + if (sen66.pm_sensor_error) { + kout << "SEN66 error: PM sensor" << endl; + } + if (sen66.fan_error) { + kout << "SEN66 error: Fan" << endl; + } + } +#endif + #ifdef CONFIG_driver_veml6075 float uva, uvb; if (veml6075.readUV(&uva, &uvb)) { @@ -392,6 +450,10 @@ int main(void) sen5x.start(); #endif +#ifdef CONFIG_driver_sen66 + sen66.start(); +#endif + #ifdef CONFIG_driver_veml6075 veml6075.init(); #endif diff --git a/src/driver/Kconfig b/src/driver/Kconfig index 4639698..86b1a5d 100644 --- a/src/driver/Kconfig +++ b/src/driver/Kconfig @@ -145,6 +145,10 @@ config driver_sen5x bool "Sensirion SEN5x PM Sensor" depends on meta_driver_i2c +config driver_sen66 +bool "Sensirion SEN66 PM+CO2 Sensor" +depends on meta_driver_i2c + config driver_veml6075 bool "VEML6075 UV Sensor" depends on meta_driver_i2c diff --git a/src/driver/sen66.cc b/src/driver/sen66.cc new file mode 100644 index 0000000..6b12e67 --- /dev/null +++ b/src/driver/sen66.cc @@ -0,0 +1,113 @@ +/* + * Copyright 2025 Birte Kristina Friesel + * + * SPDX-License-Identifier: BSD-2-Clause + */ +#include "arch.h" +#include "driver/sen66.h" +#if defined(CONFIG_meta_driver_hardware_i2c) +#include "driver/i2c.h" +#elif defined(CONFIG_driver_softi2c) +#include "driver/soft_i2c.h" +#endif + +void SEN66::cleanFan() +{ + txbuf[0] = 0x56; + txbuf[1] = 0x07; + i2c.xmit(address, 2, txbuf, 0, rxbuf); +} + +void SEN66::start() +{ + txbuf[0] = 0x00; + txbuf[1] = 0x21; + i2c.xmit(address, 2, txbuf, 0, rxbuf); +} + +void SEN66::stop() +{ + txbuf[0] = 0x01; + txbuf[1] = 0x04; + i2c.xmit(address, 2, txbuf, 0, rxbuf); +} + +bool SEN66::read() +{ + txbuf[0] = 0x03; + txbuf[1] = 0x00; + + if (i2c.xmit(address, 2, txbuf, 0, rxbuf)) { + return false; + } + arch.delay_ms(20); + if (i2c.xmit(address, 0, txbuf, 27, rxbuf)) { + return false; + } + + if (!crcValid(rxbuf, 27)) { + return false; + } + + pm1 = (rxbuf[0] << 8) + rxbuf[1]; + pm2_5 = (rxbuf[3] << 8) + rxbuf[4]; + pm4 = (rxbuf[6] << 8) + rxbuf[7]; + pm10 = (rxbuf[9] << 8) + rxbuf[10]; + humidity = (rxbuf[12] << 8) + rxbuf[13]; + temperature = (rxbuf[15] << 8) + rxbuf[16]; + voc = (rxbuf[18] << 8) + rxbuf[19]; + nox = (rxbuf[21] << 8) + rxbuf[22]; + co2 = (rxbuf[24] << 8) + rxbuf[25]; + return true; +} + +bool SEN66::readStatus() +{ + txbuf[0] = 0xd2; + txbuf[1] = 0x06; + if (i2c.xmit(address, 2, txbuf, 0, rxbuf)) { + return false; + } + arch.delay_ms(20); + if (i2c.xmit(address, 0, txbuf, 6, rxbuf)) { + return false; + } + + if (!crcValid(rxbuf, 6)) { + return false; + } + + fan_speed_warning = rxbuf[1] & 0x20; + co2_sensor_error = rxbuf[3] & 0x12; + pm_sensor_error = rxbuf[3] & 0x08; + gas_sensor_error = rxbuf[4] & 0x80; + rht_sensor_error = rxbuf[4] & 0x40; + fan_error = rxbuf[4] & 0x10; + + return true; +} + +unsigned char SEN66::crcWord(unsigned char byte1, unsigned char byte2) +{ + unsigned char crc = 0xff ^ byte1; + for (unsigned char bit = 8; bit > 0; bit--) { + crc = (crc << 1) ^ (crc & 0x80 ? 0x31 : 0); + } + crc ^= byte2; + for (unsigned char bit = 8; bit > 0; bit--) { + crc = (crc << 1) ^ (crc & 0x80 ? 0x31 : 0); + } + return crc; +} + +bool SEN66::crcValid(unsigned char* data, unsigned char length) +{ + for (unsigned char i = 0; i < length; i += 3) { + if (crcWord(data[i], data[i+1]) != data[i+2]) { + return false; + } + } + return true; +} + +SEN66 sen66; |