23 changed files with 2397 additions and 15 deletions
@ -0,0 +1,260 @@
|
||||
# S7 PLC数据采集功能说明 |
||||
|
||||
## 概述 |
||||
|
||||
本功能实现了通过S7协议读取西门子PLC200 Smart设备的数据,支持定时自动采集和手动读写操作。 |
||||
|
||||
## 功能特性 |
||||
|
||||
### 1. 支持的地址类型 |
||||
- **M区**: M0.1, M0.2 (位地址), M10 (字节地址) |
||||
- **I区**: I0.1, I0.2 (输入位), I0 (输入字节) |
||||
- **Q区**: Q0.1, Q0.2 (输出位), Q0 (输出字节) |
||||
- **VB区**: VB12 (字节) |
||||
- **VW区**: VW314 (字,2字节) |
||||
- **VD区**: VD11 (双字,4字节,浮点数) |
||||
- **AIW区**: AIW0, AIW64 (模拟量输入字,2字节,只读) |
||||
- **AQW区**: AQW0, AQW64 (模拟量输出字,2字节,可读写) |
||||
|
||||
### 2. 核心功能 |
||||
- ✅ 定时自动采集(每5分钟执行一次) |
||||
- ✅ 手动优先逻辑(Constant.WEB_FLAG为true时跳过采集) |
||||
- ✅ 根据gateway_manage中community_type='S7'的dataCom查询采集参数 |
||||
- ✅ 指令创建、下发、解析一体化 |
||||
- ✅ 数据倍率、初始值、小数点处理 |
||||
- ✅ 连接缓存管理,避免频繁创建连接 |
||||
|
||||
## 文件结构 |
||||
|
||||
``` |
||||
user-service/src/main/java/com/mh/user/ |
||||
├── s7/ |
||||
│ └── S7ConnectorUtil.java # S7通信工具类 |
||||
├── job/ |
||||
│ └── S7PlcCollectionJob.java # 定时采集任务 |
||||
├── controller/ |
||||
│ └── S7PlcController.java # 手动读写接口 |
||||
└── mapper/ |
||||
├── GatewayManageMapper.java # 新增queryS7Gateways()方法 |
||||
└── CollectionParamsManageMapper.java # 新增selectCPMByDataCom()方法 |
||||
``` |
||||
|
||||
## 数据库配置 |
||||
|
||||
### 1. gateway_manage表配置 |
||||
```sql |
||||
-- 添加S7类型的网关记录 |
||||
INSERT INTO gateway_manage ( |
||||
gateway_name, |
||||
gateway_ip, |
||||
gateway_port, -- 格式: "rack,slot" 例如 "0,1" |
||||
data_com, -- 通讯口标识,例如 "S7_PLC_1" |
||||
community_type,-- 必须设置为 'S7' |
||||
grade -- 连接状态: 1=在线 |
||||
) VALUES ( |
||||
'PLC200Smart_1', |
||||
'192.168.1.100', |
||||
'0,1', |
||||
'S7_PLC_1', |
||||
'S7', |
||||
'1' |
||||
); |
||||
``` |
||||
|
||||
### 2. device_install表配置 |
||||
```sql |
||||
-- 添加设备安装记录,关联到S7网关 |
||||
INSERT INTO device_install ( |
||||
device_name, |
||||
device_type, |
||||
data_com, -- 与gateway_manage中的data_com一致 |
||||
building_id |
||||
) VALUES ( |
||||
'S7设备1', |
||||
'PLC', |
||||
'S7_PLC_1', |
||||
'1' |
||||
); |
||||
``` |
||||
|
||||
### 3. collection_params_manage表配置 |
||||
```sql |
||||
-- 添加采集点位配置 |
||||
INSERT INTO collection_params_manage ( |
||||
device_install_id, |
||||
register_addr, -- 寄存器地址,如 M0.1, VB12, VW314, VD11 |
||||
func_code, -- 功能码(可选) |
||||
mt_ratio, -- 倍率 |
||||
mt_init_value, -- 初始值 |
||||
digits, -- 小数点位数 |
||||
is_use, -- 是否启用: 1=启用 |
||||
other_name, -- 点位别名(唯一) |
||||
building_id |
||||
) VALUES ( |
||||
1, -- device_install_id |
||||
'M0.1', -- 寄存器地址 |
||||
NULL, |
||||
1, -- 倍率 |
||||
0, -- 初始值 |
||||
2, -- 2位小数 |
||||
1, -- 启用 |
||||
'plc_status', -- 点位名称 |
||||
'1' -- building_id |
||||
); |
||||
``` |
||||
|
||||
## 使用方式 |
||||
|
||||
### 1. 自动定时采集 |
||||
|
||||
系统启动后会自动执行定时任务,每5分钟采集一次所有S7网关的数据。 |
||||
|
||||
**定时任务配置:** |
||||
- Cron表达式: `0 0/5 * * * ?` (每5分钟) |
||||
- 可在`S7PlcCollectionJob.collectS7Data()`方法中修改 |
||||
|
||||
**手动优先逻辑:** |
||||
- 当`Constant.WEB_FLAG = true`时,跳过本次采集 |
||||
- 确保手动操作不被定时任务干扰 |
||||
|
||||
### 2. 手动写入数据 |
||||
|
||||
**API接口:** |
||||
``` |
||||
POST /s7plc/write |
||||
参数: |
||||
- cpmId: 采集参数ID (Long) |
||||
- value: 要写入的值 (Object) |
||||
|
||||
返回: |
||||
{ |
||||
"code": 200, |
||||
"msg": "写入成功" |
||||
} |
||||
``` |
||||
|
||||
**示例:** |
||||
```bash |
||||
curl -X POST "http://localhost:8080/s7plc/write?cpmId=1&value=1" |
||||
``` |
||||
|
||||
### 3. 清理连接缓存 |
||||
|
||||
**API接口:** |
||||
``` |
||||
POST /s7plc/clearCache |
||||
|
||||
返回: |
||||
{ |
||||
"code": 200, |
||||
"msg": "缓存清理成功" |
||||
} |
||||
``` |
||||
|
||||
## 代码示例 |
||||
|
||||
### 在Service中调用手动写入 |
||||
```java |
||||
@Autowired |
||||
private S7PlcCollectionJob s7PlcCollectionJob; |
||||
|
||||
public void controlDevice(Long cpmId, Object value) { |
||||
// 设置手动操作标志 |
||||
Constant.WEB_FLAG = true; |
||||
try { |
||||
boolean success = s7PlcCollectionJob.writeData(cpmId, value); |
||||
if (success) { |
||||
log.info("控制成功"); |
||||
} else { |
||||
log.error("控制失败"); |
||||
} |
||||
} finally { |
||||
Constant.WEB_FLAG = false; |
||||
} |
||||
} |
||||
``` |
||||
|
||||
## 注意事项 |
||||
|
||||
### 1. PLC连接配置 |
||||
- **IP地址**: 确保gateway_manage.gatewayIP正确配置 |
||||
- **Rack/Slot**: 默认为0,1,可通过gatewayPort字段配置(格式: "rack,slot") |
||||
- **网络**: 确保服务器能访问PLC的IP地址和端口(默认102) |
||||
|
||||
### 2. 数据类型映射 |
||||
| 地址类型 | 数据类型 | DaveArea | 说明 | |
||||
|---------|---------|----------|------| |
||||
| M0.1 | Boolean | FLAGS | 位地址,返回0或1 | |
||||
| M10 | Byte | FLAGS | 字节地址,返回0-255 | |
||||
| I0.1 | Boolean | INPUTS | 输入位,返回0或1(只读) | |
||||
| I0 | Byte | INPUTS | 输入字节,返回0-255(只读) | |
||||
| Q0.1 | Boolean | OUTPUTS | 输出位,返回0或1 | |
||||
| Q0 | Byte | OUTPUTS | 输出字节,返回0-255 | |
||||
| VB12 | Byte | DB | V区字节,返回0-255 | |
||||
| VW314 | Word | DB | V区字,返回0-65535 | |
||||
| VD11 | Float | DB | V区双字,返回浮点数 | |
||||
| AIW0 | Word | INPUTS | 模拟量输入字,返回0-65535(只读) | |
||||
| AQW0 | Word | OUTPUTS | 模拟量输出字,返回0-65535 | |
||||
|
||||
### 3. 注意事项 |
||||
|
||||
#### 读写权限说明 |
||||
- **只读区域**: I区(输入)、AIW(模拟量输入) - 这些区域通常由PLC硬件控制,不建议写入 |
||||
- **可写区域**: M区、Q区(输出)、V区、AQW(模拟量输出) |
||||
- 尝试写入只读区域时会记录警告日志,但仍会执行写入操作 |
||||
|
||||
### 4. 性能优化 |
||||
- 连接器采用缓存机制,避免频繁创建连接 |
||||
- 批量采集时使用同一个连接器实例 |
||||
- 可通过`clearCache()`方法清理缓存 |
||||
|
||||
### 5. 异常处理 |
||||
- 连接失败会记录日志并跳过该网关 |
||||
- 单个点位采集失败不影响其他点位 |
||||
- 所有异常都会记录详细日志 |
||||
|
||||
## 依赖库 |
||||
|
||||
项目已添加s7connector依赖: |
||||
```xml |
||||
<dependency> |
||||
<groupId>com.github.s7connector</groupId> |
||||
<artifactId>s7connector</artifactId> |
||||
<version>2.1</version> |
||||
</dependency> |
||||
``` |
||||
|
||||
## 后续优化方向 |
||||
|
||||
1. **连接池管理**: 实现更完善的连接池,支持断线重连 |
||||
2. **批量读写**: 支持一次性读取多个点位,提高效率 |
||||
3. **数据校验**: 增加数据范围校验和异常值过滤 |
||||
4. **监控告警**: 添加PLC连接状态监控和异常告警 |
||||
5. **配置化**: 将定时任务周期等参数配置化 |
||||
6. **测试用例**: 编写单元测试和集成测试 |
||||
|
||||
## 常见问题 |
||||
|
||||
### Q1: 连接失败怎么办? |
||||
- 检查PLC IP地址是否正确 |
||||
- 检查网络连接是否正常 |
||||
- 检查PLC是否允许S7通信 |
||||
- 查看日志中的详细错误信息 |
||||
|
||||
### Q2: 读取数据不正确? |
||||
- 检查registerAddr格式是否正确 |
||||
- 确认数据类型是否匹配(M/VB/VW/VD) |
||||
- 检查倍率和初始值配置 |
||||
- 对比PLC中的实际值 |
||||
|
||||
### Q3: 如何调试? |
||||
- 查看日志文件:`logs/user-service/info/` |
||||
- 启用DEBUG日志级别 |
||||
- 使用PLC仿真软件进行测试 |
||||
|
||||
## 联系方式 |
||||
|
||||
如有问题,请联系开发团队。 |
||||
|
||||
--- |
||||
**最后更新**: 2026-06-23 |
||||
@ -0,0 +1,303 @@
|
||||
# S7 PLC 地址类型扩展说明 |
||||
|
||||
## 更新概述 |
||||
|
||||
本次更新为S7ConnectorUtil工具类新增了以下地址类型的支持: |
||||
- **I区**: 数字量输入 (Input) |
||||
- **Q区**: 数字量输出 (Output) |
||||
- **AIW区**: 模拟量输入字 (Analog Input Word) |
||||
- **AQW区**: 模拟量输出字 (Analog Output Word) |
||||
|
||||
## 支持的地址格式 |
||||
|
||||
### 1. I区 - 数字量输入 |
||||
**地址格式:** |
||||
- 位地址: `I0.1`, `I1.5`, `I2.3` 等 |
||||
- 字节地址: `I0`, `I1`, `I10` 等 |
||||
|
||||
**特性:** |
||||
- DaveArea: `PE` (Process Inputs) |
||||
- 数据类型: Boolean (位) 或 Byte (字节) |
||||
- **通常只读**,由外部传感器/开关控制 |
||||
- 写入时会记录警告日志 |
||||
|
||||
**示例:** |
||||
```java |
||||
// 读取输入位 I0.1 |
||||
Object value = connector.readData("I0.1"); // 返回 0 或 1 |
||||
|
||||
// 读取输入字节 I0 |
||||
Object value = connector.readData("I0"); // 返回 0-255 |
||||
``` |
||||
|
||||
### 2. Q区 - 数字量输出 |
||||
**地址格式:** |
||||
- 位地址: `Q0.1`, `Q1.5`, `Q2.3` 等 |
||||
- 字节地址: `Q0`, `Q1`, `Q10` 等 |
||||
|
||||
**特性:** |
||||
- DaveArea: `PA` (Process Outputs) |
||||
- 数据类型: Boolean (位) 或 Byte (字节) |
||||
- **可读写**,用于控制继电器/指示灯等 |
||||
|
||||
**示例:** |
||||
```java |
||||
// 读取输出位 Q0.1 |
||||
Object value = connector.readData("Q0.1"); // 返回 0 或 1 |
||||
|
||||
// 写入输出位 Q0.1 |
||||
connector.writeData("Q0.1", 1); // 置位 |
||||
|
||||
// 写入输出字节 Q0 |
||||
connector.writeData("Q0", 255); // 所有位都置1 |
||||
``` |
||||
|
||||
### 3. AIW区 - 模拟量输入字 |
||||
**地址格式:** |
||||
- `AIW0`, `AIW64`, `AIW128` 等 |
||||
|
||||
**特性:** |
||||
- DaveArea: `PE` (Process Inputs) |
||||
- 数据类型: Word (2字节, 0-65535) |
||||
- **只读**,用于读取模拟量传感器(温度、压力等) |
||||
- 通常对应PLC的模拟量输入模块 |
||||
- 写入时会记录警告日志 |
||||
|
||||
**示例:** |
||||
```java |
||||
// 读取模拟量输入 AIW0 |
||||
Object value = connector.readData("AIW0"); // 返回 0-65535 |
||||
|
||||
// 转换为实际值(例如温度传感器 0-10V 对应 0-100°C) |
||||
int rawValue = (Integer) value; |
||||
double temperature = rawValue * 100.0 / 65535.0; |
||||
``` |
||||
|
||||
### 4. AQW区 - 模拟量输出字 |
||||
**地址格式:** |
||||
- `AQW0`, `AQW64`, `AQW128` 等 |
||||
|
||||
**特性:** |
||||
- DaveArea: `PA` (Process Outputs) |
||||
- 数据类型: Word (2字节, 0-65535) |
||||
- **可读写**,用于控制模拟量执行器(变频器、调节阀等) |
||||
- 通常对应PLC的模拟量输出模块 |
||||
|
||||
**示例:** |
||||
```java |
||||
// 读取模拟量输出 AQW0 |
||||
Object value = connector.readData("AQW0"); // 返回 0-65535 |
||||
|
||||
// 写入模拟量输出(例如设置变频器频率 0-50Hz) |
||||
double frequency = 25.0; // 25Hz |
||||
int outputValue = (int)(frequency * 65535.0 / 50.0); |
||||
connector.writeData("AQW0", outputValue); |
||||
``` |
||||
|
||||
## DaveArea映射表 |
||||
|
||||
| PLC区域 | DaveArea枚举 | 说明 | |
||||
|---------|-------------|------| |
||||
| M区 | FLAGS | 位存储区/Merkers | |
||||
| I区 | INPUTS | 数字量输入/Inputs | |
||||
| Q区 | OUTPUTS | 数字量输出/Outputs | |
||||
| V区(DB) | DB | 数据块/Data Block | |
||||
| AIW | INPUTS | 模拟量输入(映射到输入区) | |
||||
| AQW | OUTPUTS | 模拟量输出(映射到输出区) | |
||||
|
||||
## 数据库配置示例 |
||||
|
||||
### 配置I区点位 |
||||
```sql |
||||
-- 数字量输入点位 |
||||
INSERT INTO collection_params_manage ( |
||||
device_install_id, |
||||
register_addr, |
||||
is_use, |
||||
other_name, |
||||
building_id |
||||
) VALUES ( |
||||
1, |
||||
'I0.1', -- 急停按钮状态 |
||||
1, |
||||
'emergency_stop', |
||||
'1' |
||||
); |
||||
``` |
||||
|
||||
### 配置Q区点位 |
||||
```sql |
||||
-- 数字量输出点位 |
||||
INSERT INTO collection_params_manage ( |
||||
device_install_id, |
||||
register_addr, |
||||
is_use, |
||||
other_name, |
||||
building_id |
||||
) VALUES ( |
||||
1, |
||||
'Q0.1', -- 启动指示灯 |
||||
1, |
||||
'start_indicator', |
||||
'1' |
||||
); |
||||
``` |
||||
|
||||
### 配置AIW区点位 |
||||
```sql |
||||
-- 模拟量输入点位(温度传感器) |
||||
INSERT INTO collection_params_manage ( |
||||
device_install_id, |
||||
register_addr, |
||||
mt_ratio, -- 倍率 |
||||
digits, -- 小数点 |
||||
is_use, |
||||
other_name, |
||||
building_id |
||||
) VALUES ( |
||||
1, |
||||
'AIW0', -- 温度传感器 |
||||
0.0015259, -- 倍率: 100/65535 ≈ 0.0015259 |
||||
2, -- 2位小数 |
||||
1, |
||||
'temperature_sensor', |
||||
'1' |
||||
); |
||||
``` |
||||
|
||||
### 配置AQW区点位 |
||||
```sql |
||||
-- 模拟量输出点位(变频器控制) |
||||
INSERT INTO collection_params_manage ( |
||||
device_install_id, |
||||
register_addr, |
||||
is_use, |
||||
other_name, |
||||
building_id |
||||
) VALUES ( |
||||
1, |
||||
'AQW0', -- 变频器频率设定 |
||||
1, |
||||
'vfd_frequency', |
||||
'1' |
||||
); |
||||
``` |
||||
|
||||
## 实际应用案例 |
||||
|
||||
### 案例1: 读取多个传感器数据 |
||||
```java |
||||
// 在S7PlcCollectionJob中自动采集 |
||||
// 配置以下点位: |
||||
// - I0.1: 急停按钮 |
||||
// - I0.2: 启动按钮 |
||||
// - AIW0: 温度传感器 |
||||
// - AIW64: 压力传感器 |
||||
|
||||
// 系统会每5分钟自动读取并保存到数据库 |
||||
``` |
||||
|
||||
### 案例2: 手动控制输出 |
||||
```java |
||||
@Autowired |
||||
private S7PlcCollectionJob s7PlcCollectionJob; |
||||
|
||||
// 通过API控制Q区输出 |
||||
@PostMapping("/control/light") |
||||
public HttpResult controlLight(@RequestParam boolean on) { |
||||
Constant.WEB_PLC_FLAG = true; |
||||
try { |
||||
// 获取Q0.1对应的cpmId |
||||
Long cpmId = getCpmIdByOtherName("start_indicator"); |
||||
s7PlcCollectionJob.writeData(cpmId, on ? 1 : 0); |
||||
return HttpResult.ok(); |
||||
} finally { |
||||
Constant.WEB_PLC_FLAG = false; |
||||
} |
||||
} |
||||
``` |
||||
|
||||
### 案例3: 模拟量转换 |
||||
```java |
||||
// 温度传感器: 0-10V 对应 0-100°C |
||||
// AIW0 原始值: 0-65535 |
||||
|
||||
// 在Service中进行转换 |
||||
public double getTemperature() { |
||||
CollectionParamsManageEntity param = |
||||
collectionParamsManageMapper.selectDeviceInstallByOtherName( |
||||
"temperature_sensor", "1" |
||||
); |
||||
|
||||
// 原始值已经应用了倍率 0.0015259 |
||||
BigDecimal rawValue = param.getCurValue(); |
||||
|
||||
// 直接得到摄氏度 |
||||
return rawValue.doubleValue(); |
||||
} |
||||
``` |
||||
|
||||
## 注意事项 |
||||
|
||||
### 1. I区和AIW区的只读特性 |
||||
- 这些区域由PLC硬件或外部设备控制 |
||||
- 尝试写入会导致警告日志 |
||||
- 在实际应用中应避免写入操作 |
||||
|
||||
### 2. Q区和AQW区的写入权限 |
||||
- 确保PLC程序允许外部写入 |
||||
- 注意PLC程序中是否有互锁逻辑 |
||||
- 写入前确认设备状态安全 |
||||
|
||||
### 3. 模拟量量程匹配 |
||||
- AIW/AQW的0-65535对应实际物理量 |
||||
- 需要在PLC中配置正确的量程 |
||||
- 或在Java代码中进行转换计算 |
||||
|
||||
### 4. 地址偏移 |
||||
- AIW和AQW的地址通常是2的倍数(AIW0, AIW64, AIW128...) |
||||
- 具体取决于PLC硬件配置 |
||||
- 请参考PLC硬件手册 |
||||
|
||||
## 测试建议 |
||||
|
||||
### 1. 使用PLC仿真软件 |
||||
- S7-PLCSIM Advanced |
||||
- 可以模拟各种地址类型的读写 |
||||
- 验证地址解析是否正确 |
||||
|
||||
### 2. 逐步测试 |
||||
1. 先测试M区和V区(最常用) |
||||
2. 再测试I区和Q区(数字量) |
||||
3. 最后测试AIW和AQW(模拟量) |
||||
|
||||
### 3. 监控日志 |
||||
- 查看读写操作的日志 |
||||
- 确认数据类型转换正确 |
||||
- 检查是否有警告或错误 |
||||
|
||||
## 常见问题 |
||||
|
||||
### Q1: 为什么I区写入会有警告? |
||||
A: I区是输入区,通常由外部硬件控制。写入I区在技术上可行,但不符合PLC编程规范,可能导致意外行为。 |
||||
|
||||
### Q2: AIW和AQW的地址为什么是64、128这样的数字? |
||||
A: 这取决于PLC的硬件配置。每个模拟量通道占用2个字节,地址间隔由模块插槽位置决定。 |
||||
|
||||
### Q3: 如何确定模拟量的量程? |
||||
A: |
||||
1. 查看PLC硬件模块手册 |
||||
2. 检查PLC程序中的量程配置 |
||||
3. 使用万用表测量实际电压/电流 |
||||
4. 对比PLC显示值和实际值进行校准 |
||||
|
||||
### Q4: Q区写入后PLC没有响应? |
||||
A: |
||||
1. 检查PLC程序是否覆盖了Q区 |
||||
2. 确认PLC处于RUN模式 |
||||
3. 检查是否有硬件故障 |
||||
4. 验证网络连接正常 |
||||
|
||||
--- |
||||
**更新日期**: 2026-06-23 |
||||
**版本**: v1.1 |
||||
@ -0,0 +1,301 @@
|
||||
# S7 PLC 数据解析问题排查指南 |
||||
|
||||
## 问题现象 |
||||
虽然有数据返回,但解析出来的数值与实际PLC中的值不一致。 |
||||
|
||||
## 常见原因及解决方案 |
||||
|
||||
### 1. 字节序问题 (最常见) |
||||
|
||||
**问题描述:** |
||||
- 西门子S7协议使用**大端序**(Big-Endian) |
||||
- 如果库返回的是小端序,会导致数值错误 |
||||
|
||||
**示例:** |
||||
``` |
||||
PLC中VW100 = 256 (0x0100) |
||||
大端序: [0x01, 0x00] → 计算: (0x01 << 8) | 0x00 = 256 ✓ |
||||
小端序: [0x00, 0x01] → 计算: (0x00 << 8) | 0x01 = 1 ✗ |
||||
``` |
||||
|
||||
**当前实现:** |
||||
代码已使用大端序转换: |
||||
```java |
||||
private int bytesToWord(byte[] data) { |
||||
return ((data[0] & 0xFF) << 8) | (data[1] & 0xFF); // 大端序 |
||||
} |
||||
``` |
||||
|
||||
**排查方法:** |
||||
查看日志中的原始数据和计算结果: |
||||
``` |
||||
读取VW区: addr=VW100, data=[1, 0], hex=01 00, value=256 |
||||
``` |
||||
|
||||
--- |
||||
|
||||
### 2. DB块号问题 |
||||
|
||||
**问题描述:** |
||||
V区(VB/VW/VD)对应PLC中的数据块(DB),不同项目可能使用不同的DB块号。 |
||||
|
||||
**当前实现:** |
||||
```java |
||||
// 固定使用DB1 |
||||
data = connector.read(DaveArea.DB, 1, addressInfo.getByteOffset(), 2); |
||||
``` |
||||
|
||||
**解决方案:** |
||||
如果你的PLC使用其他DB块(如DB2、DB100等),需要修改代码或配置。 |
||||
|
||||
**排查步骤:** |
||||
1. 在PLC程序中确认V区对应的DB块号 |
||||
2. 查看TIA Portal或STEP 7中的符号表 |
||||
3. 修改代码中的DB块号参数 |
||||
|
||||
--- |
||||
|
||||
### 3. AIW/AQW地址偏移问题 |
||||
|
||||
**问题描述:** |
||||
模拟量模块的地址可能不是从0开始,而是从64、128等开始。 |
||||
|
||||
**示例:** |
||||
``` |
||||
第一个模拟量通道: AIW0 |
||||
第二个模拟量通道: AIW2 或 AIW64 (取决于硬件配置) |
||||
``` |
||||
|
||||
**排查方法:** |
||||
1. 查看PLC硬件组态 |
||||
2. 确认模拟量模块的起始地址 |
||||
3. 在PLC监控表中验证实际地址 |
||||
|
||||
--- |
||||
|
||||
### 4. 数据类型不匹配 |
||||
|
||||
**问题描述:** |
||||
使用了错误的读取方法,导致数据类型解析错误。 |
||||
|
||||
**对照表:** |
||||
| PLC地址 | 正确方法 | 错误示例 | |
||||
|---------|---------|---------| |
||||
| VW100 (字) | bytesToWord() - 2字节 | bytesToDWord() - 4字节 | |
||||
| VD200 (双字) | bytesToDWord() - 4字节 | bytesToWord() - 2字节 | |
||||
| M0.1 (位) | getBit() | 直接读取字节 | |
||||
|
||||
**排查方法:** |
||||
查看日志中的字节长度: |
||||
``` |
||||
读取VW区: data=[1, 0] ← 应该是2字节 |
||||
读取VD区: data=[64, 66, 0, 0] ← 应该是4字节 |
||||
``` |
||||
|
||||
--- |
||||
|
||||
### 5. 浮点数精度问题 |
||||
|
||||
**问题描述:** |
||||
VD区存储的是IEEE 754浮点数,转换可能有精度损失。 |
||||
|
||||
**示例:** |
||||
``` |
||||
PLC中: 25.5 |
||||
读取后: 25.500001 或 25.499999 |
||||
``` |
||||
|
||||
**解决方案:** |
||||
在应用层进行四舍五入: |
||||
```java |
||||
BigDecimal value = new BigDecimal(result); |
||||
value = value.setScale(2, BigDecimal.ROUND_HALF_UP); |
||||
``` |
||||
|
||||
--- |
||||
|
||||
## 调试步骤 |
||||
|
||||
### 第1步: 启用DEBUG日志 |
||||
|
||||
在`application.yml`或`application.properties`中设置: |
||||
```yaml |
||||
logging: |
||||
level: |
||||
com.mh.user.s7.S7ConnectorUtil: DEBUG |
||||
``` |
||||
|
||||
### 第2步: 查看原始数据日志 |
||||
|
||||
运行程序后,查看日志输出: |
||||
``` |
||||
2026-06-23 10:30:15 DEBUG - 读取VW区: addr=VW100, data=[1, 0], hex=01 00, value=256 |
||||
2026-06-23 10:30:15 INFO - 读取成功: addr=VW100, value=256 |
||||
``` |
||||
|
||||
### 第3步: 对比PLC实际值 |
||||
|
||||
1. 打开TIA Portal或STEP 7 |
||||
2. 进入在线监控模式 |
||||
3. 查看对应地址的实际值 |
||||
4. 对比日志中的value值 |
||||
|
||||
### 第4步: 分析差异 |
||||
|
||||
**情况A: 字节顺序相反** |
||||
``` |
||||
PLC显示: 256 (0x0100) |
||||
日志显示: data=[0, 1], value=1 |
||||
``` |
||||
→ 需要交换字节顺序 |
||||
|
||||
**情况B: 数值完全不对** |
||||
``` |
||||
PLC显示: 100 |
||||
日志显示: value=16777216 |
||||
``` |
||||
→ 可能是DB块号错误或地址偏移错误 |
||||
|
||||
**情况C: 浮点数异常** |
||||
``` |
||||
PLC显示: 25.5 |
||||
日志显示: value=1.23E-40 |
||||
``` |
||||
→ 字节序错误或数据类型错误 |
||||
|
||||
--- |
||||
|
||||
## 常见问题案例 |
||||
|
||||
### 案例1: VW区读数是小端序 |
||||
|
||||
**现象:** |
||||
``` |
||||
PLC: VW100 = 256 |
||||
日志: data=[0, 1], value=1 |
||||
``` |
||||
|
||||
**解决:** |
||||
检查`bytesToWord()`方法,确保是大端序: |
||||
```java |
||||
// 正确 - 大端序 |
||||
return ((data[0] & 0xFF) << 8) | (data[1] & 0xFF); |
||||
|
||||
// 错误 - 小端序 |
||||
return (data[0] & 0xFF) | ((data[1] & 0xFF) << 8); |
||||
``` |
||||
|
||||
--- |
||||
|
||||
### 案例2: VD区浮点数解析错误 |
||||
|
||||
**现象:** |
||||
``` |
||||
PLC: VD200 = 25.5 |
||||
日志: data=[65, 76, 0, 0], value=25.5 ← 正确 |
||||
或 |
||||
日志: data=[0, 0, 76, 65], value=异常值 ← 错误 |
||||
``` |
||||
|
||||
**解决:** |
||||
确认`bytesToDWord()`使用大端序: |
||||
```java |
||||
int intBits = ((data[0] & 0xFF) << 24) | |
||||
((data[1] & 0xFF) << 16) | |
||||
((data[2] & 0xFF) << 8) | |
||||
(data[3] & 0xFF); |
||||
return Float.intBitsToFloat(intBits); |
||||
``` |
||||
|
||||
--- |
||||
|
||||
### 案例3: DB块号错误 |
||||
|
||||
**现象:** |
||||
``` |
||||
PLC: DB100.DBW0 = 1000 |
||||
日志: value=0 或随机值 |
||||
``` |
||||
|
||||
**解决:** |
||||
修改读取时的DB块号: |
||||
```java |
||||
// 如果V区对应DB100 |
||||
data = connector.read(DaveArea.DB, 100, addressInfo.getByteOffset(), 2); |
||||
``` |
||||
|
||||
--- |
||||
|
||||
## 快速验证方法 |
||||
|
||||
### 方法1: 使用已知值测试 |
||||
|
||||
在PLC程序中设置一个已知值: |
||||
``` |
||||
MB10 = 123 (0x7B) |
||||
VW100 = 256 (0x0100) |
||||
VD200 = 10.5 (浮点数) |
||||
``` |
||||
|
||||
然后读取并对比: |
||||
``` |
||||
预期日志: |
||||
读取M区: addr=M10, data=[123], hex=7B, value=123 |
||||
读取VW区: addr=VW100, data=[1, 0], hex=01 00, value=256 |
||||
读取VD区: addr=VD200, data=[65, 40, 0, 0], hex=41 28 00 00, value=10.5 |
||||
``` |
||||
|
||||
### 方法2: 使用PLC仿真软件 |
||||
|
||||
1. 安装S7-PLCSIM Advanced |
||||
2. 创建测试程序,设置已知值 |
||||
3. 运行Java程序读取 |
||||
4. 对比结果 |
||||
|
||||
### 方法3: 使用其他S7客户端工具 |
||||
|
||||
推荐使用: |
||||
- **Modbus Poll** (支持S7) |
||||
- **CAS Modbus Scanner** |
||||
- **QModMaster** |
||||
|
||||
用这些工具读取相同的地址,对比结果。 |
||||
|
||||
--- |
||||
|
||||
## 性能优化建议 |
||||
|
||||
### 1. 批量读取 |
||||
|
||||
如果需要读取多个连续地址,建议使用批量读取: |
||||
```java |
||||
// 一次性读取10个字 |
||||
byte[] data = connector.read(DaveArea.DB, 1, 0, 20); // 20字节 = 10个字 |
||||
``` |
||||
|
||||
### 2. 减少日志输出 |
||||
|
||||
生产环境关闭DEBUG日志: |
||||
```yaml |
||||
logging: |
||||
level: |
||||
com.mh.user.s7.S7ConnectorUtil: INFO |
||||
``` |
||||
|
||||
### 3. 连接复用 |
||||
|
||||
当前已实现连接缓存,确保不要频繁创建新连接。 |
||||
|
||||
--- |
||||
|
||||
## 联系支持 |
||||
|
||||
如果以上方法都无法解决问题,请提供: |
||||
|
||||
1. **日志输出**: 包含完整的DEBUG日志 |
||||
2. **PLC截图**: TIA Portal中对应地址的监控值 |
||||
3. **地址信息**: 具体的registerAddr配置 |
||||
4. **期望值vs实际值**: 对比表格 |
||||
|
||||
--- |
||||
**更新日期**: 2026-06-23 |
||||
@ -0,0 +1,68 @@
|
||||
package com.mh.user.controller; |
||||
|
||||
import com.mh.common.http.HttpResult; |
||||
import com.mh.user.job.S7PlcCollectionJob; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.springframework.web.bind.annotation.*; |
||||
|
||||
/** |
||||
* S7 PLC控制接口 |
||||
* 提供手动读写PLC点位的功能 |
||||
* |
||||
* @author System |
||||
* @date 2026-06-23 |
||||
*/ |
||||
@Slf4j |
||||
@RestController |
||||
@RequestMapping("/s7plc") |
||||
public class S7PlcController { |
||||
|
||||
private final S7PlcCollectionJob s7PlcCollectionJob; |
||||
|
||||
public S7PlcController(S7PlcCollectionJob s7PlcCollectionJob) { |
||||
this.s7PlcCollectionJob = s7PlcCollectionJob; |
||||
} |
||||
|
||||
/** |
||||
* 手动写入数据到PLC |
||||
* |
||||
* @param cpmId 采集参数ID |
||||
* @param value 要写入的值 |
||||
* @return 操作结果 |
||||
*/ |
||||
@PostMapping("/write") |
||||
public HttpResult writeData(@RequestParam Long cpmId, @RequestParam Object value) { |
||||
try { |
||||
log.info("收到手动写入请求: cpmId={}, value={}", cpmId, value); |
||||
|
||||
boolean success = s7PlcCollectionJob.writeData(cpmId, value); |
||||
|
||||
if (success) { |
||||
return HttpResult.ok("写入成功"); |
||||
} else { |
||||
return HttpResult.error(500, "写入失败"); |
||||
} |
||||
} catch (Exception e) { |
||||
log.error("手动写入异常: cpmId={}", cpmId, e); |
||||
return HttpResult.error(500, "写入异常: " + e.getMessage()); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 清理S7连接器缓存 |
||||
* 用于重启连接或维护 |
||||
* |
||||
* @return 操作结果 |
||||
*/ |
||||
@PostMapping("/clearCache") |
||||
public HttpResult clearCache() { |
||||
try { |
||||
log.info("收到清理S7连接器缓存请求"); |
||||
s7PlcCollectionJob.clearConnectorCache(); |
||||
return HttpResult.ok("缓存清理成功"); |
||||
} catch (Exception e) { |
||||
log.error("清理缓存异常", e); |
||||
return HttpResult.error(500, "清理缓存异常: " + e.getMessage()); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,179 @@
|
||||
package com.mh.user.dto; |
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat; |
||||
import lombok.Getter; |
||||
import lombok.Setter; |
||||
|
||||
import java.math.BigDecimal; |
||||
import java.util.StringJoiner; |
||||
|
||||
/** |
||||
* @author LJF |
||||
* @version 1.0 |
||||
* @project EEMCS |
||||
* @description 回水泵热泵控制界面VO |
||||
* @date 2025-03-14 09:00:37 |
||||
*/ |
||||
@Setter |
||||
@Getter |
||||
public class HotWaterSupplyPumpControlVO { |
||||
|
||||
private String id; |
||||
|
||||
private String name; |
||||
|
||||
private int orderNum; |
||||
|
||||
// 定时_时开1
|
||||
private int oneHourTimeOpenSetOne; |
||||
private String oneHourTimeOpenSetOneId; |
||||
|
||||
// 定时_分开1
|
||||
private int oneMinTimeOpenSetOne; |
||||
private String oneMinTimeOpenSetOneId; |
||||
|
||||
// 定时_时关1
|
||||
private int oneHourTimeCloseSetOne; |
||||
private String oneHourTimeCloseSetOneId; |
||||
|
||||
// 定时_分关1
|
||||
private int oneMinTimeCloseSetOne; |
||||
private String oneMinTimeCloseSetOneId; |
||||
|
||||
// 定时_时分开1
|
||||
private String oneHourMinTimeOpenSetOneStr; |
||||
// 定时_时分关1
|
||||
private String oneHourMinTimeCloseSetOneStr; |
||||
|
||||
// 定时_时开2
|
||||
private int oneHourTimeOpenSetTwo; |
||||
private String oneHourTimeOpenSetTwoId; |
||||
|
||||
// 定时_分开2
|
||||
private int oneMinTimeOpenSetTwo; |
||||
private String oneMinTimeOpenSetTwoId; |
||||
|
||||
// 定时_时关2
|
||||
private int oneHourTimeCloseSetTwo; |
||||
private String oneHourTimeCloseSetTwoId; |
||||
|
||||
// 定时_分关2
|
||||
private int oneMinTimeCloseSetTwo; |
||||
private String oneMinTimeCloseSetTwoId; |
||||
|
||||
// 定时_时分开2
|
||||
private String oneHourMinTimeOpenSetTwoStr; |
||||
// 定时_时分关2
|
||||
private String oneHourMinTimeCloseSetTwoStr; |
||||
|
||||
// 定时_时开3
|
||||
private int oneHourTimeOpenSetThree; |
||||
private String oneHourTimeOpenSetThreeId; |
||||
|
||||
// 定时_分开3
|
||||
private int oneMinTimeOpenSetThree; |
||||
private String oneMinTimeOpenSetThreeId; |
||||
|
||||
// 定时_时关3
|
||||
private int oneHourTimeCloseSetThree; |
||||
private String oneHourTimeCloseSetThreeId; |
||||
|
||||
// 定时_分关3
|
||||
private int oneMinTimeCloseSetThree; |
||||
private String oneMinTimeCloseSetThreeId; |
||||
|
||||
// 定时_时分开3
|
||||
private String oneHourMinTimeOpenSetThreeStr; |
||||
// 定时_时分关3
|
||||
private String oneHourMinTimeCloseSetThreeStr; |
||||
|
||||
// 开延时
|
||||
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "0") |
||||
private int openDelayTime; |
||||
private String openDelayTimeId; |
||||
|
||||
// 温度设置
|
||||
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "0") |
||||
private BigDecimal tempSet; |
||||
private String tempSetId; |
||||
|
||||
// 累计运行时间
|
||||
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "0") |
||||
private BigDecimal runTime; |
||||
private String runTimeId; |
||||
|
||||
// 运行状态
|
||||
private int runState; |
||||
private String runStateId; |
||||
|
||||
// 启停控制
|
||||
private int startStopControl; |
||||
private String startStopControlId; |
||||
|
||||
// 故障
|
||||
private int fault; |
||||
private String faultId; |
||||
|
||||
// 一键启动
|
||||
private int startOneKey; |
||||
private String startOneKeyId; |
||||
|
||||
// 手自动切换
|
||||
private int manualAutoSwitch; |
||||
private String manualAutoSwitchId; |
||||
|
||||
// 选择两台回水泵启动
|
||||
private int twoPumpStart; |
||||
private String twoPumpStartId; |
||||
|
||||
// 温度设置上限
|
||||
private BigDecimal tempSetUpperLimit; |
||||
private String tempSetUpperLimitId; |
||||
|
||||
// 温度设置下限
|
||||
private BigDecimal tempSetLowerLimit; |
||||
private String tempSetLowerLimitId; |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return new StringJoiner(", ", HotWaterSupplyPumpControlVO.class.getSimpleName() + "[", "]") |
||||
.add("id='" + id + "'") |
||||
.add("name='" + name + "'") |
||||
.add("orderNum=" + orderNum) |
||||
.add("oneHourTimeOpenSetOne=" + oneHourTimeOpenSetOne) |
||||
.add("oneHourTimeOpenSetOneId='" + oneHourTimeOpenSetOneId + "'") |
||||
.add("oneMinTimeOpenSetOne=" + oneMinTimeOpenSetOne) |
||||
.add("oneMinTimeOpenSetOneId='" + oneMinTimeOpenSetOneId + "'") |
||||
.add("oneHourTimeCloseSetOne=" + oneHourTimeCloseSetOne) |
||||
.add("oneHourTimeCloseSetOneId='" + oneHourTimeCloseSetOneId + "'") |
||||
.add("oneMinTimeCloseSetOne=" + oneMinTimeCloseSetOne) |
||||
.add("oneMinTimeCloseSetOneId='" + oneMinTimeCloseSetOneId + "'") |
||||
.add("oneHourTimeOpenSetTwo=" + oneHourTimeOpenSetTwo) |
||||
.add("oneHourTimeOpenSetTwoId='" + oneHourTimeOpenSetTwoId + "'") |
||||
.add("oneMinTimeOpenSetTwo=" + oneMinTimeOpenSetTwo) |
||||
.add("oneMinTimeOpenSetTwoId='" + oneMinTimeOpenSetTwoId + "'") |
||||
.add("oneHourTimeCloseSetTwo=" + oneHourTimeCloseSetTwo) |
||||
.add("oneHourTimeCloseSetTwoId='" + oneHourTimeCloseSetTwoId + "'") |
||||
.add("oneMinTimeCloseSetTwo=" + oneMinTimeCloseSetTwo) |
||||
.add("oneMinTimeCloseSetTwoId='" + oneMinTimeCloseSetTwoId + "'") |
||||
.add("oneHourTimeOpenSetThree=" + oneHourTimeOpenSetThree) |
||||
.add("oneHourTimeOpenSetThreeId='" + oneHourTimeOpenSetThreeId + "'") |
||||
.add("oneMinTimeOpenSetThree=" + oneMinTimeOpenSetThree) |
||||
.add("oneMinTimeOpenSetThreeId='" + oneMinTimeOpenSetThreeId + "'") |
||||
.add("oneHourTimeCloseSetThree=" + oneHourTimeCloseSetThree) |
||||
.add("oneHourTimeCloseSetThreeId='" + oneHourTimeCloseSetThreeId + "'") |
||||
.add("openDelayTime=" + openDelayTime) |
||||
.add("openDelayTimeId='" + openDelayTimeId + "'") |
||||
.add("tempSet=" + tempSet) |
||||
.add("tempSetId='" + tempSetId + "'") |
||||
.add("runTime=" + runTime) |
||||
.add("runTimeId='" + runTimeId + "'") |
||||
.add("runState=" + runState) |
||||
.add("runStateId='" + runStateId + "'") |
||||
.add("startStopControl=" + startStopControl) |
||||
.add("startStopControlId='" + startStopControlId + "'") |
||||
.add("fault=" + fault) |
||||
.add("faultId='" + faultId + "'") |
||||
.toString(); |
||||
} |
||||
} |
||||
@ -0,0 +1,337 @@
|
||||
package com.mh.user.job; |
||||
|
||||
import com.mh.user.constants.Constant; |
||||
import com.mh.user.entity.CollectionParamsManageEntity; |
||||
import com.mh.user.entity.GatewayManageEntity; |
||||
import com.mh.user.mapper.CollectionParamsManageMapper; |
||||
import com.mh.user.mapper.GatewayManageMapper; |
||||
import com.mh.user.s7.S7ConnectorUtil; |
||||
import com.mh.user.utils.DateUtil; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.springframework.scheduling.annotation.Scheduled; |
||||
import org.springframework.stereotype.Component; |
||||
|
||||
import java.math.BigDecimal; |
||||
import java.util.Date; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.concurrent.ConcurrentHashMap; |
||||
import java.util.stream.Collectors; |
||||
|
||||
/** |
||||
* S7 PLC定时采集任务 |
||||
* 支持M、VB、VW、VD等地址类型的读写操作 |
||||
* |
||||
* @author System |
||||
* @date 2026-06-23 |
||||
*/ |
||||
@Slf4j |
||||
@Component |
||||
public class S7PlcCollectionJob { |
||||
|
||||
private final GatewayManageMapper gatewayManageMapper; |
||||
private final CollectionParamsManageMapper collectionParamsManageMapper; |
||||
|
||||
// 缓存S7连接器,避免频繁创建连接
|
||||
private static final Map<String, S7ConnectorUtil> connectorCache = new ConcurrentHashMap<>(); |
||||
|
||||
public S7PlcCollectionJob(GatewayManageMapper gatewayManageMapper, |
||||
CollectionParamsManageMapper collectionParamsManageMapper) { |
||||
this.gatewayManageMapper = gatewayManageMapper; |
||||
this.collectionParamsManageMapper = collectionParamsManageMapper; |
||||
} |
||||
|
||||
/** |
||||
* 定时采集S7 PLC数据 |
||||
* 每5分钟执行一次,可根据实际需求调整 |
||||
* 优先处理手动操作:如果Constant.WEB_PLC_FLAG为true,则跳过本次采集 |
||||
*/ |
||||
@Scheduled(cron = "0 0/5 * * * ?") |
||||
public void collectS7Data() { |
||||
log.info("------S7 PLC定时采集开始>>>>Constant.FLAG=={}------", Constant.PLC_FLAG); |
||||
|
||||
try { |
||||
// 检查是否有手动操作正在进行
|
||||
if (Constant.PLC_FLAG || Constant.WEB_PLC_FLAG) { |
||||
log.info("存在手动操作,跳过本次S7 PLC采集"); |
||||
return; |
||||
} |
||||
|
||||
Constant.PLC_FLAG = true; |
||||
|
||||
// 查询所有在线的S7网关
|
||||
List<GatewayManageEntity> s7Gateways = gatewayManageMapper.queryS7Gateways(); |
||||
if (s7Gateways == null || s7Gateways.isEmpty()) { |
||||
log.info("未找到在线的S7网关"); |
||||
return; |
||||
} |
||||
|
||||
log.info("找到{}个在线S7网关", s7Gateways.size()); |
||||
|
||||
// 遍历每个S7网关
|
||||
for (GatewayManageEntity gateway : s7Gateways) { |
||||
try { |
||||
processGateway(gateway); |
||||
} catch (Exception e) { |
||||
log.error("处理S7网关异常: gatewayName={}, dataCom={}", |
||||
gateway.getGatewayName(), gateway.getDataCom(), e); |
||||
} |
||||
} |
||||
|
||||
} catch (Exception e) { |
||||
log.error("S7 PLC定时采集异常", e); |
||||
} finally { |
||||
Constant.PLC_FLAG = false; |
||||
log.info("------S7 PLC定时采集结束>>>>Constant.FLAG=={}------", Constant.PLC_FLAG); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 处理单个S7网关的数据采集 |
||||
*/ |
||||
private void processGateway(GatewayManageEntity gateway) { |
||||
String dataCom = gateway.getDataCom(); |
||||
if (dataCom == null || dataCom.isEmpty()) { |
||||
log.warn("网关dataCom为空: {}", gateway.getGatewayName()); |
||||
return; |
||||
} |
||||
|
||||
// 获取或创建S7连接器
|
||||
S7ConnectorUtil connector = getOrCreateConnector(gateway); |
||||
if (connector == null) { |
||||
log.error("无法创建S7连接器: {}", gateway.getGatewayName()); |
||||
return; |
||||
} |
||||
|
||||
// 查询该网关对应的采集参数
|
||||
List<CollectionParamsManageEntity> params = collectionParamsManageMapper.selectCPMByDataCom(dataCom); |
||||
if (params == null || params.isEmpty()) { |
||||
log.info("网关{}没有配置采集参数", gateway.getGatewayName()); |
||||
return; |
||||
} |
||||
|
||||
log.info("开始采集网关{}的{}个点位", gateway.getGatewayName(), params.size()); |
||||
|
||||
// 过滤掉重复的采集参数
|
||||
params = params.stream().distinct().collect(Collectors.toList()); |
||||
String dateStr = DateUtil.dateToString(new Date(), "yyyy-MM-dd HH:mm:ss"); |
||||
|
||||
// 遍历采集参数并读取数据
|
||||
for (CollectionParamsManageEntity param : params) { |
||||
try { |
||||
// 再次检查是否有手动操作
|
||||
if (Constant.WEB_PLC_FLAG) { |
||||
log.info("检测到手动操作,中断采集"); |
||||
break; |
||||
} |
||||
|
||||
readAndSaveData(connector, param, dateStr); |
||||
|
||||
} catch (Exception e) { |
||||
log.error("采集点位异常: registerAddr={}, otherName={}", |
||||
param.getRegisterAddr(), param.getOtherName(), e); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 读取并保存数据 |
||||
*/ |
||||
private void readAndSaveData(S7ConnectorUtil connector, |
||||
CollectionParamsManageEntity param, |
||||
String dateStr) { |
||||
String registerAddr = param.getRegisterAddr(); |
||||
if (registerAddr == null || registerAddr.isEmpty()) { |
||||
log.warn("点位寄存器地址为空: id={}", param.getId()); |
||||
return; |
||||
} |
||||
|
||||
// 读取数据
|
||||
Object value = connector.readData(registerAddr); |
||||
if (value == null) { |
||||
log.warn("读取数据为空: registerAddr={}", registerAddr); |
||||
return; |
||||
} |
||||
|
||||
// 转换值为BigDecimal
|
||||
BigDecimal curValue; |
||||
try { |
||||
// BigDecimal 构造函数可以直接处理 String 和 Number.toString()
|
||||
curValue = new BigDecimal(value.toString()); |
||||
} catch (NumberFormatException e) { |
||||
log.warn("无法转换为数字: registerAddr={}, value={}", "", value); |
||||
return; |
||||
} |
||||
|
||||
// 应用倍率和初始值
|
||||
if (param.getMtRatio() != null && param.getMtRatio() != 1) { |
||||
curValue = curValue.multiply(new BigDecimal(param.getMtRatio())); |
||||
} |
||||
if (param.getMtInitValue() != null) { |
||||
curValue = curValue.add(param.getMtInitValue()); |
||||
} |
||||
|
||||
// 应用小数点位数
|
||||
if (param.getDigits() != null && param.getDigits() > 0) { |
||||
curValue = curValue.setScale(param.getDigits(), BigDecimal.ROUND_HALF_UP); |
||||
} |
||||
|
||||
// 更新数据库
|
||||
collectionParamsManageMapper.updateCollectionParamsManage( |
||||
param.getDeviceInstallId().intValue(), |
||||
registerAddr, |
||||
curValue.toString(), |
||||
dateStr, |
||||
param.getBuildingId() != null ? param.getBuildingId().toString() : null |
||||
); |
||||
|
||||
log.debug("采集成功: registerAddr={}, value={}, otherName={}", |
||||
registerAddr, curValue, param.getOtherName()); |
||||
} |
||||
|
||||
/** |
||||
* 获取或创建S7连接器 |
||||
*/ |
||||
private S7ConnectorUtil getOrCreateConnector(GatewayManageEntity gateway) { |
||||
String cacheKey = gateway.getDataCom(); |
||||
|
||||
// 从缓存中获取
|
||||
S7ConnectorUtil connector = connectorCache.get(cacheKey); |
||||
if (connector != null) { |
||||
return connector; |
||||
} |
||||
|
||||
// 创建新连接器
|
||||
try { |
||||
String ipAddress = gateway.getGatewayIP(); |
||||
if (ipAddress == null || ipAddress.isEmpty()) { |
||||
log.error("网关IP地址为空: {}", gateway.getGatewayName()); |
||||
return null; |
||||
} |
||||
|
||||
// 解析rack和slot,默认为0和1
|
||||
int rack = 0; |
||||
int slot = 1; |
||||
if (gateway.getGatewayPort() != null && !gateway.getGatewayPort().isEmpty()) { |
||||
try { |
||||
String[] parts = gateway.getGatewayPort().split(","); |
||||
if (parts.length >= 2) { |
||||
rack = Integer.parseInt(parts[0].trim()); |
||||
slot = Integer.parseInt(parts[1].trim()); |
||||
} |
||||
} catch (NumberFormatException e) { |
||||
log.warn("解析rack/slot失败,使用默认值: {}", gateway.getGatewayPort()); |
||||
} |
||||
} |
||||
|
||||
connector = new S7ConnectorUtil(ipAddress, rack, slot); |
||||
connectorCache.put(cacheKey, connector); |
||||
log.info("创建S7连接器成功: IP={}, rack={}, slot={}", ipAddress, rack, slot); |
||||
return connector; |
||||
|
||||
} catch (Exception e) { |
||||
log.error("创建S7连接器失败: {}", gateway.getGatewayName(), e); |
||||
return null; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 手动写入数据到PLC(供Controller调用) |
||||
* |
||||
* @param cpmId 采集参数ID |
||||
* @param value 要写入的值 |
||||
* @return 是否成功 |
||||
*/ |
||||
public boolean writeData(Long cpmId, Object value) { |
||||
try { |
||||
// 查询采集参数
|
||||
CollectionParamsManageEntity param = collectionParamsManageMapper.selectById(cpmId.toString()); |
||||
if (param == null) { |
||||
log.error("采集参数不存在: cpmId={}", cpmId); |
||||
return false; |
||||
} |
||||
|
||||
// 查询网关信息
|
||||
String dataCom = getDataComByDeviceId(param.getDeviceInstallId()); |
||||
if (dataCom == null) { |
||||
log.error("无法获取设备对应的dataCom: deviceInstallId={}", param.getDeviceInstallId()); |
||||
return false; |
||||
} |
||||
|
||||
GatewayManageEntity gateway = getGatewayByDataCom(dataCom); |
||||
if (gateway == null) { |
||||
log.error("无法获取网关信息: dataCom={}", dataCom); |
||||
return false; |
||||
} |
||||
|
||||
// 获取连接器
|
||||
S7ConnectorUtil connector = getOrCreateConnector(gateway); |
||||
if (connector == null) { |
||||
log.error("无法获取S7连接器"); |
||||
return false; |
||||
} |
||||
|
||||
// 写入数据
|
||||
connector.writeData(param.getRegisterAddr(), value); |
||||
|
||||
// 更新数据库
|
||||
String dateStr = DateUtil.dateToString(new Date(), "yyyy-MM-dd HH:mm:ss"); |
||||
// BigDecimal curValue;
|
||||
// if (value instanceof Number) {
|
||||
// curValue = new BigDecimal(value.toString());
|
||||
// } else {
|
||||
// curValue = new BigDecimal(value.toString());
|
||||
// }
|
||||
|
||||
collectionParamsManageMapper.updateCollectionParamsManageById( |
||||
cpmId, |
||||
value.toString(), |
||||
dateStr |
||||
); |
||||
|
||||
log.info("手动写入成功: cpmId={}, registerAddr={}, value={}", cpmId, param.getRegisterAddr(), value); |
||||
return true; |
||||
|
||||
} catch (Exception e) { |
||||
log.error("手动写入失败: cpmId={}", cpmId, e); |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 根据deviceInstallId获取dataCom |
||||
*/ |
||||
private String getDataComByDeviceId(Long deviceInstallId) { |
||||
return gatewayManageMapper.getDataComByDeviceInstallId(deviceInstallId); |
||||
} |
||||
|
||||
/** |
||||
* 根据dataCom获取网关信息 |
||||
*/ |
||||
private GatewayManageEntity getGatewayByDataCom(String dataCom) { |
||||
List<GatewayManageEntity> gateways = gatewayManageMapper.queryS7Gateways(); |
||||
if (gateways != null) { |
||||
for (GatewayManageEntity gw : gateways) { |
||||
if (dataCom.equals(gw.getDataCom())) { |
||||
return gw; |
||||
} |
||||
} |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
/** |
||||
* 清理连接器缓存(可选,用于重启或维护) |
||||
*/ |
||||
public void clearConnectorCache() { |
||||
for (Map.Entry<String, S7ConnectorUtil> entry : connectorCache.entrySet()) { |
||||
try { |
||||
entry.getValue().disconnect(); |
||||
} catch (Exception e) { |
||||
log.error("断开S7连接异常: {}", entry.getKey(), e); |
||||
} |
||||
} |
||||
connectorCache.clear(); |
||||
log.info("S7连接器缓存已清理"); |
||||
} |
||||
} |
||||
@ -0,0 +1,543 @@
|
||||
package com.mh.user.s7; |
||||
|
||||
import com.github.s7connector.api.DaveArea; |
||||
import com.github.s7connector.api.S7Connector; |
||||
import com.github.s7connector.api.factory.S7ConnectorFactory; |
||||
import com.mh.user.utils.ExchangeStringUtil; |
||||
import lombok.Data; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
|
||||
import java.math.BigDecimal; |
||||
import java.util.regex.Matcher; |
||||
import java.util.regex.Pattern; |
||||
|
||||
/** |
||||
* S7 PLC通信工具类 |
||||
* 支持M、VB、VW、VD等地址类型的读写操作 |
||||
*/ |
||||
@Slf4j |
||||
public class S7ConnectorUtil { |
||||
|
||||
private S7Connector connector; |
||||
private String ipAddress; |
||||
private int rack; |
||||
private int slot; |
||||
|
||||
/** |
||||
* 构造函数 - 初始化S7连接 |
||||
* |
||||
* @param ipAddress PLC IP地址 |
||||
* @param rack 机架号(通常为0) |
||||
* @param slot 插槽号(通常为1) |
||||
*/ |
||||
public S7ConnectorUtil(String ipAddress, int rack, int slot) { |
||||
this.ipAddress = ipAddress; |
||||
this.rack = rack; |
||||
this.slot = slot; |
||||
connect(); |
||||
} |
||||
|
||||
/** |
||||
* 建立S7连接 |
||||
*/ |
||||
public void connect() { |
||||
try { |
||||
if (connector != null) { |
||||
disconnect(); |
||||
} |
||||
connector = S7ConnectorFactory |
||||
.buildTCPConnector() |
||||
.withHost(ipAddress) |
||||
.withRack(rack) |
||||
.withSlot(slot) |
||||
.build(); |
||||
log.info("S7 PLC连接成功: {} (rack={}, slot={})", ipAddress, rack, slot); |
||||
} catch (Exception e) { |
||||
log.error("S7 PLC连接失败: {}", ipAddress, e); |
||||
throw new RuntimeException("S7 PLC连接失败", e); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 断开S7连接 |
||||
*/ |
||||
public void disconnect() { |
||||
try { |
||||
if (connector != null) { |
||||
connector.close(); |
||||
log.info("S7 PLC连接已关闭: {}", ipAddress); |
||||
} |
||||
} catch (Exception e) { |
||||
log.error("S7 PLC断开连接异常: {}", ipAddress, e); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 根据寄存器地址读取数据 |
||||
* |
||||
* @param registerAddr 寄存器地址,如 M0.1, VB12, VW314, VD11 |
||||
* @return 读取的值 |
||||
*/ |
||||
public Object readData(String registerAddr) { |
||||
try { |
||||
AddressInfo addressInfo = parseAddress(registerAddr); |
||||
if (addressInfo == null) { |
||||
log.error("无法解析寄存器地址: {}", registerAddr); |
||||
return null; |
||||
} |
||||
|
||||
byte[] data = null; |
||||
Object result = null; |
||||
|
||||
switch (addressInfo.getArea()) { |
||||
case "M": |
||||
// M区是位存储区(Merkers/Flags),按字节读取
|
||||
data = connector.read(DaveArea.FLAGS, 0, 1, addressInfo.getByteOffset()); |
||||
if (data == null || data.length == 0) { |
||||
log.warn("读取M区数据为空: addr={}", registerAddr); |
||||
return null; |
||||
} |
||||
log.info("读取M区: addr={}, data=[{}], hex={}", |
||||
registerAddr, data[0] & 0xFF, bytesToHex(data)); |
||||
if (addressInfo.isBit()) { |
||||
// 如果是位地址,提取指定位
|
||||
boolean bitValue = getBit(data[0], addressInfo.getBitOffset()); |
||||
result = bitValue ? 1 : 0; |
||||
} else { |
||||
result = data[0] & 0xFF; |
||||
} |
||||
break; |
||||
|
||||
case "I": |
||||
// I区是输入区(Inputs),使用INPUTS
|
||||
data = connector.read(DaveArea.INPUTS, 0, 1, addressInfo.getByteOffset()); |
||||
if (data == null || data.length == 0) { |
||||
log.warn("读取I区数据为空: addr={}, 可能PLC未配置该输入点", registerAddr); |
||||
return 0; // 输入区无数据时返回0
|
||||
} |
||||
log.info("读取I区: addr={}, data=[{}], hex={}", |
||||
registerAddr, data[0] & 0xFF, bytesToHex(data)); |
||||
if (addressInfo.isBit()) { |
||||
boolean bitValue = getBit(data[0], addressInfo.getBitOffset()); |
||||
result = bitValue ? 1 : 0; |
||||
} else { |
||||
result = data[0] & 0xFF; |
||||
} |
||||
break; |
||||
|
||||
case "Q": |
||||
// Q区是输出区(Outputs),使用OUTPUTS
|
||||
data = connector.read(DaveArea.OUTPUTS, 0, 1, addressInfo.getByteOffset()); |
||||
if (data == null || data.length == 0) { |
||||
log.warn("读取Q区数据为空: addr={}, 可能PLC未配置该输出点", registerAddr); |
||||
return 0; // 输出区无数据时返回0
|
||||
} |
||||
log.info("读取Q区: addr={}, data=[{}], hex={}", |
||||
registerAddr, data[0] & 0xFF, bytesToHex(data)); |
||||
if (addressInfo.isBit()) { |
||||
boolean bitValue = getBit(data[0], addressInfo.getBitOffset()); |
||||
result = bitValue ? 1 : 0; |
||||
} else { |
||||
result = data[0] & 0xFF; |
||||
} |
||||
break; |
||||
|
||||
case "VB": |
||||
// V区字节 (DB块)
|
||||
data = connector.read(DaveArea.DB, 1, 1, addressInfo.getByteOffset()); |
||||
if (data == null || data.length == 0) { |
||||
log.warn("读取VB区数据为空: addr={}", registerAddr); |
||||
return null; |
||||
} |
||||
String string = bytesToHex(data).replace(" ", ""); |
||||
result = ExchangeStringUtil.hexToDec(string.substring(string.length() - 2)); |
||||
log.info("读取VB区: addr={}, data=[{}], hex={}", |
||||
registerAddr, data[0] & 0xFF, string); |
||||
break; |
||||
|
||||
case "VW": |
||||
// V区字(2字节) - 大端序
|
||||
data = connector.read(DaveArea.DB, 1, 2, addressInfo.getByteOffset()); |
||||
if (data == null || data.length < 2) { |
||||
log.warn("读取VW区数据为空或长度不足: addr={}", registerAddr); |
||||
return null; |
||||
} |
||||
String s = bytesToHex(data).replace(" ", ""); |
||||
// bytesToHex(data)获取最右边的2个字节
|
||||
result = ExchangeStringUtil.hexToDec(s.substring(s.length() - 4)); |
||||
log.info("读取VW区: addr={}, data=[{}, {}], hex={}, value={}", |
||||
registerAddr, data[0] & 0xFF, data[1] & 0xFF, s, result); |
||||
break; |
||||
|
||||
case "VD": |
||||
// V区双字(4字节) - 大端序浮点数
|
||||
data = connector.read(DaveArea.DB, 1, 4, addressInfo.getByteOffset()); |
||||
if (data == null || data.length < 4) { |
||||
log.warn("读取VD区数据为空或长度不足: addr={}", registerAddr); |
||||
return null; |
||||
} |
||||
String hex = bytesToHex(data).replace(" ", ""); |
||||
result = ExchangeStringUtil.hexToSingle(hex.substring(hex.length() - 8)); |
||||
log.info("读取VD区: addr={}, data=[{}, {}, {}, {}], hex={}, value={}", |
||||
registerAddr, data[0] & 0xFF, data[1] & 0xFF, data[2] & 0xFF, data[3] & 0xFF, |
||||
hex, result); |
||||
break; |
||||
|
||||
case "AIW": |
||||
// 模拟量输入字(2字节),只读,使用INPUTS - 大端序
|
||||
data = connector.read(DaveArea.INPUTS, 0, 2, addressInfo.getByteOffset()); |
||||
if (data == null || data.length < 2) { |
||||
log.warn("读取AIW区数据为空或长度不足: addr={}", registerAddr); |
||||
return 0; |
||||
} |
||||
result = bytesToWord(data); |
||||
log.info("读取AIW区: addr={}, data=[{}, {}], hex={}, value={}", |
||||
registerAddr, data[0] & 0xFF, data[1] & 0xFF, bytesToHex(data), result); |
||||
break; |
||||
|
||||
case "AQW": |
||||
// 模拟量输出字(2字节),可读写,使用OUTPUTS - 大端序
|
||||
data = connector.read(DaveArea.OUTPUTS, 0, 2, addressInfo.getByteOffset()); |
||||
if (data == null || data.length < 2) { |
||||
log.warn("读取AQW区数据为空或长度不足: addr={}", registerAddr); |
||||
return 0; |
||||
} |
||||
String aqwStr = bytesToHex(data).replace(" ",""); |
||||
result = ExchangeStringUtil.hexToDec(aqwStr.substring(aqwStr.length() - 4)); |
||||
log.info("读取AQW区: addr={}, data=[{}, {}], hex={}, value={}", |
||||
registerAddr, data[0] & 0xFF, data[1] & 0xFF, aqwStr, result); |
||||
break; |
||||
|
||||
default: |
||||
log.error("不支持的地址类型: {}", addressInfo.getArea()); |
||||
return null; |
||||
} |
||||
|
||||
log.info("读取成功: addr={}, value={}", registerAddr, result); |
||||
return result; |
||||
|
||||
} catch (Exception e) { |
||||
log.error("读取S7数据失败: {}", registerAddr, e); |
||||
return null; |
||||
} |
||||
} |
||||
|
||||
|
||||
/** |
||||
* 根据寄存器地址写入数据 |
||||
* |
||||
* @param registerAddr 寄存器地址,如 M0.1, VB12, VW314, VD11 |
||||
* @param value 要写入的值 |
||||
*/ |
||||
public void writeData(String registerAddr, Object value) { |
||||
try { |
||||
AddressInfo addressInfo = parseAddress(registerAddr); |
||||
if (addressInfo == null) { |
||||
log.error("无法解析寄存器地址: {}", registerAddr); |
||||
return; |
||||
} |
||||
|
||||
switch (addressInfo.getArea()) { |
||||
case "M": |
||||
if (addressInfo.isBit()) { |
||||
// 写入位
|
||||
int intValue = value instanceof Number ? ((Number) value).intValue() : Integer.parseInt(value.toString()); |
||||
writeBit(DaveArea.FLAGS, 0, addressInfo.getByteOffset(), addressInfo.getBitOffset(), intValue != 0); |
||||
} else { |
||||
// 写入字节
|
||||
int intValue = value instanceof Number ? ((Number) value).intValue() : Integer.parseInt(value.toString()); |
||||
byte[] data = {(byte) (intValue & 0xFF)}; |
||||
connector.write(DaveArea.FLAGS, 0, addressInfo.getByteOffset(), data); |
||||
} |
||||
break; |
||||
case "I": |
||||
// I区是输入区,通常只读,不建议写入
|
||||
log.warn("I区是输入区,通常只读,不建议写入: {}", registerAddr); |
||||
if (addressInfo.isBit()) { |
||||
int intValue = value instanceof Number ? ((Number) value).intValue() : Integer.parseInt(value.toString()); |
||||
writeBit(DaveArea.INPUTS, 0, addressInfo.getByteOffset(), addressInfo.getBitOffset(), intValue != 0); |
||||
} else { |
||||
int intValue = value instanceof Number ? ((Number) value).intValue() : Integer.parseInt(value.toString()); |
||||
byte[] data = {(byte) (intValue & 0xFF)}; |
||||
connector.write(DaveArea.INPUTS, 0, addressInfo.getByteOffset(), data); |
||||
} |
||||
break; |
||||
case "Q": |
||||
// Q区是输出区,可读写
|
||||
if (addressInfo.isBit()) { |
||||
int intValue = value instanceof Number ? ((Number) value).intValue() : Integer.parseInt(value.toString()); |
||||
writeBit(DaveArea.OUTPUTS, 0, addressInfo.getByteOffset(), addressInfo.getBitOffset(), intValue != 0); |
||||
} else { |
||||
int intValue = value instanceof Number ? ((Number) value).intValue() : Integer.parseInt(value.toString()); |
||||
byte[] data = {(byte) (intValue & 0xFF)}; |
||||
connector.write(DaveArea.OUTPUTS, 0, addressInfo.getByteOffset(), data); |
||||
} |
||||
break; |
||||
case "VB": |
||||
// 写入字节
|
||||
int vbValue = value instanceof Number ? ((Number) value).intValue() : Integer.parseInt(value.toString()); |
||||
byte[] vbData = {(byte) (vbValue & 0xFF)}; |
||||
connector.write(DaveArea.DB, 1, addressInfo.getByteOffset(), vbData); |
||||
break; |
||||
case "VW": |
||||
// 写入字(2字节)
|
||||
int vwValue = value instanceof Number ? ((Number) value).intValue() : Integer.parseInt(value.toString()); |
||||
byte[] vwData = wordToBytes(vwValue); |
||||
connector.write(DaveArea.DB, 1, addressInfo.getByteOffset(), vwData); |
||||
break; |
||||
case "VD": |
||||
// 写入双字(4字节)
|
||||
float vdValue; |
||||
if (value instanceof Number) { |
||||
vdValue = ((Number) value).floatValue(); |
||||
} else { |
||||
vdValue = Float.parseFloat(value.toString()); |
||||
} |
||||
byte[] vdData = dwordToBytes(vdValue); |
||||
connector.write(DaveArea.DB, 1, addressInfo.getByteOffset(), vdData); |
||||
break; |
||||
case "AIW": |
||||
// AIW是模拟量输入,通常只读,不建议写入
|
||||
log.warn("AIW是模拟量输入,通常只读,不建议写入: {}", registerAddr); |
||||
int aiwValue = value instanceof Number ? ((Number) value).intValue() : Integer.parseInt(value.toString()); |
||||
byte[] aiwData = wordToBytes(aiwValue); |
||||
connector.write(DaveArea.INPUTS, 0, addressInfo.getByteOffset(), aiwData); |
||||
break; |
||||
case "AQW": |
||||
// AQW是模拟量输出,可读写
|
||||
int aqwValue = value instanceof Number ? ((Number) value).intValue() : Integer.parseInt(value.toString()); |
||||
byte[] aqwData = wordToBytes(aqwValue); |
||||
connector.write(DaveArea.OUTPUTS, 0, addressInfo.getByteOffset(), aqwData); |
||||
break; |
||||
default: |
||||
log.error("不支持的地址类型: {}", addressInfo.getArea()); |
||||
} |
||||
log.info("写入S7数据成功: {} = {}", registerAddr, value); |
||||
} catch (Exception e) { |
||||
log.error("写入S7数据失败: {}", registerAddr, e); |
||||
throw new RuntimeException("写入S7数据失败", e); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 解析寄存器地址 |
||||
* |
||||
* @param registerAddr 寄存器地址字符串 |
||||
* @return 地址信息对象 |
||||
*/ |
||||
private AddressInfo parseAddress(String registerAddr) { |
||||
if (registerAddr == null || registerAddr.isEmpty()) { |
||||
return null; |
||||
} |
||||
|
||||
AddressInfo info = new AddressInfo(); |
||||
|
||||
// 匹配纯M地址 (如 M0, M10)
|
||||
Pattern mBytePattern = Pattern.compile("^M(\\d+)$"); |
||||
Matcher mByteMatcher = mBytePattern.matcher(registerAddr.toUpperCase()); |
||||
if (mByteMatcher.find()) { |
||||
info.setArea("M"); |
||||
info.setByteOffset(Integer.parseInt(mByteMatcher.group(1))); |
||||
info.setBit(false); |
||||
return info; |
||||
} |
||||
|
||||
// 匹配M位地址 (如 M0.1, M10.5)
|
||||
Pattern mBitPattern = Pattern.compile("^M(\\d+)\\.(\\d+)$"); |
||||
Matcher mBitMatcher = mBitPattern.matcher(registerAddr.toUpperCase()); |
||||
if (mBitMatcher.find()) { |
||||
info.setArea("M"); |
||||
info.setByteOffset(Integer.parseInt(mBitMatcher.group(1))); |
||||
info.setBitOffset(Integer.parseInt(mBitMatcher.group(2))); |
||||
info.setBit(true); |
||||
return info; |
||||
} |
||||
|
||||
// 匹配I字节地址 (如 I0, I1)
|
||||
Pattern iBytePattern = Pattern.compile("^I(\\d+)$"); |
||||
Matcher iByteMatcher = iBytePattern.matcher(registerAddr.toUpperCase()); |
||||
if (iByteMatcher.find()) { |
||||
info.setArea("I"); |
||||
info.setByteOffset(Integer.parseInt(iByteMatcher.group(1))); |
||||
info.setBit(false); |
||||
return info; |
||||
} |
||||
|
||||
// 匹配I位地址 (如 I0.1, I1.5)
|
||||
Pattern iBitPattern = Pattern.compile("^I(\\d+)\\.(\\d+)$"); |
||||
Matcher iBitMatcher = iBitPattern.matcher(registerAddr.toUpperCase()); |
||||
if (iBitMatcher.find()) { |
||||
info.setArea("I"); |
||||
info.setByteOffset(Integer.parseInt(iBitMatcher.group(1))); |
||||
info.setBitOffset(Integer.parseInt(iBitMatcher.group(2))); |
||||
info.setBit(true); |
||||
return info; |
||||
} |
||||
|
||||
// 匹配Q字节地址 (如 Q0, Q1)
|
||||
Pattern qBytePattern = Pattern.compile("^Q(\\d+)$"); |
||||
Matcher qByteMatcher = qBytePattern.matcher(registerAddr.toUpperCase()); |
||||
if (qByteMatcher.find()) { |
||||
info.setArea("Q"); |
||||
info.setByteOffset(Integer.parseInt(qByteMatcher.group(1))); |
||||
info.setBit(false); |
||||
return info; |
||||
} |
||||
|
||||
// 匹配Q位地址 (如 Q0.1, Q1.5)
|
||||
Pattern qBitPattern = Pattern.compile("^Q(\\d+)\\.(\\d+)$"); |
||||
Matcher qBitMatcher = qBitPattern.matcher(registerAddr.toUpperCase()); |
||||
if (qBitMatcher.find()) { |
||||
info.setArea("Q"); |
||||
info.setByteOffset(Integer.parseInt(qBitMatcher.group(1))); |
||||
info.setBitOffset(Integer.parseInt(qBitMatcher.group(2))); |
||||
info.setBit(true); |
||||
return info; |
||||
} |
||||
|
||||
// 匹配VB地址 (如 VB12)
|
||||
Pattern vbPattern = Pattern.compile("^VB(\\d+)$"); |
||||
Matcher vbMatcher = vbPattern.matcher(registerAddr.toUpperCase()); |
||||
if (vbMatcher.find()) { |
||||
info.setArea("VB"); |
||||
info.setByteOffset(Integer.parseInt(vbMatcher.group(1))); |
||||
info.setBit(false); |
||||
return info; |
||||
} |
||||
|
||||
// 匹配VW地址 (如 VW314)
|
||||
Pattern vwPattern = Pattern.compile("^VW(\\d+)$"); |
||||
Matcher vwMatcher = vwPattern.matcher(registerAddr.toUpperCase()); |
||||
if (vwMatcher.find()) { |
||||
info.setArea("VW"); |
||||
info.setByteOffset(Integer.parseInt(vwMatcher.group(1))); |
||||
info.setBit(false); |
||||
return info; |
||||
} |
||||
|
||||
// 匹配VD地址 (如 VD11)
|
||||
Pattern vdPattern = Pattern.compile("^VD(\\d+)$"); |
||||
Matcher vdMatcher = vdPattern.matcher(registerAddr.toUpperCase()); |
||||
if (vdMatcher.find()) { |
||||
info.setArea("VD"); |
||||
info.setByteOffset(Integer.parseInt(vdMatcher.group(1))); |
||||
info.setBit(false); |
||||
return info; |
||||
} |
||||
|
||||
// 匹配AIW地址 (模拟量输入字,如 AIW0, AIW64)
|
||||
Pattern aiwPattern = Pattern.compile("^AIW(\\d+)$"); |
||||
Matcher aiwMatcher = aiwPattern.matcher(registerAddr.toUpperCase()); |
||||
if (aiwMatcher.find()) { |
||||
info.setArea("AIW"); |
||||
info.setByteOffset(Integer.parseInt(aiwMatcher.group(1))); |
||||
info.setBit(false); |
||||
return info; |
||||
} |
||||
|
||||
// 匹配AQW地址 (模拟量输出字,如 AQW0, AQW64)
|
||||
Pattern aqwPattern = Pattern.compile("^AQW(\\d+)$"); |
||||
Matcher aqwMatcher = aqwPattern.matcher(registerAddr.toUpperCase()); |
||||
if (aqwMatcher.find()) { |
||||
info.setArea("AQW"); |
||||
info.setByteOffset(Integer.parseInt(aqwMatcher.group(1))); |
||||
info.setBit(false); |
||||
return info; |
||||
} |
||||
|
||||
log.error("无法识别的寄存器地址格式: {}", registerAddr); |
||||
return null; |
||||
} |
||||
|
||||
/** |
||||
* 从字节中获取指定位的值 |
||||
*/ |
||||
private boolean getBit(byte data, int bitPosition) { |
||||
return ((data >> bitPosition) & 0x01) == 1; |
||||
} |
||||
|
||||
/** |
||||
* 字节数组转十六进制字符串(用于调试) |
||||
*/ |
||||
private String bytesToHex(byte[] data) { |
||||
if (data == null || data.length == 0) { |
||||
return ""; |
||||
} |
||||
StringBuilder sb = new StringBuilder(); |
||||
for (byte b : data) { |
||||
sb.append(String.format("%02X ", b & 0xFF)); |
||||
} |
||||
return sb.toString().trim(); |
||||
} |
||||
|
||||
/** |
||||
* 写入指定位的值 |
||||
*/ |
||||
private void writeBit(DaveArea area, int dbNumber, int byteOffset, int bitPosition, boolean value) throws Exception { |
||||
// 先读取当前字节
|
||||
byte[] currentData = connector.read(area, dbNumber, 1, byteOffset); |
||||
byte currentByte = currentData[0]; |
||||
|
||||
// 修改指定位
|
||||
if (value) { |
||||
currentByte |= (1 << bitPosition); // 置位
|
||||
} else { |
||||
currentByte &= ~(1 << bitPosition); // 复位
|
||||
} |
||||
|
||||
// 写回
|
||||
connector.write(area, dbNumber, byteOffset, new byte[]{currentByte}); |
||||
} |
||||
|
||||
/** |
||||
* 字节数组转字(2字节,无符号) |
||||
*/ |
||||
private int bytesToWord(byte[] data) { |
||||
return ((data[0] & 0xFF) << 8) | (data[1] & 0xFF); |
||||
} |
||||
|
||||
/** |
||||
* 字转字节数组(2字节) |
||||
*/ |
||||
private byte[] wordToBytes(int value) { |
||||
return new byte[]{ |
||||
(byte) ((value >> 8) & 0xFF), |
||||
(byte) (value & 0xFF) |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* 字节数组转双字(4字节,浮点数) |
||||
*/ |
||||
private float bytesToDWord(byte[] data) { |
||||
int intBits = ((data[0] & 0xFF) << 24) | |
||||
((data[1] & 0xFF) << 16) | |
||||
((data[2] & 0xFF) << 8) | |
||||
(data[3] & 0xFF); |
||||
return Float.intBitsToFloat(intBits); |
||||
} |
||||
|
||||
/** |
||||
* 双字(浮点数)转字节数组(4字节) |
||||
*/ |
||||
private byte[] dwordToBytes(float value) { |
||||
int intBits = Float.floatToIntBits(value); |
||||
return new byte[]{ |
||||
(byte) ((intBits >> 24) & 0xFF), |
||||
(byte) ((intBits >> 16) & 0xFF), |
||||
(byte) ((intBits >> 8) & 0xFF), |
||||
(byte) (intBits & 0xFF) |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* 地址信息内部类 |
||||
*/ |
||||
@Data |
||||
private static class AddressInfo { |
||||
private String area; // 区域类型: M, VB, VW, VD
|
||||
private int byteOffset; // 字节偏移
|
||||
private int bitOffset; // 位偏移(仅M区位地址使用)
|
||||
private boolean isBit; // 是否为位地址
|
||||
} |
||||
} |
||||
Loading…
Reference in new issue