org.openmuc.j62056.Connection.java Source code

Java tutorial

Introduction

Here is the source code for org.openmuc.j62056.Connection.java

Source

/*
 * Copyright 2013-14 Fraunhofer ISE
 *
 * This file is part of j62056.
 * For more information visit http://www.openmuc.org
 *
 * j62056 is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 2.1 of the License, or
 * (at your option) any later version.
 *
 * j62056 is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with j62056.  If not, see <http://www.gnu.org/licenses/>.
 *
 */
package org.openmuc.j62056;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeoutException;

import gnu.io.CommPortIdentifier;
import gnu.io.NoSuchPortException;
import gnu.io.PortInUseException;
import gnu.io.RXTXPort;
import gnu.io.SerialPort;
import gnu.io.UnsupportedCommOperationException;

import org.apache.commons.codec.binary.Hex;

public class Connection {

    private final String serialPortName;
    private SerialPort serialPort;

    private final byte[] initMessage;
    private final boolean handleEcho;
    private final int baudRateChangeDelay;
    private int timeout = 5000;

    private DataOutputStream os;
    private DataInputStream is;

    private static char[] hexArray = "0123456789ABCDEF".toCharArray();
    private static int[] baudRates = new int[] { 300, 600, 1200, 2400, 4800, 9600, 19200 };

    private static final byte[] REQUEST_MESSAGE = new byte[] { (byte) 0x2F, (byte) 0x3F, (byte) 0x21, (byte) 0x0D,
            (byte) 0x0A };

    private static final byte[] ACKNOWLEDGE = new byte[] { (byte) 0x06, (byte) 0x30, (byte) 0x30, (byte) 0x30,
            (byte) 0x0D, (byte) 0x0A };

    private static final int INPUT_BUFFER_LENGTH = 1024;
    private byte[] buffer = new byte[INPUT_BUFFER_LENGTH];

    private static final Charset charset = Charset.forName("US-ASCII");

    private static final int SLEEP_INTERVAL = 100;

    /**
     * Creates a Connection object. You must call <code>open()</code> before
     * calling <code>read()</code> in order to read data. The timeout is set by
     * default to 5s.
     * 
     * @param serialPort
     *            examples for serial port identifiers are on Linux "/dev/ttyS0"
     *            or "/dev/ttyUSB0" and on Windows "COM1"
     * @param initMessage
     *              extra pre init bytes
     * @param handleEcho
     *            tells the connection to throw away echos of outgoing messages.
     *            Echos are caused by some optical transceivers.
     * @param baudRateChangeDelay
     *            tells the connection the time in ms to wait before changing
     *            the baud rate during message exchange. This parameter can
     *            usually be set to zero for regular serial ports. If a USB to
     *            serial converter is used, you might have to use a delay of
     *            around 250ms because otherwise the baud rate is changed before
     *            the previous message (i.e. the acknowledgment) has been
     *            completely sent.
     */
    public Connection(String serialPort, byte[] initMessage, boolean handleEcho, int baudRateChangeDelay) {
        if (serialPort == null) {
            throw new IllegalArgumentException("serialPort may not be NULL");
        }

        serialPortName = serialPort;
        this.initMessage = initMessage;
        this.handleEcho = handleEcho;
        this.baudRateChangeDelay = baudRateChangeDelay;
    }

    /**
     * Creates a Connection object. The option handleEcho is set to false and
     * the baudRateChangeDelay is set to 0.
     * 
     * @param serialPort
     *            examples for serial port identifiers on Linux are "/dev/ttyS0"
     *            or "/dev/ttyUSB0" and on Windows "COM1"
     */
    public Connection(String serialPort) {
        this(serialPort, null, false, 0);
    }

    /**
     * Sets the maximum time in ms to wait for new data from the remote device.
     * A timeout of zero is interpreted as an infinite timeout.
     * 
     * @param timeout
     *            the maximum time in ms to wait for new data.
     */
    public void setTimeout(int timeout) {
        this.timeout = timeout;
    }

    /**
     * Returns the timeout in ms.
     * 
     * @return the timeout in ms.
     */
    public int getTimeout() {
        return timeout;
    }

