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}