
package uk.co.wingpath.modbus;

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

/**
* This class provides methods that construct a {@link ModbusTransaction},
* pass it to a {@link ModbusClient}, and interpret the
* response to update a {@link ModbusModel}.
* <p>A ModbusMaster handles the transactions for a single Modbus slave.
* <p>Some Modbus commands (Command7, Command8, Command11, Command12, Command17,
* Command43 and CommandCustom) do not use a ModbusModel and are sent
* directly to the ModbusClient instead of via a ModbusMaster.
*/
public class ModbusMaster
{
    /**
    * Implementation of ModbusWaiter used by synchronous command methods.
    */
    private class Waiter
        implements ModbusWaiter
    {
        private ModbusException exception = null;
        private boolean finished = false;

        public synchronized void inform (ModbusException exception)
        {
            this.exception = exception;
            finished = true;
            notifyAll ();
        }

        synchronized void waitUntilFinished ()
            throws ModbusException, InterruptedException
        {
            while (!finished)
                wait ();
            if (exception != null)
                throw exception;
        }
    }

    private final ModbusModel model;
    private ModbusFilePacker filePacker;
    private final ModbusClient client;
    private final int slaveId;
    private final int maxPdu;
    private final boolean checkCountLimits;
    private final boolean strictChecking;
    private final boolean allowLongMessages;
    private final boolean singleWrite;
    private final ResponseTimeAverager responseTimer;
    private final Reporter reporter;

    public ModbusMaster (ModbusModel model, ModbusClient client, int slaveId,
        int maxPdu, boolean checkCountLimits,
        boolean strictChecking, boolean allowLongMessages, boolean singleWrite)
    {
        this.model = model;
        this.client = client;
        this.slaveId = slaveId;
        this.maxPdu = maxPdu;
        this.checkCountLimits = checkCountLimits;
        this.strictChecking = strictChecking;
        this.allowLongMessages = allowLongMessages;
        this.singleWrite = singleWrite;
        reporter = client.getReporter ();
        filePacker = null;
        responseTimer = new ResponseTimeAverager ();
    }

    public ModbusMaster (ModbusModel model, ModbusClient client, int slaveId)
    {
        this (model, client, slaveId, Modbus.MAX_PDU_SIZE, true, true, false,
            false);
    }

    /**
    * Sets the Modbus file packer to be used for commands 20 & 21.
    * @param filePacker the Modbus file packer.
    */
    public void setModbusFilePacker (ModbusFilePacker filePacker)
    {
        this.filePacker = filePacker;
    }

    public int getSlaveId ()
    {
        return slaveId;
    }

    private void cantBroadcast ()
        throws ModbusException
    {
        if (slaveId == 0)
        {
            reporter.warn ("M123",
                "Invalid command: broadcasting a read request");
        }
    }

    private void checkWaiter (ModbusWaiter waiter)
    {
        if (waiter == null)
            throw new NullPointerException ("waiter");
    }

    private void informWaiter (ModbusWaiter waiter, ModbusException exception)
    {
        Tracer tracer = client.getTracer ();
        if (tracer != null)
            tracer.endTransaction ();
        waiter.inform (exception);
    }

    /**
    * Gets the slave's average response time.
    * @return the average response time in nanoseconds, or 0 if no
    * responses have been received.
    */
    public long getResponseTime ()
    {
        return responseTimer.getAverage ();
    }

    /**
    * Gets the slave's response time deviation.
    * @return the response time deviation in nanoseconds, or 0 if no
    * responses have been received.
    */
    public long getResponseTimeDeviation ()
    {
        return responseTimer.getDeviation ();
    }

    public void read (final int address, final int nvalues,
            final ModbusWaiter waiter)
        throws ModbusException, InterruptedException
    {
        checkWaiter (waiter);
        cantBroadcast ();
        AddressMap map = model.getAddressMap ();
        AddressMap.Map space = map.getAddressSpace (address);
        if (space == null)
        {
            throw new ModbusException (
                Modbus.ERROR_ILLEGAL_DATA_ADDRESS,
                "M010",
                "Address " + address + " is not in any address range");
        }
        if (space == map.getHoldingRegisterMap ())
        {
            readHoldingRegisters (address, nvalues, waiter);
        }
        else if (space == map.getInputRegisterMap ())
        {
            readInputRegisters (address, nvalues, waiter);
        }
        else if (space == map.getCoilMap ())
        {
            int size = model.getCoilValueSize ();
            readCoils (address, size == 0 ? nvalues : nvalues * size * 8,
                waiter);
        }
        else if (space == map.getDiscreteInputMap ())
        {
            int size = model.getDiscreteInputValueSize ();
            readDiscreteInputs (address,
                size == 0 ? nvalues : nvalues * size * 8, waiter);
        }
    }

    public void read (int address, int nvalues)
        throws ModbusException, InterruptedException
    {
        Waiter waiter = new Waiter ();
        read (address, nvalues, waiter);
        waiter.waitUntilFinished ();
    }

    public void write (int address, int nvalues, final ModbusWaiter waiter)
        throws ModbusException, InterruptedException
    {
        AddressMap map = model.getAddressMap ();
        AddressMap.Map space = map.getAddressSpace (address);
        if (space == null)
        {
            Modbus.addressError ("M010",
                "Address " + address + " is not in any address range");
        }
        if (space == map.getHoldingRegisterMap ())
        {
            if (singleWrite)
            {
                int count = model.getCount (address, nvalues, space);
                if (count != 1)
                {
                    throw new ModbusException (
                        Modbus.ERROR_ILLEGAL_FUNCTION, null,
                        "Can't use Write Single Register for multi-register " +
                        "value at address " + address +
                        "(count " + count + ")");
                }
                writeSingleRegister (address, waiter);
            }
            else
            {
                writeMultipleRegisters (address, nvalues, waiter);
            }
        }
        else if (space == map.getCoilMap ())
        {
            int size = model.getCoilValueSize ();
            if (singleWrite)
            {
                if (nvalues != 1 || size != 0)
                {
                    throw new ModbusException (
                        Modbus.ERROR_ILLEGAL_FUNCTION, null,
                        "Can't use Write Single Coil for multiple " +
                        "coils at address " + address);
                }
                writeCoil (address, waiter);
            }
            else
            {
                writeCoils (address, size == 0 ? nvalues : nvalues * size * 8,
                    waiter);
            }
        }
        else if (space == map.getInputRegisterMap ())
        {
            throw new ModbusException (
                Modbus.ERROR_ILLEGAL_FUNCTION, null,
                "Writing to Input Register not allowed");
        }
        else if (space == map.getDiscreteInputMap ())
        {
            throw new ModbusException (
                Modbus.ERROR_ILLEGAL_FUNCTION, null,
                "Writing to Discrete Input not allowed");
        }
    }

