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   103132 use v5.26;
  2         8  
7 2     2   8 use warnings;
  2         3  
  2         57  
8              
9 2     2   1008 use Object::Pad 0.70 ':experimental(adjust_params)';
  2         16416  
  2         11  
10              
11             package Electronics::PSU::DPSxxxx 0.03;
12             class Electronics::PSU::DPSxxxx;
13              
14 2     2   749 use Carp;
  2         4  
  2         92  
15 2     2   449 use Future::AsyncAwait;
  2         13685  
  2         11  
16 2     2   465 use Future::IO;
  2         10342  
  2         91  
17              
18 2     2   12 use Fcntl qw( O_NOCTTY O_NDELAY );
  2         3  
  2         107  
19 2     2   925 use IO::Termios;
  2         45941  
  2         15  
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             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   32 {
  20         29  
  20         27  
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         25 my $crc = 0xFFFF;
87 20         63 foreach my $d ( split //, $data ) {
88 115         129 $crc ^= ord $d;
89 115         144 foreach ( 1 .. 8 ) {
90 920 100       1145 if( $crc & 0x0001 ) {
91 427         437 $crc >>= 1;
92 427         487 $crc ^= 0xA001;
93             }
94             else {
95 493         573 $crc >>= 1;
96             }
97             }
98             }
99              
100 20         61 return $crc;
101             }
102              
103 10         11 async method _send_command ( $func, $format, @args )
  10         14  
  10         16  
  10         15  
  10         13  
104 10         19 {
105 10         57 my $request = pack "C C $format", $_addr, $func, @args;
106              
107             # CRC is appended in little-endian format
108 10         20 $request .= pack "S<", _calc_crc( $request );
109              
110 10         34 await Future::IO->syswrite( $_fh, $request );
111 10     10   13 }
112              
113 10         12 async method _recv_response ( $func, $len, $format )
  10         15  
  10         13  
  10         13  
  10         11  
114 10         28 {
115 10         36 my $response = await Future::IO->sysread_exactly( $_fh, 2 + $len + 2 );
116              
117 10         12723 my $got_crc = unpack "S<", substr( $response, -2, 2, "" );
118 10 50       23 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         37 my ( $got_addr, $got_func, $payload ) = unpack "C C a*", $response;
124 10 50       27 $got_addr == $_addr or croak "Received response from unexpected addr";
125 10 50       18 $got_func == $func or croak "Received response for unexpected function";
126              
127 10         54 return unpack $format, $payload;
128 10     10   17 }
129              
130             use constant {
131 2         817 FUNC_READ_HOLDING_REGISTER => 0x03,
132             FUNC_WRITE_SINGLE_REGISTER => 0x06,
133 2     2   1996 };
  2         4  
134              
135 7         10 async method _read_holding_registers ( $reg, $count = 1 )
  7         12  
  7         10  
  7         9  
136 7         11 {
137 7         17 await $self->_send_command( FUNC_READ_HOLDING_REGISTER, "S> S>", $reg, $count );
138              
139 7         14373 my ( $nbytes, @regs ) =
140             await $self->_recv_response( FUNC_READ_HOLDING_REGISTER, 1 + 2*$count, "C (S>)*" );
141              
142 7         479 return @regs;
143 7     7   14 }
144              
145 3         3 async method _write_single_register ( $reg, $value )
  3         5  
  3         5  
  3         4  
146 3         6 {
147 3         8 await $self->_send_command( FUNC_WRITE_SINGLE_REGISTER, "S> S>", $reg, $value );
148              
149             # ignore result
150 3         3613 await $self->_recv_response( FUNC_WRITE_SINGLE_REGISTER, 2 + 2, "S> S>" );
151              
152 3         192 return;
153 3     3   5 }
154              
155             =head1 METHODS
156              
157             =cut
158              
159             use constant {
160 2         696 REG_USET => 0x00, # centivolts
161             REG_ISET => 0x01, # miliamps
162             REG_UOUT => 0x02, # centivolts (RO)
163             REG_IOUT => 0x03, # miliiamps (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   14 };
  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         1  
184 1         5 {
185 1         6 await $self->_write_single_register( REG_USET, $voltage * 100 );
186 1     1 1 3056 }
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         2  
197 1         4 {
198 1         5 await $self->_write_single_register( REG_ISET, $current * 1000 );
199 1     1 1 119 }
200              
201             my %READINGS;
202              
203             # Build up all the reading methods
204             BEGIN {
205 2     2   12 use Object::Pad qw( :experimental(mop) );
  2         5  
  2         16  
206              
207 2     2   1059 my $metaclass = Object::Pad::MOP::Class->for_caller;
208              
209             %READINGS = ( # register, scale
210 2         9 output_voltage => [ REG_UOUT, sub { $_ / 100 } ],
211 2         10 output_current => [ REG_IOUT, sub { $_ / 1000 } ],
212 1         8 input_voltage => [ REG_UIN, sub { $_ / 100 } ],
213 0         0 output_protect => [ REG_PROTECT, sub { (qw( ok OVP OCP OPP ))[$_]} ],
214 2         130 output_mode => [ REG_CVCC, sub { (qw( CV CC ))[$_] } ],
  1         7  
215             );
216              
217 2         20 foreach my $name ( keys %READINGS ) {
218             $metaclass->add_method(
219 10         1379 "read_$name" => async method { return await $self->read_multiple( $name ) }
  4         9685  
  4         15  
  4         12  
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         9 async method read_multiple ( @names )
  5         11  
  5         8  
275 5         10 {
276 5         7 my ( $minreg, $maxreg );
277              
278             my @regs = map {
279 5 50       11 my $m = $READINGS{$_} or croak "Measurement '$_' is not recognised";
  6         19  
280 6         9 my $reg = $m->[0];
281 6 100 66     21 $minreg = $reg if !defined $minreg or $reg < $minreg;
282 6 50 66     36 $maxreg = $reg if !defined $maxreg or $reg > $maxreg;
283             } @names;
284              
285 5         15 my @values = await $self->_read_holding_registers( $minreg, $maxreg-$minreg+1 );
286              
287             return map {
288 5         301 my $m = $READINGS{$_};
  6         10  
289 6         19 $m->[1]->( local $_ = $values[$m->[0] - $minreg] );
290             } @names;
291 5     5 1 2911 }
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         2 async method set_output_state ( $on )
  1         2  
  1         3  
302 1         4 {
303 1         3 await $self->_write_single_register( REG_ONOFF, !!$on );
304 1     1 1 3264 }
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         4 {
316 1         4 return await $self->_read_holding_registers( REG_MODEL );
317 1     1 1 1636 }
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         17 {
329 1         5 return await $self->_read_holding_registers( REG_VERSION );
330 1     1 1 3702 }
331              
332             =head1 AUTHOR
333              
334             Paul Evans
335              
336             =cut
337              
338             0x55AA;