Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Negotiated PDU Size #21

Closed
olesa79 opened this issue Feb 1, 2015 · 8 comments
Closed

Negotiated PDU Size #21

olesa79 opened this issue Feb 1, 2015 · 8 comments

Comments

@olesa79
Copy link

@olesa79 olesa79 commented Feb 1, 2015

This is my modified Open-code which supports negotiated PDU size. The original code hardcodes an effective 222-byte PDU size (240-18). S7-400 supports 480-18 and I believe S7-1500 supports 960-18.
This code requests highest possible PDU size and PLC will respond with its highest supported size which is saved to the PDUSize property.

I am using this code successfully with 300 and 400 PLCs and I've briefly tested with 1500 but I don't remember if I got a 480 or 960 PDU.

PS. Sorry for posting code here instead of contributing :)

    public ErrorCode Open()
    {
        byte[] bReceive = new byte[256];

        if (OnlyConnectIfRespondsToPing && !Ping())
        {
            LastErrorCode = ErrorCode.IPAddressNotAvailable;
            LastErrorString = string.Format("Destination IP-Address '{0}' is not available!", IP);
            return LastErrorCode;
        }

        try {
            // open the channel
            if (_mSocket != null)
                _mSocket.Dispose();

            _mSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

            _mSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveTimeout, ReceiveTimeout);
            _mSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.SendTimeout, SendTimeout);

            IPEndPoint server = new IPEndPoint(IPAddress.Parse(IP), Port);
            IAsyncResult result = _mSocket.BeginConnect(server, null, null);
            result.AsyncWaitHandle.WaitOne(1000, true);
            if (!_mSocket.Connected)
                throw new TimeoutException();
        }
        catch (TimeoutException ex)
        {
            LastErrorCode = ErrorCode.ConnectionTimeout;
            LastErrorString = ex.Message;
            return ErrorCode.ConnectionError;
        }
        catch (Exception ex)
        {
            LastErrorCode = ErrorCode.ConnectionError;
            LastErrorString = ex.Message;
            return ErrorCode.ConnectionError;
        }

        try 
        {
            byte[] bSend1 = { 3, 0, 0, 22, 17, 224, 0, 0, 0, 46, 0, 193, 2, 1, 0, 194, 2, 3, 0, 192, 1, 9 };
            // 0 = version number (for S7 communication always 0x03) 
            // 1 = reserved (always 0x00) 
            // 2-3 = size of ISO-TCP message (including this header) in byte 
            // 4 = length (in byte) of TPDU header  (without this byte and possible user data)
            // 5 = CR code (1110) & credit (always 0000)
            // 6-7 = destination reference 
            // 8-9 = source reference 
            // 10 = class option (always class 0)
            // 11 = 0xC1 (code: calling TSAP-ID) 
            // 12 = 0x02 (number of bytes following)
            // 13 = 0x02 (unknown function; part of TSAP-ID)
            // 14 = TSAP-ID (rack & slot) 
            // 15 = 0xC2 (code: called TSAP-ID)
            // 16 = 0x02 (number of bytes following)
            // 17 = 0x02 (unknown function; part of TSAP-ID)
            // 18 = TSAP-ID (rack & slot)
            // 19 = 0xC0 (code: TPDU size)
            // 20 = 0x01 (number of bytes following)
            // 21 = TPDU size (as exponent to base of 2)

            switch (CPU) {
                case CpuType.S7200:
                    //S7200: Chr(193) & Chr(2) & Chr(16) & Chr(0) 'Eigener Tsap
                    bSend1[11] = 193;
                    bSend1[12] = 2;
                    bSend1[13] = 16;
                    bSend1[14] = 0;
                    //S7200: Chr(194) & Chr(2) & Chr(16) & Chr(0) 'Fremder Tsap
                    bSend1[15] = 194;
                    bSend1[16] = 2;
                    bSend1[17] = 16;
                    bSend1[18] = 0;
                    break;
                case CpuType.S7300:
                    //S7300: Chr(193) & Chr(2) & Chr(1) & Chr(0)  'Eigener Tsap
                    bSend1[11] = 193;
                    bSend1[12] = 2;
                    bSend1[13] = 1;
                    bSend1[14] = 0;
                    //S7300: Chr(194) & Chr(2) & Chr(3) & Chr(2)  'Fremder Tsap
                    bSend1[15] = 194;
                    bSend1[16] = 2;
                    bSend1[17] = 3;
                    bSend1[18] = (byte)(Rack * 2 * 16 + Slot);
                    break;
                case CpuType.S7400:
                    //S7400: Chr(193) & Chr(2) & Chr(1) & Chr(0)  'Eigener Tsap
                    bSend1[11] = 193;
                    bSend1[12] = 2;
                    bSend1[13] = 1;
                    bSend1[14] = 0;
                    //S7400: Chr(194) & Chr(2) & Chr(3) & Chr(3)  'Fremder Tsap
                    bSend1[15] = 194;
                    bSend1[16] = 2;
                    bSend1[17] = 3;
                    bSend1[18] = (byte)(Rack * 2 * 16 + Slot);
                    break;
                //case CpuType.S71200:
                case CpuType.S71500:
                    // Eigener Tsap
                    bSend1[11] = 193;
                    bSend1[12] = 2;
                    bSend1[13] = 0x10;
                    bSend1[14] = 0x2;
                    // Fremder Tsap
                    bSend1[15] = 194;
                    bSend1[16] = 2;
                    bSend1[17] = 0x3;
                    bSend1[18] = 0x0;
                    break;
                default:
                    return ErrorCode.WrongCPU_Type;
            }


            //Console.WriteLine("> " + string.Join(" ", bSend1.Select(b => b.ToString("x2"))));
            _mSocket.Send(bSend1, 22, SocketFlags.None);
            int receiveCount = _mSocket.Receive(bReceive, 22, SocketFlags.None);

            /*
            0-3 ISO-TCP header 
            4 length (in byte) of TPDU header (without this byte and possible user data) 
            5 CC code (1101) & credit (always 0000) 
            6-7 destination reference 
            8-9 source reference 
            10 class option (always class 0) 
            11 0xC0 (code: TPDU size) 
            12 0x01 (length of bytes following) 
            13 TPDU size (as exponent to base of 2) 
            14 0xC1 (code: calling TSAP-ID) 
            15 0x02 (length of id) 
            16 0x02 (unknown function; part of TSAP-ID) 
            17 TSAP-ID (rack & slot) 
            18 0xC2 (code: called TSAP-ID) 
            19 0x02 (length of id) 
            20 0x02 (unknown function; part of TSAP-ID) 
            21 TSAP-ID (rack & slot) 
             * 
             * More about data structurs can be found on http://www.bj-ig.de/147.html
            */

            //Console.WriteLine("< " + string.Join(" ", bReceive.Take(receiveCount).Select(b => b.ToString("x2"))));
            //Console.WriteLine("PDU: " + bReceive[13] + " (" + Math.Pow(2, bReceive[13]) + ")");
            if (receiveCount != 22)
            {
                throw new Exception(ErrorCode.WrongNumberReceivedBytes.ToString());
            }

            byte[] bsend2 = { 3, 0, 0, 25, 2, 240, 128, 50, 1, 0, 0, 255, 255, 0, 8, 0, 0, 240, 0, 0, 3, 0, 3, 0x3, 0xC0 };
            //Console.WriteLine("> " + string.Join(" ", bsend2.Select(b => b.ToString("x2"))));
            _mSocket.Send(bsend2, 25, SocketFlags.None);
            receiveCount = _mSocket.Receive(bReceive, 27, SocketFlags.None);
            //Console.WriteLine("< " + string.Join(" ", bReceive.Take(receiveCount).Select(b => b.ToString("x2"))));
            if (receiveCount == 27)
            {
                PDUSize = IPAddress.NetworkToHostOrder((short)BitConverter.ToUInt16(bReceive, 25));
            }
            else
            {
                //Console.WriteLine("< " + string.Join(" ", bReceive.Select(b => b.ToString("x2"))));
                throw new Exception(ErrorCode.WrongNumberReceivedBytes.ToString());
            } 
        }
        catch 
        {
            LastErrorCode = ErrorCode.ConnectionError;
            LastErrorString = string.Format("Couldn't establish the connection to {0}!", IP);
            return ErrorCode.ConnectionError;
        }

        return ErrorCode.NoError;
    }
