diff --git a/Serialize/ASN1.cs b/Serialize/ASN1.cs
index 88d3412a..00a5f634 100644
--- a/Serialize/ASN1.cs
+++ b/Serialize/ASN1.cs
@@ -687,6 +687,15 @@ public static void encode_closing_tag(EncodeBuffer buffer, byte tagNumber)
public static void encode_bacnet_time(EncodeBuffer buffer, DateTime value)
{
+ if (value == new DateTime(1, 1, 1)) // 'Time any' wildcard, same sentinel encode_bacnet_date uses
+ {
+ buffer.Add(0xFF);
+ buffer.Add(0xFF);
+ buffer.Add(0xFF);
+ buffer.Add(0xFF);
+ return;
+ }
+
buffer.Add((byte)value.Hour);
buffer.Add((byte)value.Minute);
buffer.Add((byte)value.Second);
diff --git a/Serialize/Services.cs b/Serialize/Services.cs
index 81db00fb..2d3540e9 100644
--- a/Serialize/Services.cs
+++ b/Serialize/Services.cs
@@ -1462,6 +1462,11 @@ public static void EncodePrivateTransferAcknowledge(EncodeBuffer buffer, uint ve
{
ASN1.encode_context_unsigned(buffer, 0, vendorID);
ASN1.encode_context_unsigned(buffer, 1, serviceNumber);
+
+ // The result block is optional; omit it (rather than NRE / emit an empty [2]) when absent.
+ if (data == null || data.Length == 0)
+ return;
+
ASN1.encode_opening_tag(buffer, 2);
buffer.Add(data, data.Length);
ASN1.encode_closing_tag(buffer, 2);
@@ -2541,13 +2546,12 @@ public static void EncodeLogRecord(EncodeBuffer buffer, BacnetLogRecord record)
ASN1.encode_closing_tag(buffer, 1);
}
- /* Tag 2: status (BACnetStatusFlags is a fixed 4-bit string) */
+ /* Tag 2: status - a fixed 4-bit context-tagged BACnetStatusFlags bitstring (matches the
+ decode side, which reads a context-tagged bitstring, and ASHRAE 135 Annex F.3.8). */
var recordStatusFlags = BacnetBitString.ConvertFromInt((uint)record.statusFlags, 4);
if (recordStatusFlags.bits_used > 0)
{
- ASN1.encode_opening_tag(buffer, 2);
- ASN1.encode_application_bitstring(buffer, recordStatusFlags);
- ASN1.encode_closing_tag(buffer, 2);
+ ASN1.encode_context_bitstring(buffer, 2, recordStatusFlags);
}
}
diff --git a/Tests/Serialize/AshraeAnnexF2Tests.cs b/Tests/Serialize/AshraeAnnexF2Tests.cs
new file mode 100644
index 00000000..5aa3d4ff
--- /dev/null
+++ b/Tests/Serialize/AshraeAnnexF2Tests.cs
@@ -0,0 +1,112 @@
+using System.IO.BACnet.Serialize;
+using System.Linq;
+using System.Text;
+using Xunit;
+
+namespace System.IO.BACnet.Tests;
+
+///
+/// Golden-vector encode tests from ASHRAE 135 Annex F.2, AtomicReadFile / AtomicWriteFile
+/// (stream and record access). Harvested from #25 (DarkStarDS9), adapted to the v4 API.
+///
+public class AshraeAnnexF2Tests
+{
+ private static readonly byte[][] Stream = { Encoding.ASCII.GetBytes("Chiller01 On-Time=4.3 Hours") };
+ private static readonly byte[][] Records = { Encoding.ASCII.GetBytes("12:00,45.6"), Encoding.ASCII.GetBytes("12:15,44.8") };
+ private static int[] Counts(byte[][] b) => b.Select(x => x.Length).ToArray();
+
+ private static readonly BacnetObjectId File1 = new BacnetObjectId(BacnetObjectTypes.OBJECT_FILE, 1);
+ private static readonly BacnetObjectId File2 = new BacnetObjectId(BacnetObjectTypes.OBJECT_FILE, 2);
+
+ [Fact] // F.2.1 - AtomicReadFile stream complex-ack
+ public void F_2_1_AtomicReadFile_stream_ack()
+ {
+ var buffer = new EncodeBuffer();
+ APDU.EncodeComplexAck(buffer, BacnetPduTypes.PDU_TYPE_COMPLEX_ACK, BacnetConfirmedServices.SERVICE_CONFIRMED_ATOMIC_READ_FILE, 0);
+ Services.EncodeAtomicReadFileAcknowledge(buffer, true, false, 0, 1, Stream, Counts(Stream));
+
+ Assert.Equal(new byte[]
+ {
+ 0x30, 0x00, 0x06, 0x10, 0x0E, 0x31, 0x00, 0x65, 0x1B, 0x43, 0x68, 0x69, 0x6C, 0x6C, 0x65, 0x72, 0x30,
+ 0x31, 0x20, 0x4F, 0x6E, 0x2D, 0x54, 0x69, 0x6D, 0x65, 0x3D, 0x34, 0x2E, 0x33, 0x20, 0x48, 0x6F, 0x75,
+ 0x72, 0x73, 0x0F
+ }, buffer.ToArray());
+ }
+
+ [Fact] // F.2.1 - AtomicReadFile record request
+ public void F_2_1_AtomicReadFile_record_request()
+ {
+ var buffer = new EncodeBuffer();
+ APDU.EncodeConfirmedServiceRequest(buffer, BacnetPduTypes.PDU_TYPE_CONFIRMED_SERVICE_REQUEST,
+ BacnetConfirmedServices.SERVICE_CONFIRMED_ATOMIC_READ_FILE, BacnetMaxSegments.MAX_SEG0, BacnetMaxAdpu.MAX_APDU206, 18);
+ Services.EncodeAtomicReadFile(buffer, false, File2, 14, 3);
+
+ Assert.Equal(new byte[] { 0x00, 0x02, 0x12, 0x06, 0xC4, 0x02, 0x80, 0x00, 0x02, 0x1E, 0x31, 0x0E, 0x21, 0x03, 0x1F }, buffer.ToArray());
+ }
+
+ [Fact] // F.2.1 - AtomicReadFile record complex-ack
+ public void F_2_1_AtomicReadFile_record_ack()
+ {
+ var buffer = new EncodeBuffer();
+ APDU.EncodeComplexAck(buffer, BacnetPduTypes.PDU_TYPE_COMPLEX_ACK, BacnetConfirmedServices.SERVICE_CONFIRMED_ATOMIC_READ_FILE, 18);
+ Services.EncodeAtomicReadFileAcknowledge(buffer, false, true, 14, 2, Records, Counts(Records));
+
+ Assert.Equal(new byte[]
+ {
+ 0x30, 0x12, 0x06, 0x11, 0x1E, 0x31, 0x0E, 0x21, 0x02, 0x65, 0x0A, 0x31, 0x32, 0x3A, 0x30, 0x30, 0x2C,
+ 0x34, 0x35, 0x2E, 0x36, 0x65, 0x0A, 0x31, 0x32, 0x3A, 0x31, 0x35, 0x2C, 0x34, 0x34, 0x2E, 0x38, 0x1F
+ }, buffer.ToArray());
+ }
+
+ [Fact] // F.2.2 - AtomicWriteFile stream request
+ public void F_2_2_AtomicWriteFile_stream_request()
+ {
+ var buffer = new EncodeBuffer();
+ APDU.EncodeConfirmedServiceRequest(buffer, BacnetPduTypes.PDU_TYPE_CONFIRMED_SERVICE_REQUEST,
+ BacnetConfirmedServices.SERVICE_CONFIRMED_ATOMIC_WRITE_FILE, BacnetMaxSegments.MAX_SEG0, BacnetMaxAdpu.MAX_APDU206, 85);
+ Services.EncodeAtomicWriteFile(buffer, true, File1, 30, 1, Stream, Counts(Stream));
+
+ Assert.Equal(new byte[]
+ {
+ 0x00, 0x02, 0x55, 0x07, 0xC4, 0x02, 0x80, 0x00, 0x01, 0x0E, 0x31, 0x1E, 0x65, 0x1B, 0x43, 0x68, 0x69,
+ 0x6C, 0x6C, 0x65, 0x72, 0x30, 0x31, 0x20, 0x4F, 0x6E, 0x2D, 0x54, 0x69, 0x6D, 0x65, 0x3D, 0x34, 0x2E,
+ 0x33, 0x20, 0x48, 0x6F, 0x75, 0x72, 0x73, 0x0F
+ }, buffer.ToArray());
+ }
+
+ [Fact] // F.2.2 - AtomicWriteFile stream complex-ack
+ public void F_2_2_AtomicWriteFile_stream_ack()
+ {
+ var buffer = new EncodeBuffer();
+ APDU.EncodeComplexAck(buffer, BacnetPduTypes.PDU_TYPE_COMPLEX_ACK, BacnetConfirmedServices.SERVICE_CONFIRMED_ATOMIC_WRITE_FILE, 85);
+ Services.EncodeAtomicWriteFileAcknowledge(buffer, true, 30);
+
+ Assert.Equal(new byte[] { 0x30, 0x55, 0x07, 0x09, 0x1E }, buffer.ToArray());
+ }
+
+ [Fact] // F.2.2 - AtomicWriteFile record request
+ public void F_2_2_AtomicWriteFile_record_request()
+ {
+ var buffer = new EncodeBuffer();
+ APDU.EncodeConfirmedServiceRequest(buffer, BacnetPduTypes.PDU_TYPE_CONFIRMED_SERVICE_REQUEST,
+ BacnetConfirmedServices.SERVICE_CONFIRMED_ATOMIC_WRITE_FILE, BacnetMaxSegments.MAX_SEG0, BacnetMaxAdpu.MAX_APDU206, 85);
+ Services.EncodeAtomicWriteFile(buffer, false, File2, -1, 2, Records, Counts(Records));
+
+ Assert.Equal(new byte[]
+ {
+ 0x00, 0x02, 0x55, 0x07, 0xC4, 0x02, 0x80, 0x00, 0x02, 0x1E, 0x31, 0xFF, 0x21, 0x02, 0x65, 0x0A, 0x31,
+ 0x32, 0x3A, 0x30, 0x30, 0x2C, 0x34, 0x35, 0x2E, 0x36, 0x65, 0x0A, 0x31, 0x32, 0x3A, 0x31, 0x35, 0x2C,
+ 0x34, 0x34, 0x2E, 0x38, 0x1F
+ }, buffer.ToArray());
+ }
+
+ [Fact] // F.2.2 - AtomicWriteFile record complex-ack
+ public void F_2_2_AtomicWriteFile_record_ack()
+ {
+ var buffer = new EncodeBuffer();
+ APDU.EncodeComplexAck(buffer, BacnetPduTypes.PDU_TYPE_COMPLEX_ACK, BacnetConfirmedServices.SERVICE_CONFIRMED_ATOMIC_WRITE_FILE, 85);
+ Services.EncodeAtomicWriteFileAcknowledge(buffer, false, 14);
+
+ Assert.Equal(new byte[] { 0x30, 0x55, 0x07, 0x19, 0x0E }, buffer.ToArray());
+ }
+}
diff --git a/Tests/Serialize/AshraeAnnexF3Tests.cs b/Tests/Serialize/AshraeAnnexF3Tests.cs
new file mode 100644
index 00000000..1380d386
--- /dev/null
+++ b/Tests/Serialize/AshraeAnnexF3Tests.cs
@@ -0,0 +1,315 @@
+using System.Collections.Generic;
+using System.IO.BACnet.Serialize;
+using Xunit;
+
+namespace System.IO.BACnet.Tests;
+
+///
+/// Golden-vector encode tests from ASHRAE 135 Annex F, "Examples of APDU Encoding" — object access
+/// (F.3) and file access (F.2). Vectors harvested from #25 (DarkStarDS9), adapted to the v4 API.
+///
+public class AshraeAnnexF3Tests
+{
+ private static readonly BacnetObjectId Ai5 = new BacnetObjectId(BacnetObjectTypes.OBJECT_ANALOG_INPUT, 5);
+ private static readonly BacnetObjectId Ai16 = new BacnetObjectId(BacnetObjectTypes.OBJECT_ANALOG_INPUT, 16);
+ private const uint PresentValue = (uint)BacnetPropertyIds.PROP_PRESENT_VALUE;
+ private const uint Reliability = (uint)BacnetPropertyIds.PROP_RELIABILITY;
+
+ [Fact] // F.3.5 - ReadProperty request
+ public void F_3_5_ReadProperty_request()
+ {
+ var buffer = new EncodeBuffer();
+ APDU.EncodeConfirmedServiceRequest(buffer, BacnetPduTypes.PDU_TYPE_CONFIRMED_SERVICE_REQUEST,
+ BacnetConfirmedServices.SERVICE_CONFIRMED_READ_PROPERTY, BacnetMaxSegments.MAX_SEG0, BacnetMaxAdpu.MAX_APDU50, 1);
+ Services.EncodeReadProperty(buffer, Ai5, PresentValue, ASN1.BACNET_ARRAY_ALL);
+
+ Assert.Equal(new byte[] { 0x00, 0x00, 0x01, 0x0C, 0x0C, 0x00, 0x00, 0x00, 0x05, 0x19, 0x55 }, buffer.ToArray());
+ }
+
+ [Fact] // F.3.5 - ReadProperty complex-ack (Present_Value = 72.3)
+ public void F_3_5_ReadProperty_ack()
+ {
+ var buffer = new EncodeBuffer();
+ APDU.EncodeComplexAck(buffer, BacnetPduTypes.PDU_TYPE_COMPLEX_ACK,
+ BacnetConfirmedServices.SERVICE_CONFIRMED_READ_PROPERTY, 1);
+ Services.EncodeReadPropertyAcknowledge(buffer, Ai5, PresentValue, ASN1.BACNET_ARRAY_ALL,
+ new List { new BacnetValue(72.3f) });
+
+ Assert.Equal(new byte[]
+ {
+ 0x30, 0x01, 0x0C, 0x0C, 0x00, 0x00, 0x00, 0x05, 0x19, 0x55, 0x3E, 0x44, 0x42, 0x90, 0x99, 0x9A, 0x3F
+ }, buffer.ToArray());
+ }
+
+ [Fact] // F.3.7 - ReadPropertyMultiple request
+ public void F_3_7_ReadPropertyMultiple_request()
+ {
+ var buffer = new EncodeBuffer();
+ APDU.EncodeConfirmedServiceRequest(buffer, BacnetPduTypes.PDU_TYPE_CONFIRMED_SERVICE_REQUEST,
+ BacnetConfirmedServices.SERVICE_CONFIRMED_READ_PROP_MULTIPLE, BacnetMaxSegments.MAX_SEG0, BacnetMaxAdpu.MAX_APDU1024, 241);
+ Services.EncodeReadPropertyMultiple(buffer, Ai16, new List
+ {
+ new BacnetPropertyReference(PresentValue, ASN1.BACNET_ARRAY_ALL),
+ new BacnetPropertyReference(Reliability, ASN1.BACNET_ARRAY_ALL)
+ });
+
+ Assert.Equal(new byte[]
+ {
+ 0x00, 0x04, 0xF1, 0x0E, 0x0C, 0x00, 0x00, 0x00, 0x10, 0x1E, 0x09, 0x55, 0x09, 0x67, 0x1F
+ }, buffer.ToArray());
+ }
+
+ [Fact] // F.3.7 - ReadPropertyMultiple complex-ack
+ public void F_3_7_ReadPropertyMultiple_ack()
+ {
+ var result = new List
+ {
+ new BacnetReadAccessResult(Ai16, new List
+ {
+ new BacnetPropertyValue
+ {
+ property = new BacnetPropertyReference(PresentValue, ASN1.BACNET_ARRAY_ALL),
+ value = new List { new BacnetValue(72.3f) }
+ },
+ new BacnetPropertyValue
+ {
+ property = new BacnetPropertyReference(Reliability, ASN1.BACNET_ARRAY_ALL),
+ value = new List
+ {
+ new BacnetValue(BacnetApplicationTags.BACNET_APPLICATION_TAG_ENUMERATED,
+ (uint)BacnetReliability.RELIABILITY_NO_FAULT_DETECTED)
+ }
+ }
+ })
+ };
+
+ var buffer = new EncodeBuffer();
+ APDU.EncodeComplexAck(buffer, BacnetPduTypes.PDU_TYPE_COMPLEX_ACK,
+ BacnetConfirmedServices.SERVICE_CONFIRMED_READ_PROP_MULTIPLE, 241);
+ Services.EncodeReadPropertyMultipleAcknowledge(buffer, result);
+
+ Assert.Equal(new byte[]
+ {
+ 0x30, 0xF1, 0x0E, 0x0C, 0x00, 0x00, 0x00, 0x10, 0x1E, 0x29, 0x55, 0x4E, 0x44, 0x42, 0x90, 0x99, 0x9A,
+ 0x4F, 0x29, 0x67, 0x4E, 0x91, 0x00, 0x4F, 0x1F
+ }, buffer.ToArray());
+ }
+
+ [Fact] // F.3.9 - WriteProperty request (AnalogValue#1 Present_Value = 180.0)
+ public void F_3_9_WriteProperty_request()
+ {
+ var buffer = new EncodeBuffer();
+ APDU.EncodeConfirmedServiceRequest(buffer, BacnetPduTypes.PDU_TYPE_CONFIRMED_SERVICE_REQUEST,
+ BacnetConfirmedServices.SERVICE_CONFIRMED_WRITE_PROPERTY, BacnetMaxSegments.MAX_SEG0, BacnetMaxAdpu.MAX_APDU1024, 89);
+ Services.EncodeWriteProperty(buffer, new BacnetObjectId(BacnetObjectTypes.OBJECT_ANALOG_VALUE, 1),
+ PresentValue, ASN1.BACNET_ARRAY_ALL, 0, new List { new BacnetValue(180f) });
+
+ Assert.Equal(new byte[]
+ {
+ 0x00, 0x04, 0x59, 0x0F, 0x0C, 0x00, 0x80, 0x00, 0x01, 0x19, 0x55, 0x3E, 0x44, 0x43, 0x34, 0x00, 0x00, 0x3F
+ }, buffer.ToArray());
+ }
+
+ [Fact] // F.2.1 - AtomicReadFile request (stream, File#1, position 0, count 27)
+ public void F_2_1_AtomicReadFile_stream_request()
+ {
+ var buffer = new EncodeBuffer();
+ APDU.EncodeConfirmedServiceRequest(buffer, BacnetPduTypes.PDU_TYPE_CONFIRMED_SERVICE_REQUEST,
+ BacnetConfirmedServices.SERVICE_CONFIRMED_ATOMIC_READ_FILE, BacnetMaxSegments.MAX_SEG0, BacnetMaxAdpu.MAX_APDU206, 0);
+ Services.EncodeAtomicReadFile(buffer, true, new BacnetObjectId(BacnetObjectTypes.OBJECT_FILE, 1), 0, 27);
+
+ Assert.Equal(new byte[]
+ {
+ 0x00, 0x02, 0x00, 0x06, 0xC4, 0x02, 0x80, 0x00, 0x01, 0x0E, 0x31, 0x00, 0x21, 0x1B, 0x0F
+ }, buffer.ToArray());
+ }
+
+ private static List PresentValueWrite(float value) => new List
+ {
+ new BacnetPropertyValue
+ {
+ property = new BacnetPropertyReference(PresentValue, ASN1.BACNET_ARRAY_ALL),
+ value = new List { new BacnetValue(value) },
+ priority = (byte)ASN1.BACNET_NO_PRIORITY
+ }
+ };
+
+ [Fact] // F.3.10 - WritePropertyMultiple request (three AnalogValue objects)
+ public void F_3_10_WritePropertyMultiple_request()
+ {
+ var buffer = new EncodeBuffer();
+ APDU.EncodeConfirmedServiceRequest(buffer, BacnetPduTypes.PDU_TYPE_CONFIRMED_SERVICE_REQUEST,
+ BacnetConfirmedServices.SERVICE_CONFIRMED_WRITE_PROP_MULTIPLE, BacnetMaxSegments.MAX_SEG0, BacnetMaxAdpu.MAX_APDU1024, 1);
+ Services.EncodeWritePropertyMultiple(buffer, new BacnetObjectId(BacnetObjectTypes.OBJECT_ANALOG_VALUE, 5), PresentValueWrite(67f));
+ Services.EncodeWritePropertyMultiple(buffer, new BacnetObjectId(BacnetObjectTypes.OBJECT_ANALOG_VALUE, 6), PresentValueWrite(67f));
+ Services.EncodeWritePropertyMultiple(buffer, new BacnetObjectId(BacnetObjectTypes.OBJECT_ANALOG_VALUE, 7), PresentValueWrite(72f));
+
+ Assert.Equal(new byte[]
+ {
+ 0x00, 0x04, 0x01, 0x10, 0x0C, 0x00, 0x80, 0x00, 0x05, 0x1E, 0x09, 0x55, 0x2E, 0x44, 0x42, 0x86, 0x00,
+ 0x00, 0x2F, 0x1F, 0x0C, 0x00, 0x80, 0x00, 0x06, 0x1E, 0x09, 0x55, 0x2E, 0x44, 0x42, 0x86, 0x00, 0x00,
+ 0x2F, 0x1F, 0x0C, 0x00, 0x80, 0x00, 0x07, 0x1E, 0x09, 0x55, 0x2E, 0x44, 0x42, 0x90, 0x00, 0x00, 0x2F,
+ 0x1F
+ }, buffer.ToArray());
+ }
+
+ [Fact] // F.3.10 - WritePropertyMultiple simple-ack
+ public void F_3_10_WritePropertyMultiple_ack()
+ {
+ var buffer = new EncodeBuffer();
+ APDU.EncodeSimpleAck(buffer, BacnetPduTypes.PDU_TYPE_SIMPLE_ACK,
+ BacnetConfirmedServices.SERVICE_CONFIRMED_WRITE_PROP_MULTIPLE, 1);
+
+ Assert.Equal(new byte[] { 0x20, 0x01, 0x10 }, buffer.ToArray());
+ }
+
+ [Fact] // F.3.3 - CreateObject complex-ack (AnalogValue#13)
+ public void F_3_3_CreateObject_ack()
+ {
+ var buffer = new EncodeBuffer();
+ APDU.EncodeComplexAck(buffer, BacnetPduTypes.PDU_TYPE_COMPLEX_ACK,
+ BacnetConfirmedServices.SERVICE_CONFIRMED_CREATE_OBJECT, 86);
+ Services.EncodeCreateObjectAcknowledge(buffer, new BacnetObjectId(BacnetObjectTypes.OBJECT_FILE, 13));
+
+ Assert.Equal(new byte[] { 0x30, 0x56, 0x0A, 0xC4, 0x02, 0x80, 0x00, 0x0D }, buffer.ToArray());
+ }
+
+ [Fact] // F.3.4 - DeleteObject simple-ack
+ public void F_3_4_DeleteObject_ack()
+ {
+ var buffer = new EncodeBuffer();
+ APDU.EncodeSimpleAck(buffer, BacnetPduTypes.PDU_TYPE_SIMPLE_ACK,
+ BacnetConfirmedServices.SERVICE_CONFIRMED_DELETE_OBJECT, 87);
+
+ Assert.Equal(new byte[] { 0x20, 0x57, 0x0B }, buffer.ToArray());
+ }
+
+ [Fact] // F.3.1 - AddListElement simple-ack
+ public void F_3_1_AddListElement_ack()
+ {
+ var buffer = new EncodeBuffer();
+ APDU.EncodeSimpleAck(buffer, BacnetPduTypes.PDU_TYPE_SIMPLE_ACK,
+ BacnetConfirmedServices.SERVICE_CONFIRMED_ADD_LIST_ELEMENT, 1);
+
+ Assert.Equal(new byte[] { 0x20, 0x01, 0x08 }, buffer.ToArray());
+ }
+
+ [Fact] // F.3.2 - RemoveListElement simple-ack
+ public void F_3_2_RemoveListElement_ack()
+ {
+ var buffer = new EncodeBuffer();
+ APDU.EncodeSimpleAck(buffer, BacnetPduTypes.PDU_TYPE_SIMPLE_ACK,
+ BacnetConfirmedServices.SERVICE_CONFIRMED_REMOVE_LIST_ELEMENT, 52);
+
+ Assert.Equal(new byte[] { 0x20, 0x34, 0x09 }, buffer.ToArray());
+ }
+
+ [Fact] // F.3.9 - WriteProperty simple-ack
+ public void F_3_9_WriteProperty_ack()
+ {
+ var buffer = new EncodeBuffer();
+ APDU.EncodeSimpleAck(buffer, BacnetPduTypes.PDU_TYPE_SIMPLE_ACK,
+ BacnetConfirmedServices.SERVICE_CONFIRMED_WRITE_PROPERTY, 89);
+
+ Assert.Equal(new byte[] { 0x20, 0x59, 0x0F }, buffer.ToArray());
+ }
+
+ [Fact] // F.3.8 - ReadRange request (TrendLog#1 Log_Buffer, by time, 4 records)
+ public void F_3_8_ReadRange_request()
+ {
+ var buffer = new EncodeBuffer();
+ // ReadRange asks for a segmented response, so the PDU sets SEGMENTED_RESPONSE_ACCEPTED.
+ APDU.EncodeConfirmedServiceRequest(buffer,
+ BacnetPduTypes.PDU_TYPE_CONFIRMED_SERVICE_REQUEST | BacnetPduTypes.SEGMENTED_RESPONSE_ACCEPTED,
+ BacnetConfirmedServices.SERVICE_CONFIRMED_READ_RANGE, BacnetMaxSegments.MAX_SEG0, BacnetMaxAdpu.MAX_APDU206, 1);
+ Services.EncodeReadRange(buffer, new BacnetObjectId(BacnetObjectTypes.OBJECT_TRENDLOG, 1),
+ (uint)BacnetPropertyIds.PROP_LOG_BUFFER, ASN1.BACNET_ARRAY_ALL,
+ BacnetReadRangeRequestTypes.RR_BY_TIME, 0, new DateTime(1998, 3, 23, 19, 52, 34), 4);
+
+ Assert.Equal(new byte[]
+ {
+ 0x02, 0x02, 0x01, 0x1A, 0x0C, 0x05, 0x00, 0x00, 0x01, 0x19, 0x83, 0x7E, 0xA4, 0x62, 0x03, 0x17, 0x01,
+ 0xB4, 0x13, 0x34, 0x22, 0x00, 0x31, 0x04, 0x7F
+ }, buffer.ToArray());
+ }
+
+ private static BacnetReadAccessSpecification PvSpec(uint instance) =>
+ new BacnetReadAccessSpecification(new BacnetObjectId(BacnetObjectTypes.OBJECT_ANALOG_INPUT, instance),
+ new List { new BacnetPropertyReference(PresentValue, ASN1.BACNET_ARRAY_ALL) });
+
+ private static BacnetPropertyValue PvResult(BacnetValue value) => new BacnetPropertyValue
+ {
+ property = new BacnetPropertyReference(PresentValue, ASN1.BACNET_ARRAY_ALL),
+ value = new List { value }
+ };
+
+ [Fact] // F.3.7 - ReadPropertyMultiple request (multiple objects)
+ public void F_3_7_ReadPropertyMultiple_multipleObjects_request()
+ {
+ var buffer = new EncodeBuffer();
+ APDU.EncodeConfirmedServiceRequest(buffer, BacnetPduTypes.PDU_TYPE_CONFIRMED_SERVICE_REQUEST,
+ BacnetConfirmedServices.SERVICE_CONFIRMED_READ_PROP_MULTIPLE, BacnetMaxSegments.MAX_SEG0, BacnetMaxAdpu.MAX_APDU1024, 2);
+ Services.EncodeReadPropertyMultiple(buffer, new List { PvSpec(33), PvSpec(50), PvSpec(35) });
+
+ Assert.Equal(new byte[]
+ {
+ 0x00, 0x04, 0x02, 0x0E, 0x0C, 0x00, 0x00, 0x00, 0x21, 0x1E, 0x09, 0x55, 0x1F, 0x0C, 0x00, 0x00, 0x00,
+ 0x32, 0x1E, 0x09, 0x55, 0x1F, 0x0C, 0x00, 0x00, 0x00, 0x23, 0x1E, 0x09, 0x55, 0x1F
+ }, buffer.ToArray());
+ }
+
+ [Fact] // F.3.7 - ReadPropertyMultiple ack (multiple objects, one unknown-object error)
+ public void F_3_7_ReadPropertyMultiple_multipleObjects_ack()
+ {
+ var results = new List
+ {
+ new BacnetReadAccessResult(new BacnetObjectId(BacnetObjectTypes.OBJECT_ANALOG_INPUT, 33),
+ new List { PvResult(new BacnetValue(42.3f)) }),
+ new BacnetReadAccessResult(new BacnetObjectId(BacnetObjectTypes.OBJECT_ANALOG_INPUT, 50),
+ new List { PvResult(new BacnetValue(
+ new BacnetError(BacnetErrorClasses.ERROR_CLASS_OBJECT, BacnetErrorCodes.ERROR_CODE_UNKNOWN_OBJECT))) }),
+ new BacnetReadAccessResult(new BacnetObjectId(BacnetObjectTypes.OBJECT_ANALOG_INPUT, 35),
+ new List { PvResult(new BacnetValue(435.7f)) }),
+ };
+
+ var buffer = new EncodeBuffer();
+ APDU.EncodeComplexAck(buffer, BacnetPduTypes.PDU_TYPE_COMPLEX_ACK,
+ BacnetConfirmedServices.SERVICE_CONFIRMED_READ_PROP_MULTIPLE, 2);
+ Services.EncodeReadPropertyMultipleAcknowledge(buffer, results);
+
+ Assert.Equal(new byte[]
+ {
+ 0x30, 0x02, 0x0E, 0x0C, 0x00, 0x00, 0x00, 0x21, 0x1E, 0x29, 0x55, 0x4E, 0x44, 0x42, 0x29, 0x33, 0x33,
+ 0x4F, 0x1F, 0x0C, 0x00, 0x00, 0x00, 0x32, 0x1E, 0x29, 0x55, 0x5E, 0x91, 0x01, 0x91, 0x1F, 0x5F, 0x1F,
+ 0x0C, 0x00, 0x00, 0x00, 0x23, 0x1E, 0x29, 0x55, 0x4E, 0x44, 0x43, 0xD9, 0xD9, 0x9A, 0x4F, 0x1F
+ }, buffer.ToArray());
+ }
+
+ [Fact] // F.3.8 - ReadRange ack (TrendLog Log_Buffer, two records by sequence)
+ public void F_3_8_ReadRange_ack()
+ {
+ var record1 = new BacnetLogRecord(BacnetTrendLogValueType.TL_TYPE_REAL, 18.0f,
+ new DateTime(1998, 3, 23, 19, 54, 27), (BacnetStatusFlags)0);
+ var record2 = new BacnetLogRecord(BacnetTrendLogValueType.TL_TYPE_REAL, 18.1f,
+ new DateTime(1998, 3, 23, 19, 56, 27), (BacnetStatusFlags)0);
+
+ var appData = new EncodeBuffer();
+ Services.EncodeLogRecord(appData, record1);
+ Services.EncodeLogRecord(appData, record2);
+
+ var buffer = new EncodeBuffer();
+ APDU.EncodeComplexAck(buffer, BacnetPduTypes.PDU_TYPE_COMPLEX_ACK,
+ BacnetConfirmedServices.SERVICE_CONFIRMED_READ_RANGE, 1);
+ Services.EncodeReadRangeAcknowledge(buffer, new BacnetObjectId(BacnetObjectTypes.OBJECT_TRENDLOG, 1),
+ (uint)BacnetPropertyIds.PROP_LOG_BUFFER, ASN1.BACNET_ARRAY_ALL, BacnetBitString.Parse("110"), 2,
+ appData.ToArray(), BacnetReadRangeRequestTypes.RR_BY_SEQUENCE, 79201);
+
+ Assert.Equal(new byte[]
+ {
+ 0x30, 0x01, 0x1A, 0x0C, 0x05, 0x00, 0x00, 0x01, 0x19, 0x83, 0x3A, 0x05, 0xC0, 0x49, 0x02, 0x5E, 0x0E,
+ 0xA4, 0x62, 0x03, 0x17, 0x01, 0xB4, 0x13, 0x36, 0x1B, 0x00, 0x0F, 0x1E, 0x2C, 0x41, 0x90, 0x00, 0x00,
+ 0x1F, 0x2A, 0x04, 0x00, 0x0E, 0xA4, 0x62, 0x03, 0x17, 0x01, 0xB4, 0x13, 0x38, 0x1B, 0x00, 0x0F, 0x1E,
+ 0x2C, 0x41, 0x90, 0xCC, 0xCD, 0x1F, 0x2A, 0x04, 0x00, 0x5F, 0x6B, 0x01, 0x35, 0x61
+ }, buffer.ToArray());
+ }
+}
diff --git a/Tests/Serialize/AshraeAnnexFTests.cs b/Tests/Serialize/AshraeAnnexFTests.cs
new file mode 100644
index 00000000..ba084c1f
--- /dev/null
+++ b/Tests/Serialize/AshraeAnnexFTests.cs
@@ -0,0 +1,522 @@
+using System.Collections.Generic;
+using System.IO.BACnet.Serialize;
+using Xunit;
+
+namespace System.IO.BACnet.Tests;
+
+///
+/// Golden-vector encode tests from ASHRAE 135 Annex F, "Examples of APDU Encoding" — alarm/event
+/// (F.1) and remote-device management (F.4). Vectors harvested from the community test effort in
+/// #25 (DarkStarDS9) and adapted to the v4 API / xUnit. The F.1 alarm/event examples are expressed
+/// against the flat BacnetEventNotificationData / COV API (not #25's refactored event model).
+///
+public class AshraeAnnexFTests
+{
+ // AnalogInput#10 Present_Value = 65.0, Status_Flags = {false,false,false,false}
+ private static BacnetPropertyValue[] CovValues() => new[]
+ {
+ new BacnetPropertyValue
+ {
+ property = new BacnetPropertyReference((uint)BacnetPropertyIds.PROP_PRESENT_VALUE, ASN1.BACNET_ARRAY_ALL),
+ value = new List { new BacnetValue(65.0f) }
+ },
+ new BacnetPropertyValue
+ {
+ property = new BacnetPropertyReference((uint)BacnetPropertyIds.PROP_STATUS_FLAGS, ASN1.BACNET_ARRAY_ALL),
+ value = new List { new BacnetValue(BacnetBitString.Parse("0000")) }
+ }
+ };
+
+ [Fact] // F.1.2 - ConfirmedCOVNotification request
+ public void F_1_2_ConfirmedCOVNotification_request()
+ {
+ var buffer = new EncodeBuffer();
+ APDU.EncodeConfirmedServiceRequest(buffer, BacnetPduTypes.PDU_TYPE_CONFIRMED_SERVICE_REQUEST,
+ BacnetConfirmedServices.SERVICE_CONFIRMED_COV_NOTIFICATION,
+ BacnetMaxSegments.MAX_SEG0, BacnetMaxAdpu.MAX_APDU206, 15);
+ Services.EncodeCOVNotifyConfirmed(buffer, 18, 4,
+ new BacnetObjectId(BacnetObjectTypes.OBJECT_ANALOG_INPUT, 10), 0, CovValues());
+
+ Assert.Equal(new byte[]
+ {
+ 0x00, 0x02, 0x0F, 0x01,
+ 0x09, 0x12, 0x1C, 0x02, 0x00, 0x00, 0x04, 0x2C, 0x00, 0x00, 0x00, 0x0A,
+ 0x39, 0x00, 0x4E, 0x09, 0x55, 0x2E, 0x44, 0x42, 0x82, 0x00, 0x00, 0x2F,
+ 0x09, 0x6F, 0x2E, 0x82, 0x04, 0x00, 0x2F, 0x4F
+ }, buffer.ToArray());
+ }
+
+ [Fact] // F.1.2 - ConfirmedCOVNotification simple-ack
+ public void F_1_2_ConfirmedCOVNotification_ack()
+ {
+ var buffer = new EncodeBuffer();
+ APDU.EncodeSimpleAck(buffer, BacnetPduTypes.PDU_TYPE_SIMPLE_ACK,
+ BacnetConfirmedServices.SERVICE_CONFIRMED_COV_NOTIFICATION, 15);
+
+ Assert.Equal(new byte[] { 0x20, 0x0F, 0x01 }, buffer.ToArray());
+ }
+
+ [Fact] // F.1.3 - UnconfirmedCOVNotification request
+ public void F_1_3_UnconfirmedCOVNotification_request()
+ {
+ var buffer = new EncodeBuffer();
+ APDU.EncodeUnconfirmedServiceRequest(buffer, BacnetPduTypes.PDU_TYPE_UNCONFIRMED_SERVICE_REQUEST,
+ BacnetUnconfirmedServices.SERVICE_UNCONFIRMED_COV_NOTIFICATION);
+ Services.EncodeCOVNotifyUnconfirmed(buffer, 18, 4,
+ new BacnetObjectId(BacnetObjectTypes.OBJECT_ANALOG_INPUT, 10), 0, CovValues());
+
+ Assert.Equal(new byte[]
+ {
+ 0x10, 0x02,
+ 0x09, 0x12, 0x1C, 0x02, 0x00, 0x00, 0x04, 0x2C, 0x00, 0x00, 0x00, 0x0A,
+ 0x39, 0x00, 0x4E, 0x09, 0x55, 0x2E, 0x44, 0x42, 0x82, 0x00, 0x00, 0x2F,
+ 0x09, 0x6F, 0x2E, 0x82, 0x04, 0x00, 0x2F, 0x4F
+ }, buffer.ToArray());
+ }
+
+ [Fact] // F.1.4 - ConfirmedEventNotification (OutOfRange) - adapted to the flat event struct
+ public void F_1_4_ConfirmedEventNotification_request()
+ {
+ var data = new BacnetEventNotificationData
+ {
+ processIdentifier = 1,
+ initiatingObjectIdentifier = new BacnetObjectId(BacnetObjectTypes.OBJECT_DEVICE, 4),
+ eventObjectIdentifier = new BacnetObjectId(BacnetObjectTypes.OBJECT_ANALOG_INPUT, 2),
+ timeStamp = new BacnetGenericTime(default(DateTime), BacnetTimestampTags.TIME_STAMP_SEQUENCE, 16),
+ notificationClass = 4,
+ priority = 100,
+ eventType = BacnetEventTypes.EVENT_OUT_OF_RANGE,
+ notifyType = BacnetNotifyTypes.NOTIFY_ALARM,
+ ackRequired = true,
+ fromState = BacnetEventStates.EVENT_STATE_NORMAL,
+ toState = BacnetEventStates.EVENT_STATE_HIGH_LIMIT,
+ outOfRange_exceedingValue = 80.1f,
+ outOfRange_statusFlags = BacnetBitString.Parse("1000"),
+ outOfRange_deadband = 1.0f,
+ outOfRange_exceededLimit = 80.0f,
+ };
+
+ var buffer = new EncodeBuffer();
+ APDU.EncodeConfirmedServiceRequest(buffer, BacnetPduTypes.PDU_TYPE_CONFIRMED_SERVICE_REQUEST,
+ BacnetConfirmedServices.SERVICE_CONFIRMED_EVENT_NOTIFICATION,
+ BacnetMaxSegments.MAX_SEG0, BacnetMaxAdpu.MAX_APDU206, 16);
+ Services.EncodeEventNotifyConfirmed(buffer, data);
+
+ Assert.Equal(new byte[]
+ {
+ 0x00, 0x02, 0x10, 0x02, 0x09, 0x01, 0x1C, 0x02, 0x00, 0x00, 0x04, 0x2C, 0x00, 0x00, 0x00, 0x02,
+ 0x3E, 0x19, 0x10, 0x3F, 0x49, 0x04, 0x59, 0x64, 0x69, 0x05, 0x89, 0x00, 0x99, 0x01, 0xA9, 0x00,
+ 0xB9, 0x03, 0xCE, 0x5E, 0x0C, 0x42, 0xA0, 0x33, 0x33, 0x1A, 0x04, 0x80, 0x2C, 0x3F, 0x80, 0x00,
+ 0x00, 0x3C, 0x42, 0xA0, 0x00, 0x00, 0x5F, 0xCF
+ }, buffer.ToArray());
+ }
+
+ [Fact] // F.4.1 - DeviceCommunicationControl request
+ public void F_4_1_DeviceCommunicationControl_request()
+ {
+ var buffer = new EncodeBuffer();
+ APDU.EncodeConfirmedServiceRequest(buffer, BacnetPduTypes.PDU_TYPE_CONFIRMED_SERVICE_REQUEST,
+ BacnetConfirmedServices.SERVICE_CONFIRMED_DEVICE_COMMUNICATION_CONTROL,
+ BacnetMaxSegments.MAX_SEG0, BacnetMaxAdpu.MAX_APDU1024, 5);
+ Services.EncodeDeviceCommunicationControl(buffer, 5, 1 /* disable */, "#egbdf!");
+
+ Assert.Equal(
+ new byte[] { 0x00, 0x04, 0x05, 0x11, 0x09, 0x05, 0x19, 0x01, 0x2D, 0x08, 0x00, 0x23, 0x65, 0x67, 0x62, 0x64, 0x66, 0x21 },
+ buffer.ToArray());
+ }
+
+ [Fact] // F.4.1 - DeviceCommunicationControl simple-ack
+ public void F_4_1_DeviceCommunicationControl_ack()
+ {
+ var buffer = new EncodeBuffer();
+ APDU.EncodeSimpleAck(buffer, BacnetPduTypes.PDU_TYPE_SIMPLE_ACK,
+ BacnetConfirmedServices.SERVICE_CONFIRMED_DEVICE_COMMUNICATION_CONTROL, 5);
+
+ Assert.Equal(new byte[] { 0x20, 0x05, 0x11 }, buffer.ToArray());
+ }
+
+ [Fact] // F.4.4 - ReinitializeDevice request
+ public void F_4_4_ReinitializeDevice_request()
+ {
+ var buffer = new EncodeBuffer();
+ APDU.EncodeConfirmedServiceRequest(buffer, BacnetPduTypes.PDU_TYPE_CONFIRMED_SERVICE_REQUEST,
+ BacnetConfirmedServices.SERVICE_CONFIRMED_REINITIALIZE_DEVICE,
+ BacnetMaxSegments.MAX_SEG0, BacnetMaxAdpu.MAX_APDU128, 2);
+ Services.EncodeReinitializeDevice(buffer, BacnetReinitializedStates.BACNET_REINIT_WARMSTART, "AbCdEfGh");
+
+ Assert.Equal(
+ new byte[] { 0x00, 0x01, 0x02, 0x14, 0x09, 0x01, 0x1D, 0x09, 0x00, 0x41, 0x62, 0x43, 0x64, 0x45, 0x66, 0x47, 0x68 },
+ buffer.ToArray());
+ }
+
+ [Fact] // F.4.4 - ReinitializeDevice simple-ack
+ public void F_4_4_ReinitializeDevice_ack()
+ {
+ var buffer = new EncodeBuffer();
+ APDU.EncodeSimpleAck(buffer, BacnetPduTypes.PDU_TYPE_SIMPLE_ACK,
+ BacnetConfirmedServices.SERVICE_CONFIRMED_REINITIALIZE_DEVICE, 2);
+
+ Assert.Equal(new byte[] { 0x20, 0x02, 0x14 }, buffer.ToArray());
+ }
+
+ [Fact] // F.4.7 - TimeSynchronization request (1992-11-17 22:45:30.70)
+ public void F_4_7_TimeSynchronization_request()
+ {
+ var buffer = new EncodeBuffer();
+ APDU.EncodeUnconfirmedServiceRequest(buffer, BacnetPduTypes.PDU_TYPE_UNCONFIRMED_SERVICE_REQUEST,
+ BacnetUnconfirmedServices.SERVICE_UNCONFIRMED_TIME_SYNCHRONIZATION);
+ Services.EncodeTimeSync(buffer, new DateTime(1992, 11, 17, 22, 45, 30).AddMilliseconds(700));
+
+ Assert.Equal(new byte[] { 0x10, 0x06, 0xA4, 0x5C, 0x0B, 0x11, 0x02, 0xB4, 0x16, 0x2D, 0x1E, 0x46 }, buffer.ToArray());
+ }
+
+ [Fact] // F.4.8 - Who-Has by object name
+ public void F_4_8_WhoHas_by_name()
+ {
+ var buffer = new EncodeBuffer();
+ APDU.EncodeUnconfirmedServiceRequest(buffer, BacnetPduTypes.PDU_TYPE_UNCONFIRMED_SERVICE_REQUEST,
+ BacnetUnconfirmedServices.SERVICE_UNCONFIRMED_WHO_HAS);
+ Services.EncodeWhoHasBroadcast(buffer, -1, -1, null, "OATemp");
+
+ Assert.Equal(new byte[] { 0x10, 0x07, 0x3D, 0x07, 0x00, 0x4F, 0x41, 0x54, 0x65, 0x6D, 0x70 }, buffer.ToArray());
+ }
+
+ [Fact] // F.4.8 - Who-Has by object id
+ public void F_4_8_WhoHas_by_id()
+ {
+ var buffer = new EncodeBuffer();
+ APDU.EncodeUnconfirmedServiceRequest(buffer, BacnetPduTypes.PDU_TYPE_UNCONFIRMED_SERVICE_REQUEST,
+ BacnetUnconfirmedServices.SERVICE_UNCONFIRMED_WHO_HAS);
+ Services.EncodeWhoHasBroadcast(buffer, -1, -1, new BacnetObjectId(BacnetObjectTypes.OBJECT_ANALOG_INPUT, 3), null);
+
+ Assert.Equal(new byte[] { 0x10, 0x07, 0x2C, 0x00, 0x00, 0x00, 0x03 }, buffer.ToArray());
+ }
+
+ [Fact] // F.4.8 - I-Have
+ public void F_4_8_IHave()
+ {
+ var buffer = new EncodeBuffer();
+ APDU.EncodeUnconfirmedServiceRequest(buffer, BacnetPduTypes.PDU_TYPE_UNCONFIRMED_SERVICE_REQUEST,
+ BacnetUnconfirmedServices.SERVICE_UNCONFIRMED_I_HAVE);
+ Services.EncodeIhaveBroadcast(buffer, new BacnetObjectId(BacnetObjectTypes.OBJECT_DEVICE, 8),
+ new BacnetObjectId(BacnetObjectTypes.OBJECT_ANALOG_INPUT, 3), "OATemp");
+
+ Assert.Equal(new byte[]
+ {
+ 0x10, 0x01, 0xC4, 0x02, 0x00, 0x00, 0x08, 0xC4, 0x00, 0x00, 0x00, 0x03, 0x75, 0x07, 0x00, 0x4F, 0x41,
+ 0x54, 0x65, 0x6D, 0x70
+ }, buffer.ToArray());
+ }
+
+ [Fact] // F.4.9 - Who-Is by id range 3..3
+ public void F_4_9_WhoIs_by_id()
+ {
+ var buffer = new EncodeBuffer();
+ APDU.EncodeUnconfirmedServiceRequest(buffer, BacnetPduTypes.PDU_TYPE_UNCONFIRMED_SERVICE_REQUEST,
+ BacnetUnconfirmedServices.SERVICE_UNCONFIRMED_WHO_IS);
+ Services.EncodeWhoIsBroadcast(buffer, 3, 3);
+
+ Assert.Equal(new byte[] { 0x10, 0x08, 0x09, 0x03, 0x19, 0x03 }, buffer.ToArray());
+ }
+
+ [Fact] // F.4.9 - Who-Is (global)
+ public void F_4_9_WhoIs_all()
+ {
+ var buffer = new EncodeBuffer();
+ APDU.EncodeUnconfirmedServiceRequest(buffer, BacnetPduTypes.PDU_TYPE_UNCONFIRMED_SERVICE_REQUEST,
+ BacnetUnconfirmedServices.SERVICE_UNCONFIRMED_WHO_IS);
+ Services.EncodeWhoIsBroadcast(buffer, -1, -1);
+
+ Assert.Equal(new byte[] { 0x10, 0x08 }, buffer.ToArray());
+ }
+
+ [Theory] // F.4.9 - I-Am (device / max-apdu / segmentation / vendor)
+ [InlineData(1u, 480u, BacnetSegmentations.SEGMENTATION_TRANSMIT, (ushort)99, new byte[] { 0x10, 0x00, 0xC4, 0x02, 0x00, 0x00, 0x01, 0x22, 0x01, 0xE0, 0x91, 0x01, 0x21, 0x63 })]
+ [InlineData(2u, 206u, BacnetSegmentations.SEGMENTATION_RECEIVE, (ushort)33, new byte[] { 0x10, 0x00, 0xC4, 0x02, 0x00, 0x00, 0x02, 0x21, 0xCE, 0x91, 0x02, 0x21, 0x21 })]
+ [InlineData(3u, 1024u, BacnetSegmentations.SEGMENTATION_NONE, (ushort)99, new byte[] { 0x10, 0x00, 0xC4, 0x02, 0x00, 0x00, 0x03, 0x22, 0x04, 0x00, 0x91, 0x03, 0x21, 0x63 })]
+ [InlineData(4u, 128u, BacnetSegmentations.SEGMENTATION_BOTH, (ushort)66, new byte[] { 0x10, 0x00, 0xC4, 0x02, 0x00, 0x00, 0x04, 0x21, 0x80, 0x91, 0x00, 0x21, 0x42 })]
+ public void F_4_9_IAm(uint deviceId, uint maxApdu, BacnetSegmentations segmentation, ushort vendorId, byte[] expected)
+ {
+ var buffer = new EncodeBuffer();
+ APDU.EncodeUnconfirmedServiceRequest(buffer, BacnetPduTypes.PDU_TYPE_UNCONFIRMED_SERVICE_REQUEST,
+ BacnetUnconfirmedServices.SERVICE_UNCONFIRMED_I_AM);
+ Services.EncodeIamBroadcast(buffer, deviceId, maxApdu, segmentation, vendorId);
+
+ Assert.Equal(expected, buffer.ToArray());
+ }
+
+ [Fact] // F.1.6 - GetAlarmSummary complex-ack (two summaries)
+ public void F_1_6_GetAlarmSummary_ack()
+ {
+ var buffer = new EncodeBuffer();
+ APDU.EncodeComplexAck(buffer, BacnetPduTypes.PDU_TYPE_COMPLEX_ACK,
+ BacnetConfirmedServices.SERVICE_CONFIRMED_GET_ALARM_SUMMARY, 1);
+ Services.EncodeAlarmSummary(buffer, new BacnetObjectId(BacnetObjectTypes.OBJECT_ANALOG_INPUT, 2),
+ (uint)BacnetEventStates.EVENT_STATE_HIGH_LIMIT, BacnetBitString.Parse("011"));
+ Services.EncodeAlarmSummary(buffer, new BacnetObjectId(BacnetObjectTypes.OBJECT_ANALOG_INPUT, 3),
+ (uint)BacnetEventStates.EVENT_STATE_LOW_LIMIT, BacnetBitString.Parse("111"));
+
+ Assert.Equal(new byte[]
+ {
+ 0x30, 0x01, 0x03, 0xC4, 0x00, 0x00, 0x00, 0x02, 0x91, 0x03, 0x82, 0x05, 0x60, 0xC4, 0x00, 0x00, 0x00,
+ 0x03, 0x91, 0x04, 0x82, 0x05, 0xE0
+ }, buffer.ToArray());
+ }
+
+ [Fact] // F.1.8 - GetEventInformation complex-ack (unspecified timestamps -> wildcard)
+ public void F_1_8_GetEventInformation_ack()
+ {
+ var events = new[]
+ {
+ new BacnetGetEventInformationData
+ {
+ objectIdentifier = new BacnetObjectId(BacnetObjectTypes.OBJECT_ANALOG_INPUT, 2),
+ eventState = BacnetEventStates.EVENT_STATE_HIGH_LIMIT,
+ acknowledgedTransitions = BacnetBitString.Parse("011"),
+ eventTimeStamps = new[]
+ {
+ new BacnetGenericTime(new DateTime(1, 1, 1, 15, 35, 0).AddMilliseconds(200), BacnetTimestampTags.TIME_STAMP_TIME),
+ new BacnetGenericTime(default(DateTime), BacnetTimestampTags.TIME_STAMP_TIME),
+ new BacnetGenericTime(default(DateTime), BacnetTimestampTags.TIME_STAMP_TIME),
+ },
+ notifyType = BacnetNotifyTypes.NOTIFY_ALARM,
+ eventEnable = BacnetBitString.Parse("111"),
+ eventPriorities = new uint[] { 15, 15, 20 }
+ },
+ new BacnetGetEventInformationData
+ {
+ objectIdentifier = new BacnetObjectId(BacnetObjectTypes.OBJECT_ANALOG_INPUT, 3),
+ eventState = BacnetEventStates.EVENT_STATE_NORMAL,
+ acknowledgedTransitions = BacnetBitString.Parse("110"),
+ eventTimeStamps = new[]
+ {
+ new BacnetGenericTime(new DateTime(1, 1, 1, 15, 40, 0), BacnetTimestampTags.TIME_STAMP_TIME),
+ new BacnetGenericTime(default(DateTime), BacnetTimestampTags.TIME_STAMP_TIME),
+ new BacnetGenericTime(new DateTime(1, 1, 1, 15, 45, 30).AddMilliseconds(300), BacnetTimestampTags.TIME_STAMP_TIME),
+ },
+ notifyType = BacnetNotifyTypes.NOTIFY_ALARM,
+ eventEnable = BacnetBitString.Parse("111"),
+ eventPriorities = new uint[] { 15, 15, 20 }
+ }
+ };
+
+ var buffer = new EncodeBuffer();
+ APDU.EncodeComplexAck(buffer, BacnetPduTypes.PDU_TYPE_COMPLEX_ACK,
+ BacnetConfirmedServices.SERVICE_CONFIRMED_GET_EVENT_INFORMATION, 1);
+ Services.EncodeGetEventInformationAcknowledge(buffer, events, false);
+
+ Assert.Equal(new byte[]
+ {
+ 0x30, 0x01, 0x1D, 0x0E, 0x0C, 0x00, 0x00, 0x00, 0x02, 0x19, 0x03, 0x2A, 0x05, 0x60, 0x3E, 0x0C, 0x0F,
+ 0x23, 0x00, 0x14, 0x0C, 0xFF, 0xFF, 0xFF, 0xFF, 0x0C, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x49, 0x00, 0x5A,
+ 0x05, 0xE0, 0x6E, 0x21, 0x0F, 0x21, 0x0F, 0x21, 0x14, 0x6F, 0x0C, 0x00, 0x00, 0x00, 0x03, 0x19, 0x00,
+ 0x2A, 0x05, 0xC0, 0x3E, 0x0C, 0x0F, 0x28, 0x00, 0x00, 0x0C, 0xFF, 0xFF, 0xFF, 0xFF, 0x0C, 0x0F, 0x2D,
+ 0x1E, 0x1E, 0x3F, 0x49, 0x00, 0x5A, 0x05, 0xE0, 0x6E, 0x21, 0x0F, 0x21, 0x0F, 0x21, 0x14, 0x6F, 0x0F,
+ 0x19, 0x00
+ }, buffer.ToArray());
+ }
+
+ [Fact] // F.1.10 - SubscribeCOV request
+ public void F_1_10_SubscribeCOV_request()
+ {
+ var buffer = new EncodeBuffer();
+ APDU.EncodeConfirmedServiceRequest(buffer, BacnetPduTypes.PDU_TYPE_CONFIRMED_SERVICE_REQUEST,
+ BacnetConfirmedServices.SERVICE_CONFIRMED_SUBSCRIBE_COV, BacnetMaxSegments.MAX_SEG0, BacnetMaxAdpu.MAX_APDU206, 15);
+ Services.EncodeSubscribeCOV(buffer, 18, new BacnetObjectId(BacnetObjectTypes.OBJECT_ANALOG_INPUT, 10), false, true, 0);
+
+ Assert.Equal(new byte[] { 0x00, 0x02, 0x0F, 0x05, 0x09, 0x12, 0x1C, 0x00, 0x00, 0x00, 0x0A, 0x29, 0x01, 0x39, 0x00 }, buffer.ToArray());
+ }
+
+ [Fact] // F.1.11 - SubscribeCOVProperty request
+ public void F_1_11_SubscribeCOVProperty_request()
+ {
+ var buffer = new EncodeBuffer();
+ APDU.EncodeConfirmedServiceRequest(buffer, BacnetPduTypes.PDU_TYPE_CONFIRMED_SERVICE_REQUEST,
+ BacnetConfirmedServices.SERVICE_CONFIRMED_SUBSCRIBE_COV_PROPERTY, BacnetMaxSegments.MAX_SEG0, BacnetMaxAdpu.MAX_APDU206, 15);
+ Services.EncodeSubscribeProperty(buffer, 18, new BacnetObjectId(BacnetObjectTypes.OBJECT_ANALOG_INPUT, 10), false, true, 60,
+ new BacnetPropertyReference((uint)BacnetPropertyIds.PROP_PRESENT_VALUE, ASN1.BACNET_ARRAY_ALL), true, 1.0f);
+
+ Assert.Equal(new byte[]
+ {
+ 0x00, 0x02, 0x0F, 0x1C, 0x09, 0x12, 0x1C, 0x00, 0x00, 0x00, 0x0A, 0x29, 0x01, 0x39, 0x3C, 0x4E, 0x09,
+ 0x55, 0x4F, 0x5C, 0x3F, 0x80, 0x00, 0x00
+ }, buffer.ToArray());
+ }
+
+ [Fact] // F.4.2 - ConfirmedPrivateTransfer request
+ public void F_4_2_ConfirmedPrivateTransfer_request()
+ {
+ var data = new EncodeBuffer();
+ ASN1.encode_application_real(data, 72.4f);
+ ASN1.encode_application_octet_string(data, new byte[] { 0x16, 0x49 }, 0, 2);
+
+ var buffer = new EncodeBuffer();
+ APDU.EncodeConfirmedServiceRequest(buffer, BacnetPduTypes.PDU_TYPE_CONFIRMED_SERVICE_REQUEST,
+ BacnetConfirmedServices.SERVICE_CONFIRMED_PRIVATE_TRANSFER, BacnetMaxSegments.MAX_SEG0, BacnetMaxAdpu.MAX_APDU1024, 85);
+ Services.EncodePrivateTransferConfirmed(buffer, 25, 8, data.ToArray());
+
+ Assert.Equal(new byte[]
+ {
+ 0x00, 0x04, 0x55, 0x12, 0x09, 0x19, 0x19, 0x08, 0x2E, 0x44, 0x42, 0x90, 0xCC, 0xCD, 0x62, 0x16, 0x49, 0x2F
+ }, buffer.ToArray());
+ }
+
+ [Fact] // F.4.3 - ConfirmedPrivateTransfer complex-ack (no result block)
+ public void F_4_3_ConfirmedPrivateTransfer_ack()
+ {
+ var buffer = new EncodeBuffer();
+ APDU.EncodeComplexAck(buffer, BacnetPduTypes.PDU_TYPE_COMPLEX_ACK,
+ BacnetConfirmedServices.SERVICE_CONFIRMED_PRIVATE_TRANSFER, 85);
+ Services.EncodePrivateTransferAcknowledge(buffer, 25, 8, null);
+
+ Assert.Equal(new byte[] { 0x30, 0x55, 0x12, 0x09, 0x19, 0x19, 0x08 }, buffer.ToArray());
+ }
+
+ [Fact] // F.4.3 - UnconfirmedPrivateTransfer request
+ public void F_4_3_UnconfirmedPrivateTransfer_request()
+ {
+ var data = new EncodeBuffer();
+ ASN1.encode_application_real(data, 72.4f);
+ ASN1.encode_application_octet_string(data, new byte[] { 0x16, 0x49 }, 0, 2);
+
+ var buffer = new EncodeBuffer();
+ APDU.EncodeUnconfirmedServiceRequest(buffer, BacnetPduTypes.PDU_TYPE_UNCONFIRMED_SERVICE_REQUEST,
+ BacnetUnconfirmedServices.SERVICE_UNCONFIRMED_PRIVATE_TRANSFER);
+ Services.EncodePrivateTransferUnconfirmed(buffer, 25, 8, data.ToArray());
+
+ Assert.Equal(new byte[]
+ {
+ 0x10, 0x04, 0x09, 0x19, 0x19, 0x08, 0x2E, 0x44, 0x42, 0x90, 0xCC, 0xCD, 0x62, 0x16, 0x49, 0x2F
+ }, buffer.ToArray());
+ }
+
+ [Fact] // F.1.4 - ConfirmedEventNotification simple-ack
+ public void F_1_4_ConfirmedEventNotification_ack()
+ {
+ var buffer = new EncodeBuffer();
+ APDU.EncodeSimpleAck(buffer, BacnetPduTypes.PDU_TYPE_SIMPLE_ACK, BacnetConfirmedServices.SERVICE_CONFIRMED_EVENT_NOTIFICATION, 16);
+ Assert.Equal(new byte[] { 0x20, 0x10, 0x02 }, buffer.ToArray());
+ }
+
+ [Fact] // F.1.5 - AcknowledgeAlarm simple-ack
+ public void F_1_5_AcknowledgeAlarm_ack()
+ {
+ var buffer = new EncodeBuffer();
+ APDU.EncodeSimpleAck(buffer, BacnetPduTypes.PDU_TYPE_SIMPLE_ACK, BacnetConfirmedServices.SERVICE_CONFIRMED_ACKNOWLEDGE_ALARM, 7);
+ Assert.Equal(new byte[] { 0x20, 0x07, 0x00 }, buffer.ToArray());
+ }
+
+ [Fact] // F.1.9 - LifeSafetyOperation simple-ack
+ public void F_1_9_LifeSafetyOperation_ack()
+ {
+ var buffer = new EncodeBuffer();
+ APDU.EncodeSimpleAck(buffer, BacnetPduTypes.PDU_TYPE_SIMPLE_ACK, BacnetConfirmedServices.SERVICE_CONFIRMED_LIFE_SAFETY_OPERATION, 15);
+ Assert.Equal(new byte[] { 0x20, 0x0F, 0x1B }, buffer.ToArray());
+ }
+
+ [Fact] // F.1.10 - SubscribeCOV simple-ack
+ public void F_1_10_SubscribeCOV_ack()
+ {
+ var buffer = new EncodeBuffer();
+ APDU.EncodeSimpleAck(buffer, BacnetPduTypes.PDU_TYPE_SIMPLE_ACK, BacnetConfirmedServices.SERVICE_CONFIRMED_SUBSCRIBE_COV, 15);
+ Assert.Equal(new byte[] { 0x20, 0x0F, 0x05 }, buffer.ToArray());
+ }
+
+ [Fact] // F.1.11 - SubscribeCOVProperty simple-ack
+ public void F_1_11_SubscribeCOVProperty_ack()
+ {
+ var buffer = new EncodeBuffer();
+ APDU.EncodeSimpleAck(buffer, BacnetPduTypes.PDU_TYPE_SIMPLE_ACK, BacnetConfirmedServices.SERVICE_CONFIRMED_SUBSCRIBE_COV_PROPERTY, 15);
+ Assert.Equal(new byte[] { 0x20, 0x0F, 0x1C }, buffer.ToArray());
+ }
+
+ [Fact] // F.1.5 - AcknowledgeAlarm request
+ public void F_1_5_AcknowledgeAlarm_request()
+ {
+ var buffer = new EncodeBuffer();
+ APDU.EncodeConfirmedServiceRequest(buffer, BacnetPduTypes.PDU_TYPE_CONFIRMED_SERVICE_REQUEST,
+ BacnetConfirmedServices.SERVICE_CONFIRMED_ACKNOWLEDGE_ALARM, BacnetMaxSegments.MAX_SEG0, BacnetMaxAdpu.MAX_APDU206, 7);
+ Services.EncodeAlarmAcknowledge(buffer, 1, new BacnetObjectId(BacnetObjectTypes.OBJECT_ANALOG_INPUT, 2),
+ (uint)BacnetEventStates.EVENT_STATE_HIGH_LIMIT, "MDL",
+ new BacnetGenericTime(default(DateTime), BacnetTimestampTags.TIME_STAMP_SEQUENCE, 16),
+ new BacnetGenericTime(new DateTime(1992, 6, 21, 13, 3, 41).AddMilliseconds(90), BacnetTimestampTags.TIME_STAMP_DATETIME));
+
+ Assert.Equal(new byte[]
+ {
+ 0x00, 0x02, 0x07, 0x00, 0x09, 0x01, 0x1C, 0x00, 0x00, 0x00, 0x02, 0x29, 0x03, 0x3E, 0x19, 0x10, 0x3F,
+ 0x4C, 0x00, 0x4D, 0x44, 0x4C, 0x5E, 0x2E, 0xA4, 0x5C, 0x06, 0x15, 0x07, 0xB4, 0x0D, 0x03, 0x29, 0x09, 0x2F, 0x5F
+ }, buffer.ToArray());
+ }
+
+ [Fact] // F.1.5 - UnconfirmedEventNotification (OutOfRange, initiating Device 9)
+ public void F_1_5_UnconfirmedEventNotification_request()
+ {
+ var data = new BacnetEventNotificationData
+ {
+ processIdentifier = 1,
+ initiatingObjectIdentifier = new BacnetObjectId(BacnetObjectTypes.OBJECT_DEVICE, 9),
+ eventObjectIdentifier = new BacnetObjectId(BacnetObjectTypes.OBJECT_ANALOG_INPUT, 2),
+ timeStamp = new BacnetGenericTime(default(DateTime), BacnetTimestampTags.TIME_STAMP_SEQUENCE, 16),
+ notificationClass = 4,
+ priority = 100,
+ eventType = BacnetEventTypes.EVENT_OUT_OF_RANGE,
+ notifyType = BacnetNotifyTypes.NOTIFY_ALARM,
+ ackRequired = true,
+ fromState = BacnetEventStates.EVENT_STATE_NORMAL,
+ toState = BacnetEventStates.EVENT_STATE_HIGH_LIMIT,
+ outOfRange_exceedingValue = 80.1f,
+ outOfRange_statusFlags = BacnetBitString.Parse("1000"),
+ outOfRange_deadband = 1.0f,
+ outOfRange_exceededLimit = 80.0f,
+ };
+
+ var buffer = new EncodeBuffer();
+ APDU.EncodeUnconfirmedServiceRequest(buffer, BacnetPduTypes.PDU_TYPE_UNCONFIRMED_SERVICE_REQUEST,
+ BacnetUnconfirmedServices.SERVICE_UNCONFIRMED_EVENT_NOTIFICATION);
+ Services.EncodeEventNotifyUnconfirmed(buffer, data);
+
+ Assert.Equal(new byte[]
+ {
+ 0x10, 0x03, 0x09, 0x01, 0x1C, 0x02, 0x00, 0x00, 0x09, 0x2C, 0x00, 0x00, 0x00, 0x02, 0x3E, 0x19, 0x10,
+ 0x3F, 0x49, 0x04, 0x59, 0x64, 0x69, 0x05, 0x89, 0x00, 0x99, 0x01, 0xA9, 0x00, 0xB9, 0x03, 0xCE, 0x5E,
+ 0x0C, 0x42, 0xA0, 0x33, 0x33, 0x1A, 0x04, 0x80, 0x2C, 0x3F, 0x80, 0x00, 0x00, 0x3C, 0x42, 0xA0, 0x00,
+ 0x00, 0x5F, 0xCF
+ }, buffer.ToArray());
+ }
+
+ [Fact] // F.1.6 - GetAlarmSummary request (no parameters)
+ public void F_1_6_GetAlarmSummary_request()
+ {
+ var buffer = new EncodeBuffer();
+ APDU.EncodeConfirmedServiceRequest(buffer, BacnetPduTypes.PDU_TYPE_CONFIRMED_SERVICE_REQUEST,
+ BacnetConfirmedServices.SERVICE_CONFIRMED_GET_ALARM_SUMMARY, BacnetMaxSegments.MAX_SEG0, BacnetMaxAdpu.MAX_APDU206, 1);
+
+ Assert.Equal(new byte[] { 0x00, 0x02, 0x01, 0x03 }, buffer.ToArray());
+ }
+
+ [Fact] // F.1.8 - GetEventInformation request (get all)
+ public void F_1_8_GetEventInformation_request()
+ {
+ var buffer = new EncodeBuffer();
+ APDU.EncodeConfirmedServiceRequest(buffer, BacnetPduTypes.PDU_TYPE_CONFIRMED_SERVICE_REQUEST,
+ BacnetConfirmedServices.SERVICE_CONFIRMED_GET_EVENT_INFORMATION, BacnetMaxSegments.MAX_SEG0, BacnetMaxAdpu.MAX_APDU206, 1);
+ Services.EncodeGetEventInformation(buffer, false, default(BacnetObjectId));
+
+ Assert.Equal(new byte[] { 0x00, 0x02, 0x01, 0x1D }, buffer.ToArray());
+ }
+
+ [Fact] // F.1.9 - LifeSafetyOperation request
+ public void F_1_9_LifeSafetyOperation_request()
+ {
+ var buffer = new EncodeBuffer();
+ APDU.EncodeConfirmedServiceRequest(buffer, BacnetPduTypes.PDU_TYPE_CONFIRMED_SERVICE_REQUEST,
+ BacnetConfirmedServices.SERVICE_CONFIRMED_LIFE_SAFETY_OPERATION, BacnetMaxSegments.MAX_SEG0, BacnetMaxAdpu.MAX_APDU206, 15);
+ Services.EncodeLifeSafetyOperation(buffer, 18, "MDL",
+ (uint)BacnetLifeSafetyOperations.LIFE_SAFETY_OP_RESET, new BacnetObjectId(BacnetObjectTypes.OBJECT_LIFE_SAFETY_POINT, 1));
+
+ Assert.Equal(new byte[]
+ {
+ 0x00, 0x02, 0x0F, 0x1B, 0x09, 0x12, 0x1C, 0x00, 0x4D, 0x44, 0x4C, 0x29, 0x04, 0x3C, 0x05, 0x40, 0x00, 0x01
+ }, buffer.ToArray());
+ }
+}