Compare commits

...

21 Commits

Author SHA1 Message Date
Diego
dbee8496cb opcuaServer支持decimal类型 2025-07-08 16:53:28 +08:00
Diego
044e78bea9 opcuaserver回退修改 2025-07-08 15:52:12 +08:00
Diego
fe79128d90 build: 10.9.26
fix: opcuaserver添加变量属性出现错误,已回退
refactor: 网关监控树节点保持展开状态
refactor: cache插件类日志输出
2025-07-08 14:33:27 +08:00
Diego
34120da008 ToDecimal方法添加异步捕获,防止double类型值超限 2025-07-08 11:21:06 +08:00
Diego
4c62bb0b21 10.9.25 2025-07-08 10:19:01 +08:00
Diego
46b16279c7 build: 10.9.24 2025-07-08 09:41:45 +08:00
2248356998 qq.com
0779efc5dd refactor: 冗余服务代码修正 2025-07-08 02:38:05 +08:00
Diego
8acdb780e8 nuget本地源创建文件夹 2025-07-07 19:07:08 +08:00
Diego
2e310b919e 添加Keys文件夹 2025-07-07 16:54:58 +08:00
Diego
4155c07269 10.9.23 2025-07-07 15:20:39 +08:00
Diego
32fa833736 build: 10.9.22 2025-07-07 15:10:06 +08:00
Diego
2258f08555 build: 10.9.19 2025-07-07 13:44:19 +08:00
2248356998 qq.com
d90b32f165 refactor: 调整冗余服务代码 2025-07-07 00:15:46 +08:00
2248356998 qq.com
1492377322 refactor:调整部分插件父类 2025-07-06 01:33:15 +08:00
Diego
32eefbf545 build: 10.9.18
feat: 网关监控树节点添加右侧常用操作按钮
2025-07-04 13:54:35 +08:00
Diego
a825ca5f6f 网关监控树节点添加右侧常用操作按钮 2025-07-04 12:50:07 +08:00
Diego
e270b0c4f6 no message 2025-07-04 09:01:31 +08:00
Diego
6ae44ccf58 10.9.17 2025-07-04 08:36:37 +08:00
Diego
1aa0df6339 feat: 支持变量低内存导出 2025-07-03 23:24:56 +08:00
Diego
62c3693dbe 导出通道和设备支持IEnumerable低内存 2025-07-03 19:34:51 +08:00
Diego
e4e503c97b build: 添加nuget.config,自定义nuget源,方便本地调试自行编译 2025-07-03 12:43:51 +08:00
160 changed files with 5408 additions and 4785 deletions

View File

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

View File

@@ -90,8 +90,8 @@
<h6> @((100 - (availableMemory * 100.00 / memory)).ToString("F2") + " %") </h6>
<span> @Localizer["WorkingSet"] <i> @(HardwareJob.HardwareInfo.WorkingSet + " MB")</i></span>
<span> @Localizer["AvailableMemory"] <i> @((availableMemory / 1024.00 / 1024 / 1024).ToString("F2") + " GB")</i></span>
<span> @Localizer["TotalMemory"] <i> @((memory / 1024.00 / 1024 / 1024).ToString("F2") + " GB")</i></span>
<span> @Localizer["AvailableMemory"] <i> @((availableMemory / 1024.00 / 1024).ToString("F2") + " GB")</i></span>
<span> @Localizer["TotalMemory"] <i> @((memory / 1024.00 / 1024).ToString("F2") + " GB")</i></span>
</div>
</Circle>

View File

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

View File

@@ -107,7 +107,7 @@ public class Startup : AppStartup
.AddHubOptions(options =>
{
//单个传入集线器消息的最大大小。默认 32 KB
options.MaximumReceiveMessageSize = 1024 * 1024;
options.MaximumReceiveMessageSize = 32 * 1024 * 1024;
//可为客户端上载流缓冲的最大项数。 如果达到此限制,则会阻止处理调用,直到服务器处理流项。
options.StreamBufferCapacity = 30;
options.ClientTimeoutInterval = TimeSpan.FromMinutes(2);
@@ -126,7 +126,7 @@ public class Startup : AppStartup
}).AddHubOptions(options =>
{
//单个传入集线器消息的最大大小。默认 32 KB
options.MaximumReceiveMessageSize = 1024 * 1024;
options.MaximumReceiveMessageSize = 32 * 1024 * 1024;
//可为客户端上载流缓冲的最大项数。 如果达到此限制,则会阻止处理调用,直到服务器处理流项。
options.StreamBufferCapacity = 30;
options.ClientTimeoutInterval = TimeSpan.FromMinutes(2);

View File

@@ -56,6 +56,35 @@ public static class QueryPageOptionsExtensions
return datas;
}
public static IEnumerable<T> GetQuery<T>(this IEnumerable<T> query, QueryPageOptions option, Func<IEnumerable<T>, IEnumerable<T>>? queryFunc = null, FilterKeyValueAction where = null)
{
if (queryFunc != null)
query = queryFunc(query);
where ??= option.ToFilter();
if (where.HasFilters())
{
query = query.Where(where.GetFilterFunc<T>());//name asc模式
}
if (option.SortOrder != SortOrder.Unset && !string.IsNullOrEmpty(option.SortName))
{
var invoker = Utility.GetSortFunc<T>();
query = invoker(query, option.SortName, option.SortOrder);
}
else if (option.SortList.Count > 0)
{
var invoker = Utility.GetSortListFunc<T>();
query = invoker(query, option.SortList);
}
else if (option.AdvancedSortList.Count > 0)
{
var invoker = Utility.GetSortListFunc<T>();
query = invoker(query, option.AdvancedSortList);
}
return query;
}
/// <summary>
/// 根据查询条件返回sqlsugar ISugarQueryable
/// </summary>

View File

@@ -78,6 +78,13 @@ public class BaseService<T> : IDataService<T>, IDisposable where T : class, new(
/// <inheritdoc/>
public virtual async Task<QueryData<T>> QueryAsync(QueryPageOptions option, Func<ISugarQueryable<T>, ISugarQueryable<T>>? queryFunc = null, FilterKeyValueAction where = null)
{
using var db = GetDB();
ISugarQueryable<T> query = GetQuery(db, option, queryFunc, where);
return await GetQueryData(option, query).ConfigureAwait(false);
}
public static async Task<QueryData<T>> GetQueryData(QueryPageOptions option, ISugarQueryable<T> query)
{
var ret = new QueryData<T>()
{
@@ -87,12 +94,6 @@ public class BaseService<T> : IDataService<T>, IDisposable where T : class, new(
IsSearch = option.Searches.Count > 0
};
using var db = GetDB();
var query = db.Queryable<T>();
if (queryFunc != null)
query = queryFunc(query);
query = db.GetQuery<T>(option, query, where);
if (option.IsPage)
{
RefAsync<int> totalCount = 0;
@@ -117,9 +118,19 @@ public class BaseService<T> : IDataService<T>, IDisposable where T : class, new(
ret.TotalCount = items.Count;
ret.Items = items;
}
return ret;
}
public static ISugarQueryable<T> GetQuery(SqlSugarClient db, QueryPageOptions option, Func<ISugarQueryable<T>, ISugarQueryable<T>>? queryFunc, FilterKeyValueAction where)
{
var query = db.Queryable<T>();
if (queryFunc != null)
query = queryFunc(query);
query = db.GetQuery<T>(option, query, where);
return query;
}
/// <inheritdoc/>
public virtual async Task<bool> SaveAsync(T model, ItemChangedType changedType)
{

View File

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

View File

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

View File

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

View File

@@ -88,11 +88,11 @@ public class MachineInfo : IExtend
[DisplayName("磁盘序列号")]
public String? DiskID { get; set; }
/// <summary>内存总量。单位Byte</summary>
/// <summary>内存总量。单位KB</summary>
[DisplayName("内存总量")]
public UInt64 Memory { get; set; }
/// <summary>可用内存。单位Byte</summary>
/// <summary>可用内存。单位KB</summary>
[DisplayName("可用内存")]
public UInt64 AvailableMemory { get; set; }
@@ -337,7 +337,7 @@ public class MachineInfo : IExtend
#if NETFRAMEWORK || WINDOWS
{
var ci = new Microsoft.VisualBasic.Devices.ComputerInfo();
Memory = ci.TotalPhysicalMemory;
Memory = (ulong)(ci.TotalPhysicalMemory / 1024.0);
}
#endif
@@ -557,7 +557,7 @@ public class MachineInfo : IExtend
//if (dic2.TryGetValue("Model Name", out str)) Product = str;
if (dic.TryGetValue("Model Identifier", out var str)) Product = str;
if (dic.TryGetValue("Processor Name", out str)) Processor = str;
if (dic.TryGetValue("Memory", out str)) Memory = (UInt64)str.TrimEnd("GB").Trim().ToLong() * 1024 * 1024 * 1024;
if (dic.TryGetValue("Memory", out str)) Memory = (UInt64)str.TrimEnd("GB").Trim().ToLong() * 1024 * 1024;
if (dic.TryGetValue("Serial Number (system)", out str)) Serial = str;
if (dic.TryGetValue("Hardware UUID", out str)) UUID = str;
if (dic.TryGetValue("Processor Name", out str)) Processor = str;
@@ -594,8 +594,8 @@ public class MachineInfo : IExtend
ms.Init();
if (GlobalMemoryStatusEx(ref ms))
{
Memory = ms.ullTotalPhys;
AvailableMemory = ms.ullAvailPhys;
Memory = (ulong)(ms.ullTotalPhys / 1024.0);
AvailableMemory = (ulong)(ms.ullAvailPhys / 1024.0);
}
GetSystemTimes(out var idleTime, out var kernelTime, out var userTime);
@@ -695,15 +695,15 @@ public class MachineInfo : IExtend
if (dic != null)
{
if (dic.TryGetValue("MemTotal", out var str) && !str.IsNullOrEmpty())
Memory = (UInt64)str.TrimEnd(" kB").ToInt() * 1024;
Memory = (UInt64)str.TrimEnd(" kB").ToLong();
if (dic.TryGetValue("MemAvailable", out str) && !str.IsNullOrEmpty())
AvailableMemory = (UInt64)str.TrimEnd(" kB").ToInt() * 1024;
AvailableMemory = (UInt64)str.TrimEnd(" kB").ToLong();
else if (dic.TryGetValue("MemFree", out str) && !str.IsNullOrEmpty())
AvailableMemory =
(UInt64)(str.TrimEnd(" kB").ToInt() +
dic["Buffers"]?.TrimEnd(" kB").ToInt() ?? 0 +
dic["Cached"]?.TrimEnd(" kB").ToInt() ?? 0) * 1024;
(UInt64)(str.TrimEnd(" kB").ToLong() +
dic["Buffers"]?.TrimEnd(" kB").ToLong() ?? 0 +
dic["Cached"]?.TrimEnd(" kB").ToLong() ?? 0);
}
// A2/A4温度获取BuildrootCPU温度和主板温度

View File

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

View File

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

View File

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

View File

@@ -245,7 +245,7 @@ public abstract class Logger : ILog
sb.AppendFormat("#CPU: {0}\r\n", Environment.ProcessorCount);
if (mi != null)
{
sb.AppendFormat("#Memory: {0:n0}M/{1:n0}M\r\n", mi.AvailableMemory / 1024 / 1024, mi.Memory / 1024 / 1024);
sb.AppendFormat("#Memory: {0:n0}M/{1:n0}M\r\n", mi.AvailableMemory / 1024, mi.Memory / 1024);
sb.AppendFormat("#Processor: {0}\r\n", mi.Processor);
if (!mi.Product.IsNullOrEmpty()) sb.AppendFormat("#Product: {0} / {1}\r\n", mi.Product, mi.Vendor);
if (mi.Temperature > 0) sb.AppendFormat("#Temperature: {0}\r\n", mi.Temperature);

View File

@@ -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不要调用析构函数

View File

@@ -44,3 +44,14 @@ public class ImportPreviewOutput<T> : ImportPreviewOutputBase where T : class
/// </summary>
public Dictionary<string, T> Data { get; set; } = new();
}
/// <summary>
/// 导入预览
/// </summary>
/// <typeparam name="T"></typeparam>
public class ImportPreviewListOutput<T> : ImportPreviewOutputBase where T : class
{
/// <summary>
/// 数据
/// </summary>
public List<T> Data { get; set; } = new();
}

View File

@@ -683,7 +683,7 @@ namespace ThingsGateway.SqlSugar
var entityInfo = this.Context.EntityMaintenance.GetEntityInfo<T>();
var columns = UtilMethods.GetColumnInfo(dr);
var cacheKey = "ForEachDataReader" + typeof(T).GetHashCode() + string.Join(",", columns.Select(it => it.Item1 + it.Item2.Name + "_"));
IDataReaderEntityBuilder<T> entytyList = this.Context.Utilities.GetReflectionInoCacheInstance().GetOrCreate("cacheKey", () =>
IDataReaderEntityBuilder<T> entytyList = this.Context.Utilities.GetReflectionInoCacheInstance().GetOrCreate(cacheKey, () =>
{
var cacheResult = new IDataReaderEntityBuilder<T>(this.Context, dr,
columns.Select(it => it.Item1).ToList()).CreateBuilder(typeof(T));
@@ -703,6 +703,38 @@ namespace ThingsGateway.SqlSugar
this.Context.Ado.Close();
}
}
public IEnumerable<T> GetEnumerable()
{
var queryable = this.Clone();
var sql = queryable.ToSql();
var dr = this.Context.Ado.GetDataReader(sql.Key, sql.Value);
var entityInfo = this.Context.EntityMaintenance.GetEntityInfo<T>();
var columns = UtilMethods.GetColumnInfo(dr);
var cacheKey = "GetEnumerable" + typeof(T).GetHashCode() + string.Join(",", columns.Select(it => it.Item1 + it.Item2.Name + "_"));
IDataReaderEntityBuilder<T> entytyList = this.Context.Utilities.GetReflectionInoCacheInstance().GetOrCreate(cacheKey, () =>
{
var cacheResult = new IDataReaderEntityBuilder<T>(this.Context, dr,
columns.Select(it => it.Item1).ToList()).CreateBuilder(typeof(T));
return cacheResult;
});
using (dr)
{
while (dr.Read())
{
var order = entytyList.Build(dr);
yield return order;
}
}
if (this.Context.CurrentConnectionConfig.IsAutoCloseConnection)
{
this.Context.Ado.Close();
}
}
public async Task ForEachDataReaderAsync(Action<T> action)
{
var queryable = this.Clone();
@@ -711,7 +743,7 @@ namespace ThingsGateway.SqlSugar
var entityInfo = this.Context.EntityMaintenance.GetEntityInfo<T>();
var columns = UtilMethods.GetColumnInfo(dr);
var cacheKey = "ForEachDataReader" + typeof(T).GetHashCode() + string.Join(",", columns.Select(it => it.Item1 + it.Item2.Name + "_"));
IDataReaderEntityBuilder<T> entytyList = this.Context.Utilities.GetReflectionInoCacheInstance().GetOrCreate("cacheKey", () =>
IDataReaderEntityBuilder<T> entytyList = this.Context.Utilities.GetReflectionInoCacheInstance().GetOrCreate(cacheKey, () =>
{
var cacheResult = new IDataReaderEntityBuilder<T>(this.Context, dr,
columns.Select(it => it.Item1).ToList()).CreateBuilder(typeof(T));
@@ -731,6 +763,42 @@ namespace ThingsGateway.SqlSugar
this.Context.Ado.Close();
}
}
/// <summary>
/// Diego 新增一个延迟返回
/// </summary>
/// <returns></returns>
public async IAsyncEnumerable<T> GetAsyncEnumerable()
{
var queryable = this.Clone();
var sql = queryable.ToSql();
var dr = await Context.Ado.GetDataReaderAsync(sql.Key, sql.Value).ConfigureAwait(false);
var entityInfo = this.Context.EntityMaintenance.GetEntityInfo<T>();
var columns = UtilMethods.GetColumnInfo(dr);
var cacheKey = "GetAsyncEnumerable" + typeof(T).GetHashCode() + string.Join(",", columns.Select(it => it.Item1 + it.Item2.Name + "_"));
IDataReaderEntityBuilder<T> entytyList = this.Context.Utilities.GetReflectionInoCacheInstance().GetOrCreate(cacheKey, () =>
{
var cacheResult = new IDataReaderEntityBuilder<T>(this.Context, dr,
columns.Select(it => it.Item1).ToList()).CreateBuilder(typeof(T));
return cacheResult;
});
using (dr)
{
while (dr.Read())
{
var order = entytyList.Build(dr);
yield return order;
}
}
if (this.Context.CurrentConnectionConfig.IsAutoCloseConnection)
{
this.Context.Ado.Close();
}
}
public virtual void ForEach(Action<T> action, int singleMaxReads = 300, System.Threading.CancellationTokenSource cancellationTokenSource = null)
{
Check.Exception(this.QueryBuilder.Skip > 0 || this.QueryBuilder.Take > 0, ErrorMessage.GetThrowMessage("no support Skip take, use PageForEach", "不支持Skip Take,请使用 Queryale.PageForEach"));

View File

@@ -32,7 +32,8 @@ namespace ThingsGateway.SqlSugar
NavISugarQueryable<T> Includes<TReturn1, TReturn2, TReturn3>(Expression<Func<T, TReturn1>> include1, Expression<Func<TReturn1, TReturn2>> include2, Expression<Func<TReturn2, List<TReturn3>>> include3);
NavISugarQueryable<T> Includes<TReturn1, TReturn2, TReturn3>(Expression<Func<T, List<TReturn1>>> include1, Expression<Func<TReturn1, TReturn2>> include2, Expression<Func<TReturn2, TReturn3>> include3);
NavISugarQueryable<T> Includes<TReturn1, TReturn2, TReturn3>(Expression<Func<T, TReturn1>> include1, Expression<Func<TReturn1, List<TReturn2>>> include2, Expression<Func<TReturn2, List<TReturn3>>> include3);
IAsyncEnumerable<T> GetAsyncEnumerable();
IEnumerable<T> GetEnumerable();
}
/// <summary>

View File

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

View File

@@ -1,10 +1,11 @@
<Project>
<PropertyGroup>
<PluginVersion>10.9.14</PluginVersion>
<ProPluginVersion>10.9.14</ProPluginVersion>
<AuthenticationVersion>2.9.5</AuthenticationVersion>
<SourceGeneratorVersion>10.9.5</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>

View File

@@ -51,6 +51,4 @@ public enum DataTypeEnum
/// <inheritdoc/>
Double,
/// <inheritdoc/>
Decimal,
}

View File

@@ -34,7 +34,6 @@ public static class DataTypeExtensions
DataTypeEnum.UInt64 => 8,
DataTypeEnum.Single => 4,
DataTypeEnum.Double => 8,
DataTypeEnum.Decimal => 16,
_ => 0,
};
}

View File

@@ -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())
{

View File

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

View File

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

View File

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

View File

@@ -7,6 +7,7 @@
void Start();
void Stop();
public Int32 Period { get; }
bool Enable { get; }
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 //上传设备内存队列中的数据
}
}

View File

@@ -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 //上传设备内存队列中的数据
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 //上传变量内存队列中的数据
}
}

View File

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

View File

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

View File

@@ -20,7 +20,7 @@ namespace ThingsGateway.Gateway.Application
foreach (var a in data)
{
var value = a.Value as IList<Dictionary<string, object>>;
var value = (a.Value as IEnumerable<Dictionary<string, object>>).ToList();
var uSheetData = new USheetData();
uSheetData.id = a.Key;

View File

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

View File

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

View File

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

View File

@@ -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": "获取变量信息",

View File

@@ -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);
@@ -28,7 +30,6 @@ public static partial class GatewayMapper
[MapProperty(nameof(Variable.InitValue), nameof(VariableRuntime.Value))]
public static partial VariableRuntime AdaptVariableRuntime(this Variable src);
public static partial List<Variable> AdaptListVariable(this IEnumerable<Variable> src);
public static partial DeviceRuntime AdaptDeviceRuntime(this Device src);
@@ -56,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);
}

