Compare commits
	
		
			18 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					562b3f17c9 | ||
| 
						 | 
					0f78f81c1c | ||
| 
						 | 
					6594937d0a | ||
| 
						 | 
					64bc6084be | ||
| 
						 | 
					20434d5bd2 | ||
| 
						 | 
					9ecc9380e6 | ||
| 
						 | 
					a57c55080b | ||
| 
						 | 
					b8ca06c6be | ||
| 
						 | 
					5aff6461a1 | ||
| 
						 | 
					6cf53fefec | ||
| 
						 | 
					45132f3503 | ||
| 
						 | 
					f1e78a0e8a | ||
| 
						 | 
					0bf28ec275 | ||
| 
						 | 
					41f8412c97 | ||
| 
						 | 
					c535974362 | ||
| 
						 | 
					1860c5f215 | ||
| 
						 | 
					6d778b2d39 | ||
| 
						 | 
					f48b99c259 | 
@@ -115,7 +115,7 @@ public sealed class OperDescAttribute : MoAttribute
 | 
			
		||||
    private SysOperateLog GetOperLog(Type? localizerType, MethodContext context)
 | 
			
		||||
    {
 | 
			
		||||
        var methodBase = context.Method;
 | 
			
		||||
        var clientInfo = AppService.ClientInfo;
 | 
			
		||||
        var userAgent = AppService.UserAgent;
 | 
			
		||||
        string? paramJson = null;
 | 
			
		||||
        if (IsRecordPar)
 | 
			
		||||
        {
 | 
			
		||||
@@ -138,8 +138,8 @@ public sealed class OperDescAttribute : MoAttribute
 | 
			
		||||
            Category = LogCateGoryEnum.Operate,
 | 
			
		||||
            ExeStatus = true,
 | 
			
		||||
            OpIp = AppService?.RemoteIpAddress ?? string.Empty,
 | 
			
		||||
            OpBrowser = clientInfo?.UA?.Family + clientInfo?.UA?.Major,
 | 
			
		||||
            OpOs = clientInfo?.OS?.Family + clientInfo?.OS?.Major,
 | 
			
		||||
            OpBrowser = userAgent?.Browser,
 | 
			
		||||
            OpOs = userAgent?.Platform,
 | 
			
		||||
            OpTime = DateTime.Now,
 | 
			
		||||
            OpAccount = UserManager.UserAccount,
 | 
			
		||||
            ReqUrl = null,
 | 
			
		||||
 
 | 
			
		||||
@@ -55,7 +55,7 @@ public class HardwareJob : IJob, IHardwareJob
 | 
			
		||||
    public async Task<List<HistoryHardwareInfo>> GetHistoryHardwareInfos()
 | 
			
		||||
    {
 | 
			
		||||
        using var db = DbContext.Db.GetConnectionScopeWithAttr<HistoryHardwareInfo>().CopyNew();
 | 
			
		||||
        return await db.Queryable<HistoryHardwareInfo>().ToListAsync().ConfigureAwait(false);
 | 
			
		||||
        return await db.Queryable<HistoryHardwareInfo>().Where(a => a.Date > DateTime.Now.AddDays(-3)).ToListAsync().ConfigureAwait(false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private bool error = false;
 | 
			
		||||
 
 | 
			
		||||
@@ -18,8 +18,6 @@ using ThingsGateway.Logging;
 | 
			
		||||
using ThingsGateway.NewLife.Json.Extension;
 | 
			
		||||
using ThingsGateway.Razor;
 | 
			
		||||
 | 
			
		||||
using UAParser;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Admin.Application;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
@@ -53,7 +51,7 @@ public class DatabaseLoggingWriter : IDatabaseLoggingWriter
 | 
			
		||||
        if (loggingMonitor.Validation == null)
 | 
			
		||||
        {
 | 
			
		||||
            var operation = logMsg.Context.Get(LoggingConst.Operation).ToString();//获取操作名称
 | 
			
		||||
            var client = (ClientInfo)logMsg.Context.Get(LoggingConst.Client);//获取客户端信息
 | 
			
		||||
            var client = (UserAgent)logMsg.Context.Get(LoggingConst.Client);//获取客户端信息
 | 
			
		||||
            var path = logMsg.Context.Get(LoggingConst.Path).ToString();//获取操作名称
 | 
			
		||||
            var method = logMsg.Context.Get(LoggingConst.Method).ToString();//获取方法
 | 
			
		||||
            //表示访问日志
 | 
			
		||||
@@ -92,10 +90,10 @@ public class DatabaseLoggingWriter : IDatabaseLoggingWriter
 | 
			
		||||
    /// <param name="operation">操作名称</param>
 | 
			
		||||
    /// <param name="path">请求地址</param>
 | 
			
		||||
    /// <param name="loggingMonitor">loggingMonitor</param>
 | 
			
		||||
    /// <param name="clientInfo">客户端信息</param>
 | 
			
		||||
    /// <param name="userAgent">客户端信息</param>
 | 
			
		||||
    /// <param name="flush"></param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    private async Task<bool> CreateOperationLog(string operation, string path, LoggingMonitorJson loggingMonitor, ClientInfo clientInfo, bool flush)
 | 
			
		||||
    private async Task<bool> CreateOperationLog(string operation, string path, LoggingMonitorJson loggingMonitor, UserAgent userAgent, bool flush)
 | 
			
		||||
    {
 | 
			
		||||
        //账号
 | 
			
		||||
        var opAccount = loggingMonitor.AuthorizationClaims?.Where(it => it.Type == ClaimConst.Account).Select(it => it.Value).FirstOrDefault();
 | 
			
		||||
@@ -120,8 +118,8 @@ public class DatabaseLoggingWriter : IDatabaseLoggingWriter
 | 
			
		||||
            Category = LogCateGoryEnum.Operate,
 | 
			
		||||
            ExeStatus = true,
 | 
			
		||||
            OpIp = loggingMonitor.RemoteIPv4,
 | 
			
		||||
            OpBrowser = clientInfo?.UA?.Family + clientInfo?.UA?.Major,
 | 
			
		||||
            OpOs = clientInfo?.OS?.Family + clientInfo?.OS?.Major,
 | 
			
		||||
            OpBrowser = userAgent?.Browser,
 | 
			
		||||
            OpOs = userAgent?.Platform,
 | 
			
		||||
            OpTime = loggingMonitor.LogDateTime.LocalDateTime,
 | 
			
		||||
            OpAccount = opAccount,
 | 
			
		||||
            ReqMethod = loggingMonitor.HttpMethod,
 | 
			
		||||
@@ -161,9 +159,9 @@ public class DatabaseLoggingWriter : IDatabaseLoggingWriter
 | 
			
		||||
    /// <param name="operation">访问类型</param>
 | 
			
		||||
    /// <param name="path"></param>
 | 
			
		||||
    /// <param name="loggingMonitor">loggingMonitor</param>
 | 
			
		||||
    /// <param name="clientInfo">客户端信息</param>
 | 
			
		||||
    /// <param name="userAgent">客户端信息</param>
 | 
			
		||||
    /// <param name="flush"></param>
 | 
			
		||||
    private async Task<bool> CreateVisitLog(string operation, string path, LoggingMonitorJson loggingMonitor, ClientInfo clientInfo, bool flush)
 | 
			
		||||
    private async Task<bool> CreateVisitLog(string operation, string path, LoggingMonitorJson loggingMonitor, UserAgent userAgent, bool flush)
 | 
			
		||||
    {
 | 
			
		||||
        long verificatId = 0;//验证Id
 | 
			
		||||
        var opAccount = "";//用户账号
 | 
			
		||||
@@ -188,8 +186,8 @@ public class DatabaseLoggingWriter : IDatabaseLoggingWriter
 | 
			
		||||
            Category = path == "/api/auth/login" ? LogCateGoryEnum.Login : LogCateGoryEnum.Logout,
 | 
			
		||||
            ExeStatus = true,
 | 
			
		||||
            OpIp = loggingMonitor.RemoteIPv4,
 | 
			
		||||
            OpBrowser = clientInfo?.UA?.Family + clientInfo?.UA?.Major,
 | 
			
		||||
            OpOs = clientInfo?.OS?.Family + clientInfo?.OS?.Major,
 | 
			
		||||
            OpBrowser = userAgent?.Browser,
 | 
			
		||||
            OpOs = userAgent?.Platform,
 | 
			
		||||
            OpTime = loggingMonitor.LogDateTime.LocalDateTime,
 | 
			
		||||
            VerificatId = verificatId,
 | 
			
		||||
            OpAccount = opAccount,
 | 
			
		||||
 
 | 
			
		||||
@@ -15,12 +15,15 @@ using Microsoft.AspNetCore.WebUtilities;
 | 
			
		||||
 | 
			
		||||
using System.Security.Claims;
 | 
			
		||||
 | 
			
		||||
using UAParser;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Admin.Application;
 | 
			
		||||
 | 
			
		||||
public class AppService : IAppService
 | 
			
		||||
{
 | 
			
		||||
    private readonly IUserAgentService UserAgentService;
 | 
			
		||||
    public AppService(IUserAgentService userAgentService)
 | 
			
		||||
    {
 | 
			
		||||
        UserAgentService = userAgentService;
 | 
			
		||||
    }
 | 
			
		||||
    public string GetReturnUrl(string returnUrl)
 | 
			
		||||
    {
 | 
			
		||||
        var url = QueryHelpers.AddQueryString(CookieAuthenticationDefaults.LoginPath, new Dictionary<string, string?>
 | 
			
		||||
@@ -41,18 +44,16 @@ public class AppService : IAppService
 | 
			
		||||
        {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    public Parser Parser = Parser.GetDefault();
 | 
			
		||||
    public ClientInfo? ClientInfo
 | 
			
		||||
    public UserAgent? UserAgent
 | 
			
		||||
    {
 | 
			
		||||
        get
 | 
			
		||||
        {
 | 
			
		||||
            var str = App.HttpContext?.Request?.Headers?.UserAgent;
 | 
			
		||||
            ClientInfo? clientInfo = null;
 | 
			
		||||
            if (!string.IsNullOrEmpty(str))
 | 
			
		||||
            {
 | 
			
		||||
                clientInfo = Parser.Parse(str);
 | 
			
		||||
                return UserAgentService.Parse(str);
 | 
			
		||||
            }
 | 
			
		||||
            return clientInfo;
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -13,19 +13,17 @@ using Microsoft.Extensions.DependencyInjection;
 | 
			
		||||
 | 
			
		||||
using System.Security.Claims;
 | 
			
		||||
 | 
			
		||||
using UAParser;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Admin.Application;
 | 
			
		||||
 | 
			
		||||
public class HybridAppService : IAppService
 | 
			
		||||
{
 | 
			
		||||
    public HybridAppService()
 | 
			
		||||
    public HybridAppService(IUserAgentService userAgentService)
 | 
			
		||||
    {
 | 
			
		||||
        var str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Edg/127.0.0.0";
 | 
			
		||||
        ClientInfo = Parser.GetDefault().Parse(str);
 | 
			
		||||
        UserAgent = userAgentService.Parse(str);
 | 
			
		||||
        RemoteIpAddress = "127.0.0.1";
 | 
			
		||||
    }
 | 
			
		||||
    public ClientInfo? ClientInfo { get; }
 | 
			
		||||
    public UserAgent? UserAgent { get; }
 | 
			
		||||
 | 
			
		||||
    private static BlazorHybridAuthenticationStateProvider _authenticationStateProvider;
 | 
			
		||||
    private static BlazorHybridAuthenticationStateProvider AuthenticationStateProvider
 | 
			
		||||
 
 | 
			
		||||
@@ -11,8 +11,6 @@
 | 
			
		||||
 | 
			
		||||
using System.Security.Claims;
 | 
			
		||||
 | 
			
		||||
using UAParser;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Admin.Application;
 | 
			
		||||
 | 
			
		||||
public interface IAppService
 | 
			
		||||
@@ -20,7 +18,7 @@ public interface IAppService
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// ClientInfo
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public ClientInfo? ClientInfo { get; }
 | 
			
		||||
    public UserAgent? UserAgent { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// ClaimsPrincipal
 | 
			
		||||
 
 | 
			
		||||
@@ -237,7 +237,7 @@ public class AuthService : IAuthService
 | 
			
		||||
        var logingEvent = new LoginEvent
 | 
			
		||||
        {
 | 
			
		||||
            Ip = _appService.RemoteIpAddress,
 | 
			
		||||
            Device = App.GetService<IAppService>().ClientInfo?.OS?.ToString(),
 | 
			
		||||
            Device = App.GetService<IAppService>().UserAgent?.Platform,
 | 
			
		||||
            Expire = expire,
 | 
			
		||||
            SysUser = sysUser,
 | 
			
		||||
            VerificatId = verificatId
 | 
			
		||||
 
 | 
			
		||||
@@ -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("ClientIds", new ConcurrentList<long>().ToJsonNetString()).Where(a => a.Id >= 0).ExecuteCommand();
 | 
			
		||||
        db.Updateable<VerificatInfo>().SetColumns(a => a.ClientIds == null).Where(a => a.Id > 0).ExecuteCommand();
 | 
			
		||||
        VerificatInfoService.RemoveCache();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -24,6 +24,11 @@ public sealed class SqlSugarOption : ConnectionConfig
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public bool InitSeedData { get; set; } = false;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 初始化数据库
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public bool InitDatabase { get; set; } = false;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 初始化表
 | 
			
		||||
    /// </summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -36,6 +36,7 @@ public class Startup : AppStartup
 | 
			
		||||
        services.AddSingleton<ISugarAopService, SugarAopService>();
 | 
			
		||||
        services.AddSingleton<ISugarConfigAopService, SugarConfigAopService>();
 | 
			
		||||
 | 
			
		||||
        services.AddSingleton<IUserAgentService, UserAgentService>();
 | 
			
		||||
        services.AddSingleton<IAppService, AppService>();
 | 
			
		||||
 | 
			
		||||
        StaticConfig.EnableAllWhereIF = true;
 | 
			
		||||
@@ -89,7 +90,7 @@ public class Startup : AppStartup
 | 
			
		||||
        DbContext.DbConfigs?.ForEach(it =>
 | 
			
		||||
        {
 | 
			
		||||
            var connection = DbContext.Db.GetConnection(it.ConfigId);//获取数据库连接对象
 | 
			
		||||
            if (it.InitTable == true)
 | 
			
		||||
            if (it.InitDatabase == true)
 | 
			
		||||
                connection.DbMaintenance.CreateDatabase();//创建数据库,如果存在则不创建
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -19,7 +19,6 @@
 | 
			
		||||
	
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
		<PackageReference Include="BootstrapBlazor.TableExport" Version="9.2.4" />
 | 
			
		||||
		<PackageReference Include="UAParser" Version="3.1.47" />
 | 
			
		||||
		<PackageReference Include="Rougamo.Fody" Version="5.0.0" />
 | 
			
		||||
		<PackageReference Include="SqlSugarCore" Version="5.1.4.193" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,14 @@
 | 
			
		||||
namespace ThingsGateway.Admin.Application
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>Default interface for UserAgentService</summary>
 | 
			
		||||
    public interface IUserAgentService
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>Gets or sets the settings.</summary>
 | 
			
		||||
        public UserAgentSettings Settings { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>Parses the specified user agent string.</summary>
 | 
			
		||||
        /// <param name="userAgentString">The user agent string.</param>
 | 
			
		||||
        /// <returns>An UserAgent object</returns>
 | 
			
		||||
        UserAgent? Parse(string userAgentString);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										145
									
								
								src/Admin/ThingsGateway.Admin.Application/UserAgent/UserAgent.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										145
									
								
								src/Admin/ThingsGateway.Admin.Application/UserAgent/UserAgent.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,145 @@
 | 
			
		||||
using System.Text.RegularExpressions;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Admin.Application
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Parsed UserAgent object
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public class UserAgent
 | 
			
		||||
    {
 | 
			
		||||
        private readonly UserAgentSettings settings;
 | 
			
		||||
 | 
			
		||||
        internal string Agent = "";
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets a value indicating whether this UserAgent is a browser.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>
 | 
			
		||||
        ///   <c>true</c> if this UserAgent is a browser; otherwise, <c>false</c>.
 | 
			
		||||
        /// </value>
 | 
			
		||||
        public bool IsBrowser { get; set; } = false;
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets a value indicating whether this UserAgent is a robot.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>
 | 
			
		||||
        ///   <c>true</c> if this UserAgent is a robot; otherwise, <c>false</c>.
 | 
			
		||||
        /// </value>
 | 
			
		||||
        public bool IsRobot { get; set; } = false;
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets a value indicating whether this UserAgent is a mobile device.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>
 | 
			
		||||
        ///   <c>true</c> if this UserAgent is a mobile device; otherwise, <c>false</c>.
 | 
			
		||||
        /// </value>
 | 
			
		||||
        public bool IsMobile { get; set; } = false;
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the platform.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>
 | 
			
		||||
        /// The platform or operating system.
 | 
			
		||||
        /// </value>
 | 
			
		||||
        public string Platform { get; set; } = "";
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the browser.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>
 | 
			
		||||
        /// The browser.
 | 
			
		||||
        /// </value>
 | 
			
		||||
        public string Browser { get; set; } = "";
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the browser version.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>
 | 
			
		||||
        /// The browser version.
 | 
			
		||||
        /// </value>
 | 
			
		||||
        public string BrowserVersion { get; set; } = "";
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the mobile device.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>
 | 
			
		||||
        /// The mobile device.
 | 
			
		||||
        /// </value>
 | 
			
		||||
        public string Mobile { get; set; } = "";
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the robot.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>
 | 
			
		||||
        /// The robot.
 | 
			
		||||
        /// </value>
 | 
			
		||||
        public string Robot { get; set; } = "";
 | 
			
		||||
 | 
			
		||||
        internal UserAgent(UserAgentSettings settings, string? userAgentString = null)
 | 
			
		||||
        {
 | 
			
		||||
            this.settings = settings;
 | 
			
		||||
 | 
			
		||||
            if (userAgentString != null)
 | 
			
		||||
            {
 | 
			
		||||
                Agent = userAgentString.Trim();
 | 
			
		||||
                SetPlatform();
 | 
			
		||||
                if (SetRobot()) return;
 | 
			
		||||
                if (SetBrowser()) return;
 | 
			
		||||
                if (SetMobile()) return;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        internal bool SetPlatform()
 | 
			
		||||
        {
 | 
			
		||||
            foreach (var item in settings.Platforms)
 | 
			
		||||
            {
 | 
			
		||||
                if (Regex.IsMatch(Agent, $"{Regex.Escape(item.Key)}", RegexOptions.IgnoreCase))
 | 
			
		||||
                {
 | 
			
		||||
                    Platform = item.Value;
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            Platform = "Unknown Platform";
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        internal bool SetBrowser()
 | 
			
		||||
        {
 | 
			
		||||
            foreach (var item in settings.Browsers)
 | 
			
		||||
            {
 | 
			
		||||
                var match = Regex.Match(Agent, $@"{item.Key}.*?([0-9\.]+)", RegexOptions.IgnoreCase);
 | 
			
		||||
                if (match.Success)
 | 
			
		||||
                {
 | 
			
		||||
                    IsBrowser = true;
 | 
			
		||||
                    BrowserVersion = match.Groups[1].Value;
 | 
			
		||||
                    Browser = item.Value;
 | 
			
		||||
                    SetMobile();
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        internal bool SetRobot()
 | 
			
		||||
        {
 | 
			
		||||
            foreach (var item in settings.Robots)
 | 
			
		||||
            {
 | 
			
		||||
                if (Regex.IsMatch(Agent, $"{Regex.Escape(item.Key)}", RegexOptions.IgnoreCase))
 | 
			
		||||
                {
 | 
			
		||||
                    IsRobot = true;
 | 
			
		||||
                    Robot = item.Value;
 | 
			
		||||
                    SetMobile();
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        internal bool SetMobile()
 | 
			
		||||
        {
 | 
			
		||||
            foreach (var item in settings.Mobiles)
 | 
			
		||||
            {
 | 
			
		||||
                if (Agent?.IndexOf(item.Key, StringComparison.OrdinalIgnoreCase) != -1)
 | 
			
		||||
                {
 | 
			
		||||
                    IsMobile = true;
 | 
			
		||||
                    Mobile = item.Value;
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,43 @@
 | 
			
		||||
using ThingsGateway.NewLife.Caching;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Admin.Application
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// The UserAgent service
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <seealso cref="ThingsGateway.Admin.Application.IUserAgentService" />
 | 
			
		||||
    public class UserAgentService : IUserAgentService
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the settings.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public UserAgentSettings Settings { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Initializes a new instance of the <see cref="UserAgentService"/> class.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public UserAgentService()
 | 
			
		||||
        {
 | 
			
		||||
            Settings = new UserAgentSettings();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private MemoryCache MemoryCache { get; set; } = new();
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Parses the specified user agent string.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="userAgentString">The user agent string.</param>
 | 
			
		||||
        /// <returns>
 | 
			
		||||
        /// An UserAgent object
 | 
			
		||||
        /// </returns>
 | 
			
		||||
        public UserAgent? Parse(string? userAgentString)
 | 
			
		||||
        {
 | 
			
		||||
            userAgentString = ((userAgentString?.Length > Settings.UaStringSizeLimit) ? userAgentString?.Trim().Substring(0, Settings.UaStringSizeLimit) : userAgentString?.Trim()) ?? "";
 | 
			
		||||
            return MemoryCache.GetOrAdd(userAgentString, entry =>
 | 
			
		||||
            {
 | 
			
		||||
                return new UserAgent(Settings, userAgentString);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,214 @@
 | 
			
		||||
namespace ThingsGateway.Admin.Application
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// UserAgent settings container.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public class UserAgentSettings
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the maximum size of the useragent string. Limiting the length of the useragent string protects from hackers sending in extremely long user agent strings.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public int UaStringSizeLimit { get; set; } = 512;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets a dictionary containing mappings for platforms.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public Dictionary<string, string> Platforms { get; } = new()
 | 
			
		||||
        {
 | 
			
		||||
            {"windows nt 10.0", "Windows 10"},
 | 
			
		||||
            {"windows nt 6.3", "Windows 8.1"},
 | 
			
		||||
            {"windows nt 6.2", "Windows 8"},
 | 
			
		||||
            {"windows nt 6.1", "Windows 7"},
 | 
			
		||||
            {"windows nt 6.0", "Windows Vista"},
 | 
			
		||||
            {"windows nt 5.2", "Windows 2003"},
 | 
			
		||||
            {"windows nt 5.1", "Windows XP"},
 | 
			
		||||
            {"windows nt 5.0", "Windows 2000"},
 | 
			
		||||
            {"windows nt 4.0", "Windows NT 4.0"},
 | 
			
		||||
            {"winnt4.0", "Windows NT 4.0"},
 | 
			
		||||
            {"winnt 4.0", "Windows NT"},
 | 
			
		||||
            {"winnt", "Windows NT"},
 | 
			
		||||
            {"windows 98", "Windows 98"},
 | 
			
		||||
            {"win98", "Windows 98"},
 | 
			
		||||
            {"windows 95", "Windows 95"},
 | 
			
		||||
            {"win95", "Windows 95"},
 | 
			
		||||
            {"windows phone", "Windows Phone"},
 | 
			
		||||
            {"windows", "Unknown Windows OS"},
 | 
			
		||||
            {"android", "Android"},
 | 
			
		||||
            {"blackberry", "BlackBerry"},
 | 
			
		||||
            {"iphone", "iOS"},
 | 
			
		||||
            {"ipad", "iOS"},
 | 
			
		||||
            {"ipod", "iOS"},
 | 
			
		||||
            {"os x", "Mac OS X"},
 | 
			
		||||
            {"ppc mac", "Power PC Mac"},
 | 
			
		||||
            {"freebsd", "FreeBSD"},
 | 
			
		||||
            {"ppc", "Macintosh"},
 | 
			
		||||
            {"linux", "Linux"},
 | 
			
		||||
            {"debian", "Debian"},
 | 
			
		||||
            {"sunos", "Sun Solaris"},
 | 
			
		||||
            {"beos", "BeOS"},
 | 
			
		||||
            {"apachebench", "ApacheBench"},
 | 
			
		||||
            {"aix", "AIX"},
 | 
			
		||||
            {"irix", "Irix"},
 | 
			
		||||
            {"osf", "DEC OSF"},
 | 
			
		||||
            {"hp-ux", "HP-UX"},
 | 
			
		||||
            {"netbsd", "NetBSD"},
 | 
			
		||||
            {"bsdi", "BSDi"},
 | 
			
		||||
            {"openbsd", "OpenBSD"},
 | 
			
		||||
            {"gnu", "GNU/Linux"},
 | 
			
		||||
            {"unix", "Unknown Unix OS"},
 | 
			
		||||
            {"symbian", "Symbian OS"},
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets a dictionary containing mappings for browsers.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public Dictionary<string, string> Browsers { get; } = new()
 | 
			
		||||
        {
 | 
			
		||||
            {"Microsoft Outlook", "Microsoft Outlook"},
 | 
			
		||||
            {"OPR", "Opera"},
 | 
			
		||||
            {"Flock", "Flock"},
 | 
			
		||||
            {"Edge", "Edge"},
 | 
			
		||||
            {"Edg", "Edge"},
 | 
			
		||||
            {"Chrome", "Chrome"},
 | 
			
		||||
            {"Opera.*?Version", "Opera"},
 | 
			
		||||
            {"Opera", "Opera"},
 | 
			
		||||
            {"MSIE", "Internet Explorer"},
 | 
			
		||||
            {"Internet Explorer", "Internet Explorer"},
 | 
			
		||||
            {"Trident.* rv" , "Internet Explorer"},
 | 
			
		||||
            {"Shiira", "Shiira"},
 | 
			
		||||
            {"Firefox", "Firefox"},
 | 
			
		||||
            {"Chimera", "Chimera"},
 | 
			
		||||
            {"Phoenix", "Phoenix"},
 | 
			
		||||
            {"Firebird", "Firebird"},
 | 
			
		||||
            {"Camino", "Camino"},
 | 
			
		||||
            {"Netscape", "Netscape"},
 | 
			
		||||
            {"OmniWeb", "OmniWeb"},
 | 
			
		||||
            {"Safari", "Safari"},
 | 
			
		||||
            {"Mozilla", "Mozilla"},
 | 
			
		||||
            {"Konqueror", "Konqueror"},
 | 
			
		||||
            {"icab", "iCab"},
 | 
			
		||||
            {"Lynx", "Lynx"},
 | 
			
		||||
            {"Links", "Links"},
 | 
			
		||||
            {"hotjava", "HotJava"},
 | 
			
		||||
            {"amaya", "Amaya"},
 | 
			
		||||
            {"IBrowse", "IBrowse"},
 | 
			
		||||
            {"Maxthon", "Maxthon"},
 | 
			
		||||
            {"Ubuntu", "Ubuntu Web Browser"},
 | 
			
		||||
            {"Vivaldi", "Vivaldi"},
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets a dictionary containing mappings for mobiles.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public Dictionary<string, string> Mobiles { get; } = new()
 | 
			
		||||
        {
 | 
			
		||||
            // Legacy
 | 
			
		||||
            {"mobileexplorer", "Mobile Explorer"},
 | 
			
		||||
            {"palmsource", "Palm"},
 | 
			
		||||
            {"palmscape", "Palmscape"},
 | 
			
		||||
            // Phones and Manufacturers
 | 
			
		||||
            {"motorola", "Motorola"},
 | 
			
		||||
            {"nokia", "Nokia"},
 | 
			
		||||
            {"palm", "Palm"},
 | 
			
		||||
            {"iphone", "Apple iPhone"},
 | 
			
		||||
            {"ipad", "iPad"},
 | 
			
		||||
            {"ipod", "Apple iPod Touch"},
 | 
			
		||||
            {"sony", "Sony Ericsson"},
 | 
			
		||||
            {"ericsson", "Sony Ericsson"},
 | 
			
		||||
            {"blackberry", "BlackBerry"},
 | 
			
		||||
            {"cocoon", "O2 Cocoon"},
 | 
			
		||||
            {"blazer", "Treo"},
 | 
			
		||||
            {"lg", "LG"},
 | 
			
		||||
            {"amoi", "Amoi"},
 | 
			
		||||
            {"xda", "XDA"},
 | 
			
		||||
            {"mda", "MDA"},
 | 
			
		||||
            {"vario", "Vario"},
 | 
			
		||||
            {"htc", "HTC"},
 | 
			
		||||
            {"samsung", "Samsung"},
 | 
			
		||||
            {"sharp", "Sharp"},
 | 
			
		||||
            {"sie-", "Siemens"},
 | 
			
		||||
            {"alcatel", "Alcatel"},
 | 
			
		||||
            {"benq", "BenQ"},
 | 
			
		||||
            {"ipaq", "HP iPaq"},
 | 
			
		||||
            {"mot-", "Motorola"},
 | 
			
		||||
            {"playstation portable", "PlayStation Portable"},
 | 
			
		||||
            {"playstation 3", "PlayStation 3"},
 | 
			
		||||
            {"playstation vita", "PlayStation Vita"},
 | 
			
		||||
            {"hiptop", "Danger Hiptop"},
 | 
			
		||||
            {"nec-", "NEC"},
 | 
			
		||||
            {"panasonic", "Panasonic"},
 | 
			
		||||
            {"philips", "Philips"},
 | 
			
		||||
            {"sagem", "Sagem"},
 | 
			
		||||
            {"sanyo", "Sanyo"},
 | 
			
		||||
            {"spv", "SPV"},
 | 
			
		||||
            {"zte", "ZTE"},
 | 
			
		||||
            {"sendo", "Sendo"},
 | 
			
		||||
            {"nintendo dsi", "Nintendo DSi"},
 | 
			
		||||
            {"nintendo ds", "Nintendo DS"},
 | 
			
		||||
            {"nintendo 3ds", "Nintendo 3DS"},
 | 
			
		||||
            {"wii", "Nintendo Wii"},
 | 
			
		||||
            {"open web", "Open Web"},
 | 
			
		||||
            {"openweb", "OpenWeb"},
 | 
			
		||||
            // Operating Systems
 | 
			
		||||
            {"android", "Android"},
 | 
			
		||||
            {"symbian", "Symbian"},
 | 
			
		||||
            {"SymbianOS", "SymbianOS"},
 | 
			
		||||
            {"elaine", "Palm"},
 | 
			
		||||
            {"series60", "Symbian S60"},
 | 
			
		||||
            {"windows ce", "Windows CE"},
 | 
			
		||||
            // Browsers
 | 
			
		||||
            {"obigo", "Obigo"},
 | 
			
		||||
            {"netfront", "Netfront Browser"},
 | 
			
		||||
            {"openwave", "Openwave Browser"},
 | 
			
		||||
            {"mobilexplorer", "Mobile Explorer"},
 | 
			
		||||
            {"operamini", "Opera Mini"},
 | 
			
		||||
            {"opera mini", "Opera Mini"},
 | 
			
		||||
            {"opera mobi", "Opera Mobile"},
 | 
			
		||||
            {"fennec", "Firefox Mobile"},
 | 
			
		||||
            // Other
 | 
			
		||||
            {"digital paths", "Digital Paths"},
 | 
			
		||||
            {"avantgo", "AvantGo"},
 | 
			
		||||
            {"xiino", "Xiino"},
 | 
			
		||||
            {"novarra", "Novarra Transcoder"},
 | 
			
		||||
            {"vodafone", "Vodafone"},
 | 
			
		||||
            {"docomo", "NTT DoCoMo"},
 | 
			
		||||
            {"o2", "O2"},
 | 
			
		||||
            // Fallback
 | 
			
		||||
            {"mobile", "Generic Mobile"},
 | 
			
		||||
            {"wireless", "Generic Mobile"},
 | 
			
		||||
            {"j2me", "Generic Mobile"},
 | 
			
		||||
            {"midp", "Generic Mobile"},
 | 
			
		||||
            {"cldc", "Generic Mobile"},
 | 
			
		||||
            {"up.link", "Generic Mobile"},
 | 
			
		||||
            {"up.browser", "Generic Mobile"},
 | 
			
		||||
            {"smartphone", "Generic Mobile"},
 | 
			
		||||
            {"cellphone", "Generic Mobile"},
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets a dictionary containing mappings for robots.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public Dictionary<string, string> Robots { get; } = new()
 | 
			
		||||
        {
 | 
			
		||||
            {"googlebot", "Googlebot"},
 | 
			
		||||
            {"msnbot", "MSNBot"},
 | 
			
		||||
            {"baiduspider", "Baiduspider"},
 | 
			
		||||
            {"bingbot", "Bing"},
 | 
			
		||||
            {"slurp", "Inktomi Slurp"},
 | 
			
		||||
            {"yahoo", "Yahoo"},
 | 
			
		||||
            {"ask jeeves", "Ask Jeeves"},
 | 
			
		||||
            {"fastcrawler", "FastCrawler"},
 | 
			
		||||
            {"infoseek", "InfoSeek Robot 1.0"},
 | 
			
		||||
            {"lycos", "Lycos"},
 | 
			
		||||
            {"yandex", "YandexBot"},
 | 
			
		||||
            {"mediapartners-google", "MediaPartners Google"},
 | 
			
		||||
            {"CRAZYWEBCRAWLER", "Crazy Webcrawler"},
 | 
			
		||||
            {"adsbot-google", "AdsBot Google"},
 | 
			
		||||
            {"feedfetcher-google", "Feedfetcher Google"},
 | 
			
		||||
            {"curious george", "Curious George"},
 | 
			
		||||
            {"ia_archiver", "Alexa Crawler"},
 | 
			
		||||
            {"MJ12bot", "Majestic-12"},
 | 
			
		||||
            {"Uptimebot", "Uptimebot"},
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -220,7 +220,7 @@ public class Startup : AppStartup
 | 
			
		||||
                var httpContext = context.HttpContext;//获取httpContext
 | 
			
		||||
 | 
			
		||||
                //获取客户端信息
 | 
			
		||||
                var client = App.GetService<IAppService>().ClientInfo;
 | 
			
		||||
                var client = App.GetService<IAppService>().UserAgent;
 | 
			
		||||
                // 获取控制器/操作描述器
 | 
			
		||||
                var controllerActionDescriptor = context.ActionDescriptor as ControllerActionDescriptor;
 | 
			
		||||
                //操作名称默认是控制器名加方法名,自定义操作名称要在action上加Description特性
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,8 @@
 | 
			
		||||
using System.Buffers;
 | 
			
		||||
using System.Runtime.CompilerServices;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.NewLife.Collections;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.NewLife.Buffers;
 | 
			
		||||
 | 
			
		||||
internal sealed class BufferSegment : ReadOnlySequenceSegment<Byte>
 | 
			
		||||
@@ -81,7 +83,7 @@ internal sealed class BufferSegment : ReadOnlySequenceSegment<Byte>
 | 
			
		||||
        }
 | 
			
		||||
        else if (_array != null)
 | 
			
		||||
        {
 | 
			
		||||
            ArrayPool<Byte>.Shared.Return(_array);
 | 
			
		||||
            Pool.Shared.Return(_array);
 | 
			
		||||
            _array = null;
 | 
			
		||||
        }
 | 
			
		||||
        base.Memory = default;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,8 @@
 | 
			
		||||
using System.Buffers;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.NewLife.Collections;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#if NETFRAMEWORK || NETSTANDARD2_0
 | 
			
		||||
using ValueTask = System.Threading.Tasks.Task;
 | 
			
		||||
#endif
 | 
			
		||||
@@ -25,7 +28,7 @@ public sealed class PooledByteBufferWriter : IBufferWriter<Byte>, IDisposable
 | 
			
		||||
    /// <param name="initialCapacity"></param>
 | 
			
		||||
    public PooledByteBufferWriter(Int32 initialCapacity)
 | 
			
		||||
    {
 | 
			
		||||
        _rentedBuffer = ArrayPool<Byte>.Shared.Rent(initialCapacity);
 | 
			
		||||
        _rentedBuffer = Pool.Shared.Rent(initialCapacity);
 | 
			
		||||
        _index = 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -42,7 +45,7 @@ public sealed class PooledByteBufferWriter : IBufferWriter<Byte>, IDisposable
 | 
			
		||||
    /// <param name="initialCapacity"></param>
 | 
			
		||||
    public void InitializeEmptyInstance(Int32 initialCapacity)
 | 
			
		||||
    {
 | 
			
		||||
        _rentedBuffer = ArrayPool<Byte>.Shared.Rent(initialCapacity);
 | 
			
		||||
        _rentedBuffer = Pool.Shared.Rent(initialCapacity);
 | 
			
		||||
        _index = 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -60,7 +63,7 @@ public sealed class PooledByteBufferWriter : IBufferWriter<Byte>, IDisposable
 | 
			
		||||
 | 
			
		||||
        var rentedBuffer = _rentedBuffer;
 | 
			
		||||
        _rentedBuffer = null!;
 | 
			
		||||
        ArrayPool<Byte>.Shared.Return(rentedBuffer);
 | 
			
		||||
        Pool.Shared.Return(rentedBuffer);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>通知 IBufferWriter,已向输出写入 count 数据项。</summary>
 | 
			
		||||
@@ -116,11 +119,11 @@ public sealed class PooledByteBufferWriter : IBufferWriter<Byte>, IDisposable
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        var rentedBuffer = _rentedBuffer;
 | 
			
		||||
        _rentedBuffer = ArrayPool<Byte>.Shared.Rent(num4);
 | 
			
		||||
        _rentedBuffer = Pool.Shared.Rent(num4);
 | 
			
		||||
        var span = rentedBuffer.AsSpan(0, _index);
 | 
			
		||||
        span.CopyTo(_rentedBuffer);
 | 
			
		||||
        span.Clear();
 | 
			
		||||
        ArrayPool<Byte>.Shared.Return(rentedBuffer);
 | 
			
		||||
        Pool.Shared.Return(rentedBuffer);
 | 
			
		||||
    }
 | 
			
		||||
    #endregion
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -242,7 +242,7 @@ public static class SpanHelper
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var array = ArrayPool<Byte>.Shared.Rent(buffer.Length);
 | 
			
		||||
        var array = Pool.Shared.Rent(buffer.Length);
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
@@ -252,7 +252,7 @@ public static class SpanHelper
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            ArrayPool<Byte>.Shared.Return(array);
 | 
			
		||||
            Pool.Shared.Return(array);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -266,7 +266,7 @@ public static class SpanHelper
 | 
			
		||||
        if (MemoryMarshal.TryGetArray(buffer, out var segment))
 | 
			
		||||
            return stream.WriteAsync(segment.Array!, segment.Offset, segment.Count, cancellationToken);
 | 
			
		||||
 | 
			
		||||
        var array = ArrayPool<Byte>.Shared.Rent(buffer.Length);
 | 
			
		||||
        var array = Pool.Shared.Rent(buffer.Length);
 | 
			
		||||
        buffer.Span.CopyTo(array);
 | 
			
		||||
 | 
			
		||||
        var writeTask = stream.WriteAsync(array, 0, buffer.Length, cancellationToken);
 | 
			
		||||
@@ -278,7 +278,7 @@ public static class SpanHelper
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
                ArrayPool<Byte>.Shared.Return(array);
 | 
			
		||||
                Pool.Shared.Return(array);
 | 
			
		||||
            }
 | 
			
		||||
        }, cancellationToken);
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -324,13 +324,21 @@ 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);
 | 
			
		||||
                        count++;
 | 
			
		||||
                        Interlocked.Decrement(ref _FreeCount);
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        // 可能是另一个对象,放回去
 | 
			
		||||
                        _free2.Enqueue(pi2);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -157,7 +157,7 @@ public class Binary : FormatterBase, IBinary
 | 
			
		||||
#if NETCOREAPP || NETSTANDARD2_1_OR_GREATER
 | 
			
		||||
        Stream.Write(buffer);
 | 
			
		||||
#else
 | 
			
		||||
        var array = ArrayPool<Byte>.Shared.Rent(buffer.Length);
 | 
			
		||||
        var array = Pool.Shared.Rent(buffer.Length);
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            buffer.CopyTo(array);
 | 
			
		||||
@@ -166,7 +166,7 @@ public class Binary : FormatterBase, IBinary
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            ArrayPool<Byte>.Shared.Return(array);
 | 
			
		||||
            Pool.Shared.Return(array);
 | 
			
		||||
        }
 | 
			
		||||
#endif
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -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.0</PluginVersion>
 | 
			
		||||
		<ProPluginVersion>10.6.0</ProPluginVersion>
 | 
			
		||||
		<PluginVersion>10.6.11</PluginVersion>
 | 
			
		||||
		<ProPluginVersion>10.6.11</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>
 | 
			
		||||
 
 | 
			
		||||
@@ -141,7 +141,7 @@ public class ControlController : ControllerBase
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return await GlobalData.RpcService.InvokeDeviceMethodAsync($"WebApi-{UserManager.UserAccount}-{App.HttpContext?.GetRemoteIpAddressToIPv4()}", deviceDatas).ConfigureAwait(false);
 | 
			
		||||
        return (await GlobalData.RpcService.InvokeDeviceMethodAsync($"WebApi-{UserManager.UserAccount}-{App.HttpContext?.GetRemoteIpAddressToIPv4()}", deviceDatas).ConfigureAwait(false)).ToDictionary(a => a.Key, a => a.Value.ToDictionary(b => b.Key, b => (OperResult)b.Value));
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -31,7 +31,7 @@ namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
/// 采集插件,继承实现不同PLC通讯
 | 
			
		||||
/// <para></para>
 | 
			
		||||
/// </summary>
 | 
			
		||||
public abstract class CollectBase : DriverBase
 | 
			
		||||
public abstract class CollectBase : DriverBase, IRpcDriver
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 插件配置项
 | 
			
		||||
@@ -550,7 +550,7 @@ public abstract class CollectBase : DriverBase
 | 
			
		||||
    /// <param name="writeInfoLists">要写入的变量及其对应的数据</param>
 | 
			
		||||
    /// <param name="cancellationToken">取消操作的通知</param>
 | 
			
		||||
    /// <returns>写入操作的结果字典</returns>
 | 
			
		||||
    internal async ValueTask<Dictionary<string, OperResult<object>>> InvokeMethodAsync(Dictionary<VariableRuntime, JToken> writeInfoLists, CancellationToken cancellationToken)
 | 
			
		||||
    public async ValueTask<Dictionary<string, Dictionary<string, IOperResult>>> InvokeMethodAsync(Dictionary<VariableRuntime, JToken> writeInfoLists, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        // 初始化结果字典
 | 
			
		||||
        Dictionary<string, OperResult<object>> results = new Dictionary<string, OperResult<object>>();
 | 
			
		||||
@@ -610,7 +610,13 @@ public abstract class CollectBase : DriverBase
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 将转换失败的变量和写入成功的变量的操作结果合并到结果字典中
 | 
			
		||||
        return results.Concat(operResults).ToDictionary(a => a.Key, a => a.Value);
 | 
			
		||||
        return new Dictionary<string, Dictionary<string, IOperResult>>()
 | 
			
		||||
        {
 | 
			
		||||
            {
 | 
			
		||||
               this.DeviceName ,
 | 
			
		||||
            results.Concat(operResults).ToDictionary(a => a.Key, a => (IOperResult)a.Value)
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -619,7 +625,7 @@ public abstract class CollectBase : DriverBase
 | 
			
		||||
    /// <param name="writeInfoLists">要写入的变量及其对应的数据</param>
 | 
			
		||||
    /// <param name="cancellationToken">取消操作的通知</param>
 | 
			
		||||
    /// <returns>写入操作的结果字典</returns>
 | 
			
		||||
    internal async ValueTask<Dictionary<string, OperResult>> InVokeWriteAsync(Dictionary<VariableRuntime, JToken> writeInfoLists, CancellationToken cancellationToken)
 | 
			
		||||
    public async ValueTask<Dictionary<string, Dictionary<string, IOperResult>>> InVokeWriteAsync(Dictionary<VariableRuntime, JToken> writeInfoLists, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        // 初始化结果字典
 | 
			
		||||
        Dictionary<string, OperResult> results = new Dictionary<string, OperResult>();
 | 
			
		||||
@@ -664,7 +670,14 @@ public abstract class CollectBase : DriverBase
 | 
			
		||||
            cancellationToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
        // 将转换失败的变量和写入成功的变量的操作结果合并到结果字典中
 | 
			
		||||
        return results.Concat(results1).ToDictionary(a => a.Key, a => a.Value);
 | 
			
		||||
 | 
			
		||||
        return new Dictionary<string, Dictionary<string, IOperResult>>()
 | 
			
		||||
        {
 | 
			
		||||
            {
 | 
			
		||||
               this.DeviceName ,
 | 
			
		||||
           results.Concat(results1).ToDictionary(a => a.Key, a => (IOperResult)a.Value)
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -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));
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,23 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
//  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
 | 
			
		||||
//  此代码版权(除特别声明外的代码)归作者本人Diego所有
 | 
			
		||||
//  源代码使用协议遵循本仓库的开源协议及附加协议
 | 
			
		||||
//  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
 | 
			
		||||
//  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
 | 
			
		||||
//  使用文档:https://thingsgateway.cn/
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
using Newtonsoft.Json.Linq;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
 | 
			
		||||
public interface IRpcDriver
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    ValueTask<Dictionary<string, Dictionary<string, IOperResult>>> InvokeMethodAsync(Dictionary<VariableRuntime, JToken> writeInfoLists, CancellationToken cancellationToken);
 | 
			
		||||
 | 
			
		||||
    ValueTask<Dictionary<string, Dictionary<string, IOperResult>>> InVokeWriteAsync(Dictionary<VariableRuntime, JToken> writeInfoLists, CancellationToken cancellationToken);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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} 不可为空",
 | 
			
		||||
 
 | 
			
		||||
@@ -120,10 +120,7 @@ public class ChannelRuntime : Channel, IChannelOptions, IDisposable
 | 
			
		||||
        // 通过插件名称获取插件信息
 | 
			
		||||
        PluginInfo = GlobalData.PluginService.GetList().FirstOrDefault(A => A.FullName == PluginName);
 | 
			
		||||
 | 
			
		||||
        if (PluginInfo == null)
 | 
			
		||||
        {
 | 
			
		||||
            //throw new Exception($"Plugin {PluginName} not found");
 | 
			
		||||
        }
 | 
			
		||||
        GlobalData.Channels.TryRemove(Id, out _);
 | 
			
		||||
 | 
			
		||||
        GlobalData.Channels.TryAdd(Id, this);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -224,7 +224,16 @@ public class DeviceRuntime : Device, IDisposable
 | 
			
		||||
    [AutoGenerateColumn(Ignore = true)]
 | 
			
		||||
    public IDriver? Driver { get; internal set; }
 | 
			
		||||
 | 
			
		||||
    [System.Text.Json.Serialization.JsonIgnore]
 | 
			
		||||
    [Newtonsoft.Json.JsonIgnore]
 | 
			
		||||
    [AdaptIgnore]
 | 
			
		||||
    [AutoGenerateColumn(Ignore = true)]
 | 
			
		||||
    public IRpcDriver? RpcDriver { get; set; }
 | 
			
		||||
 | 
			
		||||
    [System.Text.Json.Serialization.JsonIgnore]
 | 
			
		||||
    [Newtonsoft.Json.JsonIgnore]
 | 
			
		||||
    [AutoGenerateColumn(Ignore = true)]
 | 
			
		||||
    public string? Tag { get; set; }
 | 
			
		||||
 | 
			
		||||
    public void Init(ChannelRuntime channelRuntime)
 | 
			
		||||
    {
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
@@ -375,7 +378,7 @@ public class VariableRuntime : Variable, IVariable, IDisposable
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public async ValueTask<OperResult> RpcAsync(string value, string? executive = "brower", CancellationToken cancellationToken = default)
 | 
			
		||||
    public async ValueTask<IOperResult> RpcAsync(string value, string? executive = "brower", CancellationToken cancellationToken = default)
 | 
			
		||||
    {
 | 
			
		||||
        var data = await GlobalData.RpcService.InvokeDeviceMethodAsync(executive, new Dictionary<string, Dictionary<string, string>>()
 | 
			
		||||
        {
 | 
			
		||||
 
 | 
			
		||||
@@ -19,5 +19,5 @@ public interface IRpcService
 | 
			
		||||
    /// <param name="deviceDatas">指定键为变量名称,值为附带方法参数或写入值,方法参数会按逗号分割解析</param>
 | 
			
		||||
    /// <param name="cancellationToken"><see cref="CancellationToken"/> 取消令箭</param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    Task<Dictionary<string, Dictionary<string, OperResult>>> InvokeDeviceMethodAsync(string sourceDes, Dictionary<string, Dictionary<string, string>> deviceDatas, CancellationToken cancellationToken = default);
 | 
			
		||||
    Task<Dictionary<string, Dictionary<string, IOperResult>>> InvokeDeviceMethodAsync(string sourceDes, Dictionary<string, Dictionary<string, string>> deviceDatas, CancellationToken cancellationToken = default);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -42,16 +42,15 @@ internal sealed class RpcService : IRpcService
 | 
			
		||||
    private IStringLocalizer Localizer { get; }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    public async Task<Dictionary<string, Dictionary<string, OperResult>>> InvokeDeviceMethodAsync(string sourceDes, Dictionary<string, Dictionary<string, string>> deviceDatas, CancellationToken cancellationToken = default)
 | 
			
		||||
    public async Task<Dictionary<string, Dictionary<string, IOperResult>>> InvokeDeviceMethodAsync(string sourceDes, Dictionary<string, Dictionary<string, string>> deviceDatas, CancellationToken cancellationToken = default)
 | 
			
		||||
    {
 | 
			
		||||
        // 初始化用于存储将要写入的变量和方法的字典
 | 
			
		||||
        Dictionary<CollectBase, Dictionary<VariableRuntime, JToken>> writeVariables = new();
 | 
			
		||||
        Dictionary<CollectBase, Dictionary<VariableRuntime, JToken>> writeMethods = new();
 | 
			
		||||
        Dictionary<IRpcDriver, Dictionary<VariableRuntime, JToken>> writeVariables = new();
 | 
			
		||||
        Dictionary<IRpcDriver, Dictionary<VariableRuntime, JToken>> writeMethods = new();
 | 
			
		||||
        // 用于存储结果的并发字典
 | 
			
		||||
        ConcurrentDictionary<string, Dictionary<string, OperResult>> results = new();
 | 
			
		||||
        ConcurrentDictionary<string, Dictionary<string, IOperResult>> results = new();
 | 
			
		||||
        deviceDatas.ForEach(a => results.TryAdd(a.Key, new()));
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        var deviceDict = GlobalData.Devices;
 | 
			
		||||
 | 
			
		||||
        // 对每个要操作的变量进行检查和处理
 | 
			
		||||
@@ -68,7 +67,8 @@ internal sealed class RpcService : IRpcService
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // 查找变量对应的设备
 | 
			
		||||
            var collect = device.Driver as CollectBase;
 | 
			
		||||
            var collect = device.Driver as IRpcDriver;
 | 
			
		||||
            collect ??= device.RpcDriver;
 | 
			
		||||
            if (collect == null)
 | 
			
		||||
            {
 | 
			
		||||
                // 如果设备不存在,则添加错误信息到结果中并继续下一个设备的处理
 | 
			
		||||
@@ -78,7 +78,7 @@ internal sealed class RpcService : IRpcService
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
            // 检查设备状态,如果设备处于暂停状态,则添加相应的错误信息到结果中并继续下一个变量的处理
 | 
			
		||||
            if (collect.CurrentDevice.DeviceStatus == DeviceStatusEnum.Pause)
 | 
			
		||||
            if (device.DeviceStatus == DeviceStatusEnum.Pause)
 | 
			
		||||
            {
 | 
			
		||||
                deviceData.Value.ForEach(a =>
 | 
			
		||||
                results[deviceData.Key].TryAdd(a.Key, new OperResult(Localizer["DevicePause", deviceData.Key]))
 | 
			
		||||
@@ -136,46 +136,51 @@ internal sealed class RpcService : IRpcService
 | 
			
		||||
                // 写入日志
 | 
			
		||||
                foreach (var resultItem in result)
 | 
			
		||||
                {
 | 
			
		||||
                    var empty = string.IsNullOrEmpty(resultItem.Key);
 | 
			
		||||
                    string operObj = empty ? deviceDatas[driverData.Key.DeviceName].Select(x => x.Key).ToJsonNetString() : resultItem.Key;
 | 
			
		||||
 | 
			
		||||
                    string parJson = empty ? deviceDatas.Select(x => x.Value).ToJsonNetString() : deviceDatas[driverData.Key.DeviceName][resultItem.Key];
 | 
			
		||||
 | 
			
		||||
                    if (!resultItem.Value.IsSuccess || _rpcLogOptions.SuccessLog)
 | 
			
		||||
                        _logQueues.Enqueue(
 | 
			
		||||
                            new RpcLog()
 | 
			
		||||
                            {
 | 
			
		||||
                                LogTime = DateTime.Now,
 | 
			
		||||
                                OperateMessage = resultItem.Value.IsSuccess ? null : resultItem.Value.ToString(),
 | 
			
		||||
                                IsSuccess = resultItem.Value.IsSuccess,
 | 
			
		||||
                                OperateMethod = Localizer["WriteVariable"],
 | 
			
		||||
                                OperateDevice = driverData.Key.DeviceName,
 | 
			
		||||
                                OperateObject = operObj,
 | 
			
		||||
                                OperateSource = sourceDes,
 | 
			
		||||
                                ParamJson = parJson,
 | 
			
		||||
                                ResultJson = null
 | 
			
		||||
                            }
 | 
			
		||||
                        );
 | 
			
		||||
 | 
			
		||||
                    // 不返回详细错误
 | 
			
		||||
                    if (!resultItem.Value.IsSuccess)
 | 
			
		||||
                    foreach (var variableResult in resultItem.Value)
 | 
			
		||||
                    {
 | 
			
		||||
                        OperResult result1 = resultItem.Value;
 | 
			
		||||
                        result1.Exception = null;
 | 
			
		||||
                        result[resultItem.Key] = result1;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // 将结果添加到结果字典中
 | 
			
		||||
                results[driverData.Key.DeviceName].AddRange(result);
 | 
			
		||||
                        string operObj = variableResult.Key;
 | 
			
		||||
 | 
			
		||||
                        string parJson = deviceDatas[resultItem.Key][variableResult.Key];
 | 
			
		||||
 | 
			
		||||
                        if (!variableResult.Value.IsSuccess || _rpcLogOptions.SuccessLog)
 | 
			
		||||
                            _logQueues.Enqueue(
 | 
			
		||||
                                new RpcLog()
 | 
			
		||||
                                {
 | 
			
		||||
                                    LogTime = DateTime.Now,
 | 
			
		||||
                                    OperateMessage = variableResult.Value.IsSuccess ? null : variableResult.Value.ToString(),
 | 
			
		||||
                                    IsSuccess = variableResult.Value.IsSuccess,
 | 
			
		||||
                                    OperateMethod = Localizer["WriteVariable"],
 | 
			
		||||
                                    OperateDevice = resultItem.Key,
 | 
			
		||||
                                    OperateObject = operObj,
 | 
			
		||||
                                    OperateSource = sourceDes,
 | 
			
		||||
                                    ParamJson = parJson,
 | 
			
		||||
                                    ResultJson = null
 | 
			
		||||
                                }
 | 
			
		||||
                            );
 | 
			
		||||
 | 
			
		||||
                        // 不返回详细错误
 | 
			
		||||
                        if (!variableResult.Value.IsSuccess)
 | 
			
		||||
                        {
 | 
			
		||||
                            var result1 = variableResult.Value;
 | 
			
		||||
                            result1.Exception = null;
 | 
			
		||||
                            resultItem.Value[variableResult.Key] = result1;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // 将结果添加到结果字典中
 | 
			
		||||
                    results[resultItem.Key].AddRange(resultItem.Value);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                // 将异常信息添加到结果字典中
 | 
			
		||||
                results[driverData.Key.DeviceName].AddRange(driverData.Value.Select((KeyValuePair<VariableRuntime, JToken> a) =>
 | 
			
		||||
                foreach (var item in driverData.Value)
 | 
			
		||||
                {
 | 
			
		||||
                    return new KeyValuePair<string, OperResult>(a.Key.Name, new OperResult(ex));
 | 
			
		||||
                }));
 | 
			
		||||
                    results[item.Key.DeviceName].Add(item.Key.Name, new OperResult(ex));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
@@ -192,42 +197,51 @@ internal sealed class RpcService : IRpcService
 | 
			
		||||
                // 写入日志
 | 
			
		||||
                foreach (var resultItem in result)
 | 
			
		||||
                {
 | 
			
		||||
                    // 写入日志
 | 
			
		||||
                    if (!resultItem.Value.IsSuccess || _rpcLogOptions.SuccessLog)
 | 
			
		||||
                        _logQueues.Enqueue(
 | 
			
		||||
                            new RpcLog()
 | 
			
		||||
                            {
 | 
			
		||||
                                LogTime = DateTime.Now,
 | 
			
		||||
                                OperateMessage = resultItem.Value.IsSuccess ? null : resultItem.Value.ToString(),
 | 
			
		||||
                                IsSuccess = resultItem.Value.IsSuccess,
 | 
			
		||||
                                OperateMethod = operateMethods[resultItem.Key],
 | 
			
		||||
                                OperateDevice = driverData.Key.DeviceName,
 | 
			
		||||
                                OperateObject = resultItem.Key,
 | 
			
		||||
                                OperateSource = sourceDes,
 | 
			
		||||
                                ParamJson = deviceDatas[driverData.Key.DeviceName][resultItem.Key]?.ToString(),
 | 
			
		||||
                                ResultJson = resultItem.Value.Content?.ToJsonNetString()
 | 
			
		||||
                            }
 | 
			
		||||
                        );
 | 
			
		||||
 | 
			
		||||
                    // 不返回详细错误
 | 
			
		||||
                    if (!resultItem.Value.IsSuccess)
 | 
			
		||||
                    foreach (var variableResult in resultItem.Value)
 | 
			
		||||
                    {
 | 
			
		||||
                        OperResult<object> result1 = resultItem.Value;
 | 
			
		||||
                        result1.Exception = null;
 | 
			
		||||
                        result[resultItem.Key] = result1;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                        string operObj = variableResult.Key;
 | 
			
		||||
 | 
			
		||||
                results[driverData.Key.DeviceName].AddRange(result.ToDictionary(a => a.Key, a => (OperResult)a.Value));
 | 
			
		||||
                        string parJson = deviceDatas[resultItem.Key][variableResult.Key];
 | 
			
		||||
 | 
			
		||||
                        // 写入日志
 | 
			
		||||
                        if (!variableResult.Value.IsSuccess || _rpcLogOptions.SuccessLog)
 | 
			
		||||
                            _logQueues.Enqueue(
 | 
			
		||||
                                new RpcLog()
 | 
			
		||||
                                {
 | 
			
		||||
                                    LogTime = DateTime.Now,
 | 
			
		||||
                                    OperateMessage = variableResult.Value.IsSuccess ? null : variableResult.Value.ToString(),
 | 
			
		||||
                                    IsSuccess = variableResult.Value.IsSuccess,
 | 
			
		||||
                                    OperateMethod = operateMethods[variableResult.Key],
 | 
			
		||||
                                    OperateDevice = resultItem.Key,
 | 
			
		||||
                                    OperateObject = operObj,
 | 
			
		||||
                                    OperateSource = sourceDes,
 | 
			
		||||
                                    ParamJson = parJson?.ToString(),
 | 
			
		||||
                                    ResultJson = variableResult.Value is IOperResult<object> operResult ? operResult.Content?.ToJsonNetString() : string.Empty
 | 
			
		||||
                                }
 | 
			
		||||
                            );
 | 
			
		||||
 | 
			
		||||
                        // 不返回详细错误
 | 
			
		||||
                        if (!variableResult.Value.IsSuccess)
 | 
			
		||||
                        {
 | 
			
		||||
                            var result1 = variableResult.Value;
 | 
			
		||||
                            result1.Exception = null;
 | 
			
		||||
                            resultItem.Value[variableResult.Key] = result1;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                    results[resultItem.Key].AddRange(resultItem.Value.ToDictionary(a => a.Key, a => a.Value));
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                // 将异常信息添加到结果字典中
 | 
			
		||||
                results[driverData.Key.DeviceName].AddRange(driverData.Value.Select((KeyValuePair<VariableRuntime, JToken> a) =>
 | 
			
		||||
                foreach (var item in driverData.Value)
 | 
			
		||||
                {
 | 
			
		||||
                    return new KeyValuePair<string, OperResult>(a.Key.Name, new OperResult(ex));
 | 
			
		||||
                }));
 | 
			
		||||
                    results[item.Key.DeviceName].Add(item.Key.Name, new OperResult(ex));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,7 @@ namespace ThingsGateway.Gateway.Application
 | 
			
		||||
        Task<Dictionary<string, object>> ExportVariableAsync(ExportFilter exportFilter);
 | 
			
		||||
 | 
			
		||||
        Task ImportVariableAsync(Dictionary<string, ImportPreviewOutputBase> input, bool restart, CancellationToken cancellationToken);
 | 
			
		||||
        Task InsertTestDataAsync(int testVariableCount, int testDeviceCount, string slaveUrl, bool restart, CancellationToken cancellationToken);
 | 
			
		||||
        Task InsertTestDataAsync(int testVariableCount, int testDeviceCount, string slaveUrl, bool businessEnable, bool restart, CancellationToken cancellationToken);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        Task<bool> BatchSaveVariableAsync(List<Variable> input, ItemChangedType type, bool restart, CancellationToken cancellationToken);
 | 
			
		||||
 
 | 
			
		||||
@@ -80,7 +80,7 @@ internal interface IVariableService
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 创建n个modbus变量
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    Task<(List<Channel>, List<Device>, List<Variable>)> InsertTestDataAsync(int variableCount, int deviceCount, string slaveUrl = "127.0.0.1:502");
 | 
			
		||||
    Task<(List<Channel>, List<Device>, List<Variable>)> InsertTestDataAsync(int variableCount, int deviceCount, string slaveUrl = "127.0.0.1:502", bool businessEnable = false);
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 表格查询
 | 
			
		||||
 
 | 
			
		||||
@@ -165,7 +165,7 @@ public class VariableRuntimeService : IVariableRuntimeService
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task InsertTestDataAsync(int testVariableCount, int testDeviceCount, string slaveUrl, bool restart, CancellationToken cancellationToken)
 | 
			
		||||
    public async Task InsertTestDataAsync(int testVariableCount, int testDeviceCount, string slaveUrl, bool businessEnable, bool restart, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
@@ -173,7 +173,7 @@ public class VariableRuntimeService : IVariableRuntimeService
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            var datas = await GlobalData.VariableService.InsertTestDataAsync(testVariableCount, testDeviceCount, slaveUrl).ConfigureAwait(false);
 | 
			
		||||
            var datas = await GlobalData.VariableService.InsertTestDataAsync(testVariableCount, testDeviceCount, slaveUrl, businessEnable).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            {
 | 
			
		||||
                var newChannelRuntimes = (datas.Item1).Adapt<List<ChannelRuntime>>();
 | 
			
		||||
 
 | 
			
		||||
@@ -54,7 +54,7 @@ internal sealed class VariableService : BaseService<Variable>, IVariableService
 | 
			
		||||
 | 
			
		||||
    #region 测试
 | 
			
		||||
 | 
			
		||||
    public async Task<(List<Channel>, List<Device>, List<Variable>)> InsertTestDataAsync(int variableCount, int deviceCount, string slaveUrl = "127.0.0.1:502")
 | 
			
		||||
    public async Task<(List<Channel>, List<Device>, List<Variable>)> InsertTestDataAsync(int variableCount, int deviceCount, string slaveUrl = "127.0.0.1:502", bool businessEnable = false)
 | 
			
		||||
    {
 | 
			
		||||
        if (slaveUrl.IsNullOrWhiteSpace()) slaveUrl = "127.0.0.1:502";
 | 
			
		||||
        if (deviceCount > variableCount) variableCount = deviceCount;
 | 
			
		||||
@@ -124,64 +124,69 @@ internal sealed class VariableService : BaseService<Variable>, IVariableService
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        //Channel serviceChannel = new Channel();
 | 
			
		||||
        //Device serviceDevice = new Device();
 | 
			
		||||
 | 
			
		||||
        //{
 | 
			
		||||
        //    var id = CommonUtils.GetSingleId();
 | 
			
		||||
        //    var name = $"modbusSlaveChannel{id}";
 | 
			
		||||
        //    serviceChannel.ChannelType = ChannelTypeEnum.TcpService;
 | 
			
		||||
        //    serviceChannel.Name = name;
 | 
			
		||||
        //    serviceChannel.Enable = true;
 | 
			
		||||
        //    serviceChannel.Id = id;
 | 
			
		||||
        //    serviceChannel.CreateUserId = UserManager.UserId;
 | 
			
		||||
        //    serviceChannel.CreateOrgId = UserManager.OrgId;
 | 
			
		||||
        //    serviceChannel.BindUrl = "127.0.0.1:502";
 | 
			
		||||
        //    serviceChannel.PluginName = "ThingsGateway.Plugin.Modbus.ModbusSlave";
 | 
			
		||||
        //    newChannels.Add(serviceChannel);
 | 
			
		||||
        //}
 | 
			
		||||
        //{
 | 
			
		||||
        //    var id = CommonUtils.GetSingleId();
 | 
			
		||||
        //    var name = $"modbusSlaveDevice{id}";
 | 
			
		||||
        //    serviceDevice.Name = name;
 | 
			
		||||
        //    serviceDevice.Id = id;
 | 
			
		||||
        //    serviceDevice.CreateUserId = UserManager.UserId;
 | 
			
		||||
        //    serviceDevice.CreateOrgId = UserManager.OrgId;
 | 
			
		||||
        //    serviceDevice.ChannelId = serviceChannel.Id;
 | 
			
		||||
        //    serviceDevice.IntervalTime = "1000";
 | 
			
		||||
        //    newDevices.Add(serviceDevice);
 | 
			
		||||
        //}
 | 
			
		||||
 | 
			
		||||
        Channel mqttChannel = new Channel();
 | 
			
		||||
        Device mqttDevice = new Device();
 | 
			
		||||
 | 
			
		||||
        if (businessEnable)
 | 
			
		||||
        {
 | 
			
		||||
            var id = CommonUtils.GetSingleId();
 | 
			
		||||
            var name = $"mqttChannel{id}";
 | 
			
		||||
            mqttChannel.ChannelType = ChannelTypeEnum.Other;
 | 
			
		||||
            mqttChannel.Name = name;
 | 
			
		||||
            mqttChannel.Id = id;
 | 
			
		||||
            mqttChannel.CreateUserId = UserManager.UserId;
 | 
			
		||||
            mqttChannel.CreateOrgId = UserManager.OrgId;
 | 
			
		||||
            mqttChannel.PluginName = "ThingsGateway.Plugin.Mqtt.MqttServer";
 | 
			
		||||
            newChannels.Add(mqttChannel);
 | 
			
		||||
        }
 | 
			
		||||
        {
 | 
			
		||||
            var id = CommonUtils.GetSingleId();
 | 
			
		||||
            var name = $"mqttDevice{id}";
 | 
			
		||||
            mqttDevice.Name = name;
 | 
			
		||||
            mqttDevice.Id = id;
 | 
			
		||||
            mqttDevice.CreateUserId = UserManager.UserId;
 | 
			
		||||
            mqttDevice.CreateOrgId = UserManager.OrgId;
 | 
			
		||||
            mqttDevice.ChannelId = mqttChannel.Id;
 | 
			
		||||
            mqttDevice.IntervalTime = "1000";
 | 
			
		||||
            mqttDevice.DevicePropertys = new Dictionary<string, string>
 | 
			
		||||
 | 
			
		||||
            Channel serviceChannel = new Channel();
 | 
			
		||||
            Device serviceDevice = new Device();
 | 
			
		||||
 | 
			
		||||
            {
 | 
			
		||||
                var id = CommonUtils.GetSingleId();
 | 
			
		||||
                var name = $"modbusSlaveChannel{id}";
 | 
			
		||||
                serviceChannel.ChannelType = ChannelTypeEnum.TcpService;
 | 
			
		||||
                serviceChannel.Name = name;
 | 
			
		||||
                serviceChannel.Enable = true;
 | 
			
		||||
                serviceChannel.Id = id;
 | 
			
		||||
                serviceChannel.CreateUserId = UserManager.UserId;
 | 
			
		||||
                serviceChannel.CreateOrgId = UserManager.OrgId;
 | 
			
		||||
                serviceChannel.BindUrl = "127.0.0.1:502";
 | 
			
		||||
                serviceChannel.PluginName = "ThingsGateway.Plugin.Modbus.ModbusSlave";
 | 
			
		||||
                newChannels.Add(serviceChannel);
 | 
			
		||||
            }
 | 
			
		||||
            {
 | 
			
		||||
                var id = CommonUtils.GetSingleId();
 | 
			
		||||
                var name = $"modbusSlaveDevice{id}";
 | 
			
		||||
                serviceDevice.Name = name;
 | 
			
		||||
                serviceDevice.Id = id;
 | 
			
		||||
                serviceDevice.CreateUserId = UserManager.UserId;
 | 
			
		||||
                serviceDevice.CreateOrgId = UserManager.OrgId;
 | 
			
		||||
                serviceDevice.ChannelId = serviceChannel.Id;
 | 
			
		||||
                serviceDevice.IntervalTime = "1000";
 | 
			
		||||
                newDevices.Add(serviceDevice);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            Channel mqttChannel = new Channel();
 | 
			
		||||
            Device mqttDevice = new Device();
 | 
			
		||||
 | 
			
		||||
            {
 | 
			
		||||
                var id = CommonUtils.GetSingleId();
 | 
			
		||||
                var name = $"mqttChannel{id}";
 | 
			
		||||
                mqttChannel.ChannelType = ChannelTypeEnum.Other;
 | 
			
		||||
                mqttChannel.Name = name;
 | 
			
		||||
                mqttChannel.Id = id;
 | 
			
		||||
                mqttChannel.CreateUserId = UserManager.UserId;
 | 
			
		||||
                mqttChannel.CreateOrgId = UserManager.OrgId;
 | 
			
		||||
                mqttChannel.PluginName = "ThingsGateway.Plugin.Mqtt.MqttServer";
 | 
			
		||||
                newChannels.Add(mqttChannel);
 | 
			
		||||
            }
 | 
			
		||||
            {
 | 
			
		||||
                var id = CommonUtils.GetSingleId();
 | 
			
		||||
                var name = $"mqttDevice{id}";
 | 
			
		||||
                mqttDevice.Name = name;
 | 
			
		||||
                mqttDevice.Id = id;
 | 
			
		||||
                mqttDevice.CreateUserId = UserManager.UserId;
 | 
			
		||||
                mqttDevice.CreateOrgId = UserManager.OrgId;
 | 
			
		||||
                mqttDevice.ChannelId = mqttChannel.Id;
 | 
			
		||||
                mqttDevice.IntervalTime = "1000";
 | 
			
		||||
                mqttDevice.DevicePropertys = new Dictionary<string, string>
 | 
			
		||||
            {
 | 
			
		||||
              {"IsAllVariable", "true"}
 | 
			
		||||
            };
 | 
			
		||||
            newDevices.Add(mqttDevice);
 | 
			
		||||
        }
 | 
			
		||||
                newDevices.Add(mqttDevice);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        //Channel opcuaChannel = new Channel();
 | 
			
		||||
        //Device opcuaDevice = new Device();
 | 
			
		||||
 
 | 
			
		||||
@@ -84,7 +84,7 @@ public class Startup : AppStartup
 | 
			
		||||
        {
 | 
			
		||||
            var connection = DbContext.Db.GetConnection(it.ConfigId);//获取数据库连接对象
 | 
			
		||||
 | 
			
		||||
            if (it.InitTable == true)
 | 
			
		||||
            if (it.InitDatabase == true)
 | 
			
		||||
                connection.DbMaintenance.CreateDatabase();//创建数据库,如果存在则不创建
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -70,7 +70,8 @@
 | 
			
		||||
 | 
			
		||||
    "TestVariableCount": "TestVariableCount",
 | 
			
		||||
    "TestDeviceCount": "TestDeviceCount",
 | 
			
		||||
    "SlaveUrl": "SlaveUrlUrl",
 | 
			
		||||
    "SlaveUrl": "SlaveUrl",
 | 
			
		||||
    "BusinessEnable": "BusinessEnable",
 | 
			
		||||
    "Test": "Addition of test variables"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -29,6 +29,7 @@
 | 
			
		||||
    "TestVariableCount": "变量数量",
 | 
			
		||||
    "TestDeviceCount": "采集设备数量",
 | 
			
		||||
    "SlaveUrl": "服务端Url",
 | 
			
		||||
    "BusinessEnable": "添加业务设备",
 | 
			
		||||
    "Test": "一键添加测试变量"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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 />
 | 
			
		||||
@@ -122,7 +122,7 @@
 | 
			
		||||
                <BodyTemplate>
 | 
			
		||||
                    <BootstrapInput @bind-Value=TestVariableCount ShowLabel="true" ShowLabelTooltip="true" />
 | 
			
		||||
                    <BootstrapInput @bind-Value=TestDeviceCount ShowLabel="true" ShowLabelTooltip="true" />
 | 
			
		||||
                    <BootstrapInput @bind-Value=SlaveUrl ShowLabel="true" ShowLabelTooltip="true" />
 | 
			
		||||
                    <Checkbox @bind-Value=BusinessEnable ShowLabel="true" ShowLabelTooltip="true" />
 | 
			
		||||
                </BodyTemplate>
 | 
			
		||||
 | 
			
		||||
            </PopConfirmButton>
 | 
			
		||||
 
 | 
			
		||||
@@ -133,6 +133,7 @@ public partial class VariableRuntimeInfo : IDisposable
 | 
			
		||||
    private int TestDeviceCount { get; set; }
 | 
			
		||||
 | 
			
		||||
    private string SlaveUrl { get; set; }
 | 
			
		||||
    private bool BusinessEnable { get; set; }
 | 
			
		||||
 | 
			
		||||
    #region 修改
 | 
			
		||||
    private async Task Copy(IEnumerable<Variable> variables)
 | 
			
		||||
@@ -417,7 +418,7 @@ finally
 | 
			
		||||
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                await Task.Run(() => GlobalData.VariableRuntimeService.InsertTestDataAsync(TestVariableCount, TestDeviceCount, SlaveUrl, AutoRestartThread, default));
 | 
			
		||||
                await Task.Run(() => GlobalData.VariableRuntimeService.InsertTestDataAsync(TestVariableCount, TestDeviceCount, SlaveUrl, BusinessEnable, AutoRestartThread, default));
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
 
 | 
			
		||||
@@ -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,9 @@ using Mapster;
 | 
			
		||||
using Microsoft.Extensions.Hosting;
 | 
			
		||||
using Microsoft.Extensions.Logging;
 | 
			
		||||
 | 
			
		||||
using Newtonsoft.Json.Linq;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.Extension.Generic;
 | 
			
		||||
using ThingsGateway.Gateway.Application;
 | 
			
		||||
using ThingsGateway.NewLife;
 | 
			
		||||
 | 
			
		||||
@@ -24,7 +27,7 @@ using TouchSocket.Sockets;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Management;
 | 
			
		||||
 | 
			
		||||
internal sealed class RedundancyHostedService : BackgroundService, IRedundancyHostedService
 | 
			
		||||
internal sealed class RedundancyHostedService : BackgroundService, IRedundancyHostedService, IRpcDriver
 | 
			
		||||
{
 | 
			
		||||
    private readonly ILogger _logger;
 | 
			
		||||
    private readonly IRedundancyService _redundancyService;
 | 
			
		||||
@@ -50,16 +53,17 @@ internal sealed class RedundancyHostedService : BackgroundService, IRedundancyHo
 | 
			
		||||
    private IStringLocalizer Localizer { get; }
 | 
			
		||||
    private DoTask RedundancyTask { get; set; }
 | 
			
		||||
    private WaitLock RedundancyRestartLock { get; } = new();
 | 
			
		||||
    private LoggerGroup _log { get; set; }
 | 
			
		||||
    public ILog LogMessage { get; set; }
 | 
			
		||||
    public TextFileLogger TextLogger { get; }
 | 
			
		||||
    public string LogPath { get; }
 | 
			
		||||
    private TcpDmtpClient TcpDmtpClient;
 | 
			
		||||
    private TcpDmtpService TcpDmtpService;
 | 
			
		||||
    private async Task<TcpDmtpClient> GetTcpDmtpClient(RedundancyOptions redundancy)
 | 
			
		||||
    {
 | 
			
		||||
        _log = new LoggerGroup() { LogLevel = TouchSocket.Core.LogLevel.Trace };
 | 
			
		||||
        _log?.AddLogger(new EasyLogger(Log_Out) { LogLevel = TouchSocket.Core.LogLevel.Trace });
 | 
			
		||||
        _log?.AddLogger(TextLogger);
 | 
			
		||||
        var log = new LoggerGroup() { LogLevel = TouchSocket.Core.LogLevel.Trace };
 | 
			
		||||
        log?.AddLogger(new EasyLogger(Log_Out) { LogLevel = TouchSocket.Core.LogLevel.Trace });
 | 
			
		||||
        log?.AddLogger(TextLogger);
 | 
			
		||||
        LogMessage = log;
 | 
			
		||||
        var tcpDmtpClient = new TcpDmtpClient();
 | 
			
		||||
        var config = new TouchSocketConfig()
 | 
			
		||||
               .SetRemoteIPHost(redundancy.MasterUri)
 | 
			
		||||
@@ -67,10 +71,10 @@ internal sealed class RedundancyHostedService : BackgroundService, IRedundancyHo
 | 
			
		||||
               .SetDmtpOption(new DmtpOption() { VerifyToken = redundancy.VerifyToken })
 | 
			
		||||
               .ConfigureContainer(a =>
 | 
			
		||||
               {
 | 
			
		||||
                   a.AddLogger(_log);
 | 
			
		||||
                   a.AddLogger(LogMessage);
 | 
			
		||||
                   a.AddRpcStore(store =>
 | 
			
		||||
                   {
 | 
			
		||||
                       store.RegisterServer(new ReverseCallbackServer());
 | 
			
		||||
                       store.RegisterServer(new ReverseCallbackServer(this));
 | 
			
		||||
                   });
 | 
			
		||||
               })
 | 
			
		||||
               .ConfigurePlugins(a =>
 | 
			
		||||
@@ -87,9 +91,10 @@ internal sealed class RedundancyHostedService : BackgroundService, IRedundancyHo
 | 
			
		||||
 | 
			
		||||
    private async Task<TcpDmtpService> GetTcpDmtpService(RedundancyOptions redundancy)
 | 
			
		||||
    {
 | 
			
		||||
        _log = new LoggerGroup() { LogLevel = TouchSocket.Core.LogLevel.Trace };
 | 
			
		||||
        _log?.AddLogger(new EasyLogger(Log_Out) { LogLevel = TouchSocket.Core.LogLevel.Trace });
 | 
			
		||||
        _log?.AddLogger(TextLogger);
 | 
			
		||||
        var log = new LoggerGroup() { LogLevel = TouchSocket.Core.LogLevel.Trace };
 | 
			
		||||
        log?.AddLogger(new EasyLogger(Log_Out) { LogLevel = TouchSocket.Core.LogLevel.Trace });
 | 
			
		||||
        log?.AddLogger(TextLogger);
 | 
			
		||||
        LogMessage = log;
 | 
			
		||||
        var tcpDmtpService = new TcpDmtpService();
 | 
			
		||||
        var config = new TouchSocketConfig()
 | 
			
		||||
               .SetListenIPHosts(redundancy.MasterUri)
 | 
			
		||||
@@ -97,10 +102,10 @@ internal sealed class RedundancyHostedService : BackgroundService, IRedundancyHo
 | 
			
		||||
               .SetDmtpOption(new DmtpOption() { VerifyToken = redundancy.VerifyToken })
 | 
			
		||||
               .ConfigureContainer(a =>
 | 
			
		||||
               {
 | 
			
		||||
                   a.AddLogger(_log);
 | 
			
		||||
                   a.AddLogger(LogMessage);
 | 
			
		||||
                   a.AddRpcStore(store =>
 | 
			
		||||
                   {
 | 
			
		||||
                       store.RegisterServer(new ReverseCallbackServer());
 | 
			
		||||
                       store.RegisterServer(new ReverseCallbackServer(this));
 | 
			
		||||
                   });
 | 
			
		||||
               })
 | 
			
		||||
               .ConfigurePlugins(a =>
 | 
			
		||||
@@ -161,8 +166,8 @@ internal sealed class RedundancyHostedService : BackgroundService, IRedundancyHo
 | 
			
		||||
                    {
 | 
			
		||||
                        // 将 GlobalData.CollectDevices 和 GlobalData.Variables 同步到从站
 | 
			
		||||
                        await item.GetDmtpRpcActor().InvokeAsync(
 | 
			
		||||
                                         nameof(ReverseCallbackServer.UpdateGatewayData), null, waitInvoke, deviceRunTimes).ConfigureAwait(false);
 | 
			
		||||
                        _log?.LogTrace($"{item.GetIPPort()} Update StandbyStation data success");
 | 
			
		||||
                                         nameof(ReverseCallbackServer.UpData), null, waitInvoke, deviceRunTimes).ConfigureAwait(false);
 | 
			
		||||
                        LogMessage?.LogTrace($"{item.GetIPPort()} Update StandbyStation data success");
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                }
 | 
			
		||||
@@ -170,7 +175,7 @@ internal sealed class RedundancyHostedService : BackgroundService, IRedundancyHo
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                // 输出警告日志,指示同步数据到从站时发生错误
 | 
			
		||||
                _log?.LogWarning(ex, Localizer["ErrorSynchronizingData"]);
 | 
			
		||||
                LogMessage?.LogWarning(ex, Localizer["ErrorSynchronizingData"]);
 | 
			
		||||
            }
 | 
			
		||||
            await Task.Delay(syncInterval, stoppingToken).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
@@ -182,7 +187,7 @@ internal sealed class RedundancyHostedService : BackgroundService, IRedundancyHo
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            _log?.LogWarning(ex, "Execute");
 | 
			
		||||
            LogMessage?.LogWarning(ex, "Execute");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -248,7 +253,7 @@ internal sealed class RedundancyHostedService : BackgroundService, IRedundancyHo
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    // 如果设备在线
 | 
			
		||||
                    _log?.LogTrace($"Ping ActiveStation {redundancy.MasterUri} success");
 | 
			
		||||
                    LogMessage?.LogTrace($"Ping ActiveStation {redundancy.MasterUri} success");
 | 
			
		||||
                    await StandbyAsync().ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
@@ -264,10 +269,61 @@ internal sealed class RedundancyHostedService : BackgroundService, IRedundancyHo
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            _log?.LogWarning(ex, "Execute");
 | 
			
		||||
            LogMessage?.LogWarning(ex, "Execute");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async ValueTask ForcedSync(CancellationToken cancellationToken = default)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            bool online = false;
 | 
			
		||||
            var waitInvoke = new DmtpInvokeOption()
 | 
			
		||||
            {
 | 
			
		||||
                FeedbackType = FeedbackType.WaitInvoke,
 | 
			
		||||
                Token = cancellationToken,
 | 
			
		||||
                Timeout = 30000,
 | 
			
		||||
                SerializationType = SerializationType.Json,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                online = (await TcpDmtpClient.TryConnectAsync().ConfigureAwait(false)).ResultCode == ResultCode.Success;
 | 
			
		||||
 | 
			
		||||
                // 如果 online 为 true,表示设备在线
 | 
			
		||||
                if (online)
 | 
			
		||||
                {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                    // 将 GlobalData.CollectDevices 和 GlobalData.Variables 同步到从站
 | 
			
		||||
                    var data = await TcpDmtpClient.GetDmtpRpcActor().InvokeTAsync<List<DataWithDatabase>>(
 | 
			
		||||
                                       nameof(ReverseCallbackServer.GetData), waitInvoke).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                    await GlobalData.ChannelRuntimeService.CopyAsync(data.Select(a => a.Channel).ToList(), data.SelectMany(a => a.DeviceVariables).ToDictionary(a => a.Device, a => a.Variables), true, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                    LogMessage?.LogTrace($"ForcedSync data success");
 | 
			
		||||
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                // 输出警告日志,指示同步数据到从站时发生错误
 | 
			
		||||
                LogMessage?.LogWarning(ex, "ForcedSync data error");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        catch (OperationCanceledException)
 | 
			
		||||
        {
 | 
			
		||||
        }
 | 
			
		||||
        catch (ObjectDisposedException)
 | 
			
		||||
        {
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            LogMessage?.LogWarning(ex, "Execute");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    private WaitLock _switchLock = new();
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
@@ -291,12 +347,12 @@ internal sealed class RedundancyHostedService : BackgroundService, IRedundancyHo
 | 
			
		||||
            {
 | 
			
		||||
                if (RedundancyOptions.IsMaster)
 | 
			
		||||
                {
 | 
			
		||||
                    RedundancyTask = new DoTask(a => DoMasterWork(TcpDmtpService, RedundancyOptions.SyncInterval, a), _log); // 创建新的任务
 | 
			
		||||
                    RedundancyTask = new DoTask(a => DoMasterWork(TcpDmtpService, RedundancyOptions.SyncInterval, a), LogMessage); // 创建新的任务
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
 | 
			
		||||
                    RedundancyTask = new DoTask(a => DoSlaveWork(TcpDmtpClient, RedundancyOptions, a), _log); // 创建新的任务
 | 
			
		||||
                    RedundancyTask = new DoTask(a => DoSlaveWork(TcpDmtpClient, RedundancyOptions, a), LogMessage); // 创建新的任务
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                RedundancyTask?.Start(default); // 启动任务
 | 
			
		||||
@@ -306,7 +362,7 @@ internal sealed class RedundancyHostedService : BackgroundService, IRedundancyHo
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            _log?.LogError(ex, "Start"); // 记录错误日志
 | 
			
		||||
            LogMessage?.LogError(ex, "Start"); // 记录错误日志
 | 
			
		||||
            return new(ex);
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
@@ -326,6 +382,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);
 | 
			
		||||
            }
 | 
			
		||||
@@ -350,7 +407,7 @@ internal sealed class RedundancyHostedService : BackgroundService, IRedundancyHo
 | 
			
		||||
            {
 | 
			
		||||
                // 输出日志,指示主站已恢复,从站将切换到备用状态
 | 
			
		||||
                if (first)
 | 
			
		||||
                    _log?.Warning(Localizer["SwitchSlaveState"]);
 | 
			
		||||
                    LogMessage?.Warning(Localizer["SwitchSlaveState"]);
 | 
			
		||||
 | 
			
		||||
                // 将 IsStart 设置为 false,表示当前设备为从站,切换到备用状态
 | 
			
		||||
                _gatewayRedundantSerivce.StartCollectChannelEnable = false;
 | 
			
		||||
@@ -376,7 +433,7 @@ internal sealed class RedundancyHostedService : BackgroundService, IRedundancyHo
 | 
			
		||||
            {
 | 
			
		||||
                // 输出日志,指示无法连接冗余站点,本机将切换到正常状态
 | 
			
		||||
                if (first)
 | 
			
		||||
                    _log?.Warning(Localizer["SwitchMasterState"]);
 | 
			
		||||
                    LogMessage?.Warning(Localizer["SwitchMasterState"]);
 | 
			
		||||
                _gatewayRedundantSerivce.StartCollectChannelEnable = true;
 | 
			
		||||
                await RestartAsync().ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
@@ -426,7 +483,7 @@ internal sealed class RedundancyHostedService : BackgroundService, IRedundancyHo
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            _log?.LogError(ex, "Stop"); // 记录错误日志
 | 
			
		||||
            LogMessage?.LogError(ex, "Stop"); // 记录错误日志
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
@@ -435,4 +492,164 @@ internal sealed class RedundancyHostedService : BackgroundService, IRedundancyHo
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 异步写入方法
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="writeInfoLists">要写入的变量及其对应的数据</param>
 | 
			
		||||
    /// <param name="cancellationToken">取消操作的通知</param>
 | 
			
		||||
    /// <returns>写入操作的结果字典</returns>
 | 
			
		||||
    public async ValueTask<Dictionary<string, Dictionary<string, IOperResult>>> InvokeMethodAsync(Dictionary<VariableRuntime, JToken> writeInfoLists, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        return (await Rpc(writeInfoLists, cancellationToken).ConfigureAwait(false)).ToDictionary(a => a.Key, a => a.Value.ToDictionary(b => b.Key, b => (IOperResult)b.Value));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async ValueTask<Dictionary<string, Dictionary<string, OperResult<object>>>> Rpc(Dictionary<VariableRuntime, JToken> writeInfoLists, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        Dictionary<string, Dictionary<string, OperResult<object>>> dataResult = new();
 | 
			
		||||
 | 
			
		||||
        Dictionary<string, Dictionary<string, string>> deviceDatas = new();
 | 
			
		||||
        foreach (var item in writeInfoLists)
 | 
			
		||||
        {
 | 
			
		||||
            if (deviceDatas.TryGetValue(item.Key.DeviceName ?? string.Empty, out var variableDatas))
 | 
			
		||||
            {
 | 
			
		||||
                variableDatas.Add(item.Key.Name, item.Value?.ToString() ?? string.Empty);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                deviceDatas.Add(item.Key.DeviceName ?? string.Empty, new());
 | 
			
		||||
                deviceDatas[item.Key.DeviceName ?? string.Empty].Add(item.Key.Name, item.Value?.ToString() ?? string.Empty);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (RedundancyOptions.IsMaster)
 | 
			
		||||
        {
 | 
			
		||||
            return NoOnline(dataResult, deviceDatas);
 | 
			
		||||
        }
 | 
			
		||||
        bool online = false;
 | 
			
		||||
        var waitInvoke = new DmtpInvokeOption()
 | 
			
		||||
        {
 | 
			
		||||
            FeedbackType = FeedbackType.WaitInvoke,
 | 
			
		||||
            Token = cancellationToken,
 | 
			
		||||
            Timeout = 30000,
 | 
			
		||||
            SerializationType = SerializationType.Json,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            if (!RedundancyOptions.IsMaster)
 | 
			
		||||
            {
 | 
			
		||||
                online = (await TcpDmtpClient.TryConnectAsync().ConfigureAwait(false)).ResultCode == ResultCode.Success;
 | 
			
		||||
 | 
			
		||||
                // 如果 online 为 true,表示设备在线
 | 
			
		||||
                if (online)
 | 
			
		||||
                {
 | 
			
		||||
                    // 将 GlobalData.CollectDevices 和 GlobalData.Variables 同步到从站
 | 
			
		||||
                    dataResult = await TcpDmtpClient.GetDmtpRpcActor().InvokeTAsync<Dictionary<string, Dictionary<string, OperResult<object>>>>(
 | 
			
		||||
                                       nameof(ReverseCallbackServer.Rpc), waitInvoke, deviceDatas).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                    LogMessage?.LogTrace($"Rpc success");
 | 
			
		||||
 | 
			
		||||
                    return dataResult;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                if (TcpDmtpService.Clients.Count != 0)
 | 
			
		||||
                {
 | 
			
		||||
                    online = true;
 | 
			
		||||
                }
 | 
			
		||||
                // 如果 online 为 true,表示设备在线
 | 
			
		||||
                if (online)
 | 
			
		||||
                {
 | 
			
		||||
                    foreach (var item in deviceDatas)
 | 
			
		||||
                    {
 | 
			
		||||
 | 
			
		||||
                        if (GlobalData.ReadOnlyDevices.TryGetValue(item.Key, out var device))
 | 
			
		||||
                        {
 | 
			
		||||
                            var key = device.Tag;
 | 
			
		||||
 | 
			
		||||
                            if (TcpDmtpService.TryGetClient(key, out var client))
 | 
			
		||||
                            {
 | 
			
		||||
                                try
 | 
			
		||||
                                {
 | 
			
		||||
 | 
			
		||||
                                    var data = await TcpDmtpClient.GetDmtpRpcActor().InvokeTAsync<Dictionary<string, Dictionary<string, OperResult<object>>>>(
 | 
			
		||||
                                                         nameof(ReverseCallbackServer.Rpc), waitInvoke, new Dictionary<string, Dictionary<string, string>>() { { item.Key, item.Value } }).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                                    dataResult.AddRange(data);
 | 
			
		||||
 | 
			
		||||
                                    continue;
 | 
			
		||||
                                }
 | 
			
		||||
                                catch (Exception ex)
 | 
			
		||||
                                {
 | 
			
		||||
                                    dataResult.TryAdd(item.Key, new Dictionary<string, OperResult<object>>());
 | 
			
		||||
 | 
			
		||||
                                    foreach (var vItem in item.Value)
 | 
			
		||||
                                    {
 | 
			
		||||
                                        dataResult[item.Key].Add(vItem.Key, new OperResult<object>(ex));
 | 
			
		||||
                                    }
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        dataResult.TryAdd(item.Key, new Dictionary<string, OperResult<object>>());
 | 
			
		||||
 | 
			
		||||
                        foreach (var vItem in item.Value)
 | 
			
		||||
                        {
 | 
			
		||||
                            dataResult[item.Key].Add(vItem.Key, new OperResult<object>("No online"));
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    LogMessage?.LogTrace($"Rpc success");
 | 
			
		||||
                    return dataResult;
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    LogMessage?.LogWarning("Rpc error, no client online");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return NoOnline(dataResult, deviceDatas);
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
        catch (OperationCanceledException)
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
            return NoOnline(dataResult, deviceDatas);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            // 输出警告日志,指示同步数据到从站时发生错误
 | 
			
		||||
            LogMessage?.LogWarning(ex, "Rpc error");
 | 
			
		||||
            return NoOnline(dataResult, deviceDatas);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static Dictionary<string, Dictionary<string, OperResult<object>>> NoOnline(Dictionary<string, Dictionary<string, OperResult<object>>> dataResult, Dictionary<string, Dictionary<string, string>> deviceDatas)
 | 
			
		||||
    {
 | 
			
		||||
        foreach (var item in deviceDatas)
 | 
			
		||||
        {
 | 
			
		||||
            dataResult.TryAdd(item.Key, new Dictionary<string, OperResult<object>>());
 | 
			
		||||
 | 
			
		||||
            foreach (var vItem in item.Value)
 | 
			
		||||
            {
 | 
			
		||||
                dataResult[item.Key].TryAdd(vItem.Key, new OperResult<object>("No online"));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return dataResult;
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 异步写入方法
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="writeInfoLists">要写入的变量及其对应的数据</param>
 | 
			
		||||
    /// <param name="cancellationToken">取消操作的通知</param>
 | 
			
		||||
    /// <returns>写入操作的结果字典</returns>
 | 
			
		||||
    public async ValueTask<Dictionary<string, Dictionary<string, IOperResult>>> InVokeWriteAsync(Dictionary<VariableRuntime, JToken> writeInfoLists, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        return (await Rpc(writeInfoLists, cancellationToken).ConfigureAwait(false)).ToDictionary(a => a.Key, a => a.Value.ToDictionary(b => b.Key, b => (IOperResult)b.Value));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -10,21 +10,33 @@
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.Gateway.Application;
 | 
			
		||||
 | 
			
		||||
using TouchSocket.Core;
 | 
			
		||||
using TouchSocket.Dmtp.Rpc;
 | 
			
		||||
using TouchSocket.Rpc;
 | 
			
		||||
using TouchSocket.Sockets;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Management;
 | 
			
		||||
 | 
			
		||||
public partial class ReverseCallbackServer : SingletonRpcServer
 | 
			
		||||
internal sealed partial class ReverseCallbackServer : SingletonRpcServer
 | 
			
		||||
{
 | 
			
		||||
    RedundancyHostedService RedundancyHostedService;
 | 
			
		||||
    public ReverseCallbackServer(RedundancyHostedService redundancyHostedService)
 | 
			
		||||
    {
 | 
			
		||||
        RedundancyHostedService = redundancyHostedService;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [DmtpRpc(MethodInvoke = true)]
 | 
			
		||||
    public void UpdateGatewayData(List<DeviceDataWithValue> deviceDatas)
 | 
			
		||||
    public void UpData(ICallContext callContext, List<DeviceDataWithValue> deviceDatas)
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
        foreach (var deviceData in deviceDatas)
 | 
			
		||||
        {
 | 
			
		||||
            if (GlobalData.ReadOnlyDevices.TryGetValue(deviceData.Name, out var device))
 | 
			
		||||
            {
 | 
			
		||||
                device.RpcDriver = RedundancyHostedService;
 | 
			
		||||
                device.Tag = callContext.Caller is IIdClient idClient ? idClient.Id : string.Empty;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                device.SetDeviceStatus(deviceData.ActiveTime, deviceData.DeviceStatus == DeviceStatusEnum.OnLine ? false : true, lastErrorMessage: deviceData.LastErrorMessage);
 | 
			
		||||
 | 
			
		||||
                foreach (var variableData in deviceData.ReadOnlyVariableRuntimes)
 | 
			
		||||
@@ -38,5 +50,41 @@ public partial class ReverseCallbackServer : SingletonRpcServer
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        RedundancyHostedService.LogMessage?.Trace("Update data success");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    [DmtpRpc(MethodInvoke = true)]
 | 
			
		||||
    public List<DataWithDatabase> GetData()
 | 
			
		||||
    {
 | 
			
		||||
        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;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [DmtpRpc(MethodInvoke = true)]
 | 
			
		||||
    public Task<Dictionary<string, Dictionary<string, IOperResult>>> Rpc(Dictionary<string, Dictionary<string, string>> deviceDatas, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        return GlobalData.RpcService.InvokeDeviceMethodAsync("Management", deviceDatas, cancellationToken);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -45,7 +45,7 @@ public class Startup : AppStartup
 | 
			
		||||
        DbContext.DbConfigs?.ForEach(it =>
 | 
			
		||||
        {
 | 
			
		||||
            var connection = DbContext.Db.GetConnection(it.ConfigId);//获取数据库连接对象
 | 
			
		||||
            if (it.InitTable == true)
 | 
			
		||||
            if (it.InitDatabase == true)
 | 
			
		||||
                connection.DbMaintenance.CreateDatabase();//创建数据库,如果存在则不创建
 | 
			
		||||
        });
 | 
			
		||||
        var fullName = Assembly.GetExecutingAssembly().FullName;//获取程序集全名
 | 
			
		||||
 
 | 
			
		||||
@@ -14,54 +14,60 @@ public class ExecuteScriptNode : TextNode, IActuatorNode, IExexcuteExpressionsBa
 | 
			
		||||
        Title = "ExecuteScriptNode"; Placeholder = "ExecuteScriptNode.Placeholder";
 | 
			
		||||
        Text =
 | 
			
		||||
            """
 | 
			
		||||
            using ThingsGateway.RulesEngine;
 | 
			
		||||
            using ThingsGateway.Foundation;
 | 
			
		||||
            using TouchSocket.Core;
 | 
			
		||||
            
 | 
			
		||||
            using System.Text;
 | 
			
		||||
            using ThingsGateway.Gateway.Application;
 | 
			
		||||
            using ThingsGateway.RulesEngine;
 | 
			
		||||
 | 
			
		||||
            public class TestEx : IExexcuteExpressions
 | 
			
		||||
 | 
			
		||||
            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;
 | 
			
		||||
                        if (mqttClient == null)
 | 
			
		||||
                            throw new("mqttClient NOT FOUND");
 | 
			
		||||
                        var result = await mqttClient.MqttUpAsync("test", Encoding.UTF8.GetBytes("test"),1, default);// 主题 和 负载
 | 
			
		||||
                        if (!result.IsSuccess)
 | 
			
		||||
                            throw new(result.ErrorMessage);
 | 
			
		||||
                        return new NodeOutput() { Value = result };
 | 
			
		||||
                    }
 | 
			
		||||
                    throw new("mqttClient NOT FOUND");
 | 
			
		||||
                    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");
 | 
			
		||||
 | 
			
		||||
                    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 };
 | 
			
		||||
 | 
			
		||||
                    //通过设备名称找出mqttClient插件
 | 
			
		||||
                    //var driver = GlobalData.ReadOnlyDevices.FirstOrDefault(a => a.Value.Name == "mqttDevice1").Value?.Driver;
 | 
			
		||||
                    //if (driver != 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");
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                    //TopicArray topicArray = new()
 | 
			
		||||
                    //{
 | 
			
		||||
                    //    //找到对应的MqttClient插件设备
 | 
			
		||||
                    //    var mqttClient = (ThingsGateway.Plugin.Mqtt.MqttClient)driver;
 | 
			
		||||
                    //    if (mqttClient == null)
 | 
			
		||||
                    //        throw new("mqttClient NOT FOUND");
 | 
			
		||||
                    //    var result = await mqttClient.MqttUpAsync("test", "test", default);// 主题 和 负载
 | 
			
		||||
                    //    if (!result.IsSuccess)
 | 
			
		||||
                    //        throw new(result.ErrorMessage);
 | 
			
		||||
                    //    return new NodeOutput() { Value = result };
 | 
			
		||||
                    //}
 | 
			
		||||
                    //throw new("mqttClient NOT FOUND");
 | 
			
		||||
                    //    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 };
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            
 | 
			
		||||
 | 
			
		||||
            """;
 | 
			
		||||
 
 | 
			
		||||
@@ -42,7 +42,7 @@ public class Startup : AppStartup
 | 
			
		||||
        DbContext.DbConfigs?.ForEach(it =>
 | 
			
		||||
        {
 | 
			
		||||
            var connection = DbContext.Db.GetConnection(it.ConfigId);//获取数据库连接对象
 | 
			
		||||
            if (it.InitTable == true)
 | 
			
		||||
            if (it.InitDatabase == true)
 | 
			
		||||
                connection.DbMaintenance.CreateDatabase();//创建数据库,如果存在则不创建
 | 
			
		||||
        });
 | 
			
		||||
        var fullName = Assembly.GetExecutingAssembly().FullName;//获取程序集全名
 | 
			
		||||
 
 | 
			
		||||
@@ -1,51 +0,0 @@
 | 
			
		||||
 | 
			
		||||
//using ThingsGateway.Gateway.Application;
 | 
			
		||||
//using ThingsGateway.RulesEngine;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
//public class TestEx : 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;
 | 
			
		||||
//        //    if (mqttClient == null)
 | 
			
		||||
//        //        throw new("mqttClient NOT FOUND");
 | 
			
		||||
//        //    var result = await mqttClient.MqttUpAsync("test", "test", default);// 主题 和 负载
 | 
			
		||||
//        //    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)
 | 
			
		||||
//        //        throw new("mqttClient NOT FOUND");
 | 
			
		||||
//        //    var result = await mqttClient.MqttUpAsync("test", "test", default);// 主题 和 负载
 | 
			
		||||
//        //    if (!result.IsSuccess)
 | 
			
		||||
//        //        throw new(result.ErrorMessage);
 | 
			
		||||
//        //    return new NodeOutput() { Value = result };
 | 
			
		||||
//        //}
 | 
			
		||||
//        //throw new("mqttClient NOT FOUND");
 | 
			
		||||
//    }
 | 
			
		||||
//}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -18,7 +18,7 @@ namespace ThingsGateway.Foundation.Modbus;
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// ChannelEventHandler
 | 
			
		||||
/// </summary>
 | 
			
		||||
public delegate ValueTask<OperResult> ModbusServerWriteEventHandler(ModbusAddress modbusAddress, IThingsGatewayBitConverter bitConverter, IClientChannel channel);
 | 
			
		||||
public delegate ValueTask<IOperResult> ModbusServerWriteEventHandler(ModbusAddress modbusAddress, IThingsGatewayBitConverter bitConverter, IClientChannel channel);
 | 
			
		||||
 | 
			
		||||
/// <inheritdoc/>
 | 
			
		||||
public class ModbusSlave : DeviceBase, IModbusAddress
 | 
			
		||||
 
 | 
			
		||||
@@ -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 =>
 | 
			
		||||
@@ -201,7 +201,7 @@ public class ModbusSlave : BusinessBase
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// RPC写入
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    private async ValueTask<OperResult> OnWriteData(ModbusRequest modbusRequest, IThingsGatewayBitConverter bitConverter, IChannel channel)
 | 
			
		||||
    private async ValueTask<IOperResult> OnWriteData(ModbusRequest modbusRequest, IThingsGatewayBitConverter bitConverter, IChannel channel)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
 
 | 
			
		||||
@@ -50,8 +50,9 @@
 | 
			
		||||
    "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>>()));
 | 
			
		||||
@@ -282,9 +282,9 @@ public partial class MqttClient : BusinessBaseWithCacheIntervalScript<VariableBa
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async ValueTask<Dictionary<string, Dictionary<string, OperResult>>> GetResult(MqttApplicationMessageReceivedEventArgs args, Dictionary<string, Dictionary<string, JToken>> rpcDatas)
 | 
			
		||||
    private async ValueTask<Dictionary<string, Dictionary<string, IOperResult>>> GetResult(MqttApplicationMessageReceivedEventArgs args, Dictionary<string, Dictionary<string, JToken>> rpcDatas)
 | 
			
		||||
    {
 | 
			
		||||
        var mqttRpcResult = new Dictionary<string, Dictionary<string, OperResult>>();
 | 
			
		||||
        var mqttRpcResult = new Dictionary<string, Dictionary<string, IOperResult>>();
 | 
			
		||||
        rpcDatas.ForEach(a => mqttRpcResult.Add(a.Key, new()));
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
@@ -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))
 | 
			
		||||
            {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -184,9 +184,9 @@ public partial class MqttServer : BusinessBaseWithCacheIntervalScript<VariableBa
 | 
			
		||||
 | 
			
		||||
    #endregion private
 | 
			
		||||
 | 
			
		||||
    private async ValueTask<Dictionary<string, Dictionary<string, OperResult>>> GetResult(InterceptingPublishEventArgs args, Dictionary<string, Dictionary<string, JToken>> rpcDatas)
 | 
			
		||||
    private async ValueTask<Dictionary<string, Dictionary<string, IOperResult>>> GetResult(InterceptingPublishEventArgs args, Dictionary<string, Dictionary<string, JToken>> rpcDatas)
 | 
			
		||||
    {
 | 
			
		||||
        var mqttRpcResult = new Dictionary<string, Dictionary<string, OperResult>>();
 | 
			
		||||
        var mqttRpcResult = new Dictionary<string, Dictionary<string, IOperResult>>();
 | 
			
		||||
        rpcDatas.ForEach(a => mqttRpcResult.Add(a.Key, new()));
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
@@ -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>
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user