package com.mes.connect.s7; import com.mes.connect.IndustrialInterface.AddressParser; import com.mes.connect.IndustrialInterface.IndustrialClient; import com.mes.connect.addressParser.S7AddressParser; import com.mes.connect.protocol.ProtocolAddress; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.net.Socket; import java.util.Random; import java.util.logging.Logger; /** * S7协议客户端实现 */ public class S7Client implements IndustrialClient { private static final Logger logger = Logger.getLogger(S7Client.class.getName()); private Socket socket; private DataInputStream inputStream; private DataOutputStream outputStream; private final String host; private final int port; private final int rack; private final int slot; private boolean connected; private int pduSize = 480; private int jobId = new Random().nextInt(0xFFFF); private final AddressParser addressParser = new S7AddressParser(); public S7Client(String host, int port, int rack, int slot) { this.host = host; this.port = port; this.rack = rack; this.slot = slot; } @Override public synchronized void connect() throws IOException { if (!connected) { socket = new Socket(host, port); socket.setSoTimeout(5000); // 5秒超时 inputStream = new DataInputStream(socket.getInputStream()); outputStream = new DataOutputStream(socket.getOutputStream()); // 建立COTP连接 if (!establishCotpConnection()) { disconnect(); throw new IOException("Failed to establish COTP connection"); } // 建立ISO连接 if (!establishIsoConnection()) { disconnect(); throw new IOException("Failed to establish ISO connection"); } // 建立S7连接 if (!establishS7Connection()) { disconnect(); throw new IOException("Failed to establish S7 connection"); } connected = true; logger.info("Connected to S7 server: " + host + ":" + port); } } private boolean establishCotpConnection() throws IOException { // 打印调试信息 logger.info("Attempting to establish COTP connection"); // 构造COTP连接请求包(CR) byte[] request = new byte[11]; request[0] = 0x02; // 包长度(固定为2字节) request[1] = (byte) 0xE0; // COTP TPDU类型: CR(连接请求) request[2] = 0x00; // 目标参考号(高字节) request[3] = 0x01; // 目标参考号(低字节) request[4] = 0x00; // 源参考号(高字节) request[5] = 0x00; // 源参考号(低字节) request[6] = 0x00; // 类/选项 request[7] = (byte) 0xC0; // 参数代码: TPDU-size request[8] = 0x01; // 参数长度 request[9] = 0x14; // TPDU-size(20字节,部分PLC要求此值) request[10] = 0x00; // 参数结束标记 // 发送COTP请求 outputStream.write(request); outputStream.flush(); // 打印发送的请求包(16进制) logger.info("Sent COTP CR: " + bytesToHex(request)); // 读取响应包 byte[] response = new byte[256]; int bytesRead = 0; try { // 增加读取超时处理 socket.setSoTimeout(2000); // 延长读取超时为2秒 bytesRead = inputStream.read(response); } catch (IOException e) { logger.warning("COTP response read timeout: " + e.getMessage()); return false; } finally { socket.setSoTimeout(5000); // 恢复默认超时 } // 打印接收的响应包(16进制) if (bytesRead > 0) { logger.info("Received COTP response, bytesRead: " + bytesRead); logger.info("COTP response: " + bytesToHex(response, bytesRead)); } else { logger.warning("COTP response is empty"); return false; } // 增强响应验证逻辑 if (bytesRead >= 6) { byte tpduType = response[1]; // 允许CC(0xD0)或ER(0xF0)响应,便于调试 if (tpduType == 0xD0) { // CC: 连接确认 logger.info("COTP connection established successfully"); return true; } else if (tpduType == 0xF0) { // ER: 连接拒绝 logger.warning("COTP connection rejected"); if (bytesRead >= 8) { logger.warning("COTP error code: 0x" + String.format("%02X", response[7])); } } else { logger.warning("Unexpected COTP TPDU type: 0x" + String.format("%02X", tpduType)); } } else { logger.warning("Invalid COTP response length: " + bytesRead); } return false; } // 辅助方法:字节数组转16进制字符串 private String bytesToHex(byte[] bytes) { return bytesToHex(bytes, bytes.length); } private String bytesToHex(byte[] bytes, int length) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < length; i++) { sb.append(String.format("%02X ", bytes[i])); if (i % 16 == 15) sb.append("\n"); // 每16字节换行 } return sb.toString(); } @Override public synchronized void disconnect() { if (connected) { try { if (outputStream != null) outputStream.close(); if (inputStream != null) inputStream.close(); if (socket != null) socket.close(); } catch (IOException e) { logger.warning("Error closing S7 connection: " + e.getMessage()); } finally { outputStream = null; inputStream = null; socket = null; connected = false; logger.info("Disconnected from S7 server"); } } } @Override public boolean isConnected() { return connected && socket != null && socket.isConnected() && !socket.isClosed(); } @Override public boolean readBit(String address) throws IOException { ProtocolAddress parsedAddress = addressParser.parse(address); int dbNumber = parsedAddress.getDbNumber(); int area = parsedAddress.getFunctionCode(); int startAddress = parsedAddress.getAddress(); int bit = parsedAddress.getBit(); byte[] request = buildS7ReadRequest(area, dbNumber, startAddress, 1, 0x04); // 读取字节 byte[] response = sendS7Request(request); if (response[22] == 0x00) { // 成功 byte data = response[25]; return (data & (1 << bit)) != 0; } throw new IOException("S7 read error: " + response[22]); } @Override public void writeBit(String address, boolean value) throws IOException { ProtocolAddress parsedAddress = addressParser.parse(address); int dbNumber = parsedAddress.getDbNumber(); int area = parsedAddress.getFunctionCode(); int startAddress = parsedAddress.getAddress(); int bit = parsedAddress.getBit(); // 先读取当前值 byte[] request = buildS7ReadRequest(area, dbNumber, startAddress, 1, 0x04); // 读取字节 byte[] response = sendS7Request(request); if (response[22] != 0x00) { // 读取失败 throw new IOException("S7 read error: " + response[22]); } byte data = response[25]; // 修改位值 if (value) { data |= (1 << bit); } else { data &= ~(1 << bit); } // 写入修改后的值 request = buildS7WriteRequest(area, dbNumber, startAddress, new byte[]{data}, 0x04); response = sendS7Request(request); if (response[22] != 0x00) { // 写入失败 throw new IOException("S7 write error: " + response[22]); } } @Override public int readRegister(String address) throws IOException { ProtocolAddress parsedAddress = addressParser.parse(address); int dbNumber = parsedAddress.getDbNumber(); int area = parsedAddress.getFunctionCode(); int startAddress = parsedAddress.getAddress(); byte[] request = buildS7ReadRequest(area, dbNumber, startAddress, 2, 0x06); // 读取字 byte[] response = sendS7Request(request); if (response[22] == 0x00) { // 成功 return ((response[25] & 0xFF) << 8) | (response[26] & 0xFF); } throw new IOException("S7 read error: " + response[22]); } @Override public void writeRegister(String address, int value) throws IOException { ProtocolAddress parsedAddress = addressParser.parse(address); int dbNumber = parsedAddress.getDbNumber(); int area = parsedAddress.getFunctionCode(); int startAddress = parsedAddress.getAddress(); byte[] data = new byte[2]; data[0] = (byte) (value >> 8); data[1] = (byte) value; byte[] request = buildS7WriteRequest(area, dbNumber, startAddress, data, 0x06); byte[] response = sendS7Request(request); if (response[22] != 0x00) { // 写入失败 throw new IOException("S7 write error: " + response[22]); } } @Override public int[] readRegisters(String address, int quantity) throws IOException { ProtocolAddress parsedAddress = addressParser.parse(address); int dbNumber = parsedAddress.getDbNumber(); int area = parsedAddress.getFunctionCode(); int startAddress = parsedAddress.getAddress(); byte[] request = buildS7ReadRequest(area, dbNumber, startAddress, quantity * 2, 0x06); // 读取多个字 byte[] response = sendS7Request(request); if (response[22] == 0x00) { // 成功 int[] result = new int[quantity]; for (int i = 0; i < quantity; i++) { result[i] = ((response[25 + i * 2] & 0xFF) << 8) | (response[26 + i * 2] & 0xFF); } return result; } throw new IOException("S7 read error: " + response[22]); } @Override public void writeRegisters(String address, int[] values) throws IOException { ProtocolAddress parsedAddress = addressParser.parse(address); int dbNumber = parsedAddress.getDbNumber(); int area = parsedAddress.getFunctionCode(); int startAddress = parsedAddress.getAddress(); byte[] data = new byte[values.length * 2]; for (int i = 0; i < values.length; i++) { data[i * 2] = (byte) (values[i] >> 8); data[i * 2 + 1] = (byte) values[i]; } byte[] request = buildS7WriteRequest(area, dbNumber, startAddress, data, 0x06); byte[] response = sendS7Request(request); if (response[22] != 0x00) { // 写入失败 throw new IOException("S7 write error: " + response[22]); } } @Override public float readFloat(String address) throws IOException { int[] registers = readRegisters(address, 2); int intBits = (registers[0] << 16) | registers[1]; return Float.intBitsToFloat(intBits); } @Override public void writeFloat(String address, float value) throws IOException { int intBits = Float.floatToIntBits(value); int highWord = (intBits >> 16) & 0xFFFF; int lowWord = intBits & 0xFFFF; writeRegisters(address, new int[]{highWord, lowWord}); } @Override public String readString(String address, int length) throws IOException { int[] registers = readRegisters(address, (length + 1) / 2); byte[] bytes = new byte[registers.length * 2]; for (int i = 0; i < registers.length; i++) { bytes[i * 2] = (byte) ((registers[i] >> 8) & 0xFF); bytes[i * 2 + 1] = (byte) (registers[i] & 0xFF); } return new String(bytes, 0, length); } @Override public void writeString(String address, String value) throws IOException { byte[] bytes = value.getBytes(); int[] registers = new int[(bytes.length + 1) / 2]; for (int i = 0; i < bytes.length; i++) { int regIndex = i / 2; int byteIndex = i % 2; if (byteIndex == 0) { registers[regIndex] = (bytes[i] & 0xFF) << 8; } else { registers[regIndex] |= (bytes[i] & 0xFF); } } writeRegisters(address, registers); } @Override public void close() throws IOException { disconnect(); } private boolean establishIsoConnection() throws IOException { byte[] request = new byte[21]; request[0] = 0x03; // Protocol ID request[1] = 0x00; request[2] = 0x00; request[3] = 0x15; // Length request[4] = 0x02; // COTP header length request[5] = (byte) 0xF0; // COTP TPDU type: DT request[6] = (byte) 0x80; // TPDU number request[7] = 0x00; // Parameter code: ISO-COTP request[8] = 0x0A; // Parameter length request[9] = 0x02; // Protocol version request[10] = 0x01; // Reserved request[11] = 0x00; // Source TSAP high request[12] = 0x01; // Source TSAP low request[13] = 0x00; // Destination TSAP high request[14] = 0x01; // Destination TSAP low request[15] = 0x00; // User data length request[16] = 0x04; // S7 header length request[17] = 0x00; // S7 protocol ID request[18] = 0x11; // S7 setup communication request[19] = 0x00; // Reserved request[20] = 0x00; // Reserved outputStream.write(request); byte[] response = new byte[256]; int bytesRead = inputStream.read(response); // 验证响应 return bytesRead >= 22 && response[17] == 0x00 && response[18] == 0x0E && response[19] == 0x00; } private boolean establishS7Connection() throws IOException { byte[] request = new byte[48]; request[0] = 0x03; // Protocol ID request[1] = 0x00; request[2] = 0x00; request[3] = 0x2C; // Length request[4] = 0x02; // COTP header length request[5] = (byte) 0xF0; // COTP TPDU type: DT request[6] = (byte) 0x80; // TPDU number request[7] = 0x00; // Parameter code: ISO-COTP request[8] = 0x0A; // Parameter length request[9] = 0x02; // Protocol version request[10] = 0x01; // Reserved request[11] = 0x00; // Source TSAP high request[12] = 0x01; // Source TSAP low request[13] = 0x00; // Destination TSAP high request[14] = 0x01; // Destination TSAP low request[15] = 0x00; // User data length request[16] = 0x12; // S7 header length request[17] = 0x02; // S7 protocol ID request[18] = (byte) 0xF0; // ROSCTR (Job) request[19] = (byte) 0x80; // Reserved request[20] = (byte) (jobId >> 8); // Job ID high request[21] = (byte) jobId; // Job ID low request[22] = 0x00; // Parameter length high request[23] = 0x14; // Parameter length low request[24] = 0x04; // CPU function: Connect request[25] = 0x00; // Reserved request[26] = 0x00; // Source rack request[27] = 0x00; // Source slot request[28] = 0x00; // Destination rack request[29] = 0x01; // Destination slot request[30] = 0x00; // PDU length high request[31] = (byte) pduSize; // PDU length low request[32] = 0x00; // Max AMQ call high request[33] = 0x01; // Max AMQ call low request[34] = 0x00; // Max AMQ confirm high request[35] = 0x01; // Max AMQ confirm low request[36] = 0x00; // PDU timeout high request[37] = 0x00; // PDU timeout medium request[38] = 0x00; // PDU timeout low request[39] = 0x0A; // S7 version request[40] = 0x00; // Data length high request[41] = 0x00; // Data length low outputStream.write(request); byte[] response = new byte[256]; int bytesRead = inputStream.read(response); // 验证响应 if (bytesRead >= 38 && response[18] == 0xF0 && // ROSCTR (Ack_Data) response[24] == 0x04 && // CPU function: Connect response[32] == 0x00) { // Return code: Success pduSize = ((response[34] & 0xFF) << 8) | (response[35] & 0xFF); jobId++; return true; } return false; } private byte[] buildS7ReadRequest(int area, int dbNumber, int startAddress, int size, int wordLen) { int requestLength = 44; byte[] request = new byte[requestLength]; request[0] = 0x03; // Protocol ID request[1] = 0x00; request[2] = 0x00; request[3] = (byte) (requestLength - 4); // Length request[4] = 0x02; // COTP header length request[5] = (byte) 0xF0; // COTP TPDU type: DT request[6] = (byte) 0x80; // TPDU number request[7] = 0x00; // Parameter code: ISO-COTP request[8] = 0x0A; // Parameter length request[9] = 0x02; // Protocol version request[10] = 0x01; // Reserved request[11] = 0x00; // Source TSAP high request[12] = 0x01; // Source TSAP low request[13] = 0x00; // Destination TSAP high request[14] = 0x01; // Destination TSAP low request[15] = 0x00; // User data length request[16] = 0x12; // S7 header length request[17] = 0x02; // S7 protocol ID request[18] = (byte) 0xF0; // ROSCTR (Job) request[19] = 0x00; // Reserved request[20] = (byte) (jobId >> 8); // Job ID high request[21] = (byte) jobId; // Job ID low request[22] = 0x00; // Parameter length high request[23] = 0x0E; // Parameter length low request[24] = 0x04; // CPU function: Read Var request[25] = 0x01; // Item count request[26] = 0x12; // Item specification length request[27] = 0x0A; // Syntax ID: S7ANY request[28] = (byte) wordLen; // Variable Type (Byte/Word/DWord) request[29] = 0x01; // Transport size (1 = bit, 2 = byte, 4 = word, 8 = dword) request[30] = 0x00; // Transport size high request[31] = (byte) (size & 0xFF); // Number of items request[32] = (byte) ((size >> 8) & 0xFF); // Number of items high request[33] = 0x10; // DB specification request[34] = (byte) ((dbNumber >> 8) & 0xFF); // DB number high request[35] = (byte) (dbNumber & 0xFF); // DB number low request[36] = (byte) area; // Area request[37] = (byte) ((startAddress >> 16) & 0xFF); // Address high request[38] = (byte) ((startAddress >> 8) & 0xFF); // Address medium request[39] = (byte) (startAddress & 0xFF); // Address low request[40] = 0x00; // Data length high request[41] = 0x00; // Data length low jobId++; return request; } private byte[] buildS7WriteRequest(int area, int dbNumber, int startAddress, byte[] data, int wordLen) { int dataLength = data.length; int requestLength = 44 + dataLength; byte[] request = new byte[requestLength]; request[0] = 0x03; // Protocol ID request[1] = 0x00; request[2] = 0x00; request[3] = (byte) (requestLength - 4); // Length request[4] = 0x02; // COTP header length request[5] = (byte) 0xF0; // COTP TPDU type: DT request[6] = (byte) 0x80; // TPDU number request[7] = 0x00; // Parameter code: ISO-COTP request[8] = 0x0A; // Parameter length request[9] = 0x02; // Protocol version request[10] = 0x01; // Reserved request[11] = 0x00; // Source TSAP high request[12] = 0x01; // Source TSAP low request[13] = 0x00; // Destination TSAP high request[14] = 0x01; // Destination TSAP low request[15] = 0x00; // User data length request[16] = 0x12; // S7 header length request[17] = 0x02; // S7 protocol ID request[18] = (byte) 0xF0; // ROSCTR (Job) request[19] = 0x00; // Reserved request[20] = (byte) (jobId >> 8); // Job ID high request[21] = (byte) jobId; // Job ID low request[22] = 0x00; // Parameter length high request[23] = 0x0E; // Parameter length low request[24] = 0x05; // CPU function: Write Var request[25] = 0x01; // Item count request[26] = 0x12; // Item specification length request[27] = 0x0A; // Syntax ID: S7ANY request[28] = (byte) wordLen; // Variable Type (Byte/Word/DWord) request[29] = 0x01; // Transport size (1 = bit, 2 = byte, 4 = word, 8 = dword) request[30] = 0x00; // Transport size high request[31] = (byte) (dataLength & 0xFF); // Number of items request[32] = (byte) ((dataLength >> 8) & 0xFF); // Number of items high request[33] = 0x10; // DB specification request[34] = (byte) ((dbNumber >> 8) & 0xFF); // DB number high request[35] = (byte) (dbNumber & 0xFF); // DB number low request[36] = (byte) area; // Area request[37] = (byte) ((startAddress >> 16) & 0xFF); // Address high request[38] = (byte) ((startAddress >> 8) & 0xFF); // Address medium request[39] = (byte) (startAddress & 0xFF); // Address low request[40] = 0x00; // Data length high request[41] = (byte) (dataLength + 2); // Data length low request[42] = 0x04; // Return code: 0x04 = Success request[43] = (byte) dataLength; // Data length // 复制数据 System.arraycopy(data, 0, request, 44, dataLength); jobId++; return request; } private byte[] sendS7Request(byte[] request) throws IOException { if (!isConnected()) { connect(); } // 发送请求 outputStream.write(request); // 接收响应 byte[] header = new byte[4]; int bytesRead = 0; // 读取头部 while (bytesRead < 4) { int count = inputStream.read(header, bytesRead, 4 - bytesRead); if (count == -1) { throw new IOException("Connection closed while reading response header"); } bytesRead += count; } int length = ((header[2] & 0xFF) << 8) | (header[3] & 0xFF); byte[] response = new byte[length + 4]; // 复制头部 System.arraycopy(header, 0, response, 0, 4); // 读取剩余部分 bytesRead = 0; while (bytesRead < length) { int count = inputStream.read(response, 4 + bytesRead, length - bytesRead); if (count == -1) { throw new IOException("Connection closed while reading response body"); } bytesRead += count; } // 检查Job ID int responseJobId = ((response[20] & 0xFF) << 8) | (response[21] & 0xFF); if (responseJobId != (jobId - 1)) { throw new IOException("Job ID mismatch in response"); } return response; } }