View File

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

View File

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

View File

@@ -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
@@ -113,11 +179,11 @@ public class ChannelRuntimeService : IChannelRuntimeService
}
}
public Task<Dictionary<string, ImportPreviewOutputBase>> PreviewAsync(IBrowserFile browserFile) => GlobalData.ChannelService.PreviewAsync(browserFile);
public Task<Dictionary<string, object>> ExportChannelAsync(ExportFilter exportFilter) => GlobalData.ChannelService.ExportChannelAsync(exportFilter);
public Task<MemoryStream> ExportMemoryStream(List<Channel> data) =>
public Task<MemoryStream> ExportMemoryStream(IEnumerable<Channel> data) =>
GlobalData.ChannelService.ExportMemoryStream(data);
public async Task ImportChannelAsync(Dictionary<string, ImportPreviewOutputBase> input, bool restart = true)
@@ -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();

View File

@@ -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)
@@ -192,6 +267,14 @@ internal sealed class ChannelService : BaseService<Channel>, IChannelService
/// </summary>
/// <param name="exportFilter">查询条件</param>
public async Task<QueryData<Channel>> PageAsync(ExportFilter exportFilter)
{
var whereQuery = await GetWhereQueryFunc(exportFilter).ConfigureAwait(false);
return await QueryAsync(exportFilter.QueryPageOptions, whereQuery
, exportFilter.FilterKeyValueAction).ConfigureAwait(false);
}
private async Task<Func<ISugarQueryable<Channel>, ISugarQueryable<Channel>>> GetWhereQueryFunc(ExportFilter exportFilter)
{
HashSet<long>? channel = null;
if (exportFilter.PluginType != null)
@@ -200,16 +283,15 @@ internal sealed class ChannelService : BaseService<Channel>, IChannelService
channel = (await GetAllAsync().ConfigureAwait(false)).Where(a => pluginInfo.Contains(a.PluginName)).Select(a => a.Id).ToHashSet();
}
var dataScope = await GlobalData.SysUserService.GetCurrentUserDataScopeAsync().ConfigureAwait(false);
return await QueryAsync(exportFilter.QueryPageOptions, a => a
var whereQuery = (ISugarQueryable<Channel> a) => a
.WhereIF(!exportFilter.QueryPageOptions.SearchText.IsNullOrWhiteSpace(), a => a.Name.Contains(exportFilter.QueryPageOptions.SearchText!))
.WhereIF(!exportFilter.PluginName.IsNullOrWhiteSpace(), a => a.PluginName == exportFilter.PluginName)
.WhereIF(channel != null, a => channel.Contains(a.Id))
.WhereIF(exportFilter.ChannelId != null, a => a.Id == exportFilter.ChannelId)
.WhereIF(dataScope != null && dataScope?.Count > 0, u => dataScope.Contains(u.CreateOrgId))//在指定机构列表查询
.WhereIF(dataScope?.Count == 0, u => u.CreateUserId == UserManager.UserId)
, exportFilter.FilterKeyValueAction).ConfigureAwait(false);
.WhereIF(dataScope?.Count == 0, u => u.CreateUserId == UserManager.UserId);
return whereQuery;
}
/// <summary>
@@ -273,19 +355,31 @@ internal sealed class ChannelService : BaseService<Channel>, IChannelService
[OperDesc("ExportChannel", isRecordPar: false, localizerType: typeof(Channel))]
public async Task<Dictionary<string, object>> ExportChannelAsync(ExportFilter exportFilter)
{
var data = await PageAsync(exportFilter).ConfigureAwait(false);
return ChannelServiceHelpers.ExportChannelCore(data.Items);
var channels = await GetEnumerableData(exportFilter).ConfigureAwait(false);
var rows = ChannelServiceHelpers.ExportRows(channels); // IEnumerable 延迟执行
var sheets = ChannelServiceHelpers.WrapAsSheet(ExportString.ChannelName, rows);
return sheets;
}
private async Task<IAsyncEnumerable<Channel>> GetEnumerableData(ExportFilter exportFilter)
{
var db = GetDB();
var whereQuery = await GetWhereQueryFunc(exportFilter).ConfigureAwait(false);
var query = GetQuery(db, exportFilter.QueryPageOptions, whereQuery, exportFilter.FilterKeyValueAction);
return query.GetAsyncEnumerable();
}
/// <inheritdoc/>
[OperDesc("ExportChannel", isRecordPar: false, localizerType: typeof(Channel))]
public async Task<MemoryStream> ExportMemoryStream(List<Channel> data)
public async Task<MemoryStream> ExportMemoryStream(IEnumerable<Channel> channels)
{
var sheets = ChannelServiceHelpers.ExportChannelCore(data);
var rows = ChannelServiceHelpers.ExportRows(channels); // IEnumerable 延迟执行
var sheets = ChannelServiceHelpers.WrapAsSheet(ExportString.ChannelName, rows);
var memoryStream = new MemoryStream();
await memoryStream.SaveAsAsync(sheets).ConfigureAwait(false);
memoryStream.Seek(0, SeekOrigin.Begin);
return memoryStream;
}
@@ -304,8 +398,8 @@ internal sealed class ChannelService : BaseService<Channel>, IChannelService
{
if (item.Key == ExportString.ChannelName)
{
var channelImports = ((ImportPreviewOutput<Channel>)item.Value).Data;
channels = channelImports.Select(a => a.Value).ToList();
var channelImports = ((ImportPreviewListOutput<Channel>)item.Value).Data;
channels = channelImports;
break;
}
}
@@ -315,8 +409,16 @@ internal sealed class ChannelService : BaseService<Channel>, IChannelService
ManageHelper.CheckChannelCount(insertData.Count);
using var db = GetDB();
await db.BulkCopyAsync(insertData, 100000).ConfigureAwait(false);
await db.BulkUpdateAsync(upData, 100000).ConfigureAwait(false);
if (GlobalData.HardwareJob.HardwareInfo.MachineInfo.AvailableMemory > 2 * 1024 * 1024)
{
await db.BulkCopyAsync(insertData, 200000).ConfigureAwait(false);
await db.BulkUpdateAsync(upData, 200000).ConfigureAwait(false);
}
else
{
await db.BulkCopyAsync(insertData, 10000).ConfigureAwait(false);
await db.BulkUpdateAsync(upData, 10000).ConfigureAwait(false);
}
DeleteChannelFromCache();
return channels.Select(a => a.Id).ToHashSet();
}
@@ -333,11 +435,10 @@ internal sealed class ChannelService : BaseService<Channel>, IChannelService
//导入检验结果
Dictionary<string, ImportPreviewOutputBase> ImportPreviews = new();
//设备页
ImportPreviewOutput<Channel> channelImportPreview = new();
foreach (var sheetName in sheetNames)
{
var rows = MiniExcel.Query(path, useHeaderRow: true, sheetName: sheetName).Cast<IDictionary<string, object>>();
SetChannelData(dataScope, channelDicts, ImportPreviews, channelImportPreview, sheetName, rows);
SetChannelData(dataScope, channelDicts, ImportPreviews, sheetName, rows);
}
return ImportPreviews;
@@ -348,16 +449,15 @@ internal sealed class ChannelService : BaseService<Channel>, IChannelService
}
}
public void SetChannelData(HashSet<long>? dataScope, Dictionary<string, Channel> channelDicts, Dictionary<string, ImportPreviewOutputBase> ImportPreviews, ImportPreviewOutput<Channel> channelImportPreview, string sheetName, IEnumerable<IDictionary<string, object>> rows)
public void SetChannelData(HashSet<long>? dataScope, Dictionary<string, Channel> channelDicts, Dictionary<string, ImportPreviewOutputBase> ImportPreviews, string sheetName, IEnumerable<IDictionary<string, object>> rows)
{
#region sheet
if (sheetName == ExportString.ChannelName)
{
int row = 1;
ImportPreviewOutput<Channel> importPreviewOutput = new();
ImportPreviewListOutput<Channel> importPreviewOutput = new();
ImportPreviews.Add(sheetName, importPreviewOutput);
channelImportPreview = importPreviewOutput;
List<Channel> channels = new();
var type = typeof(Channel);
// 获取目标类型的所有属性,并根据是否需要过滤 IgnoreExcelAttribute 进行筛选
@@ -432,7 +532,7 @@ internal sealed class ChannelService : BaseService<Channel>, IChannelService
return;
}
});
importPreviewOutput.Data = channels.ToDictionary(a => a.Name);
importPreviewOutput.Data = channels.DistinctBy(a => a.Name).ToList();
}
#endregion sheet

View File

@@ -18,20 +18,17 @@ namespace ThingsGateway.Gateway.Application;
public static class ChannelServiceHelpers
{
public static USheetDatas ExportChannel(IEnumerable<Channel> models)
public static USheetDatas ExportChannel(IEnumerable<Channel> channels)
{
var data = ExportChannelCore(models);
return USheetDataHelpers.GetUSheetDatas(data);
var rows = ExportRows(channels); // IEnumerable 延迟执行
var sheets = WrapAsSheet(ExportString.ChannelName, rows);
return USheetDataHelpers.GetUSheetDatas(sheets);
}
internal static Dictionary<string, object> ExportChannelCore(IEnumerable<Channel>? data)
internal static IEnumerable<Dictionary<string, object>> ExportRows(IEnumerable<Channel>? data)
{
//总数据
Dictionary<string, object> sheets = new();
//通道页
List<Dictionary<string, object>> channelExports = new();
if (data == null)
yield break;
#region
@@ -58,23 +55,74 @@ public static class ChannelServiceHelpers
foreach (var device in data)
{
Dictionary<string, object> channelExport = new();
foreach (var item in propertyInfos)
Dictionary<string, object> row = new();
foreach (var prop in propertyInfos)
{
//描述
var desc = type.GetPropertyDisplayName(item.Name);
//数据源增加
channelExport.Add(desc ?? item.Name, item.GetValue(device)?.ToString());
var desc = type.GetPropertyDisplayName(prop.Name);
row.Add(desc ?? prop.Name, prop.GetValue(device)?.ToString());
}
//添加完整设备信息
channelExports.Add(channelExport);
yield return row;
}
//添加设备页
sheets.Add(ExportString.ChannelName, channelExports);
return sheets;
}
internal static async IAsyncEnumerable<Dictionary<string, object>> ExportRows(IAsyncEnumerable<Channel>? data)
{
if (data == null)
yield break;
#region
var type = typeof(Channel);
var propertyInfos = type.GetRuntimeProperties().Where(a => a.GetCustomAttribute<IgnoreExcelAttribute>(false) == null)
.OrderBy(
a =>
{
var order = a.GetCustomAttribute<AutoGenerateColumnAttribute>()?.Order ?? int.MaxValue; ;
if (order < 0)
{
order = order + 10000000;
}
else if (order == 0)
{
order = 10000000;
}
return order;
}
)
;
#endregion
var enumerator = data.GetAsyncEnumerator();
while (await enumerator.MoveNextAsync().ConfigureAwait(false))
{
var device = enumerator.Current;
{
Dictionary<string, object> row = new();
foreach (var prop in propertyInfos)
{
var desc = type.GetPropertyDisplayName(prop.Name);
row.Add(desc ?? prop.Name, prop.GetValue(device)?.ToString());
}
yield return row;
}
}
}
internal static Dictionary<string, object> WrapAsSheet(string sheetName, IEnumerable<IDictionary<string, object>> rows)
{
return new Dictionary<string, object>
{
[sheetName] = rows
};
}
internal static Dictionary<string, object> WrapAsSheet(string sheetName, IAsyncEnumerable<IDictionary<string, object>> rows)
{
return new Dictionary<string, object>
{
[sheetName] = rows
};
}
public static async Task<Dictionary<string, ImportPreviewOutputBase>> ImportAsync(USheetDatas uSheetDatas)
{
@@ -85,7 +133,6 @@ public static class ChannelServiceHelpers
//导入检验结果
Dictionary<string, ImportPreviewOutputBase> ImportPreviews = new();
//设备页
ImportPreviewOutput<Channel> channelImportPreview = new();
var sheetNames = uSheetDatas.sheets.Keys.ToList();
foreach (var sheetName in sheetNames)
@@ -108,10 +155,11 @@ public static class ChannelServiceHelpers
rows.Add(expando);
}
GlobalData.ChannelService.SetChannelData(dataScope, channelDicts, ImportPreviews, channelImportPreview, sheetName, rows);
if (channelImportPreview.HasError)
GlobalData.ChannelService.SetChannelData(dataScope, channelDicts, ImportPreviews, sheetName, rows);
var data = ImportPreviews?.FirstOrDefault().Value;
if (data?.HasError == true)
{
throw new(channelImportPreview.Results.FirstOrDefault(a => !a.Success).ErrorMessage ?? "error");
throw new(data.Results.FirstOrDefault(a => !a.Success).ErrorMessage ?? "error");
}
}

View File

@@ -54,8 +54,9 @@ public interface IChannelRuntimeService
Task ImportChannelAsync(Dictionary<string, ImportPreviewOutputBase> input, bool restart);
Task<Dictionary<string, object>> ExportChannelAsync(ExportFilter exportFilter);
Task<Dictionary<string, ImportPreviewOutputBase>> PreviewAsync(IBrowserFile browserFile);
Task<MemoryStream> ExportMemoryStream(List<Channel> data);
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);
}

View File

