feat: 网关监控页面树节点js更新状态

perf: 优化变量页面刷新性能
perf: 优化热点方法异步性能
This commit is contained in:
2248356998 qq.com
2025-10-17 00:41:47 +08:00
parent aa1ce08c02
commit 3461f34240
22 changed files with 1037 additions and 247 deletions

View File

@@ -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

View File

@@ -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<TimerCallback>(target);
func!(timer.State);
if (timer.TimerCallbackCachedDelegate == null)
{
timer.TimerCallbackCachedDelegate = timer.Method.As<TimerCallback>(target);
}
//var func = timer.Method.As<TimerCallback>(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<Func<Object?, ValueTask>>(target);
var task = func!(timer.State);
if (timer.ValueTaskCachedDelegate == null)
{
timer.ValueTaskCachedDelegate = timer.Method.As<Func<Object?, ValueTask>>(target);
}
//var func = timer.Method.As<Func<Object?, ValueTask>>(target);
var task = timer.ValueTaskCachedDelegate!(timer.State);
if (!task.IsCompleted)
await task.ConfigureAwait(false);
}
else
#endif
{
var func = timer.Method.As<Func<Object?, Task>>(target);
var task = func!(timer.State);
if (timer.TaskCachedDelegate == null)
{
timer.TaskCachedDelegate = timer.Method.As<Func<Object?, Task>>(target);
}
//var func = timer.Method.As<Func<Object?, Task>>(target);
var task = timer.TaskCachedDelegate!(timer.State);
if (!task.IsCompleted)
await task.ConfigureAwait(false);
}

View File

@@ -542,6 +542,12 @@ public class TimerX : ITimer, ITimerx, IDisposable
}
}
public Func<object?, Task>? TaskCachedDelegate { get; internal set; }
#if NET6_0_OR_GREATER
public Func<object?, ValueTask>? ValueTaskCachedDelegate { get; internal set; }
#endif
public TimerCallback? TimerCallbackCachedDelegate { get; internal set; }
private static void CopyNow(Object? state) => _Now = TimerScheduler.Default.GetNow();
#endregion

View File

@@ -1,9 +1,9 @@
<Project>
<PropertyGroup>
<PluginVersion>10.11.109</PluginVersion>
<ProPluginVersion>10.11.109</ProPluginVersion>
<DefaultVersion>10.11.109</DefaultVersion>
<PluginVersion>10.11.110</PluginVersion>
<ProPluginVersion>10.11.110</ProPluginVersion>
<DefaultVersion>10.11.110</DefaultVersion>
<AuthenticationVersion>10.11.6</AuthenticationVersion>
<SourceGeneratorVersion>10.11.6</SourceGeneratorVersion>
<NET8Version>8.0.21</NET8Version>

View File

@@ -26,24 +26,65 @@ public static class ChannelOptionsExtensions
/// <param name="e">接收数据</param>
/// <param name="funcs">事件</param>
/// <returns></returns>
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
/// <param name="clientChannel">通道</param>
/// <param name="funcs">事件</param>
/// <returns></returns>
internal static async Task OnChannelEvent(this IClientChannel clientChannel, ChannelEventHandler funcs)
internal static async ValueTask OnChannelEvent(this IClientChannel clientChannel, ChannelEventHandler funcs)
{
try
{

View File

@@ -65,7 +65,7 @@ public interface IChannel : ISetupConfigObject, IDisposable, IClosableClient, IC
/// <summary>
/// 接收事件回调类
/// </summary>
public class ChannelReceivedEventHandler : List<Func<IClientChannel, ReceivedDataEventArgs, bool, Task>>
public class ChannelReceivedEventHandler : List<Func<IClientChannel, ReceivedDataEventArgs, bool, ValueTask>>
{
}

View File

@@ -131,10 +131,10 @@ public class OtherChannel : SetupConfigObject, IClientChannel
m_dataHandlingAdapter = adapter;
}
private Task PrivateHandleReceivedData(ReadOnlyMemory<byte> byteBlock, IRequestInfo requestInfo)
private async Task PrivateHandleReceivedData(ReadOnlyMemory<byte> byteBlock, IRequestInfo requestInfo)
{
LastReceivedTime = DateTime.Now;
return this.OnChannelReceivedEvent(new ReceivedDataEventArgs(byteBlock, requestInfo), ChannelReceived);
await this.OnChannelReceivedEvent(new ReceivedDataEventArgs(byteBlock, requestInfo), ChannelReceived).ConfigureAwait(false);
}
/// <summary>

View File

@@ -308,7 +308,7 @@ public abstract class DeviceBase : AsyncAndSyncDisposableObject, IDevice
/// <summary>
/// 接收,非主动发送的情况,重写实现非主从并发通讯协议,如果通道存在其他设备并且不希望其他设备处理时,设置<see cref="TouchSocketEventArgs.Handled"/> 为true
/// </summary>
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;
/// <inheritdoc/>

View File

@@ -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);
}
}
}

