Skip to content

Conversation

@oscgonfer
Copy link
Contributor

@oscgonfer oscgonfer commented Jul 2, 2025

This PR adds support for the SCD4X CO2 sensors by sensirion.
It builds upon on the great work by @Coloradohusky, @hafu and @fifieldt in #4601 and https://github.com/hafu/meshtastic-firmware/tree/add-scd30 (although it doesn't add SCD30 support).

This PR tries to sync that work with the current state master branch, and it's rebased on top of #7190, which is an attempt to decouple AirQualityTelemetry from PMSA0003I sensor (I have just ordered one sensor to test). I also started using the new Sensirion library for this sensor, which has a new release (and a very annoying change in the library name import).

This also goes along with meshtastic/protobufs#719 to support having real temperature and the sensor temperature separately. Testing it, you can see some differences between two sensors that are right next to each other (SHT31 and the one inside the SCD4X).

decoded {
  portnum: TELEMETRY_APP
  payload: "time: 1751450604 air_quality_metrics {   co2: 1602   co2_temperature: 28.4126797   co2_humidity: 55.2819099 }"
}
decoded {
  portnum: TELEMETRY_APP
  payload: "time: 1751450589 environment_metrics {   temperature: 31.69   relative_humidity: 50.19 }"
}

There are some comments that I point in the code directly below. I also tried to make it work with the screen, but I don't think my system allows for testing it ATM.

🤝 Attestations

  • I have tested that my proposed changes behave as described.
  • I have tested that my proposed changes do not cause any obvious regressions on the following devices:
    • Seeed ESP32-S3 Xiao board

@oscgonfer oscgonfer marked this pull request as ready for review July 2, 2025 10:08
@oscgonfer
Copy link
Contributor Author

An additional comment: these type of sensors generally require a "control" interface to enable or disable features. This is mapped here - and could be a good outcome of the hackathon.

@oscgonfer oscgonfer changed the title Add SCD4X feat: Add SCD4X Jul 2, 2025
@thebentern
Copy link
Contributor

@oscgonfer protobufs are merged and generated upstream now

@oscgonfer oscgonfer force-pushed the feat/add-scd4x branch 2 times, most recently from 247768e to 84865cd Compare July 2, 2025 12:46
@oscgonfer
Copy link
Contributor Author

oscgonfer commented Jul 2, 2025

rebased onto #7202 - just to keep in mind that the sensirion library needs to be 1.0.0 @thebentern

@fifieldt
Copy link
Member

fifieldt commented Jul 3, 2025

sensirion 1.1.0 was released - updated upstream.

@oscgonfer
Copy link
Contributor Author

sensirion 1.1.0 was released - updated upstream.

Merged

@hafu
Copy link
Contributor

hafu commented Jul 5, 2025

Thanks for your effort! I can confirm it works with:

@oscgonfer
Copy link
Contributor Author

Thanks for your effort! I can confirm it works with:

* Heltec v3

* RAK11310

* RAK4631 (needs [some fixes](https://github.com/meshtastic/firmware/pull/7190#pullrequestreview-2990183787) in

Thank you @hafu ! I fixed and rebased #7190

@vidplace7 vidplace7 added the enhancement New feature or request label Jul 18, 2025
@CLAassistant
Copy link

CLAassistant commented Jul 22, 2025

CLA assistant check
All committers have signed the CLA.

@CLAassistant
Copy link

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ oscgonfer
❌ peterzqx
You have signed the CLA already but the status is still pending? Let us recheck it.

* Adds VOC measurements and state
* Still not working on VOC Index persistence
* Should it stay in continuous mode?
* Adds initial timer for SEN55 to not sleep if VOCstate is not stable (1h)
* Adds conditions for stability and sensor state
* Adds a new RHT/Gas only mode, with 3600s stabilization time
* Fixes the VOCState buffer mismatch
* Fixes SEN50/54/55 model mistake
@paulwalko
Copy link

Not stale, waiting for merging #7190

I saw #7190 was merged a few days ago (I really appreciate all the hard work on that!) and was wondering if there was anything else we're waiting on for this PR now? Once it gets merged I have a bunch of nodes with co2 sensors I'd be happy to test with.

@thebentern
Copy link
Contributor

Not stale, waiting for merging #7190

I saw #7190 was merged a few days ago (I really appreciate all the hard work on that!) and was wondering if there was anything else we're waiting on for this PR now? Once it gets merged I have a bunch of nodes with co2 sensors I'd be happy to test with.

Gotta work through a number of merge conflicts, but otherwise optimistic on moving this one forward :-)

@oscgonfer
Copy link
Contributor Author

oscgonfer commented Jan 16, 2026

(To avoid duplicating work) I'm aiming to work on this during the next couple of days as well @thebentern

I'm doing this in parallel to the other SEN5X series, there some conflicts on sleep mode that I have almost sorted out.

@oscgonfer
Copy link
Contributor Author

Hi folks, this PR and #7245 will need meshtastic/protobufs#750

@oscgonfer
Copy link
Contributor Author

oscgonfer commented Jan 18, 2026

@paulwalko @thebentern I added changes and tested locally. It all works, but requires meshtastic/protobufs#750

⚠️ Warning: ⚠️ This is now rebased onto #7245

There's a couple of pending to-dos, specially on the admin commands part and the I2C clock adaptation. I'd appreciate some help with admin commands checks, if possible, or some guidance on how to set it up to test them in a fast way.

@paulwalko
Copy link

Is the reclock logic needed for nrf52 architectures? When building I got the "scd41 can't be used at this clock speed, with a screen" warning, which was fixed with the following changes to exclude non esp32 architectures:

 #ifdef SCD4X_I2C_CLOCK_SPEED
+#ifdef ARCH_ESP32
 #ifdef CAN_RECLOCK_I2C
     uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false);
     if (currentClock != SCD4X_I2C_CLOCK_SPEED){
@@ -33,6 +34,7 @@ bool SCD4XSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
     LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName);
     return false;
 #endif /* CAN_RECLOCK_I2C */