@mesta1

This comment has been minimized.

Copy link
Collaborator

@mesta1 mesta1 commented Feb 2, 2015

Thanks! I will add it then make a pull request.

@mesta1

This comment has been minimized.

Copy link
Collaborator

@mesta1 mesta1 commented Feb 2, 2015

I checked the code but i need more information to merge it.
Actually i don't know the value of CpuType.S71500 and how do you use PDUSize in the code.
If you can provide the full source, i can make the merge.

@olesa79

This comment has been minimized.

Copy link
Author

@olesa79 olesa79 commented Feb 3, 2015

The only relevant part for PDU is the last two bytes of bSend2 (0x3 0xC0) which requests max PDU size from PLC. Bytes 25/26 in the response is the PLCs PDU.

A single read-request can request at most PDUSize-18 bytes. I often read several kilobytes from a datablock so I need to make multiple read requests where each request is at most PDUSize-18.
For a S7-300 which has a PDU of 240, each request can be 222 bytes max.

From my code that use the PLC-class, I use
public int MaxRequestSize { get { return Math.Max(222,plc.PDUSize - 18); } }

@neohist

This comment has been minimized.

Copy link

@neohist neohist commented May 27, 2015

Thks for the help i update the code so pls i wanna contribuite with the comunity:

public ErrorCode Open()
{
byte[] bReceive = new byte[256];

        try 
        {
            // check if available
            if (!IsAvailable)
            {
                throw new Exception();
            }
        }
        catch  
        {
            LastErrorCode = ErrorCode.IPAddressNotAvailable;
            LastErrorString = string.Format("Destination IP-Address '{0}' is not available!", IP);
            return LastErrorCode;
        }

        try {
            // open the channel
            _mSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

            _mSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveTimeout, 1000);
            _mSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.SendTimeout, 1000);

            IPEndPoint server = new IPEndPoint(IPAddress.Parse(IP), 102);
            _mSocket.Connect(server);
        }
        catch (Exception ex) {
            LastErrorCode = ErrorCode.ConnectionError;
            LastErrorString = ex.Message;
            return ErrorCode.ConnectionError;
        }

        try 
        {
            byte[] bSend1 = { 3, 0, 0, 22, 17, 224, 0, 0, 0, 46, 0, 193, 2, 1, 0, 194, 2, 3, 0, 192, 1, 9 };

            switch (CPU) {
                case CpuType.S7200:
                    //S7200: Chr(193) & Chr(2) & Chr(16) & Chr(0) 'Eigener Tsap
                    bSend1[11] = 193;
                    bSend1[12] = 2;
                    bSend1[13] = 16;
                    bSend1[14] = 0;
                    //S7200: Chr(194) & Chr(2) & Chr(16) & Chr(0) 'Fremder Tsap
                    bSend1[15] = 194;
                    bSend1[16] = 2;
                    bSend1[17] = 16;
                    bSend1[18] = 0;
                    break;
                case CpuType.S71200:
                case CpuType.S7300:
                    //S7300: Chr(193) & Chr(2) & Chr(1) & Chr(0)  'Eigener Tsap
                    bSend1[11] = 193;
                    bSend1[12] = 2;
                    bSend1[13] = 1;
                    bSend1[14] = 0;
                    //S7300: Chr(194) & Chr(2) & Chr(3) & Chr(2)  'Fremder Tsap
                    bSend1[15] = 194;
                    bSend1[16] = 2;
                    bSend1[17] = 3;
                    bSend1[18] = (byte)(Rack * 2 * 16 + Slot);
                    break;
                case CpuType.S7400:
                    //S7400: Chr(193) & Chr(2) & Chr(1) & Chr(0)  'Eigener Tsap
                    bSend1[11] = 193;
                    bSend1[12] = 2;
                    bSend1[13] = 1;
                    bSend1[14] = 0;
                    //S7400: Chr(194) & Chr(2) & Chr(3) & Chr(3)  'Fremder Tsap
                    bSend1[15] = 194;
                    bSend1[16] = 2;
                    bSend1[17] = 3;
                    bSend1[18] = (byte)(Rack * 2 * 16 + Slot);
                    break;
                case CpuType.S71500:
                    // Eigener Tsap
                    bSend1[11] = 193;
                    bSend1[12] = 2;
                    bSend1[13] = 0x10;
                    bSend1[14] = 0x2;
                    // Fremder Tsap
                    bSend1[15] = 194;
                    bSend1[16] = 2;
                    bSend1[17] = 0x3;
                    bSend1[18] = 0x0;
                    break;
                default:
                    return ErrorCode.WrongCPU_Type;
            }

            _mSocket.Send(bSend1, 22, SocketFlags.None);
            if (_mSocket.Receive(bReceive, 22, SocketFlags.None) != 22)
            {
                throw new Exception(ErrorCode.WrongNumberReceivedBytes.ToString());
            } 

            byte[] bsend2 = { 3, 0, 0, 25, 2, 240, 128, 50, 1, 0, 0, 255, 255, 0, 8, 0, 0, 240, 0, 0, 3, 0, 3, 1, 0 };
            _mSocket.Send(bsend2, 25, SocketFlags.None);

            if (_mSocket.Receive(bReceive, 27, SocketFlags.None) != 27)
            {
                throw new Exception(ErrorCode.WrongNumberReceivedBytes.ToString());
            } 
            IsConnected = true;
        }
        catch 
        {
            LastErrorCode = ErrorCode.ConnectionError;
            LastErrorString = string.Format("Couldn't establish the connection to {0}!", IP);
            IsConnected = false;
            return ErrorCode.ConnectionError;
        }

        return ErrorCode.NoError;
    }
