diff --git a/src/Admin/ThingsGateway.NewLife.X/Logger/TextFileLog.cs b/src/Admin/ThingsGateway.NewLife.X/Logger/TextFileLog.cs index 30c8c4bfb..3150d637e 100644 --- a/src/Admin/ThingsGateway.NewLife.X/Logger/TextFileLog.cs +++ b/src/Admin/ThingsGateway.NewLife.X/Logger/TextFileLog.cs @@ -234,7 +234,7 @@ public class TextFileLog : Logger, IDisposable if (!_isFile && Backups > 0) { // 判断日志目录是否已存在 - var di = LogPath.GetBasePath().AsDirectory(); + DirectoryInfo? di = new DirectoryInfo(LogPath); if (di.Exists) { // 删除*.del diff --git a/src/Admin/ThingsGateway.NewLife.X/Threading/TimerScheduler.cs b/src/Admin/ThingsGateway.NewLife.X/Threading/TimerScheduler.cs index 90685b9c7..9f3c9b994 100644 --- a/src/Admin/ThingsGateway.NewLife.X/Threading/TimerScheduler.cs +++ b/src/Admin/ThingsGateway.NewLife.X/Threading/TimerScheduler.cs @@ -318,7 +318,9 @@ public class TimerScheduler : IDisposable, ILogFeature timer.hasSetNext = false; - using var span = timer.Tracer?.NewSpan(timer.TracerName ?? $"timer:Execute", timer.Timers + ""); + string tracerName = timer.TracerName ?? "timer:ExecuteAsync"; + string timerArg = timer.Timers.ToString(); + using var span = timer.Tracer?.NewSpan(tracerName, timerArg); var sw = _stopwatchPool.Get(); sw.Restart(); try @@ -331,9 +333,12 @@ public class TimerScheduler : IDisposable, ILogFeature timer.Dispose(); return; } - - var func = timer.Method.As(target); - func!(timer.State); + if (timer.TimerCallbackCachedDelegate == null) + { + timer.TimerCallbackCachedDelegate = timer.Method.As(target); + } + //var func = timer.Method.As(target); + timer.TimerCallbackCachedDelegate!(timer.State); } catch (ThreadAbortException) { throw; } catch (ThreadInterruptedException) { throw; } @@ -367,7 +372,9 @@ public class TimerScheduler : IDisposable, ILogFeature timer.hasSetNext = false; - using var span = timer.Tracer?.NewSpan(timer.TracerName ?? $"timer:ExecuteAsync", timer.Timers + ""); + string tracerName = timer.TracerName ?? "timer:ExecuteAsync"; + string timerArg = timer.Timers.ToString(); + using var span = timer.Tracer?.NewSpan(tracerName, timerArg); var sw = _stopwatchPool.Get(); sw.Restart(); try @@ -381,19 +388,31 @@ public class TimerScheduler : IDisposable, ILogFeature return; } + + #if NET6_0_OR_GREATER if (timer.IsValueTask) { - var func = timer.Method.As>(target); - var task = func!(timer.State); + if (timer.ValueTaskCachedDelegate == null) + { + timer.ValueTaskCachedDelegate = timer.Method.As>(target); + } + + //var func = timer.Method.As>(target); + var task = timer.ValueTaskCachedDelegate!(timer.State); if (!task.IsCompleted) await task.ConfigureAwait(false); } else #endif { - var func = timer.Method.As>(target); - var task = func!(timer.State); + if (timer.TaskCachedDelegate == null) + { + timer.TaskCachedDelegate = timer.Method.As>(target); + } + + //var func = timer.Method.As>(target); + var task = timer.TaskCachedDelegate!(timer.State); if (!task.IsCompleted) await task.ConfigureAwait(false); } diff --git a/src/Admin/ThingsGateway.NewLife.X/Threading/TimerX.cs b/src/Admin/ThingsGateway.NewLife.X/Threading/TimerX.cs index 42e8e3f6a..b19faab5d 100644 --- a/src/Admin/ThingsGateway.NewLife.X/Threading/TimerX.cs +++ b/src/Admin/ThingsGateway.NewLife.X/Threading/TimerX.cs @@ -542,6 +542,12 @@ public class TimerX : ITimer, ITimerx, IDisposable } } + public Func? TaskCachedDelegate { get; internal set; } +#if NET6_0_OR_GREATER + public Func? ValueTaskCachedDelegate { get; internal set; } +#endif + public TimerCallback? TimerCallbackCachedDelegate { get; internal set; } + private static void CopyNow(Object? state) => _Now = TimerScheduler.Default.GetNow(); #endregion diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 6e24352d8..21f8cb128 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,9 +1,9 @@ - 10.11.109 - 10.11.109 - 10.11.109 + 10.11.110 + 10.11.110 + 10.11.110 10.11.6 10.11.6 8.0.21 diff --git a/src/Foundation/ThingsGateway.Foundation/Channel/Extension/ChannelOptionsExtensions.cs b/src/Foundation/ThingsGateway.Foundation/Channel/Extension/ChannelOptionsExtensions.cs index a734f3690..06018fd28 100644 --- a/src/Foundation/ThingsGateway.Foundation/Channel/Extension/ChannelOptionsExtensions.cs +++ b/src/Foundation/ThingsGateway.Foundation/Channel/Extension/ChannelOptionsExtensions.cs @@ -26,24 +26,65 @@ public static class ChannelOptionsExtensions /// 接收数据 /// 事件 /// - internal static async Task OnChannelReceivedEvent(this IClientChannel clientChannel, ReceivedDataEventArgs e, ChannelReceivedEventHandler funcs) + internal static ValueTask OnChannelReceivedEvent( + this IClientChannel clientChannel, + ReceivedDataEventArgs e, + ChannelReceivedEventHandler funcs) { clientChannel.ThrowIfNull(nameof(IClientChannel)); e.ThrowIfNull(nameof(ReceivedDataEventArgs)); funcs.ThrowIfNull(nameof(ChannelReceivedEventHandler)); - if (funcs.Count > 0) + if (funcs.Count == 0) return EasyValueTask.CompletedTask; + + return InvokeHandlersSequentially(clientChannel, e, funcs); + } + + private static ValueTask InvokeHandlersSequentially( + IClientChannel clientChannel, ReceivedDataEventArgs e, ChannelReceivedEventHandler funcs) + { + var enumerator = new HandlerEnumerator(clientChannel, e, funcs); + return enumerator.MoveNextAsync(); + } + + private struct HandlerEnumerator + { + private readonly IClientChannel _channel; + private readonly ReceivedDataEventArgs _e; + private readonly ChannelReceivedEventHandler _funcs; + private int _index; + + public HandlerEnumerator(IClientChannel channel, ReceivedDataEventArgs e, ChannelReceivedEventHandler funcs) { - for (int i = 0; i < funcs.Count; i++) + _channel = channel; + _e = e; + _funcs = funcs; + _index = -1; + } + + public ValueTask MoveNextAsync() + { + _index++; + if (_index >= _funcs.Count) return default; + + var func = _funcs[_index]; + if (func == null) return MoveNextAsync(); + + bool isLast = _index == _funcs.Count - 1; + var vt = func.Invoke(_channel, _e, isLast); + if (vt.IsCompletedSuccessfully) { - var func = funcs[i]; - if (func == null) continue; - await func.Invoke(clientChannel, e, i == funcs.Count - 1).ConfigureAwait(false); - if (e.Handled) - { - break; - } + if (_e.Handled) return default; + return MoveNextAsync(); } + return Awaited(vt); + } + + private async ValueTask Awaited(ValueTask vt) + { + await vt.ConfigureAwait(false); + if (!_e.Handled) + await MoveNextAsync().ConfigureAwait(false); } } @@ -53,7 +94,7 @@ public static class ChannelOptionsExtensions /// 通道 /// 事件 /// - internal static async Task OnChannelEvent(this IClientChannel clientChannel, ChannelEventHandler funcs) + internal static async ValueTask OnChannelEvent(this IClientChannel clientChannel, ChannelEventHandler funcs) { try { diff --git a/src/Foundation/ThingsGateway.Foundation/Channel/IChannel.cs b/src/Foundation/ThingsGateway.Foundation/Channel/IChannel.cs index 54c135886..2f0dedb83 100644 --- a/src/Foundation/ThingsGateway.Foundation/Channel/IChannel.cs +++ b/src/Foundation/ThingsGateway.Foundation/Channel/IChannel.cs @@ -65,7 +65,7 @@ public interface IChannel : ISetupConfigObject, IDisposable, IClosableClient, IC /// /// 接收事件回调类 /// -public class ChannelReceivedEventHandler : List> +public class ChannelReceivedEventHandler : List> { } diff --git a/src/Foundation/ThingsGateway.Foundation/Channel/OtherChannel.cs b/src/Foundation/ThingsGateway.Foundation/Channel/OtherChannel.cs index ce97d1d68..5c78efa0a 100644 --- a/src/Foundation/ThingsGateway.Foundation/Channel/OtherChannel.cs +++ b/src/Foundation/ThingsGateway.Foundation/Channel/OtherChannel.cs @@ -131,10 +131,10 @@ public class OtherChannel : SetupConfigObject, IClientChannel m_dataHandlingAdapter = adapter; } - private Task PrivateHandleReceivedData(ReadOnlyMemory byteBlock, IRequestInfo requestInfo) + private async Task PrivateHandleReceivedData(ReadOnlyMemory byteBlock, IRequestInfo requestInfo) { LastReceivedTime = DateTime.Now; - return this.OnChannelReceivedEvent(new ReceivedDataEventArgs(byteBlock, requestInfo), ChannelReceived); + await this.OnChannelReceivedEvent(new ReceivedDataEventArgs(byteBlock, requestInfo), ChannelReceived).ConfigureAwait(false); } /// diff --git a/src/Foundation/ThingsGateway.Foundation/Device/DeviceBase.cs b/src/Foundation/ThingsGateway.Foundation/Device/DeviceBase.cs index 60ad76bed..172da6b7c 100644 --- a/src/Foundation/ThingsGateway.Foundation/Device/DeviceBase.cs +++ b/src/Foundation/ThingsGateway.Foundation/Device/DeviceBase.cs @@ -308,7 +308,7 @@ public abstract class DeviceBase : AsyncAndSyncDisposableObject, IDevice /// /// 接收,非主动发送的情况,重写实现非主从并发通讯协议,如果通道存在其他设备并且不希望其他设备处理时,设置 为true /// - protected virtual Task ChannelReceived(IClientChannel client, ReceivedDataEventArgs e, bool last) + protected virtual ValueTask ChannelReceived(IClientChannel client, ReceivedDataEventArgs e, bool last) { if (e.RequestInfo is MessageBase response) { @@ -325,7 +325,7 @@ public abstract class DeviceBase : AsyncAndSyncDisposableObject, IDevice } } - return EasyTask.CompletedTask; + return EasyValueTask.CompletedTask; } public bool AutoConnect { get; protected set; } = true; /// diff --git a/src/Gateway/ThingsGateway.Gateway.Application/Common/Tasks/CronScheduledTask.cs b/src/Gateway/ThingsGateway.Gateway.Application/Common/Tasks/CronScheduledTask.cs index 3f808f647..75ed147df 100644 --- a/src/Gateway/ThingsGateway.Gateway.Application/Common/Tasks/CronScheduledTask.cs +++ b/src/Gateway/ThingsGateway.Gateway.Application/Common/Tasks/CronScheduledTask.cs @@ -107,7 +107,8 @@ public class CronScheduledTask : DisposeBase, IScheduledTask { if (!Check()) { - SetNext(next); + int nextValue = next; + SetNext(nextValue); } } } @@ -149,7 +150,8 @@ public class CronScheduledTask : DisposeBase, IScheduledTask { if (!Check()) { - SetNext(next); + int nextValue = next; + SetNext(nextValue); } } } diff --git a/src/Gateway/ThingsGateway.Gateway.Application/Common/Tasks/ScheduledAsyncTask.cs b/src/Gateway/ThingsGateway.Gateway.Application/Common/Tasks/ScheduledAsyncTask.cs index 12d261e85..295924e05 100644 --- a/src/Gateway/ThingsGateway.Gateway.Application/Common/Tasks/ScheduledAsyncTask.cs +++ b/src/Gateway/ThingsGateway.Gateway.Application/Common/Tasks/ScheduledAsyncTask.cs @@ -93,7 +93,8 @@ public class ScheduledAsyncTask : DisposeBase, IScheduledTask, IScheduledIntInte { if (!Check() && IntervalMS > 8) { - SetNext(next); + int nextValue = next; + SetNext(nextValue); } } } diff --git a/src/Gateway/ThingsGateway.Gateway.Application/Common/Tasks/ScheduledSyncTask.cs b/src/Gateway/ThingsGateway.Gateway.Application/Common/Tasks/ScheduledSyncTask.cs index 6d6b33a81..17a12aa08 100644 --- a/src/Gateway/ThingsGateway.Gateway.Application/Common/Tasks/ScheduledSyncTask.cs +++ b/src/Gateway/ThingsGateway.Gateway.Application/Common/Tasks/ScheduledSyncTask.cs @@ -80,7 +80,8 @@ public class ScheduledSyncTask : DisposeBase, IScheduledTask, IScheduledIntInter { if (!Check() && IntervalMS > 8) { - SetNext(next); + int nextValue = next; + SetNext(nextValue); } } } diff --git a/src/Gateway/ThingsGateway.Gateway.Application/Driver/Collect/CollectBase.cs b/src/Gateway/ThingsGateway.Gateway.Application/Driver/Collect/CollectBase.cs index 0010b8391..f3219512b 100644 --- a/src/Gateway/ThingsGateway.Gateway.Application/Driver/Collect/CollectBase.cs +++ b/src/Gateway/ThingsGateway.Gateway.Application/Driver/Collect/CollectBase.cs @@ -359,124 +359,314 @@ public abstract partial class CollectBase : DriverBase private readonly LinkedCancellationTokenSourceCache _linkedCtsCache = new(); #region 执行默认读取 - async ValueTask ReadVariableSource(object? state, CancellationToken cancellationToken) + //async ValueTask ReadVariableSource(object? state, CancellationToken cancellationToken) + //{ + + // if (state is not VariableSourceRead variableSourceRead) return; + + // if (Pause) return; + // if (cancellationToken.IsCancellationRequested) return; + // CancellationToken readToken = default; + // var readerLockTask = ReadWriteLock.ReaderLockAsync(cancellationToken); + // if (!readerLockTask.IsCompleted) + // { + // readToken = await readerLockTask.ConfigureAwait(false); + // } + // else + // { + // readToken = readerLockTask.Result; + // } + + // if (readToken.IsCancellationRequested) + // { + // await ReadVariableSource(state, cancellationToken).ConfigureAwait(false); + // return; + // } + + // var allTokenSource = _linkedCtsCache.GetLinkedTokenSource(cancellationToken, readToken); + // var allToken = allTokenSource.Token; + + // //if (LogMessage?.LogLevel <= TouchSocket.Core.LogLevel.Trace) + // // LogMessage?.Trace(string.Format("{0} - Collecting [{1} - {2}]", DeviceName, variableSourceRead?.RegisterAddress, variableSourceRead?.Length)); + + // OperResult> readResult = default; + // var readTask = ReadSourceAsync(variableSourceRead, allToken); + // if (!readTask.IsCompleted) + // { + // readResult = await readTask.ConfigureAwait(false); + // } + // else + // { + // readResult = readTask.Result; + // } + + // var readErrorCount = 0; + + // // 读取失败时重试一定次数 + // while (!readResult.IsSuccess && readErrorCount < CollectProperties.RetryCount) + // { + // if (Pause) + // return; + // if (cancellationToken.IsCancellationRequested) + // return; + + // if (readToken.IsCancellationRequested) + // { + // await ReadVariableSource(state, cancellationToken).ConfigureAwait(false); + // return; + // } + + // readErrorCount++; + // if (LogMessage?.LogLevel <= TouchSocket.Core.LogLevel.Trace) + // LogMessage?.Trace(string.Format("{0} - Collection [{1} - {2}] failed - {3}", DeviceName, variableSourceRead?.RegisterAddress, variableSourceRead?.Length, readResult.ErrorMessage)); + + // //if (LogMessage?.LogLevel <= TouchSocket.Core.LogLevel.Trace) + // // LogMessage?.Trace(string.Format("{0} - Collecting [{1} - {2}]", DeviceName, variableSourceRead?.RegisterAddress, variableSourceRead?.Length)); + // var readTask1 = ReadSourceAsync(variableSourceRead, allToken); + // if (!readTask1.IsCompleted) + // { + // readResult = await readTask1.ConfigureAwait(false); + // } + // else + // { + // readResult = readTask1.Result; + // } + + // } + + // if (readResult.IsSuccess) + // { + // // 读取成功时记录日志并增加成功计数器 + // if (LogMessage?.LogLevel <= TouchSocket.Core.LogLevel.Trace) + // LogMessage?.Trace(string.Format("{0} - Collection [{1} - {2}] data succeeded {3}", DeviceName, variableSourceRead?.RegisterAddress, variableSourceRead?.Length, readResult.Content.Span.ToHexString(' '))); + // CurrentDevice.SetDeviceStatus(TimerX.Now, null); + // } + // else + // { + // if (cancellationToken.IsCancellationRequested) + // return; + + // if (readToken.IsCancellationRequested) + // { + // await ReadVariableSource(state, cancellationToken).ConfigureAwait(false); + // return; + // } + + // // 读取失败时记录日志并增加失败计数器,更新错误信息并清除变量状态 + // if (variableSourceRead.LastErrorMessage != readResult.ErrorMessage) + // { + // if (!cancellationToken.IsCancellationRequested) + // LogMessage?.LogWarning(readResult.Exception, string.Format(AppResource.CollectFail, DeviceName, variableSourceRead?.RegisterAddress, variableSourceRead?.Length, readResult.ErrorMessage)); + // } + // else + // { + // if (!cancellationToken.IsCancellationRequested) + // { + // if (LogMessage?.LogLevel <= TouchSocket.Core.LogLevel.Trace) + // LogMessage?.Trace(string.Format("{0} - Collection [{1} - {2}] data failed - {3}", DeviceName, variableSourceRead?.RegisterAddress, variableSourceRead?.Length, readResult.ErrorMessage)); + // } + // } + + // variableSourceRead.LastErrorMessage = readResult.ErrorMessage; + // CurrentDevice.SetDeviceStatus(TimerX.Now, null, readResult.ErrorMessage); + // var time = DateTime.Now; + // foreach (var item in variableSourceRead.VariableRuntimes) + // { + // item.SetValue(null, time, isOnline: false); + // } + // } + //} + + + + private ValueTask ReadVariableSource(object? state, CancellationToken cancellationToken) { + var enumerator = new ReadVariableSourceEnumerator(this, state, cancellationToken); + return enumerator.MoveNextAsync(); + } - if (state is not VariableSourceRead variableSourceRead) return; + private struct ReadVariableSourceEnumerator + { + private readonly CollectBase _owner; + private readonly object? _state; + private readonly CancellationToken _cancellationToken; - if (Pause) return; - if (cancellationToken.IsCancellationRequested) return; - CancellationToken readToken = default; - var readerLockTask = ReadWriteLock.ReaderLockAsync(cancellationToken); - if (!readerLockTask.IsCompleted) + private VariableSourceRead _variableSourceRead; + private CancellationToken _readToken; + private CancellationToken _allToken; + private OperResult> _readResult; + private int _readErrorCount; + private ValueTask _readerLockTask; + private ValueTask>> _readTask; + private int _step; + + public ReadVariableSourceEnumerator(CollectBase owner, object? state, CancellationToken cancellationToken) { - readToken = await readerLockTask.ConfigureAwait(false); - } - else - { - readToken = readerLockTask.Result; + _owner = owner; + _state = state; + _cancellationToken = cancellationToken; + + _variableSourceRead = default!; + _readToken = default; + _allToken = default; + _readResult = default; + _readErrorCount = 0; + _readerLockTask = default; + _readTask = default; + _step = 0; } - if (readToken.IsCancellationRequested) + public ValueTask MoveNextAsync() { - await ReadVariableSource(state, cancellationToken).ConfigureAwait(false); - return; + switch (_step) + { + case 0: + if (_state is not VariableSourceRead vsr) return default; + _variableSourceRead = vsr; + + if (_owner.Pause) return default; + if (_cancellationToken.IsCancellationRequested) return default; + +#pragma warning disable CA2012 // 正确使用 ValueTask + _readerLockTask = _owner.ReadWriteLock.ReaderLockAsync(_cancellationToken); +#pragma warning restore CA2012 // 正确使用 ValueTask + if (!_readerLockTask.IsCompleted) + { + _step = 1; + return AwaitReaderLock(); + } + _readToken = _readerLockTask.Result; + goto case 2; + + case 1: + _readToken = _readerLockTask.Result; + goto case 2; + + case 2: + if (_readToken.IsCancellationRequested) + { + return _owner.ReadVariableSource(_state, _cancellationToken); + } + + var allTokenSource = _owner._linkedCtsCache.GetLinkedTokenSource(_cancellationToken, _readToken); + _allToken = allTokenSource.Token; + +#pragma warning disable CA2012 // 正确使用 ValueTask + _readTask = _owner.ReadSourceAsync(_variableSourceRead, _allToken); +#pragma warning restore CA2012 // 正确使用 ValueTask + if (!_readTask.IsCompleted) + { + _step = 3; + return AwaitRead(); + } + _readResult = _readTask.Result; + goto case 4; + + case 3: + _readResult = _readTask.Result; + goto case 4; + + case 4: + while (!_readResult.IsSuccess && _readErrorCount < _owner.CollectProperties.RetryCount) + { + if (_owner.Pause) return default; + if (_cancellationToken.IsCancellationRequested) return default; + + if (_readToken.IsCancellationRequested) + { + return _owner.ReadVariableSource(_state, _cancellationToken); + } + + _readErrorCount++; + if (_owner.LogMessage?.LogLevel <= TouchSocket.Core.LogLevel.Trace) + _owner.LogMessage?.Trace(string.Format("{0} - Collection [{1} - {2}] failed - {3}", + _owner.DeviceName, _variableSourceRead?.RegisterAddress, _variableSourceRead?.Length, _readResult.ErrorMessage)); + +#pragma warning disable CA2012 // 正确使用 ValueTask + _readTask = _owner.ReadSourceAsync(_variableSourceRead, _allToken); +#pragma warning restore CA2012 // 正确使用 ValueTask + if (!_readTask.IsCompleted) + { + _step = 5; + return AwaitReadRetry(); + } + _readResult = _readTask.Result; + } + + goto case 6; + + case 5: + _readResult = _readTask.Result; + _step = 4; + return MoveNextAsync(); + + case 6: + if (_readResult.IsSuccess) + { + if (_owner.LogMessage?.LogLevel <= TouchSocket.Core.LogLevel.Trace) + _owner.LogMessage?.Trace(string.Format("{0} - Collection [{1} - {2}] data succeeded {3}", + _owner.DeviceName, _variableSourceRead?.RegisterAddress, _variableSourceRead?.Length, _readResult.Content.Span.ToHexString(' '))); + + _owner.CurrentDevice.SetDeviceStatus(TimerX.Now, null); + } + else + { + if (_cancellationToken.IsCancellationRequested) return default; + if (_readToken.IsCancellationRequested) + { + return _owner.ReadVariableSource(_state, _cancellationToken); + } + + if (_variableSourceRead.LastErrorMessage != _readResult.ErrorMessage) + { + if (!_cancellationToken.IsCancellationRequested) + _owner.LogMessage?.LogWarning(_readResult.Exception, string.Format(AppResource.CollectFail, _owner.DeviceName, + _variableSourceRead?.RegisterAddress, _variableSourceRead?.Length, _readResult.ErrorMessage)); + } + else + { + if (!_cancellationToken.IsCancellationRequested && _owner.LogMessage?.LogLevel <= TouchSocket.Core.LogLevel.Trace) + _owner.LogMessage?.Trace(string.Format("{0} - Collection [{1} - {2}] data failed - {3}", + _owner.DeviceName, _variableSourceRead?.RegisterAddress, _variableSourceRead?.Length, _readResult.ErrorMessage)); + } + + _variableSourceRead.LastErrorMessage = _readResult.ErrorMessage; + _owner.CurrentDevice.SetDeviceStatus(TimerX.Now, null, _readResult.ErrorMessage); + var time = DateTime.Now; + foreach (var item in _variableSourceRead.VariableRuntimes) + { + item.SetValue(null, time, isOnline: false); + } + } + break; + } + + return default; } - var allTokenSource = _linkedCtsCache.GetLinkedTokenSource(cancellationToken, readToken); - var allToken = allTokenSource.Token; - - //if (LogMessage?.LogLevel <= TouchSocket.Core.LogLevel.Trace) - // LogMessage?.Trace(string.Format("{0} - Collecting [{1} - {2}]", DeviceName, variableSourceRead?.RegisterAddress, variableSourceRead?.Length)); - - OperResult> readResult = default; - var readTask = ReadSourceAsync(variableSourceRead, allToken); - if (!readTask.IsCompleted) + private async ValueTask AwaitReaderLock() { - readResult = await readTask.ConfigureAwait(false); - } - else - { - readResult = readTask.Result; + await _readerLockTask.ConfigureAwait(false); + _step = 1; + await MoveNextAsync().ConfigureAwait(false); } - var readErrorCount = 0; - - // 读取失败时重试一定次数 - while (!readResult.IsSuccess && readErrorCount < CollectProperties.RetryCount) + private async ValueTask AwaitRead() { - if (Pause) - return; - if (cancellationToken.IsCancellationRequested) - return; - - if (readToken.IsCancellationRequested) - { - await ReadVariableSource(state, cancellationToken).ConfigureAwait(false); - return; - } - - readErrorCount++; - if (LogMessage?.LogLevel <= TouchSocket.Core.LogLevel.Trace) - LogMessage?.Trace(string.Format("{0} - Collection [{1} - {2}] failed - {3}", DeviceName, variableSourceRead?.RegisterAddress, variableSourceRead?.Length, readResult.ErrorMessage)); - - //if (LogMessage?.LogLevel <= TouchSocket.Core.LogLevel.Trace) - // LogMessage?.Trace(string.Format("{0} - Collecting [{1} - {2}]", DeviceName, variableSourceRead?.RegisterAddress, variableSourceRead?.Length)); - var readTask1 = ReadSourceAsync(variableSourceRead, allToken); - if (!readTask1.IsCompleted) - { - readResult = await readTask1.ConfigureAwait(false); - } - else - { - readResult = readTask1.Result; - } - + await _readTask.ConfigureAwait(false); + _step = 3; + await MoveNextAsync().ConfigureAwait(false); } - if (readResult.IsSuccess) + private async ValueTask AwaitReadRetry() { - // 读取成功时记录日志并增加成功计数器 - if (LogMessage?.LogLevel <= TouchSocket.Core.LogLevel.Trace) - LogMessage?.Trace(string.Format("{0} - Collection [{1} - {2}] data succeeded {3}", DeviceName, variableSourceRead?.RegisterAddress, variableSourceRead?.Length, readResult.Content.Span.ToHexString(' '))); - CurrentDevice.SetDeviceStatus(TimerX.Now, null); - } - else - { - if (cancellationToken.IsCancellationRequested) - return; - - if (readToken.IsCancellationRequested) - { - await ReadVariableSource(state, cancellationToken).ConfigureAwait(false); - return; - } - - // 读取失败时记录日志并增加失败计数器,更新错误信息并清除变量状态 - if (variableSourceRead.LastErrorMessage != readResult.ErrorMessage) - { - if (!cancellationToken.IsCancellationRequested) - LogMessage?.LogWarning(readResult.Exception, string.Format(AppResource.CollectFail, DeviceName, variableSourceRead?.RegisterAddress, variableSourceRead?.Length, readResult.ErrorMessage)); - } - else - { - if (!cancellationToken.IsCancellationRequested) - { - if (LogMessage?.LogLevel <= TouchSocket.Core.LogLevel.Trace) - LogMessage?.Trace(string.Format("{0} - Collection [{1} - {2}] data failed - {3}", DeviceName, variableSourceRead?.RegisterAddress, variableSourceRead?.Length, readResult.ErrorMessage)); - } - } - - variableSourceRead.LastErrorMessage = readResult.ErrorMessage; - CurrentDevice.SetDeviceStatus(TimerX.Now, null, readResult.ErrorMessage); - var time = DateTime.Now; - foreach (var item in variableSourceRead.VariableRuntimes) - { - item.SetValue(null, time, isOnline: false); - } + await _readTask.ConfigureAwait(false); + _step = 5; + await MoveNextAsync().ConfigureAwait(false); } } + #endregion diff --git a/src/Gateway/ThingsGateway.Gateway.Application/Driver/Collect/CollectFoundationBase.cs b/src/Gateway/ThingsGateway.Gateway.Application/Driver/Collect/CollectFoundationBase.cs index 9291cc55e..d7f8253c2 100644 --- a/src/Gateway/ThingsGateway.Gateway.Application/Driver/Collect/CollectFoundationBase.cs +++ b/src/Gateway/ThingsGateway.Gateway.Application/Driver/Collect/CollectFoundationBase.cs @@ -147,43 +147,151 @@ public abstract class CollectFoundationBase : CollectBase return; } + + + //protected override async ValueTask>> ReadSourceAsync(VariableSourceRead variableSourceRead, CancellationToken cancellationToken) + //{ + // try + // { + + // if (cancellationToken.IsCancellationRequested) + // return new(new OperationCanceledException()); + + // // 从协议读取数据 + // OperResult> read = default; + // var readTask = FoundationDevice.ReadAsync(variableSourceRead.AddressObject, cancellationToken); + // if (!readTask.IsCompleted) + // { + // read = await readTask.ConfigureAwait(false); + // } + // else + // { + // read = readTask.Result; + // } + + // // 如果读取成功且有有效内容,则解析结构化内容 + // if (read.IsSuccess) + // { + // var prase = variableSourceRead.VariableRuntimes.PraseStructContent(FoundationDevice, read.Content.Span, false); + // return new OperResult>(prase); + // } + + // // 返回读取结果 + // return read; + // } + // catch (Exception ex) + // { + // // 捕获异常并返回失败结果 + // return new OperResult>(ex); + // } + //} /// /// 采集驱动读取,读取成功后直接赋值变量,失败不做处理,注意非通用设备需重写 /// - protected override async ValueTask>> ReadSourceAsync(VariableSourceRead variableSourceRead, CancellationToken cancellationToken) + protected override ValueTask>> ReadSourceAsync(VariableSourceRead variableSourceRead, CancellationToken cancellationToken) { - try + if (cancellationToken.IsCancellationRequested) + return new ValueTask>>( new OperResult>(new OperationCanceledException())); + + // 值类型状态机 + var stateMachine = new ReadSourceStateMachine(this, variableSourceRead, cancellationToken); + return stateMachine.MoveNextAsync(); + } + + private struct ReadSourceStateMachine + { + private readonly VariableSourceRead _variableSourceRead; + private readonly CancellationToken _cancellationToken; + private readonly CollectFoundationBase _owner; + private OperResult> _result; + private ValueTask>> _readTask; + + public ReadSourceStateMachine(CollectFoundationBase owner, VariableSourceRead variableSourceRead, CancellationToken cancellationToken) { - - if (cancellationToken.IsCancellationRequested) - return new(new OperationCanceledException()); - - // 从协议读取数据 - OperResult> read = default; - var readTask = FoundationDevice.ReadAsync(variableSourceRead.AddressObject, cancellationToken); - if (!readTask.IsCompleted) - { - read = await readTask.ConfigureAwait(false); - } - else - { - read = readTask.Result; - } - - // 如果读取成功且有有效内容,则解析结构化内容 - if (read.IsSuccess) - { - var prase = variableSourceRead.VariableRuntimes.PraseStructContent(FoundationDevice, read.Content.Span, false); - return new OperResult>(prase); - } - - // 返回读取结果 - return read; + _owner = owner; + _variableSourceRead = variableSourceRead; + _cancellationToken = cancellationToken; + _result = default; + State = 0; } - catch (Exception ex) + + public int State { get; private set; } + + public ValueTask>> MoveNextAsync() { - // 捕获异常并返回失败结果 - return new OperResult>(ex); + try + { + switch (State) + { + case 0: + // 异步读取 + if (_cancellationToken.IsCancellationRequested) + { + _result = new OperResult>(new OperationCanceledException()); + return new ValueTask>>(_result); + } + +#pragma warning disable CA2012 // 正确使用 ValueTask + _readTask = _owner.FoundationDevice.ReadAsync(_variableSourceRead.AddressObject, _cancellationToken); +#pragma warning restore CA2012 // 正确使用 ValueTask + + // 检查是否任务已完成 + if (_readTask.IsCompleted) + { + _result = _readTask.Result; + State = 1; + return MoveNextAsync(); + } + + // 如果任务尚未完成,继续等待 + State = 2; + return Awaited(_readTask); + + case 1: + // 解析结构化内容 + if (_result.IsSuccess) + { + var parsedResult = _variableSourceRead.VariableRuntimes.PraseStructContent(_owner.FoundationDevice, _result.Content.Span, false); + return new ValueTask>>(new OperResult>(parsedResult)); + } + + return new ValueTask>>(_result); + + case 2: + // 完成任务后,解析内容 + _result = _readTask.Result; + + if (_result.IsSuccess) + { + var parsedResult = _variableSourceRead.VariableRuntimes.PraseStructContent(_owner.FoundationDevice, _result.Content.Span, false); + return new ValueTask>>(new OperResult>(parsedResult)); + } + + return new ValueTask>>(_result); + + default: + throw new InvalidOperationException("Unexpected state."); + } + } + catch (Exception ex) + { + return new ValueTask>>(new OperResult>(ex)); + } + } + + private async ValueTask>> Awaited(ValueTask>> vt) + { + try + { + + + await vt.ConfigureAwait(false); + return await MoveNextAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + return new OperResult>(ex); + } } } diff --git a/src/Gateway/ThingsGateway.Gateway.Razor/Pages/GatewayMonitorPage/ChannelDeviceTree.razor b/src/Gateway/ThingsGateway.Gateway.Razor/Pages/GatewayMonitorPage/ChannelDeviceTree.razor index 29145e472..d3cc37e1f 100644 --- a/src/Gateway/ThingsGateway.Gateway.Razor/Pages/GatewayMonitorPage/ChannelDeviceTree.razor +++ b/src/Gateway/ThingsGateway.Gateway.Razor/Pages/GatewayMonitorPage/ChannelDeviceTree.razor @@ -2,7 +2,8 @@ @using ThingsGateway.Admin.Application @using ThingsGateway.Admin.Razor @using ThingsGateway.Gateway.Application - +@attribute [JSModuleAutoLoader("Pages/GatewayMonitorPage/ChannelDeviceTree.razor.js", AutoInvokeInit = true, AutoInvokeDispose = false, JSObjectReference = true)] +@inherits ThingsGatewayModuleComponentBase
@@ -13,7 +14,7 @@ - +
@@ -137,6 +138,6 @@ @code { RenderFragment RenderTreeItem = (item) => - @@item.ToString() ; + @@item.ToString() ; } \ No newline at end of file diff --git a/src/Gateway/ThingsGateway.Gateway.Razor/Pages/GatewayMonitorPage/ChannelDeviceTree.razor.cs b/src/Gateway/ThingsGateway.Gateway.Razor/Pages/GatewayMonitorPage/ChannelDeviceTree.razor.cs index 7dca313a0..9ad23ec68 100644 --- a/src/Gateway/ThingsGateway.Gateway.Razor/Pages/GatewayMonitorPage/ChannelDeviceTree.razor.cs +++ b/src/Gateway/ThingsGateway.Gateway.Razor/Pages/GatewayMonitorPage/ChannelDeviceTree.razor.cs @@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Components.Forms; using Microsoft.AspNetCore.Components.Web; +using Microsoft.JSInterop; using ThingsGateway.Admin.Application; using ThingsGateway.Admin.Razor; @@ -19,7 +20,7 @@ using ThingsGateway.SqlSugar; namespace ThingsGateway.Gateway.Razor; -public partial class ChannelDeviceTree : IDisposable +public partial class ChannelDeviceTree { SpinnerComponent Spinner; [Inject] @@ -297,7 +298,7 @@ public partial class ChannelDeviceTree : IDisposable { if (item.TryGetChannelRuntime(out var channelRuntime)) { - return channelRuntime.DeviceThreadManage != null ? "enable--text" : "disabled--text"; + return channelRuntime.DeviceThreadManage != null ? " enable--text" : " disabled--text "; } else if (item.TryGetDeviceRuntime(out var deviceRuntime)) { @@ -1391,24 +1392,6 @@ EventCallback.Factory.Create(this, async e => scheduler = new SmartTriggerScheduler(Notify, TimeSpan.FromMilliseconds(3000)); - _ = Task.Run(async () => - { - while (!Disposed) - { - try - { - await InvokeAsync(StateHasChanged); - } - catch - { - } - finally - { - await Task.Delay(5000); - } - } - }); - await base.OnInitializedAsync(); } private async Task Notify(CancellationToken cancellationToken) @@ -1423,7 +1406,7 @@ EventCallback.Factory.Create(this, async e => { await ChannelDeviceChanged.Invoke(Value); } - + await InvokeAsync(StateHasChanged); } private static ChannelDeviceTreeItem GetValue(ChannelDeviceTreeItem channelDeviceTreeItem) @@ -1595,10 +1578,16 @@ EventCallback.Factory.Create(this, async e => } } private bool Disposed; - public void Dispose() + + protected override async ValueTask DisposeAsync(bool disposing) { + Disposed = true; ChannelRuntimeDispatchService.UnSubscribe(Refresh); + + await Module.InvokeVoidAsync("dispose", Id); + + await base.DisposeAsync(disposing); } ChannelDeviceTreeItem? SelectModel = default; @@ -1616,5 +1605,28 @@ EventCallback.Factory.Create(this, async e => return Task.CompletedTask; } + #region js + protected override Task InvokeInitAsync() => InvokeVoidAsync("init", Id, Interop, new { Method = nameof(TriggerStateChanged) }); + + + + [JSInvokable] + public List TriggerStateChanged(List jsstring) + { + List ret = new(jsstring.Count); + foreach (var str in jsstring) + { + var item = ChannelDeviceTreeItem.FromJSString(str); + ret.Add(GetClass(item)); + } + return ret; + } + + private static string GetId(ChannelDeviceTreeItem channelDeviceTreeItem) + { + return ChannelDeviceTreeItem.ToJSString(channelDeviceTreeItem); + } + + #endregion } diff --git a/src/Gateway/ThingsGateway.Gateway.Razor/Pages/GatewayMonitorPage/ChannelDeviceTree.razor.js b/src/Gateway/ThingsGateway.Gateway.Razor/Pages/GatewayMonitorPage/ChannelDeviceTree.razor.js new file mode 100644 index 000000000..7aba8a80f --- /dev/null +++ b/src/Gateway/ThingsGateway.Gateway.Razor/Pages/GatewayMonitorPage/ChannelDeviceTree.razor.js @@ -0,0 +1,57 @@ +let handlers = {}; + +export function init(id, invoke, options) { + //function getCellByClass(row, className) { + // // 直接用 querySelector 精确查找 + // return row.querySelector(`td.${className}`); + //} + var variableHandler = setInterval(async () => { + var treeview = document.getElementById(id); + if (!treeview) { + clearInterval(variableHandler); + return; + } + + const spans = treeview.querySelectorAll( + '.tree-content[style*="--bb-tree-view-level: 2"] .tree-node > span, ' + + '.tree-content[style*="--bb-tree-view-level: 3"] .tree-node > span' + ); + + if (!spans) { + return; + } + + const ids = Array.from(spans).map(span => span.id); + + var { method } = options; + + if (!invoke) return; + var valss = await invoke.invokeMethodAsync(method, ids); + if (!valss || valss.length === 0) return; + // 遍历 valss,下标 i 对应 span[i] + for (let i = 0; i < valss.length && i < spans.length; i++) { + const val = valss[i]; + const span = spans[i]; + + if (!span) continue; + + if (span.className !== val) { + span.className = val; + } + + } + + } + , 1000) //1000ms刷新一次 + + handlers[id] = { variableHandler, invoke }; + +} +export function dispose(id) { + const handler = handlers[id]; + if (handler) { + clearInterval(handler.timer); + handler.invoke = null; + delete handlers[id]; + } +} diff --git a/src/Gateway/ThingsGateway.Gateway.Razor/Pages/GatewayMonitorPage/ChannelDeviceTreeItem.cs b/src/Gateway/ThingsGateway.Gateway.Razor/Pages/GatewayMonitorPage/ChannelDeviceTreeItem.cs index 85e05446d..6b72444ce 100644 --- a/src/Gateway/ThingsGateway.Gateway.Razor/Pages/GatewayMonitorPage/ChannelDeviceTreeItem.cs +++ b/src/Gateway/ThingsGateway.Gateway.Razor/Pages/GatewayMonitorPage/ChannelDeviceTreeItem.cs @@ -141,4 +141,77 @@ public class ChannelDeviceTreeItem : IEqualityComparer { return HashCode.Combine(obj.ChannelDevicePluginType, obj.DeviceRuntimeId, obj.ChannelRuntimeId, obj.PluginName, obj.PluginType); } + + + + public static string ToJSString(ChannelDeviceTreeItem channelDeviceTreeItem) + { + return $"{channelDeviceTreeItem.ChannelDevicePluginType}.{channelDeviceTreeItem.DeviceRuntimeId}.{channelDeviceTreeItem.ChannelRuntimeId}.{channelDeviceTreeItem.PluginName}.{channelDeviceTreeItem.PluginType}"; + } + + public static ChannelDeviceTreeItem FromJSString(string jsString) + { + if (string.IsNullOrWhiteSpace(jsString)) + throw new ArgumentNullException(nameof(jsString)); + + ReadOnlySpan span = jsString.AsSpan(); + Span ranges = stackalloc Range[5]; + + // 手动分割 + int partIndex = 0; + int start = 0; + while (partIndex < 4) // 只找前4个分隔符 + { + int idx = span[start..].IndexOf('.'); + if (idx == -1) + throw new FormatException($"Invalid format: expected 5 parts, got {partIndex + 1}"); + + ranges[partIndex] = new Range(start, start + idx); + start += idx + 1; + partIndex++; + } + + // 最后一段 + ranges[partIndex] = new Range(start, span.Length); + + // 校验段数 + if (partIndex != 4) + throw new FormatException($"Invalid format: expected 5 parts, got {partIndex + 1}"); + + var part0 = span[ranges[0]]; + var part1 = span[ranges[1]]; + var part2 = span[ranges[2]]; + var part3 = span[ranges[3]]; + var part4 = span[ranges[4]]; + + // 解析 Enum 和 long + if (!Enum.TryParse(part0, out ChannelDevicePluginTypeEnum pluginType)) + throw new FormatException($"Invalid {nameof(ChannelDevicePluginTypeEnum)}: {part0.ToString()}"); + + if (!long.TryParse(part1, out long deviceRuntimeId)) + throw new FormatException($"Invalid DeviceRuntimeId: {part1.ToString()}"); + + if (!long.TryParse(part2, out long channelRuntimeId)) + throw new FormatException($"Invalid ChannelRuntimeId: {part2.ToString()}"); + + string pluginName = part3.ToString(); + + PluginTypeEnum? parsedPluginType = null; + if (!part4.IsEmpty) + { + if (!Enum.TryParse(part4, out PluginTypeEnum tmp)) + throw new FormatException($"Invalid {nameof(PluginTypeEnum)}: {part4.ToString()}"); + parsedPluginType = tmp; + } + + return new ChannelDeviceTreeItem + { + ChannelDevicePluginType = pluginType, + DeviceRuntimeId = deviceRuntimeId, + ChannelRuntimeId = channelRuntimeId, + PluginName = pluginName, + PluginType = parsedPluginType + }; + } + } diff --git a/src/Gateway/ThingsGateway.Gateway.Razor/Pages/GatewayMonitorPage/Variable/VariableModelUtils.cs b/src/Gateway/ThingsGateway.Gateway.Razor/Pages/GatewayMonitorPage/Variable/VariableModelUtils.cs index 8ddb23b03..644bf04b3 100644 --- a/src/Gateway/ThingsGateway.Gateway.Razor/Pages/GatewayMonitorPage/Variable/VariableModelUtils.cs +++ b/src/Gateway/ThingsGateway.Gateway.Razor/Pages/GatewayMonitorPage/Variable/VariableModelUtils.cs @@ -1,6 +1,11 @@ -using ThingsGateway.NewLife.Caching; -using ThingsGateway.NewLife.Json.Extension; +using Microsoft.CSharp.RuntimeBinder; +using System.Dynamic; +using System.Linq.Expressions; + +using ThingsGateway.Common.Extension; +using ThingsGateway.NewLife.Caching; +using ThingsGateway.NewLife.Json.Extension; namespace ThingsGateway.Gateway.Razor; public static class VariableModelUtils @@ -21,11 +26,73 @@ public static class VariableModelUtils { var ret = MemoryCache.GetOrAdd(fieldName, (fieldName) => { - return LambdaExtensions.GetPropertyValueLambda(model, fieldName).Compile(); + return GetPropertyValueLambda(fieldName).Compile(); })(model); return ret; } } + /// + /// 获取属性方法 Lambda 表达式 + /// + /// + /// + /// + /// + public static Expression> GetPropertyValueLambda(string propertyName) where TModel : class, new() + { + + var type = typeof(TModel); + var parameter = Expression.Parameter(typeof(TModel)); + + return !type.Assembly.IsDynamic && propertyName.Contains('.') + ? GetComplexPropertyExpression() + : GetSimplePropertyExpression(); + + Expression> GetSimplePropertyExpression() + { + Expression body; + var p = type.GetPropertyByName(propertyName); + if (p != null) + { + body = Expression.Property(Expression.Convert(parameter, type), p); + } + else if (type.IsAssignableTo(typeof(IDynamicMetaObjectProvider))) + { + var binder = Microsoft.CSharp.RuntimeBinder.Binder.GetMember( + CSharpBinderFlags.None, + propertyName, + type, + [CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null)]); + body = Expression.Dynamic(binder, typeof(object), parameter); + } + else + { + throw new InvalidOperationException($"类型 {type.Name} 未找到 {propertyName} 属性,无法获取其值"); + } + + return Expression.Lambda>(Expression.Convert(body, typeof(TResult)), parameter); + } + + Expression> GetComplexPropertyExpression() + { + var propertyNames = propertyName.Split("."); + Expression? body = null; + Type t = type; + object? propertyInstance = new TModel(); + foreach (var name in propertyNames) + { + var p = t.GetPropertyByName(name) ?? throw new InvalidOperationException($"类型 {type.Name} 未找到 {name} 属性,无法获取其值"); + propertyInstance = p.GetValue(propertyInstance); + if (propertyInstance != null) + { + t = propertyInstance.GetType(); + } + + body = Expression.Property(body ?? Expression.Convert(parameter, type), p); + } + return Expression.Lambda>(Expression.Convert(body!, typeof(TResult)), parameter); + } + } public static string GetValue(VariableRuntime row, string fieldName) { diff --git a/src/Gateway/ThingsGateway.Gateway.Razor/Pages/GatewayMonitorPage/Variable/VariableRuntimeInfo.razor b/src/Gateway/ThingsGateway.Gateway.Razor/Pages/GatewayMonitorPage/Variable/VariableRuntimeInfo.razor index ae11f8424..43b3f4457 100644 --- a/src/Gateway/ThingsGateway.Gateway.Razor/Pages/GatewayMonitorPage/Variable/VariableRuntimeInfo.razor +++ b/src/Gateway/ThingsGateway.Gateway.Razor/Pages/GatewayMonitorPage/Variable/VariableRuntimeInfo.razor @@ -47,7 +47,7 @@ context.ChangeTime) Filterable=true Sortable=true Visible=false /> - context.CollectTime) Filterable=true Sortable=true Visible=true /> + context.CollectTime) Filterable=true Sortable=true Visible=true /> context.IsOnline) Filterable=true Sortable=true Visible=true /> context.LastErrorMessage) Filterable=true Sortable=true Visible=false /> @@ -93,7 +93,24 @@ - + + @foreach (var col in context.Columns) + { + +
+ @if (col.GetShowTips()) + { + + @VariableModelUtils.GetValue(context.Row, col.GetFieldName()) + + } + else + { + @VariableModelUtils.GetValue(context.Row, col.GetFieldName()) + } +
+ + }
diff --git a/src/Gateway/ThingsGateway.Gateway.Razor/Pages/GatewayMonitorPage/Variable/VariableRuntimeInfo.razor.cs b/src/Gateway/ThingsGateway.Gateway.Razor/Pages/GatewayMonitorPage/Variable/VariableRuntimeInfo.razor.cs index 5e0334009..46aa20e22 100644 --- a/src/Gateway/ThingsGateway.Gateway.Razor/Pages/GatewayMonitorPage/Variable/VariableRuntimeInfo.razor.cs +++ b/src/Gateway/ThingsGateway.Gateway.Razor/Pages/GatewayMonitorPage/Variable/VariableRuntimeInfo.razor.cs @@ -60,11 +60,163 @@ public partial class VariableRuntimeInfo [NotNull] public ToastService? ToastService { get; set; } - #region js - private List ColumnsFunc() + #region row + + + /// + /// 获得指定列头固定列样式 + /// + protected string? GetFixedCellStyleString(ITableColumn col, TableRowContext row, int margin = 0) { - return table.Columns; + string? ret = null; + if (col.Fixed) + { + ret = IsTail(col,row) ? GetRightStyle(col,row, margin) : GetLeftStyle(col, row); + } + return ret; } + + private string? GetLeftStyle(ITableColumn col, TableRowContext row) + { + var columns = row.Columns.ToList(); + var defaultWidth = 200; + var width = 0; + var start = 0; + var index = columns.IndexOf(col); + //if (GetFixedDetailRowHeaderColumn) + //{ + // width += DetailColumnWidth; + //} + //if (GetFixedMultipleSelectColumn) + //{ + // width += MultiColumnWidth; + //} + if (GetFixedLineNoColumn) + { + width += LineNoColumnWidth; + } + while (index > start) + { + var column = columns[start++]; + width += column.Width ?? defaultWidth; + } + return $"left: {width}px;"; + } + private bool GetFixedLineNoColumn = false; + + private string? GetRightStyle(ITableColumn col, TableRowContext row, int margin) + { + var columns = row.Columns.ToList(); + var defaultWidth = 200; + var width = 0; + var index = columns.IndexOf(col); + + // after + while (index + 1 < columns.Count) + { + var column = columns[index++]; + width += column.Width ?? defaultWidth; + } + //if (ShowExtendButtons && FixedExtendButtonsColumn) + { + width += ExtendButtonColumnWidth; + } + + // 如果是固定表头时增加滚动条位置 + if (IsFixedHeader && (index + 1) == columns.Count) + { + width += margin; + } + return $"right: {width}px;"; + } + private bool IsFixedHeader = true; + + public int LineNoColumnWidth { get; set; } = 60; + public int ExtendButtonColumnWidth { get; set; } = 220; + + + private bool IsTail(ITableColumn col,TableRowContext row) + { + var middle = Math.Floor(row.Columns.Count() * 1.0 / 2); + var index = Columns.IndexOf(col); + return middle < index; + } + + /// + /// 获得 Cell 文字样式 + /// + /// + /// + /// + /// + protected string? GetCellClassString(ITableColumn col, bool hasChildren, bool inCell) + { + + bool trigger = false; + return CssBuilder.Default("table-cell") +.AddClass(col.GetAlign().ToDescriptionString(), col.Align == Alignment.Center || col.Align == Alignment.Right) +.AddClass("is-wrap", col.GetTextWrap()) +.AddClass("is-ellips", col.GetTextEllipsis()) +.AddClass("is-tips", col.GetShowTips()) +.AddClass("is-resizable", AllowResizing) +.AddClass("is-tree", IsTree && hasChildren) +.AddClass("is-incell", inCell) +.AddClass("is-dbcell", trigger) +.AddClass(col.CssClass) +.Build(); + + } + + private bool AllowResizing = true; + private bool IsTree = false; + + /// + /// 获得指定列头固定列样式 + /// + protected string? GetFixedCellClassString(ITableColumn col, TableRowContext row) + { + return CssBuilder.Default() + .AddClass("fixed", col.Fixed) + .AddClass("fixed-right", col.Fixed && IsTail(col,row)) + .AddClass("fr", IsLastColumn(col, row)) + .AddClass("fl", IsFirstColumn(col, row)) + .Build(); + + } + + public List Columns=> table?.Columns; + + private bool IsLastColumn(ITableColumn col, TableRowContext row) + { + var ret = false; + if (col.Fixed && !IsTail(col, row)) + { + var index = Columns.IndexOf(col) + 1; + ret = index < Columns.Count && Columns[index].Fixed == false; + } + return ret; + + } + private bool IsFirstColumn(ITableColumn col, TableRowContext row) + { + + var ret = false; + if (col.Fixed && IsTail(col, row)) + { + // 查找前一列是否固定 + var index = Columns.IndexOf(col) - 1; + if (index > 0) + { + ret = !Columns[index].Fixed; + } + } + return ret; + } + + #endregion + + #region js + protected override Task InvokeInitAsync() => InvokeVoidAsync("init", Id, Interop, new { Method = nameof(TriggerStateChanged) }); private Task OnColumnVisibleChanged(string name, bool visible) @@ -143,11 +295,15 @@ public partial class VariableRuntimeInfo [Inject] private IOptions? WebsiteOption { get; set; } public bool Disposed { get; set; } - protected override ValueTask DisposeAsync(bool disposing) + protected override async ValueTask DisposeAsync(bool disposing) { + Disposed = true; VariableRuntimeDispatchService.UnSubscribe(Refresh); - return base.DisposeAsync(disposing); + + await Module.InvokeVoidAsync("dispose", Id); + + await base.DisposeAsync(disposing); } protected override void OnInitialized() diff --git a/src/Gateway/ThingsGateway.Gateway.Razor/Pages/GatewayMonitorPage/Variable/VariableRuntimeInfo.razor.js b/src/Gateway/ThingsGateway.Gateway.Razor/Pages/GatewayMonitorPage/Variable/VariableRuntimeInfo.razor.js index b9a17a29e..d459a22f3 100644 --- a/src/Gateway/ThingsGateway.Gateway.Razor/Pages/GatewayMonitorPage/Variable/VariableRuntimeInfo.razor.js +++ b/src/Gateway/ThingsGateway.Gateway.Razor/Pages/GatewayMonitorPage/Variable/VariableRuntimeInfo.razor.js @@ -1,11 +1,16 @@ -export function init(id, invoke, options) { +let handlers = {}; + +export function init(id, invoke, options) { //function getCellByClass(row, className) { // // 直接用 querySelector 精确查找 // return row.querySelector(`td.${className}`); //} - var variableHandler = setInterval(async () => { var admintable = document.getElementById(id); + if (!admintable) { + clearInterval(variableHandler); + return; + } var tables = admintable.getElementsByTagName('table'); @@ -16,10 +21,13 @@ var table = tables[tables.length - 1]; if (!table) { - clearInterval(variableHandler) + clearInterval(variableHandler); return; } + var { method } = options; + + if (!invoke) return; var valss = await invoke.invokeMethodAsync(method); if (valss == null) return; for (let rowIndex = 0; rowIndex < valss.length; rowIndex++) { @@ -48,14 +56,21 @@ var tooltipSpan = cell.querySelector('.bb-tooltip'); if (tooltipSpan) { - tooltipSpan.innerText = cellValue ?? ''; // 更新显示文字 - tooltipSpan.setAttribute('data-bs-original-title', cellValue ?? ''); // 同步 tooltip 提示 + if (tooltipSpan.innerText != cellValue) { + + tooltipSpan.innerText = cellValue ?? ''; // 更新显示文字 + tooltipSpan.setAttribute('data-bs-original-title', cellValue ?? ''); // 同步 tooltip 提示 + + } continue; } else { - cellDiv.innerText = cellValue ?? ''; + if (cellDiv.innerText != cellValue) { + + cellDiv.innerText = cellValue ?? ''; + } } } @@ -93,5 +108,14 @@ } , 500) //1000ms刷新一次 -} + handlers[id] = { variableHandler, invoke }; +} +export function dispose(id) { + const handler = handlers[id]; + if (handler) { + clearInterval(handler.timer); + handler.invoke = null; + delete handlers[id]; + } +} diff --git a/src/Plugin/ThingsGateway.Foundation.Modbus/Slave/ModbusSlave.cs b/src/Plugin/ThingsGateway.Foundation.Modbus/Slave/ModbusSlave.cs index 0e221aeb5..4fc1c9349 100644 --- a/src/Plugin/ThingsGateway.Foundation.Modbus/Slave/ModbusSlave.cs +++ b/src/Plugin/ThingsGateway.Foundation.Modbus/Slave/ModbusSlave.cs @@ -27,22 +27,22 @@ public class ModbusSlave : DeviceBase, IModbusAddress /// /// 继电器 /// - private NonBlockingDictionary ModbusServer01ByteBlocks = new(); + private ConcurrentDictionary ModbusServer01ByteBlocks = new(); /// /// 开关输入 /// - private NonBlockingDictionary ModbusServer02ByteBlocks = new(); + private ConcurrentDictionary ModbusServer02ByteBlocks = new(); /// /// 输入寄存器 /// - private NonBlockingDictionary ModbusServer03ByteBlocks = new(); + private ConcurrentDictionary ModbusServer03ByteBlocks = new(); /// /// 保持寄存器 /// - private NonBlockingDictionary ModbusServer04ByteBlocks = new(); + private ConcurrentDictionary ModbusServer04ByteBlocks = new(); /// public override void InitChannel(IChannel channel, ILog? deviceLog = null) @@ -157,7 +157,7 @@ public class ModbusSlave : DeviceBase, IModbusAddress { return PackHelper.LoadSourceRead(this, deviceVariables, maxPack, defaultIntervalTime); } - + /// protected override Task DisposeAsync(bool disposing) { @@ -187,8 +187,42 @@ public class ModbusSlave : DeviceBase, IModbusAddress /// private void Init(ModbusRequest mAddress) { - //自动扩容 - ModbusServer01ByteBlocks.GetOrAdd(mAddress.Station, a => + if (ModbusServer01ByteBlocks.ContainsKey(mAddress.Station)) + return; + else + ModbusServer01ByteBlocks.GetOrAdd(mAddress.Station, a => + { + var bytes = new ByteBlock(256, + (c) => + { + var data = ArrayPool.Shared.Rent(c); + for (int i = 0; i < data.Length; i++) + { + data[i] = 0; + } + return data; + }, + (m) => + { + if (MemoryMarshal.TryGetArray((ReadOnlyMemory)m, out var result)) + { + ArrayPool.Shared.Return(result.Array); + } + } + ); + bytes.SetLength(256); + for (int i = 0; i < bytes.Length; i++) + { + bytes.WriteByte(0); + } + bytes.Position = 0; + return bytes; + }); + + if (ModbusServer02ByteBlocks.ContainsKey(mAddress.Station)) + return; + else + ModbusServer02ByteBlocks.GetOrAdd(mAddress.Station, a => { var bytes = new ByteBlock(256, (c) => @@ -216,7 +250,11 @@ public class ModbusSlave : DeviceBase, IModbusAddress bytes.Position = 0; return bytes; }); - ModbusServer02ByteBlocks.GetOrAdd(mAddress.Station, a => + + if (ModbusServer03ByteBlocks.ContainsKey(mAddress.Station)) + return; + else + ModbusServer03ByteBlocks.GetOrAdd(mAddress.Station, a => { var bytes = new ByteBlock(256, (c) => @@ -244,35 +282,11 @@ public class ModbusSlave : DeviceBase, IModbusAddress bytes.Position = 0; return bytes; }); - ModbusServer03ByteBlocks.GetOrAdd(mAddress.Station, a => - { - var bytes = new ByteBlock(256, - (c) => - { - var data = ArrayPool.Shared.Rent(c); - for (int i = 0; i < data.Length; i++) - { - data[i] = 0; - } - return data; - }, - (m) => - { - if (MemoryMarshal.TryGetArray((ReadOnlyMemory)m, out var result)) - { - ArrayPool.Shared.Return(result.Array); - } - } - ); - bytes.SetLength(256); - for (int i = 0; i < bytes.Length; i++) - { - bytes.WriteByte(0); - } - bytes.Position = 0; - return bytes; - }); - ModbusServer04ByteBlocks.GetOrAdd(mAddress.Station, a => + + if (ModbusServer04ByteBlocks.ContainsKey(mAddress.Station)) + return; + else + ModbusServer04ByteBlocks.GetOrAdd(mAddress.Station, a => { var bytes = new ByteBlock(256, (c) => @@ -337,10 +351,11 @@ public class ModbusSlave : DeviceBase, IModbusAddress } Init(mAddress); } - var ModbusServer01ByteBlock = ModbusServer01ByteBlocks[mAddress.Station]; - var ModbusServer02ByteBlock = ModbusServer02ByteBlocks[mAddress.Station]; - var ModbusServer03ByteBlock = ModbusServer03ByteBlocks[mAddress.Station]; - var ModbusServer04ByteBlock = ModbusServer04ByteBlocks[mAddress.Station]; + + ModbusServer01ByteBlocks.TryGetValue(mAddress.Station,out var ModbusServer01ByteBlock); + ModbusServer02ByteBlocks.TryGetValue(mAddress.Station,out var ModbusServer02ByteBlock); + ModbusServer03ByteBlocks.TryGetValue(mAddress.Station,out var ModbusServer03ByteBlock); + ModbusServer04ByteBlocks.TryGetValue(mAddress.Station,out var ModbusServer04ByteBlock); if (read) { using (new ReadLock(_lockSlim)) @@ -516,12 +531,12 @@ public class ModbusSlave : DeviceBase, IModbusAddress return new OperResult(ex); } } - protected override Task ChannelReceived(IClientChannel client, ReceivedDataEventArgs e, bool last) + protected override ValueTask ChannelReceived(IClientChannel client, ReceivedDataEventArgs e, bool last) { return HandleChannelReceivedAsync(client, e, last); } - private async Task HandleChannelReceivedAsync(IClientChannel client, ReceivedDataEventArgs e, bool last) + private async ValueTask HandleChannelReceivedAsync(IClientChannel client, ReceivedDataEventArgs e, bool last) { if (!TryParseRequest(e.RequestInfo, out var modbusRequest, out var sequences, out var modbusRtu)) return;