File Coverage

blib/lib/Device/Chip/PMS5003.pm
Criterion Covered Total %
statement 57 62 91.9
branch 8 18 44.4
condition 1 3 33.3
subroutine 12 13 92.3
pod 1 3 33.3
total 79 99 79.8


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, 2023 -- leonerd@leonerd.org.uk
5              
6 3     3   737724 use v5.26;
  3         9  
7 3     3   17 use warnings;
  3         5  
  3         130  
8 3     3   13 use utf8;
  3         5  
  3         19  
9              
10 3     3   762 use Object::Pad 0.800;
  3         7412  
  3         103  
11              
12             package Device::Chip::PMS5003 0.01;
13             class Device::Chip::PMS5003
14 1     1   595 :isa( Device::Chip );
  1         17913  
  1         127  
15              
16 3     3   2068 use Device::Chip::Sensor 0.23 -declare;
  3         7779  
  3         12  
17              
18 3     3   441 use Future::AsyncAwait;
  3         4  
  3         10  
19              
20 3     3   151 use constant PROTOCOL => "UART";
  3         3  
  3         5605  
21              
22             =head1 NAME
23              
24             C - chip driver for F
25              
26             =head1 SYNOPSIS
27              
28             use Device::Chip::PMS5003;
29             use Future::AsyncAwait;
30              
31             my $chip = Device::Chip::PMS5003->new;
32             await $chip->mount( Device::Chip::Adapter::...->new );
33              
34             $chip->start;
35              
36             my $readings = await $chip->read_all;
37              
38             printf "Particulate matter readings are %d / %d / %d\n",
39             @{$readings->{concentration}}{qw( pm1 pm2_5 pm10 )};
40              
41             =head1 DESCRIPTION
42              
43             This L subclass provides specific communication to a
44             F F particle concentration sensor attached to a computer
45             via a UART adapter. (Though if the communication protocol is the same, it is
46             likely also useful for a variety of other related sensors too).
47              
48             The reader is presumed to be familiar with the general operation of this chip;
49             the documentation here will not attempt to explain or define chip-specific
50             concepts or features, only the use of this module to access them.
51              
52             =cut
53              
54             =head1 METHODS
55              
56             The following methods documented in an C expression return L
57             instances.
58              
59             =cut
60              
61             method UART_options
62             {
63             return
64             baudrate => 9600;
65             }
66              
67             async method _loop
68 2     2   6 {
69 2         14 my $buf = "";
70 2         4 while(1) {
71             # A complete notification is 32bytes long
72 4         52 $buf .= await $self->protocol->read( 32 - length $buf );
73              
74             # A notification begins "\x42\x4D"
75 2 50       338 $buf =~ s{.*(?=\x42\x4D)}{}s or next;
76              
77             # A notification should be header, length, data
78 2         11 my ( $sof, $len ) = unpack( "a2 s>", $buf );
79             # Buffer length should be 28
80 2 50       4 $len == 28 or goto next_packet;
81 2 50       7 4 + length $buf >= $len or next;
82              
83 2         7 my @data = unpack( "s>*", substr $buf, 4, 28 );
84 2         4 my $got_checksum = pop @data;
85              
86 2         3 my $want_checksum = 0;
87 2         22 $want_checksum += ord for split m//, substr $buf, 0, 30;
88              
89 2 50       7 if( $got_checksum != $want_checksum ) {
90             # Checksum failed
91 0         0 goto next_packet;
92             }
93              
94 2         4 substr( $buf, 0, 32 ) = "";
95              
96 2         9 $self->on_data( @data );
97              
98 2         2 next;
99              
100 0         0 next_packet:
101             # It's possible the header we found was not a real header but in fact
102             # spurious data in the middle of packet.
103             substr( $buf, 0, 2 ) = "";
104              
105             # Trim down to the next plausible start
106 0 0       0 $buf =~ s{^.*\x42\x4D}{}s or $buf = "";
107 0         0 next;
108             }
109             }
110              
111             field $_reading_count = 0;
112             field $_latest_reading;
113             field $_next_reading_f;
114              
115 2     2 0 3 method on_data ( @data )
  2         7  
  2         4  
  2         3  
116             {
117             # From observation, the chip outputs a reading every second but only
118             # updates its values every 3. We'll therefore ignore the next two
119 2 50       7 if( !$_reading_count ) {
120 2         21 $_latest_reading = {
121             concentration => {
122             pm1 => $data[0],
123             pm2_5 => $data[1],
124             pm10 => $data[2],
125             },
126             atmos => {
127             pm1 => $data[3],
128             pm2_5 => $data[4],
129             pm10 => $data[5],
130             },
131             particles => {
132             pm0_3 => $data[6],
133             pm0_5 => $data[7],
134             pm1 => $data[8],
135             pm2_5 => $data[9],
136             pm5 => $data[10],
137             pm10 => $data[11],
138             }
139             };
140              
141 2         3 my $f = $_next_reading_f; undef $_next_reading_f;
  2         3  
142 2 50       8 $f->done if $f;
143             }
144              
145 2         178 $_reading_count++;
146 2 50       7 $_reading_count = 0 if $_reading_count > 2;
147             }
148              
149             =head2 start
150              
151             $chip->start;
152              
153             Begins the UART reading loop. This must be called before you can use
154             L.
155              
156             =cut
157              
158             field $_run_f;
159             method start
160             {
161             $_run_f = $self->_loop
162 0     0   0 ->on_fail( sub { warn "TODO: run loop stalled: ", @_ } );
163             }
164              
165             async method initialize_sensors
166 1     1 0 3573 {
167 1         5 $self->start;
168             }
169              
170             =head2 read_all
171              
172             $readings = await $chip->read_all;
173              
174             Waits for the next report packet from the sensor, then returns the readings
175             contained in it. This is in the form of a two-level hash:
176              
177             concentration => HASH # containing pm1, pm2_5, pm10
178              
179             atmost => HASH # containing pm1, pm2_5, pm10
180              
181             particles => HASH # containing pm0_3, pm0_5, pm1, pm2_5, pm5, pm10
182              
183             =cut
184              
185             async method read_all
186 2     2 1 190 {
187 2 50       7 $_run_f or
188             die "Must call ->start before you can ->read_all";
189              
190 2   33     11 my $f = $_next_reading_f //= $_run_f->new;
191              
192 2         15 await $f;
193              
194 2         112 return $_latest_reading;
195             }
196              
197             # TODO: Ideally this should be one parametric sensor, not three separate ones
198             declare_sensor pm1 =>
199             method => async method { return ( await $self->read_all )->{concentration}{pm1} },
200             units => "µg/m³",
201             precision => 0;
202              
203             declare_sensor pm2_5 =>
204             method => async method { return ( await $self->read_all )->{concentration}{pm2_5} },
205             units => "µg/m³",
206             precision => 0;
207              
208             declare_sensor pm10 =>
209             method => async method { return ( await $self->read_all )->{concentration}{pm10} },
210             units => "µg/m³",
211             precision => 0;
212              
213             =head1 AUTHOR
214              
215             Paul Evans
216              
217             =cut
218              
219             0x55AA;