
package uk.co.wingpath.modbus;

import java.io.*;
import uk.co.wingpath.io.*;
import uk.co.wingpath.util.*;

/**
* Implementation of the {@code PacketType} interface for sending and receiving
* Modbus messages using the Modbus/ASCII protocol.
*/
public class AsciiPacketType
    implements PacketType
{
    private final int delimiter = (int) '\n';
    private volatile int preTimeout = 1000;
    private volatile int eomTimeout = 100;
    private int maxPdu = Modbus.MAX_IMP_PDU_SIZE;
    private boolean allowLongMessages = false;

    @Override
    public void setMaxPdu (int maxPdu)
    {
        assert maxPdu >= Modbus.MIN_PDU_SIZE &&
            maxPdu <= Modbus.MAX_IMP_PDU_SIZE : maxPdu;
        this.maxPdu = maxPdu;
    }

    @Override
    public void setAllowLongMessages (boolean allowLongMessages)
    {
        this.allowLongMessages = allowLongMessages;
    }

    @Override
    public void send (Connection connection, ModbusMessage message)
        throws IOException, InterruptedException
    {
        byte [] data = message.getData ();
        byte [] buf = new byte [data.length * 2 + 9];
        int offset = 0;
        int lrc = 0;

        buf [offset++] = (byte) ':';
        lrc += binToHex ((byte) message.getSlaveId (), buf, offset);
        offset += 2;
        lrc += binToHex ((byte) message.getFunctionCode (), buf, offset);
        offset += 2;

        for (int i = 0 ; i < data.length ; i++)
        {
            lrc += binToHex (data [i], buf, offset);
            offset += 2;
        }

        binToHex ((byte) -lrc, buf, offset);
        offset += 2;
        buf [offset++] = (byte) '\r';
        buf [offset++] = (byte) delimiter;
        Tracer tracer = message.getTracer ();
        if (tracer != null)
        {
            tracer.traceRaw (">", buf, 0, buf.length);
            message.traceSend ();
        }
        connection.write (buf, 0, buf.length);
    }

    private enum State { start, inBody, crRead, done };

    private void commError (ModbusCounters counters)
    {
        if (counters != null)
        {
            counters.incCommErrorCount ();
            counters.addEvent (Modbus.COMM_EVENT_RCV_COMM_ERROR);
        }
    }

    private ModbusMessage receive (Connection connection,
            boolean expectRequest, Tracer tracer, ModbusCounters counters)
        throws InterruptedIOException, IOException, InterruptedException
    {
        byte buf [] = new byte [2 * Modbus.MAX_IMP_PDU_SIZE + 7];
        int bytesRead = 0;
        boolean first = true;
        State state = State.start;

        while (state != State.done)
        {
            if (bytesRead == buf.length)
            {
                if (tracer != null)
                    tracer.traceRaw ("<", buf, 0, bytesRead);
                String msg = state == State.start ?
                    "No ':' at start of message" :
                    "Message too long (> " + buf.length + " bytes)";
                String helpId = state == State.start ? "S301" : "S309";
                ModbusMessage.traceDiscard (tracer, helpId, msg,
                    buf, 0, bytesRead);
                commError (counters);
                bytesRead = 0;
            }
            try
            {
                connection.read (buf, bytesRead, 1,
                    first ? preTimeout : eomTimeout,
                    first);
                first = false;
            }
            catch (InterruptedIOException e)
            {
                if (bytesRead != 0)
                {
                    if (tracer != null)
                        tracer.traceRaw ("<", buf, 0, bytesRead);
                    String msg = state == State.start ?
                        "No ':' at start of message" : "Incomplete message";
                    ModbusMessage.traceDiscard (tracer, "S301", msg,
                        buf, 0, bytesRead);
                    commError (counters);
                    throw new RecoverableIOException ("S301", msg);
                }
                throw e;
            }
            catch (RecoverableIOException e)
            {
                String helpId = e.getHelpId ();
                String msg = e.getMessage ();
                if (bytesRead != 0)
                {
                    if (tracer != null)
                        tracer.traceRaw ("<", buf, 0, bytesRead);
                    ModbusMessage.traceDiscard (tracer,
                        helpId, msg, buf, 0, bytesRead);
                }
                else
                {
                    ModbusMessage.traceError (tracer, helpId, msg);
                }
                if (e instanceof OverrunException)
                {
                    if (counters != null)
                    {
                        counters.incOverrunCount ();
                        counters.addEvent (Modbus.COMM_EVENT_RCV_OVERRUN);
                    }
                }
                else
                {
                    commError (counters);
                }
                throw e;
            }

            int b = buf [bytesRead++] & 0xff;

            switch (state)
            {
            case start:
                if (b == ':')
                {
                    if (bytesRead > 1)
                    {
                        if (tracer != null)
                            tracer.traceRaw ("<", buf, 0, bytesRead - 1);
                        String msg = "No ':' at start of message";
                        ModbusMessage.traceDiscard (tracer, "S301", msg,
                            buf, 0, bytesRead - 1);
                        commError (counters);
                        buf [0] = (byte) b;
                        bytesRead = 1;
                    }
                    state = State.inBody;
                }
                break;

            case inBody:
                if (b == '\r')
                {
                    state = State.crRead;
                }
                else if (b == ':')
                {
                    if (tracer != null)
                        tracer.traceRaw ("<", buf, 0, bytesRead - 1);
                    String msg = "':' in middle of message";
                    ModbusMessage.traceDiscard (tracer, "S302", msg,
                        buf, 0, bytesRead - 1);
                    commError (counters);
                    bytesRead = 1;
                }
                break;

            case crRead:
                if (b == delimiter)
                {
                    if (tracer != null)
                        tracer.traceRaw ("<", buf, 0, bytesRead);
                    state = State.done;
                }
                else if (b == ':')
                {
                    if (tracer != null)
                        tracer.traceRaw ("<", buf, 0, bytesRead - 1);
                    String msg = "':' in middle of message";
                    ModbusMessage.traceDiscard (tracer, "S302", msg,
                        buf, 0, bytesRead - 1);
                    commError (counters);
                    bytesRead = 1;
                }
                else
                {
                    if (tracer != null)
                        tracer.traceRaw ("<", buf, 0, bytesRead);
                    String msg = "LF terminator missing";
                    ModbusMessage.traceDiscard (tracer, "S303", msg,
                        buf, 0, bytesRead);
                    commError (counters);
                    bytesRead = 0;
                    state = State.start;
                }
                break;
            }
        }

        if (bytesRead < 9)
        {
            String msg = "Message too short (" + bytesRead + " bytes)";
            ModbusMessage.traceDiscard (tracer, "S304", msg, buf, 0, bytesRead);
            commError (counters);
            throw new RecoverableIOException ("S304", msg);
        }
        if ((bytesRead & 1) == 0)
        {
            String msg = "Invalid message length (" + bytesRead +
                " bytes) - should be odd";
            ModbusMessage.traceDiscard (tracer, "S305", msg, buf, 0, bytesRead);
            commError (counters);
            throw new RecoverableIOException ("S305", msg);
        }

        for (int i = 1 ; i < bytesRead - 2 ; i++)
        {
            if (Character.digit ((char) buf [i], 16) < 0)
            {
                String msg = "Non-hex character in message";
                ModbusMessage.traceDiscard (tracer, "S306", msg,
                    buf, 0, bytesRead);
                commError (counters);
                throw new RecoverableIOException ("S306", msg);
            }
        }

        int lrc = 0;
        int offset = 1;
        int slaveId = hexToBin (buf, offset);
        lrc += slaveId;
        offset += 2;
        int functionCode = hexToBin (buf, offset);
        lrc += functionCode;
        offset += 2;
        byte [] data = new byte [(bytesRead - 9) / 2];

        for (int i = 0 ; i < data.length ; i++)
        {
            data [i] = (byte) hexToBin (buf, offset);
            lrc += data [i];
            offset += 2;
        }

        lrc += hexToBin (buf, offset);
        lrc = (-lrc) & 0xff;
        if (lrc != 0)
        {
            int rcvlrc = hexToBin (buf, offset) & 0xff;
            String msg = String.format ("Invalid LRC %02x (should be %02x)",
                rcvlrc, (rcvlrc + lrc) & 0xff);
            ModbusMessage.traceDiscard (tracer, "S307", msg, buf, 0, bytesRead);
            commError (counters);
            throw new RecoverableIOException ("S307", msg);
        }

        if (data.length + 1 > maxPdu)
        {
            String msg = "PDU size (" + (data.length + 1) +
                " bytes) exceeds maximum (" + maxPdu + " bytes)";
            ModbusMessage.traceDiscard (tracer, "S308", msg, buf, 0, bytesRead);
            commError (counters);
            throw new RecoverableIOException ("S308", msg);
        }

        if (expectRequest && (functionCode & 0x80) != 0)
        {
            // We are expecting a request, but the error bit is set in
            // the function code byte, so this is presumably a response.
            expectRequest = false;
        }
        ModbusMessage message = new ModbusMessage (expectRequest, slaveId,
            functionCode, -1, data, tracer);
        try
        {
            message.checkSize (allowLongMessages);
        }
        catch (ModbusException e)
        {
            if (expectRequest)
            {
                // In a multi-drop setup we may see responses from other
                // slaves, so see if size is valid for a response.
                try
                {
                    ModbusMessage msg = new ModbusMessage (false, slaveId,
                        functionCode, -1, data, tracer);
                    msg.checkSize (allowLongMessages);
                    message = msg;
                }
                catch (ModbusException e1)
                {
                }
            }
        }
        if (counters != null)
        {
            counters.incBusMessageCount ();
            if (message.isRequest ())
            {
                int event = Modbus.COMM_EVENT_RCV_OK;
                if (message.getSlaveId () == 0)
                    event |= Modbus.COMM_EVENT_RCV_BROADCAST;
                counters.addEvent (event);
            }
        }
        message.traceReceive ();
        return message;
    }

    @Override
    public ModbusMessage receiveRequest (Connection connection, Tracer tracer,
            ModbusCounters counters)
        throws InterruptedIOException, IOException, InterruptedException
    {
        for (;;)
        {
            ModbusMessage message = receive (connection, true, tracer,
                counters);
            if (message.isRequest ())
                return message;
            // Presumably a response from another slave.
            ModbusMessage.traceDiscard (tracer, "S401", "Unexpected");
        }
    }

    @Override
    public ModbusMessage receiveResponse (Connection connection, Tracer tracer,
            ModbusCounters counters)
        throws InterruptedIOException, IOException, InterruptedException
    {
        return receive (connection, false, tracer, counters);
    }

    @Override
    public ModbusMessage receive (Connection connection, Tracer tracer,
            ModbusCounters counters)
        throws InterruptedIOException, IOException, InterruptedException
    {
        return receive (connection, true, tracer, counters);
    }

    @Override
    public boolean hasTransactionIds ()
    {
        return false;
    }

    @Override
    public void setTimeout (int timeout)
    {
        preTimeout = timeout;
    }

    @Override
    public void setEomTimeout (int timeout)
    {
        eomTimeout = timeout;
    }

    private byte binToHex (byte b)
    {
        b &= 0x0f;
        return (byte) (b < 10 ? b + 0x30 : (b - 10) + 0x41);
    }

    private int binToHex (byte b, byte [] buf, int offset)
    {
        buf [offset] = binToHex ((byte) (b >>> 4));
        buf [offset + 1] = binToHex (b);
        return b & 0xff;
    }

    private int hexToBin (int b)
    {
        if (b >= 0x30 && b <= 0x39)
            return b - 0x30;
        if (b >= 0x41 && b <= 0x46)
            return (b - 0x41) + 10;
        if (b >= 0x61 && b <= 0x66)
            return (b - 0x61) + 10;
        return 0;
    }

    private int hexToBin (byte [] buf, int offset)
    {
        return ((hexToBin (buf [offset]) << 4) |
            hexToBin (buf [offset + 1]));
    }
}

