6daf008bb2a9d81f838e4a2c87a7964c2c78c550..e57938fbda9bff702004038f76d009422968bff5
7 天以前 alielie
提交代码
e57938 对比 | 目录
1个文件已修改
46个文件已添加
5421 ■■■■■ 已修改文件
.gitignore 206 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
DeviceService.Core/Api/ICanDriver.cs 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
DeviceService.Core/Api/IModbusDriver.cs 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
DeviceService.Core/Api/IPLCDriver.cs 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
DeviceService.Core/Api/IServiceLogger.cs 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
DeviceService.Core/DeviceService.Core.csproj 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
DeviceService.Core/DriverDesc.cs 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
DeviceService.Core/Entity/MessageData.cs 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
DeviceService.Core/IDevice.cs 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
DeviceService.Core/IDeviceManager.cs 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
DeviceService.Core/Properties/AssemblyInfo.cs 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
DeviceService.sln 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
DeviceService/AddDeviceWindow.xaml 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
DeviceService/AddDeviceWindow.xaml.cs 97 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
DeviceService/App.xaml 268 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
DeviceService/App.xaml.cs 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
DeviceService/AssemblyInfo.cs 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
DeviceService/Config/log4net.config 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
DeviceService/DebugWindow.xaml 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
DeviceService/DebugWindow.xaml.cs 156 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
DeviceService/DeviceConfigControlFactory.cs 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
DeviceService/DeviceControl/CanDeviceConfigControl.xaml 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
DeviceService/DeviceControl/CanDeviceConfigControl.xaml.cs 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
DeviceService/DeviceManager/CanDevice.cs 178 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
DeviceService/DeviceManager/DeviceDatabase.cs 328 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
DeviceService/DeviceManager/DeviceDesc.cs 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
DeviceService/DeviceManager/DeviceHelper.cs 168 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
DeviceService/DeviceManager/DeviceManager.cs 126 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
DeviceService/DeviceManager/PLCDevice.cs 138 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
DeviceService/DeviceService.csproj 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
DeviceService/DriverManager/DriverDatabase.cs 257 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
DeviceService/DriverManager/DriverDispatchProxy.cs 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
DeviceService/DriverManager/DriverInstance.cs 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
DeviceService/DriverManager/DriverLoader.cs 784 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
DeviceService/DriverManager/DriverManager.cs 411 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
DeviceService/Entity/NotifyBase.cs 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
DeviceService/LogWindow.xaml 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
DeviceService/LogWindow.xaml.cs 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
DeviceService/Logging/GlobalLogger.cs 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
DeviceService/Logging/MemoryAppender.cs 108 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
DeviceService/MainWindow.xaml 181 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
DeviceService/MainWindow.xaml.cs 218 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
DeviceService/ServiceManager/IClient.cs 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
DeviceService/ServiceManager/IService.cs 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
DeviceService/ServiceManager/NamePipe/NamePipeClient.cs 180 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
DeviceService/ServiceManager/NamePipe/NamePipeService.cs 424 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
DeviceService/ServiceManager/ServiceManager.cs 130 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.gitignore
@@ -1,23 +1,191 @@
# Object files
*.o
*.ko
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# User-specific files
packages/
**/*.vsidx
*.suo
*.user
*.sln.docstates
*.csproj.user
# Build results
.vs/
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
build/
bld/
[Bb]in/
[Oo]bj/
# Roslyn cache directories
*.ide/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
#NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.elf
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Libraries
*.lib
*.a
# Chutzpah Test files
_Chutzpah*
# Shared objects (inc. Windows DLLs)
*.dll
*.so
*.so.*
*.dylib
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opensdf
*.sdf
*.cachefile
# Executables
*.exe
*.out
*.app
*.i*86
*.x86_64
*.hex
# Visual Studio profiler
*.psess
*.vsp
*.vspx
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding addin-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# NCrunch
_NCrunch_*
.*crunch*.local.xml
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
## TODO: Comment the next line if you want to checkin your
## web deploy settings but do note that will include unencrypted
## passwords
*.pubxml
# NuGet Packages
packages/*
*.nupkg
## TODO: If the tool you use requires repositories.config
## uncomment the next line
#!packages/repositories.config
# Enable "build/" folder in the NuGet Packages folder since
# NuGet packages use it for MSBuild targets.
# This line needs to be after the ignore of the build folder
# (and the packages folder if the line above has been uncommented)
!packages/build/
# Windows Azure Build Output
csx/
*.build.csdef
# Windows Store app package directory
AppPackages/
# Others
sql/
*.Cache
ClientBin/
[Ss]tyle[Cc]op.*
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.pfx
*.publishsettings
node_modules/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
# SQL Server files
*.mdf
*.ldf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
# Microsoft Fakes
FakesAssemblies/
DeviceService.Core/Api/ICanDriver.cs
New file
@@ -0,0 +1,11 @@
namespace DeviceService.Core
{
    public interface ICanDriver
    {
        bool Open(out string msg);
        bool Close(out string msg);
        bool Send(string data, out string msg);
        bool Read(out string data, out string msg);
    }
}
DeviceService.Core/Api/IModbusDriver.cs
New file
@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DeviceService.Core.Api
{
    public interface IModbusDriver
    {
        bool Open(out string msg);
        bool Close(out string msg);
        bool ReadBytes(string address, int length, out byte[] data, out string msg);
        bool WriteBytes(string address, byte[] data, out string msg);
    }
}
DeviceService.Core/Api/IPLCDriver.cs
New file
@@ -0,0 +1,11 @@
namespace DeviceService.Core.Api
{
    public interface IPLCDriver
    {
        bool Open(out string msg);
        bool Close(out string msg);
        bool ReadBytes(string address, int length, out byte[] data, out string msg);
        bool WriteBytes(string address, byte[] data, out string msg);
    }
}
DeviceService.Core/Api/IServiceLogger.cs
New file
@@ -0,0 +1,12 @@
using System;
namespace DeviceService.Core.Api
{
    public interface IServiceLogger
    {
        void Debug(string source, string message);
        void Info(string source, string message);
        void Warn(string source, string message, Exception ex = null);
        void Error(string source, string message, Exception ex = null);
    }
}
DeviceService.Core/DeviceService.Core.csproj
New file
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
  <PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
    <ProjectGuid>{0ABE4877-BDEB-4C95-9C85-1ADDE3B641C4}</ProjectGuid>
    <OutputType>Library</OutputType>
    <AppDesignerFolder>Properties</AppDesignerFolder>
    <RootNamespace>DeviceService.Core</RootNamespace>
    <AssemblyName>DeviceService.Core</AssemblyName>
    <TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
    <FileAlignment>512</FileAlignment>
    <Deterministic>true</Deterministic>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <DebugSymbols>true</DebugSymbols>
    <DebugType>full</DebugType>
    <Optimize>false</Optimize>
    <OutputPath>bin\Debug\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
    <DebugType>pdbonly</DebugType>
    <Optimize>true</Optimize>
    <OutputPath>bin\Release\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <ItemGroup>
    <Reference Include="System" />
    <Reference Include="System.Core" />
    <Reference Include="System.Xml.Linq" />
    <Reference Include="System.Data.DataSetExtensions" />
    <Reference Include="Microsoft.CSharp" />
    <Reference Include="System.Data" />
    <Reference Include="System.Net.Http" />
    <Reference Include="System.Xml" />
  </ItemGroup>
  <ItemGroup>
    <Compile Include="Api\IModbusDriver.cs" />
    <Compile Include="Api\IPLCDriver.cs" />
    <Compile Include="Api\IServiceLogger.cs" />
    <Compile Include="DriverDesc.cs" />
    <Compile Include="Api\ICanDriver.cs" />
    <Compile Include="Entity\MessageData.cs" />
    <Compile Include="IDevice.cs" />
    <Compile Include="IDeviceManager.cs" />
    <Compile Include="Properties\AssemblyInfo.cs" />
  </ItemGroup>
  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
DeviceService.Core/DriverDesc.cs
New file
@@ -0,0 +1,22 @@
using System;
using System.Runtime.InteropServices;
namespace DeviceService.Core
{
    [AttributeUsage(AttributeTargets.Class, Inherited = false)]
    [ComVisible(true)]
    public class DriverDescAttribute : Attribute
    {
        public string Type { get; private set; }
        public string Brand { get; private set; }
        public Type ClassType { get; private set; }
        public DriverDescAttribute(string Type, string Brand, Type ClassType = null)
        {
            this.Type = Type;
            this.Brand = Brand;
            this.ClassType = ClassType;
        }
    }
}
DeviceService.Core/Entity/MessageData.cs
New file
@@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DeviceService.Core
{
    public class RequestData
    {
        public string DeviceId { get; set; }
        public string Opt { get; set; }
        public string Parameters { get; set; }
    }
    public class ResponseData
    {
        public bool Success { get; set; }
        public string Message { get; set; }
        public string Result { get; set; }
    }
}
DeviceService.Core/IDevice.cs
New file
@@ -0,0 +1,46 @@
using System;
namespace DeviceService.Core
{
    /// <summary>
    /// 设备状态
    /// </summary>
    public enum DeviceStatus
    {
        Ready = 0,
        Idle = 1,
        Busy = 2,
        Error = 3,
    }
    /// <summary>
    /// 设备访问类型
    /// </summary>
    public enum AccessType
    {
        Share = 0,
        Exclusive = 1
    }
    public interface IDevice
    {
        /// <summary>数据库自增主键(Config\Devices.db 的 Device.Id),0 表示尚未入库。</summary>
        long Id { get; }
        string DeviceId { get; }
        string Type { get; }
        string Brand { get; }
        string ConnectStr { get; }
        DeviceStatus Status { get; }
        AccessType AccessType { get; }
        bool Init(out string msg);
        bool Open(out string msg);
        bool Close(out string msg);
        ResponseData HandleRequest(RequestData req);
    }
}
DeviceService.Core/IDeviceManager.cs
New file
@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DeviceService.Device
{
    public interface IDeviceManager
    {
        bool RequestDevice(string deviceId, out string msg);
    }
}
DeviceService.Core/Properties/AssemblyInfo.cs
New file
@@ -0,0 +1,33 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// 有关程序集的一般信息由以下
// 控制。更改这些特性值可修改
// 与程序集关联的信息。
[assembly: AssemblyTitle("DeviceService.Core")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("DeviceService.Core")]
[assembly: AssemblyCopyright("Copyright ©  2026")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// 将 ComVisible 设置为 false 会使此程序集中的类型
//对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型
//请将此类型的 ComVisible 特性设置为 true。
[assembly: ComVisible(false)]
// 如果此项目向 COM 公开,则下列 GUID 用于类型库的 ID
[assembly: Guid("0abe4877-bdeb-4c95-9c85-1adde3b641c4")]
// 程序集的版本信息由下列四个值组成:
//
//      主版本
//      次版本
//      生成号
//      修订号
//
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
DeviceService.sln
New file
@@ -0,0 +1,35 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.13.35828.75 d17.13
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeviceService", "DeviceService\DeviceService.csproj", "{DB628943-4DFC-491E-BB66-EC17B95F22A7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeviceService.Core", "DeviceService.Core\DeviceService.Core.csproj", "{0ABE4877-BDEB-4C95-9C85-1ADDE3B641C4}"
EndProject
Global
    GlobalSection(SolutionConfigurationPlatforms) = preSolution
        Debug|Any CPU = Debug|Any CPU
        Release|Any CPU = Release|Any CPU
    EndGlobalSection
    GlobalSection(ProjectConfigurationPlatforms) = postSolution
        {DB628943-4DFC-491E-BB66-EC17B95F22A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
        {DB628943-4DFC-491E-BB66-EC17B95F22A7}.Debug|Any CPU.Build.0 = Debug|Any CPU
        {DB628943-4DFC-491E-BB66-EC17B95F22A7}.Release|Any CPU.ActiveCfg = Release|Any CPU
        {DB628943-4DFC-491E-BB66-EC17B95F22A7}.Release|Any CPU.Build.0 = Release|Any CPU
        {0ABE4877-BDEB-4C95-9C85-1ADDE3B641C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
        {0ABE4877-BDEB-4C95-9C85-1ADDE3B641C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
        {0ABE4877-BDEB-4C95-9C85-1ADDE3B641C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
        {0ABE4877-BDEB-4C95-9C85-1ADDE3B641C4}.Release|Any CPU.Build.0 = Release|Any CPU
        {312E217E-4406-41D8-A1FF-DC7BD71C5693}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
        {312E217E-4406-41D8-A1FF-DC7BD71C5693}.Debug|Any CPU.Build.0 = Debug|Any CPU
        {312E217E-4406-41D8-A1FF-DC7BD71C5693}.Release|Any CPU.ActiveCfg = Release|Any CPU
        {312E217E-4406-41D8-A1FF-DC7BD71C5693}.Release|Any CPU.Build.0 = Release|Any CPU
    EndGlobalSection
    GlobalSection(SolutionProperties) = preSolution
        HideSolutionNode = FALSE
    EndGlobalSection
    GlobalSection(ExtensibilityGlobals) = postSolution
        SolutionGuid = {C72103FF-2DFE-42DF-9C64-2DD6A2ACB4D8}
    EndGlobalSection
EndGlobal
DeviceService/AddDeviceWindow.xaml
New file
@@ -0,0 +1,56 @@
<Window x:Class="DeviceService.AddDeviceWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        Title="添加设备"
        Width="480" Height="440"
        MinWidth="420" MinHeight="360"
        WindowStartupLocation="CenterOwner"
        ResizeMode="CanResize"
        Background="{StaticResource PageBackgroundBrush}">
    <Grid Margin="12">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <!-- 设备类型选择 -->
        <Border Grid.Row="0" Style="{StaticResource CardBorder}" Padding="10">
            <StackPanel>
                <TextBlock Text="设备类型"
                           Style="{StaticResource TitleText}"
                           Margin="2,0,0,8" />
                <ComboBox x:Name="TypeCombo"
                          Padding="8,6"
                          SelectionChanged="TypeCombo_SelectionChanged" />
            </StackPanel>
        </Border>
        <!-- 选型后加载的配置控件 -->
        <Border Grid.Row="1" Style="{StaticResource CardBorder}" Margin="0,10,0,0">
            <ContentControl x:Name="ControlHost" Margin="4" Focusable="False" />
        </Border>
        <!-- 底部按钮 -->
        <StackPanel Grid.Row="2"
                    Orientation="Horizontal"
                    HorizontalAlignment="Right"
                    Margin="0,10,0,0">
            <Button x:Name="OkButton"
                    Style="{StaticResource PrimaryButton}"
                    MinWidth="88"
                    Content="确定"
                    IsEnabled="False"
                    Click="OkButton_Click" />
            <Button x:Name="CancelButton"
                    Style="{StaticResource SecondaryButton}"
                    MinWidth="88"
                    Margin="8,0,0,0"
                    Content="取消"
                    Click="CancelButton_Click" />
        </StackPanel>
    </Grid>
</Window>
DeviceService/AddDeviceWindow.xaml.cs
New file
@@ -0,0 +1,97 @@
using DeviceService.Core;
using DeviceService.Device;
using System.Windows;
using System.Windows.Controls;
namespace DeviceService;
/// <summary>
/// 添加设备弹窗:从 AllDeviceType 中选择设备类型,
/// 选型后在下方加载该设备类型对应的配置 UserControl,确定后交回设备实例。
/// </summary>
public partial class AddDeviceWindow : Window
{
    private IDevice? _device;
    /// <summary>点击确定后返回待添加的设备实例;取消时为 null。</summary>
    public IDevice? Device { get; private set; }
    public AddDeviceWindow()
    {
        InitializeComponent();
        TypeCombo.ItemsSource = DeviceHelper.AllDeviceType;
    }
    private void TypeCombo_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        _device = null;
        ControlHost.Content = null;
        if (TypeCombo.SelectedItem is string type
            && DeviceHelper.TryCreateDevice(type, out IDevice? device)
            && device != null)
        {
            // 默认 Id:类型名-序号,可在配置控件中看到
            int seq = DeviceManager.Devices.Count(d => d.Type == type) + 1;
            SetDeviceId(device, $"{type}-{seq}");
            _device = device;
            UserControl? control = DeviceConfigControlFactory.Create(device);
            if (control == null)
            {
                ControlHost.Content = new TextBlock
                {
                    Text = $"未找到 {device.GetType().Name}ConfigControl,该设备类型暂无配置界面。",
                    Margin = new Thickness(8),
                    TextWrapping = TextWrapping.Wrap,
                    TextAlignment = TextAlignment.Center,
                    VerticalAlignment = VerticalAlignment.Center
                };
            }
            else
            {
                control.DataContext = device;
                ControlHost.Content = control;
            }
        }
        OkButton.IsEnabled = _device != null;
    }
    private void OkButton_Click(object sender, RoutedEventArgs e)
    {
        if (_device == null)
        {
            return;
        }
        string id = _device.DeviceId;
        if (string.IsNullOrWhiteSpace(id))
        {
            MessageBox.Show(this, "设备 ID 不能为空。", "添加设备",
                MessageBoxButton.OK, MessageBoxImage.Warning);
            return;
        }
        if (DeviceManager.Devices.Any(d => d.DeviceId == id))
        {
            MessageBox.Show(this, $"设备 ID \"{id}\" 已存在,请修改后重试。", "添加设备",
                MessageBoxButton.OK, MessageBoxImage.Warning);
            return;
        }
        Device = _device;
        DialogResult = true;
    }
    private void CancelButton_Click(object sender, RoutedEventArgs e)
    {
        DialogResult = false;
    }
    /// <summary>IDevice 接口只暴露 Id getter,实际实现类一般有 setter,通过反射赋值。</summary>
    private static void SetDeviceId(IDevice device, string id)
    {
        device.GetType().GetProperty(nameof(IDevice.DeviceId))?.SetValue(device, id);
    }
}
DeviceService/App.xaml
New file
@@ -0,0 +1,268 @@
<Application x:Class="DeviceService.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:DeviceService"
             StartupUri="MainWindow.xaml">
    <Application.Resources>
        <!-- ============ 调色板 ============ -->
        <SolidColorBrush x:Key="PageBackgroundBrush" Color="#F3F4F6" />
        <SolidColorBrush x:Key="CardBackgroundBrush" Color="#FFFFFF" />
        <SolidColorBrush x:Key="CardBorderBrush" Color="#E5E7EB" />
        <SolidColorBrush x:Key="AccentBrush" Color="#2563EB" />
        <SolidColorBrush x:Key="AccentHoverBrush" Color="#1D4ED8" />
        <SolidColorBrush x:Key="AccentPressedBrush" Color="#1E40AF" />
        <SolidColorBrush x:Key="AccentSoftBrush" Color="#DBEAFE" />
        <SolidColorBrush x:Key="AccentFaintBrush" Color="#EFF6FF" />
        <SolidColorBrush x:Key="TextPrimaryBrush" Color="#111827" />
        <SolidColorBrush x:Key="TextSecondaryBrush" Color="#6B7280" />
        <SolidColorBrush x:Key="InputBorderBrush" Color="#D1D5DB" />
        <SolidColorBrush x:Key="InputHoverBorderBrush" Color="#9CA3AF" />
        <SolidColorBrush x:Key="TerminalBackgroundBrush" Color="#1E293B" />
        <SolidColorBrush x:Key="TerminalForegroundBrush" Color="#E2E8F0" />
        <!-- ============ 卡片容器 ============ -->
        <Style x:Key="CardBorder" TargetType="Border">
            <Setter Property="Background" Value="{StaticResource CardBackgroundBrush}" />
            <Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
            <Setter Property="BorderThickness" Value="1" />
            <Setter Property="CornerRadius" Value="8" />
            <Setter Property="Padding" Value="8" />
        </Style>
        <Style x:Key="TitleText" TargetType="TextBlock">
            <Setter Property="FontSize" Value="14" />
            <Setter Property="FontWeight" Value="Bold" />
            <Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}" />
        </Style>
        <Style x:Key="SectionText" TargetType="TextBlock">
            <Setter Property="FontSize" Value="17" />
            <Setter Property="FontWeight" Value="Bold" />
            <Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}" />
        </Style>
        <!-- ============ 按钮 ============ -->
        <Style x:Key="PrimaryButton" TargetType="Button">
            <Setter Property="Background" Value="{StaticResource AccentBrush}" />
            <Setter Property="Foreground" Value="White" />
            <Setter Property="FontSize" Value="13" />
            <Setter Property="FontWeight" Value="SemiBold" />
            <Setter Property="Padding" Value="12,7" />
            <Setter Property="Cursor" Value="Hand" />
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="Button">
                        <Border x:Name="Bd"
                                Background="{TemplateBinding Background}"
                                CornerRadius="6"
                                Padding="{TemplateBinding Padding}">
                            <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
                        </Border>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsMouseOver" Value="True">
                                <Setter TargetName="Bd" Property="Background" Value="{StaticResource AccentHoverBrush}" />
                            </Trigger>
                            <Trigger Property="IsPressed" Value="True">
                                <Setter TargetName="Bd" Property="Background" Value="{StaticResource AccentPressedBrush}" />
                            </Trigger>
                            <Trigger Property="IsEnabled" Value="False">
                                <Setter TargetName="Bd" Property="Background" Value="#93C5FD" />
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
        <Style x:Key="SecondaryButton" TargetType="Button">
            <Setter Property="Background" Value="#FFFFFF" />
            <Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}" />
            <Setter Property="BorderBrush" Value="{StaticResource InputBorderBrush}" />
            <Setter Property="FontSize" Value="13" />
            <Setter Property="Padding" Value="12,6" />
            <Setter Property="Cursor" Value="Hand" />
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="Button">
                        <Border x:Name="Bd"
                                Background="{TemplateBinding Background}"
                                BorderBrush="{TemplateBinding BorderBrush}"
                                BorderThickness="1"
                                CornerRadius="6"
                                Padding="{TemplateBinding Padding}">
                            <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
                        </Border>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsMouseOver" Value="True">
                                <Setter TargetName="Bd" Property="Background" Value="{StaticResource AccentFaintBrush}" />
                                <Setter TargetName="Bd" Property="BorderBrush" Value="{StaticResource AccentBrush}" />
                            </Trigger>
                            <Trigger Property="IsPressed" Value="True">
                                <Setter TargetName="Bd" Property="Background" Value="{StaticResource AccentSoftBrush}" />
                            </Trigger>
                            <Trigger Property="IsEnabled" Value="False">
                                <Setter TargetName="Bd" Property="Opacity" Value="0.5" />
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
        <!-- ============ 输入框 ============ -->
        <Style x:Key="InputTextBox" TargetType="TextBox">
            <Setter Property="Background" Value="#FFFFFF" />
            <Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}" />
            <Setter Property="BorderBrush" Value="{StaticResource InputBorderBrush}" />
            <Setter Property="BorderThickness" Value="1" />
            <Setter Property="Padding" Value="10,6" />
            <Setter Property="VerticalContentAlignment" Value="Center" />
            <Setter Property="CaretBrush" Value="{StaticResource AccentBrush}" />
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="TextBox">
                        <Border x:Name="Bd"
                                Background="{TemplateBinding Background}"
                                BorderBrush="{TemplateBinding BorderBrush}"
                                BorderThickness="{TemplateBinding BorderThickness}"
                                CornerRadius="6">
                            <ScrollViewer x:Name="PART_ContentHost"
                                          Margin="{TemplateBinding Padding}"
                                          VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
                        </Border>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsMouseOver" Value="True">
                                <Setter TargetName="Bd" Property="BorderBrush" Value="{StaticResource InputHoverBorderBrush}" />
                            </Trigger>
                            <Trigger Property="IsKeyboardFocusWithin" Value="True">
                                <Setter TargetName="Bd" Property="BorderBrush" Value="{StaticResource AccentBrush}" />
                            </Trigger>
                            <Trigger Property="IsEnabled" Value="False">
                                <Setter TargetName="Bd" Property="Background" Value="#F9FAFB" />
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
        <!-- 终端风格日志框 -->
        <Style x:Key="TerminalTextBox" TargetType="TextBox" BasedOn="{StaticResource InputTextBox}">
            <Setter Property="Background" Value="{StaticResource TerminalBackgroundBrush}" />
            <Setter Property="Foreground" Value="{StaticResource TerminalForegroundBrush}" />
            <Setter Property="BorderBrush" Value="#334155" />
            <Setter Property="Padding" Value="10,8" />
            <Setter Property="FontFamily" Value="Consolas" />
            <Setter Property="FontSize" Value="12.5" />
            <Setter Property="VerticalContentAlignment" Value="Top" />
        </Style>
        <!-- ============ TreeViewItem ============ -->
        <Style x:Key="DeviceTreeViewItem" TargetType="TreeViewItem">
            <Setter Property="Background" Value="Transparent" />
            <Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}" />
            <Setter Property="Padding" Value="6,6" />
            <Setter Property="HorizontalContentAlignment" Value="Stretch" />
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="TreeViewItem">
                        <StackPanel>
                            <Border x:Name="HeaderBorder"
                                    Background="Transparent"
                                    CornerRadius="6"
                                    Padding="{TemplateBinding Padding}"
                                    Margin="0,1">
                                <Grid>
                                    <Grid.ColumnDefinitions>
                                        <ColumnDefinition Width="16" />
                                        <ColumnDefinition Width="*" />
                                    </Grid.ColumnDefinitions>
                                    <ToggleButton x:Name="Expander"
                                                  Focusable="False"
                                                  ClickMode="Press"
                                                  IsChecked="{Binding IsExpanded, RelativeSource={RelativeSource TemplatedParent}}">
                                        <ToggleButton.Template>
                                            <ControlTemplate TargetType="ToggleButton">
                                                <Border Background="Transparent">
                                                    <Path x:Name="Arrow"
                                                          Data="M0,0 L4,4 L0,8 Z"
                                                          Fill="{StaticResource TextSecondaryBrush}"
                                                          HorizontalAlignment="Center"
                                                          VerticalAlignment="Center"
                                                          RenderTransformOrigin="0.5,0.5">
                                                        <Path.RenderTransform>
                                                            <RotateTransform Angle="0" />
                                                        </Path.RenderTransform>
                                                    </Path>
                                                </Border>
                                                <ControlTemplate.Triggers>
                                                    <Trigger Property="IsChecked" Value="True">
                                                        <Setter TargetName="Arrow" Property="RenderTransform">
                                                            <Setter.Value>
                                                                <RotateTransform Angle="90" />
                                                            </Setter.Value>
                                                        </Setter>
                                                    </Trigger>
                                                    <Trigger Property="IsMouseOver" Value="True">
                                                        <Setter TargetName="Arrow" Property="Fill" Value="{StaticResource AccentBrush}" />
                                                    </Trigger>
                                                </ControlTemplate.Triggers>
                                            </ControlTemplate>
                                        </ToggleButton.Template>
                                    </ToggleButton>
                                    <ContentPresenter Grid.Column="1" ContentSource="Header" />
                                </Grid>
                            </Border>
                            <ItemsPresenter x:Name="ItemsHost" Margin="16,0,0,0" Visibility="Collapsed" />
                        </StackPanel>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsExpanded" Value="True">
                                <Setter TargetName="ItemsHost" Property="Visibility" Value="Visible" />
                            </Trigger>
                            <Trigger Property="HasItems" Value="False">
                                <Setter TargetName="Expander" Property="Visibility" Value="Hidden" />
                            </Trigger>
                            <Trigger SourceName="HeaderBorder" Property="IsMouseOver" Value="True">
                                <Setter TargetName="HeaderBorder" Property="Background" Value="{StaticResource AccentFaintBrush}" />
                            </Trigger>
                            <Trigger Property="IsSelected" Value="True">
                                <Setter TargetName="HeaderBorder" Property="Background" Value="{StaticResource AccentSoftBrush}" />
                                <Setter Property="Foreground" Value="{StaticResource AccentPressedBrush}" />
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
        <!-- ============ ListBox 项 ============ -->
        <Style x:Key="RowListBoxItem" TargetType="ListBoxItem">
            <Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}" />
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="ListBoxItem">
                        <Border x:Name="Bd"
                                Background="Transparent"
                                CornerRadius="6"
                                Padding="10,7"
                                Margin="0,1">
                            <ContentPresenter />
                        </Border>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsMouseOver" Value="True">
                                <Setter TargetName="Bd" Property="Background" Value="{StaticResource AccentFaintBrush}" />
                            </Trigger>
                            <Trigger Property="IsSelected" Value="True">
                                <Setter TargetName="Bd" Property="Background" Value="{StaticResource AccentSoftBrush}" />
                                <Setter Property="Foreground" Value="{StaticResource AccentPressedBrush}" />
                                <Setter Property="FontWeight" Value="SemiBold" />
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Application.Resources>
</Application>
DeviceService/App.xaml.cs
New file
@@ -0,0 +1,12 @@
using System.Windows;
namespace DeviceService;
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
    // log4net 由 AssemblyInfo.cs 中的 [assembly: XmlConfigurator(ConfigFile = "Config/log4net.config")]
    // 依据配置文件完成初始化(Console / RollingFile / Memory 三个 Appender 均在配置文件中定义)。
}
DeviceService/AssemblyInfo.cs
New file
@@ -0,0 +1,12 @@
using log4net.Config;
using System.Windows;
[assembly:ThemeInfo(
    ResourceDictionaryLocation.None,            //where theme specific resource dictionaries are located
                                                //(used if a resource is not found in the page,
                                                // or application resource dictionaries)
    ResourceDictionaryLocation.SourceAssembly   //where the generic resource dictionary is located
                                                //(used if a resource is not found in the page,
                                                // app, or any theme specific resource dictionaries)
)]
[assembly: XmlConfigurator(ConfigFile = "Config/log4net.config", Watch = true)]
DeviceService/Config/log4net.config
New file
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<log4net>
    <!-- 控制台日志配置 -->
    <appender name="Console" type="log4net.Appender.ConsoleAppender">
        <!-- 日志输出格式 -->
        <layout type="log4net.Layout.PatternLayout">
            <conversionPattern value="%5level [%thread] (%file:%line) - %message%newline" />
        </layout>
    </appender>
    <!-- 文件存储日志配置 -->
    <appender name="RollingFile" type="log4net.Appender.RollingFileAppender">
        <appendToFile value="true" />
        <rollingStyle value="Date" />
        <datePattern value="'log_'yyyyMMdd'.log'"/>
        <staticLogFileName value="false"/>
        <!-- 保存文件的名称 -->
        <file value="./Log/" />
        <!-- 文件的编码方式 -->
        <param name="Encoding" value="UTF-8"/>
        <!-- 每个文件的大小 -->
        <maximumFileSize value="100KB" />
        <!-- 保存文件数量 -->
        <maxSizeRollBackups value="2" />
        <!-- 日志输出格式 -->
        <layout type="log4net.Layout.PatternLayout">
            <conversionPattern value="%logger: [%date] [%thread] [%level] [%class:%L] - %message%newline" />
        </layout>
    </appender>
    <!-- 内存日志缓冲:只保留最近 1000 条,供日志窗口查看,不写文件 -->
    <appender name="Memory" type="DeviceService.Logging.MemoryAppender, DeviceService">
        <!-- 内存中保留的日志条数 -->
        <capacity value="1000" />
    </appender>
    <root>
        <level value="DEBUG" />
        <appender-ref ref="Console" />
        <appender-ref ref="RollingFile" />
        <appender-ref ref="Memory" />
    </root>
</log4net>
DeviceService/DebugWindow.xaml
New file
@@ -0,0 +1,72 @@
<Window x:Class="DeviceService.DebugWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:DeviceService"
        mc:Ignorable="d"
        Title="调试窗口" Height="470" Width="840"
        Background="{StaticResource PageBackgroundBrush}">
    <Grid Margin="12">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="200" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <!-- 左侧:已连接客户端 -->
        <Border Grid.Column="0" Style="{StaticResource CardBorder}" Margin="0,0,10,0">
            <DockPanel Margin="4">
                <TextBlock DockPanel.Dock="Top"
                           Style="{StaticResource TitleText}"
                           Text="已连接客户端"
                           Margin="4,2,0,10" />
                <ListBox x:Name="ClientList"
                         Background="Transparent"
                         BorderThickness="0"
                         ItemContainerStyle="{StaticResource RowListBoxItem}" />
            </DockPanel>
        </Border>
        <!-- 右侧:日志 + 发送区 -->
        <Grid Grid.Column="1">
            <Grid.RowDefinitions>
                <RowDefinition Height="*" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
            <Border Grid.Row="0" Style="{StaticResource CardBorder}" Padding="4">
                <TextBox x:Name="ReceiveBox"
                         IsReadOnly="True"
                         Background="Transparent"
                         BorderThickness="0"
                         VerticalScrollBarVisibility="Auto"
                         HorizontalScrollBarVisibility="Auto"
                         TextWrapping="NoWrap"
                         FontFamily="Consolas"
                         FontSize="12.5"
                         Foreground="{StaticResource TextPrimaryBrush}"
                         Padding="6" />
            </Border>
            <DockPanel Grid.Row="1" Margin="0,10,0,0">
                <Button x:Name="SendTestButton"
                        DockPanel.Dock="Right"
                        Style="{StaticResource SecondaryButton}"
                        MinWidth="110"
                        Margin="8,0,0,0"
                        Content="发送测试消息"
                        Click="SendTestButton_Click" />
                <Button x:Name="SendToClientButton"
                        DockPanel.Dock="Right"
                        Style="{StaticResource PrimaryButton}"
                        MinWidth="130"
                        Margin="8,0,0,0"
                        Content="发送到选中客户端"
                        Click="SendToClientButton_Click" />
                <TextBox x:Name="SendBox"
                         Style="{StaticResource InputTextBox}"
                         KeyDown="SendBox_KeyDown" />
            </DockPanel>
        </Grid>
    </Grid>
</Window>
DeviceService/DebugWindow.xaml.cs
New file
@@ -0,0 +1,156 @@
using DeviceService.Service;
using DeviceService.Service.NamePipe;
using System.Collections.ObjectModel;
using System.IO.Pipes;
using System.Text;
using System.Windows;
using System.Windows.Input;
namespace DeviceService;
/// <summary>
/// Interaction logic for DebugWindow.xaml
/// </summary>
public partial class DebugWindow : Window
{
    private const string PipeName = "DeviceService";
    private class ClientItem
    {
        public ClientItem(IClient client, string name)
        {
            Client = client;
            Name = name;
        }
        public IClient Client { get; }
        public string Name { get; }
        public override string ToString() => Name;
    }
    private NamePipeService? _pipeService;
    private readonly ObservableCollection<ClientItem> _clients = new ObservableCollection<ClientItem>();
    public DebugWindow()
    {
        InitializeComponent();
        ClientList.ItemsSource = _clients;
        Loaded += DebugWindow_Loaded;
        Closed += DebugWindow_Closed;
    }
    private void DebugWindow_Loaded(object sender, RoutedEventArgs e)
    {
    }
    private string FindClientName(IClient client)
    {
        ClientItem? item = _clients.FirstOrDefault(c => ReferenceEquals(c.Client, client));
        return item?.Name ?? "未知客户端";
    }
    private async void SendTestButton_Click(object sender, RoutedEventArgs e)
    {
        string text = SendBox.Text;
        if (string.IsNullOrEmpty(text))
        {
            return;
        }
        SendTestButton.IsEnabled = false;
        try
        {
            string error = await Task.Run(() => SendViaPipe(text + "\r\n"));
            AppendLine(string.IsNullOrEmpty(error)
                ? $"[测试] 已发送:{text}"
                : $"[测试] 发送失败:{error}");
        }
        finally
        {
            SendTestButton.IsEnabled = true;
        }
    }
    private async void SendToClientButton_Click(object sender, RoutedEventArgs e)
    {
        if (ClientList.SelectedItem is not ClientItem item)
        {
            AppendLine("[发送] 请先在左侧列表中选择一个客户端。");
            return;
        }
        string text = SendBox.Text;
        if (string.IsNullOrEmpty(text))
        {
            return;
        }
        string name = item.Name;
        IClient client = item.Client;
        SendToClientButton.IsEnabled = false;
        try
        {
            string error = await Task.Run(() => client.Send(text, out string err) ? string.Empty : err);
            AppendLine(string.IsNullOrEmpty(error)
                ? $"[发送→{name}] {text}"
                : $"[发送失败→{name}] {error}");
        }
        finally
        {
            SendToClientButton.IsEnabled = true;
        }
    }
    private void SendBox_KeyDown(object sender, KeyEventArgs e)
    {
        if (e.Key == Key.Enter)
        {
            // 已选中客户端则定向发送,否则走测试通道
            if (ClientList.SelectedItem is ClientItem)
            {
                SendToClientButton_Click(sender, e);
            }
            else
            {
                SendTestButton_Click(sender, e);
            }
        }
    }
    /// <summary>
    /// 以客户端身份连接本进程的管道,按行协议写入一条 UTF8 消息(以 \n 结尾)。
    /// 返回空字符串表示成功,否则为错误信息。
    /// </summary>
    private static string SendViaPipe(string text)
    {
        try
        {
            using var pipe = new NamedPipeClientStream(".", PipeName, PipeDirection.InOut);
            pipe.Connect(2000);
            byte[] payload = Encoding.UTF8.GetBytes(text + "\n");
            pipe.Write(payload, 0, payload.Length);
            pipe.Flush();
            return string.Empty;
        }
        catch (Exception ex)
        {
            return ex.Message;
        }
    }
    private void AppendLine(string text)
    {
        ReceiveBox.AppendText(text + Environment.NewLine);
        ReceiveBox.ScrollToEnd();
    }
    private void DebugWindow_Closed(object? sender, EventArgs e)
    {
    }
}
DeviceService/DeviceConfigControlFactory.cs
New file
@@ -0,0 +1,36 @@
using DeviceService.Core;
using System.Reflection;
using System.Windows.Controls;
namespace DeviceService
{
    /// <summary>
    /// 按命名规则创建设备配置控件:设备类名 CanDevice 对应 CanDeviceConfigControl。
    /// </summary>
    public static class DeviceConfigControlFactory
    {
        public static UserControl? Create(IDevice device)
        {
            if (device == null) throw new ArgumentNullException(nameof(device));
            string controlName = device.GetType().Name + "ConfigControl";
            try
            {
                Type? type = typeof(DeviceConfigControlFactory).Assembly.GetTypes()
                    .FirstOrDefault(t => t.Name == controlName
                                         && typeof(UserControl).IsAssignableFrom(t)
                                         && !t.IsAbstract);
                return type == null ? null : (UserControl?)Activator.CreateInstance(type);
            }
            catch (ReflectionTypeLoadException ex)
            {
                Type? type = ex.Types.FirstOrDefault(t => t?.Name == controlName
                                                          && typeof(UserControl).IsAssignableFrom(t)
                                                          && !t.IsAbstract);
                return type == null ? null : (UserControl?)Activator.CreateInstance(type);
            }
        }
    }
}
DeviceService/DeviceControl/CanDeviceConfigControl.xaml
New file
@@ -0,0 +1,52 @@
<UserControl x:Class="DeviceService.CanDeviceConfigControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:sys="clr-namespace:System;assembly=mscorlib"
             xmlns:dm="clr-namespace:DeviceService.Core;assembly=DeviceService.Core"
             mc:Ignorable="d"
             d:DesignHeight="220" d:DesignWidth="420">
    <UserControl.Resources>
        <ObjectDataProvider x:Key="AccessTypeValues" MethodName="GetValues" ObjectType="{x:Type sys:Enum}">
            <ObjectDataProvider.MethodParameters>
                <x:Type TypeName="dm:AccessType" />
            </ObjectDataProvider.MethodParameters>
        </ObjectDataProvider>
    </UserControl.Resources>
   <Grid VerticalAlignment="Top">
        <StackPanel>
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="80" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                </Grid.RowDefinitions>
                <TextBlock Grid.Row="0" Grid.Column="0" Text="ID" VerticalAlignment="Center" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,6" />
                <TextBox Grid.Row="0" Grid.Column="1" Style="{StaticResource InputTextBox}"
                         Text="{Binding DeviceId, UpdateSourceTrigger=PropertyChanged}" Margin="0,4" />
                <TextBlock Grid.Row="2" Grid.Column="0" Text="品牌" VerticalAlignment="Center" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,6" />
                <TextBox Grid.Row="2" Grid.Column="1" Style="{StaticResource InputTextBox}"
                         Text="{Binding Brand, UpdateSourceTrigger=PropertyChanged}" Margin="0,4" />
                <TextBlock Grid.Row="3" Grid.Column="0" Text="连接端点" VerticalAlignment="Center" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,6" />
                <TextBox Grid.Row="3" Grid.Column="1" Style="{StaticResource InputTextBox}"
                         Text="{Binding ConnectStr, UpdateSourceTrigger=PropertyChanged}" Margin="0,4" />
                <TextBlock Grid.Row="4" Grid.Column="0" Text="访问类型" VerticalAlignment="Center" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,6" />
                <ComboBox Grid.Row="4" Grid.Column="1" Margin="0,4"
                          Padding="8,6"
                          ItemsSource="{Binding Source={StaticResource AccessTypeValues}}"
                          SelectedItem="{Binding AccessType, Mode=TwoWay}" />
            </Grid>
        </StackPanel>
    </Grid>
</UserControl>
DeviceService/DeviceControl/CanDeviceConfigControl.xaml.cs
New file
@@ -0,0 +1,17 @@
using DeviceService.Device;
using System.Windows.Controls;
namespace DeviceService;
/// <summary>
/// CanDevice 的配置控件。命名规则:设备类型名 CanDevice + ConfigControl。
/// DataContext 由 MainWindow 注入为对应的 IDevice 实例。
/// </summary>
public partial class CanDeviceConfigControl : UserControl
{
    public CanDeviceConfigControl()
    {
        InitializeComponent();
        DataContext = new CanDevice();
    }
}
DeviceService/DeviceManager/CanDevice.cs
New file
@@ -0,0 +1,178 @@
using DeviceService.Core;
using DeviceService.Driver;
using DeviceService.Entity;
using System.Windows.Interop;
namespace DeviceService.Device
{
    [DeviceDesc("CAN")]
    public class CanDevice : NotifyBase, IDevice
    {
        private long _id;
        public long Id
        {
            get => _id;
            set
            {
                SetProperty(ref _id, value);
            }
        }
        private string _deviceId;
        public string DeviceId
        {
            get => _deviceId;
            set
            {
                SetProperty(ref _deviceId, value);
            }
        }
        private string _type = "CAN";
        public string Type
        {
            get => _type;
            private set
            {
                SetProperty(ref _type, value);
            }
        }
        private string _brand;
        public string Brand
        {
            get => _brand;
            set
            {
                SetProperty(ref _brand, value);
            }
        }
        private string _connectStr;
        public string ConnectStr
        {
            get => _connectStr;
            set
            {
                SetProperty(ref _connectStr, value);
            }
        }
        private DeviceStatus _status;
        public DeviceStatus Status
        {
            get => _status;
            set
            {
                SetProperty(ref _status, value);
            }
        }
        private AccessType _accessType;
        public AccessType AccessType
        {
            get => _accessType;
            set
            {
                SetProperty(ref _accessType, value);
            }
        }
        private ICanDriver driver;
        public bool Init(out string msg)
        {
            msg = string.Empty;
            try
            {
                driver = DriverManager.Instance.CreateDriverByTypeAndBrand<ICanDriver>(Type, Brand);
                return true;
            }
            catch (Exception ex)
            {
                msg = ex.Message;
                return false;
            }
        }
        public bool Open(out string msg)
        {
            if (driver == null)
            {
                msg = "设备尚未初始化或初始化失败。";
                return false;
            }
            var res = driver.Open(out msg);
            if (res)
            {
                Status = DeviceStatus.Busy;
            }
            return res;
        }
        public bool Close(out string msg)
        {
            if (driver == null)
            {
                msg = "设备尚未初始化或初始化失败。";
                return false;
            }
            var res = driver.Close(out msg);
            if (res)
            {
                Status = DeviceStatus.Idle;
            }
            return res;
        }
        public ResponseData HandleRequest(RequestData req)
        {
            try
            {
                if (req.DeviceId != DeviceId)
                {
                    return new ResponseData
                    {
                        Success = false,
                        Message = "设备ID不匹配"
                    };
                }
                if (string.Equals("open", req.Opt, StringComparison.OrdinalIgnoreCase))
                {
                    if (driver.Open(out string msg))
                    {
                        return new ResponseData
                        {
                            Success = true,
                            Message = msg
                        };
                    }
                    else
                    {
                        return new ResponseData
                        {
                            Success = false,
                            Message = msg
                        };
                    }
                }
                return new ResponseData
                {
                    Success = false,
                    Message = "无效的操作"
                };
            }
            catch (Exception ex)
            {
                return new ResponseData
                {
                    Success = false,
                    Message = ex.Message
                };
            }
        }
    }
}
DeviceService/DeviceManager/DeviceDatabase.cs
New file
@@ -0,0 +1,328 @@
using DeviceService.Core;
using DeviceService.Logging;
using System.Data.SQLite;
using System.IO;
namespace DeviceService.Device
{
    /// <summary>设备记录(Device 表的一行)。</summary>
    public sealed class DeviceRecord
    {
        /// <summary>自增主键,对应设备实例的 Id;0 表示尚未入库。</summary>
        public long Id { get; set; }
        /// <summary>设备业务标识(唯一),对应设备实例的 DeviceId。</summary>
        public string DeviceId { get; set; } = string.Empty;
        public string Type { get; set; } = string.Empty;
        public string Brand { get; set; } = string.Empty;
        public string ConnectStr { get; set; } = string.Empty;
        public AccessType AccessType { get; set; }
    }
    /// <summary>
    /// 设备信息 SQLite 持久化:数据库文件位于 应用根目录\Config\Devices.db。
    ///
    /// 表结构:
    /// - Device:Id 为自增主键,DeviceId 为唯一业务标识,Type 用于定位设备实现类
    ///
    /// 所有操作失败仅通过 log 上报,不会中断设备加载流程。
    /// </summary>
    public sealed class DeviceDatabase
    {
        private const string CreateTableSql = @"
CREATE TABLE IF NOT EXISTS Device (
    Id         INTEGER PRIMARY KEY AUTOINCREMENT,
    DeviceId   TEXT NOT NULL,
    Type       TEXT NOT NULL,
    Brand      TEXT,
    ConnectStr TEXT,
    AccessType INTEGER NOT NULL DEFAULT 0
);";
        private static readonly object _staticSync = new object();
        private static DeviceDatabase? _instance;
        public static DeviceDatabase Instance
        {
            get
            {
                lock (_staticSync)
                {
                    return _instance ??= new DeviceDatabase();
                }
            }
        }
        /// <summary>配置目录:应用根目录\Config。</summary>
        public static string ConfigDirectory =>
            Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Config");
        /// <summary>数据库文件路径。</summary>
        public static string DatabasePath =>
            Path.Combine(ConfigDirectory, "Devices.db");
        private readonly object _sync = new object();
        private bool _initialized;
        private DeviceDatabase()
        {
        }
        /// <summary>确保 Config 目录与库表结构存在。失败返回 false 并通过 log 上报。</summary>
        private bool EnsureReady()
        {
            if (_initialized)
            {
                return true;
            }
            try
            {
                if (!Directory.Exists(ConfigDirectory))
                {
                    Directory.CreateDirectory(ConfigDirectory);
                }
                using (var conn = Open())
                {
                    ExecuteNonQuery(conn, CreateTableSql, null);
                    // 旧库的结构是 DeviceId 主键,升级为 Id 自增主键
                    MigrateToAutoIncrementId(conn);
                    ExecuteNonQuery(conn,
                        "CREATE UNIQUE INDEX IF NOT EXISTS IX_Device_DeviceId ON Device(DeviceId);",
                        null);
                }
                _initialized = true;
                return true;
            }
            catch (Exception ex)
            {
                GlobalLogger.Logger.Error("DeviceManager", $"[设备库] 初始化失败({DatabasePath}): {ex.Message}");
                return false;
            }
        }
        private SQLiteConnection Open()
        {
            var conn = new SQLiteConnection($"Data Source={DatabasePath};Version=3;");
            conn.Open();
            return conn;
        }
        /// <summary>读取库中全部设备记录(按 Type、DeviceId 排序)。</summary>
        public IReadOnlyList<DeviceRecord> LoadAll()
        {
            var result = new List<DeviceRecord>();
            if (!EnsureReady())
            {
                return result;
            }
            try
            {
                lock (_sync)
                {
                    using (var conn = Open())
                    using (var cmd = conn.CreateCommand())
                    {
                        cmd.CommandText =
                            "SELECT * FROM Device ORDER BY Type, DeviceId";
                        using (var reader = cmd.ExecuteReader())
                        {
                            while (reader.Read())
                            {
                                result.Add(new DeviceRecord
                                {
                                    Id = GetInt64(reader, "Id"),
                                    DeviceId = GetString(reader, "DeviceId"),
                                    Type = GetString(reader, "Type"),
                                    Brand = GetString(reader, "Brand"),
                                    ConnectStr = GetString(reader, "ConnectStr"),
                                    AccessType = (AccessType)GetInt32(reader, "AccessType")
                                });
                            }
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                GlobalLogger.Logger.Error("DeviceManager", $"[设备库] 读取设备列表失败: {ex.Message}");
            }
            return result;
        }
        /// <summary>
        /// 写入(覆盖)一台设备的记录,返回其自增主键 Id;失败返回 0。
        /// Id 为 0 时新增,否则按 Id 更新。
        /// </summary>
        public long Save(DeviceRecord record, Action<string>? log = null)
        {
            if (record == null || string.IsNullOrWhiteSpace(record.DeviceId))
            {
                return 0;
            }
            if (!EnsureReady())
            {
                return 0;
            }
            try
            {
                lock (_sync)
                {
                    using (var conn = Open())
                    {
                        if (record.Id > 0)
                        {
                            int affected;
                            using (var cmd = conn.CreateCommand())
                            {
                                cmd.CommandText = @"
UPDATE Device SET DeviceId = @id, Type = @t, Brand = @b, ConnectStr = @c, AccessType = @a
WHERE Id = @pk";
                                cmd.Parameters.AddWithValue("@id", record.DeviceId);
                                cmd.Parameters.AddWithValue("@t", record.Type ?? string.Empty);
                                cmd.Parameters.AddWithValue("@b", record.Brand ?? string.Empty);
                                cmd.Parameters.AddWithValue("@c", record.ConnectStr ?? string.Empty);
                                cmd.Parameters.AddWithValue("@a", (int)record.AccessType);
                                cmd.Parameters.AddWithValue("@pk", record.Id);
                                affected = cmd.ExecuteNonQuery();
                            }
                            if (affected > 0)
                            {
                                return record.Id;
                            }
                        }
                        using (var cmd = conn.CreateCommand())
                        {
                            cmd.CommandText = @"
INSERT INTO Device(DeviceId, Type, Brand, ConnectStr, AccessType)
VALUES(@id, @t, @b, @c, @a)";
                            cmd.Parameters.AddWithValue("@id", record.DeviceId);
                            cmd.Parameters.AddWithValue("@t", record.Type ?? string.Empty);
                            cmd.Parameters.AddWithValue("@b", record.Brand ?? string.Empty);
                            cmd.Parameters.AddWithValue("@c", record.ConnectStr ?? string.Empty);
                            cmd.Parameters.AddWithValue("@a", (int)record.AccessType);
                            cmd.ExecuteNonQuery();
                        }
                        using (var cmd = conn.CreateCommand())
                        {
                            cmd.CommandText = "SELECT last_insert_rowid()";
                            return Convert.ToInt64(cmd.ExecuteScalar());
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                log?.Invoke($"[设备库] 保存 {record.DeviceId} 失败: {ex.Message}");
                return 0;
            }
        }
        /// <summary>删除一台设备的记录。</summary>
        public void Remove(string deviceId)
        {
            if (string.IsNullOrEmpty(deviceId) || !EnsureReady())
            {
                return;
            }
            try
            {
                lock (_sync)
                {
                    using (var conn = Open())
                    using (var cmd = conn.CreateCommand())
                    {
                        cmd.CommandText = "DELETE FROM Device WHERE DeviceId = @id";
                        cmd.Parameters.AddWithValue("@id", deviceId);
                        cmd.ExecuteNonQuery();
                    }
                }
            }
            catch (Exception ex)
            {
                GlobalLogger.Logger.Error("DeviceManager", $"[设备库] 移除 {deviceId} 失败: {ex.Message}");
            }
        }
        /// <summary>旧库(DeviceId 作主键)升级为 Id 自增主键。</summary>
        private static void MigrateToAutoIncrementId(SQLiteConnection conn)
        {
            var columns = new List<string>();
            using (var cmd = conn.CreateCommand())
            {
                cmd.CommandText = "PRAGMA table_info(Device)";
                using (var reader = cmd.ExecuteReader())
                {
                    while (reader.Read())
                    {
                        columns.Add(GetString(reader, "name"));
                    }
                }
            }
            if (columns.Count == 0 || columns.Contains("Id"))
            {
                return;
            }
            lock (_staticSync)
            {
                using (var tx = conn.BeginTransaction())
                {
                    ExecuteNonQuery(conn, "DROP TABLE IF EXISTS Device_old;", tx);
                    ExecuteNonQuery(conn, "ALTER TABLE Device RENAME TO Device_old;", tx);
                    ExecuteNonQuery(conn, CreateTableSql, tx);
                    ExecuteNonQuery(conn, @"
INSERT INTO Device(DeviceId, Type, Brand, ConnectStr, AccessType)
SELECT DeviceId, Type, Brand, ConnectStr, AccessType FROM Device_old;", tx);
                    ExecuteNonQuery(conn, "DROP TABLE Device_old;", tx);
                    tx.Commit();
                }
            }
        }
        private static void ExecuteNonQuery(SQLiteConnection conn, string sql, SQLiteTransaction? tx)
        {
            using (var cmd = conn.CreateCommand())
            {
                cmd.Transaction = tx;
                cmd.CommandText = sql;
                cmd.ExecuteNonQuery();
            }
        }
        private static string GetString(SQLiteDataReader reader, string column)
        {
            object value = reader[column];
            return value == DBNull.Value ? string.Empty : value.ToString() ?? string.Empty;
        }
        private static int GetInt32(SQLiteDataReader reader, string column)
        {
            object value = reader[column];
            return value == DBNull.Value ? 0 : Convert.ToInt32(value);
        }
        private static long GetInt64(SQLiteDataReader reader, string column)
        {
            object value = reader[column];
            return value == DBNull.Value ? 0L : Convert.ToInt64(value);
        }
    }
}
DeviceService/DeviceManager/DeviceDesc.cs
New file
@@ -0,0 +1,29 @@
using System;
using System.Runtime.InteropServices;
namespace DeviceService.Device
{
    [AttributeUsage(AttributeTargets.Class, Inherited = false)]
    [ComVisible(true)]
    public class DeviceDescAttribute : Attribute
    {
        public string Type { get; private set; }
        public DeviceDescAttribute(string Type)
        {
            this.Type = Type;
        }
    }
    [AttributeUsage(AttributeTargets.Property, Inherited = false)]
    [ComVisible(true)]
    public class DevicePropertyAttribute : Attribute
    {
        public string Name { get; private set; }
        public DevicePropertyAttribute(string Name)
        {
            this.Name = Name;
        }
    }
}
DeviceService/DeviceManager/DeviceHelper.cs
New file
@@ -0,0 +1,168 @@
using DeviceService.Core;
using DeviceService.Logging;
using System.Reflection;
namespace DeviceService.Device
{
    /// <summary>
    /// 设备信息持久化辅助:负责设备信息与 Config\Devices.db 之间的读写。
    /// DeviceManager 只负责服务正式启动后的设备管理,数据库读写一律经由本类。
    /// </summary>
    public static class DeviceHelper
    {
        private static readonly object _scanSync = new object();
        private static readonly Dictionary<string, Type> _deviceTypeMap = new Dictionary<string, Type>(StringComparer.Ordinal);
        /// <summary>
        /// 扫描到的设备类型列表(取自 DeviceDescAttribute.Type)。
        /// 程序启动后调用 <see cref="ScanDeviceTypes"/> 填充。
        /// </summary>
        public static List<string> AllDeviceType { get; } = new List<string>();
        public static List<IDevice> AllDevices { get; } = new List<IDevice>();
        /// <summary>
        /// 扫描本程序集中所有标注了 <see cref="DeviceDescAttribute"/> 且继承 <see cref="IDevice"/> 的类,
        /// 将注解中的 Type 存入 <see cref="AllDeviceType"/>,并记录 Type 到设备类的映射。
        /// </summary>
        public static void ScanDeviceTypes()
        {
            lock (_scanSync)
            {
                AllDeviceType.Clear();
                _deviceTypeMap.Clear();
                IEnumerable<Type?> types;
                try
                {
                    types = Assembly.GetExecutingAssembly().GetTypes();
                }
                catch (ReflectionTypeLoadException ex)
                {
                    types = ex.Types;
                }
                foreach (Type? t in types)
                {
                    if (t == null || !typeof(IDevice).IsAssignableFrom(t) || t.IsAbstract)
                    {
                        continue;
                    }
                    DeviceDescAttribute? attr = t.GetCustomAttribute<DeviceDescAttribute>(false);
                    if (attr == null || string.IsNullOrEmpty(attr.Type))
                    {
                        continue;
                    }
                    _deviceTypeMap[attr.Type] = t;
                    AllDeviceType.Add(attr.Type);
                }
            }
        }
        /// <summary>按注解 Type 创建对应的设备实例。</summary>
        public static bool TryCreateDevice(string type, out IDevice? device)
        {
            device = null;
            lock (_scanSync)
            {
                if (type == null || !_deviceTypeMap.TryGetValue(type, out Type? deviceType))
                {
                    return false;
                }
                device = (IDevice?)Activator.CreateInstance(deviceType);
                return device != null;
            }
        }
        /// <summary>
        /// 保存设备信息到 Config\Devices.db(仅持久化,不调用 Init、不影响 DeviceManager 的内存集合)。
        /// 返回是否保存成功;新增记录会回填自增 Id。
        /// </summary>
        public static bool SaveDevice(IDevice device)
        {
            if (device == null) throw new ArgumentNullException(nameof(device));
            // 写入 Config\Devices.db,下次启动时据此恢复设备列表;新增记录回填自增 Id
            long id = DeviceDatabase.Instance.Save(ToRecord(device));
            if (id > 0)
            {
                TrySet(device, nameof(IDevice.Id), id);
            }
            return id > 0;
        }
        /// <summary>
        /// 从 Config\Devices.db 读取全部设备并创建实例(不调用 Init、不加入任何集合)。
        /// 调用方按需加入 DeviceManager,服务正式启动后再逐一 <see cref="DeviceManager.Register"/>。
        /// </summary>
        public static IReadOnlyList<IDevice> LoadDevices()
        {
            var devices = new List<IDevice>();
            AllDevices.Clear();
            foreach (DeviceRecord record in DeviceDatabase.Instance.LoadAll())
            {
                if (string.IsNullOrWhiteSpace(record.DeviceId) || string.IsNullOrWhiteSpace(record.Type))
                {
                    continue;
                }
                if (!DeviceHelper.TryCreateDevice(record.Type, out IDevice? device) || device == null)
                {
                    GlobalLogger.Logger.Warn("DeviceHelper", $"[设备库] 未知设备类型 {record.Type},已跳过 {record.DeviceId}");
                    continue;
                }
                ApplyRecord(device, record);
                devices.Add(device);
                AllDevices.Add(device);
            }
            return devices;
        }
        /// <summary>从 Config\Devices.db 删除设备记录。</summary>
        public static void RemoveDevice(string deviceId)
        {
            DeviceDatabase.Instance.Remove(deviceId);
        }
        private static DeviceRecord ToRecord(IDevice device)
        {
            return new DeviceRecord
            {
                Id = device.Id,
                DeviceId = device.DeviceId ?? string.Empty,
                Type = device.Type ?? string.Empty,
                Brand = device.Brand ?? string.Empty,
                ConnectStr = device.ConnectStr ?? string.Empty,
                AccessType = device.AccessType
            };
        }
        /// <summary>IDevice 接口只暴露 getter,实现类一般有 setter,通过反射回填。</summary>
        private static void ApplyRecord(IDevice device, DeviceRecord record)
        {
            TrySet(device, nameof(IDevice.Id), record.Id);
            TrySet(device, nameof(IDevice.DeviceId), record.DeviceId);
            TrySet(device, nameof(IDevice.Brand), record.Brand);
            TrySet(device, nameof(IDevice.ConnectStr), record.ConnectStr);
            TrySet(device, nameof(IDevice.AccessType), record.AccessType);
        }
        private static void TrySet(object target, string propertyName, object value)
        {
            PropertyInfo? property = target.GetType().GetProperty(propertyName);
            if (property == null || !property.CanWrite || !property.PropertyType.IsInstanceOfType(value))
            {
                return;
            }
            property.SetValue(target, value);
        }
    }
}
DeviceService/DeviceManager/DeviceManager.cs
New file
@@ -0,0 +1,126 @@
using DeviceService.Core;
using DeviceService.Service;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Unicode;
namespace DeviceService.Device
{
    /// <summary>
    /// 服务正式启动后的设备管理:设备实例的内存集合、类型扫描与注册(Register)。
    /// 设备信息的数据库读写由 <see cref="DeviceHelper"/> 负责,本类不做任何持久化。
    /// </summary>
    public class DeviceManager
    {
        private readonly static object _sync = new object();
        private readonly static Dictionary<string, IDevice> _deviceCollection = new Dictionary<string, IDevice>();
        public static IReadOnlyList<IDevice> Devices
        {
            get
            {
                lock (_sync)
                {
                    return _deviceCollection.Values.ToList();
                }
            }
        }
        // ==================== 设备注册 ====================
        /// <summary>服务启动时注册设备(内部会调用 Init),无论初始化是否成功都会加入集合。</summary>
        public static bool Register(IDevice device, out string msg)
        {
            if (device == null) throw new ArgumentNullException(nameof(device));
            bool initOk = device.Init(out msg);
            if (initOk)
            {
                lock (_sync)
                {
                    _deviceCollection[device.DeviceId] = device;
                }
            }
            return initOk;
        }
        public static void UnRegisterAll()
        {
            lock (_sync)
            {
                _deviceCollection.Clear();
            }
        }
        /// <summary>
        /// 用数据库读取到的设备刷新内存集合(不调用 Init)。
        /// 已存在于内存中的实例保持不变,避免丢失已注册/已打开的运行状态;
        /// 新增记录使用传入的(从数据库还原的)实例。
        /// </summary>
        public static void Refresh(IEnumerable<IDevice> devices)
        {
            if (devices == null) throw new ArgumentNullException(nameof(devices));
            lock (_sync)
            {
                var refreshed = new Dictionary<string, IDevice>(StringComparer.Ordinal);
                foreach (IDevice device in devices)
                {
                    if (device == null || string.IsNullOrWhiteSpace(device.DeviceId))
                    {
                        continue;
                    }
                    refreshed[device.DeviceId] = _deviceCollection.TryGetValue(device.DeviceId, out IDevice? existing)
                        ? existing
                        : device;
                }
                _deviceCollection.Clear();
                foreach (KeyValuePair<string, IDevice> pair in refreshed)
                {
                    _deviceCollection[pair.Key] = pair.Value;
                }
            }
        }
        /// <summary>按 DeviceId 从内存集合移除设备(数据库记录的删除由 <see cref="DeviceHelper.RemoveDevice"/> 负责)。</summary>
        public static bool Remove(string id)
        {
            lock (_sync)
            {
                return _deviceCollection.Remove(id);
            }
        }
        private static readonly JsonSerializerOptions SafeOptions = new()
        {
            Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)
        };
        public static void HandleClientMessage(RequestArg arg)
        {
            var data = arg.Data;
            string deviceId = data.DeviceId;
            if (_deviceCollection.TryGetValue(deviceId, out IDevice device))
            {
                var res = device.HandleRequest(data);
                arg.Client.Send(JsonSerializer.Serialize(res, SafeOptions), out _);
            }
            else
            {
                var res = new ResponseData
                {
                    Success = false,
                    Message = "无效的DeviceId"
                };
                arg.Client.Send(JsonSerializer.Serialize(res, SafeOptions), out _);
            }
        }
        public static void ReleaseClient(IClient client) {
        }
    }
}
DeviceService/DeviceManager/PLCDevice.cs
New file
@@ -0,0 +1,138 @@
using DeviceService.Core;
using DeviceService.Core.Api;
using DeviceService.Driver;
using DeviceService.Entity;
namespace DeviceService.Device
{
    [DeviceDesc("PLC")]
    public class PLCDevice : NotifyBase, IDevice
    {
        private long _id;
        public long Id
        {
            get => _id;
            set
            {
                SetProperty(ref _id, value);
            }
        }
        private string _deviceId;
        public string DeviceId
        {
            get => _deviceId;
            set
            {
                SetProperty(ref _deviceId, value);
            }
        }
        private string _type = "PLC";
        public string Type
        {
            get => _type;
            private set
            {
                SetProperty(ref _type, value);
            }
        }
        private string _brand;
        public string Brand
        {
            get => _brand;
            set
            {
                SetProperty(ref _brand, value);
            }
        }
        private string _connectStr;
        public string ConnectStr
        {
            get => _connectStr;
            set
            {
                SetProperty(ref _connectStr, value);
            }
        }
        private DeviceStatus _status;
        public DeviceStatus Status
        {
            get => _status;
            set
            {
                SetProperty(ref _status, value);
            }
        }
        private AccessType _accessType;
        public AccessType AccessType
        {
            get => _accessType;
            set
            {
                SetProperty(ref _accessType, value);
            }
        }
        private IPLCDriver driver;
        public bool Init(out string msg)
        {
            msg = string.Empty;
            try
            {
                driver = DriverManager.Instance.CreateDriverByTypeAndBrand<IPLCDriver>(Type, Brand);
                return true;
            }
            catch (Exception ex)
            {
                msg = ex.Message;
                return false;
            }
        }
        public bool Open(out string msg)
        {
            if (driver == null)
            {
                msg = "设备尚未初始化或初始化失败。";
                return false;
            }
            var res = driver.Open(out msg);
            if (res)
            {
                Status = DeviceStatus.Busy;
            }
            return res;
        }
        public bool Close(out string msg)
        {
            if (driver == null)
            {
                msg = "设备尚未初始化或初始化失败。";
                return false;
            }
            var res = driver.Close(out msg);
            if (res)
            {
                Status = DeviceStatus.Idle;
            }
            return res;
        }
        public ResponseData HandleRequest(RequestData req)
        {
            return new ResponseData
            {
                Success = false,
                Message = "未实现"
            };
        }
    }
}
DeviceService/DeviceService.csproj
New file
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <OutputType>WinExe</OutputType>
        <TargetFramework>net472</TargetFramework>
        <LangVersion>12.0</LangVersion>
        <Nullable>enable</Nullable>
        <ImplicitUsings>enable</ImplicitUsings>
        <UseWPF>true</UseWPF>
    </PropertyGroup>
    <ItemGroup>
      <PackageReference Include="System.Reflection.DispatchProxy" Version="4.8.2" />
      <PackageReference Include="System.Data.SQLite.Core" Version="1.0.119" />
      <PackageReference Include="log4net" Version="3.5.0" />
      <PackageReference Include="System.Text.Json" Version="10.0.14" />
    </ItemGroup>
    <ItemGroup>
      <ProjectReference Include="..\DeviceService.Core\DeviceService.Core.csproj" />
    </ItemGroup>
    <ItemGroup>
      <None Update="Config\log4net.config">
        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      </None>
    </ItemGroup>
</Project>
DeviceService/DriverManager/DriverDatabase.cs
New file
@@ -0,0 +1,257 @@
using System;
using System.Collections.Generic;
using System.Data.SQLite;
using System.IO;
namespace DeviceService.Driver
{
    /// <summary>
    /// 驱动信息 SQLite 持久化:数据库文件位于 应用根目录\Config\Drivers.db。
    ///
    /// 表结构:
    /// - DriverPackage:已加载的驱动包(Driver 根目录下的一个子文件夹)
    /// - DriverType:驱动包内带 [DriverDesc] 注解的驱动类型((类型, 品牌) 定位)
    ///
    /// 所有操作失败仅通过 log 上报,不会中断驱动加载流程。
    /// </summary>
    public sealed class DriverDatabase
    {
        private static readonly object _staticSync = new object();
        private static DriverDatabase _instance;
        public static DriverDatabase Instance
        {
            get
            {
                lock (_staticSync)
                {
                    return _instance ??= new DriverDatabase();
                }
            }
        }
        /// <summary>配置目录:应用根目录\Config。</summary>
        public static string ConfigDirectory =>
            Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Config");
        /// <summary>数据库文件路径。</summary>
        public static string DatabasePath =>
            Path.Combine(ConfigDirectory, "Drivers.db");
        private readonly object _sync = new object();
        private bool _initialized;
        private DriverDatabase()
        {
        }
        /// <summary>确保 Config 目录与库表结构存在。失败返回 false 并通过 log 上报。</summary>
        private bool EnsureReady(Action<string> log)
        {
            if (_initialized)
            {
                return true;
            }
            try
            {
                if (!Directory.Exists(ConfigDirectory))
                {
                    Directory.CreateDirectory(ConfigDirectory);
                }
                using (var conn = Open())
                using (var cmd = conn.CreateCommand())
                {
                    cmd.CommandText = @"
CREATE TABLE IF NOT EXISTS DriverPackage (
    Folder       TEXT PRIMARY KEY,
    Directory    TEXT NOT NULL,
    AssemblyCount INTEGER NOT NULL DEFAULT 0,
    ScanTime     TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS DriverType (
    Id            INTEGER PRIMARY KEY AUTOINCREMENT,
    PackageFolder TEXT NOT NULL,
    AssemblyFile  TEXT NOT NULL,
    FullName      TEXT NOT NULL,
    DriverType    TEXT,
    Brand         TEXT,
    UNIQUE(PackageFolder, FullName)
);
CREATE INDEX IF NOT EXISTS IX_DriverType_Type_Brand ON DriverType(DriverType, Brand);";
                    cmd.ExecuteNonQuery();
                }
                _initialized = true;
                return true;
            }
            catch (Exception ex)
            {
                log?.Invoke($"[驱动库] 初始化失败({DatabasePath}): {ex.Message}");
                return false;
            }
        }
        private SQLiteConnection Open()
        {
            var conn = new SQLiteConnection($"Data Source={DatabasePath};Version=3;");
            conn.Open();
            return conn;
        }
        /// <summary>写入(覆盖)一个驱动包及其内部全部驱动类型的记录。</summary>
        public void SavePackage(DriverPackage package, Action<string> log = null)
        {
            if (package == null)
            {
                return;
            }
            if (!EnsureReady(log))
            {
                return;
            }
            try
            {
                lock (_sync)
                {
                    using (var conn = Open())
                    using (var tx = conn.BeginTransaction())
                    {
                        ExecuteNonQuery(conn, "DELETE FROM DriverType WHERE PackageFolder = @f",
                            tx, ("@f", package.Folder));
                        ExecuteNonQuery(conn, "DELETE FROM DriverPackage WHERE Folder = @f",
                            tx, ("@f", package.Folder));
                        var assemblies = package.Assemblies ?? new DriverAssemblyInfo[0];
                        ExecuteNonQuery(conn,
                            "INSERT INTO DriverPackage(Folder, Directory, AssemblyCount, ScanTime) VALUES(@f, @d, @c, @t)",
                            tx,
                            ("@f", package.Folder),
                            ("@d", package.Directory ?? string.Empty),
                            ("@c", assemblies.Length.ToString()),
                            ("@t", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")));
                        int typeCount = 0;
                        foreach (var asm in assemblies)
                        {
                            foreach (var type in asm.Types ?? new DriverTypeInfo[0])
                            {
                                ExecuteNonQuery(conn,
                                    @"INSERT OR REPLACE INTO DriverType(PackageFolder, AssemblyFile, FullName, DriverType, Brand)
                                      VALUES(@p, @a, @n, @t, @b)",
                                    tx,
                                    ("@p", package.Folder),
                                    ("@a", asm.FileName ?? string.Empty),
                                    ("@n", type.FullName ?? string.Empty),
                                    ("@t", type.DriverType ?? string.Empty),
                                    ("@b", type.Brand ?? string.Empty));
                                typeCount++;
                            }
                        }
                        tx.Commit();
                        log?.Invoke($"[驱动库] 已保存 {package.Folder}({assemblies.Length} 个 dll,{typeCount} 个驱动类型)");
                    }
                }
            }
            catch (Exception ex)
            {
                log?.Invoke($"[驱动库] 保存 {package.Folder} 失败: {ex.Message}");
            }
        }
        /// <summary>删除一个驱动包(及其全部驱动类型)的记录。</summary>
        public void RemovePackage(string folder, Action<string> log = null)
        {
            if (string.IsNullOrEmpty(folder) || !EnsureReady(log))
            {
                return;
            }
            try
            {
                lock (_sync)
                {
                    using (var conn = Open())
                    using (var tx = conn.BeginTransaction())
                    {
                        ExecuteNonQuery(conn, "DELETE FROM DriverType WHERE PackageFolder = @f", tx, ("@f", folder));
                        ExecuteNonQuery(conn, "DELETE FROM DriverPackage WHERE Folder = @f", tx, ("@f", folder));
                        tx.Commit();
                    }
                }
                log?.Invoke($"[驱动库] 已移除 {folder} 的记录");
            }
            catch (Exception ex)
            {
                log?.Invoke($"[驱动库] 移除 {folder} 失败: {ex.Message}");
            }
        }
        /// <summary>读取库中全部驱动类型记录(供界面/查询使用)。</summary>
        public IReadOnlyList<DriverTypeInfo> LoadDriverTypes(Action<string> log = null)
        {
            var result = new List<DriverTypeInfo>();
            if (!EnsureReady(log))
            {
                return result;
            }
            try
            {
                lock (_sync)
                {
                    using (var conn = Open())
                    using (var cmd = conn.CreateCommand())
                    {
                        cmd.CommandText = "SELECT PackageFolder, AssemblyFile, FullName, DriverType, Brand FROM DriverType ORDER BY DriverType, Brand, FullName";
                        using (var reader = cmd.ExecuteReader())
                        {
                            while (reader.Read())
                            {
                                result.Add(new DriverTypeInfo
                                {
                                    FullName = GetString(reader, "FullName"),
                                    DriverType = GetString(reader, "DriverType"),
                                    Brand = GetString(reader, "Brand")
                                });
                            }
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                log?.Invoke($"[驱动库] 读取驱动类型失败: {ex.Message}");
            }
            return result;
        }
        private static void ExecuteNonQuery(SQLiteConnection conn, string sql, SQLiteTransaction tx,
            params (string Name, string Value)[] parameters)
        {
            using (var cmd = conn.CreateCommand())
            {
                cmd.Transaction = tx;
                cmd.CommandText = sql;
                foreach (var (name, value) in parameters)
                {
                    cmd.Parameters.AddWithValue(name, value);
                }
                cmd.ExecuteNonQuery();
            }
        }
        private static string GetString(SQLiteDataReader reader, string column)
        {
            var value = reader[column];
            return value == DBNull.Value ? string.Empty : value.ToString();
        }
    }
}
DeviceService/DriverManager/DriverDispatchProxy.cs
New file
@@ -0,0 +1,84 @@
using System.Reflection;
namespace DeviceService.Driver
{
    /// <summary>
    /// 基于 DispatchProxy 的驱动动态代理(.NET Framework 4.7.1+ 内置 API)。
    ///
    /// 用法:宿主自定义一个"鸭子接口"(方法名、参数类型与驱动方法一一对应),
    /// 驱动无需实现该接口、无需引用宿主程序集。接口调用按"方法名 + 参数个数"
    /// 匹配到驱动方法,经 <see cref="DriverInstance"/>(MBR 透明代理)转发到
    /// 驱动所在的 AppDomain 内反射执行。
    ///
    /// 与 RealProxy 的区别:
    /// - 生成的代理是宿主域内的普通对象,不再有 Remoting 相关的怪异行为
    ///   (Object.GetType/ToString 等基类方法不会被误路由,无需拦截);
    /// - out/ref 参数通过直接改写 args 数组回填;
    /// - 异常直接抛出,不再经过 ReturnMessage 包装;
    /// - API 在 .NET Core / .NET 5+ 同样可用,宿主未来升级可无缝迁移。
    ///
    /// 约束:s
    /// - T 必须是接口(DispatchProxy 只针对接口生成代理类);
    /// - 接口方法的参数/返回值必须可跨域封送(基元类型、string、[Serializable] DTO)。
    /// </summary>
    // 注意:不能 sealed —— DispatchProxy 在运行时生成其派生类作为代理类型
    public class DriverDispatchProxy : DispatchProxy
    {
        private DriverInstance _instance;
        /// <summary>
        /// 创建实现 T 的驱动动态代理(T 为宿主自定义的鸭子接口)。
        /// </summary>
        public static T Create<T>(DriverInstance instance) where T : class
        {
            if (!typeof(T).IsInterface)
            {
                throw new ArgumentException(
                    $"T 必须是接口(驱动类型不可跨域封送,宿主也无法引用厂商程序集),实际为 {typeof(T)}",
                    nameof(T));
            }
            // DispatchProxy.Create<T, TProxy> 要求 TProxy 为非泛型类,
            // 因此代理实例的创建与状态注入分两步完成
            var proxy = (DriverDispatchProxy)(object)Create<T, DriverDispatchProxy>();
            proxy._instance = instance;
            return (T)(object)proxy;
        }
        protected override object Invoke(MethodInfo targetMethod, object[] args)
        {
            if (targetMethod == null)
            {
                throw new NotSupportedException("无法解析接口方法(可能由泛型方法调用引起)");
            }
            var mi = _instance.GetMethods().FirstOrDefault(m =>
                string.Equals(m.Name, targetMethod.Name, StringComparison.Ordinal)
                && m.ParameterCount == (args?.Length ?? 0));
            if (mi == null)
            {
                throw new MissingMethodException(_instance.TypeFullName, $"{targetMethod.Name}({args?.Length ?? 0} 参数)——驱动未提供同名且参数个数一致的方法");
            }
            var direct = _instance.InvokeDirect(mi.Index, args);
            if (!direct.Success)
            {
                throw new InvalidOperationException($"驱动方法 {targetMethod.Name} 执行失败: {direct.Error}");
            }
            // out/ref 回填:把驱动域内的回填值写回 args 数组,
            // DispatchProxy 会自动同步回调用方的 out/ref 变量
            if (direct.ArgsOut != null && args != null)
            {
                var count = Math.Min(args.Length, direct.ArgsOut.Length);
                for (var i = 0; i < count; i++)
                {
                    args[i] = direct.ArgsOut[i];
                }
            }
            return direct.Value;
        }
    }
}
DeviceService/DriverManager/DriverInstance.cs
New file
@@ -0,0 +1,74 @@
namespace DeviceService.Driver
{
    /// <summary>
    /// 由 DriverManager 创建并统一管理的驱动实例代理。
    ///
    /// 驱动对象本身活在驱动包的 AppDomain 中(WPF/普通类均不可跨域封送),
    /// 因此宿主拿到的是本类的透明代理:宿主调用 <see cref="Invoke"/>、
    /// <see cref="GetMethods"/>、<see cref="Release"/> 时,实际执行都发生在驱动自己的域内。
    /// </summary>
    public sealed class DriverInstance : MarshalByRefObject
    {
        private readonly DriverLoader _owner;
        /// <summary>实例句柄(宿主与加载器之间定位实例的唯一标识)</summary>
        public string Handle { get; }
        /// <summary>驱动类型的 CLR 全名</summary>
        public string TypeFullName { get; }
        /// <summary>注解中的"类型"(如 CAN)</summary>
        public string DriverType { get; }
        /// <summary>注解中的"品牌"(如 BA)</summary>
        public string Brand { get; }
        /// <summary>所属驱动包名(由 DriverManager 回填,仅用于展示)</summary>
        public string PackageFolder { get; set; }
        // 实例由 DriverManager 长期持有,无限生存期,避免远程租约到期被回收
        public override object InitializeLifetimeService() => null;
        internal DriverInstance(DriverLoader owner, string handle, string typeFullName,
                                string driverType, string brand)
        {
            _owner = owner;
            Handle = handle;
            TypeFullName = typeFullName;
            DriverType = driverType;
            Brand = brand;
        }
        /// <summary>列出该驱动类型可调用的公共方法(用于确定 methodIndex)</summary>
        public DriverMethodInfo[] GetMethods()
        {
            return _owner.GetMethods(TypeFullName);
        }
        /// <summary>
        /// 在该驱动实例上反射调用方法。参数为字符串形式,按目标类型自动转换,
        /// out/ref 参数的回填值会包含在结果中。
        /// </summary>
        public InvokeResult Invoke(int methodIndex, string[] rawArgs)
        {
            return _owner.InvokeOnInstance(Handle, methodIndex, rawArgs);
        }
        /// <summary>
        /// 直接调用(原始对象参数/返回值),供动态代理使用。
        /// 注意:参数与返回值必须可跨域封送(基元类型、string、[Serializable] DTO)。
        /// </summary>
        public InvokeDirectResult InvokeDirect(int methodIndex, object[] args)
        {
            return _owner.InvokeOnInstanceDirect(Handle, methodIndex, args);
        }
        /// <summary>
        /// 释放该实例(若驱动实现了 IDisposable 则调用 Dispose)。
        /// </summary>
        public void Release()
        {
            _owner.ReleaseInstance(Handle);
        }
    }
}
DeviceService/DriverManager/DriverLoader.cs
New file
@@ -0,0 +1,784 @@
using System.IO;
using System.Reflection;
namespace DeviceService.Driver
{
    /// <summary>
    /// 驱动的程序集描述(跨 AppDomain 按值封送)。
    /// 注意:这些 DTO 只服务于"宿主 &lt;-&gt; 加载器"之间的通信,
    /// 驱动 dll 不需要引用本程序集,也不需要实现任何接口。
    /// </summary>
    [Serializable]
    public sealed class DriverAssemblyInfo
    {
        /// <summary>dll 文件名</summary>
        public string FileName { get; set; }
        /// <summary>程序集完整标识(加载成功时)</summary>
        public string AssemblyFullName { get; set; }
        /// <summary>加载失败原因(成功时为 null)</summary>
        public string Error { get; set; }
        /// <summary>带 [DriverDesc] 注解的驱动类型(按类型名排序)</summary>
        public DriverTypeInfo[] Types { get; set; }
    }
    /// <summary>
    /// 一个驱动类型的描述信息(来自 DriverDescAttribute)。
    /// 通过 (类型, 品牌) 可唯一定位到该类型。
    /// </summary>
    [Serializable]
    public sealed class DriverTypeInfo
    {
        /// <summary>CLR 类型全名</summary>
        public string FullName { get; set; }
        /// <summary>注解中的"类型"(如 CAN、串口)</summary>
        public string DriverType { get; set; }
        /// <summary>注解中的"品牌"(如 BA、BB)</summary>
        public string Brand { get; set; }
    }
    /// <summary>
    /// 驱动类型上的一个公共方法。
    /// </summary>
    [Serializable]
    public sealed class DriverMethodInfo
    {
        /// <summary>方法在该类型方法表中的索引(调用时回传)</summary>
        public int Index { get; set; }
        public string Name { get; set; }
        /// <summary>可读签名,例如 int ReadStatus(int channel)</summary>
        public string Signature { get; set; }
        public bool IsStatic { get; set; }
        public int ParameterCount { get; set; }
        /// <summary>参数列表(类型名 + 参数名),用于界面提示</summary>
        public string[] Parameters { get; set; }
    }
    /// <summary>
    /// 反射调用结果。
    /// </summary>
    [Serializable]
    public sealed class InvokeResult
    {
        public bool Success { get; set; }
        /// <summary>返回值文本(成功时)</summary>
        public string Output { get; set; }
        /// <summary>错误信息(失败时)</summary>
        public string Error { get; set; }
    }
    /// <summary>
    /// 直接调用结果:保留原始返回值与 out/ref 回填值,
    /// 供 RealProxy 动态代理把驱动调用适配成强类型接口方法。
    /// 注意:Value / ArgsOut 只能携带可跨域封送的数据(基元类型、string、[Serializable] DTO)。
    /// </summary>
    [Serializable]
    public sealed class InvokeDirectResult
    {
        public bool Success { get; set; }
        /// <summary>方法返回值(void 方法为 null)</summary>
        public object Value { get; set; }
        /// <summary>调用后的参数数组(out/ref 位置为回填值,其余位置原值)</summary>
        public object[] ArgsOut { get; set; }
        /// <summary>错误信息(失败时)</summary>
        public string Error { get; set; }
    }
    /// <summary>
    /// 运行在每个驱动包专属 AppDomain 内的加载器。
    ///
    /// 设计前提:驱动 dll 来自不同厂商,彼此没有任何共同接口/基类约定,
    /// 甚至互不兼容(同一第三方依赖的不同版本)。
    /// 因此这里只做通用反射:枚举公开类型、枚举公共方法、反射创建实例、反射调用。
    ///
    /// 该域的 ApplicationBase 被设置为某个驱动子文件夹,
    /// 所以驱动的全部依赖只能从自己目录解析,天然隔离版本冲突。
    /// </summary>
    public class DriverLoader : MarshalByRefObject
    {
        private static readonly HashSet<string> FailedAssemblies =
            new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        private readonly Dictionary<string, Type> _types = new Dictionary<string, Type>(StringComparer.Ordinal);
        private readonly Dictionary<string, MethodInfo[]> _methodCache = new Dictionary<string, MethodInfo[]>(StringComparer.Ordinal);
        private readonly Dictionary<Type, object> _instances = new Dictionary<Type, object>();
        /// <summary>由 DriverManager 创建、按句柄管理的驱动实例</summary>
        private readonly Dictionary<string, ManagedInstance> _managedInstances =
            new Dictionary<string, ManagedInstance>(StringComparer.Ordinal);
        /// <summary>驱动索引:(类型, 品牌) → 驱动信息,key 形如 "CAN|BA"</summary>
        private readonly Dictionary<string, DriverTypeInfo> _driverIndex =
            new Dictionary<string, DriverTypeInfo>(StringComparer.OrdinalIgnoreCase);
        // 无限生存期,避免远程代理被回收
        public override object InitializeLifetimeService() => null;
        public DriverLoader()
        {
            AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyResolve;
        }
        #region 发现
        /// <summary>
        /// 扫描本驱动目录下的所有 dll(忽略宿主自身的 MultiPlugin.* 运行时文件),
        /// 返回每个程序集的信息与公开类型列表。
        /// </summary>
        public DriverAssemblyInfo[] LoadAssemblies()
        {
            var dir = AppDomain.CurrentDomain.BaseDirectory;
            var result = new List<DriverAssemblyInfo>();
            foreach (var path in Directory.GetFiles(dir, "*.dll").OrderBy(p => p, StringComparer.OrdinalIgnoreCase))
            {
                var fileName = Path.GetFileName(path);
                if (fileName.StartsWith("MultiPlugin.", StringComparison.OrdinalIgnoreCase))
                {
                    continue; // 宿主基础设施,非驱动
                }
                var info = new DriverAssemblyInfo { FileName = fileName };
                try
                {
                    var assembly = Assembly.Load(Path.GetFileNameWithoutExtension(path));
                    info.AssemblyFullName = assembly.FullName;
                    // 只收录带 [DriverDesc] 注解的类,其余类型一律忽略
                    var drivers = new List<DriverTypeInfo>();
                    foreach (var type in assembly.GetExportedTypes())
                    {
                        if (!type.IsClass || type.IsAbstract)
                        {
                            continue;
                        }
                        var desc = ReadDriverDesc(type);
                        if (desc == null)
                        {
                            continue;
                        }
                        _types[type.FullName] = type;
                        var driverInfo = new DriverTypeInfo
                        {
                            FullName = type.FullName,
                            DriverType = desc.Item1,
                            Brand = desc.Item2
                        };
                        drivers.Add(driverInfo);
                        _driverIndex[DriverKey(desc.Item1, desc.Item2)] = driverInfo;
                    }
                    info.Types = drivers.OrderBy(t => t.FullName, StringComparer.Ordinal).ToArray();
                }
                catch (Exception ex)
                {
                    // 非托管 dll、缺少依赖、架构不匹配等都会到这里
                    info.Error = ex.Message;
                }
                result.Add(info);
            }
            return result.ToArray();
        }
        /// <summary>
        /// 按 (类型, 品牌) 查找驱动。任一字段留空视为通配。
        /// </summary>
        /// <returns>命中的第一个驱动信息;未命中返回 null</returns>
        public DriverTypeInfo FindDriver(string driverType, string brand)
        {
            var key = DriverKey(driverType, brand);
            if (_driverIndex.TryGetValue(key, out var exact))
            {
                return exact;
            }
            // 精确未命中且存在通配字段时线性扫描
            var anyType = key.StartsWith("*|", StringComparison.Ordinal);
            var anyBrand = key.EndsWith("|*", StringComparison.Ordinal);
            if (!anyType && !anyBrand)
            {
                return null;
            }
            foreach (var info in _driverIndex.Values)
            {
                if (!anyType && !string.Equals(info.DriverType?.Trim(), driverType?.Trim(), StringComparison.OrdinalIgnoreCase))
                {
                    continue;
                }
                if (!anyBrand && !string.Equals(info.Brand?.Trim(), brand?.Trim(), StringComparison.OrdinalIgnoreCase))
                {
                    continue;
                }
                return info;
            }
            return null;
        }
        /// <summary>
        /// 用鸭子反射读取 [DriverDesc] 注解的 Type/Brand 属性值。
        /// 宿主基础设施不引用 Driver.Core,只按"属性名"解耦读取,
        /// 这样厂商 SDK 升级/换版本不会影响宿主。
        /// </summary>
        private static Tuple<string, string> ReadDriverDesc(Type type)
        {
            foreach (var attr in type.GetCustomAttributes(false))
            {
                var attrType = attr.GetType();
                if (!attrType.Name.Equals("DriverDescAttribute", StringComparison.OrdinalIgnoreCase))
                {
                    continue;
                }
                var typeProp = attrType.GetProperty("Type");
                var brandProp = attrType.GetProperty("Brand");
                if (typeProp == null || brandProp == null)
                {
                    continue;
                }
                return Tuple.Create(typeProp.GetValue(attr, null) as string,
                                    brandProp.GetValue(attr, null) as string);
            }
            return null;
        }
        private static string DriverKey(string driverType, string brand)
        {
            var t = string.IsNullOrWhiteSpace(driverType) ? "*" : driverType.Trim().ToUpperInvariant();
            var b = string.IsNullOrWhiteSpace(brand) ? "*" : brand.Trim().ToUpperInvariant();
            return $"{t}|{b}";
        }
        /// <summary>
        /// 列出某个类型的公共方法(属性访问器会被过滤掉)。
        /// </summary>
        public DriverMethodInfo[] GetMethods(string typeFullName)
        {
            var type = ResolveType(typeFullName);
            if (type == null)
            {
                return new DriverMethodInfo[0];
            }
            return GetMethodInfos(type)
                .Select((m, i) =>
                {
                    var parameters = m.GetParameters();
                    return new DriverMethodInfo
                    {
                        Index = i,
                        Name = m.Name,
                        IsStatic = m.IsStatic,
                        ParameterCount = parameters.Length,
                        Signature = BuildSignature(m),
                        Parameters = parameters
                            .Select(p => $"{FormatTypeName(p.ParameterType)} {p.Name}")
                            .ToArray()
                    };
                })
                .ToArray();
        }
        #endregion
        #region 实例管理
        private sealed class ManagedInstance
        {
            public Type Type;
            public object Target;
        }
        /// <summary>
        /// 创建驱动类型的新实例(不走 Invoke 的默认缓存实例),按句柄登记供 DriverManager 管理。
        /// </summary>
        public DriverInstance CreateInstance(string typeFullName, string driverType, string brand)
        {
            var type = ResolveType(typeFullName)
                       ?? throw new InvalidOperationException($"未找到类型: {typeFullName}");
            var ctor = type.GetConstructor(Type.EmptyTypes)
                       ?? throw new MissingMethodException(typeFullName, ".ctor(缺少公共无参构造函数)");
            var target = ctor.Invoke(null);
            var handle = Guid.NewGuid().ToString("N");
            _managedInstances[handle] = new ManagedInstance { Type = type, Target = target };
            return new DriverInstance(this, handle, type.FullName, driverType, brand);
        }
        /// <summary>
        /// 在指定句柄的驱动实例上按"原始对象参数"直接调用方法,
        /// 保留原始返回值与 out/ref 回填值(供 RealProxy 动态代理使用)。
        /// </summary>
        public InvokeDirectResult InvokeOnInstanceDirect(string handle, int methodIndex, object[] args)
        {
            var result = new InvokeDirectResult();
            if (!_managedInstances.TryGetValue(handle, out var managed))
            {
                result.Success = false;
                result.Error = $"实例不存在或已释放: {handle}";
                return result;
            }
            var methods = GetMethodInfos(managed.Type);
            if (methodIndex < 0 || methodIndex >= methods.Length)
            {
                result.Success = false;
                result.Error = $"方法索引无效: {methodIndex}(共 {methods.Length} 个方法)";
                return result;
            }
            try
            {
                var method = methods[methodIndex];
                var parameters = method.GetParameters();
                args = args ?? new object[0];
                if (parameters.Length != args.Length)
                {
                    result.Success = false;
                    result.Error = $"参数数量不匹配: {method.Name} 需要 {parameters.Length} 个,实际传入 {args.Length} 个";
                    return result;
                }
                var converted = new object[parameters.Length];
                for (var i = 0; i < parameters.Length; i++)
                {
                    converted[i] = CoerceArg(args[i], parameters[i].ParameterType, parameters[i].Name);
                }
                result.Value = method.Invoke(managed.Target, converted);
                result.ArgsOut = converted;
                result.Success = true;
                return result;
            }
            catch (TargetInvocationException ex)
            {
                var inner = ex.InnerException ?? ex;
                result.Success = false;
                result.Error = $"{inner.GetType().Name}: {inner.Message}";
                return result;
            }
            catch (Exception ex)
            {
                result.Success = false;
                result.Error = $"{ex.GetType().Name}: {ex.Message}";
                return result;
            }
        }
        /// <summary>
        /// 在指定句柄的驱动实例上反射调用方法(实际执行发生在驱动自己的域内)。
        /// </summary>
        public InvokeResult InvokeOnInstance(string handle, int methodIndex, string[] rawArgs)
        {
            if (!_managedInstances.TryGetValue(handle, out var managed))
            {
                return Fail($"实例不存在或已释放: {handle}");
            }
            var methods = GetMethodInfos(managed.Type);
            if (methodIndex < 0 || methodIndex >= methods.Length)
            {
                return Fail($"方法索引无效: {methodIndex}(共 {methods.Length} 个方法)");
            }
            return ExecuteMethod(methods[methodIndex], managed.Target, rawArgs);
        }
        /// <summary>
        /// 释放指定句柄的实例;驱动实现 IDisposable 时会调用 Dispose。
        /// </summary>
        public void ReleaseInstance(string handle)
        {
            if (!_managedInstances.TryGetValue(handle, out var managed))
            {
                return;
            }
            _managedInstances.Remove(handle);
            (managed.Target as IDisposable)?.Dispose();
        }
        #endregion
        #region 调用
        /// <summary>
        /// 反射调用指定类型的方法。
        /// 实例方法会自动用公共无参构造函数创建(并缓存)实例;
        /// 静态方法无需实例。需要长期持有实例请使用 <see cref="CreateInstance"/>。
        /// </summary>
        /// <param name="rawArgs">字符串形式的实参,由 <see cref="ConvertArg"/> 按目标参数类型转换</param>
        public InvokeResult Invoke(string typeFullName, int methodIndex, string[] rawArgs)
        {
            try
            {
                var type = ResolveType(typeFullName);
                if (type == null)
                {
                    return Fail($"未找到类型: {typeFullName}");
                }
                var methods = GetMethodInfos(type);
                if (methodIndex < 0 || methodIndex >= methods.Length)
                {
                    return Fail($"方法索引无效: {methodIndex}(共 {methods.Length} 个方法)");
                }
                var method = methods[methodIndex];
                object target = null;
                if (!method.IsStatic)
                {
                    target = GetOrCreateInstance(type);
                    if (target == null)
                    {
                        return Fail($"无法实例化 {type.FullName}: 缺少公共无参构造函数。可改为只调用该类型的静态方法。");
                    }
                }
                return ExecuteMethod(method, target, rawArgs);
            }
            catch (TargetInvocationException ex)
            {
                // 驱动内部抛出的异常
                var inner = ex.InnerException ?? ex;
                return Fail($"{inner.GetType().Name}: {inner.Message}");
            }
            catch (Exception ex)
            {
                return Fail($"{ex.GetType().Name}: {ex.Message}");
            }
        }
        /// <summary>
        /// 参数转换 → 反射调用 → 汇总返回值与 out/ref 回填值。
        /// </summary>
        private static InvokeResult ExecuteMethod(MethodInfo method, object target, string[] rawArgs)
        {
            try
            {
                var parameters = method.GetParameters();
                rawArgs = rawArgs ?? new string[0];
                if (parameters.Length != rawArgs.Length)
                {
                    return Fail($"参数数量不匹配: {method.Name} 需要 {parameters.Length} 个参数,实际传入 {rawArgs.Length} 个");
                }
                var args = new object[parameters.Length];
                for (var i = 0; i < parameters.Length; i++)
                {
                    args[i] = ConvertArg(rawArgs[i], parameters[i].ParameterType, parameters[i].Name);
                }
                var returnValue = method.Invoke(target, args);
                // 汇总返回值与 out/ref 回填值(如 Open(out string msg) 的 msg)
                var parts = new List<string>();
                if (method.ReturnType != typeof(void))
                {
                    parts.Add($"返回: {Format(returnValue)}");
                }
                for (var i = 0; i < parameters.Length; i++)
                {
                    if (parameters[i].ParameterType.IsByRef)
                    {
                        parts.Add($"{parameters[i].Name} = {Format(args[i])}");
                    }
                }
                return new InvokeResult { Success = true, Output = parts.Count == 0 ? "null" : string.Join("; ", parts) };
            }
            catch (TargetInvocationException ex)
            {
                // 驱动内部抛出的异常
                var inner = ex.InnerException ?? ex;
                return Fail($"{inner.GetType().Name}: {inner.Message}");
            }
            catch (Exception ex)
            {
                return Fail($"{ex.GetType().Name}: {ex.Message}");
            }
        }
        private object GetOrCreateInstance(Type type)
        {
            if (_instances.TryGetValue(type, out var existing))
            {
                return existing;
            }
            if (type.IsAbstract || type.IsInterface)
            {
                return null;
            }
            var ctor = type.GetConstructor(Type.EmptyTypes);
            if (ctor == null)
            {
                return null;
            }
            var instance = ctor.Invoke(null);
            _instances[type] = instance;
            return instance;
        }
        #endregion
        #region 辅助
        private Type ResolveType(string typeFullName)
        {
            if (_types.TryGetValue(typeFullName, out var type))
            {
                return type;
            }
            // 兜底:在已加载的驱动程序集里查找
            type = AppDomain.CurrentDomain.GetAssemblies()
                .Where(a => !a.IsDynamic)
                .Select(a => a.GetType(typeFullName))
                .FirstOrDefault(t => t != null);
            if (type != null)
            {
                _types[typeFullName] = type;
            }
            return type;
        }
        private MethodInfo[] GetMethodInfos(Type type)
        {
            if (_methodCache.TryGetValue(type.FullName, out var cached))
            {
                return cached;
            }
            var methods = type
                .GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly)
                .Where(m => !m.IsSpecialName)   // 过滤 get_/set_/add_/remove_ 等
                .ToArray();
            _methodCache[type.FullName] = methods;
            return methods;
        }
        private static string BuildSignature(MethodInfo method)
        {
            var parameters = method.GetParameters()
                .Select(p => $"{FormatTypeName(p.ParameterType)} {p.Name}");
            return $"{(method.IsStatic ? "static " : string.Empty)}{FormatTypeName(method.ReturnType)} "
                   + $"{method.Name}({string.Join(", ", parameters)})";
        }
        private static string FormatTypeName(Type type)
        {
            if (!type.IsGenericType)
            {
                return type.Name;
            }
            var name = type.Name.Substring(0, type.Name.IndexOf('`'));
            var args = string.Join(", ", type.GetGenericArguments().Select(FormatTypeName));
            return $"{name}<{args}>";
        }
        /// <summary>
        /// 把跨域代理传来的对象参数适配为目标参数类型:
        /// 类型兼容直接用,否则退化为字符串转换(鸭子式宽松匹配)。
        /// </summary>
        private static object CoerceArg(object arg, Type targetType, string parameterName)
        {
            if (targetType.IsByRef)
            {
                targetType = targetType.GetElementType();
            }
            if (arg == null)
            {
                return targetType.IsValueType ? Activator.CreateInstance(targetType) : null;
            }
            if (targetType.IsInstanceOfType(arg))
            {
                return arg;
            }
            return ConvertArg(arg.ToString(), targetType, parameterName);
        }
        /// <summary>
        /// 把 UI 传来的字符串转换为目标参数类型。
        /// 支持常用基元类型、枚举、Guid、DateTime、字符串数组(以分号分隔)。
        /// </summary>
        private static object ConvertArg(string raw, Type targetType, string parameterName)
        {
            raw = raw ?? string.Empty;
            // out/ref 参数实际按其元素类型转换(调用后由 CLR 回填)
            if (targetType.IsByRef)
            {
                targetType = targetType.GetElementType();
            }
            if (targetType == typeof(string) || targetType == typeof(object))
            {
                return raw;
            }
            if (raw.Length == 0 && !targetType.IsValueType)
            {
                return null;
            }
            if (targetType == typeof(bool)) return bool.Parse(raw);
            if (targetType == typeof(char)) return raw[0];
            if (targetType == typeof(byte)) return byte.Parse(raw);
            if (targetType == typeof(sbyte)) return sbyte.Parse(raw);
            if (targetType == typeof(short)) return short.Parse(raw);
            if (targetType == typeof(ushort)) return ushort.Parse(raw);
            if (targetType == typeof(int)) return int.Parse(raw);
            if (targetType == typeof(uint)) return uint.Parse(raw);
            if (targetType == typeof(long)) return long.Parse(raw);
            if (targetType == typeof(ulong)) return ulong.Parse(raw);
            if (targetType == typeof(float)) return float.Parse(raw);
            if (targetType == typeof(double)) return double.Parse(raw);
            if (targetType == typeof(decimal)) return decimal.Parse(raw);
            if (targetType == typeof(Guid)) return Guid.Parse(raw);
            if (targetType == typeof(DateTime)) return DateTime.Parse(raw);
            if (targetType.IsEnum) return Enum.Parse(targetType, raw, true);
            if (targetType.IsArray)
            {
                var elementType = targetType.GetElementType();
                var parts = raw.Length == 0 ? new string[0] : raw.Split(';');
                var array = Array.CreateInstance(elementType, parts.Length);
                for (var i = 0; i < parts.Length; i++)
                {
                    array.SetValue(ConvertArg(parts[i], elementType, parameterName), i);
                }
                return array;
            }
            throw new InvalidOperationException(
                $"参数 {parameterName} 的类型 {targetType.Name} 暂不支持从字符串转换(支持:字符串、数字、bool、枚举、Guid、DateTime、数组)");
        }
        private static string Format(object value)
        {
            if (value == null)
            {
                return "null";
            }
            if (value is string text)
            {
                return Truncate(text);
            }
            if (value is Array array)
            {
                var items = array.Cast<object>().Select(x => x == null ? "null" : x.ToString());
                return Truncate($"[{string.Join(", ", items)}]");
            }
            return Truncate(value.ToString());
        }
        private static string Truncate(string text)
        {
            const int max = 4000;
            return text.Length <= max ? text : text.Substring(0, max) + " ...(已截断)";
        }
        private static InvokeResult Fail(string message) => new InvokeResult { Success = false, Error = message };
        /// <summary>
        /// 依赖解析兜底:只在本驱动目录(及加载器所在目录)内查找,
        /// 保证不会串到其他驱动包的同名程序集。
        /// </summary>
        private static Assembly OnAssemblyResolve(object sender, ResolveEventArgs args)
        {
            var simpleName = new AssemblyName(args.Name).Name;
            if (FailedAssemblies.Contains(simpleName))
            {
                return null;
            }
            var dirs = new List<string>();
            // 优先按加载器所在目录(宿主 bin)解析宿主基础设施,再兜底驱动目录
            var loaderLocation = typeof(DriverLoader).Assembly.Location;
            var loaderDir = string.IsNullOrEmpty(loaderLocation) ? null : Path.GetDirectoryName(loaderLocation);
            if (!string.IsNullOrEmpty(loaderDir))
            {
                dirs.Add(loaderDir);
            }
            var baseDir = AppDomain.CurrentDomain.BaseDirectory;
            if (!dirs.Contains(baseDir))
            {
                dirs.Add(baseDir);
            }
            foreach (var dir in dirs)
            {
                foreach (var ext in new[] { ".dll", ".exe" })
                {
                    var candidate = Path.Combine(dir, simpleName + ext);
                    if (!File.Exists(candidate))
                    {
                        continue;
                    }
                    try
                    {
                        return Assembly.LoadFrom(candidate);
                    }
                    catch
                    {
                        // 尝试下一个候选
                    }
                }
            }
            FailedAssemblies.Add(simpleName);
            return null;
        }
        #endregion
    }
}
DeviceService/DriverManager/DriverManager.cs
New file
@@ -0,0 +1,411 @@
using System.IO;
namespace DeviceService.Driver
{
    /// <summary>
    /// 一个已隔离加载的驱动包(对应驱动根目录下的一个子文件夹)。
    /// </summary>
    public sealed class DriverPackage
    {
        /// <summary>子文件夹名(通常为厂商/设备名)</summary>
        public string Folder { get; set; }
        /// <summary>驱动包所在物理目录</summary>
        public string Directory { get; set; }
        /// <summary>驱动包专属 AppDomain</summary>
        public AppDomain Domain { get; set; }
        /// <summary>运行在该域内的加载器代理</summary>
        public DriverLoader Loader { get; set; }
        /// <summary>该包内的程序集与公开类型</summary>
        public DriverAssemblyInfo[] Assemblies { get; set; }
    }
    /// <summary>
    /// (类型, 品牌) 查找结果。
    /// </summary>
    public sealed class DriverFindResult
    {
        public DriverPackage Package { get; set; }
        public DriverTypeInfo Info { get; set; }
    }
    /// <summary>
    /// 驱动管理器:扫描根目录下所有子文件夹,每个子文件夹一个隔离域。
    ///
    /// 适用于"不同厂商提供的驱动 dll 之间没有任何契约"的场景:
    /// - 驱动 dll 不需要实现接口、不需要引用宿主程序集;
    /// - 宿主只通过反射枚举类型/方法、创建实例、调用方法;
    /// - 每个驱动包自带自己的依赖,互不干扰(可存在同名不同版本);
    /// - 需要替换驱动文件时先卸载域,文件不会被锁定(影子复制)。
    /// </summary>
    public sealed class DriverManager
    {
        private static DriverManager _instance;
        public static DriverManager Instance
        {
            get
            {
                if (_instance == null)
                {
                    _instance = new DriverManager();
                }
                return _instance;
            }
        }
        private DriverManager()
        {
        }
        private readonly List<DriverPackage> _packages = new List<DriverPackage>();
        /// <summary>由 DriverManager 统一登记管理的驱动实例(跨域透明代理)</summary>
        private readonly Dictionary<string, DriverInstance> _managedDrivers =
            new Dictionary<string, DriverInstance>(StringComparer.Ordinal);
        public IReadOnlyList<DriverPackage> Packages => _packages;
        /// <summary>当前由 DriverManager 管理的全部驱动实例</summary>
        public IReadOnlyCollection<DriverInstance> ManagedDrivers => _managedDrivers.Values;
        /// <summary>
        /// 扫描 rootPath 下的每个子文件夹并创建隔离域加载。
        /// </summary>
        public void Scan(string rootPath, Action<string> log)
        {
            if (!Directory.Exists(rootPath))
            {
                log?.Invoke($"[错误] 目录不存在: {rootPath}");
                return;
            }
            var subDirs = Directory.GetDirectories(rootPath);
            if (subDirs.Length == 0)
            {
                log?.Invoke($"[提示] {rootPath} 下没有子文件夹(每个子文件夹应为一个驱动包)");
                return;
            }
            foreach (var dir in subDirs)
            {
                var brandDirs = Directory.GetDirectories(dir);
                foreach (var brandDir in brandDirs)
                {
                    var name = Path.GetFileName(brandDir);
                    try
                    {
                        LoadPackage(brandDir, name, log);
                    }
                    catch (Exception ex)
                    {
                        log?.Invoke($"[失败] {name}: {ex.Message}");
                    }
                }
            }
        }
        /// <summary>
        /// 列出某个类型可调用的公共方法。
        /// </summary>
        public DriverMethodInfo[] GetMethods(DriverPackage package, string typeFullName)
        {
            return package.Loader.GetMethods(typeFullName);
        }
        /// <summary>
        /// 在所有已加载驱动包中按 (类型, 品牌) 查找驱动。
        /// 任一字段留空视为通配;同一组合命中多个包时记录警告并返回第一个。
        /// </summary>
        public DriverFindResult FindDriver(string driverType, string brand, Action<string> log)
        {
            DriverFindResult first = null;
            foreach (var package in _packages)
            {
                var info = package.Loader.FindDriver(driverType, brand);
                if (info == null)
                {
                    continue;
                }
                if (first == null)
                {
                    first = new DriverFindResult { Package = package, Info = info };
                }
                else
                {
                    log?.Invoke($"[警告] (类型={driverType}, 品牌={brand}) 在多个驱动包中重复: "
                                + $"{first.Package.Folder} 与 {package.Folder},返回第一个");
                }
            }
            return first;
        }
        /// <summary>
        /// 反射调用驱动方法(实际执行发生在该驱动自己的域内)。
        /// </summary>
        public InvokeResult Invoke(DriverPackage package, string typeFullName, int methodIndex, string[] args)
        {
            return package.Loader.Invoke(typeFullName, methodIndex, args);
        }
        /// <summary>
        /// 根据 (类型, 品牌) 找到对应的驱动类型,在其所属驱动包的 AppDomain 内
        /// 创建一个新实例并以透明代理形式返回,由 DriverManager 统一登记管理。
        ///
        /// 说明:驱动类型本身不可跨域封送(非 MarshalByRefObject,宿主也不引用厂商程序集),
        /// 因此无法用泛型 T 返回"原生类型";宿主一律通过 <see cref="DriverInstance"/> 代理操作,
        /// 每次调用实际执行发生在驱动自己的域内。
        /// </summary>
        /// <param name="driverType">注解中的"类型",如 CAN;留空为通配</param>
        /// <param name="brand">注解中的"品牌",如 BA;留空为通配</param>
        /// <param name="log">日志回调(可空)</param>
        /// <returns>驱动实例代理;未找到或创建失败返回 null</returns>
        public DriverInstance CreateDriverByTypeAndBrand(string driverType, string brand, Action<string> log = null)
        {
            var hit = FindDriver(driverType, brand, log);
            if (hit == null)
            {
                log?.Invoke($"[创建] 未找到 (类型={driverType}, 品牌={brand}) 对应的驱动");
                return null;
            }
            try
            {
                var instance = hit.Package.Loader.CreateInstance(hit.Info.FullName, hit.Info.DriverType, hit.Info.Brand);
                instance.PackageFolder = hit.Package.Folder;
                _managedDrivers[instance.Handle] = instance;
                log?.Invoke($"[创建] {hit.Info.FullName}({hit.Info.DriverType}/{hit.Info.Brand},包 {hit.Package.Folder})"
                            + $" => 实例 {ShortHandle(instance.Handle)}");
                return instance;
            }
            catch (Exception ex)
            {
                log?.Invoke($"[创建失败] {hit.Info.FullName}: {ex.Message}");
                return null;
            }
        }
        /// <summary>
        /// 泛型版本:T 为宿主自定义的"鸭子接口"(方法名、参数类型与驱动方法一一对应)。
        /// 返回 DispatchProxy 动态代理:接口调用按"方法名 + 参数个数"转发到驱动域内反射执行。
        ///
        /// 驱动无需实现 T、无需引用宿主程序集——这正是动态代理的意义:
        /// 不同厂商的驱动可以共享宿主定义的一套接口约定。
        ///
        /// 约束:
        /// - T 必须是接口(DispatchProxy 只针对接口生成代理类);
        /// - 接口方法的参数/返回值必须可跨域封送(基元类型、string、[Serializable] DTO);
        /// - 每次调用都是一次跨域反射,性能低于直接调用,适合设备控制类低频操作。
        /// </summary>
        public T CreateDriverByTypeAndBrand<T>(string driverType, string brand, Action<string> log = null)
            where T : class
        {
            var instance = CreateDriverByTypeAndBrand(driverType, brand, log);
            if (instance == null)
            {
                return null;
            }
            log?.Invoke($"[代理] {instance.TypeFullName} => {typeof(T).Name} 动态代理(实例 {ShortHandle(instance.Handle)})");
            return DriverDispatchProxy.Create<T>(instance);
        }
        /// <summary>
        /// 释放一个由 DriverManager 管理的驱动实例(驱动实现 IDisposable 时会调用 Dispose)。
        /// </summary>
        public void ReleaseDriver(DriverInstance instance, Action<string> log = null)
        {
            if (instance == null)
            {
                return;
            }
            _managedDrivers.Remove(instance.Handle);
            try
            {
                instance.Release();
                log?.Invoke($"[释放] 实例 {ShortHandle(instance.Handle)}({instance.TypeFullName})");
            }
            catch (Exception ex)
            {
                log?.Invoke($"[释放失败] 实例 {ShortHandle(instance.Handle)}: {ex.Message}");
            }
        }
        /// <summary>
        /// 卸载某个驱动包前,先通过代理释放其名下所有托管实例(域卸载后代理会失效)。
        /// </summary>
        private void ReleaseInstancesOf(DriverPackage package, Action<string> log)
        {
            var targets = _managedDrivers.Values
                .Where(i => i.PackageFolder == package.Folder)
                .ToList();
            foreach (var instance in targets)
            {
                ReleaseDriver(instance, log);
            }
        }
        private static string ShortHandle(string handle) => handle.Substring(0, Math.Min(8, handle.Length));
        /// <summary>
        /// 卸载所有驱动域(释放程序集、解锁 dll 文件)。
        /// </summary>
        public void UnloadAll(Action<string> log)
        {
            // 先通过代理释放所有托管实例,域卸载后代理将失效
            foreach (var instance in _managedDrivers.Values.ToList())
            {
                ReleaseDriver(instance, log);
            }
            var domains = _packages.Select(p => p.Domain).Distinct().ToList();
            _packages.Clear();
            foreach (var domain in domains)
            {
                // 注意:必须在卸载前读取 FriendlyName,域卸载后再访问其属性会抛 AppDomainUnloadedException
                var name = domain.FriendlyName;
                try
                {
                    AppDomain.Unload(domain);
                    log?.Invoke($"[卸载] {name}");
                }
                catch (Exception ex)
                {
                    log?.Invoke($"[卸载失败] {name}: {ex.Message}");
                }
            }
        }
        private DriverPackage LoadPackage(string dir, string name, Action<string> log)
        {
            // 加载器与 DTO 契约统一使用宿主目录中的唯一副本,不再向驱动目录复制
            var loaderPath = typeof(DriverLoader).Assembly.Location;
            WarnIfStaleRuntimeFiles(dir, name, log);
            var setup = new AppDomainSetup
            {
                ApplicationName = name,
                // 关键:探测根固定为该驱动目录,依赖只会从自己目录解析
                ApplicationBase = dir,
                // 影子复制:dll 加载后不锁定,可随时卸载/替换文件
                ShadowCopyFiles = "true",
                ShadowCopyDirectories = dir,
                CachePath = Path.Combine(Path.GetTempPath(), "MultiPluginCache", name)
            };
            var domain = AppDomain.CreateDomain("Driver_" + name, null, setup);
            try
            {
                var loader = (DriverLoader)domain.CreateInstanceFromAndUnwrap(loaderPath, typeof(DriverLoader).FullName);
                var assemblies = loader.LoadAssemblies();
                var package = new DriverPackage
                {
                    Folder = name,
                    Directory = dir,
                    Domain = domain,
                    Loader = loader,
                    Assemblies = assemblies
                };
                _packages.Add(package);
                // 驱动信息持久化到 Config\Drivers.db
                DriverDatabase.Instance.SavePackage(package, log);
                var totalDrivers = assemblies.Where(a => a.Types != null).Sum(a => a.Types.Length);
                var failed = assemblies.Count(a => a.Error != null);
                log?.Invoke($"[驱动包] {name}: {assemblies.Length} 个 dll,{totalDrivers} 个驱动类型"
                            + (failed > 0 ? $",{failed} 个无法加载" : string.Empty));
                foreach (var asm in assemblies.Where(a => a.Error != null))
                {
                    log?.Invoke($"    [!] {asm.FileName}: {asm.Error}");
                }
                return package;
            }
            catch
            {
                try { AppDomain.Unload(domain); } catch { /* 忽略 */ }
                throw;
            }
        }
        /// <summary>
        /// 卸载指定的驱动包(释放该域中的所有程序集)。
        /// </summary>
        public void Unload(DriverPackage package, Action<string> log)
        {
            // 域卸载前先释放该包名下的托管实例,避免操作失效代理
            ReleaseInstancesOf(package, log);
            _packages.Remove(package);
            // 同步移除该驱动包在 Config\Drivers.db 中的记录
            DriverDatabase.Instance.RemovePackage(package.Folder, log);
            try
            {
                AppDomain.Unload(package.Domain);
                log?.Invoke($"[卸载] {package.Folder}");
            }
            catch (Exception ex)
            {
                log?.Invoke($"[卸载失败] {package.Folder}: {ex.Message}");
            }
        }
        /// <summary>
        /// 重载指定的驱动包:卸载旧域后用同一目录重新创建域。
        /// 依赖影子复制,替换 dll 后无需重启宿主即可生效。
        /// </summary>
        public DriverPackage Reload(DriverPackage package, Action<string> log)
        {
            var dir = package.Directory;
            var name = package.Folder;
            Unload(package, log);
            log?.Invoke($"[重载] {name} ...");
            return LoadPackage(dir, name, log);
        }
        /// <summary>
        /// 早期版本会把加载器/DTO 复制到每个驱动目录,此处清理残留,避免旧版本 Contracts 干扰。
        /// 驱动目录本身不需要、也不应该包含宿主运行时文件。
        /// </summary>
        private static void WarnIfStaleRuntimeFiles(string dir, string name, Action<string> log)
        {
            foreach (var fileName in new[] { "MultiPlugin.Loader.dll", "MultiPlugin.Contracts.dll" })
            {
                var path = Path.Combine(dir, fileName);
                if (!File.Exists(path))
                {
                    continue;
                }
                try
                {
                    File.Delete(path);
                    log?.Invoke($"[清理] {name}: 已删除多余的 {fileName}(运行时统一使用宿主目录版本)");
                }
                catch (Exception ex)
                {
                    log?.Invoke($"[提示] {name}: 存在多余的 {fileName},已忽略({ex.Message})");
                }
            }
        }
    }
}
DeviceService/Entity/NotifyBase.cs
New file
@@ -0,0 +1,52 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace DeviceService.Entity
{
    public abstract class NotifyBase : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler? PropertyChanged;
        /// <summary>
        /// 设置字段值并触发通知。值与旧值相等(或同为 null)时不触发,返回是否真正赋值。
        /// </summary>
        protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
        {
            if (Equals(field, value))
            {
                return false;
            }
            field = value;
            OnPropertyChanged(propertyName);
            return true;
        }
        /// <summary>触发当前属性的通知,propertyName 缺省为调用方属性名。</summary>
        protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
        {
            RaiseChanged(propertyName);
        }
        /// <summary>显式触发指定属性的通知(如依赖属性联动)。</summary>
        protected void OnPropertyChanged(string propertyName, params string[] morePropertyNames)
        {
            RaiseChanged(propertyName);
            foreach (var name in morePropertyNames)
            {
                RaiseChanged(name);
            }
        }
        /// <summary>通知全部属性已变更(配合 DataTemplate 时刷新所有绑定,等效于 Text 通知)。</summary>
        protected void RaiseAllChanged()
        {
            RaiseChanged(string.Empty);
        }
        private void RaiseChanged(string? propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName ?? string.Empty));
        }
    }
}
DeviceService/LogWindow.xaml
New file
@@ -0,0 +1,42 @@
<Window x:Class="DeviceService.LogWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        Title="日志" Height="480" Width="760"
        Background="{StaticResource PageBackgroundBrush}">
    <Grid Margin="12">
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Border Grid.Row="0" Style="{StaticResource CardBorder}" Padding="4">
            <ListBox x:Name="LogList"
                     Background="Transparent"
                     BorderThickness="0"
                     FontFamily="Consolas"
                     FontSize="12.5"
                     ScrollViewer.HorizontalScrollBarVisibility="Auto" />
        </Border>
        <DockPanel Grid.Row="1" Margin="0,10,0,0">
            <Button x:Name="ClearButton"
                    DockPanel.Dock="Right"
                    Style="{StaticResource SecondaryButton}"
                    MinWidth="72"
                    Content="清空"
                    Click="ClearButton_Click" />
            <CheckBox x:Name="AutoScrollBox"
                      DockPanel.Dock="Right"
                      VerticalAlignment="Center"
                      Margin="0,0,12,0"
                      IsChecked="True"
                      Content="自动滚动" />
            <TextBlock x:Name="CountText"
                       VerticalAlignment="Center"
                       Foreground="{StaticResource TextSecondaryBrush}" />
        </DockPanel>
    </Grid>