@@ -53,7 +53,7 @@ internal interface IChannelService
/// </summary>
/// <param name="data">通道数据</param>
/// <returns>内存流</returns>
Task<MemoryStream> ExportMemoryStream(List<Channel> data);
Task<MemoryStream> ExportMemoryStream(IEnumerable<Channel> data);
/// <summary>
/// 从缓存/数据库获取全部信息
@@ -95,11 +95,12 @@ internal interface IChannelService
Task<bool> BatchSaveAsync(List<Channel> input, ItemChangedType type);
void SetChannelData(HashSet<long>? dataScope, Dictionary<string, Channel> channelDicts, Dictionary<string, ImportPreviewOutputBase> ImportPreviews, ImportPreviewOutput<Channel> channelImportPreview, string sheetName, IEnumerable<IDictionary<string, object>> rows);
void SetChannelData(HashSet<long>? dataScope, Dictionary<string, Channel> channelDicts, Dictionary<string, ImportPreviewOutputBase> ImportPreviews, string sheetName, IEnumerable<IDictionary<string, object>> rows);
/// <summary>
/// 保存是否输出日志和日志等级
/// </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);
}

View File

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

View File

@@ -218,6 +218,14 @@ internal sealed class DeviceService : BaseService<Device>, IDeviceService
/// </summary>
/// <param name="exportFilter">查询条件</param>
public async Task<QueryData<Device>> PageAsync(ExportFilter exportFilter)
{
var whereQuery = await GetWhereQueryFunc(exportFilter).ConfigureAwait(false);
return await QueryAsync(exportFilter.QueryPageOptions, whereQuery
, exportFilter.FilterKeyValueAction).ConfigureAwait(false);
}
private async Task<Func<ISugarQueryable<Device>, ISugarQueryable<Device>>> GetWhereQueryFunc(ExportFilter exportFilter)
{
HashSet<long>? channel = null;
if (!exportFilter.PluginName.IsNullOrWhiteSpace())
@@ -230,16 +238,38 @@ internal sealed class DeviceService : BaseService<Device>, IDeviceService
channel = (await _channelService.GetAllAsync().ConfigureAwait(false)).Where(a => pluginInfo.Contains(a.PluginName)).Select(a => a.Id).ToHashSet();
}
var dataScope = await GlobalData.SysUserService.GetCurrentUserDataScopeAsync().ConfigureAwait(false);
return await QueryAsync(exportFilter.QueryPageOptions, a => a
var whereQuery = (ISugarQueryable<Device> a) => a
.WhereIF(channel != null, a => channel.Contains(a.ChannelId))
.WhereIF(exportFilter.DeviceId != null, a => a.Id == exportFilter.DeviceId)
.WhereIF(!exportFilter.QueryPageOptions.SearchText.IsNullOrWhiteSpace(), a => a.Name.Contains(exportFilter.QueryPageOptions.SearchText!))
.WhereIF(dataScope != null && dataScope?.Count > 0, u => dataScope.Contains(u.CreateOrgId))//在指定机构列表查询
.WhereIF(dataScope?.Count == 0, u => u.CreateUserId == UserManager.UserId)
, exportFilter.FilterKeyValueAction).ConfigureAwait(false);
.WhereIF(dataScope?.Count == 0, u => u.CreateUserId == UserManager.UserId);
return whereQuery;
}
private async Task<Func<IEnumerable<Device>, IEnumerable<Device>>> GetWhereEnumerableFunc(ExportFilter exportFilter)
{
HashSet<long>? channel = null;
if (!exportFilter.PluginName.IsNullOrWhiteSpace())
{
channel = (await _channelService.GetAllAsync().ConfigureAwait(false)).Where(a => a.PluginName == exportFilter.PluginName).Select(a => a.Id).ToHashSet();
}
if (exportFilter.PluginType != null)
{
var pluginInfo = GlobalData.PluginService.GetList(exportFilter.PluginType).Select(a => a.FullName).ToHashSet();
channel = (await _channelService.GetAllAsync().ConfigureAwait(false)).Where(a => pluginInfo.Contains(a.PluginName)).Select(a => a.Id).ToHashSet();
}
var dataScope = await GlobalData.SysUserService.GetCurrentUserDataScopeAsync().ConfigureAwait(false);
var whereQuery = (IEnumerable<Device> a) => a
.WhereIF(channel != null, a => channel.Contains(a.ChannelId))
.WhereIF(exportFilter.DeviceId != null, a => a.Id == exportFilter.DeviceId)
.WhereIF(!exportFilter.QueryPageOptions.SearchText.IsNullOrWhiteSpace(), a => a.Name.Contains(exportFilter.QueryPageOptions.SearchText!))
.WhereIF(dataScope != null && dataScope?.Count > 0, u => dataScope.Contains(u.CreateOrgId))//在指定机构列表查询
.WhereIF(dataScope?.Count == 0, u => u.CreateUserId == UserManager.UserId);
return whereQuery;
}
/// <summary>
/// 保存设备
/// </summary>
@@ -298,18 +328,52 @@ internal sealed class DeviceService : BaseService<Device>, IDeviceService
public async Task<Dictionary<string, object>> ExportDeviceAsync(ExportFilter exportFilter)
{
//导出
var data = await PageAsync(exportFilter).ConfigureAwait(false);
var sheets = await DeviceServiceHelpers.ExportCoreAsync(data.Items).ConfigureAwait(false);
var devices = await GetAsyncEnumerableData(exportFilter).ConfigureAwait(false);
var plugins = await GetAsyncEnumerableData(exportFilter).ConfigureAwait(false);
var devicesSql = await GetEnumerableData(exportFilter).ConfigureAwait(false);
var deviceDicts = (await GlobalData.DeviceService.GetAllAsync().ConfigureAwait(false)).ToDictionary(a => a.Id);
var channelDicts = (await GlobalData.ChannelService.GetAllAsync().ConfigureAwait(false)).ToDictionary(a => a.Id);
var pluginSheetNames = devicesSql.Select(a => a.ChannelId).ToList().Select(a =>
{
channelDicts.TryGetValue(a, out var channel);
var pluginKey = channel?.PluginName;
return pluginKey;
}).ToHashSet();
var sheets = DeviceServiceHelpers.ExportSheets(devices, plugins, deviceDicts, channelDicts, pluginSheetNames); // IEnumerable 延迟执行
return sheets;
}
private async Task<IAsyncEnumerable<Device>> GetAsyncEnumerableData(ExportFilter exportFilter)
{
var whereQuery = await GetEnumerableData(exportFilter).ConfigureAwait(false);
return whereQuery.GetAsyncEnumerable();
}
private async Task<ISugarQueryable<Device>> GetEnumerableData(ExportFilter exportFilter)
{
var db = GetDB();
var whereQuery = await GetWhereQueryFunc(exportFilter).ConfigureAwait(false);
return GetQuery(db, exportFilter.QueryPageOptions, whereQuery, exportFilter.FilterKeyValueAction);
}
/// <summary>
/// 导出文件
/// </summary>
[OperDesc("ExportDevice", isRecordPar: false, localizerType: typeof(Device))]
public async Task<MemoryStream> ExportMemoryStream(IEnumerable<Device>? data, string channelName = null, string plugin = null)
public async Task<MemoryStream> ExportMemoryStream(List<Device>? models, string channelName = null, string plugin = null)
{
var sheets = await DeviceServiceHelpers.ExportCoreAsync(data, channelName, plugin).ConfigureAwait(false);
var deviceDicts = (await GlobalData.DeviceService.GetAllAsync().ConfigureAwait(false)).ToDictionary(a => a.Id);
var channelDicts = (await GlobalData.ChannelService.GetAllAsync().ConfigureAwait(false)).ToDictionary(a => a.Id);
var pluginSheetNames = models.Select(a => a.ChannelId).Select(a =>
{
channelDicts.TryGetValue(a, out var channel);
var pluginKey = channel?.PluginName;
return pluginKey;
}).ToHashSet();
var sheets = DeviceServiceHelpers.ExportSheets(models, deviceDicts, channelDicts, pluginSheetNames, channelName);
var memoryStream = new MemoryStream();
await memoryStream.SaveAsAsync(sheets).ConfigureAwait(false);
memoryStream.Seek(0, SeekOrigin.Begin);
@@ -343,8 +407,16 @@ internal sealed class DeviceService : BaseService<Device>, IDeviceService
ManageHelper.CheckDeviceCount(insertData.Count);
using var db = GetDB();
await db.BulkCopyAsync(insertData, 100000).ConfigureAwait(false);
await db.BulkUpdateAsync(upData, 100000).ConfigureAwait(false);
if (GlobalData.HardwareJob.HardwareInfo.MachineInfo.AvailableMemory > 2 * 1024 * 1024)
{
await db.BulkCopyAsync(insertData, 200000).ConfigureAwait(false);
await db.BulkUpdateAsync(upData, 200000).ConfigureAwait(false);
}
else
{
await db.BulkCopyAsync(insertData, 10000).ConfigureAwait(false);
await db.BulkUpdateAsync(upData, 10000).ConfigureAwait(false);
}
DeleteDeviceFromCache();
return devices.Select(a => a.Id).ToHashSet();
}

View File

@@ -21,106 +21,255 @@ public static class DeviceServiceHelpers
public static async Task<USheetDatas> ExportDeviceAsync(IEnumerable<Device> models)
{
var data = await ExportCoreAsync(models).ConfigureAwait(false);
var deviceDicts = (await GlobalData.DeviceService.GetAllAsync().ConfigureAwait(false)).ToDictionary(a => a.Id);
var channelDicts = (await GlobalData.ChannelService.GetAllAsync().ConfigureAwait(false)).ToDictionary(a => a.Id);
var pluginSheetNames = models.Select(a => a.ChannelId).Select(a =>
{
channelDicts.TryGetValue(a, out var channel);
var pluginKey = channel?.PluginName;
return pluginKey;
}).ToHashSet();
var data = ExportSheets(models, deviceDicts, channelDicts, pluginSheetNames); // IEnumerable 延迟执行
return USheetDataHelpers.GetUSheetDatas(data);
}
public static async Task<Dictionary<string, object>> ExportCoreAsync(IEnumerable<Device>? data, string channelName = null, string plugin = null)
public static Dictionary<string, object> ExportSheets(
IEnumerable<Device>? data,
Dictionary<long, Device>? deviceDicts,
Dictionary<long, Channel> channelDicts,
HashSet<string> pluginSheetNames,
string? channelName = null)
{
if (data?.Any() != true)
{
data = new List<Device>();
}
var deviceDicts = (await GlobalData.DeviceService.GetAllAsync().ConfigureAwait(false)).ToDictionary(a => a.Id);
var channelDicts = (await GlobalData.ChannelService.GetAllAsync().ConfigureAwait(false)).ToDictionary(a => a.Id);
//总数据
Dictionary<string, object> sheets = new();
//设备页
List<Dictionary<string, object>> deviceExports = new();
//设备附加属性转成Dict<表名,List<Dict<列名,列数据>>>的形式
Dictionary<string, List<Dictionary<string, object>>> devicePropertys = new();
var result = new Dictionary<string, object>();
result.Add(ExportString.DeviceName, GetDeviceSheets(data, deviceDicts, channelDicts, channelName));
ConcurrentDictionary<string, (object, Dictionary<string, PropertyInfo>)> propertysDict = new();
#region
var type = typeof(Device);
var propertyInfos = type.GetRuntimeProperties().Where(a => a.GetCustomAttribute<IgnoreExcelAttribute>(false) == null)
.OrderBy(
a =>
{
var order = a.GetCustomAttribute<AutoGenerateColumnAttribute>()?.Order ?? int.MaxValue; ;
if (order < 0)
{
order = order + 10000000;
}
else if (order == 0)
{
order = 10000000;
}
return order;
}
)
;
#endregion
foreach (var device in data)
foreach (var plugin in pluginSheetNames)
{
Dictionary<string, object> devExport = new();
deviceDicts.TryGetValue(device.RedundantDeviceId ?? 0, out var redundantDevice);
channelDicts.TryGetValue(device.ChannelId, out var channel);
var filtered = FilterPluginDevices(data, plugin, channelDicts);
var filtResult = PluginServiceUtil.GetFileNameAndTypeName(plugin);
var pluginSheets = GetPluginSheets(filtered, propertysDict, plugin);
result.Add(filtResult.TypeName, pluginSheets);
}
return result;
}
devExport.Add(ExportString.ChannelName, channel?.Name ?? channelName);
foreach (var item in propertyInfos)
{
//描述
var desc = type.GetPropertyDisplayName(item.Name);
//数据源增加
devExport.Add(desc ?? item.Name, item.GetValue(device)?.ToString());
}
//设备实体没有包含冗余设备名称,手动插入
devExport.Add(ExportString.RedundantDeviceName, redundantDevice?.Name);
//添加完整设备信息
deviceExports.Add(devExport);
#region sheet
//插件属性
//单个设备的行数据
Dictionary<string, object> driverInfo = new();
var propDict = device.DevicePropertys;
if (propertysDict.TryGetValue(channel?.PluginName ?? plugin, out var propertys))
public static Dictionary<string, object> ExportSheets(
IAsyncEnumerable<Device>? data1,
IAsyncEnumerable<Device>? data2,
Dictionary<long, Device>? deviceDicts,
Dictionary<long, Channel> channelDicts,
HashSet<string> pluginSheetNames,
string? channelName = null)
{
if (data1 == null || data2 == null)
return new();
var result = new Dictionary<string, object>();
result.Add(ExportString.DeviceName, GetDeviceSheets(data1, deviceDicts, channelDicts, channelName));
ConcurrentDictionary<string, (object, Dictionary<string, PropertyInfo>)> propertysDict = new();
foreach (var plugin in pluginSheetNames)
{
var filtered = FilterPluginDevices(data2, plugin, channelDicts);
var filtResult = PluginServiceUtil.GetFileNameAndTypeName(plugin);
var pluginSheets = GetPluginSheets(filtered, propertysDict, plugin);
result.Add(filtResult.TypeName, pluginSheets);
}
return result;
}
static IAsyncEnumerable<Device> FilterPluginDevices(IAsyncEnumerable<Device> data, string plugin, Dictionary<long, Channel> channelDicts)
{
return data.Where(device =>
{
if (channelDicts.TryGetValue(device.ChannelId, out var channel))
{
if (channel.PluginName == plugin)
return true;
else
return false;
}
else
{
try
{
var driverProperties = GlobalData.PluginService.GetDriver(channel?.PluginName ?? plugin).DriverProperties;
propertys.Item1 = driverProperties;
var driverPropertyType = driverProperties.GetType();
propertys.Item2 = driverPropertyType.GetRuntimeProperties()
.Where(a => a.GetCustomAttribute<DynamicPropertyAttribute>() != null)
.ToDictionary(a => driverPropertyType.GetPropertyDisplayName(a.Name, a => a.GetCustomAttribute<DynamicPropertyAttribute>(true)?.Description), a => a);
propertysDict.TryAdd(channel?.PluginName ?? plugin, propertys);
return true;
}
});
}
static IEnumerable<Device> FilterPluginDevices(IEnumerable<Device> data, string plugin, Dictionary<long, Channel> channelDicts)
{
return data.Where(device =>
{
if (channelDicts.TryGetValue(device.ChannelId, out var channel))
{
if (channel.PluginName == plugin)
return true;
else
return false;
}
else
{
return true;
}
});
}
}
catch
{
static IEnumerable<Dictionary<string, object>> GetDeviceSheets(
IEnumerable<Device> data,
Dictionary<long, Device>? deviceDicts,
Dictionary<long, Channel> channelDicts,
string? channelName)
{
var type = typeof(Device);
var propertyInfos = type.GetRuntimeProperties()
.Where(a => a.GetCustomAttribute<IgnoreExcelAttribute>(false) == null)
.OrderBy(a =>
{
var order = a.GetCustomAttribute<AutoGenerateColumnAttribute>()?.Order ?? int.MaxValue;
if (order < 0) order += 10000000;
else if (order == 0) order = 10000000;
return order;
});
}
foreach (var device in data)
{
yield return GetDeviceRows(device, propertyInfos, type, deviceDicts, channelDicts, channelName);
}
}
static IEnumerable<Dictionary<string, object>> GetPluginSheets(
IEnumerable<Device> data,
ConcurrentDictionary<string, (object, Dictionary<string, PropertyInfo>)> propertysDict,
string? plugin)
{
foreach (var device in data)
{
var row = GetPluginRows(device, plugin, propertysDict);
if (row != null)
{
yield return row;
}
}
}
static async IAsyncEnumerable<Dictionary<string, object>> GetDeviceSheets(
IAsyncEnumerable<Device> data,
Dictionary<long, Device>? deviceDicts,
Dictionary<long, Channel> channelDicts,
string? channelName)
{
var type = typeof(Device);
var propertyInfos = type.GetRuntimeProperties()
.Where(a => a.GetCustomAttribute<IgnoreExcelAttribute>(false) == null)
.OrderBy(a =>
{
var order = a.GetCustomAttribute<AutoGenerateColumnAttribute>()?.Order ?? int.MaxValue;
if (order < 0) order += 10000000;
else if (order == 0) order = 10000000;
return order;
});
var enumerator = data.GetAsyncEnumerator();
while (await enumerator.MoveNextAsync().ConfigureAwait(false))
{
var device = enumerator.Current;
yield return GetDeviceRows(device, propertyInfos, type, deviceDicts, channelDicts, channelName);
}
}
static async IAsyncEnumerable<Dictionary<string, object>> GetPluginSheets(
IAsyncEnumerable<Device> data,
ConcurrentDictionary<string, (object, Dictionary<string, PropertyInfo>)> propertysDict,
string? plugin)
{
var enumerator = data.GetAsyncEnumerator();
while (await enumerator.MoveNextAsync().ConfigureAwait(false))
{
var device = enumerator.Current;
var row = GetPluginRows(device, plugin, propertysDict);
if (row != null)
{
yield return row;
}
}
}
static Dictionary<string, object> GetDeviceRows(
Device device,
IEnumerable<PropertyInfo>? propertyInfos,
Type type,
Dictionary<long, Device>? deviceDicts,
Dictionary<long, Channel>? channelDicts,
string? channelName)
{
Dictionary<string, object> devExport = new();
deviceDicts.TryGetValue(device.RedundantDeviceId ?? 0, out var redundantDevice);
channelDicts.TryGetValue(device.ChannelId, out var channel);
devExport.Add(ExportString.ChannelName, channel?.Name ?? channelName);
foreach (var item in propertyInfos)
{
//描述
var desc = type.GetPropertyDisplayName(item.Name);
//数据源增加
devExport.Add(desc ?? item.Name, item.GetValue(device)?.ToString());
}
//设备实体没有包含冗余设备名称,手动插入
devExport.Add(ExportString.RedundantDeviceName, redundantDevice?.Name);
return devExport;
}
static Dictionary<string, object> GetPluginRows(Device device, string? plugin, ConcurrentDictionary<string, (object, Dictionary<string, PropertyInfo>)> propertysDict)
{
Dictionary<string, object> driverInfo = new();
var propDict = device.DevicePropertys;
if (!propertysDict.TryGetValue(plugin, out var propertys))
{
try
{
var driverProperties = GlobalData.PluginService.GetDriver(plugin).DriverProperties;
propertys.Item1 = driverProperties;
var driverPropertyType = driverProperties.GetType();
propertys.Item2 = driverPropertyType.GetRuntimeProperties()
.Where(a => a.GetCustomAttribute<DynamicPropertyAttribute>() != null)
.ToDictionary(a => driverPropertyType.GetPropertyDisplayName(a.Name, a => a.GetCustomAttribute<DynamicPropertyAttribute>(true)?.Description), a => a);
propertysDict.TryAdd(plugin, propertys);
}
catch
{
}
if (propertys.Item2 == null)
continue;
}
if (propertys.Item2 != null)
{
if (propertys.Item2.Count > 0)
{
@@ -141,51 +290,13 @@ public static class DeviceServiceHelpers
}
}
var pluginName = PluginServiceUtil.GetFileNameAndTypeName(channel?.PluginName ?? plugin);
if (devicePropertys.ContainsKey(pluginName.TypeName))
{
if (driverInfo.Count > 0)
devicePropertys[pluginName.TypeName].Add(driverInfo);
}
else
{
if (driverInfo.Count > 0)
devicePropertys.Add(pluginName.TypeName, new() { driverInfo });
}
#endregion sheet
if (driverInfo.Count > 0)
return driverInfo;
}
//添加设备页
sheets.Add(ExportString.DeviceName, deviceExports);
//HASH
foreach (var item in devicePropertys)
{
HashSet<string> allKeys = new();
foreach (var dict in item.Value)
{
foreach (var key in dict.Keys)
{
allKeys.Add(key);
}
}
foreach (var dict in item.Value)
{
foreach (var key in allKeys)
{
if (!dict.ContainsKey(key))
{
// 添加缺失的键,并设置默认值
dict.Add(key, null);
}
}
}
sheets.Add(item.Key, item.Value);
}
return sheets;
return null;
}
public static async Task<Dictionary<string, ImportPreviewOutputBase>> ImportAsync(USheetDatas uSheetDatas)

