Skip to main content

pamoja_sensors/bme280/
sim.rs

1//! A BME280 that is not there.
2//!
3//! Standing a part up to run a driver against used to mean writing out its side of the
4//! conversation: the chip id, two calibration blocks, a status register, and eight burst
5//! bytes, as literals, in the order the datasheet lists them. That is a lot of bytes to get
6//! right before a driver has been run once, and it is the same bytes every time.
7//!
8//! This is that part, already answering. [`part`] hands back something a [`Bme280`](super::Bme280) can be
9//! pointed at, holding a real calibration and a reading. [`reporting`] builds one that reads
10//! whatever you ask it for, by working out the raw values the compensation turns back into
11//! that reading.
12//!
13//! The calibration here was read from a real BME280, and [`BURST`] is one measurement that
14//! part took. Together they compensate to 20.44 C, 848.05 hPa, and 44.65 %.
15
16use pamoja_hal::sim::I2cPart;
17
18use super::{
19    register, Calibration, RawMeasurement, CALIB_HUMIDITY_LEN, CALIB_TEMP_PRESS_LEN, CHIP_ID,
20    DATA_LEN,
21};
22
23/// The temperature and pressure calibration a real BME280 holds.
24pub const CALIBRATION: [u8; CALIB_TEMP_PRESS_LEN] = [
25    0x45, 0x6F, 0x6F, 0x68, 0x32, 0x00, 0x46, 0x91, 0x6A, 0xD6, 0xD0, 0x0B, 0x4E, 0x1E, 0x88, 0xFF,
26    0xF9, 0xFF, 0xAC, 0x26, 0x0A, 0xD8, 0xBD, 0x10, 0x00, 0x4B,
27];
28
29/// The humidity calibration that goes with it.
30pub const CALIBRATION_HUMIDITY: [u8; CALIB_HUMIDITY_LEN] =
31    [0x62, 0x01, 0x00, 0x15, 0x23, 0x03, 0x1E];
32
33/// One measurement that part took, as the eight data registers hold it.
34///
35/// Against [`CALIBRATION`] it compensates to 20.44 C, 848.05 hPa, and 44.65 %.
36pub const BURST: [u8; DATA_LEN] = [0x65, 0x5A, 0xC0, 0x7E, 0xED, 0x00, 0x75, 0x30];
37
38/// The status a part reports when it is neither measuring nor loading its calibration.
39pub const STATUS_IDLE: u8 = 0x00;
40
41/// The calibration these registers decode to.
42///
43/// # Returns
44///
45/// The constants a measurement is compensated against.
46///
47/// # Examples
48///
49/// ```
50/// use pamoja_sensors::bme280::sim;
51///
52/// let calibration = sim::calibration();
53/// let reading = calibration.compensate(&pamoja_sensors::bme280::RawMeasurement::from_registers(
54///     &sim::BURST,
55/// ));
56/// assert_eq!(reading.temperature_centi_celsius, 2044);
57/// ```
58#[must_use]
59pub fn calibration() -> Calibration {
60    Calibration::from_registers(&CALIBRATION, &CALIBRATION_HUMIDITY)
61}
62
63/// A BME280 answering at an address, holding a real calibration and one measurement.
64///
65/// # Arguments
66///
67/// * `address` - the address it answers to, usually
68///   [`I2C_ADDRESS_PRIMARY`](super::I2C_ADDRESS_PRIMARY).
69///
70/// # Returns
71///
72/// The part, ready for a driver to reset, identify, read, and measure.
73///
74/// # Examples
75///
76/// ```
77/// use pamoja_core::Sensor;
78/// use pamoja_hal::script::{block_on, DelayLog};
79/// use pamoja_sensors::bme280::{sim, Bme280, I2C_ADDRESS_PRIMARY};
80///
81/// let mut sensor = Bme280::i2c(sim::part(I2C_ADDRESS_PRIMARY), I2C_ADDRESS_PRIMARY, DelayLog::new());
82/// let reading = block_on(sensor.read()).expect("the part answers");
83///
84/// assert_eq!(reading.celsius(), 20.44);
85/// ```
86#[must_use]
87pub fn part(address: u8) -> I2cPart {
88    I2cPart::new(address)
89        .holding(register::CHIP_ID, &[CHIP_ID])
90        .holding(register::STATUS, &[STATUS_IDLE])
91        .holding(register::CALIB_TEMP_PRESS, &CALIBRATION)
92        .holding(register::CALIB_HUMIDITY, &CALIBRATION_HUMIDITY)
93        .holding(register::DATA, &BURST)
94}
95
96/// A BME280 that reads what you ask it to.
97///
98/// # Arguments
99///
100/// * `address` - the address it answers to.
101/// * `celsius` - the temperature it should report.
102/// * `hectopascals` - the pressure it should report.
103/// * `relative_humidity` - the humidity it should report, as a percentage.
104///
105/// # Returns
106///
107/// The part. The readings land on the nearest ones its converter can represent, which is
108/// within a hundredth of a degree, a hundredth of a hectopascal, and a thousandth of a
109/// percent of what was asked for.
110///
111/// # Examples
112///
113/// ```
114/// use pamoja_core::Sensor;
115/// use pamoja_hal::script::{block_on, DelayLog};
116/// use pamoja_sensors::bme280::{sim, Bme280, I2C_ADDRESS_PRIMARY};
117///
118/// // A cold store at four degrees, which is the reading a driver should see.
119/// let cold = sim::reporting(I2C_ADDRESS_PRIMARY, 4.0, 1013.25, 80.0);
120/// let mut sensor = Bme280::i2c(cold, I2C_ADDRESS_PRIMARY, DelayLog::new());
121///
122/// let reading = block_on(sensor.read()).expect("the part answers");
123/// assert_eq!(reading.celsius(), 4.0);
124/// assert!((reading.hectopascals() - 1013.25).abs() < 0.01);
125/// ```
126#[must_use]
127pub fn reporting(address: u8, celsius: f32, hectopascals: f32, relative_humidity: f32) -> I2cPart {
128    part(address).holding(
129        register::DATA,
130        &burst_for(celsius, hectopascals, relative_humidity),
131    )
132}
133
134/// The eight data registers that compensate to a reading.
135///
136/// # Arguments
137///
138/// * `celsius` - the temperature to land on.
139/// * `hectopascals` - the pressure to land on.
140/// * `relative_humidity` - the humidity to land on, as a percentage.
141///
142/// # Returns
143///
144/// The bytes, against [`CALIBRATION`]. They are the converter values the compensation turns
145/// back into that reading, found by searching, because the formulas only run one way.
146///
147/// # Examples
148///
149/// ```
150/// use pamoja_sensors::bme280::{sim, RawMeasurement};
151///
152/// let burst = sim::burst_for(20.44, 848.05, 44.65);
153/// let reading = sim::calibration().compensate(&RawMeasurement::from_registers(&burst));
154/// assert_eq!(reading.temperature_centi_celsius, 2044);
155/// ```
156#[must_use]
157pub fn burst_for(celsius: f32, hectopascals: f32, relative_humidity: f32) -> [u8; DATA_LEN] {
158    const TEMPERATURE_MAX: i32 = 0x000f_ffff;
159    const PRESSURE_MAX: i32 = 0x000f_ffff;
160    const HUMIDITY_MAX: i32 = 0x0000_ffff;
161
162    let calibration = calibration();
163    let wanted_temperature = i64::from(rounded(celsius * 100.0));
164    let wanted_pressure = i64::from(rounded(hectopascals * 10_000.0));
165    let wanted_humidity = i64::from(rounded(relative_humidity * 1024.0));
166
167    // The compensation folds temperature into the other two, so it is settled first and
168    // then held while they are.
169    let temperature = solve(wanted_temperature, 0, TEMPERATURE_MAX, |raw| {
170        i64::from(
171            calibration
172                .compensate(&raw_of(raw, PRESSURE_MAX / 2, HUMIDITY_MAX / 2))
173                .temperature_centi_celsius,
174        )
175    });
176    let pressure = solve(wanted_pressure, 0, PRESSURE_MAX, |raw| {
177        i64::from(
178            calibration
179                .compensate(&raw_of(temperature, raw, HUMIDITY_MAX / 2))
180                .pressure_centi_pascals,
181        )
182    });
183    let humidity = solve(wanted_humidity, 0, HUMIDITY_MAX, |raw| {
184        i64::from(
185            calibration
186                .compensate(&raw_of(temperature, pressure, raw))
187                .humidity_q22_10,
188        )
189    });
190
191    registers_of(&raw_of(temperature, pressure, humidity))
192}
193
194/// The eight data registers a raw measurement sits in.
195///
196/// # Arguments
197///
198/// * `raw` - the converter values.
199///
200/// # Returns
201///
202/// The bytes, which is what [`RawMeasurement::from_registers`] reads back.
203///
204/// # Examples
205///
206/// ```
207/// use pamoja_sensors::bme280::{sim, RawMeasurement};
208///
209/// let raw = RawMeasurement::from_registers(&sim::BURST);
210/// assert_eq!(sim::registers_of(&raw), sim::BURST, "what it reads is what it holds");
211/// ```
212#[must_use]
213pub fn registers_of(raw: &RawMeasurement) -> [u8; DATA_LEN] {
214    [
215        (raw.pressure >> 12) as u8,
216        (raw.pressure >> 4) as u8,
217        ((raw.pressure << 4) & 0xf0) as u8,
218        (raw.temperature >> 12) as u8,
219        (raw.temperature >> 4) as u8,
220        ((raw.temperature << 4) & 0xf0) as u8,
221        (raw.humidity >> 8) as u8,
222        raw.humidity as u8,
223    ]
224}
225
226const fn raw_of(temperature: i32, pressure: i32, humidity: i32) -> RawMeasurement {
227    RawMeasurement {
228        temperature,
229        pressure,
230        humidity,
231    }
232}
233
234// Rounds away from zero, since a part cannot call `f32::round` without an operating system.
235pub(crate) fn rounded(value: f32) -> i32 {
236    if value >= 0.0 {
237        (value + 0.5) as i32
238    } else {
239        (value - 0.5) as i32
240    }
241}
242
243// The converter value a reading came from. The compensation only runs one way and is
244// monotonic, so this walks in from both ends; which way it leans is read off the ends
245// rather than assumed, because pressure falls as its converter value rises.
246pub(crate) fn solve(wanted: i64, low: i32, high: i32, measured: impl Fn(i32) -> i64) -> i32 {
247    let rising = measured(high) >= measured(low);
248    let (mut low, mut high) = (low, high);
249    while low < high {
250        let middle = low + (high - low) / 2;
251        let under = if rising {
252            measured(middle) < wanted
253        } else {
254            measured(middle) > wanted
255        };
256        if under {
257            low = middle + 1;
258        } else {
259            high = middle;
260        }
261    }
262    low
263}
264
265#[cfg(test)]
266mod tests {
267    use super::*;
268    use crate::bme280::{Bme280, I2C_ADDRESS_PRIMARY};
269    use pamoja_core::Sensor;
270    use pamoja_hal::script::{block_on, DelayLog};
271
272    #[test]
273    fn a_driver_reads_the_part_without_a_transfer_being_written_out() {
274        let mut sensor = Bme280::i2c(
275            part(I2C_ADDRESS_PRIMARY),
276            I2C_ADDRESS_PRIMARY,
277            DelayLog::new(),
278        );
279        sensor.init().expect("the part identifies itself");
280
281        let reading = block_on(sensor.read()).expect("a measurement");
282        assert_eq!(reading.temperature_centi_celsius, 2044);
283        assert_eq!(reading.pressure_centi_pascals, 8_480_523);
284        assert_eq!(reading.humidity_q22_10, 45_725);
285    }
286
287    #[test]
288    fn the_configuration_a_driver_wrote_can_be_read_back_off_the_part() {
289        let mut sensor = Bme280::i2c(
290            part(I2C_ADDRESS_PRIMARY),
291            I2C_ADDRESS_PRIMARY,
292            DelayLog::new(),
293        );
294        sensor.init().expect("the part identifies itself");
295        let (registers, _) = sensor.release();
296        let part = registers.release();
297
298        // The datasheet wants humidity set before the mode register is written, and the
299        // part is left asleep until a measurement is forced.
300        assert_eq!(part.register(register::CTRL_HUM), 0x01);
301        assert_eq!(part.register(register::CTRL_MEAS) & 0x03, 0x00, "asleep");
302        assert_eq!(part.register(register::RESET), 0xb6, "and it was reset");
303    }
304
305    #[test]
306    fn a_part_reports_what_it_was_asked_to() {
307        for (celsius, hectopascals, humidity) in [
308            (4.0f32, 1013.25f32, 80.0f32),
309            (20.44, 848.05, 44.65),
310            (-10.5, 950.0, 12.5),
311            (38.75, 1030.0, 95.0),
312        ] {
313            let mut sensor = Bme280::i2c(
314                reporting(I2C_ADDRESS_PRIMARY, celsius, hectopascals, humidity),
315                I2C_ADDRESS_PRIMARY,
316                DelayLog::new(),
317            );
318            let reading = block_on(sensor.read()).expect("a measurement");
319
320            assert!(
321                (reading.celsius() - celsius).abs() < 0.01,
322                "{celsius} C read back as {}",
323                reading.celsius()
324            );
325            assert!(
326                (reading.hectopascals() - hectopascals).abs() < 0.01,
327                "{hectopascals} hPa read back as {}",
328                reading.hectopascals()
329            );
330            assert!(
331                (reading.relative_humidity_percent() - humidity).abs() < 0.01,
332                "{humidity} % read back as {}",
333                reading.relative_humidity_percent()
334            );
335        }
336    }
337
338    #[test]
339    fn the_registers_a_raw_measurement_sits_in_read_back_as_that_measurement() {
340        let raw = RawMeasurement::from_registers(&BURST);
341        assert_eq!(registers_of(&raw), BURST);
342    }
343
344    #[test]
345    fn the_shipped_burst_is_the_reading_the_documentation_claims() {
346        let reading = calibration().compensate(&RawMeasurement::from_registers(&BURST));
347        assert_eq!(reading.celsius(), 20.44);
348        assert_eq!(reading.hectopascals(), 848.0523);
349        assert_eq!(reading.relative_humidity_percent(), 44.65332);
350    }
351}