@neohist

This comment has been minimized.

Copy link

@neohist neohist commented May 27, 2015

and in Enums class: me update with this:

public enum CpuType
{
S7200 = 0,
S7300 = 10,
S7400 = 20,
S71200 = 30,
S71500 = 40,
}

@killnine

This comment has been minimized.

Copy link
Member

@killnine killnine commented May 27, 2015

I haven't looked at this yet, but if you wrap it in a pull request I can take a closer look and get it merged.

@mesta1 mesta1 added the enhancement label Sep 27, 2015
@mesta1

This comment has been minimized.

Copy link
Collaborator

@mesta1 mesta1 commented Sep 30, 2015

It would be nice to have a pull request to integrate this.

@mesta1 mesta1 added the help wanted label Feb 5, 2018
@mesta1 mesta1 closed this Feb 5, 2018
@mesta1 mesta1 reopened this Feb 5, 2018
thoj added a commit to thoj/s7netplus that referenced this issue Apr 8, 2018
Use this limit insterad of hardcoded limit.
Remove var count limit.
thoj added a commit to thoj/s7netplus that referenced this issue Apr 8, 2018
Use this limit insterad of hardcoded limit.
Remove var count limit.

Remove test data
thoj added a commit to thoj/s7netplus that referenced this issue Apr 8, 2018
Use this limit insterad of hardcoded limit.
Remove var count limit.

