File Coverage

blib/lib/Electronics/PSU/DPSxxxx.pm
Criterion Covered Total %
statement 136 138 98.5
branch 9 14 64.2
condition 4 6 66.6
subroutine 28 28 100.0
pod 11 11 100.0
total 188 197 95.4


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, 2022 -- leonerd@leonerd.org.uk
5              
6 2     2   128347 use v5.26;
  2         11  
7 2     2   11 use warnings;
  2         3  
  2         67  
8              
9 2     2   1222 use Object::Pad 0.73 ':experimental(adjust_params init_expr)';
  2         19525  
  2         10  
10              
11             package Electronics::PSU::DPSxxxx 0.04;
12             class Electronics::PSU::DPSxxxx;
13              
14 2     2   913 use Carp;
  2         4  
  2         111  
15 2     2   546 use Future::AsyncAwait;
  2         16246  
  2         12  
16 2     2   564 use Future::IO;
  2         12550  
  2         114  
17              
18 2     2   13 use Fcntl qw( O_NOCTTY O_NDELAY );
  2         5  
  2         130  
19 2     2   1126 use IO::Termios;
  2         54886  
  2         14  
20              
21             # Protocol::Modbus exists but it doesn't do async, and it doesn't do proper
22             # frame detection of incoming data :(
23             # We'll just write this all ourselves; it's not hard
24              
25             # See also
26             # https://autoit.de/wcf/attachment/88980-dps3005-cnc-communication-protocol-v1-2-pdf/
27              
28             =head1 NAME
29              
30             C - control a F power supply
31              
32             =head1 SYNOPSIS
33              
34             use Future::AsyncAwait;
35              
36             use Electronics::PSU::DPSxxxx;
37              
38             my $psu = Electronics::PSU::DPSxxxx->new( dev => "/dev/ttyUSB0" );
39              
40             await $psu->set_voltage( 1.23 ); # volts
41             await $psu->set_current( 0.200 ); # amps
42              
43             await $psu->set_output_state( 1 ); # turn it on!
44              
45             =head1 DESCRIPTION
46              
47             This module allows control of a F F-series power supply, such as
48             the F, when connected over a serial port.
49              
50             =head2 Interface Design
51              
52             The interface is currently an ad-hoc collection of whatever seems to work
53             here, but my hope is to find a more generic shareable interface that multiple
54             different modules can use, to provide interfaces to various kinds of
55             electronics test equipment.
56              
57             The intention is that it should eventually be possible to write a script for
58             performing automated electronics testing or experimentation, and easily swap
59             out modules to suit the equipment available. Similar concepts apply in fields
60             like L, or L, so there should be plenty of ideas to borrow.
61              
62             =cut
63              
64             field $_fh :param = undef;
65             field $_addr :param = 1;
66              
67             ADJUST :params (
68             :$dev = undef,
69             ) {
70             unless( $_fh ) {
71             $_fh = IO::Termios->open( $dev, "9600,8,n,1", O_NOCTTY, O_NDELAY ) or
72             croak "Cannot open $dev - $!";
73              
74             $_fh->cfmakeraw;
75             $_fh->setflag_clocal( 1 );
76             $_fh->blocking( 1 );
77             $_fh->autoflush;
78             }
79             }
80              
81             sub _calc_crc ( $data )
82 20     20   29 {
  20         37  
  20         23  
83             # This is not the standard CRC16 algorithm
84             # Stolen and adapted from
85             # https://ctlsys.com/support/how_to_compute_the_modbus_rtu_message_crc/
86 20         37 my $crc = 0xFFFF;
87 20         74 foreach my $d ( split //, $data ) {
88 115         159 $crc ^= ord $d;
89 115         169 foreach ( 1 .. 8 ) {
90 920 100       1338 if( $crc & 0x0001 ) {
91 427         544 $crc >>= 1;
92 427         642 $crc ^= 0xA001;
93             }
94             else {
95 493         702 $crc >>= 1;
96             }
97             }
98             }
99              
100 20         68 return $crc;
101             }
102              
103 10         17 async method _send_command ( $func, $format, @args )
  10         13  
  10         18  
  10         21  
  10         13  