</Window>
DeviceService/LogWindow.xaml.cs
New file
@@ -0,0 +1,76 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
using DeviceService.Logging;
namespace DeviceService;
/// <summary>
/// 日志窗口:展示 MemoryAppender 内存缓冲中的最近 1000 条日志,并实时追加新日志。
/// </summary>
public partial class LogWindow : Window
{
    private readonly ObservableCollection<LogEntry> _entries = new ObservableCollection<LogEntry>();
    public LogWindow()
    {
        InitializeComponent();
        LogList.ItemsSource = _entries;
        Loaded += LogWindow_Loaded;
        Closed += LogWindow_Closed;
    }
    private void LogWindow_Loaded(object sender, RoutedEventArgs e)
    {
        foreach (LogEntry entry in MemoryAppender.Current.Snapshot)
        {
            _entries.Add(entry);
        }
        UpdateCount();
        MemoryAppender.Current.EntryAppended += OnEntryAppended;
        ScrollToEnd();
    }
    private void OnEntryAppended(LogEntry entry)
    {
        // 日志事件可能来自后台线程,切回 UI 线程更新列表
        Dispatcher.BeginInvoke(new Action(() =>
        {
            _entries.Add(entry);
            while (_entries.Count > MemoryAppender.Current.Capacity)
            {
                _entries.RemoveAt(0);
            }
            UpdateCount();
            ScrollToEnd();
        }));
    }
    private void ClearButton_Click(object sender, RoutedEventArgs e)
    {
        MemoryAppender.Current.Clear();
        _entries.Clear();
        UpdateCount();
    }
    private void UpdateCount()
    {
        CountText.Text = $"共 {_entries.Count} 条(最多保留 {MemoryAppender.Current.Capacity} 条)";
    }
    private void ScrollToEnd()
    {
        if (AutoScrollBox.IsChecked == true && _entries.Count > 0)
        {
            LogList.ScrollIntoView(_entries.Last());
        }
    }
    private void LogWindow_Closed(object? sender, EventArgs e)
    {
        MemoryAppender.Current.EntryAppended -= OnEntryAppended;
    }
}
DeviceService/Logging/GlobalLogger.cs
New file
@@ -0,0 +1,63 @@
using DeviceService.Core.Api;
using log4net;
using System.Collections.Concurrent;
namespace DeviceService.Logging
{
    public class GlobalLogger : IServiceLogger
    {
        private static GlobalLogger? _logger;
        public static GlobalLogger Logger => _logger ??= new GlobalLogger();
        private static ConcurrentDictionary<string, ILog> _allLogger = new ConcurrentDictionary<string, ILog>();
        private GlobalLogger()
        {
        }
        private ILog GetLog(string source)
        {
            if (!_allLogger.ContainsKey(source))
            {
                _allLogger[source] = LogManager.GetLogger(source);
            }
            return _allLogger[source];
        }
        public void Debug(string source, string message)
        {
            GetLog(source).Debug(message);
        }
        public void Error(string source, string message, Exception ex = null)
        {
            if (ex == null)
            {
                GetLog(source).Error(message);
            }
            else
            {
                GetLog(source).Error(message, ex);
            }
        }
        public void Info(string source, string message)
        {
            GetLog(source).Info(message);
        }
        public void Warn(string source, string message, Exception ex = null)
        {
            if (ex == null)
            {
                GetLog(source).Warn(message);
            }
            else
            {
                GetLog(source).Warn(message, ex);
            }
        }
    }
}
DeviceService/Logging/MemoryAppender.cs
New file
@@ -0,0 +1,108 @@
using log4net;
using log4net.Appender;
using log4net.Core;
namespace DeviceService.Logging;
/// <summary>内存缓冲中的一条日志。</summary>
public sealed class LogEntry
{
    public DateTime Time { get; set; }
    public string Level { get; set; } = string.Empty;
    public string Logger { get; set; } = string.Empty;
    public string Message { get; set; } = string.Empty;
    public string Display => $"{Time:yyyy-MM-dd HH:mm:ss.fff} [{Level}] {Logger} - {Message}";
    public override string ToString() => Display;
}
/// <summary>
/// 自定义 Appender:只在内存环形缓冲中保留最近 <see cref="Capacity"/> 条日志,
/// 通过 <see cref="EntryAppended"/> 与 <see cref="Snapshot"/> 供日志窗口查看。
/// 文件输出由 log4net.config 中的 RollingFileAppender 负责,本 Appender 不写任何文件。
/// 实例由 log4net 依据 Config/log4net.config 创建(appender name="Memory")。
/// </summary>
public sealed class MemoryAppender : AppenderSkeleton
{
    /// <summary>内存中最多保留的日志条数,可由 log4net.config 的 capacity 参数覆盖。</summary>
    public int Capacity { get; set; } = 1000;
    /// <summary>log4net.config 中配置的 MemoryAppender 实例。</summary>
    public static MemoryAppender Current =>
        LogManager.GetRepository(typeof(MemoryAppender).Assembly)
            .GetAppenders()
            .OfType<MemoryAppender>()
            .FirstOrDefault()
        ?? throw new InvalidOperationException(
            "未找到 MemoryAppender,请检查 Config/log4net.config 中是否配置了 name=\"Memory\" 的 appender,并在 root 中引用。");
    private readonly object _sync = new object();
    private readonly Queue<LogEntry> _entries = new Queue<LogEntry>();
    /// <summary>新日志到达(可能在后台线程触发,UI 订阅方需切换回 UI 线程)。</summary>
    public event Action<LogEntry>? EntryAppended;
    /// <summary>当前内存中日志的快照。</summary>
    public IReadOnlyList<LogEntry> Snapshot
    {
        get
        {
            lock (_sync)
            {
                return _entries.ToList();
            }
        }
    }
    public MemoryAppender()
    {
        Name = "Memory";
    }
    /// <summary>清空内存缓冲(不影响文件)。</summary>
    public void Clear()
    {
        lock (_sync)
        {
            _entries.Clear();
        }
    }
    protected override void Append(LoggingEvent loggingEvent)
    {
        if (loggingEvent == null)
        {
            return;
        }
        string message = loggingEvent.RenderedMessage ?? string.Empty;
        if (loggingEvent.ExceptionObject != null)
        {
            message += Environment.NewLine + loggingEvent.ExceptionObject;
        }
        var entry = new LogEntry
        {
            Time = loggingEvent.TimeStamp,
            Level = loggingEvent.Level?.Name ?? string.Empty,
            Logger = loggingEvent.LoggerName,
            Message = message
        };
        lock (_sync)
        {
            _entries.Enqueue(entry);
            while (_entries.Count > Capacity)
            {
                _entries.Dequeue();
            }
            // Monitor 锁可重入,回调内部若调用 Snapshot/Clear 不会死锁
            EntryAppended?.Invoke(entry);
        }
    }
}
DeviceService/MainWindow.xaml
New file
@@ -0,0 +1,181 @@
<Window x:Class="DeviceService.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:DeviceService"
        xmlns:service="clr-namespace:DeviceService.Service"
        mc:Ignorable="d"
        Title="设备服务" Height="560" Width="960"
        Background="{StaticResource PageBackgroundBrush}">
    <Grid Margin="12">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="250" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <!-- 左侧:按 Type 分组的设备树 -->
        <Border Grid.Column="0" Style="{StaticResource CardBorder}" Margin="0,0,10,0">
            <DockPanel Margin="4">
                <TextBlock DockPanel.Dock="Top"
                           Style="{StaticResource TitleText}"
                           Text="设备列表"
                           Margin="6,2,0,10" />
                <TreeView x:Name="DeviceTree"
                          Background="Transparent"
                          BorderThickness="0"
                          ItemContainerStyle="{StaticResource DeviceTreeViewItem}"
                          SelectedItemChanged="DeviceTree_SelectedItemChanged">
                    <TreeView.Resources>
                        <HierarchicalDataTemplate DataType="{x:Type local:DeviceGroupNode}"
                                                  ItemsSource="{Binding Children}">
                            <StackPanel Orientation="Horizontal">
                                <Border Background="{StaticResource AccentFaintBrush}"
                                        CornerRadius="4"
                                        Padding="5,1"
                                        Margin="0,0,6,0">
                                    <TextBlock Text="{Binding Type}"
                                               FontSize="11"
                                               FontWeight="Bold"
                                               Foreground="{StaticResource AccentPressedBrush}" />
                                </Border>
                            </StackPanel>
                        </HierarchicalDataTemplate>
                        <DataTemplate DataType="{x:Type local:DeviceNode}">
                            <TextBlock Text="{Binding DisplayName}" TextTrimming="CharacterEllipsis" />
                        </DataTemplate>
                    </TreeView.Resources>
                </TreeView>
            </DockPanel>
        </Border>
        <!-- 右侧:工具栏 + 设备配置区域 -->
        <Grid Grid.Column="1">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="*" />
            </Grid.RowDefinitions>
            <DockPanel Grid.Row="0" Margin="0,0,0,10">
                <StackPanel Orientation="Horizontal" DockPanel.Dock="Right" VerticalAlignment="Center">
                    <!-- 服务状态徽标:绑定全局状态机 ServiceStateMachine.GlobalState.State -->
                    <Border x:Name="ServiceStateBadge"
                            CornerRadius="12"
                            Padding="12,5"
                            Margin="0,0,12,0"
                            VerticalAlignment="Center">
                        <Border.Style>
                            <Style TargetType="Border">
                                <Setter Property="Background" Value="#F3F4F6" />
                                <Style.Triggers>
                                    <DataTrigger Binding="{Binding State, Source={x:Static service:ServiceStateMachine.GlobalState}}"
                                                 Value="Running">
                                        <Setter Property="Background" Value="#D1FAE5" />
                                    </DataTrigger>
                                    <DataTrigger Binding="{Binding State, Source={x:Static service:ServiceStateMachine.GlobalState}}"
                                                 Value="Error">
                                        <Setter Property="Background" Value="#FEE2E2" />
                                    </DataTrigger>
                                </Style.Triggers>
                            </Style>
                        </Border.Style>
                        <TextBlock x:Name="ServiceStateText" FontSize="12.5" FontWeight="SemiBold">
                            <TextBlock.Style>
                                <Style TargetType="TextBlock">
                                    <Setter Property="Text" Value="未启动" />
                                    <Setter Property="Foreground" Value="{StaticResource TextSecondaryBrush}" />
                                    <Style.Triggers>
                                        <DataTrigger Binding="{Binding State, Source={x:Static service:ServiceStateMachine.GlobalState}}"
                                                     Value="Running">
                                            <Setter Property="Text" Value="运行中" />
                                            <Setter Property="Foreground" Value="#047857" />
                                        </DataTrigger>
                                        <DataTrigger Binding="{Binding State, Source={x:Static service:ServiceStateMachine.GlobalState}}"
                                                     Value="Error">
                                            <Setter Property="Text" Value="异常" />
                                            <Setter Property="Foreground" Value="#B91C1C" />
                                        </DataTrigger>
                                    </Style.Triggers>
                                </Style>
                            </TextBlock.Style>
                        </TextBlock>
                    </Border>
                    <!-- 启动/停止:Running 时文案变为“停止”,点击走停止逻辑 -->
                    <Button x:Name="StartServiceBtn"
                            MinWidth="92"
                            Click="StartService_Click">
                        <Button.Style>
                            <Style TargetType="Button" BasedOn="{StaticResource PrimaryButton}">
                                <Setter Property="Content" Value="启动" />
                                <Style.Triggers>
                                    <DataTrigger Binding="{Binding State, Source={x:Static service:ServiceStateMachine.GlobalState}}"
                                                 Value="Running">
                                        <Setter Property="Content" Value="停止" />
                                        <Setter Property="Background" Value="#B91C1C" />
                                    </DataTrigger>
                                </Style.Triggers>
                            </Style>
                        </Button.Style>
                    </Button>
                    <!-- 添加设备:服务运行中禁止操作 -->
                    <Button x:Name="AddDeviceButton"
                            MinWidth="92"
                            Margin="8,0,0,0"
                            Content="添加设备"
                            Click="AddDeviceButton_Click">
                        <Button.Style>
                            <Style TargetType="Button" BasedOn="{StaticResource PrimaryButton}">
                                <Style.Triggers>
                                    <DataTrigger Binding="{Binding State, Source={x:Static service:ServiceStateMachine.GlobalState}}"
                                                 Value="Running">
                                        <Setter Property="IsEnabled" Value="False" />
                                    </DataTrigger>
                                </Style.Triggers>
                            </Style>
                        </Button.Style>
                    </Button>
                    <!-- 刷新:服务运行中禁止操作 -->
                    <Button x:Name="RefreshButton"
                            MinWidth="72"
                            Margin="8,0,0,0"
                            Content="刷新"
                            Click="RefreshButton_Click">
                        <Button.Style>
                            <Style TargetType="Button" BasedOn="{StaticResource SecondaryButton}">
                                <Style.Triggers>
                                    <DataTrigger Binding="{Binding State, Source={x:Static service:ServiceStateMachine.GlobalState}}"
                                                 Value="Running">
                                        <Setter Property="IsEnabled" Value="False" />
                                    </DataTrigger>
                                </Style.Triggers>
                            </Style>
                        </Button.Style>
                    </Button>
                    <Button x:Name="DebugWindowButton"
                            Style="{StaticResource SecondaryButton}"
                            MinWidth="92"
                            Margin="8,0,0,0"
                            Content="调试窗口"
                            Click="DebugWindowButton_Click" />
                    <Button x:Name="LogWindowButton"
                            Style="{StaticResource SecondaryButton}"
                            MinWidth="72"
                            Margin="8,0,0,0"
                            Content="日志"
                            Click="LogWindowButton_Click" />
                </StackPanel>
                <TextBlock x:Name="HeaderText"
                           Style="{StaticResource SectionText}"
                           Text="设备配置"
                           VerticalAlignment="Center" />
            </DockPanel>
            <Border Grid.Row="1" Style="{StaticResource CardBorder}">
                <ContentControl x:Name="ConfigHost" Margin="4" Focusable="False" />
            </Border>
        </Grid>
    </Grid>
