summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBirte Kristina Friesel <derf@finalrewind.org>2025-03-30 11:29:21 +0200
committerBirte Kristina Friesel <derf@finalrewind.org>2025-03-30 11:29:21 +0200
commitb956db43523f7e6c45e9ad759b0a4c70813d443d (patch)
treeaf06edd97533e6773028941d029aa8f00610ac1e
parent4fe6e97dfcdc1f747432e62606d7bd8a0bdb79d3 (diff)
Add Sensirion SEN66 driver
-rw-r--r--Makefile4
-rw-r--r--include/driver/sen66.h102
-rw-r--r--src/app/datalogger/main.cc62
-rw-r--r--src/driver/Kconfig4
-rw-r--r--src/driver/sen66.cc113
5 files changed, 285 insertions, 0 deletions
diff --git a/Makefile b/Makefile
index d49d5d5..a5fde65 100644
--- a/Makefile
+++ b/Makefile
@@ -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 &copy);
+ 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;