Java Modbus通信实战(四):Modbus通信测试与故障排查

举报
Yeats_Liao 发表于 2025/11/25 13:17:05 2025/11/25
【摘要】 在工业现场,设备通信系统就像工厂的神经网络,连接着各种传感器、控制器和执行器。当你搭建好这套系统后,最关键的一步就是全面测试,确保每个环节都能正常工作。就像汽车出厂前要经过严格的路试一样,Modbus RTU通信系统也需要经过全方位的测试验证。我们要检查能否正确读取温度传感器的数据、控制电机的启停、处理网络异常等各种情况。本文基于实际工业项目的测试经验,详细介绍Modbus RTU通信的完整...

在工业现场,设备通信系统就像工厂的神经网络,连接着各种传感器、控制器和执行器。当你搭建好这套系统后,最关键的一步就是全面测试,确保每个环节都能正常工作。

就像汽车出厂前要经过严格的路试一样,Modbus RTU通信系统也需要经过全方位的测试验证。我们要检查能否正确读取温度传感器的数据、控制电机的启停、处理网络异常等各种情况。

本文基于实际工业项目的测试经验,详细介绍Modbus RTU通信的完整测试方案,帮你构建稳定可靠的工业通信系统。

1. 测试环境搭建

1.1 测试类基础结构

在工厂的质检车间,每台设备都要经过标准化的检测流程。我们的测试框架也是如此,需要建立一套标准的测试环境:

@Slf4j
@Disabled("需要实际设备连接才能运行")
@SpringBootTest
public class ModbusSerialTest {

    @Autowired
    private ModbusSerialService modbusSerialService;
    
    @Autowired
    private ModbusSerialConfig config;
    
    private int slaveId = 1; // 默认从站地址
    
    @BeforeEach
    public void setup() {
        // 初始化测试环境
    }
    
    @AfterEach
    public void cleanup() {
        // 清理资源
    }
    
    // 各种测试方法
}

关键配置说明:

  • @Disabled注解:相当于设备检测的"安全锁",防止在没有实际设备连接时误触发测试
  • @BeforeEach:就像工人上班前的设备检查,确保测试环境准备就绪
  • @AfterEach:如同下班后的设备关机程序,及时释放系统资源

1.2 测试初始化和清理

在钢铁厂,每次开炉前都要检查设备状态,生产结束后要安全关闭。我们的测试流程也遵循同样的原则:

@BeforeEach
public void setup() {
    log.info("开始Modbus串口测试");
    // 获取配置文件中的设备地址
    slaveId = config.getDeviceAddress();
    
    // 输出可用串口列表
    String[] portNames = modbusSerialService.getAvailablePortNames().toArray(new String[0]);
    log.info("可用串口列表: {}", Arrays.toString(portNames));
    
    // 可选:测试串口连接
    // boolean connected = modbusSerialService.testConnection(config.getPortName());
}

@AfterEach
public void cleanup() {
    // 关闭连接,释放资源
    modbusSerialService.closeConnection();
    log.info("Modbus串口测试结束,连接已关闭");
}

1.3 服务类实现

这个服务类就像工厂的"中央控制室",集中管理所有设备的通信操作:

/**
 * Modbus串口通信服务
 *
 * @author XYIoT
 */
@Slf4j
@Service
public class ModbusSerialService {

    @Autowired
    private ModbusSerialConfig config;

    /**
     * 获取可用串口列表
     *
     * @return 串口名称列表
     */
    public List<String> getAvailablePortNames() {
        return ModbusSerialUtil.getPortNames();
    }

    /**
     * 测试串口连接
     *
     * @param portName 串口名称
     * @return 连接结果
     */
    public boolean testConnection(String portName) {
        return ModbusSerialUtil.testConnection(portName);
    }

    /**
     * 读取保持寄存器并解析
     *
     * @param slaveId 从站地址
     * @param offset 偏移量
     * @param quantity 数量
     * @return 解析后的数据
     */
    public Map<String, Object> readHoldingRegisters(int slaveId, int offset, int quantity) {
        int[] registers = ModbusSerialUtil.readHoldingRegisters(slaveId, offset, quantity);
        Map<String, Object> result = new HashMap<>();
        result.put("slaveId", slaveId);
        result.put("startAddress", offset);
        result.put("registers", registers);
        return result;
    }

    /**
     * 读取输入寄存器并解析
     *
     * @param slaveId 从站地址
     * @param offset 偏移量
     * @param quantity 数量
     * @return 解析后的数据
     */
    public Map<String, Object> readInputRegisters(int slaveId, int offset, int quantity) {
        int[] registers = ModbusSerialUtil.readInputRegisters(slaveId, offset, quantity);
        Map<String, Object> result = new HashMap<>();
        result.put("slaveId", slaveId);
        result.put("startAddress", offset);
        result.put("registers", registers);
        return result;
    }

