NewLife/ZeroIoT

v2.0 引用新一代Remoting,简化IoTZero架构
智能大石头 authored at 2024-06-20 23:03:45
4766d50
Tree
1 Parent(s) a00ae3a
Summary: 27 changed files with 425 additions and 925 deletions.
Modified +3 -5
Modified +3 -21
Modified +12 -5
Modified +8 -3
Modified +28 -26
Modified +23 -23
Modified +15 -15
Modified +29 -30
Modified +16 -15
Deleted +0 -26
IoTCore/Models/UpgradeInfo.cs
Modified +7 -1
Modified +2 -2
Modified +43 -246
Modified +13 -13
Modified +7 -0
Deleted +0 -67
IoTZero/Common/ApiFilterAttribute.cs
Modified +5 -101
Deleted +0 -114
IoTZero/Controllers/BaseController.cs
Modified +24 -99
Modified +47 -17
Modified +2 -1
Modified +8 -7
Modified +35 -31
Modified +5 -3
Added +26 -0
Modified +60 -51
Modified +4 -3
Modified +3 -5
diff --git "a/IoT.Data/Entity/\350\256\276\345\244\207.Biz.cs" "b/IoT.Data/Entity/\350\256\276\345\244\207.Biz.cs"
index add4f96..985e8c6 100644
--- "a/IoT.Data/Entity/\350\256\276\345\244\207.Biz.cs"
+++ "b/IoT.Data/Entity/\350\256\276\345\244\207.Biz.cs"
@@ -1,6 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.ComponentModel;
+using System.ComponentModel;
 using System.Runtime.Serialization;
 using System.Web.Script.Serialization;
 using System.Xml.Serialization;
@@ -10,13 +8,13 @@ using NewLife.Data;
 using NewLife.IoT.Models;
 using NewLife.Log;
 using NewLife.Remoting;
+using NewLife.Remoting.Models;
 using XCode;
 using XCode.Cache;
-using XCode.Membership;
 
 namespace IoT.Data;
 
