This commit is contained in:
oscgonfer 2025-07-21 14:09:47 +02:00 committed by GitHub
commit 42d6f903be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 1379 additions and 165 deletions

View File

@ -201,4 +201,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
sensirion/Sensirion I2C SCD4x@1.1.0

View File

@ -195,6 +195,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define LTR390UV_ADDR 0x53
#define XPOWERS_AXP192_AXP2101_ADDRESS 0x34 // same adress as TCA8418
#define PCT2075_ADDR 0x37
#define SEN5X_ADDR 0x69
// -----------------------------------------------------------------------------
// ACCELEROMETER

View File

@ -75,6 +75,7 @@ class ScanI2C
TCA8418KB,
PCT2075,
BMM150,
SEN5X
} DeviceType;
// typedef uint8_t DeviceAddress;

View File

@ -8,6 +8,7 @@
#endif
#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL)
#include "meshUtils.h" // vformat
#endif
bool in_array(uint8_t *array, int size, uint8_t lookfor)
@ -109,6 +110,75 @@ uint16_t ScanI2CTwoWire::getRegisterValue(const ScanI2CTwoWire::RegisterLocation
return value;
}
bool ScanI2CTwoWire::setClockSpeed(I2CPort port, uint32_t speed) {
DeviceAddress addr(port, 0x00);
TwoWire *i2cBus;
#if WIRE_INTERFACES_COUNT == 2
if (port == I2CPort::WIRE1) {
i2cBus = &Wire1;
} else {
#endif
i2cBus = &Wire;
#if WIRE_INTERFACES_COUNT == 2
}
#endif
return i2cBus->setClock(speed);
}
uint32_t ScanI2CTwoWire::getClockSpeed(I2CPort port) {
DeviceAddress addr(port, 0x00);
TwoWire *i2cBus;
#if WIRE_INTERFACES_COUNT == 2
if (port == I2CPort::WIRE1) {
i2cBus = &Wire1;
} else {
#endif
i2cBus = &Wire;
#if WIRE_INTERFACES_COUNT == 2
}
#endif
return i2cBus->getClock();
}
/// for SEN5X detection
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__); \
@ -464,21 +534,39 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
}
break;
case ICM20948_ADDR: // same as BMX160_ADDR
case ICM20948_ADDR: // same as BMX160_ADDR and SEN5X_ADDR
case ICM20948_ADDR_ALT: // same as MPU6050_ADDR
// ICM20948 Register check
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1);
if (registerValue == 0xEA) {
type = ICM20948;
logFoundDevice("ICM20948", (uint8_t)addr.address);
break;
} else if (addr.address == BMX160_ADDR) {
type = BMX160;
logFoundDevice("BMX160", (uint8_t)addr.address);
break;
} else {
type = MPU6050;
logFoundDevice("MPU6050", (uint8_t)addr.address);
break;
String prod = "";
prod = readSEN5xProductName(i2cBus, addr.address);
if (prod.startsWith("SEN55")) {
type = SEN5X;
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;
}
if (addr.address == BMX160_ADDR) {
type = BMX160;
logFoundDevice("BMX160", (uint8_t)addr.address);
break;
} else {
type = MPU6050;
logFoundDevice("MPU6050", (uint8_t)addr.address);
break;
}
}
break;

View File

@ -29,6 +29,9 @@ class ScanI2CTwoWire : public ScanI2C
size_t countDevices() const override;
bool setClockSpeed(ScanI2C::I2CPort, uint32_t);
uint32_t getClockSpeed(ScanI2C::I2CPort);
protected:
FoundDevice firstOfOrNONE(size_t, DeviceType[]) const override;

View File

@ -472,6 +472,7 @@ void setup()
Wire.setSCL(I2C_SCL);
Wire.begin();
#elif defined(I2C_SDA) && !defined(ARCH_RP2040)
LOG_INFO("Starting Bus with (SDA) %d and (SCL) %d: ", I2C_SDA, I2C_SCL);
Wire.begin(I2C_SDA, I2C_SCL);
#elif defined(ARCH_PORTDUINO)
if (settingsStrings[i2cdev] != "") {
@ -530,6 +531,26 @@ void setup()
i2cScanner->scanPort(ScanI2C::I2CPort::WIRE);
#endif
#ifdef I2C_CLOCK_SPEED
uint32_t currentClock;
currentClock = i2cScanner->getClockSpeed(ScanI2C::I2CPort::WIRE);
LOG_INFO("Clock speed: %uHz on WIRE", currentClock);
LOG_DEBUG("Setting Wire with defined clock speed, %uHz...", I2C_CLOCK_SPEED);
if(!i2cScanner->setClockSpeed(ScanI2C::I2CPort::WIRE, I2C_CLOCK_SPEED)) {
LOG_ERROR("Unable to set clock speed on WIRE");
} else {
currentClock = i2cScanner->getClockSpeed(ScanI2C::I2CPort::WIRE);
LOG_INFO("Set clock speed: %uHz on WIRE", currentClock);
}
// LOG_DEBUG("Starting Wire with defined clock speed, %d...", I2C_CLOCK_SPEED);
// if(!i2cScanner->setClockSpeed(ScanI2C::I2CPort::WIRE1, I2C_CLOCK_SPEED)) {
// LOG_ERROR("Unable to set clock speed on WIRE1");
// } else {
// LOG_INFO("Set clock speed: %d on WIRE1", I2C_CLOCK_SPEED);
// }
#endif
auto i2cCount = i2cScanner->countDevices();
if (i2cCount == 0) {
LOG_INFO("No I2C devices found");
@ -682,7 +703,7 @@ void setup()
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::RAK12035, meshtastic_TelemetrySensorType_RAK12035);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::PCT2075, meshtastic_TelemetrySensorType_PCT2075);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::SCD4X, meshtastic_TelemetrySensorType_SCD4X);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::SEN5X, meshtastic_TelemetrySensorType_SEN5X);
i2cScanner.reset();
#endif

View File

