File Coverage

blib/lib/Device/SDS011.pm
Criterion Covered Total %
statement 20 135 14.8
branch 0 40 0.0
condition 0 17 0.0
subroutine 7 23 30.4
pod 9 9 100.0
total 36 224 16.0


line stmt bran cond sub pod time code
1             package Device::SDS011;
2              
3             # Last updated November 10, 2019
4             #
5             # Author: Irakliy Sunguryan ( www.sochi-travel.info )
6             # Date Created: September 25, 2019
7              
8             ##############################################################################
9             # NOTE 1: All functions will save/update the Device ID,
10             # since all commands return it anyway.
11             ##############################################################################
12              
13 1     1   65614 use v5.10; # for "Defined OR" operator
  1         4  
14 1     1   5 use strict;
  1         2  
  1         19  
15 1     1   5 use warnings;
  1         1  
  1         38  
16              
17 1     1   6 use vars qw($VERSION);
  1         8  
  1         56  
18             $VERSION = '0.01';
19              
20 1     1   833 use Device::SerialPort;
  1         30048  
  1         57  
21 1     1   10 use List::Util 'sum';
  1         2  
  1         155  
22              
23             # =======================================================
24              
25             use constant {
26 1         1662 CMD_DATA => "\xC0",
27             CMD_REPLY => "\xC5",
28             MODE_SLEEP => 0,
29             MODE_WORK => 1,
30             #---
31             REQ_TEMPLATE => [
32             0xAA,0xB4,0x00, # header, command, instruction
33             0x00,0x00,0x00,0x00, # data
34             0x00,0x00,0x00,0x00,
35             0x00,0x00,0x00,0x00,
36             0xFF,0xFF,0x00,0xAB, # device id, checksum, tail
37             ],
38             #---
39             CMD_BYTE_REPORTING_MODE => 2,
40             CMD_BYTE_QUERY_DATA => 4,
41             CMD_BYTE_DEVICE_ID => 5,
42             CMD_BYTE_SLEEP_WORK => 6,
43             CMD_BYTE_WORKING_PERIOD => 8,
44             CMD_BYTE_FIRMWARE => 7,
45             #---
46             MAX_MSGS_READ => 10,
47             # when sensor is in "continuous" working mode (default),
48             # several data reading messages can appear before actual response to a command.
49 1     1   6 };
  1         3  