Remove test data
thoj added a commit to thoj/s7netplus that referenced this issue Apr 8, 2018
Use this limit insterad of hardcoded limit.
Remove var count limit.

Remove test data
thoj added a commit to thoj/s7netplus that referenced this issue Apr 8, 2018
Use this limit insterad of hardcoded limit.
Remove var count limit.
thoj added a commit to thoj/s7netplus that referenced this issue Apr 10, 2018
Read responses from the PLS using classes for TPKT and COPT. This
makes the communication more robust. It will now handle empty COTP
pakces SoftPLS and WinAC based PLCs send out.

Change logic to use COTP and S7Comm reponse codes instead of
relying on packet size sizes.

I use RFC names for Classes and Functions.

Read Max PDU size from connection setup. Ref S7NetPlus#21

Use this limit insterad of hardcoded limit.
Remove var count limit.
thoj added a commit to thoj/s7netplus that referenced this issue Apr 10, 2018
Read responses from the PLS using classes for TPKT and COPT. This
makes the communication more robust. It will now handle empty COTP
packets that SoftPLS and WinAC based PLCs send out. I use RFC names for
functions and classes.

Change logic to use COTP and S7Comm reponse codes instead of
relying on packet sizes.

Read Max PDU size from connection setup. Ref S7NetPlus#21
Change logic to use MaxPDU from cpu limit instead of hardcoded limit.

