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