Compare commits

...

3 Commits

Author SHA1 Message Date
Diego
4ce843182f build: 10.9.67
feat: 报警恢复增加延时功能
refactor: 移除通讯任务时,设置设备、变量离线状态

fix: 罗克韦尔PLC通讯超时错误
2025-07-24 13:25:55 +08:00
2248356998 qq.com
a4aa000cf0 10.9.66 2025-07-23 19:11:39 +08:00
Diego
01aa6ca066 10.9.65 2025-07-23 16:34:02 +08:00
32 changed files with 413 additions and 177 deletions

View File

@@ -13,7 +13,7 @@
<ItemGroup>
<PackageReference Include="BootstrapBlazor.TableExport" Version="9.2.6" />
<PackageReference Include="Yitter.IdGenerator" Version="1.0.14" />
<PackageReference Include="BootstrapBlazor" Version="9.8.1" />
<PackageReference Include="BootstrapBlazor" Version="9.8.2" />
</ItemGroup>
<ItemGroup>

View File

@@ -62,7 +62,7 @@ public class SugarAopService : ISugarAopService
if (ex.Parameters == null) return;
Console.ForegroundColor = ConsoleColor.Red;
DbContext.WriteLog($"{config.ConfigId}库操作异常");
DbContext.WriteErrorLogWithSql(UtilMethods.GetNativeSql(ex.Sql, (SugarParameter[])ex.Parameters));
DbContext.WriteErrorLogWithSql(UtilMethods.GetNativeSql(ex.Sql, (IReadOnlyList<SugarParameter>)ex.Parameters));
NewLife.Log.XTrace.WriteException(ex);
Console.ForegroundColor = ConsoleColor.White;
};

View File

@@ -5,7 +5,7 @@
<TargetFrameworks>net8.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BootstrapBlazor.FontAwesome" Version="9.0.2" />
<PackageReference Include="BootstrapBlazor.FontAwesome" Version="9.1.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,10 +1,10 @@
<Project>
<PropertyGroup>
<PluginVersion>10.9.64</PluginVersion>
<ProPluginVersion>10.9.64</ProPluginVersion>
<DefaultVersion>10.9.64</DefaultVersion>
<AuthenticationVersion>2.9.24</AuthenticationVersion>
<PluginVersion>10.9.67</PluginVersion>
<ProPluginVersion>10.9.67</ProPluginVersion>
<DefaultVersion>10.9.67</DefaultVersion>
<AuthenticationVersion>2.9.27</AuthenticationVersion>
<SourceGeneratorVersion>10.9.24</SourceGeneratorVersion>
<NET8Version>8.0.18</NET8Version>
<NET9Version>9.0.7</NET9Version>

View File

