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 23 23 100.0
pod 6 6 100.0
total 178 187 95.1


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   86564 use v5.26;
  2         8  
7 2     2   8 use warnings;
  2         2  
  2         49  
8              
9 2     2   844 use Object::Pad 0.51;
  2         14160  
  2         8  
10              
11             package Electronics::PSU::DPSxxxx 0.02;
12             class Electronics::PSU::DPSxxxx;
13              
14 2     2   600 use Carp;
  2         3  
  2         82  
15 2     2   411 use Future::AsyncAwait;
  2         11112  
  2         12  
16 2     2   420 use Future::IO;
  2         9912  
  2         65  
17              
18 2     2   12 use Fcntl qw( O_NOCTTY O_NDELAY );
  2         4  
  2         104  
19 2     2   972 use IO::Termios;
  2         41391  
  2         11  
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             differenet 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             has $_fh :param = undef;
65             has $_addr :param = 1;
66              
67             ADJUSTPARAMS ( $params )
68             {
69             unless( $_fh ) {
70             my $dev = delete $params->{dev};
71              
72             $_fh = IO::Termios->open( $dev, "9600,8,n,1", O_NOCTTY, O_NDELAY ) or
73             croak "Cannot open $dev - $!";
74              
75             $_fh->cfmakeraw;
76             $_fh->setflag_clocal( 1 );
77             $_fh->blocking( 1 );
78             $_fh->autoflush;
79             }
80             }
81              
82             sub _calc_crc ( $data )
83 20     20   21 {
  20         31  
  20         20  
84             # This is not the standard CRC16 algorithm
85             # Stolen and adapted from
86             # https://ctlsys.com/support/how_to_compute_the_modbus_rtu_message_crc/
87 20         22 my $crc = 0xFFFF;
88 20         59 foreach my $d ( split //, $data ) {
89 115         122 $crc ^= ord $d;
90 115         130 foreach ( 1 .. 8 ) {
91 920 100       1002 if( $crc & 0x0001 ) {
92 427         395 $crc >>= 1;
93 427         421 $crc ^= 0xA001;
94             }
95             else {
96 493         498 $crc >>= 1;
97             }
98             }
99             }
100              
101 20         57 return $crc;
102             }
103              
104 10         11 async method _send_command ( $func, $format, @args )
  10         11  
  10         14  
  10         12  
  10         10  
105 10         14 {
106 10         47 my $request = pack "C C $format", $_addr, $func, @args;
107              
108             # CRC is appended in little-endian format
109 10         18 $request .= pack "S<", _calc_crc( $request );
110              
111 10         37 await Future::IO->syswrite( $_fh, $request );
112 10     10   11 }
113              
114 10         14 async method _recv_response ( $func, $len, $format )
  10         12  
  10         11  
  10         11  
  10         10  
115 10         25 {
116 10         33 my $response = await Future::IO->sysread_exactly( $_fh, 2 + $len + 2 );
117              
118 10         10955 my $got_crc = unpack "S<", substr( $response, -2, 2, "" );
119 10 50       23 if( $got_crc != ( my $want_crc = _calc_crc( $response ) ) ) {
120             # Just warn for now
121 0         0 warn sprintf "Received PDU CRC %04X, expected %04X\n", $got_crc, $want_crc;
122             }
123              
124 10         37 my ( $got_addr, $got_func, $payload ) = unpack "C C a*", $response;
125 10 50       19 $got_addr == $_addr or croak "Received response from unexpected addr";
126 10 50       21 $got_func == $func or croak "Received response for unexpected function";
127              
128 10         46 return unpack $format, $payload;
129 10     10   18 }
130              
131             use constant {
132 2         663 FUNC_READ_HOLDING_REGISTER => 0x03,
133             FUNC_WRITE_SINGLE_REGISTER => 0x06,
134 2     2   1692 };
  2         3  
135              
136 7         11 async method _read_holding_registers ( $reg, $count = 1 )
  7         10  
  7         8  
  7         10  
137 7         11 {
138 7         17 await $self->_send_command( FUNC_READ_HOLDING_REGISTER, "S> S>", $reg, $count );
139              
140 7         12277 my ( $nbytes, @regs ) =
141             await $self->_recv_response( FUNC_READ_HOLDING_REGISTER, 1 + 2*$count, "C (S>)*" );
142              
143 7         373 return @regs;
144 7     7   10 }
145              
146 3         4 async method _write_single_register ( $reg, $value )
  3         4  
  3         3  
  3         4  
147 3         5 {
148 3         8 await $self->_send_command( FUNC_WRITE_SINGLE_REGISTER, "S> S>", $reg, $value );
149              
150             # ignore result
151 3         2954 await $self->_recv_response( FUNC_WRITE_SINGLE_REGISTER, 2 + 2, "S> S>" );
152              
153 3         142 return;
154 3     3   4 }
155              
156             =head1 METHODS
157              
158             =cut
159              
160             use constant {
161 2         612 REG_USET => 0x00, # centivolts
162             REG_ISET => 0x01, # miliamps
163             REG_UOUT => 0x02, # centivolts (RO)
164             REG_IOUT => 0x03, # miliiamps (RO)
165             REG_POWER => 0x04, # (RO)
166             REG_UIN => 0x05, # centivolts (RO)
167             REG_LOCK => 0x06, # 0=off 1=on
168             REG_PROTECT => 0x07, # 0=ok, 1=OVP 2=OCP 3=OPP
169             REG_CVCC => 0x08, # 0=CV 1=CC
170             REG_ONOFF => 0x09, # 0=off 1=on
171             REG_B_LED => 0x0A,
172             REG_MODEL => 0x0B, # (RO)
173             REG_VERSION => 0x0C, # (RO)
174 2     2   11 };
  2         5  
