mirror of
				https://gitee.com/ThingsGateway/ThingsGateway.git
				synced 2025-11-04 17:43:58 +08:00 
			
		
		
		
	Compare commits
	
		
			13 Commits
		
	
	
		
			10.11.26.0
			...
			10.11.39.0
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					42adee9980 | ||
| 
						 | 
					427a7404bc | ||
| 
						 | 
					3658199e0a | ||
| 
						 | 
					82eedee50a | ||
| 
						 | 
					6a18fc3e06 | ||
| 
						 | 
					c37e314ed6 | ||
| 
						 | 
					a937a85d90 | ||
| 
						 | 
					35dd4ae9d3 | ||
| 
						 | 
					0b829ac85c | ||
| 
						 | 
					aa247422d2 | ||
| 
						 | 
					2e00e8c135 | ||
| 
						 | 
					34dd2cf0a7 | ||
| 
						 | 
					8404e20c5e | 
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -364,8 +364,5 @@ FodyWeavers.xsd
 | 
			
		||||
 | 
			
		||||
/src/*Pro*/
 | 
			
		||||
/src/*Pro*
 | 
			
		||||
/src/**/*Pro*
 | 
			
		||||
/src/*pro*
 | 
			
		||||
/src/*pro*/
 | 
			
		||||
/src/ThingsGateway.Server/Configuration/GiteeOAuthSettings.json
 | 
			
		||||
/src/.idea/
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@
 | 
			
		||||
 | 
			
		||||
    <Table TItem="TItem" IsBordered="true" IsStriped="true" TableSize="TableSize.Compact" IsMultipleSelect="IsMultipleSelect" @ref="Instance" SearchTemplate="SearchTemplate"
 | 
			
		||||
           DataService="DataService" CreateItemCallback="CreateItemCallback!"
 | 
			
		||||
           IsPagination="IsPagination" PageItemsSource="PageItemsSource" IsFixedHeader="IsFixedHeader" IndentSize=24 RowHeight=RowHeight ShowSearchText="ShowSearchText" ShowSearchButton="ShowSearchButton" BeforeShowEditDialogCallback="BeforeShowEditDialogCallback!"
 | 
			
		||||
           IsPagination="IsPagination" PageItemsSource="PageItemsSource" IsFixedHeader="IsFixedHeader" IndentSize=24 RowHeight=RowHeight ShowSearchText="ShowSearchText" ShowSearchButton="ShowSearchButton" DisableEditButtonCallback="DisableEditButtonCallback" DisableDeleteButtonCallback="DisableDeleteButtonCallback" BeforeShowEditDialogCallback=" BeforeShowEditDialogCallback!"
 | 
			
		||||
           IsTree="IsTree" OnTreeExpand="OnTreeExpand!" TreeNodeConverter="TreeNodeConverter!" TreeIcon="fa-solid fa-circle-chevron-right" TreeExpandIcon="fa-solid fa-circle-chevron-right fa-rotate-90" IsAutoQueryFirstRender=IsAutoQueryFirstRender
 | 
			
		||||
           ShowDefaultButtons="ShowDefaultButtons" ShowAdvancedSearch="ShowAdvancedSearch" ShowResetButton=ShowResetButton
 | 
			
		||||
           ShowEmpty="ShowEmpty" EmptyText="@EmptyText" EmptyImage="@($"{WebsiteConst.DefaultResourceUrl}images/empty.svg")" SortString="@SortString" EditDialogSize="EditDialogSize"
 | 
			
		||||
@@ -14,7 +14,7 @@
 | 
			
		||||
           ShowSkeleton="true" ShowLoading="ShowLoading" ShowSearch="ShowSearch" SearchModel=@SearchModel ShowLineNo
 | 
			
		||||
           SearchMode=SearchMode ShowExportPdfButton=ShowExportPdfButton ExportButtonText=@ExportButtonText
 | 
			
		||||
           ShowExportButton=@ShowExportButton Items=Items ClickToSelect=ClickToSelect ScrollMode=ScrollMode
 | 
			
		||||
           ShowExportCsvButton=@ShowExportCsvButton SelectedRowsChanged=SelectedRowsChanged ShowCardView=ShowCardView
 | 
			
		||||
           ShowExportCsvButton=@ShowExportCsvButton ShowCardView=ShowCardView
 | 
			
		||||
           FixedExtendButtonsColumn=FixedExtendButtonsColumn FixedMultipleColumn=FixedMultipleColumn FixedDetailRowHeaderColumn=FixedDetailRowHeaderColumn FixedLineNoColumn=FixedLineNoColumn
 | 
			
		||||
           IsAutoRefresh=IsAutoRefresh AutoRefreshInterval=AutoRefreshInterval
 | 
			
		||||
           AllowDragColumn=@AllowDragColumn Height=@Height ShowRefresh=ShowRefresh
 | 
			
		||||
@@ -29,7 +29,7 @@
 | 
			
		||||
           ShowMultiFilterHeader=ShowMultiFilterHeader
 | 
			
		||||
           ShowFilterHeader=ShowFilterHeader
 | 
			
		||||
           ShowColumnList=ShowColumnList ExtendButtonColumnWidth="@ExtendButtonColumnWidth"
 | 
			
		||||
           CustomerSearchModel="CustomerSearchModel" SelectedRows="SelectedRows" ModelEqualityComparer="ModelEqualityComparer!"
 | 
			
		||||
           CustomerSearchModel="CustomerSearchModel" ModelEqualityComparer="ModelEqualityComparer!"
 | 
			
		||||
           ShowExtendEditButtonCallback="ShowExtendEditButtonCallback!" ShowExtendDeleteButtonCallback="ShowExtendDeleteButtonCallback!"
 | 
			
		||||
           DisableExtendEditButton="DisableExtendEditButton!" DisableExtendDeleteButton="DisableExtendDeleteButton!"
 | 
			
		||||
           DisableExtendEditButtonCallback="DisableExtendEditButtonCallback!" DisableExtendDeleteButtonCallback="DisableExtendDeleteButtonCallback!"
 | 
			
		||||
 
 | 
			
		||||
