Start Measuring
❗️❗️❗️ defmt update
Last week, defmt v0.1.0 to crates.io was released on crates.io. We prepared a handy guide on migrating your project from the github version of defmt to the crates.io version. New projects based on the app-template will automatically use the crates.io version from now on.
Refactoring of older instructions:
Please read the Chapter Bringing it all together and Chapter Hello Sensor and put your code into modules
After making sure, the communication is set up correctly by reading and logging the version number of the firmware, we'll start making measurements.
An example of this implementation can be found here: 11_scd_30_measure.rs.
✅ Go to section 1.4.1 of the Interface Description.
What are the message components we have to provide, so that continuous measuring is triggered?
Answer
0x00 Command byte
0x10 Command byte
0x00 Argument: Ambient Air Pressure
0x00 Argument: Ambient Air Pressure
0x81 CRC byte
The start and stop sign are automatically provided by the write method.
This Message does not only contain a command, but also an argument which allows to set a value for ambient air pressure.
The Role of Ambient Air Pressure
Together with temperature, air pressure determines how many gas molecules can be found in a defined space. The number of molecules rises when pressure increases and falls when pressure decreases. The Sensor's output unit for CO2 is ppm, parts per million, which means of one million particles (atoms or molecules) the air contains as a whole, a certain number are Carbon dioxide molecules.
If a very accurate sensor reading is necessary, the value for ambient air pressure should come from another sensor. When building an air quality monitor for work and school rooms, hardcoding a value is sufficient. The standard air pressure at sea-level is 1013.25 mbar, check you local weather station for a value if you live on higher altitudes.
For this tutorial we use the current value from Berlin, which is 1020 mbar.
Start Continuous Measurement
✅ Go to src/scd30/mod.rs
. In the impl SCD30
block add a new function that takes &mut self
as and pressure: u16
as arguments and returns a Result
type with the variants ()
and Error
.
Inside the function, define a mutable array for 5 u8
bytes, as this is the length of the message we will send. Leave the argument bytes and the crc byte as 0x00
.
#![allow(unused)] fn main() { pub fn start_continuous_measurement(&mut self, pressure: u16) -> Result<(), Error> { let mut command: [u8; 5] = [0x00, 0x10, 0x00, 0x00, 0x00]; // ... Ok(()) } }
Next, we fill in the argument into the command. The sensor communication works in big endian byte order.
✅ Convert the u16
value into big-endian bytes. Assign the values contained in the returned slice to their respective positions in the command.
#![allow(unused)] fn main() { let argument_bytes = &pressure.to_be_bytes(); command[2] = argument_bytes[0]; command[3] = argument_bytes[1]; }
Calculating the CRC-Byte
If we send messages that are longer then two bytes, we need to send CRC bytes for verification after every two bytes. They need to be calculated from the argument bytes.
✅ Go to cargo.toml
and add the following dependency:
#![allow(unused)] fn main() { crc_all = "0.2.0" }
✅ Go back to src/scd30/mod.rs
and bring the module into scope:
#![allow(unused)] fn main() { use crc_all::Crc; }
✅ Check the documentation of crc_all. What arguments does the instance method Crc::<u8>::new()
require?
Go to the Interface Description of the sensor, section 1.1.3 and check if you can fill in all the arguments.
Answer
|arguments|information|
|-|-|
|poly: u8|0x31|
|width: uszise|8|
|init: u8|0xff|
|xorout: u8|0x00|
|reflect: bool|false|
✅ Inside pub fn start_continuous_measurement()
, instantiate a new crc byte, with your gathered information. The variable needs to be mutable. Update the crc byte with the pressure value and assign the byte to its position in the command array. Send the command to the sensor.
#![allow(unused)] fn main() { let mut crc = Crc::<u8>::new(0x31, 8, 0xff, 0x00, false); crc.update(&pressure.to_be_bytes()); command[4] = crc.finish(); self.0.write(DEFAULT_ADDRESS, &command)?; }
✅ Go to your program file. In fn main()
set the ambient air pressure and start measuring!
#![allow(unused)] fn main() { // substitute 1020_u16 with your local value let pressure = 1020_u16; // ... sensor.start_continuous_measurement(pressure).unwrap(); loop { ///... } }
✅ Run the program. you should see a blinking led.
After powering up, the sensor takes about 2 seconds until data is ready to be read. Besides just providing values, the sensor is also able to provide the information, if data is ready yet or not.
✅ In src/scd30/mod.rs
, implement the data_ready method. Check the interface description for the command and length of the read buffer.
Answer
```rust
pub fn data_ready(&mut self) -> Result<bool, Error> {
let command: [u8; 2] = [0x02, 0x02];
let mut rd_buffer = [0u8; 3];
self.0.write(DEFAULT_ADDRESS, &command)?;
self.0.read(DEFAULT_ADDRESS, &mut rd_buffer)?;
Ok(u16::from_be_bytes([rd_buffer[0], rd_buffer[1]]) == 1)
}
```
✅ In your program file, before the blinking loop, open a new loop, that constantly reads the sensor if data is ready. Add a log statement that prints "Data ready." once the method returns true
. Then the loop breaks.
#![allow(unused)] fn main() { loop { if sensor.data_ready().unwrap() { defmt::info!("Data ready."); break } }
✅ Run your program. You should see the log output "Data ready".
Reading and Logging Sensor Data
The sensor returns three values, one for Carbon dioxide concentration, one for temperature and one for humidity. In section 1.5 in the Interface Description find the number type the sensor uses for the data.
Answer
The values the sensor returns are float numbers in big-endian format.
✅ Go to src/scd30/mod.rs
. Add a new struct
definition, with a field for each value.
#![allow(unused)] fn main() { pub struct SensorData { pub co2: f32, pub temperature: f32, pub humidity: f32, } pub const DEFAULT_ADDRESS: u8 = 0x61; pub struct SCD30<T: Instance>(Twim<T>); impl<T> SCD30<T> where T: Instance, { // ... } }
✅ Inside the impl SCD30
block, add a new method:
#![allow(unused)] fn main() { pub fn read_measurement(&mut self) -> Result<SensorData, Error> { // ... Ok(data) } }
- Check the Interface Description for the command and length of the read buffer.
- Make an instance of
SensorData
. - Convert the relevant bytes into
f32
values. Check the std documentation for conversion of big endian bytes to f32. - return the data.
Answer
#![allow(unused)] fn main() { pub fn read_measurement(&mut self) -> Result<SensorData, Error> { let command: [u8; 2] = [0x03, 0x00]; let mut rd_buffer = [0u8; 18]; self.0.write(DEFAULT_ADDRESS, &command)?; self.0.read(DEFAULT_ADDRESS, &mut rd_buffer)?; let data = SensorData { co2: f32::from_bits(u32::from_be_bytes([ rd_buffer[0], rd_buffer[1], rd_buffer[3], rd_buffer[4], ])), temperature: f32::from_bits(u32::from_be_bytes([ rd_buffer[6], rd_buffer[7], rd_buffer[9], rd_buffer[10], ])), humidity: f32::from_bits(u32::from_be_bytes([ rd_buffer[12], rd_buffer[13], rd_buffer[15], rd_buffer[16], ])), }; Ok(data) } }
✅ In your program file, inside the blinking loop, call the method and add the values and their unit to the log.
#![allow(unused)] fn main() { loop { let result = sensor.get_measurement().unwrap(); let co2 = result.co2; let temp = result.temperature; let humidity = result.humidity; defmt::info!(" CO2 {=f32} ppm Temperature {=f32} °C Humidity {=f32} % ", co2, temp, humidity ); // blinking leds } }
✅ Run the program, you should see the three values in the log output.
Optional Challenges:
- The factory calibration of the sensor is pretty good, but you can still read up on how to calibrate the sensor and implement the necessary methods.
- Implement the altitude compensation method and use it instead of pressure compensation.
- Calculate absolute humidity from the relative humidity value you get from the sensor.
- Calculate the dew point.
- Implement the remaining methods listed in the sensor's Interface Description.