@@ -18,46 +18,71 @@
<EditTemplate Context="value">
<div class="col-12 col-sm-6 col-md-4">
<Select SkipValidate="true" @bind-Value="@value.ChannelType" OnSelectedItemChanged=@((a)=>
{
return InvokeAsync(StateHasChanged);
}) />
{
return InvokeAsync(StateHasChanged);
}) />
</div>
</EditTemplate>
</EditorItem>
</EditorItem>
<EditorItem @bind-Field="@context.RemoteUrl" Ignore=@(context.ChannelType!=ChannelTypeEnum.TcpClient&&context.ChannelType!=ChannelTypeEnum.UdpSession) />
<EditorItem @bind-Field="@context.BindUrl" Ignore=@(context.ChannelType!=ChannelTypeEnum.TcpClient&&context.ChannelType!=ChannelTypeEnum.UdpSession&&context.ChannelType!=ChannelTypeEnum.TcpService) />
<EditorItem @bind-Field="@context.RemoteUrl" Ignore=@(context.ChannelType != ChannelTypeEnum.TcpClient && context.ChannelType != ChannelTypeEnum.UdpSession) />
<EditorItem @bind-Field="@context.BindUrl" Ignore=@(context.ChannelType != ChannelTypeEnum.TcpClient && context.ChannelType != ChannelTypeEnum.UdpSession && context.ChannelType != ChannelTypeEnum.TcpService) />
<EditorItem @bind-Field="@context.PortName" Ignore=@(context.ChannelType!=ChannelTypeEnum.SerialPort) />
<EditorItem @bind-Field="@context.BaudRate" Ignore=@(context.ChannelType!=ChannelTypeEnum.SerialPort) />
<EditorItem @bind-Field="@context.DataBits" Ignore=@(context.ChannelType!=ChannelTypeEnum.SerialPort) />
<EditorItem @bind-Field="@context.Parity" Ignore=@(context.ChannelType!=ChannelTypeEnum.SerialPort) />
<EditorItem @bind-Field="@context.StopBits" Ignore=@(context.ChannelType!=ChannelTypeEnum.SerialPort) />
<EditorItem @bind-Field="@context.DtrEnable" Ignore=@(context.ChannelType!=ChannelTypeEnum.SerialPort) />
<EditorItem @bind-Field="@context.RtsEnable" Ignore=@(context.ChannelType!=ChannelTypeEnum.SerialPort) />
<EditorItem @bind-Field="@context.StreamAsync" Ignore=@(context.ChannelType!=ChannelTypeEnum.SerialPort) />
<EditorItem @bind-Field="@context.PortName" Ignore=@(context.ChannelType != ChannelTypeEnum.SerialPort) />
<EditorItem @bind-Field="@context.BaudRate" Ignore=@(context.ChannelType != ChannelTypeEnum.SerialPort) />
<EditorItem @bind-Field="@context.DataBits" Ignore=@(context.ChannelType != ChannelTypeEnum.SerialPort) />
<EditorItem @bind-Field="@context.Parity" Ignore=@(context.ChannelType != ChannelTypeEnum.SerialPort) />
<EditorItem @bind-Field="@context.StopBits" Ignore=@(context.ChannelType != ChannelTypeEnum.SerialPort) />
<EditorItem @bind-Field="@context.DtrEnable" Ignore=@(context.ChannelType != ChannelTypeEnum.SerialPort) />
<EditorItem @bind-Field="@context.RtsEnable" Ignore=@(context.ChannelType != ChannelTypeEnum.SerialPort) />
<EditorItem @bind-Field="@context.StreamAsync" Ignore=@(context.ChannelType != ChannelTypeEnum.SerialPort) />
<EditorItem @bind-Field="@context.CacheTimeout" Ignore=@(context.ChannelType==ChannelTypeEnum.UdpSession||context.ChannelType==ChannelTypeEnum.Other) />
<EditorItem @bind-Field="@context.ConnectTimeout" Ignore=@(context.ChannelType==ChannelTypeEnum.UdpSession||context.ChannelType==ChannelTypeEnum.TcpService||context.ChannelType==ChannelTypeEnum.Other) />
<EditorItem @bind-Field="@context.MaxConcurrentCount" Ignore=@(context.ChannelType==ChannelTypeEnum.Other) />
<EditorItem @bind-Field="@context.CacheTimeout" Ignore=@(context.ChannelType == ChannelTypeEnum.UdpSession || context.ChannelType == ChannelTypeEnum.Other) />
<EditorItem @bind-Field="@context.ConnectTimeout" Ignore=@(context.ChannelType == ChannelTypeEnum.UdpSession || context.ChannelType == ChannelTypeEnum.TcpService || context.ChannelType == ChannelTypeEnum.Other) />
<EditorItem @bind-Field="@context.MaxConcurrentCount" Ignore=@(context.ChannelType == ChannelTypeEnum.Other) />
<EditorItem @bind-Field="@context.MaxClientCount" Ignore=@(context.ChannelType!=ChannelTypeEnum.TcpService) />
<EditorItem @bind-Field="@context.CheckClearTime" Ignore=@(context.ChannelType!=ChannelTypeEnum.TcpService) />
<EditorItem @bind-Field="@context.Heartbeat" Ignore=@(context.ChannelType!=ChannelTypeEnum.TcpService&&context.ChannelType!=ChannelTypeEnum.TcpClient&&context.ChannelType!=ChannelTypeEnum.UdpSession) />
<EditorItem @bind-Field="@context.HeartbeatTime" Ignore=@(context.ChannelType!=ChannelTypeEnum.TcpClient&&context.ChannelType!=ChannelTypeEnum.UdpSession) />
<EditorItem @bind-Field="@context.DtuId" Ignore=@(context.ChannelType!=ChannelTypeEnum.TcpClient&&context.ChannelType!=ChannelTypeEnum.UdpSession) />
<EditorItem @bind-Field="@context.DtuSeviceType" Ignore=@(context.ChannelType!=ChannelTypeEnum.TcpService&&context.ChannelType!=ChannelTypeEnum.UdpSession) />
<EditorItem @bind-Field="@context.MaxClientCount" Ignore=@(context.ChannelType != ChannelTypeEnum.TcpService) />
<EditorItem @bind-Field="@context.CheckClearTime" Ignore=@(context.ChannelType != ChannelTypeEnum.TcpService) />
</FieldItems>
<Buttons>
<Button IsAsync class="mx-2" Color=Color.Primary OnClick="ConnectClick">@Localizer["Connect"]</Button>
<Button IsAsync class="mx-2" Color=Color.Warning OnClick="DisconnectClick">@Localizer["Disconnect"]</Button>
</Buttons>
</EditorFormObject>
</ValidateForm>
<EditorItem @bind-Field="@context.Heartbeat" Ignore=@(context.ChannelType != ChannelTypeEnum.TcpService && context.ChannelType != ChannelTypeEnum.TcpClient && context.ChannelType != ChannelTypeEnum.UdpSession)>
<EditTemplate Context="value">
<div class="col-12 col-sm-6 col-md-4">
<BootstrapInput @bind-Value=value.Heartbeat ShowLabel="true" class="me-2"></BootstrapInput>
<Checkbox @bind-Value=value.HeartbeatHex ShowAfterLabel DisplayText="hex"></Checkbox>
</div>
</EditTemplate>
</EditorItem>
</BodyTemplate>
</Card>
<EditorItem @bind-Field="@context.HeartbeatTime" Ignore=@(context.ChannelType != ChannelTypeEnum.TcpClient && context.ChannelType != ChannelTypeEnum.UdpSession) />
<EditorItem @bind-Field="@context.DtuId" Ignore=@(context.ChannelType != ChannelTypeEnum.TcpClient && context.ChannelType != ChannelTypeEnum.UdpSession)>
<EditTemplate Context="value">
<div class="col-12 col-sm-6 col-md-4">
<BootstrapInput @bind-Value=value.DtuId ShowLabel="true" class="me-2"></BootstrapInput>
<Checkbox @bind-Value=value.DtuIdHex ShowAfterLabel DisplayText="hex"></Checkbox>
</div>
</EditTemplate>
</EditorItem>
<EditorItem @bind-Field="@context.DtuIdHex" Ignore=@(context.ChannelType != ChannelTypeEnum.TcpService) />
<EditorItem @bind-Field="@context.DtuSeviceType" Ignore=@(context.ChannelType != ChannelTypeEnum.TcpService && context.ChannelType != ChannelTypeEnum.UdpSession) />
</FieldItems>
<Buttons>
<Button IsAsync class="mx-2" Color=Color.Primary OnClick="ConnectClick">@Localizer["Connect"]</Button>
<Button IsAsync class="mx-2" Color=Color.Warning OnClick="DisconnectClick">@Localizer["Disconnect"]</Button>
</Buttons>
</EditorFormObject>
</ValidateForm>
</BodyTemplate>
</Card>

View File

@@ -72,6 +72,8 @@
"DtuId": "DtuId",
"DtuSeviceType": "DtuSeviceType",
"Heartbeat": "Heartbeat",
"DtuIdHex": "DtuIdHex",
"HeartbeatHex": "HeartbeatHex",
"HeartbeatTime": "HeartbeatTime",
"MaxClientCount": "MaxClientCount",
"MaxConcurrentCount": "MaxConcurrentCount",

View File