View File

@@ -69,7 +69,7 @@ internal interface IDeviceService
/// <param name="channelName">通道名称(可选)</param>
/// <param name="plugin">插件名称(可选)</param>
/// <returns>导出的内存流</returns>
Task<MemoryStream> ExportMemoryStream(IEnumerable<Device>? data, string channelName = null, string plugin = null);
Task<MemoryStream> ExportMemoryStream(List<Device>? data, string channelName = null, string plugin = null);
/// <summary>
/// 获取所有设备信息。

View File

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

View File

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

View File

@@ -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);
@@ -748,7 +748,7 @@ internal sealed class DeviceThreadManage : IAsyncDisposable, IDeviceThreadManage
var num = Drivers.Count;
foreach (var driver in Drivers.Select(a => a.Value).ToList())
foreach (var driver in Drivers.Select(a => a.Value).Where(a => a != null).ToList())
{
try
{

View File

@@ -42,7 +42,9 @@ internal sealed class GatewayMonitorHostedService : BackgroundService, IGatewayM
//网关启动时,获取所有通道
var channelRuntimes = (await GlobalData.ChannelService.GetAllAsync().ConfigureAwait(false)).AdaptListChannelRuntime();
var deviceRuntimes = (await GlobalData.DeviceService.GetAllAsync().ConfigureAwait(false)).AdaptListDeviceRuntime();
var variableRuntimes = (await GlobalData.VariableService.GetAllAsync().ConfigureAwait(false)).AdaptListVariableRuntime();
var variableRuntimes = GlobalData.VariableService.GetAllVariableRuntime();
foreach (var channelRuntime in channelRuntimes)
{
try

View File

@@ -8,6 +8,8 @@
// QQ群605534569
//------------------------------------------------------------------------------
using ThingsGateway.Authentication;
namespace ThingsGateway.Gateway.Application;
internal static class ManageHelper
@@ -21,26 +23,31 @@ internal static class ManageHelper
public static void CheckChannelCount(int addCount)
{
if (GlobalData.Channels.Count + addCount > ManageHelper.ChannelThreadOptions.MaxChannelCount)
var data = GlobalData.IdChannels.Count + addCount;
ProAuthentication.TryGetAuthorizeInfo(out var authorizeInfo);
if (data > ManageHelper.ChannelThreadOptions.MaxChannelCount || data > authorizeInfo?.MaxChannelCount)
{
throw new Exception($"The number of channels exceeds the limit {ManageHelper.ChannelThreadOptions.MaxChannelCount}");
throw new Exception($"The number of channels exceeds the limit {Math.Min(ManageHelper.ChannelThreadOptions.MaxChannelCount, authorizeInfo?.MaxChannelCount ?? 0)}");
}
}
public static void CheckDeviceCount(int addCount)
{
if (GlobalData.Devices.Count + addCount > ManageHelper.ChannelThreadOptions.MaxDeviceCount)
var data = GlobalData.Devices.Count + addCount;
ProAuthentication.TryGetAuthorizeInfo(out var authorizeInfo);
if (data > ManageHelper.ChannelThreadOptions.MaxDeviceCount || data > authorizeInfo?.MaxDeviceCount)
{
throw new Exception($"The number of devices exceeds the limit {ManageHelper.ChannelThreadOptions.MaxDeviceCount}");
throw new Exception($"The number of devices exceeds the limit {Math.Min(ManageHelper.ChannelThreadOptions.MaxDeviceCount, authorizeInfo?.MaxDeviceCount ?? 0)}");
}
}
public static void CheckVariableCount(int addCount)
{
if (GlobalData.IdVariables.Count + addCount > ManageHelper.ChannelThreadOptions.MaxVariableCount)
var data = GlobalData.IdVariables.Count + addCount;
ProAuthentication.TryGetAuthorizeInfo(out var authorizeInfo);
if (data > ManageHelper.ChannelThreadOptions.MaxVariableCount || data > authorizeInfo?.MaxVariableCount)
{
throw new Exception($"The number of variables exceeds the limit {ManageHelper.ChannelThreadOptions.MaxVariableCount}");
throw new Exception($"The number of variables exceeds the limit {Math.Min(ManageHelper.ChannelThreadOptions.MaxVariableCount, authorizeInfo?.MaxVariableCount ?? 0)}");
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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(); // 释放锁
}
}

View File

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

View File

@@ -30,7 +30,6 @@ namespace ThingsGateway.Gateway.Application
Task<Dictionary<string, ImportPreviewOutputBase>> PreviewAsync(IBrowserFile browserFile);
Task<bool> SaveVariableAsync(Variable input, ItemChangedType type, bool restart, CancellationToken cancellationToken);
void PreheatCache();
Task<MemoryStream> ExportMemoryStream(List<Variable> data, string devName);
}

View File

@@ -58,7 +58,7 @@ internal interface IVariableService
/// </summary>
/// <param name="data">要导出的变量数据。</param>
/// <param name="deviceName">设备名称(可选)。</param>
Task<MemoryStream> ExportMemoryStream(IEnumerable<Variable> data, string deviceName = null);
Task<MemoryStream> ExportMemoryStream(List<Variable> data, string deviceName = null);
/// <summary>
/// 异步导出变量数据到文件流中。
@@ -88,8 +88,6 @@ internal interface IVariableService
/// <param name="exportFilter">查询分页选项</param>
Task<QueryData<Variable>> PageAsync(ExportFilter exportFilter);
Task PreheatCache();
/// <summary>
/// 异步预览导入的数据。
/// </summary>
@@ -113,5 +111,6 @@ internal interface IVariableService
Task<List<Variable>> GetByDeviceIdAsync(List<long> deviceIds);
void DeleteVariableCache();
Task<ImportPreviewOutput<Dictionary<string, Variable>>> SetVariableData(HashSet<long>? dataScope, Dictionary<string, Device> deviceDicts, Dictionary<string, ImportPreviewOutputBase> ImportPreviews, ImportPreviewOutput<Dictionary<string, Variable>> deviceImportPreview, Dictionary<string, PluginInfo> driverPluginNameDict, ConcurrentDictionary<string, (Type, Dictionary<string, PropertyInfo>, Dictionary<string, PropertyInfo>)> propertysDict, string sheetName, IEnumerable<IDictionary<string, object>> rows);
ImportPreviewOutput<Dictionary<string, Variable>> SetVariableData(HashSet<long>? dataScope, Dictionary<string, Device> deviceDicts, Dictionary<string, ImportPreviewOutputBase> ImportPreviews, ImportPreviewOutput<Dictionary<string, Variable>> deviceImportPreview, Dictionary<string, PluginInfo> driverPluginNameDict, ConcurrentDictionary<string, (Type, Dictionary<string, PropertyInfo>, Dictionary<string, PropertyInfo>)> propertysDict, string sheetName, IEnumerable<IDictionary<string, object>> rows);
List<VariableRuntime> GetAllVariableRuntime();
}

View File

@@ -286,8 +286,6 @@ public class VariableRuntimeService : IVariableRuntimeService
}
}
public void PreheatCache() => GlobalData.VariableService.PreheatCache();
public Task<MemoryStream> ExportMemoryStream(List<Variable> data, string deviceName) => GlobalData.VariableService.ExportMemoryStream(data, deviceName);

View File