    /**
     * 读取线圈状态并解析
     *
     * @param slaveId 从站地址
     * @param offset 偏移量
     * @param quantity 数量
     * @return 解析后的数据
     */
    public Map<String, Object> readCoils(int slaveId, int offset, int quantity) {
        boolean[] coils = ModbusSerialUtil.readCoils(slaveId, offset, quantity);
        Map<String, Object> result = new HashMap<>();
        result.put("slaveId", slaveId);
        result.put("startAddress", offset);
        result.put("coils", coils);
        return result;
    }

    /**
     * 读取离散输入状态并解析
     *
     * @param slaveId 从站地址
     * @param offset 偏移量
     * @param quantity 数量
     * @return 解析后的数据
     */
    public Map<String, Object> readDiscreteInputs(int slaveId, int offset, int quantity) {
        boolean[] inputs = ModbusSerialUtil.readDiscreteInputs(slaveId, offset, quantity);
        Map<String, Object> result = new HashMap<>();
        result.put("slaveId", slaveId);
        result.put("startAddress", offset);
        result.put("inputs", inputs);
        return result;
    }

    /**
     * 写入单个保持寄存器
     *
     * @param slaveId 从站地址
     * @param offset 偏移量
     * @param value 写入值
     * @return 操作结果
     */
    public boolean writeSingleRegister(int slaveId, int offset, int value) {
        try {
            ModbusSerialUtil.writeSingleRegister(slaveId, offset, value);
            return true;
        } catch (Exception e) {
            log.error("单个保持寄存器写入操作失败", e);
            return false;
        }
    }

    /**
     * 写入多个保持寄存器
     *
     * @param slaveId 从站地址
     * @param offset 偏移量
     * @param values 写入值数组
     * @return 操作结果
     */
    public boolean writeMultipleRegisters(int slaveId, int offset, int[] values) {
        try {
            ModbusSerialUtil.writeMultipleRegisters(slaveId, offset, values);
            return true;
        } catch (Exception e) {
            log.error("多个保持寄存器写入操作失败", e);
            return false;
        }
    }

    /**
     * 写入单个线圈
     *
     * @param slaveId 从站地址
     * @param offset 偏移量
     * @param value 写入值
     * @return 操作结果
     */
    public boolean writeSingleCoil(int slaveId, int offset, boolean value) {
        try {
            ModbusSerialUtil.writeSingleCoil(slaveId, offset, value);
            return true;
        } catch (Exception e) {
            log.error("单个线圈写入操作失败", e);
            return false;
        }
    }

    /**
     * 写入多个线圈
     *
     * @param slaveId 从站地址
     * @param offset 偏移量
     * @param values 写入值数组
     * @return 操作结果
     */
    public boolean writeMultipleCoils(int slaveId, int offset, boolean[] values) {
        try {
            ModbusSerialUtil.writeMultipleCoils(slaveId, offset, values);
            return true;
        } catch (Exception e) {
            log.error("多个线圈写入操作失败", e);
            return false;
        }
    }

    /**
     * 关闭连接
     */
    public void closeConnection() {
        ModbusSerialUtil.close(null);
    }

    /**
     * 读取模拟量数据
     * 
     * @param slaveId 从站地址
     * @param offset 起始寄存器
     * @param quantity 数量
     * @param dataType 数据类型:1-无符号16位整数,2-有符号16位整数,3-无符号32位整数,4-有符号32位整数,5-浮点数
     * @return 解析后的数据
     */
    public double[] readAnalogValue(int slaveId, int offset, int quantity, int dataType) {
        int[] registers = ModbusSerialUtil.readHoldingRegisters(slaveId, offset, quantity * (dataType >= 3 ? 2 : 1));
        double[] values = new double[quantity];
        
        for (int i = 0; i < quantity; i++) {
            switch (dataType) {
                case 1: // 无符号16位整数
                    values[i] = registers[i] & 0xFFFF;
                    break;
                case 2: // 有符号16位整数
                    values[i] = (short) registers[i];
                    break;
                case 3: // 无符号32位整数
                    values[i] = ((long) (registers[i * 2] & 0xFFFF) << 16) | (registers[i * 2 + 1] & 0xFFFF);
                    break;
                case 4: // 有符号32位整数
                    values[i] = ((long) registers[i * 2] << 16) | (registers[i * 2 + 1] & 0xFFFF);
                    break;
                case 5: // 浮点数
                    int highWord = registers[i * 2];
                    int lowWord = registers[i * 2 + 1];
                    int intValue = (highWord << 16) | (lowWord & 0xFFFF);
                    values[i] = Float.intBitsToFloat(intValue);
                    break;
                default:
                    values[i] = registers[i];
            }
        }
        
        return values;
    }
}

1.4 配置类

配置类就像设备的"技术档案",详细记录了通信的各项参数:

/**
 * Modbus串口通信配置类
 *
 * @author XYIoT
 */
@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;

    /**
     * 校验位 (0-NONE, 1-ODD, 2-EVEN)
     */
    private int parity = 0;

    /**
     * 超时时间(毫秒)
     */
    private int timeout = 1000;

    /**
     * 设备地址
     */
    private int deviceAddress = 1;
} 

1.5 工具类

工具类是系统的"技术核心",负责执行具体的设备通信任务:

/**
 * Modbus串口通信工具类
 *
 * @author XYIoT
 */
