Compare commits
	
		
			14 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					dbee8496cb | ||
| 
						 | 
					044e78bea9 | ||
| 
						 | 
					fe79128d90 | ||
| 
						 | 
					34120da008 | ||
| 
						 | 
					4c62bb0b21 | ||
| 
						 | 
					46b16279c7 | ||
| 
						 | 
					0779efc5dd | ||
| 
						 | 
					8acdb780e8 | ||
| 
						 | 
					2e310b919e | ||
| 
						 | 
					4155c07269 | ||
| 
						 | 
					32fa833736 | ||
| 
						 | 
					2258f08555 | ||
| 
						 | 
					d90b32f165 | ||
| 
						 | 
					1492377322 | 
@@ -8,10 +8,30 @@
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.Extension;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Plugin.Synchronization;
 | 
			
		||||
namespace ThingsGateway.Admin.Application;
 | 
			
		||||
 | 
			
		||||
public partial class Synchronization : BusinessBase
 | 
			
		||||
/// <inheritdoc/>
 | 
			
		||||
[ThingsGateway.DependencyInjection.SuppressSniffer]
 | 
			
		||||
public static class SchemeHelper
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    public static string GetOrCreate()
 | 
			
		||||
    {
 | 
			
		||||
        var path = "Keys/SchemeKey.txt";
 | 
			
		||||
        if (File.Exists(path))
 | 
			
		||||
        {
 | 
			
		||||
            var data = File.ReadAllText(path);
 | 
			
		||||
            return data;
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            var data = DateTime.UtcNow.ToDefaultDateTimeFormat();
 | 
			
		||||
            Directory.CreateDirectory("Keys");
 | 
			
		||||
            File.WriteAllText(path, data);
 | 
			
		||||
            return data;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -13,6 +13,7 @@ using Microsoft.AspNetCore.ResponseCompression;
 | 
			
		||||
using System.Runtime.InteropServices;
 | 
			
		||||
using System.Text;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.Admin.Application;
 | 
			
		||||
using ThingsGateway.NewLife.Log;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.AdminServer;
 | 
			
		||||
@@ -29,6 +30,8 @@ public class Program
 | 
			
		||||
        // 增加中文编码支持
 | 
			
		||||
        Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
 | 
			
		||||
 | 
			
		||||
        ThingsGateway.Admin.Application.ClaimConst.Scheme = $"{typeof(Program).Assembly.GetName().Name}{SchemeHelper.GetOrCreate()}";
 | 
			
		||||
 | 
			
		||||
        #region 控制台输出Logo
 | 
			
		||||
 | 
			
		||||
        Console.Write(Environment.NewLine);
 | 
			
		||||
 
 | 
			
		||||
@@ -68,7 +68,6 @@ public sealed class DataValidationAttribute : ValidationAttribute
 | 
			
		||||
            // 进行多语言处理
 | 
			
		||||
            var errorMessage = !string.IsNullOrWhiteSpace(ErrorMessage) ? ErrorMessage : resultMessage;
 | 
			
		||||
 | 
			
		||||
            //TODO: 修改为类型本地化
 | 
			
		||||
            return new ValidationResult(string.Format(App.StringLocalizerFactory == null ? errorMessage : App.CreateLocalizerByType(validationContext.ObjectType)[errorMessage], validationContext.DisplayName ?? validationContext.MemberName));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -32,6 +32,10 @@ public class ConcurrentHashSet<T> : IEnumerable<T> where T : notnull
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public Boolean TryRemove(T item) => _dic.TryRemove(item, out _);
 | 
			
		||||
 | 
			
		||||
    public ICollection<T> Keys => _dic.Keys;
 | 
			
		||||
 | 
			
		||||
    public void Clear() => _dic.Clear();
 | 
			
		||||
 | 
			
		||||
    #region IEnumerable<T> 成员
 | 
			
		||||
    IEnumerator<T> IEnumerable<T>.GetEnumerator() => _dic.Keys.GetEnumerator();
 | 
			
		||||
    #endregion
 | 
			
		||||
 
 | 
			
		||||
@@ -466,7 +466,14 @@ public class DefaultConvert
 | 
			
		||||
 | 
			
		||||
        if (value is Double d)
 | 
			
		||||
        {
 | 
			
		||||
            return Double.IsNaN(d) ? defaultValue : (Decimal)d;
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                return Double.IsNaN(d) ? defaultValue : (Decimal)d;
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
                return defaultValue;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
 
 | 
			
		||||
@@ -199,7 +199,7 @@ public static class Runtime
 | 
			
		||||
            if (_createConfigOnMissing == null)
 | 
			
		||||
            {
 | 
			
		||||
                var val = Environment.GetEnvironmentVariable("CreateConfigOnMissing");
 | 
			
		||||
                _createConfigOnMissing = !val.IsNullOrEmpty() ? val.ToBoolean(true) : true;
 | 
			
		||||
                _createConfigOnMissing = !val.IsNullOrEmpty() ? val.ToBoolean(false) : false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return _createConfigOnMissing.Value;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,7 @@
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.NewLife.Collections;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.NewLife.DictionaryExtensions;
 | 
			
		||||
 | 
			
		||||
/// <summary>并发字典扩展</summary>
 | 
			
		||||
@@ -52,6 +54,29 @@ public static class DictionaryExtensions
 | 
			
		||||
        // 返回成功移除的项目数量
 | 
			
		||||
        return count;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 根据指定的一组 key,批量从字典中筛选对应的键值对。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <typeparam name="TKey">字典键类型</typeparam>
 | 
			
		||||
    /// <typeparam name="TValue">字典值类型</typeparam>
 | 
			
		||||
    /// <param name="dictionary">源字典</param>
 | 
			
		||||
    /// <param name="keys">要筛选的 key 集合</param>
 | 
			
		||||
    /// <returns>匹配到的键值对序列</returns>
 | 
			
		||||
    public static IEnumerable<KeyValuePair<TKey, TValue>> FilterByKeys<TKey, TValue>(
 | 
			
		||||
        this IDictionary<TKey, TValue> dictionary,
 | 
			
		||||
        IEnumerable<TKey> keys)
 | 
			
		||||
    {
 | 
			
		||||
        foreach (var key in keys)
 | 
			
		||||
        {
 | 
			
		||||
            if (dictionary.TryGetValue(key, out var value))
 | 
			
		||||
            {
 | 
			
		||||
                yield return new KeyValuePair<TKey, TValue>(key, value);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 批量出队
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -79,6 +104,61 @@ public static class DictionaryExtensions
 | 
			
		||||
        }
 | 
			
		||||
        return list;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 批量出队
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static IEnumerable<T> ToIEnumerableWithDequeue<TKEY, T>(this ConcurrentDictionary<TKEY, T> values, int maxCount = 0)
 | 
			
		||||
    {
 | 
			
		||||
        if (values.IsEmpty) yield break;
 | 
			
		||||
        if (maxCount <= 0)
 | 
			
		||||
        {
 | 
			
		||||
            maxCount = values.Count;
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            maxCount = Math.Min(maxCount, values.Count);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var keys = values.Keys;
 | 
			
		||||
        foreach (var key in keys)
 | 
			
		||||
        {
 | 
			
		||||
            if (maxCount-- <= 0) break;
 | 
			
		||||
            if (values.TryRemove(key, out var result))
 | 
			
		||||
            {
 | 
			
		||||
                yield return result;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 批量出队
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static IEnumerable<TKEY> ToIEnumerableWithDequeue<TKEY>(this ConcurrentHashSet<TKEY> values, int maxCount = 0)
 | 
			
		||||
    {
 | 
			
		||||
        if (values.IsEmpty) yield break;
 | 
			
		||||
        if (maxCount <= 0)
 | 
			
		||||
        {
 | 
			
		||||
            maxCount = values.Count;
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            maxCount = Math.Min(maxCount, values.Count);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var keys = values.Keys;
 | 
			
		||||
        foreach (var key in keys)
 | 
			
		||||
        {
 | 
			
		||||
            if (maxCount-- <= 0) break;
 | 
			
		||||
            if (values.TryRemove(key))
 | 
			
		||||
            {
 | 
			
		||||
                yield return key;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 批量出队
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -109,4 +189,62 @@ public static class DictionaryExtensions
 | 
			
		||||
        return dict;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 批量出队
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static IEnumerable<KeyValuePair<TKEY, T>> ToIEnumerableKVWithDequeue<TKEY, T>(this ConcurrentDictionary<TKEY, T> values, int maxCount = 0)
 | 
			
		||||
    {
 | 
			
		||||
        if (values.IsEmpty) yield break;
 | 
			
		||||
 | 
			
		||||
        if (maxCount <= 0)
 | 
			
		||||
        {
 | 
			
		||||
            maxCount = values.Count;
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            maxCount = Math.Min(maxCount, values.Count);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var keys = values.Keys;
 | 
			
		||||
        foreach (var key in keys)
 | 
			
		||||
        {
 | 
			
		||||
            if (maxCount-- <= 0) break;
 | 
			
		||||
            if (values.TryRemove(key, out var result))
 | 
			
		||||
            {
 | 
			
		||||
                yield return new(key, result);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 批量出队
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static List<TKEY> ToListWithDequeue<TKEY>(this ConcurrentHashSet<TKEY> values, int maxCount = 0)
 | 
			
		||||
    {
 | 
			
		||||
        List<TKEY> result = new();
 | 
			
		||||
        if (values.IsEmpty) return result;
 | 
			
		||||
        if (maxCount <= 0)
 | 
			
		||||
        {
 | 
			
		||||
            maxCount = values.Count;
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            maxCount = Math.Min(maxCount, values.Count);
 | 
			
		||||
        }
 | 
			
		||||
        var keys = values.Keys;
 | 
			
		||||
        foreach (var key in keys)
 | 
			
		||||
        {
 | 
			
		||||
            if (maxCount-- <= 0) break;
 | 
			
		||||
            if (values.TryRemove(key))
 | 
			
		||||
            {
 | 
			
		||||
                result.Add(key);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
   
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -15,6 +15,30 @@ namespace ThingsGateway.Extension.Generic;
 | 
			
		||||
/// <inheritdoc/>
 | 
			
		||||
public static class LinqExtensions
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 将序列分批,每批固定数量
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static IEnumerable<List<T>> Batch<T>(this IEnumerable<T> source, int batchSize)
 | 
			
		||||
    {
 | 
			
		||||
        if (source == null) throw new ArgumentNullException(nameof(source));
 | 
			
		||||
        if (batchSize <= 0) throw new ArgumentOutOfRangeException(nameof(batchSize));
 | 
			
		||||
 | 
			
		||||
        List<T> batch = new List<T>(batchSize);
 | 
			
		||||
        foreach (var item in source)
 | 
			
		||||
        {
 | 
			
		||||
            batch.Add(item);
 | 
			
		||||
            if (batch.Count >= batchSize)
 | 
			
		||||
            {
 | 
			
		||||
                yield return batch;
 | 
			
		||||
                batch = new List<T>(batchSize);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 剩余不足 batchSize 的最后一批
 | 
			
		||||
        if (batch.Count > 0)
 | 
			
		||||
            yield return batch;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public static ICollection<T> AddIF<T>(this ICollection<T> thisValue, bool isOk, Func<T> predicate)
 | 
			
		||||
    {
 | 
			
		||||
@@ -44,7 +68,7 @@ public static class LinqExtensions
 | 
			
		||||
        }
 | 
			
		||||
        return thisValue;
 | 
			
		||||
    }
 | 
			
		||||
#if NET6_0_OR_GREATER
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public static void AddRange<TKey, TItem>(this Dictionary<TKey, TItem> @this, IEnumerable<KeyValuePair<TKey, TItem>> values)
 | 
			
		||||
    {
 | 
			
		||||
@@ -53,7 +77,6 @@ public static class LinqExtensions
 | 
			
		||||
            @this.TryAdd(value.Key, value.Value);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public static void AddRange<T>(this ICollection<T> @this, IEnumerable<T> values)
 | 
			
		||||
    {
 | 
			
		||||
 
 | 
			
		||||
@@ -275,9 +275,11 @@ public class TimerX : ITimer, ITimerx, IDisposable
 | 
			
		||||
        //Init(_AbsolutelyNext = _cron.GetNext(DateTime.Now));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool Disposed { get; private set; }
 | 
			
		||||
    /// <summary>销毁定时器</summary>
 | 
			
		||||
    public void Dispose()
 | 
			
		||||
    {
 | 
			
		||||
        Disposed = true;
 | 
			
		||||
        Dispose(true);
 | 
			
		||||
 | 
			
		||||
        // 告诉GC,不要调用析构函数
 | 
			
		||||
 
 | 
			
		||||
@@ -131,7 +131,7 @@ namespace ThingsGateway.SqlSugar
 | 
			
		||||
            //TODO: optimize the type caching -- if fields are simply reordered, that doesn't mean that they're actually different types, so this needs to be smarter
 | 
			
		||||
            string key = string.Empty;
 | 
			
		||||
            foreach (var prop in properties)
 | 
			
		||||
                key += prop.Key + ";" + prop.Value.Name + ";";
 | 
			
		||||
                key += $"{prop.Key};{prop.Value.Name};";
 | 
			
		||||
 | 
			
		||||
            return key;
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,11 @@
 | 
			
		||||
<Project>
 | 
			
		||||
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<PluginVersion>10.9.18</PluginVersion>
 | 
			
		||||
		<ProPluginVersion>10.9.18</ProPluginVersion>
 | 
			
		||||
		<AuthenticationVersion>2.9.8</AuthenticationVersion>
 | 
			
		||||
		<SourceGeneratorVersion>10.9.8</SourceGeneratorVersion>
 | 
			
		||||
		<PluginVersion>10.9.28</PluginVersion>
 | 
			
		||||
		<ProPluginVersion>10.9.26</ProPluginVersion>
 | 
			
		||||
		<DefaultVersion>10.9.28</DefaultVersion>
 | 
			
		||||
		<AuthenticationVersion>2.9.13</AuthenticationVersion>
 | 
			
		||||
		<SourceGeneratorVersion>10.9.13</SourceGeneratorVersion>
 | 
			
		||||
		<NET8Version>8.0.17</NET8Version>
 | 
			
		||||
		<NET9Version>9.0.6</NET9Version>
 | 
			
		||||
		<SatelliteResourceLanguages>zh-Hans;en-US</SatelliteResourceLanguages>
 | 
			
		||||
@@ -56,4 +57,13 @@
 | 
			
		||||
			<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
 | 
			
		||||
		</PackageReference>
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	<Target Name="EnsureLocalNugetFolderExists" BeforeTargets="CollectPackageReferences">
 | 
			
		||||
		<MakeDir Directories="$(SolutionDir)..\..\nupkgs" />
 | 
			
		||||
	</Target>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
</Project>
 | 
			
		||||
 
 | 
			
		||||
@@ -51,6 +51,4 @@ public enum DataTypeEnum
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    Double,
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    Decimal,
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -34,7 +34,6 @@ public static class DataTypeExtensions
 | 
			
		||||
            DataTypeEnum.UInt64 => 8,
 | 
			
		||||
            DataTypeEnum.Single => 4,
 | 
			
		||||
            DataTypeEnum.Double => 8,
 | 
			
		||||
            DataTypeEnum.Decimal => 16,
 | 
			
		||||
            _ => 0,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -114,7 +114,7 @@ public static class GenericExtensions
 | 
			
		||||
    public static IEnumerable<IEnumerable<T>> ChunkBetter<T>(this IEnumerable<T> source, int chunkSize, bool isToList = false)
 | 
			
		||||
    {
 | 
			
		||||
        if (chunkSize <= 0)
 | 
			
		||||
            chunkSize = source.Count();
 | 
			
		||||
            yield break;
 | 
			
		||||
        var pos = 0;
 | 
			
		||||
        while (source.Skip(pos).Any())
 | 
			
		||||
        {
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,8 @@
 | 
			
		||||
 | 
			
		||||
using Newtonsoft.Json.Linq;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.Foundation.Extension.Generic;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Foundation;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
@@ -68,7 +70,7 @@ public static class ThingsGatewayBitConverterExtension
 | 
			
		||||
                    for (int i = 0; i < arrayLength; i++)
 | 
			
		||||
                    {
 | 
			
		||||
                        var data = byteConverter.GetBytes(strings[i]);
 | 
			
		||||
                        bytes.AddRange(data);
 | 
			
		||||
                        bytes.AddRange(data.ArrayExpandToLength(byteConverter.StringLength ?? data.Length));
 | 
			
		||||
                    }
 | 
			
		||||
                    return bytes.ToArray();
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@ public class CronScheduledTask : DisposeBase, IScheduledTask
 | 
			
		||||
    private volatile int _isRunning = 0;
 | 
			
		||||
    private volatile int _pendingTriggers = 0;
 | 
			
		||||
    public Int32 Period => _timer?.Period ?? 0;
 | 
			
		||||
    public bool Enable => _timer?.Disposed != false ? false : true;
 | 
			
		||||
 | 
			
		||||
    public CronScheduledTask(string interval, Func<object?, CancellationToken, Task> taskFunc, object? state, ILog log, CancellationToken token)
 | 
			
		||||
    {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,146 +1,146 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
//  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
 | 
			
		||||
//  此代码版权(除特别声明外的代码)归作者本人Diego所有
 | 
			
		||||
//  源代码使用协议遵循本仓库的开源协议及附加协议
 | 
			
		||||
//  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
 | 
			
		||||
//  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
 | 
			
		||||
//  使用文档:https://thingsgateway.cn/
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
////------------------------------------------------------------------------------
 | 
			
		||||
////  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
 | 
			
		||||
////  此代码版权(除特别声明外的代码)归作者本人Diego所有
 | 
			
		||||
////  源代码使用协议遵循本仓库的开源协议及附加协议
 | 
			
		||||
////  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
 | 
			
		||||
////  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
 | 
			
		||||
////  使用文档:https://thingsgateway.cn/
 | 
			
		||||
////  QQ群:605534569
 | 
			
		||||
////------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using Microsoft.Extensions.Logging;
 | 
			
		||||
//using Microsoft.Extensions.Logging;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.NewLife;
 | 
			
		||||
//using ThingsGateway.NewLife;
 | 
			
		||||
 | 
			
		||||
using TouchSocket.Core;
 | 
			
		||||
//using TouchSocket.Core;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
//namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
 | 
			
		||||
[ThingsGateway.DependencyInjection.SuppressSniffer]
 | 
			
		||||
public class DoTask
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 取消令牌
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    private CancellationTokenSource? _cancelTokenSource;
 | 
			
		||||
    private object? _state;
 | 
			
		||||
//[ThingsGateway.DependencyInjection.SuppressSniffer]
 | 
			
		||||
//public class DoTask
 | 
			
		||||
//{
 | 
			
		||||
//    /// <summary>
 | 
			
		||||
//    /// 取消令牌
 | 
			
		||||
//    /// </summary>
 | 
			
		||||
//    private CancellationTokenSource? _cancelTokenSource;
 | 
			
		||||
//    private object? _state;
 | 
			
		||||
 | 
			
		||||
    public DoTask(Func<object?, CancellationToken, Task> doWork, ILog logger, object? state = null, string taskName = null)
 | 
			
		||||
    {
 | 
			
		||||
        DoWork = doWork; Logger = logger; TaskName = taskName; _state = state;
 | 
			
		||||
    }
 | 
			
		||||
//    public DoTask(Func<object?, CancellationToken, Task> doWork, ILog logger, object? state = null, string taskName = null)
 | 
			
		||||
//    {
 | 
			
		||||
//        DoWork = doWork; Logger = logger; TaskName = taskName; _state = state;
 | 
			
		||||
//    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 执行任务方法
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public Func<object?, CancellationToken, Task> DoWork { get; }
 | 
			
		||||
    private ILog Logger { get; }
 | 
			
		||||
    private Task PrivateTask { get; set; }
 | 
			
		||||
    private string TaskName { get; }
 | 
			
		||||
//    /// <summary>
 | 
			
		||||
//    /// 执行任务方法
 | 
			
		||||
//    /// </summary>
 | 
			
		||||
//    public Func<object?, CancellationToken, Task> DoWork { get; }
 | 
			
		||||
//    private ILog Logger { get; }
 | 
			
		||||
//    private Task PrivateTask { get; set; }
 | 
			
		||||
//    private string TaskName { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 开始
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="cancellationToken">调度取消令牌</param>
 | 
			
		||||
    public void Start(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            WaitLock.Wait(cancellationToken);
 | 
			
		||||
//    /// <summary>
 | 
			
		||||
//    /// 开始
 | 
			
		||||
//    /// </summary>
 | 
			
		||||
//    /// <param name="cancellationToken">调度取消令牌</param>
 | 
			
		||||
//    public void Start(CancellationToken cancellationToken)
 | 
			
		||||
//    {
 | 
			
		||||
//        try
 | 
			
		||||
//        {
 | 
			
		||||
//            WaitLock.Wait(cancellationToken);
 | 
			
		||||
 | 
			
		||||
            if (cancellationToken.CanBeCanceled)
 | 
			
		||||
            {
 | 
			
		||||
                _cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                _cancelTokenSource = new CancellationTokenSource();
 | 
			
		||||
            }
 | 
			
		||||
//            if (cancellationToken.CanBeCanceled)
 | 
			
		||||
//            {
 | 
			
		||||
//                _cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
 | 
			
		||||
//            }
 | 
			
		||||
//            else
 | 
			
		||||
//            {
 | 
			
		||||
//                _cancelTokenSource = new CancellationTokenSource();
 | 
			
		||||
//            }
 | 
			
		||||
 | 
			
		||||
            // 异步执行
 | 
			
		||||
            PrivateTask = Do();
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            WaitLock.Release();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
//            // 异步执行
 | 
			
		||||
//            PrivateTask = Do();
 | 
			
		||||
//        }
 | 
			
		||||
//        finally
 | 
			
		||||
//        {
 | 
			
		||||
//            WaitLock.Release();
 | 
			
		||||
//        }
 | 
			
		||||
//    }
 | 
			
		||||
 | 
			
		||||
    private async Task Do()
 | 
			
		||||
    {
 | 
			
		||||
        await Task.Yield();
 | 
			
		||||
        while (!_cancelTokenSource.IsCancellationRequested)
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (_cancelTokenSource.IsCancellationRequested)
 | 
			
		||||
                    return;
 | 
			
		||||
                await DoWork(_state, _cancelTokenSource.Token).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
            catch (OperationCanceledException)
 | 
			
		||||
            {
 | 
			
		||||
            }
 | 
			
		||||
            catch (ObjectDisposedException)
 | 
			
		||||
            {
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                Logger?.LogWarning(ex, "DoWork");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
//    private async Task Do()
 | 
			
		||||
//    {
 | 
			
		||||
//        await Task.Yield();
 | 
			
		||||
//        while (!_cancelTokenSource.IsCancellationRequested)
 | 
			
		||||
//        {
 | 
			
		||||
//            try
 | 
			
		||||
//            {
 | 
			
		||||
//                if (_cancelTokenSource.IsCancellationRequested)
 | 
			
		||||
//                    return;
 | 
			
		||||
//                await DoWork(_state, _cancelTokenSource.Token).ConfigureAwait(false);
 | 
			
		||||
//            }
 | 
			
		||||
//            catch (OperationCanceledException)
 | 
			
		||||
//            {
 | 
			
		||||
//            }
 | 
			
		||||
//            catch (ObjectDisposedException)
 | 
			
		||||
//            {
 | 
			
		||||
//            }
 | 
			
		||||
//            catch (Exception ex)
 | 
			
		||||
//            {
 | 
			
		||||
//                Logger?.LogWarning(ex, "DoWork");
 | 
			
		||||
//            }
 | 
			
		||||
//        }
 | 
			
		||||
//    }
 | 
			
		||||
 | 
			
		||||
    private WaitLock WaitLock = new();
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 停止操作
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public async Task StopAsync(TimeSpan? waitTime = null)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            await WaitLock.WaitAsync().ConfigureAwait(false);
 | 
			
		||||
//    private WaitLock WaitLock = new();
 | 
			
		||||
//    /// <summary>
 | 
			
		||||
//    /// 停止操作
 | 
			
		||||
//    /// </summary>
 | 
			
		||||
//    public async Task StopAsync(TimeSpan? waitTime = null)
 | 
			
		||||
//    {
 | 
			
		||||
//        try
 | 
			
		||||
//        {
 | 
			
		||||
//            await WaitLock.WaitAsync().ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                _cancelTokenSource?.Cancel();
 | 
			
		||||
                _cancelTokenSource?.Dispose();
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                Logger?.LogWarning(ex, "Cancel error");
 | 
			
		||||
            }
 | 
			
		||||
//            try
 | 
			
		||||
//            {
 | 
			
		||||
//                _cancelTokenSource?.Cancel();
 | 
			
		||||
//                _cancelTokenSource?.Dispose();
 | 
			
		||||
//            }
 | 
			
		||||
//            catch (Exception ex)
 | 
			
		||||
//            {
 | 
			
		||||
//                Logger?.LogWarning(ex, "Cancel error");
 | 
			
		||||
//            }
 | 
			
		||||
 | 
			
		||||
            if (PrivateTask != null)
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    if (TaskName != null)
 | 
			
		||||
                        Logger?.LogInformation($"{TaskName} Stoping");
 | 
			
		||||
                    if (waitTime != null)
 | 
			
		||||
                        await PrivateTask.WaitAsync(waitTime.Value).ConfigureAwait(false);
 | 
			
		||||
                    if (TaskName != null)
 | 
			
		||||
                        Logger?.LogInformation($"{TaskName} Stoped");
 | 
			
		||||
                }
 | 
			
		||||
                catch (ObjectDisposedException)
 | 
			
		||||
                {
 | 
			
		||||
                }
 | 
			
		||||
                catch (TimeoutException)
 | 
			
		||||
                {
 | 
			
		||||
                    if (TaskName != null)
 | 
			
		||||
                        Logger?.LogWarning($"{TaskName} Stop timeout, exiting wait block");
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception ex)
 | 
			
		||||
                {
 | 
			
		||||
                    if (TaskName != null)
 | 
			
		||||
                        Logger?.LogWarning(ex, $"{TaskName} Stop error");
 | 
			
		||||
                }
 | 
			
		||||
                PrivateTask = null;
 | 
			
		||||
//            if (PrivateTask != null)
 | 
			
		||||
//            {
 | 
			
		||||
//                try
 | 
			
		||||
//                {
 | 
			
		||||
//                    if (TaskName != null)
 | 
			
		||||
//                        Logger?.LogInformation($"{TaskName} Stoping");
 | 
			
		||||
//                    if (waitTime != null)
 | 
			
		||||
//                        await PrivateTask.WaitAsync(waitTime.Value).ConfigureAwait(false);
 | 
			
		||||
//                    if (TaskName != null)
 | 
			
		||||
//                        Logger?.LogInformation($"{TaskName} Stoped");
 | 
			
		||||
//                }
 | 
			
		||||
//                catch (ObjectDisposedException)
 | 
			
		||||
//                {
 | 
			
		||||
//                }
 | 
			
		||||
//                catch (TimeoutException)
 | 
			
		||||
//                {
 | 
			
		||||
//                    if (TaskName != null)
 | 
			
		||||
//                        Logger?.LogWarning($"{TaskName} Stop timeout, exiting wait block");
 | 
			
		||||
//                }
 | 
			
		||||
//                catch (Exception ex)
 | 
			
		||||
//                {
 | 
			
		||||
//                    if (TaskName != null)
 | 
			
		||||
//                        Logger?.LogWarning(ex, $"{TaskName} Stop error");
 | 
			
		||||
//                }
 | 
			
		||||
//                PrivateTask = null;
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            WaitLock.Release();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
//            }
 | 
			
		||||
//        }
 | 
			
		||||
//        finally
 | 
			
		||||
//        {
 | 
			
		||||
//            WaitLock.Release();
 | 
			
		||||
//        }
 | 
			
		||||
//    }
 | 
			
		||||
//}
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,7 @@
 | 
			
		||||
        void Start();
 | 
			
		||||
        void Stop();
 | 
			
		||||
        public Int32 Period { get; }
 | 
			
		||||
        bool Enable { get; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ public class ScheduledAsyncTask : DisposeBase, IScheduledTask, IScheduledIntInte
 | 
			
		||||
    private volatile int _isRunning = 0;
 | 
			
		||||
    private volatile int _pendingTriggers = 0;
 | 
			
		||||
    public Int32 Period => _timer?.Period ?? 0;
 | 
			
		||||
 | 
			
		||||
    public bool Enable => _timer?.Disposed != false ? false : true;
 | 
			
		||||
    public ScheduledAsyncTask(int interval, Func<object?, CancellationToken, Task> taskFunc, object? state, ILog log, CancellationToken token)
 | 
			
		||||
    {
 | 
			
		||||
        IntervalMS = interval;
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,7 @@ public class ScheduledSyncTask : DisposeBase, IScheduledTask, IScheduledIntInter
 | 
			
		||||
    private volatile int _isRunning = 0;
 | 
			
		||||
    private volatile int _pendingTriggers = 0;
 | 
			
		||||
    public Int32 Period => _timer?.Period ?? 0;
 | 
			
		||||
    public bool Enable => _timer?.Disposed != false ? false : true;
 | 
			
		||||
 | 
			
		||||
    public ScheduledSyncTask(int interval, Action<object?, CancellationToken> taskFunc, object? state, ILog log, CancellationToken token)
 | 
			
		||||
    {
 | 
			
		||||
 
 | 
			
		||||
@@ -13,21 +13,21 @@ namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
public class SmartTriggerScheduler
 | 
			
		||||
{
 | 
			
		||||
    private readonly object _lock = new();          // 锁对象,保证线程安全
 | 
			
		||||
    private readonly Func<Task> _action;                // 实际要执行的操作
 | 
			
		||||
    private readonly Func<CancellationToken, Task> _action;                // 实际要执行的操作
 | 
			
		||||
    private readonly TimeSpan _delay;               // 执行间隔(冷却时间)
 | 
			
		||||
 | 
			
		||||
    private bool _isRunning = false;                // 当前是否有调度任务在运行
 | 
			
		||||
    private bool _hasPending = false;               // 在等待期间是否有新的触发
 | 
			
		||||
 | 
			
		||||
    // 构造函数,传入要执行的方法和最小执行间隔
 | 
			
		||||
    public SmartTriggerScheduler(Func<Task> action, TimeSpan minimumInterval)
 | 
			
		||||
    public SmartTriggerScheduler(Func<CancellationToken, Task> action, TimeSpan minimumInterval)
 | 
			
		||||
    {
 | 
			
		||||
        _action = action ?? throw new ArgumentNullException(nameof(action));
 | 
			
		||||
        _delay = minimumInterval;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 外部调用的触发方法(高频调用的地方调用这个)
 | 
			
		||||
    public void Trigger()
 | 
			
		||||
    public void Trigger(CancellationToken cancellationToken = default)
 | 
			
		||||
    {
 | 
			
		||||
        lock (_lock)
 | 
			
		||||
        {
 | 
			
		||||
@@ -41,16 +41,16 @@ public class SmartTriggerScheduler
 | 
			
		||||
 | 
			
		||||
            // 否则启动执行任务
 | 
			
		||||
            _isRunning = true;
 | 
			
		||||
            _ = Task.Run(ExecuteLoop);
 | 
			
		||||
            _ = Task.Run(() => ExecuteLoop(cancellationToken), cancellationToken);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 实际执行动作的循环逻辑
 | 
			
		||||
    private async Task ExecuteLoop()
 | 
			
		||||
    private async Task ExecuteLoop(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        while (true)
 | 
			
		||||
        {
 | 
			
		||||
            Func<Task> actionToRun = null;
 | 
			
		||||
            Func<CancellationToken, Task> actionToRun = null;
 | 
			
		||||
 | 
			
		||||
            // 拷贝 _action,并清除等待标记
 | 
			
		||||
            lock (_lock)
 | 
			
		||||
@@ -60,10 +60,10 @@ public class SmartTriggerScheduler
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // 执行外部提供的方法
 | 
			
		||||
            await actionToRun().ConfigureAwait(false);
 | 
			
		||||
            await actionToRun(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            // 等待 delay 时间,进入冷却期
 | 
			
		||||
            await Task.Delay(_delay).ConfigureAwait(false);
 | 
			
		||||
            await Task.Delay(_delay, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            lock (_lock)
 | 
			
		||||
            {
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,7 @@ public class TaskSchedulerLoop
 | 
			
		||||
            task.Start();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool Stoped => Tasks.All(a => !a.Enable);
 | 
			
		||||
    public void Stop()
 | 
			
		||||
    {
 | 
			
		||||
        foreach (var task in Tasks)
 | 
			
		||||
 
 | 
			
		||||
@@ -102,7 +102,7 @@ public class ControlController : ControllerBase
 | 
			
		||||
    [DisplayName("重启全部线程")]
 | 
			
		||||
    public async Task RestartAllThread()
 | 
			
		||||
    {
 | 
			
		||||
        await GlobalData.ChannelRuntimeService.RestartChannelAsync(GlobalData.Channels.Values).ConfigureAwait(false);
 | 
			
		||||
        await GlobalData.ChannelRuntimeService.RestartChannelAsync(GlobalData.IdChannels.Values).ConfigureAwait(false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -181,7 +181,7 @@ public class ControlController : ControllerBase
 | 
			
		||||
    [DisplayName("删除通道")]
 | 
			
		||||
    public Task<bool> DeleteChannelAsync([FromBody] List<long> ids, bool restart)
 | 
			
		||||
    {
 | 
			
		||||
        if (ids == null || ids.Count == 0) ids = GlobalData.Channels.Keys.ToList();
 | 
			
		||||
        if (ids == null || ids.Count == 0) ids = GlobalData.IdChannels.Keys.ToList();
 | 
			
		||||
        return GlobalData.ChannelRuntimeService.DeleteChannelAsync(ids, restart, default);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -28,6 +28,17 @@ namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
[Authorize(AuthenticationSchemes = "Bearer")]
 | 
			
		||||
public class RuntimeInfoController : ControllerBase
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取冗余状态
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    [HttpGet("redundancyStatus")]
 | 
			
		||||
    [DisplayName("获取冗余状态")]
 | 
			
		||||
    public bool GetRedundancyStatus()
 | 
			
		||||
    {
 | 
			
		||||
        return GlobalData.StartCollectChannelEnable;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取通道信息
 | 
			
		||||
    /// </summary>
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -0,0 +1,117 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
//  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
 | 
			
		||||
//  此代码版权(除特别声明外的代码)归作者本人Diego所有
 | 
			
		||||
//  源代码使用协议遵循本仓库的开源协议及附加协议
 | 
			
		||||
//  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
 | 
			
		||||
//  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
 | 
			
		||||
//  使用文档:https://thingsgateway.cn/
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.Extension.Generic;
 | 
			
		||||
 | 
			
		||||
using TouchSocket.Core;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 业务插件
 | 
			
		||||
/// </summary>
 | 
			
		||||
public abstract class BusinessBaseWithCacheAlarm : BusinessBaseWithCache
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    protected override bool AlarmModelEnable => true;
 | 
			
		||||
 | 
			
		||||
    protected override bool DevModelEnable => false;
 | 
			
		||||
 | 
			
		||||
    protected override bool VarModelEnable => false;
 | 
			
		||||
 | 
			
		||||
    protected override ValueTask<OperResult> UpdateDevModel(IEnumerable<CacheDBItem<DeviceBasicData>> item, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        throw new NotImplementedException();
 | 
			
		||||
    }
 | 
			
		||||
    protected override ValueTask<OperResult> UpdateVarModel(IEnumerable<CacheDBItem<VariableBasicData>> item, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        throw new NotImplementedException();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected override ValueTask<OperResult> UpdateVarModels(IEnumerable<VariableBasicData> item, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        throw new NotImplementedException();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override async Task AfterVariablesChangedAsync(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        await base.AfterVariablesChangedAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        IdVariableRuntimes.Clear();
 | 
			
		||||
        IdVariableRuntimes.AddRange(GlobalData.ReadOnlyIdVariables.Where(a => a.Value.AlarmEnable));
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        var ids = IdVariableRuntimes.Select(b => b.Value.DeviceId).ToHashSet();
 | 
			
		||||
 | 
			
		||||
        CollectDevices = GlobalData.ReadOnlyIdDevices
 | 
			
		||||
                                .Where(a => IdVariableRuntimes.Select(b => b.Value.DeviceId).Contains(a.Value.Id))
 | 
			
		||||
                                .ToDictionary(a => a.Key, a => a.Value);
 | 
			
		||||
    }
 | 
			
		||||
    protected internal override async Task InitChannelAsync(IChannel? channel, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
        GlobalData.AlarmChangedEvent -= AlarmValueChange;
 | 
			
		||||
        GlobalData.ReadOnlyRealAlarmIdVariables?.ForEach(a =>
 | 
			
		||||
        {
 | 
			
		||||
            AlarmValueChange(a.Value);
 | 
			
		||||
        });
 | 
			
		||||
        GlobalData.AlarmChangedEvent += AlarmValueChange;
 | 
			
		||||
 | 
			
		||||
        await base.InitChannelAsync(channel, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
    }
 | 
			
		||||
    protected override void Dispose(bool disposing)
 | 
			
		||||
    {
 | 
			
		||||
        GlobalData.AlarmChangedEvent -= AlarmValueChange;
 | 
			
		||||
        base.Dispose(disposing);
 | 
			
		||||
    }
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 当报警值发生变化时触发此事件处理方法。该方法内部会检查是否需要进行报警上传,如果需要,则调用 <see cref="AlarmChange(AlarmVariable)"/> 方法。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="alarmVariable">报警变量</param>
 | 
			
		||||
    protected void AlarmValueChange(AlarmVariable alarmVariable)
 | 
			
		||||
    {
 | 
			
		||||
        if (CurrentDevice?.Pause != false)
 | 
			
		||||
            return;
 | 
			
		||||
        if (TaskSchedulerLoop?.Stoped == true) return;
 | 
			
		||||
 | 
			
		||||
        if (AlarmModelEnable) return;
 | 
			
		||||
        // 如果业务属性的缓存为间隔上传,则不执行后续操作
 | 
			
		||||
        //if (_businessPropertyWithCacheInterval?.IsInterval != true)
 | 
			
		||||
        {
 | 
			
		||||
            // 检查当前设备的变量是否包含此报警变量,如果包含,则触发报警变量的变化处理方法
 | 
			
		||||
            if (IdVariableRuntimes.ContainsKey(alarmVariable.Id))
 | 
			
		||||
                AlarmChange(alarmVariable);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 当报警状态变化时触发此方法。如果不需要进行报警上传,则可以忽略此方法。通常情况下,需要在此方法中执行 <see cref="BusinessBaseWithCache.AddQueueAlarmModel"/> 方法。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="alarmVariable">报警变量</param>
 | 
			
		||||
    protected virtual void AlarmChange(AlarmVariable alarmVariable)
 | 
			
		||||
    {
 | 
			
		||||
        // 在报警状态变化时执行的自定义逻辑
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override void PauseThread(bool pause)
 | 
			
		||||
    {
 | 
			
		||||
        lock (this)
 | 
			
		||||
        {
 | 
			
		||||
            var oldV = CurrentDevice.Pause;
 | 
			
		||||
            base.PauseThread(pause);
 | 
			
		||||
            if (!pause && oldV != pause)
 | 
			
		||||
            {
 | 
			
		||||
                GlobalData.ReadOnlyRealAlarmIdVariables?.ForEach(a =>
 | 
			
		||||
               {
 | 
			
		||||
                   AlarmChange(a.Value);
 | 
			
		||||
               });
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -1,263 +0,0 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
//  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
 | 
			
		||||
//  此代码版权(除特别声明外的代码)归作者本人Diego所有
 | 
			
		||||
//  源代码使用协议遵循本仓库的开源协议及附加协议
 | 
			
		||||
//  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
 | 
			
		||||
//  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
 | 
			
		||||
//  使用文档:https://thingsgateway.cn/
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.Extension;
 | 
			
		||||
using ThingsGateway.Foundation.Extension.Generic;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 业务插件,实现实体VarModel,AlarmModel缓存
 | 
			
		||||
/// </summary>
 | 
			
		||||
public abstract class BusinessBaseWithCacheAlarmModel<VarModel, DevModel, AlarmModel> : BusinessBaseWithCacheDeviceModel<VarModel, DevModel>
 | 
			
		||||
{
 | 
			
		||||
    protected ConcurrentQueue<CacheDBItem<AlarmModel>> _memoryAlarmModelQueue = new();
 | 
			
		||||
 | 
			
		||||
    private volatile bool LocalDBCacheAlarmModelInited;
 | 
			
		||||
    private CacheDB DBCacheAlarm;
 | 
			
		||||
    protected internal override Task InitChannelAsync(IChannel? channel, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        DBCacheAlarm = LocalDBCacheAlarmModel();
 | 
			
		||||
        return base.InitChannelAsync(channel, cancellationToken);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 入缓存
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="data"></param>
 | 
			
		||||
    protected virtual void AddCache(List<CacheDBItem<AlarmModel>> data)
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
        if (_businessPropertyWithCache.CacheEnable && data?.Count > 0)
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                LogMessage?.LogInformation($"Add {typeof(DevModel).Name} data to file cache, count {data.Count}");
 | 
			
		||||
                foreach (var item in data)
 | 
			
		||||
                {
 | 
			
		||||
                    item.Id = CommonUtils.GetSingleId();
 | 
			
		||||
                }
 | 
			
		||||
                var dir = CacheDBUtil.GetCacheFilePath(CurrentDevice.Name.ToString());
 | 
			
		||||
                var fileStart = CacheDBUtil.GetFileName($"{CurrentDevice.PluginName}_{typeof(AlarmModel).FullName}_{nameof(AlarmModel)}");
 | 
			
		||||
                var fullName = dir.CombinePathWithOs($"{fileStart}{CacheDBUtil.EX}");
 | 
			
		||||
 | 
			
		||||
                lock (fullName)
 | 
			
		||||
                {
 | 
			
		||||
                    bool s = false;
 | 
			
		||||
                    while (!s)
 | 
			
		||||
                    {
 | 
			
		||||
                        s = CacheDBUtil.DeleteCache(_businessPropertyWithCache.CacheFileMaxLength, fullName);
 | 
			
		||||
                    }
 | 
			
		||||
                    using var cache = LocalDBCacheAlarmModel();
 | 
			
		||||
                    cache.DBProvider.Fastest<CacheDBItem<AlarmModel>>().PageSize(50000).BulkCopy(data);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    using var cache = LocalDBCacheAlarmModel();
 | 
			
		||||
                    lock (cache.CacheDBOption.FileFullName)
 | 
			
		||||
                    {
 | 
			
		||||
                        cache.DBProvider.Fastest<CacheDBItem<AlarmModel>>().PageSize(50000).BulkCopy(data);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception ex)
 | 
			
		||||
                {
 | 
			
		||||
                    LogMessage?.LogWarning(ex, "Add cache fail");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 添加队列,超限后会入缓存
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="data"></param>
 | 
			
		||||
    protected virtual void AddQueueAlarmModel(CacheDBItem<AlarmModel> data)
 | 
			
		||||
    {
 | 
			
		||||
        if (_businessPropertyWithCache.CacheEnable)
 | 
			
		||||
        {
 | 
			
		||||
            //检测队列长度,超限存入缓存数据库
 | 
			
		||||
            if (_memoryAlarmModelQueue.Count > _businessPropertyWithCache.QueueMaxCount)
 | 
			
		||||
            {
 | 
			
		||||
                List<CacheDBItem<AlarmModel>> list = null;
 | 
			
		||||
                lock (_memoryAlarmModelQueue)
 | 
			
		||||
                {
 | 
			
		||||
                    if (_memoryAlarmModelQueue.Count > _businessPropertyWithCache.QueueMaxCount)
 | 
			
		||||
                    {
 | 
			
		||||
                        list = _memoryAlarmModelQueue.ToListWithDequeue();
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                AddCache(list);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if (_memoryAlarmModelQueue.Count > _businessPropertyWithCache.QueueMaxCount)
 | 
			
		||||
        {
 | 
			
		||||
            lock (_memoryAlarmModelQueue)
 | 
			
		||||
            {
 | 
			
		||||
                if (_memoryAlarmModelQueue.Count > _businessPropertyWithCache.QueueMaxCount)
 | 
			
		||||
                {
 | 
			
		||||
                    LogMessage?.LogWarning($"{typeof(AlarmModel).Name} Queue exceeds limit, clear old data. If it doesn't work as expected, increase [QueueMaxCount] or Enable cache");
 | 
			
		||||
                    _memoryAlarmModelQueue.Clear();
 | 
			
		||||
                    _memoryAlarmModelQueue.Enqueue(data);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            _memoryAlarmModelQueue.Enqueue(data);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取缓存对象,注意每次获取的对象可能不一样,如顺序操作,需固定引用
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    protected virtual CacheDB LocalDBCacheAlarmModel()
 | 
			
		||||
    {
 | 
			
		||||
        var cacheDb = CacheDBUtil.GetCache(typeof(CacheDBItem<AlarmModel>), CurrentDevice.Name.ToString(), $"{CurrentDevice.PluginName}_{typeof(AlarmModel).Name}");
 | 
			
		||||
 | 
			
		||||
        if (!LocalDBCacheAlarmModelInited)
 | 
			
		||||
        {
 | 
			
		||||
            cacheDb.InitDb();
 | 
			
		||||
            LocalDBCacheAlarmModelInited = true;
 | 
			
		||||
        }
 | 
			
		||||
        return cacheDb;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected override async Task Update(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        await UpdateVarModelMemory(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        await UpdateVarModelsMemory(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        await UpdateVarModelCache(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        await UpdateVarModelsCache(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        await UpdateDevModelMemory(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        await UpdateAlarmModelMemory(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        await UpdateDevModelCache(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        await UpdateAlarmModelCache(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 需实现上传到通道
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="item"></param>
 | 
			
		||||
    /// <param name="cancellationToken"></param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    protected abstract ValueTask<OperResult> UpdateAlarmModel(IEnumerable<CacheDBItem<AlarmModel>> item, CancellationToken cancellationToken);
 | 
			
		||||
 | 
			
		||||
    protected async Task UpdateAlarmModelCache(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        if (_businessPropertyWithCache.CacheEnable)
 | 
			
		||||
        {
 | 
			
		||||
            #region //成功上传时,补上传缓存数据
 | 
			
		||||
 | 
			
		||||
            if (IsConnected())
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    while (!cancellationToken.IsCancellationRequested)
 | 
			
		||||
                    {
 | 
			
		||||
                        //循环获取,固定读最大行数量,执行完成需删除行
 | 
			
		||||
                        var varList = await DBCacheAlarm.DBProvider.Queryable<CacheDBItem<AlarmModel>>().Take(_businessPropertyWithCache.SplitSize).ToListAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                        if (varList.Count > 0)
 | 
			
		||||
                        {
 | 
			
		||||
                            try
 | 
			
		||||
                            {
 | 
			
		||||
                                if (!cancellationToken.IsCancellationRequested)
 | 
			
		||||
                                {
 | 
			
		||||
                                    var result = await UpdateAlarmModel(varList, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                                    if (result.IsSuccess)
 | 
			
		||||
                                    {
 | 
			
		||||
                                        //删除缓存
 | 
			
		||||
                                        await DBCacheAlarm.DBProvider.Deleteable<CacheDBItem<AlarmModel>>(varList).ExecuteCommandAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                                    }
 | 
			
		||||
                                    else
 | 
			
		||||
                                        break;
 | 
			
		||||
                                }
 | 
			
		||||
                                else
 | 
			
		||||
                                {
 | 
			
		||||
                                    break;
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            catch (Exception ex)
 | 
			
		||||
                            {
 | 
			
		||||
                                if (success)
 | 
			
		||||
                                    LogMessage?.LogWarning(ex);
 | 
			
		||||
                                success = false;
 | 
			
		||||
                                break;
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        else
 | 
			
		||||
                        {
 | 
			
		||||
                            break;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception ex)
 | 
			
		||||
                {
 | 
			
		||||
                    if (success)
 | 
			
		||||
                        LogMessage?.LogWarning(ex);
 | 
			
		||||
                    success = false;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            #endregion //成功上传时,补上传缓存数据
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected async Task UpdateAlarmModelMemory(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        #region //上传设备内存队列中的数据
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var list = _memoryAlarmModelQueue.ToListWithDequeue();
 | 
			
		||||
            if (list?.Count > 0)
 | 
			
		||||
            {
 | 
			
		||||
                var data = list.ChunkBetter(_businessPropertyWithCache.SplitSize);
 | 
			
		||||
                foreach (var item in data)
 | 
			
		||||
                {
 | 
			
		||||
                    try
 | 
			
		||||
                    {
 | 
			
		||||
                        if (!cancellationToken.IsCancellationRequested)
 | 
			
		||||
                        {
 | 
			
		||||
                            var result = await UpdateAlarmModel(item, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                            if (!result.IsSuccess)
 | 
			
		||||
                            {
 | 
			
		||||
                                AddCache(item.ToList());
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        else
 | 
			
		||||
                        {
 | 
			
		||||
                            break;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    catch (Exception ex)
 | 
			
		||||
                    {
 | 
			
		||||
                        if (success)
 | 
			
		||||
                            LogMessage?.LogWarning(ex);
 | 
			
		||||
                        success = false;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            if (success)
 | 
			
		||||
                LogMessage?.LogWarning(ex);
 | 
			
		||||
            success = false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        #endregion //上传设备内存队列中的数据
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,264 +0,0 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
//  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
 | 
			
		||||
//  此代码版权(除特别声明外的代码)归作者本人Diego所有
 | 
			
		||||
//  源代码使用协议遵循本仓库的开源协议及附加协议
 | 
			
		||||
//  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
 | 
			
		||||
//  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
 | 
			
		||||
//  使用文档:https://thingsgateway.cn/
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.Extension;
 | 
			
		||||
using ThingsGateway.Foundation.Extension.Generic;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 业务插件,实现实体VarModel,DevModel缓存
 | 
			
		||||
/// </summary>
 | 
			
		||||
public abstract class BusinessBaseWithCacheDeviceModel<VarModel, DevModel> : BusinessBaseWithCacheVariableModel<VarModel>
 | 
			
		||||
{
 | 
			
		||||
    protected ConcurrentQueue<CacheDBItem<DevModel>> _memoryDevModelQueue = new();
 | 
			
		||||
 | 
			
		||||
    private volatile bool LocalDBCacheDevModelInited;
 | 
			
		||||
 | 
			
		||||
    private CacheDB DBCacheDev;
 | 
			
		||||
    protected internal override Task InitChannelAsync(IChannel? channel, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        DBCacheDev = LocalDBCacheDevModel();
 | 
			
		||||
        return base.InitChannelAsync(channel, cancellationToken);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 入缓存
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="data"></param>
 | 
			
		||||
    protected virtual void AddCache(List<CacheDBItem<DevModel>> data)
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
        if (_businessPropertyWithCache.CacheEnable && data?.Count > 0)
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                LogMessage?.LogInformation($"Add {typeof(DevModel).Name} data to file cache, count {data.Count}");
 | 
			
		||||
                foreach (var item in data)
 | 
			
		||||
                {
 | 
			
		||||
                    item.Id = CommonUtils.GetSingleId();
 | 
			
		||||
                }
 | 
			
		||||
                var dir = CacheDBUtil.GetCacheFilePath(CurrentDevice.Name.ToString());
 | 
			
		||||
                var fileStart = CacheDBUtil.GetFileName($"{CurrentDevice.PluginName}_{typeof(DevModel).FullName}_{nameof(DevModel)}");
 | 
			
		||||
                var fullName = dir.CombinePathWithOs($"{fileStart}{CacheDBUtil.EX}");
 | 
			
		||||
 | 
			
		||||
                lock (fullName)
 | 
			
		||||
                {
 | 
			
		||||
                    bool s = false;
 | 
			
		||||
                    while (!s)
 | 
			
		||||
                    {
 | 
			
		||||
                        s = CacheDBUtil.DeleteCache(_businessPropertyWithCache.CacheFileMaxLength, fullName);
 | 
			
		||||
                    }
 | 
			
		||||
                    using var cache = LocalDBCacheDevModel();
 | 
			
		||||
                    cache.DBProvider.Fastest<CacheDBItem<DevModel>>().PageSize(50000).BulkCopy(data);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    using var cache = LocalDBCacheDevModel();
 | 
			
		||||
                    lock (cache.CacheDBOption.FileFullName)
 | 
			
		||||
                    {
 | 
			
		||||
                        cache.DBProvider.Fastest<CacheDBItem<DevModel>>().PageSize(50000).BulkCopy(data);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception ex)
 | 
			
		||||
                {
 | 
			
		||||
                    LogMessage?.LogWarning(ex, "Add cache fail");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 添加队列,超限后会入缓存
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="data"></param>
 | 
			
		||||
    protected virtual void AddQueueDevModel(CacheDBItem<DevModel> data)
 | 
			
		||||
    {
 | 
			
		||||
        if (_businessPropertyWithCache.CacheEnable)
 | 
			
		||||
        {
 | 
			
		||||
            //检测队列长度,超限存入缓存数据库
 | 
			
		||||
            if (_memoryDevModelQueue.Count > _businessPropertyWithCache.QueueMaxCount)
 | 
			
		||||
            {
 | 
			
		||||
                List<CacheDBItem<DevModel>> list = null;
 | 
			
		||||
                lock (_memoryDevModelQueue)
 | 
			
		||||
                {
 | 
			
		||||
                    if (_memoryDevModelQueue.Count > _businessPropertyWithCache.QueueMaxCount)
 | 
			
		||||
                    {
 | 
			
		||||
                        list = _memoryDevModelQueue.ToListWithDequeue();
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                AddCache(list);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if (_memoryDevModelQueue.Count > _businessPropertyWithCache.QueueMaxCount)
 | 
			
		||||
        {
 | 
			
		||||
            lock (_memoryDevModelQueue)
 | 
			
		||||
            {
 | 
			
		||||
                if (_memoryDevModelQueue.Count > _businessPropertyWithCache.QueueMaxCount)
 | 
			
		||||
                {
 | 
			
		||||
                    LogMessage?.LogWarning($"{typeof(DevModel).Name} Queue exceeds limit, clear old data. If it doesn't work as expected, increase [QueueMaxCount] or Enable cache");
 | 
			
		||||
                    _memoryDevModelQueue.Clear();
 | 
			
		||||
                    _memoryDevModelQueue.Enqueue(data);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            _memoryDevModelQueue.Enqueue(data);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取缓存对象,注意每次获取的对象可能不一样,如顺序操作,需固定引用
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    protected virtual CacheDB LocalDBCacheDevModel()
 | 
			
		||||
    {
 | 
			
		||||
        var cacheDb = CacheDBUtil.GetCache(typeof(CacheDBItem<DevModel>), CurrentDevice.Name.ToString(), $"{CurrentDevice.PluginName}_{typeof(DevModel).Name}");
 | 
			
		||||
        if (!LocalDBCacheDevModelInited)
 | 
			
		||||
        {
 | 
			
		||||
            cacheDb.InitDb();
 | 
			
		||||
            LocalDBCacheDevModelInited = true;
 | 
			
		||||
        }
 | 
			
		||||
        return cacheDb;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected override async Task Update(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        await UpdateVarModelMemory(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        await UpdateVarModelsMemory(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        await UpdateVarModelCache(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        await UpdateVarModelsCache(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        await UpdateDevModelMemory(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        await UpdateDevModelCache(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 需实现上传到通道
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="item"></param>
 | 
			
		||||
    /// <param name="cancellationToken"></param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    protected abstract ValueTask<OperResult> UpdateDevModel(IEnumerable<CacheDBItem<DevModel>> item, CancellationToken cancellationToken);
 | 
			
		||||
 | 
			
		||||
    protected async Task UpdateDevModelCache(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        if (_businessPropertyWithCache.CacheEnable)
 | 
			
		||||
        {
 | 
			
		||||
            #region //成功上传时,补上传缓存数据
 | 
			
		||||
 | 
			
		||||
            if (IsConnected())
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    while (!cancellationToken.IsCancellationRequested)
 | 
			
		||||
                    {
 | 
			
		||||
 | 
			
		||||
                        //循环获取
 | 
			
		||||
                        var varList = await DBCacheDev.DBProvider.Queryable<CacheDBItem<DevModel>>().Take(_businessPropertyWithCache.SplitSize).ToListAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                        if (varList.Count > 0)
 | 
			
		||||
                        {
 | 
			
		||||
                            try
 | 
			
		||||
                            {
 | 
			
		||||
                                if (!cancellationToken.IsCancellationRequested)
 | 
			
		||||
                                {
 | 
			
		||||
                                    var result = await UpdateDevModel(varList, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                                    if (result.IsSuccess)
 | 
			
		||||
                                    {
 | 
			
		||||
                                        //删除缓存
 | 
			
		||||
                                        await DBCacheDev.DBProvider.Deleteable<CacheDBItem<DevModel>>(varList).ExecuteCommandAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                                    }
 | 
			
		||||
                                    else
 | 
			
		||||
                                        break;
 | 
			
		||||
                                }
 | 
			
		||||
                                else
 | 
			
		||||
                                {
 | 
			
		||||
                                    break;
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            catch (Exception ex)
 | 
			
		||||
                            {
 | 
			
		||||
                                if (success)
 | 
			
		||||
                                    LogMessage?.LogWarning(ex);
 | 
			
		||||
                                success = false;
 | 
			
		||||
                                break;
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        else
 | 
			
		||||
                        {
 | 
			
		||||
                            break;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception ex)
 | 
			
		||||
                {
 | 
			
		||||
                    if (success)
 | 
			
		||||
                        LogMessage?.LogWarning(ex);
 | 
			
		||||
                    success = false;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            #endregion //成功上传时,补上传缓存数据
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected async Task UpdateDevModelMemory(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        #region //上传设备内存队列中的数据
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var list = _memoryDevModelQueue.ToListWithDequeue();
 | 
			
		||||
            if (list?.Count > 0)
 | 
			
		||||
            {
 | 
			
		||||
                var data = list.ChunkBetter(_businessPropertyWithCache.SplitSize);
 | 
			
		||||
                foreach (var item in data)
 | 
			
		||||
                {
 | 
			
		||||
                    try
 | 
			
		||||
                    {
 | 
			
		||||
                        if (!cancellationToken.IsCancellationRequested)
 | 
			
		||||
                        {
 | 
			
		||||
                            var result = await UpdateDevModel(item, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                            if (!result.IsSuccess)
 | 
			
		||||
                            {
 | 
			
		||||
                                AddCache(item.ToList());
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        else
 | 
			
		||||
                        {
 | 
			
		||||
                            break;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    catch (Exception ex)
 | 
			
		||||
                    {
 | 
			
		||||
                        if (success)
 | 
			
		||||
                            LogMessage?.LogWarning(ex);
 | 
			
		||||
                        success = false;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            if (success)
 | 
			
		||||
                LogMessage?.LogWarning(ex);
 | 
			
		||||
            success = false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        #endregion //上传设备内存队列中的数据
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -17,77 +17,101 @@ namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 业务插件,额外实现变量、设备、变量间隔上传
 | 
			
		||||
/// </summary>
 | 
			
		||||
public abstract class BusinessBaseWithCacheIntervalAlarmModel<VarModel, DevModel, AlarmModel> : BusinessBaseWithCacheAlarmModel<VarModel, DevModel, AlarmModel>
 | 
			
		||||
public abstract class BusinessBaseWithCacheInterval : BusinessBaseWithCache
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 业务属性
 | 
			
		||||
    /// 获取具体业务属性的缓存设置。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    protected sealed override BusinessPropertyWithCache _businessPropertyWithCache => _businessPropertyWithCacheInterval;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 业务属性
 | 
			
		||||
    /// 获取具体业务属性的缓存间隔设置。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    protected abstract BusinessPropertyWithCacheInterval _businessPropertyWithCacheInterval { get; }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    protected internal override async Task InitChannelAsync(IChannel? channel, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        GlobalData.AlarmChangedEvent -= AlarmValueChange;
 | 
			
		||||
        GlobalData.ReadOnlyRealAlarmIdVariables?.ForEach(a =>
 | 
			
		||||
        if (AlarmModelEnable)
 | 
			
		||||
        {
 | 
			
		||||
            AlarmValueChange(a.Value);
 | 
			
		||||
        });
 | 
			
		||||
            GlobalData.AlarmChangedEvent -= AlarmValueChange;
 | 
			
		||||
            GlobalData.ReadOnlyRealAlarmIdVariables?.ForEach(a =>
 | 
			
		||||
            {
 | 
			
		||||
                AlarmValueChange(a.Value);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        GlobalData.AlarmChangedEvent += AlarmValueChange;
 | 
			
		||||
        // 解绑全局数据的事件
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        // 根据业务属性的缓存是否为间隔上传来决定事件绑定
 | 
			
		||||
        if (_businessPropertyWithCacheInterval.BusinessUpdateEnum != BusinessUpdateEnum.Interval)
 | 
			
		||||
            GlobalData.AlarmChangedEvent += AlarmValueChange;
 | 
			
		||||
            // 解绑全局数据的事件
 | 
			
		||||
        }
 | 
			
		||||
        if (DevModelEnable)
 | 
			
		||||
        {
 | 
			
		||||
            // 绑定全局数据的事件
 | 
			
		||||
            GlobalData.DeviceStatusChangeEvent += DeviceStatusChange;
 | 
			
		||||
            GlobalData.VariableValueChangeEvent += VariableValueChange;
 | 
			
		||||
 | 
			
		||||
            // 如果不是间隔上传,则订阅全局变量值改变事件和设备状态改变事件,并触发一次事件处理
 | 
			
		||||
            if (_businessPropertyWithCacheInterval.BusinessUpdateEnum != BusinessUpdateEnum.Interval)
 | 
			
		||||
            {
 | 
			
		||||
                GlobalData.DeviceStatusChangeEvent += DeviceStatusChange;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (VarModelEnable)
 | 
			
		||||
        {
 | 
			
		||||
            // 注册变量值变化事件处理程序
 | 
			
		||||
            if (_businessPropertyWithCacheInterval.BusinessUpdateEnum != BusinessUpdateEnum.Interval)
 | 
			
		||||
            {
 | 
			
		||||
                GlobalData.VariableValueChangeEvent += VariableValueChange;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await base.InitChannelAsync(channel, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
    }
 | 
			
		||||
    public override async Task AfterVariablesChangedAsync(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        // 如果业务属性指定了全部变量,则设置当前设备的变量运行时列表和采集设备列表
 | 
			
		||||
        if (_businessPropertyWithCacheInterval.IsAllVariable)
 | 
			
		||||
        if (AlarmModelEnable || DevModelEnable || VarModelEnable)
 | 
			
		||||
        {
 | 
			
		||||
            LogMessage?.LogInformation("Refresh variable");
 | 
			
		||||
            // 如果业务属性指定了全部变量,则设置当前设备的变量运行时列表和采集设备列表
 | 
			
		||||
            if (_businessPropertyWithCacheInterval.IsAllVariable)
 | 
			
		||||
            {
 | 
			
		||||
                LogMessage?.LogInformation("Refresh variable");
 | 
			
		||||
                IdVariableRuntimes.Clear();
 | 
			
		||||
                IdVariableRuntimes.AddRange(GlobalData.GetEnableVariables().ToDictionary(a => a.Id));
 | 
			
		||||
                CollectDevices = GlobalData.GetEnableDevices().Where(a => a.IsCollect == true).ToDictionary(a => a.Id);
 | 
			
		||||
 | 
			
		||||
            IdVariableRuntimes.Clear();
 | 
			
		||||
            IdVariableRuntimes.AddRange(GlobalData.GetEnableVariables().ToDictionary(a => a.Id));
 | 
			
		||||
                VariableRuntimeGroups = IdVariableRuntimes.GroupBy(a => a.Value.BusinessGroup ?? string.Empty).ToDictionary(a => a.Key, a => a.Select(a => a.Value).ToList());
 | 
			
		||||
 | 
			
		||||
            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());
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            await base.AfterVariablesChangedAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                await base.AfterVariablesChangedAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        // 触发一次设备状态变化和变量值变化事件
 | 
			
		||||
        CollectDevices?.ForEach(a =>
 | 
			
		||||
        if (DevModelEnable)
 | 
			
		||||
        {
 | 
			
		||||
            if (a.Value.DeviceStatus == DeviceStatusEnum.OnLine && _businessPropertyWithCacheInterval.BusinessUpdateEnum != BusinessUpdateEnum.Interval)
 | 
			
		||||
                DeviceStatusChange(a.Value, a.Value.AdaptDeviceBasicData());
 | 
			
		||||
        });
 | 
			
		||||
        IdVariableRuntimes.ForEach(a =>
 | 
			
		||||
 | 
			
		||||
            CollectDevices?.ForEach(a =>
 | 
			
		||||
            {
 | 
			
		||||
                if (a.Value.DeviceStatus == DeviceStatusEnum.OnLine && _businessPropertyWithCacheInterval.BusinessUpdateEnum != BusinessUpdateEnum.Interval)
 | 
			
		||||
                    DeviceStatusChange(a.Value, a.Value.AdaptDeviceBasicData());
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (VarModelEnable)
 | 
			
		||||
        {
 | 
			
		||||
            if (a.Value.IsOnline && _businessPropertyWithCacheInterval.BusinessUpdateEnum != BusinessUpdateEnum.Interval)
 | 
			
		||||
                VariableValueChange(a.Value, a.Value.AdaptVariableBasicData());
 | 
			
		||||
        });
 | 
			
		||||
            // 触发一次变量值变化事件
 | 
			
		||||
            IdVariableRuntimes.ForEach(a =>
 | 
			
		||||
            {
 | 
			
		||||
                if (a.Value.IsOnline && _businessPropertyWithCacheInterval.BusinessUpdateEnum != BusinessUpdateEnum.Interval)
 | 
			
		||||
                    VariableValueChange(a.Value, a.Value.AdaptVariableBasicData());
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 当报警状态变化时触发此方法。如果不需要进行报警上传,则可以忽略此方法。通常情况下,需要在此方法中执行 <see cref="BusinessBaseWithCacheAlarmModel{T,T2,T3}.AddQueueAlarmModel(CacheDBItem{T3})"/> 方法。
 | 
			
		||||
    /// 当报警状态变化时触发此方法。如果不需要进行报警上传,则可以忽略此方法。通常情况下,需要在此方法中执行 <see cref="BusinessBaseWithCache.AddQueueAlarmModel"/> 方法。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="alarmVariable">报警变量</param>
 | 
			
		||||
    protected virtual void AlarmChange(AlarmVariable alarmVariable)
 | 
			
		||||
@@ -96,7 +120,7 @@ public abstract class BusinessBaseWithCacheIntervalAlarmModel<VarModel, DevModel
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 当设备状态变化时触发此方法。如果不需要进行设备上传,则可以忽略此方法。通常情况下,需要在此方法中执行 <see cref="BusinessBaseWithCacheDeviceModel{T,T2}.AddQueueDevModel(CacheDBItem{T2})"/> 方法。
 | 
			
		||||
    /// 当设备状态变化时触发此方法。如果不需要进行设备上传,则可以忽略此方法。通常情况下,需要在此方法中执行 <see cref="BusinessBaseWithCache.AddQueueDevModel"/> 方法。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="deviceRuntime">设备运行时信息</param>
 | 
			
		||||
    /// <param name="deviceData">设备数据</param>
 | 
			
		||||
@@ -105,7 +129,7 @@ public abstract class BusinessBaseWithCacheIntervalAlarmModel<VarModel, DevModel
 | 
			
		||||
        // 在设备状态变化时执行的自定义逻辑
 | 
			
		||||
    }
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 当设备状态定时变化时触发此方法。如果不需要进行设备上传,则可以忽略此方法。通常情况下,需要在此方法中执行 <see cref="BusinessBaseWithCacheDeviceModel{T,T2}.AddQueueDevModel(CacheDBItem{T2})"/> 方法。
 | 
			
		||||
    /// 当设备状态定时变化时触发此方法。如果不需要进行设备上传,则可以忽略此方法。通常情况下,需要在此方法中执行 <see cref="BusinessBaseWithCache.AddQueueDevModel"/> 方法。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="deviceRuntime">设备运行时信息</param>
 | 
			
		||||
    /// <param name="deviceData">设备数据</param>
 | 
			
		||||
@@ -113,6 +137,9 @@ public abstract class BusinessBaseWithCacheIntervalAlarmModel<VarModel, DevModel
 | 
			
		||||
    {
 | 
			
		||||
        // 在设备状态变化时执行的自定义逻辑
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 释放资源方法
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -137,7 +164,7 @@ public abstract class BusinessBaseWithCacheIntervalAlarmModel<VarModel, DevModel
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    protected void IntervalInsert(object? state, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        if (CurrentDevice.Pause == true)
 | 
			
		||||
        if (CurrentDevice?.Pause != false)
 | 
			
		||||
        {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
@@ -145,35 +172,42 @@ public abstract class BusinessBaseWithCacheIntervalAlarmModel<VarModel, DevModel
 | 
			
		||||
        // 如果业务属性的缓存为间隔上传,则根据定时器间隔执行相应操作
 | 
			
		||||
        if (_businessPropertyWithCacheInterval.BusinessUpdateEnum != BusinessUpdateEnum.Change)
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
 | 
			
		||||
            if (VarModelEnable)
 | 
			
		||||
            {
 | 
			
		||||
                if (LogMessage?.LogLevel <= LogLevel.Debug)
 | 
			
		||||
                    LogMessage?.LogDebug($"Interval {typeof(VarModel).Name} data, count {IdVariableRuntimes.Count}");
 | 
			
		||||
                // 间隔推送全部变量
 | 
			
		||||
                var variableRuntimes = IdVariableRuntimes.Select(a => a.Value);
 | 
			
		||||
                VariableTimeInterval(variableRuntimes, variableRuntimes.AdaptIEnumerableVariableBasicData());
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                LogMessage?.LogWarning(ex, AppResource.IntervalInsertVariableFail);
 | 
			
		||||
            }
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (CollectDevices != null)
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    if (LogMessage?.LogLevel <= LogLevel.Debug)
 | 
			
		||||
                        LogMessage?.LogDebug($"Interval {typeof(DevModel).Name} data, count {CollectDevices.Count}");
 | 
			
		||||
 | 
			
		||||
                    // 间隔推送全部设备
 | 
			
		||||
                    foreach (var deviceRuntime in CollectDevices.Select(a => a.Value))
 | 
			
		||||
                    {
 | 
			
		||||
                        DeviceTimeInterval(deviceRuntime, deviceRuntime.AdaptDeviceBasicData());
 | 
			
		||||
                    }
 | 
			
		||||
                        LogMessage?.LogDebug($"Interval {typeof(VariableBasicData).Name} data, count {IdVariableRuntimes.Count}");
 | 
			
		||||
                    // 间隔推送全部变量
 | 
			
		||||
                    var variableRuntimes = IdVariableRuntimes.Select(a => a.Value);
 | 
			
		||||
                    VariableTimeInterval(variableRuntimes, variableRuntimes.AdaptIEnumerableVariableBasicData());
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception ex)
 | 
			
		||||
                {
 | 
			
		||||
                    LogMessage?.LogWarning(ex, AppResource.IntervalInsertVariableFail);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            if (DevModelEnable)
 | 
			
		||||
            {
 | 
			
		||||
                LogMessage?.LogWarning(ex, AppResource.IntervalInsertDeviceFail);
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    if (CollectDevices != null)
 | 
			
		||||
                    {
 | 
			
		||||
                        if (LogMessage?.LogLevel <= LogLevel.Debug)
 | 
			
		||||
                            LogMessage?.LogDebug($"Interval {typeof(DeviceBasicData).Name} data, count {CollectDevices.Count}");
 | 
			
		||||
 | 
			
		||||
                        // 间隔推送全部设备
 | 
			
		||||
                        foreach (var deviceRuntime in CollectDevices.Select(a => a.Value))
 | 
			
		||||
                        {
 | 
			
		||||
                            DeviceTimeInterval(deviceRuntime, deviceRuntime.AdaptDeviceBasicData());
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception ex)
 | 
			
		||||
                {
 | 
			
		||||
                    LogMessage?.LogWarning(ex, AppResource.IntervalInsertDeviceFail);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -187,7 +221,7 @@ public abstract class BusinessBaseWithCacheIntervalAlarmModel<VarModel, DevModel
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 当变量状态变化时触发此方法。如果不需要进行变量上传,则可以忽略此方法。通常情况下,需要在此方法中执行 <see cref="BusinessBaseWithCacheVariableModel{T}.AddQueueVarModel(CacheDBItem{T})"/> 方法。
 | 
			
		||||
    /// 当变量状态变化时触发此方法。如果不需要进行变量上传,则可以忽略此方法。通常情况下,需要在此方法中执行 <see cref="BusinessBaseWithCache.AddQueueVarModel(CacheDBItem{VariableBasicData})"/> 方法。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="variableRuntime">变量运行时信息</param>
 | 
			
		||||
    /// <param name="variable">变量数据</param>
 | 
			
		||||
@@ -195,8 +229,9 @@ public abstract class BusinessBaseWithCacheIntervalAlarmModel<VarModel, DevModel
 | 
			
		||||
    {
 | 
			
		||||
        // 在变量状态变化时执行的自定义逻辑
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 当变量定时变化时触发此方法。如果不需要进行变量上传,则可以忽略此方法。通常情况下,需要在此方法中执行 <see cref="BusinessBaseWithCacheVariableModel{T}.AddQueueVarModel(CacheDBItem{T})"/> 方法。
 | 
			
		||||
    /// 当变量定时变化时触发此方法。如果不需要进行变量上传,则可以忽略此方法。通常情况下,需要在此方法中执行 <see cref="BusinessBaseWithCache.AddQueueVarModel(CacheDBItem{VariableBasicData})"/> 方法。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="variableRuntimes">变量运行时信息</param>
 | 
			
		||||
    /// <param name="variables">变量数据</param>
 | 
			
		||||
@@ -204,14 +239,18 @@ public abstract class BusinessBaseWithCacheIntervalAlarmModel<VarModel, DevModel
 | 
			
		||||
    {
 | 
			
		||||
        // 在变量状态变化时执行的自定义逻辑
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 当报警值发生变化时触发此事件处理方法。该方法内部会检查是否需要进行报警上传,如果需要,则调用 <see cref="AlarmChange(AlarmVariable)"/> 方法。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="alarmVariable">报警变量</param>
 | 
			
		||||
    private void AlarmValueChange(AlarmVariable alarmVariable)
 | 
			
		||||
    protected void AlarmValueChange(AlarmVariable alarmVariable)
 | 
			
		||||
    {
 | 
			
		||||
        if (CurrentDevice.Pause)
 | 
			
		||||
        if (CurrentDevice?.Pause != false)
 | 
			
		||||
            return;
 | 
			
		||||
        if (TaskSchedulerLoop?.Stoped == true) return;
 | 
			
		||||
 | 
			
		||||
        if (!AlarmModelEnable) return;
 | 
			
		||||
        // 如果业务属性的缓存为间隔上传,则不执行后续操作
 | 
			
		||||
        //if (_businessPropertyWithCacheInterval?.IsInterval != true)
 | 
			
		||||
        {
 | 
			
		||||
@@ -220,6 +259,8 @@ public abstract class BusinessBaseWithCacheIntervalAlarmModel<VarModel, DevModel
 | 
			
		||||
                AlarmChange(alarmVariable);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public override void PauseThread(bool pause)
 | 
			
		||||
    {
 | 
			
		||||
        lock (this)
 | 
			
		||||
@@ -228,20 +269,29 @@ public abstract class BusinessBaseWithCacheIntervalAlarmModel<VarModel, DevModel
 | 
			
		||||
            base.PauseThread(pause);
 | 
			
		||||
            if (!pause && oldV != pause)
 | 
			
		||||
            {
 | 
			
		||||
                GlobalData.ReadOnlyRealAlarmIdVariables?.ForEach(a =>
 | 
			
		||||
                if (AlarmModelEnable)
 | 
			
		||||
                {
 | 
			
		||||
                    AlarmChange(a.Value);
 | 
			
		||||
                });
 | 
			
		||||
                CollectDevices?.ForEach(a =>
 | 
			
		||||
                    GlobalData.ReadOnlyRealAlarmIdVariables?.ForEach(a =>
 | 
			
		||||
                    {
 | 
			
		||||
                        AlarmChange(a.Value);
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
                if (DevModelEnable)
 | 
			
		||||
                {
 | 
			
		||||
                    CollectDevices?.ForEach(a =>
 | 
			
		||||
                {
 | 
			
		||||
                    if (a.Value.DeviceStatus == DeviceStatusEnum.OnLine && _businessPropertyWithCacheInterval.BusinessUpdateEnum != BusinessUpdateEnum.Interval)
 | 
			
		||||
                        DeviceStatusChange(a.Value, a.Value.AdaptDeviceBasicData());
 | 
			
		||||
                });
 | 
			
		||||
                IdVariableRuntimes.ForEach(a =>
 | 
			
		||||
                }
 | 
			
		||||
                if (VarModelEnable)
 | 
			
		||||
                {
 | 
			
		||||
                    IdVariableRuntimes.ForEach(a =>
 | 
			
		||||
                {
 | 
			
		||||
                    if (a.Value.IsOnline && _businessPropertyWithCacheInterval.BusinessUpdateEnum != BusinessUpdateEnum.Interval)
 | 
			
		||||
                        VariableValueChange(a.Value, a.Value.AdaptVariableBasicData());
 | 
			
		||||
                });
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@@ -251,10 +301,12 @@ public abstract class BusinessBaseWithCacheIntervalAlarmModel<VarModel, DevModel
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="deviceRuntime">设备运行时信息</param>
 | 
			
		||||
    /// <param name="deviceData">设备数据</param>
 | 
			
		||||
    private void DeviceStatusChange(DeviceRuntime deviceRuntime, DeviceBasicData deviceData)
 | 
			
		||||
    protected void DeviceStatusChange(DeviceRuntime deviceRuntime, DeviceBasicData deviceData)
 | 
			
		||||
    {
 | 
			
		||||
        if (CurrentDevice.Pause == true)
 | 
			
		||||
        if (CurrentDevice?.Pause != false)
 | 
			
		||||
            return;
 | 
			
		||||
        if (TaskSchedulerLoop?.Stoped == true) return;
 | 
			
		||||
        if (!DevModelEnable) return;
 | 
			
		||||
        // 如果业务属性的缓存为间隔上传,则不执行后续操作
 | 
			
		||||
        //if (_businessPropertyWithCacheInterval?.IsInterval != true)
 | 
			
		||||
        {
 | 
			
		||||
@@ -269,10 +321,13 @@ public abstract class BusinessBaseWithCacheIntervalAlarmModel<VarModel, DevModel
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="variableRuntime">变量运行时信息</param>
 | 
			
		||||
    /// <param name="variable">变量数据</param>
 | 
			
		||||
    private void VariableValueChange(VariableRuntime variableRuntime, VariableBasicData variable)
 | 
			
		||||
    protected void VariableValueChange(VariableRuntime variableRuntime, VariableBasicData variable)
 | 
			
		||||
    {
 | 
			
		||||
        if (CurrentDevice.Pause == true)
 | 
			
		||||
        if (CurrentDevice?.Pause != false)
 | 
			
		||||
            return;
 | 
			
		||||
        if (!VarModelEnable) return;
 | 
			
		||||
        if (TaskSchedulerLoop?.Stoped == true) return;
 | 
			
		||||
 | 
			
		||||
        // 如果业务属性的缓存为间隔上传,则不执行后续操作
 | 
			
		||||
        //if (_businessPropertyWithCacheInterval?.IsInterval != true)
 | 
			
		||||
        {
 | 
			
		||||
@@ -281,4 +336,6 @@ public abstract class BusinessBaseWithCacheIntervalAlarmModel<VarModel, DevModel
 | 
			
		||||
                VariableChange(variableRuntime, variable);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -21,7 +21,7 @@ namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 业务插件,额外实现脚本切换实体
 | 
			
		||||
/// </summary>
 | 
			
		||||
public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevModel, AlarmModel> : BusinessBaseWithCacheIntervalAlarmModel<VarModel, DevModel, AlarmModel>
 | 
			
		||||
public abstract partial class BusinessBaseWithCacheIntervalScript : BusinessBaseWithCacheInterval
 | 
			
		||||
{
 | 
			
		||||
    protected sealed override BusinessPropertyWithCacheInterval _businessPropertyWithCacheInterval => _businessPropertyWithCacheIntervalScript;
 | 
			
		||||
 | 
			
		||||
@@ -30,7 +30,7 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
    public virtual List<string> Match(string input)
 | 
			
		||||
    {
 | 
			
		||||
        // 生成缓存键,以确保缓存的唯一性
 | 
			
		||||
        var cacheKey = $"{nameof(BusinessBaseWithCacheIntervalScript<VarModel, DevModel, AlarmModel>)}-{CultureInfo.CurrentUICulture.Name}-Match-{input}";
 | 
			
		||||
        var cacheKey = $"{nameof(BusinessBaseWithCacheIntervalScript)}-{CultureInfo.CurrentUICulture.Name}-Match-{input}";
 | 
			
		||||
 | 
			
		||||
        // 尝试从缓存中获取匹配结果,如果缓存中不存在则创建新的匹配结果
 | 
			
		||||
        var strings = App.CacheService.GetOrAdd(cacheKey, entry =>
 | 
			
		||||
@@ -61,10 +61,9 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
 | 
			
		||||
    #region 封装方法
 | 
			
		||||
 | 
			
		||||
    protected List<TopicJson> GetAlarms(IEnumerable<AlarmModel> item)
 | 
			
		||||
    protected IEnumerable<TopicJson> GetAlarms(IEnumerable<AlarmVariable> item)
 | 
			
		||||
    {
 | 
			
		||||
        var data = Application.DynamicModelExtension.GetDynamicModel<AlarmModel>(item, _businessPropertyWithCacheIntervalScript.BigTextScriptAlarmModel);
 | 
			
		||||
        var topicJsonList = new List<TopicJson>();
 | 
			
		||||
        var data = Application.DynamicModelExtension.GetDynamicModel<AlarmVariable>(item, _businessPropertyWithCacheIntervalScript.BigTextScriptAlarmModel);
 | 
			
		||||
        var topics = Match(_businessPropertyWithCacheIntervalScript.AlarmTopic);
 | 
			
		||||
        if (topics.Count > 0)
 | 
			
		||||
        {
 | 
			
		||||
@@ -92,7 +91,7 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
                        var gList = group.Select(a => a).ToList();
 | 
			
		||||
                        string json = gList.ToSystemTextJsonString(_businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                        // 将主题和 JSON 内容添加到列表中
 | 
			
		||||
                        topicJsonList.Add(new(topic, json, gList.Count));
 | 
			
		||||
                        yield return new(topic, json, gList.Count);
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
@@ -101,7 +100,7 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
                        {
 | 
			
		||||
                            string json = SystemTextJsonExtension.ToSystemTextJsonString(gro, _businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                            // 将主题和 JSON 内容添加到列表中
 | 
			
		||||
                            topicJsonList.Add(new(topic, json, 1));
 | 
			
		||||
                            yield return new(topic, json, 1);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
@@ -113,26 +112,24 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
            {
 | 
			
		||||
                var gList = data.Select(a => a).ToList();
 | 
			
		||||
                string json = gList.ToSystemTextJsonString(_businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                topicJsonList.Add(new(_businessPropertyWithCacheIntervalScript.AlarmTopic, json, gList.Count));
 | 
			
		||||
                yield return new(_businessPropertyWithCacheIntervalScript.AlarmTopic, json, gList.Count);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                foreach (var group in data)
 | 
			
		||||
                {
 | 
			
		||||
                    string json = SystemTextJsonExtension.ToSystemTextJsonString(group, _businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                    topicJsonList.Add(new(_businessPropertyWithCacheIntervalScript.AlarmTopic, json, 1));
 | 
			
		||||
                    yield return new(_businessPropertyWithCacheIntervalScript.AlarmTopic, json, 1);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return topicJsonList;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    protected List<TopicJson> GetDeviceData(IEnumerable<DevModel> item)
 | 
			
		||||
    protected IEnumerable<TopicJson> GetDeviceData(IEnumerable<DeviceBasicData> item)
 | 
			
		||||
    {
 | 
			
		||||
        var data = Application.DynamicModelExtension.GetDynamicModel<DevModel>(item, _businessPropertyWithCacheIntervalScript.BigTextScriptDeviceModel);
 | 
			
		||||
        var topicJsonList = new List<TopicJson>();
 | 
			
		||||
        var data = Application.DynamicModelExtension.GetDynamicModel<DeviceBasicData>(item, _businessPropertyWithCacheIntervalScript.BigTextScriptDeviceModel);
 | 
			
		||||
        var topics = Match(_businessPropertyWithCacheIntervalScript.DeviceTopic);
 | 
			
		||||
        if (topics.Count > 0)
 | 
			
		||||
        {
 | 
			
		||||
@@ -160,7 +157,7 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
                            var gList = group.Select(a => a).ToList();
 | 
			
		||||
                            string json = gList.ToSystemTextJsonString(_businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                            // 将主题和 JSON 内容添加到列表中
 | 
			
		||||
                            topicJsonList.Add(new(topic, json, gList.Count));
 | 
			
		||||
                            yield return new(topic, json, gList.Count);
 | 
			
		||||
                        }
 | 
			
		||||
                        else
 | 
			
		||||
                        {
 | 
			
		||||
@@ -169,7 +166,7 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
                            {
 | 
			
		||||
                                string json = SystemTextJsonExtension.ToSystemTextJsonString(gro, _businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                                // 将主题和 JSON 内容添加到列表中
 | 
			
		||||
                                topicJsonList.Add(new(topic, json, 1));
 | 
			
		||||
                                yield return new(topic, json, 1);
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
@@ -182,24 +179,22 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
            {
 | 
			
		||||
                var gList = data.Select(a => a).ToList();
 | 
			
		||||
                string json = gList.ToSystemTextJsonString(_businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                topicJsonList.Add(new(_businessPropertyWithCacheIntervalScript.DeviceTopic, json, gList.Count));
 | 
			
		||||
                yield return new(_businessPropertyWithCacheIntervalScript.DeviceTopic, json, gList.Count);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                foreach (var group in data)
 | 
			
		||||
                {
 | 
			
		||||
                    string json = SystemTextJsonExtension.ToSystemTextJsonString(group, _businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                    topicJsonList.Add(new(_businessPropertyWithCacheIntervalScript.DeviceTopic, json, 1));
 | 
			
		||||
                    yield return new(_businessPropertyWithCacheIntervalScript.DeviceTopic, json, 1);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return topicJsonList;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected List<TopicJson> GetVariable(IEnumerable<VarModel> item)
 | 
			
		||||
    protected IEnumerable<TopicJson> GetVariableScript(IEnumerable<VariableBasicData> item)
 | 
			
		||||
    {
 | 
			
		||||
        var data = Application.DynamicModelExtension.GetDynamicModel<VarModel>(item, _businessPropertyWithCacheIntervalScript.BigTextScriptVariableModel);
 | 
			
		||||
        var topicJsonList = new List<TopicJson>();
 | 
			
		||||
        var data = Application.DynamicModelExtension.GetDynamicModel<VariableBasicData>(item, _businessPropertyWithCacheIntervalScript.BigTextScriptVariableModel);
 | 
			
		||||
        var topics = Match(_businessPropertyWithCacheIntervalScript.VariableTopic);
 | 
			
		||||
        if (topics.Count > 0)
 | 
			
		||||
        {
 | 
			
		||||
@@ -227,7 +222,7 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
                            var gList = group.Select(a => a).ToList();
 | 
			
		||||
                            string json = gList.ToSystemTextJsonString(_businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                            // 将主题和 JSON 内容添加到列表中
 | 
			
		||||
                            topicJsonList.Add(new(topic, json, gList.Count));
 | 
			
		||||
                            yield return new(topic, json, gList.Count);
 | 
			
		||||
                        }
 | 
			
		||||
                        else
 | 
			
		||||
                        {
 | 
			
		||||
@@ -236,7 +231,7 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
                            {
 | 
			
		||||
                                string json = SystemTextJsonExtension.ToSystemTextJsonString(gro, _businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                                // 将主题和 JSON 内容添加到列表中
 | 
			
		||||
                                topicJsonList.Add(new(topic, json, 1));
 | 
			
		||||
                                yield return new(topic, json, 1);
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
@@ -249,33 +244,33 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
            {
 | 
			
		||||
                var gList = data.Select(a => a).ToList();
 | 
			
		||||
                string json = gList.ToSystemTextJsonString(_businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                topicJsonList.Add(new(_businessPropertyWithCacheIntervalScript.VariableTopic, json, gList.Count));
 | 
			
		||||
                yield return new(_businessPropertyWithCacheIntervalScript.VariableTopic, json, gList.Count);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                foreach (var group in data)
 | 
			
		||||
                {
 | 
			
		||||
                    string json = SystemTextJsonExtension.ToSystemTextJsonString(group, _businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                    topicJsonList.Add(new(_businessPropertyWithCacheIntervalScript.VariableTopic, json, 1));
 | 
			
		||||
                    yield return new(_businessPropertyWithCacheIntervalScript.VariableTopic, json, 1);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return topicJsonList;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    protected List<TopicJson> GetVariableBasicData(IEnumerable<VariableBasicData> item)
 | 
			
		||||
    protected IEnumerable<TopicJson> GetVariableBasicData(IEnumerable<VariableBasicData> item)
 | 
			
		||||
    {
 | 
			
		||||
        IEnumerable<VariableBasicData>? data = null;
 | 
			
		||||
        if (!_businessPropertyWithCacheIntervalScript.BigTextScriptVariableModel.IsNullOrWhiteSpace())
 | 
			
		||||
        {
 | 
			
		||||
            return GetVariable(item.Cast<VarModel>());
 | 
			
		||||
            foreach (var v in GetVariableScript(item))
 | 
			
		||||
                yield return v;
 | 
			
		||||
 | 
			
		||||
            yield break;  // 提前结束
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            data = item;
 | 
			
		||||
        }
 | 
			
		||||
        var topicJsonList = new List<TopicJson>();
 | 
			
		||||
        var topics = Match(_businessPropertyWithCacheIntervalScript.VariableTopic);
 | 
			
		||||
        if (topics.Count > 0)
 | 
			
		||||
        {
 | 
			
		||||
@@ -303,7 +298,7 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
                            // 如果是变量列表,则将整个分组转换为 JSON 字符串
 | 
			
		||||
                            string json = group.Select(a => a).GroupBy(a => a.DeviceName, b => b).ToDictionary(a => a.Key, b => b.ToList()).ToSystemTextJsonString(_businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                            // 将主题和 JSON 内容添加到列表中
 | 
			
		||||
                            topicJsonList.Add(new(topic, json, group.Count()));
 | 
			
		||||
                            yield return new(topic, json, group.Count());
 | 
			
		||||
                        }
 | 
			
		||||
                        else
 | 
			
		||||
                        {
 | 
			
		||||
@@ -312,7 +307,7 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
                            {
 | 
			
		||||
                                string json = SystemTextJsonExtension.ToSystemTextJsonString(gro, _businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                                // 将主题和 JSON 内容添加到列表中
 | 
			
		||||
                                topicJsonList.Add(new(topic, json, 1));
 | 
			
		||||
                                yield return new(topic, json, 1);
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
@@ -324,41 +319,22 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
            if (_businessPropertyWithCacheIntervalScript.IsVariableList)
 | 
			
		||||
            {
 | 
			
		||||
                string json = data.Select(a => a).GroupBy(a => a.DeviceName, b => b).ToDictionary(a => a.Key, b => b.ToList()).ToSystemTextJsonString(_businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                topicJsonList.Add(new(_businessPropertyWithCacheIntervalScript.VariableTopic, json, data.Count()));
 | 
			
		||||
                yield return new(_businessPropertyWithCacheIntervalScript.VariableTopic, json, data.Count());
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                foreach (var group in data)
 | 
			
		||||
                {
 | 
			
		||||
                    string json = SystemTextJsonExtension.ToSystemTextJsonString(group, _businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                    topicJsonList.Add(new(_businessPropertyWithCacheIntervalScript.VariableTopic, json, 1));
 | 
			
		||||
                    yield return new(_businessPropertyWithCacheIntervalScript.VariableTopic, json, 1);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return topicJsonList;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    //protected static byte[] Serialize(object value)
 | 
			
		||||
    //{
 | 
			
		||||
    //    var block = new ValueByteBlock(1024 * 64);
 | 
			
		||||
    //    try
 | 
			
		||||
    //    {
 | 
			
		||||
    //        //将数据序列化到内存块
 | 
			
		||||
    //        FastBinaryFormatter.Serialize(ref block, value);
 | 
			
		||||
    //        block.SeekToStart();
 | 
			
		||||
    //        return block.Memory.GetArray().Array;
 | 
			
		||||
    //    }
 | 
			
		||||
    //    finally
 | 
			
		||||
    //    {
 | 
			
		||||
    //        block.Dispose();
 | 
			
		||||
    //    }
 | 
			
		||||
    //}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    protected List<TopicArray> GetAlarmTopicArrays(IEnumerable<AlarmModel> item)
 | 
			
		||||
    protected IEnumerable<TopicArray> GetAlarmTopicArrays(IEnumerable<AlarmVariable> item)
 | 
			
		||||
    {
 | 
			
		||||
        var data = Application.DynamicModelExtension.GetDynamicModel<AlarmModel>(item, _businessPropertyWithCacheIntervalScript.BigTextScriptAlarmModel);
 | 
			
		||||
        List<TopicArray> topicArrayList = new List<TopicArray>();
 | 
			
		||||
        var data = Application.DynamicModelExtension.GetDynamicModel<AlarmVariable>(item, _businessPropertyWithCacheIntervalScript.BigTextScriptAlarmModel);
 | 
			
		||||
        var topics = Match(_businessPropertyWithCacheIntervalScript.AlarmTopic);
 | 
			
		||||
        if (topics.Count > 0)
 | 
			
		||||
        {
 | 
			
		||||
@@ -384,7 +360,7 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
                        var gList = group.Select(a => a).ToList();
 | 
			
		||||
                        var json = gList.ToSystemTextJsonUtf8Bytes(_businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                        // 将主题和 JSON 内容添加到列表中
 | 
			
		||||
                        topicArrayList.Add(new(topic, json, gList.Count));
 | 
			
		||||
                        yield return new(topic, json, gList.Count);
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
@@ -393,7 +369,7 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
                        {
 | 
			
		||||
                            var json = gro.ToSystemTextJsonUtf8Bytes(_businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                            // 将主题和 JSON 内容添加到列表中
 | 
			
		||||
                            topicArrayList.Add(new(topic, json, 1));
 | 
			
		||||
                            yield return new(topic, json, 1);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
@@ -405,25 +381,23 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
            {
 | 
			
		||||
                var gList = data.Select(a => a).ToList();
 | 
			
		||||
                var json = gList.ToSystemTextJsonUtf8Bytes(_businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                topicArrayList.Add(new(_businessPropertyWithCacheIntervalScript.AlarmTopic, json, gList.Count));
 | 
			
		||||
                yield return new(_businessPropertyWithCacheIntervalScript.AlarmTopic, json, gList.Count);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                foreach (var group in data)
 | 
			
		||||
                {
 | 
			
		||||
                    var json = group.ToSystemTextJsonUtf8Bytes(_businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                    topicArrayList.Add(new(_businessPropertyWithCacheIntervalScript.AlarmTopic, json, 1));
 | 
			
		||||
                    yield return new(_businessPropertyWithCacheIntervalScript.AlarmTopic, json, 1);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return topicArrayList;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected List<TopicArray> GetDeviceTopicArray(IEnumerable<DevModel> item)
 | 
			
		||||
    protected IEnumerable<TopicArray> GetDeviceTopicArray(IEnumerable<DeviceBasicData> item)
 | 
			
		||||
    {
 | 
			
		||||
        var data = Application.DynamicModelExtension.GetDynamicModel<DevModel>(item, _businessPropertyWithCacheIntervalScript.BigTextScriptDeviceModel);
 | 
			
		||||
        List<TopicArray> topicArrayList = new List<TopicArray>();
 | 
			
		||||
        var data = Application.DynamicModelExtension.GetDynamicModel<DeviceBasicData>(item, _businessPropertyWithCacheIntervalScript.BigTextScriptDeviceModel);
 | 
			
		||||
        var topics = Match(_businessPropertyWithCacheIntervalScript.DeviceTopic);
 | 
			
		||||
        if (topics.Count > 0)
 | 
			
		||||
        {
 | 
			
		||||
@@ -451,7 +425,7 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
                            var gList = group.Select(a => a).ToList();
 | 
			
		||||
                            var json = gList.ToSystemTextJsonUtf8Bytes(_businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                            // 将主题和 JSON 内容添加到列表中
 | 
			
		||||
                            topicArrayList.Add(new(topic, json, gList.Count));
 | 
			
		||||
                            yield return new(topic, json, gList.Count);
 | 
			
		||||
                        }
 | 
			
		||||
                        else
 | 
			
		||||
                        {
 | 
			
		||||
@@ -460,7 +434,7 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
                            {
 | 
			
		||||
                                var json = gro.ToSystemTextJsonUtf8Bytes(_businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                                // 将主题和 JSON 内容添加到列表中
 | 
			
		||||
                                topicArrayList.Add(new(topic, json, 1));
 | 
			
		||||
                                yield return new(topic, json, 1);
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
@@ -473,24 +447,22 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
            {
 | 
			
		||||
                var gList = data.Select(a => a).ToList();
 | 
			
		||||
                var json = gList.ToSystemTextJsonUtf8Bytes(_businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                topicArrayList.Add(new(_businessPropertyWithCacheIntervalScript.DeviceTopic, json, gList.Count));
 | 
			
		||||
                yield return new(_businessPropertyWithCacheIntervalScript.DeviceTopic, json, gList.Count);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                foreach (var group in data)
 | 
			
		||||
                {
 | 
			
		||||
                    var json = group.ToSystemTextJsonUtf8Bytes(_businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                    topicArrayList.Add(new(_businessPropertyWithCacheIntervalScript.DeviceTopic, json, 1));
 | 
			
		||||
                    yield return new(_businessPropertyWithCacheIntervalScript.DeviceTopic, json, 1);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return topicArrayList;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected List<TopicArray> GetVariableTopicArray(IEnumerable<VarModel> item)
 | 
			
		||||
    protected IEnumerable<TopicArray> GetVariableScriptTopicArray(IEnumerable<VariableBasicData> item)
 | 
			
		||||
    {
 | 
			
		||||
        var data = Application.DynamicModelExtension.GetDynamicModel<VarModel>(item, _businessPropertyWithCacheIntervalScript.BigTextScriptVariableModel);
 | 
			
		||||
        List<TopicArray> topicArrayList = new List<TopicArray>();
 | 
			
		||||
        var data = Application.DynamicModelExtension.GetDynamicModel<VariableBasicData>(item, _businessPropertyWithCacheIntervalScript.BigTextScriptVariableModel);
 | 
			
		||||
        var topics = Match(_businessPropertyWithCacheIntervalScript.VariableTopic);
 | 
			
		||||
        if (topics.Count > 0)
 | 
			
		||||
        {
 | 
			
		||||
@@ -518,7 +490,7 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
                            var gList = group.Select(a => a).ToList();
 | 
			
		||||
                            var json = gList.ToSystemTextJsonUtf8Bytes(_businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                            // 将主题和 JSON 内容添加到列表中
 | 
			
		||||
                            topicArrayList.Add(new(topic, json, gList.Count));
 | 
			
		||||
                            yield return new(topic, json, gList.Count);
 | 
			
		||||
                        }
 | 
			
		||||
                        else
 | 
			
		||||
                        {
 | 
			
		||||
@@ -527,7 +499,7 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
                            {
 | 
			
		||||
                                var json = gro.ToSystemTextJsonUtf8Bytes(_businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                                // 将主题和 JSON 内容添加到列表中
 | 
			
		||||
                                topicArrayList.Add(new(topic, json, 1));
 | 
			
		||||
                                yield return new(topic, json, 1);
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
@@ -540,33 +512,34 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
            {
 | 
			
		||||
                var gList = data.Select(a => a).ToList();
 | 
			
		||||
                var json = gList.ToSystemTextJsonUtf8Bytes(_businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                topicArrayList.Add(new(_businessPropertyWithCacheIntervalScript.VariableTopic, json, gList.Count));
 | 
			
		||||
                yield return new(_businessPropertyWithCacheIntervalScript.VariableTopic, json, gList.Count);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                foreach (var group in data)
 | 
			
		||||
                {
 | 
			
		||||
                    var json = group.ToSystemTextJsonUtf8Bytes(_businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                    topicArrayList.Add(new(_businessPropertyWithCacheIntervalScript.VariableTopic, json, 1));
 | 
			
		||||
                    yield return new(_businessPropertyWithCacheIntervalScript.VariableTopic, json, 1);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return topicArrayList;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected List<TopicArray> GetVariableBasicDataTopicArray(IEnumerable<VariableBasicData> item)
 | 
			
		||||
    protected IEnumerable<TopicArray> GetVariableBasicDataTopicArray(IEnumerable<VariableBasicData> item)
 | 
			
		||||
    {
 | 
			
		||||
        IEnumerable<VariableBasicData>? data = null;
 | 
			
		||||
        if (!_businessPropertyWithCacheIntervalScript.BigTextScriptVariableModel.IsNullOrWhiteSpace())
 | 
			
		||||
        {
 | 
			
		||||
            return GetVariableTopicArray(item.Cast<VarModel>());
 | 
			
		||||
            foreach (var v in GetVariableScriptTopicArray(item))
 | 
			
		||||
                yield return v;
 | 
			
		||||
 | 
			
		||||
            yield break;  // 提前结束
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            data = item;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        List<TopicArray> topicArrayList = new List<TopicArray>();
 | 
			
		||||
        var topics = Match(_businessPropertyWithCacheIntervalScript.VariableTopic);
 | 
			
		||||
        if (topics.Count > 0)
 | 
			
		||||
        {
 | 
			
		||||
@@ -594,7 +567,7 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
                            var gList = group.Select(a => a).ToList();
 | 
			
		||||
                            var json = gList.ToSystemTextJsonUtf8Bytes(_businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                            // 将主题和 JSON 内容添加到列表中
 | 
			
		||||
                            topicArrayList.Add(new(topic, json, gList.Count));
 | 
			
		||||
                            yield return new(topic, json, gList.Count);
 | 
			
		||||
                        }
 | 
			
		||||
                        else
 | 
			
		||||
                        {
 | 
			
		||||
@@ -603,7 +576,7 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
                            {
 | 
			
		||||
                                var json = gro.ToSystemTextJsonUtf8Bytes(_businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                                // 将主题和 JSON 内容添加到列表中
 | 
			
		||||
                                topicArrayList.Add(new(topic, json, 1));
 | 
			
		||||
                                yield return new(topic, json, 1);
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
@@ -616,18 +589,17 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
            {
 | 
			
		||||
                var gList = data.Select(a => a).ToList();
 | 
			
		||||
                var json = gList.ToSystemTextJsonUtf8Bytes(_businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                topicArrayList.Add(new(_businessPropertyWithCacheIntervalScript.VariableTopic, json, gList.Count));
 | 
			
		||||
                yield return new(_businessPropertyWithCacheIntervalScript.VariableTopic, json, gList.Count);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                foreach (var group in data)
 | 
			
		||||
                {
 | 
			
		||||
                    var json = group.ToSystemTextJsonUtf8Bytes(_businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                    topicArrayList.Add(new(_businessPropertyWithCacheIntervalScript.VariableTopic, json, 1));
 | 
			
		||||
                    yield return new(_businessPropertyWithCacheIntervalScript.VariableTopic, json, 1);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return topicArrayList;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -8,12 +8,16 @@
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Plugin.Synchronization;
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// <inheritdoc/>
 | 
			
		||||
/// 业务插件,额外实现脚本切换实体
 | 
			
		||||
/// </summary>
 | 
			
		||||
public class SynchronizationVariableProperty : VariablePropertyBase
 | 
			
		||||
public abstract partial class BusinessBaseWithCacheIntervalScriptAll : BusinessBaseWithCacheIntervalScript
 | 
			
		||||
{
 | 
			
		||||
    public override bool Enable { get; set; } = true;
 | 
			
		||||
}
 | 
			
		||||
    protected override bool AlarmModelEnable => true;
 | 
			
		||||
 | 
			
		||||
    protected override bool DevModelEnable => true;
 | 
			
		||||
 | 
			
		||||
    protected override bool VarModelEnable => true;
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,36 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
//  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
 | 
			
		||||
//  此代码版权(除特别声明外的代码)归作者本人Diego所有
 | 
			
		||||
//  源代码使用协议遵循本仓库的开源协议及附加协议
 | 
			
		||||
//  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
 | 
			
		||||
//  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
 | 
			
		||||
//  使用文档:https://thingsgateway.cn/
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 业务插件
 | 
			
		||||
/// </summary>
 | 
			
		||||
public abstract class BusinessBaseWithCacheIntervalVariable : BusinessBaseWithCacheInterval
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    protected override bool AlarmModelEnable => false;
 | 
			
		||||
 | 
			
		||||
    protected override bool DevModelEnable => false;
 | 
			
		||||
 | 
			
		||||
    protected override bool VarModelEnable => true;
 | 
			
		||||
 | 
			
		||||
    protected override ValueTask<OperResult> UpdateDevModel(IEnumerable<CacheDBItem<DeviceBasicData>> item, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        throw new NotImplementedException();
 | 
			
		||||
    }
 | 
			
		||||
    protected override ValueTask<OperResult> UpdateAlarmModel(IEnumerable<CacheDBItem<AlarmVariable>> item, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        throw new NotImplementedException();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -1,483 +0,0 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
//  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
 | 
			
		||||
//  此代码版权(除特别声明外的代码)归作者本人Diego所有
 | 
			
		||||
//  源代码使用协议遵循本仓库的开源协议及附加协议
 | 
			
		||||
//  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
 | 
			
		||||
//  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
 | 
			
		||||
//  使用文档:https://thingsgateway.cn/
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.Extension;
 | 
			
		||||
using ThingsGateway.Foundation.Extension.Generic;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 业务插件,实现实体VarModel缓存
 | 
			
		||||
/// </summary>
 | 
			
		||||
public abstract class BusinessBaseWithCacheVariableModel<VarModel> : BusinessBase
 | 
			
		||||
{
 | 
			
		||||
    protected ConcurrentQueue<CacheDBItem<VarModel>> _memoryVarModelQueue = new();
 | 
			
		||||
    protected ConcurrentQueue<CacheDBItem<List<VarModel>>> _memoryVarModelsQueue = new();
 | 
			
		||||
    protected volatile bool success = true;
 | 
			
		||||
    private volatile bool LocalDBCacheVarModelInited;
 | 
			
		||||
    private volatile bool LocalDBCacheVarModelsInited;
 | 
			
		||||
    private CacheDB DBCacheVar;
 | 
			
		||||
    private CacheDB DBCacheVars;
 | 
			
		||||
    protected internal override Task InitChannelAsync(IChannel? channel, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        DBCacheVar = LocalDBCacheVarModel();
 | 
			
		||||
        DBCacheVars = LocalDBCacheVarModels();
 | 
			
		||||
        return base.InitChannelAsync(channel, cancellationToken);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    protected sealed override BusinessPropertyBase _businessPropertyBase => _businessPropertyWithCache;
 | 
			
		||||
 | 
			
		||||
    protected abstract BusinessPropertyWithCache _businessPropertyWithCache { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 入缓存
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="data"></param>
 | 
			
		||||
    protected virtual void AddCache(List<CacheDBItem<VarModel>> data)
 | 
			
		||||
    {
 | 
			
		||||
        if (_businessPropertyWithCache.CacheEnable && data?.Count > 0)
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                LogMessage?.LogInformation($"Add {typeof(VarModel).Name} data to file cache, count {data.Count}");
 | 
			
		||||
                foreach (var item in data)
 | 
			
		||||
                {
 | 
			
		||||
                    item.Id = CommonUtils.GetSingleId();
 | 
			
		||||
                }
 | 
			
		||||
                var dir = CacheDBUtil.GetCacheFilePath(CurrentDevice.Name.ToString());
 | 
			
		||||
                var fileStart = CacheDBUtil.GetFileName($"{CurrentDevice.PluginName}_{typeof(VarModel).Name}");
 | 
			
		||||
                var fullName = dir.CombinePathWithOs($"{fileStart}{CacheDBUtil.EX}");
 | 
			
		||||
 | 
			
		||||
                lock (this)
 | 
			
		||||
                {
 | 
			
		||||
                    bool s = false;
 | 
			
		||||
                    while (!s)
 | 
			
		||||
                    {
 | 
			
		||||
                        s = CacheDBUtil.DeleteCache(_businessPropertyWithCache.CacheFileMaxLength, fullName);
 | 
			
		||||
                    }
 | 
			
		||||
                    LocalDBCacheVarModelInited = false;
 | 
			
		||||
                    using var cache = LocalDBCacheVarModel();
 | 
			
		||||
                    cache.DBProvider.Fastest<CacheDBItem<VarModel>>().PageSize(50000).BulkCopy(data);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    using var cache = LocalDBCacheVarModel();
 | 
			
		||||
                    lock (cache.CacheDBOption.FileFullName)
 | 
			
		||||
                        cache.DBProvider.Fastest<CacheDBItem<VarModel>>().PageSize(50000).BulkCopy(data);
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception ex)
 | 
			
		||||
                {
 | 
			
		||||
                    LogMessage?.LogWarning(ex, "Add cache fail");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 入缓存
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="data"></param>
 | 
			
		||||
    protected virtual void AddCache(List<CacheDBItem<List<VarModel>>> data)
 | 
			
		||||
    {
 | 
			
		||||
        if (_businessPropertyWithCache.CacheEnable && data?.Count > 0)
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                foreach (var item in data)
 | 
			
		||||
                {
 | 
			
		||||
                    item.Id = CommonUtils.GetSingleId();
 | 
			
		||||
                }
 | 
			
		||||
                var dir = CacheDBUtil.GetCacheFilePath(CurrentDevice.Name.ToString());
 | 
			
		||||
                var fileStart = CacheDBUtil.GetFileName($"{CurrentDevice.PluginName}_List_{typeof(VarModel).Name}");
 | 
			
		||||
                var fullName = dir.CombinePathWithOs($"{fileStart}{CacheDBUtil.EX}");
 | 
			
		||||
 | 
			
		||||
                lock (this)
 | 
			
		||||
                {
 | 
			
		||||
                    bool s = false;
 | 
			
		||||
                    while (!s)
 | 
			
		||||
                    {
 | 
			
		||||
                        s = CacheDBUtil.DeleteCache(_businessPropertyWithCache.CacheFileMaxLength, fullName);
 | 
			
		||||
                    }
 | 
			
		||||
                    LocalDBCacheVarModelsInited = false;
 | 
			
		||||
                    using var cache = LocalDBCacheVarModels();
 | 
			
		||||
                    cache.DBProvider.Fastest<CacheDBItem<List<VarModel>>>().PageSize(50000).BulkCopy(data);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    using var cache = LocalDBCacheVarModels();
 | 
			
		||||
                    lock (cache.CacheDBOption.FileFullName)
 | 
			
		||||
                        cache.DBProvider.Fastest<CacheDBItem<List<VarModel>>>().PageSize(50000).BulkCopy(data);
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception ex)
 | 
			
		||||
                {
 | 
			
		||||
                    LogMessage?.LogWarning(ex, "Add cache fail");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 添加队列,超限后会入缓存
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="data"></param>
 | 
			
		||||
    protected virtual void AddQueueVarModel(CacheDBItem<VarModel> data)
 | 
			
		||||
    {
 | 
			
		||||
        if (_businessPropertyWithCache.CacheEnable)
 | 
			
		||||
        {
 | 
			
		||||
            //检测队列长度,超限存入缓存数据库
 | 
			
		||||
            if (_memoryVarModelQueue.Count > _businessPropertyWithCache.QueueMaxCount)
 | 
			
		||||
            {
 | 
			
		||||
                List<CacheDBItem<VarModel>> list = null;
 | 
			
		||||
                lock (_memoryVarModelQueue)
 | 
			
		||||
                {
 | 
			
		||||
                    if (_memoryVarModelQueue.Count > _businessPropertyWithCache.QueueMaxCount)
 | 
			
		||||
                    {
 | 
			
		||||
                        list = _memoryVarModelQueue.ToListWithDequeue();
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                AddCache(list);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if (_memoryVarModelQueue.Count > _businessPropertyWithCache.QueueMaxCount)
 | 
			
		||||
        {
 | 
			
		||||
            lock (_memoryVarModelQueue)
 | 
			
		||||
            {
 | 
			
		||||
                if (_memoryVarModelQueue.Count > _businessPropertyWithCache.QueueMaxCount)
 | 
			
		||||
                {
 | 
			
		||||
                    LogMessage?.LogWarning($"{typeof(VarModel).Name} Queue exceeds limit, clear old data. If it doesn't work as expected, increase [QueueMaxCount] or Enable cache");
 | 
			
		||||
                    _memoryVarModelQueue.Clear();
 | 
			
		||||
                    _memoryVarModelQueue.Enqueue(data);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            _memoryVarModelQueue.Enqueue(data);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 添加队列,超限后会入缓存
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="data"></param>
 | 
			
		||||
    protected virtual void AddQueueVarModel(CacheDBItem<List<VarModel>> data)
 | 
			
		||||
    {
 | 
			
		||||
        if (_businessPropertyWithCache.CacheEnable)
 | 
			
		||||
        {
 | 
			
		||||
            //检测队列长度,超限存入缓存数据库
 | 
			
		||||
            if (_memoryVarModelsQueue.Count > _businessPropertyWithCache.QueueMaxCount)
 | 
			
		||||
            {
 | 
			
		||||
                List<CacheDBItem<List<VarModel>>> list = null;
 | 
			
		||||
                lock (_memoryVarModelsQueue)
 | 
			
		||||
                {
 | 
			
		||||
                    if (_memoryVarModelsQueue.Count > _businessPropertyWithCache.QueueMaxCount)
 | 
			
		||||
                    {
 | 
			
		||||
                        list = _memoryVarModelsQueue.ToListWithDequeue();
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                AddCache(list);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if (_memoryVarModelsQueue.Count > _businessPropertyWithCache.QueueMaxCount)
 | 
			
		||||
        {
 | 
			
		||||
            lock (_memoryVarModelsQueue)
 | 
			
		||||
            {
 | 
			
		||||
                if (_memoryVarModelsQueue.Count > _businessPropertyWithCache.QueueMaxCount)
 | 
			
		||||
                {
 | 
			
		||||
                    _memoryVarModelsQueue.Clear();
 | 
			
		||||
                    _memoryVarModelsQueue.Enqueue(data);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            _memoryVarModelsQueue.Enqueue(data);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取缓存对象,注意using
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    protected virtual CacheDB LocalDBCacheVarModel()
 | 
			
		||||
    {
 | 
			
		||||
        var cacheDb = CacheDBUtil.GetCache(typeof(CacheDBItem<VarModel>), CurrentDevice.Name.ToString(), $"{CurrentDevice.PluginName}_{typeof(VarModel).Name}");
 | 
			
		||||
        if (!LocalDBCacheVarModelInited)
 | 
			
		||||
        {
 | 
			
		||||
            cacheDb.InitDb();
 | 
			
		||||
            LocalDBCacheVarModelInited = true;
 | 
			
		||||
        }
 | 
			
		||||
        return cacheDb;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取缓存对象,注意using
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    protected virtual CacheDB LocalDBCacheVarModels()
 | 
			
		||||
    {
 | 
			
		||||
        var cacheDb = CacheDBUtil.GetCache(typeof(CacheDBItem<List<VarModel>>), CurrentDevice.Name.ToString(), $"{CurrentDevice.PluginName}_List_{typeof(VarModel).Name}");
 | 
			
		||||
        if (!LocalDBCacheVarModelsInited)
 | 
			
		||||
        {
 | 
			
		||||
            cacheDb.InitDb();
 | 
			
		||||
            LocalDBCacheVarModelsInited = true;
 | 
			
		||||
        }
 | 
			
		||||
        return cacheDb;
 | 
			
		||||
    }
 | 
			
		||||
    protected virtual async Task Update(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        await UpdateVarModelMemory(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        await UpdateVarModelsMemory(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        await UpdateVarModelCache(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        await UpdateVarModelsCache(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 需实现上传到通道
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="item"></param>
 | 
			
		||||
    /// <param name="cancellationToken"></param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    protected abstract ValueTask<OperResult> UpdateVarModel(IEnumerable<CacheDBItem<VarModel>> item, CancellationToken cancellationToken);
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 需实现上传到通道
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="item"></param>
 | 
			
		||||
    /// <param name="cancellationToken"></param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    protected abstract ValueTask<OperResult> UpdateVarModels(IEnumerable<VarModel> item, CancellationToken cancellationToken);
 | 
			
		||||
    protected async Task UpdateVarModelCache(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        if (_businessPropertyWithCache.CacheEnable)
 | 
			
		||||
        {
 | 
			
		||||
            #region //成功上传时,补上传缓存数据
 | 
			
		||||
 | 
			
		||||
            if (IsConnected())
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    while (!cancellationToken.IsCancellationRequested)
 | 
			
		||||
                    {
 | 
			
		||||
                        //循环获取
 | 
			
		||||
 | 
			
		||||
                        var varList = await DBCacheVar.DBProvider.Queryable<CacheDBItem<VarModel>>().Take(_businessPropertyWithCache.SplitSize).ToListAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                        if (varList.Count > 0)
 | 
			
		||||
                        {
 | 
			
		||||
                            try
 | 
			
		||||
                            {
 | 
			
		||||
                                if (!cancellationToken.IsCancellationRequested)
 | 
			
		||||
                                {
 | 
			
		||||
                                    var result = await UpdateVarModel(varList, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                                    if (result.IsSuccess)
 | 
			
		||||
                                    {
 | 
			
		||||
                                        //删除缓存
 | 
			
		||||
                                        await DBCacheVar.DBProvider.Deleteable<CacheDBItem<VarModel>>(varList).ExecuteCommandAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                                    }
 | 
			
		||||
                                    else
 | 
			
		||||
                                        break;
 | 
			
		||||
                                }
 | 
			
		||||
                                else
 | 
			
		||||
                                {
 | 
			
		||||
                                    break;
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            catch (Exception ex)
 | 
			
		||||
                            {
 | 
			
		||||
                                if (success)
 | 
			
		||||
                                    LogMessage?.LogWarning(ex);
 | 
			
		||||
                                success = false;
 | 
			
		||||
                                break;
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        else
 | 
			
		||||
                        {
 | 
			
		||||
                            break;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception ex)
 | 
			
		||||
                {
 | 
			
		||||
                    if (success)
 | 
			
		||||
                        LogMessage?.LogWarning(ex);
 | 
			
		||||
                    success = false;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            #endregion //成功上传时,补上传缓存数据
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected async Task UpdateVarModelsCache(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        if (_businessPropertyWithCache.CacheEnable)
 | 
			
		||||
        {
 | 
			
		||||
            #region //成功上传时,补上传缓存数据
 | 
			
		||||
 | 
			
		||||
            if (IsConnected())
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    while (!cancellationToken.IsCancellationRequested)
 | 
			
		||||
                    {
 | 
			
		||||
                        //循环获取
 | 
			
		||||
 | 
			
		||||
                        var varList = await DBCacheVars.DBProvider.Queryable<CacheDBItem<List<VarModel>>>().FirstAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                        if (varList?.Value?.Count > 0)
 | 
			
		||||
                        {
 | 
			
		||||
                            try
 | 
			
		||||
                            {
 | 
			
		||||
                                if (!cancellationToken.IsCancellationRequested)
 | 
			
		||||
                                {
 | 
			
		||||
                                    var result = await UpdateVarModels(varList.Value, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                                    if (result.IsSuccess)
 | 
			
		||||
                                    {
 | 
			
		||||
                                        //删除缓存
 | 
			
		||||
                                        await DBCacheVars.DBProvider.Deleteable<CacheDBItem<List<VarModel>>>(varList).ExecuteCommandAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                                    }
 | 
			
		||||
                                    else
 | 
			
		||||
                                        break;
 | 
			
		||||
                                }
 | 
			
		||||
                                else
 | 
			
		||||
                                {
 | 
			
		||||
                                    break;
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            catch (Exception ex)
 | 
			
		||||
                            {
 | 
			
		||||
                                if (success)
 | 
			
		||||
                                    LogMessage?.LogWarning(ex);
 | 
			
		||||
                                success = false;
 | 
			
		||||
                                break;
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        else
 | 
			
		||||
                        {
 | 
			
		||||
                            break;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception ex)
 | 
			
		||||
                {
 | 
			
		||||
                    if (success)
 | 
			
		||||
                        LogMessage?.LogWarning(ex);
 | 
			
		||||
                    success = false;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            #endregion //成功上传时,补上传缓存数据
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected async Task UpdateVarModelMemory(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        #region //上传变量内存队列中的数据
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var list = _memoryVarModelQueue.ToListWithDequeue();
 | 
			
		||||
            if (list?.Count > 0)
 | 
			
		||||
            {
 | 
			
		||||
                var data = list.ChunkBetter(_businessPropertyWithCache.SplitSize);
 | 
			
		||||
                foreach (var item in data)
 | 
			
		||||
                {
 | 
			
		||||
                    try
 | 
			
		||||
                    {
 | 
			
		||||
                        if (!cancellationToken.IsCancellationRequested)
 | 
			
		||||
                        {
 | 
			
		||||
                            var result = await UpdateVarModel(item, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                            if (!result.IsSuccess)
 | 
			
		||||
                            {
 | 
			
		||||
                                AddCache(item.ToList());
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        else
 | 
			
		||||
                        {
 | 
			
		||||
                            break;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    catch (Exception ex)
 | 
			
		||||
                    {
 | 
			
		||||
                        if (success)
 | 
			
		||||
                            LogMessage?.LogWarning(ex);
 | 
			
		||||
                        success = false;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            if (success)
 | 
			
		||||
                LogMessage?.LogWarning(ex);
 | 
			
		||||
            success = false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        #endregion //上传变量内存队列中的数据
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected async Task UpdateVarModelsMemory(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        #region //上传变量内存队列中的数据
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            while (_memoryVarModelsQueue.TryDequeue(out var cacheDBItem))
 | 
			
		||||
            {
 | 
			
		||||
                if (cancellationToken.IsCancellationRequested)
 | 
			
		||||
                    break;
 | 
			
		||||
                var list = cacheDBItem.Value;
 | 
			
		||||
                var data = list.ChunkBetter(_businessPropertyWithCache.SplitSize);
 | 
			
		||||
                foreach (var item in data)
 | 
			
		||||
                {
 | 
			
		||||
                    try
 | 
			
		||||
                    {
 | 
			
		||||
                        if (!cancellationToken.IsCancellationRequested)
 | 
			
		||||
                        {
 | 
			
		||||
                            var result = await UpdateVarModels(item, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                            if (!result.IsSuccess)
 | 
			
		||||
                            {
 | 
			
		||||
 | 
			
		||||
                                AddCache(new List<CacheDBItem<List<VarModel>>>() { new CacheDBItem<List<VarModel>>(item.ToList()) });
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        else
 | 
			
		||||
                        {
 | 
			
		||||
                            break;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    catch (Exception ex)
 | 
			
		||||
                    {
 | 
			
		||||
                        if (success)
 | 
			
		||||
                            LogMessage?.LogWarning(ex);
 | 
			
		||||
                        success = false;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            if (success)
 | 
			
		||||
                LogMessage?.LogWarning(ex);
 | 
			
		||||
            success = false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        #endregion //上传变量内存队列中的数据
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -1,246 +0,0 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
//  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
 | 
			
		||||
//  此代码版权(除特别声明外的代码)归作者本人Diego所有
 | 
			
		||||
//  源代码使用协议遵循本仓库的开源协议及附加协议
 | 
			
		||||
//  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
 | 
			
		||||
//  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
 | 
			
		||||
//  使用文档:https://thingsgateway.cn/
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.Extension.Generic;
 | 
			
		||||
 | 
			
		||||
using TouchSocket.Core;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 业务插件的抽象基类,用于实现设备和变量之间的间隔上传功能。
 | 
			
		||||
/// </summary>
 | 
			
		||||
/// <typeparam name="VarModel">变量数据类型</typeparam>
 | 
			
		||||
/// <typeparam name="DevModel">设备数据类型</typeparam>
 | 
			
		||||
public abstract class BusinessBaseWithCacheIntervalDeviceModel<VarModel, DevModel> : BusinessBaseWithCacheDeviceModel<VarModel, DevModel>
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取具体业务属性的缓存设置。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    protected sealed override BusinessPropertyWithCache _businessPropertyWithCache => _businessPropertyWithCacheInterval;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取业务属性与缓存间隔的抽象属性。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    protected abstract BusinessPropertyWithCacheInterval _businessPropertyWithCacheInterval { get; }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    protected internal override async Task InitChannelAsync(IChannel? channel, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        // 如果不是间隔上传,则订阅全局变量值改变事件和设备状态改变事件,并触发一次事件处理
 | 
			
		||||
        if (_businessPropertyWithCacheInterval.BusinessUpdateEnum != BusinessUpdateEnum.Interval)
 | 
			
		||||
        {
 | 
			
		||||
            GlobalData.DeviceStatusChangeEvent += DeviceStatusChange;
 | 
			
		||||
            GlobalData.VariableValueChangeEvent += VariableValueChange;
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await base.InitChannelAsync(channel, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
    }
 | 
			
		||||
    public override async Task AfterVariablesChangedAsync(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        // 如果业务属性指定了全部变量,则设置当前设备的变量运行时列表和采集设备列表
 | 
			
		||||
        if (_businessPropertyWithCacheInterval.IsAllVariable)
 | 
			
		||||
        {
 | 
			
		||||
            LogMessage?.LogInformation("Refresh variable");
 | 
			
		||||
            IdVariableRuntimes.Clear();
 | 
			
		||||
            IdVariableRuntimes.AddRange(GlobalData.GetEnableVariables().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());
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            await base.AfterVariablesChangedAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        CollectDevices?.ForEach(a =>
 | 
			
		||||
        {
 | 
			
		||||
            if (a.Value.DeviceStatus == DeviceStatusEnum.OnLine && _businessPropertyWithCacheInterval.BusinessUpdateEnum != BusinessUpdateEnum.Interval)
 | 
			
		||||
                DeviceStatusChange(a.Value, a.Value.AdaptDeviceBasicData());
 | 
			
		||||
        });
 | 
			
		||||
        IdVariableRuntimes.ForEach(a =>
 | 
			
		||||
        {
 | 
			
		||||
            if (a.Value.IsOnline && _businessPropertyWithCacheInterval.BusinessUpdateEnum != BusinessUpdateEnum.Interval)
 | 
			
		||||
                VariableValueChange(a.Value, a.Value.AdaptVariableBasicData());
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 设备状态变化时发生的虚拟方法,用于处理设备状态变化事件。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="deviceRuntime">设备运行时对象</param>
 | 
			
		||||
    /// <param name="deviceData">设备数据对象</param>
 | 
			
		||||
    protected virtual void DeviceChange(DeviceRuntime deviceRuntime, DeviceBasicData deviceData)
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 当设备状态定时变化时触发此方法。如果不需要进行设备上传,则可以忽略此方法。通常情况下,需要在此方法中执行 <see cref="BusinessBaseWithCacheDeviceModel{T,T2}.AddQueueDevModel(CacheDBItem{T2})"/> 方法。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="deviceRuntime">设备运行时信息</param>
 | 
			
		||||
    /// <param name="deviceData">设备数据</param>
 | 
			
		||||
    protected virtual void DeviceTimeInterval(DeviceRuntime deviceRuntime, DeviceBasicData deviceData)
 | 
			
		||||
    {
 | 
			
		||||
        // 在设备状态变化时执行的自定义逻辑
 | 
			
		||||
    }
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 释放资源的方法,释放插件相关资源。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="disposing">是否释放托管资源</param>
 | 
			
		||||
    protected override void Dispose(bool disposing)
 | 
			
		||||
    {
 | 
			
		||||
        // 注销全局变量值改变事件和设备状态改变事件的订阅
 | 
			
		||||
        GlobalData.VariableValueChangeEvent -= VariableValueChange;
 | 
			
		||||
        GlobalData.DeviceStatusChangeEvent -= DeviceStatusChange;
 | 
			
		||||
 | 
			
		||||
        // 清空内存队列
 | 
			
		||||
        _memoryDevModelQueue.Clear();
 | 
			
		||||
        _memoryVarModelQueue.Clear();
 | 
			
		||||
        _memoryVarModelsQueue.Clear();
 | 
			
		||||
        base.Dispose(disposing);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 间隔上传数据的方法
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    protected void IntervalInsert(object? state, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        if (CurrentDevice.Pause == true)
 | 
			
		||||
        {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 如果业务属性的缓存为间隔上传,则根据定时器间隔执行相应操作
 | 
			
		||||
        if (_businessPropertyWithCacheInterval.BusinessUpdateEnum != BusinessUpdateEnum.Change)
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (LogMessage?.LogLevel <= LogLevel.Debug)
 | 
			
		||||
                    LogMessage?.LogDebug($"Interval  {typeof(VarModel).Name}  data, count {IdVariableRuntimes.Count}");
 | 
			
		||||
                // 上传所有变量信息
 | 
			
		||||
                var variableRuntimes = IdVariableRuntimes.Select(a => a.Value);
 | 
			
		||||
                VariableTimeInterval(variableRuntimes, variableRuntimes.AdaptIEnumerableVariableBasicData());
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                LogMessage?.LogWarning(ex, AppResource.IntervalInsertVariableFail);
 | 
			
		||||
            }
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (CollectDevices != null)
 | 
			
		||||
                {
 | 
			
		||||
                    if (LogMessage?.LogLevel <= LogLevel.Debug)
 | 
			
		||||
                        LogMessage?.LogDebug($"Interval  {typeof(DevModel).Name}  data, count {CollectDevices.Count}");
 | 
			
		||||
                    // 上传所有设备信息
 | 
			
		||||
                    foreach (var deviceRuntime in CollectDevices.Select(a => a.Value))
 | 
			
		||||
                    {
 | 
			
		||||
                        DeviceTimeInterval(deviceRuntime, deviceRuntime.AdaptDeviceBasicData());
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                LogMessage?.LogWarning(ex, AppResource.IntervalInsertDeviceFail);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected override List<IScheduledTask> ProtectedGetTasks(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        var list = base.ProtectedGetTasks(cancellationToken);
 | 
			
		||||
        list.Add(ScheduledTaskHelper.GetTask(_businessPropertyWithCacheInterval.BusinessInterval, IntervalInsert, null, LogMessage, cancellationToken));
 | 
			
		||||
        return list;
 | 
			
		||||
    }
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 变量状态变化时发生的虚拟方法,用于处理变量状态变化事件。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="variableRuntime">变量运行时对象</param>
 | 
			
		||||
    /// <param name="variable">变量数据对象</param>
 | 
			
		||||
    protected virtual void VariableChange(VariableRuntime variableRuntime, VariableBasicData variable)
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 当变量定时变化时触发此方法。如果不需要进行变量上传,则可以忽略此方法。通常情况下,需要在此方法中执行 <see cref="BusinessBaseWithCacheVariableModel{T}.AddQueueVarModel(CacheDBItem{T})"/> 方法。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="variableRuntimes">变量运行时信息</param>
 | 
			
		||||
    /// <param name="variables">变量数据</param>
 | 
			
		||||
    protected virtual void VariableTimeInterval(IEnumerable<VariableRuntime> variableRuntimes, IEnumerable<VariableBasicData> variables)
 | 
			
		||||
    {
 | 
			
		||||
        // 在变量状态变化时执行的自定义逻辑
 | 
			
		||||
    }
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 设备状态改变时的事件处理方法。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="deviceRuntime">设备运行时对象</param>
 | 
			
		||||
    /// <param name="deviceData">设备数据对象</param>
 | 
			
		||||
    private void DeviceStatusChange(DeviceRuntime deviceRuntime, DeviceBasicData deviceData)
 | 
			
		||||
    {
 | 
			
		||||
        // 如果当前设备已停止运行,则直接返回,不进行处理
 | 
			
		||||
        if (CurrentDevice.Pause == true)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        // 如果业务属性不是间隔上传,则执行设备状态改变的处理逻辑
 | 
			
		||||
        //if (_businessPropertyWithCacheInterval?.IsInterval != true)
 | 
			
		||||
        {
 | 
			
		||||
            // 检查当前设备集合中是否包含该设备,并进行相应处理
 | 
			
		||||
            if (CollectDevices?.ContainsKey(deviceRuntime.Id) == true)
 | 
			
		||||
                DeviceChange(deviceRuntime, deviceData);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 变量值改变时的事件处理方法。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="variableRuntime">变量运行时对象</param>
 | 
			
		||||
    /// <param name="variable">变量数据对象</param>
 | 
			
		||||
    private void VariableValueChange(VariableRuntime variableRuntime, VariableBasicData variable)
 | 
			
		||||
    {
 | 
			
		||||
        // 如果当前设备已停止运行,则直接返回,不进行处理
 | 
			
		||||
        if (CurrentDevice.Pause == true)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        // 如果业务属性不是间隔上传,则执行变量状态改变的处理逻辑
 | 
			
		||||
        //if (_businessPropertyWithCacheInterval?.IsInterval != true)
 | 
			
		||||
        {
 | 
			
		||||
            // 检查当前设备是否包含该变量,并进行相应处理
 | 
			
		||||
            if (IdVariableRuntimes.ContainsKey(variableRuntime.Id))
 | 
			
		||||
                VariableChange(variableRuntime, variable);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override void PauseThread(bool pause)
 | 
			
		||||
    {
 | 
			
		||||
        lock (this)
 | 
			
		||||
        {
 | 
			
		||||
            var oldV = CurrentDevice.Pause;
 | 
			
		||||
            base.PauseThread(pause);
 | 
			
		||||
            if (!pause && oldV != pause)
 | 
			
		||||
            {
 | 
			
		||||
                CollectDevices?.ForEach(a =>
 | 
			
		||||
                {
 | 
			
		||||
                    if (a.Value.DeviceStatus == DeviceStatusEnum.OnLine && _businessPropertyWithCacheInterval.BusinessUpdateEnum != BusinessUpdateEnum.Interval)
 | 
			
		||||
                        DeviceStatusChange(a.Value, a.Value.AdaptDeviceBasicData());
 | 
			
		||||
                });
 | 
			
		||||
                IdVariableRuntimes.ForEach(a =>
 | 
			
		||||
                {
 | 
			
		||||
                    if (a.Value.IsOnline && _businessPropertyWithCacheInterval.BusinessUpdateEnum != BusinessUpdateEnum.Interval)
 | 
			
		||||
                        VariableValueChange(a.Value, a.Value.AdaptVariableBasicData());
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -1,169 +0,0 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
//  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
 | 
			
		||||
//  此代码版权(除特别声明外的代码)归作者本人Diego所有
 | 
			
		||||
//  源代码使用协议遵循本仓库的开源协议及附加协议
 | 
			
		||||
//  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
 | 
			
		||||
//  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
 | 
			
		||||
//  使用文档:https://thingsgateway.cn/
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.Extension.Generic;
 | 
			
		||||
 | 
			
		||||
using TouchSocket.Core;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 抽象类 <see cref="BusinessBaseWithCacheIntervalVariableModel{VarModel}"/>,表示具有缓存间隔功能的业务基类,其中 T 代表变量模型。
 | 
			
		||||
/// </summary>
 | 
			
		||||
/// <typeparam name="VarModel">变量模型类型</typeparam>
 | 
			
		||||
public abstract class BusinessBaseWithCacheIntervalVariableModel<VarModel> : BusinessBaseWithCacheVariableModel<VarModel>
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取具体业务属性的缓存设置。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    protected sealed override BusinessPropertyWithCache _businessPropertyWithCache => _businessPropertyWithCacheInterval;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取具体业务属性的缓存间隔设置。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    protected abstract BusinessPropertyWithCacheInterval _businessPropertyWithCacheInterval { get; }
 | 
			
		||||
 | 
			
		||||
    protected internal override async Task InitChannelAsync(IChannel? channel, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        // 注册变量值变化事件处理程序
 | 
			
		||||
        if (_businessPropertyWithCacheInterval.BusinessUpdateEnum != BusinessUpdateEnum.Interval)
 | 
			
		||||
        {
 | 
			
		||||
            GlobalData.VariableValueChangeEvent += VariableValueChange;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await base.InitChannelAsync(channel, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
    }
 | 
			
		||||
    public override async Task AfterVariablesChangedAsync(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        // 如果业务属性指定了全部变量,则设置当前设备的变量运行时列表和采集设备列表
 | 
			
		||||
        if (_businessPropertyWithCacheInterval.IsAllVariable)
 | 
			
		||||
        {
 | 
			
		||||
            LogMessage?.LogInformation("Refresh variable");
 | 
			
		||||
            IdVariableRuntimes.Clear();
 | 
			
		||||
            IdVariableRuntimes.AddRange(GlobalData.GetEnableVariables().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());
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            await base.AfterVariablesChangedAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 触发一次变量值变化事件
 | 
			
		||||
        IdVariableRuntimes.ForEach(a =>
 | 
			
		||||
        {
 | 
			
		||||
            if (a.Value.IsOnline && _businessPropertyWithCacheInterval.BusinessUpdateEnum != BusinessUpdateEnum.Interval)
 | 
			
		||||
                VariableValueChange(a.Value, a.Value.AdaptVariableBasicData());
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 释放资源的方法。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="disposing">是否释放托管资源</param>
 | 
			
		||||
    protected override void Dispose(bool disposing)
 | 
			
		||||
    {
 | 
			
		||||
        GlobalData.VariableValueChangeEvent -= VariableValueChange;
 | 
			
		||||
        _memoryVarModelQueue.Clear();
 | 
			
		||||
        _memoryVarModelsQueue.Clear();
 | 
			
		||||
        base.Dispose(disposing);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 间隔上传数据的方法
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    protected void IntervalInsert(object? state, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        if (CurrentDevice.Pause == true)
 | 
			
		||||
        {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 如果业务属性的缓存为间隔上传,则根据定时器间隔执行相应操作
 | 
			
		||||
        if (_businessPropertyWithCacheInterval.BusinessUpdateEnum != BusinessUpdateEnum.Change)
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (LogMessage?.LogLevel <= LogLevel.Debug)
 | 
			
		||||
                    LogMessage?.LogDebug($"Interval  {typeof(VarModel).Name}  data, count {IdVariableRuntimes.Count}");
 | 
			
		||||
                // 上传所有变量信息
 | 
			
		||||
                var variableRuntimes = IdVariableRuntimes.Select(a => a.Value);
 | 
			
		||||
                VariableTimeInterval(variableRuntimes, variableRuntimes.AdaptIEnumerableVariableBasicData());
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                LogMessage?.LogWarning(ex, AppResource.IntervalInsertVariableFail);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected override List<IScheduledTask> ProtectedGetTasks(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        var list = base.ProtectedGetTasks(cancellationToken);
 | 
			
		||||
        list.Add(ScheduledTaskHelper.GetTask(_businessPropertyWithCacheInterval.BusinessInterval, IntervalInsert, null, LogMessage, cancellationToken));
 | 
			
		||||
        return list;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 当变量状态变化时发生,通常需要执行<see cref="BusinessBaseWithCacheVariableModel{T}.AddQueueVarModel(CacheDBItem{T})"/>。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="variableRuntime">变量运行时对象</param>
 | 
			
		||||
    /// <param name="variable">变量运行时对象</param>
 | 
			
		||||
    protected virtual void VariableChange(VariableRuntime variableRuntime, VariableBasicData variable)
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 当变量定时变化时触发此方法。如果不需要进行变量上传,则可以忽略此方法。通常情况下,需要在此方法中执行 <see cref="BusinessBaseWithCacheVariableModel{T}.AddQueueVarModel(CacheDBItem{T})"/> 方法。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="variableRuntimes">变量运行时信息</param>
 | 
			
		||||
    /// <param name="variables">变量数据</param>
 | 
			
		||||
    protected virtual void VariableTimeInterval(IEnumerable<VariableRuntime> variableRuntimes, IEnumerable<VariableBasicData> variables)
 | 
			
		||||
    {
 | 
			
		||||
        // 在变量状态变化时执行的自定义逻辑
 | 
			
		||||
    }
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 当变量值发生变化时调用的方法。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="variableRuntime">变量运行时对象</param>
 | 
			
		||||
    /// <param name="variable">变量数据</param>
 | 
			
		||||
    protected void VariableValueChange(VariableRuntime variableRuntime, VariableBasicData variable)
 | 
			
		||||
    {
 | 
			
		||||
        if (CurrentDevice.Pause == true)
 | 
			
		||||
            return;
 | 
			
		||||
        //if (_businessPropertyWithCacheInterval?.IsInterval != true)
 | 
			
		||||
        {
 | 
			
		||||
            //筛选
 | 
			
		||||
            if (IdVariableRuntimes.ContainsKey(variableRuntime.Id))
 | 
			
		||||
                VariableChange(variableRuntime, variable);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    public override void PauseThread(bool pause)
 | 
			
		||||
    {
 | 
			
		||||
        lock (this)
 | 
			
		||||
        {
 | 
			
		||||
            var oldV = CurrentDevice.Pause;
 | 
			
		||||
            base.PauseThread(pause);
 | 
			
		||||
            if (!pause && oldV != pause)
 | 
			
		||||
            {
 | 
			
		||||
                IdVariableRuntimes.ForEach(a =>
 | 
			
		||||
                {
 | 
			
		||||
                    if (a.Value.IsOnline && _businessPropertyWithCacheInterval.BusinessUpdateEnum != BusinessUpdateEnum.Interval)
 | 
			
		||||
                        VariableValueChange(a.Value, a.Value.AdaptVariableBasicData());
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -68,7 +68,7 @@ public static class GlobalData
 | 
			
		||||
    public static async Task<IEnumerable<ChannelRuntime>> GetCurrentUserChannels()
 | 
			
		||||
    {
 | 
			
		||||
        var dataScope = await GlobalData.SysUserService.GetCurrentUserDataScopeAsync().ConfigureAwait(false);
 | 
			
		||||
        return ReadOnlyChannels.WhereIf(dataScope != null && dataScope?.Count > 0, u => dataScope.Contains(u.Value.CreateOrgId))//在指定机构列表查询
 | 
			
		||||
        return ReadOnlyIdChannels.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<DeviceRuntime>> GetCurrentUserDevices()
 | 
			
		||||
@@ -126,7 +126,7 @@ public static class GlobalData
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static IEnumerable<ChannelRuntime> GetEnableChannels()
 | 
			
		||||
    {
 | 
			
		||||
        return Channels.Where(a => a.Value.Enable).Select(a => a.Value);
 | 
			
		||||
        return IdChannels.Where(a => a.Value.Enable).Select(a => a.Value);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static VariableRuntime GetVariable(string deviceName, string variableName)
 | 
			
		||||
@@ -414,12 +414,20 @@ public static class GlobalData
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 只读的通道字典,提供对通道的只读访问
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static IReadOnlyDictionary<long, ChannelRuntime> ReadOnlyChannels => Channels;
 | 
			
		||||
    public static IReadOnlyDictionary<long, ChannelRuntime> ReadOnlyIdChannels => IdChannels;
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 只读的通道字典,提供对通道的只读访问
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static IReadOnlyDictionary<string, ChannelRuntime> ReadOnlyChannels => Channels;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 内部使用的通道字典,用于存储通道对象
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    internal static ConcurrentDictionary<long, ChannelRuntime> Channels { get; } = new();
 | 
			
		||||
    internal static ConcurrentDictionary<long, ChannelRuntime> IdChannels { get; } = new();
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 内部使用的通道字典,用于存储通道对象
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    internal static ConcurrentDictionary<string, ChannelRuntime> Channels { get; } = new();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -16,6 +16,7 @@ public static class AppResource
 | 
			
		||||
    public static string RulesEngineTaskStart => ThingsGateway.Foundation.AppResource.Lang == Language.Chinese ? ChineseResource.RulesEngineTaskStart : EnglishResource.RulesEngineTaskStart;
 | 
			
		||||
 | 
			
		||||
    public static string RealAlarmTaskStart => ThingsGateway.Foundation.AppResource.Lang == Language.Chinese ? ChineseResource.RealAlarmTaskStart : EnglishResource.RealAlarmTaskStart;
 | 
			
		||||
    public static string RealAlarmTaskStop => ThingsGateway.Foundation.AppResource.Lang == Language.Chinese ? ChineseResource.RealAlarmTaskStop : EnglishResource.RealAlarmTaskStop;
 | 
			
		||||
    public static string IntervalInsertAlarmFail => ThingsGateway.Foundation.AppResource.Lang == Language.Chinese ? ChineseResource.IntervalInsertAlarmFail : EnglishResource.IntervalInsertAlarmFail;
 | 
			
		||||
    public static string IntervalInsertDeviceFail => ThingsGateway.Foundation.AppResource.Lang == Language.Chinese ? ChineseResource.IntervalInsertDeviceFail : EnglishResource.IntervalInsertDeviceFail;
 | 
			
		||||
    public static string IntervalInsertVariableFail => ThingsGateway.Foundation.AppResource.Lang == Language.Chinese ? ChineseResource.IntervalInsertVariableFail : EnglishResource.IntervalInsertVariableFail;
 | 
			
		||||
@@ -50,6 +51,7 @@ public static class ChineseResource
 | 
			
		||||
    public const string RulesEngineTaskStart = "规则引擎线程启动";
 | 
			
		||||
 | 
			
		||||
    public const string RealAlarmTaskStart = "实时报警服务启动";
 | 
			
		||||
    public const string RealAlarmTaskStop = "实时报警服务停止";
 | 
			
		||||
    public const string IntervalInsertAlarmFail = "间隔上传报警失败";
 | 
			
		||||
    public const string IntervalInsertDeviceFail = "间隔上传设备失败";
 | 
			
		||||
    public const string IntervalInsertVariableFail = "间隔上传变量失败";
 | 
			
		||||
@@ -87,6 +89,8 @@ public static class EnglishResource
 | 
			
		||||
    public const string RulesEngineTaskStart = "Rules engine service started";
 | 
			
		||||
 | 
			
		||||
    public const string RealAlarmTaskStart = "Real-time alarm service started";
 | 
			
		||||
    public const string RealAlarmTaskStop = "Real-time alarm service stoped";
 | 
			
		||||
 | 
			
		||||
    public const string IntervalInsertAlarmFail = "Failed to upload alarms periodically";
 | 
			
		||||
    public const string IntervalInsertDeviceFail = "Failed to upload device data periodically";
 | 
			
		||||
    public const string IntervalInsertVariableFail = "Failed to upload variable data periodically";
 | 
			
		||||
 
 | 
			
		||||
@@ -228,6 +228,8 @@
 | 
			
		||||
    "Connect": "Connect",
 | 
			
		||||
    "ConnectTimeout": "ConnectTimeout",
 | 
			
		||||
    "CopyChannel": "Copy Channel",
 | 
			
		||||
    "UpdateGatewayData": "UpdateGatewayData",
 | 
			
		||||
    "InsertGatewayData": "InsertGatewayData",
 | 
			
		||||
    "CreateTime": "CreateTime",
 | 
			
		||||
    "CreateUser": "CreateUser",
 | 
			
		||||
    "DataBits": "DataBits",
 | 
			
		||||
@@ -422,6 +424,7 @@
 | 
			
		||||
  "ThingsGateway.Gateway.Application.RuntimeInfoController": {
 | 
			
		||||
    "CheckRealAlarm": "Confirm real-time alarm",
 | 
			
		||||
    "GetChannelListAsync": "Get channel information",
 | 
			
		||||
    "GetRedundancyStatus": "Get redundancy status",
 | 
			
		||||
    "GetDeviceListAsync": "Get device information",
 | 
			
		||||
    "GetRealAlarmList": "Get real-time alarm information",
 | 
			
		||||
    "GetVariableList": "Get variable information",
 | 
			
		||||
 
 | 
			
		||||
@@ -227,6 +227,8 @@
 | 
			
		||||
    "Connect": "连接",
 | 
			
		||||
    "ConnectTimeout": "连接超时",
 | 
			
		||||
    "CopyChannel": "复制通道",
 | 
			
		||||
    "UpdateGatewayData": "更新网关点位配置",
 | 
			
		||||
    "InsertGatewayData": "添加网关点位配置",
 | 
			
		||||
    "CreateTime": "创建时间",
 | 
			
		||||
    "CreateUser": "创建人",
 | 
			
		||||
    "DataBits": "数据位",
 | 
			
		||||
@@ -423,6 +425,7 @@
 | 
			
		||||
  "ThingsGateway.Gateway.Application.RuntimeInfoController": {
 | 
			
		||||
    "CheckRealAlarm": "确认实时报警",
 | 
			
		||||
    "GetChannelListAsync": "获取通道信息",
 | 
			
		||||
    "GetRedundancyStatus": "获取冗余状态",
 | 
			
		||||
    "GetDeviceListAsync": "获取设备信息",
 | 
			
		||||
    "GetRealAlarmList": "获取实时报警信息",
 | 
			
		||||
    "GetVariableList": "获取变量信息",
 | 
			
		||||
 
 | 
			
		||||
@@ -16,6 +16,8 @@ namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
[Mapper(UseDeepCloning = true, EnumMappingStrategy = EnumMappingStrategy.ByName, RequiredMappingStrategy = RequiredMappingStrategy.None)]
 | 
			
		||||
public static partial class GatewayMapper
 | 
			
		||||
{
 | 
			
		||||
    public static partial VariableDataWithValue AdaptVariableDataWithValue(this VariableBasicData src);
 | 
			
		||||
    public static partial VariableDataWithValue AdaptVariableDataWithValue(this VariableRuntime src);
 | 
			
		||||
    public static partial AlarmVariable AdaptAlarmVariable(this VariableRuntime src);
 | 
			
		||||
    public static partial DeviceBasicData AdaptDeviceBasicData(this DeviceRuntime src);
 | 
			
		||||
    public static partial IEnumerable<DeviceBasicData> AdaptIEnumerableDeviceBasicData(this IEnumerable<DeviceRuntime> src);
 | 
			
		||||
@@ -55,7 +57,6 @@ public static partial class GatewayMapper
 | 
			
		||||
    public static partial Device AdaptDevice(this Device src);
 | 
			
		||||
    public static partial Variable AdaptVariable(this Variable src);
 | 
			
		||||
    public static partial List<PluginInfo> AdaptListPluginInfo(this List<PluginInfo> src);
 | 
			
		||||
    public static partial List<VariableBasicData> AdaptIEnumerableVariableBasicData(this IGrouping<string, VariableBasicData> src);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -120,9 +120,11 @@ public class ChannelRuntime : Channel, IChannelOptions, IDisposable
 | 
			
		||||
        // 通过插件名称获取插件信息
 | 
			
		||||
        PluginInfo = GlobalData.PluginService.GetList().FirstOrDefault(A => A.FullName == PluginName);
 | 
			
		||||
 | 
			
		||||
        GlobalData.Channels.TryRemove(Id, out _);
 | 
			
		||||
        GlobalData.IdChannels.TryRemove(Id, out _);
 | 
			
		||||
        GlobalData.Channels.TryRemove(Name, out _);
 | 
			
		||||
 | 
			
		||||
        GlobalData.Channels.TryAdd(Id, this);
 | 
			
		||||
        GlobalData.IdChannels.TryAdd(Id, this);
 | 
			
		||||
        GlobalData.Channels.TryAdd(Name, this);
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -130,7 +132,8 @@ public class ChannelRuntime : Channel, IChannelOptions, IDisposable
 | 
			
		||||
    {
 | 
			
		||||
        //Config?.SafeDispose();
 | 
			
		||||
 | 
			
		||||
        GlobalData.Channels.TryRemove(Id, out _);
 | 
			
		||||
        GlobalData.IdChannels.TryRemove(Id, out _);
 | 
			
		||||
        GlobalData.Channels.TryRemove(Name, out _);
 | 
			
		||||
        DeviceThreadManage = null;
 | 
			
		||||
        GC.SuppressFinalize(this);
 | 
			
		||||
    }
 | 
			
		||||
@@ -146,7 +149,7 @@ public class ChannelRuntime : Channel, IChannelOptions, IDisposable
 | 
			
		||||
 | 
			
		||||
    public IChannel GetChannel(TouchSocketConfig config)
 | 
			
		||||
    {
 | 
			
		||||
        lock (GlobalData.Channels)
 | 
			
		||||
        lock (GlobalData.IdChannels)
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
            if (DeviceThreadManage?.Channel?.DisposedValue == false)
 | 
			
		||||
@@ -159,7 +162,7 @@ public class ChannelRuntime : Channel, IChannelOptions, IDisposable
 | 
			
		||||
                )
 | 
			
		||||
            {
 | 
			
		||||
                //获取相同配置的Tcp服务或Udp服务或COM
 | 
			
		||||
                var same = GlobalData.Channels.FirstOrDefault(a =>
 | 
			
		||||
                var same = GlobalData.IdChannels.FirstOrDefault(a =>
 | 
			
		||||
                 {
 | 
			
		||||
                     if (a.Value == this)
 | 
			
		||||
                         return false;
 | 
			
		||||
 
 | 
			
		||||
@@ -126,11 +126,12 @@ public class VariableBasicData
 | 
			
		||||
    /// <inheritdoc cref="VariableRuntime.IsOnline"/>
 | 
			
		||||
    public bool IsOnline { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc cref="VariableRuntime.DeviceRuntime"/>
 | 
			
		||||
    [System.Text.Json.Serialization.JsonIgnore]
 | 
			
		||||
    [Newtonsoft.Json.JsonIgnore]
 | 
			
		||||
    public DeviceBasicData DeviceRuntime { get; set; }
 | 
			
		||||
 | 
			
		||||
    ///// <inheritdoc cref="VariableRuntime.DeviceRuntime"/>
 | 
			
		||||
    //[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)]
 | 
			
		||||
 
 | 
			
		||||
@@ -59,6 +59,72 @@ public class ChannelRuntimeService : IChannelRuntimeService
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> InsertAsync(List<Channel> models, List<Device> devices, List<Variable> variables, bool restart, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            await WaitLock.WaitAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            var result = await GlobalData.ChannelService.InsertAsync(models, devices, variables).ConfigureAwait(false);
 | 
			
		||||
            var ids = models.Select(a => a.Id).ToHashSet();
 | 
			
		||||
 | 
			
		||||
            var newChannelRuntimes = await RuntimeServiceHelper.GetNewChannelRuntimesAsync(ids).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            var deviceids = devices.Select(a => a.Id).ToHashSet();
 | 
			
		||||
            var newDeviceRuntimes = await RuntimeServiceHelper.GetNewDeviceRuntimesAsync(deviceids).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            await RuntimeServiceHelper.InitAsync(newChannelRuntimes, newDeviceRuntimes, _logger).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            //根据条件重启通道线程
 | 
			
		||||
            if (restart)
 | 
			
		||||
            {
 | 
			
		||||
                await GlobalData.ChannelThreadManage.RestartChannelAsync(newChannelRuntimes).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                await RuntimeServiceHelper.ChangedDriverAsync(_logger, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            WaitLock.Release();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> UpdateAsync(List<Channel> models, List<Device> devices, List<Variable> variables, bool restart, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            await WaitLock.WaitAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            var result = await GlobalData.ChannelService.UpdateAsync(models, devices, variables).ConfigureAwait(false);
 | 
			
		||||
            var ids = models.Select(a => a.Id).ToHashSet();
 | 
			
		||||
 | 
			
		||||
            var newChannelRuntimes = await RuntimeServiceHelper.GetNewChannelRuntimesAsync(ids).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            var deviceids = devices.Select(a => a.Id).ToHashSet();
 | 
			
		||||
            var newDeviceRuntimes = await RuntimeServiceHelper.GetNewDeviceRuntimesAsync(deviceids).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            await RuntimeServiceHelper.InitAsync(newChannelRuntimes, newDeviceRuntimes, _logger).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            //根据条件重启通道线程
 | 
			
		||||
            if (restart)
 | 
			
		||||
            {
 | 
			
		||||
                await GlobalData.ChannelThreadManage.RestartChannelAsync(newChannelRuntimes).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                await RuntimeServiceHelper.ChangedDriverAsync(_logger, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            WaitLock.Release();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> BatchEditAsync(IEnumerable<Channel> models, Channel oldModel, Channel model, bool restart = true)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
@@ -202,7 +268,7 @@ public class ChannelRuntimeService : IChannelRuntimeService
 | 
			
		||||
            await WaitLock.WaitAsync().ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            //网关启动时,获取所有通道
 | 
			
		||||
            var newChannelRuntimes = (await GlobalData.ChannelService.GetAllAsync().ConfigureAwait(false)).Where(a => ids.Contains(a.Id) || !GlobalData.Channels.ContainsKey(a.Id)).AdaptListChannelRuntime();
 | 
			
		||||
            var newChannelRuntimes = (await GlobalData.ChannelService.GetAllAsync().ConfigureAwait(false)).Where(a => ids.Contains(a.Id) || !GlobalData.IdChannels.ContainsKey(a.Id)).AdaptListChannelRuntime();
 | 
			
		||||
 | 
			
		||||
            var chanelIds = newChannelRuntimes.Select(a => a.Id).ToHashSet();
 | 
			
		||||
            var newDeviceRuntimes = (await GlobalData.DeviceService.GetAllAsync().ConfigureAwait(false)).Where(a => chanelIds.Contains(a.ChannelId)).AdaptListDeviceRuntime();
 | 
			
		||||
 
 | 
			
		||||
@@ -32,6 +32,81 @@ internal sealed class ChannelService : BaseService<Channel>, IChannelService
 | 
			
		||||
{
 | 
			
		||||
    #region CURD
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    [OperDesc("InsertGatewayData", localizerType: typeof(Channel), isRecordPar: false)]
 | 
			
		||||
    public async Task<bool> InsertAsync(List<Channel> models, List<Device> devices, List<Variable> variables)
 | 
			
		||||
    {
 | 
			
		||||
        using var db = GetDB();
 | 
			
		||||
 | 
			
		||||
        //事务
 | 
			
		||||
        var result = await db.UseTranAsync(async () =>
 | 
			
		||||
        {
 | 
			
		||||
            ManageHelper.CheckChannelCount(models.Count);
 | 
			
		||||
 | 
			
		||||
            await db.Insertable(models).ExecuteCommandAsync().ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            ManageHelper.CheckDeviceCount(devices.Count);
 | 
			
		||||
 | 
			
		||||
            await db.Insertable(devices).ExecuteCommandAsync().ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            ManageHelper.CheckVariableCount(variables.Count);
 | 
			
		||||
 | 
			
		||||
            await db.Insertable(variables).ExecuteCommandAsync().ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        }).ConfigureAwait(false);
 | 
			
		||||
        if (result.IsSuccess)//如果成功了
 | 
			
		||||
        {
 | 
			
		||||
            DeleteChannelFromCache();
 | 
			
		||||
            App.GetService<IDeviceService>().DeleteDeviceFromCache();
 | 
			
		||||
            App.GetService<IVariableService>().DeleteVariableCache();
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            //写日志
 | 
			
		||||
            throw new(result.ErrorMessage, result.ErrorException);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    [OperDesc("UpdateGatewayData", localizerType: typeof(Channel), isRecordPar: false)]
 | 
			
		||||
    public async Task<bool> UpdateAsync(List<Channel> models, List<Device> devices, List<Variable> variables)
 | 
			
		||||
    {
 | 
			
		||||
        using var db = GetDB();
 | 
			
		||||
 | 
			
		||||
        //事务
 | 
			
		||||
        var result = await db.UseTranAsync(async () =>
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
            await db.Updateable(models).ExecuteCommandAsync().ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            await db.Updateable(devices).ExecuteCommandAsync().ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            await db.Updateable(variables).ExecuteCommandAsync().ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        }).ConfigureAwait(false);
 | 
			
		||||
        if (result.IsSuccess)//如果成功了
 | 
			
		||||
        {
 | 
			
		||||
            DeleteChannelFromCache();
 | 
			
		||||
            App.GetService<IDeviceService>().DeleteDeviceFromCache();
 | 
			
		||||
            App.GetService<IVariableService>().DeleteVariableCache();
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            //写日志
 | 
			
		||||
            throw new(result.ErrorMessage, result.ErrorException);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    [OperDesc("CopyChannel", localizerType: typeof(Channel), isRecordPar: false)]
 | 
			
		||||
    public async Task<bool> CopyAsync(List<Channel> models, Dictionary<Device, List<Variable>> devices)
 | 
			
		||||
 
 | 
			
		||||
@@ -57,5 +57,6 @@ public interface IChannelRuntimeService
 | 
			
		||||
    Task<MemoryStream> ExportMemoryStream(IEnumerable<Channel> data);
 | 
			
		||||
    Task RestartChannelAsync(IEnumerable<ChannelRuntime> oldChannelRuntimes);
 | 
			
		||||
    Task<bool> CopyAsync(List<Channel> models, Dictionary<Device, List<Variable>> devices, bool restart, CancellationToken cancellationToken);
 | 
			
		||||
 | 
			
		||||
    Task<bool> UpdateAsync(List<Channel> models, List<Device> devices, List<Variable> variables, bool restart, CancellationToken cancellationToken);
 | 
			
		||||
    Task<bool> InsertAsync(List<Channel> models, List<Device> devices, List<Variable> variables, bool restart, CancellationToken cancellationToken);
 | 
			
		||||
}
 | 
			
		||||
@@ -101,5 +101,6 @@ internal interface IChannelService
 | 
			
		||||
    /// 保存是否输出日志和日志等级
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    Task UpdateLogAsync(long channelId, TouchSocket.Core.LogLevel logLevel);
 | 
			
		||||
 | 
			
		||||
    Task<bool> InsertAsync(List<Channel> models, List<Device> devices, List<Variable> variables);
 | 
			
		||||
    Task<bool> UpdateAsync(List<Channel> models, List<Device> devices, List<Variable> variables);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,7 @@ using Microsoft.Extensions.Logging;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.NewLife;
 | 
			
		||||
using ThingsGateway.NewLife.Collections;
 | 
			
		||||
using ThingsGateway.NewLife.DictionaryExtensions;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
 | 
			
		||||
@@ -106,7 +107,7 @@ public class DeviceRuntimeService : IDeviceRuntimeService
 | 
			
		||||
            var result = await GlobalData.DeviceService.DeleteDeviceAsync(devids).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            //根据条件重启通道线程
 | 
			
		||||
            var deviceRuntimes = GlobalData.IdDevices.Where(a => devids.Contains(a.Key)).Select(a => a.Value).ToList();
 | 
			
		||||
            var deviceRuntimes = GlobalData.IdDevices.FilterByKeys(devids).Select(a => a.Value).ToList();
 | 
			
		||||
 | 
			
		||||
            ConcurrentHashSet<IDriver> changedDriver = RuntimeServiceHelper.DeleteDeviceRuntime(deviceRuntimes);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -11,9 +11,6 @@
 | 
			
		||||
using Microsoft.Extensions.Hosting;
 | 
			
		||||
using Microsoft.Extensions.Logging;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.Gateway.Application.Extensions;
 | 
			
		||||
using ThingsGateway.NewLife.Extension;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
@@ -26,438 +23,21 @@ internal sealed class AlarmHostedService : BackgroundService, IAlarmHostedServic
 | 
			
		||||
    public AlarmHostedService(ILogger<AlarmHostedService> logger)
 | 
			
		||||
    {
 | 
			
		||||
        _logger = logger;
 | 
			
		||||
        AlarmTask = new AlarmTask(_logger);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #region 核心实现
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取bool报警类型
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="tag">要检查的变量</param>
 | 
			
		||||
    /// <param name="limit">报警限制值</param>
 | 
			
		||||
    /// <param name="expressions">报警约束表达式</param>
 | 
			
		||||
    /// <param name="text">报警文本</param>
 | 
			
		||||
    /// <returns>报警类型枚举</returns>
 | 
			
		||||
    private static AlarmTypeEnum? GetBoolAlarmCode(VariableRuntime tag, out string limit, out string expressions, out string text)
 | 
			
		||||
    private AlarmTask AlarmTask;
 | 
			
		||||
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
 | 
			
		||||
    {
 | 
			
		||||
        limit = string.Empty; // 初始化报警限制值为空字符串
 | 
			
		||||
        expressions = string.Empty; // 初始化报警约束表达式为空字符串
 | 
			
		||||
        text = string.Empty; // 初始化报警文本为空字符串
 | 
			
		||||
 | 
			
		||||
        if (tag?.Value == null) // 检查变量是否为null或其值为null
 | 
			
		||||
        {
 | 
			
		||||
            return null; // 如果是,则返回null
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (tag.BoolCloseAlarmEnable && !tag.Value.ToBoolean(true)) // 检查是否启用了关闭报警功能,并且变量的布尔值为false
 | 
			
		||||
        {
 | 
			
		||||
            limit = false.ToString(); // 将报警限制值设置为"false"
 | 
			
		||||
            expressions = tag.BoolCloseRestrainExpressions!; // 获取关闭报警的约束表达式
 | 
			
		||||
            text = tag.BoolCloseAlarmText!; // 获取关闭报警时的报警文本
 | 
			
		||||
            return AlarmTypeEnum.Close; // 返回关闭报警类型枚举
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (tag.BoolOpenAlarmEnable && tag.Value.ToBoolean(false)) // 检查是否启用了开启报警功能,并且变量的布尔值为true
 | 
			
		||||
        {
 | 
			
		||||
            limit = true.ToString(); // 将报警限制值设置为"true"
 | 
			
		||||
            expressions = tag.BoolOpenRestrainExpressions!; // 获取开启报警的约束表达式
 | 
			
		||||
            text = tag.BoolOpenAlarmText!; // 获取开启报警时的报警文本
 | 
			
		||||
            return AlarmTypeEnum.Open; // 返回开启报警类型枚举
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return null; // 如果不符合任何报警条件,则返回null
 | 
			
		||||
        await Task.Yield();
 | 
			
		||||
        AlarmTask.StartTask(stoppingToken);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取自定义报警类型
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="tag">要检查的变量</param>
 | 
			
		||||
    /// <param name="limit">报警限制值</param>
 | 
			
		||||
    /// <param name="expressions">报警约束表达式</param>
 | 
			
		||||
    /// <param name="text">报警文本</param>
 | 
			
		||||
    /// <returns>报警类型枚举</returns>
 | 
			
		||||
    private static AlarmTypeEnum? GetCustomAlarmDegree(VariableRuntime tag, out string limit, out string expressions, out string text)
 | 
			
		||||
    public override async Task StopAsync(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        limit = string.Empty; // 初始化报警限制值为空字符串
 | 
			
		||||
        expressions = string.Empty; // 初始化报警约束表达式为空字符串
 | 
			
		||||
        text = string.Empty; // 初始化报警文本为空字符串
 | 
			
		||||
 | 
			
		||||
        if (tag?.Value == null) // 检查变量是否为null或其值为null
 | 
			
		||||
        {
 | 
			
		||||
            return null; // 如果是,则返回null
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (tag.CustomAlarmEnable) // 检查是否启用了自定义报警功能
 | 
			
		||||
        {
 | 
			
		||||
            // 调用变量的CustomAlarmCode属性的GetExpressionsResult方法,传入变量的值,获取报警表达式的计算结果
 | 
			
		||||
            var result = tag.CustomAlarmCode.GetExpressionsResult(tag.Value, tag.DeviceRuntime?.Driver?.LogMessage);
 | 
			
		||||
 | 
			
		||||
            if (result is bool boolResult) // 检查计算结果是否为布尔类型
 | 
			
		||||
            {
 | 
			
		||||
                if (boolResult) // 如果计算结果为true
 | 
			
		||||
                {
 | 
			
		||||
                    limit = tag.CustomAlarmCode; // 将报警限制值设置为自定义报警代码
 | 
			
		||||
                    expressions = tag.CustomRestrainExpressions!; // 获取自定义报警时的报警约束表达式
 | 
			
		||||
                    text = tag.CustomAlarmText!; // 获取自定义报警时的报警文本
 | 
			
		||||
                    return AlarmTypeEnum.Custom; // 返回自定义报警类型枚举
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return null; // 如果不符合自定义报警条件,则返回null
 | 
			
		||||
        AlarmTask.Dispose();
 | 
			
		||||
        await base.StopAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取decimal类型的报警类型
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="tag">要检查的变量</param>
 | 
			
		||||
    /// <param name="limit">报警限制值</param>
 | 
			
		||||
    /// <param name="expressions">报警约束表达式</param>
 | 
			
		||||
    /// <param name="text">报警文本</param>
 | 
			
		||||
    /// <returns>报警类型枚举</returns>
 | 
			
		||||
    private static AlarmTypeEnum? GetDecimalAlarmDegree(VariableRuntime tag, out string limit, out string expressions, out string text)
 | 
			
		||||
    {
 | 
			
		||||
        limit = string.Empty; // 初始化报警限制值为空字符串
 | 
			
		||||
        expressions = string.Empty; // 初始化报警约束表达式为空字符串
 | 
			
		||||
        text = string.Empty; // 初始化报警文本为空字符串
 | 
			
		||||
 | 
			
		||||
        if (tag?.Value == null) // 检查变量是否为null或其值为null
 | 
			
		||||
        {
 | 
			
		||||
            return null; // 如果是,则返回null
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 检查是否启用了高高报警功能,并且变量的值大于高高报警的限制值
 | 
			
		||||
        if (tag.HHAlarmEnable && tag.Value.ToDecimal() > tag.HHAlarmCode.ToDecimal())
 | 
			
		||||
        {
 | 
			
		||||
            limit = tag.HHAlarmCode.ToString()!; // 将报警限制值设置为高高报警的限制值
 | 
			
		||||
            expressions = tag.HHRestrainExpressions!; // 获取高高报警的约束表达式
 | 
			
		||||
            text = tag.HHAlarmText!; // 获取高高报警时的报警文本
 | 
			
		||||
            return AlarmTypeEnum.HH; // 返回高高报警类型枚举
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 检查是否启用了高报警功能,并且变量的值大于高报警的限制值
 | 
			
		||||
        if (tag.HAlarmEnable && tag.Value.ToDecimal() > tag.HAlarmCode.ToDecimal())
 | 
			
		||||
        {
 | 
			
		||||
            limit = tag.HAlarmCode.ToString()!; // 将报警限制值设置为高报警的限制值
 | 
			
		||||
            expressions = tag.HRestrainExpressions!; // 获取高报警的约束表达式
 | 
			
		||||
            text = tag.HAlarmText!; // 获取高报警时的报警文本
 | 
			
		||||
            return AlarmTypeEnum.H; // 返回高报警类型枚举
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        // 检查是否启用了低低报警功能,并且变量的值小于低低报警的限制值
 | 
			
		||||
        if (tag.LLAlarmEnable && tag.Value.ToDecimal() < tag.LLAlarmCode.ToDecimal())
 | 
			
		||||
        {
 | 
			
		||||
            limit = tag.LLAlarmCode.ToString()!; // 将报警限制值设置为低低报警的限制值
 | 
			
		||||
            expressions = tag.LLRestrainExpressions!; // 获取低低报警的约束表达式
 | 
			
		||||
            text = tag.LLAlarmText!; // 获取低低报警时的报警文本
 | 
			
		||||
            return AlarmTypeEnum.LL; // 返回低低报警类型枚举
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        // 检查是否启用了低报警功能,并且变量的值小于低报警的限制值
 | 
			
		||||
        if (tag.LAlarmEnable && tag.Value.ToDecimal() < tag.LAlarmCode.ToDecimal())
 | 
			
		||||
        {
 | 
			
		||||
            limit = tag.LAlarmCode.ToString()!; // 将报警限制值设置为低报警的限制值
 | 
			
		||||
            expressions = tag.LRestrainExpressions!; // 获取低报警的约束表达式
 | 
			
		||||
            text = tag.LAlarmText!; // 获取低报警时的报警文本
 | 
			
		||||
            return AlarmTypeEnum.L; // 返回低报警类型枚举
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return null; // 如果不符合任何报警条件,则返回null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 对变量进行报警分析,并根据需要触发相应的报警事件或恢复事件。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="item">要进行报警分析的变量</param>
 | 
			
		||||
    private static void AlarmAnalysis(VariableRuntime item)
 | 
			
		||||
    {
 | 
			
		||||
        string limit; // 报警限制值
 | 
			
		||||
        string ex; // 报警约束表达式
 | 
			
		||||
        string text; // 报警文本
 | 
			
		||||
        AlarmTypeEnum? alarmEnum; // 报警类型枚举
 | 
			
		||||
        int delay = item.AlarmDelay; // 获取报警延迟时间
 | 
			
		||||
 | 
			
		||||
        // 检查变量的数据类型
 | 
			
		||||
        if (item.Value?.GetType() == typeof(bool))
 | 
			
		||||
        {
 | 
			
		||||
            // 如果数据类型为布尔型,则调用GetBoolAlarmCode方法获取布尔型报警类型及相关信息
 | 
			
		||||
            alarmEnum = GetBoolAlarmCode(item, out limit, out ex, out text);
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            // 如果数据类型为非布尔型,则调用GetDecimalAlarmDegree方法获取数值型报警类型及相关信息
 | 
			
		||||
            alarmEnum = GetDecimalAlarmDegree(item, out limit, out ex, out text);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 如果未获取到报警类型,则尝试获取自定义报警类型
 | 
			
		||||
        if (alarmEnum == null)
 | 
			
		||||
        {
 | 
			
		||||
            alarmEnum = GetCustomAlarmDegree(item, out limit, out ex, out text);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (alarmEnum == null)
 | 
			
		||||
        {
 | 
			
		||||
            // 如果仍未获取到报警类型,则触发需恢复报警事件(如果存在)
 | 
			
		||||
            AlarmChange(item, null, text, EventTypeEnum.Finish, alarmEnum, delay);
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            // 如果获取到了报警类型,则需触发报警事件或更新报警状态
 | 
			
		||||
 | 
			
		||||
            if (!string.IsNullOrEmpty(ex))
 | 
			
		||||
            {
 | 
			
		||||
                // 如果存在报警约束表达式,则计算表达式结果,以确定是否触发报警事件
 | 
			
		||||
                var data = ex.GetExpressionsResult(item.Value, item.DeviceRuntime?.Driver?.LogMessage);
 | 
			
		||||
                if (data is bool result)
 | 
			
		||||
                {
 | 
			
		||||
                    if (result)
 | 
			
		||||
                    {
 | 
			
		||||
                        // 如果表达式结果为true,则触发报警事件
 | 
			
		||||
                        AlarmChange(item, limit, text, EventTypeEnum.Alarm, alarmEnum, delay);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                // 如果不存在报警约束表达式,则直接触发报警事件
 | 
			
		||||
                AlarmChange(item, limit, text, EventTypeEnum.Alarm, alarmEnum, delay);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 根据报警事件类型进行相应的处理操作,包括触发报警事件或更新报警状态。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="item">要处理的变量</param>
 | 
			
		||||
    /// <param name="limit">报警限制值</param>
 | 
			
		||||
    /// <param name="text">报警文本</param>
 | 
			
		||||
    /// <param name="eventEnum">报警事件类型枚举</param>
 | 
			
		||||
    /// <param name="alarmEnum">报警类型枚举</param>
 | 
			
		||||
    /// <param name="delay">报警延时</param>
 | 
			
		||||
    private static void AlarmChange(VariableRuntime item, object limit, string text, EventTypeEnum eventEnum, AlarmTypeEnum? alarmEnum, int delay)
 | 
			
		||||
    {
 | 
			
		||||
        bool changed = false;
 | 
			
		||||
        if (eventEnum == EventTypeEnum.Finish)
 | 
			
		||||
        {
 | 
			
		||||
            // 如果是需恢复报警事件
 | 
			
		||||
            // 如果实时报警列表中不存在该变量,则直接返回
 | 
			
		||||
            if (!GlobalData.RealAlarmIdVariables.ContainsKey(item.Id))
 | 
			
		||||
            {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        else if (eventEnum == EventTypeEnum.Alarm)
 | 
			
		||||
        {
 | 
			
		||||
            // 如果是触发报警事件
 | 
			
		||||
            // 在实时报警列表中查找该变量
 | 
			
		||||
            if (GlobalData.RealAlarmIdVariables.TryGetValue(item.Id, out var variable))
 | 
			
		||||
            {
 | 
			
		||||
                // 如果变量已经处于相同的报警类型,则直接返回
 | 
			
		||||
                if (item.AlarmType == alarmEnum)
 | 
			
		||||
                    return;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 更新变量的报警信息和事件时间
 | 
			
		||||
        if (eventEnum == EventTypeEnum.Alarm)
 | 
			
		||||
        {
 | 
			
		||||
            var now = DateTime.Now;
 | 
			
		||||
            //添加报警延时策略
 | 
			
		||||
            if (delay > 0)
 | 
			
		||||
            {
 | 
			
		||||
                if (item.EventType != EventTypeEnum.Alarm && item.EventType != EventTypeEnum.Prepare)
 | 
			
		||||
                {
 | 
			
		||||
                    item.EventType = EventTypeEnum.Prepare;//准备报警
 | 
			
		||||
                    item.PrepareEventTime = now;
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    if (item.EventType == EventTypeEnum.Prepare)
 | 
			
		||||
                    {
 | 
			
		||||
                        if ((now - item.PrepareEventTime!.Value).TotalSeconds > delay)
 | 
			
		||||
                        {
 | 
			
		||||
                            //超过延时时间,触发报警
 | 
			
		||||
                            item.EventType = EventTypeEnum.Alarm;
 | 
			
		||||
                            item.AlarmTime = now;
 | 
			
		||||
                            item.EventTime = now;
 | 
			
		||||
                            item.AlarmType = alarmEnum;
 | 
			
		||||
                            item.AlarmLimit = limit.ToString();
 | 
			
		||||
                            item.AlarmCode = item.Value.ToString();
 | 
			
		||||
                            item.RecoveryCode = string.Empty;
 | 
			
		||||
                            item.AlarmText = text;
 | 
			
		||||
                            item.PrepareEventTime = null;
 | 
			
		||||
 | 
			
		||||
                            changed = true;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    else if (item.EventType == EventTypeEnum.Alarm && item.AlarmType != alarmEnum)
 | 
			
		||||
                    {
 | 
			
		||||
                        //报警类型改变,重新计时
 | 
			
		||||
                        if (item.PrepareEventTime == null)
 | 
			
		||||
                            item.PrepareEventTime = now;
 | 
			
		||||
                        if ((now - item.PrepareEventTime!.Value).TotalSeconds > delay)
 | 
			
		||||
                        {
 | 
			
		||||
                            //超过延时时间,触发报警
 | 
			
		||||
                            item.EventType = EventTypeEnum.Alarm;
 | 
			
		||||
                            item.AlarmTime = now;
 | 
			
		||||
                            item.EventTime = now;
 | 
			
		||||
                            item.AlarmType = alarmEnum;
 | 
			
		||||
                            item.AlarmLimit = limit.ToString();
 | 
			
		||||
                            item.AlarmCode = item.Value.ToString();
 | 
			
		||||
                            item.RecoveryCode = string.Empty;
 | 
			
		||||
                            item.AlarmText = text;
 | 
			
		||||
                            item.PrepareEventTime = null;
 | 
			
		||||
                            changed = true;
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                // 如果是触发报警事件
 | 
			
		||||
                item.EventType = eventEnum;
 | 
			
		||||
                item.AlarmTime = now;
 | 
			
		||||
                item.EventTime = now;
 | 
			
		||||
                item.AlarmType = alarmEnum;
 | 
			
		||||
                item.AlarmLimit = limit.ToString();
 | 
			
		||||
                item.AlarmCode = item.Value.ToString();
 | 
			
		||||
                item.RecoveryCode = string.Empty;
 | 
			
		||||
                item.AlarmText = text;
 | 
			
		||||
                changed = true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
        else if (eventEnum == EventTypeEnum.Finish)
 | 
			
		||||
        {
 | 
			
		||||
            // 如果是需恢复报警事件
 | 
			
		||||
            // 获取旧的报警信息
 | 
			
		||||
            if (GlobalData.RealAlarmIdVariables.TryGetValue(item.Id, out var oldAlarm))
 | 
			
		||||
            {
 | 
			
		||||
                item.AlarmType = oldAlarm.AlarmType;
 | 
			
		||||
                item.EventType = eventEnum;
 | 
			
		||||
                item.AlarmLimit = oldAlarm.AlarmLimit;
 | 
			
		||||
                item.AlarmCode = oldAlarm.AlarmCode;
 | 
			
		||||
                item.RecoveryCode = item.Value.ToString();
 | 
			
		||||
                item.AlarmText = oldAlarm.AlarmText;
 | 
			
		||||
                item.EventTime = DateTime.Now;
 | 
			
		||||
            }
 | 
			
		||||
            changed = true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 触发报警变化事件
 | 
			
		||||
        if (changed)
 | 
			
		||||
        {
 | 
			
		||||
            if (item.EventType == EventTypeEnum.Alarm)
 | 
			
		||||
            {
 | 
			
		||||
                // 如果是触发报警事件
 | 
			
		||||
                //lock (GlobalData. RealAlarmVariables)
 | 
			
		||||
                {
 | 
			
		||||
                    // 从实时报警列表中移除旧的报警信息,并添加新的报警信息
 | 
			
		||||
                    GlobalData.RealAlarmIdVariables.AddOrUpdate(item.Id, a => item.AdaptAlarmVariable(), (a, b) => item.AdaptAlarmVariable());
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            else if (item.EventType == EventTypeEnum.Finish)
 | 
			
		||||
            {
 | 
			
		||||
                // 如果是需恢复报警事件,则从实时报警列表中移除该变量
 | 
			
		||||
                GlobalData.RealAlarmIdVariables.TryRemove(item.Id, out _);
 | 
			
		||||
                //GlobalData.RealAlarmIdVariables.AddOrUpdate(item.Id, a => item.AdaptAlarmVariable(), (a, b) => item.AdaptAlarmVariable());
 | 
			
		||||
            }
 | 
			
		||||
            GlobalData.AlarmChange(item.AdaptAlarmVariable());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void ConfirmAlarm(long variableId)
 | 
			
		||||
    {
 | 
			
		||||
        // 如果是确认报警事件
 | 
			
		||||
        if (GlobalData.AlarmEnableIdVariables.TryGetValue(variableId, out var variableRuntime))
 | 
			
		||||
        {
 | 
			
		||||
            variableRuntime.EventType = EventTypeEnum.Confirm;
 | 
			
		||||
            variableRuntime.EventTime = DateTime.Now;
 | 
			
		||||
            var data = variableRuntime.AdaptAlarmVariable();
 | 
			
		||||
            GlobalData.RealAlarmIdVariables.AddOrUpdate(variableId, a => data, (a, b) => data);
 | 
			
		||||
 | 
			
		||||
            GlobalData.AlarmChange(data);
 | 
			
		||||
        }
 | 
			
		||||
        AlarmTask?.ConfirmAlarm(variableId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #endregion 核心实现
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 执行工作任务,对设备变量进行报警分析。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="state"></param>
 | 
			
		||||
    /// <param name="cancellation">取消任务的 CancellationToken</param>
 | 
			
		||||
    private void DoWork(object? state, CancellationToken cancellation)
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            if (!GlobalData.StartBusinessChannelEnable)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            //Stopwatch stopwatch = Stopwatch.StartNew();
 | 
			
		||||
            // 遍历设备变量列表
 | 
			
		||||
 | 
			
		||||
            if (!GlobalData.AlarmEnableIdVariables.IsEmpty)
 | 
			
		||||
            {
 | 
			
		||||
                var list = GlobalData.AlarmEnableIdVariables.Select(a => a.Value).ToArray();
 | 
			
		||||
                list.ParallelForEach((item, state, index) =>
 | 
			
		||||
            {
 | 
			
		||||
                {
 | 
			
		||||
                    // 如果取消请求已经被触发,则结束任务
 | 
			
		||||
                    if (cancellation.IsCancellationRequested)
 | 
			
		||||
                        return;
 | 
			
		||||
 | 
			
		||||
                    // 如果该变量的报警功能未启用,则跳过该变量
 | 
			
		||||
                    if (!item.AlarmEnable)
 | 
			
		||||
                        return;
 | 
			
		||||
 | 
			
		||||
                    // 如果该变量离线,则跳过该变量
 | 
			
		||||
                    if (!item.IsOnline)
 | 
			
		||||
                        return;
 | 
			
		||||
 | 
			
		||||
                    // 对该变量进行报警分析
 | 
			
		||||
                    AlarmAnalysis(item);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                //if (scheduledTask.Period != 5000)
 | 
			
		||||
                //    scheduledTask.Change(0, 5000); // 如果没有启用报警的变量,则设置下次执行时间为5秒后
 | 
			
		||||
                scheduledTask.SetNext(5000); // 如果没有启用报警的变量,则设置下次执行时间为5秒后
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            //stopwatch.Stop();
 | 
			
		||||
            //_logger.LogInformation("报警分析耗时:" + stopwatch.ElapsedMilliseconds + "ms");
 | 
			
		||||
        }
 | 
			
		||||
        catch (OperationCanceledException)
 | 
			
		||||
        {
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            _logger.LogWarning(ex, "Alarm analysis fail");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private ScheduledSyncTask scheduledTask;
 | 
			
		||||
    protected override Task ExecuteAsync(CancellationToken stoppingToken)
 | 
			
		||||
    {
 | 
			
		||||
        _logger.LogInformation(AppResource.RealAlarmTaskStart);
 | 
			
		||||
        scheduledTask = new ScheduledSyncTask(10, DoWork, null, null, stoppingToken);
 | 
			
		||||
        scheduledTask.Start();
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,475 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
//  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
 | 
			
		||||
//  此代码版权(除特别声明外的代码)归作者本人Diego所有
 | 
			
		||||
//  源代码使用协议遵循本仓库的开源协议及附加协议
 | 
			
		||||
//  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
 | 
			
		||||
//  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
 | 
			
		||||
//  使用文档:https://thingsgateway.cn/
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using Microsoft.Extensions.Logging;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.Gateway.Application.Extensions;
 | 
			
		||||
using ThingsGateway.NewLife;
 | 
			
		||||
using ThingsGateway.NewLife.Extension;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 设备采集报警后台服务
 | 
			
		||||
/// </summary>
 | 
			
		||||
internal sealed class AlarmTask : IDisposable
 | 
			
		||||
{
 | 
			
		||||
    private readonly ILogger _logger;
 | 
			
		||||
    private ScheduledSyncTask scheduledTask;
 | 
			
		||||
    public AlarmTask(ILogger logger)
 | 
			
		||||
    {
 | 
			
		||||
        _logger = logger;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void StartTask(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        _logger.LogInformation(AppResource.RealAlarmTaskStart);
 | 
			
		||||
        scheduledTask = new ScheduledSyncTask(10, DoWork, null, null, cancellationToken);
 | 
			
		||||
        scheduledTask.Start();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void StopTask()
 | 
			
		||||
    {
 | 
			
		||||
        _logger.LogInformation(AppResource.RealAlarmTaskStop);
 | 
			
		||||
        scheduledTask?.Stop();
 | 
			
		||||
    }
 | 
			
		||||
    public void Dispose()
 | 
			
		||||
    {
 | 
			
		||||
        StopTask();
 | 
			
		||||
        scheduledTask?.TryDispose();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    #region 核心实现
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取bool报警类型
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="tag">要检查的变量</param>
 | 
			
		||||
    /// <param name="limit">报警限制值</param>
 | 
			
		||||
    /// <param name="expressions">报警约束表达式</param>
 | 
			
		||||
    /// <param name="text">报警文本</param>
 | 
			
		||||
    /// <returns>报警类型枚举</returns>
 | 
			
		||||
    private static AlarmTypeEnum? GetBoolAlarmCode(VariableRuntime tag, out string limit, out string expressions, out string text)
 | 
			
		||||
    {
 | 
			
		||||
        limit = string.Empty; // 初始化报警限制值为空字符串
 | 
			
		||||
        expressions = string.Empty; // 初始化报警约束表达式为空字符串
 | 
			
		||||
        text = string.Empty; // 初始化报警文本为空字符串
 | 
			
		||||
 | 
			
		||||
        if (tag?.Value == null) // 检查变量是否为null或其值为null
 | 
			
		||||
        {
 | 
			
		||||
            return null; // 如果是,则返回null
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (tag.BoolCloseAlarmEnable && !tag.Value.ToBoolean(true)) // 检查是否启用了关闭报警功能,并且变量的布尔值为false
 | 
			
		||||
        {
 | 
			
		||||
            limit = false.ToString(); // 将报警限制值设置为"false"
 | 
			
		||||
            expressions = tag.BoolCloseRestrainExpressions!; // 获取关闭报警的约束表达式
 | 
			
		||||
            text = tag.BoolCloseAlarmText!; // 获取关闭报警时的报警文本
 | 
			
		||||
            return AlarmTypeEnum.Close; // 返回关闭报警类型枚举
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (tag.BoolOpenAlarmEnable && tag.Value.ToBoolean(false)) // 检查是否启用了开启报警功能,并且变量的布尔值为true
 | 
			
		||||
        {
 | 
			
		||||
            limit = true.ToString(); // 将报警限制值设置为"true"
 | 
			
		||||
            expressions = tag.BoolOpenRestrainExpressions!; // 获取开启报警的约束表达式
 | 
			
		||||
            text = tag.BoolOpenAlarmText!; // 获取开启报警时的报警文本
 | 
			
		||||
            return AlarmTypeEnum.Open; // 返回开启报警类型枚举
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return null; // 如果不符合任何报警条件,则返回null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取自定义报警类型
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="tag">要检查的变量</param>
 | 
			
		||||
    /// <param name="limit">报警限制值</param>
 | 
			
		||||
    /// <param name="expressions">报警约束表达式</param>
 | 
			
		||||
    /// <param name="text">报警文本</param>
 | 
			
		||||
    /// <returns>报警类型枚举</returns>
 | 
			
		||||
    private static AlarmTypeEnum? GetCustomAlarmDegree(VariableRuntime tag, out string limit, out string expressions, out string text)
 | 
			
		||||
    {
 | 
			
		||||
        limit = string.Empty; // 初始化报警限制值为空字符串
 | 
			
		||||
        expressions = string.Empty; // 初始化报警约束表达式为空字符串
 | 
			
		||||
        text = string.Empty; // 初始化报警文本为空字符串
 | 
			
		||||
 | 
			
		||||
        if (tag?.Value == null) // 检查变量是否为null或其值为null
 | 
			
		||||
        {
 | 
			
		||||
            return null; // 如果是,则返回null
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (tag.CustomAlarmEnable) // 检查是否启用了自定义报警功能
 | 
			
		||||
        {
 | 
			
		||||
            // 调用变量的CustomAlarmCode属性的GetExpressionsResult方法,传入变量的值,获取报警表达式的计算结果
 | 
			
		||||
            var result = tag.CustomAlarmCode.GetExpressionsResult(tag.Value, tag.DeviceRuntime?.Driver?.LogMessage);
 | 
			
		||||
 | 
			
		||||
            if (result is bool boolResult) // 检查计算结果是否为布尔类型
 | 
			
		||||
            {
 | 
			
		||||
                if (boolResult) // 如果计算结果为true
 | 
			
		||||
                {
 | 
			
		||||
                    limit = tag.CustomAlarmCode; // 将报警限制值设置为自定义报警代码
 | 
			
		||||
                    expressions = tag.CustomRestrainExpressions!; // 获取自定义报警时的报警约束表达式
 | 
			
		||||
                    text = tag.CustomAlarmText!; // 获取自定义报警时的报警文本
 | 
			
		||||
                    return AlarmTypeEnum.Custom; // 返回自定义报警类型枚举
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return null; // 如果不符合自定义报警条件,则返回null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取decimal类型的报警类型
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="tag">要检查的变量</param>
 | 
			
		||||
    /// <param name="limit">报警限制值</param>
 | 
			
		||||
    /// <param name="expressions">报警约束表达式</param>
 | 
			
		||||
    /// <param name="text">报警文本</param>
 | 
			
		||||
    /// <returns>报警类型枚举</returns>
 | 
			
		||||
    private static AlarmTypeEnum? GetDecimalAlarmDegree(VariableRuntime tag, out string limit, out string expressions, out string text)
 | 
			
		||||
    {
 | 
			
		||||
        limit = string.Empty; // 初始化报警限制值为空字符串
 | 
			
		||||
        expressions = string.Empty; // 初始化报警约束表达式为空字符串
 | 
			
		||||
        text = string.Empty; // 初始化报警文本为空字符串
 | 
			
		||||
 | 
			
		||||
        if (tag?.Value == null) // 检查变量是否为null或其值为null
 | 
			
		||||
        {
 | 
			
		||||
            return null; // 如果是,则返回null
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 检查是否启用了高高报警功能,并且变量的值大于高高报警的限制值
 | 
			
		||||
        if (tag.HHAlarmEnable && tag.Value.ToDecimal() > tag.HHAlarmCode.ToDecimal())
 | 
			
		||||
        {
 | 
			
		||||
            limit = tag.HHAlarmCode.ToString()!; // 将报警限制值设置为高高报警的限制值
 | 
			
		||||
            expressions = tag.HHRestrainExpressions!; // 获取高高报警的约束表达式
 | 
			
		||||
            text = tag.HHAlarmText!; // 获取高高报警时的报警文本
 | 
			
		||||
            return AlarmTypeEnum.HH; // 返回高高报警类型枚举
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 检查是否启用了高报警功能,并且变量的值大于高报警的限制值
 | 
			
		||||
        if (tag.HAlarmEnable && tag.Value.ToDecimal() > tag.HAlarmCode.ToDecimal())
 | 
			
		||||
        {
 | 
			
		||||
            limit = tag.HAlarmCode.ToString()!; // 将报警限制值设置为高报警的限制值
 | 
			
		||||
            expressions = tag.HRestrainExpressions!; // 获取高报警的约束表达式
 | 
			
		||||
            text = tag.HAlarmText!; // 获取高报警时的报警文本
 | 
			
		||||
            return AlarmTypeEnum.H; // 返回高报警类型枚举
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        // 检查是否启用了低低报警功能,并且变量的值小于低低报警的限制值
 | 
			
		||||
        if (tag.LLAlarmEnable && tag.Value.ToDecimal() < tag.LLAlarmCode.ToDecimal())
 | 
			
		||||
        {
 | 
			
		||||
            limit = tag.LLAlarmCode.ToString()!; // 将报警限制值设置为低低报警的限制值
 | 
			
		||||
            expressions = tag.LLRestrainExpressions!; // 获取低低报警的约束表达式
 | 
			
		||||
            text = tag.LLAlarmText!; // 获取低低报警时的报警文本
 | 
			
		||||
            return AlarmTypeEnum.LL; // 返回低低报警类型枚举
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        // 检查是否启用了低报警功能,并且变量的值小于低报警的限制值
 | 
			
		||||
        if (tag.LAlarmEnable && tag.Value.ToDecimal() < tag.LAlarmCode.ToDecimal())
 | 
			
		||||
        {
 | 
			
		||||
            limit = tag.LAlarmCode.ToString()!; // 将报警限制值设置为低报警的限制值
 | 
			
		||||
            expressions = tag.LRestrainExpressions!; // 获取低报警的约束表达式
 | 
			
		||||
            text = tag.LAlarmText!; // 获取低报警时的报警文本
 | 
			
		||||
            return AlarmTypeEnum.L; // 返回低报警类型枚举
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return null; // 如果不符合任何报警条件,则返回null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 对变量进行报警分析,并根据需要触发相应的报警事件或恢复事件。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="item">要进行报警分析的变量</param>
 | 
			
		||||
    private static void AlarmAnalysis(VariableRuntime item)
 | 
			
		||||
    {
 | 
			
		||||
        string limit; // 报警限制值
 | 
			
		||||
        string ex; // 报警约束表达式
 | 
			
		||||
        string text; // 报警文本
 | 
			
		||||
        AlarmTypeEnum? alarmEnum; // 报警类型枚举
 | 
			
		||||
        int delay = item.AlarmDelay; // 获取报警延迟时间
 | 
			
		||||
 | 
			
		||||
        // 检查变量的数据类型
 | 
			
		||||
        if (item.Value?.GetType() == typeof(bool))
 | 
			
		||||
        {
 | 
			
		||||
            // 如果数据类型为布尔型,则调用GetBoolAlarmCode方法获取布尔型报警类型及相关信息
 | 
			
		||||
            alarmEnum = GetBoolAlarmCode(item, out limit, out ex, out text);
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            // 如果数据类型为非布尔型,则调用GetDecimalAlarmDegree方法获取数值型报警类型及相关信息
 | 
			
		||||
            alarmEnum = GetDecimalAlarmDegree(item, out limit, out ex, out text);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 如果未获取到报警类型,则尝试获取自定义报警类型
 | 
			
		||||
        if (alarmEnum == null)
 | 
			
		||||
        {
 | 
			
		||||
            alarmEnum = GetCustomAlarmDegree(item, out limit, out ex, out text);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (alarmEnum == null)
 | 
			
		||||
        {
 | 
			
		||||
            // 如果仍未获取到报警类型,则触发需恢复报警事件(如果存在)
 | 
			
		||||
            AlarmChange(item, null, text, EventTypeEnum.Finish, alarmEnum, delay);
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            // 如果获取到了报警类型,则需触发报警事件或更新报警状态
 | 
			
		||||
 | 
			
		||||
            if (!string.IsNullOrEmpty(ex))
 | 
			
		||||
            {
 | 
			
		||||
                // 如果存在报警约束表达式,则计算表达式结果,以确定是否触发报警事件
 | 
			
		||||
                var data = ex.GetExpressionsResult(item.Value, item.DeviceRuntime?.Driver?.LogMessage);
 | 
			
		||||
                if (data is bool result)
 | 
			
		||||
                {
 | 
			
		||||
                    if (result)
 | 
			
		||||
                    {
 | 
			
		||||
                        // 如果表达式结果为true,则触发报警事件
 | 
			
		||||
                        AlarmChange(item, limit, text, EventTypeEnum.Alarm, alarmEnum, delay);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                // 如果不存在报警约束表达式,则直接触发报警事件
 | 
			
		||||
                AlarmChange(item, limit, text, EventTypeEnum.Alarm, alarmEnum, delay);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 根据报警事件类型进行相应的处理操作,包括触发报警事件或更新报警状态。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="item">要处理的变量</param>
 | 
			
		||||
    /// <param name="limit">报警限制值</param>
 | 
			
		||||
    /// <param name="text">报警文本</param>
 | 
			
		||||
    /// <param name="eventEnum">报警事件类型枚举</param>
 | 
			
		||||
    /// <param name="alarmEnum">报警类型枚举</param>
 | 
			
		||||
    /// <param name="delay">报警延时</param>
 | 
			
		||||
    private static void AlarmChange(VariableRuntime item, object limit, string text, EventTypeEnum eventEnum, AlarmTypeEnum? alarmEnum, int delay)
 | 
			
		||||
    {
 | 
			
		||||
        bool changed = false;
 | 
			
		||||
        if (eventEnum == EventTypeEnum.Finish)
 | 
			
		||||
        {
 | 
			
		||||
            // 如果是需恢复报警事件
 | 
			
		||||
            // 如果实时报警列表中不存在该变量,则直接返回
 | 
			
		||||
            if (!GlobalData.RealAlarmIdVariables.ContainsKey(item.Id))
 | 
			
		||||
            {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        else if (eventEnum == EventTypeEnum.Alarm)
 | 
			
		||||
        {
 | 
			
		||||
            // 如果是触发报警事件
 | 
			
		||||
            // 在实时报警列表中查找该变量
 | 
			
		||||
            if (GlobalData.RealAlarmIdVariables.TryGetValue(item.Id, out var variable))
 | 
			
		||||
            {
 | 
			
		||||
                // 如果变量已经处于相同的报警类型,则直接返回
 | 
			
		||||
                if (item.AlarmType == alarmEnum)
 | 
			
		||||
                    return;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 更新变量的报警信息和事件时间
 | 
			
		||||
        if (eventEnum == EventTypeEnum.Alarm)
 | 
			
		||||
        {
 | 
			
		||||
            var now = DateTime.Now;
 | 
			
		||||
            //添加报警延时策略
 | 
			
		||||
            if (delay > 0)
 | 
			
		||||
            {
 | 
			
		||||
                if (item.EventType != EventTypeEnum.Alarm && item.EventType != EventTypeEnum.Prepare)
 | 
			
		||||
                {
 | 
			
		||||
                    item.EventType = EventTypeEnum.Prepare;//准备报警
 | 
			
		||||
                    item.PrepareEventTime = now;
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    if (item.EventType == EventTypeEnum.Prepare)
 | 
			
		||||
                    {
 | 
			
		||||
                        if ((now - item.PrepareEventTime!.Value).TotalSeconds > delay)
 | 
			
		||||
                        {
 | 
			
		||||
                            //超过延时时间,触发报警
 | 
			
		||||
                            item.EventType = EventTypeEnum.Alarm;
 | 
			
		||||
                            item.AlarmTime = now;
 | 
			
		||||
                            item.EventTime = now;
 | 
			
		||||
                            item.AlarmType = alarmEnum;
 | 
			
		||||
                            item.AlarmLimit = limit.ToString();
 | 
			
		||||
                            item.AlarmCode = item.Value.ToString();
 | 
			
		||||
                            item.RecoveryCode = string.Empty;
 | 
			
		||||
                            item.AlarmText = text;
 | 
			
		||||
                            item.PrepareEventTime = null;
 | 
			
		||||
 | 
			
		||||
                            changed = true;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    else if (item.EventType == EventTypeEnum.Alarm && item.AlarmType != alarmEnum)
 | 
			
		||||
                    {
 | 
			
		||||
                        //报警类型改变,重新计时
 | 
			
		||||
                        if (item.PrepareEventTime == null)
 | 
			
		||||
                            item.PrepareEventTime = now;
 | 
			
		||||
                        if ((now - item.PrepareEventTime!.Value).TotalSeconds > delay)
 | 
			
		||||
                        {
 | 
			
		||||
                            //超过延时时间,触发报警
 | 
			
		||||
                            item.EventType = EventTypeEnum.Alarm;
 | 
			
		||||
                            item.AlarmTime = now;
 | 
			
		||||
                            item.EventTime = now;
 | 
			
		||||
                            item.AlarmType = alarmEnum;
 | 
			
		||||
                            item.AlarmLimit = limit.ToString();
 | 
			
		||||
                            item.AlarmCode = item.Value.ToString();
 | 
			
		||||
                            item.RecoveryCode = string.Empty;
 | 
			
		||||
                            item.AlarmText = text;
 | 
			
		||||
                            item.PrepareEventTime = null;
 | 
			
		||||
                            changed = true;
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                // 如果是触发报警事件
 | 
			
		||||
                item.EventType = eventEnum;
 | 
			
		||||
                item.AlarmTime = now;
 | 
			
		||||
                item.EventTime = now;
 | 
			
		||||
                item.AlarmType = alarmEnum;
 | 
			
		||||
                item.AlarmLimit = limit.ToString();
 | 
			
		||||
                item.AlarmCode = item.Value.ToString();
 | 
			
		||||
                item.RecoveryCode = string.Empty;
 | 
			
		||||
                item.AlarmText = text;
 | 
			
		||||
                changed = true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
        else if (eventEnum == EventTypeEnum.Finish)
 | 
			
		||||
        {
 | 
			
		||||
            // 如果是需恢复报警事件
 | 
			
		||||
            // 获取旧的报警信息
 | 
			
		||||
            if (GlobalData.RealAlarmIdVariables.TryGetValue(item.Id, out var oldAlarm))
 | 
			
		||||
            {
 | 
			
		||||
                item.AlarmType = oldAlarm.AlarmType;
 | 
			
		||||
                item.EventType = eventEnum;
 | 
			
		||||
                item.AlarmLimit = oldAlarm.AlarmLimit;
 | 
			
		||||
                item.AlarmCode = oldAlarm.AlarmCode;
 | 
			
		||||
                item.RecoveryCode = item.Value.ToString();
 | 
			
		||||
                item.AlarmText = oldAlarm.AlarmText;
 | 
			
		||||
                item.EventTime = DateTime.Now;
 | 
			
		||||
            }
 | 
			
		||||
            changed = true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 触发报警变化事件
 | 
			
		||||
        if (changed)
 | 
			
		||||
        {
 | 
			
		||||
            if (item.EventType == EventTypeEnum.Alarm)
 | 
			
		||||
            {
 | 
			
		||||
                // 如果是触发报警事件
 | 
			
		||||
                //lock (GlobalData. RealAlarmVariables)
 | 
			
		||||
                {
 | 
			
		||||
                    // 从实时报警列表中移除旧的报警信息,并添加新的报警信息
 | 
			
		||||
                    GlobalData.RealAlarmIdVariables.AddOrUpdate(item.Id, a => item.AdaptAlarmVariable(), (a, b) => item.AdaptAlarmVariable());
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            else if (item.EventType == EventTypeEnum.Finish)
 | 
			
		||||
            {
 | 
			
		||||
                // 如果是需恢复报警事件,则从实时报警列表中移除该变量
 | 
			
		||||
                GlobalData.RealAlarmIdVariables.TryRemove(item.Id, out _);
 | 
			
		||||
                //GlobalData.RealAlarmIdVariables.AddOrUpdate(item.Id, a => item.AdaptAlarmVariable(), (a, b) => item.AdaptAlarmVariable());
 | 
			
		||||
            }
 | 
			
		||||
            GlobalData.AlarmChange(item.AdaptAlarmVariable());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void ConfirmAlarm(long variableId)
 | 
			
		||||
    {
 | 
			
		||||
        // 如果是确认报警事件
 | 
			
		||||
        if (GlobalData.AlarmEnableIdVariables.TryGetValue(variableId, out var variableRuntime))
 | 
			
		||||
        {
 | 
			
		||||
            variableRuntime.EventType = EventTypeEnum.Confirm;
 | 
			
		||||
            variableRuntime.EventTime = DateTime.Now;
 | 
			
		||||
            GlobalData.RealAlarmIdVariables.AddOrUpdate(variableId, a => variableRuntime.AdaptAlarmVariable(), (a, b) => variableRuntime.AdaptAlarmVariable());
 | 
			
		||||
            GlobalData.AlarmChange(variableRuntime.AdaptAlarmVariable());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #endregion 核心实现
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 执行工作任务,对设备变量进行报警分析。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="state"></param>
 | 
			
		||||
    /// <param name="cancellation">取消任务的 CancellationToken</param>
 | 
			
		||||
    private void DoWork(object? state, CancellationToken cancellation)
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            if (!GlobalData.StartBusinessChannelEnable)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            //Stopwatch stopwatch = Stopwatch.StartNew();
 | 
			
		||||
            // 遍历设备变量列表
 | 
			
		||||
 | 
			
		||||
            if (!GlobalData.AlarmEnableIdVariables.IsEmpty)
 | 
			
		||||
            {
 | 
			
		||||
                var list = GlobalData.AlarmEnableIdVariables.Select(a => a.Value).ToArray();
 | 
			
		||||
                list.ParallelForEach((item, state, index) =>
 | 
			
		||||
            {
 | 
			
		||||
                {
 | 
			
		||||
                    // 如果取消请求已经被触发,则结束任务
 | 
			
		||||
                    if (cancellation.IsCancellationRequested)
 | 
			
		||||
                        return;
 | 
			
		||||
 | 
			
		||||
                    // 如果该变量的报警功能未启用,则跳过该变量
 | 
			
		||||
                    if (!item.AlarmEnable)
 | 
			
		||||
                        return;
 | 
			
		||||
 | 
			
		||||
                    // 如果该变量离线,则跳过该变量
 | 
			
		||||
                    if (!item.IsOnline)
 | 
			
		||||
                        return;
 | 
			
		||||
 | 
			
		||||
                    // 对该变量进行报警分析
 | 
			
		||||
                    AlarmAnalysis(item);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                //if (scheduledTask.Period != 5000)
 | 
			
		||||
                //    scheduledTask.Change(0, 5000); // 如果没有启用报警的变量,则设置下次执行时间为5秒后
 | 
			
		||||
                scheduledTask.SetNext(5000); // 如果没有启用报警的变量,则设置下次执行时间为5秒后
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            //stopwatch.Stop();
 | 
			
		||||
            //_logger.LogInformation("报警分析耗时:" + stopwatch.ElapsedMilliseconds + "ms");
 | 
			
		||||
        }
 | 
			
		||||
        catch (OperationCanceledException)
 | 
			
		||||
        {
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            _logger.LogWarning(ex, "Alarm analysis fail");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -644,7 +644,7 @@ internal sealed class DeviceThreadManage : IAsyncDisposable, IDeviceThreadManage
 | 
			
		||||
 | 
			
		||||
            //找出新的通道,添加设备线程
 | 
			
		||||
 | 
			
		||||
            if (!GlobalData.Channels.TryGetValue(newDeviceRuntime.ChannelId, out var channelRuntime))
 | 
			
		||||
            if (!GlobalData.IdChannels.TryGetValue(newDeviceRuntime.ChannelId, out var channelRuntime))
 | 
			
		||||
                LogMessage?.LogWarning($"device {newDeviceRuntime.Name} cannot found channel with id{newDeviceRuntime.ChannelId}");
 | 
			
		||||
 | 
			
		||||
            newDeviceRuntime.Init(channelRuntime);
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,7 @@ internal static class ManageHelper
 | 
			
		||||
 | 
			
		||||
    public static void CheckChannelCount(int addCount)
 | 
			
		||||
    {
 | 
			
		||||
        var data = GlobalData.Channels.Count + addCount;
 | 
			
		||||
        var data = GlobalData.IdChannels.Count + addCount;
 | 
			
		||||
        ProAuthentication.TryGetAuthorizeInfo(out var authorizeInfo);
 | 
			
		||||
        if (data > ManageHelper.ChannelThreadOptions.MaxChannelCount || data > authorizeInfo?.MaxChannelCount)
 | 
			
		||||
        {
 | 
			
		||||
 
 | 
			
		||||
@@ -15,9 +15,9 @@ namespace ThingsGateway.Management;
 | 
			
		||||
 | 
			
		||||
public interface IRedundancyHostedService : IHostedService
 | 
			
		||||
{
 | 
			
		||||
    Task<OperResult> StartRedundancyTaskAsync();
 | 
			
		||||
    Task StopRedundancyTaskAsync();
 | 
			
		||||
    ValueTask ForcedSync(CancellationToken cancellationToken = default);
 | 
			
		||||
    Task StartTaskAsync(CancellationToken cancellationToken);
 | 
			
		||||
    Task StopTaskAsync();
 | 
			
		||||
    Task ForcedSync(CancellationToken cancellationToken = default);
 | 
			
		||||
 | 
			
		||||
    public TextFileLogger TextLogger { get; }
 | 
			
		||||
    public string LogPath { get; }
 | 
			
		||||
 
 | 
			
		||||
@@ -9,640 +9,40 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using Microsoft.Extensions.Hosting;
 | 
			
		||||
using Microsoft.Extensions.Localization;
 | 
			
		||||
using Microsoft.Extensions.Logging;
 | 
			
		||||
 | 
			
		||||
using Newtonsoft.Json.Linq;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.Extension.Generic;
 | 
			
		||||
using ThingsGateway.Gateway.Application;
 | 
			
		||||
using ThingsGateway.NewLife;
 | 
			
		||||
 | 
			
		||||
using TouchSocket.Core;
 | 
			
		||||
using TouchSocket.Dmtp;
 | 
			
		||||
using TouchSocket.Dmtp.Rpc;
 | 
			
		||||
using TouchSocket.Rpc;
 | 
			
		||||
using TouchSocket.Sockets;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Management;
 | 
			
		||||
 | 
			
		||||
internal sealed class RedundancyHostedService : BackgroundService, IRedundancyHostedService, IRpcDriver
 | 
			
		||||
internal sealed class RedundancyHostedService : BackgroundService, IRedundancyHostedService
 | 
			
		||||
{
 | 
			
		||||
    private readonly ILogger _logger;
 | 
			
		||||
    private readonly IRedundancyService _redundancyService;
 | 
			
		||||
    private readonly GatewayRedundantSerivce _gatewayRedundantSerivce;
 | 
			
		||||
    /// <inheritdoc cref="RedundancyHostedService"/>
 | 
			
		||||
    public RedundancyHostedService(ILogger<RedundancyHostedService> logger, IStringLocalizer<RedundancyHostedService> localizer, IRedundancyService redundancyService, GatewayRedundantSerivce gatewayRedundantSerivce)
 | 
			
		||||
    public RedundancyHostedService(ILogger<RedundancyHostedService> logger)
 | 
			
		||||
    {
 | 
			
		||||
        _logger = logger;
 | 
			
		||||
        Localizer = localizer;
 | 
			
		||||
        _gatewayRedundantSerivce = gatewayRedundantSerivce;
 | 
			
		||||
        // 创建新的文件日志记录器,并设置日志级别为 Trace
 | 
			
		||||
        LogPath = "Logs/RedundancyLog";
 | 
			
		||||
        TextLogger = TextFileLogger.GetMultipleFileLogger(LogPath);
 | 
			
		||||
        TextLogger.LogLevel = TouchSocket.Core.LogLevel.Trace;
 | 
			
		||||
 | 
			
		||||
        _redundancyService = redundancyService;
 | 
			
		||||
        RedundancyTask = new RedundancyTask(_logger);
 | 
			
		||||
    }
 | 
			
		||||
    public override void Dispose()
 | 
			
		||||
    {
 | 
			
		||||
        TextLogger.Dispose();
 | 
			
		||||
        base.Dispose();
 | 
			
		||||
    }
 | 
			
		||||
    private IStringLocalizer Localizer { get; }
 | 
			
		||||
    private DoTask RedundancyTask { get; set; }
 | 
			
		||||
    private WaitLock RedundancyRestartLock { get; } = new();
 | 
			
		||||
    public ILog LogMessage { get; set; }
 | 
			
		||||
    public TextFileLogger TextLogger { get; }
 | 
			
		||||
    public string LogPath { get; }
 | 
			
		||||
    private TcpDmtpClient TcpDmtpClient;
 | 
			
		||||
    private TcpDmtpService TcpDmtpService;
 | 
			
		||||
    private async Task<TcpDmtpClient> GetTcpDmtpClient(RedundancyOptions redundancy)
 | 
			
		||||
    {
 | 
			
		||||
        var log = new LoggerGroup() { LogLevel = TouchSocket.Core.LogLevel.Trace };
 | 
			
		||||
        log?.AddLogger(new EasyLogger(Log_Out) { LogLevel = TouchSocket.Core.LogLevel.Trace });
 | 
			
		||||
        log?.AddLogger(TextLogger);
 | 
			
		||||
        LogMessage = log;
 | 
			
		||||
        var tcpDmtpClient = new TcpDmtpClient();
 | 
			
		||||
        var config = new TouchSocketConfig()
 | 
			
		||||
               .SetRemoteIPHost(redundancy.MasterUri)
 | 
			
		||||
               .SetAdapterOption(new AdapterOption() { MaxPackageSize = 1024 * 1024 * 1024 })
 | 
			
		||||
               .SetDmtpOption(new DmtpOption() { VerifyToken = redundancy.VerifyToken })
 | 
			
		||||
               .ConfigureContainer(a =>
 | 
			
		||||
               {
 | 
			
		||||
                   a.AddLogger(LogMessage);
 | 
			
		||||
                   a.AddRpcStore(store =>
 | 
			
		||||
                   {
 | 
			
		||||
                       store.RegisterServer(new ReverseCallbackServer(this));
 | 
			
		||||
                   });
 | 
			
		||||
               })
 | 
			
		||||
               .ConfigurePlugins(a =>
 | 
			
		||||
               {
 | 
			
		||||
                   a.UseDmtpRpc();
 | 
			
		||||
                   a.UseDmtpHeartbeat()//使用Dmtp心跳
 | 
			
		||||
                   .SetTick(TimeSpan.FromMilliseconds(redundancy.HeartbeatInterval))
 | 
			
		||||
                   .SetMaxFailCount(redundancy.MaxErrorCount);
 | 
			
		||||
               });
 | 
			
		||||
    private RedundancyTask RedundancyTask;
 | 
			
		||||
 | 
			
		||||
        await tcpDmtpClient.SetupAsync(config).ConfigureAwait(false);
 | 
			
		||||
        return tcpDmtpClient;
 | 
			
		||||
    }
 | 
			
		||||
    public TextFileLogger TextLogger => RedundancyTask.TextLogger;
 | 
			
		||||
 | 
			
		||||
    private async Task<TcpDmtpService> GetTcpDmtpService(RedundancyOptions redundancy)
 | 
			
		||||
    {
 | 
			
		||||
        var log = new LoggerGroup() { LogLevel = TouchSocket.Core.LogLevel.Trace };
 | 
			
		||||
        log?.AddLogger(new EasyLogger(Log_Out) { LogLevel = TouchSocket.Core.LogLevel.Trace });
 | 
			
		||||
        log?.AddLogger(TextLogger);
 | 
			
		||||
        LogMessage = log;
 | 
			
		||||
        var tcpDmtpService = new TcpDmtpService();
 | 
			
		||||
        var config = new TouchSocketConfig()
 | 
			
		||||
               .SetListenIPHosts(redundancy.MasterUri)
 | 
			
		||||
               .SetAdapterOption(new AdapterOption() { MaxPackageSize = 1024 * 1024 * 1024 })
 | 
			
		||||
               .SetDmtpOption(new DmtpOption() { VerifyToken = redundancy.VerifyToken })
 | 
			
		||||
               .ConfigureContainer(a =>
 | 
			
		||||
               {
 | 
			
		||||
                   a.AddLogger(LogMessage);
 | 
			
		||||
                   a.AddRpcStore(store =>
 | 
			
		||||
                   {
 | 
			
		||||
                       store.RegisterServer(new ReverseCallbackServer(this));
 | 
			
		||||
                   });
 | 
			
		||||
               })
 | 
			
		||||
               .ConfigurePlugins(a =>
 | 
			
		||||
               {
 | 
			
		||||
                   a.UseDmtpRpc();
 | 
			
		||||
                   a.UseDmtpHeartbeat()//使用Dmtp心跳
 | 
			
		||||
                   .SetTick(TimeSpan.FromMilliseconds(redundancy.HeartbeatInterval))
 | 
			
		||||
                   .SetMaxFailCount(redundancy.MaxErrorCount);
 | 
			
		||||
               });
 | 
			
		||||
    public string LogPath => RedundancyTask.LogPath;
 | 
			
		||||
 | 
			
		||||
        await tcpDmtpService.SetupAsync(config).ConfigureAwait(false);
 | 
			
		||||
        return tcpDmtpService;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void Log_Out(TouchSocket.Core.LogLevel logLevel, object source, string message, Exception exception)
 | 
			
		||||
    {
 | 
			
		||||
        _logger?.Log_Out(logLevel, source, message, exception);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static Task RestartAsync()
 | 
			
		||||
    {
 | 
			
		||||
        return GlobalData.ChannelRuntimeService.RestartChannelAsync(GlobalData.ReadOnlyChannels.Values);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 主站
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    private async Task DoMasterWork(object? state, CancellationToken stoppingToken)
 | 
			
		||||
    {
 | 
			
		||||
        // 延迟一段时间,避免过于频繁地执行任务
 | 
			
		||||
        await Task.Delay(500, stoppingToken).ConfigureAwait(false);
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            bool online = false;
 | 
			
		||||
            var waitInvoke = new DmtpInvokeOption()
 | 
			
		||||
            {
 | 
			
		||||
                FeedbackType = FeedbackType.WaitInvoke,
 | 
			
		||||
                Token = stoppingToken,
 | 
			
		||||
                Timeout = 30000,
 | 
			
		||||
                SerializationType = SerializationType.Json,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (TcpDmtpService.Clients.Count != 0)
 | 
			
		||||
                {
 | 
			
		||||
                    online = true;
 | 
			
		||||
                }
 | 
			
		||||
                // 如果 online 为 true,表示设备在线
 | 
			
		||||
                if (online)
 | 
			
		||||
                {
 | 
			
		||||
                    var deviceRunTimes = GlobalData.ReadOnlyIdDevices.Where(a => a.Value.IsCollect == true).Select(a => a.Value).AdaptListDeviceDataWithValue();
 | 
			
		||||
 | 
			
		||||
                    foreach (var item in TcpDmtpService.Clients)
 | 
			
		||||
                    {
 | 
			
		||||
                        // 将 GlobalData.CollectDevices 和 GlobalData.Variables 同步到从站
 | 
			
		||||
                        await item.GetDmtpRpcActor().InvokeAsync(
 | 
			
		||||
                                         nameof(ReverseCallbackServer.UpData), null, waitInvoke, deviceRunTimes).ConfigureAwait(false);
 | 
			
		||||
                        LogMessage?.LogTrace($"{item.GetIPPort()} Update StandbyStation data success");
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                // 输出警告日志,指示同步数据到从站时发生错误
 | 
			
		||||
                LogMessage?.LogWarning(ex, "Synchronize data to standby site error");
 | 
			
		||||
            }
 | 
			
		||||
            await Task.Delay(RedundancyOptions.SyncInterval, stoppingToken).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
        catch (OperationCanceledException)
 | 
			
		||||
        {
 | 
			
		||||
        }
 | 
			
		||||
        catch (ObjectDisposedException)
 | 
			
		||||
        {
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            LogMessage?.LogWarning(ex, "Execute");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 从站
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    private async Task DoSlaveWork(object? state, CancellationToken stoppingToken)
 | 
			
		||||
    {
 | 
			
		||||
        // 延迟一段时间,避免过于频繁地执行任务
 | 
			
		||||
        await Task.Delay(5000, stoppingToken).ConfigureAwait(false);
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            bool online = false;
 | 
			
		||||
            var waitInvoke = new DmtpInvokeOption()
 | 
			
		||||
            {
 | 
			
		||||
                FeedbackType = FeedbackType.WaitInvoke,
 | 
			
		||||
                Token = stoppingToken,
 | 
			
		||||
                Timeout = 30000,
 | 
			
		||||
                SerializationType = SerializationType.Json,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                await TcpDmtpClient.TryConnectAsync().ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                {
 | 
			
		||||
                    // 初始化读取错误计数器
 | 
			
		||||
                    var readErrorCount = 0;
 | 
			
		||||
                    // 当读取错误次数小于最大错误计数时循环执行
 | 
			
		||||
                    while (readErrorCount < RedundancyOptions.MaxErrorCount)
 | 
			
		||||
                    {
 | 
			
		||||
                        try
 | 
			
		||||
                        {
 | 
			
		||||
                            // 发送 Ping 请求以检查设备是否在线,超时时间为 10000 毫秒
 | 
			
		||||
                            online = await TcpDmtpClient.PingAsync(10000).ConfigureAwait(false);
 | 
			
		||||
                            if (online)
 | 
			
		||||
                                break;
 | 
			
		||||
                            else
 | 
			
		||||
                            {
 | 
			
		||||
                                readErrorCount++;
 | 
			
		||||
                                await Task.Delay(RedundancyOptions.SyncInterval, stoppingToken).ConfigureAwait(false);
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        catch
 | 
			
		||||
                        {
 | 
			
		||||
                            // 捕获异常,增加读取错误计数器
 | 
			
		||||
                            readErrorCount++;
 | 
			
		||||
                            await Task.Delay(RedundancyOptions.SyncInterval, stoppingToken).ConfigureAwait(false);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                // 如果设备不在线
 | 
			
		||||
                if (!online)
 | 
			
		||||
                {
 | 
			
		||||
                    // 无法获取状态,启动本机
 | 
			
		||||
                    await ActiveAsync().ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    // 如果设备在线
 | 
			
		||||
                    LogMessage?.LogTrace($"Ping ActiveStation {RedundancyOptions.MasterUri} success");
 | 
			
		||||
                    await StandbyAsync().ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        catch (OperationCanceledException)
 | 
			
		||||
        {
 | 
			
		||||
        }
 | 
			
		||||
        catch (ObjectDisposedException)
 | 
			
		||||
        {
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            LogMessage?.LogWarning(ex, "Execute");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async ValueTask ForcedSync(CancellationToken cancellationToken = default)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            bool online = false;
 | 
			
		||||
            var waitInvoke = new DmtpInvokeOption()
 | 
			
		||||
            {
 | 
			
		||||
                FeedbackType = FeedbackType.WaitInvoke,
 | 
			
		||||
                Token = cancellationToken,
 | 
			
		||||
                Timeout = 30000,
 | 
			
		||||
                SerializationType = SerializationType.Json,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                online = (await TcpDmtpClient.TryConnectAsync().ConfigureAwait(false)).ResultCode == ResultCode.Success;
 | 
			
		||||
 | 
			
		||||
                // 如果 online 为 true,表示设备在线
 | 
			
		||||
                if (online)
 | 
			
		||||
                {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                    // 将 GlobalData.CollectDevices 和 GlobalData.Variables 同步到从站
 | 
			
		||||
                    var data = await TcpDmtpClient.GetDmtpRpcActor().InvokeTAsync<List<DataWithDatabase>>(
 | 
			
		||||
                                       nameof(ReverseCallbackServer.GetData), waitInvoke).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                    await GlobalData.ChannelRuntimeService.CopyAsync(data.Select(a => a.Channel).ToList(), data.SelectMany(a => a.DeviceVariables).ToDictionary(a => a.Device, a => a.Variables), true, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                    LogMessage?.LogTrace($"ForcedSync data success");
 | 
			
		||||
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                // 输出警告日志,指示同步数据到从站时发生错误
 | 
			
		||||
                LogMessage?.LogWarning(ex, "ForcedSync data error");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        catch (OperationCanceledException)
 | 
			
		||||
        {
 | 
			
		||||
        }
 | 
			
		||||
        catch (ObjectDisposedException)
 | 
			
		||||
        {
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            LogMessage?.LogWarning(ex, "Execute");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    private WaitLock _switchLock = new();
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
 | 
			
		||||
    {
 | 
			
		||||
        await Task.Yield();
 | 
			
		||||
        await StartRedundancyTaskAsync().ConfigureAwait(false);
 | 
			
		||||
        await RedundancyTask.StartTaskAsync(stoppingToken).ConfigureAwait(false);
 | 
			
		||||
    }
 | 
			
		||||
    public Task StartTaskAsync(CancellationToken cancellationToken) => RedundancyTask.StartTaskAsync(cancellationToken);
 | 
			
		||||
    public Task StopTaskAsync() => RedundancyTask.StopTaskAsync();
 | 
			
		||||
 | 
			
		||||
    public async Task<OperResult> StartRedundancyTaskAsync()
 | 
			
		||||
    public Task ForcedSync(CancellationToken cancellationToken = default) => RedundancyTask.ForcedSync(cancellationToken);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public override async Task StopAsync(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            await RedundancyRestartLock.WaitAsync().ConfigureAwait(false); // 等待获取锁,以确保只有一个线程可以执行以下代码
 | 
			
		||||
 | 
			
		||||
            if (RedundancyTask != null)
 | 
			
		||||
            {
 | 
			
		||||
                await RedundancyTask.StopAsync(TimeSpan.FromSeconds(30)).ConfigureAwait(false); // 停止现有任务,等待最多30秒钟
 | 
			
		||||
            }
 | 
			
		||||
            await BeforeStartAsync().ConfigureAwait(false);
 | 
			
		||||
            if (RedundancyOptions?.Enable == true)
 | 
			
		||||
            {
 | 
			
		||||
                if (RedundancyOptions.IsMaster)
 | 
			
		||||
                {
 | 
			
		||||
                    RedundancyTask = new DoTask(DoMasterWork, LogMessage); // 创建新的任务
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    RedundancyTask = new DoTask(DoSlaveWork, LogMessage); // 创建新的任务
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                RedundancyTask?.Start(default); // 启动任务
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return new();
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            LogMessage?.LogError(ex, "Start"); // 记录错误日志
 | 
			
		||||
            return new(ex);
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            RedundancyRestartLock.Release(); // 释放锁
 | 
			
		||||
        }
 | 
			
		||||
        await RedundancyTask.DisposeAsync().ConfigureAwait(false);
 | 
			
		||||
        await base.StopAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private RedundancyOptions RedundancyOptions;
 | 
			
		||||
 | 
			
		||||
    private async Task BeforeStartAsync()
 | 
			
		||||
    {
 | 
			
		||||
        RedundancyOptions = (await _redundancyService.GetRedundancyAsync().ConfigureAwait(false)).AdaptRedundancyOptions();
 | 
			
		||||
 | 
			
		||||
        if (RedundancyOptions?.Enable == true)
 | 
			
		||||
        {
 | 
			
		||||
            if (RedundancyOptions.IsMaster)
 | 
			
		||||
            {
 | 
			
		||||
                TcpDmtpService = await GetTcpDmtpService(RedundancyOptions).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                await TcpDmtpService.StartAsync().ConfigureAwait(false);//启动
 | 
			
		||||
                await ActiveAsync().ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                TcpDmtpClient = await GetTcpDmtpClient(RedundancyOptions).ConfigureAwait(false);
 | 
			
		||||
                await StandbyAsync().ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            await ActiveAsync().ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    private bool first;
 | 
			
		||||
    private async Task StandbyAsync()
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            await _switchLock.WaitAsync().ConfigureAwait(false);
 | 
			
		||||
            if (_gatewayRedundantSerivce.StartCollectChannelEnable)
 | 
			
		||||
            {
 | 
			
		||||
                // 输出日志,指示主站已恢复,从站将切换到备用状态
 | 
			
		||||
                if (first)
 | 
			
		||||
                    LogMessage?.Warning("Master site has recovered, local machine (standby) will switch to standby state");
 | 
			
		||||
 | 
			
		||||
                // 将 IsStart 设置为 false,表示当前设备为从站,切换到备用状态
 | 
			
		||||
                _gatewayRedundantSerivce.StartCollectChannelEnable = false;
 | 
			
		||||
                _gatewayRedundantSerivce.StartBusinessChannelEnable = RedundancyOptions?.IsStartBusinessDevice ?? false;
 | 
			
		||||
                await RestartAsync().ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            _switchLock.Release();
 | 
			
		||||
            first = true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task ActiveAsync()
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            await _switchLock.WaitAsync().ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            _gatewayRedundantSerivce.StartBusinessChannelEnable = true;
 | 
			
		||||
            if (!_gatewayRedundantSerivce.StartCollectChannelEnable)
 | 
			
		||||
            {
 | 
			
		||||
                // 输出日志,指示无法连接冗余站点,本机将切换到正常状态
 | 
			
		||||
                if (first)
 | 
			
		||||
                    LogMessage?.Warning("Cannot connect to redundant site, local machine will switch to normal state");
 | 
			
		||||
                _gatewayRedundantSerivce.StartCollectChannelEnable = true;
 | 
			
		||||
                await RestartAsync().ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            _switchLock.Release();
 | 
			
		||||
            first = true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task StopRedundancyTaskAsync()
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            await RedundancyRestartLock.WaitAsync().ConfigureAwait(false); // 等待获取锁,以确保只有一个线程可以执行以下代码
 | 
			
		||||
 | 
			
		||||
            if (RedundancyTask != null)
 | 
			
		||||
            {
 | 
			
		||||
                await RedundancyTask.StopAsync(TimeSpan.FromSeconds(10)).ConfigureAwait(false); // 停止任务,等待最多10秒钟
 | 
			
		||||
            }
 | 
			
		||||
            if (TcpDmtpService != null)
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    await TcpDmtpService.StopAsync().ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                catch
 | 
			
		||||
                {
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            if (TcpDmtpClient != null)
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    await TcpDmtpClient.CloseAsync().ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                catch
 | 
			
		||||
                {
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            TcpDmtpService?.Dispose();
 | 
			
		||||
            TcpDmtpClient?.Dispose();
 | 
			
		||||
            RedundancyTask = null;
 | 
			
		||||
            TcpDmtpService = null;
 | 
			
		||||
            TcpDmtpClient = null;
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            LogMessage?.LogError(ex, "Stop"); // 记录错误日志
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            first = false;
 | 
			
		||||
            RedundancyRestartLock.Release(); // 释放锁
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 异步写入方法
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="writeInfoLists">要写入的变量及其对应的数据</param>
 | 
			
		||||
    /// <param name="cancellationToken">取消操作的通知</param>
 | 
			
		||||
    /// <returns>写入操作的结果字典</returns>
 | 
			
		||||
    public async ValueTask<Dictionary<string, Dictionary<string, IOperResult>>> InvokeMethodAsync(Dictionary<VariableRuntime, JToken> writeInfoLists, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        return (await Rpc(writeInfoLists, cancellationToken).ConfigureAwait(false)).ToDictionary(a => a.Key, a => a.Value.ToDictionary(b => b.Key, b => (IOperResult)b.Value));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async ValueTask<Dictionary<string, Dictionary<string, OperResult<object>>>> Rpc(Dictionary<VariableRuntime, JToken> writeInfoLists, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        Dictionary<string, Dictionary<string, OperResult<object>>> dataResult = new();
 | 
			
		||||
 | 
			
		||||
        Dictionary<string, Dictionary<string, string>> deviceDatas = new();
 | 
			
		||||
        foreach (var item in writeInfoLists)
 | 
			
		||||
        {
 | 
			
		||||
            if (deviceDatas.TryGetValue(item.Key.DeviceName ?? string.Empty, out var variableDatas))
 | 
			
		||||
            {
 | 
			
		||||
                variableDatas.Add(item.Key.Name, item.Value?.ToString() ?? string.Empty);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                deviceDatas.Add(item.Key.DeviceName ?? string.Empty, new());
 | 
			
		||||
                deviceDatas[item.Key.DeviceName ?? string.Empty].Add(item.Key.Name, item.Value?.ToString() ?? string.Empty);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (RedundancyOptions.IsMaster)
 | 
			
		||||
        {
 | 
			
		||||
            return NoOnline(dataResult, deviceDatas);
 | 
			
		||||
        }
 | 
			
		||||
        bool online = false;
 | 
			
		||||
        var waitInvoke = new DmtpInvokeOption()
 | 
			
		||||
        {
 | 
			
		||||
            FeedbackType = FeedbackType.WaitInvoke,
 | 
			
		||||
            Token = cancellationToken,
 | 
			
		||||
            Timeout = 30000,
 | 
			
		||||
            SerializationType = SerializationType.Json,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            if (!RedundancyOptions.IsMaster)
 | 
			
		||||
            {
 | 
			
		||||
                online = (await TcpDmtpClient.TryConnectAsync().ConfigureAwait(false)).ResultCode == ResultCode.Success;
 | 
			
		||||
 | 
			
		||||
                // 如果 online 为 true,表示设备在线
 | 
			
		||||
                if (online)
 | 
			
		||||
                {
 | 
			
		||||
                    // 将 GlobalData.CollectDevices 和 GlobalData.Variables 同步到从站
 | 
			
		||||
                    dataResult = await TcpDmtpClient.GetDmtpRpcActor().InvokeTAsync<Dictionary<string, Dictionary<string, OperResult<object>>>>(
 | 
			
		||||
                                       nameof(ReverseCallbackServer.Rpc), waitInvoke, deviceDatas).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                    LogMessage?.LogTrace($"Rpc success");
 | 
			
		||||
 | 
			
		||||
                    return dataResult;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                if (TcpDmtpService.Clients.Count != 0)
 | 
			
		||||
                {
 | 
			
		||||
                    online = true;
 | 
			
		||||
                }
 | 
			
		||||
                // 如果 online 为 true,表示设备在线
 | 
			
		||||
                if (online)
 | 
			
		||||
                {
 | 
			
		||||
                    foreach (var item in deviceDatas)
 | 
			
		||||
                    {
 | 
			
		||||
 | 
			
		||||
                        if (GlobalData.ReadOnlyDevices.TryGetValue(item.Key, out var device))
 | 
			
		||||
                        {
 | 
			
		||||
                            var key = device.Tag;
 | 
			
		||||
 | 
			
		||||
                            if (TcpDmtpService.TryGetClient(key, out var client))
 | 
			
		||||
                            {
 | 
			
		||||
                                try
 | 
			
		||||
                                {
 | 
			
		||||
 | 
			
		||||
                                    var data = await TcpDmtpClient.GetDmtpRpcActor().InvokeTAsync<Dictionary<string, Dictionary<string, OperResult<object>>>>(
 | 
			
		||||
                                                         nameof(ReverseCallbackServer.Rpc), waitInvoke, new Dictionary<string, Dictionary<string, string>>() { { item.Key, item.Value } }).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                                    dataResult.AddRange(data);
 | 
			
		||||
 | 
			
		||||
                                    continue;
 | 
			
		||||
                                }
 | 
			
		||||
                                catch (Exception ex)
 | 
			
		||||
                                {
 | 
			
		||||
                                    dataResult.TryAdd(item.Key, new Dictionary<string, OperResult<object>>());
 | 
			
		||||
 | 
			
		||||
                                    foreach (var vItem in item.Value)
 | 
			
		||||
                                    {
 | 
			
		||||
                                        dataResult[item.Key].Add(vItem.Key, new OperResult<object>(ex));
 | 
			
		||||
                                    }
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        dataResult.TryAdd(item.Key, new Dictionary<string, OperResult<object>>());
 | 
			
		||||
 | 
			
		||||
                        foreach (var vItem in item.Value)
 | 
			
		||||
                        {
 | 
			
		||||
                            dataResult[item.Key].Add(vItem.Key, new OperResult<object>("No online"));
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    LogMessage?.LogTrace($"Rpc success");
 | 
			
		||||
                    return dataResult;
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    LogMessage?.LogWarning("Rpc error, no client online");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return NoOnline(dataResult, deviceDatas);
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
        catch (OperationCanceledException)
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
            return NoOnline(dataResult, deviceDatas);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            // 输出警告日志,指示同步数据到从站时发生错误
 | 
			
		||||
            LogMessage?.LogWarning(ex, "Rpc error");
 | 
			
		||||
            return NoOnline(dataResult, deviceDatas);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static Dictionary<string, Dictionary<string, OperResult<object>>> NoOnline(Dictionary<string, Dictionary<string, OperResult<object>>> dataResult, Dictionary<string, Dictionary<string, string>> deviceDatas)
 | 
			
		||||
    {
 | 
			
		||||
        foreach (var item in deviceDatas)
 | 
			
		||||
        {
 | 
			
		||||
            dataResult.TryAdd(item.Key, new Dictionary<string, OperResult<object>>());
 | 
			
		||||
 | 
			
		||||
            foreach (var vItem in item.Value)
 | 
			
		||||
            {
 | 
			
		||||
                dataResult[item.Key].TryAdd(vItem.Key, new OperResult<object>("No online"));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return dataResult;
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 异步写入方法
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="writeInfoLists">要写入的变量及其对应的数据</param>
 | 
			
		||||
    /// <param name="cancellationToken">取消操作的通知</param>
 | 
			
		||||
    /// <returns>写入操作的结果字典</returns>
 | 
			
		||||
    public async ValueTask<Dictionary<string, Dictionary<string, IOperResult>>> InVokeWriteAsync(Dictionary<VariableRuntime, JToken> writeInfoLists, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        return (await Rpc(writeInfoLists, cancellationToken).ConfigureAwait(false)).ToDictionary(a => a.Key, a => a.Value.ToDictionary(b => b.Key, b => (IOperResult)b.Value));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -31,7 +31,10 @@ public class RedundancyOptions
 | 
			
		||||
    /// 获取或设置是否为主设备。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public bool IsMaster { get; set; }
 | 
			
		||||
 | 
			
		||||
    //主站只建议是服务端
 | 
			
		||||
    //[DynamicProperty]
 | 
			
		||||
    //public bool IsServer { get; set; } = true;
 | 
			
		||||
    internal bool IsServer => IsMaster;
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取或设置用于验证的令牌。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,712 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
//  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
 | 
			
		||||
//  此代码版权(除特别声明外的代码)归作者本人Diego所有
 | 
			
		||||
//  源代码使用协议遵循本仓库的开源协议及附加协议
 | 
			
		||||
//  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
 | 
			
		||||
//  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
 | 
			
		||||
//  使用文档:https://thingsgateway.cn/
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using Microsoft.Extensions.Hosting;
 | 
			
		||||
using Microsoft.Extensions.Logging;
 | 
			
		||||
 | 
			
		||||
using Newtonsoft.Json.Linq;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.Extension.Generic;
 | 
			
		||||
using ThingsGateway.Gateway.Application;
 | 
			
		||||
using ThingsGateway.NewLife;
 | 
			
		||||
 | 
			
		||||
using TouchSocket.Core;
 | 
			
		||||
using TouchSocket.Dmtp;
 | 
			
		||||
using TouchSocket.Dmtp.Rpc;
 | 
			
		||||
using TouchSocket.Rpc;
 | 
			
		||||
using TouchSocket.Sockets;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Management;
 | 
			
		||||
 | 
			
		||||
internal sealed class RedundancyTask : IRpcDriver, IAsyncDisposable
 | 
			
		||||
{
 | 
			
		||||
    private readonly ILogger _logger;
 | 
			
		||||
    private readonly IRedundancyService _redundancyService;
 | 
			
		||||
    private readonly GatewayRedundantSerivce _gatewayRedundantSerivce;
 | 
			
		||||
 | 
			
		||||
    public RedundancyTask(ILogger logger)
 | 
			
		||||
    {
 | 
			
		||||
        _logger = logger;
 | 
			
		||||
        _gatewayRedundantSerivce = App.RootServices.GetRequiredService<GatewayRedundantSerivce>();
 | 
			
		||||
        _redundancyService = App.RootServices.GetRequiredService<IRedundancyService>();
 | 
			
		||||
        // 创建新的文件日志记录器,并设置日志级别为 Trace
 | 
			
		||||
        LogPath = "Logs/RedundancyLog";
 | 
			
		||||
        TextLogger = TextFileLogger.GetMultipleFileLogger(LogPath);
 | 
			
		||||
        TextLogger.LogLevel = TouchSocket.Core.LogLevel.Trace;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public ILog LogMessage { get; set; }
 | 
			
		||||
    public TextFileLogger TextLogger { get; }
 | 
			
		||||
    public string LogPath { get; }
 | 
			
		||||
    private TcpDmtpClient _tcpDmtpClient;
 | 
			
		||||
    private TcpDmtpService _tcpDmtpService;
 | 
			
		||||
 | 
			
		||||
    private void Log_Out(TouchSocket.Core.LogLevel logLevel, object source, string message, Exception exception)
 | 
			
		||||
    {
 | 
			
		||||
        _logger?.Log_Out(logLevel, source, message, exception);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private ScheduledAsyncTask scheduledTask;
 | 
			
		||||
 | 
			
		||||
    private int GetBatchSize()
 | 
			
		||||
    {
 | 
			
		||||
        // 默认批量数量
 | 
			
		||||
        const int defaultSize = 10000;
 | 
			
		||||
        const int highMemorySize = 100000;
 | 
			
		||||
        const long memoryThreshold = 2L * 1024 * 1024; // 2GB,单位KB
 | 
			
		||||
 | 
			
		||||
        return GlobalData.HardwareJob.HardwareInfo.MachineInfo.AvailableMemory > memoryThreshold
 | 
			
		||||
            ? highMemorySize
 | 
			
		||||
            : defaultSize;
 | 
			
		||||
    }
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 主站
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    private async Task DoMasterWork(object? state, CancellationToken stoppingToken)
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            bool online = false;
 | 
			
		||||
 | 
			
		||||
            var waitInvoke = CreateDmtpInvokeOption(stoppingToken);
 | 
			
		||||
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (_tcpDmtpService.Clients.Count != 0)
 | 
			
		||||
                {
 | 
			
		||||
                    online = true;
 | 
			
		||||
                }
 | 
			
		||||
                // 如果 online 为 true,表示设备在线
 | 
			
		||||
                if (online)
 | 
			
		||||
                {
 | 
			
		||||
 | 
			
		||||
                    int batchSize = 50;
 | 
			
		||||
 | 
			
		||||
                    var deviceRunTimes = GlobalData.ReadOnlyIdDevices.Where(a => a.Value.IsCollect == true).Select(a => a.Value).Batch(batchSize);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                    foreach (var item in _tcpDmtpService.Clients)
 | 
			
		||||
                    {
 | 
			
		||||
                        foreach (var deviceDataWithValues in deviceRunTimes)
 | 
			
		||||
                        {
 | 
			
		||||
 | 
			
		||||
                            // 将 GlobalData.CollectDevices 和 GlobalData.Variables 同步到从站
 | 
			
		||||
                            await item.GetDmtpRpcActor().InvokeAsync(
 | 
			
		||||
                                             nameof(ReverseCallbackServer.UpData), null, waitInvoke, deviceDataWithValues.AdaptListDeviceDataWithValue()).ConfigureAwait(false);
 | 
			
		||||
                        }
 | 
			
		||||
                        LogMessage?.LogTrace($"{item.GetIPPort()} Update StandbyStation data success");
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                // 输出警告日志,指示同步数据到从站时发生错误
 | 
			
		||||
                LogMessage?.LogWarning(ex, "Synchronize data to standby site error");
 | 
			
		||||
            }
 | 
			
		||||
            await Task.Delay(RedundancyOptions.SyncInterval, stoppingToken).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
        catch (OperationCanceledException)
 | 
			
		||||
        {
 | 
			
		||||
        }
 | 
			
		||||
        catch (ObjectDisposedException)
 | 
			
		||||
        {
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            LogMessage?.LogWarning(ex, "Execute");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 从站
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    private async Task DoSlaveWork(object? state, CancellationToken stoppingToken)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            bool online = false;
 | 
			
		||||
            var waitInvoke = CreateDmtpInvokeOption(stoppingToken);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                await _tcpDmtpClient.TryConnectAsync().ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                {
 | 
			
		||||
                    // 初始化读取错误计数器
 | 
			
		||||
                    var readErrorCount = 0;
 | 
			
		||||
                    // 当读取错误次数小于最大错误计数时循环执行
 | 
			
		||||
                    while (readErrorCount < RedundancyOptions.MaxErrorCount)
 | 
			
		||||
                    {
 | 
			
		||||
                        try
 | 
			
		||||
                        {
 | 
			
		||||
                            // 发送 Ping 请求以检查设备是否在线,超时时间为 10000 毫秒
 | 
			
		||||
                            online = await _tcpDmtpClient.PingAsync(10000).ConfigureAwait(false);
 | 
			
		||||
                            if (online)
 | 
			
		||||
                                break;
 | 
			
		||||
                            else
 | 
			
		||||
                            {
 | 
			
		||||
                                readErrorCount++;
 | 
			
		||||
                                await Task.Delay(RedundancyOptions.SyncInterval, stoppingToken).ConfigureAwait(false);
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        catch
 | 
			
		||||
                        {
 | 
			
		||||
                            // 捕获异常,增加读取错误计数器
 | 
			
		||||
                            readErrorCount++;
 | 
			
		||||
                            await Task.Delay(RedundancyOptions.SyncInterval, stoppingToken).ConfigureAwait(false);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                // 如果设备不在线
 | 
			
		||||
                if (!online)
 | 
			
		||||
                {
 | 
			
		||||
                    // 无法获取状态,启动本机
 | 
			
		||||
                    await ActiveAsync().ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    // 如果设备在线
 | 
			
		||||
                    LogMessage?.LogTrace($"Ping ActiveStation {RedundancyOptions.MasterUri} success");
 | 
			
		||||
                    await StandbyAsync().ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        catch (OperationCanceledException)
 | 
			
		||||
        {
 | 
			
		||||
        }
 | 
			
		||||
        catch (ObjectDisposedException)
 | 
			
		||||
        {
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            LogMessage?.LogWarning(ex, "Execute");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    private WaitLock _switchLock = new();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    private bool first;
 | 
			
		||||
    private async Task StandbyAsync()
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            await _switchLock.WaitAsync().ConfigureAwait(false);
 | 
			
		||||
            if (_gatewayRedundantSerivce.StartCollectChannelEnable)
 | 
			
		||||
            {
 | 
			
		||||
                // 输出日志,指示主站已恢复,从站将切换到备用状态
 | 
			
		||||
                if (first)
 | 
			
		||||
                    LogMessage?.Warning("Master site has recovered, local machine (standby) will switch to standby state");
 | 
			
		||||
 | 
			
		||||
                // 将 IsStart 设置为 false,表示当前设备为从站,切换到备用状态
 | 
			
		||||
                _gatewayRedundantSerivce.StartCollectChannelEnable = false;
 | 
			
		||||
                _gatewayRedundantSerivce.StartBusinessChannelEnable = RedundancyOptions?.IsStartBusinessDevice ?? false;
 | 
			
		||||
                await RestartAsync().ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            _switchLock.Release();
 | 
			
		||||
            first = true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task ActiveAsync()
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            await _switchLock.WaitAsync().ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            _gatewayRedundantSerivce.StartBusinessChannelEnable = true;
 | 
			
		||||
            if (!_gatewayRedundantSerivce.StartCollectChannelEnable)
 | 
			
		||||
            {
 | 
			
		||||
                // 输出日志,指示无法连接冗余站点,本机将切换到正常状态
 | 
			
		||||
                if (first)
 | 
			
		||||
                    LogMessage?.Warning("Cannot connect to redundant site, local machine will switch to normal state");
 | 
			
		||||
                _gatewayRedundantSerivce.StartCollectChannelEnable = true;
 | 
			
		||||
                await RestartAsync().ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            _switchLock.Release();
 | 
			
		||||
            first = true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    private static Task RestartAsync()
 | 
			
		||||
    {
 | 
			
		||||
        return GlobalData.ChannelRuntimeService.RestartChannelAsync(GlobalData.ReadOnlyIdChannels.Values);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task StartTaskAsync(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        await StopTaskAsync().ConfigureAwait(false);
 | 
			
		||||
        RedundancyOptions = (await _redundancyService.GetRedundancyAsync().ConfigureAwait(false)).AdaptRedundancyOptions();
 | 
			
		||||
 | 
			
		||||
        if (RedundancyOptions?.Enable == true)
 | 
			
		||||
        {
 | 
			
		||||
            if (RedundancyOptions.IsMaster)
 | 
			
		||||
            {
 | 
			
		||||
                _tcpDmtpService = await GetTcpDmtpService(RedundancyOptions).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                await _tcpDmtpService.StartAsync().ConfigureAwait(false);//启动
 | 
			
		||||
                await ActiveAsync().ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                _tcpDmtpClient = await GetTcpDmtpClient(RedundancyOptions).ConfigureAwait(false);
 | 
			
		||||
                await StandbyAsync().ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            await ActiveAsync().ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (RedundancyOptions?.Enable == true)
 | 
			
		||||
        {
 | 
			
		||||
            LogMessage?.LogInformation($"Redundancy task started");
 | 
			
		||||
            if (RedundancyOptions.IsMaster)
 | 
			
		||||
            {
 | 
			
		||||
                scheduledTask = new ScheduledAsyncTask(RedundancyOptions.SyncInterval, DoMasterWork, null, null, cancellationToken);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                scheduledTask = new ScheduledAsyncTask(5000, DoSlaveWork, null, null, cancellationToken);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            scheduledTask.Start();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
    public async Task StopTaskAsync()
 | 
			
		||||
    {
 | 
			
		||||
        if (scheduledTask?.Enable == true)
 | 
			
		||||
        {
 | 
			
		||||
            LogMessage?.LogInformation($"Redundancy task stoped");
 | 
			
		||||
            scheduledTask?.Stop();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (_tcpDmtpService != null)
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                await _tcpDmtpService.StopAsync().ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if (_tcpDmtpClient != null)
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                await _tcpDmtpClient.CloseAsync().ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async ValueTask DisposeAsync()
 | 
			
		||||
    {
 | 
			
		||||
        await StopTaskAsync().ConfigureAwait(false);
 | 
			
		||||
        TextLogger?.TryDispose();
 | 
			
		||||
        scheduledTask?.TryDispose();
 | 
			
		||||
 | 
			
		||||
        _tcpDmtpService?.TryDispose();
 | 
			
		||||
        _tcpDmtpClient?.TryDispose();
 | 
			
		||||
        _tcpDmtpService = null;
 | 
			
		||||
        _tcpDmtpClient = null;
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #region
 | 
			
		||||
 | 
			
		||||
    private async Task<TcpDmtpClient> GetTcpDmtpClient(RedundancyOptions redundancy)
 | 
			
		||||
    {
 | 
			
		||||
        var log = new LoggerGroup() { LogLevel = TouchSocket.Core.LogLevel.Trace };
 | 
			
		||||
        log?.AddLogger(new EasyLogger(Log_Out) { LogLevel = TouchSocket.Core.LogLevel.Trace });
 | 
			
		||||
        log?.AddLogger(TextLogger);
 | 
			
		||||
        LogMessage = log;
 | 
			
		||||
        var tcpDmtpClient = new TcpDmtpClient();
 | 
			
		||||
        var config = new TouchSocketConfig()
 | 
			
		||||
               .SetRemoteIPHost(redundancy.MasterUri)
 | 
			
		||||
               .SetAdapterOption(new AdapterOption() { MaxPackageSize = 0x20000000 })
 | 
			
		||||
               .SetDmtpOption(new DmtpOption() { VerifyToken = redundancy.VerifyToken })
 | 
			
		||||
               .ConfigureContainer(a =>
 | 
			
		||||
               {
 | 
			
		||||
                   a.AddLogger(LogMessage);
 | 
			
		||||
                   a.AddRpcStore(store =>
 | 
			
		||||
                   {
 | 
			
		||||
                       store.RegisterServer(new ReverseCallbackServer(this));
 | 
			
		||||
                   });
 | 
			
		||||
               })
 | 
			
		||||
               .ConfigurePlugins(a =>
 | 
			
		||||
               {
 | 
			
		||||
                   a.UseDmtpRpc();
 | 
			
		||||
                   a.UseDmtpHeartbeat()//使用Dmtp心跳
 | 
			
		||||
                   .SetTick(TimeSpan.FromMilliseconds(redundancy.HeartbeatInterval))
 | 
			
		||||
                   .SetMaxFailCount(redundancy.MaxErrorCount);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
               });
 | 
			
		||||
 | 
			
		||||
        await tcpDmtpClient.SetupAsync(config).ConfigureAwait(false);
 | 
			
		||||
        return tcpDmtpClient;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task<TcpDmtpService> GetTcpDmtpService(RedundancyOptions redundancy)
 | 
			
		||||
    {
 | 
			
		||||
        var log = new LoggerGroup() { LogLevel = TouchSocket.Core.LogLevel.Trace };
 | 
			
		||||
        log?.AddLogger(new EasyLogger(Log_Out) { LogLevel = TouchSocket.Core.LogLevel.Trace });
 | 
			
		||||
        log?.AddLogger(TextLogger);
 | 
			
		||||
        LogMessage = log;
 | 
			
		||||
        var tcpDmtpService = new TcpDmtpService();
 | 
			
		||||
        var config = new TouchSocketConfig()
 | 
			
		||||
               .SetListenIPHosts(redundancy.MasterUri)
 | 
			
		||||
               .SetAdapterOption(new AdapterOption() { MaxPackageSize = 0x20000000 })
 | 
			
		||||
               .SetDmtpOption(new DmtpOption() { VerifyToken = redundancy.VerifyToken })
 | 
			
		||||
               .ConfigureContainer(a =>
 | 
			
		||||
               {
 | 
			
		||||
                   a.AddLogger(LogMessage);
 | 
			
		||||
                   a.AddRpcStore(store =>
 | 
			
		||||
                   {
 | 
			
		||||
                       store.RegisterServer(new ReverseCallbackServer(this));
 | 
			
		||||
                   });
 | 
			
		||||
               })
 | 
			
		||||
               .ConfigurePlugins(a =>
 | 
			
		||||
               {
 | 
			
		||||
                   a.UseDmtpRpc();
 | 
			
		||||
                   a.UseDmtpHeartbeat()//使用Dmtp心跳
 | 
			
		||||
                   .SetTick(TimeSpan.FromMilliseconds(redundancy.HeartbeatInterval))
 | 
			
		||||
                   .SetMaxFailCount(redundancy.MaxErrorCount);
 | 
			
		||||
               });
 | 
			
		||||
 | 
			
		||||
        await tcpDmtpService.SetupAsync(config).ConfigureAwait(false);
 | 
			
		||||
        return tcpDmtpService;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private DmtpInvokeOption CreateDmtpInvokeOption(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        return new DmtpInvokeOption()
 | 
			
		||||
        {
 | 
			
		||||
            FeedbackType = FeedbackType.WaitInvoke,
 | 
			
		||||
            Token = cancellationToken,
 | 
			
		||||
            Timeout = 1800000,
 | 
			
		||||
            SerializationType = SerializationType.Json,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
    private RedundancyOptions RedundancyOptions;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    #endregion
 | 
			
		||||
 | 
			
		||||
    #region ForcedSync
 | 
			
		||||
 | 
			
		||||
    WaitLock ForcedSyncWaitLock = new WaitLock();
 | 
			
		||||
    public async Task ForcedSync(CancellationToken cancellationToken = default)
 | 
			
		||||
    {
 | 
			
		||||
        await ForcedSyncWaitLock.WaitAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
            if (!RedundancyOptions.IsMaster)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var invokeOption = CreateDmtpInvokeOption(cancellationToken);
 | 
			
		||||
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                await EnsureChannelOpenAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                // 如果在线,执行同步
 | 
			
		||||
                bool online = RedundancyOptions.IsServer
 | 
			
		||||
                    ? _tcpDmtpService.Clients.Count > 0
 | 
			
		||||
                    : _tcpDmtpClient.Online;
 | 
			
		||||
 | 
			
		||||
                if (!online)
 | 
			
		||||
                {
 | 
			
		||||
                    LogMessage?.LogWarning("ForcedSync data error, no client online");
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (RedundancyOptions.IsServer)
 | 
			
		||||
                {
 | 
			
		||||
                    foreach (var client in _tcpDmtpService.Clients)
 | 
			
		||||
                    {
 | 
			
		||||
                        await InvokeSyncDataAsync(client, invokeOption, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    await InvokeSyncDataAsync(_tcpDmtpClient, invokeOption, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            catch (OperationCanceledException) { }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                LogMessage?.LogWarning(ex, "ForcedSync data error");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            ForcedSyncWaitLock.Release();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task InvokeSyncDataAsync(IDmtpActorObject client, DmtpInvokeOption invokeOption, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        int maxBatchSize = GetBatchSize() / 10;
 | 
			
		||||
 | 
			
		||||
        var groups = GlobalData.IdVariables.Select(a => a.Value).GroupBy(a => a.DeviceRuntime);
 | 
			
		||||
 | 
			
		||||
        var channelBatch = new HashSet<Channel>();
 | 
			
		||||
        var deviceBatch = new HashSet<Device>();
 | 
			
		||||
        var variableBatch = new List<Variable>();
 | 
			
		||||
        foreach (var group in groups)
 | 
			
		||||
        {
 | 
			
		||||
            var channel = group.Key.ChannelRuntime.AdaptChannel();
 | 
			
		||||
            var device = group.Key.AdaptDevice();
 | 
			
		||||
            channelBatch.Add(channel);
 | 
			
		||||
            deviceBatch.Add(device);
 | 
			
		||||
 | 
			
		||||
            foreach (var variable in group)
 | 
			
		||||
            {
 | 
			
		||||
                channelBatch.Add(channel);
 | 
			
		||||
                deviceBatch.Add(device);
 | 
			
		||||
                variableBatch.Add(variable.AdaptVariable());
 | 
			
		||||
 | 
			
		||||
                if (variableBatch.Count >= maxBatchSize)
 | 
			
		||||
                {
 | 
			
		||||
                    // 发送一批
 | 
			
		||||
                    await client.GetDmtpRpcActor().InvokeAsync(nameof(ReverseCallbackServer.SyncData), null, invokeOption, channelBatch.ToList(), deviceBatch.ToList(), variableBatch).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                    variableBatch.Clear();
 | 
			
		||||
                    channelBatch.Remove(channel);
 | 
			
		||||
                    deviceBatch.Remove(device);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 发送最后剩余的一批
 | 
			
		||||
        if (variableBatch.Count > 0)
 | 
			
		||||
        {
 | 
			
		||||
            await client.GetDmtpRpcActor().InvokeAsync(nameof(ReverseCallbackServer.SyncData), null, invokeOption, channelBatch.ToList(), deviceBatch.ToList(), variableBatch).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        LogMessage?.LogTrace($"ForcedSync data success");
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #endregion
 | 
			
		||||
 | 
			
		||||
    #region  Rpc
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 异步写入方法
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="writeInfoLists">要写入的变量及其对应的数据</param>
 | 
			
		||||
    /// <param name="cancellationToken">取消操作的通知</param>
 | 
			
		||||
    /// <returns>写入操作的结果字典</returns>
 | 
			
		||||
    public async ValueTask<Dictionary<string, Dictionary<string, IOperResult>>> InvokeMethodAsync(Dictionary<VariableRuntime, JToken> writeInfoLists, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        return (await Rpc(writeInfoLists, cancellationToken).ConfigureAwait(false)).ToDictionary(a => a.Key, a => a.Value.ToDictionary(b => b.Key, b => (IOperResult)b.Value));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async ValueTask<Dictionary<string, Dictionary<string, OperResult<object>>>> Rpc(Dictionary<VariableRuntime, JToken> writeInfoLists, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        var dataResult = new Dictionary<string, Dictionary<string, OperResult<object>>>();
 | 
			
		||||
 | 
			
		||||
        Dictionary<string, Dictionary<string, string>> deviceDatas = new();
 | 
			
		||||
        foreach (var item in writeInfoLists)
 | 
			
		||||
        {
 | 
			
		||||
            if (deviceDatas.TryGetValue(item.Key.DeviceName ?? string.Empty, out var variableDatas))
 | 
			
		||||
            {
 | 
			
		||||
                variableDatas.TryAdd(item.Key.Name, item.Value?.ToString() ?? string.Empty);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                deviceDatas.TryAdd(item.Key.DeviceName ?? string.Empty, new());
 | 
			
		||||
                deviceDatas[item.Key.DeviceName ?? string.Empty].TryAdd(item.Key.Name, item.Value?.ToString() ?? string.Empty);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (RedundancyOptions.IsMaster)
 | 
			
		||||
            return NoOnline(dataResult, deviceDatas);
 | 
			
		||||
 | 
			
		||||
        var invokeOption = CreateDmtpInvokeOption(cancellationToken);
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            await EnsureChannelOpenAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
            bool online = RedundancyOptions.IsServer ? _tcpDmtpService.Clients.Count > 0 : _tcpDmtpClient.Online;
 | 
			
		||||
 | 
			
		||||
            if (!online)
 | 
			
		||||
            {
 | 
			
		||||
                LogMessage?.LogWarning("Rpc error, no client online");
 | 
			
		||||
                return NoOnline(dataResult, deviceDatas);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (RedundancyOptions.IsServer)
 | 
			
		||||
            {
 | 
			
		||||
                await InvokeRpcServerAsync(deviceDatas, dataResult, invokeOption).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                dataResult = await InvokeRpcClientAsync(deviceDatas, invokeOption).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            LogMessage?.LogTrace("Rpc success");
 | 
			
		||||
            return dataResult;
 | 
			
		||||
        }
 | 
			
		||||
        catch (OperationCanceledException)
 | 
			
		||||
        {
 | 
			
		||||
            return NoOnline(dataResult, deviceDatas);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            LogMessage?.LogWarning(ex, "Rpc error");
 | 
			
		||||
            return NoOnline(dataResult, deviceDatas);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task<Dictionary<string, Dictionary<string, OperResult<object>>>> InvokeRpcClientAsync(
 | 
			
		||||
        Dictionary<string, Dictionary<string, string>> deviceDatas,
 | 
			
		||||
        DmtpInvokeOption invokeOption)
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
        return await _tcpDmtpClient.GetDmtpRpcActor()
 | 
			
		||||
            .InvokeTAsync<Dictionary<string, Dictionary<string, OperResult<object>>>>(
 | 
			
		||||
                nameof(ReverseCallbackServer.Rpc), invokeOption, deviceDatas)
 | 
			
		||||
            .ConfigureAwait(false);
 | 
			
		||||
    }
 | 
			
		||||
    private async Task InvokeRpcServerAsync(
 | 
			
		||||
        Dictionary<string, Dictionary<string, string>> deviceDatas,
 | 
			
		||||
        Dictionary<string, Dictionary<string, OperResult<object>>> dataResult,
 | 
			
		||||
        DmtpInvokeOption invokeOption)
 | 
			
		||||
    {
 | 
			
		||||
        var w1 = new Dictionary<TcpDmtpSessionClient, Dictionary<string, Dictionary<string, string>>>();
 | 
			
		||||
 | 
			
		||||
        foreach (var (key, value) in deviceDatas)
 | 
			
		||||
        {
 | 
			
		||||
            if (!GlobalData.ReadOnlyDevices.TryGetValue(key, out var device))
 | 
			
		||||
            {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!_tcpDmtpService.TryGetClient(device.Tag, out var client))
 | 
			
		||||
            {
 | 
			
		||||
                // 客户端未在线
 | 
			
		||||
                dataResult.TryAdd(key, value.ToDictionary(v => v.Key, _ => new OperResult<object>("No online")));
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // 去除 endpoint 前缀
 | 
			
		||||
            var deviceName = key;
 | 
			
		||||
 | 
			
		||||
            if (!w1.TryGetValue(client, out var variableDatas))
 | 
			
		||||
            {
 | 
			
		||||
                variableDatas = new Dictionary<string, Dictionary<string, string>>();
 | 
			
		||||
                w1.Add(client, variableDatas);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            variableDatas[deviceName] = value;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        foreach (var (client, variableDatas) in w1)
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var data = await client.GetDmtpRpcActor().InvokeTAsync<Dictionary<string, Dictionary<string, OperResult<object>>>>(
 | 
			
		||||
                    nameof(ReverseCallbackServer.Rpc), invokeOption, variableDatas)
 | 
			
		||||
                    .ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                dataResult.AddRange(data);
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                foreach (var (deviceName, vars) in variableDatas)
 | 
			
		||||
                {
 | 
			
		||||
                    var errorDict = vars.ToDictionary(v => v.Key, _ => new OperResult<object>(ex));
 | 
			
		||||
                    dataResult[deviceName] = errorDict;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    private static Dictionary<string, Dictionary<string, OperResult<object>>> NoOnline(Dictionary<string, Dictionary<string, OperResult<object>>> dataResult, Dictionary<string, Dictionary<string, string>> deviceDatas)
 | 
			
		||||
    {
 | 
			
		||||
        foreach (var item in deviceDatas)
 | 
			
		||||
        {
 | 
			
		||||
            dataResult.TryAdd(item.Key, new Dictionary<string, OperResult<object>>());
 | 
			
		||||
 | 
			
		||||
            foreach (var vItem in item.Value)
 | 
			
		||||
            {
 | 
			
		||||
                dataResult[item.Key].TryAdd(vItem.Key, new OperResult<object>("No online"));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return dataResult;
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 异步写入方法
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="writeInfoLists">要写入的变量及其对应的数据</param>
 | 
			
		||||
    /// <param name="cancellationToken">取消操作的通知</param>
 | 
			
		||||
    /// <returns>写入操作的结果字典</returns>
 | 
			
		||||
    public async ValueTask<Dictionary<string, Dictionary<string, IOperResult>>> InVokeWriteAsync(Dictionary<VariableRuntime, JToken> writeInfoLists, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        return (await Rpc(writeInfoLists, cancellationToken).ConfigureAwait(false)).ToDictionary(a => a.Key, a => a.Value.ToDictionary(b => b.Key, b => (IOperResult)b.Value));
 | 
			
		||||
    }
 | 
			
		||||
    private async Task EnsureChannelOpenAsync(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        if (RedundancyOptions.IsServer)
 | 
			
		||||
        {
 | 
			
		||||
            if (_tcpDmtpService.ServerState != ServerState.Running)
 | 
			
		||||
            {
 | 
			
		||||
                if (_tcpDmtpService.ServerState != ServerState.Stopped)
 | 
			
		||||
                    await _tcpDmtpService.StopAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                await _tcpDmtpService.StartAsync().ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            if (!_tcpDmtpClient.Online)
 | 
			
		||||
                await _tcpDmtpClient.TryConnectAsync().ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    #endregion
 | 
			
		||||
}
 | 
			
		||||
@@ -19,10 +19,10 @@ namespace ThingsGateway.Management;
 | 
			
		||||
 | 
			
		||||
internal sealed partial class ReverseCallbackServer : SingletonRpcServer
 | 
			
		||||
{
 | 
			
		||||
    RedundancyHostedService RedundancyHostedService;
 | 
			
		||||
    public ReverseCallbackServer(RedundancyHostedService redundancyHostedService)
 | 
			
		||||
    RedundancyTask RedundancyTask;
 | 
			
		||||
    public ReverseCallbackServer(RedundancyTask redundancyTask)
 | 
			
		||||
    {
 | 
			
		||||
        RedundancyHostedService = redundancyHostedService;
 | 
			
		||||
        RedundancyTask = redundancyTask;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [DmtpRpc(MethodInvoke = true)]
 | 
			
		||||
@@ -33,7 +33,7 @@ internal sealed partial class ReverseCallbackServer : SingletonRpcServer
 | 
			
		||||
        {
 | 
			
		||||
            if (GlobalData.ReadOnlyDevices.TryGetValue(deviceData.Name, out var device))
 | 
			
		||||
            {
 | 
			
		||||
                device.RpcDriver = RedundancyHostedService;
 | 
			
		||||
                device.RpcDriver = RedundancyTask;
 | 
			
		||||
                device.Tag = callContext.Caller is IIdClient idClient ? idClient.Id : string.Empty;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -50,41 +50,112 @@ internal sealed partial class ReverseCallbackServer : SingletonRpcServer
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        RedundancyHostedService.LogMessage?.Trace("Update data success");
 | 
			
		||||
        RedundancyTask.LogMessage?.Trace("RpcServer Update data success");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    [DmtpRpc(MethodInvoke = true)]
 | 
			
		||||
    public List<DataWithDatabase> GetData()
 | 
			
		||||
    public async Task SyncData(List<Channel> channels, List<Device> devices, List<Variable> variables)
 | 
			
		||||
    {
 | 
			
		||||
        List<DataWithDatabase> dataWithDatabases = new();
 | 
			
		||||
        foreach (var channels in GlobalData.ReadOnlyChannels)
 | 
			
		||||
 | 
			
		||||
        List<Channel> addChannels = new();
 | 
			
		||||
        List<Device> addDevices = new();
 | 
			
		||||
        List<Variable> addVariables = new();
 | 
			
		||||
        List<Channel> upChannels = new();
 | 
			
		||||
        List<Device> upDevices = new();
 | 
			
		||||
        List<Variable> upVariables = new();
 | 
			
		||||
 | 
			
		||||
        Dictionary<long, long> channelNewId = new();
 | 
			
		||||
        Dictionary<long, long> deviceNewId = new();
 | 
			
		||||
 | 
			
		||||
        foreach (var channel in channels)
 | 
			
		||||
        {
 | 
			
		||||
            DataWithDatabase dataWithDatabase = new();
 | 
			
		||||
            dataWithDatabase.Channel = channels.Value;
 | 
			
		||||
            dataWithDatabase.DeviceVariables = new();
 | 
			
		||||
            foreach (var devices in channels.Value.ReadDeviceRuntimes)
 | 
			
		||||
            if (GlobalData.ReadOnlyChannels.TryGetValue(channel.Name, out var channelRuntime))
 | 
			
		||||
            {
 | 
			
		||||
                DeviceDataWithDatabase deviceDataWithDatabase = new();
 | 
			
		||||
 | 
			
		||||
                deviceDataWithDatabase.Device = devices.Value;
 | 
			
		||||
                deviceDataWithDatabase.Variables = devices.Value.ReadOnlyVariableRuntimes.Select(a => a.Value).Cast<Variable>().ToList();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                dataWithDatabase.DeviceVariables.Add(deviceDataWithDatabase);
 | 
			
		||||
                channelNewId.TryAdd(channel.Id, channelRuntime.Id);
 | 
			
		||||
                channel.Id = channelRuntime.Id;
 | 
			
		||||
                channel.Enable = false;
 | 
			
		||||
                upChannels.Add(channel);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                var id = CommonUtils.GetSingleId();
 | 
			
		||||
                channelNewId.TryAdd(channel.Id, id);
 | 
			
		||||
                channel.Id = id;
 | 
			
		||||
                channel.Enable = false;
 | 
			
		||||
                addChannels.Add(channel);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            dataWithDatabases.Add(dataWithDatabase);
 | 
			
		||||
        }
 | 
			
		||||
        return dataWithDatabases;
 | 
			
		||||
 | 
			
		||||
        foreach (var device in devices)
 | 
			
		||||
        {
 | 
			
		||||
            if (GlobalData.ReadOnlyDevices.TryGetValue(device.Name, out var deviceRuntime))
 | 
			
		||||
            {
 | 
			
		||||
 | 
			
		||||
                deviceNewId.TryAdd(device.Id, deviceRuntime.Id);
 | 
			
		||||
                device.Id = deviceRuntime.Id;
 | 
			
		||||
 | 
			
		||||
                channelNewId.TryGetValue(device.ChannelId, out var newid);
 | 
			
		||||
                device.ChannelId = newid;
 | 
			
		||||
 | 
			
		||||
                device.Enable = false;
 | 
			
		||||
                upDevices.Add(device);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                var id = CommonUtils.GetSingleId();
 | 
			
		||||
                deviceNewId.TryAdd(device.Id, id);
 | 
			
		||||
                device.Id = id;
 | 
			
		||||
 | 
			
		||||
                channelNewId.TryGetValue(device.ChannelId, out var newid);
 | 
			
		||||
                device.ChannelId = newid;
 | 
			
		||||
                device.Enable = false;
 | 
			
		||||
                addDevices.Add(device);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        foreach (var variable in variables)
 | 
			
		||||
        {
 | 
			
		||||
            deviceNewId.TryGetValue(variable.DeviceId, out var newid);
 | 
			
		||||
            if (GlobalData.ReadOnlyIdDevices.TryGetValue(newid, out var deviceRuntime))
 | 
			
		||||
            {
 | 
			
		||||
                if (deviceRuntime.ReadOnlyVariableRuntimes.TryGetValue(variable.Name, out var variableRuntime))
 | 
			
		||||
                {
 | 
			
		||||
                    variable.Id = variableRuntime.Id;
 | 
			
		||||
 | 
			
		||||
                    variable.DeviceId = newid;
 | 
			
		||||
 | 
			
		||||
                    upVariables.Add(variable);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    var id = CommonUtils.GetSingleId();
 | 
			
		||||
                    variable.Id = id;
 | 
			
		||||
 | 
			
		||||
                    variable.DeviceId = newid;
 | 
			
		||||
                    addVariables.Add(variable);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                var id = CommonUtils.GetSingleId();
 | 
			
		||||
                variable.Id = id;
 | 
			
		||||
 | 
			
		||||
                variable.DeviceId = newid;
 | 
			
		||||
                addVariables.Add(variable);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        await GlobalData.ChannelRuntimeService.InsertAsync(addChannels, addDevices, addVariables, true, default).ConfigureAwait(false);
 | 
			
		||||
        await GlobalData.ChannelRuntimeService.UpdateAsync(upChannels, upDevices, upVariables, true, default).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
        RedundancyTask.LogMessage?.LogTrace($"Sync data success");
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [DmtpRpc(MethodInvoke = true)]
 | 
			
		||||
    public Task<Dictionary<string, Dictionary<string, IOperResult>>> Rpc(Dictionary<string, Dictionary<string, string>> deviceDatas, CancellationToken cancellationToken)
 | 
			
		||||
    public Task<Dictionary<string, Dictionary<string, IOperResult>>> Rpc(ICallContext callContext, Dictionary<string, Dictionary<string, string>> deviceDatas, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        return GlobalData.RpcService.InvokeDeviceMethodAsync("Management", deviceDatas, cancellationToken);
 | 
			
		||||
        return GlobalData.RpcService.InvokeDeviceMethodAsync("Management" + $"[{(callContext.Caller is ITcpSession tcpSession ? tcpSession.GetIPPort() : string.Empty)}]", deviceDatas, cancellationToken);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -36,14 +36,3 @@ public class DeviceDataWithValue
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
public class DataWithDatabase
 | 
			
		||||
{
 | 
			
		||||
    public Channel Channel { get; set; }
 | 
			
		||||
    public List<DeviceDataWithDatabase> DeviceVariables { get; set; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class DeviceDataWithDatabase
 | 
			
		||||
{
 | 
			
		||||
    public Device Device { get; set; }
 | 
			
		||||
    public List<Variable> Variables { get; set; }
 | 
			
		||||
}
 | 
			
		||||
@@ -19,15 +19,17 @@ public class VariableDataWithValue
 | 
			
		||||
    /// <inheritdoc cref="Variable.Name"/>
 | 
			
		||||
    public string Name { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc cref="VariableRuntime.Value"/>
 | 
			
		||||
    /// <inheritdoc cref="VariableRuntime.RawValue"/>
 | 
			
		||||
    public object RawValue { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc cref="VariableRuntime.Value"/>
 | 
			
		||||
    public object Value { get; set; }
 | 
			
		||||
    /// <inheritdoc cref="VariableRuntime.CollectTime"/>
 | 
			
		||||
    public DateTime CollectTime { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc cref="VariableRuntime.IsOnline"/>
 | 
			
		||||
    public bool IsOnline { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc cref="VariableRuntime.RuntimeType"/>
 | 
			
		||||
    public string RuntimeType { get; set; }
 | 
			
		||||
    /// <inheritdoc cref="VariableRuntime.LastErrorMessage"/>
 | 
			
		||||
    [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
 | 
			
		||||
    [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)]
 | 
			
		||||
@@ -129,7 +129,7 @@ internal sealed class UpdateZipFileHostedService : BackgroundService, IUpdateZip
 | 
			
		||||
    {
 | 
			
		||||
        var upgradeServerOptions = App.GetOptions<UpgradeServerOptions>();
 | 
			
		||||
        if (!upgradeServerOptions.Enable)
 | 
			
		||||
            throw new Exception("未启用更新服务");
 | 
			
		||||
            throw new Exception("Update service not enabled");
 | 
			
		||||
 | 
			
		||||
        //设置调用配置
 | 
			
		||||
        var tokenSource = new CancellationTokenSource();//可取消令箭源,可用于取消Rpc的调用
 | 
			
		||||
@@ -156,16 +156,19 @@ internal sealed class UpdateZipFileHostedService : BackgroundService, IUpdateZip
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private readonly WaitLock WaitLock = new();
 | 
			
		||||
    private readonly WaitLock UpdateWaitLock = new();
 | 
			
		||||
    public async Task Update(UpdateZipFile updateZipFile, Func<Task<bool>> check = null)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            await UpdateWaitLock.WaitAsync().ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            var upgradeServerOptions = App.GetOptions<UpgradeServerOptions>();
 | 
			
		||||
            if (!upgradeServerOptions.Enable)
 | 
			
		||||
                return;
 | 
			
		||||
            if (WaitLock.Waited)
 | 
			
		||||
            {
 | 
			
		||||
                _log.LogWarning("正在更新中,请稍后再试");
 | 
			
		||||
                _log.LogWarning("Updating, please try again later");
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            try
 | 
			
		||||
@@ -198,6 +201,10 @@ internal sealed class UpdateZipFileHostedService : BackgroundService, IUpdateZip
 | 
			
		||||
            _log.LogWarning(ex);
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            UpdateWaitLock.Release();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -297,6 +304,7 @@ internal sealed class UpdateZipFileHostedService : BackgroundService, IUpdateZip
 | 
			
		||||
        var upgradeServerOptions = App.GetOptions<UpgradeServerOptions>();
 | 
			
		||||
        var config = new TouchSocketConfig()
 | 
			
		||||
               .SetRemoteIPHost(new IPHost($"{upgradeServerOptions.UpgradeServerIP}:{upgradeServerOptions.UpgradeServerPort}"))
 | 
			
		||||
               .SetAdapterOption(new AdapterOption() { MaxPackageSize = 0x20000000 })
 | 
			
		||||
               .SetDmtpOption(new DmtpOption()
 | 
			
		||||
               {
 | 
			
		||||
                   VerifyToken = upgradeServerOptions.VerifyToken
 | 
			
		||||
@@ -317,33 +325,6 @@ internal sealed class UpdateZipFileHostedService : BackgroundService, IUpdateZip
 | 
			
		||||
 | 
			
		||||
                   a.Add<FilePlugin>();
 | 
			
		||||
 | 
			
		||||
                   //使用重连
 | 
			
		||||
                   a.UseDmtpReconnection<TcpDmtpClient>()
 | 
			
		||||
                   .UsePolling(TimeSpan.FromSeconds(5))//使用轮询,每3秒检测一次
 | 
			
		||||
                   .SetActionForCheck(async (c, i) =>//重新定义检活策略
 | 
			
		||||
                   {
 | 
			
		||||
                       if (!upgradeServerOptions.Enable) return true;
 | 
			
		||||
                       //方法1,直接判断是否在握手状态。使用该方式,最好和心跳插件配合使用。因为如果直接断网,则检测不出来
 | 
			
		||||
                       //await Task.CompletedTask;//消除Task
 | 
			
		||||
                       //return c.Online;//判断是否在握手状态
 | 
			
		||||
 | 
			
		||||
                       //方法2,直接ping,如果true,则客户端必在线。如果false,则客户端不一定不在线,原因是可能当前传输正在忙
 | 
			
		||||
                       if (await c.PingAsync().ConfigureAwait(false))
 | 
			
		||||
                       {
 | 
			
		||||
                           return true;
 | 
			
		||||
                       }
 | 
			
		||||
                       //返回false时可以判断,如果最近活动时间不超过3秒,则猜测客户端确实在忙,所以跳过本次重连
 | 
			
		||||
                       else if (DateTime.Now - c.GetLastActiveTime() < TimeSpan.FromSeconds(3))
 | 
			
		||||
                       {
 | 
			
		||||
                           return null;
 | 
			
		||||
                       }
 | 
			
		||||
                       //否则,直接重连。
 | 
			
		||||
                       else
 | 
			
		||||
                       {
 | 
			
		||||
                           return false;
 | 
			
		||||
                       }
 | 
			
		||||
                   });
 | 
			
		||||
 | 
			
		||||
                   a.UseDmtpHeartbeat()//使用Dmtp心跳
 | 
			
		||||
                   .SetTick(TimeSpan.FromSeconds(5))
 | 
			
		||||
                   .SetMaxFailCount(3);
 | 
			
		||||
 
 | 
			
		||||
@@ -197,82 +197,8 @@ internal sealed class RulesEngineHostedService : BackgroundService, IRulesEngine
 | 
			
		||||
 | 
			
		||||
    #region worker服务
 | 
			
		||||
 | 
			
		||||
    private async Task StartAll(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        Clear();
 | 
			
		||||
 | 
			
		||||
        Rules = await App.GetService<IRulesService>().GetAllAsync().ConfigureAwait(false);
 | 
			
		||||
        Diagrams = new();
 | 
			
		||||
        foreach (var rules in Rules.Where(a => a.Status))
 | 
			
		||||
        {
 | 
			
		||||
            var item = Init(rules);
 | 
			
		||||
            Start(item.rulesLog, item.blazorDiagram, cancellationToken);
 | 
			
		||||
        }
 | 
			
		||||
        dispatchService.Dispatch(null);
 | 
			
		||||
 | 
			
		||||
        _ = Task.Factory.StartNew(async (state) =>
 | 
			
		||||
        {
 | 
			
		||||
            if (state is not Dictionary<RulesLog, Diagram> diagrams)
 | 
			
		||||
            {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            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);
 | 
			
		||||
            }
 | 
			
		||||
        }, Diagrams, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Default).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    private CancellationTokenSource? TokenSource { get; set; }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    internal async Task StartAsync()
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            await RestartLock.WaitAsync().ConfigureAwait(false); // 等待获取锁,以确保只有一个线程可以执行以下代码
 | 
			
		||||
            TokenSource ??= new CancellationTokenSource();
 | 
			
		||||
            await StartAll(TokenSource.Token).ConfigureAwait(false);
 | 
			
		||||
            _logger.LogInformation(ThingsGateway.Gateway.Application.AppResource.RulesEngineTaskStart);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            _logger.LogError(ex, "Start"); // 记录错误日志
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            RestartLock.Release(); // 释放锁
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    internal async Task StopAsync()
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            await RestartLock.WaitAsync().ConfigureAwait(false); // 等待获取锁,以确保只有一个线程可以执行以下代码
 | 
			
		||||
            Cancel();
 | 
			
		||||
            Clear();
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            _logger.LogError(ex, "Stop"); // 记录错误日志
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            RestartLock.Release(); // 释放锁
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void Cancel()
 | 
			
		||||
    {
 | 
			
		||||
        if (TokenSource != null)
 | 
			
		||||
@@ -295,18 +221,65 @@ internal sealed class RulesEngineHostedService : BackgroundService, IRulesEngine
 | 
			
		||||
        Diagrams.Clear();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected override Task ExecuteAsync(CancellationToken stoppingToken)
 | 
			
		||||
    protected override async Task ExecuteAsync(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
    public override Task StartAsync(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        return StartAsync();
 | 
			
		||||
        await Task.Yield();
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            await RestartLock.WaitAsync(cancellationToken).ConfigureAwait(false); // 等待获取锁,以确保只有一个线程可以执行以下代码
 | 
			
		||||
            TokenSource ??= new CancellationTokenSource();
 | 
			
		||||
            Clear();
 | 
			
		||||
 | 
			
		||||
            Rules = await App.GetService<IRulesService>().GetAllAsync().ConfigureAwait(false);
 | 
			
		||||
            Diagrams = new();
 | 
			
		||||
            foreach (var rules in Rules.Where(a => a.Status))
 | 
			
		||||
            {
 | 
			
		||||
                var item = Init(rules);
 | 
			
		||||
                Start(item.rulesLog, item.blazorDiagram, TokenSource.Token);
 | 
			
		||||
            }
 | 
			
		||||
            dispatchService.Dispatch(null);
 | 
			
		||||
 | 
			
		||||
            _logger.LogInformation(ThingsGateway.Gateway.Application.AppResource.RulesEngineTaskStart);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            _logger.LogError(ex, "Start"); // 记录错误日志
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            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 Task StopAsync(CancellationToken cancellationToken)
 | 
			
		||||
    public override async Task StopAsync(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        return StopAsync();
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            await RestartLock.WaitAsync(cancellationToken).ConfigureAwait(false); // 等待获取锁,以确保只有一个线程可以执行以下代码
 | 
			
		||||
            Cancel();
 | 
			
		||||
            Clear();
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            _logger.LogError(ex, "Stop"); // 记录错误日志
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            RestartLock.Release(); // 释放锁
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,7 @@ using Microsoft.Extensions.Logging;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.Extension.Generic;
 | 
			
		||||
using ThingsGateway.NewLife.Collections;
 | 
			
		||||
using ThingsGateway.NewLife.DictionaryExtensions;
 | 
			
		||||
 | 
			
		||||
using TouchSocket.Core;
 | 
			
		||||
 | 
			
		||||
@@ -59,7 +60,7 @@ internal static class RuntimeServiceHelper
 | 
			
		||||
        //批量修改之后,需要重新加载通道
 | 
			
		||||
        foreach (var newChannelRuntime in newChannelRuntimes)
 | 
			
		||||
        {
 | 
			
		||||
            if (GlobalData.Channels.TryGetValue(newChannelRuntime.Id, out var channelRuntime))
 | 
			
		||||
            if (GlobalData.IdChannels.TryGetValue(newChannelRuntime.Id, out var channelRuntime))
 | 
			
		||||
            {
 | 
			
		||||
                channelRuntime.Dispose();
 | 
			
		||||
                newChannelRuntime.Init();
 | 
			
		||||
@@ -85,7 +86,7 @@ internal static class RuntimeServiceHelper
 | 
			
		||||
            {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                if (GlobalData.Channels.TryGetValue(newDeviceRuntime.ChannelId, out var newChannelRuntime))
 | 
			
		||||
                if (GlobalData.IdChannels.TryGetValue(newDeviceRuntime.ChannelId, out var newChannelRuntime))
 | 
			
		||||
                {
 | 
			
		||||
                    newDeviceRuntime.Init(newChannelRuntime);
 | 
			
		||||
 | 
			
		||||
@@ -121,7 +122,7 @@ internal static class RuntimeServiceHelper
 | 
			
		||||
            {
 | 
			
		||||
                deviceRuntime.Dispose();
 | 
			
		||||
            }
 | 
			
		||||
            if (GlobalData.Channels.TryGetValue(newDeviceRuntime.ChannelId, out var channelRuntime))
 | 
			
		||||
            if (GlobalData.IdChannels.TryGetValue(newDeviceRuntime.ChannelId, out var channelRuntime))
 | 
			
		||||
            {
 | 
			
		||||
                newDeviceRuntime.Init(channelRuntime);
 | 
			
		||||
            }
 | 
			
		||||
@@ -214,7 +215,7 @@ internal static class RuntimeServiceHelper
 | 
			
		||||
        //批量修改之后,需要重新加载通道
 | 
			
		||||
        foreach (var id in ids)
 | 
			
		||||
        {
 | 
			
		||||
            if (GlobalData.Channels.TryGetValue(id, out var channelRuntime))
 | 
			
		||||
            if (GlobalData.IdChannels.TryGetValue(id, out var channelRuntime))
 | 
			
		||||
            {
 | 
			
		||||
                channelRuntime.Dispose();
 | 
			
		||||
                var devs = channelRuntime.DeviceRuntimes.Select(a => a.Value).ToArray();
 | 
			
		||||
@@ -267,7 +268,7 @@ internal static class RuntimeServiceHelper
 | 
			
		||||
    public static async Task RemoveDeviceAsync(HashSet<long> newDeciceIds)
 | 
			
		||||
    {
 | 
			
		||||
        //先找出线程管理器,停止
 | 
			
		||||
        var deviceRuntimes = GlobalData.IdDevices.Where(a => newDeciceIds.Contains(a.Key)).Select(a => a.Value);
 | 
			
		||||
        var deviceRuntimes = GlobalData.IdDevices.FilterByKeys(newDeciceIds).Select(a => a.Value);
 | 
			
		||||
        await RemoveDeviceAsync(deviceRuntimes).ConfigureAwait(false);
 | 
			
		||||
    }
 | 
			
		||||
    public static async Task RemoveDeviceAsync(IEnumerable<DeviceRuntime> deviceRuntimes)
 | 
			
		||||
@@ -317,7 +318,7 @@ internal static class RuntimeServiceHelper
 | 
			
		||||
 | 
			
		||||
    public static void AddBusinessChangedDriver(HashSet<long> variableIds, ConcurrentHashSet<IDriver> changedDriver)
 | 
			
		||||
    {
 | 
			
		||||
        var data = GlobalData.IdVariables.Where(a => variableIds.Contains(a.Key)).GroupBy(a => a.Value.DeviceRuntime);
 | 
			
		||||
        var data = GlobalData.IdVariables.FilterByKeys(variableIds).GroupBy(a => a.Value.DeviceRuntime);
 | 
			
		||||
 | 
			
		||||
        foreach (var group in data)
 | 
			
		||||
        {
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,6 @@ using BootstrapBlazor.Components;
 | 
			
		||||
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
using System.Reflection;
 | 
			
		||||
using System.Security.Cryptography.X509Certificates;
 | 
			
		||||
 | 
			
		||||
using TouchSocket.Core;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,50 +4,66 @@ namespace ThingsGateway.Gateway.Razor
 | 
			
		||||
{
 | 
			
		||||
    public static class TreeViewItemMapper
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
        public static global::System.Collections.Generic.List<global::BootstrapBlazor.Components.TreeViewItem<global::ThingsGateway.Gateway.Razor.ChannelDeviceTreeItem>> AdaptListTreeViewItemChannelDeviceTreeItem(this global::System.Collections.Generic.List<global::BootstrapBlazor.Components.TreeViewItem<global::ThingsGateway.Gateway.Razor.ChannelDeviceTreeItem>> src, global::BootstrapBlazor.Components.TreeViewItem<global::ThingsGateway.Gateway.Razor.ChannelDeviceTreeItem> parent = null)
 | 
			
		||||
        public static global::System.Collections.Generic.List<global::BootstrapBlazor.Components.TreeViewItem<global::ThingsGateway.Gateway.Razor.ChannelDeviceTreeItem>> AdaptListTreeViewItemChannelDeviceTreeItem(this global::System.Collections.Generic.List<global::BootstrapBlazor.Components.TreeViewItem<global::ThingsGateway.Gateway.Razor.ChannelDeviceTreeItem>> src)
 | 
			
		||||
        {
 | 
			
		||||
            var target = new global::System.Collections.Generic.List<global::BootstrapBlazor.Components.TreeViewItem<global::ThingsGateway.Gateway.Razor.ChannelDeviceTreeItem>>(src.Count);
 | 
			
		||||
            foreach (var item in src)
 | 
			
		||||
            NormalizeItems(src);
 | 
			
		||||
            return src.ToList();
 | 
			
		||||
        }
 | 
			
		||||
        static void NormalizeItems<T>(List<TreeViewItem<T>> nodes)
 | 
			
		||||
        {
 | 
			
		||||
            foreach (var node in nodes)
 | 
			
		||||
            {
 | 
			
		||||
                target.Add(MapToTreeViewItemOfChannelDeviceTreeItem(item, parent));
 | 
			
		||||
                if (node.Items.Count > 0)
 | 
			
		||||
                {
 | 
			
		||||
                    node.Items = node.Items.ToList();
 | 
			
		||||
                    NormalizeItems(node.Items);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            return target;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        //public static global::System.Collections.Generic.List<global::BootstrapBlazor.Components.TreeViewItem<global::ThingsGateway.Gateway.Razor.ChannelDeviceTreeItem>> AdaptListTreeViewItemChannelDeviceTreeItem(this global::System.Collections.Generic.List<global::BootstrapBlazor.Components.TreeViewItem<global::ThingsGateway.Gateway.Razor.ChannelDeviceTreeItem>> src, global::BootstrapBlazor.Components.TreeViewItem<global::ThingsGateway.Gateway.Razor.ChannelDeviceTreeItem> parent = null)
 | 
			
		||||
        //{
 | 
			
		||||
        //    var target = new global::System.Collections.Generic.List<global::BootstrapBlazor.Components.TreeViewItem<global::ThingsGateway.Gateway.Razor.ChannelDeviceTreeItem>>(src.Count);
 | 
			
		||||
        //    foreach (var item in src)
 | 
			
		||||
        //    {
 | 
			
		||||
        //        target.Add(MapToTreeViewItemOfChannelDeviceTreeItem(item, parent));
 | 
			
		||||
        //    }
 | 
			
		||||
        //    return target;
 | 
			
		||||
        //}
 | 
			
		||||
 | 
			
		||||
        private static global::BootstrapBlazor.Components.TreeViewItem<global::ThingsGateway.Gateway.Razor.ChannelDeviceTreeItem> MapToTreeViewItemOfChannelDeviceTreeItem(global::BootstrapBlazor.Components.TreeViewItem<global::ThingsGateway.Gateway.Razor.ChannelDeviceTreeItem> source, global::BootstrapBlazor.Components.TreeViewItem<global::ThingsGateway.Gateway.Razor.ChannelDeviceTreeItem> parent)
 | 
			
		||||
        {
 | 
			
		||||
            var target = new global::BootstrapBlazor.Components.TreeViewItem<global::ThingsGateway.Gateway.Razor.ChannelDeviceTreeItem>(source.Value);
 | 
			
		||||
            target.CheckedState = source.CheckedState;
 | 
			
		||||
            target.Items = AdaptListTreeViewItemChannelDeviceTreeItem(source.Items.ToList(), target);
 | 
			
		||||
            if (source.Parent != null && parent != null)
 | 
			
		||||
            {
 | 
			
		||||
                target.Parent = parent;
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                target.Parent = null;
 | 
			
		||||
            }
 | 
			
		||||
            target.Text = source.Text;
 | 
			
		||||
            target.Icon = source.Icon;
 | 
			
		||||
            target.ExpandIcon = source.ExpandIcon;
 | 
			
		||||
            target.CssClass = source.CssClass;
 | 
			
		||||
            target.IsDisabled = source.IsDisabled;
 | 
			
		||||
            target.IsActive = source.IsActive;
 | 
			
		||||
            target.Template = source.Template;
 | 
			
		||||
            if (source.Value != null)
 | 
			
		||||
            {
 | 
			
		||||
                target.Value = source.Value;
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                target.Value = null;
 | 
			
		||||
            }
 | 
			
		||||
            target.IsExpand = source.IsExpand;
 | 
			
		||||
            target.HasChildren = source.HasChildren;
 | 
			
		||||
            return target;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        //private static global::BootstrapBlazor.Components.TreeViewItem<global::ThingsGateway.Gateway.Razor.ChannelDeviceTreeItem> MapToTreeViewItemOfChannelDeviceTreeItem(global::BootstrapBlazor.Components.TreeViewItem<global::ThingsGateway.Gateway.Razor.ChannelDeviceTreeItem> source, global::BootstrapBlazor.Components.TreeViewItem<global::ThingsGateway.Gateway.Razor.ChannelDeviceTreeItem> parent)
 | 
			
		||||
        //{
 | 
			
		||||
        //    var target = new global::BootstrapBlazor.Components.TreeViewItem<global::ThingsGateway.Gateway.Razor.ChannelDeviceTreeItem>(source.Value);
 | 
			
		||||
        //    target.CheckedState = source.CheckedState;
 | 
			
		||||
        //    target.Items = AdaptListTreeViewItemChannelDeviceTreeItem(source.Items.ToList(), target);
 | 
			
		||||
        //    if (source.Parent != null && parent != null)
 | 
			
		||||
        //    {
 | 
			
		||||
        //        target.Parent = parent;
 | 
			
		||||
        //    }
 | 
			
		||||
        //    else
 | 
			
		||||
        //    {
 | 
			
		||||
        //        target.Parent = null;
 | 
			
		||||
        //    }
 | 
			
		||||
        //    target.Text = source.Text;
 | 
			
		||||
        //    target.Icon = source.Icon;
 | 
			
		||||
        //    target.ExpandIcon = source.ExpandIcon;
 | 
			
		||||
        //    target.CssClass = source.CssClass;
 | 
			
		||||
        //    target.IsDisabled = source.IsDisabled;
 | 
			
		||||
        //    target.IsActive = source.IsActive;
 | 
			
		||||
        //    target.Template = source.Template;
 | 
			
		||||
        //    if (source.Value != null)
 | 
			
		||||
        //    {
 | 
			
		||||
        //        target.Value = source.Value;
 | 
			
		||||
        //    }
 | 
			
		||||
        //    else
 | 
			
		||||
        //    {
 | 
			
		||||
        //        target.Value = null;
 | 
			
		||||
        //    }
 | 
			
		||||
        //    target.IsExpand = source.IsExpand;
 | 
			
		||||
        //    target.HasChildren = source.HasChildren;
 | 
			
		||||
        //    return target;
 | 
			
		||||
        //}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -57,8 +57,9 @@ public partial class ChannelTable : IDisposable
 | 
			
		||||
    {
 | 
			
		||||
        scheduler.Trigger();
 | 
			
		||||
    }
 | 
			
		||||
    private async Task Notify()
 | 
			
		||||
    private async Task Notify(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        if (cancellationToken.IsCancellationRequested) return;
 | 
			
		||||
        if (Disposed) return;
 | 
			
		||||
        if (table != null)
 | 
			
		||||
            await InvokeAsync(table.QueryAsync);
 | 
			
		||||
@@ -252,10 +253,10 @@ public partial class ChannelTable : IDisposable
 | 
			
		||||
                    ret = await GatewayExportService.OnChannelExport(new() { QueryPageOptions = new() { SortName = _option.SortName, SortOrder = _option.SortOrder }, PluginName = SelectModel.PluginName });
 | 
			
		||||
                    break;
 | 
			
		||||
                case ChannelDevicePluginTypeEnum.Channel:
 | 
			
		||||
                    ret = await GatewayExportService.OnChannelExport(new() { QueryPageOptions = new() { SortName = _option.SortName, SortOrder = _option.SortOrder }, ChannelId = SelectModel.ChannelRuntime.Id });
 | 
			
		||||
                    ret = await GatewayExportService.OnChannelExport(new() { QueryPageOptions = new() { SortName = _option.SortName, SortOrder = _option.SortOrder }, ChannelId = SelectModel.ChannelRuntimeId });
 | 
			
		||||
                    break;
 | 
			
		||||
                case ChannelDevicePluginTypeEnum.Device:
 | 
			
		||||
                    ret = await GatewayExportService.OnChannelExport(new() { QueryPageOptions = new() { SortName = _option.SortName, SortOrder = _option.SortOrder }, DeviceId = SelectModel.DeviceRuntime.Id, PluginType = SelectModel.DeviceRuntime.PluginType });
 | 
			
		||||
                    ret = await GatewayExportService.OnChannelExport(new() { QueryPageOptions = new() { SortName = _option.SortName, SortOrder = _option.SortOrder }, DeviceId = SelectModel.DeviceRuntimeId, PluginType = SelectModel.TryGetDeviceRuntime(out var deviceRuntime) ? deviceRuntime?.PluginType : null });
 | 
			
		||||
                    break;
 | 
			
		||||
                default:
 | 
			
		||||
                    ret = await GatewayExportService.OnChannelExport(new() { QueryPageOptions = new() });
 | 
			
		||||
 
 | 
			
		||||
@@ -1240,19 +1240,19 @@ EventCallback.Factory.Create<MouseEventArgs>(this, async e =>
 | 
			
		||||
        UnknownTreeViewItem = new TreeViewItem<ChannelDeviceTreeItem>(UnknownItem)
 | 
			
		||||
        {
 | 
			
		||||
            Text = GatewayLocalizer["Unknown"],
 | 
			
		||||
            IsActive = Value == UnknownItem,
 | 
			
		||||
            IsActive = false,
 | 
			
		||||
            IsExpand = true,
 | 
			
		||||
        };
 | 
			
		||||
        ZItem = new List<TreeViewItem<ChannelDeviceTreeItem>>() {new TreeViewItem<ChannelDeviceTreeItem>(CollectItem)
 | 
			
		||||
        {
 | 
			
		||||
            Text = GatewayLocalizer["Collect"],
 | 
			
		||||
            IsActive = Value == CollectItem,
 | 
			
		||||
            IsActive = false,
 | 
			
		||||
            IsExpand = true,
 | 
			
		||||
        },
 | 
			
		||||
        new TreeViewItem<ChannelDeviceTreeItem>(BusinessItem)
 | 
			
		||||
        {
 | 
			
		||||
            Text = GatewayLocalizer["Business"],
 | 
			
		||||
            IsActive = Value == BusinessItem,
 | 
			
		||||
            IsActive = false,
 | 
			
		||||
            IsExpand = true,
 | 
			
		||||
        }};
 | 
			
		||||
 | 
			
		||||
@@ -1313,8 +1313,9 @@ EventCallback.Factory.Create<MouseEventArgs>(this, async e =>
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    private async Task Notify()
 | 
			
		||||
    private async Task Notify(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        if (cancellationToken.IsCancellationRequested) return;
 | 
			
		||||
        if (Disposed) return;
 | 
			
		||||
        await OnClickSearch(SearchText);
 | 
			
		||||
 | 
			
		||||
@@ -1339,14 +1340,14 @@ EventCallback.Factory.Create<MouseEventArgs>(this, async e =>
 | 
			
		||||
            case ChannelDevicePluginTypeEnum.Channel:
 | 
			
		||||
                return new ChannelDeviceTreeItem()
 | 
			
		||||
                {
 | 
			
		||||
                    ChannelRuntime = GlobalData.ReadOnlyChannels.TryGetValue(channelDeviceTreeItem.ChannelRuntime?.Id ?? 0, out var channel) ? channel : channelDeviceTreeItem.ChannelRuntime,
 | 
			
		||||
                    ChannelRuntimeId = channelDeviceTreeItem.ChannelRuntimeId,
 | 
			
		||||
                    ChannelDevicePluginType = ChannelDevicePluginTypeEnum.Channel
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
            case ChannelDevicePluginTypeEnum.Device:
 | 
			
		||||
                return new ChannelDeviceTreeItem()
 | 
			
		||||
                {
 | 
			
		||||
                    DeviceRuntime = GlobalData.ReadOnlyIdDevices.TryGetValue(channelDeviceTreeItem.DeviceRuntime?.Id ?? 0, out var device) ? device : channelDeviceTreeItem.DeviceRuntime,
 | 
			
		||||
                    DeviceRuntimeId = channelDeviceTreeItem.DeviceRuntimeId,
 | 
			
		||||
                    ChannelDevicePluginType = ChannelDevicePluginTypeEnum.Device
 | 
			
		||||
                };
 | 
			
		||||
        }
 | 
			
		||||
@@ -1499,11 +1500,11 @@ EventCallback.Factory.Create<MouseEventArgs>(this, async e =>
 | 
			
		||||
        switch (x.ChannelDevicePluginType)
 | 
			
		||||
        {
 | 
			
		||||
            case ChannelDevicePluginTypeEnum.Device:
 | 
			
		||||
                return x.DeviceRuntime.Id == y.DeviceRuntime.Id;
 | 
			
		||||
                return x.DeviceRuntimeId == y.DeviceRuntimeId;
 | 
			
		||||
            case ChannelDevicePluginTypeEnum.PluginType:
 | 
			
		||||
                return x.PluginType == y.PluginType;
 | 
			
		||||
            case ChannelDevicePluginTypeEnum.Channel:
 | 
			
		||||
                return x.ChannelRuntime.Id == y.ChannelRuntime.Id;
 | 
			
		||||
                return x.ChannelRuntimeId == y.ChannelRuntimeId;
 | 
			
		||||
            case ChannelDevicePluginTypeEnum.PluginName:
 | 
			
		||||
                return x.PluginName == y.PluginName;
 | 
			
		||||
            default:
 | 
			
		||||
@@ -1532,13 +1533,5 @@ EventCallback.Factory.Create<MouseEventArgs>(this, async e =>
 | 
			
		||||
        }
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
    protected override void OnAfterRender(bool firstRender)
 | 
			
		||||
    {
 | 
			
		||||
        base.OnAfterRender(firstRender);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void OnUpdateCallbackAsync()
 | 
			
		||||
    {
 | 
			
		||||
        throw new NotImplementedException();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -22,8 +22,12 @@ public class ChannelDeviceTreeItem : IEqualityComparer<ChannelDeviceTreeItem>
 | 
			
		||||
    public long Id { get; set; }
 | 
			
		||||
    public ChannelDevicePluginTypeEnum ChannelDevicePluginType { get; set; }
 | 
			
		||||
 | 
			
		||||
    public DeviceRuntime DeviceRuntime { get; set; }
 | 
			
		||||
    public ChannelRuntime ChannelRuntime { get; set; }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public long DeviceRuntimeId { get; set; }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public long ChannelRuntimeId { get; set; }
 | 
			
		||||
    public string PluginName { get; set; }
 | 
			
		||||
    public PluginTypeEnum? PluginType { get; set; }
 | 
			
		||||
 | 
			
		||||
@@ -37,11 +41,11 @@ public class ChannelDeviceTreeItem : IEqualityComparer<ChannelDeviceTreeItem>
 | 
			
		||||
            switch (ChannelDevicePluginType)
 | 
			
		||||
            {
 | 
			
		||||
                case ChannelDevicePluginTypeEnum.Device:
 | 
			
		||||
                    return DeviceRuntime == item.DeviceRuntime;
 | 
			
		||||
                    return DeviceRuntimeId == item.DeviceRuntimeId;
 | 
			
		||||
                case ChannelDevicePluginTypeEnum.PluginType:
 | 
			
		||||
                    return PluginType == item.PluginType;
 | 
			
		||||
                case ChannelDevicePluginTypeEnum.Channel:
 | 
			
		||||
                    return ChannelRuntime == item.ChannelRuntime;
 | 
			
		||||
                    return ChannelRuntimeId == item.ChannelRuntimeId;
 | 
			
		||||
                case ChannelDevicePluginTypeEnum.PluginName:
 | 
			
		||||
                    return PluginName == item.PluginName;
 | 
			
		||||
                default:
 | 
			
		||||
@@ -54,21 +58,22 @@ public class ChannelDeviceTreeItem : IEqualityComparer<ChannelDeviceTreeItem>
 | 
			
		||||
 | 
			
		||||
    public override int GetHashCode()
 | 
			
		||||
    {
 | 
			
		||||
        return HashCode.Combine(ChannelDevicePluginType, DeviceRuntime, ChannelRuntime, PluginName, PluginType);
 | 
			
		||||
        return HashCode.Combine(ChannelDevicePluginType, DeviceRuntimeId, ChannelRuntimeId, PluginName, PluginType);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool TryGetDeviceRuntime(out DeviceRuntime deviceRuntime)
 | 
			
		||||
    {
 | 
			
		||||
        if (ChannelDevicePluginType == ChannelDevicePluginTypeEnum.Device && DeviceRuntime?.Id > 0)
 | 
			
		||||
        if (ChannelDevicePluginType == ChannelDevicePluginTypeEnum.Device && DeviceRuntimeId > 0)
 | 
			
		||||
        {
 | 
			
		||||
            deviceRuntime = DeviceRuntime;
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            deviceRuntime = null;
 | 
			
		||||
            return false;
 | 
			
		||||
            if (GlobalData.ReadOnlyIdDevices.TryGetValue(DeviceRuntimeId, out deviceRuntime))
 | 
			
		||||
            {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        deviceRuntime = null;
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool TryGetPluginName(out string pluginName)
 | 
			
		||||
    {
 | 
			
		||||
        if (ChannelDevicePluginType == ChannelDevicePluginTypeEnum.PluginName)
 | 
			
		||||
@@ -98,16 +103,15 @@ public class ChannelDeviceTreeItem : IEqualityComparer<ChannelDeviceTreeItem>
 | 
			
		||||
    }
 | 
			
		||||
    public bool TryGetChannelRuntime(out ChannelRuntime channelRuntime)
 | 
			
		||||
    {
 | 
			
		||||
        if (ChannelDevicePluginType == ChannelDevicePluginTypeEnum.Channel && ChannelRuntime?.Id > 0)
 | 
			
		||||
        if (ChannelDevicePluginType == ChannelDevicePluginTypeEnum.Channel && ChannelRuntimeId > 0)
 | 
			
		||||
        {
 | 
			
		||||
            channelRuntime = ChannelRuntime;
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            channelRuntime = null;
 | 
			
		||||
            return false;
 | 
			
		||||
            if (GlobalData.ReadOnlyIdChannels.TryGetValue(ChannelRuntimeId, out channelRuntime))
 | 
			
		||||
            {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        channelRuntime = null;
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -115,11 +119,11 @@ public class ChannelDeviceTreeItem : IEqualityComparer<ChannelDeviceTreeItem>
 | 
			
		||||
    {
 | 
			
		||||
        if (ChannelDevicePluginType == ChannelDevicePluginTypeEnum.Device)
 | 
			
		||||
        {
 | 
			
		||||
            return DeviceRuntime?.ToString();
 | 
			
		||||
            return TryGetDeviceRuntime(out var deviceRuntime) ? deviceRuntime?.ToString() : string.Empty;
 | 
			
		||||
        }
 | 
			
		||||
        else if (ChannelDevicePluginType == ChannelDevicePluginTypeEnum.Channel)
 | 
			
		||||
        {
 | 
			
		||||
            return ChannelRuntime?.ToString();
 | 
			
		||||
            return TryGetChannelRuntime(out var channelRuntime) ? channelRuntime?.ToString() : string.Empty;
 | 
			
		||||
        }
 | 
			
		||||
        else if (ChannelDevicePluginType == ChannelDevicePluginTypeEnum.PluginName)
 | 
			
		||||
        {
 | 
			
		||||
@@ -139,6 +143,6 @@ public class ChannelDeviceTreeItem : IEqualityComparer<ChannelDeviceTreeItem>
 | 
			
		||||
 | 
			
		||||
    public int GetHashCode([DisallowNull] ChannelDeviceTreeItem obj)
 | 
			
		||||
    {
 | 
			
		||||
        return HashCode.Combine(obj.ChannelDevicePluginType, obj.DeviceRuntime, obj.ChannelRuntime, obj.PluginName, obj.PluginType);
 | 
			
		||||
        return HashCode.Combine(obj.ChannelDevicePluginType, obj.DeviceRuntimeId, obj.ChannelRuntimeId, obj.PluginName, obj.PluginType);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -61,7 +61,7 @@
 | 
			
		||||
                        <EditTemplate Context="value">
 | 
			
		||||
                            <div class="col-12  col-md-6 ">
 | 
			
		||||
                                <BootstrapInputGroup>
 | 
			
		||||
                                    <Select IsVirtualize DefaultVirtualizeItemText=@(GlobalData.ReadOnlyChannels.TryGetValue(value.ChannelId,out var channelRuntime)?channelRuntime.Name:string.Empty) @bind-Value="@value.ChannelId" IsDisabled=BatchEditEnable Items="@_channelItems" OnSelectedItemChanged=OnChannelChanged ShowSearch="true" ShowLabel="true" />
 | 
			
		||||
                                    <Select IsVirtualize DefaultVirtualizeItemText=@(GlobalData.ReadOnlyIdChannels.TryGetValue(value.ChannelId,out var channelRuntime)?channelRuntime.Name:string.Empty) @bind-Value="@value.ChannelId" IsDisabled=BatchEditEnable Items="@_channelItems" OnSelectedItemChanged=OnChannelChanged ShowSearch="true" ShowLabel="true" />
 | 
			
		||||
                                    <Button IsDisabled=BatchEditEnable class="text-end" Icon="fa-solid fa-plus" OnClick="AddChannel"></Button>
 | 
			
		||||
                                </BootstrapInputGroup>
 | 
			
		||||
                            </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -111,7 +111,7 @@ public partial class DeviceEditComponent
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        var devices = await GlobalData.GetCurrentUserDevices().ConfigureAwait(false);
 | 
			
		||||
        var pluginName = GlobalData.ReadOnlyChannels.TryGetValue(device.ChannelId, out var channel) ? channel.PluginName : string.Empty;
 | 
			
		||||
        var pluginName = GlobalData.ReadOnlyIdChannels.TryGetValue(device.ChannelId, out var channel) ? channel.PluginName : string.Empty;
 | 
			
		||||
        var items = new List<SelectedItem>() { new SelectedItem(string.Empty, "none") }.Concat(devices.WhereIf(!option.SearchText.IsNullOrWhiteSpace(), a => a.Name.Contains(option.SearchText))
 | 
			
		||||
            .Where(a => a.PluginName == pluginName && a.Id != device.Id).Take(20).BuildDeviceSelectList()
 | 
			
		||||
            );
 | 
			
		||||
@@ -128,7 +128,7 @@ public partial class DeviceEditComponent
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var pluginName = GlobalData.ReadOnlyChannels.TryGetValue(selectedItem.Value.ToLong(), out var channel) ? channel.PluginName : string.Empty;
 | 
			
		||||
            var pluginName = GlobalData.ReadOnlyIdChannels.TryGetValue(selectedItem.Value.ToLong(), out var channel) ? channel.PluginName : string.Empty;
 | 
			
		||||
            if (pluginName.IsNullOrEmpty()) return;
 | 
			
		||||
            var data = GlobalData.PluginService.GetDriverPropertyTypes(pluginName);
 | 
			
		||||
            Model.ModelValueValidateForm = new ModelValueValidateForm() { Value = data.Model };
 | 
			
		||||
 
 | 
			
		||||
@@ -56,8 +56,9 @@ public partial class DeviceTable : IDisposable
 | 
			
		||||
    {
 | 
			
		||||
        scheduler.Trigger();
 | 
			
		||||
    }
 | 
			
		||||
    private async Task Notify()
 | 
			
		||||
    private async Task Notify(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        if (cancellationToken.IsCancellationRequested) return;
 | 
			
		||||
        if (Disposed) return;
 | 
			
		||||
        if (table != null)
 | 
			
		||||
            await InvokeAsync(table.QueryAsync);
 | 
			
		||||
@@ -253,10 +254,10 @@ public partial class DeviceTable : IDisposable
 | 
			
		||||
                    ret = await GatewayExportService.OnDeviceExport(new() { QueryPageOptions = new() { SortName = _option.SortName, SortOrder = _option.SortOrder }, PluginName = SelectModel.PluginName });
 | 
			
		||||
                    break;
 | 
			
		||||
                case ChannelDevicePluginTypeEnum.Channel:
 | 
			
		||||
                    ret = await GatewayExportService.OnDeviceExport(new() { QueryPageOptions = new() { SortName = _option.SortName, SortOrder = _option.SortOrder }, ChannelId = SelectModel.ChannelRuntime.Id });
 | 
			
		||||
                    ret = await GatewayExportService.OnDeviceExport(new() { QueryPageOptions = new() { SortName = _option.SortName, SortOrder = _option.SortOrder }, ChannelId = SelectModel.ChannelRuntimeId });
 | 
			
		||||
                    break;
 | 
			
		||||
                case ChannelDevicePluginTypeEnum.Device:
 | 
			
		||||
                    ret = await GatewayExportService.OnDeviceExport(new() { QueryPageOptions = new() { SortName = _option.SortName, SortOrder = _option.SortOrder }, DeviceId = SelectModel.DeviceRuntime.Id, PluginType = SelectModel.DeviceRuntime.PluginType });
 | 
			
		||||
                    ret = await GatewayExportService.OnDeviceExport(new() { QueryPageOptions = new() { SortName = _option.SortName, SortOrder = _option.SortOrder }, DeviceId = SelectModel.DeviceRuntimeId, PluginType = SelectModel.TryGetDeviceRuntime(out var deviceRuntime) ? deviceRuntime?.PluginType : null });
 | 
			
		||||
                    break;
 | 
			
		||||
                default:
 | 
			
		||||
                    ret = await GatewayExportService.OnDeviceExport(new() { QueryPageOptions = new() { SortName = _option.SortName, SortOrder = _option.SortOrder } });
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,7 @@ else if (ShowType == ShowTypeEnum.LogInfo)
 | 
			
		||||
    {
 | 
			
		||||
        <DeviceRuntimeInfo DeviceRuntime="device" />
 | 
			
		||||
    }
 | 
			
		||||
    if (GlobalData.ReadOnlyChannels.TryGetValue(ShowChannelRuntime, out var channel))
 | 
			
		||||
    if (GlobalData.ReadOnlyIdChannels.TryGetValue(ShowChannelRuntime, out var channel))
 | 
			
		||||
    {
 | 
			
		||||
        <ChannelRuntimeInfo ChannelRuntime="channel" />
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -84,8 +84,9 @@ public partial class VariableRuntimeInfo : IDisposable
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task Notify()
 | 
			
		||||
    private async Task Notify(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        if (cancellationToken.IsCancellationRequested) return;
 | 
			
		||||
        if (Disposed) return;
 | 
			
		||||
        if (table != null)
 | 
			
		||||
            await InvokeAsync(table.QueryAsync);
 | 
			
		||||
@@ -286,7 +287,7 @@ public partial class VariableRuntimeInfo : IDisposable
 | 
			
		||||
    {
 | 
			
		||||
        return Task.FromResult(new VariableRuntime()
 | 
			
		||||
        {
 | 
			
		||||
            DeviceId = SelectModel?.DeviceRuntime?.IsCollect == true ? SelectModel?.DeviceRuntime?.Id ?? 0 : 0
 | 
			
		||||
            DeviceId = SelectModel?.TryGetDeviceRuntime(out var deviceRuntime) == true ? deviceRuntime?.IsCollect == true ? deviceRuntime?.Id ?? 0 : 0 : 0
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -312,10 +313,10 @@ public partial class VariableRuntimeInfo : IDisposable
 | 
			
		||||
                    ret = await GatewayExportService.OnVariableExport(new() { QueryPageOptions = new() { SortName = _option.SortName, SortOrder = _option.SortOrder }, PluginName = SelectModel.PluginName });
 | 
			
		||||
                    break;
 | 
			
		||||
                case ChannelDevicePluginTypeEnum.Channel:
 | 
			
		||||
                    ret = await GatewayExportService.OnVariableExport(new() { QueryPageOptions = new() { SortName = _option.SortName, SortOrder = _option.SortOrder }, ChannelId = SelectModel.ChannelRuntime.Id });
 | 
			
		||||
                    ret = await GatewayExportService.OnVariableExport(new() { QueryPageOptions = new() { SortName = _option.SortName, SortOrder = _option.SortOrder }, ChannelId = SelectModel.ChannelRuntimeId });
 | 
			
		||||
                    break;
 | 
			
		||||
                case ChannelDevicePluginTypeEnum.Device:
 | 
			
		||||
                    ret = await GatewayExportService.OnVariableExport(new() { QueryPageOptions = new() { SortName = _option.SortName, SortOrder = _option.SortOrder }, DeviceId = SelectModel.DeviceRuntime.Id, PluginType = SelectModel.DeviceRuntime.PluginType });
 | 
			
		||||
                    ret = await GatewayExportService.OnVariableExport(new() { QueryPageOptions = new() { SortName = _option.SortName, SortOrder = _option.SortOrder }, DeviceId = SelectModel.DeviceRuntimeId, PluginType = SelectModel.TryGetDeviceRuntime(out var deviceRuntime) ? deviceRuntime?.PluginType : null });
 | 
			
		||||
                    break;
 | 
			
		||||
                default:
 | 
			
		||||
                    ret = await GatewayExportService.OnVariableExport(new() { QueryPageOptions = new() { SortName = _option.SortName, SortOrder = _option.SortOrder } });
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,7 @@
 | 
			
		||||
 | 
			
		||||
        <EditComponent ItemsPerRow=1 Model="Model" OnSave="OnSaveRedundancy" />
 | 
			
		||||
 | 
			
		||||
        <Button IsDisabled=@(Model.IsMaster||(!Model.Enable)) OnClick="ForcedSync">@RedundancyLocalizer["ForcedSync"]</Button>
 | 
			
		||||
        <Button IsDisabled=@(!Model.IsMaster||(!Model.Enable)) OnClick="ForcedSync">@RedundancyLocalizer["ForcedSync"]</Button>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="col-12 col-md-6 h-100">
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -74,12 +74,10 @@ public partial class RedundancyOptionsPage
 | 
			
		||||
                await RedundancyService.EditRedundancyOptionAsync(Model);
 | 
			
		||||
                await ToastService.Success(RedundancyLocalizer[nameof(RedundancyOptions)], $"{RazorLocalizer["Save"]}{RazorLocalizer["Success"]}");
 | 
			
		||||
 | 
			
		||||
                await RedundancyHostedService.StopRedundancyTaskAsync();
 | 
			
		||||
                var result = await RedundancyHostedService.StartRedundancyTaskAsync();
 | 
			
		||||
                if (result.IsSuccess)
 | 
			
		||||
                    await ToastService.Success(RedundancyLocalizer[nameof(RedundancyOptions)], $"{RazorLocalizer["Success"]}");
 | 
			
		||||
                else
 | 
			
		||||
                    await ToastService.Warning(RedundancyLocalizer[nameof(RedundancyOptions)], $"{RazorLocalizer["Fail", result.ToString()]}");
 | 
			
		||||
                await RedundancyHostedService.StopTaskAsync();
 | 
			
		||||
                await RedundancyHostedService.StartTaskAsync(CancellationToken.None);
 | 
			
		||||
                await ToastService.Success(RedundancyLocalizer[nameof(RedundancyOptions)], $"{RazorLocalizer["Success"]}");
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                await InvokeAsync(StateHasChanged);
 | 
			
		||||
 
 | 
			
		||||
@@ -118,7 +118,7 @@ public static class ResourceUtil
 | 
			
		||||
            {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                var channelRuntimeTreeItem = new ChannelDeviceTreeItem() { ChannelDevicePluginType = ChannelDevicePluginTypeEnum.Channel, ChannelRuntime = channelRuntime, Id = channelRuntime.Id };
 | 
			
		||||
                var channelRuntimeTreeItem = new ChannelDeviceTreeItem() { ChannelDevicePluginType = ChannelDevicePluginTypeEnum.Channel, ChannelRuntimeId = channelRuntime.Id, Id = channelRuntime.Id };
 | 
			
		||||
 | 
			
		||||
                var channelTreeItemItem = channelOldItems.FirstOrDefault(a => a.Value.Equals(channelRuntimeTreeItem));
 | 
			
		||||
 | 
			
		||||
@@ -142,7 +142,7 @@ public static class ResourceUtil
 | 
			
		||||
                foreach (var keyValue in channelRuntime.ReadDeviceRuntimes.OrderBy(a => a.Value.DeviceStatus))
 | 
			
		||||
                {
 | 
			
		||||
                    var deviceRuntime = keyValue.Value;
 | 
			
		||||
                    var deviceRuntimeTreeItem = new ChannelDeviceTreeItem() { ChannelDevicePluginType = ChannelDevicePluginTypeEnum.Device, DeviceRuntime = deviceRuntime, Id = deviceRuntime.Id };
 | 
			
		||||
                    var deviceRuntimeTreeItem = new ChannelDeviceTreeItem() { ChannelDevicePluginType = ChannelDevicePluginTypeEnum.Device, DeviceRuntimeId = deviceRuntime.Id, Id = deviceRuntime.Id };
 | 
			
		||||
 | 
			
		||||
                    var deviceTreeItemItem = deviceOldItems.FirstOrDefault(a => a.Value.Equals(deviceRuntimeTreeItem));
 | 
			
		||||
 | 
			
		||||
@@ -204,7 +204,7 @@ public static class ResourceUtil
 | 
			
		||||
 | 
			
		||||
            foreach (var channelRuntime in dict.Where(a => a.Key.PluginName == pluginName))
 | 
			
		||||
            {
 | 
			
		||||
                var channelRuntimeTreeItem = new ChannelDeviceTreeItem() { ChannelDevicePluginType = ChannelDevicePluginTypeEnum.Channel, ChannelRuntime = channelRuntime.Key, Id = channelRuntime.Key.Id };
 | 
			
		||||
                var channelRuntimeTreeItem = new ChannelDeviceTreeItem() { ChannelDevicePluginType = ChannelDevicePluginTypeEnum.Channel, ChannelRuntimeId = channelRuntime.Key.Id, Id = channelRuntime.Key.Id };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                var channelTreeItemItem = channelOldItems.FirstOrDefault(a => a.Value.Equals(channelRuntimeTreeItem));
 | 
			
		||||
@@ -226,7 +226,7 @@ public static class ResourceUtil
 | 
			
		||||
 | 
			
		||||
                foreach (var deviceRuntime in channelRuntime.Value.OrderBy(a => a.DeviceStatus))
 | 
			
		||||
                {
 | 
			
		||||
                    var deviceRuntimeTreeItem = new ChannelDeviceTreeItem() { ChannelDevicePluginType = ChannelDevicePluginTypeEnum.Device, DeviceRuntime = deviceRuntime, Id = deviceRuntime.Id };
 | 
			
		||||
                    var deviceRuntimeTreeItem = new ChannelDeviceTreeItem() { ChannelDevicePluginType = ChannelDevicePluginTypeEnum.Device, DeviceRuntimeId = deviceRuntime.Id, Id = deviceRuntime.Id };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -116,7 +116,8 @@ public static class JsonUtils
 | 
			
		||||
                    case BuiltInType.Int64: { return decoder.ReadInt64(fieldName); }
 | 
			
		||||
                    case BuiltInType.UInt64: { return decoder.ReadUInt64(fieldName); }
 | 
			
		||||
                    case BuiltInType.Float: { return decoder.ReadFloat(fieldName); }
 | 
			
		||||
                    case BuiltInType.Double: { return decoder.ReadDouble(fieldName); }
 | 
			
		||||
                    case BuiltInType.Double:
 | 
			
		||||
                    case BuiltInType.Number: { return decoder.ReadDouble(fieldName); }
 | 
			
		||||
                    case BuiltInType.String:
 | 
			
		||||
                        {
 | 
			
		||||
                            if (decoder.ReadField(fieldName, out var value))
 | 
			
		||||
@@ -288,7 +289,7 @@ public static class JsonUtils
 | 
			
		||||
                    }
 | 
			
		||||
                case BuiltInType.Number:
 | 
			
		||||
                    {
 | 
			
		||||
                        encoder.WriteInt64(fieldName, Convert.ToInt64(value));
 | 
			
		||||
                        encoder.WriteDouble(fieldName, Convert.ToInt64(value));
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
                case BuiltInType.UInteger:
 | 
			
		||||
 
 | 
			
		||||
@@ -49,11 +49,10 @@ public class SiemensS7Test
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        });
 | 
			
		||||
        await Task.Delay(100);
 | 
			
		||||
        bytes[12] = (byte)(((IClientChannel)(siemensS7Master.Channel)).WaitHandlePool.GetValue("m_currentSign").ToInt() - 1);
 | 
			
		||||
        var task2 = Task.Run(async () =>
 | 
			
		||||
        {
 | 
			
		||||
            await Task.Delay(100).ConfigureAwait(false);
 | 
			
		||||
            bytes[12] = (byte)(((IClientChannel)(siemensS7Master.Channel)).WaitHandlePool.GetValue("m_currentSign").ToInt() - 1);
 | 
			
		||||
            foreach (var item in bytes)
 | 
			
		||||
            {
 | 
			
		||||
                var data = new ByteBlock([item]);
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										30
									
								
								src/Plugin/ThingsGateway.Plugin.DB/Mapper/GatewayMapper.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/Plugin/ThingsGateway.Plugin.DB/Mapper/GatewayMapper.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
//  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
 | 
			
		||||
//  此代码版权(除特别声明外的代码)归作者本人Diego所有
 | 
			
		||||
//  源代码使用协议遵循本仓库的开源协议及附加协议
 | 
			
		||||
//  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
 | 
			
		||||
//  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
 | 
			
		||||
//  使用文档:https://thingsgateway.cn/
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using Riok.Mapperly.Abstractions;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.Admin.Application;
 | 
			
		||||
using ThingsGateway.Plugin.SqlHistoryAlarm;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Plugin.DB;
 | 
			
		||||
[Mapper(UseDeepCloning = true, EnumMappingStrategy = EnumMappingStrategy.ByName, RequiredMappingStrategy = RequiredMappingStrategy.None)]
 | 
			
		||||
public static partial class GatewayMapper
 | 
			
		||||
{
 | 
			
		||||
    public static partial List<HistoryAlarm> AdaptListHistoryAlarm(this IEnumerable<AlarmVariable> src);
 | 
			
		||||
 | 
			
		||||
    [MapProperty(nameof(AlarmVariable.Id), nameof(HistoryAlarm.Id), Use = nameof(MapId))]
 | 
			
		||||
    private static partial HistoryAlarm AdaptHistoryAlarm(AlarmVariable src);
 | 
			
		||||
 | 
			
		||||
    [UserMapping(Default = false)]
 | 
			
		||||
    private static long MapId(long id) => CommonUtils.GetSingleId();
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -23,8 +23,10 @@ namespace ThingsGateway.Plugin.QuestDB;
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// QuestDBProducer
 | 
			
		||||
/// </summary>
 | 
			
		||||
public partial class QuestDBProducer : BusinessBaseWithCacheIntervalVariableModel<VariableBasicData>, IDBHistoryValueService
 | 
			
		||||
public partial class QuestDBProducer : BusinessBaseWithCacheIntervalVariable, IDBHistoryValueService
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    internal readonly RealDBProducerProperty _driverPropertys = new();
 | 
			
		||||
    private readonly QuestDBProducerVariableProperty _variablePropertys = new();
 | 
			
		||||
 | 
			
		||||
@@ -45,6 +47,9 @@ public partial class QuestDBProducer : BusinessBaseWithCacheIntervalVariableMode
 | 
			
		||||
    public override VariablePropertyBase VariablePropertys => _variablePropertys;
 | 
			
		||||
 | 
			
		||||
    protected override BusinessPropertyWithCacheInterval _businessPropertyWithCacheInterval => _driverPropertys;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    private SqlSugarClient _db;
 | 
			
		||||
 | 
			
		||||
    protected override void Dispose(bool disposing)
 | 
			
		||||
@@ -53,17 +58,6 @@ public partial class QuestDBProducer : BusinessBaseWithCacheIntervalVariableMode
 | 
			
		||||
        base.Dispose(disposing);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<SqlSugarPagedList<IDBHistoryValue>> GetDBHistoryValuePagesAsync(DBHistoryValuePageInput input)
 | 
			
		||||
    {
 | 
			
		||||
        var data = await Query(input).ToPagedListAsync<QuestDBNumberHistoryValue, IDBHistoryValue>(input.Current, input.Size).ConfigureAwait(false);//分页
 | 
			
		||||
        return data;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<List<IDBHistoryValue>> GetDBHistoryValuesAsync(DBHistoryValuePageInput input)
 | 
			
		||||
    {
 | 
			
		||||
        var data = await Query(input).ToListAsync().ConfigureAwait(false);
 | 
			
		||||
        return data.Cast<IDBHistoryValue>().ToList();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected override async Task InitChannelAsync(IChannel? channel, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
@@ -81,6 +75,40 @@ public partial class QuestDBProducer : BusinessBaseWithCacheIntervalVariableMode
 | 
			
		||||
        return $" {nameof(QuestDBProducer)}";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected override async Task ProtectedStartAsync(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
        _db.DbMaintenance.CreateDatabase();
 | 
			
		||||
 | 
			
		||||
        //必须为间隔上传
 | 
			
		||||
        if (!_driverPropertys.BigTextScriptHistoryTable.IsNullOrEmpty())
 | 
			
		||||
        {
 | 
			
		||||
            DynamicSQLBase? hisModel = CSharpScriptEngineExtension.Do<DynamicSQLBase>(_driverPropertys.BigTextScriptHistoryTable);
 | 
			
		||||
            hisModel.Logger = LogMessage;
 | 
			
		||||
            await hisModel.DBInit(_db, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            _db.CodeFirst.As<QuestDBNumberHistoryValue>(_driverPropertys.NumberTableName).InitTables(typeof(QuestDBNumberHistoryValue));
 | 
			
		||||
            _db.CodeFirst.As<QuestDBHistoryValue>(_driverPropertys.StringTableName).InitTables(typeof(QuestDBHistoryValue));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await base.ProtectedStartAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public async Task<SqlSugarPagedList<IDBHistoryValue>> GetDBHistoryValuePagesAsync(DBHistoryValuePageInput input)
 | 
			
		||||
    {
 | 
			
		||||
        var data = await Query(input).ToPagedListAsync<QuestDBNumberHistoryValue, IDBHistoryValue>(input.Current, input.Size).ConfigureAwait(false);//分页
 | 
			
		||||
        return data;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<List<IDBHistoryValue>> GetDBHistoryValuesAsync(DBHistoryValuePageInput input)
 | 
			
		||||
    {
 | 
			
		||||
        var data = await Query(input).ToListAsync().ConfigureAwait(false);
 | 
			
		||||
        return data.Cast<IDBHistoryValue>().ToList();
 | 
			
		||||
    }
 | 
			
		||||
    internal ISugarQueryable<QuestDBNumberHistoryValue> Query(DBHistoryValuePageInput input)
 | 
			
		||||
    {
 | 
			
		||||
        var db = BusinessDatabaseUtil.GetDb(_driverPropertys.DbType, _driverPropertys.BigTextConnectStr);
 | 
			
		||||
@@ -142,36 +170,4 @@ public partial class QuestDBProducer : BusinessBaseWithCacheIntervalVariableMode
 | 
			
		||||
        return ret;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected override async Task ProtectedStartAsync(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
        _db.DbMaintenance.CreateDatabase();
 | 
			
		||||
 | 
			
		||||
        //必须为间隔上传
 | 
			
		||||
        if (!_driverPropertys.BigTextScriptHistoryTable.IsNullOrEmpty())
 | 
			
		||||
        {
 | 
			
		||||
            DynamicSQLBase? hisModel = CSharpScriptEngineExtension.Do<DynamicSQLBase>(_driverPropertys.BigTextScriptHistoryTable);
 | 
			
		||||
            hisModel.Logger = LogMessage;
 | 
			
		||||
            await hisModel.DBInit(_db, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            _db.CodeFirst.As<QuestDBNumberHistoryValue>(_driverPropertys.NumberTableName).InitTables(typeof(QuestDBNumberHistoryValue));
 | 
			
		||||
            _db.CodeFirst.As<QuestDBHistoryValue>(_driverPropertys.StringTableName).InitTables(typeof(QuestDBHistoryValue));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await base.ProtectedStartAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    protected override async Task ProtectedExecuteAsync(object? state, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        await UpdateVarModelMemory(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        await UpdateVarModelsMemory(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
        await UpdateVarModelCache(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        await UpdateVarModelsCache(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,7 @@ namespace ThingsGateway.Plugin.QuestDB;
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// RabbitMQProducer
 | 
			
		||||
/// </summary>
 | 
			
		||||
public partial class QuestDBProducer : BusinessBaseWithCacheIntervalVariableModel<VariableBasicData>
 | 
			
		||||
public partial class QuestDBProducer : BusinessBaseWithCacheIntervalVariable
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    protected override ValueTask<OperResult> UpdateVarModel(IEnumerable<CacheDBItem<VariableBasicData>> item, CancellationToken cancellationToken)
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ using BootstrapBlazor.Components;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.Admin.Application;
 | 
			
		||||
using ThingsGateway.Debug;
 | 
			
		||||
using ThingsGateway.Extension.Generic;
 | 
			
		||||
using ThingsGateway.Foundation;
 | 
			
		||||
using ThingsGateway.NewLife;
 | 
			
		||||
using ThingsGateway.NewLife.DictionaryExtensions;
 | 
			
		||||
@@ -24,8 +25,12 @@ namespace ThingsGateway.Plugin.SqlDB;
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// SqlDBProducer
 | 
			
		||||
/// </summary>
 | 
			
		||||
public partial class SqlDBProducer : BusinessBaseWithCacheIntervalVariableModel<VariableBasicData>, IDBHistoryValueService
 | 
			
		||||
public partial class SqlDBProducer : BusinessBaseWithCacheIntervalVariable, IDBHistoryValueService
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    internal readonly SqlDBProducerProperty _driverPropertys = new();
 | 
			
		||||
    private readonly SqlDBProducerVariableProperty _variablePropertys = new();
 | 
			
		||||
 | 
			
		||||
@@ -73,6 +78,123 @@ public partial class SqlDBProducer : BusinessBaseWithCacheIntervalVariableModel<
 | 
			
		||||
        return $" {nameof(SqlDBProducer)}";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    protected override async Task InitChannelAsync(IChannel? channel, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        _db = SqlDBBusinessDatabaseUtil.GetDb(_driverPropertys);
 | 
			
		||||
 | 
			
		||||
        if (_businessPropertyWithCacheInterval.BusinessUpdateEnum == BusinessUpdateEnum.Interval && _driverPropertys.IsReadDB)
 | 
			
		||||
        {
 | 
			
		||||
            GlobalData.VariableValueChangeEvent += VariableValueChange;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await base.InitChannelAsync(channel, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override Task AfterVariablesChangedAsync(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        RealTimeVariables.Clear();
 | 
			
		||||
        _initRealData = false;
 | 
			
		||||
        return base.AfterVariablesChangedAsync(cancellationToken);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected override async Task ProtectedStartAsync(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        _db.DbMaintenance.CreateDatabase();
 | 
			
		||||
 | 
			
		||||
        //必须为间隔上传
 | 
			
		||||
        if (!_driverPropertys.BigTextScriptHistoryTable.IsNullOrEmpty())
 | 
			
		||||
        {
 | 
			
		||||
            var hisModel = CSharpScriptEngineExtension.Do<DynamicSQLBase>(_driverPropertys.BigTextScriptHistoryTable);
 | 
			
		||||
 | 
			
		||||
            if (_driverPropertys.IsHistoryDB)
 | 
			
		||||
            {
 | 
			
		||||
                await hisModel.DBInit(_db, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            if (_driverPropertys.IsHistoryDB)
 | 
			
		||||
            {
 | 
			
		||||
                _db.CodeFirst.InitTables(typeof(SQLHistoryValue));
 | 
			
		||||
                _db.CodeFirst.InitTables(typeof(SQLNumberHistoryValue));
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if (!_driverPropertys.BigTextScriptRealTable.IsNullOrEmpty())
 | 
			
		||||
        {
 | 
			
		||||
            var realModel = CSharpScriptEngineExtension.Do<DynamicSQLBase>(_driverPropertys.BigTextScriptRealTable);
 | 
			
		||||
 | 
			
		||||
            if (_driverPropertys.IsReadDB)
 | 
			
		||||
            {
 | 
			
		||||
                await realModel.DBInit(_db, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            if (_driverPropertys.IsReadDB)
 | 
			
		||||
                _db.CodeFirst.As<SQLRealValue>(_driverPropertys.ReadDBTableName).InitTables<SQLRealValue>();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await base.ProtectedStartAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected override async Task ProtectedExecuteAsync(object? state, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        if (_driverPropertys.IsReadDB)
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var varLists = RealTimeVariables.ToIEnumerableWithDequeue().Batch(100000);
 | 
			
		||||
                foreach (var varList in varLists)
 | 
			
		||||
                {
 | 
			
		||||
                    var result = await UpdateAsync(varList, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                    if (success != result.IsSuccess)
 | 
			
		||||
                    {
 | 
			
		||||
                        if (!result.IsSuccess)
 | 
			
		||||
                            LogMessage?.LogWarning(result.ToString());
 | 
			
		||||
                        success = result.IsSuccess;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                if (success)
 | 
			
		||||
                    LogMessage?.LogWarning(ex);
 | 
			
		||||
                success = false;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (_driverPropertys.IsHistoryDB)
 | 
			
		||||
        {
 | 
			
		||||
            await Update(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private ISugarQueryable<SQLNumberHistoryValue> Query(DBHistoryValuePageInput input)
 | 
			
		||||
    {
 | 
			
		||||
        var db = SqlDBBusinessDatabaseUtil.GetDb(_driverPropertys);
 | 
			
		||||
 | 
			
		||||
        var query = db.Queryable<SQLNumberHistoryValue>().SplitTable()
 | 
			
		||||
                           .WhereIF(input.StartTime != null, a => a.CreateTime >= input.StartTime)
 | 
			
		||||
                           .WhereIF(input.EndTime != null, a => a.CreateTime <= input.EndTime)
 | 
			
		||||
                           .WhereIF(!string.IsNullOrEmpty(input.VariableName), it => it.Name.Contains(input.VariableName))
 | 
			
		||||
                           .WhereIF(input.VariableNames != null, it => input.VariableNames.Contains(it.Name))
 | 
			
		||||
                           ;
 | 
			
		||||
 | 
			
		||||
        for (int i = input.SortField.Count - 1; i >= 0; i--)
 | 
			
		||||
        {
 | 
			
		||||
            query = query.OrderByIF(!string.IsNullOrEmpty(input.SortField[i]), $"{input.SortField[i]} {(input.SortDesc[i] ? "desc" : "asc")}");
 | 
			
		||||
        }
 | 
			
		||||
        query = query.OrderBy(it => it.Id, OrderByType.Desc);//排序
 | 
			
		||||
 | 
			
		||||
        return query;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    internal async Task<QueryData<SQLNumberHistoryValue>> QueryHistoryData(QueryPageOptions option)
 | 
			
		||||
    {
 | 
			
		||||
        var db = SqlDBBusinessDatabaseUtil.GetDb(_driverPropertys);
 | 
			
		||||
@@ -154,121 +276,4 @@ public partial class SqlDBProducer : BusinessBaseWithCacheIntervalVariableModel<
 | 
			
		||||
        return ret;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected override async Task InitChannelAsync(IChannel? channel, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        _db = SqlDBBusinessDatabaseUtil.GetDb(_driverPropertys);
 | 
			
		||||
 | 
			
		||||
        if (_businessPropertyWithCacheInterval.BusinessUpdateEnum == BusinessUpdateEnum.Interval && _driverPropertys.IsReadDB)
 | 
			
		||||
        {
 | 
			
		||||
            GlobalData.VariableValueChangeEvent += VariableValueChange;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await base.InitChannelAsync(channel, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override Task AfterVariablesChangedAsync(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        RealTimeVariables.Clear();
 | 
			
		||||
        _initRealData = false;
 | 
			
		||||
        return base.AfterVariablesChangedAsync(cancellationToken);
 | 
			
		||||
    }
 | 
			
		||||
    protected override async Task ProtectedStartAsync(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        _db.DbMaintenance.CreateDatabase();
 | 
			
		||||
 | 
			
		||||
        //必须为间隔上传
 | 
			
		||||
        if (!_driverPropertys.BigTextScriptHistoryTable.IsNullOrEmpty())
 | 
			
		||||
        {
 | 
			
		||||
            var hisModel = CSharpScriptEngineExtension.Do<DynamicSQLBase>(_driverPropertys.BigTextScriptHistoryTable);
 | 
			
		||||
 | 
			
		||||
            if (_driverPropertys.IsHistoryDB)
 | 
			
		||||
            {
 | 
			
		||||
                await hisModel.DBInit(_db, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            if (_driverPropertys.IsHistoryDB)
 | 
			
		||||
            {
 | 
			
		||||
                _db.CodeFirst.InitTables(typeof(SQLHistoryValue));
 | 
			
		||||
                _db.CodeFirst.InitTables(typeof(SQLNumberHistoryValue));
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if (!_driverPropertys.BigTextScriptRealTable.IsNullOrEmpty())
 | 
			
		||||
        {
 | 
			
		||||
            var realModel = CSharpScriptEngineExtension.Do<DynamicSQLBase>(_driverPropertys.BigTextScriptRealTable);
 | 
			
		||||
 | 
			
		||||
            if (_driverPropertys.IsReadDB)
 | 
			
		||||
            {
 | 
			
		||||
                await realModel.DBInit(_db, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            if (_driverPropertys.IsReadDB)
 | 
			
		||||
                _db.CodeFirst.As<SQLRealValue>(_driverPropertys.ReadDBTableName).InitTables<SQLRealValue>();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await base.ProtectedStartAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected override async Task ProtectedExecuteAsync(object? state, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        if (_driverPropertys.IsReadDB)
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var varList = RealTimeVariables.ToListWithDequeue();
 | 
			
		||||
                if (varList.Count > 0)
 | 
			
		||||
                {
 | 
			
		||||
                    var result = await UpdateAsync(varList, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                    if (success != result.IsSuccess)
 | 
			
		||||
                    {
 | 
			
		||||
                        if (!result.IsSuccess)
 | 
			
		||||
                            LogMessage?.LogWarning(result.ToString());
 | 
			
		||||
                        success = result.IsSuccess;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                if (success)
 | 
			
		||||
                    LogMessage?.LogWarning(ex);
 | 
			
		||||
                success = false;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (_driverPropertys.IsHistoryDB)
 | 
			
		||||
        {
 | 
			
		||||
            await UpdateVarModelMemory(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
            await UpdateVarModelsMemory(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
            await UpdateVarModelCache(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
            await UpdateVarModelsCache(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private ISugarQueryable<SQLNumberHistoryValue> Query(DBHistoryValuePageInput input)
 | 
			
		||||
    {
 | 
			
		||||
        var db = SqlDBBusinessDatabaseUtil.GetDb(_driverPropertys);
 | 
			
		||||
 | 
			
		||||
        var query = db.Queryable<SQLNumberHistoryValue>().SplitTable()
 | 
			
		||||
                           .WhereIF(input.StartTime != null, a => a.CreateTime >= input.StartTime)
 | 
			
		||||
                           .WhereIF(input.EndTime != null, a => a.CreateTime <= input.EndTime)
 | 
			
		||||
                           .WhereIF(!string.IsNullOrEmpty(input.VariableName), it => it.Name.Contains(input.VariableName))
 | 
			
		||||
                           .WhereIF(input.VariableNames != null, it => input.VariableNames.Contains(it.Name))
 | 
			
		||||
                           ;
 | 
			
		||||
 | 
			
		||||
        for (int i = input.SortField.Count - 1; i >= 0; i--)
 | 
			
		||||
        {
 | 
			
		||||
            query = query.OrderByIF(!string.IsNullOrEmpty(input.SortField[i]), $"{input.SortField[i]} {(input.SortDesc[i] ? "desc" : "asc")}");
 | 
			
		||||
        }
 | 
			
		||||
        query = query.OrderBy(it => it.Id, OrderByType.Desc);//排序
 | 
			
		||||
 | 
			
		||||
        return query;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -22,7 +22,7 @@ namespace ThingsGateway.Plugin.SqlDB;
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// SqlDBProducer
 | 
			
		||||
/// </summary>
 | 
			
		||||
public partial class SqlDBProducer : BusinessBaseWithCacheIntervalVariableModel<VariableBasicData>
 | 
			
		||||
public partial class SqlDBProducer : BusinessBaseWithCacheIntervalVariable
 | 
			
		||||
{
 | 
			
		||||
    private volatile bool _initRealData;
 | 
			
		||||
    private ConcurrentDictionary<long, VariableBasicData> RealTimeVariables { get; } = new ConcurrentDictionary<long, VariableBasicData>();
 | 
			
		||||
@@ -77,7 +77,7 @@ public partial class SqlDBProducer : BusinessBaseWithCacheIntervalVariableModel<
 | 
			
		||||
 | 
			
		||||
    private void UpdateVariable(VariableRuntime variableRuntime, VariableBasicData variable)
 | 
			
		||||
    {
 | 
			
		||||
        if (_driverPropertys.IsHistoryDB)
 | 
			
		||||
        if (_driverPropertys.IsHistoryDB && _businessPropertyWithCacheInterval.BusinessUpdateEnum != BusinessUpdateEnum.Interval)
 | 
			
		||||
        {
 | 
			
		||||
            if (_driverPropertys.GroupUpdate && variable.BusinessGroupUpdateTrigger && !variable.BusinessGroup.IsNullOrEmpty() && VariableRuntimeGroups.TryGetValue(variable.BusinessGroup, out var variableRuntimeGroup))
 | 
			
		||||
            {
 | 
			
		||||
@@ -200,7 +200,7 @@ public partial class SqlDBProducer : BusinessBaseWithCacheIntervalVariableModel<
 | 
			
		||||
                    stopwatch.Start();
 | 
			
		||||
                    var ids = (await _db.Queryable<SQLRealValue>().AS(_driverPropertys.ReadDBTableName).Select(a => a.Id).ToListAsync(cancellationToken).ConfigureAwait(false)).ToHashSet();
 | 
			
		||||
                    var InsertData = IdVariableRuntimes.Where(a => !ids.Contains(a.Key)).Select(a => a.Value).AdaptListSQLRealValue();
 | 
			
		||||
                    var result = await _db.Fastest<SQLRealValue>().AS(_driverPropertys.ReadDBTableName).PageSize(100000).BulkCopyAsync(InsertData).ConfigureAwait(false);
 | 
			
		||||
                    var result = await _db.Fastest<SQLRealValue>().AS(_driverPropertys.ReadDBTableName).BulkCopyAsync(InsertData).ConfigureAwait(false);
 | 
			
		||||
                    _initRealData = true;
 | 
			
		||||
                    stopwatch.Stop();
 | 
			
		||||
                    if (result > 0)
 | 
			
		||||
 
 | 
			
		||||
@@ -11,20 +11,18 @@
 | 
			
		||||
using BootstrapBlazor.Components;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.Admin.Application;
 | 
			
		||||
using ThingsGateway.Extension.Generic;
 | 
			
		||||
using ThingsGateway.Foundation;
 | 
			
		||||
using ThingsGateway.NewLife;
 | 
			
		||||
using ThingsGateway.SqlSugar;
 | 
			
		||||
 | 
			
		||||
using TouchSocket.Core;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Plugin.SqlHistoryAlarm;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// SqlHistoryAlarm
 | 
			
		||||
/// </summary>
 | 
			
		||||
public partial class SqlHistoryAlarm : BusinessBaseWithCacheVariableModel<HistoryAlarm>, IDBHistoryAlarmService
 | 
			
		||||
public partial class SqlHistoryAlarm : BusinessBaseWithCacheAlarm, IDBHistoryAlarmService
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    internal readonly SqlHistoryAlarmProperty _driverPropertys = new();
 | 
			
		||||
    private readonly SqlHistoryAlarmVariableProperty _variablePropertys = new();
 | 
			
		||||
    public override Type DriverUIType => typeof(HistoryAlarmPage);
 | 
			
		||||
@@ -34,66 +32,14 @@ public partial class SqlHistoryAlarm : BusinessBaseWithCacheVariableModel<Histor
 | 
			
		||||
 | 
			
		||||
    protected override BusinessPropertyWithCache _businessPropertyWithCache => _driverPropertys;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    private SqlSugarClient _db;
 | 
			
		||||
 | 
			
		||||
    public static HistoryAlarm AdaptHistoryAlarm(AlarmVariable src)
 | 
			
		||||
    {
 | 
			
		||||
        var target = new HistoryAlarm();
 | 
			
		||||
        target.Name = src.Name;
 | 
			
		||||
        target.Description = src.Description;
 | 
			
		||||
        target.CreateOrgId = src.CreateOrgId;
 | 
			
		||||
        target.CreateUserId = src.CreateUserId;
 | 
			
		||||
        target.DeviceId = src.DeviceId;
 | 
			
		||||
        target.DeviceName = src.DeviceName;
 | 
			
		||||
        target.RegisterAddress = src.RegisterAddress;
 | 
			
		||||
        target.DataType = src.DataType;
 | 
			
		||||
        target.AlarmCode = src.AlarmCode;
 | 
			
		||||
        target.AlarmLimit = src.AlarmLimit;
 | 
			
		||||
        target.AlarmText = src.AlarmText;
 | 
			
		||||
        target.RecoveryCode = src.RecoveryCode;
 | 
			
		||||
        target.AlarmTime = src.AlarmTime;
 | 
			
		||||
        target.EventTime = src.EventTime;
 | 
			
		||||
        target.AlarmType = src.AlarmType;
 | 
			
		||||
        target.EventType = src.EventType;
 | 
			
		||||
        target.Remark1 = src.Remark1;
 | 
			
		||||
        target.Remark2 = src.Remark2;
 | 
			
		||||
        target.Remark3 = src.Remark3;
 | 
			
		||||
        target.Remark4 = src.Remark4;
 | 
			
		||||
        target.Remark5 = src.Remark5;
 | 
			
		||||
        target.Id = CommonUtils.GetSingleId();
 | 
			
		||||
        return target;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    protected override async Task InitChannelAsync(IChannel? channel, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        _db = BusinessDatabaseUtil.GetDb((DbType)_driverPropertys.DbType, _driverPropertys.BigTextConnectStr);
 | 
			
		||||
 | 
			
		||||
        GlobalData.AlarmChangedEvent -= AlarmWorker_OnAlarmChanged;
 | 
			
		||||
        GlobalData.ReadOnlyRealAlarmIdVariables?.ForEach(a =>
 | 
			
		||||
        {
 | 
			
		||||
            AlarmWorker_OnAlarmChanged(a.Value);
 | 
			
		||||
        });
 | 
			
		||||
        GlobalData.AlarmChangedEvent += AlarmWorker_OnAlarmChanged;
 | 
			
		||||
 | 
			
		||||
        await base.InitChannelAsync(channel, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
    }
 | 
			
		||||
    public override async Task AfterVariablesChangedAsync(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        await base.AfterVariablesChangedAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        IdVariableRuntimes.Clear();
 | 
			
		||||
        IdVariableRuntimes.AddRange(GlobalData.ReadOnlyIdVariables.Where(a => a.Value.AlarmEnable));
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        var ids = IdVariableRuntimes.Select(b => b.Value.DeviceId).ToHashSet();
 | 
			
		||||
 | 
			
		||||
        CollectDevices = GlobalData.ReadOnlyIdDevices
 | 
			
		||||
                                .Where(a => IdVariableRuntimes.Select(b => b.Value.DeviceId).Contains(a.Value.Id))
 | 
			
		||||
                                .ToDictionary(a => a.Key, a => a.Value);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
@@ -104,7 +50,6 @@ public partial class SqlHistoryAlarm : BusinessBaseWithCacheVariableModel<Histor
 | 
			
		||||
    protected override void Dispose(bool disposing)
 | 
			
		||||
    {
 | 
			
		||||
        _db?.TryDispose();
 | 
			
		||||
        GlobalData.AlarmChangedEvent -= AlarmWorker_OnAlarmChanged;
 | 
			
		||||
        base.Dispose(disposing);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -115,11 +60,6 @@ public partial class SqlHistoryAlarm : BusinessBaseWithCacheVariableModel<Histor
 | 
			
		||||
        return base.ProtectedStartAsync(cancellationToken);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected override Task ProtectedExecuteAsync(object? state, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        return Update(cancellationToken);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #region 数据查询
 | 
			
		||||
 | 
			
		||||
    public async Task<SqlSugarPagedList<IDBHistoryAlarm>> GetDBHistoryAlarmPagesAsync(DBHistoryAlarmPageInput input)
 | 
			
		||||
 
 | 
			
		||||
@@ -20,41 +20,19 @@ namespace ThingsGateway.Plugin.SqlHistoryAlarm;
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// SqlHistoryAlarm
 | 
			
		||||
/// </summary>
 | 
			
		||||
public partial class SqlHistoryAlarm : BusinessBaseWithCacheVariableModel<HistoryAlarm>
 | 
			
		||||
public partial class SqlHistoryAlarm : BusinessBaseWithCacheAlarm
 | 
			
		||||
{
 | 
			
		||||
    protected override ValueTask<OperResult> UpdateVarModel(IEnumerable<CacheDBItem<HistoryAlarm>> item, CancellationToken cancellationToken)
 | 
			
		||||
 | 
			
		||||
    protected override void AlarmChange(AlarmVariable alarmVariable)
 | 
			
		||||
    {
 | 
			
		||||
        return UpdateT(item.Select(a => a.Value).OrderBy(a => a.Id), cancellationToken);
 | 
			
		||||
        AddQueueAlarmModel(new CacheDBItem<AlarmVariable>(alarmVariable));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected override ValueTask<OperResult> UpdateVarModels(IEnumerable<HistoryAlarm> item, CancellationToken cancellationToken)
 | 
			
		||||
    protected override ValueTask<OperResult> UpdateAlarmModel(IEnumerable<CacheDBItem<AlarmVariable>> item, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        return UpdateT(item, cancellationToken);
 | 
			
		||||
        return UpdateAlarmModel(item.Select(a => a.Value).OrderBy(a => a.Id), cancellationToken);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void AlarmWorker_OnAlarmChanged(AlarmVariable alarmVariable)
 | 
			
		||||
    {
 | 
			
		||||
        if (CurrentDevice.Pause)
 | 
			
		||||
            return;
 | 
			
		||||
        AddQueueVarModel(new CacheDBItem<HistoryAlarm>(AdaptHistoryAlarm(alarmVariable)));
 | 
			
		||||
    }
 | 
			
		||||
    public override void PauseThread(bool pause)
 | 
			
		||||
    {
 | 
			
		||||
        lock (this)
 | 
			
		||||
        {
 | 
			
		||||
            var oldV = CurrentDevice.Pause;
 | 
			
		||||
            base.PauseThread(pause);
 | 
			
		||||
            if (!pause && oldV != pause)
 | 
			
		||||
            {
 | 
			
		||||
                GlobalData.ReadOnlyRealAlarmIdVariables?.ForEach(a =>
 | 
			
		||||
                {
 | 
			
		||||
                    AlarmWorker_OnAlarmChanged(a.Value);
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async ValueTask<OperResult> UpdateT(IEnumerable<HistoryAlarm> item, CancellationToken cancellationToken)
 | 
			
		||||
    private async ValueTask<OperResult> UpdateAlarmModel(IEnumerable<AlarmVariable> item, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        var result = await InserableAsync(item.ToList(), cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        if (success != result.IsSuccess)
 | 
			
		||||
@@ -67,7 +45,7 @@ public partial class SqlHistoryAlarm : BusinessBaseWithCacheVariableModel<Histor
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async ValueTask<OperResult> InserableAsync(List<HistoryAlarm> dbInserts, CancellationToken cancellationToken)
 | 
			
		||||
    private async ValueTask<OperResult> InserableAsync(List<AlarmVariable> dbInserts, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
@@ -91,7 +69,7 @@ public partial class SqlHistoryAlarm : BusinessBaseWithCacheVariableModel<Histor
 | 
			
		||||
                if (_db.CurrentConnectionConfig.DbType == SqlSugar.DbType.QuestDB)
 | 
			
		||||
                    result = await _db.Insertable(dbInserts).AS(_driverPropertys.TableName).ExecuteCommandAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                else
 | 
			
		||||
                    result = await _db.Fastest<HistoryAlarm>().AS(_driverPropertys.TableName).PageSize(50000).BulkCopyAsync(dbInserts).ConfigureAwait(false);
 | 
			
		||||
                    result = await _db.Fastest<HistoryAlarm>().AS(_driverPropertys.TableName).PageSize(50000).BulkCopyAsync(dbInserts.AdaptListHistoryAlarm()).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                stopwatch.Stop();
 | 
			
		||||
                //var result = await db.Insertable(dbInserts).SplitTable().ExecuteCommandAsync().ConfigureAwait(false);
 | 
			
		||||
 
 | 
			
		||||
@@ -25,8 +25,11 @@ namespace ThingsGateway.Plugin.TDengineDB;
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// TDengineDBProducer
 | 
			
		||||
/// </summary>
 | 
			
		||||
public partial class TDengineDBProducer : BusinessBaseWithCacheIntervalVariableModel<VariableBasicData>, IDBHistoryValueService
 | 
			
		||||
public partial class TDengineDBProducer : BusinessBaseWithCacheIntervalVariable, IDBHistoryValueService
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    internal readonly RealDBProducerProperty _driverPropertys = new()
 | 
			
		||||
    {
 | 
			
		||||
        DbType = DbType.TDengine,
 | 
			
		||||
@@ -51,17 +54,7 @@ public partial class TDengineDBProducer : BusinessBaseWithCacheIntervalVariableM
 | 
			
		||||
 | 
			
		||||
    protected override BusinessPropertyWithCacheInterval _businessPropertyWithCacheInterval => _driverPropertys;
 | 
			
		||||
 | 
			
		||||
    public async Task<SqlSugarPagedList<IDBHistoryValue>> GetDBHistoryValuePagesAsync(DBHistoryValuePageInput input)
 | 
			
		||||
    {
 | 
			
		||||
        var data = await Query(input, _driverPropertys.NumberTableNameLow).ToPagedListAsync<TDengineDBNumberHistoryValue, IDBHistoryValue>(input.Current, input.Size).ConfigureAwait(false);//分页
 | 
			
		||||
        return data;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<List<IDBHistoryValue>> GetDBHistoryValuesAsync(DBHistoryValuePageInput input)
 | 
			
		||||
    {
 | 
			
		||||
        var data = await Query(input, _driverPropertys.NumberTableNameLow).ToListAsync().ConfigureAwait(false);
 | 
			
		||||
        return data.Cast<IDBHistoryValue>().ToList(); ;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected override void Dispose(bool disposing)
 | 
			
		||||
    {
 | 
			
		||||
@@ -97,6 +90,60 @@ public partial class TDengineDBProducer : BusinessBaseWithCacheIntervalVariableM
 | 
			
		||||
        return $" {nameof(TDengineDBProducer)}";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private SqlSugarClient _db;
 | 
			
		||||
 | 
			
		||||
    protected override async Task ProtectedStartAsync(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        _db.DbMaintenance.CreateDatabase();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        //必须为间隔上传
 | 
			
		||||
        if (!_driverPropertys.BigTextScriptHistoryTable.IsNullOrEmpty())
 | 
			
		||||
        {
 | 
			
		||||
            var hisModel = CSharpScriptEngineExtension.Do<DynamicSQLBase>(_driverPropertys.BigTextScriptHistoryTable);
 | 
			
		||||
            {
 | 
			
		||||
                await hisModel.DBInit(_db, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
            var sql = $"""
 | 
			
		||||
                CREATE STABLE IF NOT EXISTS  `{_driverPropertys.StringTableNameLow}`(
 | 
			
		||||
                `createtime` TIMESTAMP   ,
 | 
			
		||||
                `collecttime` TIMESTAMP   ,
 | 
			
		||||
                `id` BIGINT   ,
 | 
			
		||||
                `isonline` BOOL   ,
 | 
			
		||||
                `value` VARCHAR(255)    ) TAGS(`devicename`  VARCHAR(100) ,`name`  VARCHAR(100))
 | 
			
		||||
                """;
 | 
			
		||||
            await _db.Ado.ExecuteCommandAsync(sql, default, cancellationToken: cancellationToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            sql = $"""
 | 
			
		||||
                CREATE STABLE IF NOT EXISTS  `{_driverPropertys.NumberTableNameLow}`(
 | 
			
		||||
                `createtime` TIMESTAMP   ,
 | 
			
		||||
                `collecttime` TIMESTAMP   ,
 | 
			
		||||
                `id` BIGINT   ,
 | 
			
		||||
                `isonline` BOOL   ,
 | 
			
		||||
                `value` DOUBLE    ) TAGS(`devicename`  VARCHAR(100) ,`name`  VARCHAR(100))
 | 
			
		||||
                """;
 | 
			
		||||
            await _db.Ado.ExecuteCommandAsync(sql, default, cancellationToken: cancellationToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
        await base.ProtectedStartAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
    }
 | 
			
		||||
    public async Task<SqlSugarPagedList<IDBHistoryValue>> GetDBHistoryValuePagesAsync(DBHistoryValuePageInput input)
 | 
			
		||||
    {
 | 
			
		||||
        var data = await Query(input, _driverPropertys.NumberTableNameLow).ToPagedListAsync<TDengineDBNumberHistoryValue, IDBHistoryValue>(input.Current, input.Size).ConfigureAwait(false);//分页
 | 
			
		||||
        return data;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<List<IDBHistoryValue>> GetDBHistoryValuesAsync(DBHistoryValuePageInput input)
 | 
			
		||||
    {
 | 
			
		||||
        var data = await Query(input, _driverPropertys.NumberTableNameLow).ToListAsync().ConfigureAwait(false);
 | 
			
		||||
        return data.Cast<IDBHistoryValue>().ToList(); ;
 | 
			
		||||
    }
 | 
			
		||||
    internal ISugarQueryable<TDengineDBNumberHistoryValue> Query(DBHistoryValuePageInput input, string tableName)
 | 
			
		||||
    {
 | 
			
		||||
        var db = TDengineDBUtil.GetDb(_driverPropertys.DbType, _driverPropertys.BigTextConnectStr, tableName);
 | 
			
		||||
@@ -156,56 +203,4 @@ public partial class TDengineDBProducer : BusinessBaseWithCacheIntervalVariableM
 | 
			
		||||
        }
 | 
			
		||||
        return ret;
 | 
			
		||||
    }
 | 
			
		||||
    private SqlSugarClient _db;
 | 
			
		||||
 | 
			
		||||
    protected override async Task ProtectedStartAsync(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        _db.DbMaintenance.CreateDatabase();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        //必须为间隔上传
 | 
			
		||||
        if (!_driverPropertys.BigTextScriptHistoryTable.IsNullOrEmpty())
 | 
			
		||||
        {
 | 
			
		||||
            var hisModel = CSharpScriptEngineExtension.Do<DynamicSQLBase>(_driverPropertys.BigTextScriptHistoryTable);
 | 
			
		||||
            {
 | 
			
		||||
                await hisModel.DBInit(_db, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
            var sql = $"""
 | 
			
		||||
                CREATE STABLE IF NOT EXISTS  `{_driverPropertys.StringTableNameLow}`(
 | 
			
		||||
                `createtime` TIMESTAMP   ,
 | 
			
		||||
                `collecttime` TIMESTAMP   ,
 | 
			
		||||
                `id` BIGINT   ,
 | 
			
		||||
                `isonline` BOOL   ,
 | 
			
		||||
                `value` VARCHAR(255)    ) TAGS(`devicename`  VARCHAR(100) ,`name`  VARCHAR(100))
 | 
			
		||||
                """;
 | 
			
		||||
            await _db.Ado.ExecuteCommandAsync(sql, default, cancellationToken: cancellationToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            sql = $"""
 | 
			
		||||
                CREATE STABLE IF NOT EXISTS  `{_driverPropertys.NumberTableNameLow}`(
 | 
			
		||||
                `createtime` TIMESTAMP   ,
 | 
			
		||||
                `collecttime` TIMESTAMP   ,
 | 
			
		||||
                `id` BIGINT   ,
 | 
			
		||||
                `isonline` BOOL   ,
 | 
			
		||||
                `value` DOUBLE    ) TAGS(`devicename`  VARCHAR(100) ,`name`  VARCHAR(100))
 | 
			
		||||
                """;
 | 
			
		||||
            await _db.Ado.ExecuteCommandAsync(sql, default, cancellationToken: cancellationToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
        await base.ProtectedStartAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected override async Task ProtectedExecuteAsync(object? state, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        await UpdateVarModelMemory(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        await UpdateVarModelsMemory(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        await UpdateVarModelCache(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        await UpdateVarModelsCache(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -26,7 +26,7 @@ namespace ThingsGateway.Plugin.TDengineDB;
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// RabbitMQProducer
 | 
			
		||||
/// </summary>
 | 
			
		||||
public partial class TDengineDBProducer : BusinessBaseWithCacheIntervalVariableModel<VariableBasicData>
 | 
			
		||||
public partial class TDengineDBProducer : BusinessBaseWithCacheIntervalVariable
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    protected override ValueTask<OperResult> UpdateVarModel(IEnumerable<CacheDBItem<VariableBasicData>> item, CancellationToken cancellationToken)
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,8 @@
 | 
			
		||||
		<ProjectReference Include="..\..\Gateway\ThingsGateway.Gateway.Razor\ThingsGateway.Gateway.Razor.csproj">
 | 
			
		||||
		</ProjectReference>
 | 
			
		||||
		
 | 
			
		||||
		<PackageReference Include="Riok.Mapperly" Version="4.2.1" ExcludeAssets="runtime" PrivateAssets="all" />
 | 
			
		||||
 | 
			
		||||
		<!--<PackageReference Include="SqlSugar.QuestDb.RestAPI" Version="4.1.17" GeneratePathProperty="true">
 | 
			
		||||
			<PrivateAssets>contentFiles;compile;build;buildMultitargeting;buildTransitive;analyzers;</PrivateAssets>
 | 
			
		||||
		</PackageReference>
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,7 @@ namespace ThingsGateway.Plugin.Webhook;
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// WebhookClient,RPC方法适配mqttNet
 | 
			
		||||
/// </summary>
 | 
			
		||||
public partial class Webhook : BusinessBaseWithCacheIntervalScript<VariableBasicData, DeviceBasicData, AlarmVariable>
 | 
			
		||||
public partial class Webhook : BusinessBaseWithCacheIntervalScriptAll
 | 
			
		||||
{
 | 
			
		||||
    private readonly WebhookProperty _driverPropertys = new();
 | 
			
		||||
    private readonly WebhookVariableProperty _variablePropertys = new();
 | 
			
		||||
@@ -23,10 +23,4 @@ public partial class Webhook : BusinessBaseWithCacheIntervalScript<VariableBasic
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public override bool IsConnected() => success;
 | 
			
		||||
 | 
			
		||||
    protected override Task ProtectedExecuteAsync(object? state, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        return Update(cancellationToken);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,7 @@ namespace ThingsGateway.Plugin.Webhook;
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// WebhookClient
 | 
			
		||||
/// </summary>
 | 
			
		||||
public partial class Webhook : BusinessBaseWithCacheIntervalScript<VariableBasicData, DeviceBasicData, AlarmVariable>
 | 
			
		||||
public partial class Webhook : BusinessBaseWithCacheIntervalScriptAll
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    protected override void AlarmChange(AlarmVariable alarmVariable)
 | 
			
		||||
@@ -116,6 +116,7 @@ public partial class Webhook : BusinessBaseWithCacheIntervalScript<VariableBasic
 | 
			
		||||
    {
 | 
			
		||||
        if (!_businessPropertyWithCacheIntervalScript.VariableTopic.IsNullOrWhiteSpace())
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
            if (_driverPropertys.GroupUpdate && variable.BusinessGroupUpdateTrigger && !variable.BusinessGroup.IsNullOrEmpty() && VariableRuntimeGroups.TryGetValue(variable.BusinessGroup, out var variableRuntimeGroup))
 | 
			
		||||
            {
 | 
			
		||||
 | 
			
		||||
@@ -177,7 +178,7 @@ public partial class Webhook : BusinessBaseWithCacheIntervalScript<VariableBasic
 | 
			
		||||
 | 
			
		||||
    #region private
 | 
			
		||||
 | 
			
		||||
    private async ValueTask<OperResult> Update(List<TopicArray> topicArrayList, CancellationToken cancellationToken)
 | 
			
		||||
    private async ValueTask<OperResult> Update(IEnumerable<TopicArray> topicArrayList, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        foreach (var topicArray in topicArrayList)
 | 
			
		||||
        {
 | 
			
		||||
 
 | 
			
		||||
@@ -19,7 +19,7 @@ namespace ThingsGateway.Plugin.Kafka;
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Kafka消息生产
 | 
			
		||||
/// </summary>
 | 
			
		||||
public partial class KafkaProducer : BusinessBaseWithCacheIntervalScript<VariableBasicData, DeviceBasicData, AlarmVariable>
 | 
			
		||||
public partial class KafkaProducer : BusinessBaseWithCacheIntervalScriptAll
 | 
			
		||||
{
 | 
			
		||||
    private readonly KafkaProducerProperty _driverPropertys = new();
 | 
			
		||||
    private readonly KafkaProducerVariableProperty _variablePropertys = new();
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,7 @@ namespace ThingsGateway.Plugin.Kafka;
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Kafka消息生产
 | 
			
		||||
/// </summary>
 | 
			
		||||
public partial class KafkaProducer : BusinessBaseWithCacheIntervalScript<VariableBasicData, DeviceBasicData, AlarmVariable>
 | 
			
		||||
public partial class KafkaProducer : BusinessBaseWithCacheIntervalScriptAll
 | 
			
		||||
{
 | 
			
		||||
    private IProducer<Null, byte[]> _producer;
 | 
			
		||||
    private ProducerBuilder<Null, byte[]> _producerBuilder;
 | 
			
		||||
@@ -89,7 +89,7 @@ public partial class KafkaProducer : BusinessBaseWithCacheIntervalScript<Variabl
 | 
			
		||||
 | 
			
		||||
                foreach (var group in varGroup)
 | 
			
		||||
                {
 | 
			
		||||
                    AddQueueVarModel(new CacheDBItem<List<VariableBasicData>>(group.AdaptIEnumerableVariableBasicData()));
 | 
			
		||||
                    AddQueueVarModel(new CacheDBItem<List<VariableBasicData>>(group.ToList()));
 | 
			
		||||
                }
 | 
			
		||||
                foreach (var variable in varList)
 | 
			
		||||
                {
 | 
			
		||||
@@ -126,7 +126,7 @@ public partial class KafkaProducer : BusinessBaseWithCacheIntervalScript<Variabl
 | 
			
		||||
 | 
			
		||||
    #region private
 | 
			
		||||
 | 
			
		||||
    private async ValueTask<OperResult> Update(List<TopicArray> topicArrayList, CancellationToken cancellationToken)
 | 
			
		||||
    private async ValueTask<OperResult> Update(IEnumerable<TopicArray> topicArrayList, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        foreach (var topicArray in topicArrayList)
 | 
			
		||||
        {
 | 
			
		||||
@@ -174,7 +174,7 @@ public partial class KafkaProducer : BusinessBaseWithCacheIntervalScript<Variabl
 | 
			
		||||
        //保留消息
 | 
			
		||||
        //分解List,避免超出字节大小限制
 | 
			
		||||
        var varData = IdVariableRuntimes.Select(a => a.Value).AdaptIEnumerableVariableBasicData().ChunkBetter(_driverPropertys.SplitSize);
 | 
			
		||||
        var devData = CollectDevices?.Select(a => a.Value).AdaptListDeviceBasicData().ChunkBetter(_driverPropertys.SplitSize);
 | 
			
		||||
        var devData = CollectDevices?.Select(a => a.Value).AdaptIEnumerableDeviceBasicData().ChunkBetter(_driverPropertys.SplitSize);
 | 
			
		||||
        var alramData = GlobalData.ReadOnlyRealAlarmIdVariables.Select(a => a.Value).ChunkBetter(_driverPropertys.SplitSize);
 | 
			
		||||
        foreach (var item in varData)
 | 
			
		||||
        {
 | 
			
		||||
 
 | 
			
		||||
@@ -30,7 +30,7 @@ public class ModbusSlave : BusinessBase
 | 
			
		||||
{
 | 
			
		||||
    private readonly ModbusSlaveProperty _driverPropertys = new();
 | 
			
		||||
 | 
			
		||||
    private readonly ConcurrentDictionary<string, VariableRuntime> ModbusVariableQueue = new();
 | 
			
		||||
    private readonly ConcurrentDictionary<string, long> ModbusVariableQueue = new();
 | 
			
		||||
 | 
			
		||||
    private readonly ModbusSlaveVariableProperty _variablePropertys = new();
 | 
			
		||||
 | 
			
		||||
@@ -172,19 +172,22 @@ public class ModbusSlave : BusinessBase
 | 
			
		||||
                await Task.Delay(10000, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        var list = ModbusVariableQueue.ToDictWithDequeue();
 | 
			
		||||
        var list = ModbusVariableQueue.ToIEnumerableKVWithDequeue();
 | 
			
		||||
        foreach (var item in list)
 | 
			
		||||
        {
 | 
			
		||||
            if (cancellationToken.IsCancellationRequested)
 | 
			
		||||
                break;
 | 
			
		||||
            var type = item.Value.GetPropertyValue(CurrentDevice.Id, nameof(ModbusSlaveVariableProperty.DataType));
 | 
			
		||||
            if (!IdVariableRuntimes.TryGetValue(item.Value, out var variableRuntime))
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            var type = variableRuntime.GetPropertyValue(CurrentDevice.Id, nameof(ModbusSlaveVariableProperty.DataType));
 | 
			
		||||
            if (Enum.TryParse(type, out DataTypeEnum result))
 | 
			
		||||
            {
 | 
			
		||||
                await _plc.WriteAsync(item.Key, JToken.FromObject(item.Value.Value), result, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                await _plc.WriteAsync(item.Key, JToken.FromObject(variableRuntime.Value), result, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                await _plc.WriteAsync(item.Key, JToken.FromObject(item.Value.Value), item.Value.DataType, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                await _plc.WriteAsync(item.Key, JToken.FromObject(variableRuntime.Value), variableRuntime.DataType, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -246,12 +249,13 @@ public class ModbusSlave : BusinessBase
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    private void VariableValueChange(VariableRuntime variableRuntime, VariableBasicData variableData)
 | 
			
		||||
    {
 | 
			
		||||
        if (CurrentDevice.Pause == true)
 | 
			
		||||
        if (CurrentDevice?.Pause != false)
 | 
			
		||||
            return;
 | 
			
		||||
        if (TaskSchedulerLoop?.Stoped == true) return;
 | 
			
		||||
        var address = variableRuntime.GetPropertyValue(DeviceId, nameof(_variablePropertys.ServiceAddress));
 | 
			
		||||
        if (address != null && variableRuntime.Value != null)
 | 
			
		||||
        {
 | 
			
		||||
            ModbusVariableQueue?.AddOrUpdate(address, address => variableRuntime, (address, addvalue) => variableRuntime);
 | 
			
		||||
            ModbusVariableQueue?.AddOrUpdate(address, address => variableRuntime.Id, (address, addvalue) => variableRuntime.Id);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -26,7 +26,7 @@ namespace ThingsGateway.Plugin.Mqtt;
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// MqttClient,RPC方法适配mqttNet
 | 
			
		||||
/// </summary>
 | 
			
		||||
public partial class MqttClient : BusinessBaseWithCacheIntervalScript<VariableBasicData, DeviceBasicData, AlarmVariable>
 | 
			
		||||
public partial class MqttClient : BusinessBaseWithCacheIntervalScriptAll
 | 
			
		||||
{
 | 
			
		||||
    private readonly MqttClientProperty _driverPropertys = new();
 | 
			
		||||
    private readonly MqttClientVariableProperty _variablePropertys = new();
 | 
			
		||||
@@ -206,12 +206,9 @@ public partial class MqttClient : BusinessBaseWithCacheIntervalScript<VariableBa
 | 
			
		||||
        //TD设备上线
 | 
			
		||||
 | 
			
		||||
        var data = ThingsBoardDeviceConnectQueue.ToListWithDequeue();
 | 
			
		||||
        if (data?.Count > 0)
 | 
			
		||||
        foreach (var item in data)
 | 
			
		||||
        {
 | 
			
		||||
            foreach (var item in data)
 | 
			
		||||
            {
 | 
			
		||||
                await UpdateThingsBoardDeviceConnect(item).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
            await UpdateThingsBoardDeviceConnect(item).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -37,7 +37,7 @@ namespace ThingsGateway.Plugin.Mqtt;
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// MqttClient
 | 
			
		||||
/// </summary>
 | 
			
		||||
public partial class MqttClient : BusinessBaseWithCacheIntervalScript<VariableBasicData, DeviceBasicData, AlarmVariable>
 | 
			
		||||
public partial class MqttClient : BusinessBaseWithCacheIntervalScriptAll
 | 
			
		||||
{
 | 
			
		||||
    private static readonly CompositeFormat RpcTopic = CompositeFormat.Parse("{0}/+");
 | 
			
		||||
    public const string ThingsBoardRpcTopic = "v1/gateway/rpc";
 | 
			
		||||
@@ -148,7 +148,7 @@ public partial class MqttClient : BusinessBaseWithCacheIntervalScript<VariableBa
 | 
			
		||||
 | 
			
		||||
                foreach (var group in varGroup)
 | 
			
		||||
                {
 | 
			
		||||
                    AddQueueVarModel(new CacheDBItem<List<VariableBasicData>>(group.AdaptIEnumerableVariableBasicData()));
 | 
			
		||||
                    AddQueueVarModel(new CacheDBItem<List<VariableBasicData>>(group.ToList()));
 | 
			
		||||
                }
 | 
			
		||||
                foreach (var variable in varList)
 | 
			
		||||
                {
 | 
			
		||||
@@ -206,7 +206,7 @@ public partial class MqttClient : BusinessBaseWithCacheIntervalScript<VariableBa
 | 
			
		||||
 | 
			
		||||
    #region private
 | 
			
		||||
 | 
			
		||||
    private async ValueTask<OperResult> Update(List<TopicArray> topicArrayList, CancellationToken cancellationToken)
 | 
			
		||||
    private async ValueTask<OperResult> Update(IEnumerable<TopicArray> topicArrayList, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        foreach (TopicArray topicArray in topicArrayList)
 | 
			
		||||
        {
 | 
			
		||||
@@ -255,7 +255,7 @@ public partial class MqttClient : BusinessBaseWithCacheIntervalScript<VariableBa
 | 
			
		||||
        //保留消息
 | 
			
		||||
        //分解List,避免超出mqtt字节大小限制
 | 
			
		||||
        var varData = IdVariableRuntimes.Select(a => a.Value).AdaptIEnumerableVariableBasicData().ChunkBetter(_driverPropertys.SplitSize);
 | 
			
		||||
        var devData = CollectDevices?.Select(a => a.Value).AdaptListDeviceBasicData().ChunkBetter(_driverPropertys.SplitSize);
 | 
			
		||||
        var devData = CollectDevices?.Select(a => a.Value).AdaptIEnumerableDeviceBasicData().ChunkBetter(_driverPropertys.SplitSize);
 | 
			
		||||
        var alramData = GlobalData.ReadOnlyRealAlarmIdVariables.Select(a => a.Value).ChunkBetter(_driverPropertys.SplitSize);
 | 
			
		||||
        foreach (var item in varData)
 | 
			
		||||
        {
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,7 @@ namespace ThingsGateway.Plugin.Mqtt;
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// MqttServer,RPC方法适配mqttNet
 | 
			
		||||
/// </summary>
 | 
			
		||||
public partial class MqttServer : BusinessBaseWithCacheIntervalScript<VariableBasicData, DeviceBasicData, AlarmVariable>
 | 
			
		||||
public partial class MqttServer : BusinessBaseWithCacheIntervalScriptAll
 | 
			
		||||
{
 | 
			
		||||
    private readonly MqttServerProperty _driverPropertys = new();
 | 
			
		||||
    private readonly MqttClientVariableProperty _variablePropertys = new();
 | 
			
		||||
 
 | 
			
		||||
@@ -34,7 +34,7 @@ namespace ThingsGateway.Plugin.Mqtt;
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// MqttServer
 | 
			
		||||
/// </summary>
 | 
			
		||||
public partial class MqttServer : BusinessBaseWithCacheIntervalScript<VariableBasicData, DeviceBasicData, AlarmVariable>
 | 
			
		||||
public partial class MqttServer : BusinessBaseWithCacheIntervalScriptAll
 | 
			
		||||
{
 | 
			
		||||
    private static readonly CompositeFormat RpcTopic = CompositeFormat.Parse("{0}/+");
 | 
			
		||||
    private MQTTnet.Server.MqttServer _mqttServer;
 | 
			
		||||
@@ -103,7 +103,7 @@ public partial class MqttServer : BusinessBaseWithCacheIntervalScript<VariableBa
 | 
			
		||||
 | 
			
		||||
                foreach (var group in varGroup)
 | 
			
		||||
                {
 | 
			
		||||
                    AddQueueVarModel(new CacheDBItem<List<VariableBasicData>>(group.AdaptIEnumerableVariableBasicData()));
 | 
			
		||||
                    AddQueueVarModel(new CacheDBItem<List<VariableBasicData>>(group.ToList()));
 | 
			
		||||
                }
 | 
			
		||||
                foreach (var variable in varList)
 | 
			
		||||
                {
 | 
			
		||||
@@ -138,7 +138,7 @@ public partial class MqttServer : BusinessBaseWithCacheIntervalScript<VariableBa
 | 
			
		||||
    }
 | 
			
		||||
    #region private
 | 
			
		||||
 | 
			
		||||
    private async ValueTask<OperResult> Update(List<TopicArray> topicArrayList, CancellationToken cancellationToken)
 | 
			
		||||
    private async ValueTask<OperResult> Update(IEnumerable<TopicArray> topicArrayList, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        foreach (var topicArray in topicArrayList)
 | 
			
		||||
        {
 | 
			
		||||
@@ -250,7 +250,7 @@ public partial class MqttServer : BusinessBaseWithCacheIntervalScript<VariableBa
 | 
			
		||||
        //首次连接时的保留消息
 | 
			
		||||
        //分解List,避免超出mqtt字节大小限制
 | 
			
		||||
        var varData = IdVariableRuntimes.Select(a => a.Value).AdaptIEnumerableVariableBasicData().ChunkBetter(_driverPropertys.SplitSize);
 | 
			
		||||
        var devData = CollectDevices?.Select(a => a.Value).AdaptListDeviceBasicData().ChunkBetter(_driverPropertys.SplitSize);
 | 
			
		||||
        var devData = CollectDevices?.Select(a => a.Value).AdaptIEnumerableDeviceBasicData().ChunkBetter(_driverPropertys.SplitSize);
 | 
			
		||||
        var alramData = GlobalData.ReadOnlyRealAlarmIdVariables.Select(a => a.Value).ChunkBetter(_driverPropertys.SplitSize);
 | 
			
		||||
        List<MqttApplicationMessage> Messages = new();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -219,6 +219,7 @@ public class OpcDaMaster : CollectBase
 | 
			
		||||
        {
 | 
			
		||||
            if (CurrentDevice.Pause)
 | 
			
		||||
                return;
 | 
			
		||||
            if (TaskSchedulerLoop?.Stoped == true) return;
 | 
			
		||||
            if (DisposedValue)
 | 
			
		||||
                return;
 | 
			
		||||
            LogMessage?.Trace($"{ToString()} Change:{Environment.NewLine} {values?.ToSystemTextJsonString()}");
 | 
			
		||||
@@ -227,6 +228,7 @@ public class OpcDaMaster : CollectBase
 | 
			
		||||
            {
 | 
			
		||||
                if (CurrentDevice.Pause)
 | 
			
		||||
                    return;
 | 
			
		||||
                if (TaskSchedulerLoop?.Stoped == true) return;
 | 
			
		||||
                if (DisposedValue)
 | 
			
		||||
                    return;
 | 
			
		||||
                var type = data.Value.GetType();
 | 
			
		||||
@@ -240,6 +242,7 @@ public class OpcDaMaster : CollectBase
 | 
			
		||||
                {
 | 
			
		||||
                    if (CurrentDevice.Pause)
 | 
			
		||||
                        return;
 | 
			
		||||
                    if (TaskSchedulerLoop?.Stoped == true) return;
 | 
			
		||||
                    if (DisposedValue)
 | 
			
		||||
                        return;
 | 
			
		||||
                    var value = data.Value;
 | 
			
		||||
 
 | 
			
		||||
@@ -363,12 +363,9 @@ public class OpcUaMaster : CollectBase
 | 
			
		||||
                return;
 | 
			
		||||
            if (DisposedValue)
 | 
			
		||||
                return;
 | 
			
		||||
            if (TaskSchedulerLoop?.Stoped == true) return;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            if (CurrentDevice.Pause)
 | 
			
		||||
            {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            LogMessage?.Trace($"Change: {Environment.NewLine} {data.monitoredItem.StartNodeId} : {data.jToken?.ToString()}");
 | 
			
		||||
 | 
			
		||||
@@ -390,6 +387,7 @@ public class OpcUaMaster : CollectBase
 | 
			
		||||
                    return;
 | 
			
		||||
                if (DisposedValue)
 | 
			
		||||
                    return;
 | 
			
		||||
                if (TaskSchedulerLoop?.Stoped == true) return;
 | 
			
		||||
 | 
			
		||||
                if (isGood)
 | 
			
		||||
                {
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user