@@ -21,7 +21,6 @@ using System.Text;
using ThingsGateway.Extension.Generic;
using ThingsGateway.Foundation.Extension.Dynamic;
using ThingsGateway.NewLife;
using ThingsGateway.SqlSugar;
using TouchSocket.Core;
@@ -212,9 +211,19 @@ internal sealed class VariableService : BaseService<Variable>, IVariableService
var result = await db.UseTranAsync(async () =>
{
await db.BulkCopyAsync(newChannels, 100000).ConfigureAwait(false);
await db.BulkCopyAsync(newDevices, 100000).ConfigureAwait(false);
await db.BulkCopyAsync(newVariables, 100000).ConfigureAwait(false);
if (GlobalData.HardwareJob.HardwareInfo.MachineInfo.AvailableMemory > 2 * 1024 * 1024)
{
await db.BulkCopyAsync(newChannels, 200000).ConfigureAwait(false);
await db.BulkCopyAsync(newDevices, 200000).ConfigureAwait(false);
await db.BulkCopyAsync(newVariables, 200000).ConfigureAwait(false);
}
else
{
await db.BulkCopyAsync(newChannels, 10000).ConfigureAwait(false);
await db.BulkCopyAsync(newDevices, 10000).ConfigureAwait(false);
await db.BulkCopyAsync(newVariables, 10000).ConfigureAwait(false);
}
}).ConfigureAwait(false);
if (result.IsSuccess)//如果成功了
{
@@ -372,6 +381,12 @@ internal sealed class VariableService : BaseService<Variable>, IVariableService
/// </summary>
/// <param name="exportFilter">查询条件</param>
public async Task<QueryData<Variable>> PageAsync(ExportFilter exportFilter)
{
var whereQuery = await GetWhereQueryFunc(exportFilter).ConfigureAwait(false);
return await QueryAsync(exportFilter.QueryPageOptions, whereQuery).ConfigureAwait(false);
}
private async Task<Func<ISugarQueryable<Variable>, ISugarQueryable<Variable>>> GetWhereQueryFunc(ExportFilter exportFilter)
{
var dataScope = await GlobalData.SysUserService.GetCurrentUserDataScopeAsync().ConfigureAwait(false);
HashSet<long>? deviceId = null;
@@ -384,7 +399,7 @@ internal sealed class VariableService : BaseService<Variable>, IVariableService
{
deviceId = (await _deviceService.GetAllAsync().ConfigureAwait(false)).Where(a => a.ChannelId == exportFilter.ChannelId).Select(a => a.Id).ToHashSet();
}
return await QueryAsync(exportFilter.QueryPageOptions, a => a
var whereQuery = (ISugarQueryable<Variable> a) => a
.WhereIF(!exportFilter.QueryPageOptions.SearchText.IsNullOrWhiteSpace(), a => a.Name.Contains(exportFilter.QueryPageOptions.SearchText!))
.WhereIF(exportFilter.PluginType == PluginTypeEnum.Collect, a => a.DeviceId == exportFilter.DeviceId)
.WhereIF(deviceId != null, a => deviceId.Contains(a.DeviceId))
@@ -393,9 +408,34 @@ internal sealed class VariableService : BaseService<Variable>, IVariableService
.WhereIF(dataScope?.Count == 0, u => u.CreateUserId == UserManager.UserId)
.WhereIF(exportFilter.PluginType == PluginTypeEnum.Business, u => SqlFunc.JsonLike(u.VariablePropertys, exportFilter.DeviceId.ToString()))
.WhereIF(exportFilter.PluginType == PluginTypeEnum.Business, u => SqlFunc.JsonLike(u.VariablePropertys, exportFilter.DeviceId.ToString()));
return whereQuery;
}
).ConfigureAwait(false);
private async Task<Func<IEnumerable<Variable>, IEnumerable<Variable>>> GetWhereEnumerableFunc(ExportFilter exportFilter)
{
var dataScope = await GlobalData.SysUserService.GetCurrentUserDataScopeAsync().ConfigureAwait(false);
HashSet<long>? deviceId = null;
if (!exportFilter.PluginName.IsNullOrWhiteSpace())
{
var channel = (await _channelService.GetAllAsync().ConfigureAwait(false)).Where(a => a.PluginName == exportFilter.PluginName).Select(a => a.Id).ToHashSet();
deviceId = (await _deviceService.GetAllAsync().ConfigureAwait(false)).Where(a => channel.Contains(a.ChannelId)).Select(a => a.Id).ToHashSet();
}
else if (exportFilter.ChannelId != null)
{
deviceId = (await _deviceService.GetAllAsync().ConfigureAwait(false)).Where(a => a.ChannelId == exportFilter.ChannelId).Select(a => a.Id).ToHashSet();
}
var whereQuery = (IEnumerable<Variable> a) => a
.WhereIF(!exportFilter.QueryPageOptions.SearchText.IsNullOrWhiteSpace(), a => a.Name.Contains(exportFilter.QueryPageOptions.SearchText!))
.WhereIF(exportFilter.PluginType == PluginTypeEnum.Collect, a => a.DeviceId == exportFilter.DeviceId)
.WhereIF(deviceId != null, a => deviceId.Contains(a.DeviceId))
.WhereIF(dataScope != null && dataScope?.Count > 0, u => dataScope.Contains(u.CreateOrgId))//在指定机构列表查询
.WhereIF(dataScope?.Count == 0, u => u.CreateUserId == UserManager.UserId)
.WhereIF(exportFilter.PluginType == PluginTypeEnum.Business, u => SqlFunc.JsonLike(u.VariablePropertys, exportFilter.DeviceId.ToString()));
return whereQuery;
}
/// <summary>
@@ -424,15 +464,36 @@ internal sealed class VariableService : BaseService<Variable>, IVariableService
App.CacheService.Remove(ThingsGatewayCacheConst.Cache_Variable);
}
public List<VariableRuntime> GetAllVariableRuntime()
{
using (var db = DbContext.GetDB<Variable>())
{
var deviceVariables = db.Queryable<Variable>().OrderBy(a => a.Id).GetEnumerable();
return deviceVariables.AdaptListVariableRuntime();
}
}
#region
/// <summary>
/// 导出文件
/// </summary>
[OperDesc("ExportVariable", isRecordPar: false, localizerType: typeof(Variable))]
public async Task<MemoryStream> ExportMemoryStream(IEnumerable<Variable> data, string deviceName = null)
public async Task<MemoryStream> ExportMemoryStream(List<Variable> variables, string deviceName = null)
{
var sheets = await VariableServiceHelpers.ExportCoreAsync(data, deviceName).ConfigureAwait(false);
var deviceDicts = (await GlobalData.DeviceService.GetAllAsync().ConfigureAwait(false)).ToDictionary(a => a.Id);
var channelDicts = (await GlobalData.ChannelService.GetAllAsync().ConfigureAwait(false)).ToDictionary(a => a.Id);
var pluginSheetNames = variables.Where(a => a.VariablePropertys?.Count > 0).SelectMany(a => a.VariablePropertys).Select(a =>
{
if (deviceDicts.TryGetValue(a.Key, out var device) && channelDicts.TryGetValue(device.ChannelId, out var channel))
{
var pluginKey = channel?.PluginName;
using var businessBase = (BusinessBase)GlobalData.PluginService.GetDriver(pluginKey);
return new KeyValuePair<string, VariablePropertyBase>(pluginKey, businessBase.VariablePropertys);
}
return new KeyValuePair<string, VariablePropertyBase>(string.Empty, null);
}).Where(a => a.Value != null).DistinctBy(a => a.Key).ToDictionary();
var sheets = VariableServiceHelpers.ExportSheets(variables, deviceDicts, channelDicts, pluginSheetNames); // IEnumerable 延迟执行
var memoryStream = new MemoryStream();
await memoryStream.SaveAsAsync(sheets).ConfigureAwait(false);
@@ -446,11 +507,52 @@ internal sealed class VariableService : BaseService<Variable>, IVariableService
[OperDesc("ExportVariable", isRecordPar: false, localizerType: typeof(Variable))]
public async Task<Dictionary<string, object>> ExportVariableAsync(ExportFilter exportFilter)
{
var data = (await PageAsync(exportFilter).ConfigureAwait(false));
var sheets = await VariableServiceHelpers.ExportCoreAsync(data.Items, sortName: exportFilter.QueryPageOptions.SortName, sortOrder: exportFilter.QueryPageOptions.SortOrder).ConfigureAwait(false);
return sheets;
}
if (GlobalData.HardwareJob.HardwareInfo.MachineInfo.AvailableMemory < 4 * 1024 * 1024)
{
var whereQuery = await GetWhereEnumerableFunc(exportFilter).ConfigureAwait(false);
//导出
var variables = GlobalData.IdVariables.Select(a => a.Value).GetQuery(exportFilter.QueryPageOptions, whereQuery, exportFilter.FilterKeyValueAction);
var deviceDicts = (await GlobalData.DeviceService.GetAllAsync().ConfigureAwait(false)).ToDictionary(a => a.Id);
var channelDicts = (await GlobalData.ChannelService.GetAllAsync().ConfigureAwait(false)).ToDictionary(a => a.Id);
var pluginSheetNames = variables.Where(a => a.VariablePropertys?.Count > 0).SelectMany(a => a.VariablePropertys).Select(a =>
{
if (deviceDicts.TryGetValue(a.Key, out var device) && channelDicts.TryGetValue(device.ChannelId, out var channel))
{
var pluginKey = channel?.PluginName;
using var businessBase = (BusinessBase)GlobalData.PluginService.GetDriver(pluginKey);
return new KeyValuePair<string, VariablePropertyBase>(pluginKey, businessBase.VariablePropertys);
}
return new KeyValuePair<string, VariablePropertyBase>(string.Empty, null);
}).Where(a => a.Value != null).DistinctBy(a => a.Key).ToDictionary();
var sheets = VariableServiceHelpers.ExportSheets(variables, deviceDicts, channelDicts, pluginSheetNames); // IEnumerable 延迟执行
return sheets;
}
else
{
var data = (await PageAsync(exportFilter).ConfigureAwait(false));
var sheets = await VariableServiceHelpers.ExportCoreAsync(data.Items, sortName: exportFilter.QueryPageOptions.SortName, sortOrder: exportFilter.QueryPageOptions.SortOrder).ConfigureAwait(false);
return sheets;
}
}
private async Task<IAsyncEnumerable<Variable>> GetAsyncEnumerableData(ExportFilter exportFilter)
{
var whereQuery = await GetEnumerableData(exportFilter).ConfigureAwait(false);
return whereQuery.GetAsyncEnumerable();
}
private async Task<ISugarQueryable<Variable>> GetEnumerableData(ExportFilter exportFilter)
{
var db = GetDB();
var whereQuery = await GetWhereQueryFunc(exportFilter).ConfigureAwait(false);
return GetQuery(db, exportFilter.QueryPageOptions, whereQuery, exportFilter.FilterKeyValueAction);
}
#endregion
@@ -475,53 +577,21 @@ internal sealed class VariableService : BaseService<Variable>, IVariableService
var insertData = variables.Where(a => !a.IsUp).ToList();
ManageHelper.CheckVariableCount(insertData.Count);
using var db = GetDB();
await db.BulkCopyAsync(insertData, 100000).ConfigureAwait(false);
await db.BulkUpdateAsync(upData, 100000).ConfigureAwait(false);
if (GlobalData.HardwareJob.HardwareInfo.MachineInfo.AvailableMemory > 2 * 1024 * 1024)
{
await db.BulkCopyAsync(insertData, 200000).ConfigureAwait(false);
await db.BulkUpdateAsync(upData, 200000).ConfigureAwait(false);
}
else
{
await db.BulkCopyAsync(insertData, 10000).ConfigureAwait(false);
await db.BulkUpdateAsync(upData, 10000).ConfigureAwait(false);
}
DeleteVariableCache();
return variables.Select(a => a.Id).ToHashSet();
}
private static readonly WaitLock _cacheLock = new();
private async Task<Dictionary<long, Dictionary<string, DeviecIdVariableImportData>>> GetVariableImportData()
{
var key = ThingsGatewayCacheConst.Cache_Variable;
var datas = App.CacheService.Get<Dictionary<long, Dictionary<string, DeviecIdVariableImportData>>>(key);
if (datas == null)
{
try
{
await _cacheLock.WaitAsync().ConfigureAwait(false);
datas = App.CacheService.Get<Dictionary<long, Dictionary<string, DeviecIdVariableImportData>>>(key);
if (datas == null)
{
using var db = GetDB();
datas = (await db.Queryable<Variable>().Select<DeviecIdVariableImportData>().ToListAsync().ConfigureAwait(false)).GroupBy(a => a.DeviceId).ToDictionary(a => a.Key, a => a.ToDictionary(a => a.Name));
App.CacheService.Set(key, datas);
}
}
finally
{
_cacheLock.Release();
}
}
return datas;
}
public Task PreheatCache()
{
return GetVariableImportData();
}
private sealed class DeviecIdVariableImportData
{
public long Id { get; set; }
public string Name { get; set; }
public long DeviceId { get; set; }
public long CreateOrgId { get; set; }
public long CreateUserId { get; set; }
}
public async Task<Dictionary<string, ImportPreviewOutputBase>> PreviewAsync(IBrowserFile browserFile)
{
@@ -552,7 +622,7 @@ internal sealed class VariableService : BaseService<Variable>, IVariableService
// 获取当前工作表的所有行数据
var rows = MiniExcel.Query(path, useHeaderRow: true, sheetName: sheetName).Cast<IDictionary<string, object>>();
deviceImportPreview = await SetVariableData(dataScope, deviceDicts, ImportPreviews, deviceImportPreview, driverPluginNameDict, propertysDict, sheetName, rows).ConfigureAwait(false);
deviceImportPreview = SetVariableData(dataScope, deviceDicts, ImportPreviews, deviceImportPreview, driverPluginNameDict, propertysDict, sheetName, rows);
}
return ImportPreviews;
@@ -564,7 +634,7 @@ internal sealed class VariableService : BaseService<Variable>, IVariableService
}
}
public async Task<ImportPreviewOutput<Dictionary<string, Variable>>> SetVariableData(HashSet<long>? dataScope, Dictionary<string, Device> deviceDicts, Dictionary<string, ImportPreviewOutputBase> ImportPreviews, ImportPreviewOutput<Dictionary<string, Variable>> deviceImportPreview, Dictionary<string, PluginInfo> driverPluginNameDict, ConcurrentDictionary<string, (Type, Dictionary<string, PropertyInfo>, Dictionary<string, PropertyInfo>)> propertysDict, string sheetName, IEnumerable<IDictionary<string, object>> rows)
public ImportPreviewOutput<Dictionary<string, Variable>> SetVariableData(HashSet<long>? dataScope, Dictionary<string, Device> deviceDicts, Dictionary<string, ImportPreviewOutputBase> ImportPreviews, ImportPreviewOutput<Dictionary<string, Variable>> deviceImportPreview, Dictionary<string, PluginInfo> driverPluginNameDict, ConcurrentDictionary<string, (Type, Dictionary<string, PropertyInfo>, Dictionary<string, PropertyInfo>)> propertysDict, string sheetName, IEnumerable<IDictionary<string, object>> rows)
{
// 变量页处理
if (sheetName == ExportString.VariableName)
@@ -581,8 +651,6 @@ internal sealed class VariableService : BaseService<Variable>, IVariableService
var variableProperties = type.GetRuntimeProperties().Where(a => (a.GetCustomAttribute<IgnoreExcelAttribute>() == null) && a.CanWrite)
.ToDictionary(a => type.GetPropertyDisplayName(a.Name), a => (a, a.IsNullableType()));
var dbVariableDicts = await GetVariableImportData().ConfigureAwait(false);
// 并行处理每一行数据
rows.ParallelForEachStreamed((item, state, index) =>
{
@@ -629,7 +697,7 @@ internal sealed class VariableService : BaseService<Variable>, IVariableService
return;
}
if (dbVariableDicts.TryGetValue(variable.DeviceId, out var dbvar1s) && dbvar1s.TryGetValue(variable.Name, out var dbvar1))
if (GlobalData.IdDevices.TryGetValue(variable.DeviceId, out var dbvar1s) && dbvar1s.VariableRuntimes.TryGetValue(variable.Name, out var dbvar1))
{
variable.Id = dbvar1.Id;
variable.CreateOrgId = dbvar1.CreateOrgId;

View File

@@ -21,12 +21,299 @@ namespace ThingsGateway.Gateway.Application;
public static class VariableServiceHelpers
{
public static async Task<USheetDatas> ExportVariableAsync(IEnumerable<Variable> models, string sortName = nameof(Variable.Id), SortOrder sortOrder = SortOrder.Asc)
public static async Task<USheetDatas> ExportVariableAsync(IEnumerable<Variable> variables, string sortName = nameof(Variable.Id), SortOrder sortOrder = SortOrder.Asc)
{
var data = await ExportCoreAsync(models, sortName: sortName, sortOrder: sortOrder).ConfigureAwait(false);
var deviceDicts = (await GlobalData.DeviceService.GetAllAsync().ConfigureAwait(false)).ToDictionary(a => a.Id);
var channelDicts = (await GlobalData.ChannelService.GetAllAsync().ConfigureAwait(false)).ToDictionary(a => a.Id);
var pluginSheetNames = variables.Where(a => a.VariablePropertys?.Count > 0).SelectMany(a => a.VariablePropertys).Select(a =>
{
if (deviceDicts.TryGetValue(a.Key, out var device) && channelDicts.TryGetValue(device.ChannelId, out var channel))
{
var pluginKey = channel?.PluginName;
using var businessBase = (BusinessBase)GlobalData.PluginService.GetDriver(pluginKey);
return new KeyValuePair<string, VariablePropertyBase>(pluginKey, businessBase.VariablePropertys);
}
return new KeyValuePair<string, VariablePropertyBase>(string.Empty, null);
}).Where(a => a.Value != null).DistinctBy(a => a.Key).ToDictionary();
var data = ExportSheets(variables, deviceDicts, channelDicts, pluginSheetNames); // IEnumerable 延迟执行
return USheetDataHelpers.GetUSheetDatas(data);
}
static IAsyncEnumerable<Variable> FilterPluginDevices(
IAsyncEnumerable<Variable> data,
string pluginName,
Dictionary<long, Device> deviceDicts,
Dictionary<long, Channel> channelDicts)
{
return data.Where(variable =>
{
if (variable.VariablePropertys == null)
return false;
foreach (var a in variable.VariablePropertys)
{
if (deviceDicts.TryGetValue(a.Key, out var device) && channelDicts.TryGetValue(device.ChannelId, out var channel))
{
if (channel.PluginName == pluginName)
return true;
}
}
return false;
});
}
static IEnumerable<Variable> FilterPluginDevices(
IEnumerable<Variable> data,
string pluginName,
Dictionary<long, Device> deviceDicts,
Dictionary<long, Channel> channelDicts)
{
return data.Where(variable =>
{
if (variable.VariablePropertys == null)
return false;
foreach (var a in variable.VariablePropertys)
{
if (deviceDicts.TryGetValue(a.Key, out var device) && channelDicts.TryGetValue(device.ChannelId, out var channel))
{
if (channel.PluginName == pluginName)
return true;
}
}
return false;
});
}
public static Dictionary<string, object> ExportSheets(
IEnumerable<Variable> data,
Dictionary<long, Device> deviceDicts,
Dictionary<long, Channel> channelDicts,
Dictionary<string, VariablePropertyBase> pluginDrivers,
string? deviceName = null)
{
var sheets = new Dictionary<string, object>();
var propertysDict = new ConcurrentDictionary<string, (VariablePropertyBase, Dictionary<string, PropertyInfo>)>();
// 主变量页
sheets.Add(ExportString.VariableName, GetVariableSheets(data, deviceDicts, deviceName));
// 插件页(动态推导)
foreach (var plugin in pluginDrivers.Keys.Distinct())
{
var filtered = FilterPluginDevices(data, plugin, deviceDicts, channelDicts);
var pluginName = PluginServiceUtil.GetFileNameAndTypeName(plugin).Item2;
sheets.Add(pluginName, GetPluginSheets(filtered, deviceDicts, channelDicts, plugin, pluginDrivers, propertysDict));
}
return sheets;
}
static IEnumerable<Dictionary<string, object>> GetVariableSheets(
IEnumerable<Variable> data,
Dictionary<long, Device> deviceDicts,
string? deviceName)
{
var type = typeof(Variable);
var propertyInfos = type.GetRuntimeProperties()
.Where(a => a.GetCustomAttribute<IgnoreExcelAttribute>(false) == null)
.OrderBy(a =>
{
var order = a.GetCustomAttribute<AutoGenerateColumnAttribute>()?.Order ?? int.MaxValue;
if (order < 0) order += 10000000;
else if (order == 0) order = 10000000;
return order;
});
foreach (var variable in data)
{
yield return GetVariable(deviceDicts, deviceName, type, propertyInfos, variable);
}
}
private static Dictionary<string, object> GetVariable(Dictionary<long, Device> deviceDicts, string? deviceName, Type type, IOrderedEnumerable<PropertyInfo> propertyInfos, Variable variable)
{
var row = new Dictionary<string, object>();
deviceDicts.TryGetValue(variable.DeviceId, out var device);
row.TryAdd(ExportString.DeviceName, device?.Name ?? deviceName);
foreach (var item in propertyInfos)
{
if (item.Name == nameof(Variable.Id))
{
continue;
}
var desc = type.GetPropertyDisplayName(item.Name);
row.TryAdd(desc ?? item.Name, item.GetValue(variable)?.ToString());
}
return row;
}
static IEnumerable<Dictionary<string, object>> GetPluginSheets(
IEnumerable<Variable> data,
Dictionary<long, Device> deviceDicts,
Dictionary<long, Channel> channelDicts,
string plugin,
Dictionary<string, VariablePropertyBase> pluginDrivers,
ConcurrentDictionary<string, (VariablePropertyBase, Dictionary<string, PropertyInfo>)> propertysDict)
{
if (!pluginDrivers.TryGetValue(plugin, out var variablePropertyBase))
yield break;
if (!propertysDict.TryGetValue(plugin, out var propertys))
{
var driverProperties = variablePropertyBase;
var driverPropertyType = driverProperties.GetType();
propertys.Item1 = driverProperties;
propertys.Item2 = driverPropertyType.GetRuntimeProperties()
.Where(a => a.GetCustomAttribute<DynamicPropertyAttribute>() != null)
.ToDictionary(
a => driverPropertyType.GetPropertyDisplayName(a.Name, a => a.GetCustomAttribute<DynamicPropertyAttribute>(true)?.Description));
propertysDict.TryAdd(plugin, propertys);
}
if (propertys.Item2?.Count == null || propertys.Item2?.Count == 0)
{
yield break;
}
foreach (var variable in data)
{
if (variable.VariablePropertys == null)
continue;
foreach (var item in variable.VariablePropertys)
{
if (!(deviceDicts.TryGetValue(item.Key, out var businessDevice) &&
deviceDicts.TryGetValue(variable.DeviceId, out var collectDevice)))
continue;
channelDicts.TryGetValue(businessDevice.ChannelId, out var channel);
if (channel?.PluginName != plugin)
continue;
yield return GetPlugin(propertys, variable, item, businessDevice, collectDevice);
}
}
}
private static Dictionary<string, object> GetPlugin((VariablePropertyBase, Dictionary<string, PropertyInfo>) propertys, Variable variable, KeyValuePair<long, Dictionary<string, string>> item, Device businessDevice, Device collectDevice)
{
var row = new Dictionary<string, object>
{
{ ExportString.DeviceName, collectDevice.Name },
{ ExportString.BusinessDeviceName, businessDevice.Name },
{ ExportString.VariableName, variable.Name }
};
foreach (var kv in propertys.Item2)
{
var propDict = item.Value;
if (propDict.TryGetValue(kv.Value.Name, out var dependencyProperty))
{
row.TryAdd(kv.Key, dependencyProperty);
}
else
{
row.TryAdd(kv.Key, ThingsGatewayStringConverter.Default.Serialize(null, kv.Value.GetValue(propertys.Item1)));
}
}
return row;
}
public static Dictionary<string, object> ExportSheets(
IAsyncEnumerable<Variable> data,
Dictionary<long, Device> deviceDicts,
Dictionary<long, Channel> channelDicts,
Dictionary<string, VariablePropertyBase> pluginDrivers,
string? deviceName = null)
{
var sheets = new Dictionary<string, object>();
var propertysDict = new ConcurrentDictionary<string, (VariablePropertyBase, Dictionary<string, PropertyInfo>)>();
// 主变量页
sheets.Add(ExportString.VariableName, GetVariableSheets(data, deviceDicts, deviceName));
// 插件页(动态推导)
foreach (var plugin in pluginDrivers.Keys.Distinct())
{
var filtered = FilterPluginDevices(data, plugin, deviceDicts, channelDicts);
var pluginName = PluginServiceUtil.GetFileNameAndTypeName(plugin).Item2;
sheets.Add(pluginName, GetPluginSheets(filtered, deviceDicts, channelDicts, plugin, pluginDrivers, propertysDict));
}
return sheets;
}
static async IAsyncEnumerable<Dictionary<string, object>> GetVariableSheets(
IAsyncEnumerable<Variable> data,
Dictionary<long, Device> deviceDicts,
string? deviceName)
{
var type = typeof(Variable);
var propertyInfos = type.GetRuntimeProperties()
.Where(a => a.GetCustomAttribute<IgnoreExcelAttribute>(false) == null)
.OrderBy(a =>
{
var order = a.GetCustomAttribute<AutoGenerateColumnAttribute>()?.Order ?? int.MaxValue;
if (order < 0) order += 10000000;
else if (order == 0) order = 10000000;
return order;
});
await foreach (var variable in data.ConfigureAwait(false))
{
yield return GetVariable(deviceDicts, deviceName, type, propertyInfos, variable);
}
}
static async IAsyncEnumerable<Dictionary<string, object>> GetPluginSheets(
IAsyncEnumerable<Variable> data,
Dictionary<long, Device> deviceDicts,
Dictionary<long, Channel> channelDicts,
string plugin,
Dictionary<string, VariablePropertyBase> pluginDrivers,
ConcurrentDictionary<string, (VariablePropertyBase, Dictionary<string, PropertyInfo>)> propertysDict)
{
if (!pluginDrivers.TryGetValue(plugin, out var variablePropertyBase))
yield break;
if (!propertysDict.TryGetValue(plugin, out var propertys))
{
var driverProperties = variablePropertyBase;
var driverPropertyType = driverProperties.GetType();
propertys.Item1 = driverProperties;
propertys.Item2 = driverPropertyType.GetRuntimeProperties()
.Where(a => a.GetCustomAttribute<DynamicPropertyAttribute>() != null)
.ToDictionary(
a => driverPropertyType.GetPropertyDisplayName(a.Name, a => a.GetCustomAttribute<DynamicPropertyAttribute>(true)?.Description));
propertysDict.TryAdd(plugin, propertys);
}
if (propertys.Item2?.Count == null || propertys.Item2?.Count == 0)
{
yield break;
}
await foreach (var variable in data.ConfigureAwait(false))
{
if (variable.VariablePropertys == null)
continue;
foreach (var item in variable.VariablePropertys)
{
if (!(deviceDicts.TryGetValue(item.Key, out var businessDevice) &&
deviceDicts.TryGetValue(variable.DeviceId, out var collectDevice)))
continue;
channelDicts.TryGetValue(businessDevice.ChannelId, out var channel);
if (channel?.PluginName != plugin)
continue;
yield return GetPlugin(propertys, variable, item, businessDevice, collectDevice);
}
}
}
public static async Task<Dictionary<string, object>> ExportCoreAsync(IEnumerable<Variable> data, string deviceName = null, string sortName = nameof(Variable.Id), SortOrder sortOrder = SortOrder.Asc)
{
@@ -67,7 +354,6 @@ public static class VariableServiceHelpers
;
#endregion
var varName = nameof(Variable.Name);
data.ParallelForEachStreamed((variable, state, index) =>
{
Dictionary<string, object> varExport = new();
@@ -83,10 +369,6 @@ public static class VariableServiceHelpers
}
//描述
var desc = type.GetPropertyDisplayName(item.Name);
if (item.Name == varName)
{
varName = desc;
}
//数据源增加
varExport.TryAdd(desc ?? item.Name, item.GetValue(variable)?.ToString());
}
@@ -128,7 +410,8 @@ public static class VariableServiceHelpers
var variablePropertyType = variableProperty.GetType();
propertys.Item2 = variablePropertyType.GetRuntimeProperties()
.Where(a => a.GetCustomAttribute<DynamicPropertyAttribute>() != null)
.ToDictionary(a => variablePropertyType.GetPropertyDisplayName(a.Name, a => a.GetCustomAttribute<DynamicPropertyAttribute>(true)?.Description));
.ToDictionary(a => variablePropertyType.GetPropertyDisplayName(a.Name, a =>
a.GetCustomAttribute<DynamicPropertyAttribute>(true)?.Description));
propertysDict.TryAdd(channel.PluginName, propertys);
}
@@ -263,7 +546,7 @@ public static class VariableServiceHelpers
rows.Add(expando);
}
deviceImportPreview = await GlobalData.VariableService.SetVariableData(dataScope, deviceDicts, ImportPreviews, deviceImportPreview, driverPluginNameDict, propertysDict, sheetName, rows).ConfigureAwait(false);
deviceImportPreview = GlobalData.VariableService.SetVariableData(dataScope, deviceDicts, ImportPreviews, deviceImportPreview, driverPluginNameDict, propertysDict, sheetName, rows);
if (ImportPreviews.Any(a => a.Value.HasError))
{
throw new(ImportPreviews.FirstOrDefault(a => a.Value.HasError).Value.Results.FirstOrDefault(a => !a.Success).ErrorMessage ?? "error");

View File

@@ -9,6 +9,7 @@
<PackageReference Include="Portable.BouncyCastle" Version="1.9.0" />
<PackageReference Include="Riok.Mapperly" Version="4.2.1" ExcludeAssets="runtime" PrivateAssets="all" />
<PackageReference Include="Rougamo.Fody" Version="5.0.1" />
<PackageReference Include="System.Linq.Async" Version="6.0.3" />
<PackageReference Include="TouchSocket.Dmtp" Version="3.1.11" />
<PackageReference Include="TouchSocket.WebApi.Swagger" Version="3.1.11" />
<PackageReference Include="ThingsGateway.Authentication" Version="$(AuthenticationVersion)" />

View File

@@ -26,7 +26,10 @@
"RegisterStatus": "RegisterStatus",
"Unauthorized": "Unauthorized",
"Unregister": "Unregister",
"UUID": "UUID"
"UUID": "UUID",
"MaxChannelCount": "MaxChannelCount",
"MaxDeviceCount": "MaxDeviceCount",
"MaxVariableCount": "MaxVariableCount"
},
"ThingsGateway.Gateway.Razor._Imports": {
@@ -104,7 +107,7 @@
"DeleteCurrentDevice": "Delete Current Device",
"DeleteDevice": "Delete Device",
"DeviceInformation": "Device Information",
"DeviceList": "Device List",
"DeviceList": "Tip: Right click to operate tree nodes",
"DeviceRedundantThread": "Switching redundancy",
"ExcelChannel": "ExcelChannel",
"ExcelDevice": "ExcelDevice",

View File

@@ -24,7 +24,10 @@
"RegisterStatus": "注册状态",
"Unauthorized": "未授权",
"Unregister": "取消注册",
"UUID": "唯一编码"
"UUID": "唯一编码",
"MaxChannelCount": "最大通道数量",
"MaxDeviceCount": "最大设备数量",
"MaxVariableCount": "最大变量数量"
},
"ThingsGateway.Gateway.Razor._Imports": {
@@ -102,7 +105,7 @@
"DeleteCurrentDevice": "删除当前节点下的设备",
"DeleteDevice": "删除设备",
"DeviceInformation": "设备信息",
"DeviceList": "设备节点",
"DeviceList": "提示:右键操作树节点",
"DeviceRedundantThread": "切换冗余",
"ExcelChannel": "在线excel编辑通道",
"ExcelDevice": "在线excel编辑设备",

View File

@@ -20,3 +20,4 @@ public static partial class GatewayMapper
}

View File

@@ -0,0 +1,70 @@
// <auto-generated />
#nullable enable
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)
{
NormalizeItems(src);
return src.ToList();
}
static void NormalizeItems<T>(List<TreeViewItem<T>> nodes)
{
foreach (var node in nodes)
{
if (node.Items.Count > 0)
{
node.Items = node.Items.ToList();
NormalizeItems(node.Items);
}
}
}
//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;
//}
}
}