104 10         22 {
105 10         58 my $request = pack "C C $format", $_addr, $func, @args;
106              
107             # CRC is appended in little-endian format
108 10         30 $request .= pack "S<", _calc_crc( $request );
109              
110 10         42 await Future::IO->syswrite( $_fh, $request );
111 10     10   18 }
112              
113 10         20 async method _recv_response ( $func, $len, $format )
  10         15  
  10         13  
  10         18  
  10         13  
114 10         31 {
115 10         38 my $response = await Future::IO->sysread_exactly( $_fh, 2 + $len + 2 );
116              
117 10         15345 my $got_crc = unpack "S<", substr( $response, -2, 2, "" );
118 10 50       27 if( $got_crc != ( my $want_crc = _calc_crc( $response ) ) ) {
119             # Just warn for now
120 0         0 warn sprintf "Received PDU CRC %04X, expected %04X\n", $got_crc, $want_crc;
121             }
122              
123 10         45 my ( $got_addr, $got_func, $payload ) = unpack "C C a*", $response;
124 10 50       30 $got_addr == $_addr or croak "Received response from unexpected addr";
125 10 50       20 $got_func == $func or croak "Received response for unexpected function";
126              
127 10         82 return unpack $format, $payload;
128 10     10   22 }
129              
130             use constant {
131 2         948 FUNC_READ_HOLDING_REGISTER => 0x03,
132             FUNC_WRITE_SINGLE_REGISTER => 0x06,
133 2     2   2591 };
  2         4  
134              
135 7         10 async method _read_holding_registers ( $reg, $count = 1 )
  7         14  
  7         11  
  7         10  
136 7         17 {
137 7         23 await $self->_send_command( FUNC_READ_HOLDING_REGISTER, "S> S>", $reg, $count );
138              
139 7         17025 my ( $nbytes, @regs ) =
140             await $self->_recv_response( FUNC_READ_HOLDING_REGISTER, 1 + 2*$count, "C (S>)*" );
141              
142 7         608 return @regs;
143 7     7   12 }
144              
145 3         4 async method _write_single_register ( $reg, $value )
  3         6  
  3         5  
  3         4  
146 3         6 {
147 3         10 await $self->_send_command( FUNC_WRITE_SINGLE_REGISTER, "S> S>", $reg, $value );
148              
149             # ignore result
150 3         4402 await $self->_recv_response( FUNC_WRITE_SINGLE_REGISTER, 2 + 2, "S> S>" );
151              
152 3         222 return;
153 3     3   7 }
154              
155             =head1 METHODS
156              
157             =cut
158              
159             use constant {
160 2         833 REG_USET => 0x00, # centivolts
161             REG_ISET => 0x01, # miliamps
162             REG_UOUT => 0x02, # centivolts (RO)
163             REG_IOUT => 0x03, # miliamps (RO)
164             REG_POWER => 0x04, # (RO)
165             REG_UIN => 0x05, # centivolts (RO)
166             REG_LOCK => 0x06, # 0=off 1=on
167             REG_PROTECT => 0x07, # 0=ok, 1=OVP 2=OCP 3=OPP
168             REG_CVCC => 0x08, # 0=CV 1=CC
169             REG_ONOFF => 0x09, # 0=off 1=on
170             REG_B_LED => 0x0A,
171             REG_MODEL => 0x0B, # (RO)
172             REG_VERSION => 0x0C, # (RO)
173 2     2   16 };
  2         4  
174              
175             =head2 set_voltage
176              
177             await $psu->set_voltage( $volts );
178              
179             Sets the output voltage, in volts.
180              
181             =cut
182              
183 1         2 async method set_voltage ( $voltage )
  1         2  
  1         2  
184 1         6 {
185 1         7 await $self->_write_single_register( REG_USET, $voltage * 100 );
186 1     1 1 3646 }
187              
188             =head2 set_current
189              
190             await $psu->set_current( $amps );
191              
192             Sets the output current, in amps.
193              
194             =cut
195              
196 1         2 async method set_current ( $current )
  1         2  
  1         4  