@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实例
     *
     * @return ModbusMaster对象
     */
    public static ModbusMaster getMaster() {
        return getMaster(null);
    }

    /**
     * 根据串口名称获取ModbusMaster实例
     *
     * @param portName 串口名称,为null则使用配置文件中的默认值
     * @return 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()) {
                    log.info("缓存连接未连接,尝试重新连接");
                    cachedMaster.connect();
                }
                return cachedMaster;
            } catch (Exception e) {
                log.warn("缓存连接失效: {},正在创建新连接", e.getMessage());
                // 如果缓存连接有问题,继续创建新连接
            }
        }

        // 创建新的连接
        try {
            // 初始化配置
            Modbus.setLogLevel(Modbus.LogLevel.LEVEL_DEBUG);
            SerialParameters serialParameters = new SerialParameters();
            serialParameters.setDevice(port);
            
            // 设置波特率
            try {
                serialParameters.setBaudRate(BaudRate.getBaudRate(config.getBaudRate()));
            } catch (Exception e) {
                log.warn("波特率设置失败: {},采用默认值9600", e.getMessage());
                serialParameters.setBaudRate(BaudRate.BAUD_RATE_9600);
            }
            
            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;
            }


            log.info("通信参数配置: 波特率={}, 数据位={}, 停止位={}, 校验位={}",
                    config.getBaudRate(), config.getDataBits(),
                    config.getStopBits(), config.getParity());
            SerialUtils.setSerialPortFactory(new SerialPortFactoryJSSC());
            // 创建ModbusMaster实例
            ModbusMaster master = ModbusMasterFactory.createModbusMasterRTU(serialParameters);
            master.setResponseTimeout(config.getTimeout());
            
            try {
                // 尝试连接串口
                log.info("正在建立串口连接...");
                SerialUtils.setSerialPortFactory(new SerialPortFactoryJSSC());
                master.connect();
                log.info("串口连接建立成功");
                
                // 连接成功,放入缓存
                CONNECTION_CACHE.put(port, master);
                return master;
            } catch (ModbusIOException e) {
                log.error("串口连接建立失败: {}", e.getMessage());
                throw new Exception("串口连接建立失败: " + e.getMessage(), e);
            }
        } catch (Exception e) {
            log.error("Modbus串口连接创建失败", e);
            throw new ServiceException("Modbus串口连接创建失败: " + (e.getMessage() != null ? e.getMessage() : "未知错误,请检查串口配置和设备连接"));
        }
    }

    /**
     * 关闭连接
     *
     * @param portName 串口名称,为null则关闭所有连接
     */
    public static void close(String portName) {
        if (StringUtils.isEmpty(portName)) {
            // 关闭所有连接
            for (Map.Entry<String, ModbusMaster> entry : CONNECTION_CACHE.entrySet()) {
                try {
                    if (entry.getValue() != null) {
                        entry.getValue().disconnect();
                    }
                } catch (ModbusIOException e) {
                    log.error("Modbus串口[{}]连接关闭失败: {}", entry.getKey(), e.getMessage());
                }
            }
            CONNECTION_CACHE.clear();
        } else {
            // 关闭指定连接
            ModbusMaster master = CONNECTION_CACHE.get(portName);
            if (master != null) {
                try {
                    master.disconnect();
                    CONNECTION_CACHE.remove(portName);
                } catch (ModbusIOException e) {
                    log.error("Modbus串口[{}]连接关闭失败: {}", portName, e.getMessage());
                }
            }
        }
    }

    /**
     * 读取保持寄存器
     *
     * @param slaveId 从站地址
     * @param offset  偏移量
     * @param quantity 读取数量
     * @return 寄存器值数组
     */
    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;
            ModbusIOException lastIoException = null;
            ModbusProtocolException lastProtocolException = null;
            
            for (int retry = 0; retry < maxRetries; retry++) {
                try {
                    log.info("正在读取保持寄存器 (第{}/{}次): 从站地址={}, 起始地址={}, 寄存器数量={}", 
                            retry + 1, maxRetries, slaveId, offset, quantity);
                    int[] result = master.readHoldingRegisters(slaveId, offset, quantity);
                    log.info("保持寄存器读取成功,数据: {}", Arrays.toString(result));
                    return result;
                } catch (ModbusIOException e) {
                    lastIoException = e;
                    log.warn("保持寄存器读取IO异常 (第{}/{}次): {}", retry + 1, maxRetries, e.getMessage());
                    // 重试前延迟一段时间
                    Thread.sleep(1000);
                } catch (ModbusProtocolException e) {
                    lastProtocolException = e;
                    log.warn("保持寄存器读取协议异常 (第{}/{}次): {}", retry + 1, maxRetries, e.getMessage());
                    Thread.sleep(1000);
                }
            }
            
            // 重试失败后抛出最后捕获的异常
            if (lastIoException != null) {
                throw lastIoException;
            }
            if (lastProtocolException != null) {
                throw lastProtocolException;
            }
            
            // 如果没有捕获到异常但仍然失败,抛出通用异常
            throw new ModbusIOException("保持寄存器读取失败,多次重试后仍未成功");
        } catch (ModbusIOException | ModbusNumberException | ModbusProtocolException e) {
            log.error("保持寄存器读取操作失败: {}", e.getMessage());
            throw new ServiceException("保持寄存器读取操作失败: " + e.getMessage());
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            log.error("保持寄存器读取操作被中断: {}", e.getMessage());
            throw new ServiceException("保持寄存器读取操作被中断: " + e.getMessage());
        }
    }

    /**
     * 读取输入寄存器
     *
     * @param slaveId 从站地址
     * @param offset  偏移量
     * @param quantity 读取数量
     * @return 寄存器值数组
     */
    public static int[] readInputRegisters(int slaveId, int offset, int quantity) {
        ModbusMaster master = getMaster();
        try {
            if (!master.isConnected()) {
                master.connect();
            }
            return master.readInputRegisters(slaveId, offset, quantity);
        } catch (ModbusIOException | ModbusNumberException | ModbusProtocolException e) {
            log.error("输入寄存器读取操作失败: {}", e.getMessage());
            throw new ServiceException("输入寄存器读取操作失败: " + e.getMessage());
        }
    }

    /**
     * 读取线圈状态
     *
     * @param slaveId 从站地址
     * @param offset  偏移量
     * @param quantity 读取数量
     * @return 线圈状态数组
     */
    public static boolean[] readCoils(int slaveId, int offset, int quantity) {
        ModbusMaster master = getMaster();
        try {
            if (!master.isConnected()) {
                master.connect();
            }
            return master.readCoils(slaveId, offset, quantity);
        } catch (ModbusIOException | ModbusNumberException | ModbusProtocolException e) {
            log.error("线圈状态读取操作失败: {}", e.getMessage());
            throw new ServiceException("线圈状态读取操作失败: " + e.getMessage());
        }
    }

    /**
     * 读取离散输入状态
     *
     * @param slaveId 从站地址
     * @param offset  偏移量
     * @param quantity 读取数量
     * @return 离散输入状态数组
     */
    public static boolean[] readDiscreteInputs(int slaveId, int offset, int quantity) {
        ModbusMaster master = getMaster();
        try {
            if (!master.isConnected()) {
                master.connect();
            }
            return master.readDiscreteInputs(slaveId, offset, quantity);
        } catch (ModbusIOException | ModbusNumberException | ModbusProtocolException e) {
            log.error("离散输入状态读取操作失败: {}", e.getMessage());
            throw new ServiceException("离散输入状态读取操作失败: " + e.getMessage());
        }
    }

    /**
     * 写入单个保持寄存器
     *
     * @param slaveId 从站地址
     * @param offset  偏移量
     * @param value   写入值
     */
    public static void writeSingleRegister(int slaveId, int offset, int value) {
        ModbusMaster master = getMaster();
        try {
            if (!master.isConnected()) {
                master.connect();
            }
            master.writeSingleRegister(slaveId, offset, value);
        } catch (ModbusIOException | ModbusNumberException | ModbusProtocolException e) {
            log.error("写入单个保持寄存器失败: {}", e.getMessage());
            throw new ServiceException("写入单个保持寄存器失败: " + e.getMessage());
        }
    }

    /**
     * 写入多个保持寄存器
     *
     * @param slaveId 从站地址
     * @param offset  偏移量
     * @param values  写入值数组
     */
    public static void writeMultipleRegisters(int slaveId, int offset, int[] values) {
        ModbusMaster master = getMaster();
        try {
            if (!master.isConnected()) {
                master.connect();
            }
            master.writeMultipleRegisters(slaveId, offset, values);
        } catch (ModbusIOException | ModbusNumberException | ModbusProtocolException e) {
            log.error("写入多个保持寄存器失败: {}", e.getMessage());
            throw new ServiceException("写入多个保持寄存器失败: " + e.getMessage());
        }
    }

    /**
     * 写入单个线圈
     *
     * @param slaveId 从站地址
     * @param offset  偏移量
     * @param value   写入值
     */
    public static void writeSingleCoil(int slaveId, int offset, boolean value) {
        ModbusMaster master = getMaster();
        try {
            if (!master.isConnected()) {
                master.connect();
            }
            master.writeSingleCoil(slaveId, offset, value);
        } catch (ModbusIOException | ModbusNumberException | ModbusProtocolException e) {
            log.error("写入单个线圈失败: {}", e.getMessage());
            throw new ServiceException("写入单个线圈失败: " + e.getMessage());
        }
    }

    /**
     * 写入多个线圈
     *
     * @param slaveId 从站地址
     * @param offset  偏移量
     * @param values  写入值数组
     */
    public static void writeMultipleCoils(int slaveId, int offset, boolean[] values) {
        ModbusMaster master = getMaster();
        try {
            if (!master.isConnected()) {
                master.connect();
            }
            master.writeMultipleCoils(slaveId, offset, values);
        } catch (ModbusIOException | ModbusNumberException | ModbusProtocolException e) {
            log.error("写入多个线圈失败: {}", e.getMessage());
            throw new ServiceException("写入多个线圈失败: " + e.getMessage());
        }
    }

    /**
     * 获取可用串口列表
     *
     * @return 串口名称数组
     */
    public static List<String> getPortNames() {
        List<String> portList = new ArrayList<>();
        
        // 方法1: 通过系统命令检测
        try {
            List<String> systemPorts = getSystemPortNames();
            if (!systemPorts.isEmpty()) {
                log.info("通过系统命令获取到串口: {}", systemPorts);
                portList.addAll(systemPorts);
            }
        } catch (Exception e) {
            log.warn("通过系统命令获取串口列表失败: {}", e.getMessage());
        }
        
        // 方法2: 使用jlibmodbus的SerialUtils获取
        if (portList.isEmpty()) {
            try {
                String[] ports = SerialUtils.getPortIdentifiers().toArray(new String[0]);
                if (ports != null && ports.length > 0) {
                    log.info("通过SerialUtils.getPortIdentifiers()获取到串口: {}", Arrays.toString(ports));
                    portList.addAll(Arrays.asList(ports));
                } else {
                    log.warn("SerialUtils.getPortIdentifiers()返回空列表");
                }
            } catch (Exception e) {
                log.warn("通过SerialUtils获取串口列表失败: {}", e.getMessage());
            }
        }
        
        // 方法3: 使用javax.comm或gnu.io的方式获取
        if (portList.isEmpty()) {
            try {
                // 通过反射调用RXTX库的方法
                Class<?> commPortIdentifierClass = Class.forName("gnu.io.CommPortIdentifier");
                Method getPortIdentifiersMethod = commPortIdentifierClass.getMethod("getPortIdentifiers");
                Enumeration<?> portEnum = (Enumeration<?>) getPortIdentifiersMethod.invoke(null);
                
                Method getNameMethod = commPortIdentifierClass.getMethod("getName");
                Method getPortTypeMethod = commPortIdentifierClass.getMethod("getPortType");
                
                while (portEnum.hasMoreElements()) {
                    Object portId = portEnum.nextElement();
                    // 只添加串行端口类型,通常判断portType == 1 (表示串行端口)
                    int portType = (Integer) getPortTypeMethod.invoke(portId);
                    if (portType == 1) {
                        String portName = (String) getNameMethod.invoke(portId);
                        if (!portList.contains(portName)) {
                            portList.add(portName);
                        }
                    }
                }
                log.info("通过RXTX库获取到串口: {}", portList);
            } catch (Exception e) {
                log.warn("通过RXTX库获取串口列表失败: {}", e.getMessage());
            }
        }
        
        // 方法4: 直接尝试常见COM口名称
        if (portList.isEmpty()) {
            log.info("尝试添加常见COM口");
            // Windows系统常见的串口命名
            for (int i = 1; i <= 10; i++) {
                String comPort = "COM" + i;
                if (!portList.contains(comPort)) {
                    portList.add(comPort);
                }
            }
            
            // Linux/Unix系统常见的串口命名
            String[] unixDevs = {
                "/dev/ttyS0", "/dev/ttyS1", "/dev/ttyS2", "/dev/ttyS3",
                "/dev/ttyUSB0", "/dev/ttyUSB1", "/dev/ttyUSB2", "/dev/ttyUSB3",
                "/dev/ttyACM0", "/dev/ttyACM1", "/dev/ttyACM2", "/dev/ttyACM3"
            };
            
            for (String dev : unixDevs) {
                if (!portList.contains(dev)) {
                    portList.add(dev);
                }
            }
        }
        
        log.info("最终获取到的串口列表: {}", portList);
        return portList;
    }

    /**
     * 检测串口连接状态
     *
     * @param portName 串口名称
     * @return 是否连接成功
     */
    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;
            }
            
            log.info("测试参数: 波特率={}, 数据位={}, 停止位={}, 校验位={}", 
                    config.getBaudRate(), config.getDataBits(), 
                    config.getStopBits(), config.getParity());
            SerialUtils.setSerialPortFactory(new SerialPortFactoryJSSC());
            // 创建ModbusMaster实例用于测试
            log.info("serialParameters: {}", serialParameters);
            master = ModbusMasterFactory.createModbusMasterRTU(serialParameters);
            master.setResponseTimeout(config.getTimeout());
            
            // 尝试连接
            log.info("开始连接测试...");
            try {
                master.connect();
            } catch (Exception e) {
                log.error("连接串口失败,详细错误:", e);
                // 输出更多调试信息
            }
            boolean connected = master.isConnected();
            log.info("连接测试结果: {}", connected ? "成功" : "失败");
            
            return connected;
        } catch (Exception e) {
            log.error("Modbus串口连接测试失败: {}", e.getMessage(), e);
            return false;
        } finally {
            if (master != null) {
                try {
                    master.disconnect();
                    log.info("测试连接已断开");
                } catch (Exception e) {
                    log.error("关闭Modbus测试连接失败: {}", e.getMessage());
                }
            }
        }
    }

    /**
     * 通过系统命令检查COM端口
     * 
     * @return 系统COM端口列表
     */
    public static List<String> getSystemPortNames() {
        List<String> portList = new ArrayList<>();
        String osName = System.getProperty("os.name").toLowerCase();
        Process process = null;
        try {
            // Windows系统使用mode命令或PowerShell
            if (osName.contains("win")) {
                log.info("检测Windows系统COM端口");
                
                // 尝试使用PowerShell命令
                try {
                    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);
                            }
                        }
                    }
                    
                    log.info("PowerShell检测到的COM端口: {}", portList);
                } catch (Exception e) {
                    log.warn("PowerShell检测COM端口失败: {}", e.getMessage());
                }
                
                // 如果PowerShell失败,尝试使用mode命令
                if (portList.isEmpty()) {
                    try {
                        process = Runtime.getRuntime().exec("mode");
                        try (BufferedReader reader = new BufferedReader(
                                new InputStreamReader(process.getInputStream()))) {
                            String line;
                            while ((line = reader.readLine()) != null) {
                                line = line.trim();
                                if (line.startsWith("COM")) {
                                    String portName = line.split("\\s+")[0].trim();
                                    if (!portList.contains(portName)) {
                                        portList.add(portName);
                                    }
                                }
                            }
                        }
                        log.info("mode命令检测到的COM端口: {}", portList);
                    } catch (Exception e) {
                        log.warn("mode命令检测COM端口失败: {}", e.getMessage());
                    }
                }
            } 
            // Linux/Unix系统
            else if (osName.contains("nix") || osName.contains("nux") || osName.contains("mac")) {
                log.info("检测Unix/Linux系统串口");
                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") || 
                            line.contains("ttyACM") || line.contains("cu.")) {
                            String[] parts = line.split("\\s+");
                            String portName = "/dev/" + parts[parts.length - 1];
                            if (!portList.contains(portName)) {
                                portList.add(portName);
                            }
                        }
                    }
                }
                log.info("ls命令检测到的串口: {}", portList);
            }
            
            return portList;
        } catch (Exception e) {
            log.warn("通过系统命令检测COM端口失败: {}", e.getMessage());
            return portList;
        } finally {
            if (process != null) {
                process.destroy();
            }
        }
    }
} 

