File Coverage

blib/lib/Device/Chip/Sensor.pm
Criterion Covered Total %
statement 83 83 100.0
branch 7 8 87.5
condition 10 11 90.9
subroutine 22 22 100.0
pod 7 9 77.7
total 129 133 96.9


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-2022 -- leonerd@leonerd.org.uk
5              
6 2     2   1703 use v5.26;
  2         6  
7 2     2   8 use Object::Pad 0.66 ':experimental(init_expr)';
  2         23  
  2         10  
8              
9             package Device::Chip::Sensor 0.24;
10              
11 2     2   246 use strict;
  2         4  
  2         40  
12 2     2   8 use warnings;
  2         2  
  2         52  
13              
14 2     2   8 use experimental 'signatures';
  2         3  
  2         12  
15 2     2   217 use Object::Pad ':experimental(mop)';
  2         3  
  2         6  
16              
17 2     2   198 use Carp;
  2         3  
  2         740  
18              
19             =head1 NAME
20              
21             C - declarations of sensor readings for C
22              
23             =head1 SYNOPSIS
24              
25             class Device::Chip::MySensorChip
26             extends Device::Chip;
27              
28             use Device::Chip::Sensor -declare;
29              
30             ...
31              
32             declare_sensor voltage =>
33             units => "volts",
34             precision => 3;
35              
36             async method read_voltage () {
37             ...
38             }
39              
40             =head1 DESCRIPTION
41              
42             This package provides some helper methods for describing metadata on
43             L drivers that provide sensor values. The resulting metadata
44             helps to describe the quantities that the sensor chip can measure, and
45             provides a consistent API for accessing them.
46              
47             =cut
48              
49             my %SENSORS_FOR_CLASS;
50              
51             =head1 CHIP METHODS
52              
53             When imported into a C driver class using the C<-declare> option
54             the following methods are added to it.
55              
56             =cut
57              
58             =head2 list_sensors
59              
60             @sensors = $chip->list_sensors;
61              
62             Returns a list of individual sensor objects. Each object represents a single
63             sensor reading that can be measured.
64              
65             =head1 OPTIONAL CHIP METHODS
66              
67             The following methods may also be provided by the chip driver class if
68             required. Callers should check they are implemented (e.g. with C) before
69             attempting to call them.
70              
71             =head2 initialize_sensors
72              
73             await $chip->initialize_sensors;
74              
75             If the chip requires any special configuration changes, initial calibrations,
76             startup delay, or other operations before the sensors are available then this
77             method should perform it. It can presume that the application wishes to
78             interact with the chip primarily via the sensors API, and thus if required it
79             can presume particular settings to make this happen.
80              
81             =head1 SENSOR DECLARATIONS
82              
83             Sensor metadata is provided by the following function.
84              
85             =head2 declare_sensor
86              
87             declare_sensor $name => %params;
88              
89             Declares a new sensor object with the given name and parameters.
90              
91             The following named parameters are recognised:
92              
93             =over 4
94              
95             =item type => STRING
96              
97             Optional. A string specifying what overall type of data is being returned.
98             Normally this is C to indicate a quantity that is measured on every
99             observation. A type of C instead indicates that the value will be an
100             integer giving the total number of times some event has happened - typically
101             used to count interrupt events from chips.
102              
103             A convenience function L exists for making counters.
104              
105             =item units => STRING
106              
107             A string describing the units in which the value is returned. This should be
108             an empty string for purely abstract counting sensors, or else describe the
109             measured quantities in physical units (such as C, C,
110             C, C, ...)
111              
112             =item precision => INT
113              
114             The number of decimal places of floating-point accuracy that values should
115             be printed with. This should be 0 for integer readings.
116              
117             =item method => STRING or CODE
118              
119             Optional string or code reference giving the method on the main chip object to
120             call to obtain a new reading of this sensor's current value. If not provided a
121             default will be created by prefixing C<"read_"> onto the sensor name.
122              
123             =item sanity_bounds => ARRAY[ 2 * NUM ]
124              
125             Optional bounding values to sanity-test reported readings. If a reading is
126             obtained that is lower than the first value or higher than the second, it is
127             declared to be out of bounds by the L method. Either bound may be set
128             to C to ignore that setting. For example, setting just a lower bound of
129             zero ensures that any negative values that are obtained are considered out of
130             the valid range.
131              
132             =back
133              
134             =head2 declare_sensor_counter
135              
136             declare_sensor_counter $name => %params;
137              
138             Declares a sensor of the C type. This will pass C for the
139             units and 0 for precision.
140              
141             =cut
142              
143             sub import ( @opts )
144 2     2   17 {
  2         5  
  2         4  
145 2         4 my $caller = caller;
146 2 100       4 declare_into( $caller ) if grep { $_ eq "-declare" } @opts;
  3         28  
147             }
148              
149             sub declare_into ( $caller )
150 1     1 0 1 {
  1         2  
  1         1  
151 1         3 my $classmeta = Object::Pad::MOP::Class->for_class( $caller );
152              
153 1   50     30 my $sensors = $SENSORS_FOR_CLASS{$classmeta->name} //= [];
154              
155 1     1   1 $classmeta->add_method( list_sensors => sub ( $self ) {
  1         3  
  1         2  
156             # TODO: some sort of superclass merge?
157 1         2 return map { $_->bind( $self ) } $sensors->@*;
  4         8  
158 1         10 } );
159              
160 4     4   4 my $declare = sub ( $name, %params ) {
  4         81  
  4         6  
  4         9  
161 4         25 push $sensors->@*, Device::Chip::Sensor->new(
162             name => $name,
163             %params,
164             );
165 1         6 };
166              
167 2     2   13 no strict 'refs';
  2         4  
  2         371  
168 1         2 *{"${caller}::declare_sensor"} = $declare;
  1         4  
169 1         1738 *{"${caller}::declare_sensor_counter"} = sub {
170 1     1   6 $declare->( @_, type => "counter", units => undef, precision => 0 );
171 1         2 };
172             }
173              
174             class Device::Chip::Sensor;
175              
176 2     2   238 use Future::AsyncAwait 0.38;
  2         74  
  2         13  