197 1         4 {
198 1         4 await $self->_write_single_register( REG_ISET, $current * 1000 );
199 1     1 1 143 }
200              
201             my %READINGS;
202              
203             # Build up all the reading methods
204             BEGIN {
205 2     2   16 use Object::Pad qw( :experimental(mop) );
  2         5  
  2         17  
206              
207 2     2   1290 my $metaclass = Object::Pad::MOP::Class->for_caller;
208              
209             %READINGS = ( # register, scale
210 2         13 output_voltage => [ REG_UOUT, sub { $_ / 100 } ],
211 2         19 output_current => [ REG_IOUT, sub { $_ / 1000 } ],
212 1         7 input_voltage => [ REG_UIN, sub { $_ / 100 } ],
213 0         0 output_protect => [ REG_PROTECT, sub { (qw( ok OVP OCP OPP ))[$_]} ],
214 2         109 output_mode => [ REG_CVCC, sub { (qw( CV CC ))[$_] } ],
  1         7  
215             );
216              
217 2         9 foreach my $name ( keys %READINGS ) {
218             $metaclass->add_method(
219 10     4 1 1773 "read_$name" => async method { return await $self->read_multiple( $name ) }
  4     4 1 11627  
  4     4 1 15  
  4     4 1 16  
        4 1    
220             );
221             }
222             }
223              
224             =head2 read_output_voltage
225              
226             $volts = await $psu->read_output_voltage;
227              
228             Returns the measured voltage at the output terminals, in volts.
229              
230             =head2 read_output_current
231              
232             $amps = await $psu->read_output_current;
233              
234             Returns the measured current at the output terminals, in amps.
235              
236             =head2 read_input_voltage
237              
238             $volts = await $psu->read_input_voltage;
239              
240             Returns the input voltage to the PSU module, in volts.
241              
242             =head2 read_output_protect
243              
244             $protect = await $psu->read_output_protect;
245              
246             Returns the output protection state as a string, either C<"ok"> if protection
247             has not been triggered, or one of C<"OVP">, C<"OCP"> or C<"OPP"> if any of the
248             protection mechanisms have been triggered.
249              
250             =head2 read_output_mode
251              
252             $mode = await $psu->read_output_mode;
253              
254             Returns the output mode, as a string either C<"CV"> for constant-voltage or
255             C<"CC"> for constant-current.
256              
257             =cut
258              
259             =head2 read_multiple
260              
261             @readings = await $psu->read_multiple( @names )
262              
263             Returns multiple measurements in a single query. This is faster than
264             performing several individual read requests. C<@names> should be a list of
265             string names, taken from the C method names. For example:
266              
267             my ( $volts, $amps ) =
268             await $psu->read_multiple(qw( output_voltage output_current ));
269              
270             Results are returned in the same order as the requested names.
271              
272             =cut
273              
274 5         8 async method read_multiple ( @names )
  5         14  
  5         7  
275 5         11 {
276 5         9 my ( $minreg, $maxreg );
277              
278             my @regs = map {
279 5 50       13 my $m = $READINGS{$_} or croak "Measurement '$_' is not recognised";
  6         20  
280 6         14 my $reg = $m->[0];
281 6 100 66     23 $minreg = $reg if !defined $minreg or $reg < $minreg;
282 6 50 66     40 $maxreg = $reg if !defined $maxreg or $reg > $maxreg;
283             } @names;
284              
285 5         22 my @values = await $self->_read_holding_registers( $minreg, $maxreg-$minreg+1 );
286              
287             return map {
288 5         365 my $m = $READINGS{$_};
  6         45  
289 6         28 $m->[1]->( local $_ = $values[$m->[0] - $minreg] );
290             } @names;
291 5     5 1 3455 }
292              
293             =head2 set_output_state
294              
295             await $psu->set_output_state( $on );
296              
297             Switches output on / off.
298              
299             =cut
300              
301 1         3 async method set_output_state ( $on )
  1         2  
  1         2  
302 1         4 {
303 1         4 await $self->_write_single_register( REG_ONOFF, !!$on );
304 1     1 1 3702 }
305              
306             =head2 read_model
307              
308             $model = await $psu->read_model;
309              
310             Returns the model number (e.g. 3005 for F).
311              
312             =cut
313              
314             async method read_model
315 1         3 {
316 1         5 return await $self->_read_holding_registers( REG_MODEL );
317 1     1 1 2148 }
318              
319             =head2 read_version
320              
321             $version = await $psu->read_version;
322              
323             Returns firmware version as an integer.
324              
325             =cut
326              
327             async method read_version
328 1         4 {
329 1         5 return await $self->_read_holding_registers( REG_VERSION );
330 1     1 1 4485 }
331              
332             =head1 AUTHOR
333              
334             Paul Evans
335              
336             =cut
337              
338             0x55AA;