
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 int delimiter = (int) '\n';
    private int oldDelimiter = (int) '\n';
    private volatile int preTimeout = 1000;
    private volatile int eomTimeout = 100;
    private int maxPdu = Modbus.MAX_IMP_PDU_SIZE;

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

    public void setAllowLongMessages (boolean allowLongMessages)
    {
    }

    public void send (Connection connection, Tracer tracer,
            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;
        if (tracer != null)
        {
            tracer.traceRaw (">", buf, 0, buf.length);
            message.traceSend (tracer);
        }
        connection.write (buf, 0, buf.length);
    }

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

    public ModbusMessage receive (Connection connection,
            Tracer tracer, boolean isRequest, Reporter reporter)
        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);
                if (reporter != null)
                    reporter.warning (helpId, "Invalid data received: " + msg);
                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);
                    if (reporter != null)
                    {
                        reporter.warning ("S301",
                            "Invalid data received: " + msg);
                    }
                    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 (reporter != null)
                    reporter.warning (helpId, "Invalid data received: " + msg);
                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);
                        if (reporter != null)
                        {
                            reporter.warning ("S301",
                                "Invalid data received: " + msg);
                        }
                        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);
                    if (reporter != null)
                    {
                        reporter.warning ("S302",
                            "Invalid data received: " + msg);
                    }
                    bytesRead = 1;
                }
                break;

            case crRead:
                if (b == delimiter || b == oldDelimiter)
                {
                    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);
                    if (reporter != null)
                    {
                        reporter.warning ("S302",
                            "Invalid data received: " + msg);
                    }
                    bytesRead = 1;
                }
                else
                {
                    if (tracer != null)
                        tracer.traceRaw ("<", buf, 0, bytesRead);
                    String msg = "LF terminator missing";
                    ModbusMessage.traceDiscard (tracer, "S303", msg,
                        buf, 0, bytesRead);
                    if (reporter != null)
                    {
                        reporter.warning ("S303",
                            "Invalid data received: " + msg);
                    }
                    bytesRead = 0;
                    state = State.start;
                }
                break;
            }
        }

        if (bytesRead < 9)
        {
            String msg = "Message too short (" + bytesRead + " bytes)";
            ModbusMessage.traceDiscard (tracer, "S304", msg, buf, 0, bytesRead);
            if (reporter != null)
                reporter.warning ("S304", "Invalid data received: " + msg);
            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);
            if (reporter != null)
                reporter.warning ("S305", "Invalid data received: " + msg);
            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);
                if (reporter != null)
                    reporter.warning ("S306", "Invalid data received: " + msg);
                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);
            if (reporter != null)
                reporter.warning ("S307", "Invalid data received: " + msg);
            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);
            if (reporter != null)
                reporter.warning ("S308", "Invalid data received: " + msg);
            throw new RecoverableIOException ("S308", msg);
        }

        ModbusMessage message;
        if (isRequest)
            message = new ModbusRequest (slaveId, functionCode, -1, data);
        else if ((functionCode & 0x80) != 0 && data.length > 0)
            message = new ModbusErrorResponse (slaveId, functionCode, -1,
                data [0] & 0xff);
        else
            message = new ModbusResponse (slaveId, functionCode, -1, data);
        return message;
    }

    public boolean hasTransactionIds ()
    {
        return false;
    }

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

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

    public int setDelimiter (int delimiter)
    {
        oldDelimiter = this.delimiter;
        this.delimiter = delimiter;
        return oldDelimiter;
    }

    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]));
    }
}

