File Coverage

blib/lib/Device/Chip/BME280.pm
Criterion Covered Total %
statement 106 118 89.8
branch 10 14 71.4
condition 1 2 50.0
subroutine 19 21 90.4
pod 6 8 75.0
total 142 163 87.1


line stmt bran cond sub pod time code
1             # You may distribute under the terms of either the GNU General Public License
2             # or the Artistic License (the same terms as Perl itself)
3             #
4             # (C) Paul Evans, 2020-2024 -- leonerd@leonerd.org.uk
5              
6 5     5   1863243 use v5.26;
  5         21  
7 5     5   42 use warnings;
  5         11  
  5         419  
8 5     5   615 use Object::Pad 0.800;
  5         8146  
  5         337  
9              
10             package Device::Chip::BME280 0.07;
11             class Device::Chip::BME280
12 5     5   4224 :isa(Device::Chip::Base::RegisteredI2C);
  5         46520  
  5         888  
13              
14 5     5   3975 use Device::Chip::Sensor 0.23 -declare;
  5         21849  
  5         32  
15              
16 5     5   4340 use Data::Bitfield qw( bitfield enumfield boolfield );
  5         20285  
  5         906  
17 5     5   53 use Future::AsyncAwait;
  5         57  
  5         63  
18              
19 5     5   397 use utf8;
  5         15  
  5         46  
20              
21             =encoding UTF-8
22              
23             =head1 NAME
24              
25             C - chip driver for F
26              
27             =head1 SYNOPSIS
28              
29             use Device::Chip::BME280;
30             use Future::AsyncAwait;
31              
32             my $chip = Device::Chip::BME280->new;
33             await $chip->mount( Device::Chip::Adapter::...->new );
34              
35             await $chip->change_config(
36             OSRS_H => 4,
37             OSRS_P => 4,
38             OSRS_T => 4,
39             MODE => "NORMAL",
40             );
41              
42             my ( $pressure, $temperature, $humidity ) = await $chip->read_sensor;
43              
44             printf "Temperature=%.2fC ", $temperature;
45             printf "Pressure=%dPa ", $pressure;
46             printf "Humidity=%.2f%%\n", $humidity;
47              
48             =head1 DESCRIPTION
49              
50             This L subclass provides specific communication to a F
51             F attached to a computer via an I²C adapter.
52              
53             The reader is presumed to be familiar with the general operation of this chip;
54             the documentation here will not attempt to explain or define chip-specific
55             concepts or features, only the use of this module to access them.
56              
57             =cut
58              
59             =head1 MOUNT PARAMETERS
60              
61             =head2 addr
62              
63             The I²C address of the device. Can be specified in decimal, octal or hex with
64             leading C<0> or C<0x> prefixes.
65              
66             =cut
67              
68 4     4 0 2268 method I2C_options ( %params )
  4         15  
  4         13  
  4         9  
69             {
70 4   50     42 my $addr = delete $params{addr} // 0x76;
71 4 50       31 $addr = oct $addr if $addr =~ m/^0/;
72              
73             return (
74 4         49 addr => $addr,
75             max_bitrate => 400E3,
76             );
77             }
78              
79             use constant {
80 5         27923 REG_DIG_T1 => 0x88,
81             REG_DIG_P1 => 0x8E,
82             REG_DIG_H1 => 0xA1,
83             REG_ID => 0xD0,
84             REG_RESET => 0xE0,
85             REG_DIG_H2 => 0xE1,
86             REG_CTRL_HUM => 0xF2,
87             REG_STATUS => 0xF3,
88             REG_CTRL_MEAS => 0xF4,
89             REG_CONFIG => 0xF5,
90             REG_PRESS => 0xF7, # 24bit
91             REG_TEMP => 0xFA, # 24bit
92             REG_HUM => 0xFD,
93 5     5   2255 };
  5         16  
94              
95             bitfield { format => "bytes-LE" }, config =>
96             # REG_CTRL_HUM
97             OSRS_H => enumfield( 0, qw( SKIP 1 2 4 8 16 16 16 ) ),
98             # REG_CTRL_MEAS
99             MODE => enumfield( 2*8+0, qw( SLEEP FORCED FORCED NORMAL ) ),
100             OSRS_P => enumfield( 2*8+2, qw( SKIP 1 2 4 8 16 16 16 ) ),
101             OSRS_T => enumfield( 2*8+5, qw( SKIP 1 2 4 8 16 16 16 ) ),
102             # REG_CONFIG
103             SPI3W_EN => boolfield( 3*8+0 ),
104             FILTER => enumfield( 3*8+2, qw( OFF 2 4 8 16 16 16 16 ) ),
105             T_SB => enumfield( 3*8+5, qw( 0.5 62.5 125 250 500 1000 10 20 ) );
106              
107             =head1 METHODS
108              
109             The following methods documented in an C expression return L
110             instances.
111              
112             =cut
113              
114             =head2 read_id
115              
116             $id = await $chip->read_id;
117              
118             Returns the chip ID.
119              
120             =cut
121              
122             async method read_id
123 1     1 1 1694 {
124 1         13 return unpack "C", await $self->read_reg( REG_ID, 1 );
125             }
126              
127             =head2 read_config
128              
129             $config = await $chip->read_config;
130              
131             Returns a C reference containing the chip config, using fields named
132             from the data sheet.
133              
134             FILTER => OFF | 2 | 4 | 8 | 16
135             MODE => SLEEP | FORCED | NORMAL
136             OSRS_H => SKIP | 1 | 2 | 4 | 8 | 16
137             OSRS_P => SKIP | 1 | 2 | 4 | 8 | 16
138             OSRS_T => SKIP | 1 | 2 | 4 | 8 | 16
139             SPI3W_EN => 0 | 1
140             T_SB => 0.5 | 10 | 20 | 62.5 | 125 | 250 | 500 | 1000
141              
142             =cut
143              
144 5     5 1 14871 async method read_config ()
  5         39  
  5         12  