    public void write (int address, int nvalues)
        throws ModbusException, InterruptedException
    {
        Waiter waiter = new Waiter ();
        write (address, nvalues, waiter);
        waiter.waitUntilFinished ();
    }

    /**
    * Checks the reply size, and gets the number of data bytes in the
    * reply.
    * If long messages are allowed, the supplied byte count is ignored and
    * the number of data bytes is calculated as the reply length minus
    * the supplied overhead.
    * If long messages are not allowed, the supplied byte count is returned.
    * If strict checking is enabled, the reply length is checked to be
    * exactly the supplied overhead plus the supplied byte count.
    * If strict checking is disabled and long messages are not allowed,
    * the reply length is checked to at least the supplied overhead plus
    * the supplied byte count.
    * @param reply the reply message.
    * @param overhead the number of bytes used in the reply for fields
    * other than data.
    * @param byteCount the data byte count extracted from the reply, or 0 if
    * the reply does not contain a byte count.
    * @return the number of data bytes in the reply.
    * @throws ModbusException if any check fails.
    */
    private int checkSize (ModbusMessage reply, int overhead, int byteCount)
        throws ModbusException
    {
        if (allowLongMessages)
        {
            reply.checkMinSize (overhead);
            return reply.size () - overhead;
        }
        if (strictChecking)
            reply.checkSize (overhead + byteCount);
        else
            reply.checkMinSize (overhead + byteCount);
        return byteCount;
    }

    /**
    * Reads into a group of contiguous holding registers.
    * The command is executed asynchronously and may not have finished when
    * this method returns.
    * @param address model address of first register.
    * @param nvalues number of values to read.
    * @param waiter to be notified when the command has finished.
    * @throws InterruptedException if the thread is interrupted.
    * @throws ModbusException if a Modbus error response is received, and
    * for various other errors relating to the construction of the request
    * and the interpretation of the reply.
    */
    public void readHoldingRegisters (final int address, final int nvalues,
            final ModbusWaiter waiter)
        throws ModbusException, InterruptedException
    {
        checkWaiter (waiter);
        cantBroadcast ();
        final AddressMap.Map space =
            model.getAddressMap ().getHoldingRegisterMap ();
        final int count = model.getCount (address, nvalues, space);
        if (strictChecking && count == 0)
        {
            reporter.warn ("M125", "Invalid read count (0)");
        }
        if (count > 65535)
        {
            Modbus.dataError ("M114",
                "Invalid read count (" + count +
                ") - will not fit in 16 bits");
        }
        final int nbytes = model.getTotalBytes (address, nvalues, space);
        if (nbytes + 2 > maxPdu)
        {
            reporter.warn ("M103",
                "Invalid read count (" + nvalues +
                ") - response would exceed max PDU size (" +
                maxPdu + " bytes)");
        }
        else if (!allowLongMessages && nbytes > 255)
        {
            reporter.warn ("M102",
                "Invalid read count (" + nvalues +
                ") - response byte count (" + nbytes +
                ") would not fit in a byte");
        }
        else if (checkCountLimits && nbytes > 125 * 2)
        {
            reporter.warn ("M104",
                "Invalid read count (" + nvalues +
                ") - exceeds count limit (125 * 2 bytes)");
        }
        int addr = model.toHoldingRegister (address);
        MessageBuilder body = new MessageBuilder ();
        body.addInt (addr);
        body.addInt (count);
        ModbusTransaction trans = client.createTransaction (slaveId,
            Modbus.FUNC_READ_HOLDING_REGISTERS, body.getData (),
            new ModbusResponseHandler ()
            {
                public void handleResponse (ModbusTransaction trans)
                {
                    ModbusException exception = trans.getException ();
                    if (exception == null)
                    {
                        try
                        {
                            ModbusMessage response = trans.getResponse ();
                            response.checkMinSize (1 + nbytes);
                            int byteCount =
                                checkSize (response, 1, response.getByte (0));
                            if (strictChecking && byteCount != nbytes)
                            {
                                Modbus.dataError ("M101",
                                    "Wrong byte count in response: " +
                                    byteCount + " when expecting " + nbytes);
                            }
                            byte [] data = response.getData (1, nbytes);
                            model.unpack (data, address, nvalues,
                                space, strictChecking,
                                slaveId, false, response.getTracer ());
                        }
                        catch (ModbusException e)
                        {
                            exception = e;
                        }
                    }
                    informWaiter (waiter, exception);
                }
            });
        trans.setResponseTimeMonitor (responseTimer);
        client.handleTransaction (trans);
    }

    /**
    * Reads into a group of contiguous holding registers.
    * The command is executed synchronously and will have finished when this
    * method returns.
    * @param address model address of first register.
    * @param nvalues number of values to read.
    * @throws InterruptedException if the thread is interrupted.
    * @throws ModbusException if a Modbus error response is received, and
    * for various other errors relating to the construction of the request
    * and the interpretation of the reply.
    */
    public void readHoldingRegisters (int address, int nvalues)
        throws ModbusException, InterruptedException
    {
        Waiter waiter = new Waiter ();
        readHoldingRegisters (address, nvalues, waiter);
        waiter.waitUntilFinished ();
    }

