瀏覽代碼

iec60870协议提交

songwenbin 2 年之前
父節點
當前提交
6216791826
共有 63 個文件被更改,包括 6357 次插入1 次删除
  1. 1 1
      gateway/src/main/java/com/gyee/edge/gateway/bridge/test/ReadGolden.java
  2. 7 0
      protocol/iec60870/build.gradle
  3. 204 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/APdu.java
  4. 305 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/ASdu.java
  5. 472 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/ASduType.java
  6. 88 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/CauseOfTransmission.java
  7. 134 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/ClientConnectionBuilder.java
  8. 169 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/CommonBuilder.java
  9. 1255 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/Connection.java
  10. 31 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/ConnectionEventListener.java
  11. 170 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/ConnectionSettings.java
  12. 42 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/ExtendedDataInputStream.java
  13. 161 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/Server.java
  14. 20 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/ServerEventListener.java
  15. 111 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/ServerThread.java
  16. 63 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/TimeoutManager.java
  17. 85 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/TimeoutTask.java
  18. 36 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/Util.java
  19. 53 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeAbstractQualifierOfCommand.java
  20. 66 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeAbstractQuality.java
  21. 46 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeAckFileOrSectionQualifier.java
  22. 114 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeBinaryCounterReading.java
  23. 93 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeBinaryStateInformation.java
  24. 67 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeCauseOfInitialization.java
  25. 35 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeChecksum.java
  26. 83 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeDoubleCommand.java
  27. 58 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeDoublePointWithQuality.java
  28. 49 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeFileReadyQualifier.java
  29. 49 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeFileSegment.java
  30. 32 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeFixedTestBitPattern.java
  31. 35 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeLastSectionOrSegmentQualifier.java
  32. 44 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeLengthOfFileOrSection.java
  33. 39 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeNameOfFile.java
  34. 36 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeNameOfSection.java
  35. 79 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeNormalizedValue.java
  36. 65 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeProtectionOutputCircuitInformation.java
  37. 32 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeProtectionQuality.java
  38. 80 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeProtectionStartEvent.java
  39. 43 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeQualifierOfCounterInterrogation.java
  40. 35 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeQualifierOfInterrogation.java
  41. 35 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeQualifierOfParameterActivation.java
  42. 57 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeQualifierOfParameterOfMeasuredValues.java
  43. 35 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeQualifierOfResetProcessCommand.java
  44. 46 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeQualifierOfSetPointCommand.java
  45. 31 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeQuality.java
  46. 83 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeRegulatingStepCommand.java
  47. 29 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeScaledValue.java
  48. 47 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeSectionReadyQualifier.java
  49. 45 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeSelectAndCallQualifier.java
  50. 42 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeShortFloat.java
  51. 32 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeSingleCommand.java
  52. 32 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeSinglePointWithQuality.java
  53. 100 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeSingleProtectionEvent.java
  54. 88 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeStatusAndStatusChanges.java
  55. 82 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeStatusOfFile.java
  56. 46 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeTestSequenceCounter.java
  57. 49 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeTime16.java
  58. 51 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeTime24.java
  59. 286 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeTime56.java
  60. 71 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeValueWithTransientState.java
  61. 10 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/InformationElement.java
  62. 472 0
      protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/InformationObject.java
  63. 1 0
      settings.gradle

+ 1 - 1
gateway/src/main/java/com/gyee/edge/gateway/bridge/test/ReadGolden.java

@@ -2,7 +2,7 @@ package com.gyee.edge.gateway.bridge.test;
 
 
 import com.gyee.edge.common.utils.ByteUtil;
-import com.gyee.edge.gateway.protobuf.UserProto;
+import com.gyee.edge.loader.protobuf.UserProto;
 
 import java.io.UnsupportedEncodingException;
 import java.util.*;

+ 7 - 0
protocol/iec60870/build.gradle

@@ -0,0 +1,7 @@
+buildscript {
+}
+
+dependencies {
+    api("commons-codec:commons-codec:$commonsCodecVersion")
+    api("org.apache.commons:commons-lang3:$commonsLang3Version")
+}

+ 204 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/APdu.java

@@ -0,0 +1,204 @@
+package com.gyee.edge.protocol.iec60870;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.net.Socket;
+import java.text.MessageFormat;
+
+
+public class APdu {
+
+    private static final int CONTROL_FIELDS_LENGTH = 4;
+    /**
+     * Since the length of the control field is control field is 4 octets.
+     */
+    private static final int MIN_APDU_LENGTH = CONTROL_FIELDS_LENGTH;
+    /**
+     * The maximum length of APDU for both directions is 253. APDU max = 255 minus start and length octet.
+     */
+    private static final int MAX_APDU_LENGTH = 253;
+    /**
+     * START flag of an APDU.
+     */
+    private static final byte START_FLAG = 0x68;
+    private final int sendSeqNum;
+    private final int receiveSeqNum;
+    private final ApciType apciType;
+    private final ASdu aSdu;
+    public APdu(int sendSeqNum, int receiveSeqNum, ApciType apciType, ASdu aSdu) {
+        this.sendSeqNum = sendSeqNum;
+        this.receiveSeqNum = receiveSeqNum;
+        this.apciType = apciType;
+        this.aSdu = aSdu;
+    }
+
+    @SuppressWarnings("resource")
+    public static APdu decode(Socket socket, ConnectionSettings settings) throws IOException {
+        socket.setSoTimeout(0);
+
+        ExtendedDataInputStream is = new ExtendedDataInputStream(socket.getInputStream());
+
+        if (is.readByte() != START_FLAG) {
+            throw new IOException("Message does not start with START flag (0x68). Broken connection.");
+        }
+
+        socket.setSoTimeout(settings.getMessageFragmentTimeout());
+
+        int length = readApduLength(is);
+
+        byte[] aPduControlFields = readControlFields(is);
+
+        ApciType apciType = ApciType.apciTypeFor(aPduControlFields[0]);
+        switch (apciType) {
+            case I_FORMAT:
+                int sendSeqNum = seqNumFrom(aPduControlFields[0], aPduControlFields[1]);
+                int receiveSeqNum = seqNumFrom(aPduControlFields[2], aPduControlFields[3]);
+
+                int aSduLength = length - CONTROL_FIELDS_LENGTH;
+
+                return new APdu(sendSeqNum, receiveSeqNum, apciType, ASdu.decode(is, settings, aSduLength));
+            case S_FORMAT:
+                return new APdu(0, seqNumFrom(aPduControlFields[2], aPduControlFields[3]), apciType, null);
+
+            default:
+                return new APdu(0, 0, apciType, null);
+        }
+
+    }
+
+    private static int seqNumFrom(byte b1, byte b2) {
+        return ((b1 & 0xfe) >> 1) + ((b2 & 0xff) << 7);
+    }
+
+    private static int readApduLength(DataInputStream is) throws IOException {
+        int length = is.readUnsignedByte();
+
+        if (length < MIN_APDU_LENGTH || length > MAX_APDU_LENGTH) {
+            String msg = MessageFormat
+                    .format("APDU has an invalid length must be between 4 and 253.\nReceived length was: {0}.", length);
+            throw new IOException(msg);
+        }
+        return length;
+    }
+
+    private static byte[] readControlFields(DataInputStream is) throws IOException {
+        byte[] aPduControlFields = new byte[CONTROL_FIELDS_LENGTH];
+        is.readFully(aPduControlFields);
+        return aPduControlFields;
+    }
+
+    private static void setV3To5zero(byte[] buffer) {
+        buffer[3] = 0x00;
+        buffer[4] = 0x00;
+        buffer[5] = 0x00;
+    }
+
+    public int encode(byte[] buffer, ConnectionSettings settings) {
+
+        buffer[0] = START_FLAG;
+
+        int length = CONTROL_FIELDS_LENGTH;
+
+        if (apciType == ApciType.I_FORMAT) {
+            buffer[2] = (byte) (sendSeqNum << 1);
+            buffer[3] = (byte) (sendSeqNum >> 7);
+            writeReceiveSeqNumTo(buffer);
+
+            length += aSdu.encode(buffer, 6, settings);
+        } else if (apciType == ApciType.STARTDT_ACT) {
+            buffer[2] = 0x07;
+            setV3To5zero(buffer);
+        } else if (apciType == ApciType.STARTDT_CON) {
+            buffer[2] = 0x0b;
+            setV3To5zero(buffer);
+        } else if (apciType == ApciType.STOPDT_ACT) {
+            buffer[2] = 0x13;
+            setV3To5zero(buffer);
+        } else if (apciType == ApciType.STOPDT_CON) {
+            buffer[2] = 0x23;
+            setV3To5zero(buffer);
+        } else if (apciType == ApciType.S_FORMAT) {
+            buffer[2] = 0x01;
+            buffer[3] = 0x00;
+            writeReceiveSeqNumTo(buffer);
+        }
+
+        buffer[1] = (byte) length;
+
+        return length + 2;
+
+    }
+
+    private void writeReceiveSeqNumTo(byte[] buffer) {
+        buffer[4] = (byte) (receiveSeqNum << 1);
+        buffer[5] = (byte) (receiveSeqNum >> 7);
+    }
+
+    public ApciType getApciType() {
+        return apciType;
+    }
+
+    public int getSendSeqNumber() {
+        return sendSeqNum;
+    }
+
+    public int getReceiveSeqNumber() {
+        return receiveSeqNum;
+    }
+
+    public ASdu getASdu() {
+        return aSdu;
+    }
+
+    public enum ApciType {
+        /**
+         * Numbered information transfer. I format APDUs always contain an ASDU.
+         */
+        I_FORMAT,
+        /**
+         * Numbered supervisory functions. S format APDUs consist of the APCI only.
+         */
+        S_FORMAT,
+
+        // Unnumbered control functions.
+
+        TESTFR_CON,
+        TESTFR_ACT,
+        STOPDT_CON,
+        STOPDT_ACT,
+        STARTDT_CON,
+        STARTDT_ACT;
+
+        private static ApciType apciTypeFor(byte controlField1) {
+            if ((controlField1 & 0x01) == 0) {
+                return ApciType.I_FORMAT;
+            }
+
+            switch ((controlField1 & 0x03)) {
+                case 1:
+                    return ApciType.S_FORMAT;
+                case 3:
+                default:
+                    return unnumberedFormatFor(controlField1);
+            }
+
+        }
+
+        private static ApciType unnumberedFormatFor(byte controlField1) {
+            if ((controlField1 & 0x80) == 0x80) {
+                return ApciType.TESTFR_CON;
+            } else if (controlField1 == 0x43) {
+                return ApciType.TESTFR_ACT;
+            } else if (controlField1 == 0x23) {
+                return ApciType.STOPDT_CON;
+            } else if (controlField1 == 0x13) {
+                return ApciType.STOPDT_ACT;
+            } else if (controlField1 == 0x0B) {
+                return ApciType.STARTDT_CON;
+            } else {
+                return ApciType.STARTDT_ACT;
+            }
+        }
+    }
+
+}

+ 305 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/ASdu.java

@@ -0,0 +1,305 @@
+package com.gyee.edge.protocol.iec60870;
+
+import java.io.IOException;
+import java.text.MessageFormat;
+
+import javax.xml.bind.DatatypeConverter;
+
+import com.gyee.edge.protocol.iec60870.infoelement.InformationObject;
+
+/**
+ * The application service data unit (ASDU). The ASDU is the payload of the application protocol data unit (APDU). Its
+ * structure is defined in IEC 60870-5-101. The ASDU consists of the Data Unit Identifier and a number of Information
+ * Objects. The Data Unit Identifier contains:
+ *
+ * <ul>
+ * <li>{@link com.gyee.edge.protocol.iec60870.ASduType} (1 byte)</li>
+ * <li>Variable Structure Qualifier (1 byte) - specifies how many Information Objects and Information Element sets are
+ * part of the ASDU.</li>
+ * <li>Cause of Transmission (COT, 1 or 2 bytes) - The first byte codes the actual
+ * {@link com.gyee.edge.protocol.iec60870.CauseOfTransmission}, a bit indicating whether the message was sent for test purposes only
+ * and a bit indicating whether a confirmation message is positive or negative. The optional second byte of the Cause of
+ * Transmission field is the Originator Address. It is the address of the originating controlling station so that
+ * responses can be routed back to it.</li>
+ * <li>Common Address of ASDU (1 or 2 bytes) - the address of the target station or the broadcast address. If the field
+ * length of the common address is 1 byte then the addresses 1 to 254 are used to address a particular station (station
+ * address) and 255 is used for broadcast addressing. If the field length of the common address is 2 bytes then the
+ * addresses 1 to 65534 are used to address a particular station and 65535 is used for broadcast addressing. Broadcast
+ * addressing is only allowed for certain TypeIDs.</li>
+ * <li>A list of Information Objects containing the actual actual data in the form of Information Elements.</li>
+ * </ul>
+ */
+public class ASdu {
+
+    private final ASduType aSduType;
+    private final boolean isSequenceOfElements;
+    private final CauseOfTransmission causeOfTransmission;
+    private final boolean test;
+    private final boolean negativeConfirm;
+    private final int originatorAddress;
+    private final int commonAddress;
+    private final InformationObject[] informationObjects;
+    private final byte[] privateInformation;
+    private final int sequenceLength;
+
+    /**
+     * Use this constructor to create standardized ASDUs.
+     *
+     * @param typeId               type identification field that defines the purpose and contents of the ASDU
+     * @param isSequenceOfElements if {@code false} then the ASDU contains a sequence of information objects consisting of a fixed number
+     *                             of information elements. If {@code true} the ASDU contains a single information object with a sequence
+     *                             of elements.
+     * @param causeOfTransmission  the cause of transmission
+     * @param test                 true if the ASDU is sent for test purposes
+     * @param negativeConfirm      true if the ASDU is a negative confirmation
+     * @param originatorAddress    the address of the originating controlling station so that responses can be routed back to it
+     * @param commonAddress        the address of the target station or the broadcast address.
+     * @param informationObjects   the information objects containing the actual data
+     */
+    public ASdu(ASduType typeId, boolean isSequenceOfElements, CauseOfTransmission causeOfTransmission, boolean test,
+                boolean negativeConfirm, int originatorAddress, int commonAddress,
+                InformationObject... informationObjects) {
+
+        this.aSduType = typeId;
+        this.isSequenceOfElements = isSequenceOfElements;
+        this.causeOfTransmission = causeOfTransmission;
+        this.test = test;
+        this.negativeConfirm = negativeConfirm;
+        this.originatorAddress = originatorAddress;
+        this.commonAddress = commonAddress;
+        this.informationObjects = informationObjects;
+        privateInformation = null;
+        if (isSequenceOfElements) {
+            sequenceLength = informationObjects[0].getInformationElements().length;
+        } else {
+            sequenceLength = informationObjects.length;
+        }
+    }
+
+    /**
+     * Use this constructor to create private ASDU with TypeIDs in the range 128-255.
+     *
+     * @param typeId               type identification field that defines the purpose and contents of the ASDU
+     * @param isSequenceOfElements if false then the ASDU contains a sequence of information objects consisting of a fixed number of
+     *                             information elements. If true the ASDU contains a single information object with a sequence of
+     *                             elements.
+     * @param sequenceLength       the number of information objects or the number elements depending depending on which is transmitted
+     *                             as a sequence
+     * @param causeOfTransmission  the cause of transmission
+     * @param test                 true if the ASDU is sent for test purposes
+     * @param negativeConfirm      true if the ASDU is a negative confirmation
+     * @param originatorAddress    the address of the originating controlling station so that responses can be routed back to it
+     * @param commonAddress        the address of the target station or the broadcast address.
+     * @param privateInformation   the bytes to be transmitted as payload
+     */
+    public ASdu(ASduType typeId, boolean isSequenceOfElements, int sequenceLength,
+                CauseOfTransmission causeOfTransmission, boolean test, boolean negativeConfirm, int originatorAddress,
+                int commonAddress, byte[] privateInformation) {
+
+        this.aSduType = typeId;
+        this.isSequenceOfElements = isSequenceOfElements;
+        this.causeOfTransmission = causeOfTransmission;
+        this.test = test;
+        this.negativeConfirm = negativeConfirm;
+        this.originatorAddress = originatorAddress;
+        this.commonAddress = commonAddress;
+        informationObjects = null;
+        this.privateInformation = privateInformation;
+        this.sequenceLength = sequenceLength;
+    }
+
+    static ASdu decode(ExtendedDataInputStream is, ConnectionSettings settings, int aSduLength) throws IOException {
+
+        int typeIdCode = is.readUnsignedByte();
+
+        ASduType typeId = ASduType.typeFor(typeIdCode);
+
+        if (typeId == null) {
+            throw new IOException(MessageFormat.format("Unknown Type Identification: {0}", typeIdCode));
+        }
+
+        int currentByte = is.readUnsignedByte();
+
+        boolean isSequenceOfElements = byteHasMask(currentByte, 0x80);
+
+        int numberOfSequenceElements;
+        int numberOfInformationObjects;
+
+        int sequenceLength = currentByte & 0x7f;
+        if (isSequenceOfElements) {
+            numberOfSequenceElements = sequenceLength;
+            numberOfInformationObjects = 1;
+        } else {
+            numberOfInformationObjects = sequenceLength;
+            numberOfSequenceElements = 1;
+        }
+
+        currentByte = is.readUnsignedByte();
+        CauseOfTransmission causeOfTransmission = CauseOfTransmission.causeFor(currentByte & 0x3f);
+        boolean test = byteHasMask(currentByte, 0x80);
+        boolean negativeConfirm = byteHasMask(currentByte, 0x40);
+
+        int originatorAddress;
+        if (settings.getCotFieldLength() == 2) {
+            originatorAddress = is.readUnsignedByte();
+            aSduLength--;
+        } else {
+            originatorAddress = -1;
+        }
+
+        int commonAddress;
+        if (settings.getCommonAddressFieldLength() == 1) {
+            commonAddress = is.readUnsignedByte();
+        } else {
+            commonAddress = is.readUnsignedByte() | (is.readUnsignedByte() << 8);
+
+            aSduLength--;
+        }
+
+        InformationObject[] informationObjects;
+        byte[] privateInformation;
+        if (typeIdCode < 128) {
+
+            informationObjects = new InformationObject[numberOfInformationObjects];
+
+            int ioaFieldLength = settings.getIoaFieldLength();
+            for (int i = 0; i < numberOfInformationObjects; i++) {
+                informationObjects[i] = InformationObject.decode(is, typeId, numberOfSequenceElements, ioaFieldLength);
+            }
+
+            return new ASdu(typeId, isSequenceOfElements, causeOfTransmission, test, negativeConfirm, originatorAddress,
+                    commonAddress, informationObjects);
+        } else {
+            privateInformation = new byte[aSduLength - 4];
+            is.readFully(privateInformation);
+
+            return new ASdu(typeId, isSequenceOfElements, sequenceLength, causeOfTransmission, test, negativeConfirm,
+                    originatorAddress, commonAddress, privateInformation);
+        }
+
+    }
+
+    private static boolean byteHasMask(int b, int mask) {
+        return (b & mask) == mask;
+    }
+
+    public ASduType getTypeIdentification() {
+        return aSduType;
+    }
+
+    public boolean isSequenceOfElements() {
+        return isSequenceOfElements;
+    }
+
+    public int getSequenceLength() {
+        return sequenceLength;
+    }
+
+    public CauseOfTransmission getCauseOfTransmission() {
+        return causeOfTransmission;
+    }
+
+    public boolean isTestFrame() {
+        return test;
+    }
+
+    public boolean isNegativeConfirm() {
+        return negativeConfirm;
+    }
+
+    public Integer getOriginatorAddress() {
+        return originatorAddress;
+    }
+
+    public int getCommonAddress() {
+        return commonAddress;
+    }
+
+    public InformationObject[] getInformationObjects() {
+        return informationObjects;
+    }
+
+    public byte[] getPrivateInformation() {
+        return privateInformation;
+    }
+
+    int encode(byte[] buffer, int i, ConnectionSettings settings) {
+
+        int origi = i;
+
+        buffer[i++] = (byte) aSduType.getId();
+        if (isSequenceOfElements) {
+            buffer[i++] = (byte) (sequenceLength | 0x80);
+        } else {
+            buffer[i++] = (byte) sequenceLength;
+        }
+
+        if (test) {
+            if (negativeConfirm) {
+                buffer[i++] = (byte) (causeOfTransmission.getId() | 0xC0);
+            } else {
+                buffer[i++] = (byte) (causeOfTransmission.getId() | 0x80);
+            }
+        } else {
+            if (negativeConfirm) {
+                buffer[i++] = (byte) (causeOfTransmission.getId() | 0x40);
+            } else {
+                buffer[i++] = (byte) causeOfTransmission.getId();
+            }
+        }
+
+        if (settings.getCotFieldLength() == 2) {
+            buffer[i++] = (byte) originatorAddress;
+        }
+
+        buffer[i++] = (byte) commonAddress;
+
+        if (settings.getCommonAddressFieldLength() == 2) {
+            buffer[i++] = (byte) (commonAddress >> 8);
+        }
+
+        if (informationObjects != null) {
+            for (InformationObject informationObject : informationObjects) {
+                i += informationObject.encode(buffer, i, settings.getIoaFieldLength());
+            }
+        } else {
+            System.arraycopy(privateInformation, 0, buffer, i, privateInformation.length);
+            i += privateInformation.length;
+        }
+        return i - origi;
+    }
+
+    @Override
+    public String toString() {
+
+        StringBuilder builder = new StringBuilder().append("ASDU Type: ")
+                .append(aSduType.getId())
+                .append(", ")
+                .append(aSduType)
+                .append(", ")
+                .append(aSduType.getDescription())
+                .append("\nCause of transmission: ")
+                .append(causeOfTransmission)
+                .append(", test: ")
+                .append(isTestFrame())
+                .append(", negative con: ")
+                .append(isNegativeConfirm())
+                .append("\nOriginator address: ")
+                .append(originatorAddress)
+                .append(", Common address: ")
+                .append(commonAddress);
+
+        if (informationObjects != null) {
+            for (InformationObject informationObject : informationObjects) {
+                builder.append("\n").append(informationObject);
+            }
+        } else {
+            builder.append("\nPrivate Information:\n");
+            builder.append(DatatypeConverter.printHexBinary(this.privateInformation));
+        }
+
+        return builder.toString();
+
+    }
+
+}