-public partial class Device : Entity<Device>
+public partial class Device : Entity<Device>, IDeviceModel
 {
     #region 对象操作
     static Device()
Modified +3 -21
diff --git "a/IoT.Data/Entity/\350\256\276\345\244\207\345\234\250\347\272\277.Biz.cs" "b/IoT.Data/Entity/\350\256\276\345\244\207\345\234\250\347\272\277.Biz.cs"
index fdd13af..bec8005 100644
--- "a/IoT.Data/Entity/\350\256\276\345\244\207\345\234\250\347\272\277.Biz.cs"
+++ "b/IoT.Data/Entity/\350\256\276\345\244\207\345\234\250\347\272\277.Biz.cs"
@@ -1,34 +1,16 @@
-using System;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.IO;
-using System.Linq;
-using System.Reflection;
-using System.Runtime.Serialization;
-using System.Text;
-using System.Threading.Tasks;
-using System.Web;
+using System.Runtime.Serialization;
 using System.Web.Script.Serialization;
 using System.Xml.Serialization;
 using NewLife;
 using NewLife.Data;
 using NewLife.IoT.Models;
-using NewLife.Log;
-using NewLife.Model;
-using NewLife.Reflection;
+using NewLife.Remoting.Models;
 using NewLife.Serialization;
-using NewLife.Threading;
-using NewLife.Web;
 using XCode;
-using XCode.Cache;
-using XCode.Configuration;
-using XCode.DataAccessLayer;
-using XCode.Membership;
-using XCode.Shards;
 
 namespace IoT.Data;
 
-public partial class DeviceOnline : Entity<DeviceOnline>
+public partial class DeviceOnline : Entity<DeviceOnline>, IOnlineModel
 {
     #region 对象操作
     static DeviceOnline()
Modified +12 -5
diff --git a/IoT.Data/IoT.Data.csproj b/IoT.Data/IoT.Data.csproj
index 4971ce0..d8e4ba6 100644
--- a/IoT.Data/IoT.Data.csproj
+++ b/IoT.Data/IoT.Data.csproj
@@ -1,17 +1,18 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>netstandard2.1</TargetFramework>
+    <TargetFramework>netstandard2.0</TargetFramework>
     <AssemblyTitle>物联网数据层</AssemblyTitle>
     <Description>IoT数据层</Description>
     <Company>新生命开发团队</Company>
-    <Copyright>©2002-2023 NewLife</Copyright>
-    <VersionPrefix>1.0</VersionPrefix>
+    <Copyright>©2002-2024 NewLife</Copyright>
+    <VersionPrefix>2.0</VersionPrefix>
     <VersionSuffix>$([System.DateTime]::Now.ToString(`yyyy.MMdd`))</VersionSuffix>
     <Version>$(VersionPrefix).$(VersionSuffix)</Version>
     <FileVersion>$(Version)</FileVersion>
     <AssemblyVersion>$(VersionPrefix).*</AssemblyVersion>
     <Deterministic>false</Deterministic>
+    <ImplicitUsings>enable</ImplicitUsings>
     <LangVersion>latest</LangVersion>
     <GenerateDocumentationFile>True</GenerateDocumentationFile>
   </PropertyGroup>
@@ -25,17 +26,23 @@
   </PropertyGroup>
 
   <ItemGroup>
+    <Compile Remove="Config\**" />
     <Compile Remove="Entity\Config\**" />
     <Compile Remove="Entity\Log\**" />
+    <Compile Remove="Log\**" />
+    <EmbeddedResource Remove="Config\**" />
     <EmbeddedResource Remove="Entity\Config\**" />
     <EmbeddedResource Remove="Entity\Log\**" />
+    <EmbeddedResource Remove="Log\**" />
+    <None Remove="Config\**" />
     <None Remove="Entity\Config\**" />
     <None Remove="Entity\Log\**" />
+    <None Remove="Log\**" />
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="NewLife.IoT" Version="1.8.2023.611-beta1629" />
-    <PackageReference Include="NewLife.XCode" Version="11.8.2023.628-beta0652" />
+    <PackageReference Include="NewLife.IoT" Version="2.2.2024.501" />
+    <PackageReference Include="NewLife.XCode" Version="11.13.2024.606" />
   </ItemGroup>
 
   <ItemGroup>
Modified +8 -3
diff --git a/IoTCore/IoTCore.csproj b/IoTCore/IoTCore.csproj
index a12439d..40e9297 100644
--- a/IoTCore/IoTCore.csproj
+++ b/IoTCore/IoTCore.csproj
@@ -5,8 +5,8 @@
     <AssemblyTitle>新生命IoT核心库</AssemblyTitle>
     <Description>IoT框架基础类库</Description>
     <Company>新生命开发团队</Company>
-    <Copyright>©2002-2023 NewLife</Copyright>
-    <VersionPrefix>1.6</VersionPrefix>
+    <Copyright>©2002-2024 NewLife</Copyright>
+    <VersionPrefix>2.0</VersionPrefix>
     <VersionSuffix>$([System.DateTime]::Now.ToString(`yyyy.MMdd`))</VersionSuffix>
     <Version>$(VersionPrefix).$(VersionSuffix)</Version>
     <FileVersion>$(Version)</FileVersion>
@@ -19,7 +19,12 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="NewLife.IoT" Version="1.8.2023.611-beta1629" />
+    <Compile Remove="Models\UpgradeInfo.cs" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <PackageReference Include="NewLife.IoT" Version="2.2.2024.501" />
+    <PackageReference Include="NewLife.Remoting" Version="3.0.2024.620-beta1407" />
   </ItemGroup>
 
 </Project>
Modified +28 -26
diff --git a/IoTCore/Models/LoginInfo.cs b/IoTCore/Models/LoginInfo.cs
index 9b98f11..e97818a 100644
--- a/IoTCore/Models/LoginInfo.cs
+++ b/IoTCore/Models/LoginInfo.cs
@@ -1,37 +1,39 @@
-using System;
+using NewLife.Remoting.Models;
 
-namespace NewLife.IoT.Models
+namespace NewLife.IoT.Models;
+
+/// <summary>节点登录信息</summary>
+public class LoginInfo : ILoginRequest
 {
-    /// <summary>节点登录信息</summary>
-    public class LoginInfo
-    {
-        #region 属性
-        /// <summary>设备编码</summary>
-        public String Code { get; set; }
+    #region 属性
+    /// <summary>设备编码</summary>
+    public String Code { get; set; }
+
+    /// <summary>设备密钥</summary>
+    public String Secret { get; set; }
 
-        /// <summary>设备密钥</summary>
-        public String Secret { get; set; }
+    /// <summary>产品证书</summary>
+    public String ProductKey { get; set; }
 
-        /// <summary>产品证书</summary>
-        public String ProductKey { get; set; }
+    /// <summary>产品密钥</summary>
+    public String ProductSecret { get; set; }
 
-        /// <summary>产品密钥</summary>
-        public String ProductSecret { get; set; }
+    /// <summary>实例。应用可能多实例部署,ip@proccessid</summary>
+    public String ClientId { get; set; }
 
-        /// <summary>名称。可用于标识设备的名称</summary>
-        public String Name { get; set; }
+    /// <summary>名称。可用于标识设备的名称</summary>
+    public String Name { get; set; }
 
-        /// <summary>版本</summary>
-        public String Version { get; set; }
+    /// <summary>版本</summary>
+    public String Version { get; set; }
 
-        /// <summary>本地IP地址</summary>
-        public String IP { get; set; }
+    /// <summary>本地IP地址</summary>
+    public String IP { get; set; }
 
-        /// <summary>唯一标识</summary>
-        public String UUID { get; set; }
+    /// <summary>唯一标识</summary>
+    public String UUID { get; set; }
 
-        /// <summary>本地UTC时间</summary>
-        public Int64 Time { get; set; }
-        #endregion
-    }
+    /// <summary>本地UTC时间</summary>
+    public Int64 Time { get; set; }
+    #endregion
 }
\ No newline at end of file
Modified +23 -23
diff --git a/IoTCore/Models/LoginResponse.cs b/IoTCore/Models/LoginResponse.cs
index 4778d71..3dd4329 100644
--- a/IoTCore/Models/LoginResponse.cs
+++ b/IoTCore/Models/LoginResponse.cs
@@ -1,34 +1,34 @@
 using System;
+using NewLife.Remoting.Models;
 
-namespace NewLife.IoT.Models
+namespace NewLife.IoT.Models;
+
+/// <summary>设备登录响应</summary>
+public class LoginResponse : ILoginResponse
 {
-    /// <summary>设备登录响应</summary>
-    public class LoginResponse
-    {
-        #region 属性
-        /// <summary>产品</summary>
-        public String ProductKey { get; set; }
+    #region 属性
+    /// <summary>产品</summary>
+    public String ProductKey { get; set; }
 
-        /// <summary>节点编码</summary>
-        public String Code { get; set; }
+    /// <summary>节点编码</summary>
+    public String Code { get; set; }
 
-        /// <summary>节点密钥</summary>
-        public String Secret { get; set; }
+    /// <summary>节点密钥</summary>
+    public String Secret { get; set; }
 
-        /// <summary>名称</summary>
-        public String Name { get; set; }
+    /// <summary>名称</summary>
+    public String Name { get; set; }
 
-        /// <summary>令牌</summary>
-        public String Token { get; set; }
+    /// <summary>令牌</summary>
+    public String Token { get; set; }
 
-        /// <summary>服务器时间</summary>
-        public Int64 Time { get; set; }
+    /// <summary>服务器时间</summary>
+    public Int64 Time { get; set; }
 
-        ///// <summary>设备通道</summary>
-        //public String Channels { get; set; }
+    ///// <summary>设备通道</summary>
+    //public String Channels { get; set; }
 
-        /// <summary>客户端唯一标识</summary>
-        public String ClientId { get; set; }
-        #endregion
-    }
+    /// <summary>客户端唯一标识</summary>
+    public String ClientId { get; set; }
+    #endregion
 }
\ No newline at end of file
Modified +15 -15
diff --git a/IoTCore/Models/LogoutResponse.cs b/IoTCore/Models/LogoutResponse.cs
index b5aaca3..352fba1 100644
--- a/IoTCore/Models/LogoutResponse.cs
+++ b/IoTCore/Models/LogoutResponse.cs
@@ -1,22 +1,22 @@
 using System;
+using NewLife.Remoting.Models;
 
-namespace NewLife.IoT.Models
+namespace NewLife.IoT.Models;
+
+/// <summary>设备注销响应</summary>
+public class LogoutResponse : ILogoutResponse
 {
-    /// <summary>设备注销响应</summary>
-    public class LogoutResponse
-    {
-        #region 属性
-        /// <summary>节点编码</summary>
-        public String Code { get; set; }
+    #region 属性
+    /// <summary>节点编码</summary>
+    public String Code { get; set; }
 
-        /// <summary>节点密钥</summary>
-        public String Secret { get; set; }
+    /// <summary>节点密钥</summary>
+    public String Secret { get; set; }
 
-        /// <summary>名称</summary>
-        public String Name { get; set; }
+    /// <summary>名称</summary>
+    public String Name { get; set; }
 
-        /// <summary>令牌</summary>
-        public String Token { get; set; }
-        #endregion
-    }
+    /// <summary>令牌</summary>
+    public String Token { get; set; }
+    #endregion
 }
\ No newline at end of file
Modified +29 -30
diff --git a/IoTCore/Models/PingInfo.cs b/IoTCore/Models/PingInfo.cs
index d1b6c6b..ca2fbaa 100644
--- a/IoTCore/Models/PingInfo.cs
+++ b/IoTCore/Models/PingInfo.cs
@@ -1,43 +1,42 @@
-using System;
+using NewLife.Remoting.Models;
 
-namespace NewLife.IoT.Models
+namespace NewLife.IoT.Models;
+
+/// <summary>心跳信息</summary>
+public class PingInfo : IPingRequest
 {
-    /// <summary>心跳信息</summary>
-    public class PingInfo
-    {
-        #region 属性
-        /// <summary>内存大小</summary>
-        public UInt64 Memory { get; set; }
+    #region 属性
+    /// <summary>内存大小</summary>
+    public UInt64 Memory { get; set; }
 
-        /// <summary>可用内存大小</summary>
-        public UInt64 AvailableMemory { get; set; }
+    /// <summary>可用内存大小</summary>
+    public UInt64 AvailableMemory { get; set; }
 
-        /// <summary>磁盘大小。应用所在盘</summary>
-        public UInt64 TotalSize { get; set; }
+    /// <summary>磁盘大小。应用所在盘</summary>
+    public UInt64 TotalSize { get; set; }
 
-        /// <summary>磁盘可用空间。应用所在盘</summary>
-        public UInt64 AvailableFreeSpace { get; set; }
+    /// <summary>磁盘可用空间。应用所在盘</summary>
+    public UInt64 AvailableFreeSpace { get; set; }
 
-        /// <summary>CPU使用率</summary>
-        public Single CpuRate { get; set; }
+    /// <summary>CPU使用率</summary>
+    public Double CpuRate { get; set; }
 
-        /// <summary>温度</summary>
-        public Double Temperature { get; set; }
+    /// <summary>温度</summary>
+    public Double Temperature { get; set; }
 
-        /// <summary>电量</summary>
-        public Double Battery { get; set; }
+    /// <summary>电量</summary>
+    public Double Battery { get; set; }
 
-        /// <summary>本地IP</summary>
-        public String IP { get; set; }
+    /// <summary>本地IP</summary>
+    public String IP { get; set; }
 
-        /// <summary>开机时间,单位s</summary>
-        public Int32 Uptime { get; set; }
+    /// <summary>开机时间,单位s</summary>
+    public Int32 Uptime { get; set; }
 
-        /// <summary>本地UTC时间。ms毫秒</summary>
-        public Int64 Time { get; set; }
+    /// <summary>本地UTC时间。ms毫秒</summary>
+    public Int64 Time { get; set; }
 
-        /// <summary>延迟。ms毫秒</summary>
-        public Int32 Delay { get; set; }
-        #endregion
-    }
+    /// <summary>延迟。ms毫秒</summary>
+    public Int32 Delay { get; set; }
+    #endregion
 }
\ No newline at end of file
Modified +16 -15
diff --git a/IoTCore/Models/PingResponse.cs b/IoTCore/Models/PingResponse.cs
index ec717a5..3296b17 100644
--- a/IoTCore/Models/PingResponse.cs
+++ b/IoTCore/Models/PingResponse.cs
@@ -1,21 +1,22 @@
-using System;
-using NewLife.IoT.ThingModels;
+using NewLife.Remoting.Models;
 
-namespace NewLife.IoT.Models
+namespace NewLife.IoT.Models;
+
+/// <summary>心跳响应</summary>
+public class PingResponse : IPingResponse
 {
-    /// <summary>心跳响应</summary>
-    public class PingResponse
-    {
-        /// <summary>本地时间。ms毫秒</summary>
-        public Int64 Time { get; set; }
+    /// <summary>本地时间。ms毫秒</summary>
+    public Int64 Time { get; set; }
+
+    /// <summary>服务器时间</summary>
+    public Int64 ServerTime { get; set; }
 
-        /// <summary>服务器时间</summary>
-        public Int64 ServerTime { get; set; }
+    /// <summary>心跳周期。单位秒</summary>
+    public Int32 Period { get; set; }
 
-        /// <summary>心跳周期。单位秒</summary>
-        public Int32 Period { get; set; }
+    /// <summary>令牌。现有令牌即将过期时,颁发新的令牌</summary>
+    public String Token { get; set; }
 
-        /// <summary>令牌。现有令牌即将过期时,颁发新的令牌</summary>
-        public String Token { get; set; }
-    }
+    /// <summary>下发命令</summary>
+    public CommandModel[]? Commands { get; set; }
 }
\ No newline at end of file
Deleted +0 -26
IoTCore/Models/UpgradeInfo.cs
Modified +7 -1
diff --git a/IoTEdge/ClientSetting.cs b/IoTEdge/ClientSetting.cs
index 749f03d..3a4e38c 100644
--- a/IoTEdge/ClientSetting.cs
+++ b/IoTEdge/ClientSetting.cs
@@ -1,11 +1,12 @@
 using System.ComponentModel;
 using NewLife.Configuration;
+using NewLife.Remoting.Clients;
 
 namespace IoTEdge;
 
 /// <summary>配置</summary>
 [Config("IoTClient")]
-public class ClientSetting : Config<ClientSetting>
+public class ClientSetting : Config<ClientSetting>, IClientSetting
 {
     #region 属性
     /// <summary>服务端地址。IoT服务平台地址</summary>
@@ -24,4 +25,9 @@ public class ClientSetting : Config<ClientSetting>
     [Description("产品证书。用于一型一密验证,对一机一密无效")]
     public String ProductKey { get; set; } = "EdgeGateway";
     #endregion
+
+    #region IClientSetting
+    String IClientSetting.Code { get => DeviceCode; set => DeviceCode = value; }
+    String IClientSetting.Secret { get => DeviceSecret; set => DeviceSecret = value; }
+    #endregion
 }
\ No newline at end of file
Modified +2 -2
diff --git a/IoTEdge/EdgeService.cs b/IoTEdge/EdgeService.cs
index 36cdc8d..983a17e 100644
--- a/IoTEdge/EdgeService.cs
+++ b/IoTEdge/EdgeService.cs
@@ -25,14 +25,14 @@ internal class EdgeService : IHostedService
             Log = XTrace.Log,
         };
 
-        await device.LoginAsync();
+        await device.Login();
 
         _device = device;
     }
 
     public async Task StopAsync(CancellationToken cancellationToken)
     {
-        if (_device != null) await _device.LogoutAsync(nameof(StopAsync));
+        if (_device != null) await _device.Logout(nameof(StopAsync));
 
         _device.TryDispose();
     }
Modified +43 -246
diff --git a/IoTEdge/HttpDevice.cs b/IoTEdge/HttpDevice.cs
index 05a2ef0..ab556d5 100644
--- a/IoTEdge/HttpDevice.cs
+++ b/IoTEdge/HttpDevice.cs
@@ -1,281 +1,91 @@
-using System.Diagnostics;
-using System.Net;
-using System.Net.NetworkInformation;
-using System.Net.WebSockets;
-using NewLife;
-using NewLife.Caching;
-using NewLife.IoT.Models;
+using NewLife.IoT.Models;
 using NewLife.IoT.ThingModels;
 using NewLife.Log;