177              
178             =head1 SENSOR METHODS
179              
180             Each returned sensor object provides the following methods.
181              
182             =head2 name
183              
184             =head2 units
185              
186             =head2 precision
187              
188             $name = $sensor->name;
189              
190             $units = $sensor->units;
191              
192             $prec = $sensor->precision;
193              
194             Metadata fields from the sensor's declaration.
195              
196             =head2 chip
197              
198             $chip = $sensor->chip;
199              
200             The L instance this sensor is a part of.
201              
202             =cut
203              
204             my %TYPES = (
205             gauge => 1,
206             counter => 1,
207             );
208              
209 2     2 1 1564 field $_type :reader :param { "gauge" };
  2         9  
210 1     1 1 3 field $_name :reader :param;
  1         4  
211 1     1 1 3 field $_units :reader :param { undef };
  1         4  
212 1     1 1 3 field $_precision :reader :param { 0 };
  1         4  
213              
214             field $_lbound;
215             field $_ubound;
216              
217             field $_method :param { undef };
218              
219 1     1 1 3 field $_chip :reader :param { undef };
  1         5  
220              
221             ADJUST
222             {
223             $TYPES{$_type} or
224             croak "Unrecognised sensor type '$_type'";
225              
226             $_method //= "read_$_name";
227             }
228              
229             ADJUSTPARAMS ( $params )
230             {
231             ( $_lbound, $_ubound ) = ( delete $params->{sanity_bounds} // [] )->@*;
232             }
233              
234 4         4 method bind ( $chip )
  4         5  
  4         4  
235 4     4 0 6 {
236 4         14 return Device::Chip::Sensor->new(
237             chip => $chip,
238              
239             type => $_type,
240             name => $_name,
241             units => $_units,
242             precision => $_precision,
243             method => $_method,
244             sanity_bounds => [ $_lbound, $_ubound ],
245             );
246             }
247              
248             =head2 read
249              
250             $value = await $sensor->read;
251              
252             Performs an actual read operation on the sensor chip to return the currently
253             measured value.
254              
255             This method always returns a single scalar value, even if the underlying
256             method on the sensor chip returned more than one.
257              
258             If the value obtained from the sensor is outside of the sanity-check bounds
259             then an exception is thrown instead.
260              
261             =cut
262              
263 6         9 async method read ()
  6         6  
264 6         29 {
265 6 50       18 defined( my $value = scalar await $_chip->$_method() )
266             or return undef;
267              
268 6 100 100     314 if( defined $_lbound and $value < $_lbound or
      100        
      100        
269             defined $_ubound and $value > $_ubound ) {
270 2         8 die sprintf "Reading %s is out of range\n", $self->format( $value );
271             }
272              
273 4         21 return $value;
274 6     6 1 2836 }
275              
276             =head2 format
277              
278             $string = $sensor->format( $value );
279              
280             Returns a string by formatting an observed value to the required precision.
281              
282             =cut
283              
284 4         5 method format ( $value )
  4         6  
  4         4  
285 4     4 1 34 {
286 4 100       12 return undef if !defined $value;
287 3         37 return sprintf "%.*f", $_precision, $value;
288             }
289              
290             =head1 AUTHOR
291              
292             Paul Evans
293              
294             =cut
295              
296             0x55AA;