File Coverage

blib/lib/Device/Chip/BME280.pm
Criterion Covered Total %
statement 105 117 89.7
branch 9 12 75.0
condition 2 3 66.6
subroutine 20 22 90.9
pod 6 8 75.0
total 142 162 87.6


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