@ -221,11 +221,7 @@ void setupModules()
// TODO: How to improve this?
#if HAS_SENSOR && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
new EnvironmentTelemetryModule();
#if __has_include("Adafruit_PM25AQI.h")
if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first > 0) {
new AirQualityTelemetryModule();
}
#endif
new AirQualityTelemetryModule();
#if !MESHTASTIC_EXCLUDE_HEALTH_TELEMETRY
if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MAX30102].first > 0 ||
nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MLX90614].first > 0) {

View File

@ -1,36 +1,66 @@
#include "configuration.h"
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include("Adafruit_PM25AQI.h")
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "AirQualityTelemetry.h"
#include "Default.h"
#include "AirQualityTelemetry.h"
#include "MeshService.h"
#include "NodeDB.h"
#include "PowerFSM.h"
#include "RTC.h"
#include "Router.h"
#include "detect/ScanI2CTwoWire.h"
#include "UnitConversions.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/images.h"
#include "main.h"
#include "sleep.h"
#include <Throttle.h>
#ifndef PMSA003I_WARMUP_MS
// from the PMSA003I datasheet:
// "Stable data should be got at least 30 seconds after the sensor wakeup
// from the sleep mode because of the fans performance."
#define PMSA003I_WARMUP_MS 30000
#if __has_include(<Adafruit_PM25AQI.h>)
#include "Sensor/PMSA003ISensor.h"
PMSA003ISensor pmsa003iSensor;
#else
NullSensor pmsa003iSensor;
#endif
// Small hack
#ifndef INCLUDE_SEN5X
#define INCLUDE_SEN5X 1
#endif
#ifdef INCLUDE_SEN5X
#include "Sensor/SEN5XSensor.h"
SEN5XSensor sen5xSensor;
#else
NullSensor sen5xSensor;
#endif
int32_t AirQualityTelemetryModule::runOnce()
{
if (sleepOnNextExecution == true) {
sleepOnNextExecution = false;
uint32_t nightyNightMs = Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.air_quality_interval,
default_telemetry_broadcast_interval_secs);
LOG_DEBUG("Sleeping for %ims, then awaking to send metrics again.", nightyNightMs);
doDeepSleep(nightyNightMs, true, false);
}
uint32_t result = UINT32_MAX;
uint32_t sen5xPendingForReady;
/*
Uncomment the preferences below if you want to use the module
without having to configure it from the PythonAPI or WebUI.
*/
// moduleConfig.telemetry.air_quality_enabled = 1;
// TODO there is no config in module_config.proto for air_quality_screen_enabled. Reusing environment one, although it should have its own
// moduleConfig.telemetry.environment_screen_enabled = 1;
// moduleConfig.telemetry.air_quality_interval = 15;
if (!(moduleConfig.telemetry.air_quality_enabled)) {
if (!(moduleConfig.telemetry.air_quality_enabled || moduleConfig.telemetry.environment_screen_enabled ||
AIR_QUALITY_TELEMETRY_MODULE_ENABLE)) {
// If this module is not enabled, and the user doesn't want the display screen don't waste any OSThread time on it
return disable();
}
@ -42,79 +72,172 @@ int32_t AirQualityTelemetryModule::runOnce()
if (moduleConfig.telemetry.air_quality_enabled) {
LOG_INFO("Air quality Telemetry: init");
#ifdef PMSA003I_ENABLE_PIN
// put the sensor to sleep on startup
pinMode(PMSA003I_ENABLE_PIN, OUTPUT);
digitalWrite(PMSA003I_ENABLE_PIN, LOW);
#endif /* PMSA003I_ENABLE_PIN */
if (pmsa003iSensor.hasSensor())
result = pmsa003iSensor.runOnce();
if (!aqi.begin_I2C()) {
#ifndef I2C_NO_RESCAN
LOG_WARN("Could not establish i2c connection to AQI sensor. Rescan");
// rescan for late arriving sensors. AQI Module starts about 10 seconds into the boot so this is plenty.
uint8_t i2caddr_scan[] = {PMSA0031_ADDR};
uint8_t i2caddr_asize = 1;
auto i2cScanner = std::unique_ptr<ScanI2CTwoWire>(new ScanI2CTwoWire());
#if defined(I2C_SDA1)
i2cScanner->scanPort(ScanI2C::I2CPort::WIRE1, i2caddr_scan, i2caddr_asize);
#endif
i2cScanner->scanPort(ScanI2C::I2CPort::WIRE, i2caddr_scan, i2caddr_asize);
auto found = i2cScanner->find(ScanI2C::DeviceType::PMSA0031);
if (found.type != ScanI2C::DeviceType::NONE) {
nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first = found.address.address;
nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].second =
i2cScanner->fetchI2CBus(found.address);
return setStartDelay();
}
#endif
return disable();
}
return setStartDelay();
if (sen5xSensor.hasSensor())
result = sen5xSensor.runOnce();
}
return disable();
// it's possible to have this module enabled, only for displaying values on the screen.
// therefore, we should only enable the sensor loop if measurement is also enabled
return result == UINT32_MAX ? disable() : setStartDelay();
} else {
// if we somehow got to a second run of this module with measurement disabled, then just wait forever
if (!moduleConfig.telemetry.air_quality_enabled)
return disable();
switch (state) {
#ifdef PMSA003I_ENABLE_PIN
case State::IDLE:
// sensor is in standby; fire it up and sleep
LOG_DEBUG("runOnce(): state = idle");
digitalWrite(PMSA003I_ENABLE_PIN, HIGH);
state = State::ACTIVE;
return PMSA003I_WARMUP_MS;
#endif /* PMSA003I_ENABLE_PIN */
case State::ACTIVE:
// sensor is already warmed up; grab telemetry and send it
LOG_DEBUG("runOnce(): state = active");
if (((lastSentToMesh == 0) ||
!Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled(
moduleConfig.telemetry.air_quality_interval,
default_telemetry_broadcast_interval_secs, numOnlineNodes))) &&
airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) &&
airTime->isTxAllowedAirUtil()) {
sendTelemetry();
lastSentToMesh = millis();
} else if (service->isToPhoneQueueEmpty()) {
// Just send to phone when it's not our time to send to mesh yet
// Only send while queue is empty (phone assumed connected)
sendTelemetry(NODENUM_BROADCAST, true);
}
#ifdef PMSA003I_ENABLE_PIN
// put sensor back to sleep
digitalWrite(PMSA003I_ENABLE_PIN, LOW);
state = State::IDLE;
#endif /* PMSA003I_ENABLE_PIN */
return sendToPhoneIntervalMs;
default:
if (!moduleConfig.telemetry.air_quality_enabled && !AIR_QUALITY_TELEMETRY_MODULE_ENABLE) {
return disable();
}
// Wake up the sensors that need it
#ifdef PMSA003I_ENABLE_PIN
if (pmsa003iSensor.hasSensor() && !pmsa003iSensor.isActive())
return pmsa003iSensor.wakeUp();
#endif /* PMSA003I_ENABLE_PIN */
// Wake up the sensors that need it, before we need to take telemetry data
if ((lastSentToMesh == 0) ||
!Throttle::isWithinTimespanMs(lastSentToMesh - SEN5X_WARMUP_MS_1, Default::getConfiguredOrDefaultMsScaled(
moduleConfig.telemetry.air_quality_interval,
default_telemetry_broadcast_interval_secs, numOnlineNodes))) {
if (sen5xSensor.hasSensor() && !sen5xSensor.isActive())
return sen5xSensor.wakeUp();
}
// Check if sen5x is ready to return data, or if it needs more time because of the low concentration threshold
if (sen5xSensor.hasSensor() && sen5xSensor.isActive()) {
sen5xPendingForReady = sen5xSensor.pendingForReady();
LOG_DEBUG("SEN5X: Pending for ready %ums", sen5xPendingForReady);
if (sen5xPendingForReady) {
return sen5xPendingForReady;
}
}
if (((lastSentToMesh == 0) ||
!Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled(
moduleConfig.telemetry.air_quality_interval,
default_telemetry_broadcast_interval_secs, numOnlineNodes))) &&
airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) &&
airTime->isTxAllowedAirUtil()) {
sendTelemetry();
lastSentToMesh = millis();
} else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) &&
(service->isToPhoneQueueEmpty())) {
// Just send to phone when it's not our time to send to mesh yet
// Only send while queue is empty (phone assumed connected)
sendTelemetry(NODENUM_BROADCAST, true);
lastSentToPhone = millis();
}
// TODO - Add logic here to send the sensor to idle ONLY if there is enough time to wake it up before the next reading cycle
#ifdef PMSA003I_ENABLE_PIN
pmsa003iSensor.sleep();
#endif /* PMSA003I_ENABLE_PIN */
if (sen5xSensor.hasSensor() && sen5xSensor.isActive()) {
if (SEN5X_WARMUP_MS_2 < Default::getConfiguredOrDefaultMsScaled(
moduleConfig.telemetry.air_quality_interval,
default_telemetry_broadcast_interval_secs, numOnlineNodes)) {
LOG_DEBUG("SEN5X: Disabling sensor until next period");
sen5xSensor.idle();
} else {
LOG_DEBUG("SEN5X: Sensor stays enabled due to warm up period");
}
}
}
return min(sendToPhoneIntervalMs, result);
}
bool AirQualityTelemetryModule::wantUIFrame()
{
return moduleConfig.telemetry.environment_screen_enabled;
}
void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
// === Setup display ===
display->clear();
display->setFont(FONT_SMALL);
display->setTextAlignment(TEXT_ALIGN_LEFT);
int line = 1;
// === Set Title
const char *titleStr = (graphics::isHighResolution) ? "Environment" : "Env.";
// === Header ===
graphics::drawCommonHeader(display, x, y, titleStr);
// === Row spacing setup ===
const int rowHeight = FONT_HEIGHT_SMALL - 4;
int currentY = graphics::getTextPositions(display)[line++];
// === Show "No Telemetry" if no data available ===
if (!lastMeasurementPacket) {
display->drawString(x, currentY, "No Telemetry");
return;
}
// Decode the telemetry message from the latest received packet
const meshtastic_Data &p = lastMeasurementPacket->decoded;
meshtastic_Telemetry telemetry;
if (!pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &telemetry)) {
display->drawString(x, currentY, "No Telemetry");
return;
}
const auto &m = telemetry.variant.air_quality_metrics;
// Check if any telemetry field has valid data
bool hasAny = m.has_pm10_standard || m.has_pm25_standard || m.has_pm100_standard;
if (!hasAny) {
display->drawString(x, currentY, "No Telemetry");
return;
}
// === First line: Show sender name + time since received (left), and first metric (right) ===
const char *sender = getSenderShortName(*lastMeasurementPacket);
uint32_t agoSecs = service->GetTimeSinceMeshPacket(lastMeasurementPacket);
String agoStr = (agoSecs > 864000) ? "?"
: (agoSecs > 3600) ? String(agoSecs / 3600) + "h"
: (agoSecs > 60) ? String(agoSecs / 60) + "m"
: String(agoSecs) + "s";
String leftStr = String(sender) + " (" + agoStr + ")";
display->drawString(x, currentY, leftStr); // Left side: who and when
// === Collect sensor readings as label strings (no icons) ===
std::vector<String> entries;
if (m.has_pm10_standard)
entries.push_back("PM1.0: " + String(m.pm10_standard) + "ug/m3");
if (m.has_pm25_standard)
entries.push_back("PM2.5: " + String(m.pm25_standard) + "ug/m3");
if (m.has_pm100_standard)
entries.push_back("PM10.0: " + String(m.pm100_standard) + "ug/m3");
// === Show first available metric on top-right of first line ===
if (!entries.empty()) {
String valueStr = entries.front();
int rightX = SCREEN_WIDTH - display->getStringWidth(valueStr);
display->drawString(rightX, currentY, valueStr);
entries.erase(entries.begin()); // Remove from queue
}
// === Advance to next line for remaining telemetry entries ===
currentY += rowHeight;
// === Draw remaining entries in 2-column format (left and right) ===
for (size_t i = 0; i < entries.size(); i += 2) {
// Left column
display->drawString(x, currentY, entries[i]);
// Right column if it exists
if (i + 1 < entries.size()) {
int rightX = SCREEN_WIDTH / 2;
display->drawString(rightX, currentY, entries[i + 1]);
}
currentY += rowHeight;
}
}
@ -128,9 +251,10 @@ bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPack
t->variant.air_quality_metrics.pm10_standard, t->variant.air_quality_metrics.pm25_standard,
t->variant.air_quality_metrics.pm100_standard);
LOG_INFO(" | PM1.0(Environmental)=%i, PM2.5(Environmental)=%i, PM10.0(Environmental)=%i",
t->variant.air_quality_metrics.pm10_environmental, t->variant.air_quality_metrics.pm25_environmental,
t->variant.air_quality_metrics.pm100_environmental);
// TODO - Decide what to do with these
// LOG_INFO(" | PM1.0(Environmental)=%i, PM2.5(Environmental)=%i, PM10.0(Environmental)=%i",
// t->variant.air_quality_metrics.pm10_environmental, t->variant.air_quality_metrics.pm25_environmental,
// t->variant.air_quality_metrics.pm100_environmental);
#endif
// release previous packet before occupying a new spot
if (lastMeasurementPacket != nullptr)
@ -144,35 +268,25 @@ bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPack
bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *m)
{
if (!aqi.read(&data)) {
LOG_WARN("Skip send measurements. Could not read AQIn");
return false;
}
bool valid = true;
bool hasSensor = false;
m->time = getTime();
m->which_variant = meshtastic_Telemetry_air_quality_metrics_tag;
m->variant.air_quality_metrics.has_pm10_standard = true;
m->variant.air_quality_metrics.pm10_standard = data.pm10_standard;
m->variant.air_quality_metrics.has_pm25_standard = true;
m->variant.air_quality_metrics.pm25_standard = data.pm25_standard;
m->variant.air_quality_metrics.has_pm100_standard = true;
m->variant.air_quality_metrics.pm100_standard = data.pm100_standard;
m->variant.air_quality_metrics = meshtastic_AirQualityMetrics_init_zero;
m->variant.air_quality_metrics.has_pm10_environmental = true;
m->variant.air_quality_metrics.pm10_environmental = data.pm10_env;
m->variant.air_quality_metrics.has_pm25_environmental = true;
m->variant.air_quality_metrics.pm25_environmental = data.pm25_env;
m->variant.air_quality_metrics.has_pm100_environmental = true;
m->variant.air_quality_metrics.pm100_environmental = data.pm100_env;
// TODO - This is currently problematic, as it assumes only one sensor connected
// We should implement some logic to avoid not getting data if one sensor disconnects
if (pmsa003iSensor.hasSensor()) {
valid = valid && pmsa003iSensor.getMetrics(m);
hasSensor = true;
}
LOG_INFO("Send: PM1.0(Standard)=%i, PM2.5(Standard)=%i, PM10.0(Standard)=%i", m->variant.air_quality_metrics.pm10_standard,
m->variant.air_quality_metrics.pm25_standard, m->variant.air_quality_metrics.pm100_standard);
if (sen5xSensor.hasSensor()) {
valid = valid && sen5xSensor.getMetrics(m);
hasSensor = true;
}
LOG_INFO(" | PM1.0(Environmental)=%i, PM2.5(Environmental)=%i, PM10.0(Environmental)=%i",
m->variant.air_quality_metrics.pm10_environmental, m->variant.air_quality_metrics.pm25_environmental,
m->variant.air_quality_metrics.pm100_environmental);
return true;
return valid && hasSensor;
}
meshtastic_MeshPacket *AirQualityTelemetryModule::allocReply()
@ -206,7 +320,15 @@ meshtastic_MeshPacket *AirQualityTelemetryModule::allocReply()
bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
{
meshtastic_Telemetry m = meshtastic_Telemetry_init_zero;
m.which_variant = meshtastic_Telemetry_air_quality_metrics_tag;
m.time = getTime();
// TODO - if one sensor fails here, we will stop taking measurements from everything
// Can we do this in a smarter way, for instance checking the nodeTelemetrySensor map and making it dynamic?
if (getAirQualityTelemetry(&m)) {
LOG_INFO("Send: pm10_standard=%.2f, pm25_standard=%.2f, pm100_standard=%.2f",
m.variant.air_quality_metrics.pm10_standard, m.variant.air_quality_metrics.pm25_standard,
m.variant.air_quality_metrics.pm100_standard);
meshtastic_MeshPacket *p = allocDataProtobuf(m);
p->to = dest;
p->decoded.want_response = false;
@ -221,16 +343,51 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
lastMeasurementPacket = packetPool.allocCopy(*p);
if (phoneOnly) {
LOG_INFO("Send packet to phone");
LOG_INFO("Sending packet to phone");
service->sendToPhone(p);
} else {
LOG_INFO("Send packet to mesh");
LOG_INFO("Sending packet to mesh");
service->sendToMesh(p, RX_SRC_LOCAL, true);
if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR && config.power.is_power_saving) {
meshtastic_ClientNotification *notification = clientNotificationPool.allocZeroed();
notification->level = meshtastic_LogRecord_Level_INFO;
notification->time = getValidTime(RTCQualityFromNet);
sprintf(notification->message, "Sending telemetry and sleeping for %us interval in a moment",
Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.air_quality_interval,
default_telemetry_broadcast_interval_secs) /
1000U);
service->sendClientNotification(notification);
sleepOnNextExecution = true;
LOG_DEBUG("Start next execution in 5s, then sleep");
setIntervalFromNow(FIVE_SECONDS_MS);
}
}
return true;
}
return false;
}
AdminMessageHandleResult AirQualityTelemetryModule::handleAdminMessageForModule(const meshtastic_MeshPacket &mp,
meshtastic_AdminMessage *request,
meshtastic_AdminMessage *response)
{
AdminMessageHandleResult result = AdminMessageHandleResult::NOT_HANDLED;
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL
if (pmsa003iSensor.hasSensor()) {
result = pmsa003iSensor.handleAdminMessage(mp, request, response);
if (result != AdminMessageHandleResult::NOT_HANDLED)
return result;
}
if (sen5xSensor.hasSensor()) {
result = sen5xSensor.handleAdminMessage(mp, request, response);
if (result != AdminMessageHandleResult::NOT_HANDLED)
return result;
}
#endif
return result;
}
#endif

