369da70f2a
* format: Strip trailing whitespaces
We want to get rid of trailing whitespaces completly as they make just git
noice. Much better to start using automated tools to get rid of them once and
not getting them back again. This way git history will be cleaner and review
easier.
Commit was generated with:
pre-commit run --all-files trailing-whitespace
* format: Files should have exactly one new line end of them
It is good practice that every file has one new line. It is not now days so
mandatory but it also is not nice if file has lot of newlines end of it. We will
use pre-commit which takes automatically care about this so let's fix all.
Commit was generated with:
pre-commit run --all-files end-of-file-fixer
* format: Convert tabs to spaces
Project mostly use spaces over tabs. When mixing tabs and spaces this usually
makes formatting issues and also when changing those in commits it will make lot
of git noise. We will force spaces most of the time and use pre-commit to fix.
Commit was generated with:
pre-commit run --all-files remove-tabs
---------
Co-authored-by: Kari Argillander <kari.argillander@fidelix.com>
869 lines
25 KiB
Perl
869 lines
25 KiB
Perl
use warnings;
|
|
use strict;
|
|
use Getopt::Long;
|
|
use Convert::Binary::C;
|
|
use Hash::Util qw/lock_hash/;
|
|
use English;
|
|
use Scalar::Util qw/looks_like_number/;
|
|
use File::Basename;
|
|
use File::Spec;
|
|
use Pod::Usage;
|
|
use Carp;
|
|
|
|
=head1 NAME
|
|
|
|
bacnet.pl - Scriptable BACnet communications
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
This is a tool for scriptable BACnet communication. Users can write their own
|
|
scripts using standard Perl syntax and API defined in this tool to perform desired
|
|
execution sequences. For details on this tool's API, see Documentation.html. For other
|
|
Perl documentation, see http://perldoc.perl.org
|
|
|
|
=begin html
|
|
<link href="syntax.css" rel="stylesheet" type="text/css">
|
|
<script src="jquery.js"></script>
|
|
<script src="syntax.js"></script>
|
|
|
|
=end html
|
|
|
|
=head1 OPTIONS
|
|
|
|
Usage: bacnet.pl [program_options] [-- script_args]
|
|
|
|
This program executes a script in perl syntax to perform BACnet/IP operations.
|
|
|
|
Possible program options:
|
|
--script=s The script to execute.
|
|
--log=s The file to log all output.
|
|
--help This help message.
|
|
|
|
Possible environment variables are:
|
|
BACNET_IFACE - set this value to dotted IP address of the interface (see
|
|
ipconfig) for which you want to bind. Default is the interface which
|
|
Windows considers to be the default (how???). Hence, if there is only a
|
|
single network interface on Windows, the applications will choose it, and
|
|
this setting will not be needed.
|
|
BACNET_IP_PORT - UDP/IP port number (0..65534) used for BACnet/IP
|
|
communications. Default is 47808 (0xBAC0).
|
|
BACNET_APDU_TIMEOUT - set this value in milliseconds to change the APDU
|
|
timeout. APDU Timeout is how much time a client waits for a response from
|
|
a BACnet device.
|
|
BACNET_BBMD_PORT - UDP/IP port number (0..65534) used for Foreign Device
|
|
Registration. Defaults to 47808 (0xBAC0).
|
|
BACNET_BBMD_TIMETOLIVE - number of seconds used in Foreign Device
|
|
Registration (0..65535). Defaults to 60000 seconds.
|
|
BACNET_BBMD_ADDRESS - dotted IPv4 address of the BBMD or Foreign Device
|
|
Registrar.
|
|
|
|
=cut
|
|
|
|
############################################
|
|
# Steps to prepare for execution
|
|
############################################
|
|
|
|
# This is the relative path to get to the base directory cotaining the BACnet
|
|
# Stack sources from the directory containing this file and the directory
|
|
# within which InlineC code is built. The reason for delaring it here and
|
|
# setting the value in a BEGIN block is so that the variable gets its value at
|
|
# compile time before Inline::C tries to use that variable.
|
|
my $relSourcePath;
|
|
my $inlineCFile;
|
|
my $inlineBuildDir;
|
|
my $libDir;
|
|
my $incDir1;
|
|
my $incDir2;
|
|
my $incDir3;
|
|
BEGIN {
|
|
# the Perl source file is in the same directory as in the InlineC file
|
|
# this path should not contain any spaces
|
|
$relSourcePath = File::Spec->rel2abs(dirname($0));
|
|
die "Install path must not have spaces.\n" if $relSourcePath =~ /\s/;
|
|
my @dirs = ();
|
|
push @dirs, $relSourcePath;
|
|
$inlineCFile = File::Spec->catfile(@dirs, "perl_bindings.c");
|
|
|
|
# all Inline C sources shall be contained in ./.Inline
|
|
push @dirs, ".Inline";
|
|
$inlineBuildDir = File::Spec->catdir(@dirs);
|
|
pop @dirs;
|
|
|
|
# to properly link, need to reference ./../../lib
|
|
push @dirs, "..";
|
|
push @dirs, "..";
|
|
push @dirs, "lib";
|
|
$libDir = File::Spec->catdir(@dirs);
|
|
pop @dirs;
|
|
|
|
# to properly build, need to reference ./../../include
|
|
push @dirs, "include";
|
|
$incDir1 = File::Spec->catdir(@dirs);
|
|
pop @dirs;
|
|
|
|
# we will use the demo handlers, need to reference ./../../demo/object
|
|
push @dirs, "demo";
|
|
push @dirs, "object";
|
|
$incDir2 = File::Spec->catdir(@dirs);
|
|
pop @dirs;
|
|
pop @dirs;
|
|
|
|
# TODO: This should be done in a more universal way
|
|
# to properly build Win32 ports, need to refrence ./../../ports/win32
|
|
push @dirs, "ports";
|
|
push @dirs, "win32";
|
|
$incDir3 = File::Spec->catdir(@dirs);
|
|
}
|
|
|
|
use Inline (
|
|
C => Config =>
|
|
LIBS => "-L$libDir -lbacnet -liphlpapi",
|
|
INC => ["-I$incDir1", "-I$incDir2", "-I$incDir3"],
|
|
DIRECTORY => $inlineBuildDir,
|
|
);
|
|
|
|
# this is the C source file for interfacing to the library. Yes, this could be
|
|
# done natively in Perl, but this is just as easy (and probably faster to
|
|
# execute).
|
|
use Inline C => "$inlineCFile";
|
|
|
|
|
|
my $ask_help = 0;
|
|
my $script;
|
|
my $log;
|
|
my $logTo = \*STDOUT;
|
|
my $logIndent = 0;
|
|
my $logIsQuiet = 0;
|
|
my $errorMsg;
|
|
my $answer = '';
|
|
|
|
($ask_help = 1) unless GetOptions(
|
|
'help|?' => \$ask_help,
|
|
'script=s' => \$script,
|
|
'log=s' => \$log,
|
|
);
|
|
|
|
if (!defined($script) || !(-f $script))
|
|
{
|
|
print "Bad or no script file scpecified.\n";
|
|
$ask_help = 1;
|
|
}
|
|
else
|
|
{
|
|
# Add the script's location to @INC so that they can include other scripts
|
|
# using relative paths
|
|
my $scriptdir = File::Spec->rel2abs(dirname($script));
|
|
push @INC,$scriptdir;
|
|
}
|
|
|
|
if ($ask_help) {
|
|
print "============================\n\n";
|
|
pod2usage(
|
|
-exitval => 0,
|
|
-verbose => 99,
|
|
-sections => "NAME|DESCRIPTION|OPTIONS"
|
|
);
|
|
}
|
|
|
|
if (defined($log))
|
|
{
|
|
open(LOG, ">$log") || croak "Cannot open $log for writing: $!\n";
|
|
$logTo = \*LOG;
|
|
}
|
|
|
|
# Pull in the BACnet enumerations from the C header file
|
|
my %C_ENUMS;
|
|
eval {
|
|
my $pwd = File::Spec->rel2abs(File::Spec->curdir());
|
|
|
|
# let's get into the directory so that we can pull in the bacnet enumerations
|
|
my @dirs = ();
|
|
push @dirs, dirname($0);
|
|
push @dirs, "../../include";
|
|
chdir(File::Spec->catdir(@dirs));
|
|
my $c = Convert::Binary::C->new->parse_file('bacenum.h');
|
|
foreach my $typedef ($c->typedef)
|
|
{
|
|
if (ref($$typedef{type}) eq "HASH")
|
|
{
|
|
my $enumeration = \%{$C_ENUMS{$$typedef{declarator}}};
|
|
foreach my $enum_name (keys %{$$typedef{type}{enumerators}})
|
|
{
|
|
${$C_ENUMS{$$typedef{declarator}}}{$enum_name} = ${$$typedef{type}{enumerators}}{$enum_name};
|
|
}
|
|
}
|
|
}
|
|
lock_hash(%C_ENUMS);
|
|
chdir($pwd);
|
|
};
|
|
if ($EVAL_ERROR)
|
|
{
|
|
croak "Error pulling in the enumerations. $@\n";
|
|
}
|
|
|
|
# Prepare things for communication
|
|
BacnetPrepareComm();
|
|
|
|
# Execute the user specified script
|
|
Log("Executing $script - start time " . scalar(localtime(time())) );
|
|
unless (my $return = do $script)
|
|
{
|
|
croak "could not parse $script: $@" if $@;
|
|
croak "could not pull in $script: $!" unless defined $return;
|
|
croak "could not execute $script" unless $return;
|
|
}
|
|
Log("Finished executing $script - end time " . scalar(localtime(time())) );
|
|
|
|
=head1 This tool's API
|
|
|
|
In addition to having all standard Perl flow control, functions, and modules,
|
|
the this tool provides an API for performing BACnet communication functions.
|
|
|
|
=cut
|
|
|
|
##########################################
|
|
# This block is the external API
|
|
##########################################
|
|
|
|
=head2 ReadProperty
|
|
|
|
This function implements the ReadProperty service. There are no built in retry
|
|
mechanisms. NOTE: all enumerations are defined in F<bacenum.h>
|
|
|
|
=head3 Inputs to ReadProperty
|
|
|
|
=begin html
|
|
<ul>
|
|
<li><b>devideInstance</b> - the instance number of the device we are reading</li>
|
|
<li><b>objectName</b> - the enumeration for the object name we are reading</li>
|
|
<li><b>objectInstance</b> - the instance number of the object we are reading</li>
|
|
<li><b>propertyName</b> - the enumeration for the property name we are reading</li>
|
|
<li><b>index</b> - Optional (default -1): the index number we are reading from. -1 if not applicable</li>
|
|
</ul>
|
|
|
|
=end html
|
|
|
|
=head3 Outputs from ReadProperty
|
|
|
|
=begin html
|
|
<ul>
|
|
<li><b>result</b> - the sting result (value or error) for ReadProperty</li>
|
|
<li><b>isFailure</b> - zero means no failure, non-zero means failure</li>
|
|
</ul>
|
|
|
|
=end html
|
|
|
|
=head3 Example of ReadProperty
|
|
|
|
The following example will read AV0.PresentValue from device 1234
|
|
|
|
my ($res, $failed) = ReadProperty(1234, 'OBJECT_ANALOG_VALUE', 0, 'PROP_PRESENT_VALUE');
|
|
|
|
=cut
|
|
|
|
sub ReadProperty {
|
|
my $deviceInstance = shift;
|
|
my $objectName = shift;
|
|
my $objectInstance = shift;
|
|
my $propertyName = shift;
|
|
my $index = shift;
|
|
my $isFailure = BindToDevice($deviceInstance);
|
|
|
|
# Loop for early exit
|
|
while(1)
|
|
{
|
|
last if $isFailure;
|
|
|
|
my ($objectPrintName, $objectValue) = LookupEnumValue('BACNET_OBJECT_TYPE', $objectName);
|
|
my ($propertyPrintName, $propertyValue) = LookupEnumValue('BACNET_PROPERTY_ID', $propertyName);
|
|
|
|
my $msg = "ReadProperty $objectPrintName" . '[' . $objectInstance . "].$propertyPrintName";
|
|
if (defined($index))
|
|
{
|
|
$msg .= ".$index";
|
|
} else {
|
|
$index = -1;
|
|
}
|
|
$msg .= " from Device" . '[' . $deviceInstance . "] ==> ";
|
|
|
|
LogAnswer('', 0);
|
|
if ( BacnetReadProperty($deviceInstance, $objectValue, $objectInstance, $propertyValue, $index) )
|
|
{
|
|
BacnetGetError($errorMsg);
|
|
$msg .= "Problem: $errorMsg";
|
|
$isFailure = 1;
|
|
}
|
|
else
|
|
{
|
|
$msg .= $answer;
|
|
$isFailure = 0;
|
|
}
|
|
Log($msg);
|
|
last;
|
|
}
|
|
|
|
return ($answer, $isFailure);
|
|
}
|
|
|
|
=head2 ReadPropertyMultiple
|
|
|
|
This function implements the ReadPropertyMultiple service. There are no built in retry
|
|
mechanisms. NOTE: all enumerations are defined in F<bacenum.h>
|
|
|
|
=head3 Inputs to ReadPropertyMultiple
|
|
|
|
=begin html
|
|
<ul>
|
|
<li><b>devideInstance</b> - the instance number of the device we are reading</li>
|
|
<li><b>r_answerList</b> - reference to a list where to store the answers</li>
|
|
<li><b>list</b> - a list of ReadAccessSpecifications</li>
|
|
<ul>
|
|
<li><b>objectType</b> - the enumeration for the object name to read from</li>
|
|
<li><b>objectInstance</b> - the instance number of the object we are reading</li>
|
|
<li><b>propertyName</b> - the enumeration for the property name we are reading</li>
|
|
<li><b>index</b> - the index number we are reading from. Use -1 if not applicable</li>
|
|
</ul>
|
|
</ul>
|
|
|
|
=end html
|
|
|
|
=head3 Outputs from ReadPropertyMultiple
|
|
|
|
=begin html
|
|
<ul>
|
|
<li><b>result</b> - the 'QQQ' delimited concatenated sting result (value or error) for ReadPropertyMultiple. The parsed out result is returned in r_answerList</li>
|
|
<li><b>isFailure</b> - zero means no failure, non-zero means failure</li>
|
|
</ul>
|
|
|
|
=end html
|
|
|
|
=head3 Example of ReadPropertyMultiple
|
|
|
|
The following example will read AV0.PresentValue and AV1.PresentValue from device 1234
|
|
|
|
my @RPM_request = ();
|
|
my @RPM_answer = ();
|
|
my $failed;
|
|
push @RPM_request, ['OBJECT_ANALOG_VALUE', 0, 'PROP_PRESENT_VALUE', -1];
|
|
push @RPM_request, ['OBJECT_ANALOG_VALUE', 1, 'PROP_PRESENT_VALUE', -1];
|
|
(undef, $failed) = ReadPropertyMultiple(1234, \@RPM_answer, @RPM_request);
|
|
|
|
=cut
|
|
|
|
sub ReadPropertyMultiple
|
|
{
|
|
my $deviceInstanceNumber = shift;
|
|
my $r_answerList = shift;
|
|
my @list = @ARG;
|
|
my @modifiedList = ();
|
|
my $msg = '';
|
|
my $isFailure = BindToDevice($deviceInstanceNumber);
|
|
|
|
# loop for early exit
|
|
while(1)
|
|
{
|
|
last if $isFailure;
|
|
|
|
Log("ReadPropertyMultiple:");
|
|
$logIndent += 4;
|
|
|
|
foreach my $r_prop (@list)
|
|
{
|
|
my @tmpList = ();
|
|
push @tmpList, $$r_prop[$_] for (0 .. 3);
|
|
(undef, $tmpList[0]) = LookupEnumValue('BACNET_OBJECT_TYPE', $$r_prop[0]);
|
|
(undef, $tmpList[2]) = LookupEnumValue('BACNET_PROPERTY_ID', $$r_prop[2]);
|
|
push @modifiedList, \@tmpList;
|
|
}
|
|
|
|
LogAnswer('', 0);
|
|
@{$r_answerList} = ();
|
|
if (BacnetReadPropertyMultiple($deviceInstanceNumber, @modifiedList))
|
|
{
|
|
BacnetGetError($errorMsg);
|
|
Log("Problem: $errorMsg");
|
|
$isFailure = 1;
|
|
}
|
|
else
|
|
{
|
|
my $i = 0;
|
|
foreach (split('QQQ', $answer))
|
|
{
|
|
my ($objectPrintName, undef) = LookupEnumValue('BACNET_OBJECT_TYPE', $list[$i][0]);
|
|
my ($propertyPrintName, undef) = LookupEnumValue('BACNET_PROPERTY_ID', $list[$i][2]);
|
|
my $msg = $objectPrintName . '.[' . $list[$i][1] . '].' . $propertyPrintName;
|
|
if ($list[$i][3] != -1)
|
|
{
|
|
$msg .= '.[' . $list[$i][3] . ']';
|
|
}
|
|
$msg .= " ==> $_";
|
|
Log($msg);
|
|
push @{$r_answerList}, $_;
|
|
$i++;
|
|
}
|
|
$isFailure = 0;
|
|
}
|
|
|
|
$logIndent -= 4;
|
|
last;
|
|
}
|
|
|
|
return ($answer, $isFailure);
|
|
}
|
|
|
|
=head2 WriteProperty
|
|
|
|
This function implements the WriteProperty service. There are no built in retry
|
|
mechanisms. NOTE: all enumerations are defined in F<bacenum.h>
|
|
|
|
=head3 Inputs to WriteProperty
|
|
|
|
=begin html
|
|
<ul>
|
|
<li><b>devideInstance</b> - the instance number of the device we are writing</li>
|
|
<li><b>objectName</b> - the enumeration for the object name we are writing</li>
|
|
<li><b>objectInstance</b> - the instance number of the object we are writing</li>
|
|
<li><b>propertyName</b> - the enumeration for the property name we are writing</li>
|
|
<li><b>tagName</b> - the enumeration for the type of value we are writing. To specify context tags, prepend the tag name with "Cn:" where 'n' is the context number.</li>
|
|
<li><b>value</b> - the value we are writing</li>
|
|
<li><b>priority</b> - Optional (default 0): the priority within Priority Array to write at. Use 1-16 when specify priority, 0 to not specify priority.</li>
|
|
<li><b>index</b> - Optional (default -1): the index within an array we are writing to. Use positive number to indicate index, -1 to not specify index.</li>
|
|
</ul>
|
|
|
|
=end html
|
|
|
|
=head3 Outputs from WriteProperty
|
|
|
|
=begin html
|
|
<ul>
|
|
<li><b>result</b> - the sting result (value or error) for WriteProperty</li>
|
|
<li><b>isFailure</b> - zero means no failure, non-zero means failure</li>
|
|
</ul>
|
|
|
|
=end html
|
|
|
|
=head3 Example of WriteProperty
|
|
|
|
The following example will write 1.0 to AV0.PresentValue in device 1234
|
|
|
|
my ($res, $failed) = WriteProperty(1234, 'OBJECT_ANALOG_VALUE', 0, 'PROP_PRESENT_VALUE', 'BACNET_APPLICATION_TAG_REAL', 1.0);
|
|
|
|
=cut
|
|
|
|
sub WriteProperty {
|
|
my $deviceInstance = shift;
|
|
my $objectName = shift;
|
|
my $objectInstance = shift;
|
|
my $propertyName = shift;
|
|
my $tagName = shift;
|
|
my $value = shift;
|
|
my $priority = shift;
|
|
my $index = shift;
|
|
my $isFailure = BindToDevice($deviceInstance);
|
|
|
|
# loop for early exit
|
|
while(1)
|
|
{
|
|
last if $isFailure;
|
|
|
|
my ($objectPrintName, $objectValue) = LookupEnumValue('BACNET_OBJECT_TYPE', $objectName);
|
|
my ($propertyPrintName, $propertyValue) = LookupEnumValue('BACNET_PROPERTY_ID', $propertyName);
|
|
|
|
my $tagValue = '';
|
|
if ($tagName =~ /^(C\d+):(.*)$/)
|
|
{
|
|
$tagName = $2;
|
|
$tagValue = "$1 ";
|
|
}
|
|
my ($tagPrintName, $tagNewValue) = LookupEnumValue('BACNET_APPLICATION_TAG', $tagName);
|
|
$tagValue .= $tagNewValue;
|
|
|
|
my $msg = "WriteProperty $tagPrintName:$value to $objectPrintName" . '[' . $objectInstance . "].$propertyPrintName";
|
|
if (defined($index))
|
|
{
|
|
$msg .= '[' . $index . ']';
|
|
}
|
|
else
|
|
{
|
|
# an index of -1 means that we are not writing to an array
|
|
$index = -1;
|
|
}
|
|
if (defined($priority))
|
|
{
|
|
$msg .= '@' . $priority
|
|
}
|
|
else
|
|
{
|
|
# a priority of 0 means we are not writing to a priority array
|
|
$priority = 0;
|
|
}
|
|
$msg .= " in Device" . '[' . $deviceInstance . "] ==> ";
|
|
|
|
LogAnswer('', 0);
|
|
if ( BacnetWriteProperty($deviceInstance, $objectValue, $objectInstance, $propertyValue, $priority, $index, $tagValue, $value) )
|
|
{
|
|
BacnetGetError($errorMsg);
|
|
$msg .= "Problem: $errorMsg\n";
|
|
$isFailure = 1;
|
|
}
|
|
else
|
|
{
|
|
$msg .= $answer;
|
|
$isFailure = 0;
|
|
}
|
|
Log($msg);
|
|
last;
|
|
}
|
|
|
|
return ($answer, $isFailure);
|
|
}
|
|
|
|
=head2 TimeSync
|
|
|
|
This function implements the TimeSync and UTCTimeSync services
|
|
|
|
=head3 Inputs to TimeSync
|
|
|
|
=begin html
|
|
<ul>
|
|
<li><b>deviceInstanceNumber</b> - the instance number of the device we are reading</li>
|
|
<li><b>year</b> - Year (i.e. 2011)</li>
|
|
<li><b>month</b> - Month (i.e. 11 for November)</li>
|
|
<li><b>day</b> - Day (i.e. 1 for first of month)</li>
|
|
<li><b>hour</b> - Hour (i.e. 23 for 11pm)</li>
|
|
<li><b>minute</b> - Minute (i.e. 0-59)</li>
|
|
<li><b>second</b> - Second (i,e. 0-59)</li>
|
|
<li><b>utcOffset</b> - Optional: if specified defines the UTC offset and forces UTCTimeSync</li>
|
|
</ul>
|
|
|
|
=end html
|
|
|
|
=head3 Outputs from TimeSync
|
|
|
|
=begin html
|
|
<ul>
|
|
<li><b>isFailure</b> - zero means no failure, non-zero means failure</li>
|
|
</ul>
|
|
|
|
=end html
|
|
|
|
=head3 Example of TimeSync
|
|
|
|
$isFailure = TimeSync($deviceInstance, $1, $2, $3, $4, $5, $6) unless $isFailure;
|
|
|
|
=cut
|
|
|
|
sub TimeSync
|
|
{
|
|
my $deviceInstanceNumber = shift;
|
|
my $year = shift;
|
|
my $month = shift;
|
|
my $day = shift;
|
|
my $hour = shift;
|
|
my $minute = shift;
|
|
my $second = shift;
|
|
my $utcOffset = shift;
|
|
my $isUTC;
|
|
|
|
my $isFailure = BindToDevice($deviceInstanceNumber);
|
|
|
|
# loop for early exit
|
|
while(1)
|
|
{
|
|
last if $isFailure;
|
|
|
|
# be a pessimist. Assume things will fail
|
|
$isFailure = 1;
|
|
|
|
if (defined($utcOffset))
|
|
{
|
|
$isUTC = 1;
|
|
Log("UTC Time Sync not yet supported.");
|
|
last;
|
|
}
|
|
else
|
|
{
|
|
$utcOffset = 0;
|
|
$isUTC = 0;
|
|
}
|
|
|
|
if ($year < 1900 || $year > 2099)
|
|
{
|
|
Log("Year '$year' is invalid.");
|
|
last;
|
|
}
|
|
|
|
if ($month <= 0 || $month > 12)
|
|
{
|
|
Log("Month '$month' is invalid.");
|
|
last;
|
|
}
|
|
|
|
if ($day <= 0 || $day > 31)
|
|
{
|
|
Log("Day '$day' is invalid.");
|
|
last;
|
|
}
|
|
|
|
if ($hour < 0 || $hour > 23)
|
|
{
|
|
Log("Hour '$hour' is invalid.");
|
|
last;
|
|
}
|
|
|
|
if ($minute < 0 || $minute > 59)
|
|
{
|
|
Log("Minute '$minute' is invalid.");
|
|
last;
|
|
}
|
|
|
|
if ($second < 0 || $second > 59)
|
|
{
|
|
Log("Second '$second' is invalid.");
|
|
last;
|
|
}
|
|
|
|
Log("TimeSync: Device[$deviceInstanceNumber] $year/$month/$day $hour:$minute:$second");
|
|
|
|
$isFailure = BacnetTimeSync($deviceInstanceNumber, $year, $month, $day, $hour, $minute, $second, $isUTC, $utcOffset);
|
|
last;
|
|
}
|
|
|
|
return $isFailure;
|
|
}
|
|
|
|
=head2 Log
|
|
|
|
This function prints out to the desired method of logging (STDOUT or file).
|
|
NewLine characters are not required when making calls to this function. If any
|
|
NewLine characters are specified, they will be stripped out. To print an empty
|
|
line, pass in a space as the message. NOTE: This function will honor previous
|
|
requests to silence the log (see SilcenseLog for details)
|
|
|
|
=head3 Inputs to Log
|
|
|
|
=begin html
|
|
<ul>
|
|
<li><b>msg</b> - the message to output
|
|
</ul>
|
|
|
|
=end html
|
|
|
|
=head3 Example of Log
|
|
|
|
The following example will print out "hello world"
|
|
|
|
Log("Hello World");
|
|
|
|
=cut
|
|
|
|
###############################################################################
|
|
# Global Variables affecting this function
|
|
# logIsQuiet do not print anytihng if the log was qieted
|
|
# logIndent how many spaces to put in front of every logged line
|
|
###############################################################################
|
|
sub Log {
|
|
my $msg = shift;
|
|
|
|
if (defined($msg) && !$logIsQuiet)
|
|
{
|
|
my @last = split('', substr($msg, -2));
|
|
|
|
# if there is nothing to print, then don't do it
|
|
return if (scalar(@last) == 0);
|
|
|
|
# if there are newline-like characters, get rid of them.
|
|
while ($msg =~/^(.*)[\r\n]+(.*)$/)
|
|
{
|
|
$msg = $1 . $2;
|
|
}
|
|
|
|
local $OUTPUT_RECORD_SEPARATOR = "\n";
|
|
print $logTo ' ' x $logIndent . $msg;
|
|
}
|
|
}
|
|
|
|
=head2 SilenceLog
|
|
|
|
This function requests that all future log messages be either suppressed or
|
|
enabled.
|
|
|
|
=head3 Inputs to SilenceLog
|
|
|
|
=begin html
|
|
<ul>
|
|
<li><b>logIsQuiet</b> - zero means print to log, non-zero means supress log
|
|
</ul>
|
|
|
|
=end html
|
|
|
|
=head3 Outputs from SilenceLog
|
|
|
|
The previous value of whether or not the log was silenced before caling this
|
|
function.
|
|
|
|
=head3 Example of SilenceLog
|
|
|
|
The following example will print out "hello", but not "world"
|
|
|
|
Log("Hello");
|
|
SilenceLog(1);
|
|
Log("World");
|
|
|
|
=cut
|
|
|
|
sub SilenceLog {
|
|
my $prevValue = $logIsQuiet;
|
|
$logIsQuiet = shift;
|
|
return $prevValue;
|
|
}
|
|
|
|
=head2 Retry
|
|
|
|
This function will try to execute the requested command up to specified number
|
|
of times, awaiting the requested answer, with a specified pause between
|
|
retries. NOTE: the only functions which can be executed by this function are
|
|
ones which return two parameres in the form of ($response, $isFailure)
|
|
|
|
=head3 Inputs to Retry
|
|
|
|
=begin html
|
|
<ul>
|
|
<li><b>r_func</b> - The reference to the function which is to be retried</li>
|
|
<li><b>r_funcArgs</b> - A reference to an array of arguments for the function to be executed</li>
|
|
<li><b>desiredOutput</b> - The condition which will terminate the retrying. Can be either a number or a regexp to patch against the $response return of the function</li>
|
|
<li><b>maxTries</b> - The maximum number of retry attempts before calling it quits</li>
|
|
<li><b>sleepSeconds</b> - The number of seconds (could be fractional) to wait between retries</li>
|
|
</ul>
|
|
|
|
=end html
|
|
|
|
=head3 Outputs from Retry
|
|
|
|
=begin html
|
|
<ul>
|
|
<li><b>$resp</b> - The response from the last execution of requested function</li>
|
|
<li><b>isFailure</b> - zero means no failure, non-zero means failure</li>
|
|
</ul>
|
|
|
|
=end html
|
|
|
|
=head3 Example of Retry
|
|
|
|
The following example will execute the ReadProperty function to read a property
|
|
from an object (see ReadProperty for details on those arguments) with up to
|
|
$maxRetries retries (with $retryDelay delay between retries) or unitl the
|
|
desired answer of 42 is received.
|
|
|
|
my ($resp, $isFailure) = Retry(
|
|
\&ReadProperty, [$deviceInstance, 'OBJECT_ANALOG_VALUE', 0, 'PROP_PRESENT_VALUE'],
|
|
42, $maxRetries, $retryDelay
|
|
);
|
|
if ($isFailure)
|
|
{
|
|
die "Value was not 42. Last response was '$resp'";
|
|
}
|
|
|
|
The following example will try to execute a WriteProperty (see that function for
|
|
details on its arguments) until the write succeeds.
|
|
|
|
my ($resp, $isFailure) = Retry(
|
|
\&WriteProperty, [$deviceInstance, 'OBJECT_ANALOG_VALUE', 0, 'PROP_PRESENT_VALUE', 'BACNET_APPLICATION_TAG_REAL', 42.0],
|
|
"Acknowledged", $maxRetries, $retryDelay
|
|
);
|
|
if ($isFailure)
|
|
{
|
|
die "Could not write 42. Last response was '$resp'";
|
|
}
|
|
|
|
=cut
|
|
sub Retry {
|
|
my $r_func = shift;
|
|
my $r_funcArgs = shift;
|
|
my $desiredOutput = shift;
|
|
my $maxTries = shift;
|
|
my $sleepSeconds = shift;
|
|
|
|
my ($resp, $failed);
|
|
|
|
my $i;
|
|
for ($i=0; $i<$maxTries; $i++)
|
|
{
|
|
($resp, $failed) = &{$r_func}(@{$r_funcArgs});
|
|
unless ($failed)
|
|
{
|
|
if (looks_like_number($desiredOutput))
|
|
{
|
|
last if (looks_like_number($resp) && ($resp == $desiredOutput));
|
|
}
|
|
else
|
|
{
|
|
last if ($resp =~ /$desiredOutput/);
|
|
}
|
|
}
|
|
select(undef, undef, undef, $sleepSeconds);
|
|
}
|
|
|
|
return ($resp, ($i == $maxTries));
|
|
}
|
|
|
|
|
|
##########################################
|
|
# These are the supporting functions
|
|
##########################################
|
|
|
|
sub LookupEnumValue {
|
|
my $enumType = shift;
|
|
my $enumName = shift;
|
|
my $printName;
|
|
|
|
if (!exists($C_ENUMS{$enumType}{$enumName}))
|
|
{
|
|
print "Requested enumeration '$enumName' does not exist within '$enumType'.\n";
|
|
exit -1;
|
|
}
|
|
|
|
# lookup the value
|
|
my $value = $C_ENUMS{$enumType}{$enumName};
|
|
|
|
# reformat the OBJECT name style
|
|
my %reformat = (
|
|
'BACNET_PROPERTY_ID' => 'PROP',
|
|
'BACNET_OBJECT_TYPE' => 'OBJECT',
|
|
'BACNET_APPLICATION_TAG' => 'BACNET_APPLICATION_TAG',
|
|
);
|
|
|
|
if (exists($reformat{$enumType}))
|
|
{
|
|
if ($enumName =~ /$reformat{$enumType}_(.*)/)
|
|
{
|
|
$printName = '';
|
|
$printName .= ucfirst lc $_ foreach (split('_', $1));
|
|
}
|
|
}
|
|
|
|
return ($printName, $value);
|
|
}
|
|
|
|
sub BindToDevice {
|
|
my $deviceInstance = shift;
|
|
my $isFailure = 0;
|
|
|
|
if ( BacnetBindToDevice($deviceInstance) )
|
|
{
|
|
BacnetGetError($errorMsg);
|
|
Log("Problem binding to deivce $deviceInstance: $errorMsg\n");
|
|
$isFailure = 1;
|
|
}
|
|
|
|
return $isFailure;
|
|
}
|
|
|
|
sub LogAnswer {
|
|
my $newAnswer = shift;
|
|
my $append = shift;
|
|
|
|
$answer = '' unless $append;
|
|
$answer .= $newAnswer;
|
|
}
|