</Window>
DeviceService/MainWindow.xaml.cs
New file
@@ -0,0 +1,218 @@
using DeviceService.Core;
using DeviceService.Device;
using DeviceService.Driver;
using DeviceService.Logging;
using DeviceService.Service;
using System.Collections.ObjectModel;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Threading;
namespace DeviceService;
/// <summary>
/// 设备树分组节点(按 IDevice.Type 分组)。
/// </summary>
public class DeviceGroupNode
{
    public DeviceGroupNode(string type, IEnumerable<DeviceNode> children)
    {
        Type = type;
        Children = new ObservableCollection<DeviceNode>(children);
    }
    public string Type { get; }
    public ObservableCollection<DeviceNode> Children { get; }
}
/// <summary>
/// 设备树叶子节点。
/// </summary>
public class DeviceNode
{
    public DeviceNode(IDevice device)
    {
        Device = device;
    }
    public IDevice Device { get; }
    public string DisplayName => $"{Device.DeviceId}({Device.Brand})";
}
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
    private readonly DriverManager _manager = DriverManager.Instance;
    private readonly ObservableCollection<DeviceGroupNode> _groups = new ObservableCollection<DeviceGroupNode>();
    public MainWindow()
    {
        InitializeComponent();
        DeviceTree.ItemsSource = _groups;
        Loaded += MainWindow_Loaded;
    }
    private void MainWindow_Loaded(object sender, RoutedEventArgs e)
    {
        // 加载驱动包,供设备 Init 时查找驱动
        _manager.Scan(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Driver"), s => { });
        // 扫描 DeviceDesc 注解,填充 AllDeviceType
        DeviceHelper.ScanDeviceTypes();
        // 以数据库为准恢复设备列表(不调用 Init,避免启动即占用硬件)
        ReloadDeviceList();
        RefreshDeviceTree();
    }
    private void RefreshButton_Click(object sender, RoutedEventArgs e)
    {
        RefreshDeviceTree();
    }
    private void AddDeviceButton_Click(object sender, RoutedEventArgs e)
    {
        var dialog = new AddDeviceWindow { Owner = this };
        if (dialog.ShowDialog() != true || dialog.Device == null)
        {
            return;
        }
        IDevice device = dialog.Device;
        if (!DeviceHelper.SaveDevice(device))
        {
            HeaderText.Text = $"设备 {device.DeviceId} 保存失败";
            return;
        }
        // 保存成功后从数据库重新读取设备列表,而不是把弹窗里新建的实例直接塞进列表
        ReloadDeviceList();
        RefreshDeviceTree();
        HeaderText.Text = $"已添加设备 {device.DeviceId}";
    }
    /// <summary>以 Config\Devices.db 为准刷新内存中的设备列表(不调用 Init)。</summary>
    private static void ReloadDeviceList()
    {
        DeviceManager.Refresh(DeviceHelper.LoadDevices());
    }
    private void StartService_Click(object sender, RoutedEventArgs e)
    {
        // 运行中点击即为“停止”
        if (ServiceStateMachine.GlobalState.State == ServiceStatus.Running)
        {
            ServiceManager.Stop();
            HeaderText.Text = "服务已停止";
            return;
        }
        try
        {
            ServiceManager.Start();
        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.Message);
        }
    }
    private void DebugWindowButton_Click(object sender, RoutedEventArgs e)
    {
        var debug = new DebugWindow { Owner = this };
        debug.Show();
    }
    private void LogWindowButton_Click(object sender, RoutedEventArgs e)
    {
        var log = new LogWindow { Owner = this };
        log.Show();
    }
    private void DeviceTree_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
    {
        if (e.NewValue is DeviceNode node)
        {
            ShowDeviceConfig(node.Device);
        }
        else
        {
            ConfigHost.Content = null;
            HeaderText.Text = "设备配置";
        }
    }
    private void ShowDeviceConfig(IDevice device)
    {
        HeaderText.Text = $"{device.Type} - {device.DeviceId}";
        UserControl? control = DeviceConfigControlFactory.Create(device);
        if (control == null)
        {
            // 未按命名规则找到对应配置控件
            ConfigHost.Content = new TextBlock
            {
                Text = $"未找到 {device.GetType().Name}ConfigControl,无法显示该设备的配置界面。",
                Margin = new Thickness(8),
                TextWrapping = TextWrapping.Wrap,
                TextAlignment = TextAlignment.Center,
                VerticalAlignment = VerticalAlignment.Center
            };
            return;
        }
        control.DataContext = device;
        ConfigHost.Content = control;
    }
    private void RefreshDeviceTree()
    {
        string? selectedId = (DeviceTree.SelectedItem as DeviceNode)?.Device.DeviceId;
        _groups.Clear();
        foreach (var group in DeviceManager.Devices
                     .GroupBy(d => d.Type)
                     .OrderBy(g => g.Key, StringComparer.Ordinal))
        {
            _groups.Add(new DeviceGroupNode(group.Key, group.Select(d => new DeviceNode(d))));
        }
        // 容器在本次布局之后才生成,延后展开,确保新增节点能显示出来
        Dispatcher.BeginInvoke(new Action(ExpandAllGroups), DispatcherPriority.Loaded);
    }
    /// <summary>展开所有分组节点。</summary>
    private void ExpandAllGroups()
    {
        if (DeviceTree.ItemContainerGenerator.Status != GeneratorStatus.ContainersGenerated)
        {
            EventHandler? handler = null;
            handler = (s, e) =>
            {
                if (DeviceTree.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
                {
                    DeviceTree.ItemContainerGenerator.StatusChanged -= handler;
                    ExpandAllGroups();
                }
            };
            DeviceTree.ItemContainerGenerator.StatusChanged += handler;
            return;
        }
        for (int i = 0; i < DeviceTree.Items.Count; i++)
        {
            if (DeviceTree.ItemContainerGenerator.ContainerFromIndex(i) is TreeViewItem item)
            {
                item.IsExpanded = true;
            }
        }
    }
}
DeviceService/ServiceManager/IClient.cs
New file
@@ -0,0 +1,8 @@
namespace DeviceService.Service
{
    public interface IClient
    {
        bool Send(string data, out string msg);
        bool Send(byte[] data, out string msg);
    }
}
DeviceService/ServiceManager/IService.cs
New file
@@ -0,0 +1,23 @@
using DeviceService.Core;
namespace DeviceService.Service
{
    public class RequestArg
    {
        public RequestData Data { get; set; }
        public string Raw { get; set; }
        public IClient Client { get; set; }
    }
    public interface IService : IDisposable
    {
        event EventHandler<IClient> ClientConnected;
        event EventHandler<IClient> ClientDisConnected;
        event EventHandler<RequestArg> DataReceived;
        bool Start(out string msg);
        bool Stop(out string msg);
    }
}
DeviceService/ServiceManager/NamePipe/NamePipeClient.cs
New file
@@ -0,0 +1,180 @@
using System.IO;
using System.IO.Pipes;
using System.Text;
namespace DeviceService.Service.NamePipe
{
    /// <summary>
    /// 命名管道客户端连接封装。
    /// 每个实例对应管道服务端上的一条已建立连接,收发均通过 <see cref="StreamReader"/> / <see cref="StreamWriter"/> 完成。
    /// 协议约定:每条消息为一行 UTF-8 文本(以 \n 结尾),发送端单次写入“内容+换行”,
    /// 使 Message 模式下一条消息恰好是完整的一行。
    /// </summary>
    public class NamePipeClient : IClient, IDisposable
    {
        /// <summary>单条消息允许的最大字节数(16MB)。</summary>
        internal const int MaxMessageLength = 16 * 1024 * 1024;
        /// <summary>等待发送锁的超时时间(毫秒)。</summary>
        private const int SendLockTimeout = 5000;
        /// <summary>消息行结束符。StreamWriter 每次写入都会触发 AutoFlush,必须与内容在同一次 Write 中写入。</summary>
        private const char LineEnding = '\n';
        private readonly NamedPipeServerStream _stream;
        private readonly StreamWriter writer;
        private readonly StreamReader reader;
        private readonly SemaphoreSlim _sendLock = new SemaphoreSlim(1, 1);
        private volatile bool _disposed;
        public NamePipeClient(NamedPipeServerStream serverStream)
        {
            var encoding = new UTF8Encoding(false);
            _stream = serverStream;
            reader = new StreamReader(serverStream, encoding, false, 1024, true);
            writer = new StreamWriter(serverStream, encoding, 1024, true) { AutoFlush = true };
        }
        /// <summary>连接是否仍然可用。</summary>
        public bool IsConnected
        {
            get
            {
                if (_disposed)
                {
                    return false;
                }
                return _stream.IsConnected;
            }
        }
        public bool Send(string data, out string msg)
        {
            if (data == null)
            {
                msg = "待发送的数据不能为空。";
                return false;
            }
            return SendLine(data, Encoding.UTF8.GetByteCount(data) + 1, out msg);
        }
        public bool Send(byte[] data, out string msg)
        {
            if (data == null)
            {
                msg = "待发送的数据不能为空。";
                return false;
            }
            // 负载按 UTF-8 文本处理,与行协议保持一致
            return SendLine(Encoding.UTF8.GetString(data), data.Length + 1, out msg);
        }
        /// <summary>
        /// 写入一行消息。内容与换行符必须单次 Write 写入,配合 AutoFlush 才能成为一条完整消息。
        /// </summary>
        private bool SendLine(string text, int byteLength, out string msg)
        {
            if (byteLength > MaxMessageLength)
            {
                msg = $"数据长度 {byteLength} 字节超过单条消息上限 {MaxMessageLength} 字节。";
                return false;
            }
            if (_disposed)
            {
                msg = "管道连接已关闭,无法发送数据。";
                return false;
            }
            if (!IsConnected)
            {
                msg = "管道连接已断开,无法发送数据。";
                return false;
            }
            bool lockTaken = false;
            try
            {
                lockTaken = _sendLock.Wait(SendLockTimeout);
                if (!lockTaken)
                {
                    msg = "发送超时:上一条数据仍未写出。";
                    return false;
                }
                // writer 非 thread-safe,统一由 _sendLock 串行化
                writer.Write(text + LineEnding);
                writer.Flush();
                msg = string.Empty;
                return true;
            }
            catch (Exception ex)
            {
                msg = $"发送失败:{ex.Message}";
                return false;
            }
            finally
            {
                if (lockTaken)
                {
                    _sendLock.Release();
                }
            }
        }
        /// <summary>
        /// 读取一行消息。返回 null 表示对端已关闭连接。
        /// </summary>
        internal async Task<string?> ReadMessageAsync()
        {
            if (_disposed)
            {
                return null;
            }
            string? line = await reader.ReadLineAsync().ConfigureAwait(false);
            return line;
        }
        public void Dispose()
        {
            if (_disposed)
            {
                return;
            }
            _disposed = true;
            try
            {
                reader.Dispose();
            }
            catch
            {
                // 忽略关闭异常
            }
            try
            {
                writer.Dispose();
            }
            catch
            {
                // 忽略关闭异常
            }
            try
            {
                _stream.Dispose();
            }
            catch
            {
                // 关闭过程中的异常可以忽略
            }
            _sendLock.Dispose();
        }
    }
}
DeviceService/ServiceManager/NamePipe/NamePipeService.cs
New file
@@ -0,0 +1,424 @@
using DeviceService.Core;
using System.IO;
using System.IO.Pipes;
using System.Text.Json;
namespace DeviceService.Service.NamePipe
{
    /// <summary>
    /// 基于命名管道的服务端实现(Message 模式)。
    /// 启动后持续监听客户端连接,收到的每条消息通过 <see cref="DataReceived"/> 事件上抛,
    /// 并携带可回写数据的 <see cref="NamePipeClient"/> 实例。
    /// </summary>
    public class NamePipeService : IService, IDisposable
    {
        /// <summary>默认允许的最大管道实例数(即最大并发连接数)。</summary>
        public const int DefaultMaxInstances = 254;
        /// <summary>停止服务时等待后台任务结束的最长时间。</summary>
        private static readonly TimeSpan StopTimeout = TimeSpan.FromSeconds(3);
        private readonly string _pipeName;
        private readonly int _maxInstances;
        private readonly object _sync = new object();
        private readonly Dictionary<NamePipeClient, Task> _clients = new Dictionary<NamePipeClient, Task>();
        private CancellationTokenSource? _cts;
        private NamedPipeServerStream? _listenStream;
        private Task? _acceptTask;
        private bool _running;
        private bool _disposed;
        /// <param name="pipeName">管道名称,为空时在 Start 阶段校验失败。</param>
        /// <param name="maxInstances">最大并发连接数。</param>
        public NamePipeService(string? pipeName = null, int maxInstances = DefaultMaxInstances)
        {
            if (maxInstances <= 0)
            {
                throw new ArgumentOutOfRangeException(nameof(maxInstances), "最大实例数必须大于 0。");
            }
            _pipeName = pipeName ?? string.Empty;
            _maxInstances = maxInstances;
        }
        public event EventHandler<IClient>? ClientConnected;
        public event EventHandler<IClient>? ClientDisConnected;
        public event EventHandler<RequestArg>? DataReceived;
        /// <summary>管道名称。</summary>
        public string PipeName => _pipeName;
        /// <summary>服务是否处于监听状态。</summary>
        public bool IsRunning
        {
            get
            {
                lock (_sync)
                {
                    return _running;
                }
            }
        }
        /// <summary>当前已连接的客户端数量。</summary>
        public int ClientCount
        {
            get
            {
                lock (_sync)
                {
                    return _clients.Count;
                }
            }
        }
        public bool Start(out string msg)
        {
            lock (_sync)
            {
                if (_disposed)
                {
                    msg = "服务已释放,无法启动。";
                    return false;
                }
                if (_running)
                {
                    msg = "服务已处于运行状态。";
                    return false;
                }
                if (string.IsNullOrWhiteSpace(_pipeName))
                {
                    msg = "管道名称未设置,无法启动。";
                    return false;
                }
                NamedPipeServerStream listenStream;
                try
                {
                    listenStream = CreatePipeStream();
                }
                catch (Exception ex)
                {
                    msg = $"创建命名管道 \"{_pipeName}\" 失败:{ex.Message}";
                    return false;
                }
                var cts = new CancellationTokenSource();
                _cts = cts;
                _listenStream = listenStream;
                _running = true;
                _acceptTask = Task.Run(() => AcceptLoopAsync(listenStream, cts.Token));
            }
            msg = $"命名管道 \"{_pipeName}\" 已启动监听。";
            return true;
        }
        public bool Stop(out string msg)
        {
            Task? acceptTask;
            List<Task> clientTasks = new List<Task>();
            lock (_sync)
            {
                if (!_running)
                {
                    msg = "服务未运行。";
                    return false;
                }
                _running = false;
                acceptTask = _acceptTask;
                _acceptTask = null;
                try
                {
                    _cts?.Cancel();
                }
                catch
                {
                    // 忽略取消过程中的异常
                }
                try
                {
                    _listenStream?.Dispose();
                }
                catch
                {
                    // 忽略关闭监听流的异常
                }
                _listenStream = null;
                // 关闭所有客户端连接,使阻塞中的读取立即结束
                foreach (KeyValuePair<NamePipeClient, Task> item in _clients)
                {
                    try
                    {
                        item.Key.Dispose();
                    }
                    catch
                    {
                        // 忽略单个连接的关闭异常
                    }
                    clientTasks.Add(item.Value);
                }
                _clients.Clear();
                _cts?.Dispose();
                _cts = null;
            }
            try
            {
                acceptTask?.Wait(StopTimeout);
                if (clientTasks.Count > 0)
                {
                    Task.WaitAll(clientTasks.ToArray(), StopTimeout);
                }
            }
            catch (AggregateException)
            {
                // 后台任务在校验/取消阶段抛出的异常无需向上传播
            }
            msg = "服务已停止。";
            return true;
        }
        private NamedPipeServerStream CreatePipeStream()
        {
            var pipe = new NamedPipeServerStream(
                _pipeName,
                PipeDirection.InOut,
                _maxInstances,
                PipeTransmissionMode.Message,
                PipeOptions.Asynchronous);
            pipe.ReadMode = PipeTransmissionMode.Message;
            return pipe;
        }
        /// <summary>
        /// 连接接受循环:每接受一个连接就交给独立的处理任务,并准备下一个管道实例继续监听。
        /// </summary>
        private async Task AcceptLoopAsync(NamedPipeServerStream initial, CancellationToken token)
        {
            NamedPipeServerStream? listenStream = initial;
            try
            {
                while (!token.IsCancellationRequested)
                {
                    NamedPipeServerStream stream = listenStream!;
                    listenStream = null;
                    try
                    {
                        await stream.WaitForConnectionAsync(token).ConfigureAwait(false);
                    }
                    catch (OperationCanceledException)
                    {
                        stream.Dispose();
                        break;
                    }
                    catch (ObjectDisposedException)
                    {
                        break;
                    }
                    catch (IOException)
                    {
                        stream.Dispose();
                        break;
                    }
                    // .NET Framework 下 WaitForConnectionAsync 可能提前返回,此处校验连接确实建立
                    if (!stream.IsConnected)
                    {
                        try
                        {
                            await Task.Delay(50, token).ConfigureAwait(false);
                        }
                        catch (OperationCanceledException)
                        {
                            break;
                        }
                        listenStream = stream;
                        continue;
                    }
                    var client = new NamePipeClient(stream);
                    Task handler = Task.Run(() => HandleClientAsync(client, token));
                    RegisterClient(client, handler);
                    // 为下一个连接准备新的管道实例
                    try
                    {
                        listenStream = CreatePipeStream();
                    }
                    catch (Exception)
                    {
                        break;
                    }
                    lock (_sync)
                    {
                        if (!_running)
                        {
                            break;
                        }
                        _listenStream = listenStream;
                    }
                }
            }
            catch (Exception)
            {
                // 监听循环中的未知异常直接结束循环,由 Stop 负责收尾
            }
            finally
            {
                try
                {
                    listenStream?.Dispose();
                }
                catch
                {
                    // 忽略关闭异常
                }
                lock (_sync)
                {
                    _listenStream = null;
                }
            }
        }
        /// <summary>
        /// 单个连接的读取循环:按行读取消息并触发 <see cref="DataReceived"/>。
        /// </summary>
        private async Task HandleClientAsync(NamePipeClient client, CancellationToken token)
        {
            OnClientEvent(ClientConnected, client);
            try
            {
                while (!token.IsCancellationRequested)
                {
                    string? message = await client.ReadMessageAsync().ConfigureAwait(false);
                    if (message == null)
                    {
                        break;
                    }
                    RaiseDataReceived(client, message);
                }
            }
            catch (OperationCanceledException)
            {
                // 服务停止导致的正常取消
            }
            catch (Exception)
            {
                // 连接断开、数据非法等情况直接结束该连接的读取
            }
            finally
            {
                OnClientEvent(ClientDisConnected, client);
                UnregisterClient(client);
                client.Dispose();
            }
        }
        private void OnClientEvent(EventHandler<IClient>? handler, IClient client)
        {
            if (handler == null)
            {
                return;
            }
            try
            {
                handler(this, client);
            }
            catch
            {
                // 订阅方的异常不应影响连接的建立与释放
            }
        }
        private void RaiseDataReceived(NamePipeClient client, string raw)
        {
            var data = JsonSerializer.Deserialize<RequestData>(raw);
            var arg = new RequestArg
            {
                Data = data,
                Raw = raw,
                Client = client
            };
            OnDataReceived(arg);
        }
        private void RegisterClient(NamePipeClient client, Task handler)
        {
            lock (_sync)
            {
                if (_running)
                {
                    _clients[client] = handler;
                    return;
                }
            }
            // 服务正在停止,直接释放该连接
            client.Dispose();
        }
        private void UnregisterClient(NamePipeClient client)
        {
            lock (_sync)
            {
                _clients.Remove(client);
            }
        }
        private void OnDataReceived(RequestArg arg)
        {
            EventHandler<RequestArg>? handler = DataReceived;
            if (handler == null)
            {
                return;
            }
            try
            {
                handler?.Invoke(this, arg);
            }
            catch
            {
                // 订阅方的异常不应中断管道读取
            }
        }
        public void Dispose()
        {
            if (_disposed)
            {
                return;
            }
            try
            {
                Stop(out _);
            }
            catch
            {
                // 释放阶段的异常忽略
            }
            _disposed = true;
        }
    }
}
DeviceService/ServiceManager/ServiceManager.cs
New file
@@ -0,0 +1,130 @@
using DeviceService.Device;
using DeviceService.Entity;
using DeviceService.Logging;
using DeviceService.Service.NamePipe;
using System.Windows;
namespace DeviceService.Service
{
    public enum ServiceStatus
    {
        Idle = 0,
        Running = 1,
        Error = 2,
    }
    public class ServiceStateMachine : NotifyBase
    {
        public static ServiceStateMachine GlobalState = new ServiceStateMachine();
        private ServiceStateMachine()
        {
        }
        private ServiceStatus _state;
        public ServiceStatus State
        {
            get => _state;
            set
            {
                SetProperty(ref _state, value);
            }
        }
        private bool _isRunning;
        public bool IsRunning
        {
            get => _isRunning;
            set
            {
                SetProperty(ref _isRunning, value);
            }
        }
    }
    public class ServiceManager
    {
        private static IService _serviceImpl;
        private const string PipeName = "DeviceService";
        public static void Start()
        {
            if (ServiceStateMachine.GlobalState.State == ServiceStatus.Running)
            {
                MessageBox.Show("已启动");
                return;
            }
            _serviceImpl = new NamePipeService(PipeName);
            _serviceImpl.DataReceived += PipeService_DataReceived;
            _serviceImpl.ClientConnected += PipeService_ClientConnected;
            _serviceImpl.ClientDisConnected += PipeService_ClientDisConnected;
            string err = string.Empty;
            bool allDone = true;
            foreach (var device in DeviceHelper.AllDevices)
            {
                if (!DeviceManager.Register(device, out string fail))
                {
                    allDone = false;
                    err += $"{fail}\r\n";
                }
            }
            if (!allDone && MessageBox.Show(err, "提示", MessageBoxButton.OKCancel) == MessageBoxResult.Cancel)
            {
                DeviceManager.UnRegisterAll();
                ServiceStateMachine.GlobalState.State = ServiceStatus.Error;
                return;
            }
            if (!_serviceImpl.Start(out string msg))
            {
                GlobalLogger.Logger.Error("ServiceManager", $"[服务] 管道服务启动失败:{msg}");
                Stop();
                // Stop 会把状态置为 Idle,这里再标记为异常,供界面显示
                ServiceStateMachine.GlobalState.State = ServiceStatus.Error;
                throw new Exception(msg);
            }
            ServiceStateMachine.GlobalState.State = ServiceStatus.Running;
        }
        public static void Stop()
        {
            if (_serviceImpl == null)
            {
                return;
            }
            _serviceImpl.DataReceived -= PipeService_DataReceived;
            _serviceImpl.ClientConnected -= PipeService_ClientConnected;
            _serviceImpl.ClientDisConnected -= PipeService_ClientDisConnected;
            _serviceImpl.Stop(out _);
            _serviceImpl.Dispose();
            _serviceImpl = null;
            ServiceStateMachine.GlobalState.State = ServiceStatus.Idle;
        }
        private static void PipeService_DataReceived(object? sender, RequestArg e)
        {
            // 数据到达事件来自后台线程,需要切换到 UI 线程显示
            DeviceManager.HandleClientMessage(e);
        }
        private static void PipeService_ClientConnected(object? sender, IClient client)
        {
            // 连接/断开事件来自后台线程,列表更新需切换到 UI 线程
        }
        private static void PipeService_ClientDisConnected(object? sender, IClient client)
        {
        }
    }
}