+ 472 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/ASduType.java

@@ -0,0 +1,472 @@
+package com.gyee.edge.protocol.iec60870;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Every ASDU contains a type identification field that defines the purpose and contents of the ASDU. Every Type
+ * Identifier is of the form A_BB_CC_1 with the following meanings:
+ *
+ * <ul>
+ *
+ * <li>A - can be 'M' for information in monitor direction, 'C' for system information in control direction, 'P' for
+ * parameter in control direction or 'F' for file transfer.</li>
+ *
+ * <li>BB - a two letter abbreviation of the function of the message (e.g. "SC" for Single Command)</li>
+ *
+ * <li>CC - additional information to distinguish different messages with the same function (e.g. "NA" for no timestamp
+ * and "TA" for with timestamp)</li>
+ *
+ * </ul>
+ */
+public enum ASduType {
+
+    /**
+     * 1 - Single-point information without time tag
+     */
+    M_SP_NA_1(1, "Single-point information without time tag"),
+    /**
+     * 2 - Single-point information with time tag
+     */
+    M_SP_TA_1(2, "Single-point information with time tag"),
+    /**
+     * 3 - Double-point information without time tag
+     */
+    M_DP_NA_1(3, "Double-point information without time tag"),
+    /**
+     * 4 - Double-point information with time tag
+     */
+    M_DP_TA_1(4, "Double-point information with time tag"),
+    /**
+     * 5 - Step position information
+     */
+    M_ST_NA_1(5, "Step position information"),
+    /**
+     * 6 - Step position information with time tag
+     */
+    M_ST_TA_1(6, "Step position information with time tag"),
+    /**
+     * 7 - Bitstring of 32 bit
+     */
+    M_BO_NA_1(7, "Bitstring of 32 bit"),
+    /**
+     * 8 - Bitstring of 32 bit with time tag
+     */
+    M_BO_TA_1(8, "Bitstring of 32 bit with time tag"),
+    /**
+     * 9 - Measured value, normalized value
+     */
+    M_ME_NA_1(9, "Measured value, normalized value"),
+    /**
+     * 10 - Measured value, normalized value with time tag
+     */
+    M_ME_TA_1(10, "Measured value, normalized value with time tag"),
+    /**
+     * 11 - Measured value, scaled value
+     */
+    M_ME_NB_1(11, "Measured value, scaled value"),
+    /**
+     * 12 - Measured value, scaled value with time tag
+     */
+    M_ME_TB_1(12, "Measured value, scaled value with time tag"),
+    /**
+     * 13 - Measured value, short floating point number
+     */
+    M_ME_NC_1(13, "Measured value, short floating point number"),
+    /**
+     * 14 - Measured value, short floating point number with time tag
+     */
+    M_ME_TC_1(14, "Measured value, short floating point number with time tag"),
+    /**
+     * 15 - Integrated totals
+     */
+    M_IT_NA_1(15, "Integrated totals"),
+    /**
+     * 16 - Integrated totals with time tag
+     */
+    M_IT_TA_1(16, "Integrated totals with time tag"),
+    /**
+     * 17 - Event of protection equipment with time tag
+     */
+    M_EP_TA_1(17, "Event of protection equipment with time tag"),
+    /**
+     * 18 - Packed start events of protection equipment with time tag
+     */
+    M_EP_TB_1(18, "Packed start events of protection equipment with time tag"),
+    /**
+     * 19 - Packed output circuit information of protection equipment with time tag
+     */
+    M_EP_TC_1(19, "Packed output circuit information of protection equipment with time tag"),
+    /**
+     * 20 - Packed single-point information with status change detection
+     */
+    M_PS_NA_1(20, "Packed single-point information with status change detection"),
+    /**
+     * 21 - Measured value, normalized value without quality descriptor
+     */
+    M_ME_ND_1(21, "Measured value, normalized value without quality descriptor"),
+    /**
+     * 30 - Single-point information with time tag CP56Time2a
+     */
+    M_SP_TB_1(30, "Single-point information with time tag CP56Time2a"),
+    /**
+     * 31 - Double-point information with time tag CP56Time2a
+     */
+    M_DP_TB_1(31, "Double-point information with time tag CP56Time2a"),
+    /**
+     * 32 - Step position information with time tag CP56Time2a
+     */
+    M_ST_TB_1(32, "Step position information with time tag CP56Time2a"),
+    /**
+     * 33 - Bitstring of 32 bits with time tag CP56Time2a
+     */
+    M_BO_TB_1(33, "Bitstring of 32 bits with time tag CP56Time2a"),
+    /**
+     * 34 - Measured value, normalized value with time tag CP56Time2a
+     */
+    M_ME_TD_1(34, "Measured value, normalized value with time tag CP56Time2a"),
+    /**
+     * 35 - Measured value, scaled value with time tag CP56Time2a
+     */
+    M_ME_TE_1(35, "Measured value, scaled value with time tag CP56Time2a"),
+    /**
+     * 36 - Measured value, short floating point number with time tag CP56Time2a
+     */
+    M_ME_TF_1(36, "Measured value, short floating point number with time tag CP56Time2a"),
+    /**
+     * 37 - Integrated totals with time tag CP56Time2a
+     */
+    M_IT_TB_1(37, "Integrated totals with time tag CP56Time2a"),
+    /**
+     * 38 - Event of protection equipment with time tag CP56Time2a
+     */
+    M_EP_TD_1(38, "Event of protection equipment with time tag CP56Time2a"),
+    /**
+     * 39 - Packed start events of protection equipment with time tag CP56Time2a
+     */
+    M_EP_TE_1(39, "Packed start events of protection equipment with time tag CP56Time2a"),
+    /**
+     * 40 - Packed output circuit information of protection equipment with time tag CP56Time2a
+     */
+    M_EP_TF_1(40, "Packed output circuit information of protection equipment with time tag CP56Time2a"),
+    /**
+     * 45 - Single command
+     */
+    C_SC_NA_1(45, "Single command"),
+    /**
+     * 46 - Double command
+     */
+    C_DC_NA_1(46, "Double command"),
+    /**
+     * 47 - Regulating step command
+     */
+    C_RC_NA_1(47, "Regulating step command"),
+    /**
+     * 48 - Set point command, normalized value
+     */
+    C_SE_NA_1(48, "Set point command, normalized value"),
+    /**
+     * 49 - Set point command, scaled value
+     */
+    C_SE_NB_1(49, "Set point command, scaled value"),
+    /**
+     * 50 - Set point command, short floating point number
+     */
+    C_SE_NC_1(50, "Set point command, short floating point number"),
+    /**
+     * 51 - Bitstring of 32 bits
+     */
+    C_BO_NA_1(51, "Bitstring of 32 bits"),
+    /**
+     * 58 - Single command with time tag CP56Time2a
+     */
+    C_SC_TA_1(58, "Single command with time tag CP56Time2a"),
+    /**
+     * 59 - Double command with time tag CP56Time2a
+     */
+    C_DC_TA_1(59, "Double command with time tag CP56Time2a"),
+    /**
+     * 60 - Regulating step command with time tag CP56Time2a
+     */
+    C_RC_TA_1(60, "Regulating step command with time tag CP56Time2a"),
+    /**
+     * 61 - Set-point command with time tag CP56Time2a, normalized value
+     */
+    C_SE_TA_1(61, "Set-point command with time tag CP56Time2a, normalized value"),
+    /**
+     * 62 - Set-point command with time tag CP56Time2a, scaled value
+     */
+    C_SE_TB_1(62, "Set-point command with time tag CP56Time2a, scaled value"),
+    /**
+     * 63 - C_SE_TC_1 Set-point command with time tag CP56Time2a, short floating point number
+     */
+    C_SE_TC_1(63, "C_SE_TC_1 Set-point command with time tag CP56Time2a, short floating point number"),
+    /**
+     * 64 - Bitstring of 32 bit with time tag CP56Time2a
+     */
+    C_BO_TA_1(64, "Bitstring of 32 bit with time tag CP56Time2a"),
+    /**
+     * 70 - End of initialization
+     */
+    M_EI_NA_1(70, "End of initialization"),
+    /**
+     * 100 - Interrogation command
+     */
+    C_IC_NA_1(100, "Interrogation command"),
+    /**
+     * 101 - Counter interrogation command
+     */
+    C_CI_NA_1(101, "Counter interrogation command"),
+    /**
+     * 102 - Read command
+     */
+    C_RD_NA_1(102, "Read command"),
+    /**
+     * 103 - Clock synchronization command
+     */
+    C_CS_NA_1(103, "Clock synchronization command"),
+    /**
+     * 104 - Test command
+     */
+    C_TS_NA_1(104, "Test command"),
+    /**
+     * 105 - Reset process command
+     */
+    C_RP_NA_1(105, "Reset process command"),
+    /**
+     * 106 - Delay acquisition command
+     */
+    C_CD_NA_1(106, "Delay acquisition command"),
+    /**
+     * 107 - Test command with time tag CP56Time2a
+     */
+    C_TS_TA_1(107, "Test command with time tag CP56Time2a"),
+    /**
+     * 110 - Parameter of measured value, normalized value
+     */
+    P_ME_NA_1(110, "Parameter of measured value, normalized value"),
+    /**
+     * 111 - Parameter of measured value, scaled value
+     */
+    P_ME_NB_1(111, "Parameter of measured value, scaled value"),
+    /**
+     * 112 - Parameter of measured value, short floating point number
+     */
+    P_ME_NC_1(112, "Parameter of measured value, short floating point number"),
+    /**
+     * 113 - Parameter activation
+     */
+    P_AC_NA_1(113, "Parameter activation"),
+    /**
+     * 120 - File ready
+     */
+    F_FR_NA_1(120, "File ready"),
+    /**
+     * 121 - Section ready
+     */
+    F_SR_NA_1(121, "Section ready"),
+    /**
+     * 122 - Call directory, select file, call file, call section
+     */
+    F_SC_NA_1(122, "Call directory, select file, call file, call section"),
+    /**
+     * 123 - Last section, last segment
+     */
+    F_LS_NA_1(123, "Last section, last segment"),
+    /**
+     * 124 - Ack file, ack section
+     */
+    F_AF_NA_1(124, "Ack file, ack section"),
+    /**
+     * 125 - Segment
+     */
+    F_SG_NA_1(125, "Segment"),
+    /**
+     * 126 - Directory
+     */
+    F_DR_TA_1(126, "Directory"),
+    /**
+     * 127 - QueryLog, request archive file
+     */
+    F_SC_NB_1(127, "QueryLog, request archive file"),
+
+    PRIVATE_128(128),
+    PRIVATE_129(129),
+    PRIVATE_130(130),
+    PRIVATE_131(131),
+    PRIVATE_132(132),
+    PRIVATE_133(133),
+    PRIVATE_134(134),
+    PRIVATE_135(135),
+    PRIVATE_136(136),
+    PRIVATE_137(137),
+    PRIVATE_138(138),
+    PRIVATE_139(139),
+    PRIVATE_140(140),
+    PRIVATE_141(141),
+    PRIVATE_142(142),
+    PRIVATE_143(143),
+    PRIVATE_144(144),
+    PRIVATE_145(145),
+    PRIVATE_146(146),
+    PRIVATE_147(147),
+    PRIVATE_148(148),
+    PRIVATE_149(149),
+    PRIVATE_150(150),
+    PRIVATE_151(151),
+    PRIVATE_152(152),
+    PRIVATE_153(153),
+    PRIVATE_154(154),
+    PRIVATE_155(155),
+    PRIVATE_156(156),
+    PRIVATE_157(157),
+    PRIVATE_158(158),
+    PRIVATE_159(159),
+    PRIVATE_160(160),
+    PRIVATE_161(161),
+    PRIVATE_162(162),
+    PRIVATE_163(163),
+    PRIVATE_164(164),
+    PRIVATE_165(165),
+    PRIVATE_166(166),
+    PRIVATE_167(167),
+    PRIVATE_168(168),
+    PRIVATE_169(169),
+    PRIVATE_170(170),
+    PRIVATE_171(171),
+    PRIVATE_172(172),
+    PRIVATE_173(173),
+    PRIVATE_174(174),
+    PRIVATE_175(175),
+    PRIVATE_176(176),
+    PRIVATE_177(177),
+    PRIVATE_178(178),
+    PRIVATE_179(179),
+    PRIVATE_180(180),
+    PRIVATE_181(181),
+    PRIVATE_182(182),
+    PRIVATE_183(183),
+    PRIVATE_184(184),
+    PRIVATE_185(185),
+    PRIVATE_186(186),
+    PRIVATE_187(187),
+    PRIVATE_188(188),
+    PRIVATE_189(189),
+    PRIVATE_190(190),
+    PRIVATE_191(191),
+    PRIVATE_192(192),
+    PRIVATE_193(193),
+    PRIVATE_194(194),
+    PRIVATE_195(195),
+    PRIVATE_196(196),
+    PRIVATE_197(197),
+    PRIVATE_198(198),
+    PRIVATE_199(199),
+    PRIVATE_200(200),
+    PRIVATE_201(201),
+    PRIVATE_202(202),
+    PRIVATE_203(203),
+    PRIVATE_204(204),
+    PRIVATE_205(205),
+    PRIVATE_206(206),
+    PRIVATE_207(207),
+    PRIVATE_208(208),
+    PRIVATE_209(209),
+    PRIVATE_210(210),
+    PRIVATE_211(211),
+    PRIVATE_212(212),
+    PRIVATE_213(213),
+    PRIVATE_214(214),
+    PRIVATE_215(215),
+    PRIVATE_216(216),
+    PRIVATE_217(217),
+    PRIVATE_218(218),
+    PRIVATE_219(219),
+    PRIVATE_220(220),
+    PRIVATE_221(221),
+    PRIVATE_222(222),
+    PRIVATE_223(223),
+    PRIVATE_224(224),
+    PRIVATE_225(225),
+    PRIVATE_226(226),
+    PRIVATE_227(227),
+    PRIVATE_228(228),
+    PRIVATE_229(229),
+    PRIVATE_230(230),
+    PRIVATE_231(231),
+    PRIVATE_232(232),
+    PRIVATE_233(233),
+    PRIVATE_234(234),
+    PRIVATE_235(235),
+    PRIVATE_236(236),
+    PRIVATE_237(237),
+    PRIVATE_238(238),
+    PRIVATE_239(239),
+    PRIVATE_240(240),
+    PRIVATE_241(241),
+    PRIVATE_242(242),
+    PRIVATE_243(243),
+    PRIVATE_244(244),
+    PRIVATE_245(245),
+    PRIVATE_246(246),
+    PRIVATE_247(247),
+    PRIVATE_248(248),
+    PRIVATE_249(249),
+    PRIVATE_250(250),
+    PRIVATE_251(251),
+    PRIVATE_252(252),
+    PRIVATE_253(253),
+    PRIVATE_254(254),
+    PRIVATE_255(255);
+
+    private static final Map<Integer, ASduType> idMap = new HashMap<>();
+
+    static {
+        for (ASduType enumInstance : ASduType.values()) {
+            if (idMap.put(enumInstance.getId(), enumInstance) != null) {
+                throw new IllegalArgumentException("duplicate ID: " + enumInstance.getId());
+            }
+        }
+    }
+
+    private final int id;
+    private final String description;
+
+    private ASduType(int id) {
+        this(id, "private range");
+    }
+
+    private ASduType(int id, String description) {
+        this.id = id;
+        this.description = description;
+    }
+
+    /**
+     * Returns the ASduType that corresponds to the given ID. Returns <code>null</code> if no ASduType with the given ID
+     * exists.
+     *
+     * @param id the ID
+     * @return the ASduType that corresponds to the given ID
+     */
+    public static ASduType typeFor(int id) {
+        return idMap.get(id);
+    }
+
+    /**
+     * Returns the description of this ASduType.
+     *
+     * @return the description
+     */
+    public String getDescription() {
+        return description;
+    }
+
+    /**
+     * Returns the ID of this ASduType.
+     *
+     * @return the ID
+     */
+    public int getId() {
+        return id;
+    }
+}

+ 88 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/CauseOfTransmission.java

@@ -0,0 +1,88 @@
+package com.gyee.edge.protocol.iec60870;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Every ASDU contains a single Cause Of Transmission field so the recipient knows why the message it received was sent.
+ * Parts IEC 60870-5-101 and IEC 60870-5-104 define what CauseOfTransmissions are allowed for the different ASDU types.
+ * CauseOfTransmissions 44 to 47 are meant for replies to commands with undefined values.
+ */
+public enum CauseOfTransmission {
+    PERIODIC(1),
+    BACKGROUND_SCAN(2),
+    SPONTANEOUS(3),
+    INITIALIZED(4),
+    REQUEST(5),
+    ACTIVATION(6),
+    ACTIVATION_CON(7),
+    DEACTIVATION(8),
+    DEACTIVATION_CON(9),
+    ACTIVATION_TERMINATION(10),
+    RETURN_INFO_REMOTE(11),
+    RETURN_INFO_LOCAL(12),
+    FILE_TRANSFER(13),
+    INTERROGATED_BY_STATION(20),
+    INTERROGATED_BY_GROUP_1(21),
+    INTERROGATED_BY_GROUP_2(22),
+    INTERROGATED_BY_GROUP_3(23),
+    INTERROGATED_BY_GROUP_4(24),
+    INTERROGATED_BY_GROUP_5(25),
+    INTERROGATED_BY_GROUP_6(26),
+    INTERROGATED_BY_GROUP_7(27),
+    INTERROGATED_BY_GROUP_8(28),
+    INTERROGATED_BY_GROUP_9(29),
+    INTERROGATED_BY_GROUP_10(30),
+    INTERROGATED_BY_GROUP_11(31),
+    INTERROGATED_BY_GROUP_12(32),
+    INTERROGATED_BY_GROUP_13(33),
+    INTERROGATED_BY_GROUP_14(34),
+    INTERROGATED_BY_GROUP_15(35),
+    INTERROGATED_BY_GROUP_16(36),
+    REQUESTED_BY_GENERAL_COUNTER(37),
+    REQUESTED_BY_GROUP_1_COUNTER(38),
+    REQUESTED_BY_GROUP_2_COUNTER(39),
+    REQUESTED_BY_GROUP_3_COUNTER(40),
+    REQUESTED_BY_GROUP_4_COUNTER(41),
+    UNKNOWN_TYPE_ID(44),
+    UNKNOWN_CAUSE_OF_TRANSMISSION(45),
+    UNKNOWN_COMMON_ADDRESS_OF_ASDU(46),
+    UNKNOWN_INFORMATION_OBJECT_ADDRESS(47);
+
+    private static final Map<Integer, CauseOfTransmission> idMap = new HashMap<>();
+
+    static {
+        for (CauseOfTransmission enumInstance : CauseOfTransmission.values()) {
+            if (idMap.put(enumInstance.getId(), enumInstance) != null) {
+                throw new IllegalArgumentException("duplicate ID: " + enumInstance.getId());
+            }
+        }
+    }
+
+    private final int id;
+
+    private CauseOfTransmission(int id) {
+        this.id = id;
+    }
+
+    /**
+     * Returns the CauseOfTransmission that corresponds to the given ID. Returns <code>null</code> if no
+     * CauseOfTransmission with the given ID exists.
+     *
+     * @param id the ID.
+     * @return the CauseOfTransmission that corresponds to the given ID.
+     */
+    public static CauseOfTransmission causeFor(int id) {
+        return idMap.get(id);
+    }
+
+    /**
+     * Returns the ID of this CauseOfTransmission.
+     *
+     * @return the ID.
+     */
+    public int getId() {
+        return id;
+    }
+
+}

