#include "configuration.h" #if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "SCD30Sensor.h" #define SCD30_NO_ERROR 0 SCD30Sensor::SCD30Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SCD30, "SCD30") {} bool SCD30Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { LOG_INFO("%s: Init sensor", sensorName); _bus = bus; _address = dev->address.address; #ifdef SCD30_I2C_CLOCK_SPEED _port = dev->address.port; reClockI2C.setup(_bus, _port); LOG_INFO("%s: attempting to reclock speed to %uHz", sensorName, SCD30_I2C_CLOCK_SPEED); reClockI2C.setClock(SCD30_I2C_CLOCK_SPEED); #endif /* SCD30_I2C_CLOCK_SPEED */ scd30.begin(*_bus, _address); if (!startMeasurement()) { LOG_ERROR("%s: Failed to start periodic measurement", sensorName); #ifdef SCD30_I2C_CLOCK_SPEED LOG_INFO("%s: restoring clock speed", sensorName); reClockI2C.restoreClock(); #endif /* SCD30_I2C_CLOCK_SPEED */ return false; } if (!getASC(ascActive)) { LOG_WARN("%s: Could not determine ASC state", sensorName); } #ifdef SCD30_I2C_CLOCK_SPEED LOG_INFO("%s: restoring clock speed", sensorName); reClockI2C.restoreClock(); #endif /* SCD30_I2C_CLOCK_SPEED */ if (state == SCD30_MEASUREMENT) { status = 1; } else { status = 0; } initI2CSensor(); return true; } bool SCD30Sensor::getMetrics(meshtastic_Telemetry *measurement) { float co2, temperature, humidity; #ifdef SCD30_I2C_CLOCK_SPEED LOG_INFO("%s: attempting to reclock speed to %uHz", sensorName, SCD30_I2C_CLOCK_SPEED); reClockI2C.setClock(SCD30_I2C_CLOCK_SPEED); #endif /* SCD30_I2C_CLOCK_SPEED */ if (scd30.readMeasurementData(co2, temperature, humidity) != SCD30_NO_ERROR) { LOG_ERROR("%s: Failed to read measurement data", sensorName); #ifdef SCD30_I2C_CLOCK_SPEED LOG_INFO("%s: restoring clock speed", sensorName); reClockI2C.restoreClock(); #endif /* SCD30_I2C_CLOCK_SPEED */ return false; } #ifdef SCD30_I2C_CLOCK_SPEED LOG_INFO("%s: restoring clock speed", sensorName); reClockI2C.restoreClock(); #endif /* SCD30_I2C_CLOCK_SPEED */ if (co2 == 0) { LOG_ERROR("%s: Invalid CO₂ reading", sensorName); return false; } measurement->variant.air_quality_metrics.has_co2 = true; measurement->variant.air_quality_metrics.has_co2_temperature = true; measurement->variant.air_quality_metrics.has_co2_humidity = true; measurement->variant.air_quality_metrics.co2 = (uint32_t)co2; measurement->variant.air_quality_metrics.co2_temperature = temperature; measurement->variant.air_quality_metrics.co2_humidity = humidity; LOG_DEBUG("%s: Got readings: co2=%u, co2_temp=%.2f, co2_hum=%.2f", sensorName, (uint32_t)co2, temperature, humidity); return true; } bool SCD30Sensor::setMeasurementInterval(uint16_t measInterval) { uint16_t error; LOG_INFO("%s: setting measurement interval at %us", sensorName, measInterval); error = scd30.setMeasurementInterval(measInterval); if (error != SCD30_NO_ERROR) { LOG_ERROR("%s: Unable to set measurement interval. Error code: %u", sensorName, error); return false; } // Restart measuring so we don't need to wait the current interval to finish // (useful when you come from very long intervals) scd30.stopPeriodicMeasurement(); scd30.startPeriodicMeasurement(0); getMeasurementInterval(measurementInterval); return true; } bool SCD30Sensor::getMeasurementInterval(uint16_t &measInterval) { uint16_t error; LOG_INFO("%s: getting measurement interval", sensorName); error = scd30.getMeasurementInterval(measInterval); if (error != SCD30_NO_ERROR) { LOG_ERROR("%s: Unable to get measurement interval. Error code: %u", sensorName, error); return false; } LOG_INFO("%s: measurement interval is %us", sensorName, measInterval); return true; } /** * @brief Start measurement mode * @note This function should not change the clock */ bool SCD30Sensor::startMeasurement() { uint16_t error; if (state == SCD30_MEASUREMENT) { LOG_DEBUG("%s: Already in measurement mode", sensorName); return true; } error = scd30.startPeriodicMeasurement(0); if (error == SCD30_NO_ERROR) { LOG_INFO("%s: Started measurement mode", sensorName); state = SCD30_MEASUREMENT; return true; } else { LOG_ERROR("%s: Couldn't start measurement mode", sensorName); return false; } } /** * @brief Stop measurement mode * @note This function should not change the clock */ bool SCD30Sensor::stopMeasurement() { uint16_t error; error = scd30.stopPeriodicMeasurement(); if (error != SCD30_NO_ERROR) { LOG_ERROR("%s: Unable to stop measurement", sensorName); return false; } state = SCD30_IDLE; return true; } bool SCD30Sensor::performFRC(uint16_t targetCO2) { uint16_t error; LOG_INFO("%s: Issuing FRC. Ensure device has been working at least 3 minutes in stable target environment", sensorName); LOG_INFO("%s: Target CO2: %u ppm", sensorName, targetCO2); error = scd30.forceRecalibration((uint16_t)targetCO2); if (error != SCD30_NO_ERROR) { LOG_ERROR("%s: Unable to perform forced recalibration.", sensorName); return false; } LOG_INFO("%s: FRC Correction successful.", sensorName); return true; } bool SCD30Sensor::setASC(bool ascEnabled) { uint16_t error; LOG_INFO("%s: %s ASC", sensorName, ascEnabled ? "Enabling" : "Disabling"); error = scd30.activateAutoCalibration((uint16_t)ascEnabled); if (error != SCD30_NO_ERROR) { LOG_ERROR("%s: Unable to send command.", sensorName); return false; } if (!getASC(ascActive)) { LOG_ERROR("%s: Unable to check if ASC is enabled", sensorName); return false; } return true; } bool SCD30Sensor::getASC(uint16_t &_ascActive) { uint16_t error; // LOG_INFO("%s: Getting ASC", sensorName); error = scd30.getAutoCalibrationStatus(_ascActive); if (error != SCD30_NO_ERROR) { LOG_ERROR("%s: Unable to send command.", sensorName); return false; } LOG_INFO("%s: ASC is %s", sensorName, _ascActive ? "enabled" : "disabled"); return true; } /** * @brief Set the temperature reference. Unit ℃. * * The on-board RH/T sensor is influenced by thermal self-heating of SCD30 * and other electrical components. Design-in alters the thermal properties * of SCD30 such that temperature and humidity offsets may occur when * operating the sensor in end-customer devices. Compensation of those * effects is achievable by writing the temperature offset found in * continuous operation of the device into the sensor. Temperature offset * value is saved in non-volatile memory. The last set value will be used * for temperature offset compensation after repowering. * * @param[in] tempReference * @note this function is certainly confusing and it's not recommended */ bool SCD30Sensor::setTemperature(float tempReference) { uint16_t error; uint16_t updatedTempOffset; float tempOffset; uint16_t _tempOffset; float co2; float temperature; float humidity; if (tempReference == 100) { // Requesting the value of 100 will restore the temperature offset LOG_INFO("%s: Setting reference temperature at 0degC", sensorName); _tempOffset = 0; } else { LOG_INFO("%s: Setting reference temperature at: %.2f", sensorName, tempReference); error = scd30.readMeasurementData(co2, temperature, humidity); if (error != SCD30_NO_ERROR) { LOG_ERROR("%s: Unable to read current temperature. Error code: %u", sensorName, error); return false; } LOG_INFO("%s: Current sensor temperature: %.2f", sensorName, temperature); tempOffset = (temperature - tempReference); if (tempOffset < 0) { LOG_ERROR("%s: temperature offset is only positive", sensorName); return false; } tempOffset *= 100; _tempOffset = static_cast(tempOffset); } LOG_INFO("%s: Setting temperature offset: %u (*100)", sensorName, _tempOffset); error = scd30.setTemperatureOffset(_tempOffset); if (error != SCD30_NO_ERROR) { LOG_ERROR("%s: Unable to set temperature offset. Error code: %u", sensorName, error); return false; } scd30.getTemperatureOffset(updatedTempOffset); LOG_INFO("%s: Updated sensor temperature offset: %u (*100)", sensorName, updatedTempOffset); return true; } bool SCD30Sensor::setAltitude(uint16_t altitude) { uint16_t error; LOG_INFO("%s: setting altitude at %um", sensorName, altitude); error = scd30.setAltitudeCompensation(altitude); if (error != SCD30_NO_ERROR) { LOG_ERROR("%s: Unable to set altitude. Error code: %u", sensorName, error); return false; } uint16_t newAltitude; getAltitude(newAltitude); return true; } bool SCD30Sensor::getAltitude(uint16_t &altitude) { uint16_t error; // LOG_INFO("%s: Getting altitude", sensorName); error = scd30.getAltitudeCompensation(altitude); if (error != SCD30_NO_ERROR) { LOG_ERROR("%s: Unable to get altitude. Error code: %u", sensorName, error); return false; } LOG_INFO("%s: Sensor altitude: %u", sensorName, altitude); return true; } bool SCD30Sensor::softReset() { uint16_t error; LOG_INFO("%s: Requesting soft reset", sensorName); error = scd30.softReset(); if (error != SCD30_NO_ERROR) { LOG_ERROR("%s: Unable to do soft reset. Error code: %u", sensorName, error); return false; } LOG_INFO("%s: soft reset successful", sensorName); return true; } /** * @brief Check if sensor is in measurement mode */ bool SCD30Sensor::isActive() { return state == SCD30_MEASUREMENT; } /** * @brief Start measurement mode * @note Not used in admin comands, getMetrics or init, can change clock. */ uint32_t SCD30Sensor::wakeUp() { #ifdef SCD30_I2C_CLOCK_SPEED LOG_INFO("%s: attempting to reclock speed to %uHz", sensorName, SCD30_I2C_CLOCK_SPEED); reClockI2C.setClock(SCD30_I2C_CLOCK_SPEED); #endif /* SCD30_I2C_CLOCK_SPEED */ startMeasurement(); #ifdef SCD30_I2C_CLOCK_SPEED LOG_INFO("%s: restoring clock speed", sensorName); reClockI2C.restoreClock(); #endif /* SCD30_I2C_CLOCK_SPEED */ return 0; } /** * @brief Stop measurement mode * @note Not used in admin comands, getMetrics or init, can change clock. */ void SCD30Sensor::sleep() { #ifdef SCD30_I2C_CLOCK_SPEED LOG_INFO("%s: attempting to reclock speed to %uHz", sensorName, SCD30_I2C_CLOCK_SPEED); reClockI2C.setClock(SCD30_I2C_CLOCK_SPEED); #endif /* SCD30_I2C_CLOCK_SPEED */ stopMeasurement(); #ifdef SCD30_I2C_CLOCK_SPEED LOG_INFO("%s: restoring clock speed", sensorName); reClockI2C.restoreClock(); #endif /* SCD30_I2C_CLOCK_SPEED */ } bool SCD30Sensor::canSleep() { return false; } int32_t SCD30Sensor::wakeUpTimeMs() { return 0; } int32_t SCD30Sensor::pendingForReadyMs() { return 0; } AdminMessageHandleResult SCD30Sensor::handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, meshtastic_AdminMessage *response) { AdminMessageHandleResult result; #ifdef SCD30_I2C_CLOCK_SPEED LOG_INFO("%s: attempting to reclock speed to %uHz", sensorName, SCD30_I2C_CLOCK_SPEED); reClockI2C.setClock(SCD30_I2C_CLOCK_SPEED); #endif /* SCD30_I2C_CLOCK_SPEED */ switch (request->which_payload_variant) { case meshtastic_AdminMessage_sensor_config_tag: // Check for ASC-FRC request first if (!request->sensor_config.has_scd30_config) { result = AdminMessageHandleResult::NOT_HANDLED; break; } if (request->sensor_config.scd30_config.has_soft_reset) { LOG_DEBUG("%s: Requested soft reset", sensorName); this->softReset(); } else { if (request->sensor_config.scd30_config.has_set_asc) { this->setASC(request->sensor_config.scd30_config.set_asc); if (request->sensor_config.scd30_config.set_asc == false) { LOG_DEBUG("%s: Request for FRC", sensorName); if (request->sensor_config.scd30_config.has_set_target_co2_conc) { this->performFRC(request->sensor_config.scd30_config.set_target_co2_conc); } else { // FRC requested but no target CO2 provided LOG_ERROR("%s: target CO2 not provided", sensorName); result = AdminMessageHandleResult::NOT_HANDLED; break; } } } // Check for temperature offset // NOTE: this requires to have a sensor working on stable environment // And to make it between readings if (request->sensor_config.scd30_config.has_set_temperature) { this->setTemperature(request->sensor_config.scd30_config.set_temperature); } // Check for altitude if (request->sensor_config.scd30_config.has_set_altitude) { this->setAltitude(request->sensor_config.scd30_config.set_altitude); } // Check for set measuremen interval if (request->sensor_config.scd30_config.has_set_measurement_interval) { this->setMeasurementInterval(request->sensor_config.scd30_config.set_measurement_interval); } } result = AdminMessageHandleResult::HANDLED; break; default: result = AdminMessageHandleResult::NOT_HANDLED; } #ifdef SCD30_I2C_CLOCK_SPEED LOG_INFO("%s: restoring clock speed", sensorName); reClockI2C.restoreClock(); #endif /* SCD30_I2C_CLOCK_SPEED */ return result; } #endif