diff --git a/src/Admin/ThingsGateway.DB/ThingsGateway.DB.csproj b/src/Admin/ThingsGateway.DB/ThingsGateway.DB.csproj index e7820139b..57193e42a 100644 --- a/src/Admin/ThingsGateway.DB/ThingsGateway.DB.csproj +++ b/src/Admin/ThingsGateway.DB/ThingsGateway.DB.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/Directory.Build.props b/src/Directory.Build.props index a53fd3fde..551def3e2 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,10 +1,10 @@ - 10.8.18 - 10.8.18 - 2.8.3 - 10.8.5 + 10.8.19 + 10.8.19 + 2.8.4 + 10.8.6 8.0.17 9.0.6 zh-Hans;en-US diff --git a/src/Gateway/ThingsGateway.Gateway.Application/Driver/Business/Cache/BusinessBaseWithCacheVariableModel.cs b/src/Gateway/ThingsGateway.Gateway.Application/Driver/Business/Cache/BusinessBaseWithCacheVariableModel.cs index e075cb161..6b5942b3b 100644 --- a/src/Gateway/ThingsGateway.Gateway.Application/Driver/Business/Cache/BusinessBaseWithCacheVariableModel.cs +++ b/src/Gateway/ThingsGateway.Gateway.Application/Driver/Business/Cache/BusinessBaseWithCacheVariableModel.cs @@ -439,7 +439,7 @@ public abstract class BusinessBaseWithCacheVariableModel : BusinessBas { if (cancellationToken.IsCancellationRequested) break; - var list = cacheDBItem.Value; + var list = cacheDBItem.Value; var data = list.ChunkBetter(_businessPropertyWithCache.SplitSize); foreach (var item in data) { diff --git a/src/Gateway/ThingsGateway.Gateway.Application/Driver/Collect/CollectBase.cs b/src/Gateway/ThingsGateway.Gateway.Application/Driver/Collect/CollectBase.cs index 12e741733..18135b6fb 100644 --- a/src/Gateway/ThingsGateway.Gateway.Application/Driver/Collect/CollectBase.cs +++ b/src/Gateway/ThingsGateway.Gateway.Application/Driver/Collect/CollectBase.cs @@ -344,6 +344,8 @@ public abstract class CollectBase : DriverBase, IRpcDriver var readErrorCount = 0; + await ReadWriteLock.ReaderLockAsync(cancellationToken).ConfigureAwait(false); + //if (LogMessage?.LogLevel <= TouchSocket.Core.LogLevel.Trace) // LogMessage?.Trace(string.Format("{0} - Collecting [{1} - {2}]", DeviceName, variableSourceRead?.RegisterAddress, variableSourceRead?.Length)); var readResult = await ReadSourceAsync(variableSourceRead, cancellationToken).ConfigureAwait(false); @@ -460,7 +462,40 @@ public abstract class CollectBase : DriverBase, IRpcDriver { throw new NotImplementedException(); } + protected async Task Check(Dictionary writeInfoLists, ConcurrentDictionary operResults, CancellationToken cancellationToken) + { + if (VariableSourceReadsEnable) + { + // 如果成功,每个变量都读取一次最新值,再次比较写入值 + var successfulWriteNames = operResults.Where(a => a.Value.IsSuccess).Select(a => a.Key).ToHashSet(); + var groups = writeInfoLists.Select(a => a.Key).Where(a => a.RpcWriteCheck && a.ProtectType != ProtectTypeEnum.WriteOnly && successfulWriteNames.Contains(a.Name) && a.VariableSource != null).GroupBy(a => a.VariableSource as VariableSourceRead).Where(a => a.Key != null).ToList(); + + await groups.ParallelForEachAsync(async (varRead, token) => + { + var result = await ReadSourceAsync(varRead.Key, token).ConfigureAwait(false); + if (result.IsSuccess) + { + foreach (var item in varRead) + { + if (!item.Value.Equals(writeInfoLists[item].ToObject(item.Value?.GetType()))) + { + // 如果写入值与读取值不同,则更新操作结果为失败 + operResults[item.Name] = new OperResult($"The write value is inconsistent with the read value, Write value: {writeInfoLists[item].ToObject(item.Value?.GetType())}, read value: {item.Value}"); + } + } + } + else + { + foreach (var item in varRead) + { + // 如果写入值与读取值不同,则更新操作结果为失败 + operResults[item.Name] = new OperResult($"Reading and rechecking resulted in an error: {result.ErrorMessage}", result.Exception); + } + } + }, cancellationToken).ConfigureAwait(false); + } + } #region 写入方法 diff --git a/src/Gateway/ThingsGateway.Gateway.Application/Driver/Collect/CollectFoundationBase.cs b/src/Gateway/ThingsGateway.Gateway.Application/Driver/Collect/CollectFoundationBase.cs index ff4aa46f5..6858c2b49 100644 --- a/src/Gateway/ThingsGateway.Gateway.Application/Driver/Collect/CollectFoundationBase.cs +++ b/src/Gateway/ThingsGateway.Gateway.Application/Driver/Collect/CollectFoundationBase.cs @@ -133,7 +133,6 @@ public abstract class CollectFoundationBase : CollectBase { try { - await ReadWriteLock.ReaderLockAsync(cancellationToken).ConfigureAwait(false); if (cancellationToken.IsCancellationRequested) return new(new OperationCanceledException()); @@ -202,6 +201,8 @@ public abstract class CollectFoundationBase : CollectBase } }, CollectProperties.MaxConcurrentCount, cancellationToken).ConfigureAwait(false); + await Check(writeInfoLists, operResults, cancellationToken).ConfigureAwait(false); + // 返回包含操作结果的字典 return new Dictionary(operResults); } @@ -210,4 +211,5 @@ public abstract class CollectFoundationBase : CollectBase } } + } diff --git a/src/Gateway/ThingsGateway.Gateway.Application/Entity/Variable.cs b/src/Gateway/ThingsGateway.Gateway.Application/Entity/Variable.cs index c069a39fa..3424ca560 100644 --- a/src/Gateway/ThingsGateway.Gateway.Application/Entity/Variable.cs +++ b/src/Gateway/ThingsGateway.Gateway.Application/Entity/Variable.cs @@ -66,6 +66,7 @@ public class Variable : BaseDataEntity, IValidatableObject private bool lAlarmEnable; private bool customAlarmEnable; private bool businessGroupUpdateTrigger = true; + private bool rpcWriteCheck; private object _value; private string name; @@ -148,6 +149,13 @@ public class Variable : BaseDataEntity, IValidatableObject [AutoGenerateColumn(Visible = true, Filterable = true, Sortable = true, Order = 1)] public virtual bool BusinessGroupUpdateTrigger { get => businessGroupUpdateTrigger; set => businessGroupUpdateTrigger = value; } + /// + /// 写入后再次读取检查值是否一致 + /// + [SugarColumn(ColumnDescription = "写入后再次读取检查值是否一致", IsNullable = true)] + [AutoGenerateColumn(Visible = true, Filterable = true, Sortable = true, Order = 1)] + public virtual bool RpcWriteCheck { get => rpcWriteCheck; set => rpcWriteCheck = value; } + /// /// 描述 /// diff --git a/src/Gateway/ThingsGateway.Gateway.Application/Locales/en-US.json b/src/Gateway/ThingsGateway.Gateway.Application/Locales/en-US.json index 8bdc8fac2..fab323a75 100644 --- a/src/Gateway/ThingsGateway.Gateway.Application/Locales/en-US.json +++ b/src/Gateway/ThingsGateway.Gateway.Application/Locales/en-US.json @@ -439,6 +439,7 @@ "BoolOpenRestrainExpressions": "BoolOpenRestrainExpressions", "BusinessGroup": "BusinessGroup", "BusinessGroupUpdateTrigger": "BusinessGroupUpdateTrigger", + "RpcWriteCheck": "RpcWriteCheck", "ClearVariable": "Clear Variable", "CollectGroup": "CollectGroup", "CopyVariable": "Copy Variable", diff --git a/src/Gateway/ThingsGateway.Gateway.Application/Locales/zh-CN.json b/src/Gateway/ThingsGateway.Gateway.Application/Locales/zh-CN.json index 7426da37f..967771435 100644 --- a/src/Gateway/ThingsGateway.Gateway.Application/Locales/zh-CN.json +++ b/src/Gateway/ThingsGateway.Gateway.Application/Locales/zh-CN.json @@ -440,6 +440,7 @@ "BoolOpenRestrainExpressions": "布尔开报警约束", "BusinessGroup": "业务组", "BusinessGroupUpdateTrigger": "分组上传触发", + "RpcWriteCheck": "写入后再次读取检查值是否一致", "ClearVariable": "清空变量", "CollectGroup": "采集组", "CopyVariable": "复制变量", diff --git a/src/Gateway/ThingsGateway.Gateway.Application/Services/RulesEngine/Node/Actuator/ExecuteScriptNode.cs b/src/Gateway/ThingsGateway.Gateway.Application/Services/RulesEngine/Node/Actuator/ExecuteScriptNode.cs index b323882cc..2b34e3d30 100644 --- a/src/Gateway/ThingsGateway.Gateway.Application/Services/RulesEngine/Node/Actuator/ExecuteScriptNode.cs +++ b/src/Gateway/ThingsGateway.Gateway.Application/Services/RulesEngine/Node/Actuator/ExecuteScriptNode.cs @@ -104,12 +104,21 @@ public class ExecuteScriptNode : TextNode, IActuatorNode, IExexcuteExpressionsBa } } - Task IActuatorNode.ExecuteAsync(NodeInput input, CancellationToken cancellationToken) + Task> IActuatorNode.ExecuteAsync(NodeInput input, CancellationToken cancellationToken) { - Logger?.Trace($"Execute script"); - var exexcuteExpressions = CSharpScriptEngineExtension.Do(Text); - exexcuteExpressions.Logger = Logger; - return exexcuteExpressions.ExecuteAsync(input, cancellationToken); + try + { + Logger?.Trace($"Execute script"); + var exexcuteExpressions = CSharpScriptEngineExtension.Do(Text); + exexcuteExpressions.Logger = Logger; + return exexcuteExpressions.ExecuteAsync(input, cancellationToken); + } + catch (Exception ex) + { + Logger?.LogWarning(ex); + return Task.FromResult(new OperResult(ex)); + } + } diff --git a/src/Gateway/ThingsGateway.Gateway.Application/Services/RulesEngine/Node/Actuator/VariableRpcNode.cs b/src/Gateway/ThingsGateway.Gateway.Application/Services/RulesEngine/Node/Actuator/VariableRpcNode.cs index 0870b6df7..b8132ef9e 100644 --- a/src/Gateway/ThingsGateway.Gateway.Application/Services/RulesEngine/Node/Actuator/VariableRpcNode.cs +++ b/src/Gateway/ThingsGateway.Gateway.Application/Services/RulesEngine/Node/Actuator/VariableRpcNode.cs @@ -12,22 +12,32 @@ public class VariableRpcNode : VariableNode, IActuatorNode public VariableRpcNode(string id, Point? position = null) : base(id, position) { Title = "VariableRpcNode"; } - async Task IActuatorNode.ExecuteAsync(NodeInput input, CancellationToken cancellationToken) + async Task> IActuatorNode.ExecuteAsync(NodeInput input, CancellationToken cancellationToken) { - if ((!DeviceText.IsNullOrWhiteSpace()) && GlobalData.ReadOnlyDevices.TryGetValue(DeviceText, out var device)) + try { - if (device.ReadOnlyVariableRuntimes.TryGetValue(Text, out var value)) + + if ((!DeviceText.IsNullOrWhiteSpace()) && GlobalData.ReadOnlyDevices.TryGetValue(DeviceText, out var device)) { - var data = await value.RpcAsync(input.JToken.ToString(), $"RulesEngine: {RulesEngineName}", cancellationToken).ConfigureAwait(false); - if (data.IsSuccess) - Logger?.Trace($" VariableRpcNode - VariableName {Text} : execute success"); - else - Logger?.Warning($" VariableRpcNode - VariableName {Text} : {data.ErrorMessage}"); - return new NodeOutput() { Value = data }; + if (device.ReadOnlyVariableRuntimes.TryGetValue(Text, out var value)) + { + var data = await value.RpcAsync(input.JToken.ToString(), $"RulesEngine: {RulesEngineName}", cancellationToken).ConfigureAwait(false); + if (data.IsSuccess) + Logger?.Trace($" VariableRpcNode - VariableName {Text} : execute success"); + else + Logger?.Warning($" VariableRpcNode - VariableName {Text} : {data.ErrorMessage}"); + return new OperResult() { Content = new NodeOutput() { Value = data } }; + } } + Logger?.Warning($" VariableRpcNode - VariableName {Text} : not found"); + + return new OperResult() { Content = new NodeOutput() { } }; + } + catch (Exception ex) + { + Logger?.LogWarning(ex); + return new OperResult(ex); } - Logger?.Warning($" VariableRpcNode - VariableName {Text} : not found"); - return new NodeOutput() { }; } diff --git a/src/Gateway/ThingsGateway.Gateway.Application/Services/RulesEngine/Node/Expression/DataNode.cs b/src/Gateway/ThingsGateway.Gateway.Application/Services/RulesEngine/Node/Expression/DataNode.cs index a2a000637..61501bb48 100644 --- a/src/Gateway/ThingsGateway.Gateway.Application/Services/RulesEngine/Node/Expression/DataNode.cs +++ b/src/Gateway/ThingsGateway.Gateway.Application/Services/RulesEngine/Node/Expression/DataNode.cs @@ -16,12 +16,21 @@ public class DataNode : TextNode, IExpressionNode } - Task IExpressionNode.ExecuteAsync(NodeInput input, CancellationToken cancellationToken) + Task> IExpressionNode.ExecuteAsync(NodeInput input, CancellationToken cancellationToken) { - var value = Text.GetExpressionsResult(input.Value, Logger); - NodeOutput nodeOutput = new(); - nodeOutput.Value = value; - Logger?.Trace($"Data result: {nodeOutput.JToken?.ToString(Newtonsoft.Json.Formatting.Indented)}"); - return Task.FromResult(nodeOutput); + try + { + var value = Text.GetExpressionsResult(input.Value, Logger); + NodeOutput nodeOutput = new(); + nodeOutput.Value = value; + Logger?.Trace($"Data result: {nodeOutput.JToken?.ToString(Newtonsoft.Json.Formatting.Indented)}"); + return Task.FromResult(new OperResult() { Content = nodeOutput }); + } + catch (Exception ex) + { + Logger?.LogWarning(ex); + return Task.FromResult(new OperResult(ex)); + } + } } diff --git a/src/Gateway/ThingsGateway.Gateway.Application/Services/RulesEngine/Node/Expression/DelayNode.cs b/src/Gateway/ThingsGateway.Gateway.Application/Services/RulesEngine/Node/Expression/DelayNode.cs index 1e55ff37c..8192c9861 100644 --- a/src/Gateway/ThingsGateway.Gateway.Application/Services/RulesEngine/Node/Expression/DelayNode.cs +++ b/src/Gateway/ThingsGateway.Gateway.Application/Services/RulesEngine/Node/Expression/DelayNode.cs @@ -10,11 +10,20 @@ public class DelayNode : NumberNode, IExpressionNode { public DelayNode(string id, Point? position = null) : base(id, position) { Title = "DelayNode"; Placeholder = "DelayNode.Placeholder"; } - async Task IExpressionNode.ExecuteAsync(NodeInput input, CancellationToken cancellationToken) + async Task> IExpressionNode.ExecuteAsync(NodeInput input, CancellationToken cancellationToken) { - Logger?.Trace($"Delay {Number} ms"); - await Task.Delay(Number ?? 0, cancellationToken).ConfigureAwait(false); - return new NodeOutput(); + try + { + Logger?.Trace($"Delay {Number} ms"); + await Task.Delay(Number ?? 0, cancellationToken).ConfigureAwait(false); + return new OperResult() { Content = new NodeOutput() }; + } + catch (Exception ex) + { + Logger?.LogWarning(ex); + return new OperResult(ex); + } + } } diff --git a/src/Gateway/ThingsGateway.Gateway.Application/Services/RulesEngine/Node/INode.cs b/src/Gateway/ThingsGateway.Gateway.Application/Services/RulesEngine/Node/INode.cs index 4557730c6..76715f923 100644 --- a/src/Gateway/ThingsGateway.Gateway.Application/Services/RulesEngine/Node/INode.cs +++ b/src/Gateway/ThingsGateway.Gateway.Application/Services/RulesEngine/Node/INode.cs @@ -13,11 +13,11 @@ public interface IConditionNode : INode public interface IExpressionNode : INode { - public Task ExecuteAsync(NodeInput input, CancellationToken cancellationToken); + public Task> ExecuteAsync(NodeInput input, CancellationToken cancellationToken); } public interface IActuatorNode : INode { - public Task ExecuteAsync(NodeInput input, CancellationToken cancellationToken); + public Task> ExecuteAsync(NodeInput input, CancellationToken cancellationToken); } public interface ITriggerNode : INode @@ -30,6 +30,6 @@ public interface IExexcuteExpressionsBase public interface IExexcuteExpressions : IExexcuteExpressionsBase { public TouchSocket.Core.ILog Logger { get; set; } - Task ExecuteAsync(NodeInput input, CancellationToken cancellationToken); + Task> ExecuteAsync(NodeInput input, CancellationToken cancellationToken); } diff --git a/src/Gateway/ThingsGateway.Gateway.Application/Services/RulesEngine/Services/RulesEngineHostedService.cs b/src/Gateway/ThingsGateway.Gateway.Application/Services/RulesEngine/Services/RulesEngineHostedService.cs index a7aa7f761..c8da06aed 100644 --- a/src/Gateway/ThingsGateway.Gateway.Application/Services/RulesEngine/Services/RulesEngineHostedService.cs +++ b/src/Gateway/ThingsGateway.Gateway.Application/Services/RulesEngine/Services/RulesEngineHostedService.cs @@ -65,7 +65,7 @@ internal sealed class RulesEngineHostedService : BackgroundService, IRulesEngine if (rules.Status) { var data = Init(rules); - await Start(data.rulesLog, data.blazorDiagram, TokenSource.Token).ConfigureAwait(false); + Start(data.rulesLog, data.blazorDiagram, TokenSource.Token); dispatchService.Dispatch(null); } @@ -115,7 +115,7 @@ internal sealed class RulesEngineHostedService : BackgroundService, IRulesEngine return result; } - private static Task Start(RulesLog rulesLog, DefaultDiagram item, CancellationToken cancellationToken) + private static void Start(RulesLog rulesLog, DefaultDiagram item, CancellationToken cancellationToken) { rulesLog.Log.Trace("Start"); var startNodes = item.Nodes.Where(a => a is IStartNode); @@ -123,7 +123,6 @@ internal sealed class RulesEngineHostedService : BackgroundService, IRulesEngine { _ = Analysis((link.Target.Model as PortModel)?.Parent, new NodeInput(), rulesLog, cancellationToken); } - return Task.CompletedTask; } private static async Task Analysis(NodeModel targetNode, NodeInput input, RulesLog rulesLog, CancellationToken cancellationToken) @@ -152,17 +151,24 @@ internal sealed class RulesEngineHostedService : BackgroundService, IRulesEngine else if (targetNode is IExpressionNode expressionNode) { var nodeOutput = await expressionNode.ExecuteAsync(input, cancellationToken).ConfigureAwait(false); - foreach (var link in targetNode.PortLinks.Where(a => ((a.Target.Model as PortModel)?.Parent) != targetNode)) + if (nodeOutput.IsSuccess) { - await Analysis((link.Target.Model as PortModel)?.Parent, new NodeInput() { Value = nodeOutput.Value, }, rulesLog, cancellationToken).ConfigureAwait(false); + foreach (var link in targetNode.PortLinks.Where(a => ((a.Target.Model as PortModel)?.Parent) != targetNode)) + { + await Analysis((link.Target.Model as PortModel)?.Parent, new NodeInput() { Value = nodeOutput.Content.Value, }, rulesLog, cancellationToken).ConfigureAwait(false); + } } + } else if (targetNode is IActuatorNode actuatorNode) { var nodeOutput = await actuatorNode.ExecuteAsync(input, cancellationToken).ConfigureAwait(false); - foreach (var link in targetNode.PortLinks.Where(a => ((a.Target.Model as PortModel)?.Parent) != targetNode)) + if (nodeOutput.IsSuccess) { - await Analysis((link.Target.Model as PortModel)?.Parent, new NodeInput() { Value = nodeOutput.Value }, rulesLog, cancellationToken).ConfigureAwait(false); + foreach (var link in targetNode.PortLinks.Where(a => ((a.Target.Model as PortModel)?.Parent) != targetNode)) + { + await Analysis((link.Target.Model as PortModel)?.Parent, new NodeInput() { Value = nodeOutput.Content.Value }, rulesLog, cancellationToken).ConfigureAwait(false); + } } } else if (targetNode is ITriggerNode triggerNode) @@ -200,11 +206,10 @@ internal sealed class RulesEngineHostedService : BackgroundService, IRulesEngine foreach (var rules in Rules.Where(a => a.Status)) { var item = Init(rules); - await Start(item.rulesLog, item.blazorDiagram, cancellationToken).ConfigureAwait(false); + Start(item.rulesLog, item.blazorDiagram, cancellationToken); } dispatchService.Dispatch(null); - _ = Task.Factory.StartNew(async (state) => { if (state is not Dictionary diagrams) diff --git a/src/Gateway/ThingsGateway.Gateway.Razor/Pages/GatewayMonitorPage/Variable/VariableEditComponent.razor b/src/Gateway/ThingsGateway.Gateway.Razor/Pages/GatewayMonitorPage/Variable/VariableEditComponent.razor index d96e3575a..38fba7ae5 100644 --- a/src/Gateway/ThingsGateway.Gateway.Razor/Pages/GatewayMonitorPage/Variable/VariableEditComponent.razor +++ b/src/Gateway/ThingsGateway.Gateway.Razor/Pages/GatewayMonitorPage/Variable/VariableEditComponent.razor @@ -28,226 +28,227 @@ RenderFragment renderFragment => @ - + - + - - - -
-
@GatewayLocalizer["BasicInformation"]
-
-
-
+ + + +
+
@GatewayLocalizer["BasicInformation"]
+
+
+
- + - - - + + + + - - - - + + + + - - -
- -
-
-
+ + +
+ +
+
+
- + - - -
-
@GatewayLocalizer["Connection"]
-
-
-
+ + +
+
@GatewayLocalizer["Connection"]
+
+
+
- - -
- - + + +
+
+
- - + + - - -
- +
+
+
- - -
- - @if(GlobalData.ReadOnlyIdDevices.TryGetValue(value.DeviceId,out var device)&&device.Driver is CollectBase collectBase) - { - - } - else + + +
+ + @if(GlobalData.ReadOnlyIdDevices.TryGetValue(value.DeviceId,out var device)&&device.Driver is CollectBase collectBase) + { + + } + else { } - - -
-
-
- + +
+
+
+
+ - - + + - - -
-
@GatewayLocalizer["Remark"]
-
-
-
- - - - - + + +
+
@GatewayLocalizer["Remark"]
+
+
+
+ + + + + -
+
-
-
+ +
- - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @if(!BatchEditEnable) - { + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +@if (!BatchEditEnable) +{
-
-
+ await RefreshBusinessPropertyClickAsync(ChoiceBusinessDeviceId); + foreach (var item in Model.VariablePropertyModels) + { + if (item.Value != null) + { + item.Value.ValidateForm = null; + } + } + }"> + @GatewayLocalizer["RefreshBusinessProperty"] +
- @if (Model.VariablePropertyModels != null) +
+ @if (Model.VariablePropertyModels != null) { @foreach (var a in Model.VariablePropertyModels) - { + { - var item = a; + var item = a; - var custom = VariablePropertyRenderFragments.TryGetValue(item.Key, out var renderFragment); + var custom = VariablePropertyRenderFragments.TryGetValue(item.Key, out var renderFragment); - if (!custom) - { - var has = VariablePropertyEditors.TryGetValue(item.Key, out var items); - if (has) - { + if (!custom) + { + var has = VariablePropertyEditors.TryGetValue(item.Key, out var items); + if (has) + { - - - @{ - GlobalData.ReadOnlyIdDevices.TryGetValue(item.Key, out var items); + + + @{ + GlobalData.ReadOnlyIdDevices.TryGetValue(item.Key, out var items); + } +
+ @($"{items.Name} - {PluginServiceUtil.GetFileNameAndTypeName(items?.PluginName).TypeName}") +
+ +