mirror of
https://gitee.com/ThingsGateway/ThingsGateway.git
synced 2025-10-20 18:51:28 +08:00
feat: 更新依赖包
This commit is contained in:
@@ -21,15 +21,19 @@
|
||||
<PackageReference Include="BootstrapBlazor.MiniExcel" Version="1.38.0" />
|
||||
<PackageReference Include="UAParser" Version="3.1.47" />
|
||||
<PackageReference Include="Rougamo.Fody" Version="5.0.0" />
|
||||
<PackageReference Include="MailKit" Version="4.9.0" />
|
||||
<PackageReference Include="MailKit" Version="4.10.0" />
|
||||
<PackageReference Include="SqlSugarCore" Version="5.1.4.175" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' ">
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.*" />
|
||||
<PackageReference Include="System.Formats.Asn1" Version="8.*" />
|
||||
<PackageReference Include="System.Threading.RateLimiting" Version="8.*" />
|
||||
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'net9.0' ">
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.*" />
|
||||
|
||||
<PackageReference Include="System.Formats.Asn1" Version="9.*" />
|
||||
<PackageReference Include="System.Threading.RateLimiting" Version="9.*" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Remove="SeedData\Admin\*.json" />
|
||||
|
@@ -41,13 +41,20 @@
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<!--安装服务守护-->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.*" />
|
||||
</ItemGroup>
|
||||
<!--安装服务守护-->
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' ">
|
||||
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="8.*" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.*" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'net9.0' ">
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="9.*" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.*" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
<None Update="Dockerfile">
|
||||
|
@@ -38,23 +38,27 @@
|
||||
<PackageReference Include="Ben.Demystifier" Version="0.4.1" />
|
||||
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
|
||||
<PackageReference Include="Mapster" Version="7.4.0" />
|
||||
<PackageReference Include="System.Text.Json" Version="9.*" />
|
||||
<PackageReference Include="System.Reflection.MetadataLoadContext" Version="9.*" />
|
||||
<PackageReference Include="MiniProfiler.AspNetCore.Mvc" Version="4.5.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="9.*" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" />
|
||||
<PackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="8.3.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' ">
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.*" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.*" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="8.*" />
|
||||
<PackageReference Include="System.Reflection.MetadataLoadContext" Version="8.*" />
|
||||
<PackageReference Include="System.Text.Json" Version="8.*" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'net9.0' ">
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="9.*" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.*" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="9.*" />
|
||||
<PackageReference Include="System.Reflection.MetadataLoadContext" Version="9.*" />
|
||||
<PackageReference Include="System.Text.Json" Version="9.*" />
|
||||
|
||||
</ItemGroup>
|
||||
|
||||
|
@@ -7,7 +7,7 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BootstrapBlazor.FontAwesome" Version="9.0.2" />
|
||||
<PackageReference Include="BootstrapBlazor" Version="9.2.8" />
|
||||
<PackageReference Include="BootstrapBlazor" Version="9.3.0" />
|
||||
<PackageReference Include="Yitter.IdGenerator" Version="1.0.14" />
|
||||
</ItemGroup>
|
||||
|
||||
|
@@ -1,8 +1,8 @@
|
||||
<Project>
|
||||
|
||||
<PropertyGroup>
|
||||
<PluginVersion>10.0.0.19</PluginVersion>
|
||||
<ProPluginVersion>10.0.0.19</ProPluginVersion>
|
||||
<PluginVersion>10.0.0.20</PluginVersion>
|
||||
<ProPluginVersion>10.0.0.20</ProPluginVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
@@ -181,7 +181,7 @@ public partial class LogConsole : IDisposable
|
||||
string pattern = @"[\\/:*?""<>|]";
|
||||
// 使用正则表达式将不符合规则的部分替换为下划线
|
||||
string sanitizedFileName = Regex.Replace(HeaderText, pattern, "_");
|
||||
await DownloadService.DownloadFromStreamAsync(sanitizedFileName + DateTime.Now.ToFileDateTimeFormat(), memoryStream);
|
||||
await DownloadService.DownloadFromStreamAsync($"{sanitizedFileName}{DateTime.Now.ToFileDateTimeFormat()}.txt", memoryStream);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@@ -10,8 +10,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Localization.Abstractions" Version="9.*" />
|
||||
<PackageReference Include="TouchSocket" Version="3.0.12" />
|
||||
<PackageReference Include="TouchSocket.SerialPorts" Version="3.0.12" />
|
||||
<PackageReference Include="TouchSocket" Version="3.0.13" />
|
||||
<PackageReference Include="TouchSocket.SerialPorts" Version="3.0.13" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@@ -101,7 +101,7 @@ public class RuntimeInfoController : ControllerBase
|
||||
if (GlobalData.ReadOnlyRealAlarmVariables.TryGetValue(variableName, out var variable))
|
||||
{
|
||||
await GlobalData.SysUserService.CheckApiDataScopeAsync(variable.CreateOrgId, variable.CreateUserId).ConfigureAwait(false);
|
||||
GlobalData.AlarmHostedService.ConfirmAlarm(variable);
|
||||
GlobalData.AlarmHostedService.ConfirmAlarm(variableName);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -94,7 +94,7 @@ public abstract class BusinessBase : DriverBase
|
||||
|
||||
|
||||
// 获取当前设备需要采集的设备
|
||||
CollectDevices = GlobalData.GetEnableDevices().Where(a => CurrentDevice.VariableRuntimes.Select(b => b.Value.DeviceId).ToHashSet().Contains(a.Key)).ToDictionary(a => a.Key, a => a.Value);
|
||||
CollectDevices = GlobalData.GetEnableDevices().Where(a => variableRuntimes.Select(b => b.Value.DeviceId).ToHashSet().Contains(a.Key)).ToDictionary(a => a.Key, a => a.Value);
|
||||
|
||||
VariableRuntimes = variableRuntimes.ToDictionary();
|
||||
}
|
||||
|
@@ -62,7 +62,7 @@ public abstract class BusinessBaseWithCacheIntervalAlarmModel<VarModel, DevModel
|
||||
{
|
||||
VariableRuntimes = new(GlobalData.GetEnableVariables());
|
||||
|
||||
CollectDevices = GlobalData.GetEnableDevices().ToDictionary();
|
||||
CollectDevices = GlobalData.GetEnableDevices().Where(a => a.Value.IsCollect == true).ToDictionary();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@@ -67,7 +67,7 @@ public abstract class BusinessBaseWithCacheIntervalDeviceModel<VarModel, DevMode
|
||||
if (_businessPropertyWithCacheInterval.IsAllVariable)
|
||||
{
|
||||
VariableRuntimes = new(GlobalData.GetEnableVariables());
|
||||
CollectDevices = GlobalData.GetEnableDevices().ToDictionary();
|
||||
CollectDevices = GlobalData.GetEnableDevices().Where(a => a.Value.IsCollect == true).ToDictionary();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@@ -55,7 +55,7 @@ public abstract class BusinessBaseWithCacheIntervalVariableModel<T> : BusinessBa
|
||||
if (_businessPropertyWithCacheInterval.IsAllVariable)
|
||||
{
|
||||
VariableRuntimes = new(GlobalData.GetEnableVariables());
|
||||
CollectDevices = GlobalData.GetEnableDevices().ToDictionary();
|
||||
CollectDevices = GlobalData.GetEnableDevices().Where(a => a.Value.IsCollect == true).ToDictionary();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@@ -52,7 +52,7 @@ public abstract class CollectBase : DriverBase
|
||||
public override void AfterVariablesChanged()
|
||||
{
|
||||
var currentDevice = CurrentDevice;
|
||||
VariableRuntimes = currentDevice.VariableRuntimes.Where(a => a.Value.Enable).ToDictionary();
|
||||
VariableRuntimes = currentDevice.VariableRuntimes.Where(a => a.Value.Enable).ToDictionary(a => a.Value.Name, a => a.Value);
|
||||
|
||||
//预热脚本,加速编译
|
||||
VariableRuntimes.Where(a => !string.IsNullOrWhiteSpace(a.Value.ReadExpressions))
|
||||
|
@@ -387,7 +387,7 @@ public static class GlobalData
|
||||
/// <summary>
|
||||
/// 内部使用的报警配置变量字典
|
||||
/// </summary>
|
||||
internal static ConcurrentDictionary<string, VariableRuntime> AlarmEnableVariables { get; } = new();
|
||||
internal static ConcurrentDictionary<long, VariableRuntime> AlarmEnableVariables { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 内部使用的报警配置变量字典
|
||||
|
@@ -128,7 +128,7 @@ public class DeviceRuntime : Device, IDisposable
|
||||
[Newtonsoft.Json.JsonIgnore]
|
||||
[AdaptIgnore]
|
||||
[AutoGenerateColumn(Ignore = true)]
|
||||
public IReadOnlyDictionary<string, VariableRuntime>? ReadVariableRuntimes => VariableRuntimes;
|
||||
public IReadOnlyDictionary<long, VariableRuntime>? ReadVariableRuntimes => VariableRuntimes;
|
||||
|
||||
/// <summary>
|
||||
/// 设备变量
|
||||
@@ -137,7 +137,7 @@ public class DeviceRuntime : Device, IDisposable
|
||||
[Newtonsoft.Json.JsonIgnore]
|
||||
[AdaptIgnore]
|
||||
[AutoGenerateColumn(Ignore = true)]
|
||||
internal ConcurrentDictionary<string, VariableRuntime>? VariableRuntimes { get; set; } = new(Environment.ProcessorCount, 1000);
|
||||
internal ConcurrentDictionary<long, VariableRuntime>? VariableRuntimes { get; set; } = new(Environment.ProcessorCount, 1000);
|
||||
|
||||
#region 采集
|
||||
/// <summary>
|
||||
|
@@ -316,31 +316,31 @@ public class VariableRuntime : Variable, IVariable, IDisposable
|
||||
#endregion 报警
|
||||
public void Init(DeviceRuntime deviceRuntime)
|
||||
{
|
||||
DeviceRuntime?.VariableRuntimes?.TryRemove(Name, out _);
|
||||
DeviceRuntime?.VariableRuntimes?.TryRemove(Id, out _);
|
||||
|
||||
DeviceRuntime = deviceRuntime;
|
||||
|
||||
DeviceRuntime.VariableRuntimes.TryAdd(Name, this);
|
||||
DeviceRuntime.VariableRuntimes.TryAdd(Id, this);
|
||||
GlobalData.IdVariables.TryRemove(Id, out _);
|
||||
GlobalData.IdVariables.TryAdd(Id, this);
|
||||
GlobalData.Variables.TryRemove(Name, out _);
|
||||
GlobalData.Variables.TryAdd(Name, this);
|
||||
if (AlarmEnable)
|
||||
{
|
||||
GlobalData.AlarmEnableVariables.TryRemove(Name, out _);
|
||||
GlobalData.AlarmEnableVariables.TryAdd(Name, this);
|
||||
GlobalData.AlarmEnableVariables.TryRemove(Id, out _);
|
||||
GlobalData.AlarmEnableVariables.TryAdd(Id, this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
DeviceRuntime?.VariableRuntimes?.TryRemove(Name, out _);
|
||||
DeviceRuntime?.VariableRuntimes?.TryRemove(Id, out _);
|
||||
|
||||
GlobalData.IdVariables.TryRemove(Id, out _);
|
||||
GlobalData.Variables.TryRemove(Name, out _);
|
||||
|
||||
GlobalData.AlarmEnableVariables.TryRemove(Name, out _);
|
||||
GlobalData.AlarmEnableVariables.TryRemove(Id, out _);
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
@@ -376,12 +376,15 @@ internal sealed class AlarmHostedService : BackgroundService, IAlarmHostedServic
|
||||
|
||||
}
|
||||
|
||||
public void ConfirmAlarm(VariableRuntime item)
|
||||
public void ConfirmAlarm(string variableName)
|
||||
{
|
||||
// 如果是确认报警事件
|
||||
item.EventType = EventTypeEnum.Confirm;
|
||||
item.EventTime = DateTime.Now;
|
||||
GlobalData.AlarmChange(item.Adapt<AlarmVariable>());
|
||||
if (GlobalData.RealAlarmVariables.TryGetValue(variableName, out var variableRuntime))
|
||||
{
|
||||
variableRuntime.EventType = EventTypeEnum.Confirm;
|
||||
variableRuntime.EventTime = DateTime.Now;
|
||||
GlobalData.AlarmChange(variableRuntime.Adapt<AlarmVariable>());
|
||||
}
|
||||
}
|
||||
|
||||
#endregion 核心实现
|
||||
@@ -425,7 +428,7 @@ internal sealed class AlarmHostedService : BackgroundService, IAlarmHostedServic
|
||||
|
||||
foreach (var item in GlobalData.RealAlarmVariables)
|
||||
{
|
||||
if (!GlobalData.AlarmEnableVariables.ContainsKey(item.Key))
|
||||
if (!GlobalData.AlarmEnableVariables.ContainsKey(item.Value.Id))
|
||||
{
|
||||
if (GlobalData.RealAlarmVariables.TryRemove(item.Key, out var oldAlarm))
|
||||
{
|
||||
|
@@ -17,6 +17,5 @@ public interface IAlarmHostedService : IHostedService
|
||||
/// <summary>
|
||||
/// 确认报警
|
||||
/// </summary>
|
||||
/// <param name="item"></param>
|
||||
void ConfirmAlarm(VariableRuntime item);
|
||||
void ConfirmAlarm(string variableName);
|
||||
}
|
||||
|
@@ -8,8 +8,8 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SqlSugar.TDengineCore" Version="4.18.1" />
|
||||
<PackageReference Include="Rougamo.Fody" Version="5.0.0" />
|
||||
<PackageReference Include="TouchSocket.Dmtp" Version="3.0.12" />
|
||||
<PackageReference Include="TouchSocket.WebApi.Swagger" Version="3.0.12" />
|
||||
<PackageReference Include="TouchSocket.Dmtp" Version="3.0.13" />
|
||||
<PackageReference Include="TouchSocket.WebApi.Swagger" Version="3.0.13" />
|
||||
|
||||
</ItemGroup>
|
||||
|
||||
|
@@ -466,7 +466,7 @@ public partial class ChannelDeviceTree : IDisposable
|
||||
{
|
||||
var op = new DialogOption()
|
||||
{
|
||||
IsScrolling = false,
|
||||
IsScrolling = true,
|
||||
ShowMaximizeButton = true,
|
||||
Size = Size.ExtraLarge,
|
||||
Title = item.Text,
|
||||
@@ -515,7 +515,7 @@ public partial class ChannelDeviceTree : IDisposable
|
||||
|
||||
var op = new DialogOption()
|
||||
{
|
||||
IsScrolling = false,
|
||||
IsScrolling = true,
|
||||
ShowMaximizeButton = true,
|
||||
Size = Size.ExtraLarge,
|
||||
Title = item.Text,
|
||||
|
@@ -147,7 +147,7 @@ public partial class VariableEditComponent
|
||||
|
||||
var op = new DialogOption()
|
||||
{
|
||||
IsScrolling = false,
|
||||
IsScrolling = true,
|
||||
ShowMaximizeButton = true,
|
||||
Size = Size.ExtraLarge,
|
||||
Title = DeviceLocalizer["SaveDevice"],
|
||||
|
@@ -130,7 +130,7 @@ public partial class VariableRuntimeInfo : IDisposable
|
||||
{
|
||||
var op = new DialogOption()
|
||||
{
|
||||
IsScrolling = false,
|
||||
IsScrolling = true,
|
||||
ShowMaximizeButton = true,
|
||||
Size = Size.ExtraLarge,
|
||||
Title = RazorLocalizer["BatchEdit"],
|
||||
|
@@ -6,8 +6,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "other", "other", "{0B748352
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.editorconfig = .editorconfig
|
||||
Directory.Build.props = Directory.Build.props
|
||||
..\git_pull.bat = ..\git_pull.bat
|
||||
PluginVersion.props = PluginVersion.props
|
||||
Version.props = Version.props
|
||||
EndProjectSection
|
||||
EndProject
|
||||
|
@@ -31,11 +31,15 @@
|
||||
"Url": "/OpcUaMaster",
|
||||
"Text": "OpcUaMaster"
|
||||
},
|
||||
{
|
||||
"Url": "/OpcUaMaster105",
|
||||
"Text": "OpcUaMaster105"
|
||||
},
|
||||
{
|
||||
"Url": "/OpcDaMaster",
|
||||
"Text": "OpcDaMaster"
|
||||
}
|
||||
|
||||
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@@ -31,6 +31,10 @@
|
||||
"Url": "/OpcUaMaster",
|
||||
"Text": "OpcUaMaster"
|
||||
},
|
||||
{
|
||||
"Url": "/OpcUaMaster105",
|
||||
"Text": "OpcUaMaster105"
|
||||
},
|
||||
{
|
||||
"Url": "/OpcDaMaster",
|
||||
"Text": "OpcDaMaster"
|
||||
|
@@ -12,8 +12,6 @@ namespace ThingsGateway.Foundation.OpcUa;
|
||||
|
||||
internal static class CollectionExtension
|
||||
{
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 将项目列表分解为特定大小的块
|
||||
/// </summary>
|
||||
|
16
src/Plugin/ThingsGateway.Foundation.OpcUa105/GlobalUsings.cs
Normal file
16
src/Plugin/ThingsGateway.Foundation.OpcUa105/GlobalUsings.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
|
||||
// 此代码版权(除特别声明外的代码)归作者本人Diego所有
|
||||
// 源代码使用协议遵循本仓库的开源协议及附加协议
|
||||
// Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
|
||||
// Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
|
||||
// 使用文档:https://thingsgateway.cn/
|
||||
// QQ群:605534569
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
global using Newtonsoft.Json.Linq;
|
||||
|
||||
global using Opc.Ua;
|
||||
global using Opc.Ua.Client;
|
||||
global using Opc.Ua.Client.ComplexTypes;
|
||||
global using Opc.Ua.Configuration;
|
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"ThingsGateway.Foundation.OpcUa105.OpcUaProperty": {
|
||||
"OpcUrl": "OpcUrl",
|
||||
"UserName": "UserName",
|
||||
"Password": "Password",
|
||||
"CheckDomain": "CheckDomain",
|
||||
"UpdateRate": "UpdateRate",
|
||||
"ActiveSubscribe": "ActiveSubscribe",
|
||||
"GroupSize": "GroupSize",
|
||||
"DeadBand": "DeadBand",
|
||||
"KeepAliveInterval": "KeepAliveInterval(ms)",
|
||||
"UseSecurity": "IsUseSecurity",
|
||||
"LoadType": "LoadType",
|
||||
"AutoAcceptUntrustedCertificates": "AutoAcceptUntrustedCertificates",
|
||||
"ExportC": "ExportCertificate",
|
||||
|
||||
"Connect": "Connect",
|
||||
"Disconnect": "Disconnect",
|
||||
"Add": "Add",
|
||||
"Remove": "Remove",
|
||||
"Read": "Read",
|
||||
"Write": "Write",
|
||||
"ShowImport": "View OPC space",
|
||||
"RegisterAddress": "RegisterAddress",
|
||||
"WriteValue": "WriteValue",
|
||||
|
||||
"NoVariablesAvailable": "No variables available",
|
||||
"Success": "Success",
|
||||
"Close": "Close",
|
||||
"Save": "Import to system",
|
||||
"Export": "Export",
|
||||
"ShowSubvariable": "ShowSubvariable"
|
||||
},
|
||||
|
||||
"ThingsGateway.Foundation.OpcUa105.OpcUaResource": {
|
||||
}
|
||||
}
|
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"ThingsGateway.Foundation.OpcUa105.OpcUaProperty": {
|
||||
"OpcUrl": "OpcUrl",
|
||||
"UserName": "登录账号",
|
||||
"Password": "登录密码",
|
||||
"CheckDomain": "检查域",
|
||||
"UpdateRate": "推送间隔",
|
||||
"ActiveSubscribe": "订阅",
|
||||
"GroupSize": "分组大小",
|
||||
"DeadBand": "死区",
|
||||
"KeepAliveInterval": "心跳间隔(ms)",
|
||||
"UseSecurity": "安全策略",
|
||||
"LoadType": "加载服务端数据类型",
|
||||
"AutoAcceptUntrustedCertificates": "自动接受不受信任的证书",
|
||||
"ExportC": "导出证书",
|
||||
"Connect": "连接",
|
||||
"Disconnect": "断开",
|
||||
"Add": "添加",
|
||||
"Remove": "移除",
|
||||
"Read": "读取",
|
||||
"Write": "写入",
|
||||
"ShowImport": "查看OPC空间",
|
||||
"RegisterAddress": "地址",
|
||||
"WriteValue": "写入值",
|
||||
|
||||
"NoVariablesAvailable": "无可用变量",
|
||||
"Success": "成功",
|
||||
"Close": "关闭",
|
||||
"Save": "导入到系统",
|
||||
"Export": "导出",
|
||||
"ShowSubvariable": "显示变量子节点"
|
||||
},
|
||||
|
||||
"ThingsGateway.Foundation.OpcUa105.OpcUaResource": {
|
||||
}
|
||||
}
|
@@ -0,0 +1,37 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
|
||||
// 此代码版权(除特别声明外的代码)归作者本人Diego所有
|
||||
// 源代码使用协议遵循本仓库的开源协议及附加协议
|
||||
// Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
|
||||
// Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
|
||||
// 使用文档:https://thingsgateway.cn/
|
||||
// QQ群:605534569
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace ThingsGateway.Foundation.OpcUa105;
|
||||
|
||||
/// <summary>
|
||||
/// 读取属性过程中用于描述的
|
||||
/// </summary>
|
||||
public class OPCNodeAttribute
|
||||
{
|
||||
/// <summary>
|
||||
/// 属性的名称
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 操作结果状态描述
|
||||
/// </summary>
|
||||
public StatusCode StatusCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 属性的类型描述
|
||||
/// </summary>
|
||||
public string Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 属性的值,如果读取错误,返回文本描述
|
||||
/// </summary>
|
||||
public object Value { get; set; }
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,80 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
|
||||
// 此代码版权(除特别声明外的代码)归作者本人Diego所有
|
||||
// 源代码使用协议遵循本仓库的开源协议及附加协议
|
||||
// Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
|
||||
// Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
|
||||
// 使用文档:https://thingsgateway.cn/
|
||||
// QQ群:605534569
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace ThingsGateway.Foundation.OpcUa105;
|
||||
|
||||
/// <summary>
|
||||
/// OpcUaMaster配置项
|
||||
/// </summary>
|
||||
public class OpcUaProperty
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否订阅
|
||||
/// </summary>
|
||||
public bool ActiveSubscribe { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 自动接受证书
|
||||
/// </summary>
|
||||
public bool AutoAcceptUntrustedCertificates { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 检查域
|
||||
/// </summary>
|
||||
public bool CheckDomain { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 死区
|
||||
/// </summary>
|
||||
public double DeadBand { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 分组大小
|
||||
/// </summary>
|
||||
public int GroupSize { get; set; } = 500;
|
||||
|
||||
public int KeepAliveInterval { get; set; } = 3000;
|
||||
|
||||
/// <summary>
|
||||
/// 加载服务端数据类型
|
||||
/// </summary>
|
||||
public bool LoadType { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// OpcUrl
|
||||
/// </summary>
|
||||
public string OpcUrl { get; set; } = "opc.tcp://127.0.0.1:49320";
|
||||
|
||||
/// <summary>
|
||||
/// 登录密码
|
||||
/// </summary>
|
||||
public string Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 更新间隔
|
||||
/// </summary>
|
||||
public int UpdateRate { get; set; } = 1000;
|
||||
|
||||
/// <summary>
|
||||
/// 登录账号
|
||||
/// </summary>
|
||||
public string UserName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 安全策略
|
||||
/// </summary>
|
||||
public bool UseSecurity { get; set; } = false;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
{
|
||||
return OpcUrl;
|
||||
}
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
|
||||
<Import Project="$(SolutionDir)PackNuget.props" />
|
||||
<Import Project="$(SolutionDir)Version.props" />
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net48;netstandard2.1;net6.0;</TargetFrameworks>
|
||||
<Description>工业设备通讯协议-OpcUa协议</Description>
|
||||
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||
<DocumentationFile></DocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Client.ComplexTypes" Version="1.5.375.443" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Remove="Locales\*.json" />
|
||||
<EmbeddedResource Include="Locales\*.json">
|
||||
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
@@ -0,0 +1,62 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
|
||||
// 此代码版权(除特别声明外的代码)归作者本人Diego所有
|
||||
// 源代码使用协议遵循本仓库的开源协议及附加协议
|
||||
// Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
|
||||
// Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
|
||||
// 使用文档:https://thingsgateway.cn/
|
||||
// QQ群:605534569
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace ThingsGateway.Foundation.OpcUa105;
|
||||
|
||||
internal static class CollectionExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// 将项目列表分解为特定大小的块
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="source">原数组</param>
|
||||
/// <param name="chunkSize">分组大小</param>
|
||||
/// <param name="isToList">是否ToList</param>
|
||||
/// <returns></returns>
|
||||
internal static IEnumerable<IEnumerable<T>> ChunkBetter<T>(this IEnumerable<T> source, int chunkSize, bool isToList = false)
|
||||
{
|
||||
if (chunkSize <= 0)
|
||||
chunkSize = source.Count();
|
||||
var pos = 0;
|
||||
while (source.Skip(pos).Any())
|
||||
{
|
||||
var chunk = source.Skip(pos).Take(chunkSize);
|
||||
yield return isToList ? chunk.ToList() : chunk;
|
||||
pos += chunkSize;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 移除符合条件的元素
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="this"></param>
|
||||
/// <param name="where"></param>
|
||||
internal static void RemoveWhere<T>(this ICollection<T> @this, Func<T, bool> @where)
|
||||
{
|
||||
foreach (var obj in @this.Where(where).ToList())
|
||||
{
|
||||
@this.Remove(obj);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步Select
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <typeparam name="TResult"></typeparam>
|
||||
/// <param name="source"></param>
|
||||
/// <param name="selector"></param>
|
||||
/// <returns></returns>
|
||||
internal static Task<TResult[]> SelectAsync<T, TResult>(this IEnumerable<T> source, Func<T, Task<TResult>> selector)
|
||||
{
|
||||
return Task.WhenAll(source.Select(selector));
|
||||
}
|
||||
}
|
@@ -0,0 +1,106 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
|
||||
// 此代码版权(除特别声明外的代码)归作者本人Diego所有
|
||||
// 源代码使用协议遵循本仓库的开源协议及附加协议
|
||||
// Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
|
||||
// Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
|
||||
// 使用文档:https://thingsgateway.cn/
|
||||
// QQ群:605534569
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace ThingsGateway.Foundation.OpcUa105;
|
||||
|
||||
/// <summary>
|
||||
/// DictionaryExtension
|
||||
/// </summary>
|
||||
internal static class DictionaryExtension
|
||||
{
|
||||
#region 字典扩展
|
||||
|
||||
/// <summary>
|
||||
/// 移除满足条件的项目。
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey"></typeparam>
|
||||
/// <typeparam name="TValue"></typeparam>
|
||||
/// <param name="pairs"></param>
|
||||
/// <param name="func"></param>
|
||||
/// <returns></returns>
|
||||
internal static int RemoveWhen<TKey, TValue>(this ConcurrentDictionary<TKey, TValue> pairs, Func<KeyValuePair<TKey, TValue>, bool> func) where TKey : notnull
|
||||
{
|
||||
var list = new List<TKey>();
|
||||
foreach (var item in pairs)
|
||||
{
|
||||
if (func?.Invoke(item) == true)
|
||||
{
|
||||
list.Add(item.Key);
|
||||
}
|
||||
}
|
||||
|
||||
var count = 0;
|
||||
foreach (var item in list)
|
||||
{
|
||||
if (pairs.TryRemove(item, out _))
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
#if NET45_OR_GREATER || NETSTANDARD2_0_OR_GREATER
|
||||
|
||||
/// <summary>
|
||||
/// 尝试添加
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey"></typeparam>
|
||||
/// <typeparam name="TValue"></typeparam>
|
||||
/// <param name="dictionary"></param>
|
||||
/// <param name="tkey"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
internal static bool TryAdd<TKey, TValue>(this Dictionary<TKey, TValue> dictionary, TKey tkey, TValue value)
|
||||
{
|
||||
if (dictionary.ContainsKey(tkey))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
dictionary.Add(tkey, value);
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// 尝试添加
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey"></typeparam>
|
||||
/// <typeparam name="TValue"></typeparam>
|
||||
/// <param name="dictionary"></param>
|
||||
/// <param name="tkey"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
internal static void AddOrUpdate<TKey, TValue>(this Dictionary<TKey, TValue> dictionary, TKey tkey, TValue value) where TKey : notnull
|
||||
{
|
||||
if (!dictionary.TryAdd(tkey, value))
|
||||
{
|
||||
dictionary[tkey] = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取值。如果键不存在,则返回默认值。
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey"></typeparam>
|
||||
/// <typeparam name="TValue"></typeparam>
|
||||
/// <param name="dictionary"></param>
|
||||
/// <param name="tkey"></param>
|
||||
/// <returns></returns>
|
||||
internal static TValue GetValue<TKey, TValue>(this Dictionary<TKey, TValue> dictionary, TKey tkey) where TKey : notnull
|
||||
{
|
||||
return dictionary.TryGetValue(tkey, out var value) ? value : default;
|
||||
}
|
||||
|
||||
#endregion 字典扩展
|
||||
}
|
530
src/Plugin/ThingsGateway.Foundation.OpcUa105/Utils/JsonUtils.cs
Normal file
530
src/Plugin/ThingsGateway.Foundation.OpcUa105/Utils/JsonUtils.cs
Normal file
@@ -0,0 +1,530 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
|
||||
// 此代码版权(除特别声明外的代码)归作者本人Diego所有
|
||||
// 源代码使用协议遵循本仓库的开源协议及附加协议
|
||||
// Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
|
||||
// Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
|
||||
// 使用文档:https://thingsgateway.cn/
|
||||
// QQ群:605534569
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
using System.Collections;
|
||||
using System.Text;
|
||||
using System.Xml;
|
||||
|
||||
#pragma warning disable CS8605 // 取消装箱可能为 null 的值。
|
||||
|
||||
namespace ThingsGateway.Foundation.OpcUa105;
|
||||
|
||||
/// <summary>
|
||||
/// 扩展方法
|
||||
/// </summary>
|
||||
public static class JsonUtils
|
||||
{
|
||||
#region Decode
|
||||
|
||||
/// <summary>
|
||||
/// 解析获取DataValue
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static DataValue Decode(
|
||||
IServiceMessageContext Context,
|
||||
NodeId dataTypeId,
|
||||
BuiltInType builtInType,
|
||||
int valueRank,
|
||||
JToken json
|
||||
)
|
||||
{
|
||||
var data = DecoderObject(Context, dataTypeId, builtInType, valueRank, json);
|
||||
var dataValue = new DataValue(new Variant(data));
|
||||
return dataValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析获取object
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static object DecoderObject(
|
||||
IServiceMessageContext Context,
|
||||
NodeId dataTypeId,
|
||||
BuiltInType builtInType,
|
||||
int valueRank,
|
||||
JToken json
|
||||
)
|
||||
{
|
||||
object newData;
|
||||
switch (builtInType)
|
||||
{
|
||||
case BuiltInType.ExtensionObject:
|
||||
newData = new
|
||||
{
|
||||
Value = new
|
||||
{
|
||||
TypeId = new { Id = dataTypeId.Identifier, Namespace = dataTypeId.NamespaceIndex },
|
||||
Body = json
|
||||
}
|
||||
}.ToJsonString();
|
||||
break;
|
||||
|
||||
case BuiltInType.Variant:
|
||||
var type = TypeInfo.GetDataTypeId(GetSystemType(json.Type));
|
||||
newData = new
|
||||
{
|
||||
Value = new
|
||||
{
|
||||
Type = type.Identifier,
|
||||
Body = json
|
||||
}
|
||||
}.ToJsonString();
|
||||
break;
|
||||
|
||||
default:
|
||||
newData = new
|
||||
{
|
||||
Value = json
|
||||
}.ToJsonString();
|
||||
break;
|
||||
}
|
||||
|
||||
using var decoder = new JsonDecoder(newData.ToString(), Context);
|
||||
var data = DecodeRawData(decoder, builtInType, valueRank, "Value");
|
||||
return data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DecodeRawData
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private static object DecodeRawData(JsonDecoder decoder, BuiltInType builtInType, int ValueRank, string fieldName)
|
||||
{
|
||||
if (builtInType != 0)
|
||||
{
|
||||
if (ValueRank == ValueRanks.Scalar)
|
||||
{
|
||||
switch (builtInType)
|
||||
{
|
||||
case BuiltInType.Null: { var variant = decoder.ReadVariant(fieldName); return variant.Value; }
|
||||
case BuiltInType.Boolean: { return decoder.ReadBoolean(fieldName); }
|
||||
case BuiltInType.SByte: { return decoder.ReadSByte(fieldName); }
|
||||
case BuiltInType.Byte: { return decoder.ReadByte(fieldName); }
|
||||
case BuiltInType.Int16: { return decoder.ReadInt16(fieldName); }
|
||||
case BuiltInType.UInt16: { return decoder.ReadUInt16(fieldName); }
|
||||
case BuiltInType.Int32: { return decoder.ReadInt32(fieldName); }
|
||||
case BuiltInType.UInt32: { return decoder.ReadUInt32(fieldName); }
|
||||
case BuiltInType.Int64: { return decoder.ReadInt64(fieldName); }
|
||||
case BuiltInType.UInt64: { return decoder.ReadUInt64(fieldName); }
|
||||
case BuiltInType.Float: { return decoder.ReadFloat(fieldName); }
|
||||
case BuiltInType.Double: { return decoder.ReadDouble(fieldName); }
|
||||
case BuiltInType.String: { return decoder.ReadField(fieldName, out var cancellationToken) ? cancellationToken?.ToString() : null; }
|
||||
case BuiltInType.DateTime: { return decoder.ReadDateTime(fieldName); }
|
||||
case BuiltInType.Guid: { return decoder.ReadGuid(fieldName); }
|
||||
case BuiltInType.ByteString: { return decoder.ReadByteString(fieldName); }
|
||||
case BuiltInType.XmlElement: { return decoder.ReadXmlElement(fieldName); }
|
||||
case BuiltInType.NodeId: { return decoder.ReadNodeId(fieldName); }
|
||||
case BuiltInType.ExpandedNodeId: { return decoder.ReadExpandedNodeId(fieldName); }
|
||||
case BuiltInType.StatusCode: { return decoder.ReadStatusCode(fieldName); }
|
||||
case BuiltInType.QualifiedName: { return decoder.ReadQualifiedName(fieldName); }
|
||||
case BuiltInType.LocalizedText: { return decoder.ReadLocalizedText(fieldName); }
|
||||
case BuiltInType.ExtensionObject: { return decoder.ReadExtensionObject(fieldName); }
|
||||
case BuiltInType.DataValue: { return decoder.ReadDataValue(fieldName); }
|
||||
case BuiltInType.Enumeration:
|
||||
{
|
||||
Type type = TypeInfo.GetSystemType(builtInType, ValueRank);
|
||||
return type.IsEnum ? decoder.ReadEnumerated(fieldName, type) : decoder.ReadInt32(fieldName);
|
||||
}
|
||||
case BuiltInType.DiagnosticInfo: { return decoder.ReadDiagnosticInfo(fieldName); }
|
||||
case BuiltInType.Variant: { return decoder.ReadVariant(fieldName); }
|
||||
}
|
||||
}
|
||||
if (ValueRank >= ValueRanks.OneDimension)
|
||||
{
|
||||
return decoder.ReadArray(fieldName, ValueRank, builtInType);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
#endregion Decode
|
||||
|
||||
#region Encode
|
||||
|
||||
/// <summary>
|
||||
/// OPCUAValue解析为Jtoken
|
||||
/// </summary>
|
||||
/// <param name="Context"></param>
|
||||
/// <param name="type"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
internal static JToken Encode(
|
||||
IServiceMessageContext Context,
|
||||
BuiltInType type,
|
||||
object value
|
||||
)
|
||||
{
|
||||
//对于Integer,Int64,Number等会转化为string JValue!
|
||||
|
||||
using var encoder = CreateEncoder(Context, null, false);
|
||||
Encode(encoder, type, "Value", value);
|
||||
var textbuffer = encoder.CloseAndReturnText();
|
||||
using var stringReader = new StringReader(textbuffer);
|
||||
using var jsonReader = new JsonTextReader(stringReader);
|
||||
var jToken = JToken.Load(jsonReader);
|
||||
return jToken["Value"];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CreateEncoder
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private static JsonEncoder CreateEncoder(
|
||||
IServiceMessageContext context,
|
||||
Stream stream,
|
||||
bool useReversibleEncoding = false,
|
||||
bool topLevelIsArray = false,
|
||||
bool includeDefaultValues = true,
|
||||
bool includeDefaultNumbers = true
|
||||
)
|
||||
{
|
||||
return new JsonEncoder(context, useReversibleEncoding, topLevelIsArray, stream)
|
||||
{
|
||||
IncludeDefaultValues = includeDefaultValues,
|
||||
IncludeDefaultNumberValues = includeDefaultNumbers
|
||||
};
|
||||
}
|
||||
|
||||
private static void Encode(JsonEncoder encoder, BuiltInType builtInType, string fieldName, object value)
|
||||
{
|
||||
bool isArray = (value?.GetType().IsArray ?? false) && (builtInType != BuiltInType.ByteString);
|
||||
bool isCollection = (value is IList) && (builtInType != BuiltInType.ByteString);
|
||||
if (!isArray && !isCollection)
|
||||
{
|
||||
switch (builtInType)
|
||||
{
|
||||
case BuiltInType.Null: { encoder.WriteVariant(fieldName, new Variant(value)); return; }
|
||||
//case BuiltInType.Boolean: { encoder.WriteBoolean(fieldName, (bool)value); return; }
|
||||
//case BuiltInType.SByte: { encoder.WriteSByte(fieldName, (sbyte)value); return; }
|
||||
//case BuiltInType.Byte: { encoder.WriteByte(fieldName, (byte)value); return; }
|
||||
//case BuiltInType.Int16: { encoder.WriteInt16(fieldName, (short)value); return; }
|
||||
//case BuiltInType.UInt16: { encoder.WriteUInt16(fieldName, (ushort)value); return; }
|
||||
//case BuiltInType.Int32: { encoder.WriteInt32(fieldName, (int)value); return; }
|
||||
//case BuiltInType.UInt32: { encoder.WriteUInt32(fieldName, (uint)value); return; }
|
||||
//case BuiltInType.Int64: { encoder.WriteInt64(fieldName, (long)value); return; }
|
||||
//case BuiltInType.UInt64: { encoder.WriteUInt64(fieldName, (ulong)value); return; }
|
||||
//case BuiltInType.Float: { encoder.WriteFloat(fieldName, (float)value); return; }
|
||||
//case BuiltInType.Double: { encoder.WriteDouble(fieldName, (double)value); return; }
|
||||
|
||||
//case BuiltInType.Integer: { encoder.WriteInt64(fieldName, (long)value); return; }
|
||||
//case BuiltInType.Number: { encoder.WriteInt64(fieldName, (long)value); return; }
|
||||
//case BuiltInType.UInteger: { encoder.WriteUInt64(fieldName, (ulong)value); return; }
|
||||
//case BuiltInType.String: { encoder.WriteString(fieldName, value?.ToString()); return; }
|
||||
//case BuiltInType.DateTime: { encoder.WriteDateTime(fieldName, (DateTime)value); return; }
|
||||
case BuiltInType.Boolean:
|
||||
{
|
||||
encoder.WriteBoolean(fieldName, Convert.ToBoolean(value));
|
||||
return;
|
||||
}
|
||||
case BuiltInType.SByte:
|
||||
{
|
||||
encoder.WriteSByte(fieldName, Convert.ToSByte(value));
|
||||
return;
|
||||
}
|
||||
case BuiltInType.Byte:
|
||||
{
|
||||
encoder.WriteByte(fieldName, Convert.ToByte(value));
|
||||
return;
|
||||
}
|
||||
case BuiltInType.Int16:
|
||||
{
|
||||
encoder.WriteInt16(fieldName, Convert.ToInt16(value));
|
||||
return;
|
||||
}
|
||||
case BuiltInType.UInt16:
|
||||
{
|
||||
encoder.WriteUInt16(fieldName, Convert.ToUInt16(value));
|
||||
return;
|
||||
}
|
||||
case BuiltInType.Int32:
|
||||
{
|
||||
encoder.WriteInt32(fieldName, Convert.ToInt32(value));
|
||||
return;
|
||||
}
|
||||
case BuiltInType.UInt32:
|
||||
{
|
||||
encoder.WriteUInt32(fieldName, Convert.ToUInt32(value));
|
||||
return;
|
||||
}
|
||||
case BuiltInType.Int64:
|
||||
{
|
||||
encoder.WriteInt64(fieldName, Convert.ToInt64(value));
|
||||
return;
|
||||
}
|
||||
case BuiltInType.UInt64:
|
||||
{
|
||||
encoder.WriteUInt64(fieldName, Convert.ToUInt64(value));
|
||||
return;
|
||||
}
|
||||
case BuiltInType.Float:
|
||||
{
|
||||
encoder.WriteFloat(fieldName, Convert.ToSingle(value));
|
||||
return;
|
||||
}
|
||||
case BuiltInType.Double:
|
||||
{
|
||||
encoder.WriteDouble(fieldName, Convert.ToDouble(value));
|
||||
return;
|
||||
}
|
||||
case BuiltInType.Integer:
|
||||
{
|
||||
encoder.WriteInt64(fieldName, Convert.ToInt64(value));
|
||||
return;
|
||||
}
|
||||
case BuiltInType.Number:
|
||||
{
|
||||
encoder.WriteInt64(fieldName, Convert.ToInt64(value));
|
||||
return;
|
||||
}
|
||||
case BuiltInType.UInteger:
|
||||
{
|
||||
encoder.WriteUInt64(fieldName, Convert.ToUInt64(value));
|
||||
return;
|
||||
}
|
||||
case BuiltInType.String:
|
||||
{
|
||||
encoder.WriteString(fieldName, Convert.ToString(value));
|
||||
return;
|
||||
}
|
||||
case BuiltInType.DateTime:
|
||||
{
|
||||
encoder.WriteDateTime(fieldName, Convert.ToDateTime(value));
|
||||
return;
|
||||
}
|
||||
|
||||
case BuiltInType.Guid: { encoder.WriteGuid(fieldName, (Uuid)value); return; }
|
||||
case BuiltInType.ByteString: { encoder.WriteByteString(fieldName, (byte[])value); return; }
|
||||
case BuiltInType.XmlElement: { encoder.WriteXmlElement(fieldName, (XmlElement)value); return; }
|
||||
case BuiltInType.NodeId: { encoder.WriteNodeId(fieldName, (NodeId)value); return; }
|
||||
case BuiltInType.ExpandedNodeId: { encoder.WriteExpandedNodeId(fieldName, (ExpandedNodeId)value); return; }
|
||||
case BuiltInType.StatusCode: { encoder.WriteStatusCode(fieldName, (StatusCode)value); return; }
|
||||
case BuiltInType.QualifiedName: { encoder.WriteQualifiedName(fieldName, (QualifiedName)value); return; }
|
||||
case BuiltInType.LocalizedText: { encoder.WriteLocalizedText(fieldName, (LocalizedText)value); return; }
|
||||
case BuiltInType.ExtensionObject: { encoder.WriteExtensionObject(fieldName, (ExtensionObject)value); return; }
|
||||
case BuiltInType.DataValue: { encoder.WriteDataValue(fieldName, (DataValue)value); return; }
|
||||
case BuiltInType.Enumeration:
|
||||
{
|
||||
if (value?.GetType().IsEnum == true)
|
||||
{
|
||||
encoder.WriteEnumerated(fieldName, (Enum)value);
|
||||
}
|
||||
else
|
||||
{
|
||||
encoder.WriteEnumerated(fieldName, (Enumeration)value);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case BuiltInType.Variant: { encoder.WriteVariant(fieldName, new Variant(value)); return; }
|
||||
case BuiltInType.DiagnosticInfo: { encoder.WriteDiagnosticInfo(fieldName, (DiagnosticInfo)value); return; }
|
||||
|
||||
//case BuiltInType.Boolean:
|
||||
//case BuiltInType.SByte:
|
||||
//case BuiltInType.Byte:
|
||||
//case BuiltInType.Int16:
|
||||
//case BuiltInType.UInt16:
|
||||
//case BuiltInType.Int32:
|
||||
//case BuiltInType.UInt32:
|
||||
//case BuiltInType.Int64:
|
||||
//case BuiltInType.UInt64:
|
||||
//case BuiltInType.Float:
|
||||
//case BuiltInType.Double:
|
||||
//case BuiltInType.String:
|
||||
//case BuiltInType.Number:
|
||||
//case BuiltInType.Integer:
|
||||
//case BuiltInType.UInteger:
|
||||
// { encoder.WriteString(fieldName, value?.ToString()); return; }
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Array c = value as Array;
|
||||
encoder.WriteArray(fieldName, c, c.Rank, builtInType);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Encode
|
||||
|
||||
#region json
|
||||
|
||||
/// <summary>
|
||||
/// 维度
|
||||
/// </summary>
|
||||
/// <param name="jToken"></param>
|
||||
/// <returns></returns>
|
||||
internal static int CalculateActualValueRank(this JToken jToken)
|
||||
{
|
||||
if (jToken.Type != JTokenType.Array)
|
||||
return -1;
|
||||
|
||||
var jArray = jToken.ToArray();
|
||||
int numDimensions = 1;
|
||||
|
||||
while (jArray.GetElementsType() == JTokenType.Array)
|
||||
{
|
||||
jArray = jArray.Children().ToArray();
|
||||
numDimensions++;
|
||||
}
|
||||
return numDimensions;
|
||||
}
|
||||
|
||||
private static bool ElementsHasSameType(this JToken[] jTokens)
|
||||
{
|
||||
var checkType = jTokens[0].Type == JTokenType.Integer ? JTokenType.Float : jTokens[0].Type;
|
||||
return jTokens
|
||||
.Select(x => (x.Type == JTokenType.Integer) ? JTokenType.Float : x.Type)
|
||||
.All(t => t == checkType);
|
||||
}
|
||||
|
||||
private static JTokenType GetElementsType(this JToken[] jTokens)
|
||||
{
|
||||
if (!jTokens.ElementsHasSameType())
|
||||
throw new Exception("The array sent must have the same type of element in each dimension");
|
||||
return jTokens.First().Type;
|
||||
}
|
||||
|
||||
private static Type GetSystemType(JTokenType jsonType)
|
||||
{
|
||||
return jsonType switch
|
||||
{
|
||||
JTokenType.None => typeof(string),
|
||||
JTokenType.Object => typeof(string),
|
||||
JTokenType.Array => typeof(Array),
|
||||
JTokenType.Constructor => typeof(string),
|
||||
JTokenType.Property => typeof(string),
|
||||
JTokenType.Comment => typeof(string),
|
||||
JTokenType.Integer => typeof(long),
|
||||
JTokenType.Float => typeof(float),
|
||||
JTokenType.String => typeof(string),
|
||||
JTokenType.Boolean => typeof(bool),
|
||||
JTokenType.Null => typeof(string),
|
||||
JTokenType.Undefined => typeof(string),
|
||||
JTokenType.Date => typeof(DateTime),
|
||||
JTokenType.Raw => typeof(string),
|
||||
JTokenType.Bytes => typeof(byte[]),
|
||||
JTokenType.Guid => typeof(Guid),
|
||||
JTokenType.Uri => typeof(Uri),
|
||||
JTokenType.TimeSpan => typeof(TimeSpan),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
#endregion json
|
||||
|
||||
#region Json序列化和反序列化
|
||||
|
||||
/// <summary>
|
||||
/// 从字符串到json
|
||||
/// </summary>
|
||||
/// <param name="json"></param>
|
||||
/// <param name="type"></param>
|
||||
/// <returns></returns>
|
||||
internal static object FromJsonString(this string json, Type type)
|
||||
{
|
||||
return Newtonsoft.Json.JsonConvert.DeserializeObject(json, type);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从字符串到json
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="json"></param>
|
||||
/// <returns></returns>
|
||||
internal static T FromJsonString<T>(this string json)
|
||||
{
|
||||
return (T)FromJsonString(json, typeof(T));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Json反序列化
|
||||
/// </summary>
|
||||
/// <typeparam name="T">反序列化类型</typeparam>
|
||||
/// <param name="datas">数据</param>
|
||||
/// <returns></returns>
|
||||
internal static T JsonDeserializeFromBytes<T>(byte[] datas)
|
||||
{
|
||||
return (T)JsonDeserializeFromBytes(datas, typeof(T));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Json反序列化
|
||||
/// </summary>
|
||||
/// <param name="datas"></param>
|
||||
/// <param name="type"></param>
|
||||
/// <returns></returns>
|
||||
internal static object JsonDeserializeFromBytes(byte[] datas, Type type)
|
||||
{
|
||||
return FromJsonString(Encoding.UTF8.GetString(datas), type);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Json反序列化
|
||||
/// </summary>
|
||||
/// <typeparam name="T">反序列化类型</typeparam>
|
||||
/// <param name="path">文件路径</param>
|
||||
/// <returns></returns>
|
||||
internal static T JsonDeserializeFromFile<T>(string path)
|
||||
{
|
||||
return JsonDeserializeFromString<T>(File.ReadAllText(path));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Json反序列化
|
||||
/// </summary>
|
||||
/// <typeparam name="T">类型</typeparam>
|
||||
/// <param name="json">json字符串</param>
|
||||
/// <returns></returns>
|
||||
internal static T JsonDeserializeFromString<T>(string json)
|
||||
{
|
||||
return FromJsonString<T>(json);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Json序列化数据对象
|
||||
/// </summary>
|
||||
/// <param name="obj">数据对象</param>
|
||||
/// <returns></returns>
|
||||
internal static byte[] JsonSerializeToBytes(object obj)
|
||||
{
|
||||
return Encoding.UTF8.GetBytes(ToJsonString(obj));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Json序列化至文件
|
||||
/// </summary>
|
||||
/// <param name="obj"></param>
|
||||
/// <param name="path"></param>
|
||||
internal static void JsonSerializeToFile(object obj, string path)
|
||||
{
|
||||
using (var fileStream = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite))
|
||||
{
|
||||
var date = JsonSerializeToBytes(obj);
|
||||
fileStream.Write(date, 0, date.Length);
|
||||
fileStream.Close();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 转换为Json
|
||||
/// </summary>
|
||||
/// <param name="item"></param>
|
||||
/// <param name="isIndented"></param>
|
||||
/// <returns></returns>
|
||||
internal static string ToJsonString(this object item, bool isIndented = false)
|
||||
{
|
||||
if (isIndented)
|
||||
return Newtonsoft.Json.JsonConvert.SerializeObject(item, Newtonsoft.Json.Formatting.Indented);
|
||||
else
|
||||
return Newtonsoft.Json.JsonConvert.SerializeObject(item);
|
||||
}
|
||||
|
||||
#endregion Json序列化和反序列化
|
||||
}
|
1153
src/Plugin/ThingsGateway.Foundation.OpcUa105/Utils/OpcUaUtils.cs
Normal file
1153
src/Plugin/ThingsGateway.Foundation.OpcUa105/Utils/OpcUaUtils.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,7 @@
|
||||
<PkgOPCFoundation_NetStandard_Opc_Ua_Security_CertificatesPackageFiles Include="$(PkgOPCFoundation_NetStandard_Opc_Ua_Security_Certificates)\lib\net8.0\*.*" />
|
||||
<PkgOPCFoundation_NetStandard_Opc_Ua_ClientPackageFiles Include="$(PkgOPCFoundation_NetStandard_Opc_Ua_Client)\lib\net8.0\*.*" />
|
||||
<PkgOPCFoundation_NetStandard_Opc_Ua_Client_ComplexTypesPackageFiles Include="$(PkgOPCFoundation_NetStandard_Opc_Ua_Client_ComplexTypes)\lib\net8.0\*.*" />
|
||||
<PkgSystem_Formats_Asn1PackageFiles Include="$(PkgSystem_Formats_Asn1)\lib\net9.0\*.*" />
|
||||
<PkgSystem_Formats_Asn1PackageFiles Include="$(PkgSystem_Formats_Asn1)\lib\net8.0\*.*" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
@@ -167,7 +167,7 @@ public partial class ThingsGatewayServer : StandardServer
|
||||
configuration.SecurityConfiguration.UserIssuerCertificates != null)
|
||||
{
|
||||
CertificateValidator certificateValidator = new();
|
||||
certificateValidator.Update(configuration.SecurityConfiguration).Wait();
|
||||
certificateValidator.Update(configuration).GetAwaiter().GetResult();
|
||||
certificateValidator.Update(configuration.SecurityConfiguration.UserIssuerCertificates,
|
||||
configuration.SecurityConfiguration.TrustedUserCertificates,
|
||||
configuration.SecurityConfiguration.RejectedCertificateStore);
|
||||
|
@@ -57,7 +57,7 @@ public partial class OpcUaServer : BusinessBase
|
||||
{
|
||||
VariableRuntimes = new(GlobalData.GetEnableVariables());
|
||||
|
||||
CollectDevices = GlobalData.GetEnableDevices().ToDictionary();
|
||||
CollectDevices = GlobalData.GetEnableDevices().Where(a => a.Value.IsCollect == true).ToDictionary();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@@ -1,4 +1,4 @@
|
||||
@page "/OpcUaMaster"
|
||||
@page "/OpcUaMaster105"
|
||||
@using BootstrapBlazor.Components
|
||||
@using ThingsGateway.Extension
|
||||
@using ThingsGateway.Foundation
|
||||
|
@@ -49,7 +49,7 @@
|
||||
<PrivateAssets>contentFiles;compile;build;buildMultitargeting;buildTransitive;analyzers;</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
||||
<PackageReference Include="System.Formats.Asn1" Version="9.*" GeneratePathProperty="true">
|
||||
<PackageReference Include="System.Formats.Asn1" Version="8.*" GeneratePathProperty="true">
|
||||
<PrivateAssets>contentFiles;compile;build;buildMultitargeting;buildTransitive;analyzers;</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
||||
|
@@ -0,0 +1,42 @@
|
||||
<Project>
|
||||
|
||||
|
||||
<Target Name="CopyNugetPackages" AfterTargets="Build">
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' != 'net6.0'">
|
||||
<PkgOPCFoundation_NetStandard_Opc_Ua_ServerPackageFiles Include="$(PkgOPCFoundation_NetStandard_Opc_Ua_Server)\lib\net8.0\*.*" />
|
||||
<PkgOPCFoundation_NetStandard_Opc_Ua_ConfigurationPackageFiles Include="$(PkgOPCFoundation_NetStandard_Opc_Ua_Configuration)\lib\net8.0\*.*" />
|
||||
<PkgOPCFoundation_NetStandard_Opc_Ua_CorePackageFiles Include="$(PkgOPCFoundation_NetStandard_Opc_Ua_Core)\lib\net8.0\*.*" />
|
||||
<PkgOPCFoundation_NetStandard_Opc_Ua_ServerPackageFiles Include="$(PkgOPCFoundation_NetStandard_Opc_Ua_Server)\lib\net8.0\*.*" />
|
||||
<PkgOPCFoundation_NetStandard_Opc_Ua_Security_CertificatesPackageFiles Include="$(PkgOPCFoundation_NetStandard_Opc_Ua_Security_Certificates)\lib\net8.0\*.*" />
|
||||
<PkgOPCFoundation_NetStandard_Opc_Ua_ClientPackageFiles Include="$(PkgOPCFoundation_NetStandard_Opc_Ua_Client)\lib\net8.0\*.*" />
|
||||
<PkgOPCFoundation_NetStandard_Opc_Ua_Client_ComplexTypesPackageFiles Include="$(PkgOPCFoundation_NetStandard_Opc_Ua_Client_ComplexTypes)\lib\net8.0\*.*" />
|
||||
<PkgSystem_Formats_Asn1PackageFiles Include="$(PkgSystem_Formats_Asn1)\lib\net8.0\*.*" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<ApplicationFolder>$(TargetDir)</ApplicationFolder>
|
||||
</PropertyGroup>
|
||||
<Copy SourceFiles="@(PkgOPCFoundation_NetStandard_Opc_Ua_ServerPackageFiles)" DestinationFolder="$(ApplicationFolder)%(RecursiveDir)" />
|
||||
<Copy SourceFiles="@(PkgOPCFoundation_NetStandard_Opc_Ua_ConfigurationPackageFiles)" DestinationFolder="$(ApplicationFolder)%(RecursiveDir)" />
|
||||
<Copy SourceFiles="@(PkgOPCFoundation_NetStandard_Opc_Ua_CorePackageFiles)" DestinationFolder="$(ApplicationFolder)%(RecursiveDir)" />
|
||||
<Copy SourceFiles="@(PkgOPCFoundation_NetStandard_Opc_Ua_ServerPackageFiles)" DestinationFolder="$(ApplicationFolder)%(RecursiveDir)" />
|
||||
<Copy SourceFiles="@(PkgOPCFoundation_NetStandard_Opc_Ua_Security_CertificatesPackageFiles)" DestinationFolder="$(ApplicationFolder)%(RecursiveDir)" />
|
||||
<Copy SourceFiles="@(PkgSystem_Formats_Asn1PackageFiles)" DestinationFolder="$(ApplicationFolder)%(RecursiveDir)" />
|
||||
<!--<Copy SourceFiles="@(PkgThingsGateway_Foundation_OpcUaPackageFiles)" DestinationFolder="$(ApplicationFolder)%(RecursiveDir)" />-->
|
||||
<Copy SourceFiles="@(PkgOPCFoundation_NetStandard_Opc_Ua_ClientPackageFiles)" DestinationFolder="$(ApplicationFolder)%(RecursiveDir)" />
|
||||
<Copy SourceFiles="@(PkgOPCFoundation_NetStandard_Opc_Ua_Client_ComplexTypesPackageFiles)" DestinationFolder="$(ApplicationFolder)%(RecursiveDir)" />
|
||||
</Target>
|
||||
|
||||
|
||||
<!--在构建后触发的。它通过在 Nuget 包的 Content 文件夹中包含目标目录中的所有文件和子文件夹来创建 nuget 包-->
|
||||
<Target Name="IncludeAllFilesInTargetDir" AfterTargets="Build">
|
||||
<ItemGroup>
|
||||
<Content Include="$(ProjectDir)bin\$(Configuration)\$(TargetFramework)\**\*Opc*.dll">
|
||||
<Pack>true</Pack>
|
||||
<PackagePath>Content</PackagePath>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
</Project>
|
14
src/Plugin/ThingsGateway.Plugin.OpcUa105/GlobalUsings.cs
Normal file
14
src/Plugin/ThingsGateway.Plugin.OpcUa105/GlobalUsings.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
|
||||
// 此代码版权(除特别声明外的代码)归作者本人Diego所有
|
||||
// 源代码使用协议遵循本仓库的开源协议及附加协议
|
||||
// Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
|
||||
// Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
|
||||
// 使用文档:https://thingsgateway.cn/
|
||||
// QQ群:605534569
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
global using System;
|
||||
global using System.Collections.Generic;
|
||||
|
||||
global using ThingsGateway.Foundation;
|
38
src/Plugin/ThingsGateway.Plugin.OpcUa105/Locales/en-US.json
Normal file
38
src/Plugin/ThingsGateway.Plugin.OpcUa105/Locales/en-US.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"ThingsGateway.Plugin.OpcUa105.OpcUaImportVariable": {
|
||||
"NoVariablesAvailable": "No variables available",
|
||||
"Success": "Success"
|
||||
},
|
||||
"ThingsGateway.Foundation.OpcUa105.OpcUaProperty": {
|
||||
"OpcUrl": "OpcUrl",
|
||||
"UserName": "UserName",
|
||||
"Password": "Password",
|
||||
"CheckDomain": "CheckDomain",
|
||||
"UpdateRate": "UpdateRate",
|
||||
"ActiveSubscribe": "ActiveSubscribe",
|
||||
"GroupSize": "GroupSize",
|
||||
"DeadBand": "DeadBand",
|
||||
"KeepAliveInterval": "KeepAliveInterval(ms)",
|
||||
"UseSecurity": "UseSecurity",
|
||||
"LoadType": "LoadType",
|
||||
"AutoAcceptUntrustedCertificates": "AutoAcceptUntrustedCertificates",
|
||||
"ExportC": "ExportCertificate",
|
||||
"SourceTimestampEnable": "SourceTimestampEnable",
|
||||
"ReIntervalTime": "ReIntervalTime",
|
||||
"RetryCount": "RetryCount"
|
||||
},
|
||||
"ThingsGateway.Plugin.OpcUa105.OpcUaServerProperty": {
|
||||
"IsAllVariable": "IsAllVariable",
|
||||
"OpcUaStringUrl": "OpcUaStringUrl",
|
||||
"BigTextSubjectName": "SubjectName",
|
||||
"BigTextApplicationUri": "ApplicationUri",
|
||||
"SecurityPolicy": "SecurityPolicy",
|
||||
"AutoAcceptUntrustedCertificates": "AutoAcceptUntrustedCertificates"
|
||||
},
|
||||
"ThingsGateway.Plugin.OpcUa105.OpcUaServerVariableProperty": {
|
||||
"DataType": "DataType"
|
||||
},
|
||||
"ThingsGateway.Plugin.OpcUa105.OpcUaServer": {
|
||||
"CanStartService": "Can start service"
|
||||
}
|
||||
}
|
39
src/Plugin/ThingsGateway.Plugin.OpcUa105/Locales/zh-CN.json
Normal file
39
src/Plugin/ThingsGateway.Plugin.OpcUa105/Locales/zh-CN.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"ThingsGateway.Plugin.OpcUa105.OpcUaImportVariable": {
|
||||
"NoVariablesAvailable": "无可用变量",
|
||||
"Success": "成功"
|
||||
},
|
||||
|
||||
"ThingsGateway.Plugin.OpcUa105.OpcUaMasterProperty": {
|
||||
"OpcUrl": "OpcUrl",
|
||||
"UserName": "登录账号",
|
||||
"Password": "登录密码",
|
||||
"CheckDomain": "检查域",
|
||||
"UpdateRate": "推送间隔",
|
||||
"ActiveSubscribe": "订阅",
|
||||
"GroupSize": "分组大小",
|
||||
"DeadBand": "死区",
|
||||
"KeepAliveInterval": "心跳间隔(ms)",
|
||||
"UseSecurity": "安全策略",
|
||||
"LoadType": "加载服务端数据类型",
|
||||
"AutoAcceptUntrustedCertificates": "自动接受不受信任的证书",
|
||||
"SourceTimestampEnable": "服务端时间戳",
|
||||
"ExportC": "导出证书",
|
||||
"ReIntervalTime": "离线恢复时间",
|
||||
"RetryCount": "失败重试次数"
|
||||
},
|
||||
"ThingsGateway.Plugin.OpcUa105.OpcUaServerProperty": {
|
||||
"IsAllVariable": "选择全部变量",
|
||||
"OpcUaStringUrl": "服务地址",
|
||||
"BigTextSubjectName": "SubjectName",
|
||||
"BigTextApplicationUri": "ApplicationUri",
|
||||
"SecurityPolicy": "安全策略",
|
||||
"AutoAcceptUntrustedCertificates": "自动接受不受信任的证书"
|
||||
},
|
||||
"ThingsGateway.Plugin.OpcUa105.OpcUaServerVariableProperty": {
|
||||
"DataType": "数据类型"
|
||||
},
|
||||
"ThingsGateway.Plugin.OpcUa105.OpcUaServer": {
|
||||
"CanStartService": "无法启动服务"
|
||||
}
|
||||
}
|
@@ -0,0 +1,359 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
|
||||
// 此代码版权(除特别声明外的代码)归作者本人Diego所有
|
||||
// 源代码使用协议遵循本仓库的开源协议及附加协议
|
||||
// Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
|
||||
// Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
|
||||
// 使用文档:https://thingsgateway.cn/
|
||||
// QQ群:605534569
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
using Opc.Ua;
|
||||
|
||||
using ThingsGateway.Foundation.Extension.Generic;
|
||||
using ThingsGateway.Foundation.OpcUa105;
|
||||
using ThingsGateway.Gateway.Application;
|
||||
using ThingsGateway.NewLife.Json.Extension;
|
||||
using ThingsGateway.NewLife.Threading;
|
||||
|
||||
using TouchSocket.Core;
|
||||
|
||||
namespace ThingsGateway.Plugin.OpcUa105;
|
||||
|
||||
/// <summary>
|
||||
/// <inheritdoc/>
|
||||
/// </summary>
|
||||
public class OpcUaMaster : CollectBase
|
||||
{
|
||||
private readonly OpcUaMasterProperty _driverProperties = new();
|
||||
|
||||
private ThingsGateway.Foundation.OpcUa105.OpcUaMaster _plc;
|
||||
|
||||
private CancellationToken _token;
|
||||
|
||||
private volatile bool connectFirstFail;
|
||||
private volatile bool connectFirstFailLoged;
|
||||
private volatile bool success = true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override CollectPropertyBase CollectProperties => _driverProperties;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Type DriverDebugUIType => typeof(ThingsGateway.Debug.OpcUaMaster);
|
||||
|
||||
public override Type DriverPropertyUIType => typeof(OpcUaMasterPropertyRazor);
|
||||
|
||||
|
||||
|
||||
protected override void InitChannel(IChannel? channel = null)
|
||||
{
|
||||
|
||||
|
||||
//载入配置
|
||||
OpcUaProperty config = new()
|
||||
{
|
||||
OpcUrl = _driverProperties.OpcUrl,
|
||||
UpdateRate = _driverProperties.UpdateRate,
|
||||
DeadBand = _driverProperties.DeadBand,
|
||||
GroupSize = _driverProperties.GroupSize,
|
||||
KeepAliveInterval = _driverProperties.KeepAliveInterval,
|
||||
UseSecurity = _driverProperties.UseSecurity,
|
||||
ActiveSubscribe = _driverProperties.ActiveSubscribe,
|
||||
UserName = _driverProperties.UserName,
|
||||
Password = _driverProperties.Password,
|
||||
CheckDomain = _driverProperties.CheckDomain,
|
||||
LoadType = _driverProperties.LoadType,
|
||||
AutoAcceptUntrustedCertificates = _driverProperties.AutoAcceptUntrustedCertificates,
|
||||
};
|
||||
if (_plc == null)
|
||||
{
|
||||
_plc = new();
|
||||
_plc.LogEvent += _plc_LogEvent;
|
||||
_plc.DataChangedHandler += DataChangedHandler;
|
||||
}
|
||||
_plc.OpcUaProperty = config;
|
||||
base.InitChannel(channel);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool IsConnected() => _plc?.Connected == true;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{_driverProperties.OpcUrl}";
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (_plc != null)
|
||||
{
|
||||
_plc.DataChangedHandler -= DataChangedHandler;
|
||||
_plc.LogEvent -= _plc_LogEvent;
|
||||
|
||||
_plc.Disconnect();
|
||||
_plc.SafeDispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
protected override string GetAddressDescription()
|
||||
{
|
||||
return _plc?.GetAddressDescription();
|
||||
}
|
||||
|
||||
protected override async Task ProtectedStartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_token = cancellationToken;
|
||||
try
|
||||
{
|
||||
await _plc.ConnectAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogMessage?.LogWarning(ex, "Connect Fail");
|
||||
connectFirstFail = true;
|
||||
}
|
||||
await base.ProtectedStartAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
protected override async ValueTask ProtectedExecuteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (connectFirstFail && !IsConnected())
|
||||
{
|
||||
try
|
||||
{
|
||||
await _plc.ConnectAsync(cancellationToken).ConfigureAwait(false);
|
||||
connectFirstFail = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (!connectFirstFailLoged)
|
||||
LogMessage?.LogWarning(ex, "Connect Fail");
|
||||
|
||||
connectFirstFailLoged = true;
|
||||
CurrentDevice.SetDeviceStatus(TimerX.Now, true, ex.Message);
|
||||
await Task.Delay(10000, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
connectFirstFail = false;
|
||||
}
|
||||
if (_driverProperties.ActiveSubscribe)
|
||||
{
|
||||
|
||||
//获取设备连接状态
|
||||
if (IsConnected())
|
||||
{
|
||||
//更新设备活动时间
|
||||
CurrentDevice.SetDeviceStatus(TimerX.Now, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
CurrentDevice.SetDeviceStatus(TimerX.Now, true);
|
||||
}
|
||||
|
||||
ScriptVariableRun(cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
await base.ProtectedExecuteAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override List<VariableSourceRead> ProtectedLoadSourceRead(List<VariableRuntime> deviceVariables)
|
||||
{
|
||||
if (deviceVariables.Count > 0)
|
||||
{
|
||||
var dataLists = deviceVariables.ChunkBetter(_driverProperties.GroupSize);
|
||||
_plc.Variables = new();
|
||||
_plc.Variables.AddRange(dataLists.Select(a => a.Where(a => !a.RegisterAddress.IsNullOrEmpty()).Select(a => a.RegisterAddress!).ToList()).ToList());
|
||||
var dataResult = new List<VariableSourceRead>();
|
||||
foreach (var variable in dataLists)
|
||||
{
|
||||
var sourVars = new VariableSourceRead()
|
||||
{
|
||||
TimeTick = new(_driverProperties.UpdateRate.ToString()),
|
||||
RegisterAddress = "",
|
||||
};
|
||||
foreach (var item in variable)
|
||||
{
|
||||
sourVars.AddVariable(item);
|
||||
}
|
||||
dataResult.Add(sourVars);
|
||||
}
|
||||
|
||||
return dataResult;
|
||||
}
|
||||
else
|
||||
{
|
||||
return new();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override async ValueTask<OperResult<byte[]>> ReadSourceAsync(VariableSourceRead deviceVariableSourceRead, CancellationToken cancellationToken)
|
||||
{
|
||||
DateTime time = DateTime.Now;
|
||||
var addresss = deviceVariableSourceRead.VariableRuntimes.Where(a => !a.RegisterAddress.IsNullOrEmpty()).Select(a => a.RegisterAddress!).ToArray();
|
||||
try
|
||||
{
|
||||
await ReadWriteLock.ReaderLockAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var result = await _plc.ReadJTokenValueAsync(addresss, cancellationToken).ConfigureAwait(false);
|
||||
foreach (var data in result)
|
||||
{
|
||||
if (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var data1 = deviceVariableSourceRead.VariableRuntimes.Where(a => a.RegisterAddress == data.Item1);
|
||||
|
||||
foreach (var item in data1)
|
||||
{
|
||||
object value;
|
||||
if (data.Item3 is JValue jValue)
|
||||
{
|
||||
value = jValue.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
value = data.Item3;
|
||||
}
|
||||
|
||||
var isGood = StatusCode.IsGood(data.Item2.StatusCode);
|
||||
if (_driverProperties.SourceTimestampEnable)
|
||||
{
|
||||
time = data.Item2.SourceTimestamp.ToLocalTime();
|
||||
}
|
||||
if (isGood)
|
||||
{
|
||||
item.SetValue(value, time);
|
||||
}
|
||||
else
|
||||
{
|
||||
item.SetValue(null, time, false);
|
||||
item.VariableSource.LastErrorMessage = data.Item2.StatusCode.ToString();
|
||||
}
|
||||
}
|
||||
LogMessage.Trace($"{ToString()} Change:{Environment.NewLine}{data.Item1} : {data.Item3}");
|
||||
}
|
||||
}
|
||||
if (result.Any(a => StatusCode.IsBad(a.Item2.StatusCode)))
|
||||
{
|
||||
return new OperResult<byte[]>($"OPC quality bad");
|
||||
}
|
||||
else
|
||||
{
|
||||
return OperResult.CreateSuccessResult<byte[]>(null);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new OperResult<byte[]>($"ReadSourceAsync {addresss.ToJsonNetString()}:{Environment.NewLine}{ex}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override async ValueTask<Dictionary<string, OperResult>> WriteValuesAsync(Dictionary<VariableRuntime, JToken> writeInfoLists, CancellationToken cancellationToken)
|
||||
{
|
||||
using var writeLock = ReadWriteLock.WriterLock();
|
||||
try
|
||||
{
|
||||
var result = await _plc.WriteNodeAsync(writeInfoLists.ToDictionary(a => a.Key.RegisterAddress!, a => a.Value), cancellationToken).ConfigureAwait(false);
|
||||
return result.ToDictionary<KeyValuePair<string, Tuple<bool, string>>, string, OperResult>(a =>
|
||||
{
|
||||
return writeInfoLists.Keys.FirstOrDefault(b => b.RegisterAddress == a.Key)?.Name!;
|
||||
}
|
||||
, a =>
|
||||
{
|
||||
if (!a.Value.Item1)
|
||||
return new OperResult(a.Value.Item2);
|
||||
else
|
||||
return OperResult.Success;
|
||||
})!;
|
||||
}
|
||||
finally
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private void _plc_LogEvent(byte level, object sender, string message, Exception ex)
|
||||
{
|
||||
LogMessage?.Log((LogLevel)level, sender, message, ex);
|
||||
}
|
||||
|
||||
private void DataChangedHandler((VariableNode variableNode, DataValue dataValue, JToken jToken) data)
|
||||
{
|
||||
DateTime time = DateTime.Now;
|
||||
try
|
||||
{
|
||||
if (CurrentDevice.Pause)
|
||||
return;
|
||||
if (_token.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
LogMessage.Trace($"{ToString()} Change: {Environment.NewLine} {data.variableNode.NodeId} : {data.jToken?.ToString()}");
|
||||
|
||||
if (CurrentDevice.Pause)
|
||||
{
|
||||
return;
|
||||
}
|
||||
//尝试固定点位的数据类型
|
||||
var type = TypeInfo.GetSystemType(TypeInfo.GetBuiltInType(data.variableNode.DataType, _plc.Session.SystemContext.TypeTable), data.variableNode.ValueRank);
|
||||
|
||||
var itemReads = VariableRuntimes.Select(a => a.Value).Where(it => it.RegisterAddress == data.variableNode.NodeId);
|
||||
|
||||
object value;
|
||||
if (data.jToken is JValue jValue)
|
||||
{
|
||||
value = jValue.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
value = data.jToken;
|
||||
}
|
||||
var isGood = StatusCode.IsGood(data.dataValue.StatusCode);
|
||||
if (_driverProperties.SourceTimestampEnable)
|
||||
{
|
||||
time = data.dataValue.SourceTimestamp.ToLocalTime();
|
||||
}
|
||||
foreach (var item in itemReads)
|
||||
{
|
||||
if (CurrentDevice.Pause)
|
||||
return;
|
||||
if (_token.IsCancellationRequested)
|
||||
return;
|
||||
if (item.DataType == DataTypeEnum.Object)
|
||||
if (type.Namespace.StartsWith("System"))
|
||||
{
|
||||
var enumValues = Enum.GetValues<DataTypeEnum>();
|
||||
var stringList = enumValues.Select(e => e.ToString());
|
||||
if (stringList.Contains(type.Name))
|
||||
try { item.DataType = Enum.Parse<DataTypeEnum>(type.Name); } catch { }
|
||||
}
|
||||
if (isGood)
|
||||
{
|
||||
item.SetValue(value, time);
|
||||
}
|
||||
else
|
||||
{
|
||||
item.SetValue(null, time, false);
|
||||
item.VariableSource.LastErrorMessage = data.Item2.StatusCode.ToString();
|
||||
}
|
||||
}
|
||||
success = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (success)
|
||||
LogMessage?.LogWarning(ex);
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,98 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
|
||||
// 此代码版权(除特别声明外的代码)归作者本人Diego所有
|
||||
// 源代码使用协议遵循本仓库的开源协议及附加协议
|
||||
// Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
|
||||
// Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
|
||||
// 使用文档:https://thingsgateway.cn/
|
||||
// QQ群:605534569
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using ThingsGateway.Gateway.Application;
|
||||
|
||||
namespace ThingsGateway.Plugin.OpcUa105;
|
||||
|
||||
/// <summary>
|
||||
/// <inheritdoc/>
|
||||
/// </summary>
|
||||
public class OpcUaMasterProperty : CollectPropertyBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 连接Url
|
||||
/// </summary>
|
||||
[DynamicProperty]
|
||||
public string OpcUrl { get; set; } = "opc.tcp://127.0.0.1:49320";
|
||||
|
||||
/// <summary>
|
||||
/// 登录账号
|
||||
/// </summary>
|
||||
[DynamicProperty]
|
||||
public string? UserName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 登录密码
|
||||
/// </summary>
|
||||
[DynamicProperty]
|
||||
public string? Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 检查域
|
||||
/// </summary>
|
||||
[DynamicProperty]
|
||||
public bool CheckDomain { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 安全策略
|
||||
/// </summary>
|
||||
[DynamicProperty]
|
||||
public bool UseSecurity { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 接受不受信任的证书
|
||||
/// </summary>
|
||||
[DynamicProperty()]
|
||||
public bool AutoAcceptUntrustedCertificates { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 是否使用SourceTime
|
||||
/// </summary>
|
||||
[DynamicProperty]
|
||||
public bool SourceTimestampEnable { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 加载服务端数据类型
|
||||
/// </summary>
|
||||
[DynamicProperty]
|
||||
public bool LoadType { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 激活订阅
|
||||
/// </summary>
|
||||
[DynamicProperty]
|
||||
public bool ActiveSubscribe { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 更新频率
|
||||
/// </summary>
|
||||
[DynamicProperty]
|
||||
public int UpdateRate { get; set; } = 1000;
|
||||
|
||||
/// <summary>
|
||||
/// 死区
|
||||
/// </summary>
|
||||
[DynamicProperty]
|
||||
public double DeadBand { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 最大组大小
|
||||
/// </summary>
|
||||
[DynamicProperty]
|
||||
public int GroupSize { get; set; } = 500;
|
||||
|
||||
/// <summary>
|
||||
/// 心跳频率
|
||||
/// </summary>
|
||||
[DynamicProperty]
|
||||
public int KeepAliveInterval { get; set; } = 3000;
|
||||
|
||||
}
|
@@ -0,0 +1,37 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
|
||||
// 此代码版权(除特别声明外的代码)归作者本人Diego所有
|
||||
// 源代码使用协议遵循本仓库的开源协议及附加协议
|
||||
// Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
|
||||
// Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
|
||||
// 使用文档:https://thingsgateway.cn/
|
||||
// QQ群:605534569
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using Opc.Ua.Configuration;
|
||||
|
||||
using TouchSocket.Core;
|
||||
|
||||
namespace ThingsGateway.Plugin.OpcUa105;
|
||||
|
||||
public class ApplicationMessageDlg : IApplicationMessageDlg
|
||||
{
|
||||
private ILog _log;
|
||||
private string message = string.Empty;
|
||||
|
||||
public ApplicationMessageDlg(ILog log)
|
||||
{
|
||||
_log = log;
|
||||
}
|
||||
|
||||
public override void Message(string text, bool ask)
|
||||
{
|
||||
message = text;
|
||||
}
|
||||
|
||||
public override Task<bool> ShowAsync()
|
||||
{
|
||||
_log.Warning(message);
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
}
|
@@ -0,0 +1,65 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
|
||||
// 此代码版权(除特别声明外的代码)归作者本人Diego所有
|
||||
// 源代码使用协议遵循本仓库的开源协议及附加协议
|
||||
// Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
|
||||
// Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
|
||||
// 使用文档:https://thingsgateway.cn/
|
||||
// QQ群:605534569
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using TouchSocket.Core;
|
||||
|
||||
#pragma warning disable CS8633 // 类型参数的约束中的为 Null 性与隐式实现接口方法中的类型参数的约束不匹配。
|
||||
|
||||
#pragma warning disable CS8766 // 返回类型中引用类型的为 Null 性与隐式实现的成员不匹配(可能是由于为 Null 性特性)。
|
||||
|
||||
namespace ThingsGateway.Plugin.OpcUa105;
|
||||
|
||||
internal sealed class OpcUaLogger : ILogger
|
||||
{
|
||||
private ILog _log;
|
||||
|
||||
public OpcUaLogger(ILog log)
|
||||
{
|
||||
_log = log;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the log level
|
||||
/// </summary>
|
||||
public Microsoft.Extensions.Logging.LogLevel LogLevel { get; set; } = Microsoft.Extensions.Logging.LogLevel.Trace;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IDisposable BeginScope<TState>(TState state) where TState : notnull
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsEnabled(Microsoft.Extensions.Logging.LogLevel logLevel) => logLevel >= LogLevel;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Log<TState>(
|
||||
Microsoft.Extensions.Logging.LogLevel logLevel,
|
||||
EventId eventId,
|
||||
TState state,
|
||||
Exception? exception,
|
||||
Func<TState, Exception?, string> formatter)
|
||||
{
|
||||
if (!IsEnabled(logLevel))
|
||||
{
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
var message = formatter(state, exception);
|
||||
//if (logLevel > Microsoft.Extensions.Logging.LogLevel.Warning)
|
||||
{
|
||||
_log.Log((TouchSocket.Core.LogLevel)(byte)logLevel, state, message, exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
|
||||
// 此代码版权(除特别声明外的代码)归作者本人Diego所有
|
||||
// 源代码使用协议遵循本仓库的开源协议及附加协议
|
||||
// Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
|
||||
// Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
|
||||
// 使用文档:https://thingsgateway.cn/
|
||||
// QQ群:605534569
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using Opc.Ua;
|
||||
|
||||
namespace ThingsGateway.Plugin.OpcUa105;
|
||||
|
||||
internal sealed class OpcUaTag : BaseDataVariableState
|
||||
{
|
||||
internal OpcUaTag(NodeState parent) : base(parent)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 变量Id
|
||||
/// </summary>
|
||||
internal long Id { get; set; }
|
||||
|
||||
internal bool IsDataTypeInit { get; set; }
|
||||
}
|
@@ -0,0 +1,477 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
|
||||
// 此代码版权(除特别声明外的代码)归作者本人Diego所有
|
||||
// 源代码使用协议遵循本仓库的开源协议及附加协议
|
||||
// Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
|
||||
// Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
|
||||
// 使用文档:https://thingsgateway.cn/
|
||||
// QQ群:605534569
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
using Opc.Ua;
|
||||
using Opc.Ua.Server;
|
||||
|
||||
using ThingsGateway.Foundation.OpcUa105;
|
||||
using ThingsGateway.Gateway.Application;
|
||||
|
||||
using TouchSocket.Core;
|
||||
|
||||
namespace ThingsGateway.Plugin.OpcUa105;
|
||||
|
||||
/// <summary>
|
||||
/// 数据节点
|
||||
/// </summary>
|
||||
public class ThingsGatewayNodeManager : CustomNodeManager2
|
||||
{
|
||||
private const string ReferenceServer = "https://thingsgateway.cn/";
|
||||
|
||||
//private readonly TypeAdapterConfig _config;
|
||||
/// <summary>
|
||||
/// OPC和网关对应表
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, OpcUaTag> NodeIdTags = new();
|
||||
|
||||
private BusinessBase _businessBase;
|
||||
private volatile bool success = true;
|
||||
|
||||
/// <inheritdoc cref="ThingsGatewayNodeManager"/>
|
||||
public ThingsGatewayNodeManager(BusinessBase businessBase, IServerInternal server, ApplicationConfiguration configuration) : base(server, configuration, ReferenceServer)
|
||||
{
|
||||
_businessBase = businessBase;
|
||||
//_config = new TypeAdapterConfig();
|
||||
//_config.ForType<HistoryValue, DataValue>()
|
||||
//.Map(dest => dest.WrappedValue, (src) => new Variant(src.Value))
|
||||
//.Map(dest => dest.SourceTimestamp, src => DateTime.SpecifyKind(src.CollectTime, DateTimeKind.Utc))
|
||||
//.Map(dest => dest.StatusCode, (src) =>
|
||||
//src.IsOnline ? StatusCodes.Good : StatusCodes.Bad);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建服务目录结构
|
||||
/// </summary>
|
||||
/// <param name="externalReferences"></param>
|
||||
public override void CreateAddressSpace(IDictionary<NodeId, IList<IReference>> externalReferences)
|
||||
{
|
||||
lock (Lock)
|
||||
{
|
||||
if (!externalReferences.TryGetValue(ObjectIds.ObjectsFolder, out IList<IReference> references))
|
||||
{
|
||||
externalReferences[ObjectIds.ObjectsFolder] = references = new List<IReference>();
|
||||
}
|
||||
//首节点
|
||||
FolderState rootFolder = CreateFolder(null, "ThingsGateway", "ThingsGateway");
|
||||
rootFolder.AddReference(ReferenceTypes.Organizes, true, ObjectIds.ObjectsFolder);
|
||||
references.Add(new NodeStateReference(ReferenceTypes.Organizes, false, rootFolder.NodeId));
|
||||
rootFolder.EventNotifier = EventNotifiers.SubscribeToEvents;
|
||||
AddRootNotifier(rootFolder);
|
||||
|
||||
//创建设备树
|
||||
var _geviceGroup = _businessBase.VariableRuntimes.Select(a => a.Value)
|
||||
.GroupBy(a => a.DeviceName);
|
||||
// 开始寻找设备信息,并计算一些节点信息
|
||||
foreach (var item in _geviceGroup)
|
||||
{
|
||||
//设备树会有两层
|
||||
FolderState fs = CreateFolder(rootFolder, item.Key, item.Key);
|
||||
fs.AddReference(ReferenceTypes.Organizes, true, ObjectIds.ObjectsFolder);
|
||||
fs.EventNotifier = EventNotifiers.SubscribeToEvents;
|
||||
if (item?.Count() > 0)
|
||||
{
|
||||
foreach (var item2 in item)
|
||||
{
|
||||
CreateVariable(fs, item2);
|
||||
}
|
||||
}
|
||||
}
|
||||
AddPredefinedNode(SystemContext, rootFolder);
|
||||
}
|
||||
}
|
||||
|
||||
///// <summary>
|
||||
///// 读取历史数据
|
||||
///// </summary>
|
||||
//public override void HistoryRead(OperationContext context,
|
||||
// HistoryReadDetails details,
|
||||
// TimestampsToReturn timestampsToReturn,
|
||||
// bool releaseContinuationPoints,
|
||||
// IList<HistoryReadValueId> nodesToRead,
|
||||
// IList<HistoryReadResult> results,
|
||||
// IList<ServiceResult> errors)
|
||||
//{
|
||||
// base.HistoryRead(context, details, timestampsToReturn, releaseContinuationPoints, nodesToRead, results, errors);
|
||||
// //必须带有时间范围
|
||||
// if (details is not ReadRawModifiedDetails readDetail || readDetail.StartTime == DateTime.MinValue || readDetail.EndTime == DateTime.MinValue)
|
||||
// {
|
||||
// errors[0] = StatusCodes.BadHistoryOperationUnsupported;
|
||||
// return;
|
||||
// }
|
||||
// var service = BackgroundServiceUtil.GetBackgroundService<HistoryValueWorker>();
|
||||
// if (!service.StatuString.IsSuccess)
|
||||
// {
|
||||
// errors[0] = StatusCodes.BadHistoryOperationUnsupported;
|
||||
// return;
|
||||
// }
|
||||
|
||||
// var db = service.GetHistoryDbAsync().GetAwaiter().GetResult();
|
||||
// if (!db.IsSuccess)
|
||||
// {
|
||||
// errors[0] = StatusCodes.BadHistoryOperationUnsupported;
|
||||
// return;
|
||||
// }
|
||||
// var startTime = readDetail.StartTime;
|
||||
// var endTime = readDetail.EndTime;
|
||||
|
||||
// for (int i = 0; i < nodesToRead.Count; i++)
|
||||
// {
|
||||
// var historyRead = nodesToRead[i];
|
||||
// if (NodeIdTags.TryGetValue(historyRead.NodeId, out OPCUATag tag))
|
||||
// {
|
||||
// var data = db.Content.Queryable<HistoryValue>()
|
||||
// .Where(a => a.Name == tag.SymbolicName)
|
||||
// .Where(a => a.CollectTime >= startTime)
|
||||
// .Where(a => a.CollectTime <= endTime)
|
||||
// .ToList();
|
||||
|
||||
// if (data.Count > 0)
|
||||
// {
|
||||
// var hisDataValue = data.Adapt<List<DataValue>>(_config);
|
||||
// HistoryData hisData = new();
|
||||
// hisData.DataValues.AddRange(hisDataValue);
|
||||
// errors[i] = StatusCodes.Good;
|
||||
// //切记Processed设为true,否则客户端会报错
|
||||
// historyRead.Processed = true;
|
||||
// results[i] = new HistoryReadResult()
|
||||
// {
|
||||
// StatusCode = StatusCodes.Good,
|
||||
// HistoryData = new ExtensionObject(hisData)
|
||||
// };
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// results[i] = new HistoryReadResult()
|
||||
// {
|
||||
// StatusCode = StatusCodes.GoodNoData
|
||||
// };
|
||||
// }
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// results[i] = new HistoryReadResult()
|
||||
// {
|
||||
// StatusCode = StatusCodes.BadNotFound
|
||||
// };
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override NodeId New(ISystemContext context, NodeState node)
|
||||
{
|
||||
if (node is BaseInstanceState instance && instance.Parent != null)
|
||||
{
|
||||
string id = instance.Parent.NodeId.Identifier?.ToString();
|
||||
if (id != null)
|
||||
{
|
||||
//用下划线分割
|
||||
return new NodeId(id + "_" + instance.SymbolicName, instance.Parent.NodeId.NamespaceIndex);
|
||||
}
|
||||
}
|
||||
return node.NodeId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新变量
|
||||
/// </summary>
|
||||
/// <param name="variable"></param>
|
||||
public void UpVariable(VariableData variable)
|
||||
{
|
||||
if (!NodeIdTags.TryGetValue(variable.Name, out var uaTag))
|
||||
return;
|
||||
object initialItemValue = null;
|
||||
initialItemValue = variable.Value;
|
||||
if (initialItemValue != null)
|
||||
{
|
||||
var code = variable.IsOnline ? StatusCodes.Good : StatusCodes.Bad;
|
||||
if (code == StatusCodes.Good)
|
||||
{
|
||||
ChangeNodeData(uaTag, initialItemValue, variable.ChangeTime);
|
||||
}
|
||||
|
||||
if (uaTag.StatusCode != code)
|
||||
{
|
||||
uaTag.StatusCode = code;
|
||||
}
|
||||
uaTag.UpdateChangeMasks(NodeStateChangeMasks.Value);
|
||||
uaTag.ClearChangeMasks(SystemContext, false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <inheritdoc/>
|
||||
/// </summary>
|
||||
/// <param name="disposing"></param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
NodeIdTags.Clear();
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
private static NodeId DataNodeType(Type tp)
|
||||
{
|
||||
if (tp == typeof(bool))
|
||||
return DataTypeIds.Boolean;
|
||||
if (tp == typeof(byte))
|
||||
return DataTypeIds.Byte;
|
||||
if (tp == typeof(sbyte))
|
||||
return DataTypeIds.SByte;
|
||||
if (tp == typeof(short))
|
||||
return DataTypeIds.Int16;
|
||||
if (tp == typeof(ushort))
|
||||
return DataTypeIds.UInt16;
|
||||
if (tp == typeof(int))
|
||||
return DataTypeIds.Int32;
|
||||
if (tp == typeof(uint))
|
||||
return DataTypeIds.UInt32;
|
||||
if (tp == typeof(long))
|
||||
return DataTypeIds.Int64;
|
||||
if (tp == typeof(ulong))
|
||||
return DataTypeIds.UInt64;
|
||||
if (tp == typeof(float))
|
||||
return DataTypeIds.Float;
|
||||
if (tp == typeof(double))
|
||||
return DataTypeIds.Double;
|
||||
if (tp == typeof(string))
|
||||
return DataTypeIds.String;
|
||||
if (tp == typeof(DateTime))
|
||||
return DataTypeIds.DateTime;
|
||||
return DataTypeIds.String;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在服务器端直接更改对应数据节点的值
|
||||
/// </summary>
|
||||
private void ChangeNodeData(OpcUaTag tag, object value, DateTime dateTime)
|
||||
{
|
||||
object newValue;
|
||||
try
|
||||
{
|
||||
if (!tag.IsDataTypeInit)
|
||||
{
|
||||
if (tag.DataType == DataTypeIds.String)
|
||||
{
|
||||
if (value != null)
|
||||
{
|
||||
SetDataType(tag, value);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
SetRank(tag, value);
|
||||
}
|
||||
}
|
||||
var jToken = JToken.FromObject((tag.DataType == DataTypeIds.String ? value?.ToString() : value));
|
||||
var dataValue = JsonUtils.DecoderObject(
|
||||
Server.MessageContext,
|
||||
tag.DataType,
|
||||
TypeInfo.GetBuiltInType(tag.DataType, SystemContext.TypeTable),
|
||||
jToken.CalculateActualValueRank(),
|
||||
jToken
|
||||
);
|
||||
newValue = dataValue;
|
||||
success = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (success)
|
||||
_businessBase.LogMessage.LogWarning(ex, "Conversion value error");
|
||||
success = false;
|
||||
newValue = value;
|
||||
}
|
||||
tag.Value = newValue;
|
||||
tag.Timestamp = dateTime;
|
||||
|
||||
void SetDataType(OpcUaTag tag, object value)
|
||||
{
|
||||
tag.IsDataTypeInit = true;
|
||||
var tp = value.GetType();
|
||||
if (tp == typeof(JArray))
|
||||
{
|
||||
try
|
||||
{
|
||||
tp = ((JValue)((JArray)value).FirstOrDefault()).Value.GetType();
|
||||
tag.ValueRank = ValueRanks.OneOrMoreDimensions;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
if (tp == typeof(JValue))
|
||||
{
|
||||
tp = ((JValue)value).Value.GetType();
|
||||
tag.ValueRank = ValueRanks.Scalar;
|
||||
}
|
||||
tag.DataType = DataNodeType(tp);
|
||||
tag.ClearChangeMasks(SystemContext, false);
|
||||
}
|
||||
|
||||
void SetRank(OpcUaTag tag, object value)
|
||||
{
|
||||
tag.IsDataTypeInit = true;
|
||||
var tp = value.GetType();
|
||||
if (tp == typeof(JArray))
|
||||
{
|
||||
try
|
||||
{
|
||||
tp = ((JValue)((JArray)value).FirstOrDefault()).Value.GetType();
|
||||
tag.ValueRank = ValueRanks.OneOrMoreDimensions;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
tag.ClearChangeMasks(SystemContext, false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建文件夹
|
||||
/// </summary>
|
||||
private FolderState CreateFolder(NodeState parent, string name, string description)
|
||||
{
|
||||
FolderState folder = new(parent)
|
||||
{
|
||||
SymbolicName = name,
|
||||
ReferenceTypeId = ReferenceTypes.Organizes,
|
||||
TypeDefinitionId = ObjectTypeIds.FolderType,
|
||||
Description = description,
|
||||
NodeId = new NodeId(name, NamespaceIndex),
|
||||
BrowseName = new QualifiedName(name, NamespaceIndex),
|
||||
DisplayName = new LocalizedText(name),
|
||||
WriteMask = AttributeWriteMask.None,
|
||||
UserWriteMask = AttributeWriteMask.None,
|
||||
EventNotifier = EventNotifiers.None
|
||||
};
|
||||
|
||||
parent?.AddChild(folder);
|
||||
|
||||
return folder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个值节点,类型需要在创建的时候指定
|
||||
/// </summary>
|
||||
private OpcUaTag CreateVariable(NodeState parent, VariableRuntime variableRuntime)
|
||||
{
|
||||
OpcUaTag variable = new(parent)
|
||||
{
|
||||
SymbolicName = variableRuntime.Name,
|
||||
ReferenceTypeId = ReferenceTypes.Organizes,
|
||||
TypeDefinitionId = VariableTypeIds.BaseDataVariableType,
|
||||
NodeId = new NodeId(variableRuntime.Name, NamespaceIndex),
|
||||
Description = variableRuntime.Description,
|
||||
BrowseName = new QualifiedName(variableRuntime.Name, NamespaceIndex),
|
||||
DisplayName = new LocalizedText(variableRuntime.Name),
|
||||
WriteMask = AttributeWriteMask.DisplayName | AttributeWriteMask.Description,
|
||||
UserWriteMask = AttributeWriteMask.DisplayName | AttributeWriteMask.Description,
|
||||
ValueRank = ValueRanks.Scalar,
|
||||
Id = variableRuntime.Id,
|
||||
DataType = DataNodeType(variableRuntime)
|
||||
};
|
||||
var level = ThingsGatewayNodeManager.ProtectTypeTrans(variableRuntime);
|
||||
variable.AccessLevel = level;
|
||||
variable.UserAccessLevel = level;
|
||||
variable.Historizing = false;//历史存储状态
|
||||
variable.Value = Opc.Ua.TypeInfo.GetDefaultValue(variable.DataType, ValueRanks.Any, Server.TypeTree);
|
||||
var code = variableRuntime.IsOnline ? StatusCodes.Good : StatusCodes.Bad;
|
||||
variable.StatusCode = code;
|
||||
variable.Timestamp = variableRuntime.CollectTime ?? DateTime.MinValue;
|
||||
variable.OnWriteValue = OnWriteDataValue;
|
||||
parent?.AddChild(variable);
|
||||
NodeIdTags.AddOrUpdate(variable.SymbolicName, variable);
|
||||
return variable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 网关转OPC数据类型
|
||||
/// </summary>
|
||||
/// <param name="variableRuntime"></param>
|
||||
/// <returns></returns>
|
||||
private NodeId DataNodeType(VariableRuntime variableRuntime)
|
||||
{
|
||||
var str = variableRuntime.GetPropertyValue(_businessBase.DeviceId, nameof(OpcUaServerVariableProperty.DataType)) ?? "";
|
||||
Type tp;
|
||||
if (Enum.TryParse(str, out DataTypeEnum result))
|
||||
{
|
||||
tp = result.GetSystemType();
|
||||
}
|
||||
else
|
||||
{
|
||||
tp = variableRuntime.DataType.GetSystemType(); ;
|
||||
}
|
||||
|
||||
return DataNodeType(tp);
|
||||
}
|
||||
|
||||
private ServiceResult OnWriteDataValue(ISystemContext context, NodeState node, NumericRange indexRange, QualifiedName dataEncoding, ref object value, ref StatusCode statusCode, ref DateTime timestamp)
|
||||
{
|
||||
try
|
||||
{
|
||||
var context1 = context as ServerSystemContext;
|
||||
|
||||
//取消注释,插件不限制匿名用户的写入
|
||||
//if (context1.UserIdentity.TokenType == UserTokenType.Anonymous)
|
||||
//{
|
||||
// return StatusCodes.BadUserAccessDenied;
|
||||
//}
|
||||
OpcUaTag variable = node as OpcUaTag;
|
||||
if (NodeIdTags.TryGetValue(variable.SymbolicName, out OpcUaTag tag))
|
||||
{
|
||||
if (StatusCode.IsGood(variable.StatusCode))
|
||||
{
|
||||
//仅当指定了值时才将值写入
|
||||
if (variable.Value != null)
|
||||
{
|
||||
var result = GlobalData.RpcService.InvokeDeviceMethodAsync("OpcUaSlave - " + context1?.OperationContext?.Session?.Identity?.DisplayName,
|
||||
new()
|
||||
{
|
||||
{ variable.SymbolicName, value?.ToString() }
|
||||
}
|
||||
|
||||
).ConfigureAwait(true).GetAwaiter().GetResult();
|
||||
if (result.Values.FirstOrDefault().IsSuccess)
|
||||
{
|
||||
return StatusCodes.Good;
|
||||
}
|
||||
else
|
||||
{
|
||||
return new(StatusCodes.BadWaitingForResponse, result.Values.FirstOrDefault().ErrorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return StatusCodes.BadWaitingForResponse;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return StatusCodes.BadTypeMismatch;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte ProtectTypeTrans(VariableRuntime variableRuntime)
|
||||
{
|
||||
byte result = 0;
|
||||
result = variableRuntime.ProtectType switch
|
||||
{
|
||||
ProtectTypeEnum.ReadOnly => (byte)(result | AccessLevels.CurrentRead),
|
||||
ProtectTypeEnum.ReadWrite => (byte)(result | AccessLevels.CurrentReadOrWrite),
|
||||
_ => (byte)(result | AccessLevels.CurrentRead),
|
||||
};
|
||||
//if (variableRuntime.HistoryEnable)
|
||||
//{
|
||||
// result = (byte)(result | AccessLevels.HistoryRead);
|
||||
//}
|
||||
return result;
|
||||
}
|
||||
}
|
@@ -0,0 +1,328 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
|
||||
// 此代码版权(除特别声明外的代码)归作者本人Diego所有
|
||||
// 源代码使用协议遵循本仓库的开源协议及附加协议
|
||||
// Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
|
||||
// Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
|
||||
// 使用文档:https://thingsgateway.cn/
|
||||
// QQ群:605534569
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using Opc.Ua;
|
||||
using Opc.Ua.Server;
|
||||
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
using ThingsGateway.Admin.Application;
|
||||
using ThingsGateway.Gateway.Application;
|
||||
|
||||
using TouchSocket.Core;
|
||||
|
||||
namespace ThingsGateway.Plugin.OpcUa105;
|
||||
|
||||
/// <summary>
|
||||
/// UAServer核心实现
|
||||
/// </summary>
|
||||
public partial class ThingsGatewayServer : StandardServer
|
||||
{
|
||||
/// <summary>
|
||||
/// 自定义节点
|
||||
/// </summary>
|
||||
public ThingsGatewayNodeManager NodeManager;
|
||||
|
||||
private readonly BusinessBase _businessBase;
|
||||
|
||||
private ICertificateValidator m_userCertificateValidator;
|
||||
|
||||
/// <inheritdoc cref="ThingsGatewayServer"/>
|
||||
public ThingsGatewayServer(BusinessBase businessBase)
|
||||
{
|
||||
_businessBase = businessBase;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override UserTokenPolicyCollection GetUserTokenPolicies(ApplicationConfiguration configuration, EndpointDescription description)
|
||||
{
|
||||
var policies = base.GetUserTokenPolicies(configuration, description);
|
||||
|
||||
// 样品如何修改默认用户令牌的政策
|
||||
if (description.SecurityPolicyUri == SecurityPolicies.Aes256_Sha256_RsaPss &&
|
||||
description.SecurityMode == MessageSecurityMode.SignAndEncrypt)
|
||||
{
|
||||
policies = new UserTokenPolicyCollection(policies.Where(u => u.TokenType != UserTokenType.Certificate));
|
||||
}
|
||||
else if (description.SecurityPolicyUri == SecurityPolicies.Aes128_Sha256_RsaOaep &&
|
||||
description.SecurityMode == MessageSecurityMode.Sign)
|
||||
{
|
||||
policies = new UserTokenPolicyCollection(policies.Where(u => u.TokenType != UserTokenType.Anonymous));
|
||||
}
|
||||
else if (description.SecurityPolicyUri == SecurityPolicies.Aes128_Sha256_RsaOaep &&
|
||||
description.SecurityMode == MessageSecurityMode.SignAndEncrypt)
|
||||
{
|
||||
policies = new UserTokenPolicyCollection(policies.Where(u => u.TokenType != UserTokenType.UserName));
|
||||
}
|
||||
return policies;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override MasterNodeManager CreateMasterNodeManager(IServerInternal server, ApplicationConfiguration configuration)
|
||||
{
|
||||
List<INodeManager> nodeManagers = new List<INodeManager>();
|
||||
// 创建自定义节点管理器.
|
||||
NodeManager = new ThingsGatewayNodeManager(_businessBase, server, configuration);
|
||||
nodeManagers.Add(NodeManager);
|
||||
// 创建主节点管理器.
|
||||
var masterNodeManager = new MasterNodeManager(server, configuration, null, nodeManagers.ToArray());
|
||||
return masterNodeManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override ResourceManager CreateResourceManager(IServerInternal server, ApplicationConfiguration configuration)
|
||||
{
|
||||
ResourceManager resourceManager = new(server, configuration);
|
||||
|
||||
System.Reflection.FieldInfo[] fields = typeof(StatusCodes).GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
|
||||
|
||||
foreach (System.Reflection.FieldInfo field in fields)
|
||||
{
|
||||
uint? id = field.GetValue(typeof(StatusCodes)) as uint?;
|
||||
|
||||
if (id != null)
|
||||
{
|
||||
resourceManager.Add(id.Value, "en-US", field.Name);
|
||||
}
|
||||
}
|
||||
|
||||
resourceManager.Add("InvalidPassword", "zh-cn", "密码验证失败,'{0}'.");
|
||||
resourceManager.Add("UnexpectedUserTokenError", "zh-cn", "错误的用户令牌。");
|
||||
resourceManager.Add("BadUserAccessDenied", "zh-cn", "当前用户名不存在。");
|
||||
|
||||
return resourceManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
NodeManager?.SafeDispose();
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override ServerProperties LoadServerProperties()
|
||||
{
|
||||
ServerProperties properties = new()
|
||||
{
|
||||
ManufacturerName = "Diego",
|
||||
ProductName = "ThingsGateway OPCUAServer",
|
||||
ProductUri = "https://gitee.com/diego2098/ThingsGateway",
|
||||
SoftwareVersion = Utils.GetAssemblySoftwareVersion(),
|
||||
BuildNumber = Utils.GetAssemblyBuildNumber(),
|
||||
BuildDate = Utils.GetAssemblyTimestamp()
|
||||
};
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnServerStarted(IServerInternal server)
|
||||
{
|
||||
// 当用户身份改变时请求。
|
||||
server.SessionManager.ImpersonateUser += SessionManager_ImpersonateUser;
|
||||
base.OnServerStarted(server);
|
||||
_businessBase.LogMessage.LogInformation("OPCUAServer Started");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnServerStarting(ApplicationConfiguration configuration)
|
||||
{
|
||||
_businessBase.LogMessage.LogInformation("OPCUAServer Starting");
|
||||
base.OnServerStarting(configuration);
|
||||
|
||||
// 由应用程序决定如何验证用户身份令牌。
|
||||
// 此函数为 X509 身份令牌创建验证器。
|
||||
CreateUserIdentityValidators(configuration);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnServerStopping()
|
||||
{
|
||||
_businessBase.LogMessage.LogInformation("OPCUAServer Stoping");
|
||||
base.OnServerStopping();
|
||||
}
|
||||
|
||||
private void CreateUserIdentityValidators(ApplicationConfiguration configuration)
|
||||
{
|
||||
for (int ii = 0; ii < configuration.ServerConfiguration.UserTokenPolicies.Count; ii++)
|
||||
{
|
||||
UserTokenPolicy policy = configuration.ServerConfiguration.UserTokenPolicies[ii];
|
||||
|
||||
// 为证书令牌策略创建验证器。
|
||||
if (policy.TokenType == UserTokenType.Certificate)
|
||||
{
|
||||
// check if user certificate trust lists are specified in configuration.
|
||||
if (configuration.SecurityConfiguration.TrustedUserCertificates != null &&
|
||||
configuration.SecurityConfiguration.UserIssuerCertificates != null)
|
||||
{
|
||||
CertificateValidator certificateValidator = new();
|
||||
certificateValidator.Update(configuration).GetAwaiter().GetResult();
|
||||
certificateValidator.Update(configuration.SecurityConfiguration.UserIssuerCertificates,
|
||||
configuration.SecurityConfiguration.TrustedUserCertificates,
|
||||
configuration.SecurityConfiguration.RejectedCertificateStore);
|
||||
|
||||
// set custom validator for user certificates.
|
||||
m_userCertificateValidator = certificateValidator.GetChannelValidator();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SessionManager_ImpersonateUser(Session session, ImpersonateEventArgs args)
|
||||
{
|
||||
// check for a user name cancellationToken.
|
||||
|
||||
if (args.NewIdentity is UserNameIdentityToken userNameToken)
|
||||
{
|
||||
args.Identity = VerifyPassword(userNameToken);
|
||||
|
||||
// set AuthenticatedUser role for accepted user/password authentication
|
||||
args.Identity.GrantedRoleIds.Add(ObjectIds.WellKnownRole_AuthenticatedUser);
|
||||
|
||||
if (args.Identity is SystemConfigurationIdentity)
|
||||
{
|
||||
// set ConfigureAdmin role for user with permission to configure server
|
||||
args.Identity.GrantedRoleIds.Add(ObjectIds.WellKnownRole_ConfigureAdmin);
|
||||
args.Identity.GrantedRoleIds.Add(ObjectIds.WellKnownRole_SecurityAdmin);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// check for x509 user cancellationToken.
|
||||
|
||||
if (args.NewIdentity is X509IdentityToken x509Token)
|
||||
{
|
||||
VerifyUserTokenCertificate(x509Token.Certificate);
|
||||
args.Identity = new UserIdentity(x509Token);
|
||||
Utils.LogInfo(Utils.TraceMasks.Security, "X509 Token Accepted: {0}", args.Identity?.DisplayName);
|
||||
|
||||
// set AuthenticatedUser role for accepted certificate authentication
|
||||
args.Identity.GrantedRoleIds.Add(ObjectIds.WellKnownRole_AuthenticatedUser);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// check for anonymous cancellationToken.
|
||||
if (args.NewIdentity is AnonymousIdentityToken || args.NewIdentity == null)
|
||||
{
|
||||
// allow anonymous authentication and set Anonymous role for this authentication
|
||||
args.Identity = new UserIdentity();
|
||||
args.Identity.GrantedRoleIds.Add(ObjectIds.WellKnownRole_Anonymous);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// unsuported identity cancellationToken type.
|
||||
throw ServiceResultException.Create(StatusCodes.BadIdentityTokenInvalid,
|
||||
"Not supported user cancellationToken type: {0}.", args.NewIdentity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从第三方用户中校验
|
||||
/// </summary>
|
||||
/// <param name="userNameToken"></param>
|
||||
/// <returns></returns>
|
||||
private IUserIdentity VerifyPassword(UserNameIdentityToken userNameToken)
|
||||
{
|
||||
var userName = userNameToken.UserName;
|
||||
var password = userNameToken.DecryptedPassword;
|
||||
if (string.IsNullOrEmpty(userName))
|
||||
{
|
||||
// an empty username is not accepted.
|
||||
throw ServiceResultException.Create(StatusCodes.BadIdentityTokenInvalid,
|
||||
"Security cancellationToken is not a valid username cancellationToken. An empty username is not accepted.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(password))
|
||||
{
|
||||
// an empty password is not accepted.
|
||||
throw ServiceResultException.Create(StatusCodes.BadIdentityTokenRejected,
|
||||
"Security cancellationToken is not a valid username cancellationToken. An empty password is not accepted.");
|
||||
}
|
||||
var sysUserService = App.RootServices.GetService<ISysUserService>();
|
||||
var userInfo = sysUserService.GetUserByAccountAsync(userName, null).ConfigureAwait(true).GetAwaiter().GetResult();//获取用户信息
|
||||
if (userInfo == null)
|
||||
{
|
||||
// construct translation object with default text.
|
||||
TranslationInfo info = new(
|
||||
"InvalidPassword",
|
||||
"en-US",
|
||||
"Invalid username or password.",
|
||||
userName);
|
||||
|
||||
// create an exception with a vendor defined sub-code.
|
||||
throw new ServiceResultException(new ServiceResult(
|
||||
StatusCodes.BadUserAccessDenied,
|
||||
"InvalidPassword",
|
||||
LoadServerProperties().ProductUri,
|
||||
new LocalizedText(info)));
|
||||
}
|
||||
// 有权配置服务器的用户
|
||||
if (userName == userInfo.Account && password == userInfo.Password)
|
||||
{
|
||||
return new SystemConfigurationIdentity(new UserIdentity(userNameToken));
|
||||
}
|
||||
else
|
||||
{
|
||||
return new UserIdentity(userNameToken);
|
||||
}
|
||||
}
|
||||
|
||||
private void VerifyUserTokenCertificate(X509Certificate2 certificate)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (m_userCertificateValidator != null)
|
||||
{
|
||||
m_userCertificateValidator.Validate(certificate);
|
||||
}
|
||||
else
|
||||
{
|
||||
CertificateValidator.Validate(certificate);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
TranslationInfo info;
|
||||
StatusCode result = StatusCodes.BadIdentityTokenRejected;
|
||||
if (e is ServiceResultException se && se.StatusCode == StatusCodes.BadCertificateUseNotAllowed)
|
||||
{
|
||||
info = new TranslationInfo(
|
||||
"InvalidCertificate",
|
||||
"en-US",
|
||||
"'{0}' is an invalid user certificate.",
|
||||
certificate.Subject);
|
||||
|
||||
result = StatusCodes.BadIdentityTokenInvalid;
|
||||
}
|
||||
else
|
||||
{
|
||||
// construct translation object with default text.
|
||||
info = new TranslationInfo(
|
||||
"UntrustedCertificate",
|
||||
"en-US",
|
||||
"'{0}' is not a trusted user certificate.",
|
||||
certificate.Subject);
|
||||
}
|
||||
|
||||
// create an exception with a vendor defined sub-code.
|
||||
throw new ServiceResultException(new ServiceResult(
|
||||
result,
|
||||
info.Key,
|
||||
LoadServerProperties().ProductUri,
|
||||
new LocalizedText(info)));
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,391 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
|
||||
// 此代码版权(除特别声明外的代码)归作者本人Diego所有
|
||||
// 源代码使用协议遵循本仓库的开源协议及附加协议
|
||||
// Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
|
||||
// Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
|
||||
// 使用文档:https://thingsgateway.cn/
|
||||
// QQ群:605534569
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using Mapster;
|
||||
|
||||
using Microsoft.Extensions.Localization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using Opc.Ua;
|
||||
using Opc.Ua.Configuration;
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
using ThingsGateway.Extension;
|
||||
using ThingsGateway.Gateway.Application;
|
||||
using ThingsGateway.NewLife.Threading;
|
||||
|
||||
using TouchSocket.Core;
|
||||
|
||||
namespace ThingsGateway.Plugin.OpcUa105;
|
||||
|
||||
/// <summary>
|
||||
/// OPCUA服务端
|
||||
/// </summary>
|
||||
public partial class OpcUaServer : BusinessBase
|
||||
{
|
||||
private readonly OpcUaServerProperty _driverPropertys = new();
|
||||
private readonly OpcUaServerVariableProperty _variablePropertys = new();
|
||||
private ApplicationInstance m_application;
|
||||
private ApplicationConfiguration m_configuration;
|
||||
private ThingsGatewayServer m_server;
|
||||
private volatile bool success = true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override VariablePropertyBase VariablePropertys => _variablePropertys;
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override BusinessPropertyBase _businessPropertyBase => _driverPropertys;
|
||||
|
||||
protected IStringLocalizer Localizer { get; private set; }
|
||||
private ConcurrentQueue<VariableData> CollectVariableRuntimes { get; set; } = new();
|
||||
|
||||
private static readonly string[] separator = new string[] { ";" };
|
||||
|
||||
|
||||
public override void AfterVariablesChanged()
|
||||
{
|
||||
// 如果业务属性指定了全部变量,则设置当前设备的变量运行时列表和采集设备列表
|
||||
if (_driverPropertys.IsAllVariable)
|
||||
{
|
||||
VariableRuntimes = new(GlobalData.GetEnableVariables());
|
||||
|
||||
CollectDevices = GlobalData.GetEnableDevices().Where(a => a.Value.IsCollect == true).ToDictionary();
|
||||
}
|
||||
else
|
||||
{
|
||||
base.AfterVariablesChanged();
|
||||
}
|
||||
VariableRuntimes.ForEach(a =>
|
||||
{
|
||||
VariableValueChange(a.Value, a.Value.Adapt<VariableData>());
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
protected override void InitChannel(IChannel? channel = null)
|
||||
{
|
||||
ApplicationInstance.MessageDlg = new ApplicationMessageDlg(LogMessage);//默认返回true
|
||||
|
||||
//Utils.SetLogger(new OpcUaLogger(LogMessage)); //调试用途
|
||||
m_application = new ApplicationInstance();
|
||||
m_configuration = GetDefaultConfiguration();
|
||||
m_configuration.Validate(ApplicationType.Server).GetAwaiter().GetResult();
|
||||
m_application.ApplicationConfiguration = m_configuration;
|
||||
if (m_configuration.SecurityConfiguration.AutoAcceptUntrustedCertificates)
|
||||
{
|
||||
m_configuration.CertificateValidator.CertificateValidation += (s, e) =>
|
||||
{
|
||||
e.Accept = (e.Error.StatusCode == StatusCodes.BadCertificateUntrusted);
|
||||
};
|
||||
}
|
||||
|
||||
m_server = new(this);
|
||||
CollectVariableRuntimes.Clear();
|
||||
|
||||
|
||||
GlobalData.VariableValueChangeEvent += VariableValueChange;
|
||||
|
||||
Localizer = App.CreateLocalizerByType(typeof(OpcUaServer))!;
|
||||
base.InitChannel(channel);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool IsConnected()
|
||||
{
|
||||
try
|
||||
{
|
||||
return m_server?.CurrentInstance.CurrentState == Opc.Ua.ServerState.Running;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
GlobalData.VariableValueChangeEvent -= VariableValueChange;
|
||||
m_application?.Stop();
|
||||
m_server?.SafeDispose();
|
||||
CollectVariableRuntimes?.Clear();
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
protected override async Task ProtectedStartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// 启动服务器。
|
||||
await m_application.CheckApplicationInstanceCertificates(false, 1200, cancellationToken).ConfigureAwait(false);
|
||||
await m_application.Start(m_server).ConfigureAwait(false);
|
||||
await base.ProtectedStartAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
protected override async ValueTask ProtectedExecuteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (IsConnected())
|
||||
{
|
||||
//更新设备活动时间
|
||||
CurrentDevice.SetDeviceStatus(TimerX.Now, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
CurrentDevice.SetDeviceStatus(TimerX.Now, true);
|
||||
try
|
||||
{
|
||||
await m_application.CheckApplicationInstanceCertificates(false, 1200, cancellationToken).ConfigureAwait(false);
|
||||
await m_application.Start(m_server).ConfigureAwait(false);
|
||||
success = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (success)
|
||||
LogMessage.LogWarning(ex, Localizer["CanStartService"]);
|
||||
success = false;
|
||||
await Task.Delay(10000, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
var data = CollectVariableRuntimes.ToListWithDequeue();
|
||||
data.Reverse();
|
||||
////变化推送
|
||||
var varList = data.DistinctBy(a => a.Name).ToList();
|
||||
|
||||
if (varList?.Count > 0)
|
||||
{
|
||||
foreach (var item in varList)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
m_server?.NodeManager?.UpVariable(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogMessage.LogWarning(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
success = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (success)
|
||||
LogMessage.LogWarning(ex);
|
||||
success = false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private ApplicationConfiguration GetDefaultConfiguration()
|
||||
{
|
||||
ApplicationConfiguration config = new();
|
||||
var urls = _driverPropertys.OpcUaStringUrl.Split(separator, StringSplitOptions.RemoveEmptyEntries);
|
||||
// 签名及加密验证
|
||||
ServerSecurityPolicyCollection policies = new();
|
||||
var userTokens = new UserTokenPolicyCollection();
|
||||
if (_driverPropertys.SecurityPolicy)
|
||||
{
|
||||
policies.Add(new ServerSecurityPolicy()
|
||||
{
|
||||
SecurityMode = MessageSecurityMode.Sign,
|
||||
SecurityPolicyUri = SecurityPolicies.Basic128Rsa15
|
||||
});
|
||||
policies.Add(new ServerSecurityPolicy()
|
||||
{
|
||||
SecurityMode = MessageSecurityMode.Sign,
|
||||
SecurityPolicyUri = SecurityPolicies.Basic256
|
||||
});
|
||||
policies.Add(new ServerSecurityPolicy()
|
||||
{
|
||||
SecurityMode = MessageSecurityMode.Sign,
|
||||
SecurityPolicyUri = SecurityPolicies.Basic256Sha256
|
||||
});
|
||||
policies.Add(new ServerSecurityPolicy()
|
||||
{
|
||||
SecurityMode = MessageSecurityMode.Sign,
|
||||
SecurityPolicyUri = SecurityPolicies.Aes128_Sha256_RsaOaep
|
||||
});
|
||||
policies.Add(new ServerSecurityPolicy()
|
||||
{
|
||||
SecurityMode = MessageSecurityMode.Sign,
|
||||
SecurityPolicyUri = SecurityPolicies.Aes256_Sha256_RsaPss
|
||||
});
|
||||
policies.Add(new ServerSecurityPolicy()
|
||||
{
|
||||
SecurityMode = MessageSecurityMode.SignAndEncrypt,
|
||||
SecurityPolicyUri = SecurityPolicies.Basic128Rsa15
|
||||
});
|
||||
policies.Add(new ServerSecurityPolicy()
|
||||
{
|
||||
SecurityMode = MessageSecurityMode.SignAndEncrypt,
|
||||
SecurityPolicyUri = SecurityPolicies.Basic256
|
||||
});
|
||||
policies.Add(new ServerSecurityPolicy()
|
||||
{
|
||||
SecurityMode = MessageSecurityMode.SignAndEncrypt,
|
||||
SecurityPolicyUri = SecurityPolicies.Basic256Sha256
|
||||
});
|
||||
policies.Add(new ServerSecurityPolicy()
|
||||
{
|
||||
SecurityMode = MessageSecurityMode.SignAndEncrypt,
|
||||
SecurityPolicyUri = SecurityPolicies.Aes128_Sha256_RsaOaep
|
||||
});
|
||||
policies.Add(new ServerSecurityPolicy()
|
||||
{
|
||||
SecurityMode = MessageSecurityMode.SignAndEncrypt,
|
||||
SecurityPolicyUri = SecurityPolicies.Aes256_Sha256_RsaPss
|
||||
});
|
||||
|
||||
userTokens.Add(new UserTokenPolicy(UserTokenType.UserName));
|
||||
}
|
||||
else
|
||||
{
|
||||
userTokens.Add(new UserTokenPolicy(UserTokenType.UserName));
|
||||
policies.Add(new ServerSecurityPolicy()
|
||||
{
|
||||
SecurityMode = MessageSecurityMode.None,
|
||||
SecurityPolicyUri = SecurityPolicies.None
|
||||
});
|
||||
userTokens.Add(new UserTokenPolicy(UserTokenType.Anonymous));
|
||||
}
|
||||
|
||||
config.ApplicationName = "ThingsGateway OPCUAServer";
|
||||
config.ApplicationType = ApplicationType.Server;
|
||||
config.ApplicationUri = _driverPropertys.BigTextApplicationUri;
|
||||
|
||||
config.ServerConfiguration = new ServerConfiguration()
|
||||
{
|
||||
// 配置登录的地址
|
||||
BaseAddresses = urls,
|
||||
|
||||
SecurityPolicies = policies,
|
||||
UserTokenPolicies = userTokens,
|
||||
ShutdownDelay = 1,
|
||||
|
||||
MinRequestThreadCount = 50,
|
||||
MaxRequestThreadCount = 1000,
|
||||
MaxQueuedRequestCount = 20000,
|
||||
|
||||
|
||||
DiagnosticsEnabled = false, // 是否启用诊断
|
||||
MaxSessionCount = 1000, // 最大打开会话数
|
||||
MinSessionTimeout = 10000, // 允许该会话在与客户端断开时(单位毫秒)仍然保持连接的最小时间
|
||||
MaxSessionTimeout = 60000, // 允许该会话在与客户端断开时(单位毫秒)仍然保持连接的最大时间
|
||||
MaxBrowseContinuationPoints = 1000, // 用于Browse / BrowseNext操作的连续点的最大数量。
|
||||
MaxQueryContinuationPoints = 1000, // 用于Query / QueryNext操作的连续点的最大数量
|
||||
MaxHistoryContinuationPoints = 500, // 用于HistoryRead操作的最大连续点数。
|
||||
MaxRequestAge = 1000000, // 传入请求的最大年龄(旧请求被拒绝)。
|
||||
MinPublishingInterval = 50, // 服务器支持的最小发布间隔(以毫秒为单位)
|
||||
MaxPublishingInterval = 3600000, // 服务器支持的最大发布间隔(以毫秒为单位)1小时
|
||||
PublishingResolution = 50, // 支持的发布间隔(以毫秒为单位)的最小差异
|
||||
MaxSubscriptionLifetime = 3600000, // 订阅将在没有客户端发布的情况下保持打开多长时间 1小时
|
||||
MaxMessageQueueSize = 100, // 每个订阅队列中保存的最大消息数
|
||||
MaxNotificationQueueSize = 100, // 为每个被监视项目保存在队列中的最大证书数
|
||||
MaxNotificationsPerPublish = 1000, // 每次发布的最大通知数
|
||||
MinMetadataSamplingInterval = 1000, // 元数据的最小采样间隔
|
||||
MaxRegistrationInterval = -1, // 两次注册尝试之间的最大时间(以毫秒为单位)//不提供注册
|
||||
|
||||
MinSubscriptionLifetime = 1000,
|
||||
MaxPublishRequestCount = 200,
|
||||
MaxSubscriptionCount = 1000,
|
||||
MaxEventQueueSize = 50000,
|
||||
|
||||
|
||||
MaxTrustListSize = 0,
|
||||
MultiCastDnsEnabled = false,
|
||||
|
||||
};
|
||||
config.SecurityConfiguration = new SecurityConfiguration()
|
||||
{
|
||||
AddAppCertToTrustedStore = true,
|
||||
SendCertificateChain = true,
|
||||
AutoAcceptUntrustedCertificates = _driverPropertys.AutoAcceptUntrustedCertificates,
|
||||
UseValidatedCertificates = true,
|
||||
RejectSHA1SignedCertificates = false,
|
||||
RejectUnknownRevocationStatus = true,
|
||||
MinimumCertificateKeySize = 1024,
|
||||
SuppressNonceValidationErrors = true,
|
||||
ApplicationCertificate = new CertificateIdentifier()
|
||||
{
|
||||
StoreType = CertificateStoreType.X509Store,
|
||||
StorePath = "CurrentUser\\UAServer_ThingsGateway",
|
||||
SubjectName = _driverPropertys.BigTextSubjectName,
|
||||
//ValidationOptions = CertificateValidationOptions.SuppressHostNameInvalid,
|
||||
},
|
||||
|
||||
TrustedPeerCertificates = new CertificateTrustList()
|
||||
{
|
||||
StoreType = CertificateStoreType.Directory,
|
||||
StorePath = AppContext.BaseDirectory + @"OPCUAServerCertificate\pki\trustedPeer",
|
||||
},
|
||||
|
||||
TrustedIssuerCertificates = new CertificateTrustList()
|
||||
{
|
||||
StoreType = CertificateStoreType.Directory,
|
||||
StorePath = AppContext.BaseDirectory + @"OPCUAServerCertificate\pki\trustedIssuer",
|
||||
},
|
||||
|
||||
RejectedCertificateStore = new CertificateStoreIdentifier()
|
||||
{
|
||||
StoreType = CertificateStoreType.Directory,
|
||||
StorePath = AppContext.BaseDirectory + @"OPCUAServerCertificate\pki\rejected",
|
||||
},
|
||||
UserIssuerCertificates = new CertificateTrustList
|
||||
{
|
||||
StoreType = CertificateStoreType.Directory,
|
||||
StorePath = AppContext.BaseDirectory + @"OPCUAServerCertificate\pki\issuerUser",
|
||||
},
|
||||
TrustedUserCertificates = new CertificateTrustList
|
||||
{
|
||||
StoreType = CertificateStoreType.Directory,
|
||||
StorePath = AppContext.BaseDirectory + @"OPCUAServerCertificate\pki\trustedUser",
|
||||
}
|
||||
};
|
||||
|
||||
config.TransportConfigurations = new TransportConfigurationCollection();
|
||||
config.TransportQuotas = new TransportQuotas
|
||||
{
|
||||
OperationTimeout = 6000000,
|
||||
MaxStringLength = int.MaxValue,
|
||||
MaxByteStringLength = int.MaxValue,
|
||||
MaxArrayLength = 65535,
|
||||
MaxMessageSize = 419430400,
|
||||
MaxBufferSize = 65535,
|
||||
ChannelLifetime = 300000,
|
||||
SecurityTokenLifetime = 3600000
|
||||
};
|
||||
config.ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 60000 };
|
||||
config.TraceConfiguration = new TraceConfiguration();
|
||||
|
||||
config.CertificateValidator = new CertificateValidator();
|
||||
config.CertificateValidator.Update(config).GetAwaiter().GetResult();
|
||||
config.Extensions = new XmlElementCollection();
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
private void VariableValueChange(VariableRuntime variableRuntime, VariableData variableData)
|
||||
{
|
||||
if (CurrentDevice.Pause)
|
||||
return;
|
||||
if (DisposedValue) return;
|
||||
if (VariableRuntimes.ContainsKey(variableData.Name))
|
||||
CollectVariableRuntimes.Enqueue(variableData);
|
||||
}
|
||||
}
|
@@ -0,0 +1,56 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
|
||||
// 此代码版权(除特别声明外的代码)归作者本人Diego所有
|
||||
// 源代码使用协议遵循本仓库的开源协议及附加协议
|
||||
// Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
|
||||
// Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
|
||||
// 使用文档:https://thingsgateway.cn/
|
||||
// QQ群:605534569
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using BootstrapBlazor.Components;
|
||||
|
||||
using Opc.Ua;
|
||||
|
||||
using ThingsGateway.Gateway.Application;
|
||||
|
||||
namespace ThingsGateway.Plugin.OpcUa105;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public class OpcUaServerProperty : BusinessPropertyBase
|
||||
{
|
||||
[DynamicProperty]
|
||||
public bool IsAllVariable { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 服务地址
|
||||
/// </summary>
|
||||
[DynamicProperty(Remark = "分号分割数组,可设置多个url")]
|
||||
public string OpcUaStringUrl { get; set; } = "opc.tcp://127.0.0.1:49321";
|
||||
|
||||
/// <summary>
|
||||
/// SubjectName
|
||||
/// </summary>
|
||||
[DynamicProperty()]
|
||||
[AutoGenerateColumn(ComponentType = typeof(Textarea), Rows = 1)]
|
||||
public string BigTextSubjectName { get; set; } = "CN=ThingsGateway OPCUAServer, C=CN, S=GUANGZHOU, O=ThingsGateway, DC=" + System.Net.Dns.GetHostName();
|
||||
|
||||
/// <summary>
|
||||
/// ApplicationUri
|
||||
/// </summary>
|
||||
[AutoGenerateColumn(ComponentType = typeof(Textarea), Rows = 1)]
|
||||
[DynamicProperty()]
|
||||
public string BigTextApplicationUri { get; set; } = Utils.Format(@"urn:{0}:thingsgatewayopcuaserver", System.Net.Dns.GetHostName());
|
||||
|
||||
/// <summary>
|
||||
/// 安全策略
|
||||
/// </summary>
|
||||
[DynamicProperty()]
|
||||
public bool SecurityPolicy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 接受不受信任的证书
|
||||
/// </summary>
|
||||
[DynamicProperty()]
|
||||
public bool AutoAcceptUntrustedCertificates { get; set; } = true;
|
||||
}
|
@@ -0,0 +1,23 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
|
||||
// 此代码版权(除特别声明外的代码)归作者本人Diego所有
|
||||
// 源代码使用协议遵循本仓库的开源协议及附加协议
|
||||
// Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
|
||||
// Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
|
||||
// 使用文档:https://thingsgateway.cn/
|
||||
// QQ群:605534569
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using ThingsGateway.Gateway.Application;
|
||||
|
||||
namespace ThingsGateway.Plugin.OpcUa105;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public class OpcUaServerVariableProperty : VariablePropertyBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 数据类型
|
||||
/// </summary>
|
||||
[DynamicProperty()]
|
||||
public DataTypeEnum DataType { get; set; } = DataTypeEnum.Object;
|
||||
}
|
@@ -0,0 +1,59 @@
|
||||
@namespace ThingsGateway.Debug
|
||||
@using BootstrapBlazor.Components
|
||||
@using Microsoft.AspNetCore.Components.Web;
|
||||
@using System.Reflection;
|
||||
|
||||
@using Opc.Ua
|
||||
@using ThingsGateway.Foundation.OpcUa105;
|
||||
|
||||
<div class="row ">
|
||||
|
||||
<div class="col-12 col-md-6 p-1" style="min-height:500px;max-height:80vh;overflow: auto;">
|
||||
<TreeView TItem="OpcUaTagModel" Items="Items" ShowIcon="true" ShowCheckbox="true" AutoCheckParent="true" AutoCheckChildren="true" IsVirtualize="true"
|
||||
OnExpandNodeAsync=OnExpandNodeAsync OnTreeItemChecked="OnTreeItemChecked" OnTreeItemClick=@(async a=>
|
||||
{
|
||||
if(a?.Value?.Tag?.NodeId!=null)
|
||||
{
|
||||
ClickItem=a;
|
||||
NodeAttributes = Plc.ReadNoteAttributes(ClickItem.Value.NodeId.ToString());
|
||||
}
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}) ShowSkeleton=ShowSkeleton
|
||||
IsAccordion ClickToggleNode ModelEqualityComparer="OpcUaImportVariable.ModelEqualityComparer" />
|
||||
</div>
|
||||
<div class="col-12 col-md-6 p-2" style="min-height:500px;max-height:80vh;overflow: auto;">
|
||||
|
||||
@if (ClickItem?.Value?.Tag?.NodeId != null && NodeAttributes != null)
|
||||
{
|
||||
<Display @bind-Value=@ClickItem.Value.Tag.NodeId DisplayText="NodeId" ShowLabel="true" />
|
||||
foreach (var item in NodeAttributes)
|
||||
{
|
||||
<Display class=@($"{(StatusCode.IsBad(item.StatusCode)?"red--text":"green--text")}") @bind-Value=@item.Value DisplayText=@item.Name ShowLabel="true" />
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
@{
|
||||
#if Plugin
|
||||
}
|
||||
<div class="form-footer">
|
||||
<Button IsAsync Color="Color.Secondary" Icon="fa-solid fa-xmark" Text="@OpcUaPropertyLocalizer["Close"]" OnClickWithoutRender="OnClickClose" />
|
||||
<Button IsAsync Color="Color.Primary" Icon="fa-solid fa-check" Text="@OpcUaPropertyLocalizer["Export"]" OnClickWithoutRender="OnClickExport" />
|
||||
|
||||
<Button IsAsync Color="Color.Primary" Icon="fa-solid fa-check" Text="@OpcUaPropertyLocalizer["Save"]" OnClickWithoutRender="OnClickSave" />
|
||||
</div>
|
||||
@{
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
|
||||
@code {
|
||||
RenderFragment<OpcUaTagModel> RenderTreeItem => item =>
|
||||
@<span class="flex-fill">@item.Name</span>
|
||||
;
|
||||
private OPCNodeAttribute[] NodeAttributes = new OPCNodeAttribute[] { };
|
||||
private TreeViewItem<OpcUaTagModel> ClickItem;
|
||||
}
|
@@ -0,0 +1,429 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
|
||||
// 此代码版权(除特别声明外的代码)归作者本人Diego所有
|
||||
// 源代码使用协议遵循本仓库的开源协议及附加协议
|
||||
// Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
|
||||
// Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
|
||||
// 使用文档:https://thingsgateway.cn/
|
||||
// QQ群:605534569
|
||||
//------------------------------------------------------------------------------
|
||||
#pragma warning disable CA2007 // 考虑对等待的任务调用 ConfigureAwait
|
||||
|
||||
using BootstrapBlazor.Components;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Localization;
|
||||
|
||||
using Opc.Ua;
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
using ThingsGateway.Extension;
|
||||
using ThingsGateway.Foundation.OpcUa105;
|
||||
|
||||
#if Plugin
|
||||
|
||||
using ThingsGateway.Gateway.Application;
|
||||
using ThingsGateway.Plugin.OpcUa105;
|
||||
|
||||
#endif
|
||||
|
||||
using ThingsGateway.Razor;
|
||||
|
||||
using TouchSocket.Core;
|
||||
|
||||
namespace ThingsGateway.Debug;
|
||||
|
||||
/// <summary>
|
||||
/// 导入变量
|
||||
/// </summary>
|
||||
public partial class OpcUaImportVariable
|
||||
{
|
||||
private List<TreeViewItem<OpcUaTagModel>> Items = new();
|
||||
private IEnumerable<OpcUaTagModel> Nodes;
|
||||
private bool ShowSkeleton = true;
|
||||
|
||||
/// <summary>
|
||||
/// Opc对象
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public ThingsGateway.Foundation.OpcUa105.OpcUaMaster Plc { get; set; }
|
||||
|
||||
[CascadingParameter]
|
||||
private Func<Task>? OnCloseAsync { get; set; }
|
||||
|
||||
[Inject]
|
||||
[NotNull]
|
||||
private IStringLocalizer<OpcUaProperty>? OpcUaPropertyLocalizer { get; set; }
|
||||
|
||||
[Inject]
|
||||
[NotNull]
|
||||
private ToastService? ToastService { get; set; }
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
Items = BuildTreeItemList(await PopulateBranchAsync(ObjectIds.ObjectsFolder), RenderTreeItem).ToList();
|
||||
ShowSkeleton = false;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
});
|
||||
}
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建树节点,传入的列表已经是树结构
|
||||
/// </summary>
|
||||
private static IEnumerable<TreeViewItem<OpcUaTagModel>> BuildTreeItemList(IEnumerable<OpcUaTagModel> opcUaTagModels, Microsoft.AspNetCore.Components.RenderFragment<OpcUaTagModel> render = null, TreeViewItem<OpcUaTagModel>? parent = null)
|
||||
{
|
||||
if (opcUaTagModels == null) return Enumerable.Empty<TreeViewItem<OpcUaTagModel>>();
|
||||
var trees = new List<TreeViewItem<OpcUaTagModel>>();
|
||||
foreach (var node in opcUaTagModels)
|
||||
{
|
||||
if (node == null) continue;
|
||||
var item = new TreeViewItem<OpcUaTagModel>(node)
|
||||
{
|
||||
Text = node.Name,
|
||||
Parent = parent,
|
||||
IsExpand = false,
|
||||
Template = render,
|
||||
HasChildren = node.Children != null,
|
||||
};
|
||||
item.Items = BuildTreeItemList(node.Children, render, item).ToList();
|
||||
trees.Add(item);
|
||||
}
|
||||
return trees;
|
||||
}
|
||||
|
||||
private static bool ModelEqualityComparer(OpcUaTagModel x, OpcUaTagModel y) => x.NodeId == y.NodeId;
|
||||
|
||||
private async Task<IEnumerable<TreeViewItem<OpcUaTagModel>>> OnExpandNodeAsync(TreeViewItem<OpcUaTagModel> treeViewItem)
|
||||
{
|
||||
var data = BuildTreeItemList(await PopulateBranchAsync(treeViewItem.Value.NodeId), RenderTreeItem);
|
||||
return data;
|
||||
}
|
||||
|
||||
private Task OnTreeItemChecked(List<TreeViewItem<OpcUaTagModel>> items)
|
||||
{
|
||||
Nodes = items.Select(a => a.Value);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task PopulateBranch(OpcUaTagModel model)
|
||||
{
|
||||
if (model.Children != null)
|
||||
{
|
||||
if (model.Children.Count == 0)
|
||||
{
|
||||
model.Children = await PopulateBranchAsync((NodeId)model.Tag.NodeId);
|
||||
}
|
||||
foreach (var item in model.Children)
|
||||
{
|
||||
await PopulateBranch(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ReferenceDescriptionCollection> GetReferenceDescriptionCollectionAsync(NodeId sourceId)
|
||||
{
|
||||
BrowseDescription nodeToBrowse1 = new()
|
||||
{
|
||||
NodeId = sourceId,
|
||||
BrowseDirection = BrowseDirection.Forward,
|
||||
ReferenceTypeId = ReferenceTypeIds.Aggregates,
|
||||
IncludeSubtypes = true,
|
||||
NodeClassMask = (uint)(NodeClass.Object | NodeClass.Variable | NodeClass.Method | NodeClass.ReferenceType | NodeClass.ObjectType | NodeClass.View | NodeClass.VariableType | NodeClass.DataType),
|
||||
ResultMask = (uint)BrowseResultMask.All
|
||||
};
|
||||
|
||||
BrowseDescription nodeToBrowse2 = new()
|
||||
{
|
||||
NodeId = sourceId,
|
||||
BrowseDirection = BrowseDirection.Forward,
|
||||
ReferenceTypeId = ReferenceTypeIds.Organizes,
|
||||
IncludeSubtypes = true,
|
||||
NodeClassMask = (uint)(NodeClass.Object | NodeClass.Variable | NodeClass.Method | NodeClass.View | NodeClass.ReferenceType | NodeClass.ObjectType | NodeClass.VariableType | NodeClass.DataType),
|
||||
ResultMask = (uint)BrowseResultMask.All
|
||||
};
|
||||
|
||||
BrowseDescriptionCollection nodesToBrowse = new()
|
||||
{
|
||||
nodeToBrowse1,
|
||||
nodeToBrowse2
|
||||
};
|
||||
|
||||
ReferenceDescriptionCollection references = await OpcUaUtils.BrowseAsync(Plc.Session, nodesToBrowse, false);
|
||||
return references;
|
||||
}
|
||||
|
||||
[Parameter]
|
||||
public bool ShowSubvariable { get; set; }
|
||||
|
||||
private async Task<List<OpcUaTagModel>> PopulateBranchAsync(NodeId sourceId, bool isAll = false)
|
||||
{
|
||||
if (!Plc.Connected)
|
||||
{
|
||||
return new() { };
|
||||
}
|
||||
List<OpcUaTagModel> nodes = new()
|
||||
{
|
||||
};
|
||||
|
||||
ReferenceDescriptionCollection references = await GetReferenceDescriptionCollectionAsync(sourceId);
|
||||
List<OpcUaTagModel> list = new();
|
||||
if (references != null)
|
||||
{
|
||||
for (int ii = 0; ii < references.Count; ii++)
|
||||
{
|
||||
ReferenceDescription target = references[ii];
|
||||
OpcUaTagModel child = new()
|
||||
{
|
||||
Name = Utils.Format("{0}", target),
|
||||
Tag = target
|
||||
};
|
||||
if (ShowSubvariable || target.NodeClass != NodeClass.Variable)
|
||||
{
|
||||
var data = await GetReferenceDescriptionCollectionAsync((NodeId)target.NodeId);
|
||||
if (data != null && data.Count > 0)
|
||||
{
|
||||
if (isAll)
|
||||
child.Children = await PopulateBranchAsync((NodeId)target.NodeId);
|
||||
else
|
||||
child.Children = new();
|
||||
}
|
||||
}
|
||||
|
||||
list.Add(child);
|
||||
}
|
||||
}
|
||||
|
||||
nodes.Clear();
|
||||
nodes.AddRange(list.ToArray());
|
||||
return nodes;
|
||||
}
|
||||
|
||||
#if Plugin
|
||||
|
||||
private async Task<List<OpcUaTagModel>> GetAllTag(IEnumerable<OpcUaTagModel> opcUaTagModels)
|
||||
{
|
||||
List<OpcUaTagModel> result = new();
|
||||
foreach (var item in opcUaTagModels)
|
||||
{
|
||||
await PopulateBranch(item);
|
||||
|
||||
result.AddRange(item.GetAllTags().Where(a => a.Children == null).ToList());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task OnClickClose()
|
||||
{
|
||||
if (OnCloseAsync != null)
|
||||
await OnCloseAsync();
|
||||
}
|
||||
|
||||
private async Task OnClickExport()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Nodes == null) return;
|
||||
var data = await GetImportVariableList((await GetAllTag(Nodes)).DistinctBy(a => a.NodeId));
|
||||
if (data.Item3 == null || data.Item3?.Count == 0)
|
||||
{
|
||||
await ToastService.Warning(OpcUaPropertyLocalizer["NoVariablesAvailable"], OpcUaPropertyLocalizer["NoVariablesAvailable"]);
|
||||
return;
|
||||
}
|
||||
|
||||
await DownChannelExportAsync(data.Item1);
|
||||
await DownDeviceExportAsync(data.Item2, data.Item1.Name);
|
||||
await DownDeviceVariableExportAsync(data.Item3.ToList(), data.Item2.Name);
|
||||
await ToastService.Default();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await ToastService.Warn(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnClickSave()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Nodes == null) return;
|
||||
var data = await GetImportVariableList((await GetAllTag(Nodes)).DistinctBy(a => a.NodeId));
|
||||
if (data.Item3 == null || data.Item3?.Count == 0)
|
||||
{
|
||||
await ToastService.Warning(OpcUaPropertyLocalizer["NoVariablesAvailable"], OpcUaPropertyLocalizer["NoVariablesAvailable"]);
|
||||
return;
|
||||
}
|
||||
await App.RootServices.GetRequiredService<IChannelRuntimeService>().SaveChannelAsync(data.Item1, ItemChangedType.Add);
|
||||
await App.RootServices.GetRequiredService<IDeviceRuntimeService>().SaveDeviceAsync(data.Item2, ItemChangedType.Add);
|
||||
await App.RootServices.GetRequiredService<IVariableRuntimeService>().AddBatchAsync(data.Item3.ToList());
|
||||
await ToastService.Default();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await ToastService.Warning(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取设备与变量列表
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private async Task<(Channel, Device, IList<Variable>)> GetImportVariableList(IEnumerable<OpcUaTagModel> opcUaTagModels)
|
||||
{
|
||||
var channel = GetImportChannel();
|
||||
var device = GetImportDevice(channel.Id);
|
||||
var variables = new ConcurrentList<Variable>();
|
||||
await opcUaTagModels.ParallelForEachAsync(async (b, token) =>
|
||||
{
|
||||
var a = b.Tag;
|
||||
var nodeClass = (await Plc.ReadNoteAttributeAsync(a.NodeId.ToString(), Opc.Ua.Attributes.NodeClass, token)).FirstOrDefault().Value.ToString();
|
||||
if (nodeClass == nameof(NodeClass.Variable))
|
||||
{
|
||||
ProtectTypeEnum level = ProtectTypeEnum.ReadOnly;
|
||||
DataTypeEnum dataTypeEnum = DataTypeEnum.Object;
|
||||
try
|
||||
{
|
||||
var userAccessLevel = (AccessLevelType)(await Plc.ReadNoteAttributeAsync(a.NodeId.ToString(), Opc.Ua.Attributes.UserAccessLevel, token)).FirstOrDefault().Value;
|
||||
level = (userAccessLevel.HasFlag(AccessLevelType.CurrentRead)) ?
|
||||
userAccessLevel.HasFlag(AccessLevelType.CurrentWrite) ?
|
||||
ProtectTypeEnum.ReadWrite : ProtectTypeEnum.ReadOnly : ProtectTypeEnum.WriteOnly;
|
||||
|
||||
var dataTypeId = (Opc.Ua.NodeId)(await Plc.ReadNoteAttributeAsync(a.NodeId.ToString(), Opc.Ua.Attributes.DataType, token)).FirstOrDefault().Value;
|
||||
var dataType = Opc.Ua.TypeInfo.GetSystemType(dataTypeId, Plc.Session.Factory);
|
||||
var result = dataType != null && Enum.TryParse<DataTypeEnum>(dataType.Name, out dataTypeEnum);
|
||||
if (!result)
|
||||
{
|
||||
dataTypeEnum = DataTypeEnum.Object;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
var id = Admin.Application.CommonUtils.GetSingleId();
|
||||
|
||||
variables.Add(new Variable()
|
||||
{
|
||||
Name = a.DisplayName.Text + "-" + id,
|
||||
RegisterAddress = a.NodeId.ToString(),
|
||||
DeviceId = device.Id,
|
||||
DataType = dataTypeEnum,
|
||||
Enable = true,
|
||||
Id = id,
|
||||
ProtectType = level,
|
||||
IntervalTime = "1000",
|
||||
RpcWriteEnable = true,
|
||||
});
|
||||
}
|
||||
}, Environment.ProcessorCount / 2);
|
||||
|
||||
return (channel, device, variables);
|
||||
}
|
||||
|
||||
private Device GetImportDevice(long channelId)
|
||||
{
|
||||
var id = Admin.Application.CommonUtils.GetSingleId();
|
||||
var data = new Device()
|
||||
{
|
||||
Name = Plc.OpcUaProperty.OpcUrl + "-" + id,
|
||||
Id = id,
|
||||
ChannelId = channelId,
|
||||
Enable = true,
|
||||
DevicePropertys = new(),
|
||||
};
|
||||
data.DevicePropertys.Add(nameof(OpcUaMasterProperty.OpcUrl), Plc.OpcUaProperty.OpcUrl);
|
||||
data.DevicePropertys.Add(nameof(OpcUaMasterProperty.UserName), Plc.OpcUaProperty.UserName);
|
||||
data.DevicePropertys.Add(nameof(OpcUaMasterProperty.Password), Plc.OpcUaProperty.Password);
|
||||
data.DevicePropertys.Add(nameof(OpcUaMasterProperty.CheckDomain), Plc.OpcUaProperty.CheckDomain.ToString());
|
||||
data.DevicePropertys.Add(nameof(OpcUaMasterProperty.LoadType), Plc.OpcUaProperty.LoadType.ToString());
|
||||
data.DevicePropertys.Add(nameof(OpcUaMasterProperty.UseSecurity), Plc.OpcUaProperty.UseSecurity.ToString());
|
||||
data.DevicePropertys.Add(nameof(OpcUaMasterProperty.ActiveSubscribe), true.ToString());
|
||||
data.DevicePropertys.Add(nameof(OpcUaMasterProperty.DeadBand), Plc.OpcUaProperty.DeadBand.ToString());
|
||||
data.DevicePropertys.Add(nameof(OpcUaMasterProperty.GroupSize), Plc.OpcUaProperty.GroupSize.ToString());
|
||||
data.DevicePropertys.Add(nameof(OpcUaMasterProperty.UpdateRate), Plc.OpcUaProperty.UpdateRate.ToString());
|
||||
data.DevicePropertys.Add(nameof(OpcUaMasterProperty.KeepAliveInterval), Plc.OpcUaProperty.KeepAliveInterval.ToString());
|
||||
return data;
|
||||
}
|
||||
|
||||
private Channel GetImportChannel()
|
||||
{
|
||||
var id = Admin.Application.CommonUtils.GetSingleId();
|
||||
var data = new Channel()
|
||||
{
|
||||
Name = Plc.OpcUaProperty.OpcUrl + "-" + id,
|
||||
Id = id,
|
||||
Enable = true,
|
||||
ChannelType = ChannelTypeEnum.Other,
|
||||
PluginName = "ThingsGateway.Plugin.OpcUa105.OpcUaMaster",
|
||||
};
|
||||
return data;
|
||||
}
|
||||
|
||||
[Inject]
|
||||
private DownloadService DownloadService { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 导出到excel
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task DownChannelExportAsync(Channel data)
|
||||
{
|
||||
using var memoryStream = await App.RootServices.GetRequiredService<IChannelRuntimeService>().ExportMemoryStream(new List<Channel>() { data });
|
||||
await DownloadService.DownloadFromStreamAsync($"channel{DateTime.Now.ToFileDateTimeFormat()}.xlsx", memoryStream);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导出到excel
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task DownDeviceExportAsync(Device data, string channelName)
|
||||
{
|
||||
using var memoryStream = await App.RootServices.GetRequiredService<IDeviceRuntimeService>().ExportMemoryStream(new List<Device>() { data }, channelName);
|
||||
await DownloadService.DownloadFromStreamAsync($"device{DateTime.Now.ToFileDateTimeFormat()}.xlsx", memoryStream);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导出到excel
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task DownDeviceVariableExportAsync(List<Variable> data, string devName)
|
||||
{
|
||||
using var memoryStream = await App.RootServices.GetRequiredService<IVariableRuntimeService>().ExportMemoryStream(data, devName);
|
||||
await DownloadService.DownloadFromStreamAsync($"variable{DateTime.Now.ToFileDateTimeFormat()}.xlsx", memoryStream);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
internal sealed class OpcUaTagModel
|
||||
{
|
||||
internal List<OpcUaTagModel> Children { get; set; }
|
||||
internal string Name { get; set; }
|
||||
internal string NodeId => (Tag?.NodeId)?.ToString();
|
||||
internal ReferenceDescription Tag { get; set; }
|
||||
|
||||
public List<OpcUaTagModel> GetAllTags()
|
||||
{
|
||||
List<OpcUaTagModel> allTags = new();
|
||||
OpcUaTagModel.GetAllTagsRecursive(this, allTags);
|
||||
return allTags;
|
||||
}
|
||||
|
||||
private static void GetAllTagsRecursive(OpcUaTagModel parentTag, List<OpcUaTagModel> allTags)
|
||||
{
|
||||
allTags.Add(parentTag);
|
||||
if (parentTag.Children != null)
|
||||
foreach (OpcUaTagModel childTag in parentTag.Children)
|
||||
{
|
||||
OpcUaTagModel.GetAllTagsRecursive(childTag, allTags);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
111
src/Plugin/ThingsGateway.Plugin.OpcUa105/Pages/OpcUaMaster.razor
Normal file
111
src/Plugin/ThingsGateway.Plugin.OpcUa105/Pages/OpcUaMaster.razor
Normal file
@@ -0,0 +1,111 @@
|
||||
@page "/OpcUaMaster"
|
||||
@using BootstrapBlazor.Components
|
||||
@using ThingsGateway.Extension
|
||||
@using ThingsGateway.Foundation
|
||||
@namespace ThingsGateway.Debug
|
||||
@using ThingsGateway.Foundation.OpcUa105
|
||||
@using TouchSocket.Core
|
||||
|
||||
|
||||
<div class="w-100 h-100">
|
||||
|
||||
@if (_plc?.OpcUaProperty != null)
|
||||
{
|
||||
<Card>
|
||||
<BodyTemplate>
|
||||
|
||||
<EditorForm Model="_plc.OpcUaProperty" AutoGenerateAllItem=false RowType="RowType.Inline" ItemsPerRow="4" ShowLabelTooltip="true" ShowLabel="true">
|
||||
<FieldItems>
|
||||
|
||||
<EditorItem @bind-Field=context.OpcUrl />
|
||||
<EditorItem @bind-Field=context.UserName />
|
||||
<EditorItem @bind-Field=context.Password />
|
||||
<EditorItem @bind-Field=context.KeepAliveInterval />
|
||||
<EditorItem @bind-Field=context.GroupSize />
|
||||
<EditorItem @bind-Field=context.UpdateRate />
|
||||
<EditorItem @bind-Field=context.DeadBand />
|
||||
<EditorItem @bind-Field=context.UseSecurity />
|
||||
<EditorItem @bind-Field=context.ActiveSubscribe />
|
||||
<EditorItem @bind-Field=context.CheckDomain />
|
||||
<EditorItem @bind-Field=context.LoadType />
|
||||
<EditorItem @bind-Field=context.AutoAcceptUntrustedCertificates />
|
||||
|
||||
</FieldItems>
|
||||
<Buttons>
|
||||
<Button IsAsync Color="Color.Primary" OnClick="Connect">@OpcUaPropertyLocalizer["Connect"]</Button>
|
||||
|
||||
<Button IsAsync Color="Color.Warning" OnClick="Disconnect">@OpcUaPropertyLocalizer["Disconnect"]</Button>
|
||||
|
||||
<Button IsAsync Color="Color.Primary" OnClick="Export">@OpcUaPropertyLocalizer["ExportC"]</Button>
|
||||
</Buttons>
|
||||
</EditorForm>
|
||||
|
||||
|
||||
</BodyTemplate>
|
||||
</Card>
|
||||
|
||||
}
|
||||
|
||||
<DeviceComponent DefaultAddress="" LogPath=@LogPath Logger="LogMessage" @ref=DeviceComponent ShowDefaultOtherContent=false ShowDefaultReadWriteContent=false>
|
||||
|
||||
<ReadWriteContent>
|
||||
|
||||
<BootstrapInput @bind-Value=@RegisterAddress DisplayText=@OpcUaPropertyLocalizer["RegisterAddress"]
|
||||
ShowLabel="true" class="w-100" />
|
||||
|
||||
|
||||
<div class="row mx-1 form-inline mt-2">
|
||||
|
||||
|
||||
<div class="col-12 col-md-4 p-1">
|
||||
|
||||
<Button IsAsync Color="Color.Primary" OnClick="Add">@OpcUaPropertyLocalizer["Add"]</Button>
|
||||
|
||||
</div>
|
||||
<div class="col-12 col-md-4 p-1">
|
||||
|
||||
<Button IsAsync Color="Color.Primary" OnClick="Remove">@OpcUaPropertyLocalizer["Remove"]</Button>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-4 p-1">
|
||||
|
||||
<Button IsAsync Color="Color.Primary" OnClick="ReadAsync">@OpcUaPropertyLocalizer["Read"]</Button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
<div class="row mx-1 form-inline mt-2">
|
||||
|
||||
<div class="col-12 col-md-8 p-1">
|
||||
<Textarea @bind-Value=@WriteValue ShowLabelTooltip="true" DisplayText=@OpcUaPropertyLocalizer["WriteValue"]
|
||||
ShowLabel="true" />
|
||||
|
||||
</div>
|
||||
<div class="col-12 col-md-4 p-1">
|
||||
<Button IsAsync Color="Color.Primary" OnClick="WriteAsync">@OpcUaPropertyLocalizer["Write"]</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mx-1 form-inline mt-2">
|
||||
|
||||
<div class="col-12 col-md-4 p-1">
|
||||
<Checkbox @bind-Value=ShowSubvariable DisplayText=@OpcUaPropertyLocalizer["ShowSubvariable"] ShowLabel="true" />
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-4 p-1">
|
||||
<Button IsAsync Color="Color.Primary" OnClick="ShowImport">@OpcUaPropertyLocalizer["ShowImport"]</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</ReadWriteContent>
|
||||
|
||||
|
||||
</DeviceComponent>
|
||||
</div>
|
||||
|
||||
|
||||
|
@@ -0,0 +1,213 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
|
||||
// 此代码版权(除特别声明外的代码)归作者本人Diego所有
|
||||
// 源代码使用协议遵循本仓库的开源协议及附加协议
|
||||
// Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
|
||||
// Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
|
||||
// 使用文档:https://thingsgateway.cn/
|
||||
// QQ群:605534569
|
||||
//------------------------------------------------------------------------------
|
||||
#pragma warning disable CA2007 // 考虑对等待的任务调用 ConfigureAwait
|
||||
|
||||
using BootstrapBlazor.Components;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.Extensions.Localization;
|
||||
|
||||
using ThingsGateway.Foundation.OpcUa105;
|
||||
using ThingsGateway.NewLife.Extension;
|
||||
using ThingsGateway.NewLife.Json.Extension;
|
||||
using ThingsGateway.Razor;
|
||||
|
||||
using TouchSocket.Core;
|
||||
|
||||
namespace ThingsGateway.Debug;
|
||||
|
||||
public partial class OpcUaMaster : IDisposable
|
||||
{
|
||||
public LoggerGroup? LogMessage;
|
||||
private readonly OpcUaProperty OpcUaProperty = new();
|
||||
private ThingsGateway.Foundation.OpcUa105.OpcUaMaster _plc;
|
||||
private string LogPath;
|
||||
private string RegisterAddress;
|
||||
private bool ShowSubvariable;
|
||||
private string WriteValue;
|
||||
|
||||
/// <inheritdoc/>
|
||||
~OpcUaMaster()
|
||||
{
|
||||
this.SafeDispose();
|
||||
}
|
||||
|
||||
private DeviceComponent DeviceComponent { get; set; }
|
||||
|
||||
[Inject]
|
||||
private DialogService DialogService { get; set; }
|
||||
|
||||
[Inject]
|
||||
private IStringLocalizer<OpcUaProperty> OpcUaPropertyLocalizer { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
_plc?.SafeDispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
[Inject]
|
||||
private DownloadService DownloadService { get; set; }
|
||||
[Inject]
|
||||
private ToastService ToastService { get; set; }
|
||||
private async Task Export()
|
||||
{
|
||||
try
|
||||
{
|
||||
await _plc.CheckApplicationInstanceCertificate();
|
||||
string path = $"{AppContext.BaseDirectory}OPCUAClientCertificate/pki/trustedPeer/certs";
|
||||
Directory.CreateDirectory(path);
|
||||
var files = Directory.GetFiles(path);
|
||||
if (files.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
foreach (var item in files)
|
||||
{
|
||||
using var fileStream = new FileStream(item, FileMode.Open, FileAccess.Read);
|
||||
|
||||
var extension = Path.GetExtension(item);
|
||||
extension ??= ".der";
|
||||
|
||||
await DownloadService.DownloadFromStreamAsync($"ThingsGateway{extension}", fileStream);
|
||||
}
|
||||
await ToastService.Default();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await ToastService.Warn(ex);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
_plc = new ThingsGateway.Foundation.OpcUa105.OpcUaMaster();
|
||||
_plc.OpcUaProperty = OpcUaProperty;
|
||||
|
||||
LogMessage = new TouchSocket.Core.LoggerGroup() { LogLevel = TouchSocket.Core.LogLevel.Trace };
|
||||
var logger = TextFileLogger.GetMultipleFileLogger(_plc.GetHashCode().ToLong().GetDebugLogPath());
|
||||
logger.LogLevel = LogLevel.Trace;
|
||||
LogMessage.AddLogger(logger);
|
||||
|
||||
_plc.LogEvent = (a, b, c, d) => LogMessage.Log((LogLevel)a, b, c, d);
|
||||
_plc.DataChangedHandler += (a) => LogMessage.Trace(a.ToJsonNetString());
|
||||
base.OnInitialized();
|
||||
}
|
||||
|
||||
private async Task Add()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_plc.Connected)
|
||||
await _plc.AddSubscriptionAsync(Guid.NewGuid().ToString(), [RegisterAddress]);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogMessage?.LogWarning(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Connect()
|
||||
{
|
||||
try
|
||||
{
|
||||
_plc.Disconnect();
|
||||
LogPath = _plc?.GetHashCode().ToLong().GetDebugLogPath();
|
||||
await GetOpc().ConnectAsync(CancellationToken.None);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogMessage?.Log(LogLevel.Error, null, ex.Message, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void Disconnect()
|
||||
{
|
||||
try
|
||||
{
|
||||
_plc.Disconnect();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogMessage?.Log(LogLevel.Error, null, ex.Message, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private ThingsGateway.Foundation.OpcUa105.OpcUaMaster GetOpc()
|
||||
{
|
||||
//载入配置
|
||||
_plc.OpcUaProperty = OpcUaProperty;
|
||||
return _plc;
|
||||
}
|
||||
|
||||
private async Task ReadAsync()
|
||||
{
|
||||
if (_plc.Connected)
|
||||
{
|
||||
try
|
||||
{
|
||||
var data = await _plc.ReadJTokenValueAsync([RegisterAddress]);
|
||||
|
||||
LogMessage?.LogInformation($" {data[0].Item1}:{data[0].Item3}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogMessage?.LogWarning(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Remove()
|
||||
{
|
||||
if (_plc.Connected)
|
||||
_plc.RemoveSubscription("");
|
||||
}
|
||||
|
||||
private async Task ShowImport()
|
||||
{
|
||||
var op = new DialogOption()
|
||||
{
|
||||
IsScrolling = false,
|
||||
Title = OpcUaPropertyLocalizer["ShowImport"],
|
||||
ShowFooter = false,
|
||||
ShowCloseButton = false,
|
||||
Size = Size.ExtraLarge
|
||||
};
|
||||
op.Component = BootstrapDynamicComponent.CreateComponent<OpcUaImportVariable>(new Dictionary<string, object?>
|
||||
{
|
||||
[nameof(OpcUaImportVariable.Plc)] = _plc,
|
||||
[nameof(OpcUaImportVariable.ShowSubvariable)] = ShowSubvariable,
|
||||
});
|
||||
await DialogService.Show(op);
|
||||
}
|
||||
|
||||
private async Task WriteAsync()
|
||||
{
|
||||
if (_plc.Connected)
|
||||
{
|
||||
var data = await _plc.WriteNodeAsync(
|
||||
new()
|
||||
{
|
||||
{RegisterAddress, WriteValue.GetJTokenFromString()}
|
||||
}
|
||||
).ConfigureAwait(false);
|
||||
|
||||
foreach (var item in data)
|
||||
{
|
||||
if (item.Value.Item1)
|
||||
LogMessage?.LogInformation(item.ToJsonNetString());
|
||||
else
|
||||
LogMessage?.LogWarning(item.ToJsonNetString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
@using BootstrapBlazor.Components
|
||||
@using Microsoft.Extensions.Localization
|
||||
@using ThingsGateway.Extension
|
||||
@using ThingsGateway.Foundation
|
||||
@using ThingsGateway.Admin.Application
|
||||
@using ThingsGateway.Admin.Razor
|
||||
@using ThingsGateway.Gateway.Application
|
||||
@using ThingsGateway.Plugin.OpcUa105
|
||||
@namespace ThingsGateway.Plugin.OpcUa105
|
||||
|
||||
|
||||
<ValidateForm Model="Model.Value"
|
||||
@key=@($"DeviceEditValidateForm{Id}{Model.Value.GetType().TypeHandle.Value}")
|
||||
@ref=Model.ValidateForm
|
||||
Id=@($"DeviceEditValidateForm{Id}{Model.Value.GetType().TypeHandle.Value}")>
|
||||
|
||||
<EditorFormObject class="p-2" Items=PluginPropertyEditorItems IsDisplay="!CanWrite" AutoGenerateAllItem="false" RowType=RowType.Inline ItemsPerRow=@(CanWrite?2:3) ShowLabelTooltip=true LabelWidth=@(CanWrite?240:120) Model="Model.Value" ShowLabel="true" @key=@($"DeviceEditEditorFormObject{Id}{Model.Value.GetType().TypeHandle.Value}")>
|
||||
|
||||
<Buttons>
|
||||
<Button IsAsync class="mx-2" Color=Color.Primary OnClick="Export">@Localizer["ExportC"]</Button>
|
||||
</Buttons>
|
||||
</EditorFormObject>
|
||||
</ValidateForm>
|
||||
|
||||
|
@@ -0,0 +1,79 @@
|
||||
// ------------------------------------------------------------------------------
|
||||
// <20>˴<EFBFBD><CBB4><EFBFBD><EFBFBD><EFBFBD>Ȩ<EFBFBD><C8A8><EFBFBD><EFBFBD>Ϊȫ<CEAA>ļ<EFBFBD><C4BC><EFBFBD><EFBFBD>ǣ<EFBFBD><C7A3><EFBFBD><EFBFBD><EFBFBD>ԭ<EFBFBD><D4AD><EFBFBD><EFBFBD><EFBFBD>ر<EFBFBD><D8B1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>·<EFBFBD><C2B7>ֶ<EFBFBD><D6B6><EFBFBD><EFBFBD><EFBFBD>
|
||||
// <20>˴<EFBFBD><CBB4><EFBFBD><EFBFBD><EFBFBD>Ȩ<EFBFBD><C8A8><EFBFBD><EFBFBD><EFBFBD>ر<EFBFBD><D8B1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ĵ<EFBFBD><C4B4>룩<EFBFBD><EBA3A9><EFBFBD><EFBFBD><EFBFBD>߱<EFBFBD><DFB1><EFBFBD>Diego<67><6F><EFBFBD><EFBFBD>
|
||||
// Դ<><D4B4><EFBFBD><EFBFBD>ʹ<EFBFBD><CAB9>Э<EFBFBD><D0AD><EFBFBD><EFBFBD>ѭ<EFBFBD><D1AD><EFBFBD>ֿ<EFBFBD><D6BF>Ŀ<EFBFBD>ԴЭ<D4B4>鼰<EFBFBD><E9BCB0><EFBFBD><EFBFBD>Э<EFBFBD><D0AD>
|
||||
// GiteeԴ<65><D4B4><EFBFBD><EFBFBD><EFBFBD>ֿ⣺https://gitee.com/diego2098/ThingsGateway
|
||||
// GithubԴ<62><D4B4><EFBFBD><EFBFBD><EFBFBD>ֿ⣺https://github.com/kimdiego2098/ThingsGateway
|
||||
// ʹ<><CAB9><EFBFBD>ĵ<EFBFBD><C4B5><EFBFBD>https://thingsgateway.cn/
|
||||
// QQȺ<51><C8BA>605534569
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
#pragma warning disable CA2007 // <20><><EFBFBD>ǶԵȴ<D4B5><C8B4><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> ConfigureAwait
|
||||
using BootstrapBlazor.Components;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.Extensions.Localization;
|
||||
|
||||
using ThingsGateway.Razor;
|
||||
|
||||
namespace ThingsGateway.Plugin.OpcUa105
|
||||
{
|
||||
public partial class OpcUaMasterPropertyRazor : IPropertyUIBase
|
||||
{
|
||||
[Parameter, EditorRequired]
|
||||
public string Id { get; set; }
|
||||
[Parameter, EditorRequired]
|
||||
public bool CanWrite { get; set; }
|
||||
[Parameter, EditorRequired]
|
||||
public ModelValueValidateForm Model { get; set; }
|
||||
|
||||
[Parameter, EditorRequired]
|
||||
public IEnumerable<IEditorItem> PluginPropertyEditorItems { get; set; }
|
||||
|
||||
IStringLocalizer Localizer { get; set; }
|
||||
|
||||
protected override Task OnParametersSetAsync()
|
||||
{
|
||||
Localizer = App.CreateLocalizerByType(Model.Value.GetType());
|
||||
|
||||
return base.OnParametersSetAsync();
|
||||
}
|
||||
|
||||
[Inject]
|
||||
private DownloadService DownloadService { get; set; }
|
||||
[Inject]
|
||||
private ToastService ToastService { get; set; }
|
||||
|
||||
|
||||
private async Task Export()
|
||||
{
|
||||
try
|
||||
{
|
||||
var plc = new ThingsGateway.Foundation.OpcUa105.OpcUaMaster();
|
||||
await plc.CheckApplicationInstanceCertificate().ConfigureAwait(false);
|
||||
string path = $"{AppContext.BaseDirectory}OPCUAClientCertificate/pki/trustedPeer/certs";
|
||||
Directory.CreateDirectory(path);
|
||||
var files = Directory.GetFiles(path);
|
||||
if (files.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
foreach (var item in files)
|
||||
{
|
||||
using var fileStream = new FileStream(item, FileMode.Open, FileAccess.Read);
|
||||
|
||||
var extension = Path.GetExtension(item);
|
||||
extension ??= ".der";
|
||||
|
||||
await DownloadService.DownloadFromStreamAsync($"ThingsGateway{extension}", fileStream).ConfigureAwait(false);
|
||||
}
|
||||
await ToastService.Default();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await ToastService.Warn(ex);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,54 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||
<Import Project="$(SolutionDir)Version.props" />
|
||||
<Import Project="$(SolutionDir)PackNuget.props" />
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0;</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<DefineConstants>Plugin</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Remove="Locales\*.json" />
|
||||
<EmbeddedResource Include="Locales\*.json">
|
||||
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Foundation\ThingsGateway.Foundation.Razor\ThingsGateway.Foundation.Razor.csproj">
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\Gateway\ThingsGateway.Gateway.Razor\ThingsGateway.Gateway.Razor.csproj">
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\ThingsGateway.Foundation.OpcUa105\ThingsGateway.Foundation.OpcUa105.csproj" />
|
||||
|
||||
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Server" Version="1.5.375.443" GeneratePathProperty="true">
|
||||
<PrivateAssets>contentFiles;compile;build;buildMultitargeting;buildTransitive;analyzers;</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
||||
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Client.ComplexTypes" Version="1.5.375.443" GeneratePathProperty="true">
|
||||
<PrivateAssets>contentFiles;compile;build;buildMultitargeting;buildTransitive;analyzers;</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
||||
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Client" Version="1.5.375.443" GeneratePathProperty="true">
|
||||
<PrivateAssets>contentFiles;compile;build;buildMultitargeting;buildTransitive;analyzers;</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
||||
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Core" Version="1.5.375.443" GeneratePathProperty="true">
|
||||
<PrivateAssets>contentFiles;compile;build;buildMultitargeting;buildTransitive;analyzers;</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
||||
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Configuration" Version="1.5.375.443" GeneratePathProperty="true">
|
||||
<PrivateAssets>contentFiles;compile;build;buildMultitargeting;buildTransitive;analyzers;</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
||||
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Security.Certificates" Version="1.5.375.443" GeneratePathProperty="true">
|
||||
<PrivateAssets>contentFiles;compile;build;buildMultitargeting;buildTransitive;analyzers;</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
||||
<PackageReference Include="System.Formats.Asn1" Version="8.*" GeneratePathProperty="true">
|
||||
<PrivateAssets>contentFiles;compile;build;buildMultitargeting;buildTransitive;analyzers;</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
||||
</ItemGroup>
|
||||
</Project>
|
12
src/Plugin/ThingsGateway.Plugin.OpcUa105/_Imports.razor
Normal file
12
src/Plugin/ThingsGateway.Plugin.OpcUa105/_Imports.razor
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
@using System.Net.Http
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.JSInterop
|
||||
@using ThingsGateway.Razor;
|
||||
@using ThingsGateway;
|
||||
@using System.Net.Http.Json
|
||||
@using System.IO;
|
||||
@using System.Text.Json;
|
@@ -5,8 +5,8 @@
|
||||
<ItemGroup>
|
||||
<!-- setting up the variable for convenience -->
|
||||
<ApplicationPackageFiles Include="$(PkgRabbitMQ_Client)\lib\net8.0\*.*" />
|
||||
<ApplicationPackageFiles1 Include="$(PkgSystem_IO_Pipelines)\lib\net8.0\*.*" />
|
||||
<ApplicationPackageFiles2 Include="$(PkgSystem_Threading_RateLimiting)\lib\net8.0\*.*" />
|
||||
<!--<ApplicationPackageFiles1 Include="$(PkgSystem_IO_Pipelines)\lib\net8.0\*.*" />-->
|
||||
<!--<ApplicationPackageFiles2 Include="$(PkgSystem_Threading_RateLimiting)\lib\net8.0\*.*" />-->
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<ApplicationFolder>$(TargetDir)</ApplicationFolder>
|
||||
|
@@ -16,15 +16,9 @@
|
||||
<PackageReference Include="RabbitMQ.Client" Version="7.0.0" GeneratePathProperty="true">
|
||||
<PrivateAssets>contentFiles;compile;build;buildMultitargeting;buildTransitive;analyzers;</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="System.IO.Pipelines" Version="9.*" GeneratePathProperty="true">
|
||||
<PrivateAssets>contentFiles;runtime;compile;build;buildMultitargeting;buildTransitive;analyzers;</PrivateAssets>
|
||||
|
||||
</PackageReference>
|
||||
<PackageReference Include="System.Threading.RateLimiting" Version="9.*" GeneratePathProperty="true">
|
||||
<PrivateAssets>contentFiles;runtime;compile;build;buildMultitargeting;buildTransitive;analyzers;</PrivateAssets>
|
||||
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<Content Remove="Locales\*.json" />
|
||||
|
@@ -59,9 +59,11 @@
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.*" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.*" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="..\ThingsGateway.Server\Index\GatewayIndex.razor" Link="Index\GatewayIndex.razor" />
|
||||
|
@@ -68,10 +68,16 @@
|
||||
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
<!--安装服务守护-->
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.*" />
|
||||
</ItemGroup>
|
||||
<!--安装服务守护-->
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' ">
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="8.*" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.*" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'net9.0' ">
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="9.*" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.*" />
|
||||
</ItemGroup>
|
||||
|
@@ -9,6 +9,7 @@
|
||||
<ProjectReference Include="..\Plugin\ThingsGateway.Plugin.Mqtt\ThingsGateway.Plugin.Mqtt.csproj" />
|
||||
<ProjectReference Include="..\Plugin\ThingsGateway.Plugin.OpcDa\ThingsGateway.Plugin.OpcDa.csproj" />
|
||||
<ProjectReference Include="..\Plugin\ThingsGateway.Plugin.OpcUa\ThingsGateway.Plugin.OpcUa.csproj" />
|
||||
<ProjectReference Include="..\Plugin\ThingsGateway.Plugin.OpcUa105\ThingsGateway.Plugin.OpcUa105.csproj" />
|
||||
<ProjectReference Include="..\Plugin\ThingsGateway.Plugin.RabbitMQ\ThingsGateway.Plugin.RabbitMQ.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
@@ -10,8 +10,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "other", "other", "{0B748352
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.editorconfig = .editorconfig
|
||||
Directory.Build.props = Directory.Build.props
|
||||
..\git_pull.bat = ..\git_pull.bat
|
||||
PluginVersion.props = PluginVersion.props
|
||||
Version.props = Version.props
|
||||
EndProjectSection
|
||||
EndProject
|
||||
@@ -34,8 +32,8 @@ Global
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {199B1B96-4F56-4828-9531-813BA02DB282}
|
||||
RESX_Rules = {"EnabledRules":[]}
|
||||
RESX_NeutralResourcesLanguage = zh-Hans
|
||||
RESX_Rules = {"EnabledRules":[]}
|
||||
SolutionGuid = {199B1B96-4F56-4828-9531-813BA02DB282}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Version>10.0.0.19</Version>
|
||||
<Version>10.0.0.20</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
Reference in New Issue
Block a user