View File

@ -1,12 +1,18 @@
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include("Adafruit_PM25AQI.h")
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
#pragma once
#ifndef AIR_QUALITY_TELEMETRY_MODULE_ENABLE
#define AIR_QUALITY_TELEMETRY_MODULE_ENABLE 0
#endif
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "Adafruit_PM25AQI.h"
#include "NodeDB.h"
#include "ProtobufModule.h"
#include <OLEDDisplay.h>
#include <OLEDDisplayUi.h>
class AirQualityTelemetryModule : private concurrency::OSThread, public ProtobufModule<meshtastic_Telemetry>
{
@ -20,18 +26,15 @@ class AirQualityTelemetryModule : private concurrency::OSThread, public Protobuf
ProtobufModule("AirQualityTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg)
{
lastMeasurementPacket = nullptr;
setIntervalFromNow(10 * 1000);
aqi = Adafruit_PM25AQI();
nodeStatusObserver.observe(&nodeStatus->onNewStatus);
#ifdef PMSA003I_ENABLE_PIN
// the PMSA003I sensor uses about 300mW on its own; support powering it off when it's not actively taking
// a reading
state = State::IDLE;
#else
state = State::ACTIVE;
#endif
setIntervalFromNow(10 * 1000);
}
virtual bool wantUIFrame() override;
#if !HAS_SCREEN
void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
#else
virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override;
#endif
protected:
/** Called to handle a particular incoming message
@ -49,19 +52,15 @@ class AirQualityTelemetryModule : private concurrency::OSThread, public Protobuf
*/
bool sendTelemetry(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false);
virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp,
meshtastic_AdminMessage *request,
meshtastic_AdminMessage *response) override;
private:
enum State {
IDLE = 0,
ACTIVE = 1,
};
State state;
Adafruit_PM25AQI aqi;
PM25_AQI_Data data = {0};
bool firstTime = true;
meshtastic_MeshPacket *lastMeasurementPacket;
uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute
uint32_t lastSentToMesh = 0;
uint32_t lastSentToPhone = 0;
};
#endif