    /**
    * Reads into a group of contiguous input registers.
    * The command is executed asynchronously and may not have finished when
    * this method returns.
    * @param address model address of first register.
    * @param nvalues number of values to read.
    * @param waiter to be notified when the command has finished.
    * @throws InterruptedException if the thread is interrupted.
    * @throws ModbusException if a Modbus error response is received, and
    * for various other errors relating to the construction of the request
    * and the interpretation of the reply.
    */
    public void readInputRegisters (final int address, final int nvalues,
            final ModbusWaiter waiter)
        throws ModbusException, InterruptedException
    {
        checkWaiter (waiter);
        cantBroadcast ();
        final AddressMap.Map space =
            model.getAddressMap ().getInputRegisterMap ();
        final int count = model.getCount (address, nvalues, space);
        if (strictChecking && count == 0)
        {
            reporter.warn ("M125", "Invalid read count (0)");
        }
        if (count > 65535)
        {
            Modbus.dataError ("M114",
                "Invalid read count (" + count +
                ") - will not fit in 16 bits");
        }
        final int nbytes = model.getTotalBytes (address, nvalues, space);
        if (nbytes + 2 > maxPdu)
        {
            reporter.warn ("M103",
                "Invalid read count (" + nvalues +
                ") - response would exceed max PDU size (" +
                maxPdu + " bytes)");
        }
        else if (!allowLongMessages && nbytes > 255)
        {
            reporter.warn ("M102",
                "Invalid read count (" + nvalues +
                ") - response byte count (" + nbytes +
                ") would not fit in a byte");
        }
        else if (checkCountLimits && nbytes > 125 * 2)
        {
            reporter.warn ("M104",
                "Invalid read count (" + nvalues +
                ") - exceeds count limit (125 * 2 bytes)");
        }
        int addr = model.toInputRegister (address);
        MessageBuilder body = new MessageBuilder ();
        body.addInt (addr);
        body.addInt (count);
        ModbusTransaction trans = client.createTransaction (slaveId,
            Modbus.FUNC_READ_INPUT_REGISTERS, body.getData (),
            new ModbusResponseHandler ()
            {
                public void handleResponse (ModbusTransaction trans)
                {
                    ModbusException exception = trans.getException ();
                    if (exception == null)
                    {
                        try
                        {
                            ModbusMessage response = trans.getResponse ();
                            response.checkMinSize (1 + nbytes);
                            int byteCount =
                                checkSize (response, 1, response.getByte (0));
                            if (strictChecking && byteCount != nbytes)
                            {
                                Modbus.dataError ("M101",
                                    "Wrong byte count in response: " +
                                    byteCount + " when expecting " + nbytes);
                            }
                            byte [] data = response.getData (1, nbytes);
                            model.unpack (data, address, nvalues,
                                space, strictChecking,
                                slaveId, false, response.getTracer ());
                        }
                        catch (ModbusException e)
                        {
                            exception = e;
                        }
                    }
                    informWaiter (waiter, exception);
                }
            });
        trans.setResponseTimeMonitor (responseTimer);
        client.handleTransaction (trans);
    }

    /**
    * Reads into a group of contiguous input registers.
    * The command is executed synchronously and will have finished when this
    * method returns.
    * @param address model address of first register.
    * @param nvalues number of values to read.
    * @throws InterruptedException if the thread is interrupted.
    * @throws ModbusException if a Modbus error response is received, and
    * for various other errors relating to the construction of the request
    * and the interpretation of the reply.
    */
    public void readInputRegisters (int address, int nvalues)
        throws ModbusException, InterruptedException
    {
        Waiter waiter = new Waiter ();
        readInputRegisters (address, nvalues, waiter);
        waiter.waitUntilFinished ();
    }

    /** Write from a group of contiguous holding registers */

    public void writeMultipleRegisters (int address, int nvalues,
            final ModbusWaiter waiter)
        throws ModbusException, InterruptedException
    {
        checkWaiter (waiter);
        AddressMap.Map space = model.getAddressMap ().getHoldingRegisterMap ();
        final int count = model.getCount (address, nvalues, space);
        if (strictChecking && count == 0)
        {
            reporter.warn ("M126", "Invalid write count (0)");
        }
        if (count > 65535)
        {
            Modbus.dataError ("M115",
                "Invalid write count (" + count +
                ") - will not fit in 16 bits");
        }
        final int addr = model.toHoldingRegister (address);
        Tracer.Tracing tracing = new Tracer.Tracing ();
        byte [] data = model.pack (address, nvalues, space,
            tracing, slaveId, true);
        if (data.length + 6 > maxPdu)
        {
            Modbus.dataError ("M106",
                "Invalid write count (" + nvalues +
                ") - request would exceed max PDU size (" +
                maxPdu + " bytes)");
        }
        if (!allowLongMessages && data.length > 255)
        {
            Modbus.dataError ("M105",
                "Invalid write count (" + nvalues +
                ") - byte count (" + data.length +
                ") would not fit in a byte");
        }
        if (checkCountLimits && data.length > 123 * 2)
        {
            reporter.warn ("M107",
                "Invalid write count (" + nvalues +
                ") - exceeds count limit (123 * 2 bytes)");
        }
        MessageBuilder body = new MessageBuilder ();
        body.addInt (addr);
        body.addInt (count);
        body.addByte (data.length > 255 ? 0 : data.length);
        body.addData (data);
        ModbusTransaction trans = client.createTransaction (slaveId,
            Modbus.FUNC_WRITE_MULTIPLE_REGISTERS, body.getData (),
            new ModbusResponseHandler ()
            {
                public void handleResponse (ModbusTransaction trans)
                {
                    ModbusException exception = trans.getException ();
                    if (exception == null)
                    {
                        try
                        {
                            ModbusMessage response = trans.getResponse ();
                            if (strictChecking)
                            {
                                response.checkSize (4);
                                if (response.getInt (0) != addr)
                                {
                                    Modbus.dataError ("M109",
                                        "Wrong address in response: " +
                                        response.getInt (0) +
                                        " instead of " + addr);
                                }
                                if (response.getInt (2) != count)
                                {
                                    Modbus.dataError ("M110",
                                        "Wrong count in response: " +
                                        response.getInt (2) +
                                        " instead of " + count);
                                }
                            }
                        }
                        catch (ModbusException e)
                        {
                            exception = e;
                        }
                    }
                    informWaiter (waiter, exception);
                }
            });
        trans.setResponseTimeMonitor (responseTimer);
        trans.getRequest ().setTracing (tracing);
        client.handleTransaction (trans);
    }