View File

@@ -93,7 +93,8 @@ public class ScheduledAsyncTask : DisposeBase, IScheduledTask, IScheduledIntInte
{
if (!Check() && IntervalMS > 8)
{
SetNext(next);
int nextValue = next;
SetNext(nextValue);
}
}
}

View File

@@ -80,7 +80,8 @@ public class ScheduledSyncTask : DisposeBase, IScheduledTask, IScheduledIntInter
{
if (!Check() && IntervalMS > 8)
{
SetNext(next);
int nextValue = next;
SetNext(nextValue);
}
}
}

View File

@@ -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<ReadOnlyMemory<byte>> 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<ReadOnlyMemory<byte>> _readResult;
private int _readErrorCount;
private ValueTask<CancellationToken> _readerLockTask;
private ValueTask<OperResult<ReadOnlyMemory<byte>>> _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<ReadOnlyMemory<byte>> 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

View File

@@ -147,43 +147,151 @@ public abstract class CollectFoundationBase : CollectBase
return;
}
//protected override async ValueTask<OperResult<ReadOnlyMemory<byte>>> ReadSourceAsync(VariableSourceRead variableSourceRead, CancellationToken cancellationToken)
//{
// try
// {
// if (cancellationToken.IsCancellationRequested)
// return new(new OperationCanceledException());
// // 从协议读取数据
// OperResult<ReadOnlyMemory<byte>> 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<ReadOnlyMemory<byte>>(prase);
// }
// // 返回读取结果
// return read;
// }
// catch (Exception ex)
// {
// // 捕获异常并返回失败结果
// return new OperResult<ReadOnlyMemory<byte>>(ex);
// }
//}
/// <summary>
/// 采集驱动读取,读取成功后直接赋值变量,失败不做处理,注意非通用设备需重写
/// </summary>
protected override async ValueTask<OperResult<ReadOnlyMemory<byte>>> ReadSourceAsync(VariableSourceRead variableSourceRead, CancellationToken cancellationToken)
protected override ValueTask<OperResult<ReadOnlyMemory<byte>>> ReadSourceAsync(VariableSourceRead variableSourceRead, CancellationToken cancellationToken)
{
try
if (cancellationToken.IsCancellationRequested)
return new ValueTask<OperResult<ReadOnlyMemory<byte>>>( new OperResult<ReadOnlyMemory<byte>>(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<ReadOnlyMemory<byte>> _result;
private ValueTask<OperResult<ReadOnlyMemory<byte>>> _readTask;
public ReadSourceStateMachine(CollectFoundationBase owner, VariableSourceRead variableSourceRead, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
return new(new OperationCanceledException());
// 从协议读取数据
OperResult<ReadOnlyMemory<byte>> 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<ReadOnlyMemory<byte>>(prase);
}
// 返回读取结果
return read;
_owner = owner;
_variableSourceRead = variableSourceRead;
_cancellationToken = cancellationToken;
_result = default;
State = 0;
}
catch (Exception ex)
public int State { get; private set; }
public ValueTask<OperResult<ReadOnlyMemory<byte>>> MoveNextAsync()
{
// 捕获异常并返回失败结果
return new OperResult<ReadOnlyMemory<byte>>(ex);
try
{
switch (State)
{
case 0:
// 异步读取
if (_cancellationToken.IsCancellationRequested)
{
_result = new OperResult<ReadOnlyMemory<byte>>(new OperationCanceledException());
return new ValueTask<OperResult<ReadOnlyMemory<byte>>>(_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<OperResult<ReadOnlyMemory<byte>>>(new OperResult<ReadOnlyMemory<byte>>(parsedResult));
}
return new ValueTask<OperResult<ReadOnlyMemory<byte>>>(_result);
case 2:
// 完成任务后,解析内容
_result = _readTask.Result;
if (_result.IsSuccess)
{
var parsedResult = _variableSourceRead.VariableRuntimes.PraseStructContent(_owner.FoundationDevice, _result.Content.Span, false);
return new ValueTask<OperResult<ReadOnlyMemory<byte>>>(new OperResult<ReadOnlyMemory<byte>>(parsedResult));
}
return new ValueTask<OperResult<ReadOnlyMemory<byte>>>(_result);
default:
throw new InvalidOperationException("Unexpected state.");
}
}
catch (Exception ex)
{
return new ValueTask<OperResult<ReadOnlyMemory<byte>>>(new OperResult<ReadOnlyMemory<byte>>(ex));
}
}
private async ValueTask<OperResult<ReadOnlyMemory<byte>>> Awaited(ValueTask<OperResult<ReadOnlyMemory<byte>>> vt)
{
try
{
await vt.ConfigureAwait(false);
return await MoveNextAsync().ConfigureAwait(false);
}
catch (Exception ex)
{
return new OperResult<ReadOnlyMemory<byte>>(ex);
}
}
}

View File

@@ -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
<div class="listtree-view">
<div class="d-flex align-items-center">
@@ -13,7 +14,7 @@
<ContextMenuZone>
<TreeView TItem="ChannelDeviceTreeItem" Items="Items" ShowIcon="false" ShowSearch IsAccordion=false IsVirtualize="true" OnTreeItemClick="OnTreeItemClick" OnSearchAsync="OnClickSearch" ModelEqualityComparer=ModelEqualityComparer ShowToolbar="true" >
<TreeView Id=@Id TItem="ChannelDeviceTreeItem" Items="Items" ShowIcon="false" ShowSearch IsAccordion=false IsVirtualize="true" OnTreeItemClick="OnTreeItemClick" OnSearchAsync="OnClickSearch" ModelEqualityComparer=ModelEqualityComparer ShowToolbar="true" >
<ToolbarTemplate>
<div class="tree-node-toolbar-edit" @onclick:preventDefault @onclick:stopPropagation>
@@ -137,6 +138,6 @@
@code {
RenderFragment<ChannelDeviceTreeItem> RenderTreeItem = (item) =>
@<span class=@(GetClass(item))>@item.ToString()</span> ;
@<span class=@(GetClass(item)) id=@(GetId(item))>@item.ToString()</span> ;
}

View File

@@ -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<MouseEventArgs>(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<MouseEventArgs>(this, async e =>
{
await ChannelDeviceChanged.Invoke(Value);
}
await InvokeAsync(StateHasChanged);
}
private static ChannelDeviceTreeItem GetValue(ChannelDeviceTreeItem channelDeviceTreeItem)
@@ -1595,10 +1578,16 @@ EventCallback.Factory.Create<MouseEventArgs>(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<MouseEventArgs>(this, async e =>
return Task.CompletedTask;
}
#region js
protected override Task InvokeInitAsync() => InvokeVoidAsync("init", Id, Interop, new { Method = nameof(TriggerStateChanged) });
[JSInvokable]
public List<string> TriggerStateChanged(List<string> jsstring)
{
List<string> 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
}

View File

@@ -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];
}
}

View File

@@ -141,4 +141,77 @@ public class ChannelDeviceTreeItem : IEqualityComparer<ChannelDeviceTreeItem>
{
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<char> span = jsString.AsSpan();
Span<Range> 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
};
}
}

