Browse Source

广州软件学院兼容PLC相关逻辑内容:

1、读写PLC点位;
2、控制界面兼容;
dev
3067418132@qq.com 6 days ago
parent
commit
e6b2518c94
  1. 260
      S7_PLC使用说明.md
  2. 303
      S7_PLC地址类型扩展说明.md
  3. 301
      S7_PLC数据解析问题排查.md
  4. 7
      user-service/pom.xml
  5. 5
      user-service/src/main/java/com/mh/user/constants/Constant.java
  6. 13
      user-service/src/main/java/com/mh/user/controller/DeviceOperateController.java
  7. 2
      user-service/src/main/java/com/mh/user/controller/HotWaterMonitorController.java
  8. 68
      user-service/src/main/java/com/mh/user/controller/S7PlcController.java
  9. 179
      user-service/src/main/java/com/mh/user/dto/HotWaterSupplyPumpControlVO.java
  10. 54
      user-service/src/main/java/com/mh/user/dto/HotWaterSystemControlVO.java
  11. 337
      user-service/src/main/java/com/mh/user/job/S7PlcCollectionJob.java
  12. 2
      user-service/src/main/java/com/mh/user/job/StartOrStopHotpumpJob.java
  13. 44
      user-service/src/main/java/com/mh/user/mapper/CollectionParamsManageMapper.java
  14. 34
      user-service/src/main/java/com/mh/user/mapper/GatewayManageMapper.java
  15. 543
      user-service/src/main/java/com/mh/user/s7/S7ConnectorUtil.java
  16. 4
      user-service/src/main/java/com/mh/user/serialport/SendAndReceiveByCom.java
  17. 2
      user-service/src/main/java/com/mh/user/service/DeviceControlService.java
  18. 194
      user-service/src/main/java/com/mh/user/service/impl/CollectionParamsManageServiceImpl.java
  19. 14
      user-service/src/main/java/com/mh/user/service/impl/DeviceControlServiceImpl.java
  20. 26
      user-service/src/main/java/com/mh/user/utils/Test.java
  21. 2
      user-service/src/main/resources/application-dev.yml
  22. 16
      user-service/src/main/resources/application-prod.yml
  23. 2
      user-service/src/main/resources/application.yml

260
S7_PLC使用说明.md

@ -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

303
S7_PLC地址类型扩展说明.md

@ -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

301
S7_PLC数据解析问题排查.md

@ -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

7
user-service/pom.xml

@ -179,6 +179,13 @@
<version>4.1.86.Final</version> <version>4.1.86.Final</version>
</dependency> </dependency>
<!-- S7 PLC通信库 -->
<dependency>
<groupId>com.github.s7connector</groupId>
<artifactId>s7connector</artifactId>
<version>2.1</version>
</dependency>
</dependencies> </dependencies>
<dependencyManagement> <dependencyManagement>
<dependencies> <dependencies>

5
user-service/src/main/java/com/mh/user/constants/Constant.java

