File Coverage

blib/lib/App/Device/Chip/sensor.pm
Criterion Covered Total %
statement 206 230 89.5
branch 28 40 70.0
condition 10 16 62.5
subroutine 39 45 86.6
pod 9 13 69.2
total 292 344 84.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, 2020-2024 -- leonerd@leonerd.org.uk
5              
6 7     7   2242257 use v5.26;
  7         33  
7 7     7   57 use warnings;
  7         24  
  7         533  
8 7     7   1506 use Object::Pad 0.800;
  7         13581  
  7         383  
9              
10             class App::Device::Chip::sensor 0.07;
11              
12 7     7   7567 use Carp;
  7         21  
  7         688  
13              
14 7     7   4461 use Feature::Compat::Defer;
  7         4074  
  7         32  
15 7     7   5019 use Feature::Compat::Try;
  7         3496  
  7         38  
16 7     7   2087 use Future::AsyncAwait;
  7         55076  
  7         75  
17 7     7   4940 use Sublike::Extended;
  7         5681  
  7         82  
18              
19 7     7   5875 use Device::Chip::Adapter;
  7         63995  
  7         525  
20 7     7   3833 use Device::Chip::Sensor 0.19; # ->type
  7         28417  
  7         44  
21 7     7   2733 use Future::IO 0.08; # ->alarm
  7         151118  
  7         491  
22 7     7   6080 use Getopt::Long qw( GetOptionsFromArray );
  7         161995  
  7         44  
23 7     7   2802 use List::Util 1.29 qw( max pairgrep );
  7         165  
  7         772  
24 7     7   55 use Scalar::Util qw( refaddr );
  7         63  
  7         52353  
25              
26             =head1 NAME
27              
28             C - Base class to build C-based applications on
29              
30             =head1 SYNOPSIS
31              
32             #!/usr/bin/perl
33             use v5.26;
34              
35             use Object::Pad;
36             use Future::AsyncAwait;
37              
38             class App extends App::Device::Chip::sensor
39             {
40             method output_readings ( $now, $sensors, $values )
41             {
42             print "At time $now, we have some sensor values...\n";
43             }
44             }
45              
46             await App->new->parse_argv->run;
47              
48             =head1 DESCRIPTION
49              
50             This module provides a base class to assist in writing applications that
51             process data periodically from one or more L-based sensors, via
52             the L interface. A typical program using this module
53             would derive a subclass from it, provide the remaining methods as necessary,
54             and eventually call the L method to start the application.
55              
56             =cut
57              
58             =head1 COMMANDLINE OPTIONS
59              
60             The following commandline options are recognised by the base class and may be
61             used in addition to any defined by the actual application logic.
62              
63             =over 4
64              
65             =item * --blib, -b
66              
67             Uses the L module to add additional paths into C<@INC> to search for
68             more Perl modules. May be useful when testing chip drivers under development
69             without needing to install them.
70              
71             =item * --interval, -i TIME
72              
73             Specifies the time, in seconds, between every round of collecting sensor
74             readings and invoking the L method.
75              
76             Defaults to 10 seconds.
77              
78             =item * --adapter, -A STR
79              
80             Adapter configuration string to pass to L
81             to construct the chip adapter used for communication with the actual chip
82             hardware.
83              
84             =item * --filter, -F STR
85              
86             Specifies the kind of filtering to apply to gauge values. See L
87             for more detail.
88              
89             =item * --mid3, -m
90              
91             Enable "middle-of-3" filtering of gauge values, to reduce sensor noise from
92             unreliable sensors. This is equivalent to setting C<-F mid3>.
93              
94             =item * --best-effort, -B
95              
96             Enables best-effort mode, which causes failures of sensor readings to be
97             ignored, reporting C instead. In this mode, the C
98             method may be invoked for failures; it can further refine what the behaviour
99             should be.
100              
101             =back
102              
103             =cut
104              
105             field @_CHIPCONFIGS;
106             method _chipconfigs { @_CHIPCONFIGS } # for unit testing
107              
108 3     3 0 27 field $_interval :mutator = 10;
109              
110 3     0 0 21 field $_best_effort :mutator;
  0         0  
  0         0  
111              
112 0     0 0 0 field $_filter :mutator;
  0         0  