View File

@@ -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<VariableRuntime, object?>(model, fieldName).Compile();
return GetPropertyValueLambda<VariableRuntime, object?>(fieldName).Compile();
})(model);
return ret;
}
}
/// <summary>
/// 获取属性方法 Lambda 表达式
/// </summary>
/// <typeparam name="TModel"></typeparam>
/// <typeparam name="TResult"></typeparam>
/// <param name="propertyName"></param>
/// <returns></returns>
public static Expression<Func<TModel, TResult>> GetPropertyValueLambda<TModel, TResult>(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<Func<TModel, TResult>> 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<Func<TModel, TResult>>(Expression.Convert(body, typeof(TResult)), parameter);
}
Expression<Func<TModel, TResult>> 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<Func<TModel, TResult>>(Expression.Convert(body!, typeof(TResult)), parameter);
}
}
public static string GetValue(VariableRuntime row, string fieldName)
{

View File

@@ -47,7 +47,7 @@
<TableColumn @bind-Field="@context.Enable" Filterable=true Sortable=true Visible="false" />
<TableColumn Field="@context.ChangeTime" Width=120 ShowTips=true FieldExpression=@(() => context.ChangeTime) Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.CollectTime" Width=120 ShowTips =true FieldExpression=@(() => context.CollectTime) Filterable=true Sortable=true Visible=true />
<TableColumn Field="@context.CollectTime" Width=120 ShowTips=true FieldExpression=@(() => context.CollectTime) Filterable=true Sortable=true Visible=true />
<TableColumn Field="@context.IsOnline" FieldExpression=@(() => context.IsOnline) Filterable=true Sortable=true Visible=true />
<TableColumn Field="@context.LastErrorMessage" ShowTips=true FieldExpression=@(() => context.LastErrorMessage) Filterable=true Sortable=true Visible=false />
@@ -93,7 +93,24 @@
<RowContentTemplate Context="context">
<VariableRow RowContent="@context" ColumnsFunc="ColumnsFunc"></VariableRow>
@foreach (var col in context.Columns)
{
<td class="@GetFixedCellClassString(col, context)" style="@GetFixedCellStyleString(col, context)" @key=col>
<div class="@GetCellClassString(col, false, false)" @key=col>
@if (col.GetShowTips())
{
<Tooltip @key=col Title="@VariableModelUtils.GetValue(context.Row, col.GetFieldName())" class="text-truncate d-block">
@VariableModelUtils.GetValue(context.Row, col.GetFieldName())
</Tooltip>
}
else
{
@VariableModelUtils.GetValue(context.Row, col.GetFieldName())
}
</div>
</td>
}
</RowContentTemplate>

View File

@@ -60,11 +60,163 @@ public partial class VariableRuntimeInfo
[NotNull]
public ToastService? ToastService { get; set; }
#region js
private List<ITableColumn> ColumnsFunc()
#region row
/// <summary>
/// 获得指定列头固定列样式
/// </summary>
protected string? GetFixedCellStyleString(ITableColumn col, TableRowContext<VariableRuntime> 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<VariableRuntime> 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<VariableRuntime> 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<VariableRuntime> row)
{
var middle = Math.Floor(row.Columns.Count() * 1.0 / 2);
var index = Columns.IndexOf(col);
return middle < index;
}
/// <summary>
/// 获得 Cell 文字样式
/// </summary>
/// <param name="col"></param>
/// <param name="hasChildren"></param>
/// <param name="inCell"></param>
/// <returns></returns>
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;
/// <summary>
/// 获得指定列头固定列样式
/// </summary>
protected string? GetFixedCellClassString(ITableColumn col, TableRowContext<VariableRuntime> 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<ITableColumn> Columns=> table?.Columns;
private bool IsLastColumn(ITableColumn col, TableRowContext<VariableRuntime> 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<VariableRuntime> 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<WebsiteOptions>? 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()

View File

@@ -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];
}
}