+ 134 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/ClientConnectionBuilder.java

@@ -0,0 +1,134 @@
+package com.gyee.edge.protocol.iec60870;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+
+import javax.net.SocketFactory;
+import javax.net.ssl.SSLSocketFactory;
+
+/**
+ * The client connection builder is used to connect to IEC 60870-5-104 servers. A client application that wants to
+ * connect to a server should first create an instance of {@link ClientConnectionBuilder}. Next all the necessary
+ * configuration parameters can be set. Finally the {@link ClientConnectionBuilder#build()} function is called to
+ * connect to the server. An instance of {@link ClientConnectionBuilder} can be used to create an unlimited number of
+ * connections. Changing the parameters of a {@link ClientConnectionBuilder} has no affect on connections that have
+ * already been created.
+ *
+ * <p>
+ * Note that the configured lengths of the fields COT, CA and IOA have to be the same for all communicating nodes in a
+ * network. The default values used by {@link ClientConnectionBuilder} are those most commonly used in IEC 60870-5-104
+ * communication.
+ * </p>
+ */
+public class ClientConnectionBuilder extends CommonBuilder<ClientConnectionBuilder, Connection> {
+
+    private static final int DEFAULT_PORT = 2404;
+
+    private SocketFactory socketFactory;
+    private InetAddress address;
+    private int port;
+    private InetAddress localAddr;
+    private int localPort;
+
+    /**
+     * Creates a client connection builder that can be used to connect to the given address.
+     *
+     * @param address the address to connect to
+     */
+    public ClientConnectionBuilder(InetAddress address) {
+        this.address = address;
+        this.port = DEFAULT_PORT;
+
+        this.localAddr = null;
+
+        this.socketFactory = SocketFactory.getDefault();
+    }
+
+    public ClientConnectionBuilder(String inetAddress) throws UnknownHostException {
+        this(InetAddress.getByName(inetAddress));
+    }
+
+    /**
+     * Set the socket factory to used to create the socket for the connection. The default is
+     * {@link SocketFactory#getDefault()}. You could pass an {@link SSLSocketFactory} to enable SSL.
+     *
+     * @param socketFactory the socket factory
+     * @return this builder
+     */
+    public ClientConnectionBuilder setSocketFactory(SocketFactory socketFactory) {
+        this.socketFactory = socketFactory;
+        return this;
+    }
+
+    /**
+     * Sets the port to connect to. The default port is 2404.
+     *
+     * @param port the port to connect to.
+     * @return this builder
+     */
+    public ClientConnectionBuilder setPort(int port) {
+        this.port = port;
+        return this;
+    }
+
+    /**
+     * Sets the address to connect to.
+     *
+     * @param address the address to connect to.
+     * @return this builder
+     */
+    public ClientConnectionBuilder setAddress(InetAddress address) {
+        this.address = address;
+        return this;
+    }
+
+    /**
+     * Sets the local (client) address and port the socket will connect to.
+     *
+     * @param address the local address the socket is bound to, or null for any local address.
+     * @param port    the local port the socket is bound to or zero for a system selected free port.
+     * @return this builder
+     */
+    public ClientConnectionBuilder setLocalAddress(InetAddress address, int port) {
+        this.localAddr = address;
+        this.localPort = port;
+        return this;
+    }
+
+    /**
+     * Sets connection time out t0, in milliseconds.
+     *
+     * @param time the timeout in milliseconds. Default is 20 s, if set to 0
+     * @return this builder
+     */
+    public ClientConnectionBuilder setConnectionTimeout(int time) {
+        if (time < 100) {
+            throw new IllegalArgumentException("invalid timeout: " + time + ", time must be bigger then 100ms");
+        }
+        settings.setConnectionTimeout(time);
+        return this;
+    }
+
+    /**
+     * Connects to the server. The TCP/IP connection is build up and a {@link Connection} object is returned that can be
+     * used to communicate with the server.
+     *
+     * @return the {@link Connection} object that can be used to communicate with the server.
+     * @throws IOException if any kind of error occurs during connection build up.
+     */
+    @Override
+    public Connection build() throws IOException {
+        Socket socket = socketFactory.createSocket();
+        socket.setSoTimeout(settings.getMessageFragmentTimeout());
+
+        if (localAddr != null) {
+            socket.bind(new InetSocketAddress(localAddr, localPort));
+        }
+        socket.connect(new InetSocketAddress(address, port), settings.getConnectionTimeout());
+        return new Connection(socket, null, new ConnectionSettings(settings));
+    }
+
+}

+ 169 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/CommonBuilder.java

@@ -0,0 +1,169 @@
+package com.gyee.edge.protocol.iec60870;
+
+import java.io.IOException;
+
+abstract class CommonBuilder<T extends CommonBuilder<T, C>, C> {
+
+    final ConnectionSettings settings = new ConnectionSettings();
+
+    /**
+     * Access the casted this reference.
+     *
+     * @return the reference of the object.
+     */
+    @SuppressWarnings("unchecked")
+    private T self() {
+        return (T) this;
+    }
+
+    /**
+     * Sets the length of the Cause Of Transmission (COT) field of the ASDU. Allowed values are 1 or 2. The default is
+     * 2.
+     *
+     * @param length the length of the Cause Of Transmission field
+     * @return this builder
+     */
+    public T setCotFieldLength(int length) {
+        if (length != 1 && length != 2) {
+            throw new IllegalArgumentException("invalid length");
+        }
+        settings.setCotFieldLength(length);
+        return self();
+    }
+
+    /**
+     * Sets the length of the Common Address (CA) field of the ASDU. Allowed values are 1 or 2. The default is 2.
+     *
+     * @param length the length of the Common Address (CA) field
+     * @return this builder
+     */
+    public T setCommonAddressFieldLength(int length) {
+        if (length != 1 && length != 2) {
+            throw new IllegalArgumentException("invalid length");
+        }
+        settings.setCommonAddressFieldLength(length);
+        return self();
+    }
+
+    /**
+     * Sets the length of the Information Object Address (IOA) field of the ASDU. Allowed values are 1, 2 or 3. The
+     * default is 3.
+     *
+     * @param length the length of the Information Object Address field
+     * @return this builder
+     */
+    public T setIoaFieldLength(int length) {
+        if (length < 1 || length > 3) {
+            throw new IllegalArgumentException("invalid length: " + length);
+        }
+        settings.setIoaFieldLength(length);
+        return self();
+    }
+
+    /**
+     * Sets the maximum time in ms that no acknowledgement has been received (for I-Frames or Test-Frames) before
+     * actively closing the connection. This timeout is called t1 by the standard. Default is 15s, minimum is 1s,
+     * maximum is 255s.
+     *
+     * @param time the maximum time in ms that no acknowledgement has been received before actively closing the
+     *             connection.
+     * @return this builder
+     */
+    public T setMaxTimeNoAckReceived(int time) {
+        checkTimeRange(time);
+        settings.setMaxTimeNoAckReceived(time);
+        return self();
+    }
+
+    /**
+     * Sets the maximum time in ms before confirming received messages that have not yet been acknowledged using an S
+     * format APDU. This timeout is called t2 by the standard. Default is 10s, minimum is 1s, maximum is 255s.
+     *
+     * @param time the maximum time in ms before confirming received messages that have not yet been acknowledged using
+     *             an S format APDU.
+     * @return this builder
+     */
+    public T setMaxTimeNoAckSent(int time) {
+        checkTimeRange(time);
+        settings.setMaxTimeNoAckSent(time);
+        return self();
+    }
+
+    private void checkTimeRange(int time) {
+        if (time < 1000 || time > 255000) {
+            throw new IllegalArgumentException(
+                    "invalid timeout: " + time + ", time must be between 1000ms and 255000ms");
+        }
+    }
+
+    /**
+     * Sets the maximum time in ms that the connection may be idle before sending a test frame. This timeout is called
+     * t3 by the standard. Default is 20s, minimum is 1s, maximum is 172800s (48h).
+     *
+     * @param time the maximum time in ms that the connection may be idle before sending a test frame.
+     * @return this builder
+     */
+    public T setMaxIdleTime(int time) {
+        if (time < 1000 || time > 172800000) {
+            throw new IllegalArgumentException(
+                    "invalid timeout: " + time + ", time must be between 1000ms and 172800000ms");
+        }
+        settings.setMaxIdleTime(time);
+        return self();
+    }
+
+    /**
+     * Sets the number of maximum difference send sequence number to send acknowledge variable before the connection
+     * will automatically closed. This parameter is called k by the standard. Default is 12, minimum is 1, maximum is
+     * 32767.
+     *
+     * @param maxNum the maximum number of sequentially numbered I format APDUs that the DTE may have outstanding
+     * @return this builder
+     */
+    public T setMaxNumOfOutstandingIPdus(int maxNum) {
+        if (maxNum < 1 || maxNum > 32767) {
+            throw new IllegalArgumentException("invalid maxNum: " + maxNum + ", must be a value between 1 and 32767");
+        }
+        settings.setMaxNumOfOutstandingIPdus(maxNum);
+        return self();
+    }
+
+    /**
+     * Sets the number of unacknowledged I format APDUs received before the connection will automatically send an S
+     * format APDU to confirm them. This parameter is called w by the standard. Default is 8, minimum is 1, maximum is
+     * 32767.
+     *
+     * @param maxNum the number of unacknowledged I format APDUs received before the connection will automatically send an
+     *               S format APDU to confirm them.
+     * @return this builder
+     */
+    public T setMaxUnconfirmedIPdusReceived(int maxNum) {
+        if (maxNum < 1 || maxNum > 32767) {
+            throw new IllegalArgumentException("invalid maxNum: " + maxNum + ", must be a value between 1 and 32767");
+        }
+        settings.setMaxUnconfirmedIPdusReceived(maxNum);
+        return self();
+    }
+
+    /**
+     * Sets SO_TIMEOUT with the specified timeout, in milliseconds.
+     *
+     * @param time the timeout in milliseconds. Default is 5 s, minimum 100 ms.
+     * @return this builder
+     */
+    public T setMessageFragmentTimeout(int time) {
+        if (time < 100) {
+            throw new IllegalArgumentException("invalid timeout: " + time + ", time must be bigger then 100ms");
+        }
+        settings.setMessageFragmentTimeout(time);
+        return self();
+    }
+
+    public T useSharedThreadPool() {
+        settings.setUseSharedThreadPool(true);
+        return self();
+    }
+
+    public abstract C build() throws IOException;
+
+}

文件差異過大導致無法顯示
+ 1255 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/Connection.java


+ 31 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/ConnectionEventListener.java

@@ -0,0 +1,31 @@
+package com.gyee.edge.protocol.iec60870;
+
+import java.io.IOException;
+import java.util.EventListener;
+
+/**
+ * The listener interface for receiving incoming ASDUs and connection closed events. The class that is interested in
+ * incoming ASDUs implements this interface. The object of that class is registered as a listener through the
+ * {@link Connection#startDataTransfer(ConnectionEventListener, int)} or
+ * {@link Connection#waitForStartDT(ConnectionEventListener, int)} method. Incoming ASDUs are queued so that
+ * {@link #newASdu(ASdu)} is never called simultaneously for the same connection.
+ */
+public interface ConnectionEventListener extends EventListener {
+
+    /**
+     * Invoked when a new ASDU arrives.
+     *
+     * @param aSdu the ASDU that arrived.
+     */
+    void newASdu(ASdu aSdu);
+
+    /**
+     * Invoked when an IOException occurred while listening for incoming ASDUs. An IOException implies that the
+     * {@link Connection} that feeds this listener was automatically closed and can no longer be used to send commands
+     * or receive ASDUs.
+     *
+     * @param cause the exception that occurred.
+     */
+    void connectionClosed(IOException cause);
+
+}

+ 170 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/ConnectionSettings.java

@@ -0,0 +1,170 @@
+package com.gyee.edge.protocol.iec60870;
+
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadPoolExecutor;
+
+class ConnectionSettings {
+    private static final ThreadPoolExecutor threadPool;
+    private static volatile int numOpenConnections;
+
+    static {
+        threadPool = (ThreadPoolExecutor) Executors.newCachedThreadPool();
+        numOpenConnections = 0;
+    }
+
+    private int messageFragmentTimeout;
+
+    private int cotFieldLength;
+    private int commonAddressFieldLength;
+    private int ioaFieldLength;
+
+    private int maxTimeNoAckReceived;
+    private int maxTimeNoAckSent;
+    private int maxIdleTime;
+    private int connectionTimeout;
+
+    private int maxUnconfirmedIPdusReceived;
+    private int maxNumOfOutstandingIPdus;
+
+    private boolean useSharedThreadPool;
+
+    public ConnectionSettings() {
+        this.messageFragmentTimeout = 5_000;
+
+        this.cotFieldLength = 2;
+        this.commonAddressFieldLength = 2;
+        this.ioaFieldLength = 3;
+
+        this.connectionTimeout = 30_000;
+        this.maxTimeNoAckReceived = 15_000;
+        this.maxTimeNoAckSent = 10_000;
+        this.maxIdleTime = 20_000;
+        this.maxUnconfirmedIPdusReceived = 8;
+        this.maxNumOfOutstandingIPdus = 12;
+
+        this.useSharedThreadPool = false;
+    }
+
+    public ConnectionSettings(ConnectionSettings connectionSettings) {
+
+        messageFragmentTimeout = connectionSettings.messageFragmentTimeout;
+
+        cotFieldLength = connectionSettings.cotFieldLength;
+        commonAddressFieldLength = connectionSettings.commonAddressFieldLength;
+        ioaFieldLength = connectionSettings.ioaFieldLength;
+
+        maxTimeNoAckReceived = connectionSettings.maxTimeNoAckReceived;
+        maxTimeNoAckSent = connectionSettings.maxTimeNoAckSent;
+        maxIdleTime = connectionSettings.maxIdleTime;
+        connectionTimeout = connectionSettings.connectionTimeout;
+
+        maxUnconfirmedIPdusReceived = connectionSettings.maxUnconfirmedIPdusReceived;
+        maxNumOfOutstandingIPdus = connectionSettings.maxNumOfOutstandingIPdus;
+
+        this.useSharedThreadPool = connectionSettings.useSharedThreadPool;
+    }
+
+    public static ThreadPoolExecutor getThreadPool() {
+        return threadPool;
+    }
+
+    public static synchronized void incremntConnectionsCounter() {
+        numOpenConnections++;
+    }
+
+    public static synchronized void decrementConnectionsCounter() {
+        if (--numOpenConnections == 0) {
+            threadPool.shutdown();
+        }
+    }
+
+    public boolean useSharedThreadPool() {
+        return useSharedThreadPool;
+    }
+
+    public int getMessageFragmentTimeout() {
+        return messageFragmentTimeout;
+    }
+
+    public void setMessageFragmentTimeout(int messageFragmentTimeout) {
+        this.messageFragmentTimeout = messageFragmentTimeout;
+    }
+
+    public int getCotFieldLength() {
+        return cotFieldLength;
+    }
+
+    public void setCotFieldLength(int cotFieldLength) {
+        this.cotFieldLength = cotFieldLength;
+    }
+
+    public int getCommonAddressFieldLength() {
+        return commonAddressFieldLength;
+    }
+
+    public void setCommonAddressFieldLength(int commonAddressFieldLength) {
+        this.commonAddressFieldLength = commonAddressFieldLength;
+    }
+
+    public int getIoaFieldLength() {
+        return ioaFieldLength;
+    }
+
+    public void setIoaFieldLength(int ioaFieldLength) {
+        this.ioaFieldLength = ioaFieldLength;
+    }
+
+    public int getMaxTimeNoAckReceived() {
+        return maxTimeNoAckReceived;
+    }
+
+    public void setMaxTimeNoAckReceived(int maxTimeNoAckReceived) {
+        this.maxTimeNoAckReceived = maxTimeNoAckReceived;
+    }
+
+    public int getMaxTimeNoAckSent() {
+        return maxTimeNoAckSent;
+    }
+
+    public void setMaxTimeNoAckSent(int maxTimeNoAckSent) {
+        this.maxTimeNoAckSent = maxTimeNoAckSent;
+    }
+
+    public int getMaxIdleTime() {
+        return maxIdleTime;
+    }
+
+    public void setMaxIdleTime(int maxIdleTime) {
+        this.maxIdleTime = maxIdleTime;
+    }
+
+    public int getMaxUnconfirmedIPdusReceived() {
+        return maxUnconfirmedIPdusReceived;
+    }
+
+    public void setMaxUnconfirmedIPdusReceived(int maxUnconfirmedIPdusReceived) {
+        this.maxUnconfirmedIPdusReceived = maxUnconfirmedIPdusReceived;
+    }
+
+    public int getMaxNumOfOutstandingIPdus() {
+        return this.maxNumOfOutstandingIPdus;
+    }
+
+    public void setMaxNumOfOutstandingIPdus(int maxNumOfOutstandingIPdus) {
+        this.maxNumOfOutstandingIPdus = maxNumOfOutstandingIPdus;
+    }
+
+    public int getConnectionTimeout() {
+        return this.connectionTimeout;
+    }
+
+    public void setConnectionTimeout(int time) {
+        this.connectionTimeout = time;
+
+    }
+
+    public void setUseSharedThreadPool(boolean useSharedThreadPool) {
+        this.useSharedThreadPool = useSharedThreadPool;
+    }
+
+}

+ 42 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/ExtendedDataInputStream.java

@@ -0,0 +1,42 @@
+package com.gyee.edge.protocol.iec60870;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * 从字节流中读取基本数据类型的方法,根据iec60870-5-4中的定义扩展了部分方法
+ */
+public class ExtendedDataInputStream extends DataInputStream {
+
+    private static final int INTEGER_BYTES = 4;
+    private static final int SHORT_BYTES = 2;
+
+    public ExtendedDataInputStream(InputStream in) {
+        super(in);
+    }
+
+    public int readLittleEndianInt() throws IOException {
+        return (int) readNLittleEndianBytes(INTEGER_BYTES);
+    }
+
+    public long readLittleEndianUnsignedInt() throws IOException {
+        return readLittleEndianInt() & 0xffffffffl;
+    }
+
+    public short readLittleEndianShort() throws IOException {
+        return (short) readNLittleEndianBytes(SHORT_BYTES);
+    }
+
+    public int readLittleEndianUnsignedShort() throws IOException {
+        return readLittleEndianShort() & 0xffff;
+    }
+
+    private long readNLittleEndianBytes(int n) throws IOException {
+        long res = 0;
+        for (int i = 0; i < n; ++i) {
+            res |= readUnsignedByte() << 8 * i;
+        }
+        return res;
+    }
+}

+ 161 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/Server.java

