Compare commits
2
Commits
b15e57d150
...
479eae64a1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
479eae64a1 | ||
|
|
4973979169 |
No files matched your search
+4
-4
@@ -2,17 +2,17 @@
|
||||
|
||||
## Project Structure & Module Organization
|
||||
- Solution: `Sharp.sln` aggregates all modules in this folder.
|
||||
- Libraries: `Common`, `Config`, `Log`, `EventBus`, `Sharp.Infrastructure`.
|
||||
- Libraries: `Common`, `Config`, `Log`, `Cache`, `EventBus`, `Data`, `NetServer`, `NetClient`, `Ports`, `Plc`, `Plc.Siemens`, `Process`, `Python`, `Wpf`, `Wpf.HandyControl`, `Wpf.DevTools`.
|
||||
- Networking: `NetServer` (HTTP server + middleware), `NetClient` (typed HTTP client).
|
||||
- UI: `Wpf` (shared WPF helpers), `Client` (WPF app using `Wpf`).
|
||||
- UI: `Wpf` (shared WPF helpers), `Wpf.HandyControl`, `Wpf.DevTools`, `Sharp.Demo` (WPF demo app).
|
||||
- Examples: `NetServer/Examples` contains a runnable sample entry point.
|
||||
- Tests: solution includes external projects under `../../test/*` (e.g., `Common.Test`, `NetClient.Test`).
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
- Restore: `dotnet restore`
|
||||
- Build (Debug/Release): `dotnet build Sharp.sln -c Debug` | `-c Release`
|
||||
- Run server example: `dotnet run --project NetServer/NetServer.csproj`
|
||||
- Run WPF client (Windows): `dotnet run --project Client/Client.csproj`
|
||||
- Run server example: `dotnet run --project NetServer/Examples/NetServer.Examples.csproj`
|
||||
- Run WPF demo (Windows): `dotnet run --project Sharp.Demo/Sharp.Demo.csproj`
|
||||
- Run all tests: `dotnet test Sharp.sln -c Debug --no-build`
|
||||
- Run a single test project: `dotnet test ../../test/Common.Test/Common.Test.csproj`
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<RootNamespace>Sharp.Cache</RootNamespace>
|
||||
|
||||
<!-- 程序集元数据 -->
|
||||
<Description>Sharp Framework 缓存库 - 内存缓存抽象和实现(规划中)</Description>
|
||||
<Description>Sharp Framework 缓存库 - 内存缓存抽象和实现</Description>
|
||||
|
||||
<!-- NuGet 包元数据 -->
|
||||
<PackageTags>sharp;cache;memory-cache;caching</PackageTags>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# Sharp.Cache
|
||||
|
||||
> **🚧 开发中 / Under Construction**
|
||||
|
||||
轻量级内存缓存模块,基于 `Microsoft.Extensions.Caching.Memory`,提供简洁的缓存API。
|
||||
|
||||
## 📋 为什么需要 Cache?
|
||||
@@ -72,7 +70,7 @@ public class ProductService {
|
||||
|
||||
---
|
||||
|
||||
## 🚀 快速开始(未来)
|
||||
## 🚀 快速开始
|
||||
|
||||
### 1. 注册服务
|
||||
|
||||
@@ -646,7 +644,5 @@ public async Task<Result<Product>> GetProductAsync(int id) {
|
||||
|
||||
---
|
||||
|
||||
**状态**:设计阶段,未实现
|
||||
**预计实现时间**:根据实际需求决定
|
||||
**实现难度**:低(100行代码即可完成核心功能)
|
||||
**依赖**:Microsoft.Extensions.Caching.Memory(.NET内置)
|
||||
**状态**:已实现(内存缓存)
|
||||
**当前实现**:IMemoryCache 封装 + 绝对/滑动过期 + 前缀移除
|
||||
@@ -0,0 +1,22 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Authors Condition="'$(Authors)' == ''">duyl328</Authors>
|
||||
<PackageProjectUrl Condition="'$(PackageProjectUrl)' == ''">https://seyl.me</PackageProjectUrl>
|
||||
<PackageLicenseFile Condition="'$(PackageLicenseFile)' == ''">LICENSE</PackageLicenseFile>
|
||||
<PackageReadmeFile Condition="'$(PackageReadmeFile)' == ''">README.md</PackageReadmeFile>
|
||||
<PackageRequireLicenseAcceptance Condition="'$(PackageRequireLicenseAcceptance)' == ''">false</PackageRequireLicenseAcceptance>
|
||||
<Version Condition="'$(Version)' == ''">1.0.0</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="$(MSBuildThisFileDirectory)../../LICENSE"
|
||||
Pack="true"
|
||||
PackagePath=""
|
||||
Link="LICENSE"
|
||||
Condition="Exists('$(MSBuildThisFileDirectory)../../LICENSE')" />
|
||||
<None Update="README.md"
|
||||
Pack="true"
|
||||
PackagePath=""
|
||||
Condition="Exists('$(MSBuildProjectDirectory)/README.md')" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -8,7 +8,7 @@
|
||||
<RootNamespace>Sharp.EventBus</RootNamespace>
|
||||
|
||||
<!-- 程序集元数据 -->
|
||||
<Description>Sharp Framework 事件总线库 - 进程内事件发布/订阅(规划中)</Description>
|
||||
<Description>Sharp Framework 事件总线库 - 进程内事件发布/订阅</Description>
|
||||
|
||||
<!-- NuGet 包元数据 -->
|
||||
<PackageTags>sharp;eventbus;pub-sub;events</PackageTags>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# Sharp.EventBus
|
||||
|
||||
> **🚧 开发中 / Under Construction**
|
||||
|
||||
轻量级进程内事件总线,用于解耦应用程序中的模块通信。
|
||||
|
||||
## 📋 为什么需要 EventBus?
|
||||
@@ -74,7 +72,7 @@ public class PlcDataLogger {
|
||||
|
||||
---
|
||||
|
||||
## 🚀 快速开始(未来)
|
||||
## 🚀 快速开始
|
||||
|
||||
### 1. 注册服务
|
||||
|
||||
@@ -472,6 +470,5 @@ System.Threading.Channels.Channel<EventEnvelope>
|
||||
|
||||
---
|
||||
|
||||
**状态**:设计阶段,未实现
|
||||
**预计实现时间**:根据实际需求决定
|
||||
**当前替代方案**:使用 Common 模块的 Global 容器 + 委托事件
|
||||
**状态**:已实现(进程内 InMemoryEventBus)
|
||||
**当前实现**:基于 Channel 的异步队列 + 订阅优先级/过滤
|
||||
+7
-7
@@ -1,7 +1,7 @@
|
||||
# Sharp Project Context
|
||||
|
||||
## Project Overview
|
||||
**Sharp** is a modular C# .NET 8 solution designed for industrial automation and network services. It features a "Spring Boot-like" HTTP server framework, a robust PLC communication library, and WPF-based client/server applications. The architecture emphasizes dependency injection, functional error handling (Result pattern), and clean separation of concerns.
|
||||
**Sharp** is a modular C# .NET 8 solution designed for industrial automation and network services. It features a "Spring Boot-like" HTTP server framework, a robust PLC communication library, and WPF UI libraries with a demo application. The architecture emphasizes dependency injection, functional error handling (Result pattern), and clean separation of concerns.
|
||||
|
||||
## Key Modules
|
||||
|
||||
@@ -20,9 +20,10 @@
|
||||
- **Ports**: Serial/COM port abstractions.
|
||||
|
||||
### Applications (UI)
|
||||
- **Client**: Main WPF Client application.
|
||||
- **Server**: WPF Server application (likely a dashboard or local control interface).
|
||||
- **Sharp.Demo**: WPF demo application showcasing key modules.
|
||||
- **Wpf**: Shared WPF UI logic, behaviors, and converters.
|
||||
- **Wpf.HandyControl**: HandyControl integration and dialog services.
|
||||
- **Wpf.DevTools**: WPF developer tooling (visual tree/log/perf).
|
||||
|
||||
## Development Workflow
|
||||
|
||||
@@ -39,14 +40,13 @@
|
||||
```bash
|
||||
dotnet build Sharp.sln -c Debug
|
||||
```
|
||||
- **Run WPF Client**:
|
||||
- **Run WPF Demo**:
|
||||
```bash
|
||||
dotnet run --project Client/Client.csproj
|
||||
dotnet run --project Sharp.Demo/Sharp.Demo.csproj
|
||||
```
|
||||
- **Run NetServer Example**:
|
||||
```bash
|
||||
dotnet run --project NetServer/Examples
|
||||
# Note: Check specific project path if Examples is a folder or project
|
||||
dotnet run --project NetServer/Examples/NetServer.Examples.csproj
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
@@ -5,6 +5,7 @@ using Serilog;
|
||||
using Serilog.Events;
|
||||
using Serilog.Formatting.Json;
|
||||
using Sharp.Log.Options;
|
||||
using System.Text;
|
||||
|
||||
namespace Sharp.Log.Internal;
|
||||
|
||||
@@ -47,6 +48,7 @@ internal static class SerilogConfigurator
|
||||
// Sinks
|
||||
if (options.UseConsole)
|
||||
{
|
||||
TrySetConsoleEncoding();
|
||||
if (options.ConsoleJson)
|
||||
{
|
||||
cfg = cfg.WriteTo.Async(a => a.Console(new JsonFormatter(renderMessage: true)));
|
||||
@@ -94,6 +96,19 @@ internal static class SerilogConfigurator
|
||||
return cfg.CreateLogger();
|
||||
}
|
||||
|
||||
private static void TrySetConsoleEncoding()
|
||||
{
|
||||
try
|
||||
{
|
||||
Console.OutputEncoding = Encoding.UTF8;
|
||||
Console.InputEncoding = Encoding.UTF8;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore environments without a console.
|
||||
}
|
||||
}
|
||||
|
||||
private static LogEventLevel MapLevel(LogLevel level)
|
||||
{
|
||||
return level switch
|
||||
@@ -133,4 +148,4 @@ internal static class SerilogConfigurator
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>Sharp.NetServer.Examples</AssemblyName>
|
||||
<RootNamespace>Sharp.NetServer.Examples</RootNamespace>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\NetServer.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -18,6 +18,10 @@
|
||||
<ProjectReference Include="..\Config\Config.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="Examples/**"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0"/>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0"/>
|
||||
|
||||
+14
-14
@@ -10,11 +10,11 @@
|
||||
- **[Sharp.Common](Common/README.md)**: 基础工具库。提供轻量级 DI 容器 (`Global`)、函数式结果模式 (`Result<T>`)、以及丰富的扩展方法(String, DateTime, Encryption, Reflection)。
|
||||
- **[Sharp.Config](Config/README.md)**: 强大的配置管理。支持 JSON/XML/INI/TOML 多源聚合、强类型绑定和数据注解验证。
|
||||
- **[Sharp.Log](Log/README.md)**: 统一日志门面。基于 Serilog 封装,支持一行代码接入,提供结构化日志和文件滚动策略。
|
||||
- **[Sharp.EventBus](EventBus/README.md)**: (开发中) 轻量级事件总线。
|
||||
- **[Sharp.EventBus](EventBus/README.md)**: 轻量级事件总线。
|
||||
- **[Sharp.Cache](Cache/README.md)**: 轻量级内存缓存抽象与实现。
|
||||
|
||||
### 💾 数据与存储
|
||||
- **[Sharp.Data](Data/README.md)**: 极简数据访问层。基于 EF Core,提供开箱即用的仓储模式、自动审计(创建人/时间)、软删除和分页支持。
|
||||
- **[Sharp.Repository](Repository/README.md)**: 数据库集成模块。通过配置切换 SQL Server/MySQL/PostgreSQL/Sqlite,支持自动迁移。
|
||||
- **[Sharp.Data](Data/README.md)**: 极简数据访问层。基于 EF Core,提供仓储模式、自动审计(创建人/时间)、软删除和分页支持,兼容多种数据库。
|
||||
|
||||
### 🌐 网络与通信
|
||||
- **[Sharp.NetServer](NetServer/README.md)**: 轻量级 Web 框架。基于 `HttpListener`,提供类似 ASP.NET Core 的 Controller/Route/Middleware 开发体验,适合嵌入式或桌面应用内嵌 Web 服务。
|
||||
@@ -27,8 +27,13 @@
|
||||
|
||||
### 🖥️ 桌面应用 UI
|
||||
- **[Sharp.Wpf](Wpf/README.md)**: WPF 基础库。提供 MVVM 基类、全局异常捕获、常用转换器、对话框服务和行为扩展。
|
||||
- **[Sharp.Client](Client/README.md)**: 综合客户端示例。集成 PLC 监控、串口调试和用户登录功能的工业控制台应用。
|
||||
- **[Sharp.Server](Server/README.md)**: 服务器控制台示例。展示了如何在一个 WPF 应用中托管 NetServer Web 服务,并提供可视化管理界面。
|
||||
- **[Sharp.Wpf.HandyControl](Wpf.HandyControl/README.md)**: HandyControl 组件库集成与对话框服务。
|
||||
- **[Sharp.Wpf.DevTools](Wpf.DevTools/README.md)**: WPF 开发者工具库(可视化树、日志、性能监控)。
|
||||
- **[Sharp.Demo](Sharp.Demo/README.md)**: 综合演示应用,覆盖主要模块用法。
|
||||
|
||||
### 🧩 工具与集成
|
||||
- **[Sharp.Process](Process/README.md)**: 外部进程执行、管理和监控。
|
||||
- **[Sharp.Python](Python/README.md)**: Python 脚本执行与互操作。
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
@@ -47,19 +52,14 @@ dotnet build Sharp.sln -c Debug
|
||||
|
||||
### 运行示例
|
||||
|
||||
**1. 运行 WPF 客户端:**
|
||||
**1. 运行 WPF 综合演示:**
|
||||
```bash
|
||||
dotnet run --project Client/Client.csproj
|
||||
dotnet run --project Sharp.Demo/Sharp.Demo.csproj
|
||||
```
|
||||
|
||||
**2. 运行嵌入式 Web 服务器:**
|
||||
**2. 运行 NetServer 纯控制台示例:**
|
||||
```bash
|
||||
dotnet run --project Server/Server.csproj
|
||||
```
|
||||
|
||||
**3. 运行 NetServer 纯控制台示例:**
|
||||
```bash
|
||||
dotnet run --project NetServer/Examples
|
||||
dotnet run --project NetServer/Examples/NetServer.Examples.csproj
|
||||
```
|
||||
|
||||
## 🧪 测试
|
||||
|
||||
@@ -8,20 +8,15 @@
|
||||
<UseWPF>true</UseWPF>
|
||||
<AssemblyName>Sharp.Demo</AssemblyName>
|
||||
<RootNamespace>Sharp.Demo</RootNamespace>
|
||||
<IsPackable>false</IsPackable>
|
||||
|
||||
<!-- 程序集元数据 -->
|
||||
<Description>Sharp Framework 综合演示应用 - 展示所有模块的最佳实践和使用方式</Description>
|
||||
|
||||
<!-- NuGet 包元数据 -->
|
||||
<PackageTags>sharp;demo;wpf;framework;example;best-practices</PackageTags>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- 包含 README 到 NuGet 包 -->
|
||||
<ItemGroup>
|
||||
<None Include="README.md" Pack="true" PackagePath="\"/>
|
||||
</ItemGroup>
|
||||
|
||||
<!-- NuGet 包 -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0"/>
|
||||
|
||||
@@ -9,6 +9,7 @@ using Sharp.Wpf.Navigation;
|
||||
using Sharp.Wpf.Abstractions;
|
||||
using Plc.Abstractions;
|
||||
using Plc.Models;
|
||||
using Plc.Siemens.Options;
|
||||
|
||||
namespace Sharp.Demo.ViewModels.Pages;
|
||||
|
||||
@@ -20,8 +21,13 @@ public class PlcDebugViewModel : ViewModelBase, INavigationAware
|
||||
private readonly IPlcService _plcService;
|
||||
private readonly ILogger<PlcDebugViewModel> _logger;
|
||||
private readonly IDispatcher _dispatcher;
|
||||
private readonly SiemensPlcOptions _siemensOptions;
|
||||
|
||||
private string _ipAddress = "192.168.1.100";
|
||||
private PlcBrand _selectedBrand = PlcBrand.Siemens;
|
||||
private string _siemensIpAddress;
|
||||
private string _siemensPort;
|
||||
private string _omronIpAddress = "192.168.1.100";
|
||||
private string _omronPort = "9600";
|
||||
private string _status = "未连接";
|
||||
private string _readAddress = "DB1.DBW0";
|
||||
private string _readValue = "-";
|
||||
@@ -36,11 +42,16 @@ public class PlcDebugViewModel : ViewModelBase, INavigationAware
|
||||
public PlcDebugViewModel(
|
||||
IPlcService plcService,
|
||||
ILogger<PlcDebugViewModel> logger,
|
||||
IDispatcher dispatcher)
|
||||
IDispatcher dispatcher,
|
||||
SiemensPlcOptions siemensOptions)
|
||||
{
|
||||
_plcService = plcService ?? throw new ArgumentNullException(nameof(plcService));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
|
||||
_siemensOptions = siemensOptions ?? throw new ArgumentNullException(nameof(siemensOptions));
|
||||
|
||||
_siemensIpAddress = _siemensOptions.IpAddress;
|
||||
_siemensPort = _siemensOptions.Port.ToString();
|
||||
|
||||
// 初始化命令
|
||||
ConnectCommand = new AsyncCommand(ConnectAsync);
|
||||
@@ -63,10 +74,62 @@ public class PlcDebugViewModel : ViewModelBase, INavigationAware
|
||||
|
||||
#region 属性
|
||||
|
||||
public PlcBrand SelectedBrand
|
||||
{
|
||||
get => _selectedBrand;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _selectedBrand, value))
|
||||
{
|
||||
OnPropertyChanged(nameof(IpAddress));
|
||||
OnPropertyChanged(nameof(Port));
|
||||
OnPropertyChanged(nameof(IsBrandSupported));
|
||||
OnPropertyChanged(nameof(SelectedBrandName));
|
||||
OnPropertyChanged(nameof(SelectedBrandDescription));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string SelectedBrandName => SelectedBrand switch
|
||||
{
|
||||
PlcBrand.Siemens => "西门子",
|
||||
PlcBrand.Omron => "欧姆龙",
|
||||
PlcBrand.Mitsubishi => "三菱",
|
||||
PlcBrand.ModbusTcp => "Modbus TCP",
|
||||
_ => "未知"
|
||||
};
|
||||
|
||||
public string SelectedBrandDescription => SelectedBrand switch
|
||||
{
|
||||
PlcBrand.Siemens => "Sharp.Plc.Siemens - 西门子 PLC 通信驱动 (S7-1200/1500/300/400)",
|
||||
PlcBrand.Omron => "Sharp.Plc.Omron - 欧姆龙 PLC 通信驱动(规划中)",
|
||||
_ => "Sharp.Plc - PLC 通信驱动"
|
||||
};
|
||||
|
||||
public bool IsBrandSupported => SelectedBrand == PlcBrand.Siemens;
|
||||
|
||||
public string IpAddress
|
||||
{
|
||||
get => _ipAddress;
|
||||
set => SetProperty(ref _ipAddress, value);
|
||||
get => SelectedBrand == PlcBrand.Siemens ? _siemensIpAddress : _omronIpAddress;
|
||||
set
|
||||
{
|
||||
if (SelectedBrand == PlcBrand.Siemens)
|
||||
SetProperty(ref _siemensIpAddress, value, nameof(IpAddress));
|
||||
else
|
||||
SetProperty(ref _omronIpAddress, value, nameof(IpAddress));
|
||||
}
|
||||
}
|
||||
|
||||
public string Port
|
||||
{
|
||||
get => SelectedBrand == PlcBrand.Siemens ? _siemensPort : _omronPort;
|
||||
set
|
||||
{
|
||||
if (SelectedBrand == PlcBrand.Siemens)
|
||||
SetProperty(ref _siemensPort, value, nameof(Port));
|
||||
else
|
||||
SetProperty(ref _omronPort, value, nameof(Port));
|
||||
}
|
||||
}
|
||||
|
||||
public string Status
|
||||
@@ -198,8 +261,21 @@ public class PlcDebugViewModel : ViewModelBase, INavigationAware
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!IsBrandSupported)
|
||||
{
|
||||
Status = "当前品牌暂未支持";
|
||||
AppendLog($"⚠️ {SelectedBrandName} 驱动尚未实现");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TryApplySiemensOptions())
|
||||
{
|
||||
Status = "配置错误";
|
||||
return;
|
||||
}
|
||||
|
||||
Status = "正在连接...";
|
||||
AppendLog($"正在连接 PLC: {IpAddress}");
|
||||
AppendLog($"正在连接 PLC ({SelectedBrandName}): {IpAddress}:{Port}");
|
||||
|
||||
await _plcService.StartAsync();
|
||||
|
||||
@@ -318,6 +394,25 @@ public class PlcDebugViewModel : ViewModelBase, INavigationAware
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryApplySiemensOptions()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(IpAddress))
|
||||
{
|
||||
AppendLog("❌ IP 地址不能为空");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!int.TryParse(Port, out var port) || port < 1 || port > 65535)
|
||||
{
|
||||
AppendLog("❌ 端口格式不正确(1-65535)");
|
||||
return false;
|
||||
}
|
||||
|
||||
_siemensOptions.IpAddress = IpAddress;
|
||||
_siemensOptions.Port = port;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ClearLog()
|
||||
{
|
||||
LogText = string.Empty;
|
||||
@@ -637,4 +732,4 @@ public class WriteQueueItem : ViewModelBase
|
||||
get => _dataTypeName;
|
||||
set => SetProperty(ref _dataTypeName, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ using Sharp.Wpf.Commands;
|
||||
using Sharp.Wpf.Abstractions;
|
||||
using Plc.Abstractions;
|
||||
using Plc.Models;
|
||||
using Plc.Siemens.Options;
|
||||
|
||||
namespace Sharp.Demo.ViewModels.Windows;
|
||||
|
||||
@@ -22,8 +23,13 @@ public class PlcDebugViewModel : ViewModelBase
|
||||
private readonly IPlcService _plcService;
|
||||
private readonly ILogger<PlcDebugViewModel> _logger;
|
||||
private readonly IDispatcher _dispatcher;
|
||||
private readonly SiemensPlcOptions _siemensOptions;
|
||||
|
||||
private string _ipAddress = "192.168.1.100";
|
||||
private PlcBrand _selectedBrand = PlcBrand.Siemens;
|
||||
private string _siemensIpAddress;
|
||||
private string _siemensPort;
|
||||
private string _omronIpAddress = "192.168.1.100";
|
||||
private string _omronPort = "9600";
|
||||
private string _status = "未连接";
|
||||
private string _readAddress = "DB1.DBW0";
|
||||
private string _readValue = "-";
|
||||
@@ -38,11 +44,16 @@ public class PlcDebugViewModel : ViewModelBase
|
||||
public PlcDebugViewModel(
|
||||
IPlcService plcService,
|
||||
ILogger<PlcDebugViewModel> logger,
|
||||
IDispatcher dispatcher)
|
||||
IDispatcher dispatcher,
|
||||
SiemensPlcOptions siemensOptions)
|
||||
{
|
||||
_plcService = plcService ?? throw new ArgumentNullException(nameof(plcService));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
|
||||
_siemensOptions = siemensOptions ?? throw new ArgumentNullException(nameof(siemensOptions));
|
||||
|
||||
_siemensIpAddress = _siemensOptions.IpAddress;
|
||||
_siemensPort = _siemensOptions.Port.ToString();
|
||||
|
||||
// 初始化命令
|
||||
ConnectCommand = new AsyncCommand(ConnectAsync);
|
||||
@@ -66,10 +77,54 @@ public class PlcDebugViewModel : ViewModelBase
|
||||
|
||||
#region 属性
|
||||
|
||||
public PlcBrand SelectedBrand
|
||||
{
|
||||
get => _selectedBrand;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _selectedBrand, value))
|
||||
{
|
||||
OnPropertyChanged(nameof(IpAddress));
|
||||
OnPropertyChanged(nameof(Port));
|
||||
OnPropertyChanged(nameof(IsBrandSupported));
|
||||
OnPropertyChanged(nameof(SelectedBrandName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string SelectedBrandName => SelectedBrand switch
|
||||
{
|
||||
PlcBrand.Siemens => "西门子",
|
||||
PlcBrand.Omron => "欧姆龙",
|
||||
PlcBrand.Mitsubishi => "三菱",
|
||||
PlcBrand.ModbusTcp => "Modbus TCP",
|
||||
_ => "未知"
|
||||
};
|
||||
|
||||
public bool IsBrandSupported => SelectedBrand == PlcBrand.Siemens;
|
||||
|
||||
public string IpAddress
|
||||
{
|
||||
get => _ipAddress;
|
||||
set => SetProperty(ref _ipAddress, value);
|
||||
get => SelectedBrand == PlcBrand.Siemens ? _siemensIpAddress : _omronIpAddress;
|
||||
set
|
||||
{
|
||||
if (SelectedBrand == PlcBrand.Siemens)
|
||||
SetProperty(ref _siemensIpAddress, value, nameof(IpAddress));
|
||||
else
|
||||
SetProperty(ref _omronIpAddress, value, nameof(IpAddress));
|
||||
}
|
||||
}
|
||||
|
||||
public string Port
|
||||
{
|
||||
get => SelectedBrand == PlcBrand.Siemens ? _siemensPort : _omronPort;
|
||||
set
|
||||
{
|
||||
if (SelectedBrand == PlcBrand.Siemens)
|
||||
SetProperty(ref _siemensPort, value, nameof(Port));
|
||||
else
|
||||
SetProperty(ref _omronPort, value, nameof(Port));
|
||||
}
|
||||
}
|
||||
|
||||
public string Status
|
||||
@@ -187,8 +242,21 @@ public class PlcDebugViewModel : ViewModelBase
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!IsBrandSupported)
|
||||
{
|
||||
Status = "当前品牌暂未支持";
|
||||
AppendLog($"⚠️ {SelectedBrandName} 驱动尚未实现");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TryApplySiemensOptions())
|
||||
{
|
||||
Status = "配置错误";
|
||||
return;
|
||||
}
|
||||
|
||||
Status = "正在连接...";
|
||||
AppendLog($"正在连接 PLC: {IpAddress}");
|
||||
AppendLog($"正在连接 PLC ({SelectedBrandName}): {IpAddress}:{Port}");
|
||||
|
||||
await _plcService.StartAsync();
|
||||
|
||||
@@ -307,6 +375,25 @@ public class PlcDebugViewModel : ViewModelBase
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryApplySiemensOptions()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(IpAddress))
|
||||
{
|
||||
AppendLog("❌ IP 地址不能为空");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!int.TryParse(Port, out var port) || port < 1 || port > 65535)
|
||||
{
|
||||
AppendLog("❌ 端口格式不正确(1-65535)");
|
||||
return false;
|
||||
}
|
||||
|
||||
_siemensOptions.IpAddress = IpAddress;
|
||||
_siemensOptions.Port = port;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void AppendLog(string message)
|
||||
{
|
||||
var timestamp = DateTime.Now.ToString("HH:mm:ss");
|
||||
@@ -864,4 +951,4 @@ public class WriteQueueItem : ViewModelBase
|
||||
get => _verify;
|
||||
set => SetProperty(ref _verify, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||
xmlns:plc="clr-namespace:Plc.Models;assembly=Plc"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="1000"
|
||||
d:DesignWidth="1200">
|
||||
@@ -18,7 +19,7 @@
|
||||
Margin="0,10,0,30"
|
||||
FontSize="14"
|
||||
Foreground="{DynamicResource SecondaryTextBrush}"
|
||||
Text="Sharp.Plc.Siemens - 西门子 PLC 通信驱动 (S7-1200/1500/300/400)" />
|
||||
Text="{Binding SelectedBrandDescription}" />
|
||||
|
||||
<!-- 连接控制 -->
|
||||
<Border
|
||||
@@ -31,11 +32,40 @@
|
||||
<StackPanel>
|
||||
<TextBlock Text="PLC 连接" FontSize="16" FontWeight="Bold" Margin="0,0,0,10" />
|
||||
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
|
||||
<TextBlock Text="IP 地址:" VerticalAlignment="Center" Margin="0,0,10,0" Width="80" />
|
||||
<TextBox Text="{Binding IpAddress}" Width="200" Height="32" Padding="8,0"
|
||||
BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1" />
|
||||
</StackPanel>
|
||||
<TabControl SelectedValue="{Binding SelectedBrand}" SelectedValuePath="Tag" Margin="0,0,0,10">
|
||||
<TabItem Header="西门子" Tag="{x:Static plc:PlcBrand.Siemens}">
|
||||
<StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
|
||||
<TextBlock Text="IP 地址:" VerticalAlignment="Center" Margin="0,0,10,0"
|
||||
Width="80" />
|
||||
<TextBox Text="{Binding IpAddress}" Width="200" Height="32" Padding="8,0"
|
||||
BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1" />
|
||||
<TextBlock Text="端口:" VerticalAlignment="Center" Margin="15,0,10,0" Width="50" />
|
||||
<TextBox Text="{Binding Port}" Width="100" Height="32" Padding="8,0"
|
||||
BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1" />
|
||||
</StackPanel>
|
||||
<TextBlock Text="S7-1200/1500/300/400,默认端口 102"
|
||||
Foreground="{DynamicResource SecondaryTextBrush}"
|
||||
FontSize="12" />
|
||||
</StackPanel>
|
||||
</TabItem>
|
||||
<TabItem Header="欧姆龙" Tag="{x:Static plc:PlcBrand.Omron}">
|
||||
<StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
|
||||
<TextBlock Text="IP 地址:" VerticalAlignment="Center" Margin="0,0,10,0"
|
||||
Width="80" />
|
||||
<TextBox Text="{Binding IpAddress}" Width="200" Height="32" Padding="8,0"
|
||||
BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1" />
|
||||
<TextBlock Text="端口:" VerticalAlignment="Center" Margin="15,0,10,0" Width="50" />
|
||||
<TextBox Text="{Binding Port}" Width="100" Height="32" Padding="8,0"
|
||||
BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1" />
|
||||
</StackPanel>
|
||||
<TextBlock Text="欧姆龙驱动尚未实现,后续可按品牌扩展配置项"
|
||||
Foreground="{DynamicResource SecondaryTextBrush}"
|
||||
FontSize="12" />
|
||||
</StackPanel>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
|
||||
<TextBlock Text="状态:" VerticalAlignment="Center" Margin="0,0,10,0" Width="80" />
|
||||
@@ -45,10 +75,12 @@
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Button Content="连接" Width="100" Height="35" Margin="0,0,10,0"
|
||||
Command="{Binding ConnectCommand}"
|
||||
Style="{StaticResource ButtonSuccess}" />
|
||||
Style="{StaticResource ButtonSuccess}"
|
||||
IsEnabled="{Binding IsBrandSupported}" />
|
||||
<Button Content="断开" Width="100" Height="35"
|
||||
Command="{Binding DisconnectCommand}"
|
||||
Style="{StaticResource ButtonDanger}" />
|
||||
Style="{StaticResource ButtonDanger}"
|
||||
IsEnabled="{Binding IsBrandSupported}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
@@ -296,4 +328,4 @@
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
</UserControl>
|
||||
@@ -4,6 +4,7 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||
xmlns:plc="clr-namespace:Plc.Models;assembly=Plc"
|
||||
mc:Ignorable="d"
|
||||
Title="PLC 调试工具"
|
||||
Height="800"
|
||||
@@ -23,16 +24,38 @@
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="IP 地址:" VerticalAlignment="Center" Margin="0,0,10,0" />
|
||||
<TextBox Text="{Binding IpAddress}" Width="150" Height="28" Margin="0,0,10,0" />
|
||||
<TabControl Grid.Row="0" SelectedValue="{Binding SelectedBrand}" SelectedValuePath="Tag"
|
||||
Margin="0,0,0,10">
|
||||
<TabItem Header="西门子" Tag="{x:Static plc:PlcBrand.Siemens}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="IP 地址:" VerticalAlignment="Center" Margin="0,0,10,0" />
|
||||
<TextBox Text="{Binding IpAddress}" Width="150" Height="28" Margin="0,0,10,0" />
|
||||
<TextBlock Text="端口:" VerticalAlignment="Center" Margin="10,0,10,0" />
|
||||
<TextBox Text="{Binding Port}" Width="80" Height="28" Margin="0,0,10,0" />
|
||||
</StackPanel>
|
||||
</TabItem>
|
||||
<TabItem Header="欧姆龙" Tag="{x:Static plc:PlcBrand.Omron}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="IP 地址:" VerticalAlignment="Center" Margin="0,0,10,0" />
|
||||
<TextBox Text="{Binding IpAddress}" Width="150" Height="28" Margin="0,0,10,0" />
|
||||
<TextBlock Text="端口:" VerticalAlignment="Center" Margin="10,0,10,0" />
|
||||
<TextBox Text="{Binding Port}" Width="80" Height="28" Margin="0,0,10,0" />
|
||||
<TextBlock Text="驱动未实现" VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource SecondaryTextBrush}" />
|
||||
</StackPanel>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
|
||||
<StackPanel Grid.Row="1" Orientation="Horizontal">
|
||||
<Button Content="🔗 连接" Width="90" Height="32" Margin="0,0,10,0"
|
||||
Command="{Binding ConnectCommand}" Style="{StaticResource ButtonSuccess}" />
|
||||
Command="{Binding ConnectCommand}" Style="{StaticResource ButtonSuccess}"
|
||||
IsEnabled="{Binding IsBrandSupported}" />
|
||||
<Button Content="🔌 断开" Width="90" Height="32" Margin="0,0,10,0"
|
||||
Command="{Binding DisconnectCommand}" Style="{StaticResource ButtonDanger}" />
|
||||
Command="{Binding DisconnectCommand}" Style="{StaticResource ButtonDanger}"
|
||||
IsEnabled="{Binding IsBrandSupported}" />
|
||||
|
||||
<Separator Margin="10,0" />
|
||||
|
||||
@@ -42,7 +65,7 @@
|
||||
</StackPanel>
|
||||
|
||||
<!-- 最新日志 -->
|
||||
<TextBlock Grid.Row="1" Text="{Binding LastLogMessage}"
|
||||
<TextBlock Grid.Row="2" Text="{Binding LastLogMessage}"
|
||||
Foreground="{DynamicResource SecondaryTextBrush}"
|
||||
FontSize="11" Margin="0,5,0,0" TextTrimming="CharacterEllipsis" />
|
||||
</Grid>
|
||||
@@ -318,4 +341,4 @@
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
</Grid>
|
||||
</Window>
|
||||
</Window>
|
||||
@@ -0,0 +1,43 @@
|
||||
<Window x:Class="Sharp.Wpf.HandyControl.Dialogs.InputDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="输入"
|
||||
Width="420"
|
||||
SizeToContent="Height"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
ResizeMode="NoResize">
|
||||
<Grid Margin="20">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock x:Name="MessageText"
|
||||
TextWrapping="Wrap"
|
||||
Margin="0,0,0,10" />
|
||||
|
||||
<TextBox x:Name="InputTextBox"
|
||||
Grid.Row="1"
|
||||
Height="32"
|
||||
Padding="6,0"
|
||||
MinWidth="300" />
|
||||
|
||||
<StackPanel Grid.Row="2"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Right"
|
||||
Margin="0,15,0,0">
|
||||
<Button x:Name="OkButton"
|
||||
Content="确定"
|
||||
Width="90"
|
||||
Margin="0,0,10,0"
|
||||
IsDefault="True"
|
||||
Click="OkButton_Click" />
|
||||
<Button x:Name="CancelButton"
|
||||
Content="取消"
|
||||
Width="90"
|
||||
IsCancel="True"
|
||||
Click="CancelButton_Click" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,31 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace Sharp.Wpf.HandyControl.Dialogs;
|
||||
|
||||
public partial class InputDialog : Window
|
||||
{
|
||||
public string InputValue => InputTextBox.Text;
|
||||
|
||||
public InputDialog(string message, string? title = null, string? defaultValue = null)
|
||||
{
|
||||
InitializeComponent();
|
||||
Title = string.IsNullOrWhiteSpace(title) ? "输入" : title;
|
||||
MessageText.Text = message;
|
||||
InputTextBox.Text = defaultValue ?? string.Empty;
|
||||
Loaded += (_, _) =>
|
||||
{
|
||||
InputTextBox.Focus();
|
||||
InputTextBox.SelectAll();
|
||||
};
|
||||
}
|
||||
|
||||
private void OkButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
DialogResult = true;
|
||||
}
|
||||
|
||||
private void CancelButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
DialogResult = false;
|
||||
}
|
||||
}
|
||||
@@ -304,7 +304,7 @@ public interface IHandyDialogService : IDialogService
|
||||
Task<HandyDialogResult> AskAsync(string message, string? title = null);
|
||||
|
||||
/// <summary>
|
||||
/// 显示输入对话框(尚未实现)
|
||||
/// 显示输入对话框
|
||||
/// </summary>
|
||||
Task<Result<string>> ShowInputAsync(string message, string? title = null, string? defaultValue = null);
|
||||
|
||||
@@ -1037,19 +1037,9 @@ services.AddSharpHandyControl();
|
||||
services.AddSingleton<IDialogService, MyCustomDialogService>();
|
||||
```
|
||||
|
||||
### Q5: `ShowInputAsync` 为什么返回失败?
|
||||
### Q5: `ShowInputAsync` 如何返回结果?
|
||||
|
||||
**A:** `ShowInputAsync` 功能尚未实现(需要自定义对话框窗口),当前版本会返回:
|
||||
|
||||
```csharp
|
||||
Result.Failure<string>("输入对话框功能尚未实现,需要自定义对话框窗口")
|
||||
```
|
||||
|
||||
如果需要输入功能,可以临时使用:
|
||||
|
||||
- 创建自定义 Window 进行输入
|
||||
- 使用 HandyControl 的其他输入控件
|
||||
- 等待后续版本实现
|
||||
**A:** `ShowInputAsync` 会弹出输入对话框,点击“确定”返回 `Result.Success`,点击“取消”返回失败结果(错误信息为“用户取消输入”)。
|
||||
|
||||
### Q6: 能否在 WinForms 项目中使用?
|
||||
|
||||
@@ -1217,7 +1207,7 @@ protected override void OnStartup(StartupEventArgs e)
|
||||
|------------------------------------------------|------------|---------------------------|--------|
|
||||
| `ConfirmAsync(message, title)` | 确认对话框 | `Task<bool>` | 确定/取消 |
|
||||
| `AskAsync(message, title)` | 询问对话框 | `Task<HandyDialogResult>` | 是/否/取消 |
|
||||
| `ShowInputAsync(message, title, defaultValue)` | 输入对话框(未实现) | `Task<Result<string>>` | 确定/取消 |
|
||||
| `ShowInputAsync(message, title, defaultValue)` | 输入对话框 | `Task<Result<string>>` | 确定/取消 |
|
||||
|
||||
### Growl 通知方法
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Windows;
|
||||
using Common.Results;
|
||||
using HandyControl.Controls;
|
||||
using HandyControl.Data;
|
||||
using Sharp.Wpf.HandyControl.Dialogs;
|
||||
using Sharp.Wpf.HandyControl.Options;
|
||||
using Sharp.Wpf.Services;
|
||||
using MessageBox = HandyControl.Controls.MessageBox;
|
||||
@@ -96,13 +97,29 @@ public class HandyDialogService : IHandyDialogService
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示输入对话框(尚未实现)
|
||||
/// 显示输入对话框
|
||||
/// </summary>
|
||||
public Task<Result<string>> ShowInputAsync(string message, string? title = null, string? defaultValue = null)
|
||||
{
|
||||
// TODO: 需要自定义 TextDialog 窗口来实现此功能
|
||||
// 当前版本暂不支持,返回失败结果
|
||||
return Task.FromResult(Result.Failure<string>("输入对话框功能尚未实现,需要自定义对话框窗口"));
|
||||
return Task.Run(() =>
|
||||
{
|
||||
if (Application.Current == null)
|
||||
return Result.Failure<string>("当前应用上下文不可用");
|
||||
|
||||
return Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
var dialog = new InputDialog(message, title, defaultValue)
|
||||
{
|
||||
Owner = Application.Current.MainWindow
|
||||
};
|
||||
|
||||
var dialogResult = dialog.ShowDialog();
|
||||
if (dialogResult == true)
|
||||
return Result.Success(dialog.InputValue);
|
||||
|
||||
return Result.Failure<string>("用户取消输入");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -178,4 +195,4 @@ public class HandyDialogService : IHandyDialogService
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user