Compare commits
No commits in common. "main" and "curve-marker-feature" have entirely different histories.
main
...
curve-mark
188
Docs/开发文档.md
188
Docs/开发文档.md
@ -1,6 +1,6 @@
|
|||||||
# 固态功率控制器动态参数测试系统 — 开发文档
|
# 固态功率控制器动态参数测试系统 — 开发文档
|
||||||
|
|
||||||
> 版本 v1.3 | 更新:**2026-07-08 14:58:50 +08:00** | 技术栈:**C# + WPF + .NET 8** | 口径:**以当前仓库代码为准**
|
> 版本 v1.1 | 技术栈:**C# + WPF + .NET 8** | 口径:**以当前仓库代码为准**
|
||||||
>
|
>
|
||||||
> 本文档描述当前仓库的真实结构、配置方式、核心流程和维护约定。
|
> 本文档描述当前仓库的真实结构、配置方式、核心流程和维护约定。
|
||||||
> 若文档与代码冲突,以代码实现为准,并应同步回写本文档。
|
> 若文档与代码冲突,以代码实现为准,并应同步回写本文档。
|
||||||
@ -12,7 +12,7 @@
|
|||||||
对本项目进行新增、修改、排障时,默认遵守以下原则:
|
对本项目进行新增、修改、排障时,默认遵守以下原则:
|
||||||
|
|
||||||
1. **分层隔离优先**:UI 只依赖 ViewModel / Service / Logic,不直接依赖具体设备驱动。
|
1. **分层隔离优先**:UI 只依赖 ViewModel / Service / Logic,不直接依赖具体设备驱动。
|
||||||
2. **接口先行**:SSPC、电源、负载、DAQ、安全 DIO 一律经 `SSPCTester.Devices.Interfaces` 暴露,真假驱动由配置决定。
|
2. **接口先行**:SSPC、电源、负载、DAQ 一律经 `SSPCTester.Devices.Interfaces` 暴露,真假驱动由配置决定。
|
||||||
3. **按设备粒度切换 Mock/Real**:当前不再是“一个全局 `MockMode` 管全部设备”,而是每类设备都有自己的 `UseMock`。
|
3. **按设备粒度切换 Mock/Real**:当前不再是“一个全局 `MockMode` 管全部设备”,而是每类设备都有自己的 `UseMock`。
|
||||||
4. **配置分层加载**:程序先读内置默认值,再读 `appsettings.json`,最后读 `%LOCALAPPDATA%\SSPCTester\settings.json`。
|
4. **配置分层加载**:程序先读内置默认值,再读 `appsettings.json`,最后读 `%LOCALAPPDATA%\SSPCTester\settings.json`。
|
||||||
5. **测试态独占**:进入测试后只展示当前项目,禁止切换其他项目,设备连接由测试态独占。
|
5. **测试态独占**:进入测试后只展示当前项目,禁止切换其他项目,设备连接由测试态独占。
|
||||||
@ -29,7 +29,7 @@
|
|||||||
- 基础通断状态
|
- 基础通断状态
|
||||||
- 开启/关断曲线与时间参数
|
- 开启/关断曲线与时间参数
|
||||||
- 功率损耗与效率
|
- 功率损耗与效率
|
||||||
- 保护功能测试结果
|
- 保护功能演示结果
|
||||||
|
|
||||||
### 1.2 当前测试台硬件
|
### 1.2 当前测试台硬件
|
||||||
|
|
||||||
@ -37,10 +37,9 @@
|
|||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| 工控机 | Windows x64 | 运行 WPF 主程序 | 已支持 |
|
| 工控机 | Windows x64 | 运行 WPF 主程序 | 已支持 |
|
||||||
| 被测件 SSPC | 24 路 | 控制 24 路导通/关断 | 已有 `SspcModbus` |
|
| 被测件 SSPC | 24 路 | 控制 24 路导通/关断 | 已有 `SspcModbus` |
|
||||||
| 程控电源 | UDP5080-100 | 输入端电压/电流读取、输出开关控制 | 已有真实驱动 |
|
| 程控电源 | UDP5080-100 | 输入端电压/电流读取 | 已有真实驱动,控制命令保留接口 |
|
||||||
| 可编程负载 | IT8733P / IT8702P | 输出端电压/电流/功率读取、输入开关控制 | 已有真实驱动 |
|
| 可编程负载 | IT8733P / IT8702P | 输出端电压/电流/功率读取 | 已有真实驱动 |
|
||||||
| 高速采集卡 | PCIe8586 | 曲线波形采集 | 已有 `Pcie8586Daq` |
|
| 高速采集卡 | PCIe8586 | 曲线波形采集 | 已有 `Pcie8586Daq` |
|
||||||
| 安全 DIO 卡 | PCI2312 | 急停输入读取、报警灯输出控制 | 已有 `Pci2312SafetyDio` |
|
|
||||||
| 示波器 | Tektronix MDO3024 | 线下校准基准 | 不纳入软件控制 |
|
| 示波器 | Tektronix MDO3024 | 线下校准基准 | 不纳入软件控制 |
|
||||||
|
|
||||||
### 1.3 当前实现事实
|
### 1.3 当前实现事实
|
||||||
@ -48,8 +47,7 @@
|
|||||||
- SSPC 24 路通过 **3 个 Modbus 模块 × 每模块 8 路** 映射。
|
- SSPC 24 路通过 **3 个 Modbus 模块 × 每模块 8 路** 映射。
|
||||||
- 采集卡实现已经统一为 **PCIe8586**,不再使用旧文档中的 `PCIe9318` 命名。
|
- 采集卡实现已经统一为 **PCIe8586**,不再使用旧文档中的 `PCIe9318` 命名。
|
||||||
- 真假设备切换已经演进为 **每类设备独立 `UseMock`**。
|
- 真假设备切换已经演进为 **每类设备独立 `UseMock`**。
|
||||||
- 主程序启动后会按配置自动连接 SSPC、电源、负载、DAQ、PCI2312。
|
- 主程序启动后会按配置自动连接 SSPC、电源、负载、DAQ。
|
||||||
- 新增设备安全监控链路:后台轮询 PCI2312 急停输入,触发后接通报警输出,并向 SSPC、电源、负载发送安全关断指令。
|
|
||||||
- 根目录脚本 `run.ps1` / `publish.ps1` 已是当前标准运行入口。
|
- 根目录脚本 `run.ps1` / `publish.ps1` 已是当前标准运行入口。
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -73,33 +71,9 @@
|
|||||||
| 基础测试 | 24 路逐路开/关、读电压电流、判定稳态关断 | 已实现 |
|
| 基础测试 | 24 路逐路开/关、读电压电流、判定稳态关断 | 已实现 |
|
||||||
| 开启与关断曲线 | 支持开通/关断波形采集、指标计算、独立曲线视窗、波形落盘 | 已实现 |
|
| 开启与关断曲线 | 支持开通/关断波形采集、指标计算、独立曲线视窗、波形落盘 | 已实现 |
|
||||||
| 功率损耗 | 使用 UDP5080 读输入端,IT87xx 读输出端功率/电压/电流 | 已实现 |
|
| 功率损耗 | 使用 UDP5080 读输入端,IT87xx 读输出端功率/电压/电流 | 已实现 |
|
||||||
| 保护功能 | 固定保护项、自动获取/人工观测数据记录、人工判别结果 | 已实现 |
|
| 保护功能 | UI、数据结构和演示结果生成已完成,真实联动未接 | 框架完成 |
|
||||||
|
|
||||||
### 2.2 设备安全与急停报警
|
### 2.2 指标口径
|
||||||
|
|
||||||
当前代码新增了独立于“保护功能测试页”的**运行时设备安全链路**:
|
|
||||||
|
|
||||||
- 硬件连接:`PCI2312` 通过厂商 `PCI2312_64.dll` 以 P/Invoke 方式接入,使用 DI 通道读取急停状态,使用 DO 通道控制报警灯。
|
|
||||||
- 后台监控:`SafetyMonitorService.Start()` 在主窗口初始化后启动,按 `Devices:Pci2312:PollIntervalMs` 周期轮询急停输入。
|
|
||||||
- 硬件急停:当配置的 DI 通道达到有效电平时,程序锁存硬件急停状态,写入报警 DO,并依次执行 SSPC 全通道关断、电源输出关闭、负载输入关闭。
|
|
||||||
- 软件急停:主窗口顶部“报警急停”按钮调用 `TriggerSoftwareEmergencyAsync()`,走同一套报警灯和设备关断链路;纯软件急停需要通过“状态重置”解除。
|
|
||||||
- 状态重置与硬件解除:报警灯点亮后按钮变为“状态重置”。若硬件急停 DI 仍有效,手动重置会被拒绝;硬件急停曾触发后,DI 复位会自动关闭报警输出、清除硬件急停锁存和软件急停状态,并刷新 UI 回到“报警急停”。
|
|
||||||
|
|
||||||
控制链路如下:
|
|
||||||
|
|
||||||
```text
|
|
||||||
PCI2312 DI 急停输入 / UI 软件急停按钮
|
|
||||||
-> SafetyMonitorService
|
|
||||||
-> PCI2312 DO 报警灯输出
|
|
||||||
-> ISspc.TurnOffAsync(1..24)
|
|
||||||
-> IPowerSupply.OutputAsync(false) // UDP5080 :OUTput OFF
|
|
||||||
-> ILoad.InputAsync(false) // IT8702P INP OFF
|
|
||||||
-> UI 状态、日志和报警灯图标同步刷新
|
|
||||||
```
|
|
||||||
|
|
||||||
注意:这是软件侧联动安全链路,用于快速下发关断和报警指令;现场仍应保留硬件级急停/互锁作为最终安全边界。
|
|
||||||
|
|
||||||
### 2.3 指标口径
|
|
||||||
|
|
||||||
当前计算与判定主要围绕以下指标:
|
当前计算与判定主要围绕以下指标:
|
||||||
|
|
||||||
@ -108,7 +82,7 @@ PCI2312 DI 急停输入 / UI 软件急停按钮
|
|||||||
- `1.5` 上升/下降时间:10%~90% 或 90%~10% 时间
|
- `1.5` 上升/下降时间:10%~90% 或 90%~10% 时间
|
||||||
- `1.9` 电压降:`Vin - Vout`
|
- `1.9` 电压降:`Vin - Vout`
|
||||||
- `1.10 / 1.13` 功率损耗、效率
|
- `1.10 / 1.13` 功率损耗、效率
|
||||||
- `1.14+` 保护功能相关触发条件、实测/观测值与人工判别结果
|
- `1.14+` 保护功能相关故障时间与演示状态
|
||||||
|
|
||||||
阈值定义和最终判断以 `Project.CurveThresholds` 与各页结果模型为准。
|
阈值定义和最终判断以 `Project.CurveThresholds` 与各页结果模型为准。
|
||||||
|
|
||||||
@ -124,8 +98,6 @@ PCI2312 DI 急停输入 / UI 软件急停按钮
|
|||||||
| DI / Host | `Microsoft.Extensions.Hosting` | 启动时装配所有服务 |
|
| DI / Host | `Microsoft.Extensions.Hosting` | 启动时装配所有服务 |
|
||||||
| Modbus | `NModbus` | SSPC 串口控制 |
|
| Modbus | `NModbus` | SSPC 串口控制 |
|
||||||
| 串口 | `System.IO.Ports` | SSPC/部分设备底层通信 |
|
| 串口 | `System.IO.Ports` | SSPC/部分设备底层通信 |
|
||||||
| TCP Socket | `System.Net.Sockets.TcpClient` | UDP5080 / IT87xx SCPI-RAW 通信 |
|
|
||||||
| 原生驱动 | P/Invoke + 厂商 DLL | PCIe8586 / PCI2312 接入 |
|
|
||||||
| 波形绘制 | `ScottPlot` / `ScottPlot.WPF` | 曲线页与报告插图 |
|
| 波形绘制 | `ScottPlot` / `ScottPlot.WPF` | 曲线页与报告插图 |
|
||||||
| 报告 | `QuestPDF` + `DocumentFormat.OpenXml` | PDF 与 Word 双实现 |
|
| 报告 | `QuestPDF` + `DocumentFormat.OpenXml` | PDF 与 Word 双实现 |
|
||||||
| Web 预览 | `Microsoft.Web.WebView2` | 报告页 PDF 预览 |
|
| Web 预览 | `Microsoft.Web.WebView2` | 报告页 PDF 预览 |
|
||||||
@ -165,7 +137,7 @@ SSPCTester.sln
|
|||||||
│ ├── Storage/ # 项目与波形持久化
|
│ ├── Storage/ # 项目与波形持久化
|
||||||
│ └── Models/ # 项目、结果、阈值、枚举
|
│ └── Models/ # 项目、结果、阈值、枚举
|
||||||
├── SSPCTester.Devices/ # 设备抽象与驱动
|
├── SSPCTester.Devices/ # 设备抽象与驱动
|
||||||
│ ├── Interfaces/ # ISspc / IPowerSupply / ILoad / IDaq / ISafetyDio
|
│ ├── Interfaces/ # ISspc / IPowerSupply / ILoad / IDaq
|
||||||
│ ├── Drivers/Mock/ # Mock 驱动
|
│ ├── Drivers/Mock/ # Mock 驱动
|
||||||
│ ├── Drivers/Real/ # 真设备驱动
|
│ ├── Drivers/Real/ # 真设备驱动
|
||||||
│ ├── Drivers/ConfigurableDevices.cs # 按设备切换 Mock/Real
|
│ ├── Drivers/ConfigurableDevices.cs # 按设备切换 Mock/Real
|
||||||
@ -205,11 +177,10 @@ SSPCTester.sln
|
|||||||
- `Devices:PowerSupply:UseMock`
|
- `Devices:PowerSupply:UseMock`
|
||||||
- `Devices:Load:UseMock`
|
- `Devices:Load:UseMock`
|
||||||
- `Devices:Daq:UseMock`
|
- `Devices:Daq:UseMock`
|
||||||
- `Devices:Pci2312:UseMock`
|
|
||||||
|
|
||||||
`Devices:MockMode` 仅保留兼容旧配置用途,不应再作为新逻辑入口。
|
`Devices:MockMode` 仅保留兼容旧配置用途,不应再作为新逻辑入口。
|
||||||
|
|
||||||
### 5.3 当前 `appsettings.json` 节选
|
### 5.3 当前 `appsettings.json` 示例
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@ -238,26 +209,9 @@ SSPCTester.sln
|
|||||||
"UseMock": false,
|
"UseMock": false,
|
||||||
"AutoConnect": true,
|
"AutoConnect": true,
|
||||||
"LogicalDeviceId": 0,
|
"LogicalDeviceId": 0,
|
||||||
"ChannelCount": 1,
|
"ChannelCount": 4,
|
||||||
"InputRange": "PlusMinus5V",
|
"PreTriggerMs": 10,
|
||||||
"ClockDivider": 1000,
|
"PostTriggerMs": 40
|
||||||
"PreTriggerMs": 3,
|
|
||||||
"PostTriggerMs": 100,
|
|
||||||
"Channels": [
|
|
||||||
{ "PhysicalChannel": 0, "Role": "OutputVoltage", "Name": "输出电压", "Scale": 1.0, "Offset": 0.0, "IsCalibrated": true },
|
|
||||||
{ "PhysicalChannel": 1, "Role": "Unconfigured", "Name": "CH1", "Scale": 1.0, "Offset": 0.0, "IsCalibrated": false }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"Pci2312": {
|
|
||||||
"Enabled": true,
|
|
||||||
"UseMock": false,
|
|
||||||
"AutoConnect": true,
|
|
||||||
"DeviceId": 0,
|
|
||||||
"EmergencyInputChannel": 0,
|
|
||||||
"EmergencyInputActiveHigh": true,
|
|
||||||
"AlarmOutputChannel": 0,
|
|
||||||
"AlarmOutputActiveHigh": true,
|
|
||||||
"PollIntervalMs": 100
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Storage": {
|
"Storage": {
|
||||||
@ -352,25 +306,6 @@ public interface IDaq : IDeviceConnection
|
|||||||
`CaptureAroundActionAsync()` 是当前曲线测试的核心接口:
|
`CaptureAroundActionAsync()` 是当前曲线测试的核心接口:
|
||||||
先进入预采集,再在触发点附近执行开关动作,以得到统一对齐的波形。
|
先进入预采集,再在触发点附近执行开关动作,以得到统一对齐的波形。
|
||||||
|
|
||||||
### 6.6 安全 DIO
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public interface ISafetyDio : IDeviceConnection
|
|
||||||
{
|
|
||||||
Task<bool> ReadInputAsync(int channel, CancellationToken ct = default);
|
|
||||||
Task WriteOutputAsync(int channel, bool on, CancellationToken ct = default);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
当前真实实现为 `Pci2312SafetyDio`:
|
|
||||||
|
|
||||||
- `ConnectAsync()` 调用 `PCI2312_CreateDevice(DeviceId)` 创建设备句柄。
|
|
||||||
- `ReadInputAsync(channel)` 调用 `PCI2312_GetDeviceDI()` 读取 0~15 路 DI。
|
|
||||||
- `WriteOutputAsync(channel, on)` 维护 16 路 DO 状态数组,并调用 `PCI2312_SetDeviceDO()` 一次性写出。
|
|
||||||
- `DisconnectAsync()` 调用 `PCI2312_ReleaseDevice()` 释放句柄。
|
|
||||||
|
|
||||||
`ConfigurableSafetyDio` 按 `Devices:Pci2312:UseMock` 在 `MockSafetyDio` 与 `Pci2312SafetyDio` 间切换。PCI2312 是否参与自动连接由 `Devices:Pci2312:Enabled` 和 `Devices:Pci2312:AutoConnect` 共同决定。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. 核心业务逻辑
|
## 7. 核心业务逻辑
|
||||||
@ -397,8 +332,6 @@ public interface ITestModule
|
|||||||
|
|
||||||
`TestRunner` 依据 `Project.Selection` 串行执行模块。
|
`TestRunner` 依据 `Project.Selection` 串行执行模块。
|
||||||
|
|
||||||
设备安全监控不属于 `ITestModule`,不会进入项目测试结果,也不会由 `TestRunner` 调度;它由 UI 层 `SafetyMonitorService` 在程序运行期持续维护。
|
|
||||||
|
|
||||||
### 7.2 浏览态 / 测试态
|
### 7.2 浏览态 / 测试态
|
||||||
|
|
||||||
当前程序已实现两种应用状态:
|
当前程序已实现两种应用状态:
|
||||||
@ -469,74 +402,14 @@ TurnOnAsync(ch)
|
|||||||
|
|
||||||
### 7.6 保护功能
|
### 7.6 保护功能
|
||||||
|
|
||||||
当前保护功能页和逻辑层已经按固定保护项落地,结果由 `ProtectRow` 持久化。`ProtectTestPlan` 负责初始化和刷新固定测试项:
|
当前保护功能页和逻辑层是**演示框架**:
|
||||||
|
|
||||||
- `跳闸`:触发条件默认 `1.5×额定电流`
|
- UI 已完成
|
||||||
- `短路`:触发条件默认 `输出短路`
|
- 结果模型已完成
|
||||||
- `过温`:触发条件默认 `≥85℃`
|
- `ProtectTest` 目前生成演示数据
|
||||||
- `过压`:触发条件默认 `≥32V`
|
- 未接真实故障触发与真实曲线采集链路
|
||||||
- `欠压`:触发条件默认 `≤22V`
|
|
||||||
- `限流`:触发条件默认 `≥1.2A`
|
|
||||||
|
|
||||||
保护功能数据分为两类:
|
### 7.7 报告
|
||||||
|
|
||||||
- **自动获取**:对 `跳闸`、`过压`、`欠压`、`限流` 等可由电源读数支撑的项目,页面提供“测”按钮,调用 `IPowerSupply.ReadAsync()` 获取当前电压/电流并写入 `MeasuredValue`。
|
|
||||||
- **人工观测**:对 `短路`、`过温` 等需要现场动作、仪表或人员观察的项目,由操作人员在 `MeasuredValue`、`Trigger` 等字段中填写观测记录。
|
|
||||||
|
|
||||||
判定口径:
|
|
||||||
|
|
||||||
- 最终判别标准为**人工判别**,不由程序自动计算合格/不合格。
|
|
||||||
- `JudgeMode` 默认值为 `人工判别`。
|
|
||||||
- `Status` 由操作人员在表格中选择 `合格` / `不合格` / `未判定`。
|
|
||||||
- `ProtectTest.RunAsync()` 只负责刷新测量值和进度状态;若某项已经被人工判定为 `Pass` 或 `Fail`,运行批量测试时会保留该人工判定结果。
|
|
||||||
|
|
||||||
### 7.7 设备安全监控
|
|
||||||
|
|
||||||
`SafetyMonitorService` 是当前急停/报警链路的中心,依赖:
|
|
||||||
|
|
||||||
- `ISafetyDio`:读取急停 DI、写报警灯 DO。
|
|
||||||
- `ISspc`:触发急停后逐路执行 `TurnOffAsync(1..24)`。
|
|
||||||
- `IPowerSupply`:触发急停后执行 `OutputAsync(false)`,真实 UDP5080 命令为 `:OUTput OFF`。
|
|
||||||
- `ILoad`:触发急停后执行 `InputAsync(false)`,真实 IT8702P 命令为 `INP OFF`。
|
|
||||||
- `UiLogSink`:写入 UI 日志,便于现场追踪急停动作。
|
|
||||||
|
|
||||||
启动链路:
|
|
||||||
|
|
||||||
```text
|
|
||||||
App.ConfigureServices()
|
|
||||||
-> 注册 MockSafetyDio / Pci2312SafetyDio / ConfigurableSafetyDio
|
|
||||||
-> 注册 SafetyMonitorService
|
|
||||||
-> MainViewModel.Initialize()
|
|
||||||
-> ConnectConfiguredDevicesAsync()
|
|
||||||
-> SafetyMonitorService.Start()
|
|
||||||
```
|
|
||||||
|
|
||||||
运行链路:
|
|
||||||
|
|
||||||
```text
|
|
||||||
Timer(PollIntervalMs,代码中限制 50~10000ms)
|
|
||||||
-> 自动连接 PCI2312(若启用 AutoConnect)
|
|
||||||
-> ReadInputAsync(EmergencyInputChannel)
|
|
||||||
-> 按 EmergencyInputActiveHigh 解释有效电平
|
|
||||||
-> DI 有效时锁存硬件急停并触发 HandleEmergencyAsync()
|
|
||||||
-> 硬件急停锁存后 DI 复位时触发 ClearHardwareEmergencyAsync()
|
|
||||||
```
|
|
||||||
|
|
||||||
急停处理具有防重入保护:`_gate` 和 `_handlingEmergency` 避免重复并发关断;`_hardwareEmergencyLatched` 用于区分“硬件急停曾触发后解除”和“纯软件急停等待手动重置”。每个设备的关断失败会单独记录日志,不阻断后续设备的关断尝试。
|
|
||||||
|
|
||||||
解除行为:
|
|
||||||
|
|
||||||
- 硬件急停触发后,DI 复位会自动关闭报警 DO,清除 `_handlingEmergency`、`_softwareEmergencyActive`、`_hardwareEmergencyLatched`,并刷新 UI 按钮回到“报警急停”。
|
|
||||||
- 纯软件急停不会因为硬件 DI 处于未触发状态而被轮询自动解除,仍需要点击“状态重置”;若此时硬件 DI 仍有效,重置会被拒绝。
|
|
||||||
|
|
||||||
主窗口顶部状态区会展示:
|
|
||||||
|
|
||||||
- 报警灯动态图标:绑定 `AlarmLampActive`
|
|
||||||
- 急停/重置按钮:绑定 `ToggleEmergencyAlarmCommand`
|
|
||||||
- 安全状态文本:来自 `SafetyMonitorService.StatusText / DetailText`
|
|
||||||
- 设备面板中的 `报警/急停` 连接状态:来自 `ISafetyDio.IsConnected`
|
|
||||||
|
|
||||||
### 7.8 报告
|
|
||||||
|
|
||||||
当前报告支持:
|
当前报告支持:
|
||||||
|
|
||||||
@ -649,7 +522,6 @@ PowerShell -ExecutionPolicy Bypass -File .\publish.ps1
|
|||||||
|
|
||||||
- 当前脚本明确没有走 WPF 单文件发布默认方案
|
- 当前脚本明确没有走 WPF 单文件发布默认方案
|
||||||
- 发布时会检查 `ACTS1000_64.dll` 是否存在
|
- 发布时会检查 `ACTS1000_64.dll` 是否存在
|
||||||
- PCI2312 真设备模式还需要 `PCI2312_64.dll` 可由进程加载;当前发布脚本未做该 DLL 的存在性检查
|
|
||||||
|
|
||||||
### 9.4 运行环境
|
### 9.4 运行环境
|
||||||
|
|
||||||
@ -658,7 +530,6 @@ PowerShell -ExecutionPolicy Bypass -File .\publish.ps1
|
|||||||
- Windows x64
|
- Windows x64
|
||||||
- .NET 8 SDK x64
|
- .NET 8 SDK x64
|
||||||
- WebView2 Runtime
|
- WebView2 Runtime
|
||||||
- 真卡模式下的 PCIe8586 / PCI2312 厂商驱动与 x64 DLL
|
|
||||||
|
|
||||||
若只运行现成发布版,则至少需要满足发布产物依赖和本机驱动/DLL 条件。
|
若只运行现成发布版,则至少需要满足发布产物依赖和本机驱动/DLL 条件。
|
||||||
|
|
||||||
@ -671,18 +542,17 @@ PowerShell -ExecutionPolicy Bypass -File .\publish.ps1
|
|||||||
- WPF 主程序骨架、主题资源、页面导航
|
- WPF 主程序骨架、主题资源、页面导航
|
||||||
- 浏览态 / 测试态切换
|
- 浏览态 / 测试态切换
|
||||||
- 项目新建、加载、保存
|
- 项目新建、加载、保存
|
||||||
- 基础测试、曲线测试、功率损耗、保护功能页和结果记录
|
- 基础测试、曲线测试、功率损耗、保护功能页
|
||||||
- PDF / Word 报告生成
|
- PDF / Word 报告生成
|
||||||
- 真/假设备可按设备粒度切换
|
- 真/假设备可按设备粒度切换
|
||||||
- 实际 SSPC / 电源 / 负载 / PCIe8586 / PCI2312 驱动接入代码
|
- 实际 SSPC / 电源 / 负载 / PCIe8586 驱动接入代码
|
||||||
- PCI2312 急停输入轮询、报警输出、软件急停、硬件解除联动、状态重置和设备关断联动
|
|
||||||
|
|
||||||
### 10.2 待统一 / 待完善
|
### 10.2 待统一 / 待完善
|
||||||
|
|
||||||
|
- 保护功能仍为演示逻辑,未接真实测试链路
|
||||||
- 功率损耗旧版 `PowerTestLegacy` 仍在仓库,后续应明确去留
|
- 功率损耗旧版 `PowerTestLegacy` 仍在仓库,后续应明确去留
|
||||||
- 曲线弹窗窗口已实现,但当前主页面尚未提供入口按钮
|
- 曲线弹窗窗口已实现,但当前主页面尚未提供入口按钮
|
||||||
- 报告生成对新旧曲线视窗字段的使用口径后续建议统一
|
- 报告生成对新旧曲线视窗字段的使用口径后续建议统一
|
||||||
- PCI2312 真设备链路需要在现场确认 DI/DO 接线、电平有效性、报警灯供电方式和 DLL 随发布产物部署方式
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -691,12 +561,9 @@ PowerShell -ExecutionPolicy Bypass -File .\publish.ps1
|
|||||||
1. **DAQ 仍是最高风险点**:厂商 DLL、设备 ID、采样参数、输入量程和触发时序都依赖现场。
|
1. **DAQ 仍是最高风险点**:厂商 DLL、设备 ID、采样参数、输入量程和触发时序都依赖现场。
|
||||||
2. **Modbus 地址和端口仍需现场核实**:虽然当前实现已支持 3 模块映射,但设备地址/串口号应视现场配置为准。
|
2. **Modbus 地址和端口仍需现场核实**:虽然当前实现已支持 3 模块映射,但设备地址/串口号应视现场配置为准。
|
||||||
3. **电源与负载真实驱动已接入,但控制命令支持度需按实物验证**。
|
3. **电源与负载真实驱动已接入,但控制命令支持度需按实物验证**。
|
||||||
4. **PCI2312 急停报警链路依赖现场接线和 DLL 部署**:DI/DO 通道号、高/低有效配置、报警灯供电、`PCI2312_64.dll` 位数和搜索路径都必须逐项核实。
|
4. **所有长流程应贯穿 `CancellationToken`**,中止后优先保证设备回到安全状态。
|
||||||
5. **软件安全链路不能替代硬件安全回路**:`SafetyMonitorService` 通过轮询和软件命令下发关断,适合作为上位机联动保护;最终急停/互锁仍应由硬件电气回路兜底。
|
5. **示波器不纳入软件控制**,仅作为线下校准基准。
|
||||||
6. **状态重置与硬件解除都必须确认急停输入已复位**:当前代码在 DI 仍有效时拒绝手动重置;硬件急停锁存后只有检测到 DI 复位才会自动关闭报警灯并清除软件急停状态,避免误复位。
|
6. **任何涉及数据模型持久化格式的修改,都要考虑旧项目兼容**。
|
||||||
7. **所有长流程应贯穿 `CancellationToken`**,中止后优先保证设备回到安全状态。
|
|
||||||
8. **示波器不纳入软件控制**,仅作为线下校准基准。
|
|
||||||
9. **任何涉及数据模型持久化格式的修改,都要考虑旧项目兼容**。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -706,5 +573,4 @@ PowerShell -ExecutionPolicy Bypass -File .\publish.ps1
|
|||||||
2. 改项目结构前,先检查 `run.ps1`、`publish.ps1`、`Seed`、测试项目是否受影响。
|
2. 改项目结构前,先检查 `run.ps1`、`publish.ps1`、`Seed`、测试项目是否受影响。
|
||||||
3. 改曲线页前,注意当前已经有“主页面 + 弹窗 + 报告图”的三处视窗相关逻辑。
|
3. 改曲线页前,注意当前已经有“主页面 + 弹窗 + 报告图”的三处视窗相关逻辑。
|
||||||
4. 改报告前,确认 `ReportViewModel`、`Logic/Report`、`WebView2` 预览链路同时保持可用。
|
4. 改报告前,确认 `ReportViewModel`、`Logic/Report`、`WebView2` 预览链路同时保持可用。
|
||||||
5. 改设备安全链路前,先确认 `ISafetyDio`、`Pci2312SafetyDio`、`ConfigurableSafetyDio`、`SafetyMonitorService`、主窗口急停按钮和设置页 PCI2312 配置同步调整。
|
5. 若发现文档内容再次与代码偏离,应优先更新本文档,而不是继续在过时约束上开发。
|
||||||
6. 若发现文档内容再次与代码偏离,应优先更新本文档,而不是继续在过时约束上开发。
|
|
||||||
|
|||||||
Binary file not shown.
BIN
Docs/用户使用手册.docx
Normal file
BIN
Docs/用户使用手册.docx
Normal file
Binary file not shown.
2148
Docs/用户使用手册.md
Normal file
2148
Docs/用户使用手册.md
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,4 +0,0 @@
|
|||||||
2026-07-12 10:04:51.673 | RX | 05 03 30 00 00 00 17 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FE B7 00 1D 00 00 00 00 00 00 00 00 00 14 FF E9 00 00 00 00 00 00 00 14 33 12 | CRC OK | 150ms
|
|
||||||
2026-07-12 10:04:51.522 | TX | 05 03 00 00 00 18 44 44 | CRC OK | 0ms
|
|
||||||
2026-07-12 10:02:53.259 | RX | 04 03 30 03 E3 03 E5 03 E5 03 E4 03 E9 03 E3 03 E7 03 E6 03 E4 03 E6 03 E6 03 E3 03 E5 03 E7 03 E5 03 E3 03 E5 03 E5 03 E3 03 E5 03 E4 03 E3 03 E6 00 00 A9 D0 | CRC OK | 139ms
|
|
||||||
2026-07-12 10:02:53.117 | TX | 04 03 00 00 00 18 45 95 | CRC OK | 0ms
|
|
||||||
@ -57,10 +57,6 @@ public sealed class PowerSupplyOptions
|
|||||||
public string IdnCommand { get; set; } = "*IDN?";
|
public string IdnCommand { get; set; } = "*IDN?";
|
||||||
public string VoltageCommand { get; set; } = "MEASure:VOLTage?";
|
public string VoltageCommand { get; set; } = "MEASure:VOLTage?";
|
||||||
public string CurrentCommand { get; set; } = "MEASure:CURRent?";
|
public string CurrentCommand { get; set; } = "MEASure:CURRent?";
|
||||||
public string SetVoltageCommand { get; set; } = "VOLT {0}";
|
|
||||||
public string SetCurrentLimitCommand { get; set; } = "CURR {0}";
|
|
||||||
public string OutputStateCommand { get; set; } = "OUTP?";
|
|
||||||
public string ErrorCommand { get; set; } = "SYST:ERR?";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum LoadQueryMode
|
public enum LoadQueryMode
|
||||||
@ -82,11 +78,6 @@ public sealed class LoadOptions
|
|||||||
public int Channel { get; set; } = 1;
|
public int Channel { get; set; } = 1;
|
||||||
public bool UseFetch { get; set; } = true;
|
public bool UseFetch { get; set; } = true;
|
||||||
public LoadQueryMode QueryMode { get; set; } = LoadQueryMode.Combined;
|
public LoadQueryMode QueryMode { get; set; } = LoadQueryMode.Combined;
|
||||||
public string SetModeCommand { get; set; } = "";
|
|
||||||
public string SetValueCommand { get; set; } = "{0} {1}";
|
|
||||||
public string ResetCommand { get; set; } = "*RST";
|
|
||||||
public string InputStateCommand { get; set; } = "INP?";
|
|
||||||
public string ErrorCommand { get; set; } = "SYST:ERR?";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class DaqOptions
|
public sealed class DaqOptions
|
||||||
|
|||||||
@ -61,10 +61,8 @@ public sealed class ConfigurablePowerSupply : IPowerSupply
|
|||||||
public Task SetVoltageAsync(double volts, CancellationToken ct = default) => Current.SetVoltageAsync(volts, ct);
|
public Task SetVoltageAsync(double volts, CancellationToken ct = default) => Current.SetVoltageAsync(volts, ct);
|
||||||
public Task SetCurrentLimitAsync(double amps, CancellationToken ct = default) => Current.SetCurrentLimitAsync(amps, ct);
|
public Task SetCurrentLimitAsync(double amps, CancellationToken ct = default) => Current.SetCurrentLimitAsync(amps, ct);
|
||||||
public Task OutputAsync(bool on, CancellationToken ct = default) => Current.OutputAsync(on, ct);
|
public Task OutputAsync(bool on, CancellationToken ct = default) => Current.OutputAsync(on, ct);
|
||||||
public Task<bool> GetOutputStateAsync(CancellationToken ct = default) => Current.GetOutputStateAsync(ct);
|
|
||||||
public Task<(double volts, double amps)> ReadAsync(CancellationToken ct = default) => Current.ReadAsync(ct);
|
public Task<(double volts, double amps)> ReadAsync(CancellationToken ct = default) => Current.ReadAsync(ct);
|
||||||
public Task<string> QueryIdentityAsync(CancellationToken ct = default) => Current.QueryIdentityAsync(ct);
|
public Task<string> QueryIdentityAsync(CancellationToken ct = default) => Current.QueryIdentityAsync(ct);
|
||||||
public Task<string> QueryErrorAsync(CancellationToken ct = default) => Current.QueryErrorAsync(ct);
|
|
||||||
private void Forward(object? sender, bool connected) => ConnectionChanged?.Invoke(this, connected);
|
private void Forward(object? sender, bool connected) => ConnectionChanged?.Invoke(this, connected);
|
||||||
private static async Task SafeDisconnect(IDeviceConnection device) { try { await device.DisconnectAsync(); } catch (NotImplementedException) { } }
|
private static async Task SafeDisconnect(IDeviceConnection device) { try { await device.DisconnectAsync(); } catch (NotImplementedException) { } }
|
||||||
}
|
}
|
||||||
@ -88,12 +86,9 @@ public sealed class ConfigurableLoad : ILoad
|
|||||||
public Task SetModeAsync(LoadMode mode, CancellationToken ct = default) => Current.SetModeAsync(mode, ct);
|
public Task SetModeAsync(LoadMode mode, CancellationToken ct = default) => Current.SetModeAsync(mode, ct);
|
||||||
public Task SetValueAsync(double value, CancellationToken ct = default) => Current.SetValueAsync(value, ct);
|
public Task SetValueAsync(double value, CancellationToken ct = default) => Current.SetValueAsync(value, ct);
|
||||||
public Task InputAsync(bool on, CancellationToken ct = default) => Current.InputAsync(on, ct);
|
public Task InputAsync(bool on, CancellationToken ct = default) => Current.InputAsync(on, ct);
|
||||||
public Task<bool> GetInputStateAsync(CancellationToken ct = default) => Current.GetInputStateAsync(ct);
|
|
||||||
public Task ResetAsync(CancellationToken ct = default) => Current.ResetAsync(ct);
|
|
||||||
public Task<(double volts, double amps)> ReadAsync(CancellationToken ct = default) => Current.ReadAsync(ct);
|
public Task<(double volts, double amps)> ReadAsync(CancellationToken ct = default) => Current.ReadAsync(ct);
|
||||||
public Task<PowerReading> ReadPowerAsync(CancellationToken ct = default) => Current.ReadPowerAsync(ct);
|
public Task<PowerReading> ReadPowerAsync(CancellationToken ct = default) => Current.ReadPowerAsync(ct);
|
||||||
public Task<string> QueryIdentityAsync(CancellationToken ct = default) => Current.QueryIdentityAsync(ct);
|
public Task<string> QueryIdentityAsync(CancellationToken ct = default) => Current.QueryIdentityAsync(ct);
|
||||||
public Task<string> QueryErrorAsync(CancellationToken ct = default) => Current.QueryErrorAsync(ct);
|
|
||||||
private void Forward(object? sender, bool connected) => ConnectionChanged?.Invoke(this, connected);
|
private void Forward(object? sender, bool connected) => ConnectionChanged?.Invoke(this, connected);
|
||||||
private static async Task SafeDisconnect(IDeviceConnection device) { try { await device.DisconnectAsync(); } catch (NotImplementedException) { } }
|
private static async Task SafeDisconnect(IDeviceConnection device) { try { await device.DisconnectAsync(); } catch (NotImplementedException) { } }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,7 @@ public sealed class MockDaq : MockDeviceBase, IDaq
|
|||||||
private double? _nextTriggerSec;
|
private double? _nextTriggerSec;
|
||||||
|
|
||||||
/// <summary>满量程电压(V)。</summary>
|
/// <summary>满量程电压(V)。</summary>
|
||||||
public double NominalVoltage { get; set; } = 5.0;
|
public double NominalVoltage { get; set; } = 28.0;
|
||||||
|
|
||||||
/// <summary>满量程电流(A)。</summary>
|
/// <summary>满量程电流(A)。</summary>
|
||||||
public double NominalCurrent { get; set; } = 1.0;
|
public double NominalCurrent { get; set; } = 1.0;
|
||||||
@ -49,9 +49,6 @@ public sealed class MockDaq : MockDeviceBase, IDaq
|
|||||||
public async Task<CurveData> CaptureAsync(
|
public async Task<CurveData> CaptureAsync(
|
||||||
int[] channels, int sampleRateHz, double durationSec, CancellationToken ct = default)
|
int[] channels, int sampleRateHz, double durationSec, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(channels);
|
|
||||||
if (channels.Length == 0)
|
|
||||||
throw new ArgumentException("At least one channel must be requested.", nameof(channels));
|
|
||||||
if (sampleRateHz <= 0) throw new ArgumentOutOfRangeException(nameof(sampleRateHz));
|
if (sampleRateHz <= 0) throw new ArgumentOutOfRangeException(nameof(sampleRateHz));
|
||||||
if (durationSec <= 0) throw new ArgumentOutOfRangeException(nameof(durationSec));
|
if (durationSec <= 0) throw new ArgumentOutOfRangeException(nameof(durationSec));
|
||||||
|
|
||||||
@ -60,18 +57,9 @@ public sealed class MockDaq : MockDeviceBase, IDaq
|
|||||||
await Task.Delay(simulatedDelayMs, ct).ConfigureAwait(false);
|
await Task.Delay(simulatedDelayMs, ct).ConfigureAwait(false);
|
||||||
|
|
||||||
int n = (int)Math.Round(sampleRateHz * durationSec);
|
int n = (int)Math.Round(sampleRateHz * durationSec);
|
||||||
var cardVoltage = new double[n];
|
var v = new double[n];
|
||||||
var cardCurrent = new double[n];
|
var i = new double[n];
|
||||||
double dt = 1.0 / sampleRateHz;
|
double dt = 1.0 / sampleRateHz;
|
||||||
int channelCount = Math.Max(1, _opts.ChannelCount);
|
|
||||||
var requestedChannels = channels.Distinct().ToArray();
|
|
||||||
foreach (int channel in requestedChannels)
|
|
||||||
{
|
|
||||||
if (channel < 0 || channel >= channelCount)
|
|
||||||
throw new ArgumentOutOfRangeException(
|
|
||||||
nameof(channels),
|
|
||||||
$"Requested channel {channel} is outside the configured PCIe8586 channel count {channelCount}.");
|
|
||||||
}
|
|
||||||
|
|
||||||
bool rising = NextCaptureIsRising;
|
bool rising = NextCaptureIsRising;
|
||||||
double delay = rising ? OnDelaySec : OffDelaySec;
|
double delay = rising ? OnDelaySec : OffDelaySec;
|
||||||
@ -93,7 +81,7 @@ public sealed class MockDaq : MockDeviceBase, IDaq
|
|||||||
voltage = tRel <= 0 ? NominalVoltage : NominalVoltage * Math.Exp(-tRel / TauSec);
|
voltage = tRel <= 0 ? NominalVoltage : NominalVoltage * Math.Exp(-tRel / TauSec);
|
||||||
}
|
}
|
||||||
voltage += NextGaussian() * NominalVoltage * 0.005;
|
voltage += NextGaussian() * NominalVoltage * 0.005;
|
||||||
cardVoltage[k] = voltage;
|
v[k] = voltage;
|
||||||
|
|
||||||
double current;
|
double current;
|
||||||
if (rising)
|
if (rising)
|
||||||
@ -101,10 +89,16 @@ public sealed class MockDaq : MockDeviceBase, IDaq
|
|||||||
else
|
else
|
||||||
current = tRel <= 0 ? NominalCurrent : NominalCurrent * Math.Exp(-tRel / TauSec);
|
current = tRel <= 0 ? NominalCurrent : NominalCurrent * Math.Exp(-tRel / TauSec);
|
||||||
current += NextGaussian() * NominalCurrent * 0.01;
|
current += NextGaussian() * NominalCurrent * 0.01;
|
||||||
cardCurrent[k] = current;
|
i[k] = current;
|
||||||
}
|
}
|
||||||
|
|
||||||
return BuildCurve(cardVoltage, cardCurrent, sampleRateHz, triggerSec, triggerIndex: 0, requestedChannels[0]);
|
return new CurveData
|
||||||
|
{
|
||||||
|
SampleRateHz = sampleRateHz,
|
||||||
|
TriggerTimeSec = triggerSec,
|
||||||
|
Voltage = v,
|
||||||
|
Current = i
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<double[]> ReadInstantAsync(int[] channels, CancellationToken ct = default)
|
public Task<double[]> ReadInstantAsync(int[] channels, CancellationToken ct = default)
|
||||||
@ -118,7 +112,7 @@ public sealed class MockDaq : MockDeviceBase, IDaq
|
|||||||
? DaqChannelRole.Unconfigured
|
? DaqChannelRole.Unconfigured
|
||||||
: Pcie8586Daq.ParseRole(cfg.Role);
|
: Pcie8586Daq.ParseRole(cfg.Role);
|
||||||
|
|
||||||
double cardValue = role switch
|
result[k] = role switch
|
||||||
{
|
{
|
||||||
DaqChannelRole.OutputVoltage or DaqChannelRole.InputVoltage =>
|
DaqChannelRole.OutputVoltage or DaqChannelRole.InputVoltage =>
|
||||||
NominalVoltage * (1 + (_rng.NextDouble() - 0.5) * 0.004),
|
NominalVoltage * (1 + (_rng.NextDouble() - 0.5) * 0.004),
|
||||||
@ -126,7 +120,6 @@ public sealed class MockDaq : MockDeviceBase, IDaq
|
|||||||
NominalCurrent * (1 + (_rng.NextDouble() - 0.5) * 0.02),
|
NominalCurrent * (1 + (_rng.NextDouble() - 0.5) * 0.02),
|
||||||
_ => 0
|
_ => 0
|
||||||
};
|
};
|
||||||
result[k] = ApplyCalibration(cardValue, cfg);
|
|
||||||
}
|
}
|
||||||
return Task.FromResult(result);
|
return Task.FromResult(result);
|
||||||
}
|
}
|
||||||
@ -144,7 +137,7 @@ public sealed class MockDaq : MockDeviceBase, IDaq
|
|||||||
NextCaptureIsRising = request.IsRising;
|
NextCaptureIsRising = request.IsRising;
|
||||||
await action(ct).ConfigureAwait(false);
|
await action(ct).ConfigureAwait(false);
|
||||||
_nextTriggerSec = request.PreTriggerMs / 1_000.0;
|
_nextTriggerSec = request.PreTriggerMs / 1_000.0;
|
||||||
var curve = await CaptureAsync(Enumerable.Range(0, Math.Max(1, _opts.ChannelCount)).ToArray(), sampleRate, durationSec, ct).ConfigureAwait(false);
|
var curve = await CaptureAsync(new[] { 0 }, sampleRate, durationSec, ct).ConfigureAwait(false);
|
||||||
int triggerIndex = (int)Math.Round(sampleRate * request.PreTriggerMs / 1_000.0);
|
int triggerIndex = (int)Math.Round(sampleRate * request.PreTriggerMs / 1_000.0);
|
||||||
return new CurveData
|
return new CurveData
|
||||||
{
|
{
|
||||||
@ -153,60 +146,14 @@ public sealed class MockDaq : MockDeviceBase, IDaq
|
|||||||
TriggerTimeSec = triggerIndex / (double)curve.SampleRateHz,
|
TriggerTimeSec = triggerIndex / (double)curve.SampleRateHz,
|
||||||
Voltage = curve.Voltage,
|
Voltage = curve.Voltage,
|
||||||
Current = curve.Current,
|
Current = curve.Current,
|
||||||
EngineeringChannels = curve.EngineeringChannels
|
EngineeringChannels = new[]
|
||||||
|
{
|
||||||
|
new CurveChannelData(0, DaqChannelRole.OutputVoltage, "模拟输出电压", "V", 1, 0, curve.Voltage),
|
||||||
|
new CurveChannelData(1, DaqChannelRole.OutputCurrent, "模拟输出电流", "A", 1, 0, curve.Current!)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private CurveData BuildCurve(
|
|
||||||
double[] cardVoltage,
|
|
||||||
double[] cardCurrent,
|
|
||||||
int sampleRateHz,
|
|
||||||
double triggerSec,
|
|
||||||
int triggerIndex,
|
|
||||||
int primaryPhysicalChannel)
|
|
||||||
{
|
|
||||||
var engineering = new List<CurveChannelData>();
|
|
||||||
int channelCount = Math.Max(1, _opts.ChannelCount);
|
|
||||||
for (int channel = 0; channel < channelCount; channel++)
|
|
||||||
{
|
|
||||||
var cfg = _opts.Channels.FirstOrDefault(x => x.PhysicalChannel == channel)
|
|
||||||
?? new DaqChannelOptions { PhysicalChannel = channel, Name = $"CH{channel}" };
|
|
||||||
var role = Pcie8586Daq.ParseRole(cfg.Role);
|
|
||||||
var source = role is DaqChannelRole.OutputCurrent or DaqChannelRole.InputCurrent
|
|
||||||
? cardCurrent
|
|
||||||
: cardVoltage;
|
|
||||||
var values = source.Select(x => ApplyCalibration(x, cfg)).ToArray();
|
|
||||||
engineering.Add(new CurveChannelData(
|
|
||||||
channel, role, cfg.Name, UnitFor(role), cfg.Scale, cfg.Offset, values));
|
|
||||||
}
|
|
||||||
|
|
||||||
var outputVoltage = engineering.Where(x => x.Role == DaqChannelRole.OutputVoltage).ToArray();
|
|
||||||
var primary = outputVoltage.Length == 1
|
|
||||||
? outputVoltage[0]
|
|
||||||
: engineering.FirstOrDefault(x => x.PhysicalChannel == primaryPhysicalChannel) ?? engineering.FirstOrDefault();
|
|
||||||
var current = engineering.FirstOrDefault(x => x.Role == DaqChannelRole.OutputCurrent)?.Samples;
|
|
||||||
|
|
||||||
return new CurveData
|
|
||||||
{
|
|
||||||
SampleRateHz = sampleRateHz,
|
|
||||||
TriggerSampleIndex = triggerIndex,
|
|
||||||
TriggerTimeSec = triggerSec,
|
|
||||||
Voltage = primary?.Samples ?? Array.Empty<double>(),
|
|
||||||
Current = current,
|
|
||||||
EngineeringChannels = engineering
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static double ApplyCalibration(double value, DaqChannelOptions? cfg) =>
|
|
||||||
cfg is null ? value : value * cfg.Scale + cfg.Offset;
|
|
||||||
|
|
||||||
private static string UnitFor(DaqChannelRole role) => role switch
|
|
||||||
{
|
|
||||||
DaqChannelRole.OutputVoltage or DaqChannelRole.InputVoltage => "V",
|
|
||||||
DaqChannelRole.OutputCurrent or DaqChannelRole.InputCurrent => "A",
|
|
||||||
_ => ""
|
|
||||||
};
|
|
||||||
|
|
||||||
/// <summary>由 ReadInstantAsync 衍生的"电流读数"接口(基础测试页用)。</summary>
|
/// <summary>由 ReadInstantAsync 衍生的"电流读数"接口(基础测试页用)。</summary>
|
||||||
public (double volts, double amps) ReadVoltageCurrent(int channel, bool channelOn)
|
public (double volts, double amps) ReadVoltageCurrent(int channel, bool channelOn)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -36,17 +36,6 @@ public sealed class MockLoad : MockDeviceBase, ILoad
|
|||||||
_input = on;
|
_input = on;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<bool> GetInputStateAsync(CancellationToken ct = default) =>
|
|
||||||
Task.FromResult(_input);
|
|
||||||
|
|
||||||
public async Task ResetAsync(CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
await Task.Delay(10, ct).ConfigureAwait(false);
|
|
||||||
_mode = LoadMode.CC;
|
|
||||||
_value = 0;
|
|
||||||
_input = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<(double volts, double amps)> ReadAsync(CancellationToken ct = default)
|
public Task<(double volts, double amps)> ReadAsync(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
if (!_input) return Task.FromResult((0.0, 0.0));
|
if (!_input) return Task.FromResult((0.0, 0.0));
|
||||||
@ -68,7 +57,4 @@ public sealed class MockLoad : MockDeviceBase, ILoad
|
|||||||
|
|
||||||
public Task<string> QueryIdentityAsync(CancellationToken ct = default) =>
|
public Task<string> QueryIdentityAsync(CancellationToken ct = default) =>
|
||||||
Task.FromResult("ITECH,IT8702P,MOCK,1.0");
|
Task.FromResult("ITECH,IT8702P,MOCK,1.0");
|
||||||
|
|
||||||
public Task<string> QueryErrorAsync(CancellationToken ct = default) =>
|
|
||||||
Task.FromResult("0,\"No error\"");
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,9 +36,6 @@ public sealed class MockPowerSupply : MockDeviceBase, IPowerSupply
|
|||||||
_output = on;
|
_output = on;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<bool> GetOutputStateAsync(CancellationToken ct = default) =>
|
|
||||||
Task.FromResult(_output);
|
|
||||||
|
|
||||||
public Task<(double volts, double amps)> ReadAsync(CancellationToken ct = default)
|
public Task<(double volts, double amps)> ReadAsync(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
if (!_output) return Task.FromResult((0.0, 0.0));
|
if (!_output) return Task.FromResult((0.0, 0.0));
|
||||||
@ -50,7 +47,4 @@ public sealed class MockPowerSupply : MockDeviceBase, IPowerSupply
|
|||||||
|
|
||||||
public Task<string> QueryIdentityAsync(CancellationToken ct = default) =>
|
public Task<string> QueryIdentityAsync(CancellationToken ct = default) =>
|
||||||
Task.FromResult("UNI-T,UDP5080-100,MOCK,1.0");
|
Task.FromResult("UNI-T,UDP5080-100,MOCK,1.0");
|
||||||
|
|
||||||
public Task<string> QueryErrorAsync(CancellationToken ct = default) =>
|
|
||||||
Task.FromResult("0,\"No error\"");
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,7 +18,6 @@ public sealed class It87xxLoad : ILoad
|
|||||||
private TcpClient? _client;
|
private TcpClient? _client;
|
||||||
private NetworkStream? _stream;
|
private NetworkStream? _stream;
|
||||||
private bool _isConnected;
|
private bool _isConnected;
|
||||||
private LoadMode _mode = LoadMode.CC;
|
|
||||||
|
|
||||||
public It87xxLoad(IOptions<DeviceOptions> options, ILogger<It87xxLoad> logger)
|
public It87xxLoad(IOptions<DeviceOptions> options, ILogger<It87xxLoad> logger)
|
||||||
{
|
{
|
||||||
@ -78,42 +77,15 @@ public sealed class It87xxLoad : ILoad
|
|||||||
SetConnected(false);
|
SetConnected(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SetModeAsync(LoadMode mode, CancellationToken ct = default)
|
public Task SetModeAsync(LoadMode mode, CancellationToken ct = default) =>
|
||||||
{
|
throw new NotSupportedException("IT8702P load control commands are not enabled; this driver only reads V/I/P.");
|
||||||
_mode = mode;
|
|
||||||
if (string.IsNullOrWhiteSpace(_options.SetModeCommand))
|
|
||||||
{
|
|
||||||
if (mode == LoadMode.CC)
|
|
||||||
return;
|
|
||||||
|
|
||||||
throw new NotSupportedException($"IT8702P mode {mode} is not configured.");
|
public Task SetValueAsync(double value, CancellationToken ct = default) =>
|
||||||
}
|
throw new NotSupportedException("IT8702P load control commands are not enabled; this driver only reads V/I/P.");
|
||||||
|
|
||||||
await SendAsync(FormatCommand(_options.SetModeCommand, ModeToken(mode)), ct).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task SetValueAsync(double value, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
ValidateSetpoint(value, nameof(value));
|
|
||||||
return SendAsync(FormatCommand(_options.SetValueCommand, ValueCommand(_mode), value), ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task InputAsync(bool on, CancellationToken ct = default) =>
|
public Task InputAsync(bool on, CancellationToken ct = default) =>
|
||||||
SendAsync(on ? "INP ON" : "INP OFF", ct);
|
SendAsync(on ? "INP ON" : "INP OFF", ct);
|
||||||
|
|
||||||
public async Task<bool> GetInputStateAsync(CancellationToken ct = default) =>
|
|
||||||
ParseSwitchState(await QueryAsync(_options.InputStateCommand, ct).ConfigureAwait(false));
|
|
||||||
|
|
||||||
public async Task ResetAsync(CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(_options.ResetCommand))
|
|
||||||
await SendAsync(_options.ResetCommand, ct).ConfigureAwait(false);
|
|
||||||
|
|
||||||
await SendAsync("SYST:REM", ct).ConfigureAwait(false);
|
|
||||||
await SendAsync($"CHAN {Math.Clamp(_options.Channel, 1, 8)}", ct).ConfigureAwait(false);
|
|
||||||
_mode = LoadMode.CC;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<(double volts, double amps)> ReadAsync(CancellationToken ct = default)
|
public async Task<(double volts, double amps)> ReadAsync(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var reading = await ReadPowerAsync(ct).ConfigureAwait(false);
|
var reading = await ReadPowerAsync(ct).ConfigureAwait(false);
|
||||||
@ -149,8 +121,6 @@ public sealed class It87xxLoad : ILoad
|
|||||||
|
|
||||||
public Task<string> QueryIdentityAsync(CancellationToken ct = default) => QueryAsync("*IDN?", ct);
|
public Task<string> QueryIdentityAsync(CancellationToken ct = default) => QueryAsync("*IDN?", ct);
|
||||||
|
|
||||||
public Task<string> QueryErrorAsync(CancellationToken ct = default) => QueryAsync(_options.ErrorCommand, ct);
|
|
||||||
|
|
||||||
public async Task<string> QueryAsync(string command, CancellationToken ct = default)
|
public async Task<string> QueryAsync(string command, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await _gate.WaitAsync(ct).ConfigureAwait(false);
|
await _gate.WaitAsync(ct).ConfigureAwait(false);
|
||||||
@ -205,40 +175,6 @@ public sealed class It87xxLoad : ILoad
|
|||||||
private static double ParseNumber(string value) =>
|
private static double ParseNumber(string value) =>
|
||||||
double.Parse(value.Trim(), NumberStyles.Float, CultureInfo.InvariantCulture);
|
double.Parse(value.Trim(), NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
public static bool ParseSwitchState(string response)
|
|
||||||
{
|
|
||||||
string value = response.Trim().Trim('"');
|
|
||||||
if (value.Equals("ON", StringComparison.OrdinalIgnoreCase) || value == "1")
|
|
||||||
return true;
|
|
||||||
if (value.Equals("OFF", StringComparison.OrdinalIgnoreCase) || value == "0")
|
|
||||||
return false;
|
|
||||||
|
|
||||||
throw new FormatException($"IT8702P response is not a switch state: {response}");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ValidateSetpoint(double value, string name)
|
|
||||||
{
|
|
||||||
if (!double.IsFinite(value) || value < 0)
|
|
||||||
throw new ArgumentOutOfRangeException(name, value, "Setpoint must be a finite non-negative number.");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string FormatCommand(string template, params object[] args)
|
|
||||||
{
|
|
||||||
string commandTemplate = string.IsNullOrWhiteSpace(template) ? "{0} {1}" : template.Trim();
|
|
||||||
return string.Format(CultureInfo.InvariantCulture, commandTemplate, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string ModeToken(LoadMode mode) => mode.ToString().ToUpperInvariant();
|
|
||||||
|
|
||||||
private static string ValueCommand(LoadMode mode) => mode switch
|
|
||||||
{
|
|
||||||
LoadMode.CC => "CURR",
|
|
||||||
LoadMode.CV => "VOLT",
|
|
||||||
LoadMode.CR => "RES",
|
|
||||||
LoadMode.CP => "POW",
|
|
||||||
_ => "CURR"
|
|
||||||
};
|
|
||||||
|
|
||||||
private async Task WriteLineAsync(NetworkStream stream, string command, CancellationToken ct)
|
private async Task WriteLineAsync(NetworkStream stream, string command, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var payload = Encoding.ASCII.GetBytes(command.TrimEnd('\r', '\n') + Terminator);
|
var payload = Encoding.ASCII.GetBytes(command.TrimEnd('\r', '\n') + Terminator);
|
||||||
|
|||||||
@ -13,10 +13,8 @@ public sealed class Udp5080Placeholder : IPowerSupply
|
|||||||
public Task SetVoltageAsync(double volts, CancellationToken ct = default) => throw new NotImplementedException();
|
public Task SetVoltageAsync(double volts, CancellationToken ct = default) => throw new NotImplementedException();
|
||||||
public Task SetCurrentLimitAsync(double amps, CancellationToken ct = default) => throw new NotImplementedException();
|
public Task SetCurrentLimitAsync(double amps, CancellationToken ct = default) => throw new NotImplementedException();
|
||||||
public Task OutputAsync(bool on, CancellationToken ct = default) => throw new NotImplementedException();
|
public Task OutputAsync(bool on, CancellationToken ct = default) => throw new NotImplementedException();
|
||||||
public Task<bool> GetOutputStateAsync(CancellationToken ct = default) => throw new NotImplementedException();
|
|
||||||
public Task<(double volts, double amps)> ReadAsync(CancellationToken ct = default) => throw new NotImplementedException();
|
public Task<(double volts, double amps)> ReadAsync(CancellationToken ct = default) => throw new NotImplementedException();
|
||||||
public Task<string> QueryIdentityAsync(CancellationToken ct = default) => throw new NotImplementedException();
|
public Task<string> QueryIdentityAsync(CancellationToken ct = default) => throw new NotImplementedException();
|
||||||
public Task<string> QueryErrorAsync(CancellationToken ct = default) => throw new NotImplementedException();
|
|
||||||
private void Raise(bool s) => ConnectionChanged?.Invoke(this, s);
|
private void Raise(bool s) => ConnectionChanged?.Invoke(this, s);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -61,7 +61,7 @@ public sealed class Udp5080 : IPowerSupply
|
|||||||
_client = null;
|
_client = null;
|
||||||
client.Dispose();
|
client.Dispose();
|
||||||
SetConnected(false);
|
SetConnected(false);
|
||||||
throw new InvalidOperationException($"UDP5080 connection failed: {_options.Host}:{_options.Port}, {ex.Message}", ex);
|
throw new InvalidOperationException($"UDP5080 连接失败:{_options.Host}:{_options.Port},{ex.Message}", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,24 +75,15 @@ public sealed class Udp5080 : IPowerSupply
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task SetVoltageAsync(double volts, CancellationToken ct = default)
|
public Task SetVoltageAsync(double volts, CancellationToken ct = default) =>
|
||||||
{
|
throw new NotSupportedException("UDP5080 当前仅接入实时测量,未启用未验证的电压设置 SCPI 命令。");
|
||||||
ValidateSetpoint(volts, nameof(volts));
|
|
||||||
return SendAsync(FormatCommand(_options.SetVoltageCommand, volts), ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task SetCurrentLimitAsync(double amps, CancellationToken ct = default)
|
public Task SetCurrentLimitAsync(double amps, CancellationToken ct = default) =>
|
||||||
{
|
throw new NotSupportedException("UDP5080 当前仅接入实时测量,未启用未验证的电流限制 SCPI 命令。");
|
||||||
ValidateSetpoint(amps, nameof(amps));
|
|
||||||
return SendAsync(FormatCommand(_options.SetCurrentLimitCommand, amps), ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task OutputAsync(bool on, CancellationToken ct = default) =>
|
public Task OutputAsync(bool on, CancellationToken ct = default) =>
|
||||||
SendAsync(on ? ":OUTput ON" : ":OUTput OFF", ct);
|
SendAsync(on ? ":OUTput ON" : ":OUTput OFF", ct);
|
||||||
|
|
||||||
public async Task<bool> GetOutputStateAsync(CancellationToken ct = default) =>
|
|
||||||
ParseSwitchState(await QueryAsync(_options.OutputStateCommand, ct).ConfigureAwait(false));
|
|
||||||
|
|
||||||
public async Task<(double volts, double amps)> ReadAsync(CancellationToken ct = default)
|
public async Task<(double volts, double amps)> ReadAsync(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@ -110,8 +101,6 @@ public sealed class Udp5080 : IPowerSupply
|
|||||||
|
|
||||||
public Task<string> QueryIdentityAsync(CancellationToken ct = default) => QueryAsync(_options.IdnCommand, ct);
|
public Task<string> QueryIdentityAsync(CancellationToken ct = default) => QueryAsync(_options.IdnCommand, ct);
|
||||||
|
|
||||||
public Task<string> QueryErrorAsync(CancellationToken ct = default) => QueryAsync(_options.ErrorCommand, ct);
|
|
||||||
|
|
||||||
public async Task<string> QueryAsync(string command, CancellationToken ct = default)
|
public async Task<string> QueryAsync(string command, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await _gate.WaitAsync(ct).ConfigureAwait(false);
|
await _gate.WaitAsync(ct).ConfigureAwait(false);
|
||||||
@ -123,7 +112,7 @@ public sealed class Udp5080 : IPowerSupply
|
|||||||
catch (Exception ex) when (ex is SocketException or IOException or OperationCanceledException)
|
catch (Exception ex) when (ex is SocketException or IOException or OperationCanceledException)
|
||||||
{
|
{
|
||||||
SetConnected(false);
|
SetConnected(false);
|
||||||
throw new InvalidOperationException($"UDP5080 query failed: {command}, {ex.Message}", ex);
|
throw new InvalidOperationException($"UDP5080 查询失败:{command},{ex.Message}", ex);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@ -160,37 +149,14 @@ public sealed class Udp5080 : IPowerSupply
|
|||||||
response.Trim(),
|
response.Trim(),
|
||||||
@"[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?");
|
@"[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?");
|
||||||
if (!match.Success)
|
if (!match.Success)
|
||||||
throw new FormatException($"UDP5080 response does not contain a number: {response}");
|
throw new FormatException($"UDP5080 响应中没有数值:{response}");
|
||||||
|
|
||||||
var value = double.Parse(match.Value, CultureInfo.InvariantCulture);
|
var value = double.Parse(match.Value, CultureInfo.InvariantCulture);
|
||||||
if (!double.IsFinite(value))
|
if (!double.IsFinite(value))
|
||||||
throw new FormatException($"UDP5080 response number is not finite: {response}");
|
throw new FormatException($"UDP5080 响应数值无效:{response}");
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool ParseSwitchState(string response)
|
|
||||||
{
|
|
||||||
string value = response.Trim().Trim('"');
|
|
||||||
if (value.Equals("ON", StringComparison.OrdinalIgnoreCase) || value == "1")
|
|
||||||
return true;
|
|
||||||
if (value.Equals("OFF", StringComparison.OrdinalIgnoreCase) || value == "0")
|
|
||||||
return false;
|
|
||||||
|
|
||||||
throw new FormatException($"UDP5080 response is not a switch state: {response}");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ValidateSetpoint(double value, string name)
|
|
||||||
{
|
|
||||||
if (!double.IsFinite(value) || value < 0)
|
|
||||||
throw new ArgumentOutOfRangeException(name, value, "Setpoint must be a finite non-negative number.");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string FormatCommand(string template, double value)
|
|
||||||
{
|
|
||||||
string commandTemplate = string.IsNullOrWhiteSpace(template) ? "{0}" : template.Trim();
|
|
||||||
return string.Format(CultureInfo.InvariantCulture, commandTemplate, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private TimeSpan Timeout => TimeSpan.FromSeconds(_options.TimeoutSeconds > 0 ? _options.TimeoutSeconds : 2);
|
private TimeSpan Timeout => TimeSpan.FromSeconds(_options.TimeoutSeconds > 0 ? _options.TimeoutSeconds : 2);
|
||||||
private string Terminator => string.IsNullOrEmpty(_options.Terminator) ? "\n" : _options.Terminator;
|
private string Terminator => string.IsNullOrEmpty(_options.Terminator) ? "\n" : _options.Terminator;
|
||||||
|
|
||||||
@ -256,7 +222,7 @@ public sealed class Udp5080 : IPowerSupply
|
|||||||
private NetworkStream RequireStream()
|
private NetworkStream RequireStream()
|
||||||
{
|
{
|
||||||
if (!IsConnected || _stream is null)
|
if (!IsConnected || _stream is null)
|
||||||
throw new InvalidOperationException("UDP5080 is not connected.");
|
throw new InvalidOperationException("UDP5080 尚未连接。");
|
||||||
return _stream;
|
return _stream;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -31,17 +31,10 @@ public interface ILoad : IDeviceConnection
|
|||||||
/// <summary>开启 / 关闭负载输入。</summary>
|
/// <summary>开启 / 关闭负载输入。</summary>
|
||||||
Task InputAsync(bool on, CancellationToken ct = default);
|
Task InputAsync(bool on, CancellationToken ct = default);
|
||||||
|
|
||||||
/// <summary>从设备查询当前输入开关状态。</summary>
|
|
||||||
Task<bool> GetInputStateAsync(CancellationToken ct = default);
|
|
||||||
|
|
||||||
Task ResetAsync(CancellationToken ct = default);
|
|
||||||
|
|
||||||
/// <summary>读取实际电压、电流。</summary>
|
/// <summary>读取实际电压、电流。</summary>
|
||||||
Task<(double volts, double amps)> ReadAsync(CancellationToken ct = default);
|
Task<(double volts, double amps)> ReadAsync(CancellationToken ct = default);
|
||||||
|
|
||||||
Task<PowerReading> ReadPowerAsync(CancellationToken ct = default);
|
Task<PowerReading> ReadPowerAsync(CancellationToken ct = default);
|
||||||
|
|
||||||
Task<string> QueryIdentityAsync(CancellationToken ct = default);
|
Task<string> QueryIdentityAsync(CancellationToken ct = default);
|
||||||
|
|
||||||
Task<string> QueryErrorAsync(CancellationToken ct = default);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,15 +14,9 @@ public interface IPowerSupply : IDeviceConnection
|
|||||||
/// <summary>开启 / 关闭输出。</summary>
|
/// <summary>开启 / 关闭输出。</summary>
|
||||||
Task OutputAsync(bool on, CancellationToken ct = default);
|
Task OutputAsync(bool on, CancellationToken ct = default);
|
||||||
|
|
||||||
/// <summary>从设备查询当前输出开关状态。</summary>
|
|
||||||
Task<bool> GetOutputStateAsync(CancellationToken ct = default);
|
|
||||||
|
|
||||||
/// <summary>读取当前实际电压、电流。</summary>
|
/// <summary>读取当前实际电压、电流。</summary>
|
||||||
Task<(double volts, double amps)> ReadAsync(CancellationToken ct = default);
|
Task<(double volts, double amps)> ReadAsync(CancellationToken ct = default);
|
||||||
|
|
||||||
/// <summary>Query instrument identity for connection diagnostics.</summary>
|
/// <summary>Query instrument identity for connection diagnostics.</summary>
|
||||||
Task<string> QueryIdentityAsync(CancellationToken ct = default);
|
Task<string> QueryIdentityAsync(CancellationToken ct = default);
|
||||||
|
|
||||||
/// <summary>Query instrument error queue/status text.</summary>
|
|
||||||
Task<string> QueryErrorAsync(CancellationToken ct = default);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -375,14 +375,13 @@ public sealed class ReportGenerator
|
|||||||
})));
|
})));
|
||||||
|
|
||||||
private static void PowerTable(IContainer container, Project p) =>
|
private static void PowerTable(IContainer container, Project p) =>
|
||||||
DataTable(container, new[] { 0.58f, 0.78f, 0.78f, 0.86f, 0.78f, 0.78f, 0.86f, 0.78f, 0.86f, 0.72f, 0.68f },
|
DataTable(container, new[] { 0.62f, 0.9f, 0.9f, 0.9f, 0.9f, 0.9f, 0.9f, 0.76f },
|
||||||
new[] { "通道", "输入电压\nV", "输入电流\nA", "输入功率\nW", "输出电压\nV", "输出电流\nA", "输出功率\nW", "电压降\nV", "功率损耗\nW", "效率\n%", "状态" },
|
new[] { "通道", "输入电压\nV", "输入电流\nA", "输出电压\nV", "输出电流\nA", "功率损耗\nW", "效率\n%", "状态" },
|
||||||
p.PowerResults.Select(r => new ReportRow(r.Status, new[]
|
p.PowerResults.Select(r => new ReportRow(r.Status, new[]
|
||||||
{
|
{
|
||||||
$"CH{r.Channel:00}", Format(r.Vin, "F3"), Format(r.Iin, "F3"),
|
$"CH{r.Channel:00}", Format(r.Vin, "F3"), Format(r.Iin, "F3"),
|
||||||
Format(r.PowerIn, "F3"), Format(r.Vout, "F3"), Format(r.Iout, "F3"),
|
Format(r.Vout, "F3"), Format(r.Iout, "F3"),
|
||||||
Format(r.PowerOut, "F3"), Format(r.VoltageDrop, "F3"), Format(r.PowerLossW, "F3"),
|
Format(r.PowerLossW, "F3"), Format(r.EfficiencyPct, "F3"), StatusText(r.Status)
|
||||||
Format(r.EfficiencyPct, "F1"), StatusText(r.Status)
|
|
||||||
})));
|
})));
|
||||||
|
|
||||||
private static void ProtectTable(IContainer container, Project p) =>
|
private static void ProtectTable(IContainer container, Project p) =>
|
||||||
|
|||||||
@ -234,22 +234,18 @@ internal sealed class LegacyReportGenerator
|
|||||||
cd.ConstantColumn(50);
|
cd.ConstantColumn(50);
|
||||||
cd.RelativeColumn(); cd.RelativeColumn(); cd.RelativeColumn();
|
cd.RelativeColumn(); cd.RelativeColumn(); cd.RelativeColumn();
|
||||||
cd.RelativeColumn(); cd.RelativeColumn(); cd.RelativeColumn();
|
cd.RelativeColumn(); cd.RelativeColumn(); cd.RelativeColumn();
|
||||||
cd.RelativeColumn(); cd.RelativeColumn(); cd.RelativeColumn();
|
|
||||||
cd.ConstantColumn(60);
|
cd.ConstantColumn(60);
|
||||||
});
|
});
|
||||||
HeaderRow(t, "通道", "Vin(V)", "Iin(A)", "Pin(W)", "Vout(V)", "Iout(A)", "Pout(W)", "压降(V)", "损耗(W)", "效率(%)", "状态");
|
HeaderRow(t, "通道", "Vin(V)", "Iin(A)", "Vout(V)", "Iout(A)", "损耗(W)", "效率(%)", "状态");
|
||||||
foreach (var r in p.PowerResults)
|
foreach (var r in p.PowerResults)
|
||||||
{
|
{
|
||||||
t.Cell().Padding(3).Text($"CH{r.Channel}");
|
t.Cell().Padding(3).Text($"CH{r.Channel}");
|
||||||
t.Cell().Padding(3).Text(r.Vin.ToString("F3"));
|
t.Cell().Padding(3).Text(r.Vin.ToString("F3"));
|
||||||
t.Cell().Padding(3).Text(r.Iin.ToString("F3"));
|
t.Cell().Padding(3).Text(r.Iin.ToString("F3"));
|
||||||
t.Cell().Padding(3).Text(r.PowerIn.ToString("F3"));
|
|
||||||
t.Cell().Padding(3).Text(r.Vout.ToString("F3"));
|
t.Cell().Padding(3).Text(r.Vout.ToString("F3"));
|
||||||
t.Cell().Padding(3).Text(r.Iout.ToString("F3"));
|
t.Cell().Padding(3).Text(r.Iout.ToString("F3"));
|
||||||
t.Cell().Padding(3).Text(r.PowerOut.ToString("F3"));
|
|
||||||
t.Cell().Padding(3).Text(r.VoltageDrop.ToString("F3"));
|
|
||||||
t.Cell().Padding(3).Text(r.PowerLossW.ToString("F3"));
|
t.Cell().Padding(3).Text(r.PowerLossW.ToString("F3"));
|
||||||
t.Cell().Padding(3).Text(r.EfficiencyPct.ToString("F1"));
|
t.Cell().Padding(3).Text(r.EfficiencyPct.ToString("F3"));
|
||||||
t.Cell().Padding(3).Text(StatusText(r.Status)).FontColor(StatusColor(r.Status));
|
t.Cell().Padding(3).Text(StatusText(r.Status)).FontColor(StatusColor(r.Status));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -158,13 +158,12 @@ public sealed class WordReportGenerator
|
|||||||
Section(body, section++, "功率损耗", "POWER LOSS");
|
Section(body, section++, "功率损耗", "POWER LOSS");
|
||||||
if (p.PowerResults.Count > 0)
|
if (p.PowerResults.Count > 0)
|
||||||
body.Append(ResultTable(
|
body.Append(ResultTable(
|
||||||
new[] { "通道", "输入电压 V", "输入电流 A", "输入功率 W", "输出电压 V", "输出电流 A", "输出功率 W", "电压降 V", "功率损耗 W", "效率 %", "状态" },
|
new[] { "通道", "输入电压 V", "输入电流 A", "输出电压 V", "输出电流 A", "功率损耗 W", "效率 %", "状态" },
|
||||||
p.PowerResults.Select(r => new[]
|
p.PowerResults.Select(r => new[]
|
||||||
{
|
{
|
||||||
$"CH{r.Channel:00}", Format(r.Vin, "F3"), Format(r.Iin, "F3"),
|
$"CH{r.Channel:00}", Format(r.Vin, "F3"), Format(r.Iin, "F3"),
|
||||||
Format(r.PowerIn, "F3"), Format(r.Vout, "F3"), Format(r.Iout, "F3"),
|
Format(r.Vout, "F3"), Format(r.Iout, "F3"),
|
||||||
Format(r.PowerOut, "F3"), Format(r.VoltageDrop, "F3"), Format(r.PowerLossW, "F3"),
|
Format(r.PowerLossW, "F3"), Format(r.EfficiencyPct, "F3"), StatusText(r.Status)
|
||||||
Format(r.EfficiencyPct, "F1"), StatusText(r.Status)
|
|
||||||
})));
|
})));
|
||||||
ManualNote(body, p.TestFunction?.Power);
|
ManualNote(body, p.TestFunction?.Power);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,9 +7,8 @@ namespace SSPCTester.Logic.Testing;
|
|||||||
public static class BasicTestCriteria
|
public static class BasicTestCriteria
|
||||||
{
|
{
|
||||||
public const double OnVoltageThreshold = 1.0;
|
public const double OnVoltageThreshold = 1.0;
|
||||||
public const double OffVoltageThreshold = 0.3;
|
public const double OffVoltageThreshold = 0.5;
|
||||||
public const double CurrentPresentThreshold = 0.001;
|
public const double CurrentPresentThreshold = 0.001;
|
||||||
public const double OffCurrentThreshold = 0.3;
|
|
||||||
public const int SwitchSettleDelayMs = 1_000;
|
public const int SwitchSettleDelayMs = 1_000;
|
||||||
public const int OffSettleTimeoutMs = 2_000;
|
public const int OffSettleTimeoutMs = 2_000;
|
||||||
public const int OffInitialDelayMs = 100;
|
public const int OffInitialDelayMs = 100;
|
||||||
@ -28,7 +27,7 @@ public static class BasicTestCriteria
|
|||||||
HasVoltageData(voltage) && HasCurrentData(current);
|
HasVoltageData(voltage) && HasCurrentData(current);
|
||||||
|
|
||||||
public static bool IsOffState(double voltage, double current) =>
|
public static bool IsOffState(double voltage, double current) =>
|
||||||
IsOff(voltage) && Math.Abs(current) < OffCurrentThreshold;
|
IsOff(voltage) && !HasCurrentData(current);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Waits until the channel is stably off: no voltage and no current for consecutive samples.
|
/// Waits until the channel is stably off: no voltage and no current for consecutive samples.
|
||||||
|
|||||||
@ -15,10 +15,10 @@ public sealed class BasicTestCriteriaTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(0.29, true)]
|
[InlineData(0.49, true)]
|
||||||
[InlineData(-0.29, true)]
|
[InlineData(-0.49, true)]
|
||||||
[InlineData(0.30, false)]
|
[InlineData(0.50, false)]
|
||||||
[InlineData(-0.30, false)]
|
[InlineData(-0.50, false)]
|
||||||
public void IsOff_UsesAbsoluteVoltageThreshold(double voltage, bool expected)
|
public void IsOff_UsesAbsoluteVoltageThreshold(double voltage, bool expected)
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, BasicTestCriteria.IsOff(voltage));
|
Assert.Equal(expected, BasicTestCriteria.IsOff(voltage));
|
||||||
@ -35,12 +35,12 @@ public sealed class BasicTestCriteriaTests
|
|||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(0.0, 0.0, true)]
|
[InlineData(0.0, 0.0, true)]
|
||||||
[InlineData(0.29, 0.0, true)]
|
[InlineData(0.49, 0.0, true)]
|
||||||
[InlineData(-0.29, 0.0, true)]
|
[InlineData(-0.49, 0.0, true)]
|
||||||
[InlineData(0.0, 0.29, true)]
|
[InlineData(0.0, 0.0005, true)]
|
||||||
[InlineData(0.30, 0.0, false)]
|
[InlineData(0.50, 0.0, false)]
|
||||||
[InlineData(0.75, 0.0, false)]
|
[InlineData(0.75, 0.0, false)]
|
||||||
[InlineData(0.0, 0.30, false)]
|
[InlineData(0.0, 0.01, false)]
|
||||||
[InlineData(28.0, 0.0, false)]
|
[InlineData(28.0, 0.0, false)]
|
||||||
public void IsOffState_RequiresNoVoltageAndNoCurrent(double voltage, double current, bool expected)
|
public void IsOffState_RequiresNoVoltageAndNoCurrent(double voltage, double current, bool expected)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -56,51 +56,6 @@ public sealed class DaqIntegrationTests
|
|||||||
Assert.False(await sspc.GetChannelStateAsync(3));
|
Assert.False(await sspc.GetChannelStateAsync(3));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task MockDaq_AppliesScaleAndOffset()
|
|
||||||
{
|
|
||||||
var options = new DeviceOptions();
|
|
||||||
options.Daq.ClockDivider = 100_000;
|
|
||||||
options.Daq.ChannelCount = 2;
|
|
||||||
options.Daq.Channels[0].Role = nameof(DaqChannelRole.OutputVoltage);
|
|
||||||
options.Daq.Channels[0].Name = "Vout";
|
|
||||||
options.Daq.Channels[0].Scale = 2;
|
|
||||||
options.Daq.Channels[0].Offset = 1;
|
|
||||||
options.Daq.Channels[0].IsCalibrated = true;
|
|
||||||
options.Daq.Channels[1].Role = nameof(DaqChannelRole.OutputCurrent);
|
|
||||||
options.Daq.Channels[1].Name = "Iout";
|
|
||||||
options.Daq.Channels[1].Scale = 3;
|
|
||||||
options.Daq.Channels[1].Offset = -1;
|
|
||||||
options.Daq.Channels[1].IsCalibrated = true;
|
|
||||||
var daq = new MockDaq(Options.Create(options), NullLogger<MockDaq>.Instance)
|
|
||||||
{
|
|
||||||
NominalVoltage = 10,
|
|
||||||
NominalCurrent = 1
|
|
||||||
};
|
|
||||||
|
|
||||||
var curve = await daq.CaptureAroundActionAsync(
|
|
||||||
new DaqActionCaptureRequest(10, 40, true),
|
|
||||||
_ => Task.CompletedTask);
|
|
||||||
var instant = await daq.ReadInstantAsync(new[] { 0, 1 });
|
|
||||||
|
|
||||||
Assert.True(curve.Voltage.Max() > 20);
|
|
||||||
Assert.True(curve.Current!.Max() > 1.8);
|
|
||||||
Assert.Contains(curve.EngineeringChannels, x =>
|
|
||||||
x.PhysicalChannel == 0 &&
|
|
||||||
x.Role == DaqChannelRole.OutputVoltage &&
|
|
||||||
x.Name == "Vout" &&
|
|
||||||
x.Scale == 2 &&
|
|
||||||
x.Offset == 1);
|
|
||||||
Assert.Contains(curve.EngineeringChannels, x =>
|
|
||||||
x.PhysicalChannel == 1 &&
|
|
||||||
x.Role == DaqChannelRole.OutputCurrent &&
|
|
||||||
x.Name == "Iout" &&
|
|
||||||
x.Scale == 3 &&
|
|
||||||
x.Offset == -1);
|
|
||||||
Assert.InRange(instant[0], 20.9, 21.1);
|
|
||||||
Assert.InRange(instant[1], 1.9, 2.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(1000, 100_000)]
|
[InlineData(1000, 100_000)]
|
||||||
[InlineData(100, 1_000_000)]
|
[InlineData(100, 1_000_000)]
|
||||||
|
|||||||
@ -138,13 +138,11 @@ public sealed class InterfacePowerTests
|
|||||||
public Task DisconnectAsync() => Task.CompletedTask;
|
public Task DisconnectAsync() => Task.CompletedTask;
|
||||||
public Task SetModeAsync(LoadMode mode, CancellationToken ct = default) => throw new InvalidOperationException();
|
public Task SetModeAsync(LoadMode mode, CancellationToken ct = default) => throw new InvalidOperationException();
|
||||||
public Task SetValueAsync(double value, CancellationToken ct = default) => throw new InvalidOperationException();
|
public Task SetValueAsync(double value, CancellationToken ct = default) => throw new InvalidOperationException();
|
||||||
public Task ResetAsync(CancellationToken ct = default) => throw new InvalidOperationException();
|
|
||||||
public Task InputAsync(bool on, CancellationToken ct = default)
|
public Task InputAsync(bool on, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
if (on) InputOnCount++;
|
if (on) InputOnCount++;
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
public Task<bool> GetInputStateAsync(CancellationToken ct = default) => Task.FromResult(true);
|
|
||||||
public Task<(double volts, double amps)> ReadAsync(CancellationToken ct = default) => Task.FromResult((28.0, 1.0));
|
public Task<(double volts, double amps)> ReadAsync(CancellationToken ct = default) => Task.FromResult((28.0, 1.0));
|
||||||
public Task<PowerReading> ReadPowerAsync(CancellationToken ct = default)
|
public Task<PowerReading> ReadPowerAsync(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
@ -152,7 +150,6 @@ public sealed class InterfacePowerTests
|
|||||||
return Task.FromResult(new PowerReading(28.0, 1.0, 27.5));
|
return Task.FromResult(new PowerReading(28.0, 1.0, 27.5));
|
||||||
}
|
}
|
||||||
public Task<string> QueryIdentityAsync(CancellationToken ct = default) => Task.FromResult("it8702p");
|
public Task<string> QueryIdentityAsync(CancellationToken ct = default) => Task.FromResult("it8702p");
|
||||||
public Task<string> QueryErrorAsync(CancellationToken ct = default) => Task.FromResult("0,\"No error\"");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class FixedPowerSupply : IPowerSupply
|
private sealed class FixedPowerSupply : IPowerSupply
|
||||||
@ -170,10 +167,8 @@ public sealed class InterfacePowerTests
|
|||||||
if (on) OutputOnCount++;
|
if (on) OutputOnCount++;
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
public Task<bool> GetOutputStateAsync(CancellationToken ct = default) => Task.FromResult(true);
|
|
||||||
public Task<(double volts, double amps)> ReadAsync(CancellationToken ct = default) => Task.FromResult((30.0, 1.0));
|
public Task<(double volts, double amps)> ReadAsync(CancellationToken ct = default) => Task.FromResult((30.0, 1.0));
|
||||||
public Task<string> QueryIdentityAsync(CancellationToken ct = default) => Task.FromResult("fixed");
|
public Task<string> QueryIdentityAsync(CancellationToken ct = default) => Task.FromResult("fixed");
|
||||||
public Task<string> QueryErrorAsync(CancellationToken ct = default) => Task.FromResult("0,\"No error\"");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class ForbiddenPowerSupply : IPowerSupply
|
private sealed class ForbiddenPowerSupply : IPowerSupply
|
||||||
@ -186,10 +181,8 @@ public sealed class InterfacePowerTests
|
|||||||
public Task SetVoltageAsync(double volts, CancellationToken ct = default) => throw new InvalidOperationException();
|
public Task SetVoltageAsync(double volts, CancellationToken ct = default) => throw new InvalidOperationException();
|
||||||
public Task SetCurrentLimitAsync(double amps, CancellationToken ct = default) => throw new InvalidOperationException();
|
public Task SetCurrentLimitAsync(double amps, CancellationToken ct = default) => throw new InvalidOperationException();
|
||||||
public Task OutputAsync(bool on, CancellationToken ct = default) => throw new InvalidOperationException();
|
public Task OutputAsync(bool on, CancellationToken ct = default) => throw new InvalidOperationException();
|
||||||
public Task<bool> GetOutputStateAsync(CancellationToken ct = default) => throw new InvalidOperationException();
|
|
||||||
public Task<(double volts, double amps)> ReadAsync(CancellationToken ct = default) => throw new InvalidOperationException();
|
public Task<(double volts, double amps)> ReadAsync(CancellationToken ct = default) => throw new InvalidOperationException();
|
||||||
public Task<string> QueryIdentityAsync(CancellationToken ct = default) => throw new InvalidOperationException();
|
public Task<string> QueryIdentityAsync(CancellationToken ct = default) => throw new InvalidOperationException();
|
||||||
public Task<string> QueryErrorAsync(CancellationToken ct = default) => throw new InvalidOperationException();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class ForbiddenLoad : ILoad
|
private sealed class ForbiddenLoad : ILoad
|
||||||
@ -201,13 +194,10 @@ public sealed class InterfacePowerTests
|
|||||||
public Task DisconnectAsync() => Task.CompletedTask;
|
public Task DisconnectAsync() => Task.CompletedTask;
|
||||||
public Task SetModeAsync(LoadMode mode, CancellationToken ct = default) => throw new InvalidOperationException();
|
public Task SetModeAsync(LoadMode mode, CancellationToken ct = default) => throw new InvalidOperationException();
|
||||||
public Task SetValueAsync(double value, CancellationToken ct = default) => throw new InvalidOperationException();
|
public Task SetValueAsync(double value, CancellationToken ct = default) => throw new InvalidOperationException();
|
||||||
public Task ResetAsync(CancellationToken ct = default) => throw new InvalidOperationException();
|
|
||||||
public Task InputAsync(bool on, CancellationToken ct = default) => throw new InvalidOperationException();
|
public Task InputAsync(bool on, CancellationToken ct = default) => throw new InvalidOperationException();
|
||||||
public Task<bool> GetInputStateAsync(CancellationToken ct = default) => throw new InvalidOperationException();
|
|
||||||
public Task<(double volts, double amps)> ReadAsync(CancellationToken ct = default) => throw new InvalidOperationException();
|
public Task<(double volts, double amps)> ReadAsync(CancellationToken ct = default) => throw new InvalidOperationException();
|
||||||
public Task<PowerReading> ReadPowerAsync(CancellationToken ct = default) => throw new InvalidOperationException();
|
public Task<PowerReading> ReadPowerAsync(CancellationToken ct = default) => throw new InvalidOperationException();
|
||||||
public Task<string> QueryIdentityAsync(CancellationToken ct = default) => throw new InvalidOperationException();
|
public Task<string> QueryIdentityAsync(CancellationToken ct = default) => throw new InvalidOperationException();
|
||||||
public Task<string> QueryErrorAsync(CancellationToken ct = default) => throw new InvalidOperationException();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#pragma warning restore CS0067
|
#pragma warning restore CS0067
|
||||||
|
|||||||
@ -5,7 +5,6 @@ using Microsoft.Extensions.Logging.Abstractions;
|
|||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using SSPCTester.Devices.Config;
|
using SSPCTester.Devices.Config;
|
||||||
using SSPCTester.Devices.Drivers.Real;
|
using SSPCTester.Devices.Drivers.Real;
|
||||||
using SSPCTester.Devices.Interfaces;
|
|
||||||
|
|
||||||
namespace SSPCTester.Tests;
|
namespace SSPCTester.Tests;
|
||||||
|
|
||||||
@ -14,11 +13,9 @@ public sealed class It87xxLoadTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task CombinedQuery_ReadsVoltageCurrentAndPower()
|
public async Task CombinedQuery_ReadsVoltageCurrentAndPower()
|
||||||
{
|
{
|
||||||
var sync = new object();
|
|
||||||
var commands = new List<string>();
|
var commands = new List<string>();
|
||||||
await using var server = await FakeScpiServer.StartAsync(command =>
|
await using var server = await FakeScpiServer.StartAsync(command =>
|
||||||
{
|
{
|
||||||
lock (sync)
|
|
||||||
commands.Add(command);
|
commands.Add(command);
|
||||||
return command switch
|
return command switch
|
||||||
{
|
{
|
||||||
@ -68,71 +65,6 @@ public sealed class It87xxLoadTests
|
|||||||
Assert.Equal(3.0, reading.Watts, 6);
|
Assert.Equal(3.0, reading.Watts, 6);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task ControlCommands_SetCurrentAndReset()
|
|
||||||
{
|
|
||||||
var sync = new object();
|
|
||||||
var commands = new List<string>();
|
|
||||||
await using var server = await FakeScpiServer.StartAsync(command =>
|
|
||||||
{
|
|
||||||
lock (sync)
|
|
||||||
commands.Add(command);
|
|
||||||
return command switch
|
|
||||||
{
|
|
||||||
"*IDN?" => "ITECH,IT8702P,SN,1.0",
|
|
||||||
"SYST:REM" => FakeScpiServer.NoResponse,
|
|
||||||
"CHAN 1" => FakeScpiServer.NoResponse,
|
|
||||||
"CURR 1.25" => FakeScpiServer.NoResponse,
|
|
||||||
"INP?" => "0",
|
|
||||||
"*RST" => FakeScpiServer.NoResponse,
|
|
||||||
"SYST:LOC" => FakeScpiServer.NoResponse,
|
|
||||||
_ => "0"
|
|
||||||
};
|
|
||||||
});
|
|
||||||
var driver = CreateDriver(server.Port, LoadQueryMode.Combined);
|
|
||||||
|
|
||||||
Assert.True(await driver.ConnectAsync());
|
|
||||||
await driver.SetModeAsync(LoadMode.CC);
|
|
||||||
await driver.SetValueAsync(1.25);
|
|
||||||
Assert.False(await driver.GetInputStateAsync());
|
|
||||||
await driver.ResetAsync();
|
|
||||||
await driver.DisconnectAsync();
|
|
||||||
await WaitForCommandAsync("SYST:LOC");
|
|
||||||
|
|
||||||
string[] sentCommands;
|
|
||||||
lock (sync)
|
|
||||||
sentCommands = commands.ToArray();
|
|
||||||
|
|
||||||
Assert.Contains("CURR 1.25", sentCommands);
|
|
||||||
Assert.Contains("INP?", sentCommands);
|
|
||||||
Assert.Contains("*RST", sentCommands);
|
|
||||||
Assert.Equal(2, sentCommands.Count(x => x == "SYST:REM"));
|
|
||||||
Assert.Equal(2, sentCommands.Count(x => x == "CHAN 1"));
|
|
||||||
Assert.DoesNotContain("MODE CC", sentCommands);
|
|
||||||
|
|
||||||
async Task WaitForCommandAsync(string expected)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < 20; i++)
|
|
||||||
{
|
|
||||||
lock (sync)
|
|
||||||
{
|
|
||||||
if (commands.Contains(expected))
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.Delay(10);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData("ON", true)]
|
|
||||||
[InlineData("1", true)]
|
|
||||||
[InlineData("OFF", false)]
|
|
||||||
[InlineData("0", false)]
|
|
||||||
public void ParseSwitchState_AcceptsScpiValues(string response, bool expected) =>
|
|
||||||
Assert.Equal(expected, It87xxLoad.ParseSwitchState(response));
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task QueryFailure_MarksDisconnected()
|
public async Task QueryFailure_MarksDisconnected()
|
||||||
{
|
{
|
||||||
@ -209,26 +141,14 @@ public sealed class It87xxLoadTests
|
|||||||
using var client = await _listener.AcceptTcpClientAsync(_cts.Token);
|
using var client = await _listener.AcceptTcpClientAsync(_cts.Token);
|
||||||
await using var stream = client.GetStream();
|
await using var stream = client.GetStream();
|
||||||
var buffer = new byte[1024];
|
var buffer = new byte[1024];
|
||||||
var pending = "";
|
|
||||||
while (!_cts.IsCancellationRequested)
|
while (!_cts.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
int read = await stream.ReadAsync(buffer, _cts.Token);
|
int read = await stream.ReadAsync(buffer, _cts.Token);
|
||||||
if (read <= 0) break;
|
if (read <= 0) break;
|
||||||
|
|
||||||
pending += Encoding.ASCII.GetString(buffer, 0, read);
|
string command = Encoding.ASCII.GetString(buffer, 0, read).Trim();
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
int newline = pending.IndexOf('\n');
|
|
||||||
if (newline < 0)
|
|
||||||
break;
|
|
||||||
|
|
||||||
string command = pending[..newline].Trim();
|
|
||||||
pending = pending[(newline + 1)..];
|
|
||||||
if (string.IsNullOrEmpty(command))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
string response = _handler(command);
|
string response = _handler(command);
|
||||||
if (response == CloseConnection) return;
|
if (response == CloseConnection) break;
|
||||||
if (response == NoResponse) continue;
|
if (response == NoResponse) continue;
|
||||||
|
|
||||||
byte[] payload = Encoding.ASCII.GetBytes(response + "\n");
|
byte[] payload = Encoding.ASCII.GetBytes(response + "\n");
|
||||||
@ -239,4 +159,3 @@ public sealed class It87xxLoadTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@ -46,64 +46,6 @@ public sealed class Udp5080Tests
|
|||||||
Assert.True(driver.IsConnected);
|
Assert.True(driver.IsConnected);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task TcpDriver_SendsVoltageAndCurrentLimitCommands()
|
|
||||||
{
|
|
||||||
var sync = new object();
|
|
||||||
var commands = new List<string>();
|
|
||||||
await using var server = await FakeScpiServer.StartAsync(command =>
|
|
||||||
{
|
|
||||||
lock (sync)
|
|
||||||
commands.Add(command);
|
|
||||||
return command switch
|
|
||||||
{
|
|
||||||
"*IDN?" => "UNI-T,UDP5080-100,AWPK225300032,1.03.0513",
|
|
||||||
"VOLT 12.345" => FakeScpiServer.NoResponse,
|
|
||||||
"CURR 1.25" => FakeScpiServer.NoResponse,
|
|
||||||
"OUTP?" => "1",
|
|
||||||
_ => "0"
|
|
||||||
};
|
|
||||||
});
|
|
||||||
var driver = CreateDriver(server.Port);
|
|
||||||
|
|
||||||
Assert.True(await driver.ConnectAsync());
|
|
||||||
await driver.SetVoltageAsync(12.345);
|
|
||||||
await driver.SetCurrentLimitAsync(1.25);
|
|
||||||
Assert.True(await driver.GetOutputStateAsync());
|
|
||||||
await WaitForCommandAsync("CURR 1.25");
|
|
||||||
|
|
||||||
string[] sentCommands;
|
|
||||||
lock (sync)
|
|
||||||
sentCommands = commands.ToArray();
|
|
||||||
|
|
||||||
Assert.Contains("VOLT 12.345", sentCommands);
|
|
||||||
Assert.Contains("CURR 1.25", sentCommands);
|
|
||||||
Assert.Contains("OUTP?", sentCommands);
|
|
||||||
|
|
||||||
async Task WaitForCommandAsync(string expected)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < 20; i++)
|
|
||||||
{
|
|
||||||
lock (sync)
|
|
||||||
{
|
|
||||||
if (commands.Contains(expected))
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.Delay(10);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData("ON", true)]
|
|
||||||
[InlineData("1", true)]
|
|
||||||
[InlineData("OFF", false)]
|
|
||||||
[InlineData("0", false)]
|
|
||||||
public void ParseSwitchState_AcceptsScpiValues(string response, bool expected) =>
|
|
||||||
Assert.Equal(expected, Udp5080.ParseSwitchState(response));
|
|
||||||
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task TcpDriver_MarksDisconnectedWhenRemoteCloses()
|
public async Task TcpDriver_MarksDisconnectedWhenRemoteCloses()
|
||||||
{
|
{
|
||||||
@ -150,7 +92,6 @@ public sealed class Udp5080Tests
|
|||||||
|
|
||||||
private sealed class FakeScpiServer : IAsyncDisposable
|
private sealed class FakeScpiServer : IAsyncDisposable
|
||||||
{
|
{
|
||||||
public const string NoResponse = "__NO_RESPONSE__";
|
|
||||||
private readonly TcpListener _listener;
|
private readonly TcpListener _listener;
|
||||||
private readonly Func<string, string?> _handler;
|
private readonly Func<string, string?> _handler;
|
||||||
private readonly CancellationTokenSource _cts = new();
|
private readonly CancellationTokenSource _cts = new();
|
||||||
@ -188,27 +129,14 @@ public sealed class Udp5080Tests
|
|||||||
using var client = await _listener.AcceptTcpClientAsync(_cts.Token);
|
using var client = await _listener.AcceptTcpClientAsync(_cts.Token);
|
||||||
await using var stream = client.GetStream();
|
await using var stream = client.GetStream();
|
||||||
var buffer = new byte[1024];
|
var buffer = new byte[1024];
|
||||||
var pending = "";
|
|
||||||
while (!_cts.IsCancellationRequested)
|
while (!_cts.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
int read = await stream.ReadAsync(buffer, _cts.Token);
|
int read = await stream.ReadAsync(buffer, _cts.Token);
|
||||||
if (read <= 0) break;
|
if (read <= 0) break;
|
||||||
|
|
||||||
pending += Encoding.ASCII.GetString(buffer, 0, read);
|
string command = Encoding.ASCII.GetString(buffer, 0, read).Trim();
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
int newline = pending.IndexOf('\n');
|
|
||||||
if (newline < 0)
|
|
||||||
break;
|
|
||||||
|
|
||||||
string command = pending[..newline].Trim();
|
|
||||||
pending = pending[(newline + 1)..];
|
|
||||||
if (string.IsNullOrEmpty(command))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
string? response = _handler(command);
|
string? response = _handler(command);
|
||||||
if (response is null) return;
|
if (response is null) break;
|
||||||
if (response == NoResponse) continue;
|
|
||||||
|
|
||||||
byte[] payload = Encoding.ASCII.GetBytes(response + "\n");
|
byte[] payload = Encoding.ASCII.GetBytes(response + "\n");
|
||||||
await stream.WriteAsync(payload, _cts.Token);
|
await stream.WriteAsync(payload, _cts.Token);
|
||||||
@ -218,4 +146,3 @@ public sealed class Udp5080Tests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@ -130,16 +130,9 @@ public partial class App : Application
|
|||||||
}
|
}
|
||||||
var storageOpts = cfg.GetSection(StorageOptions.SectionName).Get<StorageOptions>() ?? new StorageOptions();
|
var storageOpts = cfg.GetSection(StorageOptions.SectionName).Get<StorageOptions>() ?? new StorageOptions();
|
||||||
var reportOpts = cfg.GetSection(ReportOptions.SectionName).Get<ReportOptions>() ?? new ReportOptions();
|
var reportOpts = cfg.GetSection(ReportOptions.SectionName).Get<ReportOptions>() ?? new ReportOptions();
|
||||||
// Preset collections must be loaded as complete arrays. ConfigurationBinder
|
|
||||||
// merges array indexes, which brings deleted default presets back on restart.
|
|
||||||
var consoleOpts = UserSettingsStore.LoadConsoleOptions()
|
|
||||||
?? cfg.GetSection(ConsoleOptions.SectionName).Get<ConsoleOptions>()
|
|
||||||
?? new ConsoleOptions();
|
|
||||||
EnsureConsoleOptions(consoleOpts);
|
|
||||||
services.AddSingleton<IOptions<DeviceOptions>>(Options.Create(devOpts));
|
services.AddSingleton<IOptions<DeviceOptions>>(Options.Create(devOpts));
|
||||||
services.AddSingleton<IOptions<StorageOptions>>(Options.Create(storageOpts));
|
services.AddSingleton<IOptions<StorageOptions>>(Options.Create(storageOpts));
|
||||||
services.AddSingleton<IOptions<ReportOptions>>(Options.Create(reportOpts));
|
services.AddSingleton<IOptions<ReportOptions>>(Options.Create(reportOpts));
|
||||||
services.AddSingleton<IOptions<ConsoleOptions>>(Options.Create(consoleOpts));
|
|
||||||
|
|
||||||
services.AddSingleton<MockSspc>();
|
services.AddSingleton<MockSspc>();
|
||||||
services.AddSingleton<SspcModbus>();
|
services.AddSingleton<SspcModbus>();
|
||||||
@ -180,10 +173,6 @@ public partial class App : Application
|
|||||||
services.AddTransient<HomeViewModel>();
|
services.AddTransient<HomeViewModel>();
|
||||||
services.AddTransient<ProjectInfoViewModel>();
|
services.AddTransient<ProjectInfoViewModel>();
|
||||||
services.AddTransient<TestHostViewModel>();
|
services.AddTransient<TestHostViewModel>();
|
||||||
// The experiment console controls application-wide hardware and is not
|
|
||||||
// owned by a project. Keep one instance so switching project pages does
|
|
||||||
// not replace its state or interrupt its refresh loop.
|
|
||||||
services.AddSingleton<ExperimentConsoleViewModel>();
|
|
||||||
services.AddTransient<BasicTestViewModel>();
|
services.AddTransient<BasicTestViewModel>();
|
||||||
services.AddTransient<CurveTestViewModel>();
|
services.AddTransient<CurveTestViewModel>();
|
||||||
services.AddTransient<PowerTestViewModel>();
|
services.AddTransient<PowerTestViewModel>();
|
||||||
@ -244,9 +233,6 @@ public partial class App : Application
|
|||||||
["Devices:PowerSupply:IdnCommand"] = "*IDN?",
|
["Devices:PowerSupply:IdnCommand"] = "*IDN?",
|
||||||
["Devices:PowerSupply:VoltageCommand"] = "MEASure:VOLTage?",
|
["Devices:PowerSupply:VoltageCommand"] = "MEASure:VOLTage?",
|
||||||
["Devices:PowerSupply:CurrentCommand"] = "MEASure:CURRent?",
|
["Devices:PowerSupply:CurrentCommand"] = "MEASure:CURRent?",
|
||||||
["Devices:PowerSupply:SetVoltageCommand"] = "VOLT {0}",
|
|
||||||
["Devices:PowerSupply:SetCurrentLimitCommand"] = "CURR {0}",
|
|
||||||
["Devices:PowerSupply:ErrorCommand"] = "SYST:ERR?",
|
|
||||||
["Devices:Load:PortName"] = "COM3",
|
["Devices:Load:PortName"] = "COM3",
|
||||||
["Devices:Load:UseMock"] = "false",
|
["Devices:Load:UseMock"] = "false",
|
||||||
["Devices:Load:AutoConnect"] = "true",
|
["Devices:Load:AutoConnect"] = "true",
|
||||||
@ -258,10 +244,6 @@ public partial class App : Application
|
|||||||
["Devices:Load:Channel"] = "1",
|
["Devices:Load:Channel"] = "1",
|
||||||
["Devices:Load:UseFetch"] = "true",
|
["Devices:Load:UseFetch"] = "true",
|
||||||
["Devices:Load:QueryMode"] = "Combined",
|
["Devices:Load:QueryMode"] = "Combined",
|
||||||
["Devices:Load:SetModeCommand"] = "",
|
|
||||||
["Devices:Load:SetValueCommand"] = "{0} {1}",
|
|
||||||
["Devices:Load:ResetCommand"] = "*RST",
|
|
||||||
["Devices:Load:ErrorCommand"] = "SYST:ERR?",
|
|
||||||
["Devices:Daq:LogicalDeviceId"] = "0",
|
["Devices:Daq:LogicalDeviceId"] = "0",
|
||||||
["Devices:Daq:UseMock"] = "false",
|
["Devices:Daq:UseMock"] = "false",
|
||||||
["Devices:Daq:AutoConnect"] = "true",
|
["Devices:Daq:AutoConnect"] = "true",
|
||||||
@ -320,14 +302,6 @@ public partial class App : Application
|
|||||||
options.Channels = DaqChannelOptionsNormalizer.Normalize(options.Channels);
|
options.Channels = DaqChannelOptionsNormalizer.Normalize(options.Channels);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void EnsureConsoleOptions(ConsoleOptions options)
|
|
||||||
{
|
|
||||||
if (options.PowerPresets == null)
|
|
||||||
options.PowerPresets = ConsoleOptions.DefaultPowerPresets();
|
|
||||||
if (options.LoadPresets == null)
|
|
||||||
options.LoadPresets = ConsoleOptions.DefaultLoadPresets();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
|
private static void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
|
||||||
{
|
{
|
||||||
FatalCrash(e.Exception);
|
FatalCrash(e.Exception);
|
||||||
|
|||||||
@ -18,9 +18,6 @@
|
|||||||
<DataTemplate DataType="{x:Type vm:ReportViewModel}">
|
<DataTemplate DataType="{x:Type vm:ReportViewModel}">
|
||||||
<v:ReportView />
|
<v:ReportView />
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
<DataTemplate DataType="{x:Type vm:ExperimentConsoleViewModel}">
|
|
||||||
<v:ExperimentConsoleView />
|
|
||||||
</DataTemplate>
|
|
||||||
<DataTemplate DataType="{x:Type vm:BasicTestViewModel}">
|
<DataTemplate DataType="{x:Type vm:BasicTestViewModel}">
|
||||||
<v:BasicTestView />
|
<v:BasicTestView />
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
|
|||||||
@ -13,37 +13,3 @@ public sealed class ReportOptions
|
|||||||
public string SavePath { get; set; } = "";
|
public string SavePath { get; set; } = "";
|
||||||
public string Format { get; set; } = "Pdf";
|
public string Format { get; set; } = "Pdf";
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class ConsoleOptions
|
|
||||||
{
|
|
||||||
public const string SectionName = "Console";
|
|
||||||
public List<PowerPreset> PowerPresets { get; set; } = DefaultPowerPresets();
|
|
||||||
public List<LoadPreset> LoadPresets { get; set; } = DefaultLoadPresets();
|
|
||||||
|
|
||||||
public static List<PowerPreset> DefaultPowerPresets() =>
|
|
||||||
[
|
|
||||||
new() { Name = "3V / 1A 基础环境", Voltage = 3.0, CurrentLimit = 1.0 },
|
|
||||||
new() { Name = "5V / 1A 调试环境", Voltage = 5.0, CurrentLimit = 1.0 },
|
|
||||||
new() { Name = "28V / 2A 标准环境", Voltage = 28.0, CurrentLimit = 2.0 }
|
|
||||||
];
|
|
||||||
|
|
||||||
public static List<LoadPreset> DefaultLoadPresets() =>
|
|
||||||
[
|
|
||||||
new() { Name = "1A 基础拉载", Current = 1.0 },
|
|
||||||
new() { Name = "0.5A 低负载", Current = 0.5 },
|
|
||||||
new() { Name = "2A 调试负载", Current = 2.0 }
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class PowerPreset
|
|
||||||
{
|
|
||||||
public string Name { get; set; } = "";
|
|
||||||
public double Voltage { get; set; }
|
|
||||||
public double CurrentLimit { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class LoadPreset
|
|
||||||
{
|
|
||||||
public string Name { get; set; } = "";
|
|
||||||
public double Current { get; set; }
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using SSPCTester.Devices.Config;
|
using SSPCTester.Devices.Config;
|
||||||
@ -17,17 +15,10 @@ public sealed class SafetyMonitorService : IDisposable
|
|||||||
private readonly UiLogSink _log;
|
private readonly UiLogSink _log;
|
||||||
private readonly ILogger<SafetyMonitorService> _logger;
|
private readonly ILogger<SafetyMonitorService> _logger;
|
||||||
private readonly SemaphoreSlim _gate = new(1, 1);
|
private readonly SemaphoreSlim _gate = new(1, 1);
|
||||||
private readonly string _traceLogPath;
|
|
||||||
private readonly object _traceLock = new();
|
|
||||||
private Timer? _timer;
|
private Timer? _timer;
|
||||||
private bool _handlingEmergency;
|
private bool _handlingEmergency;
|
||||||
private bool _softwareEmergencyActive;
|
private bool _softwareEmergencyActive;
|
||||||
private bool _hardwareEmergencyLatched;
|
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
private long _pollSequence;
|
|
||||||
private long _pollGateBusyCount;
|
|
||||||
private string? _lastStatusSnapshot;
|
|
||||||
private string? _lastPollSnapshot;
|
|
||||||
|
|
||||||
public SafetyMonitorService(
|
public SafetyMonitorService(
|
||||||
ISafetyDio safetyDio,
|
ISafetyDio safetyDio,
|
||||||
@ -45,14 +36,6 @@ public sealed class SafetyMonitorService : IDisposable
|
|||||||
_options = options.Value;
|
_options = options.Value;
|
||||||
_log = log;
|
_log = log;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
string logDir = Path.Combine(
|
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
|
||||||
"SSPCTester",
|
|
||||||
"Logs");
|
|
||||||
Directory.CreateDirectory(logDir);
|
|
||||||
_traceLogPath = Path.Combine(logDir, $"safety-trace-{DateTime.Now:yyyyMMdd-HHmmss}.log");
|
|
||||||
Trace($"Safety trace started. UseMock={_options.Pci2312.UseMock}, AutoConnect={_options.Pci2312.AutoConnect}, Enabled={_options.Pci2312.Enabled}, PollIntervalMs={_options.Pci2312.PollIntervalMs}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsEmergencyActive { get; private set; }
|
public bool IsEmergencyActive { get; private set; }
|
||||||
@ -66,23 +49,19 @@ public sealed class SafetyMonitorService : IDisposable
|
|||||||
if (_timer is not null) return;
|
if (_timer is not null) return;
|
||||||
|
|
||||||
var options = _options.Pci2312;
|
var options = _options.Pci2312;
|
||||||
Trace($"Start() called. Enabled={options.Enabled}, AutoConnect={options.AutoConnect}, DeviceId={options.DeviceId}, DI={options.EmergencyInputChannel}, DO={options.AlarmOutputChannel}, PollIntervalMs={options.PollIntervalMs}");
|
|
||||||
if (!options.Enabled)
|
if (!options.Enabled)
|
||||||
{
|
{
|
||||||
SetStatus(false, false, "设备故障", "PCI2312 安全监控未启用");
|
SetStatus(false, false, "设备故障", "PCI2312 安全监控未启用");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int interval = Math.Clamp(options.PollIntervalMs, 50, 10000);
|
int interval = Math.Clamp(options.PollIntervalMs, 50, 10_000);
|
||||||
_timer = new Timer(OnTimer, null, TimeSpan.Zero, TimeSpan.FromMilliseconds(interval));
|
_timer = new Timer(OnTimer, null, TimeSpan.Zero, TimeSpan.FromMilliseconds(interval));
|
||||||
SetStatus(false, false, "设备正常", "正在监控 PCI2312 急停输入");
|
SetStatus(false, false, "设备正常", "正在监控 PCI2312 急停输入");
|
||||||
_log.Add(UiLogLevel.Info, $"安全观测日志已启用:{_traceLogPath}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task TriggerSoftwareEmergencyAsync(CancellationToken ct = default)
|
public async Task TriggerSoftwareEmergencyAsync(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var sw = Stopwatch.StartNew();
|
|
||||||
Trace("TriggerSoftwareEmergencyAsync enter.");
|
|
||||||
await _gate.WaitAsync(ct).ConfigureAwait(false);
|
await _gate.WaitAsync(ct).ConfigureAwait(false);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -97,7 +76,6 @@ public sealed class SafetyMonitorService : IDisposable
|
|||||||
await TryShutdownPowerAsync().ConfigureAwait(false);
|
await TryShutdownPowerAsync().ConfigureAwait(false);
|
||||||
await TryShutdownLoadAsync().ConfigureAwait(false);
|
await TryShutdownLoadAsync().ConfigureAwait(false);
|
||||||
SetStatus(true, true, "软件急停已触发", "报警灯已接通,SSPC 通道已全部关断,电源输出和负载输入已发送关闭指令");
|
SetStatus(true, true, "软件急停已触发", "报警灯已接通,SSPC 通道已全部关断,电源输出和负载输入已发送关闭指令");
|
||||||
Trace($"TriggerSoftwareEmergencyAsync success in {sw.ElapsedMilliseconds}ms.");
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -106,7 +84,6 @@ public sealed class SafetyMonitorService : IDisposable
|
|||||||
_logger.LogError(ex, "Software emergency shutdown failed.");
|
_logger.LogError(ex, "Software emergency shutdown failed.");
|
||||||
_log.Add(UiLogLevel.Error, $"软件急停执行异常:{ex.Message}");
|
_log.Add(UiLogLevel.Error, $"软件急停执行异常:{ex.Message}");
|
||||||
SetStatus(true, IsAlarmOutputOn, "设备故障", ex.Message);
|
SetStatus(true, IsAlarmOutputOn, "设备故障", ex.Message);
|
||||||
Trace($"TriggerSoftwareEmergencyAsync failed in {sw.ElapsedMilliseconds}ms. {ex.GetType().Name}: {ex.Message}");
|
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@ -130,23 +107,19 @@ public sealed class SafetyMonitorService : IDisposable
|
|||||||
|
|
||||||
private async Task<SafetyResetResult> ResetCoreAsync(CancellationToken ct)
|
private async Task<SafetyResetResult> ResetCoreAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
Trace("ResetCoreAsync enter.");
|
|
||||||
var options = _options.Pci2312;
|
var options = _options.Pci2312;
|
||||||
await EnsureSafetyDioConnectedAsync(ct).ConfigureAwait(false);
|
await EnsureSafetyDioConnectedAsync(ct).ConfigureAwait(false);
|
||||||
if (await IsEmergencyInputActiveAsync(options, ct).ConfigureAwait(false))
|
if (await IsEmergencyInputActiveAsync(options, ct).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
const string message = "硬件急停按钮仍处于触发状态,无法执行状态重置。请先旋转或释放设备上的急停按钮,确认急停输入复位后再重试。";
|
const string message = "硬件急停按钮仍处于触发状态,无法执行状态重置。请先旋转或释放设备上的急停按钮,确认急停输入复位后再重试。";
|
||||||
SetStatus(true, IsAlarmOutputOn, "急停已触发", message);
|
SetStatus(true, IsAlarmOutputOn, "急停已触发", message);
|
||||||
Trace("ResetCoreAsync blocked: emergency input still active.");
|
|
||||||
return new SafetyResetResult(false, message);
|
return new SafetyResetResult(false, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
await SetAlarmOutputAsync(false, ct).ConfigureAwait(false);
|
await SetAlarmOutputAsync(false, ct).ConfigureAwait(false);
|
||||||
_softwareEmergencyActive = false;
|
_softwareEmergencyActive = false;
|
||||||
_hardwareEmergencyLatched = false;
|
|
||||||
_handlingEmergency = false;
|
_handlingEmergency = false;
|
||||||
SetStatus(false, false, "设备正常", "急停状态已重置,报警灯已关闭,正在监控 PCI2312 输入");
|
SetStatus(false, false, "设备正常", "急停状态已重置,报警灯已关闭,正在监控 PCI2312 输入");
|
||||||
Trace("ResetCoreAsync success.");
|
|
||||||
return new SafetyResetResult(true, "急停状态已重置。");
|
return new SafetyResetResult(true, "急停状态已重置。");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,53 +131,27 @@ public sealed class SafetyMonitorService : IDisposable
|
|||||||
|
|
||||||
private async Task PollAsync()
|
private async Task PollAsync()
|
||||||
{
|
{
|
||||||
if (!await _gate.WaitAsync(0).ConfigureAwait(false))
|
if (!await _gate.WaitAsync(0).ConfigureAwait(false)) return;
|
||||||
{
|
|
||||||
long skipped = Interlocked.Increment(ref _pollGateBusyCount);
|
|
||||||
if (skipped % 20 == 0)
|
|
||||||
Trace($"PollAsync skipped by gate contention. skipped={skipped}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
long seq = Interlocked.Increment(ref _pollSequence);
|
|
||||||
var options = _options.Pci2312;
|
var options = _options.Pci2312;
|
||||||
if (!options.Enabled) return;
|
if (!options.Enabled) return;
|
||||||
|
|
||||||
if (options.AutoConnect && !_safetyDio.IsConnected)
|
if (options.AutoConnect && !_safetyDio.IsConnected)
|
||||||
{
|
|
||||||
Trace($"Poll#{seq}: attempting auto connect for safety DIO.");
|
|
||||||
await _safetyDio.ConnectAsync().ConfigureAwait(false);
|
await _safetyDio.ConnectAsync().ConfigureAwait(false);
|
||||||
}
|
|
||||||
|
|
||||||
bool active = await IsEmergencyInputActiveAsync(options, CancellationToken.None).ConfigureAwait(false);
|
bool active = await IsEmergencyInputActiveAsync(options, CancellationToken.None).ConfigureAwait(false);
|
||||||
string snapshot = $"poll={seq},active={active},handling={_handlingEmergency},soft={_softwareEmergencyActive},latched={_hardwareEmergencyLatched},emergency={IsEmergencyActive},alarm={IsAlarmOutputOn},dioConnected={_safetyDio.IsConnected}";
|
|
||||||
if (!string.Equals(_lastPollSnapshot, snapshot, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
_lastPollSnapshot = snapshot;
|
|
||||||
Trace($"Poll snapshot: {snapshot}");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (active)
|
if (active)
|
||||||
{
|
{
|
||||||
_hardwareEmergencyLatched = true;
|
|
||||||
Trace($"Poll#{seq}: branch=ACTIVE -> HandleEmergencyAsync");
|
|
||||||
await HandleEmergencyAsync().ConfigureAwait(false);
|
await HandleEmergencyAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else if (_hardwareEmergencyLatched)
|
|
||||||
{
|
|
||||||
Trace($"Poll#{seq}: branch=HardwareLatchedClear -> ClearHardwareEmergencyAsync");
|
|
||||||
await ClearHardwareEmergencyAsync(options).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
else if (_softwareEmergencyActive)
|
else if (_softwareEmergencyActive)
|
||||||
{
|
{
|
||||||
Trace($"Poll#{seq}: branch=SoftwareActiveWaitReset");
|
|
||||||
SetStatus(true, IsAlarmOutputOn, "软件急停已触发", "等待状态重置,电源和负载不会自动恢复");
|
SetStatus(true, IsAlarmOutputOn, "软件急停已触发", "等待状态重置,电源和负载不会自动恢复");
|
||||||
}
|
}
|
||||||
else if (IsEmergencyActive || IsAlarmOutputOn)
|
else if (IsEmergencyActive || IsAlarmOutputOn)
|
||||||
{
|
{
|
||||||
Trace($"Poll#{seq}: branch=CleanupByStatus -> ClearHardwareEmergencyAsync");
|
SetStatus(true, IsAlarmOutputOn, "急停已触发", $"DI{options.EmergencyInputChannel} 已复位,请点击状态重置关闭报警灯");
|
||||||
await ClearHardwareEmergencyAsync(options).ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -215,7 +162,6 @@ public sealed class SafetyMonitorService : IDisposable
|
|||||||
{
|
{
|
||||||
_logger.LogWarning(ex, "Safety monitor poll failed.");
|
_logger.LogWarning(ex, "Safety monitor poll failed.");
|
||||||
SetStatus(IsEmergencyActive, IsAlarmOutputOn, "设备故障", ex.Message);
|
SetStatus(IsEmergencyActive, IsAlarmOutputOn, "设备故障", ex.Message);
|
||||||
Trace($"PollAsync failed. {ex.GetType().Name}: {ex.Message}");
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@ -225,37 +171,18 @@ public sealed class SafetyMonitorService : IDisposable
|
|||||||
|
|
||||||
private async Task HandleEmergencyAsync()
|
private async Task HandleEmergencyAsync()
|
||||||
{
|
{
|
||||||
if (_handlingEmergency)
|
if (_handlingEmergency) return;
|
||||||
{
|
|
||||||
Trace("HandleEmergencyAsync skipped because _handlingEmergency=true.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var sw = Stopwatch.StartNew();
|
|
||||||
_handlingEmergency = true;
|
_handlingEmergency = true;
|
||||||
SetStatus(true, IsAlarmOutputOn, "急停已触发", "正在执行安全关断");
|
SetStatus(true, IsAlarmOutputOn, "急停已触发", "正在执行安全关断");
|
||||||
_log.Add(UiLogLevel.Error, "检测到急停输入接通,开始执行报警灯、电源、负载关断。");
|
_log.Add(UiLogLevel.Error, "检测到急停输入接通,开始执行报警灯、电源、负载关断。");
|
||||||
Trace("HandleEmergencyAsync begin.");
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var step = Stopwatch.StartNew();
|
|
||||||
await SetAlarmOutputAsync(true, CancellationToken.None).ConfigureAwait(false);
|
await SetAlarmOutputAsync(true, CancellationToken.None).ConfigureAwait(false);
|
||||||
Trace($"HandleEmergencyAsync step SetAlarmOutputAsync OK in {step.ElapsedMilliseconds}ms.");
|
|
||||||
|
|
||||||
step.Restart();
|
|
||||||
await TryShutdownSspcAsync().ConfigureAwait(false);
|
await TryShutdownSspcAsync().ConfigureAwait(false);
|
||||||
Trace($"HandleEmergencyAsync step TryShutdownSspcAsync done in {step.ElapsedMilliseconds}ms.");
|
|
||||||
|
|
||||||
step.Restart();
|
|
||||||
await TryShutdownPowerAsync().ConfigureAwait(false);
|
await TryShutdownPowerAsync().ConfigureAwait(false);
|
||||||
Trace($"HandleEmergencyAsync step TryShutdownPowerAsync done in {step.ElapsedMilliseconds}ms.");
|
|
||||||
|
|
||||||
step.Restart();
|
|
||||||
await TryShutdownLoadAsync().ConfigureAwait(false);
|
await TryShutdownLoadAsync().ConfigureAwait(false);
|
||||||
Trace($"HandleEmergencyAsync step TryShutdownLoadAsync done in {step.ElapsedMilliseconds}ms.");
|
|
||||||
SetStatus(true, true, "急停已触发", "报警灯已接通,SSPC 通道已全部关断,电源输出和负载输入已发送关闭指令");
|
SetStatus(true, true, "急停已触发", "报警灯已接通,SSPC 通道已全部关断,电源输出和负载输入已发送关闭指令");
|
||||||
Trace($"HandleEmergencyAsync success in {sw.ElapsedMilliseconds}ms.");
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -263,34 +190,15 @@ public sealed class SafetyMonitorService : IDisposable
|
|||||||
_log.Add(UiLogLevel.Error, $"急停关断执行异常:{ex.Message}");
|
_log.Add(UiLogLevel.Error, $"急停关断执行异常:{ex.Message}");
|
||||||
SetStatus(true, IsAlarmOutputOn, "设备故障", ex.Message);
|
SetStatus(true, IsAlarmOutputOn, "设备故障", ex.Message);
|
||||||
_handlingEmergency = false;
|
_handlingEmergency = false;
|
||||||
Trace($"HandleEmergencyAsync failed in {sw.ElapsedMilliseconds}ms. {ex.GetType().Name}: {ex.Message}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ClearHardwareEmergencyAsync(Pci2312Options options)
|
|
||||||
{
|
|
||||||
var sw = Stopwatch.StartNew();
|
|
||||||
Trace("ClearHardwareEmergencyAsync begin.");
|
|
||||||
if (IsAlarmOutputOn)
|
|
||||||
await SetAlarmOutputAsync(false, CancellationToken.None).ConfigureAwait(false);
|
|
||||||
|
|
||||||
_handlingEmergency = false;
|
|
||||||
_softwareEmergencyActive = false;
|
|
||||||
_hardwareEmergencyLatched = false;
|
|
||||||
SetStatus(false, false, "设备正常", $"DI{options.EmergencyInputChannel} 已复位,硬件急停已解除,报警灯已关闭,设备输出不会自动恢复");
|
|
||||||
_log.Add(UiLogLevel.Info, "硬件急停输入已复位,软件急停状态和报警灯已自动解除。");
|
|
||||||
Trace($"ClearHardwareEmergencyAsync success in {sw.ElapsedMilliseconds}ms.");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task TryShutdownSspcAsync()
|
private async Task TryShutdownSspcAsync()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!_sspc.IsConnected && _options.Sspc.AutoConnect)
|
if (!_sspc.IsConnected && _options.Sspc.AutoConnect)
|
||||||
{
|
|
||||||
Trace("TryShutdownSspcAsync: auto-connecting SSPC.");
|
|
||||||
await _sspc.ConnectAsync().ConfigureAwait(false);
|
await _sspc.ConnectAsync().ConfigureAwait(false);
|
||||||
}
|
|
||||||
if (_sspc.IsConnected)
|
if (_sspc.IsConnected)
|
||||||
{
|
{
|
||||||
for (int ch = 1; ch <= _sspc.ChannelCount; ch++)
|
for (int ch = 1; ch <= _sspc.ChannelCount; ch++)
|
||||||
@ -309,7 +217,6 @@ public sealed class SafetyMonitorService : IDisposable
|
|||||||
{
|
{
|
||||||
_logger.LogWarning(ex, "SSPC 批量关断失败。");
|
_logger.LogWarning(ex, "SSPC 批量关断失败。");
|
||||||
_log.Add(UiLogLevel.Error, $"SSPC 通道批量关断失败:{ex.Message}");
|
_log.Add(UiLogLevel.Error, $"SSPC 通道批量关断失败:{ex.Message}");
|
||||||
Trace($"TryShutdownSspcAsync failed. {ex.GetType().Name}: {ex.Message}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -318,10 +225,7 @@ public sealed class SafetyMonitorService : IDisposable
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!_power.IsConnected && _options.PowerSupply.AutoConnect)
|
if (!_power.IsConnected && _options.PowerSupply.AutoConnect)
|
||||||
{
|
|
||||||
Trace("TryShutdownPowerAsync: auto-connecting power supply.");
|
|
||||||
await _power.ConnectAsync().ConfigureAwait(false);
|
await _power.ConnectAsync().ConfigureAwait(false);
|
||||||
}
|
|
||||||
if (_power.IsConnected)
|
if (_power.IsConnected)
|
||||||
await _power.OutputAsync(false).ConfigureAwait(false);
|
await _power.OutputAsync(false).ConfigureAwait(false);
|
||||||
_log.Add(UiLogLevel.Warning, "UDP5080 电源输出已发送关闭指令。");
|
_log.Add(UiLogLevel.Warning, "UDP5080 电源输出已发送关闭指令。");
|
||||||
@ -330,7 +234,6 @@ public sealed class SafetyMonitorService : IDisposable
|
|||||||
{
|
{
|
||||||
_logger.LogWarning(ex, "Power shutdown failed.");
|
_logger.LogWarning(ex, "Power shutdown failed.");
|
||||||
_log.Add(UiLogLevel.Error, $"UDP5080 电源关闭失败:{ex.Message}");
|
_log.Add(UiLogLevel.Error, $"UDP5080 电源关闭失败:{ex.Message}");
|
||||||
Trace($"TryShutdownPowerAsync failed. {ex.GetType().Name}: {ex.Message}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -339,10 +242,7 @@ public sealed class SafetyMonitorService : IDisposable
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!_load.IsConnected && _options.Load.AutoConnect)
|
if (!_load.IsConnected && _options.Load.AutoConnect)
|
||||||
{
|
|
||||||
Trace("TryShutdownLoadAsync: auto-connecting load.");
|
|
||||||
await _load.ConnectAsync().ConfigureAwait(false);
|
await _load.ConnectAsync().ConfigureAwait(false);
|
||||||
}
|
|
||||||
if (_load.IsConnected)
|
if (_load.IsConnected)
|
||||||
await _load.InputAsync(false).ConfigureAwait(false);
|
await _load.InputAsync(false).ConfigureAwait(false);
|
||||||
_log.Add(UiLogLevel.Warning, "IT8702P 负载输入已发送关闭指令。");
|
_log.Add(UiLogLevel.Warning, "IT8702P 负载输入已发送关闭指令。");
|
||||||
@ -351,7 +251,6 @@ public sealed class SafetyMonitorService : IDisposable
|
|||||||
{
|
{
|
||||||
_logger.LogWarning(ex, "Load shutdown failed.");
|
_logger.LogWarning(ex, "Load shutdown failed.");
|
||||||
_log.Add(UiLogLevel.Error, $"IT8702P 负载关闭失败:{ex.Message}");
|
_log.Add(UiLogLevel.Error, $"IT8702P 负载关闭失败:{ex.Message}");
|
||||||
Trace($"TryShutdownLoadAsync failed. {ex.GetType().Name}: {ex.Message}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -376,21 +275,11 @@ public sealed class SafetyMonitorService : IDisposable
|
|||||||
throw new InvalidOperationException("PCI2312 安全输入输出未启用,无法控制报警灯。");
|
throw new InvalidOperationException("PCI2312 安全输入输出未启用,无法控制报警灯。");
|
||||||
|
|
||||||
if (!_safetyDio.IsConnected)
|
if (!_safetyDio.IsConnected)
|
||||||
{
|
|
||||||
Trace("EnsureSafetyDioConnectedAsync: connecting safety DIO.");
|
|
||||||
await _safetyDio.ConnectAsync(ct).ConfigureAwait(false);
|
await _safetyDio.ConnectAsync(ct).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private void SetStatus(bool emergencyActive, bool alarmOutputOn, string statusText, string detailText)
|
private void SetStatus(bool emergencyActive, bool alarmOutputOn, string statusText, string detailText)
|
||||||
{
|
{
|
||||||
string snapshot = $"emergency={emergencyActive},alarm={alarmOutputOn},status={statusText},detail={detailText}";
|
|
||||||
if (!string.Equals(_lastStatusSnapshot, snapshot, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
_lastStatusSnapshot = snapshot;
|
|
||||||
Trace($"SetStatus changed: {snapshot}");
|
|
||||||
}
|
|
||||||
|
|
||||||
IsEmergencyActive = emergencyActive;
|
IsEmergencyActive = emergencyActive;
|
||||||
IsAlarmOutputOn = alarmOutputOn;
|
IsAlarmOutputOn = alarmOutputOn;
|
||||||
StatusText = statusText;
|
StatusText = statusText;
|
||||||
@ -400,27 +289,10 @@ public sealed class SafetyMonitorService : IDisposable
|
|||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Trace("Dispose() called. Safety monitor disposed.");
|
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
_timer?.Dispose();
|
_timer?.Dispose();
|
||||||
_gate.Dispose();
|
_gate.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Trace(string message)
|
|
||||||
{
|
|
||||||
string line = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} [T{Environment.CurrentManagedThreadId}] {message}{Environment.NewLine}";
|
|
||||||
try
|
|
||||||
{
|
|
||||||
lock (_traceLock)
|
|
||||||
{
|
|
||||||
File.AppendAllText(_traceLogPath, line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// Temporary diagnostic trace should never break runtime behavior.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record SafetyResetResult(bool Success, string Message);
|
public sealed record SafetyResetResult(bool Success, string Message);
|
||||||
|
|||||||
@ -17,64 +17,23 @@ public sealed class UserSettingsStore : IDisposable
|
|||||||
private readonly DeviceOptions _devices;
|
private readonly DeviceOptions _devices;
|
||||||
private readonly StorageOptions _storage;
|
private readonly StorageOptions _storage;
|
||||||
private readonly ReportOptions _report;
|
private readonly ReportOptions _report;
|
||||||
private readonly ConsoleOptions _console;
|
|
||||||
private readonly SemaphoreSlim _writeLock = new(1, 1);
|
private readonly SemaphoreSlim _writeLock = new(1, 1);
|
||||||
|
|
||||||
public UserSettingsStore(
|
public UserSettingsStore(
|
||||||
IOptions<DeviceOptions> devices,
|
IOptions<DeviceOptions> devices,
|
||||||
IOptions<StorageOptions> storage,
|
IOptions<StorageOptions> storage,
|
||||||
IOptions<ReportOptions> report,
|
IOptions<ReportOptions> report)
|
||||||
IOptions<ConsoleOptions> console)
|
|
||||||
{
|
{
|
||||||
_devices = devices.Value;
|
_devices = devices.Value;
|
||||||
_storage = storage.Value;
|
_storage = storage.Value;
|
||||||
_report = report.Value;
|
_report = report.Value;
|
||||||
_console = console.Value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string SettingsPath => Path.Combine(
|
public static string SettingsPath => Path.Combine(
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||||
"SSPCTester", "settings.json");
|
"SSPCTester", "settings.json");
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Loads console presets directly from the user settings JSON so preset arrays
|
|
||||||
/// replace the defaults as a whole. IConfiguration merges arrays by index and
|
|
||||||
/// cannot represent a deleted item or an explicitly empty preset list.
|
|
||||||
/// </summary>
|
|
||||||
public static ConsoleOptions? LoadConsoleOptions()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
string path = SettingsPath;
|
|
||||||
if (!File.Exists(path)) return null;
|
|
||||||
|
|
||||||
using var document = JsonDocument.Parse(File.ReadAllText(path));
|
|
||||||
if (!document.RootElement.TryGetProperty(ConsoleOptions.SectionName, out var consoleElement))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var console = consoleElement.Deserialize<ConsoleOptions>(JsonOptions);
|
|
||||||
if (console is null) return null;
|
|
||||||
|
|
||||||
EnsureConsoleDefaults(console);
|
|
||||||
return console;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SaveAsync()
|
public async Task SaveAsync()
|
||||||
{
|
|
||||||
await SaveAsync(_devices, _storage, _report, _console).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SaveAsync(DeviceOptions devices, StorageOptions storage, ReportOptions report)
|
|
||||||
{
|
|
||||||
await SaveAsync(devices, storage, report, _console).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SaveAsync(DeviceOptions devices, StorageOptions storage, ReportOptions report, ConsoleOptions console)
|
|
||||||
{
|
{
|
||||||
await _writeLock.WaitAsync().ConfigureAwait(false);
|
await _writeLock.WaitAsync().ConfigureAwait(false);
|
||||||
try
|
try
|
||||||
@ -82,58 +41,21 @@ public sealed class UserSettingsStore : IDisposable
|
|||||||
string path = SettingsPath;
|
string path = SettingsPath;
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||||
string temp = path + ".tmp";
|
string temp = path + ".tmp";
|
||||||
var snapshot = CreateSnapshot(devices, storage, report, console);
|
_devices.Daq.Channels = DaqChannelOptionsNormalizer.Normalize(_devices.Daq.Channels);
|
||||||
var model = new
|
var model = new
|
||||||
{
|
{
|
||||||
Devices = snapshot.Devices,
|
Devices = _devices,
|
||||||
Storage = snapshot.Storage,
|
Storage = _storage,
|
||||||
Report = snapshot.Report,
|
Report = _report
|
||||||
Console = snapshot.Console
|
|
||||||
};
|
};
|
||||||
await File.WriteAllTextAsync(temp, JsonSerializer.Serialize(model, JsonOptions)).ConfigureAwait(false);
|
await File.WriteAllTextAsync(temp, JsonSerializer.Serialize(model, JsonOptions)).ConfigureAwait(false);
|
||||||
File.Move(temp, path, true);
|
File.Move(temp, path, true);
|
||||||
WriteDaqSaveTrace(path, snapshot.Devices);
|
WriteDaqSaveTrace(path);
|
||||||
}
|
}
|
||||||
finally { _writeLock.Release(); }
|
finally { _writeLock.Release(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
public SettingsSnapshot? LoadSnapshot()
|
private void WriteDaqSaveTrace(string settingsPath)
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
string path = SettingsPath;
|
|
||||||
if (!File.Exists(path)) return null;
|
|
||||||
var json = File.ReadAllText(path);
|
|
||||||
var payload = JsonSerializer.Deserialize<SettingsPayload>(json, JsonOptions);
|
|
||||||
if (payload is null) return null;
|
|
||||||
return CreateSnapshot(payload.Devices, payload.Storage, payload.Report, payload.Console);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static SettingsSnapshot CreateSnapshot(DeviceOptions devices, StorageOptions storage, ReportOptions report, ConsoleOptions console)
|
|
||||||
{
|
|
||||||
var copiedDevices = JsonSerializer.Deserialize<DeviceOptions>(JsonSerializer.Serialize(devices, JsonOptions)) ?? new DeviceOptions();
|
|
||||||
var copiedStorage = JsonSerializer.Deserialize<StorageOptions>(JsonSerializer.Serialize(storage, JsonOptions)) ?? new StorageOptions();
|
|
||||||
var copiedReport = JsonSerializer.Deserialize<ReportOptions>(JsonSerializer.Serialize(report, JsonOptions)) ?? new ReportOptions();
|
|
||||||
var copiedConsole = JsonSerializer.Deserialize<ConsoleOptions>(JsonSerializer.Serialize(console, JsonOptions)) ?? new ConsoleOptions();
|
|
||||||
copiedDevices.Daq.Channels = DaqChannelOptionsNormalizer.Normalize(copiedDevices.Daq.Channels);
|
|
||||||
EnsureConsoleDefaults(copiedConsole);
|
|
||||||
return new SettingsSnapshot(copiedDevices, copiedStorage, copiedReport, copiedConsole);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void EnsureConsoleDefaults(ConsoleOptions console)
|
|
||||||
{
|
|
||||||
if (console.PowerPresets == null)
|
|
||||||
console.PowerPresets = ConsoleOptions.DefaultPowerPresets();
|
|
||||||
if (console.LoadPresets == null)
|
|
||||||
console.LoadPresets = ConsoleOptions.DefaultLoadPresets();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void WriteDaqSaveTrace(string settingsPath, DeviceOptions devices)
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -141,25 +63,15 @@ public sealed class UserSettingsStore : IDisposable
|
|||||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||||
"SSPCTester", "Logs");
|
"SSPCTester", "Logs");
|
||||||
Directory.CreateDirectory(logDir);
|
Directory.CreateDirectory(logDir);
|
||||||
string channels = string.Join(", ", devices.Daq.Channels
|
string channels = string.Join(", ", _devices.Daq.Channels
|
||||||
.OrderBy(x => x.PhysicalChannel)
|
.OrderBy(x => x.PhysicalChannel)
|
||||||
.Select(x => $"CH{x.PhysicalChannel}:{x.Role}/{x.IsCalibrated}"));
|
.Select(x => $"CH{x.PhysicalChannel}:{x.Role}/{x.IsCalibrated}"));
|
||||||
File.AppendAllText(
|
File.AppendAllText(
|
||||||
Path.Combine(logDir, "settings-save.log"),
|
Path.Combine(logDir, "settings-save.log"),
|
||||||
$"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} {settingsPath} Pre={devices.Daq.PreTriggerMs:R} Post={devices.Daq.PostTriggerMs:R} Channels=[{channels}]{Environment.NewLine}");
|
$"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} {settingsPath} Pre={_devices.Daq.PreTriggerMs:R} Post={_devices.Daq.PostTriggerMs:R} Channels=[{channels}]{Environment.NewLine}");
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose() => _writeLock.Dispose();
|
public void Dispose() => _writeLock.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record SettingsSnapshot(DeviceOptions Devices, StorageOptions Storage, ReportOptions Report, ConsoleOptions Console);
|
|
||||||
|
|
||||||
internal sealed class SettingsPayload
|
|
||||||
{
|
|
||||||
public DeviceOptions Devices { get; set; } = new();
|
|
||||||
public StorageOptions Storage { get; set; } = new();
|
|
||||||
public ReportOptions Report { get; set; } = new();
|
|
||||||
public ConsoleOptions Console { get; set; } = new();
|
|
||||||
}
|
|
||||||
|
|||||||
@ -298,36 +298,6 @@ public partial class CurveTestViewModel : ObservableObject
|
|||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private Task CaptureFalling() => CaptureSingleCurveAsync(CurveCaptureKind.Falling);
|
private Task CaptureFalling() => CaptureSingleCurveAsync(CurveCaptureKind.Falling);
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private void Clear()
|
|
||||||
{
|
|
||||||
foreach (var row in Rows)
|
|
||||||
{
|
|
||||||
row.OnTimeMs = 0;
|
|
||||||
row.RiseTimeMs = 0;
|
|
||||||
row.OffTimeMs = 0;
|
|
||||||
row.FallTimeMs = 0;
|
|
||||||
row.RisingWaveformPath = null;
|
|
||||||
row.FallingWaveformPath = null;
|
|
||||||
row.ViewStartMs = null;
|
|
||||||
row.ViewEndMs = null;
|
|
||||||
row.RisingViewStartMs = null;
|
|
||||||
row.RisingViewEndMs = null;
|
|
||||||
row.FallingViewStartMs = null;
|
|
||||||
row.FallingViewEndMs = null;
|
|
||||||
row.Status = TestStatus.Untested;
|
|
||||||
}
|
|
||||||
|
|
||||||
RisingCurve = null;
|
|
||||||
FallingCurve = null;
|
|
||||||
RisingViewStartMs = null;
|
|
||||||
RisingViewEndMs = null;
|
|
||||||
FallingViewStartMs = null;
|
|
||||||
FallingViewEndMs = null;
|
|
||||||
SaveProjectQuietly();
|
|
||||||
_log.Add(UiLogLevel.Info, "开启与关断曲线结果已清空。");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task CaptureSingleCurveAsync(CurveCaptureKind kind)
|
private async Task CaptureSingleCurveAsync(CurveCaptureKind kind)
|
||||||
{
|
{
|
||||||
if (Project == null || Busy) return;
|
if (Project == null || Busy) return;
|
||||||
|
|||||||
@ -1,692 +0,0 @@
|
|||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.Windows;
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
|
||||||
using CommunityToolkit.Mvvm.Input;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using SSPCTester.Devices.Interfaces;
|
|
||||||
using SSPCTester.UI.Services;
|
|
||||||
|
|
||||||
namespace SSPCTester.UI.ViewModels;
|
|
||||||
|
|
||||||
public partial class ExperimentConsoleViewModel : ObservableObject
|
|
||||||
{
|
|
||||||
private readonly IPowerSupply _power;
|
|
||||||
private readonly ISspc _sspc;
|
|
||||||
private readonly ILoad _load;
|
|
||||||
private readonly ConsoleOptions _options;
|
|
||||||
private readonly UserSettingsStore _settingsStore;
|
|
||||||
private readonly UiLogSink _log;
|
|
||||||
private readonly object _pollLock = new();
|
|
||||||
private readonly HashSet<object> _activeHosts = new(ReferenceEqualityComparer.Instance);
|
|
||||||
private CancellationTokenSource? _pollCts;
|
|
||||||
private Task? _pollTask;
|
|
||||||
|
|
||||||
[ObservableProperty] private bool _busy;
|
|
||||||
[ObservableProperty] private double _powerVoltageSetpoint = 3.0;
|
|
||||||
[ObservableProperty] private double _powerCurrentLimit = 1.0;
|
|
||||||
[ObservableProperty] private PowerPreset? _selectedPowerPreset;
|
|
||||||
[ObservableProperty] private double _powerVoltage = double.NaN;
|
|
||||||
[ObservableProperty] private double _powerCurrent = double.NaN;
|
|
||||||
[ObservableProperty] private double _powerWatts = double.NaN;
|
|
||||||
[ObservableProperty] private bool? _isPowerOutputOn;
|
|
||||||
[ObservableProperty] private string _powerOutputStateText = "未知";
|
|
||||||
[ObservableProperty] private string _powerModeText = "未知";
|
|
||||||
[ObservableProperty] private string _powerProtectionStateText = "未连接";
|
|
||||||
|
|
||||||
[ObservableProperty] private double _loadCurrentSetpoint = 1.0;
|
|
||||||
[ObservableProperty] private LoadPreset? _selectedLoadPreset;
|
|
||||||
[ObservableProperty] private double _loadVoltage = double.NaN;
|
|
||||||
[ObservableProperty] private double _loadCurrent = double.NaN;
|
|
||||||
[ObservableProperty] private double _loadWatts = double.NaN;
|
|
||||||
[ObservableProperty] private bool? _isLoadInputOn;
|
|
||||||
[ObservableProperty] private string _loadInputStateText = "未知";
|
|
||||||
[ObservableProperty] private string _loadProtectionStateText = "未连接";
|
|
||||||
|
|
||||||
public ObservableCollection<PowerPreset> PowerPresets { get; } = new();
|
|
||||||
public ObservableCollection<LoadPreset> LoadPresets { get; } = new();
|
|
||||||
public ObservableCollection<ExperimentConsoleChannelRow> Channels { get; } = new();
|
|
||||||
|
|
||||||
public ExperimentConsoleViewModel(
|
|
||||||
IPowerSupply power,
|
|
||||||
ISspc sspc,
|
|
||||||
ILoad load,
|
|
||||||
IOptions<ConsoleOptions> options,
|
|
||||||
UserSettingsStore settingsStore,
|
|
||||||
UiLogSink log)
|
|
||||||
{
|
|
||||||
_power = power;
|
|
||||||
_sspc = sspc;
|
|
||||||
_load = load;
|
|
||||||
_options = options.Value;
|
|
||||||
_settingsStore = settingsStore;
|
|
||||||
_log = log;
|
|
||||||
|
|
||||||
foreach (var preset in _options.PowerPresets)
|
|
||||||
PowerPresets.Add(Clone(preset));
|
|
||||||
foreach (var preset in _options.LoadPresets)
|
|
||||||
LoadPresets.Add(Clone(preset));
|
|
||||||
for (int i = 1; i <= _sspc.ChannelCount; i++)
|
|
||||||
Channels.Add(new ExperimentConsoleChannelRow(i));
|
|
||||||
|
|
||||||
SelectedPowerPreset = PowerPresets.FirstOrDefault();
|
|
||||||
SelectedLoadPreset = LoadPresets.FirstOrDefault();
|
|
||||||
|
|
||||||
_power.ConnectionChanged += (_, _) => RaiseConnectionStatus();
|
|
||||||
_sspc.ConnectionChanged += (_, _) => RaiseConnectionStatus();
|
|
||||||
_load.ConnectionChanged += (_, _) => RaiseConnectionStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
public string PowerConnectionStatus => _power.IsConnected ? "已连接" : "未连接";
|
|
||||||
public string RelayConnectionStatus => _sspc.IsConnected ? "已连接" : "未连接";
|
|
||||||
public string LoadConnectionStatus => _load.IsConnected ? "已连接" : "未连接";
|
|
||||||
public bool IsPowerConnected => _power.IsConnected;
|
|
||||||
public bool IsRelayConnected => _sspc.IsConnected;
|
|
||||||
public bool IsLoadConnected => _load.IsConnected;
|
|
||||||
public bool CanTurnPowerOutputOn => IsPowerOutputOn != true;
|
|
||||||
public bool CanTurnPowerOutputOff => IsPowerOutputOn != false;
|
|
||||||
public bool CanTurnLoadInputOn => IsLoadInputOn != true;
|
|
||||||
public bool CanTurnLoadInputOff => IsLoadInputOn != false;
|
|
||||||
|
|
||||||
partial void OnIsPowerOutputOnChanged(bool? value)
|
|
||||||
{
|
|
||||||
OnPropertyChanged(nameof(CanTurnPowerOutputOn));
|
|
||||||
OnPropertyChanged(nameof(CanTurnPowerOutputOff));
|
|
||||||
}
|
|
||||||
|
|
||||||
partial void OnIsLoadInputOnChanged(bool? value)
|
|
||||||
{
|
|
||||||
OnPropertyChanged(nameof(CanTurnLoadInputOn));
|
|
||||||
OnPropertyChanged(nameof(CanTurnLoadInputOff));
|
|
||||||
}
|
|
||||||
|
|
||||||
partial void OnSelectedPowerPresetChanged(PowerPreset? value)
|
|
||||||
{
|
|
||||||
DeletePowerPresetCommand.NotifyCanExecuteChanged();
|
|
||||||
if (value == null) return;
|
|
||||||
PowerVoltageSetpoint = value.Voltage;
|
|
||||||
PowerCurrentLimit = value.CurrentLimit;
|
|
||||||
}
|
|
||||||
|
|
||||||
partial void OnSelectedLoadPresetChanged(LoadPreset? value)
|
|
||||||
{
|
|
||||||
DeleteLoadPresetCommand.NotifyCanExecuteChanged();
|
|
||||||
if (value == null) return;
|
|
||||||
LoadCurrentSetpoint = value.Current;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetActive(object host, bool active)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(host);
|
|
||||||
|
|
||||||
lock (_pollLock)
|
|
||||||
{
|
|
||||||
if (active)
|
|
||||||
{
|
|
||||||
_activeHosts.Add(host);
|
|
||||||
StartPolling();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_activeHosts.Remove(host);
|
|
||||||
if (_activeHosts.Count == 0)
|
|
||||||
StopPolling();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async Task ApplyPower()
|
|
||||||
{
|
|
||||||
if (!ValidateNonNegative(PowerVoltageSetpoint, "电源电压设定") ||
|
|
||||||
!ValidateNonNegative(PowerCurrentLimit, "电源电流限值"))
|
|
||||||
return;
|
|
||||||
|
|
||||||
await RunDeviceActionAsync("应用电源设置", async ct =>
|
|
||||||
{
|
|
||||||
if (!await EnsureConnectedAsync(_power, "电源", ct)) return;
|
|
||||||
await _power.SetVoltageAsync(PowerVoltageSetpoint, ct);
|
|
||||||
await _power.SetCurrentLimitAsync(PowerCurrentLimit, ct);
|
|
||||||
_log.Add(UiLogLevel.Success, $"电源设置已应用:V={PowerVoltageSetpoint:F3}V,限流={PowerCurrentLimit:F3}A。");
|
|
||||||
await RefreshPowerAsync(ct);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async Task SavePowerPreset()
|
|
||||||
{
|
|
||||||
if (!ValidateNonNegative(PowerVoltageSetpoint, "电源电压设定") ||
|
|
||||||
!ValidateNonNegative(PowerCurrentLimit, "电源电流限值"))
|
|
||||||
return;
|
|
||||||
|
|
||||||
string name = $"{PowerVoltageSetpoint:F3}V / {PowerCurrentLimit:F3}A";
|
|
||||||
var existing = PowerPresets.FirstOrDefault(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase));
|
|
||||||
if (existing == null)
|
|
||||||
{
|
|
||||||
existing = new PowerPreset { Name = name };
|
|
||||||
PowerPresets.Add(existing);
|
|
||||||
}
|
|
||||||
|
|
||||||
existing.Voltage = PowerVoltageSetpoint;
|
|
||||||
existing.CurrentLimit = PowerCurrentLimit;
|
|
||||||
SelectedPowerPreset = existing;
|
|
||||||
await PersistPresetsAsync();
|
|
||||||
_log.Add(UiLogLevel.Success, $"电源预设已保存:{existing.Name}。");
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool CanDeletePowerPreset() => SelectedPowerPreset != null;
|
|
||||||
|
|
||||||
[RelayCommand(CanExecute = nameof(CanDeletePowerPreset))]
|
|
||||||
private async Task DeletePowerPreset()
|
|
||||||
{
|
|
||||||
var preset = SelectedPowerPreset;
|
|
||||||
if (preset == null) return;
|
|
||||||
if (!Confirm($"确认删除电源预设:{preset.Name}?")) return;
|
|
||||||
|
|
||||||
int index = PowerPresets.IndexOf(preset);
|
|
||||||
PowerPresets.Remove(preset);
|
|
||||||
SelectedPowerPreset = PowerPresets.Count == 0
|
|
||||||
? null
|
|
||||||
: PowerPresets[Math.Clamp(index, 0, PowerPresets.Count - 1)];
|
|
||||||
await PersistPresetsAsync();
|
|
||||||
_log.Add(UiLogLevel.Warning, $"电源预设已删除:{preset.Name}。");
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async Task PowerOutputOn() => await SetPowerOutputAsync(true);
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async Task PowerOutputOff() => await SetPowerOutputAsync(false);
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async Task PowerSafeReset()
|
|
||||||
{
|
|
||||||
if (!Confirm("确认执行电源安全复位?")) return;
|
|
||||||
|
|
||||||
await RunDeviceActionAsync("电源安全复位", async ct =>
|
|
||||||
{
|
|
||||||
if (!await EnsureConnectedAsync(_power, "电源", ct)) return;
|
|
||||||
await _power.OutputAsync(false, ct);
|
|
||||||
await _power.SetVoltageAsync(0, ct);
|
|
||||||
await _power.SetCurrentLimitAsync(0, ct);
|
|
||||||
PowerVoltageSetpoint = 0;
|
|
||||||
PowerCurrentLimit = 0;
|
|
||||||
SelectedPowerPreset = null;
|
|
||||||
_log.Add(UiLogLevel.Warning, "电源已安全复位:输出关闭,电压和电流限值设为 0。");
|
|
||||||
await RefreshPowerAsync(ct);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async Task ApplyLoad()
|
|
||||||
{
|
|
||||||
if (!ValidateNonNegative(LoadCurrentSetpoint, "负载电流")) return;
|
|
||||||
|
|
||||||
await RunDeviceActionAsync("应用负载设置", async ct =>
|
|
||||||
{
|
|
||||||
if (!await EnsureConnectedAsync(_load, "负载", ct)) return;
|
|
||||||
await _load.SetModeAsync(LoadMode.CC, ct);
|
|
||||||
await _load.SetValueAsync(LoadCurrentSetpoint, ct);
|
|
||||||
_log.Add(UiLogLevel.Success, $"负载设置已应用:CC {LoadCurrentSetpoint:F3}A。");
|
|
||||||
await RefreshLoadAsync(ct);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async Task SaveLoadPreset()
|
|
||||||
{
|
|
||||||
if (!ValidateNonNegative(LoadCurrentSetpoint, "负载电流")) return;
|
|
||||||
|
|
||||||
string name = $"{LoadCurrentSetpoint:F3}A";
|
|
||||||
var existing = LoadPresets.FirstOrDefault(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase));
|
|
||||||
if (existing == null)
|
|
||||||
{
|
|
||||||
existing = new LoadPreset { Name = name };
|
|
||||||
LoadPresets.Add(existing);
|
|
||||||
}
|
|
||||||
|
|
||||||
existing.Current = LoadCurrentSetpoint;
|
|
||||||
SelectedLoadPreset = existing;
|
|
||||||
await PersistPresetsAsync();
|
|
||||||
_log.Add(UiLogLevel.Success, $"负载预设已保存:{existing.Name}。");
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool CanDeleteLoadPreset() => SelectedLoadPreset != null;
|
|
||||||
|
|
||||||
[RelayCommand(CanExecute = nameof(CanDeleteLoadPreset))]
|
|
||||||
private async Task DeleteLoadPreset()
|
|
||||||
{
|
|
||||||
var preset = SelectedLoadPreset;
|
|
||||||
if (preset == null) return;
|
|
||||||
if (!Confirm($"确认删除负载预设:{preset.Name}?")) return;
|
|
||||||
|
|
||||||
int index = LoadPresets.IndexOf(preset);
|
|
||||||
LoadPresets.Remove(preset);
|
|
||||||
SelectedLoadPreset = LoadPresets.Count == 0
|
|
||||||
? null
|
|
||||||
: LoadPresets[Math.Clamp(index, 0, LoadPresets.Count - 1)];
|
|
||||||
await PersistPresetsAsync();
|
|
||||||
_log.Add(UiLogLevel.Warning, $"负载预设已删除:{preset.Name}。");
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async Task LoadInputOn() => await SetLoadInputAsync(true);
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async Task LoadInputOff() => await SetLoadInputAsync(false);
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async Task LoadSafeReset()
|
|
||||||
{
|
|
||||||
if (!Confirm("确认执行负载安全复位?")) return;
|
|
||||||
|
|
||||||
await RunDeviceActionAsync("负载安全复位", async ct =>
|
|
||||||
{
|
|
||||||
if (!await EnsureConnectedAsync(_load, "负载", ct)) return;
|
|
||||||
await _load.InputAsync(false, ct);
|
|
||||||
await _load.ResetAsync(ct);
|
|
||||||
await _load.SetModeAsync(LoadMode.CC, ct);
|
|
||||||
await _load.SetValueAsync(0, ct);
|
|
||||||
await _load.InputAsync(false, ct);
|
|
||||||
LoadCurrentSetpoint = 0;
|
|
||||||
SelectedLoadPreset = null;
|
|
||||||
_log.Add(UiLogLevel.Warning, "负载已安全复位:输入关闭,CC 电流设为 0。");
|
|
||||||
await RefreshLoadAsync(ct);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async Task RelayAllOn()
|
|
||||||
{
|
|
||||||
if (!Confirm("确认全开 24 路通道?")) return;
|
|
||||||
|
|
||||||
await RunDeviceActionAsync("继电器全开", async ct =>
|
|
||||||
{
|
|
||||||
if (!await EnsureConnectedAsync(_sspc, "继电器", ct)) return;
|
|
||||||
foreach (var row in Channels)
|
|
||||||
await _sspc.TurnOnAsync(row.Channel, ct);
|
|
||||||
_log.Add(UiLogLevel.Warning, "继电器 24 路通道已全开。");
|
|
||||||
await RefreshRelayAsync(ct);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async Task RelayAllOff()
|
|
||||||
{
|
|
||||||
if (!Confirm("确认全关 24 路通道?")) return;
|
|
||||||
|
|
||||||
await RunDeviceActionAsync("继电器全关", async ct =>
|
|
||||||
{
|
|
||||||
if (!await EnsureConnectedAsync(_sspc, "继电器", ct)) return;
|
|
||||||
foreach (var row in Channels)
|
|
||||||
await _sspc.TurnOffAsync(row.Channel, ct);
|
|
||||||
_log.Add(UiLogLevel.Warning, "继电器 24 路通道已全关。");
|
|
||||||
await RefreshRelayAsync(ct);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async Task ReadRelayAll()
|
|
||||||
{
|
|
||||||
await RunDeviceActionAsync("读取继电器", async ct =>
|
|
||||||
{
|
|
||||||
if (!await EnsureConnectedAsync(_sspc, "继电器", ct)) return;
|
|
||||||
await RefreshRelayAsync(ct);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async Task TurnRelayOn(ExperimentConsoleChannelRow? row)
|
|
||||||
{
|
|
||||||
if (row == null) return;
|
|
||||||
await RunDeviceActionAsync($"CH{row.Channel:00} 开", async ct =>
|
|
||||||
{
|
|
||||||
if (!await EnsureConnectedAsync(_sspc, "继电器", ct)) return;
|
|
||||||
await _sspc.TurnOnAsync(row.Channel, ct);
|
|
||||||
await RefreshRelayChannelAsync(row, ct);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async Task TurnRelayOff(ExperimentConsoleChannelRow? row)
|
|
||||||
{
|
|
||||||
if (row == null) return;
|
|
||||||
await RunDeviceActionAsync($"CH{row.Channel:00} 关", async ct =>
|
|
||||||
{
|
|
||||||
if (!await EnsureConnectedAsync(_sspc, "继电器", ct)) return;
|
|
||||||
await _sspc.TurnOffAsync(row.Channel, ct);
|
|
||||||
await RefreshRelayChannelAsync(row, ct);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SetPowerOutputAsync(bool on)
|
|
||||||
{
|
|
||||||
await RunDeviceActionAsync(on ? "电源输出开" : "电源输出关", async ct =>
|
|
||||||
{
|
|
||||||
if (!await EnsureConnectedAsync(_power, "电源", ct)) return;
|
|
||||||
await _power.OutputAsync(on, ct);
|
|
||||||
_log.Add(UiLogLevel.Success, on ? "电源输出已开启。" : "电源输出已关闭。");
|
|
||||||
await RefreshPowerAsync(ct);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SetLoadInputAsync(bool on)
|
|
||||||
{
|
|
||||||
await RunDeviceActionAsync(on ? "负载输入开" : "负载输入关", async ct =>
|
|
||||||
{
|
|
||||||
if (!await EnsureConnectedAsync(_load, "负载", ct)) return;
|
|
||||||
await _load.InputAsync(on, ct);
|
|
||||||
_log.Add(UiLogLevel.Success, on ? "负载输入已开启。" : "负载输入已关闭。");
|
|
||||||
await RefreshLoadAsync(ct);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void StartPolling()
|
|
||||||
{
|
|
||||||
lock (_pollLock)
|
|
||||||
{
|
|
||||||
if (_pollTask is { IsCompleted: false }) return;
|
|
||||||
_pollCts = new CancellationTokenSource();
|
|
||||||
_pollTask = PollAsync(_pollCts.Token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void StopPolling()
|
|
||||||
{
|
|
||||||
Task? taskToObserve;
|
|
||||||
CancellationTokenSource? ctsToDispose;
|
|
||||||
lock (_pollLock)
|
|
||||||
{
|
|
||||||
taskToObserve = _pollTask;
|
|
||||||
ctsToDispose = _pollCts;
|
|
||||||
_pollTask = null;
|
|
||||||
_pollCts = null;
|
|
||||||
ctsToDispose?.Cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (taskToObserve != null)
|
|
||||||
_ = taskToObserve.ContinueWith(_ => ctsToDispose?.Dispose(), TaskScheduler.Default);
|
|
||||||
else
|
|
||||||
ctsToDispose?.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task PollAsync(CancellationToken ct)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
while (!ct.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
await RefreshAllConnectedAsync(ct).ConfigureAwait(false);
|
|
||||||
await Task.Delay(TimeSpan.FromSeconds(1), ct).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException) when (ct.IsCancellationRequested) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RefreshAllConnectedAsync(CancellationToken ct)
|
|
||||||
{
|
|
||||||
if (_power.IsConnected)
|
|
||||||
await RefreshPowerAsync(ct).ConfigureAwait(false);
|
|
||||||
else
|
|
||||||
await RunOnUiAsync(() =>
|
|
||||||
{
|
|
||||||
IsPowerOutputOn = null;
|
|
||||||
PowerOutputStateText = "未连接";
|
|
||||||
PowerProtectionStateText = "未连接";
|
|
||||||
}).ConfigureAwait(false);
|
|
||||||
if (_load.IsConnected)
|
|
||||||
await RefreshLoadAsync(ct).ConfigureAwait(false);
|
|
||||||
else
|
|
||||||
await RunOnUiAsync(() =>
|
|
||||||
{
|
|
||||||
IsLoadInputOn = null;
|
|
||||||
LoadInputStateText = "未连接";
|
|
||||||
LoadProtectionStateText = "未连接";
|
|
||||||
}).ConfigureAwait(false);
|
|
||||||
if (_sspc.IsConnected)
|
|
||||||
await RefreshRelayAsync(ct).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RefreshPowerAsync(CancellationToken ct)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var (volts, amps) = await _power.ReadAsync(ct).ConfigureAwait(false);
|
|
||||||
bool outputOn = await _power.GetOutputStateAsync(ct).ConfigureAwait(false);
|
|
||||||
string errorState = await QueryErrorStateAsync(_power, ct).ConfigureAwait(false);
|
|
||||||
await RunOnUiAsync(() =>
|
|
||||||
{
|
|
||||||
PowerVoltage = volts;
|
|
||||||
PowerCurrent = Math.Abs(amps);
|
|
||||||
PowerWatts = Math.Abs(volts * amps);
|
|
||||||
PowerModeText = InferPowerMode(Math.Abs(amps));
|
|
||||||
IsPowerOutputOn = outputOn;
|
|
||||||
PowerOutputStateText = outputOn ? "ON" : "OFF";
|
|
||||||
PowerProtectionStateText = errorState;
|
|
||||||
}).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException) when (ct.IsCancellationRequested) { }
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
await RunOnUiAsync(() =>
|
|
||||||
{
|
|
||||||
IsPowerOutputOn = null;
|
|
||||||
PowerOutputStateText = _power.IsConnected ? "未知" : "未连接";
|
|
||||||
PowerProtectionStateText = _power.IsConnected ? "通讯异常" : "未连接";
|
|
||||||
}).ConfigureAwait(false);
|
|
||||||
_log.Add(UiLogLevel.Error, $"电源读数刷新失败:{ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RefreshLoadAsync(CancellationToken ct)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var reading = await _load.ReadPowerAsync(ct).ConfigureAwait(false);
|
|
||||||
bool inputOn = await _load.GetInputStateAsync(ct).ConfigureAwait(false);
|
|
||||||
string errorState = await QueryErrorStateAsync(_load, ct).ConfigureAwait(false);
|
|
||||||
await RunOnUiAsync(() =>
|
|
||||||
{
|
|
||||||
LoadVoltage = reading.Volts;
|
|
||||||
LoadCurrent = Math.Abs(reading.Amps);
|
|
||||||
LoadWatts = Math.Abs(reading.Watts);
|
|
||||||
IsLoadInputOn = inputOn;
|
|
||||||
LoadInputStateText = inputOn ? "ON" : "OFF";
|
|
||||||
LoadProtectionStateText = errorState;
|
|
||||||
}).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException) when (ct.IsCancellationRequested) { }
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
await RunOnUiAsync(() =>
|
|
||||||
{
|
|
||||||
IsLoadInputOn = null;
|
|
||||||
LoadInputStateText = _load.IsConnected ? "未知" : "未连接";
|
|
||||||
LoadProtectionStateText = _load.IsConnected ? "通讯异常" : "未连接";
|
|
||||||
}).ConfigureAwait(false);
|
|
||||||
_log.Add(UiLogLevel.Error, $"负载读数刷新失败:{ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RefreshRelayAsync(CancellationToken ct)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var measurements = await _sspc.ReadMeasurementsAsync(ct).ConfigureAwait(false);
|
|
||||||
await RunOnUiAsync(() =>
|
|
||||||
{
|
|
||||||
foreach (var measurement in measurements)
|
|
||||||
{
|
|
||||||
var row = Channels.FirstOrDefault(x => x.Channel == measurement.PhysicalSlot);
|
|
||||||
row?.Apply(measurement.Voltage, Math.Abs(measurement.Current));
|
|
||||||
}
|
|
||||||
}).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException) when (ct.IsCancellationRequested) { }
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_log.Add(UiLogLevel.Error, $"继电器读数刷新失败:{ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RefreshRelayChannelAsync(ExperimentConsoleChannelRow row, CancellationToken ct)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var measurement = await _sspc.ReadMeasurementAsync(row.Channel, ct).ConfigureAwait(false);
|
|
||||||
await RunOnUiAsync(() => row.Apply(measurement.Voltage, Math.Abs(measurement.Current))).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException) when (ct.IsCancellationRequested) { }
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_log.Add(UiLogLevel.Error, $"CH{row.Channel:00} 读数刷新失败:{ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RunDeviceActionAsync(string actionName, Func<CancellationToken, Task> action)
|
|
||||||
{
|
|
||||||
if (Busy) return;
|
|
||||||
Busy = true;
|
|
||||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await action(cts.Token).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
_log.Add(UiLogLevel.Error, $"{actionName} 超时或已取消。");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_log.Add(UiLogLevel.Error, $"{actionName} 失败:{ex.Message}");
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
await RunOnUiAsync(() =>
|
|
||||||
{
|
|
||||||
Busy = false;
|
|
||||||
RaiseConnectionStatus();
|
|
||||||
}).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<bool> EnsureConnectedAsync(IDeviceConnection device, string name, CancellationToken ct)
|
|
||||||
{
|
|
||||||
if (device.IsConnected) return true;
|
|
||||||
|
|
||||||
bool connected = await device.ConnectAsync(ct).ConfigureAwait(false);
|
|
||||||
await RunOnUiAsync(RaiseConnectionStatus).ConfigureAwait(false);
|
|
||||||
if (connected && device.IsConnected) return true;
|
|
||||||
|
|
||||||
_log.Add(UiLogLevel.Warning, $"{name} 未连接,操作未执行。");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string InferPowerMode(double amps)
|
|
||||||
{
|
|
||||||
if (!IsValidNonNegative(PowerCurrentLimit) || PowerCurrentLimit <= 0 || double.IsNaN(amps))
|
|
||||||
return "未知";
|
|
||||||
return amps >= PowerCurrentLimit * 0.95 ? "CC" : "CV";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<string> QueryErrorStateAsync(IPowerSupply power, CancellationToken ct)
|
|
||||||
{
|
|
||||||
if (!power.IsConnected) return "未连接";
|
|
||||||
return FormatErrorState(await power.QueryErrorAsync(ct).ConfigureAwait(false));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<string> QueryErrorStateAsync(ILoad load, CancellationToken ct)
|
|
||||||
{
|
|
||||||
if (!load.IsConnected) return "未连接";
|
|
||||||
return FormatErrorState(await load.QueryErrorAsync(ct).ConfigureAwait(false));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string FormatErrorState(string? response)
|
|
||||||
{
|
|
||||||
string text = (response ?? "").Trim();
|
|
||||||
if (string.IsNullOrWhiteSpace(text))
|
|
||||||
return "正常";
|
|
||||||
|
|
||||||
string lower = text.ToLowerInvariant();
|
|
||||||
string firstPart = text.Split(',', 2)[0].Trim().Trim('"');
|
|
||||||
bool zeroCode = int.TryParse(firstPart.TrimStart('+'), out int code) && code == 0;
|
|
||||||
if (zeroCode || lower.Contains("no error") || lower.Contains("noerror"))
|
|
||||||
return "正常";
|
|
||||||
|
|
||||||
return text.Length > 64 ? $"异常:{text[..64]}..." : $"异常:{text}";
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool ValidateNonNegative(double value, string name)
|
|
||||||
{
|
|
||||||
if (IsValidNonNegative(value)) return true;
|
|
||||||
_log.Add(UiLogLevel.Error, $"{name} 必须是有限非负数。");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsValidNonNegative(double value) =>
|
|
||||||
double.IsFinite(value) && value >= 0;
|
|
||||||
|
|
||||||
private async Task PersistPresetsAsync()
|
|
||||||
{
|
|
||||||
_options.PowerPresets = PowerPresets.Select(Clone).ToList();
|
|
||||||
_options.LoadPresets = LoadPresets.Select(Clone).ToList();
|
|
||||||
await _settingsStore.SaveAsync().ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static PowerPreset Clone(PowerPreset source) => new()
|
|
||||||
{
|
|
||||||
Name = source.Name,
|
|
||||||
Voltage = source.Voltage,
|
|
||||||
CurrentLimit = source.CurrentLimit
|
|
||||||
};
|
|
||||||
|
|
||||||
private static LoadPreset Clone(LoadPreset source) => new()
|
|
||||||
{
|
|
||||||
Name = source.Name,
|
|
||||||
Current = source.Current
|
|
||||||
};
|
|
||||||
|
|
||||||
private static bool Confirm(string message) =>
|
|
||||||
MessageBox.Show(message, "确认操作", MessageBoxButton.YesNo, MessageBoxImage.Warning) == MessageBoxResult.Yes;
|
|
||||||
|
|
||||||
private void RaiseConnectionStatus()
|
|
||||||
{
|
|
||||||
OnPropertyChanged(nameof(PowerConnectionStatus));
|
|
||||||
OnPropertyChanged(nameof(RelayConnectionStatus));
|
|
||||||
OnPropertyChanged(nameof(LoadConnectionStatus));
|
|
||||||
OnPropertyChanged(nameof(IsPowerConnected));
|
|
||||||
OnPropertyChanged(nameof(IsRelayConnected));
|
|
||||||
OnPropertyChanged(nameof(IsLoadConnected));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Task RunOnUiAsync(Action action)
|
|
||||||
{
|
|
||||||
var dispatcher = Application.Current?.Dispatcher;
|
|
||||||
if (dispatcher == null || dispatcher.CheckAccess())
|
|
||||||
{
|
|
||||||
action();
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
return dispatcher.InvokeAsync(action).Task;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public partial class ExperimentConsoleChannelRow : ObservableObject
|
|
||||||
{
|
|
||||||
public ExperimentConsoleChannelRow(int channel)
|
|
||||||
{
|
|
||||||
Channel = channel;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int Channel { get; }
|
|
||||||
public string ChannelText => $"CH{Channel:00}";
|
|
||||||
|
|
||||||
[ObservableProperty] private double _voltage = double.NaN;
|
|
||||||
[ObservableProperty] private double _current = double.NaN;
|
|
||||||
|
|
||||||
public void Apply(double voltage, double current)
|
|
||||||
{
|
|
||||||
Voltage = voltage;
|
|
||||||
Current = current;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -163,10 +163,7 @@ public partial class MainViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
SafetyStatusText = _safetyMonitor.StatusText;
|
SafetyStatusText = _safetyMonitor.StatusText;
|
||||||
SafetyDetailText = _safetyMonitor.DetailText;
|
SafetyDetailText = _safetyMonitor.DetailText;
|
||||||
// Enter the alarm UI state as soon as an emergency is detected. The
|
AlarmLampActive = _safetyMonitor.IsAlarmOutputOn;
|
||||||
// physical alarm output and the sequential device shutdown can take
|
|
||||||
// time, so waiting for the DO state makes the UI appear unresponsive.
|
|
||||||
AlarmLampActive = _safetyMonitor.IsEmergencyActive || _safetyMonitor.IsAlarmOutputOn;
|
|
||||||
EmergencyButtonText = AlarmLampActive ? "状态重置" : "报警急停";
|
EmergencyButtonText = AlarmLampActive ? "状态重置" : "报警急停";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,10 +11,10 @@ namespace SSPCTester.UI.ViewModels;
|
|||||||
/// <summary>功率损耗页 VM。</summary>
|
/// <summary>功率损耗页 VM。</summary>
|
||||||
public partial class PowerTestViewModel : ObservableObject
|
public partial class PowerTestViewModel : ObservableObject
|
||||||
{
|
{
|
||||||
|
private readonly ISspc _sspc;
|
||||||
private readonly IPowerSupply _power;
|
private readonly IPowerSupply _power;
|
||||||
private readonly ILoad _load;
|
private readonly ILoad _load;
|
||||||
private readonly UiLogSink _log;
|
private readonly UiLogSink _log;
|
||||||
private bool _syncingSelection;
|
|
||||||
|
|
||||||
public ObservableCollection<PowerLossRow> Rows { get; } = new();
|
public ObservableCollection<PowerLossRow> Rows { get; } = new();
|
||||||
public ObservableCollection<int> Channels { get; } = new();
|
public ObservableCollection<int> Channels { get; } = new();
|
||||||
@ -29,6 +29,7 @@ public partial class PowerTestViewModel : ObservableObject
|
|||||||
|
|
||||||
public PowerTestViewModel(ISspc sspc, IPowerSupply power, ILoad load, UiLogSink log)
|
public PowerTestViewModel(ISspc sspc, IPowerSupply power, ILoad load, UiLogSink log)
|
||||||
{
|
{
|
||||||
|
_sspc = sspc;
|
||||||
_power = power;
|
_power = power;
|
||||||
_load = load;
|
_load = load;
|
||||||
_log = log;
|
_log = log;
|
||||||
@ -48,34 +49,48 @@ public partial class PowerTestViewModel : ObservableObject
|
|||||||
p.PowerResults.Add(new PowerLossRow { Channel = i });
|
p.PowerResults.Add(new PowerLossRow { Channel = i });
|
||||||
}
|
}
|
||||||
foreach (var r in p.PowerResults) Rows.Add(r);
|
foreach (var r in p.PowerResults) Rows.Add(r);
|
||||||
SyncCurrentFromSelectedChannel();
|
Current = Rows.FirstOrDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void OnSelectedChannelChanged(int value)
|
[RelayCommand]
|
||||||
|
private async Task TurnOnCurrentChannel()
|
||||||
{
|
{
|
||||||
if (_syncingSelection) return;
|
Busy = true;
|
||||||
SyncCurrentFromSelectedChannel();
|
int ch = SelectedChannel;
|
||||||
}
|
|
||||||
|
|
||||||
partial void OnCurrentChanged(PowerLossRow? value)
|
|
||||||
{
|
|
||||||
if (_syncingSelection || value == null) return;
|
|
||||||
if (SelectedChannel != value.Channel)
|
|
||||||
SelectedChannel = value.Channel;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SyncCurrentFromSelectedChannel()
|
|
||||||
{
|
|
||||||
_syncingSelection = true;
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Current = Rows.FirstOrDefault(x => x.Channel == SelectedChannel)
|
if (!_sspc.IsConnected) await _sspc.ConnectAsync();
|
||||||
?? Project?.PowerResults.FirstOrDefault(x => x.Channel == SelectedChannel)
|
await _sspc.TurnOnAsync(ch);
|
||||||
?? Rows.FirstOrDefault();
|
_log.Add(UiLogLevel.Info, $"CH{ch} 已开启。");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_log.Add(UiLogLevel.Error, $"CH{ch} 开启失败:{ex.Message}");
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_syncingSelection = false;
|
Busy = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task TurnOffCurrentChannel()
|
||||||
|
{
|
||||||
|
Busy = true;
|
||||||
|
int ch = SelectedChannel;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!_sspc.IsConnected) await _sspc.ConnectAsync();
|
||||||
|
await _sspc.TurnOffAsync(ch);
|
||||||
|
_log.Add(UiLogLevel.Info, $"CH{ch} 已关断。");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_log.Add(UiLogLevel.Error, $"CH{ch} 关断失败:{ex.Message}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Busy = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,23 +181,6 @@ public partial class PowerTestViewModel : ObservableObject
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private void Clear()
|
|
||||||
{
|
|
||||||
foreach (var row in Rows)
|
|
||||||
{
|
|
||||||
row.Vin = 0;
|
|
||||||
row.Iin = 0;
|
|
||||||
row.Vout = 0;
|
|
||||||
row.Iout = 0;
|
|
||||||
row.MeasuredPowerOut = null;
|
|
||||||
row.Status = TestStatus.Untested;
|
|
||||||
}
|
|
||||||
|
|
||||||
SyncCurrentFromSelectedChannel();
|
|
||||||
_log.Add(UiLogLevel.Info, "功率损耗结果已清空。");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>解析用于输入端电压的高速采集卡通道(取已标定的电压通道,优先 OutputVoltage)。</summary>
|
/// <summary>解析用于输入端电压的高速采集卡通道(取已标定的电压通道,优先 OutputVoltage)。</summary>
|
||||||
private static int ResolveDaqVoltageChannel(DaqOptions options)
|
private static int ResolveDaqVoltageChannel(DaqOptions options)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -51,10 +51,9 @@ public partial class SettingsViewModel : ObservableObject
|
|||||||
_settingsStore = settingsStore;
|
_settingsStore = settingsStore;
|
||||||
_projectStore = projectStore;
|
_projectStore = projectStore;
|
||||||
|
|
||||||
var persisted = _settingsStore.LoadSnapshot();
|
Devices = CloneDeviceOptions(_runtimeDevices);
|
||||||
Devices = CloneDeviceOptions(persisted?.Devices ?? _runtimeDevices);
|
Storage = CloneStorageOptions(_runtimeStorage);
|
||||||
Storage = CloneStorageOptions(persisted?.Storage ?? _runtimeStorage);
|
Report = CloneReportOptions(_runtimeReport);
|
||||||
Report = CloneReportOptions(persisted?.Report ?? _runtimeReport);
|
|
||||||
Devices.Daq.Channels = DaqChannelOptionsNormalizer.Normalize(Devices.Daq.Channels);
|
Devices.Daq.Channels = DaqChannelOptionsNormalizer.Normalize(Devices.Daq.Channels);
|
||||||
|
|
||||||
LoadDaqChannelEditors();
|
LoadDaqChannelEditors();
|
||||||
@ -174,27 +173,11 @@ public partial class SettingsViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
ApplyDaqChannelEditorsToDraft();
|
ApplyDaqChannelEditorsToDraft();
|
||||||
Devices.Daq.Channels = DaqChannelOptionsNormalizer.Normalize(Devices.Daq.Channels);
|
Devices.Daq.Channels = DaqChannelOptionsNormalizer.Normalize(Devices.Daq.Channels);
|
||||||
await _settingsStore.SaveAsync(Devices, Storage, Report);
|
|
||||||
|
|
||||||
var persisted = _settingsStore.LoadSnapshot();
|
|
||||||
if (persisted is not null)
|
|
||||||
{
|
|
||||||
ApplyToRuntime(persisted.Devices, _runtimeDevices);
|
|
||||||
ApplyToRuntime(persisted.Storage, _runtimeStorage);
|
|
||||||
ApplyToRuntime(persisted.Report, _runtimeReport);
|
|
||||||
ApplyToRuntime(persisted.Devices, Devices);
|
|
||||||
ApplyToRuntime(persisted.Storage, Storage);
|
|
||||||
ApplyToRuntime(persisted.Report, Report);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ApplyToRuntime(Devices, _runtimeDevices);
|
ApplyToRuntime(Devices, _runtimeDevices);
|
||||||
ApplyToRuntime(Storage, _runtimeStorage);
|
ApplyToRuntime(Storage, _runtimeStorage);
|
||||||
ApplyToRuntime(Report, _runtimeReport);
|
ApplyToRuntime(Report, _runtimeReport);
|
||||||
}
|
|
||||||
|
|
||||||
LoadDaqChannelEditors();
|
|
||||||
ApplyProjectsRootToStore();
|
ApplyProjectsRootToStore();
|
||||||
|
await _settingsStore.SaveAsync();
|
||||||
OnPropertyChanged(nameof(HasUnsavedChanges));
|
OnPropertyChanged(nameof(HasUnsavedChanges));
|
||||||
OnPropertyChanged(nameof(DaqValidationMessage));
|
OnPropertyChanged(nameof(DaqValidationMessage));
|
||||||
if (logSaved)
|
if (logSaved)
|
||||||
@ -602,10 +585,7 @@ public partial class SettingsViewModel : ObservableObject
|
|||||||
Terminator = source.Terminator,
|
Terminator = source.Terminator,
|
||||||
IdnCommand = source.IdnCommand,
|
IdnCommand = source.IdnCommand,
|
||||||
VoltageCommand = source.VoltageCommand,
|
VoltageCommand = source.VoltageCommand,
|
||||||
CurrentCommand = source.CurrentCommand,
|
CurrentCommand = source.CurrentCommand
|
||||||
SetVoltageCommand = source.SetVoltageCommand,
|
|
||||||
SetCurrentLimitCommand = source.SetCurrentLimitCommand,
|
|
||||||
ErrorCommand = source.ErrorCommand
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private static LoadOptions CloneLoadOptions(LoadOptions source) => new()
|
private static LoadOptions CloneLoadOptions(LoadOptions source) => new()
|
||||||
@ -620,11 +600,7 @@ public partial class SettingsViewModel : ObservableObject
|
|||||||
Terminator = source.Terminator,
|
Terminator = source.Terminator,
|
||||||
Channel = source.Channel,
|
Channel = source.Channel,
|
||||||
UseFetch = source.UseFetch,
|
UseFetch = source.UseFetch,
|
||||||
QueryMode = source.QueryMode,
|
QueryMode = source.QueryMode
|
||||||
SetModeCommand = source.SetModeCommand,
|
|
||||||
SetValueCommand = source.SetValueCommand,
|
|
||||||
ResetCommand = source.ResetCommand,
|
|
||||||
ErrorCommand = source.ErrorCommand
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private static DaqOptions CloneDaqOptions(DaqOptions source) => new()
|
private static DaqOptions CloneDaqOptions(DaqOptions source) => new()
|
||||||
@ -716,9 +692,6 @@ public partial class SettingsViewModel : ObservableObject
|
|||||||
target.IdnCommand = source.IdnCommand;
|
target.IdnCommand = source.IdnCommand;
|
||||||
target.VoltageCommand = source.VoltageCommand;
|
target.VoltageCommand = source.VoltageCommand;
|
||||||
target.CurrentCommand = source.CurrentCommand;
|
target.CurrentCommand = source.CurrentCommand;
|
||||||
target.SetVoltageCommand = source.SetVoltageCommand;
|
|
||||||
target.SetCurrentLimitCommand = source.SetCurrentLimitCommand;
|
|
||||||
target.ErrorCommand = source.ErrorCommand;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ApplyToRuntime(LoadOptions source, LoadOptions target)
|
private static void ApplyToRuntime(LoadOptions source, LoadOptions target)
|
||||||
@ -734,10 +707,6 @@ public partial class SettingsViewModel : ObservableObject
|
|||||||
target.Channel = source.Channel;
|
target.Channel = source.Channel;
|
||||||
target.UseFetch = source.UseFetch;
|
target.UseFetch = source.UseFetch;
|
||||||
target.QueryMode = source.QueryMode;
|
target.QueryMode = source.QueryMode;
|
||||||
target.SetModeCommand = source.SetModeCommand;
|
|
||||||
target.SetValueCommand = source.SetValueCommand;
|
|
||||||
target.ResetCommand = source.ResetCommand;
|
|
||||||
target.ErrorCommand = source.ErrorCommand;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ApplyToRuntime(DaqOptions source, DaqOptions target)
|
private static void ApplyToRuntime(DaqOptions source, DaqOptions target)
|
||||||
|
|||||||
@ -8,29 +8,17 @@ public partial class TestHostViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
[ObservableProperty] private Project? _project;
|
[ObservableProperty] private Project? _project;
|
||||||
[ObservableProperty] private TestSessionViewModel? _session;
|
[ObservableProperty] private TestSessionViewModel? _session;
|
||||||
[ObservableProperty] private int _selectedTabIndex;
|
|
||||||
private bool _isViewActive;
|
|
||||||
|
|
||||||
public ExperimentConsoleViewModel Console { get; }
|
|
||||||
public BasicTestViewModel Basic { get; }
|
public BasicTestViewModel Basic { get; }
|
||||||
public CurveTestViewModel Curve { get; }
|
public CurveTestViewModel Curve { get; }
|
||||||
public PowerTestViewModel Power { get; }
|
public PowerTestViewModel Power { get; }
|
||||||
public ProtectViewModel Protect { get; }
|
public ProtectViewModel Protect { get; }
|
||||||
|
|
||||||
public TestHostViewModel(ExperimentConsoleViewModel console, BasicTestViewModel basic, CurveTestViewModel curve, PowerTestViewModel power, ProtectViewModel protect)
|
public TestHostViewModel(BasicTestViewModel basic, CurveTestViewModel curve, PowerTestViewModel power, ProtectViewModel protect)
|
||||||
{
|
{
|
||||||
Console = console;
|
|
||||||
Basic = basic; Curve = curve; Power = power; Protect = protect;
|
Basic = basic; Curve = curve; Power = power; Protect = protect;
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void OnSelectedTabIndexChanged(int value) => UpdateConsoleActive();
|
|
||||||
|
|
||||||
public void SetViewActive(bool active)
|
|
||||||
{
|
|
||||||
_isViewActive = active;
|
|
||||||
UpdateConsoleActive();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetProject(Project p)
|
public void SetProject(Project p)
|
||||||
{
|
{
|
||||||
Project = p;
|
Project = p;
|
||||||
@ -46,12 +34,4 @@ public partial class TestHostViewModel : ObservableObject
|
|||||||
Curve.AttachSession(s);
|
Curve.AttachSession(s);
|
||||||
Protect.AttachSession(s);
|
Protect.AttachSession(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateConsoleActive()
|
|
||||||
{
|
|
||||||
// Identify this host when acquiring/releasing console polling. During a
|
|
||||||
// project switch WPF can load the new host before unloading the old one;
|
|
||||||
// the old host must not stop the shared console on behalf of the new host.
|
|
||||||
Console.SetActive(this, _isViewActive && SelectedTabIndex == 0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
<RowDefinition Height="2*" MinHeight="180" />
|
<RowDefinition Height="2*" MinHeight="180" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<TextBlock Text="开启与关断曲线" FontSize="{StaticResource FsH1}" FontWeight="Bold" Margin="0,0,0,12" />
|
<TextBlock Text="开启与关断曲线(覆盖指标 1.1–1.6)" FontSize="{StaticResource FsH1}" FontWeight="Bold" Margin="0,0,0,12" />
|
||||||
|
|
||||||
<Grid Grid.Row="1">
|
<Grid Grid.Row="1">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
@ -144,17 +144,7 @@
|
|||||||
|
|
||||||
<Border Style="{StaticResource Card}" Margin="0,0,12,0">
|
<Border Style="{StaticResource Card}" Margin="0,0,12,0">
|
||||||
<DockPanel>
|
<DockPanel>
|
||||||
<Grid DockPanel.Dock="Top" Margin="0,0,0,8">
|
<TextBlock DockPanel.Dock="Top" Style="{StaticResource CardTitle}" Text="各通道时间参数" />
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<TextBlock Style="{StaticResource CardTitle}" Text="各通道时间参数" />
|
|
||||||
<Button Grid.Column="1"
|
|
||||||
Content="清空结果"
|
|
||||||
Command="{Binding ClearCommand}"
|
|
||||||
IsEnabled="{Binding Busy, Converter={StaticResource InverseBoolConverter}}" />
|
|
||||||
</Grid>
|
|
||||||
<DataGrid ItemsSource="{Binding Rows}"
|
<DataGrid ItemsSource="{Binding Rows}"
|
||||||
SelectedItem="{Binding SelectedRow, Mode=TwoWay}">
|
SelectedItem="{Binding SelectedRow, Mode=TwoWay}">
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
|
|||||||
@ -1,419 +0,0 @@
|
|||||||
<UserControl x:Class="SSPCTester.UI.Views.ExperimentConsoleView"
|
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:vm="clr-namespace:SSPCTester.UI.ViewModels"
|
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
|
||||||
mc:Ignorable="d"
|
|
||||||
d:DataContext="{d:DesignInstance Type=vm:ExperimentConsoleViewModel}">
|
|
||||||
<UserControl.Resources>
|
|
||||||
<Style x:Key="ConsoleSection" TargetType="Border">
|
|
||||||
<Setter Property="Background" Value="{StaticResource BgSurfaceAltBrush}" />
|
|
||||||
<Setter Property="BorderBrush" Value="{StaticResource BorderSubtleBrush}" />
|
|
||||||
<Setter Property="BorderThickness" Value="1" />
|
|
||||||
<Setter Property="CornerRadius" Value="{StaticResource RadiusMd}" />
|
|
||||||
<Setter Property="Padding" Value="14" />
|
|
||||||
<Setter Property="Margin" Value="0,0,0,12" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style x:Key="MetricCard" TargetType="Border">
|
|
||||||
<Setter Property="Background" Value="{StaticResource BgSurfaceBrush}" />
|
|
||||||
<Setter Property="BorderBrush" Value="{StaticResource BorderSubtleBrush}" />
|
|
||||||
<Setter Property="BorderThickness" Value="1" />
|
|
||||||
<Setter Property="CornerRadius" Value="{StaticResource RadiusMd}" />
|
|
||||||
<Setter Property="Padding" Value="10" />
|
|
||||||
<Setter Property="MinHeight" Value="68" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style x:Key="MetricLabel" TargetType="TextBlock">
|
|
||||||
<Setter Property="Foreground" Value="{StaticResource TextSecondaryBrush}" />
|
|
||||||
<Setter Property="FontSize" Value="{StaticResource FsSmall}" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style x:Key="MetricValue" TargetType="TextBlock">
|
|
||||||
<Setter Property="FontFamily" Value="{StaticResource FontMono}" />
|
|
||||||
<Setter Property="FontSize" Value="20" />
|
|
||||||
<Setter Property="FontWeight" Value="SemiBold" />
|
|
||||||
<Setter Property="Margin" Value="0,7,0,0" />
|
|
||||||
</Style>
|
|
||||||
</UserControl.Resources>
|
|
||||||
|
|
||||||
<Grid Margin="20">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
<ColumnDefinition Width="1.28*" />
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<!-- 电源 -->
|
|
||||||
<Border Style="{StaticResource Card}" Padding="16" Margin="0,0,16,0">
|
|
||||||
<Grid>
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="*" />
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<Grid Margin="0,0,0,14">
|
|
||||||
<TextBlock Text="电源 UDP5080-100" FontSize="{StaticResource FsH2}" FontWeight="SemiBold" />
|
|
||||||
<Border HorizontalAlignment="Right"
|
|
||||||
Style="{StaticResource StatusBadge}"
|
|
||||||
Background="{Binding IsPowerConnected, Converter={StaticResource StatusToSoftBrush}}">
|
|
||||||
<TextBlock Text="{Binding PowerConnectionStatus}"
|
|
||||||
Foreground="{Binding IsPowerConnected, Converter={StaticResource StatusToBrush}}"
|
|
||||||
FontSize="{StaticResource FsSmall}"
|
|
||||||
FontWeight="SemiBold" />
|
|
||||||
</Border>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
|
|
||||||
<StackPanel>
|
|
||||||
<Border Style="{StaticResource ConsoleSection}">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Style="{StaticResource CardTitle}" Text="参数设置" FontSize="{StaticResource FsBody}" />
|
|
||||||
<Grid Margin="0,0,0,8">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="88" />
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
<ColumnDefinition Width="34" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<TextBlock Style="{StaticResource FieldLabel}" Text="电压设定" />
|
|
||||||
<TextBox Grid.Column="1" Text="{Binding PowerVoltageSetpoint, UpdateSourceTrigger=PropertyChanged}" FontFamily="{StaticResource FontMono}" />
|
|
||||||
<TextBlock Grid.Column="2" Text="V" VerticalAlignment="Center" HorizontalAlignment="Center" Foreground="{StaticResource TextSecondaryBrush}" />
|
|
||||||
</Grid>
|
|
||||||
<Grid Margin="0,0,0,8">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="88" />
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
<ColumnDefinition Width="34" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<TextBlock Style="{StaticResource FieldLabel}" Text="电流限值" />
|
|
||||||
<TextBox Grid.Column="1" Text="{Binding PowerCurrentLimit, UpdateSourceTrigger=PropertyChanged}" FontFamily="{StaticResource FontMono}" />
|
|
||||||
<TextBlock Grid.Column="2" Text="A" VerticalAlignment="Center" HorizontalAlignment="Center" Foreground="{StaticResource TextSecondaryBrush}" />
|
|
||||||
</Grid>
|
|
||||||
<Grid Margin="0,0,0,12">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="88" />
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<TextBlock Style="{StaticResource FieldLabel}" Text="预设方案" />
|
|
||||||
<ComboBox Grid.Column="1"
|
|
||||||
ItemsSource="{Binding PowerPresets}"
|
|
||||||
SelectedItem="{Binding SelectedPowerPreset}"
|
|
||||||
DisplayMemberPath="Name" />
|
|
||||||
</Grid>
|
|
||||||
<UniformGrid Columns="3">
|
|
||||||
<Button Style="{StaticResource PrimaryButton}" Content="应用设置" Margin="0,0,6,0" Command="{Binding ApplyPowerCommand}" />
|
|
||||||
<Button Content="保存" Margin="6,0" Command="{Binding SavePowerPresetCommand}" />
|
|
||||||
<Button Content="删除" Margin="6,0,0,0" Command="{Binding DeletePowerPresetCommand}" />
|
|
||||||
</UniformGrid>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<Border Style="{StaticResource ConsoleSection}">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Style="{StaticResource CardTitle}" Text="输出控制" FontSize="{StaticResource FsBody}" />
|
|
||||||
<UniformGrid Columns="3">
|
|
||||||
<Button Style="{StaticResource ToggleOnButton}" Content="输出开" Margin="0,0,6,0" Command="{Binding PowerOutputOnCommand}" IsEnabled="{Binding CanTurnPowerOutputOn}" />
|
|
||||||
<Button Style="{StaticResource ToggleOffButton}" Content="输出关" Margin="6,0" Command="{Binding PowerOutputOffCommand}" IsEnabled="{Binding CanTurnPowerOutputOff}" />
|
|
||||||
<Button Style="{StaticResource DangerButton}" Content="安全复位" Margin="6,0,0,0" Command="{Binding PowerSafeResetCommand}" />
|
|
||||||
</UniformGrid>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<Border Style="{StaticResource ConsoleSection}" Margin="0">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Style="{StaticResource CardTitle}" Text="实时读数" FontSize="{StaticResource FsBody}" />
|
|
||||||
<UniformGrid Columns="2" Margin="0,0,0,10">
|
|
||||||
<Border Style="{StaticResource MetricCard}" Margin="0,0,5,5">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Style="{StaticResource MetricLabel}" Text="实际电压" />
|
|
||||||
<TextBlock Style="{StaticResource MetricValue}">
|
|
||||||
<Run Text="{Binding PowerVoltage, Converter={StaticResource NumberOrDash}, ConverterParameter=F3}" />
|
|
||||||
<Run Text=" V" FontSize="{StaticResource FsSmall}" Foreground="{StaticResource TextSecondaryBrush}" />
|
|
||||||
</TextBlock>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
<Border Style="{StaticResource MetricCard}" Margin="5,0,0,5">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Style="{StaticResource MetricLabel}" Text="实际电流" />
|
|
||||||
<TextBlock Style="{StaticResource MetricValue}">
|
|
||||||
<Run Text="{Binding PowerCurrent, Converter={StaticResource NumberOrDash}, ConverterParameter=F3}" />
|
|
||||||
<Run Text=" A" FontSize="{StaticResource FsSmall}" Foreground="{StaticResource TextSecondaryBrush}" />
|
|
||||||
</TextBlock>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
<Border Style="{StaticResource MetricCard}" Margin="0,5,5,0">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Style="{StaticResource MetricLabel}" Text="实际功率" />
|
|
||||||
<TextBlock Style="{StaticResource MetricValue}">
|
|
||||||
<Run Text="{Binding PowerWatts, Converter={StaticResource NumberOrDash}, ConverterParameter=F3}" />
|
|
||||||
<Run Text=" W" FontSize="{StaticResource FsSmall}" Foreground="{StaticResource TextSecondaryBrush}" />
|
|
||||||
</TextBlock>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
<Border Style="{StaticResource MetricCard}" Margin="5,5,0,0">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Style="{StaticResource MetricLabel}" Text="工作模式" />
|
|
||||||
<TextBlock Style="{StaticResource MetricValue}" Text="{Binding PowerModeText}" />
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
</UniformGrid>
|
|
||||||
<Grid Margin="0,2">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<TextBlock Text="输出状态" Foreground="{StaticResource TextSecondaryBrush}" />
|
|
||||||
<Border Grid.Column="1" Style="{StaticResource StatusBadge}" Background="{StaticResource NeutralSoftBrush}">
|
|
||||||
<TextBlock Text="{Binding PowerOutputStateText}" FontWeight="SemiBold" FontSize="{StaticResource FsSmall}" />
|
|
||||||
</Border>
|
|
||||||
</Grid>
|
|
||||||
<Grid Margin="0,8,0,0">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<TextBlock Text="异常状态" Foreground="{StaticResource TextSecondaryBrush}" />
|
|
||||||
<Border Grid.Column="1" Style="{StaticResource StatusBadge}" Background="{StaticResource NeutralSoftBrush}">
|
|
||||||
<TextBlock Text="{Binding PowerProtectionStateText}" FontWeight="SemiBold" FontSize="{StaticResource FsSmall}" />
|
|
||||||
</Border>
|
|
||||||
</Grid>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
</StackPanel>
|
|
||||||
</ScrollViewer>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- 继电器 -->
|
|
||||||
<Border Grid.Column="1" Style="{StaticResource Card}" Padding="16" Margin="0,0,16,0">
|
|
||||||
<Grid>
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="*" />
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<Grid Margin="0,0,0,14">
|
|
||||||
<TextBlock Text="继电器 / 24路通道" FontSize="{StaticResource FsH2}" FontWeight="SemiBold" />
|
|
||||||
<Border HorizontalAlignment="Right"
|
|
||||||
Style="{StaticResource StatusBadge}"
|
|
||||||
Background="{Binding IsRelayConnected, Converter={StaticResource StatusToSoftBrush}}">
|
|
||||||
<TextBlock Text="{Binding RelayConnectionStatus}"
|
|
||||||
Foreground="{Binding IsRelayConnected, Converter={StaticResource StatusToBrush}}"
|
|
||||||
FontSize="{StaticResource FsSmall}"
|
|
||||||
FontWeight="SemiBold" />
|
|
||||||
</Border>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Border Grid.Row="1" Style="{StaticResource ConsoleSection}">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Style="{StaticResource CardTitle}" Text="批量控制" FontSize="{StaticResource FsBody}" />
|
|
||||||
<UniformGrid Columns="3">
|
|
||||||
<Button Style="{StaticResource ToggleOnButton}" Content="全开" Margin="0,0,6,0" Command="{Binding RelayAllOnCommand}" />
|
|
||||||
<Button Style="{StaticResource ToggleOffButton}" Content="全关" Margin="6,0" Command="{Binding RelayAllOffCommand}" />
|
|
||||||
<Button Style="{StaticResource PrimaryButton}" Content="读取全部" Margin="6,0,0,0" Command="{Binding ReadRelayAllCommand}" />
|
|
||||||
</UniformGrid>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<Grid Grid.Row="2" MinHeight="0">
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="*" />
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
<TextBlock Text="通道控制与读数" FontWeight="SemiBold" Margin="0,0,0,8" />
|
|
||||||
<DataGrid Grid.Row="1" ItemsSource="{Binding Channels}" CanUserAddRows="False">
|
|
||||||
<DataGrid.Columns>
|
|
||||||
<DataGridTextColumn Header="通道" Binding="{Binding ChannelText}" Width="76">
|
|
||||||
<DataGridTextColumn.ElementStyle>
|
|
||||||
<Style TargetType="TextBlock">
|
|
||||||
<Setter Property="FontWeight" Value="SemiBold" />
|
|
||||||
<Setter Property="HorizontalAlignment" Value="Center" />
|
|
||||||
<Setter Property="VerticalAlignment" Value="Center" />
|
|
||||||
</Style>
|
|
||||||
</DataGridTextColumn.ElementStyle>
|
|
||||||
</DataGridTextColumn>
|
|
||||||
<DataGridTextColumn Header="电压" Width="*">
|
|
||||||
<DataGridTextColumn.Binding>
|
|
||||||
<Binding Path="Voltage" Converter="{StaticResource NumberOrDash}" ConverterParameter="F3" StringFormat="{}{0} V" />
|
|
||||||
</DataGridTextColumn.Binding>
|
|
||||||
<DataGridTextColumn.ElementStyle>
|
|
||||||
<Style TargetType="TextBlock" BasedOn="{StaticResource MonoText}" />
|
|
||||||
</DataGridTextColumn.ElementStyle>
|
|
||||||
</DataGridTextColumn>
|
|
||||||
<DataGridTextColumn Header="电流" Width="*">
|
|
||||||
<DataGridTextColumn.Binding>
|
|
||||||
<Binding Path="Current" Converter="{StaticResource NumberOrDash}" ConverterParameter="F3" StringFormat="{}{0} A" />
|
|
||||||
</DataGridTextColumn.Binding>
|
|
||||||
<DataGridTextColumn.ElementStyle>
|
|
||||||
<Style TargetType="TextBlock" BasedOn="{StaticResource MonoText}" />
|
|
||||||
</DataGridTextColumn.ElementStyle>
|
|
||||||
</DataGridTextColumn>
|
|
||||||
<DataGridTemplateColumn Header="开" Width="64">
|
|
||||||
<DataGridTemplateColumn.CellTemplate>
|
|
||||||
<DataTemplate>
|
|
||||||
<Button Style="{StaticResource ToggleOnButton}"
|
|
||||||
Content="开"
|
|
||||||
Command="{Binding DataContext.TurnRelayOnCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
|
||||||
CommandParameter="{Binding}" />
|
|
||||||
</DataTemplate>
|
|
||||||
</DataGridTemplateColumn.CellTemplate>
|
|
||||||
</DataGridTemplateColumn>
|
|
||||||
<DataGridTemplateColumn Header="关" Width="64">
|
|
||||||
<DataGridTemplateColumn.CellTemplate>
|
|
||||||
<DataTemplate>
|
|
||||||
<Button Style="{StaticResource ToggleOffButton}"
|
|
||||||
Content="关"
|
|
||||||
Command="{Binding DataContext.TurnRelayOffCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
|
||||||
CommandParameter="{Binding}" />
|
|
||||||
</DataTemplate>
|
|
||||||
</DataGridTemplateColumn.CellTemplate>
|
|
||||||
</DataGridTemplateColumn>
|
|
||||||
</DataGrid.Columns>
|
|
||||||
</DataGrid>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- 负载 -->
|
|
||||||
<Border Grid.Column="2" Style="{StaticResource Card}" Padding="16">
|
|
||||||
<Grid>
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="*" />
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<Grid Margin="0,0,0,14">
|
|
||||||
<TextBlock Text="负载 IT8702P" FontSize="{StaticResource FsH2}" FontWeight="SemiBold" />
|
|
||||||
<Border HorizontalAlignment="Right"
|
|
||||||
Style="{StaticResource StatusBadge}"
|
|
||||||
Background="{Binding IsLoadConnected, Converter={StaticResource StatusToSoftBrush}}">
|
|
||||||
<TextBlock Text="{Binding LoadConnectionStatus}"
|
|
||||||
Foreground="{Binding IsLoadConnected, Converter={StaticResource StatusToBrush}}"
|
|
||||||
FontSize="{StaticResource FsSmall}"
|
|
||||||
FontWeight="SemiBold" />
|
|
||||||
</Border>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
|
|
||||||
<StackPanel>
|
|
||||||
<Border Style="{StaticResource ConsoleSection}">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Style="{StaticResource CardTitle}" Text="参数设置" FontSize="{StaticResource FsBody}" />
|
|
||||||
<Grid Margin="0,0,0,8">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="88" />
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<TextBlock Style="{StaticResource FieldLabel}" Text="工作模式" />
|
|
||||||
<Border Grid.Column="1" Background="{StaticResource PrimarySoftBrush}" CornerRadius="{StaticResource RadiusMd}" Height="{StaticResource ControlHeight}">
|
|
||||||
<TextBlock Text="CC 恒流" VerticalAlignment="Center" Margin="10,0" Foreground="{StaticResource PrimaryBrush}" FontWeight="SemiBold" />
|
|
||||||
</Border>
|
|
||||||
</Grid>
|
|
||||||
<Grid Margin="0,0,0,8">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="88" />
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
<ColumnDefinition Width="34" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<TextBlock Style="{StaticResource FieldLabel}" Text="负载电流" />
|
|
||||||
<TextBox Grid.Column="1" Text="{Binding LoadCurrentSetpoint, UpdateSourceTrigger=PropertyChanged}" FontFamily="{StaticResource FontMono}" />
|
|
||||||
<TextBlock Grid.Column="2" Text="A" VerticalAlignment="Center" HorizontalAlignment="Center" Foreground="{StaticResource TextSecondaryBrush}" />
|
|
||||||
</Grid>
|
|
||||||
<Grid Margin="0,0,0,12">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="88" />
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<TextBlock Style="{StaticResource FieldLabel}" Text="预设方案" />
|
|
||||||
<ComboBox Grid.Column="1"
|
|
||||||
ItemsSource="{Binding LoadPresets}"
|
|
||||||
SelectedItem="{Binding SelectedLoadPreset}"
|
|
||||||
DisplayMemberPath="Name" />
|
|
||||||
</Grid>
|
|
||||||
<UniformGrid Columns="3">
|
|
||||||
<Button Style="{StaticResource PrimaryButton}" Content="应用设置" Margin="0,0,6,0" Command="{Binding ApplyLoadCommand}" />
|
|
||||||
<Button Content="保存" Margin="6,0" Command="{Binding SaveLoadPresetCommand}" />
|
|
||||||
<Button Content="删除" Margin="6,0,0,0" Command="{Binding DeleteLoadPresetCommand}" />
|
|
||||||
</UniformGrid>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<Border Style="{StaticResource ConsoleSection}">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Style="{StaticResource CardTitle}" Text="输入控制" FontSize="{StaticResource FsBody}" />
|
|
||||||
<UniformGrid Columns="3">
|
|
||||||
<Button Style="{StaticResource ToggleOnButton}" Content="输入开" Margin="0,0,6,0" Command="{Binding LoadInputOnCommand}" IsEnabled="{Binding CanTurnLoadInputOn}" />
|
|
||||||
<Button Style="{StaticResource ToggleOffButton}" Content="输入关" Margin="6,0" Command="{Binding LoadInputOffCommand}" IsEnabled="{Binding CanTurnLoadInputOff}" />
|
|
||||||
<Button Style="{StaticResource DangerButton}" Content="安全复位" Margin="6,0,0,0" Command="{Binding LoadSafeResetCommand}" />
|
|
||||||
</UniformGrid>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<Border Style="{StaticResource ConsoleSection}" Margin="0">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Style="{StaticResource CardTitle}" Text="实时读数" FontSize="{StaticResource FsBody}" />
|
|
||||||
<UniformGrid Columns="2" Margin="0,0,0,10">
|
|
||||||
<Border Style="{StaticResource MetricCard}" Margin="0,0,5,5">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Style="{StaticResource MetricLabel}" Text="实际电压" />
|
|
||||||
<TextBlock Style="{StaticResource MetricValue}">
|
|
||||||
<Run Text="{Binding LoadVoltage, Converter={StaticResource NumberOrDash}, ConverterParameter=F3}" />
|
|
||||||
<Run Text=" V" FontSize="{StaticResource FsSmall}" Foreground="{StaticResource TextSecondaryBrush}" />
|
|
||||||
</TextBlock>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
<Border Style="{StaticResource MetricCard}" Margin="5,0,0,5">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Style="{StaticResource MetricLabel}" Text="实际电流" />
|
|
||||||
<TextBlock Style="{StaticResource MetricValue}">
|
|
||||||
<Run Text="{Binding LoadCurrent, Converter={StaticResource NumberOrDash}, ConverterParameter=F3}" />
|
|
||||||
<Run Text=" A" FontSize="{StaticResource FsSmall}" Foreground="{StaticResource TextSecondaryBrush}" />
|
|
||||||
</TextBlock>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
<Border Style="{StaticResource MetricCard}" Margin="0,5,5,0">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Style="{StaticResource MetricLabel}" Text="实际功率" />
|
|
||||||
<TextBlock Style="{StaticResource MetricValue}">
|
|
||||||
<Run Text="{Binding LoadWatts, Converter={StaticResource NumberOrDash}, ConverterParameter=F3}" />
|
|
||||||
<Run Text=" W" FontSize="{StaticResource FsSmall}" Foreground="{StaticResource TextSecondaryBrush}" />
|
|
||||||
</TextBlock>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
<Border Style="{StaticResource MetricCard}" Margin="5,5,0,0">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Style="{StaticResource MetricLabel}" Text="工作模式" />
|
|
||||||
<TextBlock Style="{StaticResource MetricValue}" Text="CC" />
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
</UniformGrid>
|
|
||||||
<Grid Margin="0,2">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<TextBlock Text="输入状态" Foreground="{StaticResource TextSecondaryBrush}" />
|
|
||||||
<Border Grid.Column="1" Style="{StaticResource StatusBadge}" Background="{StaticResource NeutralSoftBrush}">
|
|
||||||
<TextBlock Text="{Binding LoadInputStateText}" FontWeight="SemiBold" FontSize="{StaticResource FsSmall}" />
|
|
||||||
</Border>
|
|
||||||
</Grid>
|
|
||||||
<Grid Margin="0,8,0,0">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<TextBlock Text="异常状态" Foreground="{StaticResource TextSecondaryBrush}" />
|
|
||||||
<Border Grid.Column="1" Style="{StaticResource StatusBadge}" Background="{StaticResource NeutralSoftBrush}">
|
|
||||||
<TextBlock Text="{Binding LoadProtectionStateText}" FontWeight="SemiBold" FontSize="{StaticResource FsSmall}" />
|
|
||||||
</Border>
|
|
||||||
</Grid>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
</StackPanel>
|
|
||||||
</ScrollViewer>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
</Grid>
|
|
||||||
</UserControl>
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
using System.Windows.Controls;
|
|
||||||
|
|
||||||
namespace SSPCTester.UI.Views;
|
|
||||||
|
|
||||||
public partial class ExperimentConsoleView : UserControl
|
|
||||||
{
|
|
||||||
public ExperimentConsoleView() { InitializeComponent(); }
|
|
||||||
}
|
|
||||||
@ -13,7 +13,7 @@
|
|||||||
<RowDefinition Height="3*" MinHeight="220" />
|
<RowDefinition Height="3*" MinHeight="220" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<TextBlock Text="功率损耗" FontSize="{StaticResource FsH1}" FontWeight="Bold" Margin="0,0,0,12" />
|
<TextBlock Text="功率损耗(采集输入/输出两端数据)" FontSize="{StaticResource FsH1}" FontWeight="Bold" Margin="0,0,0,12" />
|
||||||
|
|
||||||
<Grid Grid.Row="1">
|
<Grid Grid.Row="1">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
@ -30,6 +30,14 @@
|
|||||||
<TextBox Text="{Binding SamplesPerChannel}" Margin="0,0,0,12" FontFamily="{StaticResource FontMono}" />
|
<TextBox Text="{Binding SamplesPerChannel}" Margin="0,0,0,12" FontFamily="{StaticResource FontMono}" />
|
||||||
<TextBlock Style="{StaticResource FieldLabel}" Text="采集间隔 (ms)" />
|
<TextBlock Style="{StaticResource FieldLabel}" Text="采集间隔 (ms)" />
|
||||||
<TextBox Text="{Binding IntervalMs}" Margin="0,0,0,12" FontFamily="{StaticResource FontMono}" />
|
<TextBox Text="{Binding IntervalMs}" Margin="0,0,0,12" FontFamily="{StaticResource FontMono}" />
|
||||||
|
<UniformGrid Columns="2" Margin="0,0,0,0">
|
||||||
|
<Button Content="开启当前通道" Margin="0,0,6,0"
|
||||||
|
Command="{Binding TurnOnCurrentChannelCommand}"
|
||||||
|
IsEnabled="{Binding CanOperate}" />
|
||||||
|
<Button Content="关断当前通道" Margin="6,0,0,0"
|
||||||
|
Command="{Binding TurnOffCurrentChannelCommand}"
|
||||||
|
IsEnabled="{Binding CanOperate}" />
|
||||||
|
</UniformGrid>
|
||||||
<Button Style="{StaticResource PrimaryButton}" Content="测量当前通道" Margin="0,10,0,0"
|
<Button Style="{StaticResource PrimaryButton}" Content="测量当前通道" Margin="0,10,0,0"
|
||||||
Command="{Binding MeasureCurrentCommand}"
|
Command="{Binding MeasureCurrentCommand}"
|
||||||
IsEnabled="{Binding CanOperate}" />
|
IsEnabled="{Binding CanOperate}" />
|
||||||
@ -170,19 +178,8 @@
|
|||||||
|
|
||||||
<Border Style="{StaticResource Card}" Margin="0,0,12,0">
|
<Border Style="{StaticResource Card}" Margin="0,0,12,0">
|
||||||
<DockPanel>
|
<DockPanel>
|
||||||
<Grid DockPanel.Dock="Top" Margin="0,0,0,8">
|
<TextBlock DockPanel.Dock="Top" Style="{StaticResource CardTitle}" Text="各通道汇总" />
|
||||||
<Grid.ColumnDefinitions>
|
<DataGrid ItemsSource="{Binding Rows}">
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<TextBlock Style="{StaticResource CardTitle}" Text="各通道汇总" />
|
|
||||||
<Button Grid.Column="1"
|
|
||||||
Content="清空结果"
|
|
||||||
Command="{Binding ClearCommand}"
|
|
||||||
IsEnabled="{Binding CanOperate}" />
|
|
||||||
</Grid>
|
|
||||||
<DataGrid ItemsSource="{Binding Rows}"
|
|
||||||
SelectedItem="{Binding Current, Mode=TwoWay}">
|
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
<DataGridTextColumn Header="通道" Width="60">
|
<DataGridTextColumn Header="通道" Width="60">
|
||||||
<DataGridTextColumn.Binding>
|
<DataGridTextColumn.Binding>
|
||||||
@ -205,14 +202,6 @@
|
|||||||
<Style TargetType="TextBlock" BasedOn="{StaticResource MonoText}" />
|
<Style TargetType="TextBlock" BasedOn="{StaticResource MonoText}" />
|
||||||
</DataGridTextColumn.ElementStyle>
|
</DataGridTextColumn.ElementStyle>
|
||||||
</DataGridTextColumn>
|
</DataGridTextColumn>
|
||||||
<DataGridTextColumn Header="电源功率(W)" Width="*">
|
|
||||||
<DataGridTextColumn.Binding>
|
|
||||||
<Binding Path="PowerIn" Converter="{StaticResource NumberOrDash}" ConverterParameter="F3" />
|
|
||||||
</DataGridTextColumn.Binding>
|
|
||||||
<DataGridTextColumn.ElementStyle>
|
|
||||||
<Style TargetType="TextBlock" BasedOn="{StaticResource MonoText}" />
|
|
||||||
</DataGridTextColumn.ElementStyle>
|
|
||||||
</DataGridTextColumn>
|
|
||||||
<DataGridTextColumn Header="负载电压(V)" Width="*">
|
<DataGridTextColumn Header="负载电压(V)" Width="*">
|
||||||
<DataGridTextColumn.Binding>
|
<DataGridTextColumn.Binding>
|
||||||
<Binding Path="Vout" Converter="{StaticResource NumberOrDash}" ConverterParameter="F3" />
|
<Binding Path="Vout" Converter="{StaticResource NumberOrDash}" ConverterParameter="F3" />
|
||||||
@ -229,14 +218,6 @@
|
|||||||
<Style TargetType="TextBlock" BasedOn="{StaticResource MonoText}" />
|
<Style TargetType="TextBlock" BasedOn="{StaticResource MonoText}" />
|
||||||
</DataGridTextColumn.ElementStyle>
|
</DataGridTextColumn.ElementStyle>
|
||||||
</DataGridTextColumn>
|
</DataGridTextColumn>
|
||||||
<DataGridTextColumn Header="负载功率(W)" Width="*">
|
|
||||||
<DataGridTextColumn.Binding>
|
|
||||||
<Binding Path="PowerOut" Converter="{StaticResource NumberOrDash}" ConverterParameter="F3" />
|
|
||||||
</DataGridTextColumn.Binding>
|
|
||||||
<DataGridTextColumn.ElementStyle>
|
|
||||||
<Style TargetType="TextBlock" BasedOn="{StaticResource MonoText}" />
|
|
||||||
</DataGridTextColumn.ElementStyle>
|
|
||||||
</DataGridTextColumn>
|
|
||||||
<DataGridTextColumn Header="电压降(V)" Width="*">
|
<DataGridTextColumn Header="电压降(V)" Width="*">
|
||||||
<DataGridTextColumn.Binding>
|
<DataGridTextColumn.Binding>
|
||||||
<Binding Path="VoltageDrop" Converter="{StaticResource NumberOrDash}" ConverterParameter="F3" />
|
<Binding Path="VoltageDrop" Converter="{StaticResource NumberOrDash}" ConverterParameter="F3" />
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TextBlock Text="{Binding Project.Name, StringFormat='项目:{0}'}" FontSize="{StaticResource FsH1}" FontWeight="Bold" />
|
<TextBlock Text="{Binding Project.Name, StringFormat='项目:{0}'}" FontSize="{StaticResource FsH1}" FontWeight="Bold" />
|
||||||
|
<TextBlock Text="填写被测件信息并选择测试项。" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,4,0,0" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
<RowDefinition Height="2*" MinHeight="200" />
|
<RowDefinition Height="2*" MinHeight="200" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<TextBlock Text="保护功能" FontSize="{StaticResource FsH1}" FontWeight="Bold" Margin="0,0,0,8" />
|
<TextBlock Text="保护功能(覆盖指标 11/14/15/17/19/21/22/23)" FontSize="{StaticResource FsH1}" FontWeight="Bold" Margin="0,0,0,8" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -18,10 +18,7 @@
|
|||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<TextBlock FontSize="{StaticResource FsH1}" FontWeight="Bold">
|
<TextBlock Text="测试报告" FontSize="{StaticResource FsH1}" FontWeight="Bold" />
|
||||||
<Run Text="测试报告:" />
|
|
||||||
<Run Text="{Binding Project.Name, Mode=OneWay}" />
|
|
||||||
</TextBlock>
|
|
||||||
<StackPanel Grid.Column="1" Orientation="Horizontal">
|
<StackPanel Grid.Column="1" Orientation="Horizontal">
|
||||||
<ComboBox Width="130"
|
<ComboBox Width="130"
|
||||||
Margin="0,0,8,0"
|
Margin="0,0,8,0"
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="34" />
|
<RowDefinition Height="34" />
|
||||||
<RowDefinition Height="252" />
|
<RowDefinition Height="252" />
|
||||||
<RowDefinition Height="430" />
|
<RowDefinition Height="320" />
|
||||||
<RowDefinition Height="90" />
|
<RowDefinition Height="90" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
@ -171,15 +171,13 @@
|
|||||||
<Grid>
|
<Grid>
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="40" />
|
<RowDefinition Height="32" />
|
||||||
<RowDefinition Height="28" />
|
|
||||||
<RowDefinition Height="24" />
|
<RowDefinition Height="24" />
|
||||||
<RowDefinition Height="*" />
|
<RowDefinition Height="*" />
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<TextBlock Grid.Row="0" Style="{StaticResource CardTitle}" Margin="0,0,0,6" Text="采集参数和通道角色" />
|
<TextBlock Grid.Row="0" Style="{StaticResource CardTitle}" Margin="0,0,0,6" Text="采集参数和通道角色" />
|
||||||
|
|
||||||
<Grid Grid.Row="1" Margin="0,0,0,8" VerticalAlignment="Center">
|
<Grid Grid.Row="1" Margin="0,0,0,3">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="70" />
|
<ColumnDefinition Width="70" />
|
||||||
<ColumnDefinition Width="90" />
|
<ColumnDefinition Width="90" />
|
||||||
@ -208,30 +206,10 @@
|
|||||||
|
|
||||||
<TextBlock Grid.Row="2" Text="{Binding DaqValidationMessage}" Foreground="{StaticResource TextSecondaryBrush}" VerticalAlignment="Center" />
|
<TextBlock Grid.Row="2" Text="{Binding DaqValidationMessage}" Foreground="{StaticResource TextSecondaryBrush}" VerticalAlignment="Center" />
|
||||||
|
|
||||||
<Grid Grid.Row="3" Margin="0,0,0,4">
|
<ItemsControl Grid.Row="3" ItemsSource="{Binding DaqChannelEditors}">
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="52" />
|
|
||||||
<ColumnDefinition Width="64" />
|
|
||||||
<ColumnDefinition Width="170" />
|
|
||||||
<ColumnDefinition Width="170" />
|
|
||||||
<ColumnDefinition Width="120" />
|
|
||||||
<ColumnDefinition Width="120" />
|
|
||||||
<ColumnDefinition Width="110" />
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<TextBlock Text="通道" Foreground="{StaticResource TextMutedBrush}" FontSize="{StaticResource FsSmall}" VerticalAlignment="Center" />
|
|
||||||
<TextBlock Grid.Column="1" Text="启用" Foreground="{StaticResource TextMutedBrush}" FontSize="{StaticResource FsSmall}" VerticalAlignment="Center" />
|
|
||||||
<TextBlock Grid.Column="2" Text="角色" Foreground="{StaticResource TextMutedBrush}" FontSize="{StaticResource FsSmall}" VerticalAlignment="Center" />
|
|
||||||
<TextBlock Grid.Column="3" Margin="8,0,0,0" Text="名称" Foreground="{StaticResource TextMutedBrush}" FontSize="{StaticResource FsSmall}" VerticalAlignment="Center" />
|
|
||||||
<TextBlock Grid.Column="4" Margin="8,0,0,0" Text="Scale" Foreground="{StaticResource TextMutedBrush}" FontSize="{StaticResource FsSmall}" VerticalAlignment="Center" />
|
|
||||||
<TextBlock Grid.Column="5" Margin="8,0,0,0" Text="Offset" Foreground="{StaticResource TextMutedBrush}" FontSize="{StaticResource FsSmall}" VerticalAlignment="Center" />
|
|
||||||
<TextBlock Grid.Column="6" Margin="10,0,0,0" Text="标定" Foreground="{StaticResource TextMutedBrush}" FontSize="{StaticResource FsSmall}" VerticalAlignment="Center" />
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<ItemsControl Grid.Row="4" ItemsSource="{Binding DaqChannelEditors}">
|
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<Grid Height="34" Margin="0,0,0,2">
|
<Grid Height="30" Margin="0">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="52" />
|
<ColumnDefinition Width="52" />
|
||||||
<ColumnDefinition Width="64" />
|
<ColumnDefinition Width="64" />
|
||||||
@ -254,12 +232,6 @@
|
|||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ItemsControl.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
|
|
||||||
<TextBlock Grid.Row="5"
|
|
||||||
Margin="0,10,0,0"
|
|
||||||
Foreground="{StaticResource TextSecondaryBrush}"
|
|
||||||
TextWrapping="Wrap"
|
|
||||||
Text="Scale 和 Offset 用于把采集卡输入电压换算为工程值:工程值 = 采集卡电压 × Scale + Offset。若前端有分压或传感器,请按实际比例填写 Scale,并用 Offset 修正零点偏移;曲线测试使用的 OutputVoltage 通道必须勾选已标定。" />
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
@ -325,7 +297,7 @@
|
|||||||
<StackPanel Grid.Column="1" Orientation="Horizontal">
|
<StackPanel Grid.Column="1" Orientation="Horizontal">
|
||||||
<Button Content="刷新串口" Command="{Binding RefreshPortsCommand}" Margin="0,0,8,0" />
|
<Button Content="刷新串口" Command="{Binding RefreshPortsCommand}" Margin="0,0,8,0" />
|
||||||
<Button Content="重新连接" Command="{Binding ReconnectAllCommand}" Margin="0,0,8,0" />
|
<Button Content="重新连接" Command="{Binding ReconnectAllCommand}" Margin="0,0,8,0" />
|
||||||
<Button Style="{StaticResource PrimaryButton}" Content="保存设置" Command="{Binding SaveSettingsCommand}" Click="SaveSettingsClicked" />
|
<Button Style="{StaticResource PrimaryButton}" Content="保存设置" Command="{Binding SaveSettingsCommand}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@ -1,9 +1,6 @@
|
|||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
using System.Windows.Controls.Primitives;
|
using System.Windows.Controls.Primitives;
|
||||||
using System.Windows.Data;
|
|
||||||
using System.Windows.Input;
|
|
||||||
using System.Windows.Media;
|
|
||||||
|
|
||||||
namespace SSPCTester.UI.Views;
|
namespace SSPCTester.UI.Views;
|
||||||
|
|
||||||
@ -11,14 +8,6 @@ public partial class SettingsManualView : UserControl
|
|||||||
{
|
{
|
||||||
public SettingsManualView() => InitializeComponent();
|
public SettingsManualView() => InitializeComponent();
|
||||||
|
|
||||||
private void SaveSettingsClicked(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (!IsLoaded) return;
|
|
||||||
CommitAllBindings();
|
|
||||||
if (DataContext is ViewModels.SettingsViewModel vm)
|
|
||||||
vm.NotifyDraftChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DraftChanged(object sender, RoutedEventArgs e)
|
private void DraftChanged(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (!IsLoaded) return;
|
if (!IsLoaded) return;
|
||||||
@ -44,54 +33,4 @@ public partial class SettingsManualView : UserControl
|
|||||||
toggleButton.GetBindingExpression(ToggleButton.IsCheckedProperty)?.UpdateSource();
|
toggleButton.GetBindingExpression(ToggleButton.IsCheckedProperty)?.UpdateSource();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CommitAllBindings()
|
|
||||||
{
|
|
||||||
// Ensure the currently focused editor pushes its latest value first.
|
|
||||||
if (Keyboard.FocusedElement is FrameworkElement focused)
|
|
||||||
UpdateFrameworkElementBindings(focused);
|
|
||||||
|
|
||||||
foreach (var element in EnumerateVisuals(this).OfType<FrameworkElement>())
|
|
||||||
UpdateFrameworkElementBindings(element);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IEnumerable<DependencyObject> EnumerateVisuals(DependencyObject root)
|
|
||||||
{
|
|
||||||
int count = VisualTreeHelper.GetChildrenCount(root);
|
|
||||||
for (int i = 0; i < count; i++)
|
|
||||||
{
|
|
||||||
var child = VisualTreeHelper.GetChild(root, i);
|
|
||||||
yield return child;
|
|
||||||
foreach (var nested in EnumerateVisuals(child))
|
|
||||||
yield return nested;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void UpdateFrameworkElementBindings(FrameworkElement element)
|
|
||||||
{
|
|
||||||
switch (element)
|
|
||||||
{
|
|
||||||
case TextBox textBox:
|
|
||||||
textBox.GetBindingExpression(TextBox.TextProperty)?.UpdateSource();
|
|
||||||
break;
|
|
||||||
case ComboBox comboBox:
|
|
||||||
comboBox.GetBindingExpression(Selector.SelectedValueProperty)?.UpdateSource();
|
|
||||||
comboBox.GetBindingExpression(Selector.SelectedItemProperty)?.UpdateSource();
|
|
||||||
comboBox.GetBindingExpression(ComboBox.TextProperty)?.UpdateSource();
|
|
||||||
break;
|
|
||||||
case ToggleButton toggleButton:
|
|
||||||
toggleButton.GetBindingExpression(ToggleButton.IsCheckedProperty)?.UpdateSource();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
if (element is Selector selector)
|
|
||||||
{
|
|
||||||
selector.GetBindingExpression(Selector.SelectedValueProperty)?.UpdateSource();
|
|
||||||
selector.GetBindingExpression(Selector.SelectedItemProperty)?.UpdateSource();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (element.BindingGroup != null)
|
|
||||||
element.BindingGroup.UpdateSources();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,16 +4,10 @@
|
|||||||
xmlns:vm="clr-namespace:SSPCTester.UI.ViewModels"
|
xmlns:vm="clr-namespace:SSPCTester.UI.ViewModels"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
Loaded="OnLoaded"
|
|
||||||
Unloaded="OnUnloaded"
|
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
d:DataContext="{d:DesignInstance Type=vm:TestHostViewModel}">
|
d:DataContext="{d:DesignInstance Type=vm:TestHostViewModel}">
|
||||||
<Border Background="{StaticResource BgSurfaceBrush}" BorderBrush="{StaticResource BorderSubtleBrush}" BorderThickness="0,0,0,1">
|
<Border Background="{StaticResource BgSurfaceBrush}" BorderBrush="{StaticResource BorderSubtleBrush}" BorderThickness="0,0,0,1">
|
||||||
<Grid>
|
<TabControl x:Name="Tabs">
|
||||||
<TabControl x:Name="Tabs" SelectedIndex="{Binding SelectedTabIndex, Mode=TwoWay}">
|
|
||||||
<TabItem Header="控制台">
|
|
||||||
<ContentControl Content="{Binding Console}" />
|
|
||||||
</TabItem>
|
|
||||||
<TabItem Header="基础测试">
|
<TabItem Header="基础测试">
|
||||||
<ContentControl Content="{Binding Basic}" />
|
<ContentControl Content="{Binding Basic}" />
|
||||||
</TabItem>
|
</TabItem>
|
||||||
@ -27,16 +21,5 @@
|
|||||||
<ContentControl Content="{Binding Protect}" />
|
<ContentControl Content="{Binding Protect}" />
|
||||||
</TabItem>
|
</TabItem>
|
||||||
</TabControl>
|
</TabControl>
|
||||||
|
|
||||||
<TextBlock Text="{Binding Project.Name, StringFormat='当前项目:{0}'}"
|
|
||||||
HorizontalAlignment="Right"
|
|
||||||
VerticalAlignment="Top"
|
|
||||||
Margin="0,10,20,0"
|
|
||||||
Foreground="{StaticResource TextSecondaryBrush}"
|
|
||||||
FontWeight="SemiBold"
|
|
||||||
TextTrimming="CharacterEllipsis"
|
|
||||||
MaxWidth="420"
|
|
||||||
IsHitTestVisible="False" />
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
</Border>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@ -1,21 +1,8 @@
|
|||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
using SSPCTester.UI.ViewModels;
|
|
||||||
|
|
||||||
namespace SSPCTester.UI.Views;
|
namespace SSPCTester.UI.Views;
|
||||||
|
|
||||||
public partial class TestHostView : UserControl
|
public partial class TestHostView : UserControl
|
||||||
{
|
{
|
||||||
public TestHostView() { InitializeComponent(); }
|
public TestHostView() { InitializeComponent(); }
|
||||||
|
|
||||||
private void OnLoaded(object sender, System.Windows.RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (DataContext is TestHostViewModel vm)
|
|
||||||
vm.SetViewActive(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnUnloaded(object sender, System.Windows.RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (DataContext is TestHostViewModel vm)
|
|
||||||
vm.SetViewActive(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,11 +29,7 @@
|
|||||||
"Terminator": "\n",
|
"Terminator": "\n",
|
||||||
"IdnCommand": "*IDN?",
|
"IdnCommand": "*IDN?",
|
||||||
"VoltageCommand": "MEASure:VOLTage?",
|
"VoltageCommand": "MEASure:VOLTage?",
|
||||||
"CurrentCommand": "MEASure:CURRent?",
|
"CurrentCommand": "MEASure:CURRent?"
|
||||||
"SetVoltageCommand": "VOLT {0}",
|
|
||||||
"SetCurrentLimitCommand": "CURR {0}",
|
|
||||||
"OutputStateCommand": "OUTP?",
|
|
||||||
"ErrorCommand": "SYST:ERR?"
|
|
||||||
},
|
},
|
||||||
"Load": {
|
"Load": {
|
||||||
"UseMock": false,
|
"UseMock": false,
|
||||||
@ -46,12 +42,7 @@
|
|||||||
"Terminator": "\n",
|
"Terminator": "\n",
|
||||||
"Channel": 1,
|
"Channel": 1,
|
||||||
"UseFetch": true,
|
"UseFetch": true,
|
||||||
"QueryMode": "Combined",
|
"QueryMode": "Combined"
|
||||||
"SetModeCommand": "",
|
|
||||||
"SetValueCommand": "{0} {1}",
|
|
||||||
"ResetCommand": "*RST",
|
|
||||||
"InputStateCommand": "INP?",
|
|
||||||
"ErrorCommand": "SYST:ERR?"
|
|
||||||
},
|
},
|
||||||
"Daq": {
|
"Daq": {
|
||||||
"UseMock": false,
|
"UseMock": false,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user