@ -17,12 +17,17 @@ public class Constant {
public static final String WEATHER_DATA = "weather_data"; public static final String WEATHER_DATA = "weather_data";
public static final String COMMUNITY_TYPE_REAL_COM = "realCom"; public static final String COMMUNITY_TYPE_REAL_COM = "realCom";
public static final String COMMUNITY_TYPE_TCP = "tcp"; public static final String COMMUNITY_TYPE_TCP = "tcp";
public static final String COMMUNITY_TYPE_S7 = "S7";
public static boolean CONTROL_WEB_FLAG = false; public static boolean CONTROL_WEB_FLAG = false;
public static boolean SEND_STATUS = false; // 指令发送状态 public static boolean SEND_STATUS = false; // 指令发送状态
public static volatile boolean FLAG = false; public static volatile boolean FLAG = false;
public static volatile boolean WEB_FLAG = false; // 判断是否有前端指令下发 public static volatile boolean WEB_FLAG = false; // 判断是否有前端指令下发
public static volatile boolean PLC_FLAG = false;
public static volatile boolean WEB_PLC_FLAG = false; // 判断是否有前端指令下发
public static final String FAIL = "fail"; public static final String FAIL = "fail";
public static final String SUCCESS = "success"; public static final String SUCCESS = "success";

13
user-service/src/main/java/com/mh/user/controller/DeviceOperateController.java

@ -10,6 +10,7 @@ import com.mh.user.entity.ControlSetEntity;
import com.mh.user.entity.DeviceCodeParamEntity; import com.mh.user.entity.DeviceCodeParamEntity;
import com.mh.user.entity.DeviceInstallEntity; import com.mh.user.entity.DeviceInstallEntity;
import com.mh.user.entity.PumpSetEntity; import com.mh.user.entity.PumpSetEntity;
import com.mh.user.job.S7PlcCollectionJob;
import com.mh.user.model.DeviceModel; import com.mh.user.model.DeviceModel;
import com.mh.user.model.SerialPortModel; import com.mh.user.model.SerialPortModel;
import com.mh.user.serialport.SerialPortSingle2; import com.mh.user.serialport.SerialPortSingle2;
@ -54,6 +55,10 @@ public class DeviceOperateController {
@Autowired @Autowired
private IMqttGatewayService iMqttGatewayService; private IMqttGatewayService iMqttGatewayService;
@Autowired
private S7PlcCollectionJob s7PlcCollectionJob;
//操作设备 //操作设备
@SysLogger(title = "控制管理", optDesc = "设置设备参数值") @SysLogger(title = "控制管理", optDesc = "设置设备参数值")
@PostMapping(value = "/operate") @PostMapping(value = "/operate")
@ -92,6 +97,14 @@ public class DeviceOperateController {
iMqttGatewayService.publish(sendTopic, sendOrder, 0); iMqttGatewayService.publish(sendTopic, sendOrder, 0);
} }
} }
} else if (deviceControlService.isS7Control(params)) {
for (SerialPortModel serialPortModel :
params) {
boolean s7Write = s7PlcCollectionJob.writeData(Long.valueOf(serialPortModel.getCpmId()), serialPortModel.getDataValue());
if (!s7Write) {
return HttpResult.error(500, "fail");
}
}
} else { } else {
String returnStr = deviceControlService.readOrWriteDevice(params, Constant.WRITE); String returnStr = deviceControlService.readOrWriteDevice(params, Constant.WRITE);
if (!StringUtils.isBlank(returnStr) && "fail".equals(returnStr)) { if (!StringUtils.isBlank(returnStr) && "fail".equals(returnStr)) {

2
user-service/src/main/java/com/mh/user/controller/HotWaterMonitorController.java

@ -43,7 +43,7 @@ public class HotWaterMonitorController {
* @return * @return
*/ */
@GetMapping("/operateList") @GetMapping("/operateList")
public HttpResult operateList(@RequestParam("buildingId") String buildingId, @RequestParam("deviceInstallId") Integer deviceInstallId) { public HttpResult operateList(@RequestParam("buildingId") String buildingId, @RequestParam(value = "deviceInstallId", required = false) Integer deviceInstallId) {
if (deviceInstallId == null) { if (deviceInstallId == null) {
List<HotWaterControlDTO> list = collectionParamsManageService.operateList(buildingId); List<HotWaterControlDTO> list = collectionParamsManageService.operateList(buildingId);
return HttpResult.ok(list); return HttpResult.ok(list);

68
user-service/src/main/java/com/mh/user/controller/S7PlcController.java

@ -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());
}
}
}

179
user-service/src/main/java/com/mh/user/dto/HotWaterSupplyPumpControlVO.java

@ -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();
}
}

54
user-service/src/main/java/com/mh/user/dto/HotWaterSystemControlVO.java

@ -27,6 +27,12 @@ public class HotWaterSystemControlVO {
private int timeSet; private int timeSet;
private String timeSetId; private String timeSetId;
/**
* 校时手自动切换22
*/
private int timeSetAuto;
private String timeSetAutoId;
// 水箱温度 2 // 水箱温度 2
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "0") @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "0")
private BigDecimal tankTemp; private BigDecimal tankTemp;
@ -97,6 +103,26 @@ public class HotWaterSystemControlVO {
private int yearTimeSet; private int yearTimeSet;
private String yearTimeSetId; private String yearTimeSetId;
// 自动_分_写 8
private int minTimeSetAuto;
private String minTimeSetAutoId;
// 自动_时_写 9
private int hourTimeSetAuto;
private String hourTimeSetAutoId;
// 自动_日_写 9
private int dayTimeSetAuto;
private String dayTimeSetAutoId;
// 自动_月_写 10
private int monthTimeSetAuto;
private String monthTimeSetAutoId;
// 自动_年_写 11
private int yearTimeSetAuto;
private String yearTimeSetAutoId;
private int orderNum; private int orderNum;
/** /**
@ -105,6 +131,34 @@ public class HotWaterSystemControlVO {
private int reset; private int reset;
private String resetId; private String resetId;
/**
* pressureSet 压力设置
* @return
*/
private int pressureSet;
private String pressureSetId;
/**
* 单箱液位
* @return
*/
private int singleBoxLevel;
private String singleBoxLevelId;
/**
* 多箱液位
* @return
*/
private int multiBoxLevel;
private String multiBoxLevelId;
/**
* 水箱高度 26
* @return
*/
private int tankHeight;
private String tankHeightId;
@Override @Override
public String toString() { public String toString() {
return new StringJoiner(", ", HotWaterSystemControlVO.class.getSimpleName() + "[", "]") return new StringJoiner(", ", HotWaterSystemControlVO.class.getSimpleName() + "[", "]")

337
user-service/src/main/java/com/mh/user/job/S7PlcCollectionJob.java

@ -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定时采集任务
* 支持MVBVWVD等地址类型的读写操作
*
* @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连接器缓存已清理");
}
}

2
user-service/src/main/java/com/mh/user/job/StartOrStopHotpumpJob.java

@ -40,7 +40,7 @@ public class StartOrStopHotpumpJob {
this.deviceControlService = deviceControlService; this.deviceControlService = deviceControlService;
} }
@Scheduled(cron = "0 0/1 * * * ?") // @Scheduled(cron = "0 0/1 * * * ?")
public void startOrStopHotpump() { public void startOrStopHotpump() {
log.info("定时开关机热泵开始"); log.info("定时开关机热泵开始");
// 查询定时时间 // 查询定时时间

44
user-service/src/main/java/com/mh/user/mapper/CollectionParamsManageMapper.java

@ -380,4 +380,48 @@ public interface CollectionParamsManageMapper extends BaseMapper<CollectionParam
@Result(column = "order_num", property = "orderNum") @Result(column = "order_num", property = "orderNum")
}) })
List<HotWaterControlZDListVO> selectHotWaterByDeviceInstallId(String buildingId, Integer deviceInstallId); List<HotWaterControlZDListVO> selectHotWaterByDeviceInstallId(String buildingId, Integer deviceInstallId);
/**
* 根据dataCom查询对应的采集参数列表(S7协议使用)
* @param dataCom 通讯口
* @return 采集参数列表
*/
@Select("SELECT cpm.* FROM collection_params_manage cpm " +
"JOIN device_install di ON cpm.device_install_id = di.id " +
"WHERE di.data_com = #{dataCom} AND cpm.is_use = 1 " +
"ORDER BY cpm.device_install_id, cpm.order_num")
@Results({
@Result(column = "id", property = "id"),
@Result(column = "create_time", property = "createTime"),
@Result(column = "update_time", property = "updateTime"),
@Result(column = "device_install_id", property = "deviceInstallId"),
@Result(column = "register_addr", property = "registerAddr"),
@Result(column = "func_code", property = "funcCode"),
@Result(column = "mt_ratio", property = "mtRatio"),
@Result(column = "mt_init_value", property = "mtInitValue"),
@Result(column = "digits", property = "digits"),
@Result(column = "data_type", property = "dataType"),
@Result(column = "cur_value", property = "curValue"),
@Result(column = "cur_time", property = "curTime"),
@Result(column = "mt_is_sum", property = "mtIsSum"),
@Result(column = "unit", property = "unit"),
@Result(column = "order_num", property = "orderNum"),
@Result(column = "remark", property = "remark"),
@Result(column = "register_size", property = "registerSize"),
@Result(column = "is_use", property = "isUse"),
@Result(column = "other_name", property = "otherName"),
@Result(column = "grade", property = "grade"),
@Result(column = "param_type_id", property = "paramTypeId"),
@Result(column = "param_type_group_id", property = "paramTypeGroupId"),
@Result(column = "collection_type", property = "collectionType"),
@Result(column = "quality", property = "quality"),
@Result(column = "building_id", property = "buildingId")
})
List<CollectionParamsManageEntity> selectCPMByDataCom(String dataCom);
@Update("update collection_params_manage set cur_value = #{value}, cur_time = #{dateStr} where id = #{cpmId} ")
void updateCollectionParamsManageById(@Param("cpmId") Long cpmId,
@Param("value") String value,
@Param("dateStr") String dateStr);
} }

