"不用去车间,浏览器里点一下,就让温控器升温!"
本文属于专栏 《Java × 工业智能》第 5 篇 | GitHub 源码:github.com/iweidujiang/java-industrial-smart
一、引言:工业控制的"最后一公里"难题
在现代化工厂中,PLC、温控器、变频器等设备普遍采用 Modbus RTU 协议 通过 RS485 总线通信。
这种协议简单、稳定、成本低,已沿用数十年。然而,它的封闭性也带来了显著痛点:
- 操作依赖专用软件:工程师必须安装厂商提供的 HMI 或 SCADA 客户端;
- 无法远程干预:设备异常时,必须派人到现场按按钮或改参数;
- 难以集成新系统:想通过 Web 端点点按钮控制风机?想用 Python 脚本批量调整温度?几乎不可能。
这些问题的本质,是 OT(运营技术)与 IT(信息技术)之间的协议鸿沟。
而我们的目标,就是用 Java 构建一座桥梁------一个轻量级的 Modbus 控制网关 ,它能将 Modbus 的底层指令,转化为标准的 HTTP REST API。从此,任何能发 HTTP 请求的系统(Web、App、脚本、云平台)都能安全、可靠地控制现场设备。
二、整体设计
下图展示了本方案的核心架构:

该架构具有以下优势:
- 解耦:上层应用无需关心 Modbus 帧格式、CRC、串口细节;
- 抽象 :提供语义化接口(如
/temperature),而非裸寄存器地址; - 可扩展:未来可轻松加入认证、日志、限流等企业级功能;
- 轻量:仅需一个 Spring Boot 应用 + 串口转 USB 线,即可部署在边缘设备。
三、核心实现原理
1. Modbus RTU 写操作详解
Modbus RTU 支持多种"写"功能码,最常用的是:
- 功能码 06(Write Single Register):写单个 16 位寄存器
- 功能码 16(Write Multiple Registers):写多个连续寄存器
以功能码 06 为例,一帧完整数据如下(十六进制):
| 字段 | 长度 | 示例 | 说明 |
|---|---|---|---|
| 设备地址 | 1 byte | 01 |
从站 ID |
| 功能码 | 1 byte | 06 |
写单寄存器 |
| 寄存器高字节 | 1 byte | 00 |
地址高位 |
| 寄存器低字节 | 1 byte | 01 |
地址低位(即寄存器 1) |
| 值高字节 | 1 byte | 00 |
数据高位 |
| 值低字节 | 1 byte | FF |
数据低位(即值 255) |
| CRC 低字节 | 1 byte | ... |
循环冗余校验 |
| CRC 高字节 | 1 byte | ... |
设备收到信息后,会返回相同帧作为确认(除非出错)。
因此,我们的 Java 程序必须:
- 正确构造上述 8 字节帧;
- 使用 CRC16-MODBUS 算法计算校验值;
- 通过串口发送,并等待响应。
2. 串口通信的可靠性处理
RS485 是半双工总线,通信需注意:
- 波特率匹配:必须与设备一致(常见 9600、19200);
- 延迟控制:发送后需短暂等待(如 100ms)再读响应;
- 资源独占:同一时间只能一个线程操作串口。
为此,在 ModbusRtuWriter 中采用 "打开 → 操作 → 关闭" 模式,确保线程安全(虽牺牲性能,但简化设计)。
生产环境可引入连接池优化。
四、关键代码解析
📌 完整可运行代码已开源至 GitHub:github.com/iweidujiang/java-industrial-smart
模块路径:
code/05-modbus-rest-control
1. 底层通信:ModbusRtuWriter
java
// 构造 Modbus RTU 写帧(功能码 06)
ByteBuffer buffer = ByteBuffer.allocate(6).order(ByteOrder.BIG_ENDIAN);
buffer.put((byte) deviceId)
.put((byte) 0x06) // 写单寄存器
.putShort((short) regAddr)
.putShort((short) value);
// 计算 CRC16 并附加
int crc = calculateCRC16(frame, 6);
frame[6] = (byte) (crc & 0xFF);
frame[7] = (byte) ((crc >> 8) & 0xFF);
这段代码是整个网关的"引擎",负责与物理设备对话。CRC 算法经过多款国产温控器验证,兼容大多数国产温控器与 PLC。
2. 业务封装:ModbusControlService
java
public void setTargetTemperature(double temperature) {
int rawValue = (int) Math.round(temperature * 10); // 25.5℃ → 255
ModbusRTUWriter.writeRegister("COM4", 9600, 1, 1, rawValue);
}
这里体现了领域驱动设计思想:将技术细节(寄存器地址、缩放因子)隐藏在服务层,对外暴露业务语义。
3. REST 接口:ModbusController
java
@RestController
@RequestMapping("/api/modbus")
public class ModbusController {
@Autowired
private ModbusControlService service;
@PostMapping("/temperature")
public ResponseEntity<String> setTemperature(@RequestBody Map<String, Double> req) {
double temp = req.get("temperature");
service.setTargetTemperature(temp);
return ResponseEntity.ok("目标温度已设为 " + temp + "℃");
}
@PostMapping("/pump")
public ResponseEntity<String> controlPump(@RequestBody Map<String, Boolean> req) {
boolean start = req.get("start");
service.controlPump(start);
return ResponseEntity.ok("水泵已" + (start ? "启动" : "停止"));
}
}
支持三种典型操作:
/api/modbus/temperature:设置目标温度/api/modbus/pump:启停水泵/api/modbus/write:通用寄存器写入
五、模拟测试
即使没有 RS485 设备,咱们可以通过软件来模拟完成验证功能。
5.1 创建虚拟串口对
由于 Modbus Slave 和 Java 程序不能同时占用同一物理串口,我们需要虚拟串口对 ,使用 Virtuak Serial Port Driver (VSPD)工具模拟。

