All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.fhg.iee.bacnet.AbstractTransport Maven / Gradle / Ivy

The newest version!
/**
 * Copyright 2011-2018 Fraunhofer-Gesellschaft zur Förderung der angewandten Wissenschaften e.V.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package de.fhg.iee.bacnet;

import de.fhg.iee.bacnet.apdu.ApduConstants;
import de.fhg.iee.bacnet.apdu.ProtocolControlInformation;
import de.fhg.iee.bacnet.api.DeviceAddress;
import de.fhg.iee.bacnet.api.Indication;
import de.fhg.iee.bacnet.api.IndicationListener;
import de.fhg.iee.bacnet.api.Transport;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.BitSet;
import java.util.Collection;
import java.util.Iterator;
import java.util.PriorityQueue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Abstract Transport base class that handles invoke IDs and IndicationListeners
 * for concrete implementations. Implementations need to perform the link level
 * I/O through
 * {@link #sendData(java.nio.ByteBuffer, de.iwes.bacnet.api.Transport.Priority, boolean, de.iwes.bacnet.api.DeviceAddress) sendData}
 * and {@link #receivedPackage(de.iwes.bacnet.api.Indication) receivedPackage}.
 *
 * @author jlapp
 */
public abstract class AbstractTransport implements Transport {

    Collection> listeners = new ConcurrentLinkedQueue<>();
    final PriorityQueue pendingReplies = new PriorityQueue<>();
    InvokeIds invokeIds = new InvokeIds();

    final Logger logger = LoggerFactory.getLogger(getClass());
    final Thread timeoutThread;

    private long messageTimeout = 1000;
    private int messageRetries = 3;
    
    private final ThreadGroup executorThreads = new ThreadGroup("BACnet UDP transport executors");
    private final ExecutorService executor;