@@ -210,14 +210,6 @@ public partial class AdminTable<TItem> where TItem : class, new()
 | 
			
		||||
    [Parameter]
 | 
			
		||||
    public RenderFragment<TItem>? SearchTemplate { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc cref="Table{TItem}.SelectedRows"/>
 | 
			
		||||
    [Parameter]
 | 
			
		||||
    public List<TItem>? SelectedRows { get; set; } = new List<TItem>();
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc cref="Table{TItem}.SelectedRowsChanged"/>
 | 
			
		||||
    [Parameter]
 | 
			
		||||
    public EventCallback<List<TItem>> SelectedRowsChanged { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc cref="Table{TItem}.SetRowClassFormatter"/>
 | 
			
		||||
    [Parameter]
 | 
			
		||||
    public Func<TItem, string?>? SetRowClassFormatter { get; set; }
 | 
			
		||||
@@ -266,6 +258,15 @@ public partial class AdminTable<TItem> where TItem : class, new()
 | 
			
		||||
    [Parameter]
 | 
			
		||||
    public bool ShowExportButton { get; set; } = false;
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc cref="Table{TItem}.DisableEditButtonCallback"/>
 | 
			
		||||
    public Func<List<TItem>, bool> DisableEditButtonCallback { get; set; } = (list) =>
 | 
			
		||||
    list.Count != 1;
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc cref="Table{TItem}.DisableDeleteButtonCallback"/>
 | 
			
		||||
    [Parameter]
 | 
			
		||||
    public Func<List<TItem>, bool> DisableDeleteButtonCallback { get; set; } = (list) =>
 | 
			
		||||
    list.Count <= 0;
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc cref="Table{TItem}.ShowExportCsvButton"/>
 | 
			
		||||
    [Parameter]
 | 
			
		||||
    public bool ShowExportCsvButton { get; set; } = false;
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,7 @@
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
		<PackageReference Include="BootstrapBlazor.TableExport" Version="9.2.6" />
 | 
			
		||||
		<PackageReference Include="Yitter.IdGenerator" Version="1.0.14" />
 | 
			
		||||
		<PackageReference Include="BootstrapBlazor" Version="9.10.0" />
 | 
			
		||||
		<PackageReference Include="BootstrapBlazor" Version="9.10.1" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
using System.Diagnostics;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.NewLife.Log;
 | 
			
		||||
using ThingsGateway.NewLife.Reflection;
 | 
			
		||||
@@ -64,7 +63,10 @@ public class ObjectPool<T> : DisposeBase, IPool<T> where T : notnull
 | 
			
		||||
        // 启动定期清理的定时器
 | 
			
		||||
        StartTimer();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ~ObjectPool()
 | 
			
		||||
    {
 | 
			
		||||
        this.TryDispose();
 | 
			
		||||
    }
 | 
			
		||||
    /// <summary>销毁</summary>
 | 
			
		||||
    /// <param name="disposing"></param>
 | 
			
		||||
    protected override void Dispose(Boolean disposing)
 | 
			
		||||
@@ -73,7 +75,7 @@ public class ObjectPool<T> : DisposeBase, IPool<T> where T : notnull
 | 
			
		||||
 | 
			
		||||
        _timer.TryDispose();
 | 
			
		||||
 | 
			
		||||
        WriteLog($"Dispose {typeof(T).FullName} FreeCount={FreeCount:n0} BusyCount={BusyCount:n0} Total={Total:n0}");
 | 
			
		||||
        WriteLog($"Dispose {typeof(T).FullName} FreeCount={FreeCount:n0} BusyCount={BusyCount:n0}");
 | 
			
		||||
 | 
			
		||||
        Clear();
 | 
			
		||||
    }
 | 
			
		||||
@@ -109,10 +111,6 @@ public class ObjectPool<T> : DisposeBase, IPool<T> where T : notnull
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public virtual T Get()
 | 
			
		||||
    {
 | 
			
		||||
        var sw = Log == null || Log == Logger.Null ? null : Stopwatch.StartNew();
 | 
			
		||||
        Interlocked.Increment(ref _Total);
 | 
			
		||||
 | 
			
		||||
        var success = false;
 | 
			
		||||
        Item? pi = null;
 | 
			
		||||
        do
 | 
			
		||||
        {
 | 
			
		||||
@@ -120,8 +118,6 @@ public class ObjectPool<T> : DisposeBase, IPool<T> where T : notnull
 | 
			
		||||
            if (_free.TryPop(out pi) || _free2.TryDequeue(out pi))
 | 
			
		||||
            {
 | 
			
		||||
                Interlocked.Decrement(ref _FreeCount);
 | 
			
		||||
 | 
			
		||||
                success = true;
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
@@ -147,8 +143,6 @@ public class ObjectPool<T> : DisposeBase, IPool<T> where T : notnull
 | 
			
		||||
                WriteLog("Acquire Create Free={0} Busy={1}", FreeCount, count + 1);
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
                Interlocked.Increment(ref _NewCount);
 | 
			
		||||
                success = false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // 借出时如果不可用,再次借取
 | 
			
		||||
@@ -161,17 +155,6 @@ public class ObjectPool<T> : DisposeBase, IPool<T> where T : notnull
 | 
			
		||||
        _busy.TryAdd(pi.Value, pi);
 | 
			
		||||
 | 
			
		||||
        Interlocked.Increment(ref _BusyCount);
 | 
			
		||||
        if (success) Interlocked.Increment(ref _Success);
 | 
			
		||||
        if (sw != null)
 | 
			
		||||
        {
 | 
			
		||||
            sw.Stop();
 | 
			
		||||
            var ms = sw.Elapsed.TotalMilliseconds;
 | 
			
		||||
 | 
			
		||||
            if (Cost < 0.001)
 | 
			
		||||
                Cost = ms;
 | 
			
		||||
            else
 | 
			
		||||
                Cost = (Cost * 3 + ms) / 4;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return pi.Value;
 | 
			
		||||
    }
 | 
			
		||||
@@ -197,7 +180,6 @@ public class ObjectPool<T> : DisposeBase, IPool<T> where T : notnull
 | 
			
		||||
#if DEBUG
 | 
			
		||||
            WriteLog("Return Error");
 | 
			
		||||
#endif
 | 
			
		||||
            Interlocked.Increment(ref _ReleaseCount);
 | 
			
		||||
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
@@ -207,13 +189,11 @@ public class ObjectPool<T> : DisposeBase, IPool<T> where T : notnull
 | 
			
		||||
        // 是否可用
 | 
			
		||||
        if (!OnReturn(value))
 | 
			
		||||
        {
 | 
			
		||||
            Interlocked.Increment(ref _ReleaseCount);
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (value is DisposeBase db && db.Disposed)
 | 
			
		||||
        {
 | 
			
		||||
            Interlocked.Increment(ref _ReleaseCount);
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -370,39 +350,14 @@ public class ObjectPool<T> : DisposeBase, IPool<T> where T : notnull
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var ncount = _NewCount;
 | 
			
		||||
        var fcount = _ReleaseCount;
 | 
			
		||||
        if (count > 0 || ncount > 0 || fcount > 0)
 | 
			
		||||
        if (count > 0)
 | 
			
		||||
        {
 | 
			
		||||
            Interlocked.Add(ref _NewCount, -ncount);
 | 
			
		||||
            Interlocked.Add(ref _ReleaseCount, -fcount);
 | 
			
		||||
 | 
			
		||||
            var p = Total == 0 ? 0 : (Double)Success / Total;
 | 
			
		||||
 | 
			
		||||
            WriteLog("Release New={6:n0} Release={7:n0} Free={0} Busy={1} 清除过期资源 {2:n0} 项。总请求 {3:n0} 次,命中 {4:p2},平均 {5:n2}us", FreeCount, BusyCount, count, Total, p, Cost * 1000, ncount, fcount);
 | 
			
		||||
            WriteLog("Release New={6:n0} Release={7:n0} Free={0} Busy={1} 清除过期资源 {2:n0} 项。", FreeCount, BusyCount, count);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    #endregion
 | 
			
		||||
 | 
			
		||||
    #region 统计
 | 
			
		||||
    private Int32 _Total;
 | 
			
		||||
    /// <summary>总请求数</summary>
 | 
			
		||||
    public Int32 Total => _Total;
 | 
			
		||||
 | 
			
		||||
    private Int32 _Success;
 | 
			
		||||
    /// <summary>成功数</summary>
 | 
			
		||||
    public Int32 Success => _Success;
 | 
			
		||||
 | 
			
		||||
    /// <summary>新创建数</summary>
 | 
			
		||||
    private Int32 _NewCount;
 | 
			
		||||
 | 
			
		||||
    /// <summary>释放数</summary>
 | 
			
		||||
    private Int32 _ReleaseCount;
 | 
			
		||||
 | 
			
		||||
    /// <summary>平均耗时。单位ms</summary>
 | 
			
		||||
    private Double Cost;
 | 
			
		||||
    #endregion
 | 
			
		||||
 | 
			
		||||
    #region 日志
 | 
			
		||||
    /// <summary>日志</summary>
 | 
			
		||||
    public ILog Log { get; set; } = Logger.Null;
 | 
			
		||||
 
 | 
			
		||||
@@ -5,39 +5,45 @@ namespace ThingsGateway.NewLife;
 | 
			
		||||
 | 
			
		||||
public class ExpiringDictionary<TKey, TValue> : IDisposable
 | 
			
		||||
{
 | 
			
		||||
    private readonly ConcurrentDictionary<TKey, TValue> _dict = new();
 | 
			
		||||
    private ConcurrentDictionary<TKey, TValue> _dict = new();
 | 
			
		||||
    private readonly TimerX _cleanupTimer;
 | 
			
		||||
 | 
			
		||||
    public ExpiringDictionary(int cleanupInterval = 600000)
 | 
			
		||||
    public ExpiringDictionary(int cleanupInterval = 60000)
 | 
			
		||||
    {
 | 
			
		||||
        _cleanupTimer = new TimerX(Clear, null, cleanupInterval, cleanupInterval) { Async = true };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void TryAdd(TKey key, TValue value)
 | 
			
		||||
    {
 | 
			
		||||
        if (_cleanupTimer.Disposed) throw new ObjectDisposedException(nameof(ExpiringDictionary<TKey, TValue>));
 | 
			
		||||
        _dict.TryAdd(key, value);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool TryGetValue(TKey key, out TValue value)
 | 
			
		||||
    {
 | 
			
		||||
        if (_cleanupTimer.Disposed) throw new ObjectDisposedException(nameof(ExpiringDictionary<TKey, TValue>));
 | 
			
		||||
        return _dict.TryGetValue(key, out value);
 | 
			
		||||
    }
 | 
			
		||||
    public TValue GetOrAdd(TKey key, Func<TKey, TValue> func)
 | 
			
		||||
    {
 | 
			
		||||
        if (_cleanupTimer.Disposed) throw new ObjectDisposedException(nameof(ExpiringDictionary<TKey, TValue>));
 | 
			
		||||
        return _dict.GetOrAdd(key, func);
 | 
			
		||||
    }
 | 
			
		||||
    public TValue GetOrAdd(TKey key, TValue value)
 | 
			
		||||
    {
 | 
			
		||||
        if (_cleanupTimer.Disposed) throw new ObjectDisposedException(nameof(ExpiringDictionary<TKey, TValue>));
 | 
			
		||||
        return _dict.GetOrAdd(key, value);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool TryRemove(TKey key) => _dict.TryRemove(key, out _);
 | 
			
		||||
 | 
			
		||||
    public void Clear() => _dict.Clear();
 | 
			
		||||
    public void Clear() => Clear(null);
 | 
			
		||||
 | 
			
		||||
    private void Clear(object? state)
 | 
			
		||||
    {
 | 
			
		||||
        _dict.Clear();
 | 
			
		||||
        var data = _dict;
 | 
			
		||||
        _dict = new();
 | 
			
		||||
        data.Clear();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void Dispose()
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,8 @@
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.NewLife.Log;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.NewLife;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
@@ -50,24 +52,21 @@ public sealed class WaitLock : IDisposable
 | 
			
		||||
    public int CurrentCount => _waiterLock.CurrentCount;
 | 
			
		||||
    public bool Waitting => _waiterLock.CurrentCount < MaxCount;
 | 
			
		||||
 | 
			
		||||
    private object m_lockObj = new();
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 离开锁
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public void Release()
 | 
			
		||||
    {
 | 
			
		||||
        if (DisposedValue) return;
 | 
			
		||||
        lock (m_lockObj)
 | 
			
		||||
        //if (Waitting)
 | 
			
		||||
        {
 | 
			
		||||
            if (Waitting)
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    _waiterLock.Release();
 | 
			
		||||
                }
 | 
			
		||||
                catch (SemaphoreFullException)
 | 
			
		||||
                {
 | 
			
		||||
                }
 | 
			
		||||
                _waiterLock.Release();
 | 
			
		||||
            }
 | 
			
		||||
            catch (SemaphoreFullException)
 | 
			
		||||
            {
 | 
			
		||||
                XTrace.WriteException(new Exception($"WaitLock {_name} 释放失败,当前信号量无需释放"));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -554,10 +554,7 @@ public static class Reflect
 | 
			
		||||
    //}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    private static class DelegateCache<TFunc>
 | 
			
		||||
    {
 | 
			
		||||
        public static readonly ExpiringDictionary<DelegateCacheKey, TFunc> Cache = new();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>把一个方法转为泛型委托,便于快速反射调用</summary>
 | 
			
		||||
    /// <typeparam name="TFunc"></typeparam>
 | 
			
		||||
@@ -580,38 +577,42 @@ public static class Reflect
 | 
			
		||||
        return func;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private readonly struct DelegateCacheKey : IEquatable<DelegateCacheKey>
 | 
			
		||||
    #endregion
 | 
			
		||||
}
 | 
			
		||||
public static class DelegateCache<TFunc>
 | 
			
		||||
{
 | 
			
		||||
    public static readonly ExpiringDictionary<DelegateCacheKey, TFunc> Cache = new();
 | 
			
		||||
}
 | 
			
		||||
public readonly struct DelegateCacheKey : IEquatable<DelegateCacheKey>
 | 
			
		||||
{
 | 
			
		||||
    public readonly MethodInfo Method;
 | 
			
		||||
    public readonly Type FuncType;
 | 
			
		||||
    public readonly object? Target;
 | 
			
		||||
 | 
			
		||||
    public DelegateCacheKey(MethodInfo method, Type funcType, object? target)
 | 
			
		||||
    {
 | 
			
		||||
        public readonly MethodInfo Method;
 | 
			
		||||
        public readonly Type FuncType;
 | 
			
		||||
        public readonly object? Target;
 | 
			
		||||
        Method = method;
 | 
			
		||||
        FuncType = funcType;
 | 
			
		||||
        Target = target;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        public DelegateCacheKey(MethodInfo method, Type funcType, object? target)
 | 
			
		||||
    public bool Equals(DelegateCacheKey other) =>
 | 
			
		||||
        Method.Equals(other.Method)
 | 
			
		||||
        && FuncType.Equals(other.FuncType)
 | 
			
		||||
        && ReferenceEquals(Target, other.Target);
 | 
			
		||||
 | 
			
		||||
    public override bool Equals(object? obj) =>
 | 
			
		||||
        obj is DelegateCacheKey other && Equals(other);
 | 
			
		||||
 | 
			
		||||
    public override int GetHashCode()
 | 
			
		||||
    {
 | 
			
		||||
        unchecked
 | 
			
		||||
        {
 | 
			
		||||
            Method = method;
 | 
			
		||||
            FuncType = funcType;
 | 
			
		||||
            Target = target;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public bool Equals(DelegateCacheKey other) =>
 | 
			
		||||
            Method.Equals(other.Method)
 | 
			
		||||
            && FuncType.Equals(other.FuncType)
 | 
			
		||||
            && ReferenceEquals(Target, other.Target);
 | 
			
		||||
 | 
			
		||||
        public override bool Equals(object? obj) =>
 | 
			
		||||
            obj is DelegateCacheKey other && Equals(other);
 | 
			
		||||
 | 
			
		||||
        public override int GetHashCode()
 | 
			
		||||
        {
 | 
			
		||||
            unchecked
 | 
			
		||||
            {
 | 
			
		||||
                int hash = Method.GetHashCode();
 | 
			
		||||
                hash = (hash * 397) ^ FuncType.GetHashCode();
 | 
			
		||||
                if (Target != null)
 | 
			
		||||
                    hash = (hash * 397) ^ RuntimeHelpers.GetHashCode(Target); // 不受对象重写 GetHashCode 影响
 | 
			
		||||
                return hash;
 | 
			
		||||
            }
 | 
			
		||||
            int hash = Method.GetHashCode();
 | 
			
		||||
            hash = (hash * 397) ^ FuncType.GetHashCode();
 | 
			
		||||
            if (Target != null)
 | 
			
		||||
                hash = (hash * 397) ^ RuntimeHelpers.GetHashCode(Target); // 不受对象重写 GetHashCode 影响
 | 
			
		||||
            return hash;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    #endregion
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
using System.Reflection;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.NewLife.Log;
 | 
			
		||||
using ThingsGateway.NewLife.Reflection;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.NewLife.Threading;
 | 
			
		||||
 | 
			
		||||
@@ -389,6 +390,13 @@ public class TimerX : ITimer, ITimerx, IDisposable
 | 
			
		||||
 | 
			
		||||
        // 释放非托管资源
 | 
			
		||||
        Scheduler?.Remove(this, disposing ? "Dispose" : "GC");
 | 
			
		||||
 | 
			
		||||
        DelegateCache<TimerCallback>.Cache.Clear();
 | 
			
		||||
#if NET6_0_OR_GREATER
 | 
			
		||||
        DelegateCache<Func<Object?, ValueTask>>.Cache.Clear();
 | 
			
		||||
#endif
 | 
			
		||||
        DelegateCache<Func<Object?, Task>>.Cache.Clear();
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
#if NET6_0_OR_GREATER
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@
 | 
			
		||||
       ShowSkeleton="true" ShowLoading="ShowLoading" ShowSearch="ShowSearch" SearchModel=@SearchModel ShowLineNo
 | 
			
		||||
       SearchMode=SearchMode ShowExportPdfButton=ShowExportPdfButton  
 | 
			
		||||
       ShowExportButton=@ShowExportButton Items=Items ClickToSelect=ClickToSelect ScrollMode=ScrollMode
 | 
			
		||||
       ShowExportCsvButton=@ShowExportCsvButton SelectedRowsChanged=SelectedRowsChanged ShowCardView=ShowCardView
 | 
			
		||||
       ShowExportCsvButton=@ShowExportCsvButton ShowCardView=ShowCardView
 | 
			
		||||
       FixedExtendButtonsColumn IsAutoRefresh=IsAutoRefresh AutoRefreshInterval=AutoRefreshInterval
 | 
			
		||||
       AllowDragColumn=@AllowDragColumn Height=@Height ShowRefresh=ShowRefresh
 | 
			
		||||
       AllowResizing=@AllowResizing ExportButtonDropdownTemplate=ExportButtonDropdownTemplate
 | 
			
		||||
@@ -24,7 +24,7 @@
 | 
			
		||||
       ShowMultiFilterHeader=ShowMultiFilterHeader
 | 
			
		||||
       ShowFilterHeader=ShowFilterHeader
 | 
			
		||||
       ShowColumnList=ShowColumnList ExtendButtonColumnWidth="@ExtendButtonColumnWidth"
 | 
			
		||||
       CustomerSearchModel="CustomerSearchModel" SelectedRows="SelectedRows" ModelEqualityComparer="ModelEqualityComparer!"
 | 
			
		||||
       CustomerSearchModel="CustomerSearchModel" ModelEqualityComparer="ModelEqualityComparer!"
 | 
			
		||||
       ShowExtendEditButtonCallback="ShowExtendEditButtonCallback!" ShowExtendDeleteButtonCallback="ShowExtendDeleteButtonCallback!"
 | 
			
		||||
       DisableExtendEditButton="DisableExtendEditButton!" DisableExtendDeleteButton="DisableExtendDeleteButton!"
 | 
			
		||||
       DisableExtendEditButtonCallback="DisableExtendEditButtonCallback!" DisableExtendDeleteButtonCallback="DisableExtendDeleteButtonCallback!"
 | 
			
		||||
 
 | 
			
		||||
@@ -186,14 +186,6 @@ public partial class DefaultTable<TItem> where TItem : class, new()
 | 
			
		||||
    [Parameter]
 | 
			
		||||
    public RenderFragment<TItem>? SearchTemplate { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc cref="Table{TItem}.SelectedRows"/>
 | 
			
		||||
    [Parameter]
 | 
			
		||||
    public List<TItem>? SelectedRows { get; set; } = new List<TItem>();
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc cref="Table{TItem}.SelectedRowsChanged"/>
 | 
			
		||||
    [Parameter]
 | 
			
		||||
    public EventCallback<List<TItem>> SelectedRowsChanged { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc cref="Table{TItem}.SetRowClassFormatter"/>
 | 
			
		||||
    [Parameter]
 | 
			
		||||
    public Func<TItem, string?>? SetRowClassFormatter { get; set; }
 | 
			
		||||
 
 | 
			
		||||
@@ -24,15 +24,15 @@
 | 
			
		||||
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
		<PackageReference Include="SqlSugarCore.Dm" Version="8.8.2" />
 | 
			
		||||
		<PackageReference Include="SqlSugarCore.Kdbndp" Version="9.3.7.821" />
 | 
			
		||||
		<PackageReference Include="SqlSugarCore.Kdbndp" Version="9.3.7.905" />
 | 
			
		||||
		<PackageReference Include="Microsoft.Data.Sqlite" Version="7.0.20" />
 | 
			
		||||
		<!--<PackageReference Include="Microsoft.Data.Sqlite" Version="$(NET9Version)" />-->
 | 
			
		||||
		<PackageReference Include="MySqlConnector" Version="2.4.0" />
 | 
			
		||||
		<PackageReference Include="Npgsql" Version="9.0.3" />
 | 
			
		||||
		<PackageReference Include="CsvHelper" Version="33.1.0" />
 | 
			
		||||
		<PackageReference Include="TDengine.Connector" Version="3.1.8" />
 | 
			
		||||
		<PackageReference Include="TDengine.Connector" Version="3.1.9" />
 | 
			
		||||
		<PackageReference Include="Oracle.ManagedDataAccess.Core" Version="23.9.1" />
 | 
			
		||||
		<PackageReference Include="Oscar.Data.SqlClient" Version="4.2.23" />
 | 
			
		||||
		<PackageReference Include="Oscar.Data.SqlClient" Version="4.2.25" />
 | 
			
		||||
		<PackageReference Include="System.Data.Common" Version="4.3.0" />
 | 
			
		||||
		<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.1" />
 | 
			
		||||
		<PackageReference Include="System.Reflection.Emit.Lightweight" Version="4.7.0" />
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +1,17 @@
 | 
			
		||||
<Project>
 | 
			
		||||
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<PluginVersion>10.11.26</PluginVersion>
 | 
			
		||||
		<ProPluginVersion>10.11.26</ProPluginVersion>
 | 
			
		||||
		<DefaultVersion>10.11.26</DefaultVersion>
 | 
			
		||||
		<AuthenticationVersion>10.11.3</AuthenticationVersion>
 | 
			
		||||
		<SourceGeneratorVersion>10.11.3</SourceGeneratorVersion>
 | 
			
		||||
		<NET8Version>8.0.19</NET8Version>
 | 
			
		||||
		<NET9Version>9.0.8</NET9Version>
 | 
			
		||||
		<PluginVersion>10.11.39</PluginVersion>
 | 
			
		||||
		<ProPluginVersion>10.11.39</ProPluginVersion>
 | 
			
		||||
		<DefaultVersion>10.11.39</DefaultVersion>
 | 
			
		||||
		<AuthenticationVersion>10.11.5</AuthenticationVersion>
 | 
			
		||||
		<SourceGeneratorVersion>10.11.4</SourceGeneratorVersion>
 | 
			
		||||
		<NET8Version>8.0.20</NET8Version>
 | 
			
		||||
		<NET9Version>9.0.9</NET9Version>
 | 
			
		||||
		<SatelliteResourceLanguages>zh-Hans;en-US</SatelliteResourceLanguages>
 | 
			
		||||
		<IsTrimmable>false</IsTrimmable>
 | 
			
		||||
		<ManagementProPluginVersion>10.11.22</ManagementProPluginVersion>
 | 
			
		||||
		<ManagementPluginVersion>10.11.22</ManagementPluginVersion>
 | 
			
		||||
		<ManagementProPluginVersion>10.11.36</ManagementProPluginVersion>
 | 
			
		||||
		<ManagementPluginVersion>10.11.36</ManagementPluginVersion>
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
 
 | 
			
		||||
@@ -49,12 +49,11 @@ public class DDPTcpSessionClientChannel : TcpSessionClientChannel
 | 
			
		||||
        this.ThrowIfDisposed();
 | 
			
		||||
        this.ThrowIfClientNotConnected();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        if (!await this.OnTcpSending(memory).ConfigureAwait(false)) return;
 | 
			
		||||
 | 
			
		||||
        var transport = this.Transport;
 | 
			
		||||
        var adapter = this.DataHandlingAdapter;
 | 
			
		||||
        var locker = transport.SemaphoreSlimForWriter;
 | 
			
		||||
        var locker = transport.WriteLocker;
 | 
			
		||||
 | 
			
		||||
        await locker.WaitAsync(token).ConfigureAwait(false);
 | 
			
		||||
        try
 | 
			
		||||
@@ -62,7 +61,7 @@ public class DDPTcpSessionClientChannel : TcpSessionClientChannel
 | 
			
		||||
            // 如果数据处理适配器未设置,则使用默认发送方式。
 | 
			
		||||
            if (adapter == null)
 | 
			
		||||
            {
 | 
			
		||||
                await transport.Output.WriteAsync(memory, token).ConfigureAwait(false);
 | 
			
		||||
                await transport.Writer.WriteAsync(memory, token).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
@@ -70,7 +69,7 @@ public class DDPTcpSessionClientChannel : TcpSessionClientChannel
 | 
			
		||||
                var ddpSend = new DDPSend(memory, Id, true);
 | 
			
		||||
                ddpSend.Build(ref byteBlock);
 | 
			
		||||
                var newMemory = byteBlock.Memory;
 | 
			
		||||
                var writer = new PipeBytesWriter(transport.Output);
 | 
			
		||||
                var writer = new PipeBytesWriter(transport.Writer);
 | 
			
		||||
                adapter.SendInput(ref writer, in newMemory);
 | 
			
		||||
                await writer.FlushAsync(token).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
@@ -100,7 +99,7 @@ public class DDPTcpSessionClientChannel : TcpSessionClientChannel
 | 
			
		||||
 | 
			
		||||
        var transport = this.Transport;
 | 
			
		||||
        var adapter = this.DataHandlingAdapter;
 | 
			
		||||
        var locker = transport.SemaphoreSlimForWriter;
 | 
			
		||||
        var locker = transport.WriteLocker;
 | 
			
		||||
 | 
			
		||||
        await locker.WaitAsync(token).ConfigureAwait(false);
 | 
			
		||||
        try
 | 
			
		||||
@@ -113,7 +112,7 @@ public class DDPTcpSessionClientChannel : TcpSessionClientChannel
 | 
			
		||||
            requestInfoBuilder.Build(ref byteBlock);
 | 
			
		||||
            var ddpSend = new DDPSend(byteBlock.Memory, Id, true);
 | 
			
		||||
 | 
			
		||||
            var writer = new PipeBytesWriter(transport.Output);
 | 
			
		||||
            var writer = new PipeBytesWriter(transport.Writer);
 | 
			
		||||
            adapter.SendInput(ref writer, ddpSend);
 | 
			
		||||
            await writer.FlushAsync(token).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -37,9 +37,8 @@ public static class ChannelOptionsExtensions
 | 
			
		||||
            for (int i = 0; i < funcs.Count; i++)
 | 
			
		||||
            {
 | 
			
		||||
                var func = funcs[i];
 | 
			
		||||
                var task = func.Invoke(clientChannel, e, i == funcs.Count - 1);
 | 
			
		||||
                if (!task.IsCompleted)
 | 
			
		||||
                    await task.ConfigureAwait(false);
 | 
			
		||||
                if (func == null) continue;
 | 
			
		||||
                await func.Invoke(clientChannel, e, i == funcs.Count - 1).ConfigureAwait(false);
 | 
			
		||||
                if (e.Handled)
 | 
			
		||||
                {
 | 
			
		||||
                    break;
 | 
			
		||||
@@ -67,6 +66,7 @@ public static class ChannelOptionsExtensions
 | 
			
		||||
                for (int i = 0; i < funcs.Count; i++)
 | 
			
		||||
                {
 | 
			
		||||
                    var func = funcs[i];
 | 
			
		||||
                    if (func == null) continue;
 | 
			
		||||
                    var handled = await func.Invoke(clientChannel, i == funcs.Count - 1).ConfigureAwait(false);
 | 
			
		||||
                    if (handled)
 | 
			
		||||
                    {
 | 
			
		||||
@@ -101,16 +101,16 @@ public static class ChannelOptionsExtensions
 | 
			
		||||
        switch (channelType)
 | 
			
		||||
        {
 | 
			
		||||
            case ChannelTypeEnum.TcpClient:
 | 
			
		||||
                return config.GetTcpClientWithIPHost(channelOptions);
 | 
			
		||||
                return config.GetTcpClient(channelOptions);
 | 
			
		||||
 | 
			
		||||
            case ChannelTypeEnum.TcpService:
 | 
			
		||||
                return config.GetTcpServiceWithBindIPHost(channelOptions);
 | 
			
		||||
                return config.GetTcpService(channelOptions);
 | 
			
		||||
 | 
			
		||||
            case ChannelTypeEnum.SerialPort:
 | 
			
		||||
                return config.GetSerialPortWithOption(channelOptions);
 | 
			
		||||
                return config.GetSerialPort(channelOptions);
 | 
			
		||||
 | 
			
		||||
            case ChannelTypeEnum.UdpSession:
 | 
			
		||||
                return config.GetUdpSessionWithIPHost(channelOptions);
 | 
			
		||||
                return config.GetUdpSession(channelOptions);
 | 
			
		||||
            case ChannelTypeEnum.Other:
 | 
			
		||||
                channelOptions.Config = config;
 | 
			
		||||
                OtherChannel otherChannel = new OtherChannel(channelOptions);
 | 
			
		||||
@@ -125,13 +125,12 @@ public static class ChannelOptionsExtensions
 | 
			
		||||
    /// <param name="config">配置</param>
 | 
			
		||||
    /// <param name="channelOptions">串口配置</param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public static SerialPortChannel GetSerialPortWithOption(this TouchSocketConfig config, IChannelOptions channelOptions)
 | 
			
		||||
    public static SerialPortChannel GetSerialPort(this TouchSocketConfig config, IChannelOptions channelOptions)
 | 
			
		||||
    {
 | 
			
		||||
        var serialPortOption = channelOptions.Map<SerialPortOption>();
 | 
			
		||||
        serialPortOption.ThrowIfNull(nameof(SerialPortOption));
 | 
			
		||||
        channelOptions.Config = config;
 | 
			
		||||
        config.SetSerialPortOption(serialPortOption);
 | 
			
		||||
 | 
			
		||||
        //载入配置
 | 
			
		||||
        SerialPortChannel serialPortChannel = new SerialPortChannel(channelOptions);
 | 
			
		||||
        return serialPortChannel;
 | 
			
		||||
@@ -144,7 +143,7 @@ public static class ChannelOptionsExtensions
 | 
			
		||||
    /// <param name="channelOptions">通道配置</param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    /// <exception cref="ArgumentNullException"></exception>
 | 
			
		||||
    public static TcpClientChannel GetTcpClientWithIPHost(this TouchSocketConfig config, IChannelOptions channelOptions)
 | 
			
		||||
    public static TcpClientChannel GetTcpClient(this TouchSocketConfig config, IChannelOptions channelOptions)
 | 
			
		||||
    {
 | 
			
		||||
        var remoteUrl = channelOptions.RemoteUrl;
 | 
			
		||||
        var bindUrl = channelOptions.BindUrl;
 | 
			
		||||
@@ -166,7 +165,7 @@ public static class ChannelOptionsExtensions
 | 
			
		||||
    /// <param name="channelOptions">通道配置</param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    /// <exception cref="ArgumentNullException"></exception>
 | 
			
		||||
    public static IChannel GetTcpServiceWithBindIPHost(this TouchSocketConfig config, IChannelOptions channelOptions)
 | 
			
		||||
    public static IChannel GetTcpService(this TouchSocketConfig config, IChannelOptions channelOptions)
 | 
			
		||||
    {
 | 
			
		||||
        var bindUrl = channelOptions.BindUrl;
 | 
			
		||||
        bindUrl.ThrowIfNull(nameof(bindUrl));
 | 
			
		||||
@@ -194,7 +193,7 @@ public static class ChannelOptionsExtensions
 | 
			
		||||
    /// <param name="channelOptions">通道配置</param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    /// <exception cref="ArgumentNullException"></exception>
 | 
			
		||||
    public static UdpSessionChannel GetUdpSessionWithIPHost(this TouchSocketConfig config, IChannelOptions channelOptions)
 | 
			
		||||
    public static UdpSessionChannel GetUdpSession(this TouchSocketConfig config, IChannelOptions channelOptions)
 | 
			
		||||
    {
 | 
			
		||||
        var remoteUrl = channelOptions.RemoteUrl;
 | 
			
		||||
        var bindUrl = channelOptions.BindUrl;
 | 
			
		||||
 
 | 
			
		||||
@@ -19,6 +19,11 @@ namespace ThingsGateway.Foundation;
 | 
			
		||||
/// </summary>
 | 
			
		||||
public class OtherChannel : SetupConfigObject, IClientChannel
 | 
			
		||||
{
 | 
			
		||||
    ~OtherChannel()
 | 
			
		||||
    {
 | 
			
		||||
        this.SafeDispose();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private SingleStreamDataHandlingAdapter m_dataHandlingAdapter;
 | 
			
		||||
    public DataHandlingAdapter ReadOnlyDataHandlingAdapter => m_dataHandlingAdapter;
 | 
			
		||||
 | 
			
		||||
@@ -29,17 +34,7 @@ public class OtherChannel : SetupConfigObject, IClientChannel
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override TouchSocketConfig Config => base.Config ?? ChannelOptions.Config;
 | 
			
		||||
    public void SetDataHandlingAdapterLogger(ILog log)
 | 
			
		||||
    {
 | 
			
		||||
        if (_deviceDataHandleAdapter != ReadOnlyDataHandlingAdapter && ReadOnlyDataHandlingAdapter is IDeviceDataHandleAdapter handleAdapter)
 | 
			
		||||
        {
 | 
			
		||||
            _deviceDataHandleAdapter = handleAdapter;
 | 
			
		||||
        }
 | 
			
		||||
        if (_deviceDataHandleAdapter != null)
 | 
			
		||||
        {
 | 
			
		||||
            _deviceDataHandleAdapter.Logger = log;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public void ResetSign(int minSign = 0, int maxSign = ushort.MaxValue)
 | 
			
		||||
    {
 | 
			
		||||
@@ -83,16 +78,27 @@ public class OtherChannel : SetupConfigObject, IClientChannel
 | 
			
		||||
 | 
			
		||||
    //private readonly WaitLock _connectLock = new WaitLock();
 | 
			
		||||
 | 
			
		||||
    private IDeviceDataHandleAdapter _deviceDataHandleAdapter;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    private bool logSet;
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public void SetDataHandlingAdapterLogger(ILog log)
 | 
			
		||||
    {
 | 
			
		||||
        if (!logSet && ReadOnlyDataHandlingAdapter is IDeviceDataHandleAdapter handleAdapter)
 | 
			
		||||
        {
 | 
			
		||||
            logSet = true;
 | 
			
		||||
            handleAdapter.Logger = log;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public void SetDataHandlingAdapter(DataHandlingAdapter adapter)
 | 
			
		||||
    {
 | 
			
		||||
        if (adapter is SingleStreamDataHandlingAdapter singleStreamDataHandlingAdapter)
 | 
			
		||||
            SetAdapter(singleStreamDataHandlingAdapter);
 | 
			
		||||
        if (adapter is IDeviceDataHandleAdapter deviceDataHandleAdapter)
 | 
			
		||||
            _deviceDataHandleAdapter = deviceDataHandleAdapter;
 | 
			
		||||
 | 
			
		||||
        logSet = false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 设置数据处理适配器。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -34,7 +34,26 @@ public static class PluginUtil
 | 
			
		||||
 | 
			
		||||
            if (channelOptions.ChannelType == ChannelTypeEnum.TcpClient)
 | 
			
		||||
            {
 | 
			
		||||
                action += a => a.UseReconnection<IClientChannel>();
 | 
			
		||||
                action += a => a.UseReconnection<IClientChannel>().SetActionForCheck((channel, failCount) =>
 | 
			
		||||
                {
 | 
			
		||||
                    if (channel.Online)
 | 
			
		||||
                    {
 | 
			
		||||
                        return Task.FromResult(ConnectionCheckResult.Alive);
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        if (failCount > 1)
 | 
			
		||||
                        {
 | 
			
		||||
                            return Task.FromResult(ConnectionCheckResult.Dead);
 | 
			
		||||
                        }
 | 
			
		||||
                        return Task.FromResult(ConnectionCheckResult.Skip);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                })
 | 
			
		||||
                .SetPollingTick(TimeSpan.FromSeconds(5)
 | 
			
		||||
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
            return action;
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -19,6 +19,10 @@ namespace ThingsGateway.Foundation;
 | 
			
		||||
/// </summary>
 | 
			
		||||
public class SerialPortChannel : SerialPortClient, IClientChannel
 | 
			
		||||
{
 | 
			
		||||
    ~SerialPortChannel()
 | 
			
		||||
    {
 | 
			
		||||
        this.SafeDispose();
 | 
			
		||||
    }
 | 
			
		||||
    public SerialPortChannel(IChannelOptions channelOptions)
 | 
			
		||||
    {
 | 
			
		||||
        ChannelOptions = channelOptions;
 | 
			
		||||
@@ -47,16 +51,15 @@ public class SerialPortChannel : SerialPortClient, IClientChannel
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public DataHandlingAdapter ReadOnlyDataHandlingAdapter => ProtectedDataHandlingAdapter;
 | 
			
		||||
    private IDeviceDataHandleAdapter _deviceDataHandleAdapter;
 | 
			
		||||
 | 
			
		||||
    private bool logSet;
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public void SetDataHandlingAdapterLogger(ILog log)
 | 
			
		||||
    {
 | 
			
		||||
        if (_deviceDataHandleAdapter != ProtectedDataHandlingAdapter && ProtectedDataHandlingAdapter is IDeviceDataHandleAdapter handleAdapter)
 | 
			
		||||
        if (!logSet && ProtectedDataHandlingAdapter is IDeviceDataHandleAdapter handleAdapter)
 | 
			
		||||
        {
 | 
			
		||||
            _deviceDataHandleAdapter = handleAdapter;
 | 
			
		||||
        }
 | 
			
		||||
        if (_deviceDataHandleAdapter != null)
 | 
			
		||||
        {
 | 
			
		||||
            _deviceDataHandleAdapter.Logger = log;
 | 
			
		||||
            logSet = true;
 | 
			
		||||
            handleAdapter.Logger = log;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
@@ -64,10 +67,11 @@ public class SerialPortChannel : SerialPortClient, IClientChannel
 | 
			
		||||
    {
 | 
			
		||||
        if (adapter is SingleStreamDataHandlingAdapter singleStreamDataHandlingAdapter)
 | 
			
		||||
            SetAdapter(singleStreamDataHandlingAdapter);
 | 
			
		||||
        if (adapter is IDeviceDataHandleAdapter deviceDataHandleAdapter)
 | 
			
		||||
            _deviceDataHandleAdapter = deviceDataHandleAdapter;
 | 
			
		||||
 | 
			
		||||
        logSet = false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public ChannelEventHandler Started { get; } = new();
 | 
			
		||||
 | 
			
		||||
@@ -192,16 +196,12 @@ public class SerialPortChannel : SerialPortClient, IClientChannel
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    protected override async Task OnSerialReceived(ReceivedDataEventArgs e)
 | 
			
		||||
    {
 | 
			
		||||
        var receivedTask = base.OnSerialReceived(e);
 | 
			
		||||
        if (!receivedTask.IsCompleted)
 | 
			
		||||
            await receivedTask.ConfigureAwait(false);
 | 
			
		||||
        await base.OnSerialReceived(e).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
        if (e.Handled)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        var channelReceivedTask = this.OnChannelReceivedEvent(e, ChannelReceived);
 | 
			
		||||
        if (!channelReceivedTask.IsCompleted)
 | 
			
		||||
            await channelReceivedTask.ConfigureAwait(false);
 | 
			
		||||
        await this.OnChannelReceivedEvent(e, ChannelReceived).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,10 @@ namespace ThingsGateway.Foundation;
 | 
			
		||||
/// </summary>
 | 
			
		||||
public class TcpClientChannel : TcpClient, IClientChannel
 | 
			
		||||
{
 | 
			
		||||
    ~TcpClientChannel()
 | 
			
		||||
    {
 | 
			
		||||
        this.SafeDispose();
 | 
			
		||||
    }
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public TcpClientChannel(IChannelOptions channelOptions)
 | 
			
		||||
    {
 | 
			
		||||
@@ -30,16 +34,13 @@ public class TcpClientChannel : TcpClient, IClientChannel
 | 
			
		||||
        WaitHandlePool = new WaitHandlePool<MessageBase>(minSign, maxSign);
 | 
			
		||||
        pool?.CancelAll();
 | 
			
		||||
    }
 | 
			
		||||
    private IDeviceDataHandleAdapter _deviceDataHandleAdapter;
 | 
			
		||||
    private bool logSet;
 | 
			
		||||
    public void SetDataHandlingAdapterLogger(ILog log)
 | 
			
		||||
    {
 | 
			
		||||
        if (_deviceDataHandleAdapter != DataHandlingAdapter && DataHandlingAdapter is IDeviceDataHandleAdapter handleAdapter)
 | 
			
		||||
        if (!logSet && DataHandlingAdapter is IDeviceDataHandleAdapter handleAdapter)
 | 
			
		||||
        {
 | 
			
		||||
            _deviceDataHandleAdapter = handleAdapter;
 | 
			
		||||
        }
 | 
			
		||||
        if (_deviceDataHandleAdapter != null)
 | 
			
		||||
        {
 | 
			
		||||
            _deviceDataHandleAdapter.Logger = log;
 | 
			
		||||
            logSet = true;
 | 
			
		||||
            handleAdapter.Logger = log;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
@@ -47,8 +48,8 @@ public class TcpClientChannel : TcpClient, IClientChannel
 | 
			
		||||
    {
 | 
			
		||||
        if (adapter is SingleStreamDataHandlingAdapter singleStreamDataHandlingAdapter)
 | 
			
		||||
            SetAdapter(singleStreamDataHandlingAdapter);
 | 
			
		||||
        if (adapter is IDeviceDataHandleAdapter deviceDataHandleAdapter)
 | 
			
		||||
            _deviceDataHandleAdapter = deviceDataHandleAdapter;
 | 
			
		||||
 | 
			
		||||
        logSet = false;
 | 
			
		||||
    }
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public ChannelReceivedEventHandler ChannelReceived { get; } = new();
 | 
			
		||||
@@ -179,16 +180,12 @@ public class TcpClientChannel : TcpClient, IClientChannel
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    protected override async Task OnTcpReceived(ReceivedDataEventArgs e)
 | 
			
		||||
    {
 | 
			
		||||
        var receivedTask = base.OnTcpReceived(e);
 | 
			
		||||
        if (!receivedTask.IsCompleted)
 | 
			
		||||
            await receivedTask.ConfigureAwait(false);
 | 
			
		||||
        await base.OnTcpReceived(e).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
        if (e.Handled)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        var channelReceivedTask = this.OnChannelReceivedEvent(e, ChannelReceived);
 | 
			
		||||
        if (!channelReceivedTask.IsCompleted)
 | 
			
		||||
            await channelReceivedTask.ConfigureAwait(false);
 | 
			
		||||
        await this.OnChannelReceivedEvent(e, ChannelReceived).ConfigureAwait(false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,11 @@ namespace ThingsGateway.Foundation;
 | 
			
		||||
/// <typeparam name="TClient"></typeparam>
 | 
			
		||||
public abstract class TcpServiceChannelBase<TClient> : TcpService<TClient>, ITcpService<TClient> where TClient : TcpSessionClientChannel, new()
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    ~TcpServiceChannelBase()
 | 
			
		||||
    {
 | 
			
		||||
        this.SafeDispose();
 | 
			
		||||
    }
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public ConcurrentList<IDevice> Collects { get; } = new();
 | 
			
		||||
 | 
			
		||||
@@ -241,16 +246,12 @@ public class TcpServiceChannel<TClient> : TcpServiceChannelBase<TClient>, IChann
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    protected override async Task OnTcpReceived(TClient socketClient, ReceivedDataEventArgs e)
 | 
			
		||||
    {
 | 
			
		||||
        var receivedTask = base.OnTcpReceived(socketClient, e);
 | 
			
		||||
        if (!receivedTask.IsCompleted)
 | 
			
		||||
            await receivedTask.ConfigureAwait(false);
 | 
			
		||||
        await base.OnTcpReceived(socketClient, e).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
        if (e.Handled)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        var channelReceivedTask = socketClient.OnChannelReceivedEvent(e, ChannelReceived);
 | 
			
		||||
        if (!channelReceivedTask.IsCompleted)
 | 
			
		||||
            await channelReceivedTask.ConfigureAwait(false);
 | 
			
		||||
        await socketClient.OnChannelReceivedEvent(e, ChannelReceived).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -17,20 +17,22 @@ namespace ThingsGateway.Foundation;
 | 
			
		||||
/// </summary>
 | 
			
		||||
public class TcpSessionClientChannel : TcpSessionClient, IClientChannel
 | 
			
		||||
{
 | 
			
		||||
    ~TcpSessionClientChannel()
 | 
			
		||||
    {
 | 
			
		||||
        this.SafeDispose();
 | 
			
		||||
    }
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public TcpSessionClientChannel()
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
    private IDeviceDataHandleAdapter _deviceDataHandleAdapter;
 | 
			
		||||
    private bool logSet;
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public void SetDataHandlingAdapterLogger(ILog log)
 | 
			
		||||
    {
 | 
			
		||||
        if (_deviceDataHandleAdapter != DataHandlingAdapter && DataHandlingAdapter is IDeviceDataHandleAdapter handleAdapter)
 | 
			
		||||
        if (!logSet && DataHandlingAdapter is IDeviceDataHandleAdapter handleAdapter)
 | 
			
		||||
        {
 | 
			
		||||
            _deviceDataHandleAdapter = handleAdapter;
 | 
			
		||||
        }
 | 
			
		||||
        if (_deviceDataHandleAdapter != null)
 | 
			
		||||
        {
 | 
			
		||||
            _deviceDataHandleAdapter.Logger = log;
 | 
			
		||||
            logSet = true;
 | 
			
		||||
            handleAdapter.Logger = log;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
@@ -38,9 +40,10 @@ public class TcpSessionClientChannel : TcpSessionClient, IClientChannel
 | 
			
		||||
    {
 | 
			
		||||
        if (adapter is SingleStreamDataHandlingAdapter singleStreamDataHandlingAdapter)
 | 
			
		||||
            SetAdapter(singleStreamDataHandlingAdapter);
 | 
			
		||||
        if (adapter is IDeviceDataHandleAdapter deviceDataHandleAdapter)
 | 
			
		||||
            _deviceDataHandleAdapter = deviceDataHandleAdapter;
 | 
			
		||||
 | 
			
		||||
        logSet = false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void ResetSign(int minSign = 0, int maxSign = ushort.MaxValue)
 | 
			
		||||
    {
 | 
			
		||||
        var pool = WaitHandlePool;
 | 
			
		||||
@@ -145,15 +148,11 @@ public class TcpSessionClientChannel : TcpSessionClient, IClientChannel
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    protected override async Task OnTcpReceived(ReceivedDataEventArgs e)
 | 
			
		||||
    {
 | 
			
		||||
        var receivedTask = base.OnTcpReceived(e);
 | 
			
		||||
        if (!receivedTask.IsCompleted)
 | 
			
		||||
            await receivedTask.ConfigureAwait(false);
 | 
			
		||||
        await base.OnTcpReceived(e).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
        if (e.Handled)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        var channelReceivedTask = this.OnChannelReceivedEvent(e, ChannelReceived);
 | 
			
		||||
        if (!channelReceivedTask.IsCompleted)
 | 
			
		||||
            await channelReceivedTask.ConfigureAwait(false);
 | 
			
		||||
        await this.OnChannelReceivedEvent(e, ChannelReceived).ConfigureAwait(false);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,10 @@ namespace ThingsGateway.Foundation;
 | 
			
		||||
/// </summary>
 | 
			
		||||
public class UdpSessionChannel : UdpSession, IClientChannel
 | 
			
		||||
{
 | 
			
		||||
    ~UdpSessionChannel()
 | 
			
		||||
    {
 | 
			
		||||
        this.SafeDispose();
 | 
			
		||||
    }
 | 
			
		||||
    private readonly WaitLock _connectLock = new WaitLock(nameof(UdpSessionChannel));
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
@@ -26,16 +30,14 @@ public class UdpSessionChannel : UdpSession, IClientChannel
 | 
			
		||||
        ResetSign();
 | 
			
		||||
    }
 | 
			
		||||
    public override TouchSocketConfig Config => base.Config ?? ChannelOptions.Config;
 | 
			
		||||
    private IDeviceDataHandleAdapter _deviceDataHandleAdapter;
 | 
			
		||||
    private bool logSet;
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public void SetDataHandlingAdapterLogger(ILog log)
 | 
			
		||||
    {
 | 
			
		||||
        if (_deviceDataHandleAdapter != DataHandlingAdapter && DataHandlingAdapter is IDeviceDataHandleAdapter handleAdapter)
 | 
			
		||||
        if (!logSet && DataHandlingAdapter is IDeviceDataHandleAdapter handleAdapter)
 | 
			
		||||
        {
 | 
			
		||||
            _deviceDataHandleAdapter = handleAdapter;
 | 
			
		||||
        }
 | 
			
		||||
        if (_deviceDataHandleAdapter != null)
 | 
			
		||||
        {
 | 
			
		||||
            _deviceDataHandleAdapter.Logger = log;
 | 
			
		||||
            logSet = true;
 | 
			
		||||
            handleAdapter.Logger = log;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
@@ -43,9 +45,11 @@ public class UdpSessionChannel : UdpSession, IClientChannel
 | 
			
		||||
    {
 | 
			
		||||
        if (adapter is UdpDataHandlingAdapter udpDataHandlingAdapter)
 | 
			
		||||
            SetAdapter(udpDataHandlingAdapter);
 | 
			
		||||
        if (adapter is IDeviceDataHandleAdapter deviceDataHandleAdapter)
 | 
			
		||||
            _deviceDataHandleAdapter = deviceDataHandleAdapter;
 | 
			
		||||
 | 
			
		||||
        logSet = false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public void ResetSign(int minSign = 0, int maxSign = ushort.MaxValue)
 | 
			
		||||
    {
 | 
			
		||||
        var pool = WaitHandlePool;
 | 
			
		||||
@@ -196,16 +200,12 @@ public class UdpSessionChannel : UdpSession, IClientChannel
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    protected override async Task OnUdpReceived(UdpReceivedDataEventArgs e)
 | 
			
		||||
    {
 | 
			
		||||
        var receivedTask = base.OnUdpReceived(e);
 | 
			
		||||
        if (!receivedTask.IsCompleted)
 | 
			
		||||
            await receivedTask.ConfigureAwait(false);
 | 
			
		||||
        await base.OnUdpReceived(e).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
        if (e.Handled)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        var channelReceivedTask = this.OnChannelReceivedEvent(e, ChannelReceived);
 | 
			
		||||
        if (!channelReceivedTask.IsCompleted)
 | 
			
		||||
            await channelReceivedTask.ConfigureAwait(false);
 | 
			
		||||
        await this.OnChannelReceivedEvent(e, ChannelReceived).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -10,8 +10,6 @@
 | 
			
		||||
 | 
			
		||||
using System.Text;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.NewLife.Collections;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Foundation;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
@@ -59,13 +57,6 @@ public class DeviceSingleStreamDataHandleAdapter<TRequest> : CustomDataHandlingA
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    public void SetRequest(ISendMessage sendMessage)
 | 
			
		||||
    {
 | 
			
		||||
        if (IsSingleThread)
 | 
			
		||||
        {
 | 
			
		||||
            if (Request != null)
 | 
			
		||||
            {
 | 
			
		||||
                _requestPool.Return(Request);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        var request = GetInstance();
 | 
			
		||||
        request.Sign = sendMessage.Sign;
 | 
			
		||||
        request.SendInfo(sendMessage);
 | 
			
		||||
@@ -165,25 +156,13 @@ public class DeviceSingleStreamDataHandleAdapter<TRequest> : CustomDataHandlingA
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static ObjectPool<TRequest> _requestPool { get; } = new ObjectPool<TRequest>();
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取泛型实例。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    protected virtual TRequest GetInstance()
 | 
			
		||||
    {
 | 
			
		||||
        if (IsSingleThread)
 | 
			
		||||
        {
 | 
			
		||||
            var request = _requestPool.Get();
 | 
			
		||||
            request.OperCode = -1;
 | 
			
		||||
            request.Sign = -1;
 | 
			
		||||
            return request;
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            return new TRequest() { OperCode = -1, Sign = -1 };
 | 
			
		||||
        }
 | 
			
		||||
        return new TRequest() { OperCode = -1, Sign = -1 };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override void SendInput<TWriter>(ref TWriter writer, in ReadOnlyMemory<byte> memory)
 | 
			
		||||
 
 | 
			
		||||
@@ -11,8 +11,6 @@
 | 
			
		||||
using System.Net;
 | 
			
		||||
using System.Text;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.NewLife.Collections;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Foundation;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
@@ -52,13 +50,6 @@ public class DeviceUdpDataHandleAdapter<TRequest> : UdpDataHandlingAdapter, IDev
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    public void SetRequest(ISendMessage sendMessage)
 | 
			
		||||
    {
 | 
			
		||||
        if (IsSingleThread)
 | 
			
		||||
        {
 | 
			
		||||
            if (Request != null)
 | 
			
		||||
            {
 | 
			
		||||
                _requestPool.Return(Request);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        var request = GetInstance();
 | 
			
		||||
        request.Sign = sendMessage.Sign;
 | 
			
		||||
        request.SendInfo(sendMessage);
 | 
			
		||||
@@ -71,26 +62,14 @@ public class DeviceUdpDataHandleAdapter<TRequest> : UdpDataHandlingAdapter, IDev
 | 
			
		||||
        return Owner.ToString();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static ObjectPool<TRequest> _requestPool { get; } = new ObjectPool<TRequest>();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取泛型实例。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    protected virtual TRequest GetInstance()
 | 
			
		||||
    {
 | 
			
		||||
        if (IsSingleThread)
 | 
			
		||||
        {
 | 
			
		||||
            var request = _requestPool.Get();
 | 
			
		||||
            request.OperCode = -1;
 | 
			
		||||
            request.Sign = -1;
 | 
			
		||||
            return request;
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            return new TRequest() { OperCode = -1, Sign = -1 };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return new TRequest() { OperCode = -1, Sign = -1 };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,7 @@ using System.Net;
 | 
			
		||||
using ThingsGateway.Foundation.Extension.Generic;
 | 
			
		||||
using ThingsGateway.Foundation.Extension.String;
 | 
			
		||||
using ThingsGateway.NewLife;
 | 
			
		||||
using ThingsGateway.NewLife.Collections;
 | 
			
		||||
using ThingsGateway.NewLife.Extension;
 | 
			
		||||
 | 
			
		||||
using TouchSocket.SerialPorts;
 | 
			
		||||
@@ -330,45 +331,23 @@ public abstract class DeviceBase : AsyncAndSyncDisposableObject, IDevice
 | 
			
		||||
    }
 | 
			
		||||
    public bool AutoConnect { get; protected set; } = true;
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    private async ValueTask<OperResult> SendAsync(ISendMessage sendMessage, IClientChannel channel = default, EndPoint endPoint = default, CancellationToken token = default)
 | 
			
		||||
    private async Task SendAsync(ISendMessage sendMessage, IClientChannel channel, CancellationToken token = default)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
 | 
			
		||||
        if (SendDelayTime != 0)
 | 
			
		||||
            await Task.Delay(SendDelayTime, token).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
        if (channel is IDtuUdpSessionChannel udpSession)
 | 
			
		||||
        {
 | 
			
		||||
            if (channel == default)
 | 
			
		||||
            {
 | 
			
		||||
                if (Channel is not IClientChannel clientChannel) { throw new ArgumentNullException(nameof(channel)); }
 | 
			
		||||
                channel = clientChannel;
 | 
			
		||||
            }
 | 
			
		||||
            EndPoint? endPoint = GetUdpEndpoint();
 | 
			
		||||
            await udpSession.SendAsync(endPoint, sendMessage, token).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            if (SendDelayTime != 0)
 | 
			
		||||
                await Task.Delay(SendDelayTime, token).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            if (token.IsCancellationRequested)
 | 
			
		||||
                return new OperResult(new OperationCanceledException());
 | 
			
		||||
 | 
			
		||||
            if (channel is IDtuUdpSessionChannel udpSession)
 | 
			
		||||
            {
 | 
			
		||||
                var sendTask = udpSession.SendAsync(endPoint, sendMessage, token);
 | 
			
		||||
                if (!sendTask.IsCompleted)
 | 
			
		||||
                {
 | 
			
		||||
                    await sendTask.ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                var sendTask = channel.SendAsync(sendMessage, token);
 | 
			
		||||
                if (!sendTask.IsCompleted)
 | 
			
		||||
                {
 | 
			
		||||
                    await sendTask.ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return OperResult.Success;
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            return new(ex);
 | 
			
		||||
            await channel.SendAsync(sendMessage, token).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Task BeforeSendAsync(IClientChannel channel, CancellationToken token)
 | 
			
		||||
@@ -415,25 +394,20 @@ public abstract class DeviceBase : AsyncAndSyncDisposableObject, IDevice
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var dtuId = this is IDtu dtu1 ? dtu1.DtuId : null;
 | 
			
		||||
            var channelResult = GetChannel(dtuId);
 | 
			
		||||
            var channelResult = GetChannel();
 | 
			
		||||
            if (!channelResult.IsSuccess) return new OperResult<byte[]>(channelResult);
 | 
			
		||||
            WaitLock? waitLock = GetWaitLock(channelResult.Content, dtuId);
 | 
			
		||||
            WaitLock? waitLock = GetWaitLock(channelResult.Content);
 | 
			
		||||
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var beforeSendTask = BeforeSendAsync(channelResult.Content, cancellationToken);
 | 
			
		||||
                if (!beforeSendTask.IsCompleted)
 | 
			
		||||
                {
 | 
			
		||||
                    await beforeSendTask.ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                await BeforeSendAsync(channelResult.Content, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                await waitLock.WaitAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                channelResult.Content.SetDataHandlingAdapterLogger(Logger);
 | 
			
		||||
 | 
			
		||||
                EndPoint? endPoint = GetUdpEndpoint(dtuId);
 | 
			
		||||
 | 
			
		||||
                return await SendAsync(sendMessage, channelResult.Content, endPoint, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                await SendAsync(sendMessage, channelResult.Content, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                return OperResult.Success;
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
@@ -442,15 +416,18 @@ public abstract class DeviceBase : AsyncAndSyncDisposableObject, IDevice
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            if (!cancellationToken.IsCancellationRequested)
 | 
			
		||||
                await Task.Delay(1000, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
            return new(ex);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public virtual OperResult<IClientChannel> GetChannel(string socketId)
 | 
			
		||||
    public virtual OperResult<IClientChannel> GetChannel()
 | 
			
		||||
    {
 | 
			
		||||
        if (Channel is IClientChannel clientChannel1)
 | 
			
		||||
            return new OperResult<IClientChannel>() { Content = clientChannel1 };
 | 
			
		||||
 | 
			
		||||
        var socketId = this is IDtu dtu1 ? dtu1.DtuId : null;
 | 
			
		||||
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(socketId))
 | 
			
		||||
        {
 | 
			
		||||
            if (Channel is IClientChannel clientChannel)
 | 
			
		||||
@@ -485,10 +462,11 @@ public abstract class DeviceBase : AsyncAndSyncDisposableObject, IDevice
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public virtual EndPoint GetUdpEndpoint(string socketId)
 | 
			
		||||
    public virtual EndPoint GetUdpEndpoint()
 | 
			
		||||
    {
 | 
			
		||||
        if (Channel is IDtuUdpSessionChannel udpSessionChannel)
 | 
			
		||||
        {
 | 
			
		||||
            var socketId = this is IDtu dtu1 ? dtu1.DtuId : null;
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(socketId))
 | 
			
		||||
                return udpSessionChannel.DefaultEndpoint;
 | 
			
		||||
 | 
			
		||||
@@ -514,7 +492,7 @@ public abstract class DeviceBase : AsyncAndSyncDisposableObject, IDevice
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public virtual ValueTask<OperResult<ReadOnlyMemory<byte>>> SendThenReturnAsync(ISendMessage sendMessage, CancellationToken cancellationToken = default)
 | 
			
		||||
    {
 | 
			
		||||
        var channelResult = GetChannel(this is IDtu dtu ? dtu.DtuId : null);
 | 
			
		||||
        var channelResult = GetChannel();
 | 
			
		||||
        if (!channelResult.IsSuccess) return EasyValueTask.FromResult(new OperResult<ReadOnlyMemory<byte>>(channelResult));
 | 
			
		||||
        return SendThenReturnAsync(sendMessage, channelResult.Content, cancellationToken);
 | 
			
		||||
    }
 | 
			
		||||
@@ -524,18 +502,8 @@ public abstract class DeviceBase : AsyncAndSyncDisposableObject, IDevice
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var sendTask = SendThenReturnMessageAsync(sendMessage, channel, cancellationToken);
 | 
			
		||||
            if (!sendTask.IsCompleted)
 | 
			
		||||
            {
 | 
			
		||||
                var result = await sendTask.ConfigureAwait(false);
 | 
			
		||||
                return new OperResult<ReadOnlyMemory<byte>>(result) { Content = result.Content };
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                var result = sendTask.Result;
 | 
			
		||||
                return new OperResult<ReadOnlyMemory<byte>>(result) { Content = result.Content };
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var result = await SendThenReturnMessageAsync(sendMessage, channel, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
            return new OperResult<ReadOnlyMemory<byte>>(result) { Content = result.Content };
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
@@ -546,7 +514,7 @@ public abstract class DeviceBase : AsyncAndSyncDisposableObject, IDevice
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    protected virtual ValueTask<MessageBase> SendThenReturnMessageAsync(ISendMessage sendMessage, CancellationToken cancellationToken = default)
 | 
			
		||||
    {
 | 
			
		||||
        var channelResult = GetChannel(this is IDtu dtu ? dtu.DtuId : null);
 | 
			
		||||
        var channelResult = GetChannel();
 | 
			
		||||
        if (!channelResult.IsSuccess) return EasyValueTask.FromResult(new MessageBase(channelResult));
 | 
			
		||||
        return SendThenReturnMessageAsync(sendMessage, channelResult.Content, cancellationToken);
 | 
			
		||||
    }
 | 
			
		||||
@@ -557,103 +525,87 @@ public abstract class DeviceBase : AsyncAndSyncDisposableObject, IDevice
 | 
			
		||||
        return GetResponsedDataAsync(command, clientChannel, Timeout, cancellationToken);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private ObjectPool<ReusableCancellationTokenSource> _reusableTimeouts = new();
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 发送并等待数据
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    protected async ValueTask<MessageBase> GetResponsedDataAsync(ISendMessage command, IClientChannel clientChannel, int timeout = 3000, CancellationToken cancellationToken = default)
 | 
			
		||||
    protected async ValueTask<MessageBase> GetResponsedDataAsync(
 | 
			
		||||
        ISendMessage command,
 | 
			
		||||
        IClientChannel clientChannel,
 | 
			
		||||
        int timeout = 3000,
 | 
			
		||||
        CancellationToken cancellationToken = default)
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        var waitData = clientChannel.WaitHandlePool.GetWaitDataAsync(out var sign);
 | 
			
		||||
        command.Sign = sign;
 | 
			
		||||
        WaitLock? waitLock = null;
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var beforeSendTask = BeforeSendAsync(clientChannel, cancellationToken);
 | 
			
		||||
            if (!beforeSendTask.IsCompleted)
 | 
			
		||||
            {
 | 
			
		||||
                await beforeSendTask.ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
            var dtuId = this is IDtu dtu1 ? dtu1.DtuId : null;
 | 
			
		||||
            waitLock = GetWaitLock(clientChannel, dtuId);
 | 
			
		||||
            await BeforeSendAsync(clientChannel, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            waitLock = GetWaitLock(clientChannel);
 | 
			
		||||
 | 
			
		||||
            await waitLock.WaitAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            EndPoint? endPoint = GetUdpEndpoint(dtuId);
 | 
			
		||||
 | 
			
		||||
            if (cancellationToken.IsCancellationRequested)
 | 
			
		||||
                return new MessageBase(new OperationCanceledException());
 | 
			
		||||
 | 
			
		||||
            clientChannel.SetDataHandlingAdapterLogger(Logger);
 | 
			
		||||
 | 
			
		||||
            await SendAsync(command, clientChannel, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            if (cancellationToken.IsCancellationRequested)
 | 
			
		||||
                return new MessageBase(new OperationCanceledException());
 | 
			
		||||
            if (waitData.Status == WaitDataStatus.Success)
 | 
			
		||||
                return waitData.CompletedData;
 | 
			
		||||
 | 
			
		||||
            OperResult sendOperResult = default;
 | 
			
		||||
            var sendTask = SendAsync(command, clientChannel, endPoint, cancellationToken);
 | 
			
		||||
            if (!sendTask.IsCompleted)
 | 
			
		||||
            {
 | 
			
		||||
                sendOperResult = await sendTask.ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                sendOperResult = sendTask.Result;
 | 
			
		||||
            }
 | 
			
		||||
            bool timeoutStatus = false;
 | 
			
		||||
 | 
			
		||||
            if (!sendOperResult.IsSuccess)
 | 
			
		||||
                return new MessageBase(sendOperResult);
 | 
			
		||||
 | 
			
		||||
            using var ctsTime = new CancellationTokenSource(timeout);
 | 
			
		||||
            var reusableTimeout = _reusableTimeouts.Get();
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                using var cts = CancellationTokenSource.CreateLinkedTokenSource(ctsTime.Token, Channel.ClosedToken);
 | 
			
		||||
                var waitDataTask = waitData.WaitAsync(cts.Token);
 | 
			
		||||
                if (!waitDataTask.IsCompleted)
 | 
			
		||||
                {
 | 
			
		||||
                    await waitDataTask.ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var cts = reusableTimeout.GetTokenSource(timeout, cancellationToken, Channel.ClosedToken);
 | 
			
		||||
                await waitData.WaitAsync(cts.Token).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
            catch (OperationCanceledException)
 | 
			
		||||
            {
 | 
			
		||||
                if (ctsTime.IsCancellationRequested)
 | 
			
		||||
                {
 | 
			
		||||
                    return new MessageBase(new TimeoutException());
 | 
			
		||||
                }
 | 
			
		||||
                timeoutStatus = reusableTimeout.TimeoutStatus;
 | 
			
		||||
                return timeoutStatus
 | 
			
		||||
                    ? new MessageBase(new TimeoutException())
 | 
			
		||||
                    : new MessageBase(new OperationCanceledException());
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                return new MessageBase(ex);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var result = waitData.Check(ctsTime.Token);
 | 
			
		||||
 | 
			
		||||
            if (result.IsSuccess)
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
                return waitData.CompletedData;
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                return new MessageBase(result);
 | 
			
		||||
                reusableTimeout.Set();
 | 
			
		||||
                timeoutStatus = reusableTimeout.TimeoutStatus;
 | 
			
		||||
                _reusableTimeouts.Return(reusableTimeout);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return waitData.Status == WaitDataStatus.Success
 | 
			
		||||
                ? waitData.CompletedData
 | 
			
		||||
                : new MessageBase(waitData.Check(timeoutStatus));
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            if (!cancellationToken.IsCancellationRequested)
 | 
			
		||||
                await Task.Delay(1000, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
            return new MessageBase(ex);
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            waitLock?.Release();
 | 
			
		||||
            waitData?.SafeDispose();
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static WaitLock GetWaitLock(IClientChannel clientChannel, string dtuId)
 | 
			
		||||
 | 
			
		||||
    private WaitLock GetWaitLock(IClientChannel clientChannel)
 | 
			
		||||
    {
 | 
			
		||||
        WaitLock? waitLock = null;
 | 
			
		||||
        if (clientChannel is IDtuUdpSessionChannel udpSessionChannel)
 | 
			
		||||
        {
 | 
			
		||||
            waitLock = udpSessionChannel.GetLock(dtuId);
 | 
			
		||||
            waitLock = udpSessionChannel.GetLock(this is IDtu dtu1 ? dtu1.DtuId : null);
 | 
			
		||||
        }
 | 
			
		||||
        waitLock ??= clientChannel.GetLock(null);
 | 
			
		||||
        return waitLock;
 | 
			
		||||
@@ -677,7 +629,7 @@ public abstract class DeviceBase : AsyncAndSyncDisposableObject, IDevice
 | 
			
		||||
            DataTypeEnum.UInt32 => await ReadUInt32Async(address, length, cancellationToken: cancellationToken).ConfigureAwait(false),
 | 
			
		||||
            DataTypeEnum.Int64 => await ReadInt64Async(address, length, cancellationToken: cancellationToken).ConfigureAwait(false),
 | 
			
		||||
            DataTypeEnum.UInt64 => await ReadUInt64Async(address, length, cancellationToken: cancellationToken).ConfigureAwait(false),
 | 
			
		||||
            DataTypeEnum.Single => await ReadSingleAsync(address, length, cancellationToken: cancellationToken).ConfigureAwait(false),
 | 
			
		||||
            DataTypeEnum.Float => await ReadSingleAsync(address, length, cancellationToken: cancellationToken).ConfigureAwait(false),
 | 
			
		||||
            DataTypeEnum.Double => await ReadDoubleAsync(address, length, cancellationToken: cancellationToken).ConfigureAwait(false),
 | 
			
		||||
            DataTypeEnum.Decimal => await ReadDecimalAsync(address, length, cancellationToken: cancellationToken).ConfigureAwait(false),
 | 
			
		||||
            _ => new OperResult<Array>(string.Format(AppResource.DataTypeNotSupported, dataType)),
 | 
			
		||||
@@ -703,7 +655,7 @@ public abstract class DeviceBase : AsyncAndSyncDisposableObject, IDevice
 | 
			
		||||
                    DataTypeEnum.UInt32 => await WriteAsync(address, jArray.ToObject<UInt32[]>().AsMemory(), cancellationToken: cancellationToken).ConfigureAwait(false),
 | 
			
		||||
                    DataTypeEnum.Int64 => await WriteAsync(address, jArray.ToObject<Int64[]>().AsMemory(), cancellationToken: cancellationToken).ConfigureAwait(false),
 | 
			
		||||
                    DataTypeEnum.UInt64 => await WriteAsync(address, jArray.ToObject<UInt64[]>().AsMemory(), cancellationToken: cancellationToken).ConfigureAwait(false),
 | 
			
		||||
                    DataTypeEnum.Single => await WriteAsync(address, jArray.ToObject<Single[]>().AsMemory(), cancellationToken: cancellationToken).ConfigureAwait(false),
 | 
			
		||||
                    DataTypeEnum.Float => await WriteAsync(address, jArray.ToObject<Single[]>().AsMemory(), cancellationToken: cancellationToken).ConfigureAwait(false),
 | 
			
		||||
                    DataTypeEnum.Double => await WriteAsync(address, jArray.ToObject<Double[]>().AsMemory(), cancellationToken: cancellationToken).ConfigureAwait(false),
 | 
			
		||||
                    DataTypeEnum.Decimal => await WriteAsync(address, jArray.ToObject<Decimal[]>().AsMemory(), cancellationToken: cancellationToken).ConfigureAwait(false),
 | 
			
		||||
                    _ => new OperResult(string.Format(AppResource.DataTypeNotSupported, dataType)),
 | 
			
		||||
@@ -722,7 +674,7 @@ public abstract class DeviceBase : AsyncAndSyncDisposableObject, IDevice
 | 
			
		||||
                    DataTypeEnum.UInt32 => await WriteAsync(address, value.ToObject<UInt32>(), bitConverter, cancellationToken).ConfigureAwait(false),
 | 
			
		||||
                    DataTypeEnum.Int64 => await WriteAsync(address, value.ToObject<Int64>(), bitConverter, cancellationToken).ConfigureAwait(false),
 | 
			
		||||
                    DataTypeEnum.UInt64 => await WriteAsync(address, value.ToObject<UInt64>(), bitConverter, cancellationToken).ConfigureAwait(false),
 | 
			
		||||
                    DataTypeEnum.Single => await WriteAsync(address, value.ToObject<Single>(), bitConverter, cancellationToken).ConfigureAwait(false),
 | 
			
		||||
                    DataTypeEnum.Float => await WriteAsync(address, value.ToObject<Single>(), bitConverter, cancellationToken).ConfigureAwait(false),
 | 
			
		||||
                    DataTypeEnum.Double => await WriteAsync(address, value.ToObject<Double>(), bitConverter, cancellationToken).ConfigureAwait(false),
 | 
			
		||||
                    DataTypeEnum.Decimal => await WriteAsync(address, value.ToObject<Decimal>(), bitConverter, cancellationToken).ConfigureAwait(false),
 | 
			
		||||
                    _ => new OperResult(string.Format(AppResource.DataTypeNotSupported, dataType)),
 | 
			
		||||
@@ -912,7 +864,7 @@ public abstract class DeviceBase : AsyncAndSyncDisposableObject, IDevice
 | 
			
		||||
    public virtual ValueTask<OperResult> WriteAsync(string address, float value, IThingsGatewayBitConverter bitConverter = null, CancellationToken cancellationToken = default)
 | 
			
		||||
    {
 | 
			
		||||
        bitConverter ??= ThingsGatewayBitConverter.GetTransByAddress(address);
 | 
			
		||||
        return WriteAsync(address, bitConverter.GetBytes(value), DataTypeEnum.Single, cancellationToken);
 | 
			
		||||
        return WriteAsync(address, bitConverter.GetBytes(value), DataTypeEnum.Float, cancellationToken);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
@@ -985,7 +937,7 @@ public abstract class DeviceBase : AsyncAndSyncDisposableObject, IDevice
 | 
			
		||||
    public virtual ValueTask<OperResult> WriteAsync(string address, ReadOnlyMemory<float> value, IThingsGatewayBitConverter bitConverter = null, CancellationToken cancellationToken = default)
 | 
			
		||||
    {
 | 
			
		||||
        bitConverter ??= ThingsGatewayBitConverter.GetTransByAddress(address);
 | 
			
		||||
        return WriteAsync(address, bitConverter.GetBytes(value.Span), DataTypeEnum.Single, cancellationToken);
 | 
			
		||||
        return WriteAsync(address, bitConverter.GetBytes(value.Span), DataTypeEnum.Float, cancellationToken);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
@@ -1068,7 +1020,7 @@ public abstract class DeviceBase : AsyncAndSyncDisposableObject, IDevice
 | 
			
		||||
                Channel.Collects.Remove(this);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        _reusableTimeouts?.SafeDispose();
 | 
			
		||||
        _deviceLogger?.TryDispose();
 | 
			
		||||
        base.Dispose(disposing);
 | 
			
		||||
    }
 | 
			
		||||
@@ -1120,6 +1072,7 @@ public abstract class DeviceBase : AsyncAndSyncDisposableObject, IDevice
 | 
			
		||||
            Channel.Collects.Remove(this);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        _reusableTimeouts?.SafeDispose();
 | 
			
		||||
        _deviceLogger?.TryDispose();
 | 
			
		||||
        base.Dispose(disposing);
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -134,7 +134,7 @@ public static partial class DeviceExtension
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 当状态不是<see cref="WaitDataStatus.Success"/>时返回异常。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static OperResult Check(this AsyncWaitData<MessageBase> waitDataAsync, CancellationToken cancellationToken)
 | 
			
		||||
    public static OperResult Check(this AsyncWaitData<MessageBase> waitDataAsync, bool timeout)
 | 
			
		||||
    {
 | 
			
		||||
        switch (waitDataAsync.Status)
 | 
			
		||||
        {
 | 
			
		||||
@@ -142,7 +142,7 @@ public static partial class DeviceExtension
 | 
			
		||||
                return new();
 | 
			
		||||
 | 
			
		||||
            case WaitDataStatus.Canceled:
 | 
			
		||||
                if (cancellationToken.IsCancellationRequested)
 | 
			
		||||
                if (timeout)
 | 
			
		||||
                {
 | 
			
		||||
                    if (waitDataAsync.CompletedData != null)
 | 
			
		||||
                    {
 | 
			
		||||
 
 | 
			
		||||
@@ -426,9 +426,8 @@ public interface IDevice : IDisposable, IDisposableObject, IAsyncDisposable
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取通道
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="socketId"></param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    OperResult<IClientChannel> GetChannel(string socketId);
 | 
			
		||||
    OperResult<IClientChannel> GetChannel();
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 发送,会经过适配器,可传入socketId,如果为空,则默认通道必须为<see cref="IClientChannel"/>类型
 | 
			
		||||
 
 | 
			
		||||
@@ -45,12 +45,16 @@ public enum DataTypeEnum
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    UInt64,
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    Single,
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 大部分人并不认识Single,但都认识Float
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    Float,
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    Double,
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    Decimal,
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -32,7 +32,7 @@ public static class DataTypeExtensions
 | 
			
		||||
            DataTypeEnum.UInt32 => 4,
 | 
			
		||||
            DataTypeEnum.Int64 => 8,
 | 
			
		||||
            DataTypeEnum.UInt64 => 8,
 | 
			
		||||
            DataTypeEnum.Single => 4,
 | 
			
		||||
            DataTypeEnum.Float => 4,
 | 
			
		||||
            DataTypeEnum.Double => 8,
 | 
			
		||||
            DataTypeEnum.Decimal => 16,
 | 
			
		||||
            _ => 0,
 | 
			
		||||
@@ -57,7 +57,7 @@ public static class DataTypeExtensions
 | 
			
		||||
            TypeCode.UInt32 => DataTypeEnum.UInt32,
 | 
			
		||||
            TypeCode.Int64 => DataTypeEnum.Int64,
 | 
			
		||||
            TypeCode.UInt64 => DataTypeEnum.UInt64,
 | 
			
		||||
            TypeCode.Single => DataTypeEnum.Single,
 | 
			
		||||
            TypeCode.Single => DataTypeEnum.Float,
 | 
			
		||||
            TypeCode.Double => DataTypeEnum.Double,
 | 
			
		||||
            TypeCode.Decimal => DataTypeEnum.Decimal,
 | 
			
		||||
            _ => DataTypeEnum.Object,
 | 
			
		||||
@@ -82,7 +82,7 @@ public static class DataTypeExtensions
 | 
			
		||||
            DataTypeEnum.UInt32 => typeof(uint),
 | 
			
		||||
            DataTypeEnum.Int64 => typeof(long),
 | 
			
		||||
            DataTypeEnum.UInt64 => typeof(ulong),
 | 
			
		||||
            DataTypeEnum.Single => typeof(float),
 | 
			
		||||
            DataTypeEnum.Float => typeof(float),
 | 
			
		||||
            DataTypeEnum.Double => typeof(double),
 | 
			
		||||
            DataTypeEnum.Decimal => typeof(decimal),
 | 
			
		||||
            _ => typeof(object),
 | 
			
		||||
 
 | 
			
		||||
@@ -11,8 +11,8 @@
 | 
			
		||||
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
		<PackageReference Include="Microsoft.Extensions.Localization.Abstractions" Version="$(NET9Version)" />
 | 
			
		||||
		<PackageReference Include="TouchSocket" Version="4.0.0-beta.13" />
 | 
			
		||||
		<PackageReference Include="TouchSocket.SerialPorts" Version="4.0.0-beta.13" />
 | 
			
		||||
		<PackageReference Include="TouchSocket" Version="4.0.0-beta.27" />
 | 
			
		||||
		<PackageReference Include="TouchSocket.SerialPorts" Version="4.0.0-beta.27" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
 
 | 
			
		||||
@@ -56,7 +56,7 @@ public static class ThingsGatewayBitConverterExtension
 | 
			
		||||
                case DataTypeEnum.UInt64:
 | 
			
		||||
                    return byteConverter.GetBytes(value.ToObject<UInt64[]>());
 | 
			
		||||
 | 
			
		||||
                case DataTypeEnum.Single:
 | 
			
		||||
                case DataTypeEnum.Float:
 | 
			
		||||
                    return byteConverter.GetBytes(value.ToObject<Single[]>());
 | 
			
		||||
 | 
			
		||||
                case DataTypeEnum.Double:
 | 
			
		||||
@@ -107,7 +107,7 @@ public static class ThingsGatewayBitConverterExtension
 | 
			
		||||
                case DataTypeEnum.UInt64:
 | 
			
		||||
                    return byteConverter.GetBytes(value.ToObject<UInt64>());
 | 
			
		||||
 | 
			
		||||
                case DataTypeEnum.Single:
 | 
			
		||||
                case DataTypeEnum.Float:
 | 
			
		||||
                    return byteConverter.GetBytes(value.ToObject<Single>());
 | 
			
		||||
 | 
			
		||||
                case DataTypeEnum.Double:
 | 
			
		||||
@@ -333,7 +333,7 @@ public static class ThingsGatewayBitConverterExtension
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            case DataTypeEnum.Single:
 | 
			
		||||
            case DataTypeEnum.Float:
 | 
			
		||||
                if (arrayLength > 1)
 | 
			
		||||
                {
 | 
			
		||||
                    var newVal = byteConverter.ToSingle(buffer, index, arrayLength);
 | 
			
		||||
 
 | 
			
		||||
@@ -8,35 +8,48 @@
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
namespace ThingsGateway.Foundation;
 | 
			
		||||
 | 
			
		||||
public class LinkedCancellationTokenSourceCache : IDisposable
 | 
			
		||||
{
 | 
			
		||||
    private CancellationTokenSource? _cachedCts;
 | 
			
		||||
    private CancellationToken _token1;
 | 
			
		||||
    private CancellationToken _token2;
 | 
			
		||||
    private CancellationToken _token3;
 | 
			
		||||
    private readonly object _lock = new();
 | 
			
		||||
    ~LinkedCancellationTokenSourceCache()
 | 
			
		||||
    {
 | 
			
		||||
        Dispose();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取一个 CancellationTokenSource,它是由两个 token 链接而成的。
 | 
			
		||||
    /// 会尝试复用之前缓存的 CTS,前提是两个 token 仍然相同且未取消。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public CancellationTokenSource GetLinkedTokenSource(CancellationToken token1, CancellationToken token2)
 | 
			
		||||
    public CancellationTokenSource GetLinkedTokenSource(CancellationToken token1, CancellationToken token2, CancellationToken token3 = default)
 | 
			
		||||
    {
 | 
			
		||||
        lock (_lock)
 | 
			
		||||
        {
 | 
			
		||||
            // 如果缓存的 CTS 已经取消或 Dispose,或者 token 不同,重新创建
 | 
			
		||||
            if (_cachedCts?.IsCancellationRequested != false ||
 | 
			
		||||
                !_token1.Equals(token1) || !_token2.Equals(token2))
 | 
			
		||||
                !_token1.Equals(token1) || !_token2.Equals(token2) || !_token3.Equals(token3))
 | 
			
		||||
            {
 | 
			
		||||
#if NET6_0_OR_GREATER
 | 
			
		||||
                if (_cachedCts?.TryReset() != true)
 | 
			
		||||
                {
 | 
			
		||||
                    _cachedCts?.Dispose();
 | 
			
		||||
                    _cachedCts = CancellationTokenSource.CreateLinkedTokenSource(token1, token2, token3);
 | 
			
		||||
                }
 | 
			
		||||
#else
 | 
			
		||||
                _cachedCts?.Dispose();
 | 
			
		||||
 | 
			
		||||
                _cachedCts = CancellationTokenSource.CreateLinkedTokenSource(token1, token2);
 | 
			
		||||
                _cachedCts = CancellationTokenSource.CreateLinkedTokenSource(token1, token2, token3);
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                _token1 = token1;
 | 
			
		||||
                _token2 = token2;
 | 
			
		||||
                _token3 = token3;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return _cachedCts;
 | 
			
		||||
@@ -54,4 +67,3 @@ public class LinkedCancellationTokenSourceCache : IDisposable
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -0,0 +1,81 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
//  此代码版权(除特别声明或在XREF结尾的命名空间的代码)归作者本人若汝棋茗所有
 | 
			
		||||
//  源代码使用协议遵循本仓库的开源协议及附加协议,若本仓库没有设置,则按MIT开源协议授权
 | 
			
		||||
//  CSDN博客:https://blog.csdn.net/qq_40374647
 | 
			
		||||
//  哔哩哔哩视频:https://space.bilibili.com/94253567
 | 
			
		||||
//  Gitee源代码仓库:https://gitee.com/RRQM_Home
 | 
			
		||||
//  Github源代码仓库:https://github.com/RRQM
 | 
			
		||||
//  API首页:https://touchsocket.net/
 | 
			
		||||
//  交流QQ群:234762506
 | 
			
		||||
//  感谢您的下载和使用
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Foundation;
 | 
			
		||||
 | 
			
		||||
using System;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
 | 
			
		||||
public sealed class ReusableCancellationTokenSource : IDisposable
 | 
			
		||||
{
 | 
			
		||||
    private readonly Timer _timer;
 | 
			
		||||
    private CancellationTokenSource? _cts;
 | 
			
		||||
 | 
			
		||||
    public ReusableCancellationTokenSource()
 | 
			
		||||
    {
 | 
			
		||||
        _timer = new Timer(OnTimeout, null, Timeout.Infinite, Timeout.Infinite);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool TimeoutStatus = false;
 | 
			
		||||
 | 
			
		||||
    private void OnTimeout(object? state)
 | 
			
		||||
    {
 | 
			
		||||
        TimeoutStatus = true;
 | 
			
		||||
 | 
			
		||||
        if (_cts?.IsCancellationRequested == false)
 | 
			
		||||
            _cts?.Cancel();
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private readonly LinkedCancellationTokenSourceCache _linkedCtsCache = new();
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取一个 CTS,并启动超时
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public CancellationTokenSource GetTokenSource(long timeout, CancellationToken external1 = default, CancellationToken external2 = default, CancellationToken external3 = default)
 | 
			
		||||
    {
 | 
			
		||||
        TimeoutStatus = false;
 | 
			
		||||
 | 
			
		||||
        // 创建新的 CTS
 | 
			
		||||
        _cts = _linkedCtsCache.GetLinkedTokenSource(external1, external2, external3);
 | 
			
		||||
 | 
			
		||||
        // 启动 Timer
 | 
			
		||||
        _timer.Change(timeout, Timeout.Infinite);
 | 
			
		||||
 | 
			
		||||
        return _cts;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public void Set()
 | 
			
		||||
    {
 | 
			
		||||
        _timer?.Change(Timeout.Infinite, Timeout.Infinite);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 手动取消
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public void Cancel()
 | 
			
		||||
    {
 | 
			
		||||
        _cts?.SafeCancel();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void Dispose()
 | 
			
		||||
    {
 | 
			
		||||
        _cts?.SafeCancel();
 | 
			
		||||
        _cts?.SafeDispose();
 | 
			
		||||
        _linkedCtsCache.SafeDispose();
 | 
			
		||||
        _timer.SafeDispose();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -3,7 +3,7 @@
 | 
			
		||||
	<Import Project="..\..\PackNuget.props" />
 | 
			
		||||
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
 | 
			
		||||
		<TargetFrameworks>net8.0</TargetFrameworks>
 | 
			
		||||
		
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -214,6 +214,17 @@ public class ControlController : ControllerBase, IRpcServer
 | 
			
		||||
        return GlobalData.VariableRuntimeService.InsertTestDataAsync(testVariableCount, testDeviceCount, slaveUrl, businessEnable, restart);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 增加测试Dtu数据
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [HttpPost("insertTestDtuData")]
 | 
			
		||||
    [DisplayName("增加测试Dtu数据")]
 | 
			
		||||
    [TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Post)]
 | 
			
		||||
    public Task InsertTestDtuDataAsync(int testDeviceCount, string slaveUrl, bool restart = true)
 | 
			
		||||
    {
 | 
			
		||||
        return GlobalData.VariableRuntimeService.InsertTestDtuDataAsync(testDeviceCount, slaveUrl, restart);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 确认实时报警
 | 
			
		||||
    /// </summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -338,6 +338,14 @@ public partial class ManagementController : ControllerBase, IRpcServer
 | 
			
		||||
    public Task InsertTestDataAsync([FromBody] InsertTestDataInput input) =>
 | 
			
		||||
        App.GetService<IVariablePageService>().InsertTestDataAsync(input.TestVariableCount, input.TestDeviceCount, input.SlaveUrl, input.BusinessEnable, input.Restart);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    [HttpPost]
 | 
			
		||||
    [TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Post)]
 | 
			
		||||
    public Task InsertTestDtuDataAsync([FromBody] InsertTestDtuDataInput input) =>
 | 
			
		||||
        App.GetService<IVariablePageService>().InsertTestDtuDataAsync(input.TestDeviceCount, input.SlaveUrl, input.Restart);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    [HttpPost]
 | 
			
		||||
    [TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Post)]
 | 
			
		||||
    public Task<bool> IsRedundantDeviceAsync(long id) =>
 | 
			
		||||
@@ -628,7 +636,12 @@ public class InsertTestDataInput
 | 
			
		||||
    public bool BusinessEnable { get; set; }
 | 
			
		||||
    public bool Restart { get; set; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class InsertTestDtuDataInput
 | 
			
		||||
{
 | 
			
		||||
    public int TestDeviceCount { get; set; }
 | 
			
		||||
    public string SlaveUrl { get; set; }
 | 
			
		||||
    public bool Restart { get; set; }
 | 
			
		||||
}
 | 
			
		||||
public class LastLogDataInput
 | 
			
		||||
{
 | 
			
		||||
    public string File { get; set; }
 | 
			
		||||
 
 | 
			
		||||
@@ -55,7 +55,15 @@ public abstract partial class CollectBase : DriverBase
 | 
			
		||||
    /// 特殊方法
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public List<DriverMethodInfo>? DriverMethodInfos { get; private set; }
 | 
			
		||||
 | 
			
		||||
    protected virtual bool IsRuntimeSourceValid(VariableRuntime a)
 | 
			
		||||
    {
 | 
			
		||||
        //筛选特殊变量地址
 | 
			
		||||
        //1、DeviceStatus
 | 
			
		||||
        return !a.RegisterAddress.Equals(nameof(DeviceRuntime.DeviceStatus), StringComparison.OrdinalIgnoreCase) &&
 | 
			
		||||
        !a.RegisterAddress.Equals("Script", StringComparison.OrdinalIgnoreCase) &&
 | 
			
		||||
        !a.RegisterAddress.Equals("ScriptRead", StringComparison.OrdinalIgnoreCase)
 | 
			
		||||
        ;
 | 
			
		||||
    }
 | 
			
		||||
    public override async Task AfterVariablesChangedAsync(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        LogMessage?.LogInformation("Refresh variable");
 | 
			
		||||
@@ -84,21 +92,13 @@ public abstract partial class CollectBase : DriverBase
 | 
			
		||||
            && string.IsNullOrEmpty(it.OtherMethod)
 | 
			
		||||
            && !string.IsNullOrEmpty(it.RegisterAddress));
 | 
			
		||||
 | 
			
		||||
        //筛选特殊变量地址
 | 
			
		||||
        //1、DeviceStatus
 | 
			
		||||
        Func<VariableRuntime, bool> source = (a =>
 | 
			
		||||
        {
 | 
			
		||||
            return !a.RegisterAddress.Equals(nameof(DeviceRuntime.DeviceStatus), StringComparison.OrdinalIgnoreCase) &&
 | 
			
		||||
            !a.RegisterAddress.Equals("Script", StringComparison.OrdinalIgnoreCase) &&
 | 
			
		||||
            !a.RegisterAddress.Equals("ScriptRead", StringComparison.OrdinalIgnoreCase)
 | 
			
		||||
            ;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        var now = DateTime.Now;
 | 
			
		||||
#pragma warning disable CA1851
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
            currentDevice.VariableScriptReads = tags.Where(a => !source(a)).Select(a =>
 | 
			
		||||
            currentDevice.VariableScriptReads = tags.Where(a => !IsRuntimeSourceValid(a)).Select(a =>
 | 
			
		||||
            {
 | 
			
		||||
                var data = new VariableScriptRead();
 | 
			
		||||
                data.VariableRuntime = a;
 | 
			
		||||
@@ -111,9 +111,9 @@ public abstract partial class CollectBase : DriverBase
 | 
			
		||||
            // 如果出现异常,记录日志并初始化 VariableSourceReads 属性为新实例
 | 
			
		||||
            currentDevice.VariableScriptReads = new();
 | 
			
		||||
            LogMessage?.LogWarning(ex, string.Format(AppResource.VariablePackError, ex.Message));
 | 
			
		||||
            tags.Where(a => !source(a)).ForEach(a => a.SetValue(null, now, isOnline: false));
 | 
			
		||||
            tags.Where(a => !IsRuntimeSourceValid(a)).ForEach(a => a.SetValue(null, now, isOnline: false));
 | 
			
		||||
        }
 | 
			
		||||
        var variableReads = tags.Where(source).ToList();
 | 
			
		||||
        var variableReads = tags.Where(IsRuntimeSourceValid).ToList();
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            // 将打包后的结果存储在当前设备的 VariableSourceReads 属性中
 | 
			
		||||
@@ -482,6 +482,7 @@ public abstract partial class CollectBase : DriverBase
 | 
			
		||||
 | 
			
		||||
    protected virtual Task TestOnline(object? state, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        CurrentDevice.SetDeviceStatus(TimerX.Now, false, null);
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,53 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
//  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
 | 
			
		||||
//  此代码版权(除特别声明外的代码)归作者本人Diego所有
 | 
			
		||||
//  源代码使用协议遵循本仓库的开源协议及附加协议
 | 
			
		||||
//  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
 | 
			
		||||
//  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
 | 
			
		||||
//  使用文档:https://thingsgateway.cn/
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 插件配置项
 | 
			
		||||
/// <br></br>
 | 
			
		||||
/// 使用<see cref="DynamicPropertyAttribute"/> 标识所需的配置属性
 | 
			
		||||
/// </summary>
 | 
			
		||||
public class MempryDevicePropertyBase : CollectPropertyBase
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// <para></para>
 | 
			
		||||
/// 采集插件,继承实现不同PLC通讯
 | 
			
		||||
/// <para></para>
 | 
			
		||||
/// </summary>
 | 
			
		||||
public class MempryDevice : CollectBase
 | 
			
		||||
{
 | 
			
		||||
    private MempryDevicePropertyBase _driverPropertyBase = new MempryDevicePropertyBase();
 | 
			
		||||
    public override CollectPropertyBase CollectProperties => _driverPropertyBase;
 | 
			
		||||
 | 
			
		||||
#if !Management
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 是否连接成功
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public override bool IsConnected()
 | 
			
		||||
    {
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected override Task<List<VariableSourceRead>> ProtectedLoadSourceReadAsync(List<VariableRuntime> deviceVariables)
 | 
			
		||||
    {
 | 
			
		||||
        return Task.FromResult(new List<VariableSourceRead>());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected override bool IsRuntimeSourceValid(VariableRuntime a)
 | 
			
		||||
    {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
@@ -409,7 +409,7 @@ internal sealed class DeviceThreadManage : IAsyncDisposable, IDeviceThreadManage
 | 
			
		||||
                 // 查找具有指定设备ID的驱动程序对象
 | 
			
		||||
                 if (Drivers.TryRemove(deviceId, out var driver))
 | 
			
		||||
                 {
 | 
			
		||||
                     driver.CurrentDevice.SetDeviceStatus(now, false, "Communication connection has been removed");
 | 
			
		||||
                     driver.CurrentDevice.SetDeviceStatus(now, true, "Communication connection has been removed");
 | 
			
		||||
                     if (IsCollectChannel == true)
 | 
			
		||||
                     {
 | 
			
		||||
                         foreach (var a in driver.IdVariableRuntimes)
 | 
			
		||||
 
 | 
			
		||||
@@ -221,7 +221,8 @@ public interface IManagementRpcServer : IRpcServer
 | 
			
		||||
 | 
			
		||||
    [DmtpRpc]
 | 
			
		||||
    Task InsertTestDataAsync(int testVariableCount, int testDeviceCount, string slaveUrl, bool businessEnable, bool restart);
 | 
			
		||||
 | 
			
		||||
    [DmtpRpc]
 | 
			
		||||
    Task InsertTestDtuDataAsync(int testDeviceCount, string slaveUrl, bool restart);
 | 
			
		||||
    [DmtpRpc]
 | 
			
		||||
    Task<bool> IsRedundantDeviceAsync(long id);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -179,6 +179,10 @@ public partial class ManagementRpcServer : IRpcServer, IManagementRpcServer, IBa
 | 
			
		||||
    public Task InsertTestDataAsync(int testVariableCount, int testDeviceCount, string slaveUrl, bool businessEnable, bool restart) =>
 | 
			
		||||
        App.GetService<IVariablePageService>().InsertTestDataAsync(testVariableCount, testDeviceCount, slaveUrl, businessEnable, restart);
 | 
			
		||||
 | 
			
		||||
    public Task InsertTestDtuDataAsync(int testDeviceCount, string slaveUrl, bool restart) =>
 | 
			
		||||
        App.GetService<IVariablePageService>().InsertTestDtuDataAsync(testDeviceCount, slaveUrl, restart);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public Task<bool> IsRedundantDeviceAsync(long id) =>
 | 
			
		||||
        App.GetService<IDevicePageService>().IsRedundantDeviceAsync(id);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -93,7 +93,7 @@ public partial class ManagementTask : AsyncDisposableObject
 | 
			
		||||
        var config = new TouchSocketConfig()
 | 
			
		||||
               .SetRemoteIPHost(_managementOptions.ServerUri)
 | 
			
		||||
               .SetAdapterOption(new AdapterOption() { MaxPackageSize = 1024 * 1024 * 1024 })
 | 
			
		||||
               .SetDmtpOption(new DmtpOption() { VerifyToken = _managementOptions.VerifyToken })
 | 
			
		||||
               .SetDmtpOption(a => a.VerifyToken = _managementOptions.VerifyToken)
 | 
			
		||||
               .ConfigureContainer(a =>
 | 
			
		||||
               {
 | 
			
		||||
                   a.AddDmtpRouteService();//添加路由策略
 | 
			
		||||
@@ -134,7 +134,7 @@ public partial class ManagementTask : AsyncDisposableObject
 | 
			
		||||
                   .SetTick(TimeSpan.FromMilliseconds(_managementOptions.HeartbeatInterval))
 | 
			
		||||
                   .SetMaxFailCount(3);
 | 
			
		||||
 | 
			
		||||
                   a.AddDmtpHandshakedPlugin(async () =>
 | 
			
		||||
                   a.AddDmtpCreatedChannelPlugin(async () =>
 | 
			
		||||
                   {
 | 
			
		||||
                       try
 | 
			
		||||
                       {
 | 
			
		||||
@@ -157,7 +157,7 @@ public partial class ManagementTask : AsyncDisposableObject
 | 
			
		||||
        var config = new TouchSocketConfig()
 | 
			
		||||
               .SetListenIPHosts(_managementOptions.ServerUri)
 | 
			
		||||
                   .SetAdapterOption(new AdapterOption() { MaxPackageSize = 1024 * 1024 * 1024 })
 | 
			
		||||
               .SetDmtpOption(new DmtpOption() { VerifyToken = _managementOptions.VerifyToken })
 | 
			
		||||
               .SetDmtpOption(a => a.VerifyToken = _managementOptions.VerifyToken)
 | 
			
		||||
               .ConfigureContainer(a =>
 | 
			
		||||
               {
 | 
			
		||||
                   a.AddDmtpRouteService();//添加路由策略
 | 
			
		||||
 
 | 
			
		||||
@@ -332,7 +332,7 @@ internal sealed class RedundancyTask : IRpcDriver, IAsyncDisposable
 | 
			
		||||
        var config = new TouchSocketConfig()
 | 
			
		||||
               .SetRemoteIPHost(redundancy.MasterUri)
 | 
			
		||||
               .SetAdapterOption(new AdapterOption() { MaxPackageSize = 0x20000000 })
 | 
			
		||||
               .SetDmtpOption(new DmtpOption() { VerifyToken = redundancy.VerifyToken })
 | 
			
		||||
               .SetDmtpOption(a => a.VerifyToken = redundancy.VerifyToken)
 | 
			
		||||
               .ConfigureContainer(a =>
 | 
			
		||||
               {
 | 
			
		||||
                   a.AddLogger(LogMessage);
 | 
			
		||||
@@ -377,7 +377,7 @@ internal sealed class RedundancyTask : IRpcDriver, IAsyncDisposable
 | 
			
		||||
        var config = new TouchSocketConfig()
 | 
			
		||||
               .SetListenIPHosts(redundancy.MasterUri)
 | 
			
		||||
               .SetAdapterOption(new AdapterOption() { MaxPackageSize = 0x20000000 })
 | 
			
		||||
               .SetDmtpOption(new DmtpOption() { VerifyToken = redundancy.VerifyToken })
 | 
			
		||||
               .SetDmtpOption(a => a.VerifyToken = redundancy.VerifyToken)
 | 
			
		||||
               .ConfigureContainer(a =>
 | 
			
		||||
               {
 | 
			
		||||
                   a.AddLogger(LogMessage);
 | 
			
		||||
 
 | 
			
		||||
@@ -35,5 +35,6 @@ namespace ThingsGateway.Gateway.Application
 | 
			
		||||
        Task<OperResult<object>> OnWriteVariableAsync(long id, string writeData);
 | 
			
		||||
        Task<Dictionary<string, ImportPreviewOutputBase>> ImportVariableAsync(IBrowserFile a, bool restart);
 | 
			
		||||
        Task<Dictionary<string, ImportPreviewOutputBase>> ImportVariableFileAsync(string filePath, bool restart);
 | 
			
		||||
        Task InsertTestDtuDataAsync(int deviceCount, string slaveUrl, bool restart);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -111,4 +111,5 @@ internal interface IVariableService
 | 
			
		||||
    Task<Dictionary<string, ImportPreviewOutputBase>> PreviewAsync(string filePath);
 | 
			
		||||
    Task<HashSet<long>> ImportVariableAsync(List<Variable> upData, List<Variable> insertData);
 | 
			
		||||
    Task<Dictionary<string, ImportPreviewOutputBase>> PreviewAsync(IFormFile browserFile);
 | 
			
		||||
    Task<(List<Channel>, List<Device>, List<Variable>)> InsertTestDtuDataAsync(int deviceCount, string slaveUrl = "127.0.0.1:502");
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -423,6 +423,45 @@ public class VariableRuntimeService : IVariableRuntimeService
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task InsertTestDtuDataAsync(int deviceCount, string slaveUrl, bool restart)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            // await WaitLock.WaitAsync().ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            var datas = await GlobalData.VariableService.InsertTestDtuDataAsync(deviceCount, slaveUrl).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            {
 | 
			
		||||
                var newChannelRuntimes = datas.Item1.AdaptListChannelRuntime();
 | 
			
		||||
 | 
			
		||||
                //批量修改之后,需要重新加载通道
 | 
			
		||||
                RuntimeServiceHelper.Init(newChannelRuntimes);
 | 
			
		||||
 | 
			
		||||
                {
 | 
			
		||||
                    var newDeviceRuntimes = datas.Item2.AdaptListDeviceRuntime();
 | 
			
		||||
 | 
			
		||||
                    RuntimeServiceHelper.Init(newDeviceRuntimes);
 | 
			
		||||
                }
 | 
			
		||||
                {
 | 
			
		||||
                    var newVariableRuntimes = datas.Item3.AdaptListVariableRuntime();
 | 
			
		||||
                    RuntimeServiceHelper.Init(newVariableRuntimes);
 | 
			
		||||
                }
 | 
			
		||||
                //根据条件重启通道线程
 | 
			
		||||
 | 
			
		||||
                if (restart)
 | 
			
		||||
                {
 | 
			
		||||
                    await GlobalData.ChannelThreadManage.RestartChannelAsync(newChannelRuntimes).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                    await RuntimeServiceHelper.ChangedDriverAsync(_logger).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            //WaitLock.Release();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Task<Dictionary<string, ImportPreviewOutputBase>> PreviewAsync(IBrowserFile browserFile)
 | 
			
		||||
    {
 | 
			
		||||
        return GlobalData.VariableService.PreviewAsync(browserFile);
 | 
			
		||||
 
 | 
			
		||||
@@ -235,6 +235,144 @@ internal sealed class VariableService : BaseService<Variable>, IVariableService
 | 
			
		||||
        return (newChannels, newDevices, newVariables);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public async Task<(List<Channel>, List<Device>, List<Variable>)> InsertTestDtuDataAsync(int deviceCount, string slaveUrl = "127.0.0.1:502")
 | 
			
		||||
    {
 | 
			
		||||
        if (slaveUrl.IsNullOrWhiteSpace()) slaveUrl = "127.0.0.1:502";
 | 
			
		||||
        List<Channel> newChannels = new();
 | 
			
		||||
        List<Device> newDevices = new();
 | 
			
		||||
        List<Variable> newVariables = new();
 | 
			
		||||
 | 
			
		||||
        ManageHelper.CheckChannelCount(deviceCount);
 | 
			
		||||
        ManageHelper.CheckDeviceCount(deviceCount);
 | 
			
		||||
        ManageHelper.CheckVariableCount(deviceCount);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        //DTU
 | 
			
		||||
 | 
			
		||||
        for (int i = 0; i < deviceCount; i++)
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
            Channel serviceChannel = new Channel();
 | 
			
		||||
            Device serviceDevice = new Device();
 | 
			
		||||
 | 
			
		||||
            {
 | 
			
		||||
                var id = CommonUtils.GetSingleId();
 | 
			
		||||
                var name = $"modbusSlaveChannel{id}";
 | 
			
		||||
                serviceChannel.ChannelType = ChannelTypeEnum.TcpClient;
 | 
			
		||||
                serviceChannel.Name = name;
 | 
			
		||||
                serviceChannel.Enable = true;
 | 
			
		||||
                serviceChannel.Id = id;
 | 
			
		||||
                serviceChannel.CreateUserId = UserManager.UserId;
 | 
			
		||||
                serviceChannel.CreateOrgId = UserManager.OrgId;
 | 
			
		||||
                serviceChannel.RemoteUrl = "127.0.0.1:502";
 | 
			
		||||
                serviceChannel.DtuId = name;
 | 
			
		||||
                serviceChannel.Heartbeat = "ThingsGateway.Plugin.Modbus";
 | 
			
		||||
                serviceChannel.PluginName = "ThingsGateway.Plugin.Modbus.ModbusSlave";
 | 
			
		||||
                newChannels.Add(serviceChannel);
 | 
			
		||||
            }
 | 
			
		||||
            {
 | 
			
		||||
                var id = CommonUtils.GetSingleId();
 | 
			
		||||
                var name = $"modbusSlaveDevice{id}";
 | 
			
		||||
                serviceDevice.Name = name;
 | 
			
		||||
                serviceDevice.Id = id;
 | 
			
		||||
                serviceDevice.CreateUserId = UserManager.UserId;
 | 
			
		||||
                serviceDevice.CreateOrgId = UserManager.OrgId;
 | 
			
		||||
                serviceDevice.ChannelId = serviceChannel.Id;
 | 
			
		||||
                serviceDevice.IntervalTime = "1000";
 | 
			
		||||
                newDevices.Add(serviceDevice);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        //SERVICE
 | 
			
		||||
        var dtuids = newChannels.Select(a => a.Name).ToList();
 | 
			
		||||
        Channel channel = new Channel();
 | 
			
		||||
        {
 | 
			
		||||
            var id = CommonUtils.GetSingleId();
 | 
			
		||||
            var name = $"modbusChannel{id}";
 | 
			
		||||
            channel.ChannelType = ChannelTypeEnum.TcpService;
 | 
			
		||||
            channel.Name = name;
 | 
			
		||||
            channel.Id = id;
 | 
			
		||||
            channel.CreateUserId = UserManager.UserId;
 | 
			
		||||
            channel.CreateOrgId = UserManager.OrgId;
 | 
			
		||||
            channel.BindUrl = slaveUrl;
 | 
			
		||||
            channel.Heartbeat = "ThingsGateway.Plugin.Modbus";
 | 
			
		||||
            channel.PluginName = "ThingsGateway.Plugin.Modbus.ModbusMaster";
 | 
			
		||||
            //动态插件属性默认
 | 
			
		||||
            newChannels.Add(channel);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        foreach (var item in dtuids)
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
            Device device = new Device();
 | 
			
		||||
            {
 | 
			
		||||
                var id = CommonUtils.GetSingleId();
 | 
			
		||||
                var name = $"modbusDevice{id}";
 | 
			
		||||
                device.Name = name;
 | 
			
		||||
                device.Id = id;
 | 
			
		||||
                device.ChannelId = channel.Id;
 | 
			
		||||
                device.CreateUserId = UserManager.UserId;
 | 
			
		||||
                device.CreateOrgId = UserManager.OrgId;
 | 
			
		||||
                device.IntervalTime = "1000";
 | 
			
		||||
                device.DevicePropertys = new Dictionary<string, string>()
 | 
			
		||||
                {
 | 
			
		||||
                    {
 | 
			
		||||
                        nameof(CollectFoundationDtuPackPropertyBase.DtuId),item
 | 
			
		||||
                    }
 | 
			
		||||
                };
 | 
			
		||||
                //动态插件属性默认
 | 
			
		||||
                newDevices.Add(device);
 | 
			
		||||
            }
 | 
			
		||||
            {
 | 
			
		||||
                var address = $"400001";
 | 
			
		||||
                var id = CommonUtils.GetSingleId();
 | 
			
		||||
                var name = $"modbus{address}";
 | 
			
		||||
                Variable variable = new Variable();
 | 
			
		||||
                variable.DataType = DataTypeEnum.Int16;
 | 
			
		||||
                variable.Name = name;
 | 
			
		||||
                variable.Id = id;
 | 
			
		||||
                variable.CreateOrgId = UserManager.OrgId;
 | 
			
		||||
                variable.CreateUserId = UserManager.UserId;
 | 
			
		||||
                variable.DeviceId = device.Id;
 | 
			
		||||
                variable.RegisterAddress = address;
 | 
			
		||||
                newVariables.Add(variable);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        using var db = GetDB();
 | 
			
		||||
 | 
			
		||||
        var result = await db.UseTranAsync(async () =>
 | 
			
		||||
        {
 | 
			
		||||
            if (GlobalData.HardwareJob.HardwareInfo.MachineInfo.AvailableMemory < 2 * 1024 * 1024 || WebEnableVariable.WebEnable == false)
 | 
			
		||||
            {
 | 
			
		||||
                await db.BulkCopyAsync(newChannels, 10000).ConfigureAwait(false);
 | 
			
		||||
                await db.BulkCopyAsync(newDevices, 10000).ConfigureAwait(false);
 | 
			
		||||
                await db.BulkCopyAsync(newVariables, 10000).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                await db.BulkCopyAsync(newChannels, 200000).ConfigureAwait(false);
 | 
			
		||||
                await db.BulkCopyAsync(newDevices, 200000).ConfigureAwait(false);
 | 
			
		||||
                await db.BulkCopyAsync(newVariables, 200000).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
        }).ConfigureAwait(false);
 | 
			
		||||
        if (result.IsSuccess)//如果成功了
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            throw new(result.ErrorMessage, result.ErrorException);
 | 
			
		||||
        }
 | 
			
		||||
        return (newChannels, newDevices, newVariables);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    #endregion 测试
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -11,9 +11,9 @@
 | 
			
		||||
		<PackageReference Include="Riok.Mapperly" Version="4.2.1" ExcludeAssets="runtime" PrivateAssets="all" />
 | 
			
		||||
		<PackageReference Include="Rougamo.Fody" Version="5.0.1" />
 | 
			
		||||
		<PackageReference Include="System.Linq.Async" Version="6.0.3" />
 | 
			
		||||
		<PackageReference Include="TouchSocket.Dmtp" Version="4.0.0-beta.13" />
 | 
			
		||||
		<!--<PackageReference Include="TouchSocket.WebApi.Swagger" Version="4.0.0-beta.13" />-->
 | 
			
		||||
		<PackageReference Include="TouchSocket.WebApi" Version="4.0.0-beta.13" />
 | 
			
		||||
		<PackageReference Include="TouchSocket.Dmtp" Version="4.0.0-beta.27" />
 | 
			
		||||
		<!--<PackageReference Include="TouchSocket.WebApi.Swagger" Version="4.0.0-beta.27" />-->
 | 
			
		||||
		<PackageReference Include="TouchSocket.WebApi" Version="4.0.0-beta.27" />
 | 
			
		||||
		<PackageReference Include="ThingsGateway.Authentication" Version="$(AuthenticationVersion)" />
 | 
			
		||||
		<!--<ProjectReference Include="..\..\PluginPro\ThingsGateway.Authentication\ThingsGateway.Authentication.csproj" />-->
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -48,7 +48,7 @@
 | 
			
		||||
}
 | 
			
		||||
    /*切换高度*/
 | 
			
		||||
    .quickactions-list.is-open {
 | 
			
		||||
        height: 300px;
 | 
			
		||||
        height: 200px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
.quickactions-header {
 | 
			
		||||
 
 | 
			
		||||
@@ -243,6 +243,7 @@
 | 
			
		||||
    "BusinessEnable": "BusinessEnable",
 | 
			
		||||
    "SlaveUrl": "SlaveUrl",
 | 
			
		||||
    "Test": "Addition of test variables",
 | 
			
		||||
    "TestDtu": "Addition of test dtu variables",
 | 
			
		||||
    "TestDeviceCount": "TestDeviceCount",
 | 
			
		||||
    "TestVariableCount": "TestVariableCount",
 | 
			
		||||
    "WriteValue": "WriteValue",
 | 
			
		||||
 
 | 
			
		||||
@@ -243,6 +243,7 @@
 | 
			
		||||
    "BusinessEnable": "添加业务设备",
 | 
			
		||||
    "SlaveUrl": "服务端Url",
 | 
			
		||||
    "Test": "一键添加测试变量",
 | 
			
		||||
    "TestDtu": "一键添加Dtu测试变量",
 | 
			
		||||
    "TestDeviceCount": "采集设备数量",
 | 
			
		||||
    "TestVariableCount": "变量数量",
 | 
			
		||||
    "WriteValue": "写入值",
 | 
			
		||||
 
 | 
			
		||||
@@ -76,18 +76,34 @@ public partial class DeviceRuntimeInfo1 : IDisposable
 | 
			
		||||
        {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        await DialogService.Show(new DialogOption()
 | 
			
		||||
        {
 | 
			
		||||
            IsScrolling = false,
 | 
			
		||||
            ShowMaximizeButton = true,
 | 
			
		||||
            Size = Size.ExtraExtraLarge,
 | 
			
		||||
            Title = DeviceRuntime.Name,
 | 
			
		||||
            Component = BootstrapDynamicComponent.CreateComponent(driver, new Dictionary<string, object?>()
 | 
			
		||||
        var renderFragment = BootstrapDynamicComponent.CreateComponent(driver, new Dictionary<string, object?>()
 | 
			
		||||
        {
 | 
			
		||||
            {nameof(IDriverUIBase.Driver),DeviceRuntime.Driver},
 | 
			
		||||
        })
 | 
			
		||||
        });
 | 
			
		||||
        }).Render();
 | 
			
		||||
        if (renderFragment != null)
 | 
			
		||||
        {
 | 
			
		||||
            var option = new WinBoxOption()
 | 
			
		||||
            {
 | 
			
		||||
                Title = DeviceRuntime.Name,
 | 
			
		||||
                ContentTemplate = renderFragment,
 | 
			
		||||
                Max = false,
 | 
			
		||||
                Width = "80%",
 | 
			
		||||
                Height = "80%",
 | 
			
		||||
                Top = "0%",
 | 
			
		||||
                Left = "10%",
 | 
			
		||||
                Background = "var(--bb-primary-color)",
 | 
			
		||||
                Overflow = true
 | 
			
		||||
            };
 | 
			
		||||
            await WinBoxService.Show(option);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Inject]
 | 
			
		||||
    [NotNull]
 | 
			
		||||
    private WinBoxService? WinBoxService { get; set; }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
    [Inject]
 | 
			
		||||
 
 | 
			
		||||
@@ -48,8 +48,22 @@ public partial class GatewayMonitorPage
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                VariableRuntimes = deviceRuntime.Driver?.IdVariableRuntimes?.Where(a => a.Value != null)
 | 
			
		||||
                if (deviceRuntime.Driver == null)
 | 
			
		||||
                {
 | 
			
		||||
                    _ = Task.Run(async () =>
 | 
			
		||||
                    {
 | 
			
		||||
                        await Task.Delay(2000);
 | 
			
		||||
                        VariableRuntimes = deviceRuntime.Driver?.IdVariableRuntimes?.Where(a => a.Value != null)
 | 
			
		||||
.Select(a => a.Value) ?? Enumerable.Empty<VariableRuntime>();
 | 
			
		||||
                        await InvokeAsync(StateHasChanged);
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    VariableRuntimes = deviceRuntime.Driver?.IdVariableRuntimes?.Where(a => a.Value != null)
 | 
			
		||||
.Select(a => a.Value) ?? Enumerable.Empty<VariableRuntime>();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
            ChannelRuntimes = Enumerable.Repeat(deviceRuntime.ChannelRuntime, 1);
 | 
			
		||||
            DeviceRuntimes = Enumerable.Repeat(deviceRuntime, 1);
 | 
			
		||||
 
 | 
			
		||||
@@ -127,7 +127,7 @@
 | 
			
		||||
        @if (WebsiteOption.Value.Demo || App.HostEnvironment.IsDevelopment())
 | 
			
		||||
        {
 | 
			
		||||
            <PopConfirmButton Color=Color.Warning Text="@Localizer["Test"]" IsKeepDisabled=@(!AuthorizeButton(AdminOperConst.Add))
 | 
			
		||||
                              IsAsync OnConfirm=@(InsertTestDataAsync)>
 | 
			
		||||
                              IsAsync OnConfirm=@(InsertTestDataAsync) class="me-1">
 | 
			
		||||
 | 
			
		||||
                <BodyTemplate>
 | 
			
		||||
                    <BootstrapInput @bind-Value=TestVariableCount ShowLabel="true" ShowLabelTooltip="true" />
 | 
			
		||||
@@ -136,6 +136,16 @@
 | 
			
		||||
                </BodyTemplate>
 | 
			
		||||
 | 
			
		||||
            </PopConfirmButton>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            <PopConfirmButton Color=Color.Warning Text="@Localizer["TestDtu"]" IsKeepDisabled=@(!AuthorizeButton(AdminOperConst.Add))
 | 
			
		||||
                              IsAsync OnConfirm=@(InsertTestDtuDataAsync)>
 | 
			
		||||
 | 
			
		||||
                <BodyTemplate>
 | 
			
		||||
                    <BootstrapInput @bind-Value=TestDeviceCount ShowLabel="true" ShowLabelTooltip="true" />
 | 
			
		||||
                </BodyTemplate>
 | 
			
		||||
 | 
			
		||||
            </PopConfirmButton>
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    </TableToolbarTemplate>
 | 
			
		||||
 
 | 
			
		||||
@@ -28,15 +28,7 @@ public partial class VariableRuntimeInfo : IDisposable
 | 
			
		||||
 | 
			
		||||
    [Parameter]
 | 
			
		||||
    public IEnumerable<VariableRuntime>? Items { get; set; } = Enumerable.Empty<VariableRuntime>();
 | 
			
		||||
    private IEnumerable<VariableRuntime>? _previousItemsRef;
 | 
			
		||||
    protected override async Task OnParametersSetAsync()
 | 
			
		||||
    {
 | 
			
		||||
        if (!ReferenceEquals(_previousItemsRef, Items))
 | 
			
		||||
        {
 | 
			
		||||
            _previousItemsRef = Items;
 | 
			
		||||
            await Refresh(null);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
    private static void BeforeShowEditDialogCallback(ITableEditDialogOption<VariableRuntime> tableEditDialogOption)
 | 
			
		||||
@@ -481,6 +473,32 @@ finally
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    private async Task InsertTestDtuDataAsync()
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                await Task.Run(() => VariablePageService.InsertTestDtuDataAsync(TestDeviceCount, SlaveUrl, AutoRestartThread));
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
                await InvokeAsync(async () =>
 | 
			
		||||
                {
 | 
			
		||||
                    await ToastService.Default();
 | 
			
		||||
                    await table.QueryAsync();
 | 
			
		||||
                    StateHasChanged();
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            await InvokeAsync(async () => await ToastService.Warn(ex));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    [Parameter]
 | 
			
		||||
    public bool AutoRestartThread { get; set; }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,207 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
//  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
 | 
			
		||||
//  此代码版权(除特别声明外的代码)归作者本人Diego所有
 | 
			
		||||
//  源代码使用协议遵循本仓库的开源协议及附加协议
 | 
			
		||||
//  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
 | 
			
		||||
//  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
 | 
			
		||||
//  使用文档:https://kimdiego2098.github.io/
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using BenchmarkConsoleApp;
 | 
			
		||||
 | 
			
		||||
using BenchmarkDotNet.Attributes;
 | 
			
		||||
using BenchmarkDotNet.Diagnosers;
 | 
			
		||||
 | 
			
		||||
using HslCommunication.ModBus;
 | 
			
		||||
 | 
			
		||||
using System.IO.Pipelines;
 | 
			
		||||
using System.Net.Sockets;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.Foundation.Modbus;
 | 
			
		||||
 | 
			
		||||
using TouchSocket.Core;
 | 
			
		||||
using TouchSocket.Modbus;
 | 
			
		||||
 | 
			
		||||
using IModbusMaster = NModbus.IModbusMaster;
 | 
			
		||||
using ModbusMaster = ThingsGateway.Foundation.Modbus.ModbusMaster;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Foundation;
 | 
			
		||||
 | 
			
		||||
[MemoryDiagnoser]
 | 
			
		||||
public class ModbusBenchmark : IDisposable
 | 
			
		||||
{
 | 
			
		||||
    private List<ModbusMaster> thingsgatewaymodbuss = new();
 | 
			
		||||
    private List<IModbusMaster> nmodbuss = new();
 | 
			
		||||
    private List<ModbusTcpNet> modbusTcpNets = new();
 | 
			
		||||
    private List<ModbusTcpMaster> modbusTcpMasters = new();
 | 
			
		||||
    private PipeOptions GetNoDelayPipeOptions()
 | 
			
		||||
    {
 | 
			
		||||
        return new PipeOptions(
 | 
			
		||||
            readerScheduler: PipeScheduler.Inline,
 | 
			
		||||
            writerScheduler: PipeScheduler.Inline,
 | 
			
		||||
            useSynchronizationContext: false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public ModbusBenchmark()
 | 
			
		||||
    {
 | 
			
		||||
        for (int i = 0; i < Program.ClientCount; i++)
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
            var clientConfig = new TouchSocket.Core.TouchSocketConfig();
 | 
			
		||||
            //clientConfig.SetTransportOption(new TouchSocket.Sockets.TransportOption()
 | 
			
		||||
            //{
 | 
			
		||||
            //    ReceivePipeOptions = GetNoDelayPipeOptions(),
 | 
			
		||||
            //    SendPipeOptions = GetNoDelayPipeOptions(),
 | 
			
		||||
            //}).SetNoDelay(true);
 | 
			
		||||
            var clientChannel = clientConfig.GetTcpClient(new ChannelOptions() { RemoteUrl = "127.0.0.1:502", MaxConcurrentCount = 10 });
 | 
			
		||||
            var thingsgatewaymodbus = new ModbusMaster()
 | 
			
		||||
            {
 | 
			
		||||
                //modbus协议格式
 | 
			
		||||
                ModbusType = ModbusTypeEnum.ModbusTcp,
 | 
			
		||||
            };
 | 
			
		||||
            thingsgatewaymodbus.InitChannel(clientChannel);
 | 
			
		||||
            clientChannel.SetupAsync(clientChannel.Config).GetFalseAwaitResult();
 | 
			
		||||
            clientChannel.Logger.LogLevel = LogLevel.Warning;
 | 
			
		||||
            thingsgatewaymodbus.ConnectAsync(CancellationToken.None).GetFalseAwaitResult();
 | 
			
		||||
            thingsgatewaymodbus.ReadAsync("40001", 100).GetAwaiter().GetResult();
 | 
			
		||||
            thingsgatewaymodbuss.Add(thingsgatewaymodbus);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        for (int i = 0; i < Program.ClientCount; i++)
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
            var factory = new NModbus.ModbusFactory();
 | 
			
		||||
            var nmodbus = factory.CreateMaster(new TcpClient("127.0.0.1", 502));
 | 
			
		||||
            nmodbus.ReadHoldingRegistersAsync(1, 0, 100).GetFalseAwaitResult();
 | 
			
		||||
            nmodbuss.Add(nmodbus);
 | 
			
		||||
        }
 | 
			
		||||
        for (int i = 0; i < Program.ClientCount; i++)
 | 
			
		||||
        {
 | 
			
		||||
            ModbusTcpNet modbusTcpNet = new();
 | 
			
		||||
            modbusTcpNet.IpAddress = "127.0.0.1";
 | 
			
		||||
            modbusTcpNet.Port = 502;
 | 
			
		||||
            modbusTcpNet.ConnectServer();
 | 
			
		||||
            modbusTcpNet.ReadAsync("0", 100).GetFalseAwaitResult();
 | 
			
		||||
            modbusTcpNets.Add(modbusTcpNet);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for (int i = 0; i < Program.ClientCount; i++)
 | 
			
		||||
        {
 | 
			
		||||
            var client = new ModbusTcpMaster();
 | 
			
		||||
            client.SetupAsync(new TouchSocketConfig()
 | 
			
		||||
    .SetRemoteIPHost("127.0.0.1:502")).GetFalseAwaitResult();
 | 
			
		||||
            client.ConnectAsync(CancellationToken.None).GetFalseAwaitResult();
 | 
			
		||||
            client.ReadHoldingRegistersAsync(0, 100).GetFalseAwaitResult();
 | 
			
		||||
            modbusTcpMasters.Add(client);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Benchmark]
 | 
			
		||||
    public async Task ThingsGateway()
 | 
			
		||||
    {
 | 
			
		||||
        ModbusAddress addr = new ModbusAddress() { FunctionCode = 3, StartAddress = 0, Length = 100 };
 | 
			
		||||
        List<Task> tasks = new List<Task>();
 | 
			
		||||
        foreach (var thingsgatewaymodbus in thingsgatewaymodbuss)
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
            for (int i = 0; i < Program.TaskNumberOfItems; i++)
 | 
			
		||||
            {
 | 
			
		||||
                tasks.Add(Task.Run(async () =>
 | 
			
		||||
                {
 | 
			
		||||
                    for (int i = 0; i < Program.NumberOfItems; i++)
 | 
			
		||||
                    {
 | 
			
		||||
                        var result = await thingsgatewaymodbus.ModbusReadAsync(addr);
 | 
			
		||||
                        if (!result.IsSuccess)
 | 
			
		||||
                        {
 | 
			
		||||
                            throw new Exception(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss ffff") + result.ToString());
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        await Task.WhenAll(tasks);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    [Benchmark]
 | 
			
		||||
    public async Task TouchSocket()
 | 
			
		||||
    {
 | 
			
		||||
        List<Task> tasks = new List<Task>();
 | 
			
		||||
        foreach (var modbusTcpMaster in modbusTcpMasters)
 | 
			
		||||
        {
 | 
			
		||||
            for (int i = 0; i < Program.TaskNumberOfItems; i++)
 | 
			
		||||
            {
 | 
			
		||||
                tasks.Add(Task.Run(async () =>
 | 
			
		||||
                {
 | 
			
		||||
                    for (int i = 0; i < Program.NumberOfItems; i++)
 | 
			
		||||
                    {
 | 
			
		||||
                        var result = await modbusTcpMaster.ReadHoldingRegistersAsync(0, 100);
 | 
			
		||||
                        if (!result.IsSuccess)
 | 
			
		||||
                        {
 | 
			
		||||
                            throw new Exception(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss ffff") + result.ToString());
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        await Task.WhenAll(tasks);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    [Benchmark]
 | 
			
		||||
    public async Task NModbus4()
 | 
			
		||||
    {
 | 
			
		||||
        List<Task> tasks = new List<Task>();
 | 
			
		||||
        foreach (var nmodbus in nmodbuss)
 | 
			
		||||
        {
 | 
			
		||||
            for (int i = 0; i < Program.TaskNumberOfItems; i++)
 | 
			
		||||
            {
 | 
			
		||||
                tasks.Add(Task.Run(async () =>
 | 
			
		||||
                {
 | 
			
		||||
                    for (int i = 0; i < Program.NumberOfItems; i++)
 | 
			
		||||
                    {
 | 
			
		||||
                        var result = await nmodbus.ReadHoldingRegistersAsync(1, 0, 100);
 | 
			
		||||
                    }
 | 
			
		||||
                }));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        await Task.WhenAll(tasks);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    //并发失败
 | 
			
		||||
    //[Benchmark]
 | 
			
		||||
    //public async Task HslCommunication()
 | 
			
		||||
    //{
 | 
			
		||||
    //    List<Task> tasks = new List<Task>();
 | 
			
		||||
    //    foreach (var modbusTcpNet in modbusTcpNets)
 | 
			
		||||
    //    {
 | 
			
		||||
    //        for (int i = 0; i < Program.TaskNumberOfItems; i++)
 | 
			
		||||
    //        {
 | 
			
		||||
    //            tasks.Add(Task.Run(async () =>
 | 
			
		||||
    //            {
 | 
			
		||||
    //                for (int i = 0; i < Program.NumberOfItems; i++)
 | 
			
		||||
    //                {
 | 
			
		||||
    //                    var result = await modbusTcpNet.ReadAsync("0", 100);
 | 
			
		||||
    //                    if (!result.IsSuccess)
 | 
			
		||||
    //                    {
 | 
			
		||||
    //                        throw new Exception(result.Message);
 | 
			
		||||
    //                    }
 | 
			
		||||
    //                }
 | 
			
		||||
    //            }));
 | 
			
		||||
    //        }
 | 
			
		||||
    //    }
 | 
			
		||||
    //    await Task.WhenAll(tasks);
 | 
			
		||||
    //}
 | 
			
		||||
 | 
			
		||||
    public void Dispose()
 | 
			
		||||
    {
 | 
			
		||||
        thingsgatewaymodbuss?.ForEach(a => a.Channel.SafeDispose());
 | 
			
		||||
        thingsgatewaymodbuss?.ForEach(a => a.SafeDispose());
 | 
			
		||||
        nmodbuss?.ForEach(a => a.SafeDispose());
 | 
			
		||||
        modbusTcpNets?.ForEach(a => a.SafeDispose());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,166 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
//  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
 | 
			
		||||
//  此代码版权(除特别声明外的代码)归作者本人Diego所有
 | 
			
		||||
//  源代码使用协议遵循本仓库的开源协议及附加协议
 | 
			
		||||
//  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
 | 
			
		||||
//  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
 | 
			
		||||
//  使用文档:https://kimdiego2098.github.io/
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using BenchmarkConsoleApp;
 | 
			
		||||
 | 
			
		||||
using BenchmarkDotNet.Attributes;
 | 
			
		||||
using BenchmarkDotNet.Diagnosers;
 | 
			
		||||
 | 
			
		||||
using HslCommunication.Profinet.Siemens;
 | 
			
		||||
 | 
			
		||||
using S7.Net;
 | 
			
		||||
 | 
			
		||||
using System.IO.Pipelines;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.Foundation.SiemensS7;
 | 
			
		||||
 | 
			
		||||
using TouchSocket.Core;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Foundation;
 | 
			
		||||
 | 
			
		||||
[MemoryDiagnoser]
 | 
			
		||||
public class S7Benchmark : IDisposable
 | 
			
		||||
{
 | 
			
		||||
    private List<SiemensS7Master> siemensS7s = new();
 | 
			
		||||
 | 
			
		||||
    private List<Plc> plcs = new();
 | 
			
		||||
    private List<SiemensS7Net> siemensS7Nets = new();
 | 
			
		||||
    private PipeOptions GetNoDelayPipeOptions()
 | 
			
		||||
    {
 | 
			
		||||
        return new PipeOptions(
 | 
			
		||||
            readerScheduler: PipeScheduler.Inline,
 | 
			
		||||
            writerScheduler: PipeScheduler.Inline,
 | 
			
		||||
            useSynchronizationContext: false);
 | 
			
		||||
    }
 | 
			
		||||
    public S7Benchmark()
 | 
			
		||||
 | 
			
		||||
    {
 | 
			
		||||
        {
 | 
			
		||||
            for (int i = 0; i < Program.ClientCount; i++)
 | 
			
		||||
            {
 | 
			
		||||
                var clientConfig = new TouchSocket.Core.TouchSocketConfig();
 | 
			
		||||
                clientConfig.SetTransportOption(new TouchSocket.Sockets.TransportOption()
 | 
			
		||||
                {
 | 
			
		||||
                    ReceivePipeOptions = GetNoDelayPipeOptions(),
 | 
			
		||||
                    SendPipeOptions = GetNoDelayPipeOptions(),
 | 
			
		||||
                }).SetNoDelay(true);
 | 
			
		||||
                var clientChannel = clientConfig.GetTcpClient(new ChannelOptions() { RemoteUrl = "127.0.0.1:102" });
 | 
			
		||||
                var siemensS7 = new SiemensS7Master()
 | 
			
		||||
                {
 | 
			
		||||
                    //modbus协议格式
 | 
			
		||||
                    SiemensS7Type = SiemensTypeEnum.S1500
 | 
			
		||||
                };
 | 
			
		||||
                siemensS7.InitChannel(clientChannel);
 | 
			
		||||
                clientChannel.SetupAsync(clientChannel.Config).GetFalseAwaitResult();
 | 
			
		||||
                clientChannel.Logger.LogLevel = LogLevel.Warning;
 | 
			
		||||
                siemensS7.ConnectAsync(CancellationToken.None).GetFalseAwaitResult();
 | 
			
		||||
                siemensS7.ReadAsync("M1", 100).GetAwaiter().GetResult();
 | 
			
		||||
                siemensS7s.Add(siemensS7);
 | 
			
		||||
            }
 | 
			
		||||
            for (int i = 0; i < Program.ClientCount; i++)
 | 
			
		||||
            {
 | 
			
		||||
                var siemensS7Net = new SiemensS7Net(SiemensPLCS.S1500, "127.0.0.1");
 | 
			
		||||
                siemensS7Net.ConnectServer();
 | 
			
		||||
                siemensS7Net.ReadAsync("M0", 100).GetFalseAwaitResult();
 | 
			
		||||
                siemensS7Nets.Add(siemensS7Net);
 | 
			
		||||
            }
 | 
			
		||||
            for (int i = 0; i < Program.ClientCount; i++)
 | 
			
		||||
            {
 | 
			
		||||
                var plc = new Plc(CpuType.S7300, "127.0.0.1", 102, 0, 0);
 | 
			
		||||
                plc.Open();//打开plc连接
 | 
			
		||||
                plc.ReadAsync(DataType.Memory, 1, 0, VarType.Byte, 100).GetFalseAwaitResult();
 | 
			
		||||
                plcs.Add(plc);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Benchmark]
 | 
			
		||||
    public async Task S7netplus()
 | 
			
		||||
    {
 | 
			
		||||
        List<Task> tasks = new List<Task>();
 | 
			
		||||
        foreach (var plc in plcs)
 | 
			
		||||
        {
 | 
			
		||||
            for (int i = 0; i < Program.TaskNumberOfItems; i++)
 | 
			
		||||
            {
 | 
			
		||||
                tasks.Add(Task.Run(async () =>
 | 
			
		||||
                {
 | 
			
		||||
                    for (int i = 0; i < Program.NumberOfItems; i++)
 | 
			
		||||
                    {
 | 
			
		||||
                        var result = await plc.ReadAsync(DataType.Memory, 1, 0, VarType.Byte, 100);
 | 
			
		||||
                    }
 | 
			
		||||
                }));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        await Task.WhenAll(tasks);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    //并发失败
 | 
			
		||||
    //[Benchmark]
 | 
			
		||||
    //public async Task HslCommunication()
 | 
			
		||||
    //{
 | 
			
		||||
    //    List<Task> tasks = new List<Task>();
 | 
			
		||||
    //    foreach (var siemensS7Net in siemensS7Nets)
 | 
			
		||||
    //    {
 | 
			
		||||
    //        for (int i = 0; i < Program.TaskNumberOfItems; i++)
 | 
			
		||||
    //        {
 | 
			
		||||
    //            tasks.Add(Task.Run(async () =>
 | 
			
		||||
    //            {
 | 
			
		||||
    //                for (int i = 0; i < Program.NumberOfItems; i++)
 | 
			
		||||
    //                {
 | 
			
		||||
    //                    var result = await siemensS7Net.ReadAsync("M0", 100);
 | 
			
		||||
    //                    if (!result.IsSuccess)
 | 
			
		||||
    //                    {
 | 
			
		||||
    //                        throw new Exception(result.Message);
 | 
			
		||||
    //                    }
 | 
			
		||||
    //                }
 | 
			
		||||
    //            }));
 | 
			
		||||
    //        }
 | 
			
		||||
    //    }
 | 
			
		||||
    //    await Task.WhenAll(tasks);
 | 
			
		||||
    //}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    [Benchmark]
 | 
			
		||||
    public async Task ThingsGateway()
 | 
			
		||||
    {
 | 
			
		||||
        SiemensS7Address[] siemensS7Address = [SiemensS7Address.ParseFrom("M1", 100)];
 | 
			
		||||
        List<Task> tasks = new List<Task>();
 | 
			
		||||
        foreach (var siemensS7 in siemensS7s)
 | 
			
		||||
        {
 | 
			
		||||
            for (int i = 0; i < Program.TaskNumberOfItems; i++)
 | 
			
		||||
            {
 | 
			
		||||
                tasks.Add(Task.Run(async () =>
 | 
			
		||||
                {
 | 
			
		||||
                    for (int i = 0; i < Program.NumberOfItems; i++)
 | 
			
		||||
                    {
 | 
			
		||||
                        var result = await siemensS7.S7ReadAsync(siemensS7Address);
 | 
			
		||||
                        if (!result.IsSuccess)
 | 
			
		||||
                        {
 | 
			
		||||
                            throw new Exception(result.ToString());
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        await Task.WhenAll(tasks);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void Dispose()
 | 
			
		||||
    {
 | 
			
		||||
        plcs.ForEach(a => a.SafeDispose());
 | 
			
		||||
        siemensS7Nets.ForEach(a => a.SafeDispose());
 | 
			
		||||
        siemensS7s.ForEach(a => a.Channel.SafeDispose());
 | 
			
		||||
        siemensS7s.ForEach(a => a.SafeDispose());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,62 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
//  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
 | 
			
		||||
//  此代码版权(除特别声明外的代码)归作者本人Diego所有
 | 
			
		||||
//  源代码使用协议遵循本仓库的开源协议及附加协议
 | 
			
		||||
//  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
 | 
			
		||||
//  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
 | 
			
		||||
//  使用文档:https://kimdiego2098.github.io/
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using BenchmarkDotNet.Attributes;
 | 
			
		||||
using BenchmarkDotNet.Diagnosers;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.NewLife.Collections;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Foundation;
 | 
			
		||||
 | 
			
		||||
[MemoryDiagnoser]
 | 
			
		||||
public class TimeoutBenchmark
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    [Benchmark]
 | 
			
		||||
    public async ValueTask CtsWaitAsync()
 | 
			
		||||
    {
 | 
			
		||||
        using var otherCts = new CancellationTokenSource();
 | 
			
		||||
        for (int i1 = 0; i1 < 10; i1++)
 | 
			
		||||
            for (int i = 0; i < 10; i++)
 | 
			
		||||
            {
 | 
			
		||||
                using var ctsTime = new CancellationTokenSource(TimeSpan.FromMilliseconds(10));
 | 
			
		||||
                using var cts = CancellationTokenSource.CreateLinkedTokenSource(ctsTime.Token, otherCts.Token);
 | 
			
		||||
 | 
			
		||||
                await Task.Delay(5, cts.Token).ConfigureAwait(false); // 模拟工作
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private ObjectPool<ReusableCancellationTokenSource> _reusableTimeouts;
 | 
			
		||||
    [Benchmark]
 | 
			
		||||
    public async ValueTask ReusableTimeoutWaitAsync()
 | 
			
		||||
    {
 | 
			
		||||
        _reusableTimeouts ??= new();
 | 
			
		||||
        using var otherCts = new CancellationTokenSource();
 | 
			
		||||
        for (int i1 = 0; i1 < 10; i1++)
 | 
			
		||||
            for (int i = 0; i < 10; i++)
 | 
			
		||||
            {
 | 
			
		||||
                var _reusableTimeout = _reusableTimeouts.Get();
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    await Task.Delay(5, _reusableTimeout.GetTokenSource(10, otherCts.Token).Token).ConfigureAwait(false); // 模拟工作
 | 
			
		||||
                }
 | 
			
		||||
                finally
 | 
			
		||||
                {
 | 
			
		||||
                    _reusableTimeouts.Return(_reusableTimeout);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        _reusableTimeouts.Dispose();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										59
									
								
								src/Plugin/ThingsGateway.Foundation.Benchmark/Program.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src/Plugin/ThingsGateway.Foundation.Benchmark/Program.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,59 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
//  此代码版权(除特别声明或在XREF结尾的命名空间的代码)归作者本人若汝棋茗所有
 | 
			
		||||
//  源代码使用协议遵循本仓库的开源协议及附加协议,若本仓库没有设置,则按MIT开源协议授权
 | 
			
		||||
//  CSDN博客:https://blog.csdn.net/qq_40374647
 | 
			
		||||
//  哔哩哔哩视频:https://space.bilibili.com/94253567
 | 
			
		||||
//  Gitee源代码仓库:https://gitee.com/RRQM_Home
 | 
			
		||||
//  Github源代码仓库:https://github.com/RRQM
 | 
			
		||||
//  API首页:http://rrqm_home.gitee.io/touchsocket/
 | 
			
		||||
//  交流QQ群:234762506
 | 
			
		||||
//  感谢您的下载和使用
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using BenchmarkDotNet.Configs;
 | 
			
		||||
using BenchmarkDotNet.Running;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.Foundation;
 | 
			
		||||
 | 
			
		||||
namespace BenchmarkConsoleApp
 | 
			
		||||
{
 | 
			
		||||
    internal class Program
 | 
			
		||||
    {
 | 
			
		||||
        public static int ClientCount = 30;
 | 
			
		||||
        public static int TaskNumberOfItems = 10;
 | 
			
		||||
        public static int NumberOfItems = 30;
 | 
			
		||||
 | 
			
		||||
        private static async Task Main(string[] args)
 | 
			
		||||
        {
 | 
			
		||||
            Console.WriteLine("开始测试前,请先启动ModbusSlave,建议使用本项目自带的ThingsGateway.Debug.Photino软件开启,S7可以用KEPSERVER的S7模拟服务");
 | 
			
		||||
            Console.WriteLine($"多客户端({ClientCount}),多线程({TaskNumberOfItems})并发读取({NumberOfItems})测试,共{ClientCount * TaskNumberOfItems * NumberOfItems}次");
 | 
			
		||||
            await Task.CompletedTask;
 | 
			
		||||
            //ModbusBenchmark modbusBenchmark = new ModbusBenchmark();
 | 
			
		||||
            //System.Diagnostics.Stopwatch stopwatch = new();
 | 
			
		||||
            //stopwatch.Start();
 | 
			
		||||
            //await modbusBenchmark.ThingsGateway();
 | 
			
		||||
            //stopwatch.Stop();
 | 
			
		||||
            //Console.WriteLine($"ThingsGateway耗时:{stopwatch.ElapsedMilliseconds}ms");
 | 
			
		||||
            //stopwatch.Restart();
 | 
			
		||||
            //await modbusBenchmark.TouchSocket();
 | 
			
		||||
            //stopwatch.Stop();
 | 
			
		||||
            //Console.WriteLine($"TouchSocket耗时:{stopwatch.ElapsedMilliseconds}ms");
 | 
			
		||||
            //Console.ReadLine();
 | 
			
		||||
 | 
			
		||||
            //            BenchmarkRunner.Run<TimeoutBenchmark>(
 | 
			
		||||
            //ManualConfig.Create(DefaultConfig.Instance)
 | 
			
		||||
            //.WithOptions(ConfigOptions.DisableOptimizationsValidator)
 | 
			
		||||
            //);
 | 
			
		||||
            BenchmarkRunner.Run<ModbusBenchmark>(
 | 
			
		||||
       ManualConfig.Create(DefaultConfig.Instance)
 | 
			
		||||
           .WithOptions(ConfigOptions.DisableOptimizationsValidator)
 | 
			
		||||
   );
 | 
			
		||||
            //            BenchmarkRunner.Run<S7Benchmark>(
 | 
			
		||||
            //ManualConfig.Create(DefaultConfig.Instance)
 | 
			
		||||
            //.WithOptions(ConfigOptions.DisableOptimizationsValidator)
 | 
			
		||||
            //);
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,58 @@
 | 
			
		||||
<Project Sdk="Microsoft.NET.Sdk">
 | 
			
		||||
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<OutputType>Exe</OutputType>
 | 
			
		||||
		<TargetFramework>net8.0</TargetFramework>
 | 
			
		||||
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
 | 
			
		||||
		<AnalysisModeDesign>None</AnalysisModeDesign>
 | 
			
		||||
		<AnalysisModeDocumentation>None</AnalysisModeDocumentation>
 | 
			
		||||
		<AnalysisModeGlobalization>None</AnalysisModeGlobalization>
 | 
			
		||||
		<AnalysisModeInteroperability>None</AnalysisModeInteroperability>
 | 
			
		||||
		<AnalysisModeMaintainability>None</AnalysisModeMaintainability>
 | 
			
		||||
		<AnalysisModeNaming>None</AnalysisModeNaming>
 | 
			
		||||
		<AnalysisModePerformance>None</AnalysisModePerformance>
 | 
			
		||||
		<AnalysisModeSingleFile>None</AnalysisModeSingleFile>
 | 
			
		||||
		<AnalysisModeReliability>None</AnalysisModeReliability>
 | 
			
		||||
		<AnalysisModeSecurity>None</AnalysisModeSecurity>
 | 
			
		||||
		<AnalysisModeUsage>None</AnalysisModeUsage>
 | 
			
		||||
		<AnalysisModeStyle>None</AnalysisModeStyle>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		<TargetFrameworks>net8.0;</TargetFrameworks>
 | 
			
		||||
		<LangVersion>13.0</LangVersion>
 | 
			
		||||
		<ImplicitUsings>enable</ImplicitUsings>
 | 
			
		||||
		<Nullable>enable</Nullable>
 | 
			
		||||
		<Authors>Diego</Authors>
 | 
			
		||||
		<Company>Diego</Company>
 | 
			
		||||
		<Product>Diego</Product>
 | 
			
		||||
		<Copyright>版权所有 © 2023-present Diego</Copyright>
 | 
			
		||||
		<RepositoryUrl>https://gitee.com/diego2098/ThingsGateway</RepositoryUrl>
 | 
			
		||||
		<RepositoryType>Gitee</RepositoryType>
 | 
			
		||||
		<GenerateResxSourceIncludeDefaultValues>true</GenerateResxSourceIncludeDefaultValues>
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
		<PackageReference Remove="Roslynator.Analyzers">
 | 
			
		||||
		</PackageReference>
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
		<PackageReference Include="BenchmarkDotNet" Version="0.15.2" />
 | 
			
		||||
		<PackageReference Include="HslCommunication" Version="12.5.0" />
 | 
			
		||||
		<PackageReference Include="NModbus" Version="3.0.81" />
 | 
			
		||||
		<PackageReference Include="NModbus.Serial" Version="3.0.81" />
 | 
			
		||||
		<PackageReference Include="S7netplus" Version="0.20.0" />
 | 
			
		||||
		<PackageReference Include="ThingsGateway.Foundation.Modbus" Version="$(DefaultVersion)" />
 | 
			
		||||
		<PackageReference Include="ThingsGateway.Foundation.SiemensS7" Version="$(DefaultVersion)" />
 | 
			
		||||
		<PackageReference Include="TouchSocket.Modbus" Version="4.0.0-beta.27" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
		<!--<ProjectReference Include="..\..\Plugin\ThingsGateway.Foundation.Modbus\ThingsGateway.Foundation.Modbus.csproj" />
 | 
			
		||||
		<ProjectReference Include="..\..\Plugin\ThingsGateway.Foundation.SiemensS7\ThingsGateway.Foundation.SiemensS7.csproj" />-->
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
</Project>
 | 
			
		||||
@@ -10,6 +10,7 @@
 | 
			
		||||
 | 
			
		||||
using System.Buffers;
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
using System.Runtime.InteropServices;
 | 
			
		||||
 | 
			
		||||
using TouchSocket.Sockets;
 | 
			
		||||
 | 
			
		||||
@@ -184,10 +185,28 @@ public class ModbusSlave : DeviceBase, IModbusAddress
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    private void Init(ModbusRequest mAddress)
 | 
			
		||||
    {
 | 
			
		||||
        //自动扩容
 | 
			
		||||
        ModbusServer01ByteBlocks.GetOrAdd(mAddress.Station, a =>
 | 
			
		||||
        {
 | 
			
		||||
            var bytes = new ByteBlock(ushort.MaxValue * 2);
 | 
			
		||||
            bytes.SetLength(ushort.MaxValue * 2);
 | 
			
		||||
            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);
 | 
			
		||||
@@ -197,8 +216,25 @@ public class ModbusSlave : DeviceBase, IModbusAddress
 | 
			
		||||
        });
 | 
			
		||||
        ModbusServer02ByteBlocks.GetOrAdd(mAddress.Station, a =>
 | 
			
		||||
        {
 | 
			
		||||
            var bytes = new ByteBlock(ushort.MaxValue * 2);
 | 
			
		||||
            bytes.SetLength(ushort.MaxValue * 2);
 | 
			
		||||
            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);
 | 
			
		||||
@@ -208,8 +244,25 @@ public class ModbusSlave : DeviceBase, IModbusAddress
 | 
			
		||||
        });
 | 
			
		||||
        ModbusServer03ByteBlocks.GetOrAdd(mAddress.Station, a =>
 | 
			
		||||
        {
 | 
			
		||||
            var bytes = new ByteBlock(ushort.MaxValue * 2);
 | 
			
		||||
            bytes.SetLength(ushort.MaxValue * 2);
 | 
			
		||||
            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);
 | 
			
		||||
@@ -219,8 +272,25 @@ public class ModbusSlave : DeviceBase, IModbusAddress
 | 
			
		||||
        });
 | 
			
		||||
        ModbusServer04ByteBlocks.GetOrAdd(mAddress.Station, a =>
 | 
			
		||||
        {
 | 
			
		||||
            var bytes = new ByteBlock(ushort.MaxValue * 2);
 | 
			
		||||
            bytes.SetLength(ushort.MaxValue * 2);
 | 
			
		||||
            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);
 | 
			
		||||
@@ -278,16 +348,20 @@ public class ModbusSlave : DeviceBase, IModbusAddress
 | 
			
		||||
                    switch (f)
 | 
			
		||||
                    {
 | 
			
		||||
                        case 1:
 | 
			
		||||
                            return OperResult.CreateSuccessResult(ModbusServer01ByteBlock.Memory.Slice(mAddress.StartAddress, len));
 | 
			
		||||
                            ModbusServer01ByteBlock.Position = mAddress.StartAddress;
 | 
			
		||||
                            return OperResult.CreateSuccessResult((ReadOnlyMemory<byte>)ModbusServer01ByteBlock.GetMemory(len).Slice(0, len));
 | 
			
		||||
 | 
			
		||||
                        case 2:
 | 
			
		||||
                            return OperResult.CreateSuccessResult(ModbusServer02ByteBlock.Memory.Slice(mAddress.StartAddress, len));
 | 
			
		||||
                            ModbusServer02ByteBlock.Position = mAddress.StartAddress;
 | 
			
		||||
                            return OperResult.CreateSuccessResult((ReadOnlyMemory<byte>)ModbusServer02ByteBlock.GetMemory(len).Slice(0, len));
 | 
			
		||||
 | 
			
		||||
                        case 3:
 | 
			
		||||
                            return OperResult.CreateSuccessResult(ModbusServer03ByteBlock.Memory.Slice(mAddress.StartAddress * RegisterByteLength, len));
 | 
			
		||||
                            ModbusServer03ByteBlock.Position = mAddress.StartAddress * RegisterByteLength;
 | 
			
		||||
                            return OperResult.CreateSuccessResult((ReadOnlyMemory<byte>)ModbusServer03ByteBlock.GetMemory(len).Slice(0, len));
 | 
			
		||||
 | 
			
		||||
                        case 4:
 | 
			
		||||
                            return OperResult.CreateSuccessResult(ModbusServer04ByteBlock.Memory.Slice(mAddress.StartAddress * RegisterByteLength, len));
 | 
			
		||||
                            ModbusServer04ByteBlock.Position = mAddress.StartAddress * RegisterByteLength;
 | 
			
		||||
                            return OperResult.CreateSuccessResult((ReadOnlyMemory<byte>)ModbusServer04ByteBlock.GetMemory(len).Slice(0, len));
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -139,157 +139,192 @@ public partial class SiemensS7Master : DeviceBase
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 此方法并不会智能分组以最大化效率,减少传输次数,因为返回值是byte[],所以一切都按地址数组的顺序执行,最后合并数组
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public async ValueTask<OperResult<ReadOnlyMemory<byte>>> S7ReadAsync(SiemensS7Address[] sAddresss, CancellationToken cancellationToken = default)
 | 
			
		||||
    public async ValueTask<OperResult<ReadOnlyMemory<byte>>> S7ReadAsync(
 | 
			
		||||
        SiemensS7Address[] addresses,
 | 
			
		||||
        CancellationToken cancellationToken = default)
 | 
			
		||||
    {
 | 
			
		||||
        var byteBuffer = new ValueByteBlock(512);
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var byteBlock = new ValueByteBlock(2048);
 | 
			
		||||
            try
 | 
			
		||||
            foreach (var address in addresses)
 | 
			
		||||
            {
 | 
			
		||||
                foreach (var sAddress in sAddresss)
 | 
			
		||||
                int readCount = 0;
 | 
			
		||||
                int totalLength = address.Length == 0 ? 1 : address.Length;
 | 
			
		||||
                int originalStart = address.AddressStart;
 | 
			
		||||
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    int num = 0;
 | 
			
		||||
                    var addressLen = sAddress.Length == 0 ? 1 : sAddress.Length;
 | 
			
		||||
                    var start = sAddress.AddressStart;
 | 
			
		||||
                    try
 | 
			
		||||
                    while (readCount < totalLength)
 | 
			
		||||
                    {
 | 
			
		||||
                        while (num < addressLen)
 | 
			
		||||
                        // 每次读取的 PDU 长度,循环直到读取完整
 | 
			
		||||
                        int chunkLength = Math.Min(totalLength - readCount, PduLength);
 | 
			
		||||
                        address.Length = chunkLength;
 | 
			
		||||
 | 
			
		||||
                        var result = await SendThenReturnAsync(
 | 
			
		||||
                            new S7Send([address], true),
 | 
			
		||||
                            cancellationToken: cancellationToken
 | 
			
		||||
                        ).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                        if (!result.IsSuccess)
 | 
			
		||||
                            return result;
 | 
			
		||||
 | 
			
		||||
                        byteBuffer.Write(result.Content.Span);
 | 
			
		||||
 | 
			
		||||
                        if (readCount + chunkLength >= totalLength)
 | 
			
		||||
                        {
 | 
			
		||||
                            //pdu长度,重复生成报文,直至全部生成
 | 
			
		||||
                            int len = Math.Min(addressLen - num, PduLength);
 | 
			
		||||
                            sAddress.Length = len;
 | 
			
		||||
                            var result = await SendThenReturnAsync(new S7Send([sAddress], true), cancellationToken: cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                            if (!result.IsSuccess) return result;
 | 
			
		||||
 | 
			
		||||
                            byteBlock.Write(result.Content.Span);
 | 
			
		||||
                            num += len;
 | 
			
		||||
 | 
			
		||||
                            if (sAddress.DataCode == S7Area.TM || sAddress.DataCode == S7Area.CT)
 | 
			
		||||
                            if (addresses.Length == 1)
 | 
			
		||||
                            {
 | 
			
		||||
                                sAddress.AddressStart += len / 2;
 | 
			
		||||
                            }
 | 
			
		||||
                            else
 | 
			
		||||
                            {
 | 
			
		||||
                                sAddress.AddressStart += len * 8;
 | 
			
		||||
                                return result;
 | 
			
		||||
                            }
 | 
			
		||||
                            break;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    finally
 | 
			
		||||
                    {
 | 
			
		||||
                        sAddress.AddressStart = start;
 | 
			
		||||
 | 
			
		||||
                        readCount += chunkLength;
 | 
			
		||||
 | 
			
		||||
                        // 更新地址起点
 | 
			
		||||
                        if (address.DataCode == S7Area.TM || address.DataCode == S7Area.CT)
 | 
			
		||||
                            address.AddressStart += chunkLength / 2;
 | 
			
		||||
                        else
 | 
			
		||||
                            address.AddressStart += chunkLength * 8;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                finally
 | 
			
		||||
                {
 | 
			
		||||
                    address.AddressStart = originalStart;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
                return new OperResult<ReadOnlyMemory<byte>>() { Content = byteBlock.ToArray() };
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                return new OperResult<ReadOnlyMemory<byte>>(ex);
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
                byteBlock.SafeDispose();
 | 
			
		||||
            }
 | 
			
		||||
            return new OperResult<ReadOnlyMemory<byte>> { Content = byteBuffer.ToArray() };
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            return new OperResult<ReadOnlyMemory<byte>>(ex);
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            byteBuffer.SafeDispose();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 此方法并不会智能分组以最大化效率,减少传输次数,因为返回值是byte[],所以一切都按地址数组的顺序执行,最后合并数组
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public async ValueTask<Dictionary<SiemensS7Address, OperResult>> S7WriteAsync(SiemensS7Address[] sAddresss, CancellationToken cancellationToken = default)
 | 
			
		||||
    public async ValueTask<Dictionary<SiemensS7Address, OperResult>> S7WriteAsync(
 | 
			
		||||
    SiemensS7Address[] addresses,
 | 
			
		||||
    CancellationToken cancellationToken = default)
 | 
			
		||||
    {
 | 
			
		||||
        var dictOperResult = new Dictionary<SiemensS7Address, OperResult>();
 | 
			
		||||
 | 
			
		||||
        void SetFailOperResult(OperResult operResult)
 | 
			
		||||
        {
 | 
			
		||||
            foreach (var item in sAddresss)
 | 
			
		||||
            foreach (var address in addresses)
 | 
			
		||||
            {
 | 
			
		||||
                dictOperResult.TryAdd(item, operResult);
 | 
			
		||||
                dictOperResult.TryAdd(address, operResult);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var firstAddress = addresses[0];
 | 
			
		||||
 | 
			
		||||
        // 单位写入(位写入)
 | 
			
		||||
        if (addresses.Length <= 1 && firstAddress.IsBit)
 | 
			
		||||
        {
 | 
			
		||||
            var sAddress = sAddresss[0];
 | 
			
		||||
            if (sAddresss.Length <= 1 && sAddress.IsBit)
 | 
			
		||||
            var byteBuffer = new ValueByteBlock(512);
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var writeResult = await SendThenReturnAsync(
 | 
			
		||||
                    new S7Send([firstAddress], false),
 | 
			
		||||
                    cancellationToken: cancellationToken
 | 
			
		||||
                ).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                dictOperResult.TryAdd(firstAddress, writeResult);
 | 
			
		||||
                return dictOperResult;
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                SetFailOperResult(new OperResult(ex));
 | 
			
		||||
                return dictOperResult;
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
                byteBuffer.SafeDispose();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            // 多写入
 | 
			
		||||
            var addressChunks = new List<List<SiemensS7Address>>();
 | 
			
		||||
            ushort dataLength = 0;
 | 
			
		||||
            ushort itemCount = 1;
 | 
			
		||||
            var currentChunk = new List<SiemensS7Address>();
 | 
			
		||||
 | 
			
		||||
            for (int i = 0; i < addresses.Length; i++)
 | 
			
		||||
            {
 | 
			
		||||
                var address = addresses[i];
 | 
			
		||||
                dataLength += (ushort)(address.Data.Length + 4);
 | 
			
		||||
                ushort telegramLength = (ushort)(itemCount * 12 + 19 + dataLength);
 | 
			
		||||
 | 
			
		||||
                if (telegramLength < PduLength)
 | 
			
		||||
                {
 | 
			
		||||
                    currentChunk.Add(address);
 | 
			
		||||
                    itemCount++;
 | 
			
		||||
 | 
			
		||||
                    if (i == addresses.Length - 1)
 | 
			
		||||
                        addressChunks.Add(currentChunk);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    addressChunks.Add(currentChunk);
 | 
			
		||||
                    currentChunk = new List<SiemensS7Address>();
 | 
			
		||||
                    dataLength = 0;
 | 
			
		||||
                    itemCount = 1;
 | 
			
		||||
 | 
			
		||||
                    dataLength += (ushort)(address.Data.Length + 4);
 | 
			
		||||
                    telegramLength = (ushort)(itemCount * 12 + 19 + dataLength);
 | 
			
		||||
 | 
			
		||||
                    if (telegramLength < PduLength)
 | 
			
		||||
                    {
 | 
			
		||||
                        currentChunk.Add(address);
 | 
			
		||||
                        itemCount++;
 | 
			
		||||
 | 
			
		||||
                        if (i == addresses.Length - 1)
 | 
			
		||||
                            addressChunks.Add(currentChunk);
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        SetFailOperResult(new OperResult("Write length exceeds limit"));
 | 
			
		||||
                        return dictOperResult;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            foreach (var chunk in addressChunks)
 | 
			
		||||
            {
 | 
			
		||||
                var byteBlock = new ValueByteBlock(2048);
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    var wresult = await SendThenReturnAsync(new S7Send([sAddress], false), cancellationToken: cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                    dictOperResult.TryAdd(sAddress, wresult);
 | 
			
		||||
                    return dictOperResult;
 | 
			
		||||
                    var result = await SendThenReturnAsync(
 | 
			
		||||
                        new S7Send(chunk.ToArray(), false),
 | 
			
		||||
                        cancellationToken: cancellationToken
 | 
			
		||||
                    ).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                    foreach (var addr in chunk)
 | 
			
		||||
                    {
 | 
			
		||||
                        dictOperResult.TryAdd(addr, result);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception ex)
 | 
			
		||||
                {
 | 
			
		||||
                    SetFailOperResult(new OperResult(ex));
 | 
			
		||||
                    return dictOperResult;
 | 
			
		||||
                }
 | 
			
		||||
                finally
 | 
			
		||||
                {
 | 
			
		||||
                    byteBlock.SafeDispose();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                //多写
 | 
			
		||||
                List<List<SiemensS7Address>> siemensS7Addresses = new();
 | 
			
		||||
                ushort dataLen = 0;
 | 
			
		||||
                ushort itemLen = 1;
 | 
			
		||||
                List<SiemensS7Address> addresses = new();
 | 
			
		||||
                for (int i = 0; i < sAddresss.Length; i++)
 | 
			
		||||
                {
 | 
			
		||||
                    var item = sAddresss[i];
 | 
			
		||||
                    dataLen = (ushort)(dataLen + item.Data.Length + 4);
 | 
			
		||||
                    ushort telegramLen = (ushort)(itemLen * 12 + 19 + dataLen);
 | 
			
		||||
                    if (telegramLen < PduLength)
 | 
			
		||||
                    {
 | 
			
		||||
                        addresses.Add(item);
 | 
			
		||||
                        itemLen++;
 | 
			
		||||
                        if (i == sAddresss.Length - 1)
 | 
			
		||||
                            siemensS7Addresses.Add(addresses);
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        siemensS7Addresses.Add(addresses);
 | 
			
		||||
                        addresses = new();
 | 
			
		||||
                        dataLen = 0;
 | 
			
		||||
                        itemLen = 1;
 | 
			
		||||
                        dataLen = (ushort)(dataLen + item.Data.Length + 4);
 | 
			
		||||
                        telegramLen = (ushort)(itemLen * 12 + 19 + dataLen);
 | 
			
		||||
                        if (telegramLen < PduLength)
 | 
			
		||||
                        {
 | 
			
		||||
                            addresses.Add(item);
 | 
			
		||||
                            itemLen++;
 | 
			
		||||
                            if (i == sAddresss.Length - 1)
 | 
			
		||||
                                siemensS7Addresses.Add(addresses);
 | 
			
		||||
                        }
 | 
			
		||||
                        else
 | 
			
		||||
                        {
 | 
			
		||||
                            SetFailOperResult(new OperResult("Write length exceeds limit"));
 | 
			
		||||
                            return dictOperResult;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                foreach (var item in siemensS7Addresses)
 | 
			
		||||
                {
 | 
			
		||||
                    try
 | 
			
		||||
                    {
 | 
			
		||||
                        var result = await SendThenReturnAsync(new S7Send(item.ToArray(), false), cancellationToken: cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                        foreach (var i1 in item)
 | 
			
		||||
                        {
 | 
			
		||||
                            dictOperResult.TryAdd(i1, result);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    catch (Exception ex)
 | 
			
		||||
                    {
 | 
			
		||||
                        SetFailOperResult(new OperResult(ex));
 | 
			
		||||
                        return dictOperResult;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                return dictOperResult;
 | 
			
		||||
            }
 | 
			
		||||
            return dictOperResult;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    #region 读写
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
 
 | 
			
		||||
@@ -126,7 +126,7 @@ public partial class MainLayout : IDisposable
 | 
			
		||||
        if (context is TabItem tabItem)
 | 
			
		||||
        {
 | 
			
		||||
            await WinboxRender(tabItem.ChildContent, tabItem.Text);
 | 
			
		||||
            await _tab.RemoveTab(tabItem);
 | 
			
		||||
            //await _tab.RemoveTab(tabItem);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    [Inject]
 | 
			
		||||
 
 | 
			
		||||
@@ -35,7 +35,8 @@
 | 
			
		||||
	<!--<Import Project="targets\Pro3.targets" Condition=" '$(SolutionName)' == 'ThingsGatewayPro' " />
 | 
			
		||||
	<Import Project="targets\Pro5.targets" Condition=" '$(SolutionName)' == 'ThingsGatewayPro' " />-->
 | 
			
		||||
	<!--<Import Project="targets\Pro6.targets" Condition=" '$(SolutionName)' == 'ThingsGatewayPro' AND '$(Configuration)' != 'Debug'" />-->
 | 
			
		||||
	<Import Project="targets\Pro7.targets" Condition=" '$(SolutionName)' == 'ThingsGatewayPro' AND '$(Configuration)' != 'Debug'" />
 | 
			
		||||
	<!--nuget包解压复制文件,上下文动态加载,Pro插件-->
 | 
			
		||||
	<Import Project="targets\Pro7.targets" Condition=" '$(SolutionName)' != 'ThingsGatewayPro' OR  '$(Configuration)' != 'Debug'" />
 | 
			
		||||
 | 
			
		||||
	<!--打包复制-->
 | 
			
		||||
	<Import Project="targets\PluginPublish.targets" />
 | 
			
		||||
 
 | 
			
		||||
@@ -113,6 +113,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "debug", "debug", "{053AB5FA
 | 
			
		||||
EndProject
 | 
			
		||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ThingsGateway.Foundation.Demo", "Foundation\ThingsGateway.Foundation.Demo\ThingsGateway.Foundation.Demo.csproj", "{520DEEAA-1CBD-C0CB-2363-EB190D7DE4EA}"
 | 
			
		||||
EndProject
 | 
			
		||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ThingsGateway.Foundation.Benchmark", "Plugin\ThingsGateway.Foundation.Benchmark\ThingsGateway.Foundation.Benchmark.csproj", "{B0957BD6-CF77-36E7-B657-2D0DB85F386F}"
 | 
			
		||||
EndProject
 | 
			
		||||
Global
 | 
			
		||||
	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 | 
			
		||||
		Debug|Any CPU = Debug|Any CPU
 | 
			
		||||
@@ -287,6 +289,10 @@ Global
 | 
			
		||||
		{520DEEAA-1CBD-C0CB-2363-EB190D7DE4EA}.Debug|Any CPU.Build.0 = Debug|Any CPU
 | 
			
		||||
		{520DEEAA-1CBD-C0CB-2363-EB190D7DE4EA}.Release|Any CPU.ActiveCfg = Release|Any CPU
 | 
			
		||||
		{520DEEAA-1CBD-C0CB-2363-EB190D7DE4EA}.Release|Any CPU.Build.0 = Release|Any CPU
 | 
			
		||||
		{B0957BD6-CF77-36E7-B657-2D0DB85F386F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 | 
			
		||||
		{B0957BD6-CF77-36E7-B657-2D0DB85F386F}.Debug|Any CPU.Build.0 = Debug|Any CPU
 | 
			
		||||
		{B0957BD6-CF77-36E7-B657-2D0DB85F386F}.Release|Any CPU.ActiveCfg = Release|Any CPU
 | 
			
		||||
		{B0957BD6-CF77-36E7-B657-2D0DB85F386F}.Release|Any CPU.Build.0 = Release|Any CPU
 | 
			
		||||
	EndGlobalSection
 | 
			
		||||
	GlobalSection(SolutionProperties) = preSolution
 | 
			
		||||
		HideSolutionNode = FALSE
 | 
			
		||||
@@ -334,10 +340,11 @@ Global
 | 
			
		||||
		{E6EF2033-F02A-CDAD-5A72-EE397A89742E} = {36510D70-161F-4241-B8D0-781E21032816}
 | 
			
		||||
		{053AB5FA-9742-96EC-76A1-2AEC739860C6} = {36510D70-161F-4241-B8D0-781E21032816}
 | 
			
		||||
		{520DEEAA-1CBD-C0CB-2363-EB190D7DE4EA} = {2AC600BB-4325-4E0A-93A7-B1F53C8E2CA7}
 | 
			
		||||
		{B0957BD6-CF77-36E7-B657-2D0DB85F386F} = {1D9CD7A3-9700-A851-0ABD-183347D9CC33}
 | 
			
		||||
	EndGlobalSection
 | 
			
		||||
	GlobalSection(ExtensibilityGlobals) = postSolution
 | 
			
		||||
		SolutionGuid = {199B1B96-4F56-4828-9531-813BA02DB282}
 | 
			
		||||
		RESX_Rules = {"EnabledRules":[]}
 | 
			
		||||
		RESX_NeutralResourcesLanguage = zh-Hans
 | 
			
		||||
		RESX_Rules = {"EnabledRules":[]}
 | 
			
		||||
	EndGlobalSection
 | 
			
		||||
EndGlobal
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user