@@ -69,9 +69,11 @@
"ConnectTimeout": "连接超时",
"DataBits": "数据位",
"DtrEnable": "Dtr",
"DtuId": "Dtu终端注册包(utf-8)",
"DtuId": "Dtu终端注册包",
"DtuSeviceType": "DTU服务类型",
"Heartbeat": "心跳内容(utf8)",
"Heartbeat": "心跳内容",
"DtuIdHex": "Dtu终端注册包是否Hex",
"HeartbeatHex": "心跳内容是否Hex",
"HeartbeatTime": "心跳间隔",
"MaxClientCount": "最大连接数",
"MaxConcurrentCount": "最大并发数",

View File

@@ -94,10 +94,12 @@ namespace ThingsGateway.Foundation
public virtual int MaxClientCount { get; set; }
public virtual int CheckClearTime { get; set; }
public virtual string Heartbeat { get; set; }
public virtual bool HeartbeatHex { get; set; }
#region dtu终端
public virtual int HeartbeatTime { get; set; }
public virtual string DtuId { get; set; }
public virtual bool DtuIdHex { get; set; }
#endregion
public virtual DtuSeviceType DtuSeviceType { get; set; }

View File

@@ -54,20 +54,29 @@ public static class ChannelOptionsExtensions
/// <returns></returns>
internal static async Task OnChannelEvent(this IClientChannel clientChannel, ChannelEventHandler funcs)
{
clientChannel.ThrowIfNull(nameof(IClientChannel));
funcs.ThrowIfNull(nameof(ChannelEventHandler));
if (funcs.Count > 0)
try
{
for (int i = 0; i < funcs.Count; i++)
clientChannel.ThrowIfNull(nameof(IClientChannel));
funcs.ThrowIfNull(nameof(ChannelEventHandler));
if (funcs.Count > 0)
{
var func = funcs[i];
var handled = await func.Invoke(clientChannel, i == funcs.Count - 1).ConfigureAwait(false);
if (handled)
for (int i = 0; i < funcs.Count; i++)
{
break;
var func = funcs[i];
var handled = await func.Invoke(clientChannel, i == funcs.Count - 1).ConfigureAwait(false);
if (handled)
{
break;
}
}
}
}
catch (Exception ex)
{
clientChannel.Logger?.LogWarning(ex, "fail ChannelEvent");
}
}

View File

@@ -111,10 +111,13 @@ public interface IChannelOptions
int CheckClearTime { get; set; }
/// <summary>
/// 心跳检测(utf8)
/// 心跳检测
/// </summary>
string Heartbeat { get; set; }
/// <summary>
/// 心跳检测是否hex
/// </summary>
bool HeartbeatHex { get; set; }
#region dtu终端
/// <summary>
/// 心跳时间
@@ -122,10 +125,13 @@ public interface IChannelOptions
public int HeartbeatTime { get; set; }
/// <summary>
/// 默认Dtu注册包(utf-8)
/// 默认Dtu注册包
/// </summary>
public string DtuId { get; set; }
/// <summary>
/// 默认Dtu注册包是否hex
/// </summary>
bool DtuIdHex { get; set; }
#endregion
public DtuSeviceType DtuSeviceType { get; set; }

View File

@@ -10,6 +10,8 @@
using System.Text;
using ThingsGateway.Foundation.Extension.String;
namespace ThingsGateway.Foundation;
/// <inheritdoc/>
@@ -28,13 +30,34 @@ public class DtuPlugin : PluginBase, ITcpReceivingPlugin
set
{
_heartbeat = value;
if (!_heartbeat.IsNullOrEmpty())
HeartbeatByte = new ArraySegment<byte>(Encoding.UTF8.GetBytes(value));
if (!heartbeatHex)
HeartbeatByte = new ArraySegment<byte>(Encoding.UTF8.GetBytes(_heartbeat ?? string.Empty));
else
HeartbeatByte = new ArraySegment<byte>(_heartbeat?.HexStringToBytes() ?? Array.Empty<byte>());
}
}
private string _heartbeat;
private ArraySegment<byte> HeartbeatByte = new();
private bool heartbeatHex;
public bool HeartbeatHex
{
get
{
return heartbeatHex;
}
set
{
heartbeatHex = value;
if (!heartbeatHex)
HeartbeatByte = new ArraySegment<byte>(Encoding.UTF8.GetBytes(_heartbeat ?? string.Empty));
else
HeartbeatByte = new ArraySegment<byte>(_heartbeat?.HexStringToBytes() ?? Array.Empty<byte>());
}
}
public bool DtuIdHex { get; set; }
/// <inheritdoc/>
public async Task OnTcpReceiving(ITcpSession client, ByteBlockEventArgs e)
{
@@ -43,7 +66,7 @@ public class DtuPlugin : PluginBase, ITcpReceivingPlugin
{
if (!socket.Id.StartsWith("ID="))
{
var id = $"ID={e.ByteBlock}";
var id = DtuIdHex ? $"ID={e.ByteBlock.Span.ToHexString()}" : $"ID={e.ByteBlock.ToString(0, e.ByteBlock.Length)}";
if (tcpServiceChannel.TryGetClient(id, out var oldClient))
{
try

View File

@@ -10,6 +10,8 @@
using System.Text;
using ThingsGateway.Foundation.Extension.String;
namespace ThingsGateway.Foundation;
[PluginOption(Singleton = true)]
@@ -24,7 +26,10 @@ internal sealed class HeartbeatAndReceivePlugin : PluginBase, ITcpConnectedPlugi
set
{
_dtuId = value;
DtuIdByte = new ArraySegment<byte>(Encoding.UTF8.GetBytes(value));
if (!dtuIdHex)
DtuIdByte = new ArraySegment<byte>(Encoding.UTF8.GetBytes(_dtuId ?? string.Empty));
else
DtuIdByte = new ArraySegment<byte>(_dtuId?.HexStringToBytes() ?? Array.Empty<byte>());
}
}
private string _dtuId;
@@ -42,15 +47,64 @@ internal sealed class HeartbeatAndReceivePlugin : PluginBase, ITcpConnectedPlugi
set
{
_heartbeat = value;
if (!_heartbeat.IsNullOrEmpty())
{
HeartbeatByte = new ArraySegment<byte>(Encoding.UTF8.GetBytes(value));
}
if (!heartbeatHex)
HeartbeatByte = new ArraySegment<byte>(Encoding.UTF8.GetBytes(_heartbeat ?? string.Empty));
else
HeartbeatByte = new ArraySegment<byte>(_heartbeat?.HexStringToBytes() ?? Array.Empty<byte>());
}
}
private string _heartbeat;
private ArraySegment<byte> HeartbeatByte;
private bool heartbeatHex;
public bool HeartbeatHex
{
get
{
return heartbeatHex;
}
set
{
heartbeatHex = value;
if (!heartbeatHex)
{
HeartbeatByte = new ArraySegment<byte>(Encoding.UTF8.GetBytes(_heartbeat ?? string.Empty));
}
else
{
HeartbeatByte = new ArraySegment<byte>(_heartbeat?.HexStringToBytes() ?? Array.Empty<byte>());
}
}
}
private bool dtuIdHex;
public bool DtuIdHex
{
get
{
return dtuIdHex;
}
set
{
dtuIdHex = value;
if (!dtuIdHex)
{
DtuIdByte = new ArraySegment<byte>(Encoding.UTF8.GetBytes(_dtuId ?? string.Empty));
}
else
{
DtuIdByte = new ArraySegment<byte>(_dtuId?.HexStringToBytes() ?? Array.Empty<byte>());
}
}
}
private Task Task;
private bool SendHeartbeat;
public int HeartbeatTime { get; set; } = 3000;

View File

@@ -25,6 +25,8 @@ public static class PluginUtil
action += a =>
{
var plugin = a.Add<HeartbeatAndReceivePlugin>();
plugin.HeartbeatHex = channelOptions.HeartbeatHex;
plugin.DtuIdHex = channelOptions.DtuIdHex;
plugin.Heartbeat = channelOptions.Heartbeat;
plugin.DtuId = channelOptions.DtuId;
plugin.HeartbeatTime = channelOptions.HeartbeatTime;
@@ -52,7 +54,9 @@ public static class PluginUtil
action += a =>
{
var plugin = a.Add<DtuPlugin>();
plugin.HeartbeatHex = channelOptions.HeartbeatHex;
plugin.Heartbeat = channelOptions.Heartbeat;
plugin.DtuIdHex = channelOptions.DtuIdHex;
};
}
return action;

View File

@@ -238,7 +238,7 @@ public abstract class TcpCustomDataHandlingAdapter<TRequest> : SingleStreamDataH
// 如果缓存的数据长度超过设定的最大包大小,则抛出异常。
if (this.m_tempByteBlock.Length > this.MaxPackageSize)
{
throw new Exception("缓存的数据长度大于设定值的情况下未收到解析信号");
throw new Exception($"The parsed signal was not received when the cached data length {m_tempByteBlock.Length} exceeds the set value {MaxPackageSize}");
}
// 将字节块指针移到末尾。
@@ -306,7 +306,7 @@ public abstract class TcpCustomDataHandlingAdapter<TRequest> : SingleStreamDataH
if (this.m_tempByteBlock.Length > this.MaxPackageSize)
{
this.OnError(default, "缓存的数据长度大于设定值的情况下未收到解析信号", true, true);
this.OnError(default, $"The parsed signal was not received when the cached data length {m_tempByteBlock.Length} exceeds the set value {MaxPackageSize}", true, true);
}
}
if (this.UpdateCacheTimeWhenRev)

View File

@@ -142,7 +142,20 @@ public static partial class DeviceExtension
return new();
case WaitDataStatus.Canceled: return new(new OperationCanceledException());
case WaitDataStatus.Overtime: return waitDataAsync.WaitResult == null ? new(new TimeoutException()) : new(waitDataAsync.WaitResult);
case WaitDataStatus.Overtime:
{
if (waitDataAsync.WaitResult != null)
{
waitDataAsync.WaitResult.Exception = new TimeoutException();
if (waitDataAsync.WaitResult.IsSuccess) waitDataAsync.WaitResult.OperCode = 999;
if (waitDataAsync.WaitResult.ErrorMessage.IsNullOrEmpty()) waitDataAsync.WaitResult.ErrorMessage = "Timeout";
return new(waitDataAsync.WaitResult);
}
else
{
return new(new TimeoutException());
}
}
case WaitDataStatus.Disposed:
case WaitDataStatus.Default:
default:

View File

@@ -12,6 +12,8 @@
"DtuSeviceType": "DtuSeviceType",
"Heartbeat": "Heartbeat",
"HeartbeatTime": "HeartbeatTime",
"DtuIdHex": "DtuIdHex",
"HeartbeatHex": "HeartbeatHex",
"MaxClientCount": "MaxClientCount",
"MaxConcurrentCount": "MaxConcurrentCount",
"Name": "Name",
@@ -73,7 +75,7 @@
"Timeout": "Timeout"
},
"ThingsGateway.Foundation.DtuServiceDeviceBase": {
"DtuId": "DtuId(UTF8)"
"DtuId": "DtuId"
},
"ThingsGateway.Foundation.VariableClass": {
"RegisterAddress": "RegisterAddress"

View File

@@ -8,9 +8,11 @@
"ConnectTimeout": "连接超时",
"DataBits": "数据位",
"DtrEnable": "Dtr",
"DtuId": "Dtu终端注册包(utf-8)",
"DtuId": "Dtu终端注册包",
"DtuSeviceType": "DTU服务类型",
"Heartbeat": "心跳内容(utf8)",
"Heartbeat": "心跳内容",
"DtuIdHex": "Dtu终端注册包是否Hex",
"HeartbeatHex": "心跳内容是否Hex",
"HeartbeatTime": "心跳间隔",
"MaxClientCount": "最大连接数",
"MaxConcurrentCount": "最大并发数",
@@ -73,7 +75,7 @@
"Timeout": "读写超时"
},
"ThingsGateway.Foundation.DtuServiceDeviceBase": {
"DtuId": "Dtu注册包(UTF8)"
"DtuId": "Dtu注册包"
},
"ThingsGateway.Foundation.VariableClass": {
"RegisterAddress": "寄存器地址"

View File

@@ -177,6 +177,10 @@ public class Channel : ChannelOptionsBase, IPrimaryIdEntity, IBaseDataEntity, IB
[AutoGenerateColumn(Visible = true, Filterable = true, Sortable = true)]
public override string Heartbeat { get; set; }
[SugarColumn(ColumnDescription = "心跳内容是否Hex", IsNullable = true)]
[AutoGenerateColumn(Visible = true, Filterable = true, Sortable = true)]
public override bool HeartbeatHex { get; set; }
#region dtu终端
[SugarColumn(ColumnDescription = "心跳间隔", IsNullable = true)]
@@ -187,6 +191,9 @@ public class Channel : ChannelOptionsBase, IPrimaryIdEntity, IBaseDataEntity, IB
[AutoGenerateColumn(Visible = true, Filterable = true, Sortable = true)]
public override string DtuId { get; set; }
[SugarColumn(ColumnDescription = "DtuId是否Hex", IsNullable = true)]
[AutoGenerateColumn(Visible = true, Filterable = true, Sortable = true)]
public override bool DtuIdHex { get; set; }
#endregion
[SugarColumn(ColumnDescription = "Dtu类型", IsNullable = true)]

View File

@@ -18,7 +18,7 @@ public enum EventTypeEnum
/// <summary>
/// 准备报警
/// </summary>
Prepare,
PrepareAlarm,
/// <summary>
/// 报警产生
@@ -34,4 +34,9 @@ public enum EventTypeEnum
/// 报警恢复
/// </summary>
Finish,
/// <summary>
/// 准备恢复
/// </summary>
PrepareFinish,
}

View File

@@ -242,6 +242,8 @@
"ExportChannel": "Export Channel",
"Heartbeat": "Heartbeat",
"HeartbeatTime": "HeartbeatTime",
"DtuIdHex": "DtuIdHex",
"HeartbeatHex": "HeartbeatHex",
"ImportChannel": "Import Channel",
"ImportNullError": "Unable to recognize",
"LogEnable": "LogEnable",
@@ -274,10 +276,10 @@
},
"ThingsGateway.Gateway.Application.CollectFoundationDtuPackPropertyBase": {
"DtuId": "DtuId(UTF8)"
"DtuId": "DtuId"
},
"ThingsGateway.Gateway.Application.CollectFoundationDtuPropertyBase": {
"DtuId": "DtuId(UTF8)"
"DtuId": "DtuId"
},
"ThingsGateway.Gateway.Application.CollectFoundationPackPropertyBase": {
"MaxPack": "MaxPack"

View File

@@ -235,11 +235,13 @@
"DeleteChannel": "删除通道",
"Disconnect": "断开",
"DtrEnable": "Dtr",
"DtuId": "Dtu终端注册包(utf-8)",
"DtuId": "Dtu终端注册包",
"DtuSeviceType": "DTU服务类型",
"Enable": "启用",
"ExportChannel": "导出通道",
"Heartbeat": "心跳内容(utf8)",
"Heartbeat": "心跳内容",
"DtuIdHex": "Dtu终端注册包是否Hex",
"HeartbeatHex": "心跳内容是否Hex",
"HeartbeatTime": "心跳间隔",
"ImportChannel": "导入通道",
"ImportNullError": "无法识别",
@@ -273,10 +275,10 @@
},
"ThingsGateway.Gateway.Application.CollectFoundationDtuPackPropertyBase": {
"DtuId": "Dtu注册包(UTF8)"
"DtuId": "Dtu注册包"
},
"ThingsGateway.Gateway.Application.CollectFoundationDtuPropertyBase": {
"DtuId": "Dtu注册包(UTF8)"
"DtuId": "Dtu注册包"
},
"ThingsGateway.Gateway.Application.CollectFoundationPackPropertyBase": {
"MaxPack": "最大打包长度"

View File

@@ -21,7 +21,8 @@ namespace ThingsGateway.Gateway.Application;
/// </summary>
public partial class VariableRuntime : Variable, IVariable, IDisposable
{
private DateTime? prepareEventTime;
private DateTime? prepareAlarmEventTime;
private DateTime? prepareFinishEventTime;
private EventTypeEnum? eventType;
private AlarmTypeEnum? alarmType { get; set; }

View File

@@ -55,7 +55,12 @@ public partial class VariableRuntime : Variable, IVariable, IDisposable
/// 事件时间
/// </summary>
[AutoGenerateColumn(Ignore = true)]
internal DateTime? PrepareEventTime { get => prepareEventTime; set => prepareEventTime = value; }
internal DateTime? PrepareAlarmEventTime { get => prepareAlarmEventTime; set => prepareAlarmEventTime = value; }
/// <summary>
/// 事件时间
/// </summary>
[AutoGenerateColumn(Ignore = true)]
internal DateTime? PrepareFinishEventTime { get => prepareFinishEventTime; set => prepareFinishEventTime = value; }
/// <summary>
/// 变化时间

View File

@@ -284,16 +284,16 @@ internal sealed class AlarmTask : IDisposable
//添加报警延时策略
if (delay > 0)
{
if (item.EventType != EventTypeEnum.Alarm && item.EventType != EventTypeEnum.Prepare)
if (item.EventType != EventTypeEnum.Alarm && item.EventType != EventTypeEnum.PrepareAlarm)
{
item.EventType = EventTypeEnum.Prepare;//准备报警
item.PrepareEventTime = now;
item.EventType = EventTypeEnum.PrepareAlarm;//准备报警
item.PrepareAlarmEventTime = now;
}
else
{
if (item.EventType == EventTypeEnum.Prepare)
if (item.EventType == EventTypeEnum.PrepareAlarm)
{
if ((now - item.PrepareEventTime!.Value).TotalSeconds > delay)
if ((now - item.PrepareAlarmEventTime!.Value).TotalMilliseconds > delay)
{
//超过延时时间,触发报警
item.EventType = EventTypeEnum.Alarm;
@@ -304,7 +304,7 @@ internal sealed class AlarmTask : IDisposable
item.AlarmCode = item.Value.ToString();
item.RecoveryCode = string.Empty;
item.AlarmText = text;
item.PrepareEventTime = null;
item.PrepareAlarmEventTime = null;
changed = true;
}
@@ -312,9 +312,9 @@ internal sealed class AlarmTask : IDisposable
else if (item.EventType == EventTypeEnum.Alarm && item.AlarmType != alarmEnum)
{
//报警类型改变,重新计时
if (item.PrepareEventTime == null)
item.PrepareEventTime = now;
if ((now - item.PrepareEventTime!.Value).TotalSeconds > delay)
if (item.PrepareAlarmEventTime == null)
item.PrepareAlarmEventTime = now;
if ((now - item.PrepareAlarmEventTime!.Value).TotalMilliseconds > delay)
{
//超过延时时间,触发报警
item.EventType = EventTypeEnum.Alarm;
@@ -325,7 +325,7 @@ internal sealed class AlarmTask : IDisposable
item.AlarmCode = item.Value.ToString();
item.RecoveryCode = string.Empty;
item.AlarmText = text;
item.PrepareEventTime = null;
item.PrepareAlarmEventTime = null;
changed = true;
}
}
@@ -346,24 +346,64 @@ internal sealed class AlarmTask : IDisposable
item.AlarmCode = item.Value.ToString();
item.RecoveryCode = string.Empty;
item.AlarmText = text;
item.PrepareAlarmEventTime = null;
changed = true;
}
}
else if (eventEnum == EventTypeEnum.Finish)
{
// 如果是需恢复报警事件
// 获取旧的报警信息
if (GlobalData.RealAlarmIdVariables.TryGetValue(item.Id, out var oldAlarm))
var now = DateTime.Now;
//添加报警延时策略
if (delay > 0)
{
item.AlarmType = oldAlarm.AlarmType;
item.EventType = eventEnum;
item.AlarmLimit = oldAlarm.AlarmLimit;
item.AlarmCode = oldAlarm.AlarmCode;
item.RecoveryCode = item.Value.ToString();
item.AlarmText = oldAlarm.AlarmText;
item.EventTime = DateTime.Now;
if (item.EventType != EventTypeEnum.Finish && item.EventType != EventTypeEnum.PrepareFinish)
{
item.EventType = EventTypeEnum.PrepareFinish;//准备报警
item.PrepareFinishEventTime = now;
}
else
{
if (item.EventType == EventTypeEnum.PrepareFinish)
{
if ((now - item.PrepareFinishEventTime!.Value).TotalMilliseconds > delay)
{
if (GlobalData.RealAlarmIdVariables.TryGetValue(item.Id, out var oldAlarm))
{
item.AlarmType = oldAlarm.AlarmType;
item.EventType = eventEnum;
item.AlarmLimit = oldAlarm.AlarmLimit;
item.AlarmCode = oldAlarm.AlarmCode;
item.RecoveryCode = item.Value.ToString();
item.AlarmText = oldAlarm.AlarmText;
item.EventTime = DateTime.Now;
item.PrepareFinishEventTime = null;
changed = true;
}
}
}
else
{
return;
}
}
}
else
{
// 如果是需恢复报警事件
// 获取旧的报警信息
if (GlobalData.RealAlarmIdVariables.TryGetValue(item.Id, out var oldAlarm))
{
item.AlarmType = oldAlarm.AlarmType;
item.EventType = eventEnum;
item.AlarmLimit = oldAlarm.AlarmLimit;
item.AlarmCode = oldAlarm.AlarmCode;
item.RecoveryCode = item.Value.ToString();
item.AlarmText = oldAlarm.AlarmText;
item.EventTime = DateTime.Now;
item.PrepareFinishEventTime = null;
changed = true;
}
}
changed = true;
}
// 触发报警变化事件

View File

@@ -333,6 +333,8 @@ internal sealed class DeviceThreadManage : IAsyncDisposable, IDeviceThreadManage
}
}
driver.IdVariableRuntimes.ForEach(a => a.Value.SetErrorMessage(null));
CancellationTokenSources.TryAdd(driver.DeviceId, cts);
_ = Task.Factory.StartNew((state) => DriverStart(state, token), driver, token);
@@ -395,17 +397,21 @@ internal sealed class DeviceThreadManage : IAsyncDisposable, IDeviceThreadManage
// 查找具有指定设备ID的驱动程序对象
if (Drivers.TryRemove(deviceId, out var driver))
{
driver.CurrentDevice.SetDeviceStatus(now, false, "Communication connection has been removed");
if (IsCollectChannel == true)
{
foreach (var a in driver.IdVariableRuntimes)
{
a.Value.SetValue(a.Value.Value, now, false);
a.Value.SetErrorMessage("Communication connection has been removed");
if (a.Value.SaveValue && !a.Value.DynamicVariable)
{
saveVariableRuntimes.Add(a.Value);
}
}
}
}
// 取消驱动程序的操作

View File

@@ -33,102 +33,125 @@
RenderFragment renderFragment =>
@<EditorForm class="p-2" AutoGenerateAllItem="false" RowType=RowType.Inline ItemsPerRow=2 LabelWidth=200 Model="Model">
<FieldItems>
<EditorItem TValue="string" TModel="Channel" @bind-Field="@context.Name">
<EditTemplate Context="value">
<div class="col-12">
<h6>@GatewayLocalizer["BasicInformation"]</h6>
</div>
</EditTemplate>
</EditorItem>
<FieldItems>
<EditorItem TValue="string" TModel="Channel" @bind-Field="@context.Name">
<EditTemplate Context="value">
<div class="col-12">
<h6>@GatewayLocalizer["BasicInformation"]</h6>
</div>
</EditTemplate>
</EditorItem>
<EditorItem @bind-Field="@context.Name" Readonly=BatchEditEnable />
<EditorItem @bind-Field="@context.Name" Readonly=BatchEditEnable />
<EditorItem @bind-Field="@context.PluginName">
<EditTemplate Context="value">
<div class="col-12 col-md-6">
<Select @bind-Value="@value.PluginName"
Items="@PluginNames" IsDisabled=BatchEditEnable
ShowSearch="true">
<ItemTemplate Context="name">
@if (PluginDcit.TryGetValue(name.Value, out var pluginOutput))
{
if (pluginOutput.EducationPlugin)
{
<div class="d-flex">
<span>@name.Text</span>
<div style="flex-grow: 1;"></div>
<Tag Color="Color.Primary">PRO</Tag>
</div>
}
else
<EditorItem @bind-Field="@context.PluginName">
<EditTemplate Context="value">
<div class="col-12 col-md-6">
<Select @bind-Value="@value.PluginName"
Items="@PluginNames" IsDisabled=BatchEditEnable
ShowSearch="true">
<ItemTemplate Context="name">
@if (PluginDcit.TryGetValue(name.Value, out var pluginOutput))
{
if (pluginOutput.EducationPlugin)
{
<div class="d-flex">
<span>@name.Text</span>
<div style="flex-grow: 1;"></div>
<Tag Color="Color.Primary">PRO</Tag>
</div>
}
else
{
<span>@name.Text</span>
}
}
else
}
else
{
<span>@name.Value</span>
}
</ItemTemplate>
</Select>
</div>
</EditTemplate>
</EditorItem>
</ItemTemplate>
</Select>
</div>
</EditTemplate>
</EditorItem>
<EditorItem @bind-Field="@context.Enable" />
<EditorItem @bind-Field="@context.LogLevel" />
<EditorItem @bind-Field="@context.Enable" />
<EditorItem @bind-Field="@context.LogLevel" />
<EditorItem TValue="string" TModel="Channel" @bind-Field="@context.Name">
<EditTemplate Context="value">
<div class="col-12">
<h6>@GatewayLocalizer["Connection"]</h6>
</div>
</EditTemplate>
</EditorItem>
<EditorItem TValue="string" TModel="Channel" @bind-Field="@context.Name">
<EditTemplate Context="value">
<div class="col-12">
<h6>@GatewayLocalizer["Connection"]</h6>
</div>
</EditTemplate>
</EditorItem>
<EditorItem @bind-Field="@context.ChannelType">
<EditorItem @bind-Field="@context.ChannelType">
<EditTemplate Context="value">
<div class="col-12 col-sm-6 col-md-6">
<Select SkipValidate="true" @bind-Value="@value.ChannelType" OnSelectedItemChanged=@((a)=>
{
return InvokeAsync(StateHasChanged);
}) />
</div>
</EditTemplate>
<EditTemplate Context="value">
<div class="col-12 col-sm-6 col-md-6">
<Select SkipValidate="true" @bind-Value="@value.ChannelType" OnSelectedItemChanged=@((a)=>
{
return InvokeAsync(StateHasChanged);
}) />
</div>
</EditTemplate>
</EditorItem>
</EditorItem>
<EditorItem @bind-Field="@context.RemoteUrl" Ignore=@(context.ChannelType!=ChannelTypeEnum.TcpClient&&context.ChannelType!=ChannelTypeEnum.UdpSession) />
<EditorItem @bind-Field="@context.BindUrl" Ignore=@(context.ChannelType!=ChannelTypeEnum.TcpClient&&context.ChannelType!=ChannelTypeEnum.UdpSession&&context.ChannelType!=ChannelTypeEnum.TcpService) />
<EditorItem @bind-Field="@context.RemoteUrl" Ignore=@(context.ChannelType != ChannelTypeEnum.TcpClient && context.ChannelType != ChannelTypeEnum.UdpSession) />
<EditorItem @bind-Field="@context.BindUrl" Ignore=@(context.ChannelType != ChannelTypeEnum.TcpClient && context.ChannelType != ChannelTypeEnum.UdpSession && context.ChannelType != ChannelTypeEnum.TcpService) />
<EditorItem @bind-Field="@context.PortName" Ignore=@(context.ChannelType!=ChannelTypeEnum.SerialPort) />
<EditorItem @bind-Field="@context.BaudRate" Ignore=@(context.ChannelType!=ChannelTypeEnum.SerialPort) />
<EditorItem @bind-Field="@context.DataBits" Ignore=@(context.ChannelType!=ChannelTypeEnum.SerialPort) />
<EditorItem @bind-Field="@context.Parity" Ignore=@(context.ChannelType!=ChannelTypeEnum.SerialPort) />
<EditorItem @bind-Field="@context.StopBits" Ignore=@(context.ChannelType!=ChannelTypeEnum.SerialPort) />
<EditorItem @bind-Field="@context.DtrEnable" Ignore=@(context.ChannelType!=ChannelTypeEnum.SerialPort) />
<EditorItem @bind-Field="@context.RtsEnable" Ignore=@(context.ChannelType!=ChannelTypeEnum.SerialPort) />
<EditorItem @bind-Field="@context.StreamAsync" Ignore=@(context.ChannelType!=ChannelTypeEnum.SerialPort) />
<EditorItem @bind-Field="@context.PortName" Ignore=@(context.ChannelType != ChannelTypeEnum.SerialPort) />
<EditorItem @bind-Field="@context.BaudRate" Ignore=@(context.ChannelType != ChannelTypeEnum.SerialPort) />
<EditorItem @bind-Field="@context.DataBits" Ignore=@(context.ChannelType != ChannelTypeEnum.SerialPort) />
<EditorItem @bind-Field="@context.Parity" Ignore=@(context.ChannelType != ChannelTypeEnum.SerialPort) />
<EditorItem @bind-Field="@context.StopBits" Ignore=@(context.ChannelType != ChannelTypeEnum.SerialPort) />
<EditorItem @bind-Field="@context.DtrEnable" Ignore=@(context.ChannelType != ChannelTypeEnum.SerialPort) />
<EditorItem @bind-Field="@context.RtsEnable" Ignore=@(context.ChannelType != ChannelTypeEnum.SerialPort) />
<EditorItem @bind-Field="@context.StreamAsync" Ignore=@(context.ChannelType != ChannelTypeEnum.SerialPort) />
<EditorItem @bind-Field="@context.CacheTimeout" Ignore=@(context.ChannelType==ChannelTypeEnum.UdpSession||context.ChannelType==ChannelTypeEnum.Other) />
<EditorItem @bind-Field="@context.ConnectTimeout" Ignore=@(context.ChannelType==ChannelTypeEnum.UdpSession||context.ChannelType==ChannelTypeEnum.TcpService||context.ChannelType==ChannelTypeEnum.Other) />
<EditorItem @bind-Field="@context.MaxConcurrentCount" Ignore=@(context.ChannelType==ChannelTypeEnum.Other) />
<EditorItem @bind-Field="@context.CacheTimeout" Ignore=@(context.ChannelType == ChannelTypeEnum.UdpSession || context.ChannelType == ChannelTypeEnum.Other) />
<EditorItem @bind-Field="@context.ConnectTimeout" Ignore=@(context.ChannelType == ChannelTypeEnum.UdpSession || context.ChannelType == ChannelTypeEnum.TcpService || context.ChannelType == ChannelTypeEnum.Other) />
<EditorItem @bind-Field="@context.MaxConcurrentCount" Ignore=@(context.ChannelType == ChannelTypeEnum.Other) />
<EditorItem @bind-Field="@context.MaxClientCount" Ignore=@(context.ChannelType!=ChannelTypeEnum.TcpService) />
<EditorItem @bind-Field="@context.CheckClearTime" Ignore=@(context.ChannelType!=ChannelTypeEnum.TcpService) />
<EditorItem @bind-Field="@context.Heartbeat" Ignore=@(context.ChannelType!=ChannelTypeEnum.TcpService&&context.ChannelType!=ChannelTypeEnum.TcpClient&&context.ChannelType!=ChannelTypeEnum.UdpSession) />
<EditorItem @bind-Field="@context.HeartbeatTime" Ignore=@(context.ChannelType!=ChannelTypeEnum.TcpClient&&context.ChannelType!=ChannelTypeEnum.UdpSession) />
<EditorItem @bind-Field="@context.DtuId" Ignore=@(context.ChannelType!=ChannelTypeEnum.TcpClient&&context.ChannelType!=ChannelTypeEnum.UdpSession) />
<EditorItem @bind-Field="@context.DtuSeviceType" Ignore=@(context.ChannelType!=ChannelTypeEnum.TcpService&&context.ChannelType!=ChannelTypeEnum.UdpSession) />
<EditorItem @bind-Field="@context.MaxClientCount" Ignore=@(context.ChannelType != ChannelTypeEnum.TcpService) />
<EditorItem @bind-Field="@context.CheckClearTime" Ignore=@(context.ChannelType != ChannelTypeEnum.TcpService) />
</FieldItems>
<EditorItem @bind-Field="@context.Heartbeat" Ignore=@(context.ChannelType != ChannelTypeEnum.TcpService && context.ChannelType != ChannelTypeEnum.TcpClient && context.ChannelType != ChannelTypeEnum.UdpSession)>
<EditTemplate Context="value">
<div class="col-12 col-md-6">
<BootstrapInput @bind-Value=value.Heartbeat ShowLabel="true" class="me-2"></BootstrapInput>
<Checkbox @bind-Value=value.HeartbeatHex ShowAfterLabel DisplayText="hex"></Checkbox>
</div>
</EditTemplate>
</EditorItem>
</EditorForm>;
<EditorItem @bind-Field="@context.HeartbeatTime" Ignore=@(context.ChannelType != ChannelTypeEnum.TcpClient && context.ChannelType != ChannelTypeEnum.UdpSession) />
<EditorItem @bind-Field="@context.DtuId" Ignore=@(context.ChannelType != ChannelTypeEnum.TcpClient && context.ChannelType != ChannelTypeEnum.UdpSession)>
<EditTemplate Context="value">
<div class="col-12 col-md-6">
<BootstrapInput @bind-Value=value.DtuId ShowLabel="true" class="me-2"></BootstrapInput>
<Checkbox @bind-Value=value.DtuIdHex ShowAfterLabel DisplayText="hex"></Checkbox>
</div>
</EditTemplate>
</EditorItem>
<EditorItem @bind-Field="@context.DtuIdHex" Ignore=@(context.ChannelType != ChannelTypeEnum.TcpService) />
<EditorItem @bind-Field="@context.DtuSeviceType" Ignore=@(context.ChannelType != ChannelTypeEnum.TcpService && context.ChannelType != ChannelTypeEnum.UdpSession) />
</FieldItems>
</EditorForm>;
}