    /**
     * Opens the serial port associated with this connection.
     * 
     * @throws IOException
     *             if any kind of error occurs opening the serial port.
     */
    public void open() throws IOException {

        CommPortIdentifier portIdentifier;
        try {
            portIdentifier = CommPortIdentifier.getPortIdentifier(serialPortName);
        } catch (NoSuchPortException e) {
            throw new IOException("Serial port with given name does not exist", e);
        }

        if (portIdentifier.isCurrentlyOwned()) {
            throw new IOException("Serial port is currently in use.");
        }

        // fixed issue as rxtx library originally used in j62056 does use
        // different version of rxtx
        // com port in their version is using gnu.io.CommPort
        RXTXPort commPort;
        try {
            commPort = portIdentifier.open(this.getClass().getName(), 2000);
        } catch (PortInUseException e) {
            throw new IOException("Serial port is currently in use.", e);
        }

        if (!(commPort instanceof SerialPort)) {
            commPort.close();
            throw new IOException("The specified CommPort is not a serial port");
        }

        serialPort = commPort;

        try {
            os = new DataOutputStream(serialPort.getOutputStream());
            is = new DataInputStream(serialPort.getInputStream());
        } catch (IOException e) {
            serialPort.close();
            serialPort = null;
            throw new IOException("Error getting input or output or input stream from serial port", e);
        }

    }

    /**
     * Closes the serial port.
     */
    public void close() {
        if (serialPort == null) {
            return;
        }
        serialPort.close();
        serialPort = null;
    }

