Compare commits
	
		
			37 Commits
		
	
	
		
			10.11.24.0
			...
			10.11.67.0
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					ff1f632de2 | ||
| 
						 | 
					fc3d7015ee | ||
| 
						 | 
					40c5acb522 | ||
| 
						 | 
					f9cc1cbb05 | ||
| 
						 | 
					cf6e8b58f0 | ||
| 
						 | 
					615e3bb24c | ||
| 
						 | 
					4a7534b210 | ||
| 
						 | 
					58e099cb93 | ||
| 
						 | 
					a94a9c953c | ||
| 
						 | 
					35e1ffa3e9 | ||
| 
						 | 
					4921642151 | ||
| 
						 | 
					d71ee29da8 | ||
| 
						 | 
					901aa2d59f | ||
| 
						 | 
					764957c014 | ||
| 
						 | 
					0e3898218b | ||
| 
						 | 
					61f13cef3c | ||
| 
						 | 
					0b663d9e01 | ||
| 
						 | 
					6c95c6209f | ||
| 
						 | 
					4d223d2622 | ||
| 
						 | 
					e8d7e91b64 | ||
| 
						 | 
					8175f541ec | ||
| 
						 | 
					0adbdb926b | ||
| 
						 | 
					42adee9980 | ||
| 
						 | 
					427a7404bc | ||
| 
						 | 
					3658199e0a | ||
| 
						 | 
					82eedee50a | ||
| 
						 | 
					6a18fc3e06 | ||
| 
						 | 
					c37e314ed6 | ||
| 
						 | 
					a937a85d90 | ||
| 
						 | 
					35dd4ae9d3 | ||
| 
						 | 
					0b829ac85c | ||
| 
						 | 
					aa247422d2 | ||
| 
						 | 
					2e00e8c135 | ||
| 
						 | 
					34dd2cf0a7 | ||
| 
						 | 
					8404e20c5e | ||
| 
						 | 
					662aa162e9 | ||
| 
						 | 
					5927738c32 | 
							
								
								
									
										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/
 | 
			
		||||
 
 | 
			
		||||
@@ -4,9 +4,9 @@
 | 
			
		||||
 | 
			
		||||
<div class="tg-table h-100">
 | 
			
		||||
 | 
			
		||||
    <Table TItem="TItem" IsBordered="true" IsStriped="true" TableSize="TableSize.Compact" IsMultipleSelect="IsMultipleSelect" @ref="Instance" SearchTemplate="SearchTemplate"
 | 
			
		||||
    <Table TItem="TItem" IsBordered="true" IsStriped="true" TableSize="TableSize.Compact" SelectedRows=SelectedRows SelectedRowsChanged=privateSelectedRowsChanged 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!"
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,24 @@ namespace ThingsGateway.Admin.Razor;
 | 
			
		||||
[CascadingTypeParameter(nameof(TItem))]
 | 
			
		||||