View File

@ -743,8 +743,6 @@ bool EnvironmentTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
LOG_INFO("Send: soil_temperature=%f, soil_moisture=%u", m.variant.environment_metrics.soil_temperature,
m.variant.environment_metrics.soil_moisture);
sensor_read_error_count = 0;
meshtastic_MeshPacket *p = allocDataProtobuf(m);
p->to = dest;
p->decoded.want_response = false;

View File

@ -62,7 +62,6 @@ class EnvironmentTelemetryModule : private concurrency::OSThread, public Protobu
uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute
uint32_t lastSentToMesh = 0;
uint32_t lastSentToPhone = 0;
uint32_t sensor_read_error_count = 0;
};
#endif

View File

@ -0,0 +1,97 @@
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<Adafruit_PM25AQI.h>)
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "PMSA003ISensor.h"
#include "TelemetrySensor.h"
#include "detect/ScanI2CTwoWire.h"
#include <Adafruit_PM25AQI.h>
PMSA003ISensor::PMSA003ISensor() : TelemetrySensor(meshtastic_TelemetrySensorType_PMSA003I, "PMSA003I") {}
int32_t PMSA003ISensor::runOnce()
{
LOG_INFO("Init sensor: %s", sensorName);
if (!hasSensor()) {
return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
}
#ifdef PMSA003I_ENABLE_PIN
// TODO not sure why this was like this
sleep();
#endif /* PMSA003I_ENABLE_PIN */
if (!pmsa003i.begin_I2C()){
#ifndef I2C_NO_RESCAN
LOG_WARN("Could not establish i2c connection to AQI sensor. Rescan");
// rescan for late arriving sensors. AQI Module starts about 10 seconds into the boot so this is plenty.
uint8_t i2caddr_scan[] = {PMSA0031_ADDR};
uint8_t i2caddr_asize = 1;
auto i2cScanner = std::unique_ptr<ScanI2CTwoWire>(new ScanI2CTwoWire());
#if defined(I2C_SDA1)
i2cScanner->scanPort(ScanI2C::I2CPort::WIRE1, i2caddr_scan, i2caddr_asize);
#endif
i2cScanner->scanPort(ScanI2C::I2CPort::WIRE, i2caddr_scan, i2caddr_asize);
auto found = i2cScanner->find(ScanI2C::DeviceType::PMSA0031);
if (found.type != ScanI2C::DeviceType::NONE) {
nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first = found.address.address;
nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].second =
i2cScanner->fetchI2CBus(found.address);
return initI2CSensor();
}
#endif
return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
}
return initI2CSensor();
}
void PMSA003ISensor::setup()
{
#ifdef PMSA003I_ENABLE_PIN
pinMode(PMSA003I_ENABLE_PIN, OUTPUT);
#endif /* PMSA003I_ENABLE_PIN */
}
#ifdef PMSA003I_ENABLE_PIN
void PMSA003ISensor::sleep() {
digitalWrite(PMSA003I_ENABLE_PIN, LOW);
state = State::IDLE;
}
uint32_t PMSA003ISensor::wakeUp() {
digitalWrite(PMSA003I_ENABLE_PIN, HIGH);
state = State::ACTIVE;
return PMSA003I_WARMUP_MS;
}
#endif /* PMSA003I_ENABLE_PIN */
bool PMSA003ISensor::isActive() {
return state == State::ACTIVE;
}
bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement)
{
if (!pmsa003i.read(&pmsa003iData)) {
LOG_WARN("Skip send measurements. Could not read AQI");
return false;
}
measurement->variant.air_quality_metrics.has_pm10_standard = true;
measurement->variant.air_quality_metrics.pm10_standard = pmsa003iData.pm10_standard;
measurement->variant.air_quality_metrics.has_pm25_standard = true;
measurement->variant.air_quality_metrics.pm25_standard = pmsa003iData.pm25_standard;
measurement->variant.air_quality_metrics.has_pm100_standard = true;
measurement->variant.air_quality_metrics.pm100_standard = pmsa003iData.pm100_standard;
// measurement->variant.air_quality_metrics.has_pm10_environmental = true;
// measurement->variant.air_quality_metrics.pm10_environmental = pmsa003iData.pm10_env;
// measurement->variant.air_quality_metrics.has_pm25_environmental = true;
// measurement->variant.air_quality_metrics.pm25_environmental = pmsa003iData.pm25_env;
// measurement->variant.air_quality_metrics.has_pm100_environmental = true;
// measurement->variant.air_quality_metrics.pm100_environmental = pmsa003iData.pm100_env;
return true;
}
#endif