-using NewLife.Remoting;
+using NewLife.Model;
+using NewLife.Remoting.Clients;
+using NewLife.Remoting.Models;
 using NewLife.Security;
-using NewLife.Serialization;
-using NewLife.Threading;
 
 namespace IoTEdge;
 
 /// <summary>Http协议设备</summary>
-public class HttpDevice : DisposeBase
+public class HttpDevice : ClientBase
 {
     #region 属性
-    /// <summary>服务器地址</summary>
-    public String Server { get; set; }
-
-    /// <summary>设备编码。从IoT管理平台获取(需提前分配),或者本地提交后动态注册</summary>
-    public String DeviceCode { get; set; }
-
-    /// <summary>密钥。设备密钥或产品密钥,分别用于一机一密和一型一密,从IoT管理平台获取</summary>
-    public String DeviceSecret { get; set; }
-
     /// <summary>产品编码。从IoT管理平台获取</summary>
     public String ProductKey { get; set; }
 
-    /// <summary>密码散列提供者。避免密码明文提交</summary>
-    public IPasswordProvider PasswordProvider { get; set; } = new SaltPasswordProvider { Algorithm = "md5", SaltTime = 60 };
-
-    /// <summary>收到命令时触发</summary>
-    public event EventHandler<ServiceEventArgs> Received;
-
     private readonly ClientSetting _setting;
-    private ApiHttpClient _client;
-    private Int32 _delay;
     #endregion
 
     #region 构造
-    public HttpDevice() { }
+    public HttpDevice() => Prefix = "Device/";
 
-    public HttpDevice(ClientSetting setting)
+    public HttpDevice(ClientSetting setting) : base(setting)
     {
         _setting = setting;
 
-        Server = setting.Server;
-        DeviceCode = setting.DeviceCode;
-        DeviceSecret = setting.DeviceSecret;
         ProductKey = setting.ProductKey;
     }
-
-    protected override void Dispose(Boolean disposing)
-    {
-        base.Dispose(disposing);
-
-        StopTimer();
-    }
     #endregion
 
-    #region 登录注销
-    /// <summary>
-    /// 登录
-    /// </summary>
-    /// <param name="inf"></param>
-    /// <returns></returns>
-    public async Task LoginAsync()
+    #region 方法
+    protected override void OnInit()
     {
-        var client = new ApiHttpClient(Server)
-        {
-            Tracer = Tracer,
-            Log = XTrace.Log,
-        };
-
-        var info = new LoginInfo
-        {
-            Code = DeviceCode,
-            Secret = DeviceSecret.IsNullOrEmpty() ? null : PasswordProvider.Hash(DeviceSecret),
-            ProductKey = ProductKey,
-        };
-        var rs = await client.PostAsync<LoginResponse>("Device/Login", info);
-        client.Token = rs.Token;
+        var provider = ServiceProvider ??= ObjectContainer.Provider;
 
-        if (!rs.Code.IsNullOrEmpty() && !rs.Secret.IsNullOrEmpty())
+        // 找到容器,注册默认的模型实现,供后续InvokeAsync时自动创建正确的模型对象
+        var container = ModelExtension.GetService<IObjectContainer>(provider) ?? ObjectContainer.Current;
+        if (container != null)
         {
-            WriteLog("下发证书:{0}/{1}", rs.Code, rs.Secret);
-            DeviceCode = rs.Code;
-            DeviceSecret = rs.Secret;
-
-            _setting.DeviceCode = rs.Code;
-            _setting.DeviceSecret = rs.Secret;
-            _setting.Save();
+            container.TryAddTransient<ILoginRequest, LoginInfo>();
+            //container.TryAddTransient<ILoginResponse, LoginResponse>();
+            //container.TryAddTransient<ILogoutResponse, LogoutResponse>();
+            container.TryAddTransient<IPingRequest, PingInfo>();
+            //container.TryAddTransient<IPingResponse, PingResponse>();
+            //container.TryAddTransient<IUpgradeInfo, UpgradeInfo>();
         }
 
-        _client = client;
-
-        StartTimer();
+        base.OnInit();
     }
-
-    /// <summary>注销</summary>
-    /// <param name="reason"></param>
-    /// <returns></returns>
-    public async Task LogoutAsync(String reason) => await _client.PostAsync<LogoutResponse>("Device/Logout", new { reason });
     #endregion
 
-    #region 心跳&长连接
-    /// <summary>心跳</summary>
-    /// <returns></returns>
-    public virtual async Task PingAsync()
+    #region 登录注销
+    public override ILoginRequest BuildLoginRequest()
     {
-        if (Tracer != null) DefaultSpan.Current = null;
-
-        using var span = Tracer?.NewSpan("Ping");
-        try
+        var request = base.BuildLoginRequest();
+        if (request is LoginInfo info)
         {
-            var info = GetHeartInfo();
-
-            var rs = await _client.PostAsync<PingResponse>("Device/Ping", info);
-
-            var dt = rs.Time.ToDateTime();
-            if (dt.Year > 2000)
-            {
-                // 计算延迟
-                var ts = DateTime.UtcNow - dt;
-                var ms = (Int32)ts.TotalMilliseconds;
-                _delay = (_delay + ms) / 2;
-            }
-
-            var svc = _client.Services.FirstOrDefault();
-            if (svc != null && _client.Token != null && (_websocket == null || _websocket.State != WebSocketState.Open))
-            {
-                var url = svc.Address.ToString().Replace("http://", "ws://").Replace("https://", "wss://");
-                var uri = new Uri(new Uri(url), "/Device/Notify");
-                var client = new ClientWebSocket();
-                client.Options.SetRequestHeader("Authorization", "Bearer " + _client.Token);
-                await client.ConnectAsync(uri, default);
-
-                _websocket = client;
-
-                _source = new CancellationTokenSource();
-                _ = Task.Run(() => DoPull(client, _source.Token));
-            }
-
-            // 令牌
-            if (rs is PingResponse pr && !pr.Token.IsNullOrEmpty())
-                _client.Token = pr.Token;
+            info.ProductKey = ProductKey;
         }
-        catch (Exception ex)
-        {
-            span?.SetError(ex, null);
 
-            throw;
-        }
+        return request;
     }
+    #endregion
 
-    /// <summary>获取心跳信息</summary>
-    public PingInfo GetHeartInfo()
+    #region 心跳
+    public override IPingRequest BuildPingRequest()
     {
-        var mi = MachineInfo.GetCurrent();
-        mi.Refresh();
-
-        var properties = IPGlobalProperties.GetIPGlobalProperties();
-        var connections = properties.GetActiveTcpConnections();
-
-        var mcs = NetHelper.GetMacs().Select(e => e.ToHex("-")).OrderBy(e => e).Join(",");
-        var driveInfo = new DriveInfo(Path.GetPathRoot(".".GetFullPath()));
-        var ip = NetHelper.GetIPs().Where(ip => ip.IsIPv4() && !IPAddress.IsLoopback(ip) && ip.GetAddressBytes()[0] != 169).Join();
-        var ext = new PingInfo
+        var request = base.BuildPingRequest();
+        if (request is PingInfo info)
         {
-            Memory = mi.Memory,
-            AvailableMemory = mi.AvailableMemory,
-            TotalSize = (UInt64)driveInfo.TotalSize,
-            AvailableFreeSpace = (UInt64)driveInfo.AvailableFreeSpace,
-            CpuRate = (Single)Math.Round(mi.CpuRate, 4),
-            Temperature = mi.Temperature,
-            Battery = mi.Battery,
-            Uptime = Environment.TickCount / 1000,
-
-            IP = ip,
-
-            Time = DateTime.UtcNow.ToLong(),
-            Delay = _delay,
-        };
-        // 开始时间 Environment.TickCount 很容易溢出,导致开机24天后变成负数。
-        // 后来在 netcore3.0 增加了Environment.TickCount64
-        // 现在借助 Stopwatch 来解决
-        if (Stopwatch.IsHighResolution) ext.Uptime = (Int32)(Stopwatch.GetTimestamp() / Stopwatch.Frequency);
-
-        return ext;
-    }
-
-    private TimerX _timer;
-    /// <summary>开始心跳定时器</summary>
-    protected virtual void StartTimer()
-    {
-        if (_timer == null)
-            lock (this)
-                _timer ??= new TimerX(async s => await PingAsync(), null, 3_000, 60_000, "Device") { Async = true };
-    }
-
-    /// <summary>停止心跳定时器</summary>
-    protected void StopTimer()
-    {
-        if (_websocket != null && _websocket.State == WebSocketState.Open)
-            _websocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "finish", default).Wait();
-        _source?.Cancel();
-
-        _websocket = null;
-
-        _timer.TryDispose();
-        _timer = null;
-    }
-
-    private WebSocket _websocket;
-    private CancellationTokenSource _source;
-    private ICache _cache = new MemoryCache();
 
-    private async Task DoPull(WebSocket socket, CancellationToken cancellationToken)
-    {
-        DefaultSpan.Current = null;
-        try
-        {
-            var buf = new Byte[4 * 1024];
-            while (!cancellationToken.IsCancellationRequested && socket.State == WebSocketState.Open)
-            {
-                var data = await socket.ReceiveAsync(new ArraySegment<Byte>(buf), cancellationToken);
-                var model = buf.ToStr(null, 0, data.Count).ToJsonEntity<ServiceModel>();
-                if (model != null && _cache.Add($"cmd:{model.Id}", model, 3600))
-                {
-                    // 建立追踪链路
-                    using var span = Tracer?.NewSpan("service:" + model.Name, model);
-                    span?.Detach(model.TraceId);
-                    try
-                    {
-                        if (model.Expire.Year < 2000 || model.Expire > DateTime.Now)
-                            await OnReceiveCommand(model);
-                        else
-                            await ServiceReply(new ServiceReplyModel { Id = model.Id, Status = ServiceStatus.取消 });
-                    }
-                    catch (Exception ex)
-                    {
-                        span?.SetError(ex, null);
-                    }
-                }
-            }
-        }
-        catch (Exception ex)
-        {
-            XTrace.WriteException(ex);
         }
 
-        if (socket.State == WebSocketState.Open) await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "finish", default);
+        return request;
     }
-    #endregion
 
-    #region 服务
-    /// <summary>
-    /// 触发收到命令的动作
-    /// </summary>
-    /// <param name="model"></param>
-    protected virtual async Task OnReceiveCommand(ServiceModel model)
+    public override Task<Object> CommandReply(CommandReplyModel model) => InvokeAsync<Object>("Thing/ServiceReply", new ServiceReplyModel
     {
-        var e = new ServiceEventArgs { Model = model };
-        Received?.Invoke(this, e);
-
-        if (e.Reply != null) await ServiceReply(e.Reply);
-    }
-
-    /// <summary>上报服务调用结果</summary>
-    /// <param name="model"></param>
-    /// <returns></returns>
-    public virtual async Task<Object> ServiceReply(ServiceReplyModel model) => await _client.PostAsync<Object>("Thing/ServiceReply", model);
+        Id = model.Id,
+        Status = (ServiceStatus)model.Status,
+        Data = model.Data,
+    });
     #endregion
 
+    #region 数据
+    /// <summary>上传数据</summary>
+    /// <returns></returns>
     public async Task PostDataAsync()
     {
         if (Tracer != null) DefaultSpan.Current = null;
@@ -285,17 +95,16 @@ public class HttpDevice : DisposeBase
         {
             var items = new List<DataModel>
             {
-                new DataModel
-                {
+                new() {
                     Time = DateTime.UtcNow.ToLong(),
                     Name = "TestValue",
                     Value = Rand.Next(0, 100) + ""
                 }
             };
 
-            var data = new DataModels { DeviceCode = DeviceCode, Items = items.ToArray() };
+            var data = new DataModels { DeviceCode = Code, Items = items.ToArray() };
 
-            await _client.PostAsync<Int32>("Thing/PostData", data);
+            await InvokeAsync<Int32>("Thing/PostData", data);
         }
         catch (Exception ex)
         {
@@ -304,17 +113,5 @@ public class HttpDevice : DisposeBase
             throw;
         }
     }
-
-    #region 日志
-    /// <summary>链路追踪</summary>
-    public ITracer Tracer { get; set; }
-
-    /// <summary>日志</summary>
-    public ILog Log { get; set; }
-
-    /// <summary>写日志</summary>
-    /// <param name="format"></param>
-    /// <param name="args"></param>
-    public void WriteLog(String format, params Object[] args) => Log?.Info(format, args);
     #endregion
 }
\ No newline at end of file
Modified +13 -13
diff --git a/IoTEdge/IoTEdge.csproj b/IoTEdge/IoTEdge.csproj
index 7ade8c9..c55b801 100644
--- a/IoTEdge/IoTEdge.csproj
+++ b/IoTEdge/IoTEdge.csproj
@@ -6,8 +6,8 @@
     <AssemblyTitle>物联网网关</AssemblyTitle>
     <Description>IoT边缘网关</Description>
     <Company>新生命开发团队</Company>
-    <Copyright>©2002-2023 NewLife</Copyright>
-    <VersionPrefix>1.0</VersionPrefix>
+    <Copyright>©2002-2024 NewLife</Copyright>
+    <VersionPrefix>2.0</VersionPrefix>
     <VersionSuffix>$([System.DateTime]::Now.ToString(`yyyy.MMdd`))</VersionSuffix>
     <Version>$(VersionPrefix).$(VersionSuffix)</Version>
     <FileVersion>$(Version)</FileVersion>
@@ -21,17 +21,17 @@
 
   <ItemGroup>
     <PackageReference Include="NewLife.BACnet" Version="1.0.2023.520-beta0022" />
-    <PackageReference Include="NewLife.Core" Version="10.3.2023.627-beta1611" />
-    <PackageReference Include="NewLife.IoT" Version="1.8.2023.611-beta1629" />
-    <PackageReference Include="NewLife.Modbus" Version="1.6.2023.511" />
-    <PackageReference Include="NewLife.ModbusRTU" Version="1.6.2023.511" />
-    <PackageReference Include="NewLife.MQTT" Version="1.4.2023.620-beta1039" />
-    <PackageReference Include="NewLife.NetPing" Version="1.1.2023.511" />
-    <PackageReference Include="NewLife.PC" Version="1.0.2023.511" />
-    <PackageReference Include="NewLife.Schneider" Version="1.0.2023.511" />
-    <PackageReference Include="NewLife.Siemens" Version="1.0.2023.511" />
-    <PackageReference Include="NewLife.Stardust" Version="2.9.2023.627-beta0441" />
-    <PackageReference Include="SmartA2" Version="1.0.2023.606-beta1304" />
+    <PackageReference Include="NewLife.Core" Version="10.10.2024.601" />
+    <PackageReference Include="NewLife.IoT" Version="2.2.2024.501" />
+    <PackageReference Include="NewLife.Modbus" Version="1.8.2024.217" />
+    <PackageReference Include="NewLife.ModbusRTU" Version="1.8.2024.217" />
+    <PackageReference Include="NewLife.MQTT" Version="2.0.2024.516" />
+    <PackageReference Include="NewLife.NetPing" Version="1.1.2024.217" />
+    <PackageReference Include="NewLife.PC" Version="1.0.2024.217" />
+    <PackageReference Include="NewLife.Schneider" Version="1.0.2024.218" />
+    <PackageReference Include="NewLife.Siemens" Version="1.1.2024.218" />
+    <PackageReference Include="NewLife.Stardust" Version="2.9.2024.402" />
+    <PackageReference Include="SmartA2" Version="1.1.2024.218" />
     <PackageReference Include="SmartA4" Version="1.0.2023.606-beta1305" />
   </ItemGroup>
 
Modified +7 -0
diff --git a/IoTEdge/Program.cs b/IoTEdge/Program.cs
index d80c712..86f3b61 100644
--- a/IoTEdge/Program.cs
+++ b/IoTEdge/Program.cs
@@ -10,6 +10,10 @@ using Stardust;
 // 启用控制台日志,拦截所有异常
 XTrace.UseConsole();
 
+#if DEBUG
+XTrace.Log.Level = NewLife.Log.LogLevel.Debug;
+#endif
+
 // 初始化对象容器,提供注入能力
 var services = ObjectContainer.Current;
 services.AddSingleton(XTrace.Log);
@@ -18,6 +22,9 @@ services.AddSingleton(XTrace.Log);
 var star = new StarFactory();
 if (star.Server.IsNullOrEmpty()) star = null;
 
+var set = ClientSetting.Current;
+services.AddSingleton(set);
+
 // 初始化Redis、MQTT、RocketMQ,注册服务到容器
 InitMqtt(services, star?.Tracer);
 
Deleted +0 -67
IoTZero/Common/ApiFilterAttribute.cs
Modified +5 -101
diff --git a/IoTZero/Controllers/AppController.cs b/IoTZero/Controllers/AppController.cs
index f1cae7a..7796929 100644
--- a/IoTZero/Controllers/AppController.cs
+++ b/IoTZero/Controllers/AppController.cs
@@ -1,36 +1,23 @@
 using IoT.Data;
-using IoTZero.Common;
 using IoTZero.Services;
 using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.Controllers;
-using Microsoft.AspNetCore.Mvc.Filters;
 using NewLife;
-using NewLife.Cube;
 using NewLife.IoT.Models;
 using NewLife.IoT.ThingModels;
 using NewLife.Log;
 using NewLife.Remoting;
-using NewLife.Serialization;
-using NewLife.Web;
-using IActionFilter = Microsoft.AspNetCore.Mvc.Filters.IActionFilter;
+using NewLife.Remoting.Extensions;
 
 namespace IoTZero.Controllers;
 
 /// <summary>物模型Api控制器。用于应用系统调用</summary>
+[ApiFilter]
 [ApiController]
 [Route("[controller]")]
-public class AppController : ControllerBase, IActionFilter
+public class AppController : BaseController
 {
-    /// <summary>用户主机</summary>
-    public String UserHost => HttpContext.GetUserHost();
-
-    /// <summary>令牌</summary>
-    public String Token { get; set; }
-
-    private readonly MyDeviceService _deviceService;
     private readonly ThingService _thingService;
     private readonly ITracer _tracer;
-    private IDictionary<String, Object> _args;
 
     #region 构造
     /// <summary>
@@ -40,91 +27,11 @@ public class AppController : ControllerBase, IActionFilter
     /// <param name="deviceService"></param>
     /// <param name="thingService"></param>
     /// <param name="tracer"></param>
-    public AppController(MyDeviceService deviceService, ThingService thingService, ITracer tracer)
+    public AppController(IServiceProvider serviceProvider, ThingService thingService, ITracer tracer) : base(serviceProvider)
     {
-        _deviceService = deviceService;
         _thingService = thingService;
         _tracer = tracer;
     }
-
-    void IActionFilter.OnActionExecuting(ActionExecutingContext context)
-    {
-        _args = context.ActionArguments;
-
-        // 访问令牌
-        var request = context.HttpContext.Request;
-        var token = request.Query["Token"] + "";
-        if (token.IsNullOrEmpty()) token = (request.Headers["Authorization"] + "").TrimStart("Bearer ");
-        if (token.IsNullOrEmpty()) token = request.Headers["X-Token"] + "";
-        if (token.IsNullOrEmpty()) token = request.Cookies["Token"] + "";
-        Token = token;
-
-        try
-        {
-            if (!token.IsNullOrEmpty())
-            {
-                //todo 验证令牌有效性
-            }
-
-            //if (context.ActionDescriptor is ControllerActionDescriptor act && !act.MethodInfo.IsDefined(typeof(AllowAnonymousAttribute)))
-            //    throw new ApiException(403, "设备认证失败");
-        }
-        catch (Exception ex)
-        {
-            var traceId = DefaultSpan.Current?.TraceId;
-            context.Result = ex is ApiException aex
-                ? new JsonResult(new { code = aex.Code, data = aex.Message, traceId })
-                : new JsonResult(new { code = 500, data = ex.Message, traceId });
-
-            WriteError(ex, context);
-        }
-    }
-
-    /// <summary>请求处理后</summary>
-    /// <param name="context"></param>
-    void IActionFilter.OnActionExecuted(ActionExecutedContext context)
-    {
-        if (context.Exception != null) WriteError(context.Exception, context);
-
-        var traceId = DefaultSpan.Current?.TraceId;
-
-        if (context.Result != null)
-            if (context.Result is ObjectResult obj)
-            {
-                //context.Result = new JsonResult(new { code = obj.StatusCode ?? 0, data = obj.Value });
-                var rs = new { code = obj.StatusCode ?? 0, data = obj.Value, traceId };
-                context.Result = new ContentResult
-                {
-                    Content = rs.ToJson(false, true, true),
-                    ContentType = "application/json",
-                    StatusCode = 200
-                };
-            }
-            else if (context.Result is EmptyResult)
-                context.Result = new JsonResult(new { code = 0, data = new { }, traceId });
-            else if (context.Exception != null && !context.ExceptionHandled)
-            {
-                var ex = context.Exception.GetTrue();
-                if (ex is ApiException aex)
-                    context.Result = new JsonResult(new { code = aex.Code, data = aex.Message, traceId });
-                else
-                    context.Result = new JsonResult(new { code = 500, data = ex.Message, traceId });
-
-                context.ExceptionHandled = true;
-
-                // 输出异常日志
-                if (XTrace.Debug) XTrace.WriteException(ex);
-            }
-    }
-
-    private void WriteError(Exception ex, ActionContext context)
-    {
-        // 拦截全局异常,写日志
-        var action = context.HttpContext.Request.Path + "";
-        if (context.ActionDescriptor is ControllerActionDescriptor act) action = $"{act.ControllerName}/{act.ActionName}";
-
-        XTrace.WriteLine("{0} {1}", action, ex.Message);
-    }
     #endregion
 
     #region 物模型
@@ -132,7 +39,6 @@ public class AppController : ControllerBase, IActionFilter
     /// <param name="deviceId">设备编号</param>
     /// <param name="deviceCode">设备编码</param>
     /// <returns></returns>
-    [ApiFilter]
     [HttpGet(nameof(GetProperty))]
     public PropertyModel[] GetProperty(Int32 deviceId, String deviceCode)
     {
@@ -145,9 +51,8 @@ public class AppController : ControllerBase, IActionFilter
     /// <summary>设置设备属性</summary>
     /// <param name="model">数据</param>
     /// <returns></returns>
-    [ApiFilter]
     [HttpPost(nameof(SetProperty))]
-    public async Task<ServiceReplyModel> SetProperty(DevicePropertyModel model)
+    public Task<ServiceReplyModel> SetProperty(DevicePropertyModel model)
     {
         var dv = Device.FindByCode(model.DeviceCode);
         if (dv == null) return null;
@@ -158,7 +63,6 @@ public class AppController : ControllerBase, IActionFilter
     /// <summary>调用设备服务</summary>
     /// <param name="service">服务</param>
     /// <returns></returns>
-    [ApiFilter]
     [HttpPost(nameof(InvokeService))]
     public async Task<ServiceReplyModel> InvokeService(ServiceRequest service)
     {
Deleted +0 -114
IoTZero/Controllers/BaseController.cs
Modified +24 -99
diff --git a/IoTZero/Controllers/DeviceController.cs b/IoTZero/Controllers/DeviceController.cs
index 69b5cf5..fdfe300 100644
--- a/IoTZero/Controllers/DeviceController.cs
+++ b/IoTZero/Controllers/DeviceController.cs
@@ -1,126 +1,86 @@
-using IoTZero.Common;
+using IoT.Data;
 using IoTZero.Services;
-using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Mvc;
-using NewLife.Http;
 using NewLife.IoT.Drivers;
 using NewLife.IoT.Models;
 using NewLife.IoT.ThingModels;
 using NewLife.Log;
 using NewLife.Remoting;
-using WebSocket = System.Net.WebSockets.WebSocket;
+using NewLife.Remoting.Extensions;
+using NewLife.Remoting.Models;
 
 namespace IoTZero.Controllers;
 
 /// <summary>设备控制器</summary>
+[ApiFilter]
 [ApiController]
 [Route("[controller]")]
-public class DeviceController : BaseController
+public class DeviceController : BaseDeviceController
 {
-    private readonly QueueService _queue;
-    private readonly MyDeviceService _deviceService;
+    /// <summary>当前设备</summary>
+    public Device Device { get; set; }
+
     private readonly ThingService _thingService;
     private readonly ITracer _tracer;
 
+    #region 构造
     /// <summary>实例化设备控制器</summary>
+    /// <param name="serviceProvider"></param>
     /// <param name="queue"></param>
     /// <param name="deviceService"></param>
     /// <param name="thingService"></param>
-    /// <param name="hookService"></param>
-    /// <param name="setting"></param>
     /// <param name="tracer"></param>
-    public DeviceController(QueueService queue, MyDeviceService deviceService, ThingService thingService, IoTSetting setting, ITracer tracer) : base(deviceService, setting)
+    public DeviceController(IServiceProvider serviceProvider, ThingService thingService, ITracer tracer) : base(serviceProvider)
     {
-        _queue = queue;
-        _deviceService = deviceService;
         _thingService = thingService;
         _tracer = tracer;
     }
 
-    #region 登录
-    /// <summary>设备登录</summary>
-    /// <param name="model"></param>
-    /// <returns></returns>
-    [AllowAnonymous]
-    [ApiFilter]
-    [HttpPost(nameof(Login))]
-    public LoginResponse Login(LoginInfo model) => _deviceService.Login(model, "Http", UserHost);
-
-    /// <summary>设备注销</summary>
-    /// <param name="reason">注销原因</param>
-    /// <returns></returns>
-    [ApiFilter]
-    [HttpGet(nameof(Logout))]
-    public LogoutResponse Logout(String reason)
+    protected override Boolean OnAuthorize(String token)
     {
-        var device = Device;
-        if (device != null) _deviceService.Logout(device, reason, "Http", UserHost);
+        if (!base.OnAuthorize(token)) return false;
 
-        return new LogoutResponse
-        {
-            Name = device?.Name,
-            Token = null,
-        };
+        Device = _device as Device;
+
+        return true;
     }
     #endregion
 
     #region 心跳
     /// <summary>设备心跳</summary>
-    /// <param name="model"></param>
+    /// <param name="request"></param>
     /// <returns></returns>
-    [ApiFilter]
     [HttpPost(nameof(Ping))]
-    public PingResponse Ping(PingInfo model)
+    public override IPingResponse Ping([FromBody] IPingRequest request)
     {
-        var rs = new PingResponse
-        {
-            Time = model.Time,
-            ServerTime = DateTime.UtcNow.ToLong(),
-        };
+        var rs = base.Ping(request);
 
         var device = Device;
-        if (device != null)
+        if (device != null && rs != null)
         {
             rs.Period = device.Period;
-
-            var olt = _deviceService.Ping(device, model, Token, UserHost);
-
-            // 令牌有效期检查,10分钟内到期的令牌,颁发新令牌。
-            // 这里将来由客户端提交刷新令牌,才能颁发新的访问令牌。
-            var tm = _deviceService.ValidAndIssueToken(device.Code, Token);
-            if (tm != null)
-            {
-                rs.Token = tm.AccessToken;
-
-                //_deviceService.WriteHistory(device, "刷新令牌", true, tm.ToJson(), UserHost);
-            }
         }
 
         return rs;
     }
-
-    [ApiFilter]
-    [HttpGet(nameof(Ping))]
-    public PingResponse Ping() => new() { Time = 0, ServerTime = DateTime.UtcNow.ToLong(), };
     #endregion
 
     #region 升级
     /// <summary>升级检查</summary>
     /// <returns></returns>
-    [ApiFilter]
     [HttpGet(nameof(Upgrade))]
-    public UpgradeInfo Upgrade()
+    public override IUpgradeInfo Upgrade()
     {
-        var device = Device ?? throw new ApiException(402, "节点未登录");
+        var device = Device ?? throw new ApiException(ApiCode.Unauthorized, "节点未登录");
 
-        throw new NotImplementedException();
+        //throw new NotImplementedException();
+        return new UpgradeInfo { };
     }
     #endregion
 
     #region 设备通道
     /// <summary>获取设备信息,包括主设备和子设备</summary>
     /// <returns></returns>
-    [ApiFilter]
     [HttpGet(nameof(GetDevices))]
     public DeviceModel[] GetDevices() => throw new NotImplementedException();
 
@@ -132,7 +92,6 @@ public class DeviceController : BaseController
     /// </remarks>
     /// <param name="devices">设备信息集合。可传递参数模版</param>
     /// <returns>返回上报信息对应的反馈,如果新增子设备,则返回子设备信息</returns>
-    [ApiFilter]
     [HttpPost(nameof(SetOnline))]
     public IDeviceInfo[] SetOnline(DeviceModel[] devices) => throw new NotImplementedException();
 
@@ -142,53 +101,19 @@ public class DeviceController : BaseController
     /// </remarks>
     /// <param name="devices">设备编码集合。用于子设备离线</param>
     /// <returns>返回上报信息对应的反馈,如果新增子设备,则返回子设备信息</returns>
-    [ApiFilter]
     [HttpPost(nameof(SetOffline))]
     public IDeviceInfo[] SetOffline(String[] devices) => throw new NotImplementedException();
 
     /// <summary>获取设备点位表</summary>
     /// <param name="deviceCode">设备编码</param>
     /// <returns></returns>
-    [ApiFilter]
     [HttpGet(nameof(GetPoints))]
     public PointModel[] GetPoints(String deviceCode) => throw new NotImplementedException();
 
     /// <summary>提交驱动信息。客户端把自己的驱动信息提交到平台</summary>
     /// <param name="drivers"></param>
     /// <returns></returns>
-    [ApiFilter]
     [HttpPost(nameof(PostDriver))]
     public Int32 PostDriver(DriverInfo[] drivers) => throw new NotImplementedException();
     #endregion
-
-    #region 下行通知
-    /// <summary>下行通知</summary>
-    /// <returns></returns>
-    [HttpGet("/Device/Notify")]
-    public async Task Notify()
-    {
-        if (HttpContext.WebSockets.IsWebSocketRequest)
-        {
-            using var socket = await HttpContext.WebSockets.AcceptWebSocketAsync();
-
-            await Handle(socket, Token);
-        }
-        else
-        {
-            HttpContext.Response.StatusCode = 400;
-        }
-    }
-
-    private async Task Handle(WebSocket socket, String token)
-    {
-        var device = Device ?? throw new InvalidOperationException("未登录!");
-
-        _deviceService.WriteHistory(device, "WebSocket连接", true, socket.State + "", UserHost);
-
-        var source = new CancellationTokenSource();
-        var queue = _queue.GetQueue(device.Code);
-        _ = Task.Run(() => socket.ConsumeAndPushAsync(queue, onProcess: null, source));
-        await socket.WaitForClose(null, source);
-    }
-    #endregion
 }
\ No newline at end of file
Modified +47 -17
diff --git a/IoTZero/Controllers/ThingController.cs b/IoTZero/Controllers/ThingController.cs
index c7e9830..77011a1 100644
--- a/IoTZero/Controllers/ThingController.cs
+++ b/IoTZero/Controllers/ThingController.cs
@@ -1,57 +1,73 @@
-using IoTZero.Common;
+using IoT.Data;
 using IoTZero.Services;
 using Microsoft.AspNetCore.Mvc;
+using NewLife;
 using NewLife.IoT.Models;
 using NewLife.IoT.ThingModels;
 using NewLife.IoT.ThingSpecification;
+using NewLife.Remoting;
+using NewLife.Remoting.Extensions;
 
 namespace IoTZero.Controllers;
 
 /// <summary>物模型控制器</summary>
+[ApiFilter]
 [ApiController]
 [Route("[controller]")]
 public class ThingController : BaseController
 {
+    /// <summary>当前设备</summary>
+    public Device Device { get; set; }
+
     private readonly QueueService _queue;
     private readonly ThingService _thingService;
 
+    #region 构造
     /// <summary>实例化物模型控制器</summary>
+    /// <param name="serviceProvider"></param>
     /// <param name="queue"></param>
-    /// <param name="deviceService"></param>
     /// <param name="thingService"></param>
-    /// <param name="setting"></param>
-    public ThingController(QueueService queue, MyDeviceService deviceService, ThingService thingService, IoTSetting setting) : base(deviceService, setting)
+    public ThingController(IServiceProvider serviceProvider, QueueService queue, ThingService thingService) : base(serviceProvider)
     {
         _queue = queue;
         _thingService = thingService;
     }
 
+    protected override Boolean OnAuthorize(String token)
+    {
+        if (!base.OnAuthorize(token) || Jwt == null) return false;
+
+        var dv = Device.FindByCode(Jwt.Subject);
+        if (dv == null || !dv.Enable) throw new ApiException(ApiCode.Forbidden, "无效设备!");
+
+        Device = dv;
+
+        return true;
+    }
+    #endregion
+
     #region 设备属性
     /// <summary>上报设备属性</summary>
     /// <param name="model">属性集合</param>
     /// <returns></returns>
-    [ApiFilter]
     [HttpPost(nameof(PostProperty))]
     public Int32 PostProperty(PropertyModels model) => throw new NotImplementedException();
 
     /// <summary>批量上报设备属性,融合多个子设备数据批量上传</summary>
     /// <param name="models">属性集合</param>
     /// <returns></returns>
-    [ApiFilter]
     [HttpPost(nameof(PostProperties))]
     public Int32 PostProperties(PropertyModels[] models) => throw new NotImplementedException();
 
     /// <summary>获取设备属性</summary>
     /// <param name="deviceCode">设备编码</param>
     /// <returns></returns>
-    [ApiFilter]
     [HttpGet(nameof(GetProperty))]
     public PropertyModel[] GetProperty(String deviceCode) => throw new NotImplementedException();
 
     /// <summary>设备数据上报</summary>
     /// <param name="model">模型</param>
     /// <returns></returns>
-    [ApiFilter]
     [HttpPost(nameof(PostData))]
     public Int32 PostData(DataModels model)
     {
@@ -63,7 +79,6 @@ public class ThingController : BaseController
     /// <summary>批量设备数据上报,融合多个子设备数据批量上传</summary>
     /// <param name="models">模型</param>
     /// <returns></returns>
-    [ApiFilter]
     [HttpPost(nameof(PostDatas))]
     public Int32 PostDatas(DataModels[] models) => throw new NotImplementedException();
     #endregion
@@ -72,14 +87,12 @@ public class ThingController : BaseController
     /// <summary>设备事件上报</summary>
     /// <param name="model">模型</param>
     /// <returns></returns>
-    [ApiFilter]
     [HttpPost(nameof(PostEvent))]
     public Int32 PostEvent(EventModels model) => throw new NotImplementedException();
 
     /// <summary>批量设备事件上报,融合多个子设备数据批量上传</summary>
     /// <param name="models">模型</param>
     /// <returns></returns>
-    [ApiFilter]
     [HttpPost(nameof(PostEvents))]
     public Int32 PostEvents(EventModels[] models) => throw new NotImplementedException();
     #endregion
@@ -88,7 +101,6 @@ public class ThingController : BaseController
     /// <summary>设备端响应服务调用</summary>
     /// <param name="model">服务</param>
     /// <returns></returns>
-    [ApiFilter]
     [HttpPost(nameof(ServiceReply))]
     public Int32 ServiceReply(ServiceReplyModel model) => throw new NotImplementedException();
     #endregion
@@ -97,14 +109,12 @@ public class ThingController : BaseController
     /// <summary>获取设备所属产品的物模型</summary>
     /// <param name="deviceCode">设备编码</param>
     /// <returns></returns>
-    [ApiFilter]
     [HttpGet(nameof(GetSpecification))]
     public ThingSpec GetSpecification(String deviceCode) => throw new NotImplementedException();
 
     /// <summary>上报物模型</summary>
     /// <param name="model"></param>
     /// <returns></returns>
-    [ApiFilter]
     [HttpPost(nameof(PostSpecification))]
     public IPoint[] PostSpecification(ThingSpecModel model) => throw new NotImplementedException();
     #endregion
@@ -118,14 +128,12 @@ public class ThingController : BaseController
     /// </remarks>
     /// <param name="model">数据</param>
     /// <returns></returns>
-    [ApiFilter]
     [HttpPost(nameof(PostShadow))]
     public Int32 PostShadow(ShadowModel model) => throw new NotImplementedException();
 
     /// <summary>获取设备影子</summary>
     /// <param name="deviceCode">设备编码</param>
     /// <returns></returns>
-    [ApiFilter]
     [HttpGet(nameof(GetShadow))]
     public String GetShadow(String deviceCode) => throw new NotImplementedException();
     #endregion
@@ -134,8 +142,30 @@ public class ThingController : BaseController
     /// <summary>设备端查询配置信息</summary>
     /// <param name="deviceCode">设备编码</param>
     /// <returns></returns>
-    [ApiFilter]
     [HttpGet(nameof(GetConfig))]
     public IDictionary<String, Object> GetConfig(String deviceCode) => throw new NotImplementedException();
     #endregion
+
+    #region 辅助
+
+    /// <summary>
+    /// 查找子设备
+    /// </summary>
+    /// <param name="deviceCode"></param>
+    /// <returns></returns>
+    protected Device GetDevice(String deviceCode)
+    {
+        var dv = Device;
+        if (dv == null) return null;
+
+        if (deviceCode.IsNullOrEmpty() || dv.Code == deviceCode) return dv;
+
+        var child = Device.FindByCode(deviceCode);
+
+        //dv = dv.Childs.FirstOrDefault(e => e.Code == deviceCode);
+        if (child == null || child.Id != dv.Id) throw new Exception($"非法设备编码,[{deviceCode}]并非当前登录设备[{Device}]的子设备");
+
+        return child;
+    }
+    #endregion
 }
\ No newline at end of file
Modified +2 -1
diff --git a/IoTZero/IoTSetting.cs b/IoTZero/IoTSetting.cs
index 988b3d7..4d34d69 100644
--- a/IoTZero/IoTSetting.cs
+++ b/IoTZero/IoTSetting.cs
@@ -1,6 +1,7 @@
 using System.ComponentModel;
 using NewLife;
 using NewLife.Configuration;
+using NewLife.Remoting.Extensions.Models;
 using NewLife.Security;
 using XCode.Configuration;
 
@@ -8,7 +9,7 @@ namespace IoTZero;
 
 /// <summary>配置</summary>
 [Config("IoTZero")]
-public class IoTSetting : Config<IoTSetting>
+public class IoTSetting : Config<IoTSetting>, ITokenSetting
 {
     #region 静态
     static IoTSetting() => Provider = new DbConfigProvider { UserId = 0, Category = "IoTServer" };
Modified +8 -7
diff --git a/IoTZero/IoTZero.csproj b/IoTZero/IoTZero.csproj
index 57dcfaa..79e9783 100644
--- a/IoTZero/IoTZero.csproj
+++ b/IoTZero/IoTZero.csproj
@@ -5,8 +5,8 @@
     <AssemblyTitle>物联网服务平台</AssemblyTitle>
     <Description>IoT服务平台</Description>
     <Company>新生命开发团队</Company>
-    <Copyright>©2002-2023 NewLife</Copyright>
-    <VersionPrefix>1.0</VersionPrefix>
+    <Copyright>©2002-2024 NewLife</Copyright>
+    <VersionPrefix>2.0</VersionPrefix>
     <VersionSuffix>$([System.DateTime]::Now.ToString(`yyyy.MMdd`))</VersionSuffix>
     <Version>$(VersionPrefix).$(VersionSuffix)</Version>
     <FileVersion>$(Version)</FileVersion>
@@ -29,11 +29,12 @@
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="NewLife.Cube.Core" Version="5.5.2023.625-beta1355" />
-    <PackageReference Include="NewLife.IoT" Version="1.8.2023.611-beta1629" />
-    <PackageReference Include="NewLife.MQTT" Version="1.4.2023.620-beta1039" />
-    <PackageReference Include="NewLife.Redis" Version="5.4.2023.624-beta0342" />
-    <PackageReference Include="NewLife.Stardust.Extensions" Version="2.9.2023.627-beta0441" />
+    <PackageReference Include="NewLife.Cube.Core" Version="6.1.2024.403" />
+    <PackageReference Include="NewLife.IoT" Version="2.2.2024.501" />
+    <PackageReference Include="NewLife.MQTT" Version="2.0.2024.516" />
+    <PackageReference Include="NewLife.Redis" Version="5.7.2024.602" />
+    <PackageReference Include="NewLife.Remoting.Extensions" Version="3.0.2024.620-beta1407" />
+    <PackageReference Include="NewLife.Stardust.Extensions" Version="2.9.2024.402" />
   </ItemGroup>
 
   <ItemGroup>
Modified +35 -31
diff --git a/IoTZero/Program.cs b/IoTZero/Program.cs
index 23a1584..f8dede0 100644
--- a/IoTZero/Program.cs
+++ b/IoTZero/Program.cs
@@ -3,7 +3,6 @@ using IoTZero.Services;
 using NewLife.Caching;
 using NewLife.Cube;
 using NewLife.Log;
-using NewLife.Security;
 using XCode;
 
 // 日志输出到控制台,并拦截全局异常
@@ -12,46 +11,23 @@ XTrace.UseConsole();
 var builder = WebApplication.CreateBuilder(args);
 var services = builder.Services;
 
+InitConfig();
+
 // 配置星尘。借助StarAgent,或者读取配置文件 config/star.config 中的服务器地址、应用标识、密钥
 var star = services.AddStardust(null);
 
-// 把数据目录指向上层,例如部署到 /root/iot/edge/,这些目录放在 /root/iot/
-var set = NewLife.Setting.Current;
-if (set.IsNew)
-{
-    set.LogPath = "../Log";
-    set.DataPath = "../Data";
-    set.BackupPath = "../Backup";
-    set.Save();
-}
-var set2 = CubeSetting.Current;
-if (set2.IsNew)
-{
-    set2.AvatarPath = "../Avatars";
-    set2.UploadPath = "../Uploads";
-    set2.Save();
-}
-var set3 = XCodeSetting.Current;
-if (set3.IsNew)
-{
-    set3.ShowSQL = false;
-    set3.EntityCacheExpire = 60;
-    set3.SingleCacheExpire = 60;
-    set3.Save();
-}
-
 // 系统设置
-services.AddSingleton(IoTSetting.Current);
+var set = IoTSetting.Current;
+services.AddSingleton(set);
 
 // 逐个注册每一个用到的服务,必须做到清晰明了
-services.AddSingleton<IPasswordProvider>(new SaltPasswordProvider { Algorithm = "md5" });
-
 services.AddSingleton<ThingService>();
 services.AddSingleton<DataService>();
 services.AddSingleton<QueueService>();
-services.AddSingleton<MyDeviceService>();
 
-services.AddHttpClient("hc", e => e.Timeout = TimeSpan.FromSeconds(5));
+// 注册IoT
+services.AddIoT(set);
+//services.AddRemoting(set);
 
 services.AddSingleton<ICache, MemoryCache>();
 
@@ -100,3 +76,31 @@ app.MapControllerRoute(
 app.RegisterService("AlarmServer", null, app.Environment.EnvironmentName);
 
 app.Run();
+
+void InitConfig()
+{
+    // 把数据目录指向上层,例如部署到 /root/iot/edge/,这些目录放在 /root/iot/
+    var set = NewLife.Setting.Current;
+    if (set.IsNew)
+    {
+        set.LogPath = "../Log";
+        set.DataPath = "../Data";
+        set.BackupPath = "../Backup";
+        set.Save();
+    }
+    var set2 = CubeSetting.Current;
+    if (set2.IsNew)
+    {
+        set2.AvatarPath = "../Avatars";
+        set2.UploadPath = "../Uploads";
+        set2.Save();
+    }
+    var set3 = XCodeSetting.Current;
+    if (set3.IsNew)
+    {
+        set3.ShowSQL = false;
+        set3.EntityCacheExpire = 60;
+        set3.SingleCacheExpire = 60;
+        set3.Save();
+    }
+}
\ No newline at end of file
Modified +5 -3
diff --git a/IoTZero/Services/DeviceOnlineService.cs b/IoTZero/Services/DeviceOnlineService.cs
index f69a407..edcd9d5 100644
--- a/IoTZero/Services/DeviceOnlineService.cs
+++ b/IoTZero/Services/DeviceOnlineService.cs
@@ -1,6 +1,7 @@
 using IoT.Data;
 using NewLife;
 using NewLife.Log;
+using NewLife.Remoting.Extensions.Services;
 using NewLife.Threading;
 
 namespace IoTZero.Services;
@@ -10,7 +11,7 @@ public class DeviceOnlineService : IHostedService
 {
     #region 属性
     private TimerX _timer;
-    private readonly MyDeviceService _deviceService;
+    private readonly IDeviceService _deviceService;
     private readonly IoTSetting _setting;
     private readonly ITracer _tracer;
     #endregion
@@ -22,7 +23,7 @@ public class DeviceOnlineService : IHostedService
     /// <param name="deviceService"></param>
     /// <param name="setting"></param>
     /// <param name="tracer"></param>
-    public DeviceOnlineService(MyDeviceService deviceService, IoTSetting setting, ITracer tracer)
+    public DeviceOnlineService(IDeviceService deviceService, IoTSetting setting, ITracer tracer)
     {
         _deviceService = deviceService;
         _setting = setting;
@@ -71,7 +72,8 @@ public class DeviceOnlineService : IHostedService
                     var msg = $"[{device}]登录于{olt.CreateTime.ToFullString()},最后活跃于{olt.UpdateTime.ToFullString()}";
                     _deviceService.WriteHistory(device, "超时下线", true, msg, olt.CreateIP);
 
-                    _deviceService.RemoveOnline(olt.DeviceId, olt.CreateIP);
+                    if (_deviceService is MyDeviceService ds)
+                        ds.RemoveOnline(olt.DeviceId, olt.CreateIP);
 
                     if (device != null)
                     {
Added +26 -0
diff --git a/IoTZero/Services/IoTExtensions.cs b/IoTZero/Services/IoTExtensions.cs
new file mode 100644
index 0000000..e8cfd5e
--- /dev/null
+++ b/IoTZero/Services/IoTExtensions.cs
@@ -0,0 +1,26 @@
+using NewLife.IoT.Models;
+using NewLife.Remoting.Extensions;
+using NewLife.Remoting.Extensions.Models;
+using NewLife.Remoting.Extensions.Services;
+using NewLife.Remoting.Models;
+
+namespace IoTZero.Services;
+
+/// <summary>IoT扩展</summary>
+public static class IoTExtensions
+{
+    public static IServiceCollection AddIoT(this IServiceCollection services, ITokenSetting setting)
+    {
+        ArgumentNullException.ThrowIfNull(setting);
+
+        services.AddSingleton<IDeviceService, MyDeviceService>();
+
+        services.AddTransient<ILoginRequest, LoginInfo>();
+        services.AddTransient<IPingRequest, PingInfo>();
+
+        // 注册Remoting所必须的服务
+        services.AddRemoting(setting);
+
+        return services;
+    }
+}
Modified +60 -51
diff --git a/IoTZero/Services/MyDeviceService.cs b/IoTZero/Services/MyDeviceService.cs
index f6ca961..9abcb0a 100644
--- a/IoTZero/Services/MyDeviceService.cs
+++ b/IoTZero/Services/MyDeviceService.cs
@@ -2,24 +2,25 @@
 using IoT.Data;
 using NewLife;
 using NewLife.Caching;
+using NewLife.Caching.Queues;
 using NewLife.IoT.Models;
 using NewLife.Log;
 using NewLife.Remoting;
+using NewLife.Remoting.Extensions.Services;
+using NewLife.Remoting.Models;
 using NewLife.Security;
 using NewLife.Serialization;
 using NewLife.Web;
+using LoginResponse = NewLife.Remoting.Models.LoginResponse;
 
 namespace IoTZero.Services;
 
 /// <summary>设备服务</summary>
-public class MyDeviceService
+public class MyDeviceService : IDeviceService
 {
-    /// <summary>节点引用,令牌无效时使用</summary>
-    public Device Current { get; set; }
-
+    private readonly ICacheProvider _cacheProvider;
     private readonly ICache _cache;
     private readonly IPasswordProvider _passwordProvider;
-    private readonly DataService _dataService;
     private readonly IoTSetting _setting;
     private readonly ITracer _tracer;
 
@@ -31,10 +32,10 @@ public class MyDeviceService
     /// <param name="cacheProvider"></param>
     /// <param name="setting"></param>
     /// <param name="tracer"></param>
-    public MyDeviceService(IPasswordProvider passwordProvider, DataService dataService, ICacheProvider cacheProvider, IoTSetting setting, ITracer tracer)
+    public MyDeviceService(IPasswordProvider passwordProvider, ICacheProvider cacheProvider, IoTSetting setting, ITracer tracer)
     {
         _passwordProvider = passwordProvider;
-        _dataService = dataService;
+        _cacheProvider = cacheProvider;
         _cache = cacheProvider.InnerCache;
         _setting = setting;
         _tracer = tracer;
@@ -44,30 +45,31 @@ public class MyDeviceService
     /// <summary>
     /// 设备登录验证,内部支持动态注册
     /// </summary>
-    /// <param name="inf">登录信息</param>
+    /// <param name="request">登录信息</param>
     /// <param name="source">登录来源</param>
     /// <param name="ip">远程IP</param>
     /// <returns></returns>
     /// <exception cref="ApiException"></exception>
-    public LoginResponse Login(LoginInfo inf, String source, String ip)
+    public (IDeviceModel, IOnlineModel, ILoginResponse) Login(ILoginRequest request, String source, String ip)
     {
+        if (request is not LoginInfo inf) throw new ArgumentOutOfRangeException(nameof(request));
+
         var code = inf.Code;
         var secret = inf.Secret;
 
         var dv = Device.FindByCode(code);
-        Current = dv;
 
         var autoReg = false;
         if (dv == null)
         {
-            if (inf.ProductKey.IsNullOrEmpty()) throw new ApiException(98, "找不到设备,且产品证书为空,无法登录");
+            if (inf.ProductKey.IsNullOrEmpty()) throw new ApiException(ApiCode.NotFound, "找不到设备,且产品证书为空,无法登录");
 
             dv = AutoRegister(null, inf, ip);
             autoReg = true;
         }
         else
         {
-            if (!dv.Enable) throw new ApiException(99, "禁止登录");
+            if (!dv.Enable) throw new ApiException(ApiCode.Forbidden, "禁止登录");
 
             // 校验唯一编码,防止客户端拷贝配置
             var uuid = inf.UUID;
@@ -78,7 +80,7 @@ public class MyDeviceService
             if (dv == null || !dv.Secret.IsNullOrEmpty()
                 && (secret.IsNullOrEmpty() || !_passwordProvider.Verify(dv.Secret, secret)))
             {
-                if (inf.ProductKey.IsNullOrEmpty()) throw new ApiException(98, "设备验证失败,且产品证书为空,无法登录");
+                if (inf.ProductKey.IsNullOrEmpty()) throw new ApiException(ApiCode.Unauthorized, "设备验证失败,且产品证书为空,无法登录");
 
                 dv = AutoRegister(dv, inf, ip);
                 autoReg = true;
@@ -87,16 +89,13 @@ public class MyDeviceService
 
         //if (dv != null && !dv.Enable) throw new ApiException(99, "禁止登录");
 
-        Current = dv ?? throw new ApiException(12, "节点鉴权失败");
+        if (dv == null) throw new ApiException(ApiCode.Unauthorized, "节点鉴权失败");
 
         dv.Login(inf, ip);
 
-        // 设置令牌
-        var tm = IssueToken(dv.Code, _setting);
-
         // 在线记录
         var olt = GetOnline(dv, ip) ?? CreateOnline(dv, ip);
-        olt.Save(inf, null, tm.AccessToken);
+        olt.Save(inf, null, null);
 
         //SetChildOnline(dv, ip);
 
@@ -105,20 +104,13 @@ public class MyDeviceService
 
         var rs = new LoginResponse
         {
-            Name = dv.Name,
-            Token = tm.AccessToken,
-            Time = DateTime.UtcNow.ToLong(),
+            Name = dv.Name
         };
 
-        // 动态注册的设备不可用时,不要发令牌,只发证书
-        if (!dv.Enable) rs.Token = null;
-
         // 动态注册,下发节点证书
         if (autoReg) rs.Secret = dv.Secret;
 
-        rs.Code = dv.Code;
-
-        return rs;
+        return (dv, olt, rs);
     }
 
     /// <summary>设置设备在线,同时检查在线表</summary>
@@ -199,55 +191,55 @@ public class MyDeviceService
     /// <param name="source">登录来源</param>
     /// <param name="ip">远程IP</param>
     /// <returns></returns>
-    public Device Logout(Device device, String reason, String source, String ip)
+    public IOnlineModel Logout(IDeviceModel device, String reason, String source, String ip)
     {
-        var olt = GetOnline(device, ip);
+        var dv = device as Device;
+        var olt = GetOnline(dv, ip);
         if (olt != null)
         {
             var msg = $"{reason} [{device}]]登录于{olt.CreateTime.ToFullString()},最后活跃于{olt.UpdateTime.ToFullString()}";
             WriteHistory(device, source + "设备下线", true, msg, ip);
             olt.Delete();
 
-            var sid = $"{device.Id}@{ip}";
+            var sid = $"{dv.Id}@{ip}";
             _cache.Remove($"DeviceOnline:{sid}");
 
             // 计算在线时长
             if (olt.CreateTime.Year > 2000)
             {
-                device.OnlineTime += (Int32)(DateTime.Now - olt.CreateTime).TotalSeconds;
-                device.Logout();
+                dv.OnlineTime += (Int32)(DateTime.Now - olt.CreateTime).TotalSeconds;
+                dv.Logout();
             }
 
             //DeviceOnlineService.CheckOffline(device, "注销");
         }
 
-        return device;
+        return olt;
     }
     #endregion
 
     #region 心跳
-    /// <summary>
-    /// 心跳
-    /// </summary>
-    /// <param name="device"></param>
+    /// <summary>心跳</summary>
     /// <param name="inf"></param>
     /// <param name="token"></param>
     /// <param name="ip"></param>
     /// <returns></returns>
-    public DeviceOnline Ping(Device device, PingInfo inf, String token, String ip)
+    public IOnlineModel Ping(IDeviceModel device, IPingRequest? request, String token, String ip)
     {
-        if (inf != null && !inf.IP.IsNullOrEmpty()) device.IP = inf.IP;
+        var dv = device as Device;
+        var inf = request as PingInfo;
+        if (inf != null && !inf.IP.IsNullOrEmpty()) dv.IP = inf.IP;
 
         // 自动上线
-        if (device != null && !device.Online) device.SetOnline(ip, "心跳");
+        if (dv != null && !dv.Online) dv.SetOnline(ip, "心跳");
 
-        device.UpdateIP = ip;
-        device.SaveAsync();
+        dv.UpdateIP = ip;
+        dv.SaveAsync();
 
-        var olt = GetOnline(device, ip) ?? CreateOnline(device, ip);
+        var olt = GetOnline(dv, ip) ?? CreateOnline(dv, ip);
         olt.Name = device.Name;
-        olt.GroupPath = device.GroupPath;
-        olt.ProductId = device.ProductId;
+        olt.GroupPath = dv.GroupPath;
+        olt.ProductId = dv.ProductId;
         olt.Save(null, inf, token);
 
         return olt;
@@ -344,7 +336,7 @@ public class MyDeviceService
     public Device DecodeToken(String token, String tokenSecret)
     {
         //if (token.IsNullOrEmpty()) throw new ArgumentNullException(nameof(token));
-        if (token.IsNullOrEmpty()) throw new ApiException(402, "节点未登录");
+        if (token.IsNullOrEmpty()) throw new ApiException(ApiCode.Unauthorized, "节点未登录");
 
         // 解码令牌
         var ss = tokenSecret.Split(':');
@@ -356,8 +348,7 @@ public class MyDeviceService
 
         var rs = jwt.TryDecode(token, out var message);
         var node = Device.FindByCode(jwt.Subject);
-        Current = node;
-        if (!rs) throw new ApiException(403, $"非法访问 {message}");
+        if (!rs) throw new ApiException(ApiCode.Forbidden, $"非法访问 {message}");
 
         return node;
     }
@@ -368,7 +359,7 @@ public class MyDeviceService
     /// <param name="deviceCode"></param>
     /// <param name="token"></param>
     /// <returns></returns>
-    public TokenModel ValidAndIssueToken(String deviceCode, String token)
+    public TokenModel ValidAndIssueToken(String deviceCode, String? token)
     {
         if (token.IsNullOrEmpty()) return null;
 
@@ -388,6 +379,24 @@ public class MyDeviceService
     }
 
     /// <summary>
+    /// 获取指定设备的命令队列
+    /// </summary>
+    /// <param name="deviceCode"></param>
+    /// <returns></returns>
+    public IProducerConsumer<String> GetQueue(String deviceCode)
+    {
+        var q = _cacheProvider.GetQueue<String>($"cmd:{deviceCode}");
+        if (q is QueueBase qb) qb.TraceName = "ServiceQueue";
+
+        return q;
+    }
+
+    /// <summary>查找设备</summary>
+    /// <param name="code"></param>
+    /// <returns></returns>
+    public IDeviceModel QueryDevice(String code) => Device.FindByCode(code);
+
+    /// <summary>
     /// 写设备历史
     /// </summary>
     /// <param name="device"></param>
@@ -395,10 +404,10 @@ public class MyDeviceService
     /// <param name="success"></param>
     /// <param name="remark"></param>
     /// <param name="ip"></param>
-    public void WriteHistory(Device device, String action, Boolean success, String remark, String ip)
+    public void WriteHistory(IDeviceModel device, String action, Boolean success, String remark, String ip)
     {
         var traceId = DefaultSpan.Current?.TraceId;
-        var hi = DeviceHistory.Create(device ?? Current, action, success, remark, Environment.MachineName, ip, traceId);
+        var hi = DeviceHistory.Create(device as Device, action, success, remark, Environment.MachineName, ip, traceId);
     }
     #endregion
 }
\ No newline at end of file
Modified +4 -3
diff --git a/IoTZero/Services/ThingService.cs b/IoTZero/Services/ThingService.cs
index 48ba707..6e333ad 100644
--- a/IoTZero/Services/ThingService.cs
+++ b/IoTZero/Services/ThingService.cs
@@ -4,6 +4,7 @@ using NewLife.Caching;
 using NewLife.Data;
 using NewLife.IoT.ThingModels;
 using NewLife.Log;
+using NewLife.Remoting.Extensions.Services;
 using NewLife.Security;
 
 namespace IoTZero.Services;
@@ -13,7 +14,7 @@ public class ThingService
 {
     private readonly DataService _dataService;
     private readonly QueueService _queueService;
-    private readonly MyDeviceService _deviceService;
+    private readonly IDeviceService _deviceService;
     private readonly ICacheProvider _cacheProvider;
     private readonly IoTSetting _setting;
     private readonly ITracer _tracer;
@@ -30,7 +31,7 @@ public class ThingService
     /// <param name="cacheProvider"></param>
     /// <param name="setting"></param>
     /// <param name="tracer"></param>
-    public ThingService(DataService dataService, QueueService queueService, MyDeviceService deviceService, ICacheProvider cacheProvider, IoTSetting setting, ITracer tracer)
+    public ThingService(DataService dataService, QueueService queueService, IDeviceService deviceService, ICacheProvider cacheProvider, IoTSetting setting, ITracer tracer)
     {
         _dataService = dataService;
         _queueService = queueService;
@@ -64,7 +65,7 @@ public class ThingService
         }
 
         // 自动上线
-        if (device != null) _deviceService.SetDeviceOnline(device, ip, kind);
+        if (device != null && _deviceService is MyDeviceService ds) ds.SetDeviceOnline(device, ip, kind);
 
         //todo 触发指定设备的联动策略