    public void writeMultipleRegisters (int address, int nvalues)
        throws ModbusException, InterruptedException
    {
        Waiter waiter = new Waiter ();
        writeMultipleRegisters (address, nvalues, waiter);
        waiter.waitUntilFinished ();
    }

    /** Write from a single holding register */

    public void writeSingleRegister (int address, final ModbusWaiter waiter)
        throws ModbusException, InterruptedException
    {
        checkWaiter (waiter);
        AddressMap.Map space = model.getAddressMap ().getHoldingRegisterMap ();
        final int addr = model.toHoldingRegister (address);
        Tracer.Tracing tracing = new Tracer.Tracing ();
        byte [] data = model.pack (address, 1, space,
            tracing, slaveId, true);
        MessageBuilder body = new MessageBuilder ();
        body.addInt (addr);
        body.addData (data);
        ModbusTransaction trans = client.createTransaction (slaveId,
            Modbus.FUNC_WRITE_SINGLE_REGISTER, body.getData (),
            new ModbusResponseHandler ()
            {
                public void handleResponse (ModbusTransaction trans)
                {
                    ModbusException exception = trans.getException ();
                    if (exception == null)
                    {
                        try
                        {
                            ModbusMessage response = trans.getResponse ();
                            if (strictChecking)
                            {
                                response.checkSize (
                                    trans.getRequest ().size ());
                                if (response.getInt (0) != addr)
                                {
                                    Modbus.dataError ("M109",
                                        "Wrong address in response: " +
                                        response.getInt (0) +
                                        " instead of " + addr);
                                }
                                if (!Arrays.equals (
                                    trans.getRequest().getData (),
                                    response.getData ()))
                                {
                                    Modbus.dataError ("M111",
                                        "Wrong value in response");
                                }
                            }
                        }
                        catch (ModbusException e)
                        {
                            exception = e;
                        }
                    }
                    informWaiter (waiter, exception);
                }
            });
        trans.setResponseTimeMonitor (responseTimer);
        trans.getRequest ().setTracing (tracing);
        client.handleTransaction (trans);
    }

    public void writeSingleRegister (int address)
        throws ModbusException, InterruptedException
    {
        Waiter waiter = new Waiter ();
        writeSingleRegister (address, waiter);
        waiter.waitUntilFinished ();
    }

    public void maskWriteRegister (int address, long andMask, long orMask,
            final ModbusWaiter waiter)
        throws ModbusException, InterruptedException
    {
        checkWaiter (waiter);
        final int addr = model.toHoldingRegister (address);
        int size = model.getValueSize (address);
        byte [] data = new byte [size * 2];
        model.packValue (size, andMask, data, 0, 0);
        model.packValue (size, orMask, data, size, 0);  
        MessageBuilder body = new MessageBuilder ();
        body.addInt (addr);
        body.addData (data);
        ModbusTransaction trans = client.createTransaction (slaveId,
            Modbus.FUNC_MASK_WRITE_REGISTER, body.getData (),
            new ModbusResponseHandler ()
            {
                public void handleResponse (ModbusTransaction trans)
                {
                    ModbusException exception = trans.getException ();
                    if (exception == null)
                    {
                        try
                        {
                            ModbusMessage response = trans.getResponse ();
                            if (strictChecking)
                            {
                                response.checkSize (
                                    trans.getRequest ().size ());
                                if (response.getInt (0) != addr)
                                {
                                    Modbus.dataError ("M109",
                                        "Wrong address in response: " +
                                        response.getInt (0) +
                                        " instead of " + addr);
                                }
                                if (!Arrays.equals (
                                    trans.getRequest ().getData (),
                                    response.getData ()))
                                {
                                    Modbus.dataError ("M112",
                                        "Wrong masks in response");
                                }
                            }
                        }
                        catch (ModbusException e)
                        {
                            exception = e;
                        }
                    }
                    informWaiter (waiter, exception);
                }
            });
        trans.setResponseTimeMonitor (responseTimer);
        client.handleTransaction (trans);
    }

    public void maskWriteRegister (int address, long andMask, long orMask)
        throws ModbusException, InterruptedException
    {
        Waiter waiter = new Waiter ();
        maskWriteRegister (address, andMask, orMask, waiter);
        waiter.waitUntilFinished ();
    }

