File Coverage

blib/lib/IO/HyCon.pm
Criterion Covered Total %
statement 30 269 11.1
branch 0 134 0.0
condition 0 35 0.0
subroutine 10 40 25.0
pod 30 30 100.0
total 70 508 13.7


line stmt bran cond sub pod time code
1             #
2             # The HyCon-package provides an object oriented interface to the HYCON
3             # hybrid controller for the Analogparadigm Model-1 analog computer.
4             #
5             # 06-AUG-2016 B. Ulmann Initial version
6             # 07-AUG-2016 B. Ulmann Added extensive error checking, changed
7             # c-/C-commands for easier interfacing
8             # 08-AUG-2016 B. Ulmann Analog calibration capability added
9             # 31-AUG-2016 B. Ulmann Support of digital potentiometers
10             # 01-SEP-2016 B. Ulmann Initial potentiometer setting based on
11             # configuration file etc.
12             # 13-MAY-2017 B. Ulmann Start adaptation to new, AVR2560-based hybrid
13             # controller with lots of new features
14             # 16-MAY-2017 B. Ulmann single_run_sync() implemented
15             # 08-FEB-2018 B. Ulmann Changed read_element to expect the name of a
16             # computing element instead of its address
17             # 01-SEP-2018 B. Ulmann Adapted to the final implementation of the
18             # hybrid controller (version 0.4)
19             # 02-SEP-2018 B. Ulmann Bug fixes, get_response wasn't implemented too
20             # cleverly, it is now much faster than before :-)
21             # 13-SEP-2018 B. Ulmann Fixed a warning problem when used with
22             # hc_gui.pl
23             # 20-FEB-2019 B. Ulmann Changed the reset routine within new since the
24             # old one sometimes failed
25             # 31-JUL-2019 B. Ulmann read_elements() does no longer implicitly halt
26             # the analog computer!
27             # set_pt() now limits values outside of the
28             # interval [-1, +1] to -1/+1 and croaks.
29             # 05-SEP-2019 B. Ulmann Added set_ro_group and read_ro_group functions.
30             # 11-SEP-2019 B. Ulmann Made HyCon into a proper Perl module suitable
31             # for CPAN.
32             # 12-SEP-2019 B. Ulmann Added requirements to Makefile.PL which were
33             # missing.
34             # 15-SEP-2019 B. Ulmann Fixed some typos in the POD.
35             # 21-SEP-2019 B. Ulmann set_ro_group expected decimal addresses instead
36             # of hexadecimal ones
37             # 29-SEP-2019 B. Ulmann new() now takes care of determining the
38             # configuration file name
39             # 28-OCT-2019 B. Ulmann Typos in documentation corrected.
40             # 14-DEC-2019 B. Ulmann Adapted to new firmware, added XBAR command, added
41             # DPT-query, set_address entfernt
42             # 16-DEC-2019 B. Ulmann set_pt expected a decimal potentiometer value
43             # while the P command of the HC expects it as hex...
44             # 17-DEC-2019 B. Ulmann Added support for data logging
45             # 18-DEC-2019 B. Ulmann Fixed bug in RO-group handling. The group was
46             # reset whenever a digital output was changed...
47             #
48              
49             package IO::HyCon;
50              
51             =pod
52              
53             =head1 NAME
54              
55             IO::HyCon - Perl interface to the Analog Paradigm hybrid controller.
56              
57             =head1 VERSION
58              
59             This document refers to version 1.1 of HyCon
60              
61             =head1 SYNOPSIS
62              
63             use strict;
64             use warnings;
65              
66             use File::Basename;
67             use HyCon;
68              
69             (my $config_filename = basename($0)) =~ s/\.pl$//;
70             print "Create object...\n";
71             my $ac = HyCon->new("$config_filename.yml");
72              
73             $ac->set_ic_time(500); # Set IC-time to 500 ms
74             $ac->set_op_time(1000); # Set OP-Time to 1000 ms
75             $ac->single_run(); # Perform a single computation run
76              
77             # Read a value from a specific computing element:
78             my $element_name = 'SUM8-0';
79             my $value = $ac->read_element($element_name);
80              
81             =head1 DESCRIPTION
82              
83             This module implements a simple object oriented interface to the Arduino\textregistered~ based
84             Analog Paradigm hybrid controller which interfaces an analog computer to a
85             digital computer and thus allows true hybrid computation.
86              
87             =cut
88              
89 1     1   53802 use strict;
  1         1  
  1         24  
90 1     1   3 use warnings;
  1         2  
  1         22  
91              
92 1     1   3 use vars qw($VERSION);
  1         2  
  1         42  
93             our $VERSION = '1.1';
94              
95 1     1   382 use YAML qw(LoadFile);
  1         7298  
  1         45  