145 5         15 {
146 5         48 my $bytes = await $self->cached_read_reg( REG_CTRL_HUM, 4 );
147              
148 5         25529 return { unpack_config( $bytes ) };
149             }
150              
151             =head2 change_config
152              
153             await $chip->change_config( %changes );
154              
155             Writes updates to the configuration registers.
156              
157             Note that these two methods use a cache of configuration bytes to make
158             subsequent modifications more efficient.
159              
160             =cut
161              
162 1     1 1 5226 async method change_config ( %changes )
  1         7  
  1         8  
  1         3  
163 1         4 {
164 1         9 my $config = await $self->read_config;
165              
166 1         193 my $bytes = pack_config( %$config, %changes );
167              
168             # Don't write REG_STATUS
169 1         240 await $self->cached_write_reg( REG_CTRL_HUM, substr( $bytes, 0, 1 ) );
170 1         3031 await $self->cached_write_reg( REG_CTRL_MEAS, substr( $bytes, 2, 2 ) );
171             }
172              
173 0     0 0 0 async method initialize_sensors ()
  0         0  
  0         0  
174 0         0 {
175 0         0 await $self->change_config(
176             MODE => "NORMAL",
177             OSRS_H => 4,
178             OSRS_P => 4,
179             OSRS_T => 4,
180             FILTER => 4,
181             );
182              
183             # First read after startup contains junk values
184 0         0 await $self->read_sensor;
185             }
186              
187             =head2 read_status
188              
189             $status = await $chip->read_status;
190              
191             =cut
192              
193 0     0 1 0 async method read_status ()
  0         0  
  0         0  
194 0         0 {
195 0         0 my $byte = await $self->read_reg( REG_STATUS, 1 );
196              
197             return {
198 0         0 MEASURING => !!( $byte & (1<<3) ),
199             IM_UPDATE => !!( $byte & (1<<0) ),
200             };
201             }
202              
203             =head2 read_raw
204              
205             ( $adc_P, $adc_T, $adc_H ) = await $chip->read_raw;
206              
207             Returns three integers containing the raw ADC reading values from the sensor.
208              
209             This method is mostly for testing or internal purposes only. For converted
210             sensor readings in real-world units you want to use L.
211              
212             =cut
213              
214 8     8 1 1860 async method read_raw ()
  8         25  
  8         47  
215 8         16 {
216 8         52 my ( $bytesP, $bytesT, $bytesH ) = unpack "a3 a3 a2",
217             await $self->read_reg( REG_PRESS, 8 );
218              
219             return (
220 7         20174 unpack( "L>", "\x00" . $bytesP ) >> 4,
221             unpack( "L>", "\x00" . $bytesT ) >> 4,
222             unpack( "S>", $bytesH ),
223             );
224             }
225              
226             # Compensation formulae directly from BME280 datasheet section 8.1
227              
228             field $_t_fine;
229              
230             field @_dig_T;
231              
232 10     10   22 async method _compensate_temperature ( $adc_T )
  10         40  
  10         20  
  10         13  
233 10         20 {
234 10 100       64 @_dig_T or
235             @_dig_T = ( undef, unpack "S< s< s<", await $self->read_reg( REG_DIG_T1, 6 ) );
236              
237 10         4414 my $var1 = ($adc_T / 16384 - $_dig_T[1] / 1024) * $_dig_T[2];
238 10         70 my $var2 = ($adc_T / 131072 - $_dig_T[1] / 8192) ** 2 * $_dig_T[3];
239              
240 10         58 $_t_fine = int( $var1 + $var2 );
241 10         22 my $T = ( $var1 + $var2 ) / 5120.0;
242 10         103 return $T;
243             }
244              
245             field @_dig_P;
246              
247 4     4   9 async method _compensate_pressure ( $adc_P )
  4         16  
  4         8  
  4         6  