50              
51             sub new {
52 0     0 1   my $class = shift;
53 0           my $serial_port = shift;
54              
55 0           my $self = {
56             _device_id => undef,
57             _reporting_mode => undef,
58             _operation_mode => undef,
59             _working_period => undef,
60             _firmware_verion => undef,
61             };
62              
63 0           $self->{port} = Device::SerialPort->new($serial_port);
64 0           $self->{port}->read_const_time(10_000); # 10 seconds timeout
65              
66             # The UART communication protocol:
67             # bit rate: 9600
68             # data bit: 8
69             # parity bit: NO
70             # stop bit: 1
71              
72 0           $self->{port}->baudrate(9600);
73 0           $self->{port}->databits(8);
74 0           $self->{port}->parity('none');
75 0           $self->{port}->stopbits(1);
76              
77 0 0         $self->{port}->write_settings || undef $self->{port};
78              
79 0           bless $self, $class;
80 0           return $self;
81             }
82              
83             sub _checksum {
84 0     0     my @data_bytes = @_;
85 0           return sum(@data_bytes) % 256;
86             }
87              
88             sub _read_serial {
89 0     0     my $self = shift;
90 0           my $cmdChar = shift; # C0 - sensor data; C5 - command reply
91 0           my $msg = '';
92 0           my $readMessages = 0;
93 0           my $failures = 0; # a way to stop the infinite loop in case read() will start to time out
94 0           $self->{port}->lookclear;
95 0           while(1) {
96 0           my $byte = $self->{port}->read(1);
97 0 0 0       if ( defined $byte and length $byte ) {
98 0           $msg .= $byte;
99 0           $msg = substr($msg,-10);
100 0 0 0       if (length($msg) == 10
      0        
101             && substr($msg,0,1) eq "\xAA"
102             && substr($msg,-1) eq "\xAB")
103             {
104 0           $readMessages++;
105 0 0         last if $cmdChar eq CMD_DATA;
106 0 0         last if $readMessages >= MAX_MSGS_READ; # give up after this many messages
107 0 0 0       last if $cmdChar && substr($msg,1,1) eq $cmdChar;
108             }
109             } else {
110 0           $failures++;
111 0 0         last if $failures == 5;
112             }
113             }
114 0 0 0       $msg = undef if $cmdChar && substr($msg,1,1) ne $cmdChar;
115 0           return $msg;
116             }
117              
118             sub _write_serial {
119 0     0     my $self = shift;
120 0           my $bytes = shift;
121 0           my $str = pack('C*', @$bytes);
122 0           $self->{port}->lookclear;
123 0           my $count_out = $self->{port}->write($str);
124             # $self->{port}->write_drain;
125 0 0         warn "write failed\n" unless $count_out;
126 0 0         warn "write incomplete\n" if $count_out != length($str);
127 0           return $count_out;
128             }
129              
130             # ACCEPTS: (1) [required] array ref of 15 data bytes (intergers)
131             # (2) [optional] expected response type: \xC0 (sensor data), or
132             # \xC5 (command reply) <- default
133             # RETURNS: a response (string of bytes)
134             sub _write_msg {
135 0     0     my $self = shift;
136 0           my ($data, $response_type) = @_;
137 0           my @out = @{REQ_TEMPLATE()};
  0            
138 0           @out[2..16] = @$data;
139 0           $out[17] = _checksum(@out[2..16]);
140 0           $self->_write_serial(\@out);
141 0   0       return $self->_read_serial(($response_type // CMD_REPLY));
142             }
143              
144             sub _update_device_id {
145 0     0     my $self = shift;
146 0           my $msg = shift; # full response message
147 0 0         if (!$self->{_device_id}) {
148 0           my @deviceId = map { ord } split //, substr($msg,6,2);
  0            
149 0           $self->{_device_id} = \@deviceId;
150             }
151             }
152              
153             # ---------------------------------------------------------------------------
154              
155             ##############################################################################
156             # RETURNS: Array ref of calculated sensor values: [PM25, PM10]
157             ##############################################################################
158             sub live_data {
159 0     0 1   my $self = shift;
160 0           my $response = $self->_read_serial(CMD_DATA);
161 0           my @values = map { ord } split //, $response;
  0            
162             return [
163 0           (($values[3] * 256) + $values[2]) / 10,
164             (($values[5] * 256) + $values[4]) / 10,
165             ];
166             }
167              
168             sub query_data {
169 0     0 1   my $self = shift;
170 0           my @out = @{REQ_TEMPLATE()}[2..16];
  0            
171 0           my $response = $self->_write_msg([CMD_BYTE_QUERY_DATA, @{REQ_TEMPLATE()}[3..16]], CMD_DATA);
  0            
172 0 0         if ($response) {
173 0           $self->_update_device_id($response);
174 0           my @values = map { ord } split //, $response;
  0            
175             return [
176 0           (($values[3] * 256) + $values[2]) / 10,
177             (($values[5] * 256) + $values[4]) / 10,
178             ];
179             } else {
180 0           return undef;
181             }
182             }
183              
184             sub _change_mode {
185 0     0     my $self = shift;
186 0           my ($mode_type, $mode_value) = @_;
187 0           my @out = @{REQ_TEMPLATE()}[2..16];
  0            
188 0           $out[0] = $mode_type;
189             # CMD_BYTE_REPORTING_MODE / CMD_BYTE_SLEEP_WORK / CMD_BYTE_WORKING_PERIOD
190 0 0         ($out[1], $out[2]) = defined($mode_value) ? (1,$mode_value) : (0,0);
191 0           my $response = $self->_write_msg(\@out);
192 0 0         $self->_update_device_id($response) if $response;
193 0 0         return ($response ? ord(substr($response,4,1)) : undef);
194             }
195              
196             ##############################################################################
197             # ACCEPTS: OPTIONAL Mode to set: 0=Report active mode, 1=Report query mode
198             # RETURNS: Current reporting mode
199             ##############################################################################
200             sub reporting_mode {
201 0     0 1   my $self = shift;
202 0           my $mode = shift;
203 0           return $self->_change_mode(CMD_BYTE_REPORTING_MODE, $mode);
204             }
205              
206             ##############################################################################
207             # ACCEPTS: OPTIONAL Mode to set: 0=Sleep, 1=Work
208             # RETURNS: Current mode
209             ##############################################################################
210             sub sensor_mode {
211 0     0 1   my $self = shift;
212 0           my $mode = shift;
213 0           return $self->_change_mode(CMD_BYTE_SLEEP_WORK, $mode);
214             }
215              
216             ##############################################################################
217             # ACCEPTS: OPTIONAL Mode/Period in minutes to set:
218             # 0=continuous mode, 1-30 minutes (work 30 seconds and sleep n*60-30 seconds)
219             # RETURNS: Current mode/Period in minutes
220             ##############################################################################
221             sub working_period {
222 0     0 1   my $self = shift;
223 0           my $minutes = shift;
224 0           return $self->_change_mode(CMD_BYTE_WORKING_PERIOD, $minutes);
225             }
226              
227             ##############################################################################
228             # RETURNS: Array ref [year, month, day] of the firmware version
229             # NOTE: This will only read the value from the device if it wasn't read before
230             ##############################################################################
231             sub firmware {
232 0     0 1   my $self = shift;
233 0 0         if (!$self->{_firmware_verion}) {
234 0           my $response = $self->_write_msg([CMD_BYTE_FIRMWARE, @{REQ_TEMPLATE()}[3..16]]);
  0            
235 0 0         if (defined $response) {
236 0           my @version = map { ord } split //, substr($response,3,3);
  0            
237             # Firmware version byte 1: year
238             # Firmware version byte 2: month
239             # Firmware version byte 3: day
240 0           $self->{_firmware_verion} = \@version;
241 0           $self->_update_device_id($response);
242             }
243             }
244             # TODO: question: if it was successfully read on previous call
245             # and the $self->{_firmware_verion} is set, should I undef it in case this read fails?
246 0           return $self->{_firmware_verion};
247             }
248              
249             sub device_id {
250 0     0 1   my $self = shift;
251 0           my @new_id = @_; # 2 bytes (integers)
252 0 0         if (@new_id) {
253 0           my @out = @{REQ_TEMPLATE()}[2..16];
  0            
254 0           $out[0] = CMD_BYTE_DEVICE_ID;
255 0           ($out[11], $out[12]) = @new_id;
256 0           my $response = $self->_write_msg(\@out);
257 0           $self->_update_device_id($response);
258             }
259             else {
260             # (ab)use reporing mode function to read and update the ID
261 0 0         $self->reporting_mode if (!$self->{_device_id});
262             }
263 0           return $self->{_device_id};
264             }
265              
266             sub done {
267 0     0 1   my $self = shift;
268 0           undef $self->{port};
269             }
270              
271             sub DESTROY {
272 0     0     my $self = shift;
273 0 0         undef $self->{port} if $self->{port};
274             }
275              
276             1;
277              
278             __END__