org.eclipse.scada.da.server.exporter.modbus.ModbusExport.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.scada.da.server.exporter.modbus.ModbusExport.java

Source

/*******************************************************************************
 * Copyright (c) 2013, 2014 IBH SYSTEMS GmbH and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBH SYSTEMS GmbH - initial API and implementation
 *     IBH SYSTEMS GmbH - more data types
 *******************************************************************************/
package org.eclipse.scada.da.server.exporter.modbus;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.ScheduledExecutorService;

import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.service.IoProcessor;
import org.apache.mina.core.session.IdleStatus;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.transport.socket.SocketAcceptor;
import org.apache.mina.transport.socket.nio.NioSession;
import org.apache.mina.transport.socket.nio.NioSocketAcceptor;
import org.eclipse.scada.da.server.common.DataItem;
import org.eclipse.scada.da.server.common.exporter.ObjectExporter;
import org.eclipse.scada.da.server.common.osgi.factory.ObjectPoolDataItemFactory;
import org.eclipse.scada.da.server.exporter.common.HiveSource;
import org.eclipse.scada.da.server.exporter.modbus.internal.InformationBean;
import org.eclipse.scada.da.server.exporter.modbus.internal.MemoryBlock;
import org.eclipse.scada.da.server.exporter.modbus.io.SourceDefinition;
import org.eclipse.scada.protocol.modbus.codec.ModbusSlaveProtocolFilter;
import org.eclipse.scada.protocol.modbus.codec.ModbusTcpDecoder;
import org.eclipse.scada.protocol.modbus.codec.ModbusTcpEncoder;
import org.eclipse.scada.protocol.modbus.message.BaseMessage;
import org.eclipse.scada.protocol.modbus.message.ErrorResponse;
import org.eclipse.scada.protocol.modbus.message.ReadRequest;
import org.eclipse.scada.protocol.modbus.message.ReadResponse;
import org.eclipse.scada.protocol.modbus.message.WriteMultiDataRequest;
import org.eclipse.scada.protocol.modbus.message.WriteMultiDataResponse;
import org.eclipse.scada.protocol.modbus.message.WriteSingleDataRequest;
import org.eclipse.scada.protocol.modbus.message.WriteSingleDataResponse;
import org.eclipse.scada.utils.osgi.pool.ManageableObjectPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public abstract class ModbusExport {
    private final static Logger logger = LoggerFactory.getLogger(ModbusExport.class);

    private MemoryBlock block;

    private final ScheduledExecutorService executor;

    private Properties properties;

    private final HiveSource hiveSource;

    private final IoProcessor<NioSession> processor;

    private SocketAcceptor acceptor;

    private SocketAddress currentAddress;

    private short slaveId;

    private Integer readTimeout;

    private final InformationBean info = new InformationBean();

    private ObjectExporter exporter;

    private final String logName;

    /**
     * Create a new modbus exporter
     *
     * @param executor
     *            the executor used for
     * @param processor
     *            the IO processor
     * @param hiveSource
     *            the source of the hive to export
     * @param itemFactory
     *            an optional item factory for publishing statistics
     * @param logName
     *            an optional name for logging
     */
    public ModbusExport(final ScheduledExecutorService executor, final IoProcessor<NioSession> processor,
            final HiveSource hiveSource, final ObjectPoolDataItemFactory itemFactory, final String logName) {
        this.executor = executor;
        this.hiveSource = hiveSource;
        this.processor = processor;
        this.logName = logName != null ? logName : toString();

        if (itemFactory != null) {
            this.exporter = new ObjectExporter(itemFactory, true, true);
            this.exporter.attachTarget(this.info);
        } else {
            this.exporter = null;
        }
    }

    /**
     * Create a new modbus exporter
     *
     * @param executor
     *            the executor used for
     * @param processor
     *            the IO processor
     * @param hiveSource
     *            the source of the hive to export
     * @param itemFactory
     *            an optional item factory for publishing statistics
     */
    public ModbusExport(final ScheduledExecutorService executor, final IoProcessor<NioSession> processor,
            final HiveSource hiveSource, final ObjectPoolDataItemFactory itemFactory) {
        this(executor, processor, hiveSource, itemFactory, null);
    }

    public ModbusExport(final String id, final ScheduledExecutorService executor,
            final IoProcessor<NioSession> processor, final HiveSource hiveSource,
            final ManageableObjectPool<DataItem> itemObjectPool) {
        this(executor, processor, hiveSource,
                new ObjectPoolDataItemFactory(executor, itemObjectPool,
                        String.format("org.eclipse.scada.da.server.exporter.modbus.export.%s.information.", id)), //$NON-NLS-1$
                "ModbusExporter/" + id);
    }

    public void dispose() {
        logger.debug("Disposing"); //$NON-NLS-1$

        if (this.exporter != null) {
            // the exporter also disposes the item factory
            this.exporter.dispose();
            this.exporter = null;
        }
        disposeAcceptor();
        if (this.block != null) {
            this.block.dispose();
            this.block = null;
        }
    }

    private void disposeAcceptor() {
        if (this.acceptor != null) {
            this.acceptor
                    .dispose(!Boolean.getBoolean("org.eclipse.scada.da.server.exporter.modbus.dontWaitDispose")); //$NON-NLS-1$
            this.acceptor = null;
        }
    }

    private void createAcceptor() {
        final NioSocketAcceptor acceptor = new NioSocketAcceptor(this.processor);
        try {
            acceptor.setReuseAddress(true);
            acceptor.setBacklog(
                    Integer.getInteger("org.eclipse.scada.da.server.exporter.modbus.acceptor.backlog", 5)); //$NON-NLS-1$

            final ModbusTcpEncoder encoder = new ModbusTcpEncoder();
            final ModbusTcpDecoder decoder = new ModbusTcpDecoder();
            acceptor.getFilterChain().addLast("modbusPdu", new ProtocolCodecFilter(encoder, decoder)); //$NON-NLS-1$
            acceptor.getFilterChain().addLast("modbus", new ModbusSlaveProtocolFilter()); //$NON-NLS-1$

            acceptor.setHandler(new IoHandlerAdapter() {
                @Override
                public void exceptionCaught(final IoSession session, final Throwable cause) throws Exception {
                    session.close(true);
                };

                @Override
                public void sessionOpened(final IoSession session) throws Exception {
                    logger.info("Session opened: {}", session); //$NON-NLS-1$
                    ModbusExport.this.info.incrementActiveSessions();
                    handleSessionOpened(session);
                };

                @Override
                public void sessionIdle(final IoSession session, final IdleStatus status) throws Exception {
                    logger.info("Session idle: {}", session); //$NON-NLS-1$
                    handleSessionIdle(session);
                };

                @Override
                public void sessionClosed(final IoSession session) throws Exception {
                    logger.info("Session closed: {}", session); //$NON-NLS-1$
                    ModbusExport.this.info.decrementActiveSessions();
                };

                @Override
                public void messageReceived(final IoSession session, final Object message) throws Exception {
                    handleMessageReceived(session, message);
                };
            });
            this.acceptor = acceptor;
            this.acceptor.bind(this.currentAddress);
        } catch (final Exception e) {
            logger.warn("Failed to create acceptor", e);
            this.acceptor.dispose();
            throw new RuntimeException(e);
        }
    }

    protected void setReadTimeout(final Integer readTimeout) {
        this.readTimeout = readTimeout;
    }

    protected void setSlaveId(final short slaveId) {
        logger.debug("Setting slave id: {}", slaveId); //$NON-NLS-1$
        this.slaveId = slaveId;
    }

    protected void setPort(final int port) throws IOException {
        final SocketAddress address = new InetSocketAddress(port);

        if (this.currentAddress == null || !this.currentAddress.equals(address)) {
            logger.info("Rebinding interface - {} to {}", this.currentAddress, address); //$NON-NLS-1$
            disposeAcceptor();
            this.currentAddress = address;
            createAcceptor();
        }
    }

    protected void setBlockConfiguration(final List<SourceDefinition> defs) {
        this.block.setConfiguration(defs);
    }

    protected void setProperties(final Properties properties) {
        if (this.block == null) {
            logger.debug("Create new block"); //$NON-NLS-1$
            this.block = new MemoryBlock(this.executor, this.hiveSource, properties, this.logName);
        } else if (!this.properties.equals(properties)) {
            // we do need to create a new hive session
            logger.debug("Re-create block"); //$NON-NLS-1$
            this.block.dispose();
            this.block = null;
            this.block = new MemoryBlock(this.executor, this.hiveSource, properties, this.logName);
        }
        this.properties = properties;
    }

    protected void handleSessionOpened(final IoSession session) {
        Integer idle = this.readTimeout;
        if (idle != null) {
            idle = idle / 1000;
            if (idle < 0) {
                idle = 1;
            }
            logger.debug("Setting read idle timeout: {} second(s)", idle); //$NON-NLS-1$
            session.getConfig().setIdleTime(IdleStatus.READER_IDLE, idle);
        }
    }

    protected void handleSessionIdle(final IoSession session) {
        logger.info("Closing session due to reader timeout");
        session.close(true);
    }

    protected void handleMessageReceived(final IoSession session, final Object message) {
        logger.trace("New message - message: {}, session: {}", message, session); //$NON-NLS-1$

        this.info.incrementMessagesReceived();

        if (!(message instanceof BaseMessage)) {
            return;
        }

        final BaseMessage baseMessage = (BaseMessage) message;
        if (baseMessage.getUnitIdentifier() != this.slaveId) {
            logger.trace("Invalid unit id - use: {}, them: {}", this.slaveId, baseMessage.getUnitIdentifier()); //$NON-NLS-1$
            // silently ignore
            return;
        }

        if (message instanceof ReadRequest) {
            this.info.incrementReadRequestReceived();
            handleRead(session, (ReadRequest) message);
        } else if (message instanceof WriteSingleDataRequest) {
            this.info.incrementWriteRequestSingleReceived();
            handleWrite(session, (WriteSingleDataRequest) message);
        } else if (message instanceof WriteMultiDataRequest) {
            this.info.incrementWriteRequestMultiReceived();
            handleWrite(session, (WriteMultiDataRequest) message);
        }
    }

    private void handleWrite(final IoSession session, final WriteMultiDataRequest message) {
        switch (message.getFunctionCode()) {
        case 16:
            writeRegister(session, message);
            break;
        default:
            logger.info("Function code {} is not implemented", message.getFunctionCode()); //$NON-NLS-1$
            sendReply(session, makeError(message, 0x01));
            break;
        }
    }

    private void handleWrite(final IoSession session, final WriteSingleDataRequest message) {
        switch (message.getFunctionCode()) {
        case 6:
            writeRegister(session, message);
            break;
        default:
            logger.info("Function code {} is not implemented", message.getFunctionCode()); //$NON-NLS-1$
            sendReply(session, makeError(message, 0x01));
            break;
        }
    }

    private void writeRegister(final IoSession session, final WriteSingleDataRequest message) {
        final IoBuffer buffer = IoBuffer.wrap(message.getData());
        final int rc = performWrite(message.getAddress(), buffer);

        if (rc != 0) {
            sendReply(session, makeError(message, rc));
            return;
        }

        final WriteSingleDataResponse response = new WriteSingleDataResponse(message.getTransactionId(),
                message.getUnitIdentifier(), message.getFunctionCode(), message.getAddress(), message.getData());
        sendReply(session, response);
    }

    private void writeRegister(final IoSession session, final WriteMultiDataRequest message) {
        final int rc = performWrite(message.getStartAddress(), IoBuffer.wrap(message.getData()));

        if (rc != 0) {
            sendReply(session, makeError(message, rc));
            return;
        }

        final WriteMultiDataResponse response = new WriteMultiDataResponse(message.getTransactionId(),
                message.getUnitIdentifier(), message.getFunctionCode(), message.getStartAddress(),
                message.getNumRegisters());
        sendReply(session, response);
    }

    private int performWrite(final int address, final IoBuffer buffer) {
        return this.block.write(address, buffer);
    }

    private void handleRead(final IoSession session, final ReadRequest message) {
        switch (message.getFunctionCode()) {
        case 3:
            this.info.incrementReadHoldingRequestReceived();
            readHoldingData(session, message);
            break;
        case 4:
            this.info.incrementReadInputRequestReceived();
            // for now we return holding data
            readHoldingData(session, message);
            break;
        default:
            logger.info("Function code {} is not implemented", message.getFunctionCode()); //$NON-NLS-1$
            sendReply(session, makeError(message, 0x01));
            break;
        }
    }

    protected void readHoldingData(final IoSession session, final ReadRequest message) {
        final int byteOffset = message.getStartAddress() * 2;
        final int byteLength = message.getQuantity() * 2;

        logger.debug("Reading - byteOffset: {}, byteLength: {}", byteOffset, byteLength); //$NON-NLS-1$

        if (message.getQuantity() < 0 || message.getQuantity() >= 0x7D) {
            logger.debug("Invalid quanity"); //$NON-NLS-1$
            sendReply(session, makeError(message, 0x02));
            return;
        }

        final IoBuffer data = this.block.readData(byteOffset, byteLength);
        if (data == null) {
            logger.debug("No data"); //$NON-NLS-1$
            sendReply(session, makeError(message, 0x04));
        } else {
            // reply data
            sendReply(session, makeData(message, data));
        }
    }

    protected Object makeData(final BaseMessage message, final IoBuffer data) {
        data.flip();

        logger.trace("Create data message - data: {}", data); //$NON-NLS-1$
        return new ReadResponse(message.getTransactionId(), message.getUnitIdentifier(), message.getFunctionCode(),
                data);
    }

    protected ErrorResponse makeError(final BaseMessage message, final int exceptionCode) {
        this.info.incrementErrorReplies();

        final byte functionCode = message.getFunctionCode();
        return new ErrorResponse(message.getTransactionId(), message.getUnitIdentifier(), functionCode,
                (byte) exceptionCode);
    }

    protected void sendReply(final IoSession session, final Object message) {
        logger.trace("Send reply - message: {}, session: {}", message, session); //$NON-NLS-1$
        session.write(message);
    }

}