34
user-service/src/main/java/com/mh/user/mapper/GatewayManageMapper.java

@ -121,4 +121,38 @@ public interface GatewayManageMapper {
@Select("select top 1 building_id from device_install di join gateway_manage gm on di.data_com = gm.data_com and gm.sn = #{sn}") @Select("select top 1 building_id from device_install di join gateway_manage gm on di.data_com = gm.data_com and gm.sn = #{sn}")
String queryBuildingIdBySn(String sn); String queryBuildingIdBySn(String sn);
/**
* 根据deviceInstallId查询对应的dataCom
* @param deviceInstallId 设备安装ID
* @return dataCom
*/
@Select("select top 1 di.data_com from device_install di where di.id = #{deviceInstallId}")
String getDataComByDeviceInstallId(@Param("deviceInstallId") Long deviceInstallId);
/**
* 查询所有S7类型的网关
* @return S7网关列表
*/
@Select("select * from gateway_manage where community_type = 'S7' and grade = '1'")
@Results(id="s7Rs",value = {
@Result(column = "id", property = "id"),
@Result(column = "gateway_name", property = "gatewayName"),
@Result(column = "gateway_ip", property = "gatewayIP"),
@Result(column = "gateway_address", property = "gatewayAddress"),
@Result(column = "gateway_port", property = "gatewayPort"),
@Result(column = "grade", property = "grade"),
@Result(column = "internet_card", property = "internetCard"),
@Result(column = "operator", property = "operator"),
@Result(column = "create_date", property = "createDate"),
@Result(column = "connect_date", property = "connectDate"),
@Result(column = "remarks", property = "remarks"),
@Result(column = "heart_beat", property = "heartBeat"),
@Result(column = "imei", property = "imei"),
@Result(column = "sn", property = "sn"),
@Result(column = "data_com", property = "dataCom"),
@Result(column = "thread", property = "thread"),
@Result(column = "community_type", property = "communityType")
})
List<GatewayManageEntity> queryS7Gateways();
} }

543
user-service/src/main/java/com/mh/user/s7/S7ConnectorUtil.java

@ -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通信工具类
* 支持MVBVWVD等地址类型的读写操作
*/
@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; // 是否为位地址
}
}

4
user-service/src/main/java/com/mh/user/serialport/SendAndReceiveByCom.java

@ -117,13 +117,13 @@ public class SendAndReceiveByCom {
byte[] bytes = SerialTool.readFromPort(serialPort); byte[] bytes = SerialTool.readFromPort(serialPort);
Date date1 = new Date(); Date date1 = new Date();
String dateStr = DateUtil.dateToString(date1, "yyyy-MM-dd HH:mm:ss"); String dateStr = DateUtil.dateToString(date1, "yyyy-MM-dd HH:mm:ss");
if (bytes == null) { if (bytes == null || bytes.length == 0) {
SerialTool.closePort(serialPort); SerialTool.closePort(serialPort);
log.info("串口{}没有数据返回!{}", serialPort.getName(), i); log.info("串口{}没有数据返回!{}", serialPort.getName(), i);
SendAndReceiveByTcp.printLog(deviceAddr, deviceType, buildingId, buildingName, dateStr, log, deviceInstallService, nowDataService, comName); SendAndReceiveByTcp.printLog(deviceAddr, deviceType, buildingId, buildingName, dateStr, log, deviceInstallService, nowDataService, comName);
} }
// 处理返回来的数据报文 // 处理返回来的数据报文
dealReceiveData(dateStr, serialPort, i, deviceAddr, deviceType, registerAddr, brand, buildingId, buildingName, bytes, device, null); dealReceiveData(dateStr, serialPort, i, deviceAddr, deviceType, registerAddr, brand, buildingId, buildingName, bytes, device, deviceManageEntityList.get(i));
} catch (Exception e) { } catch (Exception e) {
if (null != serialPort) { if (null != serialPort) {

2
user-service/src/main/java/com/mh/user/service/DeviceControlService.java

@ -25,4 +25,6 @@ public interface DeviceControlService {
String operationDevice(SerialPortModel params); String operationDevice(SerialPortModel params);
String getSn(SerialPortModel serialPortModel); String getSn(SerialPortModel serialPortModel);
boolean isS7Control(List<SerialPortModel> params);
} }

194
user-service/src/main/java/com/mh/user/service/impl/CollectionParamsManageServiceImpl.java

@ -344,6 +344,8 @@ public class CollectionParamsManageServiceImpl implements CollectionParamsManage
return createHotPumpControlVO(dlEntry, dlItems, parentDto); return createHotPumpControlVO(dlEntry, dlItems, parentDto);
case "循环泵": case "循环泵":
return createCircuitPumpControlVO(dlEntry, dlItems, parentDto); return createCircuitPumpControlVO(dlEntry, dlItems, parentDto);
case "供水泵":
return createSupplyPumpControlVO(dlEntry, dlItems, parentDto);
case "回水泵": case "回水泵":
return createBackPumpControlVO(dlEntry, dlItems, parentDto); return createBackPumpControlVO(dlEntry, dlItems, parentDto);
case "设备校准": case "设备校准":
@ -356,6 +358,152 @@ public class CollectionParamsManageServiceImpl implements CollectionParamsManage
} }
} }
private HotWaterSupplyPumpControlVO createSupplyPumpControlVO(
Map.Entry<String, List<HotWaterControlListVO>> dlEntry,
List<HotWaterControlListVO> dlItems,
HotWaterControlDTO parentDto) {
HotWaterSupplyPumpControlVO supplyPumpVo = new HotWaterSupplyPumpControlVO();
setupBasicDeviceInfo(supplyPumpVo, dlEntry, dlItems, parentDto);
dlItems.forEach(item -> {
switch (item.getParamTypeId()) {
case "1":
// 启停控制
supplyPumpVo.setStartStopControl(item.getCurValue().intValue());
supplyPumpVo.setStartStopControlId(item.getCpmId());
break;
case "2":
// 运行状态
supplyPumpVo.setRunState(item.getCurValue().intValue());
supplyPumpVo.setRunStateId(item.getCpmId());
break;
case "3":
// 故障状态
supplyPumpVo.setFault(item.getCurValue().intValue());
supplyPumpVo.setFaultId(item.getCpmId());
break;
case "4":
// 时控
handleSupplyPumpTimeParameters(supplyPumpVo, item);
break;
case "7":
// 温度设置
supplyPumpVo.setTempSet(item.getCurValue());
supplyPumpVo.setTempSetId(item.getCpmId());
break;
case "15":
// 时间
supplyPumpVo.setOpenDelayTime(item.getCurValue().intValue());
supplyPumpVo.setOpenDelayTimeId(item.getCpmId());
break;
case "16":
// 累积运行时间
if (item.getOtherName().contains("小时")) {
supplyPumpVo.setRunTime(item.getCurValue());
supplyPumpVo.setRunTimeId(item.getCpmId());
}
break;
case "21":
// 一键启动
supplyPumpVo.setStartOneKey(item.getCurValue().intValue());
supplyPumpVo.setStartOneKeyId(item.getCpmId());
break;
case "22":
// 手动自动切换
supplyPumpVo.setManualAutoSwitch(item.getCurValue().intValue());
supplyPumpVo.setManualAutoSwitchId(item.getCpmId());
break;
case "25":
// 两台回水泵启动
supplyPumpVo.setTwoPumpStart(item.getCurValue().intValue());
supplyPumpVo.setTwoPumpStartId(item.getCpmId());
break;
case "26":
// 温度上限
supplyPumpVo.setTempSetUpperLimit(item.getCurValue());
supplyPumpVo.setTempSetUpperLimitId(item.getCpmId());
break;
case "27":
// 温度下限
supplyPumpVo.setTempSetLowerLimit(item.getCurValue());
supplyPumpVo.setTempSetLowerLimitId(item.getCpmId());
break;
default:
break;
}
});
if (supplyPumpVo.getOneHourTimeOpenSetOneId() != null
&& supplyPumpVo.getOneHourTimeCloseSetOneId() != null
&& supplyPumpVo.getOneMinTimeOpenSetOneId() != null
&& supplyPumpVo.getOneMinTimeCloseSetOneId() != null) {
// 设置时分开写入oneHourMinTimeOpenSetOneStr,oneHourMinTimeCloseSetOneStr
supplyPumpVo.setOneHourMinTimeOpenSetOneStr(String.format("%02d:%02d", supplyPumpVo.getOneHourTimeOpenSetOne(), supplyPumpVo.getOneMinTimeOpenSetOne()));
supplyPumpVo.setOneHourMinTimeCloseSetOneStr(String.format("%02d:%02d", supplyPumpVo.getOneHourTimeCloseSetOne(), supplyPumpVo.getOneMinTimeCloseSetOne()));
}
if (supplyPumpVo.getOneHourTimeOpenSetTwoId() != null
&& supplyPumpVo.getOneHourTimeCloseSetTwoId() != null
&& supplyPumpVo.getOneMinTimeOpenSetTwoId() != null
&& supplyPumpVo.getOneMinTimeCloseSetTwoId() != null) {
// 获取时分开写入oneHourMinTimeOpenSetTwoStr,oneHourMinTimeCloseSetTwoStr
supplyPumpVo.setOneHourMinTimeOpenSetTwoStr(String.format("%02d:%02d", supplyPumpVo.getOneHourTimeOpenSetTwo(), supplyPumpVo.getOneMinTimeOpenSetTwo()));
supplyPumpVo.setOneHourMinTimeCloseSetTwoStr(String.format("%02d:%02d", supplyPumpVo.getOneHourTimeCloseSetTwo(), supplyPumpVo.getOneMinTimeCloseSetTwo()));
}
if (supplyPumpVo.getOneHourTimeOpenSetThreeId() != null
&& supplyPumpVo.getOneHourTimeCloseSetThreeId() != null
&& supplyPumpVo.getOneMinTimeOpenSetThreeId() != null
&& supplyPumpVo.getOneMinTimeCloseSetThreeId() != null) {
// 获取时分开写入oneHourMinTimeOpenSetThreeStr,oneHourMinTimeCloseSetThreeStr
supplyPumpVo.setOneHourMinTimeOpenSetThreeStr(String.format("%02d:%02d", supplyPumpVo.getOneHourTimeOpenSetThree(), supplyPumpVo.getOneMinTimeOpenSetThree()));
supplyPumpVo.setOneHourMinTimeCloseSetThreeStr(String.format("%02d:%02d", supplyPumpVo.getOneHourTimeCloseSetThree(), supplyPumpVo.getOneMinTimeCloseSetThree()));
}
return supplyPumpVo;
}
private void handleSupplyPumpTimeParameters(HotWaterSupplyPumpControlVO backPumpVo, HotWaterControlListVO item) {
String otherName = item.getOtherName();
int value = item.getCurValue().intValue();
String cpmId = item.getCpmId();
if (otherName.contains("定时_时开1")) {
backPumpVo.setOneHourTimeOpenSetOne(value);
backPumpVo.setOneHourTimeOpenSetOneId(cpmId);
} else if (otherName.contains("定时_时关1")) {
backPumpVo.setOneHourTimeCloseSetOne(value);
backPumpVo.setOneHourTimeCloseSetOneId(cpmId);
} else if (otherName.contains("定时_分开1")) {
backPumpVo.setOneMinTimeOpenSetOne(value);
backPumpVo.setOneMinTimeOpenSetOneId(cpmId);
} else if (otherName.contains("定时_分关1")) {
backPumpVo.setOneMinTimeCloseSetOne(value);
backPumpVo.setOneMinTimeCloseSetOneId(cpmId);
} else if (otherName.contains("定时_时开2")) {
backPumpVo.setOneHourTimeOpenSetTwo(value);
backPumpVo.setOneHourTimeOpenSetTwoId(cpmId);
} else if (otherName.contains("定时_时关2")) {
backPumpVo.setOneHourTimeCloseSetTwo(value);
backPumpVo.setOneHourTimeCloseSetTwoId(cpmId);
} else if (otherName.contains("定时_分开2")) {
backPumpVo.setOneMinTimeOpenSetTwo(value);
backPumpVo.setOneMinTimeOpenSetTwoId(cpmId);
} else if (otherName.contains("定时_分关2")) {
backPumpVo.setOneMinTimeCloseSetTwo(value);
backPumpVo.setOneMinTimeCloseSetTwoId(cpmId);
} else if (otherName.contains("定时_时开3")) {
backPumpVo.setOneHourTimeOpenSetThree(value);
backPumpVo.setOneHourTimeOpenSetThreeId(cpmId);
} else if (otherName.contains("定时_时关3")) {
backPumpVo.setOneHourTimeCloseSetThree(value);
backPumpVo.setOneHourTimeCloseSetThreeId(cpmId);
} else if (otherName.contains("定时_分开3")) {
backPumpVo.setOneMinTimeOpenSetThree(value);
backPumpVo.setOneMinTimeOpenSetThreeId(cpmId);
} else if (otherName.contains("定时_分关3")) {
backPumpVo.setOneMinTimeCloseSetThree(value);
backPumpVo.setOneMinTimeCloseSetThreeId(cpmId);
}
}
private HotWaterSystemControlVO createSystemControlVO( private HotWaterSystemControlVO createSystemControlVO(
Map.Entry<String, List<HotWaterControlListVO>> dlEntry, Map.Entry<String, List<HotWaterControlListVO>> dlEntry,
List<HotWaterControlListVO> dlItems, List<HotWaterControlListVO> dlItems,
@ -367,15 +515,28 @@ public class CollectionParamsManageServiceImpl implements CollectionParamsManage
dlItems.forEach(item -> { dlItems.forEach(item -> {
switch (item.getParamTypeId()) { switch (item.getParamTypeId()) {
case "1": case "1":
if (item.getOtherName().contains("自动")) {
break;
}
// 时间写入控制 // 时间写入控制
vo.setTimeSet(item.getCurValue().intValue()); vo.setTimeSet(item.getCurValue().intValue());
vo.setTimeSetId(item.getCpmId()); vo.setTimeSetId(item.getCpmId());
break; break;
case "22":
// 校时手自动切换
vo.setTimeSetAuto(item.getCurValue().intValue());
vo.setTimeSetAutoId(item.getCpmId());
break;
case "5": case "5":
// 压力 // 压力
vo.setPressure(item.getCurValue()); vo.setPressure(item.getCurValue());
vo.setPressureId(item.getCpmId()); vo.setPressureId(item.getCpmId());
break; break;
case "27":
// 压力设置
vo.setPressureSet(item.getCurValue().intValue());
vo.setPressureSetId(item.getCpmId());
break;
case "6": case "6":
case "12": case "12":
// 回水温度 // 回水温度
@ -396,6 +557,19 @@ public class CollectionParamsManageServiceImpl implements CollectionParamsManage
// 时间 // 时间
handleSystemTimeParameters(vo, item); handleSystemTimeParameters(vo, item);
break; break;
case "26":
// 水箱高度和液位
if (item.getOtherName().contains("单箱液位")) {
vo.setSingleBoxLevel(item.getCurValue().intValue());
vo.setSingleBoxLevelId(item.getCpmId());
} else if (item.getOtherName().contains("多箱液位")) {
vo.setMultiBoxLevel(item.getCurValue().intValue());
vo.setMultiBoxLevelId(item.getCpmId());
} else if (item.getOtherName().contains("高度")) {
vo.setTankHeight(item.getCurValue().intValue());
vo.setTankHeightId(item.getCpmId());
}
break;
case "28": case "28":
// modbus 重置复位 // modbus 重置复位
vo.setReset(item.getCurValue().intValue()); vo.setReset(item.getCurValue().intValue());
@ -470,6 +644,26 @@ public class CollectionParamsManageServiceImpl implements CollectionParamsManage
vo.setMinTimeSet(value); vo.setMinTimeSet(value);
vo.setMinTimeSetId(cpmId); vo.setMinTimeSetId(cpmId);
break; break;
case "自动_年_写":
vo.setYearTimeSetAuto(Integer.parseInt("20" + value));
vo.setYearTimeSetAutoId(cpmId);
break;
case "自动_月_写":
vo.setMonthTimeSetAuto(value);
vo.setMonthTimeSetAutoId(cpmId);
break;
case "自动_日_写":
vo.setDayTimeSetAuto(value);
vo.setDayTimeSetAutoId(cpmId);
break;
case "自动_时_写":
vo.setHourTimeSetAuto(value);
vo.setHourTimeSetAutoId(cpmId);
break;
case "自动_分_写":
vo.setMinTimeSetAuto(value);
vo.setMinTimeSetAutoId(cpmId);
break;
default: default:
break; break;
} }

14
user-service/src/main/java/com/mh/user/service/impl/DeviceControlServiceImpl.java

@ -70,6 +70,20 @@ public class DeviceControlServiceImpl implements DeviceControlService {
return false; return false;
} }
@Override
public boolean isS7Control(List<SerialPortModel> params) {
if (!params.isEmpty()) {
for (SerialPortModel serialPortModel : params) {
// 判断是否是mqtt通讯
String communicationType = collectionParamsManageMapper.selectCommunicationType(serialPortModel.getCpmId());
if (Constant.COMMUNITY_TYPE_S7.equals(communicationType)) {
return true;
}
}
}
return false;
}
@Override @Override
public String operationDevice(SerialPortModel params) { public String operationDevice(SerialPortModel params) {
// 拼接发送的报文 // 拼接发送的报文

26
user-service/src/main/java/com/mh/user/utils/Test.java

@ -9,6 +9,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import java.io.BufferedWriter; import java.io.BufferedWriter;
import java.io.IOException; import java.io.IOException;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
@ -18,6 +19,8 @@ import java.text.ParseException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Calendar; import java.util.Calendar;
import java.util.Date; import java.util.Date;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** /**
* @author ljf * @author ljf
@ -113,7 +116,28 @@ public class Test {
} }
public static void main(String[] args) throws ParseException, IOException { public static void main(String[] args) throws ParseException, IOException {
String aqwStr = "8A 00 00 3F 93 33 33 41 50 00 00 3E 5D DD DE 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 42 8A 00 00 3F 93 33 33 41 50 00 00 3E 5D DD DE 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 6C 00 15 9A 00 00 00 40 80 00 00 00 00 26 06 25 11 07 07 00 05 00 00 1A";
aqwStr = aqwStr.replace(" ","");
aqwStr = aqwStr.substring(aqwStr.length() - 4);
System.out.println(aqwStr);
String result = ExchangeStringUtil.hexToDec(aqwStr);
System.out.println(result);
// 匹配AIW地址 (模拟量输入字,如 AIW0, AIW64)
Pattern aiwPattern = Pattern.compile("^VD(\\d+)$");
Matcher aiwMatcher = aiwPattern.matcher("VD0".toUpperCase());
if (aiwMatcher.find()) {
System.out.println("VD");
System.out.println(Integer.parseInt(aiwMatcher.group(1)));
System.out.println (false);
}
Object value = "89";
BigDecimal curValue;
if (value instanceof Number) {
curValue = new BigDecimal(value.toString());
} else {
log.warn("不支持的数据类型: registerAddr={}, value={}", "", value);
return;
}
} }

2
user-service/src/main/resources/application-dev.yml

@ -22,7 +22,7 @@ spring:
type: com.alibaba.druid.pool.DruidDataSource type: com.alibaba.druid.pool.DruidDataSource
druid: druid:
#添加allowMultiQueries=true 在批量更新时才不会出错 #添加allowMultiQueries=true 在批量更新时才不会出错
url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=chws_bsdz;allowMultiQueries=true;encrypt=false url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=chws_gr;allowMultiQueries=true;encrypt=false
driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
username: sa username: sa
password: mh@803 password: mh@803

16
user-service/src/main/resources/application-prod.yml

@ -75,16 +75,16 @@ spring:
# password: chws_gw@803 # password: chws_gw@803
# # 华软江门 # # 华软江门
# url: jdbc:sqlserver://127.0.0.1:57238;DatabaseName=chws_jm;allowMultiQueries=true;encrypt=false url: jdbc:sqlserver://127.0.0.1:57238;DatabaseName=chws_jm;allowMultiQueries=true;encrypt=false
# driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
# username: chws_jm username: chws_jm
# password: Mhtech@803 password: Mhtech@803
# 珠海北师大 # 珠海北师大
url: jdbc:sqlserver://127.0.0.1:8033;DatabaseName=chws_bsdz;allowMultiQueries=true;encrypt=false # url: jdbc:sqlserver://127.0.0.1:8033;DatabaseName=chws_bsdz;allowMultiQueries=true;encrypt=false
driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver # driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
username: chws_bsdz # username: chws_bsdz
password: Mhtech@803803 # password: Mhtech@803803
# #南方学院 # #南方学院
# url: jdbc:sqlserver://127.0.0.1:8033;DatabaseName=chws_nfxy;allowMultiQueries=true;encrypt=false # url: jdbc:sqlserver://127.0.0.1:8033;DatabaseName=chws_nfxy;allowMultiQueries=true;encrypt=false

2
user-service/src/main/resources/application.yml

@ -1,6 +1,6 @@
spring: spring:
profiles: profiles:
active: prod active: dev
mvc: mvc:
pathmatch: pathmatch:
matching-strategy: ant_path_matcher matching-strategy: ant_path_matcher

Loading…
Cancel
Save