Unverified Commit b956db43 authored by Birte Kristina Friesel's avatar Birte Kristina Friesel
Browse files

Add Sensirion SEN66 driver

parent 4fe6e97d
Loading
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -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

include/driver/sen66.h

0 → 100644
+102 −0
Original line number Diff line number Diff line
/*
 * 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
+62 −0
Original line number Diff line number Diff line
@@ -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
+4 −0
Original line number Diff line number Diff line
@@ -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

src/driver/sen66.cc

0 → 100644
+113 −0
Original line number Diff line number Diff line
/*
 * 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;