14 changed files with 1301 additions and 1 deletions
@ -0,0 +1,178 @@ |
|||
package com.techsor.datacenter.receiver.listener.csdj; |
|||
|
|||
import com.google.gson.Gson; |
|||
import com.techsor.datacenter.receiver.config.DataCenterEnvConfig; |
|||
import com.techsor.datacenter.receiver.listener.csdj.config.CsdjProperties; |
|||
import com.techsor.datacenter.receiver.listener.csdj.entity.CsdjEntity; |
|||
import com.techsor.datacenter.receiver.listener.csdj.session.CsdjSession; |
|||
import com.techsor.datacenter.receiver.listener.csdj.protocol.CsdjFrame; |
|||
import com.techsor.datacenter.receiver.utils.DefaultHttpRequestUtil; |
|||
import com.techsor.datacenter.receiver.utils.MyHTTPResponse; |
|||
import jakarta.annotation.PostConstruct; |
|||
import jakarta.annotation.PreDestroy; |
|||
import jakarta.annotation.Resource; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.stereotype.Component; |
|||
|
|||
import java.io.IOException; |
|||
import java.net.ServerSocket; |
|||
import java.net.Socket; |
|||
import java.util.Arrays; |
|||
import java.util.HashMap; |
|||
import java.util.Map; |
|||
import java.util.concurrent.ExecutorService; |
|||
import java.util.concurrent.Executors; |
|||
|
|||
@Slf4j |
|||
@Component |
|||
@RequiredArgsConstructor |
|||
public class CsdjServer { |
|||
|
|||
private final DefaultHttpRequestUtil defaultHttpRequestUtil; |
|||
|
|||
private final DataCenterEnvConfig dataCenterEnvConfig; |
|||
|
|||
private final CsdjProperties properties; |
|||
|
|||
private final ExecutorService executor = Executors.newCachedThreadPool(); |
|||
|
|||
// HTTP异步处理专用线程池
|
|||
private final ExecutorService httpExecutor = Executors.newCachedThreadPool(); |
|||
|
|||
@PostConstruct |
|||
public void start() { |
|||
if (!properties.getServer().isEnabled()) { |
|||
log.info("CSDJ Server is disabled"); |
|||
return; |
|||
} |
|||
|
|||
executor.submit(this::serverLoop); |
|||
} |
|||
|
|||
private void serverLoop() { |
|||
try (ServerSocket serverSocket = new java.net.ServerSocket(properties.getServer().getPort())) { |
|||
log.info("CSDJ Server started on port {}", properties.getServer().getPort()); |
|||
|
|||
while (!serverSocket.isClosed() && !Thread.currentThread().isInterrupted()) { |
|||
try { |
|||
Socket socket = serverSocket.accept(); |
|||
log.info("New connection from {}", socket.getInetAddress()); |
|||
executor.submit(() -> handleConnection(socket)); |
|||
} catch (IOException e) { |
|||
if (!serverSocket.isClosed()) { |
|||
log.error("Accept error", e); |
|||
} |
|||
} |
|||
} |
|||
} catch (IOException e) { |
|||
log.error("Server error", e); |
|||
} |
|||
} |
|||
|
|||
private void handleConnection(Socket socket) { |
|||
CsdjSession session = null; |
|||
try { |
|||
session = createSession(socket); |
|||
session.start(); |
|||
session.initiateAuthentication(); |
|||
|
|||
// 优雅等待:使用CountDownLatch阻塞直到连接关闭
|
|||
session.awaitClose(); |
|||
|
|||
} catch (InterruptedException e) { |
|||
log.info("Connection interrupted"); |
|||
Thread.currentThread().interrupt(); |
|||
} catch (Exception e) { |
|||
log.error("Connection error", e); |
|||
} finally { |
|||
if (session != null) { |
|||
session.close(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private CsdjSession createSession(Socket socket) throws IOException { |
|||
return new CsdjSession(socket, properties, true) { |
|||
|
|||
@Override |
|||
protected void processInformation(CsdjFrame frame) { |
|||
|
|||
// ========== 打印设备上报数据 ==========
|
|||
printDeviceData(frame); |
|||
|
|||
} |
|||
|
|||
/** |
|||
* 打印设备上报数据 |
|||
*/ |
|||
private void printDeviceData(CsdjFrame frame) { |
|||
String terminalId = frame.getTerminalId(); |
|||
byte[] data = frame.getInfoPart(); |
|||
|
|||
log.info("========================================"); |
|||
log.info("Received device notification data"); |
|||
log.info("----------------------------------------"); |
|||
log.info("Terminal ID: {}", terminalId); |
|||
|
|||
if (data != null && data.length > 0) { |
|||
|
|||
String hexData = bytesToHex(data); |
|||
log.info("Data length: {} bytes", data.length); |
|||
log.info("Received Hex: {}", hexData); |
|||
log.info("Decimal array: {}", Arrays.toString(data)); |
|||
|
|||
httpExecutor.submit(() -> { |
|||
try { |
|||
//要用异步,不然会和这个csdj数据接收冲突阻塞
|
|||
sendDataAsync(terminalId, hexData); |
|||
} catch (Exception e) { |
|||
log.error("Failed to send data asynchronously", e); |
|||
} |
|||
}); |
|||
} else { |
|||
log.info("Data: empty"); |
|||
} |
|||
|
|||
log.info("========================================"); |
|||
} |
|||
}; |
|||
} |
|||
|
|||
private void sendDataAsync(String terminalId, String hexData) { |
|||
CsdjEntity csdjEntity = new CsdjEntity(); |
|||
csdjEntity.setTerminalId(terminalId); |
|||
csdjEntity.setData(hexData); |
|||
csdjEntity.setTs(System.currentTimeMillis()); |
|||
|
|||
Gson gson = new Gson(); |
|||
String jsonParams = gson.toJson(csdjEntity); |
|||
MyHTTPResponse response = defaultHttpRequestUtil.postJson(dataCenterEnvConfig.getCsdjApiUrl(), jsonParams); |
|||
if (response.getCode() == 200) { |
|||
log.info("csdj data sent successfully...."); |
|||
} else { |
|||
log.error("csdj data sent failed...."); |
|||
} |
|||
} |
|||
|
|||
|
|||
|
|||
/** |
|||
* 字节数组转十六进制字符串 |
|||
*/ |
|||
private String bytesToHex(byte[] bytes) { |
|||
StringBuilder sb = new StringBuilder(); |
|||
for (byte b : bytes) { |
|||
sb.append(String.format("%02X ", b)); |
|||
} |
|||
return sb.toString().trim(); |
|||
} |
|||
|
|||
@PreDestroy |
|||
public void stop() { |
|||
log.info("正在关闭 CSDJ Server..."); |
|||
executor.shutdown(); |
|||
httpExecutor.shutdown(); |
|||
log.info("CSDJ Server 已关闭"); |
|||
} |
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
package com.techsor.datacenter.receiver.listener.csdj.client; |
|||
|
|||
import com.techsor.datacenter.receiver.listener.csdj.config.CsdjProperties; |
|||
import com.techsor.datacenter.receiver.listener.csdj.protocol.CsdjFrame; |
|||
import com.techsor.datacenter.receiver.listener.csdj.session.CsdjSession; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.stereotype.Component; |
|||
|
|||
import java.io.IOException; |
|||
import java.net.Socket; |
|||
|
|||
@Slf4j |
|||
@Component |
|||
@RequiredArgsConstructor |
|||
public class CsdjClient { |
|||
private final CsdjProperties properties; |
|||
|
|||
public CsdjSession connect(String terminalId, String userId, String password) throws IOException { |
|||
if (!properties.getClient().isEnabled()) { |
|||
throw new IllegalStateException("CSDJ Client is disabled"); |
|||
} |
|||
|
|||
Socket socket = new Socket(properties.getClient().getHost(), properties.getClient().getPort()); |
|||
log.info("Connected to server: {}:{}", properties.getClient().getHost(), properties.getClient().getPort()); |
|||
|
|||
CsdjSession session = new CsdjSession(socket, properties, false); |
|||
session.start(); |
|||
return session; |
|||
} |
|||
|
|||
public void sendNotification(CsdjSession session, String terminalId, byte[] data) { |
|||
CsdjFrame frame = CsdjFrame.createInfoFrame(terminalId, data, true, 0); |
|||
session.queueFrame(frame); |
|||
} |
|||
} |
|||
@ -0,0 +1,50 @@ |
|||
package com.techsor.datacenter.receiver.listener.csdj.config; |
|||
|
|||
import lombok.Data; |
|||
import org.springframework.boot.context.properties.ConfigurationProperties; |
|||
import org.springframework.stereotype.Component; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
|
|||
@Data |
|||
@Component |
|||
@ConfigurationProperties(prefix = "csdj") |
|||
public class CsdjProperties { |
|||
private Server server = new Server(); |
|||
private Client client = new Client(); |
|||
private List<User> users = new ArrayList<>(); |
|||
private Timeout timeout = new Timeout(); |
|||
|
|||
@Data |
|||
public static class Server { |
|||
private int port = 7114; |
|||
private boolean enabled = true; |
|||
} |
|||
|
|||
@Data |
|||
public static class Client { |
|||
private boolean enabled = false; |
|||
private String host = "127.0.0.1"; |
|||
private int port = 7114; |
|||
} |
|||
|
|||
@Data |
|||
public static class User { |
|||
private String userId; |
|||
private String password; |
|||
} |
|||
|
|||
@Data |
|||
public static class Timeout { |
|||
private long ts0 = 30000; |
|||
private long ts1 = 10000; |
|||
private long ts2 = 10000; |
|||
private long ts3 = 10000; |
|||
private long ts4 = 10000; |
|||
private long tr1 = 10000; |
|||
private long tr2 = 10000; |
|||
private long tr4 = 10000; |
|||
private long tr5 = 10000; |
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
package com.techsor.datacenter.receiver.listener.csdj.entity; |
|||
|
|||
import lombok.Data; |
|||
|
|||
import java.io.Serializable; |
|||
|
|||
@Data |
|||
public class CsdjEntity implements Serializable { |
|||
private String terminalId; |
|||
private Long ts; |
|||
private String data; |
|||
} |
|||
@ -0,0 +1,60 @@ |
|||
package com.techsor.datacenter.receiver.listener.csdj.protocol; |
|||
|
|||
import lombok.Getter; |
|||
|
|||
@Getter |
|||
public enum ControlCommand { |
|||
INTEGRATED_VALUE_CLEAR((byte) 0x01, "積算値クリア要求"), |
|||
TERMINAL_STATUS_NOTIFY((byte) 0x02, "端子状態通知要求"), |
|||
TERMINAL_STATUS_NOTIFY_CSDJ((byte) 0x03, "端子状態通知要求(CSDJ用)"), |
|||
DIGITAL_OUTPUT_CONTROL((byte) 0x04, "デジタル出力制御要求"), |
|||
OUTPUT_TERMINAL_CONTROL((byte) 0x05, "出力端子制御要求"), |
|||
HISTORY_COLLECT_1((byte) 0x0A, "履歴収集要求1"), |
|||
HISTORY_COLLECT_1_OLD((byte) 0x0C, "履歴収集要求1(旧)"), |
|||
HISTORY_COLLECT_2((byte) 0x2A, "履歴収集要求2"), |
|||
HISTORY_COLLECT_2_OLD((byte) 0x2C, "履歴収集要求2(旧)"), |
|||
DATA_CONTROL_REQUEST((byte) 0x00, "データコントロール要求"), |
|||
TIME_INQUIRY((byte) 0x0E, "時刻問い合わせ要求"), |
|||
TIME_SETTING((byte) 0x0F, "時刻設定要求"), |
|||
TERMINAL_INFO((byte) 0x18, "端末情報要求"), |
|||
TERMINAL_INFO_OLD((byte) 0x11, "端末情報要求(旧)"), |
|||
RESET((byte) 0x19, "リセット要求(CSDJ用)"), |
|||
|
|||
INTEGRATED_VALUE_CLEAR_RESP((byte) 0x41, "積算値クリア通知"), |
|||
TERMINAL_STATUS_RESP((byte) 0x42, "端子状態通知"), |
|||
TERMINAL_STATUS_RESP_CSDJ((byte) 0x43, "端子状態通知(CSDJ用)"), |
|||
DIGITAL_OUTPUT_CONTROL_RESP((byte) 0x44, "デジタル出力制御完了通知"), |
|||
OUTPUT_TERMINAL_CONTROL_RESP((byte) 0x45, "出力端子制御完了通知"), |
|||
HISTORY_COLLECT_1_RESP((byte) 0x4A, "履歴収集応答1"), |
|||
HISTORY_COLLECT_1_RESP_OLD((byte) 0x4C, "履歴収集応答1(旧)"), |
|||
HISTORY_COLLECT_1_RESP_MEM_ERROR((byte) 0xCB, "履歴収集応答1(メモリ異常)"), |
|||
HISTORY_COLLECT_1_RESP_ERROR_OLD((byte) 0xCD, "履歴収集応答1(メモリ異常旧)"), |
|||
HISTORY_COLLECT_2_RESP((byte) 0x6A, "履歴収集応答2"), |
|||
HISTORY_COLLECT_2_RESP_OLD((byte) 0x6C, "履歴収集応答2(旧)"), |
|||
DATA_CONTROL_REQUEST_RESP((byte) 0x40, "データコントロール要求応答"), |
|||
TIME_INQUIRY_RESP((byte) 0x4E, "時刻問い合わせ応答"), |
|||
TIME_SETTING_RESP((byte) 0x4F, "時刻設定応答"), |
|||
TERMINAL_INFO_RESP((byte) 0x58, "端末情報応答"), |
|||
TERMINAL_INFO_RESP_OLD((byte) 0x51, "端末情報応答(旧)"), |
|||
TERMINAL_INFO_RESP_ERROR((byte) 0xD8, "端末情報応答(異常)"), |
|||
TERMINAL_INFO_RESP_ERROR_OLD((byte) 0xD1, "端末情報応答(異常旧)"), |
|||
RESET_RESP((byte) 0x59, "リセット応答(CSDJ用)"), |
|||
INVALID_COMMAND((byte) 0xFF, "不正コマンド/無応答通知"); |
|||
|
|||
private final byte code; |
|||
private final String description; |
|||
|
|||
ControlCommand(byte code, String description) { |
|||
this.code = code; |
|||
this.description = description; |
|||
} |
|||
|
|||
public static ControlCommand fromCode(byte code) { |
|||
for (ControlCommand cmd : values()) { |
|||
if (cmd.code == code) { |
|||
return cmd; |
|||
} |
|||
} |
|||
return INVALID_COMMAND; |
|||
} |
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
package com.techsor.datacenter.receiver.listener.csdj.protocol; |
|||
|
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Builder; |
|||
import lombok.Data; |
|||
import lombok.NoArgsConstructor; |
|||
|
|||
@Data |
|||
@Builder |
|||
@NoArgsConstructor |
|||
@AllArgsConstructor |
|||
public class ControlPart { |
|||
private FrameIdentifier identifier; |
|||
private boolean endBit; |
|||
private int sequenceNumber; |
|||
|
|||
public byte[] toBytes() { |
|||
byte[] bytes = new byte[2]; |
|||
bytes[0] = identifier.getCode(); |
|||
byte attr = (byte) ((endBit ? 0x80 : 0x00) | (sequenceNumber & 0x7F)); |
|||
bytes[1] = attr; |
|||
return bytes; |
|||
} |
|||
|
|||
public static ControlPart fromBytes(byte[] bytes) { |
|||
if (bytes.length != 2) { |
|||
throw new IllegalArgumentException("Control part must be 2 bytes"); |
|||
} |
|||
FrameIdentifier id = FrameIdentifier.fromCode(bytes[0]); |
|||
boolean endBit = (bytes[1] & 0x80) != 0; |
|||
int seq = bytes[1] & 0x7F; |
|||
return ControlPart.builder() |
|||
.identifier(id) |
|||
.endBit(endBit) |
|||
.sequenceNumber(seq) |
|||
.build(); |
|||
} |
|||
|
|||
public static ControlPart create(FrameIdentifier id) { |
|||
return ControlPart.builder() |
|||
.identifier(id) |
|||
.endBit(true) |
|||
.sequenceNumber(0) |
|||
.build(); |
|||
} |
|||
} |
|||
@ -0,0 +1,103 @@ |
|||
package com.techsor.datacenter.receiver.listener.csdj.protocol; |
|||
|
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Builder; |
|||
import lombok.Data; |
|||
import lombok.NoArgsConstructor; |
|||
|
|||
import java.nio.ByteBuffer; |
|||
import java.nio.ByteOrder; |
|||
import java.nio.charset.StandardCharsets; |
|||
import java.util.Arrays; |
|||
|
|||
@Data |
|||
@Builder |
|||
@NoArgsConstructor |
|||
@AllArgsConstructor |
|||
public class CsdjFrame { |
|||
public static final int HEADER_LENGTH = 16; |
|||
public static final int MAX_FRAME_LENGTH = 6144; |
|||
public static final int MAX_INFO_LENGTH = 6128; |
|||
public static final int TERMINAL_ID_LENGTH = 12; |
|||
|
|||
private int dataLength; |
|||
private String terminalId; |
|||
private ControlPart controlPart; |
|||
private byte[] infoPart; |
|||
|
|||
public byte[] toBytes() { |
|||
int totalLength = HEADER_LENGTH + (infoPart != null ? infoPart.length : 0); |
|||
ByteBuffer buffer = ByteBuffer.allocate(totalLength); |
|||
buffer.order(ByteOrder.BIG_ENDIAN); |
|||
|
|||
buffer.putShort((short) totalLength); |
|||
|
|||
byte[] idBytes = new byte[TERMINAL_ID_LENGTH]; |
|||
if (terminalId != null) { |
|||
byte[] srcBytes = terminalId.getBytes(StandardCharsets.US_ASCII); |
|||
int len = Math.min(srcBytes.length, TERMINAL_ID_LENGTH); |
|||
System.arraycopy(srcBytes, 0, idBytes, 0, len); |
|||
} |
|||
buffer.put(idBytes); |
|||
|
|||
buffer.put(controlPart.toBytes()); |
|||
|
|||
if (infoPart != null && infoPart.length > 0) { |
|||
buffer.put(infoPart); |
|||
} |
|||
|
|||
return buffer.array(); |
|||
} |
|||
|
|||
public static CsdjFrame fromBytes(byte[] bytes) { |
|||
if (bytes.length < HEADER_LENGTH) { |
|||
throw new IllegalArgumentException("Frame too short"); |
|||
} |
|||
ByteBuffer buffer = ByteBuffer.wrap(bytes); |
|||
buffer.order(ByteOrder.BIG_ENDIAN); |
|||
|
|||
int dataLength = buffer.getShort() & 0xFFFF; |
|||
|
|||
byte[] idBytes = new byte[TERMINAL_ID_LENGTH]; |
|||
buffer.get(idBytes); |
|||
String terminalId = new String(idBytes, StandardCharsets.US_ASCII).trim(); |
|||
|
|||
byte[] ctrlBytes = new byte[2]; |
|||
buffer.get(ctrlBytes); |
|||
ControlPart controlPart = ControlPart.fromBytes(ctrlBytes); |
|||
|
|||
int infoLength = dataLength - HEADER_LENGTH; |
|||
byte[] infoPart = null; |
|||
if (infoLength > 0) { |
|||
infoPart = new byte[infoLength]; |
|||
buffer.get(infoPart); |
|||
} |
|||
|
|||
return CsdjFrame.builder() |
|||
.dataLength(dataLength) |
|||
.terminalId(terminalId) |
|||
.controlPart(controlPart) |
|||
.infoPart(infoPart) |
|||
.build(); |
|||
} |
|||
|
|||
public static CsdjFrame createSimpleFrame(FrameIdentifier id, String terminalId) { |
|||
return CsdjFrame.builder() |
|||
.terminalId(terminalId) |
|||
.controlPart(ControlPart.create(id)) |
|||
.infoPart(null) |
|||
.build(); |
|||
} |
|||
|
|||
public static CsdjFrame createInfoFrame(String terminalId, byte[] info, boolean endBit, int seq) { |
|||
return CsdjFrame.builder() |
|||
.terminalId(terminalId) |
|||
.controlPart(ControlPart.builder() |
|||
.identifier(FrameIdentifier.INFORMATION) |
|||
.endBit(endBit) |
|||
.sequenceNumber(seq) |
|||
.build()) |
|||
.infoPart(info) |
|||
.build(); |
|||
} |
|||
} |
|||
@ -0,0 +1,106 @@ |
|||
package com.techsor.datacenter.receiver.listener.csdj.protocol; |
|||
|
|||
import lombok.Getter; |
|||
|
|||
/** |
|||
* 帧识别码枚举 |
|||
* 定义了CSDJ协议中所有帧类型的识别码 |
|||
*/ |
|||
@Getter |
|||
public enum FrameIdentifier { |
|||
/** |
|||
* 信息帧 (Information) |
|||
* 用于发送通報数据、数据控制命令等业务数据 |
|||
*/ |
|||
INFORMATION((byte) 0x01, "I", "信息(通報データ等の送信)"), |
|||
|
|||
/** |
|||
* 接收确认 (Receive Ready) |
|||
* 用于确认收到I帧 |
|||
*/ |
|||
RECEIVE_READY((byte) 0x81, "RR", "Iフレームの受信確認"), |
|||
|
|||
/** |
|||
* 接收未就绪 (Receive Not Ready) |
|||
* 用于要求对方重发I帧(例如校验错误或丢包) |
|||
*/ |
|||
RECEIVE_NOT_READY((byte) 0x82, "RNR", "Iフレームの再送要求"), |
|||
|
|||
/** |
|||
* 拒绝 (Reject) |
|||
* 用于强制终止通信 |
|||
*/ |
|||
REJECT((byte) 0x83, "REJ", "通信強制終了"), |
|||
|
|||
/** |
|||
* ID/密码请求 (ID/PassWord) |
|||
* 服务器端发送,要求终端发送认证信息 |
|||
*/ |
|||
ID_PASSWORD((byte) 0xC1, "IDPWD", "ユーザID/パスワード要求"), |
|||
|
|||
/** |
|||
* ID/密码确认 (ID/PassWord-Unnumbered Acknowledge) |
|||
* 终端发送,包含用户ID和密码进行认证 |
|||
*/ |
|||
ID_PASSWORD_ACK((byte) 0xC2, "IDPWD-UA", "ユーザID/パスワードの送信"), |
|||
|
|||
/** |
|||
* 新消息发送准备 (New Message Send Ready) |
|||
* 表示"我准备好接收数据了"或"我发完了,轮到你了" |
|||
*/ |
|||
NEW_MESSAGE_SEND_READY((byte) 0xC3, "NMSR", "新規メッセージ送信確認"), |
|||
|
|||
/** |
|||
* 断开请求 (DisConnect) |
|||
* 请求断开连接 |
|||
*/ |
|||
DISCONNECT((byte) 0xC4, "DISC", "切断要求"), |
|||
|
|||
/** |
|||
* 断开确认 (DisConnect-Unnumbered Acknowledge) |
|||
* 确认断开连接 |
|||
*/ |
|||
DISCONNECT_ACK((byte) 0xC5, "DISC-UA", "(DISCに対する)確認応答"); |
|||
|
|||
/** |
|||
* 识别码(1字节) |
|||
*/ |
|||
private final byte code; |
|||
|
|||
/** |
|||
* 简称(如I、RR等) |
|||
*/ |
|||
private final String name; |
|||
|
|||
/** |
|||
* 详细描述 |
|||
*/ |
|||
private final String description; |
|||
|
|||
/** |
|||
* 构造函数 |
|||
* @param code 识别码字节 |
|||
* @param name 简称 |
|||
* @param description 详细描述 |
|||
*/ |
|||
FrameIdentifier(byte code, String name, String description) { |
|||
this.code = code; |
|||
this.name = name; |
|||
this.description = description; |
|||
} |
|||
|
|||
/** |
|||
* 根据字节码查找对应的识别码枚举 |
|||
* @param code 识别码字节 |
|||
* @return 对应的帧识别码枚举 |
|||
* @throws IllegalArgumentException 如果找不到匹配的识别码 |
|||
*/ |
|||
public static FrameIdentifier fromCode(byte code) { |
|||
for (FrameIdentifier id : values()) { |
|||
if (id.code == code) { |
|||
return id; |
|||
} |
|||
} |
|||
throw new IllegalArgumentException("Unknown frame identifier: " + code); |
|||
} |
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
package com.techsor.datacenter.receiver.listener.csdj.protocol; |
|||
|
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Builder; |
|||
import lombok.Data; |
|||
import lombok.NoArgsConstructor; |
|||
|
|||
import java.nio.ByteBuffer; |
|||
import java.nio.charset.StandardCharsets; |
|||
|
|||
@Data |
|||
@Builder |
|||
@NoArgsConstructor |
|||
@AllArgsConstructor |
|||
public class IdPasswordAck { |
|||
private String userId; |
|||
private String password; |
|||
|
|||
public byte[] toBytes() { |
|||
ByteBuffer buffer = ByteBuffer.allocate(32); |
|||
putString(buffer, userId, 16); |
|||
putString(buffer, password, 16); |
|||
return buffer.array(); |
|||
} |
|||
|
|||
public static IdPasswordAck fromBytes(byte[] bytes) { |
|||
if (bytes.length < 32) { |
|||
throw new IllegalArgumentException("ID/PWD ACK must be 32 bytes"); |
|||
} |
|||
ByteBuffer buffer = ByteBuffer.wrap(bytes); |
|||
String userId = getString(buffer, 16); |
|||
String password = getString(buffer, 16); |
|||
return IdPasswordAck.builder() |
|||
.userId(userId) |
|||
.password(password) |
|||
.build(); |
|||
} |
|||
|
|||
private static void putString(ByteBuffer buffer, String str, int length) { |
|||
byte[] bytes = new byte[length]; |
|||
if (str != null) { |
|||
byte[] src = str.getBytes(StandardCharsets.US_ASCII); |
|||
int len = Math.min(src.length, length); |
|||
System.arraycopy(src, 0, bytes, 0, len); |
|||
} |
|||
buffer.put(bytes); |
|||
} |
|||
|
|||
private static String getString(ByteBuffer buffer, int length) { |
|||
byte[] bytes = new byte[length]; |
|||
buffer.get(bytes); |
|||
return new String(bytes, StandardCharsets.US_ASCII).trim(); |
|||
} |
|||
} |
|||
@ -0,0 +1,280 @@ |
|||
package com.techsor.datacenter.receiver.listener.csdj.session; |
|||
|
|||
import com.techsor.datacenter.receiver.listener.csdj.config.CsdjProperties; |
|||
import com.techsor.datacenter.receiver.listener.csdj.protocol.*; |
|||
import lombok.Getter; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
|
|||
import java.io.IOException; |
|||
import java.io.InputStream; |
|||
import java.io.OutputStream; |
|||
import java.net.Socket; |
|||
import java.nio.ByteBuffer; |
|||
import java.nio.ByteOrder; |
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
import java.util.concurrent.*; |
|||
|
|||
@Slf4j |
|||
public class CsdjSession implements AutoCloseable { |
|||
private final Socket socket; |
|||
private final InputStream input; |
|||
private final OutputStream output; |
|||
private final CsdjProperties properties; |
|||
private final boolean isServer; |
|||
private final List<CsdjFrame> pendingFrames = new ArrayList<>(); |
|||
|
|||
@Getter |
|||
private SessionState state = SessionState.INIT; |
|||
@Getter |
|||
private String terminalId = ""; |
|||
@Getter |
|||
private boolean authenticated = false; |
|||
private String authenticatedUserId = ""; |
|||
|
|||
private final ExecutorService executor = Executors.newSingleThreadExecutor(); |
|||
private Future<?> readFuture; |
|||
private volatile boolean active = true; |
|||
private final CountDownLatch closeLatch = new CountDownLatch(1); |
|||
|
|||
public boolean isActive() { |
|||
return active && !socket.isClosed() && state != SessionState.CLOSED; |
|||
} |
|||
|
|||
public void awaitClose() throws InterruptedException { |
|||
closeLatch.await(); |
|||
} |
|||
|
|||
public boolean awaitClose(long timeout, TimeUnit unit) throws InterruptedException { |
|||
return closeLatch.await(timeout, unit); |
|||
} |
|||
|
|||
public CsdjSession(Socket socket, CsdjProperties properties, boolean isServer) throws IOException { |
|||
this.socket = socket; |
|||
this.input = socket.getInputStream(); |
|||
this.output = socket.getOutputStream(); |
|||
this.properties = properties; |
|||
this.isServer = isServer; |
|||
} |
|||
|
|||
public void start() { |
|||
readFuture = executor.submit(this::readLoop); |
|||
} |
|||
|
|||
private void readLoop() { |
|||
try { |
|||
while (!socket.isClosed() && !Thread.currentThread().isInterrupted()) { |
|||
CsdjFrame frame = readFrame(); |
|||
if (frame != null) { |
|||
handleFrame(frame); |
|||
} |
|||
} |
|||
} catch (Exception e) { |
|||
log.error("Read loop error", e); |
|||
} finally { |
|||
close(); |
|||
} |
|||
} |
|||
|
|||
private CsdjFrame readFrame() throws IOException { |
|||
byte[] header = new byte[CsdjFrame.HEADER_LENGTH]; |
|||
int read = input.read(header); |
|||
if (read != CsdjFrame.HEADER_LENGTH) { |
|||
return null; |
|||
} |
|||
|
|||
ByteBuffer buffer = ByteBuffer.wrap(header); |
|||
buffer.order(ByteOrder.BIG_ENDIAN); |
|||
int dataLength = buffer.getShort() & 0xFFFF; |
|||
|
|||
int infoLength = dataLength - CsdjFrame.HEADER_LENGTH; |
|||
byte[] fullFrame = new byte[dataLength]; |
|||
System.arraycopy(header, 0, fullFrame, 0, CsdjFrame.HEADER_LENGTH); |
|||
|
|||
if (infoLength > 0) { |
|||
read = input.read(fullFrame, CsdjFrame.HEADER_LENGTH, infoLength); |
|||
if (read != infoLength) { |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
return CsdjFrame.fromBytes(fullFrame); |
|||
} |
|||
|
|||
private void handleFrame(CsdjFrame frame) { |
|||
log.debug("Received frame: {}", frame.getControlPart().getIdentifier().getName()); |
|||
|
|||
FrameIdentifier id = frame.getControlPart().getIdentifier(); |
|||
|
|||
switch (id) { |
|||
case ID_PASSWORD: |
|||
handleIdPassword(frame); |
|||
break; |
|||
case ID_PASSWORD_ACK: |
|||
handleIdPasswordAck(frame); |
|||
break; |
|||
case NEW_MESSAGE_SEND_READY: |
|||
handleNmsr(frame); |
|||
break; |
|||
case INFORMATION: |
|||
handleInformation(frame); |
|||
break; |
|||
case RECEIVE_READY: |
|||
handleRr(frame); |
|||
break; |
|||
case DISCONNECT: |
|||
handleDisc(frame); |
|||
break; |
|||
case DISCONNECT_ACK: |
|||
handleDiscAck(frame); |
|||
break; |
|||
default: |
|||
log.warn("Unknown frame: {}", id); |
|||
} |
|||
} |
|||
|
|||
private void handleIdPassword(CsdjFrame frame) { |
|||
if (isServer) { |
|||
state = SessionState.WAITING_IDPWD_UA; |
|||
} |
|||
} |
|||
|
|||
private void handleIdPasswordAck(CsdjFrame frame) { |
|||
if (isServer) { |
|||
IdPasswordAck auth = IdPasswordAck.fromBytes(frame.getInfoPart()); |
|||
authenticated = authenticate(auth.getUserId(), auth.getPassword()); |
|||
authenticatedUserId = auth.getUserId(); |
|||
if (authenticated) { |
|||
log.info("Authentication success: {}", auth.getUserId()); |
|||
sendFrame(CsdjFrame.createSimpleFrame(FrameIdentifier.NEW_MESSAGE_SEND_READY, terminalId)); |
|||
state = SessionState.WAITING_I; |
|||
} else { |
|||
log.warn("Authentication failed: {}", auth.getUserId()); |
|||
close(); |
|||
} |
|||
} else { |
|||
state = SessionState.WAITING_NMSR; |
|||
} |
|||
} |
|||
|
|||
private boolean authenticate(String userId, String password) { |
|||
return properties.getUsers().stream() |
|||
.anyMatch(u -> u.getUserId().equals(userId) && u.getPassword().equals(password)); |
|||
} |
|||
|
|||
private void handleNmsr(CsdjFrame frame) { |
|||
log.info("Received NMSR"); |
|||
|
|||
if (!pendingFrames.isEmpty()) { |
|||
sendPendingFrame(); |
|||
} else if (isServer) { |
|||
// 服务器端,没有数据要发,直接断开
|
|||
log.info("No data to send, sending DISC to disconnect."); |
|||
sendFrame(CsdjFrame.createSimpleFrame(FrameIdentifier.DISCONNECT, terminalId)); |
|||
state = SessionState.CLOSING; |
|||
} else { |
|||
state = SessionState.WAITING_I; |
|||
} |
|||
} |
|||
|
|||
private void handleInformation(CsdjFrame frame) { |
|||
log.info("Received information frame from terminal: {}", frame.getTerminalId()); |
|||
this.terminalId = frame.getTerminalId(); |
|||
|
|||
// 1. 先发送 RR 确认收到
|
|||
sendFrame(CsdjFrame.createSimpleFrame(FrameIdentifier.RECEIVE_READY, terminalId)); |
|||
|
|||
// 2. 先设置状态为等待 NMSR
|
|||
state = SessionState.WAITING_I; |
|||
|
|||
// 3. 再处理业务数据
|
|||
processInformation(frame); |
|||
} |
|||
|
|||
protected void processInformation(CsdjFrame frame) { |
|||
log.info("Processing information: {} bytes", frame.getInfoPart() != null ? frame.getInfoPart().length : 0); |
|||
} |
|||
|
|||
private void handleRr(CsdjFrame frame) { |
|||
if (!pendingFrames.isEmpty()) { |
|||
pendingFrames.remove(0); |
|||
if (!pendingFrames.isEmpty()) { |
|||
sendPendingFrame(); |
|||
} else { |
|||
sendFrame(CsdjFrame.createSimpleFrame(FrameIdentifier.NEW_MESSAGE_SEND_READY, terminalId)); |
|||
state = SessionState.WAITING_I; |
|||
} |
|||
} |
|||
} |
|||
|
|||
private void handleDisc(CsdjFrame frame) { |
|||
sendFrame(CsdjFrame.createSimpleFrame(FrameIdentifier.DISCONNECT_ACK, terminalId)); |
|||
state = SessionState.CLOSED; |
|||
close(); |
|||
} |
|||
|
|||
private void handleDiscAck(CsdjFrame frame) { |
|||
state = SessionState.CLOSED; |
|||
close(); |
|||
} |
|||
|
|||
private void sendPendingFrame() { |
|||
if (!pendingFrames.isEmpty()) { |
|||
sendFrame(pendingFrames.get(0)); |
|||
state = SessionState.WAITING_RR; |
|||
} |
|||
} |
|||
|
|||
public void sendFrame(CsdjFrame frame) { |
|||
try { |
|||
byte[] bytes = frame.toBytes(); |
|||
output.write(bytes); |
|||
output.flush(); |
|||
log.debug("Sent frame: {}", frame.getControlPart().getIdentifier().getName()); |
|||
} catch (IOException e) { |
|||
log.error("Send frame error", e); |
|||
close(); |
|||
} |
|||
} |
|||
|
|||
public void queueFrame(CsdjFrame frame) { |
|||
pendingFrames.add(frame); |
|||
} |
|||
|
|||
public void initiateAuthentication() { |
|||
sendFrame(CsdjFrame.createSimpleFrame(FrameIdentifier.ID_PASSWORD, terminalId)); |
|||
state = SessionState.WAITING_IDPWD_UA; |
|||
} |
|||
|
|||
public void sendAuthentication(String userId, String password) { |
|||
IdPasswordAck auth = IdPasswordAck.builder() |
|||
.userId(userId) |
|||
.password(password) |
|||
.build(); |
|||
CsdjFrame frame = CsdjFrame.builder() |
|||
.terminalId(terminalId) |
|||
.controlPart(ControlPart.create(FrameIdentifier.ID_PASSWORD_ACK)) |
|||
.infoPart(auth.toBytes()) |
|||
.build(); |
|||
sendFrame(frame); |
|||
} |
|||
|
|||
@Override |
|||
public void close() { |
|||
if (!active) { |
|||
return; // 避免重复关闭
|
|||
} |
|||
active = false; |
|||
state = SessionState.CLOSED; |
|||
if (readFuture != null) { |
|||
readFuture.cancel(true); |
|||
} |
|||
try { |
|||
socket.close(); |
|||
} catch (IOException e) { |
|||
log.error("Close socket error", e); |
|||
} |
|||
executor.shutdown(); |
|||
closeLatch.countDown(); // 释放锁,唤醒awaitClose()
|
|||
} |
|||
} |
|||
@ -0,0 +1,60 @@ |
|||
package com.techsor.datacenter.receiver.listener.csdj.session; |
|||
|
|||
/** |
|||
* CSDJ会话状态枚举 |
|||
*/ |
|||
public enum SessionState { |
|||
/** |
|||
* 初始化状态 |
|||
* - 会话刚建立,准备开始 |
|||
*/ |
|||
INIT, |
|||
|
|||
/** |
|||
* 等待IDPWD-UA |
|||
* - 服务器发IDPWD,等设备回应IDPWD-UA |
|||
*/ |
|||
WAITING_IDPWD_UA, |
|||
|
|||
/** |
|||
* 等待NMSR |
|||
* - 发送NMSR,等对方回应NMSR或I帧 |
|||
*/ |
|||
WAITING_NMSR, |
|||
|
|||
/** |
|||
* 等待I帧 |
|||
* - 等待设备发通報数据或响应 |
|||
*/ |
|||
WAITING_I, |
|||
|
|||
/** |
|||
* 等待RR |
|||
* - 发送了I帧,等RR确认 |
|||
*/ |
|||
WAITING_RR, |
|||
|
|||
/** |
|||
* 等待响应 |
|||
* - 发送了数据控制命令,等设备响应 |
|||
*/ |
|||
WAITING_RESPONSE, |
|||
|
|||
/** |
|||
* 发送中 |
|||
* - 正在发送帧中 |
|||
*/ |
|||
SENDING, |
|||
|
|||
/** |
|||
* 关闭中 |
|||
* - 发送了DISC,等DISC-UA |
|||
*/ |
|||
CLOSING, |
|||
|
|||
/** |
|||
* 已关闭 |
|||
* - 会话结束 |
|||
*/ |
|||
CLOSED |
|||
} |
|||
@ -0,0 +1,287 @@ |
|||
package com.techsor.datacenter; |
|||
|
|||
import lombok.extern.slf4j.Slf4j; |
|||
|
|||
import java.io.IOException; |
|||
import java.io.InputStream; |
|||
import java.io.OutputStream; |
|||
import java.net.Socket; |
|||
import java.nio.ByteBuffer; |
|||
import java.nio.ByteOrder; |
|||
import java.util.Arrays; |
|||
import java.util.concurrent.TimeUnit; |
|||
|
|||
/** |
|||
* CSDJ设备模拟器 - 带状态机校验 |
|||
*/ |
|||
@Slf4j |
|||
public class CsdjClientSimulator { |
|||
|
|||
// 配置
|
|||
private static final String SERVER_HOST = "127.0.0.1"; |
|||
private static final int SERVER_PORT = 7114; |
|||
private static final String TERMINAL_ID = "TEST001"; |
|||
private static final String USER_ID = "csdj"; |
|||
private static final String PASSWORD = "csdj"; |
|||
|
|||
// 帧类型定义
|
|||
private static final byte FRAME_ID_PASSWORD = (byte) 0xC1; |
|||
private static final byte FRAME_ID_PASSWORD_UA = (byte) 0xC2; |
|||
private static final byte FRAME_NEW_MESSAGE_SEND_READY = (byte) 0xC3; |
|||
private static final byte FRAME_INFORMATION = (byte) 0x01; |
|||
private static final byte FRAME_RECEIVE_READY = (byte) 0x81; |
|||
private static final byte FRAME_DISCONNECT = (byte) 0xC4; |
|||
private static final byte FRAME_DISCONNECT_UA = (byte) 0xC5; |
|||
|
|||
// 状态机
|
|||
private enum State { |
|||
WAIT_IDPWD, WAIT_NMSR1, WAIT_RR, WAIT_DISC, FINISHED |
|||
} |
|||
|
|||
public static void main(String[] args) throws InterruptedException { |
|||
log.info("CSDJ设备模拟器启动..."); |
|||
log.info("连接服务器: {}:{}", SERVER_HOST, SERVER_PORT); |
|||
|
|||
try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { |
|||
log.info("连接成功!"); |
|||
|
|||
InputStream input = socket.getInputStream(); |
|||
OutputStream output = socket.getOutputStream(); |
|||
|
|||
State state = State.WAIT_IDPWD; |
|||
|
|||
// ========== 步骤1: 等待 IDPWD ==========
|
|||
log.info("---------- 步骤1: 等待 IDPWD ----------"); |
|||
CsdjFrame idpwdFrame = expectFrame(input, FRAME_ID_PASSWORD); |
|||
log.info("✅ 收到: IDPWD (0xC1)"); |
|||
logFrame(idpwdFrame); |
|||
state = State.WAIT_NMSR1; |
|||
|
|||
// ========== 步骤2: 发送 IDPWD-UA ==========
|
|||
log.info("---------- 步骤2: 发送 IDPWD-UA ----------"); |
|||
CsdjFrame idpwdUaFrame = createIdpwdUaFrame(USER_ID, PASSWORD); |
|||
sendFrame(output, idpwdUaFrame); |
|||
log.info("✅ 已发送: IDPWD-UA (0xC2)"); |
|||
logFrame(idpwdUaFrame); |
|||
|
|||
// ========== 步骤3: 等待 NMSR ==========
|
|||
log.info("---------- 步骤3: 等待 NMSR ----------"); |
|||
CsdjFrame nmsrFrame = expectFrame(input, FRAME_NEW_MESSAGE_SEND_READY); |
|||
log.info("✅ 收到: NMSR (0xC3)"); |
|||
logFrame(nmsrFrame); |
|||
state = State.WAIT_RR; |
|||
|
|||
// ========== 步骤4: 发送 I帧(通報数据)==========
|
|||
log.info("---------- 步骤4: 发送通報数据 ----------"); |
|||
byte[] sensorData = createMockSensorData(); // 模拟温湿度数据
|
|||
CsdjFrame iFrame = createInfoFrame(sensorData); |
|||
sendFrame(output, iFrame); |
|||
log.info("✅ 已发送: I帧 (0x01)"); |
|||
logFrame(iFrame); |
|||
log.info("通報数据: {}", bytesToHex(sensorData)); |
|||
|
|||
// ========== 步骤5: 等待 RR ==========
|
|||
log.info("---------- 步骤5: 等待 RR ----------"); |
|||
CsdjFrame rrFrame = expectFrame(input, FRAME_RECEIVE_READY); |
|||
log.info("✅ 收到: RR (0x81)"); |
|||
logFrame(rrFrame); |
|||
state = State.WAIT_DISC; |
|||
|
|||
// ========== 步骤6: 发送 NMSR (我发完了!)==========
|
|||
log.info("---------- 步骤6: 发送 NMSR ----------"); |
|||
CsdjFrame nmsrFrame2 = createSimpleFrame(FRAME_NEW_MESSAGE_SEND_READY); |
|||
sendFrame(output, nmsrFrame2); |
|||
log.info("✅ 已发送: NMSR (0xC3)"); |
|||
logFrame(nmsrFrame2); |
|||
|
|||
// ========== 步骤7: 等待 DISC ==========
|
|||
log.info("---------- 步骤7: 等待 DISC ----------"); |
|||
CsdjFrame discFrame = expectFrame(input, FRAME_DISCONNECT); |
|||
log.info("✅ 收到: DISC (0xC4)"); |
|||
logFrame(discFrame); |
|||
state = State.FINISHED; |
|||
|
|||
// ========== 步骤8: 发送 DISC-UA ==========
|
|||
log.info("---------- 步骤8: 发送 DISC-UA ----------"); |
|||
CsdjFrame discUaFrame = createSimpleFrame(FRAME_DISCONNECT_UA); |
|||
sendFrame(output, discUaFrame); |
|||
log.info("✅ 已发送: DISC-UA (0xC5)"); |
|||
logFrame(discUaFrame); |
|||
|
|||
log.info("========================================"); |
|||
log.info("✅ 通報流程全部完成!"); |
|||
log.info("========================================"); |
|||
|
|||
} catch (IOException e) { |
|||
log.error("❌ 连接错误: {}", e.getMessage(), e); |
|||
} |
|||
|
|||
TimeUnit.SECONDS.sleep(1); |
|||
} |
|||
|
|||
// ==================== 状态机校验帧 ====================
|
|||
private static CsdjFrame expectFrame(InputStream input, byte expectedFrameId) throws IOException { |
|||
CsdjFrame frame = readFrame(input); |
|||
if (frame.controlId != expectedFrameId) { |
|||
String receivedName = getFrameName(frame.controlId); |
|||
String expectedName = getFrameName(expectedFrameId); |
|||
throw new IOException(String.format("❌ 帧类型错误!期望: %s(0x%02X), 实际: %s(0x%02X)", |
|||
expectedName, expectedFrameId & 0xFF, receivedName, frame.controlId & 0xFF)); |
|||
} |
|||
return frame; |
|||
} |
|||
|
|||
// ==================== 帧处理方法 ====================
|
|||
private static CsdjFrame readFrame(InputStream input) throws IOException { |
|||
// 先读头部 (2字节长度 + 12字节终端ID + 2字节控制部 = 16字节)
|
|||
byte[] header = new byte[16]; |
|||
int readLen = input.read(header); |
|||
if (readLen != 16) { |
|||
throw new IOException("读取头部失败: " + readLen + " != 16"); |
|||
} |
|||
|
|||
// 解析长度
|
|||
ByteBuffer buffer = ByteBuffer.wrap(header); |
|||
buffer.order(ByteOrder.BIG_ENDIAN); |
|||
int length = buffer.getShort() & 0xFFFF; |
|||
|
|||
// 读信息部
|
|||
int infoLen = length - 16; |
|||
byte[] info = null; |
|||
if (infoLen > 0) { |
|||
info = new byte[infoLen]; |
|||
readLen = input.read(info); |
|||
if (readLen != infoLen) { |
|||
throw new IOException("读取信息部失败: " + readLen + " != " + infoLen); |
|||
} |
|||
} |
|||
|
|||
return new CsdjFrame(header, info); |
|||
} |
|||
|
|||
private static void sendFrame(OutputStream output, CsdjFrame frame) throws IOException { |
|||
output.write(frame.toBytes()); |
|||
output.flush(); |
|||
} |
|||
|
|||
// ==================== 创建帧方法 ====================
|
|||
private static CsdjFrame createIdpwdUaFrame(String userId, String password) { |
|||
// 信息部: userId (16字节) + password (16字节) = 32字节
|
|||
byte[] info = new byte[32]; |
|||
byte[] userIdBytes = userId.getBytes(); |
|||
byte[] passwordBytes = password.getBytes(); |
|||
System.arraycopy(userIdBytes, 0, info, 0, Math.min(userIdBytes.length, 16)); |
|||
System.arraycopy(passwordBytes, 0, info, 16, Math.min(passwordBytes.length, 16)); |
|||
|
|||
return createFrame(FRAME_ID_PASSWORD_UA, info); |
|||
} |
|||
|
|||
private static CsdjFrame createInfoFrame(byte[] info) { |
|||
return createFrame(FRAME_INFORMATION, info); |
|||
} |
|||
|
|||
private static CsdjFrame createSimpleFrame(byte frameId) { |
|||
return createFrame(frameId, null); |
|||
} |
|||
|
|||
private static CsdjFrame createFrame(byte frameId, byte[] info) { |
|||
int infoLen = (info != null) ? info.length : 0; |
|||
int length = 16 + infoLen; |
|||
|
|||
ByteBuffer buffer = ByteBuffer.allocate(length); |
|||
buffer.order(ByteOrder.BIG_ENDIAN); |
|||
|
|||
// 数据长度
|
|||
buffer.putShort((short) length); |
|||
|
|||
// 终端ID
|
|||
byte[] terminalIdBytes = new byte[12]; |
|||
byte[] src = TERMINAL_ID.getBytes(); |
|||
System.arraycopy(src, 0, terminalIdBytes, 0, Math.min(src.length, 12)); |
|||
buffer.put(terminalIdBytes); |
|||
|
|||
// 控制部: 识别码 + 属性 (0x80: 结束位)
|
|||
buffer.put(frameId); |
|||
buffer.put((byte) 0x80); |
|||
|
|||
// 信息部
|
|||
if (info != null) { |
|||
buffer.put(info); |
|||
} |
|||
|
|||
return new CsdjFrame(buffer.array(), info); |
|||
} |
|||
|
|||
// ==================== 模拟数据 ====================
|
|||
private static byte[] createMockSensorData() { |
|||
// 模拟数据: 温度=25.6℃,湿度=60%
|
|||
byte[] data = new byte[4]; |
|||
data[0] = 0x19; // 温度高8位 (25)
|
|||
data[1] = 0x06; // 温度低8位 (0.6)
|
|||
data[2] = 0x3C; // 湿度60 (0x3C)
|
|||
data[3] = 0x01; // 状态:正常
|
|||
return data; |
|||
} |
|||
|
|||
// ==================== 日志打印 ====================
|
|||
private static String getFrameName(byte frameId) { |
|||
switch (frameId) { |
|||
case FRAME_ID_PASSWORD: return "IDPWD"; |
|||
case FRAME_ID_PASSWORD_UA: return "IDPWD-UA"; |
|||
case FRAME_NEW_MESSAGE_SEND_READY: return "NMSR"; |
|||
case FRAME_INFORMATION: return "I帧"; |
|||
case FRAME_RECEIVE_READY: return "RR"; |
|||
case FRAME_DISCONNECT: return "DISC"; |
|||
case FRAME_DISCONNECT_UA: return "DISC-UA"; |
|||
default: return String.format("UNKNOWN(0x%02X)", frameId); |
|||
} |
|||
} |
|||
|
|||
private static void logFrame(CsdjFrame frame) { |
|||
log.info(" 长度: {} 字节", frame.data.length); |
|||
log.info(" 终端ID: {}", frame.terminalId); |
|||
log.info(" 控制码: 0x{}", String.format("%02X", frame.controlId)); |
|||
if (frame.info != null) { |
|||
log.info(" 信息部: {}", bytesToHex(frame.info)); |
|||
} |
|||
} |
|||
|
|||
private static String bytesToHex(byte[] bytes) { |
|||
StringBuilder sb = new StringBuilder(); |
|||
for (byte b : bytes) { |
|||
sb.append(String.format("%02X ", b)); |
|||
} |
|||
return sb.toString().trim(); |
|||
} |
|||
|
|||
// ==================== 内部类 ====================
|
|||
static class CsdjFrame { |
|||
byte[] data; |
|||
String terminalId; |
|||
byte controlId; |
|||
byte[] info; |
|||
|
|||
CsdjFrame(byte[] header, byte[] info) { |
|||
this.data = new byte[header.length + (info != null ? info.length : 0)]; |
|||
System.arraycopy(header, 0, data, 0, header.length); |
|||
if (info != null) { |
|||
System.arraycopy(info, 0, data, header.length, info.length); |
|||
} |
|||
|
|||
// 解析终端ID
|
|||
byte[] idBytes = new byte[12]; |
|||
System.arraycopy(header, 2, idBytes, 0, 12); |
|||
this.terminalId = new String(idBytes).trim(); |
|||
|
|||
// 解析控制码
|
|||
this.controlId = header[14]; |
|||
|
|||
// 信息部
|
|||
this.info = info; |
|||
} |
|||
|
|||
byte[] toBytes() { |
|||
return data; |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue