KstopaIOT.Embedded
1.1.0-beta
dotnet add package KstopaIOT.Embedded --version 1.1.0-beta
NuGet\Install-Package KstopaIOT.Embedded -Version 1.1.0-beta
<PackageReference Include="KstopaIOT.Embedded" Version="1.1.0-beta" />
<PackageVersion Include="KstopaIOT.Embedded" Version="1.1.0-beta" />
<PackageReference Include="KstopaIOT.Embedded" />
paket add KstopaIOT.Embedded --version 1.1.0-beta
#r "nuget: KstopaIOT.Embedded, 1.1.0-beta"
#:package KstopaIOT.Embedded@1.1.0-beta
#addin nuget:?package=KstopaIOT.Embedded&version=1.1.0-beta&prerelease
#tool nuget:?package=KstopaIOT.Embedded&version=1.1.0-beta&prerelease
KstopaIOT.Embedded 使用文档
把 KstopaIOT 网关内核直接内嵌进你的 WPF / WinForms 桌面程序——进程内采集、进程内北向对接,无需再部署一套独立的 B/S 服务。
1. 这是什么
KstopaIOT.Embedded 是 KstopaIOT 的嵌入式发行版。它把原 KstopaIOT 的「驱动加载 + 设备采集引擎 + 北向网关客户端」打包成一个 NuGet 库,让桌面程序引用即用:
- 用 SqlSugar 直连
kstopaiot.db(SQLite);数据库不存在时通过 CodeFirst 自动建库建表并写入最小种子数据(网关配置 + 扫描到的驱动),已存在则仅映射复用原表结构(零 EF 依赖) - 用
DriverLoader在进程内扫描并实例化南向驱动(drivers/net8.0/*.dll) - 用
GatewayEngine在进程内跑设备采集线程(读/写/表达式/触发/变化上传) - 用
KstopaIOTGateway进程内对接 IoTKstopa 平台(遥测、事件、触发、RPC 反向控制) - 附带开箱即用的 WPF 配置界面(
ConfigControl)和网关监控仪表盘(GatewayMonitorView)
与完整版 KstopaIOT 的区别
| 维度 | 完整版 KstopaIOT | KstopaIOT.Embedded |
|---|---|---|
| 部署形态 | 独立 ASP.NET Core 服务(B/S) | 库,内嵌进你的桌面程序 |
| 数据访问 | EF Core | SqlSugar(零 EF) |
| 北向网关 | 进程内路由 + PlatformHandler | KstopaIOTGateway(同 KstopaIOT.Embedded API) |
| 配置界面 | 浏览器 LayUI | 内置 WPF ConfigControl(可嵌入) |
| 适用场景 | 中心网关服务器 | 单机/边缘设备上的桌面客户端 |
| 平台 | Windows / Linux | Windows(net8.0-windows + WPF) |
特性继承:全部驱动协议(Siemens S7 / ModBus / MelsecMc / OmronFins / OPC UA / MTConnect / Fanuc 等)、DynamicExpresso 表达式引擎、变化上传、触发机制、缓存批量读写在嵌入式版中完全一致。
想几行就跑起来? 先看
QUICKSTART.md(零配置首跑 + 最小控制台 Demo)。本文件下面 §5 / §6 是完整集成与 API 参考。当前版本已统一为IKstopaIOTMessageHandler单一通路:KstopaIOTGateway/EmbeddedGatewayHost不再暴露任何EventHandler,所有消息(设备级 + 网关级错误/连接态/强制刷新响应)都经IKstopaIOTMessageHandler交付。
2. 架构总览
┌──────────────────────── 你的桌面程序 (WPF / WinForms) ────────────────────────┐
│ │
│ UI 层 │
│ ┌────────────────────┐ ┌─────────────────────────────────┐ │
│ │ ConfigControl │ │ GatewayMonitorView │ │
│ │ (设备/变量/驱动配置)│ │ (连接横幅+指标卡+吞吐曲线+设备卡)│ │
│ └─────────┬──────────┘ └───────────────┬─────────────────┘ │
│ │ AttachEngine / AttachGateway │ Bind(GatewayMonitorService) │
│ ▼ ▼ │
│ ┌─────────────────────── 嵌入内核 ───────────────────────┐ │
│ │ KstopaIOTGateway (北向门面) │ │
│ │ ├─ KstopaIOTGatewayClient ── 连接 IoTKstopa 平台 │ │
│ │ └─ KstopaIOTDeviceProxyFactory ── 每设备 Proxy(Channel) │ │
│ │ │ │
│ │ GatewayEngine (采集引擎) │ │
│ │ ├─ DriverLoader → IDriver 实例 │ │
│ │ └─ DeviceWorker(每设备一线程: 读/写/表达式/触发) │ │
│ │ │ │
│ │ EmbeddedDb (SqlSugar → kstopaiot.db) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ 南向设备 (PLC/CNC/OPC…) IoTKstopa 平台(北向) │
└────────────────────────────────────────────────────────────────────────────────┘
3. 项目结构
| 目录 / 文件 | 职责 |
|---|---|
Data/EmbeddedDb.cs |
SqlSugar 数据访问层,映射 kstopaiot.db 各表 |
Models/*.cs |
实体:Device / DeviceVariable / DeviceConfig / Driver / SystemConfig / RpcLog / Enums |
Drivers/DriverLoader.cs |
扫描 drivers/net8.0/*.dll,按 [ConfigParameter] 注入配置,构建 [Method]/[MethodSet]/[Cache] 委托 |
Engine/GatewayEngine.cs |
采集引擎门面:加载设备→实例化驱动→每设备 DeviceWorker→Start/Stop + 读/写/连接 API + 事件(引擎级事件仅方式 C 直接用时订阅) |
Engine/DeviceWorker.cs |
单设备采集线程(读循环、写队列、表达式、触发、变化上传、重连) |
Engine/DynamicExpressoUtility.cs |
表达式求值(raw / $pv / $ppv / TriggerEvent()) |
Engine/TelemetryEvents.cs |
引擎事件参数(VariableValueChangedEventArgs 等) |
Client/EmbeddedGatewayHost.cs |
一站式宿主(推荐入口):统一驱动目录→建 KstopaIOTGateway→起监控→启动引擎→AttachConfigControl/AttachMonitorView |
Client/EmbeddedGatewayHostBuilder.cs |
宿主流式构造器:WithDataFolder/WithDriversFolder/WithLogger/WithHandler/WithDevices/Build() |
Client/EmbeddedGatewayHostOptions.cs |
单一运行时选项源(DbPath/DriversDir/日志/Handler/设备) |
Client/NullMessageHandler.cs |
默认空处理器,使「不实现 Handler」也能启动采集 |
Client/KstopaIOTGateway.cs |
北向门面:Client + ProxyFactory + 自动路由到 IKstopaIOTMessageHandler(不再暴露任何 EventHandler) |
Client/KstopaIOTGatewayClient.cs |
KstopaIOT.Embedded 实现(进程内路由到 GatewayEngine) |
Client/KstopaIOTDeviceProxy.cs |
每设备消息代理:Channel 缓冲 + 串行/限流并行消费 + 背压 + 丢弃统计 |
Client/KstopaIOTDeviceProxyFactory.cs |
代理生命周期管理(创建/缓存/释放) |
Client/IKstopaIOTMessageHandler.cs |
业务处理器接口(唯一交付面:遥测/事件/触发/上下线/写回调 + 错误/连接态变更/强制刷新响应) |
Client/IoTKstopa/*.cs |
北向数据模型 KstopaIOTData / KstopaIOTRpcRequest / KstopaIOTRpcResponse / WriteDetail / KstopaIOTFlushResponse |
Client/Catalog/*.cs |
配置目录 API:IKstopaIOTCatalog + 不可变快照/索引 + 只读 DTO + 级联·连接查询(设备 → 分组 → 变量)。只读、纯配置,不含实时值 |
Monitoring/GatewayMonitorService.cs |
网关监控聚合服务(每秒采快照,UI 无关,可复用) |
Monitoring/GatewayMonitorSnapshot.cs |
监控快照模型(连接态 + 网关级累计/速率 + 每设备统计) |
Wpf/ConfigControl.xaml(.cs) |
配置界面:设备维护 / 通讯设置 / 变量配置 / 驱动管理 / RPC 日志 |
Wpf/ConfigViewModel.cs |
配置界面 ViewModel(树、变量网格、增改删、导入导出) |
Wpf/GatewayMonitorView.xaml(.cs) |
监控仪表盘(零依赖 Canvas 自绘曲线) |
Wpf/WriteValueWindow / ValueDetailWindow / CopyDeviceWindow |
写入/查看/复制设备弹窗 |
build/KstopaIOT.Embedded.targets |
NuGet 安装时把驱动 dll 拷贝到输出 drivers/net8.0/ |
两个 Demo(参考实现,功能完全对等——各 5 个 Tab):
| Demo | 说明 |
|---|---|
KstopaIOT.Embedded.Wpf.Demo |
WPF 完整用法:网关 + 配置 + 实时监视 + 写入测试 + 事件/业务处理 + 监控 |
KstopaIOT.Embedded.WinForms.Demo |
WinForms 用 ElementHost 嵌入同一套 WPF 控件(ConfigControl / GatewayMonitorView),其余面板用原生 WinForms 控件实现对等功能 |
两个 Demo 共用同一个
KstopaIOT.Embedded库;ConfigControl与GatewayMonitorView是库内的 WPF 控件,WinForms 侧通过System.Windows.Forms.Integration.ElementHost承载。
4. 环境要求
| 项目 | 要求 |
|---|---|
| .NET | .NET 8 |
| OS | Windows(程序集为 net8.0-windows,依赖 WPF) |
| NuGet 依赖(已自带,无需手动装) | SqlSugarCore / DynamicExpresso.Core / Microsoft.Extensions.Logging / DotNetCore.NPOI / HslCommunication / Newtonsoft.Json / Microsoft.Extensions.Logging.Abstractions;KstopaIOT.Interface 随包内嵌 dll(消费端无需单独还原) |
| 运行期数据 | 输出目录下的 data/kstopaiot.db(NuGet 包自动带种子库) |
| 驱动 | 输出目录下的 drivers/net8.0/*.dll(需自行拷贝;见 §10) |
5. 集成方式(WPF / WinForms / 无界面)
5.0 两个 Demo 的 5 个 Tab 对照
无论 WPF 还是 WinForms,推荐把界面组织成 5 个 Tab,与 Demo 完全对应:
| # | Tab 名称 | 功能 | 关键 API | WPF 控件 | WinForms 控件 |
|---|---|---|---|---|---|
| ① | 监控配置 | 设备/变量/驱动配置编辑器 | ConfigControl.AttachEngine/AttachGateway |
ConfigControl(库内 WPF) |
ElementHost + ConfigControl |
| ② | 实时监视 | 变量实时值网格 | IKstopaIOTMessageHandler.HandleTelemetryAsync |
DataGrid + ObservableCollection<MonitorRow> |
DataGridView + BindingList<MonitorRow>(MonitorRow 实现 INotifyPropertyChanged) |
| ③ | 写入测试 | 下发/强制刷新 | SendWriteAsync / PublishWriteAsync / PublishForceUploadAsync |
ComboBox+TextBox+Button |
同左(原生 WinForms) |
| ④ | 事件 / 业务处理 | 彩色日志 + 来源过滤 | IKstopaIOTMessageHandler(遥测/上下线/事件/触发/写回调/错误/连接态/强制刷新) |
ListBox + LevelToBrushConverter |
ListView + 按 LogLevel 着色 |
| ⑤ | 网关监控 | 连接/吞吐/设备代理聚合 | GatewayMonitorService + GatewayMonitorView.Bind |
GatewayMonitorView(库内 WPF) |
ElementHost + GatewayMonitorView |
状态栏(顶栏):连接点(红/绿)、连接态、设备数、采样计数、业务处理计数——两 Demo 一致。
5.1 方式 A:WPF 完整接入(推荐)
最适合「想直接把网关跑起来,并带完整配置界面」的场景。
步骤 1:新建项目与 csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\KstopaIOT.Embedded\KstopaIOT.Embedded.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="..\KstopaIOT\data\kstopaiot.db"
Link="data\kstopaiot.db" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<Target Name="CopyDrivers" AfterTargets="Build"
Condition="Exists('..\KstopaIOT\bin\$(Configuration)\net8.0\drivers\net8.0')">
<ItemGroup>
<DriverDlls Include="..\KstopaIOT\bin\$(Configuration)\net8.0\drivers\net8.0\**\*.*" />
</ItemGroup>
<MakeDir Directories="$(OutDir)drivers\net8.0" />
<Copy SourceFiles="@(DriverDlls)"
DestinationFolder="$(OutDir)drivers\net8.0\%(RecursiveDir)"
SkipUnchangedFiles="true" />
</Target>
</Project>
步骤 2:App.xaml.cs 启动常驻网关
把 EmbeddedGatewayHost 放到 Application 上常驻,整个进程只有一个网关实例,所有窗口共享同一引擎。
using KstopaIOT.Embedded;
namespace KstopaIOT.Embedded.Wpf.Demo;
public partial class App : Application
{
// 一站式宿主(高级门面:统一驱动目录 + 建网关 + 起监控 + 起引擎)
public EmbeddedGatewayHost Host { get; private set; }
public DemoBusinessHandler BusinessHandler { get; private set; }
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
BusinessHandler = new DemoBusinessHandler(
a => Dispatcher.Invoke(a),
OnTelemetry, (d, online) => OnDeviceConnect(d, online), OnResponse);
// 流式构造;不传 WithHandler 时默认 NullMessageHandler(无需实现接口即可启动)
Host = new EmbeddedGatewayHostBuilder()
.WithHandler(_ => BusinessHandler)
.Build();
// 启动(默认连接配置 = KstopaIOTConnectionConfig.Embedded())
_ = Host.StartAsync();
}
protected override void OnExit(ExitEventArgs e)
{
Host?.StopAsync().GetAwaiter().GetResult(); // 优雅停机:停引擎 + 断开 + 释放资源
base.OnExit(e);
}
// DemoBusinessHandler 经 IKstopaIOTMessageHandler 回传业务日志,这里把设备上下线等转给 MainWindow
private void OnTelemetry(KstopaIOTData d) { /* 见步骤 4:更新实时监视网格 */ }
private void OnDeviceConnect(KstopaIOTData d, bool online) { /* 见步骤 4 */ }
private void OnResponse(KstopaIOTRpcResponse r) { /* 见步骤 4 */ }
}
为什么可以没有真实消息中间件就启动? 嵌入式版的网关是进程内引擎,
StartAsync后IsConnected即变true(不依赖外部传输/平台);配置界面的变量网格照样刷新。所有消息(设备级 + 网关级错误/连接态/强制刷新响应)统一经IKstopaIOTMessageHandler单一通路交付,不再有OnTelemetry/OnDeviceConnect等EventHandler。
步骤 3:MainWindow.xaml — 5 个 Tab 骨架
注意 xmlns 映射:库内 WPF 控件位于 KstopaIOT.Embedded.Wpf,必须用 assembly=KstopaIOT.Embedded 引用。
<Window x:Class="KstopaIOT.Embedded.Wpf.Demo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:KstopaIOT.Embedded.Wpf.Demo"
xmlns:vm="clr-namespace:KstopaIOT.Embedded.Wpf;assembly=KstopaIOT.Embedded"
xmlns:cfg="clr-namespace:KstopaIOT.Embedded.Wpf;assembly=KstopaIOT.Embedded"
Title="KstopaIOT.Embedded — 完整使用 Demo" Height="780" Width="1200"
WindowStartupLocation="CenterScreen">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Border Grid.Row="0" Background="#F2F4F7" CornerRadius="6" Padding="10,6" Margin="0,0,0,8">
<StackPanel Orientation="Horizontal">
<Ellipse x:Name="ConnDot" Width="12" Height="12" Fill="#C0392B" Margin="0,0,8,0"/>
<TextBlock x:Name="ConnText" Text="未连接" VerticalAlignment="Center" FontWeight="Medium"/>
<TextBlock x:Name="DeviceCountText" Text="设备数:0" VerticalAlignment="Center" Margin="24,0,0,0"/>
<TextBlock x:Name="SampleText" Text="采样:0" VerticalAlignment="Center" Margin="24,0,0,0"/>
<TextBlock x:Name="BizText" Text="业务处理:0" VerticalAlignment="Center" Margin="24,0,0,0"/>
</StackPanel>
</Border>
<TabControl Grid.Row="1">
<TabItem Header="监控配置">
<cfg:ConfigControl x:Name="Config"/>
</TabItem>
<TabItem Header="实时监视">
<DataGrid x:Name="MonitorGrid" AutoGenerateColumns="False" IsReadOnly="True" CanUserAddRows="False">
<DataGrid.Columns>
<DataGridTextColumn Header="设备" Binding="{Binding Device}" Width="170"/>
<DataGridTextColumn Header="变量" Binding="{Binding Variable}" Width="170"/>
<DataGridTextColumn Header="值" Binding="{Binding Value}" Width="130"/>
<DataGridTextColumn Header="状态" Binding="{Binding Status}" Width="70"/>
<DataGridTextColumn Header="时间" Binding="{Binding Time, StringFormat=HH:mm:ss.fff}" Width="120"/>
</DataGrid.Columns>
</DataGrid>
</TabItem>
<TabItem Header="写入测试">
<StackPanel Margin="12">
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
<TextBlock Text="设备" VerticalAlignment="Center" Width="48"/>
<ComboBox x:Name="DeviceCombo" IsEditable="True" Width="170" Margin="0,0,10,0"/>
<TextBlock Text="分组" VerticalAlignment="Center" Width="48"/>
<TextBox x:Name="GroupBox" Width="120" Margin="0,0,10,0"/>
<TextBlock Text="变量" VerticalAlignment="Center" Width="48"/>
<TextBox x:Name="VarBox" Width="150" Margin="0,0,10,0"/>
<TextBlock Text="值" VerticalAlignment="Center" Width="36"/>
<TextBox x:Name="ValueBox" Width="140"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
<Button x:Name="WriteWaitBtn" Content="下发(等回调)" Width="120" Click="WriteWaitBtn_Click"/>
<Button x:Name="WriteFireBtn" Content="下发(不等待)" Width="120" Click="WriteFireBtn_Click"/>
<Button x:Name="FlushBtn" Content="强制刷新" Width="100" Click="FlushBtn_Click"/>
</StackPanel>
<TextBlock Text="响应结果:" FontWeight="Medium" Margin="0,6,0,2"/>
<TextBox x:Name="ResponseBox" Height="130" AcceptsReturn="True" IsReadOnly="True"
FontFamily="Consolas" VerticalScrollBarVisibility="Auto"/>
</StackPanel>
</TabItem>
<TabItem Header="事件 / 业务处理">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Margin="0,4,0,6">
<TextBlock Text="事件来源:" VerticalAlignment="Center" Margin="0,0,8,0"/>
<CheckBox x:Name="ChkTelemetry" Content="遥测" IsChecked="True" Margin="0,0,10,0"/>
<CheckBox x:Name="ChkConnect" Content="上下线" IsChecked="True" Margin="0,0,10,0"/>
<CheckBox x:Name="ChkResponse" Content="写回调" IsChecked="True" Margin="0,0,10,0"/>
<CheckBox x:Name="ChkBusiness" Content="业务处理" IsChecked="True" Margin="0,0,10,0"/>
<CheckBox x:Name="ChkError" Content="错误" IsChecked="True" Margin="0,0,10,0"/>
<Button x:Name="ClearLogBtn" Content="清空" Width="70" Click="ClearLogBtn_Click" Margin="12,0,0,0"/>
</StackPanel>
<ListBox x:Name="LogList" Grid.Row="1"/>
</Grid>
</TabItem>
<TabItem Header="网关监控">
<vm:GatewayMonitorView x:Name="MonitorView"/>
</TabItem>
</TabControl>
</Grid>
</Window>
步骤 4:MainWindow.xaml.cs — 接入、事件订阅、写值
using System.Collections.ObjectModel;
using System.Windows;
using KstopaIOT.Embedded;
using KstopaIOT.Embedded.IoTKstopa;
namespace KstopaIOT.Embedded.Wpf.Demo;
public partial class MainWindow : Window
{
private App App => (App)Application.Current;
private EmbeddedGatewayHost Host => App.Host;
private readonly ObservableCollection<MonitorRow> _monitor = new();
private readonly Dictionary<string, MonitorRow> _monitorIndex = new();
private readonly ObservableCollection<LogEntry> _logs = new();
private readonly ObservableCollection<string> _deviceNames = new();
private int _sampleCount, _bizCount;
public MainWindow()
{
InitializeComponent();
MonitorGrid.ItemsSource = _monitor;
LogList.ItemsSource = _logs;
DeviceCombo.ItemsSource = _deviceNames;
Closing += MainWindow_Closing;
}
// 窗口 Loaded 后接管 UI 控件(宿主 StartAsync 已在 App.OnStartup 完成)
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
// StartAsync 后引擎已就绪,直接把同一引擎/监控接管到控件(无需等事件)
Host.AttachConfigControl(Config);
Host.AttachMonitorView(MonitorView);
ConnDot.Fill = new System.Windows.Media.SolidColorBrush(
System.Windows.Media.Color.FromRgb(0x27, 0xAE, 0x60));
ConnText.Text = "已连接";
RefreshDeviceNames();
}
private void MainWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e)
=> Host?.StopAsync().GetAwaiter().GetResult();
private void RefreshDeviceNames()
{
var statuses = Host.Gateway?.Engine?.GetDeviceNameStatuses();
if (statuses == null) return;
foreach (var kv in statuses) if (!_deviceNames.Contains(kv.Key)) _deviceNames.Add(kv.Key);
DeviceCountText.Text = $"设备数:{statuses.Count}";
}
// ── 实时监视(由 DemoBusinessHandler 经 IKstopaIOTMessageHandler.HandleTelemetryAsync 回传) ──
private void OnTelemetry(KstopaIOTData d)
{
_sampleCount++;
SampleText.Text = $"采样:{_sampleCount}";
foreach (var kv in d.Data) UpsertMonitor(d.Device, kv.Key, kv.Value);
if (ChkTelemetry.IsChecked == true)
AddLog(LogLevel.Data, "Telemetry",
$"{d.Device}: {string.Join(", ", d.Data.Select(x => $"{x.Key}={x.Value}"))}");
}
private void UpsertMonitor(string device, string variable, object value)
{
var key = $"{device}|{variable}";
if (_monitorIndex.TryGetValue(key, out var row)) { row.Value = value; row.Time = System.DateTime.Now; }
else
{
row = new MonitorRow { Device = device, Variable = variable, Value = value };
_monitorIndex[key] = row;
_monitor.Add(row);
}
}
private void OnDeviceConnect(KstopaIOTData d, bool online)
{
if (d.Device != null && !_deviceNames.Contains(d.Device)) _deviceNames.Add(d.Device);
if (ChkConnect.IsChecked == true)
AddLog(online ? LogLevel.Success : LogLevel.Warning, "Connect",
$"设备 {d.Device} {(online ? "上线" : "离线")}");
RefreshDeviceNames();
}
private void OnResponse(KstopaIOTRpcResponse r) =>
AddLog(r.Success ? LogLevel.Success : LogLevel.Error, "Response",
$"写回调 成功={r.Success} 设备={r.DeviceName} 消息={r.ResultMessage}");
// ── 写值测试(经 Host.Gateway 直接调用,API 不变) ──
private KstopaIOTRpcRequest BuildRequest()
{
var device = DeviceCombo.Text?.Trim();
var variable = VarBox.Text?.Trim();
var raw = ValueBox.Text?.Trim();
if (string.IsNullOrEmpty(device) || string.IsNullOrEmpty(variable) || string.IsNullOrEmpty(raw))
{ ResponseBox.Text = "请填写 设备 / 变量 / 值"; return null; }
object value = double.TryParse(raw, out var d) ? d : raw;
return new KstopaIOTRpcRequest
{
Device = device,
Group = GroupBox.Text?.Trim(), // 分组可空=默认分组
Data = new Dictionary<string, object> { [variable] = value }
};
}
private async void WriteWaitBtn_Click(object sender, RoutedEventArgs e)
{
var req = BuildRequest(); if (req == null) return;
try
{
var r = await Host.Gateway.SendWriteAsync(req, timeout: System.TimeSpan.FromSeconds(3));
ResponseBox.Text = $"成功={r.Success} 耗时={r.ElapsedTime.TotalMilliseconds:F1}ms\n消息={r.ResultMessage}";
if (r.Response?.WriteDetails != null)
foreach (var w in r.Response.WriteDetails)
ResponseBox.Text += $"\n {w.Variable} = {w.Value} ({w.Method})";
}
catch (System.Exception ex) { ResponseBox.Text = "异常:" + ex.Message; }
}
private async void WriteFireBtn_Click(object sender, RoutedEventArgs e)
{
var req = BuildRequest(); if (req == null) return;
await Host.Gateway.PublishWriteAsync(req);
ResponseBox.Text = "已下发(fire-and-forget,不等待回调)";
}
private async void FlushBtn_Click(object sender, RoutedEventArgs e)
{
if (Host.Gateway == null || !Host.Gateway.IsConnected) { ResponseBox.Text = "未连接"; return; }
await Host.Gateway.PublishForceUploadAsync(null);
ResponseBox.Text = "已触发强制刷新上传";
}
private void AddLog(LogLevel level, string source, string message) =>
_logs.Add(new LogEntry(level, source, message));
private void ClearLogBtn_Click(object sender, RoutedEventArgs e) => _logs.Clear();
}
单一通路要点:数据流(遥测/上下线/事件/触发/写回调)以及网关级信号(错误/连接态变更/强制刷新响应)全部经
IKstopaIOTMessageHandler交付,不再有OnTelemetry/OnDeviceConnect/OnError等EventHandler。DemoBusinessHandler在后台线程收到消息后,自行经marshal(WPF 用Dispatcher.Invoke)切回 UI 线程更新控件——线程切换是 Handler 的责任,宿主不捕获 UI 线程。MonitorRow/LogEntry/LogLevel/LevelToBrushConverter放在DemoModels.cs中(WPF Demo 自带)。
步骤 5:业务处理器 DemoBusinessHandler
实现 IKstopaIOTMessageHandler(唯一交付面,含网关级信号)——业务/UI 回调用构造注入;marshal 把后台线程的消息切回 UI 线程:
public class DemoBusinessHandler : IKstopaIOTMessageHandler
{
public event Action<LogEntry> BusinessLog;
private readonly Action<Action> _marshal;
private readonly Action<KstopaIOTData> _onTelemetry;
private readonly Action<KstopaIOTData, bool> _onDeviceConnect;
private readonly Action<KstopaIOTRpcResponse> _onResponse;
public DemoBusinessHandler(Action<Action> marshal,
Action<KstopaIOTData> onTelemetry,
Action<KstopaIOTData, bool> onDeviceConnect,
Action<KstopaIOTRpcResponse> onResponse)
{
_marshal = marshal ?? (a => a());
_onTelemetry = onTelemetry;
_onDeviceConnect = onDeviceConnect;
_onResponse = onResponse;
}
public Task HandleTelemetryAsync(KstopaIOTData d)
{
BusinessLog?.Invoke(new LogEntry(LogLevel.Info, "Business", $"遥测 {d.Device}: {Summarize(d)}"));
_marshal(() => _onTelemetry?.Invoke(d)); // 切回 UI 线程
return Task.CompletedTask;
}
public Task HandleEventAsync(KstopaIOTData d) =>
Emit(LogLevel.Info, $"事件 {Summarize(d)}");
public Task HandleTriggerAsync(KstopaIOTData d) =>
Emit(LogLevel.Warning, $"触发 {Summarize(d)}");
public Task HandleDeviceConnectAsync(KstopaIOTData d)
{
var online = d.Data.TryGetValue("connected", out var v) && v is bool b && b;
_marshal(() => _onDeviceConnect?.Invoke(d, online));
return Task.CompletedTask;
}
public Task HandleResponseAsync(KstopaIOTRpcResponse r) =>
Emit(r.Success ? LogLevel.Success : LogLevel.Error, $"写回调 {r.DeviceName}");
// ── 网关级信号(设备级之外,同样经 Handler 交付) ──
public Task HandleErrorAsync(string error)
=> Emit(LogLevel.Error, $"网关错误:{error}");
public Task HandleConnectionChangedAsync(bool connected)
=> Emit(LogLevel.Info, connected ? "网关已连接" : "网关已断开");
public Task HandleFlushResponseAsync(KstopaIOTFlushResponse r)
=> Emit(LogLevel.Info, $"强制刷新响应:{r.Message}");
private Task Emit(LogLevel level, string msg)
{
BusinessLog?.Invoke(new LogEntry(level, "Business", msg));
return Task.CompletedTask;
}
private static string Summarize(KstopaIOTData d)
{
var sb = new System.Text.StringBuilder();
if (d.Data != null) foreach (var kv in d.Data) { if (sb.Length > 0) sb.Append(", "); sb.Append(kv.Key).Append('=').Append(kv.Value); }
return sb.ToString();
}
}
全部 8 个接口方法都在后台线程执行;要更新 UI 必须自行切回 UI 线程(Demo 用
marshal)。Telemetry / DeviceConnect / Response走 BoundedChannel + 单消费者(顺序 FIFO);Event / Trigger走 UnboundedChannel +SemaphoreSlim限流并行(默认 20 并发,运行时零丢失)。Handler 里可以await异步操作。
步骤 6:运行与验证
- 先把原 KstopaIOT 构建一次(产出
KstopaIOT/bin/Debug/net8.0/drivers/net8.0/*.dll),供CopyDrivers目标拷贝。 - 将
KstopaIOT.Embedded.Wpf.Demo设为启动项目 → F5。 - 顶部状态栏「未连接」变「已连接」,① 监控配置 Tab 的变量列表开始显示实时值。
- ② 实时监视随遥测涨「采样」计数;③ 写入测试可下发/强制刷新;④ 事件日志随勾选滚动;⑤ 网关监控显示吞吐曲线与设备卡。
5.2 方式 B:WinForms 完整接入(ElementHost)
原理与方式 A 完全一致,唯一区别:WinForms 没有原生 WPF,需要 System.Windows.Forms.Integration.ElementHost 承载库内的两个 WPF 控件(ConfigControl / GatewayMonitorView),其余面板用原生 WinForms 控件实现对等功能。
步骤 1:csproj(关键:两个 UI 框架都要开)
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<UseWPF>true</UseWPF>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\KstopaIOT.Embedded\KstopaIOT.Embedded.csproj" />
</ItemGroup>
</Project>
⚠️ 坑:WinForms 项目若只开
UseWindowsForms不开UseWPF,ElementHost与ConfigControl/GatewayMonitorView都会报「找不到类型」。两者必须同时开启。
步骤 2:Program.cs 入口
using System;
using System.Windows.Forms;
namespace KstopaIOT.Embedded.WinForms.Demo;
internal static class Program
{
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new MainForm()); // 所有引导在 MainForm 构造里完成
}
}
步骤 3:MainForm 构造 —— TabControl + ElementHost
using System.IO;
using System.Windows.Forms;
using System.Windows.Forms.Integration;
using KstopaIOT.Embedded.Monitoring;
using KstopaIOT.Embedded.Wpf;
public class MainForm : Form
{
private readonly ConfigControl _config;
private readonly GatewayMonitorView _monitorView;
private EmbeddedGatewayHost _host;
private DemoBusinessHandler _handler;
// 实时监视(BindingList + INotifyPropertyChanged 实现单元格实时刷新)
private readonly BindingList<MonitorRow> _monitor = new();
private readonly Dictionary<string, MonitorRow> _monitorIndex = new();
private readonly List<LogEntry> _logBuffer = new();
private readonly ListView _logList; // 事件/业务处理日志
private readonly DataGridView _monitorGrid; // 实时监视
private ComboBox _deviceCombo; // 写值测试设备下拉
// …… 状态栏标签、过滤开关、其余控件
public MainForm()
{
Text = "KstopaIOT.Embedded — WinForms 完整使用 Demo";
Size = new Size(1200, 780);
StartPosition = FormStartPosition.CenterScreen;
MinimumSize = new Size(900, 600);
var dbPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "data", "kstopaiot.db");
// ① 监控配置:WPF ConfigControl 经 ElementHost 嵌入
_config = new ConfigControl(dbPath); // 指定 db 路径
var cfgHost = new ElementHost { Dock = DockStyle.Fill, Child = _config };
var cfgPage = new TabPage("监控配置");
cfgPage.Controls.Add(cfgHost);
// ② 实时监视:原生 DataGridView,经 Handler.HandleTelemetryAsync 回传更新
_monitorGrid = new DataGridView { /* Dock=Fill, ReadOnly, AutoGenerateColumns=false … */ };
_monitorGrid.DataSource = _monitor;
// 加列:Device / Variable / Value / Status / Time(Time 用 HH:mm:ss.fff 格式)
// ……
// ③ 写入测试:BuildWriteTab(...)
// ④ 事件/业务处理:ListView + 过滤栏
// ⑤ 网关监控:WPF GatewayMonitorView 经 ElementHost
_monitorView = new GatewayMonitorView();
var monViewHost = new ElementHost { Dock = DockStyle.Fill, Child = _monitorView };
var monViewPage = new TabPage("网关监控");
monViewPage.Controls.Add(monViewHost);
// 主 TabControl
var tabs = new TabControl { Dock = DockStyle.Fill };
tabs.TabPages.AddRange(new TabPage[] { cfgPage, monPage, writePage, logPage, monViewPage });
Controls.Add(tabs);
// 启动内嵌网关(与 WPF Demo 等价的引导)
StartGateway();
}
private void MainForm_Shown(object sender, EventArgs e)
{
// StartAsync 后引擎已就绪,直接接管 UI 控件
_host.AttachConfigControl(_config);
_host.AttachMonitorView(_monitorView);
_connDot.BackColor = Color.FromArgb(0x27, 0xAE, 0x60);
_connText.Text = "已连接";
RefreshDeviceNames();
}
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
=> _host?.StopAsync().GetAwaiter().GetResult();
}
ElementHost 布局坑(已踩过):
ElementHost.Dock = DockStyle.Fill直接托管含TabControl的 WPF 控件时,标签头条可能被裁剪。Demo 用TableLayoutPanel(状态栏 30px 固定 + 内容区 100% 填充)包住ElementHost,再放进TabControl的TabPage;若仍偶发标签头被裁,可在窗口Shown时强制 WPF 重排:_config.Dispatcher.InvokeAsync(() => { _config.InvalidateArrange(); _config.UpdateLayout(); });。
步骤 4:启动网关(Host + Handler 单一通路,含跨线程编组)
private void StartGateway()
{
_handler = new DemoBusinessHandler(
Ui, // marshal:把后台线程消息切回 UI 线程
OnTelemetry, (d, online) => OnDeviceConnect(d, online), OnResponse);
_handler.BusinessLog += msg => Ui(() => // 业务处理器回传 → UI 线程
{
_bizCount++;
_bizText.Text = $"业务处理:{_bizCount}";
AddLog(LogLevel.Info, "Business", msg);
};
// 流式构造;handlerFactory 对所有设备返回同一处理器
_host = new EmbeddedGatewayHostBuilder()
.WithHandler(_ => _handler)
.Build();
// 启动(默认连接配置 = KstopaIOTConnectionConfig.Embedded())
_ = _host.StartAsync();
// 控件接管在 MainForm_Shown 里做(StartAsync 后引擎已就绪)
}
// 跨线程安全回到 UI 线程
private void Ui(Action a)
{
if (IsDisposed) return;
if (InvokeRequired) Invoke(a); else a();
}
WinForms ↔ WPF 线程要点:
- 网关消息在后台线程经
IKstopaIOTMessageHandler交付;更新 WinForms 控件前用Ui()(或 Handler 构造里的marshal)切回 UI 线程。- 操作
ConfigControl/GatewayMonitorView这些 WPF 控件时,要走它们自己的Dispatcher(如_config.Dispatcher.InvokeAsync(...)),不要直接在 WinForms UI 线程上碰 WPF 内部属性。WPF Demo 的MainWindow_Loaded/ WinForms 的MainForm_Shown里AttachConfigControl/AttachMonitorView已在正确时机接管。
步骤 5:业务处理器(WinForms 版)
与 WPF 版逻辑完全一致,仅日志回调类型不同——WinForms 版用 event Action<string>,配合自己的 DemoModels.cs(LogLevel / LogEntry / MonitorRow),避免依赖 WPF 类型:
public class DemoBusinessHandler : IKstopaIOTMessageHandler
{
public event Action<string> BusinessLog; // 注意:WinForms 版回传 string
public Task HandleTelemetryAsync(KstopaIOTData d)
{
BusinessLog?.Invoke($"遥测 {d.Device}: {Summarize(d)}");
return Task.CompletedTask;
}
// HandleEventAsync / HandleTriggerAsync / HandleDeviceConnectAsync / HandleResponseAsync 同构
private static string Summarize(KstopaIOTData d) { /* 同 WPF 版 */ }
}
步骤 6:实时监视 / 写值 / 日志(原生 WinForms 对等实现)
| 功能 | WPF 做法 | WinForms 做法(等价) |
|---|---|---|
| 实时监视 | ObservableCollection<MonitorRow> 绑 DataGrid |
BindingList<MonitorRow> 绑 DataGridView,且 MonitorRow 实现 INotifyPropertyChanged(单元格值变化实时刷新) |
| 写值测试 | 3 个 Button 的 Click 事件 |
3 个 Button,WinForms 事件只能用 btn.Click += Handler(不能用对象初始化器里的 Click = Handler,否则 CS0079) |
| 事件日志 | ListBox + LevelToBrushConverter |
ListView(Details 视图)+ 按 LogLevel 用 ForeColor 着色;过滤勾选用 CheckBox.CheckedChanged |
WinForms 编译常见错误:CS0079——事件不能在对象初始化器
{ }里用Click = handler赋值,必须先new Button{...}再.Click += Handler。CS0234——Padding属于System.Windows.Forms,不要写成System.Drawing.Padding。
步骤 7:运行与验证
与方式 A 步骤 6 相同(设为启动项目 → F5 → 状态栏变绿 → 5 个 Tab 均工作)。
5.3 方式 C:无界面 / 控制台(只用内核)
只想进程内采集 + 读写,不连 IoTKstopa 平台、不显示任何界面——直接用 GatewayEngine:
var dbPath = Path.Combine(AppContext.BaseDirectory, "data", "kstopaiot.db");
var engine = new GatewayEngine(dbPath);
await engine.StartAsync(); // 自动加载所有 AutoStart 设备
// 订阅实时值变化(Good 且 IsUpload 的变量)
engine.ValueChanged += (_, e) =>
Console.WriteLine($"{e.DeviceName}/{e.VariableName} = {e.Value} ({e.Status})");
engine.StatusChanged += (_, e) =>
Console.WriteLine($"设备 {e.DeviceName} {(e.IsConnected ? "上线" : "离线")}");
// 按需读 / 写 / 连接
var v = await engine.ReadAsync("设备A", "温度");
await engine.WriteAsync("设备A", new RpcRequest { /* 见下方 §9 */ });
await engine.ConnectAsync("设备A");
// 程序退出时
await engine.StopAsync();
6. 核心 API 参考
6.1 EmbeddedDb(数据访问)
轻封装 SqlSugarClient,连接 kstopaiot.db(SQLite)。若数据库文件不存在则通过 CodeFirst 自动建库建表(6 张核心表)并写入最小种子数据(网关配置一行 + 扫描到的驱动记录);已存在则仅映射、不建表、不迁移。 全程零 EF 依赖。
var db = new EmbeddedDb(dbPath);
List<Device> devices = db.GetDevices();
List<DeviceVariable> vars = db.GetDeviceVariables(deviceId);
List<DeviceConfig> configs = db.GetDeviceConfigs(deviceId);
List<Driver> drivers = db.GetDrivers();
SystemConfig sysCfg = db.GetSystemConfig();
List<RpcLog> logs = db.GetRpcLogs(500); // 最近 500 条
int n = db.DeleteRpcLogs(new[]{ id }); // 删选中
int m = db.DeleteAllRpcLogs(); // 清空
SqlSugarClient raw = db.Client; // 需要手写查询时用
所有实体都带
[SugarTable]特性,字段名与kstopaiot.db原表一一对应(见 §8)。
6.2 GatewayEngine(采集引擎)
进程内采集引擎门面。方式 A/B 中它由 KstopaIOTGateway 内部持有并通过 .Engine 暴露;方式 C 中你直接 new。
生命周期
| 方法 | 说明 |
|---|---|
StartAsync() |
加载所有 AutoStart=true 的设备 → 创建 DeviceWorker → 起轮询 |
StopAsync() |
停止并摘除所有设备工作器 |
StopDeviceAsync(deviceId) |
停止并摘除单个设备 |
RestartDeviceAsync(deviceId) |
关闭并按最新 DB 配置重建(保存设备属性后调用) |
EnsureDeviceRunningAsync(deviceId) |
让引擎运行时状态与 DB 完全一致(应运行则启/重,否则停) |
直接调用 API
| 方法 | 说明 |
|---|---|
ReadAsync(deviceName, variableName) |
按需读单个变量,返回 DriverReturnValueModel |
WriteAsync(deviceName, RpcRequest) |
写值(入队到该设备写队列) |
ConnectAsync(deviceName) / DisconnectAsync(deviceName) |
连接 / 断开设备 |
ForceFlush(string[] deviceNames = null) |
强制刷新上传(无视变化检测,全量推 IsUpload 变量) |
SnapshotDevice(deviceId) |
立即补推该设备最新一帧(解决 UI 先于引擎接入时首帧丢失) |
GetDeviceStatuses() / GetDeviceNameStatuses() |
当前运行设备的连接状态 |
Catalog |
设备 / 分组 / 变量 配置目录(IKstopaIOTCatalog,只读、纯配置、不含实时值;见 §6.9) |
事件
| 事件 | 参数 | 触发时机 |
|---|---|---|
ValueChanged |
VariableValueChangedEventArgs |
变量值变化 |
VariableSampled |
VariableValueChangedEventArgs |
采样(Good 且 IsUpload,供 UI 实时显示) |
StatusChanged |
DeviceStatusChangedEventArgs |
设备上线/离线 |
OnForceFlush |
ForceFlushEventArgs |
收到强制刷新指令 |
RpcCompleted |
RpcResponse |
写操作完成 |
GroupPublished |
VariableGroupEventArgs |
触发分组整包发布(Event/Trigger 频道) |
// 写值示例
await engine.WriteAsync("设备A", new RpcRequest
{
DeviceName = "设备A",
Method = "Write", // 由网关按变量覆盖自动判定 write / writecache
Data = new Dictionary<string, object> { ["温度上限"] = 80 }
});
6.3 DriverLoader(驱动加载)
通常不需要直接调用——GatewayEngine 内部已用它。但若你要自定义驱动实例化流程,可手动使用:
// 1. 扫描所有实现 IDriver 的非抽象类型
List<Type> types = DriverLoader.LoadDriverTypes(); // 扫描 drivers/net8.0/*.dll
// 2. 实例化一个驱动((string deviceName, ILogger) 构造 + [ConfigParameter] 注入 + 三套委托构建)
LoadedDriver loaded = DriverLoader.CreateDriver(
driverType, deviceName, logger, configs /* List<DeviceConfig> */);
IDriver instance = loaded.Instance;
var readDel = loaded.MethodDelegates["Read"]; // Func<ioArg,Task<ret>>
var setDel = loaded.MethodSetDelegates["..."];
var cacheDel = loaded.ResolverCacheDelegates["..."];
DriverLoader使用自定义AssemblyLoadContext(DriverLoadContext)加载驱动:优先复用主程序已加载的共享程序集(KstopaIOT.Interface等,保证typeof(IDriver)与驱动内的IDriver是同一类型),驱动专属依赖(HslCommunication/Opc.Ua.*等)才从drivers/net8.0/目录加载——避免类型身份分裂导致「DLL 在却识别不到驱动」。
6.4 KstopaIOTGateway(北向门面)/ EmbeddedGatewayHost(一站式宿主)
EmbeddedGatewayHost(推荐入口)
把「统一驱动目录 → 建 KstopaIOTGateway → 起监控聚合 → 启动引擎 → 接管 UI 控件」压缩成几行:
var host = new EmbeddedGatewayHostBuilder()
.WithHandler(deviceCode => new MyHandler(deviceCode)) // 可选;缺省 NullMessageHandler
.WithLogger(msg => Console.WriteLine(msg)) // 可选
.Build();
await host.StartAsync(); // 默认 KstopaIOTConnectionConfig.Embedded()
host.AttachConfigControl(configControl); // 接管配置界面(引擎已就绪,直接 Attach)
host.AttachMonitorView(monitorView); // 接管监控仪表盘
// ...
await host.StopAsync();
| 成员 | 说明 |
|---|---|
StartAsync(config = null) |
统一驱动目录 → 建 KstopaIOTGateway → 起监控 → 启动引擎;config 为 null 时用 KstopaIOTConnectionConfig.Embedded() |
StopAsync() |
停引擎 + 断开 + 释放资源 |
AttachConfigControl(ConfigControl) |
接管配置界面(直接 AttachEngine+AttachGateway,无需等事件) |
AttachMonitorView(GatewayMonitorView) |
接管监控仪表盘 |
Gateway |
底层 KstopaIOTGateway(写值/查询 API 经此) |
Catalog / RefreshCatalog() |
配置目录(IKstopaIOTCatalog,见 §6.9);配置变更后可用 RefreshCatalog() 立即重建 |
Engine / Monitor / Options |
底层引擎 / 监控聚合服务 / 本次使用的运行时选项 |
不再暴露任何
EventHandler:网关层(错误/连接态变更/强制刷新响应)与设备层(遥测/上下线/事件/触发/写回调)消息统一经IKstopaIOTMessageHandler单一通路交付(见 §6.6)。宿主不捕获 UI 线程,UI 更新由 Handler 负责。
KstopaIOTGateway(底层门面)
一行启动 IoTKstopa 对接,自动完成「按设备建代理 → 消息入 Channel → 路由到业务 Handler」。通常直接用上面的 EmbeddedGatewayHost,无需直接 new 它。
// 构造:传入 handlerFactory(deviceCode → IKstopaIOTMessageHandler),可选 logger 与 dbPath
var gateway = new KstopaIOTGateway(
handlerFactory: deviceCode => new MyHandler(deviceCode),
logger: msg => Console.WriteLine(msg));
await gateway.StartAsync(new KstopaIOTConnectionConfig { Ip = "127.0.0.1", Port = 1883 });
await gateway.StopAsync();
属性 / 方法
| 成员 | 说明 |
|---|---|
StartAsync(config) |
建立连接并启动消费者 |
StopAsync() / DisposeAsync() |
断开 + 释放代理与资源 |
PublishWriteAsync(req) |
写请求,fire-and-forget 不等待 |
SendWriteAsync(req, timeout, ct) |
写请求 + 等待回调,返回 CallbackWaitResult |
PublishForceUploadAsync(deviceNames = null) |
强制刷新上传(null=连接配置中的设备列表) |
RemoveDeviceAsync(deviceCode) |
移除设备代理(设备下线时) |
GetDroppedStats(code) / GetReceivedStats(code) |
丢弃 / 接收统计(背压诊断) |
GetDeviceStats(code) / GetAllDeviceStats() |
单设备 / 全部设备统计(DeviceStats) |
GetInfo() |
网关完整快照(GatewayInfo) |
Engine |
底层 GatewayEngine(供 ConfigControl.AttachEngine 复用同一单引擎) |
IsConnected / ConnectionCount / ProxyCount / ConnectionConfig |
连接态 / 累计连接数 / 代理数 / 当前配置 |
无 EventHandler:所有消息都走 IKstopaIOTMessageHandler(见 §6.6)。
6.5 KstopaIOTConnectionConfig
public class KstopaIOTConnectionConfig
{
public string ClientId = "lms";
public bool AppendTimestampToClientId = false; // true 时 ClientId 追加 yyyyMMddHHmmssfff
public string Ip = "127.0.0.1";
public int Port = 1883;
public string UName; // IoTKstopa 通常留空
public string UPwd;
public string[] Devices; // 设备白名单;PublishForceUploadAsync(null) 时作为默认设备集
}
嵌入式场景可直接用
KstopaIOTConnectionConfig.Embedded()取得默认值(ClientId="embedded"、Ip="127.0.0.1"、Port=1883),EmbeddedGatewayHost.StartAsync()不传参时即用它。
6.6 IKstopaIOTMessageHandler(业务处理器)
实现此接口处理来自特定设备的消息——这是网关唯一的对外交付面(设备级 + 网关级信号都走它)。网关自动按 deviceCode 扇出到对应代理;网关级信号(错误/连接态/强制刷新响应)用保留键 __gateway__ 取到同一个 handler 实例交付。
public class MyHandler : IKstopaIOTMessageHandler
{
private readonly string _device;
public MyHandler(string deviceCode) => _device = deviceCode;
// ── 设备级消息 ──
public Task HandleTelemetryAsync(KstopaIOTData d)
=> Task.FromResult(Console.WriteLine($"[{_device}] 遥测: {string.Join(", ", d.Data)}"));
public Task HandleEventAsync(KstopaIOTData d) => Task.CompletedTask;
public Task HandleTriggerAsync(KstopaIOTData d) => Task.CompletedTask;
public Task HandleDeviceConnectAsync(KstopaIOTData d) => Task.CompletedTask; // d.Data["connected"]=true/false 判上下线
public Task HandleResponseAsync(KstopaIOTRpcResponse r) => Task.CompletedTask;
// ── 网关级信号(所有设备复用同一 handler 实例时,会到达同一个 MyHandler) ──
public Task HandleErrorAsync(string error)
=> Task.FromResult(Console.WriteLine($"网关错误: {error}"));
public Task HandleConnectionChangedAsync(bool connected)
=> Task.FromResult(Console.WriteLine(connected ? "网关已连接" : "网关已断开"));
public Task HandleFlushResponseAsync(KstopaIOTFlushResponse r)
=> Task.FromResult(Console.WriteLine($"强制刷新响应: {r.Message}"));
}
消费策略:
Telemetry / DeviceConnect / Response走 BoundedChannel + 单消费者(顺序 FIFO);Event / Trigger走 UnboundedChannel +SemaphoreSlim限流并行(默认 20 并发,运行时零丢失)。Handler 在后台线程执行,要更新 UI 必须自行切回 UI 线程;Handler 里可以await异步操作。
北向数据模型
public class KstopaIOTData // 遥测/事件/触发
{ string Device; string Group; Dictionary<string,object> Data; }
public class KstopaIOTRpcRequest // 写请求
{ string Device; string Group; string Id; Dictionary<string,object> Data; }
public class KstopaIOTRpcResponse // 写回调
{ bool Success; string ResultMessage; string DeviceName; string Group; string RequestId; List<WriteDetail> WriteDetails; }
public class CallbackWaitResult // SendWriteAsync 返回值
{ bool Success; TimeSpan ElapsedTime; string ResultMessage; KstopaIOTRpcResponse Response; }
6.7 KstopaIOTDeviceProxy 统计(背压诊断)
代理内部维护计数,可通过 gateway.GetDroppedStats(...) / GetReceivedStats(...) / GetDeviceStats(...) 获取:
- 接收计数:
TelemetryReceived/EventReceived/TriggerReceived/ResponseReceived/ConnectStatusReceived - 丢弃计数:
TelemetryDropped/EventDropped/TriggerDropped/ConnectStatusDropped/ResponseDropped(Bounded 通道满时递增) - 积压:
EventPendingCount/TriggerPendingCount(Unbounded 通道的实时积压) - 连接状态机:
IsDeviceConnected/ConnectionStatus/ReconnectCount/HeartbeatCount/FirstConnectTime/LastStatusChangeTime
调优项(构造代理时设置):
ChannelCapacity(默认 1024,作用于 Telemetry/Connect/Response);ParallelMaxDegree(默认 20,作用于 Event/Trigger)。
6.8 GatewayMonitorService / Snapshot(监控)
UI 无关的聚合服务,每秒从网关采一次快照,可复用于任何宿主。连接态变更通过轮询 _gateway.IsConnected 检测(不再依赖网关事件):
var svc = new GatewayMonitorService(gateway);
svc.SnapshotUpdated += (_, snap) => // 每秒一拍
{
Console.WriteLine($"连接={snap.IsConnected} 遥测累计={snap.TotalTelemetry} 速率={snap.RateTelemetry}/s");
foreach (var d in snap.Devices)
Console.WriteLine($" {d.DeviceCode} 在线={d.IsOnline} 丢弃={d.TelemetryDropped}");
};
svc.ConnectionChanged += (_, e) => Console.WriteLine(e.connected ? "已连" : $"断开:{e.error}"); // 轮询检测到的连接态变迁
svc.Start();
// ...
svc.Stop();
GatewayMonitorView(WPF)已内置 Bind(service),直接复用此服务,无需自己画曲线。
6.9 IKstopaIOTCatalog(设备 / 分组 / 变量 配置目录)
把「设备 → 分组 → 变量」的配置拓扑与变量元数据一次性对外提供,供调用方快速关联运行时消息。
只读、纯配置:本 API 只返回配置信息,不返回任何实时值 / 运行态(在线状态、当前值、时间戳、计数都不在这里)。 实时数据继续走
IKstopaIOTMessageHandler单一通路,两条通路职责不重叠。
// 入口 ①:宿主(StartAsync 之后可用)
var catalog = host.Catalog;
// 入口 ②:只要配置、不启动引擎 / 不采集
var catalog2 = KstopaIOTCatalog.Open(@"data/kstopaiot.db");
// 入口 ③:方式 C 只用内核时
var catalog3 = new GatewayEngine(dbPath).Catalog; // 引擎未 StartAsync 也可用
单层查询
| 方法 | 说明 |
|---|---|
GetDevices(onlyDevices = false) |
全部设备(含设备组),按 Index 升序;onlyDevices: true 排除设备组 |
GetDevice(deviceName) |
单设备;含驱动名、父子设备组、Path(如 车间A/1号机)、周期、自动启动、变化上传复位策略 |
GetGroups(deviceName) |
分组清单("" = 默认分组),顺序 = 组内最小变量 Index 升序;含 PublishChannel(该组是逐变量遥测还是 event/trigger 整包) |
GetVariables(deviceName, group = null) |
变量清单;含类型 / 地址 / 字节序 / 读写表达式 / 权限 / 触发 / 上报 / 排序 + 派生判定 IsWritable、IsCached |
级联 / 连接查询(设备 → 分组 → 变量)
// 级联:三层嵌套一次拿到(query 为 null 等价「全部」)
foreach (var dn in catalog.GetCascade(new KstopaIOTCatalogQuery { OnlyDevices = true }))
{
Console.WriteLine($"{dn.Device.Path}({dn.Device.DriverName})共 {dn.VariableCount} 个变量");
foreach (var gn in dn.Groups)
Console.WriteLine($" [{gn.Group.DisplayName}] {gn.Group.PublishChannel} × {gn.Variables.Count}");
}
// 连接:一行一变量的扁平投影(= Iot_Device ⨝ Iot_DeviceVariable),直接绑表格 / 导出
foreach (var r in catalog.QueryVariables(new KstopaIOTCatalogQuery { OnlyWritable = true }))
Console.WriteLine($"{r.DevicePath} / {r.GroupDisplay} / {r.Variable.Name} @ {r.Variable.DeviceAddress}");
// 分步级联(UI 懒加载:选设备才拉分组、选分组才拉变量)
var groups = catalog.GetGroups("1号机");
var vars = catalog.GetVariables("1号机", groups[0].Key);
KstopaIOTCatalogQuery 三层各自可选白名单,另有 OnlyDevices / OnlyWritable / OnlyUpload / OnlyTrigger 预设:
null或空集合 = 该层不过滤;Groups = new[] { "" }= 只看默认分组。- 无过滤时与单层投影同构(无变量的设备组节点也保留,它承载组织层级); 有变量层谓词时,因谓词变空的分组 / 设备节点会被剪除(不留空壳)。
- 三种投影(树 / 级联 / 扁平)共用同一份索引,结果天然自洽。
关联(运行时消息 → 配置项)
// ① 单点关联(语义与引擎一致:group 传 null 时按变量名取首个候选)
var info = catalog.ResolveVariable("1号机", "温度", "TechParam");
// ② 一次关联整条消息:变量名 → 配置项(不含值)
var map = catalog.ResolveMessage(data);
foreach (var kv in data.Data)
if (map.TryGetValue(kv.Key, out var hit))
Console.WriteLine($"{hit.DeviceName}/{hit.Group}/{hit.Name} = {kv.Value}({hit.DataType})");
// ③ 反查:这个变量名在哪些设备 / 分组里
var all = catalog.FindVariablesByName("温度");
关联口径有三条必须知道的规则(都与引擎对齐):
- 严格匹配:设备名 / 变量名 / 分组键都是序数、大小写敏感比较(引擎如此)。
分组键会做
Trim归一(配置侧保存时已 Trim)。未命中返回null,不猜。 group = null取首个候选:等价引擎FindDeviceVariable的group == null ? FirstOrDefault(), 而不是报「歧义」。- 空分组的上下线消息:
DeviceWorker对空分组设备会用设备名充当分组名 (gk = 空分组 ? 设备名 : 分组键),因此上下线消息里可能Group == Device。ResolveMessage会把这种情况还原为默认分组;若该设备下确实存在同名分组,则不会被误判。
返回值里的
Group是归一后的规范值(info.Group)。构造写请求时请用它,不要回用你传入的原始字符串。
刷新与事件
| 成员 | 说明 |
|---|---|
Snapshot |
当前不可变快照(任意线程可读);首次访问构建,之后按 TTL 惰性重建 |
Refresh() |
立即重建并原子替换;内容不变则复用旧快照、不触发事件 |
SnapshotChanged |
快照内容变化时触发(首次构建不触发,TTL 重建时也可能触发);在触发线程上同步触发,UI 编组由调用方负责 |
- 默认 TTL 5s,无定时器、无后台线程(访问时才判 TTL):
.WithCatalogRefreshInterval(TimeSpan.FromSeconds(2))调整,TimeSpan.Zero= 关闭自动重建(仅显式Refresh())。 - 目录不接触引擎运行态(不读
_workers、不订阅引擎事件),因此与采集线程零交互。 - 与配置界面并发写库时的
database is locked会重试一次;仍失败则保留旧快照并记录日志,不会抛给调用方。 Snapshot.OrphanVariableCount报告被排除的孤儿变量数(DeviceId为空或指向不存在的设备 —— 这类变量引擎也不会加载)。
设备类型口径:
DeviceTypeEnum0 = 真实设备(可采集,引擎为其建采集线程)、1 = 设备组(仅归类,无驱动、不采集)。 目录里的Kind即按此归一(Device/DeviceGroup);GetDevices(onlyDevices: true)只回真实设备。
6.10 变量执行顺序(= 配置界面展示顺序)
变量的采集 / 发布顺序分三层,ConfigControl 变量网格与目录接口的展示顺序与之完全一致:
- 组顺序:按组内最小
Index升序(左侧设备树的分组节点同序); - 组内先后:先读触发变量、再读非触发变量(触发是否满足决定本组普通变量是否采集);
- 组内同级:按
Index升序。
引擎加载变量时已显式按 Index 排序(GatewayEngine.CreateWorkerForDevice),因此 GroupBy 的首次出现顺序
就等于「组内最小 Index 升序」—— 执行顺序是一份确定契约,不随 SQLite 返回顺序漂移。
Index就是执行顺序:调小会提前采集、调大则推后;新增变量默认取「本设备最大 Index + 1」。 注意:若 B 组变量表达式引用了 A 组变量(如A组_温度),请确保 A 组的最小Index排在 B 组之前, 否则第一个采集周期该引用可能尚未取到值(可用docs/变量展示顺序与执行顺序对齐方案.md里的体检方法排查)。
7. 内置 WPF 控件
7.1 ConfigControl(配置界面)
完整的设备采集配置 UI,含 5 个 Tab:设备维护 / 通讯设置 / 变量配置 / 驱动管理 / RPC 日志。
// 方式 1:用默认路径(AppContext.BaseDirectory/data/kstopaiot.db)
// —— WPF Demo 在 XAML 里直接 <cfg:ConfigControl x:Name="Config"/> 即用此路径
var control = new ConfigControl();
// 方式 2:指定 db 路径(WinForms Demo 用这种,因为程序基目录/工作目录不一定一致)
var control = new ConfigControl(dbPath);
// 接入运行中的引擎 → 变量网格实时显示 原值/值/状态
control.AttachEngine(engine);
// 接入运行中的网关 → 删除设备时同步摘除监控代理
control.AttachGateway(gateway);
接入时机:必须等引擎就绪(
gateway.Engine != null)后再AttachEngine,否则变量网格不刷新。用EmbeddedGatewayHost时,StartAsync()内部已等待引擎启动完成,随后直接AttachConfigControl(control)(或宿主自动在就绪后接管)即可,无需再监听任何连接事件。不要把AttachEngine放在App.OnStartup里——那时宿主还没StartAsync。WinForms 注意:操作
ConfigControl必须在它的Dispatcher上(_config.Dispatcher.InvokeAsync(...)),见 §5.2 步骤 4。
变量配置支持:增删改、关键字+权限+类型组合筛选、Excel 导入/导出(NPOI,支持下拉框数据校验)。导入对齐 KstopaIOT 前端语义:弹窗选择「替换导入(同分组同名覆盖更新)/ 增量导入(同分组同名跳过)」,新名称一律新增;不合法行整行跳过并在结果中逐行提示(成功新增/替换/跳过/失败统计)。
界面预览(ConfigControl · 5 个 Tab)
以下截图取自 WPF / WinForms Demo,展示
ConfigControl各 Tab 的实际界面。
① 设备维护 —— 设备增删改、驱动绑定、自动启动、采集/指令周期等。
② 通讯设置 —— 网关连接参数(地址 / 端口 / 账号)与运行态。
③ 变量配置 —— 变量增删改、组合筛选、Excel 导入/导出。
④ 驱动管理 —— 已扫描驱动的查看与维护;「注册驱动」弹窗下拉选择 drivers/net8.0 中的驱动 DLL(已注册项置灰),选中后 AssembleName 由反射自动填充(= IDriver 类型 FullName),无需手填程序集名。
⑤ RPC 日志 —— 平台反向控制调用的记录与结果。
7.2 GatewayMonitorView(监控仪表盘)
零依赖自绘的网关监控面板(连接横幅 + 6 个指标卡 + 吞吐曲线 + 设备代理卡)。
var view = new GatewayMonitorView();
var svc = new GatewayMonitorService(gateway);
svc.Start();
view.Bind(svc); // 进程内直连聚合服务,无需 SignalR
界面预览(GatewayMonitorView)
零依赖 Canvas 自绘:连接横幅 + 指标卡 + 吞吐曲线 + 设备代理卡。
7.3 权限模式
ConfigControl 支持只读/全权限切换(禁用所有增改删按钮与可编辑网格):
control.PermissionMode = ConfigPermissionMode.ViewOnly; // 只读查看
control.PermissionMode = ConfigPermissionMode.Full; // 所有权限(默认)
// 或:control.SetPermission(ConfigPermissionMode.ViewOnly);
8. 数据模型映射
EmbeddedDb 用 [SugarTable] 映射原 kstopaiot.db 的表,字段保持一致:
| 实体(C#) | 表名 | 关键字段 |
|---|---|---|
Device |
Iot_Device |
ID, DeviceName, Index, DriverId, AutoStart, CgUpload, EnforcePeriod, CmdPeriod, DeviceTypeEnum, ParentId, ResetEnforceOnChange, ResetEnforceScope |
DeviceVariable |
Iot_DeviceVariable |
ID, Name, Method, DeviceAddress, DataType, IsTrigger, EndianType, Expressions, IsUpload, ProtectType, Group, DeviceId |
DeviceConfig |
Iot_DeviceConfig |
ID, DeviceConfigName, DataSide, Value, EnumInfo, DeviceId(驱动 [ConfigParameter] 注入来源) |
Driver |
Iot_Driver |
ID, DriverName, FileName, AssembleName |
SystemConfig |
Iot_SystemConfig |
ID, GatewayName, ClientId, MqttIp, MqttPort, MqttUName, MqttUPwd, IoTPlatformType |
RpcLog |
Iot_RpcLog |
ID, RpcSide, StartTime, DeviceId, Method, Params, EndTime, IsSuccess, Description |
枚举(Enums.cs,与 devices 表原值一致):DeviceTypeEnum(Device=0/Group=1)、DataSide、IoTPlatformType(None=0…Kstopa=5)、DeviceStatusTypeEnum(Good/Bad/UnKnow)、RpcSide。
变量配置字段含义(方法/地址/表达式/触发/权限)与原 KstopaIOT 完全一致,详见根目录
README.md第 8 章「变量配置详解」。
9. 写入与反向控制
平台或本地向设备写值有两种方式:
// ① 不等待(fire-and-forget)
await gateway.PublishWriteAsync(new KstopaIOTRpcRequest
{
Device = "设备A",
Group = "TechParam", // 与变量分组一致,可空=默认分组
Data = new Dictionary<string, object> { ["温度上限"] = 80 }
});
// ② 等待回调(带超时)
var r = await gateway.SendWriteAsync(req, timeout: TimeSpan.FromSeconds(3));
Console.WriteLine($"成功={r.Success} 耗时={r.ElapsedTime.TotalMilliseconds:F1}ms 消息={r.ResultMessage}");
foreach (var w in r.Response?.WriteDetails)
Console.WriteLine($" {w.Variable} = {w.Value} ({w.Method})");
写入模式(write / writecache)无需指定——网关根据变量覆盖情况自动判定:全部
ReadFromCache可写变量都在入参中 → writecache,否则 → write。
10. 部署与目录约定
运行期目录
你的程序输出目录/
├─ data/
│ └─ kstopaiot.db ← 种子库(NuGet 包自动带,或用 KstopaIOT 现成的 db)
├─ drivers/
│ └─ net8.0/
│ ├─ Kstopa.PLC.SiemensS7.dll ← 驱动 DLL(需自行拷贝,见下)
│ ├─ Kstopa.PLC.ModBus.dll
│ └─ …(HslCommunication 等驱动依赖也放这里)
└─ KstopaIOT.Embedded.dll
驱动 DLL 从哪来
KstopaIOT.Embedded 本身不含具体驱动实现(只引用 PluginInterface 契约)。驱动 DLL 来自原 KstopaIOT 构建产物 KstopaIOT/bin/{Configuration}/net8.0/drivers/net8.0/。
- Demo 项目用
CopyDrivers编译后目标自动拷贝:..\KstopaIOT\bin\$(Configuration)\net8.0\drivers\net8.0\**\*.*→ 输出drivers/net8.0/。 - 你自己集成时:把需要的驱动 DLL(及它们的依赖,如
HslCommunication.dll)拷到输出drivers/net8.0/,程序启动即自动扫描加载。
NuGet 打包说明(给包作者)
.csproj已把kstopaiot.db作为 content 打包进data/,安装后落到消费端data/。build/KstopaIOT.Embedded.targets在消费端编译后,把包内content/data/kstopaiot.db拷贝到输出data/(与KstopaIOT.Driver.*包一致的 Copy 任务方式;PackageReference 模式下CopyToOutputDirectory不可靠,故用 Copy 任务)。drivers/net8.0/由各KstopaIOT.Driver.*包自行拷贝。- 各
KstopaIOT.Driver.*包应通过自己的build/*.targets把驱动 DLL 拷贝到消费端drivers/net8.0/。
11. 常见问题
Q:ConfigControl 里变量值不刷新?
A:必须 AttachEngine(engine) 且引擎已 StartAsync()。用 EmbeddedGatewayHost 时在 StartAsync() 之后调用 AttachConfigControl(control) 即可(宿主已保证引擎就绪),变量网格即开始显示实时值。
Q:不连 IoTKstopa 平台能用吗?
A:能。直接用 GatewayEngine(§5 方式 C)即可进程内采集与读写,完全不依赖外部传输 / 平台。
Q:怎么拿到「设备 / 分组 / 变量」的配置清单(做级联下拉 / 全量校验 / 报表)?
A:用配置目录 host.Catalog(IKstopaIOTCatalog,见 §6.9):单层用 GetDevices() / GetGroups(设备) / GetVariables(设备, 分组);
一次拿三层嵌套用 GetCascade()(设备 → 分组 → 变量);要一行一变量的扁平表用 QueryVariables();
把运行时消息关联到配置项用 ResolveMessage(msg) / ResolveVariable(设备, 变量名, 分组)。
它是只读、纯配置的,不含任何实时值(在线状态 / 当前值仍走 IKstopaIOTMessageHandler)。
不想启动引擎也能用:KstopaIOTCatalog.Open(dbPath)。
Q:配置界面刚保存了变量,为什么 Catalog 里查不到?
A:目录快照默认 5s 惰性重建。保存后立即调用 host.RefreshCatalog()(或 Catalog.Refresh())即可马上看到;
内容没变时 Refresh() 会复用旧快照、不触发 SnapshotChanged。
Q:WinForms 里 ElementHost 托管的 ConfigControl 标签页头不显示?
A:这是 ElementHost 托管含 TabControl 的 WPF 控件时的已知互操作问题——标签头条高度在 WinForms→WPF 布局传递中未被正确计入。Demo 用 TableLayoutPanel(状态栏固定高 + 内容区填充)包住 ElementHost 再放进 TabPage;若仍偶发裁剪,在窗口 Shown 时强制 WPF 重排:_config.Dispatcher.InvokeAsync(() => { _config.InvalidateArrange(); _config.UpdateLayout(); });。
Q:WinForms 编译报 CS0079 / CS0234?
A:CS0079——事件不能在对象初始化器里用 Click = handler 赋值,必须 new Button{...} 后 .Click += Handler。CS0234——Padding 属于 System.Windows.Forms(不是 System.Drawing),写全限定名会找不到类型。
Q:监控卡片的「心跳」为什么不动?
A:设计为「设备进入在线态的连接事件计数」(HeartbeatCount),只在设备代理 SetDeviceConnected(true) 边沿 +1(首连/重连成功)。设备稳定在线后该事件只发一次,故心跳停在 1——这是预期行为,不代表采集停了(遥测/事件计数仍在涨)。若想要一个「真在跳」的存活指示,可让引擎在每次收到遥测时调 SetDeviceConnected(true)(心跳≈采样数),或加周期保活 ping。
Q:驱动加载失败 / 下拉识别不到驱动?
A:先确认 drivers/net8.0/ 下有对应 DLL 及其依赖(如 HslCommunication)。若 DLL 明明在、但「注册驱动」下拉为空,通常是类型身份分裂:DriverLoader 现在用自定义 AssemblyLoadContext(优先复用主程序已加载的 KstopaIOT.Interface),已从根上解决;若仍复现,请用 DebugView / 调试器看 DriverLoader 的 Debug.WriteLine 输出(加载失败的 DLL 会逐条打印原因)。
Q:Channel 满了会怎样?
A:Event/Trigger 用 UnboundedChannel,永不丢;Telemetry/Connect/Response 用 BoundedChannel,满时 TryWrite 返回 false、对应 *Dropped 计数递增,可用 GetDroppedStats() 监控。
Q:设备断线后采集线程怎么办?
A:引擎按 IsConnected 与周期驱动重连(对齐原 DeviceThread 行为);北向侧自动重连,设备重新上线后消息继续投递。
Q:能动态增删设备吗?
A:能。配置界面保存后 GatewayEngine 通过 EnsureDeviceRunningAsync / RestartDeviceAsync 让运行时状态与 DB 一致;北向代理按设备消息自动 GetOrCreate,下线调 RemoveDeviceAsync 释放。
Q:关闭程序时驱动 CloseAsync() 会抛异常吗?
A:不会。优雅停机链路(EmbeddedGatewayHost.StopAsync → GatewayEngine.StopAsync → DeviceWorker.StopAsync)对 _driver.CloseAsync() 都做了 try/catch 吞掉并记录日志,所以正常关闭程序不会弹出异常。区别在 _driver.CloseAsync() 的两种调用场景:
- 关闭程序 / 摘除设备:走
StopAsync/StopDeviceAsync,异常被吞(仅日志)。 - 手动断开单设备(
GatewayEngine.DisconnectAsync(deviceName)):该 API 同样已加try/catch保护,异常时返回false并记录日志,不会冒泡成未处理异常。
若你在某条路径上仍看到「异常提示」,请确认是否是驱动自身在故障态(如设备已硬件断连)下
CloseAsync抛出的底层异常——此时应为日志中的断开设备异常/停止 X 异常,属预期告警,不影响停机。
Q:运行时设备连接/读写出错会怎样?
A:设备层异常被采集循环捕获并触发重连(指数退避 + 抖动),不会终止进程;网关级错误经 IKstopaIOTMessageHandler.HandleErrorAsync 交付给调用方,由你的 Handler 决定如何呈现(日志 / 提示)。
12. 许可
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0-windows7.0 is compatible. net9.0-windows was computed. net10.0-windows was computed. |
-
net8.0-windows7.0
- DotNetCore.NPOI (>= 1.2.3)
- DynamicExpresso.Core (>= 2.19.2)
- HslCommunication (>= 12.9.1)
- KstopaIOT.Interface (>= 1.1.0-beta)
- KstopaIOT.License.Core (>= 1.1.0-beta)
- Microsoft.Extensions.Logging (>= 8.0.1)
- Microsoft.Extensions.Logging.Abstractions (>= 8.0.2)
- Microsoft.Extensions.Logging.Console (>= 8.0.1)
- Newtonsoft.Json (>= 13.0.4)
- SqlSugarCore (>= 5.1.4.210)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.1.0-beta | 73 | 9/11/2026 |
| 1.0.10-beta | 67 | 9/11/2026 |
| 1.0.9-beta7 | 77 | 9/10/2026 |
| 1.0.9-beta6 | 69 | 9/10/2026 |
| 1.0.9-beta5 | 77 | 9/10/2026 |
| 1.0.9-beta4 | 70 | 9/8/2026 |
| 1.0.9-beta3 | 78 | 9/8/2026 |
| 1.0.9-beta2 | 76 | 9/8/2026 |
| 1.0.9-beta | 78 | 9/2/2026 |
| 1.0.8-beta | 61 | 9/1/2026 |
| 1.0.7-beta | 67 | 9/1/2026 |
| 1.0.6-beta | 74 | 9/1/2026 |
| 1.0.5-beta | 79 | 8/20/2026 |
| 1.0.4-beta | 72 | 8/19/2026 |
| 1.0.2-beta | 80 | 8/19/2026 |
| 1.0.1-beta | 73 | 8/19/2026 |
| 1.0.0-beta | 88 | 8/17/2026 |