KstopaIOT.Embedded 1.1.0-beta

This is a prerelease version of KstopaIOT.Embedded.
dotnet add package KstopaIOT.Embedded --version 1.1.0-beta
                    
NuGet\Install-Package KstopaIOT.Embedded -Version 1.1.0-beta
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="KstopaIOT.Embedded" Version="1.1.0-beta" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="KstopaIOT.Embedded" Version="1.1.0-beta" />
                    
Directory.Packages.props
<PackageReference Include="KstopaIOT.Embedded" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add KstopaIOT.Embedded --version 1.1.0-beta
                    
#r "nuget: KstopaIOT.Embedded, 1.1.0-beta"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package KstopaIOT.Embedded@1.1.0-beta
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=KstopaIOT.Embedded&version=1.1.0-beta&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=KstopaIOT.Embedded&version=1.1.0-beta&prerelease
                    
Install as a Cake Tool

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:运行与验证
  1. 先把原 KstopaIOT 构建一次(产出 KstopaIOT/bin/Debug/net8.0/drivers/net8.0/*.dll),供 CopyDrivers 目标拷贝。
  2. 将 KstopaIOT.Embedded.Wpf.Demo 设为启动项目 → F5。
  3. 顶部状态栏「未连接」变「已连接」,① 监控配置 Tab 的变量列表开始显示实时值。
  4. ② 实时监视随遥测涨「采样」计数;③ 写入测试可下发/强制刷新;④ 事件日志随勾选滚动;⑤ 网关监控显示吞吐曲线与设备卡。

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("温度");

关联口径有三条必须知道的规则(都与引擎对齐):

  1. 严格匹配:设备名 / 变量名 / 分组键都是序数、大小写敏感比较(引擎如此)。 分组键会做 Trim 归一(配置侧保存时已 Trim)。未命中返回 null,不猜。
  2. group = null 取首个候选:等价引擎 FindDeviceVariable 的 group == null ? FirstOrDefault(), 而不是报「歧义」。
  3. 空分组的上下线消息: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 为空或指向不存在的设备 —— 这类变量引擎也不会加载)。

设备类型口径:DeviceTypeEnum 0 = 真实设备(可采集,引擎为其建采集线程)、1 = 设备组(仅归类,无驱动、不采集)。 目录里的 Kind 即按此归一(Device / DeviceGroup);GetDevices(onlyDevices: true) 只回真实设备。

6.10 变量执行顺序(= 配置界面展示顺序)

变量的采集 / 发布顺序分三层,ConfigControl 变量网格与目录接口的展示顺序与之完全一致:

  1. 组顺序:按组内最小 Index 升序(左侧设备树的分组节点同序);
  2. 组内先后:先读触发变量、再读非触发变量(触发是否满足决定本组普通变量是否采集);
  3. 组内同级:按 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 日志 —— 平台反向控制调用的记录与结果。

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 Compatible and additional computed target framework versions.
.NET net8.0-windows7.0 is compatible.  net9.0-windows was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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