mirror of
				https://gitee.com/ThingsGateway/ThingsGateway.git
				synced 2025-11-04 17:43:58 +08:00 
			
		
		
		
	Compare commits
	
		
			25 Commits
		
	
	
		
			10.10.10.0
			...
			10.11.10.0
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					20a2e3ff8e | ||
| 
						 | 
					61a973b1b5 | ||
| 
						 | 
					cbd72e2081 | ||
| 
						 | 
					4e0377b20c | ||
| 
						 | 
					fd318d3cdc | ||
| 
						 | 
					515bdb9700 | ||
| 
						 | 
					46c1780017 | ||
| 
						 | 
					fe78a4c3ca | ||
| 
						 | 
					2d7effadf9 | ||
| 
						 | 
					346c560f8b | ||
| 
						 | 
					8e3bd89f61 | ||
| 
						 | 
					6da142d080 | ||
| 
						 | 
					ff7d029e6f | ||
| 
						 | 
					21b4695683 | ||
| 
						 | 
					02ad494a26 | ||
| 
						 | 
					280366e1b2 | ||
| 
						 | 
					6660ce3e34 | ||
| 
						 | 
					7499162c1a | ||
| 
						 | 
					40208a5cd6 | ||
| 
						 | 
					fa347f4f68 | ||
| 
						 | 
					d7df6fc605 | ||
| 
						 | 
					eb4bb2fd48 | ||
| 
						 | 
					faa9858974 | ||
| 
						 | 
					1b3d2dda49 | ||
| 
						 | 
					a8a9453611 | 
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -364,6 +364,7 @@ FodyWeavers.xsd
 | 
			
		||||
 | 
			
		||||
/src/*Pro*/
 | 
			
		||||
/src/*Pro*
 | 
			
		||||
/src/**/*Pro*
 | 
			
		||||
/src/*pro*
 | 
			
		||||
/src/*pro*/
 | 
			
		||||
/src/ThingsGateway.Server/Configuration/GiteeOAuthSettings.json
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@
 | 
			
		||||
		<IncludeBuildOutput>false</IncludeBuildOutput>
 | 
			
		||||
		<!-- 避免 DLL 被打包到 lib/ -->
 | 
			
		||||
		<EnableSourceGenerator>true</EnableSourceGenerator>
 | 
			
		||||
		
 | 
			
		||||
		<!-- 可选 -->
 | 
			
		||||
 | 
			
		||||
		
 | 
			
		||||
 
 | 
			
		||||
@@ -37,9 +37,8 @@ public class FileController : ControllerBase
 | 
			
		||||
        var root = Directory.GetCurrentDirectory();
 | 
			
		||||
        var wwwroot = Path.Combine(root, "wwwroot");
 | 
			
		||||
        var filePath = Path.Combine(wwwroot, fileName);
 | 
			
		||||
        // 防止路径穿越攻击
 | 
			
		||||
#pragma warning disable CA3003
 | 
			
		||||
        if (!filePath.StartsWith(wwwroot, StringComparison.OrdinalIgnoreCase) || !System.IO.File.Exists(filePath))
 | 
			
		||||
        if ((!(fileName.StartsWith(@"../Logs") || fileName.StartsWith(@"..\Logs")) && filePath.Contains("..")) || !System.IO.File.Exists(filePath))
 | 
			
		||||
        {
 | 
			
		||||
            return NotFound();
 | 
			
		||||
        }
 | 
			
		||||
@@ -49,6 +48,6 @@ public class FileController : ControllerBase
 | 
			
		||||
 | 
			
		||||
        Response.Headers.Append("Access-Control-Expose-Headers", "Content-Disposition");
 | 
			
		||||
 | 
			
		||||
        return File(fileStream, "application/octet-stream", (fileName.Replace('/', '_')));
 | 
			
		||||
        return File(fileStream, "application/octet-stream", (Path.GetFileName(filePath).Replace('/', '_')));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -21,16 +21,7 @@
 | 
			
		||||
    "UserNoModule": "This account has not been assigned a module. Please contact the administrator",
 | 
			
		||||
    "UserNull": "User {0} does not exist"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.BaseDataEntity": {
 | 
			
		||||
    "CreateOrgId": "CreateOrgId"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.BaseEntity": {
 | 
			
		||||
    "CreateTime": "CreateTime",
 | 
			
		||||
    "CreateUser": "CreateUser",
 | 
			
		||||
    "SortCode": "SortCode",
 | 
			
		||||
    "UpdateTime": "UpdateTime",
 | 
			
		||||
    "UpdateUser": "UpdateUser"
 | 
			
		||||
  },
 | 
			
		||||
  
 | 
			
		||||
  "ThingsGateway.Admin.Application.BlazorAuthenticationHandler": {
 | 
			
		||||
    "UserExpire": "User expired, please login again"
 | 
			
		||||
  },
 | 
			
		||||
 
 | 
			
		||||
@@ -21,16 +21,7 @@
 | 
			
		||||
    "UserNoModule": "该账号未分配模块,请联系管理员",
 | 
			
		||||
    "UserNull": "用户 {0} 不存在"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.BaseDataEntity": {
 | 
			
		||||
    "CreateOrgId": "创建机构Id"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.BaseEntity": {
 | 
			
		||||
    "CreateTime": "创建时间",
 | 
			
		||||
    "CreateUser": "创建人",
 | 
			
		||||
    "SortCode": "排序",
 | 
			
		||||
    "UpdateTime": "更新时间",
 | 
			
		||||
    "UpdateUser": "更新人"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  "ThingsGateway.Admin.Application.BlazorAuthenticationHandler": {
 | 
			
		||||
    "UserExpire": "用户登录已过期,请重新登录"
 | 
			
		||||
  },
 | 
			
		||||
 
 | 
			
		||||
@@ -377,9 +377,9 @@ internal sealed class SysUserService : BaseService<SysUser>, ISysUserService
 | 
			
		||||
    /// 获取用户拥有的资源
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="id">用户id</param>
 | 
			
		||||
    public async Task<GrantResourceData> OwnResourceAsync(long id)
 | 
			
		||||
    public Task<GrantResourceData> OwnResourceAsync(long id)
 | 
			
		||||
    {
 | 
			
		||||
        return await _roleService.OwnResourceAsync(id, RelationCategoryEnum.UserHasResource).ConfigureAwait(false);
 | 
			
		||||
        return _roleService.OwnResourceAsync(id, RelationCategoryEnum.UserHasResource);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -505,10 +505,10 @@ internal sealed class SysUserService : BaseService<SysUser>, ISysUserService
 | 
			
		||||
        var password = await GetDefaultPassWord(true).ConfigureAwait(false);//获取默认密码,这里不走Aop所以需要加密一下
 | 
			
		||||
        using var db = GetDB();
 | 
			
		||||
        //重置密码
 | 
			
		||||
        if (await db.UpdateSetColumnsTrueAsync<SysUser>(it => new SysUser
 | 
			
		||||
        if ((await db.UpdateSetColumnsTrueAsync<SysUser>(it => new SysUser
 | 
			
		||||
        {
 | 
			
		||||
            Password = password
 | 
			
		||||
        }, it => it.Id == id).ConfigureAwait(false))
 | 
			
		||||
        }, it => it.Id == id).ConfigureAwait(false)) > 0)
 | 
			
		||||
        {
 | 
			
		||||
            DeleteUserFromCache(id);//从cache删除用户信息
 | 
			
		||||
            var verificatInfoIds = _verificatInfoService.GetListByUserId(id);
 | 
			
		||||
 
 | 
			
		||||
@@ -185,12 +185,12 @@ internal sealed class UserCenterService : BaseService<SysUser>, IUserCenterServi
 | 
			
		||||
        using var db = GetDB();
 | 
			
		||||
 | 
			
		||||
        //更新指定字段
 | 
			
		||||
        var result = await db.UpdateSetColumnsTrueAsync<SysUser>(it => new SysUser
 | 
			
		||||
        var result = (await db.UpdateSetColumnsTrueAsync<SysUser>(it => new SysUser
 | 
			
		||||
        {
 | 
			
		||||
            Email = input.Email,
 | 
			
		||||
            Phone = input.Phone,
 | 
			
		||||
            Avatar = input.Avatar,
 | 
			
		||||
        }, it => it.Id == UserManager.UserId).ConfigureAwait(false);
 | 
			
		||||
        }, it => it.Id == UserManager.UserId).ConfigureAwait(false)) > 0;
 | 
			
		||||
        if (result)
 | 
			
		||||
            _userService.DeleteUserFromCache(UserManager.UserId);//cache删除用户数据
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@
 | 
			
		||||
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<GenerateDocumentationFile>True</GenerateDocumentationFile>
 | 
			
		||||
		
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
 | 
			
		||||
 
 | 
			
		||||