View File

@@ -39,48 +39,55 @@
<TableColumn @bind-Field="@context.LogLevel" Filterable=true Sortable=true Visible="false" />
<TableColumn @bind-Field="@context.ChannelType" Filterable=true Sortable=true Visible="false" />
<TableColumn Field="@context.CacheTimeout" FieldExpression=@(()=>context.CacheTimeout) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.CheckClearTime" FieldExpression=@(()=>context.CheckClearTime) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.ConnectTimeout" FieldExpression=@(()=>context.ConnectTimeout) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.Heartbeat" FieldExpression=@(()=>context.Heartbeat) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.HeartbeatTime" FieldExpression=@(()=>context.HeartbeatTime) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.DtuSeviceType" FieldExpression=@(()=>context.DtuSeviceType) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.DtuId" FieldExpression=@(()=>context.DtuId) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.CacheTimeout" FieldExpression=@(() => context.CacheTimeout) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.CheckClearTime" FieldExpression=@(() => context.CheckClearTime) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.ConnectTimeout" FieldExpression=@(() => context.ConnectTimeout) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.Heartbeat" FieldExpression=@(() => context.Heartbeat) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.HeartbeatTime" FieldExpression=@(() => context.HeartbeatTime) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.DtuSeviceType" FieldExpression=@(() => context.DtuSeviceType) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.DtuId" FieldExpression=@(() => context.DtuId) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.PluginType" FieldExpression=@(()=>context.PluginType) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.PortName" FieldExpression=@(()=>context.PortName) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.RemoteUrl" FieldExpression=@(()=>context.RemoteUrl) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.BindUrl" FieldExpression=@(()=>context.BindUrl) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.MaxClientCount" FieldExpression=@(()=>context.MaxClientCount) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.MaxConcurrentCount" FieldExpression=@(()=>context.MaxConcurrentCount) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.PluginType" FieldExpression=@(() => context.PluginType) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.PortName" FieldExpression=@(() => context.PortName) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.RemoteUrl" FieldExpression=@(() => context.RemoteUrl) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.BindUrl" FieldExpression=@(() => context.BindUrl) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.MaxClientCount" FieldExpression=@(() => context.MaxClientCount) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.MaxConcurrentCount" FieldExpression=@(() => context.MaxConcurrentCount) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn @bind-Field="@context.Id" Filterable=true Sortable=true Visible="false" DefaultSort=true DefaultSortOrder="SortOrder.Asc" />
</TableColumns>
<EditTemplate Context="context">
<ChannelEditComponent Model=@(context) ValidateEnable=false BatchEditEnable=false PluginType="ChannelDeviceHelpers.GetPluginType( SelectModel)"></ChannelEditComponent>
<ChannelEditComponent Model=@(context) ValidateEnable=false BatchEditEnable=false PluginType="ChannelDeviceHelpers.GetPluginType(SelectModel)"></ChannelEditComponent>
</EditTemplate>
<ExportButtonDropdownTemplate Context="ExportContext">
<Button class="dropdown-item" OnClick="() => ExcelExportAsync(ExportContext,true)" IsDisabled=@(!AuthorizeButton("导出"))>
<i class="fas fa-file-export"></i>
<span>@RazorLocalizer["ExportAll"]</span>
</Button>
<Button class="dropdown-item" OnClick="() => ExcelExportAsync(ExportContext)" IsDisabled=@(!AuthorizeButton("导出"))>
<i class="fas fa-file-export"></i>
<span>@RazorLocalizer["TablesExportButtonExcelText"]</span>
</Button>
<Button class="dropdown-item" OnClick="() => ExcelImportAsync(ExportContext)" IsDisabled=@(!AuthorizeButton("导入"))>
<i class="fas fa-file-import"></i>
<span>@RazorLocalizer["TablesImportButtonExcelText"]</span>
</Button>
<Button class="dropdown-item" OnClick="() => ExcelChannelAsync(ExportContext)" IsDisabled=@(!AuthorizeButton("导入"))>
<i class="fas fa-file-import"></i>
<span>@GatewayLocalizer["ExcelChannel"]</span>
</Button>
@if ((AuthorizeButton("导出")))
{
<Button class="dropdown-item" OnClick="() => ExcelExportAsync(ExportContext, true)" IsKeepDisabled=@(!AuthorizeButton("导出"))>
<i class="fas fa-file-export"></i>
<span>@RazorLocalizer["ExportAll"]</span>
</Button>
<Button class="dropdown-item" OnClick="() => ExcelExportAsync(ExportContext)" IsKeepDisabled=@(!AuthorizeButton("导出"))>
<i class="fas fa-file-export"></i>
<span>@RazorLocalizer["TablesExportButtonExcelText"]</span>
</Button>
}
@if ((AuthorizeButton("导入")))
{
<Button class="dropdown-item" OnClick="() => ExcelImportAsync(ExportContext)" IsKeepDisabled=@(!AuthorizeButton("导入"))>
<i class="fas fa-file-import"></i>
<span>@RazorLocalizer["TablesImportButtonExcelText"]</span>
</Button>
<Button class="dropdown-item" OnClick="() => ExcelChannelAsync(ExportContext)" IsKeepDisabled=@(!AuthorizeButton("导入"))>
<i class="fas fa-file-import"></i>
<span>@GatewayLocalizer["ExcelChannel"]</span>
</Button>
}
</ExportButtonDropdownTemplate>
<TableToolbarTemplate>

View File

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

View File