View File

@ -0,0 +1,49 @@
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<Adafruit_PM25AQI.h>)
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "TelemetrySensor.h"
#include "detect/ScanI2CTwoWire.h"
#include <Adafruit_PM25AQI.h>
#ifndef PMSA003I_WARMUP_MS
// from the PMSA003I datasheet:
// "Stable data should be got at least 30 seconds after the sensor wakeup
// from the sleep mode because of the fans performance."
#define PMSA003I_WARMUP_MS 30000
#endif
class PMSA003ISensor : public TelemetrySensor
{
private:
Adafruit_PM25AQI pmsa003i = Adafruit_PM25AQI();
PM25_AQI_Data pmsa003iData = {0};
protected:
virtual void setup() override;
public:
enum State {
IDLE = 0,
ACTIVE = 1,
};
#ifdef PMSA003I_ENABLE_PIN
void sleep();
uint32_t wakeUp();
// the PMSA003I sensor uses about 300mW on its own; support powering it off when it's not actively taking
// a reading
// put the sensor to sleep on startup
State state = State::IDLE;
#else
State state = State::ACTIVE;
#endif
PMSA003ISensor();
bool isActive();
virtual int32_t runOnce() override;
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
};
#endif

View File

@ -0,0 +1,671 @@
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "SEN5XSensor.h"
#include "TelemetrySensor.h"
#include "FSCommon.h"
#include "SPILock.h"
SEN5XSensor::SEN5XSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SEN5X, "SEN5X") {}
bool SEN5XSensor::restoreClock(uint32_t currentClock){
#ifdef SEN5X_I2C_CLOCK_SPEED
if (currentClock != SEN5X_I2C_CLOCK_SPEED){
// LOG_DEBUG("Restoring I2C clock to %uHz", currentClock);
return bus->setClock(currentClock);
}
return true;
#endif
}
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: %0.2f", firmwareVer);
LOG_INFO("SEN5X Hardware Version: %0.2f", hardwareVer);
LOG_INFO("SEN5X Protocol Version: %0.2f", 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 = sen5xCRC(&buffer[bi - 2]);
toSend[i++] = calcCRC;
}
}
#ifdef SEN5X_I2C_CLOCK_SPEED
uint32_t currentClock;
currentClock = bus->getClock();
if (currentClock != SEN5X_I2C_CLOCK_SPEED){
// LOG_DEBUG("Changing I2C clock to %u", SEN5X_I2C_CLOCK_SPEED);
bus->setClock(SEN5X_I2C_CLOCK_SPEED);
}
#endif
// Transmit the data
// LOG_INFO("Beginning connection to SEN5X: 0x%x", address);
bus->beginTransmission(address);
size_t writtenBytes = bus->write(toSend, bufferSize);
uint8_t i2c_error = bus->endTransmission();
restoreClock(currentClock);
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)
{
#ifdef SEN5X_I2C_CLOCK_SPEED
uint32_t currentClock;
currentClock = bus->getClock();
if (currentClock != SEN5X_I2C_CLOCK_SPEED){
// LOG_DEBUG("Changing I2C clock to %u", SEN5X_I2C_CLOCK_SPEED);
bus->setClock(SEN5X_I2C_CLOCK_SPEED);
}
#endif
size_t readBytes = bus->requestFrom(address, byteNumber);
if (readBytes != byteNumber) {
LOG_ERROR("SEN5X: Error reading I2C bus");
return 0;
}
uint8_t i = 0;
uint8_t receivedBytes = 0;
while (readBytes > 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 = sen5xCRC(&buffer[i - 2]);
if (recvCRC != calcCRC) {
LOG_ERROR("SEN5X: Checksum error while receiving msg");
return 0;
}
readBytes -=3;
receivedBytes += 2;
}
restoreClock(currentClock);
return receivedBytes;
}
uint8_t SEN5XSensor::sen5xCRC(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;
}
bool SEN5XSensor::I2Cdetect(TwoWire *_Wire, uint8_t address)
{
_Wire->beginTransmission(address);
byte error = _Wire->endTransmission();
if (error == 0) return true;
else return false;
}
bool SEN5XSensor::idle()
{
// In continous mode we don't sleep
if (continousMode || forcedContinousMode) {
LOG_ERROR("SEN5X: Not going to idle mode, we are in continous mode!!");
return false;
}
// TODO - Get VOC state before going to idle mode
// vocStateFromSensor();
if (!sendCommand(SEN5X_STOP_MEASUREMENT)) {
LOG_ERROR("SEN5X: Error stoping measurement");
return false;
}
// delay(200); // From Sensirion Arduino library
LOG_INFO("SEN5X: Stop measurement mode");
state = SEN5X_IDLE;
measureStarted = 0;
return true;
}
void SEN5XSensor::loadCleaningState()
{
#ifdef FSCom
spiLock->lock();
auto file = FSCom.open(sen5XCleaningFileName, FILE_O_READ);
if (file) {
file.read();
file.close();
LOG_INFO("SEN5X: Cleaning state %u read for %s read from %s", lastCleaning, sensorName, sen5XCleaningFileName);
} else {
LOG_INFO("SEN5X: No %s state found (File: %s)", sensorName, sen5XCleaningFileName);
}
spiLock->unlock();
#else
LOG_ERROR("SEN5X: ERROR - Filesystem not implemented");
#endif
}
void SEN5XSensor::updateCleaningState()
{
#ifdef FSCom
spiLock->lock();
if (FSCom.exists(sen5XCleaningFileName) && !FSCom.remove(sen5XCleaningFileName)) {
LOG_WARN("SEN5X: Can't remove old state file");
}
auto file = FSCom.open(sen5XCleaningFileName, FILE_O_WRITE);
if (file) {
LOG_INFO("SEN5X: Save cleaning state %u for %s to %s", lastCleaning, sensorName, sen5XCleaningFileName);
file.write(lastCleaning);
file.flush();
file.close();
} else {
LOG_INFO("SEN5X: Can't write %s state (File: %s)", sensorName, sen5XCleaningFileName);
}
spiLock->unlock();
#else
LOG_ERROR("SEN5X: ERROR: Filesystem not implemented");
#endif
}
bool SEN5XSensor::isActive(){
return state == SEN5X_MEASUREMENT || state == SEN5X_MEASUREMENT_2;
}
uint32_t SEN5XSensor::wakeUp(){
// LOG_INFO("SEN5X: Attempting to wakeUp sensor");
if (!sendCommand(SEN5X_START_MEASUREMENT)) {
LOG_INFO("SEN5X: Error starting measurement");
return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
}
// delay(50); // From Sensirion Arduino library
// LOG_INFO("SEN5X: Setting measurement mode");
uint32_t now;
now = getTime();
measureStarted = now;
state = SEN5X_MEASUREMENT;
if (state == SEN5X_MEASUREMENT)
LOG_INFO("SEN5X: Started measurement mode");
return SEN5X_WARMUP_MS_1;
}
bool SEN5XSensor::startCleaning()
{
state = SEN5X_CLEANING;
// Note that this command can only be run when the sensor is in measurement mode
if (!sendCommand(SEN5X_START_MEASUREMENT)) {
LOG_ERROR("SEN5X: Error starting measurment mode");
return false;
}
delay(50); // From Sensirion Arduino library
if (!sendCommand(SEN5X_START_FAN_CLEANING)) {
LOG_ERROR("SEN5X: Error starting fan cleaning");
return false;
}
// delay(20); // From Sensirion Arduino library
// This message will be always printed so the user knows the device it's not hung
LOG_INFO("SEN5X: Started fan cleaning it will take 10 seconds...");
uint16_t started = millis();
while (millis() - started < 10500) {
// Serial.print(".");
delay(500);
}
LOG_INFO("SEN5X: Cleaning done!!");
// Save timestamp in flash so we know when a week has passed
uint32_t now;
now = getTime();
lastCleaning = now;
updateCleaningState();
idle();
return true;
}
int32_t SEN5XSensor::runOnce()
{
state = SEN5X_NOT_DETECTED;
LOG_INFO("Init sensor: %s", sensorName);
if (!hasSensor()) {
return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
}
bus = nodeTelemetrySensorsMap[sensorType].second;
address = (uint8_t)nodeTelemetrySensorsMap[sensorType].first;
delay(50); // without this there is an error on the deviceReset function
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;
}
// 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;
}
delay(200); // From Sensirion Arduino library
// Detection succeeded
state = SEN5X_IDLE;
status = 1;
LOG_INFO("SEN5X Enabled");
// Check if it is time to do a cleaning
// TODO - this is not currently working as intended - always reading 0 from the file. We should probably make a unified approach for both the cleaning and the VOCstate
loadCleaningState();
LOG_INFO("SEN5X: Last cleaning time: %u", lastCleaning);
if (lastCleaning) {
LOG_INFO("SEN5X: Last cleaning is valid");
uint32_t now;
now = getTime();
LOG_INFO("SEN5X: Current time %us", now);
uint32_t passed = now - lastCleaning;
LOG_INFO("SEN5X: Elapsed time since last cleaning: %us", passed);
if (passed > ONE_WEEK_IN_SECONDS && (now > 1514764800)) { // If current date greater than 01/01/2018 (validity check)
LOG_INFO("SEN5X: More than a week since las cleaning, cleaning...");
startCleaning();
} else {
LOG_INFO("SEN5X: Last cleaning date (in epoch): %u", lastCleaning);
}
} else {
LOG_INFO("SEN5X: Last cleaning is not valid");
// We asume the device has just been updated or it is new, so no need to trigger a cleaning.
// Just save the timestamp to do a cleaning one week from now.
lastCleaning = getTime();
updateCleaningState();
LOG_INFO("SEN5X: No valid last cleaning date found, saving it now: %u", lastCleaning);
}
return initI2CSensor();
}
void SEN5XSensor::setup()
{
}
bool SEN5XSensor::readValues()
{
if (!sendCommand(SEN5X_READ_VALUES)){
LOG_ERROR("SEN5X: Error sending read command");
return false;
}
LOG_DEBUG("SEN5X: Reading PM Values");
delay(20); // From Sensirion Arduino library
uint8_t dataBuffer[24];
size_t receivedNumber = readBuffer(&dataBuffer[0], 24);
if (receivedNumber == 0) {
LOG_ERROR("SEN5X: Error getting values");
return false;
}
// First get the integers
uint16_t uint_pM1p0 = static_cast<uint16_t>((dataBuffer[0] << 8) | dataBuffer[1]);
uint16_t uint_pM2p5 = static_cast<uint16_t>((dataBuffer[2] << 8) | dataBuffer[3]);
uint16_t uint_pM4p0 = static_cast<uint16_t>((dataBuffer[4] << 8) | dataBuffer[5]);
uint16_t uint_pM10p0 = static_cast<uint16_t>((dataBuffer[6] << 8) | dataBuffer[7]);
int16_t int_humidity = static_cast<int16_t>((dataBuffer[8] << 8) | dataBuffer[9]);
int16_t int_temperature = static_cast<int16_t>((dataBuffer[10] << 8) | dataBuffer[11]);
int16_t int_vocIndex = static_cast<int16_t>((dataBuffer[12] << 8) | dataBuffer[13]);
int16_t int_noxIndex = static_cast<int16_t>((dataBuffer[14] << 8) | dataBuffer[15]);
// TODO we should check if values are NAN before converting them
// convert them based on Sensirion Arduino lib
// TODO - Change based on the type of final values
sen5xmeasurement.pM1p0 = uint_pM1p0 / 10.0f;
sen5xmeasurement.pM2p5 = uint_pM2p5 / 10.0f;
sen5xmeasurement.pM4p0 = uint_pM4p0 / 10.0f;
sen5xmeasurement.pM10p0 = uint_pM10p0 / 10.0f;
sen5xmeasurement.humidity = int_humidity / 100.0f;
sen5xmeasurement.temperature = int_temperature / 200.0f;
sen5xmeasurement.vocIndex = int_vocIndex / 10.0f;
sen5xmeasurement.noxIndex = int_noxIndex / 10.0f;
// TODO - change depending on the final values
LOG_DEBUG("Got: pM1p0=%.2f, pM2p5=%.2f, pM4p0=%.2f, pM10p0=%.2f",
sen5xmeasurement.pM1p0, sen5xmeasurement.pM2p5,
sen5xmeasurement.pM4p0, sen5xmeasurement.pM10p0);
return true;
}
bool SEN5XSensor::readPnValues()
{
if (!sendCommand(SEN5X_READ_PM_VALUES)){
LOG_ERROR("SEN5X: Error sending read command");
return false;
}
LOG_DEBUG("SEN5X: Reading PN Values");
delay(20); // From Sensirion Arduino library
uint8_t dataBuffer[30];
size_t receivedNumber = readBuffer(&dataBuffer[0], 30);
if (receivedNumber == 0) {
LOG_ERROR("SEN5X: Error getting PN values");
return false;
}
// First get the integers
// uint16_t uint_pM1p0 = static_cast<uint16_t>((dataBuffer[0] << 8) | dataBuffer[1]);
// uint16_t uint_pM2p5 = static_cast<uint16_t>((dataBuffer[2] << 8) | dataBuffer[3]);
// uint16_t uint_pM4p0 = static_cast<uint16_t>((dataBuffer[4] << 8) | dataBuffer[5]);
// uint16_t uint_pM10p0 = static_cast<uint16_t>((dataBuffer[6] << 8) | dataBuffer[7]);
uint16_t uint_pN0p5 = static_cast<uint16_t>((dataBuffer[8] << 8) | dataBuffer[9]);
uint16_t uint_pN1p0 = static_cast<uint16_t>((dataBuffer[10] << 8) | dataBuffer[11]);
uint16_t uint_pN2p5 = static_cast<uint16_t>((dataBuffer[12] << 8) | dataBuffer[13]);
uint16_t uint_pN4p0 = static_cast<uint16_t>((dataBuffer[14] << 8) | dataBuffer[15]);
uint16_t uint_pN10p0 = static_cast<uint16_t>((dataBuffer[16] << 8) | dataBuffer[17]);
uint16_t uint_tSize = static_cast<uint16_t>((dataBuffer[18] << 8) | dataBuffer[19]);
// Convert them based on Sensirion Arduino lib
sen5xmeasurement.pN0p5 = uint_pN0p5 / 10;
sen5xmeasurement.pN1p0 = uint_pN1p0 / 10;
sen5xmeasurement.pN2p5 = uint_pN2p5 / 10;
sen5xmeasurement.pN4p0 = uint_pN4p0 / 10;
sen5xmeasurement.pN10p0 = uint_pN10p0 / 10;
sen5xmeasurement.tSize = uint_tSize / 1000.0f;
// Convert PN readings from #/cm3 to #/0.1l
// TODO Remove accumuluative values:
// https://github.com/fablabbcn/smartcitizen-kit-2x/issues/85
sen5xmeasurement.pN0p5 *= 100;
sen5xmeasurement.pN1p0 *= 100;
sen5xmeasurement.pN2p5 *= 100;
sen5xmeasurement.pN4p0 *= 100;
sen5xmeasurement.pN10p0 *= 100;
sen5xmeasurement.tSize *= 100;
// TODO - Change depending on the final values
LOG_DEBUG("Got: pN0p5=%u, pN1p0=%u, pN2p5=%u, pN4p0=%u, pN10p0=%u, tSize=%.2f",
sen5xmeasurement.pN0p5, sen5xmeasurement.pN1p0,
sen5xmeasurement.pN2p5, sen5xmeasurement.pN4p0,
sen5xmeasurement.pN10p0, sen5xmeasurement.tSize
);
return true;
}
// TODO - Decide if we want to have this here or not
// bool SEN5XSensor::readRawValues()
// {
// if (!sendCommand(SEN5X_READ_RAW_VALUES)){
// LOG_ERROR("SEN5X: Error sending read command");
// return false;
// }
// delay(20); // From Sensirion Arduino library
// uint8_t dataBuffer[12];
// size_t receivedNumber = readBuffer(&dataBuffer[0], 12);
// if (receivedNumber == 0) {
// LOG_ERROR("SEN5X: Error getting Raw values");
// return false;
// }
// // Get values
// rawHumidity = static_cast<int16_t>((dataBuffer[0] << 8) | dataBuffer[1]);
// rawTemperature = static_cast<int16_t>((dataBuffer[2] << 8) | dataBuffer[3]);
// rawVoc = static_cast<uint16_t>((dataBuffer[4] << 8) | dataBuffer[5]);
// rawNox = static_cast<uint16_t>((dataBuffer[6] << 8) | dataBuffer[7]);
// return true;
// }
uint8_t SEN5XSensor::getMeasurements()
{
// Try to get new data
if (!sendCommand(SEN5X_READ_DATA_READY)){
LOG_ERROR("SEN5X: Error sending command data ready flag");
return 2;
}
delay(20); // From Sensirion Arduino library
uint8_t dataReadyBuffer[3];
size_t charNumber = readBuffer(&dataReadyBuffer[0], 3);
if (charNumber == 0) {
LOG_ERROR("SEN5X: Error getting device version value");
return 2;
}
bool data_ready = dataReadyBuffer[1];
if (!data_ready) {
LOG_INFO("SEN5X: Data is not ready");
return 1;
}
if(!readValues()) {
LOG_ERROR("SEN5X: Error getting readings");
return 2;
}
if(!readPnValues()) {
LOG_ERROR("SEN5X: Error getting PM readings");
return 2;
}
// if(!readRawValues()) {
// LOG_ERROR("SEN5X: Error getting Raw readings");
// return 2;
// }
return 0;
}
int32_t SEN5XSensor::pendingForReady(){
uint32_t now;
now = getTime();
uint32_t sinceMeasureStarted = (now - measureStarted)*1000;
LOG_DEBUG("SEN5X: Since measure started: %ums", sinceMeasureStarted);
switch (state) {
case SEN5X_MEASUREMENT: {
if (sinceMeasureStarted < SEN5X_WARMUP_MS_1) {
LOG_INFO("SEN5X: not enough time passed since starting measurement");
return SEN5X_WARMUP_MS_1 - sinceMeasureStarted;
}
// Get PN values to check if we are above or below threshold
readPnValues();
// If the reading is low (the tyhreshold is in #/cm3) and second warmUp hasn't passed we return to come back later
if ((sen5xmeasurement.pN4p0 / 100) < SEN5X_PN4P0_CONC_THD && sinceMeasureStarted < SEN5X_WARMUP_MS_2) {
LOG_INFO("SEN5X: Concentration is low, we will ask again in the second warm up period");
state = SEN5X_MEASUREMENT_2;
// Report how many seconds are pending to cover the first warm up period
return SEN5X_WARMUP_MS_2 - sinceMeasureStarted;
}
return 0;
}
case SEN5X_MEASUREMENT_2: {
if (sinceMeasureStarted < SEN5X_WARMUP_MS_2) {
// Report how many seconds are pending to cover the first warm up period
return SEN5X_WARMUP_MS_2 - sinceMeasureStarted;
}
return 0;
}
default: {
return -1;
}
}
}
bool SEN5XSensor::getMetrics(meshtastic_Telemetry *measurement)
{
LOG_INFO("SEN5X: Attempting to get metrics");
if (!isActive()){
LOG_INFO("SEN5X: not in measurement mode");
return false;
}
uint8_t response;
response = getMeasurements();
if (response == 0) {
measurement->variant.air_quality_metrics.has_pm10_standard = true;
measurement->variant.air_quality_metrics.pm10_standard = sen5xmeasurement.pM1p0;
measurement->variant.air_quality_metrics.has_pm25_standard = true;
measurement->variant.air_quality_metrics.pm25_standard = sen5xmeasurement.pM2p5;
measurement->variant.air_quality_metrics.has_pm40_standard = true;
measurement->variant.air_quality_metrics.pm40_standard = sen5xmeasurement.pM4p0;
measurement->variant.air_quality_metrics.has_pm100_standard = true;
measurement->variant.air_quality_metrics.pm100_standard = sen5xmeasurement.pM10p0;
measurement->variant.air_quality_metrics.has_particles_05um = true;
measurement->variant.air_quality_metrics.particles_05um = sen5xmeasurement.pN0p5;
measurement->variant.air_quality_metrics.has_particles_10um = true;
measurement->variant.air_quality_metrics.particles_10um = sen5xmeasurement.pN1p0;
measurement->variant.air_quality_metrics.has_particles_25um = true;
measurement->variant.air_quality_metrics.particles_25um = sen5xmeasurement.pN2p5;
measurement->variant.air_quality_metrics.has_particles_40um = true;
measurement->variant.air_quality_metrics.particles_40um = sen5xmeasurement.pN4p0;
measurement->variant.air_quality_metrics.has_particles_100um = true;
measurement->variant.air_quality_metrics.particles_100um = sen5xmeasurement.pN10p0;
if (model == SEN54 || model == SEN55) {
measurement->variant.air_quality_metrics.has_pm_humidity = true;
measurement->variant.air_quality_metrics.pm_humidity = sen5xmeasurement.humidity;
measurement->variant.air_quality_metrics.has_pm_temperature = true;
measurement->variant.air_quality_metrics.pm_temperature = sen5xmeasurement.temperature;
measurement->variant.air_quality_metrics.has_pm_nox_idx = true;
measurement->variant.air_quality_metrics.pm_nox_idx = sen5xmeasurement.noxIndex;
}
if (model == SEN55) {
measurement->variant.air_quality_metrics.has_pm_voc_idx = true;
measurement->variant.air_quality_metrics.pm_voc_idx = sen5xmeasurement.vocIndex;
}
return true;
} else if (response == 1) {
// TODO return because data was not ready yet
// Should this return false?
idle();
return false;
} else if (response == 2) {
// Return with error for non-existing data
idle();
return false;
}
return true;
}
#endif