@@ -0,0 +1,161 @@
+package com.gyee.edge.protocol.iec60870;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import javax.net.ServerSocketFactory;
+
+/**
+ * The server is used to start listening for IEC 60870-5-104 client connections.
+ */
+public class Server {
+
+    private final int port;
+    private final InetAddress bindAddr;
+    private final int backlog;
+    private final ServerSocketFactory serverSocketFactory;
+    private final int maxConnections;
+    private final ConnectionSettings settings;
+    private ServerThread serverThread;
+    private ExecutorService exec;
+
+    private Server(Builder builder) {
+        port = builder.port;
+        bindAddr = builder.bindAddr;
+        backlog = builder.backlog;
+        serverSocketFactory = builder.serverSocketFactory;
+        maxConnections = builder.maxConnections;
+        settings = new ConnectionSettings(builder.settings);
+    }
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /**
+     * Starts a new thread that listens on the configured port. This method is non-blocking.
+     *
+     * @param listener the ServerConnectionListener that will be notified when remote clients are connecting or the server
+     *                 stopped listening.
+     * @throws IOException if any kind of error occures while creating the server socket.
+     */
+    public void start(ServerEventListener listener) throws IOException {
+        ConnectionSettings.incremntConnectionsCounter();
+        if (this.settings.useSharedThreadPool()) {
+            this.exec = ConnectionSettings.getThreadPool();
+        } else {
+            this.exec = Executors.newCachedThreadPool();
+        }
+        serverThread = new ServerThread(serverSocketFactory.createServerSocket(port, backlog, bindAddr), settings,
+                maxConnections, listener, exec);
+        this.exec.execute(this.serverThread);
+    }
+
+    /**
+     * Stop listening for new connections. Existing connections are not touched.
+     */
+    public void stop() {
+        if (serverThread == null) {
+            return;
+        }
+
+        serverThread.stopServer();
+
+        if (this.settings.useSharedThreadPool()) {
+            ConnectionSettings.decrementConnectionsCounter();
+        } else {
+            this.exec.shutdown();
+        }
+
+        serverThread = null;
+    }
+
+    /**
+     * The server builder which builds a 60870 server instance.
+     *
+     * @see Server#builder()
+     */
+    public static class Builder extends CommonBuilder<Builder, Server> {
+
+        private int port = 2404;
+        private InetAddress bindAddr = null;
+        private int backlog = 0;
+        private ServerSocketFactory serverSocketFactory = ServerSocketFactory.getDefault();
+
+        private int maxConnections = 100;
+
+        private Builder() {
+        }
+
+        /**
+         * Sets the TCP port that the server will listen on. IEC 60870-5-104 usually uses port 2404.
+         *
+         * @param port the port
+         * @return this builder
+         */
+        public Builder setPort(int port) {
+            this.port = port;
+            return this;
+        }
+
+        /**
+         * Sets the backlog that is passed to the java.net.ServerSocket.
+         *
+         * @param backlog the backlog
+         * @return this builder
+         */
+        public Builder setBacklog(int backlog) {
+            this.backlog = backlog;
+            return this;
+        }
+
+        /**
+         * Sets the IP address to bind to. It is passed to java.net.ServerSocket
+         *
+         * @param bindAddr the IP address to bind to
+         * @return this builder
+         */
+        public Builder setBindAddr(InetAddress bindAddr) {
+            this.bindAddr = bindAddr;
+            return this;
+        }
+
+        /**
+         * Sets the ServerSocketFactory to be used to create the ServerSocket. Default is
+         * ServerSocketFactory.getDefault().
+         *
+         * @param socketFactory the ServerSocketFactory to be used to create the ServerSocket
+         * @return this builder
+         */
+        public Builder setSocketFactory(ServerSocketFactory socketFactory) {
+            this.serverSocketFactory = socketFactory;
+            return this;
+        }
+
+        /**
+         * Set the maximum number of client connections that are allowed in parallel.
+         *
+         * @param maxConnections the number of connections allowed (default is 100) @ return this builder
+         * @return this builder
+         */
+        public Builder setMaxConnections(int maxConnections) {
+            if (maxConnections <= 0) {
+                throw new IllegalArgumentException("maxConnections is out of bound");
+            }
+            this.maxConnections = maxConnections;
+            return this;
+        }
+
+        /**
+         * To start/activate the server call {@link Server#start(ServerEventListener)} on the returned server.
+         */
+        @Override
+        public Server build() {
+            return new Server(this);
+        }
+
+    }
+
+}

+ 20 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/ServerEventListener.java

@@ -0,0 +1,20 @@
+package com.gyee.edge.protocol.iec60870;
+
+import java.io.IOException;
+import java.util.EventListener;
+
+public interface ServerEventListener extends EventListener {
+
+    void connectionIndication(Connection connection);
+
+    /**
+     * This function is only called when an IOException in ServerSocket.accept() occurred which was not forced using
+     * ServerSap.stopListening()
+     *
+     * @param e The IOException caught form ServerSocket.accept()
+     */
+    void serverStoppedListeningIndication(IOException e);
+
+    void connectionAttemptFailed(IOException e);
+
+}

+ 111 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/ServerThread.java

@@ -0,0 +1,111 @@
+package com.gyee.edge.protocol.iec60870;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.concurrent.ExecutorService;
+
+class ServerThread implements Runnable {
+
+    private final ServerSocket serverSocket;
+    private final ConnectionSettings settings;
+    private final int maxConnections;
+    private final ServerEventListener serverSapListener;
+    private final ExecutorService executor;
+    private volatile boolean stopServer = false;
+    private int numConnections = 0;
+
+    ServerThread(ServerSocket serverSocket, ConnectionSettings settings, int maxConnections,
+                 ServerEventListener serverSapListener, ExecutorService exec) {
+        this.serverSocket = serverSocket;
+        this.settings = settings;
+        this.maxConnections = maxConnections;
+        this.serverSapListener = serverSapListener;
+
+        this.executor = exec;
+    }
+
+    @Override
+    public void run() {
+        Thread.currentThread().setName("ServerThread");
+        Socket clientSocket = null;
+
+        while (!stopServer) {
+            try {
+                clientSocket = serverSocket.accept();
+            } catch (IOException e) {
+                if (!stopServer) {
+                    serverSapListener.serverStoppedListeningIndication(e);
+                }
+                return;
+            }
+
+            boolean startConnection = false;
+
+            synchronized (this) {
+                if (numConnections < maxConnections) {
+                    numConnections++;
+                    startConnection = true;
+                }
+            }
+
+            if (startConnection) {
+                ConnectionHandler connectionHandler = new ConnectionHandler(clientSocket, this);
+                executor.execute(connectionHandler);
+            } else {
+                serverSapListener.connectionAttemptFailed(new IOException(
+                        "Maximum number of connections reached. Ignoring connection request. Maximum number of connections: "
+                                + maxConnections));
+            }
+
+        }
+    }
+
+    void connectionClosedSignal() {
+        synchronized (this) {
+            numConnections--;
+        }
+    }
+
+    /**
+     * Stops listening for new connections. Existing connections are not touched.
+     */
+    void stopServer() {
+        stopServer = true;
+        if (serverSocket.isBound()) {
+            try {
+                serverSocket.close();
+            } catch (IOException e) {
+                // ignore any errors.
+            }
+        }
+    }
+
+    private class ConnectionHandler implements Runnable {
+
+        private final Socket socket;
+        private final ServerThread serverThread;
+
+        public ConnectionHandler(Socket socket, ServerThread serverThread) {
+            this.socket = socket;
+            this.serverThread = serverThread;
+        }
+
+        @Override
+        public void run() {
+            Thread.currentThread().setName("ConnectionHandler");
+            Connection serverConnection;
+            try {
+                serverConnection = new Connection(socket, serverThread, settings);
+            } catch (IOException e) {
+                synchronized (ServerThread.this) {
+                    numConnections--;
+                }
+                serverSapListener.connectionAttemptFailed(e);
+                return;
+            }
+            serverSapListener.connectionIndication(serverConnection);
+        }
+    }
+
+}

+ 63 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/TimeoutManager.java

@@ -0,0 +1,63 @@
+package com.gyee.edge.protocol.iec60870;
+
+import java.util.concurrent.PriorityBlockingQueue;
+
+class TimeoutManager implements Runnable {
+
+    private final PriorityBlockingQueue<TimeoutTask> queue;
+
+    private final Object guadedLock;
+
+    boolean canceled;
+
+    public TimeoutManager() {
+        this.queue = new PriorityBlockingQueue<>(4);
+        this.guadedLock = new Object();
+    }
+
+    public void addTimerTask(TimeoutTask task) {
+        task.updateDueTime();
+        removeDuplicates(task);
+        this.queue.add(task);
+        synchronized (this.guadedLock) {
+            this.guadedLock.notifyAll();
+        }
+    }
+
+    private void removeDuplicates(TimeoutTask task) {
+        while (queue.remove(task)) {
+            ;
+        }
+    }
+
+    public void cancel() {
+        this.canceled = true;
+    }
+
+    @Override
+    public void run() {
+        Thread.currentThread().setName("TimeoutManager");
+        TimeoutTask currTask;
+        while (!canceled) {
+            try {
+                long sleepMillis;
+                currTask = queue.take();
+
+                while ((sleepMillis = currTask.sleepTimeFromDueTime()) > 0) {
+                    queue.put(currTask);
+
+                    synchronized (this.guadedLock) {
+                        this.guadedLock.wait(sleepMillis);
+                    }
+                    currTask = queue.take();
+                }
+
+                currTask.manExec();
+            } catch (InterruptedException e) {
+                // Restore interrupted state...
+                Thread.currentThread().interrupt();
+                return;
+            }
+        }
+    }
+}

+ 85 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/TimeoutTask.java

@@ -0,0 +1,85 @@
+package com.gyee.edge.protocol.iec60870;
+
+abstract class TimeoutTask implements Comparable<TimeoutTask> {
+    private final int timeout;
+
+    private long dueTime;
+
+    private boolean canceled;
+    private boolean done;
+
+    public TimeoutTask(int timeout) {
+
+        this.timeout = timeout;
+        this.done = false;
+        this.canceled = false;
+        this.dueTime = 0;
+    }
+
+    void manExec() {
+
+        if (canceled) {
+            return;
+        }
+
+        try {
+            execute();
+        } finally {
+            this.done = true;
+        }
+    }
+
+    void updateDueTime() {
+
+        this.dueTime = System.currentTimeMillis() + timeout;
+        this.canceled = false;
+        this.done = false;
+    }
+
+    protected abstract void execute();
+
+    public boolean isPlanned() {
+
+        return !this.canceled && !this.done && dueTime != 0;
+    }
+
+    public boolean isDone() {
+
+        return done;
+    }
+
+    public void cancel() {
+
+        this.canceled = true;
+    }
+
+    public long sleepTimeFromDueTime() {
+
+        return dueTime - System.currentTimeMillis();
+    }
+
+    @Override
+    public int compareTo(TimeoutTask o) {
+
+        return Long.compare(this.dueTime, o.dueTime);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+
+        if (!(obj instanceof TimeoutTask)) {
+            return false;
+        }
+
+        TimeoutTask o = (TimeoutTask) obj;
+        return this.dueTime == o.dueTime && this.canceled == o.canceled && this.done == o.done
+                && this.timeout == o.timeout;
+    }
+
+    @Override
+    public int hashCode() {
+
+        return this.timeout ^ ((Boolean.valueOf(canceled).hashCode()) << 2) ^ Boolean.valueOf(done).hashCode();
+    }
+
+}

+ 36 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/Util.java

@@ -0,0 +1,36 @@
+package com.gyee.edge.protocol.iec60870;
+
+/**
+ * Class offering static utility functions.
+ */
+public class Util {
+
+    private Util() {
+    }
+
+    /**
+     * Returns the Information Object Address (IOA) calculated from the given bytes. The first byte is the least
+     * significant byte of the IOA.
+     *
+     * @param byte1 the first byte
+     * @param byte2 the second byte
+     * @param byte3 the third byte
+     * @return the IOA
+     */
+    public static int convertToInformationObjectAddress(int byte1, int byte2, int byte3) {
+        return byte1 + (byte2 << 8) + (byte3 << 16);
+    }
+
+    /**
+     * Returns the Common Address (CA) calculated from the given bytes. The first byte is the least significant byte of
+     * the CA.
+     *
+     * @param byte1 the first byte
+     * @param byte2 the second byte
+     * @return the CA
+     */
+    public static int convertToCommonAddress(int byte1, int byte2) {
+        return byte1 + (byte2 << 8);
+    }
+
+}

+ 53 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeAbstractQualifierOfCommand.java

@@ -0,0 +1,53 @@
+
+package com.gyee.edge.protocol.iec60870.infoelement;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+
+abstract class IeAbstractQualifierOfCommand extends InformationElement {
+
+    protected int value;
+
+    IeAbstractQualifierOfCommand(int qualifier, boolean select) {
+
+        if (qualifier < 0 || qualifier > 31) {
+            throw new IllegalArgumentException("Qualifier is out of bound: " + qualifier);
+        }
+
+        value = qualifier << 2;
+
+        if (select) {
+            value |= 0x80;
+        }
+
+    }
+
+    IeAbstractQualifierOfCommand(DataInputStream is) throws IOException {
+        value = (is.readByte() & 0xff);
+    }
+
+    @Override
+    int encode(byte[] buffer, int i) {
+        buffer[i] = (byte) value;
+        return 1;
+    }
+
+    /**
+     * Returns true if the command selects and false if the command executes.
+     *
+     * @return true if the command selects and false if the command executes.
+     */
+    public boolean isSelect() {
+        return (value & 0x80) == 0x80;
+    }
+
+    public int getQualifier() {
+        return (value >> 2) & 0x1f;
+    }
+
+    @Override
+    public String toString() {
+        return "selected: " + isSelect() + ", qualifier: " + getQualifier();
+    }
+
+}

+ 66 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeAbstractQuality.java

@@ -0,0 +1,66 @@
+
+package com.gyee.edge.protocol.iec60870.infoelement;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.text.MessageFormat;
+
+abstract class IeAbstractQuality extends InformationElement {
+
+    protected int value;
+
+    public IeAbstractQuality(boolean blocked, boolean substituted, boolean notTopical, boolean invalid) {
+
+        value = 0;
+
+        if (blocked) {
+            value |= 0x10;
+        }
+        if (substituted) {
+            value |= 0x20;
+        }
+        if (notTopical) {
+            value |= 0x40;
+        }
+        if (invalid) {
+            value |= 0x80;
+        }
+
+    }
+
+    IeAbstractQuality(DataInputStream is) throws IOException {
+        value = is.readUnsignedByte();
+    }
+
+    @Override
+    int encode(byte[] buffer, int i) {
+        buffer[i] = (byte) value;
+        return 1;
+    }
+
+    public boolean isBlocked() {
+        return hasBitSet(0x10);
+    }
+
+    public boolean isSubstituted() {
+        return hasBitSet(0x20);
+    }
+
+    public boolean isNotTopical() {
+        return hasBitSet(0x40);
+    }
+
+    public boolean isInvalid() {
+        return hasBitSet(0x80);
+    }
+
+    private boolean hasBitSet(int mask) {
+        return (value & mask) == mask;
+    }
+
+    @Override
+    public String toString() {
+        return MessageFormat.format("blocked: {0}, substituted: {1}, not topical: {2}, invalid: {3}", isBlocked(),
+                isSubstituted(), isNotTopical(), isInvalid());
+    }
+}

+ 46 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeAckFileOrSectionQualifier.java

@@ -0,0 +1,46 @@
+
+package com.gyee.edge.protocol.iec60870.infoelement;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+
+/**
+ * Represents an acknowledge file or section qualifier (AFQ) information element.
+ */
+public class IeAckFileOrSectionQualifier extends InformationElement {
+
+    private final int action;
+    private final int notice;
+
+    public IeAckFileOrSectionQualifier(int action, int notice) {
+        this.action = action;
+        this.notice = notice;
+    }
+
+    static IeAckFileOrSectionQualifier decode(DataInputStream is) throws IOException {
+
+        int b1 = is.readUnsignedByte();
+        int action = b1 & 0x0f;
+        int notice = (b1 >> 4) & 0x0f;
+        return new IeAckFileOrSectionQualifier(action, notice);
+    }
+
+    @Override
+    int encode(byte[] buffer, int i) {
+        buffer[i] = (byte) (action | (notice << 4));
+        return 1;
+    }
+
+    public int getRequest() {
+        return action;
+    }
+
+    public int getFreeze() {
+        return notice;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("Acknowledge file or section qualifier, action: %d, notice: %d.", action, notice);
+    }
+}

+ 114 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeBinaryCounterReading.java

@@ -0,0 +1,114 @@
+
+package com.gyee.edge.protocol.iec60870.infoelement;
+
+import com.gyee.edge.protocol.iec60870.ExtendedDataInputStream;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.EnumSet;
+import java.util.Iterator;
+import java.util.Set;
+
+/**
+ * Represents a binary counter reading (BCR) information element.
+ */
+public class IeBinaryCounterReading extends InformationElement {
+
+    private final int counterReading;
+    private final int sequenceNumber;
+
+    private final Set<Flag> flags;
+
+    public IeBinaryCounterReading(int counterReading, int sequenceNumber, Set<Flag> flags) {
+        this.counterReading = counterReading;
+        this.sequenceNumber = sequenceNumber;
+        this.flags = flags;
+    }
+
+    public IeBinaryCounterReading(int counterReading, int sequenceNumber) {
+        this(counterReading, sequenceNumber, EnumSet.noneOf(Flag.class));
+    }
+
+    public IeBinaryCounterReading(int counterReading, int sequenceNumber, Flag firstFlag, Flag... flag) {
+        this(counterReading, sequenceNumber, EnumSet.of(firstFlag, flag));
+    }
+
+    static IeBinaryCounterReading decode(ExtendedDataInputStream is) throws IOException {
+
+        int counterReading = is.readLittleEndianInt();
+
+        byte b0 = is.readByte();
+
+        int sequenceNumber = b0 & 0x1f;
+
+        Set<Flag> flags = Flag.flagsFor(b0);
+        return new IeBinaryCounterReading(counterReading, sequenceNumber, flags);
+
+    }
+
+    @Override
+    int encode(byte[] buffer, int i) {
+
+        ByteBuffer buf = ByteBuffer.wrap(buffer, i, buffer.length - i);
+        buf.order(ByteOrder.LITTLE_ENDIAN);
+        buf.putInt(counterReading).put(seq());
+
+        return buf.position() - i;
+
+    }
+
+    private byte seq() {
+        byte v = (byte) sequenceNumber;
+        for (Flag flag : flags) {
+            v |= flag.mask;
+        }
+
+        return v;
+    }
+
+    public int getCounterReading() {
+        return counterReading;
+    }
+
+    public int getSequenceNumber() {
+        return sequenceNumber;
+    }
+
+    public Set<Flag> getFlags() {
+        return flags;
+    }
+
+    @Override
+    public String toString() {
+        return "Binary counter reading: " + counterReading + ", seq num: " + sequenceNumber + ", flags: " + flags;
+    }
+
+    public enum Flag {
+        CARRY(0x20),
+        COUNTER_ADJUSTED(0x40),
+        INVALID(0x80);
+
+        private int mask;
+
+        private Flag(int mask) {
+            this.mask = mask;
+        }
+
+        private static Set<Flag> flagsFor(byte b) {
+            EnumSet<Flag> s = EnumSet.allOf(Flag.class);
+
+            Iterator<Flag> iter = s.iterator();
+
+            while (iter.hasNext()) {
+                int mask2 = iter.next().mask;
+                if ((mask2 & b) != mask2) {
+                    iter.remove();
+                }
+            }
+
+            return s;
+        }
+
+    }
+}

+ 93 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeBinaryStateInformation.java

