tosca_drivers/
bh1750.rs

1//! # BH1750 Driver
2//!
3//! This module provides an asynchronous, architecture-agnostic driver for the
4//! `BH1750` ambient light sensor, enabling the reading of light intensity in
5//! lux over the I²C protocol.
6//!
7//! The driver implements all instructions of the sensor instruction set
8//! architecture.
9//!
10//! For detailed specifications, refer to the
11//! [datasheet](https://www.alldatasheet.com/datasheet-pdf/pdf/338083/ROHM/BH1750FVI.html).
12
13use core::result::Result::{self, Ok};
14
15use embedded_hal_async::delay::DelayNs;
16use embedded_hal_async::i2c::I2c;
17
18// Instruction set architecture opcodes.
19const POWER_DOWN: u8 = 0x00;
20const POWER_ON: u8 = 0x01;
21const RESET: u8 = 0x07;
22
23// MTreg configuration opcodes.
24// The 8-bit MTreg value is split into a high and a low instruction byte.
25const MTREG_HIGH: u8 = 0x40;
26const MTREG_LOW: u8 = 0x60;
27
28/// Minimum allowed value for the `MTreg` register.
29pub const MTREG_MIN: u8 = 31;
30/// Maximum allowed value for `MTreg` register.
31pub const MTREG_MAX: u8 = 254;
32const DEFAULT_MTREG: u8 = 69; // Default per datasheet.
33
34/// Errors that may occur when interacting with the `BH1750` sensor.
35#[derive(Debug, Copy, Clone)]
36pub enum Bh1750Error<E> {
37    /// I²C bus error.
38    I2c(E),
39    /// Continuous measurement not started.
40    ///
41    /// This error occurs when attempting to read a continuous measurement
42    /// before the measurement has started.
43    ContinuousMeasurementNotStarted,
44}
45
46impl<E> From<E> for Bh1750Error<E> {
47    fn from(e: E) -> Self {
48        Bh1750Error::I2c(e)
49    }
50}
51
52/// I²C address of the `BH1750` sensor.
53///
54/// The sensor supports two possible addresses, determined by the
55/// state of the ADD pin.
56#[derive(Debug, Clone, Copy)]
57pub enum Address {
58    /// Low: `0x23` when the ADD is connected to GND or left floating.
59    Low = 0x23,
60    /// High: `0x23` when the ADD is connected to VCC.
61    High = 0x5C,
62}
63
64/// Measurement resolution modes for the `BH1750` sensor.
65#[derive(Debug, Clone, Copy)]
66pub enum Resolution {
67    /// High resolution mode: 1 lx per count.
68    ///
69    /// The measurement time is 120 ms, assuming the default `MTreg` value.
70    High,
71    /// High resolution mode 2: 0.5 lx per count.
72    ///
73    /// The measurement time is 120 ms, assuming the default `MTreg` value.
74    High2,
75    /// Low resolution mode: 4 lx per count.
76    ///
77    /// The measurement time is 16 ms, assuming the default `MTreg` value.
78    Low,
79}
80
81impl Resolution {
82    #[inline]
83    const fn continuous_measurement_opcode(self) -> u8 {
84        match self {
85            Self::High => 0x10,
86            Self::High2 => 0x11,
87            Self::Low => 0x13,
88        }
89    }
90
91    #[inline]
92    const fn one_time_measurement_opcode(self) -> u8 {
93        match self {
94            Self::High => 0x20,
95            Self::High2 => 0x21,
96            Self::Low => 0x23,
97        }
98    }
99
100    #[inline]
101    const fn default_measurement_time_ms(self) -> u32 {
102        // Returns the default lux per count for this resolution mode,
103        // assuming default `MTreg` value.
104        match self {
105            Self::High | Self::High2 => 120,
106            Self::Low => 16,
107        }
108    }
109
110    #[inline]
111    const fn default_resolution_lx_count(self) -> f32 {
112        // Returns the default measurement time for this resolution mode,
113        // assuming default `MTreg` value.
114        match self {
115            Self::High => 1.0,
116            Self::High2 => 0.5,
117            Self::Low => 4.0,
118        }
119    }
120}
121
122/// The `BH1750` driver.
123pub struct Bh1750<I2C, D>
124where
125    D: DelayNs,
126{
127    i2c: I2C,
128    delay: D,
129    address: Address,
130    mtreg: u8,
131    continuous_resolution: Option<Resolution>,
132}
133
134impl<I2C, E, D> Bh1750<I2C, D>
135where
136    I2C: I2c<u8, Error = E>,
137    D: DelayNs,
138{
139    /// Creates a [`Bh1750`] driver with the given I²C bus, delay provider,
140    /// and address.
141    ///
142    /// The `MTreg` register is initialized to its default value.
143    #[must_use]
144    pub fn new(i2c: I2C, delay: D, address: Address) -> Self {
145        Self {
146            i2c,
147            delay,
148            address,
149            mtreg: DEFAULT_MTREG,
150            continuous_resolution: None,
151        }
152    }
153
154    /// Puts the sensor into the `Power On` state.
155    ///
156    /// # Errors
157    ///
158    /// Returns an error if the I²C communication with the sensor fails.
159    pub async fn power_on(&mut self) -> Result<(), Bh1750Error<E>> {
160        self.send_instruction(POWER_ON).await
161    }
162
163    /// Puts the sensor into the `Power Down` state.
164    ///
165    /// # Errors
166    ///
167    /// Returns an error if the I²C communication with the sensor fails.
168    pub async fn power_down(&mut self) -> Result<(), Bh1750Error<E>> {
169        self.send_instruction(POWER_DOWN).await
170    }
171
172    /// Resets the sensor data register.
173    ///
174    /// Must be called only when the sensor is in the `Power On` state.
175    ///
176    /// # Errors
177    ///
178    /// Returns an error if the I²C communication with the sensor fails.
179    pub async fn reset(&mut self) -> Result<(), Bh1750Error<E>> {
180        self.send_instruction(RESET).await
181    }
182
183    /// Sets the measurement time register (`MTreg`) to adjust sensitivity.
184    ///
185    /// The given value is clamped between [`MTREG_MIN`] and [`MTREG_MAX`].
186    ///
187    /// # Errors
188    ///
189    /// Returns an error if the I²C write operation to the sensor fails.
190    pub async fn set_mtreg(&mut self, mtreg: u8) -> Result<(), Bh1750Error<E>> {
191        let mt = mtreg.clamp(MTREG_MIN, MTREG_MAX);
192
193        // Split the 8-bit MTreg value into two parts and send as separate opcodes.
194        let high = MTREG_HIGH | (mt >> 5);
195        let low = MTREG_LOW | (mt & 0x1F);
196
197        self.send_instruction(high).await?;
198        self.send_instruction(low).await?;
199
200        self.mtreg = mt;
201
202        Ok(())
203    }
204
205    /// Performs a one-time measurement and returns the light level in lux.
206    ///
207    /// After the measurement, the sensor automatically powers down.
208    ///
209    /// # Errors
210    ///
211    /// Returns an error if I²C communication with the sensor fails.
212    pub async fn one_time_measurement(&mut self, res: Resolution) -> Result<f32, Bh1750Error<E>> {
213        self.start_one_time_measurement(res).await?;
214        self.delay.delay_ms(self.measurement_time_ms(res)).await;
215        let raw = self.read_raw().await?;
216
217        Ok(self.raw_to_lux(raw, res))
218    }
219
220    /// Starts continuous measurement at the given resolution.
221    ///
222    /// # Errors
223    ///
224    /// Returns an error if the I²C configuration write fails.
225    pub async fn start_continuous_measurement(
226        &mut self,
227        res: Resolution,
228    ) -> Result<(), Bh1750Error<E>> {
229        self.send_instruction(res.continuous_measurement_opcode())
230            .await?;
231        self.continuous_resolution = Some(res);
232
233        Ok(())
234    }
235
236    /// Reads the most recent value from a continuous measurement, in lux.
237    ///
238    /// # Errors
239    ///
240    /// - [`Bh1750Error::ContinuousMeasurementNotStarted`] if the caller
241    ///   attempts to read before starting the continuous measurement mode.
242    /// - An I²C error if communication with the sensor fails.
243    pub async fn read_continuous_measurement(&mut self) -> Result<f32, Bh1750Error<E>> {
244        let res = self
245            .continuous_resolution
246            .ok_or(Bh1750Error::ContinuousMeasurementNotStarted)?;
247
248        // Wait for the effective measurement duration.
249        self.delay.delay_ms(self.measurement_time_ms(res)).await;
250
251        let raw = self.read_raw().await?;
252
253        Ok(self.raw_to_lux(raw, res))
254    }
255
256    async fn start_one_time_measurement(&mut self, res: Resolution) -> Result<(), Bh1750Error<E>> {
257        self.send_instruction(res.one_time_measurement_opcode())
258            .await
259    }
260
261    async fn read_raw(&mut self) -> Result<u16, E> {
262        let mut buf = [0u8; 2];
263        self.i2c.read(self.address as u8, &mut buf).await?;
264
265        Ok(u16::from_be_bytes(buf))
266    }
267
268    fn raw_to_lux(&self, raw: u16, res: Resolution) -> f32 {
269        // Convert the raw 16-bit reading to lux.
270        //
271        // Formula from BH1750 datasheet:
272        //   lux = (raw_value / 1.2) * (resolution_factor) * (MTreg / 69)
273        // where:
274        //   - 1.2 is a scaling constant defined by the sensor manufacturer,
275        //   - resolution_factor = 1.0, 0.5, or 4.0 depending on mode,
276        //   - current MTreg value.
277        f32::from(raw)
278            * res.default_resolution_lx_count()
279            * (f32::from(self.mtreg) / f32::from(DEFAULT_MTREG))
280            / 1.2
281    }
282
283    #[inline]
284    fn measurement_time_ms(&self, res: Resolution) -> u32 {
285        // Adjust measurement time according to the current MTreg value.
286        // The measurement time scales linearly with MTreg:
287        //   t = default_time * (MTreg / 69)
288        res.default_measurement_time_ms() * u32::from(self.mtreg) / u32::from(DEFAULT_MTREG)
289    }
290
291    #[inline]
292    async fn send_instruction(&mut self, instr: u8) -> Result<(), Bh1750Error<E>> {
293        self.i2c.write(self.address as u8, &[instr]).await?;
294
295        Ok(())
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302
303    extern crate std;
304    use std::vec;
305
306    use embedded_hal_mock::eh1::delay::NoopDelay;
307    use embedded_hal_mock::eh1::i2c::{Mock as I2cMock, Transaction as I2cTransaction};
308
309    fn raw_to_lux(raw: u16, res: Resolution, mtreg: u8) -> f32 {
310        f32::from(raw)
311            * res.default_resolution_lx_count()
312            * (f32::from(mtreg) / f32::from(DEFAULT_MTREG))
313            / 1.2
314    }
315
316    #[tokio::test]
317    async fn test_power_on() {
318        let expectations = [I2cTransaction::write(0x23, vec![0x01])]; // POWER_ON.
319
320        let i2c = I2cMock::new(&expectations);
321        let delay = NoopDelay::new();
322        let mut bh1750 = Bh1750::new(i2c, delay, Address::Low);
323
324        bh1750.power_on().await.unwrap();
325
326        bh1750.i2c.done();
327    }
328
329    #[tokio::test]
330    async fn test_power_down() {
331        let expectations = [I2cTransaction::write(0x23, vec![0x00])]; // POWER_DOWN.
332
333        let i2c = I2cMock::new(&expectations);
334        let delay = NoopDelay::new();
335        let mut bh1750 = Bh1750::new(i2c, delay, Address::Low);
336
337        bh1750.power_down().await.unwrap();
338
339        bh1750.i2c.done();
340    }
341
342    #[tokio::test]
343    async fn test_reset() {
344        let expectations = [I2cTransaction::write(0x23, vec![0x07])]; // RESET.
345
346        let i2c = I2cMock::new(&expectations);
347        let delay = NoopDelay::new();
348        let mut bh1750 = Bh1750::new(i2c, delay, Address::Low);
349
350        bh1750.reset().await.unwrap();
351
352        bh1750.i2c.done();
353    }
354
355    #[tokio::test]
356    async fn test_set_mtreg_clamping_high() {
357        // MTreg equal to 255 should be clamped to 254 (max).
358        let high = 0x40 | (254 >> 5);
359        let low = 0x60 | (254 & 0x1F);
360        let expectations = [
361            I2cTransaction::write(0x23, vec![high]),
362            I2cTransaction::write(0x23, vec![low]),
363        ];
364
365        let i2c = I2cMock::new(&expectations);
366        let delay = NoopDelay::new();
367        let mut bh1750 = Bh1750::new(i2c, delay, Address::Low);
368
369        bh1750.set_mtreg(255).await.unwrap();
370        assert_eq!(bh1750.mtreg, 254);
371
372        bh1750.i2c.done();
373    }
374
375    #[tokio::test]
376    async fn test_one_time_measurement() {
377        // One-time measurement opcode (High resolution): 0x20.
378        // Raw value read: 0x1234.
379        let expectations = [
380            I2cTransaction::write(0x23, vec![0x20]), // Start one-time.
381            I2cTransaction::read(0x23, vec![0x12, 0x34]),
382        ];
383
384        let i2c = I2cMock::new(&expectations);
385        let delay = NoopDelay::new();
386        let mut bh1750 = Bh1750::new(i2c, delay, Address::Low);
387
388        let lux = bh1750.one_time_measurement(Resolution::High).await.unwrap();
389        assert!((lux - raw_to_lux(0x1234, Resolution::High, DEFAULT_MTREG)).abs() < f32::EPSILON);
390        bh1750.i2c.done();
391    }
392
393    #[tokio::test]
394    async fn test_continuous_measurement_flow() {
395        // Start continuous measurement (High resolution): 0x10.
396        // Read value: 0x5678.
397        let expectations = [
398            I2cTransaction::write(0x23, vec![0x10]), // Start continuous.
399            I2cTransaction::read(0x23, vec![0x56, 0x78]),
400        ];
401
402        let i2c = I2cMock::new(&expectations);
403        let delay = NoopDelay::new();
404
405        let mut bh1750 = Bh1750::new(i2c, delay, Address::Low);
406        bh1750
407            .start_continuous_measurement(Resolution::High)
408            .await
409            .unwrap();
410
411        let lux = bh1750.read_continuous_measurement().await.unwrap();
412        assert!((lux - raw_to_lux(0x5678, Resolution::High, DEFAULT_MTREG)).abs() < f32::EPSILON);
413
414        bh1750.i2c.done();
415    }
416
417    #[tokio::test]
418    async fn test_continuous_measurement_error_if_not_started() {
419        let i2c = I2cMock::new(&[]);
420        let delay = NoopDelay::new();
421        let mut bh1750 = Bh1750::new(i2c, delay, Address::Low);
422
423        let err = bh1750.read_continuous_measurement().await.unwrap_err();
424        matches!(err, Bh1750Error::ContinuousMeasurementNotStarted);
425
426        bh1750.i2c.done();
427    }
428}