
package uk.co.wingpath.modbus;

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

/**
* This class implements ModbusRequestHandler by sending the request
* over a Connection
*/

public class ModbusClient
    implements ModbusRequestHandler
{
    private final Connection connection;
    private final PacketType packetType;
    private final Tracer tracer;
    private final Reporter reporter;
    private volatile int retries;
    private long requestCount = 0;
    private long responseTime = 0;
    private long totalResponseTime = 0;
    private boolean alwaysResponds;
    private static int transId = 0;

    public ModbusClient (Connection connection, PacketType packetType,
        Tracer tracer, boolean alwaysResponds, Reporter reporter)
    {
        this.connection = connection;
        this.packetType = packetType;
        this.tracer = tracer;
        this.alwaysResponds = alwaysResponds;
        this.reporter = reporter;
        retries = 3;
    }

    public synchronized long getResponseTime ()
    {
        requestCount = 0;
        totalResponseTime = 0;
        return responseTime;
    }

    public Connection getConnection ()
    {
        return connection;
    }

    public Tracer getTracer ()
    {
        return tracer;
    }

    public void setTimeout (int timeout)
    {
        packetType.setTimeout (timeout);
    }

    public void setRetries (int retries)
    {
        if (retries < 0)
            throw new IllegalArgumentException ("'retries' cannot be negative");
        this.retries = retries;
    }

    public synchronized int genTransId ()
    {
        return packetType.hasTransactionIds () ? (++transId) & 0xffff : -1;
    }

    private ModbusResponse receiveReply ()
        throws ModbusException, InterruptedException, IOException
    {
        try
        {
            ModbusMessage message = packetType.receive (connection, tracer,
                false, reporter);
            message.traceReceive (tracer);
            return (ModbusResponse) message;
        }
        catch (HInterruptedIOException e)
        {
            throw new ModbusException (Modbus.ERROR_TARGET_FAILED_TO_RESPOND,
                e.getHelpId (), e.getMessage ());
        }
        catch (RecoverableIOException e)
        {
            throw new ModbusException (Modbus.ERROR_TARGET_FAILED_TO_RESPOND,
                e.getHelpId (), e.getMessage ());
        }
    }

    // This method synchronizes on the Connection to prevent us from
    // "pipelining" requests (we don't have the mechanism to use the
    // transaction ID to pass a response back to the sending thread), and
    // also to ensure thread-safety/atomicity of connection reads/writes.
    public ModbusResponse handleRequest (ModbusRequest request)
        throws ModbusException, InterruptedException, IOException
    {
        ModbusException exception = null;
        assert retries >= 0;

    loop:
        for (int attempt = 0 ; attempt <= retries ; attempt++)
        {
            synchronized (connection)
            {
                int oldDelimiter = 0;

                try
                {
                    long startTime = System.nanoTime ();

                    // If using RTU or ASCII, discard any unexpected input and
                    // flush the ouput buffer.

                    if (!packetType.hasTransactionIds ())
                    {
                        byte [] data = connection.discardInput ();
                        if (data != null)
                        {
                            ModbusMessage.traceDiscard (tracer, "C005",
                                "Unexpected", data, 0, data.length);
                        }
                        connection.flush ();
                    }

                    // Send the request

                    packetType.send (connection, tracer, request);

                    // Wait for request to have been sent.
                    // This is so that the reply timeout is measured from the
                    // end of transmission of the request, not from the
                    // beginning - at 1200bps transmitting a request could
                    // a couple of seconds.

                    connection.drain ();

                    // If the command told the slave to change its ASCII
                    // delimiter, then we should do the same.
                    if (request.getFunctionCode () == Modbus.FUNC_DIAGNOSTICS &&
                        request.getInt (0) ==
                            Modbus.DIAG_CHANGE_ASCII_INPUT_DELIMITER)
                    {
                        oldDelimiter =
                            packetType.setDelimiter (request.getInt (2) & 0xff);
                    }

                    if (!alwaysResponds && !request.getReplyExpected ())
                    {
                        // We are not using TCP and the request is a
                        // broadcast, so the slave won't respond.
                        // Fabricate a response.

                        if (tracer != null)
                            tracer.endTransaction ();
                        return request.broadcastResponse ();
                    }

                    // Get response from slave.
                    // If transaction IDs are supported and the reply has the
                    // wrong transaction ID, ignore the reply.

                    ModbusResponse reply;

                    for (;;)
                    {
                        reply = receiveReply ();
                        if (reply.getSlaveId () != request.getSlaveId ())
                        {
                            reply.traceDiscard (tracer, "C002",
                                "Wrong slave ID - " + reply.getSlaveId () +
                                " instead of " + request.getSlaveId ());
                            if (exception == null)
                            {
                                exception = new ModbusException (
                                    Modbus.ERROR_TARGET_FAILED_TO_RESPOND,
                                    "C002",
                                    "Wrong slave ID in response: " +
                                    reply.getSlaveId () +
                                    " instead of " + request.getSlaveId ());
                            }
                            continue;
                        }
                        if ((reply.getFunctionCode () & 0x7f)
                            != request.getFunctionCode ())
                        {
                            reply.traceDiscard (tracer, "C004",
                                "Wrong function code - " +
                                reply.getFunctionCode () +
                                " instead of " + request.getFunctionCode ());
                            if (exception == null)
                            {
                                exception = new ModbusException (
                                    Modbus.ERROR_TARGET_FAILED_TO_RESPOND,
                                    "C004",
                                    "Wrong function code in response: " +
                                    reply.getFunctionCode () +
                                    " instead of " +
                                    request.getFunctionCode ());
                            }
                            continue;
                        }
                        if (packetType.hasTransactionIds () &&
                            reply.getTransId () != request.getTransId ())
                        {
                            reply.traceDiscard (tracer, "C003",
                                "Wrong transaction ID - " +
                                reply.getTransId () +
                                " instead of " + request.getTransId ());
                            if (exception == null)
                            {
                                exception = new ModbusException (
                                    Modbus.ERROR_TARGET_FAILED_TO_RESPOND,
                                    "C003",
                                    "Wrong transaction ID in response: " +
                                    reply.getTransId () +
                                    " instead of " + request.getTransId ());
                            }
                            continue;
                        }
                        break;
                    }

                    long endTime = System.nanoTime ();
                    synchronized (this)
                    {
                        requestCount++;
                        totalResponseTime += (endTime - startTime) / 1000;
                        responseTime = totalResponseTime / requestCount;
                    }

                    if ((reply.getFunctionCode () & 0x80) != 0)
                    {
                        throw new ModbusException (reply);
                    }
                    if (tracer != null)
                        tracer.endTransaction ();
                    return reply;
                }
                catch (ModbusException e)
                {
                    // If the command told the slave to change its ASCII
                    // delimiter, but the command didn't succeed, reset our
                    // delimiter.
                    if (request.getFunctionCode () == Modbus.FUNC_DIAGNOSTICS &&
                        request.getInt (0) ==
                            Modbus.DIAG_CHANGE_ASCII_INPUT_DELIMITER)
                    {
                        packetType.setDelimiter (oldDelimiter);
                    }

                    if (exception == null)
                        exception = e;

                    switch (e.getErrorCode ())
                    {
                    case Modbus.ERROR_ILLEGAL_FUNCTION:
                    case Modbus.ERROR_ILLEGAL_DATA_VALUE:
                    case Modbus.ERROR_ILLEGAL_DATA_ADDRESS:
                    case Modbus.ERROR_PATH_UNAVAILABLE:
                        break loop;

                    case Modbus.ERROR_TARGET_FAILED_TO_RESPOND:
                        if (request.getFunctionCode () ==
                                Modbus.FUNC_DIAGNOSTICS &&
                            request.getInt (0) ==
                                Modbus.DIAG_FORCE_LISTEN_ONLY_MODE)
                        {
                            // No response to "force listen-only mode" - assume
                            // that request succeeded.
                            if (tracer != null)
                                tracer.endTransaction ();
                            return request.broadcastResponse ();
                        }
                        break;
                    }
                }
                catch (HEOFException e)
                {
                    // Connection closed (probably by server) - try re-opening.
                    connection.open ();
                }
            }
        }

        if (tracer != null)
            tracer.endTransaction ();
        throw exception;
    }
}