    public void readCoils (final int address, int nbits,
            final ModbusWaiter waiter)
        throws ModbusException, InterruptedException
    {
        checkWaiter (waiter);
        cantBroadcast ();
        if (strictChecking && nbits == 0)
        {
            reporter.warn ("M125", "Invalid read count (0)");
        }
        if (nbits > 65535)
        {
            Modbus.dataError ("M114",
                "Invalid read count (" + nbits + ") - will not fit in 16 bits");
        }
        final int nbytes = (nbits + 7) / 8;
        if (nbytes + 2 > maxPdu)
        {
            reporter.warn ("M103",
                "Invalid read count (" + nbits +
                ") - response would exceed max PDU size (" +
                maxPdu + " bytes)");
        }
        else if (!allowLongMessages && nbytes > 255)
        {
            reporter.warn ("M102",
                "Invalid read count (" + nbits +
                ") - response byte count (" + nbytes +
                ") would not fit in a byte");
        }
        else if (checkCountLimits && nbits > 2000)
        {
            reporter.warn ("M104",
                "Invalid read count (" + nbits +
                ") - exceeds count limit (2000)");
        }
        final AddressMap.Map space = model.getAddressMap ().getCoilMap ();
        int valueSize = model.getCoilValueSize ();
        int multiplier = valueSize == 0 ? 1 : valueSize * 8;
        int addr = model.toCoil (new ModelAddress (address, 0));
        final int nvalues = (nbits + multiplier - 1) / multiplier;
        int size = valueSize == 0 ? (nvalues + 7) / 8 : nvalues * valueSize;
        MessageBuilder body = new MessageBuilder ();
        body.addInt (addr);
        body.addInt (nbits);
        ModbusTransaction trans = client.createTransaction (slaveId,
            Modbus.FUNC_READ_COILS, body.getData (),
            new ModbusResponseHandler ()
            {
                public void handleResponse (ModbusTransaction trans)
                {
                    ModbusException exception = trans.getException ();
                    if (exception == null)
                    {
                        try
                        {
                            ModbusMessage response = trans.getResponse ();
                            checkSize (response, 1, nbytes);
                            if (strictChecking &&
                                response.getByte (0) != nbytes)
                            {
                                Modbus.dataError ("M101",
                                    "Wrong byte count in response: " +
                                    response.getByte (0) +
                                    " when expecting " + nbytes);
                            }
                            byte [] data = response.getData (1, nbytes);
                            if (model.bitReverseCoils ())
                                Bytes.reverseBits (data);
                            if (model.byteSwapCoils ())
                                Bytes.byteSwap (data);
                            model.unpack (data, address, nvalues,
                                space, strictChecking,
                                slaveId, false, response.getTracer ());
                        }
                        catch (ModbusException e)
                        {
                            exception = e;
                        }
                    }
                    informWaiter (waiter, exception);
                }
            });
        trans.setResponseTimeMonitor (responseTimer);
        client.handleTransaction (trans);
    }

    public void readCoils (int address, int nbits)
        throws ModbusException, InterruptedException
    {
        Waiter waiter = new Waiter ();
        readCoils (address, nbits, waiter);
        waiter.waitUntilFinished ();
    }

    public void readDiscreteInputs (final int address, int nbits,
            final ModbusWaiter waiter)
        throws ModbusException, InterruptedException
    {
        checkWaiter (waiter);
        cantBroadcast ();
        if (strictChecking && nbits == 0)
        {
            reporter.warn ("M125", "Invalid read count (0)");
        }
        if (nbits > 65535)
        {
            Modbus.dataError ("M114",
                "Invalid read count (" + nbits + ") - will not fit in 16 bits");
        }
        final AddressMap.Map space =
            model.getAddressMap ().getDiscreteInputMap ();
        int valueSize = model.getDiscreteInputValueSize ();
        final int nbytes = (nbits + 7) / 8;
        if (nbytes + 2 > maxPdu)
        {
            reporter.warn ("M103",
                "Invalid read count (" + nbits +
                ") - response would exceed max PDU size (" +
                maxPdu + " bytes)");
        }
        else if (!allowLongMessages && nbytes > 255)
        {
            reporter.warn ("M102",
                "Invalid read count (" + nbits +
                ") - response byte count (" + nbytes +
                ") would not fit in a byte");
        }
        else if (checkCountLimits && nbits > 2000)
        {
            reporter.warn ("M104",
                "Invalid read count (" + nbits +
                ") exceeds count limit (2000)");
        }
        int multiplier = valueSize == 0 ? 1 : valueSize * 8;
        int addr = model.toDiscreteInput (new ModelAddress (address, 0));
        final int nvalues = (nbits + multiplier - 1) / multiplier;
        int size = valueSize == 0 ? (nvalues + 7) / 8 : nvalues * valueSize;
        MessageBuilder body = new MessageBuilder ();
        body.addInt (addr);
        body.addInt (nbits);
        ModbusTransaction trans = client.createTransaction (slaveId,
            Modbus.FUNC_READ_DISCRETE_INPUTS, body.getData (),
            new ModbusResponseHandler ()
            {
                public void handleResponse (ModbusTransaction trans)
                {
                    ModbusException exception = trans.getException ();
                    if (exception == null)
                    {
                        try
                        {
                            ModbusMessage response = trans.getResponse ();
                            checkSize (response, 1, nbytes);
                            if (strictChecking &&
                                response.getByte (0) != nbytes)
                            {
                                Modbus.dataError ("M101",
                                    "Wrong byte count in response: " +
                                    response.getByte (0) +
                                    " when expecting " + nbytes);
                            }
                            byte [] data = response.getData (1, nbytes);
                            if (model.bitReverseDiscreteInputs ())
                                Bytes.reverseBits (data);
                            if (model.byteSwapDiscreteInputs ())
                                Bytes.byteSwap (data);
                            model.unpack (data, address, nvalues,
                                space, strictChecking,
                                slaveId, false, response.getTracer ());
                        }
                        catch (ModbusException e)
                        {
                            exception = e;
                        }
                    }
                    informWaiter (waiter, exception);
                }
            });
        trans.setResponseTimeMonitor (responseTimer);
        client.handleTransaction (trans);
    }

    public void readDiscreteInputs (int address, int nbits)
        throws ModbusException, InterruptedException
    {
        Waiter waiter = new Waiter ();
        readDiscreteInputs (address, nbits, waiter);
        waiter.waitUntilFinished ();
    }

