| | |
| | | package com.mes.plc.client.impl; |
| | | |
| | | import com.fasterxml.jackson.core.type.TypeReference; |
| | | import com.fasterxml.jackson.databind.ObjectMapper; |
| | | import com.mes.connect.modbus.ModbusTcpClient; |
| | | import com.mes.device.entity.DeviceConfig; |
| | | import com.mes.device.util.ConfigJsonHelper; |
| | | import com.mes.plc.client.PlcClient; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | |
| | | import java.util.Collections; |
| | | import java.util.HashMap; |
| | | import java.util.Map; |
| | | import java.io.IOException; |
| | | import java.util.*; |
| | | |
| | | /** |
| | | * Modbus协议PLC客户端实现 |
| | |
| | | */ |
| | | @Slf4j |
| | | public class ModbusPlcClient implements PlcClient { |
| | | |
| | | |
| | | // PLC IP地址 |
| | | private final String plcIp; |
| | | |
| | | |
| | | // PLC端口 |
| | | private final int plcPort; |
| | | |
| | | |
| | | // 从站地址 |
| | | private final int unitId; |
| | | |
| | | |
| | | // 设备配置 |
| | | private final DeviceConfig device; |
| | | |
| | | |
| | | // Modbus客户端实例 |
| | | private ModbusTcpClient modbusClient; |
| | | |
| | | |
| | | // 连接状态 |
| | | private boolean connected = false; |
| | | |
| | | |
| | | // 超时时间(毫秒) |
| | | private int timeout = 5000; |
| | | |
| | | |
| | | // ObjectMapper用于JSON解析 |
| | | private final ObjectMapper objectMapper = new ObjectMapper(); |
| | | |
| | | |
| | | // 地址映射缓存:字段名 -> Modbus地址 |
| | | private Map<String, String> addressMappingCache; |
| | | |
| | | |
| | | /** |
| | | * 构造函数 |
| | | * |
| | |
| | | this.device = device; |
| | | this.plcIp = device.getPlcIp(); |
| | | this.plcPort = device.getPlcPort() != null ? device.getPlcPort() : 502; |
| | | |
| | | |
| | | // 从配置中获取从站地址,默认1 |
| | | int unitIdValue = 1; |
| | | try { |
| | |
| | | log.warn("解析unitId失败,使用默认值1: deviceId={}", device.getId(), e); |
| | | } |
| | | this.unitId = unitIdValue; |
| | | |
| | | // 初始化地址映射 |
| | | this.addressMappingCache = loadAddressMapping(); |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 解析设备的extraParams |
| | | * |
| | |
| | | if (extraParamsJson == null || extraParamsJson.isEmpty()) { |
| | | return new HashMap<>(); |
| | | } |
| | | |
| | | |
| | | try { |
| | | // 这里简化处理,实际项目中应该使用Jackson或Gson解析 |
| | | // 由于项目中已有ObjectMapper,这里暂时返回空Map,后续完善 |
| | | return new HashMap<>(); |
| | | TypeReference<Map<String, Object>> typeRef = new TypeReference<Map<String, Object>>() {}; |
| | | return objectMapper.readValue(extraParamsJson, typeRef); |
| | | } catch (Exception e) { |
| | | log.error("解析extraParams失败: {}", extraParamsJson, e); |
| | | return null; |
| | | return new HashMap<>(); |
| | | } |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 加载地址映射配置(从configJson或extraParams.addressMapping) |
| | | * |
| | | * @return 字段名 -> Modbus地址的映射 |
| | | */ |
| | | private Map<String, String> loadAddressMapping() { |
| | | Map<String, String> mapping = new HashMap<>(); |
| | | |
| | | try { |
| | | // 1. 优先从configJson获取 |
| | | Map<String, Object> configParams = ConfigJsonHelper.parseToMap(device.getConfigJson(), objectMapper); |
| | | if (!configParams.isEmpty()) { |
| | | for (Map.Entry<String, Object> entry : configParams.entrySet()) { |
| | | String fieldName = entry.getKey(); |
| | | Object addressObj = entry.getValue(); |
| | | if (addressObj != null) { |
| | | mapping.put(fieldName, String.valueOf(addressObj)); |
| | | } |
| | | } |
| | | if (!mapping.isEmpty()) { |
| | | log.info("从configJson加载Modbus地址映射成功: deviceId={}, count={}", device.getId(), mapping.size()); |
| | | return mapping; |
| | | } |
| | | } |
| | | |
| | | // 2. 从extraParams.addressMapping获取 |
| | | Map<String, Object> extraParams = parseExtraParams(device.getExtraParams()); |
| | | Object addressMappingObj = extraParams.get("addressMapping"); |
| | | if (addressMappingObj != null) { |
| | | if (addressMappingObj instanceof Map) { |
| | | @SuppressWarnings("unchecked") |
| | | Map<String, Object> addrMap = (Map<String, Object>) addressMappingObj; |
| | | for (Map.Entry<String, Object> entry : addrMap.entrySet()) { |
| | | mapping.put(entry.getKey(), String.valueOf(entry.getValue())); |
| | | } |
| | | } else if (addressMappingObj instanceof String) { |
| | | // 如果是JSON字符串,解析它 |
| | | TypeReference<Map<String, Object>> typeRef = new TypeReference<Map<String, Object>>() {}; |
| | | Map<String, Object> addrMap = objectMapper.readValue((String) addressMappingObj, typeRef); |
| | | for (Map.Entry<String, Object> entry : addrMap.entrySet()) { |
| | | mapping.put(entry.getKey(), String.valueOf(entry.getValue())); |
| | | } |
| | | } |
| | | if (!mapping.isEmpty()) { |
| | | log.info("从extraParams.addressMapping加载Modbus地址映射成功: deviceId={}, count={}", device.getId(), mapping.size()); |
| | | return mapping; |
| | | } |
| | | } |
| | | |
| | | log.warn("未找到Modbus地址映射配置: deviceId={}", device.getId()); |
| | | } catch (Exception e) { |
| | | log.error("加载Modbus地址映射失败: deviceId={}", device.getId(), e); |
| | | } |
| | | |
| | | return mapping; |
| | | } |
| | | |
| | | /** |
| | | * 获取字段对应的Modbus地址 |
| | | * |
| | | * @param fieldName 字段名 |
| | | * @return Modbus地址(格式:功能码.寄存器地址,如 "3.40001") |
| | | */ |
| | | private String getModbusAddress(String fieldName) { |
| | | String address = addressMappingCache.get(fieldName); |
| | | if (address == null || address.isEmpty()) { |
| | | log.warn("字段 {} 未找到Modbus地址映射: deviceId={}", fieldName, device.getId()); |
| | | return null; |
| | | } |
| | | return address; |
| | | } |
| | | |
| | | /** |
| | | * 推断数据类型(根据字段名或值) |
| | | */ |
| | | private String inferDataType(String fieldName, Object value) { |
| | | if (value == null) { |
| | | // 根据字段名推断 |
| | | String lowerName = fieldName.toLowerCase(); |
| | | if (lowerName.contains("float") || lowerName.contains("real")) { |
| | | return "float"; |
| | | } else if (lowerName.contains("string") || lowerName.contains("str") || lowerName.contains("id")) { |
| | | return "string"; |
| | | } |
| | | return "int"; // 默认int |
| | | } |
| | | |
| | | // 根据值类型推断 |
| | | if (value instanceof Float || value instanceof Double) { |
| | | return "float"; |
| | | } else if (value instanceof String) { |
| | | return "string"; |
| | | } else if (value instanceof Number) { |
| | | return "int"; |
| | | } |
| | | return "int"; |
| | | } |
| | | |
| | | @Override |
| | | public boolean connect() { |
| | | try { |
| | | if (modbusClient != null && isConnected()) { |
| | | return true; |
| | | } |
| | | |
| | | |
| | | // 创建Modbus客户端实例 |
| | | this.modbusClient = new ModbusTcpClient(this.plcIp, this.plcPort, this.unitId); |
| | | |
| | | |
| | | // 连接PLC |
| | | this.modbusClient.connect(); |
| | | this.connected = true; |
| | |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | |
| | | @Override |
| | | public void disconnect() { |
| | | try { |
| | |
| | | this.modbusClient = null; |
| | | } |
| | | } |
| | | |
| | | |
| | | @Override |
| | | public Map<String, Object> readAllData() { |
| | | if (!isConnected() && !connect()) { |
| | | log.error("Modbus PLC未连接,无法读取数据: {}:{}", this.plcIp, this.plcPort); |
| | | return Collections.emptyMap(); |
| | | } |
| | | |
| | | |
| | | try { |
| | | // TODO: 实现Modbus读取所有数据 |
| | | // 这里暂时返回空Map,后续完善 |
| | | log.warn("Modbus readAllData未实现,返回空Map"); |
| | | return new HashMap<>(); |
| | | if (addressMappingCache.isEmpty()) { |
| | | log.warn("Modbus地址映射为空,无法读取所有数据: deviceId={}", device.getId()); |
| | | return Collections.emptyMap(); |
| | | } |
| | | |
| | | // 读取所有配置的字段 |
| | | Map<String, Object> result = new HashMap<>(); |
| | | for (String fieldName : addressMappingCache.keySet()) { |
| | | try { |
| | | Object value = readFieldValue(fieldName); |
| | | if (value != null) { |
| | | result.put(fieldName, value); |
| | | } |
| | | } catch (Exception e) { |
| | | log.warn("读取字段失败: fieldName={}, deviceId={}, error={}", fieldName, device.getId(), e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | log.info("Modbus读取所有数据成功: deviceId={}, fieldCount={}", device.getId(), result.size()); |
| | | return result; |
| | | } catch (Exception e) { |
| | | log.error("Modbus PLC读取所有数据失败: {}:{}", this.plcIp, this.plcPort, e); |
| | | this.connected = false; |
| | | return Collections.emptyMap(); |
| | | } |
| | | } |
| | | |
| | | |
| | | @Override |
| | | public Map<String, Object> readData(String... fields) { |
| | | if (!isConnected() && !connect()) { |
| | | log.error("Modbus PLC未连接,无法读取数据: {}:{}", this.plcIp, this.plcPort); |
| | | return Collections.emptyMap(); |
| | | } |
| | | |
| | | |
| | | if (fields == null || fields.length == 0) { |
| | | return readAllData(); |
| | | } |
| | | |
| | | |
| | | try { |
| | | // TODO: 实现Modbus读取指定字段数据 |
| | | // 这里暂时返回空Map,后续完善 |
| | | log.warn("Modbus readData未实现,返回空Map"); |
| | | return new HashMap<>(); |
| | | Map<String, Object> result = new HashMap<>(); |
| | | for (String fieldName : fields) { |
| | | if (fieldName == null || fieldName.isEmpty()) { |
| | | continue; |
| | | } |
| | | try { |
| | | Object value = readFieldValue(fieldName); |
| | | if (value != null) { |
| | | result.put(fieldName, value); |
| | | } |
| | | } catch (Exception e) { |
| | | log.warn("读取字段失败: fieldName={}, deviceId={}, error={}", fieldName, device.getId(), e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | log.info("Modbus读取指定字段数据成功: deviceId={}, fields={}, resultCount={}", |
| | | device.getId(), Arrays.toString(fields), result.size()); |
| | | return result; |
| | | } catch (Exception e) { |
| | | log.error("Modbus PLC读取数据失败: {}:{}", this.plcIp, this.plcPort, e); |
| | | this.connected = false; |
| | | return Collections.emptyMap(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 读取单个字段的值 |
| | | */ |
| | | private Object readFieldValue(String fieldName) throws IOException { |
| | | String address = getModbusAddress(fieldName); |
| | | if (address == null) { |
| | | return null; |
| | | } |
| | | |
| | | // 根据字段名推断数据类型(优先从配置中获取) |
| | | String dataType = inferDataType(fieldName, null); |
| | | |
| | | // 从extraParams中获取字段的数据类型配置 |
| | | Map<String, Object> extraParams = parseExtraParams(device.getExtraParams()); |
| | | @SuppressWarnings("unchecked") |
| | | Map<String, Object> fieldConfigs = (Map<String, Object>) extraParams.get("fieldConfigs"); |
| | | if (fieldConfigs != null) { |
| | | @SuppressWarnings("unchecked") |
| | | Map<String, Object> fieldConfig = (Map<String, Object>) fieldConfigs.get(fieldName); |
| | | if (fieldConfig != null && fieldConfig.get("dataType") != null) { |
| | | dataType = String.valueOf(fieldConfig.get("dataType")); |
| | | } |
| | | } |
| | | |
| | | // 根据数据类型读取 |
| | | switch (dataType.toLowerCase()) { |
| | | case "float": |
| | | case "real": |
| | | return modbusClient.readFloat(address); |
| | | case "string": |
| | | case "str": |
| | | // 字符串需要指定长度,默认32字符 |
| | | int stringLength = 32; |
| | | if (fieldConfigs != null) { |
| | | @SuppressWarnings("unchecked") |
| | | Map<String, Object> fieldConfig = (Map<String, Object>) fieldConfigs.get(fieldName); |
| | | if (fieldConfig != null && fieldConfig.get("length") != null) { |
| | | stringLength = ((Number) fieldConfig.get("length")).intValue(); |
| | | } |
| | | } |
| | | return modbusClient.readString(address, stringLength); |
| | | case "int": |
| | | case "integer": |
| | | case "word": |
| | | default: |
| | | return modbusClient.readRegister(address); |
| | | } |
| | | } |
| | | |
| | |
| | | log.error("Modbus PLC未连接,无法写入数据: {}:{}", this.plcIp, this.plcPort); |
| | | return false; |
| | | } |
| | | |
| | | |
| | | if (data == null || data.isEmpty()) { |
| | | log.warn("写入数据为空,跳过操作: deviceId={}", device.getId()); |
| | | return true; |
| | | } |
| | | |
| | | |
| | | try { |
| | | // TODO: 实现Modbus写入数据 |
| | | // 这里暂时返回true,后续完善 |
| | | log.warn("Modbus writeData未实现,返回成功"); |
| | | int successCount = 0; |
| | | int failCount = 0; |
| | | |
| | | for (Map.Entry<String, Object> entry : data.entrySet()) { |
| | | String fieldName = entry.getKey(); |
| | | Object value = entry.getValue(); |
| | | |
| | | if (value == null) { |
| | | continue; // 跳过null值 |
| | | } |
| | | |
| | | try { |
| | | writeFieldValue(fieldName, value); |
| | | successCount++; |
| | | } catch (Exception e) { |
| | | log.error("写入字段失败: fieldName={}, value={}, deviceId={}, error={}", |
| | | fieldName, value, device.getId(), e.getMessage()); |
| | | failCount++; |
| | | } |
| | | } |
| | | |
| | | if (failCount > 0) { |
| | | log.warn("Modbus写入数据部分失败: deviceId={}, success={}, fail={}", |
| | | device.getId(), successCount, failCount); |
| | | return false; |
| | | } |
| | | |
| | | log.info("Modbus写入数据成功: deviceId={}, fieldCount={}", device.getId(), successCount); |
| | | return true; |
| | | } catch (Exception e) { |
| | | log.error("Modbus PLC写入数据失败: {}:{}", this.plcIp, this.plcPort, e); |
| | | this.connected = false; |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 写入单个字段的值 |
| | | */ |
| | | private void writeFieldValue(String fieldName, Object value) throws IOException { |
| | | String address = getModbusAddress(fieldName); |
| | | if (address == null) { |
| | | throw new IllegalArgumentException("字段 " + fieldName + " 未找到Modbus地址映射"); |
| | | } |
| | | |
| | | // 根据值类型推断数据类型 |
| | | String dataType = inferDataType(fieldName, value); |
| | | |
| | | // 从extraParams中获取字段的数据类型配置 |
| | | Map<String, Object> extraParams = parseExtraParams(device.getExtraParams()); |
| | | @SuppressWarnings("unchecked") |
| | | Map<String, Object> fieldConfigs = (Map<String, Object>) extraParams.get("fieldConfigs"); |
| | | if (fieldConfigs != null) { |
| | | @SuppressWarnings("unchecked") |
| | | Map<String, Object> fieldConfig = (Map<String, Object>) fieldConfigs.get(fieldName); |
| | | if (fieldConfig != null && fieldConfig.get("dataType") != null) { |
| | | dataType = String.valueOf(fieldConfig.get("dataType")); |
| | | } |
| | | } |
| | | |
| | | // 根据数据类型写入 |
| | | switch (dataType.toLowerCase()) { |
| | | case "float": |
| | | case "real": |
| | | float floatValue; |
| | | if (value instanceof Number) { |
| | | floatValue = ((Number) value).floatValue(); |
| | | } else { |
| | | floatValue = Float.parseFloat(String.valueOf(value)); |
| | | } |
| | | modbusClient.writeFloat(address, floatValue); |
| | | break; |
| | | case "string": |
| | | case "str": |
| | | String stringValue = String.valueOf(value); |
| | | modbusClient.writeString(address, stringValue); |
| | | break; |
| | | case "int": |
| | | case "integer": |
| | | case "word": |
| | | default: |
| | | int intValue; |
| | | if (value instanceof Number) { |
| | | intValue = ((Number) value).intValue(); |
| | | } else { |
| | | intValue = Integer.parseInt(String.valueOf(value)); |
| | | } |
| | | modbusClient.writeRegister(address, intValue); |
| | | break; |
| | | } |
| | | } |
| | | |
| | |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | |
| | | @Override |
| | | public String getPlcType() { |
| | | return "MODBUS"; |
| | | } |
| | | |
| | | |
| | | @Override |
| | | public int getTimeout() { |
| | | return this.timeout; |
| | | } |
| | | |
| | | |
| | | @Override |
| | | public void setTimeout(int timeout) { |
| | | this.timeout = timeout; |