From e57938fbda9bff702004038f76d009422968bff5 Mon Sep 17 00:00:00 2001
From: alielie <alielie@git.com>
Date: 星期五, 06 十一月 2026 18:05:07 +0800
Subject: [PATCH] 提交代码
---
DeviceService.Core/IDeviceManager.cs | 13
DeviceService/DriverManager/DriverManager.cs | 411 ++++
DeviceService/LogWindow.xaml.cs | 76
.gitignore | 206 ++
DeviceService/ServiceManager/IService.cs | 23
DeviceService.Core/DeviceService.Core.csproj | 55
DeviceService.Core/Api/IPLCDriver.cs | 11
DeviceService/DeviceManager/CanDevice.cs | 178 +
DeviceService/DriverManager/DriverDispatchProxy.cs | 84
DeviceService.sln | 35
DeviceService/ServiceManager/NamePipe/NamePipeClient.cs | 180 +
DeviceService/ServiceManager/NamePipe/NamePipeService.cs | 424 ++++
DeviceService/DeviceConfigControlFactory.cs | 36
DeviceService/Logging/GlobalLogger.cs | 63
DeviceService.Core/Entity/MessageData.cs | 26
DeviceService/DriverManager/DriverInstance.cs | 74
DeviceService/Entity/NotifyBase.cs | 52
DeviceService/Config/log4net.config | 45
DeviceService/App.xaml.cs | 12
DeviceService/ServiceManager/ServiceManager.cs | 130 +
DeviceService.Core/Api/ICanDriver.cs | 11
DeviceService/DeviceManager/DeviceDesc.cs | 29
DeviceService/DriverManager/DriverDatabase.cs | 257 ++
DeviceService/Logging/MemoryAppender.cs | 108 +
DeviceService/DebugWindow.xaml | 72
DeviceService/DriverManager/DriverLoader.cs | 784 ++++++++
DeviceService.Core/DriverDesc.cs | 22
DeviceService.Core/Properties/AssemblyInfo.cs | 33
DeviceService/MainWindow.xaml | 181 +
DeviceService/ServiceManager/IClient.cs | 8
DeviceService/MainWindow.xaml.cs | 218 ++
DeviceService/DeviceControl/CanDeviceConfigControl.xaml.cs | 17
DeviceService/App.xaml | 268 ++
DeviceService/DeviceService.csproj | 29
DeviceService/AddDeviceWindow.xaml.cs | 97 +
DeviceService/DeviceControl/CanDeviceConfigControl.xaml | 52
DeviceService/AddDeviceWindow.xaml | 56
DeviceService/DeviceManager/DeviceDatabase.cs | 328 +++
DeviceService.Core/IDevice.cs | 46
DeviceService/DeviceManager/PLCDevice.cs | 138 +
DeviceService/AssemblyInfo.cs | 12
DeviceService/DeviceManager/DeviceManager.cs | 126 +
DeviceService.Core/Api/IModbusDriver.cs | 17
DeviceService/LogWindow.xaml | 42
DeviceService/DeviceManager/DeviceHelper.cs | 168 +
DeviceService.Core/Api/IServiceLogger.cs | 12
DeviceService/DebugWindow.xaml.cs | 156 +
47 files changed, 5,402 insertions(+), 19 deletions(-)
diff --git a/.gitignore b/.gitignore
index 4d40434..561c35a 100644
--- a/.gitignore
+++ b/.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/
diff --git a/DeviceService.Core/Api/ICanDriver.cs b/DeviceService.Core/Api/ICanDriver.cs
new file mode 100644
index 0000000..300ff1b
--- /dev/null
+++ b/DeviceService.Core/Api/ICanDriver.cs
@@ -0,0 +1,11 @@
+锘縩amespace 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);
+ }
+}
diff --git a/DeviceService.Core/Api/IModbusDriver.cs b/DeviceService.Core/Api/IModbusDriver.cs
new file mode 100644
index 0000000..5ddce84
--- /dev/null
+++ b/DeviceService.Core/Api/IModbusDriver.cs
@@ -0,0 +1,17 @@
+锘縰sing 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);
+ }
+}
diff --git a/DeviceService.Core/Api/IPLCDriver.cs b/DeviceService.Core/Api/IPLCDriver.cs
new file mode 100644
index 0000000..2713447
--- /dev/null
+++ b/DeviceService.Core/Api/IPLCDriver.cs
@@ -0,0 +1,11 @@
+锘縩amespace 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);
+ }
+}
diff --git a/DeviceService.Core/Api/IServiceLogger.cs b/DeviceService.Core/Api/IServiceLogger.cs
new file mode 100644
index 0000000..c713495
--- /dev/null
+++ b/DeviceService.Core/Api/IServiceLogger.cs
@@ -0,0 +1,12 @@
+锘縰sing 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);
+ }
+}
diff --git a/DeviceService.Core/DeviceService.Core.csproj b/DeviceService.Core/DeviceService.Core.csproj
new file mode 100644
index 0000000..d333cea
--- /dev/null
+++ b/DeviceService.Core/DeviceService.Core.csproj
@@ -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>
\ No newline at end of file
diff --git a/DeviceService.Core/DriverDesc.cs b/DeviceService.Core/DriverDesc.cs
new file mode 100644
index 0000000..3043104
--- /dev/null
+++ b/DeviceService.Core/DriverDesc.cs
@@ -0,0 +1,22 @@
+锘縰sing 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;
+ }
+ }
+}
diff --git a/DeviceService.Core/Entity/MessageData.cs b/DeviceService.Core/Entity/MessageData.cs
new file mode 100644
index 0000000..e737e08
--- /dev/null
+++ b/DeviceService.Core/Entity/MessageData.cs
@@ -0,0 +1,26 @@
+锘縰sing 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; }
+ }
+}
diff --git a/DeviceService.Core/IDevice.cs b/DeviceService.Core/IDevice.cs
new file mode 100644
index 0000000..4b1c29b
--- /dev/null
+++ b/DeviceService.Core/IDevice.cs
@@ -0,0 +1,46 @@
+锘縰sing 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);
+ }
+}
diff --git a/DeviceService.Core/IDeviceManager.cs b/DeviceService.Core/IDeviceManager.cs
new file mode 100644
index 0000000..6c23b2a
--- /dev/null
+++ b/DeviceService.Core/IDeviceManager.cs
@@ -0,0 +1,13 @@
+锘縰sing 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);
+ }
+}
diff --git a/DeviceService.Core/Properties/AssemblyInfo.cs b/DeviceService.Core/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..4743609
--- /dev/null
+++ b/DeviceService.Core/Properties/AssemblyInfo.cs
@@ -0,0 +1,33 @@
+锘縰sing 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")]
diff --git a/DeviceService.sln b/DeviceService.sln
new file mode 100644
index 0000000..b6e70ce
--- /dev/null
+++ b/DeviceService.sln
@@ -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
diff --git a/DeviceService/AddDeviceWindow.xaml b/DeviceService/AddDeviceWindow.xaml
new file mode 100644
index 0000000..4a1aebc
--- /dev/null
+++ b/DeviceService/AddDeviceWindow.xaml
@@ -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>
diff --git a/DeviceService/AddDeviceWindow.xaml.cs b/DeviceService/AddDeviceWindow.xaml.cs
new file mode 100644
index 0000000..78777da
--- /dev/null
+++ b/DeviceService/AddDeviceWindow.xaml.cs
@@ -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);
+ }
+}
diff --git a/DeviceService/App.xaml b/DeviceService/App.xaml
new file mode 100644
index 0000000..581ce26
--- /dev/null
+++ b/DeviceService/App.xaml
@@ -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>
diff --git a/DeviceService/App.xaml.cs b/DeviceService/App.xaml.cs
new file mode 100644
index 0000000..2c65f18
--- /dev/null
+++ b/DeviceService/App.xaml.cs
@@ -0,0 +1,12 @@
+锘縰sing 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 鍧囧湪閰嶇疆鏂囦欢涓畾涔夛級銆�
+}
diff --git a/DeviceService/AssemblyInfo.cs b/DeviceService/AssemblyInfo.cs
new file mode 100644
index 0000000..6997d7d
--- /dev/null
+++ b/DeviceService/AssemblyInfo.cs
@@ -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)]
diff --git a/DeviceService/Config/log4net.config b/DeviceService/Config/log4net.config
new file mode 100644
index 0000000..36a9ac1
--- /dev/null
+++ b/DeviceService/Config/log4net.config
@@ -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 鏉★紝渚涙棩蹇楃獥鍙f煡鐪嬶紝涓嶅啓鏂囦欢 -->
+ <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>
\ No newline at end of file
diff --git a/DeviceService/DebugWindow.xaml b/DeviceService/DebugWindow.xaml
new file mode 100644
index 0000000..66c4c11
--- /dev/null
+++ b/DeviceService/DebugWindow.xaml
@@ -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>
diff --git a/DeviceService/DebugWindow.xaml.cs b/DeviceService/DebugWindow.xaml.cs
new file mode 100644
index 0000000..f268ad2
--- /dev/null
+++ b/DeviceService/DebugWindow.xaml.cs
@@ -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)
+ {
+ }
+}
diff --git a/DeviceService/DeviceConfigControlFactory.cs b/DeviceService/DeviceConfigControlFactory.cs
new file mode 100644
index 0000000..bbe1dc2
--- /dev/null
+++ b/DeviceService/DeviceConfigControlFactory.cs
@@ -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);
+ }
+ }
+ }
+}
diff --git a/DeviceService/DeviceControl/CanDeviceConfigControl.xaml b/DeviceService/DeviceControl/CanDeviceConfigControl.xaml
new file mode 100644
index 0000000..2fa8fc8
--- /dev/null
+++ b/DeviceService/DeviceControl/CanDeviceConfigControl.xaml
@@ -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>
diff --git a/DeviceService/DeviceControl/CanDeviceConfigControl.xaml.cs b/DeviceService/DeviceControl/CanDeviceConfigControl.xaml.cs
new file mode 100644
index 0000000..8f2d046
--- /dev/null
+++ b/DeviceService/DeviceControl/CanDeviceConfigControl.xaml.cs
@@ -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();
+ }
+}
diff --git a/DeviceService/DeviceManager/CanDevice.cs b/DeviceService/DeviceManager/CanDevice.cs
new file mode 100644
index 0000000..bc44a13
--- /dev/null
+++ b/DeviceService/DeviceManager/CanDevice.cs
@@ -0,0 +1,178 @@
+锘縰sing 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
+ };
+ }
+
+ }
+ }
+}
diff --git a/DeviceService/DeviceManager/DeviceDatabase.cs b/DeviceService/DeviceManager/DeviceDatabase.cs
new file mode 100644
index 0000000..08270e4
--- /dev/null
+++ b/DeviceService/DeviceManager/DeviceDatabase.cs
@@ -0,0 +1,328 @@
+using DeviceService.Core;
+using DeviceService.Logging;
+using System.Data.SQLite;
+using System.IO;
+
+namespace DeviceService.Device
+{
+ /// <summary>璁惧璁板綍锛圖evice 琛ㄧ殑涓�琛岋級銆�</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锛欼d 涓鸿嚜澧炰富閿紝DeviceId 涓哄敮涓�涓氬姟鏍囪瘑锛孴ype 鐢ㄤ簬瀹氫綅璁惧瀹炵幇绫�
+ ///
+ /// 鎵�鏈夋搷浣滃け璐ヤ粎閫氳繃 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銆丏eviceId 鎺掑簭锛夈��</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>鏃у簱锛圖eviceId 浣滀富閿級鍗囩骇涓� 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);
+ }
+ }
+}
diff --git a/DeviceService/DeviceManager/DeviceDesc.cs b/DeviceService/DeviceManager/DeviceDesc.cs
new file mode 100644
index 0000000..00fb61e
--- /dev/null
+++ b/DeviceService/DeviceManager/DeviceDesc.cs
@@ -0,0 +1,29 @@
+锘縰sing 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;
+ }
+ }
+}
diff --git a/DeviceService/DeviceManager/DeviceHelper.cs b/DeviceService/DeviceManager/DeviceHelper.cs
new file mode 100644
index 0000000..3a11021
--- /dev/null
+++ b/DeviceService/DeviceManager/DeviceHelper.cs
@@ -0,0 +1,168 @@
+using DeviceService.Core;
+using DeviceService.Logging;
+using System.Reflection;
+
+namespace DeviceService.Device
+{
+ /// <summary>
+ /// 璁惧淇℃伅鎸佷箙鍖栬緟鍔╋細璐熻矗璁惧淇℃伅涓� Config\Devices.db 涔嬮棿鐨勮鍐欍��
+ /// DeviceManager 鍙礋璐f湇鍔℃寮忓惎鍔ㄥ悗鐨勮澶囩鐞嗭紝鏁版嵁搴撹鍐欎竴寰嬬粡鐢辨湰绫汇��
+ /// </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"/> 鐨勭被锛�
+ /// 灏嗘敞瑙d腑鐨� 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);
+ }
+ }
+}
diff --git a/DeviceService/DeviceManager/DeviceManager.cs b/DeviceService/DeviceManager/DeviceManager.cs
new file mode 100644
index 0000000..392aba8
--- /dev/null
+++ b/DeviceService/DeviceManager/DeviceManager.cs
@@ -0,0 +1,126 @@
+锘縰sing DeviceService.Core;
+using DeviceService.Service;
+using System.Text.Encodings.Web;
+using System.Text.Json;
+using System.Text.Unicode;
+
+namespace DeviceService.Device
+{
+ /// <summary>
+ /// 鏈嶅姟姝e紡鍚姩鍚庣殑璁惧绠$悊锛氳澶囧疄渚嬬殑鍐呭瓨闆嗗悎銆佺被鍨嬫壂鎻忎笌娉ㄥ唽锛圧egister锛夈��
+ /// 璁惧淇℃伅鐨勬暟鎹簱璇诲啓鐢� <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 = "鏃犳晥鐨凞eviceId"
+ };
+ arg.Client.Send(JsonSerializer.Serialize(res, SafeOptions), out _);
+ }
+ }
+
+ public static void ReleaseClient(IClient client) {
+
+ }
+ }
+}
diff --git a/DeviceService/DeviceManager/PLCDevice.cs b/DeviceService/DeviceManager/PLCDevice.cs
new file mode 100644
index 0000000..04efea0
--- /dev/null
+++ b/DeviceService/DeviceManager/PLCDevice.cs
@@ -0,0 +1,138 @@
+锘縰sing 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 = "鏈疄鐜�"
+ };
+ }
+ }
+}
diff --git a/DeviceService/DeviceService.csproj b/DeviceService/DeviceService.csproj
new file mode 100644
index 0000000..ed6e541
--- /dev/null
+++ b/DeviceService/DeviceService.csproj
@@ -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>
diff --git a/DeviceService/DriverManager/DriverDatabase.cs b/DeviceService/DriverManager/DriverDatabase.cs
new file mode 100644
index 0000000..8b67373
--- /dev/null
+++ b/DeviceService/DriverManager/DriverDatabase.cs
@@ -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锛氬凡鍔犺浇鐨勯┍鍔ㄥ寘锛圖river 鏍圭洰褰曚笅鐨勪竴涓瓙鏂囦欢澶癸級
+ /// - 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();
+ }
+ }
+}
diff --git a/DeviceService/DriverManager/DriverDispatchProxy.cs b/DeviceService/DriverManager/DriverDispatchProxy.cs
new file mode 100644
index 0000000..183e096
--- /dev/null
+++ b/DeviceService/DriverManager/DriverDispatchProxy.cs
@@ -0,0 +1,84 @@
+using System.Reflection;
+
+namespace DeviceService.Driver
+{
+ /// <summary>
+ /// 鍩轰簬 DispatchProxy 鐨勯┍鍔ㄥ姩鎬佷唬鐞嗭紙.NET Framework 4.7.1+ 鍐呯疆 API锛夈��
+ ///
+ /// 鐢ㄦ硶锛氬涓昏嚜瀹氫箟涓�涓�"楦瓙鎺ュ彛"锛堟柟娉曞悕銆佸弬鏁扮被鍨嬩笌椹卞姩鏂规硶涓�涓�瀵瑰簲锛夛紝
+ /// 椹卞姩鏃犻渶瀹炵幇璇ユ帴鍙c�佹棤闇�寮曠敤瀹夸富绋嬪簭闆嗐�傛帴鍙h皟鐢ㄦ寜"鏂规硶鍚� + 鍙傛暟涓暟"
+ /// 鍖归厤鍒伴┍鍔ㄦ柟娉曪紝缁� <see cref="DriverInstance"/>锛圡BR 閫忔槑浠g悊锛夎浆鍙戝埌
+ /// 椹卞姩鎵�鍦ㄧ殑 AppDomain 鍐呭弽灏勬墽琛屻��
+ ///
+ /// 涓� RealProxy 鐨勫尯鍒細
+ /// - 鐢熸垚鐨勪唬鐞嗘槸瀹夸富鍩熷唴鐨勬櫘閫氬璞★紝涓嶅啀鏈� Remoting 鐩稿叧鐨勬�紓琛屼负
+ /// 锛圤bject.GetType/ToString 绛夊熀绫绘柟娉曚笉浼氳璇矾鐢憋紝鏃犻渶鎷︽埅锛夛紱
+ /// - out/ref 鍙傛暟閫氳繃鐩存帴鏀瑰啓 args 鏁扮粍鍥炲~锛�
+ /// - 寮傚父鐩存帴鎶涘嚭锛屼笉鍐嶇粡杩� ReturnMessage 鍖呰锛�
+ /// - API 鍦� .NET Core / .NET 5+ 鍚屾牱鍙敤锛屽涓绘湭鏉ュ崌绾у彲鏃犵紳杩佺Щ銆�
+ ///
+ /// 绾︽潫锛歴
+ /// - T 蹇呴』鏄帴鍙o紙DispatchProxy 鍙拡瀵规帴鍙g敓鎴愪唬鐞嗙被锛夛紱
+ /// - 鎺ュ彛鏂规硶鐨勫弬鏁�/杩斿洖鍊煎繀椤诲彲璺ㄥ煙灏侀�侊紙鍩哄厓绫诲瀷銆乻tring銆乕Serializable] DTO锛夈��
+ /// </summary>
+ // 娉ㄦ剰锛氫笉鑳� sealed 鈥斺�� DispatchProxy 鍦ㄨ繍琛屾椂鐢熸垚鍏舵淳鐢熺被浣滀负浠g悊绫诲瀷
+ public class DriverDispatchProxy : DispatchProxy
+ {
+ private DriverInstance _instance;
+
+ /// <summary>
+ /// 鍒涘缓瀹炵幇 T 鐨勯┍鍔ㄥ姩鎬佷唬鐞嗭紙T 涓哄涓昏嚜瀹氫箟鐨勯腑瀛愭帴鍙o級銆�
+ /// </summary>
+ public static T Create<T>(DriverInstance instance) where T : class
+ {
+ if (!typeof(T).IsInterface)
+ {
+ throw new ArgumentException(
+ $"T 蹇呴』鏄帴鍙o紙椹卞姩绫诲瀷涓嶅彲璺ㄥ煙灏侀�侊紝瀹夸富涔熸棤娉曞紩鐢ㄥ巶鍟嗙▼搴忛泦锛夛紝瀹為檯涓� {typeof(T)}",
+ nameof(T));
+ }
+
+ // DispatchProxy.Create<T, TProxy> 瑕佹眰 TProxy 涓洪潪娉涘瀷绫伙紝
+ // 鍥犳浠g悊瀹炰緥鐨勫垱寤轰笌鐘舵�佹敞鍏ュ垎涓ゆ瀹屾垚
+ 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("鏃犳硶瑙f瀽鎺ュ彛鏂规硶锛堝彲鑳界敱娉涘瀷鏂规硶璋冪敤寮曡捣锛�");
+ }
+
+ 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;
+ }
+ }
+}
diff --git a/DeviceService/DriverManager/DriverInstance.cs b/DeviceService/DriverManager/DriverInstance.cs
new file mode 100644
index 0000000..de337a8
--- /dev/null
+++ b/DeviceService/DriverManager/DriverInstance.cs
@@ -0,0 +1,74 @@
+namespace DeviceService.Driver
+{
+ /// <summary>
+ /// 鐢� DriverManager 鍒涘缓骞剁粺涓�绠$悊鐨勯┍鍔ㄥ疄渚嬩唬鐞嗐��
+ ///
+ /// 椹卞姩瀵硅薄鏈韩娲诲湪椹卞姩鍖呯殑 AppDomain 涓紙WPF/鏅�氱被鍧囦笉鍙法鍩熷皝閫侊級锛�
+ /// 鍥犳瀹夸富鎷垮埌鐨勬槸鏈被鐨勯�忔槑浠g悊锛氬涓昏皟鐢� <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>
+ /// 鐩存帴璋冪敤锛堝師濮嬪璞″弬鏁�/杩斿洖鍊硷級锛屼緵鍔ㄦ�佷唬鐞嗕娇鐢ㄣ��
+ /// 娉ㄦ剰锛氬弬鏁颁笌杩斿洖鍊煎繀椤诲彲璺ㄥ煙灏侀�侊紙鍩哄厓绫诲瀷銆乻tring銆乕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);
+ }
+ }
+}
diff --git a/DeviceService/DriverManager/DriverLoader.cs b/DeviceService/DriverManager/DriverLoader.cs
new file mode 100644
index 0000000..fa2c731
--- /dev/null
+++ b/DeviceService/DriverManager/DriverLoader.cs
@@ -0,0 +1,784 @@
+using System.IO;
+using System.Reflection;
+
+namespace DeviceService.Driver
+{
+ /// <summary>
+ /// 椹卞姩鐨勭▼搴忛泦鎻忚堪锛堣法 AppDomain 鎸夊�煎皝閫侊級銆�
+ /// 娉ㄦ剰锛氳繖浜� DTO 鍙湇鍔′簬"瀹夸富 <-> 鍔犺浇鍣�"涔嬮棿鐨勯�氫俊锛�
+ /// 椹卞姩 dll 涓嶉渶瑕佸紩鐢ㄦ湰绋嬪簭闆嗭紝涔熶笉闇�瑕佸疄鐜颁换浣曟帴鍙c��
+ /// </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銆佷覆鍙o級</summary>
+ public string DriverType { get; set; }
+
+ /// <summary>娉ㄨВ涓殑"鍝佺墝"锛堝 BA銆丅B锛�</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 鍔ㄦ�佷唬鐞嗘妸椹卞姩璋冪敤閫傞厤鎴愬己绫诲瀷鎺ュ彛鏂规硶銆�
+ /// 娉ㄦ剰锛歏alue / ArgsOut 鍙兘鎼哄甫鍙法鍩熷皝閫佺殑鏁版嵁锛堝熀鍏冪被鍨嬨�乻tring銆乕Serializable] DTO锛夈��
+ /// </summary>
+ [Serializable]
+ public sealed class InvokeDirectResult
+ {
+ public bool Success { get; set; }
+
+ /// <summary>鏂规硶杩斿洖鍊硷紙void 鏂规硶涓� null锛�</summary>
+ public object Value { get; set; }
+
+ /// <summary>璋冪敤鍚庣殑鍙傛暟鏁扮粍锛坥ut/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>椹卞姩绱㈠紩锛�(绫诲瀷, 鍝佺墝) 鈫� 椹卞姩淇℃伅锛宬ey 褰㈠ "CAN|BA"</summary>
+ private readonly Dictionary<string, DriverTypeInfo> _driverIndex =
+ new Dictionary<string, DriverTypeInfo>(StringComparer.OrdinalIgnoreCase);
+
+ // 鏃犻檺鐢熷瓨鏈燂紝閬垮厤杩滅▼浠g悊琚洖鏀�
+ 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锛屽彧鎸�"灞炴�у悕"瑙h�﹁鍙栵紝
+ /// 杩欐牱鍘傚晢 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 浼犳潵鐨勫瓧绗︿覆杞崲涓虹洰鏍囧弬鏁扮被鍨嬨��
+ /// 鏀寔甯哥敤鍩哄厓绫诲瀷銆佹灇涓俱�丟uid銆丏ateTime銆佸瓧绗︿覆鏁扮粍锛堜互鍒嗗彿鍒嗛殧锛夈��
+ /// </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} 鏆備笉鏀寔浠庡瓧绗︿覆杞崲锛堟敮鎸侊細瀛楃涓层�佹暟瀛椼�乥ool銆佹灇涓俱�丟uid銆丏ateTime銆佹暟缁勶級");
+ }
+
+ 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>
+ /// 渚濊禆瑙f瀽鍏滃簳锛氬彧鍦ㄦ湰椹卞姩鐩綍锛堝強鍔犺浇鍣ㄦ墍鍦ㄧ洰褰曪級鍐呮煡鎵撅紝
+ /// 淇濊瘉涓嶄細涓插埌鍏朵粬椹卞姩鍖呯殑鍚屽悕绋嬪簭闆嗐��
+ /// </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
+ }
+}
diff --git a/DeviceService/DriverManager/DriverManager.cs b/DeviceService/DriverManager/DriverManager.cs
new file mode 100644
index 0000000..242ac4f
--- /dev/null
+++ b/DeviceService/DriverManager/DriverManager.cs
@@ -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>杩愯鍦ㄨ鍩熷唴鐨勫姞杞藉櫒浠g悊</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 涓嶉渶瑕佸疄鐜版帴鍙c�佷笉闇�瑕佸紩鐢ㄥ涓荤▼搴忛泦锛�
+ /// - 瀹夸富鍙�氳繃鍙嶅皠鏋氫妇绫诲瀷/鏂规硶銆佸垱寤哄疄渚嬨�佽皟鐢ㄦ柟娉曪紱
+ /// - 姣忎釜椹卞姩鍖呰嚜甯﹁嚜宸辩殑渚濊禆锛屼簰涓嶅共鎵帮紙鍙瓨鍦ㄥ悓鍚嶄笉鍚岀増鏈級锛�
+ /// - 闇�瑕佹浛鎹㈤┍鍔ㄦ枃浠舵椂鍏堝嵏杞藉煙锛屾枃浠朵笉浼氳閿佸畾锛堝奖瀛愬鍒讹級銆�
+ /// </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 缁熶竴鐧昏绠$悊鐨勯┍鍔ㄥ疄渚嬶紙璺ㄥ煙閫忔槑浠g悊锛�</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 鍐�
+ /// 鍒涘缓涓�涓柊瀹炰緥骞朵互閫忔槑浠g悊褰㈠紡杩斿洖锛岀敱 DriverManager 缁熶竴鐧昏绠$悊銆�
+ ///
+ /// 璇存槑锛氶┍鍔ㄧ被鍨嬫湰韬笉鍙法鍩熷皝閫侊紙闈� MarshalByRefObject锛屽涓讳篃涓嶅紩鐢ㄥ巶鍟嗙▼搴忛泦锛夛紝
+ /// 鍥犳鏃犳硶鐢ㄦ硾鍨� T 杩斿洖"鍘熺敓绫诲瀷"锛涘涓讳竴寰嬮�氳繃 <see cref="DriverInstance"/> 浠g悊鎿嶄綔锛�
+ /// 姣忔璋冪敤瀹為檯鎵ц鍙戠敓鍦ㄩ┍鍔ㄨ嚜宸辩殑鍩熷唴銆�
+ /// </summary>
+ /// <param name="driverType">娉ㄨВ涓殑"绫诲瀷"锛屽 CAN锛涚暀绌轰负閫氶厤</param>
+ /// <param name="brand">娉ㄨВ涓殑"鍝佺墝"锛屽 BA锛涚暀绌轰负閫氶厤</param>
+ /// <param name="log">鏃ュ織鍥炶皟锛堝彲绌猴級</param>
+ /// <returns>椹卞姩瀹炰緥浠g悊锛涙湭鎵惧埌鎴栧垱寤哄け璐ヨ繑鍥� 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>
+ /// 娉涘瀷鐗堟湰锛歍 涓哄涓昏嚜瀹氫箟鐨�"楦瓙鎺ュ彛"锛堟柟娉曞悕銆佸弬鏁扮被鍨嬩笌椹卞姩鏂规硶涓�涓�瀵瑰簲锛夈��
+ /// 杩斿洖 DispatchProxy 鍔ㄦ�佷唬鐞嗭細鎺ュ彛璋冪敤鎸�"鏂规硶鍚� + 鍙傛暟涓暟"杞彂鍒伴┍鍔ㄥ煙鍐呭弽灏勬墽琛屻��
+ ///
+ /// 椹卞姩鏃犻渶瀹炵幇 T銆佹棤闇�寮曠敤瀹夸富绋嬪簭闆嗏�斺�旇繖姝f槸鍔ㄦ�佷唬鐞嗙殑鎰忎箟锛�
+ /// 涓嶅悓鍘傚晢鐨勯┍鍔ㄥ彲浠ュ叡浜涓诲畾涔夌殑涓�濂楁帴鍙g害瀹氥��
+ ///
+ /// 绾︽潫锛�
+ /// - T 蹇呴』鏄帴鍙o紙DispatchProxy 鍙拡瀵规帴鍙g敓鎴愪唬鐞嗙被锛夛紱
+ /// - 鎺ュ彛鏂规硶鐨勫弬鏁�/杩斿洖鍊煎繀椤诲彲璺ㄥ煙灏侀�侊紙鍩哄厓绫诲瀷銆乻tring銆乕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($"[浠g悊] {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>
+ /// 鍗歌浇鏌愪釜椹卞姩鍖呭墠锛屽厛閫氳繃浠g悊閲婃斁鍏跺悕涓嬫墍鏈夋墭绠″疄渚嬶紙鍩熷嵏杞藉悗浠g悊浼氬け鏁堬級銆�
+ /// </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)
+ {
+ // 鍏堥�氳繃浠g悊閲婃斁鎵�鏈夋墭绠″疄渚嬶紝鍩熷嵏杞藉悗浠g悊灏嗗け鏁�
+ 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,
+ // 鍏抽敭锛氭帰娴嬫牴鍥哄畾涓鸿椹卞姩鐩綍锛屼緷璧栧彧浼氫粠鑷繁鐩綍瑙f瀽
+ ApplicationBase = dir,
+ // 褰卞瓙澶嶅埗锛歞ll 鍔犺浇鍚庝笉閿佸畾锛屽彲闅忔椂鍗歌浇/鏇挎崲鏂囦欢
+ 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}锛�");
+ }
+ }
+ }
+ }
+}
diff --git a/DeviceService/Entity/NotifyBase.cs b/DeviceService/Entity/NotifyBase.cs
new file mode 100644
index 0000000..e8f3be9
--- /dev/null
+++ b/DeviceService/Entity/NotifyBase.cs
@@ -0,0 +1,52 @@
+锘縰sing 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>瑙﹀彂褰撳墠灞炴�х殑閫氱煡锛宲ropertyName 缂虹渷涓鸿皟鐢ㄦ柟灞炴�у悕銆�</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));
+ }
+ }
+}
diff --git a/DeviceService/LogWindow.xaml b/DeviceService/LogWindow.xaml
new file mode 100644
index 0000000..93c7743
--- /dev/null
+++ b/DeviceService/LogWindow.xaml
@@ -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>
diff --git a/DeviceService/LogWindow.xaml.cs b/DeviceService/LogWindow.xaml.cs
new file mode 100644
index 0000000..1e3d5d9
--- /dev/null
+++ b/DeviceService/LogWindow.xaml.cs
@@ -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;
+ }
+}
diff --git a/DeviceService/Logging/GlobalLogger.cs b/DeviceService/Logging/GlobalLogger.cs
new file mode 100644
index 0000000..6b693de
--- /dev/null
+++ b/DeviceService/Logging/GlobalLogger.cs
@@ -0,0 +1,63 @@
+锘縰sing 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);
+ }
+ }
+ }
+}
diff --git a/DeviceService/Logging/MemoryAppender.cs b/DeviceService/Logging/MemoryAppender.cs
new file mode 100644
index 0000000..2460cf4
--- /dev/null
+++ b/DeviceService/Logging/MemoryAppender.cs
@@ -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"/> 渚涙棩蹇楃獥鍙f煡鐪嬨��
+/// 鏂囦欢杈撳嚭鐢� log4net.config 涓殑 RollingFileAppender 璐熻矗锛屾湰 Appender 涓嶅啓浠讳綍鏂囦欢銆�
+/// 瀹炰緥鐢� log4net 渚濇嵁 Config/log4net.config 鍒涘缓锛坅ppender 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);
+ }
+ }
+}
diff --git a/DeviceService/MainWindow.xaml b/DeviceService/MainWindow.xaml
new file mode 100644
index 0000000..aeaa9f2
--- /dev/null
+++ b/DeviceService/MainWindow.xaml
@@ -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>
+
+ <!-- 鍚姩/鍋滄锛歊unning 鏃舵枃妗堝彉涓衡�滃仠姝⑩�濓紝鐐瑰嚮璧板仠姝㈤�昏緫 -->
+ <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>
diff --git a/DeviceService/MainWindow.xaml.cs b/DeviceService/MainWindow.xaml.cs
new file mode 100644
index 0000000..3ca094d
--- /dev/null
+++ b/DeviceService/MainWindow.xaml.cs
@@ -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;
+ }
+ }
+ }
+}
diff --git a/DeviceService/ServiceManager/IClient.cs b/DeviceService/ServiceManager/IClient.cs
new file mode 100644
index 0000000..5064374
--- /dev/null
+++ b/DeviceService/ServiceManager/IClient.cs
@@ -0,0 +1,8 @@
+锘縩amespace DeviceService.Service
+{
+ public interface IClient
+ {
+ bool Send(string data, out string msg);
+ bool Send(byte[] data, out string msg);
+ }
+}
diff --git a/DeviceService/ServiceManager/IService.cs b/DeviceService/ServiceManager/IService.cs
new file mode 100644
index 0000000..14dce73
--- /dev/null
+++ b/DeviceService/ServiceManager/IService.cs
@@ -0,0 +1,23 @@
+锘縰sing 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);
+ }
+}
diff --git a/DeviceService/ServiceManager/NamePipe/NamePipeClient.cs b/DeviceService/ServiceManager/NamePipe/NamePipeClient.cs
new file mode 100644
index 0000000..e092026
--- /dev/null
+++ b/DeviceService/ServiceManager/NamePipe/NamePipeClient.cs
@@ -0,0 +1,180 @@
+锘縰sing 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>娑堟伅琛岀粨鏉熺銆係treamWriter 姣忔鍐欏叆閮戒細瑙﹀彂 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();
+ }
+ }
+}
diff --git a/DeviceService/ServiceManager/NamePipe/NamePipeService.cs b/DeviceService/ServiceManager/NamePipe/NamePipeService.cs
new file mode 100644
index 0000000..5efa826
--- /dev/null
+++ b/DeviceService/ServiceManager/NamePipe/NamePipeService.cs
@@ -0,0 +1,424 @@
+锘縰sing DeviceService.Core;
+using System.IO;
+using System.IO.Pipes;
+using System.Text.Json;
+
+namespace DeviceService.Service.NamePipe
+{
+ /// <summary>
+ /// 鍩轰簬鍛藉悕绠¢亾鐨勬湇鍔$瀹炵幇锛圡essage 妯″紡锛夈��
+ /// 鍚姩鍚庢寔缁洃鍚鎴风杩炴帴锛屾敹鍒扮殑姣忔潯娑堟伅閫氳繃 <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;
+ }
+ }
+
+ // 鏈嶅姟姝e湪鍋滄锛岀洿鎺ラ噴鏀捐杩炴帴
+ 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;
+ }
+ }
+}
diff --git a/DeviceService/ServiceManager/ServiceManager.cs b/DeviceService/ServiceManager/ServiceManager.cs
new file mode 100644
index 0000000..a5acb4e
--- /dev/null
+++ b/DeviceService/ServiceManager/ServiceManager.cs
@@ -0,0 +1,130 @@
+锘縰sing 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)
+ {
+
+ }
+ }
+}
--
Gitblit v1.9.1