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
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.
In addition to having all standard Perl flow control, functions, and modules,
+the this tool provides an API for performing BACnet communication functions.
devideInstance - the instance number of the device we are writing
+
objectName - the enumeration for the object name we are writing
+
objectInstance - the instance number of the object we are writing
+
propertyName - the enumeration for the property name we are writing
+
tagName - 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.
+
value - the value we are writing
+
priority - Optional (default 0): the priority within Priority Array to write at. Use 1-16 when specify priority, 0 to not specify priority.
+
index - Optional (default -1): the index within an array we are writing to. Use positive number to indicate index, -1 to not specify index.
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)
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)
r_func - The reference to the function which is to be retried
+
r_funcArgs - A reference to an array of arguments for the function to be executed
+
desiredOutput - The condition which will terminate the retrying. Can be either a number or a regexp to patch against the $response return of the function
+
maxTries - The maximum number of retry attempts before calling it quits
+
sleepSeconds - The number of seconds (could be fractional) to wait between retries
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'";
+ }
a",d=a.getElementsByTagName("*"),e=a.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=a.getElementsByTagName("input")[0],k={leadingWhitespace:a.firstChild.nodeType===3,tbody:!a.getElementsByTagName("tbody").length,htmlSerialize:!!a.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,unknownElems:!!a.getElementsByTagName("nav").length,checkOn:i.value==="on",optSelected:h.selected,getSetAttribute:a.className!=="t",enctype:!!c.createElement("form").enctype,submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0},i.checked=!0,k.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,k.optDisabled=!h.disabled;try{delete a.test}catch(v){k.deleteExpando=!1}!a.addEventListener&&a.attachEvent&&a.fireEvent&&(a.attachEvent("onclick",function(){k.noCloneEvent=!1}),a.cloneNode(!0).fireEvent("onclick")),i=c.createElement("input"),i.value="t",i.setAttribute("type","radio"),k.radioValue=i.value==="t",i.setAttribute("checked","checked"),a.appendChild(i),l=c.createDocumentFragment(),l.appendChild(a.lastChild),k.checkClone=l.cloneNode(!0).cloneNode(!0).lastChild.checked,a.innerHTML="",a.style.width=a.style.paddingLeft="1px",m=c.getElementsByTagName("body")[0],o=c.createElement(m?"div":"body"),p={visibility:"hidden",width:0,height:0,border:0,margin:0,background:"none"},m&&f.extend(p,{position:"absolute",left:"-999px",top:"-999px"});for(t in p)o.style[t]=p[t];o.appendChild(a),n=m||b,n.insertBefore(o,n.firstChild),k.appendChecked=i.checked,k.boxModel=a.offsetWidth===2,"zoom"in a.style&&(a.style.display="inline",a.style.zoom=1,k.inlineBlockNeedsLayout=a.offsetWidth===2,a.style.display="",a.innerHTML="",k.shrinkWrapBlocks=a.offsetWidth!==2),a.innerHTML="
t
",q=a.getElementsByTagName("td"),u=q[0].offsetHeight===0,q[0].style.display="",q[1].style.display="none",k.reliableHiddenOffsets=u&&q[0].offsetHeight===0,a.innerHTML="",c.defaultView&&c.defaultView.getComputedStyle&&(j=c.createElement("div"),j.style.width="0",j.style.marginRight="0",a.appendChild(j),k.reliableMarginRight=(parseInt((c.defaultView.getComputedStyle(j,null)||{marginRight:0}).marginRight,10)||0)===0);if(a.attachEvent)for(t in{submit:1,change:1,focusin:1})s="on"+t,u=s in a,u||(a.setAttribute(s,"return;"),u=typeof a[s]=="function"),k[t+"Bubbles"]=u;f(function(){var a,b,d,e,g,h,i=1,j="position:absolute;top:0;left:0;width:1px;height:1px;margin:0;",l="visibility:hidden;border:0;",n="style='"+j+"border:5px solid #000;padding:0;'",p="
"&&!p?o.childNodes:[];for(i=q.length-1;i>=0;--i)f.nodeName(q[i],"tbody")&&!q[i].childNodes.length&&q[i].parentNode.removeChild(q[i])}!f.support.leadingWhitespace&&$.test(k)&&o.insertBefore(b.createTextNode($.exec(k)[0]),o.firstChild),k=o.childNodes}var r;if(!f.support.appendChecked)if(k[0]&&typeof (r=k.length)=="number")for(i=0;i=0)return b+"px"}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return bt.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=f.isNumeric(b)?"alpha(opacity="+b*100+")":"",g=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&f.trim(g.replace(bs,""))===""){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bs.test(g)?g.replace(bs,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){var c;f.swap(a,{display:"inline-block"},function(){b?c=bB(a,"margin-right","marginRight"):c=a.style.marginRight});return c}})}),c.defaultView&&c.defaultView.getComputedStyle&&(bC=function(a,c){var d,e,g;c=c.replace(bu,"-$1").toLowerCase();if(!(e=a.ownerDocument.defaultView))return b;if(g=e.getComputedStyle(a,null))d=g.getPropertyValue(c),d===""&&!f.contains(a.ownerDocument.documentElement,a)&&(d=f.style(a,c));return d}),c.documentElement.currentStyle&&(bD=function(a,b){var c,d,e,f=a.currentStyle&&a.currentStyle[b],g=a.style;f===null&&g&&(e=g[b])&&(f=e),!bv.test(f)&&bw.test(f)&&(c=g.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),g.left=b==="fontSize"?"1em":f||0,f=g.pixelLeft+"px",g.left=c,d&&(a.runtimeStyle.left=d));return f===""?"auto":f}),bB=bC||bD,f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style&&a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)});var bF=/%20/g,bG=/\[\]$/,bH=/\r?\n/g,bI=/#.*$/,bJ=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bK=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bL=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,bM=/^(?:GET|HEAD)$/,bN=/^\/\//,bO=/\?/,bP=/
+
+
+=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
+
+=head3 Inputs to ReadProperty
+
+=begin html
+
+
devideInstance - the instance number of the device we are reading
+
objectName - the enumeration for the object name we are reading
+
objectInstance - the instance number of the object we are reading
+
propertyName - the enumeration for the property name we are reading
+
index - Optional (default -1): the index number we are reading from. -1 if not applicable
+
+
+=end html
+
+=head3 Outputs from ReadProperty
+
+=begin html
+
+
result - the sting result (value or error) for ReadProperty
+
isFailure - zero means no failure, non-zero means failure
+
+
+=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
+
+=head3 Inputs to ReadPropertyMultiple
+
+=begin html
+
+
devideInstance - the instance number of the device we are reading
+
r_answerList - reference to a list where to store the answers
+
list - a list of ReadAccessSpecifications
+
+
objectType - the enumeration for the object name to read from
+
objectInstance - the instance number of the object we are reading
+
propertyName - the enumeration for the property name we are reading
+
index - the index number we are reading from. Use -1 if not applicable
+
+
+
+=end html
+
+=head3 Outputs from ReadPropertyMultiple
+
+=begin html
+
+
result - the 'QQQ' delimited concatenated sting result (value or error) for ReadPropertyMultiple. The parsed out result is returned in r_answerList
+
isFailure - zero means no failure, non-zero means failure
+
+
+=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
+
+=head3 Inputs to WriteProperty
+
+=begin html
+
+
devideInstance - the instance number of the device we are writing
+
objectName - the enumeration for the object name we are writing
+
objectInstance - the instance number of the object we are writing
+
propertyName - the enumeration for the property name we are writing
+
tagName - 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.
+
value - the value we are writing
+
priority - Optional (default 0): the priority within Priority Array to write at. Use 1-16 when specify priority, 0 to not specify priority.
+
index - Optional (default -1): the index within an array we are writing to. Use positive number to indicate index, -1 to not specify index.
+
+
+=end html
+
+=head3 Outputs from WriteProperty
+
+=begin html
+
+
result - the sting result (value or error) for WriteProperty
+
isFailure - zero means no failure, non-zero means failure
+
+
+=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
+
+
deviceInstanceNumber - the instance number of the device we are reading
+
year - Year (i.e. 2011)
+
month - Month (i.e. 11 for November)
+
day - Day (i.e. 1 for first of month)
+
hour - Hour (i.e. 23 for 11pm)
+
minute - Minute (i.e. 0-59)
+
second - Second (i,e. 0-59)
+
utcOffset - Optional: if specified defines the UTC offset and forces UTCTimeSync
+
+
+=end html
+
+=head3 Outputs from TimeSync
+
+=begin html
+
+
isFailure - zero means no failure, non-zero means failure
+
+
+=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
+
+
msg - the message to output
+
+
+=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
+
+
logIsQuiet - zero means print to log, non-zero means supress log
+
+
+=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
+
+
r_func - The reference to the function which is to be retried
+
r_funcArgs - A reference to an array of arguments for the function to be executed
+
desiredOutput - The condition which will terminate the retrying. Can be either a number or a regexp to patch against the $response return of the function
+
maxTries - The maximum number of retry attempts before calling it quits
+
sleepSeconds - The number of seconds (could be fractional) to wait between retries
+
+
+=end html
+
+=head3 Outputs from Retry
+
+=begin html
+
+
$resp - The response from the last execution of requested function
+
isFailure - zero means no failure, non-zero means failure
+
+
+=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;
+}
+
diff --git a/bacnet-stack/demo/perl/example_readprop.pl b/bacnet-stack/demo/perl/example_readprop.pl
new file mode 100644
index 00000000..dd36c430
--- /dev/null
+++ b/bacnet-stack/demo/perl/example_readprop.pl
@@ -0,0 +1,16 @@
+use warnings;
+use strict;
+
+if (scalar(@ARGV) == 1)
+{
+ my $device = $ARGV[0];
+ my ($resp, $failed) = ReadProperty($device, 'OBJECT_ANALOG_VALUE', 0, 'PROP_PRESENT_VALUE');
+
+ print "status was '$failed' and the response was '$resp'\n";
+}
+else
+{
+ print "Must specify device instance number as an argument to this script\n";
+}
+
+1;
diff --git a/bacnet-stack/demo/perl/perl_bindings.c b/bacnet-stack/demo/perl/perl_bindings.c
new file mode 100644
index 00000000..62b23a36
--- /dev/null
+++ b/bacnet-stack/demo/perl/perl_bindings.c
@@ -0,0 +1,1126 @@
+#include "bacdef.h"
+#include "handlers.h"
+#include "bacenum.h"
+#include "datalink.h"
+#include "device.h"
+#include
+#include "arf.h"
+
+// Free is redefined as a macro, but Perl does not like that.
+#undef free
+
+/* global variables used in this file */
+static uint32_t Target_Device_Object_Instance = 4194303;
+static unsigned Target_Max_APDU = 0;
+static bool Error_Detected = false;
+static BACNET_ADDRESS Target_Address;
+static uint8_t Request_Invoke_ID = 0;
+static bool isReadPropertyHandlerRegistered = false;
+static bool isReadPropertyMultipleHandlerRegistered = false;
+static bool isWritePropertyHandlerRegistered = false;
+static bool isAtomicWriteFileHandlerRegistered = false;
+static bool isAtomicReadFileHandlerRegistered = false;
+
+/****************************************/
+// Logging Support
+/****************************************/
+#define MAX_ERROR_STRING 128
+#define NO_ERROR "No Error"
+static char Last_Error[MAX_ERROR_STRING] = NO_ERROR;
+static void LogError(const char *msg)
+{
+ strcpy(Last_Error, msg);
+ Error_Detected = true;
+}
+void BacnetGetError(SV *errorMsg)
+{
+ sv_setpv(errorMsg, Last_Error);
+ strcpy(Last_Error, NO_ERROR);
+ Error_Detected = false;
+}
+static void __LogAnswer(const char *msg, unsigned append)
+{
+ dSP;
+ ENTER;
+ SAVETMPS;
+ PUSHMARK(SP);
+ XPUSHs(sv_2mortal(newSVpv(msg, 0)));
+ XPUSHs(sv_2mortal(newSViv(append)));
+ PUTBACK;
+ call_pv("LogAnswer", G_DISCARD);
+ FREETMPS;
+ LEAVE;
+}
+
+/****************************************/
+// TODO: This should really be fixed in the library
+
+/* used to load the app data struct with the proper data
+ converted from a command line argument */
+bool my_bacapp_parse_application_data(
+ BACNET_APPLICATION_TAG tag_number,
+ const char *argv,
+ BACNET_APPLICATION_DATA_VALUE * value)
+{
+ int hour, min, sec, hundredths;
+ int year, month, day, wday;
+ int object_type = 0;
+ uint32_t instance = 0;
+ bool status = false;
+ long long_value = 0;
+ unsigned long unsigned_long_value = 0;
+ double double_value = 0.0;
+ int count = 0;
+
+ if (value && (tag_number < MAX_BACNET_APPLICATION_TAG)) {
+ status = true;
+ value->tag = tag_number;
+ switch (tag_number) {
+ case BACNET_APPLICATION_TAG_BOOLEAN:
+ long_value = strtol(argv, NULL, 0);
+ if (long_value)
+ value->type.Boolean = true;
+ else
+ value->type.Boolean = false;
+ break;
+ case BACNET_APPLICATION_TAG_UNSIGNED_INT:
+ unsigned_long_value = strtoul(argv, NULL, 0);
+ value->type.Unsigned_Int = unsigned_long_value;
+ break;
+ case BACNET_APPLICATION_TAG_SIGNED_INT:
+ long_value = strtol(argv, NULL, 0);
+ value->type.Signed_Int = long_value;
+ break;
+ case BACNET_APPLICATION_TAG_REAL:
+ double_value = strtod(argv, NULL);
+ value->type.Real = (float) double_value;
+ break;
+#if defined (BACAPP_DOUBLE)
+ case BACNET_APPLICATION_TAG_DOUBLE:
+ double_value = strtod(argv, NULL);
+ value->type.Double = double_value;
+ break;
+#endif
+ case BACNET_APPLICATION_TAG_OCTET_STRING:
+ status =
+ octetstring_init(&value->type.Octet_String,
+ (uint8_t *) argv, strlen(argv));
+ break;
+ case BACNET_APPLICATION_TAG_CHARACTER_STRING:
+ status =
+ characterstring_init_ansi(&value->type.Character_String,
+ (char *) argv);
+ break;
+ case BACNET_APPLICATION_TAG_BIT_STRING:
+ /* FIXME: how to parse a bit string? */
+ status = false;
+ bitstring_init(&value->type.Bit_String);
+ break;
+ case BACNET_APPLICATION_TAG_ENUMERATED:
+ unsigned_long_value = strtoul(argv, NULL, 0);
+ value->type.Enumerated = unsigned_long_value;
+ break;
+ case BACNET_APPLICATION_TAG_DATE:
+ count =
+ sscanf(argv, "%d/%d/%d:%d", &year, &month, &day, &wday);
+ if (count == 3) {
+ datetime_set_date(&value->type.Date, (uint16_t) year,
+ (uint8_t) month, (uint8_t) day);
+ } else if (count == 4) {
+ value->type.Date.year = (uint16_t) year;
+ value->type.Date.month = (uint8_t) month;
+ value->type.Date.day = (uint8_t) day;
+ value->type.Date.wday = (uint8_t) wday;
+ } else {
+ status = false;
+ }
+ break;
+ case BACNET_APPLICATION_TAG_TIME:
+ count =
+ sscanf(argv, "%d:%d:%d.%d", &hour, &min, &sec,
+ &hundredths);
+ if (count == 4) {
+ value->type.Time.hour = (uint8_t) hour;
+ value->type.Time.min = (uint8_t) min;
+ value->type.Time.sec = (uint8_t) sec;
+ value->type.Time.hundredths = (uint8_t) hundredths;
+ } else if (count == 3) {
+ value->type.Time.hour = (uint8_t) hour;
+ value->type.Time.min = (uint8_t) min;
+ value->type.Time.sec = (uint8_t) sec;
+ value->type.Time.hundredths = 0;
+ } else if (count == 2) {
+ value->type.Time.hour = (uint8_t) hour;
+ value->type.Time.min = (uint8_t) min;
+ value->type.Time.sec = 0;
+ value->type.Time.hundredths = 0;
+ } else {
+ status = false;
+ }
+ break;
+ case BACNET_APPLICATION_TAG_OBJECT_ID:
+ count = sscanf(argv, "%d:%d", &object_type, &instance);
+ if (count == 2) {
+ value->type.Object_Id.type = (uint16_t) object_type;
+ value->type.Object_Id.instance = instance;
+ } else {
+ status = false;
+ }
+ break;
+ default:
+ break;
+ }
+ value->next = NULL;
+ }
+
+ return status;
+}
+
+// end of TODO
+/****************************************/
+
+/**************************************/
+// error handlers
+/*************************************/
+static void MyAbortHandler(
+ BACNET_ADDRESS * src,
+ uint8_t invoke_id,
+ uint8_t abort_reason,
+ bool server)
+{
+ (void) server;
+ if (address_match(&Target_Address, src) &&
+ (invoke_id == Request_Invoke_ID))
+ {
+ char msg[MAX_ERROR_STRING];
+ sprintf(msg, "BACnet Abort: %s", bactext_abort_reason_name((int) abort_reason));
+ LogError(msg);
+ }
+}
+
+static void MyRejectHandler(
+ BACNET_ADDRESS * src,
+ uint8_t invoke_id,
+ uint8_t reject_reason)
+{
+ if (address_match(&Target_Address, src) &&
+ (invoke_id == Request_Invoke_ID))
+ {
+ char msg[MAX_ERROR_STRING];
+ sprintf(msg, "BACnet Reject: %s", bactext_reject_reason_name((int) reject_reason));
+ LogError(msg);
+ }
+}
+
+static void My_Error_Handler(
+ BACNET_ADDRESS * src,
+ uint8_t invoke_id,
+ BACNET_ERROR_CLASS error_class,
+ BACNET_ERROR_CODE error_code)
+{
+ if (address_match(&Target_Address, src) &&
+ (invoke_id == Request_Invoke_ID))
+ {
+ char msg[MAX_ERROR_STRING];
+ sprintf(msg, "BACnet Error: %s: %s", bactext_error_class_name((int) error_class), bactext_error_code_name((int) error_code));
+ LogError(msg);
+ }
+}
+
+/**********************************/
+/* ACK handlers */
+/**********************************/
+
+/*****************************************/
+// Decode the ReadProperty Ack and pass to perl
+/****************************************/
+#define MAX_ACK_STRING 512
+void rp_ack_extract_data(BACNET_READ_PROPERTY_DATA * data)
+{
+ char ackString[MAX_ACK_STRING] = "";
+ char *pAckString = &ackString[0];
+ BACNET_OBJECT_PROPERTY_VALUE object_value; /* for bacapp printing */
+ BACNET_APPLICATION_DATA_VALUE value; /* for decode value data */
+ int len = 0;
+ uint8_t *application_data;
+ int application_data_len;
+ bool first_value = true;
+ bool print_brace = false;
+ size_t str_len;
+
+ if (data)
+ {
+ application_data = data->application_data;
+ application_data_len = data->application_data_len;
+ /* FIXME: what if application_data_len is bigger than 255? */
+ /* value? need to loop until all of the len is gone... */
+ for (;;) {
+ len =
+ bacapp_decode_application_data(application_data,
+ (uint8_t) application_data_len, &value);
+ if (first_value && (len < application_data_len))
+ {
+ first_value = false;
+ strncat(pAckString, "{", 1);
+ pAckString += 1;
+ print_brace = true;
+ }
+ object_value.object_type = data->object_type;
+ object_value.object_instance = data->object_instance;
+ object_value.object_property = data->object_property;
+ object_value.array_index = data->array_index;
+ object_value.value = &value;
+ bacapp_extract_value(&pAckString, ackString+MAX_ACK_STRING, &str_len, &object_value);
+ if (len > 0) {
+ if (len < application_data_len) {
+ application_data += len;
+ application_data_len -= len;
+ /* there's more! */
+ strncat(pAckString, ",", 1);
+ pAckString += 1;
+ } else {
+ break;
+ }
+ } else {
+ break;
+ }
+ }
+ if (print_brace)
+ {
+ strncat(pAckString, "}", 1);
+ pAckString += 1;
+ }
+
+ // Now let's call a Perl function to display the data
+ __LogAnswer(ackString, 0);
+ }
+}
+
+/*****************************************/
+// Decode the ReadPropertyMultiple Ack and pass to perl
+/****************************************/
+void rpm_ack_extract_data(BACNET_READ_ACCESS_DATA * rpm_data)
+{
+ BACNET_OBJECT_PROPERTY_VALUE object_value; /* for bacapp printing */
+ BACNET_PROPERTY_REFERENCE *listOfProperties;
+ BACNET_APPLICATION_DATA_VALUE *value;
+ bool array_value = false;
+ char ackString[MAX_ACK_STRING] = "";
+ char *pAckString = &ackString[0];
+ size_t str_len;
+
+ if (rpm_data) {
+ listOfProperties = rpm_data->listOfProperties;
+ while (listOfProperties) {
+ value = listOfProperties->value;
+ if (value) {
+ if (value->next) {
+ strncat(pAckString, "{", 1);
+ pAckString++;
+ array_value = true;
+ } else {
+ array_value = false;
+ }
+ object_value.object_type = rpm_data->object_type;
+ object_value.object_instance = rpm_data->object_instance;
+ while (value) {
+ object_value.object_property = listOfProperties->propertyIdentifier;
+ object_value.array_index = listOfProperties->propertyArrayIndex;
+ object_value.value = value;
+ bacapp_extract_value(&pAckString, ackString+MAX_ACK_STRING, &str_len, &object_value);
+ if (value->next) {
+ strncat(pAckString, ",", 1);
+ pAckString++;
+ } else {
+ if (array_value) {
+ strncat(pAckString, "}", 1);
+ pAckString++;
+ }
+ }
+ value = value->next;
+ }
+ } else {
+ /* AccessError */
+ sprintf(ackString, "BACnet Error: %s: %s",
+ bactext_error_class_name((int) listOfProperties->
+ error.error_class),
+ bactext_error_code_name((int) listOfProperties->
+ error.error_code));
+ LogError(ackString);
+ }
+ listOfProperties = listOfProperties->next;
+
+ // Add a separator between consecutive entries so that Perl can
+ // parse this out
+ strncat(pAckString, "QQQ", 3);
+ pAckString += 3;
+ }
+
+ // Now let's call a Perl function to display the data
+ __LogAnswer(ackString, 1);
+ }
+}
+
+static void AtomicReadFileAckHandler(
+ uint8_t * service_request,
+ uint16_t service_len,
+ BACNET_ADDRESS * src,
+ BACNET_CONFIRMED_SERVICE_ACK_DATA * service_data)
+{
+ int len = 0;
+ BACNET_ATOMIC_READ_FILE_DATA data;
+
+ if (address_match(&Target_Address, src) && (service_data->invoke_id == Request_Invoke_ID))
+ {
+ len = arf_ack_decode_service_request(service_request, service_len, &data);
+ if (len > 0)
+ {
+ /* validate the parameters before storing data */
+ if ((data.access == FILE_STREAM_ACCESS) && (service_data->invoke_id == Request_Invoke_ID))
+ {
+ char msg[32];
+ uint8_t *pFileData;
+ int i;
+
+ sprintf(msg, "EOF=%d,start=%d,", data.endOfFile, data.type.stream.fileStartPosition);
+ __LogAnswer(msg, 0);
+
+ pFileData = octetstring_value(&data.fileData);
+ for (i=0; iinvoke_id == Request_Invoke_ID)) {
+ len = rp_ack_decode_service_request(service_request, service_len, &data);
+ if (len > 0)
+ {
+ rp_ack_extract_data(&data);
+ }
+ }
+}
+
+/** Handler for a ReadPropertyMultiple ACK.
+ * @ingroup DSRPM
+ * For each read property, print out the ACK'd data,
+ * and free the request data items from linked property list.
+ *
+ * @param service_request [in] The contents of the service request.
+ * @param service_len [in] The length of the service_request.
+ * @param src [in] BACNET_ADDRESS of the source of the message
+ * @param service_data [in] The BACNET_CONFIRMED_SERVICE_DATA information
+ * decoded from the APDU header of this message.
+ */
+static void My_Read_Property_Multiple_Ack_Handler(
+ uint8_t * service_request,
+ uint16_t service_len,
+ BACNET_ADDRESS * src,
+ BACNET_CONFIRMED_SERVICE_ACK_DATA * service_data)
+{
+ int len = 0;
+ BACNET_READ_ACCESS_DATA *rpm_data;
+ BACNET_READ_ACCESS_DATA *old_rpm_data;
+ BACNET_PROPERTY_REFERENCE *rpm_property;
+ BACNET_PROPERTY_REFERENCE *old_rpm_property;
+ BACNET_APPLICATION_DATA_VALUE *value;
+ BACNET_APPLICATION_DATA_VALUE *old_value;
+
+ if (address_match(&Target_Address, src) &&
+ (service_data->invoke_id == Request_Invoke_ID)) {
+ rpm_data = calloc(1, sizeof(BACNET_READ_ACCESS_DATA));
+ if (rpm_data) {
+ len =
+ rpm_ack_decode_service_request(service_request, service_len,
+ rpm_data);
+ }
+ if (len > 0) {
+ while (rpm_data) {
+ rpm_ack_extract_data(rpm_data);
+ rpm_property = rpm_data->listOfProperties;
+ while (rpm_property) {
+ value = rpm_property->value;
+ while (value) {
+ old_value = value;
+ value = value->next;
+ free(old_value);
+ }
+ old_rpm_property = rpm_property;
+ rpm_property = rpm_property->next;
+ free(old_rpm_property);
+ }
+ old_rpm_data = rpm_data;
+ rpm_data = rpm_data->next;
+ free(old_rpm_data);
+ }
+ } else {
+ LogError("RPM Ack Malformed! Freeing memory...");
+ while (rpm_data) {
+ rpm_property = rpm_data->listOfProperties;
+ while (rpm_property) {
+ value = rpm_property->value;
+ while (value) {
+ old_value = value;
+ value = value->next;
+ free(old_value);
+ }
+ old_rpm_property = rpm_property;
+ rpm_property = rpm_property->next;
+ free(old_rpm_property);
+ }
+ old_rpm_data = rpm_data;
+ rpm_data = rpm_data->next;
+ free(old_rpm_data);
+ }
+ }
+ }
+}
+
+void My_Write_Property_SimpleAck_Handler(
+ BACNET_ADDRESS * src,
+ uint8_t invoke_id)
+{
+ if (address_match(&Target_Address, src) &&
+ (invoke_id == Request_Invoke_ID))
+ {
+ __LogAnswer("WriteProperty Acknowledged!", 0);
+ }
+}
+
+
+static void Init_Service_Handlers()
+{
+ Device_Init(NULL);
+
+ /* we need to handle who-is to support dynamic device binding to us */
+ apdu_set_unconfirmed_handler(SERVICE_UNCONFIRMED_WHO_IS, handler_who_is);
+
+ /* handle i-am to support binding to other devices */
+ apdu_set_unconfirmed_handler(SERVICE_UNCONFIRMED_I_AM, handler_i_am_bind);
+
+ /* set the handler for all the services we don't implement
+ It is required to send the proper reject message... */
+ apdu_set_unrecognized_service_handler_handler (handler_unrecognized_service);
+
+ /* we must implement read property - it's required! */
+ apdu_set_confirmed_handler(SERVICE_CONFIRMED_READ_PROPERTY, handler_read_property);
+
+ /* handle generic errors coming back */
+ apdu_set_abort_handler(MyAbortHandler);
+ apdu_set_reject_handler(MyRejectHandler);
+}
+
+typedef enum
+{
+ waitAnswer,
+ waitBind,
+} waitAction;
+
+static void Wait_For_Answer_Or_Timeout(unsigned timeout_ms, waitAction action)
+{
+ // Wait for timeout, failure, or success
+ time_t last_seconds = time(NULL);
+ time_t timeout_seconds = (apdu_timeout() / 1000) * apdu_retries();
+ time_t elapsed_seconds = 0;
+ uint16_t pdu_len = 0;
+ BACNET_ADDRESS src = {0}; /* address where message came from */
+ uint8_t Rx_Buf[MAX_MPDU] = { 0 };
+
+ while (true)
+ {
+ time_t current_seconds = time(NULL);
+
+ // If error was detected then bail out
+ if (Error_Detected)
+ {
+ LogError("Some other error occurred");
+ break;
+ }
+
+ if (elapsed_seconds > timeout_seconds)
+ {
+ LogError("APDU Timeout");
+ break;
+ }
+
+ /* Process PDU if one comes in */
+ pdu_len = datalink_receive(&src, &Rx_Buf[0], MAX_MPDU, timeout_ms);
+ if (pdu_len)
+ {
+ npdu_handler(&src, &Rx_Buf[0], pdu_len);
+ }
+
+ /* at least one second has passed */
+ if (current_seconds != last_seconds)
+ {
+ tsm_timer_milliseconds(((current_seconds - last_seconds) * 1000));
+ }
+
+ if (action == waitAnswer)
+ {
+ // Response was received. Exit.
+ if (tsm_invoke_id_free(Request_Invoke_ID))
+ {
+ break;
+ }
+ else if (tsm_invoke_id_failed(Request_Invoke_ID))
+ {
+ LogError("TSM Timeout!");
+ tsm_free_invoke_id(Request_Invoke_ID);
+ break;
+ }
+ }
+ else if (action == waitBind)
+ {
+ if (address_bind_request(Target_Device_Object_Instance, &Target_Max_APDU, &Target_Address))
+ {
+ break;
+ }
+ }
+ else
+ {
+ LogError("Invalid waitAction requested");
+ break;
+ }
+
+ // Keep track of time
+ elapsed_seconds += (current_seconds - last_seconds);
+ last_seconds = current_seconds;
+ }
+}
+
+/****************************************************/
+/* Interface API */
+/****************************************************/
+
+/****************************************************/
+// This is the most fundamental setup needed to start communication
+/****************************************************/
+void BacnetPrepareComm()
+{
+ /* setup my info */
+ Device_Set_Object_Instance_Number(BACNET_MAX_INSTANCE);
+ address_init();
+ Init_Service_Handlers();
+ dlenv_init();
+}
+
+/****************************************************/
+// Try to bind to a device. If successful, return zero. If failure, return
+// non-zero and log the error details
+/****************************************************/
+int BacnetBindToDevice(int deviceInstanceNumber)
+{
+ int isFailure = 0;
+
+ // Store the requested device instance number in the global variable for
+ // reference in other communication routines
+ Target_Device_Object_Instance = deviceInstanceNumber;
+
+ /* try to bind with the device */
+ if (! address_bind_request(deviceInstanceNumber, &Target_Max_APDU, &Target_Address))
+ {
+ Send_WhoIs(Target_Device_Object_Instance, Target_Device_Object_Instance);
+
+ // Wait for timeout, failure, or success
+ Wait_For_Answer_Or_Timeout(100, waitBind);
+ }
+
+ // Clean up after ourselves
+ isFailure = Error_Detected;
+ Error_Detected = false;
+ return isFailure;
+}
+
+/****************************************************/
+// This is the interface to ReadProperty
+/****************************************************/
+int BacnetReadProperty(int deviceInstanceNumber, int objectType, int objectInstanceNumber, int objectProperty, int objectIndex)
+{
+ if (!isReadPropertyHandlerRegistered)
+ {
+ /* handle the data coming back from confirmed requests */
+ apdu_set_confirmed_ack_handler(SERVICE_CONFIRMED_READ_PROPERTY, My_Read_Property_Ack_Handler);
+
+ /* handle any errors coming back */
+ apdu_set_error_handler(SERVICE_CONFIRMED_READ_PROPERTY, My_Error_Handler);
+
+ // indicate that handlers are now registered
+ isReadPropertyHandlerRegistered = true;
+ }
+
+ // Send the message out
+ Request_Invoke_ID = Send_Read_Property_Request(deviceInstanceNumber, objectType, objectInstanceNumber, objectProperty, objectIndex);
+ Wait_For_Answer_Or_Timeout(100, waitAnswer);
+
+ int isFailure = Error_Detected;
+ Error_Detected = 0;
+ return isFailure;
+}
+
+/************************************************/
+// This is the interface to ReadPropertyMultiple
+/************************************************/
+int BacnetReadPropertyMultiple(int deviceInstanceNumber, ... )
+{
+ // Get the variable argument list from the stack
+ Inline_Stack_Vars;
+ int rpmIndex = 1;
+ BACNET_READ_ACCESS_DATA *rpm_object = calloc(1, sizeof(BACNET_READ_ACCESS_DATA));
+ BACNET_READ_ACCESS_DATA *Read_Access_Data = rpm_object;
+ BACNET_PROPERTY_REFERENCE *rpm_property;
+ uint8_t buffer[MAX_PDU] = { 0 };
+
+ while (rpmIndex < Inline_Stack_Items)
+ {
+ SV *pSV = Inline_Stack_Item(rpmIndex++);
+
+ // Make sure the argument is an Array Reference
+ if (SvTYPE(SvRV(pSV)) != SVt_PVAV)
+ {
+ LogError("Argument is not an Array reference");
+ break;
+ }
+
+ // Make sure we can access the memory
+ if (rpm_object)
+ {
+ rpm_object->listOfProperties = NULL;
+ }
+ else
+ {
+ LogError("Memory Allocation Issue");
+ break;
+ }
+
+ AV *pAV = (AV *)SvRV(pSV);
+ SV **ppSV;
+
+ // The 0th argument is the object type
+ ppSV = av_fetch(pAV, 0, 0);
+ if (ppSV)
+ {
+ rpm_object->object_type = SvIV(*ppSV);
+ }
+ else
+ {
+ LogError("Problem parsing the Array of arguments");
+ break;
+ }
+
+ // The 1st argument is the object instance
+ ppSV = av_fetch(pAV, 1, 0);
+ if (ppSV)
+ {
+ rpm_object->object_instance = SvIV(*ppSV);
+ }
+ else
+ {
+ LogError("Problem parsing the Array of arguments");
+ break;
+ }
+
+ // The 2nd argument is the property type
+ ppSV = av_fetch(pAV, 2, 0);
+ if (ppSV)
+ {
+ rpm_property = calloc(1, sizeof(BACNET_PROPERTY_REFERENCE));
+ rpm_object->listOfProperties = rpm_property;
+ if (rpm_property)
+ {
+ rpm_property->propertyIdentifier = SvIV(*ppSV);
+ }
+ else
+ {
+ LogError("Memory allocation error");
+ break;
+ }
+ }
+ else
+ {
+ LogError("Problem parsing the Array of arguments");
+ break;
+ }
+
+ // The 3rd argument is the property index
+ ppSV = av_fetch(pAV, 3, 0);
+ if (ppSV)
+ {
+ rpm_property->propertyArrayIndex = SvIV(*ppSV);
+ }
+ else
+ {
+ LogError("Problem parsing the Array of arguments");
+ break;
+ }
+
+ // Advance to the next RPM index
+ if (rpmIndex < Inline_Stack_Items)
+ {
+ rpm_object->next = calloc(1, sizeof(BACNET_READ_ACCESS_DATA));
+ rpm_object = rpm_object->next;
+ }
+ else
+ {
+ rpm_object->next = NULL;
+ }
+ }
+
+ if (!isReadPropertyMultipleHandlerRegistered)
+ {
+ /* handle the data coming back from confirmed requests */
+ apdu_set_confirmed_ack_handler(SERVICE_CONFIRMED_READ_PROP_MULTIPLE,
+ My_Read_Property_Multiple_Ack_Handler);
+
+ /* handle any errors coming back */
+ apdu_set_error_handler(SERVICE_CONFIRMED_READ_PROP_MULTIPLE, My_Error_Handler);
+
+ // indicate that handlers are now registered
+ isReadPropertyMultipleHandlerRegistered = true;
+ }
+
+ // Send the message out
+ if (!Error_Detected)
+ {
+ Request_Invoke_ID = Send_Read_Property_Multiple_Request(
+ &buffer[0], sizeof(buffer),
+ deviceInstanceNumber, Read_Access_Data);
+ Wait_For_Answer_Or_Timeout(100, waitAnswer);
+ }
+
+ // Clean up allocated memory
+ BACNET_READ_ACCESS_DATA *old_rpm_object;
+ BACNET_PROPERTY_REFERENCE *old_rpm_property;
+
+ rpm_object = Read_Access_Data;
+ old_rpm_object = rpm_object;
+ while (rpm_object)
+ {
+ rpm_property = rpm_object->listOfProperties;
+ while (rpm_property)
+ {
+ old_rpm_property = rpm_property;
+ rpm_property = rpm_property->next;
+ free(old_rpm_property);
+ }
+ old_rpm_object = rpm_object;
+ rpm_object = rpm_object->next;
+ free(old_rpm_object);
+ }
+
+ // Process the return value
+ int isFailure = Error_Detected;
+ Error_Detected = 0;
+ return isFailure;
+}
+
+/****************************************************/
+// This is the interface to WriteProperty
+/****************************************************/
+int BacnetWriteProperty(int deviceInstanceNumber,
+ int objectType,
+ int objectInstanceNumber,
+ int objectProperty,
+ int objectPriority,
+ int objectIndex,
+ const char *tag,
+ const char *value)
+{
+ char msg[MAX_ERROR_STRING];
+ int isFailure = 1;
+
+ if (!isWritePropertyHandlerRegistered)
+ {
+ /* handle the ack coming back */
+ apdu_set_confirmed_simple_ack_handler(SERVICE_CONFIRMED_WRITE_PROPERTY, My_Write_Property_SimpleAck_Handler);
+
+ /* handle any errors coming back */
+ apdu_set_error_handler(SERVICE_CONFIRMED_WRITE_PROPERTY, My_Error_Handler);
+
+ // indicate that handlers are now registered
+ isWritePropertyHandlerRegistered = true;
+ }
+
+ if (objectIndex == -1)
+ {
+ objectIndex = BACNET_ARRAY_ALL;
+ }
+
+ // Loop for eary exit;
+ do
+ {
+ // Handle the tag/value pair
+ uint8_t context_tag = 0;
+ BACNET_APPLICATION_TAG property_tag;
+ BACNET_APPLICATION_DATA_VALUE propertyValue;
+
+ if (toupper(tag[0]) == 'C')
+ {
+ context_tag = strtol(&tag[1], NULL, 0);
+ propertyValue.context_tag = context_tag;
+ propertyValue.context_specific = true;
+ }
+ else
+ {
+ propertyValue.context_specific = false;
+ }
+ property_tag = strtol(tag, NULL, 0);
+
+ if (property_tag >= MAX_BACNET_APPLICATION_TAG)
+ {
+ sprintf(msg, "Error: tag=%u - it must be less than %u", property_tag, MAX_BACNET_APPLICATION_TAG);
+ LogError(msg);
+ break;
+ }
+ if (!my_bacapp_parse_application_data(property_tag, value, &propertyValue))
+ {
+ sprintf(msg, "Error: unable to parse the tag value");
+ LogError(msg);
+ break;
+ }
+ propertyValue.next = NULL;
+
+ // Send out the message
+ Request_Invoke_ID = Send_Write_Property_Request(
+ deviceInstanceNumber,
+ objectType, objectInstanceNumber,
+ objectProperty, &propertyValue, objectPriority, objectIndex);
+ Wait_For_Answer_Or_Timeout(100, waitAnswer);
+
+ // If we get here, then there were no explicit failures. However, there
+ // could have been implicit failures. Let's look at those also.
+ isFailure = Error_Detected;
+ } while(false);
+
+ // Clean up after ourselves.
+ Error_Detected = false;
+ return isFailure;
+}
+
+
+int BacnetAtomicWriteFile (int deviceInstanceNumber,
+ int fileInstanceNumber,
+ int blockStartAddr,
+ int blockNumBytes,
+ char *nibbleBuffer)
+{
+ BACNET_OCTET_STRING fileData;
+ int i, nibble;
+ uint8_t byteValue;
+ unsigned char nibbleValue;
+
+ if (!isAtomicWriteFileHandlerRegistered)
+ {
+ /* handle any errors coming back */
+ apdu_set_error_handler(SERVICE_CONFIRMED_ATOMIC_WRITE_FILE, My_Error_Handler);
+
+ // indicate that handlers are now registered
+ isAtomicWriteFileHandlerRegistered = true;
+ }
+
+ for (i=0; i= '0') && (nibbleValue <= '9') )
+ {
+ byteValue += (nibbleValue-'0') << (4*(1-nibble));
+ }
+ else if ( (nibbleValue >= 'A') && (nibbleValue <= 'F') )
+ {
+ byteValue += (nibbleValue-'A'+10) << (4*(1-nibble));
+ }
+ else
+ {
+ LogError("Bad data in buffer.");
+ }
+ }
+ fileData.value[i] = byteValue;
+ }
+ octetstring_truncate(&fileData, blockNumBytes);
+
+ // Send out the message and wait for answer
+ if (!Error_Detected)
+ {
+ Request_Invoke_ID = Send_Atomic_Write_File_Stream(
+ deviceInstanceNumber,
+ fileInstanceNumber,
+ blockStartAddr,
+ &fileData);
+ Wait_For_Answer_Or_Timeout(100, waitAnswer);
+ }
+
+ int isFailure = Error_Detected;
+ Error_Detected = 0;
+ return isFailure;
+}
+
+int BacnetGetMaxApdu()
+{
+ unsigned requestedOctetCount = 0;
+ uint16_t my_max_apdu = 0;
+
+ /* calculate the smaller of our APDU size or theirs
+ and remove the overhead of the APDU (varies depending on size).
+ note: we could fail if there is a bottle neck (router)
+ and smaller MPDU in betweeen. */
+ if (Target_Max_APDU < MAX_APDU) {
+ my_max_apdu = Target_Max_APDU;
+ } else {
+ my_max_apdu = MAX_APDU;
+ }
+ /* Typical sizes are 50, 128, 206, 480, 1024, and 1476 octets */
+ if (my_max_apdu <= 50) {
+ requestedOctetCount = my_max_apdu - 19;
+ } else if (my_max_apdu <= 480) {
+ requestedOctetCount = my_max_apdu - 32;
+ } else if (my_max_apdu <= 1476) {
+ requestedOctetCount = my_max_apdu - 64;
+ } else {
+ requestedOctetCount = my_max_apdu / 2;
+ }
+
+ return requestedOctetCount;
+}
+
+int BacnetTimeSync(int deviceInstanceNumber,
+ int year,
+ int month,
+ int day,
+ int hour,
+ int minute,
+ int second,
+ int isUTC,
+ int UTCOffset)
+
+{
+ BACNET_DATE bdate;
+ BACNET_TIME btime;
+ struct tm my_time;
+ time_t aTime;
+ struct tm *newTime;
+
+ my_time.tm_sec = second;
+ my_time.tm_min = minute;
+ my_time.tm_hour = hour;
+ my_time.tm_mday = day;
+ my_time.tm_mon = month-1;
+ my_time.tm_year = year-1900;
+ my_time.tm_wday = 0; // does not matter
+ my_time.tm_yday = 0; // does not matter
+ my_time.tm_isdst = 0; // does not matter
+
+ aTime = mktime(&my_time);
+ newTime = localtime(&aTime);
+
+ bdate.year = newTime->tm_year;
+ bdate.month = newTime->tm_mon+1;
+ bdate.day = newTime->tm_mday;
+ bdate.wday = newTime->tm_wday ? newTime->tm_wday : 7;
+ btime.hour = newTime->tm_hour;
+ btime.min = newTime->tm_min;
+ btime.sec = newTime->tm_sec;
+ btime.hundredths = 0;
+
+ int len = 0;
+ int pdu_len = 0;
+ int bytes_sent = 0;
+ BACNET_NPDU_DATA npdu_data;
+ BACNET_ADDRESS my_address;
+ uint8_t Handler_Transmit_Buffer[MAX_PDU] = { 0 };
+
+ // Loop for eary exit
+ do
+ {
+ if (!dcc_communication_enabled())
+ {
+ LogError("DCC communicaiton is not enabled");
+ break;
+ }
+
+ /* encode the NPDU portion of the packet */
+ npdu_encode_npdu_data(&npdu_data, false, MESSAGE_PRIORITY_NORMAL);
+ datalink_get_my_address(&my_address);
+ pdu_len = npdu_encode_pdu(&Handler_Transmit_Buffer[0], &Target_Address, &my_address, &npdu_data);
+
+ /* encode the APDU portion of the packet */
+ len = timesync_encode_apdu(&Handler_Transmit_Buffer[pdu_len], &bdate, &btime);
+ pdu_len += len;
+
+ /* send it out the datalink */
+ bytes_sent = datalink_send_pdu(&Target_Address, &npdu_data, &Handler_Transmit_Buffer[0], pdu_len);
+ if (bytes_sent <= 0)
+ {
+ char errorMsg[64];
+ sprintf(errorMsg, "Failed to Send Time-Synchronization Request (%s)!", strerror(errno));
+ LogError(errorMsg);
+ break;
+ }
+
+ Wait_For_Answer_Or_Timeout(100, waitAnswer);
+ } while (false);
+
+ int isFailure = Error_Detected;
+ Error_Detected = 0;
+ return isFailure;
+}
+
+/****************************************************/
+// This is the interface to AtomicReadFile
+/****************************************************/
+int BacnetAtomicReadFile(int deviceInstanceNumber, int fileInstanceNumber, int startOffset, int numBytes)
+{
+ if (!isAtomicReadFileHandlerRegistered)
+ {
+ /* handle the data coming back from confirmed requests */
+ apdu_set_confirmed_ack_handler(SERVICE_CONFIRMED_ATOMIC_READ_FILE, AtomicReadFileAckHandler);
+
+ /* handle any errors coming back */
+ apdu_set_error_handler(SERVICE_CONFIRMED_ATOMIC_READ_FILE, My_Error_Handler);
+
+ // indicate that handlers are now registered
+ isAtomicReadFileHandlerRegistered = true;
+ }
+
+ // Send the message out
+ Request_Invoke_ID = Send_Atomic_Read_File_Stream(deviceInstanceNumber, fileInstanceNumber, startOffset, numBytes);
+ Wait_For_Answer_Or_Timeout(100, waitAnswer);
+
+ int isFailure = Error_Detected;
+ Error_Detected = 0;
+ return isFailure;
+}
+
diff --git a/bacnet-stack/demo/perl/readme.txt b/bacnet-stack/demo/perl/readme.txt
new file mode 100644
index 00000000..bb33b8a4
--- /dev/null
+++ b/bacnet-stack/demo/perl/readme.txt
@@ -0,0 +1,14 @@
+The BACnet Scriptable (using Perl) Tool.
+
+* Running this tool assumes that the library has been already built. Currently,
+ the tool assumes only win32 port, but should be easily modifiable for any
+ port build. The library has to be built with BBMD_DEFINE=-DBBMD_ENABLED\=1
+* This tool has to be run from a path without any spaces. The presence of the
+ .Inline directory is required.
+* Run the tool without any arguments to see usage instructions
+* To run the example ReapProperty script (which reads Analog Value 0 Present
+ Value) for Device at instance 1234 run the following command
+
+ perl bacnet.pl --script example_readprop.pl -- 1234
+
+