2. 读取操作测试

在工业现场,读取操作就像工程师查看仪表盘,需要从不同类型的设备获取各种数据。温度传感器提供温度值,压力表显示压力数据,开关状态指示设备运行情况。Modbus支持多种读取操作,我们需要用不同的功能码读取设备的各种数据:

2.1 读取保持寄存器(功能码03)

保持寄存器相当于设备的"参数设置面板",存储着各种可调节的参数。就像变频器的频率设定、温控器的目标温度、流量计的量程设置等,这些参数既可以读取也可以修改:

@Test
public void testReadHoldingRegisters() {
    try {
        // 读取地址为0的10个保持寄存器
        Map<String, Object> result = modbusSerialService.readHoldingRegisters(slaveId, 0, 10);
        log.info("读取保持寄存器结果: {}", result);
        
        // 输出每个寄存器的值
        int[] registers = (int[]) result.get("registers");
        for (int i = 0; i < registers.length; i++) {
            log.info("寄存器[{}] = {}", i, registers[i]);
        }
    } catch (Exception e) {
        log.error("读取保持寄存器测试失败", e);
    }
}

保持寄存器应用场景:

  • 设备配置参数存储
  • PLC控制参数
  • 工作状态设置值

2.2 读取输入寄存器(功能码04)

输入寄存器就像工厂里的"数据显示屏",专门用来显示各种测量数据。比如锅炉的当前温度、水泵的实际流量、电机的运行电流等,这些数据只能读取,无法通过通信修改:

