public enum BlockType {

    /** Organization Block */
    OB(0x38),

    /** User Data Block */
    DB(0x41),

    /** System Data Block */
    SDB(0x42),

    /** User Function */
    FC(0x43),

    /** System Function */
    SFC(0x44),

    /** User Function Block */
    FB(0x45),

    /** System Function Block */
    SFB(0x46);

    private final byte value;

    BlockType(int value) {
        this.value = (byte) value;
    }

    public byte getValue() {
        return value;
    }
} public enum PlcCpuStatus {

    UNKNOWN(0x00), STOP(0x04), RUN(0x08);

    private final byte value;

    PlcCpuStatus(int value) {
        this.value = (byte) value;
    }

    public byte getValue() {
        return value;
    }

    public static PlcCpuStatus valueOf(byte b) {
        // Since RUN status is always 0x08 for all CPUs and CPs, STOP status
        // sometime can be coded as 0x03 (especially for old cpu...)
        for (PlcCpuStatus s : values()) {
            if (s.value == b) {
                return s;
            }
        }
        return RUN;
    }
} public class S7BlockInfo {

    public BlockSubType blkType;
    public BlockLang blkLang;
    public String author, family, header;
    public int blkFlags, blkNumber, checksum, loadSize, localData, mc7Size, sbbLength, version;
    public LocalDate codeDate, intfDate;

    public static S7BlockInfo of(byte[] src, int pos) {
        S7BlockInfo info = new S7BlockInfo();
        info.decode(src, pos);
        return info;
    }

    protected void decode(byte[] buffer, int pos) {
        blkFlags = S7.getUnsignedIntAt(buffer, pos);
        blkLang = BlockLang.of(S7.getByteAt(buffer, pos + 1));
        blkType = BlockSubType.of(S7.getByteAt(buffer, pos + 2));
        blkNumber = S7.getWordAt(buffer, pos + 3);
        loadSize = S7.getDIntAt(buffer, pos + 5);
        codeDate = LocalDate.of(1984, 1, 1).plusDays(S7.getWordAt(buffer, pos + 17));
        intfDate = LocalDate.of(1984, 1, 1).plusDays(S7.getWordAt(buffer, pos + 23));
        sbbLength = S7.getWordAt(buffer, pos + 25);
        localData = S7.getWordAt(buffer, pos + 29);
        mc7Size = S7.getWordAt(buffer, pos + 31);
        author = S7.getStringAt(buffer, pos + 33, 8).trim();
        family = S7.getStringAt(buffer, pos + 41, 8).trim();
        header = S7.getStringAt(buffer, pos + 49, 8).trim();
        version = S7.getUnsignedIntAt(buffer, pos + 57);
        checksum = S7.getWordAt(buffer, pos + 59);
    }

    public String getBlockFlagString() {
        return String.format("%08d", Integer.valueOf(Integer.toString(blkFlags, 2)));
    }

    public String getVersionString() {
        byte low = (byte) ((version >> 4) & 0xFF);
        byte high = (byte) ((version >> 8) & 0xFF);
        return String.format("%d.%d", low, high);
    }

    @Override
    public String toString() {
        return "S7BlockInfo [author=" + author + ", family=" + family + ", header=" + header + ", blkFlags=" + blkFlags + ", blkLang=" + blkLang
                + ", blkNumber=" + blkNumber + ", blkType=" + blkType + ", checksum=" + checksum + ", loadSize=" + loadSize + ", localData=" + localData
                + ", mc7Size=" + mc7Size + ", sbbLength=" + sbbLength + ", version=" + version + ", codeDate=" + codeDate + ", intfDate=" + intfDate + "]";
    }

} public class S7BlockList {

    private final Map<BlockSubType, Integer> map = new EnumMap<>(BlockSubType.class);

    public static S7BlockList of(byte[] src, int pos) {
        S7BlockList list = new S7BlockList();
        list.decode(src, pos);
        return list;
    }

    protected void decode(byte[] src, int pos) {
        for (int i = 0; i < 28; i += 4) {
            add(S7.getStringAt(src, pos + i, 2), S7.getWordAt(src, pos + i + 2));
        }
    }

    private void add(String blk, int count) {
        BlockSubType type = BlockSubType.of((byte) Integer.parseInt(blk, 16));
        map.put(type, count);
    }

    public int getSize(BlockSubType type) {
        return map.getOrDefault(type, 0);
    }

    @Override
    public String toString() {
        return "S7BlockList [map=" + map + "]";
    }

} public class S7CpInfo {

    public int maxBusRate;
    public int maxConnections;
    public int maxMpiRate;
    public int maxPduLength;

    public static S7CpInfo of(byte[] src, int pos) {
        S7CpInfo info = new S7CpInfo();
        info.decode(src, pos);
        return info;
    }

    protected void decode(byte[] src, int pos) {
        maxPduLength = S7.getShortAt(src, 2);
        maxConnections = S7.getShortAt(src, 4);
        maxMpiRate = S7.getDIntAt(src, 6);
        maxBusRate = S7.getDIntAt(src, 10);
    }

    @Override
    public String toString() {
        return "S7CpInfo [maxBusRate=" + maxBusRate + ", maxConnections=" + maxConnections + ", maxMpiRate=" + maxMpiRate + ", maxPduLength=" + maxPduLength
                + "]";
    }
} public class S7CpuInfo {

    public String asName;
    public String copyright;
    public String moduleName;
    public String moduleTypeName;
    public String serialNumber;

    public static S7CpuInfo of(byte[] src, int pos) {
        S7CpuInfo info = new S7CpuInfo();
        info.decode(src, pos);
        return info;
    }

    protected void decode(byte[] src, int pos) {
        asName = S7.getStringAt(src, pos + 2, 24);
        copyright = S7.getStringAt(src, pos + 104, 26);
        moduleName = S7.getStringAt(src, pos + 36, 24);
        moduleTypeName = S7.getStringAt(src, pos + 172, 32);
        serialNumber = S7.getStringAt(src, pos + 138, 24);
    }

    @Override
    public String toString() {
        return "S7CpuInfo [asName=" + asName + ", copyright=" + copyright + ", moduleName=" + moduleName + ", moduleTypeName=" + moduleTypeName
                + ", serialNumber=" + serialNumber + "]";
    }
} public class S7DataItem extends DataItem{

    public int result;
    public int amount;
    public byte[] data;

    public S7DataItem(AreaType area, DataType type, int db, int start, int amount) {
        this(area, type, db, start, amount, null);
    }

    public S7DataItem(AreaType area, DataType type, int db, int start, int amount, byte[] data) {
        super(area, type, db, start);
        int wordSize = DataType.getByteLength(Objects.requireNonNull(type));
        switch (Objects.requireNonNull(area)) {
            case CT_INPUTS:
            case CT_OUTPUTS:
                this.type = DataType.COUNTER;
                break;
            case TM:
                this.type = DataType.TIMER;
                break;
            default:
                this.type = type;
                break;
        }

        switch (this.type) {
            case BIT:
                // Only 1 bit can be transferred at time
                this.amount = 1;
                this.start = start;
            case COUNTER:
            case TIMER:
                this.amount = amount;
                this.start = start;
                break;
            default:
                // convert to Bit request
                this.amount = amount * wordSize;
                this.start = start * 8;
                break;
        }

        this.area = area;
        this.db = db;
        this.result = ReturnCode.ERR_ITEM_NOT_AVAILABLE;
        this.data = data != null ? data : new byte[this.amount];
    }

    @Override
    public String toString() {
        return "S7DataItem [" + (area != null ? "area=" + area + ", " : "") + (type != null ? "type=" + type + ", " : "") + "result=" + result + ", db=" + db
                + ", start=" + start + ", amount=" + amount + ", " + (data != null ? "data.length=" + data.length : "") + "]";
    }

} public class S7OrderCode {

    public String code;
    public int v1;
    public int v2;
    public int v3;

    public static S7OrderCode of(byte[] src, int pos, int size) {
        S7OrderCode oc = new S7OrderCode();
        oc.decode(src, pos, size);
        return oc;
    }

    public String getCode() {
        return code;
    }

    public String getFirmware() {
        return String.format("%s.%s.%s", v1, v2, v3);
    }

    protected void decode(byte[] src, int pos, int size) {
        code = S7.getStringAt(src, pos + 2, 20);
        v1 = src[pos + size - 3] & 0xFF;
        v2 = src[pos + size - 2] & 0xFF;
        v3 = src[pos + size - 1] & 0xFF;
    }

    @Override
    public String toString() {
        return "S7OrderCode [code=" + code + ", v1=" + v1 + ", v2=" + v2 + ", v3=" + v3 + "]";
    }
} public class S7Protection {

    /**
     * Startup switch setting (1:CRST, 2:WRST, 0:undefined, does not exist of cannot be determined)
     */
    public int anl_sch;

    /**
     * Mode selector setting (1:RUN, 2:RUN-P, 3:STOP, 4:MRES, 0:undefined or cannot be determined)
     */
    public int bart_sch;

    /**
     * Protection level set in parameters (0, 1, 2, 3; 0: no password, protection level invalid)
     */
    public int sch_par;

    /** Valid protection level of the CPU */
    public int sch_rel;

    /** Protection level set with the mode selector (1, 2, 3) */
    public int sch_schal;

    public static S7Protection of(byte[] src) {
        S7Protection pro = new S7Protection();
        pro.decode(src);
        return pro;
    }

    protected void decode(byte[] src) {
        sch_schal = S7.getWordAt(src, 2);
        sch_par = S7.getWordAt(src, 4);
        sch_rel = S7.getWordAt(src, 6);
        bart_sch = S7.getWordAt(src, 8);
        anl_sch = S7.getWordAt(src, 10);
    }

    @Override
    public String toString() {
        return "S7Protection [anl_sch=" + anl_sch + ", bart_sch=" + bart_sch + ", sch_par=" + sch_par + ", sch_rel=" + sch_rel + ", sch_schal=" + sch_schal
                + "]";
    }
} public class S7Szl {

    public byte[] data;
    public int dataSize;
    public int lenthdr;
    public int n_dr;

    public S7Szl(int bufferSize) {
        data = new byte[bufferSize];
    }

    public void copy(byte[] src, int srcPos, int destPos, int size) {
        System.arraycopy(src, srcPos, data, destPos, size);
    }
} public class S7Exception extends Exception {

    private static final long serialVersionUID = -3135373121524472268L;

    private final int errorCode;

    public S7Exception(int error, String message) {
        super(message);
        errorCode = error;
    }

    public S7Exception(int error, String message, Throwable th) {
        super(message, th);
        errorCode = error;
    }

    public int getErrorCode() {
        return errorCode;
    }
} public enum AreaType {
    /** System Information (caller) */
    SYS_INFO(0x03),

    /** S7 counters */
    SYSTEM_FLAGS(0x05),

    /** Analog Inputs - System info (200 family) */
    ANALOG_INPUTS(0x06),

    /** Analog Outputs - System flags (200 family) */
    ANALOG_OUTPUTS(0x07),

    /** Analog Counter Input area (200 family) */
    CT_INPUTS(0x1C),

    /** S7 Timer area */
    TM(0x1D),

    /** Analog Counter Output area (200 family) */
    CT_OUTPUTS(0x1E),

    /** IEC counters (200 family) */
    TM_IEC(0x1F),

    /** PE (inputs) instance area */
    PE(0x81),

    /** PA (outputs) instance area */
    PA(0x82),

    /** Marker (flags) area */
    MK(0x83),

    /** DataBlock (Peripheral I/O) area */
    DB(0x84),

    /** DataBlock area */
    DI(0x85),

    /** DataBlock (local) area */
    DB_LOCAL(0x86),

    /** IEC timers (200 family) */
    TM_V(0x87);

    private final byte value;

    AreaType(int value) {
        this.value = (byte) value;
    }

    public byte getValue() {
        return value;
    }

    public final static List<AreaType> SUPPORTED = Arrays.asList(MK, DB, TM);

} public enum DataType {

    /** Bit, Boolean */
    BIT(0x01),

    /** Unsigned Byte */
    BYTE(0x02),

    /** Unsigned Byte,Char */
    CHAR(0x03),

    /** Unsigned Word, Short, BCD */
    WORD(0x04),

    /** Signed Word, Short, BCD */
    INT(0x05),

    /** Unsigned Double Word, Long, LBCD, Float */
    DWORD(0x06),

    /** Signed Double Word, Long, LBCD, Float */
    DINT(0x07),

    /** Float */
    REAL(0x08),

    /** Counter type */
    COUNTER(0x1C),

    /** Timer type */
    TIMER(0x1D);

    private final byte value;

    DataType(int value) {
        this.value = (byte) value;
    }

    public byte getValue() {
        return value;
    }

    /**
     * Type to byte length
     *
     * @param type DataType
     * @return encoded byte[] length
     */
    public static int getByteLength(DataType type) {
        switch (type) {
            case BIT:// S7 sends 1 byte per bit
            case BYTE:
            case CHAR:
                return 1;
            case WORD:
            case INT:
            case COUNTER:
            case TIMER:
                return 2;
            case DWORD:
            case DINT:
            case REAL:
                return 4;
            default:
                return 0;
        }
    }
} public interface ReturnCode {

    static final ResourceBundle bundle = ResourceBundle.getBundle("mokka7");
    static final String BUNDLE_ERR_PREFIX = "err_";

    public static final int SUCCEED = 0x00;
    public static final int TCP_CONNECTION_FAILED = 0x0001;
    public static final int TCP_DATA_SEND = 0x0002;
    public static final int TCP_DATA_RECV = 0x0003;
    public static final int TCP_DATA_RECV_TOUT = 0x0004;
    public static final int TCP_CONNECTION_RESET = 0x0005;
    public static final int ISO_INVALID_PDU = 0x0006;
    public static final int ISO_CONNECTION_FAILED = 0x0007;
    public static final int ISO_NEGOTIATING_PDU = 0x0008;
    public static final int S7_INVALID_PDU = 0x0009;
    public static final int S7_DATA_READ = 0x000A;
    public static final int S7_DATA_WRITE = 0x000B;
    public static final int S7_BUFFER_TOO_SMALL = 0x000C;
    public static final int S7_FUNCTION_ERROR = 0x000D;
    public static final int S7_INVALID_PARAMS = 0x000E;

    public static final int CODE7_OK = 0x0000;
    public static final int CODE7_ADDRESS_OUT_OF_RANGE = 0x0005;
    public static final int CODE7_INVALID_TRANSPORT_SIZE = 0x0006;
    public static final int CODE7_WRITE_DATA_SIZE_MISMATCH = 0x0007;
    public static final int CODE7_RES_ITEM_NOT_AVAILABLE = 0x000A;
    public static final int CODE7_RES_ITEM_NOT_AVAILABLE1 = 0xD209;
    public static final int CODE7_INVALID_VALUE = 0xDC01;
    public static final int CODE7_NEED_PASSWORD = 0xD241;
    public static final int CODE7_INVALID_PASSWORD = 0xD602;
    public static final int CODE7_NO_PASSWORD_TO_CLEAR = 0xD604;
    public static final int CODE7_NO_PASSWORD_TO_SET = 0xD605;
    public static final int CODE7_FUN_NOT_AVAILABLE = 0x8104;
    public static final int CODE7_DATA_OVER_PDU = 0x8500;

    public static final int ERR_TCP_UNREACHABLE_HOST = 0x00002751;

    public static final int ERR_ISO_CONNECT = 0x00010000;
    public static final int ERR_ISO_INVALID_PDU = 0x00030000;
    public static final int ERR_ISO_INVALID_DATA_SIZE = 0x00040000;

    public static final int ERR_NEGOTIATING_PDU = 0x00100000;
    public static final int ERR_INVALID_PARAMS = 0x00200000;
    public static final int ERR_JOB_PENDING = 0x00300000;
    public static final int ERR_TOO_MANY_ITEMS = 0x00400000;
    public static final int ERR_INVALID_WORD_LEN = 0x00500000;
    public static final int ERR_PARTIAL_DATA_WRITTEN = 0x00600000;
    public static final int ERR_SIZE_OVER_PDU = 0x00700000;
    public static final int ERR_INVALID_PLC_ANSWER = 0x00800000;
    public static final int ERR_ADDRESS_OUT_OF_RANGE = 0x00900000;
    public static final int ERR_INVALID_TRANSPORT_SIZE = 0x00A00000;
    public static final int ERR_WRITE_DATA_SIZE_MISMATCH = 0x00B00000;
    public static final int ERR_ITEM_NOT_AVAILABLE = 0x00C00000;
    public static final int ERR_INVALID_VALUE = 0x00D00000;
    public static final int ERR_CANNOT_STARTPLC = 0x00E00000;
    public static final int ERR_ALREADY_RUN = 0x00F00000;
    public static final int ERR_CANNOT_STOPPLC = 0x01000000;
    public static final int ERR_CANNOT_COPY_RAM_TO_ROM = 0x01100000;
    public static final int ERR_CANNOT_COMPRESS = 0x01200000;
    public static final int ERR_ALREADY_STOP = 0x01300000;
    public static final int ERR_FUN_NOT_AVAILABLE = 0x01400000;
    public static final int ERR_UPLOAD_SEQUENCE_FAILED = 0x01500000;
    public static final int ERR_INVALID_DATA_SIZE_RECVD = 0x01600000;
    public static final int ERR_INVALID_BLOCK_TYPE = 0x01700000;
    public static final int ERR_INVALID_BLOCK_NUMBER = 0x01800000;
    public static final int ERR_INVALID_BLOCK_SIZE = 0x01900000;
    public static final int ERR_NEED_PASSWORD = 0x01D00000;
    public static final int ERR_INVALID_PASSWORD = 0x01E00000;
    public static final int ERR_NO_PASSWORD_TO_SET_OR_CLEAR = 0x01F00000;
    public static final int ERR_JOB_TIMEOUT = 0x02000000;
    public static final int ERR_PARTIAL_DATA_READ = 0x02100000;
    public static final int ERR_BUFFER_TOO_SMALL = 0x02200000;
    public static final int ERR_FUNCTION_REFUSED = 0x02300000;
    public static final int ERR_DESTROYING = 0x02400000;
    public static final int ERR_INVALID_PARAM_NUMBER = 0x02500000;
    public static final int ERR_CANNOT_CHANGE_PARAM = 0x02600000;
    public static final int ERR_FUNCTION_NOT_IMPLEMENTED = 0x02700000;

    public static String getErrorText(int errorCode) {
        try {
            return bundle.getString(BUNDLE_ERR_PREFIX + Integer.toHexString(errorCode));
        } catch (Exception e) {
            return String.format("error code: [0x%s]", Integer.toHexString(errorCode));
        }
    }

    public static int getCpuError(int error) {
        switch (error) {
            case CODE7_OK:
                return 0;
            case CODE7_ADDRESS_OUT_OF_RANGE:
                return ERR_ADDRESS_OUT_OF_RANGE;
            case CODE7_INVALID_TRANSPORT_SIZE:
                return ERR_INVALID_TRANSPORT_SIZE;
            case CODE7_WRITE_DATA_SIZE_MISMATCH:
                return ERR_WRITE_DATA_SIZE_MISMATCH;
            case CODE7_RES_ITEM_NOT_AVAILABLE:
            case CODE7_RES_ITEM_NOT_AVAILABLE1:
                return ERR_ITEM_NOT_AVAILABLE;
            case CODE7_DATA_OVER_PDU:
                return ERR_SIZE_OVER_PDU;
            case CODE7_INVALID_VALUE:
                return ERR_INVALID_VALUE;
            case CODE7_FUN_NOT_AVAILABLE:
                return ERR_FUN_NOT_AVAILABLE;
            case CODE7_NEED_PASSWORD:
                return ERR_NEED_PASSWORD;
            case CODE7_INVALID_PASSWORD:
                return ERR_INVALID_PASSWORD;
            case CODE7_NO_PASSWORD_TO_SET:
            case CODE7_NO_PASSWORD_TO_CLEAR:
                return ERR_NO_PASSWORD_TO_SET_OR_CLEAR;
            default:
                return ERR_FUNCTION_REFUSED;
        }
    }
} public class S7 {

    private static final byte[] BIT_MASK = { (byte) 0x01, (byte) 0x02, (byte) 0x04, (byte) 0x08, (byte) 0x10, (byte) 0x20, (byte) 0x40, (byte) 0x80 };

    private final static char EMP_CHAR = '\u0020', DOT_CHAR = '.';
    private final static char HEX_DIGIT[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };

    public static int bcdToByte(byte b) {
        return ((b >> 4) * 10) + (b & 0x0F);
    }

    public static byte byteToBCD(int value) {
        return (byte) (((value / 10) << 4) | (value % 10));
    }


    public static boolean getBitAt(byte b, int bitpos) {
        int value = b & 0x0FF;
        return (value & BIT_MASK[bitpos]) != 0;
    }

    public static boolean getBitAt(byte[] buffer, int pos, int bitpos) {
        int value = buffer[pos] & 0x0FF;
        return (value & BIT_MASK[bitpos]) != 0;
    }

    public static byte[] getBytesAt(byte[] buffer, int pos, int maxLen) {
        return Arrays.copyOfRange(buffer, pos, pos + maxLen);
    }

    public static Date getDateAt(byte[] buffer, int pos) {
        int year, month, day, hour, min, sec;
        Calendar cal = Calendar.getInstance();

        year = bcdToByte(buffer[pos]);
        if (year < 90) {
            year += 2000;
        } else {
            year += 1900;
        }

        month = bcdToByte(buffer[pos + 1]) - 1;
        day = bcdToByte(buffer[pos + 2]);
        hour = bcdToByte(buffer[pos + 3]);
        min = bcdToByte(buffer[pos + 4]);
        sec = bcdToByte(buffer[pos + 5]);

        cal.set(year, month, day, hour, min, sec);
        cal.set(Calendar.MILLISECOND, 0);
        return cal.getTime();
    }

    public static LocalDateTime getDateTimeAt(byte[] buffer, int pos) {
        int year, month, day, hour, min, sec;
        year = bcdToByte(buffer[pos]);
        year += year < 90 ? 2000 : 1900;
        month = bcdToByte(buffer[pos + 1]);
        day = bcdToByte(buffer[pos + 2]);
        hour = bcdToByte(buffer[pos + 3]);
        min = bcdToByte(buffer[pos + 4]);
        sec = bcdToByte(buffer[pos + 5]);

        // First two digits of miliseconds
        // int msecH = bcdToByte(buffer[pos + 6]) * 10;
        // Last digit of miliseconds
        // int msecL = bcdToByte(buffer[pos + 7]) / 10;

        return LocalDateTime.of(year, month, day, hour, min, sec);
    }

    // Returns a 32 bit signed value : from 0 to 4294967295 (2^32-1)
    public static int getDIntAt(byte[] buffer, int pos) {
        return ((buffer[pos]) << 24 | (buffer[pos + 1] & 0xFF) << 16 | (buffer[pos + 2] & 0xFF) << 8 | (buffer[pos + 3] & 0xFF));
    }

    // Returns a 32 bit unsigned value : from 0 to 4294967295 (2^32-1)
    public static long getDWordAt(byte[] buffer, int pos) {
        return ((buffer[pos]) << 24 | (buffer[pos + 1] & 0xFF) << 16 | (buffer[pos + 2] & 0xFF) << 8 | (buffer[pos + 3] & 0xFF));
    }

    // Returns a 32 bit floating point
    public static float getFloatAt(byte[] buffer, int pos) {
        int IntFloat = getDIntAt(buffer, pos);
        return Float.intBitsToFloat(IntFloat);
    }

    public static String getPrintableStringAt(byte[] buffer, int pos, int maxLen) {
        StringBuilder sb = new StringBuilder();
        for (int i = pos; i < pos + maxLen; i++) {
            char ch = (char) (buffer[i] & 0xFF);
            if (Character.isISOControl(ch) || Character.isWhitespace(ch)) {
                sb.append(DOT_CHAR);
                continue;
            }
            sb.append(ch);
        }
        return sb.toString();
    }

    // Returns a 16 bit signed value : from -32768 to 32767
    public static int getShortAt(byte[] buffer, int pos) {
        int hi = (buffer[pos]);
        int lo = (buffer[pos + 1] & 0x00FF);
        return ((hi << 8) + lo);
    }

    public static String getStringAt(byte[] buffer, int pos, int maxLen) {
        return getStringAt(buffer, pos, maxLen, StandardCharsets.UTF_8);
    }

    public static String getStringAt(byte[] buffer, int pos, int maxLen, Charset charset) {
        return new String(buffer, pos, maxLen, charset);
    }

    public static String getS7StringAt(byte[] buffer, int pos) {
        int length = buffer[pos + 1];
        return new String(buffer, pos + 2, length, StandardCharsets.UTF_8);
    }

    public static byte getByteAt(byte[] buffer, int pos) {
        return buffer[pos];
    }

    public static int getUnsignedIntAt(byte[] buffer, int pos) {
        return Byte.toUnsignedInt(buffer[pos]);
    }

    /**
     * Returns a 16 bit unsigned value : from 0 to 65535 (2^16-1)
     *
     * @param buffer
     * @param pos start position
     * @return
     */
    public static int getWordAt(byte[] buffer, int pos) {
        int hi = (buffer[pos] & 0x00FF);
        int lo = (buffer[pos + 1] & 0x00FF);
        return (hi << 8) + lo;
    }

    public static S7Timer getS7TimerAt(byte[] buffer, int pos) {
        return S7Timer.of(buffer, pos);
    }

    public static void setBitAt(byte[] buffer, int pos, int bitPos, boolean value) {
        if (bitPos < 0) {
            bitPos = 0;
        } else if (bitPos > 7) {
            bitPos = 7;
        }
        if (value) {
            buffer[pos] = (byte) (buffer[pos] | BIT_MASK[bitPos]);
        } else {
            buffer[pos] = (byte) (buffer[pos] & ~BIT_MASK[bitPos]);
        }
    }

    public static void setDateAt(byte[] buffer, int pos, Date date) {
        int year, month, day, hour, min, sec;
        Calendar cal = Calendar.getInstance();
        cal.setTime(date);

        year = cal.get(Calendar.YEAR);
        month = cal.get(Calendar.MONTH) + 1;
        day = cal.get(Calendar.DAY_OF_MONTH);
        hour = cal.get(Calendar.HOUR_OF_DAY);
        min = cal.get(Calendar.MINUTE);
        sec = cal.get(Calendar.SECOND);
        // milli = cal.get(Calendar.MILLISECOND);
        // // First two digits of miliseconds
        // int msecH = milli / 10;
        // // Last digit of miliseconds
        // int msecL = milli % 10;

        if (year > 1999) {
            year -= 2000;
        }
        buffer[pos] = byteToBCD(year);
        buffer[pos + 1] = byteToBCD(month);
        buffer[pos + 2] = byteToBCD(day);
        buffer[pos + 3] = byteToBCD(hour);
        buffer[pos + 4] = byteToBCD(min);
        buffer[pos + 5] = byteToBCD(sec);
        buffer[pos + 6] = byteToBCD(0);
        buffer[pos + 7] = byteToBCD(0);

    }

    public static void setDateTimeAt(byte[] buffer, int pos, LocalDateTime dateTime) {
        int year, month, day, hour, min, sec;
        year = dateTime.get(ChronoField.YEAR);
        month = dateTime.get(ChronoField.MONTH_OF_YEAR);
        day = dateTime.get(ChronoField.DAY_OF_MONTH);
        hour = dateTime.get(ChronoField.HOUR_OF_DAY);
        min = dateTime.get(ChronoField.MINUTE_OF_HOUR);
        sec = dateTime.get(ChronoField.SECOND_OF_MINUTE);
        // milli = dateTime.get(ChronoField.MILLI_OF_SECOND);
        // // First two digits of miliseconds
        // int msecH = milli / 10;
        // // Last digit of miliseconds
        // int msecL = milli % 10;

        if (year > 1999) {
            year -= 2000;
        }
        buffer[pos] = byteToBCD(year);
        buffer[pos + 1] = byteToBCD(month);
        buffer[pos + 2] = byteToBCD(day);
        buffer[pos + 3] = byteToBCD(hour);
        buffer[pos + 4] = byteToBCD(min);
        buffer[pos + 5] = byteToBCD(sec);
        buffer[pos + 6] = byteToBCD(0);
        buffer[pos + 7] = byteToBCD(0);
    }

    public static void setDIntAt(byte[] buffer, int pos, int value) {
        buffer[pos + 3] = (byte) (value & 0xFF);
        buffer[pos + 2] = (byte) ((value >> 8) & 0xFF);
        buffer[pos + 1] = (byte) ((value >> 16) & 0xFF);
        buffer[pos] = (byte) ((value >> 24) & 0xFF);
    }

    public static void setDWordAt(byte[] buffer, int pos, long value) {
        buffer[pos + 3] = (byte) (value & 0xFF);
        buffer[pos + 2] = (byte) ((value >> 8) & 0xFF);
        buffer[pos + 1] = (byte) ((value >> 16) & 0xFF);
        buffer[pos] = (byte) ((value >> 24) & 0xFF);
    }

    public static void setFloatAt(byte[] buffer, int pos, float value) {
        int DInt = Float.floatToIntBits(value);
        setDIntAt(buffer, pos, DInt);
    }

    public static void setShortAt(byte[] buffer, int pos, short value) {
        buffer[pos] = (byte) (value >> 8);
        buffer[pos + 1] = (byte) (value & 0x00FF);
    }

    public static void setWordAt(byte[] buffer, int pos, int value) {
        setShortAt(buffer, pos, (short) value);
    }

    public static void setSwapWordAt(byte[] buffer, int pos, int value) {
        buffer[pos + 1] = (byte) (value >> 8);
        buffer[pos] = (byte) (value & 0x00FF);
    }

    public static void setByteAt(byte[] buffer, int pos, byte b) {
        buffer[pos] = b;
    }

    public static void setBytesAt(byte[] buffer, int pos, byte[] bytes) {
        System.arraycopy(bytes, 0, buffer, pos, bytes.length);
    }

    public static void setStringAt(byte[] buffer, int pos, String value) {
        setStringAt(buffer, pos, value, StandardCharsets.UTF_8);
    }

    public static void setStringAt(byte[] buffer, int pos, String value, Charset charset) {
        setBytesAt(buffer, pos, value.getBytes(charset));
    }

    public static void setS7StringAt(byte[] buffer, int pos, int maxLen, String value) {
        byte[] data = value.getBytes(StandardCharsets.UTF_8);
        buffer[pos] = (byte) (maxLen & 0xFF);
        buffer[pos + 1] = (byte) (data.length & 0xFF);
        System.arraycopy(data, 0, buffer, pos + 2, Math.min(data.length, maxLen));
    }

    public static void hexDump(byte[] buffer, Consumer<String> c) {
        hexDump(buffer, 128, c);
    }

    public static void hexDump(byte[] buffer, int maxlines, Consumer<String> c) {
        hexDump(buffer, maxlines, 0, c);
    }

    public static void hexDump(byte[] buffer, int maxlines, int offset, Consumer<String> c) {
        if (buffer == null || buffer.length == 0 || maxlines < 1 || offset < 0) {
            return;
        }
        int length, pos;
        int line = 0;
        StringBuilder sb = new StringBuilder();
        while (line < maxlines && (pos = (line * 16) + offset) < buffer.length) {
            length = Math.min(16, buffer.length - pos);
            if (length < 1) {
                return;
            }
            sb.setLength(0);
            for (int i = 12; i >= 0; i -= 4) {
                sb.append(HEX_DIGIT[0x0F & line >>> i]);
            }
            sb.append('0').append(':').append(EMP_CHAR).append(EMP_CHAR);
            line++;
            for (int i = 0; i < 16; i++) {
                if (i < length) {
                    sb.append(HEX_DIGIT[0x0F & buffer[pos + i] >> 4]);
                    sb.append(HEX_DIGIT[0x0F & buffer[pos + i]]);
                } else {
                    sb.append(EMP_CHAR).append(EMP_CHAR);
                }
                sb.append(EMP_CHAR);
                if (i == 7) {
                    sb.append(EMP_CHAR);
                }
            }
            sb.append(EMP_CHAR);
            for (int i = 0; i < 16; i++) {
                if (i >= length) {
                    sb.append(EMP_CHAR);
                    continue;
                }
                char ch = (char) (buffer[pos + i] & 0xFF);
                if (Character.isISOControl(ch) || Character.isWhitespace(ch)) {
                    sb.append(DOT_CHAR);
                    continue;
                }
                sb.append(ch);
            }
            c.accept(sb.toString());
        }
    }

    public static DataItem buildDataItem(String address) {
        try {
            DataItem item;
            int i0 = address.indexOf('.');
            switch (address.charAt(0)) {
                case 'D':
                    if (i0 < 1){
                        throw new IllegalArgumentException(String.format("invalid DB address: %s", address));
                    }

                    int i1 = address.indexOf('.', i0 + 1);
                    int db = Integer.parseUnsignedInt(address.substring(2, i0));
                    DataType type;
                    switch (address.charAt(i0 + 3)) {
                        case 'X':
                            type = DataType.BIT;
                            break;
                        case 'B':
                            type = DataType.BYTE;
                            break;
                        case 'W':
                            type = DataType.WORD;
                            break;
                        case 'D':
                            type = DataType.DWORD;
                            break;
                        default:
                            throw new IllegalArgumentException(String.format("invalid DB address: %s", address));
                    }
                    int start = Integer.parseUnsignedInt(address.substring(i0 + 4, i1 > 0 ? i1 : address.length()));
                    item = new DataItem(AreaType.DB, type, db, start, i1 < 1 ? 0 : Integer.parseUnsignedInt(address.substring(i1 + 1, address.length())));
                    break;
                case 'M':
                    int startM = Integer.parseUnsignedInt(address.substring(1, i0 > 1 ? i0 : address.length()));
                    item = new DataItem(AreaType.MK, i0 > 1 ? DataType.BIT : DataType.BYTE, 0, startM, i0 < 1 ? 0 : Integer.parseUnsignedInt(address.substring(i0 + 1, address.length())));
                    break;
                default:
                    throw new IllegalArgumentException(String.format("invalid Memory address: %s", address));
            }
            return item;
        } catch (IllegalArgumentException e) {
            throw e;
        } catch (Exception e) {
            throw new IllegalArgumentException(String.format("address: %s not parsable", address), e);
        }

    }


}
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import org.comtel2000.mokka7.block.BlockType;
import org.comtel2000.mokka7.block.PlcCpuStatus;
import org.comtel2000.mokka7.block.S7BlockInfo;
import org.comtel2000.mokka7.block.S7BlockList;
import org.comtel2000.mokka7.block.S7CpInfo;
import org.comtel2000.mokka7.block.S7CpuInfo;
import org.comtel2000.mokka7.block.S7DataItem;
import org.comtel2000.mokka7.block.S7OrderCode;
import org.comtel2000.mokka7.block.S7Protection;
import org.comtel2000.mokka7.block.S7Szl;
import org.comtel2000.mokka7.exception.S7Exception;
import org.comtel2000.mokka7.type.AreaType;
import org.comtel2000.mokka7.type.DataType;
import org.comtel2000.mokka7.util.ReturnCode;
import org.comtel2000.mokka7.util.S7;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
        try {
            openTcpConnect(5000);
            openIsoConnect();
            updateNegotiatePduLength();
        } catch (S7Exception e) {
            disconnect();
            throw e;
        }
        return connected = true;
    }

    @Override
    public boolean connect(String address, int rack, int slot) throws S7Exception {
        config.setHost(address);
        config.setRack(rack);
        config.setSlot(slot);
        config.setRemoteTSAP((config.getType().getValue() << 8) + (rack * 0x20) + slot);
        return connect();
    }

    @Override
    public boolean isConnected() {
        return connected;
    }

    public int getRecvTimeout() {
        return recvTimeout;
    }

    public void setRecvTimeout(int recvTimeout) {
        this.recvTimeout = recvTimeout;
    }

    @Override
    public int dbGet(int db, byte[] buffer) throws S7Exception {
        S7BlockInfo block = getAgBlockInfo(BlockType.DB, db);
        if (block != null) {
            int sizeToRead = block.mc7Size;
            // Checks the room
            if (sizeToRead > buffer.length) {
                logger.error("buffer size to small ({}/{})", sizeToRead, buffer.length);
                throw buildException(S7_BUFFER_TOO_SMALL);
            }
            if (readArea(AreaType.DB, db, 0, sizeToRead, DataType.BYTE, buffer) > 0) {
                return sizeToRead;
            }
        }
        throw buildException(S7_FUNCTION_ERROR);
    }

    @Override
    public int dbFill(int db, byte fill) throws S7Exception {
        S7BlockInfo block = getAgBlockInfo(BlockType.DB, db);
        if (block != null) {
            byte[] buffer = new byte[block.mc7Size];
            Arrays.fill(buffer, fill);
            if (writeArea(AreaType.DB, db, 0, buffer.length, DataType.BYTE, buffer)) {
                return buffer.length;
            }
        }
        throw buildException(S7_FUNCTION_ERROR);
    }

    @Override
    public void disconnect() {
        logger.debug("disconnecting..");
        closeSilently(inStream);
        closeSilently(outStream);
        closeSilently(tcpSocket);
        connected = false;
        pduLength = 0;
    }

    @Override
    public S7BlockInfo getAgBlockInfo(BlockType blockType, int blockNumber) throws S7Exception {
        // Block Type
        S7_BI[30] = blockType.getValue();
        // Block Number
        S7_BI[31] = (byte) ((blockNumber / 10000) + 0x30);
        blockNumber = blockNumber % 10000;
        S7_BI[32] = (byte) ((blockNumber / 1000) + 0x30);
        blockNumber = blockNumber % 1000;
        S7_BI[33] = (byte) ((blockNumber / 100) + 0x30);
        blockNumber = blockNumber % 100;
        S7_BI[34] = (byte) ((blockNumber / 10) + 0x30);
        blockNumber = blockNumber % 10;
        S7_BI[35] = (byte) ((blockNumber) + 0x30);

        if (sendPacket(S7_BI)) {
            int length = recvIsoPacket();
            if (length < 28) {
                throw buildException(ISO_INVALID_PDU);
            }
            if (S7.getWordAt(pdu, 27) != 0) {
                throw buildException(S7.getWordAt(pdu, 27));
            }
            return S7BlockInfo.of(pdu, 42);
        }
        throw buildException(S7_FUNCTION_ERROR);
    }

    @Override
    public S7BlockList getS7BlockList() throws S7Exception {

        S7.setWordAt(S7_BL, 11, incrementAndGet());
        if (sendPacket(S7_BL)) {
            int length = recvIsoPacket();
            if (length < 28) {
                throw buildException(ISO_INVALID_PDU);
            }
            if (S7.getWordAt(pdu, 27) != 0) {
                throw buildException(S7.getWordAt(pdu, 27));
            }
            return S7BlockList.of(pdu, 33);
        }
        throw buildException(S7_FUNCTION_ERROR);
    }

    @Override
    public S7CpInfo getCpInfo() throws S7Exception {
        S7Szl szl = getSzl(0x0131, 0x0001, 1024);