View File

@@ -53,6 +53,8 @@
<TableColumn Field="@context.BindUrl" FieldExpression=@(() => context.BindUrl) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.MaxClientCount" FieldExpression=@(() => context.MaxClientCount) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.MaxConcurrentCount" FieldExpression=@(() => context.MaxConcurrentCount) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.DtuIdHex" FieldExpression=@(() => context.DtuIdHex) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.HeartbeatHex" FieldExpression=@(() => context.HeartbeatHex) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn @bind-Field="@context.Id" Filterable=true Sortable=true Visible="false" DefaultSort=true DefaultSortOrder="SortOrder.Asc" />

View File

@@ -138,9 +138,8 @@ public partial class VariableEditComponent
AddressDynamicComponent = new BootstrapDynamicComponent(AddressUIType, new Dictionary<string, object?>
{
[nameof(IAddressUIBase.Model)] = Model.RegisterAddress,
[nameof(IAddressUIBase.ModelChanged)] =
(string address) => Model.RegisterAddress = address
(Action<string>)(address => Model.RegisterAddress = address)
});
}
else

View File

@@ -59,7 +59,7 @@ public partial class ModbusMaster : ComponentBase, IDisposable
op.Component = BootstrapDynamicComponent.CreateComponent<ModbusAddressComponent>(new Dictionary<string, object?>
{
{nameof(ModbusAddressComponent.ModelChanged), (string a) => DeviceComponent?.SetRegisterAddress(a)},
{nameof(ModbusAddressComponent.ModelChanged), (Action<string>)(a => DeviceComponent?.SetRegisterAddress(a))},
{nameof(ModbusAddressComponent.Model),address },
});

View File

@@ -52,7 +52,7 @@ public partial class ModbusSlave : ComponentBase, IDisposable
op.Component = BootstrapDynamicComponent.CreateComponent<ModbusAddressComponent>(new Dictionary<string, object?>
{
{nameof(ModbusAddressComponent.ModelChanged), (string a) => DeviceComponent?.SetRegisterAddress(a)},
{nameof(ModbusAddressComponent.ModelChanged), (Action<string>)(a => DeviceComponent?.SetRegisterAddress(a))},
{nameof(ModbusAddressComponent.Model),address },
});