View File

@ -0,0 +1,134 @@
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "TelemetrySensor.h"
#include "Wire.h"
#include "RTC.h"
// Warm up times for SEN5X from the datasheet
#ifndef SEN5X_WARMUP_MS_1
#define SEN5X_WARMUP_MS_1 15000
#endif
#ifndef SEN5X_WARMUP_MS_2
#define SEN5X_WARMUP_MS_2 30000
#endif
#ifndef SEN5X_I2C_CLOCK_SPEED
#define SEN5X_I2C_CLOCK_SPEED 100000
#endif
#define ONE_WEEK_IN_SECONDS 604800
// TODO - These are currently ints in the protobuf
// Decide on final type for this values and change accordingly
struct _SEN5XMeasurements {
float pM1p0;
float pM2p5;
float pM4p0;
float pM10p0;
uint32_t pN0p5;
uint32_t pN1p0;
uint32_t pN2p5;
uint32_t pN4p0;
uint32_t pN10p0;
float tSize;
float humidity;
float temperature;
float vocIndex;
float noxIndex;
};
class SEN5XSensor : public TelemetrySensor
{
private:
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;
// TODO - Remove
bool continousMode = false;
bool forcedContinousMode = false;
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 sen5xCRC(uint8_t* buffer);
bool I2Cdetect(TwoWire *_Wire, uint8_t address);
bool restoreClock(uint32_t);
bool startCleaning();
uint8_t getMeasurements();
bool readRawValues();
bool readPnValues();
bool readValues();
uint32_t measureStarted = 0;
_SEN5XMeasurements sen5xmeasurement;
protected:
// Store status of the sensor in this file
const char *sen5XCleaningFileName = "/prefs/sen5XCleaning.dat";
const char *sen5XVOCFileName = "/prefs/sen5XVOC.dat";
// Cleaning State
#define SEN5X_MAX_CLEANING_SIZE 32
// Last cleaning status - if > 0 - valid, otherwise 0
uint32_t lastCleaning = 0;
void loadCleaningState();
void updateCleaningState();
// TODO - VOC State
// # define SEN5X_VOC_STATE_BUFFER_SIZE 12
// uint8_t VOCstate[SEN5X_VOC_STATE_BUFFER_SIZE];
// struct VOCstateStruct { uint8_t state[SEN5X_VOC_STATE_BUFFER_SIZE]; uint32_t time; bool valid=true; };
// void loadVOCState();
// void updateVOCState();
virtual void setup() override;
public:
SEN5XSensor();
bool isActive();
uint32_t wakeUp();
bool idle();
virtual int32_t runOnce() override;
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
// Sensirion recommends taking a reading after 15 seconds, if the Particle number reading is over 100#/cm3 the reading is OK, but if it is lower wait until 30 seconds and take it again.
// https://sensirion.com/resource/application_note/low_power_mode/sen5x
#define SEN5X_PN4P0_CONC_THD 100
// This value represents the time needed for pending data
int32_t pendingForReady();
};
#endif