@Test
public void testReadInputRegisters() {
    try {
        // 读取地址为0的10个输入寄存器
        Map<String, Object> result = modbusSerialService.readInputRegisters(slaveId, 0, 10);
        log.info("读取输入寄存器结果: {}", result);
        
        // 输出每个寄存器的值
        int[] registers = (int[]) result.get("registers");
        for (int i = 0; i < registers.length; i++) {
            log.info("输入寄存器[{}] = {}", i, registers[i]);
        }
    } catch (Exception e) {
        log.error("读取输入寄存器测试失败", e);
    }
}

输入寄存器应用场景:

  • 传感器测量值(温度、湿度、压力等)
  • ADC转换结果
  • 设备状态信息

2.3 读取线圈状态(功能码01)

线圈状态就像控制柜里的"指示灯",显示各种设备的开关状态。比如电机是否运行、阀门是否打开、报警器是否激活等:

@Test
public void testReadCoils() {
    try {
        // 读取地址为0的10个线圈
        Map<String, Object> result = modbusSerialService.readCoils(slaveId, 0, 10);
        log.info("读取线圈状态结果: {}", result);
        
        // 输出每个线圈的状态
        boolean[] coils = (boolean[]) result.get("coils");
        for (int i = 0; i < coils.length; i++) {
            log.info("线圈[{}] = {}", i, coils[i]);
        }
    } catch (Exception e) {
        log.error("读取线圈状态测试失败", e);
    }
}