public partial class AdminTable<TItem> where TItem : class, new()
 | 
			
		||||
{
 | 
			
		||||
    /// <inheritdoc cref="Table{TItem}.SelectedRowsChanged"/>
 | 
			
		||||
    [Parameter]
 | 
			
		||||
    public EventCallback<List<TItem>> SelectedRowsChanged { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc cref="Table{TItem}.SelectedRows"/>
 | 
			
		||||
    [Parameter]
 | 
			
		||||
    public List<TItem> SelectedRows { get; set; } = new();
 | 
			
		||||
 | 
			
		||||
    private async Task privateSelectedRowsChanged(List<TItem> items)
 | 
			
		||||
    {
 | 
			
		||||
        SelectedRows = items;
 | 
			
		||||
        if (SelectedRowsChanged.HasDelegate)
 | 
			
		||||
            await SelectedRowsChanged.InvokeAsync(items);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc cref="Table{TItem}.DoubleClickToEdit"/>
 | 
			
		||||
    [Parameter]
 | 
			
		||||
    public bool DoubleClickToEdit { get; set; } = false;
 | 
			
		||||
@@ -210,14 +228,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 +276,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;
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,7 @@
 | 
			
		||||
        </Card>
 | 
			
		||||
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="col-12 col-sm-10 h-100">
 | 
			
		||||
    <div class="col-12 col-sm-10 h-100 ps-2">
 | 
			
		||||
    <AdminTable @ref=table TItem="SysUser"
 | 
			
		||||
        AutoGenerateColumns="true"
 | 
			
		||||
        ShowAdvancedSearch=false
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@
 | 
			
		||||
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
		<ProjectReference Include="..\ThingsGateway.Admin.Application\ThingsGateway.Admin.Application.csproj" />
 | 
			
		||||
		<PackageReference Include="BootstrapBlazor.Chart" Version="9.0.0" />
 | 
			
		||||
		<PackageReference Include="BootstrapBlazor.Chart" Version="9.0.1" />
 | 
			
		||||
		<PackageReference Include="BootstrapBlazor.UniverSheet" Version="9.0.5" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,8 @@
 | 
			
		||||
    <link rel="stylesheet" href=@($"_content/BootstrapBlazor/css/motronic.min.css?v={this.GetType().Assembly.GetName().Version}") />
 | 
			
		||||
    <link rel="stylesheet" href=@($"ThingsGateway.AdminServer.styles.css?v={this.GetType().Assembly.GetName().Version}") />
 | 
			
		||||
    <link rel="stylesheet" href=@($"{WebsiteConst.DefaultResourceUrl}css/site.css?v={this.GetType().Assembly.GetName().Version}") />
 | 
			
		||||
    <link rel="stylesheet" href=@($"{WebsiteConst.DefaultResourceUrl}css/devui.css?v={this.GetType().Assembly.GetName().Version}") />
 | 
			
		||||
 | 
			
		||||
    @* <script src=@($"{WebsiteConst.DefaultResourceUrl}js/theme.js") type="module"></script><!-- 初始主题 --> *@
 | 
			
		||||
    <!-- PWA Manifest -->
 | 
			
		||||
    <link rel="manifest" href="./manifest.json" />
 | 
			
		||||
@@ -44,6 +46,7 @@
 | 
			
		||||
    <!-- PWA Service Worker -->
 | 
			
		||||
    <script type="text/javascript">'serviceWorker' in navigator && navigator.serviceWorker.register('./service-worker.js')</script>
 | 
			
		||||
 | 
			
		||||
    <script src="pwa-install.js"></script>
 | 
			
		||||
</body>
 | 
			
		||||
 | 
			
		||||
</html>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										34
									
								
								src/Admin/ThingsGateway.AdminServer/wwwroot/pwa-install.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/Admin/ThingsGateway.AdminServer/wwwroot/pwa-install.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,34 @@
 | 
			
		||||
let installPromptTriggered = false;
 | 
			
		||||
 | 
			
		||||
function getCookie(name) {
 | 
			
		||||
    const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
 | 
			
		||||
    return match ? match[2] : null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function hasShownInstallPrompt() {
 | 
			
		||||
    return getCookie("tgPWAInstallPromptShown") === "true";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function markInstallPromptShown() {
 | 
			
		||||
    document.cookie = "tgPWAInstallPromptShown=true; max-age=31536000; path=/";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
window.addEventListener('beforeinstallprompt', (e) => {
 | 
			
		||||
    e.preventDefault();
 | 
			
		||||
 | 
			
		||||
    if (!hasShownInstallPrompt() && !installPromptTriggered) {
 | 
			
		||||
        installPromptTriggered = true;
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
            e.prompt()
 | 
			
		||||
                .then(() => e.userChoice)
 | 
			
		||||
                .then(choiceResult => {
 | 
			
		||||
                    markInstallPromptShown();
 | 
			
		||||
                })
 | 
			
		||||
                .catch(err => {
 | 
			
		||||
                    // 可选错误处理
 | 
			
		||||
                });
 | 
			
		||||
        }, 2000); // 延迟 2 秒提示
 | 
			
		||||
    } else {
 | 
			
		||||
        // console.log("已提示过安装,不再弹出");
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
@@ -24,7 +24,7 @@ public static class ParallelExtensions
 | 
			
		||||
    /// <typeparam name="T">集合元素类型</typeparam>
 | 
			
		||||
    /// <param name="source">要操作的集合</param>
 | 
			
		||||
    /// <param name="body">要执行的操作</param>
 | 
			
		||||
    public static void ParallelForEach<T>(this IList<T> source, Action<T> body)
 | 
			
		||||
    public static void ParallelForEach<T>(this IEnumerable<T> source, Action<T> body)
 | 
			
		||||
    {
 | 
			
		||||
        ParallelOptions options = new();
 | 
			
		||||
        options.MaxDegreeOfParallelism = Environment.ProcessorCount;
 | 
			
		||||
@@ -38,7 +38,7 @@ public static class ParallelExtensions
 | 
			
		||||
    /// <typeparam name="T">集合元素类型</typeparam>
 | 
			
		||||
    /// <param name="source">要操作的集合</param>
 | 
			
		||||
    /// <param name="body">要执行的操作</param>
 | 
			
		||||
    public static void ParallelForEach<T>(this IList<T> source, Action<T, ParallelLoopState, long> body)
 | 
			
		||||
    public static void ParallelForEach<T>(this IEnumerable<T> source, Action<T, ParallelLoopState, long> body)
 | 
			
		||||
    {
 | 
			
		||||
        ParallelOptions options = new();
 | 
			
		||||
        options.MaxDegreeOfParallelism = Environment.ProcessorCount;
 | 
			
		||||
@@ -53,7 +53,7 @@ public static class ParallelExtensions
 | 
			
		||||
    /// <param name="source">要操作的集合</param>
 | 
			
		||||
    /// <param name="body">要执行的操作</param>
 | 
			
		||||
    /// <param name="parallelCount">最大并行度</param>
 | 
			
		||||
    public static void ParallelForEach<T>(this IList<T> source, Action<T> body, int parallelCount)
 | 
			
		||||
    public static void ParallelForEach<T>(this IEnumerable<T> source, Action<T> body, int parallelCount)
 | 
			
		||||
    {
 | 
			
		||||
        // 创建并行操作的选项对象,设置最大并行度为指定的值
 | 
			
		||||
        var options = new ParallelOptions();
 | 
			
		||||
@@ -109,7 +109,7 @@ public static class ParallelExtensions
 | 
			
		||||
    /// <param name="parallelCount">最大并行度</param>
 | 
			
		||||
    /// <param name="cancellationToken">取消操作的标志</param>
 | 
			
		||||
    /// <returns>表示异步操作的任务</returns>
 | 
			
		||||
    public static Task ParallelForEachAsync<T>(this IList<T> source, Func<T, CancellationToken, ValueTask> body, int parallelCount, CancellationToken cancellationToken = default)
 | 
			
		||||
    public static Task ParallelForEachAsync<T>(this IEnumerable<T> source, Func<T, CancellationToken, ValueTask> body, int parallelCount, CancellationToken cancellationToken = default)
 | 
			
		||||
    {
 | 
			
		||||
        // 创建并行操作的选项对象,设置最大并行度和取消标志
 | 
			
		||||
        var options = new ParallelOptions();
 | 
			
		||||
@@ -126,7 +126,7 @@ public static class ParallelExtensions
 | 
			
		||||
    /// <param name="body">异步执行的操作</param>
 | 
			
		||||
    /// <param name="cancellationToken">取消操作的标志</param>
 | 
			
		||||
    /// <returns>表示异步操作的任务</returns>
 | 
			
		||||
    public static Task ParallelForEachAsync<T>(this IList<T> source, Func<T, CancellationToken, ValueTask> body, CancellationToken cancellationToken = default)
 | 
			
		||||
    public static Task ParallelForEachAsync<T>(this IEnumerable<T> source, Func<T, CancellationToken, ValueTask> body, CancellationToken cancellationToken = default)
 | 
			
		||||
    {
 | 
			
		||||
        return ParallelForEachAsync(source, body, Environment.ProcessorCount, cancellationToken);
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -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.2" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
 
 | 
			
		||||
@@ -27,10 +27,15 @@ public abstract class PrimaryIdEntity : IPrimaryIdEntity
 | 
			
		||||
    public virtual long Id { get; set; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public interface IPrimaryKeyEntity
 | 
			
		||||
{
 | 
			
		||||
    string ExtJson { get; set; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 主键实体基类
 | 
			
		||||
/// </summary>
 | 
			
		||||
public abstract class PrimaryKeyEntity : PrimaryIdEntity
 | 
			
		||||
public abstract class PrimaryKeyEntity : PrimaryIdEntity, IPrimaryKeyEntity
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 拓展信息
 | 
			
		||||
 
 | 
			
		||||
@@ -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,10 +5,10 @@ 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 };
 | 
			
		||||
    }
 | 
			
		||||
@@ -33,11 +33,13 @@ public class ExpiringDictionary<TKey, TValue> : IDisposable
 | 
			
		||||
 | 
			
		||||
    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()
 | 
			
		||||
 
 | 
			
		||||
@@ -106,6 +106,17 @@ public static class Runtime
 | 
			
		||||
 | 
			
		||||
#if NET6_0_OR_GREATER
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public static Boolean IsSystemd
 | 
			
		||||
    {
 | 
			
		||||
        get
 | 
			
		||||
        {
 | 
			
		||||
            var id = Environment.GetEnvironmentVariable("INVOCATION_ID");
 | 
			
		||||
            return !string.IsNullOrEmpty(id);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public static Boolean? isLegacyWindows;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -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} 释放失败,当前信号量无需释放"));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@
 | 
			
		||||
 | 
			
		||||
using Newtonsoft.Json.Linq;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Foundation;
 | 
			
		||||
namespace ThingsGateway.NewLife.Json.Extension;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// JTokenUtil
 | 
			
		||||
@@ -131,6 +131,63 @@ public static class JTokenUtil
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 把任意对象转换为 JToken。
 | 
			
		||||
    /// 支持 JsonElement、JToken、本地 CLR 类型。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static JToken GetJTokenFromObj(this object value)
 | 
			
		||||
    {
 | 
			
		||||
        if (value == null)
 | 
			
		||||
            return JValue.CreateNull();
 | 
			
		||||
 | 
			
		||||
        switch (value)
 | 
			
		||||
        {
 | 
			
		||||
            case JToken jt:
 | 
			
		||||
                return jt;
 | 
			
		||||
#if NET6_0_OR_GREATER
 | 
			
		||||
            case System.Text.Json.JsonElement elem:
 | 
			
		||||
                return elem.ToJToken();
 | 
			
		||||
#endif
 | 
			
		||||
            case string s:
 | 
			
		||||
                return new JValue(s);
 | 
			
		||||
 | 
			
		||||
            case bool b:
 | 
			
		||||
                return new JValue(b);
 | 
			
		||||
 | 
			
		||||
            case int i:
 | 
			
		||||
                return new JValue(i);
 | 
			
		||||
 | 
			
		||||
            case long l:
 | 
			
		||||
                return new JValue(l);
 | 
			
		||||
 | 
			
		||||
            case double d:
 | 
			
		||||
                return new JValue(d);
 | 
			
		||||
 | 
			
		||||
            case float f:
 | 
			
		||||
                return new JValue(f);
 | 
			
		||||
 | 
			
		||||
            case decimal m:
 | 
			
		||||
                return new JValue(m);
 | 
			
		||||
 | 
			
		||||
            case DateTime dt:
 | 
			
		||||
                return new JValue(dt);
 | 
			
		||||
 | 
			
		||||
            case DateTimeOffset dto:
 | 
			
		||||
                return new JValue(dto);
 | 
			
		||||
 | 
			
		||||
            case Guid g:
 | 
			
		||||
                return new JValue(g);
 | 
			
		||||
 | 
			
		||||
            default:
 | 
			
		||||
                // 兜底:用 Newtonsoft 来包装成 JToken
 | 
			
		||||
                return JToken.FromObject(value);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    #region json
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -0,0 +1,176 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
//  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
 | 
			
		||||
//  此代码版权(除特别声明外的代码)归作者本人Diego所有
 | 
			
		||||
//  源代码使用协议遵循本仓库的开源协议及附加协议
 | 
			
		||||
//  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
 | 
			
		||||
//  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
 | 
			
		||||
//  使用文档:https://thingsgateway.cn/
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
#if NET6_0_OR_GREATER
 | 
			
		||||
using Newtonsoft.Json;
 | 
			
		||||
using Newtonsoft.Json.Linq;
 | 
			
		||||
 | 
			
		||||
using System;
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
using System.Numerics;
 | 
			
		||||
using System.Text.Json;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.NewLife.Json.Extension;
 | 
			
		||||
 | 
			
		||||
public static class JsonElementExtensions
 | 
			
		||||
{
 | 
			
		||||
    public static string GetValue(object src, bool parseBoolNumber = false)
 | 
			
		||||
    {
 | 
			
		||||
        if (src == null)
 | 
			
		||||
            return string.Empty;
 | 
			
		||||
 | 
			
		||||
        switch (src)
 | 
			
		||||
        {
 | 
			
		||||
            case string strValue:
 | 
			
		||||
                return strValue;
 | 
			
		||||
 | 
			
		||||
            case bool boolValue:
 | 
			
		||||
                return boolValue ? parseBoolNumber ? "1" : "True" : parseBoolNumber ? "0" : "False";
 | 
			
		||||
 | 
			
		||||
            case JsonElement elem: // System.Text.Json.JsonElement
 | 
			
		||||
                return elem.ValueKind switch
 | 
			
		||||
                {
 | 
			
		||||
                    JsonValueKind.String => elem.GetString(),
 | 
			
		||||
                    JsonValueKind.Number => elem.GetRawText(),  // 或 elem.GetDecimal().ToString()
 | 
			
		||||
                    JsonValueKind.True => "1",
 | 
			
		||||
                    JsonValueKind.False => "0",
 | 
			
		||||
                    JsonValueKind.Null => string.Empty,
 | 
			
		||||
                    _ => elem.GetRawText(), // 对象、数组等直接输出 JSON
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
            default:
 | 
			
		||||
                return (src).GetJTokenFromObj().ToString();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 将 System.Text.Json.JsonElement 递归转换为 Newtonsoft.Json.Linq.JToken
 | 
			
		||||
    /// - tryParseDates: 是否尝试把字符串解析为 DateTime/DateTimeOffset
 | 
			
		||||
    /// - tryParseGuids: 是否尝试把字符串解析为 Guid
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static JToken ToJToken(this JsonElement element, bool tryParseDates = true, bool tryParseGuids = true)
 | 
			
		||||
    {
 | 
			
		||||
        switch (element.ValueKind)
 | 
			
		||||
        {
 | 
			
		||||
            case JsonValueKind.Object:
 | 
			
		||||
                var obj = new JObject();
 | 
			
		||||
                foreach (var prop in element.EnumerateObject())
 | 
			
		||||
                    obj.Add(prop.Name, prop.Value.ToJToken(tryParseDates, tryParseGuids));
 | 
			
		||||
                return obj;
 | 
			
		||||
 | 
			
		||||
            case JsonValueKind.Array:
 | 
			
		||||
                var arr = new JArray();
 | 
			
		||||
                foreach (var item in element.EnumerateArray())
 | 
			
		||||
                    arr.Add(item.ToJToken(tryParseDates, tryParseGuids));
 | 
			
		||||
                return arr;
 | 
			
		||||
 | 
			
		||||
            case JsonValueKind.String:
 | 
			
		||||
                // 优先按语义尝试解析 Guid / DateTimeOffset / DateTime
 | 
			
		||||
                if (tryParseGuids && element.TryGetGuid(out Guid g))
 | 
			
		||||
                    return new JValue(g);
 | 
			
		||||
 | 
			
		||||
                if (tryParseDates && element.TryGetDateTimeOffset(out DateTimeOffset dto))
 | 
			
		||||
                    return new JValue(dto);
 | 
			
		||||
 | 
			
		||||
                if (tryParseDates && element.TryGetDateTime(out DateTime dt))
 | 
			
		||||
                    return new JValue(dt);
 | 
			
		||||
 | 
			
		||||
                return new JValue(element.GetString());
 | 
			
		||||
 | 
			
		||||
            case JsonValueKind.Number:
 | 
			
		||||
                return NumberElementToJToken(element);
 | 
			
		||||
 | 
			
		||||
            case JsonValueKind.True:
 | 
			
		||||
                return new JValue(true);
 | 
			
		||||
 | 
			
		||||
            case JsonValueKind.False:
 | 
			
		||||
                return new JValue(false);
 | 
			
		||||
 | 
			
		||||
            case JsonValueKind.Null:
 | 
			
		||||
            case JsonValueKind.Undefined:
 | 
			
		||||
            default:
 | 
			
		||||
                return JValue.CreateNull();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static JToken NumberElementToJToken(JsonElement element)
 | 
			
		||||
    {
 | 
			
		||||
        // 取原始文本(保持原始表示,方便处理超出标准类型范围的数字)
 | 
			
		||||
        string raw = element.GetRawText(); // 例如 "123", "1.23e4"
 | 
			
		||||
 | 
			
		||||
        // 如果不含小数点或指数,优先尝试整数解析(long / ulong / BigInteger)
 | 
			
		||||
        if (!raw.Contains('.') && !raw.Contains('e') && !raw.Contains('E'))
 | 
			
		||||
        {
 | 
			
		||||
            if (long.TryParse(raw, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out var l))
 | 
			
		||||
                return new JValue(l);
 | 
			
		||||
 | 
			
		||||
            if (ulong.TryParse(raw, NumberStyles.None, CultureInfo.InvariantCulture, out var ul))
 | 
			
		||||
                return new JValue(ul);
 | 
			
		||||
 | 
			
		||||
            if (BigInteger.TryParse(raw, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out var bi))
 | 
			
		||||
                // BigInteger 可能不被 JValue 直接识别为数字类型,使用 FromObject 保证正确表示
 | 
			
		||||
                return JToken.FromObject(bi);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 含小数或指数,或整数解析失败,尝试 decimal -> double
 | 
			
		||||
        if (decimal.TryParse(raw, NumberStyles.Float, CultureInfo.InvariantCulture, out var dec))
 | 
			
		||||
            return new JValue(dec);
 | 
			
		||||
 | 
			
		||||
        if (double.TryParse(raw, NumberStyles.Float, CultureInfo.InvariantCulture, out var d))
 | 
			
		||||
            return new JValue(d);
 | 
			
		||||
 | 
			
		||||
        // 最后兜底:把原始文本当字符串返回(极端情况)
 | 
			
		||||
        return new JValue(raw);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 把 JToken 转成“平面”字符串,适合用于日志或写入 CSV 的单元格:
 | 
			
		||||
    /// - string -> 原文
 | 
			
		||||
    /// - bool -> "1"/"0"
 | 
			
		||||
    /// - number -> 原始数字文本
 | 
			
		||||
    /// - date -> ISO 8601 (o)
 | 
			
		||||
    /// - object/array -> 紧凑的 JSON 文本
 | 
			
		||||
    /// - null/undefined -> empty string
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static string JTokenToPlainString(this JToken token)
 | 
			
		||||
    {
 | 
			
		||||
        if (token == null || token.Type == JTokenType.Null || token.Type == JTokenType.Undefined)
 | 
			
		||||
            return string.Empty;
 | 
			
		||||
 | 
			
		||||
        switch (token.Type)
 | 
			
		||||
        {
 | 
			
		||||
            case JTokenType.String:
 | 
			
		||||
                return token.Value<string>() ?? string.Empty;
 | 
			
		||||
 | 
			
		||||
            case JTokenType.Boolean:
 | 
			
		||||
                return token.Value<bool>() ? "1" : "0";
 | 
			
		||||
 | 
			
		||||
            case JTokenType.Integer:
 | 
			
		||||
            case JTokenType.Float:
 | 
			
		||||
                // 保持紧凑数字文本(不加引号)
 | 
			
		||||
                return token.ToString(Formatting.None);
 | 
			
		||||
 | 
			
		||||
            case JTokenType.Date:
 | 
			
		||||
                {
 | 
			
		||||
                    // Date 类型可能是 DateTime 或 DateTimeOffset
 | 
			
		||||
                    var val = token.Value<object>();
 | 
			
		||||
                    if (val is DateTimeOffset dto) return dto.ToString("o");
 | 
			
		||||
                    if (val is DateTime dt) return dt.ToString("o");
 | 
			
		||||
                    return token.ToString(Formatting.None);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            default:
 | 
			
		||||
                // 对象/数组等,返回紧凑 JSON 表示
 | 
			
		||||
                return token.ToString(Formatting.None);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
@@ -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
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,7 @@ public class Setting : Config<Setting>
 | 
			
		||||
    #region 属性
 | 
			
		||||
    /// <summary>是否启用全局调试。默认启用</summary>
 | 
			
		||||
    [Description("全局调试。XTrace.Debug")]
 | 
			
		||||
    [XmlIgnore, IgnoreDataMember]
 | 
			
		||||
    public Boolean Debug { get; set; } = true;
 | 
			
		||||
 | 
			
		||||
    /// <summary>日志等级,只输出大于等于该级别的日志,All/Debug/Info/Warn/Error/Fatal,默认Info</summary>
 | 
			
		||||
@@ -30,6 +31,7 @@ public class Setting : Config<Setting>
 | 
			
		||||
 | 
			
		||||
    /// <summary>文件日志目录。默认Log子目录</summary>
 | 
			
		||||
    [Description("文件日志目录。默认Log子目录")]
 | 
			
		||||
    [XmlIgnore, IgnoreDataMember]
 | 
			
		||||
    public String LogPath { get; set; } = "Logs/XLog";
 | 
			
		||||
 | 
			
		||||
    /// <summary>日志文件上限。超过上限后拆分新日志文件,默认5MB,0表示不限制大小</summary>
 | 
			
		||||
@@ -42,6 +44,7 @@ public class Setting : Config<Setting>
 | 
			
		||||
 | 
			
		||||
    /// <summary>日志文件格式。默认{0:yyyy_MM_dd}.log,支持日志等级如 {1}_{0:yyyy_MM_dd}.log</summary>
 | 
			
		||||
    [Description("日志文件格式。默认{0:yyyy_MM_dd}.log,支持日志等级如 {1}_{0:yyyy_MM_dd}.log")]
 | 
			
		||||
    [XmlIgnore, IgnoreDataMember]
 | 
			
		||||
    public String LogFileFormat { get; set; } = "{0:yyyy_MM_dd}.log";
 | 
			
		||||
 | 
			
		||||
    /// <summary>日志行格式。默认Time|ThreadId|Kind|Name|Message,还支持Level</summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -52,7 +52,7 @@
 | 
			
		||||
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
		<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
 | 
			
		||||
		<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
 | 
			
		||||
		<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
@namespace ThingsGateway.Razor
 | 
			
		||||
@typeparam TItem
 | 
			
		||||
 | 
			
		||||
<Table TItem="TItem" IsBordered="true" IsStriped="true" IsMultipleSelect="IsMultipleSelect" @ref="Instance" TableSize=TableSize SearchTemplate=SearchTemplate
 | 
			
		||||
<Table TItem="TItem" IsBordered="true" IsStriped="true" IsMultipleSelect="IsMultipleSelect" @ref="Instance" TableSize=TableSize SearchTemplate=SearchTemplate SelectedRows=SelectedRows SelectedRowsChanged=privateSelectedRowsChanged 
 | 
			
		||||
       IsPagination="IsPagination" PageItemsSource="PageItemsSource" IsFixedHeader="IsFixedHeader" IndentSize=24 RowHeight=RowHeight ShowSearchText="ShowSearchText"
 | 
			
		||||
       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
 | 
			
		||||
@@ -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!"
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,23 @@ namespace ThingsGateway.Razor;
 | 
			
		||||
[CascadingTypeParameter(nameof(TItem))]
 | 
			
		||||
public partial class DefaultTable<TItem> where TItem : class, new()
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc cref="Table{TItem}.SelectedRowsChanged"/>
 | 
			
		||||
    [Parameter]
 | 
			
		||||
    public EventCallback<List<TItem>> SelectedRowsChanged { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc cref="Table{TItem}.SelectedRows"/>
 | 
			
		||||
    [Parameter]
 | 
			
		||||
    public List<TItem> SelectedRows { get; set; } = new();
 | 
			
		||||
 | 
			
		||||
    private async Task privateSelectedRowsChanged(List<TItem> items)
 | 
			
		||||
    {
 | 
			
		||||
        SelectedRows = items;
 | 
			
		||||
        if (SelectedRowsChanged.HasDelegate)
 | 
			
		||||
            await SelectedRowsChanged.InvokeAsync(items);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc cref="Table{TItem}.AllowDragColumn"/>
 | 
			
		||||
    [Parameter]
 | 
			
		||||
    public bool AllowDragColumn { get; set; } = false;
 | 
			
		||||
@@ -186,14 +203,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; }
 | 
			
		||||
 
 | 
			
		||||
@@ -2,5 +2,5 @@
 | 
			
		||||
 | 
			
		||||
@if (show)
 | 
			
		||||
{
 | 
			
		||||
    <Spinner class="ms-auto"></Spinner>
 | 
			
		||||
    <Spinner Size="Size" class="ms-auto"></Spinner>
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -18,4 +18,7 @@ public partial class SpinnerComponent
 | 
			
		||||
        StateHasChanged();
 | 
			
		||||
    }
 | 
			
		||||
    private bool show;
 | 
			
		||||
 | 
			
		||||
    [Parameter]
 | 
			
		||||
    public Size Size { get; set; } = Size.Small;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -738,7 +738,7 @@ namespace ThingsGateway.SqlSugar
 | 
			
		||||
               .Where(it => it.Name == "Insertable")
 | 
			
		||||
               .Where(it => it.GetGenericArguments().Length != 0)
 | 
			
		||||
               .Where(it => it.GetParameters().Any(z => z.ParameterType.Name.StartsWith("IReadOnlyCollection")))
 | 
			
		||||
               .Where(it => it.Name == "Insertable");
 | 
			
		||||
               ;
 | 
			
		||||
                var method = methods.Single().MakeGenericMethod(newList.GetType().GetGenericArguments().First());
 | 
			
		||||
                InsertMethodInfo result = new InsertMethodInfo()
 | 
			
		||||
                {
 | 
			
		||||
@@ -751,11 +751,11 @@ namespace ThingsGateway.SqlSugar
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                var methods = this.Context.GetType().GetMethods()
 | 
			
		||||
                    .Where(it => it.Name == "Insertable")
 | 
			
		||||
                    .Where(it => it.Name == "InsertableT" || it.Name == "Insertable")
 | 
			
		||||
                    .Where(it => it.GetGenericArguments().Length != 0)
 | 
			
		||||
                    .Where(it => it.GetParameters().Any(z => z.ParameterType.Name == "T"))
 | 
			
		||||
                    .Where(it => it.Name == "Insertable");
 | 
			
		||||
                    .Where(it => it.GetParameters().Any(z => z.ParameterType.Name == "T"));
 | 
			
		||||
                var method = methods.Single().MakeGenericMethod(singleEntityObjectOrListObject.GetType());
 | 
			
		||||
 | 
			
		||||
                InsertMethodInfo result = new InsertMethodInfo()
 | 
			
		||||
                {
 | 
			
		||||
                    Context = this.Context,
 | 
			
		||||
@@ -826,8 +826,7 @@ namespace ThingsGateway.SqlSugar
 | 
			
		||||
                var methods = this.Context.GetType().GetMethods()
 | 
			
		||||
               .Where(it => it.Name == "Deleteable")
 | 
			
		||||
               .Where(it => it.GetGenericArguments().Length != 0)
 | 
			
		||||
               .Where(it => it.GetParameters().Any(z => z.Name != "pkValue" && z.ParameterType.Name.StartsWith("IReadOnlyCollection")))
 | 
			
		||||
               .Where(it => it.Name == "Deleteable");
 | 
			
		||||
               .Where(it => it.GetParameters().Any(z => z.Name != "pkValue" && z.ParameterType.Name.StartsWith("IReadOnlyCollection")));
 | 
			
		||||
                var method = methods.FirstOrDefault().MakeGenericMethod(newList.GetType().GetGenericArguments().FirstOrDefault());
 | 
			
		||||
                DeleteMethodInfo result = new DeleteMethodInfo()
 | 
			
		||||
                {
 | 
			
		||||
@@ -840,10 +839,9 @@ namespace ThingsGateway.SqlSugar
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                var methods = this.Context.GetType().GetMethods()
 | 
			
		||||
                    .Where(it => it.Name == "Deleteable")
 | 
			
		||||
                    .Where(it => it.Name == "Deleteable" || it.Name == "DeleteableT")
 | 
			
		||||
                    .Where(it => it.GetGenericArguments().Length != 0)
 | 
			
		||||
                    .Where(it => it.GetParameters().Any(z => z.ParameterType.Name == "T"))
 | 
			
		||||
                    .Where(it => it.Name == "Deleteable");
 | 
			
		||||
                    .Where(it => it.GetParameters().Any(z => z.ParameterType.Name == "T"));
 | 
			
		||||
                var method = methods.Single().MakeGenericMethod(singleEntityObjectOrListObject.GetType());
 | 
			
		||||
                DeleteMethodInfo result = new DeleteMethodInfo()
 | 
			
		||||
                {
 | 
			
		||||
@@ -911,8 +909,7 @@ namespace ThingsGateway.SqlSugar
 | 
			
		||||
                var methods = this.Context.GetType().GetMethods()
 | 
			
		||||
               .Where(it => it.Name == "Updateable")
 | 
			
		||||
               .Where(it => it.GetGenericArguments().Length != 0)
 | 
			
		||||
               .Where(it => it.GetParameters().Any(z => z.ParameterType.Name.StartsWith("IReadOnlyCollection")))
 | 
			
		||||
               .Where(it => it.Name == "Updateable");
 | 
			
		||||
               .Where(it => it.GetParameters().Any(z => z.ParameterType.Name.StartsWith("IReadOnlyCollection")));
 | 
			
		||||
                var method = methods.Single().MakeGenericMethod(newList.GetType().GetGenericArguments().First());
 | 
			
		||||
                UpdateMethodInfo result = new UpdateMethodInfo()
 | 
			
		||||
                {
 | 
			
		||||
@@ -925,10 +922,9 @@ namespace ThingsGateway.SqlSugar
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                var methods = this.Context.GetType().GetMethods()
 | 
			
		||||
                    .Where(it => it.Name == "Updateable")
 | 
			
		||||
                    .Where(it => it.Name == "UpdateableT" || it.Name == "Updateable")
 | 
			
		||||
                    .Where(it => it.GetGenericArguments().Length != 0)
 | 
			
		||||
                    .Where(it => it.GetParameters().Any(z => z.ParameterType.Name == "T"))
 | 
			
		||||
                    .Where(it => it.Name == "Updateable");
 | 
			
		||||
                    .Where(it => it.GetParameters().Any(z => z.ParameterType.Name == "T"));
 | 
			
		||||
                var method = methods.Single().MakeGenericMethod(singleEntityObjectOrListObject.GetType());
 | 
			
		||||
                UpdateMethodInfo result = new UpdateMethodInfo()
 | 
			
		||||
                {
 | 
			
		||||
@@ -943,10 +939,9 @@ namespace ThingsGateway.SqlSugar
 | 
			
		||||
        {
 | 
			
		||||
            UpdateExpressionMethodInfo result = new UpdateExpressionMethodInfo();
 | 
			
		||||
            var methods = this.Context.GetType().GetMethods()
 | 
			
		||||
             .Where(it => it.Name == "Updateable")
 | 
			
		||||
             .Where(it => it.Name == "UpdateableT" || it.Name == "Updateable")
 | 
			
		||||
             .Where(it => it.GetGenericArguments().Length != 0)
 | 
			
		||||
             .Where(it => it.GetParameters().Length == 0)
 | 
			
		||||
             .Where(it => it.Name == "Updateable");
 | 
			
		||||
             .Where(it => it.GetParameters().Length == 0);
 | 
			
		||||
            var method = methods.Single().MakeGenericMethod(entityType);
 | 
			
		||||
            result.Context = this.Context;
 | 
			
		||||
            result.MethodInfo = method;
 | 
			
		||||
@@ -1109,8 +1104,7 @@ namespace ThingsGateway.SqlSugar
 | 
			
		||||
                var methods = this.Context.GetType().GetMethods()
 | 
			
		||||
               .Where(it => it.Name == "Storageable")
 | 
			
		||||
               .Where(it => it.GetGenericArguments().Length != 0)
 | 
			
		||||
               .Where(it => it.GetParameters().Any(z => z.ParameterType.Name.StartsWith("IEnumerable")))
 | 
			
		||||
               .Where(it => it.Name == "Storageable");
 | 
			
		||||
               .Where(it => it.GetParameters().Any(z => z.ParameterType.Name.StartsWith("IEnumerable")));
 | 
			
		||||
                var method = methods.Single().MakeGenericMethod(newList.GetType().GetGenericArguments().First());
 | 
			
		||||
                StorageableMethodInfo result = new StorageableMethodInfo()
 | 
			
		||||
                {
 | 
			
		||||
@@ -1123,10 +1117,9 @@ namespace ThingsGateway.SqlSugar
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                var methods = this.Context.GetType().GetMethods()
 | 
			
		||||
                    .Where(it => it.Name == "Storageable")
 | 
			
		||||
                    .Where(it => it.Name == "StorageableT" || it.Name == "Storageable")
 | 
			
		||||
                    .Where(it => it.GetGenericArguments().Length != 0)
 | 
			
		||||
                    .Where(it => it.GetParameters().Any(z => z.ParameterType.Name == "T"))
 | 
			
		||||
                    .Where(it => it.Name == "Storageable");
 | 
			
		||||
                    .Where(it => it.GetParameters().Any(z => z.ParameterType.Name == "T"));
 | 
			
		||||
                var method = methods.Single().MakeGenericMethod(singleEntityObjectOrList.GetType());
 | 
			
		||||
                StorageableMethodInfo result = new StorageableMethodInfo()
 | 
			
		||||
                {
 | 
			
		||||
 
 | 
			
		||||
@@ -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.24</PluginVersion>
 | 
			
		||||
		<ProPluginVersion>10.11.24</ProPluginVersion>
 | 
			
		||||
		<DefaultVersion>10.11.24</DefaultVersion>
 | 
			
		||||
		<AuthenticationVersion>10.11.3</AuthenticationVersion>
 | 
			
		||||
		<SourceGeneratorVersion>10.11.3</SourceGeneratorVersion>
 | 
			
		||||
		<NET8Version>8.0.19</NET8Version>
 | 
			
		||||
		<NET9Version>9.0.8</NET9Version>
 | 
			
		||||
		<PluginVersion>10.11.67</PluginVersion>
 | 
			
		||||
		<ProPluginVersion>10.11.67</ProPluginVersion>
 | 
			
		||||
		<DefaultVersion>10.11.67</DefaultVersion>
 | 
			
		||||
		<AuthenticationVersion>10.11.6</AuthenticationVersion>
 | 
			
		||||
		<SourceGeneratorVersion>10.11.6</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>
 | 
			
		||||
 
 | 
			
		||||
@@ -136,6 +136,9 @@ public static class CSharpScriptEngineExtension
 | 
			
		||||
                    }
 | 
			
		||||
                    catch (NullReferenceException)
 | 
			
		||||
                    {
 | 
			
		||||
                        //如果编译失败,应该不重复编译,避免oom
 | 
			
		||||
                        Instance.Set<T>(field, null, TimeSpan.FromHours(1));
 | 
			
		||||
 | 
			
		||||
                        string exString = string.Format(CSScriptResource.CSScriptResource.Error1, typeof(T).FullName);
 | 
			
		||||
                        throw new(exString);
 | 
			
		||||
                    }
 | 
			
		||||
 
 | 
			
		||||
@@ -68,6 +68,7 @@ public class ModbusMasterDemo
 | 
			
		||||
 | 
			
		||||
        bytes = await device.ModbusReadAsync(new ModbusAddress()
 | 
			
		||||
        {
 | 
			
		||||
            Station = 1,
 | 
			
		||||
            StartAddress = 0,
 | 
			
		||||
            FunctionCode = 3,
 | 
			
		||||
            Length = 10,
 | 
			
		||||
@@ -92,6 +93,7 @@ public class ModbusMasterDemo
 | 
			
		||||
 | 
			
		||||
        write = await device.ModbusRequestAsync(new ModbusAddress()
 | 
			
		||||
        {
 | 
			
		||||
            Station = 1,
 | 
			
		||||
            StartAddress = 0,
 | 
			
		||||
            FunctionCode = 3,
 | 
			
		||||
            MasterWriteDatas = device.ThingsGatewayBitConverter.GetBytes(new double[] { 123.456, 123.456 })
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,7 @@ using Newtonsoft.Json.Linq;
 | 
			
		||||
using System.Linq.Expressions;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.Gateway.Application.Extensions;
 | 
			
		||||
using ThingsGateway.NewLife.Json.Extension;
 | 
			
		||||
using ThingsGateway.NewLife.Reflection;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Foundation;
 | 
			
		||||
@@ -68,12 +69,12 @@ public abstract class VariableObject
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public virtual JToken GetExpressionsValue(object value, VariableRuntimeProperty variableRuntimeProperty)
 | 
			
		||||
    {
 | 
			
		||||
        var jToken = JToken.FromObject(value);
 | 
			
		||||
        var jToken = value.GetJTokenFromObj();
 | 
			
		||||
        if (!string.IsNullOrEmpty(variableRuntimeProperty.Attribute.WriteExpressions))
 | 
			
		||||
        {
 | 
			
		||||
            object rawdata = jToken.GetObjectFromJToken();
 | 
			
		||||
            object data = variableRuntimeProperty.Attribute.WriteExpressions.GetExpressionsResult(rawdata, Device?.Logger);
 | 
			
		||||
            jToken = JToken.FromObject(data);
 | 
			
		||||
            jToken = data.GetJTokenFromObj();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return jToken;
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
                    {
 | 
			
		||||
@@ -98,19 +98,32 @@ public static class ChannelOptionsExtensions
 | 
			
		||||
 | 
			
		||||
        if (channelOptions.MaxClientCount > 0)
 | 
			
		||||
            config.SetMaxCount(channelOptions.MaxClientCount);
 | 
			
		||||
 | 
			
		||||
        config.SetTransportOption(new TouchSocket.Sockets.TransportOption()
 | 
			
		||||
        {
 | 
			
		||||
            SendPipeOptions = new System.IO.Pipelines.PipeOptions(
 | 
			
		||||
             minimumSegmentSize: 1024,
 | 
			
		||||
             useSynchronizationContext: false),
 | 
			
		||||
            ReceivePipeOptions = new System.IO.Pipelines.PipeOptions(
 | 
			
		||||
             minimumSegmentSize: 1024,
 | 
			
		||||
                pauseWriterThreshold: 1024 * 1024,
 | 
			
		||||
                resumeWriterThreshold: 1024 * 512,
 | 
			
		||||
                useSynchronizationContext: false)
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        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 +138,12 @@ public static class ChannelOptionsExtensions
 | 
			
		||||
    /// <param name="config">配置</param>
 | 
			
		||||
    /// <param name="channelOptions">串口配置</param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public static SerialPortChannel GetSerialPortWithOption(this TouchSocketConfig config, IChannelOptions channelOptions)
 | 
			
		||||
    private 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 +156,7 @@ public static class ChannelOptionsExtensions
 | 
			
		||||
    /// <param name="channelOptions">通道配置</param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    /// <exception cref="ArgumentNullException"></exception>
 | 
			
		||||
    public static TcpClientChannel GetTcpClientWithIPHost(this TouchSocketConfig config, IChannelOptions channelOptions)
 | 
			
		||||
    private static TcpClientChannel GetTcpClient(this TouchSocketConfig config, IChannelOptions channelOptions)
 | 
			
		||||
    {
 | 
			
		||||
        var remoteUrl = channelOptions.RemoteUrl;
 | 
			
		||||
        var bindUrl = channelOptions.BindUrl;
 | 
			
		||||
@@ -166,7 +178,7 @@ public static class ChannelOptionsExtensions
 | 
			
		||||
    /// <param name="channelOptions">通道配置</param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    /// <exception cref="ArgumentNullException"></exception>
 | 
			
		||||
    public static IChannel GetTcpServiceWithBindIPHost(this TouchSocketConfig config, IChannelOptions channelOptions)
 | 
			
		||||
    private static IChannel GetTcpService(this TouchSocketConfig config, IChannelOptions channelOptions)
 | 
			
		||||
    {
 | 
			
		||||
        var bindUrl = channelOptions.BindUrl;
 | 
			
		||||
        bindUrl.ThrowIfNull(nameof(bindUrl));
 | 
			
		||||
@@ -194,7 +206,7 @@ public static class ChannelOptionsExtensions
 | 
			
		||||
    /// <param name="channelOptions">通道配置</param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    /// <exception cref="ArgumentNullException"></exception>
 | 
			
		||||
    public static UdpSessionChannel GetUdpSessionWithIPHost(this TouchSocketConfig config, IChannelOptions channelOptions)
 | 
			
		||||
    private static UdpSessionChannel GetUdpSession(this TouchSocketConfig config, IChannelOptions channelOptions)
 | 
			
		||||
    {
 | 
			
		||||
        var remoteUrl = channelOptions.RemoteUrl;
 | 
			
		||||
        var bindUrl = channelOptions.BindUrl;
 | 
			
		||||
 
 | 
			
		||||
@@ -57,7 +57,7 @@ public interface IChannel : ISetupConfigObject, IDisposable, IClosableClient, IC
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public ChannelEventHandler Stoping { get; }
 | 
			
		||||
 | 
			
		||||
    void ResetSign(int minSign = 0, int maxSign = ushort.MaxValue);
 | 
			
		||||
    void ResetSign(int minSign = 1, int maxSign = ushort.MaxValue - 1);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,7 @@ namespace ThingsGateway.Foundation;
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 终端通道
 | 
			
		||||
/// </summary>
 | 
			
		||||
public interface IClientChannel : IChannel, ISender, IClient, IClientSender, IOnlineClient
 | 
			
		||||
public interface IClientChannel : IChannel, ISender, IClient, IClientSender, IOnlineClient, IDependencyClient
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 当前通道的数据处理适配器
 | 
			
		||||
@@ -30,6 +30,7 @@ public interface IClientChannel : IChannel, ISender, IClient, IClientSender, IOn
 | 
			
		||||
    WaitHandlePool<MessageBase> WaitHandlePool { get; }
 | 
			
		||||
 | 
			
		||||
    WaitLock GetLock(string key);
 | 
			
		||||
    void LogSeted(bool logSeted);
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 设置数据处理适配器
 | 
			
		||||
 
 | 
			
		||||
@@ -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,19 +34,9 @@ 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)
 | 
			
		||||
 | 
			
		||||
    public void ResetSign(int minSign = 1, int maxSign = ushort.MaxValue - 1)
 | 
			
		||||
    {
 | 
			
		||||
        var pool = WaitHandlePool;
 | 
			
		||||
        WaitHandlePool = new WaitHandlePool<MessageBase>(minSign, maxSign);
 | 
			
		||||
@@ -73,7 +68,7 @@ public class OtherChannel : SetupConfigObject, IClientChannel
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 等待池
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public WaitHandlePool<MessageBase> WaitHandlePool { get; internal set; } = new(0, ushort.MaxValue);
 | 
			
		||||
    public WaitHandlePool<MessageBase> WaitHandlePool { get; internal set; } = new(1, ushort.MaxValue - 1);
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public WaitLock WaitLock => ChannelOptions.WaitLock;
 | 
			
		||||
@@ -83,15 +78,29 @@ 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;
 | 
			
		||||
    }
 | 
			
		||||
    public void LogSeted(bool logSeted)
 | 
			
		||||
    {
 | 
			
		||||
        logSet = logSeted;
 | 
			
		||||
    }
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 设置数据处理适配器。
 | 
			
		||||
 
 | 
			
		||||
@@ -34,7 +34,26 @@ public static class PluginUtil
 | 
			
		||||
 | 
			
		||||
            if (channelOptions.ChannelType == ChannelTypeEnum.TcpClient)
 | 
			
		||||
            {
 | 
			
		||||
                action += a => a.UseReconnection<ITcpClient>();
 | 
			
		||||
                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;
 | 
			
		||||
@@ -27,7 +31,7 @@ public class SerialPortChannel : SerialPortClient, IClientChannel
 | 
			
		||||
 | 
			
		||||
    public override TouchSocketConfig Config => base.Config ?? ChannelOptions.Config;
 | 
			
		||||
 | 
			
		||||
    public void ResetSign(int minSign = 0, int maxSign = ushort.MaxValue)
 | 
			
		||||
    public void ResetSign(int minSign = 1, int maxSign = ushort.MaxValue - 1)
 | 
			
		||||
    {
 | 
			
		||||
        var pool = WaitHandlePool;
 | 
			
		||||
        WaitHandlePool = new WaitHandlePool<MessageBase>(minSign, maxSign);
 | 
			
		||||
@@ -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,16 @@ public class SerialPortChannel : SerialPortClient, IClientChannel
 | 
			
		||||
    {
 | 
			
		||||
        if (adapter is SingleStreamDataHandlingAdapter singleStreamDataHandlingAdapter)
 | 
			
		||||
            SetAdapter(singleStreamDataHandlingAdapter);
 | 
			
		||||
        if (adapter is IDeviceDataHandleAdapter deviceDataHandleAdapter)
 | 
			
		||||
            _deviceDataHandleAdapter = deviceDataHandleAdapter;
 | 
			
		||||
 | 
			
		||||
        logSet = false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void LogSeted(bool logSeted)
 | 
			
		||||
    {
 | 
			
		||||
        logSet = logSeted;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public ChannelEventHandler Started { get; } = new();
 | 
			
		||||
 | 
			
		||||
@@ -82,7 +91,7 @@ public class SerialPortChannel : SerialPortClient, IClientChannel
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 等待池
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public WaitHandlePool<MessageBase> WaitHandlePool { get; internal set; } = new(0, ushort.MaxValue);
 | 
			
		||||
    public WaitHandlePool<MessageBase> WaitHandlePool { get; internal set; } = new(1, ushort.MaxValue - 1);
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public WaitLock WaitLock => ChannelOptions.WaitLock;
 | 
			
		||||
@@ -192,16 +201,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)
 | 
			
		||||
    {
 | 
			
		||||
@@ -24,22 +28,19 @@ public class TcpClientChannel : TcpClient, IClientChannel
 | 
			
		||||
        ResetSign();
 | 
			
		||||
    }
 | 
			
		||||
    public override TouchSocketConfig Config => base.Config ?? ChannelOptions.Config;
 | 
			
		||||
    public void ResetSign(int minSign = 0, int maxSign = ushort.MaxValue)
 | 
			
		||||
    public void ResetSign(int minSign = 1, int maxSign = ushort.MaxValue - 1)
 | 
			
		||||
    {
 | 
			
		||||
        var pool = WaitHandlePool;
 | 
			
		||||
        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,12 @@ public class TcpClientChannel : TcpClient, IClientChannel
 | 
			
		||||
    {
 | 
			
		||||
        if (adapter is SingleStreamDataHandlingAdapter singleStreamDataHandlingAdapter)
 | 
			
		||||
            SetAdapter(singleStreamDataHandlingAdapter);
 | 
			
		||||
        if (adapter is IDeviceDataHandleAdapter deviceDataHandleAdapter)
 | 
			
		||||
            _deviceDataHandleAdapter = deviceDataHandleAdapter;
 | 
			
		||||
 | 
			
		||||
        logSet = false;
 | 
			
		||||
    }
 | 
			
		||||
    public void LogSeted(bool logSeted)
 | 
			
		||||
    {
 | 
			
		||||
        logSet = logSeted;
 | 
			
		||||
    }
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public ChannelReceivedEventHandler ChannelReceived { get; } = new();
 | 
			
		||||
@@ -78,7 +83,7 @@ public class TcpClientChannel : TcpClient, IClientChannel
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 等待池
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public WaitHandlePool<MessageBase> WaitHandlePool { get; internal set; } = new(0, ushort.MaxValue);
 | 
			
		||||
    public WaitHandlePool<MessageBase> WaitHandlePool { get; internal set; } = new(1, ushort.MaxValue - 1);
 | 
			
		||||
    public virtual WaitLock GetLock(string key) => WaitLock;
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
@@ -179,16 +184,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();
 | 
			
		||||
 | 
			
		||||
@@ -205,7 +210,7 @@ public class TcpServiceChannel<TClient> : TcpServiceChannelBase<TClient>, IChann
 | 
			
		||||
    }
 | 
			
		||||
    public int MinSign { get; private set; } = 0;
 | 
			
		||||
    public int MaxSign { get; private set; } = ushort.MaxValue;
 | 
			
		||||
    public void ResetSign(int minSign = 0, int maxSign = ushort.MaxValue)
 | 
			
		||||
    public void ResetSign(int minSign = 1, int maxSign = ushort.MaxValue - 1)
 | 
			
		||||
    {
 | 
			
		||||
        MinSign = minSign;
 | 
			
		||||
        MaxSign = maxSign;
 | 
			
		||||
@@ -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,10 +40,14 @@ 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)
 | 
			
		||||
    public void LogSeted(bool logSeted)
 | 
			
		||||
    {
 | 
			
		||||
        logSet = logSeted;
 | 
			
		||||
    }
 | 
			
		||||
    public void ResetSign(int minSign = 1, int maxSign = ushort.MaxValue - 1)
 | 
			
		||||
    {
 | 
			
		||||
        var pool = WaitHandlePool;
 | 
			
		||||
        WaitHandlePool = new WaitHandlePool<MessageBase>(minSign, maxSign);
 | 
			
		||||
@@ -76,7 +82,7 @@ public class TcpSessionClientChannel : TcpSessionClient, IClientChannel
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 等待池
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public WaitHandlePool<MessageBase> WaitHandlePool { get; private set; } = new(0, ushort.MaxValue);
 | 
			
		||||
    public WaitHandlePool<MessageBase> WaitHandlePool { get; private set; } = new(1, ushort.MaxValue - 1);
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public WaitLock WaitLock { get; internal set; } = new(nameof(TcpSessionClientChannel));
 | 
			
		||||
@@ -145,15 +151,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,27 +30,31 @@ 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;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    public void LogSeted(bool logSeted)
 | 
			
		||||
    {
 | 
			
		||||
        logSet = logSeted;
 | 
			
		||||
    }
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public void SetDataHandlingAdapter(DataHandlingAdapter adapter)
 | 
			
		||||
    {
 | 
			
		||||
        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)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public void ResetSign(int minSign = 1, int maxSign = ushort.MaxValue - 1)
 | 
			
		||||
    {
 | 
			
		||||
        var pool = WaitHandlePool;
 | 
			
		||||
        WaitHandlePool = new WaitHandlePool<MessageBase>(minSign, maxSign);
 | 
			
		||||
@@ -85,7 +93,7 @@ public class UdpSessionChannel : UdpSession, IClientChannel
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 等待池
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public WaitHandlePool<MessageBase> WaitHandlePool { get; set; } = new(0, ushort.MaxValue);
 | 
			
		||||
    public WaitHandlePool<MessageBase> WaitHandlePool { get; internal set; } = new(1, ushort.MaxValue - 1);
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public WaitLock WaitLock => ChannelOptions.WaitLock;
 | 
			
		||||
@@ -196,16 +204,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,93 @@ 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()) { ErrorMessage = $"Timeout, sign: {sign}" }
 | 
			
		||||
                    : new MessageBase(new OperationCanceledException());
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                return new MessageBase(ex);
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
                reusableTimeout.Set();
 | 
			
		||||
                timeoutStatus = reusableTimeout.TimeoutStatus;
 | 
			
		||||
                _reusableTimeouts.Return(reusableTimeout);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var result = waitData.Check(ctsTime.Token);
 | 
			
		||||
 | 
			
		||||
            if (result.IsSuccess)
 | 
			
		||||
            if (waitData.Status == WaitDataStatus.Success)
 | 
			
		||||
            {
 | 
			
		||||
                return waitData.CompletedData;
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                return new MessageBase(result);
 | 
			
		||||
                var operResult = waitData.Check(timeoutStatus);
 | 
			
		||||
                return new MessageBase(operResult) { ErrorMessage = $"{operResult.ErrorMessage}, sign: {sign}" };
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        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 +635,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 +661,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 +680,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 +870,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 +943,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/>
 | 
			
		||||
@@ -1026,6 +984,7 @@ public abstract class DeviceBase : AsyncAndSyncDisposableObject, IDevice
 | 
			
		||||
        {
 | 
			
		||||
            lock (Channel)
 | 
			
		||||
            {
 | 
			
		||||
 | 
			
		||||
                Channel.Starting.Remove(ChannelStarting);
 | 
			
		||||
                Channel.Stoped.Remove(ChannelStoped);
 | 
			
		||||
                Channel.Started.Remove(ChannelStarted);
 | 
			
		||||
@@ -1066,9 +1025,14 @@ public abstract class DeviceBase : AsyncAndSyncDisposableObject, IDevice
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                Channel.Collects.Remove(this);
 | 
			
		||||
                if (Channel is IClientChannel clientChannel)
 | 
			
		||||
                {
 | 
			
		||||
                    clientChannel.LogSeted(false);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        _reusableTimeouts?.SafeDispose();
 | 
			
		||||
        _deviceLogger?.TryDispose();
 | 
			
		||||
        base.Dispose(disposing);
 | 
			
		||||
    }
 | 
			
		||||
@@ -1118,8 +1082,15 @@ public abstract class DeviceBase : AsyncAndSyncDisposableObject, IDevice
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            Channel.Collects.Remove(this);
 | 
			
		||||
 | 
			
		||||
            if (Channel is IClientChannel clientChannel)
 | 
			
		||||
            {
 | 
			
		||||
                clientChannel.LogSeted(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        _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),
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ using System.Globalization;
 | 
			
		||||
using System.Net;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.NewLife.Extension;
 | 
			
		||||
using ThingsGateway.NewLife.Json.Extension;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Foundation.Extension.String;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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.57" />
 | 
			
		||||
		<PackageReference Include="TouchSocket.SerialPorts" Version="4.0.0-beta.57" />
 | 
			
		||||
	</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);
 | 
			
		||||
@@ -435,5 +435,311 @@ public static class ThingsGatewayBitConverterExtension
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 根据数据类型获取实际值
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static bool GetChangedDataFormJToken(
 | 
			
		||||
        JToken jToken,
 | 
			
		||||
        DataTypeEnum dataType,
 | 
			
		||||
        int arrayLength,
 | 
			
		||||
        object? oldValue,
 | 
			
		||||
        out object? result)
 | 
			
		||||
    {
 | 
			
		||||
        switch (dataType)
 | 
			
		||||
        {
 | 
			
		||||
            case DataTypeEnum.Boolean:
 | 
			
		||||
                if (arrayLength > 1)
 | 
			
		||||
                {
 | 
			
		||||
                    var newVal = jToken.ToObject<Boolean[]>();
 | 
			
		||||
                    if (oldValue is bool[] oldArr && newVal.SequenceEqual(oldArr))
 | 
			
		||||
                    {
 | 
			
		||||
                        result = oldValue;
 | 
			
		||||
                        return false;
 | 
			
		||||
                    }
 | 
			
		||||
                    result = newVal;
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    var newVal = jToken.ToObject<Boolean>();
 | 
			
		||||
                    if (oldValue is bool oldVal && oldVal == newVal)
 | 
			
		||||
                    {
 | 
			
		||||
                        result = oldValue;
 | 
			
		||||
                        return false;
 | 
			
		||||
                    }
 | 
			
		||||
                    result = newVal;
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            case DataTypeEnum.Byte:
 | 
			
		||||
                if (arrayLength > 1)
 | 
			
		||||
                {
 | 
			
		||||
                    var newVal = jToken.ToObject<Byte[]>();
 | 
			
		||||
                    if (oldValue is byte[] oldArr && newVal.SequenceEqual(oldArr))
 | 
			
		||||
                    {
 | 
			
		||||
                        result = oldValue;
 | 
			
		||||
                        return false;
 | 
			
		||||
                    }
 | 
			
		||||
                    result = newVal;
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    var newVal = jToken.ToObject<Byte>();
 | 
			
		||||
                    if (oldValue is byte oldVal && oldVal == newVal)
 | 
			
		||||
                    {
 | 
			
		||||
                        result = oldValue;
 | 
			
		||||
                        return false;
 | 
			
		||||
                    }
 | 
			
		||||
                    result = newVal;
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            case DataTypeEnum.Int16:
 | 
			
		||||
                if (arrayLength > 1)
 | 
			
		||||
                {
 | 
			
		||||
                    var newVal = jToken.ToObject<Int16[]>();
 | 
			
		||||
                    if (oldValue is short[] oldArr && newVal.SequenceEqual(oldArr))
 | 
			
		||||
                    {
 | 
			
		||||
                        result = oldValue;
 | 
			
		||||
                        return false;
 | 
			
		||||
                    }
 | 
			
		||||
                    result = newVal;
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    var newVal = jToken.ToObject<Int16>();
 | 
			
		||||
                    if (oldValue is short oldVal && oldVal == newVal)
 | 
			
		||||
                    {
 | 
			
		||||
                        result = oldValue;
 | 
			
		||||
                        return false;
 | 
			
		||||
                    }
 | 
			
		||||
                    result = newVal;
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            case DataTypeEnum.UInt16:
 | 
			
		||||
                if (arrayLength > 1)
 | 
			
		||||
                {
 | 
			
		||||
                    var newVal = jToken.ToObject<UInt16[]>();
 | 
			
		||||
                    if (oldValue is ushort[] oldArr && newVal.SequenceEqual(oldArr))
 | 
			
		||||
                    {
 | 
			
		||||
                        result = oldValue;
 | 
			
		||||
                        return false;
 | 
			
		||||
                    }
 | 
			
		||||
                    result = newVal;
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    var newVal = jToken.ToObject<UInt16>();
 | 
			
		||||
                    if (oldValue is ushort oldVal && oldVal == newVal)
 | 
			
		||||
                    {
 | 
			
		||||
                        result = oldValue;
 | 
			
		||||
                        return false;
 | 
			
		||||
                    }
 | 
			
		||||
                    result = newVal;
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            case DataTypeEnum.Int32:
 | 
			
		||||
                if (arrayLength > 1)
 | 
			
		||||
                {
 | 
			
		||||
                    var newVal = jToken.ToObject<Int32[]>();
 | 
			
		||||
                    if (oldValue is int[] oldArr && newVal.SequenceEqual(oldArr))
 | 
			
		||||
                    {
 | 
			
		||||
                        result = oldValue;
 | 
			
		||||
                        return false;
 | 
			
		||||
                    }
 | 
			
		||||
                    result = newVal;
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    var newVal = jToken.ToObject<Int32>();
 | 
			
		||||
                    if (oldValue is int oldVal && oldVal == newVal)
 | 
			
		||||
                    {
 | 
			
		||||
                        result = oldValue;
 | 
			
		||||
                        return false;
 | 
			
		||||
                    }
 | 
			
		||||
                    result = newVal;
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            case DataTypeEnum.UInt32:
 | 
			
		||||
                if (arrayLength > 1)
 | 
			
		||||
                {
 | 
			
		||||
                    var newVal = jToken.ToObject<UInt32[]>();
 | 
			
		||||
                    if (oldValue is uint[] oldArr && newVal.SequenceEqual(oldArr))
 | 
			
		||||
                    {
 | 
			
		||||
                        result = oldValue;
 | 
			
		||||
                        return false;
 | 
			
		||||
                    }
 | 
			
		||||
                    result = newVal;
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    var newVal = jToken.ToObject<UInt32>();
 | 
			
		||||
                    if (oldValue is uint oldVal && oldVal == newVal)
 | 
			
		||||
                    {
 | 
			
		||||
                        result = oldValue;
 | 
			
		||||
                        return false;
 | 
			
		||||
                    }
 | 
			
		||||
                    result = newVal;
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            case DataTypeEnum.Int64:
 | 
			
		||||
                if (arrayLength > 1)
 | 
			
		||||
                {
 | 
			
		||||
                    var newVal = jToken.ToObject<Int64[]>();
 | 
			
		||||
                    if (oldValue is long[] oldArr && newVal.SequenceEqual(oldArr))
 | 
			
		||||
                    {
 | 
			
		||||
                        result = oldValue;
 | 
			
		||||
                        return false;
 | 
			
		||||
                    }
 | 
			
		||||
                    result = newVal;
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    var newVal = jToken.ToObject<Int64>();
 | 
			
		||||
                    if (oldValue is long oldVal && oldVal == newVal)
 | 
			
		||||
                    {
 | 
			
		||||
                        result = oldValue;
 | 
			
		||||
                        return false;
 | 
			
		||||
                    }
 | 
			
		||||
                    result = newVal;
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            case DataTypeEnum.UInt64:
 | 
			
		||||
                if (arrayLength > 1)
 | 
			
		||||
                {
 | 
			
		||||
                    var newVal = jToken.ToObject<UInt64[]>();
 | 
			
		||||
                    if (oldValue is ulong[] oldArr && newVal.SequenceEqual(oldArr))
 | 
			
		||||
                    {
 | 
			
		||||
                        result = oldValue;
 | 
			
		||||
                        return false;
 | 
			
		||||
                    }
 | 
			
		||||
                    result = newVal;
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    var newVal = jToken.ToObject<UInt64>();
 | 
			
		||||
                    if (oldValue is ulong oldVal && oldVal == newVal)
 | 
			
		||||
                    {
 | 
			
		||||
                        result = oldValue;
 | 
			
		||||
                        return false;
 | 
			
		||||
                    }
 | 
			
		||||
                    result = newVal;
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            case DataTypeEnum.Float:
 | 
			
		||||
                if (arrayLength > 1)
 | 
			
		||||
                {
 | 
			
		||||
                    var newVal = jToken.ToObject<Single[]>();
 | 
			
		||||
                    if (oldValue is float[] oldArr && newVal.SequenceEqual(oldArr))
 | 
			
		||||
                    {
 | 
			
		||||
                        result = oldValue;
 | 
			
		||||
                        return false;
 | 
			
		||||
                    }
 | 
			
		||||
                    result = newVal;
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    var newVal = jToken.ToObject<Single>();
 | 
			
		||||
                    if (oldValue is float oldVal && oldVal == newVal)
 | 
			
		||||
                    {
 | 
			
		||||
                        result = oldValue;
 | 
			
		||||
                        return false;
 | 
			
		||||
                    }
 | 
			
		||||
                    result = newVal;
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            case DataTypeEnum.Double:
 | 
			
		||||
                if (arrayLength > 1)
 | 
			
		||||
                {
 | 
			
		||||
                    var newVal = jToken.ToObject<Double[]>();
 | 
			
		||||
                    if (oldValue is double[] oldArr && newVal.SequenceEqual(oldArr))
 | 
			
		||||
                    {
 | 
			
		||||
                        result = oldValue;
 | 
			
		||||
                        return false;
 | 
			
		||||
                    }
 | 
			
		||||
                    result = newVal;
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    var newVal = jToken.ToObject<Double>();
 | 
			
		||||
                    if (oldValue is double oldVal && oldVal == newVal)
 | 
			
		||||
                    {
 | 
			
		||||
                        result = oldValue;
 | 
			
		||||
                        return false;
 | 
			
		||||
                    }
 | 
			
		||||
                    result = newVal;
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
            case DataTypeEnum.Decimal:
 | 
			
		||||
                if (arrayLength > 1)
 | 
			
		||||
                {
 | 
			
		||||
                    var newVal = jToken.ToObject<Decimal[]>();
 | 
			
		||||
                    if (oldValue is decimal[] oldArr && newVal.SequenceEqual(oldArr))
 | 
			
		||||
                    {
 | 
			
		||||
                        result = oldValue;
 | 
			
		||||
                        return false;
 | 
			
		||||
                    }
 | 
			
		||||
                    result = newVal;
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    var newVal = jToken.ToObject<Decimal>();
 | 
			
		||||
                    if (oldValue is decimal oldVal && oldVal == newVal)
 | 
			
		||||
                    {
 | 
			
		||||
                        result = oldValue;
 | 
			
		||||
                        return false;
 | 
			
		||||
                    }
 | 
			
		||||
                    result = newVal;
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
            case DataTypeEnum.String:
 | 
			
		||||
            default:
 | 
			
		||||
                if (arrayLength > 1)
 | 
			
		||||
                {
 | 
			
		||||
                    var newArr = new string[arrayLength];
 | 
			
		||||
                    for (int i = 0; i < arrayLength; i++)
 | 
			
		||||
                    {
 | 
			
		||||
                        newArr[i] = jToken.ToObject<string>();
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    if (oldValue is string[] oldArr && newArr.SequenceEqual(oldArr))
 | 
			
		||||
                    {
 | 
			
		||||
                        result = oldValue;
 | 
			
		||||
                        return false;
 | 
			
		||||
                    }
 | 
			
		||||
                    result = newArr;
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    var str = jToken.ToObject<string>();
 | 
			
		||||
                    if (oldValue is string oldStr && oldStr == str)
 | 
			
		||||
                    {
 | 
			
		||||
                        result = oldStr;
 | 
			
		||||
                        return false;
 | 
			
		||||
                    }
 | 
			
		||||
                    result = str;
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    #endregion 获取对应数据类型的数据
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,30 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
//  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
 | 
			
		||||
//  此代码版权(除特别声明外的代码)归作者本人Diego所有
 | 
			
		||||
//  源代码使用协议遵循本仓库的开源协议及附加协议
 | 
			
		||||
//  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
 | 
			
		||||
//  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
 | 
			
		||||
//  使用文档:https://thingsgateway.cn/
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
 | 
			
		||||
[AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)]
 | 
			
		||||
public class StringNotMemoryAttribute : ValidationAttribute
 | 
			
		||||
{
 | 
			
		||||
    public StringNotMemoryAttribute()
 | 
			
		||||
      : base("The field {0} cannot be 'Memory'.")
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override bool IsValid(object value)
 | 
			
		||||
    {
 | 
			
		||||
        var str = value as string;
 | 
			
		||||
        return !string.Equals(str, "Memory", StringComparison.OrdinalIgnoreCase);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -48,18 +48,6 @@ public class ControlController : ControllerBase, IRpcServer
 | 
			
		||||
        App.CacheService.Clear();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 删除通道/设备缓存
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    [HttpPost("removeCache")]
 | 
			
		||||
    [DisplayName("删除通道/设备缓存")]
 | 
			
		||||
    [TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Post)]
 | 
			
		||||
    public void RemoveCache()
 | 
			
		||||
    {
 | 
			
		||||
        App.GetService<IDeviceService>().DeleteDeviceFromCache();
 | 
			
		||||
        App.GetService<IChannelService>().DeleteChannelFromCache();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 控制设备线程暂停
 | 
			
		||||
@@ -226,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>
 | 
			
		||||
 
 | 
			
		||||
@@ -77,7 +77,7 @@ public partial class ManagementController : ControllerBase, IRpcServer
 | 
			
		||||
        App.GetService<IDevicePageService>().ClearDeviceAsync(restart);
 | 
			
		||||
    [HttpPost]
 | 
			
		||||
    [TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Post)]
 | 
			
		||||
    public Task ClearRulesAsync() => App.GetService<IRulesService>().ClearRulesAsync();
 | 
			
		||||
    public Task ClearRulesAsync() => App.GetService<IRulesPageService>().ClearRulesAsync();
 | 
			
		||||
    [HttpPost]
 | 
			
		||||
    [TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Post)]
 | 
			
		||||
    public Task<bool> ClearVariableAsync(bool restart) =>
 | 
			
		||||
@@ -140,7 +140,7 @@ public partial class ManagementController : ControllerBase, IRpcServer
 | 
			
		||||
    public Task DeleteRuleRuntimesAsync(List<long> ids) => App.GetService<IRulesEngineHostedService>().DeleteRuleRuntimesAsync(ids);
 | 
			
		||||
    [HttpPost]
 | 
			
		||||
    [TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Post)]
 | 
			
		||||
    public Task<bool> DeleteRulesAsync(List<long> ids) => App.GetService<IRulesService>().DeleteRulesAsync(ids);
 | 
			
		||||
    public Task<bool> DeleteRulesAsync(List<long> ids) => App.GetService<IRulesPageService>().DeleteRulesAsync(ids);
 | 
			
		||||
 | 
			
		||||
    [HttpPost]
 | 
			
		||||
    [TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Post)]
 | 
			
		||||
@@ -200,13 +200,6 @@ public partial class ManagementController : ControllerBase, IRpcServer
 | 
			
		||||
    public Task<List<Variable>> GetVariableListAsync([FromBody] GetListRequest<QueryPageOptions> request) =>
 | 
			
		||||
        App.GetService<IVariablePageService>().GetVariableListAsync(request.Options, request.Max);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    [HttpGet]
 | 
			
		||||
    [TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Get)]
 | 
			
		||||
    public Task<List<Rules>> GetAllRulesAsync() => App.GetService<IRulesService>().GetAllRulesAsync();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    [HttpGet]
 | 
			
		||||
    [TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Get)]
 | 
			
		||||
    public Task<string> GetChannelNameAsync(long id) =>
 | 
			
		||||
@@ -345,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) =>
 | 
			
		||||
@@ -458,7 +459,7 @@ public partial class ManagementController : ControllerBase, IRpcServer
 | 
			
		||||
    [HttpPost]
 | 
			
		||||
    [TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Post)]
 | 
			
		||||
    public Task<QueryData<Rules>> RulesPageAsync([FromBody] KVQueryPageOptions option) =>
 | 
			
		||||
        App.GetService<IRulesService>().RulesPageAsync(option.Options, option.FilterKeyValueAction);
 | 
			
		||||
        App.GetService<IRulesPageService>().RulesPageAsync(option.Options, option.FilterKeyValueAction);
 | 
			
		||||
 | 
			
		||||
    [HttpPost]
 | 
			
		||||
    [TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Post)]
 | 
			
		||||
@@ -478,7 +479,7 @@ public partial class ManagementController : ControllerBase, IRpcServer
 | 
			
		||||
    [HttpPost]
 | 
			
		||||
    [TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Post)]
 | 
			
		||||
    public Task<bool> SaveRulesAsync([FromBody] Rules input, ItemChangedType type) =>
 | 
			
		||||
        App.GetService<IRulesService>().SaveRulesAsync(input, type);
 | 
			
		||||
        App.GetService<IRulesPageService>().SaveRulesAsync(input, type);
 | 
			
		||||
 | 
			
		||||
    [HttpPost]
 | 
			
		||||
    [TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Post)]
 | 
			
		||||
@@ -635,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; }
 | 
			
		||||
 
 | 
			
		||||
@@ -77,7 +77,7 @@ public abstract class BusinessBaseWithCacheInterval : BusinessBaseWithCache
 | 
			
		||||
            {
 | 
			
		||||
                LogMessage?.LogInformation("Refresh variable");
 | 
			
		||||
                IdVariableRuntimes.Clear();
 | 
			
		||||
                IdVariableRuntimes.AddRange(GlobalData.GetEnableVariables().ToDictionary(a => a.Id));
 | 
			
		||||
                IdVariableRuntimes.AddRange(GlobalData.GetEnableVariables().Where(a => a.IsInternalMemoryVariable == false).ToDictionary(a => a.Id));
 | 
			
		||||
                CollectDevices = GlobalData.GetEnableDevices().Where(a => a.IsCollect == true).ToDictionary(a => a.Id);
 | 
			
		||||
 | 
			
		||||
                VariableRuntimeGroups = IdVariableRuntimes.GroupBy(a => a.Value.BusinessGroup ?? string.Empty).ToDictionary(a => a.Key, a => a.Select(a => a.Value).ToList());
 | 
			
		||||
 
 | 
			
		||||
@@ -106,5 +106,5 @@ public class BusinessPropertyWithCacheIntervalScript : BusinessPropertyWithCache
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [DynamicProperty]
 | 
			
		||||
    [AutoGenerateColumn(Visible = true, IsVisibleWhenEdit = false, IsVisibleWhenAdd = false)]
 | 
			
		||||
    public string? BigTextScriptPluginEventDataModel { get; set; }
 | 
			
		||||
    public virtual string? BigTextScriptPluginEventDataModel { 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 属性中
 | 
			
		||||
@@ -540,7 +540,7 @@ public abstract partial class CollectBase : DriverBase
 | 
			
		||||
            // 如果成功,每个变量都读取一次最新值,再次比较写入值
 | 
			
		||||
            var successfulWriteNames = operResults.Where(a => a.Value.IsSuccess).Select(a => a.Key).ToHashSet();
 | 
			
		||||
 | 
			
		||||
            var groups = writeInfoLists.Select(a => a.Key).Where(a => a.RpcWriteCheck && a.ProtectType != ProtectTypeEnum.WriteOnly && successfulWriteNames.Contains(a.Name) && a.VariableSource != null).GroupBy(a => a.VariableSource as VariableSourceRead).Where(a => a.Key != null).ToList();
 | 
			
		||||
            var groups = writeInfoLists.Select(a => a.Key).Where(a => a.RpcWriteCheck && a.ProtectType != ProtectTypeEnum.WriteOnly && successfulWriteNames.Contains(a.Name) && a.VariableSource != null).GroupBy(a => a.VariableSource as VariableSourceRead).Where(a => a.Key != null).ToArray();
 | 
			
		||||
 | 
			
		||||
            await groups.ParallelForEachAsync(async (varRead, token) =>
 | 
			
		||||
            {
 | 
			
		||||
@@ -594,7 +594,7 @@ public abstract partial class CollectBase : DriverBase
 | 
			
		||||
                    // 根据写入表达式转换数据
 | 
			
		||||
                    object data = deviceVariable.WriteExpressions.GetExpressionsResult(rawdata, LogMessage);
 | 
			
		||||
                    // 将转换后的数据重新赋值给写入信息列表
 | 
			
		||||
                    writeInfoLists[deviceVariable] = JToken.FromObject(data);
 | 
			
		||||
                    writeInfoLists[deviceVariable] = JTokenUtil.GetJTokenFromObj(data);
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception ex)
 | 
			
		||||
                {
 | 
			
		||||
@@ -610,7 +610,7 @@ public abstract partial class CollectBase : DriverBase
 | 
			
		||||
        using var writeLock = await ReadWriteLock.WriterLockAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        var list = writeInfoLists
 | 
			
		||||
        .Where(a => !results.Any(b => b.Key == a.Key.Name))
 | 
			
		||||
        .ToDictionary(item => item.Key, item => item.Value).ToArray();
 | 
			
		||||
        .ToArray();
 | 
			
		||||
        // 使用并发方式遍历写入信息列表,并进行异步写入操作
 | 
			
		||||
        await list.ParallelForEachAsync(async (writeInfo, cancellationToken) =>
 | 
			
		||||
        {
 | 
			
		||||
@@ -662,7 +662,7 @@ public abstract partial class CollectBase : DriverBase
 | 
			
		||||
                    // 根据写入表达式转换数据
 | 
			
		||||
                    object data = deviceVariable.WriteExpressions.GetExpressionsResult(rawdata, LogMessage);
 | 
			
		||||
                    // 将转换后的数据重新赋值给写入信息列表
 | 
			
		||||
                    writeInfoLists[deviceVariable] = JToken.FromObject(data);
 | 
			
		||||
                    writeInfoLists[deviceVariable] = JTokenUtil.GetJTokenFromObj(data);
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception ex)
 | 
			
		||||
                {
 | 
			
		||||
 
 | 
			
		||||
@@ -200,9 +200,8 @@ public abstract class CollectFoundationBase : CollectBase
 | 
			
		||||
 | 
			
		||||
        // 创建用于存储操作结果的并发字典
 | 
			
		||||
        ConcurrentDictionary<string, OperResult> operResults = new();
 | 
			
		||||
        var list = writeInfoLists.ToArray();
 | 
			
		||||
        // 使用并发方式遍历写入信息列表,并进行异步写入操作
 | 
			
		||||
        await list.ParallelForEachAsync(async (writeInfo, cancellationToken) =>
 | 
			
		||||
        await writeInfoLists.ParallelForEachAsync(async (writeInfo, cancellationToken) =>
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
 
 | 
			
		||||
@@ -37,6 +37,8 @@ public abstract class DriverBase : AsyncDisposableObject, IDriver
 | 
			
		||||
 | 
			
		||||
    #region 属性
 | 
			
		||||
 | 
			
		||||
    public virtual bool RefreshRuntimeAlways { get; set; } = false;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 调试UI Type,如果不存在,返回null
 | 
			
		||||
    /// </summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -62,28 +62,40 @@ public static class DynamicModelExtension
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        if (GlobalData.IdDevices.TryGetValue(businessId, out var deviceRuntime))
 | 
			
		||||
        {
 | 
			
		||||
            if (deviceRuntime.Driver is BusinessBase businessBase && businessBase.DriverProperties is IBusinessPropertyAllVariableBase property)
 | 
			
		||||
            if (deviceRuntime.Driver is BusinessBase businessBase)
 | 
			
		||||
            {
 | 
			
		||||
                if (property.IsAllVariable)
 | 
			
		||||
                if (businessBase.DriverProperties is IBusinessPropertyAllVariableBase property && property.IsAllVariable)
 | 
			
		||||
                {
 | 
			
		||||
                    // 检查是否存在对应的业务设备Id
 | 
			
		||||
                    if (variableRuntime.VariablePropertys?.ContainsKey(businessId) == true)
 | 
			
		||||
                    {
 | 
			
		||||
                        variableRuntime.VariablePropertys[businessId].TryGetValue(propertyName, out var value);
 | 
			
		||||
                        return value; // 返回属性值
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        return ThingsGatewayStringConverter.Default.Serialize(null, businessBase.VariablePropertys.GetValue(propertyName, false));
 | 
			
		||||
                    }
 | 
			
		||||
                    if (variableRuntime.IsInternalMemoryVariable == false)
 | 
			
		||||
                        return GetVariableProperty(variableRuntime, businessId, propertyName, businessBase);
 | 
			
		||||
                }
 | 
			
		||||
                else if (businessBase.RefreshRuntimeAlways)
 | 
			
		||||
                {
 | 
			
		||||
                    return GetVariableProperty(variableRuntime, businessId, propertyName, businessBase);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        return null; // 未找到对应的业务设备Id,返回null
 | 
			
		||||
 | 
			
		||||
        static string? GetVariableProperty(VariableRuntime variableRuntime, long businessId, string propertyName, BusinessBase businessBase)
 | 
			
		||||
        {
 | 
			
		||||
            // 检查是否存在对应的业务设备Id
 | 
			
		||||
            if (variableRuntime.VariablePropertys?.TryGetValue(businessId, out var kv) == true)
 | 
			
		||||
            {
 | 
			
		||||
                kv.TryGetValue(propertyName, out var value);
 | 
			
		||||
                return value; // 返回属性值
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                return ThingsGatewayStringConverter.Default.Serialize(null, businessBase.VariablePropertys.GetValue(propertyName, false));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
 
 | 
			
		||||
@@ -43,6 +43,7 @@ namespace ThingsGateway.Gateway.Application
 | 
			
		||||
        string PluginDirectory { get; }
 | 
			
		||||
        Dictionary<long, VariableRuntime> IdVariableRuntimes { get; }
 | 
			
		||||
        IDeviceThreadManage DeviceThreadManage { get; }
 | 
			
		||||
        bool RefreshRuntimeAlways { get; set; }
 | 
			
		||||
 | 
			
		||||
        bool IsConnected();
 | 
			
		||||
        void PauseThread(bool pause);
 | 
			
		||||
 
 | 
			
		||||
@@ -43,6 +43,7 @@ public class Device : BaseDataEntity, IValidatableObject
 | 
			
		||||
    [AutoGenerateColumn(Visible = true, Filterable = true, Sortable = true)]
 | 
			
		||||
    [Required]
 | 
			
		||||
    [RegularExpression(@"^[^.]*$", ErrorMessage = "The field {0} cannot contain a dot ('.')")]
 | 
			
		||||
    [StringNotMemoryAttribute]
 | 
			
		||||
    public virtual string Name { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,8 @@ using Riok.Mapperly.Abstractions;
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.NewLife.Json.Extension;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
@@ -302,13 +304,11 @@ public class Variable : BaseDataEntity, IValidatableObject
 | 
			
		||||
 | 
			
		||||
    #endregion 备用字段
 | 
			
		||||
 | 
			
		||||
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
 | 
			
		||||
    public virtual IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
 | 
			
		||||
    {
 | 
			
		||||
        if (string.IsNullOrEmpty(RegisterAddress) && string.IsNullOrEmpty(OtherMethod))
 | 
			
		||||
        {
 | 
			
		||||
            yield return new ValidationResult("Both RegisterAddress and OtherMethod cannot be empty or null.", new[] { nameof(OtherMethod), nameof(RegisterAddress) });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -23,5 +23,5 @@ public enum PluginTypeEnum
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 业务
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    Business,
 | 
			
		||||
    Business
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -29,6 +29,7 @@ public delegate void DelegateOnDeviceChanged(DeviceRuntime deviceRuntime, Device
 | 
			
		||||
/// <param name="variableRuntime">变量运行时对象</param>
 | 
			
		||||
/// <param name="variableData">变量数据对象</param>
 | 
			
		||||
public delegate void VariableChangeEventHandler(VariableRuntime variableRuntime, VariableBasicData variableData);
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 变量采集事件委托,用于通知变量进行采集时的事件
 | 
			
		||||
/// </summary>
 | 
			
		||||
@@ -100,10 +101,11 @@ public static class GlobalData
 | 
			
		||||
        return ReadOnlyIdDevices.WhereIf(dataScope != null && dataScope?.Count > 0, u => dataScope.Contains(u.Value.CreateOrgId))//在指定机构列表查询
 | 
			
		||||
          .WhereIf(dataScope?.Count == 0, u => u.Value.CreateUserId == UserManager.UserId).Select(a => a.Value);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static async Task<IEnumerable<VariableRuntime>> GetCurrentUserIdVariables()
 | 
			
		||||
    {
 | 
			
		||||
        var dataScope = await GlobalData.SysUserService.GetCurrentUserDataScopeAsync().ConfigureAwait(false);
 | 
			
		||||
        return IdVariables.WhereIf(dataScope != null && dataScope?.Count > 0, u => dataScope.Contains(u.Value.CreateOrgId))//在指定机构列表查询
 | 
			
		||||
        return IdVariables.Where(a => a.Value.IsInternalMemoryVariable == false).WhereIf(dataScope != null && dataScope?.Count > 0, u => dataScope.Contains(u.Value.CreateOrgId))//在指定机构列表查询
 | 
			
		||||
          .WhereIf(dataScope?.Count == 0, u => u.Value.CreateUserId == UserManager.UserId).Select(a => a.Value);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -117,7 +119,7 @@ public static class GlobalData
 | 
			
		||||
    public static async Task<IEnumerable<VariableRuntime>> GetCurrentUserAlarmEnableVariables()
 | 
			
		||||
    {
 | 
			
		||||
        var dataScope = await GlobalData.SysUserService.GetCurrentUserDataScopeAsync().ConfigureAwait(false);
 | 
			
		||||
        return AlarmEnableIdVariables.WhereIf(dataScope != null && dataScope?.Count > 0, u => dataScope.Contains(u.Value.CreateOrgId))//在指定机构列表查询
 | 
			
		||||
        return AlarmEnableIdVariables.Where(a => a.Value.IsInternalMemoryVariable == false).WhereIf(dataScope != null && dataScope?.Count > 0, u => dataScope.Contains(u.Value.CreateOrgId))//在指定机构列表查询
 | 
			
		||||
          .WhereIf(dataScope?.Count == 0, u => u.Value.CreateUserId == UserManager.UserId).Select(a => a.Value);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -125,9 +127,15 @@ public static class GlobalData
 | 
			
		||||
    {
 | 
			
		||||
        if (GlobalData.IdDevices.TryGetValue(businessDeviceId, out var deviceRuntime))
 | 
			
		||||
        {
 | 
			
		||||
            if (deviceRuntime.Driver?.DriverProperties is IBusinessPropertyAllVariableBase property)
 | 
			
		||||
 | 
			
		||||
            if (deviceRuntime.Driver is BusinessBase businessBase)
 | 
			
		||||
            {
 | 
			
		||||
                if (property.IsAllVariable)
 | 
			
		||||
                if (businessBase.DriverProperties is IBusinessPropertyAllVariableBase property && property.IsAllVariable)
 | 
			
		||||
                {
 | 
			
		||||
                    if (a.IsInternalMemoryVariable == false)
 | 
			
		||||
                        return true;
 | 
			
		||||
                }
 | 
			
		||||
                else if (businessBase.RefreshRuntimeAlways)
 | 
			
		||||
                {
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
@@ -141,7 +149,46 @@ public static class GlobalData
 | 
			
		||||
 | 
			
		||||
        return a.VariablePropertys?.ContainsKey(businessDeviceId) == true;
 | 
			
		||||
    }
 | 
			
		||||
    public static DeviceRuntime[] GetAllVariableBusinessDeviceRuntime()
 | 
			
		||||
    {
 | 
			
		||||
        var channelDevice = GlobalData.IdDevices.Where(a =>
 | 
			
		||||
          {
 | 
			
		||||
              if (a.Value.Driver is BusinessBase businessBase)
 | 
			
		||||
              {
 | 
			
		||||
                  if (businessBase.DriverProperties is IBusinessPropertyAllVariableBase property && property.IsAllVariable)
 | 
			
		||||
                  {
 | 
			
		||||
                      return true;
 | 
			
		||||
                  }
 | 
			
		||||
                  else if (businessBase.RefreshRuntimeAlways)
 | 
			
		||||
                  {
 | 
			
		||||
                      return true;
 | 
			
		||||
                  }
 | 
			
		||||
              }
 | 
			
		||||
              return false;
 | 
			
		||||
 | 
			
		||||
          }).Select(a => a.Value).ToArray();
 | 
			
		||||
        return channelDevice;
 | 
			
		||||
    }
 | 
			
		||||
    public static IDriver[] GetAllVariableBusinessDriver()
 | 
			
		||||
    {
 | 
			
		||||
        var channelDevice = GlobalData.IdDevices.Where(a =>
 | 
			
		||||
        {
 | 
			
		||||
            if (a.Value.Driver is BusinessBase businessBase)
 | 
			
		||||
            {
 | 
			
		||||
                if (businessBase.DriverProperties is IBusinessPropertyAllVariableBase property && property.IsAllVariable)
 | 
			
		||||
                {
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
                else if (businessBase.RefreshRuntimeAlways)
 | 
			
		||||
                {
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            return false;
 | 
			
		||||
 | 
			
		||||
        }).Select(a => a.Value.Driver).ToArray();
 | 
			
		||||
        return channelDevice;
 | 
			
		||||
    }
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 只读的通道字典,提供对通道的只读访问
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -162,6 +209,32 @@ public static class GlobalData
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static VariableRuntime GetVariable(string variableName)
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
        if (string.IsNullOrEmpty(variableName))
 | 
			
		||||
            return null;
 | 
			
		||||
        var names = variableName.Split('.');
 | 
			
		||||
        if (names.Length > 2)
 | 
			
		||||
            return null;
 | 
			
		||||
 | 
			
		||||
        if (names.Length == 1)
 | 
			
		||||
        {
 | 
			
		||||
            if (MemoryVariables.TryGetValue(names[0], out var variable))
 | 
			
		||||
            {
 | 
			
		||||
                return variable;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        else if (Devices.TryGetValue(names[0], out var device))
 | 
			
		||||
        {
 | 
			
		||||
            if (device.VariableRuntimes.TryGetValue(names[1], out var variable))
 | 
			
		||||
            {
 | 
			
		||||
                return variable;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static IEnumerable<DeviceRuntime> GetEnableDevices()
 | 
			
		||||
    {
 | 
			
		||||
        var idSet = GetRedundantDeviceIds();
 | 
			
		||||
@@ -189,7 +262,7 @@ public static class GlobalData
 | 
			
		||||
 | 
			
		||||
    public static IEnumerable<VariableRuntime> GetEnableVariables()
 | 
			
		||||
    {
 | 
			
		||||
        return IdDevices.SelectMany(a => a.Value.VariableRuntimes).Where(a => a.Value?.Enable == true).Select(a => a.Value);
 | 
			
		||||
        return IdVariables.Where(a => a.Value.DeviceRuntime?.Enable != false && a.Value.DeviceRuntime?.ChannelRuntime?.Enable != false && a.Value?.Enable == true).Select(a => a.Value);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static bool TryGetDeviceThreadManage(DeviceRuntime deviceRuntime, out IDeviceThreadManage deviceThreadManage)
 | 
			
		||||
@@ -483,6 +556,9 @@ public static class GlobalData
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    internal static ConcurrentDictionary<long, VariableRuntime> IdVariables { get; } = new();
 | 
			
		||||
 | 
			
		||||
    internal static ConcurrentDictionary<string, VariableRuntime> MemoryVariables { get; } = new();
 | 
			
		||||
    public static IReadOnlyDictionary<string, VariableRuntime> ReadOnlyMemoryVariables => MemoryVariables;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 实时报警列表
 | 
			
		||||
    /// </summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -61,7 +61,7 @@ public class LogJob : IJob
 | 
			
		||||
 | 
			
		||||
        //网关通道日志以通道id命名
 | 
			
		||||
        var rulesService = App.RootServices.GetService<IRulesService>();
 | 
			
		||||
        var ruleNames = (await rulesService.GetAllRulesAsync().ConfigureAwait(false)).Select(a => a.Name.ToString()).ToHashSet();
 | 
			
		||||
        var ruleNames = (await rulesService.GetFromDBAsync(a => a.Name.ToString()).ConfigureAwait(false)).ToHashSet();
 | 
			
		||||
        var ruleBaseDir = RulesEngineHostedService.LogDir;
 | 
			
		||||
        Directory.CreateDirectory(ruleBaseDir);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -131,8 +131,10 @@ public class VariableBasicData
 | 
			
		||||
    //[System.Text.Json.Serialization.JsonIgnore]
 | 
			
		||||
    //[Newtonsoft.Json.JsonIgnore]
 | 
			
		||||
    //public DeviceBasicData DeviceRuntime { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc cref="Variable.DeviceId"/>
 | 
			
		||||
    public long DeviceId { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc cref="VariableRuntime.LastErrorMessage"/>
 | 
			
		||||
    [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
 | 
			
		||||
    [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)]
 | 
			
		||||
@@ -142,6 +144,11 @@ public class VariableBasicData
 | 
			
		||||
    [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)]
 | 
			
		||||
    public string? RegisterAddress { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc cref="Variable.OtherMethod"/>
 | 
			
		||||
    [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
 | 
			
		||||
    [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)]
 | 
			
		||||
    public string? OtherMethod { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc cref="Variable.Unit"/>
 | 
			
		||||
    [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
 | 
			
		||||
    [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)]
 | 
			
		||||
@@ -190,5 +197,6 @@ public class VariableBasicData
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc cref="VariableRuntime.ValueInited"/>
 | 
			
		||||
    public bool ValueInited { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc cref="VariableRuntime.IsMemoryVariable"/>
 | 
			
		||||
    public bool IsMemoryVariable { get; set; }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,16 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
//  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
 | 
			
		||||
//  此代码版权(除特别声明外的代码)归作者本人Diego所有
 | 
			
		||||
//  源代码使用协议遵循本仓库的开源协议及附加协议
 | 
			
		||||
//  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
 | 
			
		||||
//  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
 | 
			
		||||
//  使用文档:https://thingsgateway.cn/
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
 | 
			
		||||
public interface IMemoryVariableRpc
 | 
			
		||||
{
 | 
			
		||||
    OperResult MemoryVariableRpc(string value, CancellationToken cancellationToken = default);
 | 
			
		||||
}
 | 
			
		||||
@@ -14,6 +14,7 @@ using Newtonsoft.Json.Linq;
 | 
			
		||||
 | 
			
		||||
using Riok.Mapperly.Abstractions;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#if !Management
 | 
			
		||||
using ThingsGateway.Gateway.Application.Extensions;
 | 
			
		||||
#endif
 | 
			
		||||
@@ -32,6 +33,13 @@ public partial class VariableRuntime : Variable
 | 
			
		||||
    IDisposable
 | 
			
		||||
#endif
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    [AutoGenerateColumn(Ignore = true)]
 | 
			
		||||
    public virtual bool IsMemoryVariable { get => _isMemoryVariable; set => _isMemoryVariable = value; }
 | 
			
		||||
    [AutoGenerateColumn(Ignore = true)]
 | 
			
		||||
    public virtual bool IsInternalMemoryVariable { get => _isInternalMemoryVariable; set => _isInternalMemoryVariable = value; }
 | 
			
		||||
 | 
			
		||||
    [AutoGenerateColumn(Ignore = true)]
 | 
			
		||||
    public bool ValueInited { get => _valueInited; set => _valueInited = value; }
 | 
			
		||||
 | 
			
		||||
@@ -74,15 +82,22 @@ public partial class VariableRuntime : Variable
 | 
			
		||||
    public object RawValue { get => rawValue; set => rawValue = value; }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#if !Management
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    [Newtonsoft.Json.JsonIgnore]
 | 
			
		||||
    [System.Text.Json.Serialization.JsonIgnore]
 | 
			
		||||
    [AutoGenerateColumn(Ignore = true)]
 | 
			
		||||
    public TouchSocket.Core.ILog? LogMessage => DeviceRuntime?.Driver?.LogMessage;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 所在采集设备
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [Newtonsoft.Json.JsonIgnore]
 | 
			
		||||
    [System.Text.Json.Serialization.JsonIgnore]
 | 
			
		||||
    [AutoGenerateColumn(Ignore = true)]
 | 
			
		||||
    public DeviceRuntime? DeviceRuntime { get => deviceRuntime; set => deviceRuntime = value; }
 | 
			
		||||
    public DeviceRuntime? DeviceRuntime { get => deviceRuntime; private set => deviceRuntime = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// VariableSource
 | 
			
		||||
@@ -239,6 +254,8 @@ public partial class VariableRuntime : Variable
 | 
			
		||||
#pragma warning restore CS0169
 | 
			
		||||
#pragma warning restore CS0414
 | 
			
		||||
    private bool _valueInited;
 | 
			
		||||
    private bool _isMemoryVariable;
 | 
			
		||||
    private bool _isInternalMemoryVariable;
 | 
			
		||||
 | 
			
		||||
    private object _value;
 | 
			
		||||
    private object lastSetValue;
 | 
			
		||||
@@ -272,7 +289,7 @@ public partial class VariableRuntime : Variable
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var data = ReadExpressions.GetExpressionsResult(RawValue, DeviceRuntime?.Driver?.LogMessage);
 | 
			
		||||
                var data = ReadExpressions.GetExpressionsResult(RawValue, LogMessage);
 | 
			
		||||
                Set(data, dateTime);
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
@@ -291,7 +308,7 @@ public partial class VariableRuntime : Variable
 | 
			
		||||
                }
 | 
			
		||||
                if (oldMessage != _lastErrorMessage)
 | 
			
		||||
                {
 | 
			
		||||
                    DeviceRuntime?.Driver?.LogMessage?.LogWarning(_lastErrorMessage);
 | 
			
		||||
                    LogMessage?.LogWarning(_lastErrorMessage);
 | 
			
		||||
                }
 | 
			
		||||
                return new($"{Name} Conversion expression failed", ex);
 | 
			
		||||
            }
 | 
			
		||||
@@ -394,6 +411,12 @@ public partial class VariableRuntime : Variable
 | 
			
		||||
 | 
			
		||||
        DeviceRuntime = deviceRuntime;
 | 
			
		||||
 | 
			
		||||
        if (deviceRuntime == null)
 | 
			
		||||
        {
 | 
			
		||||
            GlobalData.MemoryVariables.Remove(Name);
 | 
			
		||||
            GlobalData.MemoryVariables.TryAdd(Name, this);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        DeviceRuntime?.VariableRuntimes?.TryAdd(Name, this);
 | 
			
		||||
        GlobalData.IdVariables.Remove(Id);
 | 
			
		||||
        GlobalData.IdVariables.TryAdd(Id, this);
 | 
			
		||||
@@ -407,7 +430,10 @@ public partial class VariableRuntime : Variable
 | 
			
		||||
    public void Dispose()
 | 
			
		||||
    {
 | 
			
		||||
        DeviceRuntime?.VariableRuntimes?.Remove(Name);
 | 
			
		||||
 | 
			
		||||
        if (DeviceRuntime == null)
 | 
			
		||||
        {
 | 
			
		||||
            GlobalData.MemoryVariables.Remove(Name);
 | 
			
		||||
        }
 | 
			
		||||
        GlobalData.IdVariables.Remove(Id);
 | 
			
		||||
 | 
			
		||||
        GlobalData.AlarmEnableIdVariables.Remove(Id);
 | 
			
		||||
@@ -422,7 +448,7 @@ public partial class VariableRuntime : Variable
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public async ValueTask<IOperResult> RpcAsync(string value, string executive = "brower", CancellationToken cancellationToken = default)
 | 
			
		||||
    public virtual async ValueTask<IOperResult> RpcAsync(string value, string executive = "brower", CancellationToken cancellationToken = default)
 | 
			
		||||
    {
 | 
			
		||||
        var data = await GlobalData.RpcService.InvokeDeviceMethodAsync(executive, new Dictionary<string, Dictionary<string, string>>()
 | 
			
		||||
        {
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,6 @@
 | 
			
		||||
 | 
			
		||||
using Microsoft.Extensions.Logging;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.Common.Extension;
 | 
			
		||||
using ThingsGateway.Gateway.Application.Extensions;
 | 
			
		||||
using ThingsGateway.NewLife.Extension;
 | 
			
		||||
 | 
			
		||||
@@ -110,7 +109,7 @@ internal sealed class AlarmTask : IDisposable
 | 
			
		||||
        if (tag.AlarmPropertys.CustomAlarmEnable) // 检查是否启用了自定义报警功能
 | 
			
		||||
        {
 | 
			
		||||
            // 调用变量的CustomAlarmCode属性的GetExpressionsResult方法,传入变量的值,获取报警表达式的计算结果
 | 
			
		||||
            var result = tag.AlarmPropertys.CustomAlarmCode.GetExpressionsResult(tag.Value, tag.DeviceRuntime?.Driver?.LogMessage);
 | 
			
		||||
            var result = tag.AlarmPropertys.CustomAlarmCode.GetExpressionsResult(tag.Value, tag.LogMessage);
 | 
			
		||||
 | 
			
		||||
            if (result is bool boolResult) // 检查计算结果是否为布尔类型
 | 
			
		||||
            {
 | 
			
		||||
@@ -227,7 +226,7 @@ internal sealed class AlarmTask : IDisposable
 | 
			
		||||
            if (!string.IsNullOrEmpty(ex))
 | 
			
		||||
            {
 | 
			
		||||
                // 如果存在报警约束表达式,则计算表达式结果,以确定是否触发报警事件
 | 
			
		||||
                var data = ex.GetExpressionsResult(item.Value, item.DeviceRuntime?.Driver?.LogMessage);
 | 
			
		||||
                var data = ex.GetExpressionsResult(item.Value, item.LogMessage);
 | 
			
		||||
                if (data is bool result)
 | 
			
		||||
                {
 | 
			
		||||
                    if (result)
 | 
			
		||||
@@ -487,6 +486,11 @@ internal sealed class AlarmTask : IDisposable
 | 
			
		||||
 | 
			
		||||
    #endregion 核心实现
 | 
			
		||||
 | 
			
		||||
    ParallelOptions ParallelOptions = new()
 | 
			
		||||
    {
 | 
			
		||||
        MaxDegreeOfParallelism = Math.Max(1, Environment.ProcessorCount / 2)
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 执行工作任务,对设备变量进行报警分析。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -499,32 +503,36 @@ internal sealed class AlarmTask : IDisposable
 | 
			
		||||
            if (!GlobalData.StartBusinessChannelEnable)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            //Stopwatch stopwatch = Stopwatch.StartNew();
 | 
			
		||||
            // 遍历设备变量列表
 | 
			
		||||
            //System.Diagnostics.Stopwatch stopwatch = System.Diagnostics.Stopwatch.StartNew();
 | 
			
		||||
 | 
			
		||||
            if (scheduledTask.Period < 100 && scheduledTask.Period > 1 && GlobalData.AlarmEnableIdVariables.Count > 50000)
 | 
			
		||||
            {
 | 
			
		||||
                scheduledTask.Change(100, 100);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // 遍历设备变量列表
 | 
			
		||||
            if (!GlobalData.AlarmEnableIdVariables.IsEmpty)
 | 
			
		||||
            {
 | 
			
		||||
                var list = GlobalData.AlarmEnableIdVariables.Select(a => a.Value).ToArray();
 | 
			
		||||
                list.ParallelForEach((item, state, index) =>
 | 
			
		||||
                // 使用 Parallel.ForEach 执行指定的操作
 | 
			
		||||
                Parallel.ForEach(GlobalData.AlarmEnableIdVariables, ParallelOptions, (item, state, index) =>
 | 
			
		||||
            {
 | 
			
		||||
                {
 | 
			
		||||
                    // 如果取消请求已经被触发,则结束任务
 | 
			
		||||
                    if (cancellation.IsCancellationRequested)
 | 
			
		||||
                        return;
 | 
			
		||||
                // 如果取消请求已经被触发,则结束任务
 | 
			
		||||
                if (cancellation.IsCancellationRequested)
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                    // 如果该变量的报警功能未启用,则跳过该变量
 | 
			
		||||
                    if (!item.AlarmEnable)
 | 
			
		||||
                        return;
 | 
			
		||||
                // 如果该变量的报警功能未启用,则跳过该变量
 | 
			
		||||
                if (!item.Value.AlarmEnable)
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                    // 如果该变量离线,则跳过该变量
 | 
			
		||||
                    if (!item.IsOnline)
 | 
			
		||||
                        return;
 | 
			
		||||
                // 如果该变量离线,则跳过该变量
 | 
			
		||||
                if (!item.Value.IsOnline)
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                    // 对该变量进行报警分析
 | 
			
		||||
                    AlarmAnalysis(item);
 | 
			
		||||
                }
 | 
			
		||||
                // 对该变量进行报警分析
 | 
			
		||||
                AlarmAnalysis(item.Value);
 | 
			
		||||
            });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                //if (scheduledTask.Period != 5000)
 | 
			
		||||
 
 | 
			
		||||
@@ -58,9 +58,7 @@ internal sealed class ChannelService : BaseService<Channel>, IChannelService
 | 
			
		||||
        }).ConfigureAwait(false);
 | 
			
		||||
        if (result.IsSuccess)//如果成功了
 | 
			
		||||
        {
 | 
			
		||||
            DeleteChannelFromCache();
 | 
			
		||||
            App.GetService<IDeviceService>().DeleteDeviceFromCache();
 | 
			
		||||
            App.GetService<IVariableService>().DeleteVariableCache();
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
@@ -87,9 +85,7 @@ internal sealed class ChannelService : BaseService<Channel>, IChannelService
 | 
			
		||||
        }).ConfigureAwait(false);
 | 
			
		||||
        if (result.IsSuccess)//如果成功了
 | 
			
		||||
        {
 | 
			
		||||
            DeleteChannelFromCache();
 | 
			
		||||
            App.GetService<IDeviceService>().DeleteDeviceFromCache();
 | 
			
		||||
            App.GetService<IVariableService>().DeleteVariableCache();
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
@@ -124,9 +120,7 @@ internal sealed class ChannelService : BaseService<Channel>, IChannelService
 | 
			
		||||
        }).ConfigureAwait(false);
 | 
			
		||||
        if (result.IsSuccess)//如果成功了
 | 
			
		||||
        {
 | 
			
		||||
            DeleteChannelFromCache();
 | 
			
		||||
            App.GetService<IDeviceService>().DeleteDeviceFromCache();
 | 
			
		||||
            App.GetService<IVariableService>().DeleteVariableCache();
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
@@ -149,7 +143,7 @@ internal sealed class ChannelService : BaseService<Channel>, IChannelService
 | 
			
		||||
        }).ConfigureAwait(false);
 | 
			
		||||
        if (result.IsSuccess)//如果成功了
 | 
			
		||||
        {
 | 
			
		||||
            DeleteChannelFromCache();
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
@@ -181,7 +175,7 @@ internal sealed class ChannelService : BaseService<Channel>, IChannelService
 | 
			
		||||
            }).ConfigureAwait(false);
 | 
			
		||||
            if (result.IsSuccess)//如果成功了
 | 
			
		||||
            {
 | 
			
		||||
                DeleteChannelFromCache();
 | 
			
		||||
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
@@ -214,7 +208,7 @@ internal sealed class ChannelService : BaseService<Channel>, IChannelService
 | 
			
		||||
        }).ConfigureAwait(false);
 | 
			
		||||
        if (result.IsSuccess)//如果成功了
 | 
			
		||||
        {
 | 
			
		||||
            DeleteChannelFromCache();
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
@@ -224,11 +218,6 @@ internal sealed class ChannelService : BaseService<Channel>, IChannelService
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    public void DeleteChannelFromCache()
 | 
			
		||||
    {
 | 
			
		||||
        //App.CacheService.Remove(ThingsGatewayCacheConst.Cache_Channel);//删除通道缓存
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -299,7 +288,7 @@ internal sealed class ChannelService : BaseService<Channel>, IChannelService
 | 
			
		||||
 | 
			
		||||
        if (await base.SaveAsync(input, type).ConfigureAwait(false))
 | 
			
		||||
        {
 | 
			
		||||
            DeleteChannelFromCache();
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
        return false;
 | 
			
		||||
@@ -320,7 +309,7 @@ internal sealed class ChannelService : BaseService<Channel>, IChannelService
 | 
			
		||||
 | 
			
		||||
        if (await base.SaveAsync(input, type).ConfigureAwait(false))
 | 
			
		||||
        {
 | 
			
		||||
            DeleteChannelFromCache();
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
        return false;
 | 
			
		||||
@@ -404,7 +393,7 @@ internal sealed class ChannelService : BaseService<Channel>, IChannelService
 | 
			
		||||
            await db.BulkUpdateAsync(upData, 200000).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
        DeleteChannelFromCache();
 | 
			
		||||
 | 
			
		||||
        return upData.Select(a => a.Id).Concat(insertData.Select(a => a.Id)).ToHashSet();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -38,11 +38,6 @@ internal interface IChannelService
 | 
			
		||||
    /// <param name="ids">待删除通道的ID列表</param>
 | 
			
		||||
    Task<bool> DeleteChannelAsync(IEnumerable<long> ids);
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 从缓存中删除通道
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    void DeleteChannelFromCache();
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 导出通道为文件流结果
 | 
			
		||||
    /// </summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -63,8 +63,6 @@ internal sealed class DeviceService : BaseService<Device>, IDeviceService
 | 
			
		||||
        }).ConfigureAwait(false);
 | 
			
		||||
        if (result.IsSuccess)//如果成功了
 | 
			
		||||
        {
 | 
			
		||||
            DeleteDeviceFromCache();
 | 
			
		||||
            App.GetService<IVariableService>().DeleteVariableCache();
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
@@ -87,7 +85,7 @@ internal sealed class DeviceService : BaseService<Device>, IDeviceService
 | 
			
		||||
        }).ConfigureAwait(false);
 | 
			
		||||
        if (result.IsSuccess)//如果成功了
 | 
			
		||||
        {
 | 
			
		||||
            DeleteDeviceFromCache();
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
@@ -112,8 +110,7 @@ internal sealed class DeviceService : BaseService<Device>, IDeviceService
 | 
			
		||||
             .WhereIf(dataScope?.Count == 0, u => u.CreateUserId == UserManager.UserId)
 | 
			
		||||
             .ToList();
 | 
			
		||||
            var result = (await db.Updateable(data).UpdateColumns(differences.Select(a => a.Key).ToArray()).ExecuteCommandAsync().ConfigureAwait(false)) > 0;
 | 
			
		||||
            if (result)
 | 
			
		||||
                DeleteDeviceFromCache();
 | 
			
		||||
 | 
			
		||||
            return result;
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
@@ -141,7 +138,7 @@ internal sealed class DeviceService : BaseService<Device>, IDeviceService
 | 
			
		||||
        }).ConfigureAwait(false);
 | 
			
		||||
        if (result.IsSuccess)//如果成功了
 | 
			
		||||
        {
 | 
			
		||||
            DeleteDeviceFromCache();
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
@@ -169,7 +166,7 @@ internal sealed class DeviceService : BaseService<Device>, IDeviceService
 | 
			
		||||
        }).ConfigureAwait(false);
 | 
			
		||||
        if (result.IsSuccess)//如果成功了
 | 
			
		||||
        {
 | 
			
		||||
            DeleteDeviceFromCache();
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
@@ -179,12 +176,6 @@ internal sealed class DeviceService : BaseService<Device>, IDeviceService
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    public void DeleteDeviceFromCache()
 | 
			
		||||
    {
 | 
			
		||||
        //App.CacheService.Remove(ThingsGatewayCacheConst.Cache_Device);//删除设备缓存
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 从缓存/数据库获取全部信息
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -274,7 +265,7 @@ internal sealed class DeviceService : BaseService<Device>, IDeviceService
 | 
			
		||||
 | 
			
		||||
        if (await base.SaveAsync(input, type).ConfigureAwait(false))
 | 
			
		||||
        {
 | 
			
		||||
            DeleteDeviceFromCache();
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
        return false;
 | 
			
		||||
@@ -290,7 +281,7 @@ internal sealed class DeviceService : BaseService<Device>, IDeviceService
 | 
			
		||||
 | 
			
		||||
        if (await base.SaveAsync(input, type).ConfigureAwait(false))
 | 
			
		||||
        {
 | 
			
		||||
            DeleteDeviceFromCache();
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
        return false;
 | 
			
		||||
@@ -400,7 +391,7 @@ internal sealed class DeviceService : BaseService<Device>, IDeviceService
 | 
			
		||||
            await db.BulkCopyAsync(insertData, 200000).ConfigureAwait(false);
 | 
			
		||||
            await db.BulkUpdateAsync(upData, 200000).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
        DeleteDeviceFromCache();
 | 
			
		||||
 | 
			
		||||
        return upData.Select(a => a.Id).Concat(insertData.Select(a => a.Id)).ToHashSet();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -51,10 +51,6 @@ internal interface IDeviceService
 | 
			
		||||
    /// <returns>删除是否成功的异步任务</returns>
 | 
			
		||||
    Task<bool> DeleteDeviceAsync(IEnumerable<long> ids);
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 删除设备缓存信息。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    void DeleteDeviceFromCache();
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 导出设备信息到文件流。
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
@@ -573,8 +573,7 @@ internal sealed class DeviceThreadManage : IAsyncDisposable, IDeviceThreadManage
 | 
			
		||||
    {
 | 
			
		||||
        //传入变量
 | 
			
		||||
        //newDeviceRuntime.VariableRuntimes.ParallelForEach(a => a.Value.SafeDispose());
 | 
			
		||||
        var list = deviceRuntime.VariableRuntimes.Select(a => a.Value).ToArray();
 | 
			
		||||
        list.ParallelForEach(a => a.Init(newDeviceRuntime));
 | 
			
		||||
        deviceRuntime.VariableRuntimes.ParallelForEach(a => a.Value.Init(newDeviceRuntime));
 | 
			
		||||
        GlobalData.VariableRuntimeDispatchService.Dispatch(null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -54,7 +54,7 @@ internal sealed class GatewayMonitorHostedService : BackgroundService, IGatewayM
 | 
			
		||||
                    {
 | 
			
		||||
                        item.Init(channelRuntime);
 | 
			
		||||
 | 
			
		||||
                        var varRuntimes = variableRuntimes.Where(x => x.DeviceId == item.Id).ToArray();
 | 
			
		||||
                        var varRuntimes = variableRuntimes.Where(x => x.DeviceId == item.Id);
 | 
			
		||||
 | 
			
		||||
                        varRuntimes.ParallelForEach(varItem => varItem.Init(item));
 | 
			
		||||
                    }
 | 
			
		||||
 
 | 
			
		||||
@@ -134,12 +134,6 @@ public interface IManagementRpcServer : IRpcServer
 | 
			
		||||
    [DmtpRpc]
 | 
			
		||||
    Task<string> ExportVariableFileAsync(GatewayExportFilter exportFilter);
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 从缓存/数据库获取全部信息
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns>规则列表</returns>
 | 
			
		||||
    [DmtpRpc]
 | 
			
		||||
    Task<List<Rules>> GetAllRulesAsync();
 | 
			
		||||
 | 
			
		||||
    [DmtpRpc]
 | 
			
		||||
    Task<List<Channel>> GetChannelListAsync(QueryPageOptions options, int max = 0);
 | 
			
		||||
@@ -227,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);
 | 
			
		||||
 | 
			
		||||
@@ -287,6 +282,8 @@ public interface IManagementRpcServer : IRpcServer
 | 
			
		||||
 | 
			
		||||
    [DmtpRpc]
 | 
			
		||||
    Task RestartDeviceAsync(long id, bool deleteCache);
 | 
			
		||||
    [DmtpRpc]
 | 
			
		||||
    Task RestartRuleRuntimeAsync();
 | 
			
		||||
 | 
			
		||||
    [DmtpRpc]
 | 
			
		||||
    Task RestartServerAsync();
 | 
			
		||||
 
 | 
			
		||||
@@ -48,7 +48,7 @@ public partial class ManagementRpcServer : IRpcServer, IManagementRpcServer, IBa
 | 
			
		||||
    public Task<bool> ClearDeviceAsync(bool restart) =>
 | 
			
		||||
        App.GetService<IDevicePageService>().ClearDeviceAsync(restart);
 | 
			
		||||
 | 
			
		||||
    public Task ClearRulesAsync() => App.GetService<IRulesService>().ClearRulesAsync();
 | 
			
		||||
    public Task ClearRulesAsync() => App.GetService<IRulesPageService>().ClearRulesAsync();
 | 
			
		||||
 | 
			
		||||
    public Task<bool> ClearVariableAsync(bool restart) =>
 | 
			
		||||
        App.GetService<IVariablePageService>().ClearVariableAsync(restart);
 | 
			
		||||
@@ -75,7 +75,7 @@ public partial class ManagementRpcServer : IRpcServer, IManagementRpcServer, IBa
 | 
			
		||||
 | 
			
		||||
    public Task DeleteRuleRuntimesAsync(List<long> ids) => App.GetService<IRulesEngineHostedService>().DeleteRuleRuntimesAsync(ids);
 | 
			
		||||
 | 
			
		||||
    public Task<bool> DeleteRulesAsync(List<long> ids) => App.GetService<IRulesService>().DeleteRulesAsync(ids);
 | 
			
		||||
    public Task<bool> DeleteRulesAsync(List<long> ids) => App.GetService<IRulesPageService>().DeleteRulesAsync(ids);
 | 
			
		||||
 | 
			
		||||
    public Task<bool> DeleteVariableAsync(List<long> ids, bool restart) =>
 | 
			
		||||
    App.GetService<IVariablePageService>().DeleteVariableAsync(ids, restart);
 | 
			
		||||
@@ -107,8 +107,6 @@ public partial class ManagementRpcServer : IRpcServer, IManagementRpcServer, IBa
 | 
			
		||||
 | 
			
		||||
    public Task<string> ExportVariableFileAsync(GatewayExportFilter exportFilter) => App.GetService<IVariablePageService>().ExportVariableFileAsync(exportFilter);
 | 
			
		||||
 | 
			
		||||
    public Task<List<Rules>> GetAllRulesAsync() => App.GetService<IRulesService>().GetAllRulesAsync();
 | 
			
		||||
 | 
			
		||||
    public Task<List<Channel>> GetChannelListAsync(QueryPageOptions options, int max = 0) =>
 | 
			
		||||
        App.GetService<IChannelPageService>().GetChannelListAsync(options, max);
 | 
			
		||||
 | 
			
		||||
@@ -181,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);
 | 
			
		||||
 | 
			
		||||
@@ -244,7 +246,7 @@ public partial class ManagementRpcServer : IRpcServer, IManagementRpcServer, IBa
 | 
			
		||||
 | 
			
		||||
    public Task<string> RulesLogPathAsync(long rulesId) => App.GetService<IRulesEngineHostedService>().RulesLogPathAsync(rulesId);
 | 
			
		||||
 | 
			
		||||
    public Task<QueryData<Rules>> RulesPageAsync(QueryPageOptions option, FilterKeyValueAction filterKeyValueAction = null) => App.GetService<IRulesService>().RulesPageAsync(option, filterKeyValueAction);
 | 
			
		||||
    public Task<QueryData<Rules>> RulesPageAsync(QueryPageOptions option, FilterKeyValueAction filterKeyValueAction = null) => App.GetService<IRulesPageService>().RulesPageAsync(option, filterKeyValueAction);
 | 
			
		||||
 | 
			
		||||
    public Task<bool> SaveChannelAsync(Channel input, ItemChangedType type, bool restart) =>
 | 
			
		||||
        App.GetService<IChannelPageService>().SaveChannelAsync(input, type, restart);
 | 
			
		||||
@@ -254,7 +256,7 @@ public partial class ManagementRpcServer : IRpcServer, IManagementRpcServer, IBa
 | 
			
		||||
 | 
			
		||||
    public Task SavePluginByPathAsync(PluginAddPathInput plugin) => App.GetService<IPluginPageService>().SavePluginByPathAsync(plugin);
 | 
			
		||||
 | 
			
		||||
    public Task<bool> SaveRulesAsync(Rules input, ItemChangedType type) => App.GetService<IRulesService>().SaveRulesAsync(input, type);
 | 
			
		||||
    public Task<bool> SaveRulesAsync(Rules input, ItemChangedType type) => App.GetService<IRulesPageService>().SaveRulesAsync(input, type);
 | 
			
		||||
 | 
			
		||||
    public Task<bool> SaveVariableAsync(Variable input, ItemChangedType type, bool restart) =>
 | 
			
		||||
        App.GetService<IVariablePageService>().SaveVariableAsync(input, type, restart);
 | 
			
		||||
@@ -284,4 +286,7 @@ public partial class ManagementRpcServer : IRpcServer, IManagementRpcServer, IBa
 | 
			
		||||
    public Task UnAuthorizeAsync() => App.GetService<IAuthenticationService>().UnAuthorizeAsync();
 | 
			
		||||
 | 
			
		||||
    public Task<string> UUIDAsync() => App.GetService<IAuthenticationService>().UUIDAsync();
 | 
			
		||||
 | 
			
		||||
    public Task RestartRuleRuntimeAsync() => App.GetService<IRulesEngineHostedService>().RestartRuleRuntimeAsync();
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -92,8 +92,8 @@ public partial class ManagementTask : AsyncDisposableObject
 | 
			
		||||
        var tcpDmtpClient = new TcpDmtpClient();
 | 
			
		||||
        var config = new TouchSocketConfig()
 | 
			
		||||
               .SetRemoteIPHost(_managementOptions.ServerUri)
 | 
			
		||||
               .SetAdapterOption(new AdapterOption() { MaxPackageSize = 1024 * 1024 * 1024 })
 | 
			
		||||
               .SetDmtpOption(new DmtpOption() { VerifyToken = _managementOptions.VerifyToken })
 | 
			
		||||
               .SetAdapterOption(a => a.MaxPackageSize = 1024 * 1024 * 1024)
 | 
			
		||||
               .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
 | 
			
		||||
                       {
 | 
			
		||||
@@ -156,8 +156,8 @@ public partial class ManagementTask : AsyncDisposableObject
 | 
			
		||||
        var tcpDmtpService = new TcpDmtpService();
 | 
			
		||||
        var config = new TouchSocketConfig()
 | 
			
		||||
               .SetListenIPHosts(_managementOptions.ServerUri)
 | 
			
		||||
                   .SetAdapterOption(new AdapterOption() { MaxPackageSize = 1024 * 1024 * 1024 })
 | 
			
		||||
               .SetDmtpOption(new DmtpOption() { VerifyToken = _managementOptions.VerifyToken })
 | 
			
		||||
                   .SetAdapterOption(a => a.MaxPackageSize = 1024 * 1024 * 1024)
 | 
			
		||||
               .SetDmtpOption(a => a.VerifyToken = _managementOptions.VerifyToken)
 | 
			
		||||
               .ConfigureContainer(a =>
 | 
			
		||||
               {
 | 
			
		||||
                   a.AddDmtpRouteService();//添加路由策略
 | 
			
		||||
 
 | 
			
		||||
@@ -331,8 +331,8 @@ internal sealed class RedundancyTask : IRpcDriver, IAsyncDisposable
 | 
			
		||||
        var tcpDmtpClient = new TcpDmtpClient();
 | 
			
		||||
        var config = new TouchSocketConfig()
 | 
			
		||||
               .SetRemoteIPHost(redundancy.MasterUri)
 | 
			
		||||
               .SetAdapterOption(new AdapterOption() { MaxPackageSize = 0x20000000 })
 | 
			
		||||
               .SetDmtpOption(new DmtpOption() { VerifyToken = redundancy.VerifyToken })
 | 
			
		||||
               .SetAdapterOption(a => a.MaxPackageSize = 0x20000000)
 | 
			
		||||
               .SetDmtpOption(a => a.VerifyToken = redundancy.VerifyToken)
 | 
			
		||||
               .ConfigureContainer(a =>
 | 
			
		||||
               {
 | 
			
		||||
                   a.AddLogger(LogMessage);
 | 
			
		||||
@@ -376,8 +376,8 @@ internal sealed class RedundancyTask : IRpcDriver, IAsyncDisposable
 | 
			
		||||
        var tcpDmtpService = new TcpDmtpService();
 | 
			
		||||
        var config = new TouchSocketConfig()
 | 
			
		||||
               .SetListenIPHosts(redundancy.MasterUri)
 | 
			
		||||
               .SetAdapterOption(new AdapterOption() { MaxPackageSize = 0x20000000 })
 | 
			
		||||
               .SetDmtpOption(new DmtpOption() { VerifyToken = redundancy.VerifyToken })
 | 
			
		||||
               .SetAdapterOption(a => a.MaxPackageSize = 0x20000000)
 | 
			
		||||
               .SetDmtpOption(a => a.VerifyToken = redundancy.VerifyToken)
 | 
			
		||||
               .ConfigureContainer(a =>
 | 
			
		||||
               {
 | 
			
		||||
                   a.AddLogger(LogMessage);
 | 
			
		||||
@@ -479,7 +479,7 @@ internal sealed class RedundancyTask : IRpcDriver, IAsyncDisposable
 | 
			
		||||
    {
 | 
			
		||||
        int maxBatchSize = GetBatchSize() / 10;
 | 
			
		||||
 | 
			
		||||
        var groups = GlobalData.IdVariables.Select(a => a.Value).GroupBy(a => a.DeviceRuntime);
 | 
			
		||||
        var groups = GlobalData.IdVariables.Select(a => a.Value).GroupBy(a => a.DeviceRuntime).Where(a => a.Key != null);
 | 
			
		||||
 | 
			
		||||
        var channelBatch = new HashSet<Channel>();
 | 
			
		||||
        var deviceBatch = new HashSet<Device>();
 | 
			
		||||
 
 | 
			
		||||
@@ -30,6 +30,10 @@ internal sealed class RpcService : IRpcService
 | 
			
		||||
{
 | 
			
		||||
    private readonly ConcurrentQueue<RpcLog> _logQueues = new();
 | 
			
		||||
    private readonly RpcLogOptions? _rpcLogOptions;
 | 
			
		||||
    private SqlSugarClient _db = DbContext.GetDB<RpcLog>(); // 创建一个新的数据库上下文实例
 | 
			
		||||
 | 
			
		||||
    private IStringLocalizer Localizer { get; }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc cref="RpcService"/>
 | 
			
		||||
    public RpcService(IStringLocalizer<RpcService> localizer)
 | 
			
		||||
    {
 | 
			
		||||
@@ -38,50 +42,118 @@ internal sealed class RpcService : IRpcService
 | 
			
		||||
        _rpcLogOptions = App.GetOptions<RpcLogOptions>();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private IStringLocalizer Localizer { get; }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    public async Task<Dictionary<string, Dictionary<string, IOperResult>>> InvokeDeviceMethodAsync(string sourceDes, Dictionary<string, Dictionary<string, string>> deviceDatas, CancellationToken cancellationToken = default)
 | 
			
		||||
    public async Task<Dictionary<string, Dictionary<string, IOperResult>>> InvokeDeviceMethodAsync(
 | 
			
		||||
        string sourceDes,
 | 
			
		||||
        Dictionary<string, Dictionary<string, string>> deviceDatas,
 | 
			
		||||
        CancellationToken cancellationToken = default)
 | 
			
		||||
    {
 | 
			
		||||
        // 初始化用于存储将要写入的变量和方法的字典
 | 
			
		||||
        Dictionary<IRpcDriver, Dictionary<VariableRuntime, JToken>> writeVariables = new();
 | 
			
		||||
        Dictionary<IRpcDriver, Dictionary<VariableRuntime, JToken>> writeMethods = new();
 | 
			
		||||
        Dictionary<VariableRuntime, string> memoryVariables = new();
 | 
			
		||||
 | 
			
		||||
        // 用于存储结果的并发字典
 | 
			
		||||
        ConcurrentDictionary<string, Dictionary<string, IOperResult>> results = new();
 | 
			
		||||
        deviceDatas.ForEach(a => results.TryAdd(a.Key, new()));
 | 
			
		||||
 | 
			
		||||
        // 对每个要操作的变量进行检查和处理(内存变量)
 | 
			
		||||
        foreach (var item in deviceDatas.Where(a => a.Key.IsNullOrEmpty() || a.Key.Equals("Memory", StringComparison.OrdinalIgnoreCase)).SelectMany(a => a.Value))
 | 
			
		||||
        {
 | 
			
		||||
            // 查找变量是否存在
 | 
			
		||||
            if (!(GlobalData.MemoryVariables.TryGetValue(item.Key, out var tag) &&
 | 
			
		||||
                  tag is IMemoryVariableRpc memoryVariableRuntime))
 | 
			
		||||
            {
 | 
			
		||||
                // 如果变量不存在,则添加错误信息到结果中并继续下一个变量的处理
 | 
			
		||||
                results["Memory"].TryAdd(item.Key, new OperResult(Localizer["VariableNotNull", item.Key]));
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // 检查变量的保护类型和远程写入权限
 | 
			
		||||
            if (tag.ProtectType == ProtectTypeEnum.ReadOnly)
 | 
			
		||||
            {
 | 
			
		||||
                results["Memory"].TryAdd(item.Key, new OperResult(Localizer["VariableReadOnly", item.Key]));
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!tag.RpcWriteEnable)
 | 
			
		||||
            {
 | 
			
		||||
                results["Memory"].TryAdd(item.Key, new OperResult(Localizer["VariableWriteDisable", item.Key]));
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var start = DateTime.Now;
 | 
			
		||||
 | 
			
		||||
                var variableResult = memoryVariableRuntime.MemoryVariableRpc(item.Value, cancellationToken);
 | 
			
		||||
                var end = DateTime.Now;
 | 
			
		||||
 | 
			
		||||
                string operObj = tag.Name;
 | 
			
		||||
                string parJson = deviceDatas["Memory"][tag.Name];
 | 
			
		||||
 | 
			
		||||
                if (!variableResult.IsSuccess || _rpcLogOptions.SuccessLog)
 | 
			
		||||
                {
 | 
			
		||||
                    _logQueues.Enqueue(
 | 
			
		||||
                        new RpcLog()
 | 
			
		||||
                        {
 | 
			
		||||
                            LogTime = start,
 | 
			
		||||
                            ExecutionTime = (int)(end - start).TotalMilliseconds,
 | 
			
		||||
                            OperateMessage = variableResult.IsSuccess ? null : variableResult.ToString(),
 | 
			
		||||
                            IsSuccess = variableResult.IsSuccess,
 | 
			
		||||
                            OperateMethod = AppResource.WriteVariable,
 | 
			
		||||
                            OperateDevice = string.Empty,
 | 
			
		||||
                            OperateObject = operObj,
 | 
			
		||||
                            OperateSource = sourceDes,
 | 
			
		||||
                            ParamJson = parJson,
 | 
			
		||||
                            ResultJson = null
 | 
			
		||||
                        });
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // 不返回详细错误
 | 
			
		||||
                if (!variableResult.IsSuccess)
 | 
			
		||||
                {
 | 
			
		||||
                    var result1 = variableResult;
 | 
			
		||||
                    result1.Exception = null;
 | 
			
		||||
                    variableResult = result1;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                results["Memory"].Add(tag.Name, variableResult);
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                // 将异常信息添加到结果字典中
 | 
			
		||||
                results["Memory"].Add(tag.Name, new OperResult(ex));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var deviceDict = GlobalData.Devices;
 | 
			
		||||
 | 
			
		||||
        // 对每个要操作的变量进行检查和处理
 | 
			
		||||
        foreach (var deviceData in deviceDatas)
 | 
			
		||||
        // 对每个要操作的变量进行检查和处理(设备变量)
 | 
			
		||||
        foreach (var deviceData in deviceDatas.Where(a => (!a.Key.IsNullOrEmpty() && !a.Key.Equals("Memory", StringComparison.OrdinalIgnoreCase))))
 | 
			
		||||
        {
 | 
			
		||||
            // 查找设备是否存在
 | 
			
		||||
            if (!deviceDict.TryGetValue(deviceData.Key, out var device))
 | 
			
		||||
            {
 | 
			
		||||
                // 如果设备不存在,则添加错误信息到结果中并继续下一个设备的处理
 | 
			
		||||
                deviceData.Value.ForEach(a =>
 | 
			
		||||
                results[deviceData.Key].TryAdd(a.Key, new OperResult(Localizer["DeviceNotNull", deviceData.Key]))
 | 
			
		||||
                );
 | 
			
		||||
                    results[deviceData.Key].TryAdd(a.Key, new OperResult(Localizer["DeviceNotNull", deviceData.Key])));
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // 查找变量对应的设备
 | 
			
		||||
            var collect = device.Driver as IRpcDriver;
 | 
			
		||||
            collect ??= device.RpcDriver;
 | 
			
		||||
            var collect = device.Driver as IRpcDriver ?? device.RpcDriver;
 | 
			
		||||
            if (collect == null)
 | 
			
		||||
            {
 | 
			
		||||
                // 如果设备不存在,则添加错误信息到结果中并继续下一个设备的处理
 | 
			
		||||
                deviceData.Value.ForEach(a =>
 | 
			
		||||
                results[deviceData.Key].TryAdd(a.Key, new OperResult(Localizer["DriverNotNull", deviceData.Key]))
 | 
			
		||||
                );
 | 
			
		||||
                    results[deviceData.Key].TryAdd(a.Key, new OperResult(Localizer["DriverNotNull", deviceData.Key])));
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
            // 检查设备状态,如果设备处于暂停状态,则添加相应的错误信息到结果中并继续下一个变量的处理
 | 
			
		||||
 | 
			
		||||
            // 检查设备状态
 | 
			
		||||
            if (device.DeviceStatus == DeviceStatusEnum.Pause)
 | 
			
		||||
            {
 | 
			
		||||
                deviceData.Value.ForEach(a =>
 | 
			
		||||
                results[deviceData.Key].TryAdd(a.Key, new OperResult(Localizer["DevicePause", deviceData.Key]))
 | 
			
		||||
                );
 | 
			
		||||
                    results[deviceData.Key].TryAdd(a.Key, new OperResult(Localizer["DevicePause", deviceData.Key])));
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@@ -90,7 +162,6 @@ internal sealed class RpcService : IRpcService
 | 
			
		||||
                // 查找变量是否存在
 | 
			
		||||
                if (!device.VariableRuntimes.TryGetValue(item.Key, out var tag))
 | 
			
		||||
                {
 | 
			
		||||
                    // 如果变量不存在,则添加错误信息到结果中并继续下一个变量的处理
 | 
			
		||||
                    results[deviceData.Key].TryAdd(item.Key, new OperResult(Localizer["VariableNotNull", item.Key]));
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
@@ -101,6 +172,7 @@ internal sealed class RpcService : IRpcService
 | 
			
		||||
                    results[deviceData.Key].TryAdd(item.Key, new OperResult(Localizer["VariableReadOnly", item.Key]));
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (!tag.RpcWriteEnable)
 | 
			
		||||
                {
 | 
			
		||||
                    results[deviceData.Key].TryAdd(item.Key, new OperResult(Localizer["VariableWriteDisable", item.Key]));
 | 
			
		||||
@@ -110,6 +182,7 @@ internal sealed class RpcService : IRpcService
 | 
			
		||||
                JToken tagValue = JTokenUtil.GetJTokenFromString(item.Value);
 | 
			
		||||
                bool isOtherMethodEmpty = string.IsNullOrEmpty(tag.OtherMethod);
 | 
			
		||||
                var collection = isOtherMethodEmpty ? writeVariables : writeMethods;
 | 
			
		||||
 | 
			
		||||
                if (collection.TryGetValue(collect, out var value))
 | 
			
		||||
                {
 | 
			
		||||
                    value.Add(tag, tagValue);
 | 
			
		||||
@@ -121,26 +194,26 @@ internal sealed class RpcService : IRpcService
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        var writeVariableArrays = writeVariables.ToArray();
 | 
			
		||||
 | 
			
		||||
        // 使用并行方式写入变量
 | 
			
		||||
        var writeVariableArrays = writeVariables;
 | 
			
		||||
        await writeVariableArrays.ParallelForEachAsync(async (driverData, cancellationToken) =>
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var start = DateTime.Now;
 | 
			
		||||
                // 调用设备的写入方法
 | 
			
		||||
                var result = await driverData.Key.InVokeWriteAsync(driverData.Value, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                var end = DateTime.Now;
 | 
			
		||||
                // 写入日志
 | 
			
		||||
 | 
			
		||||
                foreach (var resultItem in result)
 | 
			
		||||
                {
 | 
			
		||||
                    foreach (var variableResult in resultItem.Value)
 | 
			
		||||
                    {
 | 
			
		||||
                        string operObj = variableResult.Key;
 | 
			
		||||
 | 
			
		||||
                        string parJson = deviceDatas[resultItem.Key][variableResult.Key];
 | 
			
		||||
 | 
			
		||||
                        if (!variableResult.Value.IsSuccess || _rpcLogOptions.SuccessLog)
 | 
			
		||||
                        {
 | 
			
		||||
                            _logQueues.Enqueue(
 | 
			
		||||
                                new RpcLog()
 | 
			
		||||
                                {
 | 
			
		||||
@@ -154,10 +227,9 @@ internal sealed class RpcService : IRpcService
 | 
			
		||||
                                    OperateSource = sourceDes,
 | 
			
		||||
                                    ParamJson = parJson,
 | 
			
		||||
                                    ResultJson = null
 | 
			
		||||
                                }
 | 
			
		||||
                            );
 | 
			
		||||
                                });
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        // 不返回详细错误
 | 
			
		||||
                        if (!variableResult.Value.IsSuccess)
 | 
			
		||||
                        {
 | 
			
		||||
                            var result1 = variableResult.Value;
 | 
			
		||||
@@ -166,44 +238,42 @@ internal sealed class RpcService : IRpcService
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // 将结果添加到结果字典中
 | 
			
		||||
                    results[resultItem.Key].AddRange(resultItem.Value);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                // 将异常信息添加到结果字典中
 | 
			
		||||
                foreach (var item in driverData.Value)
 | 
			
		||||
                {
 | 
			
		||||
                    results[item.Key.DeviceName].Add(item.Key.Name, new OperResult(ex));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        var writeMethodArrays = writeMethods.ToArray();
 | 
			
		||||
 | 
			
		||||
        // 使用并行方式执行方法
 | 
			
		||||
        var writeMethodArrays = writeMethods;
 | 
			
		||||
        await writeMethodArrays.ParallelForEachAsync(async (driverData, cancellationToken) =>
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var start = DateTime.Now;
 | 
			
		||||
                // 调用设备的写入方法
 | 
			
		||||
                var result = await driverData.Key.InvokeMethodAsync(driverData.Value, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                Dictionary<string, string> operateMethods = driverData.Value.Select(a => a.Key).ToDictionary(a => a.Name, a => a.OtherMethod!);
 | 
			
		||||
                Dictionary<string, string> operateMethods = driverData.Value
 | 
			
		||||
                    .Select(a => a.Key)
 | 
			
		||||
                    .ToDictionary(a => a.Name, a => a.OtherMethod!);
 | 
			
		||||
 | 
			
		||||
                var end = DateTime.Now;
 | 
			
		||||
 | 
			
		||||
                // 写入日志
 | 
			
		||||
                foreach (var resultItem in result)
 | 
			
		||||
                {
 | 
			
		||||
                    foreach (var variableResult in resultItem.Value)
 | 
			
		||||
                    {
 | 
			
		||||
                        string operObj = variableResult.Key;
 | 
			
		||||
 | 
			
		||||
                        string parJson = deviceDatas[resultItem.Key][variableResult.Key];
 | 
			
		||||
 | 
			
		||||
                        // 写入日志
 | 
			
		||||
                        if (!variableResult.Value.IsSuccess || _rpcLogOptions.SuccessLog)
 | 
			
		||||
                        {
 | 
			
		||||
                            _logQueues.Enqueue(
 | 
			
		||||
                                new RpcLog()
 | 
			
		||||
                                {
 | 
			
		||||
@@ -216,11 +286,12 @@ internal sealed class RpcService : IRpcService
 | 
			
		||||
                                    OperateObject = operObj,
 | 
			
		||||
                                    OperateSource = sourceDes,
 | 
			
		||||
                                    ParamJson = parJson?.ToString(),
 | 
			
		||||
                                    ResultJson = variableResult.Value is IOperResult<object> operResult ? operResult.Content?.ToSystemTextJsonString() : string.Empty
 | 
			
		||||
                                }
 | 
			
		||||
                            );
 | 
			
		||||
                                    ResultJson = variableResult.Value is IOperResult<object> operResult
 | 
			
		||||
                                        ? operResult.Content?.ToSystemTextJsonString()
 | 
			
		||||
                                        : string.Empty
 | 
			
		||||
                                });
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        // 不返回详细错误
 | 
			
		||||
                        if (!variableResult.Value.IsSuccess)
 | 
			
		||||
                        {
 | 
			
		||||
                            var result1 = variableResult.Value;
 | 
			
		||||
@@ -234,7 +305,6 @@ internal sealed class RpcService : IRpcService
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                // 将异常信息添加到结果字典中
 | 
			
		||||
                foreach (var item in driverData.Value)
 | 
			
		||||
                {
 | 
			
		||||
                    results[item.Key.DeviceName].Add(item.Key.Name, new OperResult(ex));
 | 
			
		||||
@@ -246,8 +316,6 @@ internal sealed class RpcService : IRpcService
 | 
			
		||||
        return new(results);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private SqlSugarClient _db = DbContext.GetDB<RpcLog>(); // 创建一个新的数据库上下文实例
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 异步执行RPC日志插入操作的方法。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -258,11 +326,12 @@ internal sealed class RpcService : IRpcService
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var data = _logQueues.ToListWithDequeue(); // 从日志队列中获取数据
 | 
			
		||||
                var data = _logQueues.ToListWithDequeue();
 | 
			
		||||
                if (data.Count > 0)
 | 
			
		||||
                {
 | 
			
		||||
                    // 将数据插入到数据库中
 | 
			
		||||
                    await _db.InsertableWithAttr(data).ExecuteCommandAsync(appLifetime.ApplicationStopping).ConfigureAwait(false);
 | 
			
		||||
                    await _db.InsertableWithAttr(data)
 | 
			
		||||
                        .ExecuteCommandAsync(appLifetime.ApplicationStopping)
 | 
			
		||||
                        .ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
@@ -271,7 +340,7 @@ internal sealed class RpcService : IRpcService
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
                await Task.Delay(3000).ConfigureAwait(false); // 在finally块中等待一段时间后继续下一次循环
 | 
			
		||||
                await Task.Delay(3000).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@ using ThingsGateway.Blazor.Diagrams.Core;
 | 
			
		||||
using ThingsGateway.Blazor.Diagrams.Core.Anchors;
 | 
			
		||||
using ThingsGateway.Blazor.Diagrams.Core.Geometry;
 | 
			
		||||
using ThingsGateway.Blazor.Diagrams.Core.Models;
 | 
			
		||||
using ThingsGateway.NewLife.Json.Extension;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
 | 
			
		||||
@@ -88,7 +89,7 @@ public static class RuleHelpers
 | 
			
		||||
        var propertyInfos = nodeModel.GetType().GetRuntimeProperties().Where(a => a.GetCustomAttribute<ModelValue>() != null);
 | 
			
		||||
        foreach (var item in propertyInfos)
 | 
			
		||||
        {
 | 
			
		||||
            jtokens.Add(item.Name, JToken.FromObject(item.GetValue(nodeModel) ?? JValue.CreateNull()));
 | 
			
		||||
            jtokens.Add(item.Name, (item.GetValue(nodeModel) ?? JValue.CreateNull()).GetJTokenFromObj());
 | 
			
		||||
        }
 | 
			
		||||
        return jtokens;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,7 @@
 | 
			
		||||
using Newtonsoft.Json.Linq;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.NewLife.Json.Extension;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
 | 
			
		||||
public class NodeInput
 | 
			
		||||
@@ -9,7 +11,7 @@ public class NodeInput
 | 
			
		||||
    {
 | 
			
		||||
        get
 | 
			
		||||
        {
 | 
			
		||||
            return JToken.FromObject(input);
 | 
			
		||||
            return (input).GetJTokenFromObj();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,7 @@
 | 
			
		||||
using Newtonsoft.Json.Linq;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.NewLife.Json.Extension;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
 | 
			
		||||
public class NodeOutput
 | 
			
		||||
@@ -9,7 +11,7 @@ public class NodeOutput
 | 
			
		||||
    {
 | 
			
		||||
        get
 | 
			
		||||
        {
 | 
			
		||||
            return JToken.FromObject(output);
 | 
			
		||||
            return (output).GetJTokenFromObj();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@
 | 
			
		||||
// QQ群:605534569
 | 
			
		||||
// ------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
 | 
			
		||||
public interface IRulesEngineHostedService
 | 
			
		||||
@@ -18,4 +19,6 @@ public interface IRulesEngineHostedService
 | 
			
		||||
    Task<Rules> GetRuleRuntimesAsync(long rulesId);
 | 
			
		||||
    Task DeleteRuleRuntimesAsync(List<long> ids);
 | 
			
		||||
    Task EditRuleRuntimesAsync(Rules rules);
 | 
			
		||||
    Task RestartRuleRuntimeAsync();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -10,9 +10,11 @@
 | 
			
		||||
 | 
			
		||||
using BootstrapBlazor.Components;
 | 
			
		||||
 | 
			
		||||
using System.Linq.Expressions;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
 | 
			
		||||
public interface IRulesService
 | 
			
		||||
public interface IRulesPageService
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 清除所有规则
 | 
			
		||||
@@ -25,12 +27,6 @@ public interface IRulesService
 | 
			
		||||
    /// <param name="ids">待删除规则的ID列表</param>
 | 
			
		||||
    Task<bool> DeleteRulesAsync(List<long> ids);
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 从缓存/数据库获取全部信息
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns>规则列表</returns>
 | 
			
		||||
    Task<List<Rules>> GetAllRulesAsync();
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 报表查询
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -45,3 +41,13 @@ public interface IRulesService
 | 
			
		||||
    /// <param name="type">保存类型</param>
 | 
			
		||||
    Task<bool> SaveRulesAsync(Rules input, ItemChangedType type);
 | 
			
		||||
}
 | 
			
		||||
public interface IRulesService : IRulesPageService
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 从缓存/数据库获取全部信息
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns>规则列表</returns>
 | 
			
		||||
    Task<List<TResult>> GetFromDBAsync<TResult>(Expression<Func<Rules, TResult>> slct, Expression<Func<Rules, bool>> expression = null, SqlSugarClient db = null);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -234,13 +234,31 @@ internal sealed class RulesEngineHostedService : BackgroundService, IRulesEngine
 | 
			
		||||
    {
 | 
			
		||||
        await Task.Yield();
 | 
			
		||||
 | 
			
		||||
        await RestartRuleRuntimeAsync().ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
        while (!cancellationToken.IsCancellationRequested)
 | 
			
		||||
        {
 | 
			
		||||
            foreach (var item in Diagrams?.Values?.SelectMany(a => a.Nodes) ?? new List<NodeModel>())
 | 
			
		||||
            {
 | 
			
		||||
                if (item is IExexcuteExpressionsBase)
 | 
			
		||||
                {
 | 
			
		||||
                    CSharpScriptEngineExtension.SetExpire((item as TextNode).Text);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            await Task.Delay(60000, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task RestartRuleRuntimeAsync()
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            await RestartLock.WaitAsync(cancellationToken).ConfigureAwait(false); // 等待获取锁,以确保只有一个线程可以执行以下代码
 | 
			
		||||
            TokenSource ??= new CancellationTokenSource();
 | 
			
		||||
            await RestartLock.WaitAsync().ConfigureAwait(false);
 | 
			
		||||
            Cancel();
 | 
			
		||||
            Clear();
 | 
			
		||||
            TokenSource ??= new CancellationTokenSource();
 | 
			
		||||
 | 
			
		||||
            Rules = await App.GetService<IRulesService>().GetAllRulesAsync().ConfigureAwait(false);
 | 
			
		||||
            Rules = await App.GetService<IRulesService>().GetFromDBAsync(a => a).ConfigureAwait(false);
 | 
			
		||||
            Diagrams = new();
 | 
			
		||||
            foreach (var rules in Rules.Where(a => a.Status))
 | 
			
		||||
            {
 | 
			
		||||
@@ -259,18 +277,6 @@ internal sealed class RulesEngineHostedService : BackgroundService, IRulesEngine
 | 
			
		||||
        {
 | 
			
		||||
            RestartLock.Release(); // 释放锁
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        while (!cancellationToken.IsCancellationRequested)
 | 
			
		||||
        {
 | 
			
		||||
            foreach (var item in Diagrams?.Values?.SelectMany(a => a.Nodes) ?? new List<NodeModel>())
 | 
			
		||||
            {
 | 
			
		||||
                if (item is IExexcuteExpressionsBase)
 | 
			
		||||
                {
 | 
			
		||||
                    CSharpScriptEngineExtension.SetExpire((item as TextNode).Text);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            await Task.Delay(60000, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override async Task StopAsync(CancellationToken cancellationToken)
 | 
			
		||||
 
 | 
			
		||||
@@ -10,9 +10,7 @@
 | 
			
		||||
 | 
			
		||||
using BootstrapBlazor.Components;
 | 
			
		||||
 | 
			
		||||
using System.Data;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.Extension.Generic;
 | 
			
		||||
using System.Linq.Expressions;
 | 
			
		||||
 | 
			
		||||
using TouchSocket.Core;
 | 
			
		||||
 | 
			
		||||
@@ -38,15 +36,12 @@ internal sealed class RulesService : BaseService<Rules>, IRulesService
 | 
			
		||||
        var dataScope = await GlobalData.SysUserService.GetCurrentUserDataScopeAsync().ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
        using var db = GetDB();
 | 
			
		||||
        var ids = await db.Queryable<Rules>().WhereIF(dataScope != null && dataScope?.Count > 0, u => dataScope.Contains(u.CreateOrgId))//在指定机构列表查询
 | 
			
		||||
            .WhereIF(dataScope?.Count == 0, u => u.CreateUserId == UserManager.UserId)
 | 
			
		||||
            .Select(a => a.Id).ToListAsync().ConfigureAwait(false);
 | 
			
		||||
        await db.Deleteable<Rules>(a => ids.Contains(a.Id)).ExecuteCommandAsync().ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
        var data = (await GetAllRulesAsync().ConfigureAwait(false))
 | 
			
		||||
              .WhereIf(dataScope != null && dataScope?.Count > 0, u => dataScope.Contains(u.CreateOrgId))//在指定机构列表查询
 | 
			
		||||
            .WhereIf(dataScope?.Count == 0, u => u.CreateUserId == UserManager.UserId)
 | 
			
		||||
            .Select(a => a.Id).ToList();
 | 
			
		||||
        await db.Deleteable<Rules>(a => data.Contains(a.Id)).ExecuteCommandAsync().ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
        DeleteRulesFromCache();
 | 
			
		||||
        await RulesEngineHostedService.DeleteRuleRuntimesAsync(data).ConfigureAwait(false);
 | 
			
		||||
        await RulesEngineHostedService.DeleteRuleRuntimesAsync(ids).ConfigureAwait(false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [OperDesc("DeleteRules", localizerType: typeof(Rules))]
 | 
			
		||||
@@ -60,30 +55,20 @@ internal sealed class RulesService : BaseService<Rules>, IRulesService
 | 
			
		||||
 | 
			
		||||
.ExecuteCommandAsync().ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
        DeleteRulesFromCache();
 | 
			
		||||
        await RulesEngineHostedService.DeleteRuleRuntimesAsync(ids).ConfigureAwait(false);
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
    private const string cacheKey = "ThingsGateway:Cache_RulesEngines:List";
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    public void DeleteRulesFromCache()
 | 
			
		||||
    {
 | 
			
		||||
        App.CacheService.Remove(cacheKey);//删除通道缓存
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 从缓存/数据库获取全部信息
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns>列表</returns>
 | 
			
		||||
    public async Task<List<Rules>> GetAllRulesAsync()
 | 
			
		||||
    public async Task<List<TResult>> GetFromDBAsync<TResult>(Expression<Func<Rules, TResult>> slct, Expression<Func<Rules, bool>> expression = null, SqlSugarClient db = null)
 | 
			
		||||
    {
 | 
			
		||||
        var channels = App.CacheService.Get<List<Rules>>(cacheKey);
 | 
			
		||||
        if (channels == null)
 | 
			
		||||
        {
 | 
			
		||||
            using var db = GetDB();
 | 
			
		||||
            channels = await db.Queryable<Rules>().ToListAsync().ConfigureAwait(false);
 | 
			
		||||
            App.CacheService.Set(cacheKey, channels);
 | 
			
		||||
        }
 | 
			
		||||
        db ??= GetDB();
 | 
			
		||||
        var channels = await db.Queryable<Rules>().WhereIF(expression != null, expression).OrderBy(a => a.Id).Select(slct).ToListAsync().ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
        return channels;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -117,7 +102,6 @@ internal sealed class RulesService : BaseService<Rules>, IRulesService
 | 
			
		||||
 | 
			
		||||
        if (await base.SaveAsync(input, type).ConfigureAwait(false))
 | 
			
		||||
        {
 | 
			
		||||
            DeleteRulesFromCache();
 | 
			
		||||
            await RulesEngineHostedService.EditRuleRuntimesAsync(input).ConfigureAwait(false);
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -35,7 +35,7 @@ internal static class RuntimeServiceHelper
 | 
			
		||||
                {
 | 
			
		||||
                    newDeviceRuntime.Init(newChannelRuntime);
 | 
			
		||||
 | 
			
		||||
                    var newVariableRuntimes = (await GlobalData.VariableService.GetAllAsync(newDeviceRuntime.Id).ConfigureAwait(false)).AdaptListVariableRuntime();
 | 
			
		||||
                    var newVariableRuntimes = (await GlobalData.VariableService.GetAllAsync(newDeviceRuntime.Id).ConfigureAwait(false)).AdaptEnumerableVariableRuntime();
 | 
			
		||||
 | 
			
		||||
                    newVariableRuntimes.ParallelForEach(item => item.Init(newDeviceRuntime));
 | 
			
		||||
                }
 | 
			
		||||
@@ -79,7 +79,7 @@ internal static class RuntimeServiceHelper
 | 
			
		||||
                {
 | 
			
		||||
                    newDeviceRuntime.Init(newChannelRuntime);
 | 
			
		||||
 | 
			
		||||
                    var newVariableRuntimes = (await GlobalData.VariableService.GetAllAsync(newDeviceRuntime.Id).ConfigureAwait(false)).AdaptListVariableRuntime();
 | 
			
		||||
                    var newVariableRuntimes = (await GlobalData.VariableService.GetAllAsync(newDeviceRuntime.Id).ConfigureAwait(false)).AdaptEnumerableVariableRuntime();
 | 
			
		||||
 | 
			
		||||
                    newVariableRuntimes.ParallelForEach(item => item.Init(newDeviceRuntime));
 | 
			
		||||
                }
 | 
			
		||||
@@ -113,8 +113,7 @@ internal static class RuntimeServiceHelper
 | 
			
		||||
            }
 | 
			
		||||
            if (deviceRuntime != null)
 | 
			
		||||
            {
 | 
			
		||||
                var list = deviceRuntime.VariableRuntimes.Select(a => a.Value).ToArray();
 | 
			
		||||
                list.ParallelForEach(a => a.Init(newDeviceRuntime));
 | 
			
		||||
                deviceRuntime.VariableRuntimes.ParallelForEach(a => a.Value.Init(newDeviceRuntime));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -141,7 +140,7 @@ internal static class RuntimeServiceHelper
 | 
			
		||||
    {
 | 
			
		||||
        var channels = oldChannelRuntimes.ToArray();
 | 
			
		||||
        var devs = channels.SelectMany(a => a.DeviceRuntimes).Select(a => a.Value).ToArray();
 | 
			
		||||
        devs.SelectMany(a => a.VariableRuntimes).Select(a => a.Value).ToArray().ParallelForEach(a => a.Dispose());
 | 
			
		||||
        devs.SelectMany(a => a.VariableRuntimes).Select(a => a.Value).ParallelForEach(a => a.Dispose());
 | 
			
		||||
        devs.ParallelForEach(a => a.Dispose());
 | 
			
		||||
        channels.ParallelForEach(a => a.Dispose());
 | 
			
		||||
 | 
			
		||||
@@ -168,7 +167,7 @@ internal static class RuntimeServiceHelper
 | 
			
		||||
        foreach (var deviceRuntime in deviceRuntimes)
 | 
			
		||||
        {
 | 
			
		||||
            //也需要删除变量
 | 
			
		||||
            var vars = deviceRuntime.VariableRuntimes.Select(a => a.Value).ToArray();
 | 
			
		||||
            var vars = deviceRuntime.VariableRuntimes.Select(a => a.Value);
 | 
			
		||||
            vars.ParallelForEach(v =>
 | 
			
		||||
            {
 | 
			
		||||
                //需要重启业务线程
 | 
			
		||||
@@ -205,7 +204,7 @@ internal static class RuntimeServiceHelper
 | 
			
		||||
                //也需要删除设备和变量
 | 
			
		||||
                devs.ParallelForEach((a =>
 | 
			
		||||
                {
 | 
			
		||||
                    var vars = a.VariableRuntimes.Select(b => b.Value).ToArray();
 | 
			
		||||
                    var vars = a.VariableRuntimes.Select(b => b.Value);
 | 
			
		||||
                    ParallelExtensions.ParallelForEach(vars, (v =>
 | 
			
		||||
                    {
 | 
			
		||||
                        //需要重启业务线程
 | 
			
		||||
@@ -240,6 +239,11 @@ internal static class RuntimeServiceHelper
 | 
			
		||||
            if (group.Key != null)
 | 
			
		||||
                await group.Key.RestartDeviceAsync(group.Value, false).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
        foreach (var group in GlobalData.GetAllVariableBusinessDeviceRuntime().Where(a => a.Driver?.DeviceThreadManage != null).GroupBy(a => a.Driver.DeviceThreadManage))
 | 
			
		||||
        {
 | 
			
		||||
            if (group.Key != null)
 | 
			
		||||
                await group.Key.RestartDeviceAsync(group.ToArray(), false).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    public static async Task RemoveDeviceAsync(HashSet<long> newDeciceIds)
 | 
			
		||||
    {
 | 
			
		||||
@@ -255,11 +259,18 @@ internal static class RuntimeServiceHelper
 | 
			
		||||
            if (group.Key != null)
 | 
			
		||||
                await group.Key.RemoveDeviceAsync(group.Value.Select(a => a.Id).ToArray()).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        foreach (var group in GlobalData.GetAllVariableBusinessDeviceRuntime().Where(a => a.Driver?.DeviceThreadManage != null).GroupBy(a => a.Driver.DeviceThreadManage))
 | 
			
		||||
        {
 | 
			
		||||
            if (group.Key != null)
 | 
			
		||||
                await group.Key.RestartDeviceAsync(group.ToArray(), false).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static async Task ChangedDriverAsync(ILogger logger)
 | 
			
		||||
    {
 | 
			
		||||
        var channelDevice = GlobalData.IdDevices.Where(a => a.Value.Driver?.DriverProperties is IBusinessPropertyAllVariableBase property && property.IsAllVariable).Select(a => a.Value).ToArray();
 | 
			
		||||
        var channelDevice = GlobalData.GetAllVariableBusinessDeviceRuntime();
 | 
			
		||||
 | 
			
		||||
        await channelDevice.ParallelForEachAsync(async (item, token) =>
 | 
			
		||||
         {
 | 
			
		||||
@@ -275,7 +286,7 @@ internal static class RuntimeServiceHelper
 | 
			
		||||
    }
 | 
			
		||||
    public static async Task ChangedDriverAsync(ConcurrentHashSet<IDriver> changedDriver, ILogger logger)
 | 
			
		||||
    {
 | 
			
		||||
        var drivers = GlobalData.IdDevices.Where(a => a.Value.Driver?.DriverProperties is IBusinessPropertyAllVariableBase property && property.IsAllVariable).Select(a => a.Value.Driver);
 | 
			
		||||
        var drivers = GlobalData.GetAllVariableBusinessDriver();
 | 
			
		||||
 | 
			
		||||
        var changedDrivers = drivers.Concat(changedDriver).Where(a => a.DisposedValue == false).Distinct().ToArray();
 | 
			
		||||
        await changedDrivers.ParallelForEachAsync(async (driver, token) =>
 | 
			
		||||
@@ -293,7 +304,7 @@ internal static class RuntimeServiceHelper
 | 
			
		||||
 | 
			
		||||
    public static void AddBusinessChangedDriver(HashSet<long> variableIds, ConcurrentHashSet<IDriver> changedDriver)
 | 
			
		||||
    {
 | 
			
		||||
        var data = GlobalData.IdVariables.FilterByKeys(variableIds).GroupBy(a => a.Value.DeviceRuntime);
 | 
			
		||||
        var data = GlobalData.IdVariables.FilterByKeys(variableIds).GroupBy(a => a.Value.DeviceRuntime).Where(a => a.Key != null);
 | 
			
		||||
 | 
			
		||||
        foreach (var group in data)
 | 
			
		||||
        {
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -106,10 +106,10 @@ internal interface IVariableService
 | 
			
		||||
    Task UpdateInitValueAsync(List<Variable> variables);
 | 
			
		||||
 | 
			
		||||
    Task<List<Variable>> GetByDeviceIdAsync(List<long> deviceIds);
 | 
			
		||||
    void DeleteVariableCache();
 | 
			
		||||
    ImportPreviewOutput<Dictionary<string, Variable>> SetVariableData(HashSet<long>? dataScope, IReadOnlyDictionary<string, DeviceRuntime> deviceDicts, Dictionary<string, ImportPreviewOutputBase> ImportPreviews, ImportPreviewOutput<Dictionary<string, Variable>> deviceImportPreview, Dictionary<string, PluginInfo> driverPluginNameDict, ConcurrentDictionary<string, (Type, Dictionary<string, PropertyInfo>, Dictionary<string, PropertyInfo>)> propertysDict, string sheetName, IEnumerable<IDictionary<string, object>> rows);
 | 
			
		||||
    List<VariableRuntime> GetAllVariableRuntime();
 | 
			
		||||
    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);
 | 
			
		||||
 
 | 
			
		||||
@@ -226,9 +226,7 @@ internal sealed class VariableService : BaseService<Variable>, IVariableService
 | 
			
		||||
        }).ConfigureAwait(false);
 | 
			
		||||
        if (result.IsSuccess)//如果成功了
 | 
			
		||||
        {
 | 
			
		||||
            _channelService.DeleteChannelFromCache();//刷新缓存
 | 
			
		||||
            _deviceService.DeleteDeviceFromCache();
 | 
			
		||||
            DeleteVariableCache();
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
@@ -237,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>
 | 
			
		||||
@@ -265,7 +401,7 @@ internal sealed class VariableService : BaseService<Variable>, IVariableService
 | 
			
		||||
 | 
			
		||||
            if (result > 0)
 | 
			
		||||
            {
 | 
			
		||||
                DeleteVariableCache();
 | 
			
		||||
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
@@ -277,7 +413,7 @@ internal sealed class VariableService : BaseService<Variable>, IVariableService
 | 
			
		||||
 | 
			
		||||
            if (result > 0)
 | 
			
		||||
            {
 | 
			
		||||
                DeleteVariableCache();
 | 
			
		||||
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
@@ -300,8 +436,7 @@ internal sealed class VariableService : BaseService<Variable>, IVariableService
 | 
			
		||||
             .ToList();
 | 
			
		||||
 | 
			
		||||
            var result = (await db.Updateable(data).UpdateColumns(differences.Select(a => a.Key).ToArray()).ExecuteCommandAsync().ConfigureAwait(false)) > 0;
 | 
			
		||||
            if (result)
 | 
			
		||||
                DeleteVariableCache();
 | 
			
		||||
 | 
			
		||||
            return result;
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
@@ -320,8 +455,6 @@ internal sealed class VariableService : BaseService<Variable>, IVariableService
 | 
			
		||||
             .WhereIF(dataScope?.Count == 0, u => u.CreateUserId == UserManager.UserId)
 | 
			
		||||
            .ExecuteCommandAsync().ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
        if (result > 0)
 | 
			
		||||
            DeleteVariableCache();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [OperDesc("DeleteVariable", isRecordPar: false, localizerType: typeof(Variable))]
 | 
			
		||||
@@ -335,8 +468,6 @@ internal sealed class VariableService : BaseService<Variable>, IVariableService
 | 
			
		||||
             .WhereIF(dataScope?.Count == 0, u => u.CreateUserId == UserManager.UserId)
 | 
			
		||||
             .ExecuteCommandAsync().ConfigureAwait(false)) > 0;
 | 
			
		||||
 | 
			
		||||
        if (result)
 | 
			
		||||
            DeleteVariableCache();
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -441,16 +572,13 @@ internal sealed class VariableService : BaseService<Variable>, IVariableService
 | 
			
		||||
 | 
			
		||||
        if (await base.SaveAsync(input, type).ConfigureAwait(false))
 | 
			
		||||
        {
 | 
			
		||||
            DeleteVariableCache();
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void DeleteVariableCache()
 | 
			
		||||
    {
 | 
			
		||||
        //App.CacheService.Remove(ThingsGatewayCacheConst.Cache_Variable);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public List<VariableRuntime> GetAllVariableRuntime()
 | 
			
		||||
    {
 | 
			
		||||
@@ -579,7 +707,7 @@ internal sealed class VariableService : BaseService<Variable>, IVariableService
 | 
			
		||||
            await db.BulkCopyAsync(insertData, 200000).ConfigureAwait(false);
 | 
			
		||||
            await db.BulkUpdateAsync(upData, 200000).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
        DeleteVariableCache();
 | 
			
		||||
 | 
			
		||||
        return upData.Select(a => a.Id).Concat(insertData.Select(a => a.Id)).ToHashSet();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -25,6 +25,7 @@ public class Startup : AppStartup
 | 
			
		||||
        #region R
 | 
			
		||||
 | 
			
		||||
        services.AddSingleton<IRulesService, RulesService>();
 | 
			
		||||
        services.AddSingleton<IRulesPageService>(a => a.GetService<IRulesService>());
 | 
			
		||||
        services.AddGatewayHostedService<IRulesEngineHostedService, RulesEngineHostedService>();
 | 
			
		||||
 | 
			
		||||
        #endregion
 | 
			
		||||
 
 | 
			
		||||
@@ -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.57" />
 | 
			
		||||
		<!--<PackageReference Include="TouchSocket.WebApi.Swagger" Version="4.0.0-beta.57" />-->
 | 
			
		||||
		<PackageReference Include="TouchSocket.WebApi" Version="4.0.0-beta.57" />
 | 
			
		||||
		<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 {
 | 
			
		||||
 
 | 
			
		||||
@@ -167,7 +167,9 @@
 | 
			
		||||
    "Date": "Date"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Gateway.Razor.ChannelDeviceTree": {
 | 
			
		||||
    "ShowType": "ShowType"
 | 
			
		||||
    "ChannelTable": "ChannelTable",
 | 
			
		||||
    "DeviceTable": "DeviceTable",
 | 
			
		||||
    "LogInfo": "LogInfo"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Gateway.Razor.GatewayAbout": {
 | 
			
		||||
    "AuthName": "AuthName",
 | 
			
		||||
@@ -243,6 +245,7 @@
 | 
			
		||||
    "BusinessEnable": "BusinessEnable",
 | 
			
		||||
    "SlaveUrl": "SlaveUrl",
 | 
			
		||||
    "Test": "Addition of test variables",
 | 
			
		||||
    "TestDtu": "Addition of test dtu variables",
 | 
			
		||||
    "TestDeviceCount": "TestDeviceCount",
 | 
			
		||||
    "TestVariableCount": "TestVariableCount",
 | 
			
		||||
    "WriteValue": "WriteValue",
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user