113              
114             method OPTSPEC
115             {
116             return (
117 0     0   0 'b|blib' => sub { require blib; blib->import; },
  0         0  
118              
119             'i|interval=i' => \$_interval,
120              
121             'F|filter=s' => \$_filter,
122              
123 1     1   109 'm|mid3' => sub { $_filter = "mid3" },
124              
125             'B|best-effort' => \$_best_effort,
126             );
127             }
128              
129             =head1 PROVIDED METHODS
130              
131             The following methods are provided on the base class, intended for subclasses
132             or applications to invoke.
133              
134             =cut
135              
136             =head2 parse_argv
137              
138             $app->parse_argv();
139             $app->parse_argv( \@argv );
140              
141             Provides a list of commandline arguments for parsing, either from a given
142             array reference or defaulting to the process C<@ARGV> if not supplied.
143              
144             This uses L to collect the defined arguments, whose references
145             should handle the results.
146              
147             =cut
148              
149 11     11 1 14435 method parse_argv ( $argv = \@ARGV )
  11         44  
  11         26  
  11         15  
150             {
151 11         84 my %optspec = $self->OPTSPEC;
152              
153 11         45 @_CHIPCONFIGS = ();
154              
155 11         33 my $ADAPTERDESC; my $adapter;
156              
157             GetOptionsFromArray( $argv, %optspec,
158             'adapter|A=s' => sub {
159 8     8   9713 $ADAPTERDESC = $_[1];
160 8         86 undef $adapter;
161             },
162             '<>' => sub {
163 12     12   3042 my ( $chiptype, $opts ) = split m/:/, $_[0], 2;
164              
165 12   66     200 $adapter //= Device::Chip::Adapter->new_from_description( $ADAPTERDESC );
166              
167 12         918 my %config = (
168             type => $chiptype,
169             adapter => $adapter,
170             );
171              
172 12         51 while( length $opts ) {
173 1 50       8 if( $opts =~ s/^-C:(.*?)=(.*)(?:$|,)// ) {
    0          
174 1         4 $config{config}{$1} = $2;
175             }
176             elsif( $opts =~ s/^-M:(.*?)=(.*)(?:$|,)// ) {
177 0         0 $config{mountopts}{$1} = $2;
178             }
179             else {
180 0         0 croak "Unable to parse chip configuration options '$opts' for $chiptype'";
181             }
182             }
183              
184 12         80 $self->add_chip( %config );
185             },
186 11 50       150 ) or exit 1;
187              
188 11         455 return $self;
189             }
190              
191             =head2 add_chip
192              
193             $app->add_chip( %config );
194              
195             I
196              
197             Adds a new chip to the stored configuration, as if it had been given as a
198             commandline argument. Takes the following named arguments:
199              
200             =over 4
201              
202             =item type => STR
203              
204             Required string that gives the name of the chip class.
205              
206             =item adapter => Device::Chip::Adapter
207              
208             Required L instance.
209              
210             =item mountopts => HASH
211              
212             Optional HASH reference containing extra mount parameters.
213              
214             =item config => HASH
215              
216             Optional HASH reference containing extra chip configuration to set up using
217             the C method once mounted.
218              
219             =back
220              
221             =cut
222              
223 12     12 1 30 extended method add_chip ( :$type, :$adapter, %config )
  12         36  
  12         55  
  12         19  