线圈应用场景:

  • 控制继电器、电磁阀等执行器
  • 设备开关控制
  • 控制指示灯

2.4 读取离散输入状态(功能码02)

离散输入就像工厂里的"状态检测器",用来监测各种开关量信号。比如安全门是否关闭、限位开关是否触发、故障指示是否出现等,这些信号只能检测,无法控制:

@Test
public void testReadDiscreteInputs() {
    try {
        // 读取地址为0的10个离散输入
        Map<String, Object> result = modbusSerialService.readDiscreteInputs(slaveId, 0, 10);
        log.info("读取离散输入状态结果: {}", result);
        
        // 输出每个离散输入的状态
        boolean[] inputs = (boolean[]) result.get("inputs");
        for (int i = 0; i < inputs.length; i++) {
            log.info("离散输入[{}] = {}", i, inputs[i]);
        }
    } catch (Exception e) {
        log.error("读取离散输入状态测试失败", e);
    }
}

离散输入应用场景:

  • 开关量输入(按钮、开关、限位开关等)
  • 数字传感器状态
  • 故障指示信号

3. 写入操作测试

写入操作是工业控制的核心功能,就像操作员在控制室调节各种设备参数。比如调节反应釜的温度、控制输送带的速度、开关冷却水阀门等,每个操作都直接影响生产工艺和产品质量。

Modbus支持多种写入操作,就像遥控器控制电视一样,我们可以向设备发送各种控制指令:

3.1 写入单个保持寄存器(功能码06)

单个寄存器写入就像精确调节一个参数,比如设定变频器的运行频率、调节温控器的目标温度等。就像调节空调温度一样,有时我们只需要修改一个参数:

@Test
public void testWriteSingleRegister() {
    try {
        // 写入地址为0的寄存器,值为100
        boolean result = modbusSerialService.writeSingleRegister(slaveId, 0, 100);
        log.info("写入单个保持寄存器结果: {}", result ? "成功" : "失败");
        
        // 增加延迟,给设备足够处理时间
        log.info("等待设备处理写入操作...");
        Thread.sleep(2000);
        
        // 读取写入后的值进行验证
        if (result) {
            Map<String, Object> readResult = modbusSerialService.readHoldingRegisters(slaveId, 0, 1);
            int[] values = (int[]) readResult.get("registers");
            log.info("写入后读取的值: {}", values[0]);
        }
    } catch (Exception e) {
        log.error("写入单个保持寄存器测试失败", e);
    }
}

注意事项:

  1. 写入后添加延迟(2000ms),确保设备有足够时间处理
  2. 通过读取操作验证写入结果,确保写入成功

3.2 写入多个保持寄存器(功能码16)

批量寄存器写入适合同时设置多个相关参数,比如配置PID控制器的比例、积分、微分参数,或者设置多段温度曲线。就像一次性设置空调的温度、风速、模式一样,批量操作更高效:

@Test
public void testWriteMultipleRegisters() {
    try {
        // 写入地址为0开始的3个寄存器
        int[] values = {100, 200, 300};
        boolean result = modbusSerialService.writeMultipleRegisters(slaveId, 0, values);
        log.info("写入多个保持寄存器结果: {}", result ? "成功" : "失败");
        
        // 读取写入后的值进行验证
        if (result) {
            Map<String, Object> readResult = modbusSerialService.readHoldingRegisters(slaveId, 0, 3);
            int[] readValues = (int[]) readResult.get("registers");
            log.info("写入后读取的值: {}", Arrays.toString(readValues));
        }
    } catch (Exception e) {
        log.error("写入多个保持寄存器测试失败", e);
    }
}

应用场景:

  • 批量更新配置参数
  • 设置多通道值
  • 写入复杂数据结构(浮点数、32位整数等)

3.3 写入单个线圈(功能码05)

单个线圈控制就像操作控制柜上的一个按钮,比如启动一台电机、打开一个阀门、激活一个报警器。就像按下电灯开关,控制单个设备的开关:

@Test
public void testWriteSingleCoil() {
    try {
        // 写入地址为0的线圈,值为true
        boolean result = modbusSerialService.writeSingleCoil(slaveId, 0, true);
        log.info("写入单个线圈结果: {}", result ? "成功" : "失败");
        
        // 读取写入后的值进行验证
        if (result) {
            Map<String, Object> readResult = modbusSerialService.readCoils(slaveId, 0, 1);
            boolean[] values = (boolean[]) readResult.get("coils");
            log.info("写入后读取的值: {}", values[0]);
        }
    } catch (Exception e) {
        log.error("写入单个线圈测试失败", e);
    }
}

3.4 写入多个线圈(功能码15)

批量线圈控制适合同时操作多个相关设备,比如启动一条生产线上的所有电机、关闭一个区域的所有阀门。就像总控制台,一次性控制多个设备的开关:

