diff --git a/platformio.ini b/platformio.ini index fe9b18ccc..468c64c60 100644 --- a/platformio.ini +++ b/platformio.ini @@ -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 \ No newline at end of file + sensirion/Sensirion I2C SCD4x@1.1.0 \ No newline at end of file diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index 87079cb14..0cbb364a6 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -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] = ®isterValue; - - // 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); diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 9190850d0..1a2ed302e 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -24,7 +24,12 @@ PMSA003ISensor pmsa003iSensor; NullSensor pmsa003iSensor; #endif -#if __has_include() +// Small hack +#ifndef INCLUDE_SEN5X +#define INCLUDE_SEN5X 1 +#endif + +#ifdef INCLUDE_SEN5X #include "Sensor/SEN5XSensor.h" SEN5XSensor sen5xSensor; #else diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp index b65b3e76d..a352b6097 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp @@ -1,14 +1,169 @@ #include "configuration.h" -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "SEN5XSensor.h" #include "TelemetrySensor.h" -#include 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, ¬hing, 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((command & 0xFF00) >> 8); + toSend[i++] = static_cast((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; } diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.h b/src/modules/Telemetry/Sensor/SEN5XSensor.h index f2b8321ee..14168f587 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.h +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.h @@ -1,38 +1,64 @@ #include "configuration.h" -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "TelemetrySensor.h" -#include +#include "Wire.h" +// #include #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();