org.eclipse.neoscada.protocol.iec60870.apci.MessageChannel.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.neoscada.protocol.iec60870.apci.MessageChannel.java

Source

/*******************************************************************************
 * Copyright (c) 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
 *******************************************************************************/
package org.eclipse.neoscada.protocol.iec60870.apci;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;

import org.eclipse.neoscada.protocol.iec60870.ProtocolOptions;
import org.eclipse.neoscada.protocol.iec60870.apci.UnnumberedControl.Function;
import org.eclipse.neoscada.protocol.iec60870.asdu.MessageManager;
import org.eclipse.neoscada.protocol.iec60870.asdu.message.DataTransmissionMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.handler.codec.DecoderException;
import io.netty.handler.codec.EncoderException;
import io.netty.util.ReferenceCountUtil;

public class MessageChannel extends ChannelDuplexHandler {
    private final static Logger logger = LoggerFactory.getLogger(MessageChannel.class);

    private ChannelHandlerContext ctx;

    private final ProtocolOptions options;

    private Timer timer1;

    private Timer timer2;

    private Timer timer3;

    private final AckBuffer ackBuffer;

    /**
     * Sequence number of next incoming packet
     */
    private int receiveCounter;

    /**
     * Sequence number of last acknowledged packet
     */
    private int ackSentCounter = -1;

    private final Queue<WriteEvent> messageBuffer = new LinkedList<>();

    private final MessageManager manager;

    private final List<MessageSource> sources = new LinkedList<>();

    private Runnable startTimers;

    private static class WriteEvent {
        private final ByteBuf msg;

        private final ChannelPromise promise;

        private final ChannelHandlerContext ctx;

        WriteEvent(final ChannelHandlerContext ctx, final ByteBuf msg, final ChannelPromise promise) {
            this.ctx = ctx;
            this.msg = msg;
            this.promise = promise;
        }
    }

    public MessageChannel(final ProtocolOptions options, final MessageManager manager) {
        this.options = options != null ? options : new ProtocolOptions.Builder().build();
        this.ackBuffer = new AckBuffer(options.getMaxUnacknowledged(), options.getMaxSequenceNumber());
        this.manager = manager;
    }

    @Override
    public void channelActive(final ChannelHandlerContext ctx) throws Exception {
        this.ctx = ctx;

        this.timer1 = new Timer(ctx, "T1", new TimerHandler() {
            @Override
            public void handleTimeout() {
                handleTimeout1();
            }
        });
        this.timer2 = new Timer(ctx, "T2", new TimerHandler() {
            @Override
            public void handleTimeout() {
                handleTimeout2();
            }
        });
        this.timer3 = new Timer(ctx, "T3", new TimerHandler() {
            @Override
            public void handleTimeout() {
                handleTimeout3();
            }
        });

        startTimers = new Runnable() {
            @Override
            public void run() {
                MessageChannel.this.timer1.start(MessageChannel.this.options.getTimeout1());
                MessageChannel.this.timer3.start(MessageChannel.this.options.getTimeout3());
            }
        };

        super.channelActive(ctx);
    }

    @Override
    public void channelInactive(final ChannelHandlerContext ctx) throws Exception {
        logger.info("Channel inactive");
        super.channelInactive(ctx);

        this.timer1.dispose();
        this.timer2.dispose();
        this.timer3.dispose();
    }

    protected void handleTimeout1() {
        // expiration of timer #1 -> close
        logger.warn("Closing connection due to timeout: {}", this.ctx);
        this.ctx.close();
    }

    protected void handleTimeout2() {
        sendSupervisory();
    }

    private void sendSupervisory() {
        synchronized (this) {
            if (this.ackSentCounter != this.receiveCounter) {
                this.ackSentCounter = this.receiveCounter;
                this.ctx.write(new Supervisory(this.receiveCounter));
            }
        }
        this.ctx.flush();
    }

    protected void handleTimeout3() {
        // expiration of timer #3 -> TESTFR
        sendTestAct();
    }

    @Override
    public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception {
        logger.trace("channelRead - message: {}, ctx: {}", msg, ctx);

        // we received something - restart t3
        this.timer3.restart(this.options.getTimeout3());

        if (msg instanceof InformationTransfer) {
            handleAck(ctx, ((InformationTransfer) msg).getReceiveSequenceNumber());
            handleInformationTransfer((InformationTransfer) msg);
        } else if (msg instanceof UnnumberedControl) {
            handleFunction(((UnnumberedControl) msg).getFunction());
        } else if (msg instanceof Supervisory) {
            handleAck(ctx, ((Supervisory) msg).getReceiveSequenceNumber());
        }
    }