    /**
     * Requests a data message from the remote device using IEC 62056-21 Mode C.
     * The data message received is parsed and a list of data sets is returned.
     * 
     * @return A list of data sets contained in the data message response from
     *         the remote device. The first data set will contain the
     *         "identification" of the meter as the id and empty strings for
     *         value and unit.
     * @throws IOException
     *             if any kind of error other than timeout occurs while trying
     *             to read the remote device. Note that the connection is not
     *             closed when an IOException is thrown.
     * @throws TimeoutException
     *             if no response at all (not even a single byte) was received
     *             from the meter within the timeout span.
     */
    public List<DataSet> read() throws IOException, TimeoutException {

        if (serialPort == null) {
            throw new IllegalStateException("Connection is not open.");
        }

        try {
            serialPort.setSerialPortParams(300, SerialPort.DATABITS_7, SerialPort.STOPBITS_1,
                    SerialPort.PARITY_EVEN);
        } catch (UnsupportedCommOperationException e) {
            throw new IOException("Unable to set the given serial comm parameters", e);
        }
        if (initMessage != null) {
            os.write(initMessage);
            os.flush();
        }

        os.write(REQUEST_MESSAGE);
        os.flush();
        int baudRateSetting;
        char protocolMode;
        int baudRate;

        boolean readSuccessful = false;
        int timeval = 0;
        int numBytesReadTotal = 0;

        while (timeout == 0 || timeval < timeout) {
            if (is.available() > 0) {

                int numBytesRead = is.read(buffer, numBytesReadTotal, INPUT_BUFFER_LENGTH - numBytesReadTotal);
                numBytesReadTotal += numBytesRead;

                if (numBytesRead > 0) {
                    timeval = 0;
                }

                if (((handleEcho && numBytesReadTotal > 11) || (!handleEcho && numBytesReadTotal > 6))
                        && buffer[numBytesReadTotal - 2] == 0x0D && buffer[numBytesReadTotal - 1] == 0x0A) {
                    readSuccessful = true;
                    break;
                }
            }

            try {
                Thread.sleep(SLEEP_INTERVAL);
            } catch (InterruptedException e) {
            }

            timeval += SLEEP_INTERVAL;
        }

        int offset = 0;
        if (handleEcho) {
            offset = 5;
        }

        if (numBytesReadTotal == offset) {
            throw new TimeoutException();
        }

        if (!readSuccessful) {
            throw new IOException("Timeout while listening for Identification Message");
        }

        // System.out
        // .println("Got the following identification message: " +
        // getByteArrayString(buffer, numBytesReadTotal));

        int startPosition = 0;
        while (startPosition < numBytesReadTotal) {
            if (buffer[startPosition] == (byte) 0x2F) {

                if (buffer[startPosition + 1] == (byte) 0x00 | buffer[startPosition + 1] == (byte) 0x7F) {
                    startPosition++;
                } else {
                    break;
                }
                continue;
            }
            startPosition++;
        }

        byte[] bytes = new byte[numBytesReadTotal - startPosition];
        System.arraycopy(buffer, startPosition, bytes, 0, numBytesReadTotal - startPosition);

        /* baudrate identification http://manuals.lian98.biz/doc.en/html/u_iec62056_struct.htm
        * Protocol mode A : no baudrate change 
        * Protocol mode B : char from A to I; no acknowledgement
        * Protocol mode C : char from 0 to 9; with acknowledgement
        */
        baudRateSetting = bytes[offset + 4];
        if (baudRateSetting >= 0x41 && baudRateSetting <= 0x49) {
            protocolMode = 'B';//Mode B
            baudRate = baudRates[baudRateSetting - 0x40];//search with index (start with baudrate 600)
        } else if (baudRateSetting >= 0x30 && baudRateSetting <= 0x39) {
            protocolMode = 'C';//Mode C or E
            baudRate = baudRates[baudRateSetting - 0x30];//search with index
        } else if (baudRateSetting >= 0x32) {
            protocolMode = 'D';//Mode D no baudrate change
            baudRate = -1;
        } else {
            protocolMode = 'A';//Mode A no baudrate change
            baudRate = -1;
        }

        String identification = new String(buffer, offset + 5, numBytesReadTotal - offset - 7, charset);

        if (protocolMode == 'B' || protocolMode == 'C') {
            if (baudRate == -1) {
                throw new IOException(
                        "Syntax error in identification message received: unknown baud rate received: (hex: "
                                + Hex.encodeHexString(bytes) + ")");
            }
        }

        if (protocolMode == 'C') {
            ACKNOWLEDGE[2] = (byte) baudRateSetting;

            os.write(ACKNOWLEDGE);
            os.flush();
        }

        if (handleEcho) {
            readSuccessful = false;
            numBytesReadTotal = 0;
            while (timeout == 0 || timeval < timeout) {
                if (is.available() > 0) {
                    int numBytesRead = is.read(buffer, numBytesReadTotal, ACKNOWLEDGE.length - numBytesReadTotal);
                    numBytesReadTotal += numBytesRead;

                    if (numBytesRead > 0) {
                        timeval = 0;
                    }

                    if (numBytesReadTotal == ACKNOWLEDGE.length) {
                        readSuccessful = true;
                        break;
                    }
                }

                try {
                    Thread.sleep(SLEEP_INTERVAL);
                } catch (InterruptedException e) {
                }

                timeval += SLEEP_INTERVAL;
            }

            // if (readSuccessful) {
            // System.out.println("Received the following echo of the acknowledge successfully : "
            // + getByteArrayString(buffer, numBytesReadTotal));
            // }
            // else {
            // System.out.println("Receiving full echo failed.");
            // }

        }

        if (baudRateChangeDelay > 0) {
            try {
                Thread.sleep(baudRateChangeDelay);
            } catch (InterruptedException e1) {
            }
        }

        try {
            serialPort.setSerialPortParams(baudRate, SerialPort.DATABITS_7, SerialPort.STOPBITS_1,
                    SerialPort.PARITY_EVEN);
        } catch (UnsupportedCommOperationException e) {
            throw new IOException("Serial Port does not support " + baudRate + "bd 7E1");
        }

        readSuccessful = false;
        numBytesReadTotal = 0;
        int etxIndex = -1;

        while (timeout == 0 || timeval < timeout) {
            int available = is.available();
            if (available > 0) {
                if (buffer.length < (numBytesReadTotal + available)) {
                    //grow buffer
                    buffer = Arrays.copyOf(buffer, numBytesReadTotal + available);
                }
                int numBytesRead = is.read(buffer, numBytesReadTotal, available);
                numBytesReadTotal += numBytesRead;

                if (numBytesRead > 0) {
                    timeval = 0;
                }

                if ((etxIndex = findEtx(buffer, numBytesRead, numBytesReadTotal)) != -1
                        || (numBytesReadTotal > 4 && buffer[numBytesReadTotal - 3] == 0x21)
                        || (numBytesReadTotal > 7 && buffer[numBytesReadTotal - 5] == 0x21)) {
                    readSuccessful = true;
                    break;
                }
            }

            try {
                Thread.sleep(SLEEP_INTERVAL);
            } catch (InterruptedException e) {
            }

            timeval += SLEEP_INTERVAL;
        }

        if (!readSuccessful) {
            throw new IOException("Timeout while listening for Data Message");
        }

        // System.out.println("Got the following data message: " +
        // getByteArrayString(buffer, numBytesReadTotal));

        int index;
        int endIndex;
        // if 2nd last character is ETX
        if (etxIndex != -1) {
            if (numBytesReadTotal < 8) {
                throw new IOException("Data message does not have minimum length of 8.");
            }
            index = 1;
            endIndex = etxIndex - 3;
            if (buffer[0] != 0x02) {

                startPosition = 0;
                while (startPosition < numBytesReadTotal) {
                    if (buffer[startPosition] == (byte) 0x02) {
                        break;
                    } else {
                        if (startPosition < numBytesReadTotal - 2) {
                            startPosition++;
                        } else {
                            throw new IOException(
                                    "STX (0x02) character is expected but not received as first byte of data message. (hex: "
                                            + Hex.encodeHexString(buffer) + ")");
                        }
                    }
                }
            }
        } else {
            if (numBytesReadTotal < 5) {
                throw new IOException("Data message does not have minimum length of 5.");
            }
            index = 0;
            endIndex = numBytesReadTotal - 3;
        }

        if (buffer[endIndex + 1] != 0x0D) {
            throw new IOException(
                    "CR (0x0D) character is expected but not received after data block of data message. (hex: "
                            + Hex.encodeHexString(buffer) + ")");
        }

        if (buffer[endIndex + 2] != 0x0A) {
            throw new IOException(
                    "LF (0x0A) character is expected but not received after data block of data message. (hex: "
                            + Hex.encodeHexString(buffer) + ")");
        }

        List<DataSet> dataSets = new ArrayList<DataSet>();
        dataSets.add(new DataSet(identification, "", ""));

        while (index != endIndex) {
            String id = null;

            for (int i = index; i < endIndex - 1; i++) {
                if (buffer[i] == 0x28) {
                    // found '('
                    id = new String(buffer, index, i - index, charset);
                    index = i + 1;
                    break;
                }
            }
            if (id == null) {
                throw new IOException(
                        "'(' (0x28) character is expected but not received inside data block of data message. (hex: "
                                + Hex.encodeHexString(buffer) + ")");
            }

            String value = "";
            String unit = "";
            for (int i = index; i < endIndex; i++) {
                if (buffer[i] == 0x2A) {
                    // found '*'
                    if (i > index) {
                        value = new String(buffer, index, i - index, charset);
                    }
                    index = i + 1;

                    for (int j = index; j < endIndex; j++) {
                        if (buffer[j] == 0x29) {
                            // found ')'
                            unit = new String(buffer, index, j - index, charset);
                            index = j + 1;
                            break;
                        }
                    }

                    break;
                } else if (buffer[i] == 0x29) {
                    // found ')'
                    if (i > index) {
                        value = new String(buffer, index, i - index, charset);
                    }
                    index = i + 1;
                    break;
                }
            }
            if (buffer[index - 1] != 0x29) {
                throw new IOException(
                        "')' (0x29) character is expected but not received inside data block of data message. (hex: "
                                + Hex.encodeHexString(buffer) + ")");
            }

            dataSets.add(new DataSet(id, value, unit));

            if (buffer[index] == 0x0d && buffer[index + 1] == 0x0a) {
                index += 2;
            }

        }
        return dataSets;

    }

    /**
     * Returns the baud rate chosen by the server for this communication
     * 
     * @param baudCharacter
     *            Encoded baud rate (see 6.3.14 item 13c)
     * @return The chosen baud rate or -1 on error
     */
    private int getBaudRate(int baudCharacter) {
        int result = -1;
        switch (baudCharacter) {
        case 0x30:
            result = 300;
            break;
        case 0x31:
            result = 600;
            break;
        case 0x32:
            result = 1200;
            break;
        case 0x33:
            result = 2400;
            break;
        case 0x34:
            result = 4800;
            break;
        case 0x35:
            result = 9600;
            break;
        case 0x36:
            result = 19200;
            break;
        }
        return result;
    }

    private int findEtx(byte[] buffer, int numBytesRead, int numBytesReadTotal) {
        for (int i = numBytesReadTotal - 1; i > numBytesRead; i--) {
            if (buffer[i] == 0x03) {
                return i;
            }
        }
        return -1;
    }
}