    public void writeCoils (int address, final int nbits,
            final ModbusWaiter waiter)
        throws ModbusException, InterruptedException
    {
        checkWaiter (waiter);
        AddressMap.Map space = model.getAddressMap ().getCoilMap ();
        if (strictChecking && nbits == 0)
        {
            reporter.warn ("M126", "Invalid write count (0)");
        }
        if (nbits > 65535)
        {
            Modbus.dataError ("M115",
                "Invalid write count (" + nbits +
                ") - will not fit in 16 bits");
        }
        int valueSize = model.getCoilValueSize ();
        int nbytes = (nbits + 7) / 8;
        int multiplier = valueSize == 0 ? 1 : valueSize * 8;
        final int addr = model.toCoil (new ModelAddress (address, 0));
        int nvalues = (nbits + multiplier - 1) / multiplier;
        int size = valueSize == 0 ? (nvalues + 7) / 8 : nvalues * valueSize;
        Tracer.Tracing tracing = new Tracer.Tracing ();
        byte [] data = model.pack (address, nvalues, space,
            tracing, slaveId, true);
        assert data.length == nbytes;
        assert data.length == size;
        if (nbytes + 6 > maxPdu)
        {
            Modbus.dataError ("M106",
                "Invalid write count (" + nbits +
                ") - request would exceed max PDU size (" +
            maxPdu + " bytes)");
        }
        if (!allowLongMessages && nbytes > 255)
        {
            Modbus.dataError ("M105",
                "Invalid write count (" + nbits +
                ") - byte count (" + data.length +
                ") would not fit in a byte");
        }
        if (checkCountLimits && nbits > 1968)
        {
            reporter.warn ("M107",
                "Invalid write count (" + nbits +
                ") - exceeds count limit (1968)");
        }
        if (model.bitReverseCoils ())
            Bytes.reverseBits (data);
        if (model.byteSwapCoils ())
            Bytes.byteSwap (data);
        MessageBuilder body = new MessageBuilder ();
        body.addInt (addr);
        body.addInt (nbits);
        body.addByte (nbytes > 255 ? 0 : nbytes);
        body.addData (data);
        ModbusTransaction trans = client.createTransaction (slaveId,
            Modbus.FUNC_WRITE_MULTIPLE_COILS, body.getData (),
            new ModbusResponseHandler ()
            {
                public void handleResponse (ModbusTransaction trans)
                {
                    ModbusException exception = trans.getException ();
                    if (exception == null)
                    {
                        try
                        {
                            ModbusMessage response = trans.getResponse ();
                            if (strictChecking)
                            {
                                checkSize (response, 4, 0);
                                if (response.getInt (0) != addr)
                                {
                                    Modbus.dataError ("M109",
                                        "Wrong address in response: " +
                                        response.getInt (0) +
                                        " instead of " + addr);
                                }
                                if (response.getInt (2) != nbits)
                                {
                                    Modbus.dataError ("M110",
                                        "Wrong count in response: " +
                                        response.getInt (2) +
                                        " instead of " + nbits);
                                }
                            }
                        }
                        catch (ModbusException e)
                        {
                            exception = e;
                        }
                    }
                    informWaiter (waiter, exception);
                }
            });
        trans.setResponseTimeMonitor (responseTimer);
        trans.getRequest ().setTracing (tracing);
        client.handleTransaction (trans);
    }

    public void writeCoils (int address, int nbits)
        throws ModbusException, InterruptedException
    {
        Waiter waiter = new Waiter ();
        writeCoils (address, nbits, waiter);
        waiter.waitUntilFinished ();
    }

    public void writeCoil (int address, final ModbusWaiter waiter)
        throws ModbusException, InterruptedException
    {
        checkWaiter (waiter);
        AddressMap.Map space = model.getAddressMap ().getCoilMap ();
        int valueSize = model.getCoilValueSize ();
        int multiplier = valueSize == 0 ? 1 : valueSize * 8;
        int size = valueSize == 0 ? 1 : valueSize;
        final int addr = model.toCoil (new ModelAddress (address, 0));
        Tracer.Tracing tracing = new Tracer.Tracing ();
        byte [] data = model.pack (address, 1, space,
            tracing, slaveId, true);
        assert data.length == size;
        if (model.bitReverseCoils ())
            Bytes.reverseBits (data);
        if (model.byteSwapCoils ())
            Bytes.byteSwap (data);
        final int value = (data [0] & 1) != 0 ? 0xff00 : 0;
        MessageBuilder body = new MessageBuilder ();
        body.addInt (addr);
        body.addInt (value);
        ModbusTransaction trans = client.createTransaction (slaveId,
            Modbus.FUNC_WRITE_SINGLE_COIL, body.getData (),
            new ModbusResponseHandler ()
            {
                public void handleResponse (ModbusTransaction trans)
                {
                    ModbusException exception = trans.getException ();
                    if (exception == null)
                    {
                        try
                        {
                            ModbusMessage response = trans.getResponse ();
                            if (strictChecking)
                            {
                                checkSize (response, 4, 0);
                                if (response.getInt (0) != addr)
                                {
                                    Modbus.dataError ("M109",
                                        "Wrong address in response: " +
                                        response.getInt (0) +
                                        " instead of " + addr);
                                }
                                if (response.getInt (2) != value)
                                {
                                    Modbus.dataError ("M111",
                                        "Wrong value in response: " +
                                        response.getInt (2) +
                                        " instead of " + value);
                                }
                            }
                        }
                        catch (ModbusException e)
                        {
                            exception = e;
                        }
                    }
                    informWaiter (waiter, exception);
                }
            });
        trans.setResponseTimeMonitor (responseTimer);
        trans.getRequest ().setTracing (tracing);
        client.handleTransaction (trans);
    }

    public void writeCoil (int address)
        throws ModbusException, InterruptedException
    {
        Waiter waiter = new Waiter ();
        writeCoil (address, waiter);
        waiter.waitUntilFinished ();
    }