    protected void handleInformationTransfer(final InformationTransfer msg) {
        synchronized (this) {
            final int nr = msg.getSendSequenceNumber();
            if (nr != this.receiveCounter) {
                throw new RuntimeException(
                        String.format("Sequence error - expected: %s, received: %s", this.receiveCounter, nr));
            }

            incrementReceiveCounter();

            if (this.receiveCounter - this.ackSentCounter >= this.options.getAcknowledgeWindow()) {
                // send S format right now
                this.timer2.stop(); // just in case the timer was already started
                sendSupervisory();
            } else {
                //  schedule transmission for later -> start timer #2
                this.timer2.start(this.options.getTimeout2());
            }
        }

        processInformationTransfer(this.ctx, msg);
    }

    private void processInformationTransfer(final ChannelHandlerContext ctx, final Object msg) {
        final List<Object> out = new LinkedList<>();

        logger.trace("Passing to manager: {}", msg);
        final ByteBuf errorData = this.manager.receiveMessage((InformationTransfer) msg, out);
        if (errorData != null) {
            logger.debug("Write error reply");
            writeMessageToChannel(ctx, errorData, null);
            ctx.flush();
        }

        for (final Object newMsg : out) {
            logger.trace("Passing message: {}", newMsg);
            ctx.fireChannelRead(newMsg);
        }
    }

    private void incrementReceiveCounter() {
        this.receiveCounter++;
        if (this.receiveCounter > this.options.getMaxSequenceNumber()) {
            logger.info("Reset receive counter");
            this.receiveCounter = 0;
        }
    }

    protected synchronized void handleAck(final ChannelHandlerContext ctx, final int receiveSequenceNumber) {
        logger.trace("Received ACK up to: {}", receiveSequenceNumber);

        // handle ack
        this.ackBuffer.gotAck(receiveSequenceNumber);

        // now try to flush messages from the buffer
        sendFromBuffer();
        // try to send from sources
        sendFromSources();

        ctx.flush();
    }

    /**
     * Send messages from the local message buffer<br/>
     * <p>
     * <em>Note:</em> This method does not flush the context
     * </p>
     */
    private void sendFromBuffer() {
        while (!this.ackBuffer.isFull() && !this.messageBuffer.isEmpty()) {
            final WriteEvent event = this.messageBuffer.poll();
            writeMessageToChannel(event.ctx, event.msg, event.promise);
            if (logger.isDebugEnabled()) {
                logger.debug("Sending message from buffer: {} remaining", this.messageBuffer.size());
            }
        }

        if (logger.isTraceEnabled()) {
            logger.trace("AckBuffer(full) : {}, messageBuffer(empty): {}", this.ackBuffer.isFull(),
                    this.messageBuffer.isEmpty());
        }
    }

    /**
     * Send messages from the message sources <br/>
     * <p>
     * <em>Note:</em> This method does not flush the context
     * </p>
     */
    private void sendFromSources() {
        if (this.ackBuffer.isFull()) {
            return;
        }

        // this method does not cycle through the buffers

        final Iterator<MessageSource> i = this.sources.iterator();

        source: while (i.hasNext() && !this.ackBuffer.isFull() /* check again [1] */ ) {
            final MessageSource source = i.next();
            logger.trace("Try source: {}", source);

            while (!this.ackBuffer.isFull()) {
                final Object msg = source.poll();
                logger.trace("Polled message: {}", msg);
                if (msg == null) {
                    continue source; // try next source
                }
                writeMessageToChannel(this.ctx, encode(this.ctx, msg), null);
            }
        }

        /*
         * [1] check again to prevent unnecessary iterations over the sources list
         */
    }

    private void sendTestAct() {
        logger.info("Request TESTFR: {}", this.ctx);
        this.timer1.start(this.options.getTimeout1());
        this.ctx.writeAndFlush(new UnnumberedControl(Function.TESTFR_ACT));
    }

