mirror of
https://github.com/meshtastic/firmware.git
synced 2025-07-30 02:15:41 +00:00
Merge 228b315f71
into aa3b14ce72
This commit is contained in:
commit
b6614ad41a
@ -133,8 +133,6 @@ lib_deps =
|
|||||||
adafruit/Adafruit INA260 Library@1.5.3
|
adafruit/Adafruit INA260 Library@1.5.3
|
||||||
# renovate: datasource=custom.pio depName=Adafruit INA219 packageName=adafruit/library/Adafruit INA219
|
# renovate: datasource=custom.pio depName=Adafruit INA219 packageName=adafruit/library/Adafruit INA219
|
||||||
adafruit/Adafruit INA219@1.2.3
|
adafruit/Adafruit INA219@1.2.3
|
||||||
# renovate: datasource=custom.pio depName=Adafruit PM25 AQI Sensor packageName=adafruit/library/Adafruit PM25 AQI Sensor
|
|
||||||
adafruit/Adafruit PM25 AQI Sensor@2.0.0
|
|
||||||
# renovate: datasource=custom.pio depName=Adafruit MPU6050 packageName=adafruit/library/Adafruit MPU6050
|
# renovate: datasource=custom.pio depName=Adafruit MPU6050 packageName=adafruit/library/Adafruit MPU6050
|
||||||
adafruit/Adafruit MPU6050@2.2.6
|
adafruit/Adafruit MPU6050@2.2.6
|
||||||
# renovate: datasource=custom.pio depName=Adafruit LIS3DH packageName=adafruit/library/Adafruit LIS3DH
|
# renovate: datasource=custom.pio depName=Adafruit LIS3DH packageName=adafruit/library/Adafruit LIS3DH
|
||||||
|
@ -109,6 +109,75 @@ uint16_t ScanI2CTwoWire::getRegisterValue(const ScanI2CTwoWire::RegisterLocation
|
|||||||
return value;
|
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, ...) \
|
#define SCAN_SIMPLE_CASE(ADDR, T, ...) \
|
||||||
case ADDR: \
|
case ADDR: \
|
||||||
logFoundDevice(__VA_ARGS__); \
|
logFoundDevice(__VA_ARGS__); \
|
||||||
|
@ -29,6 +29,9 @@ class ScanI2CTwoWire : public ScanI2C
|
|||||||
|
|
||||||
size_t countDevices() const override;
|
size_t countDevices() const override;
|
||||||
|
|
||||||
|
bool setClockSpeed(ScanI2C::I2CPort, uint32_t);
|
||||||
|
uint32_t getClockSpeed(ScanI2C::I2CPort);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
FoundDevice firstOfOrNONE(size_t, DeviceType[]) const override;
|
FoundDevice firstOfOrNONE(size_t, DeviceType[]) const override;
|
||||||
|
|
||||||
|
34
src/main.cpp
34
src/main.cpp
@ -480,6 +480,7 @@ void setup()
|
|||||||
Wire.setSCL(I2C_SCL);
|
Wire.setSCL(I2C_SCL);
|
||||||
Wire.begin();
|
Wire.begin();
|
||||||
#elif defined(I2C_SDA) && !defined(ARCH_RP2040)
|
#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);
|
Wire.begin(I2C_SDA, I2C_SCL);
|
||||||
#elif defined(ARCH_PORTDUINO)
|
#elif defined(ARCH_PORTDUINO)
|
||||||
if (settingsStrings[i2cdev] != "") {
|
if (settingsStrings[i2cdev] != "") {
|
||||||
@ -523,6 +524,39 @@ void setup()
|
|||||||
LOG_INFO("Scan for i2c devices");
|
LOG_INFO("Scan for i2c devices");
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// Scan I2C port at desired speed
|
||||||
|
#ifdef SCAN_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...", SCAN_I2C_CLOCK_SPEED);
|
||||||
|
|
||||||
|
if(!i2cScanner->setClockSpeed(ScanI2C::I2CPort::WIRE, SCAN_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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Check if necessary
|
||||||
|
// LOG_DEBUG("Starting Wire with defined clock speed, %d...", SCAN_I2C_CLOCK_SPEED);
|
||||||
|
// if(!i2cScanner->setClockSpeed(ScanI2C::I2CPort::WIRE1, SCAN_I2C_CLOCK_SPEED)) {
|
||||||
|
// LOG_ERROR("Unable to set clock speed on WIRE1");
|
||||||
|
// } else {
|
||||||
|
// LOG_INFO("Set clock speed: %d on WIRE1", SCAN_I2C_CLOCK_SPEED);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Restore clock speed
|
||||||
|
if (currentClock != SCAN_I2C_CLOCK_SPEED) {
|
||||||
|
if(!i2cScanner->setClockSpeed(ScanI2C::I2CPort::WIRE, currentClock)) {
|
||||||
|
LOG_ERROR("Unable to restore clock speed on WIRE");
|
||||||
|
} else {
|
||||||
|
currentClock = i2cScanner->getClockSpeed(ScanI2C::I2CPort::WIRE);
|
||||||
|
LOG_INFO("Set clock speed restored to: %uHz on WIRE", currentClock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
#if defined(I2C_SDA1) || (defined(NRF52840_XXAA) && (WIRE_INTERFACES_COUNT == 2))
|
#if defined(I2C_SDA1) || (defined(NRF52840_XXAA) && (WIRE_INTERFACES_COUNT == 2))
|
||||||
i2cScanner->scanPort(ScanI2C::I2CPort::WIRE1);
|
i2cScanner->scanPort(ScanI2C::I2CPort::WIRE1);
|
||||||
#endif
|
#endif
|
||||||
|
@ -221,11 +221,7 @@ void setupModules()
|
|||||||
// TODO: How to improve this?
|
// TODO: How to improve this?
|
||||||
#if HAS_SENSOR && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
#if HAS_SENSOR && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
||||||
new EnvironmentTelemetryModule();
|
new EnvironmentTelemetryModule();
|
||||||
#if __has_include("Adafruit_PM25AQI.h")
|
new AirQualityTelemetryModule();
|
||||||
if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first > 0) {
|
|
||||||
new AirQualityTelemetryModule();
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
#if !MESHTASTIC_EXCLUDE_HEALTH_TELEMETRY
|
#if !MESHTASTIC_EXCLUDE_HEALTH_TELEMETRY
|
||||||
if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MAX30102].first > 0 ||
|
if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MAX30102].first > 0 ||
|
||||||
nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MLX90614].first > 0) {
|
nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MLX90614].first > 0) {
|
||||||
|
@ -1,36 +1,61 @@
|
|||||||
#include "configuration.h"
|
#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 "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||||
#include "AirQualityTelemetry.h"
|
|
||||||
#include "Default.h"
|
#include "Default.h"
|
||||||
|
#include "AirQualityTelemetry.h"
|
||||||
#include "MeshService.h"
|
#include "MeshService.h"
|
||||||
#include "NodeDB.h"
|
#include "NodeDB.h"
|
||||||
#include "PowerFSM.h"
|
#include "PowerFSM.h"
|
||||||
#include "RTC.h"
|
#include "RTC.h"
|
||||||
#include "Router.h"
|
#include "Router.h"
|
||||||
#include "detect/ScanI2CTwoWire.h"
|
#include "UnitConversions.h"
|
||||||
|
#include "graphics/SharedUIDisplay.h"
|
||||||
|
#include "graphics/images.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
|
#include "sleep.h"
|
||||||
#include <Throttle.h>
|
#include <Throttle.h>
|
||||||
|
// Sensor includes
|
||||||
|
#include "Sensor/PMSA003ISensor.h"
|
||||||
|
|
||||||
#ifndef PMSA003I_WARMUP_MS
|
// Sensors
|
||||||
// from the PMSA003I datasheet:
|
PMSA003ISensor pmsa003iSensor;
|
||||||
// "Stable data should be got at least 30 seconds after the sensor wakeup
|
|
||||||
// from the sleep mode because of the fan’s performance."
|
|
||||||
#define PMSA003I_WARMUP_MS 30000
|
#if __has_include(<SensirionI2cScd4x.h>)
|
||||||
|
#include "Sensor/SCD4XSensor.h"
|
||||||
|
SCD4XSensor scd4xSensor;
|
||||||
|
#else
|
||||||
|
NullSensor scd4xSensor;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#include "graphics/ScreenFonts.h"
|
||||||
|
|
||||||
int32_t AirQualityTelemetryModule::runOnce()
|
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;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Uncomment the preferences below if you want to use the module
|
Uncomment the preferences below if you want to use the module
|
||||||
without having to configure it from the PythonAPI or WebUI.
|
without having to configure it from the PythonAPI or WebUI.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// moduleConfig.telemetry.air_quality_enabled = 1;
|
// 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
|
// 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();
|
return disable();
|
||||||
}
|
}
|
||||||
@ -42,79 +67,147 @@ int32_t AirQualityTelemetryModule::runOnce()
|
|||||||
if (moduleConfig.telemetry.air_quality_enabled) {
|
if (moduleConfig.telemetry.air_quality_enabled) {
|
||||||
LOG_INFO("Air quality Telemetry: init");
|
LOG_INFO("Air quality Telemetry: init");
|
||||||
|
|
||||||
#ifdef PMSA003I_ENABLE_PIN
|
if (pmsa003iSensor.hasSensor())
|
||||||
// put the sensor to sleep on startup
|
result = pmsa003iSensor.runOnce();
|
||||||
pinMode(PMSA003I_ENABLE_PIN, OUTPUT);
|
|
||||||
digitalWrite(PMSA003I_ENABLE_PIN, LOW);
|
|
||||||
#endif /* PMSA003I_ENABLE_PIN */
|
|
||||||
|
|
||||||
if (!aqi.begin_I2C()) {
|
if (scd4xSensor.hasSensor())
|
||||||
#ifndef I2C_NO_RESCAN
|
result = scd4xSensor.runOnce();
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
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 {
|
} else {
|
||||||
// if we somehow got to a second run of this module with measurement disabled, then just wait forever
|
// if we somehow got to a second run of this module with measurement disabled, then just wait forever
|
||||||
if (!moduleConfig.telemetry.air_quality_enabled)
|
if (!moduleConfig.telemetry.air_quality_enabled && !AIR_QUALITY_TELEMETRY_MODULE_ENABLE) {
|
||||||
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:
|
|
||||||
return disable();
|
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 */
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef PMSA003I_ENABLE_PIN
|
||||||
|
pmsa003iSensor.sleep();
|
||||||
|
#endif /* PMSA003I_ENABLE_PIN */
|
||||||
|
|
||||||
|
}
|
||||||
|
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 || m.has_pm10_environmental || m.has_pm25_environmental ||
|
||||||
|
m.has_pm100_environmental || m.has_co2;
|
||||||
|
|
||||||
|
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");
|
||||||
|
if (m.has_co2)
|
||||||
|
entries.push_back("CO2: " + String(m.co2) + "ppm");
|
||||||
|
|
||||||
|
|
||||||
|
// === 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,6 +224,10 @@ bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPack
|
|||||||
LOG_INFO(" | PM1.0(Environmental)=%i, PM2.5(Environmental)=%i, PM10.0(Environmental)=%i",
|
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.pm10_environmental, t->variant.air_quality_metrics.pm25_environmental,
|
||||||
t->variant.air_quality_metrics.pm100_environmental);
|
t->variant.air_quality_metrics.pm100_environmental);
|
||||||
|
|
||||||
|
LOG_INFO(" | CO2=%i, CO2_T=%f, CO2_H=%f",
|
||||||
|
t->variant.air_quality_metrics.co2, t->variant.air_quality_metrics.co2_temperature,
|
||||||
|
t->variant.air_quality_metrics.co2_humidity);
|
||||||
#endif
|
#endif
|
||||||
// release previous packet before occupying a new spot
|
// release previous packet before occupying a new spot
|
||||||
if (lastMeasurementPacket != nullptr)
|
if (lastMeasurementPacket != nullptr)
|
||||||
@ -144,35 +241,25 @@ bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPack
|
|||||||
|
|
||||||
bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *m)
|
bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *m)
|
||||||
{
|
{
|
||||||
if (!aqi.read(&data)) {
|
bool valid = true;
|
||||||
LOG_WARN("Skip send measurements. Could not read AQIn");
|
bool hasSensor = false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
m->time = getTime();
|
m->time = getTime();
|
||||||
m->which_variant = meshtastic_Telemetry_air_quality_metrics_tag;
|
m->which_variant = meshtastic_Telemetry_air_quality_metrics_tag;
|
||||||
m->variant.air_quality_metrics.has_pm10_standard = true;
|
m->variant.air_quality_metrics = meshtastic_AirQualityMetrics_init_zero;
|
||||||
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.has_pm10_environmental = true;
|
if (pmsa003iSensor.hasSensor()) {
|
||||||
m->variant.air_quality_metrics.pm10_environmental = data.pm10_env;
|
// TODO - Should we check for sensor state here?
|
||||||
m->variant.air_quality_metrics.has_pm25_environmental = true;
|
// If a sensor is sleeping, we should know and check to wake it up
|
||||||
m->variant.air_quality_metrics.pm25_environmental = data.pm25_env;
|
valid = valid && pmsa003iSensor.getMetrics(m);
|
||||||
m->variant.air_quality_metrics.has_pm100_environmental = true;
|
hasSensor = true;
|
||||||
m->variant.air_quality_metrics.pm100_environmental = data.pm100_env;
|
}
|
||||||
|
|
||||||
LOG_INFO("Send: PM1.0(Standard)=%i, PM2.5(Standard)=%i, PM10.0(Standard)=%i", m->variant.air_quality_metrics.pm10_standard,
|
if (scd4xSensor.hasSensor()) {
|
||||||
m->variant.air_quality_metrics.pm25_standard, m->variant.air_quality_metrics.pm100_standard);
|
valid = valid && scd4xSensor.getMetrics(m);
|
||||||
|
hasSensor = true;
|
||||||
|
}
|
||||||
|
|
||||||
LOG_INFO(" | PM1.0(Environmental)=%i, PM2.5(Environmental)=%i, PM10.0(Environmental)=%i",
|
return valid && hasSensor;
|
||||||
m->variant.air_quality_metrics.pm10_environmental, m->variant.air_quality_metrics.pm25_environmental,
|
|
||||||
m->variant.air_quality_metrics.pm100_environmental);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
meshtastic_MeshPacket *AirQualityTelemetryModule::allocReply()
|
meshtastic_MeshPacket *AirQualityTelemetryModule::allocReply()
|
||||||
@ -206,7 +293,29 @@ meshtastic_MeshPacket *AirQualityTelemetryModule::allocReply()
|
|||||||
bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
|
bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
|
||||||
{
|
{
|
||||||
meshtastic_Telemetry m = meshtastic_Telemetry_init_zero;
|
meshtastic_Telemetry m = meshtastic_Telemetry_init_zero;
|
||||||
|
m.which_variant = meshtastic_Telemetry_air_quality_metrics_tag;
|
||||||
|
m.time = getTime();
|
||||||
if (getAirQualityTelemetry(&m)) {
|
if (getAirQualityTelemetry(&m)) {
|
||||||
|
|
||||||
|
bool hasAnyPM = m.variant.air_quality_metrics.has_pm10_standard || m.variant.air_quality_metrics.has_pm25_standard || m.variant.air_quality_metrics.has_pm100_standard || m.variant.air_quality_metrics.has_pm10_environmental || m.variant.air_quality_metrics.has_pm25_environmental ||
|
||||||
|
m.variant.air_quality_metrics.has_pm100_environmental;
|
||||||
|
|
||||||
|
if (hasAnyPM) {
|
||||||
|
LOG_INFO("Send: pm10_standard=%u, pm25_standard=%u, pm100_standard=%u, \
|
||||||
|
pm10_environmental=%u, pm25_environmental=%u, pm100_environmental=%u", \
|
||||||
|
m.variant.air_quality_metrics.pm10_standard, m.variant.air_quality_metrics.pm25_standard, \
|
||||||
|
m.variant.air_quality_metrics.pm100_standard, m.variant.air_quality_metrics.pm10_environmental, \
|
||||||
|
m.variant.air_quality_metrics.pm25_environmental, m.variant.air_quality_metrics.pm100_environmental);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasAnyCO2 = m.variant.air_quality_metrics.has_co2 || m.variant.air_quality_metrics.has_co2_temperature || m.variant.air_quality_metrics.has_co2_humidity;
|
||||||
|
|
||||||
|
if (hasAnyCO2) {
|
||||||
|
LOG_INFO("Send: co2=%i, co2_t=%f, co2_rh=%f",
|
||||||
|
m.variant.air_quality_metrics.co2, m.variant.air_quality_metrics.co2_temperature,
|
||||||
|
m.variant.air_quality_metrics.co2_humidity);
|
||||||
|
}
|
||||||
|
|
||||||
meshtastic_MeshPacket *p = allocDataProtobuf(m);
|
meshtastic_MeshPacket *p = allocDataProtobuf(m);
|
||||||
p->to = dest;
|
p->to = dest;
|
||||||
p->decoded.want_response = false;
|
p->decoded.want_response = false;
|
||||||
@ -221,16 +330,54 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
|
|||||||
|
|
||||||
lastMeasurementPacket = packetPool.allocCopy(*p);
|
lastMeasurementPacket = packetPool.allocCopy(*p);
|
||||||
if (phoneOnly) {
|
if (phoneOnly) {
|
||||||
LOG_INFO("Send packet to phone");
|
LOG_INFO("Sending packet to phone");
|
||||||
service->sendToPhone(p);
|
service->sendToPhone(p);
|
||||||
} else {
|
} else {
|
||||||
LOG_INFO("Send packet to mesh");
|
LOG_INFO("Sending packet to mesh");
|
||||||
service->sendToMesh(p, RX_SRC_LOCAL, true);
|
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 true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
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()) {
|
||||||
|
// TODO - Potentially implement an admin message to choose between pm_standard
|
||||||
|
// and pm_environmental. This could be configurable as it doesn't make sense so
|
||||||
|
// have both
|
||||||
|
result = pmsa003iSensor.handleAdminMessage(mp, request, response);
|
||||||
|
if (result != AdminMessageHandleResult::NOT_HANDLED)
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scd4xSensor.hasSensor()) {
|
||||||
|
result = scd4xSensor.handleAdminMessage(mp, request, response);
|
||||||
|
if (result != AdminMessageHandleResult::NOT_HANDLED)
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
@ -1,12 +1,18 @@
|
|||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
|
|
||||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include("Adafruit_PM25AQI.h")
|
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#ifndef AIR_QUALITY_TELEMETRY_MODULE_ENABLE
|
||||||
|
#define AIR_QUALITY_TELEMETRY_MODULE_ENABLE 0
|
||||||
|
#endif
|
||||||
|
|
||||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||||
#include "Adafruit_PM25AQI.h"
|
|
||||||
#include "NodeDB.h"
|
#include "NodeDB.h"
|
||||||
#include "ProtobufModule.h"
|
#include "ProtobufModule.h"
|
||||||
|
#include <OLEDDisplay.h>
|
||||||
|
#include <OLEDDisplayUi.h>
|
||||||
|
|
||||||
class AirQualityTelemetryModule : private concurrency::OSThread, public ProtobufModule<meshtastic_Telemetry>
|
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)
|
ProtobufModule("AirQualityTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg)
|
||||||
{
|
{
|
||||||
lastMeasurementPacket = nullptr;
|
lastMeasurementPacket = nullptr;
|
||||||
setIntervalFromNow(10 * 1000);
|
|
||||||
aqi = Adafruit_PM25AQI();
|
|
||||||
nodeStatusObserver.observe(&nodeStatus->onNewStatus);
|
nodeStatusObserver.observe(&nodeStatus->onNewStatus);
|
||||||
|
setIntervalFromNow(10 * 1000);
|
||||||
#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
|
|
||||||
}
|
}
|
||||||
|
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:
|
protected:
|
||||||
/** Called to handle a particular incoming message
|
/** 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);
|
bool sendTelemetry(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false);
|
||||||
|
|
||||||
|
virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp,
|
||||||
|
meshtastic_AdminMessage *request,
|
||||||
|
meshtastic_AdminMessage *response) override;
|
||||||
private:
|
private:
|
||||||
enum State {
|
|
||||||
IDLE = 0,
|
|
||||||
ACTIVE = 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
State state;
|
|
||||||
Adafruit_PM25AQI aqi;
|
|
||||||
PM25_AQI_Data data = {0};
|
|
||||||
bool firstTime = true;
|
bool firstTime = true;
|
||||||
meshtastic_MeshPacket *lastMeasurementPacket;
|
meshtastic_MeshPacket *lastMeasurementPacket;
|
||||||
uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute
|
uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute
|
||||||
uint32_t lastSentToMesh = 0;
|
uint32_t lastSentToMesh = 0;
|
||||||
|
uint32_t lastSentToPhone = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
@ -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,
|
LOG_INFO("Send: soil_temperature=%f, soil_moisture=%u", m.variant.environment_metrics.soil_temperature,
|
||||||
m.variant.environment_metrics.soil_moisture);
|
m.variant.environment_metrics.soil_moisture);
|
||||||
|
|
||||||
sensor_read_error_count = 0;
|
|
||||||
|
|
||||||
meshtastic_MeshPacket *p = allocDataProtobuf(m);
|
meshtastic_MeshPacket *p = allocDataProtobuf(m);
|
||||||
p->to = dest;
|
p->to = dest;
|
||||||
p->decoded.want_response = false;
|
p->decoded.want_response = false;
|
||||||
|
@ -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 sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute
|
||||||
uint32_t lastSentToMesh = 0;
|
uint32_t lastSentToMesh = 0;
|
||||||
uint32_t lastSentToPhone = 0;
|
uint32_t lastSentToPhone = 0;
|
||||||
uint32_t sensor_read_error_count = 0;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
175
src/modules/Telemetry/Sensor/PMSA003ISensor.cpp
Normal file
175
src/modules/Telemetry/Sensor/PMSA003ISensor.cpp
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
#include "configuration.h"
|
||||||
|
|
||||||
|
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
||||||
|
|
||||||
|
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||||
|
#include "PMSA003ISensor.h"
|
||||||
|
#include "TelemetrySensor.h"
|
||||||
|
|
||||||
|
#include <Wire.h>
|
||||||
|
|
||||||
|
PMSA003ISensor::PMSA003ISensor()
|
||||||
|
: TelemetrySensor(meshtastic_TelemetrySensorType_PMSA003I, "PMSA003I")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void PMSA003ISensor::setup()
|
||||||
|
{
|
||||||
|
#ifdef PMSA003I_ENABLE_PIN
|
||||||
|
pinMode(PMSA003I_ENABLE_PIN, OUTPUT);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PMSA003ISensor::restoreClock(uint32_t currentClock){
|
||||||
|
#ifdef PMSA003I_I2C_CLOCK_SPEED
|
||||||
|
if (currentClock != PMSA003I_I2C_CLOCK_SPEED){
|
||||||
|
// LOG_DEBUG("Restoring I2C clock to %uHz", currentClock);
|
||||||
|
return bus->setClock(currentClock);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t PMSA003ISensor::runOnce()
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
|
||||||
|
#ifdef PMSA003I_I2C_CLOCK_SPEED
|
||||||
|
uint32_t currentClock;
|
||||||
|
currentClock = bus->getClock();
|
||||||
|
if (currentClock != PMSA003I_I2C_CLOCK_SPEED){
|
||||||
|
// LOG_DEBUG("Changing I2C clock to %u", PMSA003I_I2C_CLOCK_SPEED);
|
||||||
|
bus->setClock(PMSA003I_I2C_CLOCK_SPEED);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
bus->beginTransmission(address);
|
||||||
|
if (bus->endTransmission() != 0) {
|
||||||
|
LOG_WARN("PMSA003I not found on I2C at 0x12");
|
||||||
|
return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreClock(currentClock);
|
||||||
|
|
||||||
|
status = 1;
|
||||||
|
LOG_INFO("PMSA003I Enabled");
|
||||||
|
|
||||||
|
return initI2CSensor();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement)
|
||||||
|
{
|
||||||
|
if(!isActive()){
|
||||||
|
LOG_WARN("PMSA003I is not active");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef PMSA003I_I2C_CLOCK_SPEED
|
||||||
|
uint32_t currentClock;
|
||||||
|
currentClock = bus->getClock();
|
||||||
|
if (currentClock != PMSA003I_I2C_CLOCK_SPEED){
|
||||||
|
// LOG_DEBUG("Changing I2C clock to %u", PMSA003I_I2C_CLOCK_SPEED);
|
||||||
|
bus->setClock(PMSA003I_I2C_CLOCK_SPEED);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
bus->requestFrom(address, PMSA003I_FRAME_LENGTH);
|
||||||
|
if (bus->available() < PMSA003I_FRAME_LENGTH) {
|
||||||
|
LOG_WARN("PMSA003I read failed: incomplete data (%d bytes)", bus->available());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreClock(currentClock);
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < PMSA003I_FRAME_LENGTH; i++) {
|
||||||
|
buffer[i] = bus->read();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buffer[0] != 0x42 || buffer[1] != 0x4D) {
|
||||||
|
LOG_WARN("PMSA003I frame header invalid: 0x%02X 0x%02X", buffer[0], buffer[1]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto read16 = [](uint8_t *data, uint8_t idx) -> uint16_t {
|
||||||
|
return (data[idx] << 8) | data[idx + 1];
|
||||||
|
};
|
||||||
|
|
||||||
|
computedChecksum = 0;
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < PMSA003I_FRAME_LENGTH - 2; i++) {
|
||||||
|
computedChecksum += buffer[i];
|
||||||
|
}
|
||||||
|
receivedChecksum = read16(buffer, PMSA003I_FRAME_LENGTH - 2);
|
||||||
|
|
||||||
|
if (computedChecksum != receivedChecksum) {
|
||||||
|
LOG_WARN("PMSA003I checksum failed: computed 0x%04X, received 0x%04X", computedChecksum, receivedChecksum);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
measurement->variant.air_quality_metrics.has_pm10_standard = true;
|
||||||
|
measurement->variant.air_quality_metrics.pm10_standard = read16(buffer, 4);
|
||||||
|
|
||||||
|
measurement->variant.air_quality_metrics.has_pm25_standard = true;
|
||||||
|
measurement->variant.air_quality_metrics.pm25_standard = read16(buffer, 6);
|
||||||
|
|
||||||
|
measurement->variant.air_quality_metrics.has_pm100_standard = true;
|
||||||
|
measurement->variant.air_quality_metrics.pm100_standard = read16(buffer, 8);
|
||||||
|
|
||||||
|
measurement->variant.air_quality_metrics.has_pm10_environmental = true;
|
||||||
|
measurement->variant.air_quality_metrics.pm10_environmental = read16(buffer, 10);
|
||||||
|
|
||||||
|
measurement->variant.air_quality_metrics.has_pm25_environmental = true;
|
||||||
|
measurement->variant.air_quality_metrics.pm25_environmental = read16(buffer, 12);
|
||||||
|
|
||||||
|
measurement->variant.air_quality_metrics.has_pm100_environmental = true;
|
||||||
|
measurement->variant.air_quality_metrics.pm100_environmental = read16(buffer, 14);
|
||||||
|
|
||||||
|
measurement->variant.air_quality_metrics.has_particles_03um = true;
|
||||||
|
measurement->variant.air_quality_metrics.particles_03um = read16(buffer, 16);
|
||||||
|
|
||||||
|
measurement->variant.air_quality_metrics.has_particles_05um = true;
|
||||||
|
measurement->variant.air_quality_metrics.particles_05um = read16(buffer, 18);
|
||||||
|
|
||||||
|
measurement->variant.air_quality_metrics.has_particles_10um = true;
|
||||||
|
measurement->variant.air_quality_metrics.particles_10um = read16(buffer, 20);
|
||||||
|
|
||||||
|
measurement->variant.air_quality_metrics.has_particles_25um = true;
|
||||||
|
measurement->variant.air_quality_metrics.particles_25um = read16(buffer, 22);
|
||||||
|
|
||||||
|
measurement->variant.air_quality_metrics.has_particles_50um = true;
|
||||||
|
measurement->variant.air_quality_metrics.particles_50um = read16(buffer, 24);
|
||||||
|
|
||||||
|
measurement->variant.air_quality_metrics.has_particles_100um = true;
|
||||||
|
measurement->variant.air_quality_metrics.particles_100um = read16(buffer, 26);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PMSA003ISensor::isActive()
|
||||||
|
{
|
||||||
|
return state == State::ACTIVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
#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
|
||||||
|
|
||||||
|
#endif
|
35
src/modules/Telemetry/Sensor/PMSA003ISensor.h
Normal file
35
src/modules/Telemetry/Sensor/PMSA003ISensor.h
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "TelemetrySensor.h"
|
||||||
|
|
||||||
|
#define PMSA003I_I2C_CLOCK_SPEED 100000
|
||||||
|
#define PMSA003I_FRAME_LENGTH 32
|
||||||
|
#define PMSA003I_WARMUP_MS 30000
|
||||||
|
|
||||||
|
|
||||||
|
class PMSA003ISensor : public TelemetrySensor
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
PMSA003ISensor();
|
||||||
|
virtual void setup() override;
|
||||||
|
virtual int32_t runOnce() override;
|
||||||
|
virtual bool restoreClock(uint32_t currentClock);
|
||||||
|
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
|
||||||
|
virtual bool isActive();
|
||||||
|
|
||||||
|
#ifdef PMSA003I_ENABLE_PIN
|
||||||
|
void sleep();
|
||||||
|
uint32_t wakeUp();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
private:
|
||||||
|
enum class State { IDLE, ACTIVE };
|
||||||
|
State state = State::ACTIVE;
|
||||||
|
TwoWire * bus;
|
||||||
|
uint8_t address;
|
||||||
|
|
||||||
|
uint16_t computedChecksum = 0;
|
||||||
|
uint16_t receivedChecksum = 0;
|
||||||
|
|
||||||
|
uint8_t buffer[PMSA003I_FRAME_LENGTH];
|
||||||
|
};
|
75
src/modules/Telemetry/Sensor/SCD4XSensor.cpp
Normal file
75
src/modules/Telemetry/Sensor/SCD4XSensor.cpp
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
#include "configuration.h"
|
||||||
|
|
||||||
|
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<SensirionI2cScd4x.h>)
|
||||||
|
|
||||||
|
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||||
|
#include "SCD4XSensor.h"
|
||||||
|
#include "TelemetrySensor.h"
|
||||||
|
#include <SensirionI2cScd4x.h>
|
||||||
|
|
||||||
|
#define SCD4X_NO_ERROR 0
|
||||||
|
|
||||||
|
SCD4XSensor::SCD4XSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SCD4X, "SCD4X") {}
|
||||||
|
|
||||||
|
int32_t SCD4XSensor::runOnce()
|
||||||
|
{
|
||||||
|
LOG_INFO("Init sensor: %s", sensorName);
|
||||||
|
if (!hasSensor()) {
|
||||||
|
return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t error;
|
||||||
|
|
||||||
|
scd4x.begin(*nodeTelemetrySensorsMap[sensorType].second,
|
||||||
|
nodeTelemetrySensorsMap[sensorType].first);
|
||||||
|
|
||||||
|
delay(30);
|
||||||
|
// Ensure sensor is in clean state
|
||||||
|
error = scd4x.wakeUp();
|
||||||
|
if (error != SCD4X_NO_ERROR) {
|
||||||
|
LOG_INFO("Error trying to execute wakeUp()");
|
||||||
|
return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop periodic measurement
|
||||||
|
error = scd4x.stopPeriodicMeasurement();
|
||||||
|
if (error != SCD4X_NO_ERROR) {
|
||||||
|
LOG_INFO("Error trying to stopPeriodicMeasurement()");
|
||||||
|
return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO - Decide if using Periodic mesaurement or singleshot
|
||||||
|
// status = scd4x.startLowPowerPeriodicMeasurement();
|
||||||
|
|
||||||
|
if (!scd4x.startLowPowerPeriodicMeasurement()) {
|
||||||
|
status = 1;
|
||||||
|
} else {
|
||||||
|
status = 0;
|
||||||
|
}
|
||||||
|
return initI2CSensor();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SCD4XSensor::setup() {}
|
||||||
|
|
||||||
|
bool SCD4XSensor::getMetrics(meshtastic_Telemetry *measurement)
|
||||||
|
{
|
||||||
|
uint16_t co2, error;
|
||||||
|
float temperature;
|
||||||
|
float humidity;
|
||||||
|
|
||||||
|
error = scd4x.readMeasurement(co2, temperature, humidity);
|
||||||
|
if (error != SCD4X_NO_ERROR || co2 == 0) {
|
||||||
|
LOG_DEBUG("Skipping invalid SCD4X measurement.");
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
measurement->variant.air_quality_metrics.has_co2_temperature = true;
|
||||||
|
measurement->variant.air_quality_metrics.has_co2_humidity = true;
|
||||||
|
measurement->variant.air_quality_metrics.has_co2 = true;
|
||||||
|
measurement->variant.air_quality_metrics.co2_temperature = temperature;
|
||||||
|
measurement->variant.air_quality_metrics.co2_humidity = humidity;
|
||||||
|
measurement->variant.air_quality_metrics.co2 = co2;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
23
src/modules/Telemetry/Sensor/SCD4XSensor.h
Normal file
23
src/modules/Telemetry/Sensor/SCD4XSensor.h
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
#include "configuration.h"
|
||||||
|
|
||||||
|
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<SensirionI2cScd4x.h>)
|
||||||
|
|
||||||
|
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||||
|
#include "TelemetrySensor.h"
|
||||||
|
#include <SensirionI2cScd4x.h>
|
||||||
|
|
||||||
|
class SCD4XSensor : public TelemetrySensor
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
SensirionI2cScd4x scd4x;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual void setup() override;
|
||||||
|
|
||||||
|
public:
|
||||||
|
SCD4XSensor();
|
||||||
|
virtual int32_t runOnce() override;
|
||||||
|
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
Loading…
Reference in New Issue
Block a user