@Test
public void testWriteMultipleCoils() {
    try {
        // 写入地址为0开始的3个线圈
        boolean[] values = {true, false, true};
        boolean result = modbusSerialService.writeMultipleCoils(slaveId, 0, values);
        log.info("写入多个线圈结果: {}", result ? "成功" : "失败");
        
        // 读取写入后的值进行验证
        if (result) {
            Map<String, Object> readResult = modbusSerialService.readCoils(slaveId, 0, 3);
            boolean[] readValues = (boolean[]) readResult.get("coils");
            log.info("写入后读取的值: {}", Arrays.toString(readValues));
        }
    } catch (Exception e) {
        log.error("写入多个线圈测试失败", e);
    }
}

应用场景:

  • 批量控制多个设备状态
  • LED状态设置
  • 多点输出控制

4. 高级数据类型测试

工业现场的数据类型多种多样,就像不同的仪表有不同的测量范围和精度。温度可能是小数,计数器是整数,状态是布尔值。我们需要确保系统能正确处理各种数据格式。

Modbus就像只会说简单词汇的外国人,只懂布尔值和16位整数。但我们可以把简单词汇组合成复杂句子,实现更丰富的数据类型:

@Test
public void testReadAnalogValue() {
    try {
        // 读取浮点数(32位,占用2个寄存器)
        double[] floatValues = modbusSerialService.readAnalogValue(slaveId, 0, 2, 5);
        log.info("读取浮点数: {}", Arrays.toString(floatValues));
        
        // 读取16位整数
        double[] int16Values = modbusSerialService.readAnalogValue(slaveId, 0, 4, 2);
        log.info("读取16位整数: {}", Arrays.toString(int16Values));
        
        // 读取32位整数(占用2个寄存器)
        double[] int32Values = modbusSerialService.readAnalogValue(slaveId, 0, 2, 4);
        log.info("读取32位整数: {}", Arrays.toString(int32Values));
    } catch (Exception e) {
        log.error("读取模拟量测试失败", e);
    }
}

这个测试方法展示了如何读取不同类型的模拟量:

参数说明:

  • slaveId:从站地址
  • 第二个参数:起始地址
  • 第三个参数:数据类型(2表示32位浮点数,4表示16位整数,5表示32位整数)
  • 第四个参数:要读取的点数

5. 测试技巧与最佳实践

就像医生体检有标准流程一样,Modbus测试也有一套最佳实践:

5.1 异常处理

就像开车系安全带一样,异常处理是测试的"安全带",确保单个测试失败不会影响其他测试:

try {
    // 测试代码
} catch (Exception e) {
    log.error("测试失败", e);
}

5.2 数据验证

就像寄信后查看是否送达一样,写操作后要读取验证,确保数据正确写入:

// 写入操作
boolean result = modbusSerialService.writeSingleRegister(slaveId, 0, 100);

// 读取验证
Map<String, Object> readResult = modbusSerialService.readHoldingRegisters(slaveId, 0, 1);
int[] values = (int[]) readResult.get("registers");
assert values[0] == 100;

5.3 时序控制

Modbus设备就像老式电脑,需要时间"思考",特别是写操作后要给它缓冲时间:

// 写入操作
boolean result = modbusSerialService.writeSingleRegister(slaveId, 0, 100);

// 等待设备处理
Thread.sleep(2000);

// 读取验证

5.4 资源释放

就像用完水龙头要关闭一样,测试完成后要释放串口资源,避免资源泄露:

@AfterEach
public void cleanup() {
    modbusSerialService.closeConnection();
}

6. 常见问题与解决方案

在工业现场,Modbus通信问题就像设备故障一样常见。以下是几种典型问题的诊断和处理方法:

6.1 通信超时

现象:变频器控制指令发送后无响应,测试抛出超时异常
处理方法

  • 检查RS485线缆连接是否牢固
  • 调整超时参数(通常设置为2-5秒)
  • 确认波特率设置与设备一致(常用9600或19200)

6.2 校验和错误

现象:温度传感器数据读取时出现CRC校验失败
处理方法

  • 在操作间增加50-100ms延迟
  • 检查通信参数配置(数据位、停止位、校验位)
  • 更换质量更好的屏蔽双绞线

6.3 设备无响应

现象:PLC模块完全不回应任何Modbus指令
处理方法

  • 确认设备从站地址配置正确(通常为1-247)
  • 验证设备是否支持所使用的功能码
  • 检查设备电源和运行状态指示灯

7. 扩展应用

这套测试框架在实际工程项目中有广泛的应用价值:

生产线自动化测试
在汽车制造生产线上,每台新安装的焊接机器人都需要通过Modbus通信测试,确保能正确接收工艺参数和反馈状态信息。

设备调试与维护
当钢铁厂的轧机出现通信故障时,维护工程师可以使用这套测试代码快速定位问题,验证PLC与上位机之间的数据交换是否正常。

系统集成验证
在水处理厂的SCADA系统集成项目中,需要验证不同厂商的流量计、压力变送器等设备是否都能正确响应Modbus指令。

性能基准测试
对于大型化工装置的DCS系统,需要测试在高负载情况下Modbus通信的响应时间和稳定性,确保满足实时控制要求。

8. 总结

本文构建的测试框架涵盖了Modbus RTU通信的核心功能:从基础的线圈和寄存器读写,到复杂的浮点数和字符串处理,为工业设备通信提供了完整的验证方案。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。