File Coverage

blib/lib/Device/Chip/BME280.pm
Criterion Covered Total %
statement 102 114 89.4
branch 9 12 75.0
condition 2 3 66.6
subroutine 19 21 90.4
pod 6 8 75.0
total 138 158 87.3


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