Files
epics-base/modules/ca/src/perl/capr.pl
T

486 lines
15 KiB
Perl

#!/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 = <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 <record type>\n",
" capr.pl [options] <record name> [<interest>]\n",
"\n",
" -h Print this help message.\n",
" -D Print debug messages.\n",
"Channel Access options:\n",
" -w <sec>: Wait time, specifies CA timeout, default is $opt_w second\n",
"Database Definitions:\n",
" -d <file.dbd>: 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 <record type>: Lists all fields with interest level, data type\n",
" and other information for the given record_type.\n",
" -l <interest>: interest level\n",
" -n <elems>: Maximum number of array elements to display\n",
" Default: $opt_n\n",
"\n",
"Base version: ", CA->version, "\n";
exit 1;
}