    public void readWriteRegisters (final int readAddress,
            final int readNValues,int writeAddress, int writeNValues,
            final ModbusWaiter waiter)
        throws ModbusException, InterruptedException
    {
        checkWaiter (waiter);
        cantBroadcast ();
        final AddressMap.Map space =
            model.getAddressMap ().getHoldingRegisterMap ();
        int readCount = model.getCount (readAddress, readNValues, space);
        if (strictChecking && readCount == 0)
        {
            reporter.warn ("M125", "Invalid read count (0)");
        }
        if (readCount > 65535)
        {
            Modbus.dataError ("M114",
                "Invalid read count (" + readCount +
                ") - will not fit in 16 bits");
        }
        final int readAddr = model.toHoldingRegister (readAddress);
        int writeCount = model.getCount (writeAddress, writeNValues, space);
        if (strictChecking && writeCount == 0)
        {
            reporter.warn ("M126", "Invalid write count (0)");
        }
        if (writeCount > 65535)
        {
            Modbus.dataError ("M115",
                "Invalid write count (" + writeCount +
                ") - will not fit in 16 bits");
        }
        int writeAddr = model.toHoldingRegister (writeAddress);
        Tracer.Tracing tracing = new Tracer.Tracing ();
        byte [] data = model.pack (writeAddress, writeNValues, space,
            tracing, slaveId, true);
        if (data.length + 10 > maxPdu)
        {
            Modbus.dataError ("M106",
                "Invalid write count (" + writeNValues +
                ") - request would exceed max PDU size (" +
                maxPdu + "bytes)");
        }
        if (!allowLongMessages && data.length > 255)
        {
            Modbus.dataError ("M105",
                "Invalid write count (" + writeNValues +
                ") - request byte count (" + data.length +
                ") would not fit in a byte");
        }
        if (checkCountLimits && data.length > 121 * 2)
        {
            reporter.warn ("M107",
                "Invalid write count (" + writeNValues +
                ") - exceeds count limit (121 * 2 bytes)");
        }
        final int readBytes =
            model.getTotalBytes (readAddress, readNValues, space);
        if (readBytes + 2 > maxPdu)
        {
            reporter.warn ("M103",
                "Invalid read count (" + readNValues +
                ") - response would exceed max PDU size (" +
                maxPdu + " bytes)");
        }
        else if (!allowLongMessages && readBytes > 255)
        {
            reporter.warn ("M102",
                "Invalid read count (" + readNValues +
                ") - response byte count (" + readBytes +
                ") would not fit in a byte");
        }
        else if (checkCountLimits && readBytes > 125 * 2)
        {
            reporter.warn ("M104",
                "Invalid read count (" + readNValues +
                ") - exceeds count limit (125 * 2 bytes)");
        }
        MessageBuilder body = new MessageBuilder ();
        body.addInt (readAddr);
        body.addInt (readCount);
        body.addInt (writeAddr);
        body.addInt (writeCount);
        body.addByte (data.length > 255 ? 0 : data.length);
        body.addData (data);
        ModbusTransaction trans = client.createTransaction (slaveId,
            Modbus.FUNC_READ_WRITE_MULTIPLE_REGISTERS, body.getData (),
            new ModbusResponseHandler ()
            {
                public void handleResponse (ModbusTransaction trans)
                {
                    ModbusException exception = trans.getException ();
                    if (exception == null)
                    {
                        try
                        {
                            ModbusMessage response = trans.getResponse ();
                            response.checkMinSize (1 + readBytes);
                            int byteCount =
                                checkSize (response, 1, response.getByte (0));
                            if (strictChecking && byteCount != readBytes)
                            {
                                Modbus.dataError ("M101",
                                    "Wrong byte count in response: " +
                                    byteCount + " when expecting " + readBytes);
                            }
                            byte [] data = response.getData (1, readBytes);
                            model.unpack (data, readAddress,
                                readNValues, space, strictChecking,
                                slaveId, false, response.getTracer ());
                        }
                        catch (ModbusException e)
                        {
                            exception = e;
                        }
                    }
                    informWaiter (waiter, exception);
                }
            });
        trans.setResponseTimeMonitor (responseTimer);
        trans.getRequest ().setTracing (tracing);
        client.handleTransaction (trans);
    }

    public void readWriteRegisters (int readAddress, int readNValues,
            int writeAddress, int writeNValues)
        throws ModbusException, InterruptedException
    {
        Waiter waiter = new Waiter ();
        readWriteRegisters (readAddress, readNValues,
            writeAddress, writeNValues, waiter);
        waiter.waitUntilFinished ();
    }

    public void readFileRecord (final ModbusWaiter waiter,
            final FileGroup... groups)
        throws ModbusException, InterruptedException
    {
        checkWaiter (waiter);
        cantBroadcast ();
        int totalBytes = 0;
        MessageBuilder body = new MessageBuilder ();
        body.addByte (0);       // place-holder for data length.

        for (FileGroup group : groups)
        {
            int count = filePacker.getCount (group.fileNum, group.address,
                group.nvalues);
            if (count > 65535)
            {
                Modbus.dataError ("M114",
                    "Invalid read count (" + count +
                    ") - will not fit in 16 bits");
            }
            body.addByte (6);
            body.addInt (group.fileNum);
            body.addInt (group.address);
            body.addInt (count);
            int nbytes = filePacker.getTotalBytes (group.fileNum, group.address,
                group.nvalues);
            totalBytes += 2 + nbytes;
        }
        byte [] data = body.getData ();
        data [0] = (byte) (data.length > 256 ? 0 : data.length - 1);
        if (!allowLongMessages && data.length > 256)
        {
            Modbus.dataError ("M105",
                "Invalid request - " +
                "byte count (" + (data.length - 1) +
                ") would not fit in a byte");
        }
        if (data.length + 1 > maxPdu)
        {
            Modbus.dataError ("M106",
                "Invalid request - would exceed max PDU size (" +
                maxPdu + " bytes)");
        }
        final int responseBytes = totalBytes;
        ModbusTransaction trans = client.createTransaction (slaveId,
            Modbus.FUNC_READ_FILE_RECORD, data,
            new ModbusResponseHandler ()
            {
                public void handleResponse (ModbusTransaction trans)
                {
                    ModbusException exception = trans.getException ();
                    if (exception == null)
                    {
                        try
                        {
                            ModbusMessage response = trans.getResponse ();
                            response.checkMinSize (1 + responseBytes);
                            int size = checkSize (response, 1,
                                response.getByte (0));
                            if (strictChecking && size != responseBytes)
                            {
                                Modbus.dataError ("M101",
                                    "Wrong byte count in response: " +
                                    size + " when expecting " + responseBytes);
                            }
                            int offset = 1;

                            for (FileGroup group : groups)
                            {
                                int nbytes = filePacker.getTotalBytes (
                                    group.fileNum, group.address,
                                    group.nvalues);
                                int len = response.getByte (offset);
                                if (len != nbytes + 1)
                                {
                                    Modbus.dataError ("M117",
                                        "Wrong sub-response length: " + len +
                                        " instead of " + nbytes);
                                }
                                int refType = response.getByte (offset + 1);
                                if (refType != 6)
                                {
                                    Modbus.dataError ("M116",
                                        "Incorrect reference type: " +
                                        refType + "instead of 6");
                                }
                                byte [] data = response.getData (offset + 2,
                                    nbytes);
                                filePacker.unpack (data,
                                    group.fileNum, group.address, group.nvalues,
                                    strictChecking, slaveId, false,
                                    response.getTracer ());
                                offset += 2 + nbytes;
                            }
                            if (strictChecking && offset < size)
                            {
                                Modbus.dataError ("M005", "Excess data in message: " + size +
                                    " bytes when expecting " + offset);
                            }
                        }
                        catch (ModbusException e)
                        {
                            exception = e;
                        }
                    }
                    informWaiter (waiter, exception);
                }
            });
        trans.setResponseTimeMonitor (responseTimer);
        client.handleTransaction (trans);
    }