Remove var count limit.
thoj added a commit to thoj/s7netplus that referenced this issue Apr 16, 2018
Read responses from the PLS using classes for TPKT and COPT. This
makes the communication more robust. It will now handle empty COTP
packets that SoftPLS and WinAC based PLCs send out. I use RFC names for
functions and classes.

Change logic to use COTP and S7Comm reponse codes instead of
relying on packet sizes.

Read Max PDU size from connection setup. Ref S7NetPlus#21
I did some test on using this limit. But i met a wall when testing
against snap7 so i decided to drop changes to read/write size. I have
done some tests against WinAC cpu and it seems to handle bigger pdu's if
negotiated in the connection setup. This might just be a SNAP7 bug.
thoj added a commit to thoj/s7netplus that referenced this issue Apr 16, 2018
Read responses from the PLS using classes for TPKT and COPT. This
makes the communication more robust. It will now handle empty COTP
packets that SoftPLS and WinAC based PLCs send out. I use RFC names for
functions and classes.

Change logic to use COTP and S7Comm reponse codes instead of
relying on packet sizes.

Read Max PDU size from connection setup. Ref S7NetPlus#21
I did some test on using this limit. But i met a wall when testing
against snap7 so i decided to drop changes to read/write size. I have
done some tests against WinAC cpu and it seems to handle bigger pdu's if
negotiated in the connection setup. This might just be a SNAP7 bug.
thoj added a commit to thoj/s7netplus that referenced this issue Apr 16, 2018
Read responses from the PLS using classes for TPKT and COPT. This
makes the communication more robust. It will now handle empty COTP
packets that SoftPLS and WinAC based PLCs send out. I use RFC names for
functions and classes.

Change logic to use COTP and S7Comm reponse codes instead of
relying on packet sizes.

Read Max PDU size from connection setup. Ref S7NetPlus#21
I did some test on using this limit. But i met a wall when testing
against snap7 so i decided to drop changes to read/write size. I have
done some tests against WinAC cpu and it seems to handle bigger pdu's if
negotiated in the connection setup. This might just be a SNAP7 bug.
thoj added a commit to thoj/s7netplus that referenced this issue Apr 16, 2018
Read responses from the PLS using classes for TPKT and COPT. This
makes the communication more robust. It will now handle empty COTP
packets that SoftPLS and WinAC based PLCs send out. I use RFC names for
functions and classes.

Change logic to use COTP and S7Comm reponse codes instead of
relying on packet sizes.

Read Max PDU size from connection setup. Ref S7NetPlus#21
Change logic to use MaxPDUSize when reading istead of hardcoded limit.

I tried using MaxPDUSize when writing data but this failed when packet size is
over 256 on snap7. So i decided to drop changes to write size.
I have done some tests against WinAC cpu and it seems to handle bigger pdu's
when writing if negotiated in the connection setup. This might just be a SNAP7 bug.
thoj added a commit to thoj/s7netplus that referenced this issue Apr 16, 2018
Read responses from the PLS using classes for TPKT and COPT. This
makes the communication more robust. It will now handle empty COTP
packets that SoftPLS and WinAC based PLCs send out. I use RFC names for
functions and classes.

Change logic to use COTP and S7Comm reponse codes instead of
relying on packet sizes.

Read Max PDU size from connection setup. Ref S7NetPlus#21
Change logic to use MaxPDUSize when reading istead of hardcoded limit.

I tried using MaxPDUSize when writing data but this failed when packet size is
over 256 on snap7. So i decided to drop changes to write size.
I have done some tests against WinAC cpu and it seems to handle bigger pdu's
when writing if negotiated in the connection setup. This might just be a SNAP7 bug.
thoj added a commit to thoj/s7netplus that referenced this issue Apr 16, 2018
Read responses from the PLS using classes for TPKT and COPT. This
makes the communication more robust. It will now handle empty COTP
packets that SoftPLS and WinAC based PLCs send out. I use RFC names for
functions and classes.

Change logic to use COTP and S7Comm reponse codes instead of
relying on packet sizes.