5.2 使用 Modbus Slave 配置从站参数并绑定到虚拟串口
- 点击菜单 Setup → Slave Definition...
- 设置如下参数:
- Slave ID :
1 - Function :
03 Holding Register (4x)(用于读取保持寄存器) - Address mode :
Dec(十进制) - Address :
0(起始地址) - Quantity :
10(共 10 个寄存器,地址 0~9)
- Slave ID :
- 点击 OK
- 点击 Connection → Connect
- 选择 Serial Port ,端口选 COM3
- 波特率设为 9600,数据位 8,停止位 1,无校验


此时,Modbus Slave 会自动创建一个包含 10 个寄存器的表格,初始值如下:

5.3 修改 Java 代码中的串口名
打开 ModbusControlService.java,将串口改为 COM4(与 Slave 配对的另一端):
java
private final String SERIAL_PORT = "COM4"; // Windows
// private final String SERIAL_PORT = "/dev/ttyUSB0"; // Linux
5.4 启动 Spring Boot 应用并调用 API 观察结果
5.4.1 启动
powershell
# 进入到项目根目录 05-modbus-rest-control ,执行
mvn spring-boot:run
或者任一方便的方式启动Spring Boot应用即可(比如IDE工具上点击一下按钮)。
5.4.2 测试 1:设置目标温度为 26.5℃
假设我们希望将温度值写入 寄存器 1,但由于初始值为 0,写入后将会覆盖它。

然后我们看下 Modbus Slave 上的值是否改过来了:

成功写入数据:寄存器 1 的值应变为 265(因为 26.5 × 10 = 265)
5.4.3 测试 2:启动水泵
代码中,将水泵启停状态写入 寄存器 10 :
java
ModbusRtuWriter.writeRegister(SERIAL_PORT, BAUD_RATE, DEVICE_ID, 10, start ? 1 : 0);
所以,在 Modbus Slave 中设置的寄存器得有寄存器10:

调用接口 /api/modbus/pump:

观察 Modbus Slave :寄存器 10 的值应变为 1

六、生产环境增强建议
6.1 配置外部化
当前实现为最小可用版本。在实际部署中,一般我们会将各种配置放到配置中心,比如 Nacos ,或者在 application.yml 中配置。
以将配置放到 application.yml 文件为例:将串口、设备 ID、寄存器映射等参数配置到 application.yml,支持多设备管理。
yaml
modbus:
devices:
- id: 1
port: COM4
baud: 9600
registers:
temperature: 1
pump: 10
6.2 操作审计
记录每次写操作的:
- 时间戳
- 操作人(IP 或 Token)
- 寄存器地址与旧/新值
6.3 异常处理
- 串口打不开?返回 503 Service Unavailable
- CRC 校验失败?重试 2 次
- 设备无响应?触发邮件/短信告警
6.3 安全防护
- 添加 JWT 认证,防止未授权控制
- 对关键寄存器(如设备 ID)设置写保护
这些功能均可基于 Spring Security、AOP、Spring Boot Actuator 实现。
七、小结
通过本文,我们完成了一项看似简单却极具价值的工作:
将封闭的 Modbus 协议,转化为开放的 Web 服务。
这不仅解决了"远程控制"的刚需,更打开了工业设备与现代 IT 系统融合的大门。
完整源码已开源:
🔗 GitHub 仓库:github.com/iweidujiang/java-industrial-smart
📁 模块路径:
code/05-modbus-rest-control
欢迎 Star、Fork、提交 Issue!
本文属于专栏 《Java × 工业智能》第 5 篇
如果你对这个系列感兴趣,记得关注我哦!