
package uk.co.wingpath.modbus;

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

/**
* This class is an implementation of Service that provides a Modbus service
* using a ModbusRequestHandler.
*/

public class ModbusService
    implements Service
{
    private final ModbusRequestHandler handler;
    private final PacketType packetType;
    private final Logger logger;
    private final boolean alwaysRespond;
    private final Reporter reporter;
    private int responseDelay = 0;
    private String tracePrefix;
    private boolean traceEnabled;
    private boolean rawTraceEnabled;

    /**
    * Constructs a {@code ModbusService}.
    * @param handler the handler to be used to process each request.
    * @param packetType the type of Modbus packet to be used for the
    * requests and responses.
    * @param logger the logger to be used for tracing requests and responses.
    * May be {@code null} if no tracing is required.
    * @param alwaysRespond whether broadcast messages should be responded to,
    * and whether Modbus exceptions 11 and 12 should be sent.
    * This should normally be {@code true} for socket connections and
    * {@code false} for serial connections.
    * @param reporter handler for reporting recoverable errors (such as
    * error responses sent back to masters, and badly formatted received data).
    * May be {@code null} if no reporting is required.
    */
    public ModbusService (final ModbusRequestHandler handler,
        PacketType packetType, Logger logger, boolean alwaysRespond,
        final Reporter reporter)
    {
        this.handler = handler;
        this.packetType = packetType;
        this.logger = logger;
        this.alwaysRespond = alwaysRespond;
        this.reporter = reporter;
        tracePrefix = "";
        traceEnabled = false;
        rawTraceEnabled = false;
    }

    public void setTraceEnabled (boolean enabled)
    {
        traceEnabled = enabled;
    }

    public void setRawTraceEnabled (boolean enabled)
    {
        rawTraceEnabled = enabled;
    }

    public void setTracePrefix (String prefix)
    {
        tracePrefix = prefix;
    }

    /**
    * Sends a Modbus response
    * @param message the message to be sent
    */
    private void sendResponse (Connection connection, ModbusResponse message,
            Tracer tracer)
        throws InterruptedException, IOException
    {
        // If not using TCP, don't send reply to broadcast and don't
        // send 0x0A or 0x0B error responses.

        if (!alwaysRespond)
        {
            if (message.getSlaveId () == 0)
            {
                if (message instanceof ModbusErrorResponse)
                {
                    ModbusErrorResponse reply = (ModbusErrorResponse) message;
                    if (reply.getErrorCode () !=
                        Modbus.ERROR_TARGET_FAILED_TO_RESPOND)
                    {
                        reply.traceExplanation (tracer);
                    }
                }
                ModbusMessage.traceExplanation (tracer, "S402",
                    "Response not sent: Request was broadcast");
                if (tracer != null)
                    tracer.endTransaction ();
                return;
            }
            if (message instanceof ModbusErrorResponse)
            {
                ModbusErrorResponse reply = (ModbusErrorResponse) message;
                int error = reply.getErrorCode ();
                if (error == Modbus.ERROR_PATH_UNAVAILABLE ||
                    error == Modbus.ERROR_TARGET_FAILED_TO_RESPOND)
                {
                    reply.traceExplanation (tracer);
                    ModbusMessage.traceExplanation (tracer, "S403",
                        "Response not sent: Exception 10/11");
                    if (tracer != null)
                        tracer.endTransaction ();
                    return;
                }
            }
        }

        synchronized (connection)
        {
            packetType.send (connection, tracer, message);
        }
        if (tracer != null)
            tracer.endTransaction ();
    }

    /**
    * Provides a Modbus service on the supplied connection.
    * <p>This method repeatedly receives a Modbus request from the
    * connection, passes the request to the handler to be processed,
    * and sends the response returned by the handler to the connection.
    * @param connection the connection on which the service is to be
    * provided.
    * @throws InterruptedException if the thread is interrupted.
    * @throws IOException if an I/O error occurs or the connection is closed.
    */
    public void serve (Connection connection)
        throws InterruptedException, IOException
    {
        Tracer tracer = new Tracer (logger);
        tracer.setPrefix (tracePrefix);
        tracer.setConnection (connection);

        for (;;)
        {
            if (Thread.interrupted ())
                throw new InterruptedException ();

            // The following call to 'yield' was added to give the
            // SerialConnection.setSerialPortParams method an opportunity to
            // lock the Connection. It was having to wait for an arbitrary
            // period of time (apparently sometimes for ever under Linux) to
            // acquire the lock. Using 'yield' is not normally recommended
            // for dealing with such "starvation", since it doesn't necessarily
            // do anything, but it seems to work under Linux and Windows XP
            // using Java 1.6. The only alternatives seem to be using 'sleep'
            // (which is not recommended for the same reason), or implementing
            // some kind of fair locking system (which seems to be complicated,
            // and hence error-prone).
            Thread.yield ();

            tracer.setTraceEnabled (traceEnabled);
            tracer.setRawTraceEnabled (rawTraceEnabled);
            ModbusRequest request;
            try
            {
                synchronized (connection)
                {
                    ModbusMessage message = packetType.receive (connection,
                        tracer, true, reporter);
                    message.traceReceive (tracer);
                    request = (ModbusRequest) message;
                }
            }
            catch (InterruptedIOException e)
            {
                // Timed out.
                continue;
            }
            catch (RecoverableIOException e)
            {
                continue;
            }

            ModbusResponse reply;
            try
            {
                reply = handler.handleRequest (request);
            }
            catch (ModbusException e)
            {
                reply = request.errorResponse (e);
                if (reporter != null)
                {
                    String msg = "Error response: " +
                        Modbus.getErrorMessage (e.getErrorCode ());
                    if (e.getExplanation () != null)
                        msg += "\n" + e.getExplanation ();
                    reporter.warning (e.getHelpId (), msg);
                }
            }
            synchronized (connection)
            {
                if (!packetType.hasTransactionIds ())
                {
                    // Discard any unexpected input

                    byte [] data = connection.discardInput ();
                    if (data != null)
                    {
                        String msg = "Unexpected";
                        ModbusMessage.traceDiscard (tracer, "S401", msg,
                            data, 0, data.length);
                        if (reporter != null)
                        {
                            reporter.warning ("S401",
                                "Invalid data received: " + msg);
                        }
                    }
                }
                if (responseDelay != 0)
                    Thread.sleep (responseDelay);
                sendResponse (connection, reply, tracer);
            }
        }
    }

    /**
    * Sets the period to delay by before sending a response.
    * @param delay response delay in milliseconds.
    */
    public void setResponseDelay (int delay)
    {
        responseDelay = delay;
    }
}