Read Max PDU size from connection setup. Ref S7NetPlus#21
Change logic to use MaxPDUSize when reading istead of hardcoded limit.

I tried using MaxPDUSize when writing data but this failed when packet size is
over 256 on snap7. So i decided to drop changes to write size.
I have done some tests against WinAC cpu and it seems to handle bigger pdu's
when writing if negotiated in the connection setup. This might just be a SNAP7 bug.

Fix MaxPDUSize for readbytes
thoj added a commit to thoj/s7netplus that referenced this issue Apr 16, 2018
Read responses from the PLS using classes for TPKT and COPT. This
makes the communication more robust. It will now handle empty COTP
packets that SoftPLS and WinAC based PLCs send out. I use RFC names for
functions and classes.

Change logic to use COTP and S7Comm reponse codes instead of
relying on packet sizes.

Read Max PDU size from connection setup. Ref S7NetPlus#21
Change logic to use MaxPDUSize when reading istead of hardcoded limit.

I tried using MaxPDUSize when writing data but this failed when packet size is
over 256 on snap7. So i decided to drop changes to write size.
I have done some tests against WinAC cpu and it seems to handle bigger pdu's
when writing if negotiated in the connection setup. This might just be a SNAP7 bug.

Fix MaxPDUSize for readbytes
thoj added a commit to thoj/s7netplus that referenced this issue Apr 17, 2018
Read responses from the PLS using classes for TPKT and COPT. This
makes the communication more robust. It will now handle empty COTP
packets that SoftPLS and WinAC based PLCs send out. I use RFC names for
functions and classes.

Change logic to use COTP and S7Comm reponse codes instead of
relying on packet sizes.

Read Max PDU size from connection setup. Ref S7NetPlus#21
Change logic to use MaxPDUSize when reading istead of hardcoded limit.

I tried using MaxPDUSize when writing data but this failed when packet size is
over 256 on snap7. So i decided to drop changes to write size.
I have done some tests against WinAC cpu and it seems to handle bigger pdu's
when writing if negotiated in the connection setup. This might just be a SNAP7 bug.

Fix MaxPDUSize for readbytes
thoj added a commit to thoj/s7netplus that referenced this issue Apr 18, 2018
Read responses from the PLS using classes for TPKT and COPT. This
makes the communication more robust. It will now handle empty COTP
packets that SoftPLS and WinAC based PLCs send out. I use RFC names for
functions and classes.

Change logic to use COTP and S7Comm reponse codes instead of
relying on packet sizes.

Read Max PDU size from connection setup. Ref S7NetPlus#21
Change logic to use MaxPDUSize when reading istead of hardcoded limit.

I tried using MaxPDUSize when writing data but this failed when packet size is
over 256 on snap7. So i decided to drop changes to write size.
I have done some tests against WinAC cpu and it seems to handle bigger pdu's
when writing if negotiated in the connection setup. This might just be a SNAP7 bug.

Fix MaxPDUSize for readbytes

Remove debug line

Simplify byte copy. Remove unessesarry buffers.
thoj added a commit to thoj/s7netplus that referenced this issue Apr 18, 2018
Read responses from the PLS using classes for TPKT and COPT. This
makes the communication more robust. It will now handle empty COTP
packets that SoftPLS and WinAC based PLCs send out. I use RFC names for
functions and classes.

Change logic to use COTP and S7Comm reponse codes instead of
relying on packet sizes.

Read Max PDU size from connection setup. Ref S7NetPlus#21
Change logic to use MaxPDUSize when reading istead of hardcoded limit.

I tried using MaxPDUSize when writing data but this failed when packet size is
over 256 on snap7. So i decided to drop changes to write size.
I have done some tests against WinAC cpu and it seems to handle bigger pdu's
when writing if negotiated in the connection setup. This might just be a SNAP7 bug.

Fix MaxPDUSize for readbytes

Remove debug line

Simplify byte copy. Remove unessesarry buffer
@mesta1

This comment has been minimized.

Copy link
Collaborator

@mesta1 mesta1 commented May 13, 2018

Fixed #108

@mesta1 mesta1 closed this May 13, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
4 participants
You can’t perform that action at this time.