96 1     1   6 use Carp qw(confess cluck carp);
  1         1  
  1         38  
97 1     1   650 use Device::SerialPort;
  1         24653  
  1         45  
98 1     1   452 use Time::HiRes qw(usleep);
  1         1103  
  1         3  
99 1     1   133 use File::Basename;
  1         1  
  1         56  
100              
101             use constant {
102 1         373 DIGITAL_OUTPUT_PORTS => 8,
103             DIGITAL_INPUT_PORTS => 8,
104             DPT_RESOLUTION => 10,
105             XBAR_CONFIG_BYTES => 10,
106 1     1   5 };
  1         2  
107              
108             my $instance;
109              
110             =head1 Functions and methods
111              
112             =head2 new($filename)
113              
114             This function generates a HyCon-object. Currently there is only one hybrid
115             controller supported, so this is, in fact, a singleton and every subsequent
116             invocation will cause a fatal error. If no configuration file path is supplied
117             as parameter, new() tries to open a YAML-file with the name of the currently
118             running program but with the extension '.yml' instead of '.pl'. This file is
119             assumed to have the following structure:
120              
121             config.yml:
122             serial:
123             port: /dev/tty.usbmodem621
124             bits: 8
125             baud: 250000
126             parity: none
127             stopbits: 1
128             poll_interval: 1000
129             poll_attempts: 200
130             types:
131             0: PS
132             1: SUM8
133             2: INT4
134             3: PT8
135             4: CU
136             5: MLT8
137             6: MDS2
138             7: CMP4
139             8: HC
140             elements:
141             Y_DDOT: 0x0100
142             Y_DOT: 0x0101
143             PT_8-0: 0x0220
144             PT_8-1: 0x0221
145             PT_8-2: 0x0222
146             PT_8-3: 0x0223
147             PT_8-4: 0x0224
148             PT_8-5: 0x0225
149             PT_8-6: 0x0226
150             PT_8-7: 0x0227
151             manual_potentiometers:
152             PT_8-0,rT_8-1,PT_8-2,PT_8-3,PT_8-4,PT_8-5,PT_8-6,PT_8-7
153              
154             The setup shown above will not fit your particular analog computer
155             configuration; it just serves as an example. The remaining parameters
156             nevertheless apply in general and are mostly self-explanatory. 'poll_interval'
157             and 'poll_attempts' control how often this interface will poll the hybrid
158             controller to get a response to a command issued before. The values shown above
159             are overly pessimistic but this won't matter during normal operation.
160              
161             If the number of values specified in the array 'values' does not match the
162             number of configured potentiometers, the function will abort.
163              
164             The 'types' section contains the mapping of the devices types as returned by
165             the analog computer's readout system to their module names. This should not
166             be changed but will be expanded when new analog computer modules will be
167             developed.
168              
169             The 'elements' section contains a list of computing elements defined by an
170             arbitrary name and their respective address in the computer system. Calling
171             read_all_elements() will switch the computer into HALT-mode, read the
172             values of all elements in this list and return a reference to a hash
173             containing all values and IDs of the elements read. (If jitter during readout
174             is to be minimized, a readout-group should be defined instead, see below.)
175              
176             Ideally, all manual potentiometers are listed under
177             'manual_potentiometers' which is used for automatic readout of the settings
178             of these potentiometers by calling read_mpts(). This is useful, if a
179             simulation has been parameterized manually and these parameters are required
180             for documentation purposes or the like. Caution: All potentiometers to be read
181             out by read_mpts() must be defined in the elements-section.
182              
183             The new() function will clear the communication buffer of the hybrid
184             controller by reading and discarding and data until a timeout will be
185             reached. This currently equals the product of 'poll_interval' and
186             'poll_attempts' and may take a few seconds during startup.
187              
188             =cut
189              
190             sub new {
191 0     0 1   my ($class, $config_filename) = @_;
192              
193 0 0         confess "Only one instance of a HyCon-object at a time is supported!"
194             if $instance++;
195              
196 0 0         ($config_filename = basename($0)) =~ s/\.pl$/\.yml/
197             unless defined($config_filename);
198              
199 0 0         my $config = LoadFile($config_filename) or
200             confess "Could not read configuration YAML-file: $!";
201              
202 0 0         my $port = Device::SerialPort->new($config->{serial}{port}) or
203             confess "Unable to open USB-port: $!\n";
204 0           $port->databits($config->{serial}{bits});
205 0           $port->baudrate($config->{serial}{baud});
206 0           $port->parity($config->{serial}{parity});
207 0           $port->stopbits($config->{serial}{stopbits});
208              
209             # If no poll-interval is specified, use 1000 microseconds
210 0   0       $config->{serial}{poll_interval} //= 1000;
211 0   0       $config->{serial}{poll_attempts} //= 200; # and 200 such intervals.
212              
213             # Get rid of any data which might still be in the serial line buffer
214 0           for my $i (1 .. 10) {
215 0 0         last if $port->lookfor();
216             }
217              
218             # Now reset the controller
219 0           print "Resetting the hybrid controller...\n";
220              
221 0           my ($attempt, $data);
222 0           for my $i (1 .. 10) {
223 0           print "Reset attempt $i\n";
224 0           $port->write('x'); # Reset the hybrid controller
225 0           sleep(1);
226 0 0         last if ($data = $port->lookfor()) eq 'RESET';
227             }
228 0 0         confess "Unexpected response from controller: >>$data<<\n" unless $data eq 'RESET';
229             # print "Lookfor: ", $port->lookfor(), "\n";
230             # while (++$attempt < $config->{serial}{poll_attempts}) {
231             # $data = $port->lookfor();
232             # last OUTER if $data eq 'RESET';
233             # usleep($config->{serial}{poll_interval});
234             # }
235             # }
236              
237             # Create the actual object
238 0           my $object;
239             {
240 1     1   19 no warnings 'uninitialized';
  1         3  
  1         2496  
  0            
241             $object = bless(my $self = {
242             port => $port,
243             poll_interval => $config->{serial}{poll_interval},
244             poll_attempts => $config->{serial}{poll_attempts},
245             elements => $config->{elements},
246             types => $config->{types},
247             times => {
248             ic_time => -1,
249             op_time => -1,
250             },
251             manual_potentiometers =>
252 0           [ split(/\s*,\s*/, $config->{manual_potentiometers}) ],
253             }, $class);
254             }
255              
256 0           return $object;
257             }
258              
259             =head2 get_response()
260              
261             In some cases, e.g. external HALT conditions, it is necessary to query the
262             hybrid controller for any messages which may have occured since the last
263             command. This can be done with this method - it will poll the controller
264             for a period of 'poll_interval' times 'poll_attemps' microseconds. If this
265             timeout value is not suitable, a different value (in milliseconds) can be
266             supplied as first argument of this method. If this argument is zero or negative,
267             get_response will wait indefinitely for a response from the hybrid controller.
268              
269             =cut
270              
271             sub get_response {
272 0     0 1   my ($self, $timeout) = @_;
273 0 0         $timeout = $self->{poll_interval} unless defined($timeout);
274              
275 0           my $attempt;
276             do {
277 0           my $response = $self->{port}->lookfor();
278 0 0         return $response if $response;
279             # If we poll indefinitely, there is no need to wait at all
280 0 0         usleep($timeout) if $timeout > 0;
281 0   0       } while ($timeout < 1 or ++$attempt < $self->{poll_attempts});
282             }
283              
284             =head2 ic()
285              
286             This method switches the analog computer to IC (initial condition) mode
287             during which the integrators are (re)set to their respective initial value.
288             Since this involves charging a capacitor to a given value, this mode should
289             be activated for the a minimum duration as required by the time scale factors
290             involved.
291              
292             ic() and the two following methods should not be used when timing is critical.
293             Instead, IC- and OP-times should be setup explicitly (see below) and then a
294             single-run should be initiated which will be under control of the hybrid
295             controller. This avoids latencies involved with the communication to and from
296             the hybrid controller and allows sub-millisecond resolution.
297              
298             =head2 op()
299              
300             This method switches the analog computer to operating-mode.
301              
302             =head2 halt()
303              
304             Calling this method causes the analog computer to switch to HALT-mode. In
305             this mode the integrators are halted and store their last value. After
306             calling halt() it is possible to return to OP-mode by calling op() again.
307             Depending on the analog computer being controlled, there will be a more or
308             less substantial drift of the integrators in HALT-mode, so it is advisable
309             to keep the HALT-periods as short as possible to minimize errors.
310              
311             A typical operation cycle may look like this: IC-OP-HALT-OP-HALT-OP-HALT.
312             This would start a single computation with the possibility of reading
313             values from the analog computer during the HALT-intervals.
314              
315             Another typical cycle is called 'repetitive operation' and looks like this:
316             IC-OP-IC-OP-IC-OP... This is normally used with the integrators set to
317             time-constants of 100 or 1000 and allows to display a solution as a more or
318             less flicker free curve on an oscilloscope for example.
319              
320             =head2 enable_ovl_halt()
321              
322             During a normal computation on an analog computation there should be no
323             overloads of summers or integrators. Such overload conditions are typically
324             the result of an erroneous computer setup (normally caused by wrong scaling of
325             the underlying equations). To catch such problems it is usually a good idea to
326             switch the analog computer automatically to HALT-mode when an overload occurs.
327             The computing element(s) causing the overload condition can the easily
328             identified on the analog computer's console and the variables of the computation
329             run can be read out to identify the cause of the problem.
330              
331             =head2 disable_ovl_halt()
332              
333             Calling this method will disable the automatic halt-on-overload
334             functionality of the hybrid controller.
335              
336             =head2 enable_ext_halt()
337              
338             Sometimes it is necessary to halt a computation when some condition is
339             satisfied (some value reached etc.). This is normally detected by a
340             comparator used in the analog computer setup. The hybrid controller
341             features an EXT-HALT input jack that can be connected to such a comparator.
342             After calling this method, the hybrid controller will switch the analog
343             computer from OP-mode to HALT as soon as the input signal patched to this
344             input jack goes high.
345              
346             =head2 disable_ext_halt()
347              
348             This method disables the HALT-on-overflow feature of the hybrid controller.
349              
350             =head2 single_run()
351              
352             Calling this method will initiate a so-called 'single-run' on the analog
353             computer which automatically performs the sequence IC-OP-HALT. The times
354             spent in IC- and OP-mode are specified with the methods set_ic_time() and
355             set_op_time() (see below).
356              
357             It should be noted that the hybrid controller will not be blocked during
358             such a single-run - it is still possible to issue other commands to read or
359             set ports etc.
360              
361             =head2 single_run_sync()
362              
363             This function behaves quite like single_run() but waits for the termination
364             of the single run, thus blocking any further program execution. This method
365             returns true, if the single-run mode was terminated by an external halt
366             condition. undef is returned otherwise.
367              
368             =head2 repetitive_run()
369              
370             This initiates repetitive operation, i.e. the analog computer is commanded
371             to perform an IC-OP-IC-OP-... sequence. The hybrid controller will not block
372             during this sequence. To terminate a repetitive run either ic() or halt()
373             may be called. Note that these methods act immediately and will interrupt any
374             ongoing IC- or OP-period of the analog computer.
375              
376             =head2 pot_set()
377              
378             This function switches the analog computer to POTSET-mode, i.e. the
379             integrators are set implicitly to HALT while all (manual) potentiometers
380             are connected to +1 on their respective input side. This mode can be used
381             to read the current settings of the potentiometers.
382              
383             =cut
384              
385             # Create basic methods
386             my %methods = (
387             ic => ['i', '^IC'], # Switch AC to IC-mode
388             op => ['o', '^OP'], # Switch AC to OP-mode
389             halt => ['h', '^HALT'], # Switch AC to HALT-mode
390             disable_ovl_halt => ['a', '^OVLH=DISABLED'], # Disable HALT-on-overflow
391             enable_ovl_halt => ['A', '^OVLH=ENABLED'], # Enable HALT-on-overflow
392             disable_ext_halt => ['b', '^EXTH=DISABLED'], # Disable external HALT
393             enable_ext_halt => ['B', '^EXTH=ENABLED'], # Enable external HALT
394             repetitive_run => ['e', '^REP-MODE'], # Switch to RepOp
395             single_run => ['E', '^SINGLE-RUN'], # One IC-OP-HALT-cycle
396             pot_set => ['S', '^PS'], # Activate POTSET-mode
397             );
398              
399             eval ('
400             sub ' . $_ . ' {
401             my ($self) = @_;
402             $self->{port}->write("' . $methods{$_}[0] . '");
403             my $response = get_response($self);
404             confess "No response from hybrid controller! Command was \'' .
405             $methods{$_}[0] . '\'." unless $response;
406             confess "Unexpected response from hybrid controller:\\n\\tCOMMAND=\'' .
407             $methods{$_}[0] . '\', RESPONSE=\'$response\', PATTERN=\'' .
408             $methods{$_}[1] . '\'\\n"
409             if $response !~ /' . $methods{$_}[1] . '/;
410             }
411 0 0   0 1   ') for keys(%methods);
  0 0   0 1    
  0 0   0 1    
  0 0   0 1    
  0 0   0 1    
  0 0   0 1    
  0 0   0 1    
  0 0   0 1    
  0 0   0 1    
  0 0   0 1    
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0 0          
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
412              
413             sub single_run_sync() {
414 0     0 1   my ($self) = @_;
415 0           $self->{port}->write('F');
416 0           my $response = get_response($self);
417 0 0         confess "No Response from hybrid controller! Command was 'F'"
418             unless $response;
419 0 0         confess "Unexpected response:\n\tCOMMAND='F', RESPONSE='$response'\n"
420             if $response !~ /^SINGLE-RUN/;
421 0           my $timeout = 1.1 * ($self->{times}{ic_time} + $self->{times}{op_time});
422 0           $response = get_response($self, $timeout);
423 0 0         confess "No Response during single_run_sync within $timeout ms"
424             unless $response;
425 0 0 0       confess "Unexpected response after single_run_sync: '$response'\n"
426             if $response !~ /^EOSR/ and $response !~ /^EOSRHLT/;
427             # Return true if the run was terminated by an external halt condition
428 0           return $response =~ /^EOSRHLT/;
429             }
430              
431             =head2 set_ic_time($milliseconds)
432              
433             It is normally advisable to let the hybrid controller take care of the overall
434             timing of OP and IC operations since the communication with the digital host
435             introduces quite some jitter. This method sets the time the analog computer
436             will spend in IC-mode during a single- or repetitive run. The time is
437             specified in milliseconds and must be positive and can not exceed 999999
438             milliseconds due to limitations of the hybrid controller firmware.
439              
440             =cut
441              
442             # Set IC-time
443             sub set_ic_time {
444 0     0 1   my ($self, $ic_time) = @_;
445 0 0 0       confess 'IC-time out of range - must be >= 0 and <= 999999!'
446             if $ic_time < 0 or $ic_time > 999999;
447 0           my $pattern = "^T_IC=$ic_time\$";
448 0           my $command = sprintf("C%06d", $ic_time);
449 0           $self->{port}->write($command);
450 0           my $response = get_response($self);
451 0 0         confess 'No response from hybrid controller!' unless $response;
452 0 0         confess "Unexpected response: '$response', expected: '$pattern'"
453             if $response !~ /$pattern/;
454 0           $self->{times}{ic_time} = $ic_time;
455             }
456              
457             =head2 set_op_time($milliseconds)
458              
459             This method specifies the duration of the OP-cycle(s) during a single- or
460             repetitive analog computer run. The same limitations hold with respect to the
461             value specified as for the set_ic_time() method.
462              
463             =cut
464              
465             # Set OP-time
466             sub set_op_time {
467 0     0 1   my ($self, $op_time) = @_;
468 0 0 0       confess 'OP-time out of range - must be >= 0 and <= 999999!'
469             if $op_time < 0 or $op_time > 999999;
470 0           my $pattern = "^T_OP=$op_time\$";
471 0           my $command = sprintf("c%06d", $op_time);
472 0           $self->{port}->write($command);
473 0           my $response = get_response($self);
474 0 0         confess 'No response from hybrid controller!' unless $response;
475 0 0         confess "Unexpected response: '$response', expected: '$pattern'"
476             if $response !~ /$pattern/;
477 0           $self->{times}{op_time} = $op_time;
478             }
479              
480             =head2 read_element($name)
481              
482             This function expects the name of a computing element specified in the
483             configuation YML-file and applies the corresponding 16 bit value $address to
484             the address lines of the analog computer's bus system, asserts the active-low
485             /READ-line, reads one value from the READOUT-line, and de-asserts /READ again.
486             read_element(...) returns a reference to a hash containing the keys 'value' and
487             'id'.
488              
489             =cut
490              
491             sub read_element {
492 0     0 1   my ($self, $name) = @_;
493 0           my $address = hex($self->{elements}{$name});
494 0 0         confess "Computing element $name not configured!\n"
495             unless defined($address);
496 0           $self->{port}->write('g' . sprintf("%04X", $address & 0xffff));
497 0           my $response = get_response($self);
498 0 0         confess 'No response from hybrid controller!' unless $response;
499 0           my ($value, $id) = split(/\s+/, $response);
500 0   0       $id = $self->{types}{$id & 0xf} || 'UNKNOWN';
501 0           return { value => $value, id => $id};
502             }
503              
504             =head2 read_element_by_address($address)
505              
506             This function expects the 16 bit address of a computing element as
507             parameter and returns a data structure identically to that returned by
508             read_element. This routine should not be used in general as computing elements
509             are better addressed by their name. It is mainly provided for completeness.
510              
511             =cut
512              
513             sub read_element_by_address {
514 0     0 1   my ($self, $address) = @_;
515 0           $self->{port}->write('g' . sprintf("%04X", $address & 0xffff));
516 0           my $response = get_response($self);
517 0 0         confess 'No response from hybrid controller!' unless $response;
518 0           my ($value, $id) = split(/\s+/, $response);
519 0   0       $id = $self->{types}{$id & 0xf} || 'UNKNOWN';
520 0           return { value => $value, id => $id};
521             }
522              
523             =head2 get_data()
524              
525             get_data() reads data from the internal logging facility of the hybrid
526             controller. When a readout group has been defined and a single_run is
527             executed, the hybrid controller will gather data from the readout-group
528             automatically. There are 1024 memory cells for 16 bit data in the
529             hybrid controller. The sample rate is automatically determined.
530              
531             =cut
532              
533             sub get_data {
534 0     0 1   my ($self) = @_;
535 0           my $data = [];
536 0           $self->{port}->write('l');
537 0           while (1) {
538 0           my $response = get_response($self);
539 0 0 0       last if $response eq 'No data!' or $response =~ /EOD/;
540 0           my @values = split(/\s+/, $response);
541 0 0         push(@$data, @values == 1 ? $values[0] : \@values);
542             }
543              
544 0           return $data;
545             }
546              
547             =head2 read_all_elements()
548              
549             The routine read_all_elements() reads the current values from all elements
550             listed in the 'elements' section of the configuration file. It returns a
551             reference to a hash containing all elements read with their associated values
552             and IDs. It may be advisable to switch the analog computer to HALT mode before
553             calling read_all_elements() to minimize the effect of jitter. After calling
554             this routine the computer has to be switched back to OP mode again. A better
555             way to readout groups of elements is by means of a readout-group (see below).
556              
557             =cut
558              
559             sub read_all_elements {
560 0     0 1   my ($self) = @_;
561 0           my %result;
562 0           for my $key (sort(keys(%{$self->{elements}}))) {
  0            
563 0           my $result = $self->read_element($key);
564 0           $result{$key} = { value => $result->{value}, id => $result->{id} };
565             }
566 0           return \%result;
567             }
568              
569             =head2 set_ro_group()
570              
571             This function defines a readout group, i.e. a group of computing elements
572             specified by their respective names as defined in the configuration file. All
573             elements of such a readout group can be read by issuing a single call to
574             read_ro_group(), thus reducing the communications overhead between the HC and
575             digital computer substantially. A typical call would look like this (provided
576             the names are defined in the configuration file):
577              
578             $ac->set_ro_group('INT0_1', 'SUM2_3');
579              
580             =cut
581              
582             sub set_ro_group {
583 0     0 1   my ($self, @names) = @_;
584              
585 0           my @addresses;
586 0           for my $name (@names) {
587             confess "Computing element $name not configured!\n"
588 0 0         unless defined($self->{elements}{$name});
589 0           push(@addresses, $self->{elements}{$name});
590             }
591 0           $self->{'RO-GROUP'} = \@names;
592 0           my $command = 'G' . join(';', @addresses) . '.';
593 0           $self->{port}->write($command);
594             }
595              
596             =head2 read_ro_group()
597              
598             read_ro_group() reads all elements defined in a readout group. This minimizes
599             the communications overhead between digital and analog computer and reduces
600             the effect of jitter during readout as well as the risk of a serial line buffer
601             overflow on the side of the hybrid controller. The function returns a reference
602             to a hash containing the names of the elements forming the readout group with
603             their associated values.
604              
605             =cut
606              
607             sub read_ro_group {
608 0     0 1   my ($self) = @_;
609 0           $self->{port}->write('f'); # Issue read-ro-group command
610 0           my @values = split(/\s*;\s*/, get_response($self));
611 0           my %result;
612 0           $result{$_} = shift(@values) for @{$self->{'RO-GROUP'}};
  0            
613 0           return \%result;
614             }
615              
616             =head2 read_digital()
617              
618             In addition to these analog readout capabilities, the hybrid controller also
619             features eight digital inputs which can be used to read the state of
620             comparators or other logic elements of the analog computer being controlled.
621             This method returns an array-reference containing values of 0 or 1 for each of
622             the digital input ports.
623              
624             =cut
625              
626             # Read digital inputs
627             sub read_digital {
628 0     0 1   my ($self) = @_;
629 0           $self->{port}->write('R');
630 0           my $response = get_response($self);
631 0 0         confess 'No response from hybrid controller!' unless $response;
632 0           my $pattern = '^' . '\d+\s+' x (DIGITAL_INPUT_PORTS - 1) . '\d+';
633 0 0         confess "Unexpected response: '$response', expected: '$pattern'"
634             if $response !~ /$pattern/;
635 0           return [ split(/\s+/, $response) ];
636             }
637              
638             =head2 digital_output($port, $value)
639              
640             The hybrid controller also features eight digital outputs which can be used to
641             control the electronic switches which are part of the comparator unit. Calling
642             digital_output(0, 1) will set the first (0) digital output to 1 etc.
643              
644             =cut
645              
646             # Set/reset digital outputs
647             sub digital_output {
648 0     0 1   my ($self, $port, $state) = @_;
649 0 0 0       confess '$port must be >= 0 and < ' . DIGITAL_OUTPUT_PORTS
650             if $port < 0 or $port > DIGITAL_OUTPUT_PORTS;
651 0 0         $self->{port}->write(($state ? 'D' : 'd') . $port);
652             }
653              
654             =head2 set_xbar()
655              
656             set_xbar sends a configuration bitstream to an XBAR-module specified by its
657             name in the elements section of the configuration file. The routine expects
658             two parameters: The name of the XBAR-module and a HEX-number,
659             XBAR_CONFIG_BYTES * 2 nibbles in length.
660              
661             =cut
662              
663             sub set_xbar {
664 0     0 1   my ($self, $name, $config) = @_;
665 0 0         confess "XBAR-module >>$name<< not defined!" unless defined($self->{elements}{$name});
666 0 0         confess 'Exactly ', XBAR_CONFIG_BYTES * 2, ' HEX-nibbles are required as config data! Only ',
667             length($config), ' were found!' if length($config) != XBAR_CONFIG_BYTES * 2;
668 0           my $address = sprintf('%04X', hex($self->{elements}{$name}));
669 0           my $command = "X$address$config";
670 0           $self->{port}->write($command);
671 0           my $response = get_response($self); # Get response
672 0 0         confess 'No response from hybrid controller!' unless $response;
673 0 0         confess "Configuring XBAR failed: >>$response<<." unless $response eq 'XBAR READY';
674             }
675              
676             =head2 read_mpts()
677              
678             Calling read_mpts() returns a reference to a hash containing the current
679             settings of all manual potentiometers listed in the
680             'manual_potentiometers' section in the configuration file. To accomplish this,
681             the analog computer is switched to POTSET-mode (implying HALT for the
682             integrators). In this mode, all inputs of potentiometers are connected to
683             the positive machine unit +1, so that their current setting can be read out.
684             ("Free" potentiometers will behave erroneously unless their second input is
685             connected to ground, refer to the analog computer manual for more information
686             on that topic.)
687              
688             =cut
689              
690             sub read_mpts {
691 0     0 1   my ($self) = @_;
692 0           $self->pot_set();
693 0           my %result;
694 0           for my $key (@{$self->{manual_potentiometers}}) {
  0            
695 0           my $result = $self->read_element($key);
696 0           $result{$key} = { value => $result->{value}, id => $result->{id} };
697             }
698 0           return \%result;
699             }
700              
701             =head2 set_pt($name, $value)
702              
703             To set a digital potentiometer, set_pt() is called. The first argument is the
704             name of the the digital potentiometer to be set as specified in the elements
705             section in the configuration YML-file (an entry like 'DPT24-2: 0060/2'). The
706             second argument is a floating point value 0 <= v <= 1. If the potentiometer to
707             be set can not be found in the configuration data or if the value is out of
708             bounds, the function will die.
709              
710             =cut
711              
712             sub set_pt {
713 0     0 1   my ($self, $pot, $value) = @_;
714 0 0         confess "Potentiometer >>$pot<< not defined!" unless defined($self->{elements}{$pot});
715 0           my ($address, $number) = split('/', $self->{elements}{$pot});
716              
717 0 0 0       if ($value < 0 or $value > 1) {
718 0           carp "$value must be >= 0 and <= 1, has been limited\n";
719 0 0         $value = 1 if $value > 1;
720 0 0         $value = 0 if $value < 0;
721             }
722              
723             # Convert value to an integer suitable to setting the potentiometer and
724             # generate fixed length strings for the parameters address (single digit)
725             # and value (three digits, 0000 <= value <= 1023):
726 0           $value = sprintf('%04d', int($value * (2 ** DPT_RESOLUTION - 1)));
727              
728 0           $address = sprintf('%04X', hex($address)); # Make sure we have a four digit hex value
729 0           $number = sprintf('%02X', hex($number)); # Make sure we have a two digital pot number
730              
731 0           $self->{port}->write("P$address$number$value");
732              
733 0           my $response = get_response($self); # Get response
734 0 0         confess 'No response from hybrid controller!' unless $response;
735 0           my ($raddress, $rnumber, $rvalue) = $response =~ /^P([^.]+)\.([^=]+)=(\d+)$/;
736 0 0 0       confess "set_pt failed! $address vs. $raddress, $rnumber vs. $number, $value vs. $rvalue"
      0        
737             if (hex($address) != hex($raddress)) or (hex($number) != hex($rnumber)) or ($value != $rvalue);
738             }
739              
740             =head2 read_dpts()
741              
742             Read the current setting of all digital potentiometers. Caution: This does
743             not query the actual potentiometers as there is not readout capability
744             on the modules containing DPTs, instead this function will query the hybrid
745             controller to return the values it has stored when DPTs were set.
746              
747             =cut
748              
749             sub read_dpts {
750 0     0 1   my ($self) = @_;
751 0           $self->{port}->write('q');
752 0           my $response = get_response($self);
753 0 0         confess 'No response from hybrid controller!' unless $response;
754 0           my %result;
755 0           for my $entry (split(';', $response)) {
756 0           my ($address, $data) = split(':', $entry);
757 0           my @values;
758 0           push(@values, $_) for split(',', $data);
759 0           $result{$address} = \@values;
760             }
761 0           return \%result;
762             }
763              
764             =head2 get_status()
765              
766             Calling get_status() yields a reference to a hash containing all current
767             status information of the hybrid controller. A typical hash structure
768             returned may look like this:
769              
770             $VAR1 = {
771             'IC-time' => '500',
772             'MODE' => 'HALT',
773             'OP-time' => '1000',
774             'STATE' => 'NORM',
775             'OVLH' => 'DIS',
776             'EXTH' => 'DIS',
777             'RO_GROUP' => [..., ..., ...],
778             'DPTADDR' => [60 => 9, 80 => 8, ], # hex address and module id
779             };
780              
781             In this case the IC-time has been set to 500 ms while the OP-time is set to
782             one second. The analog computer is currently in HALT-mode and the hybrid
783             controller is in its normal state, i.e. it is not currently performing a
784             single- or repetitive-run. HALT on overload and external HALT are both
785             disabled. A readout-group has been defined, too.
786              
787             =cut
788              
789             sub get_status {
790 0     0 1   my ($self) = @_;
791 0           $self->{port}->write('s');
792 0           my $response = get_response($self);
793 0 0         confess 'No response from hybrid controller!' unless $response;
794 0           my %state;
795 0           for my $entry (split(/\s*,\s*/, $response)) {
796 0           my ($parameter, $value) = split(/\s*=\s*/, $entry);
797 0           $state{$parameter} = $value;
798             }
799              
800 0           my @addresses = split(/\s*;\s*/, $state{'RO-GROUP'});
801 0           $state{'RO-GROUP'} = \@addresses;
802              
803 0           my %mapping;
804 0           for my $entry (split(';', $state{DPTADDR})) {
805 0           my ($address, $module_id) = split('/', $entry);
806 0           $mapping{$address} = $module_id;
807             }
808 0           $state{DPTADDR} = \%mapping;
809              
810 0           return \%state;
811             }
812              
813             =head2 get_op_time()
814              
815             In some applications it is useful to be able to determine how long the analog
816             computer has been in OP-mode. As time as such is the only free variable of
817             integration in an analog-electronic analog computer, it is a central parameter
818             to know. Imagine that some integration is being performed by the analog
819             computer and the time which it took to reach some threshold value is of
820             interest. In this case, the hybrid controller would be configured
821             so that external-HALT is enabled. Then the analog computer would be placed to
822             IC-mode and then to OP-mode. After an external HALT has been triggered by some
823             comparator of the analog commputer, the hybrid controller will switch the
824             analog computer to HALT-mode immediately. Afterwards, the time the analog
825             computer spent in OP-mode can be determined by calling this method. The time
826             will be returned in microseconds (the resolution is about +/- 3 to 4
827             microseconds).
828              
829             =cut
830              
831             # Get current time the AC spent in OP-mode
832             sub get_op_time {
833 0     0 1   my ($self) = @_;
834 0           $self->{port}->write('t');
835 0           my $response = get_response($self);
836 0 0         confess 'No response from hybrid controller!' unless $response;
837 0           my $pattern = 't_OP=\-?\d*';
838 0 0         confess "Unexpected response: '$response', expected: '$pattern'"
839             if $response !~ /$pattern/;
840 0           my ($time) = $response =~ /=\s*(\-?\d+)$/;
841 0 0         return $time ? $time : -1;
842             }
843              
844             =head2 reset()
845              
846             The reset() method resets the hybrid controller to its initial setup. This
847             will also reset all digital potentiometer settings including their number!
848             During normal operations it should not be necessary to call this method which
849             was included primarily to aid debugging.
850              
851             =cut
852              
853             sub reset {
854 0     0 1   my ($self) = @_;
855 0           $self->{port}->write('x');
856 0           my $response = get_response($self);
857 0 0         confess 'No response from hybrid controller!' unless $response;
858 0 0         confess "Unexpected response: '$response', expected: 'RESET'"
859             if $response ne 'RESET';
860             }
861              
862             =head1 Examples
863              
864             The following example initates a repetitive run of the analog computer with 20
865             ms of operating time and 10 ms IC time:
866              
867             use strict;
868             use warnings;
869              
870             use File::Basename;
871             use HyCon;
872              
873             my $ac = HyCon->new();
874              
875             $ac->set_op_time(20);
876             $ac->set_ic_time(10);
877              
878             $ac->repetitive_run();
879              
880             =cut
881              
882             =head1 AUTHOR
883              
884             Dr. Bernd Ulmann, ulmann@analogparadigm.com
885              
886             =cut
887              
888             return 1;