    public AbstractTransport() {
        timeoutThread = new Thread(timeoutHandler, getClass().getSimpleName() + " Timeout and Retry Handler");
        executor = Executors.newCachedThreadPool(new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread t = new Thread(executorThreads, r, AbstractTransport.this.getClass().getSimpleName() + " executor");
                return t;
            }
        });
    }

    private static class InvokeIds {

        BitSet ids = new BitSet(256);
        int lastId = 42;

        synchronized int getId() {
            if (ids.nextClearBit(0) > 255) {
                throw new IllegalStateException("out of invoke IDs");
            }
            do {
                lastId = (lastId + 1) % 256;
            } while (ids.get(lastId));
            ids.set(lastId);
            return lastId;
        }

        synchronized void release(int id) {
            ids.clear(id & 255);
        }

    }

    private class PendingReply implements Comparable {

        private final int invokeId;
        private final DeviceAddress destination;
        private final ByteBuffer data;
        private final Priority prio;
        private final IndicationListener listener;
        private final IndicationFuture f;
        private int tryNumber = 1;
        private volatile long expiryTime;

        public PendingReply(int invokeId, DeviceAddress destination, ByteBuffer data, Priority prio, IndicationListener listener, IndicationFuture f) {
            this.invokeId = invokeId;
            this.destination = destination;
            this.data = data.duplicate();
            this.prio = prio;
            this.listener = listener;
            this.f = f;
            expiryTime = System.currentTimeMillis() + messageTimeout;
        }

        public int getInvokeId() {
            return invokeId;
        }

        public IndicationListener getListener() {
            return listener;
        }

        public int getTryNumber() {
            return tryNumber;
        }

        public long getExpiryTime() {
            return expiryTime;
        }

        public void setTryNumber(int tryNumber) {
            this.tryNumber = tryNumber;
        }

        @Override
        public int compareTo(PendingReply o) {
            int cmpTimes = Long.compare(expiryTime, o.expiryTime);
            return cmpTimes;
        }

    }

    private final Runnable timeoutHandler = new Runnable() {

        @Override
        public void run() {
            while (!Thread.interrupted()) {
                synchronized (pendingReplies) {
                    if (pendingReplies.isEmpty()) {
                        try {
                            pendingReplies.wait();
                        } catch (InterruptedException ie) {
                            //shutdown...
                            break;
                        }
                        continue;
                    }
                    PendingReply next = pendingReplies.peek();
                    long now = System.currentTimeMillis();
                    if (now >= next.expiryTime) {
                        pendingReplies.remove();
                        if (next.tryNumber >= messageRetries) {
                            //TODO: notify listener
                            logger.warn("no reply from {} for invoke ID {}", next.destination, next.invokeId);
                        } else {
                            next.tryNumber++;
                            next.expiryTime = now + messageTimeout;
                            pendingReplies.offer(next);
                            try {
                                logger.trace("resending message to {} for invoke ID {}", next.destination, next.invokeId);
                                //TODO: blocking i/o ?
                                next.data.rewind();
                                sendData(next.data, next.prio, true, next.destination);
                            } catch (IOException ex) {
                                //TODO: logging
                                logger.warn("resending failed", ex);
                            }
                        }
                    }
                    try {
                        long wait = next.expiryTime - now;
                        if (wait > 0) {
                            pendingReplies.wait(wait);
                        }
                    } catch (InterruptedException ie) {
                        //shutdown...
                        break;
                    }
                }
            }
            logger.debug("Message timeout handler shutting down.");
        }
    };

    protected final boolean hasLocalInvokeId(ProtocolControlInformation pci) {
        int pduType = pci.getPduType();
        return pduType == ApduConstants.TYPE_COMPLEX_ACK || pduType == ApduConstants.TYPE_SIMPLE_ACK || pduType == ApduConstants.TYPE_SEGMENT_ACK || pduType == ApduConstants.TYPE_ERROR || pduType == ApduConstants.TYPE_REJECT || pduType == ApduConstants.TYPE_ABORT;
    }

    private void executeListener(final PendingReply r, final Indication i) {
        if (r.getListener() == null) {
            return;
        }
        Runnable listenerCall = new Runnable() {
            @Override
            public void run() {
                try {
                    Object v = r.getListener().event(i);
                    r.f.resolve(v, null);
                } catch (Throwable t) {
                    //logger.warn("exception in event listener", t);
                    r.f.resolve(null, t);
                }
            }
        };
        executor.execute(listenerCall);
    }

    protected final void receivedPackage(Indication indication) {
        boolean indicationHandled = false;
        ProtocolControlInformation pci = indication.getProtocolControlInfo();
        if (hasLocalInvokeId(pci)) {
            int invokeId = pci.getInvokeId();
            invokeIds.release(invokeId);
            logger.trace("received message from {} for invoke ID {}", indication.getSource(), invokeId);
            synchronized (pendingReplies) {
                Iterator it = pendingReplies.iterator();
                while (it.hasNext()) {
                    PendingReply r = it.next();
                    if (r.getInvokeId() == invokeId) {
                        it.remove();
                        indicationHandled = true;
                        Indication i = new DefaultIndication(indication);
                        executeListener(r, i);
                        break;
                    }
                }
                pendingReplies.notifyAll();
            }
        }
        if (!indicationHandled) {
            for (IndicationListener l : listeners) {
                //TODO: needs executor
                Indication i = new DefaultIndication(indication);
                l.event(i);
            }
        }
    }

    @Override
    public final void addListener(IndicationListener l) {
        listeners.add(l);
    }

    @Override
    public final void removeListener(IndicationListener l) {
        Iterator> it = listeners.iterator();
        while (it.hasNext()) {
            if (it.next() == l) {
                it.remove();
            }
        }
    }

    @Override
    public abstract DeviceAddress getLocalAddress();

    @Override
    public abstract DeviceAddress getBroadcastAddress();

    @Override
    public final  Future request(DeviceAddress destination, ByteBuffer data, Priority prio, boolean expectingReply, IndicationListener l) throws IOException {
        ProtocolControlInformation pci = new ProtocolControlInformation(data);
        IndicationFuture f = new IndicationFuture<>();
        if (pci.getPduType() == ApduConstants.TYPE_CONFIRMED_REQ) {
            int invokeId = invokeIds.getId();
            
            PendingReply r = new PendingReply(invokeId, destination, data, prio, l, f);
            synchronized (pendingReplies) {
                pendingReplies.add(r);
                pendingReplies.notifyAll();
            }
            pci = pci.withInvokeId(invokeId);
            data.rewind();
            pci.write(data);
        	logger.trace("Schedulding message {} bytes to {}, expReply:{} invoke:{}",data.limit(), destination, expectingReply,
        			invokeId);
        } else {
        	logger.trace("Schedulding unconfirmed message {} bytes to {}, expReply:{}",data.limit(), destination, expectingReply);
        }
        data.rewind();
        //sendData(data, prio, expectingReply, destination);
        executeSend(data, prio, expectingReply, destination);
        return f;
    }
    
    private class IndicationFuture implements Future {
        
        final AtomicBoolean cancelled = new AtomicBoolean(false);
        final CountDownLatch done = new CountDownLatch(1);
        volatile V result;
        volatile Throwable t;

        @Override
        public boolean cancel(boolean mayInterruptIfRunning) {
            cancelled.set(true);
            return result == null;
        }

        @Override
        public boolean isCancelled() {
            return cancelled.get();
        }

        @Override
        public boolean isDone() {
            return result != null;
        }
        
        private void resolve(Object result, Throwable t) {
            this.result = (V) result;
            this.t = t;
            done.countDown();
        }

        @Override
        public V get() throws InterruptedException, ExecutionException {
            done.await();
            if (t != null) {
                throw new ExecutionException(t);
            }
            return result;
        }

        @Override
        public V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
            done.await(timeout, unit);
            if (t != null) {
                throw new ExecutionException(t);
            }
            return result;
        }
        
    }

    private void executeSend(final ByteBuffer data, Priority prio, boolean expectingReply, DeviceAddress destination) {
        Runnable send = new Runnable() {
            @Override
            public void run() {
                try {
                	logger.trace("Sending out message {} bytes to {}, expReply:{}",data.limit(), destination, expectingReply);
                    sendData(data, prio, expectingReply, destination);
                } catch (IOException ex) {
                    logger.error("send failed for destination {}", destination, ex);
                }
            }
        };
        executor.execute(send);
    }

    /**
     * @param apdu a BACnet apdu, i.e. the application protocol control
     * information plus the actual service data.
     * @param prio BACnet message priority
     * @param expectingReply
     * @param destination
     * @throws java.io.IOException
     */
    protected abstract void sendData(ByteBuffer apdu, Priority prio, boolean expectingReply, DeviceAddress destination) throws IOException;

    @Override
    public final void close() throws IOException {
        timeoutThread.interrupt();
        executor.shutdown();
        doClose();
    }

    protected abstract void doClose() throws IOException;

    @Override
    public final AbstractTransport start() {
        timeoutThread.start();
        doStart();
        return this;
    }

    protected abstract void doStart();

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy