mirror of
https://gitee.com/ThingsGateway/ThingsGateway.git
synced 2025-10-21 19:14:30 +08:00
Compare commits
15 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e3c0c173f0 | ||
![]() |
7d64e058d4 | ||
![]() |
e97ee9b64b | ||
![]() |
6a03e39eeb | ||
![]() |
525ec740b5 | ||
![]() |
b790cf5f4e | ||
![]() |
d1248811fd | ||
![]() |
022d016e8e | ||
![]() |
f73245e650 | ||
![]() |
484461fa05 | ||
![]() |
7e0b7aff2a | ||
![]() |
6c450dcb09 | ||
![]() |
227f44283f | ||
![]() |
74f6e79625 | ||
![]() |
cec43e2ce8 |
@@ -21,7 +21,7 @@
|
||||
<PackageReference Include="BootstrapBlazor.TableExport" Version="9.2.4" />
|
||||
<PackageReference Include="UAParser" Version="3.1.47" />
|
||||
<PackageReference Include="Rougamo.Fody" Version="5.0.0" />
|
||||
<PackageReference Include="SqlSugarCore" Version="5.1.4.189" />
|
||||
<PackageReference Include="SqlSugarCore" Version="5.1.4.193" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' ">
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.1" />
|
||||
|
@@ -10,6 +10,9 @@
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
|
||||
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
@@ -18,6 +21,7 @@ using Microsoft.Extensions.Options;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Unicode;
|
||||
@@ -291,6 +295,21 @@ public class Startup : AppStartup
|
||||
services.AddAuthorizationCore();
|
||||
services.AddScoped<IAuthorizationHandler, BlazorServerAuthenticationHandler>();
|
||||
services.AddScoped<AuthenticationStateProvider, BlazorServerAuthenticationStateProvider>();
|
||||
|
||||
#if NET9_0_OR_GREATER
|
||||
var certificate = X509CertificateLoader.LoadPkcs12FromFile("ThingsGateway.pfx", "ThingsGateway", X509KeyStorageFlags.EphemeralKeySet);
|
||||
#else
|
||||
var certificate = new X509Certificate2("ThingsGateway.pfx", "ThingsGateway", X509KeyStorageFlags.EphemeralKeySet);
|
||||
#endif
|
||||
services.AddDataProtection()
|
||||
.PersistKeysToFileSystem(new DirectoryInfo("../keys"))
|
||||
.ProtectKeysWithCertificate(certificate)
|
||||
.UseCryptographicAlgorithms(new AuthenticatedEncryptorConfiguration
|
||||
{
|
||||
EncryptionAlgorithm = EncryptionAlgorithm.AES_256_CBC,
|
||||
ValidationAlgorithm = ValidationAlgorithm.HMACSHA256
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@@ -72,6 +72,9 @@
|
||||
<None Update="pm2-linux.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="ThingsGateway.pfx">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="thingsgateway.service">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
|
BIN
src/Admin/ThingsGateway.AdminServer/ThingsGateway.pfx
Normal file
BIN
src/Admin/ThingsGateway.AdminServer/ThingsGateway.pfx
Normal file
Binary file not shown.
@@ -32,7 +32,7 @@ public class TimeTick
|
||||
/// <summary>
|
||||
/// 上次触发时间
|
||||
/// </summary>
|
||||
public DateTime LastTime { get; private set; } = DateTime.Now;
|
||||
public DateTime LastTime { get; private set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// 是否触发时间刻度
|
||||
@@ -62,7 +62,7 @@ public class TimeTick
|
||||
return result;
|
||||
}
|
||||
|
||||
public DateTime GetNextTime(DateTime currentTime, bool setLastTime = true)
|
||||
public DateTime GetNextTime(DateTime currentTime, bool setLastTime = false)
|
||||
{
|
||||
// 在没有 Cron 表达式的情况下,使用固定间隔
|
||||
if (cron == null)
|
||||
@@ -86,7 +86,7 @@ public class TimeTick
|
||||
|
||||
}
|
||||
|
||||
public DateTime GetNextTime(bool setLastTime = true) => GetNextTime(DateTime.UtcNow, setLastTime);
|
||||
public DateTime GetNextTime(bool setLastTime = false) => GetNextTime(DateTime.UtcNow, setLastTime);
|
||||
|
||||
/// <summary>
|
||||
/// 是否到达设置的时间间隔
|
||||
|
@@ -22,12 +22,34 @@ public static class JSRuntimeExtensions
|
||||
/// 获取文化信息
|
||||
/// </summary>
|
||||
/// <param name="jsRuntime"></param>
|
||||
public static ValueTask<string> GetCulture(this IJSRuntime jsRuntime) => jsRuntime.InvokeAsync<string>("getCultureLocalStorage");
|
||||
public static async ValueTask<string> GetCulture(this IJSRuntime jsRuntime)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await jsRuntime.InvokeAsync<string>("getCultureLocalStorage");
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置文化信息
|
||||
/// </summary>
|
||||
/// <param name="jsRuntime"></param>
|
||||
/// <param name="cultureName"></param>
|
||||
public static ValueTask SetCulture(this IJSRuntime jsRuntime, string cultureName) => jsRuntime.InvokeVoidAsync("setCultureLocalStorage", cultureName);
|
||||
public static async ValueTask SetCulture(this IJSRuntime jsRuntime, string cultureName)
|
||||
{
|
||||
try
|
||||
{
|
||||
await jsRuntime.InvokeVoidAsync("setCultureLocalStorage", cultureName);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -7,7 +7,7 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BootstrapBlazor.FontAwesome" Version="9.0.2" />
|
||||
<PackageReference Include="BootstrapBlazor" Version="9.5.10" />
|
||||
<PackageReference Include="BootstrapBlazor" Version="9.6.1" />
|
||||
<PackageReference Include="Yitter.IdGenerator" Version="1.0.14" />
|
||||
</ItemGroup>
|
||||
|
||||
|
@@ -1,8 +1,8 @@
|
||||
<Project>
|
||||
|
||||
<PropertyGroup>
|
||||
<PluginVersion>10.5.5</PluginVersion>
|
||||
<ProPluginVersion>10.5.5</ProPluginVersion>
|
||||
<PluginVersion>10.5.17</PluginVersion>
|
||||
<ProPluginVersion>10.5.17</ProPluginVersion>
|
||||
<AuthenticationVersion>2.1.7</AuthenticationVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
|
@@ -7,7 +7,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CS-Script" Version="4.9.5" />
|
||||
<PackageReference Include="CS-Script" Version="4.9.6" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@@ -42,7 +42,10 @@ internal sealed class HeartbeatAndReceivePlugin : PluginBase, ITcpConnectedPlugi
|
||||
set
|
||||
{
|
||||
_heartbeat = value;
|
||||
HeartbeatByte = new ArraySegment<byte>(Encoding.UTF8.GetBytes(value));
|
||||
if (!_heartbeat.IsNullOrEmpty())
|
||||
{
|
||||
HeartbeatByte = new ArraySegment<byte>(Encoding.UTF8.GetBytes(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
private string _heartbeat;
|
||||
@@ -59,6 +62,8 @@ internal sealed class HeartbeatAndReceivePlugin : PluginBase, ITcpConnectedPlugi
|
||||
return;//此处可判断,如果为服务器,则不用使用心跳。
|
||||
}
|
||||
|
||||
HeartbeatTime = Math.Max(HeartbeatTime, 1000);
|
||||
|
||||
if (DtuId.IsNullOrWhiteSpace()) return;
|
||||
|
||||
if (client is ITcpClient tcpClient)
|
||||
|
@@ -70,7 +70,7 @@ public static class PluginUtil
|
||||
{
|
||||
action += a =>
|
||||
{
|
||||
a.UseCheckClear()
|
||||
a.UseTcpSessionCheckClear()
|
||||
.SetCheckClearType(CheckClearType.All)
|
||||
.SetTick(TimeSpan.FromMilliseconds(channelOptions.CheckClearTime))
|
||||
.SetOnClose(async (c, t) =>
|
||||
|
@@ -97,9 +97,17 @@ public abstract class DeviceBase : DisposableObject, IDevice
|
||||
Channel.Stoping.Add(ChannelStoping);
|
||||
Channel.Started.Add(ChannelStarted);
|
||||
Channel.ChannelReceived.Add(ChannelReceived);
|
||||
|
||||
|
||||
SetChannel();
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void SetChannel()
|
||||
{
|
||||
Channel.ChannelOptions.MaxConcurrentCount = 1;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
~DeviceBase()
|
||||
{
|
||||
@@ -554,7 +562,7 @@ public abstract class DeviceBase : DisposableObject, IDevice
|
||||
finally
|
||||
{
|
||||
waitLock.Release();
|
||||
clientChannel.WaitHandlePool.Destroy(waitData);
|
||||
clientChannel.WaitHandlePool.Destroy(sign);
|
||||
Channel.ChannelReceivedWaitDict.TryRemove(sign, out _);
|
||||
}
|
||||
}
|
||||
@@ -952,7 +960,7 @@ public abstract class DeviceBase : DisposableObject, IDevice
|
||||
if (tcpServiceChannel.TryGetClient($"ID={dtu.DtuId}", out var client))
|
||||
{
|
||||
client.WaitHandlePool?.SafeDispose();
|
||||
client.Close();
|
||||
_ = client.CloseAsync();
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -10,8 +10,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Localization.Abstractions" Version="9.0.4" />
|
||||
<PackageReference Include="TouchSocket" Version="3.1.0" />
|
||||
<PackageReference Include="TouchSocket.SerialPorts" Version="3.1.0" />
|
||||
<PackageReference Include="TouchSocket" Version="3.1.2" />
|
||||
<PackageReference Include="TouchSocket.SerialPorts" Version="3.1.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@@ -40,9 +40,7 @@ public class AsyncReadWriteLock
|
||||
|
||||
private void ReleaseWriter()
|
||||
{
|
||||
Interlocked.Decrement(ref _writerCount);
|
||||
|
||||
if (Interlocked.Read(ref _writerCount) == 0)
|
||||
if (Interlocked.Decrement(ref _writerCount) == 0)
|
||||
{
|
||||
var resetEvent = _readerLock;
|
||||
_readerLock = new(false);
|
||||
|
@@ -87,12 +87,12 @@ public abstract class BusinessBaseWithCacheIntervalAlarmModel<VarModel, DevModel
|
||||
// 触发一次设备状态变化和变量值变化事件
|
||||
CollectDevices?.ForEach(a =>
|
||||
{
|
||||
if (a.Value.DeviceStatus == DeviceStatusEnum.OnLine)
|
||||
if (a.Value.DeviceStatus == DeviceStatusEnum.OnLine && _businessPropertyWithCacheInterval.BusinessUpdateEnum != BusinessUpdateEnum.Interval)
|
||||
DeviceStatusChange(a.Value, a.Value.Adapt<DeviceBasicData>());
|
||||
});
|
||||
IdVariableRuntimes.ForEach(a =>
|
||||
{
|
||||
if (a.Value.IsOnline)
|
||||
if (a.Value.IsOnline && _businessPropertyWithCacheInterval.BusinessUpdateEnum != BusinessUpdateEnum.Interval)
|
||||
VariableValueChange(a.Value, a.Value.Adapt<VariableBasicData>());
|
||||
});
|
||||
}
|
||||
|
@@ -85,12 +85,12 @@ public abstract class BusinessBaseWithCacheIntervalDeviceModel<VarModel, DevMode
|
||||
|
||||
CollectDevices?.ForEach(a =>
|
||||
{
|
||||
if (a.Value.DeviceStatus == DeviceStatusEnum.OnLine)
|
||||
if (a.Value.DeviceStatus == DeviceStatusEnum.OnLine && _businessPropertyWithCacheInterval.BusinessUpdateEnum != BusinessUpdateEnum.Interval)
|
||||
DeviceStatusChange(a.Value, a.Value.Adapt<DeviceBasicData>());
|
||||
});
|
||||
IdVariableRuntimes.ForEach(a =>
|
||||
{
|
||||
if (a.Value.IsOnline)
|
||||
if (a.Value.IsOnline && _businessPropertyWithCacheInterval.BusinessUpdateEnum != BusinessUpdateEnum.Interval)
|
||||
VariableValueChange(a.Value, a.Value.Adapt<VariableBasicData>());
|
||||
});
|
||||
}
|
||||
|
@@ -73,7 +73,7 @@ public abstract class BusinessBaseWithCacheIntervalVariableModel<T> : BusinessBa
|
||||
// 触发一次变量值变化事件
|
||||
IdVariableRuntimes.ForEach(a =>
|
||||
{
|
||||
if (a.Value.IsOnline)
|
||||
if (a.Value.IsOnline && _businessPropertyWithCacheInterval.BusinessUpdateEnum != BusinessUpdateEnum.Interval)
|
||||
VariableValueChange(a.Value, a.Value.Adapt<VariableBasicData>());
|
||||
});
|
||||
}
|
||||
|
@@ -1,32 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
|
||||
// 此代码版权(除特别声明外的代码)归作者本人Diego所有
|
||||
// 源代码使用协议遵循本仓库的开源协议及附加协议
|
||||
// Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
|
||||
// Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
|
||||
// 使用文档:https://thingsgateway.cn/
|
||||
// QQ群:605534569
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using BootstrapBlazor.Components;
|
||||
|
||||
namespace ThingsGateway.Gateway.Application;
|
||||
|
||||
/// <summary>
|
||||
/// <inheritdoc/>
|
||||
/// </summary>
|
||||
public class BusinessPropertyWithCacheIntervalDBScript : BusinessPropertyWithCacheInterval
|
||||
{
|
||||
/// <summary>
|
||||
/// 表定义实体脚本
|
||||
/// </summary>
|
||||
[DynamicProperty]
|
||||
[AutoGenerateColumn(ComponentType = typeof(Textarea), Rows = 3)]
|
||||
public string? BigTextScriptTableModel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 数据脚本
|
||||
/// </summary>
|
||||
[AutoGenerateColumn(ComponentType = typeof(Textarea), Rows = 3)]
|
||||
public string? BigTextScriptDataModel { get; set; }
|
||||
}
|
@@ -51,6 +51,13 @@ public class Variable : BaseDataEntity, IValidatableObject
|
||||
[Required]
|
||||
public virtual string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 采集组
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnDescription = "采集组", IsNullable = true)]
|
||||
[AutoGenerateColumn(Visible = true, Filterable = true, Sortable = true, Order = 1)]
|
||||
public virtual string CollectGroup { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 分组名称
|
||||
/// </summary>
|
||||
|
@@ -97,6 +97,7 @@
|
||||
"Name": "Name",
|
||||
"Description": "Description",
|
||||
"Group": "Group",
|
||||
"CollectGroup": "CollectGroup",
|
||||
"DeviceId": "CollectionDevice",
|
||||
"DeviceId.MinValue": "{0} cannot be empty",
|
||||
"DeviceId.Required": "{0} cannot be empty",
|
||||
@@ -452,6 +453,7 @@
|
||||
"Name.Required": "{0} cannot be empty",
|
||||
"Description": "Description",
|
||||
"Group": "Group",
|
||||
"CollectGroup": "CollectGroup",
|
||||
"DeviceId": "CollectionDevice",
|
||||
"DeviceId.MinValue": "{0} cannot be empty",
|
||||
"DeviceId.Required": "{0} cannot be empty",
|
||||
|
@@ -89,7 +89,8 @@
|
||||
"Name": "名称",
|
||||
"Name.Required": " {0} 不可为空",
|
||||
"Description": "描述",
|
||||
"Group": "分组",
|
||||
"Group": "业务组",
|
||||
"CollectGroup": "采集组",
|
||||
"DeviceId": "采集设备",
|
||||
"DeviceId.MinValue": " {0} 不可为空",
|
||||
"DeviceId.Required": " {0} 不可为空",
|
||||
@@ -486,7 +487,8 @@
|
||||
"Name": "名称",
|
||||
"Name.Required": " {0} 不可为空",
|
||||
"Description": "描述",
|
||||
"Group": "分组",
|
||||
"Group": "业务组",
|
||||
"CollectGroup": "采集组",
|
||||
"DeviceId": "采集设备",
|
||||
"DeviceId.MinValue": " {0} 不可为空",
|
||||
"DeviceId.Required": " {0} 不可为空",
|
||||
|
@@ -19,5 +19,8 @@ public class VariableMapper : IRegister
|
||||
{
|
||||
config.ForType<Variable, VariableRuntime>()
|
||||
.Map(dest => dest.Value, src => src.InitValue);
|
||||
|
||||
config.ForType<VariableRuntime, VariableRuntime>()
|
||||
.Ignore(dest => dest.DeviceRuntime);
|
||||
}
|
||||
}
|
||||
|
@@ -8,8 +8,8 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Portable.BouncyCastle" Version="1.9.0" />
|
||||
<PackageReference Include="Rougamo.Fody" Version="5.0.0" />
|
||||
<PackageReference Include="TouchSocket.Dmtp" Version="3.1.0" />
|
||||
<PackageReference Include="TouchSocket.WebApi.Swagger" Version="3.1.0" />
|
||||
<PackageReference Include="TouchSocket.Dmtp" Version="3.1.2" />
|
||||
<PackageReference Include="TouchSocket.WebApi.Swagger" Version="3.1.2" />
|
||||
<PackageReference Include="ThingsGateway.Authentication" Version="$(AuthenticationVersion)" />
|
||||
|
||||
</ItemGroup>
|
||||
|
@@ -1299,7 +1299,7 @@ EventCallback.Factory.Create<MouseEventArgs>(this, async e =>
|
||||
try
|
||||
{
|
||||
if (Disposed) return;
|
||||
await Task.Delay(2000);
|
||||
await Task.Delay(1000);
|
||||
await OnClickSearch(SearchText);
|
||||
|
||||
Value = GetValue(Value);
|
||||
|
@@ -129,7 +129,6 @@ public partial class PropertyComponent : IPropertyUIBase
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
var op = new DialogOption()
|
||||
{
|
||||
IsScrolling = true,
|
||||
@@ -144,6 +143,147 @@ public partial class PropertyComponent : IPropertyUIBase
|
||||
{
|
||||
{nameof(ScriptCheck.Data),data },
|
||||
{nameof(ScriptCheck.Script),script },
|
||||
{nameof(ScriptCheck.OnGetDemo),()=>
|
||||
{
|
||||
return
|
||||
pname == nameof(BusinessPropertyWithCacheIntervalScript.BigTextScriptDeviceModel)?
|
||||
"""
|
||||
using ThingsGateway.Foundation;
|
||||
|
||||
using System.Dynamic;
|
||||
using TouchSocket.Core;
|
||||
public class S1 : IDynamicModel
|
||||
{
|
||||
public IEnumerable<dynamic> GetList(IEnumerable<object> datas)
|
||||
{
|
||||
List<ExpandoObject> deviceObjs = new List<ExpandoObject>();
|
||||
foreach (var v in datas)
|
||||
{
|
||||
var device = (DeviceBasicData)v;
|
||||
var expando = new ExpandoObject();
|
||||
var deviceObj = new ExpandoObject();
|
||||
|
||||
deviceObj.TryAdd(nameof(Device.Description), device.Description);
|
||||
deviceObj.TryAdd(nameof(DeviceBasicData.ActiveTime), device.ActiveTime);
|
||||
deviceObj.TryAdd(nameof(DeviceBasicData.DeviceStatus), device.DeviceStatus.ToString());
|
||||
deviceObj.TryAdd(nameof(DeviceBasicData.LastErrorMessage), device.LastErrorMessage);
|
||||
deviceObj.TryAdd(nameof(DeviceBasicData.PluginName), device.PluginName);
|
||||
deviceObj.TryAdd(nameof(DeviceBasicData.Remark1), device.Remark1);
|
||||
deviceObj.TryAdd(nameof(DeviceBasicData.Remark2), device.Remark2);
|
||||
deviceObj.TryAdd(nameof(DeviceBasicData.Remark3), device.Remark3);
|
||||
deviceObj.TryAdd(nameof(DeviceBasicData.Remark4), device.Remark4);
|
||||
deviceObj.TryAdd(nameof(DeviceBasicData.Remark5), device.Remark5);
|
||||
|
||||
|
||||
expando.TryAdd(nameof(Device.Name), deviceObj);
|
||||
|
||||
}
|
||||
return deviceObjs;
|
||||
}
|
||||
}
|
||||
|
||||
"""
|
||||
:
|
||||
|
||||
pname == nameof(BusinessPropertyWithCacheIntervalScript.BigTextScriptVariableModel)?
|
||||
|
||||
"""
|
||||
using System.Dynamic;
|
||||
using ThingsGateway.Foundation;
|
||||
using TouchSocket.Core;
|
||||
public class S2 : IDynamicModel
|
||||
{
|
||||
public IEnumerable<dynamic> GetList(IEnumerable<object> datas)
|
||||
{
|
||||
|
||||
List<ExpandoObject> deviceObjs = new List<ExpandoObject>();
|
||||
//按设备名称分组
|
||||
var groups = datas.Where(a => !string.IsNullOrEmpty(((VariableBasicData)a).DeviceName)).GroupBy(a => ((VariableBasicData)a).DeviceName, a => ((VariableBasicData)a));
|
||||
foreach (var group in groups)
|
||||
{
|
||||
//按采集时间分组
|
||||
var data = group.GroupBy(a => a.CollectTime.DateTimeToUnixTimestamp());
|
||||
var deviceObj = new ExpandoObject();
|
||||
List<ExpandoObject> expandos = new List<ExpandoObject>();
|
||||
foreach (var item in data)
|
||||
{
|
||||
var expando = new ExpandoObject();
|
||||
expando.TryAdd("ts", item.Key);
|
||||
var variableObj = new ExpandoObject();
|
||||
foreach (var tag in item)
|
||||
{
|
||||
variableObj.TryAdd(tag.Name, tag.Value);
|
||||
}
|
||||
expando.TryAdd("values", variableObj);
|
||||
|
||||
expandos.Add(expando);
|
||||
}
|
||||
deviceObj.TryAdd(group.Key, expandos);
|
||||
deviceObjs.Add(deviceObj);
|
||||
}
|
||||
|
||||
return deviceObjs;
|
||||
}
|
||||
}
|
||||
|
||||
"""
|
||||
:
|
||||
|
||||
pname == nameof(BusinessPropertyWithCacheIntervalScript.BigTextScriptAlarmModel)?
|
||||
|
||||
"""
|
||||
using System.Dynamic;
|
||||
using ThingsGateway.Foundation;
|
||||
|
||||
using TouchSocket.Core;
|
||||
public class DeviceScript : IDynamicModel
|
||||
{
|
||||
public IEnumerable<dynamic> GetList(IEnumerable<object> datas)
|
||||
{
|
||||
|
||||
List<ExpandoObject> deviceObjs = new List<ExpandoObject>();
|
||||
//按设备名称分组
|
||||
var groups = datas.Where(a => !string.IsNullOrEmpty(((AlarmVariable)a).DeviceName)).GroupBy(a => ((AlarmVariable)a).DeviceName, a => ((AlarmVariable)a));
|
||||
foreach (var group in groups)
|
||||
{
|
||||
//按采集时间分组
|
||||
var data = group.GroupBy(a => a.AlarmTime.DateTimeToUnixTimestamp());
|
||||
var deviceObj = new ExpandoObject();
|
||||
List<ExpandoObject> expandos = new List<ExpandoObject>();
|
||||
foreach (var item in data)
|
||||
{
|
||||
var expando = new ExpandoObject();
|
||||
expando.TryAdd("ts", item.Key);
|
||||
var variableObj = new ExpandoObject();
|
||||
foreach (var tag in item)
|
||||
{
|
||||
var alarmObj = new ExpandoObject();
|
||||
alarmObj.TryAdd(nameof(tag.AlarmCode), tag.AlarmCode);
|
||||
alarmObj.TryAdd(nameof(tag.AlarmText), tag.AlarmText);
|
||||
alarmObj.TryAdd(nameof(tag.AlarmType), tag.AlarmType);
|
||||
alarmObj.TryAdd(nameof(tag.AlarmLimit), tag.AlarmLimit);
|
||||
alarmObj.TryAdd(nameof(tag.EventTime), tag.EventTime);
|
||||
alarmObj.TryAdd(nameof(tag.EventType), tag.EventType);
|
||||
|
||||
variableObj.TryAdd(tag.Name, alarmObj);
|
||||
}
|
||||
expando.TryAdd("alarms", variableObj);
|
||||
|
||||
expandos.Add(expando);
|
||||
}
|
||||
deviceObj.TryAdd(group.Key, expandos);
|
||||
deviceObjs.Add(deviceObj);
|
||||
}
|
||||
|
||||
return deviceObjs;
|
||||
}
|
||||
}
|
||||
"""
|
||||
:
|
||||
""
|
||||
;
|
||||
}
|
||||
},
|
||||
{nameof(ScriptCheck.ScriptChanged),EventCallback.Factory.Create<string>(this, v =>
|
||||
{
|
||||
if (pname == nameof(BusinessPropertyWithCacheIntervalScript.BigTextScriptAlarmModel))
|
||||
@@ -171,3 +311,7 @@ public partial class PropertyComponent : IPropertyUIBase
|
||||
[Inject]
|
||||
private DialogService DialogService { get; set; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
@@ -18,6 +18,8 @@
|
||||
<Alert Icon="fa-solid fa-circle-check" Color="Color.Success">@(new MarkupString("获取变量类实体,可用方法 <code>GlobalData.GetVariable(\"设备名称1\",\"变量名称1\")</code> "))</Alert>
|
||||
<Alert Icon="fa-solid fa-circle-check" Color="Color.Success">@(new MarkupString("详细说明查看文档对应内容页面"))</Alert>
|
||||
|
||||
<Button IsAsync OnClick="GetDemo" class="mt-3" Text="Demo" />
|
||||
|
||||
</div>
|
||||
<div class="col-6 col-md-6">
|
||||
<BootstrapLabel Value=@Localizer["Input"] ShowLabelTooltip="true" />
|
||||
|
@@ -56,4 +56,13 @@ public partial class ScriptCheck
|
||||
}
|
||||
[Inject]
|
||||
private IStringLocalizer<DeviceEditComponent> Localizer { get; set; }
|
||||
|
||||
private async Task GetDemo(Microsoft.AspNetCore.Components.Web.MouseEventArgs args)
|
||||
{
|
||||
Script = OnGetDemo?.Invoke();
|
||||
await Change(Script);
|
||||
}
|
||||
|
||||
[Parameter, EditorRequired]
|
||||
public Func<string> OnGetDemo { get; set; }
|
||||
}
|
||||
|
@@ -44,6 +44,7 @@
|
||||
<EditorItem @bind-Field="@context.Name" Readonly=BatchEditEnable />
|
||||
|
||||
<EditorItem @bind-Field="@context.Description" />
|
||||
<EditorItem @bind-Field="@context.CollectGroup" />
|
||||
<EditorItem @bind-Field="@context.Group" />
|
||||
|
||||
<EditorItem @bind-Field="@context.Unit" />
|
||||
|
@@ -212,8 +212,8 @@ public partial class VariableEditComponent
|
||||
{
|
||||
var component = new BootstrapDynamicComponent(data.VariablePropertyUIType, new Dictionary<string, object?>
|
||||
{
|
||||
[nameof(VariableEditComponent.Model)] = Model,
|
||||
[nameof(DeviceEditComponent.PluginPropertyEditorItems)] = data.EditorItems,
|
||||
[nameof(IPropertyUIBase.Model)] = Model,
|
||||
[nameof(IPropertyUIBase.PluginPropertyEditorItems)] = data.EditorItems,
|
||||
});
|
||||
VariablePropertyRenderFragments.AddOrUpdate(id, component.Render());
|
||||
}
|
||||
|
@@ -7,7 +7,7 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ThingsGateway.Gateway.Application\ThingsGateway.Gateway.Application.csproj" />
|
||||
<PackageReference Include="BootstrapBlazor.UniverSheet" Version="9.0.3" />
|
||||
<PackageReference Include="BootstrapBlazor.UniverSheet" Version="9.0.4" />
|
||||
<PackageReference Include="BootstrapBlazor.WinBox" Version="9.0.7" />
|
||||
<PackageReference Include="BootstrapBlazor.CodeEditor" Version="9.0.1" />
|
||||
<ProjectReference Include="..\..\Admin\ThingsGateway.Admin.Razor\ThingsGateway.Admin.Razor.csproj" />
|
||||
|
@@ -55,7 +55,7 @@ internal sealed class RedundancyHostedService : BackgroundService, IRedundancyHo
|
||||
public string LogPath { get; }
|
||||
private TcpDmtpClient TcpDmtpClient;
|
||||
private TcpDmtpService TcpDmtpService;
|
||||
private TcpDmtpClient GetTcpDmtpClient(RedundancyOptions redundancy)
|
||||
private async Task<TcpDmtpClient> GetTcpDmtpClient(RedundancyOptions redundancy)
|
||||
{
|
||||
_log = new LoggerGroup() { LogLevel = TouchSocket.Core.LogLevel.Trace };
|
||||
_log?.AddLogger(new EasyLogger(Log_Out) { LogLevel = TouchSocket.Core.LogLevel.Trace });
|
||||
@@ -81,11 +81,11 @@ internal sealed class RedundancyHostedService : BackgroundService, IRedundancyHo
|
||||
.SetMaxFailCount(redundancy.MaxErrorCount);
|
||||
});
|
||||
|
||||
tcpDmtpClient.Setup(config);
|
||||
await tcpDmtpClient.SetupAsync(config).ConfigureAwait(false);
|
||||
return tcpDmtpClient;
|
||||
}
|
||||
|
||||
private TcpDmtpService GetTcpDmtpService(RedundancyOptions redundancy)
|
||||
private async Task<TcpDmtpService> GetTcpDmtpService(RedundancyOptions redundancy)
|
||||
{
|
||||
_log = new LoggerGroup() { LogLevel = TouchSocket.Core.LogLevel.Trace };
|
||||
_log?.AddLogger(new EasyLogger(Log_Out) { LogLevel = TouchSocket.Core.LogLevel.Trace });
|
||||
@@ -111,7 +111,7 @@ internal sealed class RedundancyHostedService : BackgroundService, IRedundancyHo
|
||||
.SetMaxFailCount(redundancy.MaxErrorCount);
|
||||
});
|
||||
|
||||
tcpDmtpService.Setup(config);
|
||||
await tcpDmtpService.SetupAsync(config).ConfigureAwait(false);
|
||||
return tcpDmtpService;
|
||||
}
|
||||
|
||||
@@ -325,13 +325,13 @@ internal sealed class RedundancyHostedService : BackgroundService, IRedundancyHo
|
||||
{
|
||||
if (RedundancyOptions.IsMaster)
|
||||
{
|
||||
TcpDmtpService = GetTcpDmtpService(RedundancyOptions);
|
||||
TcpDmtpService = await GetTcpDmtpService(RedundancyOptions).ConfigureAwait(false);
|
||||
await TcpDmtpService.StartAsync().ConfigureAwait(false);//启动
|
||||
await ActiveAsync().ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
TcpDmtpClient = GetTcpDmtpClient(RedundancyOptions);
|
||||
TcpDmtpClient = await GetTcpDmtpClient(RedundancyOptions).ConfigureAwait(false);
|
||||
await StandbyAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
@@ -9,7 +9,64 @@ namespace ThingsGateway.RulesEngine;
|
||||
[CategoryNode(Category = "Actuator", ImgUrl = "_content/ThingsGateway.RulesEngine/img/CSharpScript.svg", Desc = nameof(ExecuteScriptNode), LocalizerType = typeof(ThingsGateway.RulesEngine._Imports), WidgetType = typeof(CSharpScriptWidget))]
|
||||
public class ExecuteScriptNode : TextNode, IActuatorNode, IExexcuteExpressionsBase, IDisposable
|
||||
{
|
||||
public ExecuteScriptNode(string id, Point? position = null) : base(id, position) { Title = "ExecuteScriptNode"; Placeholder = "ExecuteScriptNode.Placeholder"; }
|
||||
public ExecuteScriptNode(string id, Point? position = null) : base(id, position)
|
||||
{
|
||||
Title = "ExecuteScriptNode"; Placeholder = "ExecuteScriptNode.Placeholder";
|
||||
Text =
|
||||
"""
|
||||
using ThingsGateway.RulesEngine;
|
||||
using ThingsGateway.Foundation;
|
||||
using TouchSocket.Core;
|
||||
using System.Text;
|
||||
|
||||
public class TestEx : IExexcuteExpressions
|
||||
{
|
||||
|
||||
public TouchSocket.Core.ILog Logger { get; set; }
|
||||
|
||||
public async System.Threading.Tasks.Task<NodeOutput> ExecuteAsync(NodeInput input, System.Threading.CancellationToken cancellationToken)
|
||||
{
|
||||
|
||||
|
||||
//想上传mqtt,可以自己写mqtt上传代码,或者通过mqtt插件的公开方法上传
|
||||
|
||||
//直接获取mqttclient插件类型的第一个设备
|
||||
var driver = GlobalData.ReadOnlyChannels.FirstOrDefault(a => a.Value.PluginName == "ThingsGateway.Plugin.Mqtt.MqttClient").Value?.ReadDeviceRuntimes?.FirstOrDefault().Value?.Driver;
|
||||
if (driver != null)
|
||||
{
|
||||
//找到对应的MqttClient插件设备
|
||||
var mqttClient = (ThingsGateway.Plugin.Mqtt.MqttClient)driver;
|
||||
if (mqttClient == null)
|
||||
throw new("mqttClient NOT FOUND");
|
||||
var result = await mqttClient.MqttUpAsync("test", Encoding.UTF8.GetBytes("test"),1, default);// 主题 和 负载
|
||||
if (!result.IsSuccess)
|
||||
throw new(result.ErrorMessage);
|
||||
return new NodeOutput() { Value = result };
|
||||
}
|
||||
throw new("mqttClient NOT FOUND");
|
||||
|
||||
|
||||
//通过设备名称找出mqttClient插件
|
||||
//var driver = GlobalData.ReadOnlyDevices.FirstOrDefault(a => a.Value.Name == "mqttDevice1").Value?.Driver;
|
||||
//if (driver != null)
|
||||
//{
|
||||
// //找到对应的MqttClient插件设备
|
||||
// var mqttClient = (ThingsGateway.Plugin.Mqtt.MqttClient)driver;
|
||||
// if (mqttClient == null)
|
||||
// throw new("mqttClient NOT FOUND");
|
||||
// var result = await mqttClient.MqttUpAsync("test", "test", default);// 主题 和 负载
|
||||
// if (!result.IsSuccess)
|
||||
// throw new(result.ErrorMessage);
|
||||
// return new NodeOutput() { Value = result };
|
||||
//}
|
||||
//throw new("mqttClient NOT FOUND");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
""";
|
||||
|
||||
}
|
||||
|
||||
private string text;
|
||||
|
||||
|
@@ -11,7 +11,14 @@ namespace ThingsGateway.RulesEngine;
|
||||
[CategoryNode(Category = "Expression", ImgUrl = "_content/ThingsGateway.RulesEngine/img/CSharpScript.svg", Desc = nameof(ConditionNode), LocalizerType = typeof(ThingsGateway.RulesEngine._Imports), WidgetType = typeof(CSharpScriptWidget))]
|
||||
public class ConditionNode : TextNode, IConditionNode
|
||||
{
|
||||
public ConditionNode(string id, Point? position = null) : base(id, position) { Title = "ConditionNode"; Placeholder = "ConditionNode.Placeholder"; }
|
||||
public ConditionNode(string id, Point? position = null) : base(id, position)
|
||||
{
|
||||
Title = "ConditionNode"; Placeholder = "ConditionNode.Placeholder";
|
||||
Text = "return true;";
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
Task<bool> IConditionNode.ExecuteAsync(NodeInput input, CancellationToken cancellationToken)
|
||||
{
|
||||
|
@@ -8,7 +8,12 @@ namespace ThingsGateway.RulesEngine;
|
||||
[CategoryNode(Category = "Expression", ImgUrl = "_content/ThingsGateway.RulesEngine/img/CSharpScript.svg", Desc = nameof(DataNode), LocalizerType = typeof(ThingsGateway.RulesEngine._Imports), WidgetType = typeof(CSharpScriptWidget))]
|
||||
public class DataNode : TextNode, IExpressionNode
|
||||
{
|
||||
public DataNode(string id, Point? position = null) : base(id, position) { Title = "DataNode"; Placeholder = "DataNode.Placeholder"; }
|
||||
public DataNode(string id, Point? position = null) : base(id, position)
|
||||
{
|
||||
Title = "DataNode"; Placeholder = "DataNode.Placeholder";
|
||||
Text = "return 1;";
|
||||
|
||||
}
|
||||
|
||||
Task<NodeOutput> IExpressionNode.ExecuteAsync(NodeInput input, CancellationToken cancellationToken)
|
||||
{
|
||||
|
@@ -19,7 +19,7 @@ public class DeviceChangedTriggerNode : TextNode, ITriggerNode, IDisposable
|
||||
{
|
||||
Func = func;
|
||||
FuncDict.Add(this, func);
|
||||
if (!DeviceChangedTriggerNodeDict.TryGetValue(Text, out var list))
|
||||
if (!DeviceChangedTriggerNodeDict.TryGetValue(Text ?? string.Empty, out var list))
|
||||
{
|
||||
var deviceChangedTriggerNodes = new ConcurrentList<DeviceChangedTriggerNode>();
|
||||
deviceChangedTriggerNodes.Add(this);
|
||||
@@ -44,7 +44,7 @@ public class DeviceChangedTriggerNode : TextNode, ITriggerNode, IDisposable
|
||||
|
||||
private static void GlobalData_DeviceStatusChangeEvent(DeviceRuntime deviceRunTime, DeviceBasicData deviceData)
|
||||
{
|
||||
if (DeviceChangedTriggerNodeDict.TryGetValue(deviceData.Name, out var deviceChangedTriggerNodes) && deviceChangedTriggerNodes?.Count > 0)
|
||||
if (DeviceChangedTriggerNodeDict.TryGetValue(deviceData.Name ?? string.Empty, out var deviceChangedTriggerNodes) && deviceChangedTriggerNodes?.Count > 0)
|
||||
{
|
||||
if (!DeviceDatas.IsAddingCompleted)
|
||||
{
|
||||
@@ -63,7 +63,7 @@ public class DeviceChangedTriggerNode : TextNode, ITriggerNode, IDisposable
|
||||
return DeviceDatas.GetConsumingEnumerable().ParallelForEachAsync((async (deviceDatas, token) =>
|
||||
{
|
||||
|
||||
if (DeviceChangedTriggerNodeDict.TryGetValue(deviceDatas.Name, out var valueChangedTriggerNodes))
|
||||
if (DeviceChangedTriggerNodeDict.TryGetValue(deviceDatas.Name ?? string.Empty, out var valueChangedTriggerNodes))
|
||||
{
|
||||
await valueChangedTriggerNodes.ParallelForEachAsync(async (item, token) =>
|
||||
{
|
||||
@@ -89,7 +89,7 @@ public class DeviceChangedTriggerNode : TextNode, ITriggerNode, IDisposable
|
||||
public void Dispose()
|
||||
{
|
||||
FuncDict.Remove(this);
|
||||
if (DeviceChangedTriggerNodeDict.TryGetValue(Text, out var list))
|
||||
if (DeviceChangedTriggerNodeDict.TryGetValue(Text ?? string.Empty, out var list))
|
||||
{
|
||||
list.Remove(this);
|
||||
}
|
||||
|
@@ -160,9 +160,19 @@ public class OpcDaMaster : IDisposable
|
||||
/// <returns></returns>
|
||||
public Dictionary<string, List<OpcItem>> AddItemsWithSave(List<string> items)
|
||||
{
|
||||
int i = 0;
|
||||
ItemDicts = items.ConvertAll(o => new OpcItem(o)).ChunkTrivialBetter(OpcDaProperty.GroupSize).ToDictionary(a => "default" + (i++));
|
||||
return ItemDicts;
|
||||
lock (this)
|
||||
{
|
||||
|
||||
int i = ItemDicts.Count;
|
||||
var addItems = items.ConvertAll(o => new OpcItem(o)).ChunkTrivialBetter(OpcDaProperty.GroupSize).ToDictionary(a => "default" + (i++));
|
||||
|
||||
foreach (var item in addItems)
|
||||
{
|
||||
ItemDicts.TryAdd(item.Key, item.Value);
|
||||
}
|
||||
|
||||
return addItems;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@@ -251,7 +251,7 @@ public class OpcUaMaster : IDisposable
|
||||
DisplayName = subscriptionName
|
||||
};
|
||||
List<MonitoredItem> monitoredItems = new();
|
||||
var variableNodes = loadType ? await ReadNodesAsync(items, cancellationToken).ConfigureAwait(false) : null;
|
||||
var variableNodes = loadType ? await ReadNodesAsync(items, false, cancellationToken).ConfigureAwait(false) : null;
|
||||
for (int i = 0; i < items.Length; i++)
|
||||
{
|
||||
try
|
||||
@@ -743,7 +743,7 @@ public class OpcUaMaster : IDisposable
|
||||
NodeId = new NodeId(item.Key),
|
||||
AttributeId = Attributes.Value,
|
||||
};
|
||||
var variableNode = await ReadNodeAsync(item.Key, false, cancellationToken).ConfigureAwait(false);
|
||||
var variableNode = await ReadNodeAsync(item.Key, false, false, cancellationToken).ConfigureAwait(false);
|
||||
var dataValue = JsonUtils.Decode(
|
||||
m_session.MessageContext,
|
||||
variableNode.DataType,
|
||||
@@ -793,9 +793,10 @@ public class OpcUaMaster : IDisposable
|
||||
{
|
||||
if (m_session != null)
|
||||
{
|
||||
var variableNode = ReadNode(monitoreditem.StartNodeId.ToString(), false);
|
||||
foreach (var value in monitoreditem.DequeueValues())
|
||||
{
|
||||
var variableNode = ReadNode(monitoreditem.StartNodeId.ToString(), false, StatusCode.IsGood(value.StatusCode));
|
||||
|
||||
if (value.Value != null)
|
||||
{
|
||||
var data = JsonUtils.Encode(m_session.MessageContext, TypeInfo.GetBuiltInType(variableNode.DataType, m_session.SystemContext.TypeTable), value.Value);
|
||||
@@ -974,7 +975,7 @@ public class OpcUaMaster : IDisposable
|
||||
List<(string, DataValue, JToken)> jTokens = new();
|
||||
for (int i = 0; i < results.Count; i++)
|
||||
{
|
||||
var variableNode = await ReadNodeAsync(nodeIds[i].ToString(), false, cancellationToken).ConfigureAwait(false);
|
||||
var variableNode = await ReadNodeAsync(nodeIds[i].ToString(), false, StatusCode.IsGood(results[i].StatusCode), cancellationToken).ConfigureAwait(false);
|
||||
var type = TypeInfo.GetBuiltInType(variableNode.DataType, m_session.SystemContext.TypeTable);
|
||||
var jToken = JsonUtils.Encode(m_session.MessageContext, type, results[i].Value);
|
||||
jTokens.Add((variableNode.NodeId.ToString(), results[i], jToken));
|
||||
@@ -985,7 +986,7 @@ public class OpcUaMaster : IDisposable
|
||||
/// <summary>
|
||||
/// 从服务器或缓存读取节点
|
||||
/// </summary>
|
||||
private VariableNode ReadNode(string nodeIdStr, bool isOnlyServer = true)
|
||||
private VariableNode ReadNode(string nodeIdStr, bool isOnlyServer = true, bool cache = true)
|
||||
{
|
||||
if (!isOnlyServer)
|
||||
{
|
||||
@@ -1025,14 +1026,15 @@ public class OpcUaMaster : IDisposable
|
||||
|
||||
VariableNode variableNode = GetVariableNodes(itemsToRead, values, diagnosticInfos, responseHeader).FirstOrDefault();
|
||||
|
||||
_variableDicts.AddOrUpdate(nodeIdStr, a => variableNode, (a, b) => variableNode);
|
||||
if (cache)
|
||||
_variableDicts.AddOrUpdate(nodeIdStr, a => variableNode, (a, b) => variableNode);
|
||||
return variableNode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从服务器或缓存读取节点
|
||||
/// </summary>
|
||||
private async Task<VariableNode> ReadNodeAsync(string nodeIdStr, bool isOnlyServer = true, CancellationToken cancellationToken = default)
|
||||
private async Task<VariableNode> ReadNodeAsync(string nodeIdStr, bool isOnlyServer = true, bool cache = true, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!isOnlyServer)
|
||||
{
|
||||
@@ -1073,7 +1075,9 @@ public class OpcUaMaster : IDisposable
|
||||
|
||||
if (OpcUaProperty.LoadType && variableNode.DataType != NodeId.Null && TypeInfo.GetBuiltInType(variableNode.DataType, m_session.SystemContext.TypeTable) == BuiltInType.ExtensionObject)
|
||||
await typeSystem.LoadType(variableNode.DataType, ct: cancellationToken).ConfigureAwait(false);
|
||||
_variableDicts.AddOrUpdate(nodeIdStr, a => variableNode, (a, b) => variableNode);
|
||||
|
||||
if (cache)
|
||||
_variableDicts.AddOrUpdate(nodeIdStr, a => variableNode, (a, b) => variableNode);
|
||||
return variableNode;
|
||||
}
|
||||
|
||||
@@ -1127,7 +1131,7 @@ public class OpcUaMaster : IDisposable
|
||||
/// <summary>
|
||||
/// 从服务器读取节点
|
||||
/// </summary>
|
||||
private async Task<List<Node>> ReadNodesAsync(string[] nodeIdStrs, CancellationToken cancellationToken = default)
|
||||
private async Task<List<Node>> ReadNodesAsync(string[] nodeIdStrs, bool cache = false, CancellationToken cancellationToken = default)
|
||||
{
|
||||
List<Node> result = new(nodeIdStrs.Length);
|
||||
foreach (var items in nodeIdStrs.ChunkBetter(OpcUaProperty.GroupSize))
|
||||
@@ -1171,7 +1175,8 @@ public class OpcUaMaster : IDisposable
|
||||
}
|
||||
else
|
||||
{
|
||||
_variableDicts.AddOrUpdate(nodeIdStrs[i], a => node, (a, b) => node);
|
||||
if (cache)
|
||||
_variableDicts.AddOrUpdate(nodeIdStrs[i], a => node, (a, b) => node);
|
||||
if (node.DataType != NodeId.Null && TypeInfo.GetBuiltInType(node.DataType, m_session.SystemContext.TypeTable) == BuiltInType.ExtensionObject)
|
||||
{
|
||||
await typeSystem.LoadType(node.DataType, ct: cancellationToken).ConfigureAwait(false);
|
||||
|
@@ -398,10 +398,10 @@ public partial class SiemensS7Master : DeviceBase
|
||||
|
||||
try
|
||||
{
|
||||
var result2 = await GetResponsedDataAsync(new S7Send(ISO_CR), channel, Timeout).ConfigureAwait(false);
|
||||
var result2 = await SendThenReturnMessageBaseAsync(new S7Send(ISO_CR), channel).ConfigureAwait(false);
|
||||
if (!result2.IsSuccess)
|
||||
{
|
||||
Logger?.LogWarning(SiemensS7Resource.Localizer["HandshakeError1", channel.ToString(), result2.ErrorMessage]);
|
||||
Logger?.LogWarning(SiemensS7Resource.Localizer["HandshakeError1", channel.ToString(), result2]);
|
||||
await channel.CloseAsync().ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
@@ -409,16 +409,21 @@ public partial class SiemensS7Master : DeviceBase
|
||||
catch (OperationCanceledException) { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger?.LogWarning(SiemensS7Resource.Localizer["HandshakeError1", channel.ToString(), ex.Message]);
|
||||
Logger?.LogWarning(SiemensS7Resource.Localizer["HandshakeError1", channel.ToString(), ex]);
|
||||
await channel.CloseAsync().ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
try
|
||||
{
|
||||
var result2 = await GetResponsedDataAsync(new S7Send(S7_PN), channel, Timeout).ConfigureAwait(false);
|
||||
var result2 = await SendThenReturnMessageBaseAsync(new S7Send(S7_PN), channel).ConfigureAwait(false);
|
||||
if (!result2.IsSuccess)
|
||||
{
|
||||
Logger?.LogWarning(SiemensS7Resource.Localizer["HandshakeError2", channel.ToString(), result2.ErrorMessage]);
|
||||
Logger?.LogWarning(SiemensS7Resource.Localizer["HandshakeError2", channel.ToString(), result2]);
|
||||
await channel.CloseAsync().ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
if (result2.Content == null)
|
||||
{
|
||||
await channel.CloseAsync().ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
@@ -429,7 +434,7 @@ public partial class SiemensS7Master : DeviceBase
|
||||
catch (OperationCanceledException) { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger?.LogWarning(SiemensS7Resource.Localizer["HandshakeError2", channel.ToString(), ex.Message]);
|
||||
Logger?.LogWarning(SiemensS7Resource.Localizer["HandshakeError2", channel.ToString(), ex]);
|
||||
await channel.CloseAsync().ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
|
@@ -12,7 +12,7 @@
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2">
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
@@ -25,11 +25,20 @@
|
||||
<div class="col-12 col-md-12">
|
||||
<BootstrapLabel Value=@SqlDBProducerPropertyLocalizer["BigTextScriptHistoryTable"] ShowLabelTooltip="true" />
|
||||
<CodeEditor @bind-Value=@businessProperty.BigTextScriptHistoryTable Language="csharp" Theme="vs-dark" />
|
||||
|
||||
<div class="ms-2 d-flex justify-content-center align-items-center">
|
||||
<Button IsDisabled=@(!CanWrite) OnClick="()=>CheckScript(businessProperty,nameof(businessProperty.BigTextScriptHistoryTable))">
|
||||
@RazorLocalizer["Check"]
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-12">
|
||||
<BootstrapLabel Value=@SqlDBProducerPropertyLocalizer["BigTextScriptRealTable"] ShowLabelTooltip="true" />
|
||||
<CodeEditor @bind-Value=@businessProperty.BigTextScriptRealTable Language="csharp" Theme="vs-dark" />
|
||||
<div class="ms-2 d-flex justify-content-center align-items-center">
|
||||
<Button IsDisabled=@(!CanWrite) OnClick="()=>CheckScript(businessProperty,nameof(businessProperty.BigTextScriptRealTable))">
|
||||
@RazorLocalizer["Check"]
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</EditTemplate>
|
||||
</EditorItem>
|
||||
|
@@ -8,17 +8,22 @@
|
||||
// QQ群:605534569
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
#pragma warning disable CA2007 // 考虑对等待的任务调用 ConfigureAwait
|
||||
using BootstrapBlazor.Components;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.Extensions.Localization;
|
||||
|
||||
using ThingsGateway.Gateway.Razor;
|
||||
using ThingsGateway.Plugin.SqlDB;
|
||||
using ThingsGateway.Razor;
|
||||
|
||||
namespace ThingsGateway.Debug
|
||||
{
|
||||
public partial class SqlDBProducerPropertyRazor : IPropertyUIBase
|
||||
{
|
||||
[Inject]
|
||||
IStringLocalizer<ThingsGateway.Razor._Imports> RazorLocalizer { get; set; }
|
||||
|
||||
|
||||
[Parameter, EditorRequired]
|
||||
@@ -39,6 +44,144 @@ namespace ThingsGateway.Debug
|
||||
return base.OnParametersSetAsync();
|
||||
}
|
||||
|
||||
private async Task CheckScript(SqlDBProducerProperty businessProperty, string pname)
|
||||
{
|
||||
IEnumerable<object> data = null;
|
||||
string script = null;
|
||||
{
|
||||
data = new List<VariableBasicData>() { new() {
|
||||
Name = "testName",
|
||||
DeviceName = "testDevice",
|
||||
Value = "1",
|
||||
ChangeTime = DateTime.Now,
|
||||
CollectTime = DateTime.Now,
|
||||
Remark1="1",
|
||||
Remark2="2",
|
||||
Remark3="3",
|
||||
Remark4="4",
|
||||
Remark5="5",
|
||||
} ,
|
||||
new() {
|
||||
Name = "testName2",
|
||||
DeviceName = "testDevice",
|
||||
Value = "1",
|
||||
ChangeTime = DateTime.Now,
|
||||
CollectTime = DateTime.Now,
|
||||
Remark1="1",
|
||||
Remark2="2",
|
||||
Remark3="3",
|
||||
Remark4="4",
|
||||
Remark5="5",
|
||||
} };
|
||||
script = pname == businessProperty.BigTextScriptHistoryTable ? businessProperty.BigTextScriptHistoryTable : businessProperty.BigTextScriptRealTable;
|
||||
|
||||
}
|
||||
|
||||
|
||||
var op = new DialogOption()
|
||||
{
|
||||
IsScrolling = true,
|
||||
Title = RazorLocalizer["Check"],
|
||||
ShowFooter = false,
|
||||
ShowCloseButton = false,
|
||||
Size = Size.ExtraExtraLarge,
|
||||
FullScreenSize = FullScreenSize.None
|
||||
};
|
||||
|
||||
op.Component = BootstrapDynamicComponent.CreateComponent<ScriptCheck>(new Dictionary<string, object?>
|
||||
{
|
||||
{nameof(ScriptCheck.Data),data },
|
||||
{nameof(ScriptCheck.Script),script },
|
||||
{nameof(ScriptCheck.OnGetDemo),()=>
|
||||
{
|
||||
return
|
||||
pname == nameof(SqlDBProducerProperty.BigTextScriptHistoryTable)?
|
||||
""""
|
||||
using ThingsGateway.Foundation;
|
||||
|
||||
using System.Dynamic;
|
||||
|
||||
using TouchSocket.Core;
|
||||
public class S1 : DynamicSQLBase
|
||||
{
|
||||
|
||||
public override async Task DBInit(ISqlSugarClient db, CancellationToken cancellationToken)
|
||||
{
|
||||
|
||||
var sql = $"""
|
||||
|
||||
""";
|
||||
await db.Ado.ExecuteCommandAsync(sql, default, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
public override async Task DBInsertable(ISqlSugarClient db, IEnumerable<object> datas, CancellationToken cancellationToken)
|
||||
{
|
||||
var sql = $"""
|
||||
|
||||
""";
|
||||
await db.Ado.ExecuteCommandAsync(sql, default, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
""""
|
||||
:
|
||||
|
||||
pname == nameof(SqlDBProducerProperty.BigTextScriptRealTable)?
|
||||
|
||||
""""
|
||||
|
||||
using System.Dynamic;
|
||||
using ThingsGateway.Foundation;
|
||||
|
||||
|
||||
using TouchSocket.Core;
|
||||
public class S1 : DynamicSQLBase
|
||||
{
|
||||
|
||||
public override async Task DBInit(ISqlSugarClient db, CancellationToken cancellationToken)
|
||||
{
|
||||
|
||||
var sql = $"""
|
||||
|
||||
""";
|
||||
await db.Ado.ExecuteCommandAsync(sql, default, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
public override async Task DBInsertable(ISqlSugarClient db, IEnumerable<object> datas, CancellationToken cancellationToken)
|
||||
{
|
||||
var sql = $"""
|
||||
|
||||
""";
|
||||
await db.Ado.ExecuteCommandAsync(sql, default, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
""""
|
||||
:
|
||||
""
|
||||
;
|
||||
}
|
||||
},
|
||||
{nameof(ScriptCheck.ScriptChanged),EventCallback.Factory.Create<string>(this, v =>
|
||||
{
|
||||
if (pname == nameof(SqlDBProducerProperty.BigTextScriptHistoryTable))
|
||||
{
|
||||
businessProperty.BigTextScriptHistoryTable=v;
|
||||
|
||||
}
|
||||
}
|
||||
else if (pname == nameof(SqlDBProducerProperty.BigTextScriptRealTable))
|
||||
{
|
||||
businessProperty.BigTextScriptRealTable=v;
|
||||
}
|
||||
|
||||
}) },
|
||||
|
||||
});
|
||||
await DialogService.Show(op);
|
||||
|
||||
}
|
||||
[Inject]
|
||||
DialogService DialogService { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
@@ -19,7 +19,7 @@
|
||||
|
||||
|
||||
|
||||
<PackageReference Include="SqlSugar.TDengineCore" Version="4.18.8" GeneratePathProperty="true">
|
||||
<PackageReference Include="SqlSugar.TDengineCore" Version="4.18.31" GeneratePathProperty="true">
|
||||
<PrivateAssets>contentFiles;compile;build;buildMultitargeting;buildTransitive;analyzers;</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
||||
|
@@ -1,32 +1,11 @@
|
||||
<Project>
|
||||
|
||||
|
||||
|
||||
<Target Name="CopyNugetPackages" AfterTargets="Build">
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net6.0'">
|
||||
<!-- setting up the variable for convenience -->
|
||||
<ApplicationPackageFiles Include="$(PkgMQTTnet_AspNetCore)\lib\net6.0\*.*" />
|
||||
<MQTTnetApplicationPackageFiles Include="$(PkgMQTTnet)\lib\net6.0\*.*" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)' != 'net6.0'">
|
||||
<!-- setting up the variable for convenience -->
|
||||
<ApplicationPackageFiles Include="$(PkgMQTTnet_AspNetCore)\lib\net8.0\*.*" />
|
||||
<MQTTnet_ServerApplicationPackageFiles Include="$(PkgMQTTnet_Server)\lib\net8.0\*.*" />
|
||||
<MQTTnetApplicationPackageFiles Include="$(PkgMQTTnet)\lib\net8.0\*.*" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<ApplicationFolder>$(TargetDir)</ApplicationFolder>
|
||||
</PropertyGroup>
|
||||
<Copy SourceFiles="@(ApplicationPackageFiles)" DestinationFolder="$(ApplicationFolder)%(RecursiveDir)" />
|
||||
<Copy SourceFiles="@(MQTTnet_ServerApplicationPackageFiles)" DestinationFolder="$(ApplicationFolder)%(RecursiveDir)" />
|
||||
<Copy SourceFiles="@(MQTTnetApplicationPackageFiles)" DestinationFolder="$(ApplicationFolder)%(RecursiveDir)" />
|
||||
</Target>
|
||||
|
||||
|
||||
<!--在构建后触发的。它通过在 Nuget 包的 Content 文件夹中包含目标目录中的所有文件和子文件夹来创建 nuget 包-->
|
||||
<Target Name="IncludeAllFilesInTargetDir" AfterTargets="Build">
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="$(ProjectDir)bin\$(Configuration)\$(TargetFramework)\**\*MQTT*.dll">
|
||||
<Content Include="$(ProjectDir)bin\$(Configuration)\$(TargetFramework)\**\*Http*.dll">
|
||||
<Pack>true</Pack>
|
||||
<PackagePath>Content</PackagePath>
|
||||
</Content>
|
||||
|
@@ -80,6 +80,11 @@ public class ModbusMaster : CollectFoundationBase
|
||||
protected override async Task<List<VariableSourceRead>> ProtectedLoadSourceReadAsync(List<VariableRuntime> deviceVariables)
|
||||
{
|
||||
await Task.CompletedTask.ConfigureAwait(false);
|
||||
return _plc.LoadSourceRead<VariableSourceRead>(deviceVariables, _driverPropertys.MaxPack, CurrentDevice.IntervalTime);
|
||||
List<VariableSourceRead> variableSourceReads = new();
|
||||
foreach (var deviceVariable in deviceVariables.GroupBy(a => a.CollectGroup))
|
||||
{
|
||||
variableSourceReads.AddRange(_plc.LoadSourceRead<VariableSourceRead>(deviceVariable, _driverPropertys.MaxPack, CurrentDevice.IntervalTime));
|
||||
}
|
||||
return variableSourceReads;
|
||||
}
|
||||
}
|
||||
|
@@ -46,7 +46,6 @@ namespace ThingsGateway.Plugin.Mqtt
|
||||
if (mqttClientProperty.TLS == true)
|
||||
{
|
||||
{
|
||||
|
||||
var filePath = Path.Combine("PluginFile", Id, nameof(mqttClientProperty.CAFile));
|
||||
if (!Directory.Exists(filePath))//如果不存在就创建文件夹
|
||||
Directory.CreateDirectory(filePath);
|
||||
@@ -230,7 +229,6 @@ namespace ThingsGateway.Plugin.Mqtt
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
var op = new DialogOption()
|
||||
{
|
||||
IsScrolling = true,
|
||||
@@ -245,6 +243,142 @@ namespace ThingsGateway.Plugin.Mqtt
|
||||
{
|
||||
{nameof(ScriptCheck.Data),data },
|
||||
{nameof(ScriptCheck.Script),script },
|
||||
{nameof(ScriptCheck.OnGetDemo),()=>
|
||||
{
|
||||
return
|
||||
pname == nameof(BusinessPropertyWithCacheIntervalScript.BigTextScriptDeviceModel)?
|
||||
"""
|
||||
|
||||
using TouchSocket.Core;
|
||||
public class S1 : IDynamicModel
|
||||
{
|
||||
public IEnumerable<dynamic> GetList(IEnumerable<object> datas)
|
||||
{
|
||||
List<ExpandoObject> deviceObjs = new List<ExpandoObject>();
|
||||
foreach (var v in datas)
|
||||
{
|
||||
var device = (DeviceBasicData)v;
|
||||
var expando = new ExpandoObject();
|
||||
var deviceObj = new ExpandoObject();
|
||||
|
||||
deviceObj.TryAdd(nameof(Device.Description), device.Description);
|
||||
deviceObj.TryAdd(nameof(DeviceBasicData.ActiveTime), device.ActiveTime);
|
||||
deviceObj.TryAdd(nameof(DeviceBasicData.DeviceStatus), device.DeviceStatus.ToString());
|
||||
deviceObj.TryAdd(nameof(DeviceBasicData.LastErrorMessage), device.LastErrorMessage);
|
||||
deviceObj.TryAdd(nameof(DeviceBasicData.PluginName), device.PluginName);
|
||||
deviceObj.TryAdd(nameof(DeviceBasicData.Remark1), device.Remark1);
|
||||
deviceObj.TryAdd(nameof(DeviceBasicData.Remark2), device.Remark2);
|
||||
deviceObj.TryAdd(nameof(DeviceBasicData.Remark3), device.Remark3);
|
||||
deviceObj.TryAdd(nameof(DeviceBasicData.Remark4), device.Remark4);
|
||||
deviceObj.TryAdd(nameof(DeviceBasicData.Remark5), device.Remark5);
|
||||
|
||||
|
||||
expando.TryAdd(nameof(Device.Name), deviceObj);
|
||||
|
||||
}
|
||||
return deviceObjs;
|
||||
}
|
||||
}
|
||||
|
||||
"""
|
||||
:
|
||||
|
||||
pname == nameof(BusinessPropertyWithCacheIntervalScript.BigTextScriptVariableModel)?
|
||||
|
||||
"""
|
||||
|
||||
using TouchSocket.Core;
|
||||
public class S2 : IDynamicModel
|
||||
{
|
||||
public IEnumerable<dynamic> GetList(IEnumerable<object> datas)
|
||||
{
|
||||
|
||||
List<ExpandoObject> deviceObjs = new List<ExpandoObject>();
|
||||
//按设备名称分组
|
||||
var groups = datas.Where(a => !string.IsNullOrEmpty(((VariableBasicData)a).DeviceName)).GroupBy(a => ((VariableBasicData)a).DeviceName, a => ((VariableBasicData)a));
|
||||
foreach (var group in groups)
|
||||
{
|
||||
//按采集时间分组
|
||||
var data = group.GroupBy(a => a.CollectTime.DateTimeToUnixTimestamp());
|
||||
var deviceObj = new ExpandoObject();
|
||||
List<ExpandoObject> expandos = new List<ExpandoObject>();
|
||||
foreach (var item in data)
|
||||
{
|
||||
var expando = new ExpandoObject();
|
||||
expando.TryAdd("ts", item.Key);
|
||||
var variableObj = new ExpandoObject();
|
||||
foreach (var tag in item)
|
||||
{
|
||||
variableObj.TryAdd(tag.Name, tag.Value);
|
||||
}
|
||||
expando.TryAdd("values", variableObj);
|
||||
|
||||
expandos.Add(expando);
|
||||
}
|
||||
deviceObj.TryAdd(group.Key, expandos);
|
||||
deviceObjs.Add(deviceObj);
|
||||
}
|
||||
|
||||
return deviceObjs;
|
||||
}
|
||||
}
|
||||
|
||||
"""
|
||||
:
|
||||
|
||||
pname == nameof(BusinessPropertyWithCacheIntervalScript.BigTextScriptAlarmModel)?
|
||||
|
||||
"""
|
||||
|
||||
using TouchSocket.Core;
|
||||
public class DeviceScript : IDynamicModel
|
||||
{
|
||||
public IEnumerable<dynamic> GetList(IEnumerable<object> datas)
|
||||
{
|
||||
|
||||
List<ExpandoObject> deviceObjs = new List<ExpandoObject>();
|
||||
//按设备名称分组
|
||||
var groups = datas.Where(a => !string.IsNullOrEmpty(((AlarmVariable)a).DeviceName)).GroupBy(a => ((AlarmVariable)a).DeviceName, a => ((AlarmVariable)a));
|
||||
foreach (var group in groups)
|
||||
{
|
||||
//按采集时间分组
|
||||
var data = group.GroupBy(a => a.AlarmTime.DateTimeToUnixTimestamp());
|
||||
var deviceObj = new ExpandoObject();
|
||||
List<ExpandoObject> expandos = new List<ExpandoObject>();
|
||||
foreach (var item in data)
|
||||
{
|
||||
var expando = new ExpandoObject();
|
||||
expando.TryAdd("ts", item.Key);
|
||||
var variableObj = new ExpandoObject();
|
||||
foreach (var tag in item)
|
||||
{
|
||||
var alarmObj = new ExpandoObject();
|
||||
alarmObj.TryAdd(nameof(tag.AlarmCode), tag.AlarmCode);
|
||||
alarmObj.TryAdd(nameof(tag.AlarmText), tag.AlarmText);
|
||||
alarmObj.TryAdd(nameof(tag.AlarmType), tag.AlarmType);
|
||||
alarmObj.TryAdd(nameof(tag.AlarmLimit), tag.AlarmLimit);
|
||||
alarmObj.TryAdd(nameof(tag.EventTime), tag.EventTime);
|
||||
alarmObj.TryAdd(nameof(tag.EventType), tag.EventType);
|
||||
|
||||
variableObj.TryAdd(tag.Name, alarmObj);
|
||||
}
|
||||
expando.TryAdd("alarms", variableObj);
|
||||
|
||||
expandos.Add(expando);
|
||||
}
|
||||
deviceObj.TryAdd(group.Key, expandos);
|
||||
deviceObjs.Add(deviceObj);
|
||||
}
|
||||
|
||||
return deviceObjs;
|
||||
}
|
||||
}
|
||||
"""
|
||||
:
|
||||
""
|
||||
;
|
||||
}
|
||||
},
|
||||
{nameof(ScriptCheck.ScriptChanged),EventCallback.Factory.Create<string>(this, v =>
|
||||
{
|
||||
if (pname == nameof(BusinessPropertyWithCacheIntervalScript.BigTextScriptAlarmModel))
|
||||
@@ -269,6 +403,7 @@ namespace ThingsGateway.Plugin.Mqtt
|
||||
}
|
||||
|
||||
|
||||
|
||||
[Inject]
|
||||
private DialogService DialogService { get; set; }
|
||||
}
|
||||
|
@@ -122,25 +122,31 @@ public class OpcDaMaster : CollectBase
|
||||
{
|
||||
if (deviceVariables.Count > 0)
|
||||
{
|
||||
var result = _plc.AddItemsWithSave(deviceVariables.Where(a => !string.IsNullOrEmpty(a.RegisterAddress)).Select(a => a.RegisterAddress!).ToList());
|
||||
var sourVars = result?.Select(
|
||||
it =>
|
||||
{
|
||||
var read = new VariableSourceRead()
|
||||
{
|
||||
TimeTick = new(_driverProperties.UpdateRate.ToString()),
|
||||
RegisterAddress = it.Key,
|
||||
};
|
||||
HashSet<string> ids = new(it.Value.Select(b => b.ItemID));
|
||||
List<VariableSourceRead> variableSourceReads = new List<VariableSourceRead>();
|
||||
foreach (var deviceVariableGroups in deviceVariables.GroupBy(a => a.CollectGroup))
|
||||
{
|
||||
|
||||
var variables = deviceVariables.Where(a => ids.Contains(a.RegisterAddress));
|
||||
foreach (var v in variables)
|
||||
var result = _plc.AddItemsWithSave(deviceVariableGroups.Where(a => !string.IsNullOrEmpty(a.RegisterAddress)).Select(a => a.RegisterAddress!).ToList());
|
||||
var sourVars = result?.Select(
|
||||
it =>
|
||||
{
|
||||
read.AddVariable(v);
|
||||
}
|
||||
return read;
|
||||
}).ToList();
|
||||
return sourVars;
|
||||
var read = new VariableSourceRead()
|
||||
{
|
||||
TimeTick = new(_driverProperties.UpdateRate.ToString()),
|
||||
RegisterAddress = it.Key,
|
||||
};
|
||||
HashSet<string> ids = new(it.Value.Select(b => b.ItemID));
|
||||
|
||||
var variables = deviceVariableGroups.Where(a => ids.Contains(a.RegisterAddress));
|
||||
foreach (var v in variables)
|
||||
{
|
||||
read.AddVariable(v);
|
||||
}
|
||||
return read;
|
||||
}).ToList();
|
||||
variableSourceReads.AddRange(sourVars);
|
||||
}
|
||||
return variableSourceReads;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@@ -187,24 +187,27 @@ public class OpcUaMaster : CollectBase
|
||||
await Task.CompletedTask.ConfigureAwait(false);
|
||||
if (deviceVariables.Count > 0)
|
||||
{
|
||||
var dataLists = deviceVariables.ChunkBetter(_driverProperties.GroupSize);
|
||||
|
||||
var dataResult = new List<VariableSourceRead>();
|
||||
foreach (var variable in dataLists)
|
||||
List<VariableSourceRead> variableSourceReads = new List<VariableSourceRead>();
|
||||
foreach (var deviceVariableGroups in deviceVariables.GroupBy(a => a.CollectGroup))
|
||||
{
|
||||
var sourVars = new VariableSourceRead()
|
||||
{
|
||||
TimeTick = new(_driverProperties.UpdateRate.ToString()),
|
||||
RegisterAddress = Guid.NewGuid().ToString(),
|
||||
};
|
||||
foreach (var item in variable)
|
||||
{
|
||||
sourVars.AddVariable(item);
|
||||
}
|
||||
dataResult.Add(sourVars);
|
||||
}
|
||||
var dataLists = deviceVariableGroups.ChunkBetter(_driverProperties.GroupSize);
|
||||
|
||||
return dataResult;
|
||||
foreach (var variable in dataLists)
|
||||
{
|
||||
var sourVars = new VariableSourceRead()
|
||||
{
|
||||
TimeTick = new(_driverProperties.UpdateRate.ToString()),
|
||||
RegisterAddress = Guid.NewGuid().ToString(),
|
||||
};
|
||||
foreach (var item in variable)
|
||||
{
|
||||
sourVars.AddVariable(item);
|
||||
}
|
||||
variableSourceReads.Add(sourVars);
|
||||
}
|
||||
|
||||
}
|
||||
return variableSourceReads;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@@ -202,6 +202,7 @@ public partial class OpcUaServer : BusinessBase
|
||||
success = true;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (success)
|
||||
|
@@ -221,7 +221,12 @@ public class SiemensS7Master : CollectFoundationBase
|
||||
{
|
||||
|
||||
}
|
||||
return _plc.LoadSourceRead<VariableSourceRead>(deviceVariables, _plc.OnLine ? _plc.PduLength : _driverPropertys.MaxPack, CurrentDevice.IntervalTime);
|
||||
List<VariableSourceRead> variableSourceReads = new();
|
||||
foreach (var deviceVariable in deviceVariables.GroupBy(a => a.CollectGroup))
|
||||
{
|
||||
variableSourceReads.AddRange(_plc.LoadSourceRead<VariableSourceRead>(deviceVariable, _driverPropertys.MaxPack, CurrentDevice.IntervalTime));
|
||||
}
|
||||
return variableSourceReads;
|
||||
}
|
||||
finally { }
|
||||
}
|
||||
|
39
src/ThingsGateway.Photino/Properties/launchSettings.json
Normal file
39
src/ThingsGateway.Photino/Properties/launchSettings.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"dotnetRunMessages": true,
|
||||
"applicationUrl": "http://*:5000"
|
||||
},
|
||||
"demo": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Demo"
|
||||
},
|
||||
"dotnetRunMessages": true,
|
||||
"applicationUrl": "http://*:5000"
|
||||
},
|
||||
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
},
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://*:59494/",
|
||||
"sslPort": 44372
|
||||
}
|
||||
}
|
||||
}
|
@@ -11,6 +11,9 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
|
||||
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
@@ -24,6 +27,7 @@ using Microsoft.Extensions.Options;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Unicode;
|
||||
@@ -273,6 +277,21 @@ public class Startup : AppStartup
|
||||
|
||||
services.AddSignalR();
|
||||
#endregion
|
||||
|
||||
|
||||
#if NET9_0_OR_GREATER
|
||||
var certificate = X509CertificateLoader.LoadPkcs12FromFile("ThingsGateway.pfx", "ThingsGateway", X509KeyStorageFlags.EphemeralKeySet);
|
||||
#else
|
||||
var certificate = new X509Certificate2("ThingsGateway.pfx", "ThingsGateway", X509KeyStorageFlags.EphemeralKeySet);
|
||||
#endif
|
||||
services.AddDataProtection()
|
||||
.PersistKeysToFileSystem(new DirectoryInfo("../keys"))
|
||||
.ProtectKeysWithCertificate(certificate)
|
||||
.UseCryptographicAlgorithms(new AuthenticatedEncryptorConfiguration
|
||||
{
|
||||
EncryptionAlgorithm = EncryptionAlgorithm.AES_256_CBC,
|
||||
ValidationAlgorithm = ValidationAlgorithm.HMACSHA256
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
@@ -3,7 +3,7 @@
|
||||
<Import Project="$(SolutionDir)Version.props" />
|
||||
|
||||
|
||||
<ItemGroup Condition=" '$(SolutionName)' != 'ThingsGatewayRelease' " >
|
||||
<ItemGroup Condition=" '$(SolutionName)' != 'ThingsGatewayRelease' ">
|
||||
<ProjectReference Include="..\Gateway\ThingsGateway.Gateway.Application\ThingsGateway.Gateway.Application.csproj" />
|
||||
<ProjectReference Include="..\Gateway\ThingsGateway.Gateway.Razor\ThingsGateway.Gateway.Razor.csproj" />
|
||||
<ProjectReference Include="..\Gateway\ThingsGateway.Management\ThingsGateway.Management.csproj" />
|
||||
@@ -19,8 +19,8 @@
|
||||
<!--nuget包解压复制文件,上下文动态加载,网关管理和网关冗余-->
|
||||
<Import Project="..\ThingsGateway.Server\targets\GatewayOther.targets" Condition=" '$(SolutionName)' == 'ThingsGatewayRelease' " />
|
||||
|
||||
<ItemGroup Condition=" '$(SolutionName)' == 'ThingsGatewayRelease' " >
|
||||
<PackageReference Include="ThingsGateway.Photino.Blazor" Version="$(Version)" />
|
||||
<ItemGroup Condition=" '$(SolutionName)' == 'ThingsGatewayRelease' ">
|
||||
<PackageReference Include="ThingsGateway.Photino.Blazor" Version="$(Version)" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -29,15 +29,17 @@
|
||||
<!--直接引用-->
|
||||
<Import Project="..\ThingsGateway.Server\targets\PluginDebug.targets" Condition=" '$(SolutionName)' != 'ThingsGatewayPro' AND '$(Configuration)' == 'Debug' " />
|
||||
|
||||
<!--直接引用Pro-->
|
||||
<Import Project="..\ThingsGateway.Server\targets\PluginDebug.targets" Condition=" '$(SolutionName)' == 'ThingsGatewayPro' AND '$(Configuration)' == 'Debug'" />
|
||||
<Import Project="..\ThingsGateway.Server\targets\PluginContext.targets" Condition=" '$(SolutionName)' == 'ThingsGatewayPro' AND '$(Configuration)' != 'Debug' " />
|
||||
|
||||
<!--nuget包解压复制文件,插件域隔离动态加载-->
|
||||
<!--<Import Project="targets\Plugin.targets" />-->
|
||||
|
||||
<!--nuget包解压复制文件,上下文动态加载,Pro插件-->
|
||||
<Import Project="..\ThingsGateway.Server\targets\Pro2.targets" Condition=" '$(SolutionName)' != 'ThingsGatewayPro' OR '$(Configuration)' != 'Debug'" />
|
||||
|
||||
<!--直接引用Pro-->
|
||||
<Import Project="..\ThingsGateway.Server\targets\PluginDebug.targets" Condition=" '$(SolutionName)' == 'ThingsGatewayPro' AND '$(Configuration)' == 'Debug'" />
|
||||
<Import Project="..\ThingsGateway.Server\targets\PluginContext.targets" Condition=" '$(SolutionName)' == 'ThingsGatewayPro' AND '$(Configuration)' != 'Debug' " />
|
||||
|
||||
|
||||
<Import Project="..\ThingsGateway.Server\targets\ProPluginDebug.targets" Condition=" '$(SolutionName)' == 'ThingsGatewayPro' AND '$(Configuration)' == 'Debug'" />
|
||||
|
||||
@@ -87,6 +89,9 @@
|
||||
<Compile Include="..\ThingsGateway.Server\Program\SingleFilePublish.cs" Link="Program\SingleFilePublish.cs" />
|
||||
|
||||
|
||||
<Content Include="..\ThingsGateway.Server\ThingsGateway.pfx" Link="ThingsGateway.pfx">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
|
||||
|
||||
<Content Include="..\ThingsGateway.Server\wwwroot\favicon.ico" Link="wwwroot\favicon.ico" />
|
||||
@@ -119,11 +124,23 @@
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<Content Remove="launchSettings.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="favicon.ico">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="Properties\launchSettings.json">
|
||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
</Project>
|
||||
|
@@ -10,6 +10,9 @@
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
|
||||
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
@@ -18,6 +21,7 @@ using Microsoft.Extensions.Options;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Unicode;
|
||||
@@ -291,6 +295,21 @@ public class Startup : AppStartup
|
||||
services.AddAuthorizationCore();
|
||||
services.AddScoped<IAuthorizationHandler, BlazorServerAuthenticationHandler>();
|
||||
services.AddScoped<AuthenticationStateProvider, BlazorServerAuthenticationStateProvider>();
|
||||
|
||||
|
||||
#if NET9_0_OR_GREATER
|
||||
var certificate = X509CertificateLoader.LoadPkcs12FromFile("ThingsGateway.pfx", "ThingsGateway", X509KeyStorageFlags.EphemeralKeySet);
|
||||
#else
|
||||
var certificate = new X509Certificate2("ThingsGateway.pfx", "ThingsGateway", X509KeyStorageFlags.EphemeralKeySet);
|
||||
#endif
|
||||
services.AddDataProtection()
|
||||
.PersistKeysToFileSystem(new DirectoryInfo("../keys"))
|
||||
.ProtectKeysWithCertificate(certificate)
|
||||
.UseCryptographicAlgorithms(new AuthenticatedEncryptorConfiguration
|
||||
{
|
||||
EncryptionAlgorithm = EncryptionAlgorithm.AES_256_CBC,
|
||||
ValidationAlgorithm = ValidationAlgorithm.HMACSHA256
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
@@ -95,6 +95,9 @@
|
||||
<Content Include="favicon.ico">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<None Update="ThingsGateway.pfx">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="WindowsService">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
|
BIN
src/ThingsGateway.Server/ThingsGateway.pfx
Normal file
BIN
src/ThingsGateway.Server/ThingsGateway.pfx
Normal file
Binary file not shown.
@@ -3,6 +3,7 @@
|
||||
|
||||
"ConfigurationScanDirectories": [ "Configuration", "" ], // 扫描配置文件json文件夹(自动合并该文件夹里面所有json文件)
|
||||
"IgnoreConfigurationFiles": [ "" ],
|
||||
"ExternalAssemblies": [ "" ]
|
||||
"ExternalAssemblies": [ "" ],
|
||||
"DetailedErrors": true
|
||||
|
||||
}
|
||||
|
@@ -162,7 +162,7 @@ public class FileHostService : BackgroundService, IFileHostService
|
||||
_log.Exception(ex);
|
||||
try
|
||||
{
|
||||
await TcpDmtpService.StopAsync().ConfigureAwait(false);
|
||||
await TcpDmtpService.StopAsync(default).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
@@ -16,7 +16,7 @@
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="TouchSocket.Dmtp" Version="3.1.0" />
|
||||
<PackageReference Include="TouchSocket.Dmtp" Version="3.1.2" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
|
@@ -10,6 +10,9 @@
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
|
||||
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
@@ -18,6 +21,7 @@ using Microsoft.Extensions.Options;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Unicode;
|
||||
@@ -297,6 +301,21 @@ public class Startup : AppStartup
|
||||
services.AddAuthorizationCore();
|
||||
services.AddScoped<IAuthorizationHandler, BlazorServerAuthenticationHandler>();
|
||||
services.AddScoped<AuthenticationStateProvider, BlazorServerAuthenticationStateProvider>();
|
||||
|
||||
#if NET9_0_OR_GREATER
|
||||
var certificate = X509CertificateLoader.LoadPkcs12FromFile("ThingsGateway.pfx", "ThingsGateway", X509KeyStorageFlags.EphemeralKeySet);
|
||||
#else
|
||||
var certificate = new X509Certificate2("ThingsGateway.pfx", "ThingsGateway",X509KeyStorageFlags.EphemeralKeySet);
|
||||
#endif
|
||||
services.AddDataProtection()
|
||||
.PersistKeysToFileSystem(new DirectoryInfo("../keys"))
|
||||
.ProtectKeysWithCertificate(certificate)
|
||||
.UseCryptographicAlgorithms(new AuthenticatedEncryptorConfiguration
|
||||
{
|
||||
EncryptionAlgorithm = EncryptionAlgorithm.AES_256_CBC,
|
||||
ValidationAlgorithm = ValidationAlgorithm.HMACSHA256
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@@ -76,6 +76,9 @@
|
||||
<None Update="pm2-linux.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="ThingsGateway.pfx">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="WindowsServiceDelete.bat">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
|
BIN
src/Upgrade/ThingsGateway.UpgradeServer/ThingsGateway.pfx
Normal file
BIN
src/Upgrade/ThingsGateway.UpgradeServer/ThingsGateway.pfx
Normal file
Binary file not shown.
@@ -1,5 +1,5 @@
|
||||
cd ..
|
||||
sc create ThingsGateway binPath= %~dp0ThingsGateway.AdminServer.exe start= auto
|
||||
sc description ThingsGateway "ThingsGateway"
|
||||
Net Start ThingsGateway
|
||||
sc create ThingsGateway binPath= %~dp0ThingsGateway.UpgradeServer.exe start= auto
|
||||
sc description ThingsGateway.UpgradeServer "ThingsGateway.UpgradeServer"
|
||||
Net Start ThingsGateway.UpgradeServer
|
||||
pause
|
||||
|
@@ -1,3 +1,3 @@
|
||||
net stop ThingsGateway
|
||||
sc delete ThingsGateway
|
||||
net stop ThingsGateway.UpgradeServer
|
||||
sc delete ThingsGateway.UpgradeServer
|
||||
pause
|
||||
|
@@ -1,8 +1,7 @@
|
||||
{
|
||||
"urls": "http://*:5500",
|
||||
|
||||
"ConfigurationScanDirectories": [ "Configuration", "" ], // 扫描配置文件json文件夹(自动合并该文件夹里面所有json文件)
|
||||
"IgnoreConfigurationFiles": [ "" ],
|
||||
"ExternalAssemblies": [ "" ]
|
||||
"ExternalAssemblies": [ "" ],
|
||||
"DetailedErrors": true
|
||||
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@
|
||||
|
||||
"ConfigurationScanDirectories": [ "Configuration", "" ], // 扫描配置文件json文件夹(自动合并该文件夹里面所有json文件)
|
||||
"IgnoreConfigurationFiles": [ "" ],
|
||||
"ExternalAssemblies": [ "" ]
|
||||
"ExternalAssemblies": [ "" ],
|
||||
"DetailedErrors": true
|
||||
|
||||
}
|
||||
|
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"apps": {
|
||||
"name": "ThingsGateway",
|
||||
"script": "dotnet",
|
||||
"exec_mode": "fork",
|
||||
"error_file": "logs/pm2err.log",
|
||||
"out_file": "logs/pm2out.log",
|
||||
"merge_logs": true,
|
||||
"log_date_format": "YYYY-MM-DD HH:mm:ss",
|
||||
"min_uptime": "60s",
|
||||
"max_restarts": 30,
|
||||
"autorestart": true,
|
||||
"restart_delay": "60",
|
||||
"args": [
|
||||
"ThingsGateway.UpgradeServer.dll",
|
||||
" --urls=http://*:5000"
|
||||
],
|
||||
"env": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Production"
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"configProperties": {
|
||||
"System.Runtime.EnableWriteXorExecute": false
|
||||
}
|
||||
}
|
@@ -1,6 +1,6 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Version>10.5.5</Version>
|
||||
<Version>10.5.17</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
Reference in New Issue
Block a user