+#endif /* ARCH_ESP32 */
 #endif /* SCD4X_I2C_CLOCK_SPEED */

All of my nodes with SCD41 sensors are nrf52 based.

@oscgonfer
Copy link
Contributor Author

oscgonfer commented Jan 19, 2026

Is the reclock logic needed for nrf52 architectures? When building I got the "scd41 can't be used at this clock speed, with a screen" warning, which was fixed with the following changes to exclude non esp32 architectures:

 #ifdef SCD4X_I2C_CLOCK_SPEED
+#ifdef ARCH_ESP32
 #ifdef CAN_RECLOCK_I2C
     uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false);
     if (currentClock != SCD4X_I2C_CLOCK_SPEED){
@@ -33,6 +34,7 @@ bool SCD4XSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
     LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName);
     return false;
 #endif /* CAN_RECLOCK_I2C */
+#endif /* ARCH_ESP32 */
 #endif /* SCD4X_I2C_CLOCK_SPEED */

All of my nodes with SCD41 sensors are nrf52 based.

Hi, maybe the logic is not right in reClockI2C.cpp now.
The issue is the following, only ESP32 core can get and set the I2C clock speed. If there is a screen plugged in, at least in ESP32, the I2C clock speed switches from 100kHz to 700kHz on boot, which then makes us have to change the I2C speed everytime we interact with a sensor.

In particular, the SCD4X needs a I2C clock speed of max 400kHz, which is not compatible with the screen. So... we need a way to know whether or not a screen is present for those cores that can't get the Clock speed, so that we can set up the sensor speed. I thought that the HAS_SCREEN flag did it, but I guess I am wrong (or the logic in reClockI2C.cpp is not properly done... If we know that the speed is always 700kHz for all screens, then it's an easy fix, but we need to know that the screen is present.

Do your NRFs have screens?

@paulwalko
Copy link

paulwalko commented Jan 19, 2026

Thanks for the explanation. I should've mentioned, my devices do not have a screen. I did see that the SCD4x has a max of 400 Mhz, but didn't realize the screen switches up to 700 Mhz which explains a lot. My nodes are the nrf52 diy variant, but for the vast majority of other nodes with known configurations I imagine the logic should work.

One more issue I encountered, the serialization of the air_quality_metrics in src/serialization/MeshSerializer.cpp needs to be updated to support the new co2 fields:

diff --git a/src/serialization/MeshPacketSerializer.cpp b/src/serialization/MeshPacketSerializer.cpp
index 92e700368..f0425580e 100644
--- a/src/serialization/MeshPacketSerializer.cpp
+++ b/src/serialization/MeshPacketSerializer.cpp
@@ -161,6 +161,15 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
                     //     msgPayload["pm100_e"] =
                     //         new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_environmental);
                     // }
+                    if (decoded->variant.air_quality_metrics.has_co2) {
+                        msgPayload["co2"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.co2);
+                    }
+                    if (decoded->variant.air_quality_metrics.has_co2_temperature) {
+                        msgPayload["co2_temperature"] = new JSONValue(decoded->variant.air_quality_metrics.co2_temperature);
+                    }
+                    if (decoded->variant.air_quality_metrics.has_co2_humidity) {
+                        msgPayload["co2_humidity"] = new JSONValue(decoded->variant.air_quality_metrics.co2_humidity);
+                    }

@oscgonfer
Copy link
Contributor Author

oscgonfer commented Jan 19, 2026

Thanks for the explanation. I should've mentioned, my devices do not have a screen. I did see that the SCD4x has a max of 400 Mhz, but didn't realize the screen switches up to 700 Mhz which explains a lot. My nodes are the nrf52 diy variant, but for the vast majority of other nodes with known configurations I imagine the logic should work.

I think that in your particular case, compiling with HAS_SCREEN 0 would do. However, I think I'd be better to have a way to check for the presence of the screen at runtime. Maybe @thebentern can give us a hand here?

One more issue I encountered, the serialization of the air_quality_metrics in src/serialization/MeshSerializer.cpp needs to be updated to support the new co2 fields:

diff --git a/src/serialization/MeshPacketSerializer.cpp b/src/serialization/MeshPacketSerializer.cpp
index 92e700368..f0425580e 100644
--- a/src/serialization/MeshPacketSerializer.cpp
+++ b/src/serialization/MeshPacketSerializer.cpp
@@ -161,6 +161,15 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
                     //     msgPayload["pm100_e"] =
                     //         new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_environmental);
                     // }
+                    if (decoded->variant.air_quality_metrics.has_co2) {
+                        msgPayload["co2"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.co2);
+                    }
+                    if (decoded->variant.air_quality_metrics.has_co2_temperature) {
+                        msgPayload["co2_temperature"] = new JSONValue(decoded->variant.air_quality_metrics.co2_temperature);
+                    }
+                    if (decoded->variant.air_quality_metrics.has_co2_humidity) {
+                        msgPayload["co2_humidity"] = new JSONValue(decoded->variant.air_quality_metrics.co2_humidity);
+                    }

I'll fix that one! Totally missed that! Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants