NewLife/ZeroIoT

创建轻量级IoT项目
大石头 authored at 2023-05-20 12:34:44
ad4876a
Tree
0 Parent(s)
Summary: 35 changed files with 2118 additions and 0 deletions.
Added +215 -0
Added +24 -0
Added +25 -0
Added +0 -0
Added +0 -0
Added +108 -0
Added +58 -0
Added +0 -0
Added +39 -0
Added +45 -0
Added +9 -0
Added +15 -0
Added +190 -0
Added +225 -0
Added +39 -0
Added +124 -0
Added +99 -0
Added +13 -0
Added +10 -0
Added +6 -0
Added +19 -0
Added +14 -0
Added +90 -0
Added +10 -0
Added +16 -0
Added +10 -0
Added +14 -0
Added +36 -0
Added +89 -0
Added +28 -0
Added +130 -0
Added +102 -0
Added +258 -0
Added +15 -0
Added +43 -0
Added +215 -0
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..1648594
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,215 @@
+# EditorConfig is awesome:http://EditorConfig.org
+# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference
+
+# top-most EditorConfig file
+root = true
+
+# Don't use tabs for indentation.
+[*]
+indent_style = space
+# (Please don't specify an indent_size here; that has too many unintended consequences.)
+
+# Code files
+[*.{cs,csx,vb,vbx}]
+indent_size = 4
+insert_final_newline = false
+charset = utf-8-bom
+end_of_line = crlf
+
+# Xml project files
+[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}]
+indent_size = 2
+
+# Xml config files
+[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}]
+indent_size = 2
+
+# JSON files
+[*.json]
+indent_size = 2
+
+# Dotnet code style settings:
+[*.{cs,vb}]
+# Sort using and Import directives with System.* appearing first
+dotnet_sort_system_directives_first = true
+
+csharp_indent_case_contents = true
+csharp_indent_switch_labels = true
+csharp_indent_labels = flush_left
+
+#csharp_space_after_cast = true
+#csharp_space_after_keywords_in_control_flow_statements = true
+#csharp_space_between_method_declaration_parameter_list_parentheses = true
+#csharp_space_between_method_call_parameter_list_parentheses = true
+#csharp_space_between_parentheses = control_flow_statements, type_casts
+
+# 单行放置代码
+csharp_preserve_single_line_statements = true
+csharp_preserve_single_line_blocks = true
+
+# Avoid "this." and "Me." if not necessary
+dotnet_style_qualification_for_field = false:warning
+dotnet_style_qualification_for_property = false:warning
+dotnet_style_qualification_for_method = false:warning
+dotnet_style_qualification_for_event = false:warning
+
+# Use language keywords instead of framework type names for type references
+dotnet_style_predefined_type_for_locals_parameters_members = false:suggestion
+dotnet_style_predefined_type_for_member_access = false:suggestion
+#dotnet_style_require_accessibility_modifiers = for_non_interface_members:none/always:suggestion
+
+# Suggest more modern language features when available
+dotnet_style_object_initializer = true:suggestion
+dotnet_style_collection_initializer = true:suggestion
+dotnet_style_coalesce_expression = true:suggestion
+dotnet_style_null_propagation = true:suggestion
+dotnet_style_explicit_tuple_names = true:suggestion
+dotnet_style_prefer_inferred_tuple_names = true:suggestion
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
+
+# CSharp code style settings:
+[*.cs]
+# Prefer "var" everywhere
+csharp_style_var_for_built_in_types = true:warning
+csharp_style_var_when_type_is_apparent = true:warning
+csharp_style_var_elsewhere = true:warning
+
+# Prefer method-like constructs to have a block body
+csharp_style_expression_bodied_methods = when_on_single_line:suggestion
+csharp_style_expression_bodied_constructors = when_on_single_line:suggestion
+csharp_style_expression_bodied_operators = when_on_single_line:suggestion
+
+# Prefer property-like constructs to have an expression-body
+csharp_style_expression_bodied_properties = true:suggestion
+csharp_style_expression_bodied_indexers = true:suggestion
+#csharp_style_expression_bodied_accessors = true:suggestion
+
+# Suggest more modern language features when available
+csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
+csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
+csharp_style_inlined_variable_declaration = true:suggestion
+
+csharp_prefer_simple_default_expression = true:suggestion
+csharp_style_deconstructed_variable_declaration = true:suggestion
+csharp_style_pattern_local_over_anonymous_function = true:suggestion
+
+csharp_style_throw_expression = true:suggestion
+csharp_style_conditional_delegate_call = true:suggestion
+
+# 单行不需要大括号
+csharp_prefer_braces = false:suggestion
+
+# Newline settings
+csharp_new_line_before_open_brace = all
+csharp_new_line_before_else = true
+csharp_new_line_before_catch = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_members_in_anonymous_types = true
+csharp_new_line_between_query_expression_clauses = true
+csharp_using_directive_placement = outside_namespace:silent
+csharp_style_expression_bodied_accessors = true:silent
+csharp_style_expression_bodied_lambdas = true:silent
+csharp_style_expression_bodied_local_functions = false:silent
+csharp_prefer_simple_using_statement = true:suggestion
+csharp_style_namespace_declarations = block_scoped:silent
+
+[*.md]
+trim_trailing_whitespace = false
+[*.cs]
+#### 命名样式 ####
+
+# 命名规则
+
+dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
+dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
+dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
+
+dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.types_should_be_pascal_case.symbols = types
+dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
+
+dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
+dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
+
+# 符号规范
+
+dotnet_naming_symbols.interface.applicable_kinds = interface
+dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.interface.required_modifiers = 
+
+dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
+dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.types.required_modifiers = 
+
+dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
+dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.non_field_members.required_modifiers = 
+
+# 命名样式
+
+dotnet_naming_style.begins_with_i.required_prefix = I
+dotnet_naming_style.begins_with_i.required_suffix = 
+dotnet_naming_style.begins_with_i.word_separator = 
+dotnet_naming_style.begins_with_i.capitalization = pascal_case
+
+dotnet_naming_style.pascal_case.required_prefix = 
+dotnet_naming_style.pascal_case.required_suffix = 
+dotnet_naming_style.pascal_case.word_separator = 
+dotnet_naming_style.pascal_case.capitalization = pascal_case
+
+dotnet_naming_style.pascal_case.required_prefix = 
+dotnet_naming_style.pascal_case.required_suffix = 
+dotnet_naming_style.pascal_case.word_separator = 
+dotnet_naming_style.pascal_case.capitalization = pascal_case
+csharp_prefer_static_local_function = true:suggestion
+
+[*.vb]
+#### 命名样式 ####
+
+# 命名规则
+
+dotnet_naming_rule.interface_should_be_以_i_开始.severity = suggestion
+dotnet_naming_rule.interface_should_be_以_i_开始.symbols = interface
+dotnet_naming_rule.interface_should_be_以_i_开始.style = 以_i_开始
+
+dotnet_naming_rule.类型_should_be_帕斯卡拼写法.severity = suggestion
+dotnet_naming_rule.类型_should_be_帕斯卡拼写法.symbols = 类型
+dotnet_naming_rule.类型_should_be_帕斯卡拼写法.style = 帕斯卡拼写法
+
+dotnet_naming_rule.非字段成员_should_be_帕斯卡拼写法.severity = suggestion
+dotnet_naming_rule.非字段成员_should_be_帕斯卡拼写法.symbols = 非字段成员
+dotnet_naming_rule.非字段成员_should_be_帕斯卡拼写法.style = 帕斯卡拼写法
+
+# 符号规范
+
+dotnet_naming_symbols.interface.applicable_kinds = interface
+dotnet_naming_symbols.interface.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected
+dotnet_naming_symbols.interface.required_modifiers = 
+
+dotnet_naming_symbols.类型.applicable_kinds = class, struct, interface, enum
+dotnet_naming_symbols.类型.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected
+dotnet_naming_symbols.类型.required_modifiers = 
+
+dotnet_naming_symbols.非字段成员.applicable_kinds = property, event, method
+dotnet_naming_symbols.非字段成员.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected
+dotnet_naming_symbols.非字段成员.required_modifiers = 
+
+# 命名样式
+
+dotnet_naming_style.以_i_开始.required_prefix = I
+dotnet_naming_style.以_i_开始.required_suffix = 
+dotnet_naming_style.以_i_开始.word_separator = 
+dotnet_naming_style.以_i_开始.capitalization = pascal_case
+
+dotnet_naming_style.帕斯卡拼写法.required_prefix = 
+dotnet_naming_style.帕斯卡拼写法.required_suffix = 
+dotnet_naming_style.帕斯卡拼写法.word_separator = 
+dotnet_naming_style.帕斯卡拼写法.capitalization = pascal_case
+
+dotnet_naming_style.帕斯卡拼写法.required_prefix = 
+dotnet_naming_style.帕斯卡拼写法.required_suffix = 
+dotnet_naming_style.帕斯卡拼写法.word_separator = 
+dotnet_naming_style.帕斯卡拼写法.capitalization = pascal_case
Added +24 -0
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..781ca98
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,24 @@
+name: test
+
+on:
+  push:
+    branches: [ '*' ]
+  pull_request:
+    branches: [ '*' ]
+  workflow_dispatch:
+
+jobs:
+  build-test:
+
+    runs-on: ubuntu-latest
+
+    steps:
+    - uses: actions/checkout@v3
+    - name: Setup .NET
+      uses: actions/setup-dotnet@v2
+      with:
+        dotnet-version: 6.0.x
+    - name: Build
+      run: dotnet build -c Release
+    - name: Test
+      run: dotnet test -c Release
Added +25 -0
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..88ac376
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,25 @@
+################################################################################
+# 此 .gitignore 文件已由 Microsoft(R) Visual Studio 自动创建。
+################################################################################
+
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+build/
+bld/
+[Bb]in/
+[Oo]bj/
+/.vs/
+/Log
+/packages
+/Data
+Content/
+Config/
+*.log
+*.user
+/Test
+/IoTData/Properties/PublishProfiles/FolderProfile.pubxml
+/IoTData/.config/dotnet-tools.json
Added +0 -0
diff --git "a/Doc/IoTEdge\346\236\266\346\236\204.eddx" "b/Doc/IoTEdge\346\236\266\346\236\204.eddx"
new file mode 100644
index 0000000..5346117
Binary files /dev/null and "b/Doc/IoTEdge\346\236\266\346\236\204.eddx" differ
Added +0 -0
diff --git "a/Doc/IoT\345\244\247\346\225\260\346\215\256\345\255\230\345\202\250.eddx" "b/Doc/IoT\345\244\247\346\225\260\346\215\256\345\255\230\345\202\250.eddx"
new file mode 100644
index 0000000..f3d1b66
Binary files /dev/null and "b/Doc/IoT\345\244\247\346\225\260\346\215\256\345\255\230\345\202\250.eddx" differ
Added +108 -0
diff --git a/IoT.Data/Entity/Model.xml b/IoT.Data/Entity/Model.xml
new file mode 100644
index 0000000..76d3167
--- /dev/null
+++ b/IoT.Data/Entity/Model.xml
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Tables xmlns:xs="http://www.w3.org/2001/XMLSchema-instance" xs:schemaLocation="https://newlifex.com https://newlifex.com/Model2022.xsd" NameSpace="IoT.Data" ConnName="IoT" Output="" BaseClass="Entity" Version="11.3.2022.1013" Document="https://newlifex.com/xcode/model" DisplayName="" CubeOutput="" xmlns="https://newlifex.com/Model2022.xsd">
+  <Table Name="Product" Description="产品。设备的集合,通常指一组具有相同功能的设备。物联网平台为每个产品颁发全局唯一的ProductKey。">
+    <Columns>
+      <Column Name="Id" DataType="Int32" Identity="True" PrimaryKey="True" Description="编号" />
+      <Column Name="Name" DataType="String" Master="True" Description="名称" />
+      <Column Name="Code" DataType="String" Description="编码。ProductKey" />
+      <Column Name="Enable" DataType="Boolean" Description="启用。开发中/已发布" />
+      <Column Name="DeviceCount" DataType="Int32" Description="设备数量" />
+      <Column Name="CreateUser" DataType="String" Description="创建人" Model="False" Category="扩展" />
+      <Column Name="CreateUserId" DataType="Int32" Description="创建者" Model="False" Category="扩展" />
+      <Column Name="CreateTime" DataType="DateTime" Description="创建时间" Model="False" Category="扩展" />
+      <Column Name="CreateIP" DataType="String" Description="创建地址" Model="False" Category="扩展" />
+      <Column Name="UpdateUser" DataType="String" Description="更新人" Model="False" Category="扩展" />
+      <Column Name="UpdateUserId" DataType="Int32" Description="更新者" Model="False" Category="扩展" />
+      <Column Name="UpdateTime" DataType="DateTime" Description="更新时间" Model="False" Category="扩展" />
+      <Column Name="UpdateIP" DataType="String" Description="更新地址" Model="False" Category="扩展" />
+      <Column Name="Remark" DataType="String" Length="500" Description="描述" Category="扩展" />
+    </Columns>
+    <Indexes>
+      <Index Columns="Code" Unique="True" />
+    </Indexes>
+  </Table>
+  <Table Name="Device" Description="设备。归属于某个产品下的具体设备。物联网平台为设备颁发产品内唯一的证书DeviceName。设备可以直接连接物联网平台,也可以作为子设备通过网关连接物联网平台。">
+    <Columns>
+      <Column Name="Id" DataType="Int32" Identity="True" PrimaryKey="True" Description="编号" />
+      <Column Name="Name" DataType="String" Master="True" Description="名称" />
+      <Column Name="Code" DataType="String" Description="编码。设备唯一证书DeviceName,用于设备认证,在注册时由系统生成" />
+      <Column Name="ProductId" DataType="Int32" Description="产品" />
+      <Column Name="Enable" DataType="Boolean" Description="启用" />
+      <Column Name="Online" DataType="Boolean" Description="在线" />
+      <Column Name="Uuid" DataType="String" Length="200" Description="唯一标识。硬件标识,或其它能够唯一区分设备的标记" />
+      <Column Name="Location" DataType="String" Description="位置。场地安装位置,或者经纬度" Category="登录信息" />
+      <Column Name="PollingTime" DataType="Int32" Description="采集间隔。默认1000ms" Category="参数设置" />
+      <Column Name="CreateUserId" DataType="Int32" Description="创建者" Model="False" Category="扩展" />
+      <Column Name="CreateTime" DataType="DateTime" Description="创建时间" Model="False" Category="扩展" />
+      <Column Name="CreateIP" DataType="String" Description="创建地址" Model="False" Category="扩展" />
+      <Column Name="UpdateUserId" DataType="Int32" Description="更新者" Model="False" Category="扩展" />
+      <Column Name="UpdateTime" DataType="DateTime" Description="更新时间" Model="False" Category="扩展" />
+      <Column Name="UpdateIP" DataType="String" Description="更新地址" Model="False" Category="扩展" />
+      <Column Name="Remark" DataType="String" Length="500" Description="描述" Category="扩展" />
+    </Columns>
+    <Indexes>
+      <Index Columns="Code" Unique="True" />
+      <Index Columns="ProductId" />
+      <Index Columns="Uuid" />
+      <Index Columns="UpdateTime" />
+    </Indexes>
+  </Table>
+  <Table Name="DeviceHistory" Description="设备历史。记录设备上线下线等操作" >
+    <Columns>
+      <Column Name="Id" DataType="Int64" PrimaryKey="True" Description="编号" />
+      <Column Name="DeviceId" DataType="Int32" Description="设备" />
+      <Column Name="Name" DataType="String" Master="True" Description="名称" />
+      <Column Name="Action" DataType="String" Description="操作" />
+      <Column Name="Success" DataType="Boolean" Description="成功" />
+      <Column Name="TraceId" DataType="String" Description="追踪。用于记录调用链追踪标识,在APM查找调用链" />
+      <Column Name="Creator" DataType="String" Description="创建者。服务端设备" />
+      <Column Name="CreateTime" DataType="DateTime" Description="创建时间" Model="False" />
+      <Column Name="CreateIP" DataType="String" Description="创建地址" Model="False" />
+      <Column Name="Remark" DataType="String" Length="2000" Description="内容" />
+    </Columns>
+    <Indexes>
+      <Index Columns="DeviceId,Id" />
+      <Index Columns="DeviceId,Action,Id" />
+    </Indexes>
+  </Table>
+  <Table Name="DeviceProperty" Description="设备属性。设备的功能模型之一,一般用于描述设备运行时的状态,如环境监测设备所读取的当前环境温度等。一个设备有多个属性,名值表" >
+    <Columns>
+      <Column Name="Id" DataType="Int32" Identity="True" PrimaryKey="True" Description="编号" />
+      <Column Name="DeviceId" DataType="Int32" Description="设备" />
+      <Column Name="Name" DataType="String" Master="True" Description="名称" />
+      <Column Name="NickName" DataType="String" Description="昵称" />
+      <Column Name="Type" DataType="String" Description="类型" />
+      <Column Name="Value" DataType="String" Length="-1" Description="数值。设备上报数值" />
+      <Column Name="Unit" DataType="String" Description="单位" />
+      <Column Name="Enable" DataType="Boolean" Description="启用" />
+      <Column Name="TraceId" DataType="String" Description="追踪。用于记录调用链追踪标识,在APM查找调用链" Model="False" Category="扩展" />
+      <Column Name="CreateTime" DataType="DateTime" Description="创建时间" Model="False" Category="扩展" />
+      <Column Name="CreateIP" DataType="String" Description="创建地址" Model="False" Category="扩展" />
+      <Column Name="UpdateTime" DataType="DateTime" Description="更新时间" Model="False" Category="扩展" />
+      <Column Name="UpdateIP" DataType="String" Description="更新地址" Model="False" Category="扩展" />
+    </Columns>
+    <Indexes>
+      <Index Columns="DeviceId,Name" Unique="True" />
+      <Index Columns="UpdateTime" />
+    </Indexes>
+  </Table>
+  <Table Name="DeviceData" Description="设备数据。设备采集原始数据,按天分表存储" >
+    <Columns>
+      <Column Name="Id" DataType="Int64" PrimaryKey="True" Description="编号" />
+      <Column Name="DeviceId" DataType="Int32" Description="设备" />
+      <Column Name="Name" DataType="String" Master="True" Description="名称。MQTT的Topic,或者属性名" />
+      <Column Name="Kind" DataType="String" Description="类型。数据来源,如PostProperty/PostData/MqttPostData" />
+      <Column Name="Value" DataType="String" Length="2000" Description="数值" />
+      <Column Name="Timestamp" DataType="Int64" Description="时间戳。设备生成数据时的UTC毫秒" />
+      <Column Name="TraceId" DataType="String" Description="追踪标识。用于记录调用链追踪标识,在APM查找调用链" Model="False" Category="扩展" />
+      <Column Name="Creator" DataType="String" Description="创建者。服务端设备" Model="False" Category="扩展" />
+      <Column Name="CreateTime" DataType="DateTime" Description="创建时间" Model="False" Category="扩展" />
+      <Column Name="CreateIP" DataType="String" Description="创建地址" Model="False" Category="扩展" />
+    </Columns>
+    <Indexes>
+      <Index Columns="DeviceId,Id" />
+      <Index Columns="DeviceId,Name,Id" />
+      <Index Columns="DeviceId,Kind,Id" />
+    </Indexes>
+  </Table>
+</Tables>
\ No newline at end of file
Added +58 -0
diff --git a/IoT.Data/IoT.Data.csproj b/IoT.Data/IoT.Data.csproj
new file mode 100644
index 0000000..47de34a
--- /dev/null
+++ b/IoT.Data/IoT.Data.csproj
@@ -0,0 +1,58 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>netstandard2.1</TargetFramework>
+    <AssemblyTitle>物联网数据层</AssemblyTitle>
+    <Description>IoT数据层</Description>
+    <Company>新生命开发团队</Company>
+    <Copyright>©2002-2022 NewLife</Copyright>
+    <Version>1.2.2022.0214</Version>
+    <FileVersion>1.2.2022.0214</FileVersion>
+    <AssemblyVersion>1.2.*</AssemblyVersion>
+    <Deterministic>false</Deterministic>
+    <LangVersion>latest</LangVersion>
+    <GenerateDocumentationFile>True</GenerateDocumentationFile>
+  </PropertyGroup>
+
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
+    <NoWarn>1701;1702;1591</NoWarn>
+  </PropertyGroup>
+
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
+    <NoWarn>1701;1702;1591</NoWarn>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <Compile Remove="Entity\Config\**" />
+    <Compile Remove="Entity\Log\**" />
+    <EmbeddedResource Remove="Entity\Config\**" />
+    <EmbeddedResource Remove="Entity\Log\**" />
+    <None Remove="Entity\Config\**" />
+    <None Remove="Entity\Log\**" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <Compile Remove="Entity\告警历史.Biz.cs" />
+    <Compile Remove="Entity\告警历史.cs" />
+    <Compile Remove="Entity\告警设置.Biz.cs" />
+    <Compile Remove="Entity\告警设置.cs" />
+    <Compile Remove="Entity\子设备.Biz.cs" />
+    <Compile Remove="Entity\子设备.cs" />
+    <Compile Remove="Entity\设备分组.Biz.cs" />
+    <Compile Remove="Entity\设备分组.cs" />
+    <Compile Remove="Entity\设备命令.Biz.cs" />
+    <Compile Remove="Entity\设备命令.cs" />
+    <Compile Remove="Entity\设备服务状态.Biz.cs" />
+    <Compile Remove="Entity\设备服务状态.cs" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <PackageReference Include="NewLife.IoT" Version="1.7.2023.205" />
+    <PackageReference Include="NewLife.XCode" Version="11.5.2023.203" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <Service Include="{508349b6-6b84-4df5-91f0-309beebad82d}" />
+  </ItemGroup>
+
+</Project>
Added +0 -0
diff --git a/IoT.Data/xcodetool.exe b/IoT.Data/xcodetool.exe
new file mode 100644
index 0000000..e4b8ea3
Binary files /dev/null and b/IoT.Data/xcodetool.exe differ
Added +39 -0
diff --git a/IoTEdge/IoTEdge.csproj b/IoTEdge/IoTEdge.csproj
new file mode 100644
index 0000000..4f31944
--- /dev/null
+++ b/IoTEdge/IoTEdge.csproj
@@ -0,0 +1,39 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <OutputType>Exe</OutputType>
+    <TargetFramework>net7.0</TargetFramework>
+    <AssemblyTitle>物联网网关</AssemblyTitle>
+    <Description>IoT边缘网关</Description>
+    <Company>新生命开发团队</Company>
+    <Copyright>©2002-2023 NewLife</Copyright>
+    <VersionPrefix>1.0</VersionPrefix>
+    <VersionSuffix>$([System.DateTime]::Now.ToString(`yyyy.MMdd`))</VersionSuffix>
+    <Version>$(VersionPrefix).$(VersionSuffix)</Version>
+    <FileVersion>$(Version)</FileVersion>
+    <AssemblyVersion>$(VersionPrefix).*</AssemblyVersion>
+    <Deterministic>false</Deterministic>
+    <OutputPath>..\Bin\IoTEdge</OutputPath>
+    <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
+    <GenerateDocumentationFile>True</GenerateDocumentationFile>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <LangVersion>latest</LangVersion>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="NewLife.BACnet" Version="1.0.2023.520-beta0022" />
+    <PackageReference Include="NewLife.Core" Version="10.3.2023.519-beta1319" />
+    <PackageReference Include="NewLife.IoT" Version="1.8.2023.511" />
+    <PackageReference Include="NewLife.Modbus" Version="1.6.2023.511" />
+    <PackageReference Include="NewLife.ModbusRTU" Version="1.6.2023.511" />
+    <PackageReference Include="NewLife.MQTT" Version="1.3.2023.401" />
+    <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.8.2023.520-beta0003" />
+    <PackageReference Include="SmartA2" Version="1.0.2023.519-beta0731" />
+    <PackageReference Include="SmartA4" Version="1.0.2023.519" />
+  </ItemGroup>
+
+</Project>
Added +45 -0
diff --git a/IoTEdge/Program.cs b/IoTEdge/Program.cs
new file mode 100644
index 0000000..528e127
--- /dev/null
+++ b/IoTEdge/Program.cs
@@ -0,0 +1,45 @@
+using NewLife;
+using NewLife.Log;
+using NewLife.Model;
+using NewLife.MQTT;
+using Stardust;
+
+//!!! 轻量级控制台项目模板
+
+// 启用控制台日志,拦截所有异常
+XTrace.UseConsole();
+
+// 初始化对象容器,提供注入能力
+var services = ObjectContainer.Current;
+services.AddSingleton(XTrace.Log);
+
+// 配置星尘。自动读取配置文件 config/star.config 中的服务器地址
+var star = new StarFactory();
+if (star.Server.IsNullOrEmpty()) star = null;
+
+// 初始化Redis、MQTT、RocketMQ,注册服务到容器
+InitMqtt(services, star?.Tracer);
+
+// 注册后台任务 IHostedService
+var host = services.BuildHost();
+//host.Add<Worker>();
+
+// 异步阻塞,友好退出
+await host.RunAsync();
+
+
+static void InitMqtt(IObjectContainer services, ITracer? tracer)
+{
+    // 引入 MQTT
+    var mqtt = new MqttClient
+    {
+        Tracer = tracer,
+        Log = XTrace.Log,
+
+        Server = "tcp://127.0.0.1:1883",
+        ClientId = Environment.MachineName,
+        UserName = "stone",
+        Password = "Pass@word",
+    };
+    services.AddSingleton(mqtt);
+}
\ No newline at end of file
Added +9 -0
diff --git a/IoTZero/appsettings.Development.json b/IoTZero/appsettings.Development.json
new file mode 100644
index 0000000..770d3e9
--- /dev/null
+++ b/IoTZero/appsettings.Development.json
@@ -0,0 +1,9 @@
+{
+  "DetailedErrors": true,
+  "Logging": {
+    "LogLevel": {
+      "Default": "Information",
+      "Microsoft.AspNetCore": "Warning"
+    }
+  }
+}
Added +15 -0
diff --git a/IoTZero/appsettings.json b/IoTZero/appsettings.json
new file mode 100644
index 0000000..afa02b5
--- /dev/null
+++ b/IoTZero/appsettings.json
@@ -0,0 +1,15 @@
+{
+  "Logging": {
+    "LogLevel": {
+      "Default": "Information",
+      "Microsoft.AspNetCore": "Warning"
+    }
+  },
+  "AllowedHosts": "*",
+  "Urls": "http://*:1880",
+  "ConnectionStrings": {
+    "IoT": "Data Source=..\\Data\\IoT.db;Provider=Sqlite",
+    "IoTData": "Data Source=..\\Data\\IoTData.db;ShowSql=false;Provider=Sqlite",
+    "Membership": "Data Source=..\\Data\\Membership.db;Provider=Sqlite"
+  }
+}
Added +190 -0
diff --git a/IoTZero/Areas/IoT/Controllers/DeviceController.cs b/IoTZero/Areas/IoT/Controllers/DeviceController.cs
new file mode 100644
index 0000000..a4cee68
--- /dev/null
+++ b/IoTZero/Areas/IoT/Controllers/DeviceController.cs
@@ -0,0 +1,190 @@
+using System.ComponentModel;
+using IoT.Data;
+using IoTEdge.Models;
+using IoTEdge.Services;
+using Microsoft.AspNetCore.Mvc;
+using NewLife;
+using NewLife.Cube;
+using NewLife.IoT.Drivers;
+using NewLife.Log;
+using NewLife.Serialization;
+using NewLife.Web;
+using XCode;
+using XCode.Membership;
+
+namespace IoTEdge.Areas.IoT.Controllers;
+
+[IoTArea]
+[DisplayName("设备管理")]
+[Menu(80, true, Icon = "fa-mobile")]
+public class DeviceController : EntityController<Device>
+{
+    private readonly ITracer _tracer;
+    private readonly PointImportService _pointService;
+
+    static DeviceController()
+    {
+        LogOnChange = true;
+
+        ListFields.RemoveField("Secret", "Uuid", "ProvinceId", "IP", "Period", "Address", "Location", "Logins", "LastLogin", "LastLoginIP", "OnlineTime", "RegisterTime", "Remark", "AreaName");
+        ListFields.RemoveCreateField();
+        ListFields.RemoveUpdateField();
+
+        {
+            var df = ListFields.AddListField("history", "Online");
+            df.DisplayName = "历史";
+            df.Url = "/IoT/DeviceHistory?deviceId={Id}";
+        }
+
+        {
+            var df = ListFields.AddListField("property", "Online");
+            df.DisplayName = "属性";
+            df.Url = "/IoT/DeviceProperty?deviceId={Id}";
+        }
+
+        {
+            var df = ListFields.AddListField("service", "Online");
+            df.DisplayName = "服务";
+            df.Url = "/IoT/DeviceService?deviceId={Id}";
+        }
+
+        {
+            var df = ListFields.AddListField("data", "Online");
+            df.DisplayName = "数据";
+            df.Url = "/IoT/DeviceData?deviceId={Id}";
+        }
+
+        {
+            var df = ListFields.AddListField("event", "Online");
+            df.DisplayName = "事件";
+            df.Url = "/IoT/DeviceEvent?deviceId={Id}";
+        }
+    }
+
+    public DeviceController(ITracer tracer, PointImportService pointService)
+    {
+        _tracer = tracer;
+        _pointService = pointService;
+    }
+
+    protected override IEnumerable<Device> Search(Pager p)
+    {
+        var id = p["Id"].ToInt(-1);
+        if (id > 0)
+        {
+            var node = Device.FindById(id);
+            if (node != null) return new[] { node };
+        }
+
+        var productId = p["productId"].ToInt(-1);
+        var enable = p["enable"]?.ToBoolean();
+
+        var start = p["dtStart"].ToDateTime();
+        var end = p["dtEnd"].ToDateTime();
+
+        //// 如果没有指定产品和主设备,则过滤掉子设备
+        //if (productId < 0 && parentId < 0) parentId = 0;
+
+        return Device.Search(productId, enable, start, end, p["Q"], p);
+    }
+
+    protected override Boolean Valid(Device entity, DataObjectMethodType type, Boolean post)
+    {
+        var fs = type switch
+        {
+            DataObjectMethodType.Insert => AddFormFields,
+            DataObjectMethodType.Update => EditFormFields,
+            _ => null,
+        };
+
+        if (post)
+        {
+            // 使用驱动格式化配置数据
+            var driver = entity.Product?.ProtocolType;
+            if (!driver.IsNullOrEmpty())
+                try
+                {
+                    var drv = DriverFactory.Create(driver, null);
+                    var pm = drv?.GetDefaultParameter();
+                    if (pm != null)
+                        if (entity.Parameter.IsNullOrEmpty())
+                            entity.Parameter = pm.ToJson(true);
+                        else
+                        {
+                            // 添加缺失配置项
+                            var dic = JsonParser.Decode(entity.Parameter);
+                            var dic2 = pm.ToDictionary();
+                            foreach (var item in dic2)
+                                if (!dic.ContainsKey(item.Key)) dic[item.Key] = item.Value;
+                            entity.Parameter = dic.ToJson(true);
+                        }
+                }
+                catch (Exception ex)
+                {
+                    XTrace.WriteException(ex);
+                }
+        }
+
+        return base.Valid(entity, type, post);
+    }
+
+    protected override Int32 OnInsert(Device entity)
+    {
+        var rs = base.OnInsert(entity);
+
+        // 复制产品属性
+        entity.Fix(true);
+
+        entity.Product?.Fix();
+        return rs;
+    }
+
+    protected override Int32 OnUpdate(Device entity)
+    {
+        var rs = base.OnUpdate(entity);
+
+        entity.Fix(false);
+
+        entity.Product?.Fix();
+
+        return rs;
+    }
+
+    protected override Int32 OnDelete(Device entity)
+    {
+        // 删除设备时需要顺便把设备属性删除
+        var dpList = DeviceProperty.FindAllByDeviceId(entity.Id);
+        _ = dpList.Delete();
+
+        var rs = base.OnDelete(entity);
+
+        entity.Product?.Fix();
+
+        return rs;
+    }
+
+    /// <summary>点位导入页面</summary>
+    /// <param name="id"></param>
+    /// <returns></returns>
+    public ActionResult Import(Int32 id)
+    {
+        var dv = Device.FindById(id);
+
+        return View("Import", dv);
+    }
+
+    /// <summary>点位数据上传导入</summary>
+    /// <param name="model"></param>
+    /// <param name="file"></param>
+    /// <returns></returns>
+    [HttpPost]
+    [EntityAuthorize(PermissionFlags.Insert)]
+    public ActionResult Upload(PointImportModel model, IFormFile file)
+    {
+        using var span = _tracer?.NewSpan("DataImport", model);
+
+        _pointService.Import(model, file.OpenReadStream());
+
+        return RedirectToAction("Index", new { id = model.Id });
+    }
+}
\ No newline at end of file
Added +225 -0
diff --git a/IoTZero/Areas/IoT/Controllers/DeviceDataController.cs b/IoTZero/Areas/IoT/Controllers/DeviceDataController.cs
new file mode 100644
index 0000000..70cfc8c
--- /dev/null
+++ b/IoTZero/Areas/IoT/Controllers/DeviceDataController.cs
@@ -0,0 +1,225 @@
+using IoT.Data;
+using NewLife;
+using NewLife.Algorithms;
+using NewLife.Cube;
+using NewLife.Cube.Charts;
+using NewLife.Cube.Extensions;
+using NewLife.Cube.ViewModels;
+using NewLife.Data;
+using NewLife.Web;
+using XCode;
+using XCode.Membership;
+
+namespace IoTEdge.Areas.IoT.Controllers;
+
+[IoTArea]
+[Menu(0, false)]
+public class DeviceDataController : EntityController<DeviceData>
+{
+    static DeviceDataController()
+    {
+        ListFields.RemoveField("Id");
+        ListFields.AddListField("Value", null, "Kind");
+
+        {
+            var df = ListFields.GetField("Name") as ListField;
+            //df.DisplayName = "主题";
+            df.Url = "/IoT/DeviceData?deviceId={DeviceId}&name={Name}";
+        }
+        ListFields.TraceUrl("TraceId");
+    }
+
+    protected override IEnumerable<DeviceData> Search(Pager p)
+    {
+        var deviceId = p["deviceId"].ToInt(-1);
+        var name = p["name"];
+
+        var start = p["dtStart"].ToDateTime();
+        var end = p["dtEnd"].ToDateTime();
+
+        if (start.Year < 2000)
+        {
+            start = DateTime.Today;
+            p["dtStart"] = start.ToString("yyyy-MM-dd");
+            p["dtEnd"] = start.ToString("yyyy-MM-dd");
+        }
+
+        if (deviceId > 0 && p.PageSize == 20 && !name.IsNullOrEmpty() && !name.StartsWithIgnoreCase("raw-", "channel-")) p.PageSize = 14400;
+
+        var list = DeviceData.Search(deviceId, name, start, end, p["Q"], p);
+
+        // 单一设备绘制曲线
+        if (list.Count > 0 && deviceId > 0)
+        {
+            var list2 = list.Where(e => !e.Name.StartsWithIgnoreCase("raw-", "channel-") && e.Value.ToDouble(-1) >= 0).OrderBy(e => e.Id).ToList();
+
+            // 绘制曲线图
+            if (list2.Count > 0)
+            {
+                var topics = list2.Select(e => e.Name).Distinct().ToList();
+                var datax = list2.GroupBy(e => e.CreateTime).ToDictionary(e => e.Key, e => e.ToList());
+                //var topics = list2.GroupBy(e => e.Topic).ToDictionary(e => e.Key, e => e.ToList());
+                var chart = new ECharts
+                {
+                    Height = 400,
+                };
+                //chart.SetX(list2, _.CreateTime, e => e.CreateTime.ToString("mm:ss"));
+
+                // 构建X轴
+                var minT = datax.Keys.Min();
+                var maxT = datax.Keys.Max();
+                var step = p["sample"].ToInt(-1);
+                if (step > 0)
+                {
+                    if (step <= 60)
+                    {
+                        minT = new DateTime(minT.Year, minT.Month, minT.Day, minT.Hour, minT.Minute, 0, minT.Kind);
+                        maxT = new DateTime(maxT.Year, maxT.Month, maxT.Day, maxT.Hour, maxT.Minute, 0, maxT.Kind);
+                    }
+                    else
+                    {
+                        minT = new DateTime(minT.Year, minT.Month, minT.Day, minT.Hour, 0, 0, minT.Kind);
+                        maxT = new DateTime(maxT.Year, maxT.Month, maxT.Day, maxT.Hour, 0, 0, maxT.Kind);
+                        //step = 3600;
+                    }
+                    var times = new List<DateTime>();
+                    for (var dt = minT; dt <= maxT; dt = dt.AddSeconds(step))
+                    {
+                        times.Add(dt);
+                    }
+
+                    if (step < 60)
+                    {
+                        chart.XAxis = new
+                        {
+                            data = times.Select(e => e.ToString("HH:mm:ss")).ToArray(),
+                        };
+                    }
+                    else
+                    {
+                        chart.XAxis = new
+                        {
+                            data = times.Select(e => e.ToString("dd-HH:mm")).ToArray(),
+                        };
+                    }
+                }
+                else
+                {
+                    chart.XAxis = new
+                    {
+                        data = datax.Keys.Select(e => e.ToString("HH:mm:ss")).ToArray(),
+                    };
+                }
+                chart.SetY("数值");
+
+                var max = -9999.0;
+                var min = 9999.0;
+                var dps = DeviceProperty.FindAllByDeviceId(deviceId);
+                var sample = new AverageSampling();
+                //var sample = new LTTBSampling();
+                foreach (var item in topics)
+                {
+                    var name2 = item;
+
+                    // 使用属性名
+                    var dp = dps.FirstOrDefault(e => e.Name == item);
+                    if (dp != null && !dp.NickName.IsNullOrEmpty()) name2 = dp.NickName;
+
+                    var series = new Series
+                    {
+                        Name = name2,
+                        Type = "line",
+                        //Data = tps2.Select(e => Math.Round(e.Value)).ToArray(),
+                        Smooth = true,
+                    };
+
+                    if (step > 0)
+                    {
+                        //var minD = minT.Date.ToInt();
+                        var tps = new List<TimePoint>();
+                        foreach (var elm in datax)
+                        {
+                            // 可能该Topic在这个时刻没有数据,写入空
+                            var v = elm.Value.FirstOrDefault(e => e.Name == item);
+                            if (v != null)
+                                tps.Add(new TimePoint { Time = v.CreateTime.ToInt(), Value = v.Value.ToDouble() });
+                        }
+
+                        var tps2 = sample.Process(tps.ToArray(), step);
+
+                        series.Data = tps2.Select(e => Math.Round(e.Value, 2)).ToArray();
+
+                        var m1 = tps2.Select(e => e.Value).Min();
+                        if (m1 < min) min = m1;
+                        var m2 = tps2.Select(e => e.Value).Max();
+                        if (m2 > max) max = m2;
+                    }
+                    else
+                    {
+                        var list3 = new List<Object>();
+                        foreach (var elm in datax)
+                        {
+                            // 可能该Topic在这个时刻没有数据,写入空
+                            var v = elm.Value.FirstOrDefault(e => e.Name == item);
+                            if (v != null)
+                                list3.Add(v.Value);
+                            else
+                                list3.Add('-');
+                        }
+                        series.Data = list3;
+
+                        var m1 = list3.Where(e => e + "" != "-").Select(e => e.ToDouble()).Min();
+                        if (m1 < min) min = m1;
+                        var m2 = list3.Where(e => e + "" != "-").Select(e => e.ToDouble()).Max();
+                        if (m2 > max) max = m2;
+                    }
+
+                    // 单一曲线,显示最大最小和平均
+                    if (topics.Count == 1)
+                    {
+                        name = name2;
+                        series["markPoint"] = new
+                        {
+                            data = new[] {
+                                new{ type="max",name="Max"},
+                                new{ type="min",name="Min"},
+                            }
+                        };
+                        series["markLine"] = new
+                        {
+                            data = new[] {
+                                new{ type="average",name="Avg"},
+                            }
+                        };
+                    }
+
+                    // 降采样策略 lttb/average/max/min/sum
+                    series["sampling"] = "lttb";
+                    series["symbol"] = "none";
+
+                    // 开启动画
+                    series["animation"] = true;
+
+                    chart.Add(series);
+                }
+                chart.SetTooltip();
+                chart.YAxis = new
+                {
+                    name = "数值",
+                    type = "value",
+                    min = Math.Ceiling(min) - 1,
+                    max = Math.Ceiling(max),
+                };
+                ViewBag.Charts = new[] { chart };
+
+                // 减少数据显示,避免卡页面
+                list = list.Take(100).ToList();
+
+                var ar = Device.FindById(deviceId);
+                if (ar != null) ViewBag.Title = topics.Count == 1 ? $"{name} - {ar}数据" : $"{ar}数据";
+            }
+        }
+
+        return list;
+    }
+}
\ No newline at end of file
Added +39 -0
diff --git a/IoTZero/Areas/IoT/Controllers/DeviceHistoryController.cs b/IoTZero/Areas/IoT/Controllers/DeviceHistoryController.cs
new file mode 100644
index 0000000..b49e50a
--- /dev/null
+++ b/IoTZero/Areas/IoT/Controllers/DeviceHistoryController.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Collections.Generic;
+using IoT.Data;
+using NewLife.Cube;
+using NewLife.Web;
+using XCode.Membership;
+
+namespace IoTEdge.Areas.IoT.Controllers
+{
+    [IoTArea]
+    [Menu(0, false)]
+    public class DeviceHistoryController : ReadOnlyEntityController<DeviceHistory>
+    {
+        static DeviceHistoryController() => ListFields.RemoveField("ProvinceId");
+
+        protected override IEnumerable<DeviceHistory> Search(Pager p)
+        {
+            var deviceId = p["deviceId"].ToInt(-1);
+            var action = p["action"];
+
+            var start = p["dtStart"].ToDateTime();
+            var end = p["dtEnd"].ToDateTime();
+
+            //if (start.Year < 2000)
+            //{
+            //    start = new DateTime(DateTime.Today.Year, 1, 1);
+            //    p["dtStart"] = start.ToString("yyyy-MM-dd");
+            //}
+
+            if (start.Year < 2000)
+            {
+                using var split = DeviceHistory.Meta.CreateShard(DateTime.Today);
+                return DeviceHistory.Search(deviceId, action, start, end, p["Q"], p);
+            }
+            else
+                return DeviceHistory.Search(deviceId, action, start, end, p["Q"], p);
+        }
+    }
+}
\ No newline at end of file
Added +124 -0
diff --git a/IoTZero/Areas/IoT/Controllers/DevicePropertyController.cs b/IoTZero/Areas/IoT/Controllers/DevicePropertyController.cs
new file mode 100644
index 0000000..5f960d9
--- /dev/null
+++ b/IoTZero/Areas/IoT/Controllers/DevicePropertyController.cs
@@ -0,0 +1,124 @@
+using System.ComponentModel;
+using IoT.Data;
+using IoTEdge.Services;
+using Microsoft.AspNetCore.Mvc;
+using NewLife;
+using NewLife.Cube;
+using NewLife.Cube.Extensions;
+using NewLife.Cube.ViewModels;
+using NewLife.IoT;
+using NewLife.IoT.ThingModels;
+using NewLife.Serialization;
+using NewLife.Web;
+using XCode.Membership;
+
+namespace IoTEdge.Areas.IoT.Controllers;
+
+[IoTArea]
+[Menu(0, false)]
+public class DevicePropertyController : EntityController<DeviceProperty>
+{
+    private readonly ThingService _thingService;
+
+    static DevicePropertyController()
+    {
+        LogOnChange = true;
+
+        ListFields.RemoveField("UnitName", "Length", "Rule", "Readonly", "Locked", "Timestamp", "FunctionId", "Remark");
+        ListFields.RemoveCreateField();
+
+        ListFields.TraceUrl("TraceId");
+
+        {
+            var df = ListFields.GetField("DeviceName") as ListField;
+            df.Url = "/IoT/Device?Id={DeviceId}";
+        }
+        {
+            var df = ListFields.GetField("Name") as ListField;
+            df.Url = "/IoT/DeviceData?deviceId={DeviceId}&name={Name}";
+        }
+        {
+            var df = ListFields.AddDataField("Value", "Unit") as ListField;
+        }
+        {
+            var df = ListFields.AddDataField("Switch", "Address") as ListField;
+            df.DisplayName = "翻转";
+            df.Url = "/IoT/DeviceProperty/Switch?id={Id}";
+            df.DataAction = "action";
+            df.DataVisible = e => (e as DeviceProperty).Type.EqualIgnoreCase("bool");
+        }
+    }
+
+    public DevicePropertyController(ThingService thingService) => _thingService = thingService;
+
+    protected override Boolean Valid(DeviceProperty entity, DataObjectMethodType type, Boolean post)
+    {
+        var fs = type switch
+        {
+            DataObjectMethodType.Insert => AddFormFields,
+            DataObjectMethodType.Update => EditFormFields,
+            _ => null,
+        };
+
+        if (fs != null)
+        {
+            var df = fs.FirstOrDefault(e => e.Name == "Type");
+            if (df != null)
+            {
+                // 基础类型,加上所有产品类型
+                var dic = new Dictionary<String, String>(TypeHelper.GetIoTTypes(true), StringComparer.OrdinalIgnoreCase);
+
+                if (!entity.Type.IsNullOrEmpty() && !dic.ContainsKey(entity.Type)) dic[entity.Type] = entity.Type;
+                df.DataSource = e => dic;
+            }
+        }
+
+        return base.Valid(entity, type, post);
+    }
+
+    protected override IEnumerable<DeviceProperty> Search(Pager p)
+    {
+        var deviceId = p["deviceId"].ToInt(-1);
+
+        var start = p["dtStart"].ToDateTime();
+        var end = p["dtEnd"].ToDateTime();
+
+        return DeviceProperty.Search(deviceId, start, end, p["Q"], p);
+    }
+
+    [EntityAuthorize(PermissionFlags.Insert)]
+    public async Task<ActionResult> Switch(Int32 id)
+    {
+        var msg = "";
+        var entity = DeviceProperty.FindById(id);
+        if (entity != null && entity.Enable)
+        {
+            var value = entity.Value.ToBoolean();
+            value = !value;
+
+            //var rs = await _appService.SetProperty(entity.Device.Code, entity.Name, value);
+            var model = new PropertyModel { Name = entity.Name, Value = value };
+            _thingService.SetProperty(entity.Device, new[] { model }, UserHost);
+
+            // 执行远程调用
+            var dp = entity;
+            if (dp != null)
+            {
+                var input = new
+                {
+                    model.Name,
+                    model.Value,
+                    dp.Address,
+                };
+
+                var rs = await _thingService.InvokeAsync(entity.Device, "SetProperty", input.ToJson(), DateTime.Now.AddSeconds(5));
+                if (rs != null && rs.Status >= ServiceStatus.已完成)
+                {
+                    msg = $"{rs.Status} {rs.Data}";
+                }
+            }
+        }
+
+        return JsonRefresh("成功!" + msg, 1000);
+    }
+}
\ No newline at end of file
Added +99 -0
diff --git a/IoTZero/Areas/IoT/Controllers/ProductController.cs b/IoTZero/Areas/IoT/Controllers/ProductController.cs
new file mode 100644
index 0000000..00d84a1
--- /dev/null
+++ b/IoTZero/Areas/IoT/Controllers/ProductController.cs
@@ -0,0 +1,99 @@
+using System.ComponentModel;
+using IoT.Data;
+using NewLife.Cube;
+using NewLife.Cube.ViewModels;
+using NewLife.Web;
+
+namespace IoTEdge.Areas.IoT.Controllers
+{
+    [IoTArea]
+    [DisplayName("产品定义")]
+    [Menu(90, true, Icon = "fa-product-hunt")]
+    public class ProductController : EntityController<Product>
+    {
+        static ProductController()
+        {
+            LogOnChange = true;
+
+            ListFields.RemoveField("Secret", "DataFormat", "DynamicRegister", "FixedDeviceCode", "AuthType", "WhiteIP", "Remark");
+            ListFields.RemoveCreateField();
+
+            {
+                var df = ListFields.GetField("DeviceCount") as ListField;
+                df.DisplayName = "{DeviceCount}";
+                df.Url = "/IoT/Device?productId={Id}";
+                //df.DataVisible = (e, f) => (e as Product).DeviceCount > 0;
+            }
+
+            {
+                var df = ListFields.AddListField("function", "UpdateUser");
+                df.DisplayName = "功能定义";
+                df.Url = "/IoT/ProductFunction?productId={Id}";
+                df.Title = "产品的物模型属性";
+            }
+
+            {
+                var df = ListFields.AddListField("tsl", "UPdateUser");
+                df.DisplayName = "功能定义TSL";
+                df.Url = "/IoT/TSL/Edit?productId={Id}";
+                df.Title = "TSL模型";
+            }
+
+            {
+                var df = ListFields.AddListField("publish", "UPdateUser");
+                df.DisplayName = "功能发布";
+                df.Url = "/IoT/ProductFunction/PublishBatch?productId={Id}";
+                df.Title = "批量发布产品功能定义";
+                //df.DataVisible = e => (e as Product).DeviceCount > 0;
+            }
+
+            {
+                var df = ListFields.AddListField("rule", "UpdateUser");
+                df.DisplayName = "规则策略";
+                df.Url = "/IoT/RulePolicy?productId={Id}";
+            }
+
+            {
+                var df = ListFields.AddListField("Log");
+                df.DisplayName = "日志";
+                df.Url = "/Admin/Log?category=产品&linkId={Id}";
+            }
+        }
+
+        protected override IEnumerable<Product> Search(Pager p)
+        {
+            var id = p["Id"].ToInt(-1);
+            if (id > 0)
+            {
+                var entity = Product.FindById(id);
+                if (entity != null) return new[] { entity };
+            }
+
+            var code = p["code"];
+            var protocol = p["protocol"];
+
+            var start = p["dtStart"].ToDateTime();
+            var end = p["dtEnd"].ToDateTime();
+
+            return Product.Search(code, protocol, start, end, p["Q"], p);
+        }
+
+        protected override Boolean Valid(Product entity, DataObjectMethodType type, Boolean post)
+        {
+            var fs = type switch
+            {
+                DataObjectMethodType.Insert => AddFormFields,
+                DataObjectMethodType.Update => EditFormFields,
+                _ => null,
+            };
+
+            if (fs != null)
+            {
+                var df = fs.FirstOrDefault(e => e.Name == "ProtocolType");
+                if (df != null) df.DataSource = e => Driver.GetValids().ToDictionary(e => e.Name, e => e.ToString());
+            }
+
+            return base.Valid(entity, type, post);
+        }
+    }
+}
\ No newline at end of file
Added +13 -0
diff --git a/IoTZero/Areas/IoT/IoTArea.cs b/IoTZero/Areas/IoT/IoTArea.cs
new file mode 100644
index 0000000..dbdaca6
--- /dev/null
+++ b/IoTZero/Areas/IoT/IoTArea.cs
@@ -0,0 +1,13 @@
+using System.ComponentModel;
+using NewLife;
+using NewLife.Cube;
+
+namespace IoTEdge.Areas.IoT;
+
+[DisplayName("网关配置")]
+public class IoTArea : AreaBase
+{
+    public IoTArea() : base(nameof(IoTArea).TrimEnd("Area")) { }
+
+    static IoTArea() => RegisterArea<IoTArea>();
+}
\ No newline at end of file
Added +10 -0
diff --git a/IoTZero/Areas/IoT/Views/_ViewImports.cshtml b/IoTZero/Areas/IoT/Views/_ViewImports.cshtml
new file mode 100644
index 0000000..643ea47
--- /dev/null
+++ b/IoTZero/Areas/IoT/Views/_ViewImports.cshtml
@@ -0,0 +1,10 @@
+@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
+@using NewLife
+@using NewLife.Cube
+@using NewLife.Cube.Extensions
+@using NewLife.Reflection
+@using NewLife.Web
+@using XCode
+@using XCode.Membership
+@using IoT.Data
+@using IoTEdge
\ No newline at end of file
Added +6 -0
diff --git a/IoTZero/Areas/IoT/Views/_ViewStart.cshtml b/IoTZero/Areas/IoT/Views/_ViewStart.cshtml
new file mode 100644
index 0000000..de1b51e
--- /dev/null
+++ b/IoTZero/Areas/IoT/Views/_ViewStart.cshtml
@@ -0,0 +1,6 @@
+@{
+    var theme = NewLife.Cube.Setting.Current.Theme;
+    if (String.IsNullOrEmpty(theme)) theme = "ACE";
+
+    Layout = "~/Views/" + theme + "/_Layout.cshtml";
+}
\ No newline at end of file
Added +19 -0
diff --git a/IoTZero/Areas/IoT/Views/Device/_Form_Action.cshtml b/IoTZero/Areas/IoT/Views/Device/_Form_Action.cshtml
new file mode 100644
index 0000000..86edde5
--- /dev/null
+++ b/IoTZero/Areas/IoT/Views/Device/_Form_Action.cshtml
@@ -0,0 +1,19 @@
+@model Device
+@using XCode
+@using XCode.Membership;
+@{
+    var entity = Model as IEntity;
+    var isNew = entity.IsNullKey;
+}
+@if (this.Has(PermissionFlags.Insert, PermissionFlags.Update))
+{
+    <div class="clearfix form-actions col-xs-12 col-sm-12 col-md-12">
+        <label class="control-label col-xs-4 col-sm-5 col-md-5"></label>
+        @if (!isNew)
+        {
+            <a href="@Url.Action("Import", "Device", new { Id = Model.Id })" class="btn btn-success btn-sm">导入设备信息</a>
+        }
+        <button type="submit" class="btn btn-success btn-sm"><i class="glyphicon glyphicon-@(isNew ? "plus" : "save")"></i><strong>@(isNew ? "新增" : "保存")</strong></button>
+        <button type="button" class="btn btn-danger btn-sm" onclick="history.go(-1);"><i class="glyphicon glyphicon-remove"></i><strong>取消</strong></button>
+    </div>
+}
\ No newline at end of file
Added +14 -0
diff --git a/IoTZero/Areas/IoT/Views/Device/_List_Search.cshtml b/IoTZero/Areas/IoT/Views/Device/_List_Search.cshtml
new file mode 100644
index 0000000..eadaed2
--- /dev/null
+++ b/IoTZero/Areas/IoT/Views/Device/_List_Search.cshtml
@@ -0,0 +1,14 @@
+@using NewLife;
+@using NewLife.Web;
+@using NewLife.Cube;
+@using XCode;
+@using IoT.Data;
+@{
+    var fact = ViewBag.Factory as IEntityFactory;
+    var page = ViewBag.Page as Pager;
+}
+<div class="form-group">
+    <label for="productId" class="control-label">产品:</label>
+    @Html.ForDropDownList("productId", Product.FindAllWithCache(), page["productId"], "全部", true)
+</div>
+@await Html.PartialAsync("_DateRange")
\ No newline at end of file
Added +90 -0
diff --git a/IoTZero/Areas/IoT/Views/Device/Import.cshtml b/IoTZero/Areas/IoT/Views/Device/Import.cshtml
new file mode 100644
index 0000000..a847562
--- /dev/null
+++ b/IoTZero/Areas/IoT/Views/Device/Import.cshtml
@@ -0,0 +1,90 @@
+@model Device
+@using NewLife;
+@using NewLife.Web;
+@using NewLife.Cube;
+@using XCode;
+@using IoT.Data;
+@{
+    var fact = ViewBag.Factory as IEntityFactory;
+    var entity = Model as Device;
+}
+<div class="form-horizontal">
+    @using (Html.BeginForm("Upload", null, new { id = Model.Id }, FormMethod.Post, null, new { enctype = "multipart/form-data" }))
+    {
+        <div class="form-group col-xs-12 col-sm-6">
+            <label class="control-label col-xs-3 col-sm-3">名称</label>
+            <div class="input-group col-xs-9 col-sm-9">
+                @entity.Name
+            </div>
+        </div>
+        <div class="form-group col-xs-12 col-sm-6">
+            <label class="control-label col-xs-3 col-sm-3">编码</label>
+            <div class="input-group col-xs-9 col-sm-5">
+                @entity.Code
+            </div>
+        </div>
+        <div class="form-group col-xs-12 col-sm-6">
+            <label class="control-label col-xs-3 col-sm-3">协议</label>
+            <div class="input-group col-xs-9 col-sm-5">
+                <select class="multiselect" id="ProtocolType" name="ProtocolType">
+                    <option selected="selected" value="ModbusRTU">Modbus(串口RS232/RS485)</option>
+                    <option value="ModbusTCP">Modbus(以太网Tcp)</option>
+                    <option value="OpcUA">OPC UA(开放平台通信统一体系结构)</option>
+                    <option value="-1">&lt;无&gt;</option>
+                </select>
+            </div>
+            <span class="hidden-xs col-sm-4">
+                <span class="middle">设备接入网关的协议类型,如ModbusRTU/ModbusTCP</span>
+            </span>
+
+        </div>
+        <div class="form-group col-xs-12 col-sm-6">
+            <label class="control-label col-xs-3 col-sm-3">设备地址</label>
+            <div class="input-group col-xs-9 col-sm-5">
+                <input class="form-control" id="Address" name="Address" type="text" value="tcp://127.0.0.1:502" />
+            </div>
+            <span class="hidden-xs col-sm-4">
+                <span class="middle">串口号COM1,或/dev/ttyAMA2,或者ModbusTCP的设备地址,tcp://127.0.0.1:502</span>
+            </span>
+        </div>
+        <div class="form-group col-xs-12 col-sm-6">
+            <label class="control-label col-xs-3 col-sm-3">波特率</label>
+            <div class="input-group col-xs-9 col-sm-5">
+                <input class="form-control" id="Baudrate" name="Baudrate" role="number" type="text" value="9600" />
+            </div>
+            <span class="hidden-xs col-sm-4">
+                <span class="middle">仅串口使用,默认9600</span>
+            </span>
+        </div>
+        <div class="form-group col-xs-12 col-sm-6">
+            <label class="control-label col-xs-3 col-sm-3">从站号</label>
+            <div class="input-group col-xs-9 col-sm-5">
+                <input class="form-control" id="Host" name="Host" role="number" type="text" value="1" />
+            </div>
+            <span class="hidden-xs col-sm-4">
+                <span class="middle">默认 1</span>
+            </span>
+        </div>
+        <div class="form-group col-xs-12 col-sm-6">
+            <label class="control-label col-xs-3 col-sm-3">Excel文件</label>
+            <div class="input-group">
+                <span class="input-group-addon">
+                    <i class="glyphicon glyphicon-file"></i>
+                </span>
+                <input name="file" type="file" id="file" placeholder="上传文件" />
+            </div>
+        </div>
+        <div class="form-group col-xs-12 col-sm-6">
+            <label class="control-label col-xs-3 col-sm-3">导入模板</label>
+            <div class="input-group col-xs-9 col-sm-5">
+                <a href="~/Content/ModelFile/importModel.xlsx">数据点导入模板.xlsx</a>
+            </div>
+        </div>
+
+        <div class="clearfix form-actions col-xs-12 col-sm-12 col-md-12">
+            <label class="control-label col-xs-4 col-sm-5 col-md-5"></label>
+            <button type="submit" class="btn btn-success btn-sm"><i class="glyphicon glyphicon-plus"></i><strong>上传</strong></button>
+            <button type="button" class="btn btn-danger btn-sm" onclick="history.go(-1);"><i class="glyphicon glyphicon-remove"></i><strong>取消</strong></button>
+        </div>
+    }
+</div>
\ No newline at end of file
Added +10 -0
diff --git a/IoTZero/Areas/IoT/Views/DeviceData/_List_Search.cshtml b/IoTZero/Areas/IoT/Views/DeviceData/_List_Search.cshtml
new file mode 100644
index 0000000..730699b
--- /dev/null
+++ b/IoTZero/Areas/IoT/Views/DeviceData/_List_Search.cshtml
@@ -0,0 +1,10 @@
+@using NewLife;
+@using NewLife.Web;
+@using NewLife.Cube;
+@using XCode;
+@using IoT.Data;
+@{
+    var fact = ViewBag.Factory as IEntityFactory;
+    var page = ViewBag.Page as Pager;
+}
+@await Html.PartialAsync("_DateRange")
\ No newline at end of file
Added +16 -0
diff --git a/IoTZero/Areas/IoT/Views/DeviceData/_List_Toolbar_Custom.cshtml b/IoTZero/Areas/IoT/Views/DeviceData/_List_Toolbar_Custom.cshtml
new file mode 100644
index 0000000..5115a96
--- /dev/null
+++ b/IoTZero/Areas/IoT/Views/DeviceData/_List_Toolbar_Custom.cshtml
@@ -0,0 +1,16 @@
+@using NewLife;
+@using NewLife.Web;
+@using NewLife.Cube;
+@using XCode;
+@using IoT.Data;
+@{
+    var fact = ViewBag.Factory as IEntityFactory;
+    var page = ViewBag.Page as Pager;
+    var url = page.GetBaseUrl(true, true, false, new[] { "sample" });
+}
+<div class="form-group">
+    <a href="?@url">原始数据</a> &nbsp;
+    <a href="?@url&sample=60">每分钟</a> &nbsp;
+    <a href="?@url&sample=900">每15钟</a> &nbsp;
+    <a href="?@url&sample=3600">每小时</a> &nbsp;
+</div>
Added +10 -0
diff --git a/IoTZero/Areas/IoT/Views/DeviceHistory/_List_Search.cshtml b/IoTZero/Areas/IoT/Views/DeviceHistory/_List_Search.cshtml
new file mode 100644
index 0000000..730699b
--- /dev/null
+++ b/IoTZero/Areas/IoT/Views/DeviceHistory/_List_Search.cshtml
@@ -0,0 +1,10 @@
+@using NewLife;
+@using NewLife.Web;
+@using NewLife.Cube;
+@using XCode;
+@using IoT.Data;
+@{
+    var fact = ViewBag.Factory as IEntityFactory;
+    var page = ViewBag.Page as Pager;
+}
+@await Html.PartialAsync("_DateRange")
\ No newline at end of file
Added +14 -0
diff --git a/IoTZero/Areas/IoT/Views/Product/_List_Search.cshtml b/IoTZero/Areas/IoT/Views/Product/_List_Search.cshtml
new file mode 100644
index 0000000..c66c8ff
--- /dev/null
+++ b/IoTZero/Areas/IoT/Views/Product/_List_Search.cshtml
@@ -0,0 +1,14 @@
+@using IoT.Data
+@using NewLife;
+@using NewLife.Web;
+@using NewLife.Cube;
+@using XCode;
+@{
+    var fact = ViewBag.Factory as IEntityFactory;
+    var page = ViewBag.Page as Pager;
+}
+<div class="form-group">
+    <label for="protocol" class="control-label">驱动:</label>
+    @Html.ForDropDownList("protocol", Driver.GetValids().ToDictionary(e=>e.Name,e=>e.ToString()), page["protocol"], "全部", true)
+</div>
+@await Html.PartialAsync("_DateRange")
\ No newline at end of file
Added +36 -0
diff --git a/IoTZero/IoTZero.csproj b/IoTZero/IoTZero.csproj
new file mode 100644
index 0000000..39b9905
--- /dev/null
+++ b/IoTZero/IoTZero.csproj
@@ -0,0 +1,36 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+  <PropertyGroup>
+    <TargetFramework>net7.0</TargetFramework>
+    <AssemblyTitle>物联网服务平台</AssemblyTitle>
+    <Description>IoT服务平台</Description>
+    <Company>新生命开发团队</Company>
+    <Copyright>©2002-2023 NewLife</Copyright>
+    <VersionPrefix>1.0</VersionPrefix>
+    <VersionSuffix>$([System.DateTime]::Now.ToString(`yyyy.MMdd`))</VersionSuffix>
+    <Version>$(VersionPrefix).$(VersionSuffix)</Version>
+    <FileVersion>$(Version)</FileVersion>
+    <AssemblyVersion>$(VersionPrefix).*</AssemblyVersion>
+    <Deterministic>false</Deterministic>
+    <OutputPath>..\Bin\IoTZero</OutputPath>
+    <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
+    <GenerateDocumentationFile>True</GenerateDocumentationFile>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <LangVersion>latest</LangVersion>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <Folder Include="Areas\IoT\Views\DeviceProperty\" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <PackageReference Include="NewLife.Cube.Core" Version="5.5.2023.520-beta0001" />
+    <PackageReference Include="NewLife.IoT" Version="1.8.2023.511" />
+    <PackageReference Include="NewLife.Stardust.Extensions" Version="2.8.2023.520-beta0003" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\IoT.Data\IoT.Data.csproj" />
+  </ItemGroup>
+
+</Project>
Added +89 -0
diff --git a/IoTZero/Program.cs b/IoTZero/Program.cs
new file mode 100644
index 0000000..a037882
--- /dev/null
+++ b/IoTZero/Program.cs
@@ -0,0 +1,89 @@
+using IoTEdge.Services;
+using NewLife.Caching;
+using NewLife.Cube;
+using NewLife.Log;
+using XCode;
+
+// 日志输出到控制台,并拦截全局异常
+XTrace.UseConsole();
+
+var builder = WebApplication.CreateBuilder(args);
+var services = builder.Services;
+
+// 配置星尘。借助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<DataConsumerService>();
+
+services.AddSingleton<ThingService>();
+services.AddSingleton<DataService>();
+services.AddSingleton<QueueService>();
+services.AddSingleton<PushDataQueueService>();
+services.AddSingleton<RuleService>();
+services.AddSingleton<PointImportService>();
+
+services.AddHttpClient("hc", e => e.Timeout = TimeSpan.FromSeconds(5));
+
+services.AddSingleton<ICache, MemoryCache>();
+
+// 后台服务
+services.AddHostedService<EdgeHostedService>();
+
+services.AddControllersWithViews();
+
+// 引入魔方
+services.AddCube();
+
+var app = builder.Build();
+
+// 预热数据层,执行反向工程建表等操作
+EntityFactory.InitConnection("Membership");
+EntityFactory.InitConnection("Log");
+EntityFactory.InitConnection("Cube");
+EntityFactory.InitConnection("IoT");
+
+// 使用Cube前添加自己的管道
+if (app.Environment.IsDevelopment())
+    app.UseDeveloperExceptionPage();
+else
+    app.UseExceptionHandler("/CubeHome/Error");
+
+// 使用魔方
+app.UseCube(app.Environment);
+
+app.UseAuthorization();
+
+app.UseEndpoints(endpoints =>
+{
+    endpoints.MapControllerRoute(
+        name: "default",
+        pattern: "{controller=CubeHome}/{action=Index}/{id?}");
+});
+
+app.Run();
Added +28 -0
diff --git a/IoTZero/Properties/launchSettings.json b/IoTZero/Properties/launchSettings.json
new file mode 100644
index 0000000..f60d546
--- /dev/null
+++ b/IoTZero/Properties/launchSettings.json
@@ -0,0 +1,28 @@
+{
+  "iisSettings": {
+    "windowsAuthentication": false,
+    "anonymousAuthentication": true,
+    "iisExpress": {
+      "applicationUrl": "http://localhost:58926",
+      "sslPort": 0
+    }
+  },
+  "profiles": {
+    "IoTZero": {
+      "commandName": "Project",
+      "dotnetRunMessages": true,
+      "launchBrowser": true,
+      "applicationUrl": "http://localhost:5269",
+      "environmentVariables": {
+        "ASPNETCORE_ENVIRONMENT": "Development"
+      }
+    },
+    "IIS Express": {
+      "commandName": "IISExpress",
+      "launchBrowser": true,
+      "environmentVariables": {
+        "ASPNETCORE_ENVIRONMENT": "Development"
+      }
+    }
+  }
+}
Added +130 -0
diff --git a/IoTZero/Services/DataService.cs b/IoTZero/Services/DataService.cs
new file mode 100644
index 0000000..48c22db
--- /dev/null
+++ b/IoTZero/Services/DataService.cs
@@ -0,0 +1,130 @@
+using IoT.Data;
+using IoTEdge.Models;
+using NewLife;
+using NewLife.IoT.ThingModels;
+using NewLife.Log;
+
+namespace IoTEdge.Services;
+
+/// <summary>数据服务</summary>
+public class DataService
+{
+    private readonly PushDataQueueService _pushQueue;
+    private readonly ITracer _tracer;
+
+    /// <summary>实例化数据服务</summary>
+    /// <param name="pushQueue"></param>
+    /// <param name="tracer"></param>
+    public DataService(PushDataQueueService pushQueue, ITracer tracer)
+    {
+        _pushQueue = pushQueue;
+        _tracer = tracer;
+    }
+
+    #region 方法
+    /// <summary>
+    /// 插入设备原始数据,异步批量操作
+    /// </summary>
+    /// <param name="deviceId"></param>
+    /// <param name="time"></param>
+    /// <param name="name"></param>
+    /// <param name="value"></param>
+    /// <param name="kind"></param>
+    /// <param name="ip"></param>
+    /// <returns></returns>
+    public Int32 AddData(Int32 deviceId, Int64 time, String name, String value, String kind, String ip)
+    {
+        if (value.IsNullOrEmpty()) return 0;
+
+        // 原始数据不再落库,仅用于解析
+        if (name.StartsWithIgnoreCase("raw-", "channel-")) return 0;
+
+        using var span = _tracer?.NewSpan("AddData", $"{deviceId}-{name}-{value}");
+
+        //// 客户端时间
+        //if (time.Year < 2000) time = DateTime.Now;
+        //// 在按天分表加上并行插入的模式下,有一定几率出现主键冲突,因此提前生成雪花Id
+        //var snow = DeviceData.Meta.Factory.Snow;
+
+        var traceId = DefaultSpan.Current?.TraceId;
+
+        var entity = new DeviceData()
+        {
+            //Id = snow.NewId(time),
+            DeviceId = deviceId,
+            Name = name,
+            Value = value,
+            Kind = kind,
+
+            Timestamp = time,
+            TraceId = traceId,
+            Creator = Environment.MachineName,
+            CreateTime = DateTime.Now,
+            CreateIP = ip,
+        };
+
+        var rs = entity.SaveAsync() ? 1 : 0;
+
+        var dv = Device.FindById(deviceId);
+        var msg = new DataModelMessage()
+        {
+            Name = name,
+            Time = time,
+            Value = value,
+            DeviceCode = dv.Code,
+            ProductCode = dv.Product?.Code
+        };
+
+        var container = new MessageSendContainer<DataModelMessage>(msg);
+
+        // 推送主队列
+        _pushQueue.GetDataQueue()?.Add(container);
+
+        return rs;
+    }
+
+    /// <summary>添加事件</summary>
+    /// <param name="deviceId"></param>
+    /// <param name="model"></param>
+    /// <param name="ip"></param>
+    /// <returns></returns>
+    public void AddEvent(Int32 deviceId, EventModel model, String ip)
+    {
+        var traceId = DefaultSpan.Current?.TraceId;
+        //var snow = DeviceEvent.Meta.Factory.Snow;
+
+        var ev = new DeviceEvent
+        {
+            //Id = snow.NewId(time),
+            DeviceId = deviceId,
+
+            Type = model.Type,
+            Name = model.Name,
+            Remark = model.Remark,
+
+            Timestamp = model.Time,
+            TraceId = traceId,
+            Creator = Environment.MachineName,
+            CreateTime = DateTime.Now,
+            CreateIP = ip,
+        };
+
+        ev.SaveAsync();
+
+        var dv = Device.FindById(deviceId);
+        var msg = new EventModelMessage()
+        {
+            DeviceCode = dv.Code,
+            ProductCode = dv.Product?.Code,
+            Name = model.Name,
+            Time = model.Time,
+            Type = model.Type,
+            Remark = model.Remark,
+        };
+
+        var container = new MessageSendContainer<EventModelMessage>(msg);
+
+        _pushQueue.GetEventQueue()?.Add(container);
+    }
+    #endregion
+}
\ No newline at end of file
Added +102 -0
diff --git a/IoTZero/Services/QueueService.cs b/IoTZero/Services/QueueService.cs
new file mode 100644
index 0000000..7e83c96
--- /dev/null
+++ b/IoTZero/Services/QueueService.cs
@@ -0,0 +1,102 @@
+using NewLife;
+using NewLife.Caching;
+using NewLife.IoT.ThingModels;
+using NewLife.Log;
+using NewLife.Serialization;
+
+namespace IoTEdge.Services;
+
+/// <summary>队列服务</summary>
+public class QueueService
+{
+    #region 属性
+    /// <summary>
+    /// 队列主机
+    /// </summary>
+    public ICache Host { get; set; }
+
+    private readonly ITracer _tracer;
+    #endregion
+
+    #region 构造
+    /// <summary>
+    /// 实例化队列服务
+    /// </summary>
+    public QueueService(ICache cache, ITracer tracer)
+    {
+        Host = cache;
+        _tracer = tracer;
+    }
+    #endregion
+
+    /// <summary>
+    /// 获取指定设备的命令队列
+    /// </summary>
+    /// <returns></returns>
+    public IProducerConsumer<String> GetQueue() => Host.GetQueue<String>("deviceService");
+
+    /// <summary>
+    /// 向指定设备发送命令
+    /// </summary>
+    /// <param name="model"></param>
+    /// <returns></returns>
+    public Int32 Publish(ServiceModel model)
+    {
+        using var span = _tracer?.NewSpan(nameof(Publish), model);
+
+        var q = GetQueue();
+        return q.Add(model.ToJson());
+    }
+
+    IProducerConsumer<String> _consumer;
+    /// <summary>
+    /// 消费服务队列
+    /// </summary>
+    /// <returns></returns>
+    public async Task<ServiceModel> ConsumeAsync()
+    {
+        _consumer ??= GetQueue();
+
+        var msg = await _consumer.TakeOneAsync(15);
+        if (msg.IsNullOrEmpty()) return null;
+
+        return msg.ToJsonEntity<ServiceModel>();
+    }
+
+    /// <summary>
+    /// 获取指定设备的服务响应队列
+    /// </summary>
+    /// <param name="serviceLogId"></param>
+    /// <returns></returns>
+    public IProducerConsumer<String> GetReplyQueue(Int64 serviceLogId) => Host.GetQueue<String>($"service:{serviceLogId}");
+
+    /// <summary>
+    /// 发送消息到服务响应队列
+    /// </summary>
+    /// <param name="model"></param>
+    public void Publish(ServiceReplyModel model)
+    {
+        var topic = $"service:{model.Id}";
+        var queue = Host.GetQueue<String>(topic);
+
+        // 发送消息,并设置过期时间
+        queue.Add(model.ToJson());
+
+        Host.SetExpire(topic, TimeSpan.FromMinutes(10));
+    }
+
+    /// <summary>
+    /// 消费响应消息
+    /// </summary>
+    /// <param name="serviceLogId"></param>
+    /// <returns></returns>
+    public async Task<ServiceReplyModel> ConsumeOneAsync(Int64 serviceLogId)
+    {
+        var consumer = GetReplyQueue(serviceLogId);
+
+        var msg = await consumer.TakeOneAsync(15);
+        if (msg.IsNullOrEmpty()) return null;
+
+        return msg.ToJsonEntity<ServiceReplyModel>();
+    }
+}
\ No newline at end of file
Added +258 -0
diff --git a/IoTZero/Services/ThingService.cs b/IoTZero/Services/ThingService.cs
new file mode 100644
index 0000000..51c49e5
--- /dev/null
+++ b/IoTZero/Services/ThingService.cs
@@ -0,0 +1,258 @@
+using IoT.Data;
+using IoT.Data.Models;
+using NewLife;
+using NewLife.Caching;
+using NewLife.IoT;
+using NewLife.IoT.ThingModels;
+using NewLife.IoT.ThingSpecification;
+using NewLife.Log;
+using NewLife.Reflection;
+using XCode;
+
+namespace IoTEdge.Services;
+
+/// <summary>物模型服务</summary>
+public class ThingService
+{
+    private readonly DataService _dataService;
+    private readonly QueueService _queue;
+    private readonly RuleService _ruleService;
+    private readonly QueueService _queueService;
+    private readonly ITracer _tracer;
+    private static readonly ICache _cache = new MemoryCache();
+
+    /// <summary>
+    /// 实例化物模型服务
+    /// </summary>
+    /// <param name="dataService"></param>
+    /// <param name="queue"></param>
+    /// <param name="ruleService"></param>
+    /// <param name="segmentService"></param>
+    /// <param name="tracer"></param>
+    public ThingService(DataService dataService, QueueService queue, RuleService ruleService, QueueService queueService, ITracer tracer)
+    {
+        _dataService = dataService;
+        _queue = queue;
+        _ruleService = ruleService;
+        _queueService = queueService;
+        _tracer = tracer;
+    }
+
+    #region 属性
+    private void VerifyModel(Device device, String name, FunctionKinds kind)
+    {
+        // 强校验产品,需要判断该属性是否在功能定义里面
+        var prd = device.Product;
+        if (prd != null && prd.VerifyModel)
+        {
+            if (!prd.Functions.Any(e => e.Enable && e.Identifier == name && e.Kind == kind))
+                throw new Exception($"设备[{device}]的物模型不支持{kind}[{name}]");
+        }
+    }
+
+    /// <summary>上报数据,写入属性表、数据表、分段数据表</summary>
+    /// <param name="device"></param>
+    /// <param name="name"></param>
+    /// <param name="value"></param>
+    /// <param name="timestamp"></param>
+    /// <param name="kind"></param>
+    /// <param name="ip"></param>
+    /// <returns></returns>
+    public IDeviceProperty PostData(Device device, String name, Object value, Int64 timestamp, String kind, String ip)
+    {
+        var dp = PostProperty(device, name, value, timestamp, ip);
+
+        // 记录数据流水,使用经过处理的属性数值字段
+        if (dp != null)
+        {
+            _dataService.AddData(dp.DeviceId, timestamp, dp.Name, dp.Value, kind, ip);
+        }
+
+        return dp;
+    }
+
+    /// <summary>设备属性上报</summary>
+    /// <param name="device">设备</param>
+    /// <param name="items">名值对</param>
+    /// <param name="ip">IP地址</param>
+    /// <returns></returns>
+    public Int32 PostProperty(Device device, PropertyModel[] items, String ip)
+    {
+        if (items == null) return -1;
+
+        var rs = 0;
+        foreach (var item in items)
+        {
+            PostData(device, item.Name, item.Value, 0, "PostProperty", ip);
+
+            rs++;
+        }
+
+        // 触发指定设备的联动策略
+        if (rs > 0) _ruleService.Execute(device.Id);
+
+        return rs;
+    }
+
+    /// <summary>设备属性上报</summary>
+    /// <param name="device">设备</param>
+    /// <param name="name">属性名</param>
+    /// <param name="value">数值</param>
+    /// <param name="timestamp">时间戳</param>
+    /// <param name="ip">IP地址</param>
+    /// <returns></returns>
+    public IDeviceProperty PostProperty(Device device, String name, Object value, Int64 timestamp, String ip)
+    {
+        using var span = _tracer?.NewSpan("PostProperty", $"{device.Id}-{name}-{value}");
+
+        var entity = GetProperty(device, name);
+        if (entity == null)
+        {
+            // 产品开启强校验物模型属性,不会自动创建属性
+            if (device.Product.VerifyModel) return null;
+
+            VerifyModel(device, name, FunctionKinds.Property);
+
+            var key = $"{device.Id}###{name}";
+            entity = DeviceProperty.GetOrAdd(key,
+                k => DeviceProperty.FindByNameAndDeviceId(name, device.Id),
+                k => new DeviceProperty
+                {
+                    DeviceId = device.Id,
+                    Name = name,
+                    NickName = name,
+                    Enable = true,
+
+                    CreateTime = DateTime.Now,
+                    CreateIP = ip
+                });
+        }
+
+        // 检查是否锁定
+        if (!entity.Enable) return null;
+
+        //if (timestamp == 0) timestamp = DateTime.UtcNow.ToLong();
+
+        // 检查数据是否越界
+        var function = ProductFunction.FindById(entity.FunctionId);
+        if (function != null && !Valid(function, device.Id, entity, value)) return null;
+
+        // 修正数字精度,小数点位数
+        value = FixData(value, function);
+
+        entity.Name = name;
+        //entity.Enable = true;
+        entity.SetValue(value);
+
+        var hasDirty = (entity as IEntity).Dirtys[nameof(entity.Value)];
+
+        var now = DateTime.Now;
+        entity.LastPost = now;
+        entity.Timestamp = timestamp;
+        entity.TraceId = DefaultSpan.Current?.TraceId;
+        entity.UpdateTime = now;
+        entity.UpdateIP = ip;
+
+        // 如果短时间内数据没有变化(无脏数据),则不需要保存属性
+        var period = 60;
+        if (hasDirty || period <= 0 || entity.LastPost.AddSeconds(period) < now)
+        {
+            // 属性上报直接更新,数据上报异步更新
+            if (entity.Id == 0)
+                entity.Save();
+            else
+                entity.SaveAsync();
+        }
+
+        return entity;
+    }
+
+    /// <summary>修正数据精度</summary>
+    /// <param name="value"></param>
+    /// <param name="function"></param>
+    /// <returns></returns>
+    public static Object FixData(Object value, ProductFunction function)
+    {
+        // 修正数字精度,小数点位数
+        if (function == null || function.Step <= 0 || !function.DataType.EqualIgnoreCase("float", "single", "double"))
+            return value;
+
+        // 计算小数点后位数
+        var step = function.Step.ToString();
+        var p = step.IndexOf('.');
+        if (p > 0)
+        {
+            // 0.001,p=1,len=3
+            var len = step.Length - 1 - p;
+            if (len > 0)
+            {
+                var d = value.ToDouble();
+                d = Math.Round(d, p);
+                value = d;
+            }
+        }
+
+        return value;
+    }
+
+    /// <summary>获取设备属性对象,长时间缓存,便于加速属性保存</summary>
+    /// <param name="device"></param>
+    /// <param name="name"></param>
+    /// <returns></returns>
+    private DeviceProperty GetProperty(Device device, String name)
+    {
+        var key = $"{device.Id}###{name}";
+        if (_cache.TryGetValue<DeviceProperty>(key, out var property)) return property;
+
+        using var span = _tracer?.NewSpan("GetProperty", $"{device.Id}-{name}");
+
+        var entity = device.Properties.FirstOrDefault(e => e.Name.EqualIgnoreCase(name));
+        if (entity != null)
+            _cache.Set(key, entity, 3600);
+
+        return entity;
+    }
+
+    /// <summary>检查数据是否越界</summary>
+    /// <param name="function"></param>
+    /// <param name="deviceId"></param>
+    /// <param name="dp"></param>
+    /// <param name="val"></param>
+    /// <returns></returns>
+    private Boolean Valid(ProductFunction function, Int32 deviceId, DeviceProperty dp, Object val)
+    {
+        // 判断是否溢出
+        if (function.Max - function.Min > 0)
+        {
+            var d = val.ToDouble();
+            if (d < function.Min || d > function.Max) return false;
+        }
+
+        // 最大间隔。超过该值时抛弃
+        if (function.MaxStep > 0)
+        {
+            var key = $"thing:maxstep:{deviceId}:{function.Identifier}";
+            var has = _cache.TryGetValue<Double>(key, out var lastValue);
+            if (!has)
+            {
+                if (dp != null)
+                {
+                    lastValue = dp.Value.ToDouble();
+                    has = true;
+                }
+            }
+
+            // 记录最后一次数据,即使没有采用。如果连续来了两个超限值,第二个将可能被采用
+            _cache.Set(key, val.ToDouble(), 3600);
+
+            if (has)
+            {
+                var d2 = val.ToDouble() - lastValue;
+                if (Math.Abs(d2) > function.MaxStep) return false;
+            }
+        }
+
+        return true;
+    }
+    #endregion
+}
\ No newline at end of file
Added +15 -0
diff --git a/Readme.MD b/Readme.MD
new file mode 100644
index 0000000..ef79253
--- /dev/null
+++ b/Readme.MD
@@ -0,0 +1,15 @@
+# 轻量级IoT解决方案 ZeroIoT
+轻量级IoT解决方案,IoT网关通过HTTP/MQTT接入IoT平台,提供数据采集和远程控制功能。IoT平台仅包含必要接口,架构简单清晰,面向小型物联网项目。  
+
+演示地址:http://iot.feifan.link  
+
+## 客户端网关
+IoTEdge,是轻量级物联网网关,运行在远端设备附近的工控机(A2/A4)上。  
+南向对接各种电子电气设备,或者传感器,支持NewLife.IoT生态的所有物联网驱动协议。  
+北向通过HTTP/MQTT接入IoT平台,提供数据采集和远程控制功能。  
+
+## 服务端平台
+IoTZero,是轻量级物联网平台,运行在云端服务器上。  
+平台提供设备鉴权、数据上报和指令下发等接口。  
+平台暂存设备数据,提供数据查询和数据分析功能。  
+平台提供向外推送数据的能力。  
\ No newline at end of file
Added +43 -0
diff --git a/ZeroIoT.sln b/ZeroIoT.sln
new file mode 100644
index 0000000..aca6d39
--- /dev/null
+++ b/ZeroIoT.sln
@@ -0,0 +1,43 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31717.71
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IoT.Data", "IoT.Data\IoT.Data.csproj", "{060707B1-D0E3-4CEB-AA2E-94C986A18B71}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C3436398-F186-4BF2-908F-C6C70BB749D3}"
+	ProjectSection(SolutionItems) = preProject
+		.editorconfig = .editorconfig
+		Readme.MD = Readme.MD
+	EndProjectSection
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IoTZero", "IoTZero\IoTZero.csproj", "{108DA7C0-AE8C-4259-8AA5-07409303AE22}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IoTEdge", "IoTEdge\IoTEdge.csproj", "{9A45E66E-4F0B-40F7-B43E-5F3A18C5D0F0}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
+		Release|Any CPU = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{060707B1-D0E3-4CEB-AA2E-94C986A18B71}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{060707B1-D0E3-4CEB-AA2E-94C986A18B71}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{060707B1-D0E3-4CEB-AA2E-94C986A18B71}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{060707B1-D0E3-4CEB-AA2E-94C986A18B71}.Release|Any CPU.Build.0 = Release|Any CPU
+		{108DA7C0-AE8C-4259-8AA5-07409303AE22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{108DA7C0-AE8C-4259-8AA5-07409303AE22}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{108DA7C0-AE8C-4259-8AA5-07409303AE22}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{108DA7C0-AE8C-4259-8AA5-07409303AE22}.Release|Any CPU.Build.0 = Release|Any CPU
+		{9A45E66E-4F0B-40F7-B43E-5F3A18C5D0F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{9A45E66E-4F0B-40F7-B43E-5F3A18C5D0F0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{9A45E66E-4F0B-40F7-B43E-5F3A18C5D0F0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{9A45E66E-4F0B-40F7-B43E-5F3A18C5D0F0}.Release|Any CPU.Build.0 = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+	GlobalSection(ExtensibilityGlobals) = postSolution
+		SolutionGuid = {37112253-83DF-4E65-AA72-1D0DCBE8CDE7}
+	EndGlobalSection
+EndGlobal