@@ -0,0 +1,93 @@
+
+package com.gyee.edge.protocol.iec60870.infoelement;
+
+import javax.xml.bind.DatatypeConverter;
+import java.io.DataInputStream;
+import java.io.IOException;
+
+/**
+ * Represents a binary state information (BSI) information element.
+ */
+public class IeBinaryStateInformation extends InformationElement {
+
+    private final int value;
+
+    /**
+     * Creates a BSI (binary state information) information element from an integer value.
+     *
+     * @param value the bits of value represent the 32 binary states of this element. When encoded in a message, the MSB
+     *              of <code>value</code> is transmitted first and the LSB of <code>value</code> is transmitted last.
+     */
+    public IeBinaryStateInformation(int value) {
+        this.value = value;
+    }
+
+    /**
+     * Creates a BSI (binary state information) information element from a byte array.
+     *
+     * @param value the bits of value represent the 32 binary states of this element. When encoded in a message, the MSB
+     *              of the first byte is transmitted first and the LSB of fourth byte is transmitted last.
+     */
+    public IeBinaryStateInformation(byte[] value) {
+        if (value == null || value.length != 4) {
+            throw new IllegalArgumentException("value needs to be of length 4");
+        }
+        this.value = (value[0] << 24) | ((value[1] & 0xff) << 16) | ((value[2] & 0xff) << 8) | (value[3] & 0xff);
+    }
+
+    IeBinaryStateInformation(DataInputStream is) throws IOException {
+        value = is.readInt();
+    }
+
+    @Override
+    int encode(byte[] buffer, int i) {
+        buffer[i++] = (byte) (value >> 24);
+        buffer[i++] = (byte) (value >> 16);
+        buffer[i++] = (byte) (value >> 8);
+        buffer[i] = (byte) value;
+        return 4;
+    }
+
+    /**
+     * Returns the 32 binary states of this element as an integer. When encoded in a message, the MSB of the return
+     * value is transmitted first and the LSB of the return value is transmitted last.
+     *
+     * @return the 32 binary states of this element.
+     */
+    public int getValue() {
+        return value;
+    }
+
+    /**
+     * Returns the 32 binary states of this element as a byte array. When encoded in a message, the MSB of the first
+     * byte is transmitted first and the LSB of the fourth byte is transmitted last.
+     *
+     * @return the 32 binary states of this element.
+     */
+    public byte[] getValueAsByteArray() {
+        return new byte[]{(byte) (value >> 24), (byte) (value >> 16), (byte) (value >> 8), (byte) (value)};
+    }
+
+    /**
+     * Returns true if the bit at the given position is 1 and false otherwise.
+     *
+     * @param position the position in the bit string. Range: 1-32. Position 1 represents the last bit in the encoded message
+     *                 and is the least significant bit (LSB) of the value returned by <code>getValue()</code>. Position 32
+     *                 represents the first bit in the encoded message and is the most significant bit (MSB) of the value
+     *                 returned by <code>getValue()</code>.
+     * @return true if the bit at the given position is 1 and false otherwise.
+     */
+    public boolean getBinaryState(int position) {
+        if (position < 1 || position > 32) {
+            throw new IllegalArgumentException("Position out of bound. Should be between 1 and 32.");
+        }
+        return (((value >> (position - 1)) & 0x01) == 0x01);
+    }
+
+    @Override
+    public String toString() {
+        return "BinaryStateInformation (32 bits as hex): " + DatatypeConverter.printHexBinary(
+                new byte[]{(byte) (value >> 24), (byte) (value >> 16), (byte) (value >> 8), (byte) (value)});
+    }
+
+}

+ 67 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeCauseOfInitialization.java

@@ -0,0 +1,67 @@
+
+package com.gyee.edge.protocol.iec60870.infoelement;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+
+/**
+ * Represents a cause of initialization (COI) information element.
+ */
+public class IeCauseOfInitialization extends InformationElement {
+
+    private final int value;
+    private final boolean initAfterParameterChange;
+
+    /**
+     * Creates a COI (cause of initialization) information element.
+     *
+     * @param value                    value between 0 and 127
+     * @param initAfterParameterChange true if initialization after change of local parameters and false if initialization with unchanged
+     *                                 local parameters
+     */
+    public IeCauseOfInitialization(int value, boolean initAfterParameterChange) {
+
+        if (value < 0 || value > 127) {
+            throw new IllegalArgumentException("Value has to be in the range 0..127");
+        }
+
+        this.value = value;
+        this.initAfterParameterChange = initAfterParameterChange;
+
+    }
+
+    IeCauseOfInitialization(DataInputStream is) throws IOException {
+        int b1 = (is.readByte() & 0xff);
+
+        initAfterParameterChange = ((b1 & 0x80) == 0x80);
+
+        value = b1 & 0x7f;
+
+    }
+
+    @Override
+    int encode(byte[] buffer, int i) {
+
+        if (initAfterParameterChange) {
+            buffer[i] = (byte) (value | 0x80);
+        } else {
+            buffer[i] = (byte) value;
+        }
+
+        return 1;
+
+    }
+
+    public int getValue() {
+        return value;
+    }
+
+    public boolean isInitAfterParameterChange() {
+        return initAfterParameterChange;
+    }
+
+    @Override
+    public String toString() {
+        return "Cause of initialization: " + value + ", init after parameter change: " + initAfterParameterChange;
+    }
+}

+ 35 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeChecksum.java

@@ -0,0 +1,35 @@
+package com.gyee.edge.protocol.iec60870.infoelement;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+
+/**
+ * Represents a checksum (CHS) information element.
+ */
+public class IeChecksum extends InformationElement {
+
+    private final int value;
+
+    public IeChecksum(int value) {
+        this.value = value;
+    }
+
+    static IeChecksum decode(DataInputStream is) throws IOException {
+        return new IeChecksum(is.readUnsignedByte());
+    }
+
+    @Override
+    int encode(byte[] buffer, int i) {
+        buffer[i] = (byte) value;
+        return 1;
+    }
+
+    public int getValue() {
+        return value;
+    }
+
+    @Override
+    public String toString() {
+        return "Checksum: " + value;
+    }
+}

+ 83 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeDoubleCommand.java

@@ -0,0 +1,83 @@
+package com.gyee.edge.protocol.iec60870.infoelement;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Represents a double command (DCO) information element.
+ */
+public class IeDoubleCommand extends IeAbstractQualifierOfCommand {
+
+    /**
+     * Create the Double Command Information Element.
+     *
+     * @param commandState the command state
+     * @param qualifier    the qualifier
+     * @param select       true if select, false if execute
+     */
+    public IeDoubleCommand(DoubleCommandState commandState, int qualifier, boolean select) {
+        super(qualifier, select);
+
+        value |= commandState.getId();
+    }
+
+    IeDoubleCommand(DataInputStream is) throws IOException {
+        super(is);
+    }
+
+    public DoubleCommandState getCommandState() {
+        return DoubleCommandState.getInstance(value & 0x03);
+    }
+
+    @Override
+    public String toString() {
+        return "Double Command state: " + getCommandState() + ", " + super.toString();
+    }
+
+    public enum DoubleCommandState {
+        NOT_PERMITTED_A(0),
+        OFF(1),
+        ON(2),
+        NOT_PERMITTED_B(3);
+
+        private static final Map<Integer, DoubleCommandState> idMap = new HashMap<>();
+
+        static {
+            for (DoubleCommandState enumInstance : DoubleCommandState.values()) {
+                if (idMap.put(enumInstance.getId(), enumInstance) != null) {
+                    throw new IllegalArgumentException("duplicate ID: " + enumInstance.getId());
+                }
+            }
+        }
+
+        private final int id;
+
+        private DoubleCommandState(int id) {
+            this.id = id;
+        }
+
+        /**
+         * Returns the DoubleCommandState that corresponds to the given ID. Returns <code>null</code> if no
+         * DoubleCommandState with the given ID exists.
+         *
+         * @param id the ID
+         * @return the DoubleCommandState that corresponds to the given ID
+         */
+        public static DoubleCommandState getInstance(int id) {
+            return idMap.get(id);
+        }
+
+        /**
+         * Returns the ID of this DoubleCommandState.
+         *
+         * @return the ID
+         */
+        public int getId() {
+            return id;
+        }
+
+    }
+
+}

+ 58 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeDoublePointWithQuality.java

