File Coverage

blib/lib/App/Device/Chip/sensor.pm
Criterion Covered Total %
statement 140 155 90.3
branch 19 28 67.8
condition 8 12 66.6
subroutine 24 27 88.8
pod 6 8 75.0
total 197 230 85.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-2021 -- leonerd@leonerd.org.uk
5              
6 6     6   442073 use v5.26;
  6         40  
7 6     6   521 use Object::Pad 0.19;
  6         8702  
  6         24  
8              
9             package App::Device::Chip::sensor 0.04;
10             class App::Device::Chip::sensor;
11              
12 6     6   1749 use Carp;
  6         10  
  6         365  
13              
14 6     6   2421 use Feature::Compat::Defer;
  6         1589  
  6         20  
15 6     6   7088 use Future::AsyncAwait;
  6         24523  
  6         27  
16              
17 6     6   2989 use Device::Chip::Adapter;
  6         25864  
  6         236  
18 6     6   2192 use Device::Chip::Sensor 0.19; # ->type
  6         10896  
  6         24  
19 6     6   1477 use Future::IO 0.08; # ->alarm
  6         26353  
  6         205  
20 6     6   3815 use Getopt::Long qw( GetOptionsFromArray );
  6         56477  
  6         22  
21 6     6   871 use List::Util qw( all max );
  6         13  
  6         546  
22 6     6   37 use Scalar::Util qw( refaddr );
  6         12  
  6         17887  
23              
24             =head1 NAME
25              
26             C - Base class to build C-based applications on
27              
28             =head1 SYNOPSIS
29              
30             #!/usr/bin/perl
31             use v5.26;
32              
33             use Object::Pad;
34             use Future::AsyncAwait;
35              
36             class App extends App::Device::Chip::sensor
37             {
38             method output_readings ( $now, $sensors, $values )
39             {
40             print "At time $now, we have some sensor values...\n";
41             }
42             }
43              
44             await App->new->parse_argv->run;
45              
46             =head1 DESCRIPTION
47              
48             This module provides a base class to assist in writing applications that
49             process data periodically from one or more L-based sensors, via
50             the L interface. A typical program using this module
51             would derive a subclass from it, provide the remaining methods as necessary,
52             and eventually call the L method to start the application.
53              
54             =cut
55              
56             =head1 COMMANDLINE OPTIONS
57              
58             The following commandline options are recognised by the base class and may be
59             used in addition to any defined by the actual application logic.
60              
61             =over 4
62              
63             =item * --blib, -b
64              
65             Uses the L module to add additional paths into C<@INC> to search for
66             more Perl modules. May be useful when testing chip drivers under development
67             without needing to install them.
68              
69             =item * --interval, -i TIME
70              
71             Specifies the time, in seconds, between every round of collecting sensor
72             readings and invoking the L method.
73              
74             Defaults to 10 seconds.
75              
76             =item * --adapter, -A STR
77              
78             Adapter configuration string to pass to L
79             to construct the chip adapter used for communication with the actual chip
80             hardware.
81              
82             =item * --mid3, -m
83              
84             Enable "middle-of-3" filtering of gauge values, to reduce sensor noise from
85             unreliable sensors. At each round of readings, the most recent three values
86             from the sensor are sorted numerically and the middle one is reported.
87              
88             =back
89              
90             =cut
91              
92             has @_CHIPCONFIGS;
93 2     2   8 method _chipconfigs { @_CHIPCONFIGS } # for unit testing
  2         10  
