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;
|
}
|
}
|