#!/usr/bin/env perl #************************************************************************* # Copyright (c) 2022 UChicago Argonne LLC, as Operator of Argonne # National Laboratory. # SPDX-License-Identifier: EPICS # EPICS BASE is distributed subject to a Software License Agreement found # in file LICENSE that is included with this distribution. #************************************************************************* ####################################################################### # # capr: A program that attempts to do a "dbpr" command via channel # access. # ####################################################################### use 5.10.1; use strict; use FindBin qw($RealBin); use lib ("$RealBin/../../lib/perl"); use Getopt::Std; use Cwd 'abs_path'; use CA; ######### Globals ########## our ($opt_h, $opt_f, $opt_l, $opt_r, $opt_D); our $opt_d = $ENV{EPICS_CAPR_DBD_FILE} // abs_path("$RealBin/../../dbd/softIoc.dbd"); our $opt_w = 2; our $opt_n = 10; my %record = (); # Empty hash to put dbd data in # EPICS field types my %fieldType = ( DBF_CHAR => 'DBF_CHAR', DBF_UCHAR => 'DBF_CHAR', DBF_DOUBLE => 'DBF_FLOAT', DBF_FLOAT => 'DBF_FLOAT', DBF_LONG => 'DBF_LONG', DBF_INT64 => 'DBF_FLOAT', DBF_SHORT => 'DBF_LONG', DBF_ULONG => 'DBF_LONG', DBF_USHORT => 'DBF_LONG', DBF_UINT64 => 'DBF_FLOAT', DBF_DEVICE => 'DBF_STRING', DBF_ENUM => 'DBF_STRING', DBF_FWDLINK => 'DBF_STRING', DBF_INLINK => 'DBF_STRING', DBF_MENU => 'DBF_STRING', DBF_OUTLINK => 'DBF_STRING', DBF_STRING => 'DBF_STRING', DBF_NOACCESS => 'DBF_NOACCESS', ); # globals for sub caget my %connected; my $unconnected_count; my %callback_data; my $callback_incomplete; ######### Main program ############ HELP_MESSAGE() unless getopts('hDd:f:l:n:rw:'); HELP_MESSAGE() if $opt_h; die "File $opt_d not found. (\"capr.pl -h\" gives help)\n" unless -f $opt_d; parseDbd($opt_d); print "Using $opt_d\n\n"; # Print a list of record types if ($opt_r) { print ("Record types found:\n"); printList($opt_l); exit; } # Print the fields defined for given record if ($opt_f) { printRecordList($opt_f); exit; } HELP_MESSAGE() unless @ARGV; $_ = shift; if (@ARGV) { # Drop any ".FIELD" part s/\. \w+ $//x; printRecord($_, @ARGV); } else { if (m/^ \s* ([]+:;<>0-9A-Za-z[-]+) (?:\. \w+)? \s* , \s* (\d+) \s* $/x) { # Recognizes ",n" as an interest level, drops any ".FIELD" part printRecord($1, $2); } else { # Drop any ".FIELD" part s/\. \w+ $//x; printRecord($_, $opt_l); } } ########## End of main ########### # parseDbd # Takes given dbd file and parses it to produce a hash table of record types # giving their fields, and for each field its interest level and data type. # usage: parseDbd("fileName"); # Output goes to the global %record, a hash of references to other hashes # containing the fields of each record type. Those hash values (keyed by # field name) are references to arrays containing the interest level, data # type and number base of the field. sub parseDbd { my $dbdFile = shift; open(DBD, "< $dbdFile") or die "Can't open file $dbdFile: $!\n"; my @dbd = ; close(DBD) or die "Can't close $dbdFile: $!\n"; my $i = 1; my $level = 0; my $isArecord = 0; my $isAfield = 0; my $thisRecord; my $thisField; my $thisType; my $thisSize = 0; my $field = {}; my $interest = 0; my $thisBase = 'DECIMAL'; my $special = ''; while (@dbd) { $_ = shift @dbd; chomp; if ( m/recordtype \s* \( \s* (\w+) \)/x ) { die "File format error at line $i of file\n $opt_d\n" unless $level == 0; $isArecord = 1; $thisRecord = $1; } elsif ( m/field \s* \( \s* (\w+) \s* , \s* (\w+) \s* \)/x ) { die "File format error at line $i of file\n $opt_d\n" unless $level == 1 && $isArecord; $thisField = $1; $thisType = $2; $isAfield = 1; $thisSize = 1024 if $thisType =~ m/^ DBF_(IN|OUT|FWD)LINK $/x; } elsif ( m/interest \s* \( \s* (\w+) \s* \)/x ) { die "File format error at line $i of file\n $opt_d\n" unless $level == 2 && $isAfield; $interest = $1; } elsif ( m/size \s* \( \s* ([0-9]+) \s* \)/x ) { die "File format error at line $i of file\n $opt_d\n" unless $level == 2 && $isAfield; $thisSize = $1; } elsif ( m/special \s* \( \s* (\w+) \s* \)/x ) { die "File format error at line $i of file\n $opt_d\n" unless $level == 2 && $isAfield; $special = $1; } elsif ( m/base \s* \( \s* (\w+) \s* \)/x ) { die "File format error at line $i of file\n $opt_d\n" unless $level == 2 && $isAfield; $thisBase = $1; } if ( m/\{/ ) { $level++; } if ( m/\}/ ) { if ($level == 2 && $isAfield) { my $params = {}; $params->{interest} = $interest; $params->{dbfType} = $thisType; $params->{base} = $thisBase; $params->{special} = $special; $params->{size} = $thisSize; $field->{$thisField} = $params; # Reset default values $isAfield = 0; $interest = 0; $thisBase = 'DECIMAL'; $special = ''; $thisSize = 0; } elsif ($level == 1 && $isArecord) { $isArecord = 0; $record{$thisRecord} = $field; $field = {}; # start another hash } $level--; } $i++; } } # Given a record name, attempts to find the record and its type. # Usage: $recordType = getRecType("recordName"); sub getRecType { my $arg = shift; my $name = "$arg.RTYP"; my $fields_read = caget($name); die "Could not determine record type of $arg\n" unless $fields_read == 1; return $callback_data{$name}; } # Given the record type and field, returns the interest level, data type # and number base for the field # Usage: ($dataType, $interest, $base, $special, $size) = getFieldParams($recType, $field); sub getFieldParams { my ($recType, $field) = @_; my $params = $record{$recType}{$field} or die "Can't find params for $recType.$field"; exists($fieldType{$params->{dbfType}}) || die "Field data type $field for $recType not found in dbd file --"; exists($params->{interest}) || die "Interest level for $field in $recType not found in dbd file --"; my $fType = $fieldType{$params->{dbfType}}; my $fInterest = $params->{interest}; my $fBase = $params->{base}; my $fSpecial = $params->{special}; my $fSize = $params->{size}; return ($fType, $fInterest, $fBase, $fSpecial, $fSize); } # Prints field name and data for given field. Formats output so # that fields align in to 4 columns. Tries to imitate dbpf format # Usage: printField( $fieldName, $data, $dataType, $base, $firstColumnPosn) sub printField { my ($fieldName, $fieldData, $dataType, $base, $col) = @_; my $screenWidth = 80; my ($outStr, $wide); if (ref $fieldData eq 'ARRAY') { my $elems = scalar @{$fieldData}; my $field = "$fieldName\[$elems\]:"; my $count = $elems > $opt_n ? $opt_n : $elems; my @show = @{$fieldData}[0 .. $count - 1]; $outStr = sprintf('%-5s %s', $field, join(', ', @show)); $outStr .= ", ..." if $elems > $count; } else { my $field = "$fieldName:"; if ( $dataType eq 'DBF_STRING' ) { $outStr = sprintf('%-5s %s', $field, $fieldData); } elsif ( $base eq 'HEX' ) { my $val = ( $dataType eq 'DBF_CHAR' ) ? ord($fieldData) : $fieldData; $outStr = sprintf('%-5s 0x%x', $field, $val); } elsif ( $dataType eq 'DBF_DOUBLE' || $dataType eq 'DBF_FLOAT' ) { $outStr = sprintf('%-5s %.8f', $field, $fieldData); } elsif ( $dataType eq 'DBF_UCHAR' ) { $outStr = sprintf('%-5s %d', $field, ord($fieldData)); } else { # DBF_INT64, DBF_LONG, DBF_SHORT, # DBF_UINT64, DBF_ULONG, DBF_USHORT, DBF_UCHAR, $outStr = sprintf('%-5s %d', $field, $fieldData); } } my $len = length($outStr); if ($len < 20) { $wide = 20; } elsif ( $len < 40 ) { $wide = 40; } elsif ( $len < 60 ) { $wide = 60; } else { $wide = 80;} my $pad = $wide - $len; $col += $wide; if ($col > $screenWidth ) { print("\n"); $col = $wide; } print $outStr, ' ' x $pad; return $col; } # Query the native values of a list of PVs simultaneously. # The data is returned in the the %callback_data global hash. # The return value is the number of read pvs # # NOTE: Not re-entrant because results are written to global hash # %callback_data # # Usage: $fields_read = caget( @pvlist ) sub caget { #clear any previous results; %connected = (); $unconnected_count = scalar @_; %callback_data = (); $callback_incomplete = 0; my @chans = map { print " Creating channel for $_\n" if $opt_D; CA->new($_, \&canew_callback); } @_; my $channel_count = scalar @chans; return 0 unless $channel_count gt 0; print " $channel_count channels created.\n" if $opt_D; my $elapsed = 0; do { print " Waiting for $unconnected_count channels to connect\n" if $unconnected_count && $opt_D; print " Waiting for data from $callback_incomplete channels\n" if $callback_incomplete && $opt_D; CA->pend_event(0.1); $elapsed += 0.1; } until (($elapsed > $opt_w) or (scalar %connected && $callback_incomplete == 0)); my $data_count = scalar keys %callback_data; printf " Got data from %d of %d channels\n", $data_count, $channel_count if $opt_D; return $data_count; } sub canew_callback { my ($chan, $up) = @_; return unless $up; $connected{$chan->name} = $chan; $unconnected_count--; my $ftype = $chan->field_type; my $count = $chan->element_count; $ftype = 'DBR_LONG' if $ftype eq 'DBR_CHAR' && $count == 1; print " Getting ${\$chan->name} as $ftype\n" if $opt_D; # We have to fetch all elements so we can show how many there are $chan->get_callback(\&caget_callback, $ftype); $callback_incomplete++; } sub caget_callback { my ($chan, $status, $data) = @_; die $status if $status; print " Got ${\$chan->name} = '$data'\n" if $opt_D; $callback_data{$chan->name} = $data; $callback_incomplete--; } # Given record name and interest level prints data from record fields # that are at or below the interest level specified. # Usage: printRecord($recordName, $interestLevel) sub printRecord { my ($name, $interest) = @_; my $recType = getRecType($name); print("Record $name type $recType\n"); die "Record type $recType not found\n" unless exists $record{$recType}; #capture list of fields my @readlist = (); #fields to read via CA my @fields_pr = (); #fields for print-out my @ftypes = (); #types, from parser my @bases = (); #bases, from parser foreach my $field (sort keys %{$record{$recType}}) { my ($fType, $fInterest, $base, $special, $size) = getFieldParams($recType, $field); next if $fInterest > $interest; my $fToGet = "$name.$field"; if ($fType eq 'DBF_NOACCESS') { next unless $special eq 'SPC_DBADDR'; $fType = 'DBF_STRING'; } elsif ($fType eq 'DBF_STRING' && $size >= 40) { $fToGet .= '$'; } push @fields_pr, $field; push @readlist, $fToGet; push @ftypes, $fType; push @bases, $base; } my $fields_read = caget( @readlist ); my @missing; # print while iterating over lists gathered my $col = 0; for (my $i=0; $i < scalar @readlist; $i++) { my $field = $fields_pr[$i]; my $fToGet = $readlist[$i]; my ($fType, $data, $base); push @missing, $field unless exists $callback_data{$fToGet}; next unless exists $callback_data{$fToGet}; $fType = $ftypes[$i]; $base = $bases[$i]; $data = $callback_data{$fToGet}; $col = printField($field, $data, $fType, $base, $col); } print("\n"); # Final newline printf "\nUnreadable fields: %s\n", join(', ', @missing) if @missing && $opt_D; } # Prints list of record types found in dbd file. If level > 0 # then uses printRecordList to display all fields in each record type. sub printList { my $level = shift; foreach my $rkey (sort keys(%record)) { if ($level > 0) { printRecordList($rkey); } else { print(" $rkey\n"); } } } # Prints list of fields and metadata for given record type sub printRecordList { my $type = shift; if (exists($record{$type}) ) { print("Record type - $type\n"); foreach my $fkey (sort keys %{$record{$type}}) { my $param = $record{$type}{$fkey}; my $dbfType = $param->{dbfType}; printf " %-8s", $fkey; printf " interest = %d", $param->{interest}; printf " type = %-12s", $dbfType; if ($dbfType eq 'DBF_STRING') { printf " size = %s\n", $param->{size}; } elsif ($dbfType =~ m/DBF_U?(CHAR|SHORT|INT|LONG|INT64)/) { printf " base = %s\n", $param->{base}; } elsif ($dbfType eq 'DBF_NOACCESS') { printf " special = %s\n", $param->{special}; } else { print "\n"; } } } else { print("Record type $type not defined in dbd file $opt_d\n"); } } sub HELP_MESSAGE { print STDERR "\n", "Usage: capr.pl -h\n", " capr.pl [options] -r\n", " capr.pl [options] -f \n", " capr.pl [options] []\n", "\n", " -h Print this help message.\n", " -D Print debug messages.\n", "Channel Access options:\n", " -w : Wait time, specifies CA timeout, default is $opt_w second\n", "Database Definitions:\n", " -d : The file containing record type definitions.\n", " This can be set using the EPICS_CAPR_DBD_FILE environment variable.\n", " Default: ", abs_path($opt_d), "\n", "Output Options:\n", " -r Lists all record types in the selected dbd file.\n", " -f : Lists all fields with interest level, data type\n", " and other information for the given record_type.\n", " -l : interest level\n", " -n : Maximum number of array elements to display\n", " Default: $opt_n\n", "\n", "Base version: ", CA->version, "\n"; exit 1; }