File Coverage

blib/lib/IO/HyCon.pm
Criterion Covered Total %
statement 33 366 9.0
branch 0 192 0.0
condition 0 68 0.0
subroutine 11 45 24.4
pod 30 33 90.9
total 74 704 10.5


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