Compare commits
	
		
			12 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 64bc6084be | ||
|   | 20434d5bd2 | ||
|   | 9ecc9380e6 | ||
|   | a57c55080b | ||
|   | b8ca06c6be | ||
|   | 5aff6461a1 | ||
|   | 6cf53fefec | ||
|   | 45132f3503 | ||
|   | f1e78a0e8a | ||
|   | 0bf28ec275 | ||
|   | 41f8412c97 | ||
|   | c535974362 | 
| @@ -10,9 +10,6 @@ | ||||
|  | ||||
| using SqlSugar; | ||||
|  | ||||
| using ThingsGateway.List; | ||||
| using ThingsGateway.NewLife.Json.Extension; | ||||
|  | ||||
| namespace ThingsGateway.Admin.Application; | ||||
|  | ||||
| /// <summary> | ||||
| @@ -169,7 +166,7 @@ internal sealed class VerificatInfoService : BaseService<VerificatInfo>, IVerifi | ||||
|     public void RemoveAllClientId() | ||||
|     { | ||||
|         using var db = GetDB(); | ||||
|         db.Updateable<VerificatInfo>().SetColumns(a=>a.ClientIds==null).Where(a => a.Id > 0).ExecuteCommand(); | ||||
|         db.Updateable<VerificatInfo>().SetColumns(a => a.ClientIds == null).Where(a => a.Id > 0).ExecuteCommand(); | ||||
|         VerificatInfoService.RemoveCache(); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -324,14 +324,22 @@ public class ObjectPool<T> : DisposeBase, IPool<T> where T : notnull | ||||
|             // 移除扩展空闲集合里面的超时项 | ||||
|             while (_free2.TryPeek(out var pi) && pi.LastTime < exp) | ||||
|             { | ||||
|                 // 取出来销毁 | ||||
|                 if (_free2.TryDequeue(out pi)) | ||||
|                 // 取出来销毁。在并行操作中,此时返回可能是另一个对象 | ||||
|                 if (_free2.TryDequeue(out var pi2)) | ||||
|                 { | ||||
|                     pi.Value.TryDispose(); | ||||
|                     if (pi2.LastTime < exp) | ||||
|                     { | ||||
|                         pi2.Value.TryDispose(); | ||||
|  | ||||
|                         count++; | ||||
|                         Interlocked.Decrement(ref _FreeCount); | ||||
|                     } | ||||
|                     else | ||||
|                     { | ||||
|                         // 可能是另一个对象,放回去 | ||||
|                         _free2.Enqueue(pi2); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -73,7 +73,7 @@ public partial class CultureChooser | ||||
|     { | ||||
|         if (_firstRender) | ||||
|         { | ||||
|             if (OperatingSystem.IsBrowser() || !Runtime.IsWeb) | ||||
|             if (OperatingSystem.IsBrowser() || !Runtime.IsWeb || App.EffectiveTypes.FirstOrDefault(a => a.Name.Contains("WebView")) != null) | ||||
|             { | ||||
|                 var cultureName = item.Value; | ||||
|                 if (cultureName != CultureInfo.CurrentCulture.Name) | ||||
|   | ||||
| @@ -51,48 +51,6 @@ public static class GenericExtensions | ||||
|         return differences; | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /// <inheritdoc/> | ||||
|     public static IEnumerable<PropertyInfo> GetProperties(this IEnumerable<dynamic> value, params string[] names) | ||||
|     { | ||||
|         // 获取动态对象集合的类型 | ||||
|         var type = value.GetType().GetGenericArguments().LastOrDefault() ?? throw new ArgumentNullException(nameof(value)); | ||||
|  | ||||
|         var namesStr = System.Text.Json.JsonSerializer.Serialize(names); | ||||
|         // 构建缓存键,包括属性名和类型信息 | ||||
|         var cacheKey = $"{namesStr}-{type.FullName}-{type.TypeHandle.Value}"; | ||||
|  | ||||
|         // 从缓存中获取属性信息,如果缓存不存在,则创建并缓存 | ||||
|         var result = Instance.GetOrAdd(cacheKey, a => | ||||
|         { | ||||
|             // 获取动态对象类型中指定名称的属性信息 | ||||
|             var properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.GetProperty) | ||||
|                   .Where(pi => names.Contains(pi.Name)) // 筛选出指定属性名的属性信息 | ||||
|                   .Where(pi => pi != null) // 过滤空属性信息 | ||||
|                   .AsEnumerable(); | ||||
|  | ||||
|             // 检查是否找到了所有指定名称的属性,如果没有找到,则抛出异常 | ||||
|             if (names.Length != properties.Count()) | ||||
|             { | ||||
|                 throw new InvalidOperationException($"Couldn't find properties on type:{type.Name},{Environment.NewLine}names:{namesStr}"); | ||||
|             } | ||||
|  | ||||
|             return properties; // 返回属性信息集合 | ||||
|         }, 3600); // 缓存有效期为3600秒 | ||||
|  | ||||
|         return result!; // 返回属性信息集合 | ||||
|     } | ||||
|  | ||||
|     /// <inheritdoc/> | ||||
|     public static IEnumerable<IGrouping<object[], dynamic>> GroupByKeys(this IEnumerable<dynamic> values, IEnumerable<string> keys) | ||||
|     { | ||||
|         // 获取动态对象集合中指定键的属性信息 | ||||
|         var properties = GetProperties(values, keys.ToArray()); | ||||
|  | ||||
|         // 使用对象数组作为键进行分组 | ||||
|         return values.GroupBy(v => properties.Select(property => property.GetValue(v)).ToArray(), new ArrayEqualityComparer()); | ||||
|     } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// 是否都包含 | ||||
|     /// </summary> | ||||
|   | ||||
| @@ -7,7 +7,7 @@ | ||||
| 	</PropertyGroup> | ||||
| 	<ItemGroup> | ||||
| 		<PackageReference Include="BootstrapBlazor.FontAwesome" Version="9.0.2" /> | ||||
| 		<PackageReference Include="BootstrapBlazor" Version="9.6.2" /> | ||||
| 		<PackageReference Include="BootstrapBlazor" Version="9.6.3" /> | ||||
| 		<PackageReference Include="Yitter.IdGenerator" Version="1.0.14" /> | ||||
| 	</ItemGroup> | ||||
|  | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| <Project> | ||||
|  | ||||
| 	<PropertyGroup> | ||||
| 		<PluginVersion>10.6.1</PluginVersion> | ||||
| 		<ProPluginVersion>10.6.1</ProPluginVersion> | ||||
| 		<PluginVersion>10.6.5</PluginVersion> | ||||
| 		<ProPluginVersion>10.6.5</ProPluginVersion> | ||||
| 		<AuthenticationVersion>2.1.7</AuthenticationVersion> | ||||
| 	</PropertyGroup> | ||||
|  | ||||
|   | ||||
| @@ -82,7 +82,7 @@ namespace ThingsGateway.Foundation | ||||
|         public virtual int CacheTimeout { get; set; } = 500; | ||||
|         /// <inheritdoc/> | ||||
|         [MinValue(100)] | ||||
|         public virtual ushort ConnectTimeout { get; set; } = 3000; | ||||
|         public virtual int ConnectTimeout { get; set; } = 3000; | ||||
|  | ||||
|         #endregion | ||||
|  | ||||
|   | ||||
| @@ -88,7 +88,7 @@ public interface IChannelOptions | ||||
|     /// <summary> | ||||
|     /// 连接超时时间 | ||||
|     /// </summary> | ||||
|     ushort ConnectTimeout { get; set; } | ||||
|     int ConnectTimeout { get; set; } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// 通道并发控制锁 | ||||
|   | ||||
| @@ -10,8 +10,8 @@ | ||||
|  | ||||
| 	<ItemGroup> | ||||
| 		<PackageReference Include="Microsoft.Extensions.Localization.Abstractions" Version="9.0.5" /> | ||||
| 		<PackageReference Include="TouchSocket" Version="3.1.2" /> | ||||
| 		<PackageReference Include="TouchSocket.SerialPorts" Version="3.1.2" /> | ||||
| 		<PackageReference Include="TouchSocket" Version="3.1.3" /> | ||||
| 		<PackageReference Include="TouchSocket.SerialPorts" Version="3.1.3" /> | ||||
| 	</ItemGroup> | ||||
|  | ||||
| 	<ItemGroup> | ||||
|   | ||||
| @@ -105,7 +105,7 @@ public abstract class BusinessBase : DriverBase | ||||
|         var ids = IdVariableRuntimes.Select(b => b.Value.DeviceId).ToHashSet(); | ||||
|         // 获取当前设备需要采集的设备 | ||||
|         CollectDevices = GlobalData.GetEnableDevices().Where(a => ids.Contains(a.Id)).ToDictionary(a => a.Id); | ||||
|         VariableRuntimeGroups = IdVariableRuntimes.Where(a => !a.Value.Group.IsNullOrEmpty()).GroupBy(a => a.Value.Group ?? string.Empty).ToDictionary(a => a.Key, a => a.Select(a => a.Value).ToList()); | ||||
|         VariableRuntimeGroups = IdVariableRuntimes.Where(a => !a.Value.BusinessGroup.IsNullOrEmpty()).GroupBy(a => a.Value.BusinessGroup ?? string.Empty).ToDictionary(a => a.Key, a => a.Select(a => a.Value).ToList()); | ||||
|  | ||||
|         return Task.CompletedTask; | ||||
|     } | ||||
|   | ||||
| @@ -75,7 +75,7 @@ public abstract class BusinessBaseWithCacheIntervalAlarmModel<VarModel, DevModel | ||||
|  | ||||
|             CollectDevices = GlobalData.GetEnableDevices().Where(a => a.IsCollect == true).ToDictionary(a => a.Id); | ||||
|  | ||||
|             VariableRuntimeGroups = IdVariableRuntimes.GroupBy(a => a.Value.Group ?? string.Empty).ToDictionary(a => a.Key, a => a.Select(a => a.Value).ToList()); | ||||
|             VariableRuntimeGroups = IdVariableRuntimes.GroupBy(a => a.Value.BusinessGroup ?? string.Empty).ToDictionary(a => a.Key, a => a.Select(a => a.Value).ToList()); | ||||
|  | ||||
|         } | ||||
|         else | ||||
|   | ||||
| @@ -74,7 +74,7 @@ public abstract class BusinessBaseWithCacheIntervalDeviceModel<VarModel, DevMode | ||||
|             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.Group ?? string.Empty).ToDictionary(a => a.Key, a => a.Select(a => a.Value).ToList()); | ||||
|             VariableRuntimeGroups = IdVariableRuntimes.GroupBy(a => a.Value.BusinessGroup ?? string.Empty).ToDictionary(a => a.Key, a => a.Select(a => a.Value).ToList()); | ||||
|  | ||||
|         } | ||||
|         else | ||||
|   | ||||
| @@ -23,7 +23,7 @@ namespace ThingsGateway.Gateway.Application; | ||||
| /// <summary> | ||||
| /// 业务插件,额外实现脚本切换实体 | ||||
| /// </summary> | ||||
| public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevModel, AlarmModel> : BusinessBaseWithCacheIntervalAlarmModel<VarModel, DevModel, AlarmModel> where DevModel : class where VarModel : class where AlarmModel : class | ||||
| public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevModel, AlarmModel> : BusinessBaseWithCacheIntervalAlarmModel<VarModel, DevModel, AlarmModel> | ||||
| { | ||||
|     protected sealed override BusinessPropertyWithCacheInterval _businessPropertyWithCacheInterval => _businessPropertyWithCacheIntervalScript; | ||||
|  | ||||
| @@ -65,7 +65,7 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM | ||||
|  | ||||
|     protected List<TopicJson> GetAlarms(IEnumerable<AlarmModel> item) | ||||
|     { | ||||
|         IEnumerable<dynamic>? data = Application.DynamicModelExtension.GetDynamicModel<AlarmModel>(item, _businessPropertyWithCacheIntervalScript.BigTextScriptAlarmModel); | ||||
|         var data = Application.DynamicModelExtension.GetDynamicModel<AlarmModel>(item, _businessPropertyWithCacheIntervalScript.BigTextScriptAlarmModel); | ||||
|         var topicJsonList = new List<TopicJson>(); | ||||
|         var topics = Match(_businessPropertyWithCacheIntervalScript.AlarmTopic); | ||||
|         if (topics.Count > 0) | ||||
| @@ -133,7 +133,7 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM | ||||
|  | ||||
|     protected List<TopicJson> GetDeviceData(IEnumerable<DevModel> item) | ||||
|     { | ||||
|         IEnumerable<dynamic>? data = Application.DynamicModelExtension.GetDynamicModel<DevModel>(item, _businessPropertyWithCacheIntervalScript.BigTextScriptDeviceModel); | ||||
|         var data = Application.DynamicModelExtension.GetDynamicModel<DevModel>(item, _businessPropertyWithCacheIntervalScript.BigTextScriptDeviceModel); | ||||
|         var topicJsonList = new List<TopicJson>(); | ||||
|         var topics = Match(_businessPropertyWithCacheIntervalScript.DeviceTopic); | ||||
|         if (topics.Count > 0) | ||||
| @@ -200,7 +200,7 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM | ||||
|  | ||||
|     protected List<TopicJson> GetVariable(IEnumerable<VarModel> item) | ||||
|     { | ||||
|         IEnumerable<dynamic>? data = Application.DynamicModelExtension.GetDynamicModel<VarModel>(item, _businessPropertyWithCacheIntervalScript.BigTextScriptVariableModel); | ||||
|         var data = Application.DynamicModelExtension.GetDynamicModel<VarModel>(item, _businessPropertyWithCacheIntervalScript.BigTextScriptVariableModel); | ||||
|         var topicJsonList = new List<TopicJson>(); | ||||
|         var topics = Match(_businessPropertyWithCacheIntervalScript.VariableTopic); | ||||
|         if (topics.Count > 0) | ||||
| @@ -376,7 +376,7 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM | ||||
|  | ||||
|     protected List<TopicArray> GetAlarmTopicArrays(IEnumerable<AlarmModel> item) | ||||
|     { | ||||
|         IEnumerable<dynamic>? data = Application.DynamicModelExtension.GetDynamicModel<AlarmModel>(item, _businessPropertyWithCacheIntervalScript.BigTextScriptAlarmModel); | ||||
|         var data = Application.DynamicModelExtension.GetDynamicModel<AlarmModel>(item, _businessPropertyWithCacheIntervalScript.BigTextScriptAlarmModel); | ||||
|         List<TopicArray> topicArrayList = new List<TopicArray>(); | ||||
|         var topics = Match(_businessPropertyWithCacheIntervalScript.AlarmTopic); | ||||
|         if (topics.Count > 0) | ||||
| @@ -441,7 +441,7 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM | ||||
|  | ||||
|     protected List<TopicArray> GetDeviceTopicArray(IEnumerable<DevModel> item) | ||||
|     { | ||||
|         IEnumerable<dynamic>? data = Application.DynamicModelExtension.GetDynamicModel<DevModel>(item, _businessPropertyWithCacheIntervalScript.BigTextScriptDeviceModel); | ||||
|         var data = Application.DynamicModelExtension.GetDynamicModel<DevModel>(item, _businessPropertyWithCacheIntervalScript.BigTextScriptDeviceModel); | ||||
|         List<TopicArray> topicArrayList = new List<TopicArray>(); | ||||
|         var topics = Match(_businessPropertyWithCacheIntervalScript.DeviceTopic); | ||||
|         if (topics.Count > 0) | ||||
| @@ -508,7 +508,7 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM | ||||
|  | ||||
|     protected List<TopicArray> GetVariableTopicArray(IEnumerable<VarModel> item) | ||||
|     { | ||||
|         IEnumerable<dynamic>? data = Application.DynamicModelExtension.GetDynamicModel<VarModel>(item, _businessPropertyWithCacheIntervalScript.BigTextScriptVariableModel); | ||||
|         var data = Application.DynamicModelExtension.GetDynamicModel<VarModel>(item, _businessPropertyWithCacheIntervalScript.BigTextScriptVariableModel); | ||||
|         List<TopicArray> topicArrayList = new List<TopicArray>(); | ||||
|         var topics = Match(_businessPropertyWithCacheIntervalScript.VariableTopic); | ||||
|         if (topics.Count > 0) | ||||
|   | ||||
| @@ -62,7 +62,7 @@ public abstract class BusinessBaseWithCacheIntervalVariableModel<VarModel> : Bus | ||||
|             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.Group ?? string.Empty).ToDictionary(a => a.Key, a => a.Select(a => a.Value).ToList()); | ||||
|             VariableRuntimeGroups = IdVariableRuntimes.GroupBy(a => a.Value.BusinessGroup ?? string.Empty).ToDictionary(a => a.Key, a => a.Select(a => a.Value).ToList()); | ||||
|  | ||||
|         } | ||||
|         else | ||||
|   | ||||
| @@ -21,5 +21,5 @@ public abstract class VariablePropertyBase | ||||
|     /// 启用 | ||||
|     /// </summary> | ||||
|     [DynamicProperty()] | ||||
|     public bool Enable { get; set; } = true; | ||||
|     public virtual bool Enable { get; set; } = true; | ||||
| } | ||||
|   | ||||
| @@ -22,17 +22,17 @@ public static class DynamicModelExtension | ||||
|     /// <summary> | ||||
|     /// GetDynamicModel | ||||
|     /// </summary> | ||||
|     public static IEnumerable<dynamic> GetDynamicModel<T>(this IEnumerable<T> datas, string script) where T : class | ||||
|     public static IEnumerable<object> GetDynamicModel<T>(this IEnumerable<T> datas, string script) | ||||
|     { | ||||
|         if (!string.IsNullOrEmpty(script)) | ||||
|         { | ||||
|             //执行脚本,获取新实体 | ||||
|             var getDeviceModel = CSharpScriptEngineExtension.Do<IDynamicModel>(script); | ||||
|             return getDeviceModel.GetList(datas); | ||||
|             return getDeviceModel.GetList(datas.Cast<object>()); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             return datas; | ||||
|             return datas.Cast<object>(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -78,7 +78,7 @@ public static class DynamicModelExtension | ||||
|         return null; // 未找到对应的业务设备Id,返回null | ||||
|     } | ||||
|  | ||||
|     public static IEnumerable<IGrouping<object[], dynamic>> GroupByKeys(this IEnumerable<dynamic> values, IEnumerable<string> keys) | ||||
|     public static IEnumerable<IGrouping<object[], T>> GroupByKeys<T>(this IEnumerable<T> values, IEnumerable<string> keys) | ||||
|     { | ||||
|         // 获取动态对象集合中指定键的属性信息 | ||||
|         var properties = GetProperties(values, keys.ToArray()); | ||||
| @@ -87,7 +87,7 @@ public static class DynamicModelExtension | ||||
|         return values.GroupBy(v => properties.Select(property => property.GetValue(v)).ToArray(), new ArrayEqualityComparer()); | ||||
|     } | ||||
|  | ||||
|     private static PropertyInfo[] GetProperties(this IEnumerable<dynamic> value, params string[] names) | ||||
|     private static PropertyInfo[] GetProperties<T>(this IEnumerable<T> value, params string[] names) | ||||
|     { | ||||
|         // 获取动态对象集合的类型 | ||||
|         var type = value.GetType().GetGenericArguments().FirstOrDefault() ?? throw new ArgumentNullException(nameof(value)); | ||||
|   | ||||
| @@ -151,7 +151,7 @@ public class Channel : ChannelOptionsBase, IPrimaryIdEntity, IBaseDataEntity, IB | ||||
|     [SugarColumn(ColumnDescription = "连接超时", IsNullable = true, DefaultValue = "3000")] | ||||
|     [AutoGenerateColumn(Visible = true, Filterable = true, Sortable = true)] | ||||
|     [MinValue(100)] | ||||
|     public override ushort ConnectTimeout { get; set; } = 3000; | ||||
|     public override int ConnectTimeout { get; set; } = 3000; | ||||
|  | ||||
|     /// <summary> | ||||
|     /// 最大并发数 | ||||
|   | ||||
| @@ -63,7 +63,7 @@ public class Variable : BaseDataEntity, IValidatableObject | ||||
|     /// </summary> | ||||
|     [SugarColumn(ColumnDescription = "分组名称", IsNullable = true)] | ||||
|     [AutoGenerateColumn(Visible = true, Filterable = true, Sortable = true, Order = 1)] | ||||
|     public virtual string Group { get; set; } | ||||
|     public virtual string BusinessGroup { get; set; } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// 描述 | ||||
|   | ||||
| @@ -467,6 +467,8 @@ public static class GlobalData | ||||
|     /// <param name="deviceRuntime">设备运行时对象</param> | ||||
|     internal static void DeviceStatusChange(DeviceRuntime deviceRuntime) | ||||
|     { | ||||
|         deviceRuntime.Driver?.LogMessage?.LogInformation($"Status changed: {deviceRuntime.DeviceStatus}"); | ||||
|  | ||||
|         if (DeviceStatusChangeEvent != null) | ||||
|         { | ||||
|             // 触发设备状态变化事件,并将设备运行时对象转换为设备数据对象进行传递 | ||||
|   | ||||
| @@ -148,7 +148,8 @@ | ||||
|     "RawValue": "RawValue", | ||||
|     "Value": "Value", | ||||
|     "AlarmEnable": "AlarmEnable", | ||||
|  | ||||
|     "BusinessGroup": "BusinessGroup", | ||||
|     "CollectGroup": "CollectGroup", | ||||
|     "Name": "Name", | ||||
|     "Description": "Description", | ||||
|     "DeviceId": "CollectionDevice", | ||||
| @@ -400,7 +401,7 @@ | ||||
|     "Name": "Name", | ||||
|     "Name.Required": "{0} cannot be empty", | ||||
|     "Description": "Description", | ||||
|     "Group": "Group", | ||||
|     "BusinessGroup": "BusinessGroup", | ||||
|     "CollectGroup": "CollectGroup", | ||||
|     "DeviceId": "CollectionDevice", | ||||
|     "DeviceId.MinValue": "{0} cannot be empty", | ||||
|   | ||||
| @@ -144,7 +144,8 @@ | ||||
|     "RawValue": "原始值", | ||||
|     "Value": "实时值", | ||||
|     "AlarmEnable": "报警使能", | ||||
|  | ||||
|     "BusinessGroup": "业务组", | ||||
|     "CollectGroup": "采集组", | ||||
|     "Name": "名称", | ||||
|     "Description": "描述", | ||||
|     "DeviceId": "采集设备", | ||||
| @@ -419,7 +420,7 @@ | ||||
|     "Name": "名称", | ||||
|     "Name.Required": " {0} 不可为空", | ||||
|     "Description": "描述", | ||||
|     "Group": "业务组", | ||||
|     "BusinessGroup": "业务组", | ||||
|     "CollectGroup": "采集组", | ||||
|     "DeviceId": "采集设备", | ||||
|     "DeviceId.MinValue": " {0} 不可为空", | ||||
|   | ||||
| @@ -17,13 +17,17 @@ namespace ThingsGateway.Gateway.Application; | ||||
| /// <summary> | ||||
| /// 设备业务变化数据 | ||||
| /// </summary> | ||||
| public class DeviceBasicData : IPrimaryIdEntity | ||||
| public class DeviceBasicData | ||||
| { | ||||
|     [Newtonsoft.Json.JsonIgnore] | ||||
|     [System.Text.Json.Serialization.JsonIgnore] | ||||
|     public DeviceRuntime DeviceRuntime { get; set; } | ||||
|  | ||||
|     /// <inheritdoc cref="PrimaryIdEntity.Id"/> | ||||
|     public long Id { get; set; } | ||||
|  | ||||
|     /// <inheritdoc cref="Device.Name"/> | ||||
|     public string Name { get; set; } | ||||
|     public string Name => DeviceRuntime?.Name; | ||||
|  | ||||
|     /// <inheritdoc cref="DeviceRuntime.ActiveTime"/> | ||||
|     public DateTime ActiveTime { get; set; } | ||||
| @@ -37,37 +41,37 @@ public class DeviceBasicData : IPrimaryIdEntity | ||||
|     public string LastErrorMessage { get; set; } | ||||
|  | ||||
|     /// <inheritdoc cref="DeviceRuntime.PluginName"/> | ||||
|     public string PluginName { get; set; } | ||||
|     public string PluginName => DeviceRuntime?.PluginName; | ||||
|  | ||||
|     /// <inheritdoc cref="Device.Description"/> | ||||
|     [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] | ||||
|     [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] | ||||
|     public string? Description { get; set; } | ||||
|     public string? Description => DeviceRuntime?.Description; | ||||
|  | ||||
|     /// <inheritdoc cref="Device.Remark1"/> | ||||
|     [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] | ||||
|     [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] | ||||
|     public string Remark1 { get; set; } | ||||
|     public string Remark1 => DeviceRuntime?.Remark1; | ||||
|  | ||||
|     /// <inheritdoc cref="Device.Remark2"/> | ||||
|     [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] | ||||
|     [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] | ||||
|     public string Remark2 { get; set; } | ||||
|     public string Remark2 => DeviceRuntime?.Remark2; | ||||
|  | ||||
|     /// <inheritdoc cref="Device.Remark3"/> | ||||
|     [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] | ||||
|     [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] | ||||
|     public string Remark3 { get; set; } | ||||
|     public string Remark3 => DeviceRuntime?.Remark3; | ||||
|  | ||||
|     /// <inheritdoc cref="Device.Remark4"/> | ||||
|     [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] | ||||
|     [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] | ||||
|     public string Remark4 { get; set; } | ||||
|     public string Remark4 => DeviceRuntime?.Remark4; | ||||
|  | ||||
|     /// <inheritdoc cref="Device.Remark5"/> | ||||
|     [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] | ||||
|     [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] | ||||
|     public string Remark5 { get; set; } | ||||
|     public string Remark5 => DeviceRuntime?.Remark5; | ||||
|  | ||||
|  | ||||
| } | ||||
| @@ -75,23 +79,36 @@ public class DeviceBasicData : IPrimaryIdEntity | ||||
| /// <summary> | ||||
| /// 变量业务变化数据 | ||||
| /// </summary> | ||||
| public class VariableBasicData : IPrimaryIdEntity | ||||
| public class VariableBasicData | ||||
| { | ||||
|     [Newtonsoft.Json.JsonIgnore] | ||||
|     [System.Text.Json.Serialization.JsonIgnore] | ||||
|     public VariableRuntime VariableRuntime { get; set; } | ||||
|  | ||||
|     /// <inheritdoc cref="PrimaryIdEntity.Id"/> | ||||
|     public long Id { get; set; } | ||||
|     public long Id => VariableRuntime.Id; | ||||
|  | ||||
|     /// <inheritdoc cref="Variable.Name"/> | ||||
|     public string Name { get; set; } | ||||
|     /// <inheritdoc cref="Variable.Group"/> | ||||
|     public string Name => VariableRuntime.Name; | ||||
|  | ||||
|     /// <inheritdoc cref="Variable.CollectGroup"/> | ||||
|     [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] | ||||
|     [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] | ||||
|     public string Group { get; set; } | ||||
|     public string CollectGroup => VariableRuntime.CollectGroup; | ||||
|  | ||||
|     /// <inheritdoc cref="Variable.BusinessGroup"/> | ||||
|     [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] | ||||
|     [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] | ||||
|     public string BusinessGroup => VariableRuntime.BusinessGroup; | ||||
|  | ||||
|     /// <inheritdoc cref="VariableRuntime.DeviceName"/> | ||||
|     public string DeviceName { get; set; } | ||||
|     public string DeviceName => VariableRuntime.DeviceName; | ||||
|  | ||||
|     /// <inheritdoc cref="VariableRuntime.RuntimeType"/> | ||||
|     [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] | ||||
|     [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] | ||||
|     public string RuntimeType { get; set; } | ||||
|  | ||||
|     /// <inheritdoc cref="VariableRuntime.Value"/> | ||||
|     public object Value { get; set; } | ||||
|     /// <inheritdoc cref="VariableRuntime.RawValue"/> | ||||
| @@ -121,48 +138,48 @@ public class VariableBasicData : IPrimaryIdEntity | ||||
|     /// <inheritdoc cref="Variable.RegisterAddress"/> | ||||
|     [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] | ||||
|     [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] | ||||
|     public string? RegisterAddress { get; set; } | ||||
|     public string? RegisterAddress => VariableRuntime.RegisterAddress; | ||||
|  | ||||
|     /// <inheritdoc cref="Variable.Unit"/> | ||||
|     [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] | ||||
|     [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] | ||||
|     public string? Unit { get; set; } | ||||
|     public string? Unit => VariableRuntime.Unit; | ||||
|  | ||||
|     /// <inheritdoc cref="Variable.Description"/> | ||||
|     [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] | ||||
|     [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] | ||||
|     public string? Description { get; set; } | ||||
|     public string? Description => VariableRuntime.Description; | ||||
|  | ||||
|     /// <inheritdoc cref="Variable.ProtectType"/> | ||||
|     public ProtectTypeEnum ProtectType { get; set; } | ||||
|     public ProtectTypeEnum ProtectType => VariableRuntime.ProtectType; | ||||
|  | ||||
|     /// <inheritdoc cref="Variable.DataType"/> | ||||
|     public DataTypeEnum DataType { get; set; } | ||||
|     public DataTypeEnum DataType => VariableRuntime.DataType; | ||||
|  | ||||
|     /// <inheritdoc cref="Device.Remark1"/> | ||||
|     [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] | ||||
|     [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] | ||||
|     public string Remark1 { get; set; } | ||||
|     public string Remark1 => VariableRuntime.Remark1; | ||||
|  | ||||
|     /// <inheritdoc cref="Device.Remark2"/> | ||||
|     [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] | ||||
|     [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] | ||||
|     public string Remark2 { get; set; } | ||||
|     public string Remark2 => VariableRuntime.Remark2; | ||||
|  | ||||
|     /// <inheritdoc cref="Device.Remark3"/> | ||||
|     [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] | ||||
|     [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] | ||||
|     public string Remark3 { get; set; } | ||||
|     public string Remark3 => VariableRuntime.Remark3; | ||||
|  | ||||
|     /// <inheritdoc cref="Device.Remark4"/> | ||||
|     [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] | ||||
|     [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] | ||||
|     public string Remark4 { get; set; } | ||||
|     public string Remark4 => VariableRuntime.Remark4; | ||||
|  | ||||
|     /// <inheritdoc cref="Device.Remark5"/> | ||||
|     [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] | ||||
|     [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] | ||||
|     public string Remark5 { get; set; } | ||||
|     public string Remark5 => VariableRuntime.Remark5; | ||||
| } | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -22,5 +22,12 @@ public class VariableMapper : IRegister | ||||
|  | ||||
|         config.ForType<VariableRuntime, VariableRuntime>() | ||||
| .Ignore(dest => dest.DeviceRuntime); | ||||
|  | ||||
|         config.ForType<VariableRuntime, VariableBasicData>() | ||||
|             .BeforeMapping((a, b) => b.VariableRuntime = a); | ||||
|         config.ForType<DeviceRuntime, DeviceBasicData>() | ||||
|             .BeforeMapping((a, b) => b.DeviceRuntime = a); | ||||
|  | ||||
|  | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -87,18 +87,13 @@ public class VariableMethod | ||||
|             } | ||||
|  | ||||
|             dynamic result; | ||||
|             switch (MethodInfo.TaskType) | ||||
|             switch (MethodInfo.ReturnKind) | ||||
|             { | ||||
|                 case TaskReturnType.Task: | ||||
|                     await MethodInfo.InvokeAsync(driverBase, os).ConfigureAwait(false); | ||||
|                     result = OperResult.Success; | ||||
|                 case MethodReturnKind.Awaitable: | ||||
|                 case MethodReturnKind.AwaitableObject: | ||||
|                     result = await MethodInfo.InvokeAsync(driverBase, os).ConfigureAwait(false); | ||||
|                     break; | ||||
|  | ||||
|                 case TaskReturnType.TaskObject: | ||||
|                     result = await MethodInfo.InvokeObjectAsync(driverBase, os).ConfigureAwait(false); | ||||
|                     break; | ||||
|  | ||||
|                 case TaskReturnType.None: | ||||
|                 default: | ||||
|                     result = MethodInfo.Invoke(driverBase, os); | ||||
|                     break; | ||||
|   | ||||
| @@ -32,7 +32,10 @@ public class VariableRuntime : Variable, IVariable, IDisposable | ||||
|     private bool? _isOnlineChanged; | ||||
|     protected object? _value; | ||||
|  | ||||
|     public VariableRuntime() | ||||
|     { | ||||
|  | ||||
|     } | ||||
|     /// <summary> | ||||
|     /// 变化时间 | ||||
|     /// </summary> | ||||
|   | ||||
| @@ -8,8 +8,8 @@ | ||||
| 	<ItemGroup> | ||||
| 		<PackageReference Include="Portable.BouncyCastle" Version="1.9.0" /> | ||||
| 		<PackageReference Include="Rougamo.Fody" Version="5.0.0" /> | ||||
| 		<PackageReference Include="TouchSocket.Dmtp" Version="3.1.2" /> | ||||
| 		<PackageReference Include="TouchSocket.WebApi.Swagger" Version="3.1.2" /> | ||||
| 		<PackageReference Include="TouchSocket.Dmtp" Version="3.1.3" /> | ||||
| 		<PackageReference Include="TouchSocket.WebApi.Swagger" Version="3.1.3" /> | ||||
| 		<PackageReference Include="ThingsGateway.Authentication" Version="$(AuthenticationVersion)" /> | ||||
|  | ||||
| 	</ItemGroup> | ||||
|   | ||||
| @@ -36,92 +36,18 @@ public partial class PropertyComponent : IPropertyUIBase | ||||
|  | ||||
|     private async Task CheckScript(BusinessPropertyWithCacheIntervalScript businessProperty, string pname) | ||||
|     { | ||||
|         IEnumerable<object> data = null; | ||||
|         string script = null; | ||||
|         if (pname == nameof(BusinessPropertyWithCacheIntervalScript.BigTextScriptAlarmModel)) | ||||
|         { | ||||
|             data = new List<AlarmVariable>() { new() { | ||||
|                 Name = "testName", | ||||
|                 DeviceName = "testDevice", | ||||
|                 AlarmCode = "1", | ||||
|                 AlarmTime = DateTime.Now, | ||||
|                 EventTime = DateTime.Now, | ||||
|                 AlarmLimit = "3", | ||||
|                 AlarmType = AlarmTypeEnum.L, | ||||
|                 EventType=EventTypeEnum.Alarm, | ||||
|                 Remark1="1", | ||||
|                 Remark2="2", | ||||
|                 Remark3="3", | ||||
|                 Remark4="4", | ||||
|                 Remark5="5", | ||||
|             }, | ||||
|              new() { | ||||
|                 Name = "testName2", | ||||
|                 DeviceName = "testDevice", | ||||
|                 AlarmCode = "1", | ||||
|                 AlarmTime = DateTime.Now, | ||||
|                 EventTime = DateTime.Now, | ||||
|                 AlarmLimit = "3", | ||||
|                 AlarmType = AlarmTypeEnum.L, | ||||
|                 EventType=EventTypeEnum.Alarm, | ||||
|                 Remark1="1", | ||||
|                 Remark2="2", | ||||
|                 Remark3="3", | ||||
|                 Remark4="4", | ||||
|                 Remark5="5", | ||||
|             }}; | ||||
|             script = businessProperty.BigTextScriptAlarmModel; | ||||
|         } | ||||
|         else if (pname == nameof(BusinessPropertyWithCacheIntervalScript.BigTextScriptVariableModel)) | ||||
|         { | ||||
|             data = new List<VariableBasicData>() { new() { | ||||
|                 Name = "testName", | ||||
|                 DeviceName = "testDevice", | ||||
|                 Value = "1", | ||||
|                 ChangeTime = DateTime.Now, | ||||
|                 CollectTime = DateTime.Now, | ||||
|                 Remark1="1", | ||||
|                 Remark2="2", | ||||
|                 Remark3="3", | ||||
|                 Remark4="4", | ||||
|                 Remark5="5", | ||||
|             } , | ||||
|              new() { | ||||
|                 Name = "testName2", | ||||
|                 DeviceName = "testDevice", | ||||
|                 Value = "1", | ||||
|                 ChangeTime = DateTime.Now, | ||||
|                 CollectTime = DateTime.Now, | ||||
|                 Remark1="1", | ||||
|                 Remark2="2", | ||||
|                 Remark3="3", | ||||
|                 Remark4="4", | ||||
|                 Remark5="5", | ||||
|             } }; | ||||
|             script = businessProperty.BigTextScriptVariableModel; | ||||
|  | ||||
|         } | ||||
|         else if (pname == nameof(BusinessPropertyWithCacheIntervalScript.BigTextScriptDeviceModel)) | ||||
|         { | ||||
|             data = new List<DeviceBasicData>() { new() { | ||||
|                 Name = "testDevice", | ||||
|                 ActiveTime = DateTime.Now, | ||||
|                 Remark1="1", | ||||
|                 Remark2="2", | ||||
|                 Remark3="3", | ||||
|                 Remark4="4", | ||||
|                 Remark5="5", | ||||
|             } , | ||||
|             new() { | ||||
|                 Name = "testDevice2", | ||||
|                 ActiveTime = DateTime.Now, | ||||
|                 Remark1="1", | ||||
|                 Remark2="2", | ||||
|                 Remark3="3", | ||||
|                 Remark4="4", | ||||
|                 Remark5="5", | ||||
|             }}; | ||||
|  | ||||
|             script = businessProperty.BigTextScriptDeviceModel; | ||||
|         } | ||||
|         else | ||||
| @@ -141,7 +67,7 @@ public partial class PropertyComponent : IPropertyUIBase | ||||
|  | ||||
|         op.Component = BootstrapDynamicComponent.CreateComponent<ScriptCheck>(new Dictionary<string, object?> | ||||
|     { | ||||
|         {nameof(ScriptCheck.Data),data }, | ||||
|         {nameof(ScriptCheck.Data),Array.Empty<object>() }, | ||||
|         {nameof(ScriptCheck.Script),script }, | ||||
|         {nameof(ScriptCheck.OnGetDemo),()=> | ||||
|                 { | ||||
|   | ||||
| @@ -45,7 +45,7 @@ | ||||
|  | ||||
|                     <EditorItem @bind-Field="@context.Description" /> | ||||
|                     <EditorItem @bind-Field="@context.CollectGroup" /> | ||||
|                     <EditorItem @bind-Field="@context.Group" /> | ||||
|                     <EditorItem @bind-Field="@context.BusinessGroup" /> | ||||
|  | ||||
|                     <EditorItem @bind-Field="@context.Unit" /> | ||||
|                     <EditorItem @bind-Field="@context.ProtectType" /> | ||||
|   | ||||
| @@ -33,7 +33,7 @@ | ||||
|         <TableColumn Field="@context.DeviceName" FieldExpression=@(()=>context.DeviceName) ShowTips=true Filterable=true Sortable=true Visible=true /> | ||||
|         <TableColumn @bind-Field="@context.Name" ShowTips=true Filterable=true Sortable=true Visible=true /> | ||||
|         <TableColumn @bind-Field="@context.Description" ShowTips=true Filterable=true Sortable=true Visible=true /> | ||||
|         <TableColumn @bind-Field="@context.Group" ShowTips=true Filterable=true Sortable=true Visible=true /> | ||||
|         <TableColumn @bind-Field="@context.BusinessGroup" ShowTips=true Filterable=true Sortable=true Visible=true /> | ||||
|  | ||||
|         <TableColumn @bind-Field="@context.Enable" Filterable=true Sortable=true Visible="false" /> | ||||
|         <TableColumn Field="@context.ChangeTime" ShowTips=true FieldExpression=@(()=>context.ChangeTime) Filterable=true Sortable=true Visible=false /> | ||||
|   | ||||
| @@ -16,7 +16,6 @@ namespace ThingsGateway.Management; | ||||
|  | ||||
| public class DeviceDataWithValue | ||||
| { | ||||
|     public long Id { get; set; } | ||||
|     /// <inheritdoc cref="Device.Name"/> | ||||
|     public string Name { get; set; } | ||||
|  | ||||
| @@ -34,3 +33,17 @@ public class DeviceDataWithValue | ||||
|     /// <inheritdoc cref="DeviceRuntime.ReadOnlyVariableRuntimes"/> | ||||
|     public Dictionary<string, VariableDataWithValue> ReadOnlyVariableRuntimes { get; set; } | ||||
| } | ||||
|  | ||||
|  | ||||
|  | ||||
| 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; } | ||||
| } | ||||
| @@ -44,6 +44,7 @@ | ||||
|     "RedundancyDisable": "Redundant gateway site not enabled" | ||||
|   }, | ||||
|   "ThingsGateway.Management.RedundancyOptions": { | ||||
|     "ForcedSync": "Forced Synchronous", | ||||
|     "Enable": "Enable Dual-Machine Redundancy", | ||||
|     "MasterUri": "Master Node URL", | ||||
|     "IsMaster": "IsMaster", | ||||
| @@ -59,8 +60,9 @@ | ||||
|     "Switch": "Switch", | ||||
|  | ||||
|     "Restart": "The redundant service will be restarted soon", | ||||
|     "Confirm": "Confirm switching to redundant state" | ||||
|     "Confirm": "Confirm switching to redundant state", | ||||
|  | ||||
|     "ForcedSyncWarning": "Forcing synchronization will generate database configuration information.Are you sure you want to continue?" | ||||
|  | ||||
|   }, | ||||
|  | ||||
|   | ||||
| @@ -46,7 +46,7 @@ | ||||
|   }, | ||||
|  | ||||
|   "ThingsGateway.Management.RedundancyOptions": { | ||||
|  | ||||
|     "ForcedSync": "强制同步", | ||||
|     "Enable": "启用双机冗余", | ||||
|     "MasterUri": "主站Url", | ||||
|     "IsMaster": "是否为主站", | ||||
| @@ -62,7 +62,8 @@ | ||||
|     "Switch": "切换", | ||||
|  | ||||
|     "Restart": "即将重新启动冗余服务", | ||||
|     "Confirm": "确认切换冗余状态" | ||||
|     "Confirm": "确认切换冗余状态", | ||||
|     "ForcedSyncWarning": "强制同步会生成数据库配置信息,是否继续?" | ||||
|   }, | ||||
|  | ||||
|   "ThingsGateway.Management._Imports": { | ||||
|   | ||||
| @@ -17,6 +17,8 @@ internal interface IRedundancyHostedService : IHostedService | ||||
| { | ||||
|     Task<OperResult> StartRedundancyTaskAsync(); | ||||
|     Task StopRedundancyTaskAsync(); | ||||
|     ValueTask ForcedSync(CancellationToken cancellationToken = default); | ||||
|  | ||||
|     public TextFileLogger TextLogger { get; } | ||||
|     public string LogPath { get; } | ||||
| } | ||||
|   | ||||
| @@ -13,6 +13,8 @@ using Mapster; | ||||
| using Microsoft.Extensions.Hosting; | ||||
| using Microsoft.Extensions.Logging; | ||||
|  | ||||
| using System.Threading; | ||||
|  | ||||
| using ThingsGateway.Gateway.Application; | ||||
| using ThingsGateway.NewLife; | ||||
|  | ||||
| @@ -268,6 +270,59 @@ internal sealed class RedundancyHostedService : BackgroundService, IRedundancyHo | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|  | ||||
|     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.GetGatewayData), 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); | ||||
|  | ||||
|                     _log?.LogTrace($"ForcedSync data success"); | ||||
|  | ||||
|                 } | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 // 输出警告日志,指示同步数据到从站时发生错误 | ||||
|                 _log?.LogWarning(ex, "ForcedSync data error"); | ||||
|             } | ||||
|         } | ||||
|         catch (OperationCanceledException) | ||||
|         { | ||||
|         } | ||||
|         catch (ObjectDisposedException) | ||||
|         { | ||||
|         } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             _log?.LogWarning(ex, "Execute"); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|     private WaitLock _switchLock = new(); | ||||
|  | ||||
|     /// <inheritdoc/> | ||||
| @@ -326,6 +381,7 @@ internal sealed class RedundancyHostedService : BackgroundService, IRedundancyHo | ||||
|             if (RedundancyOptions.IsMaster) | ||||
|             { | ||||
|                 TcpDmtpService = await GetTcpDmtpService(RedundancyOptions).ConfigureAwait(false); | ||||
|  | ||||
|                 await TcpDmtpService.StartAsync().ConfigureAwait(false);//启动 | ||||
|                 await ActiveAsync().ConfigureAwait(false); | ||||
|             } | ||||
|   | ||||
| @@ -39,4 +39,34 @@ public partial class ReverseCallbackServer : SingletonRpcServer | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|  | ||||
|     [DmtpRpc(MethodInvoke = true)] | ||||
|     public List<DataWithDatabase> GetGatewayData() | ||||
|     { | ||||
|         List<DataWithDatabase> dataWithDatabases = new(); | ||||
|         foreach (var channels in GlobalData.ReadOnlyChannels) | ||||
|         { | ||||
|             DataWithDatabase dataWithDatabase = new(); | ||||
|             dataWithDatabase.Channel = channels.Value; | ||||
|             dataWithDatabase.DeviceVariables = new(); | ||||
|             foreach (var devices in channels.Value.ReadDeviceRuntimes) | ||||
|             { | ||||
|                 DeviceDataWithDatabase deviceDataWithDatabase = new(); | ||||
|  | ||||
|                 deviceDataWithDatabase.Device = devices.Value; | ||||
|                 deviceDataWithDatabase.Variables = devices.Value.ReadOnlyVariableRuntimes.Select(a => a.Value).Cast<Variable>().ToList(); | ||||
|  | ||||
|  | ||||
|                 dataWithDatabase.DeviceVariables.Add(deviceDataWithDatabase); | ||||
|             } | ||||
|  | ||||
|             dataWithDatabases.Add(dataWithDatabase); | ||||
|         } | ||||
|         return dataWithDatabases; | ||||
|  | ||||
|  | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -15,6 +15,7 @@ | ||||
|  | ||||
|         <EditComponent ItemsPerRow=1 Model="Model" OnSave="OnSaveRedundancy" /> | ||||
|  | ||||
|         <Button IsDisabled=@(Model.IsMaster) OnClick="ForcedSync">@RedundancyLocalizer["ForcedSync"]</Button> | ||||
|     </div> | ||||
|     <div class="col-12 col-md-6 h-100"> | ||||
|  | ||||
|   | ||||
| @@ -12,6 +12,7 @@ | ||||
| using Mapster; | ||||
|  | ||||
| using Microsoft.AspNetCore.Components.Forms; | ||||
| using Microsoft.AspNetCore.Components.Web; | ||||
|  | ||||
| using TouchSocket.Core; | ||||
|  | ||||
| @@ -82,6 +83,8 @@ public partial class RedundancyOptionsPage | ||||
|                 else | ||||
|                     await ToastService.Warning(RedundancyLocalizer[nameof(RedundancyOptions)], $"{RazorLocalizer["Fail", result.ToString()]}"); | ||||
|  | ||||
|  | ||||
|                 await InvokeAsync(StateHasChanged); | ||||
|             } | ||||
|  | ||||
|         } | ||||
| @@ -92,4 +95,16 @@ public partial class RedundancyOptionsPage | ||||
|     } | ||||
|  | ||||
|  | ||||
|     private async Task ForcedSync(MouseEventArgs args) | ||||
|     { | ||||
|         var ret = await SwalService.ShowModal(new SwalOption() | ||||
|         { | ||||
|             Category = SwalCategory.Warning, | ||||
|             Title = RedundancyLocalizer["ForcedSyncWarning"] | ||||
|         }); | ||||
|         if (ret) | ||||
|         { | ||||
|             await RedundancyHostedService.ForcedSync(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -14,56 +14,62 @@ public class ExecuteScriptNode : TextNode, IActuatorNode, IExexcuteExpressionsBa | ||||
|         Title = "ExecuteScriptNode"; Placeholder = "ExecuteScriptNode.Placeholder"; | ||||
|         Text = | ||||
|             """ | ||||
|             using ThingsGateway.RulesEngine; | ||||
|             using ThingsGateway.Foundation; | ||||
|             using TouchSocket.Core; | ||||
|             using System.Text; | ||||
|              | ||||
|             public class TestEx : IExexcuteExpressions | ||||
|             using System.Text; | ||||
|             using ThingsGateway.Gateway.Application; | ||||
|             using ThingsGateway.RulesEngine; | ||||
|  | ||||
|  | ||||
|             public class TestExexcuteExpressions : IExexcuteExpressions | ||||
|             { | ||||
|  | ||||
|                 public TouchSocket.Core.ILog Logger { get; set; } | ||||
|  | ||||
|                 public async System.Threading.Tasks.Task<NodeOutput> ExecuteAsync(NodeInput input, System.Threading.CancellationToken cancellationToken) | ||||
|                 { | ||||
|  | ||||
|  | ||||
|                     //想上传mqtt,可以自己写mqtt上传代码,或者通过mqtt插件的公开方法上传 | ||||
|  | ||||
|                     //直接获取mqttclient插件类型的第一个设备 | ||||
|                     var driver = GlobalData.ReadOnlyChannels.FirstOrDefault(a => a.Value.PluginName == "ThingsGateway.Plugin.Mqtt.MqttClient").Value?.ReadDeviceRuntimes?.FirstOrDefault().Value?.Driver; | ||||
|                     if (driver != null) | ||||
|                     { | ||||
|                         //找到对应的MqttClient插件设备 | ||||
|                         var mqttClient = (ThingsGateway.Plugin.Mqtt.MqttClient)driver; | ||||
|                     var mqttClient = GlobalData.ReadOnlyChannels.FirstOrDefault(a => a.Value.PluginName == "ThingsGateway.Plugin.Mqtt.MqttClient").Value?.ReadDeviceRuntimes?.FirstOrDefault().Value?.Driver as ThingsGateway.Plugin.Mqtt.MqttClient; | ||||
|                     if (mqttClient == null) | ||||
|                         throw new("mqttClient NOT FOUND"); | ||||
|                         var result = await mqttClient.MqttUpAsync("test", Encoding.UTF8.GetBytes("test"),1, default);// 主题 和 负载 | ||||
|  | ||||
|                     TopicArray topicArray = new() | ||||
|                     { | ||||
|                         Topic = "test", | ||||
|                         Json = Encoding.UTF8.GetBytes("test") | ||||
|                     }; | ||||
|                     var result = await mqttClient.MqttUpAsync(topicArray, default).ConfigureAwait(false);// 主题 和 负载 | ||||
|                     if (!result.IsSuccess) | ||||
|                         throw new(result.ErrorMessage); | ||||
|                     return new NodeOutput() { Value = result }; | ||||
|                     } | ||||
|                     throw new("mqttClient NOT FOUND"); | ||||
|  | ||||
|  | ||||
|                     //通过设备名称找出mqttClient插件 | ||||
|                     //var driver = GlobalData.ReadOnlyDevices.FirstOrDefault(a => a.Value.Name == "mqttDevice1").Value?.Driver; | ||||
|                     //if (driver != null) | ||||
|                     //{ | ||||
|                     //    //找到对应的MqttClient插件设备 | ||||
|                     //    var mqttClient = (ThingsGateway.Plugin.Mqtt.MqttClient)driver; | ||||
|                     //    if (mqttClient == null) | ||||
|                     //var mqttClient = GlobalData.ReadOnlyDevices.FirstOrDefault(a => a.Value.Name == "mqttDevice1").Value?.Driver as ThingsGateway.Plugin.Mqtt.MqttClient; | ||||
|  | ||||
|                     //if (mqttClient == null) | ||||
|                     //    throw new("mqttClient NOT FOUND"); | ||||
|                     //    var result = await mqttClient.MqttUpAsync("test", "test", default);// 主题 和 负载 | ||||
|                     //    if (!result.IsSuccess) | ||||
|  | ||||
|  | ||||
|                     //TopicArray topicArray = new() | ||||
|                     //{ | ||||
|                     //    Topic = "test", | ||||
|                     //    Json = Encoding.UTF8.GetBytes("test") | ||||
|                     //}; | ||||
|                     //var result = await mqttClient.MqttUpAsync(topicArray, default).ConfigureAwait(false);// 主题 和 负载 | ||||
|                     //if (!result.IsSuccess) | ||||
|                     //    throw new(result.ErrorMessage); | ||||
|                     //    return new NodeOutput() { Value = result }; | ||||
|                     //} | ||||
|                     //throw new("mqttClient NOT FOUND"); | ||||
|                     //return new NodeOutput() { Value = result }; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|  | ||||
|  | ||||
|              | ||||
|              | ||||
|  | ||||
|             """; | ||||
|  | ||||
|     } | ||||
|   | ||||
| @@ -401,6 +401,9 @@ public partial class SiemensS7Master : DeviceBase | ||||
|                 var result2 = await SendThenReturnMessageBaseAsync(new S7Send(ISO_CR), channel).ConfigureAwait(false); | ||||
|                 if (!result2.IsSuccess) | ||||
|                 { | ||||
|                     if (result2.Exception is OperationCanceledException) | ||||
|                         return true; | ||||
|  | ||||
|                     Logger?.LogWarning(SiemensS7Resource.Localizer["HandshakeError1", channel.ToString(), result2]); | ||||
|                     await channel.CloseAsync().ConfigureAwait(false); | ||||
|                     return true; | ||||
| @@ -418,6 +421,9 @@ public partial class SiemensS7Master : DeviceBase | ||||
|                 var result2 = await SendThenReturnMessageBaseAsync(new S7Send(S7_PN), channel).ConfigureAwait(false); | ||||
|                 if (!result2.IsSuccess) | ||||
|                 { | ||||
|                     if (result2.Exception is OperationCanceledException) | ||||
|                         return true; | ||||
|  | ||||
|                     Logger?.LogWarning(SiemensS7Resource.Localizer["HandshakeError2", channel.ToString(), result2]); | ||||
|                     await channel.CloseAsync().ConfigureAwait(false); | ||||
|                     return true; | ||||
|   | ||||
| @@ -51,8 +51,8 @@ public partial class QuestDBProducer : BusinessBaseWithCacheIntervalVariableMode | ||||
|     { | ||||
|         if (_driverPropertys.GroupUpdate) | ||||
|         { | ||||
|             var varList = variables.Where(a => a.Group.IsNullOrEmpty()); | ||||
|             var varGroup = variables.Where(a => !a.Group.IsNullOrEmpty()).GroupBy(a => a.Group); | ||||
|             var varList = variables.Where(a => a.BusinessGroup.IsNullOrEmpty()); | ||||
|             var varGroup = variables.Where(a => !a.BusinessGroup.IsNullOrEmpty()).GroupBy(a => a.BusinessGroup); | ||||
|  | ||||
|             foreach (var group in varGroup) | ||||
|             { | ||||
| @@ -75,7 +75,7 @@ public partial class QuestDBProducer : BusinessBaseWithCacheIntervalVariableMode | ||||
|  | ||||
|     private void UpdateVariable(VariableRuntime variableRuntime, VariableBasicData variable) | ||||
|     { | ||||
|         if (_driverPropertys.GroupUpdate && !variable.Group.IsNullOrEmpty() && VariableRuntimeGroups.TryGetValue(variable.Group, out var variableRuntimeGroup)) | ||||
|         if (_driverPropertys.GroupUpdate && !variable.BusinessGroup.IsNullOrEmpty() && VariableRuntimeGroups.TryGetValue(variable.BusinessGroup, out var variableRuntimeGroup)) | ||||
|         { | ||||
|  | ||||
|             AddQueueVarModel(new CacheDBItem<List<QuestDBHistoryValue>>(variableRuntimeGroup.Adapt<List<QuestDBHistoryValue>>(_config))); | ||||
|   | ||||
| @@ -46,33 +46,8 @@ namespace ThingsGateway.Debug | ||||
|  | ||||
|         private async Task CheckScript(SqlDBProducerProperty businessProperty, string pname) | ||||
|         { | ||||
|             IEnumerable<object> data = null; | ||||
|             string script = null; | ||||
|             { | ||||
|                 data = new List<VariableBasicData>() { new() { | ||||
|                 Name = "testName", | ||||
|                 DeviceName = "testDevice", | ||||
|                 Value = "1", | ||||
|                 ChangeTime = DateTime.Now, | ||||
|                 CollectTime = DateTime.Now, | ||||
|                 Remark1="1", | ||||
|                 Remark2="2", | ||||
|                 Remark3="3", | ||||
|                 Remark4="4", | ||||
|                 Remark5="5", | ||||
|             } , | ||||
|              new() { | ||||
|                 Name = "testName2", | ||||
|                 DeviceName = "testDevice", | ||||
|                 Value = "1", | ||||
|                 ChangeTime = DateTime.Now, | ||||
|                 CollectTime = DateTime.Now, | ||||
|                 Remark1="1", | ||||
|                 Remark2="2", | ||||
|                 Remark3="3", | ||||
|                 Remark4="4", | ||||
|                 Remark5="5", | ||||
|             } }; | ||||
|                 script = pname == businessProperty.BigTextScriptHistoryTable ? businessProperty.BigTextScriptHistoryTable : businessProperty.BigTextScriptRealTable; | ||||
|  | ||||
|             } | ||||
| @@ -90,7 +65,7 @@ namespace ThingsGateway.Debug | ||||
|  | ||||
|             op.Component = BootstrapDynamicComponent.CreateComponent<ScriptCheck>(new Dictionary<string, object?> | ||||
|     { | ||||
|         {nameof(ScriptCheck.Data),data }, | ||||
|         {nameof(ScriptCheck.Data),Array.Empty < object >() }, | ||||
|         {nameof(ScriptCheck.Script),script }, | ||||
|         {nameof(ScriptCheck.OnGetDemo),()=> | ||||
|                 { | ||||
|   | ||||
| @@ -217,7 +217,7 @@ public partial class SqlDBProducer : BusinessBaseWithCacheIntervalVariableModel< | ||||
|                     var varList = IdVariableRuntimes.Select(a => a.Value); | ||||
|                     if (_driverPropertys.GroupUpdate) | ||||
|                     { | ||||
|                         var groups = varList.GroupBy(a => a.Group); | ||||
|                         var groups = varList.GroupBy(a => a.BusinessGroup); | ||||
|                         foreach (var item in groups) | ||||
|                         { | ||||
|                             var result = await UpdateAsync(item.Adapt<List<SQLRealValue>>(), cancellationToken).ConfigureAwait(false); | ||||
|   | ||||
| @@ -54,8 +54,8 @@ public partial class SqlDBProducer : BusinessBaseWithCacheIntervalVariableModel< | ||||
|     { | ||||
|         if (_driverPropertys.GroupUpdate) | ||||
|         { | ||||
|             var varList = variables.Where(a => a.Group.IsNullOrEmpty()); | ||||
|             var varGroup = variables.Where(a => !a.Group.IsNullOrEmpty()).GroupBy(a => a.Group); | ||||
|             var varList = variables.Where(a => a.BusinessGroup.IsNullOrEmpty()); | ||||
|             var varGroup = variables.Where(a => !a.BusinessGroup.IsNullOrEmpty()).GroupBy(a => a.BusinessGroup); | ||||
|  | ||||
|             foreach (var group in varGroup) | ||||
|             { | ||||
| @@ -79,7 +79,7 @@ public partial class SqlDBProducer : BusinessBaseWithCacheIntervalVariableModel< | ||||
|     { | ||||
|         if (_driverPropertys.IsHistoryDB) | ||||
|         { | ||||
|             if (_driverPropertys.GroupUpdate && !variable.Group.IsNullOrEmpty() && VariableRuntimeGroups.TryGetValue(variable.Group, out var variableRuntimeGroup)) | ||||
|             if (_driverPropertys.GroupUpdate && !variable.BusinessGroup.IsNullOrEmpty() && VariableRuntimeGroups.TryGetValue(variable.BusinessGroup, out var variableRuntimeGroup)) | ||||
|             { | ||||
|  | ||||
|                 AddQueueVarModel(new CacheDBItem<List<SQLHistoryValue>>(variableRuntimeGroup.Adapt<List<SQLHistoryValue>>(_config))); | ||||
|   | ||||
| @@ -55,8 +55,8 @@ public partial class TDengineDBProducer : BusinessBaseWithCacheIntervalVariableM | ||||
|     { | ||||
|         if (_driverPropertys.GroupUpdate) | ||||
|         { | ||||
|             var varList = variables.Where(a => a.Group.IsNullOrEmpty()); | ||||
|             var varGroup = variables.Where(a => !a.Group.IsNullOrEmpty()).GroupBy(a => a.Group); | ||||
|             var varList = variables.Where(a => a.BusinessGroup.IsNullOrEmpty()); | ||||
|             var varGroup = variables.Where(a => !a.BusinessGroup.IsNullOrEmpty()).GroupBy(a => a.BusinessGroup); | ||||
|  | ||||
|             foreach (var group in varGroup) | ||||
|             { | ||||
| @@ -78,7 +78,7 @@ public partial class TDengineDBProducer : BusinessBaseWithCacheIntervalVariableM | ||||
|  | ||||
|     private void UpdateVariable(VariableRuntime variableRuntime, VariableBasicData variable) | ||||
|     { | ||||
|         if (_driverPropertys.GroupUpdate && !variable.Group.IsNullOrEmpty() && VariableRuntimeGroups.TryGetValue(variable.Group, out var variableRuntimeGroup)) | ||||
|         if (_driverPropertys.GroupUpdate && !variable.BusinessGroup.IsNullOrEmpty() && VariableRuntimeGroups.TryGetValue(variable.BusinessGroup, out var variableRuntimeGroup)) | ||||
|         { | ||||
|  | ||||
|             AddQueueVarModel(new CacheDBItem<List<TDengineDBHistoryValue>>(variableRuntimeGroup.Adapt<List<TDengineDBHistoryValue>>(_config))); | ||||
|   | ||||
| @@ -91,8 +91,8 @@ public partial class Webhook : BusinessBaseWithCacheIntervalScript<VariableBasic | ||||
|         { | ||||
|             if (_driverPropertys.GroupUpdate) | ||||
|             { | ||||
|                 var varList = variables.Where(a => a.Group.IsNullOrEmpty()); | ||||
|                 var varGroup = variables.Where(a => !a.Group.IsNullOrEmpty()).GroupBy(a => a.Group); | ||||
|                 var varList = variables.Where(a => a.BusinessGroup.IsNullOrEmpty()); | ||||
|                 var varGroup = variables.Where(a => !a.BusinessGroup.IsNullOrEmpty()).GroupBy(a => a.BusinessGroup); | ||||
|  | ||||
|                 foreach (var group in varGroup) | ||||
|                 { | ||||
| @@ -118,7 +118,7 @@ public partial class Webhook : BusinessBaseWithCacheIntervalScript<VariableBasic | ||||
|     { | ||||
|         if (!_businessPropertyWithCacheIntervalScript.VariableTopic.IsNullOrWhiteSpace()) | ||||
|         { | ||||
|             if (_driverPropertys.GroupUpdate && !variable.Group.IsNullOrEmpty() && VariableRuntimeGroups.TryGetValue(variable.Group, out var variableRuntimeGroup)) | ||||
|             if (_driverPropertys.GroupUpdate && !variable.BusinessGroup.IsNullOrEmpty() && VariableRuntimeGroups.TryGetValue(variable.BusinessGroup, out var variableRuntimeGroup)) | ||||
|             { | ||||
|  | ||||
|                 AddQueueVarModel(new CacheDBItem<List<VariableBasicData>>(variableRuntimeGroup.Adapt<List<VariableBasicData>>())); | ||||
|   | ||||
| @@ -86,8 +86,8 @@ public partial class KafkaProducer : BusinessBaseWithCacheIntervalScript<Variabl | ||||
|         { | ||||
|             if (_driverPropertys.GroupUpdate) | ||||
|             { | ||||
|                 var varList = variables.Where(a => a.Group.IsNullOrEmpty()); | ||||
|                 var varGroup = variables.Where(a => !a.Group.IsNullOrEmpty()).GroupBy(a => a.Group); | ||||
|                 var varList = variables.Where(a => a.BusinessGroup.IsNullOrEmpty()); | ||||
|                 var varGroup = variables.Where(a => !a.BusinessGroup.IsNullOrEmpty()).GroupBy(a => a.BusinessGroup); | ||||
|  | ||||
|                 foreach (var group in varGroup) | ||||
|                 { | ||||
| @@ -112,7 +112,7 @@ public partial class KafkaProducer : BusinessBaseWithCacheIntervalScript<Variabl | ||||
|     { | ||||
|         if (!_businessPropertyWithCacheIntervalScript.VariableTopic.IsNullOrWhiteSpace()) | ||||
|         { | ||||
|             if (_driverPropertys.GroupUpdate && !variable.Group.IsNullOrEmpty() && VariableRuntimeGroups.TryGetValue(variable.Group, out var variableRuntimeGroup)) | ||||
|             if (_driverPropertys.GroupUpdate && !variable.BusinessGroup.IsNullOrEmpty() && VariableRuntimeGroups.TryGetValue(variable.BusinessGroup, out var variableRuntimeGroup)) | ||||
|             { | ||||
|  | ||||
|                 AddQueueVarModel(new CacheDBItem<List<VariableBasicData>>(variableRuntimeGroup.Adapt<List<VariableBasicData>>())); | ||||
|   | ||||
| @@ -127,7 +127,7 @@ public class ModbusSlave : BusinessBase | ||||
|         _modbusVariableQueue?.Clear(); | ||||
|         IdVariableRuntimes.ForEach(a => | ||||
|         { | ||||
|             VariableValueChange(a.Value, null); | ||||
|             VariableValueChange(a.Value, default); | ||||
|         }); | ||||
|  | ||||
|         ModbusVariables = IdVariableRuntimes.ToDictionary(a => | ||||
|   | ||||
| @@ -50,7 +50,8 @@ | ||||
|     "UserName": "UserName", | ||||
|     "Password": "Password", | ||||
|     "ConnectId": "ConnectId", | ||||
|     "ConnectTimeout": "ConnectTimeout" | ||||
|     "ConnectTimeout": "ConnectTimeout", | ||||
|     "CheckClearTime": "VariableCheckClearTime(s)" | ||||
|  | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -51,7 +51,8 @@ | ||||
|     "UserName": "用户名", | ||||
|     "Password": "密码", | ||||
|     "ConnectId": "连接ID", | ||||
|     "ConnectTimeout": "连接超时时间" | ||||
|     "ConnectTimeout": "连接超时时间", | ||||
|     "CheckClearTime": "变量过期时间(s)" | ||||
|  | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -144,8 +144,8 @@ public partial class MqttClient : BusinessBaseWithCacheIntervalScript<VariableBa | ||||
|         { | ||||
|             if (_driverPropertys.GroupUpdate) | ||||
|             { | ||||
|                 var varList = variables.Where(a => a.Group.IsNullOrEmpty()); | ||||
|                 var varGroup = variables.Where(a => !a.Group.IsNullOrEmpty()).GroupBy(a => a.Group); | ||||
|                 var varList = variables.Where(a => a.BusinessGroup.IsNullOrEmpty()); | ||||
|                 var varGroup = variables.Where(a => !a.BusinessGroup.IsNullOrEmpty()).GroupBy(a => a.BusinessGroup); | ||||
|  | ||||
|                 foreach (var group in varGroup) | ||||
|                 { | ||||
| @@ -170,7 +170,7 @@ public partial class MqttClient : BusinessBaseWithCacheIntervalScript<VariableBa | ||||
|     { | ||||
|         if (!_businessPropertyWithCacheIntervalScript.VariableTopic.IsNullOrWhiteSpace()) | ||||
|         { | ||||
|             if (_driverPropertys.GroupUpdate && !variable.Group.IsNullOrEmpty() && VariableRuntimeGroups.TryGetValue(variable.Group, out var variableRuntimeGroup)) | ||||
|             if (_driverPropertys.GroupUpdate && !variable.BusinessGroup.IsNullOrEmpty() && VariableRuntimeGroups.TryGetValue(variable.BusinessGroup, out var variableRuntimeGroup)) | ||||
|             { | ||||
|                 //获取组内全部变量 | ||||
|                 AddQueueVarModel(new CacheDBItem<List<VariableBasicData>>(variableRuntimeGroup.Adapt<List<VariableBasicData>>())); | ||||
| @@ -472,7 +472,7 @@ public partial class MqttClient : BusinessBaseWithCacheIntervalScript<VariableBa | ||||
|             if (isConnect.IsSuccess) | ||||
|             { | ||||
|                 var variableMessage = new MqttApplicationMessageBuilder() | ||||
|     .WithTopic(topicArray.Topic).WithQualityOfServiceLevel(_driverPropertys.MqttQualityOfServiceLevel) | ||||
|     .WithTopic(topicArray.Topic).WithQualityOfServiceLevel(_driverPropertys.MqttQualityOfServiceLevel).WithRetainFlag() | ||||
|     .WithPayload(topicArray.Json).Build(); | ||||
|                 var result = await _mqttClient.PublishAsync(variableMessage, cancellationToken).ConfigureAwait(false); | ||||
|                 if (result.IsSuccess) | ||||
|   | ||||
| @@ -136,92 +136,18 @@ namespace ThingsGateway.Plugin.Mqtt | ||||
|  | ||||
|         private async Task CheckScript(BusinessPropertyWithCacheIntervalScript businessProperty, string pname) | ||||
|         { | ||||
|             IEnumerable<object> data = null; | ||||
|             string script = null; | ||||
|             if (pname == nameof(BusinessPropertyWithCacheIntervalScript.BigTextScriptAlarmModel)) | ||||
|             { | ||||
|                 data = new List<AlarmVariable>() { new() { | ||||
|                 Name = "testName", | ||||
|                 DeviceName = "testDevice", | ||||
|                 AlarmCode = "1", | ||||
|                 AlarmTime = DateTime.Now, | ||||
|                 EventTime = DateTime.Now, | ||||
|                 AlarmLimit = "3", | ||||
|                 AlarmType = AlarmTypeEnum.L, | ||||
|                 EventType=EventTypeEnum.Alarm, | ||||
|                 Remark1="1", | ||||
|                 Remark2="2", | ||||
|                 Remark3="3", | ||||
|                 Remark4="4", | ||||
|                 Remark5="5", | ||||
|             }, | ||||
|              new() { | ||||
|                 Name = "testName2", | ||||
|                 DeviceName = "testDevice", | ||||
|                 AlarmCode = "1", | ||||
|                 AlarmTime = DateTime.Now, | ||||
|                 EventTime = DateTime.Now, | ||||
|                 AlarmLimit = "3", | ||||
|                 AlarmType = AlarmTypeEnum.L, | ||||
|                 EventType=EventTypeEnum.Alarm, | ||||
|                 Remark1="1", | ||||
|                 Remark2="2", | ||||
|                 Remark3="3", | ||||
|                 Remark4="4", | ||||
|                 Remark5="5", | ||||
|             }}; | ||||
|                 script = businessProperty.BigTextScriptAlarmModel; | ||||
|             } | ||||
|             else if (pname == nameof(BusinessPropertyWithCacheIntervalScript.BigTextScriptVariableModel)) | ||||
|             { | ||||
|                 data = new List<VariableBasicData>() { new() { | ||||
|                 Name = "testName", | ||||
|                 DeviceName = "testDevice", | ||||
|                 Value = "1", | ||||
|                 ChangeTime = DateTime.Now, | ||||
|                 CollectTime = DateTime.Now, | ||||
|                 Remark1="1", | ||||
|                 Remark2="2", | ||||
|                 Remark3="3", | ||||
|                 Remark4="4", | ||||
|                 Remark5="5", | ||||
|             } , | ||||
|              new() { | ||||
|                 Name = "testName2", | ||||
|                 DeviceName = "testDevice", | ||||
|                 Value = "1", | ||||
|                 ChangeTime = DateTime.Now, | ||||
|                 CollectTime = DateTime.Now, | ||||
|                 Remark1="1", | ||||
|                 Remark2="2", | ||||
|                 Remark3="3", | ||||
|                 Remark4="4", | ||||
|                 Remark5="5", | ||||
|             } }; | ||||
|                 script = businessProperty.BigTextScriptVariableModel; | ||||
|  | ||||
|             } | ||||
|             else if (pname == nameof(BusinessPropertyWithCacheIntervalScript.BigTextScriptDeviceModel)) | ||||
|             { | ||||
|                 data = new List<DeviceBasicData>() { new() { | ||||
|                 Name = "testDevice", | ||||
|                 ActiveTime = DateTime.Now, | ||||
|                 Remark1="1", | ||||
|                 Remark2="2", | ||||
|                 Remark3="3", | ||||
|                 Remark4="4", | ||||
|                 Remark5="5", | ||||
|             } , | ||||
|             new() { | ||||
|                 Name = "testDevice2", | ||||
|                 ActiveTime = DateTime.Now, | ||||
|                 Remark1="1", | ||||
|                 Remark2="2", | ||||
|                 Remark3="3", | ||||
|                 Remark4="4", | ||||
|                 Remark5="5", | ||||
|             }}; | ||||
|  | ||||
|                 script = businessProperty.BigTextScriptDeviceModel; | ||||
|             } | ||||
|             else | ||||
| @@ -241,14 +167,16 @@ namespace ThingsGateway.Plugin.Mqtt | ||||
|  | ||||
|             op.Component = BootstrapDynamicComponent.CreateComponent<ScriptCheck>(new Dictionary<string, object?> | ||||
|     { | ||||
|         {nameof(ScriptCheck.Data),data }, | ||||
|         {nameof(ScriptCheck.Data),Array.Empty<object>() }, | ||||
|         {nameof(ScriptCheck.Script),script }, | ||||
|         {nameof(ScriptCheck.OnGetDemo),()=> | ||||
|                 { | ||||
|                     return | ||||
|                     pname == nameof(BusinessPropertyWithCacheIntervalScript.BigTextScriptDeviceModel)? | ||||
|                     """ | ||||
|                     using ThingsGateway.Foundation; | ||||
|                      | ||||
|                     using System.Dynamic; | ||||
|                     using TouchSocket.Core; | ||||
|                     public class S1 : IDynamicModel | ||||
|                     { | ||||
| @@ -287,6 +215,9 @@ namespace ThingsGateway.Plugin.Mqtt | ||||
|  | ||||
|                     """ | ||||
|  | ||||
|                     using ThingsGateway.Foundation; | ||||
|                      | ||||
|                     using System.Dynamic; | ||||
|                     using TouchSocket.Core; | ||||
|                     public class S2 : IDynamicModel | ||||
|                     { | ||||
|   | ||||
| @@ -65,7 +65,7 @@ public partial class MqttCollect : CollectBase | ||||
|                 } | ||||
|  | ||||
|                比如vendor/device;ModuleUnoccupied.EquipId,结果是"E12" | ||||
|                比如vendor/device;ModuleUnoccupied.EquipId;raw.SelectToken("ModuleUnoccupied.LotId").ToString().ToInt()==1,结果是"E12" | ||||
|                比如vendor/device;ModuleUnoccupied.EquipId;((JToken)raw).SelectToken("ModuleUnoccupied.LotId").ToString().ToInt()==1,结果是"E12" | ||||
|              | ||||
|             """; | ||||
|     } | ||||
| @@ -181,6 +181,7 @@ public partial class MqttCollect : CollectBase | ||||
|  | ||||
|     protected override async Task InitChannelAsync(IChannel? channel, CancellationToken cancellationToken) | ||||
|     { | ||||
|         ETime = TimeSpan.FromSeconds(_driverPropertys.CheckClearTime); | ||||
|  | ||||
|         #region 初始化 | ||||
|  | ||||
| @@ -216,10 +217,34 @@ public partial class MqttCollect : CollectBase | ||||
|         _mqttClient.ApplicationMessageReceivedAsync += MqttClient_ApplicationMessageReceivedAsync; | ||||
|  | ||||
|         #endregion 初始化 | ||||
|  | ||||
|         await base.InitChannelAsync(channel, cancellationToken).ConfigureAwait(false); | ||||
|     } | ||||
|  | ||||
|     private TimeSpan ETime = TimeSpan.FromSeconds(60000); | ||||
|     protected override async Task ProtectedStartAsync(CancellationToken cancellationToken) | ||||
|     { | ||||
|         _ = Task.Run(async () => | ||||
|         { | ||||
|             while (!cancellationToken.IsCancellationRequested && !DisposedValue) | ||||
|             { | ||||
|                 try | ||||
|                 { | ||||
|                     foreach (var item in IdVariableRuntimes) | ||||
|                     { | ||||
|                         if (DateTime.Now - item.Value.CollectTime > ETime) | ||||
|                         { | ||||
|                             item.Value.SetValue(null, DateTime.Now, false); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 finally | ||||
|                 { | ||||
|                     await Task.Delay(200).ConfigureAwait(false); | ||||
|                 } | ||||
|             } | ||||
|         }, cancellationToken); | ||||
|  | ||||
|         await base.ProtectedStartAsync(cancellationToken).ConfigureAwait(false); | ||||
|         if (_mqttClient != null) | ||||
|         { | ||||
|   | ||||
| @@ -74,6 +74,7 @@ public class MqttCollectProperty : CollectPropertyBase | ||||
|     [DynamicProperty] | ||||
|     public int ConnectTimeout { get; set; } = 3000; | ||||
|  | ||||
|     public override int ReIntervalTime { get; set; } = 30; | ||||
|     public override int RetryCount { get; set; } = 3; | ||||
|     [DynamicProperty] | ||||
|     public int CheckClearTime { get; set; } = 60000; | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -100,8 +100,8 @@ public partial class MqttServer : BusinessBaseWithCacheIntervalScript<VariableBa | ||||
|         { | ||||
|             if (_driverPropertys.GroupUpdate) | ||||
|             { | ||||
|                 var varList = variables.Where(a => a.Group.IsNullOrEmpty()); | ||||
|                 var varGroup = variables.Where(a => !a.Group.IsNullOrEmpty()).GroupBy(a => a.Group); | ||||
|                 var varList = variables.Where(a => a.BusinessGroup.IsNullOrEmpty()); | ||||
|                 var varGroup = variables.Where(a => !a.BusinessGroup.IsNullOrEmpty()).GroupBy(a => a.BusinessGroup); | ||||
|  | ||||
|                 foreach (var group in varGroup) | ||||
|                 { | ||||
| @@ -126,7 +126,7 @@ public partial class MqttServer : BusinessBaseWithCacheIntervalScript<VariableBa | ||||
|     { | ||||
|         if (!_businessPropertyWithCacheIntervalScript.VariableTopic.IsNullOrWhiteSpace()) | ||||
|         { | ||||
|             if (_driverPropertys.GroupUpdate && !variable.Group.IsNullOrEmpty() && VariableRuntimeGroups.TryGetValue(variable.Group, out var variableRuntimeGroup)) | ||||
|             if (_driverPropertys.GroupUpdate && !variable.BusinessGroup.IsNullOrEmpty() && VariableRuntimeGroups.TryGetValue(variable.BusinessGroup, out var variableRuntimeGroup)) | ||||
|             { | ||||
|  | ||||
|  | ||||
| @@ -400,7 +400,7 @@ public partial class MqttServer : BusinessBaseWithCacheIntervalScript<VariableBa | ||||
|         try | ||||
|         { | ||||
|             var message = new MqttApplicationMessageBuilder() | ||||
| .WithTopic(topicArray.Topic).WithQualityOfServiceLevel(_driverPropertys.MqttQualityOfServiceLevel) | ||||
| .WithTopic(topicArray.Topic).WithQualityOfServiceLevel(_driverPropertys.MqttQualityOfServiceLevel).WithRetainFlag() | ||||
| .WithPayload(topicArray.Json).Build(); | ||||
|             await _mqttServer.InjectApplicationMessage( | ||||
|                     new InjectedMqttApplicationMessage(message), cancellationToken).ConfigureAwait(false); | ||||
|   | ||||
| @@ -42,9 +42,9 @@ public class OpcUaMaster : CollectBase | ||||
|     /// <inheritdoc/> | ||||
|     public override Type DriverDebugUIType => typeof(ThingsGateway.Debug.OpcUaMaster); | ||||
|  | ||||
|     public override Type DriverPropertyUIType => typeof(OpcUaMasterPropertyRazor); | ||||
|  | ||||
|     public override Type DriverPropertyUIType => typeof(OpcUaMasterRuntimeRazor); | ||||
|  | ||||
|     public override Type DriverUIType => typeof(OpcUaMasterRuntimeRazor); | ||||
|  | ||||
|     protected override async Task InitChannelAsync(IChannel? channel, CancellationToken cancellationToken) | ||||
|     { | ||||
|   | ||||
| @@ -60,6 +60,14 @@ public class ThingsGatewayNodeManager : CustomNodeManager2 | ||||
|         lock (Lock) | ||||
|         { | ||||
|             if (rootFolder == null) return; | ||||
|  | ||||
|             rootFolder?.SafeDispose(); | ||||
|             rootFolder = null; | ||||
|             rootFolder = CreateFolder(null, "ThingsGateway", "ThingsGateway"); | ||||
|             rootFolder.EventNotifier = EventNotifiers.SubscribeToEvents; | ||||
|  | ||||
|             rootFolder.ClearChangeMasks(SystemContext, true); | ||||
|  | ||||
|             rootFolder.RemoveReferences(ReferenceTypes.Organizes, true); | ||||
|             rootFolder.AddReference(ReferenceTypes.Organizes, true, ObjectIds.ObjectsFolder); | ||||
|             AddRootNotifier(rootFolder); | ||||
| @@ -84,6 +92,7 @@ public class ThingsGatewayNodeManager : CustomNodeManager2 | ||||
|             } | ||||
|             AddPredefinedNode(SystemContext, rootFolder); | ||||
|  | ||||
|             rootFolder.ClearChangeMasks(SystemContext, true); | ||||
|  | ||||
|         } | ||||
|  | ||||
| @@ -110,6 +119,7 @@ public class ThingsGatewayNodeManager : CustomNodeManager2 | ||||
|  | ||||
|             RefreshVariable(); | ||||
|         } | ||||
|  | ||||
|     } | ||||
|  | ||||
|     /// <summary> | ||||
|   | ||||
| @@ -51,9 +51,19 @@ public partial class OpcUaServer : BusinessBase | ||||
|  | ||||
|     private static readonly string[] separator = new string[] { ";" }; | ||||
|  | ||||
|  | ||||
|     private volatile int VariableChangedCount = 0; | ||||
|     public override async Task AfterVariablesChangedAsync(CancellationToken cancellationToken) | ||||
|     { | ||||
|         //opcua类库内部有大量缓存,如果刷新变量次数大于一定数量,应该重启服务以防止OOM | ||||
|         if (Interlocked.Increment(ref VariableChangedCount) > 100) | ||||
|         { | ||||
|             _ = Task.Run(async () => | ||||
|             { | ||||
|                 await this.DeviceThreadManage.RestartDeviceAsync(this.CurrentDevice, false).ConfigureAwait(false); | ||||
|             } | ||||
|             , cancellationToken); | ||||
|             return; | ||||
|         } | ||||
|         // 如果业务属性指定了全部变量,则设置当前设备的变量运行时列表和采集设备列表 | ||||
|         if (_driverPropertys.IsAllVariable) | ||||
|         { | ||||
| @@ -74,6 +84,8 @@ public partial class OpcUaServer : BusinessBase | ||||
|         { | ||||
|             VariableValueChange(a.Value, a.Value.Adapt<VariableBasicData>()); | ||||
|         }); | ||||
|  | ||||
|  | ||||
|         m_server?.NodeManager?.RefreshVariable(); | ||||
|  | ||||
|     } | ||||
|   | ||||
| @@ -1,25 +0,0 @@ | ||||
| @using BootstrapBlazor.Components | ||||
| @using Microsoft.Extensions.Localization | ||||
| @using ThingsGateway.Extension | ||||
| @using ThingsGateway.Foundation | ||||
| @using ThingsGateway.Admin.Application | ||||
| @using ThingsGateway.Admin.Razor | ||||
| @using ThingsGateway.Gateway.Application | ||||
| @using ThingsGateway.Plugin.OpcUa | ||||
| @namespace ThingsGateway.Plugin.OpcUa | ||||
|  | ||||
|  | ||||
| <ValidateForm Model="Model.Value" | ||||
|               @key=@($"DeviceEditValidateForm{Id}{Model.Value.GetType().TypeHandle.Value}") | ||||
|               @ref=Model.ValidateForm | ||||
|               Id=@($"DeviceEditValidateForm{Id}{Model.Value.GetType().TypeHandle.Value}")> | ||||
|  | ||||
|     <EditorFormObject class="p-2" Items=PluginPropertyEditorItems IsDisplay="!CanWrite" AutoGenerateAllItem="false" RowType=RowType.Inline ItemsPerRow=@(CanWrite?2:3) ShowLabelTooltip=true LabelWidth=@(CanWrite?240:120) Model="Model.Value" ShowLabel="true" @key=@($"DeviceEditEditorFormObject{Id}{Model.Value.GetType().TypeHandle.Value}")> | ||||
|  | ||||
|         <Buttons> | ||||
|             <Button IsAsync class="mx-2" Color=Color.Primary OnClick="Export">@Localizer["ExportC"]</Button> | ||||
|         </Buttons> | ||||
|     </EditorFormObject> | ||||
| </ValidateForm> | ||||
|  | ||||
|  | ||||
| @@ -0,0 +1,16 @@ | ||||
| @using BootstrapBlazor.Components | ||||
| @using Microsoft.Extensions.Localization | ||||
| @using ThingsGateway.Extension | ||||
| @using ThingsGateway.Foundation | ||||
| @using ThingsGateway.Admin.Application | ||||
| @using ThingsGateway.Admin.Razor | ||||
| @using ThingsGateway.Gateway.Application | ||||
| @using ThingsGateway.Plugin.OpcUa | ||||
| @namespace ThingsGateway.Plugin.OpcUa | ||||
|  | ||||
|  | ||||
|  | ||||
| <Button IsAsync class="mx-2" Color=Color.Primary OnClick="Export">@OpcUaMasterPropertyLocalizer["ExportC"]</Button> | ||||
|  | ||||
|  | ||||
|  | ||||
| @@ -18,32 +18,19 @@ using ThingsGateway.Razor; | ||||
| 
 | ||||
| namespace ThingsGateway.Plugin.OpcUa | ||||
| { | ||||
|     public partial class OpcUaMasterPropertyRazor : IPropertyUIBase | ||||
|     public partial class OpcUaMasterRuntimeRazor : IDriverUIBase | ||||
|     { | ||||
|         [Parameter, EditorRequired] | ||||
|         public string Id { get; set; } | ||||
|         [Parameter, EditorRequired] | ||||
|         public bool CanWrite { get; set; } | ||||
|         [Parameter, EditorRequired] | ||||
|         public ModelValueValidateForm Model { get; set; } | ||||
| 
 | ||||
|         [Parameter, EditorRequired] | ||||
|         public IEnumerable<IEditorItem> PluginPropertyEditorItems { get; set; } | ||||
| 
 | ||||
|         IStringLocalizer Localizer { get; set; } | ||||
| 
 | ||||
|         protected override Task OnParametersSetAsync() | ||||
|         { | ||||
|             Localizer = App.CreateLocalizerByType(Model.Value.GetType()); | ||||
| 
 | ||||
|             return base.OnParametersSetAsync(); | ||||
|         } | ||||
|         public object Driver { get; set; } | ||||
| 
 | ||||
|         [Inject] | ||||
|         private DownloadService DownloadService { get; set; } | ||||
|         [Inject] | ||||
|         private ToastService ToastService { get; set; } | ||||
| 
 | ||||
|         [Inject] | ||||
|         IStringLocalizer<OpcUaMasterProperty> OpcUaMasterPropertyLocalizer { get; set; } | ||||
| 
 | ||||
| 
 | ||||
|         private async Task Export() | ||||
|         { | ||||
| @@ -87,8 +87,8 @@ public partial class RabbitMQProducer : BusinessBaseWithCacheIntervalScript<Vari | ||||
|         { | ||||
|             if (_driverPropertys.GroupUpdate) | ||||
|             { | ||||
|                 var varList = variables.Where(a => a.Group.IsNullOrEmpty()); | ||||
|                 var varGroup = variables.Where(a => !a.Group.IsNullOrEmpty()).GroupBy(a => a.Group); | ||||
|                 var varList = variables.Where(a => a.BusinessGroup.IsNullOrEmpty()); | ||||
|                 var varGroup = variables.Where(a => !a.BusinessGroup.IsNullOrEmpty()).GroupBy(a => a.BusinessGroup); | ||||
|  | ||||
|                 foreach (var group in varGroup) | ||||
|                 { | ||||
| @@ -113,7 +113,7 @@ public partial class RabbitMQProducer : BusinessBaseWithCacheIntervalScript<Vari | ||||
|     { | ||||
|         if (!_businessPropertyWithCacheIntervalScript.VariableTopic.IsNullOrWhiteSpace()) | ||||
|         { | ||||
|             if (_driverPropertys.GroupUpdate && !variable.Group.IsNullOrEmpty() && VariableRuntimeGroups.TryGetValue(variable.Group, out var variableRuntimeGroup)) | ||||
|             if (_driverPropertys.GroupUpdate && !variable.BusinessGroup.IsNullOrEmpty() && VariableRuntimeGroups.TryGetValue(variable.BusinessGroup, out var variableRuntimeGroup)) | ||||
|  | ||||
|             { | ||||
|                 //获取组内全部变量 | ||||
|   | ||||
| @@ -0,0 +1,18 @@ | ||||
| <Project> | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| 	<!--在构建后触发的。它通过在 Nuget 包的 Content 文件夹中包含目标目录中的所有文件和子文件夹来创建 nuget 包--> | ||||
| 	<Target Name="IncludeAllFilesInTargetDir" AfterTargets="Build"> | ||||
| 		<ItemGroup> | ||||
| 			<Content Include="$(ProjectDir)bin\$(Configuration)\$(TargetFramework)\**\*Synchronization*.dll"> | ||||
| 				<Pack>true</Pack> | ||||
| 				<PackagePath>Content</PackagePath> | ||||
| 			</Content> | ||||
| 		</ItemGroup> | ||||
| 	</Target> | ||||
|  | ||||
| </Project> | ||||
| @@ -0,0 +1,16 @@ | ||||
| //------------------------------------------------------------------------------ | ||||
| //  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充 | ||||
| //  此代码版权(除特别声明外的代码)归作者本人Diego所有 | ||||
| //  源代码使用协议遵循本仓库的开源协议及附加协议 | ||||
| //  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway | ||||
| //  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway | ||||
| //  使用文档:https://thingsgateway.cn/ | ||||
| //  QQ群:605534569 | ||||
| //------------------------------------------------------------------------------ | ||||
|  | ||||
| global using System; | ||||
| global using System.Collections.Generic; | ||||
| global using System.Threading; | ||||
| global using System.Threading.Tasks; | ||||
|  | ||||
| global using ThingsGateway.Gateway.Application; | ||||
| @@ -0,0 +1,14 @@ | ||||
| { | ||||
|  | ||||
|   "ThingsGateway.Plugin.Synchronization.SynchronizationProperty": { | ||||
|     "IsServer": "IsServer", | ||||
|     "IsMaster": "IsMaster", | ||||
|     "IsAllVariable": "IsAllVariable", | ||||
|     "ServerUri": "ServerUri", | ||||
|     "VerifyToken": "VerifyToken", | ||||
|     "HeartbeatInterval": "HeartbeatInterval", | ||||
|     "SyncInterval": "SyncInterval" | ||||
|  | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,14 @@ | ||||
| { | ||||
|  | ||||
|   "ThingsGateway.Plugin.Synchronization.SynchronizationProperty": { | ||||
|     "IsServer": "是否Tcp服务", | ||||
|     "IsMaster": "是否主站", | ||||
|     "IsAllVariable": "选择全部变量", | ||||
|     "ServerUri": "服务Uri", | ||||
|     "VerifyToken": "验证令牌", | ||||
|     "HeartbeatInterval": "心跳间隔", | ||||
|     "SyncInterval": "同步间隔" | ||||
|  | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,46 @@ | ||||
| //------------------------------------------------------------------------------ | ||||
| //  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充 | ||||
| //  此代码版权(除特别声明外的代码)归作者本人Diego所有 | ||||
| //  源代码使用协议遵循本仓库的开源协议及附加协议 | ||||
| //  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway | ||||
| //  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway | ||||
| //  使用文档:https://thingsgateway.cn/ | ||||
| //  QQ群:605534569 | ||||
| //------------------------------------------------------------------------------ | ||||
|  | ||||
| using Newtonsoft.Json; | ||||
|  | ||||
| namespace ThingsGateway.Plugin.Synchronization; | ||||
|  | ||||
| public class DeviceDataWithValue | ||||
| { | ||||
|     /// <inheritdoc cref="Device.Name"/> | ||||
|     public string Name { get; set; } | ||||
|  | ||||
|     /// <inheritdoc cref="DeviceRuntime.ActiveTime"/> | ||||
|     public DateTime ActiveTime { get; set; } | ||||
|  | ||||
|     /// <inheritdoc cref="DeviceRuntime.DeviceStatus"/> | ||||
|     public DeviceStatusEnum DeviceStatus { get; set; } | ||||
|  | ||||
|     /// <inheritdoc cref="DeviceRuntime.LastErrorMessage"/> | ||||
|     [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] | ||||
|     [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] | ||||
|     public string LastErrorMessage { get; set; } | ||||
|  | ||||
|     /// <inheritdoc cref="DeviceRuntime.ReadOnlyVariableRuntimes"/> | ||||
|     public Dictionary<string, VariableDataWithValue> ReadOnlyVariableRuntimes { get; set; } | ||||
| } | ||||
|  | ||||
|  | ||||
| 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; } | ||||
| } | ||||
| @@ -0,0 +1,69 @@ | ||||
| //------------------------------------------------------------------------------ | ||||
| //  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充 | ||||
| //  此代码版权(除特别声明外的代码)归作者本人Diego所有 | ||||
| //  源代码使用协议遵循本仓库的开源协议及附加协议 | ||||
| //  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway | ||||
| //  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway | ||||
| //  使用文档:https://thingsgateway.cn/ | ||||
| //  QQ群:605534569 | ||||
| //------------------------------------------------------------------------------ | ||||
|  | ||||
| using TouchSocket.Core; | ||||
| using TouchSocket.Dmtp.Rpc; | ||||
| using TouchSocket.Rpc; | ||||
|  | ||||
| namespace ThingsGateway.Plugin.Synchronization; | ||||
|  | ||||
| public partial class ReverseCallbackServer : SingletonRpcServer | ||||
| { | ||||
|     Synchronization Synchronization; | ||||
|     public ReverseCallbackServer(Synchronization synchronization) | ||||
|     { | ||||
|         Synchronization = synchronization; | ||||
|     } | ||||
|     [DmtpRpc(MethodInvoke = true)] | ||||
|     public void UpData(List<DeviceDataWithValue> deviceDatas) | ||||
|     { | ||||
|         foreach (var deviceData in deviceDatas) | ||||
|         { | ||||
|             if (GlobalData.ReadOnlyDevices.TryGetValue(deviceData.Name, out var device)) | ||||
|             { | ||||
|                 device.SetDeviceStatus(deviceData.ActiveTime, deviceData.DeviceStatus == DeviceStatusEnum.OnLine ? false : true, lastErrorMessage: deviceData.LastErrorMessage); | ||||
|  | ||||
|                 foreach (var variableData in deviceData.ReadOnlyVariableRuntimes) | ||||
|                 { | ||||
|                     if (device.ReadOnlyVariableRuntimes.TryGetValue(variableData.Key, out var value)) | ||||
|                     { | ||||
|                         value.SetValue(variableData.Value.RawValue, variableData.Value.CollectTime, variableData.Value.IsOnline); | ||||
|                         value.SetErrorMessage(variableData.Value.LastErrorMessage); | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         Synchronization.LogMessage?.Trace("Update data success"); | ||||
|     } | ||||
|  | ||||
|     [DmtpRpc(MethodInvoke = true)] | ||||
|     public List<DataWithDatabase> GetData() | ||||
|     { | ||||
|         List<DataWithDatabase> dataWithDatabases = new(); | ||||
|         foreach (var device in Synchronization.CollectDevices) | ||||
|         { | ||||
|             DataWithDatabase dataWithDatabase = new(); | ||||
|             dataWithDatabase.Channel = device.Value.ChannelRuntime; | ||||
|             dataWithDatabase.DeviceVariables = new(); | ||||
|             DeviceDataWithDatabase deviceDataWithDatabase = new(); | ||||
|  | ||||
|             deviceDataWithDatabase.Device = device.Value; | ||||
|  | ||||
|             deviceDataWithDatabase.Variables = device.Value.ReadOnlyVariableRuntimes.Select(a => a.Value).Cast<Variable>().ToList(); | ||||
|  | ||||
|             dataWithDatabase.DeviceVariables.Add(deviceDataWithDatabase); | ||||
|  | ||||
|             dataWithDatabases.Add(dataWithDatabase); | ||||
|         } | ||||
|         return dataWithDatabases; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,303 @@ | ||||
| //------------------------------------------------------------------------------ | ||||
| //  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充 | ||||
| //  此代码版权(除特别声明外的代码)归作者本人Diego所有 | ||||
| //  源代码使用协议遵循本仓库的开源协议及附加协议 | ||||
| //  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway | ||||
| //  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway | ||||
| //  使用文档:https://thingsgateway.cn/ | ||||
| //  QQ群:605534569 | ||||
| //------------------------------------------------------------------------------ | ||||
|  | ||||
| using Mapster; | ||||
|  | ||||
| using System.Threading; | ||||
|  | ||||
| using ThingsGateway.Extension.Generic; | ||||
| using ThingsGateway.Foundation; | ||||
| using ThingsGateway.Gateway.Application; | ||||
|  | ||||
| using TouchSocket.Core; | ||||
| using TouchSocket.Dmtp; | ||||
| using TouchSocket.Dmtp.Rpc; | ||||
| using TouchSocket.Rpc; | ||||
| using TouchSocket.Sockets; | ||||
|  | ||||
| namespace ThingsGateway.Plugin.Synchronization; | ||||
|  | ||||
|  | ||||
| public partial class Synchronization : BusinessBase | ||||
| { | ||||
|     public override VariablePropertyBase VariablePropertys => new SynchronizationVariableProperty(); | ||||
|     internal SynchronizationProperty _driverPropertys = new(); | ||||
|     protected override BusinessPropertyBase _businessPropertyBase => _driverPropertys; | ||||
|  | ||||
|     public override Type DriverUIType => typeof(SynchronizationRuntimeRazor); | ||||
|     public override async Task AfterVariablesChangedAsync(CancellationToken cancellationToken) | ||||
|     { | ||||
|         // 如果业务属性指定了全部变量,则设置当前设备的变量运行时列表和采集设备列表 | ||||
|         if (_driverPropertys.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); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|     public override bool IsConnected() | ||||
|     { | ||||
|         return _driverPropertys.IsServer ? _tcpDmtpService?.ServerState == ServerState.Running : _tcpDmtpClient?.Online == true; | ||||
|     } | ||||
|  | ||||
|  | ||||
|  | ||||
|     protected override async ValueTask ProtectedExecuteAsync(CancellationToken cancellationToken) | ||||
|     { | ||||
|  | ||||
|         try | ||||
|         { | ||||
|  | ||||
|  | ||||
|             if (_driverPropertys.IsServer) | ||||
|             { | ||||
|                 if (_tcpDmtpService.ServerState != ServerState.Running) | ||||
|                 { | ||||
|                     if (_tcpDmtpService.ServerState != ServerState.Stopped) | ||||
|                     { | ||||
|                         await _tcpDmtpService.StopAsync(cancellationToken).ConfigureAwait(false); | ||||
|                     } | ||||
|  | ||||
|                     await _tcpDmtpService.StartAsync().ConfigureAwait(false); | ||||
|                 } | ||||
|  | ||||
|                 if (_driverPropertys.IsMaster) | ||||
|                 { | ||||
|                     bool online = false; | ||||
|                     var waitInvoke = new DmtpInvokeOption() | ||||
|                     { | ||||
|                         FeedbackType = FeedbackType.WaitInvoke, | ||||
|                         Token = cancellationToken, | ||||
|                         Timeout = 30000, | ||||
|                         SerializationType = SerializationType.Json, | ||||
|                     }; | ||||
|                     if (_tcpDmtpService.Clients.Count != 0) | ||||
|                     { | ||||
|                         online = true; | ||||
|                     } | ||||
|                     // 如果 online 为 true,表示设备在线 | ||||
|                     if (online) | ||||
|                     { | ||||
|                         var deviceRunTimes = CollectDevices.Where(a => a.Value.IsCollect == true).Select(a => a.Value).Adapt<List<DeviceDataWithValue>>(); | ||||
|  | ||||
|                         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 data success"); | ||||
|                         } | ||||
|  | ||||
|                     } | ||||
|  | ||||
|                 } | ||||
|  | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 bool online = true; | ||||
|                 var waitInvoke = new DmtpInvokeOption() | ||||
|                 { | ||||
|                     FeedbackType = FeedbackType.WaitInvoke, | ||||
|                     Token = cancellationToken, | ||||
|                     Timeout = 30000, | ||||
|                     SerializationType = SerializationType.Json, | ||||
|                 }; | ||||
|                 if (!_tcpDmtpClient.Online) | ||||
|                     online = (await _tcpDmtpClient.TryConnectAsync().ConfigureAwait(false)).ResultCode == ResultCode.Success; | ||||
|                 if (_driverPropertys.IsMaster) | ||||
|                 { | ||||
|  | ||||
|  | ||||
|                     // 如果 online 为 true,表示设备在线 | ||||
|                     { | ||||
|                         var deviceRunTimes = CollectDevices.Where(a => a.Value.IsCollect == true).Select(a => a.Value).Adapt<List<DeviceDataWithValue>>(); | ||||
|  | ||||
|                         // 将 GlobalData.CollectDevices 和 GlobalData.Variables 同步到从站 | ||||
|                         await _tcpDmtpClient.GetDmtpRpcActor().InvokeAsync( | ||||
|                                          nameof(ReverseCallbackServer.UpData), null, waitInvoke, deviceRunTimes).ConfigureAwait(false); | ||||
|                         LogMessage?.LogTrace($"Update data success"); | ||||
|  | ||||
|                     } | ||||
|  | ||||
|                 } | ||||
|  | ||||
|  | ||||
|             } | ||||
|  | ||||
|         } | ||||
|         catch (OperationCanceledException) { } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             LogMessage.LogWarning(ex); | ||||
|         } | ||||
|     } | ||||
|     private TcpDmtpClient? _tcpDmtpClient; | ||||
|     private TcpDmtpService? _tcpDmtpService; | ||||
|     protected override async Task InitChannelAsync(IChannel? channel, CancellationToken cancellationToken) | ||||
|     { | ||||
|         if (_driverPropertys.IsServer) | ||||
|         { | ||||
|             _tcpDmtpService = await GetTcpDmtpService().ConfigureAwait(false); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             _tcpDmtpClient = await GetTcpDmtpClient().ConfigureAwait(false); | ||||
|         } | ||||
|  | ||||
|         await base.InitChannelAsync(channel, cancellationToken).ConfigureAwait(false); | ||||
|     } | ||||
|  | ||||
|     protected override void Dispose(bool disposing) | ||||
|     { | ||||
|         _tcpDmtpClient?.SafeDispose(); | ||||
|         _tcpDmtpService?.SafeDispose(); | ||||
|         base.Dispose(disposing); | ||||
|     } | ||||
|  | ||||
|     private async Task<TcpDmtpClient> GetTcpDmtpClient() | ||||
|     { | ||||
|         var tcpDmtpClient = new TcpDmtpClient(); | ||||
|         var config = new TouchSocketConfig() | ||||
|                .SetRemoteIPHost(_driverPropertys.ServerUri) | ||||
|                .SetAdapterOption(new AdapterOption() { MaxPackageSize = 1024 * 1024 * 1024 }) | ||||
|                .SetDmtpOption(new DmtpOption() { VerifyToken = _driverPropertys.VerifyToken }) | ||||
|                .ConfigureContainer(a => | ||||
|                { | ||||
|                    a.AddLogger(LogMessage); | ||||
|                    a.AddRpcStore(store => | ||||
|                    { | ||||
|                        store.RegisterServer(new ReverseCallbackServer(this)); | ||||
|                    }); | ||||
|                }) | ||||
|                .ConfigurePlugins(a => | ||||
|                { | ||||
|                    a.UseDmtpRpc(); | ||||
|                    a.UseDmtpHeartbeat()//使用Dmtp心跳 | ||||
|                    .SetTick(TimeSpan.FromMilliseconds(_driverPropertys.HeartbeatInterval)) | ||||
|                    .SetMaxFailCount(3); | ||||
|                }); | ||||
|  | ||||
|         await tcpDmtpClient.SetupAsync(config).ConfigureAwait(false); | ||||
|         return tcpDmtpClient; | ||||
|     } | ||||
|  | ||||
|     private async Task<TcpDmtpService> GetTcpDmtpService() | ||||
|     { | ||||
|         var tcpDmtpService = new TcpDmtpService(); | ||||
|         var config = new TouchSocketConfig() | ||||
|                .SetListenIPHosts(_driverPropertys.ServerUri) | ||||
|                .SetAdapterOption(new AdapterOption() { MaxPackageSize = 1024 * 1024 * 1024 }) | ||||
|                .SetDmtpOption(new DmtpOption() { VerifyToken = _driverPropertys.VerifyToken }) | ||||
|                .ConfigureContainer(a => | ||||
|                { | ||||
|                    a.AddLogger(LogMessage); | ||||
|                    a.AddRpcStore(store => | ||||
|                    { | ||||
|                        store.RegisterServer(new ReverseCallbackServer(this)); | ||||
|                    }); | ||||
|                }) | ||||
|                .ConfigurePlugins(a => | ||||
|                { | ||||
|                    a.UseDmtpRpc(); | ||||
|                    a.UseDmtpHeartbeat()//使用Dmtp心跳 | ||||
|                    .SetTick(TimeSpan.FromMilliseconds(_driverPropertys.HeartbeatInterval)) | ||||
|                    .SetMaxFailCount(3); | ||||
|                }); | ||||
|  | ||||
|         await tcpDmtpService.SetupAsync(config).ConfigureAwait(false); | ||||
|         return tcpDmtpService; | ||||
|     } | ||||
|  | ||||
|  | ||||
|     public async ValueTask ForcedSync(CancellationToken cancellationToken = default) | ||||
|     { | ||||
|         if (_driverPropertys.IsMaster) | ||||
|         { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         bool online = false; | ||||
|         var waitInvoke = new DmtpInvokeOption() | ||||
|         { | ||||
|             FeedbackType = FeedbackType.WaitInvoke, | ||||
|             Token = cancellationToken, | ||||
|             Timeout = 30000, | ||||
|             SerializationType = SerializationType.Json, | ||||
|         }; | ||||
|  | ||||
|         try | ||||
|         { | ||||
|             if (!_driverPropertys.IsServer) | ||||
|             { | ||||
|                 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); | ||||
|  | ||||
|                     data.ForEach(a => a.Channel.Enable = 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"); | ||||
|  | ||||
|                 } | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 if (_tcpDmtpService.Clients.Count != 0) | ||||
|                 { | ||||
|                     online = true; | ||||
|                 } | ||||
|                 // 如果 online 为 true,表示设备在线 | ||||
|                 if (online) | ||||
|                 { | ||||
|                     foreach (var item in _tcpDmtpService.Clients) | ||||
|                     { | ||||
|                         var data = await item.GetDmtpRpcActor().InvokeTAsync<List<DataWithDatabase>>(nameof(ReverseCallbackServer.GetData), waitInvoke).ConfigureAwait(false); | ||||
|                         data.ForEach(a => a.Channel.Enable = 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($"{item.GetIPPort()}: ForcedSync data success"); | ||||
|                     } | ||||
|  | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     LogMessage?.LogWarning("ForcedSync data error, no client online"); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|  | ||||
|         } | ||||
|         catch (OperationCanceledException) { } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             // 输出警告日志,指示同步数据到从站时发生错误 | ||||
|             LogMessage?.LogWarning(ex, "ForcedSync data error"); | ||||
|         } | ||||
|  | ||||
|     } | ||||
|  | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,17 @@ | ||||
| //------------------------------------------------------------------------------ | ||||
| //  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充 | ||||
| //  此代码版权(除特别声明外的代码)归作者本人Diego所有 | ||||
| //  源代码使用协议遵循本仓库的开源协议及附加协议 | ||||
| //  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway | ||||
| //  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway | ||||
| //  使用文档:https://thingsgateway.cn/ | ||||
| //  QQ群:605534569 | ||||
| //------------------------------------------------------------------------------ | ||||
|  | ||||
|  | ||||
| namespace ThingsGateway.Plugin.Synchronization; | ||||
|  | ||||
| public partial class Synchronization : BusinessBase | ||||
| { | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,54 @@ | ||||
| //------------------------------------------------------------------------------ | ||||
| //  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充 | ||||
| //  此代码版权(除特别声明外的代码)归作者本人Diego所有 | ||||
| //  源代码使用协议遵循本仓库的开源协议及附加协议 | ||||
| //  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway | ||||
| //  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway | ||||
| //  使用文档:https://thingsgateway.cn/ | ||||
| //  QQ群:605534569 | ||||
| //------------------------------------------------------------------------------ | ||||
|  | ||||
| using System.ComponentModel.DataAnnotations; | ||||
|  | ||||
| namespace ThingsGateway.Plugin.Synchronization; | ||||
|  | ||||
| /// <summary> | ||||
| /// <inheritdoc/> | ||||
| /// </summary> | ||||
| public class SynchronizationProperty : BusinessPropertyBase | ||||
| { | ||||
|     [DynamicProperty] | ||||
|     public bool IsServer { get; set; } = true; | ||||
|     [DynamicProperty] | ||||
|     public bool IsMaster { get; set; } | ||||
|  | ||||
|     [DynamicProperty] | ||||
|     public bool IsAllVariable { get; set; } = true; | ||||
|  | ||||
|     /// <summary> | ||||
|     /// 获取或设置远程 URI,用于通信。 | ||||
|     /// </summary> | ||||
|     [DynamicProperty] | ||||
|     public string ServerUri { get; set; } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// 获取或设置用于验证的令牌。 | ||||
|     /// </summary> | ||||
|     [Required] | ||||
|     [DynamicProperty] | ||||
|     public string VerifyToken { get; set; } = "ThingsGateway"; | ||||
|  | ||||
|     /// <summary> | ||||
|     /// 获取或设置心跳间隔。 | ||||
|     /// </summary> | ||||
|     [MinValue(3000)] | ||||
|     [DynamicProperty] | ||||
|     public int HeartbeatInterval { get; set; } = 5000; | ||||
|  | ||||
|     /// <summary> | ||||
|     /// 获取或设置冗余数据同步间隔(ms)。 | ||||
|     /// </summary> | ||||
|     [MinValue(1000)] | ||||
|     [DynamicProperty] | ||||
|     public int SyncInterval { get; set; } = 3000; | ||||
| } | ||||
| @@ -0,0 +1,15 @@ | ||||
| @using BootstrapBlazor.Components | ||||
| @using Microsoft.Extensions.Localization | ||||
| @using ThingsGateway.Extension | ||||
| @using ThingsGateway.Foundation | ||||
| @using ThingsGateway.Admin.Application | ||||
| @using ThingsGateway.Admin.Razor | ||||
| @using ThingsGateway.Gateway.Application | ||||
| @namespace ThingsGateway.Plugin.Synchronization | ||||
|  | ||||
|  | ||||
|  | ||||
| <Button IsDisabled=Synchronization._driverPropertys.IsMaster IsAsync class="mx-2" Color=Color.Primary OnClick="ForcedSync">ForcedSync</Button> | ||||
|  | ||||
|  | ||||
|  | ||||
| @@ -0,0 +1,47 @@ | ||||
| // ------------------------------------------------------------------------------ | ||||
| // 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充 | ||||
| // 此代码版权(除特别声明外的代码)归作者本人Diego所有 | ||||
| // 源代码使用协议遵循本仓库的开源协议及附加协议 | ||||
| // Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway | ||||
| // Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway | ||||
| // 使用文档:https://thingsgateway.cn/ | ||||
| // QQ群:605534569 | ||||
| // ------------------------------------------------------------------------------ | ||||
|  | ||||
| #pragma warning disable CA2007 // 考虑对等待的任务调用 ConfigureAwait | ||||
| using BootstrapBlazor.Components; | ||||
|  | ||||
| using Microsoft.AspNetCore.Components; | ||||
| using Microsoft.Extensions.Localization; | ||||
|  | ||||
| using Rougamo; | ||||
|  | ||||
| using ThingsGateway.Razor; | ||||
|  | ||||
| namespace ThingsGateway.Plugin.Synchronization | ||||
| { | ||||
|     public partial class SynchronizationRuntimeRazor : IDriverUIBase | ||||
|     { | ||||
|         [Parameter, EditorRequired] | ||||
|         public object Driver { get; set; } | ||||
|         public Synchronization Synchronization => Driver as Synchronization; | ||||
|         [Inject] | ||||
|         private DownloadService DownloadService { get; set; } | ||||
|         [Inject] | ||||
|         private ToastService ToastService { get; set; } | ||||
|  | ||||
|  | ||||
|         private async Task ForcedSync() | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 await Synchronization.ForcedSync(); | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 await ToastService.Warn(ex); | ||||
|             } | ||||
|  | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,19 @@ | ||||
| //------------------------------------------------------------------------------ | ||||
| //  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充 | ||||
| //  此代码版权(除特别声明外的代码)归作者本人Diego所有 | ||||
| //  源代码使用协议遵循本仓库的开源协议及附加协议 | ||||
| //  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway | ||||
| //  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway | ||||
| //  使用文档:https://thingsgateway.cn/ | ||||
| //  QQ群:605534569 | ||||
| //------------------------------------------------------------------------------ | ||||
|  | ||||
| namespace ThingsGateway.Plugin.Synchronization; | ||||
|  | ||||
| /// <summary> | ||||
| /// <inheritdoc/> | ||||
| /// </summary> | ||||
| public class SynchronizationVariableProperty : VariablePropertyBase | ||||
| { | ||||
|     public override bool Enable { get; set; } = true; | ||||
| } | ||||
| @@ -0,0 +1,33 @@ | ||||
| //------------------------------------------------------------------------------ | ||||
| //  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充 | ||||
| //  此代码版权(除特别声明外的代码)归作者本人Diego所有 | ||||
| //  源代码使用协议遵循本仓库的开源协议及附加协议 | ||||
| //  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway | ||||
| //  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway | ||||
| //  使用文档:https://thingsgateway.cn/ | ||||
| //  QQ群:605534569 | ||||
| //------------------------------------------------------------------------------ | ||||
|  | ||||
| using Newtonsoft.Json; | ||||
|  | ||||
| namespace ThingsGateway.Plugin.Synchronization; | ||||
|  | ||||
| public class VariableDataWithValue | ||||
| { | ||||
|     /// <inheritdoc cref="Variable.Name"/> | ||||
|     public string Name { get; set; } | ||||
|  | ||||
|     /// <inheritdoc cref="VariableRuntime.Value"/> | ||||
|     public object RawValue { get; set; } | ||||
|  | ||||
|     /// <inheritdoc cref="VariableRuntime.CollectTime"/> | ||||
|     public DateTime CollectTime { get; set; } | ||||
|  | ||||
|     /// <inheritdoc cref="VariableRuntime.IsOnline"/> | ||||
|     public bool IsOnline { get; set; } | ||||
|  | ||||
|     /// <inheritdoc cref="VariableRuntime.LastErrorMessage"/> | ||||
|     [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] | ||||
|     [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] | ||||
|     public string? LastErrorMessage { get; set; } | ||||
| } | ||||
| @@ -0,0 +1,27 @@ | ||||
| <Project Sdk="Microsoft.NET.Sdk.Razor"> | ||||
| 	<Import Project="$(SolutionDir)Version.props" /> | ||||
| 	<Import Project="$(SolutionDir)PackNuget.props" /> | ||||
| 	<PropertyGroup> | ||||
| 		<TargetFrameworks>net8.0;</TargetFrameworks> | ||||
| 	</PropertyGroup> | ||||
| 	<ItemGroup> | ||||
| 		<ProjectReference Include="..\..\Foundation\ThingsGateway.Foundation.Razor\ThingsGateway.Foundation.Razor.csproj"> | ||||
| 		</ProjectReference> | ||||
| 		<ProjectReference Include="..\..\Gateway\ThingsGateway.Gateway.Razor\ThingsGateway.Gateway.Razor.csproj"> | ||||
| 		</ProjectReference> | ||||
|  | ||||
|  | ||||
| 	</ItemGroup> | ||||
|  | ||||
|  | ||||
| 	<ItemGroup> | ||||
| 		<Content Remove="Locales\*.json" /> | ||||
| 		<EmbeddedResource Include="Locales\*.json"> | ||||
| 			<CopyToOutputDirectory>Never</CopyToOutputDirectory> | ||||
| 		</EmbeddedResource> | ||||
| 	</ItemGroup> | ||||
|  | ||||
|  | ||||
| </Project> | ||||
|  | ||||
|  | ||||
| @@ -61,6 +61,13 @@ | ||||
| 			<Private>false</Private> | ||||
| 			<IncludeAssets> native;</IncludeAssets> | ||||
| 		</PackageReference> | ||||
|  | ||||
|  | ||||
| 		<PackageReference Include="ThingsGateway.Plugin.Synchronization" Version="$(PluginVersion)" GeneratePathProperty="true"> | ||||
| 			<Private>false</Private> | ||||
| 			<IncludeAssets> native;</IncludeAssets> | ||||
| 		</PackageReference> | ||||
| 		 | ||||
| 	</ItemGroup> | ||||
|  | ||||
| 	<Target Name="CopyPluginNugetPackages" AfterTargets="Build"> | ||||
| @@ -88,6 +95,7 @@ | ||||
| 			<PkgThingsGateway_Plugin_OpcUaPackageFiles Include="$(PkgThingsGateway_Plugin_OpcUa)\Content\$(PluginTargetFramework)\**\*.*" /> | ||||
| 			<PkgThingsGateway_Plugin_RabbitMQPackageFiles Include="$(PkgThingsGateway_Plugin_RabbitMQ)\Content\$(PluginTargetFramework)\**\*.*" /> | ||||
| 			<PkgThingsGateway_Plugin_HttpPackageFiles Include="$(PkgThingsGateway_Plugin_Http)\Content\$(PluginTargetFramework)\**\*.*" /> | ||||
| 			<PkgThingsGateway_Plugin_SynchronizationPackageFiles Include="$(PkgThingsGateway_Plugin_Synchronization)\Content\$(PluginTargetFramework)\**\*.*" /> | ||||
| 		</ItemGroup> | ||||
|  | ||||
| 		<Message Text="将插件复制到插件目录 $(GatewayPluginFolder)" Importance="high" /> | ||||
| @@ -100,7 +108,8 @@ | ||||
| 		<Copy SourceFiles="@(PkgThingsGateway_Plugin_OpcDaPackageFiles)" DestinationFolder="$(GatewayPluginFolder)ThingsGateway.Plugin.OpcDa%(RecursiveDir)" /> | ||||
| 		<Copy SourceFiles="@(PkgThingsGateway_Plugin_OpcUaPackageFiles)" DestinationFolder="$(GatewayPluginFolder)ThingsGateway.Plugin.OpcUa%(RecursiveDir)" /> | ||||
| 		<Copy SourceFiles="@(PkgThingsGateway_Plugin_RabbitMQPackageFiles)" DestinationFolder="$(GatewayPluginFolder)ThingsGateway.Plugin.RabbitMQ%(RecursiveDir)" /> | ||||
| 		<Copy SourceFiles="@(PkgThingsGateway_Plugin_HttpPackageFiles)" DestinationFolder="$(GatewayPluginFolder)ThingsGateway.Plugin.RabbitMQ%(RecursiveDir)" /> | ||||
| 		<Copy SourceFiles="@(PkgThingsGateway_Plugin_HttpPackageFiles)" DestinationFolder="$(GatewayPluginFolder)ThingsGateway.Plugin.Http%(RecursiveDir)" /> | ||||
| 		<Copy SourceFiles="@(PkgThingsGateway_Plugin_SynchronizationPackageFiles)" DestinationFolder="$(GatewayPluginFolder)ThingsGateway.Plugin.Synchronization%(RecursiveDir)" /> | ||||
|  | ||||
| 	</Target> | ||||
|  | ||||
|   | ||||
| @@ -63,6 +63,11 @@ | ||||
| 			<Private>false</Private> | ||||
| 			<IncludeAssets> native;</IncludeAssets> | ||||
| 		</PackageReference> | ||||
|  | ||||
| 		<PackageReference Include="ThingsGateway.Plugin.Synchronization" Version="$(PluginVersion)" GeneratePathProperty="true"> | ||||
| 			<Private>false</Private> | ||||
| 			<IncludeAssets> native;</IncludeAssets> | ||||
| 		</PackageReference> | ||||
| 	</ItemGroup> | ||||
|  | ||||
| 	<Target Name="CopyPluginNugetPackages" AfterTargets="Build"> | ||||
| @@ -90,6 +95,7 @@ | ||||
| 			<PkgThingsGateway_Plugin_OpcUaPackageFiles Include="$(PkgThingsGateway_Plugin_OpcUa)\Content\$(PluginTargetFramework)\**\*.*" /> | ||||
| 			<PkgThingsGateway_Plugin_RabbitMQPackageFiles Include="$(PkgThingsGateway_Plugin_RabbitMQ)\Content\$(PluginTargetFramework)\**\*.*" /> | ||||
| 			<PkgThingsGateway_Plugin_HttpPackageFiles Include="$(PkgThingsGateway_Plugin_Http)\Content\$(PluginTargetFramework)\**\*.*" /> | ||||
| 			<PkgThingsGateway_Plugin_SynchronizationPackageFiles Include="$(PkgThingsGateway_Plugin_Synchronization)\Content\$(PluginTargetFramework)\**\*.*" /> | ||||
| 		</ItemGroup> | ||||
|  | ||||
|  | ||||
| @@ -105,6 +111,7 @@ | ||||
| 		<Copy SourceFiles="@(PkgThingsGateway_Plugin_OpcUaPackageFiles)" DestinationFolder="$(PluginFolder)%(RecursiveDir)" /> | ||||
| 		<Copy SourceFiles="@(PkgThingsGateway_Plugin_RabbitMQPackageFiles)" DestinationFolder="$(PluginFolder)%(RecursiveDir)" /> | ||||
| 		<Copy SourceFiles="@(PkgThingsGateway_Plugin_HttpPackageFiles)" DestinationFolder="$(PluginFolder)%(RecursiveDir)" /> | ||||
| 		<Copy SourceFiles="@(PkgThingsGateway_Plugin_SynchronizationPackageFiles)" DestinationFolder="$(PluginFolder)%(RecursiveDir)" /> | ||||
| 		 | ||||
| 	</Target> | ||||
|  | ||||
|   | ||||
| @@ -11,6 +11,7 @@ | ||||
| 		<ProjectReference Include="..\Plugin\ThingsGateway.Plugin.OpcUa\ThingsGateway.Plugin.OpcUa.csproj" /> | ||||
| 		<ProjectReference Include="..\Plugin\ThingsGateway.Plugin.RabbitMQ\ThingsGateway.Plugin.RabbitMQ.csproj" /> | ||||
| 		<ProjectReference Include="..\Plugin\ThingsGateway.Plugin.Http\ThingsGateway.Plugin.Http.csproj" /> | ||||
| 		<ProjectReference Include="..\Plugin\ThingsGateway.Plugin.Synchronization\ThingsGateway.Plugin.Synchronization.csproj" /> | ||||
| 	</ItemGroup> | ||||
|  | ||||
|    | ||||
|   | ||||
| @@ -99,6 +99,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ThingsGateway.Upgrade", "Up | ||||
| EndProject | ||||
| Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ThingsGateway.UpgradeServer", "Upgrade\ThingsGateway.UpgradeServer\ThingsGateway.UpgradeServer.csproj", "{29DCAC9C-2D0F-E251-E907-F07D804CA117}" | ||||
| EndProject | ||||
| Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ThingsGateway.Plugin.Synchronization", "Plugin\ThingsGateway.Plugin.Synchronization\ThingsGateway.Plugin.Synchronization.csproj", "{438B86D4-0CAE-DCC3-E952-90CE77BB8661}" | ||||
| EndProject | ||||
| Global | ||||
| 	GlobalSection(SolutionConfigurationPlatforms) = preSolution | ||||
| 		Debug|Any CPU = Debug|Any CPU | ||||
| @@ -261,6 +263,10 @@ Global | ||||
| 		{29DCAC9C-2D0F-E251-E907-F07D804CA117}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||||
| 		{29DCAC9C-2D0F-E251-E907-F07D804CA117}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||||
| 		{29DCAC9C-2D0F-E251-E907-F07D804CA117}.Release|Any CPU.Build.0 = Release|Any CPU | ||||
| 		{438B86D4-0CAE-DCC3-E952-90CE77BB8661}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||||
| 		{438B86D4-0CAE-DCC3-E952-90CE77BB8661}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||||
| 		{438B86D4-0CAE-DCC3-E952-90CE77BB8661}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||||
| 		{438B86D4-0CAE-DCC3-E952-90CE77BB8661}.Release|Any CPU.Build.0 = Release|Any CPU | ||||
| 	EndGlobalSection | ||||
| 	GlobalSection(SolutionProperties) = preSolution | ||||
| 		HideSolutionNode = FALSE | ||||
| @@ -303,10 +309,11 @@ Global | ||||
| 		{7E4696E3-1170-6AF6-844C-265BBCEC5DA7} = {36510D70-161F-4241-B8D0-781E21032816} | ||||
| 		{7D5E01DE-D6D7-E45D-58FD-E01B38A312B2} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} | ||||
| 		{29DCAC9C-2D0F-E251-E907-F07D804CA117} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} | ||||
| 		{438B86D4-0CAE-DCC3-E952-90CE77BB8661} = {36510D70-161F-4241-B8D0-781E21032816} | ||||
| 	EndGlobalSection | ||||
| 	GlobalSection(ExtensibilityGlobals) = postSolution | ||||
| 		SolutionGuid = {199B1B96-4F56-4828-9531-813BA02DB282} | ||||
| 		RESX_NeutralResourcesLanguage = zh-Hans | ||||
| 		RESX_Rules = {"EnabledRules":[]} | ||||
| 		RESX_NeutralResourcesLanguage = zh-Hans | ||||
| 		SolutionGuid = {199B1B96-4F56-4828-9531-813BA02DB282} | ||||
| 	EndGlobalSection | ||||
| EndGlobal | ||||
|   | ||||
| @@ -16,7 +16,7 @@ | ||||
|  | ||||
|  | ||||
| 	<ItemGroup> | ||||
| 	  <PackageReference Include="TouchSocket.Dmtp" Version="3.1.2" /> | ||||
| 	  <PackageReference Include="TouchSocket.Dmtp" Version="3.1.3" /> | ||||
| 	</ItemGroup> | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| <Project> | ||||
|   <PropertyGroup> | ||||
|     <Version>10.6.1</Version> | ||||
|     <Version>10.6.5</Version> | ||||
|   </PropertyGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user