A:增加订阅类型选择
This commit is contained in:
1 parent
6ed0d3f1c2
commit
de1a29c522
4 files changed
+144
-20
No files matched your search
@@ -73,6 +73,7 @@
|
|||||||
<GroupBox Header="实时监控 (Add Watch)" Grid.Row="2" Margin="0,0,0,15">
|
<GroupBox Header="实时监控 (Add Watch)" Grid.Row="2" Margin="0,0,0,15">
|
||||||
<StackPanel Orientation="Horizontal" Margin="10">
|
<StackPanel Orientation="Horizontal" Margin="10">
|
||||||
<TextBox hc:InfoElement.Placeholder="地址 (如 DB1.DBW0)" Text="{Binding MonitorAddress}" Width="200" Margin="0,0,10,0"/>
|
<TextBox hc:InfoElement.Placeholder="地址 (如 DB1.DBW0)" Text="{Binding MonitorAddress}" Width="200" Margin="0,0,10,0"/>
|
||||||
|
<ComboBox ItemsSource="{Binding SupportedMonitorTypes}" SelectedItem="{Binding SelectedMonitorType}" Width="120" Margin="0,0,10,0" DisplayMemberPath="Name"/>
|
||||||
<Button Content="添加监控" Command="{Binding AddMonitorCommand}"/>
|
<Button Content="添加监控" Command="{Binding AddMonitorCommand}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</GroupBox>
|
</GroupBox>
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using Plc.Models;
|
||||||
|
using Sharp.Wpf.Mvvm;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Client.ViewModels;
|
||||||
|
|
||||||
|
public class PlcTagViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
private readonly PlcTag _tag;
|
||||||
|
|
||||||
|
public PlcTagViewModel(PlcTag tag)
|
||||||
|
{
|
||||||
|
_tag = tag ?? throw new ArgumentNullException(nameof(tag));
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Id => _tag.Id;
|
||||||
|
|
||||||
|
public string Address => _tag.Address;
|
||||||
|
|
||||||
|
public Type DataType => _tag.DataType;
|
||||||
|
|
||||||
|
public object? Value => _tag.Value;
|
||||||
|
|
||||||
|
public PlcTag Tag => _tag;
|
||||||
|
|
||||||
|
public void UpdateValue()
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(Value));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,6 +31,7 @@ public class PlcViewModel : ViewModelBase
|
|||||||
|
|
||||||
// 监控
|
// 监控
|
||||||
private string _monitorAddress = "DB1.DBW0";
|
private string _monitorAddress = "DB1.DBW0";
|
||||||
|
private Type _selectedMonitorType = typeof(short);
|
||||||
|
|
||||||
public PlcViewModel(
|
public PlcViewModel(
|
||||||
IPlcService plcService,
|
IPlcService plcService,
|
||||||
@@ -116,10 +117,30 @@ public class PlcViewModel : ViewModelBase
|
|||||||
get => _monitorAddress;
|
get => _monitorAddress;
|
||||||
set => SetProperty(ref _monitorAddress, value);
|
set => SetProperty(ref _monitorAddress, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Type SelectedMonitorType
|
||||||
|
{
|
||||||
|
get => _selectedMonitorType;
|
||||||
|
set => SetProperty(ref _selectedMonitorType, value);
|
||||||
|
}
|
||||||
|
|
||||||
// 数据源
|
// 数据源
|
||||||
public ObservableCollection<PlcBrand> Brands { get; } = new(Enum.GetValues<PlcBrand>());
|
public ObservableCollection<PlcBrand> Brands { get; } = new(Enum.GetValues<PlcBrand>());
|
||||||
public ObservableCollection<CpuType> CpuTypes { get; } = new(Enum.GetValues<CpuType>());
|
public ObservableCollection<CpuType> CpuTypes { get; } = new(Enum.GetValues<CpuType>());
|
||||||
|
public ObservableCollection<Type> SupportedMonitorTypes { get; } = new()
|
||||||
|
{
|
||||||
|
typeof(bool),
|
||||||
|
typeof(byte),
|
||||||
|
typeof(short),
|
||||||
|
typeof(ushort),
|
||||||
|
typeof(int),
|
||||||
|
typeof(uint),
|
||||||
|
typeof(long),
|
||||||
|
typeof(ulong),
|
||||||
|
typeof(float),
|
||||||
|
typeof(double),
|
||||||
|
typeof(string)
|
||||||
|
};
|
||||||
public ObservableCollection<PlcTagViewModel> MonitoredTags { get; } = new();
|
public ObservableCollection<PlcTagViewModel> MonitoredTags { get; } = new();
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@@ -206,7 +227,7 @@ public class PlcViewModel : ViewModelBase
|
|||||||
var tag = new PlcTag
|
var tag = new PlcTag
|
||||||
{
|
{
|
||||||
Address = MonitorAddress,
|
Address = MonitorAddress,
|
||||||
DataType = typeof(short)
|
DataType = SelectedMonitorType
|
||||||
};
|
};
|
||||||
|
|
||||||
var vm = new PlcTagViewModel(tag);
|
var vm = new PlcTagViewModel(tag);
|
||||||
|
|||||||
+91
-19
@@ -189,6 +189,96 @@ _plcService.Subscribe(tag);
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## ⚙️ 核心通信优化机制
|
||||||
|
|
||||||
|
本项目采用了**分桶(Batching/Bucketing)**优化策略,这是高性能 PLC 通信的关键。
|
||||||
|
|
||||||
|
### 1. 为什么需要优化?
|
||||||
|
PLC 通信通常基于 TCP/IP 协议,每次读写请求都包含网络握手、协议头封装和 PLC 内部处理时间。如果用户订阅了 100 个离散点位,如果发送 100 次网络请求,延迟将极高(例如 100 * 20ms = 2000ms),无法满足实时监控需求。
|
||||||
|
|
||||||
|
### 2. 优化原理
|
||||||
|
系统通过 `IPlcOptimizer`(如 `SiemensOptimizer`)在内存中动态重组读写请求,将多个小的、地址相邻的请求合并为一个大的网络请求。
|
||||||
|
|
||||||
|
#### 处理流程(伪代码)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A[原始订阅Tags] --> B[1. 分组 Grouping]
|
||||||
|
B --> C[2. 排序 Sorting]
|
||||||
|
C --> D[3. 合并 Merging]
|
||||||
|
D --> E[执行网络请求]
|
||||||
|
```
|
||||||
|
|
||||||
|
1. **分组 (Grouping)**: 根据内存区域(如 DB1, DB2, M区, I区)将 Tag 分组。不同区域无法合并。
|
||||||
|
2. **排序 (Sorting)**: 在同一组内,按字节偏移量(Byte Offset)从小到大排序。
|
||||||
|
3. **合并 (Merging)**:
|
||||||
|
* 遍历排序后的 Tags。
|
||||||
|
* 计算两个 Tag 之间的**间隙 (Gap)**。
|
||||||
|
* 如果 `Gap <= MaxGapBytes` (默认20字节) 且 `TotalLength <= MaxPduSize` (最大包长),则合并。
|
||||||
|
* 否则,创建一个新的**批次 (Batch)**。
|
||||||
|
|
||||||
|
### 3. 实际案例分析
|
||||||
|
|
||||||
|
假设用户订阅了以下点位(西门子 S7):
|
||||||
|
- Tag A: `DB1.DBW0` (2 bytes)
|
||||||
|
- Tag B: `DB1.DBW4` (2 bytes)
|
||||||
|
- Tag C: `DB1.DBW1000` (2 bytes)
|
||||||
|
|
||||||
|
**处理逻辑:**
|
||||||
|
1. **Tag A 和 Tag B**:
|
||||||
|
* Tag A 结束于 byte 2,Tag B 开始于 byte 4。
|
||||||
|
* 间隙 Gap = 4 - 2 = 2 bytes。
|
||||||
|
* 2 bytes < 20 bytes (阈值),且总长未超 PDU。
|
||||||
|
* **结果**: 合并为一个请求:`Start: 0, Length: 6`。虽然读回了无用的 byte 2-3,但节省了一次网络请求。
|
||||||
|
2. **Tag B 和 Tag C**:
|
||||||
|
* Tag B 结束于 byte 6,Tag C 开始于 byte 1000。
|
||||||
|
* 间隙 Gap = 994 bytes。
|
||||||
|
* 994 bytes > 20 bytes。
|
||||||
|
* **结果**: 断开,Tag C 放入新的批次。
|
||||||
|
|
||||||
|
**最终网络请求**: 2 次(而不是 3 次)。
|
||||||
|
|
||||||
|
### 4. 极限情况处理
|
||||||
|
|
||||||
|
* **只订阅 1 个点位**: 优化器生成 1 个批次,包含 1 个 Tag。无额外开销。
|
||||||
|
* **连续订阅 1000 个点位 (例如 DB1.DBW0 - DB1.DBW2000)**:
|
||||||
|
* 总长度 2000 字节。
|
||||||
|
* 假设 PLC PDU 限制为 960 字节。
|
||||||
|
* 优化器会自动切分为 3 个批次:
|
||||||
|
* Batch 1: 0 - 960
|
||||||
|
* Batch 2: 960 - 1920
|
||||||
|
* Batch 3: 1920 - 2000
|
||||||
|
* 系统自动处理分包,上层业务无感。
|
||||||
|
|
||||||
|
### 5. 未来扩展性 (适配欧姆龙/三菱)
|
||||||
|
|
||||||
|
本架构设计之初就考虑了多品牌支持。适配新品牌只需实现以下接口,**无需修改 Client/ViewModel 代码**:
|
||||||
|
|
||||||
|
1. **实现 `IPlcDriver`**: 处理底层的 Socket/串口 通信(如 FINS, MC 协议)。
|
||||||
|
2. **实现 `IPlcAddress`**: 定义新品牌的地址格式(如欧姆龙的 `D100`, `W0.00`)。
|
||||||
|
3. **实现 `IPlcOptimizer`**: 针对新协议调整优化参数。
|
||||||
|
|
||||||
|
#### 扩展示例(伪代码)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 欧姆龙 FINS 协议优化器示例
|
||||||
|
public class OmronOptimizer : IPlcOptimizer
|
||||||
|
{
|
||||||
|
// 欧姆龙按 Word (2byte) 寻址,且单次读取限制不同
|
||||||
|
private const int MaxWordCount = 2000;
|
||||||
|
|
||||||
|
public List<PlcReadBatch> Optimize(IEnumerable<PlcTag> tags, int maxPduSize)
|
||||||
|
{
|
||||||
|
// 1. 按内存区 (DM, CIO, WR) 分组
|
||||||
|
// 2. 按 Word 地址排序
|
||||||
|
// 3. 合并逻辑:如果两个地址相差 < 10 个 Words,则合并读取
|
||||||
|
// 4. 返回适用于 FINS 协议的 ReadRequest 列表
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 🎯 高级功能
|
## 🎯 高级功能
|
||||||
|
|
||||||
### 1. 标签订阅与监控
|
### 1. 标签订阅与监控
|
||||||
@@ -211,24 +301,6 @@ foreach (var tag in tags)
|
|||||||
await _plcService.StartAsync();
|
await _plcService.StartAsync();
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. 批量优化原理
|
|
||||||
|
|
||||||
优化器会自动:
|
|
||||||
1. **按区域分组** - DB1、M、I、Q 等分开处理
|
|
||||||
2. **按地址排序** - 确保连续性
|
|
||||||
3. **间隙合并** - 小于 `MaxGapBytes` 的间隙合并为一次读取
|
|
||||||
4. **PDU限制** - 单次读取不超过 `MaxPduSize`
|
|
||||||
|
|
||||||
**示例:**
|
|
||||||
```
|
|
||||||
标签1: DB1.DBW0 (2 bytes)
|
|
||||||
标签2: DB1.DBW4 (2 bytes) ← 间隙 2 bytes
|
|
||||||
标签3: DB1.DBW8 (2 bytes) ← 间隙 2 bytes
|
|
||||||
|
|
||||||
优化后: 读取 DB1.DBW0, 10 bytes
|
|
||||||
包含所有三个标签和中间的间隙
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 使用特性绑定
|
### 3. 使用特性绑定
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
@@ -555,4 +627,4 @@ MIT License - 详见 [LICENSE](LICENSE) 文件
|
|||||||
|
|
||||||
- 项目主页: [GitHub](https://github.com/yourcompany/plc)
|
- 项目主页: [GitHub](https://github.com/yourcompany/plc)
|
||||||
- 问题反馈: [Issues](https://github.com/yourcompany/plc/issues)
|
- 问题反馈: [Issues](https://github.com/yourcompany/plc/issues)
|
||||||
- 文档: [Wiki](https://github.com/yourcompany/plc/wiki)
|
- 文档: [Wiki](https://github.com/yourcompany/plc/wiki)
|
||||||
Reference in new issue
Block a user