From 9361362913811ce06370da457ae4581defb0860a Mon Sep 17 00:00:00 2001 From: Jakub Bartkowiak Date: Thu, 2 Jul 2026 13:07:14 +0200 Subject: [PATCH 1/2] fix: correct three BACnet encoders surfaced by the Annex F vectors (#25) ASN1.encode_bacnet_time now emits the wildcard FF FF FF FF for an unspecified time (default DateTime), matching encode_bacnet_date; it previously wrote 00:00:00.00. EncodePrivateTransferAcknowledge no longer NREs on null data and omits the optional [2] result block when there is none. EncodeLogRecord now writes the record status as a context-tagged BACnetStatusFlags bitstring (matching the decode side and ASHRAE F.3.8) instead of a constructed application bitstring. All three fixes mirror #25. Co-Authored-By: Rainer Perl --- Serialize/ASN1.cs | 9 +++++++++ Serialize/Services.cs | 12 ++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) 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); } } From 571da48ef2e699c2787f855140d6da1ff6554246 Mon Sep 17 00:00:00 2001 From: Jakub Bartkowiak Date: Thu, 2 Jul 2026 13:07:14 +0200 Subject: [PATCH 2/2] test: add ASHRAE 135 Annex F golden-vector encode tests Harvest the ASHRAE Annex F 'Examples of APDU Encoding' vectors from #25, adapted to the v4 API / xUnit, covering all four sections (F.1 alarm/event, F.2 file access, F.3 object access, F.4 remote device management) - requests, complex-acks and simple-acks. The F.1 alarm/event examples use the flat BacnetEventNotificationData rather than #25's refactored event model. Only the examples needing v4's missing request encoders (CreateObject/DeleteObject, AddListElement's BacnetObjectPropertyReference) are left out. Co-Authored-By: Rainer Perl --- Tests/Serialize/AshraeAnnexF2Tests.cs | 112 ++++++ Tests/Serialize/AshraeAnnexF3Tests.cs | 315 ++++++++++++++++ Tests/Serialize/AshraeAnnexFTests.cs | 522 ++++++++++++++++++++++++++ 3 files changed, 949 insertions(+) create mode 100644 Tests/Serialize/AshraeAnnexF2Tests.cs create mode 100644 Tests/Serialize/AshraeAnnexF3Tests.cs create mode 100644 Tests/Serialize/AshraeAnnexFTests.cs 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()); + } +}