diff --git a/.github/workflows/nuget-test-and-build.yml b/.github/workflows/nuget-test-and-build.yml index 86d2529..e74c607 100644 --- a/.github/workflows/nuget-test-and-build.yml +++ b/.github/workflows/nuget-test-and-build.yml @@ -71,6 +71,39 @@ jobs: run: dotnet restore - name: Build run: dotnet build --no-restore --configuration release + + # - name: Show current directory (debug) + # run: | + # echo "Current directory: $(pwd)" + # - name: Show repo files (debug) + # if: ${{ matrix.platform == 'ubuntu' }} + # run: | + # echo "Repo root: $(pwd)" + # ls -la + # echo "List _data:" + # ls -la Kepware.Api.Test/_data || true + # echo "List _data/projectLoadSerializeData:" + # ls -la Kepware.Api.Test/_data/projectLoadSerializeData || true + + # - name: Build test project (debug) + # if: ${{ matrix.platform == 'ubuntu' }} + # run: dotnet build Kepware.Api.Test/Kepware.Api.Test.csproj -c Release + + # - name: Show build output (debug) + # if: ${{ matrix.platform == 'ubuntu' }} + # run: ls -la Kepware.Api.Test/bin/Release || true + + # - name: Show TFM outputs (debug) + # if: ${{ matrix.platform == 'ubuntu' }} + # run: ls -la Kepware.Api.Test/bin/Release/* || true + + # - name: Show test _data (debug) + # if: ${{ matrix.platform == 'ubuntu' }} + # run: ls -la Kepware.Api.Test/bin/Release/net8.0/_data || true + # - name: Show test _data/projectLoadSerializeData (debug) + # if: ${{ matrix.platform == 'ubuntu' }} + # run: ls -la Kepware.Api.Test/bin/Release/net8.0/_data/projectLoadSerializeData || true + - name: Test run: dotnet test Kepware.Api.Test/Kepware.Api.Test.csproj --no-build --verbosity normal --configuration Release --logger "trx;LogFilePrefix=${{ matrix.platform }}-test-results" - name: Publish Test Reports (${{ matrix.platform }}) diff --git a/.gitignore b/.gitignore index 8d0032d..5b67a39 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ docs/docfx/api/ docs/docfx/_* docs/docfx/Kepware* docs/docfx/*.md +.API * # User-specific files *.rsuser diff --git a/Kepware.Api.Sample/Program.cs b/Kepware.Api.Sample/Program.cs index b323b24..e77c49a 100644 --- a/Kepware.Api.Sample/Program.cs +++ b/Kepware.Api.Sample/Program.cs @@ -57,6 +57,47 @@ static async Task Main(string[] args) await api.Project.Devices.UpdateDeviceAsync(device, true); + // --- IoT Gateway: MQTT Client Agent --- + var mqttAgent = await api.Project.IotGateway.GetOrCreateMqttClientAgentAsync("MQTT Agent by Api"); + mqttAgent.Url = "tcp://broker.example.com:1883"; + mqttAgent.Topic = "kepware/data"; + await api.Project.IotGateway.UpdateMqttClientAgentAsync(mqttAgent); + + // Add an IoT Item referencing a tag on the device created above + var mqttItem = await api.Project.IotGateway.GetOrCreateIotItemAsync( + $"{channel1.Name}.{device.Name}.BooleanByApi", mqttAgent); + mqttItem.ScanRateMs = 500; + await api.Project.IotGateway.UpdateIotItemAsync(mqttItem); + + // Clean up MQTT IoT Item and agent + await api.Project.IotGateway.DeleteIotItemAsync(mqttItem); + await api.Project.IotGateway.DeleteMqttClientAgentAsync(mqttAgent); + + // --- IoT Gateway: REST Client Agent --- + var restClientAgent = await api.Project.IotGateway.GetOrCreateRestClientAgentAsync("REST Client by Api"); + restClientAgent.Url = "https://api.example.com/data"; + restClientAgent.HttpMethod = RestClientHttpMethod.Post; + await api.Project.IotGateway.UpdateRestClientAgentAsync(restClientAgent); + + // Add an IoT Item to the REST Client agent + var restClientItem = await api.Project.IotGateway.GetOrCreateIotItemAsync( + $"{channel1.Name}.{device.Name}.SineByApi", restClientAgent); + await api.Project.IotGateway.DeleteIotItemAsync(restClientItem); + await api.Project.IotGateway.DeleteRestClientAgentAsync(restClientAgent); + + // --- IoT Gateway: REST Server Agent --- + var restServerAgent = await api.Project.IotGateway.GetOrCreateRestServerAgentAsync("REST Server by Api"); + restServerAgent.PortNumber = 39321; + restServerAgent.EnableWriteEndpoint = true; + await api.Project.IotGateway.UpdateRestServerAgentAsync(restServerAgent); + + // Add an IoT Item to the REST Server agent + var restServerItem = await api.Project.IotGateway.GetOrCreateIotItemAsync( + $"{channel1.Name}.{device.Name}.RampByApi", restServerAgent); + await api.Project.IotGateway.DeleteIotItemAsync(restServerItem); + await api.Project.IotGateway.DeleteRestServerAgentAsync(restServerAgent); + + // Clean up channel and device await api.Project.Devices.DeleteDeviceAsync(device); await api.Project.Channels.DeleteChannelAsync(channel1); diff --git a/Kepware.Api.Test/ApiClient/DeleteTests.cs b/Kepware.Api.Test/ApiClient/DeleteTests.cs index e1226c5..69142bd 100644 --- a/Kepware.Api.Test/ApiClient/DeleteTests.cs +++ b/Kepware.Api.Test/ApiClient/DeleteTests.cs @@ -4,6 +4,7 @@ using Moq.Contrib.HttpClient; using Shouldly; using System.Net; +using System.Linq; namespace Kepware.Api.Test.ApiClient; @@ -302,7 +303,8 @@ public async Task Delete_MultipleItems_WhenSuccessful_ShouldDeleteAll() var result = await _kepwareApiClient.GenericConfig.DeleteItemsAsync(tags, owner: device); // Assert - result.ShouldBeTrue(); + result.Length.ShouldBe(tags.Count); + result.All(r => r).ShouldBeTrue(); foreach (var tag in tags) { var endpoint = $"/config/v1/project/channels/{channel.Name}/devices/{device.Name}/tags/{tag.Name}"; @@ -326,7 +328,8 @@ public async Task Delete_MultipleItems_WithHttpError_ShouldLogError() var result = await _kepwareApiClient.GenericConfig.DeleteItemsAsync(tags, owner: device); // Assert - result.ShouldBeFalse(); + result.Length.ShouldBe(tags.Count); + result[0].ShouldBeFalse(); _httpMessageHandlerMock.VerifyRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{endpoint}", Times.Once()); _loggerMockGeneric.Verify(logger => logger.Log( @@ -354,7 +357,8 @@ public async Task Delete_MultipleItems_WithConnectionError_ShouldHandleGracefull var result = await _kepwareApiClient.GenericConfig.DeleteItemsAsync(tags, owner: device); // Assert - result.ShouldBeFalse(); + result.Length.ShouldBe(tags.Count); + result[0].ShouldBeFalse(); _httpMessageHandlerMock.VerifyRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{endpoint}", Times.Once()); _loggerMockGeneric.Verify(logger => logger.Log( @@ -379,7 +383,41 @@ public async Task Delete_MultipleItems_WithEmptyList_ShouldNoop() var result = await _kepwareApiClient.GenericConfig.DeleteItemsAsync(tags, owner: device); // Assert - result.ShouldBeTrue(); + result.Length.ShouldBe(0); _httpMessageHandlerMock.VerifyRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{endpoint}", Times.Never()); } + + [Fact] + public async Task Delete_MultipleItems_WithMixedResults_ShouldReturnOrderedResults() + { + // Arrange + var channel = new Channel { Name = "TestChannel" }; + var device = new Device { Name = "ParentDevice", Owner = channel }; + var tags = CreateTestTags(); + var firstEndpoint = $"/config/v1/project/channels/{channel.Name}/devices/{device.Name}/tags/{tags[0].Name}"; + var secondEndpoint = $"/config/v1/project/channels/{channel.Name}/devices/{device.Name}/tags/{tags[1].Name}"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{firstEndpoint}") + .ReturnsResponse(HttpStatusCode.OK); + _httpMessageHandlerMock.SetupRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{secondEndpoint}") + .ReturnsResponse(HttpStatusCode.InternalServerError, "Server Error"); + + // Act + var result = await _kepwareApiClient.GenericConfig.DeleteItemsAsync(tags, owner: device); + + // Assert + result.Length.ShouldBe(tags.Count); + result[0].ShouldBeTrue(); + result[1].ShouldBeFalse(); + _httpMessageHandlerMock.VerifyRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{firstEndpoint}", Times.Once()); + _httpMessageHandlerMock.VerifyRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{secondEndpoint}", Times.Once()); + _loggerMockGeneric.Verify(logger => + logger.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => true), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } } diff --git a/Kepware.Api.Test/ApiClient/GenericHandleTests.cs b/Kepware.Api.Test/ApiClient/GenericHandleTests.cs new file mode 100644 index 0000000..cb6931b --- /dev/null +++ b/Kepware.Api.Test/ApiClient/GenericHandleTests.cs @@ -0,0 +1,210 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Reflection; +using System.Text.Json; +using System.Threading.Tasks; +using Kepware.Api.Model; +using Kepware.Api.Test.ApiClient; +using Microsoft.Extensions.Logging; +using Moq; +using Moq.Contrib.HttpClient; +using Shouldly; +using Xunit; + +namespace Kepware.Api.Test.ApiClient +{ + public class GenericHandler : TestApiClientBase + { + [Fact] + public async Task CompareAndApplyDetailedAsync_ShouldCountUpdateAsFailed_When200ContainsNotApplied() + { + // Arrange + var channel = new Channel { Name = "Channel1" }; + channel.SetDynamicProperty(Properties.Channel.DeviceDriver, "Simulator"); + + var sourceDevice = new Device { Name = "Device1", Description = "new description", Owner = channel }; + var targetDevice = new Device { Name = "Device1", Description = "old description", Owner = channel }; + + var sourceCollection = new DeviceCollection { sourceDevice }; + var targetCollection = new DeviceCollection { targetDevice }; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels/Channel1/devices/Device1") + .ReturnsResponse(HttpStatusCode.OK, JsonSerializer.Serialize(targetDevice), "application/json"); + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Put, TEST_ENDPOINT + "/config/v1/project/channels/Channel1/devices/Device1") + .ReturnsResponse(HttpStatusCode.OK, + """ + { + "not_applied": { + "servermain.DEVICE_ID_OCTAL": 1, + "servermain.DEVICE_MODEL": 0 + }, + "code": 200, + "message": "Not all properties were applied." + } + """, + "application/json"); + + // Act + var result = await _kepwareApiClient.GenericConfig.CompareAndApplyDetailedAsync(sourceCollection, targetCollection, channel); + + // Assert + result.Updates.ShouldBe(0); + result.Failures.ShouldBe(1); + result.FailureList.Count.ShouldBe(1); + result.FailureList[0].Operation.ShouldBe(ApplyOperation.Update); + (result.FailureList[0].AttemptedItem as Device)?.Name.ShouldBe("Device1"); + result.FailureList[0].NotAppliedProperties.ShouldNotBeNull(); + result.FailureList[0].NotAppliedProperties!.ShouldContain("servermain.DEVICE_ID_OCTAL"); + result.FailureList[0].NotAppliedProperties!.ShouldContain("servermain.DEVICE_MODEL"); + } + + [Fact] + public async Task CompareAndApplyDetailedAsync_ShouldMap207InsertFeedbackToItems() + { + // Arrange + var channel = new Channel { Name = "Channel1" }; + channel.SetDynamicProperty(Properties.Channel.DeviceDriver, "Simulator"); + var ownerDevice = new Device { Name = "Device1", Owner = channel }; + + var tag1 = new Tag { Name = "Tag1", TagAddress = "RAMP" }; + var tag2 = new Tag { Name = "Tag2", TagAddress = "SINE" }; + + var sourceTags = new DeviceTagCollection { tag1, tag2 }; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Post, TEST_ENDPOINT + "/config/v1/project/channels/Channel1/devices/Device1/tags") + .ReturnsResponse((HttpStatusCode)207, + """ + [ + { + "property": "common.ALLTYPES_NAME", + "description": "The name 'Tag1' is already used.", + "error_line": 3, + "code": 400, + "message": "Validation failed on property common.ALLTYPES_NAME in object definition at line 3: The name 'Tag1' is already used." + }, + { + "code": 201, + "message": "Created" + } + ] + """, + "application/json"); + + // Act + var result = await _kepwareApiClient.GenericConfig.CompareAndApplyDetailedAsync(sourceTags, null, ownerDevice); + + // Assert + result.Inserts.ShouldBe(1); + result.Failures.ShouldBe(1); + result.FailureList.Count.ShouldBe(1); + result.FailureList[0].Operation.ShouldBe(ApplyOperation.Insert); + (result.FailureList[0].AttemptedItem as Tag)?.Name.ShouldBe("Tag1"); + result.FailureList[0].ResponseCode.ShouldBe(400); + result.FailureList[0].Property.ShouldBe("common.ALLTYPES_NAME"); + result.FailureList[0].Description.ShouldBe("The name 'Tag1' is already used."); + result.FailureList[0].ErrorLine.ShouldBe(3); + } + + [Fact] + public void AppendQueryString_PrivateMethod_EncodesAndSkipsNullsAndAppendsCorrectly() + { + // Arrange + var method = typeof(Kepware.Api.ClientHandler.GenericApiHandler) + .GetMethod("AppendQueryString", BindingFlags.NonPublic | BindingFlags.Static); + Assert.NotNull(method); + + var query = new[] + { + new KeyValuePair("a", "b"), + new KeyValuePair("space", "x y"), + new KeyValuePair("skip", null) // should be skipped + }; + + // Act + var result1 = (string)method!.Invoke(null, new object[] { "https://api/config", query })!; + var result2 = (string)method!.Invoke(null, new object[] { "https://api/config?existing=1", query })!; + + // Assert + Assert.Equal("https://api/config?a=b&space=x%20y", result1); + Assert.Equal("https://api/config?existing=1&a=b&space=x%20y", result2); + } + + [Fact] + public async Task LoadCollectionAsync_AppendsQueryAndReturnsCollection() + { + // Arrange + var channelsJson = """ + [ + { + "PROJECT_ID": 676550906, + "common.ALLTYPES_NAME": "Channel1", + "common.ALLTYPES_DESCRIPTION": "Example Simulator Channel", + "servermain.MULTIPLE_TYPES_DEVICE_DRIVER": "Simulator" + }, + { + "PROJECT_ID": 676550906, + "common.ALLTYPES_NAME": "Data Type Examples", + "common.ALLTYPES_DESCRIPTION": "Example Simulator Channel", + "servermain.MULTIPLE_TYPES_DEVICE_DRIVER": "Simulator" + } + ] + """; + + var query = new[] + { + new KeyValuePair("status", "active"), + new KeyValuePair("name", "John Doe"), + new KeyValuePair("skip", null) + }; + + // Expect encoded space in "John Doe" and null entry skipped + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels?status=active&name=John%20Doe") + .ReturnsResponse(channelsJson, "application/json"); + + // Act + var result = await _kepwareApiClient.GenericConfig.LoadCollectionAsync((string?)null, query); + + // Assert + Assert.NotNull(result); + Assert.Equal(2, result.Count); + Assert.Contains(result, c => c.Name == "Channel1"); + Assert.Contains(result, c => c.Name == "Data Type Examples"); + } + + [Fact] + public async Task LoadEntityAsync_AppendsQueryAndReturnsEntity() + { + // Arrange + var channelJson = """ + { + "PROJECT_ID": 676550906, + "common.ALLTYPES_NAME": "Channel1", + "common.ALLTYPES_DESCRIPTION": "Example Simulator Channel", + "servermain.MULTIPLE_TYPES_DEVICE_DRIVER": "Simulator" + } + """; + + var query = new[] + { + new KeyValuePair("status", "active"), + new KeyValuePair("name", "John Doe"), + new KeyValuePair("skip", null) + }; + + // Expect encoded space in "John Doe" and null entry skipped + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels/Channel1?status=active&name=John%20Doe") + .ReturnsResponse(channelJson, "application/json"); + + // Act + var result = await _kepwareApiClient.GenericConfig.LoadEntityAsync("Channel1", query); + + // Assert + Assert.NotNull(result); + Assert.Equal("Channel1", result.Name); + Assert.Equal("Example Simulator Channel", result.Description); + } + } +} diff --git a/Kepware.Api.Test/ApiClient/GetProductInfo.cs b/Kepware.Api.Test/ApiClient/GetProductInfoTests.cs similarity index 79% rename from Kepware.Api.Test/ApiClient/GetProductInfo.cs rename to Kepware.Api.Test/ApiClient/GetProductInfoTests.cs index 92089b2..0cb6368 100644 --- a/Kepware.Api.Test/ApiClient/GetProductInfo.cs +++ b/Kepware.Api.Test/ApiClient/GetProductInfoTests.cs @@ -32,6 +32,16 @@ public async Task GetProductInfoAsync_ShouldReturnProductInfo_WhenApiRespondsSuc Assert.Equal(240, result.ProductVersionBuild); Assert.Equal(0, result.ProductVersionPatch); + // Also verify that the ProductInfo property on the client is populated correctly + Assert.NotNull(_kepwareApiClient.ProductInfo); + Assert.Equal("012", _kepwareApiClient.ProductInfo.ProductId); + Assert.Equal("KEPServerEX", _kepwareApiClient.ProductInfo.ProductName); + Assert.Equal("V6.17.240.0", _kepwareApiClient.ProductInfo.ProductVersion); + Assert.Equal(6, _kepwareApiClient.ProductInfo.ProductVersionMajor); + Assert.Equal(17, _kepwareApiClient.ProductInfo.ProductVersionMinor); + Assert.Equal(240, _kepwareApiClient.ProductInfo.ProductVersionBuild); + Assert.Equal(0, _kepwareApiClient.ProductInfo.ProductVersionPatch); + } #region GetProductInfoAsync - SupportsJsonProjectLoadService @@ -57,6 +67,10 @@ public async Task GetProductInfoAsync_ShouldReturnCorrect_SupportsJsonProjectLoa // Assert Assert.NotNull(result); Assert.Equal(expectedResult, result.SupportsJsonProjectLoadService); + + // Also verify that the ProductInfo property on the client is populated correctly + Assert.NotNull(_kepwareApiClient.ProductInfo); + Assert.Equal(expectedResult, _kepwareApiClient.ProductInfo.SupportsJsonProjectLoadService); } #endregion @@ -99,6 +113,9 @@ public async Task GetProductInfoAsync_ShouldReturnNull_WhenApiReturnsError() // Assert Assert.Null(result); + + // ProductInfo property should also be null on error + Assert.Null(_kepwareApiClient.ProductInfo); } [Fact] @@ -114,6 +131,9 @@ public async Task GetProductInfoAsync_ShouldReturnNull_WhenApiReturnsInvalidJson // Assert Assert.Null(result); + + // ProductInfo property should also be null on error + Assert.Null(_kepwareApiClient.ProductInfo); } [Fact] @@ -128,6 +148,9 @@ public async Task GetProductInfoAsync_ShouldReturnNull_OnHttpRequestException() // Assert Assert.Null(result); + + // ProductInfo property should also be null on error + Assert.Null(_kepwareApiClient.ProductInfo); } #endregion } diff --git a/Kepware.Api.Test/ApiClient/InsertTests.cs b/Kepware.Api.Test/ApiClient/InsertTests.cs index a931cf2..6f1ac66 100644 --- a/Kepware.Api.Test/ApiClient/InsertTests.cs +++ b/Kepware.Api.Test/ApiClient/InsertTests.cs @@ -143,7 +143,7 @@ public async Task Insert_MultipleItems_WithPartialSuccess_ShouldReturnMixedResul } [Fact] - public async Task Insert_MultipleItems_WithUnsupportedDriver_ShouldSkipItems() + public async Task Insert_MultipleItems_WithUnsupportedDriver_ShouldFailUnsupportedItems() { // Arrange await ConfigureToServeDrivers(); @@ -159,8 +159,10 @@ public async Task Insert_MultipleItems_WithUnsupportedDriver_ShouldSkipItems() var results = await _kepwareApiClient.GenericConfig.InsertItemsAsync(channels); // Assert - results.Length.ShouldBe(1); // Nur der Advanced Simulator-Channel sollte eingefügt werden - results[0].ShouldBeTrue(); + results.Length.ShouldBe(2); // Both channels have results + results[0].ShouldBeFalse(); // UnsupportedDriver should fail + results[1].ShouldBeTrue(); // Advanced Simulator should succeed + _httpMessageHandlerMock.VerifyRequest(HttpMethod.Post, $"{TEST_ENDPOINT}{endpoint}", Times.Once()); _loggerMockGeneric.Verify(logger => logger.Log( diff --git a/Kepware.Api.Test/ApiClient/IotGatewayTests.cs b/Kepware.Api.Test/ApiClient/IotGatewayTests.cs new file mode 100644 index 0000000..59b2899 --- /dev/null +++ b/Kepware.Api.Test/ApiClient/IotGatewayTests.cs @@ -0,0 +1,1093 @@ +using Kepware.Api.ClientHandler; +using Kepware.Api.Model; +using Kepware.Api.Serializer; +using Microsoft.Extensions.Logging; +using Moq; +using Moq.Contrib.HttpClient; +using Shouldly; +using System.Net; +using System.Text.Json; + +namespace Kepware.Api.Test.ApiClient; + +public class IotGatewayTests : TestApiClientBase +{ + #region ServerTagToItemName Tests + + [Theory] + [InlineData("Channel1.Device1.Tag1", "Channel1_Device1_Tag1")] + [InlineData("_System._Time", "System__Time")] + [InlineData("_System._Date", "System__Date")] + [InlineData("Channel1.Device1._InternalTag", "Channel1_Device1__InternalTag")] + [InlineData("SimpleTag", "SimpleTag")] + [InlineData("_LeadingUnderscore", "LeadingUnderscore")] + public void ServerTagToItemName_ShouldConvertCorrectly(string serverTag, string expectedName) + { + var result = IotGatewayApiHandler.ServerTagToItemName(serverTag); + result.ShouldBe(expectedName); + } + + #endregion + + #region MQTT Client Agent Tests + + [Fact] + public async Task GetOrCreateMqttClientAgent_WhenNotExists_ShouldCreateAgent() + { + // Arrange + var getEndpoint = "/config/v1/project/_iot_gateway/mqtt_clients/TestAgent"; + var postEndpoint = "/config/v1/project/_iot_gateway/mqtt_clients"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, $"{TEST_ENDPOINT}{getEndpoint}") + .ReturnsResponse(HttpStatusCode.NotFound, "Not Found"); + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Post, $"{TEST_ENDPOINT}{postEndpoint}") + .ReturnsResponse(HttpStatusCode.OK); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.GetOrCreateMqttClientAgentAsync("TestAgent"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("TestAgent"); + _httpMessageHandlerMock.VerifyRequest(HttpMethod.Post, $"{TEST_ENDPOINT}{postEndpoint}", Times.Once()); + } + + [Fact] + public async Task GetOrCreateMqttClientAgent_WhenExists_ShouldReturnExistingAgent() + { + // Arrange + var agentJson = """ + { + "common.ALLTYPES_NAME": "TestAgent", + "iot_gateway.AGENTTYPES_ENABLED": true, + "iot_gateway.MQTT_CLIENT_URL": "tcp://localhost:1883" + } + """; + var getEndpoint = "/config/v1/project/_iot_gateway/mqtt_clients/TestAgent"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, $"{TEST_ENDPOINT}{getEndpoint}") + .ReturnsResponse(HttpStatusCode.OK, agentJson, "application/json"); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.GetOrCreateMqttClientAgentAsync("TestAgent"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("TestAgent"); + result.Url.ShouldBe("tcp://localhost:1883"); + // Should NOT have called POST + _httpMessageHandlerMock.VerifyRequest(HttpMethod.Post, $"{TEST_ENDPOINT}/config/v1/project/_iot_gateway/mqtt_clients", Times.Never()); + } + + [Fact] + public async Task GetOrCreateMqttClientAgent_WhenCreateFails_ShouldThrowInvalidOperationException() + { + // Arrange + var getEndpoint = "/config/v1/project/_iot_gateway/mqtt_clients/TestAgent"; + var postEndpoint = "/config/v1/project/_iot_gateway/mqtt_clients"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, $"{TEST_ENDPOINT}{getEndpoint}") + .ReturnsResponse(HttpStatusCode.NotFound, "Not Found"); + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Post, $"{TEST_ENDPOINT}{postEndpoint}") + .ReturnsResponse(HttpStatusCode.InternalServerError, "Server Error"); + + // Act & Assert + await Should.ThrowAsync(async () => + await _kepwareApiClient.Project.IotGateway.GetOrCreateMqttClientAgentAsync("TestAgent")); + } + + [Fact] + public async Task GetOrCreateMqttClientAgent_WithEmptyName_ShouldThrowArgumentException() + { + await Should.ThrowAsync(async () => + await _kepwareApiClient.Project.IotGateway.GetOrCreateMqttClientAgentAsync("")); + } + + + [Fact] + public async Task CreateMqttClientAgent_WhenSuccessful_ShouldReturnAgent() + { + // Arrange + var endpoint = "/config/v1/project/_iot_gateway/mqtt_clients"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Post, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.OK); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.CreateMqttClientAgentAsync("TestAgent"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("TestAgent"); + _httpMessageHandlerMock.VerifyRequest(HttpMethod.Post, $"{TEST_ENDPOINT}{endpoint}", Times.Once()); + } + + [Fact] + public async Task CreateMqttClientAgent_WithProperties_ShouldSetProperties() + { + // Arrange + var endpoint = "/config/v1/project/_iot_gateway/mqtt_clients"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Post, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.OK); + + var properties = new Dictionary + { + { Properties.MqttClientAgent.Url, "tcp://localhost:1883" }, + { Properties.MqttClientAgent.Topic, "test/topic" } + }; + + // Act + var result = await _kepwareApiClient.Project.IotGateway.CreateMqttClientAgentAsync("TestAgent", properties); + + // Assert + result.ShouldNotBeNull(); + result.Url.ShouldBe("tcp://localhost:1883"); + result.Topic.ShouldBe("test/topic"); + } + + [Fact] + public async Task CreateMqttClientAgent_WithHttpError_ShouldReturnNull() + { + // Arrange + var endpoint = "/config/v1/project/_iot_gateway/mqtt_clients"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Post, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.InternalServerError, "Server Error"); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.CreateMqttClientAgentAsync("TestAgent"); + + // Assert + result.ShouldBeNull(); + _loggerMockGeneric.Verify(logger => + logger.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => true), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Fact] + public async Task CreateMqttClientAgent_WithEmptyName_ShouldThrowArgumentException() + { + await Should.ThrowAsync(async () => + await _kepwareApiClient.Project.IotGateway.CreateMqttClientAgentAsync("")); + } + + [Fact] + public async Task GetMqttClientAgent_WhenExists_ShouldReturnAgent() + { + // Arrange + var agentJson = """ + { + "common.ALLTYPES_NAME": "TestAgent", + "iot_gateway.AGENTTYPES_ENABLED": true, + "iot_gateway.MQTT_CLIENT_URL": "tcp://localhost:1883" + } + """; + var endpoint = "/config/v1/project/_iot_gateway/mqtt_clients/TestAgent"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.OK, agentJson, "application/json"); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.GetMqttClientAgentAsync("TestAgent"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("TestAgent"); + result.Enabled.ShouldBe(true); + result.Url.ShouldBe("tcp://localhost:1883"); + } + + [Fact] + public async Task GetMqttClientAgent_WhenNotFound_ShouldReturnNull() + { + // Arrange + var endpoint = "/config/v1/project/_iot_gateway/mqtt_clients/NonExistent"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.NotFound, "Not Found"); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.GetMqttClientAgentAsync("NonExistent"); + + // Assert + result.ShouldBeNull(); + } + + [Fact] + public async Task DeleteMqttClientAgent_ByEntity_WhenSuccessful_ShouldReturnTrue() + { + // Arrange + var agent = new MqttClientAgent("TestAgent"); + var endpoint = "/config/v1/project/_iot_gateway/mqtt_clients/TestAgent"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.OK); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.DeleteMqttClientAgentAsync(agent); + + // Assert + result.ShouldBeTrue(); + _httpMessageHandlerMock.VerifyRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{endpoint}", Times.Once()); + } + + [Fact] + public async Task DeleteMqttClientAgent_ByName_WhenSuccessful_ShouldReturnTrue() + { + // Arrange + var endpoint = "/config/v1/project/_iot_gateway/mqtt_clients/TestAgent"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.OK); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.DeleteMqttClientAgentAsync("TestAgent"); + + // Assert + result.ShouldBeTrue(); + _httpMessageHandlerMock.VerifyRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{endpoint}", Times.Once()); + } + + [Fact] + public async Task DeleteMqttClientAgent_WithHttpError_ShouldReturnFalse() + { + // Arrange + var endpoint = "/config/v1/project/_iot_gateway/mqtt_clients/TestAgent"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.InternalServerError, "Server Error"); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.DeleteMqttClientAgentAsync("TestAgent"); + + // Assert + result.ShouldBeFalse(); + } + + [Fact] + public async Task DeleteMqttClientAgent_WithConnectionError_ShouldReturnFalse() + { + // Arrange + var endpoint = "/config/v1/project/_iot_gateway/mqtt_clients/TestAgent"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{endpoint}") + .Throws(new HttpRequestException("Connection error")); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.DeleteMqttClientAgentAsync("TestAgent"); + + // Assert + result.ShouldBeFalse(); + } + + #endregion + + #region REST Client Agent Tests + + [Fact] + public async Task GetOrCreateRestClientAgent_WhenNotExists_ShouldCreateAgent() + { + // Arrange + var getEndpoint = "/config/v1/project/_iot_gateway/rest_clients/TestRestClient"; + var postEndpoint = "/config/v1/project/_iot_gateway/rest_clients"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, $"{TEST_ENDPOINT}{getEndpoint}") + .ReturnsResponse(HttpStatusCode.NotFound, "Not Found"); + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Post, $"{TEST_ENDPOINT}{postEndpoint}") + .ReturnsResponse(HttpStatusCode.OK); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.GetOrCreateRestClientAgentAsync("TestRestClient"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("TestRestClient"); + } + + [Fact] + public async Task GetOrCreateRestClientAgent_WhenExists_ShouldReturnAgent() + { + // Arrange + var agentJson = """ + { + "common.ALLTYPES_NAME": "TestRestClient", + "iot_gateway.AGENTTYPES_ENABLED": true, + "iot_gateway.REST_CLIENT_URL": "https://api.example.com" + } + """; + var endpoint = "/config/v1/project/_iot_gateway/rest_clients/TestRestClient"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.OK, agentJson, "application/json"); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.GetOrCreateRestClientAgentAsync("TestRestClient"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("TestRestClient"); + result.Url.ShouldBe("https://api.example.com"); + } + + [Fact] + public async Task GetOrCreateRestClientAgent_WhenCreateFails_ShouldThrowInvalidOperationException() + { + // Arrange + var getEndpoint = "/config/v1/project/_iot_gateway/rest_clients/TestRestClient"; + var postEndpoint = "/config/v1/project/_iot_gateway/rest_clients"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, $"{TEST_ENDPOINT}{getEndpoint}") + .ReturnsResponse(HttpStatusCode.NotFound, "Not Found"); + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Post, $"{TEST_ENDPOINT}{postEndpoint}") + .ReturnsResponse(HttpStatusCode.InternalServerError, "Server Error"); + + // Act & Assert + await Should.ThrowAsync(async () => + await _kepwareApiClient.Project.IotGateway.GetOrCreateRestClientAgentAsync("TestRestClient")); + } + + [Fact] + public async Task GetOrCreateRestClientAgent_WithEmptyName_ShouldThrowArgumentException() + { + await Should.ThrowAsync(async () => + await _kepwareApiClient.Project.IotGateway.GetOrCreateRestClientAgentAsync("")); + } + + [Fact] + public async Task CreateRestClientAgent_WhenSuccessful_ShouldReturnAgent() + { + // Arrange + var endpoint = "/config/v1/project/_iot_gateway/rest_clients"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Post, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.OK); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.CreateRestClientAgentAsync("TestRestClient"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("TestRestClient"); + } + + [Fact] + public async Task CreateRestClientAgent_WithProperties_ShouldSetProperties() + { + // Arrange + var endpoint = "/config/v1/project/_iot_gateway/rest_clients"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Post, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.OK); + + var properties = new Dictionary + { + { Properties.RestClientAgent.Url, "https://api.example.com" } + }; + + // Act + var result = await _kepwareApiClient.Project.IotGateway.CreateRestClientAgentAsync("TestRestClient", properties); + + // Assert + result.ShouldNotBeNull(); + result.Url.ShouldBe("https://api.example.com"); + } + + [Fact] + public async Task CreateRestClientAgent_WithHttpError_ShouldReturnNull() + { + // Arrange + var endpoint = "/config/v1/project/_iot_gateway/rest_clients"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Post, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.InternalServerError, "Server Error"); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.CreateRestClientAgentAsync("TestRestClient"); + + // Assert + result.ShouldBeNull(); + } + + [Fact] + public async Task CreateRestClientAgent_WithEmptyName_ShouldThrowArgumentException() + { + await Should.ThrowAsync(async () => + await _kepwareApiClient.Project.IotGateway.CreateRestClientAgentAsync("")); + } + + [Fact] + public async Task GetRestClientAgent_WhenExists_ShouldReturnAgent() + { + // Arrange + var agentJson = """ + { + "common.ALLTYPES_NAME": "TestRestClient", + "iot_gateway.AGENTTYPES_ENABLED": true, + "iot_gateway.REST_CLIENT_URL": "https://api.example.com" + } + """; + var endpoint = "/config/v1/project/_iot_gateway/rest_clients/TestRestClient"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.OK, agentJson, "application/json"); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.GetRestClientAgentAsync("TestRestClient"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("TestRestClient"); + result.Url.ShouldBe("https://api.example.com"); + } + + [Fact] + public async Task GetRestClientAgent_WhenNotFound_ShouldReturnNull() + { + // Arrange + var endpoint = "/config/v1/project/_iot_gateway/rest_clients/NonExistent"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.NotFound, "Not Found"); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.GetRestClientAgentAsync("NonExistent"); + + // Assert + result.ShouldBeNull(); + } + + [Fact] + public async Task DeleteRestClientAgent_ByEntity_WhenSuccessful_ShouldReturnTrue() + { + // Arrange + var agent = new RestClientAgent("TestRestClient"); + var endpoint = "/config/v1/project/_iot_gateway/rest_clients/TestRestClient"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.OK); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.DeleteRestClientAgentAsync(agent); + + // Assert + result.ShouldBeTrue(); + _httpMessageHandlerMock.VerifyRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{endpoint}", Times.Once()); + } + + [Fact] + public async Task DeleteRestClientAgent_ByName_WhenSuccessful_ShouldReturnTrue() + { + // Arrange + var endpoint = "/config/v1/project/_iot_gateway/rest_clients/TestRestClient"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.OK); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.DeleteRestClientAgentAsync("TestRestClient"); + + // Assert + result.ShouldBeTrue(); + } + + [Fact] + public async Task DeleteRestClientAgent_WithHttpError_ShouldReturnFalse() + { + // Arrange + var endpoint = "/config/v1/project/_iot_gateway/rest_clients/TestRestClient"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.InternalServerError, "Server Error"); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.DeleteRestClientAgentAsync("TestRestClient"); + + // Assert + result.ShouldBeFalse(); + } + + #endregion + + #region REST Server Agent Tests + + [Fact] + public async Task GetOrCreateRestServerAgent_WhenNotExists_ShouldCreateAgent() + { + // Arrange + var getEndpoint = "/config/v1/project/_iot_gateway/rest_servers/TestRestServer"; + var postEndpoint = "/config/v1/project/_iot_gateway/rest_servers"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, $"{TEST_ENDPOINT}{getEndpoint}") + .ReturnsResponse(HttpStatusCode.NotFound, "Not Found"); + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Post, $"{TEST_ENDPOINT}{postEndpoint}") + .ReturnsResponse(HttpStatusCode.OK); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.GetOrCreateRestServerAgentAsync("TestRestServer"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("TestRestServer"); + } + + [Fact] + public async Task GetOrCreateRestServerAgent_WhenExists_ShouldReturnAgent() + { + // Arrange + var agentJson = """ + { + "common.ALLTYPES_NAME": "TestRestServer", + "iot_gateway.AGENTTYPES_ENABLED": true, + "iot_gateway.REST_SERVER_PORT_NUMBER": 39320 + } + """; + var endpoint = "/config/v1/project/_iot_gateway/rest_servers/TestRestServer"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.OK, agentJson, "application/json"); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.GetOrCreateRestServerAgentAsync("TestRestServer"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("TestRestServer"); + result.PortNumber.ShouldBe(39320); + } + + [Fact] + public async Task GetOrCreateRestServerAgent_WhenCreateFails_ShouldThrowInvalidOperationException() + { + // Arrange + var getEndpoint = "/config/v1/project/_iot_gateway/rest_servers/TestRestServer"; + var postEndpoint = "/config/v1/project/_iot_gateway/rest_servers"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, $"{TEST_ENDPOINT}{getEndpoint}") + .ReturnsResponse(HttpStatusCode.NotFound, "Not Found"); + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Post, $"{TEST_ENDPOINT}{postEndpoint}") + .ReturnsResponse(HttpStatusCode.InternalServerError, "Server Error"); + + // Act & Assert + await Should.ThrowAsync(async () => + await _kepwareApiClient.Project.IotGateway.GetOrCreateRestServerAgentAsync("TestRestServer")); + } + + [Fact] + public async Task GetOrCreateRestServerAgent_WithEmptyName_ShouldThrowArgumentException() + { + await Should.ThrowAsync(async () => + await _kepwareApiClient.Project.IotGateway.GetOrCreateRestServerAgentAsync("")); + } + + [Fact] + public async Task CreateRestServerAgent_WhenSuccessful_ShouldReturnAgent() + { + // Arrange + var endpoint = "/config/v1/project/_iot_gateway/rest_servers"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Post, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.OK); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.CreateRestServerAgentAsync("TestRestServer"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("TestRestServer"); + } + + [Fact] + public async Task CreateRestServerAgent_WithProperties_ShouldSetProperties() + { + // Arrange + var endpoint = "/config/v1/project/_iot_gateway/rest_servers"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Post, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.OK); + + var properties = new Dictionary + { + { Properties.RestServerAgent.PortNumber, 39320 } + }; + + // Act + var result = await _kepwareApiClient.Project.IotGateway.CreateRestServerAgentAsync("TestRestServer", properties); + + // Assert + result.ShouldNotBeNull(); + result.PortNumber.ShouldBe(39320); + } + + [Fact] + public async Task CreateRestServerAgent_WithHttpError_ShouldReturnNull() + { + // Arrange + var endpoint = "/config/v1/project/_iot_gateway/rest_servers"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Post, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.InternalServerError, "Server Error"); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.CreateRestServerAgentAsync("TestRestServer"); + + // Assert + result.ShouldBeNull(); + } + + [Fact] + public async Task CreateRestServerAgent_WithEmptyName_ShouldThrowArgumentException() + { + await Should.ThrowAsync(async () => + await _kepwareApiClient.Project.IotGateway.CreateRestServerAgentAsync("")); + } + + [Fact] + public async Task GetRestServerAgent_WhenExists_ShouldReturnAgent() + { + // Arrange + var agentJson = """ + { + "common.ALLTYPES_NAME": "TestRestServer", + "iot_gateway.AGENTTYPES_ENABLED": true, + "iot_gateway.REST_SERVER_PORT_NUMBER": 39320 + } + """; + var endpoint = "/config/v1/project/_iot_gateway/rest_servers/TestRestServer"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.OK, agentJson, "application/json"); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.GetRestServerAgentAsync("TestRestServer"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("TestRestServer"); + result.PortNumber.ShouldBe(39320); + } + + [Fact] + public async Task GetRestServerAgent_WhenNotFound_ShouldReturnNull() + { + // Arrange + var endpoint = "/config/v1/project/_iot_gateway/rest_servers/NonExistent"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.NotFound, "Not Found"); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.GetRestServerAgentAsync("NonExistent"); + + // Assert + result.ShouldBeNull(); + } + + [Fact] + public async Task DeleteRestServerAgent_ByEntity_WhenSuccessful_ShouldReturnTrue() + { + // Arrange + var agent = new RestServerAgent("TestRestServer"); + var endpoint = "/config/v1/project/_iot_gateway/rest_servers/TestRestServer"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.OK); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.DeleteRestServerAgentAsync(agent); + + // Assert + result.ShouldBeTrue(); + _httpMessageHandlerMock.VerifyRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{endpoint}", Times.Once()); + } + + [Fact] + public async Task DeleteRestServerAgent_ByName_WhenSuccessful_ShouldReturnTrue() + { + // Arrange + var endpoint = "/config/v1/project/_iot_gateway/rest_servers/TestRestServer"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.OK); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.DeleteRestServerAgentAsync("TestRestServer"); + + // Assert + result.ShouldBeTrue(); + } + + [Fact] + public async Task DeleteRestServerAgent_WithHttpError_ShouldReturnFalse() + { + // Arrange + var endpoint = "/config/v1/project/_iot_gateway/rest_servers/TestRestServer"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.InternalServerError, "Server Error"); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.DeleteRestServerAgentAsync("TestRestServer"); + + // Assert + result.ShouldBeFalse(); + } + + #endregion + + #region IoT Item Tests + + [Fact] + public async Task GetOrCreateIotItem_WhenNotExists_ShouldCreateWithDerivedName() + { + // Arrange - server tag "Channel1.Device1.Tag1" should query with name "Channel1_Device1_Tag1" + var parentAgent = new MqttClientAgent("ParentAgent"); + var getEndpoint = "/config/v1/project/_iot_gateway/mqtt_clients/ParentAgent/iot_items/Channel1_Device1_Tag1"; + var postEndpoint = "/config/v1/project/_iot_gateway/mqtt_clients/ParentAgent/iot_items"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, $"{TEST_ENDPOINT}{getEndpoint}") + .ReturnsResponse(HttpStatusCode.NotFound, "Not Found"); + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Post, $"{TEST_ENDPOINT}{postEndpoint}") + .ReturnsResponse(HttpStatusCode.OK); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentAgent); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("Channel1_Device1_Tag1"); + result.ServerTag.ShouldBe("Channel1.Device1.Tag1"); + } + + [Fact] + public async Task GetOrCreateIotItem_WhenExists_ShouldReturnExistingItem() + { + // Arrange + var parentAgent = new MqttClientAgent("ParentAgent"); + var itemJson = """ + { + "common.ALLTYPES_NAME": "Channel1_Device1_Tag1", + "iot_gateway.IOT_ITEM_SERVER_TAG": "Channel1.Device1.Tag1", + "iot_gateway.IOT_ITEM_ENABLED": true + } + """; + var getEndpoint = "/config/v1/project/_iot_gateway/mqtt_clients/ParentAgent/iot_items/Channel1_Device1_Tag1"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, $"{TEST_ENDPOINT}{getEndpoint}") + .ReturnsResponse(HttpStatusCode.OK, itemJson, "application/json"); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentAgent); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("Channel1_Device1_Tag1"); + // Should NOT have called POST + _httpMessageHandlerMock.VerifyRequest(HttpMethod.Post, $"{TEST_ENDPOINT}/config/v1/project/_iot_gateway/mqtt_clients/ParentAgent/iot_items", Times.Never()); + } + + [Fact] + public async Task GetOrCreateIotItem_WhenCreateFails_ShouldThrowInvalidOperationException() + { + // Arrange + var parentAgent = new MqttClientAgent("ParentAgent"); + var getEndpoint = "/config/v1/project/_iot_gateway/mqtt_clients/ParentAgent/iot_items/Channel1_Device1_Tag1"; + var postEndpoint = "/config/v1/project/_iot_gateway/mqtt_clients/ParentAgent/iot_items"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, $"{TEST_ENDPOINT}{getEndpoint}") + .ReturnsResponse(HttpStatusCode.NotFound, "Not Found"); + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Post, $"{TEST_ENDPOINT}{postEndpoint}") + .ReturnsResponse(HttpStatusCode.InternalServerError, "Server Error"); + + // Act & Assert + await Should.ThrowAsync(async () => + await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentAgent)); + } + + [Fact] + public async Task CreateIotItem_ShouldDeriveNameFromServerTag() + { + // Arrange - "Channel1.Device1.Tag1" should produce item named "Channel1_Device1_Tag1" + var parentAgent = new MqttClientAgent("ParentAgent"); + var endpoint = "/config/v1/project/_iot_gateway/mqtt_clients/ParentAgent/iot_items"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Post, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.OK); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.CreateIotItemAsync("Channel1.Device1.Tag1", parentAgent); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("Channel1_Device1_Tag1"); + result.ServerTag.ShouldBe("Channel1.Device1.Tag1"); + } + + [Fact] + public async Task CreateIotItem_WithSystemTag_ShouldStripLeadingUnderscore() + { + // Arrange - "_System._Time" should produce item named "System__Time" + var parentAgent = new MqttClientAgent("ParentAgent"); + var endpoint = "/config/v1/project/_iot_gateway/mqtt_clients/ParentAgent/iot_items"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Post, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.OK); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.CreateIotItemAsync("_System._Time", parentAgent); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("System__Time"); + result.ServerTag.ShouldBe("_System._Time"); + } + + [Fact] + public async Task CreateIotItem_WithHttpError_ShouldReturnNull() + { + // Arrange + var parentAgent = new MqttClientAgent("ParentAgent"); + var endpoint = "/config/v1/project/_iot_gateway/mqtt_clients/ParentAgent/iot_items"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Post, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.InternalServerError, "Server Error"); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.CreateIotItemAsync("Channel1.Device1.Tag1", parentAgent); + + // Assert + result.ShouldBeNull(); + } + + [Fact] + public async Task CreateIotItem_WithEmptyServerTag_ShouldThrowArgumentException() + { + var parentAgent = new MqttClientAgent("ParentAgent"); + + await Should.ThrowAsync(async () => + await _kepwareApiClient.Project.IotGateway.CreateIotItemAsync("", parentAgent)); + } + + [Fact] + public async Task CreateIotItem_WithNullParent_ShouldThrowArgumentNullException() + { + await Should.ThrowAsync(async () => + await _kepwareApiClient.Project.IotGateway.CreateIotItemAsync("Channel1.Device1.Tag1", null!)); + } + + [Fact] + public async Task GetIotItem_ShouldTranslateServerTagToItemName() + { + // Arrange - querying by server tag "Channel1.Device1.Tag1" should GET using name "Channel1_Device1_Tag1" + var parentAgent = new MqttClientAgent("ParentAgent"); + var itemJson = """ + { + "common.ALLTYPES_NAME": "Channel1_Device1_Tag1", + "iot_gateway.IOT_ITEM_SERVER_TAG": "Channel1.Device1.Tag1", + "iot_gateway.IOT_ITEM_ENABLED": true + } + """; + var endpoint = "/config/v1/project/_iot_gateway/mqtt_clients/ParentAgent/iot_items/Channel1_Device1_Tag1"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.OK, itemJson, "application/json"); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.GetIotItemAsync("Channel1.Device1.Tag1", parentAgent); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("Channel1_Device1_Tag1"); + result.ServerTag.ShouldBe("Channel1.Device1.Tag1"); + result.Enabled.ShouldBe(true); + } + + [Fact] + public async Task DeleteIotItem_ByEntity_WhenSuccessful_ShouldReturnTrue() + { + // Arrange + var parentAgent = new MqttClientAgent("ParentAgent"); + var item = new IotItem("Channel1_Device1_Tag1") { Owner = parentAgent }; + var endpoint = "/config/v1/project/_iot_gateway/mqtt_clients/ParentAgent/iot_items/Channel1_Device1_Tag1"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.OK); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.DeleteIotItemAsync(item); + + // Assert + result.ShouldBeTrue(); + _httpMessageHandlerMock.VerifyRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{endpoint}", Times.Once()); + } + + [Fact] + public async Task DeleteIotItem_ByServerTag_ShouldTranslateToItemName() + { + // Arrange - deleting by server tag "Channel1.Device1.Tag1" should DELETE using name "Channel1_Device1_Tag1" + var parentAgent = new MqttClientAgent("ParentAgent"); + var endpoint = "/config/v1/project/_iot_gateway/mqtt_clients/ParentAgent/iot_items/Channel1_Device1_Tag1"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.OK); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.DeleteIotItemAsync("Channel1.Device1.Tag1", parentAgent); + + // Assert + result.ShouldBeTrue(); + _httpMessageHandlerMock.VerifyRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{endpoint}", Times.Once()); + } + + [Fact] + public async Task DeleteIotItem_WithHttpError_ShouldReturnFalse() + { + // Arrange + var parentAgent = new MqttClientAgent("ParentAgent"); + var item = new IotItem("Channel1_Device1_Tag1") { Owner = parentAgent }; + var endpoint = "/config/v1/project/_iot_gateway/mqtt_clients/ParentAgent/iot_items/Channel1_Device1_Tag1"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.InternalServerError, "Server Error"); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.DeleteIotItemAsync(item); + + // Assert + result.ShouldBeFalse(); + } + + [Fact] + public async Task DeleteIotItem_WithConnectionError_ShouldReturnFalse() + { + // Arrange + var parentAgent = new MqttClientAgent("ParentAgent"); + var item = new IotItem("Channel1_Device1_Tag1") { Owner = parentAgent }; + var endpoint = "/config/v1/project/_iot_gateway/mqtt_clients/ParentAgent/iot_items/Channel1_Device1_Tag1"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{endpoint}") + .Throws(new HttpRequestException("Connection error")); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.DeleteIotItemAsync(item); + + // Assert + result.ShouldBeFalse(); + } + + #endregion + + #region Argument Validation Tests + + [Fact] + public async Task GetMqttClientAgent_WithEmptyName_ShouldThrowArgumentException() + { + await Should.ThrowAsync(async () => + await _kepwareApiClient.Project.IotGateway.GetMqttClientAgentAsync("")); + } + + [Fact] + public async Task GetRestClientAgent_WithEmptyName_ShouldThrowArgumentException() + { + await Should.ThrowAsync(async () => + await _kepwareApiClient.Project.IotGateway.GetRestClientAgentAsync("")); + } + + [Fact] + public async Task GetRestServerAgent_WithEmptyName_ShouldThrowArgumentException() + { + await Should.ThrowAsync(async () => + await _kepwareApiClient.Project.IotGateway.GetRestServerAgentAsync("")); + } + + [Fact] + public async Task DeleteMqttClientAgent_WithNullEntity_ShouldThrowArgumentNullException() + { + await Should.ThrowAsync(async () => + await _kepwareApiClient.Project.IotGateway.DeleteMqttClientAgentAsync((MqttClientAgent)null!)); + } + + [Fact] + public async Task DeleteRestClientAgent_WithNullEntity_ShouldThrowArgumentNullException() + { + await Should.ThrowAsync(async () => + await _kepwareApiClient.Project.IotGateway.DeleteRestClientAgentAsync((RestClientAgent)null!)); + } + + [Fact] + public async Task DeleteRestServerAgent_WithNullEntity_ShouldThrowArgumentNullException() + { + await Should.ThrowAsync(async () => + await _kepwareApiClient.Project.IotGateway.DeleteRestServerAgentAsync((RestServerAgent)null!)); + } + + [Fact] + public async Task GetIotItem_WithEmptyServerTag_ShouldThrowArgumentException() + { + var parentAgent = new MqttClientAgent("ParentAgent"); + + await Should.ThrowAsync(async () => + await _kepwareApiClient.Project.IotGateway.GetIotItemAsync("", parentAgent)); + } + + [Fact] + public async Task GetIotItem_WithNullParent_ShouldThrowArgumentNullException() + { + await Should.ThrowAsync(async () => + await _kepwareApiClient.Project.IotGateway.GetIotItemAsync("Channel1.Device1.Tag1", null!)); + } + + [Fact] + public async Task DeleteIotItem_WithNullEntity_ShouldThrowArgumentNullException() + { + await Should.ThrowAsync(async () => + await _kepwareApiClient.Project.IotGateway.DeleteIotItemAsync((IotItem)null!)); + } + + [Fact] + public async Task GetOrCreateIotItem_WithEmptyServerTag_ShouldThrowArgumentException() + { + var parentAgent = new MqttClientAgent("ParentAgent"); + + await Should.ThrowAsync(async () => + await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("", parentAgent)); + } + + [Fact] + public async Task GetOrCreateIotItem_WithNullParent_ShouldThrowArgumentNullException() + { + await Should.ThrowAsync(async () => + await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", null!)); + } + + [Fact] + public async Task DeleteIotItem_ByServerTag_WithEmptyTag_ShouldThrowArgumentException() + { + var parentAgent = new MqttClientAgent("ParentAgent"); + + await Should.ThrowAsync(async () => + await _kepwareApiClient.Project.IotGateway.DeleteIotItemAsync("", parentAgent)); + } + + [Fact] + public async Task DeleteIotItem_ByServerTag_WithNullParent_ShouldThrowArgumentNullException() + { + await Should.ThrowAsync(async () => + await _kepwareApiClient.Project.IotGateway.DeleteIotItemAsync("Channel1.Device1.Tag1", null!)); + } + + #endregion +} diff --git a/Kepware.Api.Test/ApiClient/LoadEntity.cs b/Kepware.Api.Test/ApiClient/LoadEntity.cs index 8d12269..761b948 100644 --- a/Kepware.Api.Test/ApiClient/LoadEntity.cs +++ b/Kepware.Api.Test/ApiClient/LoadEntity.cs @@ -379,7 +379,7 @@ public async Task LoadEntityAsync_ShouldThrowInvalidOperationException_WhenLoadR #endregion - #region LoadEntityAsync - Single Tag mit DynamicProperties + #region LoadEntityAsync - Single Tag with DynamicProperties [Fact] public async Task LoadEntityAsync_ShouldReturnTag_WithCorrectDynamicProperties() diff --git a/Kepware.Api.Test/ApiClient/ProjectApiHandlerTests.cs b/Kepware.Api.Test/ApiClient/ProjectApiHandlerTests.cs index 34989a0..d4a5a48 100644 --- a/Kepware.Api.Test/ApiClient/ProjectApiHandlerTests.cs +++ b/Kepware.Api.Test/ApiClient/ProjectApiHandlerTests.cs @@ -464,7 +464,7 @@ public async Task CompareAndApply_ShouldReturn2Inserts1Update1Delete_WhenSourceH newProject.SetDynamicProperty("uaserverinterface.PROJECT_OPC_UA_ANONYMOUS_LOGIN", false); // Act - var result = await _projectApiHandler.CompareAndApply(newProject); + var result = await _projectApiHandler.CompareAndApplyAsync(newProject); // Assert Assert.Equal(2, result.inserts); // 2 new channels added diff --git a/Kepware.Api.Test/ApiClient/ProjectLoadTests.cs b/Kepware.Api.Test/ApiClient/ProjectLoadTests.cs index af3ed55..cd1991c 100644 --- a/Kepware.Api.Test/ApiClient/ProjectLoadTests.cs +++ b/Kepware.Api.Test/ApiClient/ProjectLoadTests.cs @@ -15,92 +15,120 @@ using Kepware.Api.Test.ApiClient; using Kepware.Api.Util; using Shouldly; +using Xunit.Sdk; namespace Kepware.Api.Test.ApiClient { public class ProjectLoadTests : TestApiClientBase { - //private async Task ConfigureToServeEndpoints() - //{ - // var projectData = await LoadJsonTestDataAsync(); - - // var channels = projectData.Project?.Channels?.Select(c => new Channel { Name = c.Name, Description = c.Description, DynamicProperties = c.DynamicProperties }).ToList() ?? []; - - // // Serve project details - // _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project") - // .ReturnsResponse(JsonSerializer.Serialize(new Project { Description = projectData?.Project?.Description, DynamicProperties = projectData?.Project?.DynamicProperties ?? [] }), "application/json"); - - // // Serve channels without nested devices - // _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels") - // .ReturnsResponse(JsonSerializer.Serialize(channels), "application/json"); + [Theory] + [InlineData("KEPServerEX", "12", 6, 17, true)] + [InlineData("KEPServerEX", "12", 6, 16, false)] + [InlineData("ThingWorxKepwareEdge", "13", 1, 10, true)] + [InlineData("ThingWorxKepwareEdge", "13", 1, 9, false)] + [InlineData("UnknownProduct", "99", 10, 0, false)] + public async Task LoadProject_ShouldLoadCorrectly_BasedOnProductSupport( + string productName, string productId, int majorVersion, int minorVersion, bool supportsJsonLoad) + { + // This test will validate that the LoadProjectAsync method correctly loads the project structure and + // content based on whether the connected server version supports JsonProjectLoad. It will compare the loaded project against expected test data to ensure accuracy. + // For servers that support JsonProjectLoad, the test will configure the mock server to serve a full JSON project + // and validate that the loaded project matches the test data exactly. - // foreach (var channel in projectData?.Project?.Channels ?? []) - // { - // _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + $"/config/v1/project/channels/{channel.Name}") - // .ReturnsResponse(JsonSerializer.Serialize(new Channel { Name = channel.Name, Description = channel.Description, DynamicProperties = channel.DynamicProperties }), "application/json"); + // Arrange + ConfigureConnectedClient(productName, productId, majorVersion, minorVersion); - // if (channel.Devices != null) - // { - // var devices = channel.Devices.Select(d => new Device { Name = d.Name, Description = d.Description, DynamicProperties = d.DynamicProperties }).ToList(); - // _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + $"/config/v1/project/channels/{channel.Name}/devices") - // .ReturnsResponse(JsonSerializer.Serialize(devices), "application/json"); + if (supportsJsonLoad) + { + await ConfigureToServeFullProject(); + } + else + { + await ConfigureToServeEndpoints(); + } - // foreach (var device in channel.Devices) - // { - // var deviceEndpoint = TEST_ENDPOINT + $"/config/v1/project/channels/{channel.Name}/devices/{device.Name}"; - // _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, deviceEndpoint) - // .ReturnsResponse(JsonSerializer.Serialize(new Device { Name = device.Name, Description = device.Description, DynamicProperties = device.DynamicProperties }), "application/json"); + // Act + var project = await _kepwareApiClient.Project.LoadProjectAsync(blnLoadFullProject: true); + // Assert + project.IsLoadedByProjectLoadService.ShouldBe(supportsJsonLoad); - // _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, deviceEndpoint + "/tags") - // .ReturnsResponse(JsonSerializer.Serialize(device.Tags), "application/json"); + project.ShouldNotBeNull(); + project.Channels.ShouldNotBeEmpty("Channels list should not be empty."); - // ConfigureToServeEndpointsTagGroupsRecursive(deviceEndpoint, device.TagGroups ?? []); - // } - // } - // } - //} + var testProject = await LoadJsonTestDataAsync(); + var compareResult = EntityCompare.Compare(testProject?.Project?.Channels, project?.Channels); - //private void ConfigureToServeEndpointsTagGroupsRecursive(string endpoint, IEnumerable tagGroups) - //{ - // var tagGroupEndpoint = endpoint + "/tag_groups"; + compareResult.ShouldNotBeNull(); + compareResult.UnchangedItems.ShouldNotBeEmpty("All channels should be unchanged."); + compareResult.ChangedItems.ShouldBeEmpty("No channels should be changed."); + compareResult.ItemsOnlyInLeft.ShouldBeEmpty("No channels should exist only in the test data."); + compareResult.ItemsOnlyInRight.ShouldBeEmpty("No channels should exist only in the loaded project."); - // _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, tagGroupEndpoint) - // .ReturnsResponse(JsonSerializer.Serialize(tagGroups), "application/json"); + foreach (var (ExpectedChannel, LoadedChannel) in testProject?.Project?.Channels?.Zip(project?.Channels ?? []) ?? []) + { + var deviceCompareResult = EntityCompare.Compare(ExpectedChannel.Devices, LoadedChannel.Devices); + deviceCompareResult.ShouldNotBeNull(); + deviceCompareResult.UnchangedItems.ShouldNotBeEmpty($"All devices in channel {ExpectedChannel.Name} should be unchanged."); + deviceCompareResult.ChangedItems.ShouldBeEmpty($"No devices in channel {ExpectedChannel.Name} should be changed."); + deviceCompareResult.ItemsOnlyInLeft.ShouldBeEmpty($"No devices should exist only in the test data for channel {ExpectedChannel.Name}."); + deviceCompareResult.ItemsOnlyInRight.ShouldBeEmpty($"No devices should exist only in the loaded project for channel {ExpectedChannel.Name}."); - // foreach (var tagGroup in tagGroups) - // { - // _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, string.Concat(tagGroupEndpoint, "/", tagGroup.Name, "/tags")) - // .ReturnsResponse(JsonSerializer.Serialize(tagGroup.Tags), "application/json"); + foreach (var (ExpectedDevice, LoadedDevice) in ExpectedChannel.Devices?.Zip(LoadedChannel.Devices ?? []) ?? []) + { + if (ExpectedDevice.Tags?.Count > 0 || LoadedDevice.Tags?.Count > 0) + { + var tagCompareResult = EntityCompare.Compare(ExpectedDevice.Tags, LoadedDevice.Tags); + tagCompareResult.ShouldNotBeNull(); + tagCompareResult.UnchangedItems.ShouldNotBeEmpty($"All tags in device {ExpectedDevice.Name} should be unchanged."); + tagCompareResult.ChangedItems.ShouldBeEmpty($"No tags in device {ExpectedDevice.Name} should be changed."); + tagCompareResult.ItemsOnlyInLeft.ShouldBeEmpty($"No tags should exist only in the test data for device {ExpectedDevice.Name}."); + tagCompareResult.ItemsOnlyInRight.ShouldBeEmpty($"No tags should exist only in the loaded project for device {ExpectedDevice.Name}."); + } - // ConfigureToServeEndpointsTagGroupsRecursive(string.Concat(tagGroupEndpoint, "/", tagGroup.Name), tagGroup.TagGroups ?? []); - // } - //} + CompareTagGroupsRecursive(ExpectedDevice.TagGroups, LoadedDevice.TagGroups, ExpectedDevice.Name); + } + } + } [Theory] [InlineData("KEPServerEX", "12", 6, 17, true)] - [InlineData("KEPServerEX", "12", 6, 16, false)] + [InlineData("ThingWorxKepwareServer", "12", 6, 17, true)] [InlineData("ThingWorxKepwareEdge", "13", 1, 10, true)] - [InlineData("ThingWorxKepwareEdge", "13", 1, 9, false)] - [InlineData("UnknownProduct", "99", 10, 0, false)] - public async Task LoadProject_ShouldLoadCorrectly_BasedOnProductSupport( + [InlineData("Kepware Edge", "13", 1, 0, true)] + public async Task LoadProject_ShouldLoadCorrectly_Serialize_BasedOnProductSupport( string productName, string productId, int majorVersion, int minorVersion, bool supportsJsonLoad) { + // This test will validate that the LoadProjectAsync method correctly loads the project structure using the optimized recursion method. + // It will compare the loaded project against expected test data to ensure accuracy. The test will configure the mock server to serve + // endpoints to support an optimized recursion load and validate that the loaded project matches the test data exactly. + + // Arrange ConfigureConnectedClient(productName, productId, majorVersion, minorVersion); if (supportsJsonLoad) { - await ConfigureToServeFullProject(); + await ConfigureToServeEndpoints(); } else { - await ConfigureToServeEndpoints(); + // Skip this test case at runtime because it expects the server to serve a full JSON project. + throw SkipException.ForSkip($"Product {productName} v{majorVersion}.{minorVersion} (id={productId}) does not support JSON project load. Skipping full-project test case."); } - var project = await _kepwareApiClient.Project.LoadProject(true); + // Override the tag limit to ensure that we are testing the optimized recursion and selectively load objects based on the tag limit. + // See _data/simdemo_en.json and json chunks in _data/projectLoadSerializeData for data that is served by the mock server for this test. + var tagLimitOverride = 100; - project.IsLoadedByProjectLoadService.ShouldBe(supportsJsonLoad); + + // Act + var project = await _kepwareApiClient.Project.LoadProjectAsync(blnLoadFullProject: true, projectLoadTagLimit: tagLimitOverride); + + + // Assert + // Optimized recursion is done for this test, which will result in false. + project.IsLoadedByProjectLoadService.ShouldBeFalse(); project.ShouldNotBeNull(); project.Channels.ShouldNotBeEmpty("Channels list should not be empty."); @@ -138,6 +166,12 @@ public async Task LoadProject_ShouldLoadCorrectly_BasedOnProductSupport( CompareTagGroupsRecursive(ExpectedDevice.TagGroups, LoadedDevice.TagGroups, ExpectedDevice.Name); } } + + // Verify expected number of calls to the project load endpoints to ensure that the optimized recursion is selectively loading objects based on the tag limit. + foreach (var uri in _optimizedRecursionUris) + { + _httpMessageHandlerMock.VerifyRequest(HttpMethod.Get, uri); + } } private static void CompareTagGroupsRecursive(DeviceTagGroupCollection? expected, DeviceTagGroupCollection? actual, string parentName) @@ -182,7 +216,7 @@ public async Task LoadProject_NotFull_ShouldLoadCorrectly_BasedOnProductSupport( await ConfigureToServeEndpoints(); - var project = await _kepwareApiClient.Project.LoadProject(blnLoadFullProject: false); + var project = await _kepwareApiClient.Project.LoadProjectAsync(blnLoadFullProject: false); project.ShouldNotBeNull(); project.Channels.ShouldBeNull("Channels list should be null."); @@ -201,7 +235,7 @@ public async Task LoadProject_ShouldReturnEmptyProject_WhenHttpRequestFails() .ThrowsAsync(new HttpRequestException()); // Act - var project = await _kepwareApiClient.Project.LoadProject(true); + var project = await _kepwareApiClient.Project.LoadProjectAsync(true); // Assert project.ShouldNotBeNull(); diff --git a/Kepware.Api.Test/ApiClient/TestConnection.cs b/Kepware.Api.Test/ApiClient/TestConnectionTests.cs similarity index 100% rename from Kepware.Api.Test/ApiClient/TestConnection.cs rename to Kepware.Api.Test/ApiClient/TestConnectionTests.cs diff --git a/Kepware.Api.Test/ApiClient/_TestApiClientBase.cs b/Kepware.Api.Test/ApiClient/_TestApiClientBase.cs index eb3133e..2b445e6 100644 --- a/Kepware.Api.Test/ApiClient/_TestApiClientBase.cs +++ b/Kepware.Api.Test/ApiClient/_TestApiClientBase.cs @@ -25,9 +25,15 @@ public abstract class TestApiClientBase protected readonly Mock> _loggerMockAdmin; protected readonly Mock> _loggerMockProject; protected readonly Mock> _loggerMockGeneric; + protected readonly Mock> _loggerMockIotGateway; protected readonly Mock _loggerFactoryMock; protected readonly KepwareApiClient _kepwareApiClient; + /// + /// URIs used to optimize recursion endpoints for tests. Populated via . + /// + protected readonly List _optimizedRecursionUris = new List(); + protected TestApiClientBase() { _httpMessageHandlerMock = new Mock(); @@ -35,6 +41,7 @@ protected TestApiClientBase() _loggerMockAdmin = new Mock>(); _loggerMockGeneric = new Mock>(); _loggerMockProject = new Mock>(); + _loggerMockIotGateway = new Mock>(); _loggerFactoryMock = new Mock(); _loggerFactoryMock.Setup(factory => factory.CreateLogger(It.IsAny())).Returns((string name) => @@ -47,6 +54,8 @@ protected TestApiClientBase() return _loggerMockGeneric.Object; else if (name == typeof(ProjectApiHandler).FullName) return _loggerMockProject.Object; + else if (name == typeof(IotGatewayApiHandler).FullName) + return _loggerMockIotGateway.Object; else return Mock.Of(); }); @@ -146,7 +155,22 @@ protected async Task ConfigureToServeEndpoints(string filePath = "_data/simdemo_ { var projectData = await LoadJsonTestDataAsync(filePath); - var channels = projectData.Project?.Channels?.Select(c => new Channel { Name = c.Name, Description = c.Description, DynamicProperties = c.DynamicProperties }).ToList() ?? []; + + var channels = projectData.Project?.Channels? + .Select(c => + { + var ch = new Channel { Name = c.Name, Description = c.Description, DynamicProperties = c.DynamicProperties }; + int staticCount = c.Name switch + { + "Channel1" => 2, + "Simulation Examples" => 24, + "Data Type Examples" => 216, + "OptRecursionTest" => 318, + _ => 0 + }; + ch.SetDynamicProperty(Properties.Channel.StaticTagCount, staticCount); + return ch; + }).ToList() ?? new List(); // Serve project details _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project") @@ -163,13 +187,30 @@ protected async Task ConfigureToServeEndpoints(string filePath = "_data/simdemo_ if (channel.Devices != null) { - var devices = channel.Devices.Select(d => new Device { Name = d.Name, Description = d.Description, DynamicProperties = d.DynamicProperties }).ToList(); + var devices = channel.Devices + .Select(d => + { + var dev = new Device { Name = d.Name, Description = d.Description, DynamicProperties = d.DynamicProperties }; + int staticCount = d.Name switch + { + "Device1" => 2, + "Functions" => 24, + "16 Bit Device" => 98, + "8 Bit Device" => 118, + "RecursionTestDevice" => 318, + _ => 0 + }; + dev.SetDynamicProperty(Properties.Device.StaticTagCount, staticCount); + return dev; + }).ToList() ?? new List(); _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + $"/config/v1/project/channels/{channel.Name}/devices") .ReturnsResponse(JsonSerializer.Serialize(devices), "application/json"); foreach (var device in channel.Devices) { var deviceEndpoint = TEST_ENDPOINT + $"/config/v1/project/channels/{channel.Name}/devices/{device.Name}"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, deviceEndpoint) .ReturnsResponse(JsonSerializer.Serialize(new Device { Name = device.Name, Description = device.Description, DynamicProperties = device.DynamicProperties }), "application/json"); @@ -181,15 +222,120 @@ protected async Task ConfigureToServeEndpoints(string filePath = "_data/simdemo_ } } } + + // Additional endpoints for content=serialize mocking + var projectPropertiesString = await File.ReadAllTextAsync("_data/projectLoadSerializeData/projectProperties.json"); + var channel1String = await File.ReadAllTextAsync("_data/projectLoadSerializeData/Channel1.json"); + var sixteenBitDeviceString = await File.ReadAllTextAsync("_data/projectLoadSerializeData/dataTypeExamples.16bitDevice.json"); + var simExamplesChannelString = await File.ReadAllTextAsync("_data/projectLoadSerializeData/simulationExamples.json"); + var dte8BitBRegTagGroupString = await File.ReadAllTextAsync("_data/projectLoadSerializeData/dte.8bitDevice.Breg.json"); + var dte8BitKRegTagGroupString = await File.ReadAllTextAsync("_data/projectLoadSerializeData/dte.8bitDevice.Kreg.json"); + var dte8BitRRegTagGroupString = await File.ReadAllTextAsync("_data/projectLoadSerializeData/dte.8bitDevice.Rreg.json"); + var dte8BitSRegTagGroupString = await File.ReadAllTextAsync("_data/projectLoadSerializeData/dte.8bitDevice.Sreg.json"); + var optRecursionDeviceBRegTagGroupString = await File.ReadAllTextAsync("_data/projectLoadSerializeData/opt.recursionDevice.Breg.json"); + var optRecursionDeviceKRegTagGroupString = await File.ReadAllTextAsync("_data/projectLoadSerializeData/opt.recursionDevice.Kreg.json"); + var optRecursionDeviceRRegTagGroupString = await File.ReadAllTextAsync("_data/projectLoadSerializeData/opt.recursionDevice.Rreg.json"); + var optRecursionDeviceSRegTagGroupString = await File.ReadAllTextAsync("_data/projectLoadSerializeData/opt.recursionDevice.Sreg.json"); + var optRecursionDeviceRecursionTestLevel1String = await File.ReadAllTextAsync("_data/projectLoadSerializeData/opt.recursionDevice.RecursionTest.Level1.json"); + var optRecursionDeviceRecursionTestLevel2String = await File.ReadAllTextAsync("_data/projectLoadSerializeData/opt.recursionDevice.RecursionTest.Level2.json"); + var optRecursionDeviceRecursionTestLevel3String = await File.ReadAllTextAsync("_data/projectLoadSerializeData/opt.recursionDevice.RecursionTest.Level3.json"); + var optRecursionDeviceRecursionTestLevel4String = await File.ReadAllTextAsync("_data/projectLoadSerializeData/opt.recursionDevice.RecursionTest.Level4.json"); + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project") + .ReturnsResponse(projectPropertiesString, "application/json"); + _optimizedRecursionUris.Add(TEST_ENDPOINT + "/config/v1/project"); + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels/Channel1?content=serialize") + .ReturnsResponse(channel1String, "application/json"); + _optimizedRecursionUris.Add(TEST_ENDPOINT + "/config/v1/project/channels/Channel1?content=serialize"); + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels/Data Type Examples/devices/16 Bit Device?content=serialize") + .ReturnsResponse(sixteenBitDeviceString, "application/json"); + _optimizedRecursionUris.Add(TEST_ENDPOINT + "/config/v1/project/channels/Data Type Examples/devices/16 Bit Device?content=serialize"); + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels/Simulation Examples?content=serialize") + .ReturnsResponse(simExamplesChannelString, "application/json"); + _optimizedRecursionUris.Add(TEST_ENDPOINT + "/config/v1/project/channels/Simulation Examples?content=serialize"); + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels/Data Type Examples/devices/8 Bit Device/tag_groups/B Registers?content=serialize") + .ReturnsResponse(dte8BitBRegTagGroupString, "application/json"); + _optimizedRecursionUris.Add(TEST_ENDPOINT + "/config/v1/project/channels/Data Type Examples/devices/8 Bit Device/tag_groups/B Registers?content=serialize"); + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels/Data Type Examples/devices/8 Bit Device/tag_groups/K Registers?content=serialize") + .ReturnsResponse(dte8BitKRegTagGroupString, "application/json"); + _optimizedRecursionUris.Add(TEST_ENDPOINT + "/config/v1/project/channels/Data Type Examples/devices/8 Bit Device/tag_groups/K Registers?content=serialize"); + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels/Data Type Examples/devices/8 Bit Device/tag_groups/R Registers?content=serialize") + .ReturnsResponse(dte8BitRRegTagGroupString, "application/json"); + _optimizedRecursionUris.Add(TEST_ENDPOINT + "/config/v1/project/channels/Data Type Examples/devices/8 Bit Device/tag_groups/R Registers?content=serialize"); + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels/Data Type Examples/devices/8 Bit Device/tag_groups/S Registers?content=serialize") + .ReturnsResponse(dte8BitSRegTagGroupString, "application/json"); + _optimizedRecursionUris.Add(TEST_ENDPOINT + "/config/v1/project/channels/Data Type Examples/devices/8 Bit Device/tag_groups/S Registers?content=serialize"); + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels/OptRecursionTest/devices/RecursionTestDevice/tag_groups/B Registers?content=serialize") + .ReturnsResponse(optRecursionDeviceBRegTagGroupString, "application/json"); + _optimizedRecursionUris.Add(TEST_ENDPOINT + "/config/v1/project/channels/OptRecursionTest/devices/RecursionTestDevice/tag_groups/B Registers?content=serialize"); + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels/OptRecursionTest/devices/RecursionTestDevice/tag_groups/K Registers?content=serialize") + .ReturnsResponse(optRecursionDeviceKRegTagGroupString, "application/json"); + _optimizedRecursionUris.Add(TEST_ENDPOINT + "/config/v1/project/channels/OptRecursionTest/devices/RecursionTestDevice/tag_groups/K Registers?content=serialize"); + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels/OptRecursionTest/devices/RecursionTestDevice/tag_groups/R Registers?content=serialize") + .ReturnsResponse(optRecursionDeviceRRegTagGroupString, "application/json"); + _optimizedRecursionUris.Add(TEST_ENDPOINT + "/config/v1/project/channels/OptRecursionTest/devices/RecursionTestDevice/tag_groups/R Registers?content=serialize"); + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels/OptRecursionTest/devices/RecursionTestDevice/tag_groups/S Registers?content=serialize") + .ReturnsResponse(optRecursionDeviceSRegTagGroupString, "application/json"); + _optimizedRecursionUris.Add(TEST_ENDPOINT + "/config/v1/project/channels/OptRecursionTest/devices/RecursionTestDevice/tag_groups/S Registers?content=serialize"); + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels/OptRecursionTest/devices/RecursionTestDevice/tag_groups/RecursionTest/tag_groups/Level1?content=serialize") + .ReturnsResponse(optRecursionDeviceRecursionTestLevel1String, "application/json"); + _optimizedRecursionUris.Add(TEST_ENDPOINT + "/config/v1/project/channels/OptRecursionTest/devices/RecursionTestDevice/tag_groups/RecursionTest/tag_groups/Level1?content=serialize"); + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels/OptRecursionTest/devices/RecursionTestDevice/tag_groups/RecursionTest/tag_groups/Level2?content=serialize") + .ReturnsResponse(optRecursionDeviceRecursionTestLevel2String, "application/json"); + _optimizedRecursionUris.Add(TEST_ENDPOINT + "/config/v1/project/channels/OptRecursionTest/devices/RecursionTestDevice/tag_groups/RecursionTest/tag_groups/Level2?content=serialize"); + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels/OptRecursionTest/devices/RecursionTestDevice/tag_groups/RecursionTest/tag_groups/Level3?content=serialize") + .ReturnsResponse(optRecursionDeviceRecursionTestLevel3String, "application/json"); + _optimizedRecursionUris.Add(TEST_ENDPOINT + "/config/v1/project/channels/OptRecursionTest/devices/RecursionTestDevice/tag_groups/RecursionTest/tag_groups/Level3?content=serialize"); + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels/OptRecursionTest/devices/RecursionTestDevice/tag_groups/RecursionTest/tag_groups/Level4?content=serialize") + .ReturnsResponse(optRecursionDeviceRecursionTestLevel4String, "application/json"); + _optimizedRecursionUris.Add(TEST_ENDPOINT + "/config/v1/project/channels/OptRecursionTest/devices/RecursionTestDevice/tag_groups/RecursionTest/tag_groups/Level4?content=serialize"); + } private void ConfigureToServeEndpointsTagGroupsRecursive(string endpoint, IEnumerable tagGroups) { var tagGroupEndpoint = endpoint + "/tag_groups"; + var updatedTagGroups = tagGroups + .Select(tg => + { + var tagGrp = tg; + int staticCount = tg.Name switch + { + "B Registers" => 5, + "K Registers" => 54, + "R Registers" => 54, + "S Registers" => 5, + "RecursionTest" => 220, + "Level1" => 88, + "Level2" => 44, + "Level3" => 44, + "Level4" => 44, + "Level1_1" => 44, + _ => 0 + }; + tagGrp.SetDynamicProperty(Properties.DeviceTagGroup.TotalTagCount, staticCount); + return tagGrp; + }).ToList() ?? new List(); + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, tagGroupEndpoint) - .ReturnsResponse(JsonSerializer.Serialize(tagGroups), "application/json"); + .ReturnsResponse(JsonSerializer.Serialize(updatedTagGroups), "application/json"); - foreach (var tagGroup in tagGroups) + foreach (var tagGroup in updatedTagGroups) { _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, string.Concat(tagGroupEndpoint, "/", tagGroup.Name, "/tags")) .ReturnsResponse(JsonSerializer.Serialize(tagGroup.Tags), "application/json"); diff --git a/Kepware.Api.Test/Kepware.Api.Test.csproj b/Kepware.Api.Test/Kepware.Api.Test.csproj index a9e2f77..9827459 100644 --- a/Kepware.Api.Test/Kepware.Api.Test.csproj +++ b/Kepware.Api.Test/Kepware.Api.Test.csproj @@ -34,9 +34,9 @@ - + PreserveNewest - + diff --git a/Kepware.Api.Test/Model/IotGateway/IotGatewayModelTests.cs b/Kepware.Api.Test/Model/IotGateway/IotGatewayModelTests.cs new file mode 100644 index 0000000..3807cf6 --- /dev/null +++ b/Kepware.Api.Test/Model/IotGateway/IotGatewayModelTests.cs @@ -0,0 +1,496 @@ +using Kepware.Api.Model; +using Kepware.Api.Serializer; +using System.Text.Json; + +namespace Kepware.Api.Test.Model.IotGateway +{ + public class IotGatewayModelTests + { + #region MqttClientAgent Tests + + [Fact] + public void MqttClientAgent_Deserialize_ShouldPopulateProperties() + { + var json = """ + { + "common.ALLTYPES_NAME": "TestMqttAgent", + "common.ALLTYPES_DESCRIPTION": "A test MQTT client agent", + "iot_gateway.AGENTTYPES_TYPE": "MQTT Client", + "iot_gateway.AGENTTYPES_ENABLED": true, + "iot_gateway.IGNORE_QUALITY_CHANGES": false, + "iot_gateway.MQTT_CLIENT_URL": "tcp://localhost:1883", + "iot_gateway.MQTT_CLIENT_TOPIC": "iotgateway", + "iot_gateway.MQTT_CLIENT_QOS": 1, + "iot_gateway.AGENTTYPES_PUBLISH_TYPE": 0, + "iot_gateway.AGENTTYPES_RATE_MS": 10000, + "iot_gateway.AGENTTYPES_PUBLISH_FORMAT": 0, + "iot_gateway.AGENTTYPES_MAX_EVENTS": 1000, + "iot_gateway.AGENTTYPES_TIMEOUT_S": 5, + "iot_gateway.AGENTTYPES_MESSAGE_FORMAT": 0, + "iot_gateway.AGENTTYPES_SEND_INITIAL_UPDATE": true, + "iot_gateway.MQTT_CLIENT_ENABLE_LAST_WILL": false, + "iot_gateway.MQTT_CLIENT_ENABLE_WRITE_TOPIC": false, + "iot_gateway.MQTT_TLS_VERSION": 0, + "iot_gateway.MQTT_CLIENT_CERTIFICATE": false + } + """; + + var agent = JsonSerializer.Deserialize(json, KepJsonContext.Default.MqttClientAgent); + + Assert.NotNull(agent); + Assert.Equal("TestMqttAgent", agent.Name); + Assert.Equal("A test MQTT client agent", agent.Description); + Assert.Equal("MQTT Client", agent.AgentType); + Assert.True(agent.Enabled); + Assert.False(agent.IgnoreQualityChanges); + Assert.Equal("tcp://localhost:1883", agent.Url); + Assert.Equal("iotgateway", agent.Topic); + Assert.Equal(MqttQos.AtLeastOnce, agent.Qos); + Assert.Equal(IotPublishType.Interval, agent.PublishType); + Assert.Equal(10000, agent.RateMs); + Assert.Equal(IotPublishFormat.NarrowFormat, agent.PublishFormat); + Assert.Equal(1000, agent.MaxEventsPerPublish); + Assert.Equal(5, agent.TransactionTimeoutS); + Assert.Equal(IotMessageFormat.StandardTemplate, agent.MessageFormat); + Assert.True(agent.SendInitialUpdate); + Assert.False(agent.EnableLastWill); + Assert.False(agent.EnableWriteTopic); + Assert.Equal(MqttTlsVersion.Default, agent.TlsVersion); + + Assert.False(agent.ClientCertificate); + } + + [Fact] + public void MqttClientAgent_SetProperties_ShouldUpdateDynamicProperties() + { + var agent = new MqttClientAgent("TestAgent"); + + agent.Url = "tcp://broker:1883"; + agent.Topic = "test/topic"; + agent.Qos = MqttQos.ExactlyOnce; + agent.Enabled = true; + agent.PublishType = IotPublishType.OnDataChange; + agent.RateMs = 5000; + agent.TlsVersion = MqttTlsVersion.V1_2; + agent.EnableLastWill = true; + agent.LastWillTopic = "lwt/topic"; + agent.LastWillMessage = "offline"; + agent.EnableWriteTopic = true; + agent.WriteTopic = "write/topic"; + + Assert.Equal("tcp://broker:1883", agent.Url); + Assert.Equal("test/topic", agent.Topic); + Assert.Equal(MqttQos.ExactlyOnce, agent.Qos); + Assert.True(agent.Enabled); + Assert.Equal(IotPublishType.OnDataChange, agent.PublishType); + Assert.Equal(5000, agent.RateMs); + Assert.Equal(MqttTlsVersion.V1_2, agent.TlsVersion); + Assert.True(agent.EnableLastWill); + Assert.Equal("lwt/topic", agent.LastWillTopic); + Assert.Equal("offline", agent.LastWillMessage); + Assert.True(agent.EnableWriteTopic); + Assert.Equal("write/topic", agent.WriteTopic); + } + + [Fact] + public void MqttClientAgent_RoundTrip_ShouldPreserveProperties() + { + var agent = new MqttClientAgent("RoundTripAgent"); + agent.Enabled = true; + agent.Url = "tcp://localhost:1883"; + agent.Topic = "test"; + agent.Qos = MqttQos.AtLeastOnce; + agent.PublishType = IotPublishType.Interval; + agent.RateMs = 10000; + + var json = JsonSerializer.Serialize(agent, KepJsonContext.Default.MqttClientAgent); + var deserialized = JsonSerializer.Deserialize(json, KepJsonContext.Default.MqttClientAgent); + + Assert.NotNull(deserialized); + Assert.Equal("RoundTripAgent", deserialized.Name); + Assert.True(deserialized.Enabled); + Assert.Equal("tcp://localhost:1883", deserialized.Url); + Assert.Equal("test", deserialized.Topic); + Assert.Equal(MqttQos.AtLeastOnce, deserialized.Qos); + Assert.Equal(IotPublishType.Interval, deserialized.PublishType); + Assert.Equal(10000, deserialized.RateMs); + } + + [Fact] + public void MqttClientAgent_WithIotItems_ShouldDeserializeChildren() + { + var json = """ + { + "common.ALLTYPES_NAME": "AgentWithItems", + "iot_gateway.AGENTTYPES_ENABLED": true, + "iot_items": [ + { + "common.ALLTYPES_NAME": "Item1", + "iot_gateway.IOT_ITEM_SERVER_TAG": "Channel1.Device1.Tag1", + "iot_gateway.IOT_ITEM_ENABLED": true, + "iot_gateway.IOT_ITEM_SCAN_RATE_MS": 1000 + } + ] + } + """; + + var agent = JsonSerializer.Deserialize(json, KepJsonContext.Default.MqttClientAgent); + + Assert.NotNull(agent); + Assert.NotNull(agent.IotItems); + Assert.Single(agent.IotItems); + Assert.Equal("Item1", agent.IotItems[0].Name); + Assert.Equal("Channel1.Device1.Tag1", agent.IotItems[0].ServerTag); + Assert.True(agent.IotItems[0].Enabled); + Assert.Equal(1000, agent.IotItems[0].ScanRateMs); + } + + #endregion + + #region RestClientAgent Tests + + [Fact] + public void RestClientAgent_Deserialize_ShouldPopulateProperties() + { + var json = """ + { + "common.ALLTYPES_NAME": "TestRestClient", + "iot_gateway.AGENTTYPES_TYPE": "REST Client", + "iot_gateway.AGENTTYPES_ENABLED": true, + "iot_gateway.REST_CLIENT_URL": "http://127.0.0.1:3000", + "iot_gateway.REST_CLIENT_METHOD": 0, + "iot_gateway.AGENTTYPES_PUBLISH_TYPE": 1, + "iot_gateway.AGENTTYPES_RATE_MS": 5000, + "iot_gateway.AGENTTYPES_PUBLISH_FORMAT": 1, + "iot_gateway.AGENTTYPES_MESSAGE_FORMAT": 1, + "iot_gateway.REST_CLIENT_PUBLISH_MEDIA_TYPE": 0, + "iot_gateway.BUFFER_ON_FAILED_PUBLISH": true, + "iot_gateway.AGENTTYPES_SEND_INITIAL_UPDATE": true + } + """; + + var agent = JsonSerializer.Deserialize(json, KepJsonContext.Default.RestClientAgent); + + Assert.NotNull(agent); + Assert.Equal("TestRestClient", agent.Name); + Assert.Equal("REST Client", agent.AgentType); + Assert.True(agent.Enabled); + Assert.Equal("http://127.0.0.1:3000", agent.Url); + Assert.Equal(RestClientHttpMethod.Post, agent.HttpMethod); + Assert.Equal(IotPublishType.OnDataChange, agent.PublishType); + Assert.Equal(5000, agent.RateMs); + Assert.Equal(IotPublishFormat.WideFormat, agent.PublishFormat); + Assert.Equal(IotMessageFormat.AdvancedTemplate, agent.MessageFormat); + Assert.Equal(RestClientMediaType.ApplicationJson, agent.PublishMediaType); + Assert.True(agent.BufferOnFailedPublish); + Assert.True(agent.SendInitialUpdate); + } + + [Fact] + public void RestClientAgent_SetProperties_ShouldUpdateDynamicProperties() + { + var agent = new RestClientAgent("TestAgent"); + + agent.Url = "https://api.example.com"; + agent.HttpMethod = RestClientHttpMethod.Put; + agent.HttpHeader = "Authorization: Bearer token123"; + agent.PublishMediaType = RestClientMediaType.TextPlain; + agent.Username = "user"; + agent.Password = "pass"; + agent.BufferOnFailedPublish = false; + + Assert.Equal("https://api.example.com", agent.Url); + Assert.Equal(RestClientHttpMethod.Put, agent.HttpMethod); + Assert.Equal("Authorization: Bearer token123", agent.HttpHeader); + Assert.Equal(RestClientMediaType.TextPlain, agent.PublishMediaType); + Assert.Equal("user", agent.Username); + Assert.Equal("pass", agent.Password); + Assert.False(agent.BufferOnFailedPublish); + } + + [Fact] + public void RestClientAgent_RoundTrip_ShouldPreserveProperties() + { + var agent = new RestClientAgent("RoundTripRestClient"); + agent.Enabled = true; + agent.Url = "https://api.example.com"; + agent.HttpMethod = RestClientHttpMethod.Put; + agent.PublishType = IotPublishType.OnDataChange; + agent.RateMs = 5000; + agent.PublishFormat = IotPublishFormat.WideFormat; + agent.BufferOnFailedPublish = true; + + var json = JsonSerializer.Serialize(agent, KepJsonContext.Default.RestClientAgent); + var deserialized = JsonSerializer.Deserialize(json, KepJsonContext.Default.RestClientAgent); + + Assert.NotNull(deserialized); + Assert.Equal("RoundTripRestClient", deserialized.Name); + Assert.True(deserialized.Enabled); + Assert.Equal("https://api.example.com", deserialized.Url); + Assert.Equal(RestClientHttpMethod.Put, deserialized.HttpMethod); + Assert.Equal(IotPublishType.OnDataChange, deserialized.PublishType); + Assert.Equal(5000, deserialized.RateMs); + Assert.Equal(IotPublishFormat.WideFormat, deserialized.PublishFormat); + Assert.True(deserialized.BufferOnFailedPublish); + } + + #endregion + + #region RestServerAgent Tests + + [Fact] + public void RestServerAgent_Deserialize_ShouldPopulateProperties() + { + var json = """ + { + "common.ALLTYPES_NAME": "TestRestServer", + "iot_gateway.AGENTTYPES_TYPE": "REST Server", + "iot_gateway.AGENTTYPES_ENABLED": true, + "iot_gateway.IGNORE_QUALITY_CHANGES": false, + "iot_gateway.REST_SERVER_PORT_NUMBER": 39320, + "iot_gateway.REST_SERVER_USE_HTTPS": true, + "iot_gateway.REST_SERVER_ENABLE_WRITE_ENDPOINT": false, + "iot_gateway.REST_SERVER_ALLOW_ANONYMOUS_LOGIN": false + } + """; + + var agent = JsonSerializer.Deserialize(json, KepJsonContext.Default.RestServerAgent); + + Assert.NotNull(agent); + Assert.Equal("TestRestServer", agent.Name); + Assert.Equal("REST Server", agent.AgentType); + Assert.True(agent.Enabled); + Assert.False(agent.IgnoreQualityChanges); + Assert.Equal(39320, agent.PortNumber); + Assert.True(agent.UseHttps); + Assert.False(agent.EnableWriteEndpoint); + Assert.False(agent.AllowAnonymousLogin); + } + + [Fact] + public void RestServerAgent_ShouldNotHavePublishProperties() + { + // Verify that RestServerAgent inherits from IotAgent directly, not PublishingIotAgent + Assert.False(typeof(RestServerAgent).IsSubclassOf(typeof(PublishingIotAgent))); + Assert.True(typeof(RestServerAgent).IsSubclassOf(typeof(IotAgent))); + } + + [Fact] + public void RestServerAgent_SetProperties_ShouldUpdateDynamicProperties() + { + var agent = new RestServerAgent("TestServer"); + + agent.PortNumber = 8080; + agent.UseHttps = false; + agent.EnableWriteEndpoint = true; + agent.AllowAnonymousLogin = true; + agent.CorsAllowedOrigins = "http://localhost:3000,http://example.com"; + agent.Enabled = true; + + Assert.Equal(8080, agent.PortNumber); + Assert.False(agent.UseHttps); + Assert.True(agent.EnableWriteEndpoint); + Assert.True(agent.AllowAnonymousLogin); + Assert.Equal("http://localhost:3000,http://example.com", agent.CorsAllowedOrigins); + Assert.True(agent.Enabled); + } + + [Fact] + public void RestServerAgent_RoundTrip_ShouldPreserveProperties() + { + var agent = new RestServerAgent("RoundTripRestServer"); + agent.Enabled = true; + agent.PortNumber = 8080; + agent.UseHttps = false; + agent.EnableWriteEndpoint = true; + agent.AllowAnonymousLogin = true; + agent.CorsAllowedOrigins = "http://localhost:3000"; + + var json = JsonSerializer.Serialize(agent, KepJsonContext.Default.RestServerAgent); + var deserialized = JsonSerializer.Deserialize(json, KepJsonContext.Default.RestServerAgent); + + Assert.NotNull(deserialized); + Assert.Equal("RoundTripRestServer", deserialized.Name); + Assert.True(deserialized.Enabled); + Assert.Equal(8080, deserialized.PortNumber); + Assert.False(deserialized.UseHttps); + Assert.True(deserialized.EnableWriteEndpoint); + Assert.True(deserialized.AllowAnonymousLogin); + Assert.Equal("http://localhost:3000", deserialized.CorsAllowedOrigins); + } + + #endregion + + #region IotItem Tests + + [Fact] + public void IotItem_Deserialize_ShouldPopulateProperties() + { + var json = """ + { + "common.ALLTYPES_NAME": "TestItem", + "common.ALLTYPES_DESCRIPTION": "A test IoT item", + "iot_gateway.IOT_ITEM_SERVER_TAG": "Channel1.Device1.Tag1", + "iot_gateway.IOT_ITEM_USE_SCAN_RATE": true, + "iot_gateway.IOT_ITEM_SCAN_RATE_MS": 500, + "iot_gateway.IOT_ITEM_SEND_EVERY_SCAN": false, + "iot_gateway.IOT_ITEM_DEADBAND_PERCENT": 2.5, + "iot_gateway.IOT_ITEM_ENABLED": true, + "iot_gateway.IOT_ITEM_DATA_TYPE": -1 + } + """; + + var item = JsonSerializer.Deserialize(json, KepJsonContext.Default.IotItem); + + Assert.NotNull(item); + Assert.Equal("TestItem", item.Name); + Assert.Equal("A test IoT item", item.Description); + Assert.Equal("Channel1.Device1.Tag1", item.ServerTag); + Assert.True(item.UseScanRate); + Assert.Equal(500, item.ScanRateMs); + Assert.False(item.PublishEveryScan); + Assert.Equal(2.5, item.DeadbandPercent); + Assert.True(item.Enabled); + Assert.Equal(IotItemDataType.Default, item.DataType); + } + + [Fact] + public void IotItem_SetProperties_ShouldUpdateDynamicProperties() + { + var item = new IotItem("Item1"); + + item.ServerTag = "Channel1.Device1.Tag2"; + item.UseScanRate = false; + item.ScanRateMs = 2000; + item.PublishEveryScan = true; + item.DeadbandPercent = 5.0; + item.Enabled = false; + item.DataType = IotItemDataType.Float; + + Assert.Equal("Channel1.Device1.Tag2", item.ServerTag); + Assert.False(item.UseScanRate); + Assert.Equal(2000, item.ScanRateMs); + Assert.True(item.PublishEveryScan); + Assert.Equal(5.0, item.DeadbandPercent); + Assert.False(item.Enabled); + Assert.Equal(IotItemDataType.Float, item.DataType); + } + + [Fact] + public void IotItem_RoundTrip_ShouldPreserveProperties() + { + var item = new IotItem("RoundTripItem"); + item.ServerTag = "Ch1.Dev1.Tag1"; + item.Enabled = true; + item.ScanRateMs = 1000; + item.DataType = IotItemDataType.Double; + + var json = JsonSerializer.Serialize(item, KepJsonContext.Default.IotItem); + var deserialized = JsonSerializer.Deserialize(json, KepJsonContext.Default.IotItem); + + Assert.NotNull(deserialized); + Assert.Equal("RoundTripItem", deserialized.Name); + Assert.Equal("Ch1.Dev1.Tag1", deserialized.ServerTag); + Assert.True(deserialized.Enabled); + Assert.Equal(1000, deserialized.ScanRateMs); + Assert.Equal(IotItemDataType.Double, deserialized.DataType); + } + + #endregion + + #region Inheritance Tests + + [Fact] + public void MqttClientAgent_ShouldInheritFromPublishingIotAgent() + { + Assert.True(typeof(MqttClientAgent).IsSubclassOf(typeof(PublishingIotAgent))); + Assert.True(typeof(MqttClientAgent).IsSubclassOf(typeof(IotAgent))); + Assert.True(typeof(MqttClientAgent).IsSubclassOf(typeof(NamedEntity))); + } + + [Fact] + public void RestClientAgent_ShouldInheritFromPublishingIotAgent() + { + Assert.True(typeof(RestClientAgent).IsSubclassOf(typeof(PublishingIotAgent))); + Assert.True(typeof(RestClientAgent).IsSubclassOf(typeof(IotAgent))); + Assert.True(typeof(RestClientAgent).IsSubclassOf(typeof(NamedEntity))); + } + + [Fact] + public void RestServerAgent_ShouldInheritFromIotAgentDirectly() + { + Assert.True(typeof(RestServerAgent).IsSubclassOf(typeof(IotAgent))); + Assert.True(typeof(RestServerAgent).IsSubclassOf(typeof(NamedEntity))); + Assert.False(typeof(RestServerAgent).IsSubclassOf(typeof(PublishingIotAgent))); + } + + [Fact] + public void IotItem_ShouldInheritFromNamedEntity() + { + Assert.True(typeof(IotItem).IsSubclassOf(typeof(NamedEntity))); + Assert.False(typeof(IotItem).IsSubclassOf(typeof(IotAgent))); + } + + #endregion + + #region Collection Deserialization Tests + + [Fact] + public void MqttClientAgentCollection_Deserialize_ShouldWork() + { + var json = """ + [ + { + "common.ALLTYPES_NAME": "Agent1", + "iot_gateway.AGENTTYPES_ENABLED": true, + "iot_gateway.MQTT_CLIENT_URL": "tcp://localhost:1883" + }, + { + "common.ALLTYPES_NAME": "Agent2", + "iot_gateway.AGENTTYPES_ENABLED": false, + "iot_gateway.MQTT_CLIENT_URL": "tcp://broker:1883" + } + ] + """; + + var agents = JsonSerializer.Deserialize>(json, KepJsonContext.Default.ListMqttClientAgent); + + Assert.NotNull(agents); + Assert.Equal(2, agents.Count); + Assert.Equal("Agent1", agents[0].Name); + Assert.Equal("Agent2", agents[1].Name); + Assert.True(agents[0].Enabled); + Assert.False(agents[1].Enabled); + } + + [Fact] + public void IotItemCollection_Deserialize_ShouldWork() + { + var json = """ + [ + { + "common.ALLTYPES_NAME": "Item1", + "iot_gateway.IOT_ITEM_SERVER_TAG": "Ch1.Dev1.Tag1", + "iot_gateway.IOT_ITEM_ENABLED": true + }, + { + "common.ALLTYPES_NAME": "Item2", + "iot_gateway.IOT_ITEM_SERVER_TAG": "Ch1.Dev1.Tag2", + "iot_gateway.IOT_ITEM_ENABLED": false + } + ] + """; + + var items = JsonSerializer.Deserialize>(json, KepJsonContext.Default.ListIotItem); + + Assert.NotNull(items); + Assert.Equal(2, items.Count); + Assert.Equal("Item1", items[0].Name); + Assert.Equal("Item2", items[1].Name); + Assert.True(items[0].Enabled); + Assert.False(items[1].Enabled); + } + + #endregion + } +} diff --git a/Kepware.Api.Test/Model/IotGateway/IotGatewayProjectIntegrationTests.cs b/Kepware.Api.Test/Model/IotGateway/IotGatewayProjectIntegrationTests.cs new file mode 100644 index 0000000..1ca6328 --- /dev/null +++ b/Kepware.Api.Test/Model/IotGateway/IotGatewayProjectIntegrationTests.cs @@ -0,0 +1,465 @@ +using System.Text.Json; +using Kepware.Api.Model; +using Kepware.Api.Serializer; +using Xunit; + +namespace Kepware.Api.Test.Model.IotGateway +{ + public class IotGatewayProjectIntegrationTests + { + #region Project Deserialization + + [Fact] + public void ProjectWithIotGateway_ShouldDeserializeFromJson() + { + var json = @"{ + ""PROJECT_ID"": 42, + ""common.ALLTYPES_DESCRIPTION"": ""Test project"", + ""_iot_gateway"": [ + { + ""common.ALLTYPES_NAME"": ""_IoT_Gateway"", + ""mqtt_clients"": [ + { + ""common.ALLTYPES_NAME"": ""MqttAgent1"", + ""common.ALLTYPES_DESCRIPTION"": ""Test MQTT agent"", + ""iot_gateway.AGENTTYPES_ENABLED"": true, + ""iot_gateway.MQTT_CLIENT_URL"": ""tcp://broker:1883"", + ""iot_items"": [ + { + ""common.ALLTYPES_NAME"": ""Item1"", + ""iot_gateway.IOT_ITEM_SERVER_TAG"": ""Channel1.Device1.Tag1"", + ""iot_gateway.IOT_ITEM_ENABLED"": true + }, + { + ""common.ALLTYPES_NAME"": ""_MqttItem2"", + ""iot_gateway.IOT_ITEM_SERVER_TAG"": ""Channel1.Device1.Tag5"", + ""iot_gateway.IOT_ITEM_ENABLED"": false + } + ] + } + ], + ""rest_clients"": [ + { + ""common.ALLTYPES_NAME"": ""RestClient1"", + ""iot_gateway.AGENTTYPES_ENABLED"": true, + ""iot_gateway.REST_CLIENT_URL"": ""https://api.example.com"", + ""iot_items"": [ + { + ""common.ALLTYPES_NAME"": ""_RestClientItem1"", + ""iot_gateway.IOT_ITEM_SERVER_TAG"": ""Channel1.Device1.Tag2"", + ""iot_gateway.IOT_ITEM_ENABLED"": true + }, + { + ""common.ALLTYPES_NAME"": ""RestClientItem2"", + ""iot_gateway.IOT_ITEM_SERVER_TAG"": ""Channel1.Device1.Tag3"", + ""iot_gateway.IOT_ITEM_ENABLED"": false + } + ] + } + ], + ""rest_servers"": [ + { + ""common.ALLTYPES_NAME"": ""RestServer1"", + ""iot_gateway.AGENTTYPES_ENABLED"": false, + ""iot_gateway.REST_SERVER_PORT_NUMBER"": 39320, + ""iot_items"": [ + { + ""common.ALLTYPES_NAME"": ""RestServerItem1"", + ""iot_gateway.IOT_ITEM_SERVER_TAG"": ""Channel1.Device1.Tag4"", + ""iot_gateway.IOT_ITEM_ENABLED"": true + }, + { + ""common.ALLTYPES_NAME"": ""_RestServerItem2"", + ""iot_gateway.IOT_ITEM_SERVER_TAG"": ""Channel2.Device2.Tag1"", + ""iot_gateway.IOT_ITEM_ENABLED"": false + } + ] + } + ] + } + ] + }"; + + var project = JsonSerializer.Deserialize(json, KepJsonContext.Default.Project); + + Assert.NotNull(project); + Assert.Equal(42, project.ProjectId); + Assert.Equal("Test project", project.Description); + + Assert.NotNull(project.IotGateway); + Assert.False(project.IotGateway.IsEmpty); + + // MQTT Client + Assert.NotNull(project.IotGateway.MqttClientAgents); + Assert.Single(project.IotGateway.MqttClientAgents); + var mqtt = project.IotGateway.MqttClientAgents[0]; + Assert.Equal("MqttAgent1", mqtt.Name); + Assert.True(mqtt.Enabled); + Assert.Equal("tcp://broker:1883", mqtt.Url); + + // MQTT Client IoT Items + Assert.NotNull(mqtt.IotItems); + Assert.Equal(2, mqtt.IotItems.Count); + Assert.Equal("Item1", mqtt.IotItems[0].Name); + Assert.Equal("Channel1.Device1.Tag1", mqtt.IotItems[0].ServerTag); + Assert.True(mqtt.IotItems[0].Enabled); + Assert.Equal("_MqttItem2", mqtt.IotItems[1].Name); + Assert.Equal("Channel1.Device1.Tag5", mqtt.IotItems[1].ServerTag); + Assert.False(mqtt.IotItems[1].Enabled); + + // REST Client + Assert.NotNull(project.IotGateway.RestClientAgents); + Assert.Single(project.IotGateway.RestClientAgents); + var restClient = project.IotGateway.RestClientAgents[0]; + Assert.Equal("RestClient1", restClient.Name); + Assert.True(restClient.Enabled); + + // REST Client IoT Items + Assert.NotNull(restClient.IotItems); + Assert.Equal(2, restClient.IotItems.Count); + Assert.Equal("_RestClientItem1", restClient.IotItems[0].Name); + Assert.Equal("Channel1.Device1.Tag2", restClient.IotItems[0].ServerTag); + Assert.True(restClient.IotItems[0].Enabled); + Assert.Equal("RestClientItem2", restClient.IotItems[1].Name); + Assert.Equal("Channel1.Device1.Tag3", restClient.IotItems[1].ServerTag); + Assert.False(restClient.IotItems[1].Enabled); + + // REST Server + Assert.NotNull(project.IotGateway.RestServerAgents); + Assert.Single(project.IotGateway.RestServerAgents); + var restServer = project.IotGateway.RestServerAgents[0]; + Assert.Equal("RestServer1", restServer.Name); + Assert.False(restServer.Enabled); + Assert.Equal(39320, restServer.PortNumber); + + // REST Server IoT Items + Assert.NotNull(restServer.IotItems); + Assert.Equal(2, restServer.IotItems.Count); + Assert.Equal("RestServerItem1", restServer.IotItems[0].Name); + Assert.Equal("Channel1.Device1.Tag4", restServer.IotItems[0].ServerTag); + Assert.True(restServer.IotItems[0].Enabled); + Assert.Equal("_RestServerItem2", restServer.IotItems[1].Name); + Assert.Equal("Channel2.Device2.Tag1", restServer.IotItems[1].ServerTag); + Assert.False(restServer.IotItems[1].Enabled); + } + + [Fact] + public void ProjectWithoutIotGateway_ShouldDeserializeCorrectly() + { + var json = @"{ + ""PROJECT_ID"": 1, + ""common.ALLTYPES_DESCRIPTION"": ""No IoT"" + }"; + + var project = JsonSerializer.Deserialize(json, KepJsonContext.Default.Project); + + Assert.NotNull(project); + Assert.Null(project.IotGateway); + } + + [Fact] + public void ProjectWithEmptyIotGateway_ShouldNotSerializeIotGateway() + { + var project = new Project + { + IotGateway = new IotGatewayContainer() + }; + + var json = JsonSerializer.Serialize(project, KepJsonContext.Default.Project); + + Assert.DoesNotContain("_iot_gateway", json); + } + + #endregion + + #region Round-trip Serialization + + [Fact] + public void ProjectWithIotGateway_ShouldRoundTripSerialize() + { + var project = new Project(); + project.IotGateway = new IotGatewayContainer + { + MqttClientAgents = new MqttClientAgentCollection + { + new MqttClientAgent + { + Name = "MqttAgent1", + Enabled = true, + Url = "tcp://broker:1883", + Topic = "test/topic", + IotItems = new IotItemCollection + { + new IotItem + { + Name = "Item1", + ServerTag = "Channel1.Device1.Tag1", + Enabled = true + }, + new IotItem + { + Name = "_MqttItem2", + ServerTag = "Channel1.Device1.Tag5", + Enabled = false + } + } + } + }, + RestClientAgents = new RestClientAgentCollection + { + new RestClientAgent + { + Name = "RestClient1", + Enabled = true, + Url = "https://api.example.com", + IotItems = new IotItemCollection + { + new IotItem + { + Name = "_RestClientItem1", + ServerTag = "Channel1.Device1.Tag2", + Enabled = true + }, + new IotItem + { + Name = "RestClientItem2", + ServerTag = "Channel1.Device1.Tag3", + Enabled = false + } + } + } + }, + RestServerAgents = new RestServerAgentCollection + { + new RestServerAgent + { + Name = "RestServer1", + Enabled = false, + PortNumber = 39320, + IotItems = new IotItemCollection + { + new IotItem + { + Name = "RestServerItem1", + ServerTag = "Channel1.Device1.Tag4", + Enabled = true + }, + new IotItem + { + Name = "_RestServerItem2", + ServerTag = "Channel2.Device2.Tag1", + Enabled = false + } + } + } + } + }; + + var json = JsonSerializer.Serialize(project, KepJsonContext.Default.Project); + var deserialized = JsonSerializer.Deserialize(json, KepJsonContext.Default.Project); + + Assert.NotNull(deserialized); + Assert.NotNull(deserialized.IotGateway); + Assert.False(deserialized.IotGateway.IsEmpty); + + // MQTT + Assert.NotNull(deserialized.IotGateway.MqttClientAgents); + Assert.Single(deserialized.IotGateway.MqttClientAgents); + Assert.Equal("MqttAgent1", deserialized.IotGateway.MqttClientAgents[0].Name); + Assert.Equal("tcp://broker:1883", deserialized.IotGateway.MqttClientAgents[0].Url); + Assert.Equal("test/topic", deserialized.IotGateway.MqttClientAgents[0].Topic); + + // MQTT Items + Assert.NotNull(deserialized.IotGateway.MqttClientAgents[0].IotItems); + Assert.Equal(2, deserialized.IotGateway.MqttClientAgents[0].IotItems!.Count); + Assert.Equal("Item1", deserialized.IotGateway.MqttClientAgents[0].IotItems![0].Name); + Assert.Equal("_MqttItem2", deserialized.IotGateway.MqttClientAgents[0].IotItems![1].Name); + Assert.False(deserialized.IotGateway.MqttClientAgents[0].IotItems![1].Enabled); + + // REST Client + Assert.NotNull(deserialized.IotGateway.RestClientAgents); + Assert.Single(deserialized.IotGateway.RestClientAgents); + Assert.Equal("RestClient1", deserialized.IotGateway.RestClientAgents[0].Name); + + // REST Client Items + Assert.NotNull(deserialized.IotGateway.RestClientAgents[0].IotItems); + Assert.Equal(2, deserialized.IotGateway.RestClientAgents[0].IotItems!.Count); + Assert.Equal("_RestClientItem1", deserialized.IotGateway.RestClientAgents[0].IotItems![0].Name); + Assert.Equal("Channel1.Device1.Tag2", deserialized.IotGateway.RestClientAgents[0].IotItems![0].ServerTag); + Assert.Equal("RestClientItem2", deserialized.IotGateway.RestClientAgents[0].IotItems![1].Name); + Assert.Equal("Channel1.Device1.Tag3", deserialized.IotGateway.RestClientAgents[0].IotItems![1].ServerTag); + + // REST Server + Assert.NotNull(deserialized.IotGateway.RestServerAgents); + Assert.Single(deserialized.IotGateway.RestServerAgents); + Assert.Equal("RestServer1", deserialized.IotGateway.RestServerAgents[0].Name); + Assert.Equal(39320, deserialized.IotGateway.RestServerAgents[0].PortNumber); + + // REST Server Items + Assert.NotNull(deserialized.IotGateway.RestServerAgents[0].IotItems); + Assert.Equal(2, deserialized.IotGateway.RestServerAgents[0].IotItems!.Count); + Assert.Equal("RestServerItem1", deserialized.IotGateway.RestServerAgents[0].IotItems![0].Name); + Assert.Equal("Channel1.Device1.Tag4", deserialized.IotGateway.RestServerAgents[0].IotItems![0].ServerTag); + Assert.Equal("_RestServerItem2", deserialized.IotGateway.RestServerAgents[0].IotItems![1].Name); + Assert.Equal("Channel2.Device2.Tag1", deserialized.IotGateway.RestServerAgents[0].IotItems![1].ServerTag); + } + + #endregion + + #region Project.IsEmpty + + [Fact] + public void Project_IsEmpty_WithNullIotGateway_ShouldBeTrue() + { + var project = new Project(); + Assert.True(project.IsEmpty); + } + + [Fact] + public void Project_IsEmpty_WithEmptyIotGateway_ShouldBeTrue() + { + var project = new Project + { + IotGateway = new IotGatewayContainer() + }; + Assert.True(project.IsEmpty); + } + + [Fact] + public void Project_IsEmpty_WithIotGatewayAgents_ShouldBeFalse() + { + var project = new Project + { + IotGateway = new IotGatewayContainer + { + MqttClientAgents = new MqttClientAgentCollection + { + new MqttClientAgent { Name = "Agent1" } + } + } + }; + Assert.False(project.IsEmpty); + } + + #endregion + + #region JsonProjectRoot + + [Fact] + public void FullProjectJson_ShouldDeserializeWithIotGatewayInProjectRoot() + { + var json = @"{ + ""project"": { + ""PROJECT_ID"": 100, + ""common.ALLTYPES_DESCRIPTION"": ""Full project"", + ""_iot_gateway"": [ + { + ""common.ALLTYPES_NAME"": ""_IoT_Gateway"", + ""mqtt_clients"": [ + { + ""common.ALLTYPES_NAME"": ""Mqtt1"", + ""iot_gateway.AGENTTYPES_ENABLED"": true, + ""iot_items"": [ + { + ""common.ALLTYPES_NAME"": ""_MqttRootItem1"", + ""iot_gateway.IOT_ITEM_SERVER_TAG"": ""Channel1.Device1.Tag1"", + ""iot_gateway.IOT_ITEM_ENABLED"": true + } + ] + } + ], + ""rest_servers"": [ + { + ""common.ALLTYPES_NAME"": ""RestSrv1"", + ""iot_gateway.REST_SERVER_PORT_NUMBER"": 8080, + ""iot_items"": [ + { + ""common.ALLTYPES_NAME"": ""_RestSrvItem1"", + ""iot_gateway.IOT_ITEM_SERVER_TAG"": ""Channel1.Device1.Tag2"", + ""iot_gateway.IOT_ITEM_ENABLED"": false + } + ] + } + ] + } + ], + ""channels"": [ + { + ""common.ALLTYPES_NAME"": ""Channel1"", + ""servermain.MULTIPLE_TYPES_DEVICE_DRIVER"": ""Simulator"" + } + ] + } + }"; + + var root = JsonSerializer.Deserialize(json, KepJsonContext.Default.JsonProjectRoot); + + Assert.NotNull(root); + Assert.NotNull(root.Project); + Assert.Equal(100, root.Project.ProjectId); + + // Channels + Assert.NotNull(root.Project.Channels); + Assert.Single(root.Project.Channels); + Assert.Equal("Channel1", root.Project.Channels[0].Name); + + // IoT Gateway + Assert.NotNull(root.Project.IotGateway); + Assert.NotNull(root.Project.IotGateway.MqttClientAgents); + Assert.Single(root.Project.IotGateway.MqttClientAgents); + Assert.Equal("Mqtt1", root.Project.IotGateway.MqttClientAgents[0].Name); + Assert.True(root.Project.IotGateway.MqttClientAgents[0].Enabled); + Assert.NotNull(root.Project.IotGateway.MqttClientAgents[0].IotItems); + Assert.Single(root.Project.IotGateway.MqttClientAgents[0].IotItems!); + Assert.Equal("_MqttRootItem1", root.Project.IotGateway.MqttClientAgents[0].IotItems![0].Name); + Assert.Equal("Channel1.Device1.Tag1", root.Project.IotGateway.MqttClientAgents[0].IotItems![0].ServerTag); + + Assert.NotNull(root.Project.IotGateway.RestServerAgents); + Assert.Single(root.Project.IotGateway.RestServerAgents); + Assert.Equal("RestSrv1", root.Project.IotGateway.RestServerAgents[0].Name); + Assert.Equal(8080, root.Project.IotGateway.RestServerAgents[0].PortNumber); + Assert.NotNull(root.Project.IotGateway.RestServerAgents[0].IotItems); + Assert.Single(root.Project.IotGateway.RestServerAgents[0].IotItems!); + Assert.Equal("_RestSrvItem1", root.Project.IotGateway.RestServerAgents[0].IotItems![0].Name); + Assert.False(root.Project.IotGateway.RestServerAgents[0].IotItems![0].Enabled); + } + + #endregion + + #region IotGatewayContainer.IsEmpty + + [Fact] + public void IotGatewayContainer_IsEmpty_WithNullCollections_ShouldBeTrue() + { + var container = new IotGatewayContainer(); + Assert.True(container.IsEmpty); + } + + [Fact] + public void IotGatewayContainer_IsEmpty_WithEmptyCollections_ShouldBeTrue() + { + var container = new IotGatewayContainer + { + MqttClientAgents = new MqttClientAgentCollection(), + RestClientAgents = new RestClientAgentCollection(), + RestServerAgents = new RestServerAgentCollection() + }; + Assert.True(container.IsEmpty); + } + + [Fact] + public void IotGatewayContainer_IsEmpty_WithAgents_ShouldBeFalse() + { + var container = new IotGatewayContainer + { + RestClientAgents = new RestClientAgentCollection + { + new RestClientAgent { Name = "Agent1" } + } + }; + Assert.False(container.IsEmpty); + } + + #endregion + } +} diff --git a/Kepware.Api.Test/_data/projectLoadSerializeData/Channel1.json b/Kepware.Api.Test/_data/projectLoadSerializeData/Channel1.json new file mode 100644 index 0000000..8ece78c --- /dev/null +++ b/Kepware.Api.Test/_data/projectLoadSerializeData/Channel1.json @@ -0,0 +1,52 @@ +{ + "channels": { + "common.ALLTYPES_NAME": "Channel1", + "common.ALLTYPES_DESCRIPTION": "Example Simulator Channel", + "servermain.MULTIPLE_TYPES_DEVICE_DRIVER": "Simulator", + "servermain.CHANNEL_DIAGNOSTICS_CAPTURE": false, + "servermain.CHANNEL_UNIQUE_ID": 1704486747, + "servermain.CHANNEL_WRITE_OPTIMIZATIONS_METHOD": 2, + "servermain.CHANNEL_WRITE_OPTIMIZATIONS_DUTY_CYCLE": 10, + "servermain.CHANNEL_NON_NORMALIZED_FLOATING_POINT_HANDLING": 0, + "simulator.CHANNEL_ITEM_PERSISTENCE": false, + "simulator.CHANNEL_ITEM_PERSISTENCE_DATA_FILE": "C:\\ProgramData\\PTC\\Kepware Server\\V7\\Simulator\\Channel1.dat", + "devices": [ + { + "common.ALLTYPES_NAME": "Device1", + "common.ALLTYPES_DESCRIPTION": "Example Simulator Device", + "servermain.MULTIPLE_TYPES_DEVICE_DRIVER": "Simulator", + "servermain.DEVICE_MODEL": 0, + "servermain.DEVICE_UNIQUE_ID": 1808204482, + "servermain.DEVICE_ID_FORMAT": 1, + "servermain.DEVICE_ID_STRING": "1", + "servermain.DEVICE_ID_HEXADECIMAL": 1, + "servermain.DEVICE_ID_DECIMAL": 1, + "servermain.DEVICE_ID_OCTAL": 1, + "servermain.DEVICE_DATA_COLLECTION": true, + "servermain.DEVICE_SCAN_MODE": 0, + "servermain.DEVICE_SCAN_MODE_RATE_MS": 1000, + "servermain.DEVICE_SCAN_MODE_PROVIDE_INITIAL_UPDATES_FROM_CACHE": false, + "tags": [ + { + "common.ALLTYPES_NAME": "Tag1", + "common.ALLTYPES_DESCRIPTION": "Ramping Read/Write tag used to verify client connection", + "servermain.TAG_ADDRESS": "R0001", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Tag2", + "common.ALLTYPES_DESCRIPTION": "Constant Read/Write tag used to verify client connection", + "servermain.TAG_ADDRESS": "K0001", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + } + ] + } +} \ No newline at end of file diff --git a/Kepware.Api.Test/_data/projectLoadSerializeData/dataTypeExamples.16bitDevice.json b/Kepware.Api.Test/_data/projectLoadSerializeData/dataTypeExamples.16bitDevice.json new file mode 100644 index 0000000..6b491dc --- /dev/null +++ b/Kepware.Api.Test/_data/projectLoadSerializeData/dataTypeExamples.16bitDevice.json @@ -0,0 +1,926 @@ +{ + "devices": { + "common.ALLTYPES_NAME": "16 Bit Device", + "common.ALLTYPES_DESCRIPTION": "Example Simulator Device", + "servermain.MULTIPLE_TYPES_DEVICE_DRIVER": "Simulator", + "servermain.DEVICE_MODEL": 0, + "servermain.DEVICE_UNIQUE_ID": 18472794, + "servermain.DEVICE_ID_FORMAT": 1, + "servermain.DEVICE_ID_STRING": "3", + "servermain.DEVICE_ID_HEXADECIMAL": 3, + "servermain.DEVICE_ID_DECIMAL": 3, + "servermain.DEVICE_ID_OCTAL": 3, + "servermain.DEVICE_DATA_COLLECTION": true, + "servermain.DEVICE_SCAN_MODE": 0, + "servermain.DEVICE_SCAN_MODE_RATE_MS": 1000, + "servermain.DEVICE_SCAN_MODE_PROVIDE_INITIAL_UPDATES_FROM_CACHE": false, + "tag_groups": [ + { + "common.ALLTYPES_NAME": "B Registers", + "common.ALLTYPES_DESCRIPTION": "Boolean registers", + "tags": [ + { + "common.ALLTYPES_NAME": "Boolean1", + "common.ALLTYPES_DESCRIPTION": "Boolean register", + "servermain.TAG_ADDRESS": "B0001", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean2", + "common.ALLTYPES_DESCRIPTION": "Boolean register", + "servermain.TAG_ADDRESS": "B0002", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean3", + "common.ALLTYPES_DESCRIPTION": "Boolean register", + "servermain.TAG_ADDRESS": "B0003", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean4", + "common.ALLTYPES_DESCRIPTION": "Boolean register", + "servermain.TAG_ADDRESS": "B0004", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "BooleanArray", + "common.ALLTYPES_DESCRIPTION": "Array of 4 Boolean Registers", + "servermain.TAG_ADDRESS": "B0010 [4]", + "servermain.TAG_DATA_TYPE": 21, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + }, + { + "common.ALLTYPES_NAME": "K Registers", + "common.ALLTYPES_DESCRIPTION": "Constant Registers", + "tags": [ + { + "common.ALLTYPES_NAME": "Boolean1", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.00", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean2", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.01", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean3", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.02", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean4", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.03", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double1", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0400", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double2", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0404", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double3", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0408", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double4", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0412", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DoubleArray", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0440 [4]", + "servermain.TAG_DATA_TYPE": 29, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord1", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0500", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord2", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0502", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord3", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0504", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord4", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0506", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWordArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0550 [4]", + "servermain.TAG_DATA_TYPE": 27, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float1", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0600", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float2", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0602", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float3", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0604", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float4", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0606", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "FloatArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0660 [4]", + "servermain.TAG_DATA_TYPE": 28, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong1", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1200", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong2", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1204", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong3", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1208", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong4", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1212", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLongArray", + "common.ALLTYPES_DESCRIPTION": "64-bit signed integer array", + "servermain.TAG_ADDRESS": "K1240 [4]", + "servermain.TAG_DATA_TYPE": 33, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long1", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0700", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long2", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0702", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long3", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0704", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long4", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0706", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LongArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0770 [4]", + "servermain.TAG_DATA_TYPE": 26, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord1", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1100", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord2", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1104", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord3", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1108", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord4", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1112", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWordArray", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned Integer array", + "servermain.TAG_ADDRESS": "K1140 [4]", + "servermain.TAG_DATA_TYPE": 34, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short1", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0800", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short2", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0801", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short3", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0802", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short4", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0803", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "ShortArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0880 [4]", + "servermain.TAG_DATA_TYPE": 24, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word1", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0900", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word2", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0901", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word3", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0902", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word4", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0903", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "WordArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0990 [4]", + "servermain.TAG_DATA_TYPE": 25, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + }, + { + "common.ALLTYPES_NAME": "R Registers", + "common.ALLTYPES_DESCRIPTION": "Ramping Registers", + "tags": [ + { + "common.ALLTYPES_NAME": "Boolean1", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "R0100.00", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean2", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "R0100.01", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean3", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "R0100.02", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean4", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "R0100.03", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double1", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0400", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double2", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0404", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double3", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0408", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double4", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0412", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DoubleArray", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "R0440 [4]", + "servermain.TAG_DATA_TYPE": 29, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord1", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0500", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord2", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0502", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord3", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0504", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord4", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0506", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWordArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "R0550 [4]", + "servermain.TAG_DATA_TYPE": 27, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float1", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0600", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float2", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0602", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float3", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0604", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float4", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0606", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "FloatArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "R0660 [4]", + "servermain.TAG_DATA_TYPE": 28, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong1", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "R1200", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong2", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "R1204", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong3", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "R1208", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong4", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "R1212", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLongArray", + "common.ALLTYPES_DESCRIPTION": "64-bit signed integer array", + "servermain.TAG_ADDRESS": "R1240 [4]", + "servermain.TAG_DATA_TYPE": 33, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long1", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "R0700", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long2", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "R0702", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long3", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "R0704", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long4", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "R0706", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LongArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer array", + "servermain.TAG_ADDRESS": "R0770 [4]", + "servermain.TAG_DATA_TYPE": 26, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord1", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "R1100", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord2", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "R1104", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord3", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "R1108", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord4", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "R1112", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWordArray", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned Integer array", + "servermain.TAG_ADDRESS": "R1140 [4]", + "servermain.TAG_DATA_TYPE": 34, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short1", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "R0800", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short2", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "R0801", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short3", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "R0802", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short4", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "R0803", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "ShortArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer array", + "servermain.TAG_ADDRESS": "R0880 [4]", + "servermain.TAG_DATA_TYPE": 24, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word1", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0900", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word2", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0901", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word3", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0902", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word4", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0903", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "WordArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "R0990 [4]", + "servermain.TAG_DATA_TYPE": 25, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + }, + { + "common.ALLTYPES_NAME": "S Registers", + "common.ALLTYPES_DESCRIPTION": "String Registers", + "tags": [ + { + "common.ALLTYPES_NAME": "String1", + "common.ALLTYPES_DESCRIPTION": "Null terminated Unicode string", + "servermain.TAG_ADDRESS": "S0001", + "servermain.TAG_DATA_TYPE": 0, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "String2", + "common.ALLTYPES_DESCRIPTION": "Null terminated Unicode string", + "servermain.TAG_ADDRESS": "S0002", + "servermain.TAG_DATA_TYPE": 0, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "String3", + "common.ALLTYPES_DESCRIPTION": "Null terminated Unicode string", + "servermain.TAG_ADDRESS": "S0003", + "servermain.TAG_DATA_TYPE": 0, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "String4", + "common.ALLTYPES_DESCRIPTION": "Null terminated Unicode string", + "servermain.TAG_ADDRESS": "S0004", + "servermain.TAG_DATA_TYPE": 0, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "StringArray[4]", + "common.ALLTYPES_DESCRIPTION": "Null terminated Unicode string array", + "servermain.TAG_ADDRESS": "S0010 [4]", + "servermain.TAG_DATA_TYPE": 20, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + } + ] + } +} \ No newline at end of file diff --git a/Kepware.Api.Test/_data/projectLoadSerializeData/dte.8bitDevice.Breg.json b/Kepware.Api.Test/_data/projectLoadSerializeData/dte.8bitDevice.Breg.json new file mode 100644 index 0000000..cf3dfee --- /dev/null +++ b/Kepware.Api.Test/_data/projectLoadSerializeData/dte.8bitDevice.Breg.json @@ -0,0 +1,53 @@ +{ + "tag_groups": { + "common.ALLTYPES_NAME": "B Registers", + "common.ALLTYPES_DESCRIPTION": "Boolean registers", + "tags": [ + { + "common.ALLTYPES_NAME": "Boolean1", + "common.ALLTYPES_DESCRIPTION": "Boolean register", + "servermain.TAG_ADDRESS": "B0001", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean2", + "common.ALLTYPES_DESCRIPTION": "Boolean register", + "servermain.TAG_ADDRESS": "B0002", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean3", + "common.ALLTYPES_DESCRIPTION": "Boolean register", + "servermain.TAG_ADDRESS": "B0003", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean4", + "common.ALLTYPES_DESCRIPTION": "Boolean register", + "servermain.TAG_ADDRESS": "B0004", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "BooleanArray", + "common.ALLTYPES_DESCRIPTION": "Array of 4 Boolean Registers", + "servermain.TAG_ADDRESS": "B0010 [4]", + "servermain.TAG_DATA_TYPE": 21, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + } +} \ No newline at end of file diff --git a/Kepware.Api.Test/_data/projectLoadSerializeData/dte.8bitDevice.Kreg.json b/Kepware.Api.Test/_data/projectLoadSerializeData/dte.8bitDevice.Kreg.json new file mode 100644 index 0000000..0a35190 --- /dev/null +++ b/Kepware.Api.Test/_data/projectLoadSerializeData/dte.8bitDevice.Kreg.json @@ -0,0 +1,494 @@ +{ + "tag_groups": { + "common.ALLTYPES_NAME": "K Registers", + "common.ALLTYPES_DESCRIPTION": "Constant Registers", + "tags": [ + { + "common.ALLTYPES_NAME": "Boolean1", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.00", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean2", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.01", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean3", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.02", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean4", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.03", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Byte1", + "common.ALLTYPES_DESCRIPTION": "8-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0200", + "servermain.TAG_DATA_TYPE": 3, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Byte2", + "common.ALLTYPES_DESCRIPTION": "8-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0201", + "servermain.TAG_DATA_TYPE": 3, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Byte3", + "common.ALLTYPES_DESCRIPTION": "8-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0202", + "servermain.TAG_DATA_TYPE": 3, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Byte4", + "common.ALLTYPES_DESCRIPTION": "8-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0203", + "servermain.TAG_DATA_TYPE": 3, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "ByteArray", + "common.ALLTYPES_DESCRIPTION": "8-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0220 [4]", + "servermain.TAG_DATA_TYPE": 23, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Char1", + "common.ALLTYPES_DESCRIPTION": "8-Bit signed integer", + "servermain.TAG_ADDRESS": "K0300", + "servermain.TAG_DATA_TYPE": 2, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Char2", + "common.ALLTYPES_DESCRIPTION": "8-Bit signed integer", + "servermain.TAG_ADDRESS": "K0301", + "servermain.TAG_DATA_TYPE": 2, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Char3", + "common.ALLTYPES_DESCRIPTION": "8-Bit signed integer", + "servermain.TAG_ADDRESS": "K0302", + "servermain.TAG_DATA_TYPE": 2, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Char4", + "common.ALLTYPES_DESCRIPTION": "8-Bit signed integer", + "servermain.TAG_ADDRESS": "K0303", + "servermain.TAG_DATA_TYPE": 2, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "CharArray", + "common.ALLTYPES_DESCRIPTION": "8-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0330 [4]", + "servermain.TAG_DATA_TYPE": 22, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double1", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0400", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double2", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0408", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double3", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0416", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double4", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0424", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DoubleArray", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0440 [4]", + "servermain.TAG_DATA_TYPE": 29, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord1", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0500", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord2", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0504", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord3", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0508", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord4", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0512", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWordArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0550 [4]", + "servermain.TAG_DATA_TYPE": 27, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float1", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0600", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float2", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0604", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float3", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0608", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float4", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0612", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "FloatArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0660 [4]", + "servermain.TAG_DATA_TYPE": 28, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong1", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1200", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong2", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1208", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong3", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1216", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong4", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1224", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLongArray", + "common.ALLTYPES_DESCRIPTION": "64-bit signed integer array", + "servermain.TAG_ADDRESS": "K1240 [4]", + "servermain.TAG_DATA_TYPE": 33, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long1", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0700", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long2", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0704", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long3", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0708", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long4", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0712", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LongArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0770 [4]", + "servermain.TAG_DATA_TYPE": 26, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord1", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1100", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord2", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1108", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord3", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1116", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord4", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1124", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWordArray", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned Integer array", + "servermain.TAG_ADDRESS": "K1140 [4]", + "servermain.TAG_DATA_TYPE": 34, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short1", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0800", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short2", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0802", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short3", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0804", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short4", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0806", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "ShortArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0880 [4]", + "servermain.TAG_DATA_TYPE": 24, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word1", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0900", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word2", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0902", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word3", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0904", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word4", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0906", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "WordArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0990 [4]", + "servermain.TAG_DATA_TYPE": 25, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + } +} \ No newline at end of file diff --git a/Kepware.Api.Test/_data/projectLoadSerializeData/dte.8bitDevice.Rreg.json b/Kepware.Api.Test/_data/projectLoadSerializeData/dte.8bitDevice.Rreg.json new file mode 100644 index 0000000..f7dbe82 --- /dev/null +++ b/Kepware.Api.Test/_data/projectLoadSerializeData/dte.8bitDevice.Rreg.json @@ -0,0 +1,494 @@ +{ + "tag_groups": { + "common.ALLTYPES_NAME": "R Registers", + "common.ALLTYPES_DESCRIPTION": "Ramping Registers", + "tags": [ + { + "common.ALLTYPES_NAME": "Boolean1", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "R0100.00", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean2", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "R0100.01", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean3", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "R0100.02", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean4", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "R0100.03", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Byte1", + "common.ALLTYPES_DESCRIPTION": "8-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0200", + "servermain.TAG_DATA_TYPE": 3, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Byte2", + "common.ALLTYPES_DESCRIPTION": "8-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0201", + "servermain.TAG_DATA_TYPE": 3, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Byte3", + "common.ALLTYPES_DESCRIPTION": "8-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0202", + "servermain.TAG_DATA_TYPE": 3, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Byte4", + "common.ALLTYPES_DESCRIPTION": "8-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0203", + "servermain.TAG_DATA_TYPE": 3, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "ByteArray", + "common.ALLTYPES_DESCRIPTION": "8-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "R0220 [4]", + "servermain.TAG_DATA_TYPE": 23, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Char1", + "common.ALLTYPES_DESCRIPTION": "8-Bit signed integer", + "servermain.TAG_ADDRESS": "R0300", + "servermain.TAG_DATA_TYPE": 2, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Char2", + "common.ALLTYPES_DESCRIPTION": "8-Bit signed integer", + "servermain.TAG_ADDRESS": "R0301", + "servermain.TAG_DATA_TYPE": 2, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Char3", + "common.ALLTYPES_DESCRIPTION": "8-Bit signed integer", + "servermain.TAG_ADDRESS": "R0302", + "servermain.TAG_DATA_TYPE": 2, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Char4", + "common.ALLTYPES_DESCRIPTION": "8-Bit signed integer", + "servermain.TAG_ADDRESS": "R0303", + "servermain.TAG_DATA_TYPE": 2, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "CharArray", + "common.ALLTYPES_DESCRIPTION": "8-Bit signed integer array", + "servermain.TAG_ADDRESS": "R0330 [4]", + "servermain.TAG_DATA_TYPE": 22, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double1", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0400", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double2", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0408", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double3", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0416", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double4", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0424", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DoubleArray", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "R0440 [4]", + "servermain.TAG_DATA_TYPE": 29, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord1", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0500", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord2", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0504", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord3", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0508", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord4", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0512", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWordArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "R0550 [4]", + "servermain.TAG_DATA_TYPE": 27, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float1", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0600", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float2", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0604", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float3", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0608", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float4", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0612", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "FloatArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "R0660 [4]", + "servermain.TAG_DATA_TYPE": 28, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong1", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "R1200", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong2", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "R1208", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong3", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "R1216", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong4", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "R1224", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLongArray", + "common.ALLTYPES_DESCRIPTION": "64-bit signed integer array", + "servermain.TAG_ADDRESS": "R1240 [4]", + "servermain.TAG_DATA_TYPE": 33, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long1", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "R0700", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long2", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "R0704", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long3", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "R0708", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long4", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "R0712", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LongArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer array", + "servermain.TAG_ADDRESS": "R0770 [4]", + "servermain.TAG_DATA_TYPE": 26, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord1", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "R1100", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord2", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "R1108", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord3", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "R1116", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord4", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "R1124", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWordArray", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned Integer array", + "servermain.TAG_ADDRESS": "R1140 [4]", + "servermain.TAG_DATA_TYPE": 34, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short1", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "R0800", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short2", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "R0802", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short3", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "R0804", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short4", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "R0806", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "ShortArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer array", + "servermain.TAG_ADDRESS": "R0880 [4]", + "servermain.TAG_DATA_TYPE": 24, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word1", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0900", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word2", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0902", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word3", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0904", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word4", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0906", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "WordArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "R0990 [4]", + "servermain.TAG_DATA_TYPE": 25, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + } +} \ No newline at end of file diff --git a/Kepware.Api.Test/_data/projectLoadSerializeData/dte.8bitDevice.Sreg.json b/Kepware.Api.Test/_data/projectLoadSerializeData/dte.8bitDevice.Sreg.json new file mode 100644 index 0000000..718fa10 --- /dev/null +++ b/Kepware.Api.Test/_data/projectLoadSerializeData/dte.8bitDevice.Sreg.json @@ -0,0 +1,53 @@ +{ + "tag_groups": { + "common.ALLTYPES_NAME": "S Registers", + "common.ALLTYPES_DESCRIPTION": "String Registers", + "tags": [ + { + "common.ALLTYPES_NAME": "String1", + "common.ALLTYPES_DESCRIPTION": "Null terminated Unicode string", + "servermain.TAG_ADDRESS": "S0001", + "servermain.TAG_DATA_TYPE": 0, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "String2", + "common.ALLTYPES_DESCRIPTION": "Null terminated Unicode string", + "servermain.TAG_ADDRESS": "S0002", + "servermain.TAG_DATA_TYPE": 0, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "String3", + "common.ALLTYPES_DESCRIPTION": "Null terminated Unicode string", + "servermain.TAG_ADDRESS": "S0003", + "servermain.TAG_DATA_TYPE": 0, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "String4", + "common.ALLTYPES_DESCRIPTION": "Null terminated Unicode string", + "servermain.TAG_ADDRESS": "S0004", + "servermain.TAG_DATA_TYPE": 0, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "StringArray[4]", + "common.ALLTYPES_DESCRIPTION": "Null terminated Unicode string array", + "servermain.TAG_ADDRESS": "S0010 [4]", + "servermain.TAG_DATA_TYPE": 20, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + } +} \ No newline at end of file diff --git a/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.Breg.json b/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.Breg.json new file mode 100644 index 0000000..cf3dfee --- /dev/null +++ b/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.Breg.json @@ -0,0 +1,53 @@ +{ + "tag_groups": { + "common.ALLTYPES_NAME": "B Registers", + "common.ALLTYPES_DESCRIPTION": "Boolean registers", + "tags": [ + { + "common.ALLTYPES_NAME": "Boolean1", + "common.ALLTYPES_DESCRIPTION": "Boolean register", + "servermain.TAG_ADDRESS": "B0001", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean2", + "common.ALLTYPES_DESCRIPTION": "Boolean register", + "servermain.TAG_ADDRESS": "B0002", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean3", + "common.ALLTYPES_DESCRIPTION": "Boolean register", + "servermain.TAG_ADDRESS": "B0003", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean4", + "common.ALLTYPES_DESCRIPTION": "Boolean register", + "servermain.TAG_ADDRESS": "B0004", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "BooleanArray", + "common.ALLTYPES_DESCRIPTION": "Array of 4 Boolean Registers", + "servermain.TAG_ADDRESS": "B0010 [4]", + "servermain.TAG_DATA_TYPE": 21, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + } +} \ No newline at end of file diff --git a/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.Kreg.json b/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.Kreg.json new file mode 100644 index 0000000..939be19 --- /dev/null +++ b/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.Kreg.json @@ -0,0 +1,404 @@ +{ + "tag_groups": { + "common.ALLTYPES_NAME": "K Registers", + "common.ALLTYPES_DESCRIPTION": "Constant Registers", + "tags": [ + { + "common.ALLTYPES_NAME": "Boolean1", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.00", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean2", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.01", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean3", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.02", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean4", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.03", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double1", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0400", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double2", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0404", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double3", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0408", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double4", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0412", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DoubleArray", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0440 [4]", + "servermain.TAG_DATA_TYPE": 29, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord1", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0500", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord2", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0502", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord3", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0504", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord4", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0506", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWordArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0550 [4]", + "servermain.TAG_DATA_TYPE": 27, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float1", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0600", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float2", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0602", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float3", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0604", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float4", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0606", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "FloatArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0660 [4]", + "servermain.TAG_DATA_TYPE": 28, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong1", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1200", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong2", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1204", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong3", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1208", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong4", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1212", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLongArray", + "common.ALLTYPES_DESCRIPTION": "64-bit signed integer array", + "servermain.TAG_ADDRESS": "K1240 [4]", + "servermain.TAG_DATA_TYPE": 33, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long1", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0700", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long2", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0702", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long3", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0704", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long4", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0706", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LongArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0770 [4]", + "servermain.TAG_DATA_TYPE": 26, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord1", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1100", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord2", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1104", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord3", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1108", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord4", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1112", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWordArray", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned Integer array", + "servermain.TAG_ADDRESS": "K1140 [4]", + "servermain.TAG_DATA_TYPE": 34, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short1", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0800", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short2", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0801", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short3", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0802", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short4", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0803", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "ShortArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0880 [4]", + "servermain.TAG_DATA_TYPE": 24, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word1", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0900", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word2", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0901", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word3", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0902", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word4", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0903", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "WordArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0990 [4]", + "servermain.TAG_DATA_TYPE": 25, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + } +} \ No newline at end of file diff --git a/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.RecursionTest.Level1.json b/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.RecursionTest.Level1.json new file mode 100644 index 0000000..e708b5f --- /dev/null +++ b/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.RecursionTest.Level1.json @@ -0,0 +1,806 @@ +{ + "tag_groups": { + "common.ALLTYPES_NAME": "Level1", + "tags": [ + { + "common.ALLTYPES_NAME": "Boolean1", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.00", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean2", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.01", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean3", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.02", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean4", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.03", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double1", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0400", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double2", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0404", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double3", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0408", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double4", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0412", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DoubleArray", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0440 [4]", + "servermain.TAG_DATA_TYPE": 29, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord1", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0500", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord2", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0502", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord3", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0504", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord4", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0506", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWordArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0550 [4]", + "servermain.TAG_DATA_TYPE": 27, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float1", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0600", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float2", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0602", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float3", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0604", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float4", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0606", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "FloatArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0660 [4]", + "servermain.TAG_DATA_TYPE": 28, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong1", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1200", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong2", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1204", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong3", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1208", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong4", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1212", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLongArray", + "common.ALLTYPES_DESCRIPTION": "64-bit signed integer array", + "servermain.TAG_ADDRESS": "K1240 [4]", + "servermain.TAG_DATA_TYPE": 33, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long1", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0700", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long2", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0702", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long3", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0704", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long4", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0706", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LongArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0770 [4]", + "servermain.TAG_DATA_TYPE": 26, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord1", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1100", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord2", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1104", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord3", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1108", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord4", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1112", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWordArray", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned Integer array", + "servermain.TAG_ADDRESS": "K1140 [4]", + "servermain.TAG_DATA_TYPE": 34, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short1", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0800", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short2", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0801", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short3", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0802", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short4", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0803", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "ShortArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0880 [4]", + "servermain.TAG_DATA_TYPE": 24, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word1", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0900", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word2", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0901", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word3", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0902", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word4", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0903", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "WordArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0990 [4]", + "servermain.TAG_DATA_TYPE": 25, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ], + "tag_groups": [ + { + "common.ALLTYPES_NAME": "Level1_1", + "tags": [ + { + "common.ALLTYPES_NAME": "Boolean1", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.00", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean2", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.01", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean3", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.02", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean4", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.03", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double1", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0400", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double2", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0404", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double3", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0408", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double4", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0412", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DoubleArray", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0440 [4]", + "servermain.TAG_DATA_TYPE": 29, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord1", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0500", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord2", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0502", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord3", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0504", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord4", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0506", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWordArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0550 [4]", + "servermain.TAG_DATA_TYPE": 27, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float1", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0600", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float2", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0602", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float3", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0604", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float4", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0606", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "FloatArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0660 [4]", + "servermain.TAG_DATA_TYPE": 28, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong1", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1200", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong2", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1204", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong3", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1208", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong4", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1212", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLongArray", + "common.ALLTYPES_DESCRIPTION": "64-bit signed integer array", + "servermain.TAG_ADDRESS": "K1240 [4]", + "servermain.TAG_DATA_TYPE": 33, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long1", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0700", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long2", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0702", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long3", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0704", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long4", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0706", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LongArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0770 [4]", + "servermain.TAG_DATA_TYPE": 26, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord1", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1100", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord2", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1104", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord3", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1108", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord4", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1112", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWordArray", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned Integer array", + "servermain.TAG_ADDRESS": "K1140 [4]", + "servermain.TAG_DATA_TYPE": 34, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short1", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0800", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short2", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0801", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short3", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0802", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short4", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0803", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "ShortArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0880 [4]", + "servermain.TAG_DATA_TYPE": 24, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word1", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0900", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word2", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0901", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word3", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0902", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word4", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0903", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "WordArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0990 [4]", + "servermain.TAG_DATA_TYPE": 25, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + } + ] + } +} \ No newline at end of file diff --git a/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.RecursionTest.Level2.json b/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.RecursionTest.Level2.json new file mode 100644 index 0000000..71954e4 --- /dev/null +++ b/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.RecursionTest.Level2.json @@ -0,0 +1,403 @@ +{ + "tag_groups": { + "common.ALLTYPES_NAME": "Level2", + "tags": [ + { + "common.ALLTYPES_NAME": "Boolean1", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.00", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean2", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.01", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean3", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.02", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean4", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.03", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double1", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0400", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double2", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0404", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double3", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0408", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double4", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0412", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DoubleArray", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0440 [4]", + "servermain.TAG_DATA_TYPE": 29, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord1", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0500", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord2", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0502", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord3", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0504", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord4", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0506", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWordArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0550 [4]", + "servermain.TAG_DATA_TYPE": 27, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float1", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0600", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float2", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0602", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float3", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0604", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float4", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0606", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "FloatArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0660 [4]", + "servermain.TAG_DATA_TYPE": 28, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong1", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1200", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong2", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1204", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong3", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1208", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong4", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1212", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLongArray", + "common.ALLTYPES_DESCRIPTION": "64-bit signed integer array", + "servermain.TAG_ADDRESS": "K1240 [4]", + "servermain.TAG_DATA_TYPE": 33, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long1", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0700", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long2", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0702", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long3", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0704", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long4", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0706", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LongArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0770 [4]", + "servermain.TAG_DATA_TYPE": 26, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord1", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1100", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord2", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1104", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord3", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1108", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord4", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1112", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWordArray", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned Integer array", + "servermain.TAG_ADDRESS": "K1140 [4]", + "servermain.TAG_DATA_TYPE": 34, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short1", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0800", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short2", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0801", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short3", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0802", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short4", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0803", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "ShortArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0880 [4]", + "servermain.TAG_DATA_TYPE": 24, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word1", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0900", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word2", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0901", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word3", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0902", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word4", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0903", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "WordArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0990 [4]", + "servermain.TAG_DATA_TYPE": 25, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + } +} \ No newline at end of file diff --git a/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.RecursionTest.Level3.json b/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.RecursionTest.Level3.json new file mode 100644 index 0000000..8b86cb6 --- /dev/null +++ b/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.RecursionTest.Level3.json @@ -0,0 +1,403 @@ +{ + "tag_groups": { + "common.ALLTYPES_NAME": "Level3", + "tags": [ + { + "common.ALLTYPES_NAME": "Boolean1", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.00", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean2", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.01", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean3", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.02", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean4", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.03", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double1", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0400", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double2", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0404", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double3", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0408", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double4", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0412", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DoubleArray", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0440 [4]", + "servermain.TAG_DATA_TYPE": 29, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord1", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0500", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord2", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0502", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord3", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0504", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord4", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0506", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWordArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0550 [4]", + "servermain.TAG_DATA_TYPE": 27, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float1", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0600", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float2", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0602", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float3", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0604", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float4", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0606", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "FloatArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0660 [4]", + "servermain.TAG_DATA_TYPE": 28, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong1", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1200", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong2", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1204", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong3", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1208", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong4", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1212", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLongArray", + "common.ALLTYPES_DESCRIPTION": "64-bit signed integer array", + "servermain.TAG_ADDRESS": "K1240 [4]", + "servermain.TAG_DATA_TYPE": 33, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long1", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0700", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long2", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0702", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long3", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0704", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long4", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0706", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LongArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0770 [4]", + "servermain.TAG_DATA_TYPE": 26, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord1", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1100", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord2", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1104", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord3", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1108", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord4", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1112", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWordArray", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned Integer array", + "servermain.TAG_ADDRESS": "K1140 [4]", + "servermain.TAG_DATA_TYPE": 34, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short1", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0800", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short2", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0801", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short3", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0802", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short4", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0803", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "ShortArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0880 [4]", + "servermain.TAG_DATA_TYPE": 24, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word1", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0900", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word2", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0901", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word3", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0902", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word4", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0903", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "WordArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0990 [4]", + "servermain.TAG_DATA_TYPE": 25, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + } +} \ No newline at end of file diff --git a/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.RecursionTest.Level4.json b/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.RecursionTest.Level4.json new file mode 100644 index 0000000..d7688fe --- /dev/null +++ b/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.RecursionTest.Level4.json @@ -0,0 +1,403 @@ +{ + "tag_groups": { + "common.ALLTYPES_NAME": "Level4", + "tags": [ + { + "common.ALLTYPES_NAME": "Boolean1", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.00", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean2", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.01", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean3", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.02", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean4", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.03", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double1", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0400", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double2", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0404", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double3", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0408", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double4", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0412", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DoubleArray", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0440 [4]", + "servermain.TAG_DATA_TYPE": 29, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord1", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0500", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord2", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0502", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord3", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0504", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord4", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0506", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWordArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0550 [4]", + "servermain.TAG_DATA_TYPE": 27, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float1", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0600", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float2", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0602", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float3", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0604", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float4", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0606", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "FloatArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0660 [4]", + "servermain.TAG_DATA_TYPE": 28, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong1", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1200", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong2", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1204", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong3", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1208", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong4", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1212", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLongArray", + "common.ALLTYPES_DESCRIPTION": "64-bit signed integer array", + "servermain.TAG_ADDRESS": "K1240 [4]", + "servermain.TAG_DATA_TYPE": 33, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long1", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0700", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long2", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0702", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long3", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0704", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long4", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0706", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LongArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0770 [4]", + "servermain.TAG_DATA_TYPE": 26, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord1", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1100", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord2", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1104", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord3", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1108", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord4", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1112", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWordArray", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned Integer array", + "servermain.TAG_ADDRESS": "K1140 [4]", + "servermain.TAG_DATA_TYPE": 34, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short1", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0800", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short2", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0801", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short3", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0802", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short4", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0803", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "ShortArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0880 [4]", + "servermain.TAG_DATA_TYPE": 24, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word1", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0900", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word2", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0901", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word3", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0902", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word4", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0903", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "WordArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0990 [4]", + "servermain.TAG_DATA_TYPE": 25, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + } +} \ No newline at end of file diff --git a/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.Rreg.json b/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.Rreg.json new file mode 100644 index 0000000..f4043f2 --- /dev/null +++ b/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.Rreg.json @@ -0,0 +1,404 @@ +{ + "tag_groups": { + "common.ALLTYPES_NAME": "R Registers", + "common.ALLTYPES_DESCRIPTION": "Ramping Registers", + "tags": [ + { + "common.ALLTYPES_NAME": "Boolean1", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "R0100.00", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean2", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "R0100.01", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean3", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "R0100.02", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean4", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "R0100.03", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double1", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0400", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double2", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0404", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double3", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0408", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double4", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0412", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DoubleArray", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "R0440 [4]", + "servermain.TAG_DATA_TYPE": 29, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord1", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0500", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord2", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0502", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord3", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0504", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord4", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0506", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWordArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "R0550 [4]", + "servermain.TAG_DATA_TYPE": 27, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float1", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0600", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float2", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0602", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float3", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0604", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float4", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0606", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "FloatArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "R0660 [4]", + "servermain.TAG_DATA_TYPE": 28, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong1", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "R1200", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong2", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "R1204", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong3", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "R1208", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong4", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "R1212", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLongArray", + "common.ALLTYPES_DESCRIPTION": "64-bit signed integer array", + "servermain.TAG_ADDRESS": "R1240 [4]", + "servermain.TAG_DATA_TYPE": 33, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long1", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "R0700", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long2", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "R0702", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long3", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "R0704", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long4", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "R0706", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LongArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer array", + "servermain.TAG_ADDRESS": "R0770 [4]", + "servermain.TAG_DATA_TYPE": 26, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord1", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "R1100", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord2", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "R1104", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord3", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "R1108", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord4", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "R1112", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWordArray", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned Integer array", + "servermain.TAG_ADDRESS": "R1140 [4]", + "servermain.TAG_DATA_TYPE": 34, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short1", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "R0800", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short2", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "R0801", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short3", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "R0802", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short4", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "R0803", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "ShortArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer array", + "servermain.TAG_ADDRESS": "R0880 [4]", + "servermain.TAG_DATA_TYPE": 24, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word1", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0900", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word2", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0901", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word3", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0902", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word4", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0903", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "WordArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "R0990 [4]", + "servermain.TAG_DATA_TYPE": 25, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + } +} \ No newline at end of file diff --git a/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.Sreg.json b/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.Sreg.json new file mode 100644 index 0000000..718fa10 --- /dev/null +++ b/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.Sreg.json @@ -0,0 +1,53 @@ +{ + "tag_groups": { + "common.ALLTYPES_NAME": "S Registers", + "common.ALLTYPES_DESCRIPTION": "String Registers", + "tags": [ + { + "common.ALLTYPES_NAME": "String1", + "common.ALLTYPES_DESCRIPTION": "Null terminated Unicode string", + "servermain.TAG_ADDRESS": "S0001", + "servermain.TAG_DATA_TYPE": 0, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "String2", + "common.ALLTYPES_DESCRIPTION": "Null terminated Unicode string", + "servermain.TAG_ADDRESS": "S0002", + "servermain.TAG_DATA_TYPE": 0, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "String3", + "common.ALLTYPES_DESCRIPTION": "Null terminated Unicode string", + "servermain.TAG_ADDRESS": "S0003", + "servermain.TAG_DATA_TYPE": 0, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "String4", + "common.ALLTYPES_DESCRIPTION": "Null terminated Unicode string", + "servermain.TAG_ADDRESS": "S0004", + "servermain.TAG_DATA_TYPE": 0, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "StringArray[4]", + "common.ALLTYPES_DESCRIPTION": "Null terminated Unicode string array", + "servermain.TAG_ADDRESS": "S0010 [4]", + "servermain.TAG_DATA_TYPE": 20, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + } +} \ No newline at end of file diff --git a/Kepware.Api.Test/_data/projectLoadSerializeData/projectProperties.json b/Kepware.Api.Test/_data/projectLoadSerializeData/projectProperties.json new file mode 100644 index 0000000..27a7a92 --- /dev/null +++ b/Kepware.Api.Test/_data/projectLoadSerializeData/projectProperties.json @@ -0,0 +1,80 @@ +{ + "PROJECT_ID": 960924075, + "common.ALLTYPES_DESCRIPTION": "Example project utilizing Simulator Driver.", + "servermain.PROJECT_TITLE": "Simulation Driver Demo", + "servermain.PROJECT_TAGS_DEFINED": "246", + "opcdaserver.PROJECT_OPC_DA_1_ENABLED": true, + "opcdaserver.PROJECT_OPC_DA_2_ENABLED": true, + "opcdaserver.PROJECT_OPC_DA_3_ENABLED": true, + "opcdaserver.PROJECT_OPC_SHOW_HINTS_ON_BROWSE": false, + "opcdaserver.PROJECT_OPC_SHOW_TAG_PROPERTIES_ON_BROWSE": false, + "opcdaserver.PROJECT_OPC_SHUTDOWN_WAIT_SEC": 15, + "opcdaserver.PROJECT_OPC_SYNC_REQUEST_WAIT_SEC": 15, + "opcdaserver.PROJECT_OPC_ENABLE_DIAGS": false, + "opcdaserver.PROJECT_OPC_MAX_CONNECTIONS": 512, + "opcdaserver.PROJECT_OPC_MAX_TAG_GROUPS": 2000, + "opcdaserver.PROJECT_OPC_REJECT_UNSUPPORTED_LANG_ID": true, + "opcdaserver.PROJECT_OPC_IGNORE_DEADBAND_ON_CACHE": false, + "opcdaserver.PROJECT_OPC_IGNORE_BROWSE_FILTER": false, + "opcdaserver.PROJECT_OPC_205A_DATA_TYPE_SUPPORT": true, + "opcdaserver.PROJECT_OPC_SYNC_READ_ERROR_ON_BAD_QUALITY": false, + "opcdaserver.PROJECT_OPC_RETURN_INITIAL_UPDATES_IN_SINGLE_CALLBACK": false, + "opcdaserver.PROJECT_OPC_RESPECT_CLIENT_LANG_ID": true, + "opcdaserver.PROJECT_OPC_COMPLIANT_DATA_CHANGE": true, + "opcdaserver.PROJECT_OPC_IGNORE_GROUP_UPDATE_RATE": false, + "wwtoolkitinterface.ENABLED": false, + "wwtoolkitinterface.SERVICE_NAME": "server_runtime", + "wwtoolkitinterface.CLIENT_UPDATE_INTERVAL_MS": 100, + "ddeserver.ENABLE": false, + "ddeserver.SERVICE_NAME": "ptcdde", + "ddeserver.ADVANCED_DDE": true, + "ddeserver.XLTABLE": true, + "ddeserver.CF_TEXT": true, + "ddeserver.CLIENT_UPDATE_INTERVAL_MS": 100, + "ddeserver.REQUEST_TIMEOUT_SEC": 15, + "uaserverinterface.PROJECT_OPC_UA_ENABLE": true, + "uaserverinterface.PROJECT_OPC_UA_DIAGNOSTICS": false, + "uaserverinterface.PROJECT_OPC_UA_ANONYMOUS_LOGIN": false, + "uaserverinterface.PROJECT_OPC_UA_MAX_CONNECTIONS": 128, + "uaserverinterface.PROJECT_OPC_UA_MIN_SESSION_TIMEOUT_SEC": 15, + "uaserverinterface.PROJECT_OPC_UA_MAX_SESSION_TIMEOUT_SEC": 60, + "uaserverinterface.PROJECT_OPC_UA_TAG_CACHE_TIMEOUT_SEC": 5, + "uaserverinterface.PROJECT_OPC_UA_BROWSE_TAG_PROPERTIES": false, + "uaserverinterface.PROJECT_OPC_UA_BROWSE_ADDRESS_HINTS": false, + "uaserverinterface.PROJECT_OPC_UA_MAX_DATA_QUEUE_SIZE": 2, + "uaserverinterface.PROJECT_OPC_UA_MAX_RETRANSMIT_QUEUE_SIZE": 10, + "uaserverinterface.PROJECT_OPC_UA_MAX_NOTIFICATION_PER_PUBLISH": 65536, + "aeserverinterface.ENABLE_AE_SERVER": false, + "aeserverinterface.ENABLE_SIMPLE_EVENTS": true, + "aeserverinterface.MAX_SUBSCRIPTION_BUFFER_SIZE": 100, + "aeserverinterface.MIN_SUBSCRIPTION_BUFFER_TIME_MS": 1000, + "aeserverinterface.MIN_KEEP_ALIVE_TIME_MS": 1000, + "hdaserver.ENABLE": false, + "hdaserver.ENABLE_DIAGNOSTICS": false, + "thingworxinterface.ENABLED": false, + "thingworxinterface.HOSTNAME": "localhost", + "thingworxinterface.PORT": 443, + "thingworxinterface.RESOURCE": "/Thingworx/WS", + "thingworxinterface.APPKEY": "", + "thingworxinterface.ALLOW_SELF_SIGNED_CERTIFICATE": false, + "thingworxinterface.TRUST_ALL_CERTIFICATES": false, + "thingworxinterface.DISABLE_ENCRYPTION": false, + "thingworxinterface.MAX_THING_COUNT": 500, + "thingworxinterface.THING_NAME": "Kepware Server", + "thingworxinterface.PUBLISH_FLOOR_MSEC": 1000, + "thingworxinterface.LOGGING_ENABLED": false, + "thingworxinterface.LOG_LEVEL": 3, + "thingworxinterface.VERBOSE": false, + "thingworxinterface.STORE_AND_FORWARD_ENABLED": false, + "thingworxinterface.STORAGE_PATH": "C:\\ProgramData\\PTC\\Kepware Server\\V7", + "thingworxinterface.DATASTORE_MAXSIZE": 2048, + "thingworxinterface.FORWARD_MODE": 0, + "thingworxinterface.DATASTORE_ID": 421728385, + "thingworxinterface.DELAY_BETWEEN_PUBLISHES": 0, + "thingworxinterface.MAX_UPDATES_PER_PUBLISH": 25000, + "thingworxinterface.PROXY_ENABLED": false, + "thingworxinterface.PROXY_HOST": "localhost", + "thingworxinterface.PROXY_PORT": 3128, + "thingworxinterface.PROXY_USERNAME": "", + "thingworxinterface.PROXY_PASSWORD": "" +} \ No newline at end of file diff --git a/Kepware.Api.Test/_data/projectLoadSerializeData/simulationExamples.json b/Kepware.Api.Test/_data/projectLoadSerializeData/simulationExamples.json new file mode 100644 index 0000000..9729550 --- /dev/null +++ b/Kepware.Api.Test/_data/projectLoadSerializeData/simulationExamples.json @@ -0,0 +1,250 @@ +{ + "channels": { + "common.ALLTYPES_NAME": "Simulation Examples", + "common.ALLTYPES_DESCRIPTION": "Example Simulator Channel", + "servermain.MULTIPLE_TYPES_DEVICE_DRIVER": "Simulator", + "servermain.CHANNEL_DIAGNOSTICS_CAPTURE": false, + "servermain.CHANNEL_UNIQUE_ID": 2691320731, + "servermain.CHANNEL_WRITE_OPTIMIZATIONS_METHOD": 2, + "servermain.CHANNEL_WRITE_OPTIMIZATIONS_DUTY_CYCLE": 10, + "servermain.CHANNEL_NON_NORMALIZED_FLOATING_POINT_HANDLING": 0, + "simulator.CHANNEL_ITEM_PERSISTENCE": false, + "simulator.CHANNEL_ITEM_PERSISTENCE_DATA_FILE": "C:\\ProgramData\\PTC\\Kepware Server\\V7\\Simulator\\Simulation Examples.dat", + "devices": [ + { + "common.ALLTYPES_NAME": "Functions", + "common.ALLTYPES_DESCRIPTION": "Example Simulator Device", + "servermain.MULTIPLE_TYPES_DEVICE_DRIVER": "Simulator", + "servermain.DEVICE_MODEL": 0, + "servermain.DEVICE_UNIQUE_ID": 2266623120, + "servermain.DEVICE_ID_FORMAT": 1, + "servermain.DEVICE_ID_STRING": "4", + "servermain.DEVICE_ID_HEXADECIMAL": 4, + "servermain.DEVICE_ID_DECIMAL": 4, + "servermain.DEVICE_ID_OCTAL": 4, + "servermain.DEVICE_DATA_COLLECTION": true, + "servermain.DEVICE_SCAN_MODE": 0, + "servermain.DEVICE_SCAN_MODE_RATE_MS": 1000, + "servermain.DEVICE_SCAN_MODE_PROVIDE_INITIAL_UPDATES_FROM_CACHE": false, + "tags": [ + { + "common.ALLTYPES_NAME": "Ramp1", + "common.ALLTYPES_DESCRIPTION": "Value increments by 4 from 35 to 100 every 120 ms", + "servermain.TAG_ADDRESS": "RAMP (120, 35, 100, 4)", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Ramp2", + "common.ALLTYPES_DESCRIPTION": "Value decrements by 0.25 from 200.50 to 150.75 every 300 ms", + "servermain.TAG_ADDRESS": "RAMP (300, 150.750000, 200.500000, -0.250000)", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Ramp3", + "common.ALLTYPES_DESCRIPTION": "Value increments by 1 from 0 to 1000 every 250 ms", + "servermain.TAG_ADDRESS": "RAMP (250, 0, 1000, 1)", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Ramp4", + "common.ALLTYPES_DESCRIPTION": "Value decrements by 5 from 1000 to -1000 every 2000 ms", + "servermain.TAG_ADDRESS": "RAMP (2000, -1000, 1000, -5)", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Ramp5", + "common.ALLTYPES_DESCRIPTION": "Value decrements by 500 from 1000000 to -1000000 every 250 ms", + "servermain.TAG_ADDRESS": "RAMP (250, -1000000, 1000000, -500)", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Ramp6", + "common.ALLTYPES_DESCRIPTION": "Value increments by 1250 from 0 to 1 billion every 1000 ms", + "servermain.TAG_ADDRESS": "RAMP (1000, 0, 1000000000, 1250)", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Ramp7", + "common.ALLTYPES_DESCRIPTION": "Value decrements by 1 billion to -1 billion every 1000 ms", + "servermain.TAG_ADDRESS": "RAMP (1000, -1000000000, 1000000000, -5555)", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Ramp8", + "common.ALLTYPES_DESCRIPTION": "Value decrements by 0.25 from 200.50 to 150.75 every 300 ms", + "servermain.TAG_ADDRESS": "RAMP (1000, 150.750000, 200.500000, -0.250000)", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Random1", + "common.ALLTYPES_DESCRIPTION": "Random values from -20 to 75 that change every 30 ms", + "servermain.TAG_ADDRESS": "RANDOM (30, -20, 75)", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Random2", + "common.ALLTYPES_DESCRIPTION": "Random values from 0 to 1000 that change every 100 ms", + "servermain.TAG_ADDRESS": "RANDOM (100, 0, 1000)", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Random3", + "common.ALLTYPES_DESCRIPTION": "Random values from -1000 to 0 that change every 100 ms", + "servermain.TAG_ADDRESS": "RANDOM (100, -1000, 0)", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Random4", + "common.ALLTYPES_DESCRIPTION": "Random values from -999 to 999 that change every 1000 ms", + "servermain.TAG_ADDRESS": "RANDOM (1000, -999, 999)", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Random5", + "common.ALLTYPES_DESCRIPTION": "Random values from -1000000000 to 1000000000 that change every 100 ms", + "servermain.TAG_ADDRESS": "RANDOM (100, -1000000000, 1000000000)", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Random6", + "common.ALLTYPES_DESCRIPTION": "Random values from -1000000 to 1000000 that change every 1000 ms", + "servermain.TAG_ADDRESS": "RANDOM (1000, -1000000, 1000000)", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Random7", + "common.ALLTYPES_DESCRIPTION": "Random values from 0 to 1000000000 that change every 1000 ms", + "servermain.TAG_ADDRESS": "RANDOM (1000, 0, 1000000000)", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Random8", + "common.ALLTYPES_DESCRIPTION": "Random values from 0 to 1000000 that change every 100 ms", + "servermain.TAG_ADDRESS": "RANDOM (100, 0, 1000000)", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Sine1", + "common.ALLTYPES_DESCRIPTION": "Sine values between -40 and 40 at 0.05 Hz with 0 phase shift", + "servermain.TAG_ADDRESS": "SINE (10, -40.000000, 40.000000, 0.050000, 0)", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Sine2", + "common.ALLTYPES_DESCRIPTION": "Sine values between -40 and 40 at 0.05 Hz with 180 phase shift", + "servermain.TAG_ADDRESS": "SINE (10, -40.000000, 40.000000, 0.050000, 180)", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Sine3", + "common.ALLTYPES_DESCRIPTION": "Sine values between -40 and 40 at 0.1 Hz with 0 phase shift", + "servermain.TAG_ADDRESS": "SINE (10, -40.000000, 40.000000, 0.100000, 0)", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Sine4", + "common.ALLTYPES_DESCRIPTION": "Sine values between -40 and 40 at 0.1 Hz with 360 phase shift", + "servermain.TAG_ADDRESS": "SINE (10, -40.000000, 40.000000, 0.100000, 360)", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "User1", + "common.ALLTYPES_DESCRIPTION": "Sequential string values that change every 1000 ms", + "servermain.TAG_ADDRESS": "USER (1000,Hello,world!,This,is,a,test.)", + "servermain.TAG_DATA_TYPE": 0, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "User2", + "common.ALLTYPES_DESCRIPTION": "Sequential float values that change every 250 ms", + "servermain.TAG_ADDRESS": "USER (250,15.16,23.42,4.8)", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "User3", + "common.ALLTYPES_DESCRIPTION": "Sequential Boolean values that change every 200 ms", + "servermain.TAG_ADDRESS": "USER (200,1,0,0,1,0,0,1,0,0,0,1,0,1,0,1,0,0,0,1,0,1,0,1,0,0,0)", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "User4", + "common.ALLTYPES_DESCRIPTION": "A comma is a delimiter unless it is preceded with a backslash", + "servermain.TAG_ADDRESS": "USER (1500,To display a comma\\, place,a backslash in front of it.)", + "servermain.TAG_DATA_TYPE": 0, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + } + ] + } +} \ No newline at end of file diff --git a/Kepware.Api.Test/_data/simdemo_en-us.json b/Kepware.Api.Test/_data/simdemo_en-us.json index ce4243c..a1069e4 100644 --- a/Kepware.Api.Test/_data/simdemo_en-us.json +++ b/Kepware.Api.Test/_data/simdemo_en-us.json @@ -2,6 +2,8 @@ "project": { "common.ALLTYPES_DESCRIPTION": "Example project utilizing Simulator Driver.", "servermain.PROJECT_TITLE": "Simulation Driver Demo", + "servermain.CREATE_USER": "", + "servermain.CREATE_TIME": 1773769017, "channels": [ { "common.ALLTYPES_NAME": "Channel1", @@ -13,7 +15,7 @@ "servermain.CHANNEL_WRITE_OPTIMIZATIONS_DUTY_CYCLE": 10, "servermain.CHANNEL_NON_NORMALIZED_FLOATING_POINT_HANDLING": 0, "simulator.CHANNEL_ITEM_PERSISTENCE": false, - "simulator.CHANNEL_ITEM_PERSISTENCE_DATA_FILE": "C:\\ProgramData\\Kepware\\KEPServerEX\\V6\\Simulator\\Channel1.dat", + "simulator.CHANNEL_ITEM_PERSISTENCE_DATA_FILE": "C:\\ProgramData\\PTC\\Kepware Server\\V7\\Simulator\\Channel1.dat", "devices": [ { "common.ALLTYPES_NAME": "Device1", @@ -63,7 +65,7 @@ "servermain.CHANNEL_WRITE_OPTIMIZATIONS_DUTY_CYCLE": 10, "servermain.CHANNEL_NON_NORMALIZED_FLOATING_POINT_HANDLING": 0, "simulator.CHANNEL_ITEM_PERSISTENCE": false, - "simulator.CHANNEL_ITEM_PERSISTENCE_DATA_FILE": "C:\\ProgramData\\Kepware\\KEPServerEX\\V6\\Simulator\\Data Type Examples.dat", + "simulator.CHANNEL_ITEM_PERSISTENCE_DATA_FILE": "C:\\ProgramData\\PTC\\Kepware Server\\V7\\Simulator\\Data Type Examples.dat", "devices": [ { "common.ALLTYPES_NAME": "16 Bit Device", @@ -2095,6 +2097,2954 @@ } ] }, + { + "common.ALLTYPES_NAME": "OptRecursionTest", + "servermain.MULTIPLE_TYPES_DEVICE_DRIVER": "Simulator", + "servermain.CHANNEL_DIAGNOSTICS_CAPTURE": false, + "servermain.CHANNEL_UNIQUE_ID": 2764480803, + "servermain.CHANNEL_WRITE_OPTIMIZATIONS_METHOD": 2, + "servermain.CHANNEL_WRITE_OPTIMIZATIONS_DUTY_CYCLE": 10, + "servermain.CHANNEL_NON_NORMALIZED_FLOATING_POINT_HANDLING": 0, + "simulator.CHANNEL_ITEM_PERSISTENCE": false, + "simulator.CHANNEL_ITEM_PERSISTENCE_DATA_FILE": "C:\\ProgramData\\PTC\\Kepware Server\\V7\\Simulator\\OptRecursionTest.dat", + "devices": [ + { + "common.ALLTYPES_NAME": "RecursionTestDevice", + "servermain.MULTIPLE_TYPES_DEVICE_DRIVER": "Simulator", + "servermain.DEVICE_MODEL": 0, + "servermain.DEVICE_UNIQUE_ID": 2797076252, + "servermain.DEVICE_ID_FORMAT": 1, + "servermain.DEVICE_ID_STRING": "1", + "servermain.DEVICE_ID_HEXADECIMAL": 1, + "servermain.DEVICE_ID_DECIMAL": 1, + "servermain.DEVICE_ID_OCTAL": 1, + "servermain.DEVICE_DATA_COLLECTION": true, + "servermain.DEVICE_SCAN_MODE": 0, + "servermain.DEVICE_SCAN_MODE_RATE_MS": 1000, + "servermain.DEVICE_SCAN_MODE_PROVIDE_INITIAL_UPDATES_FROM_CACHE": false, + "tag_groups": [ + { + "common.ALLTYPES_NAME": "B Registers", + "common.ALLTYPES_DESCRIPTION": "Boolean registers", + "tags": [ + { + "common.ALLTYPES_NAME": "Boolean1", + "common.ALLTYPES_DESCRIPTION": "Boolean register", + "servermain.TAG_ADDRESS": "B0001", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean2", + "common.ALLTYPES_DESCRIPTION": "Boolean register", + "servermain.TAG_ADDRESS": "B0002", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean3", + "common.ALLTYPES_DESCRIPTION": "Boolean register", + "servermain.TAG_ADDRESS": "B0003", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean4", + "common.ALLTYPES_DESCRIPTION": "Boolean register", + "servermain.TAG_ADDRESS": "B0004", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "BooleanArray", + "common.ALLTYPES_DESCRIPTION": "Array of 4 Boolean Registers", + "servermain.TAG_ADDRESS": "B0010 [4]", + "servermain.TAG_DATA_TYPE": 21, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + }, + { + "common.ALLTYPES_NAME": "K Registers", + "common.ALLTYPES_DESCRIPTION": "Constant Registers", + "tags": [ + { + "common.ALLTYPES_NAME": "Boolean1", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.00", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean2", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.01", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean3", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.02", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean4", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.03", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double1", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0400", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double2", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0404", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double3", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0408", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double4", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0412", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DoubleArray", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0440 [4]", + "servermain.TAG_DATA_TYPE": 29, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord1", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0500", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord2", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0502", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord3", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0504", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord4", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0506", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWordArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0550 [4]", + "servermain.TAG_DATA_TYPE": 27, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float1", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0600", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float2", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0602", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float3", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0604", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float4", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0606", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "FloatArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0660 [4]", + "servermain.TAG_DATA_TYPE": 28, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong1", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1200", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong2", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1204", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong3", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1208", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong4", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1212", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLongArray", + "common.ALLTYPES_DESCRIPTION": "64-bit signed integer array", + "servermain.TAG_ADDRESS": "K1240 [4]", + "servermain.TAG_DATA_TYPE": 33, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long1", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0700", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long2", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0702", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long3", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0704", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long4", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0706", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LongArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0770 [4]", + "servermain.TAG_DATA_TYPE": 26, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord1", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1100", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord2", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1104", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord3", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1108", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord4", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1112", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWordArray", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned Integer array", + "servermain.TAG_ADDRESS": "K1140 [4]", + "servermain.TAG_DATA_TYPE": 34, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short1", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0800", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short2", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0801", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short3", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0802", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short4", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0803", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "ShortArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0880 [4]", + "servermain.TAG_DATA_TYPE": 24, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word1", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0900", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word2", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0901", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word3", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0902", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word4", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0903", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "WordArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0990 [4]", + "servermain.TAG_DATA_TYPE": 25, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + }, + { + "common.ALLTYPES_NAME": "R Registers", + "common.ALLTYPES_DESCRIPTION": "Ramping Registers", + "tags": [ + { + "common.ALLTYPES_NAME": "Boolean1", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "R0100.00", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean2", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "R0100.01", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean3", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "R0100.02", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean4", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "R0100.03", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double1", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0400", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double2", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0404", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double3", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0408", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double4", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0412", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DoubleArray", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "R0440 [4]", + "servermain.TAG_DATA_TYPE": 29, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord1", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0500", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord2", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0502", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord3", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0504", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord4", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0506", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWordArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "R0550 [4]", + "servermain.TAG_DATA_TYPE": 27, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float1", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0600", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float2", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0602", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float3", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0604", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float4", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0606", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "FloatArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "R0660 [4]", + "servermain.TAG_DATA_TYPE": 28, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong1", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "R1200", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong2", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "R1204", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong3", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "R1208", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong4", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "R1212", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLongArray", + "common.ALLTYPES_DESCRIPTION": "64-bit signed integer array", + "servermain.TAG_ADDRESS": "R1240 [4]", + "servermain.TAG_DATA_TYPE": 33, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long1", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "R0700", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long2", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "R0702", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long3", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "R0704", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long4", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "R0706", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LongArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer array", + "servermain.TAG_ADDRESS": "R0770 [4]", + "servermain.TAG_DATA_TYPE": 26, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord1", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "R1100", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord2", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "R1104", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord3", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "R1108", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord4", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "R1112", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWordArray", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned Integer array", + "servermain.TAG_ADDRESS": "R1140 [4]", + "servermain.TAG_DATA_TYPE": 34, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short1", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "R0800", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short2", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "R0801", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short3", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "R0802", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short4", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "R0803", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "ShortArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer array", + "servermain.TAG_ADDRESS": "R0880 [4]", + "servermain.TAG_DATA_TYPE": 24, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word1", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0900", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word2", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0901", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word3", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0902", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word4", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0903", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "WordArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "R0990 [4]", + "servermain.TAG_DATA_TYPE": 25, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + }, + { + "common.ALLTYPES_NAME": "RecursionTest", + "tag_groups": [ + { + "common.ALLTYPES_NAME": "Level1", + "tags": [ + { + "common.ALLTYPES_NAME": "Boolean1", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.00", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean2", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.01", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean3", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.02", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean4", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.03", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double1", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0400", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double2", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0404", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double3", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0408", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double4", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0412", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DoubleArray", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0440 [4]", + "servermain.TAG_DATA_TYPE": 29, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord1", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0500", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord2", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0502", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord3", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0504", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord4", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0506", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWordArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0550 [4]", + "servermain.TAG_DATA_TYPE": 27, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float1", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0600", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float2", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0602", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float3", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0604", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float4", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0606", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "FloatArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0660 [4]", + "servermain.TAG_DATA_TYPE": 28, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong1", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1200", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong2", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1204", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong3", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1208", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong4", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1212", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLongArray", + "common.ALLTYPES_DESCRIPTION": "64-bit signed integer array", + "servermain.TAG_ADDRESS": "K1240 [4]", + "servermain.TAG_DATA_TYPE": 33, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long1", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0700", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long2", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0702", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long3", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0704", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long4", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0706", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LongArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0770 [4]", + "servermain.TAG_DATA_TYPE": 26, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord1", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1100", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord2", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1104", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord3", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1108", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord4", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1112", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWordArray", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned Integer array", + "servermain.TAG_ADDRESS": "K1140 [4]", + "servermain.TAG_DATA_TYPE": 34, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short1", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0800", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short2", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0801", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short3", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0802", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short4", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0803", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "ShortArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0880 [4]", + "servermain.TAG_DATA_TYPE": 24, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word1", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0900", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word2", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0901", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word3", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0902", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word4", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0903", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "WordArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0990 [4]", + "servermain.TAG_DATA_TYPE": 25, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ], + "tag_groups": [ + { + "common.ALLTYPES_NAME": "Level1_1", + "tags": [ + { + "common.ALLTYPES_NAME": "Boolean1", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.00", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean2", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.01", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean3", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.02", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean4", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.03", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double1", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0400", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double2", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0404", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double3", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0408", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double4", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0412", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DoubleArray", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0440 [4]", + "servermain.TAG_DATA_TYPE": 29, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord1", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0500", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord2", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0502", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord3", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0504", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord4", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0506", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWordArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0550 [4]", + "servermain.TAG_DATA_TYPE": 27, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float1", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0600", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float2", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0602", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float3", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0604", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float4", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0606", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "FloatArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0660 [4]", + "servermain.TAG_DATA_TYPE": 28, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong1", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1200", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong2", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1204", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong3", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1208", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong4", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1212", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLongArray", + "common.ALLTYPES_DESCRIPTION": "64-bit signed integer array", + "servermain.TAG_ADDRESS": "K1240 [4]", + "servermain.TAG_DATA_TYPE": 33, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long1", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0700", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long2", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0702", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long3", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0704", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long4", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0706", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LongArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0770 [4]", + "servermain.TAG_DATA_TYPE": 26, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord1", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1100", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord2", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1104", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord3", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1108", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord4", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1112", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWordArray", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned Integer array", + "servermain.TAG_ADDRESS": "K1140 [4]", + "servermain.TAG_DATA_TYPE": 34, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short1", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0800", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short2", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0801", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short3", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0802", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short4", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0803", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "ShortArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0880 [4]", + "servermain.TAG_DATA_TYPE": 24, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word1", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0900", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word2", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0901", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word3", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0902", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word4", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0903", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "WordArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0990 [4]", + "servermain.TAG_DATA_TYPE": 25, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + } + ] + }, + { + "common.ALLTYPES_NAME": "Level2", + "tags": [ + { + "common.ALLTYPES_NAME": "Boolean1", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.00", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean2", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.01", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean3", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.02", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean4", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.03", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double1", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0400", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double2", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0404", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double3", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0408", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double4", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0412", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DoubleArray", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0440 [4]", + "servermain.TAG_DATA_TYPE": 29, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord1", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0500", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord2", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0502", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord3", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0504", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord4", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0506", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWordArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0550 [4]", + "servermain.TAG_DATA_TYPE": 27, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float1", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0600", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float2", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0602", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float3", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0604", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float4", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0606", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "FloatArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0660 [4]", + "servermain.TAG_DATA_TYPE": 28, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong1", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1200", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong2", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1204", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong3", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1208", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong4", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1212", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLongArray", + "common.ALLTYPES_DESCRIPTION": "64-bit signed integer array", + "servermain.TAG_ADDRESS": "K1240 [4]", + "servermain.TAG_DATA_TYPE": 33, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long1", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0700", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long2", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0702", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long3", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0704", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long4", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0706", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LongArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0770 [4]", + "servermain.TAG_DATA_TYPE": 26, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord1", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1100", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord2", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1104", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord3", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1108", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord4", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1112", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWordArray", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned Integer array", + "servermain.TAG_ADDRESS": "K1140 [4]", + "servermain.TAG_DATA_TYPE": 34, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short1", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0800", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short2", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0801", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short3", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0802", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short4", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0803", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "ShortArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0880 [4]", + "servermain.TAG_DATA_TYPE": 24, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word1", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0900", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word2", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0901", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word3", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0902", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word4", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0903", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "WordArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0990 [4]", + "servermain.TAG_DATA_TYPE": 25, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + }, + { + "common.ALLTYPES_NAME": "Level3", + "tags": [ + { + "common.ALLTYPES_NAME": "Boolean1", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.00", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean2", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.01", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean3", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.02", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean4", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.03", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double1", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0400", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double2", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0404", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double3", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0408", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double4", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0412", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DoubleArray", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0440 [4]", + "servermain.TAG_DATA_TYPE": 29, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord1", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0500", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord2", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0502", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord3", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0504", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord4", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0506", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWordArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0550 [4]", + "servermain.TAG_DATA_TYPE": 27, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float1", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0600", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float2", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0602", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float3", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0604", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float4", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0606", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "FloatArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0660 [4]", + "servermain.TAG_DATA_TYPE": 28, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong1", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1200", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong2", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1204", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong3", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1208", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong4", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1212", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLongArray", + "common.ALLTYPES_DESCRIPTION": "64-bit signed integer array", + "servermain.TAG_ADDRESS": "K1240 [4]", + "servermain.TAG_DATA_TYPE": 33, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long1", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0700", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long2", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0702", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long3", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0704", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long4", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0706", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LongArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0770 [4]", + "servermain.TAG_DATA_TYPE": 26, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord1", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1100", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord2", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1104", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord3", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1108", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord4", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1112", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWordArray", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned Integer array", + "servermain.TAG_ADDRESS": "K1140 [4]", + "servermain.TAG_DATA_TYPE": 34, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short1", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0800", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short2", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0801", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short3", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0802", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short4", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0803", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "ShortArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0880 [4]", + "servermain.TAG_DATA_TYPE": 24, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word1", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0900", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word2", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0901", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word3", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0902", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word4", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0903", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "WordArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0990 [4]", + "servermain.TAG_DATA_TYPE": 25, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + }, + { + "common.ALLTYPES_NAME": "Level4", + "tags": [ + { + "common.ALLTYPES_NAME": "Boolean1", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.00", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean2", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.01", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean3", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.02", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean4", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.03", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double1", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0400", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double2", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0404", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double3", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0408", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double4", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0412", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DoubleArray", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0440 [4]", + "servermain.TAG_DATA_TYPE": 29, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord1", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0500", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord2", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0502", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord3", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0504", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord4", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0506", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWordArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0550 [4]", + "servermain.TAG_DATA_TYPE": 27, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float1", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0600", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float2", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0602", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float3", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0604", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float4", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0606", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "FloatArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0660 [4]", + "servermain.TAG_DATA_TYPE": 28, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong1", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1200", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong2", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1204", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong3", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1208", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong4", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1212", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLongArray", + "common.ALLTYPES_DESCRIPTION": "64-bit signed integer array", + "servermain.TAG_ADDRESS": "K1240 [4]", + "servermain.TAG_DATA_TYPE": 33, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long1", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0700", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long2", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0702", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long3", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0704", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long4", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0706", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LongArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0770 [4]", + "servermain.TAG_DATA_TYPE": 26, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord1", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1100", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord2", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1104", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord3", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1108", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord4", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1112", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWordArray", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned Integer array", + "servermain.TAG_ADDRESS": "K1140 [4]", + "servermain.TAG_DATA_TYPE": 34, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short1", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0800", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short2", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0801", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short3", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0802", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short4", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0803", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "ShortArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0880 [4]", + "servermain.TAG_DATA_TYPE": 24, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word1", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0900", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word2", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0901", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word3", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0902", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word4", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0903", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "WordArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0990 [4]", + "servermain.TAG_DATA_TYPE": 25, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + } + ] + }, + { + "common.ALLTYPES_NAME": "S Registers", + "common.ALLTYPES_DESCRIPTION": "String Registers", + "tags": [ + { + "common.ALLTYPES_NAME": "String1", + "common.ALLTYPES_DESCRIPTION": "Null terminated Unicode string", + "servermain.TAG_ADDRESS": "S0001", + "servermain.TAG_DATA_TYPE": 0, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "String2", + "common.ALLTYPES_DESCRIPTION": "Null terminated Unicode string", + "servermain.TAG_ADDRESS": "S0002", + "servermain.TAG_DATA_TYPE": 0, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "String3", + "common.ALLTYPES_DESCRIPTION": "Null terminated Unicode string", + "servermain.TAG_ADDRESS": "S0003", + "servermain.TAG_DATA_TYPE": 0, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "String4", + "common.ALLTYPES_DESCRIPTION": "Null terminated Unicode string", + "servermain.TAG_ADDRESS": "S0004", + "servermain.TAG_DATA_TYPE": 0, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "StringArray[4]", + "common.ALLTYPES_DESCRIPTION": "Null terminated Unicode string array", + "servermain.TAG_ADDRESS": "S0010 [4]", + "servermain.TAG_DATA_TYPE": 20, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + } + ] + } + ] + }, { "common.ALLTYPES_NAME": "Simulation Examples", "common.ALLTYPES_DESCRIPTION": "Example Simulator Channel", @@ -2105,7 +5055,7 @@ "servermain.CHANNEL_WRITE_OPTIMIZATIONS_DUTY_CYCLE": 10, "servermain.CHANNEL_NON_NORMALIZED_FLOATING_POINT_HANDLING": 0, "simulator.CHANNEL_ITEM_PERSISTENCE": false, - "simulator.CHANNEL_ITEM_PERSISTENCE_DATA_FILE": "C:\\ProgramData\\Kepware\\KEPServerEX\\V6\\Simulator\\Simulation Examples.dat", + "simulator.CHANNEL_ITEM_PERSISTENCE_DATA_FILE": "C:\\ProgramData\\PTC\\Kepware Server\\V7\\Simulator\\Simulation Examples.dat", "devices": [ { "common.ALLTYPES_NAME": "Functions", @@ -2361,6 +5311,21 @@ "aeserverinterface.MIN_SUBSCRIPTION_BUFFER_TIME_MS": 1000, "aeserverinterface.MIN_KEEP_ALIVE_TIME_MS": 1000 }, + { + "common.ALLTYPES_NAME": "ddeserver", + "ddeserver.ENABLE": false, + "ddeserver.SERVICE_NAME": "ptcdde", + "ddeserver.ADVANCED_DDE": true, + "ddeserver.XLTABLE": true, + "ddeserver.CF_TEXT": true, + "ddeserver.CLIENT_UPDATE_INTERVAL_MS": 100, + "ddeserver.REQUEST_TIMEOUT_SEC": 15 + }, + { + "common.ALLTYPES_NAME": "hdaserver", + "hdaserver.ENABLE": false, + "hdaserver.ENABLE_DIAGNOSTICS": false + }, { "common.ALLTYPES_NAME": "opcdaserver", "opcdaserver.PROJECT_OPC_DA_1_ENABLED": true, @@ -2394,13 +5359,13 @@ "thingworxinterface.TRUST_ALL_CERTIFICATES": false, "thingworxinterface.DISABLE_ENCRYPTION": false, "thingworxinterface.MAX_THING_COUNT": 500, - "thingworxinterface.THING_NAME": "KEPServerEX", + "thingworxinterface.THING_NAME": "Kepware Server", "thingworxinterface.PUBLISH_FLOOR_MSEC": 1000, "thingworxinterface.LOGGING_ENABLED": false, "thingworxinterface.LOG_LEVEL": 3, "thingworxinterface.VERBOSE": false, "thingworxinterface.STORE_AND_FORWARD_ENABLED": false, - "thingworxinterface.STORAGE_PATH": "C:\\ProgramData\\Kepware\\KEPServerEX\\V6\\", + "thingworxinterface.STORAGE_PATH": "C:\\ProgramData\\PTC\\Kepware Server\\V7", "thingworxinterface.DATASTORE_MAXSIZE": 2048, "thingworxinterface.FORWARD_MODE": 0, "thingworxinterface.DATASTORE_ID": 421728385, @@ -2426,6 +5391,59 @@ "uaserverinterface.PROJECT_OPC_UA_MAX_DATA_QUEUE_SIZE": 2, "uaserverinterface.PROJECT_OPC_UA_MAX_RETRANSMIT_QUEUE_SIZE": 10, "uaserverinterface.PROJECT_OPC_UA_MAX_NOTIFICATION_PER_PUBLISH": 65536 + }, + { + "common.ALLTYPES_NAME": "wwtoolkitinterface", + "wwtoolkitinterface.ENABLED": false, + "wwtoolkitinterface.SERVICE_NAME": "server_runtime", + "wwtoolkitinterface.CLIENT_UPDATE_INTERVAL_MS": 100 + } + ], + "_ua_gateway": [ + { + "common.ALLTYPES_NAME": "_UA_Gateway", + "common.ALLTYPES_DESCRIPTION": "The parent interface of the OPC UA Gateway. Configuring this interface and managing the Instance Certificate view will allow communication between the UA Gateway service, its children interfaces, and the rest of Kepware+.", + "ua_client_interfaces": [ + { + "common.ALLTYPES_NAME": "Client Interface", + "common.ALLTYPES_DESCRIPTION": "The client interface of the OPC UA Gateway. Configuring this interface and adding client connections will allow communication between the OPC UA Gateway and other OPC UA servers.", + "client_instance_certificates": [ + { + "common.ALLTYPES_NAME": "Client Instance Certificate", + "common.ALLTYPES_DESCRIPTION": "The Client Interface Instance Certificate.", + "ua_gateway.UA_DISTINGUISHED_NAMES": "CN = PTC OPC UA Gateway Client Interface\n" + } + ] + } + ], + "ua_server_interfaces": [ + { + "common.ALLTYPES_NAME": "Server Interface", + "common.ALLTYPES_DESCRIPTION": "The server interface of the OPC UA Gateway. Configuring this interface and adding server endpoints will allow other OPC UA clients to connect to the OPC UA Gateway.", + "ua_gateway.UA_SERVER_INTERFACE_USER_IDENTITY_POLICY_ANONYMOUS": false, + "ua_gateway.UA_SERVER_INTERFACE_USER_IDENTITY_POLICY_USERNAME_PASSWORD": true, + "ua_gateway.UA_SERVER_INTERFACE_USER_IDENTITY_POLICY_X509": true, + "ua_gateway.UA_SERVER_INTERFACE_SECURITY_POLICIES_NONE": false, + "ua_gateway.UA_SERVER_INTERFACE_SECURITY_POLICIES_BASIC256SHA256": 2, + "ua_gateway.UA_SERVER_INTERFACE_SECURITY_POLICIES_AES128_SHA256_RSAOAEP": 0, + "ua_gateway.UA_SERVER_INTERFACE_SECURITY_POLICIES_AES256_SHA256_RSAPSS": 0, + "ua_gateway.LDS_REGISTRATION_ENABLED": false, + "ua_gateway.LDS_MAX_REGISTRATION_INTERVAL": 30000, + "ua_gateway.UA_SERVER_INTERFACE_MAX_SUBSCRIPTION_LIFETIME": 3600000, + "ua_gateway.UA_SERVER_INTERFACE_MIN_SUBSCRIPTION_LIFETIME": 10000, + "ua_gateway.UA_SERVER_INTERFACE_MAX_SESSION_TIMEOUT": 3600000, + "ua_gateway.UA_SERVER_INTERFACE_MIN_SESSION_TIMEOUT": 10000, + "ua_gateway.UA_SERVER_INTERFACE_MAX_NOTIFICATIONS_PER_PUBLISH": 1000, + "ua_gateway.UA_SERVER_INTERFACE_MAX_NOTIFICATIONS_QUEUE_SIZE": 100, + "server_instance_certificates": [ + { + "common.ALLTYPES_NAME": "Server Instance Certificate", + "common.ALLTYPES_DESCRIPTION": "The Server Interface Instance Certificate.", + "ua_gateway.UA_DISTINGUISHED_NAMES": "CN = PTC OPC UA Gateway Server Interface\n" + } + ] + } + ] } ] } diff --git a/Kepware.Api.TestIntg/ApiClient/GetProductInfo.cs b/Kepware.Api.TestIntg/ApiClient/GetProductInfoTests.cs similarity index 57% rename from Kepware.Api.TestIntg/ApiClient/GetProductInfo.cs rename to Kepware.Api.TestIntg/ApiClient/GetProductInfoTests.cs index d609c6a..d35505d 100644 --- a/Kepware.Api.TestIntg/ApiClient/GetProductInfo.cs +++ b/Kepware.Api.TestIntg/ApiClient/GetProductInfoTests.cs @@ -30,6 +30,15 @@ public async Task GetProductInfoAsync_ShouldReturnProductInfo_WhenApiRespondsSuc Assert.Equal(_productInfo.ProductVersionBuild, result.ProductVersionBuild); Assert.Equal(_productInfo.ProductVersionPatch, result.ProductVersionPatch); + // Also verify that the ProductInfo property on the client is updated + Assert.NotNull(_kepwareApiClient.ProductInfo); + Assert.Equal(_productInfo.ProductId, _kepwareApiClient.ProductInfo.ProductId); + Assert.Equal(_productInfo.ProductName, _kepwareApiClient.ProductInfo.ProductName); + Assert.Equal(_productInfo.ProductVersion, _kepwareApiClient.ProductInfo.ProductVersion); + Assert.Equal(_productInfo.ProductVersionMajor, _kepwareApiClient.ProductInfo.ProductVersionMajor); + Assert.Equal(_productInfo.ProductVersionMinor, _kepwareApiClient.ProductInfo.ProductVersionMinor); + Assert.Equal(_productInfo.ProductVersionBuild, _kepwareApiClient.ProductInfo.ProductVersionBuild); + Assert.Equal(_productInfo.ProductVersionPatch, _kepwareApiClient.ProductInfo.ProductVersionPatch); } } diff --git a/Kepware.Api.TestIntg/ApiClient/InsertTests.cs b/Kepware.Api.TestIntg/ApiClient/InsertTests.cs index a74ead6..61ee9f3 100644 --- a/Kepware.Api.TestIntg/ApiClient/InsertTests.cs +++ b/Kepware.Api.TestIntg/ApiClient/InsertTests.cs @@ -84,7 +84,7 @@ public async Task Insert_MultipleItems_WithPartialSuccess_ShouldReturnMixedResul } [Fact] - public async Task Insert_MultipleItems_WithUnsupportedDriver_ShouldSkipItems() + public async Task Insert_MultipleItems_WithUnsupportedDriver_ShouldItems() { // Arrange var channel1 = CreateTestChannel("Channel1", "UnsupportedDriver"); @@ -95,8 +95,9 @@ public async Task Insert_MultipleItems_WithUnsupportedDriver_ShouldSkipItems() var results = await _kepwareApiClient.GenericConfig.InsertItemsAsync(channels); // Assert - results.Length.ShouldBe(1); // Only the Simulator channel should be inserted - results[0].ShouldBeTrue(); + results.Length.ShouldBe(2); // Both channels have results + results[0].ShouldBeFalse(); // UnsupportedDriver should fail + results[1].ShouldBeTrue(); // Simulator should succeed // Clean up await DeleteAllChannelsAsync(); diff --git a/Kepware.Api.TestIntg/ApiClient/IotGatewayTests.cs b/Kepware.Api.TestIntg/ApiClient/IotGatewayTests.cs new file mode 100644 index 0000000..ab60c58 --- /dev/null +++ b/Kepware.Api.TestIntg/ApiClient/IotGatewayTests.cs @@ -0,0 +1,973 @@ +using Kepware.Api.ClientHandler; +using Kepware.Api.Model; +using Kepware.Api.Serializer; +using Microsoft.Extensions.Logging; +using Moq; +using Moq.Contrib.HttpClient; +using Shouldly; +using System.Net; +using System.Text.Json; + +namespace Kepware.Api.TestIntg.ApiClient; + +public class IotGatewayTests : TestIntgApiClientBase +{ + + #region MQTT Client Agent Tests + + [Fact] + public async Task GetOrCreateMqttClientAgent_WhenNotExists_ShouldCreateAgent() + { + + // Act + var result = await _kepwareApiClient.Project.IotGateway.GetOrCreateMqttClientAgentAsync("TestMqttAgent"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("TestMqttAgent"); + + // Clean up + await DeleteAllIoTAgentsAsync(); + + } + + [Fact] + public async Task GetOrCreateMqttClientAgent_WhenExists_ShouldReturnExistingAgent() + { + // Arrange + var agentProperties = new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("true").RootElement }, + {"iot_gateway.MQTT_CLIENT_URL", JsonDocument.Parse($"\"tcp://localhost:1883\"").RootElement} }; + var agent = await AddTestMqttClientAgent("TestMqttAgent", agentProperties); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.GetOrCreateMqttClientAgentAsync("TestMqttAgent"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe(agent.Name); + result.Url.ShouldBe(agent.Url); + result.Enabled.ShouldBe(agent.Enabled); + + // Clean up + await DeleteAllIoTAgentsAsync(); + + } + + + [Fact] + public async Task CreateMqttClientAgent_WhenSuccessful_ShouldReturnAgent() + { + // Act + var result = await _kepwareApiClient.Project.IotGateway.CreateMqttClientAgentAsync("TestMqttAgent"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("TestMqttAgent"); + + // Clean up + await DeleteAllIoTAgentsAsync(); + } + + [Fact] + public async Task CreateMqttClientAgent_WithProperties_ShouldSetProperties() + { + // Arrange + + var properties = new Dictionary + { + { Properties.MqttClientAgent.Url, "tcp://localhost:1883" }, + { Properties.MqttClientAgent.Topic, "test/topic" } + }; + + // Act + var result = await _kepwareApiClient.Project.IotGateway.CreateMqttClientAgentAsync("TestAgent", properties); + + // Assert + result.ShouldNotBeNull(); + result.Url.ShouldBe("tcp://localhost:1883"); + result.Topic.ShouldBe("test/topic"); + + // Clean up + await DeleteAllIoTAgentsAsync(); + + } + + [Fact] + public async Task CreateMqttClientAgent_WithHttpError_AlreadyExists_ShouldReturnNull() + { + // Arrange + var agentProperties = new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("true").RootElement }, + {"iot_gateway.MQTT_CLIENT_URL", JsonDocument.Parse($"\"tcp://localhost:1883\"").RootElement} }; + var agent = await AddTestMqttClientAgent("TestMqttAgent", agentProperties); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.CreateMqttClientAgentAsync("TestMqttAgent"); + + // Assert + result.ShouldBeNull(); + + // Clean up + await DeleteAllIoTAgentsAsync(); + + } + + [Fact] + public async Task GetMqttClientAgent_WhenExists_ShouldReturnAgent() + { + // Arrange + var agentProperties = new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("true").RootElement }, + {"iot_gateway.MQTT_CLIENT_URL", JsonDocument.Parse($"\"tcp://localhost:1883\"").RootElement} }; + var agent = await AddTestMqttClientAgent("TestMqttAgent", agentProperties); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.GetMqttClientAgentAsync("TestMqttAgent"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("TestMqttAgent"); + result.Enabled.ShouldBe(true); + result.Url.ShouldBe("tcp://localhost:1883"); + + // Clean up + await DeleteAllIoTAgentsAsync(); + + } + + [Fact] + public async Task GetMqttClientAgent_WhenNotFound_ShouldReturnNull() + { + + // Act + var result = await _kepwareApiClient.Project.IotGateway.GetMqttClientAgentAsync("NonExistent"); + + // Assert + result.ShouldBeNull(); + } + + [Fact] + public async Task DeleteMqttClientAgent_ByEntity_WhenSuccessful_ShouldReturnTrue() + { + // Arrange + var agentProperties = new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("true").RootElement }, + {"iot_gateway.MQTT_CLIENT_URL", JsonDocument.Parse($"\"tcp://localhost:1883\"").RootElement} }; + var agent = await AddTestMqttClientAgent("TestMqttAgent", agentProperties); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.DeleteMqttClientAgentAsync(agent); + + // Assert + result.ShouldBeTrue(); + + // Clean up + await DeleteAllIoTAgentsAsync(); + } + + [Fact] + public async Task DeleteMqttClientAgent_ByName_WhenSuccessful_ShouldReturnTrue() + { + // Arrange + var agentProperties = new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("true").RootElement }, + {"iot_gateway.MQTT_CLIENT_URL", JsonDocument.Parse($"\"tcp://localhost:1883\"").RootElement} }; + var agent = await AddTestMqttClientAgent("TestMqttAgent", agentProperties); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.DeleteMqttClientAgentAsync("TestMqttAgent"); + + // Assert + result.ShouldBeTrue(); + + // Clean up + await DeleteAllIoTAgentsAsync(); + + } + + [Fact] + public async Task DeleteMqttClientAgent_WithHttpError_ShouldReturnFalse() + { + + // Act + var result = await _kepwareApiClient.Project.IotGateway.DeleteMqttClientAgentAsync("TestMqttAgent"); + + // Assert + result.ShouldBeFalse(); + } + + + #endregion + + #region REST Client Agent Tests + + + [SkippableFact] + public async Task GetOrCreateRestClientAgent_WhenNotExists_ShouldCreateAgent() + { + // Skip the test if the product is not "Server" productId + Skip.If(_productInfo.ProductId != "012", "Test only applicable for Server productIds"); + // Act + var result = await _kepwareApiClient.Project.IotGateway.GetOrCreateRestClientAgentAsync("TestRestClient"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("TestRestClient"); + + // Clean up + await DeleteAllIoTAgentsAsync(); + + } + + [SkippableFact] + public async Task GetOrCreateRestClientAgent_WhenExists_ShouldReturnExistingAgent() + { + // Skip the test if the product is not "Server" productId + Skip.If(_productInfo.ProductId != "012", "Test only applicable for Server productIds"); + + // Arrange + var agentProperties = new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("true").RootElement }, + {"iot_gateway.REST_CLIENT_URL", JsonDocument.Parse($"\"https://api.example.com\"").RootElement} }; + var agent = await AddTestRestClientAgent("TestRestClient", agentProperties); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.GetOrCreateRestClientAgentAsync("TestRestClient"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe(agent.Name); + result.Url.ShouldBe(agent.Url); + result.Enabled.ShouldBe(agent.Enabled); + + // Clean up + await DeleteAllIoTAgentsAsync(); + + } + + + [SkippableFact] + public async Task CreateRestClientAgent_WhenSuccessful_ShouldReturnAgent() + { + // Skip the test if the product is not "Server" productId + Skip.If(_productInfo.ProductId != "012", "Test only applicable for Server productIds"); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.CreateRestClientAgentAsync("TestRestClient"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("TestRestClient"); + + // Clean up + await DeleteAllIoTAgentsAsync(); + + } + + [SkippableFact] + public async Task CreateRestClientAgent_WithProperties_ShouldSetProperties() + { + // Skip the test if the product is not "Server" productId + Skip.If(_productInfo.ProductId != "012", "Test only applicable for Server productIds"); + + // Arrange + + var properties = new Dictionary + { + { Properties.RestClientAgent.Url, "https://api.example.com" } + }; + + // Act + var result = await _kepwareApiClient.Project.IotGateway.CreateRestClientAgentAsync("TestRestClient", properties); + + // Assert + result.ShouldNotBeNull(); + result.Url.ShouldBe("https://api.example.com"); + + // Clean up + await DeleteAllIoTAgentsAsync(); + + } + + [SkippableFact] + public async Task CreateRestClientAgent_WithHttpError_AlreadyExists_ShouldReturnNull() + { + // Skip the test if the product is not "Server" productId + Skip.If(_productInfo.ProductId != "012", "Test only applicable for Server productIds"); + + // Arrange + var agentProperties = new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("true").RootElement }, + {"iot_gateway.REST_CLIENT_URL", JsonDocument.Parse($"\"https://api.example.com\"").RootElement} }; + var agent = await AddTestRestClientAgent("TestRestClient", agentProperties); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.CreateRestClientAgentAsync("TestRestClient"); + + // Assert + result.ShouldBeNull(); + + // Clean up + await DeleteAllIoTAgentsAsync(); + + } + + + [SkippableFact] + public async Task GetRestClientAgent_WhenExists_ShouldReturnAgent() + { + // Skip the test if the product is not "Server" productId + Skip.If(_productInfo.ProductId != "012", "Test only applicable for Server productIds"); + + // Arrange + var agentProperties = new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("true").RootElement }, + {"iot_gateway.REST_CLIENT_URL", JsonDocument.Parse($"\"https://api.example.com\"").RootElement} }; + var agent = await AddTestRestClientAgent("TestRestClient", agentProperties); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.GetRestClientAgentAsync("TestRestClient"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("TestRestClient"); + result.Url.ShouldBe("https://api.example.com"); + + // Clean up + await DeleteAllIoTAgentsAsync(); + + } + + [SkippableFact] + public async Task GetRestClientAgent_WhenNotFound_ShouldReturnNull() + { + // Skip the test if the product is not "Server" productId + Skip.If(_productInfo.ProductId != "012", "Test only applicable for Server productIds"); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.GetRestClientAgentAsync("NonExistent"); + + // Assert + result.ShouldBeNull(); + } + + [SkippableFact] + public async Task DeleteRestClientAgent_ByEntity_WhenSuccessful_ShouldReturnTrue() + { + // Skip the test if the product is not "Server" productId + Skip.If(_productInfo.ProductId != "012", "Test only applicable for Server productIds"); + + // Arrange + var agentProperties = new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("true").RootElement }, + {"iot_gateway.REST_CLIENT_URL", JsonDocument.Parse($"\"https://api.example.com\"").RootElement} }; + var agent = await AddTestRestClientAgent("TestRestClient", agentProperties); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.DeleteRestClientAgentAsync(agent); + + // Assert + result.ShouldBeTrue(); + + // Clean up + await DeleteAllIoTAgentsAsync(); + + } + + [SkippableFact] + public async Task DeleteRestClientAgent_ByName_WhenSuccessful_ShouldReturnTrue() + { + // Skip the test if the product is not "Server" productId + Skip.If(_productInfo.ProductId != "012", "Test only applicable for Server productIds"); + + // Arrange + var agentProperties = new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("true").RootElement }, + {"iot_gateway.REST_CLIENT_URL", JsonDocument.Parse($"\"https://api.example.com\"").RootElement} }; + var agent = await AddTestRestClientAgent("TestRestClient", agentProperties); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.DeleteRestClientAgentAsync("TestRestClient"); + + // Assert + result.ShouldBeTrue(); + + // Clean up + await DeleteAllIoTAgentsAsync(); + } + + [SkippableFact] + public async Task DeleteRestClientAgent_WithHttpError_ShouldReturnFalse() + { + // Skip the test if the product is not "Server" productId + Skip.If(_productInfo.ProductId != "012", "Test only applicable for Server productIds"); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.DeleteRestClientAgentAsync("TestRestClient"); + + // Assert + result.ShouldBeFalse(); + } + + #endregion + + #region REST Server Agent Tests + + [SkippableFact] + public async Task GetOrCreateRestServerAgent_WhenNotExists_ShouldCreateAgent() + { + // Skip the test if the product is not "Server" productId + Skip.If(_productInfo.ProductId != "012", "Test only applicable for Server productIds"); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.GetOrCreateRestServerAgentAsync("TestRestServer"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("TestRestServer"); + + // Clean up + await DeleteAllIoTAgentsAsync(); + + } + + [SkippableFact] + public async Task GetOrCreateRestServerAgent_WhenExists_ShouldReturnExistingAgent() + { + // Skip the test if the product is not "Server" productId + Skip.If(_productInfo.ProductId != "012", "Test only applicable for Server productIds"); + + // Arrange + + var agentProperties = new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("true").RootElement }, + {"iot_gateway.REST_SERVER_PORT_NUMBER", JsonDocument.Parse("39320").RootElement} }; + var agent = await AddTestRestServerAgent("TestRestServer", agentProperties); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.GetOrCreateRestServerAgentAsync("TestRestServer"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe(agent.Name); + result.PortNumber.ShouldBe(agent.PortNumber); + result.Enabled.ShouldBe(agent.Enabled); + + // Clean up + await DeleteAllIoTAgentsAsync(); + + } + + + [SkippableFact] + public async Task CreateRestServerAgent_WhenSuccessful_ShouldReturnAgent() + { + // Skip the test if the product is not "Server" productId + Skip.If(_productInfo.ProductId != "012", "Test only applicable for Server productIds"); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.CreateRestServerAgentAsync("TestRestServer"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("TestRestServer"); + + // Clean up + await DeleteAllIoTAgentsAsync(); + + } + + [SkippableFact] + public async Task CreateRestServerAgent_WithProperties_ShouldSetProperties() + { + // Skip the test if the product is not "Server" productId + Skip.If(_productInfo.ProductId != "012", "Test only applicable for Server productIds"); + + // Arrange + + var properties = new Dictionary + { + { Properties.RestServerAgent.PortNumber, 39320 } + }; + + // Act + var result = await _kepwareApiClient.Project.IotGateway.CreateRestServerAgentAsync("TestRestServer", properties); + + // Assert + result.ShouldNotBeNull(); + result.PortNumber.ShouldBe(39320); + + // Clean up + await DeleteAllIoTAgentsAsync(); + } + + [SkippableFact] + public async Task CreateRestServerAgent_WithHttpError_AlreadyExists_ShouldReturnNull() + { + // Skip the test if the product is not "Server" productId + Skip.If(_productInfo.ProductId != "012", "Test only applicable for Server productIds"); + + // Arrange + var agentProperties = new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("true").RootElement }, + {"iot_gateway.REST_SERVER_PORT_NUMBER", JsonDocument.Parse("39320").RootElement} }; + var agent = await AddTestRestServerAgent("TestRestServer", agentProperties); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.CreateRestServerAgentAsync("TestRestServer"); + + // Assert + result.ShouldBeNull(); + + // Clean up + await DeleteAllIoTAgentsAsync(); + + } + + + [SkippableFact] + public async Task GetRestServerAgent_WhenExists_ShouldReturnAgent() + { + // Skip the test if the product is not "Server" productId + Skip.If(_productInfo.ProductId != "012", "Test only applicable for Server productIds"); + + // Arrange + var agentProperties = new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("true").RootElement }, + {"iot_gateway.REST_SERVER_PORT_NUMBER", JsonDocument.Parse("39320").RootElement} }; + var agent = await AddTestRestServerAgent("TestRestServer", agentProperties); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.GetRestServerAgentAsync("TestRestServer"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("TestRestServer"); + result.PortNumber.ShouldBe(39320); + + // Clean up + await DeleteAllIoTAgentsAsync(); + + } + + [SkippableFact] + public async Task GetRestServerAgent_WhenNotFound_ShouldReturnNull() + { + // Skip the test if the product is not "Server" productId + Skip.If(_productInfo.ProductId != "012", "Test only applicable for Server productIds"); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.GetRestServerAgentAsync("NonExistent"); + + // Assert + result.ShouldBeNull(); + } + + [SkippableFact] + public async Task DeleteRestServerAgent_ByEntity_WhenSuccessful_ShouldReturnTrue() + { + // Skip the test if the product is not "Server" productId + Skip.If(_productInfo.ProductId != "012", "Test only applicable for Server productIds"); + + // Arrange + var agentProperties = new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("true").RootElement }, + {"iot_gateway.REST_SERVER_PORT_NUMBER", JsonDocument.Parse("39320").RootElement} }; + var agent = await AddTestRestServerAgent("TestRestServer", agentProperties); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.DeleteRestServerAgentAsync(agent); + + // Assert + result.ShouldBeTrue(); + + // Clean up + await DeleteAllIoTAgentsAsync(); + } + + [SkippableFact] + public async Task DeleteRestServerAgent_ByName_WhenSuccessful_ShouldReturnTrue() + { + // Skip the test if the product is not "Server" productId + Skip.If(_productInfo.ProductId != "012", "Test only applicable for Server productIds"); + + // Arrange + var agentProperties = new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("true").RootElement }, + {"iot_gateway.REST_SERVER_PORT_NUMBER", JsonDocument.Parse("39320").RootElement} }; + var agent = await AddTestRestServerAgent("TestRestServer", agentProperties); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.DeleteRestServerAgentAsync("TestRestServer"); + + // Assert + result.ShouldBeTrue(); + + // Clean up + await DeleteAllIoTAgentsAsync(); + } + + [SkippableFact] + public async Task DeleteRestServerAgent_WithHttpError_ShouldReturnFalse() + { + // Skip the test if the product is not "Server" productId + Skip.If(_productInfo.ProductId != "012", "Test only applicable for Server productIds"); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.DeleteRestServerAgentAsync("TestRestServer"); + + // Assert + result.ShouldBeFalse(); + } + + #endregion + + #region IoT Item Tests + + [Fact] + public async Task GetOrCreateIotItem_WhenNotExists_ShouldCreateWithDerivedName() + { + // Arrange - server tag "Channel1.Device1.Tag1" should query with name "Channel1_Device1_Tag1" + var channel = await AddTestChannel("Channel1"); + var device = await AddTestDevice(channel, "Device1"); + var tag = await AddSimulatorTestTag(device, "Tag1"); + + var parentMqttAgent = await AddTestMqttClientAgent("ParentMqttAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + + // Act + var resultMqttClient = await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentMqttAgent); + + + // Act - create a system tag item to test the leading underscore logic + var systemTagItem = await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("_System._Time", parentMqttAgent); + + // Assert + resultMqttClient.ShouldNotBeNull(); + resultMqttClient.Name.ShouldBe("Channel1_Device1_Tag1"); + resultMqttClient.ServerTag.ShouldBe("Channel1.Device1.Tag1"); + + systemTagItem.ShouldNotBeNull(); + systemTagItem.Name.ShouldBe("System__Time"); + systemTagItem.ServerTag.ShouldBe("_System._Time"); + + + // If Kepware Server, test that REST Client and REST Server agents also create items with the same name and server tag + if (_productInfo.ProductId == "012") + { + // Arrange + var parentRestClientAgent = await AddTestRestClientAgent("ParentRestClientAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + var parentRestServerAgent = await AddTestRestServerAgent("ParentRestServerAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + + //Act + var resultRestClient = await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentRestClientAgent); + var resultRestServer = await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentRestServerAgent); + + // Assert + resultRestClient.ShouldNotBeNull(); + resultRestClient.Name.ShouldBe("Channel1_Device1_Tag1"); + resultRestClient.ServerTag.ShouldBe("Channel1.Device1.Tag1"); + + resultRestServer.ShouldNotBeNull(); + resultRestServer.Name.ShouldBe("Channel1_Device1_Tag1"); + resultRestServer.ServerTag.ShouldBe("Channel1.Device1.Tag1"); + } + + + // Clean up + await DeleteAllIoTAgentsAsync(); + await DeleteAllChannelsAsync(); + } + + [Fact] + public async Task GetOrCreateIotItem_WhenExists_ShouldReturnExistingItem() + { + // Arrange + var channel = await AddTestChannel("Channel1"); + var device = await AddTestDevice(channel, "Device1"); + var tag = await AddSimulatorTestTag(device, "Tag1"); + + var parentMqttAgent = await AddTestMqttClientAgent("ParentMqttAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + + // TODO: add items via client base method + var itemMqttClient = await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentMqttAgent); + + // Act + var resultMqttClient = await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentMqttAgent); + + + // Act - create a system tag item to test the leading underscore logic + var systemTagItem = await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("_System._Time", parentMqttAgent); + + // Assert + resultMqttClient.ShouldNotBeNull(); + resultMqttClient.Name.ShouldBe(itemMqttClient.Name); + resultMqttClient.ServerTag.ShouldBe(itemMqttClient.ServerTag); + + systemTagItem.ShouldNotBeNull(); + systemTagItem.Name.ShouldBe(systemTagItem.Name); + systemTagItem.ServerTag.ShouldBe(systemTagItem.ServerTag); + + // If Kepware Server, test that REST Client and REST Server agents also return items with the same name and server tag + if (_productInfo.ProductId == "012") + { + // Arrange + var parentRestClientAgent = await AddTestRestClientAgent("ParentRestClientAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + var parentRestServerAgent = await AddTestRestServerAgent("ParentRestServerAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + + // TODO: add items via client base method + var itemRestClient = await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentRestClientAgent); + var itemRestServer = await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentRestServerAgent); + + // Act + var resultRestClient = await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentRestClientAgent); + var resultRestServer = await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentRestServerAgent); + + // Assert + resultRestClient.ShouldNotBeNull(); + resultRestClient.Name.ShouldBe(itemRestClient.Name); + resultRestClient.ServerTag.ShouldBe(itemRestClient.ServerTag); + + resultRestServer.ShouldNotBeNull(); + resultRestServer.Name.ShouldBe(itemRestServer.Name); + resultRestServer.ServerTag.ShouldBe(itemRestServer.ServerTag); + } + + // Clean up + await DeleteAllIoTAgentsAsync(); + await DeleteAllChannelsAsync(); + } + + [Fact] + public async Task GetOrCreateIotItem_WhenCreateFails_ShouldThrowInvalidOperationException() + { + // Arrange + var parentMqttAgent = await AddTestMqttClientAgent("ParentMqttAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + + // Act + await Should.ThrowAsync(async () => + await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentMqttAgent)); + + if (_productInfo.ProductId == "012") + { + // Arrange + var parentRestClientAgent = await AddTestRestClientAgent("ParentRestClientAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + var parentRestServerAgent = await AddTestRestServerAgent("ParentRestServerAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + + // Act + await Should.ThrowAsync(async () => + await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentRestClientAgent)); + await Should.ThrowAsync(async () => + await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentRestServerAgent)); + } + // Clean up + await DeleteAllIoTAgentsAsync(); + + } + + [Fact] + public async Task CreateIotItem_ShouldDeriveNameFromServerTag() + { + // Arrange - server tag "Channel1.Device1.Tag1" should query with name "Channel1_Device1_Tag1" + var channel = await AddTestChannel("Channel1"); + var device = await AddTestDevice(channel, "Device1"); + var tag = await AddSimulatorTestTag(device, "Tag1"); + + var parentMqttAgent = await AddTestMqttClientAgent("ParentMqttAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + + + // Act + var resultMqttClient = await _kepwareApiClient.Project.IotGateway.CreateIotItemAsync("Channel1.Device1.Tag1", parentMqttAgent); + + + // Act - create a system tag item to test the leading underscore logic + var systemTagItem = await _kepwareApiClient.Project.IotGateway.CreateIotItemAsync("_System._Time", parentMqttAgent); + + // Assert + resultMqttClient.ShouldNotBeNull(); + resultMqttClient.Name.ShouldBe("Channel1_Device1_Tag1"); + resultMqttClient.ServerTag.ShouldBe("Channel1.Device1.Tag1"); + + systemTagItem.ShouldNotBeNull(); + systemTagItem.Name.ShouldBe("System__Time"); + systemTagItem.ServerTag.ShouldBe("_System._Time"); + + if (_productInfo.ProductId == "012") + { + // Arrange + var parentRestClientAgent = await AddTestRestClientAgent("ParentRestClientAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + var parentRestServerAgent = await AddTestRestServerAgent("ParentRestServerAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + + // Act + var resultRestClient = await _kepwareApiClient.Project.IotGateway.CreateIotItemAsync("Channel1.Device1.Tag1", parentRestClientAgent); + var resultRestServer = await _kepwareApiClient.Project.IotGateway.CreateIotItemAsync("Channel1.Device1.Tag1", parentRestServerAgent); + + // Assert + resultRestClient.ShouldNotBeNull(); + resultRestClient.Name.ShouldBe("Channel1_Device1_Tag1"); + resultRestClient.ServerTag.ShouldBe("Channel1.Device1.Tag1"); + + resultRestServer.ShouldNotBeNull(); + resultRestServer.Name.ShouldBe("Channel1_Device1_Tag1"); + resultRestServer.ServerTag.ShouldBe("Channel1.Device1.Tag1"); + + } + // Clean up + await DeleteAllIoTAgentsAsync(); + await DeleteAllChannelsAsync(); + } + + [Fact] + public async Task CreateIotItem_WithHttpError_AlreadyExists_ShouldReturnNull() + { + // Arrange + var channel = await AddTestChannel("Channel1"); + var device = await AddTestDevice(channel, "Device1"); + var tag = await AddSimulatorTestTag(device, "Tag1"); + + var parentMqttAgent = await AddTestMqttClientAgent("ParentMqttAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + + // TODO: add items via client base method + var itemMqttClient = await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentMqttAgent); + + // Act + var resultMqttClient = await _kepwareApiClient.Project.IotGateway.CreateIotItemAsync("Channel1.Device1.Tag1", parentMqttAgent); + + // Assert + resultMqttClient.ShouldBeNull(); + + if (_productInfo.ProductId == "012") + { + // Arrange + var parentRestClientAgent = await AddTestRestClientAgent("ParentRestClientAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + var parentRestServerAgent = await AddTestRestServerAgent("ParentRestServerAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + + // TODO: add items via client base method + + var itemRestClient = await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentRestClientAgent); + var itemRestServer = await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentRestServerAgent); + + // Act + + var resultRestClient = await _kepwareApiClient.Project.IotGateway.CreateIotItemAsync("Channel1.Device1.Tag1", parentRestClientAgent); + var resultRestServer = await _kepwareApiClient.Project.IotGateway.CreateIotItemAsync("Channel1.Device1.Tag1", parentRestServerAgent); + + resultRestClient.ShouldBeNull(); + resultRestServer.ShouldBeNull(); + } + + // Clean up + await DeleteAllIoTAgentsAsync(); + await DeleteAllChannelsAsync(); + } + + + [Fact] + public async Task DeleteIotItem_ByEntity_WhenSuccessful_ShouldReturnTrue() + { + // Arrange + var channel = await AddTestChannel("Channel1"); + var device = await AddTestDevice(channel, "Device1"); + var tag = await AddSimulatorTestTag(device, "Tag1"); + + var parentMqttAgent = await AddTestMqttClientAgent("ParentMqttAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + + // TODO: add items via client base method + var itemMqttClient = await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentMqttAgent); + + // Act + var resultMqttClient = await _kepwareApiClient.Project.IotGateway.DeleteIotItemAsync(itemMqttClient); + + // Assert + resultMqttClient.ShouldBeTrue(); + + if (_productInfo.ProductId == "012") + { + // Arrange + var parentRestClientAgent = await AddTestRestClientAgent("ParentRestClientAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + var parentRestServerAgent = await AddTestRestServerAgent("ParentRestServerAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + + // TODO: add items via client base method + + var itemRestClient = await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentRestClientAgent); + var itemRestServer = await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentRestServerAgent); + + // Act + + var resultRestClient = await _kepwareApiClient.Project.IotGateway.DeleteIotItemAsync(itemRestClient); + var resultRestServer = await _kepwareApiClient.Project.IotGateway.DeleteIotItemAsync(itemRestServer); + + // Assert + resultRestClient.ShouldBeTrue(); + resultRestServer.ShouldBeTrue(); + } + + // Clean up + await DeleteAllIoTAgentsAsync(); + await DeleteAllChannelsAsync(); + + } + + [Fact] + public async Task DeleteIotItem_ByServerTag_ShouldTranslateToItemName() + { + // Arrange + var channel = await AddTestChannel("Channel1"); + var device = await AddTestDevice(channel, "Device1"); + var tag = await AddSimulatorTestTag(device, "Tag1"); + + var parentMqttAgent = await AddTestMqttClientAgent("ParentMqttAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + + // TODO: add items via client base method + var itemMqttClient = await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentMqttAgent); + + // Act + var resultMqttClient = await _kepwareApiClient.Project.IotGateway.DeleteIotItemAsync("Channel1.Device1.Tag1", parentMqttAgent); + + // Assert + resultMqttClient.ShouldBeTrue(); + + if (_productInfo.ProductId == "012") + { + + var parentRestClientAgent = await AddTestRestClientAgent("ParentRestClientAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + var parentRestServerAgent = await AddTestRestServerAgent("ParentRestServerAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + + // TODO: add items via client base method + var itemRestClient = await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentRestClientAgent); + var itemRestServer = await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentRestServerAgent); + + // Act + var resultRestClient = await _kepwareApiClient.Project.IotGateway.DeleteIotItemAsync("Channel1.Device1.Tag1", parentRestClientAgent); + var resultRestServer = await _kepwareApiClient.Project.IotGateway.DeleteIotItemAsync("Channel1.Device1.Tag1", parentRestServerAgent); + + // Assert + resultRestClient.ShouldBeTrue(); + resultRestServer.ShouldBeTrue(); + } + // Clean up + await DeleteAllIoTAgentsAsync(); + await DeleteAllChannelsAsync(); + + } + + [Fact] + public async Task DeleteIotItem_WithHttpError_ShouldReturnFalse() + { + // Arrange + var parentMqttAgent = await AddTestMqttClientAgent("ParentMqttAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + + // Act + var resultMqttClient = await _kepwareApiClient.Project.IotGateway.DeleteIotItemAsync("Channel1.Device1.Tag1", parentMqttAgent); + + // Assert + resultMqttClient.ShouldBeFalse(); + + if (_productInfo.ProductId == "012") + { + // Arrange + var parentRestClientAgent = await AddTestRestClientAgent("ParentRestClientAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + var parentRestServerAgent = await AddTestRestServerAgent("ParentRestServerAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + + // Act + var resultRestClient = await _kepwareApiClient.Project.IotGateway.DeleteIotItemAsync("Channel1.Device1.Tag1", parentRestClientAgent); + var resultRestServer = await _kepwareApiClient.Project.IotGateway.DeleteIotItemAsync("Channel1.Device1.Tag1", parentRestServerAgent); + + // Assert + resultRestClient.ShouldBeFalse(); + resultRestServer.ShouldBeFalse(); + } + + // Clean up + await DeleteAllIoTAgentsAsync(); + + } + + #endregion + +} diff --git a/Kepware.Api.TestIntg/ApiClient/LoadEntity.cs b/Kepware.Api.TestIntg/ApiClient/LoadEntityTests.cs similarity index 86% rename from Kepware.Api.TestIntg/ApiClient/LoadEntity.cs rename to Kepware.Api.TestIntg/ApiClient/LoadEntityTests.cs index f1ebfc3..c688852 100644 --- a/Kepware.Api.TestIntg/ApiClient/LoadEntity.cs +++ b/Kepware.Api.TestIntg/ApiClient/LoadEntityTests.cs @@ -276,7 +276,7 @@ public async Task LoadEntityAsync_ShouldReturnTagCollection_WhenApiRespondsSucce [Fact] public async Task LoadEntityAsync_ShouldReturnTagGroupCollectionInTagGroup_WhenApiRespondsSuccessfully() { - // TODO: Currently this test fails due to issue in EndpointResolver. + // TODO: Clean up test. Fix was made and test is successful. // Arrange try { @@ -335,5 +335,46 @@ public async Task LoadEntityAsync_ShouldReturnTagCollectionFromTagGroup_WhenApiR await DeleteAllChannelsAsync(); } #endregion + + #region LoadEntityAsync - Return Channel Collection with all children - Serialize query + [SkippableFact] + public async Task LoadEntityAsync_ShouldReturnChannelAndChildren_WhenApiRespondsSuccessfully() + { + // Skip the test if the serialize feature is not supported by server version. + Skip.If(!_productInfo.SupportsJsonProjectLoadService, "Test only applicable for versions that support JsonProjectLoad"); + + // Arrange + var channel = await AddTestChannel(); + var device = await AddTestDevice(channel); + var tagGroup = await AddTestTagGroup(device); + var tagGroup2 = await AddTestTagGroup(tagGroup, "TagGroup2"); + + var query = new[] + { + new KeyValuePair("content", "serialize"), + }; + + // Act + var result = await _kepwareApiClient.GenericConfig.LoadEntityAsync(channel.Name, query); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Devices); + Assert.Contains(result.Devices, g => g.Name == device.Name); + + var foundDevice = result.Devices.Find(d => d.Name == device.Name); + Assert.NotNull(foundDevice); + Assert.NotNull(foundDevice.TagGroups); + Assert.Contains(foundDevice.TagGroups, tg => tg.Name == tagGroup.Name); + + var foundTagGroup = foundDevice.TagGroups.Find(tg => tg.Name == tagGroup.Name); + Assert.NotNull(foundTagGroup); + Assert.NotNull(foundTagGroup.TagGroups); + Assert.Contains(foundTagGroup.TagGroups, tg => tg.Name == tagGroup2.Name); + + // Cleanup + await DeleteAllChannelsAsync(); + } + #endregion } } diff --git a/Kepware.Api.TestIntg/ApiClient/ProjectApiHandlerTests.cs b/Kepware.Api.TestIntg/ApiClient/ProjectApiHandlerTests.cs index a56b342..a901ddb 100644 --- a/Kepware.Api.TestIntg/ApiClient/ProjectApiHandlerTests.cs +++ b/Kepware.Api.TestIntg/ApiClient/ProjectApiHandlerTests.cs @@ -312,7 +312,7 @@ public async Task CompareAndApply_ShouldReturnChanges() newProject.SetDynamicProperty("uaserverinterface.PROJECT_OPC_UA_ANONYMOUS_LOGIN", !existingProperties?.ProjectProperties.OpcUaAllowAnonymousLogin); // Act - var result = await _kepwareApiClient.Project.CompareAndApply(newProject); + var result = await _kepwareApiClient.Project.CompareAndApplyAsync(newProject); // Assert Assert.Equal(2, result.inserts); // 2 new channels added diff --git a/Kepware.Api.TestIntg/ApiClient/ProjectLoadTests.cs b/Kepware.Api.TestIntg/ApiClient/ProjectLoadTests.cs index 6127e3f..bc01688 100644 --- a/Kepware.Api.TestIntg/ApiClient/ProjectLoadTests.cs +++ b/Kepware.Api.TestIntg/ApiClient/ProjectLoadTests.cs @@ -110,6 +110,153 @@ private static void CompareTagGroupsRecursive(DeviceTagGroupCollection? expected } } + [Fact] + public async Task LoadProject_Full_ShouldLoadCorrectly_BasedOnProductSupport() + { + // Arrange + var channel = await AddTestChannel(); + var device = await AddTestDevice(channel); + var tags = await AddSimulatorTestTags(device); + var tagGroup = await AddTestTagGroup(device); + var tagGroup2 = await AddTestTagGroup(tagGroup, "TagGroup2"); + + var channel2 = await AddTestChannel("Channel2"); + var device2 = await AddTestDevice(channel2); + var tags2 = await AddSimulatorTestTags(device2); + var tagGroup_2 = await AddTestTagGroup(device2); + var tagGroup2_2 = await AddTestTagGroup(tagGroup_2, "TagGroup2"); + + var pro = new Project(); + + // Act + var project = await _kepwareApiClient.Project.LoadProjectAsync(blnLoadFullProject: true); + + // Assert + Assert.NotNull(project); + Assert.NotNull(project.Channels); + Assert.Contains(project.Channels, c => c.Name == channel.Name); + + var foundChannel = project.Channels.Find(c => c.Name == channel.Name); + Assert.NotNull(foundChannel); + Assert.NotNull(foundChannel.Devices); + Assert.Contains(foundChannel.Devices, d => d.Name == device.Name); + + var foundDevice = foundChannel.Devices.Find(d => d.Name == device.Name); + Assert.NotNull(foundDevice); + Assert.NotNull(foundDevice.Tags); + Assert.Equal(tags.Count, foundDevice.Tags.Count); + Assert.NotNull(foundDevice.TagGroups); + Assert.Contains(foundDevice.TagGroups, tg => tg.Name == tagGroup.Name); + + var foundTagGroup = foundDevice.TagGroups.Find(tg => tg.Name == tagGroup.Name); + Assert.NotNull(foundTagGroup); + Assert.NotNull(foundTagGroup.TagGroups); + Assert.Contains(foundTagGroup.TagGroups, tg => tg.Name == tagGroup2.Name); + + + // Clean up + await DeleteAllChannelsAsync(); + } + + [Fact] + public async Task LoadProject_Full_LargeProject_ShouldLoadCorrectly_BasedOnProductSupport() + { + // Arrange + var channel = await AddTestChannel(); + var device = await AddTestDevice(channel); + var tags = await AddSimulatorTestTags(device, count: 10000); + var tagGroup = await AddTestTagGroup(device); + var tagGroup2 = await AddTestTagGroup(tagGroup, "TagGroup2"); + + var channel2 = await AddTestChannel("Channel2"); + var device2 = await AddTestDevice(channel2); + var tags2 = await AddSimulatorTestTags(device2); + var tagGroup_2 = await AddTestTagGroup(device2); + var tagGroup2_2 = await AddTestTagGroup(tagGroup_2, "TagGroup2"); + + // Act + var project = await _kepwareApiClient.Project.LoadProjectAsync(blnLoadFullProject: true); + + // Assert + Assert.NotNull(project); + Assert.NotNull(project.Channels); + Assert.Contains(project.Channels, c => c.Name == channel.Name); + + var foundChannel = project.Channels.Find(c => c.Name == channel.Name); + Assert.NotNull(foundChannel); + Assert.NotNull(foundChannel.Devices); + Assert.Contains(foundChannel.Devices, d => d.Name == device.Name); + + var foundDevice = foundChannel.Devices.Find(d => d.Name == device.Name); + Assert.NotNull(foundDevice); + Assert.NotNull(foundDevice.Tags); + Assert.Equal(tags.Count, foundDevice.Tags.Count); + Assert.NotNull(foundDevice.TagGroups); + Assert.Contains(foundDevice.TagGroups, tg => tg.Name == tagGroup.Name); + + var foundTagGroup = foundDevice.TagGroups.Find(tg => tg.Name == tagGroup.Name); + Assert.NotNull(foundTagGroup); + Assert.NotNull(foundTagGroup.TagGroups); + Assert.Contains(foundTagGroup.TagGroups, tg => tg.Name == tagGroup2.Name); + + + // Clean up + await DeleteAllChannelsAsync(); + } + + + [Fact] + public async Task LoadProject_Full_OverrideForOptimizedRecursion_ShouldLoadCorrectly_BasedOnProductSupport() + { + // Arrange + var channel = await AddTestChannel(); + var device = await AddTestDevice(channel); + var tags = await AddSimulatorTestTags(device, count: 200); + var tagGroup = await AddTestTagGroup(device); + var tagGroup2 = await AddTestTagGroup(tagGroup, "TagGroup2"); + var tagsTagGroup2 = await AddSimulatorTestTags(tagGroup2, count: 10); + + var channel2 = await AddTestChannel("Channel2"); + var device2 = await AddTestDevice(channel2); + var tags2 = await AddSimulatorTestTags(device2); + var tagGroup_2 = await AddTestTagGroup(device2); + var tagGroup2_2 = await AddTestTagGroup(tagGroup_2, "TagGroup2"); + + // Act + var project = await _kepwareApiClient.Project.LoadProjectAsync(blnLoadFullProject: true, projectLoadTagLimit: 100); + + // Assert + Assert.NotNull(project); + Assert.NotNull(project.Channels); + Assert.Contains(project.Channels, c => c.Name == channel.Name); + + var foundChannel = project.Channels.Find(c => c.Name == channel.Name); + Assert.NotNull(foundChannel); + Assert.NotNull(foundChannel.Devices); + Assert.Contains(foundChannel.Devices, d => d.Name == device.Name); + + var foundDevice = foundChannel.Devices.Find(d => d.Name == device.Name); + Assert.NotNull(foundDevice); + Assert.NotNull(foundDevice.Tags); + Assert.Equal(tags.Count, foundDevice.Tags.Count); + Assert.NotNull(foundDevice.TagGroups); + Assert.Contains(foundDevice.TagGroups, tg => tg.Name == tagGroup.Name); + + var foundTagGroup = foundDevice.TagGroups.Find(tg => tg.Name == tagGroup.Name); + Assert.NotNull(foundTagGroup); + Assert.NotNull(foundTagGroup.TagGroups); + Assert.Contains(foundTagGroup.TagGroups, tg => tg.Name == tagGroup2.Name); + + var foundTagGroup2 = foundTagGroup.TagGroups.Find(tg => tg.Name == tagGroup2.Name); + Assert.NotNull(foundTagGroup2); + Assert.NotNull(foundTagGroup2.Tags); + Assert.Equal(tagsTagGroup2.Count, foundTagGroup2.Tags.Count); + + + // Clean up + await DeleteAllChannelsAsync(); + } + [Fact] public async Task LoadProject_NotFull_ShouldLoadCorrectly_BasedOnProductSupport() { @@ -119,7 +266,7 @@ public async Task LoadProject_NotFull_ShouldLoadCorrectly_BasedOnProductSupport( var tags = await AddSimulatorTestTags(device); // Act - var project = await _kepwareApiClient.Project.LoadProject(blnLoadFullProject: false); + var project = await _kepwareApiClient.Project.LoadProjectAsync(blnLoadFullProject: false); // Assert project.ShouldNotBeNull(); @@ -139,7 +286,7 @@ public async Task LoadProject_ShouldReturnEmptyProject_WhenHttpRequestFails() { // Act - var project = await _badCredKepwareApiClient.Project.LoadProject(true); + var project = await _badCredKepwareApiClient.Project.LoadProjectAsync(true); // Assert project.ShouldNotBeNull(); diff --git a/Kepware.Api.TestIntg/ApiClient/ProjectPermissionTests.cs b/Kepware.Api.TestIntg/ApiClient/ProjectPermissionTests.cs index 181ea47..8aca032 100644 --- a/Kepware.Api.TestIntg/ApiClient/ProjectPermissionTests.cs +++ b/Kepware.Api.TestIntg/ApiClient/ProjectPermissionTests.cs @@ -55,13 +55,12 @@ public async Task UpdateProjectPermissionAsync_ShouldReturnTrue_WhenUpdateSucces await _kepwareApiClient.Admin.CreateOrUpdateServerUserGroupAsync(serverUserGroup); - var projectPermission = new ProjectPermission + var projectPermission = await _kepwareApiClient.Admin.GetProjectPermissionAsync(serverUserGroup, ProjectPermissionName.ServermainAlias); + if (projectPermission == null) { - Name = ProjectPermissionName.ServermainAlias, - AddObject = true, - EditObject = true, - DeleteObject = false - }; + Assert.Fail("Precondition failed: ProjectPermission should exist for the test user group."); + } + projectPermission.DeleteObject = false; // Change a property to trigger an update // Act var result = await _kepwareApiClient.Admin.UpdateProjectPermissionAsync(serverUserGroup, projectPermission); @@ -97,9 +96,6 @@ public async Task UpdateProjectPermissionAsync_ShouldReturnTrue_WhenUpdateSucces [Fact] public async Task UpdateProjectPermissionAsync_ShouldReturnFalse_WhenUpdateFails() { - // TODO: Currently fails. Unsure of expected behavior from Kepware when an update fails. As of v6.18 - // Kepware returns a 200 OK with content that indicates a "not applied" key in the payload , - // which is not consistent with other endpoints. // Arrange var serverUserGroup = new ServerUserGroup { Name = "Administrators" }; @@ -115,7 +111,7 @@ public async Task UpdateProjectPermissionAsync_ShouldReturnFalse_WhenUpdateFails var result = await _kepwareApiClient.Admin.UpdateProjectPermissionAsync(serverUserGroup, projectPermission); // Assert - result.ShouldBeFalse("Currently fails. Unsure of expected behavior from Kepware when an update fails. See comments in test."); + result.ShouldBeFalse(); } } } diff --git a/Kepware.Api.TestIntg/ApiClient/TestConnection.cs b/Kepware.Api.TestIntg/ApiClient/TestConnectionTests.cs similarity index 100% rename from Kepware.Api.TestIntg/ApiClient/TestConnection.cs rename to Kepware.Api.TestIntg/ApiClient/TestConnectionTests.cs diff --git a/Kepware.Api.TestIntg/ApiClient/UaEndpointTests.cs b/Kepware.Api.TestIntg/ApiClient/UaEndpointTests.cs index cf79bb2..4723106 100644 --- a/Kepware.Api.TestIntg/ApiClient/UaEndpointTests.cs +++ b/Kepware.Api.TestIntg/ApiClient/UaEndpointTests.cs @@ -13,12 +13,10 @@ namespace Kepware.Api.TestIntg.ApiClient { - [TestCaseOrderer("Xunit.Extensions.Ordering.TestCaseOrderer", "Xunit.Extensions.Ordering")] public class UaEndpointTests : TestIntgApiClientBase { [SkippableFact] - [Order(1)] public async Task CreateOrUpdateUaEndpointAsync_ShouldCreateUaEndpoint_WhenItDoesNotExist() { // Skip the test if the product is not "Edge" productId @@ -32,10 +30,13 @@ public async Task CreateOrUpdateUaEndpointAsync_ShouldCreateUaEndpoint_WhenItDoe // Assert result.ShouldBeTrue(); + + // Clean up + await _kepwareApiClient.Admin.DeleteUaEndpointAsync(uaEndpoint.Name); + } [SkippableFact] - [Order(2)] public async Task CreateOrUpdateUaEndpointAsync_ShouldUpdateUaEndpoint_WhenItExists() { // Skip the test if the product is not "Edge" productId @@ -43,6 +44,10 @@ public async Task CreateOrUpdateUaEndpointAsync_ShouldUpdateUaEndpoint_WhenItExi // Arrange var uaEndpoint = CreateTestUaEndpoint(); + await _kepwareApiClient.GenericConfig.InsertItemAsync(uaEndpoint); + + uaEndpoint = await _kepwareApiClient.GenericConfig.LoadEntityAsync(uaEndpoint.Name); + uaEndpoint.ShouldNotBeNull(); uaEndpoint.Port = 4840; // Act @@ -50,10 +55,12 @@ public async Task CreateOrUpdateUaEndpointAsync_ShouldUpdateUaEndpoint_WhenItExi // Assert result.ShouldBeTrue(); + + // Clean up + await _kepwareApiClient.Admin.DeleteUaEndpointAsync(uaEndpoint.Name); } [SkippableFact] - [Order(3)] public async Task GetUaEndpointAsync_ShouldReturnUaEndpoint_WhenApiRespondsSuccessfully() { // Skip the test if the product is not "Edge" productId @@ -61,6 +68,7 @@ public async Task GetUaEndpointAsync_ShouldReturnUaEndpoint_WhenApiRespondsSucce // Arrange var uaEndpoint = CreateTestUaEndpoint(); + await _kepwareApiClient.GenericConfig.InsertItemAsync(uaEndpoint); // Act var result = await _kepwareApiClient.Admin.GetUaEndpointAsync(uaEndpoint.Name); @@ -69,10 +77,13 @@ public async Task GetUaEndpointAsync_ShouldReturnUaEndpoint_WhenApiRespondsSucce result.ShouldNotBeNull(); result.Name.ShouldBe(uaEndpoint.Name); result.Port.ShouldBeOfType(); + + // Clean up + await _kepwareApiClient.Admin.DeleteUaEndpointAsync(uaEndpoint.Name); + } [SkippableFact] - [Order(4)] public async Task GetUaEndpointsAsync_ShouldReturnUaEndpointCollection_WhenApiRespondsSuccessfully() { // Skip the test if the product is not "Edge" productId @@ -83,12 +94,10 @@ public async Task GetUaEndpointsAsync_ShouldReturnUaEndpointCollection_WhenApiRe // Assert result.ShouldNotBeNull(); - // result.Count.ShouldBe(2); } [SkippableFact] - [Order(5)] public async Task DeleteUaEndpointAsync_ShouldReturnTrue_WhenDeleteSuccessful() { // Skip the test if the product is not "Edge" productId @@ -96,6 +105,7 @@ public async Task DeleteUaEndpointAsync_ShouldReturnTrue_WhenDeleteSuccessful() // Arrange var uaEndpoint = CreateTestUaEndpoint(); + await _kepwareApiClient.GenericConfig.InsertItemAsync(uaEndpoint); // Act var result = await _kepwareApiClient.Admin.DeleteUaEndpointAsync(uaEndpoint.Name); @@ -105,7 +115,6 @@ public async Task DeleteUaEndpointAsync_ShouldReturnTrue_WhenDeleteSuccessful() } [SkippableFact] - [Order(6)] public async Task DeleteUaEndpointAsync_ShouldReturnFalse_WhenDeleteFails() { // Skip the test if the product is not "Edge" productId diff --git a/Kepware.Api.TestIntg/ApiClient/_TestIntgApiClientBase.cs b/Kepware.Api.TestIntg/ApiClient/_TestIntgApiClientBase.cs index fbf1a9d..1b2962f 100644 --- a/Kepware.Api.TestIntg/ApiClient/_TestIntgApiClientBase.cs +++ b/Kepware.Api.TestIntg/ApiClient/_TestIntgApiClientBase.cs @@ -153,25 +153,25 @@ protected async Task AddSimulatorTestTag(DeviceTagGroup owner, string name protected List CreateSimulatorTestTags(string name = "Tag", string address = "K000", int count = 2) { - return Enumerable.Range(1, count) + return Enumerable.Range(0, count) .Select(i => CreateTestTag(name: $"{name}{i}", address: $"{address}{i}")) .ToList(); } protected async Task> AddSimulatorTestTags(Device owner, string name = "Tag", string address = "K000", int count = 2) { - var tagsList = CreateSimulatorTestTags(name, address); + var tagsList = CreateSimulatorTestTags(name, address, count); foreach (var tag in tagsList) { tag.Owner = owner; } - await _kepwareApiClient.GenericConfig.InsertItemsAsync(tagsList, owner: owner); + await _kepwareApiClient.GenericConfig.InsertItemsAsync(tagsList, pageSize: count ,owner: owner); return tagsList; } protected async Task> AddSimulatorTestTags(DeviceTagGroup owner, string name = "Tag1", string address = "K000", int count = 2) { - var tagsList = CreateSimulatorTestTags(name, address); + var tagsList = CreateSimulatorTestTags(name, address, count); foreach (var tag in tagsList) { tag.Owner = owner; @@ -216,5 +216,96 @@ protected async Task AddTestTagGroup(DeviceTagGroup owner, strin return await _kepwareApiClient.GenericConfig.LoadEntityAsync(tagGroup.Name, owner) ?? throw new Exception($"Failed to load tag group '{tagGroup.Name}' after insertion."); } + protected async Task AddTestMqttClientAgent(string name = "MqttTestAgent", Dictionary? properties = null) + { + var agent = new MqttClientAgent(name); + if (properties != null) + { + foreach (var kvp in properties) + { + agent.DynamicProperties[kvp.Key] = kvp.Value; + } + } + + + await _kepwareApiClient.GenericConfig.InsertItemAsync(agent, cancellationToken: CancellationToken.None); + return await _kepwareApiClient.GenericConfig.LoadEntityAsync(agent.Name, cancellationToken: CancellationToken.None) ?? throw new Exception($"Failed to create MQTT Client Agent '{name}'."); + } + + protected async Task AddTestRestClientAgent(string name = "RestClientTestAgent", Dictionary? properties = null) + { + var agent = new RestClientAgent(name); + if (properties != null) + { + foreach (var kvp in properties) + { + agent.DynamicProperties[kvp.Key] = kvp.Value; + } + } + await _kepwareApiClient.GenericConfig.InsertItemAsync(agent, cancellationToken: CancellationToken.None); + return await _kepwareApiClient.GenericConfig.LoadEntityAsync(agent.Name, cancellationToken: CancellationToken.None) ?? throw new Exception($"Failed to create REST Client Agent '{name}'."); + } + + protected async Task AddTestRestServerAgent(string name = "RestServerTestAgent", Dictionary? properties = null) + { + var agent = new RestServerAgent(name); + if (properties != null) + { + foreach (var kvp in properties) + { + agent.DynamicProperties[kvp.Key] = kvp.Value; + } + } + await _kepwareApiClient.GenericConfig.InsertItemAsync(agent, cancellationToken: CancellationToken.None); + return await _kepwareApiClient.GenericConfig.LoadEntityAsync(agent.Name, cancellationToken: CancellationToken.None) ?? throw new Exception($"Failed to create REST Server Agent '{name}'."); + } + + protected async Task DeleteAllIoTAgentsAsync() + { + var mqttAgents = await _kepwareApiClient.GenericConfig.LoadCollectionAsync(); + if (mqttAgents != null) + { + foreach (var agent in mqttAgents) + { + await _kepwareApiClient.Project.IotGateway.DeleteMqttClientAgentAsync(agent.Name); + } + } + + var restClientAgents = await _kepwareApiClient.GenericConfig.LoadCollectionAsync(); + if (restClientAgents != null) + { + foreach (var agent in restClientAgents) + { + await _kepwareApiClient.Project.IotGateway.DeleteRestClientAgentAsync(agent.Name); + } + } + + var restServerAgents = await _kepwareApiClient.GenericConfig.LoadCollectionAsync(); + if (restServerAgents != null) + { + foreach (var agent in restServerAgents) + { + await _kepwareApiClient.Project.IotGateway.DeleteRestServerAgentAsync(agent.Name); + } + } + + } + + // protected async Task AddTestIotItem(string name = "IotItemTest", Dictionary? properties = null) + // { + // var item = new IotItem(name); + // if (properties != null) + // { + // foreach (var kvp in properties) + // { + // item.DynamicProperties[kvp.Key] = kvp.Value; + // } + // } + // await _kepwareApiClient.GenericConfig.InsertItemAsync(item, cancellationToken: CancellationToken.None); + // return await _kepwareApiClient.GenericConfig.LoadEntityAsync(item.Name, cancellationToken: CancellationToken.None) ?? throw new Exception($"Failed to create IoT Item '{name}'."); + // } + + + } } \ No newline at end of file diff --git a/Kepware.Api.TestIntg/Kepware.Api.TestIntg.csproj b/Kepware.Api.TestIntg/Kepware.Api.TestIntg.csproj index 884ad48..a639398 100644 --- a/Kepware.Api.TestIntg/Kepware.Api.TestIntg.csproj +++ b/Kepware.Api.TestIntg/Kepware.Api.TestIntg.csproj @@ -39,12 +39,12 @@ - + PreserveNewest - - + + PreserveNewest - + diff --git a/Kepware.Api.TestIntg/appsettings.json b/Kepware.Api.TestIntg/appsettings.json index 14128aa..c450806 100644 --- a/Kepware.Api.TestIntg/appsettings.json +++ b/Kepware.Api.TestIntg/appsettings.json @@ -2,10 +2,10 @@ "TestSettings": { "IntegrationTest": true, "TestServer": { - "Host": "https://localhost", - "Port": 57513, + "Host": "http://localhost", + "Port": 57412, "UserName": "Administrator", - "Password": "Kepware400400400" + "Password": "" } } } \ No newline at end of file diff --git a/Kepware.Api/ClientHandler/AdminApiHandler.cs b/Kepware.Api/ClientHandler/AdminApiHandler.cs index 6173cfc..eea1215 100644 --- a/Kepware.Api/ClientHandler/AdminApiHandler.cs +++ b/Kepware.Api/ClientHandler/AdminApiHandler.cs @@ -42,7 +42,7 @@ public AdminApiHandler(KepwareApiClient kepwareApiClient, ILoggerThe current or null if retrieval fails. public Task GetAdminSettingsAsync(CancellationToken cancellationToken = default) { - return m_kepwareApiClient.GenericConfig.LoadEntityAsync(name: null, cancellationToken); + return m_kepwareApiClient.GenericConfig.LoadEntityAsync(name: null, cancellationToken: cancellationToken); } /// @@ -117,7 +117,7 @@ public async Task SetAdminSettingsAsync(AdminSettings settings, Cancellati /// The configuration, or null if not found. public Task GetUaEndpointAsync(string name, CancellationToken cancellationToken = default) { - return m_kepwareApiClient.GenericConfig.LoadEntityAsync(name, cancellationToken); + return m_kepwareApiClient.GenericConfig.LoadEntityAsync(name, cancellationToken: cancellationToken); } /// @@ -135,7 +135,7 @@ public async Task CreateOrUpdateUaEndpointAsync(UaEndpoint endpoint, Cance try { var endpointUrl = EndpointResolver.ResolveEndpoint([endpoint.Name]); - var currentEndpoint = await m_kepwareApiClient.GenericConfig.LoadEntityByEndpointAsync(endpointUrl, cancellationToken); + var currentEndpoint = await m_kepwareApiClient.GenericConfig.LoadEntityByEndpointAsync(endpointUrl, cancellationToken: cancellationToken); if (currentEndpoint == null) { @@ -162,7 +162,7 @@ public async Task CreateOrUpdateUaEndpointAsync(UaEndpoint endpoint, Cance /// A token that can be used to request cancellation of the operation. /// True if the endpoint was successfully deleted; otherwise, false. public Task DeleteUaEndpointAsync(string name, CancellationToken cancellationToken = default) - => m_kepwareApiClient.GenericConfig.DeleteItemAsync(name, cancellationToken); + => m_kepwareApiClient.GenericConfig.DeleteItemAsync(name, cancellationToken: cancellationToken); #endregion @@ -186,7 +186,7 @@ public Task DeleteUaEndpointAsync(string name, CancellationToken cancellat /// The configuration, or null if not found. public Task GetServerUserGroupAsync(string name, CancellationToken cancellationToken = default) { - return m_kepwareApiClient.GenericConfig.LoadEntityAsync(name, cancellationToken); + return m_kepwareApiClient.GenericConfig.LoadEntityAsync(name, cancellationToken: cancellationToken); } /// @@ -204,7 +204,7 @@ public async Task CreateOrUpdateServerUserGroupAsync(ServerUserGroup userG try { var endpointUrl = EndpointResolver.ResolveEndpoint([userGroup.Name]); - var currentGroup = await m_kepwareApiClient.GenericConfig.LoadEntityByEndpointAsync(endpointUrl, cancellationToken); + var currentGroup = await m_kepwareApiClient.GenericConfig.LoadEntityByEndpointAsync(endpointUrl, cancellationToken: cancellationToken); if (currentGroup == null) { @@ -231,7 +231,7 @@ public async Task CreateOrUpdateServerUserGroupAsync(ServerUserGroup userG /// A token that can be used to request cancellation of the operation. /// True if the group was successfully deleted; otherwise, false. public Task DeleteServerUserGroupAsync(string name, CancellationToken cancellationToken = default) - => m_kepwareApiClient.GenericConfig.DeleteItemAsync(name, cancellationToken); + => m_kepwareApiClient.GenericConfig.DeleteItemAsync(name, cancellationToken: cancellationToken); #endregion @@ -254,7 +254,7 @@ public Task DeleteServerUserGroupAsync(string name, CancellationToken canc /// The configuration, or null if not found. public Task GetServerUserAsync(string name, CancellationToken cancellationToken = default) { - return m_kepwareApiClient.GenericConfig.LoadEntityAsync(name, cancellationToken); + return m_kepwareApiClient.GenericConfig.LoadEntityAsync(name, cancellationToken: cancellationToken); } /// @@ -272,7 +272,7 @@ public async Task CreateOrUpdateServerUserAsync(ServerUser user, Cancellat try { var endpointUrl = EndpointResolver.ResolveEndpoint([user.Name]); - var currentUser = await m_kepwareApiClient.GenericConfig.LoadEntityByEndpointAsync(endpointUrl, cancellationToken); + var currentUser = await m_kepwareApiClient.GenericConfig.LoadEntityByEndpointAsync(endpointUrl, cancellationToken: cancellationToken); if (currentUser == null) { @@ -309,7 +309,7 @@ public async Task CreateOrUpdateServerUserAsync(ServerUser user, Cancellat /// A token that can be used to request cancellation of the operation. /// True if the user was successfully deleted; otherwise, false. public Task DeleteServerUserAsync(string name, CancellationToken cancellationToken = default) - => m_kepwareApiClient.GenericConfig.DeleteItemAsync(name, cancellationToken); + => m_kepwareApiClient.GenericConfig.DeleteItemAsync(name, cancellationToken: cancellationToken); #endregion @@ -345,7 +345,7 @@ public Task DeleteServerUserAsync(string name, CancellationToken cancellat public Task GetProjectPermissionAsync(string serverUserGroupName, ProjectPermissionName projectPermissionName, CancellationToken cancellationToken = default) { var endpoint = EndpointResolver.ResolveEndpoint([serverUserGroupName, projectPermissionName]); - return m_kepwareApiClient.GenericConfig.LoadEntityByEndpointAsync(endpoint, cancellationToken); + return m_kepwareApiClient.GenericConfig.LoadEntityByEndpointAsync(endpoint, cancellationToken: cancellationToken); } /// @@ -371,7 +371,7 @@ public async Task UpdateProjectPermissionAsync(string serverUserGroupName, try { var endpointUrl = EndpointResolver.ResolveEndpoint([serverUserGroupName, projectPermission.Name]); - var existingPermission = await m_kepwareApiClient.GenericConfig.LoadEntityByEndpointAsync(endpointUrl, cancellationToken); + var existingPermission = await m_kepwareApiClient.GenericConfig.LoadEntityByEndpointAsync(endpointUrl, cancellationToken: cancellationToken); if (existingPermission == null) { diff --git a/Kepware.Api/ClientHandler/ChannelApiHandler.cs b/Kepware.Api/ClientHandler/ChannelApiHandler.cs index ffb2dbd..5e395da 100644 --- a/Kepware.Api/ClientHandler/ChannelApiHandler.cs +++ b/Kepware.Api/ClientHandler/ChannelApiHandler.cs @@ -52,6 +52,7 @@ public async Task GetOrCreateChannelAsync(string name, string driverNam channel = await CreateChannelAsync(name, driverName, properties, cancellationToken); if (channel != null) { + // TODO: Review this section. This should not do an update if (properties != null) { var currentHash = channel.Hash; diff --git a/Kepware.Api/ClientHandler/DeviceApiHandler.cs b/Kepware.Api/ClientHandler/DeviceApiHandler.cs index 8556062..a39ac57 100644 --- a/Kepware.Api/ClientHandler/DeviceApiHandler.cs +++ b/Kepware.Api/ClientHandler/DeviceApiHandler.cs @@ -205,7 +205,7 @@ public async Task UpdateDeviceAsync(Device device, bool updateTagsAndTagGr if (updateTagsAndTagGroups) { - await m_kepwareApiClient.GenericConfig.CompareAndApply(device.Tags, currentDevice.Tags, device, cancellationToken: cancellationToken); + await m_kepwareApiClient.GenericConfig.CompareAndApplyAsync(device.Tags, currentDevice.Tags, device, cancellationToken: cancellationToken); if (device.TagGroups != null) await ProjectApiHandler.RecusivlyCompareTagGroup(m_kepwareApiClient, device.TagGroups, currentDevice.TagGroups, device, cancellationToken); diff --git a/Kepware.Api/ClientHandler/GenericApiHandler.cs b/Kepware.Api/ClientHandler/GenericApiHandler.cs index 2eada65..ac3c5e3 100644 --- a/Kepware.Api/ClientHandler/GenericApiHandler.cs +++ b/Kepware.Api/ClientHandler/GenericApiHandler.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; +using System.Net; using System.Text; using System.Text.Json; using System.Text.Json.Serialization.Metadata; @@ -49,25 +50,97 @@ internal GenericApiHandler(KepwareApiClient kepwareApiClient, ILoggerThe cancellation token. /// A task that represents the asynchronous operation. The task result contains the comparison result as . - public async Task> CompareAndApply(T? sourceCollection, T? targetCollection, NamedEntity? owner = null, CancellationToken cancellationToken = default) + public async Task> CompareAndApplyAsync(T? sourceCollection, T? targetCollection, NamedEntity? owner = null, CancellationToken cancellationToken = default) + where T : EntityCollection + where K : NamedEntity, new() + { + var result = await CompareAndApplyDetailedAsync(sourceCollection, targetCollection, owner, cancellationToken).ConfigureAwait(false); + return result.CompareResult; + } + + /// + /// Compares two collections and applies changes while returning detailed success and failure information. + /// Left should represent the source collection and Right should represent the target collection in the Kepware server. + /// + /// The type of the entity collection. + /// The type of the entity. + /// The source collection. + /// The collection representing the current state of the API. + /// The owner of the entities. + /// The cancellation token. + /// A detailed apply result including successful counts and failed item details. + public async Task> CompareAndApplyDetailedAsync(T? sourceCollection, T? targetCollection, NamedEntity? owner = null, CancellationToken cancellationToken = default) where T : EntityCollection where K : NamedEntity, new() { var compareResult = EntityCompare.Compare(sourceCollection, targetCollection); + var result = new CollectionApplyResult(compareResult); - // This are the items that are in the API but not in the source - // --> we need to delete them - await DeleteItemsAsync(compareResult.ItemsOnlyInRight.Select(i => i.Right!).ToList(), owner, cancellationToken: cancellationToken).ConfigureAwait(false); + var deleteItems = compareResult.ItemsOnlyInRight.Select(i => i.Right!).ToList(); + var deleteResult = await DeleteItemsAsync(deleteItems, owner, cancellationToken: cancellationToken).ConfigureAwait(false); + for (int i = 0; i < deleteItems.Count; i++) + { + if (i < deleteResult.Length && deleteResult[i]) + { + result.AddDeleteSuccess(); + } + else + { + result.AddFailure(new ApplyFailure + { + Operation = ApplyOperation.Delete, + AttemptedItem = deleteItems[i], + }); + } + } - // This are the items both in the API and the source - // --> we need to update them - await UpdateItemsAsync(compareResult.ChangedItems.Select(i => (i.Left!, i.Right)).ToList(), owner, cancellationToken: cancellationToken).ConfigureAwait(false); + var updatePairs = compareResult.ChangedItems.Select(i => (i.Left!, i.Right)).ToList(); + var updateResult = await UpdateItemsDetailedAsync(updatePairs, owner, cancellationToken).ConfigureAwait(false); + for (int i = 0; i < updatePairs.Count; i++) + { + if (i < updateResult.Count && updateResult[i].IsSuccess) + { + result.AddUpdateSuccess(); + } + else + { + var updateOutcome = i < updateResult.Count ? updateResult[i] : new UpdateItemOutcome(false); + result.AddFailure(new ApplyFailure + { + Operation = ApplyOperation.Update, + AttemptedItem = updatePairs[i].Item1, + ResponseCode = updateOutcome.ResponseCode, + ResponseMessage = updateOutcome.ResponseMessage, + NotAppliedProperties = updateOutcome.NotAppliedProperties, + }); + } + } - // This are the items that are in the source but not in the API - // --> we need to insert them - await InsertItemsAsync(compareResult.ItemsOnlyInLeft.Select(i => i.Left!).ToList(), owner: owner, cancellationToken: cancellationToken).ConfigureAwait(false); + var insertItems = compareResult.ItemsOnlyInLeft.Select(i => i.Left!).ToList(); + var insertResult = await InsertItemsDetailedAsync(insertItems, owner: owner, cancellationToken: cancellationToken).ConfigureAwait(false); + for (int i = 0; i < insertItems.Count; i++) + { + if (i < insertResult.Count && insertResult[i].IsSuccess) + { + result.AddInsertSuccess(); + } + else + { + var insertOutcome = i < insertResult.Count ? insertResult[i] : new InsertItemOutcome(false); + result.AddFailure(new ApplyFailure + { + Operation = ApplyOperation.Insert, + AttemptedItem = insertItems[i], + ResponseCode = insertOutcome.ResponseCode, + ResponseMessage = insertOutcome.ResponseMessage, + Property = insertOutcome.Property, + Description = insertOutcome.Description, + ErrorLine = insertOutcome.ErrorLine, + }); + } + } - return compareResult; + return result; } #endregion @@ -110,6 +183,14 @@ protected internal async Task UpdateItemAsync(string endpoint, T item, } else { + var updateMessage = await TryDeserializeUpdateMessageAsync(response, cancellationToken).ConfigureAwait(false); + if (updateMessage?.NotApplied != null && updateMessage.NotApplied.Count > 0) + { + m_logger.LogError("Partial update detected for {TypeName} on {Endpoint}. Not applied properties: {NotApplied}", + typeof(T).Name, endpoint, updateMessage.NotApplied.Keys); + return false; + } + return true; } } @@ -146,24 +227,29 @@ public async Task UpdateItemAsync(K item, K? oldItem = default, Name /// /// The task result contains an array of booleans indicating whether the update for each item was successful. public async Task UpdateItemsAsync(List<(K item, K? oldItem)> items, NamedEntity? owner = null, CancellationToken cancellationToken = default) + where T : EntityCollection + where K : NamedEntity, new() + => (await UpdateItemsDetailedAsync(items, owner, cancellationToken).ConfigureAwait(false)).Select(i => i.IsSuccess).ToArray(); + + protected internal async Task> UpdateItemsDetailedAsync(List<(K item, K? oldItem)> items, NamedEntity? owner = null, CancellationToken cancellationToken = default) where T : EntityCollection where K : NamedEntity, new() { if (items.Count == 0) return []; - List result = new List(); + List result = []; try { var collectionEndpoint = EndpointResolver.ResolveEndpoint(owner).TrimEnd('/'); foreach (var pair in items) { var endpoint = $"{collectionEndpoint}/{Uri.EscapeDataString(pair.oldItem!.Name)}"; - var currentEntity = await LoadEntityByEndpointAsync(endpoint, cancellationToken).ConfigureAwait(false); + var currentEntity = await LoadEntityByEndpointAsync(endpoint, cancellationToken: cancellationToken).ConfigureAwait(false); if (currentEntity == null) { m_logger.LogError("Failed to load {TypeName} from {Endpoint}", typeof(K).Name, endpoint); - result.Add(false); + result.Add(new UpdateItemOutcome(false)); } else { @@ -179,11 +265,21 @@ public async Task UpdateItemsAsync(List<(K item, K? oldItem)> item { var message = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); m_logger.LogError("Failed to update {TypeName} from {Endpoint}: {ReasonPhrase}\n{Message}", typeof(T).Name, endpoint, response.ReasonPhrase, message); - result.Add(false); + result.Add(new UpdateItemOutcome(false, (int)response.StatusCode, message)); } else { - result.Add(true); + var updateMessage = await TryDeserializeUpdateMessageAsync(response, cancellationToken).ConfigureAwait(false); + if (updateMessage?.NotApplied != null && updateMessage.NotApplied.Count > 0) + { + var notApplied = updateMessage.NotApplied.Keys.ToList(); + m_logger.LogError("Partial update detected for {TypeName} on {Endpoint}. Not applied properties: {NotApplied}", typeof(T).Name, endpoint, notApplied); + result.Add(new UpdateItemOutcome(false, updateMessage.ResponseStatusCode, updateMessage.Message, notApplied)); + } + else + { + result.Add(new UpdateItemOutcome(true, (int)response.StatusCode, updateMessage?.Message)); + } } } } @@ -195,10 +291,14 @@ public async Task UpdateItemsAsync(List<(K item, K? oldItem)> item } if (result.Count < items.Count) - result.AddRange(Enumerable.Repeat(false, items.Count - result.Count)); + result.AddRange(Enumerable.Repeat(new UpdateItemOutcome(false), items.Count - result.Count)); - return [..result]; + return result; } + + + protected internal sealed record UpdateItemOutcome(bool IsSuccess, int? ResponseCode = null, string? ResponseMessage = null, IReadOnlyList? NotAppliedProperties = null); + #endregion #region Insert @@ -267,18 +367,27 @@ public async Task InsertItemAsync(K item, NamedEntity? owner = null, /// /// A task that represents the asynchronous operation. The task result contains an array of booleans indicating whether the each insert was successful. public async Task InsertItemsAsync(List items, int pageSize = 10, NamedEntity? owner = null, CancellationToken cancellationToken = default) + where T : EntityCollection + where K : NamedEntity, new() + => (await InsertItemsDetailedAsync(items, pageSize, owner, cancellationToken).ConfigureAwait(false)).Select(i => i.IsSuccess).ToArray(); + + protected internal async Task> InsertItemsDetailedAsync(List items, int pageSize = 10, NamedEntity? owner = null, CancellationToken cancellationToken = default) where T : EntityCollection where K : NamedEntity, new() { if (items.Count == 0) return []; - List result = new List(); + var result = new InsertItemOutcome?[items.Count]; try { var endpoint = EndpointResolver.ResolveEndpoint(owner); + var supportedItems = new List<(int index, K item)>(); + var unsupportedItems = new List(); + var unsupportedMessage = "Unsupported driver detected for insert."; + if (typeof(K) == typeof(Channel) || typeof(K) == typeof(Device)) { @@ -290,26 +399,41 @@ public async Task InsertItemsAsync(List items, int pageSize = 1 m_cachedSupportedDrivers = await GetSupportedDriversAsync(cancellationToken).ConfigureAwait(false); } - var groupedItems = items - .GroupBy(i => - { - var driver = i.GetDynamicProperty(Properties.Channel.DeviceDriver); - return !string.IsNullOrEmpty(driver) && m_cachedSupportedDrivers.ContainsKey(driver); - }); + for (int i = 0; i < items.Count; i++) + { + var item = items[i]; + var driver = item.GetDynamicProperty(Properties.Channel.DeviceDriver); + var isSupported = !string.IsNullOrEmpty(driver) && m_cachedSupportedDrivers.ContainsKey(driver); + if (isSupported) + { + supportedItems.Add((i, item)); + } + else + { + unsupportedItems.Add(item); + result[i] = new InsertItemOutcome(false, (int)HttpStatusCode.BadRequest, unsupportedMessage); + } + } - var unsupportedItems = groupedItems.FirstOrDefault(g => !g.Key)?.ToList() ?? []; if (unsupportedItems.Count > 0) { - items = groupedItems.FirstOrDefault(g => g.Key)?.ToList() ?? []; m_logger.LogWarning("The following {NumItems} {TypeName} have unsupported drivers ({ListOfUsedUnsupportedDrivers}) and will not be inserted: {ItemsNames}", unsupportedItems.Count, typeof(K).Name, unsupportedItems.Select(i => i.GetDynamicProperty(Properties.Channel.DeviceDriver)).Distinct(), unsupportedItems.Select(i => i.Name)); } } + else + { + for (int i = 0; i < items.Count; i++) + { + supportedItems.Add((i, items[i])); + } + } - var totalPageCount = (int)Math.Ceiling((double)items.Count / pageSize); + var totalPageCount = (int)Math.Ceiling((double)supportedItems.Count / pageSize); for (int i = 0; i < totalPageCount; i++) { - var pageItems = items.Skip(i * pageSize).Take(pageSize).ToList(); + var pageItemMapping = supportedItems.Skip(i * pageSize).Take(pageSize).ToList(); + var pageItems = pageItemMapping.Select(p => p.item).ToList(); m_logger.LogInformation("Inserting {NumItems} {TypeName}(s) on {Endpoint} in batch {BatchNr} of {TotalBatches} ...", pageItems.Count, typeof(K).Name, endpoint, i + 1, totalPageCount); var jsonContent = JsonSerializer.Serialize(pageItems, KepJsonContext.GetJsonListTypeInfo()); @@ -319,7 +443,10 @@ public async Task InsertItemsAsync(List items, int pageSize = 1 { var message = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); m_logger.LogError("Failed to insert {TypeName} from {Endpoint}: {ReasonPhrase}\n{Message}", typeof(T).Name, endpoint, response.ReasonPhrase, message); - result.AddRange(Enumerable.Repeat(false, pageItems.Count)); + foreach (var pageItem in pageItemMapping) + { + result[pageItem.index] = new InsertItemOutcome(false, (int)response.StatusCode, message); + } } else if (response.StatusCode == System.Net.HttpStatusCode.MultiStatus) { @@ -330,7 +457,26 @@ public async Task InsertItemsAsync(List items, int pageSize = 1 await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false), KepJsonContext.Default.ListApiResult, cancellationToken).ConfigureAwait(false) ?? []; - result.AddRange(results.Select(r => r.IsSuccessStatusCode)); + for (int entryIndex = 0; entryIndex < pageItemMapping.Count; entryIndex++) + { + var mappedItem = pageItemMapping[entryIndex]; + if (entryIndex < results.Count) + { + var itemResult = results[entryIndex]; + result[mappedItem.index] = new InsertItemOutcome( + itemResult.IsSuccessStatusCode, + itemResult.Code, + itemResult.Message, + itemResult.Property, + itemResult.Description, + itemResult.ErrorLine); + } + else + { + result[mappedItem.index] = new InsertItemOutcome(false, (int)HttpStatusCode.InternalServerError, + "Multi-status response did not contain an entry for this item."); + } + } var failedEntries = results?.Where(r => !r.IsSuccessStatusCode)?.ToList() ?? []; m_logger.LogError("{NumSuccessFull} were successfull, failed to insert {NumFailed} {TypeName} from {Endpoint}: {ReasonPhrase}\nFailed:\n{Message}", @@ -338,7 +484,10 @@ await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false } else { - result.AddRange(Enumerable.Repeat(true, pageItems.Count)); + foreach (var pageItem in pageItemMapping) + { + result[pageItem.index] = new InsertItemOutcome(true, (int)response.StatusCode, response.ReasonPhrase); + } } } } @@ -346,15 +495,44 @@ await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false { m_logger.LogWarning(httpEx, "Failed to connect to {BaseAddress}", m_httpClient.BaseAddress); m_kepwareApiClient.OnHttpRequestException(httpEx); + } - if (items.Count > result.Count) - result.AddRange(Enumerable.Repeat(false, items.Count - result.Count)); + for (int i = 0; i < result.Length; i++) + { + result[i] ??= new InsertItemOutcome(false); } - return [.. result]; + return result.Select(r => r!).ToList(); } #endregion + private async Task TryDeserializeUpdateMessageAsync(HttpResponseMessage response, CancellationToken cancellationToken) + { + var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + if (string.IsNullOrWhiteSpace(body)) + { + return null; + } + + try + { + return JsonSerializer.Deserialize(body, KepJsonContext.Default.UpdateApiResponseMessage); + } + catch (JsonException) + { + return null; + } + } + + + protected internal sealed record InsertItemOutcome( + bool IsSuccess, + int? ResponseCode = null, + string? ResponseMessage = null, + string? Property = null, + string? Description = null, + int? ErrorLine = null); + #region Delete /// /// Deletes an item from the Kepware server. @@ -420,11 +598,11 @@ protected async Task DeleteItemByEndpointAsync(string endpoint, Cancell /// /// /// - /// - public Task DeleteItemAsync(K item, NamedEntity? owner = null, CancellationToken cancellationToken = default) + /// A task that represents the asynchronous operation. The task result contains a boolean indicating whether the delete was successful. + public async Task DeleteItemAsync(K item, NamedEntity? owner = null, CancellationToken cancellationToken = default) where T : EntityCollection where K : NamedEntity, new() - => DeleteItemsAsync([item], owner, cancellationToken); + => (await DeleteItemsAsync([item], owner, cancellationToken).ConfigureAwait(false)).FirstOrDefault(); /// /// Deletes a list of items from the Kepware server. @@ -434,14 +612,16 @@ public Task DeleteItemAsync(K item, NamedEntity? owner = null, Cance /// /// /// - /// - // TODO: determine return options for mixed results (e.g. some deletes succeed and some fail) - currently returns false if any delete fails, but could also return a list of results for each item - public async Task DeleteItemsAsync(List items, NamedEntity? owner = null, CancellationToken cancellationToken = default) + /// A task that represents the asynchronous operation. The task result contains an array of booleans indicating whether each delete was successful. + public async Task DeleteItemsAsync(List items, NamedEntity? owner = null, CancellationToken cancellationToken = default) where T : EntityCollection where K : NamedEntity, new() { if (items.Count == 0) - return true; + return []; + + List result = []; + try { var collectionEndpoint = EndpointResolver.ResolveEndpoint(owner).TrimEnd('/'); @@ -456,17 +636,24 @@ public async Task DeleteItemsAsync(List items, NamedEntity? owner { var message = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); m_logger.LogError("Failed to delete {TypeName} from {Endpoint}: {ReasonPhrase}\n{Message}", typeof(T).Name, endpoint, response.ReasonPhrase, message); - return false; + result.Add(false); + } + else + { + result.Add(true); } } - return true; } catch (HttpRequestException httpEx) { m_logger.LogWarning(httpEx, "Failed to connect to {BaseAddress}", m_httpClient.BaseAddress); m_kepwareApiClient.OnHttpRequestException(httpEx); + + if (items.Count > result.Count) + result.AddRange(Enumerable.Repeat(false, items.Count - result.Count)); } - return false; + + return [.. result]; } #endregion @@ -478,13 +665,16 @@ public async Task DeleteItemsAsync(List items, NamedEntity? owner /// /// The type of the entity to load. /// The name of the entity to load. If null, loads the default entity. + /// Optional query parameters to append to the request URI. /// A token that can be used to request cancellation of the operation. /// The loaded entity of type or null if not found. - public Task LoadEntityAsync(string? name = default, CancellationToken cancellationToken = default) + public Task LoadEntityAsync(string? name = default, IEnumerable>? query = null, CancellationToken cancellationToken = default) where T : BaseEntity, new() { var endpoint = EndpointResolver.ResolveEndpoint(string.IsNullOrEmpty(name) ? [] : [name]); - return LoadEntityByEndpointAsync(endpoint, cancellationToken); + endpoint = AppendQueryString(endpoint, query); + var serializedRequest = query != null && query.Any(kv => kv.Key.Equals("content", StringComparison.OrdinalIgnoreCase) && kv.Value == "serialize"); + return LoadEntityByEndpointAsync(endpoint, serialized: serializedRequest, cancellationToken: cancellationToken); } /// @@ -492,13 +682,16 @@ public async Task DeleteItemsAsync(List items, NamedEntity? owner /// /// The type of the entity to load. /// The owner of the entity. + /// Optional query parameters to append to the request URI. /// A token that can be used to request cancellation of the operation. /// The loaded entity of type or null if not found. - public Task LoadEntityAsync(IEnumerable owner, CancellationToken cancellationToken = default) + public Task LoadEntityAsync(IEnumerable owner, IEnumerable>? query = null, CancellationToken cancellationToken = default) where T : BaseEntity, new() { var endpoint = EndpointResolver.ResolveEndpoint(owner); - return LoadEntityByEndpointAsync(endpoint, cancellationToken); + endpoint = AppendQueryString(endpoint, query); + var serializedRequest = query != null && query.Any(kv => kv.Key.Equals("content", StringComparison.OrdinalIgnoreCase) && kv.Value == "serialize"); + return LoadEntityByEndpointAsync(endpoint, serialized: serializedRequest, cancellationToken: cancellationToken); } /// @@ -507,14 +700,18 @@ public async Task DeleteItemsAsync(List items, NamedEntity? owner /// The type of the entity to load. /// The name of the entity to load. /// The owner of the entity. + /// Optional query parameters to append to the request URI. /// A token that can be used to request cancellation of the operation. /// The loaded entity of type or null if not found. - public async Task LoadEntityAsync(string name, NamedEntity owner, CancellationToken cancellationToken = default) + public async Task LoadEntityAsync(string name, NamedEntity owner, IEnumerable>? query = null, CancellationToken cancellationToken = default) where T : BaseEntity, new() { var endpoint = EndpointResolver.ResolveEndpoint(owner, name); + endpoint = AppendQueryString(endpoint, query); - var entity = await LoadEntityByEndpointAsync(endpoint, cancellationToken); + var serializedRequest = query != null && query.Any(kv => kv.Key.Equals("content", StringComparison.OrdinalIgnoreCase) && kv.Value == "serialize"); + + var entity = await LoadEntityByEndpointAsync(endpoint, serialized: serializedRequest, cancellationToken: cancellationToken); if (entity is IHaveOwner ownable) { @@ -523,7 +720,7 @@ public async Task DeleteItemsAsync(List items, NamedEntity? owner return entity; } - protected internal async Task LoadEntityByEndpointAsync(string endpoint, CancellationToken cancellationToken = default) + protected internal async Task LoadEntityByEndpointAsync(string endpoint, bool serialized = false, CancellationToken cancellationToken = default) where T : BaseEntity, new() { try @@ -536,8 +733,16 @@ public async Task DeleteItemsAsync(List items, NamedEntity? owner m_logger.LogWarning("Failed to load {TypeName} from {Endpoint}: {ReasonPhrase}", typeof(T).Name, endpoint, response.ReasonPhrase); return default; } + var entity = default(T); - var entity = await DeserializeJsonAsync(response, cancellationToken).ConfigureAwait(false); + if (serialized) + { + entity = await DeserializeJsonLoadSerializedAsync(response, cancellationToken).ConfigureAwait(false); + } + else + { + entity = await DeserializeJsonAsync(response, cancellationToken).ConfigureAwait(false); + } return entity; } @@ -558,22 +763,24 @@ public async Task DeleteItemsAsync(List items, NamedEntity? owner /// /// The type of the entity collection to load. /// The owner of the entity collection. + /// Optional query parameters to append to the request URI. /// A token that can be used to request cancellation of the operation. /// The loaded collection of entities of type or null if not found. - public Task LoadCollectionAsync(string? owner = default, CancellationToken cancellationToken = default) + public Task LoadCollectionAsync(string? owner = default, IEnumerable>? query = null, CancellationToken cancellationToken = default) where T : EntityCollection, new() - => LoadCollectionAsync(owner, cancellationToken); + => LoadCollectionAsync(owner, query, cancellationToken); /// /// Loads a collection of entities of type asynchronously by its owner from the Kepware server. /// /// The type of the entity collection to load. /// The owner of the entity collection. + /// Optional query parameters to append to the request URI. /// A token that can be used to request cancellation of the operation. /// The loaded collection of entities of type or null if not found. - public Task LoadCollectionAsync(NamedEntity owner, CancellationToken cancellationToken = default) + public Task LoadCollectionAsync(NamedEntity owner, IEnumerable>? query = null, CancellationToken cancellationToken = default) where T : EntityCollection, new() - => LoadCollectionAsync(owner, cancellationToken); + => LoadCollectionAsync(owner, query, cancellationToken); /// /// Loads a collection of entities of type asynchronously by its owner from the Kepware server. @@ -581,12 +788,13 @@ public async Task DeleteItemsAsync(List items, NamedEntity? owner /// The type of the entity collection to load. /// The type of the entities in the collection. /// The owner of the entity collection. + /// Optional query parameters to append to the request URI. /// A token that can be used to request cancellation of the operation. /// The loaded collection of entities of type or null if not found. - public Task LoadCollectionAsync(string? owner = default, CancellationToken cancellationToken = default) + public Task LoadCollectionAsync(string? owner = default, IEnumerable>? query = null, CancellationToken cancellationToken = default) where T : EntityCollection, new() where K : BaseEntity, new() - => LoadCollectionAsync(string.IsNullOrEmpty(owner) ? [] : [owner], cancellationToken); + => LoadCollectionAsync(string.IsNullOrEmpty(owner) ? [] : [owner], query, cancellationToken); /// /// Loads a collection of entities of type asynchronously by its owner from the Kepware server. @@ -594,13 +802,14 @@ public async Task DeleteItemsAsync(List items, NamedEntity? owner /// The type of the entity collection to load. /// The type of the entities in the collection. /// The owner of the entity collection. + /// Optional query parameters to append to the request URI. /// A token that can be used to request cancellation of the operation. /// The loaded collection of entities of type or null if not found. - public async Task LoadCollectionAsync(NamedEntity owner, CancellationToken cancellationToken = default) + public async Task LoadCollectionAsync(NamedEntity owner, IEnumerable>? query = null, CancellationToken cancellationToken = default) where T : EntityCollection, new() where K : BaseEntity, new() { - var collection = await LoadCollectionByEndpointAsync(EndpointResolver.ResolveEndpoint(owner), cancellationToken); + var collection = await LoadCollectionByEndpointAsync(AppendQueryString(EndpointResolver.ResolveEndpoint(owner), query), cancellationToken); if (collection != null) { collection.Owner = owner; @@ -618,12 +827,13 @@ public async Task DeleteItemsAsync(List items, NamedEntity? owner /// The type of the entity collection to load. /// The type of the entities in the collection. /// The owner of the entity collection. + /// Optional query parameters to append to the request URI. /// A token that can be used to request cancellation of the operation. /// The loaded collection of entities of type or null if not found. - public Task LoadCollectionAsync(IEnumerable owner, CancellationToken cancellationToken = default) + public Task LoadCollectionAsync(IEnumerable owner, IEnumerable>? query = null, CancellationToken cancellationToken = default) where T : EntityCollection, new() where K : BaseEntity, new() - => LoadCollectionByEndpointAsync(EndpointResolver.ResolveEndpoint(owner), cancellationToken); + => LoadCollectionByEndpointAsync(AppendQueryString(EndpointResolver.ResolveEndpoint(owner), query), cancellationToken); protected internal async Task LoadCollectionByEndpointAsync(string endpoint, CancellationToken cancellationToken = default) where T : EntityCollection, new() @@ -788,7 +998,7 @@ public async Task DeleteItemsAsync(List items, NamedEntity? owner } #endregion - #region private methods + #region private / internal methods #region deserialize protected Task DeserializeJsonAsync(HttpResponseMessage httpResponse, CancellationToken cancellationToken = default) @@ -823,9 +1033,105 @@ public async Task DeleteItemsAsync(List items, NamedEntity? owner return null; } } + + /// + /// Special deserialization method for responses from endpoints with `?content=serialize` that wrap the actual object in an additional + /// layer with the object name as the property name, e.g. `{ "Channel": { ... } }`. This is required to properly handle dynamic properties + /// on channels/devices/etc that conform to a different model from JSON type info for the base entity. This would cause + /// deserialization to fail if we tried to deserialize directly to the target type. + /// + /// + /// + /// + /// + protected Task DeserializeJsonLoadSerializedAsync(HttpResponseMessage httpResponse, CancellationToken cancellationToken = default) + where K : BaseEntity, new() => DeserializeJsonLoadSerializedAsync(httpResponse, KepJsonContext.GetJsonTypeInfo(), cancellationToken); + + /// + /// Special deserialization method for responses from endpoints with `?content=serialize` that wrap the actual object in an additional + /// layer with the object name as the property name, e.g. `{ "Channel": { ... } }`. This is required to properly handle dynamic properties + /// on channels/devices/etc that conform to a different model from JSON type info for the base entity. This would cause + /// deserialization to fail if we tried to deserialize directly to the target type. + /// + /// + /// + /// + /// + /// + protected async Task DeserializeJsonLoadSerializedAsync(HttpResponseMessage httpResponse, JsonTypeInfo jsonTypeInfo, CancellationToken cancellationToken = default) + where K : BaseEntity, new() + { + try + { + using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken); + var wrapper = await JsonSerializer.DeserializeAsync>( + stream, KepJsonContext.Default.DictionaryStringJsonElement, cancellationToken).ConfigureAwait(false) + ?? throw new JsonException("Response was not a JSON object."); + + var first = wrapper.Values.FirstOrDefault(); + if (first.ValueKind != JsonValueKind.Object) + throw new JsonException("Expected the first property to be a JSON object."); + + return first.Deserialize(jsonTypeInfo: KepJsonContext.GetJsonTypeInfo()) + ?? throw new JsonException("Failed to deserialize channel object."); + //return await JsonSerializer.DeserializeAsync(stream, jsonTypeInfo, cancellationToken); + } + catch (JsonException ex) + { + m_logger.LogError(ex, "JSON Deserialization failed"); + return default; + } + } + + #endregion + /// + /// Clears any internal caches (supported drivers, supported channels/devices). + /// Called when the underlying connection is lost so subsequent calls re-fetch data. + /// + internal void InvalidateCaches() + { + // drop cached drivers so next call re-loads from /doc endpoint + m_cachedSupportedDrivers = null; + + // clear cached channel/device property dictionaries + m_cachedSupportedChannels.Clear(); + m_cachedSupportedDevices.Clear(); + } + #endregion + /// + /// Append query parameters to an endpoint string. Encodes keys and values with Uri.EscapeDataString. + /// Null or empty values are skipped. If `endpoint` already contains a query, parameters are appended with &. + /// + private static string AppendQueryString(string endpoint, IEnumerable>? query) + { + if (query == null) + return endpoint; + + var sb = new StringBuilder(); + foreach (var kv in query) + { + if (kv.Key == null) + continue; + // Skip parameters with null values to match typical REST filter behavior. + if (kv.Value is null) + continue; + + if (sb.Length > 0) + sb.Append('&'); + + sb.Append(Uri.EscapeDataString(kv.Key)); + sb.Append('='); + sb.Append(Uri.EscapeDataString(kv.Value)); + } + + if (sb.Length == 0) + return endpoint; + + return endpoint + (endpoint.Contains('?') ? "&" : "?") + sb.ToString(); + } } -} +} \ No newline at end of file diff --git a/Kepware.Api/ClientHandler/IotGatewayApiHandler.cs b/Kepware.Api/ClientHandler/IotGatewayApiHandler.cs new file mode 100644 index 0000000..44f0bf8 --- /dev/null +++ b/Kepware.Api/ClientHandler/IotGatewayApiHandler.cs @@ -0,0 +1,520 @@ +using Kepware.Api.Model; +using Microsoft.Extensions.Logging; + +namespace Kepware.Api.ClientHandler +{ + /// + /// Handles operations related to IoT Gateway agent configurations in the Kepware server. + /// Supports MQTT Client, REST Client, and REST Server agent types and their child IoT Items. + /// + public class IotGatewayApiHandler + { + private readonly KepwareApiClient m_kepwareApiClient; + private readonly ILogger m_logger; + + /// + /// Initializes a new instance of the class. + /// + /// The Kepware Configuration API client. + /// The logger instance. + public IotGatewayApiHandler(KepwareApiClient kepwareApiClient, ILogger logger) + { + m_kepwareApiClient = kepwareApiClient; + m_logger = logger; + } + + #region MQTT Client Agent + + /// + /// Gets or creates an MQTT Client agent with the specified name. + /// If the agent exists, it is loaded and returned. If it does not exist, it is created with the specified properties. + /// + /// The name of the agent. + /// Optional properties to set on the agent. + /// The cancellation token. + /// A task that represents the asynchronous operation. The task result contains the created or loaded . + /// Thrown when the name is null or empty. + /// Thrown when the agent cannot be created or loaded. + public async Task GetOrCreateMqttClientAgentAsync(string name, IDictionary? properties = null, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(name)) + throw new ArgumentException("Agent name cannot be null or empty", nameof(name)); + + var agent = await m_kepwareApiClient.GenericConfig.LoadEntityAsync(name, cancellationToken: cancellationToken); + + if (agent == null) + { + agent = await CreateMqttClientAgentAsync(name, properties, cancellationToken); + if (agent == null) + { + throw new InvalidOperationException($"Failed to create or load MQTT Client agent '{name}'"); + } + } + + return agent; + } + + /// + /// Gets an MQTT Client agent with the specified name. + /// + /// The name of the agent. + /// The cancellation token. + /// The loaded or null if not found. + /// Thrown when the name is null or empty. + public async Task GetMqttClientAgentAsync(string name, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(name)) + throw new ArgumentException("Agent name cannot be null or empty", nameof(name)); + + return await m_kepwareApiClient.GenericConfig.LoadEntityAsync(name, cancellationToken: cancellationToken); + } + + /// + /// Creates a new MQTT Client agent with the specified name. + /// + /// The name of the agent. + /// Optional properties to set on the agent. + /// The cancellation token. + /// The created , or null if creation failed. + /// Thrown when the name is null or empty. + public async Task CreateMqttClientAgentAsync(string name, IDictionary? properties = null, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(name)) + throw new ArgumentException("Agent name cannot be null or empty", nameof(name)); + + var agent = new MqttClientAgent(name); + if (properties != null) + { + foreach (var property in properties) + { + agent.SetDynamicProperty(property.Key, property.Value); + } + } + + if (await m_kepwareApiClient.GenericConfig.InsertItemAsync(agent, cancellationToken: cancellationToken)) + { + return agent; + } + + return null; + } + + /// + /// Updates the specified MQTT Client agent. + /// + /// The agent to update. + /// The cancellation token. + /// A boolean indicating whether the update was successful. + public Task UpdateMqttClientAgentAsync(MqttClientAgent agent, CancellationToken cancellationToken = default) + => m_kepwareApiClient.GenericConfig.UpdateItemAsync(agent, oldItem: null, cancellationToken); + + /// + /// Deletes the specified MQTT Client agent. + /// + /// The agent to delete. + /// The cancellation token. + /// A boolean indicating whether the deletion was successful. + /// Thrown when the agent is null. + public Task DeleteMqttClientAgentAsync(MqttClientAgent agent, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(agent); + return m_kepwareApiClient.GenericConfig.DeleteItemAsync(agent, cancellationToken: cancellationToken); + } + + /// + /// Deletes the MQTT Client agent with the specified name. + /// + /// The name of the agent to delete. + /// The cancellation token. + /// A boolean indicating whether the deletion was successful. + /// Thrown when the name is null or empty. + public Task DeleteMqttClientAgentAsync(string name, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(name)) + throw new ArgumentException("Agent name cannot be null or empty", nameof(name)); + return m_kepwareApiClient.GenericConfig.DeleteItemAsync(name, cancellationToken: cancellationToken); + } + + #endregion + + #region REST Client Agent + + /// + /// Gets or creates a REST Client agent with the specified name. + /// If the agent exists, it is loaded and returned. If it does not exist, it is created with the specified properties. + /// + /// The name of the agent. + /// Optional properties to set on the agent. + /// The cancellation token. + /// A task that represents the asynchronous operation. The task result contains the created or loaded . + /// Thrown when the name is null or empty. + /// Thrown when the agent cannot be created or loaded. + public async Task GetOrCreateRestClientAgentAsync(string name, IDictionary? properties = null, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(name)) + throw new ArgumentException("Agent name cannot be null or empty", nameof(name)); + + var agent = await m_kepwareApiClient.GenericConfig.LoadEntityAsync(name, cancellationToken: cancellationToken); + + if (agent == null) + { + agent = await CreateRestClientAgentAsync(name, properties, cancellationToken); + if (agent == null) + { + throw new InvalidOperationException($"Failed to create or load REST Client agent '{name}'"); + } + } + + return agent; + } + + /// + /// Gets a REST Client agent with the specified name. + /// + /// The name of the agent. + /// The cancellation token. + /// The loaded or null if not found. + /// Thrown when the name is null or empty. + public async Task GetRestClientAgentAsync(string name, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(name)) + throw new ArgumentException("Agent name cannot be null or empty", nameof(name)); + + return await m_kepwareApiClient.GenericConfig.LoadEntityAsync(name, cancellationToken: cancellationToken); + } + + /// + /// Creates a new REST Client agent with the specified name. + /// + /// The name of the agent. + /// Optional properties to set on the agent. + /// The cancellation token. + /// The created , or null if creation failed. + /// Thrown when the name is null or empty. + public async Task CreateRestClientAgentAsync(string name, IDictionary? properties = null, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(name)) + throw new ArgumentException("Agent name cannot be null or empty", nameof(name)); + + var agent = new RestClientAgent(name); + if (properties != null) + { + foreach (var property in properties) + { + agent.SetDynamicProperty(property.Key, property.Value); + } + } + + if (await m_kepwareApiClient.GenericConfig.InsertItemAsync(agent, cancellationToken: cancellationToken)) + { + return agent; + } + + return null; + } + + /// + /// Updates the specified REST Client agent. + /// + /// The agent to update. + /// The cancellation token. + /// A boolean indicating whether the update was successful. + public Task UpdateRestClientAgentAsync(RestClientAgent agent, CancellationToken cancellationToken = default) + => m_kepwareApiClient.GenericConfig.UpdateItemAsync(agent, oldItem: null, cancellationToken); + + /// + /// Deletes the specified REST Client agent. + /// + /// The agent to delete. + /// The cancellation token. + /// A boolean indicating whether the deletion was successful. + /// Thrown when the agent is null. + public Task DeleteRestClientAgentAsync(RestClientAgent agent, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(agent); + return m_kepwareApiClient.GenericConfig.DeleteItemAsync(agent, cancellationToken: cancellationToken); + } + + /// + /// Deletes the REST Client agent with the specified name. + /// + /// The name of the agent to delete. + /// The cancellation token. + /// A boolean indicating whether the deletion was successful. + /// Thrown when the name is null or empty. + public Task DeleteRestClientAgentAsync(string name, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(name)) + throw new ArgumentException("Agent name cannot be null or empty", nameof(name)); + return m_kepwareApiClient.GenericConfig.DeleteItemAsync(name, cancellationToken: cancellationToken); + } + + #endregion + + #region REST Server Agent + + /// + /// Gets or creates a REST Server agent with the specified name. + /// If the agent exists, it is loaded and returned. If it does not exist, it is created with the specified properties. + /// + /// The name of the agent. + /// Optional properties to set on the agent. + /// The cancellation token. + /// A task that represents the asynchronous operation. The task result contains the created or loaded . + /// Thrown when the name is null or empty. + /// Thrown when the agent cannot be created or loaded. + public async Task GetOrCreateRestServerAgentAsync(string name, IDictionary? properties = null, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(name)) + throw new ArgumentException("Agent name cannot be null or empty", nameof(name)); + + var agent = await m_kepwareApiClient.GenericConfig.LoadEntityAsync(name, cancellationToken: cancellationToken); + + if (agent == null) + { + agent = await CreateRestServerAgentAsync(name, properties, cancellationToken); + if (agent == null) + { + throw new InvalidOperationException($"Failed to create or load REST Server agent '{name}'"); + } + } + + return agent; + } + + /// + /// Gets a REST Server agent with the specified name. + /// + /// The name of the agent. + /// The cancellation token. + /// The loaded or null if not found. + /// Thrown when the name is null or empty. + public async Task GetRestServerAgentAsync(string name, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(name)) + throw new ArgumentException("Agent name cannot be null or empty", nameof(name)); + + return await m_kepwareApiClient.GenericConfig.LoadEntityAsync(name, cancellationToken: cancellationToken); + } + + /// + /// Creates a new REST Server agent with the specified name. + /// + /// The name of the agent. + /// Optional properties to set on the agent. + /// The cancellation token. + /// The created , or null if creation failed. + /// Thrown when the name is null or empty. + public async Task CreateRestServerAgentAsync(string name, IDictionary? properties = null, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(name)) + throw new ArgumentException("Agent name cannot be null or empty", nameof(name)); + + var agent = new RestServerAgent(name); + if (properties != null) + { + foreach (var property in properties) + { + agent.SetDynamicProperty(property.Key, property.Value); + } + } + + if (await m_kepwareApiClient.GenericConfig.InsertItemAsync(agent, cancellationToken: cancellationToken)) + { + return agent; + } + + return null; + } + + /// + /// Updates the specified REST Server agent. + /// + /// The agent to update. + /// The cancellation token. + /// A boolean indicating whether the update was successful. + public Task UpdateRestServerAgentAsync(RestServerAgent agent, CancellationToken cancellationToken = default) + => m_kepwareApiClient.GenericConfig.UpdateItemAsync(agent, oldItem: null, cancellationToken); + + /// + /// Deletes the specified REST Server agent. + /// + /// The agent to delete. + /// The cancellation token. + /// A boolean indicating whether the deletion was successful. + /// Thrown when the agent is null. + public Task DeleteRestServerAgentAsync(RestServerAgent agent, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(agent); + return m_kepwareApiClient.GenericConfig.DeleteItemAsync(agent, cancellationToken: cancellationToken); + } + + /// + /// Deletes the REST Server agent with the specified name. + /// + /// The name of the agent to delete. + /// The cancellation token. + /// A boolean indicating whether the deletion was successful. + /// Thrown when the name is null or empty. + public Task DeleteRestServerAgentAsync(string name, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(name)) + throw new ArgumentException("Agent name cannot be null or empty", nameof(name)); + return m_kepwareApiClient.GenericConfig.DeleteItemAsync(name, cancellationToken: cancellationToken); + } + + #endregion + + #region IoT Items + + /// + /// Converts a dot-delimited server tag name to the IoT Item name used by the Kepware API. + /// Replaces dots with underscores and strips a leading underscore if present. + /// For example, "Channel1.Device1.Tag1" becomes "Channel1_Device1_Tag1" + /// and "_System._Time" becomes "System__Time". + /// + /// The dot-delimited server tag name. + /// The converted IoT Item name. + internal static string ServerTagToItemName(string serverTag) + { + var name = serverTag.Replace('.', '_'); + if (name.StartsWith('_')) + name = name[1..]; + return name; + } + + /// + /// Gets or creates an IoT Item for the specified server tag under the given parent agent. + /// If the item exists, it is loaded and returned. If it does not exist, it is created with the specified properties. + /// The IoT Item name is derived from the server tag by replacing dots with underscores and stripping any leading underscore. + /// + /// The dot-delimited server tag reference (e.g., "Channel1.Device1.Tag1"). + /// The parent agent that will own the IoT Item. + /// Optional properties to set on the IoT Item. + /// The cancellation token. + /// A task that represents the asynchronous operation. The task result contains the created or loaded . + /// Thrown when the server tag is null or empty. + /// Thrown when the parent agent is null. + /// Thrown when the item cannot be created or loaded. + public async Task GetOrCreateIotItemAsync(string serverTag, IotAgent parentAgent, IDictionary? properties = null, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(serverTag)) + throw new ArgumentException("Server tag cannot be null or empty", nameof(serverTag)); + ArgumentNullException.ThrowIfNull(parentAgent); + + var itemName = ServerTagToItemName(serverTag); + var item = await m_kepwareApiClient.GenericConfig.LoadEntityAsync(itemName, parentAgent, cancellationToken: cancellationToken); + + if (item == null) + { + item = await CreateIotItemAsync(serverTag, parentAgent, properties, cancellationToken); + if (item == null) + { + throw new InvalidOperationException($"Failed to create or load IoT Item for server tag '{serverTag}'"); + } + } + + return item; + } + + /// + /// Gets an IoT Item by its server tag name under the given parent agent. + /// The server tag is converted to the IoT Item name by replacing dots with underscores and stripping any leading underscore. + /// + /// The dot-delimited server tag name (e.g., "Channel1.Device1.Tag1"). + /// The parent agent that owns the IoT Item. + /// The cancellation token. + /// The loaded or null if not found. + /// Thrown when the server tag is null or empty. + /// Thrown when the parent agent is null. + public async Task GetIotItemAsync(string serverTag, IotAgent parentAgent, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(serverTag)) + throw new ArgumentException("Server tag cannot be null or empty", nameof(serverTag)); + ArgumentNullException.ThrowIfNull(parentAgent); + + var itemName = ServerTagToItemName(serverTag); + return await m_kepwareApiClient.GenericConfig.LoadEntityAsync(itemName, parentAgent, cancellationToken: cancellationToken); + } + + /// + /// Creates a new IoT Item for the specified server tag under the given parent agent. + /// The IoT Item name is derived from the server tag by replacing dots with underscores and stripping any leading underscore. + /// + /// The dot-delimited server tag reference (e.g., "Channel1.Device1.Tag1"). + /// The parent agent that will own the IoT Item. + /// Optional properties to set on the IoT Item. + /// The cancellation token. + /// The created , or null if creation failed. + /// Thrown when the server tag is null or empty. + /// Thrown when the parent agent is null. + public async Task CreateIotItemAsync(string serverTag, IotAgent parentAgent, IDictionary? properties = null, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(serverTag)) + throw new ArgumentException("Server tag cannot be null or empty", nameof(serverTag)); + ArgumentNullException.ThrowIfNull(parentAgent); + + var itemName = ServerTagToItemName(serverTag); + var item = new IotItem(itemName) { Owner = parentAgent }; + item.SetDynamicProperty(Properties.IotItem.ServerTag, serverTag); + + if (properties != null) + { + foreach (var property in properties) + { + item.SetDynamicProperty(property.Key, property.Value); + } + } + + if (await m_kepwareApiClient.GenericConfig.InsertItemAsync(item, parentAgent, cancellationToken: cancellationToken)) + { + return item; + } + + return null; + } + + /// + /// Updates the specified IoT Item. + /// + /// The IoT Item to update. + /// The cancellation token. + /// A boolean indicating whether the update was successful. + public Task UpdateIotItemAsync(IotItem item, CancellationToken cancellationToken = default) + => m_kepwareApiClient.GenericConfig.UpdateItemAsync(item, oldItem: null, cancellationToken); + + /// + /// Deletes the specified IoT Item. + /// + /// The IoT Item to delete. + /// The cancellation token. + /// A boolean indicating whether the deletion was successful. + /// Thrown when the item is null. + public Task DeleteIotItemAsync(IotItem item, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(item); + return m_kepwareApiClient.GenericConfig.DeleteItemAsync(item, cancellationToken: cancellationToken); + } + + /// + /// Deletes the IoT Item identified by the specified server tag under the given parent agent. + /// The server tag is converted to the IoT Item name by replacing dots with underscores and stripping any leading underscore. + /// + /// The dot-delimited server tag name (e.g., "Channel1.Device1.Tag1"). + /// The parent agent that owns the IoT Item. + /// The cancellation token. + /// A boolean indicating whether the deletion was successful. + /// Thrown when the server tag is null or empty. + /// Thrown when the parent agent is null. + public Task DeleteIotItemAsync(string serverTag, IotAgent parentAgent, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(serverTag)) + throw new ArgumentException("Server tag cannot be null or empty", nameof(serverTag)); + ArgumentNullException.ThrowIfNull(parentAgent); + var itemName = ServerTagToItemName(serverTag); + return m_kepwareApiClient.GenericConfig.DeleteItemAsync([parentAgent.Name!, itemName], cancellationToken: cancellationToken); + } + + #endregion + } +} diff --git a/Kepware.Api/ClientHandler/ProjectApiHandler.cs b/Kepware.Api/ClientHandler/ProjectApiHandler.cs index cd3d028..c239b94 100644 --- a/Kepware.Api/ClientHandler/ProjectApiHandler.cs +++ b/Kepware.Api/ClientHandler/ProjectApiHandler.cs @@ -36,20 +36,28 @@ public class ProjectApiHandler /// See for method references. public DeviceApiHandler Devices { get; } + /// + /// Gets the IoT Gateway handlers. + /// + /// See for method references. + public IotGatewayApiHandler IotGateway { get; } + /// /// Initializes a new instance of the class. /// /// The Kepware API client. /// The channel API handler. /// The device API handler. + /// The IoT Gateway API handler. /// The logger instance. - public ProjectApiHandler(KepwareApiClient kepwareApiClient, ChannelApiHandler channelApiHandler, DeviceApiHandler deviceApiHandler, ILogger logger) + public ProjectApiHandler(KepwareApiClient kepwareApiClient, ChannelApiHandler channelApiHandler, DeviceApiHandler deviceApiHandler, IotGatewayApiHandler iotGatewayApiHandler, ILogger logger) { m_kepwareApiClient = kepwareApiClient; m_logger = logger; Channels = channelApiHandler; Devices = deviceApiHandler; + IotGateway = iotGatewayApiHandler; } #region CompareAndApply @@ -59,11 +67,10 @@ public ProjectApiHandler(KepwareApiClient kepwareApiClient, ChannelApiHandler ch /// The source project to compare. /// The cancellation token. /// A task that represents the asynchronous operation. The task result contains a tuple with the counts of inserts, updates, and deletes. - public async Task<(int inserts, int updates, int deletes)> CompareAndApply(Project sourceProject, CancellationToken cancellationToken = default) + public async Task<(int inserts, int updates, int deletes)> CompareAndApplyAsync(Project sourceProject, CancellationToken cancellationToken = default) { - var projectFromApi = await LoadProject(blnLoadFullProject: true, cancellationToken: cancellationToken); - await projectFromApi.Cleanup(m_kepwareApiClient, true, cancellationToken).ConfigureAwait(false); - return await CompareAndApply(sourceProject, projectFromApi, cancellationToken).ConfigureAwait(false); + var result = await CompareAndApplyDetailedAsync(sourceProject, cancellationToken).ConfigureAwait(false); + return (result.Inserts, result.Updates, result.Deletes); } /// @@ -73,76 +80,87 @@ public ProjectApiHandler(KepwareApiClient kepwareApiClient, ChannelApiHandler ch /// The project loaded from the API. /// The cancellation token. /// A task that represents the asynchronous operation. The task result contains a tuple with the counts of inserts, updates, and deletes. - public async Task<(int inserts, int updates, int deletes)> CompareAndApply(Project sourceProject, Project projectFromApi, CancellationToken cancellationToken = default) + public async Task<(int inserts, int updates, int deletes)> CompareAndApplyAsync(Project sourceProject, Project projectFromApi, CancellationToken cancellationToken = default) + { + var result = await CompareAndApplyDetailedAsync(sourceProject, projectFromApi, cancellationToken).ConfigureAwait(false); + return (result.Inserts, result.Updates, result.Deletes); + } + + /// + /// Compares the source project with the project from the API and applies changes while returning detailed success and failure information. + /// + /// The source project to compare. + /// The cancellation token. + /// A including counts and failed items. + public async Task CompareAndApplyDetailedAsync(Project sourceProject, CancellationToken cancellationToken = default) + { + var projectFromApi = await LoadProjectAsync(blnLoadFullProject: true, cancellationToken: cancellationToken).ConfigureAwait(false); + await projectFromApi.Cleanup(m_kepwareApiClient, true, cancellationToken).ConfigureAwait(false); + return await CompareAndApplyDetailedAsync(sourceProject, projectFromApi, cancellationToken).ConfigureAwait(false); + } + + /// + /// Compares the source project with the project from the API and applies changes while returning detailed success and failure information. + /// + /// The source project to compare. + /// The project loaded from the API. + /// The cancellation token. + /// A including counts and failed items. + public async Task CompareAndApplyDetailedAsync(Project sourceProject, Project projectFromApi, CancellationToken cancellationToken = default) { - int inserts = 0, updates = 0, deletes = 0; + var result = new ProjectCompareAndApplyResult(); if (sourceProject.Hash != projectFromApi.Hash) { m_logger.LogInformation("Project properties has changed. Updating project properties..."); - var result = await SetProjectPropertiesAsync(sourceProject, cancellationToken: cancellationToken).ConfigureAwait(false); - if (result) + var projectPropertyFailure = await SetProjectPropertiesDetailedAsync(sourceProject, cancellationToken: cancellationToken).ConfigureAwait(false); + if (projectPropertyFailure == null) { - updates += 1; + result.AddUpdateSuccess(); } else { m_logger.LogError("Failed to update project properties..."); + result.AddFailure(projectPropertyFailure); } } - - var channelCompare = await m_kepwareApiClient.GenericConfig.CompareAndApply(sourceProject.Channels, projectFromApi.Channels, + var channelCompare = await m_kepwareApiClient.GenericConfig.CompareAndApplyDetailedAsync(sourceProject.Channels, projectFromApi.Channels, cancellationToken: cancellationToken).ConfigureAwait(false); + result.Add(channelCompare); - updates += channelCompare.ChangedItems.Count; - inserts += channelCompare.ItemsOnlyInLeft.Count; - deletes += channelCompare.ItemsOnlyInRight.Count; - - foreach (var channel in channelCompare.UnchangedItems.Concat(channelCompare.ChangedItems)) + foreach (var channel in channelCompare.CompareResult.UnchangedItems.Concat(channelCompare.CompareResult.ChangedItems)) { - var deviceCompare = await m_kepwareApiClient.GenericConfig.CompareAndApply(channel.Left!.Devices, channel.Right!.Devices, channel.Right, + var deviceCompare = await m_kepwareApiClient.GenericConfig.CompareAndApplyDetailedAsync(channel.Left!.Devices, channel.Right!.Devices, channel.Right, cancellationToken: cancellationToken).ConfigureAwait(false); + result.Add(deviceCompare); - updates += deviceCompare.ChangedItems.Count; - inserts += deviceCompare.ItemsOnlyInLeft.Count; - deletes += deviceCompare.ItemsOnlyInRight.Count; - - foreach (var device in deviceCompare.UnchangedItems.Concat(deviceCompare.ChangedItems)) + foreach (var device in deviceCompare.CompareResult.UnchangedItems.Concat(deviceCompare.CompareResult.ChangedItems)) { - var tagCompare = await m_kepwareApiClient.GenericConfig.CompareAndApply(device.Left!.Tags, device.Right!.Tags, device.Right, cancellationToken).ConfigureAwait(false); - - updates += tagCompare.ChangedItems.Count; - inserts += tagCompare.ItemsOnlyInLeft.Count; - deletes += tagCompare.ItemsOnlyInRight.Count; + var tagCompare = await m_kepwareApiClient.GenericConfig.CompareAndApplyDetailedAsync(device.Left!.Tags, device.Right!.Tags, device.Right, cancellationToken).ConfigureAwait(false); + result.Add(tagCompare); - var tagGroupCompare = await m_kepwareApiClient.GenericConfig.CompareAndApply(device.Left!.TagGroups, device.Right!.TagGroups, device.Right, cancellationToken).ConfigureAwait(false); + var tagGroupCompare = await m_kepwareApiClient.GenericConfig.CompareAndApplyDetailedAsync(device.Left!.TagGroups, device.Right!.TagGroups, device.Right, cancellationToken).ConfigureAwait(false); + result.Add(tagGroupCompare); - updates += tagGroupCompare.ChangedItems.Count; - inserts += tagGroupCompare.ItemsOnlyInLeft.Count; - deletes += tagGroupCompare.ItemsOnlyInRight.Count; - - - foreach (var tagGroup in tagGroupCompare.UnchangedItems.Concat(tagGroupCompare.ChangedItems)) + foreach (var tagGroup in tagGroupCompare.CompareResult.UnchangedItems.Concat(tagGroupCompare.CompareResult.ChangedItems)) { - var tagGroupTagCompare = await m_kepwareApiClient.GenericConfig.CompareAndApply(tagGroup.Left!.Tags, tagGroup.Right!.Tags, tagGroup.Right, cancellationToken).ConfigureAwait(false); - - updates += tagGroupTagCompare.ChangedItems.Count; - inserts += tagGroupTagCompare.ItemsOnlyInLeft.Count; - deletes += tagGroupTagCompare.ItemsOnlyInRight.Count; + var tagGroupTagCompare = await m_kepwareApiClient.GenericConfig.CompareAndApplyDetailedAsync(tagGroup.Left!.Tags, tagGroup.Right!.Tags, tagGroup.Right, cancellationToken).ConfigureAwait(false); + result.Add(tagGroupTagCompare); if (tagGroup.Left?.TagGroups != null) { - var result = await RecusivlyCompareTagGroup(tagGroup.Left!.TagGroups, tagGroup.Right!.TagGroups, tagGroup.Right, cancellationToken).ConfigureAwait(false); - updates += result.updates; - inserts += result.inserts; - deletes += result.deletes; + var recursiveResult = await RecusivlyCompareTagGroupDetailed(tagGroup.Left!.TagGroups, tagGroup.Right!.TagGroups, tagGroup.Right, cancellationToken).ConfigureAwait(false); + result.Add(recursiveResult); } } } } - return (inserts, updates, deletes); + // Compare and apply IoT Gateway agents and their IoT Items + await CompareAndApplyIotGatewayDetailedAsync(sourceProject.IotGateway, projectFromApi.IotGateway, result, cancellationToken).ConfigureAwait(false); + + return result; } #endregion @@ -169,6 +187,11 @@ public ProjectApiHandler(KepwareApiClient kepwareApiClient, ChannelApiHandler ch /// A task that represents the asynchronous operation. The task result contains a boolean indicating whether the update was successful. /// public async Task SetProjectPropertiesAsync(Project project, CancellationToken cancellationToken = default) + { + return await SetProjectPropertiesDetailedAsync(project, cancellationToken).ConfigureAwait(false) == null; + } + + private async Task SetProjectPropertiesDetailedAsync(Project project, CancellationToken cancellationToken = default) { try { @@ -199,10 +222,32 @@ public async Task SetProjectPropertiesAsync(Project project, CancellationT { var message = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); m_logger.LogError("Failed to update Project Property Settings from {Endpoint}: {ReasonPhrase}\n{Message}", endpoint, response.ReasonPhrase, message); + return new ApplyFailure + { + Operation = ApplyOperation.Update, + AttemptedItem = project, + ResponseCode = (int)response.StatusCode, + ResponseMessage = message, + }; } else { - return true; + var updateMessage = await TryDeserializeUpdateMessageAsync(response, cancellationToken).ConfigureAwait(false); + if (updateMessage?.NotApplied != null && updateMessage.NotApplied.Count > 0) + { + var notApplied = updateMessage.NotApplied.Keys.ToList(); + m_logger.LogError("Partial update detected for project properties on {Endpoint}. Not applied properties: {NotApplied}", endpoint, notApplied); + return new ApplyFailure + { + Operation = ApplyOperation.Update, + AttemptedItem = project, + ResponseCode = updateMessage.ResponseStatusCode, + ResponseMessage = updateMessage.Message, + NotAppliedProperties = notApplied, + }; + } + + return null; } } catch (HttpRequestException httpEx) @@ -211,62 +256,140 @@ public async Task SetProjectPropertiesAsync(Project project, CancellationT m_kepwareApiClient.OnHttpRequestException(httpEx); } - return false; + return new ApplyFailure + { + Operation = ApplyOperation.Update, + AttemptedItem = project, + }; + } + + private static async Task TryDeserializeUpdateMessageAsync(HttpResponseMessage response, CancellationToken cancellationToken) + { + var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + if (string.IsNullOrWhiteSpace(body)) + { + return null; + } + + try + { + return JsonSerializer.Deserialize(body, KepJsonContext.Default.UpdateApiResponseMessage); + } + catch (JsonException) + { + return null; + } } #endregion #region LoadProject + + /// + /// Does the same as but is marked as obsolete. + /// + /// Indicates whether to load the full project. + /// The cancellation token. + /// A task that represents the asynchronous operation. The task result contains the loaded . + /// Deprecated in v1.1.0; will be removed in future release + [Obsolete("Use LoadProjectAsync() instead. This will be removed in future release", false)] + [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] + public async Task LoadProject(bool blnLoadFullProject = false, CancellationToken cancellationToken = default) + { + return await LoadProjectAsync(blnLoadFullProject, m_kepwareApiClient.ClientOptions.ProjectLoadTagLimit ,cancellationToken).ConfigureAwait(false); + } + /// /// Loads the project from the Kepware server. If is true, it loads the full project, otherwise only /// the project properties will be returned. /// /// Indicates whether to load the full project. + /// The tag count threshold to determine whether to use optimized content=serialize + /// loading or basic recursive loading when loading the full project. This is only applicable for projects loaded with + /// the full project load option and when the JsonProjectLoad service is supported by the server. /// The cancellation token. /// A task that represents the asynchronous operation. The task result contains the loaded . - /// NOTE: When loading a full project, the project will be loaded via the JsonProjectLoad service if supported by the server. (Kepware Server 6.17+ - /// and Thingworx Kepware Server 1.10+) Otherwise, the project will be loaded by recursively loading all objects in the project. - public async Task LoadProject(bool blnLoadFullProject = false, CancellationToken cancellationToken = default) + /// NOTE: When loading a full project, the project will be loaded either via the JsonProjectLoad service, an "optimized" + /// recursion that uses the content=serialize query or a basic recurion through project tree. Putting a value for + /// will override the value set in during initial client creation. + public async Task LoadProjectAsync(bool blnLoadFullProject = false, int projectLoadTagLimit = 0, CancellationToken cancellationToken = default) { Stopwatch stopwatch = Stopwatch.StartNew(); + var project = await m_kepwareApiClient.GenericConfig.LoadEntityAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + + if (project == null) + { + m_logger.LogWarning("Failed to load project"); + project = new Project(); + } + + // If not loading full project, return with just project properties. + if (!blnLoadFullProject) + { + return project; + } + + var productInfo = await m_kepwareApiClient.GetProductInfoAsync(cancellationToken).ConfigureAwait(false); - if (blnLoadFullProject && productInfo?.SupportsJsonProjectLoadService == true) + if (productInfo?.SupportsJsonProjectLoadService == true) { + // Check to see if projectLoadTagLimit parameter is set on call. If not or an invalid value, use value ] + // set by as the threshold to use content=serialize + // based loading or full recursive loading. + if (projectLoadTagLimit <= 0) + projectLoadTagLimit = m_kepwareApiClient.ClientOptions.ProjectLoadTagLimit; + try { - var response = await m_kepwareApiClient.HttpClient.GetAsync(ENDPONT_FULL_PROJECT, cancellationToken).ConfigureAwait(false); - if (response.IsSuccessStatusCode) + + // Optimized recursive loading approach that uses the content=serialize query parameter. + // This approach significantly reduces the number of API calls when loading projects with a large number of tags and prevents + // timeout errors with large projects. + + if (int.TryParse(project.GetDynamicProperty(Properties.ProjectSettings.TagsDefined), out int count) && count > projectLoadTagLimit) { - var prjRoot = await JsonSerializer.DeserializeAsync( - await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false), - KepJsonContext.Default.JsonProjectRoot, cancellationToken).ConfigureAwait(false); + m_logger.LogInformation("Project has greater than {TagLimit} tags defined. Loading project via optimized recursion...", projectLoadTagLimit); + + project = await LoadProjectOptimizedRecurisveAsync(project, projectLoadTagLimit, cancellationToken).ConfigureAwait(false); - if (prjRoot?.Project != null) + if (!project.IsEmpty) { - prjRoot.Project.IsLoadedByProjectLoadService = true; + SetOwnersFullProject(project); + m_logger.LogInformation("Loaded project via optimized recursion in {ElapsedMilliseconds} ms", stopwatch.ElapsedMilliseconds); + } + return project; + } - if (prjRoot.Project.Channels != null) - foreach (var channel in prjRoot.Project.Channels) - { - if (channel.Devices != null) - foreach (var device in channel.Devices) - { - device.Owner = channel; + // If project has less than tagLimit number of tags, load full project via JsonProjectLoad service. + else + { + m_logger.LogInformation("Project has less than {TagLimit} tags defined. Loading project via JsonProjectLoad Service...", projectLoadTagLimit); + var response = await m_kepwareApiClient.HttpClient.GetAsync(ENDPONT_FULL_PROJECT, cancellationToken).ConfigureAwait(false); + if (response.IsSuccessStatusCode) + { + var prjRoot = await JsonSerializer.DeserializeAsync( + await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false), + KepJsonContext.Default.JsonProjectRoot, cancellationToken).ConfigureAwait(false); - if (device.Tags != null) - foreach (var tag in device.Tags) - tag.Owner = device; + // Set the Owner property for all loaded entities. + if (prjRoot?.Project != null) + { + prjRoot.Project.IsLoadedByProjectLoadService = true; - if (device.TagGroups != null) - SetOwnerRecursive(device.TagGroups, device); - } - } + SetOwnersFullProject(prjRoot.Project); - m_logger.LogInformation("Loaded project via JsonProjectLoad Service in {ElapsedMilliseconds} ms", stopwatch.ElapsedMilliseconds); - return prjRoot.Project; + m_logger.LogInformation("Loaded project via JsonProjectLoad Service in {ElapsedMilliseconds} ms", stopwatch.ElapsedMilliseconds); + project = prjRoot.Project; + } + else + { + m_logger.LogWarning("Failed to deserialize project loaded via JsonProjectLoad Service"); + project = new Project(); + } } + return project; } } catch (HttpRequestException httpEx) @@ -280,61 +403,297 @@ await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false } else { - var project = await m_kepwareApiClient.GenericConfig.LoadEntityAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + project = await LoadProjectRecursiveAsync(project, cancellationToken).ConfigureAwait(false); - if (project == null) + if (!project.IsEmpty) { - m_logger.LogWarning("Failed to load project"); - project = new Project(); + m_logger.LogInformation("Loaded project in via non-optimized recursion {ElapsedMilliseconds} ms", stopwatch.ElapsedMilliseconds); } - else if (blnLoadFullProject) - { - project.Channels = await m_kepwareApiClient.GenericConfig.LoadCollectionAsync(cancellationToken: cancellationToken).ConfigureAwait(false); - if (project.Channels != null) - { - int totalChannelCount = project.Channels.Count; - int loadedChannelCount = 0; - await Task.WhenAll(project.Channels.Select(async channel => + return project; + } + } + + private static void SetOwnersFullProject(Project project) + { + if (project.Channels != null) + foreach (var channel in project.Channels) + { + if (channel.Devices != null) + foreach (var device in channel.Devices) { - channel.Devices = await m_kepwareApiClient.GenericConfig.LoadCollectionAsync(channel, cancellationToken).ConfigureAwait(false); + device.Owner = channel; - if (channel.Devices != null) - { - await Task.WhenAll(channel.Devices.Select(async device => - { - device.Tags = await m_kepwareApiClient.GenericConfig.LoadCollectionAsync(device, cancellationToken: cancellationToken).ConfigureAwait(false); - device.TagGroups = await m_kepwareApiClient.GenericConfig.LoadCollectionAsync(device, cancellationToken: cancellationToken).ConfigureAwait(false); + if (device.Tags != null) + foreach (var tag in device.Tags) + tag.Owner = device; - if (device.TagGroups != null) - { - await LoadTagGroupsRecursiveAsync(m_kepwareApiClient, device.TagGroups, cancellationToken: cancellationToken).ConfigureAwait(false); - } - })); - } - // Log information, loaded channel x of y - loadedChannelCount++; - if (totalChannelCount == 1) + if (device.TagGroups != null) + SetOwnerRecursive(device.TagGroups, device); + } + } + + if (project.IotGateway != null) + { + foreach (var agent in (project.IotGateway.MqttClientAgents ?? []).Cast() + .Concat(project.IotGateway.RestClientAgents ?? []) + .Concat(project.IotGateway.RestServerAgents ?? [])) + { + if (agent.IotItems != null) + foreach (var item in agent.IotItems) + item.Owner = agent; + } + } + } + + private async Task LoadProjectOptimizedRecurisveAsync(Project project, int tagLimit, CancellationToken cancellationToken = default) + { + project.Channels = await m_kepwareApiClient.GenericConfig.LoadCollectionAsync(cancellationToken: cancellationToken); + if (project.Channels != null) + { + var query = new[] + { + new KeyValuePair("content", "serialize") + }; + int totalChannelCount = project.Channels.Count; + int loadedChannelCount = 0; + + // Create a list of tasks by iterating indices to avoid modifying the collection while it's being enumerated. + var channelTasks = new List(); + for (int c_index = 0; c_index < project.Channels.Count; c_index++) + { + int channelIndex = c_index; + var channel = project.Channels[channelIndex]; + channelTasks.Add(Task.Run(async () => + { + if (channel.GetDynamicProperty(Properties.Channel.StaticTagCount) < tagLimit) + { + var loadedChannel = await m_kepwareApiClient.GenericConfig.LoadEntityAsync(channel.Name, query, cancellationToken: cancellationToken).ConfigureAwait(false); + if (loadedChannel != null) { - m_logger.LogInformation("Loaded channel {ChannelName}", channel.Name); + project.Channels[channelIndex] = loadedChannel; } else { - m_logger.LogInformation("Loaded channel {ChannelName} {LoadedChannelCount} of {TotalChannelCount}", channel.Name, loadedChannelCount, totalChannelCount); + // Failed to load channel, log warning and end without incrementing completion. + m_logger.LogWarning("Failed to load {ChannelName}", channel.Name); + return; } + } + else + { + channel.Devices = await m_kepwareApiClient.GenericConfig.LoadCollectionAsync(channel, cancellationToken: cancellationToken).ConfigureAwait(false); + + if (channel.Devices != null) + { + var deviceTasks = new List(); + for (int d_index = 0; d_index < channel.Devices.Count; d_index++) + { + int deviceIndex = d_index; + var device = channel.Devices[deviceIndex]; + deviceTasks.Add(Task.Run(async () => + { + if (device.GetDynamicProperty(Properties.Device.StaticTagCount) < tagLimit) + { + var loadedDevice = await m_kepwareApiClient.GenericConfig.LoadEntityAsync(device.Name, channel, query, cancellationToken: cancellationToken).ConfigureAwait(false); + if (loadedDevice != null) + { + project.Channels[channelIndex].Devices![deviceIndex] = loadedDevice; + } + else + { + // Failed to load device, log warning and end without incrementing completion. + m_logger.LogWarning("Failed to load {DeviceName} in channel {ChannelName}", device.Name, channel.Name); + return; + } + } + else + { + device.Tags = await m_kepwareApiClient.GenericConfig.LoadCollectionAsync(device, cancellationToken: cancellationToken).ConfigureAwait(false); + device.TagGroups = await m_kepwareApiClient.GenericConfig.LoadCollectionAsync(device, cancellationToken: cancellationToken).ConfigureAwait(false); + + if (device.TagGroups != null) + { + await LoadTagGroupsRecursiveAsync(m_kepwareApiClient, device.TagGroups, optimizedRecursion: true, tagLimit: tagLimit, cancellationToken: cancellationToken).ConfigureAwait(false); + } + } + })); + } + await Task.WhenAll(deviceTasks).ConfigureAwait(false); + } + } + + // Log information, loaded channel x of y + System.Threading.Interlocked.Increment(ref loadedChannelCount); + if (totalChannelCount == 1) + { + m_logger.LogInformation("Loaded channel {ChannelName}", channel.Name); + } + else + { + m_logger.LogInformation("Loaded channel {ChannelName} {LoadedChannelCount} of {TotalChannelCount}", channel.Name, loadedChannelCount, totalChannelCount); + } + })); + } + await Task.WhenAll(channelTasks).ConfigureAwait(false); + + // If loaded channel count doesn't match total channel count, log warning that some channels may have failed to load. + // Return empty project to avoid returning a partially loaded project which may cause issues for consumers of the API. + if (loadedChannelCount != totalChannelCount) + { + m_logger.LogWarning("Only loaded {LoadedChannelCount} of {TotalChannelCount} channels. Some channels may have fail to load.", loadedChannelCount, totalChannelCount); + project = new Project(); + } + } + + // Load IoT Gateway agents and their IoT Items + await LoadIotGatewayRecursiveAsync(project, cancellationToken).ConfigureAwait(false); + + return project; + } + + private async Task LoadProjectRecursiveAsync(Project project, CancellationToken cancellationToken = default) + { + project.Channels = await m_kepwareApiClient.GenericConfig.LoadCollectionAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + + if (project.Channels != null) + { + int totalChannelCount = project.Channels.Count; + int loadedChannelCount = 0; + await Task.WhenAll(project.Channels.Select(async channel => + { + channel.Devices = await m_kepwareApiClient.GenericConfig.LoadCollectionAsync(channel, cancellationToken: cancellationToken).ConfigureAwait(false); + + if (channel.Devices != null) + { + await Task.WhenAll(channel.Devices.Select(async device => + { + device.Tags = await m_kepwareApiClient.GenericConfig.LoadCollectionAsync(device, cancellationToken: cancellationToken).ConfigureAwait(false); + device.TagGroups = await m_kepwareApiClient.GenericConfig.LoadCollectionAsync(device, cancellationToken: cancellationToken).ConfigureAwait(false); + + if (device.TagGroups != null) + { + await LoadTagGroupsRecursiveAsync(m_kepwareApiClient, device.TagGroups, cancellationToken: cancellationToken).ConfigureAwait(false); + } })); } + // Log information, loaded channel x of y + loadedChannelCount++; + if (totalChannelCount == 1) + { + m_logger.LogInformation("Loaded channel {ChannelName}", channel.Name); + } + else + { + m_logger.LogInformation("Loaded channel {ChannelName} {LoadedChannelCount} of {TotalChannelCount}", channel.Name, loadedChannelCount, totalChannelCount); + } - m_logger.LogInformation("Loaded project in {ElapsedMilliseconds} ms", stopwatch.ElapsedMilliseconds); + })); + // If loaded channel count doesn't match total channel count, log warning that some channels may have failed to load. + // Return empty project to avoid returning a partially loaded project which may cause issues for consumers of the API. + if (loadedChannelCount != totalChannelCount) + { + m_logger.LogWarning("Only loaded {LoadedChannelCount} of {TotalChannelCount} channels. Some channels may have fail to load.", loadedChannelCount, totalChannelCount); + project = new Project(); } + } - return project; + // Load IoT Gateway agents and their IoT Items + await LoadIotGatewayRecursiveAsync(project, cancellationToken).ConfigureAwait(false); + + return project; + } + + private async Task LoadIotGatewayRecursiveAsync(Project project, CancellationToken cancellationToken) + { + MqttClientAgentCollection? mqttClients; + RestClientAgentCollection? restClients; + RestServerAgentCollection? restServers; + + try + { + mqttClients = await m_kepwareApiClient.GenericConfig.LoadCollectionAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + restClients = await m_kepwareApiClient.GenericConfig.LoadCollectionAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + restServers = await m_kepwareApiClient.GenericConfig.LoadCollectionAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + } + catch (InvalidOperationException ex) + { + // IoT Gateway plug-in may not be installed on the server + m_logger.LogDebug(ex, "IoT Gateway plug-in not available, skipping IoT Gateway loading"); + return; + } + + if ((mqttClients != null && mqttClients.Count > 0) || + (restClients != null && restClients.Count > 0) || + (restServers != null && restServers.Count > 0)) + { + project.IotGateway = new IotGatewayContainer + { + MqttClientAgents = mqttClients, + RestClientAgents = restClients, + RestServerAgents = restServers + }; + + // Load IoT Items for each agent + var agentTasks = new List(); + foreach (var agent in (mqttClients ?? []).Cast() + .Concat(restClients ?? []) + .Concat(restServers ?? [])) + { + agentTasks.Add(Task.Run(async () => + { + agent.IotItems = await m_kepwareApiClient.GenericConfig.LoadCollectionAsync(agent, cancellationToken: cancellationToken).ConfigureAwait(false); + })); + } + await Task.WhenAll(agentTasks).ConfigureAwait(false); } } #endregion #region recursive methods + + private async Task CompareAndApplyIotGatewayDetailedAsync( + IotGatewayContainer? source, IotGatewayContainer? current, + ProjectCompareAndApplyResult result, CancellationToken cancellationToken) + { + // Compare MQTT Client agents + var mqttCompare = await m_kepwareApiClient.GenericConfig.CompareAndApplyDetailedAsync( + source?.MqttClientAgents, current?.MqttClientAgents, cancellationToken: cancellationToken).ConfigureAwait(false); + result.Add(mqttCompare); + + foreach (var agent in mqttCompare.CompareResult.UnchangedItems.Concat(mqttCompare.CompareResult.ChangedItems)) + { + var itemCompare = await m_kepwareApiClient.GenericConfig.CompareAndApplyDetailedAsync( + agent.Left!.IotItems, agent.Right!.IotItems, agent.Right, cancellationToken).ConfigureAwait(false); + result.Add(itemCompare); + } + + // Compare REST Client agents + var restClientCompare = await m_kepwareApiClient.GenericConfig.CompareAndApplyDetailedAsync( + source?.RestClientAgents, current?.RestClientAgents, cancellationToken: cancellationToken).ConfigureAwait(false); + result.Add(restClientCompare); + + foreach (var agent in restClientCompare.CompareResult.UnchangedItems.Concat(restClientCompare.CompareResult.ChangedItems)) + { + var itemCompare = await m_kepwareApiClient.GenericConfig.CompareAndApplyDetailedAsync( + agent.Left!.IotItems, agent.Right!.IotItems, agent.Right, cancellationToken).ConfigureAwait(false); + result.Add(itemCompare); + } + + // Compare REST Server agents + var restServerCompare = await m_kepwareApiClient.GenericConfig.CompareAndApplyDetailedAsync( + source?.RestServerAgents, current?.RestServerAgents, cancellationToken: cancellationToken).ConfigureAwait(false); + result.Add(restServerCompare); + + foreach (var agent in restServerCompare.CompareResult.UnchangedItems.Concat(restServerCompare.CompareResult.ChangedItems)) + { + var itemCompare = await m_kepwareApiClient.GenericConfig.CompareAndApplyDetailedAsync( + agent.Left!.IotItems, agent.Right!.IotItems, agent.Right, cancellationToken).ConfigureAwait(false); + result.Add(itemCompare); + } + } + private static void SetOwnerRecursive(IEnumerable tagGroups, NamedEntity owner) { foreach (var tagGroup in tagGroups) @@ -372,28 +731,29 @@ private static void SetOwnerRecursive(IEnumerable tagGroups, Nam /// A task that represents the asynchronous operation. The task result contains a tuple with the counts of inserts, updates, and deletes. internal static async Task<(int inserts, int updates, int deletes)> RecusivlyCompareTagGroup(KepwareApiClient apiClient, DeviceTagGroupCollection left, DeviceTagGroupCollection? right, NamedEntity owner, CancellationToken cancellationToken) { - (int inserts, int updates, int deletes) ret = (0, 0, 0); + var result = await RecusivlyCompareTagGroupDetailed(apiClient, left, right, owner, cancellationToken).ConfigureAwait(false); + return (result.Inserts, result.Updates, result.Deletes); + } - var tagGroupCompare = await apiClient.GenericConfig.CompareAndApply(left, right, owner, cancellationToken: cancellationToken).ConfigureAwait(false); + private Task RecusivlyCompareTagGroupDetailed(DeviceTagGroupCollection left, DeviceTagGroupCollection? right, NamedEntity owner, CancellationToken cancellationToken) + => RecusivlyCompareTagGroupDetailed(m_kepwareApiClient, left, right, owner, cancellationToken); - ret.inserts = tagGroupCompare.ItemsOnlyInLeft.Count; - ret.updates = tagGroupCompare.ChangedItems.Count; - ret.deletes = tagGroupCompare.ItemsOnlyInRight.Count; + internal static async Task RecusivlyCompareTagGroupDetailed(KepwareApiClient apiClient, DeviceTagGroupCollection left, DeviceTagGroupCollection? right, NamedEntity owner, CancellationToken cancellationToken) + { + var ret = new ProjectCompareAndApplyResult(); - foreach (var tagGroup in tagGroupCompare.UnchangedItems.Concat(tagGroupCompare.ChangedItems)) - { - var tagGroupTagCompare = await apiClient.GenericConfig.CompareAndApply(tagGroup.Left!.Tags, tagGroup.Right!.Tags, tagGroup.Right, cancellationToken: cancellationToken).ConfigureAwait(false); + var tagGroupCompare = await apiClient.GenericConfig.CompareAndApplyDetailedAsync(left, right, owner, cancellationToken: cancellationToken).ConfigureAwait(false); + ret.Add(tagGroupCompare); - ret.inserts = tagGroupTagCompare.ItemsOnlyInLeft.Count; - ret.updates = tagGroupTagCompare.ChangedItems.Count; - ret.deletes = tagGroupTagCompare.ItemsOnlyInRight.Count; + foreach (var tagGroup in tagGroupCompare.CompareResult.UnchangedItems.Concat(tagGroupCompare.CompareResult.ChangedItems)) + { + var tagGroupTagCompare = await apiClient.GenericConfig.CompareAndApplyDetailedAsync(tagGroup.Left!.Tags, tagGroup.Right!.Tags, tagGroup.Right, cancellationToken: cancellationToken).ConfigureAwait(false); + ret.Add(tagGroupTagCompare); if (tagGroup.Left!.TagGroups != null) { - var result = await RecusivlyCompareTagGroup(apiClient, tagGroup.Left!.TagGroups, tagGroup.Right!.TagGroups, tagGroup.Right, cancellationToken: cancellationToken).ConfigureAwait(false); - ret.updates += result.updates; - ret.deletes += result.deletes; - ret.inserts += result.inserts; + var result = await RecusivlyCompareTagGroupDetailed(apiClient, tagGroup.Left!.TagGroups, tagGroup.Right!.TagGroups, tagGroup.Right, cancellationToken: cancellationToken).ConfigureAwait(false); + ret.Add(result); } } @@ -405,20 +765,48 @@ private static void SetOwnerRecursive(IEnumerable tagGroups, Nam /// /// The API client. /// The tag groups to load. + /// A flag indicating whether to use optimized recursion with content=serialize or basic recursion. + /// This is only applicable for projects loaded with the full project load option and when the JsonProjectLoad service is + /// supported by the server. + /// Tag Limit if overridden by method call. /// The cancellation token. /// A task that represents the asynchronous operation. - internal static async Task LoadTagGroupsRecursiveAsync(KepwareApiClient apiClient, IEnumerable tagGroups, CancellationToken cancellationToken = default) + internal static async Task LoadTagGroupsRecursiveAsync(KepwareApiClient apiClient, IEnumerable tagGroups, bool optimizedRecursion = false, int tagLimit = 0, CancellationToken cancellationToken = default) { + // Falls back to the value set by if tagLimit parameter is not set or an invalid value is provided. + if (tagLimit <= 0) + tagLimit = apiClient.ClientOptions.ProjectLoadTagLimit; foreach (var tagGroup in tagGroups) { // Load the Tag Groups and Tags of the current Tag Group - tagGroup.TagGroups = await apiClient.GenericConfig.LoadCollectionAsync(tagGroup, cancellationToken).ConfigureAwait(false); - tagGroup.Tags = await apiClient.GenericConfig.LoadCollectionAsync(tagGroup, cancellationToken).ConfigureAwait(false); - - // Recursively load the Tag Groups and Tags of the child Tag Groups - if (tagGroup.TagGroups != null && tagGroup.TagGroups.Count > 0) + if (optimizedRecursion && tagGroup.TotalTagCount < tagLimit) + { + var query = new[] + { + new KeyValuePair("content", "serialize") + }; + var loadedTagGroup = await apiClient.GenericConfig.LoadEntityAsync(tagGroup.Name, tagGroup.Owner!, query, cancellationToken: cancellationToken).ConfigureAwait(false); + if (loadedTagGroup != null) + { + tagGroup.Tags = loadedTagGroup.Tags; + tagGroup.TagGroups = loadedTagGroup.TagGroups; + } + else + { + // Failed to load tag group, log warning and end without loading child tag groups. + apiClient.Logger.LogWarning("Failed to load {TagGroupName} in {OwnerName}", tagGroup.Name, tagGroup.Owner?.Name); + continue; + } + } + else { - await LoadTagGroupsRecursiveAsync(apiClient, tagGroup.TagGroups, cancellationToken).ConfigureAwait(false); + tagGroup.TagGroups = await apiClient.GenericConfig.LoadCollectionAsync(tagGroup, cancellationToken: cancellationToken).ConfigureAwait(false); + tagGroup.Tags = await apiClient.GenericConfig.LoadCollectionAsync(tagGroup, cancellationToken: cancellationToken).ConfigureAwait(false); + // Recursively load the Tag Groups and Tags of the child Tag Groups + if (tagGroup.TagGroups != null && tagGroup.TagGroups.Count > 0) + { + await LoadTagGroupsRecursiveAsync(apiClient, tagGroup.TagGroups, optimizedRecursion, tagLimit, cancellationToken).ConfigureAwait(false); + } } } } diff --git a/Kepware.Api/KepwareApiClient.cs b/Kepware.Api/KepwareApiClient.cs index 0887994..5a87ca2 100644 --- a/Kepware.Api/KepwareApiClient.cs +++ b/Kepware.Api/KepwareApiClient.cs @@ -30,6 +30,7 @@ public partial class KepwareApiClient : IKepwareDefaultValueProvider /// The value for an unknown client or hostname. /// public const string UNKNOWN = "Unknown"; + private const string ENDPOINT_STATUS = "/config/v1/status"; private const string ENDPOINT_DOC = "/config/v1/doc"; private const string ENDPOINT_ABOUT = "/config/v1/about"; @@ -40,6 +41,15 @@ public partial class KepwareApiClient : IKepwareDefaultValueProvider private bool? m_isConnected = null; private bool? m_hasValidCredentials = null; + private ProductInfo? m_productInfo = null; + + /// + /// Gets the logger instance used for logging operations. + /// + /// This property provides access to the logger, which can be used to log messages at + /// various levels. Ensure that the logger is properly initialized before use. + public ILogger Logger => m_logger; + /// /// Gets the name of the client instance. @@ -51,6 +61,14 @@ public partial class KepwareApiClient : IKepwareDefaultValueProvider /// public string ClientHostName => m_httpClient.BaseAddress?.Host ?? UNKNOWN; + /// + /// Gets the product information of the connected Kepware server, which includes + /// product name and version information. This caches the value during + /// and and cached for future use. + /// It will return null if there is no cached value. + /// + public ProductInfo? ProductInfo => m_productInfo; + /// /// Gets the client options for the Kepware server connection. /// @@ -110,7 +128,8 @@ internal KepwareApiClient(string name, KepwareApiClientOptions options, ILoggerF var channelsApiHandler = new ChannelApiHandler(this, loggerFactory.CreateLogger()); var devicesApiHandler = new DeviceApiHandler(this, loggerFactory.CreateLogger()); - Project = new ProjectApiHandler(this, channelsApiHandler, devicesApiHandler, loggerFactory.CreateLogger()); + var iotGatewayApiHandler = new IotGatewayApiHandler(this, loggerFactory.CreateLogger()); + Project = new ProjectApiHandler(this, channelsApiHandler, devicesApiHandler, iotGatewayApiHandler, loggerFactory.CreateLogger()); Admin = new AdminApiHandler(this, loggerFactory.CreateLogger()); ApiServices = new ServicesApiHandler(this, loggerFactory.CreateLogger()); } @@ -141,7 +160,7 @@ public async Task TestConnectionAsync(CancellationToken cancellationToken if (!response.IsSuccessStatusCode) { m_logger.LogWarning("Failed to connect to {ClientName}-client at {BaseAddress}, Reason: {ReasonPhrase}", ClientName, m_httpClient.BaseAddress, response.ReasonPhrase); - m_isConnected = null; // set connection state to null if we cannot connect + ClearConnectionState(); // set connection state to null if we cannot connect return false; // connection failed } @@ -155,7 +174,7 @@ await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false if (status?.FirstOrDefault()?.Healthy == false) { m_logger.LogWarning("Failed to connect to {ClientName}-client at {BaseAddress}, Reason: {String}", ClientName, m_httpClient.BaseAddress, "Server Status Check Failed"); - m_isConnected = null; // set connection state to null if we cannot connect + ClearConnectionState(); // set connection state to null if we cannot connect return false; // connection failed } @@ -167,12 +186,12 @@ await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false // Inital connection attempt or a reconnection due to failure, // we need to check the product info and credentials - var prodInfo = await GetProductInfoAsync(cancellationToken).ConfigureAwait(false); + _ = await GetProductInfoAsync(cancellationToken).ConfigureAwait(false); // If we cannot get the product info, we assume the connection is not healthy - if (prodInfo == null) + if (m_productInfo == null) { - m_isConnected = null; // set connection state to null if we cannot get product info + ClearConnectionState(); // set connection state to null if we cannot get product info return false; } @@ -182,12 +201,12 @@ await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false // If we do not have valid credentials, we assume the connection is not healthy if (m_hasValidCredentials != true) { - m_isConnected = null; // set connection state to null if we cannot connect or credentials are invalid + ClearConnectionState(); // set connection state to null if we cannot connect or credentials are invalid m_logger.LogWarning("Connection to {ClientName}-client at {BaseAddress} failed because credentials are invalid", ClientName, m_httpClient.BaseAddress); return false; } - m_logger.LogInformation("Successfully connected to {ClientName}-client: {ProductName} {ProductVersion} on {BaseAddress}", ClientName, prodInfo?.ProductName, prodInfo?.ProductVersion, m_httpClient.BaseAddress); + m_logger.LogInformation("Successfully connected to {ClientName}-client: {ProductName} {ProductVersion} on {BaseAddress}", ClientName, m_productInfo?.ProductName, m_productInfo?.ProductVersion, m_httpClient.BaseAddress); m_isConnected = true; // set connection state to true if we have a valid product info and credentials return m_isConnected.Value; // return true if we have a valid connection @@ -205,21 +224,29 @@ await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false /// /// Gets the product information from the Kepware server which includes product name and version information. + /// Will update the client's product info property, which can be used in other calls to avoid calling the API multiple times for the same information. /// Uses the /config/v1/about endpoint /// /// The cancellation token. - /// A task that represents the asynchronous operation. The task result contains the product information. + /// A task that represents the asynchronous operation. The task result contains the product information. public async Task GetProductInfoAsync(CancellationToken cancellationToken = default) { + if (m_productInfo != null) + { + // return cached product info if we have it + return m_productInfo; + } + try { var response = await m_httpClient.GetAsync(ENDPOINT_ABOUT, cancellationToken).ConfigureAwait(false); if (response.IsSuccessStatusCode) { var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); - var prodInfo = JsonSerializer.Deserialize(content, KepJsonContext.Default.ProductInfo); - - return prodInfo; + + // Set Product Info for the client if we have a valid response, so we can use it in other calls without needing to call the API again + m_productInfo = JsonSerializer.Deserialize(content, KepJsonContext.Default.ProductInfo); + return m_productInfo; } else { @@ -235,6 +262,8 @@ await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false m_logger.LogWarning(jsonEx, "Failed to parse ProductInfo from {BaseAddress}", m_httpClient.BaseAddress); } + // If we cannot get the product info, we set it to null and return null + m_productInfo = null; return null; } @@ -293,14 +322,41 @@ async Task> IKepwareDefaultValueProvider } #endregion - #region internal + #region Private / internal helper methods + /// + /// Clears all client-level connection state and optionally handler caches. + /// Call this whenever the connection should be considered lost or stale. + /// + /// If true also clears cached credential validation state. + private void ClearConnectionState(bool clearCredentials = true) + { + // Clear derived product info and connection flags + m_productInfo = null; + m_isConnected = null; + + // Optionally clear credential status so next TestConnection re-evaluates + if (clearCredentials) + m_hasValidCredentials = null; + + // Invalidate caches on handlers that keep them + try + { + // GenericConfig may implement an InvalidateCaches method (see suggestion below) + (GenericConfig as ClientHandler.GenericApiHandler)?.InvalidateCaches(); + } + catch + { + // swallow - defensive: don't throw from a state-clear helper + } + } + /// /// Invoked by Handler, when they receice a http request exception /// /// internal void OnHttpRequestException(HttpRequestException httpEx) { - m_isConnected = null; + ClearConnectionState(false); } #endregion } diff --git a/Kepware.Api/KepwareApiClientOptions.cs b/Kepware.Api/KepwareApiClientOptions.cs index 7cd951d..26c7622 100644 --- a/Kepware.Api/KepwareApiClientOptions.cs +++ b/Kepware.Api/KepwareApiClientOptions.cs @@ -60,5 +60,16 @@ public class KepwareApiClientOptions /// Gets or sets an optional tag object for additional configuration metadata. /// public object? Tag { get; init; } + + /// + /// Gets the maximum number of tags that can be loaded in a single action when using ProjectLoad methods. + /// This limit is crucial for managing performance and resource utilization when working with large projects, + /// as it helps to prevent excessive memory usage and potential timeouts that may occur when attempting to + /// load an excessively large number of tags at once. + /// + /// This property is initialized to a default value of 100,000. It is important to + /// consider this limit when working with projects that may contain a large number of tags, as exceeding this + /// limit may result in performance degradation or incomplete data loading. + public int ProjectLoadTagLimit { get; init; } = 100000; } } diff --git a/Kepware.Api/Model/ApiMessages.cs b/Kepware.Api/Model/ApiMessages.cs index cb118a7..89beb63 100644 --- a/Kepware.Api/Model/ApiMessages.cs +++ b/Kepware.Api/Model/ApiMessages.cs @@ -15,6 +15,24 @@ namespace Kepware.Api.Model /// public class ApiResult { + /// + /// Gets or sets the property name associated with a validation failure. + /// + [JsonPropertyName("property")] + public string? Property { get; set; } + + /// + /// Gets or sets the description associated with a validation failure. + /// + [JsonPropertyName("description")] + public string? Description { get; set; } + + /// + /// Gets or sets the error line associated with a validation failure. + /// + [JsonPropertyName("error_line")] + public int? ErrorLine { get; set; } + /// /// Gets or sets the status code of the API result. /// @@ -40,6 +58,18 @@ public class ApiResult public bool IsSuccessStatusCode => Code >= 200 && Code < 300; } + /// + /// Represents the response from an update endpoint that may include partially applied properties. + /// + public class UpdateApiResponseMessage : ApiResponseMessage + { + /// + /// Gets or sets the properties that were not applied. + /// + [JsonPropertyName("not_applied")] + public Dictionary? NotApplied { get; set; } + } + /// /// Represents the status of the Configuration API REST service. /// diff --git a/Kepware.Api/Model/ApplyResults.cs b/Kepware.Api/Model/ApplyResults.cs new file mode 100644 index 0000000..d817afb --- /dev/null +++ b/Kepware.Api/Model/ApplyResults.cs @@ -0,0 +1,207 @@ +using Kepware.Api.Util; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace Kepware.Api.Model +{ + /// + /// Represents the type of operation applied during compare-and-apply. + /// + public enum ApplyOperation + { + /// + /// Insert operation. + /// + Insert = 0, + + /// + /// Update operation. + /// + Update = 1, + + /// + /// Delete operation. + /// + Delete = 2, + } + + /// + /// Represents one failed apply operation and the attempted item. + /// + public sealed class ApplyFailure + { + /// + /// Gets the operation that failed. + /// + public required ApplyOperation Operation { get; init; } + + /// + /// Gets the original item used for the failed operation. + /// + public required BaseEntity AttemptedItem { get; init; } + + /// + /// Gets the response code associated with the failed operation. + /// + public int? ResponseCode { get; init; } + + /// + /// Gets the response message associated with the failed operation. + /// + public string? ResponseMessage { get; init; } + + /// + /// Gets the property name associated with an insert validation failure. + /// + public string? Property { get; init; } + + /// + /// Gets the detailed description associated with an insert validation failure. + /// + public string? Description { get; init; } + + /// + /// Gets the error line associated with an insert validation failure. + /// + public int? ErrorLine { get; init; } + + /// + /// Gets the list of update properties that were not applied. + /// + public IReadOnlyList? NotAppliedProperties { get; init; } + } + + /// + /// Represents detailed compare-and-apply results for a collection. + /// + /// The entity type. + public sealed class CollectionApplyResult + where K : NamedEntity, new() + { + private readonly List m_failures = []; + + /// + /// Initializes a new instance of the class. + /// + /// The raw compare result bucket. + public CollectionApplyResult(EntityCompare.CollectionResultBucket compareResult) + { + CompareResult = compareResult; + } + + /// + /// Gets the underlying compare result bucket. + /// + public EntityCompare.CollectionResultBucket CompareResult { get; } + + /// + /// Gets the number of successfully inserted items. + /// + public int Inserts { get; private set; } + + /// + /// Gets the number of successfully updated items. + /// + public int Updates { get; private set; } + + /// + /// Gets the number of successfully deleted items. + /// + public int Deletes { get; private set; } + + /// + /// Gets the number of failed operations. + /// + public int Failures => m_failures.Count; + + /// + /// Gets the failed operation details. + /// + public ReadOnlyCollection FailureList => m_failures.AsReadOnly(); + + internal void AddInsertSuccess() => Inserts += 1; + + internal void AddUpdateSuccess() => Updates += 1; + + internal void AddDeleteSuccess() => Deletes += 1; + + internal void AddFailure(ApplyFailure failure) => m_failures.Add(failure); + } + + /// + /// Represents detailed compare-and-apply results for a full project operation. + /// + public sealed class ProjectCompareAndApplyResult + { + private readonly List m_failures = []; + + /// + /// Gets the number of successfully inserted items. + /// + public int Inserts { get; private set; } + + /// + /// Gets the number of successfully updated items. + /// + public int Updates { get; private set; } + + /// + /// Gets the number of successfully deleted items. + /// + public int Deletes { get; private set; } + + /// + /// Gets the number of failed operations. + /// + public int Failures => m_failures.Count; + + /// + /// Gets the failed operation details. + /// + public ReadOnlyCollection FailureList => m_failures.AsReadOnly(); + + internal void Add(CollectionApplyResult result) + => Add(result.Inserts, result.Updates, result.Deletes, result.FailureList); + + internal void Add(CollectionApplyResult result) + => Add(result.Inserts, result.Updates, result.Deletes, result.FailureList); + + internal void Add(CollectionApplyResult result) + => Add(result.Inserts, result.Updates, result.Deletes, result.FailureList); + + internal void Add(CollectionApplyResult result) + => Add(result.Inserts, result.Updates, result.Deletes, result.FailureList); + + internal void Add(CollectionApplyResult result) + => Add(result.Inserts, result.Updates, result.Deletes, result.FailureList); + + internal void Add(CollectionApplyResult result) + => Add(result.Inserts, result.Updates, result.Deletes, result.FailureList); + + internal void Add(CollectionApplyResult result) + => Add(result.Inserts, result.Updates, result.Deletes, result.FailureList); + + internal void Add(CollectionApplyResult result) + => Add(result.Inserts, result.Updates, result.Deletes, result.FailureList); + + internal void Add(ProjectCompareAndApplyResult result) + => Add(result.Inserts, result.Updates, result.Deletes, result.FailureList); + + internal void AddInsertSuccess() => Inserts += 1; + + internal void AddUpdateSuccess() => Updates += 1; + + internal void AddDeleteSuccess() => Deletes += 1; + + internal void AddFailure(ApplyFailure failure) => m_failures.Add(failure); + + private void Add(int inserts, int updates, int deletes, IReadOnlyList failures) + { + Inserts += inserts; + Updates += updates; + Deletes += deletes; + m_failures.AddRange(failures); + } + } +} diff --git a/Kepware.Api/Model/EntityFactory.cs b/Kepware.Api/Model/EntityFactory.cs index 39b1e4b..0606186 100644 --- a/Kepware.Api/Model/EntityFactory.cs +++ b/Kepware.Api/Model/EntityFactory.cs @@ -19,6 +19,10 @@ static EntityFactory() Factories[typeof(Tag)] = () => new Tag(); Factories[typeof(DefaultEntity)] = () => new DefaultEntity(); Factories[typeof(NamedEntity)] = () => new NamedEntity(); + Factories[typeof(MqttClientAgent)] = () => new MqttClientAgent(); + Factories[typeof(RestClientAgent)] = () => new RestClientAgent(); + Factories[typeof(RestServerAgent)] = () => new RestServerAgent(); + Factories[typeof(IotItem)] = () => new IotItem(); } public static BaseEntity CreateInstance(Type type) diff --git a/Kepware.Api/Model/Project/Channel.Properties.cs b/Kepware.Api/Model/Project/Channel.Properties.cs index a9a0208..6f1a805 100644 --- a/Kepware.Api/Model/Project/Channel.Properties.cs +++ b/Kepware.Api/Model/Project/Channel.Properties.cs @@ -45,6 +45,11 @@ public static class Channel /// public const string DiagnosticsCapture = "servermain.CHANNEL_DIAGNOSTICS_CAPTURE"; + /// + /// Value of the static tag count for the channel. + /// + public const string StaticTagCount = Properties.NonSerialized.ChannelStaticTagCount; + } } } \ No newline at end of file diff --git a/Kepware.Api/Model/Project/Device.Properties.cs b/Kepware.Api/Model/Project/Device.Properties.cs new file mode 100644 index 0000000..e4de536 --- /dev/null +++ b/Kepware.Api/Model/Project/Device.Properties.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Kepware.Api.Model +{ + public partial class Properties + { + public static class Device + { + /// + /// The driver used by this device. + /// + public const string DeviceDriver = "servermain.MULTIPLE_TYPES_DEVICE_DRIVER"; + + /// + /// Constant value for the key of the static tag count for the device. + /// + public const string StaticTagCount = Properties.NonSerialized.DeviceStaticTagCount; + + } + } +} diff --git a/Kepware.Api/Model/Project/Device.cs b/Kepware.Api/Model/Project/Device.cs index e0015aa..9dc3944 100644 --- a/Kepware.Api/Model/Project/Device.cs +++ b/Kepware.Api/Model/Project/Device.cs @@ -70,6 +70,12 @@ public Device(string name, string channelName) [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public DeviceTagGroupCollection? TagGroups { get; set; } + /// + /// Gets the driver used by this device. + /// + [YamlIgnore, JsonIgnore] + public string? DeviceDriver => GetDynamicProperty(Properties.Device.DeviceDriver); + /// /// Gets the unique ID key for the device. /// diff --git a/Kepware.Api/Model/Project/DeviceTagGroup.Properties.cs b/Kepware.Api/Model/Project/DeviceTagGroup.Properties.cs new file mode 100644 index 0000000..0181cb9 --- /dev/null +++ b/Kepware.Api/Model/Project/DeviceTagGroup.Properties.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Kepware.Api.Model +{ + public partial class Properties + { + public static class DeviceTagGroup + { + + /// + /// Represents the constant name used to identify the count of tags in the local tag group. + /// + public const string LocalTagCount = Properties.NonSerialized.TagGrpTagCount; + + /// + /// Represents the constant name used to identify the total count of tags in the tag group, including all child tag groups. + /// + public const string TotalTagCount = Properties.NonSerialized.TagGrpTotalTagCount; + } + } +} diff --git a/Kepware.Api/Model/Project/DeviceTagGroup.cs b/Kepware.Api/Model/Project/DeviceTagGroup.cs index a062935..e51eadc 100644 --- a/Kepware.Api/Model/Project/DeviceTagGroup.cs +++ b/Kepware.Api/Model/Project/DeviceTagGroup.cs @@ -48,12 +48,32 @@ public DeviceTagGroup(string name, DeviceTagGroup owner) public DeviceTagGroupCollection? TagGroups { get; set; } /// - /// Get a flag indicating if the tag group is autogenerated + /// Get a flag indicating if the tag group was autogenerated /// [YamlIgnore] [JsonIgnore] public bool IsAutogenerated => GetDynamicProperty(Properties.NonSerialized.TagGroupAutogenerated); + /// + /// Gets the number of tags that are defined locally within this tag group. + /// + /// Use this property to determine how many tags are associated with the current + /// instance, excluding tags in children tag groups. NOTE: Provides the count from the Kepware instance + /// and only updates when loading the tag group configuration. + [YamlIgnore] + [JsonIgnore] + public int LocalTagCount => GetDynamicProperty(Properties.DeviceTagGroup.LocalTagCount); + + /// + /// Gets the total number of tags included in the tag group and all children tag groups. + /// + /// Use this property to determine how many tags are associated with the current + /// instance, including tags in children tag groups. NOTE: Provides the count from the Kepware instance + /// and only updates when loading the tag group configuration. + [YamlIgnore] + [JsonIgnore] + public int TotalTagCount => GetDynamicProperty(Properties.DeviceTagGroup.TotalTagCount); + /// /// Recursively cleans up the tag group and all its children /// diff --git a/Kepware.Api/Model/Project/IotGateway/IotAgent.cs b/Kepware.Api/Model/Project/IotGateway/IotAgent.cs new file mode 100644 index 0000000..5ce3794 --- /dev/null +++ b/Kepware.Api/Model/Project/IotGateway/IotAgent.cs @@ -0,0 +1,116 @@ +using System.Text.Json.Serialization; +using YamlDotNet.Serialization; + +namespace Kepware.Api.Model +{ + /// + /// Base class for all IoT Gateway agent types. Contains the universal properties + /// shared by MQTT Client, REST Client, and REST Server agents. + /// + public class IotAgent : NamedEntity + { + /// + /// Initializes a new instance of the class. + /// + public IotAgent() + { + } + + /// + /// Initializes a new instance of the class with the specified name. + /// + /// The name of the agent. + public IotAgent(string name) : base(name) + { + } + + #region Properties + + /// + /// Gets or sets the IoT Items in this agent. + /// + [YamlIgnore] + [JsonPropertyName("iot_items")] + [JsonPropertyOrder(100)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IotItemCollection? IotItems { get; set; } + + /// + /// Gets the agent type identifier (read-only). + /// + [YamlIgnore, JsonIgnore] + public string? AgentType + { + get => GetDynamicProperty(Properties.IotAgent.AgentType); + } + + /// + /// Gets or sets whether the agent is enabled. + /// + [YamlIgnore, JsonIgnore] + public bool? Enabled + { + get => GetDynamicProperty(Properties.IotAgent.Enabled); + set => SetDynamicProperty(Properties.IotAgent.Enabled, value); + } + + /// + /// Gets or sets whether quality changes should be ignored. + /// + [YamlIgnore, JsonIgnore] + public bool? IgnoreQualityChanges + { + get => GetDynamicProperty(Properties.IotAgent.IgnoreQualityChanges); + set => SetDynamicProperty(Properties.IotAgent.IgnoreQualityChanges, value); + } + + /// + /// Gets the total number of tags configured under this agent (read-only). + /// + [YamlIgnore, JsonIgnore] + public int? ThisAgentTotal + { + get => GetDynamicProperty(Properties.IotAgent.ThisAgentTotal); + } + + /// + /// Gets the total number of tags configured under all agents (read-only). + /// + [YamlIgnore, JsonIgnore] + public int? AllAgentsTotal + { + get => GetDynamicProperty(Properties.IotAgent.AllAgentsTotal); + } + + /// + /// Gets the license limit for configured tags (read-only). + /// + [YamlIgnore, JsonIgnore] + public string? LicenseLimit + { + get => GetDynamicProperty(Properties.IotAgent.LicenseLimit); + } + + #endregion + + /// + /// Recursively cleans up the agent and all IoT Items. + /// + /// The default value provider. + /// Whether to remove the project ID. + /// A token to cancel the operation. + /// A task that represents the asynchronous cleanup operation. + public override async Task Cleanup(IKepwareDefaultValueProvider defaultValueProvider, bool blnRemoveProjectId = false, CancellationToken cancellationToken = default) + { + await base.Cleanup(defaultValueProvider, blnRemoveProjectId, cancellationToken).ConfigureAwait(false); + + if (IotItems != null) + { + foreach (var item in IotItems) + { + await item.Cleanup(defaultValueProvider, blnRemoveProjectId, cancellationToken).ConfigureAwait(false); + } + } + } + } +} diff --git a/Kepware.Api/Model/Project/IotGateway/IotGateway.Properties.cs b/Kepware.Api/Model/Project/IotGateway/IotGateway.Properties.cs new file mode 100644 index 0000000..9933d20 --- /dev/null +++ b/Kepware.Api/Model/Project/IotGateway/IotGateway.Properties.cs @@ -0,0 +1,280 @@ +namespace Kepware.Api.Model +{ + public partial class Properties + { + /// + /// Property key constants shared by all IoT Gateway agent types. + /// + public static class IotAgent + { + /// + /// The agent type identifier (read-only). + /// + public const string AgentType = "iot_gateway.AGENTTYPES_TYPE"; + + /// + /// Specifies if the agent is enabled. + /// + public const string Enabled = "iot_gateway.AGENTTYPES_ENABLED"; + + /// + /// Specifies if changes in quality should be ignored. + /// + public const string IgnoreQualityChanges = "iot_gateway.IGNORE_QUALITY_CHANGES"; + + /// + /// Specifies the publish type (Interval or On Data Change). + /// + public const string PublishType = "iot_gateway.AGENTTYPES_PUBLISH_TYPE"; + + /// + /// Specifies the publish rate in milliseconds. + /// + public const string RateMs = "iot_gateway.AGENTTYPES_RATE_MS"; + + /// + /// Specifies the publish format (Wide or Narrow). + /// + public const string PublishFormat = "iot_gateway.AGENTTYPES_PUBLISH_FORMAT"; + + /// + /// Maximum number of tag events per publish in narrow format. + /// + public const string MaxEventsPerPublish = "iot_gateway.AGENTTYPES_MAX_EVENTS"; + + /// + /// Transaction timeout in seconds. + /// + public const string TransactionTimeoutS = "iot_gateway.AGENTTYPES_TIMEOUT_S"; + + /// + /// Specifies the message format (Standard or Advanced Template). + /// + public const string MessageFormat = "iot_gateway.AGENTTYPES_MESSAGE_FORMAT"; + + /// + /// Standard template for message formatting. + /// + public const string StandardTemplate = "iot_gateway.AGENTTYPES_STANDARD_TEMPLATE"; + + /// + /// Expansion of |VALUES| in standard template. + /// + public const string ExpansionOfValues = "iot_gateway.AGENTTYPES_EXPANSION_OF_VALUES"; + + /// + /// Advanced template for message formatting. + /// + public const string AdvancedTemplate = "iot_gateway.AGENTTYPES_ADVANCED_TEMPLATE"; + + /// + /// Specifies if the initial update should be sent when the agent starts. + /// + public const string SendInitialUpdate = "iot_gateway.AGENTTYPES_SEND_INITIAL_UPDATE"; + + /// + /// Total number of tags configured under this agent (read-only). + /// + public const string ThisAgentTotal = Properties.NonSerialized.ThisAgentTotal; + + /// + /// Total number of tags configured under all agents (read-only). + /// + public const string AllAgentsTotal = Properties.NonSerialized.AllAgentsTotal; + + /// + /// Maximum number of configured tags allowed by the license (read-only). + /// + public const string LicenseLimit = Properties.NonSerialized.LicenseLimit; + } + + /// + /// Property key constants specific to MQTT Client agents. + /// + public static class MqttClientAgent + { + /// + /// URL of the MQTT broker endpoint. + /// + public const string Url = "iot_gateway.MQTT_CLIENT_URL"; + + /// + /// Topic name for publishing data on the broker. + /// + public const string Topic = "iot_gateway.MQTT_CLIENT_TOPIC"; + + /// + /// MQTT Quality of Service level. + /// + public const string Qos = "iot_gateway.MQTT_CLIENT_QOS"; + + /// + /// Unique client identity for broker communication. + /// + public const string ClientId = "iot_gateway.MQTT_CLIENT_CLIENT_ID"; + + /// + /// Username for broker authentication. + /// + public const string Username = "iot_gateway.MQTT_CLIENT_USERNAME"; + + /// + /// Password for broker authentication. + /// + public const string Password = "iot_gateway.MQTT_CLIENT_PASSWORD"; + + /// + /// TLS version for secure connections. + /// + public const string TlsVersion = "iot_gateway.MQTT_TLS_VERSION"; + + /// + /// Enable client certificate for application-based authentication. + /// + public const string ClientCertificate = "iot_gateway.MQTT_CLIENT_CERTIFICATE"; + + /// + /// Enable Last Will and Testament. + /// + public const string EnableLastWill = "iot_gateway.MQTT_CLIENT_ENABLE_LAST_WILL"; + + /// + /// Topic for Last Will and Testament message. + /// + public const string LastWillTopic = "iot_gateway.MQTT_CLIENT_LAST_WILL_TOPIC"; + + /// + /// Last Will and Testament message text. + /// + public const string LastWillMessage = "iot_gateway.MQTT_CLIENT_LAST_WILL_MESSAGE"; + + /// + /// Enable listening for write requests. + /// + public const string EnableWriteTopic = "iot_gateway.MQTT_CLIENT_ENABLE_WRITE_TOPIC"; + + /// + /// Topic for write request subscriptions. + /// + public const string WriteTopic = "iot_gateway.MQTT_CLIENT_WRITE_TOPIC"; + } + + /// + /// Property key constants specific to REST Client agents. + /// + public static class RestClientAgent + { + /// + /// URL of the REST endpoint. + /// + public const string Url = "iot_gateway.REST_CLIENT_URL"; + + /// + /// HTTP method for publishing data (POST or PUT). + /// + public const string HttpMethod = "iot_gateway.REST_CLIENT_METHOD"; + + /// + /// HTTP header name-value pairs sent with each connection. + /// + public const string HttpHeader = "iot_gateway.REST_CLIENT_HTTP_HEADER"; + + /// + /// Content-type header for published data. + /// + public const string PublishMediaType = "iot_gateway.REST_CLIENT_PUBLISH_MEDIA_TYPE"; + + /// + /// Username for basic HTTP authentication. + /// + public const string Username = "iot_gateway.REST_CLIENT_USERNAME"; + + /// + /// Password for basic HTTP authentication. + /// + public const string Password = "iot_gateway.REST_CLIENT_PASSWORD"; + + /// + /// Buffer updates when a publish fails. + /// + public const string BufferOnFailedPublish = "iot_gateway.BUFFER_ON_FAILED_PUBLISH"; + } + + /// + /// Property key constants specific to REST Server agents. + /// + public static class RestServerAgent + { + /// + /// Network adapter for the REST server endpoint. + /// + public const string NetworkAdapter = "iot_gateway.REST_SERVER_NETWORK_ADAPTER"; + + /// + /// Port number for the REST server. + /// + public const string PortNumber = "iot_gateway.REST_SERVER_PORT_NUMBER"; + + /// + /// CORS allowed origins (comma-delimited). + /// + public const string CorsAllowedOrigins = "iot_gateway.REST_SERVER_CORS_ALLOWED_ORIGINS"; + + /// + /// Enable HTTPS encryption. + /// + public const string UseHttps = "iot_gateway.REST_SERVER_USE_HTTPS"; + + /// + /// Enable write endpoint for tag writes. + /// + public const string EnableWriteEndpoint = "iot_gateway.REST_SERVER_ENABLE_WRITE_ENDPOINT"; + + /// + /// Allow unauthenticated access. + /// + public const string AllowAnonymousLogin = "iot_gateway.REST_SERVER_ALLOW_ANONYMOUS_LOGIN"; + } + + /// + /// Property key constants for IoT Items. + /// + public static class IotItem + { + /// + /// Full channel.device.name of the referenced server tag. + /// + public const string ServerTag = "iot_gateway.IOT_ITEM_SERVER_TAG"; + + /// + /// Use scan rate to collect data from the device. + /// + public const string UseScanRate = "iot_gateway.IOT_ITEM_USE_SCAN_RATE"; + + /// + /// Scan rate in milliseconds. + /// + public const string ScanRateMs = "iot_gateway.IOT_ITEM_SCAN_RATE_MS"; + + /// + /// Force publish on every scan regardless of value change. + /// + public const string PublishEveryScan = "iot_gateway.IOT_ITEM_SEND_EVERY_SCAN"; + + /// + /// Deadband percentage for publish threshold. + /// + public const string DeadbandPercent = "iot_gateway.IOT_ITEM_DEADBAND_PERCENT"; + + /// + /// Enable or disable the IoT Item. + /// + public const string Enabled = "iot_gateway.IOT_ITEM_ENABLED"; + + /// + /// Data type of the referenced tag. + /// + public const string DataType = "iot_gateway.IOT_ITEM_DATA_TYPE"; + } + } +} diff --git a/Kepware.Api/Model/Project/IotGateway/IotGatewayCollections.cs b/Kepware.Api/Model/Project/IotGateway/IotGatewayCollections.cs new file mode 100644 index 0000000..d480d37 --- /dev/null +++ b/Kepware.Api/Model/Project/IotGateway/IotGatewayCollections.cs @@ -0,0 +1,50 @@ +namespace Kepware.Api.Model +{ + /// + /// Represents the collection of MQTT Client agents in the IoT Gateway. + /// + [Endpoint("/config/v1/project/_iot_gateway/mqtt_clients")] + public class MqttClientAgentCollection : EntityCollection + { + /// + /// Initializes a new instance of the class. + /// + public MqttClientAgentCollection() { } + } + + /// + /// Represents the collection of REST Client agents in the IoT Gateway. + /// + [Endpoint("/config/v1/project/_iot_gateway/rest_clients")] + public class RestClientAgentCollection : EntityCollection + { + /// + /// Initializes a new instance of the class. + /// + public RestClientAgentCollection() { } + } + + /// + /// Represents the collection of REST Server agents in the IoT Gateway. + /// + [Endpoint("/config/v1/project/_iot_gateway/rest_servers")] + public class RestServerAgentCollection : EntityCollection + { + /// + /// Initializes a new instance of the class. + /// + public RestServerAgentCollection() { } + } + + /// + /// Represents the collection of IoT Items in an IoT Gateway agent. + /// + [Endpoint("/config/v1/project/_iot_gateway/mqtt_clients/{agentName}/iot_items")] + public class IotItemCollection : EntityCollection + { + /// + /// Initializes a new instance of the class. + /// + public IotItemCollection() { } + } +} diff --git a/Kepware.Api/Model/Project/IotGateway/IotGatewayContainer.cs b/Kepware.Api/Model/Project/IotGateway/IotGatewayContainer.cs new file mode 100644 index 0000000..3af217f --- /dev/null +++ b/Kepware.Api/Model/Project/IotGateway/IotGatewayContainer.cs @@ -0,0 +1,41 @@ +using System.Text.Json.Serialization; + +namespace Kepware.Api.Model +{ + /// + /// Container class representing the _iot_gateway node in the full project JSON. + /// Holds the three agent collections (MQTT Client, REST Client, REST Server). + /// + public class IotGatewayContainer + { + /// + /// Gets or sets the MQTT Client agents. + /// + [JsonPropertyName("mqtt_clients")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public MqttClientAgentCollection? MqttClientAgents { get; set; } + + /// + /// Gets or sets the REST Client agents. + /// + [JsonPropertyName("rest_clients")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public RestClientAgentCollection? RestClientAgents { get; set; } + + /// + /// Gets or sets the REST Server agents. + /// + [JsonPropertyName("rest_servers")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public RestServerAgentCollection? RestServerAgents { get; set; } + + /// + /// Returns true if all three agent collections are null or empty. + /// + [JsonIgnore] + public bool IsEmpty => + (MqttClientAgents == null || MqttClientAgents.Count == 0) && + (RestClientAgents == null || RestClientAgents.Count == 0) && + (RestServerAgents == null || RestServerAgents.Count == 0); + } +} diff --git a/Kepware.Api/Model/Project/IotGateway/IotGatewayEnums.cs b/Kepware.Api/Model/Project/IotGateway/IotGatewayEnums.cs new file mode 100644 index 0000000..b6fe7e2 --- /dev/null +++ b/Kepware.Api/Model/Project/IotGateway/IotGatewayEnums.cs @@ -0,0 +1,160 @@ +namespace Kepware.Api.Model +{ + /// + /// Specifies the publish type for IoT Gateway agents. + /// + public enum IotPublishType + { + /// Publish at a set interval rate. + Interval = 0, + /// Publish on data change. + OnDataChange = 1 + } + + /// + /// Specifies the publish format for IoT Gateway agents. + /// + public enum IotPublishFormat + { + /// Output based on tags that have changed value or quality. + NarrowFormat = 0, + /// Output includes all enabled tags regardless of changes. + WideFormat = 1 + } + + /// + /// Specifies the message format for IoT Gateway agents. + /// + public enum IotMessageFormat + { + /// Standard template format. + StandardTemplate = 0, + /// Advanced template format. + AdvancedTemplate = 1 + } + + /// + /// Specifies the MQTT Quality of Service level. + /// + public enum MqttQos + { + /// At most once delivery (fire and forget). + AtMostOnce = 0, + /// At least once delivery (acknowledged delivery). + AtLeastOnce = 1, + /// Exactly once delivery (assured delivery). + ExactlyOnce = 2 + } + + /// + /// Specifies the TLS version for MQTT secure connections. + /// + public enum MqttTlsVersion + { + /// Default TLS version. + Default = 0, + /// TLS version 1.0. + V1_0 = 1, + /// TLS version 1.1. + V1_1 = 2, + /// TLS version 1.2. + V1_2 = 3 + } + + /// + /// Specifies the HTTP method for REST Client agents. + /// + public enum RestClientHttpMethod + { + /// HTTP POST method. + Post = 0, + /// HTTP PUT method. + Put = 1 + } + + /// + /// Specifies the content-type header for REST Client agent publish. + /// + public enum RestClientMediaType + { + /// application/json + ApplicationJson = 0, + /// application/xml + ApplicationXml = 1, + /// application/xhtml+xml + ApplicationXhtmlXml = 2, + /// text/plain + TextPlain = 3, + /// text/html + TextHtml = 4 + } + + /// + /// Specifies the data type for IoT Items. + /// + public enum IotItemDataType + { + /// Default / auto-detect. + Default = -1, + /// String data type. + String = 0, + /// Boolean data type. + Boolean = 1, + /// Char data type. + Char = 2, + /// Byte data type. + Byte = 3, + /// Short (16-bit signed integer) data type. + Short = 4, + /// Word (16-bit unsigned integer) data type. + Word = 5, + /// Long (32-bit signed integer) data type. + Long = 6, + /// DWord (32-bit unsigned integer) data type. + DWord = 7, + /// Float (32-bit floating point) data type. + Float = 8, + /// Double (64-bit floating point) data type. + Double = 9, + /// BCD (binary-coded decimal) data type. + BCD = 10, + /// LBCD (long binary-coded decimal) data type. + LBCD = 11, + /// Date data type. + Date = 12, + /// LLong (64-bit signed integer) data type. + LLong = 13, + /// QWord (64-bit unsigned integer) data type. + QWord = 14, + /// String array data type. + StringArray = 20, + /// Boolean array data type. + BooleanArray = 21, + /// Char array data type. + CharArray = 22, + /// Byte array data type. + ByteArray = 23, + /// Short array data type. + ShortArray = 24, + /// Word array data type. + WordArray = 25, + /// Long array data type. + LongArray = 26, + /// DWord array data type. + DWordArray = 27, + /// Float array data type. + FloatArray = 28, + /// Double array data type. + DoubleArray = 29, + /// BCD array data type. + BCDArray = 30, + /// LBCD array data type. + LBCDArray = 31, + /// Date array data type. + DateArray = 32, + /// LLong array data type. + LLongArray = 33, + /// QWord array data type. + QWordArray = 34 + } +} diff --git a/Kepware.Api/Model/Project/IotGateway/IotItem.cs b/Kepware.Api/Model/Project/IotGateway/IotItem.cs new file mode 100644 index 0000000..623996e --- /dev/null +++ b/Kepware.Api/Model/Project/IotGateway/IotItem.cs @@ -0,0 +1,102 @@ +using System.Text.Json.Serialization; +using YamlDotNet.Serialization; + +namespace Kepware.Api.Model +{ + /// + /// Represents an IoT Item in the IoT Gateway. IoT Items are children of agent types + /// and identify the data objects exposed by the agent. + /// + [Endpoint("/config/v1/project/_iot_gateway/mqtt_clients/{agentName}/iot_items/{name}")] + public class IotItem : NamedEntity + { + /// + /// Initializes a new instance of the class. + /// + public IotItem() + { + } + + /// + /// Initializes a new instance of the class with the specified name. + /// + /// The name of the IoT Item. + public IotItem(string name) : base(name) + { + } + + #region Properties + + /// + /// Gets or sets the full channel.device.name of the referenced server tag. + /// + [YamlIgnore, JsonIgnore] + public string? ServerTag + { + get => GetDynamicProperty(Properties.IotItem.ServerTag); + set => SetDynamicProperty(Properties.IotItem.ServerTag, value); + } + + /// + /// Gets or sets whether to use scan rate to collect data from the device. + /// + [YamlIgnore, JsonIgnore] + public bool? UseScanRate + { + get => GetDynamicProperty(Properties.IotItem.UseScanRate); + set => SetDynamicProperty(Properties.IotItem.UseScanRate, value); + } + + /// + /// Gets or sets the scan rate in milliseconds. + /// + [YamlIgnore, JsonIgnore] + public int? ScanRateMs + { + get => GetDynamicProperty(Properties.IotItem.ScanRateMs); + set => SetDynamicProperty(Properties.IotItem.ScanRateMs, value); + } + + /// + /// Gets or sets whether to publish on every scan regardless of value change. + /// + [YamlIgnore, JsonIgnore] + public bool? PublishEveryScan + { + get => GetDynamicProperty(Properties.IotItem.PublishEveryScan); + set => SetDynamicProperty(Properties.IotItem.PublishEveryScan, value); + } + + /// + /// Gets or sets the deadband percentage for publish threshold. + /// + [YamlIgnore, JsonIgnore] + public double? DeadbandPercent + { + get => GetDynamicProperty(Properties.IotItem.DeadbandPercent); + set => SetDynamicProperty(Properties.IotItem.DeadbandPercent, value); + } + + /// + /// Gets or sets whether the IoT Item is enabled. + /// + [YamlIgnore, JsonIgnore] + public bool? Enabled + { + get => GetDynamicProperty(Properties.IotItem.Enabled); + set => SetDynamicProperty(Properties.IotItem.Enabled, value); + } + + /// + /// Gets or sets the data type of the referenced tag. + /// + [YamlIgnore, JsonIgnore] + public IotItemDataType? DataType + { + get => (IotItemDataType?)GetDynamicProperty(Properties.IotItem.DataType); + set => SetDynamicProperty(Properties.IotItem.DataType, (int?)value); + } + + #endregion + } +} diff --git a/Kepware.Api/Model/Project/IotGateway/MqttClientAgent.cs b/Kepware.Api/Model/Project/IotGateway/MqttClientAgent.cs new file mode 100644 index 0000000..54b60ab --- /dev/null +++ b/Kepware.Api/Model/Project/IotGateway/MqttClientAgent.cs @@ -0,0 +1,169 @@ +using System.Text.Json.Serialization; +using YamlDotNet.Serialization; + +namespace Kepware.Api.Model +{ + /// + /// Represents an MQTT Client agent in the IoT Gateway. + /// + [Endpoint("/config/v1/project/_iot_gateway/mqtt_clients/{name}")] + public class MqttClientAgent : PublishingIotAgent + { + /// + /// Initializes a new instance of the class. + /// + public MqttClientAgent() + { + } + + /// + /// Initializes a new instance of the class with the specified name. + /// + /// The name of the agent. + public MqttClientAgent(string name) : base(name) + { + } + + #region MQTT Properties + + /// + /// Gets or sets the MQTT broker URL. + /// + [YamlIgnore, JsonIgnore] + public string? Url + { + get => GetDynamicProperty(Properties.MqttClientAgent.Url); + set => SetDynamicProperty(Properties.MqttClientAgent.Url, value); + } + + /// + /// Gets or sets the MQTT topic for publishing data. + /// + [YamlIgnore, JsonIgnore] + public string? Topic + { + get => GetDynamicProperty(Properties.MqttClientAgent.Topic); + set => SetDynamicProperty(Properties.MqttClientAgent.Topic, value); + } + + /// + /// Gets or sets the MQTT Quality of Service level. + /// + [YamlIgnore, JsonIgnore] + public MqttQos? Qos + { + get => (MqttQos?)GetDynamicProperty(Properties.MqttClientAgent.Qos); + set => SetDynamicProperty(Properties.MqttClientAgent.Qos, (int?)value); + } + + /// + /// Gets or sets the client ID for broker communication. + /// + [YamlIgnore, JsonIgnore] + public string? ClientId + { + get => GetDynamicProperty(Properties.MqttClientAgent.ClientId); + set => SetDynamicProperty(Properties.MqttClientAgent.ClientId, value); + } + + /// + /// Gets or sets the username for broker authentication. + /// + [YamlIgnore, JsonIgnore] + public string? Username + { + get => GetDynamicProperty(Properties.MqttClientAgent.Username); + set => SetDynamicProperty(Properties.MqttClientAgent.Username, value); + } + + /// + /// Gets or sets the password for broker authentication. + /// + [YamlIgnore, JsonIgnore] + public string? Password + { + get => GetDynamicProperty(Properties.MqttClientAgent.Password); + set => SetDynamicProperty(Properties.MqttClientAgent.Password, value); + } + + /// + /// Gets or sets the TLS version for secure connections. + /// + [YamlIgnore, JsonIgnore] + public MqttTlsVersion? TlsVersion + { + get => (MqttTlsVersion?)GetDynamicProperty(Properties.MqttClientAgent.TlsVersion); + set => SetDynamicProperty(Properties.MqttClientAgent.TlsVersion, (int?)value); + } + + /// + /// Gets or sets whether client certificate authentication is enabled. + /// + [YamlIgnore, JsonIgnore] + public bool? ClientCertificate + { + get => GetDynamicProperty(Properties.MqttClientAgent.ClientCertificate); + set => SetDynamicProperty(Properties.MqttClientAgent.ClientCertificate, value); + } + + #endregion + + #region Last Will Properties + + /// + /// Gets or sets whether Last Will and Testament is enabled. + /// + [YamlIgnore, JsonIgnore] + public bool? EnableLastWill + { + get => GetDynamicProperty(Properties.MqttClientAgent.EnableLastWill); + set => SetDynamicProperty(Properties.MqttClientAgent.EnableLastWill, value); + } + + /// + /// Gets or sets the Last Will and Testament topic. + /// + [YamlIgnore, JsonIgnore] + public string? LastWillTopic + { + get => GetDynamicProperty(Properties.MqttClientAgent.LastWillTopic); + set => SetDynamicProperty(Properties.MqttClientAgent.LastWillTopic, value); + } + + /// + /// Gets or sets the Last Will and Testament message. + /// + [YamlIgnore, JsonIgnore] + public string? LastWillMessage + { + get => GetDynamicProperty(Properties.MqttClientAgent.LastWillMessage); + set => SetDynamicProperty(Properties.MqttClientAgent.LastWillMessage, value); + } + + #endregion + + #region Subscription Properties + + /// + /// Gets or sets whether write request listening is enabled. + /// + [YamlIgnore, JsonIgnore] + public bool? EnableWriteTopic + { + get => GetDynamicProperty(Properties.MqttClientAgent.EnableWriteTopic); + set => SetDynamicProperty(Properties.MqttClientAgent.EnableWriteTopic, value); + } + + /// + /// Gets or sets the topic for write request subscriptions. + /// + [YamlIgnore, JsonIgnore] + public string? WriteTopic + { + get => GetDynamicProperty(Properties.MqttClientAgent.WriteTopic); + set => SetDynamicProperty(Properties.MqttClientAgent.WriteTopic, value); + } + + #endregion + } +} diff --git a/Kepware.Api/Model/Project/IotGateway/PublishingIotAgent.cs b/Kepware.Api/Model/Project/IotGateway/PublishingIotAgent.cs new file mode 100644 index 0000000..1b332ca --- /dev/null +++ b/Kepware.Api/Model/Project/IotGateway/PublishingIotAgent.cs @@ -0,0 +1,135 @@ +using System.Text.Json.Serialization; +using YamlDotNet.Serialization; + +namespace Kepware.Api.Model +{ + /// + /// Base class for IoT Gateway agent types that support publishing (MQTT Client and REST Client). + /// Contains publish configuration and message template properties not applicable to REST Server agents. + /// + public class PublishingIotAgent : IotAgent + { + /// + /// Initializes a new instance of the class. + /// + public PublishingIotAgent() + { + } + + /// + /// Initializes a new instance of the class with the specified name. + /// + /// The name of the agent. + public PublishingIotAgent(string name) : base(name) + { + } + + #region Publish Properties + + /// + /// Gets or sets the publish type (Interval or On Data Change). + /// + [YamlIgnore, JsonIgnore] + public IotPublishType? PublishType + { + get => (IotPublishType?)GetDynamicProperty(Properties.IotAgent.PublishType); + set => SetDynamicProperty(Properties.IotAgent.PublishType, (int?)value); + } + + /// + /// Gets or sets the publish rate in milliseconds. + /// + [YamlIgnore, JsonIgnore] + public int? RateMs + { + get => GetDynamicProperty(Properties.IotAgent.RateMs); + set => SetDynamicProperty(Properties.IotAgent.RateMs, value); + } + + /// + /// Gets or sets the publish format (Wide or Narrow). + /// + [YamlIgnore, JsonIgnore] + public IotPublishFormat? PublishFormat + { + get => (IotPublishFormat?)GetDynamicProperty(Properties.IotAgent.PublishFormat); + set => SetDynamicProperty(Properties.IotAgent.PublishFormat, (int?)value); + } + + /// + /// Gets or sets the maximum number of tag events per publish in narrow format. + /// + [YamlIgnore, JsonIgnore] + public int? MaxEventsPerPublish + { + get => GetDynamicProperty(Properties.IotAgent.MaxEventsPerPublish); + set => SetDynamicProperty(Properties.IotAgent.MaxEventsPerPublish, value); + } + + /// + /// Gets or sets the transaction timeout in seconds. + /// + [YamlIgnore, JsonIgnore] + public int? TransactionTimeoutS + { + get => GetDynamicProperty(Properties.IotAgent.TransactionTimeoutS); + set => SetDynamicProperty(Properties.IotAgent.TransactionTimeoutS, value); + } + + /// + /// Gets or sets whether the initial update should be sent when the agent starts. + /// + [YamlIgnore, JsonIgnore] + public bool? SendInitialUpdate + { + get => GetDynamicProperty(Properties.IotAgent.SendInitialUpdate); + set => SetDynamicProperty(Properties.IotAgent.SendInitialUpdate, value); + } + + #endregion + + #region Message Properties + + /// + /// Gets or sets the message format (Standard or Advanced Template). + /// + [YamlIgnore, JsonIgnore] + public IotMessageFormat? MessageFormat + { + get => (IotMessageFormat?)GetDynamicProperty(Properties.IotAgent.MessageFormat); + set => SetDynamicProperty(Properties.IotAgent.MessageFormat, (int?)value); + } + + /// + /// Gets or sets the standard template for message formatting. + /// + [YamlIgnore, JsonIgnore] + public string? StandardTemplate + { + get => GetDynamicProperty(Properties.IotAgent.StandardTemplate); + set => SetDynamicProperty(Properties.IotAgent.StandardTemplate, value); + } + + /// + /// Gets or sets the expansion of |VALUES| in the standard template. + /// + [YamlIgnore, JsonIgnore] + public string? ExpansionOfValues + { + get => GetDynamicProperty(Properties.IotAgent.ExpansionOfValues); + set => SetDynamicProperty(Properties.IotAgent.ExpansionOfValues, value); + } + + /// + /// Gets or sets the advanced template for message formatting. + /// + [YamlIgnore, JsonIgnore] + public string? AdvancedTemplate + { + get => GetDynamicProperty(Properties.IotAgent.AdvancedTemplate); + set => SetDynamicProperty(Properties.IotAgent.AdvancedTemplate, value); + } + + #endregion + } +} diff --git a/Kepware.Api/Model/Project/IotGateway/RestClientAgent.cs b/Kepware.Api/Model/Project/IotGateway/RestClientAgent.cs new file mode 100644 index 0000000..f3d8acf --- /dev/null +++ b/Kepware.Api/Model/Project/IotGateway/RestClientAgent.cs @@ -0,0 +1,101 @@ +using System.Text.Json.Serialization; +using YamlDotNet.Serialization; + +namespace Kepware.Api.Model +{ + /// + /// Represents a REST Client agent in the IoT Gateway. + /// + [Endpoint("/config/v1/project/_iot_gateway/rest_clients/{name}")] + public class RestClientAgent : PublishingIotAgent + { + /// + /// Initializes a new instance of the class. + /// + public RestClientAgent() + { + } + + /// + /// Initializes a new instance of the class with the specified name. + /// + /// The name of the agent. + public RestClientAgent(string name) : base(name) + { + } + + #region REST Client Properties + + /// + /// Gets or sets the REST endpoint URL. + /// + [YamlIgnore, JsonIgnore] + public string? Url + { + get => GetDynamicProperty(Properties.RestClientAgent.Url); + set => SetDynamicProperty(Properties.RestClientAgent.Url, value); + } + + /// + /// Gets or sets the HTTP method for publishing data (POST or PUT). + /// + [YamlIgnore, JsonIgnore] + public RestClientHttpMethod? HttpMethod + { + get => (RestClientHttpMethod?)GetDynamicProperty(Properties.RestClientAgent.HttpMethod); + set => SetDynamicProperty(Properties.RestClientAgent.HttpMethod, (int?)value); + } + + /// + /// Gets or sets the HTTP header name-value pairs. + /// + [YamlIgnore, JsonIgnore] + public string? HttpHeader + { + get => GetDynamicProperty(Properties.RestClientAgent.HttpHeader); + set => SetDynamicProperty(Properties.RestClientAgent.HttpHeader, value); + } + + /// + /// Gets or sets the content-type for published data. + /// + [YamlIgnore, JsonIgnore] + public RestClientMediaType? PublishMediaType + { + get => (RestClientMediaType?)GetDynamicProperty(Properties.RestClientAgent.PublishMediaType); + set => SetDynamicProperty(Properties.RestClientAgent.PublishMediaType, (int?)value); + } + + /// + /// Gets or sets the username for basic HTTP authentication. + /// + [YamlIgnore, JsonIgnore] + public string? Username + { + get => GetDynamicProperty(Properties.RestClientAgent.Username); + set => SetDynamicProperty(Properties.RestClientAgent.Username, value); + } + + /// + /// Gets or sets the password for basic HTTP authentication. + /// + [YamlIgnore, JsonIgnore] + public string? Password + { + get => GetDynamicProperty(Properties.RestClientAgent.Password); + set => SetDynamicProperty(Properties.RestClientAgent.Password, value); + } + + /// + /// Gets or sets whether updates should be buffered when a publish fails. + /// + [YamlIgnore, JsonIgnore] + public bool? BufferOnFailedPublish + { + get => GetDynamicProperty(Properties.RestClientAgent.BufferOnFailedPublish); + set => SetDynamicProperty(Properties.RestClientAgent.BufferOnFailedPublish, value); + } + + #endregion + } +} diff --git a/Kepware.Api/Model/Project/IotGateway/RestServerAgent.cs b/Kepware.Api/Model/Project/IotGateway/RestServerAgent.cs new file mode 100644 index 0000000..e64b349 --- /dev/null +++ b/Kepware.Api/Model/Project/IotGateway/RestServerAgent.cs @@ -0,0 +1,92 @@ +using System.Text.Json.Serialization; +using YamlDotNet.Serialization; + +namespace Kepware.Api.Model +{ + /// + /// Represents a REST Server agent in the IoT Gateway. + /// REST Server agents do not have publish or message template properties. + /// + [Endpoint("/config/v1/project/_iot_gateway/rest_servers/{name}")] + public class RestServerAgent : IotAgent + { + /// + /// Initializes a new instance of the class. + /// + public RestServerAgent() + { + } + + /// + /// Initializes a new instance of the class with the specified name. + /// + /// The name of the agent. + public RestServerAgent(string name) : base(name) + { + } + + #region REST Server Properties + + /// + /// Gets or sets the network adapter for the REST server endpoint. + /// + [YamlIgnore, JsonIgnore] + public string? NetworkAdapter + { + get => GetDynamicProperty(Properties.RestServerAgent.NetworkAdapter); + set => SetDynamicProperty(Properties.RestServerAgent.NetworkAdapter, value); + } + + /// + /// Gets or sets the port number for the REST server. + /// + [YamlIgnore, JsonIgnore] + public int? PortNumber + { + get => GetDynamicProperty(Properties.RestServerAgent.PortNumber); + set => SetDynamicProperty(Properties.RestServerAgent.PortNumber, value); + } + + /// + /// Gets or sets the CORS allowed origins (comma-delimited list). + /// + [YamlIgnore, JsonIgnore] + public string? CorsAllowedOrigins + { + get => GetDynamicProperty(Properties.RestServerAgent.CorsAllowedOrigins); + set => SetDynamicProperty(Properties.RestServerAgent.CorsAllowedOrigins, value); + } + + /// + /// Gets or sets whether HTTPS encryption is enabled. + /// + [YamlIgnore, JsonIgnore] + public bool? UseHttps + { + get => GetDynamicProperty(Properties.RestServerAgent.UseHttps); + set => SetDynamicProperty(Properties.RestServerAgent.UseHttps, value); + } + + /// + /// Gets or sets whether the write endpoint is enabled. + /// + [YamlIgnore, JsonIgnore] + public bool? EnableWriteEndpoint + { + get => GetDynamicProperty(Properties.RestServerAgent.EnableWriteEndpoint); + set => SetDynamicProperty(Properties.RestServerAgent.EnableWriteEndpoint, value); + } + + /// + /// Gets or sets whether anonymous login is allowed. + /// + [YamlIgnore, JsonIgnore] + public bool? AllowAnonymousLogin + { + get => GetDynamicProperty(Properties.RestServerAgent.AllowAnonymousLogin); + set => SetDynamicProperty(Properties.RestServerAgent.AllowAnonymousLogin, value); + } + + #endregion + } +} diff --git a/Kepware.Api/Model/Project/Project.Properties.cs b/Kepware.Api/Model/Project/Project.Properties.cs index 207fba2..af0fb24 100644 --- a/Kepware.Api/Model/Project/Project.Properties.cs +++ b/Kepware.Api/Model/Project/Project.Properties.cs @@ -20,8 +20,7 @@ public static class ProjectSettings /// /// Count of tags identified in the project. /// - // TODO: Does this need to be moved to non-seralized properties? - public const string TagsDefined = "servermain.PROJECT_TAGS_DEFINED"; + public const string TagsDefined = Properties.NonSerialized.ProjectTagsDefined; #endregion diff --git a/Kepware.Api/Model/Project/Project.cs b/Kepware.Api/Model/Project/Project.cs index bfbcceb..3b25b2b 100644 --- a/Kepware.Api/Model/Project/Project.cs +++ b/Kepware.Api/Model/Project/Project.cs @@ -24,6 +24,11 @@ public class Project : DefaultEntity /// public bool IsLoadedByProjectLoadService { get; internal set; } = false; + /// + /// If this is true, it indicates that this is an empty project object that was instantiated without data from the server. + /// + public bool IsEmpty => Channels == null && (IotGateway == null || IotGateway.IsEmpty) && DynamicProperties.Count == 0; + /// /// Initializes a new instance of the class. /// @@ -53,6 +58,15 @@ public Project() [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public ChannelCollection? Channels { get; set; } + /// + /// Gets or sets the IoT Gateway container holding MQTT Client, REST Client, and REST Server agent collections. + /// + [YamlIgnore] + [JsonPropertyName("_iot_gateway")] + [JsonPropertyOrder(101)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IotGatewayContainer? IotGateway { get; set; } + /// /// Recursively cleans up the project and all its children /// @@ -72,6 +86,16 @@ public override async Task Cleanup(IKepwareDefaultValueProvider defaultValueProv await channel.Cleanup(defaultValueProvider, blnRemoveProjectId, cancellationToken).ConfigureAwait(false); } } + + if (IotGateway != null) + { + foreach (var agent in (IotGateway.MqttClientAgents ?? []).Cast() + .Concat(IotGateway.RestClientAgents ?? []) + .Concat(IotGateway.RestServerAgents ?? [])) + { + await agent.Cleanup(defaultValueProvider, blnRemoveProjectId, cancellationToken).ConfigureAwait(false); + } + } } public async Task CloneAsync(CancellationToken cancellationToken = default) diff --git a/Kepware.Api/Model/Project/ProjectPropertiesJsonConverter.cs b/Kepware.Api/Model/Project/ProjectPropertiesJsonConverter.cs index 3401c56..db038cc 100644 --- a/Kepware.Api/Model/Project/ProjectPropertiesJsonConverter.cs +++ b/Kepware.Api/Model/Project/ProjectPropertiesJsonConverter.cs @@ -53,10 +53,10 @@ public class ProjectPropertiesJsonConverter : JsonConverter // Handle exposed properties supporting Project model // This includes properties inherited from BaseEntity such as PROJECT_ID, DESCRIPTION, etc. // TODO: Expand as needed for other known properties or consider common approach for BaseEntity properties - if (root.TryGetProperty("channels", out var channelsProp)) + if (prop.Name == "channels") { var jsonTypeInfo = (JsonTypeInfo>)options.GetTypeInfo(typeof(List)); - var channels = JsonSerializer.Deserialize(channelsProp.GetRawText(), jsonTypeInfo); + var channels = JsonSerializer.Deserialize(prop.Value.GetRawText(), jsonTypeInfo); if (channels != null) { project.Channels = new ChannelCollection(); @@ -66,6 +66,56 @@ public class ProjectPropertiesJsonConverter : JsonConverter } } } + else if (prop.Name == "_iot_gateway") + { + // _iot_gateway is an array containing a single wrapper object with + // "common.ALLTYPES_NAME": "_IoT_Gateway" and the agent collection arrays. + if (prop.Value.ValueKind == JsonValueKind.Array) + { + foreach (var gwElement in prop.Value.EnumerateArray()) + { + if (gwElement.ValueKind == JsonValueKind.Object) + { + var container = new IotGatewayContainer(); + + if (gwElement.TryGetProperty("mqtt_clients", out var mqttProp)) + { + var mqttTypeInfo = (JsonTypeInfo>)options.GetTypeInfo(typeof(List)); + var agents = JsonSerializer.Deserialize(mqttProp.GetRawText(), mqttTypeInfo); + if (agents != null) + { + container.MqttClientAgents = new MqttClientAgentCollection(); + foreach (var agent in agents) container.MqttClientAgents.Add(agent); + } + } + + if (gwElement.TryGetProperty("rest_clients", out var restClientProp)) + { + var restClientTypeInfo = (JsonTypeInfo>)options.GetTypeInfo(typeof(List)); + var agents = JsonSerializer.Deserialize(restClientProp.GetRawText(), restClientTypeInfo); + if (agents != null) + { + container.RestClientAgents = new RestClientAgentCollection(); + foreach (var agent in agents) container.RestClientAgents.Add(agent); + } + } + + if (gwElement.TryGetProperty("rest_servers", out var restServerProp)) + { + var restServerTypeInfo = (JsonTypeInfo>)options.GetTypeInfo(typeof(List)); + var agents = JsonSerializer.Deserialize(restServerProp.GetRawText(), restServerTypeInfo); + if (agents != null) + { + container.RestServerAgents = new RestServerAgentCollection(); + foreach (var agent in agents) container.RestServerAgents.Add(agent); + } + } + + project.IotGateway = container; + } + } + } + } else if (prop.Name == "PROJECT_ID") { if (prop.Value.TryGetInt64(out var projectId)) @@ -104,6 +154,40 @@ public override void Write(Utf8JsonWriter writer, Project value, JsonSerializerO JsonSerializer.Serialize(writer, value.Channels.ToList(), jsonTypeInfo); } + // Emit IoT Gateway container as array envelope (if present) + if (value.IotGateway != null && !value.IotGateway.IsEmpty) + { + writer.WritePropertyName("_iot_gateway"); + writer.WriteStartArray(); + writer.WriteStartObject(); + + writer.WriteString("common.ALLTYPES_NAME", "_IoT_Gateway"); + + if (value.IotGateway.MqttClientAgents != null && value.IotGateway.MqttClientAgents.Count > 0) + { + writer.WritePropertyName("mqtt_clients"); + var mqttTypeInfo = (JsonTypeInfo>)options.GetTypeInfo(typeof(List)); + JsonSerializer.Serialize(writer, value.IotGateway.MqttClientAgents.ToList(), mqttTypeInfo); + } + + if (value.IotGateway.RestClientAgents != null && value.IotGateway.RestClientAgents.Count > 0) + { + writer.WritePropertyName("rest_clients"); + var restClientTypeInfo = (JsonTypeInfo>)options.GetTypeInfo(typeof(List)); + JsonSerializer.Serialize(writer, value.IotGateway.RestClientAgents.ToList(), restClientTypeInfo); + } + + if (value.IotGateway.RestServerAgents != null && value.IotGateway.RestServerAgents.Count > 0) + { + writer.WritePropertyName("rest_servers"); + var restServerTypeInfo = (JsonTypeInfo>)options.GetTypeInfo(typeof(List)); + JsonSerializer.Serialize(writer, value.IotGateway.RestServerAgents.ToList(), restServerTypeInfo); + } + + writer.WriteEndObject(); + writer.WriteEndArray(); + } + // Build grouped client_interfaces element from flattened dynamic properties var clientInterfacesElement = ClientInterfacesFlattener.BuildClientInterfacesArrayFromDynamicProperties(value.DynamicProperties); if (clientInterfacesElement.HasValue) diff --git a/Kepware.Api/Model/Properties.cs b/Kepware.Api/Model/Properties.cs index c77aeb2..4b7cae2 100644 --- a/Kepware.Api/Model/Properties.cs +++ b/Kepware.Api/Model/Properties.cs @@ -71,13 +71,13 @@ public static class NonSerialized /// public const string TagGrpTotalTagCount = "servermain.TAGGROUP_TOTAL_TAG_COUNT"; /// - /// The local tag count in a tag group property key. + /// The local tag count in a tag group property key. Used for tag groups within devices. /// public const string TagGrpTagCount = "servermain.TAGGROUP_LOCAL_TAG_COUNT"; /// /// The static tag count in a channel property key. /// - public const string ChannelTagCount = "servermain.CHANNEL_STATIC_TAG_COUNT"; + public const string ChannelStaticTagCount = "servermain.CHANNEL_STATIC_TAG_COUNT"; /// /// The autogenerated tag group property key. /// @@ -96,6 +96,21 @@ public static class NonSerialized /// public const string ProjectTagsDefined = "servermain.PROJECT_TAGS_DEFINED"; + /// + /// Total number of tags configured under this agent (read-only). + /// + public const string ThisAgentTotal = "iot_gateway.AGENTTYPES_THIS_AGENT_TOTAL"; + + /// + /// Total number of tags configured under all agents (read-only). + /// + public const string AllAgentsTotal = "iot_gateway.AGENTTYPES_ALL_AGENTS_TOTAL"; + + /// + /// Maximum number of configured tags allowed by the license (read-only). + /// + public const string LicenseLimit = "iot_gateway.AGENTTYPES_LICENSE_LIMIT"; + /// /// A set of non-serialized properties. /// @@ -104,7 +119,7 @@ public static class NonSerialized ChannelAssignment, TagGrpTotalTagCount, TagGrpTagCount, - ChannelTagCount, + ChannelStaticTagCount, TagGroupAutogenerated, TagAutogenerated , DeviceStaticTagCount, diff --git a/Kepware.Api/README.md b/Kepware.Api/README.md index f3d4122..6d7d2f3 100644 --- a/Kepware.Api/README.md +++ b/Kepware.Api/README.md @@ -18,17 +18,19 @@ This package is designed to work with all versions of Kepware that support the C | :----------: | :----------: | :----------: | | **Project Properties** | Y | Y | | **Connectivity**
*(Channel, Devices, Tags, Tag Groups)* | Y | Y | +| **IoT Gateway**
*(Agents, IoT Items)* | Y | Y | | **Administration**
*(User Groups, Users, UA Endpoints, Local License Server)* | Y[^1] | Y | | **Product Info and Health Status** | Y[^4] | Y | -| **Export Project** | Y[^2] | Y | -| **Import Project (via JsonProjectLoad Service)[^3]** | N | N | +| **Export Project** | Y | Y | +| **Import Project (via JsonProjectLoad Service)** | N[^2] | N | | **Import Project (via CompareAndApply)[^3]** | Y | Y | -[^1]: UA Endpoints and Local License Server supported for Kepware Edge only -[^2]: Json serialization for the whole project was added to Kepware Server v6.17 and later builds, the SDK detects the server version and uses the appropriate service or exports the project by multiple requests if using KepwareApiClient.LoadProject. -[^3]: CompareAndApply is handled by the SDK, it compares the source project with the server project and applies the changes. The JsonProjectLoad service (added v6.17 and later) is a direct call to the server to load a new project serialize JSON configuration to the server/edge instance. +[^2]: JsonProjectLoad was added to Kepware Server v6.17 and later builds. +[^3]: [CompareAndApply](/Kepware.Api/ClientHandler/ProjectApiHandler.cs) is handled by the SDK. It compares the source project with another server project and applies the changes. [^4]: Added to Kepware Server v6.13 and later builds +**NOTE:** Exporting a project from a Kepware server is done using the KepwareApiClient.LoadProjectAsync method. This detects the server version and uses the appropriate method to either export/load the whole project or loads the project by multiple requests. This ensures that large projects can be exported/loaded from the Kepware instance as optimally as possible based on the current API design. See [LoadProjectAsync](/Kepware.Api/ClientHandler/ProjectApiHandler.cs) for more details. + 3. Configuration API *Services* implemented: | Services | KS | KE | @@ -38,9 +40,8 @@ This package is designed to work with all versions of Kepware that support the C | **ProjectLoad and ProjectSave** | N | N | | **JsonProjectLoad\*\***
*(used for import project feature)* | N | N | -4. Synchronize configurations between your application and Kepware server. -5. Supports advanced operations like project comparison, entity synchronization, and driver property queries. -6. Built-in support for Dependency Injection to simplify integration. +4. Supports advanced operations like project comparison, entity synchronization, and driver property queries. +5. Built-in support for Dependency Injection to simplify integration. ## Installation @@ -52,16 +53,36 @@ Kepware.Api NuGet package is available from NuGet repository. ``` 2. Register the `KepwareApiClient` in your application using Dependency Injection: + ```csharp services.AddKepwareApiClient( name: "default", baseUrl: "https://localhost:57512", apiUserName: "Administrator", apiPassword: "StrongAdminPassword2025!", - disableCertificateValidation: true + disableCertificateValidation: false ); ``` + or + + ```csharp + var clientOptions = new KepwareClientOptions + { + HostUri = new Uri("https://localhost:57512"), + Username = "Administrator", + Password = "StrongAdminPassword2025!", + Timeout = TimeSpan.FromSeconds(60), + DisableCertifcateValidation = false, + ProjectLoadTagLimit = 100000 + }; + + services.AddKepwareApiClient( + name: "default", + options: clientOptions + ); + ``` + ## Key Methods ### Connection and Status @@ -78,9 +99,9 @@ Kepware.Api NuGet package is available from NuGet repository. Retrieves product information about the Kepware server. ### Project Management -- **Load Project:** +- **Export / Load Project:** ```csharp - var project = await api.LoadProject(blnLoadFullProject:true); + var project = await api.LoadProjectAsync(blnLoadFullProject:true); ``` Loads the current project from the Kepware server. diff --git a/Kepware.Api/Serializer/KepJsonContext.cs b/Kepware.Api/Serializer/KepJsonContext.cs index 5555c09..513827f 100644 --- a/Kepware.Api/Serializer/KepJsonContext.cs +++ b/Kepware.Api/Serializer/KepJsonContext.cs @@ -23,10 +23,16 @@ namespace Kepware.Api.Serializer [JsonSerializable(typeof(ServerUser))] [JsonSerializable(typeof(ProjectPermission))] [JsonSerializable(typeof(ApiResponseMessage))] + [JsonSerializable(typeof(UpdateApiResponseMessage))] [JsonSerializable(typeof(JobResponseMessage))] [JsonSerializable(typeof(JobStatusMessage))] [JsonSerializable(typeof(ServiceInvocationRequest))] - + [JsonSerializable(typeof(MqttClientAgent))] + [JsonSerializable(typeof(RestClientAgent))] + [JsonSerializable(typeof(RestServerAgent))] + [JsonSerializable(typeof(IotItem))] + [JsonSerializable(typeof(IotGatewayContainer))] + [JsonSerializable(typeof(List))] [JsonSerializable(typeof(List))] [JsonSerializable(typeof(List))] @@ -36,6 +42,10 @@ namespace Kepware.Api.Serializer [JsonSerializable(typeof(List))] [JsonSerializable(typeof(List))] [JsonSerializable(typeof(List))] + [JsonSerializable(typeof(List))] + [JsonSerializable(typeof(List))] + [JsonSerializable(typeof(List))] + [JsonSerializable(typeof(List))] [JsonSerializable(typeof(List))] [JsonSerializable(typeof(List))] [JsonSerializable(typeof(List))] @@ -95,6 +105,22 @@ public static JsonTypeInfo GetJsonTypeInfo() { return (JsonTypeInfo)(object)Default.DefaultEntity; } + else if (typeof(T) == typeof(MqttClientAgent)) + { + return (JsonTypeInfo)(object)Default.MqttClientAgent; + } + else if (typeof(T) == typeof(RestClientAgent)) + { + return (JsonTypeInfo)(object)Default.RestClientAgent; + } + else if (typeof(T) == typeof(RestServerAgent)) + { + return (JsonTypeInfo)(object)Default.RestServerAgent; + } + else if (typeof(T) == typeof(IotItem)) + { + return (JsonTypeInfo)(object)Default.IotItem; + } else { throw new NotSupportedException(); @@ -140,6 +166,22 @@ public static JsonTypeInfo> GetJsonListTypeInfo() { return (JsonTypeInfo>)(object)Default.ListDefaultEntity; } + else if (typeof(T) == typeof(MqttClientAgent)) + { + return (JsonTypeInfo>)(object)Default.ListMqttClientAgent; + } + else if (typeof(T) == typeof(RestClientAgent)) + { + return (JsonTypeInfo>)(object)Default.ListRestClientAgent; + } + else if (typeof(T) == typeof(RestServerAgent)) + { + return (JsonTypeInfo>)(object)Default.ListRestServerAgent; + } + else if (typeof(T) == typeof(IotItem)) + { + return (JsonTypeInfo>)(object)Default.ListIotItem; + } else { throw new NotSupportedException(); diff --git a/Kepware.Api/version.json b/Kepware.Api/version.json index 2bd0bfe..cfd5584 100644 --- a/Kepware.Api/version.json +++ b/Kepware.Api/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "1.0", + "version": "1.1", "pathFilters": [ "." ], "publicReleaseRefSpec": [ "^refs/heads/main$", diff --git a/KepwareSync.Service/SyncService.cs b/KepwareSync.Service/SyncService.cs index a3811ac..44ab1f4 100644 --- a/KepwareSync.Service/SyncService.cs +++ b/KepwareSync.Service/SyncService.cs @@ -122,7 +122,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) private static async Task FetchCurrentProjectIdAsync(KepwareApiClient client, CancellationToken cancellationToken) { - var project = await client.Project.LoadProject(false, cancellationToken).ConfigureAwait(false); + var project = await client.Project.LoadProjectAsync(false, cancellationToken: cancellationToken).ConfigureAwait(false); return project?.ProjectId ?? -1; } @@ -166,7 +166,7 @@ private async Task ProcessChangeAsync(ChangeEvent changeEvent, CancellationToken internal async Task SyncFromPrimaryKepServerAsync(CancellationToken cancellationToken = default) { m_logger.LogInformation("Synchronizing full project from primary Kepware..."); - var project = await m_kepServerClient.Project.LoadProject(true); + var project = await m_kepServerClient.Project.LoadProjectAsync(true); await project.Cleanup(m_kepServerClient, true, cancellationToken); if (m_kepServerClient.ClientOptions.Tag is KepwareSyncTarget targetOptions && @@ -239,7 +239,7 @@ private async Task SyncFromSecondaryKepServerAsync(CancellationToken cancellatio m_logger.LogInformation("Syncing from secondary client {ClientHostName}...", clientToSyncFrom.ClientHostName); } - var projectFromSecondary = await clientToSyncFrom.Project.LoadProject(true, cancellationToken).ConfigureAwait(false); + var projectFromSecondary = await clientToSyncFrom.Project.LoadProjectAsync(true, cancellationToken: cancellationToken).ConfigureAwait(false); await SyncProjectToKepServerAsync("secondary", projectFromSecondary, m_kepServerClient, "Primary", onSyncedWithChanges: () => NotifyChange(new ChangeEvent { Source = ChangeSource.PrimaryKepServer, Reason = "Sync from secondary kepserver" }), cancellationToken: cancellationToken).ConfigureAwait(false); @@ -285,12 +285,12 @@ private async Task SyncProjectToKepServerAsync(string projectSource, Project pro if (await kepServerClient.TestConnectionAsync(cancellationToken).ConfigureAwait(false)) { - var (inserts, updates, deletes) = await kepServerClient.Project.CompareAndApply(project, cancellationToken).ConfigureAwait(false); + var compareAndApplyResults = await kepServerClient.Project.CompareAndApplyDetailedAsync(project, cancellationToken).ConfigureAwait(false); - if (updates > 0 || deletes > 0 || inserts > 0) + if (compareAndApplyResults.Updates > 0 || compareAndApplyResults.Deletes > 0 || compareAndApplyResults.Inserts > 0 || compareAndApplyResults.Failures > 0) { - m_logger.LogInformation("Completed synchronisation from {ProjectSource} to {ClientName}-kepserver ({ClientHostName}): {NumUpdates} updates, {NumInserts} inserts, {NumDeletes} deletes", - projectSource, clientName, kepServerClient.ClientHostName, updates, inserts, deletes); + m_logger.LogInformation("Completed synchronisation from {ProjectSource} to {ClientName}-kepserver ({ClientHostName}): {NumUpdates} updates, {NumInserts} inserts, {NumDeletes} deletes, {NumFailures} failures", + projectSource, clientName, kepServerClient.ClientHostName, compareAndApplyResults.Updates, compareAndApplyResults.Inserts, compareAndApplyResults.Deletes, compareAndApplyResults.Failures); onSyncedWithChanges?.Invoke(); } else diff --git a/KepwareSync.Service/appsettings.json b/KepwareSync.Service/appsettings.json index faa757c..e835447 100644 --- a/KepwareSync.Service/appsettings.json +++ b/KepwareSync.Service/appsettings.json @@ -20,6 +20,12 @@ ] }, "Storage": { - "Directory": "ExportedYaml" + "Directory": "ExportedYaml", + "PersistDefaultValue": true + }, + "Sync": { + "SyncMode": "OneWay", + "SyncDirection": "KepwareToDiskAndSecondary", + "SyncThrottlingMs": 1000 } } \ No newline at end of file