@@ -30,7 +30,7 @@ public class BlazorAppContext
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 全部菜单
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public IEnumerable<SysResource> AllMenus { get; private set; }
 | 
			
		||||
    public List<SysResource> AllMenus { get; private set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 当前用户
 | 
			
		||||
@@ -42,22 +42,22 @@ public class BlazorAppContext
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 用户个人菜单
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public IEnumerable<MenuItem> OwnMenuItems { get; private set; }
 | 
			
		||||
    public List<MenuItem> OwnMenuItems { get; private set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 不同模块的菜单
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public IEnumerable<MenuItem> AllOwnMenuItems { get; private set; }
 | 
			
		||||
    public List<MenuItem> AllOwnMenuItems { get; private set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 用户个人菜单,多个模块
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public IEnumerable<SysResource> OwnMenus { get; private set; }
 | 
			
		||||
    public List<SysResource> OwnMenus { get; private set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 用户个人菜单,非树形
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public IEnumerable<MenuItem> OwnSameLevelMenuItems { get; private set; }
 | 
			
		||||
    public List<MenuItem> OwnSameLevelMenuItems { get; private set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 个人工作台
 | 
			
		||||
@@ -67,9 +67,9 @@ public class BlazorAppContext
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 用户个人快捷方式菜单
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public IEnumerable<SysResource> UserWorkbenchOutputs { get; private set; }
 | 
			
		||||
    public List<SysResource> UserWorkbenchOutputs { get; private set; }
 | 
			
		||||
 | 
			
		||||
    public IEnumerable<SysResource> AllResource { get; private set; }
 | 
			
		||||
    public List<SysResource> AllResource { get; private set; }
 | 
			
		||||
 | 
			
		||||
    private ISysResourceService ResourceService { get; }
 | 
			
		||||
    private ISysUserService SysUserService { get; }
 | 
			
		||||
@@ -93,7 +93,7 @@ public class BlazorAppContext
 | 
			
		||||
            AllResource = sysResources;
 | 
			
		||||
            var ids = CurrentUser.ModuleList.Select(a => a.Id).ToHashSet();
 | 
			
		||||
            CurrentUser.ModuleList = AllResource.Where(a => ids.Contains(a.Id)).OrderBy(a => a.SortCode).ToList();
 | 
			
		||||
            AllMenus = AllResource.Where(a => a.Category == ResourceCategoryEnum.Menu);
 | 
			
		||||
            AllMenus = AllResource.Where(a => a.Category == ResourceCategoryEnum.Menu).ToList();
 | 
			
		||||
 | 
			
		||||
            if (moduleId == null)
 | 
			
		||||
            {
 | 
			
		||||
@@ -123,8 +123,8 @@ public class BlazorAppContext
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            var ownMenus = OwnMenus.Where(a => a.Module == CurrentModuleId);
 | 
			
		||||
            OwnMenuItems = ResourceUtil.BuildMenuTrees(ownMenus).ToList();
 | 
			
		||||
            AllOwnMenuItems = ResourceUtil.BuildMenuTrees(OwnMenus).ToList();
 | 
			
		||||
            OwnMenuItems = AdminResourceUtil.BuildMenuTrees(ownMenus).ToList();
 | 
			
		||||
            AllOwnMenuItems = AdminResourceUtil.BuildMenuTrees(OwnMenus).ToList();
 | 
			
		||||
            OwnSameLevelMenuItems = ownMenus.Where(a => !a.Href.IsNullOrWhiteSpace()).Select(item => new MenuItem()
 | 
			
		||||
            {
 | 
			
		||||
                Match = item.NavLinkMatch ?? Microsoft.AspNetCore.Components.Routing.NavLinkMatch.All,
 | 
			
		||||
@@ -132,8 +132,8 @@ public class BlazorAppContext
 | 
			
		||||
                Icon = item.Icon,
 | 
			
		||||
                Url = item.Href,
 | 
			
		||||
                Target = item.Target.ToString(),
 | 
			
		||||
            });
 | 
			
		||||
            UserWorkbenchOutputs = AllMenus.Where(it => UserWorkBench.Shortcuts.Contains(it.Id));
 | 
			
		||||
            }).ToList();
 | 
			
		||||
            UserWorkbenchOutputs = AllMenus.Where(it => UserWorkBench.Shortcuts.Contains(it.Id)).ToList();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -29,7 +29,7 @@ public partial class EditPagePolicy
 | 
			
		||||
 | 
			
		||||
    protected override Task OnParametersSetAsync()
 | 
			
		||||
    {
 | 
			
		||||
        ShortcutsTreeViewItems = ResourceUtil.BuildTreeItemList(AppContext.OwnMenus.WhereIf(!ShortcutsSearchText.IsNullOrEmpty(), a => a.Title.Contains(ShortcutsSearchText)), Model.Shortcuts, null);
 | 
			
		||||
        ShortcutsTreeViewItems = AdminResourceUtil.BuildTreeItemList(AppContext.OwnMenus.WhereIf(!ShortcutsSearchText.IsNullOrEmpty(), a => a.Title.Contains(ShortcutsSearchText)), Model.Shortcuts, null);
 | 
			
		||||
        return base.OnParametersSetAsync();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -48,6 +48,6 @@ public partial class EditPagePolicy
 | 
			
		||||
    {
 | 
			
		||||
        await Task.CompletedTask;
 | 
			
		||||
        ShortcutsSearchText = searchText;
 | 
			
		||||
        return ResourceUtil.BuildTreeItemList(AppContext.OwnMenus.WhereIf(!ShortcutsSearchText.IsNullOrEmpty(), a => a.Title.Contains(ShortcutsSearchText)), Model.Shortcuts, null);
 | 
			
		||||
        return AdminResourceUtil.BuildTreeItemList(AppContext.OwnMenus.WhereIf(!ShortcutsSearchText.IsNullOrEmpty(), a => a.Title.Contains(ShortcutsSearchText)), Model.Shortcuts, null);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -41,7 +41,7 @@ public partial class MenuChoiceDialog
 | 
			
		||||
        var all = (await SysResourceService.GetAllAsync());
 | 
			
		||||
        var items = all.Where(a => a.Category == ResourceCategoryEnum.Menu && a.Module == ModuleId);
 | 
			
		||||
        ModuleTitle = all.FirstOrDefault(a => a.Id == ModuleId)?.Title;
 | 
			
		||||
        Items = ResourceUtil.BuildTreeItemList(items, new List<long> { Value }, RenderTreeItem);
 | 
			
		||||
        Items = AdminResourceUtil.BuildTreeItemList(items, new List<long> { Value }, RenderTreeItem);
 | 
			
		||||
        await base.OnParametersSetAsync();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -26,12 +26,12 @@
 | 
			
		||||
                OnQueryAsync="OnQueryAsync" CustomerSearchModel="@CustomerSearchModel"
 | 
			
		||||
                OnSaveAsync="Save" OnDeleteAsync="Delete">
 | 
			
		||||
        <TableToolbarTemplate>
 | 
			
		||||
            <PopConfirmButton Color=Color.Warning IsDisabled="SelectedRows.Count<=0||!AuthorizeButton(AdminOperConst.Add)" Text=@OperDescLocalizer["CopyResource"] Icon="fa fa-copy" OnConfirm="OnCopy">
 | 
			
		||||
            <PopConfirmButton Color=Color.Warning IsKeepDisabled="SelectedRows.Count <= 0 || !AuthorizeButton(AdminOperConst.Add)" Text=@OperDescLocalizer["CopyResource"] Icon="fa fa-copy" OnConfirm="OnCopy">
 | 
			
		||||
                <BodyTemplate>
 | 
			
		||||
                    <Select Items="ModuleSelectedItems" @bind-Value=CopyModule ShowLabel="false" />
 | 
			
		||||
                </BodyTemplate>
 | 
			
		||||
            </PopConfirmButton>
 | 
			
		||||
            <PopConfirmButton Color=Color.Warning IsDisabled="SelectedRows.Count!=1||!AuthorizeButton(AdminOperConst.Edit)" Text=@OperDescLocalizer["ChangeParentResource"] Icon="fa fa-copy" OnConfirm="OnChangeParent">
 | 
			
		||||
            <PopConfirmButton Color=Color.Warning IsKeepDisabled="SelectedRows.Count != 1 || !AuthorizeButton(AdminOperConst.Edit)" Text=@OperDescLocalizer["ChangeParentResource"] Icon="fa fa-copy" OnConfirm="OnChangeParent">
 | 
			
		||||
                <BodyTemplate>
 | 
			
		||||
                    <div class="overflow-y-auto" style="height:500px">
 | 
			
		||||
                        <TreeView Items="MenuTreeItems" IsVirtualize="true" OnTreeItemClick="a=>{ChangeParentId=a.Value.Id;return Task.CompletedTask;}" />
 | 
			
		||||
 
 | 
			
		||||
@@ -39,8 +39,8 @@ public partial class SysResourcePage
 | 
			
		||||
 | 
			
		||||
    protected override async Task OnParametersSetAsync()
 | 
			
		||||
    {
 | 
			
		||||
        ModuleSelectedItems = ResourceUtil.BuildModuleSelectList((await SysResourceService.GetAllAsync())).ToList();
 | 
			
		||||
        MenuItems = ResourceUtil.BuildMenuSelectList((await SysResourceService.GetAllAsync())).Concat(new List<SelectedItem>() { new("0", AdminLocalizer["Root"]) }).ToList();
 | 
			
		||||
        ModuleSelectedItems = AdminResourceUtil.BuildModuleSelectList((await SysResourceService.GetAllAsync())).ToList();
 | 
			
		||||
        MenuItems = AdminResourceUtil.BuildMenuSelectList((await SysResourceService.GetAllAsync())).Concat(new List<SelectedItem>() { new("0", AdminLocalizer["Root"]) }).ToList();
 | 
			
		||||
 | 
			
		||||
        await base.OnParametersSetAsync();
 | 
			
		||||
    }
 | 
			
		||||
@@ -49,7 +49,7 @@ public partial class SysResourcePage
 | 
			
		||||
 | 
			
		||||
    private async Task<QueryData<SysResource>> OnQueryAsync(QueryPageOptions options)
 | 
			
		||||
    {
 | 
			
		||||
        MenuTreeItems = new List<TreeViewItem<SysResource>>() { new TreeViewItem<SysResource>(new SysResource()) { Text = AdminLocalizer["Root"] } }.Concat(ResourceUtil.BuildTreeItemList((await SysResourceService.GetAllAsync()).Where(a => a.Module == CustomerSearchModel.Module), new(), null)).ToList();
 | 
			
		||||
        MenuTreeItems = new List<TreeViewItem<SysResource>>() { new TreeViewItem<SysResource>(new SysResource()) { Text = AdminLocalizer["Root"] } }.Concat(AdminResourceUtil.BuildTreeItemList((await SysResourceService.GetAllAsync()).Where(a => a.Module == CustomerSearchModel.Module), new(), null)).ToList();
 | 
			
		||||
 | 
			
		||||
        var data = await SysResourceService.PageAsync(options, CustomerSearchModel);
 | 
			
		||||
        return data;
 | 
			
		||||
@@ -136,14 +136,14 @@ public partial class SysResourcePage
 | 
			
		||||
    private async Task<IEnumerable<TableTreeNode<SysResource>>> OnTreeExpand(SysResource menu)
 | 
			
		||||
    {
 | 
			
		||||
        var sysResources = await SysResourceService.GetAllAsync();
 | 
			
		||||
        var result = ResourceUtil.BuildTableTrees(sysResources, menu.Id);
 | 
			
		||||
        var result = AdminResourceUtil.BuildTableTrees(sysResources, menu.Id);
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static async Task<IEnumerable<TableTreeNode<SysResource>>> TreeNodeConverter(IEnumerable<SysResource> items)
 | 
			
		||||
    {
 | 
			
		||||
        await Task.CompletedTask;
 | 
			
		||||
        var result = ResourceUtil.BuildTableTrees(items, 0);
 | 
			
		||||
        var result = AdminResourceUtil.BuildTableTrees(items, 0);
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -35,7 +35,7 @@ public partial class GrantResourceDialog
 | 
			
		||||
    {
 | 
			
		||||
        var items = (await SysResourceService.GetAllAsync()).Where(a => a.Category != ResourceCategoryEnum.Module).OrderBy(a => a.Module).ThenBy(a => a.Id).ToList();
 | 
			
		||||
 | 
			
		||||
        Items = ResourceUtil.BuildTreeItemList(items, Value, RenderTreeItem);
 | 
			
		||||
        Items = AdminResourceUtil.BuildTreeItemList(items, Value, RenderTreeItem);
 | 
			
		||||
        ModuleList = (await SysResourceService.GetAllAsync()).Where(a => a.Category == ResourceCategoryEnum.Module).ToList();
 | 
			
		||||
        await base.OnInitializedAsync();
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -35,7 +35,7 @@ public partial class SysUserEdit
 | 
			
		||||
        BoolItems = LocalizerUtil.GetBoolItems(Model.GetType(), nameof(Model.Status));
 | 
			
		||||
        var items = await SysPositionService.SelectorAsync(new PositionSelectorInput());
 | 
			
		||||
        Items = PositionUtil.BuildCascaderItemList(items);
 | 
			
		||||
        ModuleSelectedItems = ResourceUtil.BuildModuleSelectList((await SysResourceService.GetAllAsync())).ToList();
 | 
			
		||||
        ModuleSelectedItems = AdminResourceUtil.BuildModuleSelectList((await SysResourceService.GetAllAsync())).ToList();
 | 
			
		||||
        await InvokeAsync(StateHasChanged);
 | 
			
		||||
        await base.OnInitializedAsync();
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@
 | 
			
		||||
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
 | 
			
		||||
		
 | 
			
		||||
		<!--<UseRazorSourceGenerator>false</UseRazorSourceGenerator>-->
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,7 @@ namespace ThingsGateway.Admin.Razor;
 | 
			
		||||
 | 
			
		||||
/// <inheritdoc/>
 | 
			
		||||
[ThingsGateway.DependencyInjection.SuppressSniffer]
 | 
			
		||||
public static class ResourceUtil
 | 
			
		||||
public static class AdminResourceUtil
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 构造选择项,ID/TITLE
 | 
			
		||||
@@ -29,7 +29,7 @@
 | 
			
		||||
  <Target Name="AdminPostPublish" AfterTargets="Publish">
 | 
			
		||||
    <ItemGroup>
 | 
			
		||||
      <!-- setting up the variable for convenience -->
 | 
			
		||||
      <AdminFiles Include="bin\$(Configuration)\$(TargetFramework)\SeedData\**" />
 | 
			
		||||
      <AdminFiles Include="$(OutputPath)\$(TargetFramework)\SeedData\**" />
 | 
			
		||||
    </ItemGroup>
 | 
			
		||||
    <PropertyGroup>
 | 
			
		||||
    </PropertyGroup>
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,8 @@
 | 
			
		||||
#推送:docker push registry.cn-shenzhen.aliyuncs.com/thingsgateway/thingsgateway
 | 
			
		||||
 | 
			
		||||
#aspnetcore9.0环境
 | 
			
		||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
 | 
			
		||||
#FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
 | 
			
		||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0-noble AS base
 | 
			
		||||
COPY .  /app
 | 
			
		||||
WORKDIR /app
 | 
			
		||||
#默认web
 | 
			
		||||
@@ -13,6 +14,8 @@ EXPOSE 5000
 | 
			
		||||
 | 
			
		||||
# 添加时区环境变量,亚洲,上海
 | 
			
		||||
ENV TimeZone=Asia/Shanghai
 | 
			
		||||
# 转发头
 | 
			
		||||
ENV ASPNETCORE_FORWARDEDHEADERS_ENABLED=true
 | 
			
		||||
# 使用软连接,并且将时区配置覆盖/etc/timezone
 | 
			
		||||
RUN ln -snf /usr/share/zoneinfo/$TimeZone /etc/localtime && echo $TimeZone > /etc/timezone
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,8 @@
 | 
			
		||||
#推送:docker push registry.cn-shenzhen.aliyuncs.com/thingsgateway/thingsgateway_arm64
 | 
			
		||||
 | 
			
		||||
#aspnetcore9.0环境
 | 
			
		||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine-arm64v8  AS base
 | 
			
		||||
#FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine-arm64v8  AS base
 | 
			
		||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0-noble-arm64v8 AS base
 | 
			
		||||
COPY .  /app
 | 
			
		||||
WORKDIR /app
 | 
			
		||||
#默认web
 | 
			
		||||
@@ -13,6 +14,8 @@ EXPOSE 5000
 | 
			
		||||
 | 
			
		||||
# 添加时区环境变量,亚洲,上海
 | 
			
		||||
ENV TimeZone=Asia/Shanghai
 | 
			
		||||
# 转发头
 | 
			
		||||
ENV ASPNETCORE_FORWARDEDHEADERS_ENABLED=true
 | 
			
		||||
# 使用软连接,并且将时区配置覆盖/etc/timezone
 | 
			
		||||
RUN ln -snf /usr/share/zoneinfo/$TimeZone /etc/localtime && echo $TimeZone > /etc/timezone
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -39,7 +39,7 @@
 | 
			
		||||
    <BlazorReconnector @rendermode="new InteractiveServerRenderMode(false)" />
 | 
			
		||||
 | 
			
		||||
    <script src=@($"_content/BootstrapBlazor/js/bootstrap.blazor.bundle.min.js?v={this.GetType().Assembly.GetName().Version}")></script>
 | 
			
		||||
    <script src=@($"{WebsiteConst.DefaultResourceUrl}js/culture.js?v={this.GetType().Assembly.GetName().Version}")></script>
 | 
			
		||||
    <script src=@($"{WebsiteConst.DefaultResourceUrl}js/localStorageUtil.js?v={this.GetType().Assembly.GetName().Version}")></script>
 | 
			
		||||
    <script src="_framework/blazor.web.js"></script>
 | 
			
		||||
    <!-- PWA Service Worker -->
 | 
			
		||||
    <script type="text/javascript">'serviceWorker' in navigator && navigator.serviceWorker.register('./service-worker.js')</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -45,7 +45,7 @@
 | 
			
		||||
    </app>
 | 
			
		||||
    
 | 
			
		||||
    <script src="_content/BootstrapBlazor/js/bootstrap.blazor.bundle.min.js"></script>
 | 
			
		||||
    <script src=@($"{WebsiteConst.DefaultResourceUrl}js/culture.js")></script>
 | 
			
		||||
    <script src=@($"{WebsiteConst.DefaultResourceUrl}js/localStorageUtil.js")></script>
 | 
			
		||||
    <script src="_framework/blazor.server.js"></script>
 | 
			
		||||
 | 
			
		||||
       <!-- PWA Service Worker -->
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@
 | 
			
		||||
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
 | 
			
		||||
		
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
 | 
			
		||||
	<!--<Import Project="Admin.targets" Condition=" '$(Configuration)' != 'Debug' " />-->
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@
 | 
			
		||||
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<GenerateDocumentationFile>True</GenerateDocumentationFile>
 | 
			
		||||
		
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
 | 
			
		||||
@@ -13,7 +14,7 @@
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
		<PackageReference Include="BootstrapBlazor.TableExport" Version="9.2.6" />
 | 
			
		||||
		<PackageReference Include="Yitter.IdGenerator" Version="1.0.14" />
 | 
			
		||||
		<PackageReference Include="BootstrapBlazor" Version="9.9.1" />
 | 
			
		||||
		<PackageReference Include="BootstrapBlazor" Version="9.9.3" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
 
 | 
			
		||||
@@ -53,6 +53,8 @@ public static class QueryPageOptionsExtensions
 | 
			
		||||
        return datas;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public static IEnumerable<T> GetQuery<T>(this IEnumerable<T> query, QueryPageOptions option, Func<IEnumerable<T>, IEnumerable<T>>? queryFunc = null, FilterKeyValueAction where = null)
 | 
			
		||||
    {
 | 
			
		||||
        if (queryFunc != null)
 | 
			
		||||
@@ -123,7 +125,36 @@ public static class QueryPageOptionsExtensions
 | 
			
		||||
        };
 | 
			
		||||
        var items = datas.GetData(option, out var totalCount, where);
 | 
			
		||||
        ret.TotalCount = totalCount;
 | 
			
		||||
 | 
			
		||||
        if (totalCount > 0)
 | 
			
		||||
        {
 | 
			
		||||
            if (!items.Any() && option.PageIndex != 1)
 | 
			
		||||
            {
 | 
			
		||||
                option.PageIndex = 1;
 | 
			
		||||
                items = datas.GetData(option, out totalCount, where);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        ret.Items = items.ToList();
 | 
			
		||||
        return ret;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 根据查询条件返回QueryData
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static QueryData<SelectedItem> GetQueryData<T>(this IEnumerable<T> datas, VirtualizeQueryOption option, Func<IEnumerable<T>, IEnumerable<SelectedItem>> func, FilterKeyValueAction where = null)
 | 
			
		||||
    {
 | 
			
		||||
        var ret = new QueryData<SelectedItem>()
 | 
			
		||||
        {
 | 
			
		||||
            IsSorted = false,
 | 
			
		||||
            IsFiltered = false,
 | 
			
		||||
            IsAdvanceSearch = false,
 | 
			
		||||
            IsSearch = !option.SearchText.IsNullOrWhiteSpace()
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        var items = datas.Skip((option.StartIndex)).Take(option.Count);
 | 
			
		||||
        ret.TotalCount = datas.Count();
 | 
			
		||||
 | 
			
		||||
        ret.Items = func(items).ToList();
 | 
			
		||||
        return ret;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -209,16 +209,10 @@ public static class SqlSugarExtensions
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public static async Task<bool> UpdateRangeAsync<T>(this SqlSugarClient db, List<T> updateObjs) where T : class, new()
 | 
			
		||||
    public static Task<int> UpdateSetColumnsTrueAsync<T>(this SqlSugarClient db, Expression<Func<T, T>> columns, Expression<Func<T, bool>> whereExpression) where T : class, new()
 | 
			
		||||
    {
 | 
			
		||||
        return await db.Updateable(updateObjs).ExecuteCommandAsync().ConfigureAwait(false) > 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public static async Task<bool> UpdateSetColumnsTrueAsync<T>(this SqlSugarClient db, Expression<Func<T, T>> columns, Expression<Func<T, bool>> whereExpression) where T : class, new()
 | 
			
		||||
    {
 | 
			
		||||
        return await db.Updateable<T>().SetColumns(columns, appendColumnsByDataFilter: true).Where(whereExpression)
 | 
			
		||||
            .ExecuteCommandAsync().ConfigureAwait(false) > 0;
 | 
			
		||||
        return db.Updateable<T>().SetColumns(columns, appendColumnsByDataFilter: true).Where(whereExpression)
 | 
			
		||||
            .ExecuteCommandAsync();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static IEnumerable<T> Sort<T>(this IEnumerable<T> list, BasePageInput basePageInput)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										13
									
								
								src/Admin/ThingsGateway.DB/Locales/en-US.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/Admin/ThingsGateway.DB/Locales/en-US.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
			
		||||
{
 | 
			
		||||
 
 | 
			
		||||
  "ThingsGateway.Admin.Application.BaseDataEntity": {
 | 
			
		||||
    "CreateOrgId": "CreateOrgId"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.BaseEntity": {
 | 
			
		||||
    "CreateTime": "CreateTime",
 | 
			
		||||
    "CreateUser": "CreateUser",
 | 
			
		||||
    "SortCode": "SortCode",
 | 
			
		||||
    "UpdateTime": "UpdateTime",
 | 
			
		||||
    "UpdateUser": "UpdateUser"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										13
									
								
								src/Admin/ThingsGateway.DB/Locales/zh-CN.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/Admin/ThingsGateway.DB/Locales/zh-CN.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
			
		||||
{
 | 
			
		||||
  
 | 
			
		||||
  "ThingsGateway.DB.BaseDataEntity": {
 | 
			
		||||
    "CreateOrgId": "创建机构Id"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.DB.BaseEntity": {
 | 
			
		||||
    "CreateTime": "创建时间",
 | 
			
		||||
    "CreateUser": "创建人",
 | 
			
		||||
    "SortCode": "排序",
 | 
			
		||||
    "UpdateTime": "更新时间",
 | 
			
		||||
    "UpdateUser": "更新人"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -5,6 +5,7 @@
 | 
			
		||||
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<GenerateDocumentationFile>True</GenerateDocumentationFile>
 | 
			
		||||
		
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
 | 
			
		||||
@@ -18,6 +19,12 @@
 | 
			
		||||
		<None Remove="..\..\..\README.zh-CN.md" Pack="false" PackagePath="\" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
	  <EmbeddedResource Include="Locales\en-US.json" />
 | 
			
		||||
	  <EmbeddedResource Include="Locales\zh-CN.json" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
		<!--<PackageReference Include="ThingsGateway.Razor" Version="$(SourceGeneratorVersion)" />-->
 | 
			
		||||
		<!--<ProjectReference Include="..\ThingsGateway.Razor\ThingsGateway.Razor.csproj" />-->
 | 
			
		||||
 
 | 
			
		||||
@@ -554,10 +554,9 @@ public static class App
 | 
			
		||||
        {
 | 
			
		||||
            types = ass.GetTypes();
 | 
			
		||||
        }
 | 
			
		||||
        catch
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            XTrace.Log.Warn($"Error load `{ass.FullName}` assembly.");
 | 
			
		||||
            Console.WriteLine($"Error load `{ass.FullName}` assembly.");
 | 
			
		||||
            XTrace.Log.Warn($"Error load `{ass.FullName}` assembly. : {ex.Message}");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return types.Where(u => u.IsPublic && !u.IsDefined(typeof(SuppressSnifferAttribute), false));
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@
 | 
			
		||||
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
 | 
			
		||||
		
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
	
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										45
									
								
								src/Admin/ThingsGateway.NewLife.X/Common/BoundedQueue.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/Admin/ThingsGateway.NewLife.X/Common/BoundedQueue.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,45 @@
 | 
			
		||||
namespace ThingsGateway.NewLife;
 | 
			
		||||
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
 | 
			
		||||
public class BoundedQueue<T> : IEnumerable<T>
 | 
			
		||||
{
 | 
			
		||||
    private readonly Queue<T> _queue;
 | 
			
		||||
    private readonly int _capacity;
 | 
			
		||||
    private readonly object _syncRoot = new object();
 | 
			
		||||
 | 
			
		||||
    public BoundedQueue(int capacity)
 | 
			
		||||
    {
 | 
			
		||||
        if (capacity <= 0) throw new ArgumentOutOfRangeException(nameof(capacity));
 | 
			
		||||
        _capacity = capacity;
 | 
			
		||||
        _queue = new Queue<T>(capacity);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void Enqueue(T item)
 | 
			
		||||
    {
 | 
			
		||||
        lock (_syncRoot)
 | 
			
		||||
        {
 | 
			
		||||
            if (_queue.Count == _capacity)
 | 
			
		||||
                _queue.Dequeue();
 | 
			
		||||
            _queue.Enqueue(item);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public int Count
 | 
			
		||||
    {
 | 
			
		||||
        get { lock (_syncRoot) return _queue.Count; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public IEnumerator<T> GetEnumerator()
 | 
			
		||||
    {
 | 
			
		||||
        lock (_syncRoot)
 | 
			
		||||
        {
 | 
			
		||||
            return new List<T>(_queue).GetEnumerator();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -21,6 +21,7 @@ namespace ThingsGateway.NewLife.Json.Extension;
 | 
			
		||||
/// </summary>
 | 
			
		||||
public static class SystemTextJsonExtension
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 默认Json规则(带缩进)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -31,37 +32,51 @@ public static class SystemTextJsonExtension
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static JsonSerializerOptions NoneIndentedOptions;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 默认Json规则(带缩进)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static JsonSerializerOptions IgnoreNullIndentedOptions;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 默认Json规则(无缩进)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static JsonSerializerOptions IgnoreNullNoneIndentedOptions;
 | 
			
		||||
 | 
			
		||||
    public static JsonSerializerOptions GetOptions(bool writeIndented, bool ignoreNull)
 | 
			
		||||
    {
 | 
			
		||||
        var options = new JsonSerializerOptions
 | 
			
		||||
        {
 | 
			
		||||
            Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
 | 
			
		||||
            WriteIndented = writeIndented,
 | 
			
		||||
            DefaultIgnoreCondition = ignoreNull
 | 
			
		||||
                ? JsonIgnoreCondition.WhenWritingNull
 | 
			
		||||
                : JsonIgnoreCondition.Never,
 | 
			
		||||
            NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        options.Converters.Add(new ByteArrayToNumberArrayConverterSystemTextJson());
 | 
			
		||||
        options.Converters.Add(new JTokenSystemTextJsonConverter());
 | 
			
		||||
        options.Converters.Add(new JValueSystemTextJsonConverter());
 | 
			
		||||
        options.Converters.Add(new JObjectSystemTextJsonConverter());
 | 
			
		||||
        options.Converters.Add(new JArraySystemTextJsonConverter());
 | 
			
		||||
 | 
			
		||||
        return options;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static SystemTextJsonExtension()
 | 
			
		||||
    {
 | 
			
		||||
        IndentedOptions = new JsonSerializerOptions
 | 
			
		||||
        {
 | 
			
		||||
            Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
 | 
			
		||||
            WriteIndented = true, // 缩进
 | 
			
		||||
            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, // 忽略 null
 | 
			
		||||
            NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals,
 | 
			
		||||
        };
 | 
			
		||||
        // 如有自定义Converter,这里添加
 | 
			
		||||
        // IndentedOptions.Converters.Add(new ByteArrayJsonConverter());
 | 
			
		||||
        IndentedOptions.Converters.Add(new ByteArrayToNumberArrayConverterSystemTextJson());
 | 
			
		||||
        IndentedOptions.Converters.Add(new JTokenSystemTextJsonConverter());
 | 
			
		||||
        IndentedOptions.Converters.Add(new JValueSystemTextJsonConverter());
 | 
			
		||||
        IndentedOptions.Converters.Add(new JObjectSystemTextJsonConverter());
 | 
			
		||||
        IndentedOptions.Converters.Add(new JArraySystemTextJsonConverter());
 | 
			
		||||
        NoneIndentedOptions = new JsonSerializerOptions
 | 
			
		||||
        {
 | 
			
		||||
            Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
 | 
			
		||||
            WriteIndented = false, // 不缩进
 | 
			
		||||
            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
 | 
			
		||||
            NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals,
 | 
			
		||||
        };
 | 
			
		||||
        NoneIndentedOptions.Converters.Add(new ByteArrayToNumberArrayConverterSystemTextJson());
 | 
			
		||||
        NoneIndentedOptions.Converters.Add(new JTokenSystemTextJsonConverter());
 | 
			
		||||
        NoneIndentedOptions.Converters.Add(new JValueSystemTextJsonConverter());
 | 
			
		||||
        NoneIndentedOptions.Converters.Add(new JObjectSystemTextJsonConverter());
 | 
			
		||||
        NoneIndentedOptions.Converters.Add(new JArraySystemTextJsonConverter());
 | 
			
		||||
        // NoneIndentedOptions.Converters.Add(new ByteArrayJsonConverter());
 | 
			
		||||
 | 
			
		||||
        IndentedOptions = GetOptions(true, false);
 | 
			
		||||
        NoneIndentedOptions = GetOptions(false, false);
 | 
			
		||||
 | 
			
		||||
        IgnoreNullIndentedOptions = GetOptions(true, true);
 | 
			
		||||
        IgnoreNullNoneIndentedOptions = GetOptions(false, true);
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 反序列化
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -96,17 +111,17 @@ public static class SystemTextJsonExtension
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 序列化
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static string ToSystemTextJsonString(this object item, bool indented = true)
 | 
			
		||||
    public static string ToSystemTextJsonString(this object item, bool indented = true, bool ignoreNull = true)
 | 
			
		||||
    {
 | 
			
		||||
        return JsonSerializer.Serialize(item, item?.GetType() ?? typeof(object), indented ? IndentedOptions : NoneIndentedOptions);
 | 
			
		||||
        return JsonSerializer.Serialize(item, item?.GetType() ?? typeof(object), ignoreNull ? indented ? IgnoreNullIndentedOptions : IgnoreNullNoneIndentedOptions : indented ? IndentedOptions : NoneIndentedOptions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 序列化
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static byte[] ToSystemTextJsonUtf8Bytes(this object item, bool indented = true)
 | 
			
		||||
    public static byte[] ToSystemTextJsonUtf8Bytes(this object item, bool indented = true, bool ignoreNull = true)
 | 
			
		||||
    {
 | 
			
		||||
        return JsonSerializer.SerializeToUtf8Bytes(item, item.GetType(), indented ? IndentedOptions : NoneIndentedOptions);
 | 
			
		||||
        return JsonSerializer.SerializeToUtf8Bytes(item, item.GetType(), ignoreNull ? indented ? IgnoreNullIndentedOptions : IgnoreNullNoneIndentedOptions : indented ? IndentedOptions : NoneIndentedOptions);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -190,7 +190,31 @@ public sealed class Crc32 //: HashAlgorithm
 | 
			
		||||
        crc.Update(stream, count);
 | 
			
		||||
        return crc.Value;
 | 
			
		||||
    }
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 添加Sequence进行校验
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="sequence"></param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public Crc32 Update(ReadOnlySequence<byte> sequence)
 | 
			
		||||
    {
 | 
			
		||||
        foreach (var segment in sequence)
 | 
			
		||||
        {
 | 
			
		||||
            Update(segment.Span);
 | 
			
		||||
        }
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 计算校验码 (Sequence)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="sequence"></param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public static UInt32 Compute(ReadOnlySequence<byte> sequence)
 | 
			
		||||
    {
 | 
			
		||||
        var crc = new Crc32();
 | 
			
		||||
        crc.Update(sequence);
 | 
			
		||||
        return crc.Value;
 | 
			
		||||
    }
 | 
			
		||||
    //#region 抽象实现
 | 
			
		||||
    ///// <summary>哈希核心</summary>
 | 
			
		||||
    ///// <param name="array"></param>
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@
 | 
			
		||||
		<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
 | 
			
		||||
		<SignAssembly>True</SignAssembly>
 | 
			
		||||
		<AssemblyOriginatorKeyFile>newlife.snk</AssemblyOriginatorKeyFile>
 | 
			
		||||
		
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
 
 | 
			
		||||
@@ -306,8 +306,19 @@ public class TimerScheduler : ILogFeature
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var func = timer.Method.As<Func<Object?, Task>>(target);
 | 
			
		||||
            await func!(timer.State).ConfigureAwait(false);
 | 
			
		||||
#if NET6_0_OR_GREATER
 | 
			
		||||
            if (timer.IsValueTask)
 | 
			
		||||
            {
 | 
			
		||||
                var func = timer.Method.As<Func<Object?, ValueTask>>(target);
 | 
			
		||||
                await func!(timer.State).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
#endif
 | 
			
		||||
            {
 | 
			
		||||
                var func = timer.Method.As<Func<Object?, Task>>(target);
 | 
			
		||||
                await func!(timer.State).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
        catch (ThreadAbortException) { throw; }
 | 
			
		||||
        catch (ThreadInterruptedException) { throw; }
 | 
			
		||||
 
 | 
			
		||||
@@ -87,6 +87,8 @@ public class TimerX : ITimer, ITimerx, IDisposable
 | 
			
		||||
 | 
			
		||||
    private DateTime _AbsolutelyNext;
 | 
			
		||||
    private readonly Cron[]? _crons;
 | 
			
		||||
 | 
			
		||||
    internal bool IsValueTask { get; }
 | 
			
		||||
    #endregion
 | 
			
		||||
 | 
			
		||||
    //    #region 静态
 | 
			
		||||
@@ -158,6 +160,29 @@ public class TimerX : ITimer, ITimerx, IDisposable
 | 
			
		||||
        Init(dueTime);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
#if NET6_0_OR_GREATER
 | 
			
		||||
 | 
			
		||||
    /// <summary>实例化一个不可重入的定时器</summary>
 | 
			
		||||
    /// <param name="callback">委托</param>
 | 
			
		||||
    /// <param name="state">用户数据</param>
 | 
			
		||||
    /// <param name="dueTime">多久之后开始。毫秒</param>
 | 
			
		||||
    /// <param name="period">间隔周期。毫秒</param>
 | 
			
		||||
    /// <param name="scheduler">调度器</param>
 | 
			
		||||
    public TimerX(Func<Object, ValueTask> callback, Object? state, Int32 dueTime, Int32 period, String? scheduler = null) : this(callback.Target, callback.Method, state, scheduler)
 | 
			
		||||
    {
 | 
			
		||||
        IsValueTask = true;
 | 
			
		||||
        if (callback == null) throw new ArgumentNullException(nameof(callback));
 | 
			
		||||
        if (dueTime < 0) throw new ArgumentOutOfRangeException(nameof(dueTime));
 | 
			
		||||
 | 
			
		||||
        IsAsyncTask = true;
 | 
			
		||||
        Async = true;
 | 
			
		||||
        Period = period;
 | 
			
		||||
 | 
			
		||||
        Init(dueTime);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
    /// <summary>实例化一个绝对定时器,指定时刻执行,跟当前时间和SetNext无关</summary>
 | 
			
		||||
    /// <param name="callback">委托</param>
 | 
			
		||||
    /// <param name="state">用户数据</param>
 | 
			
		||||
@@ -210,6 +235,37 @@ public class TimerX : ITimer, ITimerx, IDisposable
 | 
			
		||||
        Init(ms);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
#if NET6_0_OR_GREATER
 | 
			
		||||
 | 
			
		||||
    /// <summary>实例化一个绝对定时器,指定时刻执行,跟当前时间和SetNext无关</summary>
 | 
			
		||||
    /// <param name="callback">委托</param>
 | 
			
		||||
    /// <param name="state">用户数据</param>
 | 
			
		||||
    /// <param name="startTime">绝对开始时间</param>
 | 
			
		||||
    /// <param name="period">间隔周期。毫秒</param>
 | 
			
		||||
    /// <param name="scheduler">调度器</param>
 | 
			
		||||
    public TimerX(Func<Object, ValueTask> callback, Object? state, DateTime startTime, Int32 period, String? scheduler = null) : this(callback.Target, callback.Method, state, scheduler)
 | 
			
		||||
    {
 | 
			
		||||
        IsValueTask = true;
 | 
			
		||||
        if (callback == null) throw new ArgumentNullException(nameof(callback));
 | 
			
		||||
        if (startTime <= DateTime.MinValue) throw new ArgumentOutOfRangeException(nameof(startTime));
 | 
			
		||||
        if (period <= 0) throw new ArgumentOutOfRangeException(nameof(period));
 | 
			
		||||
 | 
			
		||||
        IsAsyncTask = true;
 | 
			
		||||
        Async = true;
 | 
			
		||||
        Period = period;
 | 
			
		||||
        Absolutely = true;
 | 
			
		||||
 | 
			
		||||
        //var now = DateTime.Now;
 | 
			
		||||
        var now = Scheduler.GetNow();
 | 
			
		||||
        var next = startTime;
 | 
			
		||||
        while (next < now) next = next.AddMilliseconds(period);
 | 
			
		||||
 | 
			
		||||
        var ms = (Int64)(next - now).TotalMilliseconds;
 | 
			
		||||
        _AbsolutelyNext = next;
 | 
			
		||||
        Init(ms);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
    /// <summary>实例化一个Cron定时器</summary>
 | 
			
		||||
    /// <param name="callback">委托</param>
 | 
			
		||||
    /// <param name="state">用户数据</param>
 | 
			
		||||
@@ -274,6 +330,42 @@ public class TimerX : ITimer, ITimerx, IDisposable
 | 
			
		||||
        //Init(_AbsolutelyNext = _cron.GetNext(DateTime.Now));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
#if NET6_0_OR_GREATER
 | 
			
		||||
    /// <summary>实例化一个Cron定时器</summary>
 | 
			
		||||
    /// <param name="callback">委托</param>
 | 
			
		||||
    /// <param name="state">用户数据</param>
 | 
			
		||||
    /// <param name="cronExpression">Cron表达式。支持多个表达式,分号分隔</param>
 | 
			
		||||
    /// <param name="scheduler">调度器</param>
 | 
			
		||||
    public TimerX(Func<Object, ValueTask> callback, Object? state, String cronExpression, String? scheduler = null) : this(callback.Target, callback.Method, state, scheduler)
 | 
			
		||||
    {
 | 
			
		||||
        IsValueTask = true;
 | 
			
		||||
        if (callback == null) throw new ArgumentNullException(nameof(callback));
 | 
			
		||||
        if (cronExpression.IsNullOrEmpty()) throw new ArgumentNullException(nameof(cronExpression));
 | 
			
		||||
 | 
			
		||||
        var list = new List<Cron>();
 | 
			
		||||
        foreach (var item in cronExpression.Split(";"))
 | 
			
		||||
        {
 | 
			
		||||
            var cron = new Cron();
 | 
			
		||||
            if (!cron.Parse(item)) throw new ArgumentException($"Invalid Cron expression[{item}]", nameof(cronExpression));
 | 
			
		||||
 | 
			
		||||
            list.Add(cron);
 | 
			
		||||
        }
 | 
			
		||||
        _crons = list.ToArray();
 | 
			
		||||
 | 
			
		||||
        IsAsyncTask = true;
 | 
			
		||||
        Async = true;
 | 
			
		||||
        Absolutely = true;
 | 
			
		||||
 | 
			
		||||
        //var now = DateTime.Now;
 | 
			
		||||
        var now = Scheduler.GetNow();
 | 
			
		||||
        var next = _crons.Min(e => e.GetNext(now));
 | 
			
		||||
        var ms = (Int64)(next - now).TotalMilliseconds;
 | 
			
		||||
        _AbsolutelyNext = next;
 | 
			
		||||
        Init(ms);
 | 
			
		||||
        //Init(_AbsolutelyNext = _cron.GetNext(DateTime.Now));
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
    public bool Disposed { get; private set; }
 | 
			
		||||
    /// <summary>销毁定时器</summary>
 | 
			
		||||
    public void Dispose()
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@
 | 
			
		||||
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
 | 
			
		||||
		
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
	
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
 
 | 
			
		||||
@@ -8,8 +8,8 @@
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.Common.Extension;
 | 
			
		||||
using ThingsGateway.NewLife;
 | 
			
		||||
using ThingsGateway.Razor.Extension;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Razor;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@
 | 
			
		||||
<Step @ref="@step" IsVertical="true">
 | 
			
		||||
    <StepItem Text=@Localizer["First"] Title=@Localizer["Upload"]>
 | 
			
		||||
        <InputUpload ShowDeleteButton="true" @bind-Value=_importFile Accept=".xlsx"></InputUpload>
 | 
			
		||||
        <Button class="mt-2" IsAsync OnClick="() => DeviceImport(_importFile)">@Localizer["Validate"]</Button>
 | 
			
		||||
        <PopConfirmButton IsAsync Color=Color.Warning class="mt-2" OnConfirm=@(SaveDeviceImport)>@Localizer["Import"]</PopConfirmButton>
 | 
			
		||||
    </StepItem>
 | 
			
		||||
    <StepItem Text=@Localizer["Second"] Title=@Localizer["ValidateText"]>
 | 
			
		||||
 | 
			
		||||
@@ -41,16 +41,12 @@
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            <PopConfirmButton IsAsync IsDisabled=@_importPreviews.Any(it => it.Value.HasError) Color=Color.Warning class="mt-2" OnConfirm=@(SaveDeviceImport)>@Localizer["Import"]</PopConfirmButton>
 | 
			
		||||
            <Button class="mt-2" IsAsync OnClick="() => DeviceImport()">@RazorLocalizer["Close"]</Button>
 | 
			
		||||
 | 
			
		||||
@* 
 | 
			
		||||
            <Button IsAsync class="mt-2" IsDisabled=@_importPreviews.Any(it => it.Value.HasError) OnClick="() => step.Next()">@Localizer["Next"]</Button> *@
 | 
			
		||||
 | 
			
		||||
        </div>
 | 
			
		||||
    </StepItem>
 | 
			
		||||
@*     <StepItem Text=@Localizer["Third"] Title=@Localizer["Import"]>
 | 
			
		||||
        <PopConfirmButton IsAsync Color=Color.Warning class="mt-2" OnConfirm=@(SaveDeviceImport)>@Localizer["Import"]</PopConfirmButton>
 | 
			
		||||
    </StepItem> *@
 | 
			
		||||
 | 
			
		||||
</Step>
 | 
			
		||||
@code {
 | 
			
		||||
    [NotNull]
 | 
			
		||||
 
 | 
			
		||||
@@ -24,18 +24,17 @@ public partial class ImportExcel
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [Parameter]
 | 
			
		||||
    [EditorRequired]
 | 
			
		||||
    public Func<Dictionary<string, ImportPreviewOutputBase>, Task> Import { get; set; }
 | 
			
		||||
    public Func<IBrowserFile, Task<Dictionary<string, ImportPreviewOutputBase>>> Import { get; set; }
 | 
			
		||||
 | 
			
		||||
    [Inject]
 | 
			
		||||
    [NotNull]
 | 
			
		||||
    private IStringLocalizer<ImportExcel>? Localizer { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 预览
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [Parameter]
 | 
			
		||||
    [EditorRequired]
 | 
			
		||||
    public Func<IBrowserFile, Task<Dictionary<string, ImportPreviewOutputBase>>> Preview { get; set; }
 | 
			
		||||
    [Inject]
 | 
			
		||||
    [NotNull]
 | 
			
		||||
    private IStringLocalizer<ThingsGateway.Razor._Imports>? RazorLocalizer { get; set; }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    [Inject]
 | 
			
		||||
    [NotNull]
 | 
			
		||||
@@ -47,13 +46,17 @@ public partial class ImportExcel
 | 
			
		||||
    [CascadingParameter]
 | 
			
		||||
    private Func<Task>? OnCloseAsync { get; set; }
 | 
			
		||||
 | 
			
		||||
    private async Task DeviceImport(IBrowserFile file)
 | 
			
		||||
    private async Task DeviceImport()
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            _importPreviews.Clear();
 | 
			
		||||
            _importPreviews = await Preview.Invoke(file);
 | 
			
		||||
            await step.Next();
 | 
			
		||||
            await InvokeAsync(async () =>
 | 
			
		||||
            {
 | 
			
		||||
                if (OnCloseAsync != null)
 | 
			
		||||
                    await OnCloseAsync();
 | 
			
		||||
                await ToastService.Default();
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
@@ -67,16 +70,12 @@ public partial class ImportExcel
 | 
			
		||||
        {
 | 
			
		||||
            await Task.Run(async () =>
 | 
			
		||||
            {
 | 
			
		||||
                await Import.Invoke(_importPreviews);
 | 
			
		||||
                _importPreviews = await Import.Invoke(_importFile);
 | 
			
		||||
                _importFile = null;
 | 
			
		||||
 | 
			
		||||
                await InvokeAsync(async () =>
 | 
			
		||||
                {
 | 
			
		||||
                    if (OnCloseAsync != null)
 | 
			
		||||
                        await OnCloseAsync();
 | 
			
		||||
                    await ToastService.Default();
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
            await step.Next();
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,58 @@
 | 
			
		||||
@using ThingsGateway.Extension
 | 
			
		||||
@namespace ThingsGateway.Razor
 | 
			
		||||
<Button OnClick="() => step.Reset()">@Localizer["Reset"]</Button>
 | 
			
		||||
<h6 class="my-3 green--text">@Localizer["Tip"] </h6>
 | 
			
		||||
<Step @ref="@step" IsVertical="true">
 | 
			
		||||
    <StepItem Text=@Localizer["First"] Title=@Localizer["Upload"]>
 | 
			
		||||
        <InputUpload ShowDeleteButton="true" @bind-Value=_importFile Accept=".xlsx"></InputUpload>
 | 
			
		||||
        <Button class="mt-2" IsAsync OnClick="() => DeviceImport(_importFile)">@Localizer["Validate"]</Button>
 | 
			
		||||
    </StepItem>
 | 
			
		||||
    <StepItem Text=@Localizer["Second"] Title=@Localizer["ValidateText"]>
 | 
			
		||||
 | 
			
		||||
        <div class="overflow-y-auto">
 | 
			
		||||
 | 
			
		||||
            @foreach (var item in _importPreviews)
 | 
			
		||||
            {
 | 
			
		||||
                <div class="mt-2">
 | 
			
		||||
                    @(
 | 
			
		||||
                                    Localizer["UploadCount", item.Key, item.Value.DataCount]
 | 
			
		||||
                                    )
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class=@((item.Value.HasError ? "my-2 red--text" : "my-2 green--text"))>
 | 
			
		||||
                    @(
 | 
			
		||||
                                    (item.Value.HasError ? "Error" : "Success")
 | 
			
		||||
                                    )
 | 
			
		||||
            </div>
 | 
			
		||||
                        if (item.Value.HasError)
 | 
			
		||||
                {
 | 
			
		||||
                    <div style="height:300px;" class="overflow-y-scroll">
 | 
			
		||||
                        <Virtualize Items="item.Value.Results.Where(a => !a.Success).OrderBy(a => a.Row).ToList()" Context="item1" ItemSize="60" OverscanCount=2>
 | 
			
		||||
                            <ItemContent>
 | 
			
		||||
                                <div class="row g-0">
 | 
			
		||||
                                    <span class="col mx-2">@item1.Row</span>
 | 
			
		||||
                                    <span class=@((item1.Success ? "green--text col-auto" : "red--text col-auto"))>
 | 
			
		||||
                                        <strong>@item1.ErrorMessage</strong>
 | 
			
		||||
                                    </span>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </ItemContent>
 | 
			
		||||
                        </Virtualize>
 | 
			
		||||
                    </div>
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            <PopConfirmButton IsAsync IsKeepDisabled=@_importPreviews.Any(it => it.Value.HasError) Color=Color.Warning class="mt-2" OnConfirm=@(SaveDeviceImport)>@Localizer["Import"]</PopConfirmButton>
 | 
			
		||||
 | 
			
		||||
@* 
 | 
			
		||||
            <Button IsAsync class="mt-2" IsDisabled=@_importPreviews.Any(it => it.Value.HasError) OnClick="() => step.Next()">@Localizer["Next"]</Button> *@
 | 
			
		||||
 | 
			
		||||
        </div>
 | 
			
		||||
    </StepItem>
 | 
			
		||||
@*     <StepItem Text=@Localizer["Third"] Title=@Localizer["Import"]>
 | 
			
		||||
        <PopConfirmButton IsAsync Color=Color.Warning class="mt-2" OnConfirm=@(SaveDeviceImport)>@Localizer["Import"]</PopConfirmButton>
 | 
			
		||||
    </StepItem> *@
 | 
			
		||||
</Step>
 | 
			
		||||
@code {
 | 
			
		||||
    [NotNull]
 | 
			
		||||
    Step? step { get; set; }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,86 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
//  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
 | 
			
		||||
//  此代码版权(除特别声明外的代码)归作者本人Diego所有
 | 
			
		||||
//  源代码使用协议遵循本仓库的开源协议及附加协议
 | 
			
		||||
//  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
 | 
			
		||||
//  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
 | 
			
		||||
//  使用文档:https://thingsgateway.cn/
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using Microsoft.AspNetCore.Components.Forms;
 | 
			
		||||
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Razor;
 | 
			
		||||
 | 
			
		||||
/// <inheritdoc/>
 | 
			
		||||
public partial class ImportExcelConfirm
 | 
			
		||||
{
 | 
			
		||||
    private Dictionary<string, ImportPreviewOutputBase> _importPreviews = new();
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 导入
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [Parameter]
 | 
			
		||||
    [EditorRequired]
 | 
			
		||||
    public Func<Dictionary<string, ImportPreviewOutputBase>, Task> Import { get; set; }
 | 
			
		||||
 | 
			
		||||
    [Inject]
 | 
			
		||||
    [NotNull]
 | 
			
		||||
    private IStringLocalizer<ImportExcelConfirm>? Localizer { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 预览
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [Parameter]
 | 
			
		||||
    [EditorRequired]
 | 
			
		||||
    public Func<IBrowserFile, Task<Dictionary<string, ImportPreviewOutputBase>>> Preview { get; set; }
 | 
			
		||||
 | 
			
		||||
    [Inject]
 | 
			
		||||
    [NotNull]
 | 
			
		||||
    private ToastService? ToastService { get; set; }
 | 
			
		||||
 | 
			
		||||
    [Required]
 | 
			
		||||
    private IBrowserFile _importFile { get; set; }
 | 
			
		||||
 | 
			
		||||
    [CascadingParameter]
 | 
			
		||||
    private Func<Task>? OnCloseAsync { get; set; }
 | 
			
		||||
 | 
			
		||||
    private async Task DeviceImport(IBrowserFile file)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            _importPreviews.Clear();
 | 
			
		||||
            _importPreviews = await Preview.Invoke(file);
 | 
			
		||||
            await step.Next();
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            await ToastService.Warn(ex);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task SaveDeviceImport()
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            await Task.Run(async () =>
 | 
			
		||||
            {
 | 
			
		||||
                await Import.Invoke(_importPreviews);
 | 
			
		||||
                _importFile = null;
 | 
			
		||||
 | 
			
		||||
                await InvokeAsync(async () =>
 | 
			
		||||
                {
 | 
			
		||||
                    if (OnCloseAsync != null)
 | 
			
		||||
                        await OnCloseAsync();
 | 
			
		||||
                    await ToastService.Default();
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            await InvokeAsync(async () => await ToastService.Warn(ex));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,9 @@
 | 
			
		||||
::deep .avatar {
 | 
			
		||||
    border-radius: 1.5rem;
 | 
			
		||||
    width: 24px;
 | 
			
		||||
    height: 24px;
 | 
			
		||||
    background-color: var(--bs-red);
 | 
			
		||||
    color: #fff;
 | 
			
		||||
    flex: 0 0 auto;
 | 
			
		||||
    font-size: 1rem;
 | 
			
		||||
}
 | 
			
		||||
@@ -10,7 +10,7 @@
 | 
			
		||||
 | 
			
		||||
using Microsoft.JSInterop;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Common.Extension;
 | 
			
		||||
namespace ThingsGateway.Razor.Extension;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// JSRuntime扩展方法
 | 
			
		||||
@@ -49,4 +49,28 @@ public static class JSRuntimeExtensions
 | 
			
		||||
        {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public static async ValueTask<T> GetLocalStorage<T>(this IJSRuntime jsRuntime, string name)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            return await jsRuntime.InvokeAsync<T>("getLocalStorage", name).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
        catch
 | 
			
		||||
        {
 | 
			
		||||
            return default;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static async ValueTask SetLocalStorage<T>(this IJSRuntime jsRuntime, string name, T data)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            await jsRuntime.InvokeVoidAsync("setLocalStorage", name, data).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
        catch
 | 
			
		||||
        {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -25,7 +25,8 @@
 | 
			
		||||
    "Success": "Success",
 | 
			
		||||
    "TablesExportButtonExcelText": "Export Excel",
 | 
			
		||||
    "TablesImportButtonExcelText": "Import Excel",
 | 
			
		||||
    "True": "Yes"
 | 
			
		||||
    "True": "Yes",
 | 
			
		||||
    "Info": "Info"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Razor.About": {
 | 
			
		||||
    "Community": "Community",
 | 
			
		||||
@@ -59,6 +60,19 @@
 | 
			
		||||
    "SearchText": "Search Page"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Razor.ImportExcel": {
 | 
			
		||||
    "First": "Step 1",
 | 
			
		||||
    "Import": "If there are no errors during verification, it will be directly imported into the database",
 | 
			
		||||
    "Next": "Next",
 | 
			
		||||
    "Reset": "Reset",
 | 
			
		||||
    "Second": "Step 2",
 | 
			
		||||
    "Third": "Step 3",
 | 
			
		||||
    "Tip": "When the data volume is large (more than 200,000), the import may take more than 1 minute, please be patient",
 | 
			
		||||
    "Upload": "Upload File",
 | 
			
		||||
    "UploadCount": " Table {0}, import {1} records",
 | 
			
		||||
    "Validate": "Validate",
 | 
			
		||||
    "ValidateText": "Validation Content"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Razor.ImportExcelConfirm": {
 | 
			
		||||
    "First": "Step 1",
 | 
			
		||||
    "Import": "Import",
 | 
			
		||||
    "Next": "Next",
 | 
			
		||||
@@ -67,7 +81,7 @@
 | 
			
		||||
    "Third": "Step 3",
 | 
			
		||||
    "Tip": "When the data volume is large (more than 200,000), the import may take more than 1 minute, please be patient",
 | 
			
		||||
    "Upload": "Upload File",
 | 
			
		||||
    "UploadCount": " Table {0}, expecting to import {1} records",
 | 
			
		||||
    "UploadCount": " Table {0}, import {1} records",
 | 
			
		||||
    "Validate": "Validate",
 | 
			
		||||
    "ValidateText": "Validation Content"
 | 
			
		||||
  },
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,8 @@
 | 
			
		||||
    "Success": "成功",
 | 
			
		||||
    "TablesExportButtonExcelText": "导出Excel",
 | 
			
		||||
    "TablesImportButtonExcelText": "导入Excel",
 | 
			
		||||
    "True": "是"
 | 
			
		||||
    "True": "是",
 | 
			
		||||
    "Info": "详情"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Razor.About": {
 | 
			
		||||
    "Community": "社区",
 | 
			
		||||
@@ -59,6 +60,19 @@
 | 
			
		||||
    "SearchText": "搜索页面"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Razor.ImportExcel": {
 | 
			
		||||
    "First": "第一步",
 | 
			
		||||
    "Import": "若验证无错误,将直接导入数据库",
 | 
			
		||||
    "Next": "下一步",
 | 
			
		||||
    "Reset": "重置",
 | 
			
		||||
    "Second": "第二步",
 | 
			
		||||
    "Third": "第三",
 | 
			
		||||
    "Tip": "数据量较大时(大于20万),所需导入时间可能超过1分钟,请耐心等待",
 | 
			
		||||
    "Upload": "上传文件",
 | 
			
		||||
    "UploadCount": " 表 {0},导入 {1} 条数据",
 | 
			
		||||
    "Validate": "验证",
 | 
			
		||||
    "ValidateText": "验证内容"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Razor.ImportExcelConfirm": {
 | 
			
		||||
    "First": "第一步",
 | 
			
		||||
    "Import": "导入",
 | 
			
		||||
    "Next": "下一步",
 | 
			
		||||
@@ -67,7 +81,7 @@
 | 
			
		||||
    "Third": "第三",
 | 
			
		||||
    "Tip": "数据量较大时(大于20万),所需导入时间可能超过1分钟,请耐心等待",
 | 
			
		||||
    "Upload": "上传文件",
 | 
			
		||||
    "UploadCount": " 表 {0},预计导入 {1} 条数据",
 | 
			
		||||
    "UploadCount": " 表 {0},导入 {1} 条数据",
 | 
			
		||||
    "Validate": "验证",
 | 
			
		||||
    "ValidateText": "验证内容"
 | 
			
		||||
  },
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@
 | 
			
		||||
	<Import Project="..\..\PackNuget.props" />
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<TargetFrameworks>net8.0</TargetFrameworks>
 | 
			
		||||
		
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
		<PackageReference Include="BootstrapBlazor.FontAwesome" Version="9.1.0" />
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +0,0 @@
 | 
			
		||||
// 设置 culture
 | 
			
		||||
function setCultureLocalStorage(culture) {
 | 
			
		||||
    localStorage.setItem("culture", culture);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 获取 culture
 | 
			
		||||
function getCultureLocalStorage() {
 | 
			
		||||
    return localStorage.getItem("culture");
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										18
									
								
								src/Admin/ThingsGateway.Razor/wwwroot/js/localStorageUtil.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/Admin/ThingsGateway.Razor/wwwroot/js/localStorageUtil.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
// 设置 culture
 | 
			
		||||
function setCultureLocalStorage(culture) {
 | 
			
		||||
    localStorage.setItem("culture", culture);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 获取 culture
 | 
			
		||||
function getCultureLocalStorage() {
 | 
			
		||||
    return localStorage.getItem("culture");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 function getLocalStorage(name) {
 | 
			
		||||
    return JSON.parse(localStorage.getItem(name)) ?? 0;
 | 
			
		||||
}
 | 
			
		||||
 function setLocalStorage(name, data) {
 | 
			
		||||
    if (localStorage) {
 | 
			
		||||
        localStorage.setItem(name, JSON.stringify(data));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -51,9 +51,14 @@ namespace ThingsGateway.SqlSugar
 | 
			
		||||
            // HTTP GET 请求执行SQL
 | 
			
		||||
            var result = string.Empty;
 | 
			
		||||
            var url = $"{this.url}/exec?query={HttpUtility.UrlEncode(sql)}";
 | 
			
		||||
 | 
			
		||||
            var request = new HttpRequestMessage(HttpMethod.Get, url);
 | 
			
		||||
            if (!string.IsNullOrWhiteSpace(authorization))
 | 
			
		||||
                client.DefaultRequestHeaders.Add("Authorization", authorization);
 | 
			
		||||
            var httpResponseMessage = await client.GetAsync(url).ConfigureAwait(false);
 | 
			
		||||
            {
 | 
			
		||||
                request.Headers.Authorization = AuthenticationHeaderValue.Parse(authorization);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            using var httpResponseMessage = await client.SendAsync(request).ConfigureAwait(false);
 | 
			
		||||
            result = await httpResponseMessage.Content.ReadAsStringAsync().ConfigureAwait(false);
 | 
			
		||||
            return result;
 | 
			
		||||
        }
 | 
			
		||||
@@ -127,35 +132,43 @@ namespace ThingsGateway.SqlSugar
 | 
			
		||||
            {
 | 
			
		||||
                // 准备多部分表单数据
 | 
			
		||||
                var boundary = "---------------" + DateTime.Now.Ticks.ToString("x");
 | 
			
		||||
                var list = new List<Hashtable>();
 | 
			
		||||
 | 
			
		||||
                tableName ??= db.EntityMaintenance.GetEntityInfo<T>().DbTableName;
 | 
			
		||||
 | 
			
		||||
                // 获取或创建列信息缓存
 | 
			
		||||
                var key = "QuestDbBulkCopy" + typeof(T).FullName + typeof(T).GetHashCode();
 | 
			
		||||
                var columns = ReflectionInoCacheService.Instance.GetOrCreate(key, () =>
 | 
			
		||||
                 db.CopyNew().DbMaintenance.GetColumnInfosByTableName(tableName));
 | 
			
		||||
 | 
			
		||||
                // 构建schema信息
 | 
			
		||||
                columns.ForEach(d =>
 | 
			
		||||
                var list = ReflectionInoCacheService.Instance.GetOrCreate($"{key}{dateFormat}List<Hashtable>", () =>
 | 
			
		||||
                {
 | 
			
		||||
                    if (d.DataType == "TIMESTAMP")
 | 
			
		||||
                    var list = new List<Hashtable>();
 | 
			
		||||
 | 
			
		||||
                    // 构建schema信息
 | 
			
		||||
                    columns.ForEach(d =>
 | 
			
		||||
                    {
 | 
			
		||||
                        list.Add(new Hashtable()
 | 
			
		||||
                        if (d.DataType == "TIMESTAMP")
 | 
			
		||||
                        {
 | 
			
		||||
                            list.Add(new Hashtable()
 | 
			
		||||
                        {
 | 
			
		||||
                            { "name", d.DbColumnName },
 | 
			
		||||
                            { "type", d.DataType },
 | 
			
		||||
                            { "pattern", dateFormat}
 | 
			
		||||
                        });
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        list.Add(new Hashtable()
 | 
			
		||||
                        }
 | 
			
		||||
                        else
 | 
			
		||||
                        {
 | 
			
		||||
                            list.Add(new Hashtable()
 | 
			
		||||
                        {
 | 
			
		||||
                            { "name", d.DbColumnName },
 | 
			
		||||
                            { "type", d.DataType }
 | 
			
		||||
                        });
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
                        }
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                    return list;
 | 
			
		||||
                }
 | 
			
		||||
 );
 | 
			
		||||
 | 
			
		||||
                var schema = JsonConvert.SerializeObject(list);
 | 
			
		||||
 | 
			
		||||
                // 写入CSV文件
 | 
			
		||||
@@ -184,8 +197,8 @@ namespace ThingsGateway.SqlSugar
 | 
			
		||||
                    "multipart/form-data; boundary=" + boundary);
 | 
			
		||||
 | 
			
		||||
                // 发送请求并处理响应
 | 
			
		||||
                var httpResponseMessage =
 | 
			
		||||
                    await Post(client, tableName, httpContent).ConfigureAwait(false);
 | 
			
		||||
                using var httpResponseMessage =
 | 
			
		||||
                      await Post(client, tableName, httpContent).ConfigureAwait(false);
 | 
			
		||||
                var readAsStringAsync = await httpResponseMessage.Content.ReadAsStringAsync().ConfigureAwait(false);
 | 
			
		||||
                var splitByLine = QuestDbRestAPHelper.SplitByLine(readAsStringAsync);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,6 @@
 | 
			
		||||
        V Get<V>(string key);
 | 
			
		||||
        IEnumerable<string> GetAllKey<V>();
 | 
			
		||||
        void Remove<V>(string key);
 | 
			
		||||
        V GetOrCreate<V>(string cacheKey, Func<V> create, int cacheDurationInSeconds = int.MaxValue);
 | 
			
		||||
        V GetOrCreate<V>(string cacheKey, Func<V> create, int cacheDurationInSeconds = 3600);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -31,7 +31,7 @@ namespace ThingsGateway.SqlSugar
 | 
			
		||||
            return ReflectionInoCore<V>.GetInstance().GetAllKey();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public V GetOrCreate<V>(string cacheKey, Func<V> create, int cacheDurationInSeconds = int.MaxValue)
 | 
			
		||||
        public V GetOrCreate<V>(string cacheKey, Func<V> create, int cacheDurationInSeconds = 3600)
 | 
			
		||||
        {
 | 
			
		||||
            return ReflectionInoCore<V>.GetInstance().GetOrCreate(cacheKey, create);
 | 
			
		||||
        }
 | 
			
		||||
@@ -43,10 +43,13 @@ namespace ThingsGateway.SqlSugar
 | 
			
		||||
    }
 | 
			
		||||
    public class ReflectionInoCore<V>
 | 
			
		||||
    {
 | 
			
		||||
        private MemoryCache InstanceCache => new MemoryCache() { Expire = 60 };
 | 
			
		||||
        private MemoryCache InstanceCache = new MemoryCache() { Expire = 180 };
 | 
			
		||||
        private static ReflectionInoCore<V> _instance = null;
 | 
			
		||||
        private static readonly object _instanceLock = new object();
 | 
			
		||||
        private ReflectionInoCore() { }
 | 
			
		||||
        private ReflectionInoCore()
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public V this[string key]
 | 
			
		||||
        {
 | 
			
		||||
@@ -107,10 +110,10 @@ namespace ThingsGateway.SqlSugar
 | 
			
		||||
            return this.InstanceCache.Keys;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public V GetOrCreate(string cacheKey, Func<V> create)
 | 
			
		||||
        public V GetOrCreate(string cacheKey, Func<V> create, int expire = 3600)
 | 
			
		||||
        {
 | 
			
		||||
            return InstanceCache.GetOrAdd<V>(cacheKey, (a) =>
 | 
			
		||||
            create());
 | 
			
		||||
            create(), expire);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    public static class ReflectionInoHelper
 | 
			
		||||
 
 | 
			
		||||
@@ -447,6 +447,28 @@ namespace ThingsGateway.SqlSugar
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public override List<DbColumnInfo> GetColumnInfosByTableName(string tableName, bool isCache = true)
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
            if (string.IsNullOrEmpty(tableName)) return new List<DbColumnInfo>();
 | 
			
		||||
            string cacheKey = "QuestDB.GetColumnInfosByTableName." + this.SqlBuilder.GetNoTranslationColumnName(tableName).ToLower() + this.Context.CurrentConnectionConfig.ConfigId;
 | 
			
		||||
            cacheKey = GetCacheKey(cacheKey);
 | 
			
		||||
 | 
			
		||||
            if (isCache)
 | 
			
		||||
            {
 | 
			
		||||
 | 
			
		||||
                return this.Context.Utilities.GetReflectionInoCacheInstance().GetOrCreate(cacheKey, () =>
 | 
			
		||||
                {
 | 
			
		||||
                    return GetColInfo(tableName);
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                return GetColInfo(tableName);
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private List<DbColumnInfo> GetColInfo(string tableName)
 | 
			
		||||
        {
 | 
			
		||||
            var sql = String.Format(GetColumnInfosByTableNameSql, tableName);
 | 
			
		||||
            List<DbColumnInfo> result = new List<DbColumnInfo>();
 | 
			
		||||
 
 | 
			
		||||
@@ -717,8 +717,32 @@ namespace ThingsGateway.SqlSugar
 | 
			
		||||
        /// <returns>列信息列表</returns>
 | 
			
		||||
        public override List<DbColumnInfo> GetColumnInfosByTableName(string tableName, bool isCache = true)
 | 
			
		||||
        {
 | 
			
		||||
            var sql = $"select * from {this.SqlBuilder.GetTranslationColumnName(tableName)} where 1=2 ";
 | 
			
		||||
 | 
			
		||||
            if (string.IsNullOrEmpty(tableName)) return new List<DbColumnInfo>();
 | 
			
		||||
            string cacheKey = "TDengine.GetColumnInfosByTableName." + this.SqlBuilder.GetNoTranslationColumnName(tableName).ToLower() + this.Context.CurrentConnectionConfig.ConfigId;
 | 
			
		||||
            cacheKey = GetCacheKey(cacheKey);
 | 
			
		||||
 | 
			
		||||
            if (isCache)
 | 
			
		||||
            {
 | 
			
		||||
 | 
			
		||||
                return this.Context.Utilities.GetReflectionInoCacheInstance().GetOrCreate(cacheKey, () =>
 | 
			
		||||
                     {
 | 
			
		||||
                         return GetColInfo(tableName);
 | 
			
		||||
                     });
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                return GetColInfo(tableName);
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private List<DbColumnInfo> GetColInfo(string tableName)
 | 
			
		||||
        {
 | 
			
		||||
            List<DbColumnInfo> result = new List<DbColumnInfo>();
 | 
			
		||||
 | 
			
		||||
            var sql = $"select * from {this.SqlBuilder.GetTranslationColumnName(tableName)} where 1=2 ";
 | 
			
		||||
            DataTable dt = null;
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@
 | 
			
		||||
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<GenerateDocumentationFile>True</GenerateDocumentationFile>
 | 
			
		||||
		
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
 | 
			
		||||
@@ -22,18 +23,18 @@
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
		<PackageReference Include="SqlSugarCore.Dm" Version="8.8.0" />
 | 
			
		||||
		<PackageReference Include="SqlSugarCore.Kdbndp" Version="9.3.7.728" />
 | 
			
		||||
		<PackageReference Include="SqlSugarCore.Dm" Version="8.8.1" />
 | 
			
		||||
		<PackageReference Include="SqlSugarCore.Kdbndp" Version="9.3.7.821" />
 | 
			
		||||
		<PackageReference Include="Microsoft.Data.Sqlite" Version="7.0.20" />
 | 
			
		||||
		<!--<PackageReference Include="Microsoft.Data.Sqlite" Version="$(NET9Version)" />-->
 | 
			
		||||
		<PackageReference Include="MySqlConnector" Version="2.4.0" />
 | 
			
		||||
		<PackageReference Include="Npgsql" Version="9.0.3" />
 | 
			
		||||
		<PackageReference Include="CsvHelper" Version="33.1.0" />
 | 
			
		||||
		<PackageReference Include="TDengine.Connector" Version="3.1.7" />
 | 
			
		||||
		<PackageReference Include="TDengine.Connector" Version="3.1.8" />
 | 
			
		||||
		<PackageReference Include="Oracle.ManagedDataAccess.Core" Version="23.9.1" />
 | 
			
		||||
		<PackageReference Include="Oscar.Data.SqlClient" Version="4.2.21" />
 | 
			
		||||
		<PackageReference Include="Oscar.Data.SqlClient" Version="4.2.23" />
 | 
			
		||||
		<PackageReference Include="System.Data.Common" Version="4.3.0" />
 | 
			
		||||
		<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.0" />
 | 
			
		||||
		<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.1" />
 | 
			
		||||
		<PackageReference Include="System.Reflection.Emit.Lightweight" Version="4.7.0" />
 | 
			
		||||
		<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
 | 
			
		||||
		<PackageReference Include="System.Formats.Asn1" Version="8.0.2" />
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,11 @@
 | 
			
		||||
<Project>
 | 
			
		||||
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<PluginVersion>10.10.7</PluginVersion>
 | 
			
		||||
		<ProPluginVersion>10.10.6</ProPluginVersion>
 | 
			
		||||
		<DefaultVersion>10.10.10</DefaultVersion>
 | 
			
		||||
		<AuthenticationVersion>10.10.1</AuthenticationVersion>
 | 
			
		||||
		<SourceGeneratorVersion>10.10.1</SourceGeneratorVersion>
 | 
			
		||||
		<PluginVersion>10.11.10</PluginVersion>
 | 
			
		||||
		<ProPluginVersion>10.11.10</ProPluginVersion>
 | 
			
		||||
		<DefaultVersion>10.11.10</DefaultVersion>
 | 
			
		||||
		<AuthenticationVersion>10.11.2</AuthenticationVersion>
 | 
			
		||||
		<SourceGeneratorVersion>10.11.2</SourceGeneratorVersion>
 | 
			
		||||
		<NET8Version>8.0.19</NET8Version>
 | 
			
		||||
		<NET9Version>9.0.8</NET9Version>
 | 
			
		||||
		<SatelliteResourceLanguages>zh-Hans;en-US</SatelliteResourceLanguages>
 | 
			
		||||
@@ -28,7 +28,8 @@
 | 
			
		||||
		<AnalysisModeStyle>None</AnalysisModeStyle>
 | 
			
		||||
 | 
			
		||||
		<NoWarn>
 | 
			
		||||
			CS8603;CS8618;CS1591;CS8625;CS8602;CS8604;CS8600;CS8601;CS8714;CS8619;CS8629;CS8765;CS8634;CS8621;CS8767;CS8633;CS8620;CS8610;CS8631;CS8605;CS8622;CS8613;NU5100;NU5104;NU1903;NU1902;CA1863;CA1812;CA1805;CA1515;CA1508;CA1819;CA1852;CA5394;CA1822;CA1815;CA1813;CA2000;CA5358;CA5384;CA5400;CA5401;CA1814;CA1835;CA5392;CA5350;CA2100;CA1848;CA1810;CA1513;CA5351;CA1510;CA1512;CA1823;NETSDK1206
 | 
			
		||||
			CS8603;CS8618;CS1591;CS8625;CS8602;CS8604;CS8600;CS8601;CS8714;CS8619;CS8629;CS8765;CS8634;CS8621;CS8767;CS8633;CS8620;CS8610;CS8631;CS8605;CS8622;CS8613;NU5100;NU5104;NU1903;NU1902;CA1863;CA1812;CA1805;CA1515;CA1508;CA1819;CA1852;CA5394;CA1822;CA1815;CA1813;CA2000;CA5358;CA5384;CA5400;CA5401;CA1814;CA1835;CA5392;CA5350;CA2100;CA1848;CA1810;CA1513;CA5351;CA1510;CA1512;CA1823;RCS1102;RCS1194;NETSDK1206
 | 
			
		||||
 | 
			
		||||
		</NoWarn>
 | 
			
		||||
		<TargetFrameworks>net8.0;</TargetFrameworks>
 | 
			
		||||
		<LangVersion>13.0</LangVersion>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3088
									
								
								src/Drivers/ThingsGateway.AllenBradley.deps.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3088
									
								
								src/Drivers/ThingsGateway.AllenBradley.deps.json
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								src/Drivers/ThingsGateway.AllenBradley.dll
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/Drivers/ThingsGateway.AllenBradley.dll
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							@@ -4,10 +4,11 @@
 | 
			
		||||
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<TargetFrameworks>netstandard2.0;</TargetFrameworks>
 | 
			
		||||
		
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
		<PackageReference Include="CS-Script" Version="4.10.1" />
 | 
			
		||||
		<PackageReference Include="CS-Script" Version="4.11.0" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
 
 | 
			
		||||
@@ -89,7 +89,7 @@ public partial class ChannelComponent : ComponentBase
 | 
			
		||||
                await Channel.SetupAsync(config);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await Channel.ConnectAsync(Channel.ChannelOptions.ConnectTimeout, default);
 | 
			
		||||
            await Channel.ConnectAsync(default);
 | 
			
		||||
 | 
			
		||||
            if (OnConnectClick.HasDelegate)
 | 
			
		||||
                await OnConnectClick.InvokeAsync(Channel);
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,7 @@
 | 
			
		||||
            <EditorItem @bind-Field=Model.EncodingName>
 | 
			
		||||
                <EditTemplate Context="value">
 | 
			
		||||
                    <div class="col-12 col-sm-4">
 | 
			
		||||
                        <Select @bind-Value=value.EncodingName Items="EncodingItems" />
 | 
			
		||||
                        <Select @bind-Value=value.EncodingName Items="EncodingItems" IsClearable/>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </EditTemplate>
 | 
			
		||||
            </EditorItem>
 | 
			
		||||
 
 | 
			
		||||
@@ -31,6 +31,6 @@ public partial class ConverterConfigComponent : ComponentBase
 | 
			
		||||
    protected override void OnInitialized()
 | 
			
		||||
    {
 | 
			
		||||
        BoolItems = LocalizerUtil.GetBoolItems(Model.GetType(), nameof(Model.VariableStringLength), true);
 | 
			
		||||
        EncodingItems = new List<SelectedItem>() { new SelectedItem("", "none") }.Concat(Encoding.GetEncodings().Select(a => new SelectedItem(a.CodePage.ToString(), a.DisplayName))).ToList();
 | 
			
		||||
        EncodingItems = Encoding.GetEncodings().Select(a => new SelectedItem(a.CodePage.ToString(), a.DisplayName)).ToList();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -95,7 +95,7 @@ public partial class LogConsole : IDisposable
 | 
			
		||||
 | 
			
		||||
            if (LogPath != null)
 | 
			
		||||
            {
 | 
			
		||||
                var files = await TextFileReadService.GetLogFiles(LogPath);
 | 
			
		||||
                var files = await TextFileReadService.GetLogFilesAsync(LogPath);
 | 
			
		||||
                if (!files.IsSuccess)
 | 
			
		||||
                {
 | 
			
		||||
                    Messages = new List<LogMessage>();
 | 
			
		||||
@@ -106,7 +106,7 @@ public partial class LogConsole : IDisposable
 | 
			
		||||
                    await Task.Run(async () =>
 | 
			
		||||
                    {
 | 
			
		||||
                        Stopwatch sw = Stopwatch.StartNew();
 | 
			
		||||
                        var result = await TextFileReadService.LastLogData(files.Content.FirstOrDefault());
 | 
			
		||||
                        var result = await TextFileReadService.LastLogDataAsync(files.Content.FirstOrDefault());
 | 
			
		||||
                        if (result.IsSuccess)
 | 
			
		||||
                        {
 | 
			
		||||
                            Messages = result.Content.Where(a => a.LogLevel >= LogLevel).Select(a => new LogMessage((int)a.LogLevel, $"{a.LogTime} - {a.Message}{(a.ExceptionString.IsNullOrWhiteSpace() ? null : $"{Environment.NewLine}{a.ExceptionString}")}")).ToList();
 | 
			
		||||
@@ -144,7 +144,7 @@ public partial class LogConsole : IDisposable
 | 
			
		||||
    {
 | 
			
		||||
        if (LogPath != null)
 | 
			
		||||
        {
 | 
			
		||||
            var files = await TextFileReadService.GetLogFiles(LogPath);
 | 
			
		||||
            var files = await TextFileReadService.GetLogFilesAsync(LogPath);
 | 
			
		||||
            if (files.IsSuccess)
 | 
			
		||||
            {
 | 
			
		||||
                foreach (var item in files.Content)
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Razor;
 | 
			
		||||
namespace ThingsGateway.Debug;
 | 
			
		||||
 | 
			
		||||
public class ValueTransformConfig
 | 
			
		||||
{
 | 
			
		||||
@@ -1,9 +1,5 @@
 | 
			
		||||
@namespace ThingsGateway.Gateway.Razor
 | 
			
		||||
@using ThingsGateway.Admin.Application
 | 
			
		||||
@using ThingsGateway.Admin.Razor
 | 
			
		||||
@namespace ThingsGateway.Debug
 | 
			
		||||
@using ThingsGateway.Foundation
 | 
			
		||||
@using ThingsGateway.Gateway.Application
 | 
			
		||||
@inherits ComponentDefault
 | 
			
		||||
 | 
			
		||||
<ValidateForm class="p-4 h-100" Model="@ValueTransformConfig" OnValidSubmit="OnSave">
 | 
			
		||||
    <EditorForm AutoGenerateAllItem="false" RowType=RowType.Inline ItemsPerRow=1 LabelWidth=150 Model="ValueTransformConfig">
 | 
			
		||||
@@ -14,7 +14,7 @@ using System.Text.RegularExpressions;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.NewLife.Extension;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Razor;
 | 
			
		||||
namespace ThingsGateway.Debug;
 | 
			
		||||
 | 
			
		||||
public partial class ValueTransformConfigPage
 | 
			
		||||
{
 | 
			
		||||
@@ -205,5 +205,10 @@ public partial class ValueTransformConfigPage
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Inject]
 | 
			
		||||
    ToastService ToastService { get; set; }
 | 
			
		||||
 | 
			
		||||
    [Inject]
 | 
			
		||||
    IStringLocalizer<ThingsGateway.Razor._Imports> RazorLocalizer { get; set; }
 | 
			
		||||
    #endregion 修改
 | 
			
		||||
}
 | 
			
		||||
@@ -1,4 +1,23 @@
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  "ThingsGateway.Debug.ValueTransformType": {
 | 
			
		||||
    "None": "None",
 | 
			
		||||
    "Linear": "Linear",
 | 
			
		||||
    "Sqrt": "Sqrt"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  "ThingsGateway.Debug.ValueTransformConfig": {
 | 
			
		||||
    "TransformType": "TransformType",
 | 
			
		||||
    "MinMax": "MinMax",
 | 
			
		||||
    "ClampToRawRange": "ClampToRawRange",
 | 
			
		||||
    "DecimalPlaces": "DecimalPlaces",
 | 
			
		||||
    "RawMin": "RawMin",
 | 
			
		||||
    "RawMax": "RawMax",
 | 
			
		||||
    "ActualMin": "ActualMin",
 | 
			
		||||
    "ActualMax": "ActualMax"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  "ThingsGateway.Debug.ChannelComponent": {
 | 
			
		||||
    "BaudRate": "Baud Rate",
 | 
			
		||||
    "BindUrl": "Local Bind IP Address",
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,21 @@
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
  "ThingsGateway.Debug.ValueTransformType": {
 | 
			
		||||
    "None": "无",
 | 
			
		||||
    "Linear": "线性",
 | 
			
		||||
    "Sqrt": "开方"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Debug.ValueTransformConfig": {
 | 
			
		||||
    "TransformType": "转换方式",
 | 
			
		||||
    "MinMax": "最小最大值",
 | 
			
		||||
    "ClampToRawRange": "限制范围",
 | 
			
		||||
    "DecimalPlaces": "保留小数位",
 | 
			
		||||
    "RawMin": "原始最小值",
 | 
			
		||||
    "RawMax": "原始最大值",
 | 
			
		||||
    "ActualMin": "实际最小值",
 | 
			
		||||
    "ActualMax": "实际最大值"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  "ThingsGateway.Debug.ChannelComponent": {
 | 
			
		||||
    "BaudRate": "波特率",
 | 
			
		||||
    "BindUrl": "本地url",
 | 
			
		||||
 
 | 
			
		||||
@@ -26,7 +26,7 @@ public class PlatformService : IPlatformService
 | 
			
		||||
 | 
			
		||||
    public async Task OnLogExport(string logPath)
 | 
			
		||||
    {
 | 
			
		||||
        var files = TextFileReader.GetLogFiles(logPath);
 | 
			
		||||
        var files = TextFileReader.GetLogFilesAsync(logPath);
 | 
			
		||||
        if (!files.IsSuccess)
 | 
			
		||||
        {
 | 
			
		||||
            return;
 | 
			
		||||
@@ -40,7 +40,7 @@ public class PlatformService : IPlatformService
 | 
			
		||||
            await using var jSObject = await JSRuntime.InvokeAsync<IJSObjectReference>("import", $"{WebsiteConst.DefaultResourceUrl}js/downloadFile.js");
 | 
			
		||||
            var path = Path.GetRelativePath("wwwroot", item);
 | 
			
		||||
            string fileName = DateTime.Now.ToFileDateTimeFormat();
 | 
			
		||||
            await jSObject.InvokeVoidAsync("blazor_downloadFile", url, fileName, new { FileName = path });
 | 
			
		||||
            await jSObject.InvokeAsync<bool>("blazor_downloadFile", url, fileName, new { FileName = path });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@
 | 
			
		||||
	<Import Project="..\..\PackNuget.props" />
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<TargetFrameworks>net8.0;</TargetFrameworks>
 | 
			
		||||
		
 | 
			
		||||
		<!--<UseRazorSourceGenerator>false</UseRazorSourceGenerator>-->
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@
 | 
			
		||||
		<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
 | 
			
		||||
		<NoPackageAnalysis>true</NoPackageAnalysis>
 | 
			
		||||
		<SignAssembly>false</SignAssembly>
 | 
			
		||||
		
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<TargetFrameworks>netstandard2.0;</TargetFrameworks>
 | 
			
		||||
		<Version>$(SourceGeneratorVersion)</Version>
 | 
			
		||||
		
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,8 @@
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using System.Buffers;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Foundation;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
@@ -16,12 +18,12 @@ namespace ThingsGateway.Foundation;
 | 
			
		||||
public abstract class DDPMessage : MessageBase, IResultMessage
 | 
			
		||||
{
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public override int HeaderLength => 4;
 | 
			
		||||
    public override long HeaderLength => 4;
 | 
			
		||||
    public byte Type = 0;
 | 
			
		||||
    public string Id;
 | 
			
		||||
    public override FilterResult CheckBody<TByteBlock>(ref TByteBlock byteBlock)
 | 
			
		||||
    {
 | 
			
		||||
        Id = byteBlock.ToString(byteBlock.Position, 11).Replace("\0", "");
 | 
			
		||||
        Id = byteBlock.ToString(byteBlock.BytesRead, 11).Replace("\0", "");
 | 
			
		||||
        OperCode = 0;
 | 
			
		||||
 | 
			
		||||
        Content = GetContent(ref byteBlock);
 | 
			
		||||
@@ -44,31 +46,31 @@ public abstract class DDPMessage : MessageBase, IResultMessage
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public abstract int GetBodyLength<TByteBlock>(ref TByteBlock byteBlock) where TByteBlock : IByteBlockReader;
 | 
			
		||||
    public abstract byte[] GetContent<TByteBlock>(ref TByteBlock byteBlock) where TByteBlock : IByteBlockReader;
 | 
			
		||||
    public abstract long GetBodyLength<TByteBlock>(ref TByteBlock byteBlock) where TByteBlock : IBytesReader;
 | 
			
		||||
    public abstract byte[] GetContent<TByteBlock>(ref TByteBlock byteBlock) where TByteBlock : IBytesReader;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class DDPTcpMessage : DDPMessage
 | 
			
		||||
{
 | 
			
		||||
    public override int GetBodyLength<TByteBlock>(ref TByteBlock byteBlock)
 | 
			
		||||
    public override long GetBodyLength<TByteBlock>(ref TByteBlock byteBlock)
 | 
			
		||||
    {
 | 
			
		||||
        return ReaderExtension.ReadValue<TByteBlock, ushort>(ref byteBlock, EndianType.Big) - 4;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override byte[] GetContent<TByteBlock>(ref TByteBlock byteBlock)
 | 
			
		||||
    {
 | 
			
		||||
        return byteBlock.Span.Slice(byteBlock.Position + 11, BodyLength - 12).ToArray();
 | 
			
		||||
        return byteBlock.TotalSequence.Slice(byteBlock.BytesRead + 11, BodyLength - 12).ToArray();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class DDPUdpMessage : DDPMessage
 | 
			
		||||
{
 | 
			
		||||
    public override int GetBodyLength<TByteBlock>(ref TByteBlock byteBlock)
 | 
			
		||||
    public override long GetBodyLength<TByteBlock>(ref TByteBlock byteBlock)
 | 
			
		||||
    {
 | 
			
		||||
        return byteBlock.Length - 4;
 | 
			
		||||
        return (byteBlock.BytesRead + byteBlock.BytesRemaining - 4);
 | 
			
		||||
    }
 | 
			
		||||
    public override byte[] GetContent<TByteBlock>(ref TByteBlock byteBlock)
 | 
			
		||||
    {
 | 
			
		||||
        return byteBlock.Span.Slice(byteBlock.Position + 12, BodyLength - 12).ToArray();
 | 
			
		||||
        return byteBlock.TotalSequence.Slice(byteBlock.BytesRead + 12, BodyLength - 12).ToArray();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -25,6 +25,7 @@ public class DDPSend : ISendMessage
 | 
			
		||||
    string Id;
 | 
			
		||||
    byte Command;
 | 
			
		||||
    bool Tcp;
 | 
			
		||||
 | 
			
		||||
    public DDPSend(ReadOnlyMemory<byte> readOnlyMemory, string id, bool tcp, byte command = 0x89)
 | 
			
		||||
    {
 | 
			
		||||
        Tcp = tcp;
 | 
			
		||||
@@ -32,7 +33,8 @@ public class DDPSend : ISendMessage
 | 
			
		||||
        Id = id;
 | 
			
		||||
        Command = command;
 | 
			
		||||
    }
 | 
			
		||||
    public void Build<TByteBlock>(ref TByteBlock byteBlock) where TByteBlock : IByteBlockWriter
 | 
			
		||||
 | 
			
		||||
    public void Build<TByteBlock>(ref TByteBlock byteBlock) where TByteBlock : IBytesWriter
 | 
			
		||||
    {
 | 
			
		||||
        WriterExtension.WriteValue(ref byteBlock, (byte)0x7b);
 | 
			
		||||
        WriterExtension.WriteValue(ref byteBlock, (byte)Command);
 | 
			
		||||
 
 | 
			
		||||
@@ -10,8 +10,6 @@
 | 
			
		||||
 | 
			
		||||
using System.Runtime.CompilerServices;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.NewLife;
 | 
			
		||||
 | 
			
		||||
using TouchSocket.Resources;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Foundation;
 | 
			
		||||
@@ -33,50 +31,120 @@ public class DDPTcpSessionClientChannel : TcpSessionClientChannel
 | 
			
		||||
            DDPAdapter.Config(Config);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 将当前实例的日志记录器和加载回调设置到适配器中
 | 
			
		||||
        DDPAdapter.Logger = Logger;
 | 
			
		||||
        DDPAdapter.OnLoaded(this);
 | 
			
		||||
 | 
			
		||||
        DDPAdapter.SendAsyncCallBack = DDPSendAsync;
 | 
			
		||||
        DDPAdapter.ReceivedAsyncCallBack = DDPHandleReceivedData;
 | 
			
		||||
        DataHandlingAdapter.SendAsyncCallBack = DefaultSendAsync;
 | 
			
		||||
        return base.OnTcpConnected(e);
 | 
			
		||||
    }
 | 
			
		||||
    protected Task DefaultSendAsync(ReadOnlyMemory<byte> memory, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        return DDPAdapter.SendInputAsync(new DDPSend(memory, Id, true), cancellationToken);
 | 
			
		||||
    }
 | 
			
		||||
    protected Task DDPSendAsync(ReadOnlyMemory<byte> memory, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        return base.ProtectedDefaultSendAsync(memory, cancellationToken);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private DDPMessage DDPMessage { get; set; }
 | 
			
		||||
    private Task DDPHandleReceivedData(IByteBlockReader byteBlock, IRequestInfo requestInfo)
 | 
			
		||||
    #region 发送
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 异步发送数据,通过适配器模式灵活处理数据发送。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="memory">待发送的只读字节内存块。</param>
 | 
			
		||||
    /// <param name="token">可取消令箭</param>
 | 
			
		||||
    /// <returns>一个异步任务,表示发送操作。</returns>
 | 
			
		||||
    protected virtual async Task NewProtectedSendAsync(ReadOnlyMemory<byte> memory, CancellationToken token)
 | 
			
		||||
    {
 | 
			
		||||
        if (requestInfo is DDPMessage dDPMessage)
 | 
			
		||||
            DDPMessage = dDPMessage;
 | 
			
		||||
        this.ThrowIfDisposed();
 | 
			
		||||
        this.ThrowIfClientNotConnected();
 | 
			
		||||
 | 
			
		||||
        return EasyTask.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private DeviceSingleStreamDataHandleAdapter<DDPTcpMessage> DDPAdapter = new();
 | 
			
		||||
    private WaitLock _waitLock = new(nameof(DDPTcpSessionClientChannel));
 | 
			
		||||
        if (!await this.OnTcpSending(memory).ConfigureAwait(false)) return;
 | 
			
		||||
 | 
			
		||||
    protected override async ValueTask<bool> OnTcpReceiving(IByteBlockReader byteBlock)
 | 
			
		||||
    {
 | 
			
		||||
        DDPMessage? message = null;
 | 
			
		||||
        var transport = this.Transport;
 | 
			
		||||
        var adapter = this.DataHandlingAdapter;
 | 
			
		||||
        var locker = transport.SemaphoreSlimForWriter;
 | 
			
		||||
 | 
			
		||||
        await locker.WaitAsync(token).ConfigureAwait(false);
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            await _waitLock.WaitAsync().ConfigureAwait(false);
 | 
			
		||||
            await DDPAdapter.ReceivedInputAsync(byteBlock).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            message = DDPMessage;
 | 
			
		||||
            DDPMessage = null;
 | 
			
		||||
            // 如果数据处理适配器未设置,则使用默认发送方式。
 | 
			
		||||
            if (adapter == null)
 | 
			
		||||
            {
 | 
			
		||||
                await transport.Output.WriteAsync(memory, token).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                var byteBlock = new ByteBlock(1024);
 | 
			
		||||
                var ddpSend = new DDPSend(memory, Id, true);
 | 
			
		||||
                ddpSend.Build(ref byteBlock);
 | 
			
		||||
                var newMemory = byteBlock.Memory;
 | 
			
		||||
                var writer = new PipeBytesWriter(transport.Output);
 | 
			
		||||
                adapter.SendInput(ref writer, in newMemory);
 | 
			
		||||
                await writer.FlushAsync(token).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            _waitLock.Release();
 | 
			
		||||
            locker.Release();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 异步发送请求信息的受保护方法。
 | 
			
		||||
    ///
 | 
			
		||||
    /// 此方法首先检查当前对象是否能够发送请求信息,如果不能,则抛出异常。
 | 
			
		||||
    /// 如果可以发送,它将使用数据处理适配器来异步发送输入请求。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="requestInfo">要发送的请求信息。</param>
 | 
			
		||||
    /// <param name="token">可取消令箭</param>
 | 
			
		||||
    /// <returns>返回一个任务,该任务代表异步操作的结果。</returns>
 | 
			
		||||
    protected virtual async Task NewProtectedSendAsync(IRequestInfo requestInfo, CancellationToken token)
 | 
			
		||||
    {
 | 
			
		||||
        // 检查是否具备发送请求的条件,如果不具备则抛出异常
 | 
			
		||||
        this.ThrowIfCannotSendRequestInfo();
 | 
			
		||||
 | 
			
		||||
        this.ThrowIfDisposed();
 | 
			
		||||
        this.ThrowIfClientNotConnected();
 | 
			
		||||
 | 
			
		||||
        var transport = this.Transport;
 | 
			
		||||
        var adapter = this.DataHandlingAdapter;
 | 
			
		||||
        var locker = transport.SemaphoreSlimForWriter;
 | 
			
		||||
 | 
			
		||||
        await locker.WaitAsync(token).ConfigureAwait(false);
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var byteBlock = new ByteBlock(1024);
 | 
			
		||||
            if (requestInfo is not IRequestInfoBuilder requestInfoBuilder)
 | 
			
		||||
            {
 | 
			
		||||
                throw new Exception();
 | 
			
		||||
            }
 | 
			
		||||
            requestInfoBuilder.Build(ref byteBlock);
 | 
			
		||||
            var ddpSend = new DDPSend(byteBlock.Memory, Id, true);
 | 
			
		||||
 | 
			
		||||
            var writer = new PipeBytesWriter(transport.Output);
 | 
			
		||||
            adapter.SendInput(ref writer, ddpSend);
 | 
			
		||||
            await writer.FlushAsync(token).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            locker.Release();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    #endregion 发送
 | 
			
		||||
    public override Task SendAsync(IRequestInfo requestInfo, CancellationToken token = default)
 | 
			
		||||
    {
 | 
			
		||||
        return NewProtectedSendAsync(requestInfo, token);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override Task SendAsync(ReadOnlyMemory<byte> memory, CancellationToken token = default)
 | 
			
		||||
    {
 | 
			
		||||
        return NewProtectedSendAsync(memory, token);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    private DeviceSingleStreamDataHandleAdapter<DDPTcpMessage> DDPAdapter = new();
 | 
			
		||||
 | 
			
		||||
    protected override async ValueTask<bool> OnTcpReceiving(IBytesReader byteBlock)
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
        if (DDPAdapter.TryParseRequest(ref byteBlock, out var message))
 | 
			
		||||
        {
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (message != null)
 | 
			
		||||
@@ -90,11 +158,11 @@ public class DDPTcpSessionClientChannel : TcpSessionClientChannel
 | 
			
		||||
 | 
			
		||||
                    if (this.DataHandlingAdapter == null)
 | 
			
		||||
                    {
 | 
			
		||||
                        await this.OnTcpReceived(new ReceivedDataEventArgs(reader, default)).ConfigureAwait(EasyTask.ContinueOnCapturedContext);
 | 
			
		||||
                        await this.OnTcpReceived(new ReceivedDataEventArgs(message.Content, default)).ConfigureAwait(false);
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        await this.DataHandlingAdapter.ReceivedInputAsync(reader).ConfigureAwait(EasyTask.ContinueOnCapturedContext);
 | 
			
		||||
                        await this.DataHandlingAdapter.ReceivedInputAsync(reader).ConfigureAwait(false);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return true;
 | 
			
		||||
@@ -127,16 +195,16 @@ public class DDPTcpSessionClientChannel : TcpSessionClientChannel
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        await ResetIdAsync(id).ConfigureAwait(false);
 | 
			
		||||
                        await ResetIdAsync(id, ClosedToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                        //发送成功
 | 
			
		||||
                        await DDPAdapter.SendInputAsync(new DDPSend(ReadOnlyMemory<byte>.Empty, id, true, 0x81), ClosedToken).ConfigureAwait(false);
 | 
			
		||||
                        await base.ProtectedSendAsync(new DDPSend(ReadOnlyMemory<byte>.Empty, id, true, 0x81), ClosedToken).ConfigureAwait(false);
 | 
			
		||||
                        if (log)
 | 
			
		||||
                            Logger?.Info(string.Format(AppResource.DtuConnected, Id));
 | 
			
		||||
                    }
 | 
			
		||||
                    else if (message.Type == 0x02)
 | 
			
		||||
                    {
 | 
			
		||||
                        await DDPAdapter.SendInputAsync(new DDPSend(ReadOnlyMemory<byte>.Empty, Id, true, 0x82), ClosedToken).ConfigureAwait(false);
 | 
			
		||||
                        await base.ProtectedSendAsync(new DDPSend(ReadOnlyMemory<byte>.Empty, Id, true, 0x82), ClosedToken).ConfigureAwait(false);
 | 
			
		||||
                        Logger?.Info(string.Format(AppResource.DtuDisconnecting, Id));
 | 
			
		||||
                        await Task.Delay(100).ConfigureAwait(false);
 | 
			
		||||
                        await this.CloseAsync().ConfigureAwait(false);
 | 
			
		||||
 
 | 
			
		||||
@@ -36,16 +36,12 @@ public class DDPUdpSessionChannel : UdpSessionChannel, IClientChannel, IDtuUdpSe
 | 
			
		||||
            DDPAdapter.Config(Config);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 将当前实例的日志记录器和加载回调设置到适配器中
 | 
			
		||||
        DDPAdapter.Logger = Logger;
 | 
			
		||||
 | 
			
		||||
        if (DDPAdapter.Owner != null)
 | 
			
		||||
        {
 | 
			
		||||
            DDPAdapter.OnLoaded(this);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        DDPAdapter.SendCallBackAsync = DDPSendAsync;
 | 
			
		||||
        DDPAdapter.ReceivedCallBack = DDPHandleReceivedData;
 | 
			
		||||
        DDPAdapter.SendCallBackAsync = base.ProtectedDefaultSendAsync;
 | 
			
		||||
        DataHandlingAdapter.SendCallBackAsync = DefaultSendAsync;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -62,22 +58,7 @@ public class DDPUdpSessionChannel : UdpSessionChannel, IClientChannel, IDtuUdpSe
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected Task DDPSendAsync(EndPoint endPoint, ReadOnlyMemory<byte> memory, CancellationToken token)
 | 
			
		||||
    {
 | 
			
		||||
        //获取endpoint
 | 
			
		||||
        return base.ProtectedDefaultSendAsync(endPoint, memory, token);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private ConcurrentDictionary<EndPoint, DDPMessage> DDPMessageDict { get; set; } = new();
 | 
			
		||||
    private Task DDPHandleReceivedData(EndPoint endPoint, IByteBlockReader byteBlock, IRequestInfo requestInfo)
 | 
			
		||||
    {
 | 
			
		||||
        if (requestInfo is DDPMessage dDPMessage)
 | 
			
		||||
        {
 | 
			
		||||
            DDPMessageDict.AddOrUpdate(endPoint, dDPMessage);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return EasyTask.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private DeviceUdpDataHandleAdapter<DDPUdpMessage> DDPAdapter = new();
 | 
			
		||||
 | 
			
		||||
@@ -98,27 +79,14 @@ public class DDPUdpSessionChannel : UdpSessionChannel, IClientChannel, IDtuUdpSe
 | 
			
		||||
        return base.StopAsync(token);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private ConcurrentDictionary<EndPoint, WaitLock> _waitLocks = new();
 | 
			
		||||
 | 
			
		||||
    protected override async ValueTask<bool> OnUdpReceiving(UdpReceiveingEventArgs e)
 | 
			
		||||
    {
 | 
			
		||||
        var byteBlock = e.ByteBlock;
 | 
			
		||||
        var byteBlock = e.Memory;
 | 
			
		||||
        var endPoint = e.EndPoint;
 | 
			
		||||
        DDPMessage? message = null;
 | 
			
		||||
        var waitLock = _waitLocks.GetOrAdd(endPoint, new WaitLock(nameof(DDPUdpSessionChannel)));
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            await waitLock.WaitAsync().ConfigureAwait(false);
 | 
			
		||||
            await DDPAdapter.ReceivedInput(endPoint, byteBlock).ConfigureAwait(EasyTask.ContinueOnCapturedContext);
 | 
			
		||||
 | 
			
		||||
            if (DDPMessageDict.TryGetValue(endPoint, out var dDPMessage))
 | 
			
		||||
                message = dDPMessage;
 | 
			
		||||
            DDPMessageDict.TryRemove(endPoint, out _);
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            waitLock.Release();
 | 
			
		||||
        }
 | 
			
		||||
        if (!DDPAdapter.TryParseRequest(endPoint, byteBlock, out var message))
 | 
			
		||||
            return true;
 | 
			
		||||
 | 
			
		||||
        if (message != null)
 | 
			
		||||
        {
 | 
			
		||||
@@ -127,15 +95,13 @@ public class DDPUdpSessionChannel : UdpSessionChannel, IClientChannel, IDtuUdpSe
 | 
			
		||||
                var id = $"ID={message.Id}";
 | 
			
		||||
                if (message.Type == 0x09)
 | 
			
		||||
                {
 | 
			
		||||
                    var reader = new ByteBlockReader(message.Content);
 | 
			
		||||
 | 
			
		||||
                    if (this.DataHandlingAdapter == null)
 | 
			
		||||
                    {
 | 
			
		||||
                        await this.OnUdpReceived(new UdpReceivedDataEventArgs(endPoint, reader, default)).ConfigureAwait(EasyTask.ContinueOnCapturedContext);
 | 
			
		||||
                        await this.OnUdpReceived(new UdpReceivedDataEventArgs(endPoint, message.Content, default)).ConfigureAwait(false);
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        await this.DataHandlingAdapter.ReceivedInput(endPoint, reader).ConfigureAwait(EasyTask.ContinueOnCapturedContext);
 | 
			
		||||
                        await this.DataHandlingAdapter.ReceivedInputAsync(endPoint, message.Content).ConfigureAwait(false);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return true;
 | 
			
		||||
 
 | 
			
		||||
@@ -8,8 +8,6 @@
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Foundation;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
@@ -59,15 +57,9 @@ public interface IChannel : ISetupConfigObject, IDisposable, IClosableClient, IC
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public ChannelEventHandler Stoping { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 主动请求时的等待池
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public ConcurrentDictionary<long, Func<IClientChannel, ReceivedDataEventArgs, bool, Task>> ChannelReceivedWaitDict { get; }
 | 
			
		||||
 | 
			
		||||
    void ResetSign(int minSign = 0, int maxSign = ushort.MaxValue);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -22,6 +22,8 @@ public interface IClientChannel : IChannel, ISender, IClient, IClientSender, IOn
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    DataHandlingAdapter ReadOnlyDataHandlingAdapter { get; }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 通道等待池
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -34,4 +36,6 @@ public interface IClientChannel : IChannel, ISender, IClient, IClientSender, IOn
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="adapter">适配器</param>
 | 
			
		||||
    void SetDataHandlingAdapter(DataHandlingAdapter adapter);
 | 
			
		||||
 | 
			
		||||
    void SetDataHandlingAdapterLogger(ILog log);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,8 +8,6 @@
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.NewLife;
 | 
			
		||||
 | 
			
		||||
using TouchSocket.SerialPorts;
 | 
			
		||||
@@ -31,13 +29,23 @@ public class OtherChannel : SetupConfigObject, IClientChannel
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override TouchSocketConfig Config => base.Config ?? ChannelOptions.Config;
 | 
			
		||||
    public void SetDataHandlingAdapterLogger(ILog log)
 | 
			
		||||
    {
 | 
			
		||||
        if (_deviceDataHandleAdapter == null && ReadOnlyDataHandlingAdapter is IDeviceDataHandleAdapter handleAdapter)
 | 
			
		||||
        {
 | 
			
		||||
            _deviceDataHandleAdapter = handleAdapter;
 | 
			
		||||
        }
 | 
			
		||||
        if (_deviceDataHandleAdapter != null)
 | 
			
		||||
        {
 | 
			
		||||
            _deviceDataHandleAdapter.Logger = log;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void ResetSign(int minSign = 0, int maxSign = ushort.MaxValue)
 | 
			
		||||
    {
 | 
			
		||||
        var pool = WaitHandlePool;
 | 
			
		||||
        WaitHandlePool = new WaitHandlePool<MessageBase>(minSign, maxSign);
 | 
			
		||||
        pool?.CancelAll();
 | 
			
		||||
        pool?.SafeDispose();
 | 
			
		||||
    }
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public ChannelReceivedEventHandler ChannelReceived { get; } = new();
 | 
			
		||||
@@ -65,22 +73,25 @@ public class OtherChannel : SetupConfigObject, IClientChannel
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 等待池
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public WaitHandlePool<MessageBase> WaitHandlePool { get; internal set; } = new();
 | 
			
		||||
    public WaitHandlePool<MessageBase> WaitHandlePool { get; internal set; } = new(0, ushort.MaxValue);
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public WaitLock WaitLock => ChannelOptions.WaitLock;
 | 
			
		||||
    public virtual WaitLock GetLock(string key) => WaitLock;
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public ConcurrentDictionary<long, Func<IClientChannel, ReceivedDataEventArgs, bool, Task>> ChannelReceivedWaitDict { get; } = new();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    //private readonly WaitLock _connectLock = new WaitLock();
 | 
			
		||||
 | 
			
		||||
    private IDeviceDataHandleAdapter _deviceDataHandleAdapter;
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public void SetDataHandlingAdapter(DataHandlingAdapter adapter)
 | 
			
		||||
    {
 | 
			
		||||
        if (adapter is SingleStreamDataHandlingAdapter singleStreamDataHandlingAdapter)
 | 
			
		||||
            SetAdapter(singleStreamDataHandlingAdapter);
 | 
			
		||||
        if (adapter is IDeviceDataHandleAdapter deviceDataHandleAdapter)
 | 
			
		||||
            _deviceDataHandleAdapter = deviceDataHandleAdapter;
 | 
			
		||||
    }
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 设置数据处理适配器。
 | 
			
		||||
@@ -104,20 +115,17 @@ public class OtherChannel : SetupConfigObject, IClientChannel
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 设置适配器的日志记录器和加载、接收数据的回调方法。
 | 
			
		||||
        adapter.Logger = Logger;
 | 
			
		||||
        adapter.OnLoaded(this);
 | 
			
		||||
        adapter.ReceivedAsyncCallBack = PrivateHandleReceivedData;
 | 
			
		||||
        //adapter.SendCallBack = this.ProtectedDefaultSend;
 | 
			
		||||
        adapter.SendAsyncCallBack = ProtectedDefaultSendAsync;
 | 
			
		||||
 | 
			
		||||
        // 将提供的适配器实例设置为当前实例的数据处理适配器。
 | 
			
		||||
        m_dataHandlingAdapter = adapter;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task PrivateHandleReceivedData(IByteBlockReader byteBlock, IRequestInfo requestInfo)
 | 
			
		||||
    private Task PrivateHandleReceivedData(ReadOnlyMemory<byte> byteBlock, IRequestInfo requestInfo)
 | 
			
		||||
    {
 | 
			
		||||
        LastReceivedTime = DateTime.Now;
 | 
			
		||||
        await this.OnChannelReceivedEvent(new ReceivedDataEventArgs(byteBlock, requestInfo), ChannelReceived).ConfigureAwait(false);
 | 
			
		||||
        return this.OnChannelReceivedEvent(new ReceivedDataEventArgs(byteBlock, requestInfo), ChannelReceived);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -154,7 +162,8 @@ public class OtherChannel : SetupConfigObject, IClientChannel
 | 
			
		||||
        return Task.FromResult(Result.Success);
 | 
			
		||||
    }
 | 
			
		||||
    public volatile bool online;
 | 
			
		||||
    public Task ConnectAsync(int millisecondsTimeout, CancellationToken token)
 | 
			
		||||
 | 
			
		||||
    public Task ConnectAsync(CancellationToken token)
 | 
			
		||||
    {
 | 
			
		||||
        var cts = m_transport;
 | 
			
		||||
        m_transport = new();
 | 
			
		||||
@@ -180,8 +189,11 @@ public class OtherChannel : SetupConfigObject, IClientChannel
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            // 否则,使用适配器的发送方法进行数据发送。
 | 
			
		||||
            return m_dataHandlingAdapter.SendInputAsync(memory, cancellationToken);
 | 
			
		||||
            var byteBlock = new ByteBlock(1024);
 | 
			
		||||
            m_dataHandlingAdapter.SendInput(ref byteBlock, memory);
 | 
			
		||||
 | 
			
		||||
            byteBlock.SafeDispose();
 | 
			
		||||
            return EasyTask.CompletedTask;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -190,9 +202,14 @@ public class OtherChannel : SetupConfigObject, IClientChannel
 | 
			
		||||
        // 检查是否具备发送请求的条件,如果不具备则抛出异常
 | 
			
		||||
        ThrowIfCannotSendRequestInfo();
 | 
			
		||||
 | 
			
		||||
        // 使用数据处理适配器异步发送输入请求
 | 
			
		||||
        return m_dataHandlingAdapter.SendInputAsync(requestInfo, cancellationToken);
 | 
			
		||||
        var byteBlock = new ByteBlock(1024);
 | 
			
		||||
        m_dataHandlingAdapter.SendInput(ref byteBlock, requestInfo);
 | 
			
		||||
 | 
			
		||||
        byteBlock.SafeDispose();
 | 
			
		||||
        return EasyTask.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    private void ThrowIfCannotSendRequestInfo()
 | 
			
		||||
    {
 | 
			
		||||
        if (m_dataHandlingAdapter?.CanSendRequestInfo != true)
 | 
			
		||||
@@ -200,4 +217,6 @@ public class OtherChannel : SetupConfigObject, IClientChannel
 | 
			
		||||
            throw new NotSupportedException($"当前适配器为空或者不支持对象发送。");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -59,19 +59,18 @@ public class DtuPlugin : PluginBase, ITcpReceivingPlugin
 | 
			
		||||
    public bool DtuIdHex { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public async Task OnTcpReceiving(ITcpSession client, ByteBlockEventArgs e)
 | 
			
		||||
    public async Task OnTcpReceiving(ITcpSession client, BytesReaderEventArgs e)
 | 
			
		||||
    {
 | 
			
		||||
        var len = HeartbeatByte.Length;
 | 
			
		||||
        if (client is TcpSessionClientChannel socket && socket.Service is ITcpServiceChannel tcpServiceChannel)
 | 
			
		||||
        {
 | 
			
		||||
            if (!socket.Id.StartsWith("ID="))
 | 
			
		||||
            {
 | 
			
		||||
                var id = DtuIdHex ? $"ID={e.ByteBlock.Span.ToHexString()}" : $"ID={e.ByteBlock.ToString(0, e.ByteBlock.Length)}";
 | 
			
		||||
                var id = DtuIdHex ? $"ID={e.Reader.ToHexString()}" : $"ID={e.Reader.ToString()}";
 | 
			
		||||
                if (tcpServiceChannel.TryGetClient(id, out var oldClient))
 | 
			
		||||
                {
 | 
			
		||||
                    try
 | 
			
		||||
                    {
 | 
			
		||||
                        //await oldClient.ShutdownAsync(System.Net.Sockets.SocketShutdown.Both).ConfigureAwait(false);
 | 
			
		||||
                        await oldClient.CloseAsync().ConfigureAwait(false);
 | 
			
		||||
                        oldClient.Dispose();
 | 
			
		||||
                    }
 | 
			
		||||
@@ -79,7 +78,7 @@ public class DtuPlugin : PluginBase, ITcpReceivingPlugin
 | 
			
		||||
                    {
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                await socket.ResetIdAsync(id).ConfigureAwait(false);
 | 
			
		||||
                await socket.ResetIdAsync(id, client.ClosedToken).ConfigureAwait(false);
 | 
			
		||||
                client.Logger?.Info(string.Format(AppResource.DtuConnected, id));
 | 
			
		||||
                e.Handled = true;
 | 
			
		||||
            }
 | 
			
		||||
@@ -88,7 +87,6 @@ public class DtuPlugin : PluginBase, ITcpReceivingPlugin
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    //await socket.ShutdownAsync(System.Net.Sockets.SocketShutdown.Both).ConfigureAwait(false);
 | 
			
		||||
                    await socket.CloseAsync().ConfigureAwait(false);
 | 
			
		||||
                    socket.Dispose();
 | 
			
		||||
                }
 | 
			
		||||
@@ -102,11 +100,11 @@ public class DtuPlugin : PluginBase, ITcpReceivingPlugin
 | 
			
		||||
 | 
			
		||||
            if (len > 0)
 | 
			
		||||
            {
 | 
			
		||||
                if (HeartbeatByte.Span.SequenceEqual(e.ByteBlock.Memory.Slice(0, len).Span))
 | 
			
		||||
                if (HeartbeatByte.Span.SequenceEqual(e.Reader.TotalSequence.Slice(0, len).First.Span))
 | 
			
		||||
                {
 | 
			
		||||
                    if (DateTimeOffset.Now - socket.LastSentTime < TimeSpan.FromMilliseconds(200))
 | 
			
		||||
                    {
 | 
			
		||||
                        await Task.Delay(200).ConfigureAwait(false);
 | 
			
		||||
                        await Task.Delay(200, client.ClosedToken).ConfigureAwait(false);
 | 
			
		||||
                    }
 | 
			
		||||
                    //回应心跳包
 | 
			
		||||
                    await socket.SendAsync(HeartbeatByte, socket.ClosedToken).ConfigureAwait(false);
 | 
			
		||||
@@ -118,4 +116,6 @@ public class DtuPlugin : PluginBase, ITcpReceivingPlugin
 | 
			
		||||
        }
 | 
			
		||||
        await e.InvokeNext().ConfigureAwait(false);//如果本插件无法处理当前数据,请将数据转至下一个插件。
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -105,7 +105,7 @@ internal sealed class HeartbeatAndReceivePlugin : PluginBase, ITcpConnectedPlugi
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    private Task Task;
 | 
			
		||||
    private Task _task;
 | 
			
		||||
    private bool SendHeartbeat;
 | 
			
		||||
    public int HeartbeatTime { get; set; } = 3000;
 | 
			
		||||
 | 
			
		||||
@@ -125,17 +125,17 @@ internal sealed class HeartbeatAndReceivePlugin : PluginBase, ITcpConnectedPlugi
 | 
			
		||||
        {
 | 
			
		||||
            await tcpClient.SendAsync(DtuIdByte, tcpClient.ClosedToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            if (Task == null)
 | 
			
		||||
            if (_task == null)
 | 
			
		||||
            {
 | 
			
		||||
                Task = Task.Factory.StartNew(async () =>
 | 
			
		||||
                _task = Task.Run(async () =>
 | 
			
		||||
                 {
 | 
			
		||||
                     var failedCount = 0;
 | 
			
		||||
                     while (SendHeartbeat)
 | 
			
		||||
                     {
 | 
			
		||||
                         await Task.Delay(HeartbeatTime).ConfigureAwait(false);
 | 
			
		||||
                         await Task.Delay(HeartbeatTime, client.ClosedToken).ConfigureAwait(false);
 | 
			
		||||
                         if (!client.Online)
 | 
			
		||||
                         {
 | 
			
		||||
                             continue;
 | 
			
		||||
                             break;
 | 
			
		||||
                         }
 | 
			
		||||
 | 
			
		||||
                         try
 | 
			
		||||
@@ -159,15 +159,15 @@ internal sealed class HeartbeatAndReceivePlugin : PluginBase, ITcpConnectedPlugi
 | 
			
		||||
                         }
 | 
			
		||||
                     }
 | 
			
		||||
 | 
			
		||||
                     Task = null;
 | 
			
		||||
                 }, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default);
 | 
			
		||||
                     _task = null;
 | 
			
		||||
                 });
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await e.InvokeNext().ConfigureAwait(false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task OnTcpReceiving(ITcpSession client, ByteBlockEventArgs e)
 | 
			
		||||
    public async Task OnTcpReceiving(ITcpSession client, BytesReaderEventArgs e)
 | 
			
		||||
    {
 | 
			
		||||
        if (client is ITcpSessionClient)
 | 
			
		||||
        {
 | 
			
		||||
@@ -181,7 +181,7 @@ internal sealed class HeartbeatAndReceivePlugin : PluginBase, ITcpConnectedPlugi
 | 
			
		||||
            var len = HeartbeatByte.Length;
 | 
			
		||||
            if (len > 0)
 | 
			
		||||
            {
 | 
			
		||||
                if (HeartbeatByte.Span.SequenceEqual(e.ByteBlock.Memory.Slice(0, len).Span))
 | 
			
		||||
                if (HeartbeatByte.Span.SequenceEqual(e.Reader.TotalSequence.Slice(0, len).First.Span))
 | 
			
		||||
                {
 | 
			
		||||
                    e.Handled = true;
 | 
			
		||||
                }
 | 
			
		||||
@@ -190,6 +190,7 @@ internal sealed class HeartbeatAndReceivePlugin : PluginBase, ITcpConnectedPlugi
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public Task OnTcpClosed(ITcpSession client, ClosedEventArgs e)
 | 
			
		||||
    {
 | 
			
		||||
        SendHeartbeat = false;
 | 
			
		||||
 
 | 
			
		||||
@@ -77,10 +77,9 @@ public static class PluginUtil
 | 
			
		||||
                a.UseTcpSessionCheckClear()
 | 
			
		||||
        .SetCheckClearType(CheckClearType.All)
 | 
			
		||||
        .SetTick(TimeSpan.FromMilliseconds(channelOptions.CheckClearTime))
 | 
			
		||||
        .SetOnClose(async (c, t) =>
 | 
			
		||||
        .SetOnClose((c, t) =>
 | 
			
		||||
        {
 | 
			
		||||
            //await c.ShutdownAsync(System.Net.Sockets.SocketShutdown.Both).ConfigureAwait(false);
 | 
			
		||||
            await c.CloseAsync($"{channelOptions.CheckClearTime}ms Timeout").ConfigureAwait(false);
 | 
			
		||||
            return c.CloseAsync($"{channelOptions.CheckClearTime}ms Timeout");
 | 
			
		||||
        });
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -8,8 +8,6 @@
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.NewLife;
 | 
			
		||||
 | 
			
		||||
using TouchSocket.SerialPorts;
 | 
			
		||||
@@ -34,7 +32,6 @@ public class SerialPortChannel : SerialPortClient, IClientChannel
 | 
			
		||||
        var pool = WaitHandlePool;
 | 
			
		||||
        WaitHandlePool = new WaitHandlePool<MessageBase>(minSign, maxSign);
 | 
			
		||||
        pool?.CancelAll();
 | 
			
		||||
        pool?.SafeDispose();
 | 
			
		||||
    }
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public ChannelReceivedEventHandler ChannelReceived { get; } = new();
 | 
			
		||||
@@ -50,6 +47,26 @@ public class SerialPortChannel : SerialPortClient, IClientChannel
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public DataHandlingAdapter ReadOnlyDataHandlingAdapter => ProtectedDataHandlingAdapter;
 | 
			
		||||
    private IDeviceDataHandleAdapter _deviceDataHandleAdapter;
 | 
			
		||||
    public void SetDataHandlingAdapterLogger(ILog log)
 | 
			
		||||
    {
 | 
			
		||||
        if (_deviceDataHandleAdapter == null && ProtectedDataHandlingAdapter is IDeviceDataHandleAdapter handleAdapter)
 | 
			
		||||
        {
 | 
			
		||||
            _deviceDataHandleAdapter = handleAdapter;
 | 
			
		||||
        }
 | 
			
		||||
        if (_deviceDataHandleAdapter != null)
 | 
			
		||||
        {
 | 
			
		||||
            _deviceDataHandleAdapter.Logger = log;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public void SetDataHandlingAdapter(DataHandlingAdapter adapter)
 | 
			
		||||
    {
 | 
			
		||||
        if (adapter is SingleStreamDataHandlingAdapter singleStreamDataHandlingAdapter)
 | 
			
		||||
            SetAdapter(singleStreamDataHandlingAdapter);
 | 
			
		||||
        if (adapter is IDeviceDataHandleAdapter deviceDataHandleAdapter)
 | 
			
		||||
            _deviceDataHandleAdapter = deviceDataHandleAdapter;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public ChannelEventHandler Started { get; } = new();
 | 
			
		||||
@@ -65,14 +82,13 @@ public class SerialPortChannel : SerialPortClient, IClientChannel
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 等待池
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public WaitHandlePool<MessageBase> WaitHandlePool { get; internal set; } = new();
 | 
			
		||||
    public WaitHandlePool<MessageBase> WaitHandlePool { get; internal set; } = new(0, ushort.MaxValue);
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public WaitLock WaitLock => ChannelOptions.WaitLock;
 | 
			
		||||
    public virtual WaitLock GetLock(string key) => WaitLock;
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public ConcurrentDictionary<long, Func<IClientChannel, ReceivedDataEventArgs, bool, Task>> ChannelReceivedWaitDict { get; } = new();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    //private readonly WaitLock _connectLock = new WaitLock();
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
@@ -86,6 +102,7 @@ public class SerialPortChannel : SerialPortClient, IClientChannel
 | 
			
		||||
                if (Online)
 | 
			
		||||
                {
 | 
			
		||||
                    PortName = null;
 | 
			
		||||
                    await this.OnChannelEvent(Stoping).ConfigureAwait(false);
 | 
			
		||||
                    var result = await base.CloseAsync(msg, token).ConfigureAwait(false);
 | 
			
		||||
                    if (!Online)
 | 
			
		||||
                    {
 | 
			
		||||
@@ -103,7 +120,7 @@ public class SerialPortChannel : SerialPortClient, IClientChannel
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public override async Task ConnectAsync(int millisecondsTimeout, CancellationToken token)
 | 
			
		||||
    public override async Task ConnectAsync(CancellationToken token)
 | 
			
		||||
    {
 | 
			
		||||
        if (!Online)
 | 
			
		||||
        {
 | 
			
		||||
@@ -119,7 +136,8 @@ public class SerialPortChannel : SerialPortClient, IClientChannel
 | 
			
		||||
                    if (port != null)
 | 
			
		||||
                        PortName = $"{port.PortName}";
 | 
			
		||||
 | 
			
		||||
                    await base.ConnectAsync(millisecondsTimeout, token).ConfigureAwait(false);
 | 
			
		||||
                    await this.OnChannelEvent(Starting).ConfigureAwait(false);
 | 
			
		||||
                    await base.ConnectAsync(token).ConfigureAwait(false);
 | 
			
		||||
                    if (Online)
 | 
			
		||||
                    {
 | 
			
		||||
                        if (token.IsCancellationRequested) return;
 | 
			
		||||
@@ -134,12 +152,7 @@ public class SerialPortChannel : SerialPortClient, IClientChannel
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public void SetDataHandlingAdapter(DataHandlingAdapter adapter)
 | 
			
		||||
    {
 | 
			
		||||
        if (adapter is SingleStreamDataHandlingAdapter singleStreamDataHandlingAdapter)
 | 
			
		||||
            SetAdapter(singleStreamDataHandlingAdapter);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private string PortName { get; set; }
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public override string? ToString()
 | 
			
		||||
@@ -153,54 +166,43 @@ public class SerialPortChannel : SerialPortClient, IClientChannel
 | 
			
		||||
        return base.ToString();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected override async Task OnSerialClosed(ClosedEventArgs e)
 | 
			
		||||
    protected override Task OnSerialClosed(ClosedEventArgs e)
 | 
			
		||||
    {
 | 
			
		||||
        Logger?.Info($"{ToString()} Closed{(e.Message.IsNullOrEmpty() ? string.Empty : $" -{e.Message}")}");
 | 
			
		||||
 | 
			
		||||
        await base.OnSerialClosed(e).ConfigureAwait(false);
 | 
			
		||||
        return base.OnSerialClosed(e);
 | 
			
		||||
    }
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    protected override async Task OnSerialClosing(ClosingEventArgs e)
 | 
			
		||||
    protected override Task OnSerialClosing(ClosingEventArgs e)
 | 
			
		||||
    {
 | 
			
		||||
        await this.OnChannelEvent(Stoping).ConfigureAwait(false);
 | 
			
		||||
        Logger?.Trace($"{ToString()} Closing{(e.Message.IsNullOrEmpty() ? string.Empty : $" -{e.Message}")}");
 | 
			
		||||
        await base.OnSerialClosing(e).ConfigureAwait(false);
 | 
			
		||||
        return base.OnSerialClosing(e);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    protected override async Task OnSerialConnecting(ConnectingEventArgs e)
 | 
			
		||||
    protected override Task OnSerialConnecting(ConnectingEventArgs e)
 | 
			
		||||
    {
 | 
			
		||||
        Logger?.Trace($"{ToString()}  Connecting{(e.Message.IsNullOrEmpty() ? string.Empty : $" -{e.Message}")}");
 | 
			
		||||
        await this.OnChannelEvent(Starting).ConfigureAwait(false);
 | 
			
		||||
        await base.OnSerialConnecting(e).ConfigureAwait(false);
 | 
			
		||||
        return base.OnSerialConnecting(e);
 | 
			
		||||
    }
 | 
			
		||||
    protected override async Task OnSerialConnected(ConnectedEventArgs e)
 | 
			
		||||
    protected override Task OnSerialConnected(ConnectedEventArgs e)
 | 
			
		||||
    {
 | 
			
		||||
        Logger?.Debug($"{ToString()} Connected");
 | 
			
		||||
        await base.OnSerialConnected(e).ConfigureAwait(false);
 | 
			
		||||
        return base.OnSerialConnected(e);
 | 
			
		||||
    }
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    protected override async Task OnSerialReceived(ReceivedDataEventArgs e)
 | 
			
		||||
    {
 | 
			
		||||
        await base.OnSerialReceived(e).ConfigureAwait(false);
 | 
			
		||||
        if (e.RequestInfo is MessageBase response)
 | 
			
		||||
        {
 | 
			
		||||
            if (ChannelReceivedWaitDict.TryRemove(response.Sign, out var func))
 | 
			
		||||
            {
 | 
			
		||||
                await func.Invoke(this, e, ChannelReceived.Count == 1).ConfigureAwait(false);
 | 
			
		||||
                e.Handled = true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (e.Handled)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        await this.OnChannelReceivedEvent(e, ChannelReceived).ConfigureAwait(false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    protected override void SafetyDispose(bool disposing)
 | 
			
		||||
    {
 | 
			
		||||
        WaitHandlePool.SafeDispose();
 | 
			
		||||
        WaitHandlePool?.CancelAll();
 | 
			
		||||
        base.SafetyDispose(disposing);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,8 +8,6 @@
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.NewLife;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Foundation;
 | 
			
		||||
@@ -31,9 +29,27 @@ public class TcpClientChannel : TcpClient, IClientChannel
 | 
			
		||||
        var pool = WaitHandlePool;
 | 
			
		||||
        WaitHandlePool = new WaitHandlePool<MessageBase>(minSign, maxSign);
 | 
			
		||||
        pool?.CancelAll();
 | 
			
		||||
        pool?.SafeDispose();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private IDeviceDataHandleAdapter _deviceDataHandleAdapter;
 | 
			
		||||
    public void SetDataHandlingAdapterLogger(ILog log)
 | 
			
		||||
    {
 | 
			
		||||
        if (_deviceDataHandleAdapter == null && DataHandlingAdapter is IDeviceDataHandleAdapter handleAdapter)
 | 
			
		||||
        {
 | 
			
		||||
            _deviceDataHandleAdapter = handleAdapter;
 | 
			
		||||
        }
 | 
			
		||||
        if (_deviceDataHandleAdapter != null)
 | 
			
		||||
        {
 | 
			
		||||
            _deviceDataHandleAdapter.Logger = log;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public void SetDataHandlingAdapter(DataHandlingAdapter adapter)
 | 
			
		||||
    {
 | 
			
		||||
        if (adapter is SingleStreamDataHandlingAdapter singleStreamDataHandlingAdapter)
 | 
			
		||||
            SetAdapter(singleStreamDataHandlingAdapter);
 | 
			
		||||
        if (adapter is IDeviceDataHandleAdapter deviceDataHandleAdapter)
 | 
			
		||||
            _deviceDataHandleAdapter = deviceDataHandleAdapter;
 | 
			
		||||
    }
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public ChannelReceivedEventHandler ChannelReceived { get; } = new();
 | 
			
		||||
 | 
			
		||||
@@ -62,14 +78,13 @@ public class TcpClientChannel : TcpClient, IClientChannel
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 等待池
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public WaitHandlePool<MessageBase> WaitHandlePool { get; internal set; } = new();
 | 
			
		||||
    public WaitHandlePool<MessageBase> WaitHandlePool { get; internal set; } = new(0, ushort.MaxValue);
 | 
			
		||||
    public virtual WaitLock GetLock(string key) => WaitLock;
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public WaitLock WaitLock => ChannelOptions.WaitLock;
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public ConcurrentDictionary<long, Func<IClientChannel, ReceivedDataEventArgs, bool, Task>> ChannelReceivedWaitDict { get; } = new();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    //private readonly WaitLock _connectLock = new WaitLock();
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
@@ -82,6 +97,7 @@ public class TcpClientChannel : TcpClient, IClientChannel
 | 
			
		||||
                //await _connectLock.WaitAsync().ConfigureAwait(false);
 | 
			
		||||
                if (Online)
 | 
			
		||||
                {
 | 
			
		||||
                    await this.OnChannelEvent(Stoping).ConfigureAwait(false);
 | 
			
		||||
                    var result = await base.CloseAsync(msg, token).ConfigureAwait(false);
 | 
			
		||||
                    if (!Online)
 | 
			
		||||
                    {
 | 
			
		||||
@@ -99,7 +115,7 @@ public class TcpClientChannel : TcpClient, IClientChannel
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public override async Task ConnectAsync(int millisecondsTimeout, CancellationToken token)
 | 
			
		||||
    public override async Task ConnectAsync(CancellationToken token)
 | 
			
		||||
    {
 | 
			
		||||
        if (!Online)
 | 
			
		||||
        {
 | 
			
		||||
@@ -109,7 +125,8 @@ public class TcpClientChannel : TcpClient, IClientChannel
 | 
			
		||||
                if (!Online)
 | 
			
		||||
                {
 | 
			
		||||
                    if (token.IsCancellationRequested) return;
 | 
			
		||||
                    await base.ConnectAsync(millisecondsTimeout, token).ConfigureAwait(false);
 | 
			
		||||
                    await this.OnChannelEvent(Starting).ConfigureAwait(false);
 | 
			
		||||
                    await base.ConnectAsync(token).ConfigureAwait(false);
 | 
			
		||||
                    if (Online)
 | 
			
		||||
                    {
 | 
			
		||||
                        if (token.IsCancellationRequested) return;
 | 
			
		||||
@@ -124,12 +141,6 @@ public class TcpClientChannel : TcpClient, IClientChannel
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public void SetDataHandlingAdapter(DataHandlingAdapter adapter)
 | 
			
		||||
    {
 | 
			
		||||
        if (adapter is SingleStreamDataHandlingAdapter singleStreamDataHandlingAdapter)
 | 
			
		||||
            SetAdapter(singleStreamDataHandlingAdapter);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public override string ToString()
 | 
			
		||||
@@ -137,48 +148,39 @@ public class TcpClientChannel : TcpClient, IClientChannel
 | 
			
		||||
        return $"{IP}:{Port}";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected override async Task OnTcpClosed(ClosedEventArgs e)
 | 
			
		||||
    protected override Task OnTcpClosed(ClosedEventArgs e)
 | 
			
		||||
    {
 | 
			
		||||
        Logger?.Info($"{ToString()}  Closed{(e.Message.IsNullOrEmpty() ? string.Empty : $" -{e.Message}")}");
 | 
			
		||||
 | 
			
		||||
        await base.OnTcpClosed(e).ConfigureAwait(false);
 | 
			
		||||
        return base.OnTcpClosed(e);
 | 
			
		||||
    }
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    protected override async Task OnTcpClosing(ClosingEventArgs e)
 | 
			
		||||
    protected override Task OnTcpClosing(ClosingEventArgs e)
 | 
			
		||||
    {
 | 
			
		||||
        await this.OnChannelEvent(Stoping).ConfigureAwait(false);
 | 
			
		||||
        Logger?.Trace($"{ToString()}  Closing{(e.Message.IsNullOrEmpty() ? string.Empty : $" -{e.Message}")}");
 | 
			
		||||
 | 
			
		||||
        await base.OnTcpClosing(e).ConfigureAwait(false);
 | 
			
		||||
        return base.OnTcpClosing(e);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    protected override async Task OnTcpConnecting(ConnectingEventArgs e)
 | 
			
		||||
    protected override Task OnTcpConnecting(ConnectingEventArgs e)
 | 
			
		||||
    {
 | 
			
		||||
        Logger?.Trace($"{ToString()}  Connecting{(e.Message.IsNullOrEmpty() ? string.Empty : $"-{e.Message}")}");
 | 
			
		||||
        await this.OnChannelEvent(Starting).ConfigureAwait(false);
 | 
			
		||||
        await base.OnTcpConnecting(e).ConfigureAwait(false);
 | 
			
		||||
        return base.OnTcpConnecting(e);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected override async Task OnTcpConnected(ConnectedEventArgs e)
 | 
			
		||||
    protected override Task OnTcpConnected(ConnectedEventArgs e)
 | 
			
		||||
    {
 | 
			
		||||
        Logger?.Info($"{ToString()}  Connected");
 | 
			
		||||
 | 
			
		||||
        await base.OnTcpConnected(e).ConfigureAwait(false);
 | 
			
		||||
        return base.OnTcpConnected(e);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    protected override async Task OnTcpReceived(ReceivedDataEventArgs e)
 | 
			
		||||
    {
 | 
			
		||||
        await base.OnTcpReceived(e).ConfigureAwait(false);
 | 
			
		||||
        if (e.RequestInfo is MessageBase response)
 | 
			
		||||
        {
 | 
			
		||||
            if (ChannelReceivedWaitDict.TryRemove(response.Sign, out var func))
 | 
			
		||||
            {
 | 
			
		||||
                await func.Invoke(this, e, ChannelReceived.Count == 1).ConfigureAwait(false);
 | 
			
		||||
                e.Handled = true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (e.Handled)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
@@ -188,7 +190,7 @@ public class TcpClientChannel : TcpClient, IClientChannel
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    protected override void SafetyDispose(bool disposing)
 | 
			
		||||
    {
 | 
			
		||||
        WaitHandlePool.SafeDispose();
 | 
			
		||||
        WaitHandlePool?.CancelAll();
 | 
			
		||||
        base.SafetyDispose(disposing);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,8 +8,6 @@
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.NewLife;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Foundation;
 | 
			
		||||
@@ -23,30 +21,6 @@ public abstract class TcpServiceChannelBase<TClient> : TcpService<TClient>, ITcp
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public ConcurrentList<IDevice> Collects { get; } = new();
 | 
			
		||||
 | 
			
		||||
    ///// <summary>
 | 
			
		||||
    ///// 停止时是否发送ShutDown
 | 
			
		||||
    ///// </summary>
 | 
			
		||||
    //public bool ShutDownEnable { get; set; } = true;
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public override async Task ClearAsync()
 | 
			
		||||
    {
 | 
			
		||||
        foreach (var client in Clients)
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                //if (ShutDownEnable)
 | 
			
		||||
                //    await client.ShutdownAsync(System.Net.Sockets.SocketShutdown.Both).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                await client.CloseAsync().ConfigureAwait(false);
 | 
			
		||||
                client.SafeDispose();
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task ClientDisposeAsync(string id)
 | 
			
		||||
    {
 | 
			
		||||
        if (this.TryGetClient(id, out var client))
 | 
			
		||||
@@ -136,6 +110,7 @@ public abstract class TcpServiceChannelBase<TClient> : TcpService<TClient>, ITcp
 | 
			
		||||
    {
 | 
			
		||||
        m_transport?.SafeCancel();
 | 
			
		||||
        m_transport?.SafeDispose();
 | 
			
		||||
        m_transport = null;
 | 
			
		||||
        base.SafetyDispose(disposing);
 | 
			
		||||
    }
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
@@ -209,7 +184,7 @@ public class TcpServiceChannel<TClient> : TcpServiceChannelBase<TClient>, IChann
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public Task ConnectAsync(int timeout = 3000, CancellationToken token = default)
 | 
			
		||||
    public Task ConnectAsync(CancellationToken token = default)
 | 
			
		||||
    {
 | 
			
		||||
        if (token.IsCancellationRequested)
 | 
			
		||||
            return EasyTask.CompletedTask;
 | 
			
		||||
@@ -268,22 +243,13 @@ public class TcpServiceChannel<TClient> : TcpServiceChannelBase<TClient>, IChann
 | 
			
		||||
    {
 | 
			
		||||
        await base.OnTcpReceived(socketClient, e).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
        if (e.RequestInfo is MessageBase response)
 | 
			
		||||
        {
 | 
			
		||||
            if (ChannelReceivedWaitDict.TryRemove(response.Sign, out var func))
 | 
			
		||||
            {
 | 
			
		||||
                await func.Invoke(socketClient, e, ChannelReceived.Count == 1).ConfigureAwait(false);
 | 
			
		||||
                e.Handled = true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if (e.Handled)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        await socketClient.OnChannelReceivedEvent(e, ChannelReceived).ConfigureAwait(false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public ConcurrentDictionary<long, Func<IClientChannel, ReceivedDataEventArgs, bool, Task>> ChannelReceivedWaitDict { get; } = new();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    IEnumerable<TcpSessionClientChannel> ITcpServiceChannel.Clients => base.Clients;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -8,8 +8,6 @@
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.NewLife;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Foundation;
 | 
			
		||||
@@ -23,13 +21,31 @@ public class TcpSessionClientChannel : TcpSessionClient, IClientChannel
 | 
			
		||||
    public TcpSessionClientChannel()
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private IDeviceDataHandleAdapter _deviceDataHandleAdapter;
 | 
			
		||||
    public void SetDataHandlingAdapterLogger(ILog log)
 | 
			
		||||
    {
 | 
			
		||||
        if (_deviceDataHandleAdapter == null && DataHandlingAdapter is IDeviceDataHandleAdapter handleAdapter)
 | 
			
		||||
        {
 | 
			
		||||
            _deviceDataHandleAdapter = handleAdapter;
 | 
			
		||||
        }
 | 
			
		||||
        if (_deviceDataHandleAdapter != null)
 | 
			
		||||
        {
 | 
			
		||||
            _deviceDataHandleAdapter.Logger = log;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public void SetDataHandlingAdapter(DataHandlingAdapter adapter)
 | 
			
		||||
    {
 | 
			
		||||
        if (adapter is SingleStreamDataHandlingAdapter singleStreamDataHandlingAdapter)
 | 
			
		||||
            SetAdapter(singleStreamDataHandlingAdapter);
 | 
			
		||||
        if (adapter is IDeviceDataHandleAdapter deviceDataHandleAdapter)
 | 
			
		||||
            _deviceDataHandleAdapter = deviceDataHandleAdapter;
 | 
			
		||||
    }
 | 
			
		||||
    public void ResetSign(int minSign = 0, int maxSign = ushort.MaxValue)
 | 
			
		||||
    {
 | 
			
		||||
        var pool = WaitHandlePool;
 | 
			
		||||
        WaitHandlePool = new WaitHandlePool<MessageBase>(minSign, maxSign);
 | 
			
		||||
        pool?.CancelAll();
 | 
			
		||||
        pool?.SafeDispose();
 | 
			
		||||
    }
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public ChannelReceivedEventHandler ChannelReceived { get; } = new();
 | 
			
		||||
@@ -60,7 +76,7 @@ public class TcpSessionClientChannel : TcpSessionClient, IClientChannel
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 等待池
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public WaitHandlePool<MessageBase> WaitHandlePool { get; private set; } = new();
 | 
			
		||||
    public WaitHandlePool<MessageBase> WaitHandlePool { get; private set; } = new(0, ushort.MaxValue);
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public WaitLock WaitLock { get; internal set; } = new(nameof(TcpSessionClientChannel));
 | 
			
		||||
@@ -69,25 +85,19 @@ public class TcpSessionClientChannel : TcpSessionClient, IClientChannel
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public override Task<Result> CloseAsync(string msg, CancellationToken token)
 | 
			
		||||
    {
 | 
			
		||||
        WaitHandlePool.SafeDispose();
 | 
			
		||||
        WaitHandlePool?.CancelAll();
 | 
			
		||||
        return base.CloseAsync(msg, token);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public Task ConnectAsync(int timeout, CancellationToken token) => Task.CompletedTask;
 | 
			
		||||
    public Task ConnectAsync(CancellationToken token) => Task.CompletedTask;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public void SetDataHandlingAdapter(DataHandlingAdapter adapter)
 | 
			
		||||
    {
 | 
			
		||||
        if (adapter is SingleStreamDataHandlingAdapter singleStreamDataHandlingAdapter)
 | 
			
		||||
            SetAdapter(singleStreamDataHandlingAdapter);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public Task SetupAsync(TouchSocketConfig config) => Task.CompletedTask;
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public ConcurrentDictionary<long, Func<IClientChannel, ReceivedDataEventArgs, bool, Task>> ChannelReceivedWaitDict { get; } = new();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public override string ToString()
 | 
			
		||||
@@ -98,7 +108,7 @@ public class TcpSessionClientChannel : TcpSessionClient, IClientChannel
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    protected override void SafetyDispose(bool disposing)
 | 
			
		||||
    {
 | 
			
		||||
        WaitHandlePool.SafeDispose();
 | 
			
		||||
        WaitHandlePool?.CancelAll();
 | 
			
		||||
        base.SafetyDispose(disposing);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -136,14 +146,7 @@ public class TcpSessionClientChannel : TcpSessionClient, IClientChannel
 | 
			
		||||
    protected override async Task OnTcpReceived(ReceivedDataEventArgs e)
 | 
			
		||||
    {
 | 
			
		||||
        await base.OnTcpReceived(e).ConfigureAwait(false);
 | 
			
		||||
        if (e.RequestInfo is MessageBase response)
 | 
			
		||||
        {
 | 
			
		||||
            if (ChannelReceivedWaitDict.TryRemove(response.Sign, out var func))
 | 
			
		||||
            {
 | 
			
		||||
                await func.Invoke(this, e, ChannelReceived.Count == 1).ConfigureAwait(false);
 | 
			
		||||
                e.Handled = true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (e.Handled)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -8,8 +8,6 @@
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.NewLife;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Foundation;
 | 
			
		||||
@@ -28,13 +26,31 @@ public class UdpSessionChannel : UdpSession, IClientChannel
 | 
			
		||||
        ResetSign();
 | 
			
		||||
    }
 | 
			
		||||
    public override TouchSocketConfig Config => base.Config ?? ChannelOptions.Config;
 | 
			
		||||
 | 
			
		||||
    private IDeviceDataHandleAdapter _deviceDataHandleAdapter;
 | 
			
		||||
    public void SetDataHandlingAdapterLogger(ILog log)
 | 
			
		||||
    {
 | 
			
		||||
        if (_deviceDataHandleAdapter == null && DataHandlingAdapter is IDeviceDataHandleAdapter handleAdapter)
 | 
			
		||||
        {
 | 
			
		||||
            _deviceDataHandleAdapter = handleAdapter;
 | 
			
		||||
        }
 | 
			
		||||
        if (_deviceDataHandleAdapter != null)
 | 
			
		||||
        {
 | 
			
		||||
            _deviceDataHandleAdapter.Logger = log;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public void SetDataHandlingAdapter(DataHandlingAdapter adapter)
 | 
			
		||||
    {
 | 
			
		||||
        if (adapter is UdpDataHandlingAdapter udpDataHandlingAdapter)
 | 
			
		||||
            SetAdapter(udpDataHandlingAdapter);
 | 
			
		||||
        if (adapter is IDeviceDataHandleAdapter deviceDataHandleAdapter)
 | 
			
		||||
            _deviceDataHandleAdapter = deviceDataHandleAdapter;
 | 
			
		||||
    }
 | 
			
		||||
    public void ResetSign(int minSign = 0, int maxSign = ushort.MaxValue)
 | 
			
		||||
    {
 | 
			
		||||
        var pool = WaitHandlePool;
 | 
			
		||||
        WaitHandlePool = new WaitHandlePool<MessageBase>(minSign, maxSign);
 | 
			
		||||
        pool?.CancelAll();
 | 
			
		||||
        pool?.SafeDispose();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
@@ -69,14 +85,13 @@ public class UdpSessionChannel : UdpSession, IClientChannel
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 等待池
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public WaitHandlePool<MessageBase> WaitHandlePool { get; set; } = new();
 | 
			
		||||
    public WaitHandlePool<MessageBase> WaitHandlePool { get; set; } = new(0, ushort.MaxValue);
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public WaitLock WaitLock => ChannelOptions.WaitLock;
 | 
			
		||||
    public virtual WaitLock GetLock(string key) => WaitLock;
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public ConcurrentDictionary<long, Func<IClientChannel, ReceivedDataEventArgs, bool, Task>> ChannelReceivedWaitDict { get; } = new();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public Task<Result> CloseAsync(string msg, CancellationToken token)
 | 
			
		||||
@@ -85,19 +100,14 @@ public class UdpSessionChannel : UdpSession, IClientChannel
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public async Task ConnectAsync(int timeout = 3000, CancellationToken token = default)
 | 
			
		||||
    public Task ConnectAsync(CancellationToken token = default)
 | 
			
		||||
    {
 | 
			
		||||
        if (token.IsCancellationRequested)
 | 
			
		||||
            return;
 | 
			
		||||
        await StartAsync().ConfigureAwait(false);
 | 
			
		||||
            return EasyTask.CompletedTask; ;
 | 
			
		||||
        return StartAsync();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public void SetDataHandlingAdapter(DataHandlingAdapter adapter)
 | 
			
		||||
    {
 | 
			
		||||
        if (adapter is UdpDataHandlingAdapter udpDataHandlingAdapter)
 | 
			
		||||
            SetAdapter(udpDataHandlingAdapter);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public CancellationToken ClosedToken => this.m_transport == null ? new CancellationToken(true) : this.m_transport.Token;
 | 
			
		||||
    private CancellationTokenSource m_transport;
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
@@ -188,14 +198,6 @@ public class UdpSessionChannel : UdpSession, IClientChannel
 | 
			
		||||
    {
 | 
			
		||||
        await base.OnUdpReceived(e).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
        if (e.RequestInfo is MessageBase response)
 | 
			
		||||
        {
 | 
			
		||||
            if (ChannelReceivedWaitDict.TryRemove(response.Sign, out var func))
 | 
			
		||||
            {
 | 
			
		||||
                await func.Invoke(this, e, ChannelReceived.Count == 1).ConfigureAwait(false);
 | 
			
		||||
                e.Handled = true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if (e.Handled)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
@@ -207,7 +209,8 @@ public class UdpSessionChannel : UdpSession, IClientChannel
 | 
			
		||||
    {
 | 
			
		||||
        m_transport?.SafeCancel();
 | 
			
		||||
        m_transport?.SafeDispose();
 | 
			
		||||
        WaitHandlePool.SafeDispose();
 | 
			
		||||
        m_transport = null;
 | 
			
		||||
        WaitHandlePool?.CancelAll();
 | 
			
		||||
        base.SafetyDispose(disposing);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -15,15 +15,18 @@ namespace ThingsGateway.Foundation;
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// TCP/Serial适配器基类
 | 
			
		||||
/// </summary>
 | 
			
		||||
public class DeviceSingleStreamDataHandleAdapter<TRequest> : TcpCustomDataHandlingAdapter<TRequest> where TRequest : MessageBase, new()
 | 
			
		||||
public class DeviceSingleStreamDataHandleAdapter<TRequest> : CustomDataHandlingAdapter<TRequest>, IDeviceDataHandleAdapter where TRequest : MessageBase, new()
 | 
			
		||||
{
 | 
			
		||||
    public new ILog Logger { get; set; }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc cref="DeviceSingleStreamDataHandleAdapter{TRequest}"/>
 | 
			
		||||
    public DeviceSingleStreamDataHandleAdapter()
 | 
			
		||||
    {
 | 
			
		||||
        CacheTimeoutEnable = true;
 | 
			
		||||
        SurLength = int.MaxValue;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public override bool CanSendRequestInfo => true;
 | 
			
		||||
 | 
			
		||||
@@ -40,11 +43,11 @@ public class DeviceSingleStreamDataHandleAdapter<TRequest> : TcpCustomDataHandli
 | 
			
		||||
    public TRequest Request { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    public void SetRequest(ISendMessage sendMessage, ref ValueByteBlock byteBlock)
 | 
			
		||||
    public void SetRequest(ISendMessage sendMessage)
 | 
			
		||||
    {
 | 
			
		||||
        var request = GetInstance();
 | 
			
		||||
        request.Sign = sendMessage.Sign;
 | 
			
		||||
        request.SendInfo(sendMessage, ref byteBlock);
 | 
			
		||||
        request.SendInfo(sendMessage);
 | 
			
		||||
        Request = request;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -55,24 +58,24 @@ public class DeviceSingleStreamDataHandleAdapter<TRequest> : TcpCustomDataHandli
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    protected override FilterResult Filter<TByteBlock>(ref TByteBlock byteBlock, bool beCached, ref TRequest request, ref int tempCapacity)
 | 
			
		||||
    protected override FilterResult Filter<TReader>(ref TReader byteBlock, bool beCached, ref TRequest request)
 | 
			
		||||
    {
 | 
			
		||||
        if (Logger?.LogLevel <= LogLevel.Trace)
 | 
			
		||||
            Logger?.Trace($"{ToString()}- Receive:{(IsHexLog ? byteBlock.AsSegmentTake().ToHexString(' ') : byteBlock.ToString(byteBlock.Position))}");
 | 
			
		||||
            Logger?.Trace($"{ToString()}- Receive:{(IsHexLog ? byteBlock.ToHexString(byteBlock.BytesRead, ' ') : byteBlock.ToString(byteBlock.BytesRead))}");
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            if (IsSingleThread)
 | 
			
		||||
                request = Request == null ? GetInstance() : Request;
 | 
			
		||||
                request = Request == null ? Request = GetInstance() : Request;
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                if (!beCached)
 | 
			
		||||
                    request = GetInstance();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var pos = byteBlock.Position;
 | 
			
		||||
            var pos = byteBlock.BytesRead;
 | 
			
		||||
 | 
			
		||||
            if (request.HeaderLength > byteBlock.CanReadLength)
 | 
			
		||||
            if (request.HeaderLength > byteBlock.BytesRemaining)
 | 
			
		||||
            {
 | 
			
		||||
                return FilterResult.Cache;//当头部都无法解析时,直接缓存
 | 
			
		||||
            }
 | 
			
		||||
@@ -80,19 +83,18 @@ public class DeviceSingleStreamDataHandleAdapter<TRequest> : TcpCustomDataHandli
 | 
			
		||||
            //检查头部合法性
 | 
			
		||||
            if (request.CheckHead(ref byteBlock))
 | 
			
		||||
            {
 | 
			
		||||
                byteBlock.Position = pos;
 | 
			
		||||
                byteBlock.BytesRead = pos;
 | 
			
		||||
                if (request.BodyLength > MaxPackageSize)
 | 
			
		||||
                {
 | 
			
		||||
                    request.OperCode = -1;
 | 
			
		||||
                    request.ErrorMessage = $"Received BodyLength={request.BodyLength}, greater than the set MaxPackageSize={MaxPackageSize}";
 | 
			
		||||
                    OnError(default, request.ErrorMessage, true, true);
 | 
			
		||||
                    SetResult(request);
 | 
			
		||||
                    Reset();
 | 
			
		||||
                    Logger?.LogWarning($"{ToString()} {request.ErrorMessage}");
 | 
			
		||||
                    return FilterResult.GoOn;
 | 
			
		||||
                }
 | 
			
		||||
                if (request.BodyLength + request.HeaderLength > byteBlock.CanReadLength)
 | 
			
		||||
                if (request.BodyLength + request.HeaderLength > byteBlock.BytesRemaining)
 | 
			
		||||
                {
 | 
			
		||||
                    //body不满足解析,开始缓存,然后保存对象
 | 
			
		||||
                    tempCapacity = request.BodyLength + request.HeaderLength;
 | 
			
		||||
                    return FilterResult.Cache;
 | 
			
		||||
                }
 | 
			
		||||
                //if (request.BodyLength <= 0)
 | 
			
		||||
@@ -101,56 +103,47 @@ public class DeviceSingleStreamDataHandleAdapter<TRequest> : TcpCustomDataHandli
 | 
			
		||||
                //    request.BodyLength = byteBlock.Length;
 | 
			
		||||
                //}
 | 
			
		||||
                var headPos = pos + request.HeaderLength;
 | 
			
		||||
                byteBlock.Position = headPos;
 | 
			
		||||
                byteBlock.BytesRead = headPos;
 | 
			
		||||
                var result = request.CheckBody(ref byteBlock);
 | 
			
		||||
                if (result == FilterResult.Cache)
 | 
			
		||||
                {
 | 
			
		||||
                    byteBlock.BytesRead = pos;
 | 
			
		||||
                    if (Logger?.LogLevel <= LogLevel.Trace)
 | 
			
		||||
                        Logger?.Trace($"{ToString()}-Received incomplete, cached message, current length:{byteBlock.Length}  {request?.ErrorMessage}");
 | 
			
		||||
                    tempCapacity = request.BodyLength + request.HeaderLength;
 | 
			
		||||
                        Logger?.Trace($"{ToString()}-Received incomplete, cached message, need length:{request.HeaderLength + request.BodyLength} ,current length:{byteBlock.BytesRead + byteBlock.BytesRemaining}  {request?.ErrorMessage}");
 | 
			
		||||
                    request.OperCode = -1;
 | 
			
		||||
                }
 | 
			
		||||
                else if (result == FilterResult.GoOn)
 | 
			
		||||
                {
 | 
			
		||||
                    var addLen = request.HeaderLength + request.BodyLength;
 | 
			
		||||
                    byteBlock.Position = pos + (addLen > 0 ? addLen : 1);
 | 
			
		||||
                    Logger?.Trace($"{ToString()}-{request?.ToString()}");
 | 
			
		||||
                    byteBlock.BytesRead = pos + (addLen > 0 ? addLen : 1);
 | 
			
		||||
                    if (Logger?.LogLevel <= LogLevel.Trace)
 | 
			
		||||
                        Logger?.Trace($"{ToString()}-{request?.ToString()}");
 | 
			
		||||
                    request.OperCode = -1;
 | 
			
		||||
                    SetResult(request);
 | 
			
		||||
                }
 | 
			
		||||
                else if (result == FilterResult.Success)
 | 
			
		||||
                {
 | 
			
		||||
                    var addLen = request.HeaderLength + request.BodyLength;
 | 
			
		||||
                    byteBlock.Position = pos + (addLen > 0 ? addLen : 1);
 | 
			
		||||
                    byteBlock.BytesRead = pos + (addLen > 0 ? addLen : 1);
 | 
			
		||||
                }
 | 
			
		||||
                return result;
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                byteBlock.Position = pos + 1;//移动游标
 | 
			
		||||
                byteBlock.BytesRead = pos + 1;//移动游标
 | 
			
		||||
                request.OperCode = -1;
 | 
			
		||||
                SetResult(request);
 | 
			
		||||
                return FilterResult.GoOn;//放弃解析
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            Logger?.LogWarning(ex, $"{ToString()} Received parsing error");
 | 
			
		||||
            byteBlock.Position = byteBlock.Length;//移动游标
 | 
			
		||||
            byteBlock.BytesRead = byteBlock.BytesRead + byteBlock.BytesRemaining;//移动游标
 | 
			
		||||
            request.Exception = ex;
 | 
			
		||||
            request.OperCode = -1;
 | 
			
		||||
            SetResult(request);
 | 
			
		||||
            return FilterResult.GoOn;//放弃解析
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void SetResult(TRequest request)
 | 
			
		||||
    {
 | 
			
		||||
        if ((Owner as IClientChannel)?.WaitHandlePool?.TryGetDataAsync(request.Sign, out var waitDataAsync) == true)
 | 
			
		||||
        {
 | 
			
		||||
            waitDataAsync.SetResult(request);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取泛型实例。
 | 
			
		||||
@@ -161,47 +154,32 @@ public class DeviceSingleStreamDataHandleAdapter<TRequest> : TcpCustomDataHandli
 | 
			
		||||
        return new TRequest() { OperCode = -1, Sign = -1 };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    protected override void OnReceivedSuccess(TRequest request)
 | 
			
		||||
    public override void SendInput<TWriter>(ref TWriter writer, in ReadOnlyMemory<byte> memory)
 | 
			
		||||
    {
 | 
			
		||||
        Request = null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    protected override async Task PreviewSendAsync(ReadOnlyMemory<byte> memory, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        cancellationToken.ThrowIfCancellationRequested();
 | 
			
		||||
        if (Logger?.LogLevel <= LogLevel.Trace)
 | 
			
		||||
            Logger?.Trace($"{ToString()}- Send:{(IsHexLog ? memory.Span.ToHexString(' ') : (memory.Span.ToString(Encoding.UTF8)))}");
 | 
			
		||||
 | 
			
		||||
        //发送
 | 
			
		||||
        await GoSendAsync(memory, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        writer.Write(memory.Span);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    protected override async Task PreviewSendAsync(IRequestInfo requestInfo, CancellationToken cancellationToken)
 | 
			
		||||
    public override void SendInput<TWriter>(ref TWriter writer, IRequestInfo requestInfo)
 | 
			
		||||
    {
 | 
			
		||||
        if (!(requestInfo is ISendMessage sendMessage))
 | 
			
		||||
        {
 | 
			
		||||
            throw new Exception($"Unable to convert {nameof(requestInfo)} to {nameof(ISendMessage)}");
 | 
			
		||||
        }
 | 
			
		||||
        cancellationToken.ThrowIfCancellationRequested();
 | 
			
		||||
        var byteBlock = new ValueByteBlock(sendMessage.MaxLength);
 | 
			
		||||
        try
 | 
			
		||||
        var span = writer.GetSpan(sendMessage.MaxLength);
 | 
			
		||||
        sendMessage.Build(ref writer);
 | 
			
		||||
        if (Logger?.LogLevel <= LogLevel.Trace)
 | 
			
		||||
        {
 | 
			
		||||
            sendMessage.Build(ref byteBlock);
 | 
			
		||||
            if (Logger?.LogLevel <= LogLevel.Trace)
 | 
			
		||||
                Logger?.Trace($"{ToString()}- Send:{(IsHexLog ? byteBlock.Span.ToHexString(' ') : (byteBlock.Span.ToString(Encoding.UTF8)))}");
 | 
			
		||||
            //非并发主从协议
 | 
			
		||||
            if (IsSingleThread)
 | 
			
		||||
            {
 | 
			
		||||
                SetRequest(sendMessage, ref byteBlock);
 | 
			
		||||
            }
 | 
			
		||||
            await GoSendAsync(byteBlock.Memory, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
            Logger?.Trace($"{ToString()}- Send:{(IsHexLog ? span.Slice(0, (int)writer.WrittenCount).ToHexString(' ') : (span.Slice(0, (int)writer.WrittenCount).ToString(Encoding.UTF8)))}");
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        //非并发主从协议
 | 
			
		||||
        if (IsSingleThread)
 | 
			
		||||
        {
 | 
			
		||||
            byteBlock.SafeDispose();
 | 
			
		||||
            SetRequest(sendMessage);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -16,8 +16,10 @@ namespace ThingsGateway.Foundation;
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// UDP适配器基类
 | 
			
		||||
/// </summary>
 | 
			
		||||
public class DeviceUdpDataHandleAdapter<TRequest> : UdpDataHandlingAdapter where TRequest : MessageBase, new()
 | 
			
		||||
public class DeviceUdpDataHandleAdapter<TRequest> : UdpDataHandlingAdapter, IDeviceDataHandleAdapter where TRequest : MessageBase, new()
 | 
			
		||||
{
 | 
			
		||||
    public new ILog Logger { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public override bool CanSendRequestInfo => true;
 | 
			
		||||
 | 
			
		||||
@@ -34,11 +36,11 @@ public class DeviceUdpDataHandleAdapter<TRequest> : UdpDataHandlingAdapter where
 | 
			
		||||
    public TRequest Request { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    public void SetRequest(ISendMessage sendMessage, ref ValueByteBlock byteBlock)
 | 
			
		||||
    public void SetRequest(ISendMessage sendMessage)
 | 
			
		||||
    {
 | 
			
		||||
        var request = GetInstance();
 | 
			
		||||
        request.Sign = sendMessage.Sign;
 | 
			
		||||
        request.SendInfo(sendMessage, ref byteBlock);
 | 
			
		||||
        request.SendInfo(sendMessage);
 | 
			
		||||
        Request = request;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -57,113 +59,132 @@ public class DeviceUdpDataHandleAdapter<TRequest> : UdpDataHandlingAdapter where
 | 
			
		||||
        return new TRequest() { OperCode = -1, Sign = -1 };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    protected override async Task PreviewReceived(EndPoint remoteEndPoint, IByteBlockReader byteBlock)
 | 
			
		||||
 | 
			
		||||
    #region ParseRequest
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 尝试从字节读取器中解析出请求信息。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="remoteEndPoint">remoteEndPoint。</param>
 | 
			
		||||
    /// <param name="memory">memory。</param>
 | 
			
		||||
    /// <param name="request">解析出的请求信息。</param>
 | 
			
		||||
    /// <returns>解析成功返回 true,否则返回 false。</returns>
 | 
			
		||||
    public bool TryParseRequest(EndPoint remoteEndPoint, ReadOnlyMemory<byte> memory, out TRequest request)
 | 
			
		||||
    {
 | 
			
		||||
        return this.ParseRequestCore(remoteEndPoint, memory, out request);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected virtual bool ParseRequestCore(EndPoint remoteEndPoint, ReadOnlyMemory<byte> memory, out TRequest request1)
 | 
			
		||||
    {
 | 
			
		||||
        request1 = null;
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            byteBlock.Position = 0;
 | 
			
		||||
 | 
			
		||||
            if (Logger?.LogLevel <= LogLevel.Trace)
 | 
			
		||||
                Logger?.Trace($"{remoteEndPoint}- Receive:{(IsHexLog ? byteBlock.AsSegmentTake().ToHexString(' ') : byteBlock.ToString(byteBlock.Position))}");
 | 
			
		||||
                Logger?.Trace($"{remoteEndPoint}- Receive:{(IsHexLog ? memory.Span.ToHexString(' ') : memory.Span.ToString(Encoding.UTF8))}");
 | 
			
		||||
 | 
			
		||||
            TRequest request = null;
 | 
			
		||||
            if (IsSingleThread)
 | 
			
		||||
                request = Request == null ? GetInstance() : Request;
 | 
			
		||||
                request = Request == null ? Request = GetInstance() : Request;
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                request = GetInstance();
 | 
			
		||||
            }
 | 
			
		||||
            request1 = request;
 | 
			
		||||
 | 
			
		||||
            var pos = byteBlock.Position;
 | 
			
		||||
            var byteBlock = new ByteBlockReader(memory);
 | 
			
		||||
            byteBlock.BytesRead = 0;
 | 
			
		||||
 | 
			
		||||
            var pos = byteBlock.BytesRead;
 | 
			
		||||
 | 
			
		||||
            if (request.HeaderLength > byteBlock.CanReadLength)
 | 
			
		||||
            {
 | 
			
		||||
                return;//当头部都无法解析时,直接缓存
 | 
			
		||||
                return false;//当头部都无法解析时,直接缓存
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            //检查头部合法性
 | 
			
		||||
            if (request.CheckHead(ref byteBlock))
 | 
			
		||||
            {
 | 
			
		||||
                byteBlock.Position = pos;
 | 
			
		||||
                byteBlock.BytesRead = pos;
 | 
			
		||||
 | 
			
		||||
                if (request.BodyLength > MaxPackageSize)
 | 
			
		||||
                {
 | 
			
		||||
                    OnError(default, $"Received BodyLength={request.BodyLength}, greater than the set MaxPackageSize={MaxPackageSize}", true, true);
 | 
			
		||||
                    return;
 | 
			
		||||
                    request.OperCode = -1;
 | 
			
		||||
                    request.ErrorMessage = $"Received BodyLength={request.BodyLength}, greater than the set MaxPackageSize={MaxPackageSize}";
 | 
			
		||||
                    Reset();
 | 
			
		||||
                    Logger?.LogWarning($"{ToString()} {request.ErrorMessage}");
 | 
			
		||||
                    return false;
 | 
			
		||||
                }
 | 
			
		||||
                if (request.BodyLength + request.HeaderLength > byteBlock.CanReadLength)
 | 
			
		||||
                {
 | 
			
		||||
                    //body不满足解析,开始缓存,然后保存对象
 | 
			
		||||
                    return;
 | 
			
		||||
                    return false;
 | 
			
		||||
                }
 | 
			
		||||
                //if (request.BodyLength <= 0)
 | 
			
		||||
                //{
 | 
			
		||||
                //    //如果body长度无法确定,直接读取全部
 | 
			
		||||
                //    request.BodyLength = byteBlock.Length;
 | 
			
		||||
                //}
 | 
			
		||||
 | 
			
		||||
                var headPos = pos + request.HeaderLength;
 | 
			
		||||
                byteBlock.Position = headPos;
 | 
			
		||||
                byteBlock.BytesRead = headPos;
 | 
			
		||||
                var result = request.CheckBody(ref byteBlock);
 | 
			
		||||
                if (result == FilterResult.Cache)
 | 
			
		||||
                {
 | 
			
		||||
                    if (Logger?.LogLevel <= LogLevel.Trace)
 | 
			
		||||
                        Logger?.Trace($"{ToString()}-Received incomplete, cached message, current length:{byteBlock.Length}  {request?.ErrorMessage}");
 | 
			
		||||
                        Logger?.Trace($"{ToString()}-Received incomplete, cached message, need length:{request.HeaderLength + request.BodyLength} ,current length:{byteBlock.BytesRead + byteBlock.BytesRemaining}  {request?.ErrorMessage}");
 | 
			
		||||
                    request.OperCode = -1;
 | 
			
		||||
                }
 | 
			
		||||
                else if (result == FilterResult.GoOn)
 | 
			
		||||
                {
 | 
			
		||||
                    var addLen = request.HeaderLength + request.BodyLength;
 | 
			
		||||
                    byteBlock.Position = pos + (addLen > 0 ? addLen : 1);
 | 
			
		||||
                    Logger?.Trace($"{ToString()}-{request?.ToString()}");
 | 
			
		||||
                    if (Logger?.LogLevel <= LogLevel.Trace)
 | 
			
		||||
                        Logger?.Trace($"{ToString()}-{request?.ToString()}");
 | 
			
		||||
                    request.OperCode = -1;
 | 
			
		||||
                    if ((Owner as IClientChannel)?.WaitHandlePool?.TryGetDataAsync(request.Sign, out var waitDataAsync) == true)
 | 
			
		||||
                    {
 | 
			
		||||
                        waitDataAsync.SetResult(request);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                else if (result == FilterResult.Success)
 | 
			
		||||
                {
 | 
			
		||||
                    var addLen = request.HeaderLength + request.BodyLength;
 | 
			
		||||
                    byteBlock.Position = pos + (addLen > 0 ? addLen : 1);
 | 
			
		||||
                    await GoReceived(remoteEndPoint, null, request).ConfigureAwait(false);
 | 
			
		||||
                    request1 = request;
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                byteBlock.Position = pos + 1;
 | 
			
		||||
                request.OperCode = -1;
 | 
			
		||||
                return;
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            Logger?.LogWarning(ex, $"{ToString()} Received parsing error");
 | 
			
		||||
            byteBlock.Position = byteBlock.Length;//移动游标
 | 
			
		||||
            return;
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #endregion
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    protected override async Task PreviewSendAsync(EndPoint endPoint, ReadOnlyMemory<byte> memory, CancellationToken cancellationToken)
 | 
			
		||||
    protected override Task PreviewReceivedAsync(EndPoint remoteEndPoint, ReadOnlyMemory<byte> memory)
 | 
			
		||||
    {
 | 
			
		||||
        if (ParseRequestCore(remoteEndPoint, memory, out var request))
 | 
			
		||||
        {
 | 
			
		||||
            return GoReceived(remoteEndPoint, null, request);
 | 
			
		||||
        }
 | 
			
		||||
        return EasyTask.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    protected override Task PreviewSendAsync(EndPoint endPoint, ReadOnlyMemory<byte> memory, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        cancellationToken.ThrowIfCancellationRequested();
 | 
			
		||||
 | 
			
		||||
        if (Logger?.LogLevel <= LogLevel.Trace)
 | 
			
		||||
            Logger?.Trace($"{ToString()}- Send:{(IsHexLog ? memory.Span.ToHexString(' ') : (memory.Span.ToString(Encoding.UTF8)))}");
 | 
			
		||||
        //发送
 | 
			
		||||
        await GoSendAsync(endPoint, memory, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        return GoSendAsync(endPoint, memory, cancellationToken);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    protected override async Task PreviewSendAsync(EndPoint endPoint, IRequestInfo requestInfo, CancellationToken cancellationToken)
 | 
			
		||||
    protected override Task PreviewSendAsync(EndPoint endPoint, IRequestInfo requestInfo, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        if (!(requestInfo is ISendMessage sendMessage))
 | 
			
		||||
        {
 | 
			
		||||
            throw new Exception($"Unable to convert {nameof(requestInfo)} to {nameof(ISendMessage)}");
 | 
			
		||||
        }
 | 
			
		||||
        cancellationToken.ThrowIfCancellationRequested();
 | 
			
		||||
 | 
			
		||||
        var byteBlock = new ValueByteBlock(sendMessage.MaxLength);
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
@@ -173,9 +194,9 @@ public class DeviceUdpDataHandleAdapter<TRequest> : UdpDataHandlingAdapter where
 | 
			
		||||
 | 
			
		||||
            if (IsSingleThread)
 | 
			
		||||
            {
 | 
			
		||||
                SetRequest(sendMessage, ref byteBlock);
 | 
			
		||||
                SetRequest(sendMessage);
 | 
			
		||||
            }
 | 
			
		||||
            await GoSendAsync(endPoint, byteBlock.Memory, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
            return GoSendAsync(endPoint, byteBlock.Memory, cancellationToken);
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,46 +0,0 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
//  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
 | 
			
		||||
//  此代码版权(除特别声明外的代码)归作者本人Diego所有
 | 
			
		||||
//  源代码使用协议遵循本仓库的开源协议及附加协议
 | 
			
		||||
//  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
 | 
			
		||||
//  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
 | 
			
		||||
//  使用文档:https://thingsgateway.cn/
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Foundation;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 定义了字节块构建器的接口,用于从内存池中构建和管理字节块。
 | 
			
		||||
/// </summary>
 | 
			
		||||
public interface IByteBlockWriterBuilder
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 构建数据时,指示内存池的申请长度。
 | 
			
		||||
    /// <para>
 | 
			
		||||
    /// 建议:该值可以尽可能的设置大一些,这样可以避免内存池扩容。
 | 
			
		||||
    /// </para>
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    int MaxLength { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 构建对象到<see cref="ByteBlock"/>
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="writer">要构建的字节块对象引用。</param>
 | 
			
		||||
    void Build<TWriter>(ref TWriter writer) where TWriter : IByteBlockWriter
 | 
			
		||||
#if AllowsRefStruct
 | 
			
		||||
,allows ref struct
 | 
			
		||||
#endif
 | 
			
		||||
        ;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 指示<see cref="IRequestInfo"/>应当如何构建
 | 
			
		||||
/// </summary>
 | 
			
		||||
public interface IRequestInfoByteBlockWriterBuilder : IRequestInfo, IByteBlockWriterBuilder
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,21 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
//  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
 | 
			
		||||
//  此代码版权(除特别声明外的代码)归作者本人Diego所有
 | 
			
		||||
//  源代码使用协议遵循本仓库的开源协议及附加协议
 | 
			
		||||
//  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
 | 
			
		||||
//  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
 | 
			
		||||
//  使用文档:https://thingsgateway.cn/
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Foundation
 | 
			
		||||
{
 | 
			
		||||
    public interface IDeviceDataHandleAdapter
 | 
			
		||||
    {
 | 
			
		||||
        bool CanSendRequestInfo { get; }
 | 
			
		||||
        bool IsHexLog { get; set; }
 | 
			
		||||
        bool IsSingleThread { get; set; }
 | 
			
		||||
        ILog Logger { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -18,7 +18,7 @@ public interface IResultMessage : IOperResult, IRequestInfo
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 数据体长度
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    int BodyLength { get; set; }
 | 
			
		||||
    long BodyLength { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 解析的字节信息
 | 
			
		||||
@@ -28,7 +28,7 @@ public interface IResultMessage : IOperResult, IRequestInfo
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 消息头的指令长度,不固定时返回0
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    int HeaderLength { get; }
 | 
			
		||||
    long HeaderLength { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 等待标识,对于并发协议,必须从协议中例如固定头部获取标识字段
 | 
			
		||||
@@ -42,17 +42,17 @@ public interface IResultMessage : IOperResult, IRequestInfo
 | 
			
		||||
    /// <para>然后返回<see cref="FilterResult.GoOn"/></para>
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns>是否成功有效</returns>
 | 
			
		||||
    FilterResult CheckBody<TByteBlock>(ref TByteBlock byteBlock) where TByteBlock : IByteBlockReader;
 | 
			
		||||
    FilterResult CheckBody<TByteBlock>(ref TByteBlock byteBlock) where TByteBlock : IBytesReader;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 检查头子节的合法性,并赋值<see cref="BodyLength"/><br />
 | 
			
		||||
    /// <para>如果返回false,意味着放弃本次解析的所有数据,包括已经解析完成的Header</para>
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns>是否成功的结果</returns>
 | 
			
		||||
    bool CheckHead<TByteBlock>(ref TByteBlock byteBlock) where TByteBlock : IByteBlockReader;
 | 
			
		||||
    bool CheckHead<TByteBlock>(ref TByteBlock byteBlock) where TByteBlock : IBytesReader;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 发送前的信息处理,例如存储某些特征信息:站号/功能码等等用于验证后续的返回信息是否合法
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    void SendInfo(ISendMessage sendMessage, ref ValueByteBlock byteBlock);
 | 
			
		||||
    void SendInfo(ISendMessage sendMessage);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,6 @@ namespace ThingsGateway.Foundation;
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 发送消息
 | 
			
		||||
/// </summary>
 | 
			
		||||
public interface ISendMessage : IRequestInfo, IWaitHandle, IRequestInfoByteBlockWriterBuilder
 | 
			
		||||
public interface ISendMessage : IRequestInfo, IWaitHandle, IRequestInfoBuilder
 | 
			
		||||
{
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -33,28 +33,28 @@ public class MessageBase : OperResultClass<ReadOnlyMemory<byte>>, IResultMessage
 | 
			
		||||
    #endregion 构造
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public int BodyLength { get; set; }
 | 
			
		||||
    public long BodyLength { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public virtual int HeaderLength { get; set; }
 | 
			
		||||
    public virtual long HeaderLength { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public virtual int Sign { get; set; } = -1;
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    public virtual FilterResult CheckBody<TByteBlock>(ref TByteBlock byteBlock) where TByteBlock : IByteBlockReader
 | 
			
		||||
    public virtual FilterResult CheckBody<TByteBlock>(ref TByteBlock byteBlock) where TByteBlock : IBytesReader
 | 
			
		||||
    {
 | 
			
		||||
        return FilterResult.Success;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public virtual bool CheckHead<TByteBlock>(ref TByteBlock byteBlock) where TByteBlock : IByteBlockReader
 | 
			
		||||
    public virtual bool CheckHead<TByteBlock>(ref TByteBlock byteBlock) where TByteBlock : IBytesReader
 | 
			
		||||
    {
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public virtual void SendInfo(ISendMessage sendMessage, ref TouchSocket.Core.ValueByteBlock byteBlock)
 | 
			
		||||
    public virtual void SendInfo(ISendMessage sendMessage)
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,306 +0,0 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
//  此代码版权(除特别声明或在XREF结尾的命名空间的代码)归作者本人若汝棋茗所有
 | 
			
		||||
//  源代码使用协议遵循本仓库的开源协议及附加协议,若本仓库没有设置,则按MIT开源协议授权
 | 
			
		||||
//  CSDN博客:https://blog.csdn.net/qq_40374647
 | 
			
		||||
//  哔哩哔哩视频:https://space.bilibili.com/94253567
 | 
			
		||||
//  Gitee源代码仓库:https://gitee.com/RRQM_Home
 | 
			
		||||
//  Github源代码仓库:https://github.com/RRQM
 | 
			
		||||
//  API首页:https://touchsocket.net/
 | 
			
		||||
//  交流QQ群:234762506
 | 
			
		||||
//  感谢您的下载和使用
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Foundation;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 用户自定义数据处理适配器,使用该适配器时,接收方收到的数据中,<see cref="ByteBlock"/>将为<see langword="null"/>,
 | 
			
		||||
/// 同时<see cref="IRequestInfo"/>将实现为TRequest,发送数据直接发送。
 | 
			
		||||
/// </summary>
 | 
			
		||||
public abstract class TcpCustomDataHandlingAdapter<TRequest> : SingleStreamDataHandlingAdapter where TRequest : IRequestInfo
 | 
			
		||||
{
 | 
			
		||||
    private readonly Type m_requestType;
 | 
			
		||||
    private ValueByteBlock m_tempByteBlock;
 | 
			
		||||
    private TRequest m_tempRequest;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 初始化自定义数据处理适配器。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <remarks>
 | 
			
		||||
    /// 该构造函数在创建<see cref="TcpCustomDataHandlingAdapter{TRequest}"/>实例时,会指定请求类型。
 | 
			
		||||
    /// </remarks>
 | 
			
		||||
    public TcpCustomDataHandlingAdapter()
 | 
			
		||||
    {
 | 
			
		||||
        this.m_requestType = typeof(TRequest);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public override bool CanSendRequestInfo => false;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 指示需要解析当前包的剩余长度。如果不能得知,请赋值<see cref="int.MaxValue"/>。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    protected int SurLength { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 尝试解析请求数据块。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <typeparam name="TByteBlock">字节块类型,必须实现IByteBlock接口。</typeparam>
 | 
			
		||||
    /// <param name="reader">待解析的字节块。</param>
 | 
			
		||||
    /// <param name="request">解析出的请求对象。</param>
 | 
			
		||||
    /// <returns>解析是否成功。</returns>
 | 
			
		||||
    public bool TryParseRequest<TByteBlock>(ref TByteBlock reader, out TRequest request) where TByteBlock : IByteBlockReader
 | 
			
		||||
    {
 | 
			
		||||
        // 检查缓存是否超时,如果超时则清除缓存。
 | 
			
		||||
        if (this.CacheTimeoutEnable && DateTimeOffset.UtcNow - this.LastCacheTime > this.CacheTimeout)
 | 
			
		||||
        {
 | 
			
		||||
            this.Reset();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 如果临时字节块为空,则尝试直接解析。
 | 
			
		||||
        if (this.m_tempByteBlock.IsEmpty)
 | 
			
		||||
        {
 | 
			
		||||
            return this.Single(ref reader, out request) == FilterResult.Success;
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            // 如果剩余长度小于等于0,则抛出异常。
 | 
			
		||||
            if (this.SurLength <= 0)
 | 
			
		||||
            {
 | 
			
		||||
                throw new Exception();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // 计算本次可以读取的长度。
 | 
			
		||||
            var len = Math.Min(this.SurLength, reader.CanReadLength);
 | 
			
		||||
 | 
			
		||||
            // 从输入字节块中读取数据到临时字节块中。
 | 
			
		||||
            var block = this.m_tempByteBlock;
 | 
			
		||||
            block.Write(reader.Span.Slice(reader.Position, len));
 | 
			
		||||
            reader.Position += len;
 | 
			
		||||
            this.SurLength -= len;
 | 
			
		||||
 | 
			
		||||
            // 重置临时字节块并准备下一次使用。
 | 
			
		||||
            this.m_tempByteBlock = default;
 | 
			
		||||
 | 
			
		||||
            // 回到字节块的起始位置。
 | 
			
		||||
            block.SeekToStart();
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                // 尝试解析字节块。
 | 
			
		||||
                var filterResult = this.Single(ref block, out request);
 | 
			
		||||
                switch (filterResult)
 | 
			
		||||
                {
 | 
			
		||||
                    case FilterResult.Cache:
 | 
			
		||||
                        {
 | 
			
		||||
                            // 如果临时字节块不为空,则继续缓存。
 | 
			
		||||
                            if (!this.m_tempByteBlock.IsEmpty)
 | 
			
		||||
                            {
 | 
			
		||||
                                reader.Position += this.m_tempByteBlock.Length;
 | 
			
		||||
                            }
 | 
			
		||||
                            return false;
 | 
			
		||||
                        }
 | 
			
		||||
                    case FilterResult.Success:
 | 
			
		||||
                        {
 | 
			
		||||
                            // 如果字节块中还有剩余数据,则回退指针。
 | 
			
		||||
                            if (block.CanReadLength > 0)
 | 
			
		||||
                            {
 | 
			
		||||
                                reader.Position -= block.CanReadLength;
 | 
			
		||||
                            }
 | 
			
		||||
                            return true;
 | 
			
		||||
                        }
 | 
			
		||||
                    case FilterResult.GoOn:
 | 
			
		||||
                    default:
 | 
			
		||||
                        // 对于需要继续解析的情况,也回退指针。
 | 
			
		||||
                        if (block.CanReadLength > 0)
 | 
			
		||||
                        {
 | 
			
		||||
                            reader.Position -= block.CanReadLength;
 | 
			
		||||
                        }
 | 
			
		||||
                        return false;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
                // 释放字节块资源。
 | 
			
		||||
                block.Dispose();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 筛选解析数据。实例化的TRequest会一直保存,直至解析成功,或手动清除。
 | 
			
		||||
    /// <para>当不满足解析条件时,请返回<see cref="FilterResult.Cache"/>,此时会保存<see cref="ByteBlock.CanReadLength"/>的数据</para>
 | 
			
		||||
    /// <para>当数据部分异常时,请移动<see cref="ByteBlock.Position"/>到指定位置,然后返回<see cref="FilterResult.GoOn"/></para>
 | 
			
		||||
    /// <para>当完全满足解析条件时,请返回<see cref="FilterResult.Success"/>最后将<see cref="ByteBlock.Position"/>移至指定位置。</para>
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="reader">字节块</param>
 | 
			
		||||
    /// <param name="beCached">是否为上次遗留对象,当该参数为<see langword="true"/>时,request也将是上次实例化的对象。</param>
 | 
			
		||||
    /// <param name="request">对象。</param>
 | 
			
		||||
    /// <param name="tempCapacity">缓存容量。当需要首次缓存时,指示申请的ByteBlock的容量。合理的值可避免ByteBlock扩容带来的性能消耗。</param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    protected abstract FilterResult Filter<TByteBlock>(ref TByteBlock reader, bool beCached, ref TRequest request, ref int tempCapacity)
 | 
			
		||||
        where TByteBlock : IByteBlockReader;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 判断请求对象是否应该被缓存。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="request">请求对象。</param>
 | 
			
		||||
    /// <returns>返回布尔值,指示请求对象是否应该被缓存。</returns>
 | 
			
		||||
    protected virtual bool IsBeCached(in TRequest request)
 | 
			
		||||
    {
 | 
			
		||||
        return this.m_requestType.IsValueType ? request.GetHashCode() != default(TRequest).GetHashCode() : request != null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 成功执行接收以后。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="request"></param>
 | 
			
		||||
    protected virtual void OnReceivedSuccess(TRequest request)
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 即将执行<see cref="SingleStreamDataHandlingAdapter.GoReceivedAsync(IByteBlockReader, IRequestInfo)"/>。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="request"></param>
 | 
			
		||||
    /// <returns>返回值标识是否继续执行</returns>
 | 
			
		||||
    protected virtual bool OnReceivingSuccess(TRequest request)
 | 
			
		||||
    {
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    /// <param name="byteBlock"></param>
 | 
			
		||||
    protected override async Task PreviewReceivedAsync(IByteBlockReader byteBlock)
 | 
			
		||||
    {
 | 
			
		||||
        if (this.CacheTimeoutEnable && DateTimeOffset.UtcNow - this.LastCacheTime > this.CacheTimeout)
 | 
			
		||||
        {
 | 
			
		||||
            this.Reset();
 | 
			
		||||
        }
 | 
			
		||||
        if (this.m_tempByteBlock.IsEmpty)
 | 
			
		||||
        {
 | 
			
		||||
            await this.SingleAsync(byteBlock).ConfigureAwait(EasyTask.ContinueOnCapturedContext);
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            this.m_tempByteBlock.Write(byteBlock.Span);
 | 
			
		||||
            using (var block = this.m_tempByteBlock)
 | 
			
		||||
            {
 | 
			
		||||
                this.m_tempByteBlock = default;
 | 
			
		||||
                await this.SingleAsync(block).ConfigureAwait(EasyTask.ContinueOnCapturedContext);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    protected override void Reset()
 | 
			
		||||
    {
 | 
			
		||||
        this.m_tempByteBlock.SafeDispose();
 | 
			
		||||
        this.m_tempByteBlock = default;
 | 
			
		||||
        this.m_tempRequest = default;
 | 
			
		||||
        this.SurLength = 0;
 | 
			
		||||
        base.Reset();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 处理单个字节块,提取请求对象。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <typeparam name="TByteBlock">字节块类型,需要实现IByteBlock接口。</typeparam>
 | 
			
		||||
    /// <param name="reader">字节块,将被解析以提取请求对象。</param>
 | 
			
		||||
    /// <param name="request">输出参数,提取出的请求对象。</param>
 | 
			
		||||
    /// <returns>返回过滤结果,指示处理的状态。</returns>
 | 
			
		||||
    protected FilterResult Single<TByteBlock>(ref TByteBlock reader, out TRequest request) where TByteBlock : IByteBlockReader
 | 
			
		||||
    {
 | 
			
		||||
        // 初始化临时缓存容量。
 | 
			
		||||
        var tempCapacity = 1024 * 64;
 | 
			
		||||
        // 执行过滤操作,根据是否应该缓存来决定如何处理字节块和请求对象。
 | 
			
		||||
        var filterResult = this.Filter(ref reader, this.IsBeCached(this.m_tempRequest), ref this.m_tempRequest, ref tempCapacity);
 | 
			
		||||
        switch (filterResult)
 | 
			
		||||
        {
 | 
			
		||||
            case FilterResult.Success:
 | 
			
		||||
                // 如果过滤结果是成功,则设置请求对象并重置临时请求对象为默认值。
 | 
			
		||||
                request = this.m_tempRequest;
 | 
			
		||||
                this.m_tempRequest = default;
 | 
			
		||||
                return filterResult;
 | 
			
		||||
 | 
			
		||||
            case FilterResult.Cache:
 | 
			
		||||
                // 如果过滤结果需要缓存,则创建一个新的字节块来缓存数据。
 | 
			
		||||
                if (reader.CanReadLength > 0)
 | 
			
		||||
                {
 | 
			
		||||
                    this.m_tempByteBlock = new ValueByteBlock(tempCapacity);
 | 
			
		||||
                    this.m_tempByteBlock.Write(reader.Span.Slice(reader.Position, reader.CanReadLength));
 | 
			
		||||
 | 
			
		||||
                    // 如果缓存的数据长度超过设定的最大包大小,则抛出异常。
 | 
			
		||||
                    if (this.m_tempByteBlock.Length > this.MaxPackageSize)
 | 
			
		||||
                    {
 | 
			
		||||
                        throw new Exception($"The parsed signal was not received when the cached data length {m_tempByteBlock.Length} exceeds the set value {MaxPackageSize}");
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // 将字节块指针移到末尾。
 | 
			
		||||
                    reader.Advance((int)reader.BytesRemaining);
 | 
			
		||||
                }
 | 
			
		||||
                // 更新缓存时间。
 | 
			
		||||
                if (this.UpdateCacheTimeWhenRev)
 | 
			
		||||
                {
 | 
			
		||||
                    this.LastCacheTime = DateTimeOffset.UtcNow;
 | 
			
		||||
                }
 | 
			
		||||
                request = default;
 | 
			
		||||
                return filterResult;
 | 
			
		||||
 | 
			
		||||
            case FilterResult.GoOn:
 | 
			
		||||
            default:
 | 
			
		||||
                // 对于继续或默认的过滤结果,更新缓存时间。
 | 
			
		||||
                if (this.UpdateCacheTimeWhenRev)
 | 
			
		||||
                {
 | 
			
		||||
                    this.LastCacheTime = DateTimeOffset.UtcNow;
 | 
			
		||||
                }
 | 
			
		||||
                request = default;
 | 
			
		||||
                return filterResult;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task SingleAsync<TByteBlock>(TByteBlock reader) where TByteBlock : IByteBlockReader
 | 
			
		||||
    {
 | 
			
		||||
        reader.Position = 0;
 | 
			
		||||
        while (reader.Position < reader.Length)
 | 
			
		||||
        {
 | 
			
		||||
            if (this.DisposedValue)
 | 
			
		||||
            {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            var tempCapacity = 1024 * 64;
 | 
			
		||||
            var filterResult = this.Filter(ref reader, this.IsBeCached(this.m_tempRequest), ref this.m_tempRequest, ref tempCapacity);
 | 
			
		||||
 | 
			
		||||
            switch (filterResult)
 | 
			
		||||
            {
 | 
			
		||||
                case FilterResult.Success:
 | 
			
		||||
                    if (this.OnReceivingSuccess(this.m_tempRequest))
 | 
			
		||||
                    {
 | 
			
		||||
                        await this.GoReceivedAsync(null, this.m_tempRequest).ConfigureAwait(EasyTask.ContinueOnCapturedContext);
 | 
			
		||||
                        this.OnReceivedSuccess(this.m_tempRequest);
 | 
			
		||||
                    }
 | 
			
		||||
                    this.m_tempRequest = default;
 | 
			
		||||
                    break;
 | 
			
		||||
 | 
			
		||||
                case FilterResult.Cache:
 | 
			
		||||
                    this.m_tempByteBlock = new ValueByteBlock(tempCapacity);
 | 
			
		||||
                    this.m_tempByteBlock.Write(reader.Span);
 | 
			
		||||
 | 
			
		||||
                    if (this.m_tempByteBlock.Length > this.MaxPackageSize)
 | 
			
		||||
                    {
 | 
			
		||||
                        this.OnError(default, $"The parsed signal was not received when the cached data length {m_tempByteBlock.Length} exceeds the set value {MaxPackageSize}", true, true);
 | 
			
		||||
                    }
 | 
			
		||||
                    if (this.UpdateCacheTimeWhenRev)
 | 
			
		||||
                    {
 | 
			
		||||
                        this.LastCacheTime = DateTimeOffset.UtcNow;
 | 
			
		||||
                    }
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                case FilterResult.GoOn:
 | 
			
		||||
                    if (this.UpdateCacheTimeWhenRev)
 | 
			
		||||
                    {
 | 
			
		||||
                        this.LastCacheTime = DateTimeOffset.UtcNow;
 | 
			
		||||
                    }
 | 
			
		||||
                    break;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -214,10 +214,7 @@ public abstract class DeviceBase : AsyncAndSyncDisposableObject, IDevice
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    protected virtual ValueTask<bool> ChannelStarting(IClientChannel channel, bool last)
 | 
			
		||||
    {
 | 
			
		||||
        if (channel.ReadOnlyDataHandlingAdapter != null)
 | 
			
		||||
        {
 | 
			
		||||
            channel.ReadOnlyDataHandlingAdapter.Logger = Logger;
 | 
			
		||||
        }
 | 
			
		||||
        channel.SetDataHandlingAdapterLogger(Logger);
 | 
			
		||||
        return EasyValueTask.FromResult(true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -318,7 +315,7 @@ public abstract class DeviceBase : AsyncAndSyncDisposableObject, IDevice
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (client.WaitHandlePool.SetRun(response))
 | 
			
		||||
                if (client.WaitHandlePool.Set(response))
 | 
			
		||||
                {
 | 
			
		||||
                    e.Handled = true;
 | 
			
		||||
                }
 | 
			
		||||
@@ -366,21 +363,13 @@ public abstract class DeviceBase : AsyncAndSyncDisposableObject, IDevice
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async ValueTask BefortSendAsync(IClientChannel channel, CancellationToken token)
 | 
			
		||||
    private Task BefortSendAsync(IClientChannel channel, CancellationToken token)
 | 
			
		||||
    {
 | 
			
		||||
        SetDataAdapter(channel);
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            await ConnectAsync(token).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception)
 | 
			
		||||
        {
 | 
			
		||||
            await Task.Delay(1000, token).ConfigureAwait(false);
 | 
			
		||||
            throw;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (token.IsCancellationRequested)
 | 
			
		||||
            throw new OperationCanceledException();
 | 
			
		||||
        return ConnectAsync(token);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private WaitLock connectWaitLock = new(nameof(DeviceBase));
 | 
			
		||||
@@ -397,13 +386,13 @@ public abstract class DeviceBase : AsyncAndSyncDisposableObject, IDevice
 | 
			
		||||
                    if (Channel.PluginManager == null)
 | 
			
		||||
                        await Channel.SetupAsync(Channel.Config.Clone()).ConfigureAwait(false);
 | 
			
		||||
                    await Channel.CloseAsync().ConfigureAwait(false);
 | 
			
		||||
                    await Task.Delay(500, token).ConfigureAwait(false);
 | 
			
		||||
                    await Channel.ConnectAsync(Channel.ChannelOptions.ConnectTimeout, token).ConfigureAwait(false);
 | 
			
		||||
                    using var ctsTime = new CancellationTokenSource(Channel.ChannelOptions.ConnectTimeout);
 | 
			
		||||
                    using var cts = CancellationTokenSource.CreateLinkedTokenSource(ctsTime.Token, token);
 | 
			
		||||
                    await Channel.ConnectAsync(cts.Token).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
                try { await Task.Delay(500, token).ConfigureAwait(false); } catch { }
 | 
			
		||||
                connectWaitLock.Release();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
@@ -424,8 +413,7 @@ public abstract class DeviceBase : AsyncAndSyncDisposableObject, IDevice
 | 
			
		||||
                await BefortSendAsync(channelResult.Content, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                await waitLock.WaitAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                if (channelResult.Content.ReadOnlyDataHandlingAdapter != null)
 | 
			
		||||
                    channelResult.Content.ReadOnlyDataHandlingAdapter.Logger = Logger;
 | 
			
		||||
                channelResult.Content.SetDataHandlingAdapterLogger(Logger);
 | 
			
		||||
 | 
			
		||||
                EndPoint? endPoint = GetUdpEndpoint(dtuId);
 | 
			
		||||
 | 
			
		||||
@@ -438,6 +426,8 @@ public abstract class DeviceBase : AsyncAndSyncDisposableObject, IDevice
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            if (!cancellationToken.IsCancellationRequested)
 | 
			
		||||
                await Task.Delay(1000, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
            return new(ex);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@@ -513,8 +503,6 @@ public abstract class DeviceBase : AsyncAndSyncDisposableObject, IDevice
 | 
			
		||||
        return SendThenReturnAsync(sendMessage, channelResult.Content, cancellationToken);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public virtual async ValueTask<OperResult<ReadOnlyMemory<byte>>> SendThenReturnAsync(ISendMessage sendMessage, IClientChannel channel, CancellationToken cancellationToken = default)
 | 
			
		||||
    {
 | 
			
		||||
@@ -530,11 +518,11 @@ public abstract class DeviceBase : AsyncAndSyncDisposableObject, IDevice
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    protected virtual async ValueTask<MessageBase> SendThenReturnMessageAsync(ISendMessage sendMessage, CancellationToken cancellationToken = default)
 | 
			
		||||
    protected virtual ValueTask<MessageBase> SendThenReturnMessageAsync(ISendMessage sendMessage, CancellationToken cancellationToken = default)
 | 
			
		||||
    {
 | 
			
		||||
        var channelResult = GetChannel(this is IDtu dtu ? dtu.DtuId : null);
 | 
			
		||||
        if (!channelResult.IsSuccess) return new MessageBase(channelResult);
 | 
			
		||||
        return await SendThenReturnMessageAsync(sendMessage, channelResult.Content, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        if (!channelResult.IsSuccess) return EasyValueTask.FromResult(new MessageBase(channelResult));
 | 
			
		||||
        return SendThenReturnMessageAsync(sendMessage, channelResult.Content, cancellationToken);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
@@ -565,10 +553,8 @@ public abstract class DeviceBase : AsyncAndSyncDisposableObject, IDevice
 | 
			
		||||
            if (cancellationToken.IsCancellationRequested)
 | 
			
		||||
                return new MessageBase(new OperationCanceledException());
 | 
			
		||||
 | 
			
		||||
            if (clientChannel.ReadOnlyDataHandlingAdapter != null)
 | 
			
		||||
                clientChannel.ReadOnlyDataHandlingAdapter.Logger = Logger;
 | 
			
		||||
            clientChannel.SetDataHandlingAdapterLogger(Logger);
 | 
			
		||||
 | 
			
		||||
            Channel.ChannelReceivedWaitDict.TryAdd(sign, ChannelReceived);
 | 
			
		||||
 | 
			
		||||
            if (cancellationToken.IsCancellationRequested)
 | 
			
		||||
                return new MessageBase(new OperationCanceledException());
 | 
			
		||||
@@ -577,20 +563,29 @@ public abstract class DeviceBase : AsyncAndSyncDisposableObject, IDevice
 | 
			
		||||
            if (!sendOperResult.IsSuccess)
 | 
			
		||||
                return new MessageBase(sendOperResult);
 | 
			
		||||
 | 
			
		||||
            using var ctsTime = new CancellationTokenSource(timeout);
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                waitData.SetCancellationToken(Channel.ClosedToken);
 | 
			
		||||
 | 
			
		||||
                await waitData.WaitAsync(timeout).ConfigureAwait(false);
 | 
			
		||||
                using var cts = CancellationTokenSource.CreateLinkedTokenSource(ctsTime.Token, Channel.ClosedToken);
 | 
			
		||||
                await waitData.WaitAsync(cts.Token).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
            catch (OperationCanceledException)
 | 
			
		||||
            {
 | 
			
		||||
                if (ctsTime.IsCancellationRequested)
 | 
			
		||||
                {
 | 
			
		||||
                    return new MessageBase(new TimeoutException());
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                return new MessageBase(ex);
 | 
			
		||||
            }
 | 
			
		||||
            var result = waitData.Check();
 | 
			
		||||
 | 
			
		||||
            var result = waitData.Check(ctsTime.Token);
 | 
			
		||||
 | 
			
		||||
            if (result.IsSuccess)
 | 
			
		||||
            {
 | 
			
		||||
                return waitData.WaitResult;
 | 
			
		||||
                return waitData.CompletedData;
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
@@ -599,13 +594,14 @@ public abstract class DeviceBase : AsyncAndSyncDisposableObject, IDevice
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            if (!cancellationToken.IsCancellationRequested)
 | 
			
		||||
                await Task.Delay(1000, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
            return new MessageBase(ex);
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            Channel.ChannelReceivedWaitDict.TryRemove(sign, out _);
 | 
			
		||||
            waitLock?.Release();
 | 
			
		||||
            clientChannel.WaitHandlePool.Destroy(sign);
 | 
			
		||||
            waitData?.SafeDispose();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -964,10 +960,10 @@ public abstract class DeviceBase : AsyncAndSyncDisposableObject, IDevice
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public virtual async ValueTask<OperResult> WriteAsync(string address, ReadOnlyMemory<string> value, IThingsGatewayBitConverter bitConverter = null, CancellationToken cancellationToken = default)
 | 
			
		||||
    public virtual ValueTask<OperResult> WriteAsync(string address, ReadOnlyMemory<string> value, IThingsGatewayBitConverter bitConverter = null, CancellationToken cancellationToken = default)
 | 
			
		||||
    {
 | 
			
		||||
        bitConverter ??= ThingsGatewayBitConverter.GetTransByAddress(address);
 | 
			
		||||
        if (bitConverter.StringLength == null) return new OperResult(AppResource.StringAddressError);
 | 
			
		||||
        if (bitConverter.StringLength == null) return EasyValueTask.FromResult(new OperResult(AppResource.StringAddressError));
 | 
			
		||||
        List<ReadOnlyMemory<byte>> bytes = new();
 | 
			
		||||
        foreach (var a in value.Span)
 | 
			
		||||
        {
 | 
			
		||||
@@ -975,7 +971,7 @@ public abstract class DeviceBase : AsyncAndSyncDisposableObject, IDevice
 | 
			
		||||
            bytes.Add(data.ArrayExpandToLength(bitConverter.StringLength ?? data.Length));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return await WriteAsync(address, bytes.CombineMemoryBlocks(), DataTypeEnum.String, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        return WriteAsync(address, bytes.CombineMemoryBlocks(), DataTypeEnum.String, cancellationToken);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #endregion 写入数组
 | 
			
		||||
@@ -997,7 +993,7 @@ public abstract class DeviceBase : AsyncAndSyncDisposableObject, IDevice
 | 
			
		||||
                {
 | 
			
		||||
                    if (Channel is ITcpServiceChannel tcpServiceChannel)
 | 
			
		||||
                    {
 | 
			
		||||
                        tcpServiceChannel.Clients.ForEach(a => a.WaitHandlePool.SafeDispose());
 | 
			
		||||
                        tcpServiceChannel.Clients.ForEach(a => a.WaitHandlePool?.CancelAll());
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    try
 | 
			
		||||
@@ -1006,7 +1002,7 @@ public abstract class DeviceBase : AsyncAndSyncDisposableObject, IDevice
 | 
			
		||||
                        _ = Channel.CloseAsync();
 | 
			
		||||
                        if (Channel is IClientChannel client)
 | 
			
		||||
                        {
 | 
			
		||||
                            client.WaitHandlePool.SafeDispose();
 | 
			
		||||
                            client.WaitHandlePool?.CancelAll();
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    catch (Exception ex)
 | 
			
		||||
@@ -1020,7 +1016,7 @@ public abstract class DeviceBase : AsyncAndSyncDisposableObject, IDevice
 | 
			
		||||
                    {
 | 
			
		||||
                        if (tcpServiceChannel.TryGetClient($"ID={dtu.DtuId}", out var client))
 | 
			
		||||
                        {
 | 
			
		||||
                            client.WaitHandlePool?.SafeDispose();
 | 
			
		||||
                            client.WaitHandlePool?.CancelAll();
 | 
			
		||||
                            _ = client.CloseAsync();
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
@@ -1049,7 +1045,7 @@ public abstract class DeviceBase : AsyncAndSyncDisposableObject, IDevice
 | 
			
		||||
            {
 | 
			
		||||
                if (Channel is ITcpServiceChannel tcpServiceChannel)
 | 
			
		||||
                {
 | 
			
		||||
                    tcpServiceChannel.Clients.ForEach(a => a.WaitHandlePool.SafeDispose());
 | 
			
		||||
                    tcpServiceChannel.Clients.ForEach(a => a.WaitHandlePool?.CancelAll());
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                try
 | 
			
		||||
@@ -1058,7 +1054,7 @@ public abstract class DeviceBase : AsyncAndSyncDisposableObject, IDevice
 | 
			
		||||
                    await Channel.CloseAsync().ConfigureAwait(false);
 | 
			
		||||
                    if (Channel is IClientChannel client)
 | 
			
		||||
                    {
 | 
			
		||||
                        client.WaitHandlePool.SafeDispose();
 | 
			
		||||
                        client.WaitHandlePool?.CancelAll();
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception ex)
 | 
			
		||||
@@ -1072,7 +1068,7 @@ public abstract class DeviceBase : AsyncAndSyncDisposableObject, IDevice
 | 
			
		||||
                {
 | 
			
		||||
                    if (tcpServiceChannel.TryGetClient($"ID={dtu.DtuId}", out var client))
 | 
			
		||||
                    {
 | 
			
		||||
                        client.WaitHandlePool?.SafeDispose();
 | 
			
		||||
                        client.WaitHandlePool?.CancelAll();
 | 
			
		||||
                        await client.CloseAsync().ConfigureAwait(false);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 
 | 
			
		||||
@@ -132,24 +132,43 @@ public static partial class DeviceExtension
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 当状态不是<see cref="WaitDataStatus.SetRunning"/>时返回异常。
 | 
			
		||||
    /// 当状态不是<see cref="WaitDataStatus.Success"/>时返回异常。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static OperResult Check(this WaitDataAsync<MessageBase> waitDataAsync)
 | 
			
		||||
    public static OperResult Check(this AsyncWaitData<MessageBase> waitDataAsync, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        switch (waitDataAsync.Status)
 | 
			
		||||
        {
 | 
			
		||||
            case WaitDataStatus.SetRunning:
 | 
			
		||||
            case WaitDataStatus.Success:
 | 
			
		||||
                return new();
 | 
			
		||||
 | 
			
		||||
            case WaitDataStatus.Canceled: return new(new OperationCanceledException());
 | 
			
		||||
            case WaitDataStatus.Canceled:
 | 
			
		||||
                if (cancellationToken.IsCancellationRequested)
 | 
			
		||||
                {
 | 
			
		||||
                    if (waitDataAsync.CompletedData != null)
 | 
			
		||||
                    {
 | 
			
		||||
                        waitDataAsync.CompletedData.Exception = new TimeoutException();
 | 
			
		||||
                        if (waitDataAsync.CompletedData.IsSuccess) waitDataAsync.CompletedData.OperCode = 999;
 | 
			
		||||
                        if (waitDataAsync.CompletedData.ErrorMessage.IsNullOrEmpty()) waitDataAsync.CompletedData.ErrorMessage = "Timeout";
 | 
			
		||||
                        return new(waitDataAsync.CompletedData);
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        return new(new TimeoutException());
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
 | 
			
		||||
                    return new(new OperationCanceledException());
 | 
			
		||||
                }
 | 
			
		||||
            case WaitDataStatus.Overtime:
 | 
			
		||||
                {
 | 
			
		||||
                    if (waitDataAsync.WaitResult != null)
 | 
			
		||||
                    if (waitDataAsync.CompletedData != null)
 | 
			
		||||
                    {
 | 
			
		||||
                        waitDataAsync.WaitResult.Exception = new TimeoutException();
 | 
			
		||||
                        if (waitDataAsync.WaitResult.IsSuccess) waitDataAsync.WaitResult.OperCode = 999;
 | 
			
		||||
                        if (waitDataAsync.WaitResult.ErrorMessage.IsNullOrEmpty()) waitDataAsync.WaitResult.ErrorMessage = "Timeout";
 | 
			
		||||
                        return new(waitDataAsync.WaitResult);
 | 
			
		||||
                        waitDataAsync.CompletedData.Exception = new TimeoutException();
 | 
			
		||||
                        if (waitDataAsync.CompletedData.IsSuccess) waitDataAsync.CompletedData.OperCode = 999;
 | 
			
		||||
                        if (waitDataAsync.CompletedData.ErrorMessage.IsNullOrEmpty()) waitDataAsync.CompletedData.ErrorMessage = "Timeout";
 | 
			
		||||
                        return new(waitDataAsync.CompletedData);
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
@@ -160,7 +179,7 @@ public static partial class DeviceExtension
 | 
			
		||||
            case WaitDataStatus.Default:
 | 
			
		||||
            default:
 | 
			
		||||
                {
 | 
			
		||||
                    return waitDataAsync.WaitResult == null ? new(new Exception(AppResource.UnknownError)) : new(waitDataAsync.WaitResult);
 | 
			
		||||
                    return waitDataAsync.CompletedData == null ? new(new Exception(AppResource.UnknownError)) : new(waitDataAsync.CompletedData);
 | 
			
		||||
                }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using System.Buffers;
 | 
			
		||||
using System.Runtime.CompilerServices;
 | 
			
		||||
using System.Text;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Foundation;
 | 
			
		||||
@@ -20,70 +21,8 @@ namespace ThingsGateway.Foundation;
 | 
			
		||||
/// </summary>
 | 
			
		||||
public static class ByteBlockExtension
 | 
			
		||||
{
 | 
			
		||||
    public static void WriteBackAddValue<TWriter>(ref TWriter writer, byte value, int pos)
 | 
			
		||||
        where TWriter : IByteBlockWriter
 | 
			
		||||
    {
 | 
			
		||||
        int nowPos = writer.Position;
 | 
			
		||||
        writer.Position = pos;
 | 
			
		||||
        WriterExtension.WriteValue(ref writer, (byte)(writer.Span[pos] + value));
 | 
			
		||||
        writer.Position = nowPos;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public static void WriteBackValue<TWriter, T>(ref TWriter writer, T value, int pos)
 | 
			
		||||
        where T : unmanaged
 | 
			
		||||
        where TWriter : IByteBlockWriter
 | 
			
		||||
    {
 | 
			
		||||
        int nowPos = writer.Position;
 | 
			
		||||
        writer.Position = pos;
 | 
			
		||||
        WriterExtension.WriteValue(ref writer, value);
 | 
			
		||||
        writer.Position = nowPos;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void WriteBackValue<TWriter, T>(ref TWriter writer, T value, EndianType endianType, int pos)
 | 
			
		||||
        where T : unmanaged
 | 
			
		||||
        where TWriter : IByteBlockWriter
 | 
			
		||||
    {
 | 
			
		||||
        int nowPos = writer.Position;
 | 
			
		||||
        writer.Position = pos;
 | 
			
		||||
        WriterExtension.WriteValue(ref writer, value, endianType);
 | 
			
		||||
        writer.Position = nowPos;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void WriteBackNormalString<TWriter>(ref TWriter writer, string value, Encoding encoding, int pos)
 | 
			
		||||
    where TWriter : IByteBlockWriter
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
        int nowPos = writer.Position;
 | 
			
		||||
        writer.Position = pos;
 | 
			
		||||
        WriterExtension.WriteNormalString(ref writer, value, encoding);
 | 
			
		||||
        writer.Position = nowPos;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public static string ReadNormalString<TReader>(ref TReader reader, int length)
 | 
			
		||||
        where TReader : IBytesReader
 | 
			
		||||
    {
 | 
			
		||||
        var span = reader.GetSpan(length).Slice(0, length);
 | 
			
		||||
        var str = span.ToString(Encoding.UTF8);
 | 
			
		||||
        reader.Advance(length);
 | 
			
		||||
        return str;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 将值类型的字节块转换为普通的字节块。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="valueByteBlock">要转换的值类型字节块。</param>
 | 
			
		||||
    /// <returns>一个新的字节块对象。</returns>
 | 
			
		||||
    public static ByteBlock AsByteBlock(this ValueByteBlock valueByteBlock)
 | 
			
		||||
    {
 | 
			
		||||
        ByteBlock byteBlock = new ByteBlock(valueByteBlock.TotalMemory.Slice(0, valueByteBlock.Length));
 | 
			
		||||
        byteBlock.Position = valueByteBlock.Position;
 | 
			
		||||
        byteBlock.SetLength(valueByteBlock.Length);
 | 
			
		||||
        return byteBlock;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #region ToArray
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -259,5 +198,252 @@ public static class ByteBlockExtension
 | 
			
		||||
        return ToString(byteBlock, offset, byteBlock.BytesRead + byteBlock.BytesRemaining - offset);
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public static string ToString<TByteBlock>(this TByteBlock byteBlock) where TByteBlock : IBytesReader
 | 
			
		||||
    {
 | 
			
		||||
        return byteBlock.TotalSequence.ToString(Encoding.UTF8);
 | 
			
		||||
    }
 | 
			
		||||
    #endregion ToString
 | 
			
		||||
 | 
			
		||||
    #region ToHexString
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public static string ToHexString<TByteBlock>(this TByteBlock byteBlock, long offset, long length, char splite = default) where TByteBlock : IBytesReader
 | 
			
		||||
    {
 | 
			
		||||
        return byteBlock.TotalSequence.Slice(offset, length).ToHexString(Encoding.UTF8, splite);
 | 
			
		||||
    }
 | 
			
		||||
    public static string ToHexString(this ReadOnlySequence<byte> byteBlock, Encoding encoding, char splite = default)
 | 
			
		||||
    {
 | 
			
		||||
        return byteBlock.ToHexString(splite);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public static string ToHexString<TByteBlock>(this TByteBlock byteBlock, long offset, char splite = default) where TByteBlock : IBytesReader
 | 
			
		||||
    {
 | 
			
		||||
        return ToHexString(byteBlock, offset, byteBlock.BytesRead + byteBlock.BytesRemaining - offset, splite);
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public static string ToHexString<TByteBlock>(this TByteBlock byteBlock, char splite = default) where TByteBlock : IBytesReader
 | 
			
		||||
    {
 | 
			
		||||
        return byteBlock.TotalSequence.ToHexString(Encoding.UTF8, splite);
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #endregion ToHexString
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 在 <see cref="ReadOnlySequence{T}"/> 中查找第一个与指定 <see cref="byte"/> 匹配的子序列的起始索引。
 | 
			
		||||
    /// <para>如果未找到则返回 -1。</para>
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="sequence">要搜索的字节序列。</param>
 | 
			
		||||
    /// <param name="firstByte">要查找的字节。</param>
 | 
			
		||||
    /// <returns>匹配子序列的起始索引,未找到则返回 -1。</returns>
 | 
			
		||||
    public static long IndexOf(this ReadOnlySequence<byte> sequence, byte firstByte)
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
        if (sequence.Length < 1)
 | 
			
		||||
        {
 | 
			
		||||
            return -1;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        long globalPosition = 0;
 | 
			
		||||
        var enumerator = sequence.GetEnumerator();
 | 
			
		||||
 | 
			
		||||
        // 遍历每个内存段
 | 
			
		||||
        while (enumerator.MoveNext())
 | 
			
		||||
        {
 | 
			
		||||
            var currentSpan = enumerator.Current.Span;
 | 
			
		||||
            var localIndex = 0;
 | 
			
		||||
 | 
			
		||||
            // 在当前段中搜索首字节
 | 
			
		||||
            while (localIndex < currentSpan.Length)
 | 
			
		||||
            {
 | 
			
		||||
                // 查找首字节匹配位置
 | 
			
		||||
                var matchIndex = currentSpan.Slice(localIndex).IndexOf(firstByte);
 | 
			
		||||
                if (matchIndex == -1)
 | 
			
		||||
                {
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                localIndex += matchIndex;
 | 
			
		||||
                var globalIndex = globalPosition + localIndex;
 | 
			
		||||
 | 
			
		||||
                // 检查剩余长度是否足够
 | 
			
		||||
                if (sequence.Length - globalIndex < 1)
 | 
			
		||||
                {
 | 
			
		||||
                    return -1;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // 检查完整匹配
 | 
			
		||||
                if (IsMatch(sequence, globalIndex, firstByte))
 | 
			
		||||
                {
 | 
			
		||||
                    return globalIndex;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                localIndex++; // 继续搜索下一个位置
 | 
			
		||||
            }
 | 
			
		||||
            globalPosition += currentSpan.Length;
 | 
			
		||||
        }
 | 
			
		||||
        return -1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static bool IsMatch(ReadOnlySequence<byte> sequence, long start, byte value)
 | 
			
		||||
    {
 | 
			
		||||
        return sequence.Slice(start, 1).First.Span[0] == value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public static byte GetByte(this ReadOnlySequence<byte> sequence, long index)
 | 
			
		||||
    {
 | 
			
		||||
        return sequence.Slice(index).First.Span[0];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 计算指定区间字节的和
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static long SumRange(this ReadOnlySequence<byte> sequence, long start, long count)
 | 
			
		||||
    {
 | 
			
		||||
        if (start < 0 || count < 0 || start + count > sequence.Length)
 | 
			
		||||
            throw new ArgumentOutOfRangeException();
 | 
			
		||||
 | 
			
		||||
        long sum = 0;
 | 
			
		||||
        long remaining = count;
 | 
			
		||||
 | 
			
		||||
        foreach (var segment in sequence)
 | 
			
		||||
        {
 | 
			
		||||
            if (start >= segment.Length)
 | 
			
		||||
            {
 | 
			
		||||
                // 起点不在当前段
 | 
			
		||||
                start -= segment.Length;
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var span = segment.Span;
 | 
			
		||||
            int take = (int)Math.Min(segment.Length - start, remaining);
 | 
			
		||||
 | 
			
		||||
            for (int i = 0; i < take; i++)
 | 
			
		||||
                sum += span[(int)start + i];
 | 
			
		||||
 | 
			
		||||
            remaining -= take;
 | 
			
		||||
            start = 0; // 后续段从头开始
 | 
			
		||||
 | 
			
		||||
            if (remaining == 0)
 | 
			
		||||
                break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return sum;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 比较 ReadOnlySequence 与 ReadOnlySpan 的内容是否一致。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static bool SequenceEqual<T>(this ReadOnlySequence<T> sequence, ReadOnlySpan<T> other)
 | 
			
		||||
        where T : IEquatable<T>
 | 
			
		||||
    {
 | 
			
		||||
        if (sequence.Length != other.Length)
 | 
			
		||||
            return false;
 | 
			
		||||
 | 
			
		||||
        // 单段,直接比较
 | 
			
		||||
        if (sequence.IsSingleSegment)
 | 
			
		||||
        {
 | 
			
		||||
            return sequence.First.Span.SequenceEqual(other);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 多段,逐段比较
 | 
			
		||||
        int offset = 0;
 | 
			
		||||
        foreach (var segment in sequence)
 | 
			
		||||
        {
 | 
			
		||||
            var span = segment.Span;
 | 
			
		||||
            if (!other.Slice(offset, span.Length).SequenceEqual(span))
 | 
			
		||||
                return false;
 | 
			
		||||
            offset += span.Length;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 将指定的字节块从当前位置<see cref="IByteBlockCore.Position"/>转换为【新】字节数组,指定长度。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <typeparam name="TByteBlock">实现<see cref="IByteBlock"/>接口的字节块类型。</typeparam>
 | 
			
		||||
    /// <param name="byteBlock">字节块对象。</param>
 | 
			
		||||
    /// <param name="length">要转换为数组的长度。</param>
 | 
			
		||||
    /// <returns>从当前位置开始,指定长度的【新】字节数组。</returns>
 | 
			
		||||
    public static byte[] ToArrayTake<TByteBlock>(this TByteBlock byteBlock, long length) where TByteBlock : IBytesReader
 | 
			
		||||
    {
 | 
			
		||||
        return byteBlock.Sequence.Slice(0, length).ToArray();
 | 
			
		||||
    }
 | 
			
		||||
    public static void Write<TByteBlock>(ref TByteBlock byteBlock, ReadOnlySequence<byte> bytes) where TByteBlock : IBytesWriter
 | 
			
		||||
    {
 | 
			
		||||
        foreach (var item in bytes)
 | 
			
		||||
        {
 | 
			
		||||
            byteBlock.Write(item.Span);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    public static void WriteBackValue<TWriter, T>(ref TWriter writer, T value, EndianType endianType, int pos)
 | 
			
		||||
    where T : unmanaged
 | 
			
		||||
    where TWriter : IByteBlockWriter
 | 
			
		||||
    {
 | 
			
		||||
        var nowPos = (int)writer.WrittenCount - pos;
 | 
			
		||||
        writer.Advance(-nowPos);
 | 
			
		||||
        var size = Unsafe.SizeOf<T>();
 | 
			
		||||
        var span = writer.GetSpan(size);
 | 
			
		||||
        TouchSocketBitConverter.GetBitConverter(endianType).WriteBytes(span, value);
 | 
			
		||||
        writer.Advance(nowPos);
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
    public static void WriteBackValue<TWriter, T>(ref TWriter writer, T value, EndianType endianType, long pos)
 | 
			
		||||
where T : unmanaged
 | 
			
		||||
where TWriter : IByteBlockWriter
 | 
			
		||||
    {
 | 
			
		||||
        var nowPos = (int)(writer.WrittenCount - pos);
 | 
			
		||||
        writer.Advance(-nowPos);
 | 
			
		||||
        var size = Unsafe.SizeOf<T>();
 | 
			
		||||
        var span = writer.GetSpan(size);
 | 
			
		||||
        TouchSocketBitConverter.GetBitConverter(endianType).WriteBytes(span, value);
 | 
			
		||||
        writer.Advance(nowPos);
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static string ReadNormalString<TReader>(ref TReader reader, int length)
 | 
			
		||||
    where TReader : IBytesReader
 | 
			
		||||
    {
 | 
			
		||||
        var span = reader.GetSpan(length).Slice(0, length);
 | 
			
		||||
        var str = span.ToString(Encoding.UTF8);
 | 
			
		||||
        reader.Advance(length);
 | 
			
		||||
        return str;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void WriteBackNormalString<TWriter>(ref TWriter writer, string value, Encoding encoding, int pos)
 | 
			
		||||
where TWriter : IByteBlockWriter
 | 
			
		||||
    {
 | 
			
		||||
        var nowPos = (int)(writer.WrittenCount - pos);
 | 
			
		||||
        writer.Advance(-nowPos);
 | 
			
		||||
        WriterExtension.WriteNormalString(ref writer, value, encoding);
 | 
			
		||||
        writer.Advance(nowPos);
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public static int WriteNormalString(this Span<byte> span, string value, Encoding encoding)
 | 
			
		||||
    {
 | 
			
		||||
        var maxSize = encoding.GetMaxByteCount(value.Length);
 | 
			
		||||
        var chars = value.AsSpan();
 | 
			
		||||
 | 
			
		||||
        unsafe
 | 
			
		||||
        {
 | 
			
		||||
            fixed (char* p = &chars[0])
 | 
			
		||||
            {
 | 
			
		||||
                fixed (byte* p1 = &span[0])
 | 
			
		||||
                {
 | 
			
		||||
                    var len = Encoding.UTF8.GetBytes(p, chars.Length, p1, maxSize);
 | 
			
		||||
                    return len;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user