WIP Sen5X functions

This commit is contained in:
oscgonfer 2025-07-07 14:34:09 +02:00 committed by Thomas Göttgens
parent 94df0cb636
commit d7bb0f7cdf
5 changed files with 337 additions and 97 deletions

View File

@ -199,6 +199,4 @@ lib_deps =
# renovate: datasource=custom.pio depName=Sensirion Core packageName=sensirion/library/Sensirion Core
sensirion/Sensirion Core@0.7.1
# renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x
sensirion/Sensirion I2C SCD4x@1.1.0
# renovate: datasource=custom.pio depName=Sensirion I2C SEN5X packageName=sensirion/library/Sensirion I2C SEN5X
sensirion/Sensirion I2C SEN5X
sensirion/Sensirion I2C SCD4x@1.1.0

View File

@ -9,10 +9,6 @@
#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL)
#include "meshUtils.h" // vformat
#define SEN50_NAME 48
#define SEN54_NAME 52
#define SEN55_NAME 53
#endif
bool in_array(uint8_t *array, int size, uint8_t lookfor)
@ -114,6 +110,58 @@ uint16_t ScanI2CTwoWire::getRegisterValue(const ScanI2CTwoWire::RegisterLocation
return value;
}
/// for SEN5X detection
bool probeSEN5X(const ScanI2C::DeviceAddress& addr, TwoWire* i2cBus) {
uint8_t cmd[] = { 0xD0, 0x33 }; // Read Serial Number command
uint8_t rxBuf[9] = {0};
i2cBus->beginTransmission(addr.address);
i2cBus->write(cmd, 2);
if (i2cBus->endTransmission() != 0)
return false;
delay(20); // wait for response
if (i2cBus->requestFrom(addr.address, (uint8_t)9) != 9)
return false;
for (int i = 0; i < 9 && i2cBus->available(); ++i)
rxBuf[i] = i2cBus->read();
return true;
}
String readSEN5xProductName(TwoWire* i2cBus, uint8_t address) {
uint8_t cmd[] = { 0xD0, 0x14 };
uint8_t response[48] = {0};
i2cBus->beginTransmission(address);
i2cBus->write(cmd, 2);
if (i2cBus->endTransmission() != 0) return "";
delay(20);
if (i2cBus->requestFrom(address, (uint8_t)48) != 48) return "";
for (int i = 0; i < 48 && i2cBus->available(); ++i) {
response[i] = i2cBus->read();
}
char productName[33] = {0};
int j = 0;
for (int i = 0; i < 48 && j < 32; i += 3) {
if (response[i] >= 32 && response[i] <= 126)
productName[j++] = response[i];
else
break;
if (response[i + 1] >= 32 && response[i + 1] <= 126)
productName[j++] = response[i + 1];
else
break;
}
return String(productName);
}
#define SCAN_SIMPLE_CASE(ADDR, T, ...) \
case ADDR: \
logFoundDevice(__VA_ARGS__); \
@ -131,6 +179,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
DeviceAddress addr(port, 0x00);
uint16_t registerValue = 0x00;
String prod = "";
ScanI2C::DeviceType type;
TwoWire *i2cBus;
#ifdef RV3028_RTC
@ -473,40 +522,25 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
case ICM20948_ADDR_ALT: // same as MPU6050_ADDR
// ICM20948 Register check
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1);
prod = readSEN5xProductName(i2cBus, addr.address);
if (registerValue == 0xEA) {
type = ICM20948;
logFoundDevice("ICM20948", (uint8_t)addr.address);
break;
} else {
// TODO refurbish to find the model
// Just a hack for the hackathon
if (addr.address == SEN5X_ADDR) {
if (prod.startsWith("SEN55")) {
type = SEN5X;
logFoundDevice("SEN5X", (uint8_t)addr.address);
logFoundDevice("Sensirion SEN55", addr.address);
break;
} else if (prod.startsWith("SEN54")) {
type = SEN5X;
logFoundDevice("Sensirion SEN54", addr.address);
break;
} else if (prod.startsWith("SEN50")) {
type = SEN5X;
logFoundDevice("Sensirion SEN50", addr.address);
break;
}
// We can get the 0xD014 register to find the model. This is not a simple task
// There is a buffer returned - getRegisterValue is not enough (maybe)
// registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xD014), 6);
// Important to leave delay
// delay(50);
// const uint8_t nameSize = 48;
// uint8_t name[nameSize] = &registerValue;
// switch(name[4]){
// case SEN50_NAME:
// type = SEN50;
// break;
// case SEN54_NAME:
// type = SEN54;
// break;
// case SEN55_NAME:
// type = SEN55;
// break;
// }
if (addr.address == BMX160_ADDR) {
type = BMX160;
logFoundDevice("BMX160", (uint8_t)addr.address);

View File

@ -24,7 +24,12 @@ PMSA003ISensor pmsa003iSensor;
NullSensor pmsa003iSensor;
#endif
#if __has_include(<SensirionI2CSen5x.h>)
// Small hack
#ifndef INCLUDE_SEN5X
#define INCLUDE_SEN5X 1
#endif
#ifdef INCLUDE_SEN5X
#include "Sensor/SEN5XSensor.h"
SEN5XSensor sen5xSensor;
#else

View File

@ -1,14 +1,169 @@
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<SensirionI2CSen5x.h>)
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "SEN5XSensor.h"
#include "TelemetrySensor.h"
#include <SensirionI2CSen5x.h>
SEN5XSensor::SEN5XSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SEN5X, "SEN5X") {}
bool SEN5XSensor::getVersion()
{
if (!sendCommand(SEN5X_GET_FIRMWARE_VERSION)){
LOG_ERROR("SEN5X: Error sending version command");
return false;
}
delay(20); // From Sensirion Arduino library
uint8_t versionBuffer[12];
size_t charNumber = readBuffer(&versionBuffer[0], 3);
if (charNumber == 0) {
LOG_ERROR("SEN5X: Error getting data ready flag value");
return false;
}
firmwareVer = versionBuffer[0] + (versionBuffer[1] / 10);
hardwareVer = versionBuffer[3] + (versionBuffer[4] / 10);
protocolVer = versionBuffer[5] + (versionBuffer[6] / 10);
LOG_INFO("SEN5X Firmware Version: %d", firmwareVer);
LOG_INFO("SEN5X Hardware Version: %d", hardwareVer);
LOG_INFO("SEN5X Protocol Version: %d", protocolVer);
return true;
}
bool SEN5XSensor::findModel()
{
if (!sendCommand(SEN5X_GET_PRODUCT_NAME)) {
LOG_ERROR("SEN5X: Error asking for product name");
return false;
}
delay(50); // From Sensirion Arduino library
const uint8_t nameSize = 48;
uint8_t name[nameSize];
size_t charNumber = readBuffer(&name[0], nameSize);
if (charNumber == 0) {
LOG_ERROR("SEN5X: Error getting device name");
return false;
}
// We only check the last character that defines the model SEN5X
switch(name[4])
{
case 48:
model = SEN50;
LOG_INFO("SEN5X: found sensor model SEN50");
break;
case 52:
model = SEN54;
LOG_INFO("SEN5X: found sensor model SEN54");
break;
case 53:
model = SEN55;
LOG_INFO("SEN5X: found sensor model SEN55");
break;
}
return true;
}
bool SEN5XSensor::sendCommand(uint16_t command)
{
uint8_t nothing;
return sendCommand(command, &nothing, 0);
}
bool SEN5XSensor::sendCommand(uint16_t command, uint8_t* buffer, uint8_t byteNumber)
{
// At least we need two bytes for the command
uint8_t bufferSize = 2;
// Add space for CRC bytes (one every two bytes)
if (byteNumber > 0) bufferSize += byteNumber + (byteNumber / 2);
uint8_t toSend[bufferSize];
uint8_t i = 0;
toSend[i++] = static_cast<uint8_t>((command & 0xFF00) >> 8);
toSend[i++] = static_cast<uint8_t>((command & 0x00FF) >> 0);
// Prepare buffer with CRC every third byte
uint8_t bi = 0;
if (byteNumber > 0) {
while (bi < byteNumber) {
toSend[i++] = buffer[bi++];
toSend[i++] = buffer[bi++];
uint8_t calcCRC = CRC(&buffer[bi - 2]);
toSend[i++] = calcCRC;
}
}
// Transmit the data
bus->beginTransmission(address);
size_t writtenBytes = bus->write(toSend, bufferSize);
uint8_t i2c_error = bus->endTransmission();
if (writtenBytes != bufferSize) {
LOG_ERROR("SEN5X: Error writting on I2C bus");
return false;
}
if (i2c_error != 0) {
LOG_ERROR("SEN5X: Error on I2c communication: %x", i2c_error);
return false;
}
return true;
}
uint8_t SEN5XSensor::readBuffer(uint8_t* buffer, uint8_t byteNumber)
{
size_t readedBytes = bus->requestFrom(address, byteNumber);
if (readedBytes != byteNumber) {
LOG_ERROR("SEN5X: Error reading I2C bus");
return 0;
}
uint8_t i = 0;
uint8_t receivedBytes = 0;
while (readedBytes > 0) {
buffer[i++] = bus->read(); // Just as a reminder: i++ returns i and after that increments.
buffer[i++] = bus->read();
uint8_t recvCRC = bus->read();
uint8_t calcCRC = CRC(&buffer[i - 2]);
if (recvCRC != calcCRC) {
LOG_ERROR("SEN5X: Checksum error while receiving msg");
return 0;
}
readedBytes -=3;
receivedBytes += 2;
}
return receivedBytes;
}
uint8_t SEN5XSensor::CRC(uint8_t* buffer)
{
// This code is based on Sensirion's own implementation https://github.com/Sensirion/arduino-core/blob/41fd02cacf307ec4945955c58ae495e56809b96c/src/SensirionCrc.cpp
uint8_t crc = 0xff;
for (uint8_t i=0; i<2; i++){
crc ^= buffer[i];
for (uint8_t bit=8; bit>0; bit--) {
if (crc & 0x80)
crc = (crc << 1) ^ 0x31;
else
crc = (crc << 1);
}
}
return crc;
}
int32_t SEN5XSensor::runOnce()
{
LOG_INFO("Init sensor: %s", sensorName);
@ -16,30 +171,54 @@ int32_t SEN5XSensor::runOnce()
return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
}
sen5x.begin(*nodeTelemetrySensorsMap[sensorType].second);
bus = nodeTelemetrySensorsMap[sensorType].second;
// sen5x.begin(*bus);
delay(25); // without this there is an error on the deviceReset function (NOT WORKING)
delay(50); // without this there is an error on the deviceReset function
uint16_t error;
char errorMessage[256];
error = sen5x.deviceReset();
if (error) {
LOG_INFO("Error trying to execute deviceReset(): ");
errorToString(error, errorMessage, 256);
LOG_INFO(errorMessage);
if (!sendCommand(SEN5X_RESET)) {
LOG_ERROR("SEN5X: Error reseting device");
return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
}
delay(200); // From Sensirion Arduino library
if (!findModel()) {
LOG_ERROR("SEN5X: error finding sensor model");
return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
}
error = sen5x.startMeasurement();
if (error) {
LOG_INFO("Error trying to execute startMeasurement(): ");
errorToString(error, errorMessage, 256);
LOG_INFO(errorMessage);
// Check if firmware version allows The direct switch between Measurement and RHT/Gas-Only Measurement mode
if (!getVersion()) return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
if (firmwareVer < 2) {
LOG_ERROR("SEN5X: error firmware is too old and will not work with this implementation");
return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
} else {
status = 1;
}
// Detection succeeded
state = SEN5X_IDLE;
status = 1;
LOG_INFO("SEN5X Enabled");
// uint16_t error;
// char errorMessage[256];
// error = sen5x.deviceReset();
// if (error) {
// LOG_INFO("Error trying to execute deviceReset(): ");
// errorToString(error, errorMessage, 256);
// LOG_INFO(errorMessage);
// return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
// }
// error = sen5x.startMeasurement();
// if (error) {
// LOG_INFO("Error trying to execute startMeasurement(): ");
// errorToString(error, errorMessage, 256);
// LOG_INFO(errorMessage);
// return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
// } else {
// status = 1;
// }
return initI2CSensor();
}
@ -47,26 +226,24 @@ void SEN5XSensor::setup()
{
#ifdef SEN5X_ENABLE_PIN
pinMode(SEN5X_ENABLE_PIN, OUTPUT);
digitalWrite(SEN5X_ENABLE_PIN, HIGH);
delay(25);
#endif /* SEN5X_ENABLE_PIN */
}
#ifdef SEN5X_ENABLE_PIN
void SEN5XSensor::sleep() {
digitalWrite(SEN5X_ENABLE_PIN, LOW);
state = State::IDLE;
}
// void SEN5XSensor::sleep() {
// digitalWrite(SEN5X_ENABLE_PIN, LOW);
// state = SSEN5XState::SEN5X_OFF;
// }
uint32_t SEN5XSensor::wakeUp() {
digitalWrite(SEN5X_ENABLE_PIN, HIGH);
state = State::ACTIVE;
return SEN5X_WARMUP_MS;
}
// uint32_t SEN5XSensor::wakeUp() {
// digitalWrite(SEN5X_ENABLE_PIN, HIGH);
// state = SEN5XState::SEN5X_IDLE;
// return SEN5X_WARMUP_MS;
// }
#endif /* SEN5X_ENABLE_PIN */
bool SEN5XSensor::isActive() {
return state == State::ACTIVE;
}
bool SEN5XSensor::getMetrics(meshtastic_Telemetry *measurement)
{
uint16_t error;
@ -82,24 +259,24 @@ bool SEN5XSensor::getMetrics(meshtastic_Telemetry *measurement)
float vocIndex;
float noxIndex;
error = sen5x.readMeasuredValues(
massConcentrationPm1p0, massConcentrationPm2p5, massConcentrationPm4p0,
massConcentrationPm10p0, ambientHumidity, ambientTemperature, vocIndex,
noxIndex);
// error = sen5x.readMeasuredValues(
// massConcentrationPm1p0, massConcentrationPm2p5, massConcentrationPm4p0,
// massConcentrationPm10p0, ambientHumidity, ambientTemperature, vocIndex,
// noxIndex);
if (error) {
LOG_INFO("Error trying to execute readMeasuredValues(): ");
errorToString(error, errorMessage, 256);
LOG_INFO(errorMessage);
return false;
}
// if (error) {
// LOG_INFO("Error trying to execute readMeasuredValues(): ");
// errorToString(error, errorMessage, 256);
// LOG_INFO(errorMessage);
// return false;
// }
measurement->variant.air_quality_metrics.has_pm10_standard = true;
measurement->variant.air_quality_metrics.pm10_standard = massConcentrationPm1p0;
measurement->variant.air_quality_metrics.has_pm25_standard = true;
measurement->variant.air_quality_metrics.pm25_standard = massConcentrationPm2p5;
measurement->variant.air_quality_metrics.has_pm100_standard = true;
measurement->variant.air_quality_metrics.pm100_standard = massConcentrationPm10p0;
// measurement->variant.air_quality_metrics.has_pm10_standard = true;
// measurement->variant.air_quality_metrics.pm10_standard = massConcentrationPm1p0;
// measurement->variant.air_quality_metrics.has_pm25_standard = true;
// measurement->variant.air_quality_metrics.pm25_standard = massConcentrationPm2p5;
// measurement->variant.air_quality_metrics.has_pm100_standard = true;
// measurement->variant.air_quality_metrics.pm100_standard = massConcentrationPm10p0;
return true;
}

View File

@ -1,38 +1,64 @@
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<SensirionI2CSen5x.h>)
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "TelemetrySensor.h"
#include <SensirionI2CSen5x.h>
#include "Wire.h"
// #include <SensirionI2CSen5x.h>
#ifndef SEN5X_WARMUP_MS
// from the SEN5X datasheet
#define SEN5X_WARMUP_MS_SMALL 30000
#define SEN5X_WARMUP_MS 30000
#endif
class SEN5XSensor : public TelemetrySensor
{
private:
SensirionI2CSen5x sen5x;
// PM25_AQI_Data pmsa003iData = {0};
TwoWire * bus;
uint8_t address;
bool getVersion();
float firmwareVer = -1;
float hardwareVer = -1;
float protocolVer = -1;
bool findModel();
// Commands
#define SEN5X_RESET 0xD304
#define SEN5X_GET_PRODUCT_NAME 0xD014
#define SEN5X_GET_FIRMWARE_VERSION 0xD100
#define SEN5X_START_MEASUREMENT 0x0021
#define SEN5X_START_MEASUREMENT_RHT_GAS 0x0037
#define SEN5X_STOP_MEASUREMENT 0x0104
#define SEN5X_READ_DATA_READY 0x0202
#define SEN5X_START_FAN_CLEANING 0x5607
#define SEN5X_RW_VOCS_STATE 0x6181
#define SEN5X_READ_VALUES 0x03C4
#define SEN5X_READ_RAW_VALUES 0x03D2
#define SEN5X_READ_PM_VALUES 0x0413
enum SEN5Xmodel { SEN5X_UNKNOWN = 0, SEN50 = 0b001, SEN54 = 0b010, SEN55 = 0b100 };
SEN5Xmodel model = SEN5X_UNKNOWN;
enum SEN5XState { SEN5X_OFF, SEN5X_IDLE, SEN5X_MEASUREMENT, SEN5X_MEASUREMENT_2, SEN5X_CLEANING, SEN5X_NOT_DETECTED };
SEN5XState state = SEN5X_OFF;
bool sendCommand(uint16_t wichCommand);
bool sendCommand(uint16_t wichCommand, uint8_t* buffer, uint8_t byteNumber=0);
uint8_t readBuffer(uint8_t* buffer, uint8_t byteNumber); // Return number of bytes received
uint8_t CRC(uint8_t* buffer);
protected:
virtual void setup() override;
public:
enum State {
IDLE = 0,
ACTIVE = 1,
};
#ifdef SEN5X_ENABLE_PIN
void sleep();
uint32_t wakeUp();
State state = State::IDLE;
#else
State state = State::ACTIVE;
#endif
// #ifdef SEN5X_ENABLE_PIN
// void sleep();
// uint32_t wakeUp();
// #endif
SEN5XSensor();
bool isActive();