@@ -0,0 +1,58 @@
+package com.gyee.edge.protocol.iec60870.infoelement;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+
+/**
+ * Represents a double-point information with quality descriptor (DIQ) information element.
+ */
+public class IeDoublePointWithQuality extends IeAbstractQuality {
+
+    public IeDoublePointWithQuality(DoublePointInformation dpi, boolean blocked, boolean substituted,
+                                    boolean notTopical, boolean invalid) {
+        super(blocked, substituted, notTopical, invalid);
+
+        switch (dpi) {
+            case INDETERMINATE_OR_INTERMEDIATE:
+                break;
+            case OFF:
+                value |= 0x01;
+                break;
+            case ON:
+                value |= 0x02;
+                break;
+            case INDETERMINATE:
+                value |= 0x03;
+                break;
+        }
+    }
+
+    IeDoublePointWithQuality(DataInputStream is) throws IOException {
+        super(is);
+    }
+
+    public DoublePointInformation getDoublePointInformation() {
+        switch (value & 0x03) {
+            case 0:
+                return DoublePointInformation.INDETERMINATE_OR_INTERMEDIATE;
+            case 1:
+                return DoublePointInformation.OFF;
+            case 2:
+                return DoublePointInformation.ON;
+            default:
+                return DoublePointInformation.INDETERMINATE;
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "Double Point: " + getDoublePointInformation() + ", " + super.toString();
+    }
+
+    public enum DoublePointInformation {
+        INDETERMINATE_OR_INTERMEDIATE,
+        OFF,
+        ON,
+        INDETERMINATE;
+    }
+}

+ 49 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeFileReadyQualifier.java

@@ -0,0 +1,49 @@
+package com.gyee.edge.protocol.iec60870.infoelement;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+
+/**
+ * Represents a file ready qualifier (FRQ) information element.
+ */
+public class IeFileReadyQualifier extends InformationElement {
+
+    private final int value;
+    private final boolean negativeConfirm;
+
+    public IeFileReadyQualifier(int value, boolean negativeConfirm) {
+        this.value = value;
+        this.negativeConfirm = negativeConfirm;
+    }
+
+    static InformationElement decode(DataInputStream is) throws IOException {
+        int b1 = is.readUnsignedByte();
+        int value = b1 & 0x7f;
+        boolean negativeConfirm = ((b1 & 0x80) == 0x80);
+
+        return new IeFileReadyQualifier(value, negativeConfirm);
+    }
+
+    @Override
+    int encode(byte[] buffer, int i) {
+        buffer[i] = (byte) value;
+        if (negativeConfirm) {
+            buffer[i] |= 0x80;
+        }
+        return 1;
+    }
+
+    public int getValue() {
+        return value;
+    }
+
+    public boolean isNegativeConfirm() {
+        return negativeConfirm;
+    }
+
+    @Override
+    public String toString() {
+        return "File ready qualifier: " + value + ", negative confirm: " + negativeConfirm;
+    }
+
+}

+ 49 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeFileSegment.java

@@ -0,0 +1,49 @@
+package com.gyee.edge.protocol.iec60870.infoelement;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+
+/**
+ * Represents the segment of a file as transferred by ASDUs of type F_SG_NA_1 (125).
+ */
+public class IeFileSegment extends InformationElement {
+
+    private final byte[] segment;
+    private final int offset;
+    private final int length;
+
+    public IeFileSegment(byte[] segment, int offset, int length) {
+        this.segment = segment;
+        this.offset = offset;
+        this.length = length;
+    }
+
+    IeFileSegment(DataInputStream is) throws IOException {
+
+        length = (is.readByte() & 0xff);
+        segment = new byte[length];
+
+        is.readFully(segment);
+        offset = 0;
+    }
+
+    @Override
+    int encode(byte[] buffer, int i) {
+
+        buffer[i++] = (byte) length;
+
+        System.arraycopy(segment, offset, buffer, i, length);
+
+        return length + 1;
+
+    }
+
+    public byte[] getSegment() {
+        return segment;
+    }
+
+    @Override
+    public String toString() {
+        return "File segment of length: " + length;
+    }
+}

+ 32 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeFixedTestBitPattern.java

@@ -0,0 +1,32 @@
+package com.gyee.edge.protocol.iec60870.infoelement;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+
+/**
+ * Represents a fixed test bit pattern (FBP) information element.
+ */
+public class IeFixedTestBitPattern extends InformationElement {
+
+    public IeFixedTestBitPattern() {
+    }
+
+    IeFixedTestBitPattern(DataInputStream is) throws IOException {
+        if ((is.readByte() & 0xff) != 0x55 || (is.readByte() & 0xff) != 0xaa) {
+            throw new IOException("Incorrect bit pattern in Fixed Test Bit Pattern.");
+        }
+    }
+
+    @Override
+    int encode(byte[] buffer, int i) {
+
+        buffer[i++] = 0x55;
+        buffer[i] = (byte) 0xaa;
+        return 2;
+    }
+
+    @Override
+    public String toString() {
+        return "Fixed test bit pattern";
+    }
+}

+ 35 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeLastSectionOrSegmentQualifier.java

@@ -0,0 +1,35 @@
+package com.gyee.edge.protocol.iec60870.infoelement;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+
+/**
+ * Represents a last section or segment qualifier (LSQ) information element.
+ */
+public class IeLastSectionOrSegmentQualifier extends InformationElement {
+
+    private final int value;
+
+    public IeLastSectionOrSegmentQualifier(int value) {
+        this.value = value;
+    }
+
+    static IeLastSectionOrSegmentQualifier decode(DataInputStream is) throws IOException {
+        return new IeLastSectionOrSegmentQualifier(is.readUnsignedByte());
+    }
+
+    @Override
+    int encode(byte[] buffer, int i) {
+        buffer[i] = (byte) value;
+        return 1;
+    }
+
+    public int getValue() {
+        return value;
+    }
+
+    @Override
+    public String toString() {
+        return "Last section or segment qualifier: " + value;
+    }
+}

+ 44 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeLengthOfFileOrSection.java

@@ -0,0 +1,44 @@
+package com.gyee.edge.protocol.iec60870.infoelement;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+
+/**
+ * Represents a length of file or section (LOF) information element.
+ */
+public class IeLengthOfFileOrSection extends InformationElement {
+
+    private final int value;
+
+    public IeLengthOfFileOrSection(int value) {
+        this.value = value;
+    }
+
+    static IeLengthOfFileOrSection decode(DataInputStream is) throws IOException {
+        int value = 0;
+        for (int i = 0; i < 3; ++i) {
+            value |= is.readUnsignedByte() << (i * 8);
+        }
+        return new IeLengthOfFileOrSection(value);
+    }
+
+    @Override
+    int encode(byte[] buffer, int i) {
+
+        buffer[i++] = (byte) value;
+        buffer[i++] = (byte) (value >> 8);
+        buffer[i] = (byte) (value >> 16);
+
+        return 3;
+
+    }
+
+    public int getValue() {
+        return value;
+    }
+
+    @Override
+    public String toString() {
+        return "Length of file or section: " + value;
+    }
+}

+ 39 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeNameOfFile.java

@@ -0,0 +1,39 @@
+package com.gyee.edge.protocol.iec60870.infoelement;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+
+/**
+ * Represents a name of file (NOF) information element.
+ */
+public class IeNameOfFile extends InformationElement {
+
+    private final int value;
+
+    public IeNameOfFile(int value) {
+        this.value = value;
+    }
+
+    static IeNameOfFile decode(DataInputStream is) throws IOException {
+        int value = is.readUnsignedByte() | (is.readUnsignedByte() << 8);
+        return new IeNameOfFile(value);
+    }
+
+    @Override
+    int encode(byte[] buffer, int i) {
+        buffer[i++] = (byte) value;
+        buffer[i] = (byte) (value >> 8);
+
+        return 2;
+
+    }
+
+    public int getValue() {
+        return value;
+    }
+
+    @Override
+    public String toString() {
+        return "Name of file: " + value;
+    }
+}

+ 36 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeNameOfSection.java

@@ -0,0 +1,36 @@
+package com.gyee.edge.protocol.iec60870.infoelement;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+
+/**
+ * Represents a name of section (NOS) information element.
+ */
+public class IeNameOfSection extends InformationElement {
+
+    private final int value;
+
+    public IeNameOfSection(int value) {
+        this.value = value;
+    }
+
+    static IeNameOfSection decode(DataInputStream is) throws IOException {
+        return new IeNameOfSection(is.readUnsignedByte());
+    }
+
+    @Override
+    int encode(byte[] buffer, int i) {
+        buffer[i] = (byte) value;
+        return 1;
+
+    }
+
+    public int getValue() {
+        return value;
+    }
+
+    @Override
+    public String toString() {
+        return "Name of section: " + value;
+    }
+}

+ 79 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeNormalizedValue.java

@@ -0,0 +1,79 @@
+package com.gyee.edge.protocol.iec60870.infoelement;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+
+/**
+ * Represents a normalized value (NVA) information element.
+ */
+public class IeNormalizedValue extends InformationElement {
+
+    final int value;
+
+    /**
+     * Normalized value is a value in the range from -1 to (1-1/(2^15)). The normalized value is encoded as a 16 bit
+     * integer ranging from -32768 to 32767. In order to get the normalized value the integer value is divided by 32768.
+     * Use this constructor to initialize the value exactly using the integer value in the range from -32768 to 32767.
+     *
+     * @param value non-normalized value in the range -32768 to 32767
+     */
+    public IeNormalizedValue(int value) {
+        if (value < -32768 || value > 32767) {
+            throw new IllegalArgumentException("Value has to be in the range -32768..32767");
+        }
+        this.value = value;
+    }
+
+    /**
+     * Normalized value is a value in the range from -1 to (1-1/(2^15)). Use this constructor to initialize the value
+     * using a double value ranging from -1 to (1-1/(2^15)).
+     *
+     * @param value normalized value in the range -1 to (1-1/(2^15))
+     */
+    public IeNormalizedValue(double value) {
+        this.value = (int) (value * 32768.0);
+        if (this.value < -32768 || this.value > 32767) {
+            throw new IllegalArgumentException(
+                    "The value multiplied by 32768 has to be an integer in the range -32768..32767, but it is: "
+                            + this.value);
+        }
+    }
+
+    IeNormalizedValue(DataInputStream is) throws IOException {
+        value = (is.readByte() & 0xff) | (is.readByte() << 8);
+    }
+
+    @Override
+    int encode(byte[] buffer, int i) {
+
+        buffer[i++] = (byte) value;
+        buffer[i] = (byte) (value >> 8);
+
+        return 2;
+    }
+
+    /**
+     * Get the value as a normalized double value ranging from -1 to (1-1/(2^15))
+     *
+     * @return the value as a normalized double.
+     */
+    public double getNormalizedValue() {
+        return ((double) value) / 32768;
+    }
+
+    /**
+     * Get the value as a non-normalized integer value ranging from -32768..32767. In order to get the normalized value
+     * the returned integer value has to be devided by 32768. The normalized value can also be retrieved using
+     * {@link #getNormalizedValue()}
+     *
+     * @return the value as a non-normalized integer value
+     */
+    public int getUnnormalizedValue() {
+        return value;
+    }
+
+    @Override
+    public String toString() {
+        return "Normalized value: " + ((double) value / 32768);
+    }
+}

+ 65 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeProtectionOutputCircuitInformation.java

@@ -0,0 +1,65 @@
+package com.gyee.edge.protocol.iec60870.infoelement;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+
+/**
+ * Represents an output circuit information of protection equipment (OCI) information element.
+ */
+public class IeProtectionOutputCircuitInformation extends InformationElement {
+
+    private int value;
+
+    public IeProtectionOutputCircuitInformation(boolean generalCommand, boolean commandToL1, boolean commandToL2,
+                                                boolean commandToL3) {
+
+        value = 0;
+
+        if (generalCommand) {
+            value |= 0x01;
+        }
+        if (commandToL1) {
+            value |= 0x02;
+        }
+        if (commandToL2) {
+            value |= 0x04;
+        }
+        if (commandToL3) {
+            value |= 0x08;
+        }
+
+    }
+
+    IeProtectionOutputCircuitInformation(DataInputStream is) throws IOException {
+        value = (is.readByte() & 0xff);
+    }
+
+    @Override
+    int encode(byte[] buffer, int i) {
+        buffer[i] = (byte) value;
+        return 1;
+    }
+
+    public boolean isGeneralCommand() {
+        return (value & 0x01) == 0x01;
+    }
+
+    public boolean isCommandToL1() {
+        return (value & 0x02) == 0x02;
+    }
+
+    public boolean isCommandToL2() {
+        return (value & 0x04) == 0x04;
+    }
+
+    public boolean isCommandToL3() {
+        return (value & 0x08) == 0x08;
+    }
+
+    @Override
+    public String toString() {
+        return "Protection output circuit information, general command: " + isGeneralCommand() + ", command to L1: "
+                + isCommandToL1() + ", command to L2: " + isCommandToL2() + ", command to L3: " + isCommandToL3();
+    }
+
+}

+ 32 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeProtectionQuality.java

@@ -0,0 +1,32 @@
+package com.gyee.edge.protocol.iec60870.infoelement;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+
+/**
+ * Represents a quality descriptor for events of protection equipment (QDP) information element.
+ */
+public class IeProtectionQuality extends IeAbstractQuality {
+
+    public IeProtectionQuality(boolean elapsedTimeInvalid, boolean blocked, boolean substituted, boolean notTopical,
+                               boolean invalid) {
+        super(blocked, substituted, notTopical, invalid);
+
+        if (elapsedTimeInvalid) {
+            value |= 0x08;
+        }
+    }
+
+    IeProtectionQuality(DataInputStream is) throws IOException {
+        super(is);
+    }
+
+    public boolean isElapsedTimeInvalid() {
+        return (value & 0x08) == 0x08;
+    }
+
+    @Override
+    public String toString() {
+        return "Protection Quality, elapsed time invalid: " + isElapsedTimeInvalid() + ", " + super.toString();
+    }
+}

+ 80 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeProtectionStartEvent.java

@@ -0,0 +1,80 @@
+package com.gyee.edge.protocol.iec60870.infoelement;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+
+/**
+ * Represents a start events of protection equipment (SPE) information element.
+ */
+public class IeProtectionStartEvent extends InformationElement {
+
+    private int value;
+
+    public IeProtectionStartEvent(boolean generalStart, boolean startOperationL1, boolean startOperationL2,
+                                  boolean startOperationL3, boolean startOperationIe, boolean startReverseOperation) {
+
+        value = 0;
+
+        if (generalStart) {
+            value |= 0x01;
+        }
+        if (startOperationL1) {
+            value |= 0x02;
+        }
+        if (startOperationL2) {
+            value |= 0x04;
+        }
+        if (startOperationL3) {
+            value |= 0x08;
+        }
+        if (startOperationIe) {
+            value |= 0x10;
+        }
+        if (startReverseOperation) {
+            value |= 0x20;
+        }
+    }
+
+    IeProtectionStartEvent(DataInputStream is) throws IOException {
+        value = (is.readByte() & 0xff);
+    }
+
+    @Override
+    int encode(byte[] buffer, int i) {
+        buffer[i] = (byte) value;
+        return 1;
+    }
+
+    public boolean isGeneralStart() {
+        return (value & 0x01) == 0x01;
+    }
+
+    public boolean isStartOperationL1() {
+        return (value & 0x02) == 0x02;
+    }
+
+    public boolean isStartOperationL2() {
+        return (value & 0x04) == 0x04;
+    }
+
+    public boolean isStartOperationL3() {
+        return (value & 0x08) == 0x08;
+    }
+
+    public boolean isStartOperationIe() {
+        return (value & 0x10) == 0x10;
+    }
+
+    public boolean isStartReverseOperation() {
+        return (value & 0x20) == 0x20;
+    }
+
+    @Override
+    public String toString() {
+        return "Protection start event, general start of operation: " + isGeneralStart() + ", start of operation L1: "
+                + isStartOperationL1() + ", start of operation L2: " + isStartOperationL2()
+                + ", start of operation L3: " + isStartOperationL3() + ", start of operation IE(earth current): "
+                + isStartOperationIe() + ", start of operation in reverse direction: " + isStartReverseOperation();
+    }
+
+}

+ 43 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeQualifierOfCounterInterrogation.java

@@ -0,0 +1,43 @@
+package com.gyee.edge.protocol.iec60870.infoelement;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+
+/**
+ * Represents a qualifier of counter interrogation (QCC) information element.
+ */
+public class IeQualifierOfCounterInterrogation extends InformationElement {
+
+    private final int request;
+    private final int freeze;
+
+    public IeQualifierOfCounterInterrogation(int request, int freeze) {
+        this.request = request;
+        this.freeze = freeze;
+    }
+
+    IeQualifierOfCounterInterrogation(DataInputStream is) throws IOException {
+        int b1 = (is.readByte() & 0xff);
+        request = b1 & 0x3f;
+        freeze = (b1 >> 6) & 0x03;
+    }
+
+    @Override
+    int encode(byte[] buffer, int i) {
+        buffer[i] = (byte) (request | (freeze << 6));
+        return 1;
+    }
+
+    public int getRequest() {
+        return request;
+    }
+
+    public int getFreeze() {
+        return freeze;
+    }
+
+    @Override
+    public String toString() {
+        return "Qualifier of counter interrogation, request: " + request + ", freeze: " + freeze;
+    }
+}

+ 35 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeQualifierOfInterrogation.java

@@ -0,0 +1,35 @@
+package com.gyee.edge.protocol.iec60870.infoelement;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+
+/**
+ * Represents a qualifier of interrogation (QOI) information element.
+ */
+public class IeQualifierOfInterrogation extends InformationElement {
+
+    private final int value;
+
+    public IeQualifierOfInterrogation(int value) {
+        this.value = value;
+    }
+
+    IeQualifierOfInterrogation(DataInputStream is) throws IOException {
+        value = (is.readByte() & 0xff);
+    }
+
+    @Override
+    int encode(byte[] buffer, int i) {
+        buffer[i] = (byte) value;
+        return 1;
+    }
+
+    public int getValue() {
+        return value;
+    }
+
+    @Override
+    public String toString() {
+        return "Qualifier of interrogation: " + value;
+    }
+}

+ 35 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeQualifierOfParameterActivation.java

@@ -0,0 +1,35 @@
+package com.gyee.edge.protocol.iec60870.infoelement;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+
+/**
+ * Represents a qualifier of parameter activation (QPA) information element.
+ */
+public class IeQualifierOfParameterActivation extends InformationElement {
+
+    private final int value;
+
+    public IeQualifierOfParameterActivation(int value) {
+        this.value = value;
+    }
+
+    static IeQualifierOfParameterActivation decode(DataInputStream is) throws IOException {
+        return new IeQualifierOfParameterActivation(is.readUnsignedByte());
+    }
+
+    @Override
+    int encode(byte[] buffer, int i) {
+        buffer[i] = (byte) value;
+        return 1;
+    }
+
+    public int getValue() {
+        return value;
+    }
+
+    @Override
+    public String toString() {
+        return "Qualifier of parameter activation: " + value;
+    }
+}

+ 57 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeQualifierOfParameterOfMeasuredValues.java

@@ -0,0 +1,57 @@
+package com.gyee.edge.protocol.iec60870.infoelement;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+
+/**
+ * Represents a qualifier of parameter of measured values (QPM) information element.
+ */
+public class IeQualifierOfParameterOfMeasuredValues extends InformationElement {
+
+    private final int kindOfParameter;
+    private final boolean change;
+    private final boolean notInOperation;
+
+    public IeQualifierOfParameterOfMeasuredValues(int kindOfParameter, boolean change, boolean notInOperation) {
+        this.kindOfParameter = kindOfParameter;
+        this.change = change;
+        this.notInOperation = notInOperation;
+    }
+
+    IeQualifierOfParameterOfMeasuredValues(DataInputStream is) throws IOException {
+        int b1 = (is.readByte() & 0xff);
+        kindOfParameter = b1 & 0x3f;
+        change = ((b1 & 0x40) == 0x40);
+        notInOperation = ((b1 & 0x80) == 0x80);
+    }
+
+    @Override
+    int encode(byte[] buffer, int i) {
+        buffer[i] = (byte) kindOfParameter;
+        if (change) {
+            buffer[i] |= 0x40;
+        }
+        if (notInOperation) {
+            buffer[i] |= 0x80;
+        }
+        return 1;
+    }
+
+    public int getKindOfParameter() {
+        return kindOfParameter;
+    }
+
+    public boolean isChange() {
+        return change;
+    }
+
+    public boolean isNotInOperation() {
+        return notInOperation;
+    }
+
+    @Override
+    public String toString() {
+        return "Qualifier of parameter of measured values, kind of parameter: " + kindOfParameter + ", change: "
+                + change + ", not in operation: " + notInOperation;
+    }
+}

+ 35 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeQualifierOfResetProcessCommand.java

@@ -0,0 +1,35 @@
+package com.gyee.edge.protocol.iec60870.infoelement;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+
+/**
+ * Represents a qualifier of reset process command (QRP) information element.
+ */
+public class IeQualifierOfResetProcessCommand extends InformationElement {
+
+    private final int value;
+
+    public IeQualifierOfResetProcessCommand(int value) {
+        this.value = value;
+    }
+
+    IeQualifierOfResetProcessCommand(DataInputStream is) throws IOException {
+        value = (is.readByte() & 0xff);
+    }
+
+    @Override
+    int encode(byte[] buffer, int i) {
+        buffer[i] = (byte) value;
+        return 1;
+    }
+
+    public int getValue() {
+        return value;
+    }
+
+    @Override
+    public String toString() {
+        return "Qualifier of reset process command: " + value;
+    }
+}

+ 46 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeQualifierOfSetPointCommand.java

@@ -0,0 +1,46 @@
+package com.gyee.edge.protocol.iec60870.infoelement;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+
+/**
+ * Represents a qualifier of set-point command (QOS) information element.
+ */
+public class IeQualifierOfSetPointCommand extends InformationElement {
+
+    private final int ql;
+    private final boolean select;
+
+    public IeQualifierOfSetPointCommand(int ql, boolean select) {
+        this.ql = ql;
+        this.select = select;
+    }
+
+    IeQualifierOfSetPointCommand(DataInputStream is) throws IOException {
+        int b1 = (is.readByte() & 0xff);
+        ql = b1 & 0x7f;
+        select = ((b1 & 0x80) == 0x80);
+    }
+
+    @Override
+    int encode(byte[] buffer, int i) {
+        buffer[i] = (byte) ql;
+        if (select) {
+            buffer[i] |= 0x80;
+        }
+        return 1;
+    }
+
+    public int getQl() {
+        return ql;
+    }
+
+    public boolean isSelect() {
+        return select;
+    }
+
+    @Override
+    public String toString() {
+        return "Qualifier of set point command, QL: " + ql + ", select: " + select;
+    }
+}

+ 31 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeQuality.java

@@ -0,0 +1,31 @@
+package com.gyee.edge.protocol.iec60870.infoelement;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+
+/**
+ * Represents a quality descriptor (QDS) information element.
+ */
+public class IeQuality extends IeAbstractQuality {
+
+    public IeQuality(boolean overflow, boolean blocked, boolean substituted, boolean notTopical, boolean invalid) {
+        super(blocked, substituted, notTopical, invalid);
+
+        if (overflow) {
+            value |= 0x01;
+        }
+    }
+
+    IeQuality(DataInputStream is) throws IOException {
+        super(is);
+    }
+
+    public boolean isOverflow() {
+        return (value & 0x01) == 0x01;
+    }
+
+    @Override
+    public String toString() {
+        return "Quality, overflow: " + isOverflow() + ", " + super.toString();
+    }
+}

+ 83 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeRegulatingStepCommand.java

@@ -0,0 +1,83 @@
+package com.gyee.edge.protocol.iec60870.infoelement;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Represents a regulating step command (RCO) information element.
+ */
+public class IeRegulatingStepCommand extends IeAbstractQualifierOfCommand {
+
+    /**
+     * Create a Regulating Step Command Information Element.
+     *
+     * @param commandState the command state
+     * @param qualifier    the qualifier
+     * @param select       true if select, false if execute
+     */
+    public IeRegulatingStepCommand(StepCommandState commandState, int qualifier, boolean select) {
+        super(qualifier, select);
+
+        value |= commandState.getId();
+    }
+
+    IeRegulatingStepCommand(DataInputStream is) throws IOException {
+        super(is);
+    }
+
+    public StepCommandState getCommandState() {
+        return StepCommandState.getInstance(value & 0x03);
+    }
+
+    @Override
+    public String toString() {
+        return "Regulating step command state: " + getCommandState() + ", " + super.toString();
+    }
+
+    public enum StepCommandState {
+        NOT_PERMITTED_A(0),
+        NEXT_STEP_LOWER(1),
+        NEXT_STEP_HIGHER(2),
+        NOT_PERMITTED_B(3);
+
+        private static final Map<Integer, StepCommandState> idMap = new HashMap<>();
+
+        static {
+            for (StepCommandState enumInstance : StepCommandState.values()) {
+                if (idMap.put(enumInstance.getId(), enumInstance) != null) {
+                    throw new IllegalArgumentException("duplicate ID: " + enumInstance.getId());
+                }
+            }
+        }
+
+        private final int id;
+
+        private StepCommandState(int id) {
+            this.id = id;
+        }
+
+        /**
+         * Returns the StepCommandState that corresponds to the given ID. Returns <code>null</code> if no
+         * StepCommandState with the given ID exists.
+         *
+         * @param id the ID
+         * @return the StepCommandState that corresponds to the given ID
+         */
+        public static StepCommandState getInstance(int id) {
+            return idMap.get(id);
+        }
+
+        /**
+         * Returns the ID of this StepCommandState.
+         *
+         * @return the ID
+         */
+        public int getId() {
+            return id;
+        }
+
+    }
+
+}

+ 29 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeScaledValue.java

@@ -0,0 +1,29 @@
+package com.gyee.edge.protocol.iec60870.infoelement;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+
+/**
+ * Represents a scaled value (SVA) information element.
+ */
+public class IeScaledValue extends IeNormalizedValue {
+
+    /**
+     * Scaled value is a 16 bit integer (short) in the range from -32768 to 32767
+     *
+     * @param value value in the range -32768 to 32767
+     */
+    public IeScaledValue(int value) {
+        super(value);
+    }
+
+    IeScaledValue(DataInputStream is) throws IOException {
+        super(is);
+    }
+
+    @Override
+    public String toString() {
+        return "Scaled value: " + value;
+    }
+
+}

+ 47 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeSectionReadyQualifier.java

@@ -0,0 +1,47 @@
+package com.gyee.edge.protocol.iec60870.infoelement;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+
+/**
+ * Represents a section ready qualifier (SRQ) information element.
+ */
+public class IeSectionReadyQualifier extends InformationElement {
+
+    private final int value;
+    private final boolean sectionNotReady;
+
+    public IeSectionReadyQualifier(int value, boolean sectionNotReady) {
+        this.value = value;
+        this.sectionNotReady = sectionNotReady;
+    }
+
+    static IeSectionReadyQualifier decode(DataInputStream is) throws IOException {
+        int b1 = is.readUnsignedByte();
+        int value = b1 & 0x7f;
+        boolean sectionNotReady = ((b1 & 0x80) == 0x80);
+        return new IeSectionReadyQualifier(value, sectionNotReady);
+    }
+
+    @Override
+    int encode(byte[] buffer, int i) {
+        buffer[i] = (byte) value;
+        if (sectionNotReady) {
+            buffer[i] |= 0x80;
+        }
+        return 1;
+    }
+
+    public int getValue() {
+        return value;
+    }
+
+    public boolean isSectionNotReady() {
+        return sectionNotReady;
+    }
+
+    @Override
+    public String toString() {
+        return "Section ready qualifier: " + value + ", section not ready: " + sectionNotReady;
+    }
+}

+ 45 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeSelectAndCallQualifier.java

@@ -0,0 +1,45 @@
+package com.gyee.edge.protocol.iec60870.infoelement;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+
+/**
+ * Represents a select and call qualifier (SCQ) information element.
+ */
+public class IeSelectAndCallQualifier extends InformationElement {
+
+    private final int action;
+    private final int notice;
+
+    public IeSelectAndCallQualifier(int action, int notice) {
+        this.action = action;
+        this.notice = notice;
+    }
+
+    static IeSelectAndCallQualifier decode(DataInputStream is) throws IOException {
+        int b1 = is.readUnsignedByte();
+
+        int action = b1 & 0x0f;
+        int notice = (b1 >> 4) & 0x0f;
+        return new IeSelectAndCallQualifier(action, notice);
+    }
+
+    @Override
+    int encode(byte[] buffer, int i) {
+        buffer[i] = (byte) (action | (notice << 4));
+        return 1;
+    }
+
+    public int getRequest() {
+        return action;
+    }
+
+    public int getFreeze() {
+        return notice;
+    }
+
+    @Override
+    public String toString() {
+        return "Select and call qualifier, action: " + action + ", notice: " + notice;
+    }
+}

+ 42 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeShortFloat.java

@@ -0,0 +1,42 @@
+package com.gyee.edge.protocol.iec60870.infoelement;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+
+/**
+ * Represents a short floating point number (R32-IEEE STD 754) information element.
+ */
+public class IeShortFloat extends InformationElement {
+
+    private final float value;
+
+    public IeShortFloat(float value) {
+        this.value = value;
+    }
+
+    IeShortFloat(DataInputStream is) throws IOException {
+        value = Float.intBitsToFloat((is.readByte() & 0xff) | ((is.readByte() & 0xff) << 8)
+                | ((is.readByte() & 0xff) << 16) | ((is.readByte() & 0xff) << 24));
+    }
+
+    @Override
+    int encode(byte[] buffer, int i) {
+
+        int tempVal = Float.floatToIntBits(value);
+        buffer[i++] = (byte) tempVal;
+        buffer[i++] = (byte) (tempVal >> 8);
+        buffer[i++] = (byte) (tempVal >> 16);
+        buffer[i] = (byte) (tempVal >> 24);
+
+        return 4;
+    }
+
+    public float getValue() {
+        return value;
+    }
+
+    @Override
+    public String toString() {
+        return "Short float value: " + value;
+    }
+}

+ 32 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeSingleCommand.java

@@ -0,0 +1,32 @@
+package com.gyee.edge.protocol.iec60870.infoelement;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+
+/**
+ * Represents a single command (SCO) information element.
+ */
+public class IeSingleCommand extends IeAbstractQualifierOfCommand {
+
+    public IeSingleCommand(boolean commandStateOn, int qualifier, boolean select) {
+        super(qualifier, select);
+
+        if (commandStateOn) {
+            value |= 0x01;
+        }
+    }
+
+    IeSingleCommand(DataInputStream is) throws IOException {
+        super(is);
+    }
+
+    public boolean isCommandStateOn() {
+        return (value & 0x01) == 0x01;
+    }
+
+    @Override
+    public String toString() {
+        return "Single Command state on: " + isCommandStateOn() + ", " + super.toString();
+    }
+
+}

+ 32 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeSinglePointWithQuality.java

@@ -0,0 +1,32 @@
+package com.gyee.edge.protocol.iec60870.infoelement;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+
+/**
+ * Represents a single-point information with quality descriptor (SIQ) information element.
+ */
+public class IeSinglePointWithQuality extends IeAbstractQuality {
+
+    public IeSinglePointWithQuality(boolean on, boolean blocked, boolean substituted, boolean notTopical,
+                                    boolean invalid) {
+        super(blocked, substituted, notTopical, invalid);
+
+        if (on) {
+            value |= 0x01;
+        }
+    }
+
+    IeSinglePointWithQuality(DataInputStream is) throws IOException {
+        super(is);
+    }
+
+    public boolean isOn() {
+        return (value & 0x01) == 0x01;
+    }
+
+    @Override
+    public String toString() {
+        return "Single Point, is on: " + isOn() + ", " + super.toString();
+    }
+}

+ 100 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeSingleProtectionEvent.java

@@ -0,0 +1,100 @@
+package com.gyee.edge.protocol.iec60870.infoelement;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+
+/**
+ * Represents a single event of protection equipment (SEP) information element.
+ */
+public class IeSingleProtectionEvent extends InformationElement {
+
+    private int value;
+
+    public IeSingleProtectionEvent(EventState eventState, boolean elapsedTimeInvalid, boolean blocked,
+                                   boolean substituted, boolean notTopical, boolean eventInvalid) {
+
+        value = 0;
+
+        switch (eventState) {
+            case OFF:
+                value |= 0x01;
+                break;
+            case ON:
+                value |= 0x02;
+                break;
+            default:
+                break;
+        }
+
+        if (elapsedTimeInvalid) {
+            value |= 0x08;
+        }
+        if (blocked) {
+            value |= 0x10;
+        }
+        if (substituted) {
+            value |= 0x20;
+        }
+        if (notTopical) {
+            value |= 0x40;
+        }
+        if (eventInvalid) {
+            value |= 0x80;
+        }
+    }
+
+    IeSingleProtectionEvent(DataInputStream is) throws IOException {
+        value = (is.readByte() & 0xff);
+    }
+
+    @Override
+    int encode(byte[] buffer, int i) {
+        buffer[i] = (byte) value;
+        return 1;
+    }
+
+    public EventState getEventState() {
+        switch (value & 0x03) {
+            case 1:
+                return EventState.OFF;
+            case 2:
+                return EventState.ON;
+            default:
+                return EventState.INDETERMINATE;
+        }
+    }
+
+    public boolean isElapsedTimeInvalid() {
+        return (value & 0x08) == 0x08;
+    }
+
+    public boolean isBlocked() {
+        return (value & 0x10) == 0x10;
+    }
+
+    public boolean isSubstituted() {
+        return (value & 0x20) == 0x20;
+    }
+
+    public boolean isNotTopical() {
+        return (value & 0x40) == 0x40;
+    }
+
+    public boolean isEventInvalid() {
+        return (value & 0x80) == 0x80;
+    }
+
+    @Override
+    public String toString() {
+        return "Single protection event, elapsed time invalid: " + isElapsedTimeInvalid() + ", blocked: " + isBlocked()
+                + ", substituted: " + isSubstituted() + ", not topical: " + isNotTopical() + ", event invalid: "
+                + isEventInvalid();
+    }
+
+    public enum EventState {
+        INDETERMINATE,
+        OFF,
+        ON;
+    }
+
+}

+ 88 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeStatusAndStatusChanges.java

@@ -0,0 +1,88 @@
+package com.gyee.edge.protocol.iec60870.infoelement;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+
+/**
+ * Represents a status and status change detection (SCD) information element.
+ */
+public class IeStatusAndStatusChanges extends InformationElement {
+
+    private final int value;
+
+    /**
+     * Creates a SCD (status and status change detection) information element.
+     *
+     * @param value the bits of value represent the status and status changed bits. Bit1 (the least significant bit) of
+     *              value represents the first status changed detection bit. Bit17 of value represents the first status
+     *              bit.
+     */
+    public IeStatusAndStatusChanges(int value) {
+
+        this.value = value;
+    }
+
+    IeStatusAndStatusChanges(DataInputStream is) throws IOException {
+        value = is.readInt();
+    }
+
+    @Override
+    int encode(byte[] buffer, int i) {
+        buffer[i++] = (byte) (value >> 24);
+        buffer[i++] = (byte) (value >> 16);
+        buffer[i++] = (byte) (value >> 8);
+        buffer[i] = (byte) value;
+        return 4;
+    }
+
+    public int getValue() {
+        return value;
+    }
+
+    /**
+     * Returns true if the status at the given position is ON(1) and false otherwise.
+     *
+     * @param position the position in the status bitstring. Range: 1-16. Status 1 is bit 17 and status 16 is bit 32 of the
+     *                 value returned by <code>getValue()</code>.
+     * @return true if the status at the given position is ON(1) and false otherwise.
+     */
+    public boolean getStatus(int position) {
+        if (position < 1 || position > 16) {
+            throw new IllegalArgumentException("Position out of bound. Should be between 1 and 16.");
+        }
+        return (((value >> (position - 17)) & 0x01) == 0x01);
+    }
+
+    /**
+     * Returns true if the status at the given position has changed and false otherwise.
+     *
+     * @param position the position in the status changed bitstring. Range: 1-16. Status changed 1 is bit 1 and status 16 is
+     *                 bit 16 of the value returned by <code>getValue()</code>.
+     * @return true if the status at the given position has changed and false otherwise.
+     */
+    public boolean hasStatusChanged(int position) {
+        if (position < 1 || position > 16) {
+            throw new IllegalArgumentException("Position out of bound. Should be between 1 and 16.");
+        }
+        return (((value >> (position - 1)) & 0x01) == 0x01);
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb1 = new StringBuilder();
+        sb1.append(Integer.toHexString(value >>> 16));
+        while (sb1.length() < 4) {
+            sb1.insert(0, '0'); // pad with leading zero if needed
+        }
+
+        StringBuilder sb2 = new StringBuilder();
+        sb2.append(Integer.toHexString(value & 0xffff));
+        while (sb2.length() < 4) {
+            sb2.insert(0, '0'); // pad with leading zero if needed
+        }
+
+        return "Status and status changes (first bit = LSB), states: " + sb1.toString() + ", state changes: "
+                + sb2.toString();
+    }
+
+}

+ 82 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeStatusOfFile.java

@@ -0,0 +1,82 @@
+package com.gyee.edge.protocol.iec60870.infoelement;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Represents a status of file (SOF) information element.
+ */
+public class IeStatusOfFile extends InformationElement {
+
+    private final int status;
+    private final Set<Flag> flags;
+
+    public IeStatusOfFile(int status, Flag... flags) {
+        this(status, new HashSet<>(Arrays.asList(flags)));
+    }
+
+    public IeStatusOfFile(int status, Set<Flag> flags) {
+        this.status = status;
+        this.flags = flags;
+    }
+
+    static IeStatusOfFile decode(DataInputStream is) throws IOException {
+        int b1 = is.readUnsignedByte();
+        int status = b1 & 0x1f;
+
+        Set<Flag> flags = Flag.flagsFor(b1);
+
+        return new IeStatusOfFile(status, flags);
+    }
+
+    @Override
+    int encode(byte[] buffer, int i) {
+        buffer[i] = (byte) status;
+        for (Flag f : flags) {
+            buffer[i] |= (byte) f.mask;
+        }
+        return 1;
+    }
+
+    public int getStatus() {
+        return status;
+    }
+
+    public Set<Flag> getFlags() {
+        return flags;
+    }
+
+    @Override
+    public String toString() {
+        return "Status of file: " + status + ", last file of directory: " + flags.contains(Flag.LAST_FILE_OF_DIRECTORY)
+                + ", name defines directory: " + flags.contains(Flag.NAME_DEFINES_DIRECTORY) + ", transfer is active: "
+                + flags.contains(Flag.TRANSFER_IS_ACTIVE);
+    }
+
+    public enum Flag {
+        LAST_FILE_OF_DIRECTORY(0x20),
+        NAME_DEFINES_DIRECTORY(0x40),
+        TRANSFER_IS_ACTIVE(0x80);
+
+        private int mask;
+
+        private Flag(int mask) {
+            this.mask = mask;
+        }
+
+        private static Set<Flag> flagsFor(int b) {
+            HashSet<Flag> res = new HashSet<>();
+            for (Flag v : values()) {
+                if ((v.mask & b) != v.mask) {
+                    continue;
+                }
+                res.add(v);
+            }
+            return res;
+        }
+
+    }
+}

+ 46 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeTestSequenceCounter.java

@@ -0,0 +1,46 @@
+package com.gyee.edge.protocol.iec60870.infoelement;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+
+/**
+ * Represents a test sequence Counter (TSC) information element.
+ */
+public class IeTestSequenceCounter extends InformationElement {
+
+    private static final int LOWER_BOUND = 0;
+    private static final int UPPER_BOUND = 65535; // 2^16 - 1
+
+    private final int value;
+
+    public IeTestSequenceCounter(int value) {
+        if (value < LOWER_BOUND || value > UPPER_BOUND) {
+            throw new IllegalArgumentException("Value has to be in the range 0..65535");
+        }
+
+        this.value = value;
+    }
+
+    static IeTestSequenceCounter decode(DataInputStream is) throws IOException {
+        return new IeTestSequenceCounter(is.readUnsignedByte() | (is.readUnsignedByte() << 8));
+    }
+
+    @Override
+    int encode(byte[] buffer, int i) {
+
+        buffer[i++] = (byte) value;
+        buffer[i] = (byte) (value >> 8);
+
+        return 2;
+    }
+
+    public int getValue() {
+        return value;
+    }
+
+    @Override
+    public String toString() {
+        return "Test sequence counter: " + getValue();
+    }
+
+}

+ 49 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeTime16.java

@@ -0,0 +1,49 @@
+package com.gyee.edge.protocol.iec60870.infoelement;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.util.Calendar;
+
+/**
+ * Represents a two octet binary time (CP16Time2a) information element.
+ */
+public class IeTime16 extends InformationElement {
+
+    private final byte[] value = new byte[2];
+
+    public IeTime16(long timestamp) {
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTimeInMillis(timestamp);
+
+        int ms = calendar.get(Calendar.MILLISECOND) + 1000 * calendar.get(Calendar.SECOND);
+
+        value[0] = (byte) ms;
+        value[1] = (byte) (ms >> 8);
+    }
+
+    public IeTime16(int timeInMs) {
+
+        int ms = timeInMs % 60000;
+        value[0] = (byte) ms;
+        value[1] = (byte) (ms >> 8);
+    }
+
+    IeTime16(DataInputStream is) throws IOException {
+        is.readFully(value);
+    }
+
+    @Override
+    int encode(byte[] buffer, int i) {
+        System.arraycopy(value, 0, buffer, i, 2);
+        return 2;
+    }
+
+    public int getTimeInMs() {
+        return (value[0] & 0xff) + ((value[1] & 0xff) << 8);
+    }
+
+    @Override
+    public String toString() {
+        return "Time16, time in ms: " + getTimeInMs();
+    }
+}

+ 51 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeTime24.java

@@ -0,0 +1,51 @@
+package com.gyee.edge.protocol.iec60870.infoelement;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.util.Calendar;
+
+/**
+ * Represents a three octet binary time (CP24Time2a) information element.
+ */
+public class IeTime24 extends InformationElement {
+
+    private final byte[] value = new byte[3];
+
+    public IeTime24(long timestamp) {
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTimeInMillis(timestamp);
+
+        int ms = calendar.get(Calendar.MILLISECOND) + 1000 * calendar.get(Calendar.SECOND);
+
+        value[0] = (byte) ms;
+        value[1] = (byte) (ms >> 8);
+        value[2] = (byte) calendar.get(Calendar.MINUTE);
+    }
+
+    public IeTime24(int timeInMs) {
+
+        int ms = timeInMs % 60000;
+        value[0] = (byte) ms;
+        value[1] = (byte) (ms >> 8);
+        value[2] = (byte) (timeInMs / 60000);
+    }
+
+    IeTime24(DataInputStream is) throws IOException {
+        is.readFully(value);
+    }
+
+    @Override
+    int encode(byte[] buffer, int i) {
+        System.arraycopy(value, 0, buffer, i, 3);
+        return 3;
+    }
+
+    public int getTimeInMs() {
+        return (value[0] & 0xff) + ((value[1] & 0xff) << 8) + value[2] * 60000;
+    }
+
+    @Override
+    public String toString() {
+        return "Time24, time in ms: " + getTimeInMs();
+    }
+}

+ 286 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeTime56.java

@@ -0,0 +1,286 @@
+package com.gyee.edge.protocol.iec60870.infoelement;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.TimeZone;
+
+/**
+ * Represents a seven octet binary time (CP56Time2a) information element.
+ */
+public class IeTime56 extends InformationElement {
+
+    private static final int LENGTH = 7;
+    private final byte[] value;
+    private final TimeZone timeZone;
+
+    /**
+     * Creates a Time56 instance using the given timestamp and time zone.
+     *
+     * @param timestamp the timestamp that shall be used to calculate Time56
+     * @param timeZone  the time zone to use
+     * @param invalid   true if the time shall be marked as invalid
+     */
+    public IeTime56(long timestamp, TimeZone timeZone, boolean invalid) {
+        Calendar calendar = Calendar.getInstance(timeZone);
+        this.timeZone = timeZone;
+        calendar.setTimeInMillis(timestamp);
+
+        short ms = (short) (calendar.get(Calendar.MILLISECOND) + 1000 * calendar.get(Calendar.SECOND));
+
+        byte day = (byte) (calendar.get(Calendar.DAY_OF_MONTH)
+                + ((((calendar.get(Calendar.DAY_OF_WEEK) + 5) % 7) + 1) << 5));
+
+        this.value = ByteBuffer.allocate(7)
+                .order(ByteOrder.LITTLE_ENDIAN)
+                .putShort(ms)
+                .put(minuteFor(invalid, calendar))
+                .put(hourOfTheDay(calendar))
+                .put(day)
+                .put((byte) (calendar.get(Calendar.MONTH) + 1))
+                .put((byte) (calendar.get(Calendar.YEAR) % 100))
+                .array();
+    }
+
+    /**
+     * Creates a valid Time56 instance using the given timestamp and the default time zone.
+     *
+     * @param timestamp the timestamp that shall be used to calculate Time56
+     */
+    public IeTime56(long timestamp) {
+        this(timestamp, TimeZone.getDefault(), false);
+    }
+
+    /**
+     * Creates a valid Time56 instance using the given byte array and the default time zone.
+     *
+     * @param value a Time56 decoded byte array that shall be set
+     */
+    public IeTime56(byte[] value) {
+        this.timeZone = TimeZone.getDefault();
+        this.value = Arrays.copyOf(value, LENGTH);
+    }
+
+    private static byte hourOfTheDay(Calendar calendar) {
+        int hod = calendar.get(Calendar.HOUR_OF_DAY);
+        if (calendar.getTimeZone().inDaylightTime(calendar.getTime())) {
+            hod |= 0x80;
+        }
+        return (byte) hod;
+    }
+
+    private static byte minuteFor(boolean invalid, Calendar calendar) {
+        int min = calendar.get(Calendar.MINUTE);
+        if (invalid) {
+            min |= 0x80;
+        }
+        return (byte) min;
+    }
+
+    static IeTime56 decode(DataInputStream is) throws IOException {
+        byte[] value = new byte[LENGTH];
+        is.readFully(value);
+
+        return new IeTime56(value);
+    }
+
+    @Override
+    int encode(byte[] buffer, int i) {
+        System.arraycopy(value, 0, buffer, i, LENGTH);
+        return LENGTH;
+    }
+
+    /**
+     * Returns the timestamp in ms equivalent to this Time56 instance.
+     * <p>
+     * Note that Time56 does not store the century of the date. Therefore you have to pass the earliest possible year of
+     * the Time56 instance. Say the year stored by Time56 is 10. From this information alone it is not possible to tell
+     * whether the real year is 1910 or 2010 or 2110. If you pass 1970 as the start of century, then this function will
+     * know that the year of the given date lies between 1970 and 2069 and can therefore calculate that the correct date
+     * is 2010.
+     *
+     * @param startOfCentury The timestamp will
+     * @param timeZone       the timezone that shall be used to calculate the timestamp.
+     * @return the timestamp in ms equivalent to this Time56 instance
+     */
+    public long getTimestamp(int startOfCentury, TimeZone timeZone) {
+
+        int century = startOfCentury / 100 * 100;
+        if (value[6] < (startOfCentury % 100)) {
+            century += 100;
+        }
+
+        Calendar calendar = Calendar.getInstance(timeZone);
+        calendar.set(Calendar.DST_OFFSET, isSummerTime() ? 3600000 : 0);
+        calendar.set(getYear() + century, getMonth() - 1, getDayOfMonth(), getHour(), getMinute(), getSecond());
+        calendar.set(Calendar.MILLISECOND, getMillisecond());
+
+        return calendar.getTimeInMillis();
+    }
+
+    /**
+     * Returns the timestamp in ms equivalent to this Time56 instance. The default time zone is used if no time zone is
+     * configured.
+     * <p>
+     * Note that Time56 does not store the century of the date. Therefore you have to pass the earliest possible year of
+     * the Time56 instance. Say the year stored by Time56 is 10. From this information alone it is not possible to tell
+     * whether the real year is 1910 or 2010 or 2110. If you pass 1970 as the start of century, then this function will
+     * know that the year of the given date lies between 1970 and 2069 and can therefore calculate that the correct date
+     * is 2010.
+     *
+     * @param startOfCentury The timestamp will
+     * @return the timestamp in ms equivalent to this Time56 instance
+     */
+    public long getTimestamp(int startOfCentury) {
+        return getTimestamp(startOfCentury, timeZone);
+    }
+
+    /**
+     * Returns the timestamp in ms equivalent to this Time56 instance. Assumes that the given date is between 1970 and
+     * 2070.The default time zone is used if no time zone is configured.
+     *
+     * @return the timestamp in ms equivalent to this Time56 instance
+     */
+    public long getTimestamp() {
+        return getTimestamp(1970, timeZone);
+    }
+
+    /**
+     * Returns the configured time zone.
+     *
+     * @return the used time zone
+     */
+    public TimeZone getTimeZone() {
+        return timeZone;
+    }
+
+    /**
+     * Returns the millisecond of the second. Returned values can range from 0 to 999.
+     *
+     * @return the millisecond of the second
+     */
+    public int getMillisecond() {
+        return getV0V1Short() % 1000;
+    }
+
+    /**
+     * Returns the second of the minute. Returned values can range from 0 to 59.
+     *
+     * @return the second of the minute
+     */
+    public int getSecond() {
+        return getV0V1Short() / 1000;
+    }
+
+    private int getV0V1Short() {
+        return ((value[0] & 0xff) + ((value[1] & 0xff) << 8));
+    }
+
+    /**
+     * Returns the minute of the hour. Returned values can range from 0 to 59.
+     *
+     * @return the minute of the hour
+     */
+    public int getMinute() {
+        return value[2] & 0x3f;
+    }
+
+    /**
+     * Returns the hour of the day. Returned values can range from 0 to 23.
+     *
+     * @return the hour of the day
+     */
+    public int getHour() {
+        return value[3] & 0x1f;
+    }
+
+    /**
+     * Returns the day of the week. Returned values can range from 1 (Monday) to 7 (Sunday).
+     *
+     * @return the day of the week
+     */
+    public int getDayOfWeek() {
+        return (value[4] & 0xe0) >> 5;
+    }
+
+    /**
+     * Returns the day of the month. Returned values can range from 1 to 31.
+     *
+     * @return the day of the month
+     */
+    public int getDayOfMonth() {
+        return value[4] & 0x1f;
+    }
+
+    /**
+     * Returns the month of the year. Returned values can range from 1 (January) to 12 (December).
+     *
+     * @return the month of the year
+     */
+    public int getMonth() {
+        return value[5] & 0x0f;
+    }
+
+    /**
+     * Returns the year in the century. Returned values can range from 0 to 99. Note that the century is not stored by
+     * Time56.
+     *
+     * @return the number of years in the century
+     */
+    public int getYear() {
+        return value[6] & 0x7f;
+    }
+
+    /**
+     * Returns true if summer time (i.e. Daylight Saving Time (DST)) is active.
+     *
+     * @return true if summer time (i.e. Daylight Saving Time (DST)) is active
+     */
+    public boolean isSummerTime() {
+        return flagIsSet(3);
+    }
+
+    /**
+     * Return true if time value is invalid.
+     *
+     * @return true if time value is invalid
+     */
+    public boolean isInvalid() {
+        return flagIsSet(2);
+    }
+
+    private boolean flagIsSet(int arrIndex) {
+        return (value[arrIndex] & 0x80) == 0x80;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder("Time56: ");
+        builder.append(String.format("%02d", getDayOfMonth()));
+        builder.append("-");
+        builder.append(String.format("%02d", getMonth()));
+        builder.append("-");
+        builder.append(String.format("%02d", getYear()));
+        builder.append(" ");
+        builder.append(String.format("%02d", getHour()));
+        builder.append(":");
+        builder.append(String.format("%02d", getMinute()));
+        builder.append(":");
+        builder.append(String.format("%02d", getSecond()));
+        builder.append(":");
+        builder.append(String.format("%03d", getMillisecond()));
+
+        if (isSummerTime()) {
+            builder.append(" DST");
+        }
+
+        if (isInvalid()) {
+            builder.append(", invalid");
+        }
+
+        return builder.toString();
+    }
+}

+ 71 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/IeValueWithTransientState.java

@@ -0,0 +1,71 @@
+package com.gyee.edge.protocol.iec60870.infoelement;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.text.MessageFormat;
+
+/**
+ * Represents a value with transient state indication (VTI) information element.
+ */
+public class IeValueWithTransientState extends InformationElement {
+
+    private final int value;
+    private final boolean transientState;
+
+    /**
+     * Creates a VTI (value with transient state indication) information element.
+     *
+     * @param value          value between -64 and 63
+     * @param transientState true if in transient state
+     */
+    public IeValueWithTransientState(int value, boolean transientState) {
+
+        if (value < -64 || value > 63) {
+            throw new IllegalArgumentException("Value has to be in the range -64..63");
+        }
+
+        this.value = value;
+        this.transientState = transientState;
+
+    }
+
+    IeValueWithTransientState(DataInputStream is) throws IOException {
+        int b1 = (is.readByte() & 0xff);
+
+        transientState = ((b1 & 0x80) == 0x80);
+
+        if ((b1 & 0x40) == 0x40) {
+            value = b1 | 0xffffff80;
+        } else {
+            value = b1 & 0x3f;
+        }
+
+    }
+
+    @Override
+    int encode(byte[] buffer, int i) {
+
+        if (transientState) {
+            buffer[i] = (byte) (value | 0x80);
+        } else {
+            buffer[i] = (byte) (value & 0x7f);
+        }
+
+        return 1;
+
+    }
+
+    public int getValue() {
+        return value;
+    }
+
+    public boolean getTransientState() {
+        return transientState;
+    }
+
+    @Override
+    public String toString() {
+        return MessageFormat.format("Value with transient state, value: {0}, transient state: {1}.", getValue(),
+                getTransientState());
+    }
+}

+ 10 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/InformationElement.java

@@ -0,0 +1,10 @@
+
+package com.gyee.edge.protocol.iec60870.infoelement;
+
+public abstract class InformationElement {
+
+    abstract int encode(byte[] buffer, int i);
+
+    @Override
+    public abstract String toString();
+}

+ 472 - 0
protocol/iec60870/src/main/java/com/gyee/edge/protocol/iec60870/infoelement/InformationObject.java

@@ -0,0 +1,472 @@
+package com.gyee.edge.protocol.iec60870.infoelement;
+
+import com.gyee.edge.protocol.iec60870.ASduType;
+import com.gyee.edge.protocol.iec60870.ExtendedDataInputStream;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+
+/**
+ * Every Information Object contains:
+ * <ul>
+ * <li>The Information Object Address (IOA) that is 1, 2 or 3 bytes long.</li>
+ * <li>A set of Information Elements or a sequence of information element sets. The type of information elements in the
+ * set and their order depend on the ASDU's TypeId and is the same for all information objects within one ASDU. If the
+ * sequence bit is set in the ASDU then the ASDU contains a single Information Object containing a sequence of
+ * information element sets. If the sequence bit is not set the ASDU contains a sequence of information objects each
+ * containing only single information elements sets.</li>
+ * </ul>
+ */
+public class InformationObject {
+
+    private final int informationObjectAddress;
+    private final InformationElement[][] informationElements;
+
+    public InformationObject(int informationObjectAddress, InformationElement[][] informationElements) {
+        this.informationObjectAddress = informationObjectAddress;
+        this.informationElements = informationElements;
+    }
+
+    public InformationObject(int informationObjectAddress, InformationElement... informationElement) {
+        this(informationObjectAddress, new InformationElement[][]{informationElement});
+    }
+
+    public static InformationObject decode(ExtendedDataInputStream is, ASduType aSduType, int numberOfSequenceElements,
+                                                                 int ioaFieldLength) throws IOException {
+        InformationElement[][] informationElements;
+
+        int informationObjectAddress = readInformationObjectAddress(is, ioaFieldLength);
+
+        switch (aSduType) {
+            // 1
+            case M_SP_NA_1:
+                informationElements = new InformationElement[numberOfSequenceElements][1];
+                for (int i = 0; i < numberOfSequenceElements; i++) {
+                    informationElements[i][0] = new IeSinglePointWithQuality(is);
+                }
+                break;
+            // 2
+            case M_SP_TA_1:
+                informationElements = new InformationElement[][]{{new IeSinglePointWithQuality(is), new IeTime24(is)}};
+                break;
+            // 3
+            case M_DP_NA_1:
+                informationElements = new InformationElement[numberOfSequenceElements][1];
+                for (int i = 0; i < numberOfSequenceElements; i++) {
+                    informationElements[i][0] = new IeDoublePointWithQuality(is);
+                }
+                break;
+            // 4
+            case M_DP_TA_1:
+                informationElements = new InformationElement[][]{{new IeDoublePointWithQuality(is), new IeTime24(is)}};
+                break;
+            // 5
+            case M_ST_NA_1:
+                informationElements = new InformationElement[numberOfSequenceElements][2];
+                for (int i = 0; i < numberOfSequenceElements; i++) {
+                    informationElements[i][0] = new IeValueWithTransientState(is);
+                    informationElements[i][1] = new IeQuality(is);
+                }
+                break;
+            // 6
+            case M_ST_TA_1:
+                informationElements = new InformationElement[][]{
+                        {new IeValueWithTransientState(is), new IeQuality(is), new IeTime24(is)}};
+                break;
+            // 7
+            case M_BO_NA_1:
+                informationElements = new InformationElement[numberOfSequenceElements][2];
+                for (int i = 0; i < numberOfSequenceElements; i++) {
+                    informationElements[i][0] = new IeBinaryStateInformation(is);
+                    informationElements[i][1] = new IeQuality(is);
+                }
+                break;
+            // 8
+            case M_BO_TA_1:
+                informationElements = new InformationElement[][]{
+                        {new IeBinaryStateInformation(is), new IeQuality(is), new IeTime24(is)}};
+                break;
+            // 9
+            case M_ME_NA_1:
+                informationElements = new InformationElement[numberOfSequenceElements][2];
+                for (InformationElement[] informationElementCombination : informationElements) {
+                    informationElementCombination[0] = new IeNormalizedValue(is);
+                    informationElementCombination[1] = new IeQuality(is);
+                }
+                break;
+            // 10
+            case M_ME_TA_1:
+                informationElements = new InformationElement[][]{
+                        {new IeNormalizedValue(is), new IeQuality(is), new IeTime24(is)}};
+                break;
+            // 11
+            case M_ME_NB_1:
+                informationElements = new InformationElement[numberOfSequenceElements][2];
+                for (InformationElement[] informationElementCombination : informationElements) {
+                    informationElementCombination[0] = new IeScaledValue(is);
+                    informationElementCombination[1] = new IeQuality(is);
+                }
+                break;
+            // 12
+            case M_ME_TB_1:
+                informationElements = new InformationElement[][]{
+                        {new IeScaledValue(is), new IeQuality(is), new IeTime24(is)}};
+                break;
+            // 13
+            case M_ME_NC_1:
+                informationElements = new InformationElement[numberOfSequenceElements][2];
+                for (InformationElement[] informationElementCombination : informationElements) {
+                    informationElementCombination[0] = new IeShortFloat(is);
+                    informationElementCombination[1] = new IeQuality(is);
+                }
+                break;
+            // 14
+            case M_ME_TC_1:
+                informationElements = new InformationElement[][]{
+                        {new IeShortFloat(is), new IeQuality(is), new IeTime24(is)}};
+                break;
+            // 15
+            case M_IT_NA_1:
+                informationElements = new InformationElement[numberOfSequenceElements][1];
+                for (InformationElement[] informationElementCombination : informationElements) {
+                    informationElementCombination[0] = IeBinaryCounterReading.decode(is);
+                }
+                break;
+            // 16
+            case M_IT_TA_1:
+                informationElements = new InformationElement[][]{
+                        {IeBinaryCounterReading.decode(is), new IeTime24(is)}};
+                break;
+            // 17
+            case M_EP_TA_1:
+                informationElements = new InformationElement[][]{
+                        {new IeSingleProtectionEvent(is), new IeTime16(is), new IeTime24(is)}};
+                break;
+            // 18
+            case M_EP_TB_1:
+                informationElements = new InformationElement[][]{{new IeProtectionStartEvent(is),
+                        new IeProtectionQuality(is), new IeTime16(is), new IeTime24(is)}};
+                break;
+            // 19
+            case M_EP_TC_1:
+                informationElements = new InformationElement[][]{{new IeProtectionOutputCircuitInformation(is),
+                        new IeProtectionQuality(is), new IeTime16(is), new IeTime24(is)}};
+                break;
+            // 20
+            case M_PS_NA_1:
+                informationElements = new InformationElement[numberOfSequenceElements][2];
+                for (InformationElement[] informationElementCombination : informationElements) {
+                    informationElementCombination[0] = new IeStatusAndStatusChanges(is);
+                    informationElementCombination[1] = new IeQuality(is);
+                }
+                break;
+            // 21
+            case M_ME_ND_1:
+                informationElements = new InformationElement[numberOfSequenceElements][1];
+                for (InformationElement[] informationElementCombination : informationElements) {
+                    informationElementCombination[0] = new IeNormalizedValue(is);
+                }
+                break;
+            // 30
+            case M_SP_TB_1:
+                informationElements = new InformationElement[][]{
+                        {new IeSinglePointWithQuality(is), IeTime56.decode(is)}};
+                break;
+            // 31
+            case M_DP_TB_1:
+                informationElements = new InformationElement[][]{
+                        {new IeDoublePointWithQuality(is), IeTime56.decode(is)}};
+                break;
+            // 32
+            case M_ST_TB_1:
+                informationElements = new InformationElement[][]{
+                        {new IeValueWithTransientState(is), new IeQuality(is), IeTime56.decode(is)}};
+                break;
+            // 33
+            case M_BO_TB_1:
+                informationElements = new InformationElement[][]{
+                        {new IeBinaryStateInformation(is), new IeQuality(is), IeTime56.decode(is)}};
+                break;
+            // 34
+            case M_ME_TD_1:
+                informationElements = new InformationElement[][]{
+                        {new IeNormalizedValue(is), new IeQuality(is), IeTime56.decode(is)}};
+                break;
+            // 35
+            case M_ME_TE_1:
+                informationElements = new InformationElement[][]{
+                        {new IeScaledValue(is), new IeQuality(is), IeTime56.decode(is)}};
+                break;
+            // 36
+            case M_ME_TF_1:
+                informationElements = new InformationElement[][]{
+                        {new IeShortFloat(is), new IeQuality(is), IeTime56.decode(is)}};
+                break;
+            // 37
+            case M_IT_TB_1:
+                informationElements = new InformationElement[][]{
+                        {IeBinaryCounterReading.decode(is), IeTime56.decode(is)}};
+                break;
+            // 38
+            case M_EP_TD_1:
+                informationElements = new InformationElement[][]{
+                        {new IeSingleProtectionEvent(is), new IeTime16(is), IeTime56.decode(is)}};
+                break;
+            // 39
+            case M_EP_TE_1:
+                informationElements = new InformationElement[][]{{new IeProtectionStartEvent(is),
+                        new IeProtectionQuality(is), new IeTime16(is), IeTime56.decode(is)}};
+                break;
+            // 40
+            case M_EP_TF_1:
+                informationElements = new InformationElement[][]{{new IeProtectionOutputCircuitInformation(is),
+                        new IeProtectionQuality(is), new IeTime16(is), IeTime56.decode(is)}};
+                break;
+            // 45
+            case C_SC_NA_1:
+                informationElements = new InformationElement[][]{{new IeSingleCommand(is)}};
+                break;
+            // 46
+            case C_DC_NA_1:
+                informationElements = new InformationElement[][]{{new IeDoubleCommand(is)}};
+                break;
+            // 47
+            case C_RC_NA_1:
+                informationElements = new InformationElement[][]{{new IeRegulatingStepCommand(is)}};
+                break;
+            // 48
+            case C_SE_NA_1:
+                informationElements = new InformationElement[][]{
+                        {new IeNormalizedValue(is), new IeQualifierOfSetPointCommand(is)}};
+                break;
+            // 49
+            case C_SE_NB_1:
+                informationElements = new InformationElement[][]{
+                        {new IeScaledValue(is), new IeQualifierOfSetPointCommand(is)}};
+                break;
+            // 50
+            case C_SE_NC_1:
+                informationElements = new InformationElement[][]{
+                        {new IeShortFloat(is), new IeQualifierOfSetPointCommand(is)}};
+                break;
+            // 51
+            case C_BO_NA_1:
+                informationElements = new InformationElement[][]{{new IeBinaryStateInformation(is)}};
+                break;
+            // 58
+            case C_SC_TA_1:
+                informationElements = new InformationElement[][]{{new IeSingleCommand(is), IeTime56.decode(is)}};
+                break;
+            // 59
+            case C_DC_TA_1:
+                informationElements = new InformationElement[][]{{new IeDoubleCommand(is), IeTime56.decode(is)}};
+                break;
+            // 60
+            case C_RC_TA_1:
+                informationElements = new InformationElement[][]{
+                        {new IeRegulatingStepCommand(is), IeTime56.decode(is)}};
+                break;
+            // 61
+            case C_SE_TA_1:
+                informationElements = new InformationElement[][]{
+                        {new IeNormalizedValue(is), new IeQualifierOfSetPointCommand(is), IeTime56.decode(is)}};
+                break;
+            // 62
+            case C_SE_TB_1:
+                informationElements = new InformationElement[][]{
+                        {new IeScaledValue(is), new IeQualifierOfSetPointCommand(is), IeTime56.decode(is)}};
+                break;
+            // 63
+            case C_SE_TC_1:
+                informationElements = new InformationElement[][]{
+                        {new IeShortFloat(is), new IeQualifierOfSetPointCommand(is), IeTime56.decode(is)}};
+                break;
+            // 64
+            case C_BO_TA_1:
+                informationElements = new InformationElement[][]{
+                        {new IeBinaryStateInformation(is), IeTime56.decode(is)}};
+                break;
+            // 70
+            case M_EI_NA_1:
+                informationElements = new InformationElement[][]{{new IeCauseOfInitialization(is)}};
+                break;
+            // 100
+            case C_IC_NA_1:
+                informationElements = new InformationElement[][]{{new IeQualifierOfInterrogation(is)}};
+                break;
+            // 101
+            case C_CI_NA_1:
+                informationElements = new InformationElement[][]{{new IeQualifierOfCounterInterrogation(is)}};
+                break;
+            // 102
+            case C_RD_NA_1:
+                informationElements = new InformationElement[0][0];
+                break;
+            // 103
+            case C_CS_NA_1:
+                informationElements = new InformationElement[][]{{IeTime56.decode(is)}};
+                break;
+            // 104
+            case C_TS_NA_1:
+                informationElements = new InformationElement[][]{{new IeFixedTestBitPattern(is)}};
+                break;
+            // 105
+            case C_RP_NA_1:
+                informationElements = new InformationElement[][]{{new IeQualifierOfResetProcessCommand(is)}};
+                break;
+            // 106
+            case C_CD_NA_1:
+                informationElements = new InformationElement[][]{{new IeTime16(is)}};
+                break;
+            // 107
+            case C_TS_TA_1:
+                informationElements = new InformationElement[][]{
+                        {IeTestSequenceCounter.decode(is), IeTime56.decode(is)}};
+                break;
+            // 110
+            case P_ME_NA_1:
+                informationElements = new InformationElement[][]{
+                        {new IeNormalizedValue(is), new IeQualifierOfParameterOfMeasuredValues(is)}};
+                break;
+            // 111
+            case P_ME_NB_1:
+                informationElements = new InformationElement[][]{
+                        {new IeScaledValue(is), new IeQualifierOfParameterOfMeasuredValues(is)}};
+                break;
+            // 112
+            case P_ME_NC_1:
+                informationElements = new InformationElement[][]{
+                        {new IeShortFloat(is), new IeQualifierOfParameterOfMeasuredValues(is)}};
+                break;
+            // 113
+            case P_AC_NA_1:
+                informationElements = new InformationElement[][]{{IeQualifierOfParameterActivation.decode(is)}};
+                break;
+            // 120
+            case F_FR_NA_1:
+                informationElements = new InformationElement[][]{
+                        {IeNameOfFile.decode(is), IeLengthOfFileOrSection.decode(is), IeFileReadyQualifier.decode(is)}};
+                break;
+            // 121
+            case F_SR_NA_1:
+                informationElements = new InformationElement[][]{{IeNameOfFile.decode(is), IeNameOfSection.decode(is),
+                        IeLengthOfFileOrSection.decode(is), IeSectionReadyQualifier.decode(is)}};
+                break;
+            // 122
+            case F_SC_NA_1:
+                informationElements = new InformationElement[][]{
+                        {IeNameOfFile.decode(is), IeNameOfSection.decode(is), IeSelectAndCallQualifier.decode(is)}};
+                break;
+            // 123
+            case F_LS_NA_1:
+                return new InformationObject(informationObjectAddress, IeNameOfFile.decode(is), IeNameOfSection.decode(is),
+                        IeLastSectionOrSegmentQualifier.decode(is), IeChecksum.decode(is));
+            // 124
+            case F_AF_NA_1:
+                return new InformationObject(informationObjectAddress, IeNameOfFile.decode(is), IeNameOfSection.decode(is),
+                        IeAckFileOrSectionQualifier.decode(is));
+
+            // 125
+            case F_SG_NA_1:
+                return new InformationObject(informationObjectAddress, IeNameOfFile.decode(is), IeNameOfSection.decode(is),
+                        new IeFileSegment(is));
+
+            // 126
+            case F_DR_TA_1:
+                informationElements = new InformationElement[numberOfSequenceElements][];
+                for (int i = 0; i < numberOfSequenceElements; i++) {
+                    informationElements[i] = valuesAsArray(IeNameOfFile.decode(is), IeLengthOfFileOrSection.decode(is),
+                            IeStatusOfFile.decode(is), IeTime56.decode(is));
+                }
+                break;
+            // 127
+            case F_SC_NB_1:
+                return new InformationObject(informationObjectAddress, IeNameOfFile.decode(is), IeTime56.decode(is),
+                        IeTime56.decode(is));
+            default:
+                throw new IOException(
+                        "Unable to parse Information Object because of unknown Type Identification: " + aSduType);
+        }
+
+        return new InformationObject(informationObjectAddress, informationElements);
+
+    }
+
+    private static InformationElement[] valuesAsArray(InformationElement... informationElements) {
+        return informationElements;
+    }
+
+    private static int readInformationObjectAddress(DataInputStream is, int ioaFieldLength) throws IOException {
+        int informationObjectAddress = 0;
+        for (int i = 0; i < ioaFieldLength; i++) {
+            informationObjectAddress |= (is.readUnsignedByte() << (8 * i));
+        }
+        return informationObjectAddress;
+    }
+
+    public int encode(byte[] buffer, int i, int ioaFieldLength) {
+        int origi = i;
+
+        buffer[i++] = (byte) informationObjectAddress;
+        if (ioaFieldLength > 1) {
+            buffer[i++] = (byte) (informationObjectAddress >> 8);
+            if (ioaFieldLength > 2) {
+                buffer[i++] = (byte) (informationObjectAddress >> 16);
+            }
+        }
+
+        for (InformationElement[] informationElementCombination : informationElements) {
+            for (InformationElement informationElement : informationElementCombination) {
+                i += informationElement.encode(buffer, i);
+            }
+        }
+
+        return i - origi;
+    }
+
+    public int getInformationObjectAddress() {
+        return informationObjectAddress;
+    }
+
+    /**
+     * Returns the information elements as a two dimensional array. The first dimension of the array is the index of the
+     * sequence of information element sets. The second dimension is the index of the information element set. For
+     * example an information object containing a single set of three information elements will have the dimension
+     * [1][3]. Note that you will have to cast the returned <code>InformationElement</code>s to a concrete
+     * implementation in order to access the data inside them.
+     *
+     * @return the information elements as a two dimensional array.
+     */
+    public InformationElement[][] getInformationElements() {
+        return informationElements;
+    }
+
+    @Override
+    public String toString() {
+
+        StringBuilder builder = new StringBuilder("IOA: " + informationObjectAddress);
+
+        if (informationElements.length > 1) {
+            int i = 1;
+            for (InformationElement[] informationElementSet : informationElements) {
+                builder.append("\nInformation Element Set " + i + ":");
+                for (InformationElement informationElement : informationElementSet) {
+                    builder.append("\n");
+                    builder.append(informationElement.toString());
+                }
+                i++;
+            }
+        } else {
+            for (InformationElement[] informationElementSet : informationElements) {
+                for (InformationElement informationElement : informationElementSet) {
+                    builder.append("\n");
+                    builder.append(informationElement.toString());
+                }
+            }
+        }
+
+        return builder.toString();
+
+    }
+
+}

+ 1 - 0
settings.gradle

@@ -1,5 +1,6 @@
 rootProject.name = "edge"
 include "common:utils"
+include "protocol:iec60870"
 include "gateway"
 include "bridge"
 include "loader"