想象一下,你要和一台工业设备"对话",比如询问温度传感器"现在多少度?"或者告诉电机"转快一点"。
Modbus RTU就是这种"对话"的标准语言,就像人与人之间说普通话一样。
它采用主从结构,就像老师和学生的关系:
本文将通过XYIoT项目的实际代码,手把手教你如何用Java实现这种"对话"。
就像做菜需要准备食材一样,我们先准备好必要的依赖包:
<!-- Modbus串口通信相关依赖 -->
<dependency>
<groupId>com.intelligt.modbus</groupId>
<artifactId>jlibmodbus</artifactId>
<version>1.2.9.7</version>
</dependency>
<!-- 串口通信依赖 -->
<dependency>
<groupId>org.scream3r</groupId>
<artifactId>jssc</artifactId>
<version>2.8.0</version>
</dependency>
串口通信就像打电话,需要设置正确的"电话号码"和"通话规则":
modbus:
serial:
# 串口名称
port-name: COM1
# 波特率
baud-rate: 9600
# 数据位(8位)
data-bits: 8
# 停止位(1位)
stop-bits: 1
# 校验位(0-NONE, 1-ODD, 2-EVEN)
parity: 0
# 超时时间(毫秒)
timeout: 2000
# 设备地址
device-address: 1
这个配置类就像是"通讯录",记录了如何连接设备:
@Data
@Configuration
@ConfigurationProperties(prefix = "modbus.serial")
public class ModbusSerialConfig {
private String portName = "COM3";
private int baudRate = 9600;
private int dataBits = 8;
private int stopBits = 1;
private int parity = 0;
private int timeout = 1000;
private int deviceAddress = 1;
}
这个工具类就像是"翻译官",负责把你的Java指令翻译成设备能懂的Modbus语言:
@Slf4j
@Component
public class ModbusSerialUtil {
private static final Map<String, ModbusMaster> CONNECTION_CACHE = new HashMap<>();
private static ModbusSerialConfig getConfig() {
return SpringUtils.getBean(ModbusSerialConfig.class);
}
/**
* 获取ModbusMaster实例 - 就像获取一个专门的"对讲机"
* 支持连接缓存(避免重复拨号)和自动重连(断线自动重拨)
*/
public static ModbusMaster getMaster(String portName) {
ModbusSerialConfig config = getConfig();
String port = StringUtils.isEmpty(portName) ? config.getPortName() : portName;
log.info("正在连接Modbus串口: {}", port);
// 优先使用缓存连接 - 就像手机的通话记录,直接重拨上次的号码
if (CONNECTION_CACHE.containsKey(port) && CONNECTION_CACHE.get(port) != null) {
ModbusMaster cachedMaster = CONNECTION_CACHE.get(port);
try {
if (!cachedMaster.isConnected()) {
cachedMaster.connect();
}
return cachedMaster;
} catch (Exception e) {
log.warn("缓存连接失效: {},重新建立连接", e.getMessage());
}
}
// 建立新的串口连接 - 就像第一次拨打一个新号码
try {
// 初始化配置
SerialParameters serialParameters = new SerialParameters();
serialParameters.setDevice(port);
serialParameters.setBaudRate(BaudRate.getBaudRate(config.getBaudRate()));
serialParameters.setDataBits(config.getDataBits());
serialParameters.setStopBits(config.getStopBits());
// 设置校验位
switch (config.getParity()) {
case 1: serialParameters.setParity(Parity.ODD); break;
case 2: serialParameters.setParity(Parity.EVEN); break;
default: serialParameters.setParity(Parity.NONE); break;
}
// 设置串口工厂(关键步骤)- 就像选择用哪家电信运营商打电话
SerialUtils.setSerialPortFactory(new SerialPortFactoryJSSC());
// 创建ModbusMaster实例
ModbusMaster master = ModbusMasterFactory.createModbusMasterRTU(serialParameters);
master.setResponseTimeout(config.getTimeout());
// 连接串口
master.connect();
// 缓存连接实例 - 把这个"电话号码"存到通讯录里
CONNECTION_CACHE.put(port, master);
return master;
} catch (Exception e) {
log.error("创建Modbus串口连接失败", e);
throw new ServiceException("串口连接建立失败: " + e.getMessage());
}
}
}
读取寄存器就像问设备"你现在的状态是什么?"比如问温度传感器"现在多少度?":
public static int[] readHoldingRegisters(int slaveId, int offset, int quantity) {
ModbusMaster master = getMaster();
try {
if (!master.isConnected()) {
log.info("检测到连接断开,正在重新连接...");
master.connect();
// 连接建立后等待设备就绪
Thread.sleep(500);
}
// 实现重试机制 - 就像打电话没接通时会自动重拨几次
int maxRetries = 3;
for (int retry = 0; retry < maxRetries; retry++) {
try {
log.info("读取保持寄存器 (第{}/{}次尝试)", retry + 1, maxRetries);
int[] result = master.readHoldingRegisters(slaveId, offset, quantity);
log.info("数据读取成功: {}", Arrays.toString(result));
return result;
} catch (ModbusIOException e) {
log.warn("IO异常,准备重试: {}", e.getMessage());
Thread.sleep(1000);
} catch (ModbusProtocolException e) {
log.warn("协议异常,准备重试: {}", e.getMessage());
Thread.sleep(1000);
}
}
throw new ModbusIOException("多次重试后读取仍然失败");
} catch (Exception e) {
log.error("保持寄存器读取失败: {}", e.getMessage());
throw new ServiceException("保持寄存器读取失败: " + e.getMessage());
}
}
写入寄存器就像给设备下指令,比如告诉空调"设定温度26度":
public static void writeSingleRegister(int slaveId, int offset, int value) {
ModbusMaster master = getMaster();
try {
if (!master.isConnected()) {
master.connect();
}
master.writeSingleRegister(slaveId, offset, value);
// 写入后等待设备处理 - 就像发短信后等对方回复"收到"
Thread.sleep(500);
} catch (Exception e) {
log.error("单个寄存器写入失败: {}", e.getMessage());
throw new ServiceException("单个寄存器写入失败: " + e.getMessage());
}
}
就像查看电脑上有哪些USB接口可以用一样,我们需要找到可用的串口:
public static List<String> getSystemPortNames() {
List<String> portList = new ArrayList<>();
String osName = System.getProperty("os.name").toLowerCase();
// Windows系统串口检测 - 就像在Windows设备管理器里查看COM口
if (osName.contains("win")) {
try {
Process process = Runtime.getRuntime().exec(new String[] {
"powershell.exe", "-Command",
"[System.IO.Ports.SerialPort]::getportnames()"
});
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (!line.isEmpty() && !portList.contains(line)) {
portList.add(line);
}
}
}
} catch (Exception e) {
log.warn("Windows串口检测失败: {}", e.getMessage());
}
}
// Linux系统串口检测 - 就像用ls命令查看/dev目录下的设备文件
else if (osName.contains("nix") || osName.contains("nux")) {
try {
Process process = Runtime.getRuntime().exec("ls -la /dev/tty*");
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
if (line.contains("ttyS") || line.contains("ttyUSB")) {
String[] parts = line.split("\\s+");
String portName = "/dev/" + parts[parts.length - 1];
portList.add(portName);
}
}
}
} catch (Exception e) {
log.warn("Linux串口检测失败: {}", e.getMessage());
}
}
return portList;
}
就像打电话前先测试信号是否正常一样,我们需要测试串口连接:
public static boolean testConnection(String portName) {
ModbusMaster master = null;
try {
log.info("正在测试串口连接: {}", portName);
ModbusSerialConfig config = getConfig();
SerialParameters serialParameters = new SerialParameters();
serialParameters.setDevice(portName);
serialParameters.setBaudRate(BaudRate.getBaudRate(config.getBaudRate()));
serialParameters.setDataBits(config.getDataBits());
serialParameters.setStopBits(config.getStopBits());
// 设置校验位
switch (config.getParity()) {
case 1: serialParameters.setParity(Parity.ODD); break;
case 2: serialParameters.setParity(Parity.EVEN); break;
default: serialParameters.setParity(Parity.NONE); break;
}
// 设置串口工厂(必需步骤)
SerialUtils.setSerialPortFactory(new SerialPortFactoryJSSC());
master = ModbusMasterFactory.createModbusMasterRTU(serialParameters);
master.setResponseTimeout(config.getTimeout());
master.connect();
boolean connected = master.isConnected();
return connected;
} catch (Exception e) {
log.error("串口连接测试失败: {}", e.getMessage());
return false;
} finally {
if (master != null) {
try {
master.disconnect();
} catch (Exception e) {
log.error("测试连接关闭失败: {}", e.getMessage());
}
}
}
}
这些是实际项目中踩过的坑,就像老司机的驾驶经验:
设置SerialPortFactory
这就像选择正确的"电话线路",不设置就像拿着手机但没插SIM卡:
SerialUtils.setSerialPortFactory(new SerialPortFactoryJSSC());
添加操作间延迟
设备需要"思考时间",就像你问朋友问题后要等他回答,不能连珠炮似的问个不停:
// 写入操作后延迟
Thread.sleep(2000);
增加超时设置
就像打电话设置等待时间,超过这个时间没人接就自动挂断:
master.setResponseTimeout(2000); // 设置2秒超时
实现重试机制
工业现场就像信号不好的地方,有时需要多打几次电话才能接通:
// 最多重试3次
for (int retry = 0; retry < 3; retry++) {
try {
return master.readHoldingRegisters(slaveId, offset, quantity);
} catch (Exception e) {
if (retry == 2) throw e;
Thread.sleep(1000);
}
}
使用连接缓存
就像手机的通话记录,避免每次都重新拨号:
private static final Map<String, ModbusMaster> CONNECTION_CACHE = new HashMap<>();
就像查看手机的通话记录一样,我们可以看到每次"对话"的详细内容。
比如你在控制台会看到这样的日志:
以下面Modbus RTU通信日志为例:
Frame sent: 0106000000648821
Frame recv: 0106000000648821
这串看似乱码的数字,其实就像一封标准格式的信件:
01
: 从站地址(设备地址),这里是106
: 功能码,表示写入单个寄存器0000
: 寄存器地址,这里是00064
: 写入的值,十六进制0x64即十进制1008821
: CRC校验和想象Modbus通信就像邮递员送信:
让我们把这个"信件"拆开来看:
数据帧0106000000648821
就像一个包裹的标签:
01 - 从站地址(设备编号,这里是1号设备)
06 - 功能码(06表示"写单个寄存器")
0000 - 寄存器地址(这里写入第0号寄存器)
0064 - 要写入的数据(0x64十六进制 = 100十进制)
8821 - CRC校验码(确保数据完整性,类似防伪标记)
功能码就像不同类型的业务单,每种单子办不同的事:
让我们看几个实际的"对话"例子:
写入命令示例(就像发指令)
0106000000648821 (写入请求)
0106000000648821 (设备回复确认)
这就像:
读取命令示例(就像询问状态)
010300000001840A (读取请求)
0103020064XX (设备回复,XX是校验码)
这就像:
有时候"电话"会出现问题,比如:
Frame sent: 010300000001840A
Frame recv: 0103000000 (应该还有后续数据)
就像打电话时信号不好,可能的原因:
所以我们要像打电话一样,说完一句话等对方回应,不要急着说下一句。
Modbus就像一个只会说数字的"外国人",它能表达的内容有限:
通过本文的学习,你已经掌握了用Java与工业设备"对话"的技能:
就像学会打电话:
实际应用场景:
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。