224             {
225             push @_CHIPCONFIGS, {
226             type => $type,
227             adapter => $adapter,
228 12     24   163 pairgrep { defined $b } %config{qw( mountopts config )}
  24         154  
229             };
230             }
231              
232             =head2 chips
233              
234             @chips = await $app->chips;
235              
236             An asynchronous memoized lazy accessor for the list of L
237             instances, whose class names are taken from the remaining commandline
238             arguments after the options are parsed.
239              
240             =cut
241              
242             field $_chips; # arrayref
243             async method chips
244 16     16 1 104 {
245 16 100       194 return @$_chips if $_chips;
246              
247 7         41 foreach my $chipconfig ( @_CHIPCONFIGS ) {
248 7         24 my $chiptype = $chipconfig->{type};
249 7         21 my $adapter = $chipconfig->{adapter};
250              
251 7         22 my $class = "Device::Chip::$chiptype";
252              
253 7         95 require ( "$class.pm" ) =~ s(::)(/)gr;
254              
255 7         135 my $chip = $class->new;
256              
257 7         122 my %mountopts;
258 7 50       31 %mountopts = $chipconfig->{mountopts}->%* if $chipconfig->{mountopts};
259              
260 7         56 await $chip->mount( $adapter, %mountopts );
261              
262 7 50       1537 if( $chipconfig->{config} ) {
263 0         0 await $chip->change_config( $chipconfig->{config}->%* );
264             }
265              
266 7         61 await $chip->protocol->power(1);
267              
268 7 100       447 if( $chip->can( "initialize_sensors" ) ) {
269 1         5 await $chip->initialize_sensors;
270             }
271              
272 7         88 push @$_chips, $chip;
273             }
274              
275 7         67 return @$_chips;
276             }
277              
278             =head2 sensors
279              
280             @sensors = await $app->sensors;
281              
282             An asynchronous memoized lazy accessor for the list of L
283             instances of each of the configured chips (from the L method).
284              
285             =cut
286              
287             field $_sensors; # arrayref
288              
289             field $_chipname_width;
290             field $_sensorname_width;
291              
292 9     9   20 sub _chipname ( $chip ) { return ( ref $chip ) =~ s/^Device::Chip:://r }
  9         47  
  9         20  
  9         85  
293              
294             async method sensors
295 9     9 1 12884 {
296 9 100       50 return @$_sensors if $_sensors;
297              
298 7         31 @$_sensors = map { $_->list_sensors } await $self->chips;
  7         358  
299              
300 7         656 $_chipname_width = max map { length _chipname $_ } @$_chips;
  7         30  
301 7         24 $_sensorname_width = max map { length $_->name } @$_sensors;
  8         43  
302              
303 7         150 await $self->after_sensors( @$_sensors );
304              
305 7         428 return @$_sensors;
306             }
307              
308 6     6 1 15 async method after_sensors ( @sensors ) { }
  6         18  
  6         21  
  6         13  
  6         67  
309              
310             =head2 run
311              
312             await $app->run;
313              
314             An asynchronous method which performs the actual run loop of the sensor
315             application. This implements the main application logic, of regular collection
316             of values from all of the sensor instances and reporting them to the
317             L method.
318              
319             In normal circumstances the L instance returned by this method would
320             remain pending for the lifetime of the program, and not complete. For an
321             application that has nothing else to perform concurrently it can simply
322             C this future to run the logic. If it has other logic to perform as
323             well it could combine this with other futures using a C<< Future->needs_all >>
324             or similar techniques.
325              
326             =cut
327              
328             field %filters_by_sensor;
329              
330 8     8 1 27375 async method run ()
  8         39  
  8         15  
331 8         21 {
332 8         48 my @chips = await $self->chips;
333              
334 8     0   623 $SIG{INT} = $SIG{TERM} = sub { exit 1; };
  0         0  
335              
336 8         38 defer {
337 8         521 try {
338 8 50       112 $chips[0] and $chips[0]->protocol->power(0)->get;
339             }
340             catch ($e) {
341 0         0 warn "Failed to turn off power while shutting down: $e";
342             }
343             }
344              
345 8         58 my @sensors = await $self->sensors;
346              
347 8         373 my $waittime = Time::HiRes::time();
348 8         16 while(1) {
349             # Read concurrently
350 25         4433 my $now = Time::HiRes::time();
351              
352             my @values = await Future->needs_all(
353             map {
354 25         73 my $sensor = $_;
355             my $f = $sensor->read;
356 1         2 $f = $f->then(
357 1     1   562 async sub ($reading) {
  1         3  
  1         3  
358 1         8 $self->on_sensor_ok( $sensor );
359 1         22 return $reading;
360 3         6 },
361 3     3   1932 async sub ($failure, @) {
  3         9  
  3         7  
362 3         20 $self->on_sensor_fail( $sensor, $failure );
363 3         67 return undef;
364             },
365             ) if $_best_effort;
366             $f;
367             } @sensors
368             );
369              
370 25         13377 foreach my $idx ( 0 .. $#sensors ) {
371 26         72 my $sensor = $sensors[$idx];
372              
373 26   66     189 my $filter = $filters_by_sensor{ refaddr $sensor } //= $self->make_filter_for_sensor( $sensor );
374              
375 26         126 $values[$idx] = $filter->filter( $values[$idx] );
376             }
377              
378 25         193 $self->output_readings( $now, \@sensors, \@values );
379              
380 25         782 $waittime += $_interval;
381 25         155 await Future::IO->alarm( $waittime );
382             }
383             }
384              
385 7     7 0 19 method make_filter_for_sensor ( $sensor )
  7         32  
  7         15  
  7         14  
386             {
387             # We only filter gauges currently
388 7 100       40 return App::Device::Chip::sensor::Filter::Null->new if $sensor->type ne "gauge";
389              
390 6 100 66     124 if( !length $_filter or $_filter eq "null" ) {
    100          
    50          
391 3         48 return App::Device::Chip::sensor::Filter::Null->new;
392             }
393             elsif( $_filter =~ m/^mid(\d+)$/ ) {
394 2         41 return App::Device::Chip::sensor::Filter::MidN->new( n => $1 );
395             }
396             elsif( $_filter =~ m/^ravg(\d+)$/ ) {
397 1         27 return App::Device::Chip::sensor::Filter::Ravg->new( alpha => 2 ** -$1 );
398             }
399             else {
400 0         0 die "Unrecognised filter name $_filter";
401             }
402             }
403              
404             =head2 print_readings
405              
406             $app->print_readings( $sensors, $values );
407              
408             Prints the sensor names and current readings in a human-readable format to the
409             currently-selected output handle (usually C).
410              
411             =cut
412              
413 2     2   4 method _format_reading ( $sensor, $value )
  2         6  
  2         3  
  2         4  
  2         4  
414             {
415 2 50       8 return undef if !defined $value;
416              
417             # Take account of extra precision required due to filtering
418 2         7 my $filter = $filters_by_sensor{ refaddr $sensor };
419 2 50       12 my $extra_digits = $filter ? $filter->extra_digits : 0;
420 2         7 return sprintf "%.*f", $sensor->precision + $extra_digits, $value;
421             }
422              
423 1     1 1 17 method print_readings ( $sensors, $values )
  1         4  
  1         3  
  1         2  
  1         2  
424             {
425 1         4 foreach my $i ( 0 .. $#$sensors ) {
426 2         17 my $sensor = $sensors->[$i];
427 2         5 my $value = $values->[$i];
428              
429 2         8 my $chip = $sensor->chip;
430 2         15 my $chipname = _chipname $chip;
431              
432 2         8 my $units = $sensor->units;
433 2 50       18 $units = " $units" if defined $units;
434              
435 2         4 my $valuestr;
436 2 50       10 if( !defined $value ) {
    100          
437 0         0 $valuestr = "";
438             }
439             elsif( $sensor->type eq "gauge" ) {
440 1   50     37 $valuestr = sprintf "%s%s", $self->_format_reading( $sensor, $value ), $units // "";
441             }
442             else {
443 1   50     15 $valuestr = sprintf "%s%s/sec", $self->_format_reading( $sensor, $value / $self->interval ), $units // "";
444             }
445              
446 2         42 printf "% *s/% *s: %s\n",
447             $_chipname_width, $chipname, $_sensorname_width, $sensor->name, $valuestr;
448             }
449             }
450              
451             =head1 REQUIRED METHODS
452              
453             This base class itself is incomplete, requiring the following methods to be
454             provided by an implementing subclass to contain the actual application logic.
455              
456             =cut
457              
458             =head2 output_readings
459              
460             $app->output_readings( $now, $sensors, $values );
461              
462             This method is invoked regularly by the L method, to provide the
463             application with the latest round of sensor readings. It is passed the current
464             UNIX epoch timestamp as C<$now>, an array reference containing the individual
465             L instances as C<$sensors>, and a congruent array
466             reference containing the most recent readings taken from them, as plain
467             numbers.
468              
469             The application should put the bulk of its processing logic in here, for
470             example writing the values to some sort of file or database, displaying them
471             in some form, or whatever else the application is supposed to do.
472              
473             =cut
474              
475             =head1 OVERRIDABLE METHODS
476              
477             The base class provides the following methods, but it is expected that
478             applications may wish to override them to customise the logic contained in
479             them.
480              
481             If using L to do so, don't forget to provide the C<:override>
482             method attribute.
483              
484             =cut
485              
486             =head2 OPTSPEC
487              
488             %optspec = $app->OPTSPEC;
489              
490             This method is invoked by the L method to construct a definition
491             of the commandline options understood by the program. These are returned in a
492             key/value list to be processed by L. If the application wishes
493             to parse additional arguments it should override this method, call the
494             superclass version, and append any extra argument specifications it requires.
495              
496             As this is invoked as a regular instance method, a convenient way to store the
497             parsed values is to pass references to instance slot variables created by the
498             L C keyword:
499              
500             field $_title;
501             field $_bgcol = "#cccccc";
502              
503             method OPTSPEC :override
504             {
505             return ( $self->SUPER::OPTSPEC,
506             'title=s' => \$_title,
507             'background-color=s' => \$_bgcol,
508             );
509             }
510              
511             =cut
512              
513             =head2 after_sensors
514              
515             await $app->after_sensors( @sensors );
516              
517             This method is invoked once on startup by the L method, after it has
518             configured the chip adapter and chips and obtained their individual sensor
519             instances. The application may wish to perform one-time startup tasks in here,
520             such as creating database files with knowledge of the specific sensor data
521             types, or other such behaviours.
522              
523             =cut
524              
525             =head2 on_sensor_ok
526              
527             $app->on_sensor_ok( $sensor );
528              
529             This method is invoked in C<--best-effort> mode after a successful reading
530             from sensor; typically this is used to clear a failure state.
531              
532             The default implementation does nothing.
533              
534             =cut
535              
536 0     0 1 0 method on_sensor_ok ( $sensor ) { }
  0         0  
  0         0  
537              
538             =head2 on_sensor_fail
539              
540             $app->on_sensor_fail( $sensor, $failure );
541              
542             This method is invoked in C<--best-effort> mode after a failure of the given
543             sensor. The caught exception is passed as C<$failure>.
544              
545             The default implementation prints this as a warning using the core C
546             function.
547              
548             =cut
549              
550 0     0 1 0 method on_sensor_fail ( $sensor, $failure )
  0         0  
  0         0  
  0         0  
  0         0  
551             {
552 0         0 my $sensorname = $sensor->name;
553 0         0 my $chipname = ref ( $sensor->chip );
554              
555 0         0 warn "Unable to read ${sensorname} of ${chipname}: $failure";
556             }
557              
558             =head1 FILTERING
559              
560             The C<--filter> setting accepts the following filter names
561              
562             =cut
563              
564             =head2 null
565              
566             No filtering is applied. Each sensor reading is reported as it stands.
567              
568             =cut
569              
570             class App::Device::Chip::sensor::Filter::Null
571             {
572 7     7   832 use constant extra_digits => 0;
  7         57  
  7         2893  
573              
574 10     10   22 method filter ( $value ) { return $value }
  10         65  
  10         24  
  10         20  
  10         36  
575             }
576              
577             =head2 midI
578              
579             The most recent I values are sorted, and the middle of these is reported.
580             To be well-behaved, I should be an odd number. (C, C, C,
581             etc...)
582              
583             =cut
584              
585             class App::Device::Chip::sensor::Filter::MidN
586             {
587 7     7   783 use List::Util 1.29 qw( all );
  7         122  
  7         1146  
588              
589             field $n :param;
590             field @readings;
591              
592 7     7   54 use constant extra_digits => 0;
  7         36  
  7         4346  
593              
594 12     12   22 method filter ( $value )
  12         49  
  12         20  
  12         19  
595             {
596             # Accumulate the past 3 readings
597 12         28 push @readings, $value;
598 12         50 shift @readings while @readings > $n;
599              
600             # Take the middle of the 3
601 12 100 66 24   90 return $value unless @readings == $n and all { defined } @readings;
  24         90  
602              
603 6         43 my @sorted = sort { $a <=> $b } @readings;
  32         67  
604 6         37 return $sorted[($n-1) / 2];
605             }
606             }
607              
608             =head2 ravgI
609              
610             Recursive average with weighting of C<2 ** -n>.
611              
612             =cut
613              
614             class App::Device::Chip::sensor::Filter::Ravg
615             {
616             field $alpha :param;
617             field $prev;
618              
619 7     7   1053 use constant extra_digits => 2;
  7         18  
  7         3221  
620              
621 4     4   7 method filter ( $value )
  4         17  
  4         9  
  4         7  
622             {
623 4 100       15 return $prev = $value if !defined $prev;
624 3         15 return $prev = $prev + $alpha * ( $value - $prev );
625             }
626             }
627              
628             =head1 AUTHOR
629              
630             Paul Evans
631              
632             =cut
633              
634             0x55AA;