File Coverage

blib/lib/IO/HyCon.pm
Criterion Covered Total %
statement 33 356 9.2
branch 0 182 0.0
condition 0 53 0.0
subroutine 11 45 24.4
pod 30 33 90.9
total 74 669 11.0


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