    public void readFileRecord (FileGroup... groups)
        throws ModbusException, InterruptedException
    {
        Waiter waiter = new Waiter ();
        readFileRecord (waiter, groups);
        waiter.waitUntilFinished ();
    }

    public void writeFileRecord (final ModbusWaiter waiter, FileGroup... groups)
        throws ModbusException, InterruptedException
    {
        checkWaiter (waiter);
        Tracer.Tracing tracing = new Tracer.Tracing ();
        MessageBuilder body = new MessageBuilder ();
        body.addByte (0);       // place-holder for data length.

        for (FileGroup group : groups)
        {
            int count = filePacker.getCount (group.fileNum, group.address,
                group.nvalues);
            if (count > 65535)
            {
                Modbus.dataError ("M115",
                    "Invalid write count (" + count +
                    ") - will not fit in 16 bits");
            }
            byte [] data = filePacker.pack (group.fileNum, group.address,
                group.nvalues, tracing, slaveId, true);
            body.addByte (6);
            body.addInt (group.fileNum);
            body.addInt (group.address);
            body.addInt (count);
            body.addData (data);
        }

        byte [] data = body.getData ();
        data [0] = (byte) (data.length > 256 ? 0 : data.length - 1);
        if (!allowLongMessages && data.length > 256)
        {
            Modbus.dataError ("M105",
                "Invalid request - " +
                "byte count (" + (data.length - 1) +
                ") would not fit in a byte");
        }
        if (data.length + 1 > maxPdu)
        {
            Modbus.dataError ("M106",
                "Invalid request - would exceed max PDU size (" +
                maxPdu + " bytes)");
        }
        ModbusTransaction trans = client.createTransaction (slaveId,
            Modbus.FUNC_WRITE_FILE_RECORD, data,
            new ModbusResponseHandler ()
            {
                public void handleResponse (ModbusTransaction trans)
                {
                    ModbusException exception = trans.getException ();
                    if (exception == null)
                    {
                        try
                        {
                            ModbusMessage response = trans.getResponse ();
                            if (strictChecking)
                            {
                                if (!Arrays.equals (
                                    trans.getRequest().getData (),
                                    response.getData ()))
                                {
                                    Modbus.dataError ("M113",
                                        "Response is not echo of request");
                                }
                            }
                        }
                        catch (ModbusException e)
                        {
                            exception = e;
                        }
                    }
                    informWaiter (waiter, exception);
                }
            });
        trans.setResponseTimeMonitor (responseTimer);
        trans.getRequest ().setTracing (tracing);
        client.handleTransaction (trans);
    }

    public void writeFileRecord (FileGroup... groups)
        throws ModbusException, InterruptedException
    {
        Waiter waiter = new Waiter ();
        writeFileRecord (waiter, groups);
        waiter.waitUntilFinished ();
    }

    public void readFifoQueue (final int address, final ModbusWaiter waiter)
        throws ModbusException, InterruptedException
    {
        checkWaiter (waiter);
        cantBroadcast ();
        final AddressMap.Map space =
            model.getAddressMap ().getHoldingRegisterMap ();
        int addr = model.toHoldingRegister (address);
        MessageBuilder body = new MessageBuilder ();
        body.addInt (addr);
        ModbusTransaction trans = client.createTransaction (slaveId,
            Modbus.FUNC_READ_FIFO_QUEUE, body.getData (),
            new ModbusResponseHandler ()
            {
                public void handleResponse (ModbusTransaction trans)
                {
                    ModbusException exception = trans.getException ();
                    if (exception == null)
                    {
                        try
                        {
                            ModbusMessage response = trans.getResponse ();
                            response.checkMinSize (4);
                            int nbytes = response.getInt (0);
                            assert reporter.debug ("readFifoQueue: nbytes %d",
                                nbytes);
                            if (strictChecking)
                                response.checkSize (2 + nbytes);
                            else
                                response.checkMinSize (2 + nbytes);
                            if (nbytes < 2)
                            {
                                Modbus.dataError ("M121",
                                    "Byte count too small: " + nbytes +
                                    " when expecting at least 2");
                            }
                            byte [] data = response.getData (2, nbytes);
                            model.unpack (data, address, 1,
                                space, false, slaveId, false,
                                response.getTracer ());
                            long nvalues = model.getLongValue (address);
                            assert reporter.debug ("readFifoQueue: nvalues %d",
                                nvalues);
                            if (nvalues > 31)
                            {
                                Modbus.dataError ("M122",
                                    "Queue count exceeds 31: " + nvalues);
                            }
                            model.unpack (data, address, (int) nvalues + 1,
                                space, strictChecking,
                                slaveId, false, response.getTracer ());
                        }
                        catch (ModbusException e)
                        {
                            exception = e;
                        }
                    }
                    informWaiter (waiter, exception);
                }
            });
        trans.setResponseTimeMonitor (responseTimer);
        client.handleTransaction (trans);
    }

    public void readFifoQueue (int address)
        throws ModbusException, InterruptedException
    {
        Waiter waiter = new Waiter ();
        readFifoQueue (address, waiter);
        waiter.waitUntilFinished ();
    }

}