248 4         26 {
249 4 100       24 @_dig_P or
250             @_dig_P = ( undef, unpack "S< s< s< s< s< s< s< s< s<", await $self->read_reg( REG_DIG_P1, 18 ) );
251              
252 4         4373 my $var1 = ($_t_fine / 2) - 64000;
253 4         14 my $var2 = $var1 * $var1 * $_dig_P[6] / 32768;
254 4         13 $var2 = $var2 + $var1 * $_dig_P[5] * 2;
255 4         13 $var2 = ($var2 / 4) + ($_dig_P[4] * 65536);
256 4         14 $var1 = ($_dig_P[3] * $var1 * $var1 / 524288 + $_dig_P[2] * $var1) / 524288;
257 4         11 $var1 = (1 + $var1 / 32768) * $_dig_P[1];
258 4 50       17 return 0 if $var1 == 0; # avoid exception caused by divide-by-zero
259 4         13 my $P = 1048576 - $adc_P;
260 4         35 $P = ($P - ($var2 / 4096)) * 6250 / $var1;
261 4         11 $var1 = $_dig_P[9] * $P * $P / 2147483648;
262 4         28 $var2 = $P * $_dig_P[8] / 32768;
263 4         13 $P = $P + ($var1 + $var2 + $_dig_P[7]) / 16;
264 4         28 return $P;
265             }
266              
267             field @_dig_H;
268              
269 4     4   136 async method _compensate_humidity ( $adc_H )
  4         14  
  4         9  
  4         6  
270 4         13 {
271 4 100       18 unless( @_dig_H ) {
272 2         15 @_dig_H = (
273             undef,
274             unpack( "C", await $self->read_reg( REG_DIG_H1, 1 ) ),
275             unpack( "s< C ccc c", await $self->read_reg( REG_DIG_H2, 7 ) ),
276             );
277             # Reshape the two 12bit values
278 2         8282 my ( $b0, $b1, $b2 ) = splice @_dig_H, 4, 3;
279 2         15 splice @_dig_H, 4, 0,
280             ( $b0 << 4 | $b1 & 0x0F ), # H4
281             ( $b1 >> 4 | $b2 << 4 ); # H5
282             }
283              
284 4         11 my $var_H = $_t_fine - 76800;
285 4         60 $var_H = ($adc_H - ($_dig_H[4] * 64.0 + $_dig_H[5] / 16384.0 * $var_H)) *
286             ($_dig_H[2] / 65536.0 * (1.0 + $_dig_H[6] / 67108864.0 * $var_H * (1.0 + $_dig_H[3] / 67108864.0 * $var_H)));
287 4         14 $var_H = $var_H * (1.0 - $_dig_H[1] * $var_H / 524288.0);
288              
289 4 50       15 return 0 if $var_H < 0;
290 4 50       61 return 100 if $var_H > 100;
291 4         26 return $var_H;
292             }
293              
294             =head2 read_sensor
295              
296             ( $pressure, $temperature, $humidity ) = await $chip->read_sensor;
297              
298             Returns the sensor readings appropriately converted into units of Pascals for
299             pressure, degrees Celcius for temperature, and percentage relative for
300             humidity.
301              
302             =cut
303              
304             async method read_sensor
305 1     1 1 7476 {
306 1         6 my ( $adc_P, $adc_T, $adc_H ) = await $self->read_raw;
307              
308             # Must do temperature first
309 1         100 my $T = await $self->_compensate_temperature( $adc_T );
310              
311             return (
312 1         105 await $self->_compensate_pressure( $adc_P ),
313             $T,
314             await $self->_compensate_humidity( $adc_H ),
315             );
316             }
317              
318             field $_pending_read_f;
319              
320             method _next_read
321             {
322             return $_pending_read_f //=
323 6     6   859 $self->read_raw->on_ready(sub { undef $_pending_read_f });
324             }
325              
326             declare_sensor pressure =>
327             method => async method {
328             my ( $rawP, $rawT, undef ) = await $self->_next_read;
329             $self->_compensate_temperature( $rawT );
330             return await $self->_compensate_pressure( $rawP );
331             },
332             units => "pascals",
333             sanity_bounds => [ 80_000, 120_000 ],
334             precision => 0;
335              
336             declare_sensor temperature =>
337             method => async method {
338             my ( undef, $rawT, undef ) = await $self->_next_read;
339             return await $self->_compensate_temperature( $rawT );
340             },
341             units => "°C",
342             sanity_bounds => [ -50, 80 ],
343             precision => 2;
344              
345             declare_sensor humidity =>
346             method => async method {
347             my ( undef, $rawT, $rawH ) = await $self->_next_read;
348             $self->_compensate_temperature( $rawT );
349             return await $self->_compensate_humidity( $rawH );
350             },
351             units => "%RH",
352             sanity_bounds => [ -1, 101 ], # give it slight headroom beyond the 0-100 range for rounding errors/etc
353             precision => 2;
354              
355             =head1 AUTHOR
356              
357             Paul Evans
358              
359             =cut
360              
361             0x55AA;