View File

@@ -27,22 +27,22 @@ public class ModbusSlave : DeviceBase, IModbusAddress
/// <summary>
/// 继电器
/// </summary>
private NonBlockingDictionary<byte, ByteBlock> ModbusServer01ByteBlocks = new();
private ConcurrentDictionary<int, ByteBlock> ModbusServer01ByteBlocks = new();
/// <summary>
/// 开关输入
/// </summary>
private NonBlockingDictionary<byte, ByteBlock> ModbusServer02ByteBlocks = new();
private ConcurrentDictionary<int, ByteBlock> ModbusServer02ByteBlocks = new();
/// <summary>
/// 输入寄存器
/// </summary>
private NonBlockingDictionary<byte, ByteBlock> ModbusServer03ByteBlocks = new();
private ConcurrentDictionary<int, ByteBlock> ModbusServer03ByteBlocks = new();
/// <summary>
/// 保持寄存器
/// </summary>
private NonBlockingDictionary<byte, ByteBlock> ModbusServer04ByteBlocks = new();
private ConcurrentDictionary<int, ByteBlock> ModbusServer04ByteBlocks = new();
/// <inheritdoc/>
public override void InitChannel(IChannel channel, ILog? deviceLog = null)
@@ -187,8 +187,42 @@ public class ModbusSlave : DeviceBase, IModbusAddress
/// <inheritdoc/>
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<byte>.Shared.Rent(c);
for (int i = 0; i < data.Length; i++)
{
data[i] = 0;
}
return data;
},
(m) =>
{
if (MemoryMarshal.TryGetArray((ReadOnlyMemory<byte>)m, out var result))
{
ArrayPool<byte>.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<byte>.Shared.Rent(c);
for (int i = 0; i < data.Length; i++)
{
data[i] = 0;
}
return data;
},
(m) =>
{
if (MemoryMarshal.TryGetArray((ReadOnlyMemory<byte>)m, out var result))
{
ArrayPool<byte>.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;