94              
95 3     3 0 15 has $_interval :reader = 10;
96              
97 3         13 has $_best_effort;
98              
99             has $_mid3;
100              
101             method OPTSPEC
102 8     8 1 27 {
103             return (
104 0     0   0 'b|blib' => sub { require blib; blib->import; },
  0         0  
105              
106 8         57 'i|interval=i' => \$_interval,
107              
108             'm|mid3' => \$_mid3,
109              
110             'B|best-effort' => \$_best_effort,
111             );
112             }
113              
114             =head1 PROVIDED METHODS
115              
116             The following methods are provided on the base class, intended for subclasses
117             or applications to invoke.
118              
119             =cut
120              
121             =head2 parse_argv
122              
123             $app->parse_argv()
124             $app->parse_argv( \@argv )
125              
126             Provides a list of commandline arguments for parsing, either from a given
127             array reference or defaulting to the process C<@ARGV> if not supplied.
128              
129             This uses L to collect the defined arguments, whose references
130             should handle the results.
131              
132             =cut
133              
134 8         14 method parse_argv ( $argv = \@ARGV )
  8         13  
  8         13  
135 8     8 1 18483 {
136 8         29 my %optspec = $self->OPTSPEC;
137              
138 8         25 @_CHIPCONFIGS = ();
139              
140 8         15 my $ADAPTERDESC; my $adapter;
141              
142             GetOptionsFromArray( $argv, %optspec,
143             'adapter|A=s' => sub {
144 5     5   2653 $ADAPTERDESC = $_[1];
145 5         15 undef $adapter;
146             },
147             '<>' => sub {
148 9     9   2009 my ( $chiptype, $opts ) = split m/:/, $_[0], 2;
149              
150 9   66     73 $adapter //= Device::Chip::Adapter->new_from_description( $ADAPTERDESC );
151              
152 9         534 my $config = {
153             type => $chiptype,
154             adapter => $adapter,
155             };
156              
157 9         51 while( length $opts ) {
158 1 50       6 if( $opts =~ s/^-C:(.*?)=(.*)(?:$|,)// ) {
    0          
159 1         5 $config->{config}{$1} = $2;
160             }
161             elsif( $opts =~ s/^-M:(.*?)=(.*)(?:$|,)// ) {
162 0         0 $config->{mountopts}{$1} = $2;
163             }
164             else {
165 0         0 croak "Unable to parse chip configuration options '$opts' for $chiptype'";
166             }
167             }
168              
169 9         30 push @_CHIPCONFIGS, $config;
170             },
171 8 50       63 ) or exit 1;
172              
173 8         210 return $self;
174             }
175              
176             =head2 chips
177              
178             @chips = await $app->chips;
179              
180             An asynchronous memoized lazy accessor for the list of L
181             instances, whose class names are taken from the remaining commandline
182             arguments after the options are parsed.
183              
184             =cut
185              
186             has $_chips; # arrayref
187             async method chips
188 9         16 {
189 9 100       44 return @$_chips if $_chips;
190              
191 4         11 foreach my $chipconfig ( @_CHIPCONFIGS ) {
192 4         8 my $chiptype = $chipconfig->{type};
193 4         8 my $adapter = $chipconfig->{adapter};
194              
195 4         10 my $class = "Device::Chip::$chiptype";
196              
197 4         32 require ( "$class.pm" ) =~ s(::)(/)gr;
198              
199 4         34 my $chip = $class->new;
200              
201 4         66 my %mountopts;
202 4 50       13 %mountopts = $chipconfig->{mountopts}->%* if $chipconfig->{mountopts};
203              
204 4         21 await $chip->mount( $adapter, %mountopts );
205              
206 4 50       539 if( $chipconfig->{config} ) {
207 0         0 await $chip->change_config( $chipconfig->{config}->%* );
208             }
209              
210 4         60 await $chip->protocol->power(1);
211              
212 4 100       186 if( $chip->can( "initialize_sensors" ) ) {
213 1         3 await $chip->initialize_sensors;
214             }
215              
216 4         44 push @$_chips, $chip;
217             }
218              
219 4         24 return @$_chips;
220 9     9 1 20 }
221              
222             =head2 chips
223              
224             @sensors = await $app->sensors;
225              
226             An asynchronous memoized lazy accessor for the list of L
227             instances of each of the configured chips (from the L method).
228              
229             =cut
230              
231             has $_sensors; # arrayref
232              
233             has $_chipname_width;
234             has $_sensorname_width;
235              
236 6     6   12 sub _chipname ( $chip ) { return ( ref $chip ) =~ s/^Device::Chip:://r }
  6         9  
  6         8  
  6         43  
237              
238             async method sensors
239 5         15 {
240 5 100       20 return @$_sensors if $_sensors;
241              
242 4         11 @$_sensors = map { $_->list_sensors } await $self->chips;
  4         129  
243              
244 4         246 $_chipname_width = max map { length _chipname $_ } @$_chips;
  4         12  
245 4         20 $_sensorname_width = max map { length $_->name } @$_sensors;
  5         33  
246              
247 4         40 await $self->after_sensors( @$_sensors );
248              
249 4         130 return @$_sensors;
250 5     5 0 1378 }
251              
252 3     3 1 6 async method after_sensors ( @sensors ) { }
  3         6  
  3         5  
  3         6  
  3         15  
253              
254             =head2 run
255              
256             await $app->run;
257              
258             An asynchronous method which performs the actual run loop of the sensor
259             application. This implements the main application logic, of regular collection
260             of values from all of the sensor instances and reporting them to the
261             L method.
262              
263             In normal circumstances the L instance returned by this method would
264             remain pending for the lifetime of the program, and not complete. For an
265             application that has nothing else to perform concurrently it can simply
266             C this future to run the logic. If it has other logic to perform as
267             well it could combine this with other futures using a C<< Future->needs_all >>
268             or similar techniques.
269              
270             =cut
271              
272 4         5 async method run ()
  4         7  
273 4         13 {
274 4         16 my @chips = await $self->chips;
275              
276 0     0   0 $SIG{INT} = $SIG{TERM} = sub { exit 1; };
  4         226  
277              
278 4         18 defer {
279             $chips[0] and $chips[0]->protocol->power(0)->get;
280             }
281              
282 4         19 my @sensors = await $self->sensors;
283              
284 4         111 my %readings_by_chip;
285              
286 4         12 my $waittime = Time::HiRes::time();
287 4         7 while(1) {
288             # Read concurrently
289 8         476 my $now = Time::HiRes::time();
290              
291             my @values = await Future->needs_all(
292             map {
293 8         15 my $sensor = $_;
294             my $f = $sensor->read;
295 0     0   0 $f = $f->else( async sub ($failure, @) {
  0         0  
  0         0  
  0         0  
296 0         0 my $sensorname = $sensor->name;
297 0         0 my $chipname = ref ( $sensor->chip );
298 0         0 warn "Unable to read $sensorname of $chipname: $failure";
299 0         0 return undef;
300             } ) if $_best_effort;
301             $f;
302             } @sensors
303             );
304              
305 8 100       2868 if( $_mid3 ) {
306 3         7 foreach my $idx ( 0 .. $#sensors ) {
307 3         7 my $sensor = $sensors[$idx];
308 3         4 my $value = $values[$idx];
309              
310 3 50       8 next unless $sensor->type eq "gauge";
311              
312             # Accumulate the past 3 readings
313 3   100     38 my $readings = $readings_by_chip{ refaddr $sensor } //= [];
314 3         7 push @$readings, $value;
315 3         6 shift @$readings while @$readings > 3;
316              
317             # Take the middle of the 3
318 3 100 66 3   7 if( @$readings == 3 and all { defined } @$readings ) {
  3         15  
319 1         4 my @sorted = sort { $a <=> $b } @$readings;
  3         6  
320 1         4 $values[$idx] = $sorted[1];
321             }
322             }
323             }
324              
325 8         31 $self->output_readings( $now, \@sensors, \@values );
326              
327 8         308 $waittime += $_interval;
328 8         28 await Future::IO->alarm( $waittime );
329             }
330 4     4 1 1329 }
331              
332             =head2 print_readings
333              
334             $app->print_readings( $sensors, $values )
335              
336             Prints the sensor names and current readings in a human-readable format to the
337             currently-selected output handle (usually C).
338              
339             =cut
340              
341 1         1 method print_readings ( $sensors, $values )
  1         2  
  1         2  
  1         26  
342 1     1 1 13 {
343 1         5 foreach my $i ( 0 .. $#$sensors ) {
344 2         12 my $sensor = $sensors->[$i];
345 2         3 my $value = $values->[$i];
346              
347 2         13 my $chip = $sensor->chip;
348 2         12 my $chipname = _chipname $chip;
349              
350 2         5 my $units = $sensor->units;
351 2 50       10 $units = " $units" if defined $units;
352              
353 2         3 my $valuestr;
354 2 50       7 if( !defined $value ) {
    100          
355 0         0 $valuestr = "";
356             }
357             elsif( $sensor->type eq "gauge" ) {
358 1   50     9 $valuestr = sprintf "%s%s", $sensor->format( $value ), $units // "";
359             }
360             else {
361 1   50     12 $valuestr = sprintf "%s%s/sec", $sensor->format( $value / $self->interval ), $units // "";
362             }
363              
364 2         32 printf "% *s/% *s: %s\n",
365             $_chipname_width, $chipname, $_sensorname_width, $sensor->name, $valuestr;
366             }
367             }
368              
369             =head1 REQUIRED METHODS
370              
371             This base class itself is incomplete, requiring the following methods to be
372             provided by an implementing subclass to contain the actual application logic.
373              
374             =cut
375              
376             =head2 output_readings
377              
378             $app->output_readings( $now, $sensors, $values );
379              
380             This method is invoked regularly by the L method, to provide the
381             application with the latest round of sensor readings. It is passed the current
382             UNIX epoch timestamp as C<$now>, an array reference containing the individual
383             L instances as C<$sensors>, and a congruent array
384             reference containing the most recent readings taken from them, as plain
385             numbers.
386              
387             The application should put the bulk of its processing logic in here, for
388             example writing the values to some sort of file or database, displaying them
389             in some form, or whatever else the application is supposed to do.
390              
391             =cut
392              
393             =head1 OVERRIDABLE METHODS
394              
395             The base class provides the following methods, but it is expected that
396             applications may wish to override them to customise the logic contained in
397             them.
398              
399             If using L to do so, don't forget to provide the C<:override>
400             method attribute.
401              
402             =cut
403              
404             =head2 OPTSPEC
405              
406             %optspec = $app->OPTSPEC;
407              
408             This method is invoked by the L method to construct a definition
409             of the commandline options understood by the program. These are returned in a
410             key/value list to be processed by L. If the application wishes
411             to parse additional arguments it should override this method, call the
412             superclass version, and append any extra argument specifications it requires.
413              
414             As this is invoked as a regular instance method, a convenient way to store the
415             parsed values is to pass references to instance slot variables created by the
416             L C keyword:
417              
418             has $_title;
419             has $_bgcol = "#cccccc";
420              
421             method OPTSPEC :override
422             {
423             return ( $self->SUPER::OPTSPEC,
424             'title=s' => \$_title,
425             'background-color=s' => \$_bgcol,
426             );
427             }
428              
429             =cut
430              
431             =head2 after_sensors
432              
433             await $app->after_sensors( @sensors )
434              
435             This method is invoked once on startup by the L method, after it has
436             configured the chip adapter and chips and obtained their individual sensor
437             instances. The application may wish to perform one-time startup tasks in here,
438             such as creating database files with knowledge of the specific sensor data
439             types, or other such behaviours.
440              
441             =cut
442              
443             =head1 AUTHOR
444              
445             Paul Evans
446              
447             =cut
448              
449             0x55AA;