175              
176             =head2 set_voltage
177              
178             await $psu->set_voltage( $volts );
179              
180             Sets the output voltage, in volts.
181              
182             =cut
183              
184 1         2 async method set_voltage ( $voltage )
  1         2  
  1         1  
185 1         3 {
186 1         6 await $self->_write_single_register( REG_USET, $voltage * 100 );
187 1     1 1 2572 }
188              
189             =head2 set_current
190              
191             await $psu->set_current( $amps );
192              
193             Sets the output current, in amps.
194              
195             =cut
196              
197 1         2 async method set_current ( $current )
  1         1  
  1         2  
198 1         3 {
199 1         5 await $self->_write_single_register( REG_ISET, $current * 1000 );
200 1     1 1 95 }
201              
202             my %READINGS;
203              
204             # Build up all the reading methods
205             BEGIN {
206 2     2   13 use Object::Pad qw( :experimental(mop) );
  2         3  
  2         14  
207              
208 2     2   882 my $metaclass = Object::Pad::MOP::Class->for_caller;
209              
210             %READINGS = ( # register, scale
211 2         9 output_voltage => [ REG_UOUT, sub { $_ / 100 } ],
212 2         11 output_current => [ REG_IOUT, sub { $_ / 1000 } ],
213 1         5 input_voltage => [ REG_UIN, sub { $_ / 100 } ],
214 0         0 output_protect => [ REG_PROTECT, sub { (qw( ok OVP OCP OPP ))[$_]} ],
215 2         78 output_mode => [ REG_CVCC, sub { (qw( CV CC ))[$_] } ],
  1         6  
216             );
217              
218 2         7 foreach my $name ( keys %READINGS ) {
219             $metaclass->add_method(
220 10         1301 "read_$name" => async method { return await $self->read_multiple( $name ) }
  4         8650  
  4         13  
  4         13  
221             );
222             }
223             }
224              
225             =head2 read_output_voltage
226              
227             $volts = await $psu->read_output_voltage;
228              
229             Returns the measured voltage at the output terminals, in volts.
230              
231             =head2 read_output_current
232              
233             $amps = await $psu->read_output_current;
234              
235             Returns the measured current at the output terminals, in amps.
236              
237             =head2 read_input_voltage
238              
239             $volts = await $psu->read_input_voltage;
240              
241             Returns the input voltage to the PSU module, in volts.
242              
243             =head2 read_output_protect
244              
245             $protect = await $psu->read_output_protect;
246              
247             Returns the output protection state as a string, either C<"ok"> if protection
248             has not been triggered, or one of C<"OVP">, C<"OCP"> or C<"OPP"> if any of the
249             protection mechanisms have been triggered.
250              
251             =head2 read_output_mode
252              
253             $mode = await $psu->read_output_mode;
254              
255             Returns the output mode, as a string either C<"CV"> for constant-voltage or
256             C<"CC"> for constant-current.
257              
258             =cut
259              
260             =head2 read_multiple
261              
262             @readings = await $psu->read_multiple( @names )
263              
264             Returns multiple measurements in a single query. This is faster than
265             performing several individual read requests. C<@names> should be a list of
266             string names, taken from the C method names. For example:
267              
268             my ( $volts, $amps ) =
269             await $psu->read_multiple(qw( output_voltage output_current ));
270              
271             Results are returned in the same order as the requested names.
272              
273             =cut
274              
275 5         7 async method read_multiple ( @names )
  5         10  
  5         5  
276 5         11 {
277 5         8 my ( $minreg, $maxreg );
278              
279             my @regs = map {
280 5 50       9 my $m = $READINGS{$_} or croak "Measurement '$_' is not recognised";
  6         20  
281 6         12 my $reg = $m->[0];
282 6 100 66     20 $minreg = $reg if !defined $minreg or $reg < $minreg;
283 6 50 66     32 $maxreg = $reg if !defined $maxreg or $reg > $maxreg;
284             } @names;
285              
286 5         15 my @values = await $self->_read_holding_registers( $minreg, $maxreg-$minreg+1 );
287              
288             return map {
289 5         226 my $m = $READINGS{$_};
  6         11  
290 6         23 $m->[1]->( local $_ = $values[$m->[0] - $minreg] );
291             } @names;
292 5     5 1 2781 }
293              
294             =head2 set_output_state
295              
296             await $psu->set_output_state( $on );
297              
298             Switches output on / off.
299              
300             =cut
301              
302 1         2 async method set_output_state ( $on )
  1         3  
  1         1  
303 1         4 {
304 1         4 await $self->_write_single_register( REG_ONOFF, !!$on );
305 1     1 1 2838 }
306              
307             =head2 read_model
308              
309             $model = await $psu->read_model;
310              
311             Returns the model number (e.g. 3005 for F).
312              
313             =cut
314              
315             async method read_model
316 1         4 {
317 1         5 return await $self->_read_holding_registers( REG_MODEL );
318 1     1 1 1463 }
319              
320             =head2 read_version
321              
322             $version = await $psu->read_version;
323              
324             Returns firmware version as an integer.
325              
326             =cut
327              
328             async method read_version
329 1         3 {
330 1         4 return await $self->_read_holding_registers( REG_VERSION );
331 1     1 1 3539 }
332              
333             =head1 AUTHOR
334              
335             Paul Evans
336              
337             =cut
338              
339             0x55AA;