    @Override
    public void write(final ChannelHandlerContext ctx, final Object msg, final ChannelPromise promise)
            throws Exception {
        logger.trace("Write {}", msg);
        synchronized (this) {
            if (msg instanceof DataTransmissionMessage) {
                switch ((DataTransmissionMessage) msg) {
                case REQUEST_START:
                    ctx.write(new UnnumberedControl(Function.STARTDT_ACT), promise);
                    break;
                case CONFIRM_START:
                    ctx.write(new UnnumberedControl(Function.STARTDT_CONFIRM), promise);
                    break;
                case REQUEST_STOP:
                    ctx.write(new UnnumberedControl(Function.STOPDT_ACT), promise);
                    break;
                case CONFIRM_STOP:
                    ctx.write(new UnnumberedControl(Function.STOPDT_CONFIRM), promise);
                    break;
                default:
                    throw new EncoderException(String.format("Unknown data transmission message: %s", msg));
                }
            } else if (msg == MessageSource.NOTIFY_TOKEN) {
                handleMessageSourceUpdates(ctx);
            } else {
                handleMessageWrite(ctx, msg, promise);
            }
        }
    }

    private synchronized void handleMessageSourceUpdates(final ChannelHandlerContext ctx) {
        if (this.ackBuffer.isFull()) {
            logger.trace("Received notify token but buffer is full");
            return;
        }

        /*
         * we can directly send from the sources since either is the message buffer
         * not empty, but then also the ackBuffer full. Which it is not at this point.
         *
         * Or the ackBuffer has room, so there won't be any message in the message buffer left.
         */

        sendFromSources();

        // we have to flush manually
        ctx.flush();
    }

    private void handleMessageWrite(final ChannelHandlerContext ctx, final Object msg,
            final ChannelPromise promise) {
        final ByteBuf data = encode(ctx, msg);

        if (data == null) {
            // ignore
            return;
        }

        // if the buffer is full
        if (this.ackBuffer.isFull()) {
            logger.trace("Store message for later transmission");
            // ... store now and re-try later
            this.messageBuffer.add(new WriteEvent(ctx, data, promise));
        } else {
            writeMessageToChannel(ctx, data, promise);
        }
    }

    private ByteBuf encode(final ChannelHandlerContext ctx, final Object msg) {
        ByteBuf buf = ctx.alloc().buffer(255);
        try {
            this.manager.encodeMessage(msg, buf);
            if (buf.isReadable()) {
                // copy away the reference so it does not get released
                final ByteBuf buf2 = buf;
                buf = null;
                return buf2;
            }
        } finally {
            ReferenceCountUtil.release(buf);
        }
        return null;
    }

    private void writeMessageToChannel(final ChannelHandlerContext ctx, final ByteBuf data,
            final ChannelPromise promise) {
        final int seq = this.ackBuffer.addMessage(data);

        if (promise == null) {
            ctx.write(new InformationTransfer(seq, this.receiveCounter, data));
        } else {
            ctx.write(new InformationTransfer(seq, this.receiveCounter, data), promise);
        }

        logger.trace("Enqueued message as {} : {}", seq, data);

        // we can stop timer #2 here ... will be restarted by receive
        this.timer2.stop();
    }

    private void handleFunction(final Function function) {
        logger.debug("Handle U-format function: {}", function);

        this.timer1.stop();
        this.timer3.restart(this.options.getTimeout3());

        switch (function) {
        case STARTDT_ACT:
            this.ctx.fireChannelRead(DataTransmissionMessage.REQUEST_START);
            return;
        case STOPDT_ACT:
            this.ctx.fireChannelRead(DataTransmissionMessage.REQUEST_STOP);
            return;
        case STARTDT_CONFIRM:
            this.ctx.fireChannelRead(DataTransmissionMessage.CONFIRM_START);
            return;
        case STOPDT_CONFIRM:
            this.ctx.fireChannelRead(DataTransmissionMessage.CONFIRM_STOP);
            return;
        case TESTFR_ACT:
            // simply reply with confirm
            this.ctx.writeAndFlush(new UnnumberedControl(Function.TESTFR_CONFIRM));
            return;
        case TESTFR_CONFIRM:
            // no-op
            return;
        default:
            throw new DecoderException(String.format("Cannot handle function: %s" + function));
        }
    }

    public synchronized void addSource(final MessageSource messageSource) {
        this.sources.add(messageSource);
    }

    public void startTimers() {
        if (startTimers != null) {
            startTimers.run();
        }
    }
}