View File

@ -125,18 +125,18 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
if (decoded->variant.air_quality_metrics.has_pm100_standard) {
msgPayload["pm100"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_standard);
}
if (decoded->variant.air_quality_metrics.has_pm10_environmental) {
msgPayload["pm10_e"] =
new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm10_environmental);
}
if (decoded->variant.air_quality_metrics.has_pm25_environmental) {
msgPayload["pm25_e"] =
new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm25_environmental);
}
if (decoded->variant.air_quality_metrics.has_pm100_environmental) {
msgPayload["pm100_e"] =
new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_environmental);
}
// if (decoded->variant.air_quality_metrics.has_pm10_environmental) {
// msgPayload["pm10_e"] =
// new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm10_environmental);
// }
// if (decoded->variant.air_quality_metrics.has_pm25_environmental) {
// msgPayload["pm25_e"] =
// new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm25_environmental);
// }
// if (decoded->variant.air_quality_metrics.has_pm100_environmental) {
// msgPayload["pm100_e"] =
// new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_environmental);
// }
} else if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) {
if (decoded->variant.power_metrics.has_ch1_voltage) {
msgPayload["voltage_ch1"] = new JSONValue(decoded->variant.power_metrics.ch1_voltage);

View File

@ -120,15 +120,15 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
if (decoded->variant.air_quality_metrics.has_pm100_standard) {
jsonObj["payload"]["pm100"] = (unsigned int)decoded->variant.air_quality_metrics.pm100_standard;
}
if (decoded->variant.air_quality_metrics.has_pm10_environmental) {
jsonObj["payload"]["pm10_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm10_environmental;
}
if (decoded->variant.air_quality_metrics.has_pm25_environmental) {
jsonObj["payload"]["pm25_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm25_environmental;
}
if (decoded->variant.air_quality_metrics.has_pm100_environmental) {
jsonObj["payload"]["pm100_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm100_environmental;
}
// if (decoded->variant.air_quality_metrics.has_pm10_environmental) {
// jsonObj["payload"]["pm10_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm10_environmental;
// }
// if (decoded->variant.air_quality_metrics.has_pm25_environmental) {
// jsonObj["payload"]["pm25_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm25_environmental;
// }
// if (decoded->variant.air_quality_metrics.has_pm100_environmental) {
// jsonObj["payload"]["pm100_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm100_environmental;
// }
} else if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) {
if (decoded->variant.power_metrics.has_ch1_voltage) {
jsonObj["payload"]["voltage_ch1"] = decoded->variant.power_metrics.ch1_voltage;