@@ -8,33 +8,89 @@
<div class="listtree-view">
<div class="d-flex align-items-center">
<RadioList class="m-2" IsButton="true" AutoSelectFirstWhenValueIsNull="false" TValue="ShowTypeEnum?" ValueExpression=@(()=> ShowType ) Value="ShowType" ValueChanged="OnShowTypeChanged" ShowLabel="false" />
<RadioList class="m-2" IsButton="true" AutoSelectFirstWhenValueIsNull="false" TValue="ShowTypeEnum?" ValueExpression=@(() => ShowType) Value="ShowType" ValueChanged="OnShowTypeChanged" ShowLabel="false" />
<SpinnerComponent @ref=Spinner></SpinnerComponent>
</div>
<span style="color:var(--bs-body-color)" class="text-h6 mb-2">@GatewayLocalizer["DeviceList"]</span>
<span style="color:var(--bs-body-color);opacity: 0.5;" class="text-h6 mb-2">@GatewayLocalizer["DeviceList"]</span>
<ContextMenuZone title="Right click operation">
<TreeView TItem="ChannelDeviceTreeItem" Items="Items" ShowIcon="false" ShowSearch IsAccordion=false IsVirtualize="true" OnTreeItemClick="OnTreeItemClick" OnSearchAsync="OnClickSearch" ModelEqualityComparer=ModelEqualityComparer>
<ContextMenuZone>
<TreeView TItem="ChannelDeviceTreeItem" Items="Items" ShowIcon="false" ShowSearch IsAccordion=false IsVirtualize="true" OnTreeItemClick="OnTreeItemClick" OnSearchAsync="OnClickSearch" ModelEqualityComparer=ModelEqualityComparer ShowToolbar="true">
<ToolbarTemplate>
<div class="tree-node-toolbar-edit" @onclick:preventDefault @onclick:stopPropagation>
@if (context.ChannelDevicePluginType < ChannelDevicePluginTypeEnum.Channel)
{
@* <Tooltip Title=@(GatewayLocalizer["AddChannel"]) Placement="Placement.Bottom"> *@
<Button Size=Size.ExtraSmall Icon="fa-solid fa-plus" Color=Color.None OnClickWithoutRender=@(() => EditChannel(GatewayLocalizer["AddChannel"], context, ItemChangedType.Add))>
</Button>
@* </Tooltip> *@
@* <Tooltip Title=@(GatewayLocalizer["DeleteChannel"]) Placement="Placement.Bottom"> *@
<Button Size=Size.ExtraSmall Icon="fa-solid fa-xmark" Color=Color.None OnClickWithoutRender=@(() => DeleteCurrentChannel(context))>
</Button>
@* </Tooltip> *@
}
@* //更新通道 *@
@if (context.ChannelDevicePluginType == ChannelDevicePluginTypeEnum.Channel)
{
@* <Tooltip Title=@(GatewayLocalizer["AddDevice"]) Placement="Placement.Bottom"> *@
<Button Size=Size.ExtraSmall Icon="fa-solid fa-plus" Color=Color.None OnClickWithoutRender=@(() => EditDevice(GatewayLocalizer["AddDevice"], context, ItemChangedType.Add))>
</Button>
@* </Tooltip> *@
@* <Tooltip Title=@(GatewayLocalizer["UpdateChannel"]) Placement="Placement.Bottom"> *@
<Button Size=Size.ExtraSmall Icon="fa-regular fa-pen-to-square" Color=Color.None OnClickWithoutRender=@(() => EditChannel(GatewayLocalizer["UpdateChannel"], context, ItemChangedType.Update))>
</Button>
@* </Tooltip> *@
@* <Tooltip Title=@(GatewayLocalizer["DeleteChannel"]) Placement="Placement.Bottom"> *@
<Button Size=Size.ExtraSmall Icon="fa-solid fa-xmark" Color=Color.None OnClickWithoutRender=@(() => DeleteCurrentChannel(context))>
</Button>
@* </Tooltip> *@
}
@* //更新设备 *@
@if (context.ChannelDevicePluginType == ChannelDevicePluginTypeEnum.Device)
{
@* <Tooltip Title=@(GatewayLocalizer["UpdateDevice"]) Placement="Placement.Bottom"> *@
<Button Size=Size.ExtraSmall Icon="fa-regular fa-pen-to-square" Color=Color.None OnClickWithoutRender=@(() => EditDevice(GatewayLocalizer["UpdateDevice"], context, ItemChangedType.Update))>
</Button>
@* </Tooltip> *@
@* <Tooltip Title=@(GatewayLocalizer["DeleteDevice"]) Placement="Placement.Bottom"> *@
<Button Size=Size.ExtraSmall Icon="fa-solid fa-xmark" Color=Color.None OnClickWithoutRender=@(() => DeleteCurrentDevice(context))>
</Button>
@* </Tooltip> *@
}
</div>
</ToolbarTemplate>
</TreeView>
<ContextMenu style="max-height:800px" class="tgTree" OnBeforeShowCallback="OnBeforeShowCallback">
@if (SelectModel != null)
{
<ContextMenuItem Icon="fa-solid fa-plus" Disabled="(SelectModel.ChannelDevicePluginType >ChannelDevicePluginTypeEnum.Channel||!AuthorizeButton(AdminOperConst.Add))" Text="@GatewayLocalizer["AddChannel"]" OnClick=@((a,b)=>EditChannel(a,b,ItemChangedType.Add))></ContextMenuItem>
<ContextMenuItem Icon="fa-solid fa-plus" Disabled="(SelectModel.ChannelDevicePluginType > ChannelDevicePluginTypeEnum.Channel || !AuthorizeButton(AdminOperConst.Add))" Text="@GatewayLocalizer["AddChannel"]" OnClick=@((a, b) => EditChannel(a, b, ItemChangedType.Add))></ContextMenuItem>
<ContextMenuItem Icon="fa-regular fa-pen-to-square" Disabled="(SelectModel.ChannelDevicePluginType >=ChannelDevicePluginTypeEnum.Channel||!AuthorizeButton(AdminOperConst.Edit))" Text="@GatewayLocalizer["BatchEditChannel"]" OnClick=@((a,b)=>BatchEditChannel(a,b))></ContextMenuItem>
<ContextMenuItem Icon="fa-regular fa-pen-to-square" Disabled="(SelectModel.ChannelDevicePluginType >= ChannelDevicePluginTypeEnum.Channel || !AuthorizeButton(AdminOperConst.Edit))" Text="@GatewayLocalizer["BatchEditChannel"]" OnClick=@((a, b) => BatchEditChannel(a, b))></ContextMenuItem>
<ContextMenuItem Icon="fa-regular fa-pen-to-square" Disabled="(!AuthorizeButton(AdminOperConst.Add))" Text="@GatewayLocalizer["ExcelChannel"]" OnClick=@((a,b)=>ExcelChannel(a,b))></ContextMenuItem>
<ContextMenuItem Icon="fa-regular fa-pen-to-square" Disabled="(!AuthorizeButton(AdminOperConst.Add))" Text="@GatewayLocalizer["ExcelChannel"]" OnClick=@((a, b) => ExcelChannel(a, b))></ContextMenuItem>
<ContextMenuItem Icon="fa-regular fa-pen-to-square" Disabled="(SelectModel.ChannelDevicePluginType !=ChannelDevicePluginTypeEnum.Channel||!AuthorizeButton(AdminOperConst.Edit))" Text="@GatewayLocalizer["UpdateChannel"]" OnClick=@((a,b)=>EditChannel(a,b,ItemChangedType.Update))></ContextMenuItem>
<ContextMenuItem Icon="fa-regular fa-pen-to-square" Disabled="(SelectModel.ChannelDevicePluginType != ChannelDevicePluginTypeEnum.Channel || !AuthorizeButton(AdminOperConst.Edit))" Text="@GatewayLocalizer["UpdateChannel"]" OnClick=@((a, b) => EditChannel(a, b, ItemChangedType.Update))></ContextMenuItem>
<ContextMenuItem Icon="fa-regular fa-copy" Disabled="(SelectModel.ChannelDevicePluginType !=ChannelDevicePluginTypeEnum.Channel||!AuthorizeButton(AdminOperConst.Add))" Text="@GatewayLocalizer["CopyChannel"]" OnClick=@((a,b)=>CopyChannel(a,b))></ContextMenuItem>
<ContextMenuItem Icon="fa-regular fa-copy" Disabled="(SelectModel.ChannelDevicePluginType != ChannelDevicePluginTypeEnum.Channel || !AuthorizeButton(AdminOperConst.Add))" Text="@GatewayLocalizer["CopyChannel"]" OnClick=@((a, b) => CopyChannel(a, b))></ContextMenuItem>
<ContextMenuItem Icon="fa-solid fa-xmark" Text="@GatewayLocalizer["DeleteChannel"]" Disabled="(SelectModel.ChannelDevicePluginType ==ChannelDevicePluginTypeEnum.Device||!AuthorizeButton(AdminOperConst.Delete))" OnClick="DeleteChannel"></ContextMenuItem>
<ContextMenuItem Icon="fa-solid fa-xmark" Text="@GatewayLocalizer["DeleteChannel"]" Disabled="(SelectModel.ChannelDevicePluginType == ChannelDevicePluginTypeEnum.Device || !AuthorizeButton(AdminOperConst.Delete))" OnClick="DeleteChannel"></ContextMenuItem>
<ContextMenuItem Icon="fas fa-file-export" Text="@GatewayLocalizer["ExportChannel"]"
Disabled=@(SelectModel.ChannelDevicePluginType ==ChannelDevicePluginTypeEnum.Device||!AuthorizeButton("导出"))
@@ -44,16 +100,16 @@
<ContextMenuDivider></ContextMenuDivider>
<ContextMenuItem Disabled="(SelectModel.ChannelDevicePluginType <ChannelDevicePluginTypeEnum.Channel)||!AuthorizeButton(AdminOperConst.Add)" Icon="fa-solid fa-plus device" Text="@GatewayLocalizer["AddDevice"]" OnClick=@((a,b)=>EditDevice(a,b,ItemChangedType.Add))></ContextMenuItem>
<ContextMenuItem Disabled="(SelectModel.ChannelDevicePluginType < ChannelDevicePluginTypeEnum.Channel) || !AuthorizeButton(AdminOperConst.Add)" Icon="fa-solid fa-plus device" Text="@GatewayLocalizer["AddDevice"]" OnClick=@((a, b) => EditDevice(a, b, ItemChangedType.Add))></ContextMenuItem>
<ContextMenuItem Icon="fa-regular fa-pen-to-square" Text="@GatewayLocalizer["BatchEditDevice"]" Disabled="(SelectModel.ChannelDevicePluginType >=ChannelDevicePluginTypeEnum.Device)||!AuthorizeButton(AdminOperConst.Edit)" OnClick=@((a,b)=>BatchEditDevice(a,b))></ContextMenuItem>
<ContextMenuItem Icon="fa-regular fa-pen-to-square" Text="@GatewayLocalizer["BatchEditDevice"]" Disabled="(SelectModel.ChannelDevicePluginType >= ChannelDevicePluginTypeEnum.Device) || !AuthorizeButton(AdminOperConst.Edit)" OnClick=@((a, b) => BatchEditDevice(a, b))></ContextMenuItem>
<ContextMenuItem Disabled="!AuthorizeButton(AdminOperConst.Add)" Icon="fa-solid fa-plus" Text="@GatewayLocalizer["ExcelDevice"]" OnClick=@((a,b)=>ExcelDevice(a,b))></ContextMenuItem>
<ContextMenuItem Disabled="!AuthorizeButton(AdminOperConst.Add)" Icon="fa-solid fa-plus" Text="@GatewayLocalizer["ExcelDevice"]" OnClick=@((a, b) => ExcelDevice(a, b))></ContextMenuItem>
<ContextMenuItem Disabled="(SelectModel.ChannelDevicePluginType !=ChannelDevicePluginTypeEnum.Device)||!AuthorizeButton(AdminOperConst.Edit)" Icon="fa-regular fa-pen-to-square" Text="@GatewayLocalizer["UpdateDevice"]" OnClick=@((a,b)=>EditDevice(a,b,ItemChangedType.Update))></ContextMenuItem>
<ContextMenuItem Disabled="(SelectModel.ChannelDevicePluginType != ChannelDevicePluginTypeEnum.Device) || !AuthorizeButton(AdminOperConst.Edit)" Icon="fa-regular fa-pen-to-square" Text="@GatewayLocalizer["UpdateDevice"]" OnClick=@((a, b) => EditDevice(a, b, ItemChangedType.Update))></ContextMenuItem>
<ContextMenuItem Disabled="(SelectModel.ChannelDevicePluginType !=ChannelDevicePluginTypeEnum.Device)||!AuthorizeButton(AdminOperConst.Add)" Icon="fa-regular fa-pen-to-square" Text="@GatewayLocalizer["CopyDevice"]" OnClick=@((a,b)=>CopyDevice(a,b))></ContextMenuItem>
<ContextMenuItem Disabled="(SelectModel.ChannelDevicePluginType != ChannelDevicePluginTypeEnum.Device) || !AuthorizeButton(AdminOperConst.Add)" Icon="fa-regular fa-pen-to-square" Text="@GatewayLocalizer["CopyDevice"]" OnClick=@((a, b) => CopyDevice(a, b))></ContextMenuItem>
<ContextMenuItem Icon="fa-solid fa-xmark" Disabled="(!AuthorizeButton(AdminOperConst.Delete))" Text="@GatewayLocalizer["DeleteDevice"]" OnClick="DeleteDevice"></ContextMenuItem>

View File

@@ -101,19 +101,22 @@ public partial class ChannelDeviceTree
#region
async Task EditChannel(ContextMenuItem item, object value, ItemChangedType itemChangedType)
Task EditChannel(ContextMenuItem item, object value, ItemChangedType itemChangedType)
{
return EditChannel(item.Text, value as ChannelDeviceTreeItem, itemChangedType);
}
async Task EditChannel(string text, ChannelDeviceTreeItem channelDeviceTreeItem, ItemChangedType itemChangedType)
{
var op = new DialogOption()
{
IsScrolling = false,
ShowMaximizeButton = true,
Size = Size.ExtraLarge,
Title = item.Text,
Title = text,
ShowFooter = false,
ShowCloseButton = false,
};
if (value is not ChannelDeviceTreeItem channelDeviceTreeItem) return;
PluginTypeEnum? pluginTypeEnum = ChannelDeviceHelpers.GetPluginType(channelDeviceTreeItem);
var oneModel = ChannelDeviceHelpers.GetChannelModel(itemChangedType, channelDeviceTreeItem);
@@ -134,12 +137,16 @@ public partial class ChannelDeviceTree
}
async Task CopyChannel(ContextMenuItem item, object value)
Task CopyChannel(ContextMenuItem item, object value)
{
return CopyChannel(item.Text, value as ChannelDeviceTreeItem);
}
async Task CopyChannel(string text, ChannelDeviceTreeItem channelDeviceTreeItem)
{
Channel oneModel = null;
Dictionary<Device, List<Variable>> deviceDict = new();
if (value is not ChannelDeviceTreeItem channelDeviceTreeItem) return;
if (channelDeviceTreeItem.TryGetChannelRuntime(out var channelRuntime))
{
@@ -159,7 +166,7 @@ public partial class ChannelDeviceTree
IsScrolling = false,
ShowMaximizeButton = true,
Size = Size.ExtraLarge,
Title = item.Text,
Title = text,
ShowFooter = false,
ShowCloseButton = false,
};
@@ -182,22 +189,21 @@ public partial class ChannelDeviceTree
}
async Task BatchEditChannel(ContextMenuItem item, object value)
Task BatchEditChannel(ContextMenuItem item, object value)
{
return BatchEditChannel(item.Text, value as ChannelDeviceTreeItem);
}
async Task BatchEditChannel(string text, ChannelDeviceTreeItem channelDeviceTreeItem)
{
Channel oldModel = null;
Channel oneModel = null;
IEnumerable<Channel>? changedModels = null;
IEnumerable<Channel>? models = null;
if (value is not ChannelDeviceTreeItem channelDeviceTreeItem) return;
if (channelDeviceTreeItem.TryGetChannelRuntime(out var channelRuntime))
{
await EditChannel(item, value, ItemChangedType.Update);
await EditChannel(text, channelDeviceTreeItem, ItemChangedType.Update);
return;
}
//批量编辑只有分类和插件名称节点
@@ -235,7 +241,7 @@ public partial class ChannelDeviceTree
IsScrolling = false,
ShowMaximizeButton = true,
Size = Size.ExtraLarge,
Title = item.Text,
Title = text,
ShowFooter = false,
ShowCloseButton = false,
};
@@ -265,8 +271,11 @@ public partial class ChannelDeviceTree
}
async Task ExcelChannel(ContextMenuItem item, object value)
Task ExcelChannel(ContextMenuItem item, object value)
{
return ExcelChannel(item.Text);
}
async Task ExcelChannel(string text)
{
var op = new DialogOption()
@@ -274,7 +283,7 @@ public partial class ChannelDeviceTree
IsScrolling = false,
ShowMaximizeButton = true,
Size = Size.ExtraExtraLarge,
Title = item.Text,
Title = text,
ShowFooter = false,
ShowCloseButton = false,
};
@@ -316,8 +325,11 @@ finally
}
async Task DeleteChannel(ContextMenuItem item, object value)
Task DeleteChannel(ContextMenuItem item, object value)
{
return DeleteChannel(value as ChannelDeviceTreeItem);
}
async Task DeleteChannel(ChannelDeviceTreeItem channelDeviceTreeItem)
{
var op = new DialogOption();
{
@@ -336,7 +348,7 @@ finally
EventCallback.Factory.Create<MouseEventArgs>(this, async e =>
{
await op.CloseDialogAsync();
await DeleteCurrentChannel(item, value);
await DeleteCurrentChannel(channelDeviceTreeItem);
}));
builder.AddComponentParameter(5, nameof(Button.Text), GatewayLocalizer["DeleteCurrentChannel"].Value);
@@ -348,7 +360,7 @@ finally
builder.AddComponentParameter(15, nameof(Button.OnClick), EventCallback.Factory.Create<MouseEventArgs>(this, async e =>
{
await op.CloseDialogAsync();
await DeleteAllChannel(item, value);
await DeleteAllChannel();
}));
builder.AddComponentParameter(16, nameof(Button.Text), GatewayLocalizer["DeleteAllChannel"].Value);
@@ -361,10 +373,9 @@ finally
;
await DialogService.Show(op);
}
async Task DeleteCurrentChannel(ContextMenuItem item, object value)
async Task DeleteCurrentChannel(ChannelDeviceTreeItem channelDeviceTreeItem)
{
IEnumerable<ChannelRuntime> modelIds = null;
if (value is not ChannelDeviceTreeItem channelDeviceTreeItem) return;
if (channelDeviceTreeItem.TryGetChannelRuntime(out var channelRuntime))
{
@@ -395,6 +406,7 @@ finally
{
var op = new SwalOption()
{
Title = GatewayLocalizer["DeleteConfirmTitle"],
BodyTemplate = (__builder) =>
{
@@ -434,7 +446,7 @@ finally
}
}
async Task DeleteAllChannel(ContextMenuItem item, object value)
async Task DeleteAllChannel()
{
try
{
@@ -479,7 +491,11 @@ finally
}
async Task ExportChannel(ContextMenuItem item, object value)
Task ExportChannel(ContextMenuItem item, object value)
{
return ExportChannel(value as ChannelDeviceTreeItem);
}
async Task ExportChannel(ChannelDeviceTreeItem channelDeviceTreeItem)
{
var op = new DialogOption();
{
@@ -498,7 +514,7 @@ finally
EventCallback.Factory.Create<MouseEventArgs>(this, async e =>
{
await op.CloseDialogAsync();
await ExportCurrentChannel(item, value);
await ExportCurrentChannel(channelDeviceTreeItem);
}));
builder.AddComponentParameter(5, nameof(Button.Text), GatewayLocalizer["ExportCurrentChannel"].Value);
@@ -510,7 +526,7 @@ EventCallback.Factory.Create<MouseEventArgs>(this, async e =>
builder.AddComponentParameter(15, nameof(Button.OnClick), EventCallback.Factory.Create<MouseEventArgs>(this, async e =>
{
await op.CloseDialogAsync();
await ExportAllChannel(item, value);
await ExportAllChannel();
}));
builder.AddComponentParameter(16, nameof(Button.Text), GatewayLocalizer["ExportAllChannel"].Value);
@@ -523,10 +539,9 @@ EventCallback.Factory.Create<MouseEventArgs>(this, async e =>
;
await DialogService.Show(op);
}
async Task ExportCurrentChannel(ContextMenuItem item, object value)
async Task ExportCurrentChannel(ChannelDeviceTreeItem channelDeviceTreeItem)
{
bool ret;
if (value is not ChannelDeviceTreeItem channelDeviceTreeItem) return;
if (channelDeviceTreeItem.TryGetChannelRuntime(out var channelRuntime))
{
@@ -550,7 +565,7 @@ EventCallback.Factory.Create<MouseEventArgs>(this, async e =>
if (ret)
await ToastService.Default();
}
async Task ExportAllChannel(ContextMenuItem item, object value)
async Task ExportAllChannel()
{
bool ret;
ret = await GatewayExportService.OnChannelExport(new() { QueryPageOptions = new() });
@@ -561,14 +576,18 @@ EventCallback.Factory.Create<MouseEventArgs>(this, async e =>
}
async Task ImportChannel(ContextMenuItem item, object value)
Task ImportChannel(ContextMenuItem item, object value)
{
return ImportChannel(item.Text);
}
async Task ImportChannel(string text)
{
var op = new DialogOption()
{
IsScrolling = true,
ShowMaximizeButton = true,
Size = Size.ExtraLarge,
Title = item.Text,
Title = text,
ShowFooter = false,
ShowCloseButton = false,
OnCloseAsync = async () =>
@@ -654,19 +673,22 @@ EventCallback.Factory.Create<MouseEventArgs>(this, async e =>
}
async Task EditDevice(ContextMenuItem item, object value, ItemChangedType itemChangedType)
Task EditDevice(ContextMenuItem item, object value, ItemChangedType itemChangedType)
{
return EditDevice(item.Text, value as ChannelDeviceTreeItem, itemChangedType);
}
async Task EditDevice(string text, ChannelDeviceTreeItem channelDeviceTreeItem, ItemChangedType itemChangedType)
{
var op = new DialogOption()
{
IsScrolling = true,
ShowMaximizeButton = true,
Size = Size.ExtraLarge,
Title = item.Text,
Title = text,
ShowFooter = false,
ShowCloseButton = false,
};
Device oneModel = null;
if (value is not ChannelDeviceTreeItem channelDeviceTreeItem) return;
if (channelDeviceTreeItem.TryGetDeviceRuntime(out var deviceRuntime))
{
@@ -845,7 +867,12 @@ finally
}
async Task DeleteDevice(ContextMenuItem item, object value)
Task DeleteDevice(ContextMenuItem item, object value)
{
return DeleteDevice(value as ChannelDeviceTreeItem);
}
async Task DeleteDevice(ChannelDeviceTreeItem channelDeviceTreeItem)
{
var op = new DialogOption();
{
@@ -864,7 +891,7 @@ finally
EventCallback.Factory.Create<MouseEventArgs>(this, async e =>
{
await op.CloseDialogAsync();
await DeleteCurrentDevice(item, value);
await DeleteCurrentDevice(channelDeviceTreeItem);
}));
builder.AddComponentParameter(5, nameof(Button.Text), GatewayLocalizer["DeleteCurrentDevice"].Value);
@@ -876,7 +903,7 @@ EventCallback.Factory.Create<MouseEventArgs>(this, async e =>
builder.AddComponentParameter(15, nameof(Button.OnClick), EventCallback.Factory.Create<MouseEventArgs>(this, async e =>
{
await op.CloseDialogAsync();
await DeleteAllDevice(item, value);
await DeleteAllDevice();
}));
builder.AddComponentParameter(16, nameof(Button.Text), GatewayLocalizer["DeleteAllDevice"].Value);
@@ -886,15 +913,14 @@ EventCallback.Factory.Create<MouseEventArgs>(this, async e =>
});
}
;
;
await DialogService.Show(op);
}
async Task DeleteCurrentDevice(ContextMenuItem item, object value)
async Task DeleteCurrentDevice(ChannelDeviceTreeItem channelDeviceTreeItem)
{
IEnumerable<DeviceRuntime> modelIds = null;
if (value is not ChannelDeviceTreeItem channelDeviceTreeItem) return;
if (channelDeviceTreeItem.TryGetDeviceRuntime(out var deviceRuntime))
{
@@ -968,7 +994,7 @@ EventCallback.Factory.Create<MouseEventArgs>(this, async e =>
}
async Task DeleteAllDevice(ContextMenuItem item, object value)
async Task DeleteAllDevice()
{
try
{
@@ -1214,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,
}};
@@ -1259,7 +1285,7 @@ EventCallback.Factory.Create<MouseEventArgs>(this, async e =>
}
}
Items = ZItem;
Items = ZItem.AdaptListTreeViewItemChannelDeviceTreeItem();
ChannelRuntimeDispatchService.Subscribe(Refresh);
scheduler = new SmartTriggerScheduler(Notify, TimeSpan.FromMilliseconds(3000));
@@ -1287,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);
@@ -1313,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
};
}
@@ -1375,7 +1402,7 @@ EventCallback.Factory.Create<MouseEventArgs>(this, async e =>
{
}
}
Items = ZItem;
Items = ZItem.AdaptListTreeViewItemChannelDeviceTreeItem();
return Items;
}
else
@@ -1459,7 +1486,7 @@ EventCallback.Factory.Create<MouseEventArgs>(this, async e =>
}
}
Items = ZItem;
Items = ZItem.AdaptListTreeViewItemChannelDeviceTreeItem();
return Items;
}
@@ -1473,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:
@@ -1506,9 +1533,5 @@ EventCallback.Factory.Create<MouseEventArgs>(this, async e =>
}
return Task.CompletedTask;
}
protected override void OnAfterRender(bool firstRender)
{
base.OnAfterRender(firstRender);
}
}

View File

@@ -2,15 +2,26 @@
.listtree-view {
height: 100%;
}
.listtree-view ::deep .bb-cm-zone {
height: calc(100% - 60px);
height: calc(100% - 60px);
/*margin-bottom: 2px;*/
}
.listtree-view ::deep .tree-view {
--bb-tree-search-height: 32px;
min-height: 300px;
height: 100%;
}
.listtree-view ::deep .tree-view .tree-root {
padding-right: 0px;
}
.deviceEnable--text {
color: var(--bs-body-color);
}
.tree-node-toolbar-edit {
background-color: white;
}

View File

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

View File

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

View File

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

View File

@@ -28,10 +28,10 @@
IsPagination=true>
<TableColumns>
<TableColumn Field="@context.Name" FieldExpression=@(()=>context.Name) ShowTips=true Filterable=true Sortable=true Visible=true />
<TableColumn Field="@context.Description" FieldExpression=@(()=>context.Description) ShowTips=true Filterable=true Sortable=true Visible=true />
<TableColumn Field="@context.IntervalTime" FieldExpression=@(()=>context.IntervalTime) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.ChannelName" FieldExpression=@(()=>context.ChannelName) ShowTips=true Filterable=true Sortable=true Visible=true />
<TableColumn Field="@context.Name" FieldExpression=@(() => context.Name) ShowTips=true Filterable=true Sortable=true Visible=true />
<TableColumn Field="@context.Description" FieldExpression=@(() => context.Description) ShowTips=true Filterable=true Sortable=true Visible=true />
<TableColumn Field="@context.IntervalTime" FieldExpression=@(() => context.IntervalTime) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.ChannelName" FieldExpression=@(() => context.ChannelName) ShowTips=true Filterable=true Sortable=true Visible=true />
<TableColumn @bind-Field="@context.Enable" Filterable=true Sortable=true Visible="true" />
<TableColumn @bind-Field="@context.LogLevel" Filterable=true Sortable=true Visible="false" />
<TableColumn @bind-Field="@context.Remark1" Filterable=true Sortable=true Visible="false" />
@@ -40,25 +40,25 @@
<TableColumn @bind-Field="@context.Remark4" Filterable=true Sortable=true Visible="false" />
<TableColumn @bind-Field="@context.Remark5" Filterable=true Sortable=true Visible="false" />
<TableColumn Field="@context.ActiveTime" FieldExpression=@(()=>context.ActiveTime) ShowTips=true Filterable=true Sortable=true Visible=true />
<TableColumn Field="@context.DeviceStatus" FieldExpression=@(()=>context.DeviceStatus) ShowTips=true Filterable=true Sortable=true Visible=true />
<TableColumn Field="@context.ActiveTime" FieldExpression=@(() => context.ActiveTime) ShowTips=true Filterable=true Sortable=true Visible=true />
<TableColumn Field="@context.DeviceStatus" FieldExpression=@(() => context.DeviceStatus) ShowTips=true Filterable=true Sortable=true Visible=true />
<TableColumn Field="@context.Pause" FieldExpression=@(()=>context.Pause) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.LastErrorMessage" FieldExpression=@(()=>context.LastErrorMessage) ShowTips=true Filterable=true Sortable=true Visible=true />
<TableColumn Field="@context.Pause" FieldExpression=@(() => context.Pause) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.LastErrorMessage" FieldExpression=@(() => context.LastErrorMessage) ShowTips=true Filterable=true Sortable=true Visible=true />
<TableColumn Field="@context.PluginName" FieldExpression=@(()=>context.PluginName) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.PluginName" FieldExpression=@(() => context.PluginName) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.DeviceVariableCount" FieldExpression=@(()=>context.DeviceVariableCount) ShowTips=true Filterable=true Sortable=true Visible=true />
<TableColumn Field="@context.MethodVariableCount" FieldExpression=@(()=>context.MethodVariableCount) ShowTips=true Filterable=true Sortable=true Visible=true />
<TableColumn Field="@context.SourceVariableCount" FieldExpression=@(()=>context.SourceVariableCount) ShowTips=true Filterable=true Sortable=true Visible=true />
<TableColumn Field="@context.DeviceVariableCount" FieldExpression=@(() => context.DeviceVariableCount) ShowTips=true Filterable=true Sortable=true Visible=true />
<TableColumn Field="@context.MethodVariableCount" FieldExpression=@(() => context.MethodVariableCount) ShowTips=true Filterable=true Sortable=true Visible=true />
<TableColumn Field="@context.SourceVariableCount" FieldExpression=@(() => context.SourceVariableCount) ShowTips=true Filterable=true Sortable=true Visible=true />
<TableColumn Field="@context.ChannelId" FieldExpression=@(()=>context.ChannelId) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.RedundantEnable" FieldExpression=@(()=>context.RedundantEnable) Ignore="true" ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.RedundantType" FieldExpression=@(()=>context.RedundantType) Ignore="true" ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.RedundantDeviceId" FieldExpression=@(()=>context.RedundantDeviceId) Ignore="true" ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.RedundantScanIntervalTime" FieldExpression=@(()=>context.RedundantScanIntervalTime) Ignore="true" ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.RedundantScript" FieldExpression=@(()=>context.RedundantScript) Ignore="true" ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.RedundantSwitchType" FieldExpression=@(()=>context.RedundantSwitchType) Ignore="true" ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.ChannelId" FieldExpression=@(() => context.ChannelId) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.RedundantEnable" FieldExpression=@(() => context.RedundantEnable) Ignore="true" ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.RedundantType" FieldExpression=@(() => context.RedundantType) Ignore="true" ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.RedundantDeviceId" FieldExpression=@(() => context.RedundantDeviceId) Ignore="true" ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.RedundantScanIntervalTime" FieldExpression=@(() => context.RedundantScanIntervalTime) Ignore="true" ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.RedundantScript" FieldExpression=@(() => context.RedundantScript) Ignore="true" ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.RedundantSwitchType" FieldExpression=@(() => context.RedundantSwitchType) Ignore="true" ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn @bind-Field="@context.Id" Filterable=true Sortable=true Visible="false" DefaultSort=true DefaultSortOrder="SortOrder.Asc" />
@@ -66,28 +66,35 @@
</TableColumns>
<EditTemplate Context="context">
<DeviceEditComponent Model=@(context) ValidateEnable=false BatchEditEnable=false AutoRestartThread=AutoRestartThread ></DeviceEditComponent>
<DeviceEditComponent Model=@(context) ValidateEnable=false BatchEditEnable=false AutoRestartThread=AutoRestartThread></DeviceEditComponent>
</EditTemplate>
<ExportButtonDropdownTemplate Context="ExportContext">
<Button class="dropdown-item" OnClick="() => ExcelExportAsync(ExportContext,true)" IsDisabled=@(!AuthorizeButton("导出"))>
<i class="fas fa-file-export"></i>
<span>@RazorLocalizer["ExportAll"]</span>
</Button>
<Button class="dropdown-item" OnClick="() => ExcelExportAsync(ExportContext)" IsDisabled=@(!AuthorizeButton("导出"))>
<i class="fas fa-file-export"></i>
<span>@RazorLocalizer["TablesExportButtonExcelText"]</span>
</Button>
<Button class="dropdown-item" OnClick="() => ExcelImportAsync(ExportContext)" IsDisabled=@(!AuthorizeButton("导入"))>
<i class="fas fa-file-import"></i>
<span>@RazorLocalizer["TablesImportButtonExcelText"]</span>
</Button>
<Button class="dropdown-item" OnClick="() => ExcelDeviceAsync(ExportContext)" IsDisabled=@(!AuthorizeButton("导入"))>
<i class="fas fa-file-import"></i>
<span>@GatewayLocalizer["ExcelDevice"]</span>
</Button>
@if ((AuthorizeButton("导出")))
{
<Button class="dropdown-item" OnClick="() => ExcelExportAsync(ExportContext, true)" IsKeepDisabled=@(!AuthorizeButton("导出"))>
<i class="fas fa-file-export"></i>
<span>@RazorLocalizer["ExportAll"]</span>
</Button>
<Button class="dropdown-item" OnClick="() => ExcelExportAsync(ExportContext)" IsKeepDisabled=@(!AuthorizeButton("导出"))>
<i class="fas fa-file-export"></i>
<span>@RazorLocalizer["TablesExportButtonExcelText"]</span>
</Button>
}
@if ((AuthorizeButton("导入")))
{
<Button class="dropdown-item" OnClick="() => ExcelImportAsync(ExportContext)" IsKeepDisabled=@(!AuthorizeButton("导入"))>
<i class="fas fa-file-import"></i>
<span>@RazorLocalizer["TablesImportButtonExcelText"]</span>
</Button>
<Button class="dropdown-item" OnClick="() => ExcelDeviceAsync(ExportContext)" IsKeepDisabled=@(!AuthorizeButton("导入"))>
<i class="fas fa-file-import"></i>
<span>@GatewayLocalizer["ExcelDevice"]</span>
</Button>
}
</ExportButtonDropdownTemplate>
<TableToolbarTemplate>

View File

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

View File

@@ -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" />
}

Some files were not shown because too many files have changed in this diff Show More