pamoja_sensors/bmp280/sim.rs
1//! A BMP280 that is not there.
2//!
3//! A BMP280 is a BME280 without humidity: the same temperature and pressure registers and the
4//! same calibration layout. So [`part`] holds the temperature and pressure half of the real
5//! part the [BME280 simulation](crate::bme280::sim) was read from, and reads 20.44 C and
6//! 848.05 hPa. [`reporting`] reads whatever it is asked to, against the BMP280's own
7//! compensation.
8
9use pamoja_hal::sim::I2cPart;
10
11use super::{register, Calibration, Measurement, CALIBRATION_LEN, CHIP_ID, DATA_LEN};
12use crate::bme280::sim::{self as bme280, rounded, solve};
13
14/// The status a part reports when it is neither measuring nor loading its calibration.
15pub const STATUS_IDLE: u8 = 0x00;
16
17/// The 24 trimming bytes [`part`] holds from 0x88: the temperature and pressure block of the
18/// real part the BME280 simulation was read from.
19pub const CALIBRATION: [u8; CALIBRATION_LEN] = leading(&bme280::CALIBRATION);
20
21/// The six data registers [`part`] holds from 0xF7: the pressure and temperature of the one
22/// measurement that part took.
23pub const BURST: [u8; DATA_LEN] = leading(&bme280::BURST);
24
25/// The calibration [`part`] holds.
26///
27/// # Returns
28///
29/// The constants a measurement is compensated against.
30#[must_use]
31pub fn calibration() -> Calibration {
32 Calibration::parse(&CALIBRATION)
33}
34
35// The first N bytes of a longer block, in a constant.
36const fn leading<const N: usize, const M: usize>(block: &[u8; M]) -> [u8; N] {
37 let mut out = [0u8; N];
38 let mut index = 0;
39 while index < N {
40 out[index] = block[index];
41 index += 1;
42 }
43 out
44}
45
46/// A BMP280 answering at an address, holding a real calibration and one measurement.
47///
48/// # Arguments
49///
50/// * `address` - the address it answers to, usually
51/// [`I2C_ADDRESS_PRIMARY`](super::I2C_ADDRESS_PRIMARY).
52///
53/// # Returns
54///
55/// The part, which compensates to 20.44 C and 848.05 hPa.
56///
57/// # Examples
58///
59/// ```
60/// use pamoja_hal::script::DelayLog;
61/// use pamoja_sensors::bmp280::{sim, Bmp280, I2C_ADDRESS_PRIMARY};
62///
63/// let mut sensor = Bmp280::i2c(sim::part(I2C_ADDRESS_PRIMARY), I2C_ADDRESS_PRIMARY, DelayLog::new());
64/// let reading = sensor.measure().expect("the part answers");
65/// assert_eq!(reading.celsius(), 20.44);
66/// ```
67#[must_use]
68pub fn part(address: u8) -> I2cPart {
69 I2cPart::new(address)
70 .holding(register::CHIP_ID, &[CHIP_ID])
71 .holding(register::STATUS, &[STATUS_IDLE])
72 .holding(register::CALIBRATION, &CALIBRATION)
73 .holding(register::DATA, &BURST)
74}
75
76/// A BMP280 that reads what it is asked to.
77///
78/// # Arguments
79///
80/// * `address` - the address it answers to.
81/// * `celsius` - the temperature it reports.
82/// * `hectopascals` - the pressure it reports.
83///
84/// # Returns
85///
86/// The part. Its readings land within a hundredth of a degree and a hundredth of a
87/// hectopascal of what was asked for.
88#[must_use]
89pub fn reporting(address: u8, celsius: f32, hectopascals: f32) -> I2cPart {
90 part(address).holding(register::DATA, &burst_for(celsius, hectopascals))
91}
92
93/// The six data registers that compensate to a reading against [`calibration`].
94///
95/// # Arguments
96///
97/// * `celsius` - the temperature to land on.
98/// * `hectopascals` - the pressure to land on.
99///
100/// # Returns
101///
102/// The bytes a burst read returns, found by searching, since the compensation only runs one
103/// way. The search reads the compensated pressure as signed: at the far end of the
104/// converter's range the reference arithmetic goes below zero and wraps when it is stored
105/// unsigned.
106#[must_use]
107pub fn burst_for(celsius: f32, hectopascals: f32) -> [u8; DATA_LEN] {
108 const RAW_MAX: i32 = 0x000f_ffff;
109 let calibration = calibration();
110 let raw_of = |temperature: i32, pressure: i32| Measurement {
111 pressure: pressure as u32,
112 temperature: temperature as u32,
113 };
114 let wanted_temperature = i64::from(rounded(celsius * 100.0));
115 let wanted_pressure = i64::from(rounded(hectopascals * 25_600.0));
116
117 let temperature = solve(wanted_temperature, 0, RAW_MAX, |raw| {
118 i64::from(
119 calibration
120 .compensate(&raw_of(raw, RAW_MAX / 2))
121 .temperature_centi_celsius,
122 )
123 });
124 let pressure = solve(wanted_pressure, 0, RAW_MAX, |raw| {
125 i64::from(
126 calibration
127 .compensate(&raw_of(temperature, raw))
128 .pressure_q24_8 as i32,
129 )
130 });
131 raw_of(temperature, pressure).to_bytes()
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use crate::bmp280::{Bmp280, I2C_ADDRESS_SECONDARY};
138 use pamoja_hal::script::DelayLog;
139
140 #[test]
141 fn the_driver_reads_the_shared_half_of_the_real_part() {
142 let mut sensor = Bmp280::i2c(
143 part(I2C_ADDRESS_SECONDARY),
144 I2C_ADDRESS_SECONDARY,
145 DelayLog::new(),
146 );
147 let reading = sensor.measure().expect("a reading");
148 assert_eq!(reading.celsius(), 20.44);
149 assert!((reading.hectopascals() - 848.05).abs() < 0.01);
150 }
151
152 #[test]
153 fn a_part_reports_what_it_was_asked_to() {
154 for (celsius, hectopascals) in [(-7.5f32, 1003.0f32), (31.25, 940.5), (-38.2, 1080.0)] {
155 let mut sensor = Bmp280::i2c(
156 reporting(I2C_ADDRESS_SECONDARY, celsius, hectopascals),
157 I2C_ADDRESS_SECONDARY,
158 DelayLog::new(),
159 );
160 let reading = sensor.measure().expect("a reading");
161 assert!(
162 (reading.celsius() - celsius).abs() < 0.01,
163 "{celsius} C read back as {}",
164 reading.celsius()
165 );
166 assert!(
167 (reading.hectopascals() - hectopascals).abs() < 0.01,
168 "{hectopascals} hPa read back as {}",
169 reading.hectopascals()
170 );
171 }
172 }
173}