|
@@ -0,0 +1,539 @@
|
|
|
|
|
+package cn.oleauto.south.transfer;
|
|
|
|
|
+
|
|
|
|
|
+import cn.oleauto.iec60870.ASdu;
|
|
|
|
|
+import cn.oleauto.iec60870.ASduType;
|
|
|
|
|
+import cn.oleauto.iec60870.CauseOfTransmission;
|
|
|
|
|
+import cn.oleauto.iec60870.Connection;
|
|
|
|
|
+import cn.oleauto.iec60870.ConnectionEventListener;
|
|
|
|
|
+import cn.oleauto.iec60870.ie.IeAckFileOrSectionQualifier;
|
|
|
|
|
+import cn.oleauto.iec60870.ie.IeChecksum;
|
|
|
|
|
+import cn.oleauto.iec60870.ie.IeFileReadyQualifier;
|
|
|
|
|
+import cn.oleauto.iec60870.ie.IeFileSegment;
|
|
|
|
|
+import cn.oleauto.iec60870.ie.IeLastSectionOrSegmentQualifier;
|
|
|
|
|
+import cn.oleauto.iec60870.ie.IeLengthOfFileOrSection;
|
|
|
|
|
+import cn.oleauto.iec60870.ie.IeNameOfFile;
|
|
|
|
|
+import cn.oleauto.iec60870.ie.IeNameOfSection;
|
|
|
|
|
+import cn.oleauto.iec60870.ie.IeSectionReadyQualifier;
|
|
|
|
|
+import cn.oleauto.iec60870.ie.IeSelectAndCallQualifier;
|
|
|
|
|
+import cn.oleauto.iec60870.ie.InformationElement;
|
|
|
|
|
+import cn.oleauto.iec60870.ie.InformationObject;
|
|
|
|
|
+import cn.oleauto.south.efile.EDocument;
|
|
|
|
|
+import cn.oleauto.south.efile.EParser;
|
|
|
|
|
+import cn.oleauto.south.efile.EWriter;
|
|
|
|
|
+
|
|
|
|
|
+import java.io.ByteArrayOutputStream;
|
|
|
|
|
+import java.io.File;
|
|
|
|
|
+import java.io.IOException;
|
|
|
|
|
+import java.nio.charset.Charset;
|
|
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
|
|
+import java.nio.file.Files;
|
|
|
|
|
+import java.util.concurrent.LinkedBlockingQueue;
|
|
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
|
|
+
|
|
|
|
|
+public class EFileTransfer implements ConnectionEventListener {
|
|
|
|
|
+
|
|
|
|
|
+ /** 默认每个分段的字节数(IEC 60870 ASDU 最大 253 字节,减去头部后约 240 字节,取 200 留足余量)。 */
|
|
|
|
|
+ public static final int DEFAULT_SEGMENT_SIZE = 200;
|
|
|
|
|
+
|
|
|
|
|
+ /** 默认等待对端响应超时(毫秒)。 */
|
|
|
|
|
+ public static final long DEFAULT_TIMEOUT_MS = 30_000L;
|
|
|
|
|
+
|
|
|
|
|
+ /** 文件传输过程中使用的公共地址。 */
|
|
|
|
|
+ private final int commonAddress;
|
|
|
|
|
+ private final int informationObjectAddress;
|
|
|
|
|
+ private final int segmentSize;
|
|
|
|
|
+ private final long timeoutMs;
|
|
|
|
|
+
|
|
|
|
|
+ private volatile Connection connection;
|
|
|
|
|
+ private volatile FileReceivedListener receivedListener;
|
|
|
|
|
+ private volatile ConnectionEventListener delegate;
|
|
|
|
|
+
|
|
|
|
|
+ // -------- 发送流程同步 --------
|
|
|
|
|
+ private volatile boolean sending;
|
|
|
|
|
+ private final LinkedBlockingQueue<ASdu> responseQueue = new LinkedBlockingQueue<>();
|
|
|
|
|
+
|
|
|
|
|
+ // -------- 接收流程状态 --------
|
|
|
|
|
+ private volatile boolean receiving;
|
|
|
|
|
+ private int receiveNameOfFile;
|
|
|
|
|
+ private int receiveExpectedLength;
|
|
|
|
|
+ private final ByteArrayOutputStream receiveBuffer = new ByteArrayOutputStream();
|
|
|
|
|
+ private int receiveChecksum;
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 构造传输处理器(不绑定连接)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>适用于需要先把本对象注册为 {@code ConnectionEventListener}(例如通过
|
|
|
|
|
+ * {@code ClientConnectionBuilder.setConnectionEventListener(this)})再构建连接的场景。
|
|
|
|
|
+ * 构建连接后需调用 {@link #setConnection(Connection)} 绑定。
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param commonAddress 公共地址(ASDU Common Address)
|
|
|
|
|
+ * @param informationObjectAddress 信息对象地址
|
|
|
|
|
+ */
|
|
|
|
|
+ public EFileTransfer(int commonAddress, int informationObjectAddress) {
|
|
|
|
|
+ this(null, commonAddress, informationObjectAddress, DEFAULT_SEGMENT_SIZE, DEFAULT_TIMEOUT_MS);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 使用默认分段大小和超时时间构造。
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param connection 已建立并调用 startDataTransfer() 的连接
|
|
|
|
|
+ * @param commonAddress 公共地址(ASDU Common Address)
|
|
|
|
|
+ * @param informationObjectAddress 信息对象地址
|
|
|
|
|
+ */
|
|
|
|
|
+ public EFileTransfer(Connection connection, int commonAddress, int informationObjectAddress) {
|
|
|
|
|
+ this(connection, commonAddress, informationObjectAddress,
|
|
|
|
|
+ DEFAULT_SEGMENT_SIZE, DEFAULT_TIMEOUT_MS);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public EFileTransfer(Connection connection, int commonAddress, int informationObjectAddress,
|
|
|
|
|
+ int segmentSize, long timeoutMs) {
|
|
|
|
|
+ if (segmentSize < 1 || segmentSize > 240) {
|
|
|
|
|
+ throw new IllegalArgumentException("segmentSize 必须在 1~240 之间");
|
|
|
|
|
+ }
|
|
|
|
|
+ this.connection = connection;
|
|
|
|
|
+ this.commonAddress = commonAddress;
|
|
|
|
|
+ this.informationObjectAddress = informationObjectAddress;
|
|
|
|
|
+ this.segmentSize = segmentSize;
|
|
|
|
|
+ this.timeoutMs = timeoutMs;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 绑定连接。在用 {@link #EFileTransfer(int, int)} 构造后必须调用。
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param connection 已建立的连接
|
|
|
|
|
+ */
|
|
|
|
|
+ public void setConnection(Connection connection) {
|
|
|
|
|
+ this.connection = connection;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ====================================================================
|
|
|
|
|
+ // 公开 API —— 发送
|
|
|
|
|
+ // ====================================================================
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 发送一个 E 语言文档(先序列化为字节,再走 IEC 60870 文件传输)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param nameOfFile 文件名(IEC 60870 NOF,1~65535)
|
|
|
|
|
+ * @param document E 语言文档
|
|
|
|
|
+ * @param charset 序列化字符集(常用 UTF-8 或 GB18030)
|
|
|
|
|
+ */
|
|
|
|
|
+ public void sendFile(int nameOfFile, EDocument document, Charset charset) throws IOException {
|
|
|
|
|
+ if (document == null) {
|
|
|
|
|
+ throw new IllegalArgumentException("document 不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
|
|
|
|
+ new EWriter().write(document, bos, charset);
|
|
|
|
|
+ sendFile(nameOfFile, bos.toByteArray());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 发送磁盘上的 E 语言文件(按原始字节传输,不解析)。
|
|
|
|
|
+ */
|
|
|
|
|
+ public void sendFile(int nameOfFile, File file) throws IOException {
|
|
|
|
|
+ if (file == null || !file.isFile()) {
|
|
|
|
|
+ throw new IllegalArgumentException("file 不存在或不是文件: " + file);
|
|
|
|
|
+ }
|
|
|
|
|
+ sendFile(nameOfFile, Files.readAllBytes(file.toPath()));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 发送原始字节(接收方会尝试按 E 语言解析,解析失败时回调中 {@code document} 为 null)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param nameOfFile 文件名(NOF)
|
|
|
|
|
+ * @param data 文件内容
|
|
|
|
|
+ */
|
|
|
|
|
+ public void sendFile(int nameOfFile, byte[] data) throws IOException {
|
|
|
|
|
+ if (data == null) {
|
|
|
|
|
+ throw new IllegalArgumentException("data 不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (connection == null) {
|
|
|
|
|
+ throw new IllegalStateException("未绑定 Connection,请先调用 setConnection()");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (sending || receiving) {
|
|
|
|
|
+ throw new IllegalStateException("已有文件传输正在进行");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ sending = true;
|
|
|
|
|
+ try {
|
|
|
|
|
+ doSend(nameOfFile, data);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ sending = false;
|
|
|
|
|
+ responseQueue.clear();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ====================================================================
|
|
|
|
|
+ // 公开 API —— 接收回调
|
|
|
|
|
+ // ====================================================================
|
|
|
|
|
+
|
|
|
|
|
+ public void setFileReceivedListener(FileReceivedListener listener) {
|
|
|
|
|
+ this.receivedListener = listener;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 设置非文件传输 ASDU 的委托监听器。文件传输相关的 ASDU 不会被透传给委托。
|
|
|
|
|
+ */
|
|
|
|
|
+ public void setDelegate(ConnectionEventListener delegate) {
|
|
|
|
|
+ this.delegate = delegate;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ====================================================================
|
|
|
|
|
+ // ConnectionEventListener 实现
|
|
|
|
|
+ // ====================================================================
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void newASdu(Connection connection, ASdu aSdu) {
|
|
|
|
|
+ ASduType type = aSdu.getTypeIdentification();
|
|
|
|
|
+
|
|
|
|
|
+ // 发送流程中:把文件传输响应放进队列,供 sendFile 的 wait 方法的消费
|
|
|
|
|
+ if (sending && isFileTransferType(type)) {
|
|
|
|
|
+ responseQueue.offer(aSdu);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 接收流程:处理入站文件传输 ASDU
|
|
|
|
|
+ if (isFileTransferType(type)) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ handleReceiveFlow(connection, aSdu);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ notifyError(receiveNameOfFile, "接收文件传输 ASDU 失败: " + e.getMessage());
|
|
|
|
|
+ receiving = false;
|
|
|
|
|
+ receiveBuffer.reset();
|
|
|
|
|
+ }
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 非文件传输 ASDU 透传
|
|
|
|
|
+ if (delegate != null) {
|
|
|
|
|
+ delegate.newASdu(connection, aSdu);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void connectionClosed(Connection connection, IOException cause) {
|
|
|
|
|
+ sending = false;
|
|
|
|
|
+ receiving = false;
|
|
|
|
|
+ responseQueue.clear();
|
|
|
|
|
+ if (delegate != null) {
|
|
|
|
|
+ delegate.connectionClosed(connection, cause);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void dataTransferStateChanged(Connection connection, boolean stopped) {
|
|
|
|
|
+ if (delegate != null) {
|
|
|
|
|
+ delegate.dataTransferStateChanged(connection, stopped);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ====================================================================
|
|
|
|
|
+ // 发送流程实现
|
|
|
|
|
+ // ====================================================================
|
|
|
|
|
+
|
|
|
|
|
+ private void doSend(int nameOfFile, byte[] data) throws IOException {
|
|
|
|
|
+ int checksum = computeChecksum(data);
|
|
|
|
|
+
|
|
|
|
|
+ // 1. fileReady
|
|
|
|
|
+ connection.fileReady(commonAddress, informationObjectAddress,
|
|
|
|
|
+ new IeNameOfFile(nameOfFile),
|
|
|
|
|
+ new IeLengthOfFileOrSection(data.length),
|
|
|
|
|
+ new IeFileReadyQualifier(1, false));
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 等待 callOrSelectFiles (action=2 = call file)
|
|
|
|
|
+ ASdu call = waitForResponse("call file");
|
|
|
|
|
+ if (!isCallFile(call, nameOfFile)) {
|
|
|
|
|
+ throw new IOException("期望 call file 响应,实际收到: " + call.getTypeIdentification());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 3. sectionReady (section 0)
|
|
|
|
|
+ connection.sectionReady(commonAddress, informationObjectAddress,
|
|
|
|
|
+ new IeNameOfFile(nameOfFile),
|
|
|
|
|
+ new IeNameOfSection(0),
|
|
|
|
|
+ new IeLengthOfFileOrSection(data.length),
|
|
|
|
|
+ new IeSectionReadyQualifier(1, false));
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 等待 ack section (action=2)
|
|
|
|
|
+ ASdu ackSection = waitForResponse("ack section");
|
|
|
|
|
+ if (!isAckSection(ackSection)) {
|
|
|
|
|
+ throw new IOException("期望 ack section 响应,实际收到: " + ackSection.getTypeIdentification());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 5. 发送分段
|
|
|
|
|
+ int offset = 0;
|
|
|
|
|
+ while (offset < data.length) {
|
|
|
|
|
+ int len = Math.min(segmentSize, data.length - offset);
|
|
|
|
|
+ byte[] seg = new byte[len];
|
|
|
|
|
+ System.arraycopy(data, offset, seg, 0, len);
|
|
|
|
|
+ offset += len;
|
|
|
|
|
+
|
|
|
|
|
+ connection.sendSegment(commonAddress, informationObjectAddress,
|
|
|
|
|
+ new IeNameOfFile(nameOfFile),
|
|
|
|
|
+ new IeNameOfSection(0),
|
|
|
|
|
+ new IeFileSegment(seg, 0, len));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 6. lastSectionOrSegment (qualifier=3 = last segment of file) + checksum
|
|
|
|
|
+ connection.lastSectionOrSegment(commonAddress, informationObjectAddress,
|
|
|
|
|
+ new IeNameOfFile(nameOfFile),
|
|
|
|
|
+ new IeNameOfSection(0),
|
|
|
|
|
+ new IeLastSectionOrSegmentQualifier(3),
|
|
|
|
|
+ new IeChecksum(checksum));
|
|
|
|
|
+
|
|
|
|
|
+ // 7. 等待 ack section (action=2)
|
|
|
|
|
+ ASdu ackSec2 = waitForResponse("ack section (after last segment)");
|
|
|
|
|
+ if (!isAckSection(ackSec2)) {
|
|
|
|
|
+ throw new IOException("期望末段后 ack section,实际收到: " + ackSec2.getTypeIdentification());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 8. 等待 ack file (action=1)
|
|
|
|
|
+ ASdu ackFile = waitForResponse("ack file");
|
|
|
|
|
+ if (!isAckFile(ackFile)) {
|
|
|
|
|
+ throw new IOException("期望 ack file 响应,实际收到: " + ackFile.getTypeIdentification());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private ASdu waitForResponse(String what) throws IOException {
|
|
|
|
|
+ try {
|
|
|
|
|
+ ASdu response = responseQueue.poll(timeoutMs, TimeUnit.MILLISECONDS);
|
|
|
|
|
+ if (response == null) {
|
|
|
|
|
+ throw new IOException("等待 " + what + " 超时(" + timeoutMs + " ms)");
|
|
|
|
|
+ }
|
|
|
|
|
+ return response;
|
|
|
|
|
+ } catch (InterruptedException e) {
|
|
|
|
|
+ Thread.currentThread().interrupt();
|
|
|
|
|
+ throw new IOException("等待 " + what + " 被中断", e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ====================================================================
|
|
|
|
|
+ // 接收流程实现
|
|
|
|
|
+ // ====================================================================
|
|
|
|
|
+
|
|
|
|
|
+ private void handleReceiveFlow(Connection connection, ASdu aSdu) throws IOException {
|
|
|
|
|
+ ASduType type = aSdu.getTypeIdentification();
|
|
|
|
|
+ InformationObject io = firstInformationObject(aSdu);
|
|
|
|
|
+ if (io == null) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ switch (type) {
|
|
|
|
|
+ case F_FR_NA_1: // 120 fileReady
|
|
|
|
|
+ onFileReady(connection, io);
|
|
|
|
|
+ break;
|
|
|
|
|
+ case F_SR_NA_1: // 121 sectionReady
|
|
|
|
|
+ onSectionReady(connection, io);
|
|
|
|
|
+ break;
|
|
|
|
|
+ case F_SG_NA_1: // 125 segment
|
|
|
|
|
+ onSegment(io);
|
|
|
|
|
+ break;
|
|
|
|
|
+ case F_LS_NA_1: // 123 lastSectionOrSegment
|
|
|
|
|
+ onLastSectionOrSegment(connection, io);
|
|
|
|
|
+ break;
|
|
|
|
|
+ default:
|
|
|
|
|
+ // 其他文件传输类型(如 F_SC_NA_1 call/select、F_AF_NA_1 ack)在接收方不主动处理
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void onFileReady(Connection connection, InformationObject io) throws IOException {
|
|
|
|
|
+ InformationElement[] elems = firstElementSet(io);
|
|
|
|
|
+ IeNameOfFile nof = (IeNameOfFile) elems[0];
|
|
|
|
|
+ IeLengthOfFileOrSection lof = (IeLengthOfFileOrSection) elems[1];
|
|
|
|
|
+
|
|
|
|
|
+ receiveNameOfFile = nof.getValue();
|
|
|
|
|
+ receiveExpectedLength = lof.getValue();
|
|
|
|
|
+ receiveBuffer.reset();
|
|
|
|
|
+ receiveChecksum = 0;
|
|
|
|
|
+ receiving = true;
|
|
|
|
|
+
|
|
|
|
|
+ // 回复 call file (SCQ action=2)
|
|
|
|
|
+ connection.callOrSelectFiles(commonAddress, CauseOfTransmission.FILE_TRANSFER,
|
|
|
|
|
+ informationObjectAddress,
|
|
|
|
|
+ new IeNameOfFile(receiveNameOfFile),
|
|
|
|
|
+ new IeNameOfSection(0),
|
|
|
|
|
+ new IeSelectAndCallQualifier(2, 0));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void onSectionReady(Connection connection, InformationObject io) throws IOException {
|
|
|
|
|
+ InformationElement[] elems = firstElementSet(io);
|
|
|
|
|
+ IeNameOfFile nof = (IeNameOfFile) elems[0];
|
|
|
|
|
+ IeNameOfSection nos = (IeNameOfSection) elems[1];
|
|
|
|
|
+ IeLengthOfFileOrSection los = (IeLengthOfFileOrSection) elems[2];
|
|
|
|
|
+
|
|
|
|
|
+ // 回复 ack section (AFQ action=2)
|
|
|
|
|
+ connection.ackFileOrSection(commonAddress, informationObjectAddress,
|
|
|
|
|
+ new IeNameOfFile(nof.getValue()),
|
|
|
|
|
+ nos,
|
|
|
|
|
+ new IeAckFileOrSectionQualifier(2, 0));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void onSegment(InformationObject io) throws IOException {
|
|
|
|
|
+ InformationElement[] elems = firstElementSet(io);
|
|
|
|
|
+ IeFileSegment segment = (IeFileSegment) elems[2];
|
|
|
|
|
+ byte[] data = segment.getSegment();
|
|
|
|
|
+ receiveBuffer.write(data);
|
|
|
|
|
+ for (byte b : data) {
|
|
|
|
|
+ receiveChecksum = (receiveChecksum + (b & 0xff)) & 0xff;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void onLastSectionOrSegment(Connection connection, InformationObject io) throws IOException {
|
|
|
|
|
+ InformationElement[] elems = firstElementSet(io);
|
|
|
|
|
+ IeNameOfFile nof = (IeNameOfFile) elems[0];
|
|
|
|
|
+ IeNameOfSection nos = (IeNameOfSection) elems[1];
|
|
|
|
|
+ IeLastSectionOrSegmentQualifier lsq = (IeLastSectionOrSegmentQualifier) elems[2];
|
|
|
|
|
+ IeChecksum chs = (IeChecksum) elems[3];
|
|
|
|
|
+
|
|
|
|
|
+ byte[] fileData = receiveBuffer.toByteArray();
|
|
|
|
|
+ int expectedChecksum = computeChecksum(fileData);
|
|
|
|
|
+
|
|
|
|
|
+ // ack section (action=2)
|
|
|
|
|
+ connection.ackFileOrSection(commonAddress, informationObjectAddress,
|
|
|
|
|
+ new IeNameOfFile(nof.getValue()),
|
|
|
|
|
+ nos,
|
|
|
|
|
+ new IeAckFileOrSectionQualifier(2, 0));
|
|
|
|
|
+
|
|
|
|
|
+ // ack file (action=1)
|
|
|
|
|
+ connection.ackFileOrSection(commonAddress, informationObjectAddress,
|
|
|
|
|
+ new IeNameOfFile(nof.getValue()),
|
|
|
|
|
+ nos,
|
|
|
|
|
+ new IeAckFileOrSectionQualifier(1, 0));
|
|
|
|
|
+
|
|
|
|
|
+ receiving = false;
|
|
|
|
|
+
|
|
|
|
|
+ // 校验校验和
|
|
|
|
|
+ if (chs.getValue() != expectedChecksum) {
|
|
|
|
|
+ notifyError(nof.getValue(),
|
|
|
|
|
+ "校验和不匹配: 收到=" + chs.getValue() + ", 计算=" + expectedChecksum);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 尝试解析为 E 语言文档
|
|
|
|
|
+ EDocument document = null;
|
|
|
|
|
+ try {
|
|
|
|
|
+ document = new EParser().parse(new String(fileData, StandardCharsets.UTF_8));
|
|
|
|
|
+ } catch (Exception ignored) {
|
|
|
|
|
+ // 非 E 语言或 GBK 编码时尝试 GB18030
|
|
|
|
|
+ try {
|
|
|
|
|
+ document = new EParser().parse(new String(fileData,
|
|
|
|
|
+ cn.oleauto.south.efile.ECharset.GB18030));
|
|
|
|
|
+ } catch (Exception ignored2) {
|
|
|
|
|
+ // 确实不是 E 语言文件,回调中 document 为 null
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 回调
|
|
|
|
|
+ FileReceivedListener listener = receivedListener;
|
|
|
|
|
+ if (listener != null) {
|
|
|
|
|
+ listener.onFileReceived(nof.getValue(), fileData, document);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 重置接收缓冲
|
|
|
|
|
+ receiveBuffer.reset();
|
|
|
|
|
+ receiveChecksum = 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ====================================================================
|
|
|
|
|
+ // 工具方法
|
|
|
|
|
+ // ====================================================================
|
|
|
|
|
+
|
|
|
|
|
+ private boolean isFileTransferType(ASduType type) {
|
|
|
|
|
+ switch (type) {
|
|
|
|
|
+ case F_FR_NA_1:
|
|
|
|
|
+ case F_SR_NA_1:
|
|
|
|
|
+ case F_SC_NA_1:
|
|
|
|
|
+ case F_LS_NA_1:
|
|
|
|
|
+ case F_AF_NA_1:
|
|
|
|
|
+ case F_SG_NA_1:
|
|
|
|
|
+ return true;
|
|
|
|
|
+ default:
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private boolean isCallFile(ASdu aSdu, int nameOfFile) {
|
|
|
|
|
+ if (aSdu.getTypeIdentification() != ASduType.F_SC_NA_1) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ InformationObject io = firstInformationObject(aSdu);
|
|
|
|
|
+ if (io == null) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ InformationElement[] elems = firstElementSet(io);
|
|
|
|
|
+ IeNameOfFile nof = (IeNameOfFile) elems[0];
|
|
|
|
|
+ IeSelectAndCallQualifier scq = (IeSelectAndCallQualifier) elems[2];
|
|
|
|
|
+ return nof.getValue() == nameOfFile && scq.getRequest() == 2;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private boolean isAckSection(ASdu aSdu) {
|
|
|
|
|
+ return isAckOf(aSdu, 2);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private boolean isAckFile(ASdu aSdu) {
|
|
|
|
|
+ return isAckOf(aSdu, 1);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private boolean isAckOf(ASdu aSdu, int action) {
|
|
|
|
|
+ if (aSdu.getTypeIdentification() != ASduType.F_AF_NA_1) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ InformationObject io = firstInformationObject(aSdu);
|
|
|
|
|
+ if (io == null) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ InformationElement[] elems = firstElementSet(io);
|
|
|
|
|
+ IeAckFileOrSectionQualifier afq = (IeAckFileOrSectionQualifier) elems[2];
|
|
|
|
|
+ return afq.getRequest() == action;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private InformationObject firstInformationObject(ASdu aSdu) {
|
|
|
|
|
+ InformationObject[] ios = aSdu.getInformationObjects();
|
|
|
|
|
+ return (ios != null && ios.length > 0) ? ios[0] : null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private InformationElement[] firstElementSet(InformationObject io) {
|
|
|
|
|
+ InformationElement[][] all = io.getInformationElements();
|
|
|
|
|
+ return (all != null && all.length > 0) ? all[0] : new InformationElement[0];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static int computeChecksum(byte[] data) {
|
|
|
|
|
+ int sum = 0;
|
|
|
|
|
+ for (byte b : data) {
|
|
|
|
|
+ sum = (sum + (b & 0xff)) & 0xff;
|
|
|
|
|
+ }
|
|
|
|
|
+ return sum;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void notifyError(int nameOfFile, String message) {
|
|
|
|
|
+ FileReceivedListener listener = receivedListener;
|
|
|
|
|
+ if (listener != null) {
|
|
|
|
|
+ listener.onError(nameOfFile, message);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ====================================================================
|
|
|
|
|
+ // 回调接口
|
|
|
|
|
+ // ====================================================================
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 文件接收完成回调。
|
|
|
|
|
+ */
|
|
|
|
|
+ public interface FileReceivedListener {
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 文件接收完成时调用。
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param nameOfFile 文件名(NOF)
|
|
|
|
|
+ * @param data 原始字节
|
|
|
|
|
+ * @param document 解析后的 E 语言文档;如果数据不是合法 E 语言文件则为 null
|
|
|
|
|
+ */
|
|
|
|
|
+ void onFileReceived(int nameOfFile, byte[] data, EDocument document);
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 文件传输出错时调用(校验和失败、协议异常等)。
|
|
|
|
|
+ */
|
|
|
|
|
+ default void onError(int nameOfFile, String message) {
|
|
|
|
|
+ // 默认空实现,子类按需覆写
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|