From e6b2518c94dcafd6c15bd8bf4dcc7147a7e9dd95 Mon Sep 17 00:00:00 2001 From: "3067418132@qq.com" Date: Thu, 25 Jun 2026 17:37:52 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B9=BF=E5=B7=9E=E8=BD=AF=E4=BB=B6=E5=AD=A6?= =?UTF-8?q?=E9=99=A2=E5=85=BC=E5=AE=B9PLC=E7=9B=B8=E5=85=B3=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E5=86=85=E5=AE=B9=EF=BC=9A=201=E3=80=81=E8=AF=BB?= =?UTF-8?q?=E5=86=99PLC=E7=82=B9=E4=BD=8D=EF=BC=9B=202=E3=80=81=E6=8E=A7?= =?UTF-8?q?=E5=88=B6=E7=95=8C=E9=9D=A2=E5=85=BC=E5=AE=B9=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- S7_PLC使用说明.md | 260 +++++++++ S7_PLC地址类型扩展说明.md | 303 ++++++++++ S7_PLC数据解析问题排查.md | 301 ++++++++++ user-service/pom.xml | 7 + .../java/com/mh/user/constants/Constant.java | 5 + .../controller/DeviceOperateController.java | 13 + .../controller/HotWaterMonitorController.java | 2 +- .../mh/user/controller/S7PlcController.java | 68 +++ .../user/dto/HotWaterSupplyPumpControlVO.java | 179 ++++++ .../mh/user/dto/HotWaterSystemControlVO.java | 54 ++ .../com/mh/user/job/S7PlcCollectionJob.java | 337 +++++++++++ .../mh/user/job/StartOrStopHotpumpJob.java | 2 +- .../mapper/CollectionParamsManageMapper.java | 44 ++ .../mh/user/mapper/GatewayManageMapper.java | 34 ++ .../java/com/mh/user/s7/S7ConnectorUtil.java | 543 ++++++++++++++++++ .../user/serialport/SendAndReceiveByCom.java | 4 +- .../mh/user/service/DeviceControlService.java | 2 + .../CollectionParamsManageServiceImpl.java | 194 +++++++ .../impl/DeviceControlServiceImpl.java | 14 + .../src/main/java/com/mh/user/utils/Test.java | 26 +- .../src/main/resources/application-dev.yml | 2 +- .../src/main/resources/application-prod.yml | 16 +- .../src/main/resources/application.yml | 2 +- 23 files changed, 2397 insertions(+), 15 deletions(-) create mode 100644 S7_PLC使用说明.md create mode 100644 S7_PLC地址类型扩展说明.md create mode 100644 S7_PLC数据解析问题排查.md create mode 100644 user-service/src/main/java/com/mh/user/controller/S7PlcController.java create mode 100644 user-service/src/main/java/com/mh/user/dto/HotWaterSupplyPumpControlVO.java create mode 100644 user-service/src/main/java/com/mh/user/job/S7PlcCollectionJob.java create mode 100644 user-service/src/main/java/com/mh/user/s7/S7ConnectorUtil.java diff --git a/S7_PLC使用说明.md b/S7_PLC使用说明.md new file mode 100644 index 0000000..069081b --- /dev/null +++ b/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 + + com.github.s7connector + s7connector + 2.1 + +``` + +## 后续优化方向 + +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 diff --git a/S7_PLC地址类型扩展说明.md b/S7_PLC地址类型扩展说明.md new file mode 100644 index 0000000..2f01889 --- /dev/null +++ b/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 diff --git a/S7_PLC数据解析问题排查.md b/S7_PLC数据解析问题排查.md new file mode 100644 index 0000000..c563a43 --- /dev/null +++ b/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 diff --git a/user-service/pom.xml b/user-service/pom.xml index 9aa85c2..2c8a093 100644 --- a/user-service/pom.xml +++ b/user-service/pom.xml @@ -179,6 +179,13 @@ 4.1.86.Final + + + com.github.s7connector + s7connector + 2.1 + + diff --git a/user-service/src/main/java/com/mh/user/constants/Constant.java b/user-service/src/main/java/com/mh/user/constants/Constant.java index 3c441da..620c590 100644 --- a/user-service/src/main/java/com/mh/user/constants/Constant.java +++ b/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 COMMUNITY_TYPE_REAL_COM = "realCom"; 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 SEND_STATUS = false; // 指令发送状态 public static volatile boolean 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 SUCCESS = "success"; diff --git a/user-service/src/main/java/com/mh/user/controller/DeviceOperateController.java b/user-service/src/main/java/com/mh/user/controller/DeviceOperateController.java index b0ffc7b..177251e 100644 --- a/user-service/src/main/java/com/mh/user/controller/DeviceOperateController.java +++ b/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.DeviceInstallEntity; import com.mh.user.entity.PumpSetEntity; +import com.mh.user.job.S7PlcCollectionJob; import com.mh.user.model.DeviceModel; import com.mh.user.model.SerialPortModel; import com.mh.user.serialport.SerialPortSingle2; @@ -54,6 +55,10 @@ public class DeviceOperateController { @Autowired private IMqttGatewayService iMqttGatewayService; + @Autowired + private S7PlcCollectionJob s7PlcCollectionJob; + + //操作设备 @SysLogger(title = "控制管理", optDesc = "设置设备参数值") @PostMapping(value = "/operate") @@ -92,6 +97,14 @@ public class DeviceOperateController { 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 { String returnStr = deviceControlService.readOrWriteDevice(params, Constant.WRITE); if (!StringUtils.isBlank(returnStr) && "fail".equals(returnStr)) { diff --git a/user-service/src/main/java/com/mh/user/controller/HotWaterMonitorController.java b/user-service/src/main/java/com/mh/user/controller/HotWaterMonitorController.java index 092e20f..e7068e2 100644 --- a/user-service/src/main/java/com/mh/user/controller/HotWaterMonitorController.java +++ b/user-service/src/main/java/com/mh/user/controller/HotWaterMonitorController.java @@ -43,7 +43,7 @@ public class HotWaterMonitorController { * @return */ @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) { List list = collectionParamsManageService.operateList(buildingId); return HttpResult.ok(list); diff --git a/user-service/src/main/java/com/mh/user/controller/S7PlcController.java b/user-service/src/main/java/com/mh/user/controller/S7PlcController.java new file mode 100644 index 0000000..8ea43cd --- /dev/null +++ b/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()); + } + } +} diff --git a/user-service/src/main/java/com/mh/user/dto/HotWaterSupplyPumpControlVO.java b/user-service/src/main/java/com/mh/user/dto/HotWaterSupplyPumpControlVO.java new file mode 100644 index 0000000..a874a50 --- /dev/null +++ b/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(); + } +} diff --git a/user-service/src/main/java/com/mh/user/dto/HotWaterSystemControlVO.java b/user-service/src/main/java/com/mh/user/dto/HotWaterSystemControlVO.java index 4a74df6..05f03a2 100644 --- a/user-service/src/main/java/com/mh/user/dto/HotWaterSystemControlVO.java +++ b/user-service/src/main/java/com/mh/user/dto/HotWaterSystemControlVO.java @@ -27,6 +27,12 @@ public class HotWaterSystemControlVO { private int timeSet; private String timeSetId; + /** + * 校时手自动切换:22 + */ + private int timeSetAuto; + private String timeSetAutoId; + // 水箱温度 2 @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "0") private BigDecimal tankTemp; @@ -97,6 +103,26 @@ public class HotWaterSystemControlVO { private int yearTimeSet; 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; /** @@ -105,6 +131,34 @@ public class HotWaterSystemControlVO { private int reset; 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 public String toString() { return new StringJoiner(", ", HotWaterSystemControlVO.class.getSimpleName() + "[", "]") diff --git a/user-service/src/main/java/com/mh/user/job/S7PlcCollectionJob.java b/user-service/src/main/java/com/mh/user/job/S7PlcCollectionJob.java new file mode 100644 index 0000000..75abfb7 --- /dev/null +++ b/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定时采集任务 + * 支持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 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 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 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 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 entry : connectorCache.entrySet()) { + try { + entry.getValue().disconnect(); + } catch (Exception e) { + log.error("断开S7连接异常: {}", entry.getKey(), e); + } + } + connectorCache.clear(); + log.info("S7连接器缓存已清理"); + } +} diff --git a/user-service/src/main/java/com/mh/user/job/StartOrStopHotpumpJob.java b/user-service/src/main/java/com/mh/user/job/StartOrStopHotpumpJob.java index 5a61a09..6d6f2a6 100644 --- a/user-service/src/main/java/com/mh/user/job/StartOrStopHotpumpJob.java +++ b/user-service/src/main/java/com/mh/user/job/StartOrStopHotpumpJob.java @@ -40,7 +40,7 @@ public class StartOrStopHotpumpJob { this.deviceControlService = deviceControlService; } - @Scheduled(cron = "0 0/1 * * * ?") +// @Scheduled(cron = "0 0/1 * * * ?") public void startOrStopHotpump() { log.info("定时开关机热泵开始"); // 查询定时时间 diff --git a/user-service/src/main/java/com/mh/user/mapper/CollectionParamsManageMapper.java b/user-service/src/main/java/com/mh/user/mapper/CollectionParamsManageMapper.java index d7648d5..fe4cd77 100644 --- a/user-service/src/main/java/com/mh/user/mapper/CollectionParamsManageMapper.java +++ b/user-service/src/main/java/com/mh/user/mapper/CollectionParamsManageMapper.java @@ -380,4 +380,48 @@ public interface CollectionParamsManageMapper extends BaseMapper 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 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); + } diff --git a/user-service/src/main/java/com/mh/user/mapper/GatewayManageMapper.java b/user-service/src/main/java/com/mh/user/mapper/GatewayManageMapper.java index 508f1e2..4593ab1 100644 --- a/user-service/src/main/java/com/mh/user/mapper/GatewayManageMapper.java +++ b/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}") 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 queryS7Gateways(); } diff --git a/user-service/src/main/java/com/mh/user/s7/S7ConnectorUtil.java b/user-service/src/main/java/com/mh/user/s7/S7ConnectorUtil.java new file mode 100644 index 0000000..e74082d --- /dev/null +++ b/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通信工具类 + * 支持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; // 是否为位地址 + } +} diff --git a/user-service/src/main/java/com/mh/user/serialport/SendAndReceiveByCom.java b/user-service/src/main/java/com/mh/user/serialport/SendAndReceiveByCom.java index 1d0c805..ce1e5f0 100644 --- a/user-service/src/main/java/com/mh/user/serialport/SendAndReceiveByCom.java +++ b/user-service/src/main/java/com/mh/user/serialport/SendAndReceiveByCom.java @@ -117,13 +117,13 @@ public class SendAndReceiveByCom { byte[] bytes = SerialTool.readFromPort(serialPort); Date date1 = new Date(); String dateStr = DateUtil.dateToString(date1, "yyyy-MM-dd HH:mm:ss"); - if (bytes == null) { + if (bytes == null || bytes.length == 0) { SerialTool.closePort(serialPort); log.info("串口{}没有数据返回!{}", serialPort.getName(), i); 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) { if (null != serialPort) { diff --git a/user-service/src/main/java/com/mh/user/service/DeviceControlService.java b/user-service/src/main/java/com/mh/user/service/DeviceControlService.java index 839ab06..92d3bfc 100644 --- a/user-service/src/main/java/com/mh/user/service/DeviceControlService.java +++ b/user-service/src/main/java/com/mh/user/service/DeviceControlService.java @@ -25,4 +25,6 @@ public interface DeviceControlService { String operationDevice(SerialPortModel params); String getSn(SerialPortModel serialPortModel); + + boolean isS7Control(List params); } diff --git a/user-service/src/main/java/com/mh/user/service/impl/CollectionParamsManageServiceImpl.java b/user-service/src/main/java/com/mh/user/service/impl/CollectionParamsManageServiceImpl.java index 3fbb721..05f8879 100644 --- a/user-service/src/main/java/com/mh/user/service/impl/CollectionParamsManageServiceImpl.java +++ b/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); case "循环泵": return createCircuitPumpControlVO(dlEntry, dlItems, parentDto); + case "供水泵": + return createSupplyPumpControlVO(dlEntry, dlItems, parentDto); case "回水泵": return createBackPumpControlVO(dlEntry, dlItems, parentDto); case "设备校准": @@ -356,6 +358,152 @@ public class CollectionParamsManageServiceImpl implements CollectionParamsManage } } + private HotWaterSupplyPumpControlVO createSupplyPumpControlVO( + Map.Entry> dlEntry, + List 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( Map.Entry> dlEntry, List dlItems, @@ -367,15 +515,28 @@ public class CollectionParamsManageServiceImpl implements CollectionParamsManage dlItems.forEach(item -> { switch (item.getParamTypeId()) { case "1": + if (item.getOtherName().contains("自动")) { + break; + } // 时间写入控制 vo.setTimeSet(item.getCurValue().intValue()); vo.setTimeSetId(item.getCpmId()); break; + case "22": + // 校时手自动切换 + vo.setTimeSetAuto(item.getCurValue().intValue()); + vo.setTimeSetAutoId(item.getCpmId()); + break; case "5": // 压力 vo.setPressure(item.getCurValue()); vo.setPressureId(item.getCpmId()); break; + case "27": + // 压力设置 + vo.setPressureSet(item.getCurValue().intValue()); + vo.setPressureSetId(item.getCpmId()); + break; case "6": case "12": // 回水温度 @@ -396,6 +557,19 @@ public class CollectionParamsManageServiceImpl implements CollectionParamsManage // 时间 handleSystemTimeParameters(vo, item); 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": // modbus 重置复位 vo.setReset(item.getCurValue().intValue()); @@ -470,6 +644,26 @@ public class CollectionParamsManageServiceImpl implements CollectionParamsManage vo.setMinTimeSet(value); vo.setMinTimeSetId(cpmId); 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: break; } diff --git a/user-service/src/main/java/com/mh/user/service/impl/DeviceControlServiceImpl.java b/user-service/src/main/java/com/mh/user/service/impl/DeviceControlServiceImpl.java index e0a10ab..0fd2eb6 100644 --- a/user-service/src/main/java/com/mh/user/service/impl/DeviceControlServiceImpl.java +++ b/user-service/src/main/java/com/mh/user/service/impl/DeviceControlServiceImpl.java @@ -70,6 +70,20 @@ public class DeviceControlServiceImpl implements DeviceControlService { return false; } + @Override + public boolean isS7Control(List 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 public String operationDevice(SerialPortModel params) { // 拼接发送的报文 diff --git a/user-service/src/main/java/com/mh/user/utils/Test.java b/user-service/src/main/java/com/mh/user/utils/Test.java index 603bcdc..560c033 100644 --- a/user-service/src/main/java/com/mh/user/utils/Test.java +++ b/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.IOException; +import java.math.BigDecimal; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -18,6 +19,8 @@ import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * @author ljf @@ -113,7 +116,28 @@ public class Test { } 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; + } } diff --git a/user-service/src/main/resources/application-dev.yml b/user-service/src/main/resources/application-dev.yml index 6a0b034..82a03ab 100644 --- a/user-service/src/main/resources/application-dev.yml +++ b/user-service/src/main/resources/application-dev.yml @@ -22,7 +22,7 @@ spring: type: com.alibaba.druid.pool.DruidDataSource druid: #添加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 username: sa password: mh@803 diff --git a/user-service/src/main/resources/application-prod.yml b/user-service/src/main/resources/application-prod.yml index 84a6005..1d9f994 100644 --- a/user-service/src/main/resources/application-prod.yml +++ b/user-service/src/main/resources/application-prod.yml @@ -75,16 +75,16 @@ spring: # password: chws_gw@803 # # 华软江门 -# url: jdbc:sqlserver://127.0.0.1:57238;DatabaseName=chws_jm;allowMultiQueries=true;encrypt=false -# driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver -# username: chws_jm -# password: Mhtech@803 + url: jdbc:sqlserver://127.0.0.1:57238;DatabaseName=chws_jm;allowMultiQueries=true;encrypt=false + driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver + username: chws_jm + password: Mhtech@803 # 珠海北师大 - url: jdbc:sqlserver://127.0.0.1:8033;DatabaseName=chws_bsdz;allowMultiQueries=true;encrypt=false - driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver - username: chws_bsdz - password: Mhtech@803803 +# url: jdbc:sqlserver://127.0.0.1:8033;DatabaseName=chws_bsdz;allowMultiQueries=true;encrypt=false +# driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver +# username: chws_bsdz +# password: Mhtech@803803 # #南方学院 # url: jdbc:sqlserver://127.0.0.1:8033;DatabaseName=chws_nfxy;allowMultiQueries=true;encrypt=false diff --git a/user-service/src/main/resources/application.yml b/user-service/src/main/resources/application.yml index 081dc84..5f109b8 100644 --- a/user-service/src/main/resources/application.yml +++ b/user-service/src/main/resources/application.yml @@ -1,6 +1,6 @@ spring: profiles: - active: prod + active: dev mvc: pathmatch: matching-strategy: ant_path_matcher